Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M .gitignore
22
*.bin
33
*.exe
4
/*.jar
45
build
56
.gradle
67
contacted.csv
78
video
89
.settings
910
.classpath
11
.idea
1012
D .idea/.gitignore
1
workspace.xml
21
D .idea/codeStyles/codeStyleConfig.xml
1
<component name="ProjectCodeStyleConfiguration">
2
  <state>
3
    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
4
  </state>
5
</component>
1
D .idea/compiler.xml
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="CompilerConfiguration">
4
    <bytecodeTargetLevel target="14" />
5
  </component>
6
</project>
1
D .idea/dictionaries/jarvisd.xml
1
<component name="ProjectDictionaryState">
2
  <dictionary name="jarvisd">
3
    <words>
4
      <w>blockquotes</w>
5
      <w>sigil</w>
6
      <w>transcoded</w>
7
    </words>
8
  </dictionary>
9
</component>
1
D .idea/gradle.xml
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>
1
D .idea/jarRepositories.xml
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>
1
D .idea/misc.xml
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_14" default="true" project-jdk-name="14" project-jdk-type="JavaSDK">
5
    <output url="file://$PROJECT_DIR$/build" />
6
  </component>
7
</project>
1
D .idea/rGraphicsSettings.xml
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>
1
D .idea/rSettings.xml
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>
1
D .idea/rpackages.xml
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>
1
D .idea/scrivenvar.iml
1
<?xml version="1.0" encoding="UTF-8"?>
2
<module type="JAVA_MODULE" version="4" />
1
D .idea/uiDesigner.xml
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>
1
D .idea/vcs.xml
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>
1
D .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
161
M README.md
1717
### Windows
1818
19
On Windows, double-click the application to start. You will have to give the application permission to run.
19
Double-click the application to start; give the application permission to run.
2020
21
When upgrading to a new version, delete the following directory;
21
When upgrading to a new version, delete the following directory:
2222
2323
    C:\Users\%USERNAME%\AppData\Local\warp\packages\keenwrite.exe
2424
2525
### Linux
2626
27
On Linux, run `chmod +x keenwrite.bin` then `./keenwrite.bin`.
27
Execute the following commands in a terminal:
28
29
``` bash
30
chmod +x keenwrite.bin
31
./keenwrite.bin
32
```
2833
2934
### Other
3035
31
On other platforms, download and install a full version of [OpenJDK 14](https://bell-sw.com/pages/downloads/?version=java-14#mn) that includes JavaFX module support, then run:
36
Download and install a full version of [OpenJDK 15](https://bell-sw.com/pages/downloads/?version=java-15#mn) that includes JavaFX module support, then run:
3237
3338
``` bash
3439
java -jar keenwrite.jar
3540
```
3641
3742
## Features
43
44
The application offers:
3845
3946
* User-defined interpolated strings
40
* Real-time preview with variable substitution
4147
* Auto-complete variable names based on variable values
42
* XML document transformation using XSLT3 or older
43
* Platform independent (Windows, Linux, MacOS)
44
* Spellcheck while typing
45
* Write mathematical formulas using a subset of TeX
48
* Real-time spell check
49
* Real-time rendering of math using TeX notation
50
* Diagrams: Mermaid, GraphViz, UML, sequence, timing, DITAA, and more!
4651
* R integration
52
* XML transformation using XSLT3 or older
53
* Customizable GUI having detachable tabs
54
* Platform independent (Windows, Linux, MacOS)
4755
4856
## Usage
...
5866
5967
This software is licensed under the [BSD 2-Clause License](LICENSE.md) and
60
derived heavily from [Markdown-Writer-FX](licenses/MARKDOWN-WRITER-FX.md).
68
based on [Markdown-Writer-FX](licenses/MARKDOWN-WRITER-FX.md).
6169
6270
A README.zh-CN.md
1
# ![Logo](docs/images/app-title.zh-CN.png)
2
3
智能写入是一个文本编辑器,它使用插值字符串引用外部定义的值。
4
5
## 下载
6
7
下载以下版本之一:
8
9
* [Windows](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.exe)
10
* [Linux](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.bin)
11
* [Java Archive](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.jar)
12
13
## 跑
14
15
在第一次运行期间,应用程序将自身解压到本地目录中。随后的启动会更快。
16
17
### Windows
18
19
双击应用程序以启动。您必须授予应用程序运行权限。 
20
21
升级时,删除以下目录:
22
23
    C:\Users\%USERNAME%\AppData\Local\warp\packages\keenwrite.exe
24
25
### Linux
26
27
执行以下命令:
28
29
``` bash
30
chmod +x keenwrite.bin
31
./keenwrite.bin
32
```
33
34
### Other
35
36
Download and install a full version of [OpenJDK 15](https://bell-sw.com/pages/downloads/?version=java-15#mn) that includes JavaFX module support, then run:
37
38
``` bash
39
java -jar keenwrite.jar
40
```
41
42
## 特征
43
44
* 用户定义的插值字符串
45
* 带变量替换的实时预览
46
* 基于变量值自动完成变量名
47
* 使用XSLT3或更早版本的XML文档转换
48
* 独立于操作系统
49
* 打字时拼写检查
50
* 使用TeX的子集编写数学公式
51
* 嵌入R语句
52
53
## 软件使用
54
55
See the [detailed documentation](docs/README.md) for information about
56
using the application.
57
58
## 截图
59
60
![Screenshot with Formulas](docs/images/equations.png)
61
62
## 软件许可证
63
64
This software is licensed under the [BSD 2-Clause License](LICENSE.md) and
65
based on [Markdown-Writer-FX](licenses/MARKDOWN-WRITER-FX.md).
66
167
M build.gradle
3030
3131
javafx {
32
  version = "14"
32
  version = "15"
3333
  modules = ['javafx.controls', 'javafx.swing']
3434
  configuration = 'compileOnly'
3535
}
3636
3737
dependencies {
38
  def v_junit = '5.4.2'
38
  def v_junit = '5.5.1'
3939
  def v_flexmark = '0.62.2'
40
  def v_jackson = '2.11.2'
40
  def v_jackson = '2.12.0'
4141
  def v_batik = '1.13'
4242
4343
  // JavaFX
44
  implementation 'org.reactfx:reactfx:1.4.1'
45
  implementation 'org.controlsfx:controlsfx:11.0.2'
44
  implementation 'org.controlsfx:controlsfx:11.0.3'
4645
  implementation 'org.fxmisc.richtext:richtextfx:0.10.5'
4746
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
4847
  implementation 'com.miglayout:miglayout-javafx:5.2'
49
  implementation('com.dlsc.preferencesfx:preferencesfx-core:11.6.0') {
48
  implementation('com.dlsc.preferencesfx:preferencesfx-core:11.7.0') {
5049
    exclude group: 'org.openjfx'
5150
  }
...
7069
  implementation "com.fasterxml.jackson.core:jackson-annotations:${v_jackson}"
7170
  implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${v_jackson}"
72
  implementation 'org.yaml:snakeyaml:1.26'
71
  implementation 'org.yaml:snakeyaml:1.27'
7372
7473
  // XML and XSL
...
9998
  implementation "org.apache.xmlgraphics:batik-util:${v_batik}"
10099
  implementation "org.apache.xmlgraphics:batik-xml:${v_batik}"
101
102
  // Spelling, TeX
103
  implementation fileTree(include: ['**/*.jar'], dir: 'libs')
104100
105101
  // Misc.
106102
  implementation 'org.ahocorasick:ahocorasick:0.4.0'
107103
  implementation 'org.apache.commons:commons-configuration2:2.7'
108104
  implementation 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
109105
  implementation 'javax.validation:validation-api:2.0.1.Final'
106
107
  // Configuration
108
  implementation 'org.apache.commons:commons-configuration2:2.7'
109
  implementation 'commons-beanutils:commons-beanutils:1.9.4'
110
111
  // Spelling, TeX, Docking
112
  implementation fileTree(include: ['**/*.jar'], dir: 'libs')
110113
111114
  def fx = ['controls', 'graphics', 'fxml', 'swing']
...
119122
  }
120123
124
  testImplementation "org.junit.jupiter:junit-jupiter-engine:${v_junit}"
121125
  testImplementation "org.junit.jupiter:junit-jupiter-api:${v_junit}"
122
  testRuntime "org.junit.jupiter:junit-jupiter-engine:${v_junit}"
126
  testImplementation "org.testfx:testfx-junit5:4.0.16-alpha"
123127
}
124128
A docs/i18n/korean.md
1
*Song of the Yellow Bird*:
2
3
	翩翩黃鳥,
4
	雌雄相依。
5
	念我之獨,
6
	誰其與歸?
7
8
English translation:
9
    	
10
	Orioles fly smoothly
11
	Female and male cuddle close together
12
	Thinking of my loneliness
13
	Whom shall I go with?
14
15
Fonts:
16
17
* Regular: 활판 인쇄술
18
* Bold: **활판 인쇄술**
19
* Monospace: `활판 인쇄술`
20
* Monospace bold: **`활판 인쇄술`**
21
* Math: $E=mc^2$
22
123
M docs/images/app-title.png
Binary file
A docs/images/app-title.zh-CN.png
Binary file
A docs/logo/logo-original.svg
1
1
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
2
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1280" height="1024" viewBox="0 0 1280 1024" xml:space="preserve">
4
<desc>Created with Fabric.js 3.6.3</desc>
5
<defs>
6
</defs>
7
<g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0153846153846 512.012312418764)" id="background-logo"  >
8
<rect style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  x="-325" y="-260" rx="0" ry="0" width="650" height="520" />
9
</g>
10
<g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0170725174504 420.4016715831266)" id="logo-logo"  >
11
<g style=""  paint-order="stroke"   >
12
		<g transform="matrix(2.537 0 0 -2.537 -86.35385711719567 85.244912)"  >
13
<linearGradient id="SVGID_1_302284" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-24.348526 -27.478867 -27.478867 24.348526 138.479 129.67187)"  x1="0" y1="0" x2="1" y2="0">
14
<stop offset="0%" style="stop-color:rgb(245,132,41);stop-opacity: 1"/>
15
<stop offset="100%" style="stop-color:rgb(251,173,23);stop-opacity: 1"/>
16
</linearGradient>
17
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: url(#SVGID_1_302284); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(-127.92674550729492, -117.16399999999999)" d="m 118.951 124.648 c -9.395 -14.441 -5.243 -20.693 -5.243 -20.693 v 0 c 0 0 6.219 9.126 9.771 5.599 v 0 c 3.051 -3.023 -2.415 -8.668 -2.415 -8.668 v 0 c 0 0 33.24 13.698 17.995 28.872 v 0 c 0 0 -3.203 3.683 -7.932 3.684 v 0 c -3.46 0 -7.736 -1.97 -12.176 -8.794" stroke-linecap="round" />
18
</g>
19
		<g transform="matrix(2.537 0 0 -2.537 -84.52085711719567 70.2729119999999)"  >
20
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(250,220,153); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(11.9895, -1.2609990716440347)" d="m 0 0 c 0 0 -6.501 6.719 -11.093 5.443 c -5.584 -1.545 -12.886 -12.078 -12.886 -12.078 c 0 0 5.98 16.932 15.29 15.731 C -1.19 8.127 0 0 0 0" stroke-linecap="round" />
21
</g>
22
		<g transform="matrix(2.537 0 0 -2.537 -22.327857117195663 48.729911999999956)"  >
23
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(-4.189, -10.432)" d="m 0 0 l -0.87 16.89 l 3.995 3.974 l 6.123 -6.156 z" stroke-linecap="round" />
24
</g>
25
		<g transform="matrix(2.537 0 0 -2.537 -11.3118571171957 24.124911999999966)"  >
26
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(4.0955, -2.037)" d="m 0 0 l -2.081 -2.069 l -6.11 6.143 l 2.081 2.069 z" stroke-linecap="round" />
27
</g>
28
		<g transform="matrix(2.537 0 0 -2.537 46.27614288280432 -57.96708800000005)"  >
29
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(12.070999999999998, 9.599000000000004)" d="m 0 0 c -1.226 0.69 -2.81 0.523 -3.862 -0.524 c -1.275 -1.268 -1.28 -3.33 -0.013 -4.604 l -31.681 -31.501 l -6.11 6.143 c 19.224 19.305 25.369 35.582 25.369 35.582 c 15.857 2.364 27.851 8.624 33.821 12.335 z" stroke-linecap="round" />
30
</g>
31
		<g transform="matrix(2.537 0 0 -2.537 -26.842857117195706 8.501911999999976)"  >
32
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(4.1075, -2.0525)" d="M 0 0 L -2.081 -2.069 L -8.215 4.11 L -6.141 6.174 Z" stroke-linecap="round" />
33
</g>
34
		<g transform="matrix(2.537 0 0 -2.537 -51.495857117195726 19.491911999999985)"  >
35
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(10.434000000000001, -1.0939999999999994)" d="m 0 0 l -3.995 -3.974 l -16.873 0.96 l 14.752 9.176 z" stroke-linecap="round" />
36
</g>
37
		<g transform="matrix(2.537 0 0 -2.537 55.72014288280434 -48.441088000000036)"  >
38
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(9.671499999999998, 11.999499999999998)" d="M 0 0 L 17.536 17.443 C 13.788 11.486 7.47 -0.468 5.021 -16.312 c 0 0 -15.526 -6.982 -35.765 -25.13 l -6.135 6.168 l 31.681 31.5 c 1.273 -1.28 3.33 -1.279 4.604 -0.012 C 0.435 -2.764 0.629 -1.223 0 0" stroke-linecap="round" />
39
</g>
40
</g>
41
</g>
42
<g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 643.7363123827618 766.1975713477327)" id="text-logo-path"  >
43
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(247,149,33); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(-186.83999999999997, 27.08)" d="M 4.47 -6.1 L 4.47 -6.1 L 4.47 -47.5 Q 4.47 -50.27 6.43 -52.23 Q 8.39 -54.19 11.16 -54.19 L 11.16 -54.19 Q 14.01 -54.19 15.95 -52.23 Q 17.89 -50.27 17.89 -47.5 L 17.89 -47.5 L 17.89 -30.09 L 34.95 -51.97 Q 35.74 -52.97 36.94 -53.58 Q 38.13 -54.19 39.42 -54.19 L 39.42 -54.19 Q 41.77 -54.19 43.42 -52.5 Q 45.07 -50.82 45.07 -48.5 L 45.07 -48.5 Q 45.07 -46.46 43.82 -44.93 L 43.82 -44.93 L 32.93 -31.44 L 46.8 -9.81 Q 47.84 -8.11 47.84 -6.27 L 47.84 -6.27 Q 47.84 -3.33 45.9 -1.39 Q 43.96 0.55 41.19 0.55 L 41.19 0.55 Q 39.42 0.55 37.89 -0.29 Q 36.37 -1.14 35.43 -2.57 L 35.43 -2.57 L 23.78 -21.15 L 17.89 -13.9 L 17.89 -6.1 Q 17.89 -3.33 15.93 -1.39 Q 13.97 0.55 11.16 0.55 L 11.16 0.55 Q 8.39 0.55 6.43 -1.39 Q 4.47 -3.33 4.47 -6.1 Z M 50.27 -19.24 L 50.27 -19.24 Q 50.27 -25.13 52.71 -29.78 Q 55.16 -34.43 59.7 -37.06 Q 64.24 -39.69 70.27 -39.69 L 70.27 -39.69 Q 76.37 -39.69 80.78 -37.09 Q 85.18 -34.49 87.43 -30.32 Q 89.69 -26.14 89.69 -21.6 L 89.69 -21.6 Q 89.69 -18.69 88.33 -17.26 Q 86.98 -15.84 83.86 -15.84 L 83.86 -15.84 L 62.89 -15.84 Q 63.23 -12.38 65.38 -10.31 Q 67.53 -8.25 70.86 -8.25 L 70.86 -8.25 Q 72.84 -8.25 74.19 -8.91 Q 75.54 -9.57 76.62 -10.64 L 76.62 -10.64 Q 77.62 -11.58 78.42 -12.03 Q 79.22 -12.48 80.43 -12.48 L 80.43 -12.48 Q 82.61 -12.48 84.19 -10.89 Q 85.77 -9.29 85.77 -7.04 L 85.77 -7.04 Q 85.77 -4.54 83.62 -2.77 L 83.62 -2.77 Q 81.71 -1.14 78.16 -0.03 Q 74.61 1.07 70.58 1.07 L 70.58 1.07 Q 64.76 1.07 60.13 -1.42 Q 55.5 -3.92 52.89 -8.53 Q 50.27 -13.14 50.27 -19.24 Z M 62.96 -23.57 L 62.96 -23.57 L 76.96 -23.57 Q 76.82 -26.97 74.93 -28.97 Q 73.05 -30.96 70.06 -30.96 L 70.06 -30.96 Q 67.08 -30.96 65.21 -28.97 Q 63.34 -26.97 62.96 -23.57 Z M 91.63 -19.24 L 91.63 -19.24 Q 91.63 -25.13 94.07 -29.78 Q 96.52 -34.43 101.06 -37.06 Q 105.6 -39.69 111.63 -39.69 L 111.63 -39.69 Q 117.73 -39.69 122.14 -37.09 Q 126.54 -34.49 128.79 -30.32 Q 131.04 -26.14 131.04 -21.6 L 131.04 -21.6 Q 131.04 -18.69 129.69 -17.26 Q 128.34 -15.84 125.22 -15.84 L 125.22 -15.84 L 104.25 -15.84 Q 104.59 -12.38 106.74 -10.31 Q 108.89 -8.25 112.22 -8.25 L 112.22 -8.25 Q 114.2 -8.25 115.55 -8.91 Q 116.9 -9.57 117.98 -10.64 L 117.98 -10.64 Q 118.98 -11.58 119.78 -12.03 Q 120.58 -12.48 121.79 -12.48 L 121.79 -12.48 Q 123.97 -12.48 125.55 -10.89 Q 127.13 -9.29 127.13 -7.04 L 127.13 -7.04 Q 127.13 -4.54 124.98 -2.77 L 124.98 -2.77 Q 123.07 -1.14 119.52 -0.03 Q 115.96 1.07 111.94 1.07 L 111.94 1.07 Q 106.12 1.07 101.49 -1.42 Q 96.86 -3.92 94.24 -8.53 Q 91.63 -13.14 91.63 -19.24 Z M 104.32 -23.57 L 104.32 -23.57 L 118.32 -23.57 Q 118.18 -26.97 116.29 -28.97 Q 114.4 -30.96 111.42 -30.96 L 111.42 -30.96 Q 108.44 -30.96 106.57 -28.97 Q 104.7 -26.97 104.32 -23.57 Z M 135.03 -6.03 L 135.03 -6.03 L 135.03 -33.14 Q 135.03 -35.64 136.85 -37.46 Q 138.67 -39.28 141.13 -39.28 L 141.13 -39.28 Q 143.7 -39.28 145.52 -37.46 Q 147.34 -35.64 147.34 -33.14 L 147.34 -33.14 L 147.34 -32.17 Q 148.97 -35.36 152.09 -37.42 Q 155.21 -39.49 159.82 -39.49 L 159.82 -39.49 Q 166.93 -39.49 170.19 -35.47 Q 173.44 -31.44 173.44 -24.44 L 173.44 -24.44 L 173.44 -6.03 Q 173.44 -3.33 171.5 -1.39 Q 169.56 0.55 166.86 0.55 L 166.86 0.55 Q 164.15 0.55 162.19 -1.39 Q 160.24 -3.33 160.24 -6.03 L 160.24 -6.03 L 160.24 -22.36 Q 160.24 -26.35 158.54 -27.91 Q 156.84 -29.47 154.65 -29.47 L 154.65 -29.47 Q 152.02 -29.47 150.13 -27.58 Q 148.24 -25.69 148.24 -20.73 L 148.24 -20.73 L 148.24 -6.03 Q 148.24 -3.33 146.3 -1.39 Q 144.36 0.55 141.65 0.55 L 141.65 0.55 Q 138.95 0.55 136.99 -1.39 Q 135.03 -3.33 135.03 -6.03 Z M 177.71 -47.56 L 177.71 -47.56 Q 177.71 -50.34 179.63 -52.26 Q 181.56 -54.19 184.23 -54.19 L 184.23 -54.19 Q 186.58 -54.19 188.39 -52.73 Q 190.19 -51.27 190.71 -48.99 L 190.71 -48.99 L 197.88 -15.12 L 206.52 -48.64 Q 207.07 -51.07 209.12 -52.63 Q 211.16 -54.19 213.69 -54.19 L 213.69 -54.19 Q 216.26 -54.19 218.25 -52.57 Q 220.25 -50.96 220.8 -48.64 L 220.8 -48.64 L 229.4 -15.39 L 236.64 -49.33 Q 237.06 -51.38 238.76 -52.78 Q 240.46 -54.19 242.61 -54.19 L 242.61 -54.19 Q 245.17 -54.19 246.94 -52.4 Q 248.71 -50.62 248.71 -48.05 L 248.71 -48.05 Q 248.71 -47.56 248.57 -46.73 L 248.57 -46.73 L 239.69 -7.38 Q 238.9 -3.99 236.11 -1.72 Q 233.32 0.55 229.68 0.55 L 229.68 0.55 Q 226.14 0.55 223.37 -1.61 Q 220.59 -3.78 219.73 -7.11 L 219.73 -7.11 L 213.07 -33.45 L 206.38 -7.11 Q 205.51 -3.71 202.79 -1.58 Q 200.07 0.55 196.53 0.55 L 196.53 0.55 Q 192.89 0.55 190.17 -1.72 Q 187.45 -3.99 186.65 -7.38 L 186.65 -7.38 L 177.85 -46.14 Q 177.71 -47.15 177.71 -47.56 Z M 253.35 -6.03 L 253.35 -6.03 L 253.35 -33.14 Q 253.35 -35.64 255.17 -37.46 Q 256.99 -39.28 259.46 -39.28 L 259.46 -39.28 Q 262.02 -39.28 263.84 -37.46 Q 265.66 -35.64 265.66 -33.14 L 265.66 -33.14 L 265.66 -31.44 L 265.94 -31.44 Q 266.8 -33.56 268.1 -35.24 Q 269.4 -36.92 270.69 -37.61 L 270.69 -37.61 Q 271.9 -38.24 273.46 -38.27 L 273.46 -38.27 Q 276.65 -38.27 278.14 -36.45 Q 279.63 -34.63 279.63 -32.52 L 279.63 -32.52 Q 279.63 -30.33 278.11 -28.62 Q 276.58 -26.9 274.08 -26.9 L 274.08 -26.9 Q 272.59 -26.9 271.07 -26.26 Q 269.54 -25.62 268.47 -24.34 L 268.47 -24.34 Q 266.56 -21.98 266.56 -17.68 L 266.56 -17.68 L 266.56 -6.03 Q 266.56 -3.33 264.62 -1.39 Q 262.68 0.55 259.98 0.55 L 259.98 0.55 Q 257.27 0.55 255.31 -1.39 Q 253.35 -3.33 253.35 -6.03 Z M 282.41 -49.71 L 282.41 -49.71 Q 282.41 -52 284.03 -53.61 Q 285.66 -55.23 287.95 -55.23 L 287.95 -55.23 L 291.21 -55.23 Q 293.5 -55.23 295.13 -53.6 Q 296.76 -51.97 296.76 -49.71 L 296.76 -49.71 Q 296.76 -47.43 295.11 -45.8 Q 293.46 -44.17 291.21 -44.17 L 291.21 -44.17 L 287.95 -44.17 Q 285.66 -44.17 284.03 -45.8 Q 282.41 -47.43 282.41 -49.71 Z M 282.96 -6.03 L 282.96 -6.03 L 282.96 -32.66 Q 282.96 -35.36 284.92 -37.32 Q 286.88 -39.28 289.58 -39.28 L 289.58 -39.28 Q 292.29 -39.28 294.23 -37.32 Q 296.17 -35.36 296.17 -32.66 L 296.17 -32.66 L 296.17 -6.03 Q 296.17 -3.33 294.21 -1.39 Q 292.25 0.55 289.58 0.55 L 289.58 0.55 Q 286.88 0.55 284.92 -1.39 Q 282.96 -3.33 282.96 -6.03 Z M 299.43 -34.29 L 299.43 -34.29 Q 299.43 -36.12 300.71 -37.41 Q 301.99 -38.69 303.76 -38.69 L 303.76 -38.69 L 306.19 -38.69 L 306.46 -43.96 Q 306.6 -46.32 308.34 -47.98 Q 310.07 -49.64 312.5 -49.64 L 312.5 -49.64 Q 314.99 -49.64 316.76 -47.86 Q 318.53 -46.07 318.53 -43.58 L 318.53 -43.58 L 318.53 -38.69 L 322.72 -38.69 Q 324.49 -38.69 325.77 -37.41 Q 327.06 -36.12 327.06 -34.36 L 327.06 -34.36 Q 327.06 -32.52 325.77 -31.24 Q 324.49 -29.95 322.72 -29.95 L 322.72 -29.95 L 318.81 -29.95 L 318.81 -14.14 Q 318.81 -11.23 320.05 -10.02 Q 321.3 -8.81 323.83 -8.81 L 323.83 -8.81 Q 325.46 -8.46 326.61 -7.14 Q 327.75 -5.82 327.75 -4.06 L 327.75 -4.06 Q 327.75 -2.57 326.94 -1.39 Q 326.12 -0.21 324.84 0.35 L 324.84 0.35 Q 322 0.83 318.11 0.87 L 318.11 0.87 Q 311.28 0.9 308.44 -2.5 L 308.44 -2.5 Q 305.67 -5.79 305.67 -12.65 L 305.67 -12.65 Q 305.67 -12.83 305.67 -13 L 305.67 -13 L 305.74 -29.95 L 303.76 -29.95 Q 301.99 -29.95 300.71 -31.24 Q 299.43 -32.52 299.43 -34.29 Z M 329.8 -19.24 L 329.8 -19.24 Q 329.8 -25.13 332.24 -29.78 Q 334.68 -34.43 339.23 -37.06 Q 343.77 -39.69 349.8 -39.69 L 349.8 -39.69 Q 355.9 -39.69 360.3 -37.09 Q 364.71 -34.49 366.96 -30.32 Q 369.21 -26.14 369.21 -21.6 L 369.21 -21.6 Q 369.21 -18.69 367.86 -17.26 Q 366.51 -15.84 363.39 -15.84 L 363.39 -15.84 L 342.42 -15.84 Q 342.76 -12.38 344.91 -10.31 Q 347.06 -8.25 350.39 -8.25 L 350.39 -8.25 Q 352.37 -8.25 353.72 -8.91 Q 355.07 -9.57 356.14 -10.64 L 356.14 -10.64 Q 357.15 -11.58 357.95 -12.03 Q 358.74 -12.48 359.96 -12.48 L 359.96 -12.48 Q 362.14 -12.48 363.72 -10.89 Q 365.3 -9.29 365.3 -7.04 L 365.3 -7.04 Q 365.3 -4.54 363.15 -2.77 L 363.15 -2.77 Q 361.24 -1.14 357.69 -0.03 Q 354.13 1.07 350.11 1.07 L 350.11 1.07 Q 344.29 1.07 339.66 -1.42 Q 335.03 -3.92 332.41 -8.53 Q 329.8 -13.14 329.8 -19.24 Z M 342.48 -23.57 L 342.48 -23.57 L 356.49 -23.57 Q 356.35 -26.97 354.46 -28.97 Q 352.57 -30.96 349.59 -30.96 L 349.59 -30.96 Q 346.61 -30.96 344.74 -28.97 Q 342.87 -26.97 342.48 -23.57 Z" stroke-linecap="round" />
44
</g>
45
</svg>
A docs/logo/logo-text.svg
1
1
<svg height="197.4767" viewBox="0 0 695.99768 197.4767" width="695.99768" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientTransform="matrix(-8.7796153 42.985832 -42.985832 -8.7796153 810.33577 828.59028)" gradientUnits="userSpaceOnUse" x1=".152358" x2=".968809" y1="-.044912" y2="-.049471"><stop offset="0" stop-color="#ec706a"/><stop offset="1" stop-color="#ecd980"/></linearGradient><g transform="translate(-295.50101 -692.52836)"><path d="m793.12811 845.45734c-1.09438 20.55837 6.93804 24.54772 6.93804 24.54772s.98325-13.16026 6.76656-11.6325c4.96369 1.30552 2.67983 10.4134 2.67983 10.4134s26.21535-34.03672 1.372-40.63137-5.51534-1.89773-10.40994.92679c-3.58074 2.06734-6.82887 6.66097-7.34649 16.37596" fill="url(#a)"/><path d="m826.30436 831.16428-10.99206-16.95952 1.75995-6.49966 10.01483 2.71233z" fill="#126d95"/><path d="m828.56081 804.89512-.91739 3.38458-9.99361-2.70665.91739-3.38458z" fill="#126d95"/><g fill="#51a9cf"><path d="m834.01973 741.0381c-1.68105.0185-3.22054 1.13771-3.68367 2.84981-.56186 2.07405.665 4.21099 2.73743 4.77241l-13.96475 51.52944-9.99361-2.70665c8.36013-31.46487 4.99411-51.98144 4.99411-51.98144 14.99782-11.92097 23.67-25.56577 27.63101-32.97331z"/><path d="m818.56767 802.18881-.9174 3.38458-10.03996-2.72957.91314-3.37522z"/><path d="m817.07405 807.70594-1.75995 6.49966-18.03534 9.08805 9.78412-18.31044z"/></g><path d="m836.1981 741.64919 7.72577-28.52932c-.3195 8.40427.28451 24.55036 7.21678 42.41047 0 0-11.89603 16.50235-21.99788 47.3763l-10.03442-2.71758 13.96533-51.5284c2.08221.56405 4.21039-.66603 4.77182-2.73844.45427-1.67248-.26571-3.38317-1.64739-4.27302" fill="#126d95"/></g><text transform="translate(-295.73751 -689.6407)"/><g style="font-style:italic;font-weight:800;font-size:133.333;font-family:Merriweather Sans;letter-spacing:0;word-spacing:0;fill:#51a9cf"><text x="16.133343" y="130.6234"><tspan x="16.133343" y="130.6234">KeenWr</tspan></text><text x="552.53137" y="130.6234"><tspan x="552.53137" y="130.6234">te</tspan></text></g></svg>
A docs/logo/logo-text.zh-CN.svg
1
1
<svg height="197.4767" viewBox="0 0 493.25561 197.4767" width="493.25562" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientTransform="matrix(-8.7796153 42.985832 -42.985832 -8.7796153 810.33577 828.59028)" gradientUnits="userSpaceOnUse" x1=".152358" x2=".968809" y1="-.044912" y2="-.049471"><stop offset="0" stop-color="#ec706a"/><stop offset="1" stop-color="#ecd980"/></linearGradient><text transform="translate(-312.52749 -472.07353)"/><text fill="#51a9cf" font-family="'Noto Serif CJK SC'" font-size="35.1025" letter-spacing="0" transform="matrix(3.7983969 0 0 3.7983969 -330.7653 961.00598)" word-spacing="0"><tspan x="91.011719" y="-209.05206"><tspan x="91.011719" y="-209.05206">智能写<tspan fill="#51a9cf"/></tspan></tspan><tspan x="91.011719" y="-165.17393"/></text><g transform="translate(-377.88503 -692.52836)"><path d="m793.12811 845.45734c-1.09438 20.55837 6.93804 24.54772 6.93804 24.54772s.98325-13.16026 6.76656-11.6325c4.96369 1.30552 2.67983 10.4134 2.67983 10.4134s26.21535-34.03672 1.372-40.63137-5.51534-1.89773-10.40994.92679c-3.58074 2.06734-6.82887 6.66097-7.34649 16.37596" fill="url(#a)"/><path d="m826.30436 831.16428-10.99206-16.95952 1.75995-6.49966 10.01483 2.71233z" fill="#126d95"/><path d="m828.56081 804.89512-.91739 3.38458-9.99361-2.70665.91739-3.38458z" fill="#126d95"/><g fill="#51a9cf"><path d="m834.01973 741.0381c-1.68105.0185-3.22054 1.13771-3.68367 2.84981-.56186 2.07405.665 4.21099 2.73743 4.77241l-13.96475 51.52944-9.99361-2.70665c8.36013-31.46487 4.99411-51.98144 4.99411-51.98144 14.99782-11.92097 23.67-25.56577 27.63101-32.97331z"/><path d="m818.56767 802.18881-.9174 3.38458-10.03996-2.72957.91314-3.37522z"/><path d="m817.07405 807.70594-1.75995 6.49966-18.03534 9.08805 9.78412-18.31044z"/></g><path d="m836.1981 741.64919 7.72577-28.52932c-.3195 8.40427.28451 24.55036 7.21678 42.41047 0 0-11.89603 16.50235-21.99788 47.3763l-10.03442-2.71758 13.96533-51.5284c2.08221.56405 4.21039-.66603 4.77182-2.73844.45427-1.67248-.26571-3.38317-1.64739-4.27302" fill="#126d95"/></g></svg>
M installer
1515
ARG_JAVA_OS="linux"
1616
ARG_JAVA_ARCH="amd64"
17
ARG_JAVA_VERSION="14.0.2"
18
ARG_JAVA_UPDATE="13"
17
ARG_JAVA_VERSION="15.0.1"
18
ARG_JAVA_UPDATE="9"
1919
ARG_JAVA_DIR="java"
2020
D libs/jmathtex/jmathtex.jar
Binary file
A libs/jmathtex.jar
Binary file
A libs/tiwulfx-dock-0.1.jar
Binary file
A licenses/BEAN-VALIDATION-API.md
11
2
                                 Apache License
3
                           Version 2.0, January 2004
4
                        http://www.apache.org/licenses/
5
6
   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
8
   1. Definitions.
9
10
      "License" shall mean the terms and conditions for use, reproduction,
11
      and distribution as defined by Sections 1 through 9 of this document.
12
13
      "Licensor" shall mean the copyright owner or entity authorized by
14
      the copyright owner that is granting the License.
15
16
      "Legal Entity" shall mean the union of the acting entity and all
17
      other entities that control, are controlled by, or are under common
18
      control with that entity. For the purposes of this definition,
19
      "control" means (i) the power, direct or indirect, to cause the
20
      direction or management of such entity, whether by contract or
21
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
      outstanding shares, or (iii) beneficial ownership of such entity.
23
24
      "You" (or "Your") shall mean an individual or Legal Entity
25
      exercising permissions granted by this License.
26
27
      "Source" form shall mean the preferred form for making modifications,
28
      including but not limited to software source code, documentation
29
      source, and configuration files.
30
31
      "Object" form shall mean any form resulting from mechanical
32
      transformation or translation of a Source form, including but
33
      not limited to compiled object code, generated documentation,
34
      and conversions to other media types.
35
36
      "Work" shall mean the work of authorship, whether in Source or
37
      Object form, made available under the License, as indicated by a
38
      copyright notice that is included in or attached to the work
39
      (an example is provided in the Appendix below).
40
41
      "Derivative Works" shall mean any work, whether in Source or Object
42
      form, that is based on (or derived from) the Work and for which the
43
      editorial revisions, annotations, elaborations, or other modifications
44
      represent, as a whole, an original work of authorship. For the purposes
45
      of this License, Derivative Works shall not include works that remain
46
      separable from, or merely link (or bind by name) to the interfaces of,
47
      the Work and Derivative Works thereof.
48
49
      "Contribution" shall mean any work of authorship, including
50
      the original version of the Work and any modifications or additions
51
      to that Work or Derivative Works thereof, that is intentionally
52
      submitted to Licensor for inclusion in the Work by the copyright owner
53
      or by an individual or Legal Entity authorized to submit on behalf of
54
      the copyright owner. For the purposes of this definition, "submitted"
55
      means any form of electronic, verbal, or written communication sent
56
      to the Licensor or its representatives, including but not limited to
57
      communication on electronic mailing lists, source code control systems,
58
      and issue tracking systems that are managed by, or on behalf of, the
59
      Licensor for the purpose of discussing and improving the Work, but
60
      excluding communication that is conspicuously marked or otherwise
61
      designated in writing by the copyright owner as "Not a Contribution."
62
63
      "Contributor" shall mean Licensor and any individual or Legal Entity
64
      on behalf of whom a Contribution has been received by Licensor and
65
      subsequently incorporated within the Work.
66
67
   2. Grant of Copyright License. Subject to the terms and conditions of
68
      this License, each Contributor hereby grants to You a perpetual,
69
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
      copyright license to reproduce, prepare Derivative Works of,
71
      publicly display, publicly perform, sublicense, and distribute the
72
      Work and such Derivative Works in Source or Object form.
73
74
   3. Grant of Patent License. Subject to the terms and conditions of
75
      this License, each Contributor hereby grants to You a perpetual,
76
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
      (except as stated in this section) patent license to make, have made,
78
      use, offer to sell, sell, import, and otherwise transfer the Work,
79
      where such license applies only to those patent claims licensable
80
      by such Contributor that are necessarily infringed by their
81
      Contribution(s) alone or by combination of their Contribution(s)
82
      with the Work to which such Contribution(s) was submitted. If You
83
      institute patent litigation against any entity (including a
84
      cross-claim or counterclaim in a lawsuit) alleging that the Work
85
      or a Contribution incorporated within the Work constitutes direct
86
      or contributory patent infringement, then any patent licenses
87
      granted to You under this License for that Work shall terminate
88
      as of the date such litigation is filed.
89
90
   4. Redistribution. You may reproduce and distribute copies of the
91
      Work or Derivative Works thereof in any medium, with or without
92
      modifications, and in Source or Object form, provided that You
93
      meet the following conditions:
94
95
      (a) You must give any other recipients of the Work or
96
          Derivative Works a copy of this License; and
97
98
      (b) You must cause any modified files to carry prominent notices
99
          stating that You changed the files; and
100
101
      (c) You must retain, in the Source form of any Derivative Works
102
          that You distribute, all copyright, patent, trademark, and
103
          attribution notices from the Source form of the Work,
104
          excluding those notices that do not pertain to any part of
105
          the Derivative Works; and
106
107
      (d) If the Work includes a "NOTICE" text file as part of its
108
          distribution, then any Derivative Works that You distribute must
109
          include a readable copy of the attribution notices contained
110
          within such NOTICE file, excluding those notices that do not
111
          pertain to any part of the Derivative Works, in at least one
112
          of the following places: within a NOTICE text file distributed
113
          as part of the Derivative Works; within the Source form or
114
          documentation, if provided along with the Derivative Works; or,
115
          within a display generated by the Derivative Works, if and
116
          wherever such third-party notices normally appear. The contents
117
          of the NOTICE file are for informational purposes only and
118
          do not modify the License. You may add Your own attribution
119
          notices within Derivative Works that You distribute, alongside
120
          or as an addendum to the NOTICE text from the Work, provided
121
          that such additional attribution notices cannot be construed
122
          as modifying the License.
123
124
      You may add Your own copyright statement to Your modifications and
125
      may provide additional or different license terms and conditions
126
      for use, reproduction, or distribution of Your modifications, or
127
      for any such Derivative Works as a whole, provided Your use,
128
      reproduction, and distribution of the Work otherwise complies with
129
      the conditions stated in this License.
130
131
   5. Submission of Contributions. Unless You explicitly state otherwise,
132
      any Contribution intentionally submitted for inclusion in the Work
133
      by You to the Licensor shall be under the terms and conditions of
134
      this License, without any additional terms or conditions.
135
      Notwithstanding the above, nothing herein shall supersede or modify
136
      the terms of any separate license agreement you may have executed
137
      with Licensor regarding such Contributions.
138
139
   6. Trademarks. This License does not grant permission to use the trade
140
      names, trademarks, service marks, or product names of the Licensor,
141
      except as required for reasonable and customary use in describing the
142
      origin of the Work and reproducing the content of the NOTICE file.
143
144
   7. Disclaimer of Warranty. Unless required by applicable law or
145
      agreed to in writing, Licensor provides the Work (and each
146
      Contributor provides its Contributions) on an "AS IS" BASIS,
147
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
      implied, including, without limitation, any warranties or conditions
149
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
      PARTICULAR PURPOSE. You are solely responsible for determining the
151
      appropriateness of using or redistributing the Work and assume any
152
      risks associated with Your exercise of permissions under this License.
153
154
   8. Limitation of Liability. In no event and under no legal theory,
155
      whether in tort (including negligence), contract, or otherwise,
156
      unless required by applicable law (such as deliberate and grossly
157
      negligent acts) or agreed to in writing, shall any Contributor be
158
      liable to You for damages, including any direct, indirect, special,
159
      incidental, or consequential damages of any character arising as a
160
      result of this License or out of the use or inability to use the
161
      Work (including but not limited to damages for loss of goodwill,
162
      work stoppage, computer failure or malfunction, or any and all
163
      other commercial damages or losses), even if such Contributor
164
      has been advised of the possibility of such damages.
165
166
   9. Accepting Warranty or Additional Liability. While redistributing
167
      the Work or Derivative Works thereof, You may choose to offer,
168
      and charge a fee for, acceptance of support, warranty, indemnity,
169
      or other liability obligations and/or rights consistent with this
170
      License. However, in accepting such obligations, You may act only
171
      on Your own behalf and on Your sole responsibility, not on behalf
172
      of any other Contributor, and only if You agree to indemnify,
173
      defend, and hold each Contributor harmless for any liability
174
      incurred by, or claims asserted against, such Contributor by reason
175
      of your accepting any such warranty or additional liability.
176
177
   END OF TERMS AND CONDITIONS
178
179
   APPENDIX: How to apply the Apache License to your work.
180
181
      To apply the Apache License to your work, attach the following
182
      boilerplate notice, with the fields enclosed by brackets "[]"
183
      replaced with your own identifying information. (Don't include
184
      the brackets!)  The text should be enclosed in the appropriate
185
      comment syntax for the file format. We also recommend that a
186
      file or class name and description of purpose be included on the
187
      same "printed page" as the copyright notice for easier
188
      identification within third-party archives.
189
190
   Copyright [yyyy] [name of copyright owner]
191
192
   Licensed under the Apache License, Version 2.0 (the "License");
193
   you may not use this file except in compliance with the License.
194
   You may obtain a copy of the License at
195
196
       http://www.apache.org/licenses/LICENSE-2.0
197
198
   Unless required by applicable law or agreed to in writing, software
199
   distributed under the License is distributed on an "AS IS" BASIS,
200
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
   See the License for the specific language governing permissions and
202
   limitations under the License.
203
D licenses/fonts/FIRACODE.txt
1
Copyright (c) 2014, The Fira Code Project Authors (https://github.com/tonsky/FiraCode)
2
3
This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
This license is copied below, and is also available with a FAQ at:
5
http://scripts.sil.org/OFL
6
7
8
-----------------------------------------------------------
9
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
-----------------------------------------------------------
11
12
PREAMBLE
13
The goals of the Open Font License (OFL) are to stimulate worldwide
14
development of collaborative font projects, to support the font creation
15
efforts of academic and linguistic communities, and to provide a free and
16
open framework in which fonts may be shared and improved in partnership
17
with others.
18
19
The OFL allows the licensed fonts to be used, studied, modified and
20
redistributed freely as long as they are not sold by themselves. The
21
fonts, including any derivative works, can be bundled, embedded,
22
redistributed and/or sold with any software provided that any reserved
23
names are not used by derivative works. The fonts and derivatives,
24
however, cannot be released under any other type of license. The
25
requirement for fonts to remain under this license does not apply
26
to any document created using the fonts or their derivatives.
27
28
DEFINITIONS
29
"Font Software" refers to the set of files released by the Copyright
30
Holder(s) under this license and clearly marked as such. This may
31
include source files, build scripts and documentation.
32
33
"Reserved Font Name" refers to any names specified as such after the
34
copyright statement(s).
35
36
"Original Version" refers to the collection of Font Software components as
37
distributed by the Copyright Holder(s).
38
39
"Modified Version" refers to any derivative made by adding to, deleting,
40
or substituting -- in part or in whole -- any of the components of the
41
Original Version, by changing formats or by porting the Font Software to a
42
new environment.
43
44
"Author" refers to any designer, engineer, programmer, technical
45
writer or other person who contributed to the Font Software.
46
47
PERMISSION & CONDITIONS
48
Permission is hereby granted, free of charge, to any person obtaining
49
a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
redistribute, and sell modified and unmodified copies of the Font
51
Software, subject to the following conditions:
52
53
1) Neither the Font Software nor any of its individual components,
54
in Original or Modified Versions, may be sold by itself.
55
56
2) Original or Modified Versions of the Font Software may be bundled,
57
redistributed and/or sold with any software, provided that each copy
58
contains the above copyright notice and this license. These can be
59
included either as stand-alone text files, human-readable headers or
60
in the appropriate machine-readable metadata fields within text or
61
binary files as long as those fields can be easily viewed by the user.
62
63
3) No Modified Version of the Font Software may use the Reserved Font
64
Name(s) unless explicit written permission is granted by the corresponding
65
Copyright Holder. This restriction only applies to the primary font name as
66
presented to the users.
67
68
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
Software shall not be used to promote, endorse or advertise any
70
Modified Version, except to acknowledge the contribution(s) of the
71
Copyright Holder(s) and the Author(s) or with their explicit written
72
permission.
73
74
5) The Font Software, modified or unmodified, in part or in whole,
75
must be distributed entirely under this license, and must not be
76
distributed under any other license. The requirement for fonts to
77
remain under this license does not apply to any document created
78
using the Font Software.
79
80
TERMINATION
81
This license becomes null and void if any of the above conditions are
82
not met.
83
84
DISCLAIMER
85
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
OTHER DEALINGS IN THE FONT SOFTWARE.
941
A licenses/fonts/NOTO-CJK.md
1
URL: https://github.com/googlefonts/noto-cjk
2
3
Version: 1.002 or later
4
5
License: SIL Open Font License v1.1
6
7
License File: LICENSE
8
9
Note: prior releases of the CJK fonts were issued under the Apache 2
10
license. This was changed to the SIL OFL v1.1 starting with Version 1.002.
11
12
Description:
13
Noto CJK fonts, supporting Simplified Chinese, Traditional Chinese,
14
Japanese, and Korean. The supported scripts are Han, Hiragana, Katakana,
15
Hangul, and Bopomofo. Latin, Greek, Cyrillic, and various symbols are also
16
supported for compatibility with CJK standards.
17
18
The fonts in this directory are developed by Google and Adobe and are
19
released as open source under the Apache license version 2.0. The copyright
20
is held by Adobe, while the trademarks on the names are held by Google.
21
22
A README-formats file has been added explaining the different formats
23
provided and their features and limitations.
124
A licenses/fonts/NOTO-SANS.md
11
2
                                 Apache License
3
                           Version 2.0, January 2004
4
                        http://www.apache.org/licenses/
5
6
   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
8
   1. Definitions.
9
10
      "License" shall mean the terms and conditions for use, reproduction,
11
      and distribution as defined by Sections 1 through 9 of this document.
12
13
      "Licensor" shall mean the copyright owner or entity authorized by
14
      the copyright owner that is granting the License.
15
16
      "Legal Entity" shall mean the union of the acting entity and all
17
      other entities that control, are controlled by, or are under common
18
      control with that entity. For the purposes of this definition,
19
      "control" means (i) the power, direct or indirect, to cause the
20
      direction or management of such entity, whether by contract or
21
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
      outstanding shares, or (iii) beneficial ownership of such entity.
23
24
      "You" (or "Your") shall mean an individual or Legal Entity
25
      exercising permissions granted by this License.
26
27
      "Source" form shall mean the preferred form for making modifications,
28
      including but not limited to software source code, documentation
29
      source, and configuration files.
30
31
      "Object" form shall mean any form resulting from mechanical
32
      transformation or translation of a Source form, including but
33
      not limited to compiled object code, generated documentation,
34
      and conversions to other media types.
35
36
      "Work" shall mean the work of authorship, whether in Source or
37
      Object form, made available under the License, as indicated by a
38
      copyright notice that is included in or attached to the work
39
      (an example is provided in the Appendix below).
40
41
      "Derivative Works" shall mean any work, whether in Source or Object
42
      form, that is based on (or derived from) the Work and for which the
43
      editorial revisions, annotations, elaborations, or other modifications
44
      represent, as a whole, an original work of authorship. For the purposes
45
      of this License, Derivative Works shall not include works that remain
46
      separable from, or merely link (or bind by name) to the interfaces of,
47
      the Work and Derivative Works thereof.
48
49
      "Contribution" shall mean any work of authorship, including
50
      the original version of the Work and any modifications or additions
51
      to that Work or Derivative Works thereof, that is intentionally
52
      submitted to Licensor for inclusion in the Work by the copyright owner
53
      or by an individual or Legal Entity authorized to submit on behalf of
54
      the copyright owner. For the purposes of this definition, "submitted"
55
      means any form of electronic, verbal, or written communication sent
56
      to the Licensor or its representatives, including but not limited to
57
      communication on electronic mailing lists, source code control systems,
58
      and issue tracking systems that are managed by, or on behalf of, the
59
      Licensor for the purpose of discussing and improving the Work, but
60
      excluding communication that is conspicuously marked or otherwise
61
      designated in writing by the copyright owner as "Not a Contribution."
62
63
      "Contributor" shall mean Licensor and any individual or Legal Entity
64
      on behalf of whom a Contribution has been received by Licensor and
65
      subsequently incorporated within the Work.
66
67
   2. Grant of Copyright License. Subject to the terms and conditions of
68
      this License, each Contributor hereby grants to You a perpetual,
69
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
      copyright license to reproduce, prepare Derivative Works of,
71
      publicly display, publicly perform, sublicense, and distribute the
72
      Work and such Derivative Works in Source or Object form.
73
74
   3. Grant of Patent License. Subject to the terms and conditions of
75
      this License, each Contributor hereby grants to You a perpetual,
76
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
      (except as stated in this section) patent license to make, have made,
78
      use, offer to sell, sell, import, and otherwise transfer the Work,
79
      where such license applies only to those patent claims licensable
80
      by such Contributor that are necessarily infringed by their
81
      Contribution(s) alone or by combination of their Contribution(s)
82
      with the Work to which such Contribution(s) was submitted. If You
83
      institute patent litigation against any entity (including a
84
      cross-claim or counterclaim in a lawsuit) alleging that the Work
85
      or a Contribution incorporated within the Work constitutes direct
86
      or contributory patent infringement, then any patent licenses
87
      granted to You under this License for that Work shall terminate
88
      as of the date such litigation is filed.
89
90
   4. Redistribution. You may reproduce and distribute copies of the
91
      Work or Derivative Works thereof in any medium, with or without
92
      modifications, and in Source or Object form, provided that You
93
      meet the following conditions:
94
95
      (a) You must give any other recipients of the Work or
96
          Derivative Works a copy of this License; and
97
98
      (b) You must cause any modified files to carry prominent notices
99
          stating that You changed the files; and
100
101
      (c) You must retain, in the Source form of any Derivative Works
102
          that You distribute, all copyright, patent, trademark, and
103
          attribution notices from the Source form of the Work,
104
          excluding those notices that do not pertain to any part of
105
          the Derivative Works; and
106
107
      (d) If the Work includes a "NOTICE" text file as part of its
108
          distribution, then any Derivative Works that You distribute must
109
          include a readable copy of the attribution notices contained
110
          within such NOTICE file, excluding those notices that do not
111
          pertain to any part of the Derivative Works, in at least one
112
          of the following places: within a NOTICE text file distributed
113
          as part of the Derivative Works; within the Source form or
114
          documentation, if provided along with the Derivative Works; or,
115
          within a display generated by the Derivative Works, if and
116
          wherever such third-party notices normally appear. The contents
117
          of the NOTICE file are for informational purposes only and
118
          do not modify the License. You may add Your own attribution
119
          notices within Derivative Works that You distribute, alongside
120
          or as an addendum to the NOTICE text from the Work, provided
121
          that such additional attribution notices cannot be construed
122
          as modifying the License.
123
124
      You may add Your own copyright statement to Your modifications and
125
      may provide additional or different license terms and conditions
126
      for use, reproduction, or distribution of Your modifications, or
127
      for any such Derivative Works as a whole, provided Your use,
128
      reproduction, and distribution of the Work otherwise complies with
129
      the conditions stated in this License.
130
131
   5. Submission of Contributions. Unless You explicitly state otherwise,
132
      any Contribution intentionally submitted for inclusion in the Work
133
      by You to the Licensor shall be under the terms and conditions of
134
      this License, without any additional terms or conditions.
135
      Notwithstanding the above, nothing herein shall supersede or modify
136
      the terms of any separate license agreement you may have executed
137
      with Licensor regarding such Contributions.
138
139
   6. Trademarks. This License does not grant permission to use the trade
140
      names, trademarks, service marks, or product names of the Licensor,
141
      except as required for reasonable and customary use in describing the
142
      origin of the Work and reproducing the content of the NOTICE file.
143
144
   7. Disclaimer of Warranty. Unless required by applicable law or
145
      agreed to in writing, Licensor provides the Work (and each
146
      Contributor provides its Contributions) on an "AS IS" BASIS,
147
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
      implied, including, without limitation, any warranties or conditions
149
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
      PARTICULAR PURPOSE. You are solely responsible for determining the
151
      appropriateness of using or redistributing the Work and assume any
152
      risks associated with Your exercise of permissions under this License.
153
154
   8. Limitation of Liability. In no event and under no legal theory,
155
      whether in tort (including negligence), contract, or otherwise,
156
      unless required by applicable law (such as deliberate and grossly
157
      negligent acts) or agreed to in writing, shall any Contributor be
158
      liable to You for damages, including any direct, indirect, special,
159
      incidental, or consequential damages of any character arising as a
160
      result of this License or out of the use or inability to use the
161
      Work (including but not limited to damages for loss of goodwill,
162
      work stoppage, computer failure or malfunction, or any and all
163
      other commercial damages or losses), even if such Contributor
164
      has been advised of the possibility of such damages.
165
166
   9. Accepting Warranty or Additional Liability. While redistributing
167
      the Work or Derivative Works thereof, You may choose to offer,
168
      and charge a fee for, acceptance of support, warranty, indemnity,
169
      or other liability obligations and/or rights consistent with this
170
      License. However, in accepting such obligations, You may act only
171
      on Your own behalf and on Your sole responsibility, not on behalf
172
      of any other Contributor, and only if You agree to indemnify,
173
      defend, and hold each Contributor harmless for any liability
174
      incurred by, or claims asserted against, such Contributor by reason
175
      of your accepting any such warranty or additional liability.
176
177
   END OF TERMS AND CONDITIONS
178
179
   APPENDIX: How to apply the Apache License to your work.
180
181
      To apply the Apache License to your work, attach the following
182
      boilerplate notice, with the fields enclosed by brackets "[]"
183
      replaced with your own identifying information. (Don't include
184
      the brackets!)  The text should be enclosed in the appropriate
185
      comment syntax for the file format. We also recommend that a
186
      file or class name and description of purpose be included on the
187
      same "printed page" as the copyright notice for easier
188
      identification within third-party archives.
189
190
   Copyright [yyyy] [name of copyright owner]
191
192
   Licensed under the Apache License, Version 2.0 (the "License");
193
   you may not use this file except in compliance with the License.
194
   You may obtain a copy of the License at
195
196
       http://www.apache.org/licenses/LICENSE-2.0
197
198
   Unless required by applicable law or agreed to in writing, software
199
   distributed under the License is distributed on an "AS IS" BASIS,
200
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
   See the License for the specific language governing permissions and
202
   limitations under the License.
203
A licenses/fonts/NOTO.md
1
Copyright 2018 The Noto Project Authors (https://github.com/googlei18n/noto-fonts)
2
3
This Font Software is licensed under the SIL Open Font License,
4
Version 1.1.
5
6
This license is copied below, and is also available with a FAQ at:
7
http://scripts.sil.org/OFL
8
9
-----------------------------------------------------------
10
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
11
-----------------------------------------------------------
12
13
PREAMBLE
14
The goals of the Open Font License (OFL) are to stimulate worldwide
15
development of collaborative font projects, to support the font
16
creation efforts of academic and linguistic communities, and to
17
provide a free and open framework in which fonts may be shared and
18
improved in partnership with others.
19
20
The OFL allows the licensed fonts to be used, studied, modified and
21
redistributed freely as long as they are not sold by themselves. The
22
fonts, including any derivative works, can be bundled, embedded,
23
redistributed and/or sold with any software provided that any reserved
24
names are not used by derivative works. The fonts and derivatives,
25
however, cannot be released under any other type of license. The
26
requirement for fonts to remain under this license does not apply to
27
any document created using the fonts or their derivatives.
28
29
DEFINITIONS
30
"Font Software" refers to the set of files released by the Copyright
31
Holder(s) under this license and clearly marked as such. This may
32
include source files, build scripts and documentation.
33
34
"Reserved Font Name" refers to any names specified as such after the
35
copyright statement(s).
36
37
"Original Version" refers to the collection of Font Software
38
components as distributed by the Copyright Holder(s).
39
40
"Modified Version" refers to any derivative made by adding to,
41
deleting, or substituting -- in part or in whole -- any of the
42
components of the Original Version, by changing formats or by porting
43
the Font Software to a new environment.
44
45
"Author" refers to any designer, engineer, programmer, technical
46
writer or other person who contributed to the Font Software.
47
48
PERMISSION & CONDITIONS
49
Permission is hereby granted, free of charge, to any person obtaining
50
a copy of the Font Software, to use, study, copy, merge, embed,
51
modify, redistribute, and sell modified and unmodified copies of the
52
Font Software, subject to the following conditions:
53
54
1) Neither the Font Software nor any of its individual components, in
55
Original or Modified Versions, may be sold by itself.
56
57
2) Original or Modified Versions of the Font Software may be bundled,
58
redistributed and/or sold with any software, provided that each copy
59
contains the above copyright notice and this license. These can be
60
included either as stand-alone text files, human-readable headers or
61
in the appropriate machine-readable metadata fields within text or
62
binary files as long as those fields can be easily viewed by the user.
63
64
3) No Modified Version of the Font Software may use the Reserved Font
65
Name(s) unless explicit written permission is granted by the
66
corresponding Copyright Holder. This restriction only applies to the
67
primary font name as presented to the users.
68
69
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
70
Software shall not be used to promote, endorse or advertise any
71
Modified Version, except to acknowledge the contribution(s) of the
72
Copyright Holder(s) and the Author(s) or with their explicit written
73
permission.
74
75
5) The Font Software, modified or unmodified, in part or in whole,
76
must be distributed entirely under this license, and must not be
77
distributed under any other license. The requirement for fonts to
78
remain under this license does not apply to any document created using
79
the Font Software.
80
81
TERMINATION
82
This license becomes null and void if any of the above conditions are
83
not met.
84
85
DISCLAIMER
86
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
87
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
88
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
89
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
90
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
91
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
92
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
93
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
94
OTHER DEALINGS IN THE FONT SOFTWARE.
195
A licenses/fonts/SOURCE-CODE-PRO.md
1
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
2
3
This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
This license is copied below, and is also available with a FAQ at:
5
http://scripts.sil.org/OFL
6
7
8
-----------------------------------------------------------
9
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
-----------------------------------------------------------
11
12
PREAMBLE
13
The goals of the Open Font License (OFL) are to stimulate worldwide
14
development of collaborative font projects, to support the font creation
15
efforts of academic and linguistic communities, and to provide a free and
16
open framework in which fonts may be shared and improved in partnership
17
with others.
18
19
The OFL allows the licensed fonts to be used, studied, modified and
20
redistributed freely as long as they are not sold by themselves. The
21
fonts, including any derivative works, can be bundled, embedded, 
22
redistributed and/or sold with any software provided that any reserved
23
names are not used by derivative works. The fonts and derivatives,
24
however, cannot be released under any other type of license. The
25
requirement for fonts to remain under this license does not apply
26
to any document created using the fonts or their derivatives.
27
28
DEFINITIONS
29
"Font Software" refers to the set of files released by the Copyright
30
Holder(s) under this license and clearly marked as such. This may
31
include source files, build scripts and documentation.
32
33
"Reserved Font Name" refers to any names specified as such after the
34
copyright statement(s).
35
36
"Original Version" refers to the collection of Font Software components as
37
distributed by the Copyright Holder(s).
38
39
"Modified Version" refers to any derivative made by adding to, deleting,
40
or substituting -- in part or in whole -- any of the components of the
41
Original Version, by changing formats or by porting the Font Software to a
42
new environment.
43
44
"Author" refers to any designer, engineer, programmer, technical
45
writer or other person who contributed to the Font Software.
46
47
PERMISSION & CONDITIONS
48
Permission is hereby granted, free of charge, to any person obtaining
49
a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
redistribute, and sell modified and unmodified copies of the Font
51
Software, subject to the following conditions:
52
53
1) Neither the Font Software nor any of its individual components,
54
in Original or Modified Versions, may be sold by itself.
55
56
2) Original or Modified Versions of the Font Software may be bundled,
57
redistributed and/or sold with any software, provided that each copy
58
contains the above copyright notice and this license. These can be
59
included either as stand-alone text files, human-readable headers or
60
in the appropriate machine-readable metadata fields within text or
61
binary files as long as those fields can be easily viewed by the user.
62
63
3) No Modified Version of the Font Software may use the Reserved Font
64
Name(s) unless explicit written permission is granted by the corresponding
65
Copyright Holder. This restriction only applies to the primary font name as
66
presented to the users.
67
68
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
Software shall not be used to promote, endorse or advertise any
70
Modified Version, except to acknowledge the contribution(s) of the
71
Copyright Holder(s) and the Author(s) or with their explicit written
72
permission.
73
74
5) The Font Software, modified or unmodified, in part or in whole,
75
must be distributed entirely under this license, and must not be
76
distributed under any other license. The requirement for fonts to
77
remain under this license does not apply to any document created
78
using the Font Software.
79
80
TERMINATION
81
This license becomes null and void if any of the above conditions are
82
not met.
83
84
DISCLAIMER
85
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
OTHER DEALINGS IN THE FONT SOFTWARE.
194
A licenses/fonts/SOURCE-SERIF-PRO.md
1
Copyright 2014-2019 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries.
2
3
This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
5
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
6
7
8
-----------------------------------------------------------
9
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
-----------------------------------------------------------
11
12
PREAMBLE
13
The goals of the Open Font License (OFL) are to stimulate worldwide
14
development of collaborative font projects, to support the font creation
15
efforts of academic and linguistic communities, and to provide a free and
16
open framework in which fonts may be shared and improved in partnership
17
with others.
18
19
The OFL allows the licensed fonts to be used, studied, modified and
20
redistributed freely as long as they are not sold by themselves. The
21
fonts, including any derivative works, can be bundled, embedded,
22
redistributed and/or sold with any software provided that any reserved
23
names are not used by derivative works. The fonts and derivatives,
24
however, cannot be released under any other type of license. The
25
requirement for fonts to remain under this license does not apply
26
to any document created using the fonts or their derivatives.
27
28
DEFINITIONS
29
"Font Software" refers to the set of files released by the Copyright
30
Holder(s) under this license and clearly marked as such. This may
31
include source files, build scripts and documentation.
32
33
"Reserved Font Name" refers to any names specified as such after the
34
copyright statement(s).
35
36
"Original Version" refers to the collection of Font Software components as
37
distributed by the Copyright Holder(s).
38
39
"Modified Version" refers to any derivative made by adding to, deleting,
40
or substituting -- in part or in whole -- any of the components of the
41
Original Version, by changing formats or by porting the Font Software to a
42
new environment.
43
44
"Author" refers to any designer, engineer, programmer, technical
45
writer or other person who contributed to the Font Software.
46
47
PERMISSION & CONDITIONS
48
Permission is hereby granted, free of charge, to any person obtaining
49
a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
redistribute, and sell modified and unmodified copies of the Font
51
Software, subject to the following conditions:
52
53
1) Neither the Font Software nor any of its individual components,
54
in Original or Modified Versions, may be sold by itself.
55
56
2) Original or Modified Versions of the Font Software may be bundled,
57
redistributed and/or sold with any software, provided that each copy
58
contains the above copyright notice and this license. These can be
59
included either as stand-alone text files, human-readable headers or
60
in the appropriate machine-readable metadata fields within text or
61
binary files as long as those fields can be easily viewed by the user.
62
63
3) No Modified Version of the Font Software may use the Reserved Font
64
Name(s) unless explicit written permission is granted by the corresponding
65
Copyright Holder. This restriction only applies to the primary font name as
66
presented to the users.
67
68
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
Software shall not be used to promote, endorse or advertise any
70
Modified Version, except to acknowledge the contribution(s) of the
71
Copyright Holder(s) and the Author(s) or with their explicit written
72
permission.
73
74
5) The Font Software, modified or unmodified, in part or in whole,
75
must be distributed entirely under this license, and must not be
76
distributed under any other license. The requirement for fonts to
77
remain under this license does not apply to any document created
78
using the Font Software.
79
80
TERMINATION
81
This license becomes null and void if any of the above conditions are
82
not met.
83
84
DISCLAIMER
85
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
OTHER DEALINGS IN THE FONT SOFTWARE.
194
D licenses/fonts/VOLLKORN.txt
1
Copyright 2017 The Vollkorn Project Authors (https://github.com/FAlthausen/Vollkorn-Typeface)
2
3
This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
This license is copied below, and is also available with a FAQ at:
5
http://scripts.sil.org/OFL
6
7
8
-----------------------------------------------------------
9
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
-----------------------------------------------------------
11
12
PREAMBLE
13
The goals of the Open Font License (OFL) are to stimulate worldwide
14
development of collaborative font projects, to support the font creation
15
efforts of academic and linguistic communities, and to provide a free and
16
open framework in which fonts may be shared and improved in partnership
17
with others.
18
19
The OFL allows the licensed fonts to be used, studied, modified and
20
redistributed freely as long as they are not sold by themselves. The
21
fonts, including any derivative works, can be bundled, embedded, 
22
redistributed and/or sold with any software provided that any reserved
23
names are not used by derivative works. The fonts and derivatives,
24
however, cannot be released under any other type of license. The
25
requirement for fonts to remain under this license does not apply
26
to any document created using the fonts or their derivatives.
27
28
DEFINITIONS
29
"Font Software" refers to the set of files released by the Copyright
30
Holder(s) under this license and clearly marked as such. This may
31
include source files, build scripts and documentation.
32
33
"Reserved Font Name" refers to any names specified as such after the
34
copyright statement(s).
35
36
"Original Version" refers to the collection of Font Software components as
37
distributed by the Copyright Holder(s).
38
39
"Modified Version" refers to any derivative made by adding to, deleting,
40
or substituting -- in part or in whole -- any of the components of the
41
Original Version, by changing formats or by porting the Font Software to a
42
new environment.
43
44
"Author" refers to any designer, engineer, programmer, technical
45
writer or other person who contributed to the Font Software.
46
47
PERMISSION & CONDITIONS
48
Permission is hereby granted, free of charge, to any person obtaining
49
a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
redistribute, and sell modified and unmodified copies of the Font
51
Software, subject to the following conditions:
52
53
1) Neither the Font Software nor any of its individual components,
54
in Original or Modified Versions, may be sold by itself.
55
56
2) Original or Modified Versions of the Font Software may be bundled,
57
redistributed and/or sold with any software, provided that each copy
58
contains the above copyright notice and this license. These can be
59
included either as stand-alone text files, human-readable headers or
60
in the appropriate machine-readable metadata fields within text or
61
binary files as long as those fields can be easily viewed by the user.
62
63
3) No Modified Version of the Font Software may use the Reserved Font
64
Name(s) unless explicit written permission is granted by the corresponding
65
Copyright Holder. This restriction only applies to the primary font name as
66
presented to the users.
67
68
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
Software shall not be used to promote, endorse or advertise any
70
Modified Version, except to acknowledge the contribution(s) of the
71
Copyright Holder(s) and the Author(s) or with their explicit written
72
permission.
73
74
5) The Font Software, modified or unmodified, in part or in whole,
75
must be distributed entirely under this license, and must not be
76
distributed under any other license. The requirement for fonts to
77
remain under this license does not apply to any document created
78
using the Font Software.
79
80
TERMINATION
81
This license becomes null and void if any of the above conditions are
82
not met.
83
84
DISCLAIMER
85
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
OTHER DEALINGS IN THE FONT SOFTWARE.
941
M src/main/java/com/keenwrite/AbstractFileFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite;
3
4
import com.keenwrite.io.FileType;
295
306
import java.nio.file.Path;
317
328
import static com.keenwrite.Constants.GLOB_PREFIX_FILE;
33
import static com.keenwrite.Constants.SETTINGS;
34
import static com.keenwrite.FileType.UNKNOWN;
9
import static com.keenwrite.Constants.sSettings;
10
import static com.keenwrite.io.FileType.UNKNOWN;
3511
import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate;
3612
...
5127
   */
5228
  public static FileType lookup( final Path path ) {
29
    assert path != null;
30
5331
    return lookup( path, GLOB_PREFIX_FILE );
5432
  }
...
6543
    assert prefix != null;
6644
67
    final var keys = SETTINGS.getKeys( prefix );
45
    final var keys = sSettings.getKeys( prefix );
6846
6947
    var found = false;
7048
    var fileType = UNKNOWN;
7149
7250
    while( keys.hasNext() && !found ) {
7351
      final var key = keys.next();
74
      final var patterns = SETTINGS.getStringSettingList( key );
52
      final var patterns = sSettings.getStringSettingList( key );
7553
      final var predicate = createFileTypePredicate( patterns );
7654
7755
      if( found = predicate.test( path.toFile() ) ) {
78
        // Remove the EXTENSIONS_PREFIX to get the filename extension mapped
56
        // Remove the EXTENSIONS_PREFIX to get the file name extension mapped
7957
        // to a standard name (as defined in the settings.properties file).
80
        final String suffix = key.replace( prefix + ".", "" );
58
        final String suffix = key.replace( prefix + '.', "" );
8159
        fileType = FileType.from( suffix );
8260
      }
M src/main/java/com/keenwrite/Bootstrap.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite;
293
30
import java.io.IOException;
314
import java.util.Properties;
325
...
4821
             Constants.class.getResourceAsStream( "/bootstrap.properties" ) ) {
4922
      BOOTSTRAP.load( stream );
50
    } catch( final IOException ignored ) {
23
    } catch( final Exception ignored ) {
5124
      // Bootstrap properties cannot be found, throw in the towel.
5225
    }
M src/main/java/com/keenwrite/Constants.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite;
293
304
import com.keenwrite.service.Settings;
315
import javafx.scene.image.Image;
326
7
import java.io.File;
8
import java.nio.charset.Charset;
339
import java.nio.file.Path;
34
import java.nio.file.Paths;
10
import java.util.ArrayList;
11
import java.util.List;
12
import java.util.Locale;
3513
3614
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
15
import static com.keenwrite.preferences.LocaleScripts.withScript;
16
import static java.io.File.separator;
3717
import static java.lang.String.format;
18
import static java.lang.System.getProperty;
3819
3920
/**
...
5132
   */
5233
  public static final String PATH_PROPERTIES_SETTINGS =
53
      format( "/com/%s/settings.properties", APP_TITLE_LOWERCASE );
34
    format( "/com/%s/settings.properties", APP_TITLE_LOWERCASE );
5435
5536
  /**
5637
   * The {@link Settings} uses {@link #PATH_PROPERTIES_SETTINGS}.
5738
   */
58
  public static final Settings SETTINGS = Services.load( Settings.class );
39
  public static final Settings sSettings = Services.load( Settings.class );
5940
60
  public static final String DEFINITION_NAME = "variables.yaml";
41
  public static final double WINDOW_X_DEFAULT = 0;
42
  public static final double WINDOW_Y_DEFAULT = 0;
43
  public static final double WINDOW_W_DEFAULT = 1200;
44
  public static final double WINDOW_H_DEFAULT = 800;
45
46
  public static final File DOCUMENT_DEFAULT = getFile( "document" );
47
  public static final File DEFINITION_DEFAULT = getFile( "definition" );
6148
6249
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
6350
64
  // Prevent double events when updating files on Linux (save and timestamp).
51
  /**
52
   * Prevent double events when updating files on Linux (save and timestamp).
53
   */
6554
  public static final int APP_WATCHDOG_TIMEOUT = get(
66
      "application.watchdog.timeout", 200 );
55
    "application.watchdog.timeout", 200 );
6756
68
  public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" );
6957
  public static final String STYLESHEET_MARKDOWN = get(
70
      "file.stylesheet.markdown" );
58
    "file.stylesheet.markdown" );
59
  public static final String STYLESHEET_MARKDOWN_LOCALE =
60
    "file.stylesheet.markdown.locale";
7161
  public static final String STYLESHEET_PREVIEW = get(
72
      "file.stylesheet.preview" );
62
    "file.stylesheet.preview" );
63
  public static final String STYLESHEET_PREVIEW_LOCALE =
64
    "file.stylesheet.preview.locale";
65
  public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" );
7366
74
  public static final String FILE_LOGO_16 = get( "file.logo.16" );
75
  public static final String FILE_LOGO_32 = get( "file.logo.32" );
76
  public static final String FILE_LOGO_128 = get( "file.logo.128" );
77
  public static final String FILE_LOGO_256 = get( "file.logo.256" );
78
  public static final String FILE_LOGO_512 = get( "file.logo.512" );
67
  public static final List<Image> LOGOS = createImages(
68
    "file.logo.16",
69
    "file.logo.32",
70
    "file.logo.128",
71
    "file.logo.256",
72
    "file.logo.512"
73
  );
7974
80
  public static final Image ICON_DIALOG = new Image( FILE_LOGO_32 );
75
  public static final Image ICON_DIALOG = LOGOS.get( 1 );
8176
82
  public static final String PREFS_ROOT = get( "preferences.root" );
83
  public static final String PREFS_STATE = get( "preferences.root.state" );
77
  public static final String FILE_PREFERENCES = getPreferencesFilename();
8478
8579
  /**
86
   * Refer to filename extension settings in the configuration file. Do not
87
   * terminate these prefixes with a period.
80
   * Refer to file name extension settings in the configuration file. Do not
81
   * terminate with a period.
8882
   */
8983
  public static final String GLOB_PREFIX_FILE = "file.ext";
90
  public static final String GLOB_PREFIX_DEFINITION =
91
      "definition." + GLOB_PREFIX_FILE;
9284
9385
  /**
...
10395
  public static final String STATUS_PARSE_ERROR = "Main.status.error.parse";
10496
  public static final String STATUS_DEFINITION_BLANK =
105
      "Main.status.error.def.blank";
97
    "Main.status.error.def.blank";
10698
  public static final String STATUS_DEFINITION_EMPTY =
107
      "Main.status.error.def.empty";
99
    "Main.status.error.def.empty";
108100
109101
  /**
110102
   * One parameter: the word under the cursor that could not be found.
111103
   */
112104
  public static final String STATUS_DEFINITION_MISSING =
113
      "Main.status.error.def.missing";
105
    "Main.status.error.def.missing";
114106
115107
  /**
116108
   * Used when creating flat maps relating to resolved variables.
117109
   */
118
  public static final int DEFAULT_MAP_SIZE = 64;
110
  public static final int MAP_SIZE_DEFAULT = 128;
119111
120112
  /**
121113
   * Default image extension order to use when scanning.
122114
   */
123115
  public static final String PERSIST_IMAGES_DEFAULT =
124
      get( "file.ext.image.order" );
116
    get( "file.ext.image.order" );
125117
126118
  /**
127119
   * Default working directory to use for R startup script.
128120
   */
129
  public static final String USER_DIRECTORY = System.getProperty( "user.dir" );
121
  public static final File USER_DIRECTORY =
122
    new File( System.getProperty( "user.dir" ) );
130123
131124
  /**
132125
   * Default path to use for an untitled (pathless) file.
133126
   */
134
  public static final Path DEFAULT_DIRECTORY = Paths.get( USER_DIRECTORY );
127
  public static final Path DEFAULT_DIRECTORY = USER_DIRECTORY.toPath();
128
129
  /**
130
   * Default character set to use when reading/writing files.
131
   */
132
  public static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
135133
136134
  /**
...
169167
   * Default text editor font size, in points.
170168
   */
171
  public static final float FONT_SIZE_EDITOR = 12f;
169
  public static final float FONT_SIZE_EDITOR_DEFAULT = 12f;
170
171
  /**
172
   * Default preview font size, in points.
173
   */
174
  public static final float FONT_SIZE_PREVIEW_DEFAULT = 13f;
175
176
  /**
177
   * Default locale for font loading, including ISO 15924 alpha-4 script code.
178
   */
179
  public static final Locale LOCALE_DEFAULT = withScript( Locale.getDefault() );
172180
173181
  /**
174182
   * Default identifier to use for synchronized scrolling.
175183
   */
176
  public static String CARET_ID = "caret";
184
  public static final String CARET_ID = "caret";
177185
178186
  /**
179187
   * Prevent instantiation.
180188
   */
181189
  private Constants() {
182190
  }
183191
184192
  private static String get( final String key ) {
185
    return SETTINGS.getSetting( key, "" );
193
    return sSettings.getSetting( key, "" );
186194
  }
187195
188
  @SuppressWarnings("SameParameterValue")
196
  @SuppressWarnings( "SameParameterValue" )
189197
  private static int get( final String key, final int defaultValue ) {
190
    return SETTINGS.getSetting( key, defaultValue );
198
    return sSettings.getSetting( key, defaultValue );
199
  }
200
201
  /**
202
   * Returns a default {@link File} instance based on the given key suffix.
203
   *
204
   * @param suffix Appended to {@code "file.default."}.
205
   * @return A new {@link File} instance that references the settings file name.
206
   */
207
  private static File getFile( final String suffix ) {
208
    return new File( get( "file.default." + suffix ) );
209
  }
210
211
  /**
212
   * Returns the equivalent of {@code $HOME/.filename.xml}.
213
   */
214
  private static String getPreferencesFilename() {
215
    return format(
216
      "%s%s.%s.xml",
217
      getProperty( "user.home" ),
218
      separator,
219
      APP_TITLE_LOWERCASE
220
    );
221
  }
222
223
  /**
224
   * Converts the given file names to images, such as application icons.
225
   *
226
   * @param keys The file names to convert to images.
227
   * @return The images loaded from the file name references.
228
   */
229
  private static List<Image> createImages( final String... keys ) {
230
    final List<Image> images = new ArrayList<>( keys.length );
231
232
    for( final var key : keys ) {
233
      images.add( new Image( get( key ) ) );
234
    }
235
236
    return images;
191237
  }
192238
}
A src/main/java/com/keenwrite/DefinitionNameInjector.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.editors.TextDefinition;
5
import com.keenwrite.editors.TextEditor;
6
import com.keenwrite.editors.definition.DefinitionTreeItem;
7
import com.keenwrite.sigils.SigilOperator;
8
9
import static com.keenwrite.Constants.*;
10
import static com.keenwrite.StatusBarNotifier.clue;
11
12
/**
13
 * Provides the logic for injecting variable names within the editor.
14
 */
15
public final class DefinitionNameInjector {
16
17
  /**
18
   * Prevent instantiation.
19
   */
20
  private DefinitionNameInjector() {
21
  }
22
23
  /**
24
   * Find a node that matches the current word and substitute the definition
25
   * reference.
26
   */
27
  public static void autoinsert(
28
    final TextEditor editor,
29
    final TextDefinition definitions,
30
    final SigilOperator operator ) {
31
    try {
32
      if( definitions.isEmpty() ) {
33
        clue( STATUS_DEFINITION_EMPTY );
34
      }
35
      else {
36
        final var indexes = editor.getCaretWord();
37
        final var word = editor.getText( indexes );
38
39
        if( word.isBlank() ) {
40
          clue( STATUS_DEFINITION_BLANK );
41
        }
42
        else {
43
          final var leaf = findLeaf( definitions, word );
44
45
          if( leaf == null ) {
46
            clue( STATUS_DEFINITION_MISSING, word );
47
          }
48
          else {
49
            editor.replaceText( indexes, operator.entoken( leaf.toPath() ) );
50
            definitions.expand( leaf );
51
          }
52
        }
53
      }
54
    } catch( final Exception ignored ) {
55
      clue( STATUS_DEFINITION_BLANK );
56
    }
57
  }
58
59
  /**
60
   * Looks for the given word, matching first by exact, next by a starts-with
61
   * condition with diacritics replaced, then by containment.
62
   *
63
   * @param word Match the word by: exact, beginning, containment, or other.
64
   */
65
  @SuppressWarnings("ConstantConditions")
66
  private static DefinitionTreeItem<String> findLeaf(
67
    final TextDefinition definition, final String word ) {
68
    assert word != null;
69
70
    DefinitionTreeItem<String> leaf = null;
71
72
    leaf = leaf == null ? definition.findLeafExact( word ) : leaf;
73
    leaf = leaf == null ? definition.findLeafStartsWith( word ) : leaf;
74
    leaf = leaf == null ? definition.findLeafContains( word ) : leaf;
75
    leaf = leaf == null ? definition.findLeafContainsNoCase( word ) : leaf;
76
77
    return leaf;
78
  }
79
}
180
M src/main/java/com/keenwrite/ExportFormat.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite;
293
304
import java.io.File;
5
import java.nio.file.Path;
316
327
import static org.apache.commons.io.FilenameUtils.removeExtension;
...
6944
7045
  /**
71
   * Returns the given file renamed with the extension that matches this
72
   * {@link ExportFormat} extension.
46
   * Returns the given {@link File} with its extension replaced by one that
47
   * matches this {@link ExportFormat} extension.
7348
   *
74
   * @param file The file to rename.
75
   * @return The renamed version of the given file.
49
   * @param file The file to perform an extension swap.
50
   * @return The given file with its extension replaced.
7651
   */
7752
  public File toExportFilename( final File file ) {
7853
    return new File( removeExtension( file.getName() ) + mExtension );
54
  }
55
56
  /**
57
   * Delegates to {@link #toExportFilename(File)} after converting the given
58
   * {@link Path} to an instance of {@link File}.
59
   *
60
   * @param path The {@link Path} to convert to a {@link File}.
61
   * @return The given path with its extension replaced.
62
   */
63
  public File toExportFilename( final Path path ) {
64
    return toExportFilename( path.toFile() );
7965
  }
8066
}
D src/main/java/com/keenwrite/FileEditorTab.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * Redistribution and use in source and binary forms, with or without
5
 * modification, are permitted provided that the following conditions are met:
6
 *
7
 *  o Redistributions of source code must retain the above copyright
8
 *    notice, this list of conditions and the following disclaimer.
9
 *
10
 *  o Redistributions in binary form must reproduce the above copyright
11
 *    notice, this list of conditions and the following disclaimer in the
12
 *    documentation and/or other materials provided with the distribution.
13
 *
14
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
 */
26
package com.keenwrite;
27
28
import com.keenwrite.editors.EditorPane;
29
import com.keenwrite.editors.markdown.MarkdownEditorPane;
30
import com.keenwrite.processors.Processor;
31
import com.keenwrite.processors.markdown.CaretPosition;
32
import com.keenwrite.service.events.Notification;
33
import com.keenwrite.service.events.Notifier;
34
import javafx.beans.binding.Bindings;
35
import javafx.beans.property.BooleanProperty;
36
import javafx.beans.property.ReadOnlyBooleanProperty;
37
import javafx.beans.property.ReadOnlyBooleanWrapper;
38
import javafx.beans.property.SimpleBooleanProperty;
39
import javafx.beans.value.ChangeListener;
40
import javafx.event.Event;
41
import javafx.event.EventHandler;
42
import javafx.event.EventType;
43
import javafx.scene.Scene;
44
import javafx.scene.control.Tab;
45
import javafx.scene.control.Tooltip;
46
import javafx.scene.text.Text;
47
import javafx.stage.Window;
48
import org.fxmisc.flowless.VirtualizedScrollPane;
49
import org.fxmisc.richtext.StyleClassedTextArea;
50
import org.fxmisc.undo.UndoManager;
51
import org.jetbrains.annotations.NotNull;
52
import org.mozilla.universalchardet.UniversalDetector;
53
54
import java.io.File;
55
import java.nio.charset.Charset;
56
import java.nio.file.Files;
57
import java.nio.file.Path;
58
59
import static com.keenwrite.Messages.get;
60
import static com.keenwrite.StatusBarNotifier.clue;
61
import static com.keenwrite.StatusBarNotifier.getNotifier;
62
import static java.nio.charset.StandardCharsets.UTF_8;
63
import static java.util.Locale.ENGLISH;
64
import static javafx.application.Platform.runLater;
65
66
/**
67
 * Editor for a single file.
68
 */
69
public final class FileEditorTab extends Tab {
70
71
  private final MarkdownEditorPane mEditorPane = new MarkdownEditorPane();
72
73
  private final ReadOnlyBooleanWrapper mModified = new ReadOnlyBooleanWrapper();
74
  private final BooleanProperty canUndo = new SimpleBooleanProperty();
75
  private final BooleanProperty canRedo = new SimpleBooleanProperty();
76
77
  /**
78
   * Character encoding used by the file (or default encoding if none found).
79
   */
80
  private Charset mEncoding = UTF_8;
81
82
  /**
83
   * File to load into the editor.
84
   */
85
  private Path mPath;
86
87
  /**
88
   * Dynamically updated position of the caret within the text editor.
89
   */
90
  private final CaretPosition mCaretPosition;
91
92
  public FileEditorTab( final Path path ) {
93
    setPath( path );
94
95
    mModified.addListener( ( observable, oldPath, newPath ) -> updateTab() );
96
97
    setOnSelectionChanged( e -> {
98
      if( isSelected() ) {
99
        runLater( this::activated );
100
        requestFocus();
101
      }
102
    } );
103
104
    mCaretPosition = createCaretPosition( getEditor() );
105
  }
106
107
  private CaretPosition createCaretPosition(
108
      final StyleClassedTextArea editor ) {
109
    final var propParaIndex = editor.currentParagraphProperty();
110
    final var propParagraphs = editor.getParagraphs();
111
    final var propParaOffset = editor.caretColumnProperty();
112
    final var propTextOffset = editor.caretPositionProperty();
113
114
    return CaretPosition
115
        .builder()
116
        .with( CaretPosition.Mutator::setParagraph, propParaIndex )
117
        .with( CaretPosition.Mutator::setParagraphs, propParagraphs )
118
        .with( CaretPosition.Mutator::setParaOffset, propParaOffset )
119
        .with( CaretPosition.Mutator::setTextOffset, propTextOffset )
120
        .build();
121
  }
122
123
  private void updateTab() {
124
    setText( getTabTitle() );
125
    setGraphic( getModifiedMark() );
126
    setTooltip( getTabTooltip() );
127
  }
128
129
  /**
130
   * Returns the base filename (without the directory names).
131
   *
132
   * @return The untitled text if the path hasn't been set.
133
   */
134
  private String getTabTitle() {
135
    return getPath().getFileName().toString();
136
  }
137
138
  /**
139
   * Returns the full filename represented by the path.
140
   *
141
   * @return The untitled text if the path hasn't been set.
142
   */
143
  private Tooltip getTabTooltip() {
144
    final Path filePath = getPath();
145
    return new Tooltip( filePath == null ? "" : filePath.toString() );
146
  }
147
148
  /**
149
   * Returns a marker to indicate whether the file has been modified.
150
   *
151
   * @return "*" when the file has changed; otherwise null.
152
   */
153
  private Text getModifiedMark() {
154
    return isModified() ? new Text( "*" ) : null;
155
  }
156
157
  /**
158
   * Called when the user switches tab.
159
   */
160
  private void activated() {
161
    // Tab is closed or no longer active.
162
    if( getTabPane() == null || !isSelected() ) {
163
      return;
164
    }
165
166
    // If the tab is devoid of content, load it.
167
    if( getContent() == null ) {
168
      readFile();
169
      initLayout();
170
      initUndoManager();
171
    }
172
  }
173
174
  private void initLayout() {
175
    setContent( getScrollPane() );
176
  }
177
178
  /**
179
   * Tracks undo requests, but can only be called <em>after</em> load.
180
   */
181
  private void initUndoManager() {
182
    final UndoManager<?> undoManager = getUndoManager();
183
    undoManager.forgetHistory();
184
185
    // Bind the editor undo manager to the properties.
186
    mModified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
187
    canUndo.bind( undoManager.undoAvailableProperty() );
188
    canRedo.bind( undoManager.redoAvailableProperty() );
189
  }
190
191
  private void requestFocus() {
192
    getEditorPane().requestFocus();
193
  }
194
195
  /**
196
   * Searches from the caret position forward for the given string.
197
   *
198
   * @param needle The text string to match.
199
   */
200
  public void searchNext( final String needle ) {
201
    final String haystack = getEditorText();
202
    int index = haystack.indexOf( needle, getCaretTextOffset() );
203
204
    // Wrap around.
205
    if( index == -1 ) {
206
      index = haystack.indexOf( needle );
207
    }
208
209
    if( index >= 0 ) {
210
      setCaretTextOffset( index );
211
      getEditor().selectRange( index, index + needle.length() );
212
    }
213
  }
214
215
  /**
216
   * Gets a reference to the scroll pane that houses the editor.
217
   *
218
   * @return The editor's scroll pane, containing a vertical scrollbar.
219
   */
220
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
221
    return getEditorPane().getScrollPane();
222
  }
223
224
  /**
225
   * Returns an instance of {@link CaretPosition} that contains information
226
   * about the caret, including the offset into the text, the paragraph into
227
   * the text, maximum number of paragraphs, and more. This allows the main
228
   * application and the {@link Processor} instances to get the current
229
   * caret position.
230
   *
231
   * @return The current values for the caret's position within the editor.
232
   */
233
  public CaretPosition getCaretPosition() {
234
    return mCaretPosition;
235
  }
236
237
  /**
238
   * Returns the index into the text where the caret blinks happily away.
239
   *
240
   * @return A number from 0 to the editor's document text length.
241
   */
242
  private int getCaretTextOffset() {
243
    return getEditor().getCaretPosition();
244
  }
245
246
  /**
247
   * Moves the caret to a given offset.
248
   *
249
   * @param offset The new caret offset.
250
   */
251
  private void setCaretTextOffset( final int offset ) {
252
    getEditor().moveTo( offset );
253
    getEditor().requestFollowCaret();
254
  }
255
256
  /**
257
   * Returns the text area associated with this tab.
258
   *
259
   * @return A text editor.
260
   */
261
  private StyleClassedTextArea getEditor() {
262
    return getEditorPane().getEditor();
263
  }
264
265
  /**
266
   * Returns true if the given path exactly matches this tab's path.
267
   *
268
   * @param check The path to compare against.
269
   * @return true The paths are the same.
270
   */
271
  public boolean isPath( final Path check ) {
272
    final Path filePath = getPath();
273
274
    return filePath != null && filePath.equals( check );
275
  }
276
277
  /**
278
   * Reads the entire file contents from the path associated with this tab.
279
   */
280
  private void readFile() {
281
    final Path path = getPath();
282
    final File file = path.toFile();
283
284
    try {
285
      if( file.exists() ) {
286
        if( file.canWrite() && file.canRead() ) {
287
          final EditorPane pane = getEditorPane();
288
          pane.setText( asString( Files.readAllBytes( path ) ) );
289
          pane.scrollToTop();
290
        }
291
        else {
292
          final String msg = get( "FileEditor.loadFailed.reason.permissions" );
293
          clue( "FileEditor.loadFailed.message", file.toString(), msg );
294
        }
295
      }
296
    } catch( final Exception ex ) {
297
      clue( ex );
298
    }
299
  }
300
301
  /**
302
   * Saves the entire file contents from the path associated with this tab.
303
   *
304
   * @return true The file has been saved.
305
   */
306
  public boolean save() {
307
    try {
308
      final EditorPane editor = getEditorPane();
309
      Files.write( getPath(), asBytes( editor.getText() ) );
310
      editor.getUndoManager().mark();
311
      return true;
312
    } catch( final Exception ex ) {
313
      return popupAlert(
314
          "FileEditor.saveFailed.title",
315
          "FileEditor.saveFailed.message",
316
          ex
317
      );
318
    }
319
  }
320
321
  /**
322
   * Creates an alert dialog and waits for it to close.
323
   *
324
   * @param titleKey   Resource bundle key for the alert dialog title.
325
   * @param messageKey Resource bundle key for the alert dialog message.
326
   * @param e          The unexpected happening.
327
   * @return false
328
   */
329
  @SuppressWarnings("SameParameterValue")
330
  private boolean popupAlert(
331
      final String titleKey, final String messageKey, final Exception e ) {
332
    final Notifier service = getNotifier();
333
    final Path filePath = getPath();
334
335
    final Notification message = service.createNotification(
336
        get( titleKey ),
337
        get( messageKey ),
338
        filePath == null ? "" : filePath,
339
        e.getMessage()
340
    );
341
342
    try {
343
      service.createError( getWindow(), message ).showAndWait();
344
    } catch( final Exception ex ) {
345
      clue( ex );
346
    }
347
348
    return false;
349
  }
350
351
  private Window getWindow() {
352
    final Scene scene = getEditorPane().getScene();
353
354
    if( scene == null ) {
355
      throw new UnsupportedOperationException( "No scene window available" );
356
    }
357
358
    return scene.getWindow();
359
  }
360
361
  /**
362
   * Returns a best guess at the file encoding. If the encoding could not be
363
   * detected, this will return the default charset for the JVM.
364
   *
365
   * @param bytes The bytes to perform character encoding detection.
366
   * @return The character encoding.
367
   */
368
  private Charset detectEncoding( final byte[] bytes ) {
369
    final var detector = new UniversalDetector( null );
370
    detector.handleData( bytes, 0, bytes.length );
371
    detector.dataEnd();
372
373
    final String charset = detector.getDetectedCharset();
374
375
    return charset == null
376
        ? Charset.defaultCharset()
377
        : Charset.forName( charset.toUpperCase( ENGLISH ) );
378
  }
379
380
  /**
381
   * Converts the given string to an array of bytes using the encoding that was
382
   * originally detected (if any) and associated with this file.
383
   *
384
   * @param text The text to convert into the original file encoding.
385
   * @return A series of bytes ready for writing to a file.
386
   */
387
  private byte[] asBytes( final String text ) {
388
    return text.getBytes( getEncoding() );
389
  }
390
391
  /**
392
   * Converts the given bytes into a Java String. This will call setEncoding
393
   * with the encoding detected by the CharsetDetector.
394
   *
395
   * @param text The text of unknown character encoding.
396
   * @return The text, in its auto-detected encoding, as a String.
397
   */
398
  private String asString( final byte[] text ) {
399
    setEncoding( detectEncoding( text ) );
400
    return new String( text, getEncoding() );
401
  }
402
403
  /**
404
   * Returns the path to the file being edited in this tab.
405
   *
406
   * @return A non-null instance.
407
   */
408
  public Path getPath() {
409
    return mPath;
410
  }
411
412
  /**
413
   * Sets the path to a file for editing and then updates the tab with the
414
   * file contents.
415
   *
416
   * @param path A non-null instance.
417
   */
418
  public void setPath( final Path path ) {
419
    assert path != null;
420
    mPath = path;
421
422
    updateTab();
423
  }
424
425
  public boolean isModified() {
426
    return mModified.get();
427
  }
428
429
  ReadOnlyBooleanProperty modifiedProperty() {
430
    return mModified.getReadOnlyProperty();
431
  }
432
433
  BooleanProperty canUndoProperty() {
434
    return this.canUndo;
435
  }
436
437
  BooleanProperty canRedoProperty() {
438
    return this.canRedo;
439
  }
440
441
  private UndoManager<?> getUndoManager() {
442
    return getEditorPane().getUndoManager();
443
  }
444
445
  /**
446
   * Forwards to the editor pane's listeners for text change events.
447
   *
448
   * @param listener The listener to notify when the text changes.
449
   */
450
  public void addTextChangeListener( final ChangeListener<String> listener ) {
451
    getEditorPane().addTextChangeListener( listener );
452
  }
453
454
  /**
455
   * Forwards to the editor pane's listeners for caret change events.
456
   *
457
   * @param listener Notified when the caret position changes.
458
   */
459
  public void addCaretPositionListener(
460
      final ChangeListener<? super Integer> listener ) {
461
    getEditorPane().addCaretPositionListener( listener );
462
  }
463
464
  public <T extends Event> void addEventFilter(
465
      final EventType<T> eventType,
466
      final EventHandler<? super T> eventFilter ) {
467
    getEditor().addEventFilter( eventType, eventFilter );
468
  }
469
470
  /**
471
   * Forwards the request to the editor pane.
472
   *
473
   * @return The text to process.
474
   */
475
  public String getEditorText() {
476
    return getEditorPane().getText();
477
  }
478
479
  /**
480
   * Returns the editor pane, or creates one if it doesn't yet exist.
481
   *
482
   * @return The editor pane, never null.
483
   */
484
  @NotNull
485
  public MarkdownEditorPane getEditorPane() {
486
    return mEditorPane;
487
  }
488
489
  /**
490
   * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been
491
   * determined.
492
   *
493
   * @return The file encoding or UTF-8 if unknown.
494
   */
495
  private Charset getEncoding() {
496
    return mEncoding;
497
  }
498
499
  private void setEncoding( final Charset encoding ) {
500
    assert encoding != null;
501
    mEncoding = encoding;
502
  }
503
504
  /**
505
   * Returns the tab title, without any modified indicators.
506
   *
507
   * @return The tab title.
508
   */
509
  @Override
510
  public String toString() {
511
    return getTabTitle();
512
  }
513
}
5141
D src/main/java/com/keenwrite/FileEditorTabPane.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
import com.keenwrite.service.Options;
31
import com.keenwrite.service.Settings;
32
import com.keenwrite.service.events.Notification;
33
import com.keenwrite.service.events.Notifier;
34
import com.keenwrite.util.Utils;
35
import javafx.beans.property.ReadOnlyBooleanProperty;
36
import javafx.beans.property.ReadOnlyBooleanWrapper;
37
import javafx.beans.property.ReadOnlyObjectProperty;
38
import javafx.beans.property.ReadOnlyObjectWrapper;
39
import javafx.beans.value.ChangeListener;
40
import javafx.collections.ListChangeListener;
41
import javafx.collections.ObservableList;
42
import javafx.event.Event;
43
import javafx.scene.control.Alert;
44
import javafx.scene.control.ButtonType;
45
import javafx.scene.control.Tab;
46
import javafx.scene.control.TabPane;
47
import javafx.stage.FileChooser;
48
import javafx.stage.FileChooser.ExtensionFilter;
49
import javafx.stage.Window;
50
51
import java.io.File;
52
import java.nio.file.Path;
53
import java.util.ArrayList;
54
import java.util.List;
55
import java.util.Optional;
56
import java.util.concurrent.atomic.AtomicReference;
57
import java.util.prefs.Preferences;
58
import java.util.stream.Collectors;
59
60
import static com.keenwrite.Constants.GLOB_PREFIX_FILE;
61
import static com.keenwrite.Constants.SETTINGS;
62
import static com.keenwrite.FileType.*;
63
import static com.keenwrite.Messages.get;
64
import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate;
65
import static com.keenwrite.service.events.Notifier.YES;
66
67
/**
68
 * Tab pane for file editors.
69
 */
70
public final class FileEditorTabPane extends TabPane {
71
72
  private static final String FILTER_EXTENSION_TITLES =
73
      "Dialog.file.choose.filter";
74
75
  private static final Options sOptions = Services.load( Options.class );
76
  private static final Notifier sNotifier = Services.load( Notifier.class );
77
78
  private final ReadOnlyObjectWrapper<Path> mOpenDefinition =
79
      new ReadOnlyObjectWrapper<>();
80
  private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor =
81
      new ReadOnlyObjectWrapper<>();
82
  private final ReadOnlyBooleanWrapper mAnyFileEditorModified =
83
      new ReadOnlyBooleanWrapper();
84
  private final ChangeListener<Integer> mCaretPositionListener;
85
86
  /**
87
   * Constructs a new file editor tab pane.
88
   *
89
   * @param caretPositionListener  Listens for changes to caret position so
90
   *                               that the status bar can update.
91
   */
92
  public FileEditorTabPane(
93
      final ChangeListener<Integer> caretPositionListener ) {
94
    final ObservableList<Tab> tabs = getTabs();
95
96
    setFocusTraversable( false );
97
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
98
99
    addTabSelectionListener(
100
        ( tabPane, oldTab, newTab ) -> {
101
          if( newTab != null ) {
102
            mActiveFileEditor.set( (FileEditorTab) newTab );
103
          }
104
        }
105
    );
106
107
    final ChangeListener<Boolean> modifiedListener =
108
        ( observable, oldValue, newValue ) -> {
109
          for( final Tab tab : tabs ) {
110
            if( ((FileEditorTab) tab).isModified() ) {
111
              mAnyFileEditorModified.set( true );
112
              break;
113
            }
114
          }
115
        };
116
117
    tabs.addListener(
118
        (ListChangeListener<Tab>) change -> {
119
          while( change.next() ) {
120
            if( change.wasAdded() ) {
121
              change.getAddedSubList().forEach(
122
                  ( tab ) -> {
123
                    final var fet = (FileEditorTab) tab;
124
                    fet.modifiedProperty().addListener( modifiedListener );
125
                  } );
126
            }
127
            else if( change.wasRemoved() ) {
128
              change.getRemoved().forEach(
129
                  ( tab ) -> {
130
                    final var fet = (FileEditorTab) tab;
131
                    fet.modifiedProperty().removeListener( modifiedListener );
132
                  }
133
              );
134
            }
135
          }
136
137
          // Changes in the tabs may also change anyFileEditorModified property
138
          // (e.g. closed modified file)
139
          modifiedListener.changed( null, null, null );
140
        }
141
    );
142
143
    mCaretPositionListener = caretPositionListener;
144
  }
145
146
  /**
147
   * Allows observers to be notified when the current file editor tab changes.
148
   *
149
   * @param listener The listener to notify of tab change events.
150
   */
151
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
152
    // Observe the tab so that when a new tab is opened or selected,
153
    // a notification is kicked off.
154
    getSelectionModel().selectedItemProperty().addListener( listener );
155
  }
156
157
  /**
158
   * Returns the tab that has keyboard focus.
159
   *
160
   * @return A non-null instance.
161
   */
162
  public FileEditorTab getActiveFileEditor() {
163
    return mActiveFileEditor.get();
164
  }
165
166
  /**
167
   * Returns the property corresponding to the tab that has focus.
168
   *
169
   * @return A non-null instance.
170
   */
171
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
172
    return mActiveFileEditor.getReadOnlyProperty();
173
  }
174
175
  /**
176
   * Property that can answer whether the text has been modified.
177
   *
178
   * @return A non-null instance, true meaning the content has not been saved.
179
   */
180
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
181
    return mAnyFileEditorModified.getReadOnlyProperty();
182
  }
183
184
  /**
185
   * Creates a new editor instance from the given path.
186
   *
187
   * @param path The file to open.
188
   * @return A non-null instance.
189
   */
190
  private FileEditorTab createFileEditor( final Path path ) {
191
    assert path != null;
192
193
    final FileEditorTab tab = new FileEditorTab( path );
194
195
    tab.setOnCloseRequest( e -> {
196
      if( !canCloseEditor( tab ) ) {
197
        e.consume();
198
      }
199
      else if( isActiveFileEditor( tab ) ) {
200
        // Prevent prompting the user to save when there are no file editor
201
        // tabs open.
202
        mActiveFileEditor.set( null );
203
      }
204
    } );
205
206
    tab.addCaretPositionListener( mCaretPositionListener );
207
208
    return tab;
209
  }
210
211
  private boolean isActiveFileEditor( final FileEditorTab tab ) {
212
    return getActiveFileEditor() == tab;
213
  }
214
215
  private Path getDefaultPath() {
216
    final String filename = getDefaultFilename();
217
    return (new File( filename )).toPath();
218
  }
219
220
  private String getDefaultFilename() {
221
    return getSettings().getSetting( "file.default", "untitled.md" );
222
  }
223
224
  /**
225
   * Called to add a new {@link FileEditorTab} to the tab pane.
226
   */
227
  void newEditor() {
228
    final FileEditorTab tab = createFileEditor( getDefaultPath() );
229
230
    getTabs().add( tab );
231
    getSelectionModel().select( tab );
232
  }
233
234
  void openFileDialog() {
235
    final FileChooser dialog = createFileChooser(
236
        "Dialog.file.choose.open.title" );
237
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
238
239
    if( files != null ) {
240
      openFiles( files );
241
    }
242
  }
243
244
  /**
245
   * Opens the files into new editors, unless one of those files was a
246
   * definition file. The definition file is loaded into the definition pane,
247
   * but only the first one selected (multiple definition files will result in a
248
   * warning).
249
   *
250
   * @param files The list of non-definition files that the were requested to
251
   *              open.
252
   */
253
  private void openFiles( final List<File> files ) {
254
    final List<String> extensions =
255
        createExtensionFilter( DEFINITION ).getExtensions();
256
    final var predicate = createFileTypePredicate( extensions );
257
258
    // The user might have opened multiple definitions files. These will
259
    // be discarded from the text editable files.
260
    final var definitions
261
        = files.stream().filter( predicate ).collect( Collectors.toList() );
262
263
    // Create a modifiable list to remove any definition files that were
264
    // opened.
265
    final var editors = new ArrayList<>( files );
266
267
    if( !editors.isEmpty() ) {
268
      saveLastDirectory( editors.get( 0 ) );
269
    }
270
271
    editors.removeAll( definitions );
272
273
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
274
    if( !editors.isEmpty() ) {
275
      openEditors( editors, 0 );
276
    }
277
278
    if( !definitions.isEmpty() ) {
279
      openDefinition( definitions.get( 0 ) );
280
    }
281
  }
282
283
  private void openEditors( final List<File> files, final int activeIndex ) {
284
    final int fileTally = files.size();
285
    final List<Tab> tabs = getTabs();
286
287
    // Close single unmodified "Untitled" tab.
288
    if( tabs.size() == 1 ) {
289
      final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
290
291
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
292
        closeEditor( fileEditor, false );
293
      }
294
    }
295
296
    for( int i = 0; i < fileTally; i++ ) {
297
      final Path path = files.get( i ).toPath();
298
299
      FileEditorTab fileEditorTab = findEditor( path );
300
301
      // Only open new files.
302
      if( fileEditorTab == null ) {
303
        fileEditorTab = createFileEditor( path );
304
        getTabs().add( fileEditorTab );
305
      }
306
307
      // Select the first file in the list.
308
      if( i == activeIndex ) {
309
        getSelectionModel().select( fileEditorTab );
310
      }
311
    }
312
  }
313
314
  /**
315
   * Returns a property that changes when a new definition file is opened.
316
   *
317
   * @return The path to a definition file that was opened.
318
   */
319
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
320
    return getOnOpenDefinitionFile().getReadOnlyProperty();
321
  }
322
323
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
324
    return mOpenDefinition;
325
  }
326
327
  /**
328
   * Called when the user has opened a definition file (using the file open
329
   * dialog box). This will replace the current set of definitions for the
330
   * active tab.
331
   *
332
   * @param definition The file to open.
333
   */
334
  private void openDefinition( final File definition ) {
335
    // TODO: Prevent reading this file twice when a new text document is opened.
336
    // (might be a matter of checking the value first).
337
    getOnOpenDefinitionFile().set( definition.toPath() );
338
  }
339
340
  /**
341
   * Called when the contents of the editor are to be saved.
342
   *
343
   * @param tab The tab containing content to save.
344
   * @return true The contents were saved (or needn't be saved).
345
   */
346
  public boolean saveEditor( final FileEditorTab tab ) {
347
    if( tab == null || !tab.isModified() ) {
348
      return true;
349
    }
350
351
    return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
352
  }
353
354
  /**
355
   * Opens the Save As dialog for the user to save the content under a new
356
   * path.
357
   *
358
   * @param tab The tab with contents to save.
359
   * @return true The contents were saved, or the tab was null.
360
   */
361
  public boolean saveEditorAs( final FileEditorTab tab ) {
362
    if( tab == null ) {
363
      return true;
364
    }
365
366
    getSelectionModel().select( tab );
367
368
    final FileChooser chooser = createFileChooser(
369
        "Dialog.file.choose.save.title" );
370
    final File file = chooser.showSaveDialog( getWindow() );
371
    if( file == null ) {
372
      return false;
373
    }
374
375
    saveLastDirectory( file );
376
    tab.setPath( file.toPath() );
377
378
    return tab.save();
379
  }
380
381
  void saveAllEditors() {
382
    for( final FileEditorTab fileEditor : getAllEditors() ) {
383
      saveEditor( fileEditor );
384
    }
385
  }
386
387
  /**
388
   * Answers whether the file has had modifications.
389
   *
390
   * @param tab THe tab to check for modifications.
391
   * @return false The file is unmodified.
392
   */
393
  @SuppressWarnings("BooleanMethodIsAlwaysInverted")
394
  boolean canCloseEditor( final FileEditorTab tab ) {
395
    final AtomicReference<Boolean> canClose = new AtomicReference<>();
396
    canClose.set( true );
397
398
    if( tab.isModified() ) {
399
      final Notification message = getNotifyService().createNotification(
400
          Messages.get( "Alert.file.close.title" ),
401
          Messages.get( "Alert.file.close.text" ),
402
          tab.getText()
403
      );
404
405
      final Alert confirmSave = getNotifyService().createConfirmation(
406
          getWindow(), message );
407
408
      final Optional<ButtonType> buttonType = confirmSave.showAndWait();
409
410
      buttonType.ifPresent(
411
          save -> canClose.set(
412
              save == YES ? saveEditor( tab ) : save == ButtonType.NO
413
          )
414
      );
415
    }
416
417
    return canClose.get();
418
  }
419
420
  boolean closeEditor( final FileEditorTab tab, final boolean save ) {
421
    if( tab == null ) {
422
      return true;
423
    }
424
425
    if( save ) {
426
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
427
      Event.fireEvent( tab, event );
428
429
      if( event.isConsumed() ) {
430
        return false;
431
      }
432
    }
433
434
    getTabs().remove( tab );
435
436
    if( tab.getOnClosed() != null ) {
437
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
438
    }
439
440
    return true;
441
  }
442
443
  boolean closeAllEditors() {
444
    final FileEditorTab[] allEditors = getAllEditors();
445
    final FileEditorTab activeEditor = getActiveFileEditor();
446
447
    // try to save active tab first because in case the user decides to cancel,
448
    // then it stays active
449
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
450
      return false;
451
    }
452
453
    // This should be called any time a tab changes.
454
    persistPreferences();
455
456
    // save modified tabs
457
    for( int i = 0; i < allEditors.length; i++ ) {
458
      final FileEditorTab fileEditor = allEditors[ i ];
459
460
      if( fileEditor == activeEditor ) {
461
        continue;
462
      }
463
464
      if( fileEditor.isModified() ) {
465
        // activate the modified tab to make its modified content visible to
466
        // the user
467
        getSelectionModel().select( i );
468
469
        if( !canCloseEditor( fileEditor ) ) {
470
          return false;
471
        }
472
      }
473
    }
474
475
    // Close all tabs.
476
    for( final FileEditorTab fileEditor : allEditors ) {
477
      if( !closeEditor( fileEditor, false ) ) {
478
        return false;
479
      }
480
    }
481
482
    return getTabs().isEmpty();
483
  }
484
485
  private FileEditorTab[] getAllEditors() {
486
    final ObservableList<Tab> tabs = getTabs();
487
    final int length = tabs.size();
488
    final FileEditorTab[] allEditors = new FileEditorTab[ length ];
489
490
    for( int i = 0; i < length; i++ ) {
491
      allEditors[ i ] = (FileEditorTab) tabs.get( i );
492
    }
493
494
    return allEditors;
495
  }
496
497
  /**
498
   * Returns the file editor tab that has the given path.
499
   *
500
   * @return null No file editor tab for the given path was found.
501
   */
502
  private FileEditorTab findEditor( final Path path ) {
503
    for( final Tab tab : getTabs() ) {
504
      final FileEditorTab fileEditor = (FileEditorTab) tab;
505
506
      if( fileEditor.isPath( path ) ) {
507
        return fileEditor;
508
      }
509
    }
510
511
    return null;
512
  }
513
514
  /**
515
   * Opens a new {@link FileChooser} at the previously selected directory.
516
   *
517
   * @param key Message key from resource bundle.
518
   * @return {@link FileChooser} GUI allowing the user to pick a file.
519
   */
520
  private FileChooser createFileChooser( final String key ) {
521
    final FileChooser fileChooser = new FileChooser();
522
523
    fileChooser.setTitle( get( key ) );
524
    fileChooser.getExtensionFilters().addAll(
525
        createExtensionFilters() );
526
527
    final String lastDirectory = getPreferences().get( "lastDirectory", null );
528
    File file = new File( (lastDirectory != null) ? lastDirectory : "." );
529
530
    if( !file.isDirectory() ) {
531
      file = new File( "." );
532
    }
533
534
    fileChooser.setInitialDirectory( file );
535
    return fileChooser;
536
  }
537
538
  private List<ExtensionFilter> createExtensionFilters() {
539
    final List<ExtensionFilter> list = new ArrayList<>();
540
541
    // TODO: Return a list of all properties that match the filter prefix.
542
    // This will allow dynamic filters to be added and removed just by
543
    // updating the properties file.
544
    list.add( createExtensionFilter( ALL ) );
545
    list.add( createExtensionFilter( SOURCE ) );
546
    list.add( createExtensionFilter( DEFINITION ) );
547
    list.add( createExtensionFilter( XML ) );
548
    return list;
549
  }
550
551
  /**
552
   * Returns a filter for file name extensions recognized by the application
553
   * that can be opened by the user.
554
   *
555
   * @param filetype Used to find the globbing pattern for extensions.
556
   * @return A filename filter suitable for use by a FileDialog instance.
557
   */
558
  private ExtensionFilter createExtensionFilter( final FileType filetype ) {
559
    final String tKey = String.format( "%s.title.%s",
560
                                       FILTER_EXTENSION_TITLES,
561
                                       filetype );
562
    final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
563
564
    return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
565
  }
566
567
  private void saveLastDirectory( final File file ) {
568
    getPreferences().put( "lastDirectory", file.getParent() );
569
  }
570
571
  public void initPreferences() {
572
    int activeIndex = 0;
573
574
    final Preferences preferences = getPreferences();
575
    final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
576
    final String activeFileName = preferences.get( "activeFile", null );
577
578
    final List<File> files = new ArrayList<>( fileNames.length );
579
580
    for( final String fileName : fileNames ) {
581
      final File file = new File( fileName );
582
583
      if( file.exists() ) {
584
        files.add( file );
585
586
        if( fileName.equals( activeFileName ) ) {
587
          activeIndex = files.size() - 1;
588
        }
589
      }
590
    }
591
592
    if( files.isEmpty() ) {
593
      newEditor();
594
    }
595
    else {
596
      openEditors( files, activeIndex );
597
    }
598
  }
599
600
  public void persistPreferences() {
601
    final var allEditors = getTabs();
602
    final List<String> fileNames = new ArrayList<>( allEditors.size() );
603
604
    for( final var tab : allEditors ) {
605
      final var fileEditor = (FileEditorTab) tab;
606
      final var filePath = fileEditor.getPath();
607
608
      if( filePath != null ) {
609
        fileNames.add( filePath.toString() );
610
      }
611
    }
612
613
    final var preferences = getPreferences();
614
    Utils.putPrefsStrings( preferences,
615
                           "file",
616
                           fileNames.toArray( new String[ 0 ] ) );
617
618
    final var activeEditor = getActiveFileEditor();
619
    final var filePath = activeEditor == null ? null : activeEditor.getPath();
620
621
    if( filePath == null ) {
622
      preferences.remove( "activeFile" );
623
    }
624
    else {
625
      preferences.put( "activeFile", filePath.toString() );
626
    }
627
  }
628
629
  private List<String> getExtensions( final String key ) {
630
    return getSettings().getStringSettingList( key );
631
  }
632
633
  private Notifier getNotifyService() {
634
    return sNotifier;
635
  }
636
637
  private Settings getSettings() {
638
    return SETTINGS;
639
  }
640
641
  private Window getWindow() {
642
    return getScene().getWindow();
643
  }
644
645
  private Preferences getPreferences() {
646
    return sOptions.getState();
647
  }
648
}
6491
D src/main/java/com/keenwrite/FileType.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
/**
31
 * Represents different file type classifications. These are high-level mappings
32
 * that correspond to the list of glob patterns found within {@code
33
 * settings.properties}.
34
 */
35
public enum FileType {
36
37
  ALL( "all" ),
38
  RMARKDOWN( "rmarkdown" ),
39
  RXML( "rxml" ),
40
  SOURCE( "source" ),
41
  DEFINITION( "definition" ),
42
  XML( "xml" ),
43
  CSV( "csv" ),
44
  JSON( "json" ),
45
  TOML( "toml" ),
46
  YAML( "yaml" ),
47
  PROPERTIES( "properties" ),
48
  UNKNOWN( "unknown" );
49
50
  private final String mType;
51
52
  /**
53
   * Default constructor for enumerated file type.
54
   *
55
   * @param type Human-readable name for the file type.
56
   */
57
  FileType( final String type ) {
58
    mType = type;
59
  }
60
61
  /**
62
   * Returns the file type that corresponds to the given string.
63
   *
64
   * @param type The string to compare against this enumeration of file types.
65
   * @return The corresponding File Type for the given string.
66
   * @throws IllegalArgumentException Type not found.
67
   */
68
  public static FileType from( final String type ) {
69
    for( final FileType fileType : FileType.values() ) {
70
      if( fileType.isType( type ) ) {
71
        return fileType;
72
      }
73
    }
74
75
    throw new IllegalArgumentException( type );
76
  }
77
78
  /**
79
   * Answers whether this file type matches the given string, case insensitive
80
   * comparison.
81
   *
82
   * @param type Presumably a file name extension to check against.
83
   * @return true The given extension corresponds to this enumerated type.
84
   */
85
  public boolean isType( final String type ) {
86
    return getType().equalsIgnoreCase( type );
87
  }
88
89
  /**
90
   * Answers whether this file type belongs to the set of file types that have
91
   * embedded R statements.
92
   *
93
   * @return {@code true} when the file type is either R Markdown or R XML.
94
   */
95
  public boolean isR() {
96
    return this == RMARKDOWN || this == RXML;
97
  }
98
99
  /**
100
   * Returns the human-readable name for the file type.
101
   *
102
   * @return A non-null instance.
103
   */
104
  private String getType() {
105
    return mType;
106
  }
107
108
  /**
109
   * Returns the lowercase version of the file name extension.
110
   *
111
   * @return The file name, in lower case.
112
   */
113
  @Override
114
  public String toString() {
115
    return getType();
116
  }
117
}
1181
M src/main/java/com/keenwrite/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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite;
293
...
3711
3812
/**
39
 * Launches the application using the {@link Main} class.
13
 * Launches the application using the {@link MainApp} class.
4014
 *
4115
 * <p>
...
5024
   * @param args Command-line arguments.
5125
   */
52
  public static void main( final String[] args ) throws IOException {
26
  public static void main( final String[] args ) {
5327
    showAppInfo();
54
    Main.main( args );
28
    MainApp.main( args );
5529
  }
5630
5731
  @SuppressWarnings("RedundantStringFormatCall")
58
  private static void showAppInfo() throws IOException {
32
  private static void showAppInfo() {
5933
    out( format( "%s version %s", APP_TITLE, getVersion() ) );
60
    out( format( "Copyright %s White Magic Software, Ltd.", getYear() ) );
61
    out( format( "Portions copyright 2020 Karl Tauber." ) );
34
    out( format( "Copyright 2016-%s White Magic Software, Ltd.", getYear() ) );
35
    out( format( "Portions copyright 2015-2020 Karl Tauber." ) );
6236
  }
6337
6438
  private static void out( final String s ) {
6539
    System.out.println( s );
6640
  }
6741
68
  private static String getVersion() throws IOException {
69
    final Properties properties = loadProperties( "app.properties" );
70
    return properties.getProperty( "application.version" );
42
  /**
43
   * Returns the application version number retrieved from the application
44
   * properties file. The properties file is generated at build time, which
45
   * keys off the repository.
46
   *
47
   * @return The application version number.
48
   * @throws RuntimeException An {@link IOException} occurred.
49
   */
50
  public static String getVersion() {
51
    try {
52
      final var properties = loadProperties( "app.properties" );
53
      return properties.getProperty( "application.version" );
54
    } catch( final Exception ex ) {
55
      throw new RuntimeException( ex );
56
    }
7157
  }
7258
7359
  private static String getYear() {
7460
    return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) );
7561
  }
7662
7763
  @SuppressWarnings("SameParameterValue")
7864
  private static Properties loadProperties( final String resource )
79
      throws IOException {
80
    final Properties properties = new Properties();
65
    throws IOException {
66
    final var properties = new Properties();
8167
    properties.load( getResourceAsStream( getResourceName( resource ) ) );
8268
    return properties;
D src/main/java/com/keenwrite/Main.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
import com.keenwrite.preferences.FilePreferencesFactory;
31
import com.keenwrite.service.Options;
32
import com.keenwrite.service.Snitch;
33
import com.keenwrite.util.ResourceWalker;
34
import com.keenwrite.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.awt.*;
41
import java.io.FileInputStream;
42
import java.io.IOException;
43
import java.io.InputStream;
44
import java.net.URI;
45
import java.util.Map;
46
import java.util.logging.LogManager;
47
48
import static com.keenwrite.Bootstrap.APP_TITLE;
49
import static com.keenwrite.Constants.*;
50
import static com.keenwrite.StatusBarNotifier.clue;
51
import static java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment;
52
import static java.awt.font.TextAttribute.*;
53
import static javafx.scene.input.KeyCode.F11;
54
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
55
56
/**
57
 * Application entry point. The application allows users to edit Markdown
58
 * files and see a real-time preview of the edits.
59
 */
60
public final class Main extends Application {
61
62
  static {
63
    // Suppress logging to standard output.
64
    LogManager.getLogManager().reset();
65
66
    // Suppress logging to standard error.
67
    System.err.close();
68
  }
69
70
  private final Options mOptions = Services.load( Options.class );
71
  private final Snitch mSnitch = Services.load( Snitch.class );
72
73
  private final Thread mSnitchThread = new Thread( getSnitch() );
74
  private final MainWindow mMainWindow = new MainWindow();
75
76
  @SuppressWarnings({"FieldCanBeLocal"})
77
  private StageState mStageState;
78
79
  /**
80
   * Application entry point.
81
   *
82
   * @param args Command-line arguments.
83
   */
84
  public static void main( final String[] args ) {
85
    initPreferences();
86
    initFonts();
87
    launch( args );
88
  }
89
90
  /**
91
   * JavaFX entry point.
92
   *
93
   * @param stage The primary application stage.
94
   */
95
  @Override
96
  public void start( final Stage stage ) {
97
    initState( stage );
98
    initStage( stage );
99
    initSnitch();
100
101
    stage.show();
102
103
    // After the stage is visible, the panel dimensions are
104
    // known, which allows scaling images to fit the preview panel.
105
    getMainWindow().init();
106
  }
107
108
  /**
109
   * This needs to run before the windowing system kicks in, otherwise the
110
   * fonts will not be found.
111
   */
112
  @SuppressWarnings({"rawtypes", "unchecked"})
113
  public static void initFonts() {
114
    final var ge = getLocalGraphicsEnvironment();
115
116
    try {
117
      ResourceWalker.walk(
118
          FONT_DIRECTORY, path -> {
119
            final var uri = path.toUri();
120
            final var filename = path.toString();
121
122
            try( final var is = openFont( uri, filename ) ) {
123
              final var font = Font.createFont( Font.TRUETYPE_FONT, is );
124
              final Map attributes = font.getAttributes();
125
126
              attributes.put( LIGATURES, LIGATURES_ON );
127
              attributes.put( KERNING, KERNING_ON );
128
              ge.registerFont( font.deriveFont( attributes ) );
129
            } catch( final Exception e ) {
130
              clue( e );
131
            }
132
          }
133
      );
134
    } catch( final Exception e ) {
135
      clue( e );
136
    }
137
  }
138
139
  private static InputStream openFont( final URI uri, final String filename )
140
      throws IOException {
141
    return uri.getScheme().equals( "jar" )
142
        ? Main.class.getResourceAsStream( filename )
143
        : new FileInputStream( filename );
144
  }
145
146
  /**
147
   * Sets the factory used for reading user preferences.
148
   */
149
  private static void initPreferences() {
150
    System.setProperty(
151
        "java.util.prefs.PreferencesFactory",
152
        FilePreferencesFactory.class.getName()
153
    );
154
  }
155
156
  private void initState( final Stage stage ) {
157
    mStageState = new StageState( stage, getOptions().getState() );
158
  }
159
160
  private void initStage( final Stage stage ) {
161
    stage.getIcons().addAll(
162
        createImage( FILE_LOGO_16 ),
163
        createImage( FILE_LOGO_32 ),
164
        createImage( FILE_LOGO_128 ),
165
        createImage( FILE_LOGO_256 ),
166
        createImage( FILE_LOGO_512 ) );
167
    stage.setTitle( APP_TITLE );
168
    stage.setScene( getScene() );
169
170
    stage.addEventHandler( KEY_PRESSED, event -> {
171
      if( F11.equals( event.getCode() ) ) {
172
        stage.setFullScreen( !stage.isFullScreen() );
173
      }
174
    } );
175
  }
176
177
  /**
178
   * Watch for file system changes.
179
   */
180
  private void initSnitch() {
181
    getSnitchThread().start();
182
  }
183
184
  /**
185
   * Stops the snitch service, if its running.
186
   *
187
   * @throws InterruptedException Couldn't stop the snitch thread.
188
   */
189
  @Override
190
  public void stop() throws InterruptedException {
191
    getSnitch().stop();
192
193
    final Thread thread = getSnitchThread();
194
    thread.interrupt();
195
    thread.join();
196
  }
197
198
  private Snitch getSnitch() {
199
    return mSnitch;
200
  }
201
202
  private Thread getSnitchThread() {
203
    return mSnitchThread;
204
  }
205
206
  private Options getOptions() {
207
    return mOptions;
208
  }
209
210
  private MainWindow getMainWindow() {
211
    return mMainWindow;
212
  }
213
214
  private Scene getScene() {
215
    return getMainWindow().getScene();
216
  }
217
218
  private Image createImage( final String filename ) {
219
    return new Image( filename );
220
  }
221
222
  /**
223
   * This is here to suppress an IDE warning, the method is not used.
224
   */
225
  public StageState getStageState() {
226
    return mStageState;
227
  }
228
}
2291
A src/main/java/com/keenwrite/MainApp.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.preferences.Workspace;
5
import com.keenwrite.service.Snitch;
6
import javafx.application.Application;
7
import javafx.application.Platform;
8
import javafx.stage.Stage;
9
10
import java.util.function.BooleanSupplier;
11
import java.util.logging.LogManager;
12
13
import static com.keenwrite.Bootstrap.APP_TITLE;
14
import static com.keenwrite.Constants.LOGOS;
15
import static com.keenwrite.preferences.Workspace.*;
16
import static com.keenwrite.util.FontLoader.initFonts;
17
import static javafx.scene.input.KeyCode.F11;
18
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
19
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
20
21
/**
22
 * Application entry point. The application allows users to edit plain text
23
 * files in a markup notation and see a real-time preview of the formatted
24
 * output.
25
 */
26
@SuppressWarnings({"FieldCanBeLocal", "unused", "RedundantSuppression"})
27
public final class MainApp extends Application {
28
29
  private final Snitch mSnitch = Services.load( Snitch.class );
30
31
  private Workspace mWorkspace;
32
33
  /**
34
   * Application entry point.
35
   *
36
   * @param args Command-line arguments.
37
   */
38
  public static void main( final String[] args ) {
39
    disableLogging();
40
    launch( args );
41
  }
42
43
  /**
44
   * Suppress logging to standard output and standard error.
45
   */
46
  private static void disableLogging() {
47
    LogManager.getLogManager().reset();
48
    System.err.close();
49
  }
50
51
  /**
52
   * JavaFX entry point.
53
   *
54
   * @param stage The primary application stage.
55
   */
56
  @Override
57
  public void start( final Stage stage ) {
58
    // Must be instantiated after the UI is initialized (i.e., not in main).
59
    mWorkspace = new Workspace();
60
61
    initFonts();
62
    initState( stage );
63
    initStage( stage );
64
    initIcons( stage );
65
    initScene( stage );
66
    initSnitch();
67
68
    stage.show();
69
  }
70
71
  /**
72
   * Saves the workspace then terminates the application.
73
   */
74
  @Override
75
  public void stop() {
76
    save();
77
    getSnitch().stop();
78
    Platform.exit();
79
    System.exit( 0 );
80
  }
81
82
  /**
83
   * Saves the current application state configuration and user preferences.
84
   */
85
  private void save() {
86
    mWorkspace.save();
87
  }
88
89
  private void initState( final Stage stage ) {
90
    final var enable = createBoundsEnabledSupplier( stage );
91
92
    stage.setX( mWorkspace.toDouble( KEY_UI_WINDOW_X ) );
93
    stage.setY( mWorkspace.toDouble( KEY_UI_WINDOW_Y ) );
94
    stage.setWidth( mWorkspace.toDouble( KEY_UI_WINDOW_W ) );
95
    stage.setHeight( mWorkspace.toDouble( KEY_UI_WINDOW_H ) );
96
    stage.setMaximized( mWorkspace.toBoolean( KEY_UI_WINDOW_MAX ) );
97
    stage.setFullScreen( mWorkspace.toBoolean( KEY_UI_WINDOW_FULL ) );
98
99
    mWorkspace.listen( KEY_UI_WINDOW_X, stage.xProperty(), enable );
100
    mWorkspace.listen( KEY_UI_WINDOW_Y, stage.yProperty(), enable );
101
    mWorkspace.listen( KEY_UI_WINDOW_W, stage.widthProperty(), enable );
102
    mWorkspace.listen( KEY_UI_WINDOW_H, stage.heightProperty(), enable );
103
    mWorkspace.listen( KEY_UI_WINDOW_MAX, stage.maximizedProperty() );
104
    mWorkspace.listen( KEY_UI_WINDOW_FULL, stage.fullScreenProperty() );
105
  }
106
107
  private void initStage( final Stage stage ) {
108
    stage.setTitle( APP_TITLE );
109
    stage.addEventHandler( WINDOW_CLOSE_REQUEST, event -> stop() );
110
    stage.addEventHandler( KEY_PRESSED, event -> {
111
      if( F11.equals( event.getCode() ) ) {
112
        stage.setFullScreen( !stage.isFullScreen() );
113
      }
114
    } );
115
  }
116
117
  private void initIcons( final Stage stage ) {
118
    stage.getIcons().addAll( LOGOS );
119
  }
120
121
  private void initScene( final Stage stage ) {
122
    stage.setScene( (new MainScene( mWorkspace )).getScene() );
123
  }
124
125
  /**
126
   * Watch for file system changes.
127
   */
128
  private void initSnitch() {
129
    getSnitch().start();
130
  }
131
132
  /**
133
   * When the window is maximized, full screen, or iconified, prevent updating
134
   * the window bounds. This is used so that if the user exits the application
135
   * when full screen (or maximized), restarting the application will recall
136
   * the previous bounds, allowing for continuity of expected behaviour.
137
   *
138
   * @param stage The window to check for "normal" status.
139
   * @return {@code false} when the bounds must not be changed, ergo persisted.
140
   */
141
  private BooleanSupplier createBoundsEnabledSupplier( final Stage stage ) {
142
    return () ->
143
      !(stage.isMaximized() || stage.isFullScreen() || stage.isIconified());
144
  }
145
146
  private Snitch getSnitch() {
147
    return mSnitch;
148
  }
149
}
1150
A src/main/java/com/keenwrite/MainPane.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.editors.TextDefinition;
5
import com.keenwrite.editors.TextEditor;
6
import com.keenwrite.editors.TextResource;
7
import com.keenwrite.editors.definition.DefinitionEditor;
8
import com.keenwrite.editors.definition.DefinitionTabSceneFactory;
9
import com.keenwrite.editors.definition.TreeTransformer;
10
import com.keenwrite.editors.definition.yaml.YamlTreeTransformer;
11
import com.keenwrite.editors.markdown.MarkdownEditor;
12
import com.keenwrite.io.MediaType;
13
import com.keenwrite.preferences.Key;
14
import com.keenwrite.preferences.Workspace;
15
import com.keenwrite.preview.HtmlPreview;
16
import com.keenwrite.processors.IdentityProcessor;
17
import com.keenwrite.processors.Processor;
18
import com.keenwrite.processors.ProcessorContext;
19
import com.keenwrite.processors.ProcessorFactory;
20
import com.keenwrite.processors.markdown.Caret;
21
import com.keenwrite.processors.markdown.CaretExtension;
22
import com.keenwrite.service.events.Notifier;
23
import com.keenwrite.sigils.RSigilOperator;
24
import com.keenwrite.sigils.SigilOperator;
25
import com.keenwrite.sigils.Tokens;
26
import com.keenwrite.sigils.YamlSigilOperator;
27
import com.panemu.tiwulfx.control.dock.DetachableTab;
28
import com.panemu.tiwulfx.control.dock.DetachableTabPane;
29
import javafx.beans.property.*;
30
import javafx.collections.ListChangeListener;
31
import javafx.event.ActionEvent;
32
import javafx.event.Event;
33
import javafx.event.EventHandler;
34
import javafx.scene.Scene;
35
import javafx.scene.control.SplitPane;
36
import javafx.scene.control.Tab;
37
import javafx.scene.control.Tooltip;
38
import javafx.scene.control.TreeItem.TreeModificationEvent;
39
import javafx.scene.input.KeyEvent;
40
import javafx.stage.Stage;
41
import javafx.stage.Window;
42
43
import java.io.File;
44
import java.nio.file.Path;
45
import java.util.*;
46
import java.util.concurrent.atomic.AtomicBoolean;
47
import java.util.function.Function;
48
import java.util.stream.Collectors;
49
50
import static com.keenwrite.Constants.*;
51
import static com.keenwrite.ExportFormat.NONE;
52
import static com.keenwrite.Messages.get;
53
import static com.keenwrite.StatusBarNotifier.clue;
54
import static com.keenwrite.io.MediaType.*;
55
import static com.keenwrite.preferences.Workspace.*;
56
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
57
import static com.keenwrite.service.events.Notifier.NO;
58
import static com.keenwrite.service.events.Notifier.YES;
59
import static java.util.stream.Collectors.groupingBy;
60
import static javafx.application.Platform.runLater;
61
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
62
import static javafx.scene.input.KeyCode.SPACE;
63
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
64
import static javafx.util.Duration.millis;
65
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
66
67
/**
68
 * Responsible for wiring together the main application components for a
69
 * particular workspace (project). These include the definition views,
70
 * text editors, and preview pane along with any corresponding controllers.
71
 */
72
public final class MainPane extends SplitPane {
73
  private static final Notifier sNotifier = Services.load( Notifier.class );
74
75
  /**
76
   * Used when opening files to determine how each file should be binned and
77
   * therefore what tab pane to be opened within.
78
   */
79
  private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of(
80
    TEXT_MARKDOWN, TEXT_R_MARKDOWN, TEXT_R_XML, UNDEFINED
81
  );
82
83
  /**
84
   * Prevents re-instantiation of processing classes.
85
   */
86
  private final Map<TextResource, Processor<String>> mProcessors =
87
    new HashMap<>();
88
89
  private final Workspace mWorkspace;
90
91
  /**
92
   * Groups similar file type tabs together.
93
   */
94
  private final Map<MediaType, DetachableTabPane> mTabPanes = new HashMap<>();
95
96
  /**
97
   * Stores definition names and values.
98
   */
99
  private final Map<String, String> mResolvedMap =
100
    new HashMap<>( MAP_SIZE_DEFAULT );
101
102
  /**
103
   * Renders the actively selected plain text editor tab.
104
   */
105
  private final HtmlPreview mHtmlPreview;
106
107
  /**
108
   * Changing the active editor fires the value changed event. This allows
109
   * refreshes to happen when external definitions are modified and need to
110
   * trigger the processing chain.
111
   */
112
  private final ObjectProperty<TextEditor> mActiveTextEditor =
113
    createActiveTextEditor();
114
115
  /**
116
   * Changing the active definition editor fires the value changed event. This
117
   * allows refreshes to happen when external definitions are modified and need
118
   * to trigger the processing chain.
119
   */
120
  private final ObjectProperty<TextDefinition> mActiveDefinitionEditor =
121
    createActiveDefinitionEditor( mActiveTextEditor );
122
123
  /**
124
   * Responsible for creating a new scene when a tab is detached into
125
   * its own window frame.
126
   */
127
  private final DefinitionTabSceneFactory mDefinitionTabSceneFactory =
128
    createDefinitionTabSceneFactory( mActiveDefinitionEditor );
129
130
  /**
131
   * Tracks the number of detached tab panels opened into their own windows,
132
   * which allows unique identification of subordinate windows by their title.
133
   * It is doubtful more than 128 windows, much less 256, will be created.
134
   */
135
  private byte mWindowCount;
136
137
  /**
138
   * Called when the definition data is changed.
139
   */
140
  private final EventHandler<TreeModificationEvent<Event>> mTreeHandler =
141
    event -> {
142
      final var editor = mActiveDefinitionEditor.get();
143
144
      resolve( editor );
145
      process( getActiveTextEditor() );
146
      save( editor );
147
    };
148
149
  /**
150
   * Adds all content panels to the main user interface. This will load the
151
   * configuration settings from the workspace to reproduce the settings from
152
   * a previous session.
153
   */
154
  public MainPane( final Workspace workspace ) {
155
    mWorkspace = workspace;
156
    mHtmlPreview = new HtmlPreview( workspace );
157
158
    open( bin( getRecentFiles() ) );
159
    viewPreview();
160
161
    final var ratio = 100f / getItems().size() / 100;
162
    final var positions = getDividerPositions();
163
164
    for( int i = 0; i < positions.length; i++ ) {
165
      positions[ i ] = ratio * i;
166
    }
167
168
    // TODO: Load divider positions from exported settings, see bin() comment.
169
    setDividerPositions( positions );
170
171
    // Once the main scene's window regains focus, update the active definition
172
    // editor to the currently selected tab.
173
    runLater(
174
      () -> getWindow().focusedProperty().addListener( ( c, o, n ) -> {
175
        if( n != null && n ) {
176
          final var pane = mTabPanes.get( TEXT_YAML );
177
          final var model = pane.getSelectionModel();
178
          final var tab = model.getSelectedItem();
179
180
          if( tab != null ) {
181
            final var editor = (TextDefinition) tab.getContent();
182
183
            mActiveDefinitionEditor.set( editor );
184
          }
185
        }
186
      } )
187
    );
188
  }
189
190
  /**
191
   * Opens all the files into the application, provided the paths are unique.
192
   * This may only be called for any type of files that a user can edit
193
   * (i.e., update and persist), such as definitions and text files.
194
   *
195
   * @param files The list of files to open.
196
   */
197
  public void open( final List<File> files ) {
198
    files.forEach( this::open );
199
  }
200
201
  /**
202
   * This opens the given file. Since the preview pane is not a file that
203
   * can be opened, it is safe to add a listener to the detachable pane.
204
   *
205
   * @param file The file to open.
206
   */
207
  private void open( final File file ) {
208
    final var tab = createTab( file );
209
    final var node = tab.getContent();
210
    final var mediaType = MediaType.valueFrom( file );
211
    final var tabPane = obtainDetachableTabPane( mediaType );
212
    final var newTabPane = !getItems().contains( tabPane );
213
214
    tab.setTooltip( createTooltip( file ) );
215
    tabPane.setFocusTraversable( false );
216
    tabPane.setTabClosingPolicy( ALL_TABS );
217
    tabPane.getTabs().add( tab );
218
219
    if( newTabPane ) {
220
      var index = getItems().size();
221
222
      if( node instanceof TextDefinition ) {
223
        tabPane.setSceneFactory( mDefinitionTabSceneFactory::create );
224
        index = 0;
225
      }
226
227
      addTabPane( index, tabPane );
228
    }
229
230
    getRecentFiles().add( file.getAbsolutePath() );
231
  }
232
233
  /**
234
   * Opens a new text editor document using the default document file name.
235
   */
236
  public void newTextEditor() {
237
    open( DOCUMENT_DEFAULT );
238
  }
239
240
  /**
241
   * Opens a new definition editor document using the default definition
242
   * file name.
243
   */
244
  public void newDefinitionEditor() {
245
    open( DEFINITION_DEFAULT );
246
  }
247
248
  /**
249
   * Iterates over all tab panes to find all {@link TextEditor}s and request
250
   * that they save themselves.
251
   */
252
  public void saveAll() {
253
    mTabPanes.forEach(
254
      ( mt, tp ) -> tp.getTabs().forEach( ( tab ) -> {
255
        final var node = tab.getContent();
256
        if( node instanceof TextEditor ) {
257
          save( ((TextEditor) node) );
258
        }
259
      } )
260
    );
261
  }
262
263
  /**
264
   * Requests that the active {@link TextEditor} saves itself. Don't bother
265
   * checking if modified first because if the user swaps external media from
266
   * an external source (e.g., USB thumb drive), save should not second-guess
267
   * the user: save always re-saves. Also, it's less code.
268
   */
269
  public void save() {
270
    save( getActiveTextEditor() );
271
  }
272
273
  /**
274
   * Saves the active {@link TextEditor} under a new name.
275
   *
276
   * @param file The new active editor {@link File} reference.
277
   */
278
  public void saveAs( final File file ) {
279
    assert file != null;
280
    final var editor = getActiveTextEditor();
281
    final var tab = getTab( editor );
282
283
    editor.rename( file );
284
    tab.ifPresent( t -> {
285
      t.setText( editor.getFilename() );
286
      t.setTooltip( createTooltip( file ) );
287
    } );
288
289
    save();
290
  }
291
292
  /**
293
   * Saves the given {@link TextResource} to a file. This is typically used
294
   * to save either an instance of {@link TextEditor} or {@link TextDefinition}.
295
   *
296
   * @param resource The resource to export.
297
   */
298
  private void save( final TextResource resource ) {
299
    try {
300
      resource.save();
301
    } catch( final Exception ex ) {
302
      clue( ex );
303
      sNotifier.alert(
304
        getWindow(), resource.getPath(), "TextResource.saveFailed", ex
305
      );
306
    }
307
  }
308
309
  /**
310
   * Closes all open {@link TextEditor}s; all {@link TextDefinition}s stay open.
311
   *
312
   * @return {@code true} when all editors, modified or otherwise, were
313
   * permitted to close; {@code false} when one or more editors were modified
314
   * and the user requested no closing.
315
   */
316
  public boolean closeAll() {
317
    var closable = true;
318
319
    for( final var entry : mTabPanes.entrySet() ) {
320
      final var tabPane = entry.getValue();
321
      final var tabIterator = tabPane.getTabs().iterator();
322
323
      while( tabIterator.hasNext() ) {
324
        final var tab = tabIterator.next();
325
        final var node = tab.getContent();
326
327
        if( node instanceof TextEditor &&
328
          (closable &= canClose( (TextEditor) node )) ) {
329
          tabIterator.remove();
330
          close( tab );
331
        }
332
      }
333
    }
334
335
    return closable;
336
  }
337
338
  /**
339
   * Calls the tab's {@link Tab#getOnClosed()} handler to carry out a close
340
   * event.
341
   *
342
   * @param tab The {@link Tab} that was closed.
343
   */
344
  private void close( final Tab tab ) {
345
    final var handler = tab.getOnClosed();
346
347
    if( handler != null ) {
348
      handler.handle( new ActionEvent() );
349
    }
350
  }
351
352
  /**
353
   * Closes the active tab; delegates to {@link #canClose(TextEditor)}.
354
   */
355
  public void close() {
356
    final var editor = getActiveTextEditor();
357
    if( canClose( editor ) ) {
358
      close( editor );
359
    }
360
  }
361
362
  /**
363
   * Closes the given {@link TextEditor}. This must not be called from within
364
   * a loop that iterates over the tab panes using {@code forEach}, lest a
365
   * concurrent modification exception be thrown.
366
   *
367
   * @param editor The {@link TextEditor} to close, without confirming with
368
   *               the user.
369
   */
370
  private void close( final TextEditor editor ) {
371
    getTab( editor ).ifPresent(
372
      ( tab ) -> {
373
        tab.getTabPane().getTabs().remove( tab );
374
        close( tab );
375
      }
376
    );
377
  }
378
379
  /**
380
   * Answers whether the given {@link TextEditor} may be closed.
381
   *
382
   * @param editor The {@link TextEditor} to try closing.
383
   * @return {@code true} when the editor may be closed; {@code false} when
384
   * the user has requested to keep the editor open.
385
   */
386
  private boolean canClose( final TextEditor editor ) {
387
    final var editorTab = getTab( editor );
388
    final var canClose = new AtomicBoolean( true );
389
390
    if( editor.isModified() ) {
391
      final var filename = new StringBuilder();
392
      editorTab.ifPresent( ( tab ) -> filename.append( tab.getText() ) );
393
394
      final var message = sNotifier.createNotification(
395
        Messages.get( "Alert.file.close.title" ),
396
        Messages.get( "Alert.file.close.text" ),
397
        filename.toString()
398
      );
399
400
      final var dialog = sNotifier.createConfirmation( getWindow(), message );
401
402
      dialog.showAndWait().ifPresent(
403
        save -> canClose.set( save == YES ? editor.save() : save == NO )
404
      );
405
    }
406
407
    return canClose.get();
408
  }
409
410
  private ObjectProperty<TextEditor> createActiveTextEditor() {
411
    final var editor = new SimpleObjectProperty<TextEditor>();
412
413
    editor.addListener( ( c, o, n ) -> {
414
      if( n != null ) {
415
        mHtmlPreview.setBaseUri( n.getPath() );
416
        process( n );
417
      }
418
    } );
419
420
    return editor;
421
  }
422
423
  /**
424
   * Adds the HTML preview tab to its own tab pane. This will only add the
425
   * preview once.
426
   */
427
  public void viewPreview() {
428
    final var tabPane = obtainDetachableTabPane( TEXT_HTML );
429
430
    // Prevent multiple HTML previews because in the end, there can be only one.
431
    for( final var tab : tabPane.getTabs() ) {
432
      if( tab.getContent() == mHtmlPreview ) {
433
        return;
434
      }
435
    }
436
437
    tabPane.addTab( "HTML", mHtmlPreview );
438
    addTabPane( tabPane );
439
  }
440
441
  public void viewRefresh() {
442
    mHtmlPreview.refresh();
443
  }
444
445
  /**
446
   * Returns the tab that contains the given {@link TextEditor}.
447
   *
448
   * @param editor The {@link TextEditor} instance to find amongst the tabs.
449
   * @return The first tab having content that matches the given tab.
450
   */
451
  private Optional<Tab> getTab( final TextEditor editor ) {
452
    return mTabPanes.values()
453
                    .stream()
454
                    .flatMap( pane -> pane.getTabs().stream() )
455
                    .filter( tab -> editor.equals( tab.getContent() ) )
456
                    .findFirst();
457
  }
458
459
  /**
460
   * Creates a new {@link DefinitionEditor} wrapped in a listener that
461
   * is used to detect when the active {@link DefinitionEditor} has changed.
462
   * Upon changing, the {@link #mResolvedMap} is updated and the active
463
   * text editor is refreshed.
464
   *
465
   * @param editor Text editor to update with the revised resolved map.
466
   * @return A newly configured property that represents the active
467
   * {@link DefinitionEditor}, never null.
468
   */
469
  private ObjectProperty<TextDefinition> createActiveDefinitionEditor(
470
    final ObjectProperty<TextEditor> editor ) {
471
    final var definitions = new SimpleObjectProperty<TextDefinition>();
472
    definitions.addListener( ( c, o, n ) -> {
473
      resolve( n == null ? createDefinitionEditor() : n );
474
      process( editor.get() );
475
    } );
476
477
    return definitions;
478
  }
479
480
  /**
481
   * Instantiates a factory that's responsible for creating new scenes when
482
   * a tab is dropped outside of any application window. The definition tabs
483
   * are fairly complex in that only one may be active at any time. When
484
   * activated, the {@link #mResolvedMap} must be updated to reflect the
485
   * hierarchy displayed in the {@link DefinitionEditor}.
486
   *
487
   * @param activeDefinitionEditor The current {@link DefinitionEditor}.
488
   * @return An object that listens to {@link DefinitionEditor} tab focus
489
   * changes.
490
   */
491
  private DefinitionTabSceneFactory createDefinitionTabSceneFactory(
492
    final ObjectProperty<TextDefinition> activeDefinitionEditor ) {
493
    return new DefinitionTabSceneFactory( ( tab ) -> {
494
      assert tab != null;
495
496
      var node = tab.getContent();
497
      if( node instanceof TextDefinition ) {
498
        activeDefinitionEditor.set( (DefinitionEditor) node );
499
      }
500
    } );
501
  }
502
503
  private DetachableTab createTab( final File file ) {
504
    final var r = createTextResource( file );
505
    final var tab = new DetachableTab( r.getFilename(), r.getNode() );
506
507
    r.modifiedProperty().addListener(
508
      ( c, o, n ) -> tab.setText( r.getFilename() + (n ? "*" : "") )
509
    );
510
511
    // This is called when either the tab is closed by the user clicking on
512
    // the tab's close icon or when closing (all) from the file menu.
513
    tab.setOnClosed(
514
      ( __ ) -> getRecentFiles().remove( file.getAbsolutePath() )
515
    );
516
517
    return tab;
518
  }
519
520
  /**
521
   * Creates bins for the different {@link MediaType}s, which eventually are
522
   * added to the UI as separate tab panes. If ever a general-purpose scene
523
   * exporter is developed to serialize a scene to an FXML file, this could
524
   * be replaced by such a class.
525
   * <p>
526
   * When binning the files, this makes sure that at least one file exists
527
   * for every type. If the user has opted to close a particular type (such
528
   * as the definition pane), the view will suppressed elsewhere.
529
   * </p>
530
   * <p>
531
   * The order that the binned files are returned will be reflected in the
532
   * order that the corresponding panes are rendered in the UI.
533
   * </p>
534
   *
535
   * @param paths The file paths to bin according to their type.
536
   * @return An in-order list of files, first by structured definition files,
537
   * then by plain text documents.
538
   */
539
  private List<File> bin( final SetProperty<String> paths ) {
540
    // Treat all files destined for the text editor as plain text documents
541
    // so that they are added to the same pane. Grouping by TEXT_PLAIN is a
542
    // bit arbitrary, but means explicitly capturing TEXT_PLAIN isn't needed.
543
    final Function<MediaType, MediaType> bin =
544
      m -> PLAIN_TEXT_FORMAT.contains( m ) ? TEXT_PLAIN : m;
545
546
    // Create two groups: YAML files and plain text files.
547
    final var bins = paths
548
      .stream()
549
      .collect(
550
        groupingBy( path -> bin.apply( MediaType.valueFrom( path ) ) )
551
      );
552
553
    bins.putIfAbsent( TEXT_YAML, List.of( DEFINITION_DEFAULT.toString() ) );
554
    bins.putIfAbsent( TEXT_PLAIN, List.of( DOCUMENT_DEFAULT.toString() ) );
555
556
    final var result = new ArrayList<File>( paths.size() );
557
558
    // Ensure that the same types are listed together (keep insertion order).
559
    bins.forEach( ( mediaType, files ) -> result.addAll(
560
      files.stream().map( File::new ).collect( Collectors.toList() ) )
561
    );
562
563
    return result;
564
  }
565
566
  /**
567
   * Uses the given {@link TextDefinition} instance to update the
568
   * {@link #mResolvedMap}.
569
   *
570
   * @param editor A non-null, possibly empty definition editor.
571
   */
572
  private void resolve( final TextDefinition editor ) {
573
    assert editor != null;
574
575
    final var tokens = createDefinitionTokens();
576
    final var operator = new YamlSigilOperator( tokens );
577
    final var map = new HashMap<String, String>();
578
579
    editor.toMap().forEach( ( k, v ) -> map.put( operator.entoken( k ), v ) );
580
581
    mResolvedMap.clear();
582
    mResolvedMap.putAll( editor.interpolate( map, tokens ) );
583
  }
584
585
  /**
586
   * Force the active editor to update, which will cause the processor
587
   * to re-evaluate the interpolated definition map thereby updating the
588
   * preview pane.
589
   *
590
   * @param editor Contains the source document to update in the preview pane.
591
   */
592
  private void process( final TextEditor editor ) {
593
    mProcessors.getOrDefault( editor, IdentityProcessor.INSTANCE )
594
               .apply( editor == null ? "" : editor.getText() );
595
    mHtmlPreview.scrollTo( CARET_ID );
596
  }
597
598
  /**
599
   * Lazily creates a {@link DetachableTabPane} configured to handle focus
600
   * requests by delegating to the selected tab's content. The tab pane is
601
   * associated with a given media type so that similar files can be grouped
602
   * together.
603
   *
604
   * @param mediaType The media type to associate with the tab pane.
605
   * @return An instance of {@link DetachableTabPane} that will handle
606
   * docking of tabs.
607
   */
608
  private DetachableTabPane obtainDetachableTabPane(
609
    final MediaType mediaType ) {
610
    return mTabPanes.computeIfAbsent(
611
      mediaType, ( mt ) -> createDetachableTabPane()
612
    );
613
  }
614
615
  /**
616
   * Creates an initialized {@link DetachableTabPane} instance.
617
   *
618
   * @return A new {@link DetachableTabPane} with all listeners configured.
619
   */
620
  private DetachableTabPane createDetachableTabPane() {
621
    final var tabPane = new DetachableTabPane();
622
623
    initStageOwnerFactory( tabPane );
624
    initTabListener( tabPane );
625
    initSelectionModelListener( tabPane );
626
627
    return tabPane;
628
  }
629
630
  /**
631
   * When any {@link DetachableTabPane} is detached from the main window,
632
   * the stage owner factory must be given its parent window, which will
633
   * own the child window. The parent window is the {@link MainPane}'s
634
   * {@link Scene}'s {@link Window} instance.
635
   *
636
   * <p>
637
   * This will derives the new title from the main window title, incrementing
638
   * the window count to help uniquely identify the child windows.
639
   * </p>
640
   *
641
   * @param tabPane A new {@link DetachableTabPane} to configure.
642
   */
643
  private void initStageOwnerFactory( final DetachableTabPane tabPane ) {
644
    tabPane.setStageOwnerFactory( ( stage ) -> {
645
      final var title = get(
646
        "Detach.tab.title",
647
        ((Stage) getWindow()).getTitle(), ++mWindowCount
648
      );
649
      stage.setTitle( title );
650
      return getScene().getWindow();
651
    } );
652
  }
653
654
  /**
655
   * Responsible for configuring the content of each {@link DetachableTab} when
656
   * it is added to the given {@link DetachableTabPane} instance.
657
   * <p>
658
   * For {@link TextEditor} contents, an instance of {@link ScrollEventHandler}
659
   * is initialized to perform synchronized scrolling between the editor and
660
   * its preview window. Additionally, the last tab in the tab pane's list of
661
   * tabs is given focus.
662
   * </p>
663
   * <p>
664
   * Note that multiple tabs can be added simultaneously.
665
   * </p>
666
   *
667
   * @param tabPane A new {@link DetachableTabPane} to configure.
668
   */
669
  private void initTabListener( final DetachableTabPane tabPane ) {
670
    tabPane.getTabs().addListener(
671
      ( final ListChangeListener.Change<? extends Tab> listener ) -> {
672
        while( listener.next() ) {
673
          if( listener.wasAdded() ) {
674
            final var tabs = listener.getAddedSubList();
675
676
            tabs.forEach( ( tab ) -> {
677
              final var node = tab.getContent();
678
679
              if( node instanceof TextEditor ) {
680
                initScrollEventListener( tab );
681
              }
682
            } );
683
684
            // Select and give focus to the last tab opened.
685
            final var index = tabs.size() - 1;
686
            if( index >= 0 ) {
687
              final var tab = tabs.get( index );
688
              tabPane.getSelectionModel().select( tab );
689
              tab.getContent().requestFocus();
690
            }
691
          }
692
        }
693
      }
694
    );
695
  }
696
697
  /**
698
   * Responsible for handling tab change events.
699
   *
700
   * @param tabPane A new {@link DetachableTabPane} to configure.
701
   */
702
  private void initSelectionModelListener( final DetachableTabPane tabPane ) {
703
    final var model = tabPane.getSelectionModel();
704
705
    model.selectedItemProperty().addListener( ( c, o, n ) -> {
706
      if( o != null && n == null ) {
707
        final var node = o.getContent();
708
709
        // If the last definition editor in the active pane was closed,
710
        // clear out the definitions then refresh the text editor.
711
        if( node instanceof TextDefinition ) {
712
          mActiveDefinitionEditor.set( createDefinitionEditor() );
713
        }
714
      }
715
      else if( n != null ) {
716
        final var node = n.getContent();
717
718
        if( node instanceof TextEditor ) {
719
          // Changing the active node will fire an event, which will
720
          // update the preview panel and grab focus.
721
          mActiveTextEditor.set( (TextEditor) node );
722
          runLater( node::requestFocus );
723
        }
724
        else if( node instanceof TextDefinition ) {
725
          mActiveDefinitionEditor.set( (DefinitionEditor) node );
726
        }
727
      }
728
    } );
729
  }
730
731
  /**
732
   * Synchronizes scrollbar positions between the given {@link Tab} that
733
   * contains an instance of {@link TextEditor} and {@link HtmlPreview} pane.
734
   *
735
   * @param tab The container for an instance of {@link TextEditor}.
736
   */
737
  private void initScrollEventListener( final Tab tab ) {
738
    final var editor = (TextEditor) tab.getContent();
739
    final var scrollPane = editor.getScrollPane();
740
    final var scrollBar = mHtmlPreview.getVerticalScrollBar();
741
    final var handler = new ScrollEventHandler( scrollPane, scrollBar );
742
    handler.enabledProperty().bind( tab.selectedProperty() );
743
  }
744
745
  private void addTabPane( final int index, final DetachableTabPane tabPane ) {
746
    final var items = getItems();
747
    if( !items.contains( tabPane ) ) {
748
      items.add( index, tabPane );
749
    }
750
  }
751
752
  private void addTabPane( final DetachableTabPane tabPane ) {
753
    addTabPane( getItems().size(), tabPane );
754
  }
755
756
  /**
757
   * @param path  Used by {@link ProcessorFactory} to determine
758
   *              {@link Processor} type to create based on file type.
759
   * @param caret Used by {@link CaretExtension} to add ID attribute into
760
   *              preview document for scrollbar synchronization.
761
   * @return A new {@link ProcessorContext} to use when creating an instance of
762
   * {@link Processor}.
763
   */
764
  private ProcessorContext createProcessorContext(
765
    final Path path, final Caret caret ) {
766
    return new ProcessorContext(
767
      mHtmlPreview, mResolvedMap, path, caret, NONE, mWorkspace
768
    );
769
  }
770
771
  public ProcessorContext createProcessorContext( final TextEditor t ) {
772
    return createProcessorContext( t.getPath(), t.getCaret() );
773
  }
774
775
  private TextResource createTextResource( final File file ) {
776
    // TODO: Create PlainTextEditor that's returned by default.
777
    return MediaType.valueFrom( file ) == TEXT_YAML
778
      ? createDefinitionEditor( file )
779
      : createMarkdownEditor( file );
780
  }
781
782
  /**
783
   * Creates an instance of {@link MarkdownEditor} that listens for both
784
   * caret change events and text change events. Text change events must
785
   * take priority over caret change events because it's possible to change
786
   * the text without moving the caret (e.g., delete selected text).
787
   *
788
   * @param file The file containing contents for the text editor.
789
   * @return A non-null text editor.
790
   */
791
  private TextResource createMarkdownEditor( final File file ) {
792
    final var path = file.toPath();
793
    final var editor = new MarkdownEditor( file, getWorkspace() );
794
    final var caret = editor.getCaret();
795
    final var context = createProcessorContext( path, caret );
796
797
    mProcessors.computeIfAbsent( editor, p -> createProcessors( context ) );
798
799
    editor.addDirtyListener( ( c, o, n ) -> {
800
      if( n ) {
801
        // Reset the status to OK after changing the text.
802
        clue();
803
804
        // Processing the text will update the status bar.
805
        process( getActiveTextEditor() );
806
      }
807
    } );
808
809
    editor.addEventListener(
810
      keyPressed( SPACE, CONTROL_DOWN ), this::autoinsert
811
    );
812
813
    // Set the active editor, which refreshes the preview panel.
814
    mActiveTextEditor.set( editor );
815
816
    return editor;
817
  }
818
819
  /**
820
   * Delegates to {@link #autoinsert()}.
821
   *
822
   * @param event Ignored.
823
   */
824
  private void autoinsert( final KeyEvent event ) {
825
    autoinsert();
826
  }
827
828
  /**
829
   * Finds a node that matches the word at the caret, then inserts the
830
   * corresponding definition. The definition token delimiters depend on
831
   * the type of file being edited.
832
   */
833
  public void autoinsert() {
834
    final var definitions = getActiveTextDefinition();
835
    final var editor = getActiveTextEditor();
836
    final var mediaType = editor.getMediaType();
837
    final var operator = getSigilOperator( mediaType );
838
839
    DefinitionNameInjector.autoinsert( editor, definitions, operator );
840
  }
841
842
  private TextDefinition createDefinitionEditor() {
843
    return createDefinitionEditor( DEFINITION_DEFAULT );
844
  }
845
846
  private TextDefinition createDefinitionEditor( final File file ) {
847
    final var transformer = createTreeTransformer();
848
    final var editor = new DefinitionEditor( file, transformer );
849
850
    editor.addTreeChangeHandler( mTreeHandler );
851
852
    return editor;
853
  }
854
855
  private TreeTransformer createTreeTransformer() {
856
    return new YamlTreeTransformer();
857
  }
858
859
  private Tooltip createTooltip( final File file ) {
860
    final var path = file.toPath();
861
    final var tooltip = new Tooltip( path.toString() );
862
863
    tooltip.setShowDelay( millis( 200 ) );
864
    return tooltip;
865
  }
866
867
  public TextEditor getActiveTextEditor() {
868
    return mActiveTextEditor.get();
869
  }
870
871
  public ReadOnlyObjectProperty<TextEditor> activeTextEditorProperty() {
872
    return mActiveTextEditor;
873
  }
874
875
  public TextDefinition getActiveTextDefinition() {
876
    return mActiveDefinitionEditor.get();
877
  }
878
879
  public Window getWindow() {
880
    return getScene().getWindow();
881
  }
882
883
  public Workspace getWorkspace() {
884
    return mWorkspace;
885
  }
886
887
  /**
888
   * Returns the sigil operator for the given {@link MediaType}.
889
   *
890
   * @param mediaType The type of file being edited.
891
   */
892
  private SigilOperator getSigilOperator( final MediaType mediaType ) {
893
    final var operator = new YamlSigilOperator( createDefinitionTokens() );
894
895
    return switch( mediaType ) {
896
      case TEXT_R_MARKDOWN, TEXT_R_XML -> new RSigilOperator(
897
        createRTokens(), operator );
898
      default -> operator;
899
    };
900
  }
901
902
  /**
903
   * Returns the set of file names opened in the application. The names must
904
   * be converted to {@link File} objects.
905
   *
906
   * @return A {@link Set} of file names.
907
   */
908
  private SetProperty<String> getRecentFiles() {
909
    return getWorkspace().setsProperty( KEY_UI_FILES_PATH );
910
  }
911
912
  private StringProperty stringProperty( final Key key ) {
913
    return getWorkspace().stringProperty( key );
914
  }
915
916
  private Tokens createRTokens() {
917
    return createTokens( KEY_R_DELIM_BEGAN, KEY_R_DELIM_ENDED );
918
  }
919
920
  private Tokens createDefinitionTokens() {
921
    return createTokens( KEY_DEF_DELIM_BEGAN, KEY_DEF_DELIM_ENDED );
922
  }
923
924
  private Tokens createTokens( final Key began, final Key ended ) {
925
    return new Tokens( stringProperty( began ), stringProperty( ended ) );
926
  }
927
}
1928
A src/main/java/com/keenwrite/MainScene.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.preferences.Workspace;
5
import com.keenwrite.ui.actions.ApplicationActions;
6
import com.keenwrite.ui.actions.ApplicationMenuBar;
7
import com.keenwrite.ui.listeners.CaretListener;
8
import javafx.scene.Node;
9
import javafx.scene.Parent;
10
import javafx.scene.Scene;
11
import javafx.scene.layout.BorderPane;
12
import org.controlsfx.control.StatusBar;
13
14
import static com.keenwrite.Constants.STYLESHEET_SCENE;
15
16
/**
17
 * Responsible for creating the bar scene: menu bar, tool bar, and status bar.
18
 */
19
public class MainScene {
20
  private final Scene mScene;
21
22
  public MainScene( final Workspace workspace ) {
23
    final var mainPane = createMainPane( workspace );
24
    final var actions = createApplicationActions( mainPane );
25
    final var menuBar = createMenuBar( actions );
26
    final var appPane = new BorderPane();
27
    final var statusBar = StatusBarNotifier.getStatusBar();
28
    final var caretListener = createCaretListener( mainPane );
29
30
    statusBar.getRightItems().add( caretListener );
31
32
    appPane.setTop( menuBar );
33
    appPane.setCenter( mainPane );
34
    appPane.setBottom( statusBar );
35
36
    mScene = createScene( appPane );
37
  }
38
39
  /**
40
   * Called by the {@link MainApp} to get a handle on the {@link Scene}
41
   * created by an instance of {@link MainScene}.
42
   *
43
   * @return The {@link Scene} created at construction time.
44
   */
45
  public Scene getScene() {
46
    return mScene;
47
  }
48
49
  private MainPane createMainPane( final Workspace workspace ) {
50
    return new MainPane( workspace );
51
  }
52
53
  private ApplicationActions createApplicationActions(
54
    final MainPane mainPane ) {
55
    return new ApplicationActions( mainPane );
56
  }
57
58
  private Node createMenuBar( final ApplicationActions actions ) {
59
    return (new ApplicationMenuBar()).createMenuBar( actions );
60
  }
61
62
  private Scene createScene( final Parent parent ) {
63
    final var scene = new Scene( parent );
64
    final var stylesheets = scene.getStylesheets();
65
    stylesheets.add( STYLESHEET_SCENE );
66
67
    return scene;
68
  }
69
70
  /**
71
   * Creates the class responsible for updating the UI with the caret position
72
   * based on the active text editor.
73
   *
74
   * @return The {@link CaretListener} responsible for updating the
75
   * {@link StatusBar} whenever the caret changes position.
76
   */
77
  private CaretListener createCaretListener( final MainPane mainPane ) {
78
    return new CaretListener( mainPane.activeTextEditorProperty() );
79
  }
80
}
181
D src/main/java/com/keenwrite/MainWindow.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
import com.dlsc.preferencesfx.PreferencesFxEvent;
31
import com.keenwrite.definition.DefinitionFactory;
32
import com.keenwrite.definition.DefinitionPane;
33
import com.keenwrite.definition.DefinitionSource;
34
import com.keenwrite.definition.MapInterpolator;
35
import com.keenwrite.definition.yaml.YamlDefinitionSource;
36
import com.keenwrite.editors.DefinitionNameInjector;
37
import com.keenwrite.editors.markdown.MarkdownEditorPane;
38
import com.keenwrite.exceptions.MissingFileException;
39
import com.keenwrite.preferences.UserPreferences;
40
import com.keenwrite.preview.HTMLPreviewPane;
41
import com.keenwrite.processors.Processor;
42
import com.keenwrite.processors.ProcessorContext;
43
import com.keenwrite.processors.ProcessorFactory;
44
import com.keenwrite.processors.markdown.MarkdownProcessor;
45
import com.keenwrite.service.Options;
46
import com.keenwrite.service.Snitch;
47
import com.keenwrite.spelling.api.SpellCheckListener;
48
import com.keenwrite.spelling.api.SpellChecker;
49
import com.keenwrite.spelling.impl.PermissiveSpeller;
50
import com.keenwrite.spelling.impl.SymSpellSpeller;
51
import com.keenwrite.util.Action;
52
import com.keenwrite.util.ActionUtils;
53
import com.keenwrite.util.SeparatorAction;
54
import com.vladsch.flexmark.parser.Parser;
55
import com.vladsch.flexmark.util.ast.NodeVisitor;
56
import com.vladsch.flexmark.util.ast.VisitHandler;
57
import javafx.beans.binding.Bindings;
58
import javafx.beans.binding.BooleanBinding;
59
import javafx.beans.property.BooleanProperty;
60
import javafx.beans.property.SimpleBooleanProperty;
61
import javafx.beans.value.ChangeListener;
62
import javafx.beans.value.ObservableBooleanValue;
63
import javafx.beans.value.ObservableValue;
64
import javafx.collections.ListChangeListener.Change;
65
import javafx.collections.ObservableList;
66
import javafx.event.Event;
67
import javafx.event.EventHandler;
68
import javafx.geometry.Pos;
69
import javafx.scene.Node;
70
import javafx.scene.Scene;
71
import javafx.scene.control.*;
72
import javafx.scene.image.ImageView;
73
import javafx.scene.input.KeyEvent;
74
import javafx.scene.layout.BorderPane;
75
import javafx.scene.layout.VBox;
76
import javafx.scene.text.Text;
77
import javafx.stage.FileChooser;
78
import javafx.stage.Window;
79
import javafx.stage.WindowEvent;
80
import javafx.util.Duration;
81
import org.apache.commons.lang3.SystemUtils;
82
import org.controlsfx.control.StatusBar;
83
import org.fxmisc.richtext.StyleClassedTextArea;
84
import org.fxmisc.richtext.model.StyleSpansBuilder;
85
import org.reactfx.value.Val;
86
87
import java.io.BufferedReader;
88
import java.io.File;
89
import java.io.IOException;
90
import java.io.InputStreamReader;
91
import java.nio.file.Path;
92
import java.util.*;
93
import java.util.concurrent.atomic.AtomicInteger;
94
import java.util.function.Consumer;
95
import java.util.function.Function;
96
import java.util.prefs.Preferences;
97
import java.util.stream.Collectors;
98
99
import static com.keenwrite.Bootstrap.APP_TITLE;
100
import static com.keenwrite.Constants.*;
101
import static com.keenwrite.ExportFormat.*;
102
import static com.keenwrite.Messages.get;
103
import static com.keenwrite.StatusBarNotifier.clue;
104
import static com.keenwrite.processors.ProcessorFactory.processChain;
105
import static com.keenwrite.util.StageState.*;
106
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
107
import static java.nio.charset.StandardCharsets.UTF_8;
108
import static java.nio.file.Files.writeString;
109
import static java.util.Collections.emptyList;
110
import static java.util.Collections.singleton;
111
import static javafx.application.Platform.runLater;
112
import static javafx.event.Event.fireEvent;
113
import static javafx.scene.control.Alert.AlertType.INFORMATION;
114
import static javafx.scene.input.KeyCode.ENTER;
115
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
116
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
117
118
/**
119
 * Main window containing a tab pane in the center for file editors.
120
 */
121
public class MainWindow implements Observer {
122
  /**
123
   * The {@code OPTIONS} variable must be declared before all other variables
124
   * to prevent subsequent initializations from failing due to missing user
125
   * preferences.
126
   */
127
  private static final Options sOptions = Services.load( Options.class );
128
  private static final Snitch SNITCH = Services.load( Snitch.class );
129
130
  private final Scene mScene;
131
  private final StatusBar mStatusBar;
132
  private final Text mLineNumberText;
133
  private final TextField mFindTextField;
134
  private final SpellChecker mSpellChecker;
135
136
  private final Object mMutex = new Object();
137
138
  /**
139
   * Prevents re-instantiation of processing classes.
140
   */
141
  private final Map<FileEditorTab, Processor<String>> mProcessors =
142
      new HashMap<>();
143
144
  private final Map<String, String> mResolvedMap =
145
      new HashMap<>( DEFAULT_MAP_SIZE );
146
147
  private final EventHandler<PreferencesFxEvent> mRPreferencesListener =
148
      event -> rerender();
149
150
  /**
151
   * Called when the definition data is changed.
152
   */
153
  private final EventHandler<TreeItem.TreeModificationEvent<Event>>
154
      mTreeHandler = event -> {
155
    exportDefinitions( getDefinitionPath() );
156
    interpolateResolvedMap();
157
    rerender();
158
  };
159
160
  /**
161
   * Called to inject the selected item when the user presses ENTER in the
162
   * definition pane.
163
   */
164
  private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
165
      event -> {
166
        if( event.getCode() == ENTER ) {
167
          getDefinitionNameInjector().injectSelectedItem();
168
        }
169
      };
170
171
  private final ChangeListener<Integer> mCaretPositionListener =
172
      ( observable, oldPosition, newPosition ) -> {
173
        processActiveTab();
174
      };
175
176
  private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
177
  private final DefinitionPane mDefinitionPane = createDefinitionPane();
178
  private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
179
  private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
180
      mCaretPositionListener );
181
182
  /**
183
   * Listens on the definition pane for double-click events.
184
   */
185
  private final DefinitionNameInjector mDefinitionNameInjector
186
      = new DefinitionNameInjector( mDefinitionPane );
187
188
  public MainWindow() {
189
    mStatusBar = createStatusBar();
190
    mLineNumberText = createLineNumberText();
191
    mFindTextField = createFindTextField();
192
    mScene = createScene();
193
    mSpellChecker = createSpellChecker();
194
195
    // Add the close request listener before the window is shown.
196
    initLayout();
197
    StatusBarNotifier.setStatusBar( mStatusBar );
198
  }
199
200
  /**
201
   * Called after the stage is shown.
202
   */
203
  public void init() {
204
    initFindInput();
205
    initSnitch();
206
    initDefinitionListener();
207
    initTabAddedListener();
208
    initTabChangedListener();
209
    initPreferences();
210
    initVariableNameInjector();
211
  }
212
213
  private void initLayout() {
214
    final var scene = getScene();
215
216
    scene.getStylesheets().add( STYLESHEET_SCENE );
217
    scene.windowProperty().addListener(
218
        ( unused, oldWindow, newWindow ) ->
219
            newWindow.setOnCloseRequest(
220
                e -> {
221
                  if( !getFileEditorPane().closeAllEditors() ) {
222
                    e.consume();
223
                  }
224
                }
225
            )
226
    );
227
  }
228
229
  /**
230
   * Initialize the find input text field to listen on F3, ENTER, and
231
   * ESCAPE key presses.
232
   */
233
  private void initFindInput() {
234
    final TextField input = getFindTextField();
235
236
    input.setOnKeyPressed( ( KeyEvent event ) -> {
237
      switch( event.getCode() ) {
238
        case F3:
239
        case ENTER:
240
          editFindNext();
241
          break;
242
        case F:
243
          if( !event.isControlDown() ) {
244
            break;
245
          }
246
        case ESCAPE:
247
          getStatusBar().setGraphic( null );
248
          getActiveFileEditorTab().getEditorPane().requestFocus();
249
          break;
250
      }
251
    } );
252
253
    // Remove when the input field loses focus.
254
    input.focusedProperty().addListener(
255
        ( focused, oldFocus, newFocus ) -> {
256
          if( !newFocus ) {
257
            getStatusBar().setGraphic( null );
258
          }
259
        }
260
    );
261
  }
262
263
  /**
264
   * Watch for changes to external files. In particular, this awaits
265
   * modifications to any XSL files associated with XML files being edited.
266
   * When
267
   * an XSL file is modified (external to the application), the snitch's ears
268
   * perk up and the file is reloaded. This keeps the XSL transformation up to
269
   * date with what's on the file system.
270
   */
271
  private void initSnitch() {
272
    SNITCH.addObserver( this );
273
  }
274
275
  /**
276
   * Listen for {@link FileEditorTabPane} to receive open definition file
277
   * event.
278
   */
279
  private void initDefinitionListener() {
280
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
281
        ( final ObservableValue<? extends Path> file,
282
          final Path oldPath, final Path newPath ) -> {
283
          openDefinitions( newPath );
284
          rerender();
285
        }
286
    );
287
  }
288
289
  /**
290
   * Re-instantiates all processors then re-renders the active tab. This
291
   * will refresh the resolved map, force R to re-initialize, and brute-force
292
   * XSLT file reloads.
293
   */
294
  private void rerender() {
295
    runLater(
296
        () -> {
297
          resetProcessors();
298
          processActiveTab();
299
        }
300
    );
301
  }
302
303
  /**
304
   * When tabs are added, hook the various change listeners onto the new
305
   * tab sothat the preview pane refreshes as necessary.
306
   */
307
  private void initTabAddedListener() {
308
    final FileEditorTabPane editorPane = getFileEditorPane();
309
310
    // Make sure the text processor kicks off when new files are opened.
311
    final ObservableList<Tab> tabs = editorPane.getTabs();
312
313
    // Update the preview pane on tab changes.
314
    tabs.addListener(
315
        ( final Change<? extends Tab> change ) -> {
316
          while( change.next() ) {
317
            if( change.wasAdded() ) {
318
              // Multiple tabs can be added simultaneously.
319
              for( final Tab newTab : change.getAddedSubList() ) {
320
                final FileEditorTab tab = (FileEditorTab) newTab;
321
322
                initTextChangeListener( tab );
323
                initScrollEventListener( tab );
324
                initSpellCheckListener( tab );
325
//              initSyntaxListener( tab );
326
              }
327
            }
328
          }
329
        }
330
    );
331
  }
332
333
  private void initTextChangeListener( final FileEditorTab tab ) {
334
    tab.addTextChangeListener(
335
        ( __, ov, nv ) -> {
336
          process( tab );
337
        }
338
    );
339
  }
340
341
  private void initScrollEventListener( final FileEditorTab tab ) {
342
    final var scrollPane = tab.getScrollPane();
343
    final var scrollBar = getPreviewPane().getVerticalScrollBar();
344
345
    addShowListener( scrollPane, ( __ ) -> {
346
      final var handler = new ScrollEventHandler( scrollPane, scrollBar );
347
      handler.enabledProperty().bind( tab.selectedProperty() );
348
    } );
349
  }
350
351
  /**
352
   * Listen for changes to the any particular paragraph and perform a quick
353
   * spell check upon it. The style classes in the editor will be changed to
354
   * mark any spelling mistakes in the paragraph. The user may then interact
355
   * with any misspelled word (i.e., any piece of text that is marked) to
356
   * revise the spelling.
357
   *
358
   * @param tab The tab to spellcheck.
359
   */
360
  private void initSpellCheckListener( final FileEditorTab tab ) {
361
    final var editor = tab.getEditorPane().getEditor();
362
363
    // When the editor first appears, run a full spell check. This allows
364
    // spell checking while typing to be restricted to the active paragraph,
365
    // which is usually substantially smaller than the whole document.
366
    addShowListener(
367
        editor, ( __ ) -> spellcheck( editor, editor.getText() )
368
    );
369
370
    // Use the plain text changes so that notifications of style changes
371
    // are suppressed. Checking against the identity ensures that only
372
    // new text additions or deletions trigger proofreading.
373
    editor.plainTextChanges()
374
          .filter( p -> !p.isIdentity() ).subscribe( change -> {
375
376
      // Only perform a spell check on the current paragraph. The
377
      // entire document is processed once, when opened.
378
      final var offset = change.getPosition();
379
      final var position = editor.offsetToPosition( offset, Forward );
380
      final var paraId = position.getMajor();
381
      final var paragraph = editor.getParagraph( paraId );
382
      final var text = paragraph.getText();
383
384
      // Ensure that styles aren't doubled-up.
385
      editor.clearStyle( paraId );
386
387
      spellcheck( editor, text, paraId );
388
    } );
389
  }
390
391
  /**
392
   * Listen for new tab selection events.
393
   */
394
  private void initTabChangedListener() {
395
    final FileEditorTabPane editorPane = getFileEditorPane();
396
397
    // Update the preview pane changing tabs.
398
    editorPane.addTabSelectionListener(
399
        ( __, oldTab, newTab ) -> {
400
          if( newTab == null ) {
401
            // Clear the preview pane when closing an editor. When the last
402
            // tab is closed, this ensures that the preview pane is empty.
403
            getPreviewPane().clear();
404
          }
405
          else {
406
            final var tab = (FileEditorTab) newTab;
407
            updateVariableNameInjector( tab );
408
            process( tab );
409
          }
410
        }
411
    );
412
  }
413
414
  /**
415
   * Reloads the preferences from the previous session.
416
   */
417
  private void initPreferences() {
418
    initDefinitionPane();
419
    getFileEditorPane().initPreferences();
420
    getUserPreferences().addSaveEventHandler( mRPreferencesListener );
421
  }
422
423
  private void initVariableNameInjector() {
424
    updateVariableNameInjector( getActiveFileEditorTab() );
425
  }
426
427
  /**
428
   * Calls the listener when the given node is shown for the first time. The
429
   * visible property is not the same as the initial showing event; visibility
430
   * can be triggered numerous times (such as going off screen).
431
   * <p>
432
   * This is called, for example, before the drag handler can be attached,
433
   * because the scrollbar for the text editor pane must be visible.
434
   * </p>
435
   *
436
   * @param node     The node to watch for showing.
437
   * @param consumer The consumer to invoke when the event fires.
438
   */
439
  private void addShowListener(
440
      final Node node, final Consumer<Void> consumer ) {
441
    final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
442
        runLater( () -> {
443
          if( newShow != null && newShow ) {
444
            try {
445
              consumer.accept( null );
446
            } catch( final Exception ex ) {
447
              clue( ex );
448
            }
449
          }
450
        } );
451
452
    Val.flatMap( node.sceneProperty(), Scene::windowProperty )
453
       .flatMap( Window::showingProperty )
454
       .addListener( listener );
455
  }
456
457
  private void scrollToCaret() {
458
    synchronized( mMutex ) {
459
      final var previewPane = getPreviewPane();
460
461
      previewPane.scrollTo( CARET_ID );
462
      previewPane.repaintScrollPane();
463
    }
464
  }
465
466
  private void updateVariableNameInjector( final FileEditorTab tab ) {
467
    getDefinitionNameInjector().addListener( tab );
468
  }
469
470
  /**
471
   * Called to update the status bar's caret position when a new tab is added
472
   * or the active tab is switched.
473
   *
474
   * @param tab The active tab containing a caret position to show.
475
   */
476
  private void updateCaretStatus( final FileEditorTab tab ) {
477
    getLineNumberText().setText( tab.getCaretPosition().toString() );
478
  }
479
480
  /**
481
   * Called whenever the preview pane becomes out of sync with the file editor
482
   * tab. This can be called when the text changes, the caret paragraph
483
   * changes, or the file tab changes.
484
   *
485
   * @param tab The file editor tab that has been changed in some fashion.
486
   */
487
  private void process( final FileEditorTab tab ) {
488
    if( tab != null ) {
489
      getPreviewPane().setPath( tab.getPath() );
490
491
      final Processor<String> processor = getProcessors().computeIfAbsent(
492
          tab, p -> createProcessors( tab )
493
      );
494
495
      try {
496
        updateCaretStatus( tab );
497
        processChain( processor, tab.getEditorText() );
498
        scrollToCaret();
499
      } catch( final Exception ex ) {
500
        clue( ex );
501
      }
502
    }
503
  }
504
505
  private void processActiveTab() {
506
    process( getActiveFileEditorTab() );
507
  }
508
509
  /**
510
   * Called when a definition source is opened.
511
   *
512
   * @param path Path to the definition source that was opened.
513
   */
514
  private void openDefinitions( final Path path ) {
515
    try {
516
      final var ds = createDefinitionSource( path );
517
      setDefinitionSource( ds );
518
519
      final var prefs = getUserPreferences();
520
      prefs.definitionPathProperty().setValue( path.toFile() );
521
      prefs.save();
522
523
      final var tooltipPath = new Tooltip( path.toString() );
524
      tooltipPath.setShowDelay( Duration.millis( 200 ) );
525
526
      final var pane = getDefinitionPane();
527
      pane.update( ds );
528
      pane.addTreeChangeHandler( mTreeHandler );
529
      pane.addKeyEventHandler( mDefinitionKeyHandler );
530
      pane.filenameProperty().setValue( path.getFileName().toString() );
531
      pane.setTooltip( tooltipPath );
532
533
      interpolateResolvedMap();
534
    } catch( final Exception ex ) {
535
      clue( ex );
536
    }
537
  }
538
539
  private void exportDefinitions( final Path path ) {
540
    try {
541
      final var pane = getDefinitionPane();
542
      final var root = pane.getTreeView().getRoot();
543
      final var problemChild = pane.isTreeWellFormed();
544
545
      if( problemChild == null ) {
546
        getDefinitionSource().getTreeAdapter().export( root, path );
547
      }
548
      else {
549
        clue( "yaml.error.tree.form", problemChild.getValue() );
550
      }
551
    } catch( final Exception ex ) {
552
      clue( ex );
553
    }
554
  }
555
556
  private void interpolateResolvedMap() {
557
    final var treeMap = getDefinitionPane().toMap();
558
    final var map = new HashMap<>( treeMap );
559
    MapInterpolator.interpolate( map );
560
561
    getResolvedMap().clear();
562
    getResolvedMap().putAll( map );
563
  }
564
565
  private void initDefinitionPane() {
566
    openDefinitions( getDefinitionPath() );
567
  }
568
569
  //---- File actions -------------------------------------------------------
570
571
  /**
572
   * Called when an {@link Observable} instance has changed. This is called
573
   * by both the {@link Snitch} service and the notify service. The @link
574
   * Snitch} service can be called for different file types, including
575
   * {@link DefinitionSource} instances.
576
   *
577
   * @param observable The observed instance.
578
   * @param value      The noteworthy item.
579
   */
580
  @Override
581
  public void update( final Observable observable, final Object value ) {
582
    if( value instanceof Path && observable instanceof Snitch ) {
583
      updateSelectedTab();
584
    }
585
  }
586
587
  /**
588
   * Called when a file has been modified.
589
   */
590
  private void updateSelectedTab() {
591
    rerender();
592
  }
593
594
  /**
595
   * After resetting the processors, they will refresh anew to be up-to-date
596
   * with the files (text and definition) currently loaded into the editor.
597
   */
598
  private void resetProcessors() {
599
    getProcessors().clear();
600
  }
601
602
  //---- File actions -------------------------------------------------------
603
604
  private void fileNew() {
605
    getFileEditorPane().newEditor();
606
  }
607
608
  private void fileOpen() {
609
    getFileEditorPane().openFileDialog();
610
  }
611
612
  private void fileClose() {
613
    getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
614
  }
615
616
  /**
617
   * TODO: Upon closing, first remove the tab change listeners. (There's no
618
   * need to re-render each tab when all are being closed.)
619
   */
620
  private void fileCloseAll() {
621
    getFileEditorPane().closeAllEditors();
622
  }
623
624
  private void fileSave() {
625
    getFileEditorPane().saveEditor( getActiveFileEditorTab() );
626
  }
627
628
  private void fileSaveAs() {
629
    final FileEditorTab editor = getActiveFileEditorTab();
630
    getFileEditorPane().saveEditorAs( editor );
631
    getProcessors().remove( editor );
632
633
    try {
634
      process( editor );
635
    } catch( final Exception ex ) {
636
      clue( ex );
637
    }
638
  }
639
640
  private void fileSaveAll() {
641
    getFileEditorPane().saveAllEditors();
642
  }
643
644
  /**
645
   * Exports the contents of the current tab according to the given
646
   * {@link ExportFormat}.
647
   *
648
   * @param format Configures the {@link MarkdownProcessor} when exporting.
649
   */
650
  private void fileExport( final ExportFormat format ) {
651
    final var tab = getActiveFileEditorTab();
652
    final var context = createProcessorContext( tab, format );
653
    final var chain = ProcessorFactory.createProcessors( context );
654
    final var doc = tab.getEditorText();
655
    final var export = processChain( chain, doc );
656
657
    final var filename = format.toExportFilename( tab.getPath().toFile() );
658
    final var dir = getPreferences().get( "lastDirectory", null );
659
    final var lastDir = new File( dir == null ? "." : dir );
660
661
    final FileChooser chooser = new FileChooser();
662
    chooser.setTitle( get( "Dialog.file.choose.export.title" ) );
663
    chooser.setInitialFileName( filename.getName() );
664
    chooser.setInitialDirectory( lastDir );
665
666
    final File file = chooser.showSaveDialog( getWindow() );
667
668
    if( file != null ) {
669
      try {
670
        writeString( file.toPath(), export, UTF_8 );
671
        final var m = get( "Main.status.export.success", file.toString() );
672
        clue( m );
673
      } catch( final IOException e ) {
674
        clue( e );
675
      }
676
    }
677
  }
678
679
  private void fileExit() {
680
    final Window window = getWindow();
681
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
682
  }
683
684
  //---- Edit actions -------------------------------------------------------
685
686
  /**
687
   * Used to find text in the active file editor window.
688
   */
689
  private void editFind() {
690
    final TextField input = getFindTextField();
691
    getStatusBar().setGraphic( input );
692
    input.requestFocus();
693
  }
694
695
  public void editFindNext() {
696
    getActiveFileEditorTab().searchNext( getFindTextField().getText() );
697
  }
698
699
  public void editPreferences() {
700
    getUserPreferences().show();
701
  }
702
703
  //---- Insert actions -----------------------------------------------------
704
705
  /**
706
   * Delegates to the active editor to handle wrapping the current text
707
   * selection with leading and trailing strings.
708
   *
709
   * @param leading  The string to put before the selection.
710
   * @param trailing The string to put after the selection.
711
   */
712
  private void insertMarkdown(
713
      final String leading, final String trailing ) {
714
    getActiveEditorPane().surroundSelection( leading, trailing );
715
  }
716
717
  private void insertMarkdown(
718
      final String leading, final String trailing, final String hint ) {
719
    getActiveEditorPane().surroundSelection( leading, trailing, hint );
720
  }
721
722
  //---- Help actions -------------------------------------------------------
723
724
  private void helpAbout() {
725
    final Alert alert = new Alert( INFORMATION );
726
    alert.setTitle( get( "Dialog.about.title", APP_TITLE ) );
727
    alert.setHeaderText( get( "Dialog.about.header", APP_TITLE ) );
728
    alert.setContentText( get( "Dialog.about.content" ) );
729
    alert.setGraphic( new ImageView( ICON_DIALOG ) );
730
    alert.initOwner( getWindow() );
731
732
    alert.showAndWait();
733
  }
734
735
  //---- Member creators ----------------------------------------------------
736
737
  private SpellChecker createSpellChecker() {
738
    try {
739
      final Collection<String> lexicon = readLexicon( "en.txt" );
740
      return SymSpellSpeller.forLexicon( lexicon );
741
    } catch( final Exception ex ) {
742
      clue( ex );
743
      return new PermissiveSpeller();
744
    }
745
  }
746
747
  /**
748
   * Creates processors suited to parsing and rendering different file types.
749
   *
750
   * @param tab The tab that is subjected to processing.
751
   * @return A processor suited to the file type specified by the tab's path.
752
   */
753
  private Processor<String> createProcessors( final FileEditorTab tab ) {
754
    final var context = createProcessorContext( tab );
755
    return ProcessorFactory.createProcessors( context );
756
  }
757
758
  private ProcessorContext createProcessorContext(
759
      final FileEditorTab tab, final ExportFormat format ) {
760
    final var pane = getPreviewPane();
761
    final var map = getResolvedMap();
762
    return new ProcessorContext( pane, map, tab, format );
763
  }
764
765
  private ProcessorContext createProcessorContext( final FileEditorTab tab ) {
766
    return createProcessorContext( tab, NONE );
767
  }
768
769
  private DefinitionPane createDefinitionPane() {
770
    return new DefinitionPane();
771
  }
772
773
  private HTMLPreviewPane createHTMLPreviewPane() {
774
    return new HTMLPreviewPane();
775
  }
776
777
  private DefinitionSource createDefaultDefinitionSource() {
778
    return new YamlDefinitionSource( getDefinitionPath() );
779
  }
780
781
  private DefinitionSource createDefinitionSource( final Path path ) {
782
    try {
783
      return createDefinitionFactory().createDefinitionSource( path );
784
    } catch( final Exception ex ) {
785
      clue( ex );
786
      return createDefaultDefinitionSource();
787
    }
788
  }
789
790
  private TextField createFindTextField() {
791
    return new TextField();
792
  }
793
794
  private DefinitionFactory createDefinitionFactory() {
795
    return new DefinitionFactory();
796
  }
797
798
  private StatusBar createStatusBar() {
799
    return new StatusBar();
800
  }
801
802
  private Scene createScene() {
803
    final SplitPane splitPane = new SplitPane(
804
        getDefinitionPane(),
805
        getFileEditorPane(),
806
        getPreviewPane() );
807
808
    splitPane.setDividerPositions(
809
        getFloat( K_PANE_SPLIT_DEFINITION, .22f ),
810
        getFloat( K_PANE_SPLIT_EDITOR, .60f ),
811
        getFloat( K_PANE_SPLIT_PREVIEW, .18f ) );
812
813
    getDefinitionPane().prefHeightProperty()
814
                       .bind( splitPane.heightProperty() );
815
816
    final BorderPane borderPane = new BorderPane();
817
    borderPane.setPrefSize( 1280, 800 );
818
    borderPane.setTop( createMenuBar() );
819
    borderPane.setBottom( getStatusBar() );
820
    borderPane.setCenter( splitPane );
821
822
    final VBox statusBar = new VBox();
823
    statusBar.setAlignment( Pos.BASELINE_CENTER );
824
    statusBar.getChildren().add( getLineNumberText() );
825
    getStatusBar().getRightItems().add( statusBar );
826
827
    // Force preview pane refresh on Windows.
828
    if( SystemUtils.IS_OS_WINDOWS ) {
829
      splitPane.getDividers().get( 1 ).positionProperty().addListener(
830
          ( l, oValue, nValue ) -> runLater(
831
              () -> getPreviewPane().repaintScrollPane()
832
          )
833
      );
834
    }
835
836
    return new Scene( borderPane );
837
  }
838
839
  private Text createLineNumberText() {
840
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
841
  }
842
843
  private Node createMenuBar() {
844
    final BooleanBinding activeFileEditorIsNull =
845
        getFileEditorPane().activeFileEditorProperty().isNull();
846
847
    // File actions
848
    final Action fileNewAction = Action
849
        .builder()
850
        .setText( "Main.menu.file.new" )
851
        .setAccelerator( "Shortcut+N" )
852
        .setIcon( FILE_ALT )
853
        .setAction( e -> fileNew() )
854
        .build();
855
    final Action fileOpenAction = Action
856
        .builder()
857
        .setText( "Main.menu.file.open" )
858
        .setAccelerator( "Shortcut+O" )
859
        .setIcon( FOLDER_OPEN_ALT )
860
        .setAction( e -> fileOpen() )
861
        .build();
862
    final Action fileCloseAction = Action
863
        .builder()
864
        .setText( "Main.menu.file.close" )
865
        .setAccelerator( "Shortcut+W" )
866
        .setAction( e -> fileClose() )
867
        .setDisabled( activeFileEditorIsNull )
868
        .build();
869
    final Action fileCloseAllAction = Action
870
        .builder()
871
        .setText( "Main.menu.file.close_all" )
872
        .setAction( e -> fileCloseAll() )
873
        .setDisabled( activeFileEditorIsNull )
874
        .build();
875
    final Action fileSaveAction = Action
876
        .builder()
877
        .setText( "Main.menu.file.save" )
878
        .setAccelerator( "Shortcut+S" )
879
        .setIcon( FLOPPY_ALT )
880
        .setAction( e -> fileSave() )
881
        .setDisabled( createActiveBooleanProperty(
882
            FileEditorTab::modifiedProperty ).not() )
883
        .build();
884
    final Action fileSaveAsAction = Action
885
        .builder()
886
        .setText( "Main.menu.file.save_as" )
887
        .setAction( e -> fileSaveAs() )
888
        .setDisabled( activeFileEditorIsNull )
889
        .build();
890
    final Action fileSaveAllAction = Action
891
        .builder()
892
        .setText( "Main.menu.file.save_all" )
893
        .setAccelerator( "Shortcut+Shift+S" )
894
        .setAction( e -> fileSaveAll() )
895
        .setDisabled( Bindings.not(
896
            getFileEditorPane().anyFileEditorModifiedProperty() ) )
897
        .build();
898
    final Action fileExportAction = Action
899
        .builder()
900
        .setText( "Main.menu.file.export" )
901
        .build();
902
    final Action fileExportHtmlSvgAction = Action
903
        .builder()
904
        .setText( "Main.menu.file.export.html_svg" )
905
        .setAction( e -> fileExport( HTML_TEX_SVG ) )
906
        .build();
907
    final Action fileExportHtmlTexAction = Action
908
        .builder()
909
        .setText( "Main.menu.file.export.html_tex" )
910
        .setAction( e -> fileExport( HTML_TEX_DELIMITED ) )
911
        .build();
912
    final Action fileExportMarkdownAction = Action
913
        .builder()
914
        .setText( "Main.menu.file.export.markdown" )
915
        .setAction( e -> fileExport( MARKDOWN_PLAIN ) )
916
        .build();
917
    fileExportAction.addSubActions(
918
        fileExportHtmlSvgAction,
919
        fileExportHtmlTexAction,
920
        fileExportMarkdownAction );
921
922
    final Action fileExitAction = Action
923
        .builder()
924
        .setText( "Main.menu.file.exit" )
925
        .setAction( e -> fileExit() )
926
        .build();
927
928
    // Edit actions
929
    final Action editUndoAction = Action
930
        .builder()
931
        .setText( "Main.menu.edit.undo" )
932
        .setAccelerator( "Shortcut+Z" )
933
        .setIcon( UNDO )
934
        .setAction( e -> getActiveEditorPane().undo() )
935
        .setDisabled( createActiveBooleanProperty(
936
            FileEditorTab::canUndoProperty ).not() )
937
        .build();
938
    final Action editRedoAction = Action
939
        .builder()
940
        .setText( "Main.menu.edit.redo" )
941
        .setAccelerator( "Shortcut+Y" )
942
        .setIcon( REPEAT )
943
        .setAction( e -> getActiveEditorPane().redo() )
944
        .setDisabled( createActiveBooleanProperty(
945
            FileEditorTab::canRedoProperty ).not() )
946
        .build();
947
948
    final Action editCutAction = Action
949
        .builder()
950
        .setText( "Main.menu.edit.cut" )
951
        .setAccelerator( "Shortcut+X" )
952
        .setIcon( CUT )
953
        .setAction( e -> getActiveEditorPane().cut() )
954
        .setDisabled( activeFileEditorIsNull )
955
        .build();
956
    final Action editCopyAction = Action
957
        .builder()
958
        .setText( "Main.menu.edit.copy" )
959
        .setAccelerator( "Shortcut+C" )
960
        .setIcon( COPY )
961
        .setAction( e -> getActiveEditorPane().copy() )
962
        .setDisabled( activeFileEditorIsNull )
963
        .build();
964
    final Action editPasteAction = Action
965
        .builder()
966
        .setText( "Main.menu.edit.paste" )
967
        .setAccelerator( "Shortcut+V" )
968
        .setIcon( PASTE )
969
        .setAction( e -> getActiveEditorPane().paste() )
970
        .setDisabled( activeFileEditorIsNull )
971
        .build();
972
    final Action editSelectAllAction = Action
973
        .builder()
974
        .setText( "Main.menu.edit.selectAll" )
975
        .setAccelerator( "Shortcut+A" )
976
        .setAction( e -> getActiveEditorPane().selectAll() )
977
        .setDisabled( activeFileEditorIsNull )
978
        .build();
979
980
    final Action editFindAction = Action
981
        .builder()
982
        .setText( "Main.menu.edit.find" )
983
        .setAccelerator( "Ctrl+F" )
984
        .setIcon( SEARCH )
985
        .setAction( e -> editFind() )
986
        .setDisabled( activeFileEditorIsNull )
987
        .build();
988
    final Action editFindNextAction = Action
989
        .builder()
990
        .setText( "Main.menu.edit.find.next" )
991
        .setAccelerator( "F3" )
992
        .setAction( e -> editFindNext() )
993
        .setDisabled( activeFileEditorIsNull )
994
        .build();
995
    final Action editPreferencesAction = Action
996
        .builder()
997
        .setText( "Main.menu.edit.preferences" )
998
        .setAccelerator( "Ctrl+Alt+S" )
999
        .setAction( e -> editPreferences() )
1000
        .build();
1001
1002
    // Format actions
1003
    final Action formatBoldAction = Action
1004
        .builder()
1005
        .setText( "Main.menu.format.bold" )
1006
        .setAccelerator( "Shortcut+B" )
1007
        .setIcon( BOLD )
1008
        .setAction( e -> insertMarkdown( "**", "**" ) )
1009
        .setDisabled( activeFileEditorIsNull )
1010
        .build();
1011
    final Action formatItalicAction = Action
1012
        .builder()
1013
        .setText( "Main.menu.format.italic" )
1014
        .setAccelerator( "Shortcut+I" )
1015
        .setIcon( ITALIC )
1016
        .setAction( e -> insertMarkdown( "*", "*" ) )
1017
        .setDisabled( activeFileEditorIsNull )
1018
        .build();
1019
    final Action formatSuperscriptAction = Action
1020
        .builder()
1021
        .setText( "Main.menu.format.superscript" )
1022
        .setAccelerator( "Shortcut+[" )
1023
        .setIcon( SUPERSCRIPT )
1024
        .setAction( e -> insertMarkdown( "^", "^" ) )
1025
        .setDisabled( activeFileEditorIsNull )
1026
        .build();
1027
    final Action formatSubscriptAction = Action
1028
        .builder()
1029
        .setText( "Main.menu.format.subscript" )
1030
        .setAccelerator( "Shortcut+]" )
1031
        .setIcon( SUBSCRIPT )
1032
        .setAction( e -> insertMarkdown( "~", "~" ) )
1033
        .setDisabled( activeFileEditorIsNull )
1034
        .build();
1035
    final Action formatStrikethroughAction = Action
1036
        .builder()
1037
        .setText( "Main.menu.format.strikethrough" )
1038
        .setAccelerator( "Shortcut+T" )
1039
        .setIcon( STRIKETHROUGH )
1040
        .setAction( e -> insertMarkdown( "~~", "~~" ) )
1041
        .setDisabled( activeFileEditorIsNull )
1042
        .build();
1043
1044
    // Insert actions
1045
    final Action insertBlockquoteAction = Action
1046
        .builder()
1047
        .setText( "Main.menu.insert.blockquote" )
1048
        .setAccelerator( "Ctrl+Q" )
1049
        .setIcon( QUOTE_LEFT )
1050
        .setAction( e -> insertMarkdown( "\n\n> ", "" ) )
1051
        .setDisabled( activeFileEditorIsNull )
1052
        .build();
1053
    final Action insertCodeAction = Action
1054
        .builder()
1055
        .setText( "Main.menu.insert.code" )
1056
        .setAccelerator( "Shortcut+K" )
1057
        .setIcon( CODE )
1058
        .setAction( e -> insertMarkdown( "`", "`" ) )
1059
        .setDisabled( activeFileEditorIsNull )
1060
        .build();
1061
    final Action insertFencedCodeBlockAction = Action
1062
        .builder()
1063
        .setText( "Main.menu.insert.fenced_code_block" )
1064
        .setAccelerator( "Shortcut+Shift+K" )
1065
        .setIcon( FILE_CODE_ALT )
1066
        .setAction( e -> insertMarkdown(
1067
            "\n\n```\n",
1068
            "\n```\n\n",
1069
            get( "Main.menu.insert.fenced_code_block.prompt" ) ) )
1070
        .setDisabled( activeFileEditorIsNull )
1071
        .build();
1072
    final Action insertLinkAction = Action
1073
        .builder()
1074
        .setText( "Main.menu.insert.link" )
1075
        .setAccelerator( "Shortcut+L" )
1076
        .setIcon( LINK )
1077
        .setAction( e -> getActiveEditorPane().insertLink() )
1078
        .setDisabled( activeFileEditorIsNull )
1079
        .build();
1080
    final Action insertImageAction = Action
1081
        .builder()
1082
        .setText( "Main.menu.insert.image" )
1083
        .setAccelerator( "Shortcut+G" )
1084
        .setIcon( PICTURE_ALT )
1085
        .setAction( e -> getActiveEditorPane().insertImage() )
1086
        .setDisabled( activeFileEditorIsNull )
1087
        .build();
1088
1089
    // Number of heading actions (H1 ... H3)
1090
    final int HEADINGS = 3;
1091
    final Action[] headings = new Action[ HEADINGS ];
1092
1093
    for( int i = 1; i <= HEADINGS; i++ ) {
1094
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
1095
      final String markup = String.format( "%n%n%s ", hashes );
1096
      final String text = "Main.menu.insert.heading." + i;
1097
      final String accelerator = "Shortcut+" + i;
1098
      final String prompt = text + ".prompt";
1099
1100
      headings[ i - 1 ] = Action
1101
          .builder()
1102
          .setText( text )
1103
          .setAccelerator( accelerator )
1104
          .setIcon( HEADER )
1105
          .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) )
1106
          .setDisabled( activeFileEditorIsNull )
1107
          .build();
1108
    }
1109
1110
    final Action insertUnorderedListAction = Action
1111
        .builder()
1112
        .setText( "Main.menu.insert.unordered_list" )
1113
        .setAccelerator( "Shortcut+U" )
1114
        .setIcon( LIST_UL )
1115
        .setAction( e -> insertMarkdown( "\n\n* ", "" ) )
1116
        .setDisabled( activeFileEditorIsNull )
1117
        .build();
1118
    final Action insertOrderedListAction = Action
1119
        .builder()
1120
        .setText( "Main.menu.insert.ordered_list" )
1121
        .setAccelerator( "Shortcut+Shift+O" )
1122
        .setIcon( LIST_OL )
1123
        .setAction( e -> insertMarkdown(
1124
            "\n\n1. ", "" ) )
1125
        .setDisabled( activeFileEditorIsNull )
1126
        .build();
1127
    final Action insertHorizontalRuleAction = Action
1128
        .builder()
1129
        .setText( "Main.menu.insert.horizontal_rule" )
1130
        .setAccelerator( "Shortcut+H" )
1131
        .setAction( e -> insertMarkdown(
1132
            "\n\n---\n\n", "" ) )
1133
        .setDisabled( activeFileEditorIsNull )
1134
        .build();
1135
1136
    // Definition actions
1137
    final Action definitionCreateAction = Action
1138
        .builder()
1139
        .setText( "Main.menu.definition.create" )
1140
        .setIcon( TREE )
1141
        .setAction( e -> getDefinitionPane().addItem() )
1142
        .build();
1143
    final Action definitionInsertAction = Action
1144
        .builder()
1145
        .setText( "Main.menu.definition.insert" )
1146
        .setAccelerator( "Ctrl+Space" )
1147
        .setIcon( STAR )
1148
        .setAction( e -> definitionInsert() )
1149
        .build();
1150
1151
    // Help actions
1152
    final Action helpAboutAction = Action
1153
        .builder()
1154
        .setText( "Main.menu.help.about" )
1155
        .setAction( e -> helpAbout() )
1156
        .build();
1157
1158
    final Action SEPARATOR_ACTION = new SeparatorAction();
1159
1160
    //---- MenuBar ----
1161
1162
    // File Menu
1163
    final var fileMenu = ActionUtils.createMenu(
1164
        get( "Main.menu.file" ),
1165
        fileNewAction,
1166
        fileOpenAction,
1167
        SEPARATOR_ACTION,
1168
        fileCloseAction,
1169
        fileCloseAllAction,
1170
        SEPARATOR_ACTION,
1171
        fileSaveAction,
1172
        fileSaveAsAction,
1173
        fileSaveAllAction,
1174
        SEPARATOR_ACTION,
1175
        fileExportAction,
1176
        SEPARATOR_ACTION,
1177
        fileExitAction );
1178
1179
    // Edit Menu
1180
    final var editMenu = ActionUtils.createMenu(
1181
        get( "Main.menu.edit" ),
1182
        SEPARATOR_ACTION,
1183
        editUndoAction,
1184
        editRedoAction,
1185
        SEPARATOR_ACTION,
1186
        editCutAction,
1187
        editCopyAction,
1188
        editPasteAction,
1189
        editSelectAllAction,
1190
        SEPARATOR_ACTION,
1191
        editFindAction,
1192
        editFindNextAction,
1193
        SEPARATOR_ACTION,
1194
        editPreferencesAction );
1195
1196
    // Format Menu
1197
    final var formatMenu = ActionUtils.createMenu(
1198
        get( "Main.menu.format" ),
1199
        formatBoldAction,
1200
        formatItalicAction,
1201
        formatSuperscriptAction,
1202
        formatSubscriptAction,
1203
        formatStrikethroughAction
1204
    );
1205
1206
    // Insert Menu
1207
    final var insertMenu = ActionUtils.createMenu(
1208
        get( "Main.menu.insert" ),
1209
        insertBlockquoteAction,
1210
        insertCodeAction,
1211
        insertFencedCodeBlockAction,
1212
        SEPARATOR_ACTION,
1213
        insertLinkAction,
1214
        insertImageAction,
1215
        SEPARATOR_ACTION,
1216
        headings[ 0 ],
1217
        headings[ 1 ],
1218
        headings[ 2 ],
1219
        SEPARATOR_ACTION,
1220
        insertUnorderedListAction,
1221
        insertOrderedListAction,
1222
        insertHorizontalRuleAction
1223
    );
1224
1225
    // Definition Menu
1226
    final var definitionMenu = ActionUtils.createMenu(
1227
        get( "Main.menu.definition" ),
1228
        definitionCreateAction,
1229
        definitionInsertAction );
1230
1231
    // Help Menu
1232
    final var helpMenu = ActionUtils.createMenu(
1233
        get( "Main.menu.help" ),
1234
        helpAboutAction );
1235
1236
    //---- MenuBar ----
1237
    final var menuBar = new MenuBar(
1238
        fileMenu,
1239
        editMenu,
1240
        formatMenu,
1241
        insertMenu,
1242
        definitionMenu,
1243
        helpMenu );
1244
1245
    //---- ToolBar ----
1246
    final var toolBar = ActionUtils.createToolBar(
1247
        fileNewAction,
1248
        fileOpenAction,
1249
        fileSaveAction,
1250
        SEPARATOR_ACTION,
1251
        editUndoAction,
1252
        editRedoAction,
1253
        editCutAction,
1254
        editCopyAction,
1255
        editPasteAction,
1256
        SEPARATOR_ACTION,
1257
        formatBoldAction,
1258
        formatItalicAction,
1259
        formatSuperscriptAction,
1260
        formatSubscriptAction,
1261
        insertBlockquoteAction,
1262
        insertCodeAction,
1263
        insertFencedCodeBlockAction,
1264
        SEPARATOR_ACTION,
1265
        insertLinkAction,
1266
        insertImageAction,
1267
        SEPARATOR_ACTION,
1268
        headings[ 0 ],
1269
        SEPARATOR_ACTION,
1270
        insertUnorderedListAction,
1271
        insertOrderedListAction );
1272
1273
    return new VBox( menuBar, toolBar );
1274
  }
1275
1276
  /**
1277
   * Performs the autoinsert function on the active file editor.
1278
   */
1279
  private void definitionInsert() {
1280
    getDefinitionNameInjector().autoinsert();
1281
  }
1282
1283
  /**
1284
   * Creates a boolean property that is bound to another boolean value of the
1285
   * active editor.
1286
   */
1287
  private BooleanProperty createActiveBooleanProperty(
1288
      final Function<FileEditorTab, ObservableBooleanValue> func ) {
1289
1290
    final BooleanProperty b = new SimpleBooleanProperty();
1291
    final FileEditorTab tab = getActiveFileEditorTab();
1292
1293
    if( tab != null ) {
1294
      b.bind( func.apply( tab ) );
1295
    }
1296
1297
    getFileEditorPane().activeFileEditorProperty().addListener(
1298
        ( observable, oldFileEditor, newFileEditor ) -> {
1299
          b.unbind();
1300
1301
          if( newFileEditor == null ) {
1302
            b.set( false );
1303
          }
1304
          else {
1305
            b.bind( func.apply( newFileEditor ) );
1306
          }
1307
        }
1308
    );
1309
1310
    return b;
1311
  }
1312
1313
  //---- Convenience accessors ----------------------------------------------
1314
1315
  private Preferences getPreferences() {
1316
    return sOptions.getState();
1317
  }
1318
1319
  private int getCurrentParagraphIndex() {
1320
    return getActiveEditorPane().getCurrentParagraphIndex();
1321
  }
1322
1323
  private float getFloat( final String key, final float defaultValue ) {
1324
    return getPreferences().getFloat( key, defaultValue );
1325
  }
1326
1327
  public Window getWindow() {
1328
    return getScene().getWindow();
1329
  }
1330
1331
  private MarkdownEditorPane getActiveEditorPane() {
1332
    return getActiveFileEditorTab().getEditorPane();
1333
  }
1334
1335
  private FileEditorTab getActiveFileEditorTab() {
1336
    return getFileEditorPane().getActiveFileEditor();
1337
  }
1338
1339
  //---- Member accessors ---------------------------------------------------
1340
1341
  protected Scene getScene() {
1342
    return mScene;
1343
  }
1344
1345
  private SpellChecker getSpellChecker() {
1346
    return mSpellChecker;
1347
  }
1348
1349
  private Map<FileEditorTab, Processor<String>> getProcessors() {
1350
    return mProcessors;
1351
  }
1352
1353
  private FileEditorTabPane getFileEditorPane() {
1354
    return mFileEditorPane;
1355
  }
1356
1357
  private HTMLPreviewPane getPreviewPane() {
1358
    return mPreviewPane;
1359
  }
1360
1361
  private void setDefinitionSource(
1362
      final DefinitionSource definitionSource ) {
1363
    assert definitionSource != null;
1364
    mDefinitionSource = definitionSource;
1365
  }
1366
1367
  private DefinitionSource getDefinitionSource() {
1368
    return mDefinitionSource;
1369
  }
1370
1371
  private DefinitionPane getDefinitionPane() {
1372
    return mDefinitionPane;
1373
  }
1374
1375
  private Text getLineNumberText() {
1376
    return mLineNumberText;
1377
  }
1378
1379
  private StatusBar getStatusBar() {
1380
    return mStatusBar;
1381
  }
1382
1383
  private TextField getFindTextField() {
1384
    return mFindTextField;
1385
  }
1386
1387
  private DefinitionNameInjector getDefinitionNameInjector() {
1388
    return mDefinitionNameInjector;
1389
  }
1390
1391
  /**
1392
   * Returns the variable map of interpolated definitions.
1393
   *
1394
   * @return A map to help dereference variables.
1395
   */
1396
  private Map<String, String> getResolvedMap() {
1397
    return mResolvedMap;
1398
  }
1399
1400
  //---- Persistence accessors ----------------------------------------------
1401
1402
  private UserPreferences getUserPreferences() {
1403
    return UserPreferences.getInstance();
1404
  }
1405
1406
  private Path getDefinitionPath() {
1407
    return getUserPreferences().getDefinitionPath();
1408
  }
1409
1410
  //---- Spelling -----------------------------------------------------------
1411
1412
  /**
1413
   * Delegates to {@link #spellcheck(StyleClassedTextArea, String, int)}.
1414
   * This is called to spell check the document, rather than a single paragraph.
1415
   *
1416
   * @param text The full document text.
1417
   */
1418
  private void spellcheck(
1419
      final StyleClassedTextArea editor, final String text ) {
1420
    spellcheck( editor, text, -1 );
1421
  }
1422
1423
  /**
1424
   * Spellchecks a subset of the entire document.
1425
   *
1426
   * @param text   Look up words for this text in the lexicon.
1427
   * @param paraId Set to -1 to apply resulting style spans to the entire
1428
   *               text.
1429
   */
1430
  private void spellcheck(
1431
      final StyleClassedTextArea editor, final String text, final int paraId ) {
1432
    final var builder = new StyleSpansBuilder<Collection<String>>();
1433
    final var runningIndex = new AtomicInteger( 0 );
1434
    final var checker = getSpellChecker();
1435
1436
    // The text nodes must be relayed through a contextual "visitor" that
1437
    // can return text in chunks with correlative offsets into the string.
1438
    // This allows Markdown, R Markdown, XML, and R XML documents to return
1439
    // sets of words to check.
1440
1441
    final var node = mParser.parse( text );
1442
    final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> {
1443
      // Treat hyphenated compound words as individual words.
1444
      final var check = visited.replace( '-', ' ' );
1445
1446
      checker.proofread( check, ( misspelled, prevIndex, currIndex ) -> {
1447
        prevIndex += bIndex;
1448
        currIndex += bIndex;
1449
1450
        // Clear styling between lexiconically absent words.
1451
        builder.add( emptyList(), prevIndex - runningIndex.get() );
1452
        builder.add( singleton( "spelling" ), currIndex - prevIndex );
1453
        runningIndex.set( currIndex );
1454
      } );
1455
    } );
1456
1457
    visitor.visit( node );
1458
1459
    // If the running index was set, at least one word triggered the listener.
1460
    if( runningIndex.get() > 0 ) {
1461
      // Clear styling after the last lexiconically absent word.
1462
      builder.add( emptyList(), text.length() - runningIndex.get() );
1463
1464
      final var spans = builder.create();
1465
1466
      if( paraId >= 0 ) {
1467
        editor.setStyleSpans( paraId, 0, spans );
1468
      }
1469
      else {
1470
        editor.setStyleSpans( 0, spans );
1471
      }
1472
    }
1473
  }
1474
1475
  @SuppressWarnings("SameParameterValue")
1476
  private Collection<String> readLexicon( final String filename )
1477
      throws Exception {
1478
    final var path = "/" + LEXICONS_DIRECTORY + "/" + filename;
1479
1480
    try( final var resource = getClass().getResourceAsStream( path ) ) {
1481
      if( resource == null ) {
1482
        throw new MissingFileException( path );
1483
      }
1484
1485
      try( final var isr = new InputStreamReader( resource, UTF_8 );
1486
           final var reader = new BufferedReader( isr ) ) {
1487
        return reader.lines().collect( Collectors.toList() );
1488
      }
1489
    }
1490
  }
1491
1492
  // TODO: #59 -- Replace using Markdown processor instantiated for Markdown
1493
  //  files.
1494
  private final Parser mParser = Parser.builder().build();
1495
1496
  // TODO: #59 -- Replace with generic interface; provide Markdown/XML
1497
  //  implementations.
1498
  private static final class TextVisitor {
1499
    private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>(
1500
        com.vladsch.flexmark.ast.Text.class, this::visit )
1501
    );
1502
1503
    private final SpellCheckListener mConsumer;
1504
1505
    public TextVisitor( final SpellCheckListener consumer ) {
1506
      mConsumer = consumer;
1507
    }
1508
1509
    private void visit( final com.vladsch.flexmark.util.ast.Node node ) {
1510
      if( node instanceof com.vladsch.flexmark.ast.Text ) {
1511
        mConsumer.accept( node.getChars().toString(),
1512
                          node.getStartOffset(),
1513
                          node.getEndOffset() );
1514
      }
1515
1516
      mVisitor.visitChildren( node );
1517
    }
1518
  }
1519
}
15201
M src/main/java/com/keenwrite/Messages.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  * Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  * Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
272
package com.keenwrite;
3
4
import com.keenwrite.preferences.Key;
285
296
import java.text.MessageFormat;
7
import java.util.Enumeration;
308
import java.util.ResourceBundle;
319
import java.util.Stack;
...
12199
      return key;
122100
    }
101
  }
102
103
  /**
104
   * Returns the value for a key from the message bundle.
105
   *
106
   * @param key Retrieve the value for this key.
107
   * @return The value for the key.
108
   */
109
  public static String get( final Key key ) {
110
    return get( key.toString() );
123111
  }
124112
...
141129
  public static String get( final String key, final Object... args ) {
142130
    return MessageFormat.format( get( key ), args );
131
  }
132
133
  /**
134
   * Answers whether the given key is contained in the application's messages
135
   * properties file.
136
   *
137
   * @param key The key to look for in the {@link ResourceBundle}.
138
   * @return {@code true} when the key exists as an exact match.
139
   */
140
  public static boolean containsKey( final String key ) {
141
    return RESOURCE_BUNDLE.containsKey( key );
142
  }
143
144
  /**
145
   * Returns all key names in the application's messages properties file.
146
   *
147
   * @return All key names in the {@link ResourceBundle} encapsulated by
148
   * this class.
149
   */
150
  public static Enumeration<String> getKeys() {
151
    return RESOURCE_BUNDLE.getKeys();
143152
  }
144153
}
M src/main/java/com/keenwrite/ScrollEventHandler.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite;
293
304
import javafx.beans.property.BooleanProperty;
315
import javafx.beans.property.SimpleBooleanProperty;
326
import javafx.event.Event;
337
import javafx.event.EventHandler;
34
import javafx.scene.Node;
358
import javafx.scene.control.ScrollBar;
369
import javafx.scene.control.skin.ScrollBarSkin;
3710
import javafx.scene.input.MouseEvent;
3811
import javafx.scene.input.ScrollEvent;
3912
import javafx.scene.layout.StackPane;
4013
import org.fxmisc.flowless.VirtualizedScrollPane;
4114
import org.fxmisc.richtext.StyleClassedTextArea;
4215
4316
import javax.swing.*;
17
import java.util.function.Consumer;
4418
4519
import static javafx.geometry.Orientation.VERTICAL;
...
10276
    mEditorScrollPane.addEventFilter( ScrollEvent.ANY, new ScrollHandler() );
10377
104
    final var thumb = getVerticalScrollBarThumb( mEditorScrollPane );
105
    thumb.setOnMouseDragged( new MouseHandler( thumb.getOnMouseDragged() ) );
78
    initVerticalScrollBarThumb(
79
        mEditorScrollPane,
80
        thumb -> {
81
          final var handler = new MouseHandler( thumb.getOnMouseDragged() );
82
          thumb.setOnMouseDragged( handler );
83
        }
84
    );
10685
  }
10786
...
128107
    if( isEnabled() ) {
129108
      final var eScrollPane = getEditorScrollPane();
130
      final int eScrollY =
109
      final var eScrollY =
131110
          eScrollPane.estimatedScrollYProperty().getValue().intValue();
132
      final int eHeight = (int)
111
      final var eHeight = (int)
133112
          (eScrollPane.totalHeightEstimateProperty().getValue().intValue()
134113
              - eScrollPane.getHeight());
135
      final double eRatio = eHeight > 0
114
      final var eRatio = eHeight > 0
136115
          ? Math.min( Math.max( eScrollY / (float) eHeight, 0 ), 1 ) : 0;
137116
...
144123
    }
145124
  }
146
147
  private StackPane getVerticalScrollBarThumb(
148
      final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
149
    final ScrollBar scrollBar = getVerticalScrollBar( pane );
150
    final ScrollBarSkin skin = (ScrollBarSkin) (scrollBar.skinProperty().get());
151125
152
    for( final Node node : skin.getChildren() ) {
153
      // Brittle, but what can you do?
154
      if( node.getStyleClass().contains( "thumb" ) ) {
155
        return (StackPane) node;
126
  private void initVerticalScrollBarThumb(
127
      final VirtualizedScrollPane<StyleClassedTextArea> pane,
128
      final Consumer<StackPane> consumer ) {
129
    // When the skin property is set, the stack pane is available (not null).
130
    getVerticalScrollBar( pane ).skinProperty().addListener( ( c, o, n ) -> {
131
      for( final var node : ((ScrollBarSkin) n).getChildren() ) {
132
        // Brittle, but what can you do?
133
        if( node.getStyleClass().contains( "thumb" ) ) {
134
          consumer.accept( (StackPane) node );
135
        }
156136
      }
157
    }
158
159
    throw new IllegalArgumentException( "No scroll bar skin found." );
137
    } );
160138
  }
161139
140
  /**
141
   * Returns the vertical {@link ScrollBar} instance associated with the
142
   * given scroll pane. This is {@code null}-safe because the scroll pane
143
   * initializes its vertical {@link ScrollBar} upon construction.
144
   *
145
   * @param pane The scroll pane that contains a vertical {@link ScrollBar}.
146
   * @return The vertical {@link ScrollBar} associated with the scroll pane.
147
   * @throws IllegalStateException Could not obtain the vertical scroll bar.
148
   */
162149
  private ScrollBar getVerticalScrollBar(
163150
      final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
164151
165
    for( final Node node : pane.getChildrenUnmodifiable() ) {
152
    for( final var node : pane.getChildrenUnmodifiable() ) {
166153
      if( node instanceof ScrollBar ) {
167
        final ScrollBar scrollBar = (ScrollBar) node;
154
        final var scrollBar = (ScrollBar) node;
168155
169156
        if( scrollBar.getOrientation() == VERTICAL ) {
170157
          return scrollBar;
171158
        }
172159
      }
173160
    }
174161
175
    throw new IllegalArgumentException( "No vertical scroll pane found." );
162
    throw new IllegalStateException( "No vertical scroll bar found." );
176163
  }
177164
M src/main/java/com/keenwrite/Services.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite;
293
M src/main/java/com/keenwrite/StatusBarNotifier.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite;
293
...
4418
4519
  private static final Notifier sNotifier = Services.load( Notifier.class );
46
  private static StatusBar sStatusBar;
47
48
  public static void setStatusBar( final StatusBar statusBar ) {
49
    sStatusBar = statusBar;
50
  }
20
  private static final StatusBar sStatusBar = new StatusBar();
5121
5222
  /**
5323
   * Resets the status bar to a default message.
5424
   */
55
  public static void clearClue() {
25
  public static void clue() {
5626
    // Don't burden the repaint thread if there's no status bar change.
5727
    if( !OK.equals( sStatusBar.getText() ) ) {
...
9767
  public static Notifier getNotifier() {
9868
    return sNotifier;
69
  }
70
71
  public static StatusBar getStatusBar() {
72
    return sStatusBar;
9973
  }
10074
D src/main/java/com/keenwrite/adapters/DocumentAdapter.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.keenwrite.adapters;
29
30
import org.xhtmlrenderer.event.DocumentListener;
31
32
import static com.keenwrite.StatusBarNotifier.clue;
33
34
/**
35
 * Allows subclasses to implement only specific events of interest.
36
 */
37
public class DocumentAdapter implements DocumentListener {
38
  @Override
39
  public void documentStarted() {
40
  }
41
42
  @Override
43
  public void documentLoaded() {
44
  }
45
46
  @Override
47
  public void onLayoutException( final Throwable t ) {
48
    clue( t );
49
  }
50
51
  @Override
52
  public void onRenderException( final Throwable t ) {
53
    clue( t );
54
  }
55
}
561
D src/main/java/com/keenwrite/adapters/ReplacedElementAdapter.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.keenwrite.adapters;
29
30
import org.w3c.dom.Element;
31
import org.xhtmlrenderer.extend.ReplacedElementFactory;
32
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
33
34
/**
35
 * Allows subclasses to implement only specific events of interest.
36
 */
37
public abstract class ReplacedElementAdapter implements ReplacedElementFactory {
38
  @Override
39
  public void reset() {
40
  }
41
42
  @Override
43
  public void remove( final Element e ) {
44
  }
45
46
  @Override
47
  public void setFormSubmissionListener(
48
      final FormSubmissionListener listener ) {
49
  }
50
}
511
D src/main/java/com/keenwrite/controls/BrowseFileButton.java
1
/*
2
 * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.keenwrite.controls;
29
30
import com.keenwrite.Messages;
31
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
32
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
33
import javafx.beans.property.ObjectProperty;
34
import javafx.beans.property.SimpleObjectProperty;
35
import javafx.event.ActionEvent;
36
import javafx.scene.control.Button;
37
import javafx.scene.control.Tooltip;
38
import javafx.scene.input.KeyCode;
39
import javafx.scene.input.KeyEvent;
40
import javafx.stage.FileChooser;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
43
import java.io.File;
44
import java.nio.file.Path;
45
import java.util.ArrayList;
46
import java.util.List;
47
48
/**
49
 * Button that opens a file chooser to select a local file for a URL.
50
 */
51
public class BrowseFileButton extends Button {
52
  private final List<ExtensionFilter> extensionFilters = new ArrayList<>();
53
54
  public BrowseFileButton() {
55
    setGraphic(
56
        FontAwesomeIconFactory.get().createIcon( FontAwesomeIcon.FILE_ALT )
57
    );
58
    setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) );
59
    setOnAction( this::browse );
60
61
    disableProperty().bind( basePath.isNull() );
62
63
    // workaround for a JavaFX bug:
64
    //   avoid closing the dialog that contains this control when the user
65
    //   closes the FileChooser or DirectoryChooser using the ESC key
66
    addEventHandler( KeyEvent.KEY_RELEASED, e -> {
67
      if( e.getCode() == KeyCode.ESCAPE ) {
68
        e.consume();
69
      }
70
    } );
71
  }
72
73
  public void addExtensionFilter( ExtensionFilter extensionFilter ) {
74
    extensionFilters.add( extensionFilter );
75
  }
76
77
  // 'basePath' property
78
  private final ObjectProperty<Path> basePath = new SimpleObjectProperty<>();
79
80
  public Path getBasePath() {
81
    return basePath.get();
82
  }
83
84
  public void setBasePath( Path basePath ) {
85
    this.basePath.set( basePath );
86
  }
87
88
  // 'url' property
89
  private final ObjectProperty<String> url = new SimpleObjectProperty<>();
90
91
  public ObjectProperty<String> urlProperty() {
92
    return url;
93
  }
94
95
  protected void browse( ActionEvent e ) {
96
    FileChooser fileChooser = new FileChooser();
97
    fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) );
98
    fileChooser.getExtensionFilters().addAll( extensionFilters );
99
    fileChooser.getExtensionFilters()
100
               .add( new ExtensionFilter( Messages.get(
101
                   "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) );
102
    fileChooser.setInitialDirectory( getInitialDirectory() );
103
    File result = fileChooser.showOpenDialog( getScene().getWindow() );
104
    if( result != null ) {
105
      updateUrl( result );
106
    }
107
  }
108
109
  protected File getInitialDirectory() {
110
    //TODO build initial directory based on current value of 'url' property
111
    return getBasePath().toFile();
112
  }
113
114
  protected void updateUrl( File file ) {
115
    String newUrl;
116
    try {
117
      newUrl = getBasePath().relativize( file.toPath() ).toString();
118
    } catch( IllegalArgumentException ex ) {
119
      newUrl = file.toString();
120
    }
121
    url.set( newUrl.replace( '\\', '/' ) );
122
  }
123
}
1241
D src/main/java/com/keenwrite/controls/EscapeTextField.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.keenwrite.controls;
29
30
import javafx.beans.property.SimpleStringProperty;
31
import javafx.beans.property.StringProperty;
32
import javafx.scene.control.TextField;
33
import javafx.util.StringConverter;
34
35
/**
36
 * Responsible for escaping/unescaping characters for markdown.
37
 */
38
public class EscapeTextField extends TextField {
39
40
  public EscapeTextField() {
41
    escapedText.bindBidirectional(
42
        textProperty(),
43
        new StringConverter<>() {
44
          @Override
45
          public String toString( String object ) {
46
            return escape( object );
47
          }
48
49
          @Override
50
          public String fromString( String string ) {
51
            return unescape( string );
52
          }
53
        }
54
    );
55
    escapeCharacters.addListener(
56
        e -> escapedText.set( escape( textProperty().get() ) )
57
    );
58
  }
59
60
  // 'escapedText' property
61
  private final StringProperty escapedText = new SimpleStringProperty();
62
63
  public StringProperty escapedTextProperty() {
64
    return escapedText;
65
  }
66
67
  // 'escapeCharacters' property
68
  private final StringProperty escapeCharacters = new SimpleStringProperty();
69
70
  public String getEscapeCharacters() {
71
    return escapeCharacters.get();
72
  }
73
74
  public void setEscapeCharacters( String escapeCharacters ) {
75
    this.escapeCharacters.set( escapeCharacters );
76
  }
77
78
  private String escape( final String s ) {
79
    final String escapeChars = getEscapeCharacters();
80
81
    return isEmpty( escapeChars ) ? s :
82
        s.replaceAll( "([" + escapeChars.replaceAll(
83
            "(.)",
84
            "\\\\$1" ) + "])", "\\\\$1" );
85
  }
86
87
  private String unescape( final String s ) {
88
    final String escapeChars = getEscapeCharacters();
89
90
    return isEmpty( escapeChars ) ? s :
91
        s.replaceAll( "\\\\([" + escapeChars
92
            .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" );
93
  }
94
95
  private static boolean isEmpty( final String s ) {
96
    return s == null || s.isEmpty();
97
  }
98
}
991
D src/main/java/com/keenwrite/definition/DefinitionFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import com.keenwrite.AbstractFileFactory;
31
import com.keenwrite.FileType;
32
import com.keenwrite.definition.yaml.YamlDefinitionSource;
33
import com.keenwrite.util.ProtocolScheme;
34
35
import java.nio.file.Path;
36
37
import static com.keenwrite.Constants.GLOB_PREFIX_DEFINITION;
38
import static com.keenwrite.FileType.YAML;
39
import static com.keenwrite.util.ProtocolResolver.getProtocol;
40
import static java.lang.String.format;
41
42
/**
43
 * Responsible for creating objects that can read and write definition data
44
 * sources. The data source could be YAML, TOML, JSON, flat files, or from a
45
 * database.
46
 */
47
public class DefinitionFactory extends AbstractFileFactory {
48
49
  /**
50
   * TODO: Use an error message key from messages properties file.
51
   */
52
  private static final String MSG_UNKNOWN_FILE_TYPE =
53
      "Unknown type '%s' for file '%s'.";
54
55
  /**
56
   * Default (empty) constructor.
57
   */
58
  public DefinitionFactory() {
59
  }
60
61
  /**
62
   * Creates a definition source capable of reading definitions from the given
63
   * path.
64
   *
65
   * @param path Path to a resource containing definitions.
66
   * @return The definition source appropriate for the given path.
67
   */
68
  public DefinitionSource createDefinitionSource( final Path path ) {
69
    assert path != null;
70
71
    final var protocol = getProtocol( path.toString() );
72
    DefinitionSource result = null;
73
74
    if( protocol.isFile() ) {
75
      final FileType filetype = lookup( path, GLOB_PREFIX_DEFINITION );
76
      result = createFileDefinitionSource( filetype, path );
77
    }
78
    else {
79
      unknownFileType( protocol, path.toString() );
80
    }
81
82
    return result;
83
  }
84
85
  /**
86
   * Creates a definition source based on the file type.
87
   *
88
   * @param filetype Property key name suffix from settings.properties file.
89
   * @param path     Path to the file that corresponds to the extension.
90
   * @return A DefinitionSource capable of parsing the data stored at the path.
91
   */
92
  private DefinitionSource createFileDefinitionSource(
93
      final FileType filetype, final Path path ) {
94
    assert filetype != null;
95
    assert path != null;
96
97
    if( filetype == YAML ) {
98
      return new YamlDefinitionSource( path );
99
    }
100
101
    throw new IllegalArgumentException( filetype.toString() );
102
  }
103
104
  /**
105
   * Throws IllegalArgumentException because the given path could not be
106
   * recognized. This exists because
107
   *
108
   * @param type The detected path type (protocol, file extension, etc.).
109
   * @param path The path to a source of definitions.
110
   */
111
  private void unknownFileType(
112
      final ProtocolScheme type, final String path ) {
113
    final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path );
114
    throw new IllegalArgumentException( msg );
115
  }
116
}
1171
D src/main/java/com/keenwrite/definition/DefinitionPane.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
31
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
32
import javafx.beans.property.SimpleStringProperty;
33
import javafx.beans.property.StringProperty;
34
import javafx.collections.ObservableList;
35
import javafx.event.ActionEvent;
36
import javafx.event.Event;
37
import javafx.event.EventHandler;
38
import javafx.geometry.Insets;
39
import javafx.geometry.Pos;
40
import javafx.scene.Node;
41
import javafx.scene.control.*;
42
import javafx.scene.input.KeyEvent;
43
import javafx.scene.layout.BorderPane;
44
import javafx.scene.layout.HBox;
45
import javafx.util.StringConverter;
46
47
import java.util.*;
48
49
import static com.keenwrite.Messages.get;
50
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
51
import static javafx.geometry.Pos.CENTER;
52
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
53
54
/**
55
 * Provides the user interface that holds a {@link TreeView}, which
56
 * allows users to interact with key/value pairs loaded from the
57
 * {@link DocumentParser} and adapted using a {@link TreeAdapter}.
58
 */
59
public final class DefinitionPane extends BorderPane {
60
61
  /**
62
   * Contains a view of the definitions.
63
   */
64
  private final TreeView<String> mTreeView = new TreeView<>();
65
66
  /**
67
   * Handlers for key press events.
68
   */
69
  private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
70
      = new HashSet<>();
71
72
  /**
73
   * Definition file name shown in the title of the pane.
74
   */
75
  private final StringProperty mFilename = new SimpleStringProperty();
76
77
  private final TitledPane mTitledPane = new TitledPane();
78
79
  /**
80
   * Constructs a definition pane with a given tree view root.
81
   */
82
  public DefinitionPane() {
83
    final var treeView = getTreeView();
84
    treeView.setEditable( true );
85
    treeView.setCellFactory( cell -> createTreeCell() );
86
    treeView.setContextMenu( createContextMenu() );
87
    treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
88
    treeView.setShowRoot( false );
89
    getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
90
91
    final var bCreate = createButton(
92
        "create", TREE, e -> addItem() );
93
    final var bRename = createButton(
94
        "rename", EDIT, e -> editSelectedItem() );
95
    final var bDelete = createButton(
96
        "delete", TRASH, e -> deleteSelectedItems() );
97
98
    final var buttonBar = new HBox();
99
    buttonBar.getChildren().addAll( bCreate, bRename, bDelete );
100
    buttonBar.setAlignment( CENTER );
101
    buttonBar.setSpacing( 10 );
102
103
    final var titledPane = getTitledPane();
104
    titledPane.textProperty().bind( mFilename );
105
    titledPane.setContent( treeView );
106
    titledPane.setCollapsible( false );
107
    titledPane.setPadding( new Insets( 0, 0, 0, 0 ) );
108
109
    setTop( buttonBar );
110
    setCenter( titledPane );
111
    setAlignment( buttonBar, Pos.TOP_CENTER );
112
    setAlignment( titledPane, Pos.TOP_CENTER );
113
114
    titledPane.prefHeightProperty().bind( this.heightProperty() );
115
  }
116
117
  public void setTooltip( final Tooltip tooltip ) {
118
    getTitledPane().setTooltip( tooltip );
119
  }
120
121
  private TitledPane getTitledPane() {
122
    return mTitledPane;
123
  }
124
125
  private Button createButton(
126
      final String msgKey,
127
      final FontAwesomeIcon icon,
128
      final EventHandler<ActionEvent> eventHandler ) {
129
    final var keyPrefix = "Pane.definition.button." + msgKey;
130
    final var button = new Button( get( keyPrefix + ".label" ) );
131
    button.setOnAction( eventHandler );
132
133
    button.setGraphic(
134
        FontAwesomeIconFactory.get().createIcon( icon )
135
    );
136
    button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) );
137
138
    return button;
139
  }
140
141
  /**
142
   * Changes the root of the {@link TreeView} to the root of the
143
   * {@link TreeView} from the {@link DefinitionSource}.
144
   *
145
   * @param definitionSource Container for the hierarchy of key/value pairs
146
   *                         to replace the existing hierarchy.
147
   */
148
  public void update( final DefinitionSource definitionSource ) {
149
    assert definitionSource != null;
150
151
    final TreeAdapter treeAdapter = definitionSource.getTreeAdapter();
152
    final TreeItem<String> root = treeAdapter.adapt(
153
        get( "Pane.definition.node.root.title" )
154
    );
155
156
    getTreeView().setRoot( root );
157
  }
158
159
  public Map<String, String> toMap() {
160
    return TreeItemAdapter.toMap( getTreeView().getRoot() );
161
  }
162
163
  /**
164
   * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
165
   * is modified. The modifications include: item value changes, item additions,
166
   * and item removals.
167
   * <p>
168
   * Safe to call multiple times; if a handler is already registered, the
169
   * old handler is used.
170
   * </p>
171
   *
172
   * @param handler The handler to call whenever any {@link TreeItem} changes.
173
   */
174
  public void addTreeChangeHandler(
175
      final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
176
    final TreeItem<String> root = getTreeView().getRoot();
177
    root.addEventHandler( TreeItem.valueChangedEvent(), handler );
178
    root.addEventHandler( TreeItem.childrenModificationEvent(), handler );
179
  }
180
181
  public void addKeyEventHandler(
182
      final EventHandler<? super KeyEvent> handler ) {
183
    getKeyEventHandlers().add( handler );
184
  }
185
186
  /**
187
   * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
188
   * well-formed for export. A tree is considered well-formed if the following
189
   * conditions are met:
190
   *
191
   * <ul>
192
   *   <li>The root node contains at least one child node having a leaf.</li>
193
   *   <li>There are no leaf nodes with sibling leaf nodes.</li>
194
   * </ul>
195
   *
196
   * @return {@code null} if the document is well-formed, otherwise the
197
   * problematic child {@link TreeItem}.
198
   */
199
  public TreeItem<String> isTreeWellFormed() {
200
    final var root = getTreeView().getRoot();
201
202
    for( final var child : root.getChildren() ) {
203
      final var problemChild = isWellFormed( child );
204
205
      if( child.isLeaf() || problemChild != null ) {
206
        return problemChild;
207
      }
208
    }
209
210
    return null;
211
  }
212
213
  /**
214
   * Determines whether the document is well-formed by ensuring that
215
   * child branches do not contain multiple leaves.
216
   *
217
   * @param item The sub-tree to check for well-formedness.
218
   * @return {@code null} when the tree is well-formed, otherwise the
219
   * problematic {@link TreeItem}.
220
   */
221
  private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
222
    int childLeafs = 0;
223
    int childBranches = 0;
224
225
    for( final TreeItem<String> child : item.getChildren() ) {
226
      if( child.isLeaf() ) {
227
        childLeafs++;
228
      }
229
      else {
230
        childBranches++;
231
      }
232
233
      final var problemChild = isWellFormed( child );
234
235
      if( problemChild != null ) {
236
        return problemChild;
237
      }
238
    }
239
240
    return ((childBranches > 0 && childLeafs == 0) ||
241
        (childBranches == 0 && childLeafs <= 1)) ? null : item;
242
  }
243
244
  /**
245
   * Delegates to {@link DefinitionTreeItem#findLeafExact(String)}.
246
   *
247
   * @param text The value to find, never {@code null}.
248
   * @return The leaf that contains the given value, or {@code null} if
249
   * not found.
250
   */
251
  public DefinitionTreeItem<String> findLeafExact( final String text ) {
252
    return getTreeRoot().findLeafExact( text );
253
  }
254
255
  /**
256
   * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}.
257
   *
258
   * @param text The value to find, never {@code null}.
259
   * @return The leaf that contains the given value, or {@code null} if
260
   * not found.
261
   */
262
  public DefinitionTreeItem<String> findLeafContains( final String text ) {
263
    return getTreeRoot().findLeafContains( text );
264
  }
265
266
  /**
267
   * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}.
268
   *
269
   * @param text The value to find, never {@code null}.
270
   * @return The leaf that contains the given value, or {@code null} if
271
   * not found.
272
   */
273
  public DefinitionTreeItem<String> findLeafContainsNoCase(
274
      final String text ) {
275
    return getTreeRoot().findLeafContainsNoCase( text );
276
  }
277
278
  /**
279
   * Delegates to {@link DefinitionTreeItem#findLeafStartsWith(String)}.
280
   *
281
   * @param text The value to find, never {@code null}.
282
   * @return The leaf that contains the given value, or {@code null} if
283
   * not found.
284
   */
285
  public DefinitionTreeItem<String> findLeafStartsWith( final String text ) {
286
    return getTreeRoot().findLeafStartsWith( text );
287
  }
288
289
  /**
290
   * Expands the node to the root, recursively.
291
   *
292
   * @param <T>  The type of tree item to expand (usually String).
293
   * @param node The node to expand.
294
   */
295
  public <T> void expand( final TreeItem<T> node ) {
296
    if( node != null ) {
297
      expand( node.getParent() );
298
299
      if( !node.isLeaf() ) {
300
        node.setExpanded( true );
301
      }
302
    }
303
  }
304
305
  public void select( final TreeItem<String> item ) {
306
    getSelectionModel().clearSelection();
307
    getSelectionModel().select( getTreeView().getRow( item ) );
308
  }
309
310
  /**
311
   * Collapses the tree, recursively.
312
   */
313
  public void collapse() {
314
    collapse( getTreeRoot().getChildren() );
315
  }
316
317
  /**
318
   * Collapses the tree, recursively.
319
   *
320
   * @param <T>   The type of tree item to expand (usually String).
321
   * @param nodes The nodes to collapse.
322
   */
323
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
324
    for( final var node : nodes ) {
325
      node.setExpanded( false );
326
      collapse( node.getChildren() );
327
    }
328
  }
329
330
  /**
331
   * @return {@code true} when the user is editing a {@link TreeItem}.
332
   */
333
  private boolean isEditingTreeItem() {
334
    return getTreeView().editingItemProperty().getValue() != null;
335
  }
336
337
  /**
338
   * Changes to edit mode for the selected item.
339
   */
340
  private void editSelectedItem() {
341
    getTreeView().edit( getSelectedItem() );
342
  }
343
344
  /**
345
   * Removes all selected items from the {@link TreeView}.
346
   */
347
  private void deleteSelectedItems() {
348
    for( final var item : getSelectedItems() ) {
349
      final var parent = item.getParent();
350
351
      if( parent != null ) {
352
        parent.getChildren().remove( item );
353
      }
354
    }
355
  }
356
357
  /**
358
   * Deletes the selected item.
359
   */
360
  private void deleteSelectedItem() {
361
    final var c = getSelectedItem();
362
    getSiblings( c ).remove( c );
363
  }
364
365
  /**
366
   * Adds a new item under the selected item (or root if nothing is selected).
367
   * There are a few conditions to consider: when adding to the root,
368
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
369
   * root must contain two items: a key and a value.
370
   */
371
  public void addItem() {
372
    final var value = createTreeItem();
373
    getSelectedItem().getChildren().add( value );
374
    expand( value );
375
    select( value );
376
  }
377
378
  private ContextMenu createContextMenu() {
379
    final ContextMenu menu = new ContextMenu();
380
    final ObservableList<MenuItem> items = menu.getItems();
381
382
    addMenuItem( items, "Definition.menu.create" )
383
        .setOnAction( e -> addItem() );
384
385
    addMenuItem( items, "Definition.menu.rename" )
386
        .setOnAction( e -> editSelectedItem() );
387
388
    addMenuItem( items, "Definition.menu.remove" )
389
        .setOnAction( e -> deleteSelectedItem() );
390
391
    return menu;
392
  }
393
394
  /**
395
   * Executes hot-keys for edits to the definition tree.
396
   *
397
   * @param event Contains the key code of the key that was pressed.
398
   */
399
  private void keyEventFilter( final KeyEvent event ) {
400
    if( !isEditingTreeItem() ) {
401
      switch( event.getCode() ) {
402
        case ENTER:
403
          expand( getSelectedItem() );
404
          event.consume();
405
          break;
406
407
        case DELETE:
408
          deleteSelectedItems();
409
          break;
410
411
        case INSERT:
412
          addItem();
413
          break;
414
415
        case R:
416
          if( event.isControlDown() ) {
417
            editSelectedItem();
418
          }
419
420
          break;
421
      }
422
423
      for( final var handler : getKeyEventHandlers() ) {
424
        handler.handle( event );
425
      }
426
    }
427
  }
428
429
  /**
430
   * Adds a menu item to a list of menu items.
431
   *
432
   * @param items    The list of menu items to append to.
433
   * @param labelKey The resource bundle key name for the menu item's label.
434
   * @return The menu item added to the list of menu items.
435
   */
436
  private MenuItem addMenuItem(
437
      final List<MenuItem> items, final String labelKey ) {
438
    final MenuItem menuItem = createMenuItem( labelKey );
439
    items.add( menuItem );
440
    return menuItem;
441
  }
442
443
  private MenuItem createMenuItem( final String labelKey ) {
444
    return new MenuItem( get( labelKey ) );
445
  }
446
447
  private DefinitionTreeItem<String> createTreeItem() {
448
    return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) );
449
  }
450
451
  private TreeCell<String> createTreeCell() {
452
    return new FocusAwareTextFieldTreeCell( createStringConverter() ) {
453
      @Override
454
      public void commitEdit( final String newValue ) {
455
        super.commitEdit( newValue );
456
        select( getTreeItem() );
457
        requestFocus();
458
      }
459
    };
460
  }
461
462
  @Override
463
  public void requestFocus() {
464
    super.requestFocus();
465
    getTreeView().requestFocus();
466
  }
467
468
  private StringConverter<String> createStringConverter() {
469
    return new StringConverter<>() {
470
      @Override
471
      public String toString( final String object ) {
472
        return object == null ? "" : object;
473
      }
474
475
      @Override
476
      public String fromString( final String string ) {
477
        return string == null ? "" : string;
478
      }
479
    };
480
  }
481
482
  /**
483
   * Returns the tree view that contains the definition hierarchy.
484
   *
485
   * @return A non-null instance.
486
   */
487
  public TreeView<String> getTreeView() {
488
    return mTreeView;
489
  }
490
491
  /**
492
   * Returns this pane.
493
   *
494
   * @return this
495
   */
496
  public Node getNode() {
497
    return this;
498
  }
499
500
  /**
501
   * Returns the property used to set the title of the pane: the file name.
502
   *
503
   * @return A non-null property used for showing the definition file name.
504
   */
505
  public StringProperty filenameProperty() {
506
    return mFilename;
507
  }
508
509
  /**
510
   * Returns the root of the tree.
511
   *
512
   * @return The first node added to the definition tree.
513
   */
514
  private DefinitionTreeItem<String> getTreeRoot() {
515
    final var root = getTreeView().getRoot();
516
517
    return root instanceof DefinitionTreeItem
518
        ? (DefinitionTreeItem<String>) root
519
        : new DefinitionTreeItem<>( "root" );
520
  }
521
522
  private ObservableList<TreeItem<String>> getSiblings(
523
      final TreeItem<String> item ) {
524
    final var root = getTreeView().getRoot();
525
    final var parent = (item == null || item == root) ? root : item.getParent();
526
527
    return parent.getChildren();
528
  }
529
530
  private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
531
    return getTreeView().getSelectionModel();
532
  }
533
534
  /**
535
   * Returns a copy of all the selected items.
536
   *
537
   * @return A list, possibly empty, containing all selected items in the
538
   * {@link TreeView}.
539
   */
540
  private List<TreeItem<String>> getSelectedItems() {
541
    return new ArrayList<>( getSelectionModel().getSelectedItems() );
542
  }
543
544
  public TreeItem<String> getSelectedItem() {
545
    final var item = getSelectionModel().getSelectedItem();
546
    return item == null ? getTreeView().getRoot() : item;
547
  }
548
549
  private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() {
550
    return mKeyEventHandlers;
551
  }
552
553
  /**
554
   * Answers whether there are any definitions in the tree.
555
   *
556
   * @return {@code true} when there are no definitions; {@code false} when
557
   * there's at least one definition.
558
   */
559
  public boolean isEmpty() {
560
    return getTreeRoot().isEmpty();
561
  }
562
}
5631
D src/main/java/com/keenwrite/definition/DefinitionSource.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
/**
31
 * Represents behaviours for reading and writing string definitions. This
32
 * class cannot have any direct hooks into the user interface, as it defines
33
 * entry points into the definition data model loaded into an object
34
 * hierarchy. That hierarchy is converted to a UI model using an adapter
35
 * pattern.
36
 */
37
public interface DefinitionSource {
38
39
  /**
40
   * Creates an object capable of producing view-based objects from this
41
   * definition source.
42
   *
43
   * @return A hierarchical tree suitable for displaying in the definition pane.
44
   */
45
  TreeAdapter getTreeAdapter();
46
}
471
D src/main/java/com/keenwrite/definition/DefinitionTreeItem.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.keenwrite.definition;
29
30
import javafx.scene.control.TreeItem;
31
32
import java.util.Stack;
33
import java.util.function.BiFunction;
34
35
import static java.text.Normalizer.Form.NFD;
36
import static java.text.Normalizer.normalize;
37
38
/**
39
 * Provides behaviour afforded to definition keys and corresponding value.
40
 *
41
 * @param <T> The type of {@link TreeItem} (usually string).
42
 */
43
public class DefinitionTreeItem<T> extends TreeItem<T> {
44
45
  /**
46
   * Constructs a new item with a default value.
47
   *
48
   * @param value Passed up to superclass.
49
   */
50
  public DefinitionTreeItem( final T value ) {
51
    super( value );
52
  }
53
54
  /**
55
   * Finds a leaf starting at the current node with text that matches the given
56
   * value. Search is performed case-sensitively.
57
   *
58
   * @param text The text to match against each leaf in the tree.
59
   * @return The leaf that has a value exactly matching the given text.
60
   */
61
  public DefinitionTreeItem<T> findLeafExact( final String text ) {
62
    return findLeaf( text, DefinitionTreeItem::valueEquals );
63
  }
64
65
  /**
66
   * Finds a leaf starting at the current node with text that matches the given
67
   * value. Search is performed case-sensitively.
68
   *
69
   * @param text The text to match against each leaf in the tree.
70
   * @return The leaf that has a value that contains the given text.
71
   */
72
  public DefinitionTreeItem<T> findLeafContains( final String text ) {
73
    return findLeaf( text, DefinitionTreeItem::valueContains );
74
  }
75
76
  /**
77
   * Finds a leaf starting at the current node with text that matches the given
78
   * value. Search is performed case-insensitively.
79
   *
80
   * @param text The text to match against each leaf in the tree.
81
   * @return The leaf that has a value that contains the given text.
82
   */
83
  public DefinitionTreeItem<T> findLeafContainsNoCase( final String text ) {
84
    return findLeaf( text, DefinitionTreeItem::valueContainsNoCase );
85
  }
86
87
  /**
88
   * Finds a leaf starting at the current node with text that matches the given
89
   * value. Search is performed case-sensitively.
90
   *
91
   * @param text The text to match against each leaf in the tree.
92
   * @return The leaf that has a value that starts with the given text.
93
   */
94
  public DefinitionTreeItem<T> findLeafStartsWith( final String text ) {
95
    return findLeaf( text, DefinitionTreeItem::valueStartsWith );
96
  }
97
98
  /**
99
   * Finds a leaf starting at the current node with text that matches the given
100
   * value.
101
   *
102
   * @param text     The text to match against each leaf in the tree.
103
   * @param findMode What algorithm is used to match the given text.
104
   * @return The leaf that has a value starting with the given text, or {@code
105
   * null} if there was no match found.
106
   */
107
  public DefinitionTreeItem<T> findLeaf(
108
      final String text,
109
      final BiFunction<DefinitionTreeItem<T>, String, Boolean> findMode ) {
110
    final var stack = new Stack<DefinitionTreeItem<T>>();
111
    stack.push( this );
112
113
    // Don't hunt for blank (empty) keys.
114
    boolean found = text.isBlank();
115
116
    while( !found && !stack.isEmpty() ) {
117
      final var node = stack.pop();
118
119
      for( final var child : node.getChildren() ) {
120
        final var result = (DefinitionTreeItem<T>) child;
121
122
        if( result.isLeaf() ) {
123
          if( found = findMode.apply( result, text ) ) {
124
            return result;
125
          }
126
        }
127
        else {
128
          stack.push( result );
129
        }
130
      }
131
    }
132
133
    return null;
134
  }
135
136
  /**
137
   * Returns the value of the string without diacritic marks.
138
   *
139
   * @return A non-null, possibly empty string.
140
   */
141
  private String getDiacriticlessValue() {
142
    return normalize( getValue().toString(), NFD )
143
        .replaceAll( "\\p{M}", "" );
144
  }
145
146
  /**
147
   * Returns true if this node is a leaf and its value equals the given text.
148
   *
149
   * @param s The text to compare against the node value.
150
   * @return true Node is a leaf and its value equals the given value.
151
   */
152
  private boolean valueEquals( final String s ) {
153
    return isLeaf() && getValue().equals( s );
154
  }
155
156
  /**
157
   * Returns true if this node is a leaf and its value contains the given text.
158
   *
159
   * @param s The text to compare against the node value.
160
   * @return true Node is a leaf and its value contains the given value.
161
   */
162
  private boolean valueContains( final String s ) {
163
    return isLeaf() && getDiacriticlessValue().contains( s );
164
  }
165
166
  /**
167
   * Returns true if this node is a leaf and its value contains the given text.
168
   *
169
   * @param s The text to compare against the node value.
170
   * @return true Node is a leaf and its value contains the given value.
171
   */
172
  private boolean valueContainsNoCase( final String s ) {
173
    return isLeaf() && getDiacriticlessValue()
174
        .toLowerCase().contains( s.toLowerCase() );
175
  }
176
177
  /**
178
   * Returns true if this node is a leaf and its value starts with the given
179
   * text.
180
   *
181
   * @param s The text to compare against the node value.
182
   * @return true Node is a leaf and its value starts with the given value.
183
   */
184
  private boolean valueStartsWith( final String s ) {
185
    return isLeaf() && getDiacriticlessValue().startsWith( s );
186
  }
187
188
  /**
189
   * Returns the path for this node, with nodes made distinct using the
190
   * separator character. This uses two loops: one for pushing nodes onto a
191
   * stack and one for popping them off to create the path in desired order.
192
   *
193
   * @return A non-null string, possibly empty.
194
   */
195
  public String toPath() {
196
    return TreeItemAdapter.toPath( getParent() );
197
  }
198
199
  /**
200
   * Answers whether there are any definitions in this tree.
201
   *
202
   * @return {@code true} when there are no definitions in the tree; {@code
203
   * false} when there is at least one definition present.
204
   */
205
  public boolean isEmpty() {
206
    return getChildren().isEmpty();
207
  }
208
}
2091
D src/main/java/com/keenwrite/definition/DocumentParser.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
/**
31
 * Responsible for parsing structured document formats.
32
 *
33
 * @param <T> The type of "node" for the document's object model.
34
 */
35
public interface DocumentParser<T> {
36
37
  /**
38
   * Parses a document into a nested object hierarchy. The object returned
39
   * from this call must be the root node in the document tree.
40
   *
41
   * @return The document's root node, which may be empty but never null.
42
   */
43
  T getDocumentRoot();
44
}
451
D src/main/java/com/keenwrite/definition/FocusAwareTextFieldTreeCell.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.keenwrite.definition;
29
30
import javafx.scene.Node;
31
import javafx.scene.control.TextField;
32
import javafx.scene.control.cell.TextFieldTreeCell;
33
import javafx.util.StringConverter;
34
35
/**
36
 * Responsible for fixing a focus lost bug in the JavaFX implementation.
37
 * See https://bugs.openjdk.java.net/browse/JDK-8089514 for details.
38
 * This implementation borrows from the official documentation on creating
39
 * tree views: https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm
40
 */
41
public class FocusAwareTextFieldTreeCell extends TextFieldTreeCell<String> {
42
  private TextField mTextField;
43
44
  public FocusAwareTextFieldTreeCell(
45
      final StringConverter<String> converter ) {
46
    super( converter );
47
  }
48
49
  @Override
50
  public void startEdit() {
51
    super.startEdit();
52
    var textField = mTextField;
53
54
    if( textField == null ) {
55
      textField = createTextField();
56
    }
57
    else {
58
      textField.setText( getItem() );
59
    }
60
61
    setText( null );
62
    setGraphic( textField );
63
    textField.selectAll();
64
    textField.requestFocus();
65
66
    // When the focus is lost, commit the edit then close the input field.
67
    // This fixes the unexpected behaviour when user clicks away.
68
    textField.focusedProperty().addListener( ( l, o, n ) -> {
69
      if( !n ) {
70
        commitEdit( mTextField.getText() );
71
      }
72
    } );
73
74
    mTextField = textField;
75
  }
76
77
  @Override
78
  public void cancelEdit() {
79
    super.cancelEdit();
80
    setText( getItem() );
81
    setGraphic( getTreeItem().getGraphic() );
82
  }
83
84
  @Override
85
  public void updateItem( String item, boolean empty ) {
86
    super.updateItem( item, empty );
87
88
    String text = null;
89
    Node graphic = null;
90
91
    if( !empty ) {
92
      if( isEditing() ) {
93
        final var textField = mTextField;
94
95
        if( textField != null ) {
96
          textField.setText( getString() );
97
        }
98
99
        graphic = textField;
100
      }
101
      else {
102
        text = getString();
103
        graphic = getTreeItem().getGraphic();
104
      }
105
    }
106
107
    setText( text );
108
    setGraphic( graphic );
109
  }
110
111
  private TextField createTextField() {
112
    final var textField = new TextField( getString() );
113
114
    textField.setOnKeyReleased( t -> {
115
      switch( t.getCode() ) {
116
        case ENTER -> commitEdit( textField.getText() );
117
        case ESCAPE -> cancelEdit();
118
      }
119
    } );
120
121
    return textField;
122
  }
123
124
  private String getString() {
125
    return getConverter().toString( getItem() );
126
  }
127
}
1281
D src/main/java/com/keenwrite/definition/MapInterpolator.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import com.keenwrite.sigils.YamlSigilOperator;
31
32
import java.util.Map;
33
import java.util.regex.Matcher;
34
35
import static com.keenwrite.sigils.YamlSigilOperator.REGEX_PATTERN;
36
37
/**
38
 * Responsible for performing string interpolation on key/value pairs stored
39
 * in a map. The values in the map can use a delimited syntax to refer to
40
 * keys in the map.
41
 */
42
public class MapInterpolator {
43
  private static final int GROUP_DELIMITED = 1;
44
45
  /**
46
   * Empty.
47
   */
48
  private MapInterpolator() {
49
  }
50
51
  /**
52
   * Performs string interpolation on the values in the given map. This will
53
   * change any value in the map that contains a variable that matches
54
   * {@link YamlSigilOperator#REGEX_PATTERN}.
55
   *
56
   * @param map Contains values that represent references to keys.
57
   */
58
  public static void interpolate( final Map<String, String> map ) {
59
    map.replaceAll( ( k, v ) -> resolve( map, v ) );
60
  }
61
62
  /**
63
   * Given a value with zero or more key references, this will resolve all
64
   * the values, recursively. If a key cannot be dereferenced, the value will
65
   * contain the key name.
66
   *
67
   * @param map   Map to search for keys when resolving key references.
68
   * @param value Value containing zero or more key references
69
   * @return The given value with all embedded key references interpolated.
70
   */
71
  private static String resolve(
72
      final Map<String, String> map, String value ) {
73
    final Matcher matcher = REGEX_PATTERN.matcher( value );
74
75
    while( matcher.find() ) {
76
      final String keyName = matcher.group( GROUP_DELIMITED );
77
      final String mapValue = map.get( keyName );
78
      final String keyValue = mapValue == null
79
          ? keyName
80
          : resolve( map, mapValue );
81
82
      value = value.replace( keyName, keyValue );
83
    }
84
85
    return value;
86
  }
87
}
881
D src/main/java/com/keenwrite/definition/RootTreeItem.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import javafx.scene.control.TreeItem;
31
import javafx.scene.control.TreeView;
32
33
/**
34
 * Indicates that this is the top-most {@link TreeItem}. This class allows
35
 * the {@link TreeItemAdapter} to ignore the topmost definition. Such
36
 * contortions are necessary because {@link TreeView} requires a root item
37
 * that isn't part of the user's definition file.
38
 * <p>
39
 * Another approach would be to associate object pairs per {@link TreeItem},
40
 * but that would be a waste of memory since the only "exception" case is
41
 * the root {@link TreeItem}.
42
 * </p>
43
 *
44
 * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}.
45
 */
46
public class RootTreeItem<T> extends DefinitionTreeItem<T> {
47
  /**
48
   * Default constructor, calls the superclass, no other behaviour.
49
   *
50
   * @param value The {@link TreeItem} node name to construct the superclass.
51
   * @see TreeItemAdapter#toMap(TreeItem) for details on how this
52
   * class is used.
53
   */
54
  public RootTreeItem( final T value ) {
55
    super( value );
56
  }
57
}
581
D src/main/java/com/keenwrite/definition/TreeAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import javafx.scene.control.TreeItem;
31
32
import java.io.IOException;
33
import java.nio.file.Path;
34
35
/**
36
 * Responsible for converting an object hierarchy into a {@link TreeItem}
37
 * hierarchy.
38
 */
39
public interface TreeAdapter {
40
  /**
41
   * Adapts the document produced by the given parser into a {@link TreeItem}
42
   * object that can be presented to the user within a GUI.
43
   *
44
   * @param root The default root node name.
45
   * @return The parsed document in a {@link TreeItem} that can be displayed
46
   * in a panel.
47
   */
48
  TreeItem<String> adapt( String root );
49
50
  /**
51
   * Exports the given root node to the given path.
52
   *
53
   * @param root The root node to export.
54
   * @param path Where to persist the data.
55
   * @throws IOException Could not write the data to the given path.
56
   */
57
  void export( TreeItem<String> root, Path path ) throws IOException;
58
}
591
D src/main/java/com/keenwrite/definition/TreeItemAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.keenwrite.sigils.YamlSigilOperator;
32
import com.keenwrite.preview.HTMLPreviewPane;
33
import javafx.scene.control.TreeItem;
34
import javafx.scene.control.TreeView;
35
36
import java.util.HashMap;
37
import java.util.Iterator;
38
import java.util.Map;
39
import java.util.Stack;
40
41
import static com.keenwrite.Constants.DEFAULT_MAP_SIZE;
42
43
/**
44
 * Given a {@link TreeItem}, this will generate a flat map with all the
45
 * values in the tree recursively interpolated. The application integrates
46
 * definition files as follows:
47
 * <ol>
48
 *   <li>Load YAML file into {@link JsonNode} hierarchy.</li>
49
 *   <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li>
50
 *   <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li>
51
 *   <li>Substitute flat map variables into document as required.</li>
52
 * </ol>
53
 *
54
 * <p>
55
 * This class is responsible for producing the interpolated flat map. This
56
 * allows dynamic edits of the {@link TreeView} to be displayed in the
57
 * {@link HTMLPreviewPane} without having to reload the definition file.
58
 * Reloading the definition file would work, but has a number of drawbacks.
59
 * </p>
60
 */
61
public class TreeItemAdapter {
62
  /**
63
   * Separates YAML definition keys (e.g., the dots in {@code $root.node.var$}).
64
   */
65
  public static final String SEPARATOR = ".";
66
67
  /**
68
   * Default buffer length for keys ({@link StringBuilder} has 16 character
69
   * buffer) that should be large enough for most keys to avoid reallocating
70
   * memory to increase the {@link StringBuilder}'s buffer.
71
   */
72
  public static final int DEFAULT_KEY_LENGTH = 64;
73
74
  /**
75
   * In-order traversal of a {@link TreeItem} hierarchy, exposing each item
76
   * as a consecutive list.
77
   */
78
  private static final class TreeIterator
79
      implements Iterator<TreeItem<String>> {
80
    private final Stack<TreeItem<String>> mStack = new Stack<>();
81
82
    public TreeIterator( final TreeItem<String> root ) {
83
      if( root != null ) {
84
        mStack.push( root );
85
      }
86
    }
87
88
    @Override
89
    public boolean hasNext() {
90
      return !mStack.isEmpty();
91
    }
92
93
    @Override
94
    public TreeItem<String> next() {
95
      final TreeItem<String> next = mStack.pop();
96
      next.getChildren().forEach( mStack::push );
97
98
      return next;
99
    }
100
  }
101
102
  private TreeItemAdapter() {
103
  }
104
105
  /**
106
   * Iterate over a given root node (at any level of the tree) and process each
107
   * leaf node into a flat map. Values must be interpolated separately.
108
   */
109
  public static Map<String, String> toMap( final TreeItem<String> root ) {
110
    final Map<String, String> map = new HashMap<>( DEFAULT_MAP_SIZE );
111
    final TreeIterator iterator = new TreeIterator( root );
112
113
    iterator.forEachRemaining( item -> {
114
      if( item.isLeaf() ) {
115
        map.put( toPath( item.getParent() ), item.getValue() );
116
      }
117
    } );
118
119
    return map;
120
  }
121
122
123
  /**
124
   * For a given node, this will ascend the tree to generate a key name
125
   * that is associated with the leaf node's value.
126
   *
127
   * @param node Ascendants represent the key to this node's value.
128
   * @param <T>  Data type that the {@link TreeItem} contains.
129
   * @return The string representation of the node's unique key.
130
   */
131
  public static <T> String toPath( TreeItem<T> node ) {
132
    assert node != null;
133
134
    final StringBuilder key = new StringBuilder( DEFAULT_KEY_LENGTH );
135
    final Stack<TreeItem<T>> stack = new Stack<>();
136
137
    while( node != null && !(node instanceof RootTreeItem) ) {
138
      stack.push( node );
139
      node = node.getParent();
140
    }
141
142
    // Gets set at end of first iteration (to avoid an if condition).
143
    String separator = "";
144
145
    while( !stack.empty() ) {
146
      final T subkey = stack.pop().getValue();
147
      key.append( separator );
148
      key.append( subkey );
149
      separator = SEPARATOR;
150
    }
151
152
    return YamlSigilOperator.entoken( key.toString() );
153
  }
154
}
1551
D src/main/java/com/keenwrite/definition/yaml/YamlDefinitionSource.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition.yaml;
29
30
import com.keenwrite.definition.DefinitionSource;
31
import com.keenwrite.definition.TreeAdapter;
32
33
import java.nio.file.Path;
34
35
/**
36
 * Represents a definition data source for YAML files.
37
 */
38
public class YamlDefinitionSource implements DefinitionSource {
39
40
  private final YamlTreeAdapter mYamlTreeAdapter;
41
42
  /**
43
   * Constructs a new YAML definition source, populated from the given file.
44
   *
45
   * @param path Path to the YAML definition file.
46
   */
47
  public YamlDefinitionSource( final Path path ) {
48
    assert path != null;
49
50
    mYamlTreeAdapter = new YamlTreeAdapter( path );
51
  }
52
53
  @Override
54
  public TreeAdapter getTreeAdapter() {
55
    return mYamlTreeAdapter;
56
  }
57
}
581
D src/main/java/com/keenwrite/definition/yaml/YamlParser.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition.yaml;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.fasterxml.jackson.databind.ObjectMapper;
32
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
33
import com.keenwrite.definition.DocumentParser;
34
35
import java.io.InputStream;
36
import java.nio.file.Files;
37
import java.nio.file.Path;
38
39
/**
40
 * Responsible for reading a YAML document into an object hierarchy.
41
 */
42
public class YamlParser implements DocumentParser<JsonNode> {
43
44
  /**
45
   * Start of the Universe (the YAML document node that contains all others).
46
   */
47
  private final JsonNode mDocumentRoot;
48
49
  /**
50
   * Creates a new YamlParser instance that attempts to parse the contents
51
   * of the YAML document given from a path. In the event that the file either
52
   * does not exist or is empty, a fake
53
   *
54
   * @param path Path to a file containing YAML data to parse.
55
   */
56
  public YamlParser( final Path path ) {
57
    assert path != null;
58
    mDocumentRoot = parse( path );
59
  }
60
61
  /**
62
   * Returns the parent node for the entire YAML document tree.
63
   *
64
   * @return The document root, never {@code null}.
65
   */
66
  @Override
67
  public JsonNode getDocumentRoot() {
68
    return mDocumentRoot;
69
  }
70
71
  /**
72
   * Parses the given path containing YAML data into an object hierarchy.
73
   *
74
   * @param path {@link Path} to the YAML resource to parse.
75
   * @return The parsed contents, or an empty object hierarchy.
76
   */
77
  private JsonNode parse( final Path path ) {
78
    try( final InputStream in = Files.newInputStream( path ) ) {
79
      return new ObjectMapper( new YAMLFactory() ).readTree( in );
80
    } catch( final Exception e ) {
81
      // Ensure that a document root node exists by relying on the
82
      // default failure condition when processing. This is required
83
      // because the input stream could not be read.
84
      return new ObjectMapper().createObjectNode();
85
    }
86
  }
87
}
881
D src/main/java/com/keenwrite/definition/yaml/YamlTreeAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition.yaml;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.fasterxml.jackson.databind.node.ObjectNode;
32
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
33
import com.keenwrite.definition.RootTreeItem;
34
import com.keenwrite.definition.TreeAdapter;
35
import com.keenwrite.definition.DefinitionTreeItem;
36
import javafx.scene.control.TreeItem;
37
import javafx.scene.control.TreeView;
38
39
import java.io.IOException;
40
import java.nio.file.Path;
41
import java.util.Map.Entry;
42
43
/**
44
 * Transforms a JsonNode hierarchy into a tree that can be displayed in a user
45
 * interface and vice-versa.
46
 */
47
public class YamlTreeAdapter implements TreeAdapter {
48
  private final YamlParser mParser;
49
50
  /**
51
   * Constructs a new instance that will use the given path to read
52
   * the object hierarchy from a data source.
53
   *
54
   * @param path Path to YAML contents to parse.
55
   */
56
  public YamlTreeAdapter( final Path path ) {
57
    mParser = new YamlParser( path );
58
  }
59
60
  @Override
61
  public void export( final TreeItem<String> treeItem, final Path path )
62
      throws IOException {
63
    final YAMLMapper mapper = new YAMLMapper();
64
    final ObjectNode root = mapper.createObjectNode();
65
66
    // Iterate over the root item's children. The root item is used by the
67
    // application to ensure definitions can always be added to a tree, as
68
    // such it is not meant to be exported, only its children.
69
    for( final TreeItem<String> child : treeItem.getChildren() ) {
70
      export( child, root );
71
    }
72
73
    // Writes as UTF8 by default.
74
    mapper.writeValue( path.toFile(), root );
75
  }
76
77
  /**
78
   * Recursive method to generate an object hierarchy that represents the
79
   * given {@link TreeItem} hierarchy.
80
   *
81
   * @param item The {@link TreeItem} to reproduce as an object hierarchy.
82
   * @param node The {@link ObjectNode} to update to reflect the
83
   *             {@link TreeItem} hierarchy.
84
   */
85
  private void export( final TreeItem<String> item, ObjectNode node ) {
86
    final var children = item.getChildren();
87
88
    // If the current item has more than one non-leaf child, it's an
89
    // object node and must become a new nested object.
90
    if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) {
91
      node = node.putObject( item.getValue() );
92
    }
93
94
    for( final TreeItem<String> child : children ) {
95
      if( child.isLeaf() ) {
96
        node.put( item.getValue(), child.getValue() );
97
      }
98
      else {
99
        export( child, node );
100
      }
101
    }
102
  }
103
104
  /**
105
   * Converts a YAML document to a {@link TreeItem} based on the document
106
   * keys. Only the first document in the stream is adapted.
107
   *
108
   * @param root Root {@link TreeItem} node name.
109
   * @return A {@link TreeItem} populated with all the keys in the YAML
110
   * document.
111
   */
112
  public TreeItem<String> adapt( final String root ) {
113
    final JsonNode rootNode = getYamlParser().getDocumentRoot();
114
    final TreeItem<String> rootItem = createRootTreeItem( root );
115
116
    rootItem.setExpanded( true );
117
    adapt( rootNode, rootItem );
118
    return rootItem;
119
  }
120
121
  /**
122
   * Iterate over a given root node (at any level of the tree) and adapt each
123
   * leaf node.
124
   *
125
   * @param rootNode A JSON node (YAML node) to adapt.
126
   * @param rootItem The tree item to use as the root when processing the node.
127
   */
128
  private void adapt(
129
      final JsonNode rootNode, final TreeItem<String> rootItem ) {
130
    rootNode.fields().forEachRemaining(
131
        ( Entry<String, JsonNode> leaf ) -> adapt( leaf, rootItem )
132
    );
133
  }
134
135
  /**
136
   * Recursively adapt each rootNode to a corresponding rootItem.
137
   *
138
   * @param rootNode The node to adapt.
139
   * @param rootItem The item to adapt using the node's key.
140
   */
141
  private void adapt(
142
      final Entry<String, JsonNode> rootNode,
143
      final TreeItem<String> rootItem ) {
144
    final JsonNode leafNode = rootNode.getValue();
145
    final String key = rootNode.getKey();
146
    final TreeItem<String> leaf = createTreeItem( key );
147
148
    if( leafNode.isValueNode() ) {
149
      leaf.getChildren().add( createTreeItem( rootNode.getValue().asText() ) );
150
    }
151
152
    rootItem.getChildren().add( leaf );
153
154
    if( leafNode.isObject() ) {
155
      adapt( leafNode, leaf );
156
    }
157
  }
158
159
  /**
160
   * Creates a new {@link TreeItem} that can be added to the {@link TreeView}.
161
   *
162
   * @param value The node's value.
163
   * @return A new {@link TreeItem}, never {@code null}.
164
   */
165
  private TreeItem<String> createTreeItem( final String value ) {
166
    return new DefinitionTreeItem<>( value );
167
  }
168
169
  /**
170
   * Creates a new {@link TreeItem} that is intended to be the root-level item
171
   * added to the {@link TreeView}. This allows the root item to be
172
   * distinguished from the other items so that reference keys do not include
173
   * "Definition" as part of their name.
174
   *
175
   * @param value The node's value.
176
   * @return A new {@link TreeItem}, never {@code null}.
177
   */
178
  private TreeItem<String> createRootTreeItem( final String value ) {
179
    return new RootTreeItem<>( value );
180
  }
181
182
  public YamlParser getYamlParser() {
183
    return mParser;
184
  }
185
}
1861
D src/main/java/com/keenwrite/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.keenwrite.dialogs;
29
30
import static com.keenwrite.Messages.get;
31
import com.keenwrite.service.events.impl.ButtonOrderPane;
32
import static javafx.scene.control.ButtonType.CANCEL;
33
import static javafx.scene.control.ButtonType.OK;
34
import javafx.scene.control.Dialog;
35
import javafx.stage.Window;
36
37
/**
38
 * Superclass that abstracts common behaviours for all dialogs.
39
 *
40
 * @param <T> The type of dialog to create (usually String).
41
 */
42
public abstract class AbstractDialog<T> extends Dialog<T> {
43
44
  /**
45
   * Ensures that all dialogs can be closed.
46
   *
47
   * @param owner The parent window of this dialog.
48
   * @param title The messages title to display in the title bar.
49
   */
50
  @SuppressWarnings( "OverridableMethodCallInConstructor" )
51
  public AbstractDialog( final Window owner, final String title ) {
52
    setTitle( get( title ) );
53
    setResizable( true );
54
55
    initOwner( owner );
56
    initCloseAction();
57
    initDialogPane();
58
    initDialogButtons();
59
    initComponents();
60
  }
61
62
  /**
63
   * Initialize the component layout.
64
   */
65
  protected abstract void initComponents();
66
67
  /**
68
   * Set the dialog to use a button order pane with an OK and a CANCEL button.
69
   */
70
  protected void initDialogPane() {
71
    setDialogPane( new ButtonOrderPane() );
72
  }
73
  
74
  /**
75
   * Set an OK and CANCEL button on the dialog.
76
   */
77
  protected void initDialogButtons() {
78
    getDialogPane().getButtonTypes().addAll( OK, CANCEL );
79
  }
80
81
  /**
82
   * Attaches a setOnCloseRequest to the dialog's [X] button so that the user
83
   * can always close the window, even if there's an error.
84
   */
85
  protected final void initCloseAction() {
86
    final Window window = getDialogPane().getScene().getWindow();
87
    window.setOnCloseRequest( event -> window.hide() );
88
  }
89
}
901
D src/main/java/com/keenwrite/dialogs/ImageDialog.java
1
/*
2
 * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.dialogs;
28
29
import static com.keenwrite.Messages.get;
30
import com.keenwrite.controls.BrowseFileButton;
31
import com.keenwrite.controls.EscapeTextField;
32
import java.nio.file.Path;
33
import javafx.application.Platform;
34
import javafx.beans.binding.Bindings;
35
import javafx.beans.property.SimpleStringProperty;
36
import javafx.beans.property.StringProperty;
37
import javafx.scene.control.ButtonBar.ButtonData;
38
import static javafx.scene.control.ButtonType.OK;
39
import javafx.scene.control.DialogPane;
40
import javafx.scene.control.Label;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
import javafx.stage.Window;
43
import org.tbee.javafx.scene.layout.fxml.MigPane;
44
45
/**
46
 * Dialog to enter a markdown image.
47
 */
48
public class ImageDialog extends AbstractDialog<String> {
49
50
  private final StringProperty image = new SimpleStringProperty();
51
52
  public ImageDialog( final Window owner, final Path basePath ) {
53
    super(owner, "Dialog.image.title" );
54
    
55
    final DialogPane dialogPane = getDialogPane();
56
    dialogPane.setContent( pane );
57
58
    linkBrowseFileButton.setBasePath( basePath );
59
    linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
60
    linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() );
61
62
    dialogPane.lookupButton( OK ).disableProperty().bind(
63
      urlField.escapedTextProperty().isEmpty()
64
      .or( textField.escapedTextProperty().isEmpty() ) );
65
66
    image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "![%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.format( "![%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) );
69
    previewField.textProperty().bind( image );
70
71
    setResultConverter( dialogButton -> {
72
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
73
      return (data == ButtonData.OK_DONE) ? image.get() : null;
74
    } );
75
76
    Platform.runLater( () -> {
77
      urlField.requestFocus();
78
79
      if( urlField.getText().startsWith( "http://" ) ) {
80
        urlField.selectRange( "http://".length(), urlField.getLength() );
81
      }
82
    } );
83
  }
84
85
  @Override
86
  protected void initComponents() {
87
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
88
    pane = new MigPane();
89
    Label urlLabel = new Label();
90
    urlField = new EscapeTextField();
91
    linkBrowseFileButton = new BrowseFileButton();
92
    Label textLabel = new Label();
93
    textField = new EscapeTextField();
94
    Label titleLabel = new Label();
95
    titleField = new EscapeTextField();
96
    Label previewLabel = new Label();
97
    previewField = new Label();
98
99
    //======== pane ========
100
    {
101
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" );
102
      pane.setRows( "[][][][]" );
103
104
      //---- urlLabel ----
105
      urlLabel.setText( get( "Dialog.image.urlLabel.text" ) );
106
      pane.add( urlLabel, "cell 0 0" );
107
108
      //---- urlField ----
109
      urlField.setEscapeCharacters( "()" );
110
      urlField.setText( "http://yourlink.com" );
111
      urlField.setPromptText( "http://yourlink.com" );
112
      pane.add( urlField, "cell 1 0" );
113
      pane.add( linkBrowseFileButton, "cell 2 0" );
114
115
      //---- textLabel ----
116
      textLabel.setText( get( "Dialog.image.textLabel.text" ) );
117
      pane.add( textLabel, "cell 0 1" );
118
119
      //---- textField ----
120
      textField.setEscapeCharacters( "[]" );
121
      pane.add( textField, "cell 1 1 2 1" );
122
123
      //---- titleLabel ----
124
      titleLabel.setText( get( "Dialog.image.titleLabel.text" ) );
125
      pane.add( titleLabel, "cell 0 2" );
126
      pane.add( titleField, "cell 1 2 2 1" );
127
128
      //---- previewLabel ----
129
      previewLabel.setText( get( "Dialog.image.previewLabel.text" ) );
130
      pane.add( previewLabel, "cell 0 3" );
131
      pane.add( previewField, "cell 1 3 2 1" );
132
    }
133
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
134
  }
135
136
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
137
  private MigPane pane;
138
  private EscapeTextField urlField;
139
  private BrowseFileButton linkBrowseFileButton;
140
  private EscapeTextField textField;
141
  private EscapeTextField titleField;
142
  private Label previewField;
143
	// JFormDesigner - End of variables declaration  //GEN-END:variables
144
}
1451
D src/main/java/com/keenwrite/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
}
871
D src/main/java/com/keenwrite/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.keenwrite.dialogs;
29
30
import com.keenwrite.controls.EscapeTextField;
31
import com.keenwrite.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.keenwrite.Messages.get;
43
import static javafx.scene.control.ButtonType.OK;
44
45
/**
46
 * Dialog to enter a markdown link.
47
 */
48
public class LinkDialog extends AbstractDialog<String> {
49
50
  private final StringProperty link = new SimpleStringProperty();
51
52
  public LinkDialog(
53
    final Window owner, final HyperlinkModel hyperlink ) {
54
    super( owner, "Dialog.link.title" );
55
56
    final DialogPane dialogPane = getDialogPane();
57
    dialogPane.setContent( pane );
58
59
    dialogPane.lookupButton( OK ).disableProperty().bind(
60
      urlField.escapedTextProperty().isEmpty() );
61
62
    textField.setText( hyperlink.getText() );
63
    urlField.setText( hyperlink.getUrl() );
64
    titleField.setText( hyperlink.getTitle() );
65
66
    link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() )
69
        .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) )
70
        .otherwise( urlField.escapedTextProperty() ) ) );
71
72
    setResultConverter( dialogButton -> {
73
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
74
      return (data == ButtonData.OK_DONE) ? link.get() : null;
75
    } );
76
77
    Platform.runLater( () -> {
78
      urlField.requestFocus();
79
      urlField.selectRange( 0, urlField.getLength() );
80
    } );
81
  }
82
83
  @Override
84
  protected void initComponents() {
85
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
86
    pane = new MigPane();
87
    Label urlLabel = new Label();
88
    urlField = new EscapeTextField();
89
    Label textLabel = new Label();
90
    textField = new EscapeTextField();
91
    Label titleLabel = new Label();
92
    titleField = new EscapeTextField();
93
94
    //======== pane ========
95
    {
96
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" );
97
      pane.setRows( "[][][][]" );
98
99
      //---- urlLabel ----
100
      urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
101
      pane.add( urlLabel, "cell 0 0" );
102
103
      //---- urlField ----
104
      urlField.setEscapeCharacters( "()" );
105
      pane.add( urlField, "cell 1 0" );
106
107
      //---- textLabel ----
108
      textLabel.setText( get( "Dialog.link.textLabel.text" ) );
109
      pane.add( textLabel, "cell 0 1" );
110
111
      //---- textField ----
112
      textField.setEscapeCharacters( "[]" );
113
      pane.add( textField, "cell 1 1 3 1" );
114
115
      //---- titleLabel ----
116
      titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
117
      pane.add( titleLabel, "cell 0 2" );
118
      pane.add( titleField, "cell 1 2 3 1" );
119
    }
120
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
121
  }
122
123
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
124
  private MigPane pane;
125
  private EscapeTextField urlField;
126
  private EscapeTextField textField;
127
  private EscapeTextField titleField;
128
  // JFormDesigner - End of variables declaration  //GEN-END:variables
129
}
1301
D src/main/java/com/keenwrite/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
}
921
D src/main/java/com/keenwrite/editors/DefinitionDecoratorFactory.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.keenwrite.editors;
29
30
import com.keenwrite.AbstractFileFactory;
31
import com.keenwrite.sigils.RSigilOperator;
32
import com.keenwrite.sigils.SigilOperator;
33
import com.keenwrite.sigils.YamlSigilOperator;
34
35
import java.nio.file.Path;
36
37
/**
38
 * Responsible for creating a definition name decorator suited to a particular
39
 * file type.
40
 */
41
public class DefinitionDecoratorFactory extends AbstractFileFactory {
42
43
  /**
44
   * Prevent instantiation.
45
   */
46
  private DefinitionDecoratorFactory() {
47
  }
48
49
  public static SigilOperator newInstance( final Path path ) {
50
    return switch( lookup( path ) ) {
51
      case RMARKDOWN, RXML -> new RSigilOperator();
52
      default -> new YamlSigilOperator();
53
    };
54
  }
55
}
561
D src/main/java/com/keenwrite/editors/DefinitionNameInjector.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.keenwrite.editors;
29
30
import com.keenwrite.FileEditorTab;
31
import com.keenwrite.definition.DefinitionPane;
32
import com.keenwrite.definition.DefinitionTreeItem;
33
import com.keenwrite.sigils.SigilOperator;
34
import javafx.scene.control.TreeItem;
35
import javafx.scene.input.KeyEvent;
36
import org.fxmisc.richtext.StyledTextArea;
37
38
import java.nio.file.Path;
39
import java.text.BreakIterator;
40
41
import static com.keenwrite.Constants.*;
42
import static com.keenwrite.StatusBarNotifier.clue;
43
import static java.lang.Character.isWhitespace;
44
import static javafx.scene.input.KeyCode.SPACE;
45
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
46
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
47
48
/**
49
 * Provides the logic for injecting variable names within the editor.
50
 */
51
public final class DefinitionNameInjector {
52
53
  /**
54
   * Recipient of name injections.
55
   */
56
  private FileEditorTab mTab;
57
58
  /**
59
   * Initiates double-click events.
60
   */
61
  private final DefinitionPane mDefinitionPane;
62
63
  /**
64
   * Initializes the variable name injector against the given pane.
65
   *
66
   * @param pane The definition panel to listen to for double-click events.
67
   */
68
  public DefinitionNameInjector( final DefinitionPane pane ) {
69
    mDefinitionPane = pane;
70
  }
71
72
  /**
73
   * Trap Control+Space.
74
   *
75
   * @param tab Editor where variable names get injected.
76
   */
77
  public void addListener( final FileEditorTab tab ) {
78
    assert tab != null;
79
    mTab = tab;
80
81
    tab.getEditorPane().addKeyboardListener(
82
        keyPressed( SPACE, CONTROL_DOWN ),
83
        this::autoinsert
84
    );
85
  }
86
87
  /**
88
   * Inserts the currently selected variable from the {@link DefinitionPane}.
89
   */
90
  public void injectSelectedItem() {
91
    final var pane = getDefinitionPane();
92
    final TreeItem<String> item = pane.getSelectedItem();
93
94
    if( item.isLeaf() ) {
95
      final var leaf = pane.findLeafExact( item.getValue() );
96
      final var editor = getEditor();
97
98
      editor.insertText( editor.getCaretPosition(), decorate( leaf ) );
99
    }
100
  }
101
102
  /**
103
   * Pressing Control+SPACE will find a node that matches the current word and
104
   * substitute the definition reference.
105
   */
106
  public void autoinsert() {
107
    final String paragraph = getCaretParagraph();
108
    final int[] bounds = getWordBoundariesAtCaret();
109
110
    try {
111
      if( isEmptyDefinitionPane() ) {
112
        clue( STATUS_DEFINITION_EMPTY );
113
      }
114
      else {
115
        final String word = paragraph.substring( bounds[ 0 ], bounds[ 1 ] );
116
117
        if( word.isBlank() ) {
118
          clue( STATUS_DEFINITION_BLANK );
119
        }
120
        else {
121
          final var leaf = findLeaf( word );
122
123
          if( leaf == null ) {
124
            clue( STATUS_DEFINITION_MISSING, word );
125
          }
126
          else {
127
            replaceText( bounds[ 0 ], bounds[ 1 ], decorate( leaf ) );
128
            expand( leaf );
129
          }
130
        }
131
      }
132
    } catch( final Exception ignored ) {
133
      clue( STATUS_DEFINITION_BLANK );
134
    }
135
  }
136
137
  /**
138
   * Pressing Control+SPACE will find a node that matches the current word and
139
   * substitute the definition reference.
140
   *
141
   * @param e Ignored -- it can only be Control+SPACE.
142
   */
143
  private void autoinsert( final KeyEvent e ) {
144
    autoinsert();
145
  }
146
147
  /**
148
   * Finds the start and end indexes for the word in the current paragraph
149
   * where the caret is located. There are a few different scenarios, where
150
   * the caret can be at: the start, end, or middle of a word; also, the
151
   * caret can be at the end or beginning of a punctuated word; as well, the
152
   * caret could be at the beginning or end of the line or document.
153
   */
154
  private int[] getWordBoundariesAtCaret() {
155
    final var paragraph = getCaretParagraph();
156
    final var length = paragraph.length();
157
    int offset = getCurrentCaretColumn();
158
159
    int began = offset;
160
    int ended = offset;
161
162
    while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
163
      began--;
164
    }
165
166
    while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
167
      ended++;
168
    }
169
170
    final var iterator = BreakIterator.getWordInstance();
171
    iterator.setText( paragraph );
172
173
    while( began < length && iterator.isBoundary( began + 1 ) ) {
174
      began++;
175
    }
176
177
    while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
178
      ended--;
179
    }
180
181
    return new int[]{began, ended};
182
  }
183
184
  /**
185
   * Decorates a {@link TreeItem} using the syntax specific to the type of
186
   * document being edited.
187
   *
188
   * @param leaf The path to the leaf (the definition key) to be decorated.
189
   */
190
  private String decorate( final DefinitionTreeItem<String> leaf ) {
191
    return decorate( leaf.toPath() );
192
  }
193
194
  /**
195
   * Decorates a variable using the syntax specific to the type of document
196
   * being edited.
197
   *
198
   * @param variable The variable to decorate in dot-notation without any
199
   *                 start or end sigils present.
200
   */
201
  private String decorate( final String variable ) {
202
    return getVariableDecorator().apply( variable );
203
  }
204
205
  /**
206
   * Updates the text at the given position within the current paragraph.
207
   *
208
   * @param posBegan The starting index in the paragraph text to replace.
209
   * @param posEnded The ending index in the paragraph text to replace.
210
   * @param text     Overwrite the paragraph substring with this text.
211
   */
212
  private void replaceText(
213
      final int posBegan, final int posEnded, final String text ) {
214
    final int p = getCurrentParagraph();
215
216
    getEditor().replaceText( p, posBegan, p, posEnded, text );
217
  }
218
219
  /**
220
   * Returns the caret's current paragraph position.
221
   *
222
   * @return A number greater than or equal to 0.
223
   */
224
  private int getCurrentParagraph() {
225
    return getEditor().getCurrentParagraph();
226
  }
227
228
  /**
229
   * Returns the text for the paragraph that contains the caret.
230
   *
231
   * @return A non-null string, possibly empty.
232
   */
233
  private String getCaretParagraph() {
234
    return getEditor().getText( getCurrentParagraph() );
235
  }
236
237
  /**
238
   * Returns the caret position within the current paragraph.
239
   *
240
   * @return A value from 0 to the length of the current paragraph.
241
   */
242
  private int getCurrentCaretColumn() {
243
    return getEditor().getCaretColumn();
244
  }
245
246
  /**
247
   * Looks for the given word, matching first by exact, next by a starts-with
248
   * condition with diacritics replaced, then by containment.
249
   *
250
   * @param word The word to match by: exact, at the beginning, or containment.
251
   * @return The matching {@link DefinitionTreeItem} for the given word, or
252
   * {@code null} if none found.
253
   */
254
  @SuppressWarnings("ConstantConditions")
255
  private DefinitionTreeItem<String> findLeaf( final String word ) {
256
    assert word != null;
257
258
    final var pane = getDefinitionPane();
259
    DefinitionTreeItem<String> leaf = null;
260
261
    leaf = leaf == null ? pane.findLeafExact( word ) : leaf;
262
    leaf = leaf == null ? pane.findLeafStartsWith( word ) : leaf;
263
    leaf = leaf == null ? pane.findLeafContains( word ) : leaf;
264
    leaf = leaf == null ? pane.findLeafContainsNoCase( word ) : leaf;
265
266
    return leaf;
267
  }
268
269
  /**
270
   * Answers whether there are any definitions in the tree.
271
   *
272
   * @return {@code true} when there are no definitions; {@code false} when
273
   * there's at least one definition.
274
   */
275
  private boolean isEmptyDefinitionPane() {
276
    return getDefinitionPane().isEmpty();
277
  }
278
279
  /**
280
   * Collapses the tree then expands and selects the given node.
281
   *
282
   * @param node The node to expand.
283
   */
284
  private void expand( final TreeItem<String> node ) {
285
    final DefinitionPane pane = getDefinitionPane();
286
    pane.collapse();
287
    pane.expand( node );
288
    pane.select( node );
289
  }
290
291
  /**
292
   * @return A variable decorator that corresponds to the given file type.
293
   */
294
  private SigilOperator getVariableDecorator() {
295
    return DefinitionDecoratorFactory.newInstance( getFilename() );
296
  }
297
298
  private Path getFilename() {
299
    return getFileEditorTab().getPath();
300
  }
301
302
  private EditorPane getEditorPane() {
303
    return getFileEditorTab().getEditorPane();
304
  }
305
306
  private StyledTextArea<?, ?> getEditor() {
307
    return getEditorPane().getEditor();
308
  }
309
310
  public FileEditorTab getFileEditorTab() {
311
    return mTab;
312
  }
313
314
  private DefinitionPane getDefinitionPane() {
315
    return mDefinitionPane;
316
  }
317
}
3181
D src/main/java/com/keenwrite/editors/EditorPane.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.editors;
29
30
import com.keenwrite.preferences.UserPreferences;
31
import javafx.beans.property.IntegerProperty;
32
import javafx.beans.property.ObjectProperty;
33
import javafx.beans.property.SimpleObjectProperty;
34
import javafx.beans.value.ChangeListener;
35
import javafx.event.Event;
36
import javafx.scene.control.ScrollPane;
37
import javafx.scene.layout.Pane;
38
import org.fxmisc.flowless.VirtualizedScrollPane;
39
import org.fxmisc.richtext.StyleClassedTextArea;
40
import org.fxmisc.undo.UndoManager;
41
import org.fxmisc.wellbehaved.event.EventPattern;
42
import org.fxmisc.wellbehaved.event.Nodes;
43
44
import java.nio.file.Path;
45
import java.util.function.Consumer;
46
47
import static com.keenwrite.StatusBarNotifier.clearClue;
48
import static java.lang.String.format;
49
import static javafx.application.Platform.runLater;
50
import static org.fxmisc.wellbehaved.event.InputMap.consume;
51
52
/**
53
 * Represents common editing features for various types of text editors.
54
 */
55
public class EditorPane extends Pane {
56
57
  /**
58
   * Used when changing the text area font size.
59
   */
60
  private static final String FMT_CSS_FONT_SIZE = "-fx-font-size: %dpt;";
61
62
  private final StyleClassedTextArea mEditor =
63
      new StyleClassedTextArea( false );
64
  private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
65
      new VirtualizedScrollPane<>( mEditor );
66
  private final ObjectProperty<Path> mPath = new SimpleObjectProperty<>();
67
68
  public EditorPane() {
69
    getScrollPane().setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS );
70
    fontsSizeProperty().addListener(
71
        ( l, o, n ) -> setFontSize( n.intValue() )
72
    );
73
74
    // Clear out any previous alerts after the user has typed. If the problem
75
    // persists, re-rendering the document will re-raise the error. If there
76
    // was no previous error, clearing the alert is essentially a no-op.
77
    mEditor.textProperty().addListener(
78
        ( l, o, n ) -> clearClue()
79
    );
80
  }
81
82
  @Override
83
  public void requestFocus() {
84
    requestFocus( 3 );
85
  }
86
87
  /**
88
   * There's a race-condition between displaying the {@link EditorPane}
89
   * and giving the {@link #mEditor} focus. Try to focus up to {@code max}
90
   * times before giving up.
91
   *
92
   * @param max The number of attempts to try to request focus.
93
   */
94
  private void requestFocus( final int max ) {
95
    if( max > 0 ) {
96
      runLater(
97
          () -> {
98
            final var editor = getEditor();
99
100
            if( !editor.isFocused() ) {
101
              editor.requestFocus();
102
              requestFocus( max - 1 );
103
            }
104
          }
105
      );
106
    }
107
  }
108
109
  public void undo() {
110
    getUndoManager().undo();
111
  }
112
113
  public void redo() {
114
    getUndoManager().redo();
115
  }
116
117
  /**
118
   * Cuts the actively selected text; if no text is selected, this will cut
119
   * the entire paragraph.
120
   */
121
  public void cut() {
122
    final var editor = getEditor();
123
    final var selected = editor.getSelectedText();
124
125
    if( selected == null || selected.isEmpty() ) {
126
      editor.selectParagraph();
127
    }
128
129
    editor.cut();
130
  }
131
132
  public void copy() {
133
    getEditor().copy();
134
  }
135
136
  public void paste() {
137
    getEditor().paste();
138
  }
139
140
  public void selectAll() {
141
    getEditor().selectAll();
142
  }
143
144
  public UndoManager<?> getUndoManager() {
145
    return getEditor().getUndoManager();
146
  }
147
148
  public String getText() {
149
    return getEditor().getText();
150
  }
151
152
  public void setText( final String text ) {
153
    final var editor = getEditor();
154
    editor.deselect();
155
    editor.replaceText( text );
156
    getUndoManager().mark();
157
  }
158
159
  /**
160
   * Call to hook into changes to the text area.
161
   *
162
   * @param listener Receives editor text change events.
163
   */
164
  public void addTextChangeListener(
165
      final ChangeListener<? super String> listener ) {
166
    getEditor().textProperty().addListener( listener );
167
  }
168
169
  /**
170
   * Notifies observers when the caret changes position.
171
   *
172
   * @param listener Receives change event.
173
   */
174
  public void addCaretPositionListener(
175
      final ChangeListener<? super Integer> listener ) {
176
    getEditor().caretPositionProperty().addListener( listener );
177
  }
178
179
  /**
180
   * This method adds listeners to editor events.
181
   *
182
   * @param <T>      The event type.
183
   * @param <U>      The consumer type for the given event type.
184
   * @param event    The event of interest.
185
   * @param consumer The method to call when the event happens.
186
   */
187
  public <T extends Event, U extends T> void addKeyboardListener(
188
      final EventPattern<? super T, ? extends U> event,
189
      final Consumer<? super U> consumer ) {
190
    Nodes.addInputMap( getEditor(), consume( event, consumer ) );
191
  }
192
193
  /**
194
   * Repositions the cursor and scroll bar to the top of the file.
195
   */
196
  public void scrollToTop() {
197
    getEditor().moveTo( 0 );
198
    getScrollPane().scrollYToPixel( 0 );
199
  }
200
201
  public StyleClassedTextArea getEditor() {
202
    return mEditor;
203
  }
204
205
  /**
206
   * Returns the scroll pane that contains the text area.
207
   *
208
   * @return The scroll pane that contains the content to edit.
209
   */
210
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
211
    return mScrollPane;
212
  }
213
214
  public Path getPath() {
215
    return mPath.get();
216
  }
217
218
  public void setPath( final Path path ) {
219
    mPath.set( path );
220
  }
221
222
  /**
223
   * Sets the font size in points.
224
   *
225
   * @param size The new font size to use for the text editor.
226
   */
227
  private void setFontSize( final int size ) {
228
    mEditor.setStyle( format( FMT_CSS_FONT_SIZE, size ) );
229
  }
230
231
  /**
232
   * Returns the text editor font size property for handling font size change
233
   * events.
234
   */
235
  private IntegerProperty fontsSizeProperty() {
236
    return UserPreferences.getInstance().fontsSizeEditorProperty();
237
  }
238
}
2391
A src/main/java/com/keenwrite/editors/TextDefinition.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors;
3
4
import com.keenwrite.editors.definition.DefinitionEditor;
5
import com.keenwrite.editors.definition.DefinitionTreeItem;
6
import com.keenwrite.editors.markdown.MarkdownEditor;
7
import com.keenwrite.sigils.Tokens;
8
import javafx.scene.control.TreeItem;
9
10
import java.util.Map;
11
12
/**
13
 * Differentiates an instance of {@link TextResource} from an instance of
14
 * {@link DefinitionEditor} or {@link MarkdownEditor}.
15
 */
16
public interface TextDefinition extends TextResource {
17
  /**
18
   * Converts the definitions into a map, ready for interpolation.
19
   *
20
   * @return The list of key value pairs delimited with tokens.
21
   */
22
  Map<String, String> toMap();
23
24
  /**
25
   * Performs string interpolation on the values in the given map. This will
26
   * change any value in the map that contains a variable that matches
27
   * the definition regex pattern against the given {@link Tokens}.
28
   *
29
   * @param map Contains values that represent references to keys.
30
   * @param tokens The beginning and ending tokens that delimit variables.
31
   */
32
  Map<String, String> interpolate( Map<String, String> map, Tokens tokens );
33
34
  /**
35
   * Requests that the visual representation be expanded to the given
36
   * node.
37
   *
38
   * @param node Request expansion to this node.
39
   */
40
  <T> void expand( TreeItem<T> node );
41
42
  /**
43
   * Adds a new item to the definition hierarchy.
44
   */
45
  void createDefinition();
46
47
  /**
48
   * Edits the currently selected definition in the hierarchy.
49
   */
50
  void renameDefinition();
51
52
  /**
53
   * Removes the currently selected definition in the hierarchy.
54
   */
55
  void deleteDefinitions();
56
57
  /**
58
   * Finds the definition that exact matches the given text.
59
   *
60
   * @param text The value to find, never {@code null}.
61
   * @return The leaf that contains the given value.
62
   */
63
  DefinitionTreeItem<String> findLeafExact( String text );
64
65
  /**
66
   * Finds the definition that starts with the given text.
67
   *
68
   * @param text The value to find, never {@code null}.
69
   * @return The leaf that starts with the given value.
70
   */
71
  DefinitionTreeItem<String> findLeafStartsWith( String text );
72
73
  /**
74
   * Finds the definition that contains the given text, matching case.
75
   *
76
   * @param text The value to find, never {@code null}.
77
   * @return The leaf that contains the exact given value.
78
   */
79
  DefinitionTreeItem<String> findLeafContains( String text );
80
81
  /**
82
   * Finds the definition that contains the given text, ignoring case.
83
   *
84
   * @param text The value to find, never {@code null}.
85
   * @return The leaf that contains the given value, regardless of case.
86
   */
87
  DefinitionTreeItem<String> findLeafContainsNoCase( String text );
88
89
  /**
90
   * Answers whether there are any definitions written.
91
   *
92
   * @return {@code true} when there are no definitions.
93
   */
94
  boolean isEmpty();
95
}
196
A src/main/java/com/keenwrite/editors/TextEditor.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors;
3
4
import com.keenwrite.processors.markdown.Caret;
5
import javafx.scene.control.IndexRange;
6
import org.fxmisc.flowless.VirtualizedScrollPane;
7
import org.fxmisc.richtext.StyleClassedTextArea;
8
9
/**
10
 * Responsible for differentiating an instance of {@link TextResource} from
11
 * other {@link TextResource} subtypes, such as a {@link TextDefinition}.
12
 * This is primarily used as a marker interface, but also defines a minimal
13
 * set of functionality required by all {@link TextEditor} instances, which
14
 * includes scrolling facilities.
15
 */
16
public interface TextEditor extends TextResource {
17
18
  /**
19
   * Returns the scrollbars associated with the editor's view so that they
20
   * can be moved for synchronized scrolling.
21
   *
22
   * @return The initialized horizontal and vertical scrollbars.
23
   */
24
  VirtualizedScrollPane<StyleClassedTextArea> getScrollPane();
25
26
  StyleClassedTextArea getTextArea();
27
28
  /**
29
   * Requests that styling be added to the document between the given
30
   * integer values.
31
   *
32
   * @param indexes Document offset where style is to start and end.
33
   * @param style The style class to apply between the given offset indexes.
34
   */
35
  default void stylize( final IndexRange indexes, final String style ) {
36
  }
37
38
  /**
39
   * Requests that the most recent styling for the given style class be
40
   * removed from the document between the given integer values.
41
   */
42
  default void unstylize( final String style ) {
43
  }
44
45
  /**
46
   * Returns the complete text for the specified paragraph index.
47
   *
48
   * @param paragraph The zero-based paragraph index.
49
   * @throws IndexOutOfBoundsException The paragraph index is less than zero
50
   *                                   or greater than the number of
51
   *                                   paragraphs in the document.
52
   */
53
  String getText( int paragraph ) throws IndexOutOfBoundsException;
54
55
  /**
56
   * Returns the text between the indexes specified by the given
57
   * {@link IndexRange}.
58
   *
59
   * @param indexes The start and end document indexes to reference.
60
   * @return The text between the specified indexes.
61
   * @throws IndexOutOfBoundsException The indexes are invalid.
62
   */
63
  String getText( IndexRange indexes ) throws IndexOutOfBoundsException;
64
65
  /**
66
   * Moves the caret to the given document offset.
67
   *
68
   * @param offset The absolute offset into the document, zero-based.
69
   */
70
  void moveTo( final int offset );
71
72
  /**
73
   * Returns an object that can be used to track the current caret position
74
   * within the document.
75
   *
76
   * @return The caret's position, which is updated continuously.
77
   */
78
  Caret getCaret();
79
80
  /**
81
   * Replaces the text within the given range with the given string.
82
   *
83
   * @param indexes The starting and ending document indexes that represent
84
   *                the range of text to replace.
85
   * @param s       The text to replace, which can be shorter or longer than the
86
   *                specified range.
87
   */
88
  void replaceText( IndexRange indexes, String s );
89
90
  /**
91
   * Returns the starting and ending indexes into the document for the
92
   * word at the current caret position.
93
   * <p>
94
   * Finds the start and end indexes for the word in the current document,
95
   * where the caret is located. There are a few different scenarios, where
96
   * the caret can be at: the start, end, or middle of a word; also, the
97
   * caret can be at the end or beginning of a punctuated word; as well, the
98
   * caret could be at the beginning or end of the line or document.
99
   * </p>
100
   *
101
   * @return The start and ending index into the current document that
102
   * represent the word boundaries of the word under the caret.
103
   */
104
  IndexRange getCaretWord();
105
106
  /**
107
   * Convenience method to get the word at the current caret position.
108
   *
109
   * @return This will return the empty string if the caret is out of bounds.
110
   */
111
  default String getCaretWordText() {
112
    return getText( getCaretWord() );
113
  }
114
115
  /**
116
   * Requests undoing the last text-changing action.
117
   */
118
  void undo();
119
120
  /**
121
   * Requests redoing the last text-changing action that was undone.
122
   */
123
  void redo();
124
125
  /**
126
   * Requests cutting the selected text, or the current line if none selected.
127
   */
128
  void cut();
129
130
  /**
131
   * Requests copying the selected text, or no operation if none selected.
132
   */
133
  void copy();
134
135
  /**
136
   * Requests pasting from the clipboard into the editor. This will replace
137
   * text if selected, otherwise the clipboard contents are inserted at the
138
   * cursor.
139
   */
140
  void paste();
141
142
  /**
143
   * Requests selecting the entire document. This will replace the existing
144
   * selection, if any.
145
   */
146
  void selectAll();
147
148
  /**
149
   * Requests making the selected text, or word at caret, bold.
150
   */
151
  default void bold() {
152
  }
153
154
  /**
155
   * Requests making the selected text, or word at caret, italic.
156
   */
157
  default void italic() {
158
  }
159
160
  /**
161
   * Requests making the selected text, or word at caret, a superscript.
162
   */
163
  default void superscript() {
164
  }
165
166
  /**
167
   * Requests making the selected text, or word at caret, a subscript.
168
   */
169
  default void subscript() {
170
  }
171
172
  /**
173
   * Requests making the selected text, or word at caret, struck.
174
   */
175
  default void strikethrough() {
176
  }
177
178
  /**
179
   * Requests making the selected text, or word at caret, a blockquote block.
180
   */
181
  default void blockquote() {
182
  }
183
184
  /**
185
   * Requests making the selected text, or word at caret, inline code.
186
   */
187
  default void code() {
188
  }
189
190
  /**
191
   * Requests making the selected text, or word at caret, a fenced code block.
192
   */
193
  default void fencedCodeBlock() {
194
  }
195
196
  /**
197
   * Requests making the selected text, or word at caret, a heading.
198
   *
199
   * @param level The heading level to apply (typically 1 through 3).
200
   */
201
  default void heading( final int level ) {
202
  }
203
204
  /**
205
   * Requests making the selected text, or word at caret, an unordered list
206
   * block.
207
   */
208
  default void unorderedList() {
209
  }
210
211
  /**
212
   * Requests making the selected text, or word at caret, an ordered list block.
213
   */
214
  default void orderedList() {
215
  }
216
217
  /**
218
   * Requests making the selected text, or inserting at the caret, a
219
   * horizontal rule.
220
   */
221
  default void horizontalRule() {
222
  }
223
}
1224
A src/main/java/com/keenwrite/editors/TextResource.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors;
3
4
import com.keenwrite.io.MediaType;
5
import javafx.beans.property.ReadOnlyBooleanProperty;
6
import javafx.scene.Node;
7
import org.mozilla.universalchardet.UniversalDetector;
8
9
import java.io.File;
10
import java.nio.charset.Charset;
11
import java.nio.file.Path;
12
13
import static com.keenwrite.Constants.DEFAULT_CHARSET;
14
import static com.keenwrite.StatusBarNotifier.clue;
15
import static java.nio.charset.Charset.forName;
16
import static java.nio.file.Files.readAllBytes;
17
import static java.nio.file.Files.write;
18
import static java.util.Locale.ENGLISH;
19
20
/**
21
 * A text resource can be persisted and retrieved from its persisted location.
22
 */
23
public interface TextResource {
24
  /**
25
   * Sets the text string that to be changed through some graphical user
26
   * interface. For example, a YAML document must be parsed from the given
27
   * text string into a tree view with which the user may interact.
28
   *
29
   * @param text The new content for the resource.
30
   */
31
  void setText( String text );
32
33
  /**
34
   * Returns the text string that may have been modified by the user through
35
   * some graphical user interface.
36
   *
37
   * @return The text value, based on the value set from
38
   * {@link #setText(String)}, but possibly mutated.
39
   */
40
  String getText();
41
42
  /**
43
   * Return the character encoding for this file.
44
   *
45
   * @return A non-null character set, primarily detected from file contents.
46
   */
47
  Charset getEncoding();
48
49
  /**
50
   * Renames the current file to the given fully qualified file name.
51
   *
52
   * @param file The new file name.
53
   */
54
  void rename( final File file );
55
56
  /**
57
   * Returns the file name, without any directory components, for this instance.
58
   * Useful for showing as a tab title.
59
   *
60
   * @return The file name value returned from {@link #getFile()}.
61
   */
62
  default String getFilename() {
63
    final var filename = getFile().toPath().getFileName();
64
    return filename == null ? "" : filename.toString();
65
  }
66
67
  /**
68
   * Returns the fully qualified {@link File} to the editable text resource.
69
   * Useful for showing as a tab tooltip, saving the file, or reading it.
70
   *
71
   * @return A non-null {@link File} instance.
72
   */
73
  File getFile();
74
75
  /**
76
   * Returns the {@link MediaType} associated with the file being edited.
77
   *
78
   * @return The {@link MediaType} for the editor's file.
79
   */
80
  default MediaType getMediaType() {
81
    return MediaType.valueFrom( getFile() );
82
  }
83
84
  /**
85
   * Returns the fully qualified {@link Path} to the editable text resource.
86
   * This delegates to {@link #getFile()}.
87
   *
88
   * @return A non-null {@link Path} instance.
89
   */
90
  default Path getPath() {
91
    return getFile().toPath();
92
  }
93
94
  /**
95
   * Read the file contents and update the text accordingly. If the file
96
   * cannot be read then no changes will happen to the text. Fails silently.
97
   *
98
   * @param path The fully qualified {@link Path}, including a file name, to
99
   *             fully read into the editor.
100
   * @return The character encoding for the file at the given {@link Path}.
101
   */
102
  default Charset open( final Path path ) {
103
    final var file = path.toFile();
104
    Charset encoding = DEFAULT_CHARSET;
105
106
    try {
107
      if( file.exists() ) {
108
        if( file.canWrite() && file.canRead() ) {
109
          final var bytes = readAllBytes( path );
110
          encoding = detectEncoding( bytes );
111
112
          setText( asString( bytes, encoding ) );
113
        }
114
        else {
115
          clue( "TextResource.load.error.permissions", file.toString() );
116
        }
117
      }
118
      else {
119
        clue( "TextResource.load.error.unsaved", file.toString() );
120
      }
121
    } catch( final Exception ex ) {
122
      clue( ex );
123
    }
124
125
    return encoding;
126
  }
127
128
  /**
129
   * Read the file contents and update the text accordingly. If the file
130
   * cannot be read then no changes will happen to the text. This delegates
131
   * to {@link #open(Path)}.
132
   *
133
   * @param file The {@link File} to fully read into the editor.
134
   * @return The file's character encoding.
135
   */
136
  default Charset open( final File file ) {
137
    return open( file.toPath() );
138
  }
139
140
  /**
141
   * Save the file contents and clear the modified flag. If the file cannot
142
   * be saved, the exception is swallowed and this method returns {@code false}.
143
   *
144
   * @return {@code true} the file was saved; {@code false} if upon exception.
145
   */
146
  default boolean save() {
147
    try {
148
      write( getPath(), asBytes( getText() ) );
149
      clearModifiedProperty();
150
      return true;
151
    } catch( final Exception ex ) {
152
      clue( ex );
153
    }
154
155
    return false;
156
  }
157
158
  /**
159
   * Returns the node associated with this {@link TextResource}.
160
   *
161
   * @return The view component for the {@link TextResource}.
162
   */
163
  Node getNode();
164
165
  /**
166
   * Answers whether the resource has been modified.
167
   *
168
   * @return {@code true} the resource has changed; {@code false} means that
169
   * no changes to the resource have been made.
170
   */
171
  default boolean isModified() {
172
    return modifiedProperty().get();
173
  }
174
175
  /**
176
   * Returns a property that answers whether this text resource has been
177
   * changed from the original text that was opened.
178
   *
179
   * @return A property representing the modified state of this
180
   * {@link TextResource}.
181
   */
182
  ReadOnlyBooleanProperty modifiedProperty();
183
184
  /**
185
   * Lowers the modified flag such that listeners to the modified property
186
   * will be informed that the text that's being edited no longer differs
187
   * from what's persisted.
188
   */
189
  void clearModifiedProperty();
190
191
  private String asString( final byte[] text, final Charset encoding ) {
192
    return new String( text, encoding );
193
  }
194
195
  /**
196
   * Converts the given string to an array of bytes using the encoding that was
197
   * originally detected (if any) and associated with this file.
198
   *
199
   * @param text The text to convert into the original file encoding.
200
   * @return A series of bytes ready for writing to a file.
201
   */
202
  private byte[] asBytes( final String text ) {
203
    return text.getBytes( getEncoding() );
204
  }
205
206
  private Charset detectEncoding( final byte[] bytes ) {
207
    final var detector = new UniversalDetector( null );
208
    detector.handleData( bytes, 0, bytes.length );
209
    detector.dataEnd();
210
211
    final var charset = detector.getDetectedCharset();
212
213
    return charset == null
214
      ? DEFAULT_CHARSET
215
      : forName( charset.toUpperCase( ENGLISH ) );
216
  }
217
}
1218
A src/main/java/com/keenwrite/editors/definition/DefinitionEditor.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import com.keenwrite.Constants;
5
import com.keenwrite.editors.TextDefinition;
6
import com.keenwrite.sigils.Tokens;
7
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
8
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
9
import javafx.beans.property.BooleanProperty;
10
import javafx.beans.property.ReadOnlyBooleanProperty;
11
import javafx.beans.property.SimpleBooleanProperty;
12
import javafx.collections.ObservableList;
13
import javafx.event.ActionEvent;
14
import javafx.event.Event;
15
import javafx.event.EventHandler;
16
import javafx.scene.Node;
17
import javafx.scene.control.*;
18
import javafx.scene.input.KeyEvent;
19
import javafx.scene.layout.BorderPane;
20
import javafx.scene.layout.HBox;
21
22
import java.io.File;
23
import java.nio.charset.Charset;
24
import java.util.*;
25
import java.util.regex.Pattern;
26
27
import static com.keenwrite.Constants.DEFINITION_DEFAULT;
28
import static com.keenwrite.Messages.get;
29
import static com.keenwrite.StatusBarNotifier.clue;
30
import static java.lang.String.format;
31
import static java.util.regex.Pattern.compile;
32
import static java.util.regex.Pattern.quote;
33
import static javafx.geometry.Pos.CENTER;
34
import static javafx.geometry.Pos.TOP_CENTER;
35
import static javafx.scene.control.SelectionMode.MULTIPLE;
36
import static javafx.scene.control.TreeItem.childrenModificationEvent;
37
import static javafx.scene.control.TreeItem.valueChangedEvent;
38
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
39
40
/**
41
 * Provides the user interface that holds a {@link TreeView}, which
42
 * allows users to interact with key/value pairs loaded from the
43
 * document parser and adapted using a {@link TreeTransformer}.
44
 */
45
public final class DefinitionEditor extends BorderPane
46
  implements TextDefinition {
47
  private static final int GROUP_DELIMITED = 1;
48
49
  /**
50
   * Contains the root that is added to the view.
51
   */
52
  private final DefinitionTreeItem<String> mTreeRoot = createRootTreeItem();
53
54
  /**
55
   * Contains a view of the definitions.
56
   */
57
  private final TreeView<String> mTreeView = new TreeView<>( mTreeRoot );
58
59
  /**
60
   * Used to adapt the structured document into a {@link TreeView}.
61
   */
62
  private final TreeTransformer mTreeTransformer;
63
64
  /**
65
   * Handlers for key press events.
66
   */
67
  private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
68
    = new HashSet<>();
69
70
  /**
71
   * File being edited by this editor instance.
72
   */
73
  private File mFile;
74
75
  /**
76
   * Opened file's character encoding, or {@link Constants#DEFAULT_CHARSET} if
77
   * either no encoding could be determined or this is a new (empty) file.
78
   */
79
  private final Charset mEncoding;
80
81
  /**
82
   * Tracks whether the in-memory definitions have changed with respect to the
83
   * persisted definitions.
84
   */
85
  private final BooleanProperty mModified = new SimpleBooleanProperty();
86
87
  /**
88
   * This is provided for unit tests that are not backed by files.
89
   *
90
   * @param treeTransformer Responsible for transforming the definitions into
91
   *                        {@link TreeItem} instances.
92
   */
93
  public DefinitionEditor(
94
    final TreeTransformer treeTransformer ) {
95
    this( DEFINITION_DEFAULT, treeTransformer );
96
  }
97
98
  /**
99
   * Constructs a definition pane with a given tree view root.
100
   *
101
   * @param file The file to
102
   */
103
  public DefinitionEditor(
104
    final File file,
105
    final TreeTransformer treeTransformer ) {
106
    assert file != null;
107
    assert treeTransformer != null;
108
109
    mFile = file;
110
    mTreeTransformer = treeTransformer;
111
112
    mTreeView.setEditable( true );
113
    mTreeView.setCellFactory( new TreeCellFactory() );
114
    mTreeView.setContextMenu( createContextMenu() );
115
    mTreeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
116
    mTreeView.setShowRoot( false );
117
    getSelectionModel().setSelectionMode( MULTIPLE );
118
119
    final var buttonBar = new HBox();
120
    buttonBar.getChildren().addAll(
121
      createButton( "create", e -> createDefinition() ),
122
      createButton( "rename", e -> renameDefinition() ),
123
      createButton( "delete", e -> deleteDefinitions() )
124
    );
125
    buttonBar.setAlignment( CENTER );
126
    buttonBar.setSpacing( 10 );
127
128
    setTop( buttonBar );
129
    setCenter( mTreeView );
130
    setAlignment( buttonBar, TOP_CENTER );
131
    addTreeChangeHandler( event -> mModified.set( true ) );
132
    mEncoding = open( mFile );
133
  }
134
135
  @Override
136
  public void setText( final String document ) {
137
    final var foster = mTreeTransformer.transform( document );
138
    final var biological = getTreeRoot();
139
140
    for( final var child : foster.getChildren() ) {
141
      biological.getChildren().add( child );
142
    }
143
144
    getTreeView().refresh();
145
  }
146
147
  @Override
148
  public String getText() {
149
    final var result = new StringBuilder( 32768 );
150
151
    try {
152
      final var root = getTreeView().getRoot();
153
      final var problem = isTreeWellFormed();
154
155
      problem.ifPresentOrElse(
156
        ( node ) -> clue( "yaml.error.tree.form", node ),
157
        () -> result.append( mTreeTransformer.transform( root ) )
158
      );
159
    } catch( final Exception ex ) {
160
      // Catch errors while checking for a well-formed tree (e.g., stack smash).
161
      // Also catch any transformation exceptions (e.g., Json processing).
162
      clue( ex );
163
    }
164
165
    return result.toString();
166
  }
167
168
  @Override
169
  public File getFile() {
170
    return mFile;
171
  }
172
173
  @Override
174
  public void rename( final File file ) {
175
    mFile = file;
176
  }
177
178
  @Override
179
  public Charset getEncoding() {
180
    return mEncoding;
181
  }
182
183
  @Override
184
  public Node getNode() {
185
    return this;
186
  }
187
188
  @Override
189
  public ReadOnlyBooleanProperty modifiedProperty() {
190
    return mModified;
191
  }
192
193
  @Override
194
  public void clearModifiedProperty() {
195
    mModified.setValue( false );
196
  }
197
198
  private Button createButton(
199
    final String msgKey, final EventHandler<ActionEvent> eventHandler ) {
200
    final var keyPrefix = "App.action.definition." + msgKey;
201
    final var button = new Button( get( keyPrefix + ".text" ) );
202
    final var icon = get( keyPrefix + ".icon" );
203
    final var glyph = FontAwesomeIcon.valueOf( icon.toUpperCase() );
204
205
    button.setOnAction( eventHandler );
206
    button.setGraphic(
207
      FontAwesomeIconFactory.get().createIcon( glyph )
208
    );
209
    button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) );
210
211
    return button;
212
  }
213
214
  @Override
215
  public Map<String, String> toMap() {
216
    return new TreeItemMapper().toMap( getTreeView().getRoot() );
217
  }
218
219
  @Override
220
  public Map<String, String> interpolate(
221
    final Map<String, String> map, final Tokens tokens ) {
222
223
    // Non-greedy match of key names delimited by definition tokens.
224
    final var pattern = compile(
225
      format( "(%s.*?%s)",
226
              quote( tokens.getBegan() ),
227
              quote( tokens.getEnded() )
228
      )
229
    );
230
231
    map.replaceAll( ( k, v ) -> resolve( map, v, pattern ) );
232
    return map;
233
  }
234
235
  /**
236
   * Given a value with zero or more key references, this will resolve all
237
   * the values, recursively. If a key cannot be de-referenced, the value will
238
   * contain the key name.
239
   *
240
   * @param map     Map to search for keys when resolving key references.
241
   * @param value   Value containing zero or more key references.
242
   * @param pattern The regular expression pattern to match variable key names.
243
   * @return The given value with all embedded key references interpolated.
244
   */
245
  private String resolve(
246
    final Map<String, String> map, String value, final Pattern pattern ) {
247
    final var matcher = pattern.matcher( value );
248
249
    while( matcher.find() ) {
250
      final var keyName = matcher.group( GROUP_DELIMITED );
251
      final var mapValue = map.get( keyName );
252
      final var keyValue = mapValue == null
253
        ? keyName
254
        : resolve( map, mapValue, pattern );
255
256
      value = value.replace( keyName, keyValue );
257
    }
258
259
    return value;
260
  }
261
262
263
  /**
264
   * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
265
   * is modified. The modifications include: item value changes, item additions,
266
   * and item removals.
267
   * <p>
268
   * Safe to call multiple times; if a handler is already registered, the
269
   * old handler is used.
270
   * </p>
271
   *
272
   * @param handler The handler to call whenever any {@link TreeItem} changes.
273
   */
274
  public void addTreeChangeHandler(
275
    final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
276
    final var root = getTreeView().getRoot();
277
    root.addEventHandler( valueChangedEvent(), handler );
278
    root.addEventHandler( childrenModificationEvent(), handler );
279
  }
280
281
  public void addKeyEventHandler(
282
    final EventHandler<? super KeyEvent> handler ) {
283
    getKeyEventHandlers().add( handler );
284
  }
285
286
  /**
287
   * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
288
   * well-formed for export. A tree is considered well-formed if the following
289
   * conditions are met:
290
   *
291
   * <ul>
292
   *   <li>The root node contains at least one child node having a leaf.</li>
293
   *   <li>There are no leaf nodes with sibling leaf nodes.</li>
294
   * </ul>
295
   *
296
   * @return {@code null} if the document is well-formed, otherwise the
297
   * problematic child {@link TreeItem}.
298
   */
299
  public Optional<TreeItem<String>> isTreeWellFormed() {
300
    final var root = getTreeView().getRoot();
301
302
    for( final var child : root.getChildren() ) {
303
      final var problemChild = isWellFormed( child );
304
305
      if( child.isLeaf() || problemChild != null ) {
306
        return Optional.ofNullable( problemChild );
307
      }
308
    }
309
310
    return Optional.empty();
311
  }
312
313
  /**
314
   * Determines whether the document is well-formed by ensuring that
315
   * child branches do not contain multiple leaves.
316
   *
317
   * @param item The sub-tree to check for well-formedness.
318
   * @return {@code null} when the tree is well-formed, otherwise the
319
   * problematic {@link TreeItem}.
320
   */
321
  private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
322
    int childLeafs = 0;
323
    int childBranches = 0;
324
325
    for( final var child : item.getChildren() ) {
326
      if( child.isLeaf() ) {
327
        childLeafs++;
328
      }
329
      else {
330
        childBranches++;
331
      }
332
333
      final var problemChild = isWellFormed( child );
334
335
      if( problemChild != null ) {
336
        return problemChild;
337
      }
338
    }
339
340
    return ((childBranches > 0 && childLeafs == 0) ||
341
      (childBranches == 0 && childLeafs <= 1)) ? null : item;
342
  }
343
344
  @Override
345
  public DefinitionTreeItem<String> findLeafExact( final String text ) {
346
    return getTreeRoot().findLeafExact( text );
347
  }
348
349
  @Override
350
  public DefinitionTreeItem<String> findLeafContains( final String text ) {
351
    return getTreeRoot().findLeafContains( text );
352
  }
353
354
  @Override
355
  public DefinitionTreeItem<String> findLeafContainsNoCase(
356
    final String text ) {
357
    return getTreeRoot().findLeafContainsNoCase( text );
358
  }
359
360
  @Override
361
  public DefinitionTreeItem<String> findLeafStartsWith( final String text ) {
362
    return getTreeRoot().findLeafStartsWith( text );
363
  }
364
365
  public void select( final TreeItem<String> item ) {
366
    getSelectionModel().clearSelection();
367
    getSelectionModel().select( getTreeView().getRow( item ) );
368
  }
369
370
  /**
371
   * Collapses the tree, recursively.
372
   */
373
  public void collapse() {
374
    collapse( getTreeRoot().getChildren() );
375
  }
376
377
  /**
378
   * Collapses the tree, recursively.
379
   *
380
   * @param <T>   The type of tree item to expand (usually String).
381
   * @param nodes The nodes to collapse.
382
   */
383
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
384
    for( final var node : nodes ) {
385
      node.setExpanded( false );
386
      collapse( node.getChildren() );
387
    }
388
  }
389
390
  /**
391
   * @return {@code true} when the user is editing a {@link TreeItem}.
392
   */
393
  private boolean isEditingTreeItem() {
394
    return getTreeView().editingItemProperty().getValue() != null;
395
  }
396
397
  /**
398
   * Changes to edit mode for the selected item.
399
   */
400
  @Override
401
  public void renameDefinition() {
402
    getTreeView().edit( getSelectedItem() );
403
  }
404
405
  /**
406
   * Removes all selected items from the {@link TreeView}.
407
   */
408
  @Override
409
  public void deleteDefinitions() {
410
    for( final var item : getSelectedItems() ) {
411
      final var parent = item.getParent();
412
413
      if( parent != null ) {
414
        parent.getChildren().remove( item );
415
      }
416
    }
417
  }
418
419
  /**
420
   * Deletes the selected item.
421
   */
422
  private void deleteSelectedItem() {
423
    final var c = getSelectedItem();
424
    getSiblings( c ).remove( c );
425
  }
426
427
  /**
428
   * Adds a new item under the selected item (or root if nothing is selected).
429
   * There are a few conditions to consider: when adding to the root,
430
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
431
   * root must contain two items: a key and a value.
432
   */
433
  @Override
434
  public void createDefinition() {
435
    final var value = createDefinitionTreeItem();
436
    getSelectedItem().getChildren().add( value );
437
    expand( value );
438
    select( value );
439
  }
440
441
  private ContextMenu createContextMenu() {
442
    final var menu = new ContextMenu();
443
    final var items = menu.getItems();
444
445
    addMenuItem( items, "App.action.definition.create.text" )
446
      .setOnAction( e -> createDefinition() );
447
    addMenuItem( items, "App.action.definition.rename.text" )
448
      .setOnAction( e -> renameDefinition() );
449
    addMenuItem( items, "App.action.definition.delete.text" )
450
      .setOnAction( e -> deleteSelectedItem() );
451
452
    return menu;
453
  }
454
455
  /**
456
   * Executes hot-keys for edits to the definition tree.
457
   *
458
   * @param event Contains the key code of the key that was pressed.
459
   */
460
  private void keyEventFilter( final KeyEvent event ) {
461
    if( !isEditingTreeItem() ) {
462
      switch( event.getCode() ) {
463
        case ENTER -> {
464
          expand( getSelectedItem() );
465
          event.consume();
466
        }
467
468
        case DELETE -> deleteDefinitions();
469
        case INSERT -> createDefinition();
470
471
        case R -> {
472
          if( event.isControlDown() ) {
473
            renameDefinition();
474
          }
475
        }
476
      }
477
478
      for( final var handler : getKeyEventHandlers() ) {
479
        handler.handle( event );
480
      }
481
    }
482
  }
483
484
  /**
485
   * Adds a menu item to a list of menu items.
486
   *
487
   * @param items    The list of menu items to append to.
488
   * @param labelKey The resource bundle key name for the menu item's label.
489
   * @return The menu item added to the list of menu items.
490
   */
491
  private MenuItem addMenuItem(
492
    final List<MenuItem> items, final String labelKey ) {
493
    final MenuItem menuItem = createMenuItem( labelKey );
494
    items.add( menuItem );
495
    return menuItem;
496
  }
497
498
  private MenuItem createMenuItem( final String labelKey ) {
499
    return new MenuItem( get( labelKey ) );
500
  }
501
502
  /**
503
   * Creates a new {@link TreeItem} that is intended to be the root-level item
504
   * added to the {@link TreeView}. This allows the root item to be
505
   * distinguished from the other items so that reference keys do not include
506
   * "Definition" as part of their name.
507
   *
508
   * @return A new {@link TreeItem}, never {@code null}.
509
   */
510
  private RootTreeItem<String> createRootTreeItem() {
511
    return new RootTreeItem<>( get( "Pane.definition.node.root.title" ) );
512
  }
513
514
  private DefinitionTreeItem<String> createDefinitionTreeItem() {
515
    return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) );
516
  }
517
518
  @Override
519
  public void requestFocus() {
520
    super.requestFocus();
521
    getTreeView().requestFocus();
522
  }
523
524
  /**
525
   * Expands the node to the root, recursively.
526
   *
527
   * @param <T>  The type of tree item to expand (usually String).
528
   * @param node The node to expand.
529
   */
530
  @Override
531
  public <T> void expand( final TreeItem<T> node ) {
532
    if( node != null ) {
533
      expand( node.getParent() );
534
      node.setExpanded( !node.isLeaf() );
535
    }
536
  }
537
538
  /**
539
   * Answers whether there are any definitions in the tree.
540
   *
541
   * @return {@code true} when there are no definitions; {@code false} when
542
   * there's at least one definition.
543
   */
544
  @Override
545
  public boolean isEmpty() {
546
    return getTreeRoot().isEmpty();
547
  }
548
549
  /**
550
   * Returns the actively selected item in the tree.
551
   *
552
   * @return The selected item, or the tree root item if no item is selected.
553
   */
554
  public TreeItem<String> getSelectedItem() {
555
    final var item = getSelectionModel().getSelectedItem();
556
    return item == null ? getTreeRoot() : item;
557
  }
558
559
  /**
560
   * Returns the {@link TreeView} that contains the definition hierarchy.
561
   *
562
   * @return A non-null instance.
563
   */
564
  private TreeView<String> getTreeView() {
565
    return mTreeView;
566
  }
567
568
  /**
569
   * Returns the root of the tree.
570
   *
571
   * @return The first node added to the definition tree.
572
   */
573
  private DefinitionTreeItem<String> getTreeRoot() {
574
    return mTreeRoot;
575
  }
576
577
  private ObservableList<TreeItem<String>> getSiblings(
578
    final TreeItem<String> item ) {
579
    final var root = getTreeView().getRoot();
580
    final var parent = (item == null || item == root) ? root : item.getParent();
581
582
    return parent.getChildren();
583
  }
584
585
  private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
586
    return getTreeView().getSelectionModel();
587
  }
588
589
  /**
590
   * Returns a copy of all the selected items.
591
   *
592
   * @return A list, possibly empty, containing all selected items in the
593
   * {@link TreeView}.
594
   */
595
  private List<TreeItem<String>> getSelectedItems() {
596
    return new ArrayList<>( getSelectionModel().getSelectedItems() );
597
  }
598
599
  private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() {
600
    return mKeyEventHandlers;
601
  }
602
}
1603
A src/main/java/com/keenwrite/editors/definition/DefinitionTabSceneFactory.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import com.panemu.tiwulfx.control.dock.DetachableTabPane;
5
import javafx.beans.property.ReadOnlyObjectProperty;
6
import javafx.scene.Scene;
7
import javafx.scene.control.SingleSelectionModel;
8
import javafx.scene.control.Tab;
9
import javafx.scene.layout.VBox;
10
11
import java.util.function.Consumer;
12
13
import static javafx.scene.layout.Priority.ALWAYS;
14
15
/**
16
 * Responsible for delegating tab selection events to a consumer. This is
17
 * required so that when a tab is detached from the main view into its own
18
 * window (scene), any tab changes in that scene can have an effect on the
19
 * main view.
20
 *
21
 * @author Amrullah Syadzili
22
 * @author White Magic Software, Ltd.
23
 */
24
public class DefinitionTabSceneFactory {
25
26
  private final Consumer<Tab> mTabSelectionConsumer;
27
28
  public DefinitionTabSceneFactory( final Consumer<Tab> tabSelectionConsumer ) {
29
    mTabSelectionConsumer = tabSelectionConsumer;
30
  }
31
32
  public Scene create( final DetachableTabPane tabPane ) {
33
    final var container = new TabContainer( tabPane );
34
    final var scene = new Scene( container, 300, 900 );
35
36
    scene.windowProperty().addListener( ( c, o, n ) -> {
37
      if( n != null ) {
38
        n.focusedProperty().addListener( ( __ ) -> {
39
          final var tab = container.getSelectedTab();
40
41
          if( tab != null ) {
42
            mTabSelectionConsumer.accept( tab );
43
          }
44
        } );
45
      }
46
    } );
47
48
    return scene;
49
  }
50
51
  private final class TabContainer extends VBox {
52
    private final DetachableTabPane mTabPane;
53
54
    public TabContainer( final DetachableTabPane tabPane ) {
55
      mTabPane = tabPane;
56
      setVgrow( tabPane, ALWAYS );
57
      getChildren().add( tabPane );
58
59
      selectedItemProperty().addListener(
60
          ( c, o, n ) -> {
61
            if( n != null ) {
62
              mTabSelectionConsumer.accept( n );
63
            }
64
          }
65
      );
66
    }
67
68
    private SingleSelectionModel<Tab> getSelectionModel() {
69
      return mTabPane.getSelectionModel();
70
    }
71
72
    private ReadOnlyObjectProperty<Tab> selectedItemProperty() {
73
      return getSelectionModel().selectedItemProperty();
74
    }
75
76
    private Tab getSelectedTab() {
77
      return getSelectionModel().getSelectedItem();
78
    }
79
  }
80
}
181
A src/main/java/com/keenwrite/editors/definition/DefinitionTreeItem.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import javafx.scene.control.TreeItem;
5
6
import java.util.Stack;
7
import java.util.function.BiFunction;
8
9
import static java.text.Normalizer.Form.NFD;
10
import static java.text.Normalizer.normalize;
11
12
/**
13
 * Provides behaviour afforded to definition keys and corresponding value.
14
 *
15
 * @param <T> The type of {@link TreeItem} (usually string).
16
 */
17
public class DefinitionTreeItem<T> extends TreeItem<T> {
18
19
  /**
20
   * Constructs a new item with a default value.
21
   *
22
   * @param value Passed up to superclass.
23
   */
24
  public DefinitionTreeItem( final T value ) {
25
    super( value );
26
  }
27
28
  /**
29
   * Finds a leaf starting at the current node with text that matches the given
30
   * value. Search is performed case-sensitively.
31
   *
32
   * @param text The text to match against each leaf in the tree.
33
   * @return The leaf that has a value exactly matching the given text.
34
   */
35
  public DefinitionTreeItem<T> findLeafExact( final String text ) {
36
    return findLeaf( text, DefinitionTreeItem::valueEquals );
37
  }
38
39
  /**
40
   * Finds a leaf starting at the current node with text that matches the given
41
   * value. Search is performed case-sensitively.
42
   *
43
   * @param text The text to match against each leaf in the tree.
44
   * @return The leaf that has a value that contains the given text.
45
   */
46
  public DefinitionTreeItem<T> findLeafContains( final String text ) {
47
    return findLeaf( text, DefinitionTreeItem::valueContains );
48
  }
49
50
  /**
51
   * Finds a leaf starting at the current node with text that matches the given
52
   * value. Search is performed case-insensitively.
53
   *
54
   * @param text The text to match against each leaf in the tree.
55
   * @return The leaf that has a value that contains the given text.
56
   */
57
  public DefinitionTreeItem<T> findLeafContainsNoCase( final String text ) {
58
    return findLeaf( text, DefinitionTreeItem::valueContainsNoCase );
59
  }
60
61
  /**
62
   * Finds a leaf starting at the current node with text that matches the given
63
   * value. Search is performed case-sensitively.
64
   *
65
   * @param text The text to match against each leaf in the tree.
66
   * @return The leaf that has a value that starts with the given text.
67
   */
68
  public DefinitionTreeItem<T> findLeafStartsWith( final String text ) {
69
    return findLeaf( text, DefinitionTreeItem::valueStartsWith );
70
  }
71
72
  /**
73
   * Finds a leaf starting at the current node with text that matches the given
74
   * value.
75
   *
76
   * @param text     The text to match against each leaf in the tree.
77
   * @param findMode What algorithm is used to match the given text.
78
   * @return The leaf that has a value starting with the given text, or {@code
79
   * null} if there was no match found.
80
   */
81
  public DefinitionTreeItem<T> findLeaf(
82
    final String text,
83
    final BiFunction<DefinitionTreeItem<T>, String, Boolean> findMode ) {
84
    final var stack = new Stack<DefinitionTreeItem<T>>();
85
    stack.push( this );
86
87
    // Don't hunt for blank (empty) keys.
88
    boolean found = text.isBlank();
89
90
    while( !found && !stack.isEmpty() ) {
91
      final var node = stack.pop();
92
93
      for( final var child : node.getChildren() ) {
94
        final var result = (DefinitionTreeItem<T>) child;
95
96
        if( result.isLeaf() ) {
97
          if( found = findMode.apply( result, text ) ) {
98
            return result;
99
          }
100
        }
101
        else {
102
          stack.push( result );
103
        }
104
      }
105
    }
106
107
    return null;
108
  }
109
110
  /**
111
   * Returns the value of the string without diacritic marks.
112
   *
113
   * @return A non-null, possibly empty string.
114
   */
115
  private String getDiacriticlessValue() {
116
    return normalize( getValue().toString(), NFD )
117
      .replaceAll( "\\p{M}", "" );
118
  }
119
120
  /**
121
   * Returns true if this node is a leaf and its value equals the given text.
122
   *
123
   * @param s The text to compare against the node value.
124
   * @return true Node is a leaf and its value equals the given value.
125
   */
126
  private boolean valueEquals( final String s ) {
127
    return isLeaf() && getValue().equals( s );
128
  }
129
130
  /**
131
   * Returns true if this node is a leaf and its value contains the given text.
132
   *
133
   * @param s The text to compare against the node value.
134
   * @return true Node is a leaf and its value contains the given value.
135
   */
136
  private boolean valueContains( final String s ) {
137
    return isLeaf() && getDiacriticlessValue().contains( s );
138
  }
139
140
  /**
141
   * Returns true if this node is a leaf and its value contains the given text.
142
   *
143
   * @param s The text to compare against the node value.
144
   * @return true Node is a leaf and its value contains the given value.
145
   */
146
  private boolean valueContainsNoCase( final String s ) {
147
    return isLeaf() &&
148
      getDiacriticlessValue().toLowerCase().contains( s.toLowerCase() );
149
  }
150
151
  /**
152
   * Returns true if this node is a leaf and its value starts with the given
153
   * text.
154
   *
155
   * @param s The text to compare against the node value.
156
   * @return true Node is a leaf and its value starts with the given value.
157
   */
158
  private boolean valueStartsWith( final String s ) {
159
    return isLeaf() && getDiacriticlessValue().startsWith( s );
160
  }
161
162
  /**
163
   * Returns the path for this node, with nodes made distinct using the
164
   * separator character. This uses two loops: one for pushing nodes onto a
165
   * stack and one for popping them off to create the path in desired order.
166
   *
167
   * @return A non-null string, possibly empty.
168
   */
169
  public String toPath() {
170
    return new TreeItemMapper().toPath( getParent() );
171
  }
172
173
  /**
174
   * Answers whether there are any definitions in this tree.
175
   *
176
   * @return {@code true} when there are no definitions in the tree; {@code
177
   * false} when there is at least one definition present.
178
   */
179
  public boolean isEmpty() {
180
    return getChildren().isEmpty();
181
  }
182
}
1183
A src/main/java/com/keenwrite/editors/definition/FocusAwareTextFieldTreeCell.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import javafx.scene.Node;
5
import javafx.scene.control.TextField;
6
import javafx.scene.control.cell.TextFieldTreeCell;
7
import javafx.util.StringConverter;
8
9
/**
10
 * Responsible for fixing a focus lost bug in the JavaFX implementation.
11
 * See https://bugs.openjdk.java.net/browse/JDK-8089514 for details.
12
 * This implementation borrows from the official documentation on creating
13
 * tree views: https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm
14
 */
15
public class FocusAwareTextFieldTreeCell extends TextFieldTreeCell<String> {
16
  private TextField mTextField;
17
18
  public FocusAwareTextFieldTreeCell(
19
      final StringConverter<String> converter ) {
20
    super( converter );
21
  }
22
23
  @Override
24
  public void startEdit() {
25
    super.startEdit();
26
    var textField = mTextField;
27
28
    if( textField == null ) {
29
      textField = createTextField();
30
    }
31
    else {
32
      textField.setText( getItem() );
33
    }
34
35
    setText( null );
36
    setGraphic( textField );
37
    textField.selectAll();
38
    textField.requestFocus();
39
40
    // When the focus is lost, commit the edit then close the input field.
41
    // This fixes the unexpected behaviour when user clicks away.
42
    textField.focusedProperty().addListener( ( l, o, n ) -> {
43
      if( !n ) {
44
        commitEdit( mTextField.getText() );
45
      }
46
    } );
47
48
    mTextField = textField;
49
  }
50
51
  @Override
52
  public void cancelEdit() {
53
    super.cancelEdit();
54
    setText( getItem() );
55
    setGraphic( getTreeItem().getGraphic() );
56
  }
57
58
  @Override
59
  public void updateItem( String item, boolean empty ) {
60
    super.updateItem( item, empty );
61
62
    String text = null;
63
    Node graphic = null;
64
65
    if( !empty ) {
66
      if( isEditing() ) {
67
        final var textField = mTextField;
68
69
        if( textField != null ) {
70
          textField.setText( getString() );
71
        }
72
73
        graphic = textField;
74
      }
75
      else {
76
        text = getString();
77
        graphic = getTreeItem().getGraphic();
78
      }
79
    }
80
81
    setText( text );
82
    setGraphic( graphic );
83
  }
84
85
  private TextField createTextField() {
86
    final var textField = new TextField( getString() );
87
88
    textField.setOnKeyReleased( t -> {
89
      switch( t.getCode() ) {
90
        case ENTER -> commitEdit( textField.getText() );
91
        case ESCAPE -> cancelEdit();
92
      }
93
    } );
94
95
    return textField;
96
  }
97
98
  private String getString() {
99
    return getConverter().toString( getItem() );
100
  }
101
}
1102
A src/main/java/com/keenwrite/editors/definition/RootTreeItem.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import javafx.scene.control.TreeItem;
5
import javafx.scene.control.TreeView;
6
7
/**
8
 * Marker interface for top-most {@link TreeItem}. This class allows the
9
 * {@link TreeItemMapper} to ignore the topmost definition. Such contortions
10
 * are necessary because {@link TreeView} requires a root item that isn't part
11
 * of the user's definition file.
12
 * <p>
13
 * Another approach would be to associate object pairs per {@link TreeItem},
14
 * but that would be a waste of memory since the only "exception" case is
15
 * the root {@link TreeItem}.
16
 * </p>
17
 *
18
 * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}.
19
 */
20
public final class RootTreeItem<T> extends DefinitionTreeItem<T> {
21
  /**
22
   * Default constructor, calls the superclass, no other behaviour.
23
   *
24
   * @param value The {@link TreeItem} node name to construct the superclass.
25
   * @see TreeItemMapper#toMap(TreeItem) for details on how this
26
   * class is used.
27
   */
28
  public RootTreeItem( final T value ) {
29
    super( value );
30
  }
31
}
132
A src/main/java/com/keenwrite/editors/definition/TreeCellFactory.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import javafx.collections.ObservableList;
5
import javafx.scene.control.TreeCell;
6
import javafx.scene.control.TreeItem;
7
import javafx.scene.control.TreeView;
8
import javafx.scene.input.ClipboardContent;
9
import javafx.scene.input.DataFormat;
10
import javafx.scene.input.DragEvent;
11
import javafx.scene.input.MouseEvent;
12
import javafx.util.Callback;
13
import javafx.util.StringConverter;
14
15
import java.util.Objects;
16
17
import static com.keenwrite.io.MediaType.APP_JAVA_OBJECT;
18
import static javafx.scene.input.TransferMode.MOVE;
19
20
/**
21
 * Responsible for producing {@link TreeCell} instances that can be edited
22
 * and respond to drag and drop functionality.
23
 */
24
public class TreeCellFactory
25
    implements Callback<TreeView<String>, TreeCell<String>> {
26
  private static final String STYLE_CLASS_DROP_TARGET = "drop-target";
27
  private static final DataFormat JAVA_FORMAT =
28
      new DataFormat( APP_JAVA_OBJECT.toString() );
29
30
  private TreeItem<String> mDraggedTreeItem;
31
  private TreeCell<String> mTargetCell;
32
33
  /**
34
   * Constructs a new {@link TreeCell} manufacturing facility called when
35
   * a new {@link TreeItem} is added to one of the editor's {@link TreeView}s.
36
   */
37
  public TreeCellFactory() {
38
  }
39
40
  @Override
41
  public TreeCell<String> call( final TreeView<String> treeView ) {
42
    final var cell = createTreeCell();
43
44
    cell.setOnDragDetected( event -> dragDetected( event, cell ) );
45
    cell.setOnDragOver( event -> dragOver( event, cell ) );
46
    cell.setOnDragDropped( event -> dragDropped( event, cell, treeView ) );
47
    cell.setOnDragDone( event -> dragClear() );
48
49
    return cell;
50
  }
51
52
  private TreeCell<String> createTreeCell() {
53
    return new FocusAwareTextFieldTreeCell( createStringConverter() ) {
54
      @Override
55
      public void commitEdit( final String newValue ) {
56
        super.commitEdit( newValue );
57
        //mEditor.select( getTreeItem() );
58
        requestFocus();
59
      }
60
    };
61
  }
62
63
  private StringConverter<String> createStringConverter() {
64
    return new StringConverter<>() {
65
      @Override
66
      public String toString( final String object ) {
67
        return sanitize( object );
68
      }
69
70
      @Override
71
      public String fromString( final String string ) {
72
        return sanitize( string );
73
      }
74
75
      private String sanitize( final String string ) {
76
        return string == null ? "" : string;
77
      }
78
    };
79
  }
80
81
  /**
82
   * Drag start.
83
   *
84
   * @param event    The drag start {@link MouseEvent}.
85
   * @param treeCell The cell being dragged.
86
   */
87
  private void dragDetected(
88
      final MouseEvent event, final TreeCell<String> treeCell ) {
89
    final var sourceItem = treeCell.getTreeItem();
90
91
    // Prevent dragging the root item.
92
    if( sourceItem != null && sourceItem.getParent() != null ) {
93
      final var dragboard = treeCell.startDragAndDrop( MOVE );
94
      final var clipboard = new ClipboardContent();
95
      clipboard.put( JAVA_FORMAT, sourceItem.getValue() );
96
      dragboard.setContent( clipboard );
97
      dragboard.setDragView( treeCell.snapshot( null, null ) );
98
      event.consume();
99
100
      mDraggedTreeItem = sourceItem;
101
    }
102
  }
103
104
  /**
105
   * Drag over another {@link TreeCell} instance.
106
   *
107
   * @param event    The drag over {@link DragEvent}.
108
   * @param treeCell The cell dragged over.
109
   * @throws IllegalStateException Drag transfer "move" mode denied.
110
   */
111
  private void dragOver(
112
      final DragEvent event, final TreeCell<String> treeCell ) {
113
    if( event.getDragboard().hasContent( JAVA_FORMAT ) ) {
114
      final var thisItem = treeCell.getTreeItem();
115
116
      if( mDraggedTreeItem == null ||
117
          thisItem == null ||
118
          thisItem == mDraggedTreeItem ) {
119
        return;
120
      }
121
122
      // Ignore dragging over the root item.
123
      if( mDraggedTreeItem.getParent() == null ) {
124
        dragClear();
125
        return;
126
      }
127
128
      event.acceptTransferModes( MOVE );
129
130
      if( !Objects.equals( mTargetCell, treeCell ) ) {
131
        dragClear();
132
        mTargetCell = treeCell;
133
        mTargetCell.getStyleClass().add( STYLE_CLASS_DROP_TARGET );
134
      }
135
    }
136
  }
137
138
  /**
139
   * Dragged item is dropped
140
   *
141
   * @param event    The drag dropped {@link DragEvent}.
142
   * @param treeCell The cell dropped onto.
143
   */
144
  private void dragDropped( final DragEvent event,
145
                            final TreeCell<String> treeCell,
146
                            final TreeView<String> treeView ) {
147
    if( !event.getDragboard().hasContent( JAVA_FORMAT ) ) {
148
      return;
149
    }
150
151
    final var sourceItem = mDraggedTreeItem;
152
    final var sourceItemParent = mDraggedTreeItem.getParent();
153
    final var targetItem = treeCell.getTreeItem();
154
    final var targetItemParent = targetItem.getParent();
155
156
    sourceItemParent.getChildren().remove( sourceItem );
157
158
    final ObservableList<TreeItem<String>> children;
159
    final int index;
160
161
    // Dropping onto a parent node makes the source item the first child.
162
    if( Objects.equals( sourceItemParent, targetItem ) ) {
163
      children = targetItem.getChildren();
164
      index = 0;
165
    }
166
    else if( targetItemParent != null) {
167
      children = targetItemParent.getChildren();
168
      index = children.indexOf( targetItem ) + 1;
169
    }
170
    else {
171
      children = sourceItemParent.getChildren();
172
      index = 0;
173
    }
174
175
    children.add( index, sourceItem );
176
177
    treeView.getSelectionModel().clearSelection();
178
    treeView.getSelectionModel().select( sourceItem );
179
180
    // TODO: Notify a listener of the old and new tree item position.
181
182
    event.setDropCompleted( true );
183
  }
184
185
  private void dragClear() {
186
    final var targetCell = mTargetCell;
187
188
    if( targetCell != null ) {
189
      targetCell.getStyleClass().remove( STYLE_CLASS_DROP_TARGET );
190
    }
191
  }
192
}
1193
A src/main/java/com/keenwrite/editors/definition/TreeItemMapper.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import com.fasterxml.jackson.databind.JsonNode;
5
import com.keenwrite.preview.HtmlPreview;
6
import javafx.scene.control.TreeItem;
7
import javafx.scene.control.TreeView;
8
9
import java.util.HashMap;
10
import java.util.Iterator;
11
import java.util.Map;
12
import java.util.Stack;
13
14
import static com.keenwrite.Constants.MAP_SIZE_DEFAULT;
15
16
/**
17
 * Given a {@link TreeItem}, this will generate a flat map with all the
18
 * values in the tree recursively interpolated. The application integrates
19
 * definition files as follows:
20
 * <ol>
21
 *   <li>Load YAML file into {@link JsonNode} hierarchy.</li>
22
 *   <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li>
23
 *   <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li>
24
 *   <li>Substitute flat map variables into document as required.</li>
25
 * </ol>
26
 *
27
 * <p>
28
 * This class is responsible for producing the interpolated flat map. This
29
 * allows dynamic edits of the {@link TreeView} to be displayed in the
30
 * {@link HtmlPreview} without having to reload the definition file.
31
 * Reloading the definition file would work, but has a number of drawbacks.
32
 * </p>
33
 */
34
public class TreeItemMapper {
35
  /**
36
   * Separates definition keys (e.g., the dots in {@code $root.node.var$}).
37
   */
38
  public static final String SEPARATOR = ".";
39
40
  /**
41
   * Default buffer length for keys ({@link StringBuilder} has 16 character
42
   * buffer) that should be large enough for most keys to avoid reallocating
43
   * memory to increase the {@link StringBuilder}'s buffer.
44
   */
45
  public static final int DEFAULT_KEY_LENGTH = 64;
46
47
  /**
48
   * In-order traversal of a {@link TreeItem} hierarchy, exposing each item
49
   * as a consecutive list.
50
   */
51
  private static final class TreeIterator
52
    implements Iterator<TreeItem<String>> {
53
    private final Stack<TreeItem<String>> mStack = new Stack<>();
54
55
    public TreeIterator( final TreeItem<String> root ) {
56
      if( root != null ) {
57
        mStack.push( root );
58
      }
59
    }
60
61
    @Override
62
    public boolean hasNext() {
63
      return !mStack.isEmpty();
64
    }
65
66
    @Override
67
    public TreeItem<String> next() {
68
      final TreeItem<String> next = mStack.pop();
69
      next.getChildren().forEach( mStack::push );
70
71
      return next;
72
    }
73
  }
74
75
  public TreeItemMapper() {
76
  }
77
78
  /**
79
   * Iterate over a given root node (at any level of the tree) and process each
80
   * leaf node into a flat map. Values must be interpolated separately.
81
   */
82
  public Map<String, String> toMap( final TreeItem<String> root ) {
83
    final var map = new HashMap<String, String>( MAP_SIZE_DEFAULT );
84
    final var iterator = new TreeIterator( root );
85
86
    iterator.forEachRemaining( item -> {
87
      if( item.isLeaf() ) {
88
        map.put( toPath( item.getParent() ), item.getValue() );
89
      }
90
    } );
91
92
    return map;
93
  }
94
95
  /**
96
   * For a given node, this will ascend the tree to generate a key name
97
   * that is associated with the leaf node's value.
98
   *
99
   * @param node Ascendants represent the key to this node's value.
100
   * @param <T>  Data type that the {@link TreeItem} contains.
101
   * @return The string representation of the node's unique key.
102
   */
103
  public <T> String toPath( TreeItem<T> node ) {
104
    assert node != null;
105
106
    final var key = new StringBuilder( DEFAULT_KEY_LENGTH );
107
    final var stack = new Stack<TreeItem<T>>();
108
109
    while( node != null && !(node instanceof RootTreeItem) ) {
110
      stack.push( node );
111
      node = node.getParent();
112
    }
113
114
    // Gets set at end of first iteration (to avoid an if condition).
115
    var separator = "";
116
117
    while( !stack.empty() ) {
118
      final T subkey = stack.pop().getValue();
119
      key.append( separator );
120
      key.append( subkey );
121
      separator = SEPARATOR;
122
    }
123
124
    return key.toString();
125
  }
126
}
1127
A src/main/java/com/keenwrite/editors/definition/TreeTransformer.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import javafx.scene.control.TreeItem;
5
6
import java.util.function.Consumer;
7
import java.util.function.Function;
8
9
/**
10
 * Responsible for converting an object hierarchy into a {@link TreeItem}
11
 * hierarchy.
12
 */
13
public interface TreeTransformer {
14
  /**
15
   * Adapts the document produced by the given parser into a {@link TreeItem}
16
   * object that can be presented to the user within a GUI. The root of the
17
   * tree must be merged by the view layer.
18
   *
19
   * @param document The document to transform into a viewable hierarchy.
20
   */
21
  TreeItem<String> transform( String document );
22
23
  /**
24
   * Exports the given root node to the given path.
25
   *
26
   * @param root The root node to export.
27
   */
28
  String transform( TreeItem<String> root );
29
}
130
A src/main/java/com/keenwrite/editors/definition/package-info.java
1
/* Copyright 2020 White Magic Software, Ltd.
2
 *
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
/**
29
 * This package contains classes that pertain to hierarchical, structured
30
 * data formats, which can be used as interpolated variables.
31
 */
32
package com.keenwrite.editors.definition;
133
A src/main/java/com/keenwrite/editors/definition/yaml/YamlParser.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition.yaml;
3
4
import com.fasterxml.jackson.databind.JsonNode;
5
import com.fasterxml.jackson.databind.ObjectMapper;
6
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
7
8
import java.util.function.Function;
9
10
/**
11
 * Responsible for reading a YAML document into an object hierarchy.
12
 */
13
class YamlParser implements Function<String, JsonNode> {
14
15
  /**
16
   * Creates a new instance that can parse the contents of a YAML
17
   * document.
18
   */
19
  YamlParser() {
20
  }
21
22
  @Override
23
  public JsonNode apply( final String yaml ) {
24
    try {
25
      return new ObjectMapper( new YAMLFactory() ).readTree( yaml );
26
    } catch( final Exception ex ) {
27
      // Ensure that a document root node exists.
28
      return new ObjectMapper().createObjectNode();
29
    }
30
  }
31
}
132
A src/main/java/com/keenwrite/editors/definition/yaml/YamlTreeTransformer.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition.yaml;
3
4
import com.fasterxml.jackson.databind.JsonNode;
5
import com.fasterxml.jackson.databind.node.ObjectNode;
6
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
7
import com.keenwrite.editors.definition.DefinitionTreeItem;
8
import com.keenwrite.editors.definition.TreeTransformer;
9
import javafx.scene.control.TreeItem;
10
import javafx.scene.control.TreeView;
11
12
import java.util.Map.Entry;
13
14
/**
15
 * Transforms a JsonNode hierarchy into a tree that can be displayed in a user
16
 * interface and vice-versa.
17
 */
18
public final class YamlTreeTransformer implements TreeTransformer {
19
20
  /**
21
   * Constructs a new instance that will use the given path to read the object
22
   * hierarchy from a data source.
23
   */
24
  public YamlTreeTransformer() {
25
  }
26
27
  @Override
28
  public String transform( final TreeItem<String> treeItem ) {
29
    try {
30
      final YAMLMapper mapper = new YAMLMapper();
31
      final ObjectNode root = mapper.createObjectNode();
32
33
      // Iterate over the root item's children. The root item is used by the
34
      // application to ensure definitions can always be added to a tree, as
35
      // such it is not meant to be exported, only its children.
36
      for( final TreeItem<String> child : treeItem.getChildren() ) {
37
        transform( child, root );
38
      }
39
40
      return mapper.writeValueAsString( root );
41
    } catch( final Exception ex ) {
42
      throw new RuntimeException( ex );
43
    }
44
  }
45
46
  /**
47
   * Recursive method to generate an object hierarchy that represents the
48
   * given {@link TreeItem} hierarchy.
49
   *
50
   * @param item The {@link TreeItem} to reproduce as an object hierarchy.
51
   * @param node The {@link ObjectNode} to update to reflect the
52
   *             {@link TreeItem} hierarchy.
53
   */
54
  private void transform( final TreeItem<String> item, ObjectNode node ) {
55
    final var children = item.getChildren();
56
57
    // If the current item has more than one non-leaf child, it's an
58
    // object node and must become a new nested object.
59
    if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) {
60
      node = node.putObject( item.getValue() );
61
    }
62
63
    for( final var child : children ) {
64
      if( child.isLeaf() ) {
65
        node.put( item.getValue(), child.getValue() );
66
      }
67
      else {
68
        transform( child, node );
69
      }
70
    }
71
  }
72
73
  /**
74
   * Converts a YAML document to a {@link TreeItem} based on the document
75
   * keys.
76
   *
77
   * @param document The YAML document to convert to a hierarchy of
78
   *                 {@link TreeItem} instances.
79
   * @throws StackOverflowError If infinite recursion is encountered.
80
   */
81
  @Override
82
  public TreeItem<String> transform( final String document ) {
83
    final var parser = new YamlParser();
84
    final var jsonNode = parser.apply( document );
85
    final var rootItem = createTreeItem( "root" );
86
87
    transform( jsonNode, rootItem );
88
89
    return rootItem;
90
  }
91
92
  /**
93
   * Iterate over a given root node (at any level of the tree) and adapt each
94
   * leaf node.
95
   *
96
   * @param node A JSON node (YAML node) to adapt.
97
   * @param item The tree item to use as the root when processing the node.
98
   * @throws StackOverflowError If infinite recursion is encountered.
99
   */
100
  private void transform( final JsonNode node, final TreeItem<String> item ) {
101
    node.fields().forEachRemaining( leaf -> transform( leaf, item ) );
102
  }
103
104
  /**
105
   * Recursively adapt each rootNode to a corresponding rootItem.
106
   *
107
   * @param node The node to adapt.
108
   * @param item The item to adapt using the node's key.
109
   * @throws StackOverflowError If infinite recursion is encountered.
110
   */
111
  private void transform(
112
      final Entry<String, JsonNode> node, final TreeItem<String> item ) {
113
    final var leafNode = node.getValue();
114
    final var key = node.getKey();
115
    final var leaf = createTreeItem( key );
116
117
    if( leafNode.isValueNode() ) {
118
      leaf.getChildren().add( createTreeItem( node.getValue().asText() ) );
119
    }
120
121
    item.getChildren().add( leaf );
122
123
    if( leafNode.isObject() ) {
124
      transform( leafNode, leaf );
125
    }
126
  }
127
128
  /**
129
   * Creates a new {@link TreeItem} that can be added to the {@link TreeView}.
130
   *
131
   * @param value The node's value.
132
   * @return A new {@link TreeItem}, never {@code null}.
133
   */
134
  private TreeItem<String> createTreeItem( final String value ) {
135
    return new DefinitionTreeItem<>( value );
136
  }
137
}
1138
A src/main/java/com/keenwrite/editors/definition/yaml/package-info.java
1
/* Copyright 2020 White Magic Software, Ltd.
2
 *
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
/**
29
 * This package contains classes that can parse YAML documents into a GUI
30
 * representation.
31
 */
32
package com.keenwrite.editors.definition.yaml;
133
A src/main/java/com/keenwrite/editors/markdown/MarkdownEditor.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.markdown;
3
4
import com.keenwrite.Constants;
5
import com.keenwrite.editors.TextEditor;
6
import com.keenwrite.preferences.LocaleProperty;
7
import com.keenwrite.preferences.Workspace;
8
import com.keenwrite.processors.markdown.Caret;
9
import com.keenwrite.spelling.impl.TextEditorSpeller;
10
import javafx.beans.binding.Bindings;
11
import javafx.beans.property.BooleanProperty;
12
import javafx.beans.property.DoubleProperty;
13
import javafx.beans.property.ReadOnlyBooleanProperty;
14
import javafx.beans.property.SimpleBooleanProperty;
15
import javafx.beans.value.ChangeListener;
16
import javafx.event.Event;
17
import javafx.scene.Node;
18
import javafx.scene.control.IndexRange;
19
import javafx.scene.input.KeyCode;
20
import javafx.scene.input.KeyEvent;
21
import javafx.scene.layout.BorderPane;
22
import org.fxmisc.flowless.VirtualizedScrollPane;
23
import org.fxmisc.richtext.StyleClassedTextArea;
24
import org.fxmisc.richtext.model.StyleSpans;
25
import org.fxmisc.undo.UndoManager;
26
import org.fxmisc.wellbehaved.event.EventPattern;
27
import org.fxmisc.wellbehaved.event.Nodes;
28
29
import java.io.File;
30
import java.nio.charset.Charset;
31
import java.text.BreakIterator;
32
import java.util.*;
33
import java.util.function.Consumer;
34
import java.util.function.Supplier;
35
import java.util.regex.Pattern;
36
37
import static com.keenwrite.Constants.*;
38
import static com.keenwrite.Messages.get;
39
import static com.keenwrite.StatusBarNotifier.clue;
40
import static com.keenwrite.preferences.Workspace.KEY_UI_FONT_EDITOR_SIZE;
41
import static com.keenwrite.preferences.Workspace.KEY_UI_FONT_LOCALE;
42
import static java.lang.Character.isWhitespace;
43
import static java.lang.Math.max;
44
import static java.lang.String.format;
45
import static java.util.Collections.singletonList;
46
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.ALWAYS;
47
import static javafx.scene.input.KeyCode.*;
48
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
49
import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
50
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
51
import static org.apache.commons.lang3.StringUtils.stripEnd;
52
import static org.apache.commons.lang3.StringUtils.stripStart;
53
import static org.fxmisc.richtext.model.StyleSpans.singleton;
54
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
55
import static org.fxmisc.wellbehaved.event.InputMap.consume;
56
57
/**
58
 * Responsible for editing Markdown documents.
59
 */
60
public class MarkdownEditor extends BorderPane implements TextEditor {
61
  private static final String NEWLINE = System.lineSeparator();
62
63
  /**
64
   * Regular expression that matches the type of markup block. This is used
65
   * when Enter is pressed to continue the block environment.
66
   */
67
  private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile(
68
    "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
69
70
  /**
71
   * The text editor.
72
   */
73
  private final StyleClassedTextArea mTextArea =
74
    new StyleClassedTextArea( false );
75
76
  /**
77
   * Wraps the text editor in scrollbars.
78
   */
79
  private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
80
    new VirtualizedScrollPane<>( mTextArea );
81
82
  private final Workspace mWorkspace;
83
84
  /**
85
   * Tracks where the caret is located in this document. This offers observable
86
   * properties for caret position changes.
87
   */
88
  private final Caret mCaret = createCaret( mTextArea );
89
90
  /**
91
   * File being edited by this editor instance.
92
   */
93
  private File mFile;
94
95
  /**
96
   * Set to {@code true} upon text or caret position changes. Value is {@code
97
   * false} by default.
98
   */
99
  private final BooleanProperty mDirty = new SimpleBooleanProperty();
100
101
  /**
102
   * Opened file's character encoding, or {@link Constants#DEFAULT_CHARSET} if
103
   * either no encoding could be determined or this is a new (empty) file.
104
   */
105
  private final Charset mEncoding;
106
107
  /**
108
   * Tracks whether the in-memory definitions have changed with respect to the
109
   * persisted definitions.
110
   */
111
  private final BooleanProperty mModified = new SimpleBooleanProperty();
112
113
  public MarkdownEditor( final Workspace workspace ) {
114
    this( DOCUMENT_DEFAULT, workspace );
115
  }
116
117
  public MarkdownEditor( final File file, final Workspace workspace ) {
118
    mEncoding = open( mFile = file );
119
    mWorkspace = workspace;
120
121
    initTextArea( mTextArea );
122
    initStyle( mTextArea );
123
    initScrollPane( mScrollPane );
124
    initSpellchecker( mTextArea );
125
    initHotKeys();
126
    initUndoManager();
127
  }
128
129
  private void initTextArea( final StyleClassedTextArea textArea ) {
130
    textArea.setWrapText( true );
131
    textArea.requestFollowCaret();
132
    textArea.moveTo( 0 );
133
134
    textArea.textProperty().addListener( ( c, o, n ) -> {
135
      // Fire, regardless of whether the caret position has changed.
136
      mDirty.set( false );
137
138
      // Prevent a caret position change from raising the dirty bits.
139
      mDirty.set( true );
140
    } );
141
    textArea.caretPositionProperty().addListener( ( c, o, n ) -> {
142
      // Fire when the caret position has changed and the text has not.
143
      mDirty.set( true );
144
      mDirty.set( false );
145
    } );
146
  }
147
148
  private void initStyle( final StyleClassedTextArea textArea ) {
149
    textArea.getStyleClass().add( "markdown" );
150
151
    final var stylesheets = textArea.getStylesheets();
152
    stylesheets.add( STYLESHEET_MARKDOWN );
153
    stylesheets.add( getStylesheetPath( getLocale() ) );
154
155
    localeProperty().addListener( ( c, o, n ) -> {
156
      if( n != null ) {
157
        stylesheets.remove( max( 0, stylesheets.size() - 1 ) );
158
        stylesheets.add( getStylesheetPath( getLocale() ) );
159
      }
160
    } );
161
162
    fontSizeProperty().addListener( ( c, o, n ) -> {
163
      mTextArea.setStyle( format( "-fx-font-size: %spt;", getFontSize() ) );
164
    } );
165
  }
166
167
  private void initScrollPane(
168
    final VirtualizedScrollPane<StyleClassedTextArea> scrollpane ) {
169
    scrollpane.setVbarPolicy( ALWAYS );
170
    setCenter( scrollpane );
171
  }
172
173
  private void initSpellchecker( final StyleClassedTextArea textarea ) {
174
    final var speller = new TextEditorSpeller();
175
    speller.checkDocument( textarea );
176
    speller.checkParagraphs( textarea );
177
  }
178
179
  private void initHotKeys() {
180
    addEventListener( keyPressed( ENTER ), this::onEnterPressed );
181
    addEventListener( keyPressed( X, CONTROL_DOWN ), this::cut );
182
    addEventListener( keyPressed( TAB ), this::tab );
183
    addEventListener( keyPressed( TAB, SHIFT_DOWN ), this::untab );
184
  }
185
186
  private void initUndoManager() {
187
    final var undoManager = getUndoManager();
188
    final var markedPosition = undoManager.atMarkedPositionProperty();
189
190
    undoManager.forgetHistory();
191
    undoManager.mark();
192
    mModified.bind( Bindings.not( markedPosition ) );
193
  }
194
195
  @Override
196
  public void moveTo( final int offset ) {
197
    assert 0 <= offset && offset <= mTextArea.getLength();
198
    mTextArea.moveTo( offset );
199
    mTextArea.requestFollowCaret();
200
  }
201
202
  /**
203
   * Delegate the focus request to the text area itself.
204
   */
205
  @Override
206
  public void requestFocus() {
207
    mTextArea.requestFocus();
208
  }
209
210
  @Override
211
  public void setText( final String text ) {
212
    mTextArea.clear();
213
    mTextArea.appendText( text );
214
    mTextArea.getUndoManager().mark();
215
  }
216
217
  @Override
218
  public String getText() {
219
    return mTextArea.getText();
220
  }
221
222
  @Override
223
  public Charset getEncoding() {
224
    return mEncoding;
225
  }
226
227
  @Override
228
  public File getFile() {
229
    return mFile;
230
  }
231
232
  @Override
233
  public void rename( final File file ) {
234
    mFile = file;
235
  }
236
237
  @Override
238
  public void undo() {
239
    final var manager = getUndoManager();
240
    xxdo( manager::isUndoAvailable, manager::undo, "Main.status.error.undo" );
241
  }
242
243
  @Override
244
  public void redo() {
245
    final var manager = getUndoManager();
246
    xxdo( manager::isRedoAvailable, manager::redo, "Main.status.error.redo" );
247
  }
248
249
  /**
250
   * Performs an undo or redo action, if possible, otherwise displays an error
251
   * message to the user.
252
   *
253
   * @param ready  Answers whether the action can be executed.
254
   * @param action The action to execute.
255
   * @param key    The informational message key having a value to display if
256
   *               the {@link Supplier} is not ready.
257
   */
258
  private void xxdo(
259
    final Supplier<Boolean> ready, final Runnable action, final String key ) {
260
    if( ready.get() ) {
261
      action.run();
262
    }
263
    else {
264
      clue( key );
265
    }
266
  }
267
268
  @Override
269
  public void cut() {
270
    final var selected = mTextArea.getSelectedText();
271
272
    // Emulate selecting the current line by firing Home then Shift+Down Arrow.
273
    if( selected == null || selected.isEmpty() ) {
274
      // Note: mTextArea.selectLine() does not select empty lines.
275
      mTextArea.fireEvent( keyEvent( HOME, false ) );
276
      mTextArea.fireEvent( keyEvent( DOWN, true ) );
277
    }
278
279
    mTextArea.cut();
280
  }
281
282
  private Event keyEvent( final KeyCode code, final boolean shift ) {
283
    return new KeyEvent(
284
      KEY_PRESSED, "", "", code, shift, false, false, false
285
    );
286
  }
287
288
  @Override
289
  public void copy() {
290
    mTextArea.copy();
291
  }
292
293
  @Override
294
  public void paste() {
295
    mTextArea.paste();
296
  }
297
298
  @Override
299
  public void selectAll() {
300
    mTextArea.selectAll();
301
  }
302
303
  @Override
304
  public void bold() {
305
    enwrap( "**" );
306
  }
307
308
  @Override
309
  public void italic() {
310
    enwrap( "*" );
311
  }
312
313
  @Override
314
  public void superscript() {
315
    enwrap( "^" );
316
  }
317
318
  @Override
319
  public void subscript() {
320
    enwrap( "~" );
321
  }
322
323
  @Override
324
  public void strikethrough() {
325
    enwrap( "~~" );
326
  }
327
328
  @Override
329
  public void blockquote() {
330
    block( "> " );
331
  }
332
333
  @Override
334
  public void code() {
335
    enwrap( "`" );
336
  }
337
338
  @Override
339
  public void fencedCodeBlock() {
340
    final var key = "App.action.insert.fenced_code_block.prompt.text";
341
342
    // TODO: Introduce sample text if nothing is selected.
343
    //enwrap( "\n\n```\n", "\n```\n\n", get( key ) );
344
  }
345
346
  @Override
347
  public void heading( final int level ) {
348
    final var hashes = new String( new char[ level ] ).replace( "\0", "#" );
349
    block( format( "%s ", hashes ) );
350
  }
351
352
  @Override
353
  public void unorderedList() {
354
    block( "* " );
355
  }
356
357
  @Override
358
  public void orderedList() {
359
    block( "1. " );
360
  }
361
362
  @Override
363
  public void horizontalRule() {
364
    block( format( "---%n%n" ) );
365
  }
366
367
  @Override
368
  public Node getNode() {
369
    return this;
370
  }
371
372
  @Override
373
  public ReadOnlyBooleanProperty modifiedProperty() {
374
    return mModified;
375
  }
376
377
  @Override
378
  public void clearModifiedProperty() {
379
    getUndoManager().mark();
380
  }
381
382
  @Override
383
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
384
    return mScrollPane;
385
  }
386
387
  @Override
388
  public StyleClassedTextArea getTextArea() {
389
    return mTextArea;
390
  }
391
392
  private final Map<String, IndexRange> mStyles = new HashMap<>();
393
394
  @Override
395
  public void stylize( final IndexRange range, final String style ) {
396
    final var began = range.getStart();
397
    final var ended = range.getEnd() + 1;
398
399
    assert 0 <= began && began <= ended;
400
    assert style != null;
401
402
    // TODO: Ensure spell check and find highlights can coexist.
403
//    final var spans = mTextArea.getStyleSpans( range );
404
//    System.out.println( "SPANS: " + spans );
405
406
//    final var spans = mTextArea.getStyleSpans( range );
407
//    mTextArea.setStyleSpans( began, merge( spans, range.getLength(), style
408
//    ) );
409
410
//    final var builder = new StyleSpansBuilder<Collection<String>>();
411
//    builder.add( singleton( style ), range.getLength() + 1 );
412
//    mTextArea.setStyleSpans( began, builder.create() );
413
414
//    final var s = mTextArea.getStyleSpans( began, ended );
415
//    System.out.println( "STYLES: " +s );
416
417
    mStyles.put( style, range );
418
    mTextArea.setStyleClass( began, ended, style );
419
420
    // Ensure that whenever the user interacts with the text that the found
421
    // word will have its highlighting removed. The handler removes itself.
422
    // This won't remove the highlighting if the caret position moves by mouse.
423
    final var handler = mTextArea.getOnKeyPressed();
424
    mTextArea.setOnKeyPressed( ( event ) -> {
425
      mTextArea.setOnKeyPressed( handler );
426
      unstylize( style );
427
    } );
428
429
    //mTextArea.setStyleSpans(began, ended, s);
430
  }
431
432
  private static StyleSpans<Collection<String>> merge(
433
    StyleSpans<Collection<String>> spans, int len, String style ) {
434
    spans = spans.overlay(
435
      singleton( singletonList( style ), len ),
436
      ( bottomSpan, list ) -> {
437
        final List<String> l =
438
          new ArrayList<>( bottomSpan.size() + list.size() );
439
        l.addAll( bottomSpan );
440
        l.addAll( list );
441
        return l;
442
      } );
443
444
    return spans;
445
  }
446
447
  @Override
448
  public void unstylize( final String style ) {
449
    final var indexes = mStyles.remove( style );
450
    if( indexes != null ) {
451
      mTextArea.clearStyle( indexes.getStart(), indexes.getEnd() + 1 );
452
    }
453
  }
454
455
  @Override
456
  public Caret getCaret() {
457
    return mCaret;
458
  }
459
460
  private Caret createCaret( final StyleClassedTextArea editor ) {
461
    return Caret
462
      .builder()
463
      .with( Caret.Mutator::setEditor, editor )
464
      .build();
465
  }
466
467
  /**
468
   * This method adds listeners to editor events.
469
   *
470
   * @param <T>      The event type.
471
   * @param <U>      The consumer type for the given event type.
472
   * @param event    The event of interest.
473
   * @param consumer The method to call when the event happens.
474
   */
475
  public <T extends Event, U extends T> void addEventListener(
476
    final EventPattern<? super T, ? extends U> event,
477
    final Consumer<? super U> consumer ) {
478
    Nodes.addInputMap( mTextArea, consume( event, consumer ) );
479
  }
480
481
  @SuppressWarnings( "unused" )
482
  private void onEnterPressed( final KeyEvent event ) {
483
    final var currentLine = getCaretParagraph();
484
    final var matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
485
486
    // By default, insert a new line by itself.
487
    String newText = NEWLINE;
488
489
    // If the pattern was matched then determine what block type to continue.
490
    if( matcher.matches() ) {
491
      if( matcher.group( 2 ).isEmpty() ) {
492
        final var pos = mTextArea.getCaretPosition();
493
        mTextArea.selectRange( pos - currentLine.length(), pos );
494
      }
495
      else {
496
        // Indent the new line with the same whitespace characters and
497
        // list markers as current line. This ensures that the indentation
498
        // is propagated.
499
        newText = newText.concat( matcher.group( 1 ) );
500
      }
501
    }
502
503
    mTextArea.replaceSelection( newText );
504
  }
505
506
  private void cut( final KeyEvent event ) {
507
    cut();
508
  }
509
510
  private void tab( final KeyEvent event ) {
511
    final var range = mTextArea.selectionProperty().getValue();
512
    final var sb = new StringBuilder( 1024 );
513
514
    if( range.getLength() > 0 ) {
515
      final var selection = mTextArea.getSelectedText();
516
517
      selection.lines().forEach(
518
        ( l ) -> sb.append( "\t" ).append( l ).append( NEWLINE )
519
      );
520
    }
521
    else {
522
      sb.append( "\t" );
523
    }
524
525
    mTextArea.replaceSelection( sb.toString() );
526
  }
527
528
  private void untab( final KeyEvent event ) {
529
    final var range = mTextArea.selectionProperty().getValue();
530
531
    if( range.getLength() > 0 ) {
532
      final var selection = mTextArea.getSelectedText();
533
      final var sb = new StringBuilder( selection.length() );
534
535
      selection.lines().forEach(
536
        ( l ) -> sb.append( l.startsWith( "\t" ) ? l.substring( 1 ) : l )
537
                   .append( NEWLINE )
538
      );
539
540
      mTextArea.replaceSelection( sb.toString() );
541
    }
542
    else {
543
      final var p = getCaretParagraph();
544
545
      if( p.startsWith( "\t" ) ) {
546
        mTextArea.selectParagraph();
547
        mTextArea.replaceSelection( p.substring( 1 ) );
548
      }
549
    }
550
  }
551
552
  /**
553
   * Observers may listen for changes to the property returned from this method
554
   * to receive notifications when either the text or caret have changed. This
555
   * should not be used to track whether the text has been modified.
556
   */
557
  public void addDirtyListener( ChangeListener<Boolean> listener ) {
558
    mDirty.addListener( listener );
559
  }
560
561
  /**
562
   * Surrounds the selected text or word under the caret in Markdown markup.
563
   *
564
   * @param token The beginning and ending token for enclosing the text.
565
   */
566
  private void enwrap( final String token ) {
567
    enwrap( token, token );
568
  }
569
570
  /**
571
   * Surrounds the selected text or word under the caret in Markdown markup.
572
   *
573
   * @param began The beginning token for enclosing the text.
574
   * @param ended The ending token for enclosing the text.
575
   */
576
  private void enwrap( final String began, String ended ) {
577
    // Ensure selected text takes precedence over the word at caret position.
578
    final var selected = mTextArea.selectionProperty().getValue();
579
    final var range = selected.getLength() == 0
580
      ? getCaretWord()
581
      : selected;
582
    String text = mTextArea.getText( range );
583
584
    int length = range.getLength();
585
    text = stripStart( text, null );
586
    final int beganIndex = range.getStart() + (length - text.length());
587
588
    length = text.length();
589
    text = stripEnd( text, null );
590
    final int endedIndex = range.getEnd() - (length - text.length());
591
592
    mTextArea.replaceText( beganIndex, endedIndex, began + text + ended );
593
  }
594
595
  /**
596
   * Inserts the given block-level markup at the current caret position
597
   * within the document. This will prepend two blank lines to ensure that
598
   * the block element begins at the start of a new line.
599
   *
600
   * @param markup The text to insert at the caret.
601
   */
602
  private void block( final String markup ) {
603
    final int pos = mTextArea.getCaretPosition();
604
    mTextArea.insertText( pos, format( "%n%n%s", markup ) );
605
  }
606
607
  /**
608
   * Returns the caret position within the current paragraph.
609
   *
610
   * @return A value from 0 to the length of the current paragraph.
611
   */
612
  private int getCaretColumn() {
613
    return mTextArea.getCaretColumn();
614
  }
615
616
  @Override
617
  public IndexRange getCaretWord() {
618
    final var paragraph = getCaretParagraph();
619
    final var length = paragraph.length();
620
    final var column = getCaretColumn();
621
622
    var began = column;
623
    var ended = column;
624
625
    while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
626
      began--;
627
    }
628
629
    while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
630
      ended++;
631
    }
632
633
    final var iterator = BreakIterator.getWordInstance();
634
    iterator.setText( paragraph );
635
636
    while( began < length && iterator.isBoundary( began + 1 ) ) {
637
      began++;
638
    }
639
640
    while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
641
      ended--;
642
    }
643
644
    final var offset = getCaretDocumentOffset( column );
645
646
    return IndexRange.normalize( began + offset, ended + offset );
647
  }
648
649
  private int getCaretDocumentOffset( final int column ) {
650
    return mTextArea.getCaretPosition() - column;
651
  }
652
653
  /**
654
   * Returns the index of the paragraph where the caret resides.
655
   *
656
   * @return A number greater than or equal to 0.
657
   */
658
  private int getCurrentParagraph() {
659
    return mTextArea.getCurrentParagraph();
660
  }
661
662
  /**
663
   * Returns the text for the paragraph that contains the caret.
664
   *
665
   * @return A non-null string, possibly empty.
666
   */
667
  private String getCaretParagraph() {
668
    return getText( getCurrentParagraph() );
669
  }
670
671
  @Override
672
  public String getText( final int paragraph ) {
673
    return mTextArea.getText( paragraph );
674
  }
675
676
  @Override
677
  public String getText( final IndexRange indexes )
678
    throws IndexOutOfBoundsException {
679
    return mTextArea.getText( indexes.getStart(), indexes.getEnd() );
680
  }
681
682
  @Override
683
  public void replaceText( final IndexRange indexes, final String s ) {
684
    mTextArea.replaceText( indexes, s );
685
  }
686
687
  private UndoManager<?> getUndoManager() {
688
    return mTextArea.getUndoManager();
689
  }
690
691
  /**
692
   * Returns the path to a {@link Locale}-specific stylesheet.
693
   *
694
   * @return A non-null string to inject into the HTML document head.
695
   */
696
  private static String getStylesheetPath( final Locale locale ) {
697
    return get(
698
      sSettings.getSetting( STYLESHEET_MARKDOWN_LOCALE, "" ),
699
      locale.getLanguage(),
700
      locale.getScript(),
701
      locale.getCountry()
702
    );
703
  }
704
705
  private Locale getLocale() {
706
    return localeProperty().toLocale();
707
  }
708
709
  private LocaleProperty localeProperty() {
710
    return mWorkspace.localeProperty( KEY_UI_FONT_LOCALE );
711
  }
712
713
  private double getFontSize() {
714
    return fontSizeProperty().get();
715
  }
716
717
  private DoubleProperty fontSizeProperty() {
718
    return mWorkspace.doubleProperty( KEY_UI_FONT_EDITOR_SIZE );
719
  }
720
}
1721
D src/main/java/com/keenwrite/editors/markdown/MarkdownEditorPane.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.editors.markdown;
29
30
import com.keenwrite.dialogs.ImageDialog;
31
import com.keenwrite.dialogs.LinkDialog;
32
import com.keenwrite.editors.EditorPane;
33
import com.keenwrite.processors.markdown.MarkdownProcessor;
34
import com.vladsch.flexmark.ast.Link;
35
import javafx.scene.control.Dialog;
36
import javafx.scene.control.IndexRange;
37
import javafx.scene.input.KeyCode;
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.keenwrite.Constants.STYLESHEET_MARKDOWN;
47
import static com.keenwrite.util.Utils.ltrim;
48
import static com.keenwrite.util.Utils.rtrim;
49
import static javafx.scene.input.KeyCode.ENTER;
50
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
51
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
52
53
/**
54
 * Provides the ability to edit a text document.
55
 */
56
public class MarkdownEditorPane extends EditorPane {
57
  private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile(
58
      "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
59
60
  public MarkdownEditorPane() {
61
    initEditor();
62
  }
63
64
  private void initEditor() {
65
    final StyleClassedTextArea textArea = getEditor();
66
67
    textArea.setWrapText( true );
68
    textArea.getStyleClass().add( "markdown-editor" );
69
    textArea.getStylesheets().add( STYLESHEET_MARKDOWN );
70
71
    addKeyboardListener( keyPressed( ENTER ), this::enterPressed );
72
    addKeyboardListener( keyPressed( KeyCode.X, CONTROL_DOWN ), this::cut );
73
  }
74
75
  public void insertLink() {
76
    insertObject( createLinkDialog() );
77
  }
78
79
  public void insertImage() {
80
    insertObject( createImageDialog() );
81
  }
82
83
  /**
84
   * Gets the index of the paragraph where the caret is positioned.
85
   *
86
   * @return The paragraph number for the caret.
87
   */
88
  public int getCurrentParagraphIndex() {
89
    return getEditor().getCurrentParagraph();
90
  }
91
92
  /**
93
   * @param leading  Characters to insert at the beginning of the current
94
   *                 selection (or paragraph).
95
   * @param trailing Characters to insert at the end of the current selection
96
   *                 (or paragraph).
97
   */
98
  public void surroundSelection( final String leading, final String trailing ) {
99
    surroundSelection( leading, trailing, null );
100
  }
101
102
  /**
103
   * @param leading  Characters to insert at the beginning of the current
104
   *                 selection (or paragraph).
105
   * @param trailing Characters to insert at the end of the current selection
106
   *                 (or paragraph).
107
   * @param hint     Instructional text inserted within the leading and
108
   *                 trailing characters, provided no text is selected.
109
   */
110
  public void surroundSelection(
111
      String leading, String trailing, 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
    String trimmedText = selectedText.trim();
123
    if( trimmedText.length() < selectedText.length() ) {
124
      start += selectedText.indexOf( trimmedText );
125
      end = start + trimmedText.length();
126
    }
127
128
    // remove leading whitespaces from leading text if selection starts at zero
129
    if( start == 0 ) {
130
      leading = ltrim( leading );
131
    }
132
133
    // remove trailing whitespaces from trailing text if selection ends at
134
    // text end
135
    if( end == textArea.getLength() ) {
136
      trailing = rtrim( trailing );
137
    }
138
139
    // remove leading line separators from leading text
140
    // if there are line separators before the selected text
141
    if( leading.startsWith( "\n" ) ) {
142
      for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) {
143
        if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
144
          break;
145
        }
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 && trimmedText.isEmpty() ) {
180
      trimmedText = 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, end, leading + trimmedText + trailing );
189
    textArea.selectRange( selStart, selEnd );
190
  }
191
192
  private void enterPressed( final KeyEvent e ) {
193
    final StyleClassedTextArea textArea = getEditor();
194
    final String currentLine =
195
        textArea.getText( textArea.getCurrentParagraph() );
196
    final Matcher matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
197
198
    String newText = "\n";
199
200
    if( matcher.matches() ) {
201
      if( !matcher.group( 2 ).isEmpty() ) {
202
        // indent new line with same whitespace characters and list markers
203
        // as current line
204
        newText = newText.concat( matcher.group( 1 ) );
205
      }
206
      else {
207
        // current line contains only whitespace characters and list markers
208
        // --> empty current line
209
        final int caretPosition = textArea.getCaretPosition();
210
        textArea.selectRange( caretPosition - currentLine.length(),
211
                              caretPosition );
212
      }
213
    }
214
215
    textArea.replaceSelection( newText );
216
217
    // Ensure that the window scrolls when Enter is pressed at the bottom of
218
    // the pane.
219
    textArea.requestFollowCaret();
220
  }
221
222
  private void cut( final KeyEvent event ) {
223
    super.cut();
224
  }
225
226
  /**
227
   * Returns one of: selected text, word under cursor, or parsed hyperlink from
228
   * the markdown AST.
229
   *
230
   * @return An instance containing the link URL and display text.
231
   */
232
  private HyperlinkModel getHyperlink() {
233
    final var textArea = getEditor();
234
    final var selectedText = textArea.getSelectedText();
235
236
    // Get the current paragraph, convert to Markdown nodes.
237
    final var mp = MarkdownProcessor.create();
238
    final var p = textArea.getCurrentParagraph();
239
    final var paragraph = textArea.getText( p );
240
    final var node = mp.toNode( paragraph );
241
    final var visitor = new LinkVisitor( textArea.getCaretColumn() );
242
    final var link = visitor.process( node );
243
244
    if( link != null ) {
245
      textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
246
    }
247
248
    return createHyperlinkModel(
249
        link, selectedText, "https://localhost"
250
    );
251
  }
252
253
  @SuppressWarnings("SameParameterValue")
254
  private HyperlinkModel createHyperlinkModel(
255
      final Link link, final String selection, final String url ) {
256
257
    return link == null
258
        ? new HyperlinkModel( selection, url )
259
        : new HyperlinkModel( link );
260
  }
261
262
  private Path getParentPath() {
263
    final Path path = getPath();
264
    return path != null ? path.getParent() : null;
265
  }
266
267
  private Dialog<String> createLinkDialog() {
268
    return new LinkDialog( getWindow(), getHyperlink() );
269
  }
270
271
  private Dialog<String> createImageDialog() {
272
    return new ImageDialog( getWindow(), getParentPath() );
273
  }
274
275
  private void insertObject( final Dialog<String> dialog ) {
276
    dialog.showAndWait().ifPresent(
277
        result -> getEditor().replaceSelection( result )
278
    );
279
  }
280
281
  private Window getWindow() {
282
    return getScrollPane().getScene().getWindow();
283
  }
284
}
2851
M src/main/java/com/keenwrite/exceptions/MissingFileException.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.exceptions;
293
A src/main/java/com/keenwrite/io/FileType.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
/**
5
 * Represents different file type classifications. These are high-level mappings
6
 * that correspond to the list of glob patterns found within {@code
7
 * settings.properties}.
8
 */
9
public enum FileType {
10
11
  ALL( "all" ),
12
  RMARKDOWN( "rmarkdown" ),
13
  RXML( "rxml" ),
14
  SOURCE( "source" ),
15
  DEFINITION( "definition" ),
16
  XML( "xml" ),
17
  CSV( "csv" ),
18
  JSON( "json" ),
19
  TOML( "toml" ),
20
  YAML( "yaml" ),
21
  PROPERTIES( "properties" ),
22
  UNKNOWN( "unknown" );
23
24
  private final String mType;
25
26
  /**
27
   * Default constructor for enumerated file type.
28
   *
29
   * @param type Human-readable name for the file type.
30
   */
31
  FileType( final String type ) {
32
    mType = type;
33
  }
34
35
  /**
36
   * Returns the file type that corresponds to the given string.
37
   *
38
   * @param type The string to compare against this enumeration of file types.
39
   * @return The corresponding File Type for the given string.
40
   * @throws IllegalArgumentException Type not found.
41
   */
42
  public static FileType from( final String type ) {
43
    for( final FileType fileType : FileType.values() ) {
44
      if( fileType.isType( type ) ) {
45
        return fileType;
46
      }
47
    }
48
49
    throw new IllegalArgumentException( type );
50
  }
51
52
  /**
53
   * Answers whether this file type matches the given string, case insensitive
54
   * comparison.
55
   *
56
   * @param type Presumably a file name extension to check against.
57
   * @return true The given extension corresponds to this enumerated type.
58
   */
59
  public boolean isType( final String type ) {
60
    return getType().equalsIgnoreCase( type );
61
  }
62
63
  /**
64
   * Returns the human-readable name for the file type.
65
   *
66
   * @return A non-null instance.
67
   */
68
  private String getType() {
69
    return mType;
70
  }
71
72
  /**
73
   * Returns the lowercase version of the file name extension.
74
   *
75
   * @return The file name, in lower case.
76
   */
77
  @Override
78
  public String toString() {
79
    return getType();
80
  }
81
}
182
A src/main/java/com/keenwrite/io/HttpMediaType.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import java.net.MalformedURLException;
5
import java.net.URI;
6
import java.net.URL;
7
import java.net.http.HttpClient;
8
import java.net.http.HttpRequest;
9
10
import static com.keenwrite.StatusBarNotifier.clue;
11
import static com.keenwrite.io.MediaType.UNDEFINED;
12
import static java.net.http.HttpClient.Redirect.NORMAL;
13
import static java.net.http.HttpRequest.BodyPublishers.noBody;
14
import static java.net.http.HttpResponse.BodyHandlers.discarding;
15
import static java.time.Duration.ofSeconds;
16
17
/**
18
 * Responsible for determining {@link MediaType} based on the content-type from
19
 * an HTTP request.
20
 */
21
public class HttpMediaType {
22
23
  private final static HttpClient HTTP_CLIENT = HttpClient
24
    .newBuilder()
25
    .connectTimeout( ofSeconds( 5 ) )
26
    .followRedirects( NORMAL )
27
    .build();
28
29
  /**
30
   * Performs an HTTP HEAD request to determine the media type based on the
31
   * Content-Type header returned from the server.
32
   *
33
   * @param uri Determine the media type for this resource.
34
   * @return The data type for the resource or {@link MediaType#UNDEFINED} if
35
   * unmapped.
36
   * @throws MalformedURLException The {@link URI} could not be converted to
37
   *                               a {@link URL}.
38
   */
39
  public static MediaType valueFrom( final URI uri )
40
    throws MalformedURLException {
41
    final var mediaType = new MediaType[]{UNDEFINED};
42
43
    try {
44
      clue( "Main.status.image.request.init" );
45
      final var request = HttpRequest
46
        .newBuilder( uri )
47
        .method( "HEAD", noBody() )
48
        .build();
49
      clue( "Main.status.image.request.fetch", uri.getHost() );
50
      final var response = HTTP_CLIENT.send( request, discarding() );
51
      final var headers = response.headers();
52
      final var map = headers.map();
53
54
      map.forEach( ( key, values ) -> {
55
        if( "content-type".equalsIgnoreCase( key ) ) {
56
          var header = values.get( 0 );
57
          // Trim off the character encoding.
58
          var i = header.indexOf( ';' );
59
          header = header.substring( 0, i == -1 ? header.length() : i );
60
61
          // Split the type and subtype.
62
          i = header.indexOf( '/' );
63
          i = i == -1 ? header.length() : i;
64
          final var type = header.substring( 0, i );
65
          final var subtype = header.substring( i + 1 );
66
67
          mediaType[ 0 ] = MediaType.valueFrom( type, subtype );
68
          clue( "Main.status.image.request.success", mediaType[ 0 ] );
69
        }
70
      } );
71
    } catch( final Exception ex ) {
72
      clue( ex );
73
    }
74
75
    return mediaType[ 0 ];
76
  }
77
}
178
A src/main/java/com/keenwrite/io/MediaType.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import java.io.File;
5
import java.nio.file.Path;
6
7
import static com.keenwrite.io.MediaType.TypeName.*;
8
import static com.keenwrite.io.MediaTypeExtensions.getMediaType;
9
import static org.apache.commons.io.FilenameUtils.getExtension;
10
11
/**
12
 * Defines various file formats and format contents.
13
 *
14
 * @see
15
 * <a href="https://www.iana.org/assignments/media-types/media-types.xhtml">IANA
16
 * Media Types</a>
17
 */
18
public enum MediaType {
19
  APP_JAVA_OBJECT(
20
    APPLICATION, "x-java-serialized-object"
21
  ),
22
23
  FONT_OTF( "otf" ),
24
  FONT_TTF( "ttf" ),
25
26
  IMAGE_APNG( "apng" ),
27
  IMAGE_ACES( "aces" ),
28
  IMAGE_AVCI( "avci" ),
29
  IMAGE_AVCS( "avcs" ),
30
  IMAGE_BMP( "bmp" ),
31
  IMAGE_CGM( "cgm" ),
32
  IMAGE_DICOM_RLE( "dicom_rle" ),
33
  IMAGE_EMF( "emf" ),
34
  IMAGE_EXAMPLE( "example" ),
35
  IMAGE_FITS( "fits" ),
36
  IMAGE_G3FAX( "g3fax" ),
37
  IMAGE_GIF( "gif" ),
38
  IMAGE_HEIC( "heic" ),
39
  IMAGE_HEIF( "heif" ),
40
  IMAGE_HEJ2K( "hej2k" ),
41
  IMAGE_HSJ2( "hsj2" ),
42
  IMAGE_X_ICON( "x-icon" ),
43
  IMAGE_JLS( "jls" ),
44
  IMAGE_JP2( "jp2" ),
45
  IMAGE_JPEG( "jpeg" ),
46
  IMAGE_JPH( "jph" ),
47
  IMAGE_JPHC( "jphc" ),
48
  IMAGE_JPM( "jpm" ),
49
  IMAGE_JPX( "jpx" ),
50
  IMAGE_JXR( "jxr" ),
51
  IMAGE_JXRA( "jxrA" ),
52
  IMAGE_JXRS( "jxrS" ),
53
  IMAGE_JXS( "jxs" ),
54
  IMAGE_JXSC( "jxsc" ),
55
  IMAGE_JXSI( "jxsi" ),
56
  IMAGE_JXSS( "jxss" ),
57
  IMAGE_KTX( "ktx" ),
58
  IMAGE_KTX2( "ktx2" ),
59
  IMAGE_NAPLPS( "naplps" ),
60
  IMAGE_PNG( "png" ),
61
  IMAGE_SVG_XML( "svg+xml" ),
62
  IMAGE_T38( "t38" ),
63
  IMAGE_TIFF( "tiff" ),
64
  IMAGE_WEBP( "webp" ),
65
  IMAGE_WMF( "wmf" ),
66
67
  TEXT_HTML( TEXT, "html" ),
68
  TEXT_MARKDOWN( TEXT, "markdown" ),
69
  TEXT_PLAIN( TEXT, "plain" ),
70
  TEXT_R_MARKDOWN( TEXT, "R+markdown" ),
71
  TEXT_R_XML( TEXT, "R+xml" ),
72
  TEXT_YAML( TEXT, "yaml" ),
73
74
  UNDEFINED( TypeName.UNDEFINED, "undefined" );
75
76
  /**
77
   * The IANA-defined types.
78
   */
79
  public enum TypeName {
80
    APPLICATION,
81
    IMAGE,
82
    TEXT,
83
    UNDEFINED
84
  }
85
86
  /**
87
   * The fully qualified IANA-defined media type.
88
   */
89
  private final String mMediaType;
90
91
  /**
92
   * The IANA-defined type name.
93
   */
94
  private final TypeName mTypeName;
95
96
  /**
97
   * The IANA-defined subtype name.
98
   */
99
  private final String mSubtype;
100
101
  /**
102
   * Constructs an instance using the default type name of "image".
103
   *
104
   * @param subtype The image subtype name.
105
   */
106
  MediaType( final String subtype ) {
107
    this( IMAGE, subtype );
108
  }
109
110
  /**
111
   * Constructs an instance using an IANA-defined type and subtype pair.
112
   *
113
   * @param typeName The media type's type name.
114
   * @param subtype  The media type's subtype name.
115
   */
116
  MediaType( final TypeName typeName, final String subtype ) {
117
    mTypeName = typeName;
118
    mSubtype = subtype;
119
    mMediaType = typeName.toString().toLowerCase() + '/' + subtype;
120
  }
121
122
  /**
123
   * Returns the {@link MediaType} associated with the given file.
124
   *
125
   * @param file Has a file name that may contain an extension associated with
126
   *             a known {@link MediaType}.
127
   * @return {@link MediaType#UNDEFINED} if the extension has not been
128
   * assigned, otherwise the {@link MediaType} associated with this
129
   * {@link File}'s file name extension.
130
   */
131
  public static MediaType valueFrom( final File file ) {
132
    return valueFrom( file.getName() );
133
  }
134
135
  /**
136
   * Returns the {@link MediaType} associated with the path to a file.
137
   *
138
   * @param path Has a file name that may contain an extension associated with
139
   *             a known {@link MediaType}.
140
   * @return {@link MediaType#UNDEFINED} if the extension has not been
141
   * assigned, otherwise the {@link MediaType} associated with this
142
   * {@link File}'s file name extension.
143
   */
144
  public static MediaType valueFrom( final Path path ) {
145
    return valueFrom( path.toFile() );
146
  }
147
148
  /**
149
   * Returns the {@link MediaType} associated with the given file name.
150
   *
151
   * @param filename The file name that may contain an extension associated
152
   *                 with a known {@link MediaType}.
153
   * @return {@link MediaType#UNDEFINED} if the extension has not been
154
   * assigned, otherwise the {@link MediaType} associated with this
155
   * URL's file name extension.
156
   */
157
  public static MediaType valueFrom( final String filename ) {
158
    return getMediaType( getExtension( filename ) );
159
  }
160
161
  /**
162
   * Returns the {@link MediaType} for the given type and subtype names.
163
   *
164
   * @param type    The IANA-defined type name.
165
   * @param subtype The IANA-defined subtype name.
166
   * @return {@link MediaType#UNDEFINED} if there is no {@link MediaType} that
167
   * matches the given type and subtype names.
168
   */
169
  public static MediaType valueFrom(
170
    final String type, final String subtype ) {
171
    for( final var mediaType : MediaType.values() ) {
172
      if( mediaType.equals( type, subtype ) ) {
173
        return mediaType;
174
      }
175
    }
176
177
    return UNDEFINED;
178
  }
179
180
  /**
181
   * Answers whether the given type and subtype names equal this enumerated
182
   * value. This performs a case-insensitive comparison.
183
   *
184
   * @param type    The type name to compare against this {@link MediaType}.
185
   * @param subtype The subtype name to compare against this {@link MediaType}.
186
   * @return {@code true} when the type and subtype name match.
187
   */
188
  public boolean equals( final String type, final String subtype ) {
189
    return mTypeName.name().equalsIgnoreCase( type ) &&
190
      mSubtype.equalsIgnoreCase( subtype );
191
  }
192
193
  /**
194
   * Answers whether the given {@link TypeName} matches this type name.
195
   *
196
   * @param typeName The {@link TypeName} to compare against the internal value.
197
   * @return {@code true} if the given value is the same IANA-defined type name.
198
   */
199
  public boolean isType( final TypeName typeName ) {
200
    return mTypeName == typeName;
201
  }
202
203
  /**
204
   * Returns the IANA-defined type and sub-type.
205
   *
206
   * @return The unique media type identifier.
207
   */
208
  public String toString() {
209
    return mMediaType;
210
  }
211
212
  /**
213
   * Used by {@link MediaTypeExtensions} to initialize associations where the
214
   * subtype name and the file name extension have a 1:1 mapping.
215
   *
216
   * @return The IANA subtype value.
217
   */
218
  String getSubtype() {
219
    return mSubtype;
220
  }
221
}
1222
A src/main/java/com/keenwrite/io/MediaTypeExtensions.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import java.util.Set;
5
6
import static com.keenwrite.io.MediaType.*;
7
import static java.util.Set.of;
8
9
/**
10
 * Responsible for associating file extensions with {@link MediaType} instances.
11
 */
12
enum MediaTypeExtensions {
13
  MEDIA_FONT_OTF( FONT_OTF ),
14
  MEDIA_FONT_TTF( FONT_TTF ),
15
16
  MEDIA_IMAGE_APNG( IMAGE_APNG ),
17
  MEDIA_IMAGE_BMP( IMAGE_BMP ),
18
  MEDIA_IMAGE_GIF( IMAGE_GIF ),
19
  MEDIA_IMAGE_ICO( IMAGE_X_ICON, of( "ico", "cur" ) ),
20
  MEDIA_IMAGE_JPEG( IMAGE_JPEG, of( "jpg", "jpeg", "jfif", "pjpeg", "pjp" ) ),
21
  MEDIA_IMAGE_PNG( IMAGE_PNG ),
22
  MEDIA_IMAGE_SVG( IMAGE_SVG_XML, of( "svg" ) ),
23
  MEDIA_IMAGE_TIFF( IMAGE_TIFF, of( "tif", "tiff" ) ),
24
  MEDIA_IMAGE_WEBP( IMAGE_WEBP ),
25
26
  MEDIA_TEXT_MARKDOWN( TEXT_MARKDOWN, of(
27
    "md", "markdown", "mdown", "mdtxt", "mdtext", "mdwn", "mkd", "mkdown",
28
    "mkdn" ) ),
29
  MEDIA_TEXT_PLAIN( TEXT_PLAIN, of( "asc", "ascii", "txt", "text", "utxt" ) ),
30
  MEDIA_TEXT_R_MARKDOWN( TEXT_R_MARKDOWN, of( "Rmd" ) ),
31
  MEDIA_TEXT_R_XML( TEXT_R_XML, of( "Rxml" ) ),
32
  MEDIA_TEXT_YAML( TEXT_YAML, of( "yaml", "yml" ) );
33
34
  private final MediaType mMediaType;
35
  private final Set<String> mExtensions;
36
37
  /**
38
   * Several media types have only one corresponding standard file name
39
   * extension; this constructor calls {@link MediaType#getSubtype()} to obtain
40
   * said extension. Some {@link MediaType}s have a single extension but their
41
   * assigned IANA name differs (e.g., {@code svg} maps to {@code svg+xml})
42
   * and thus must not use this constructor.
43
   *
44
   * @param mediaType The {@link MediaType} containing only one extension.
45
   */
46
  MediaTypeExtensions( final MediaType mediaType ) {
47
    this( mediaType, of( mediaType.getSubtype() ) );
48
  }
49
50
  /**
51
   * Constructs an association of file name extensions to a single {@link
52
   * MediaType}.
53
   *
54
   * @param mediaType  The {@link MediaType} to associate with the given
55
   *                   file name extensions.
56
   * @param extensions The file name extensions used to lookup a corresponding
57
   *                   {@link MediaType}.
58
   */
59
  MediaTypeExtensions(
60
    final MediaType mediaType, final Set<String> extensions ) {
61
    assert mediaType != null;
62
    assert extensions != null;
63
    assert !extensions.isEmpty();
64
65
    mMediaType = mediaType;
66
    mExtensions = extensions;
67
  }
68
69
  /**
70
   * Returns the {@link MediaType} associated with the given file name
71
   * extension. The extension must not contain a period.
72
   *
73
   * @param extension File name extension, case insensitive, {@code null}-safe.
74
   * @return The associated {@link MediaType} as defined by IANA.
75
   */
76
  static MediaType getMediaType( final String extension ) {
77
    final var sanitized = sanitize( extension );
78
79
    for( final var mediaType : MediaTypeExtensions.values() ) {
80
      if( mediaType.isType( sanitized ) ) {
81
        return mediaType.getMediaType();
82
      }
83
    }
84
85
    return UNDEFINED;
86
  }
87
88
  private boolean isType( final String sanitized ) {
89
    for( final var extension : mExtensions ) {
90
      if( extension.equalsIgnoreCase( sanitized ) ) {
91
        return true;
92
      }
93
    }
94
95
    return false;
96
  }
97
98
  private static String sanitize( final String extension ) {
99
    return extension == null ? "" : extension.toLowerCase();
100
  }
101
102
  private MediaType getMediaType() {
103
    return mMediaType;
104
  }
105
}
1106
M src/main/java/com/keenwrite/predicates/PredicateFactory.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.predicates;
293
D src/main/java/com/keenwrite/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.keenwrite.preferences;
29
30
import java.io.File;
31
import java.io.FileInputStream;
32
import java.io.FileOutputStream;
33
import java.util.*;
34
import java.util.prefs.AbstractPreferences;
35
import java.util.prefs.BackingStoreException;
36
37
import static com.keenwrite.StatusBarNotifier.clue;
38
39
/**
40
 * Preferences implementation that stores to a user-defined file. Local file
41
 * storage is preferred over a certain operating system's monolithic trash heap
42
 * called a registry. When the OS is locked down, the default Preferences
43
 * implementation will try to write to the registry and fail due to permissions
44
 * problems. This class sidesteps the issue entirely by writing to the user's
45
 * home directory, where permissions should be a bit more lax.
46
 */
47
public class FilePreferences extends AbstractPreferences {
48
49
  private final Map<String, String> mRoot = new TreeMap<>();
50
  private final Map<String, FilePreferences> mChildren = new TreeMap<>();
51
  private boolean mRemoved;
52
53
  private final Object mMutex = new Object();
54
55
  public FilePreferences(
56
      final AbstractPreferences parent, final String name ) {
57
    super( parent, name );
58
59
    try {
60
      sync();
61
    } catch( final BackingStoreException ex ) {
62
      clue( ex );
63
    }
64
  }
65
66
  @Override
67
  protected void putSpi( final String key, final String value ) {
68
    synchronized( mMutex ) {
69
      mRoot.put( key, value );
70
    }
71
72
    try {
73
      flush();
74
    } catch( final BackingStoreException ex ) {
75
      clue( ex );
76
    }
77
  }
78
79
  @Override
80
  protected String getSpi( final String key ) {
81
    synchronized( mMutex ) {
82
      return mRoot.get( key );
83
    }
84
  }
85
86
  @Override
87
  protected void removeSpi( final String key ) {
88
    synchronized( mMutex ) {
89
      mRoot.remove( key );
90
    }
91
92
    try {
93
      flush();
94
    } catch( final BackingStoreException ex ) {
95
      clue( ex );
96
    }
97
  }
98
99
  @Override
100
  protected void removeNodeSpi() throws BackingStoreException {
101
    mRemoved = true;
102
    flush();
103
  }
104
105
  @Override
106
  protected String[] keysSpi() {
107
    synchronized( mMutex ) {
108
      return mRoot.keySet().toArray( new String[ 0 ] );
109
    }
110
  }
111
112
  @Override
113
  protected String[] childrenNamesSpi() {
114
    return mChildren.keySet().toArray( new String[ 0 ] );
115
  }
116
117
  @Override
118
  protected FilePreferences childSpi( final String name ) {
119
    FilePreferences child = mChildren.get( name );
120
121
    if( child == null || child.isRemoved() ) {
122
      child = new FilePreferences( this, name );
123
      mChildren.put( name, child );
124
    }
125
126
    return child;
127
  }
128
129
  @Override
130
  protected void syncSpi() {
131
    if( isRemoved() ) {
132
      return;
133
    }
134
135
    final File file = FilePreferencesFactory.getPreferencesFile();
136
137
    if( !file.exists() ) {
138
      return;
139
    }
140
141
    synchronized( mMutex ) {
142
      final Properties p = new Properties();
143
144
      try( final var inputStream = new FileInputStream( file ) ) {
145
        p.load( inputStream );
146
147
        final String path = getPath();
148
        final Enumeration<?> propertyNames = p.propertyNames();
149
150
        while( propertyNames.hasMoreElements() ) {
151
          final String propKey = (String) propertyNames.nextElement();
152
153
          if( propKey.startsWith( path ) ) {
154
            final String subKey = propKey.substring( path.length() );
155
156
            // Only load immediate descendants
157
            if( subKey.indexOf( '.' ) == -1 ) {
158
              mRoot.put( subKey, p.getProperty( propKey ) );
159
            }
160
          }
161
        }
162
      } catch( final Exception ex ) {
163
        clue( ex );
164
      }
165
    }
166
  }
167
168
  private String getPath() {
169
    final FilePreferences parent = (FilePreferences) parent();
170
171
    return parent == null ? "" : parent.getPath() + name() + '.';
172
  }
173
174
  @Override
175
  protected void flushSpi() {
176
    final File file = FilePreferencesFactory.getPreferencesFile();
177
178
    synchronized( mMutex ) {
179
      final Properties p = new Properties();
180
181
      try {
182
        final String path = getPath();
183
184
        if( file.exists() ) {
185
          try( final var fis = new FileInputStream( file ) ) {
186
            p.load( fis );
187
          }
188
189
          final List<String> toRemove = new ArrayList<>();
190
191
          // Make a list of all direct children of this node to be removed
192
          final Enumeration<?> propertyNames = p.propertyNames();
193
194
          while( propertyNames.hasMoreElements() ) {
195
            final String propKey = (String) propertyNames.nextElement();
196
            if( propKey.startsWith( path ) ) {
197
              final String subKey = propKey.substring( path.length() );
198
199
              // Only do immediate descendants
200
              if( subKey.indexOf( '.' ) == -1 ) {
201
                toRemove.add( propKey );
202
              }
203
            }
204
          }
205
206
          // Remove them now that the enumeration is done with
207
          for( final String propKey : toRemove ) {
208
            p.remove( propKey );
209
          }
210
        }
211
212
        // If this node hasn't been removed, add back in any values
213
        if( !mRemoved ) {
214
          for( final String s : mRoot.keySet() ) {
215
            p.setProperty( path + s, mRoot.get( s ) );
216
          }
217
        }
218
219
        try( final var fos = new FileOutputStream( file ) ) {
220
          p.store( fos, "FilePreferences" );
221
        }
222
      } catch( final Exception ex ) {
223
        clue( ex );
224
      }
225
    }
226
  }
227
}
2281
D src/main/java/com/keenwrite/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.keenwrite.preferences;
29
30
import java.io.File;
31
import java.util.prefs.Preferences;
32
import java.util.prefs.PreferencesFactory;
33
34
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
35
import static java.io.File.separator;
36
import static java.lang.System.getProperty;
37
38
/**
39
 * PreferencesFactory implementation that stores the preferences in a
40
 * user-defined file. Usage:
41
 * <pre>
42
 * System.setProperty( "java.util.prefs.PreferencesFactory",
43
 * FilePreferencesFactory.class.getName() );
44
 * </pre>
45
 */
46
public class FilePreferencesFactory implements PreferencesFactory {
47
48
  private static File sPreferencesFile;
49
  private Preferences rootPreferences;
50
51
  @Override
52
  public Preferences systemRoot() {
53
    return userRoot();
54
  }
55
56
  @Override
57
  public Preferences userRoot() {
58
    final var prefs = rootPreferences;
59
60
    if( prefs == null ) {
61
      rootPreferences = new FilePreferences( null, "" );
62
    }
63
64
    return rootPreferences;
65
  }
66
67
  public static File getPreferencesFile() {
68
    final var prefs = sPreferencesFile;
69
70
    if( prefs == null ) {
71
      sPreferencesFile = new File( getPreferencesFilename() ).getAbsoluteFile();
72
    }
73
74
    return sPreferencesFile;
75
  }
76
77
  public static String getPreferencesFilename() {
78
    final var filename = getProperty( "application.name", APP_TITLE_LOWERCASE );
79
    return getProperty( "user.home" ) + separator + '.' + filename;
80
  }
81
}
821
A src/main/java/com/keenwrite/preferences/FileProperty.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
import javafx.beans.property.SimpleObjectProperty;
5
6
import java.io.File;
7
8
public class FileProperty extends SimpleObjectProperty<File> {
9
  public FileProperty( final File file ) {
10
    super( file );
11
  }
12
13
  public void setValue( final String filename ) {
14
    setValue( new File( filename ) );
15
  }
16
}
117
A src/main/java/com/keenwrite/preferences/Key.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
/**
5
 * Responsible for creating a type hierarchy of preference storage keys.
6
 */
7
public class Key {
8
  private final Key mParent;
9
  private final String mName;
10
11
  private Key( final Key parent, final String name ) {
12
    mParent = parent;
13
    mName = name;
14
  }
15
16
  /**
17
   * Returns a new key with no parent.
18
   *
19
   * @param name The key name, never {@code null}.
20
   * @return The new {@link Key} instance with a name but no parent.
21
   */
22
  public static Key key( final String name ) {
23
    assert name != null && !name.isEmpty();
24
    return key( null, name );
25
  }
26
27
  /**
28
   * Returns a new key with a given parent.
29
   *
30
   * @param parent The parent of this {@link Key}, or {@code null} if this is
31
   *               the topmost key in the chain.
32
   * @param name   The key name, never {@code null}.
33
   * @return The new {@link Key} instance with a name and parent.
34
   */
35
  public static Key key( final Key parent, final String name ) {
36
    assert name != null && !name.isEmpty();
37
    return new Key( parent, name );
38
  }
39
40
  private Key parent() {
41
    return mParent;
42
  }
43
44
  private String name() {
45
    return mName;
46
  }
47
48
  /**
49
   * Returns a dot-separated path representing the key's name.
50
   *
51
   * @return The recursively derived dot-separated key name.
52
   */
53
  @Override
54
  public String toString() {
55
    return parent() == null ? name() : parent().toString() + '.' + name();
56
  }
57
}
158
A src/main/java/com/keenwrite/preferences/LocaleProperty.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
import javafx.beans.property.SimpleListProperty;
5
import javafx.beans.property.SimpleObjectProperty;
6
import javafx.collections.ObservableList;
7
8
import java.util.LinkedHashMap;
9
import java.util.Locale;
10
import java.util.Map;
11
import java.util.Objects;
12
13
import static com.keenwrite.Constants.LOCALE_DEFAULT;
14
import static java.util.Locale.forLanguageTag;
15
import static javafx.collections.FXCollections.observableArrayList;
16
17
public class LocaleProperty extends SimpleObjectProperty<String> {
18
19
  /**
20
   * Lists the locales having fonts that are supported by the application.
21
   * When the Markdown and preview CSS files are loaded, a general file is
22
   * first loaded, then a specific file is loaded according to the locale.
23
   * The specific file overrides font families so that different languages
24
   * may be presented.
25
   * <p>
26
   * Using an instance of {@link LinkedHashMap} preserves display order.
27
   * </p>
28
   * <p>
29
   * See
30
   * <a href="https://www.oracle.com/java/technologies/javase/jdk12locales.html">JDK 12 Locales</a>
31
   * for details.
32
   * </p>
33
   */
34
  private static final Map<String, Locale> sLocales = new LinkedHashMap<>();
35
36
  static {
37
    final String[] tags = {
38
      "en-Latn-AU",
39
      "en-Latn-CA",
40
      "en-Latn-GB",
41
      "en-Latn-NZ",
42
      "en-Latn-US",
43
      "en-Latn-ZA",
44
      "ja-Jpan-JP",
45
      "ko-Kore-KR",
46
      "zh-Hans-CN",
47
      "zh-Hans-SG",
48
      "zh-Hant-HK",
49
      "zh-Hant-TW",
50
    };
51
52
    for( final var tag : tags ) {
53
      final var locale = forLanguageTag( tag );
54
      sLocales.put( locale.getDisplayName(), locale );
55
    }
56
  }
57
58
  public LocaleProperty( final Locale locale ) {
59
    super( sanitize( locale ).getDisplayName() );
60
  }
61
62
  public static String parseLocale( final String languageTag ) {
63
    final var locale = forLanguageTag( languageTag );
64
    final var key = getKey( sLocales, locale );
65
    return key == null ? LOCALE_DEFAULT.getDisplayName() : key;
66
  }
67
68
  public static String toLanguageTag( final String displayName ) {
69
    return sLocales.getOrDefault( displayName, LOCALE_DEFAULT ).toLanguageTag();
70
  }
71
72
  public Locale toLocale() {
73
    return sLocales.getOrDefault( getValue(), LOCALE_DEFAULT );
74
  }
75
76
  private static Locale sanitize( final Locale locale ) {
77
    return locale == null || "und".equalsIgnoreCase( locale.toLanguageTag() )
78
      ? LOCALE_DEFAULT
79
      : locale;
80
  }
81
82
  public static ObservableList<String> localeListProperty() {
83
    return new SimpleListProperty<>( observableArrayList( sLocales.keySet() ) );
84
  }
85
86
  /**
87
   * Performs an O(n) search through the given map to find the key that is
88
   * mapped to the given value. A bi-directional map would be faster, but
89
   * also introduces additional dependencies. This doesn't need to be fast
90
   * because it happens once, at start up, and there aren't a lot of values.
91
   *
92
   * @param map   The map containing a key to find based on a value.
93
   * @param value The value to find within the map.
94
   * @param <K>   The type of key associated with a value.
95
   * @param <V>   The type of value associated with a key.
96
   * @return The key that corresponds to the given value, or {@code null} if
97
   * the key is not found.
98
   */
99
  @SuppressWarnings( "SameParameterValue" )
100
  private static <K, V> K getKey( final Map<K, V> map, final V value ) {
101
    for( final var entry : map.entrySet() ) {
102
      if( Objects.equals( value, entry.getValue() ) ) {
103
        return entry.getKey();
104
      }
105
    }
106
107
    return null;
108
  }
109
}
1110
A src/main/java/com/keenwrite/preferences/LocaleScripts.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
import java.util.Collections;
5
import java.util.HashMap;
6
import java.util.Locale;
7
import java.util.Map;
8
9
import static java.util.Arrays.asList;
10
11
/**
12
 * Responsible for adding an ISO 15924 alpha-4 script code to {@link Locale}
13
 * instances. This allows all {@link Locale} objects to produce language tags
14
 * using the same format.
15
 */
16
public class LocaleScripts {
17
  /**
18
   * ISO 15924 alpha-4 script code to represent Latin scripts.
19
   */
20
  private static final String SCRIPT_LATIN = "Latn";
21
22
  /**
23
   * This value is returned when a script hasn't been mapped for an instance of
24
   * {@link Locale}.
25
   */
26
  private static final Map<String, String> SCRIPT_DEFAULT = m( SCRIPT_LATIN );
27
28
  private static final Map<String, Map<String, String>> SCRIPTS =
29
    new HashMap<>();
30
31
  static {
32
    put( "en", m( "Latn" ) );
33
    put( "jp", m( "Jpan" ) );
34
    put( "ko", m( "Kore" ) );
35
    put( "zh", m( "Hant" ), m( "Hans", "CN", "MN", "MY", "SG" ) );
36
  }
37
38
  /**
39
   * Adds a script to a given {@link Locale} object. If the given {@link Locale}
40
   * already has a script, then it is returned unchanged.
41
   *
42
   * @param locale The {@link Locale} to update with its associated script.
43
   * @return The given {@link Locale} with a script included.
44
   */
45
  public static Locale withScript( Locale locale ) {
46
    assert locale != null;
47
48
    final var script = locale.getScript();
49
50
    if( script == null || script.isBlank() ) {
51
      final var builder = new Locale.Builder();
52
      builder.setLocale( locale );
53
      builder.setScript( getScript( locale ) );
54
      locale = builder.build();
55
    }
56
57
    return locale;
58
  }
59
60
  @SafeVarargs
61
  private static void put(
62
    final String language, final Map<String, String>... scripts ) {
63
    final var merged = new HashMap<String, String>();
64
    asList( scripts ).forEach( merged::putAll );
65
    SCRIPTS.put( language, merged );
66
  }
67
68
  /**
69
   * Returns the ISO 15924 alpha-4 script code for the given {@link Locale}.
70
   *
71
   * @param locale Language and country are used to find the script code.
72
   * @return The ISO code for the given locale, or {@link #SCRIPT_LATIN} if
73
   * no code has been mapped yet.
74
   */
75
  private static String getScript( final Locale locale ) {
76
    return SCRIPTS.getOrDefault( locale.getLanguage(), SCRIPT_DEFAULT )
77
                  .getOrDefault( locale.getCountry(), SCRIPT_LATIN );
78
  }
79
80
  /**
81
   * Helper method to instantiate a new {@link Map} having all keys referencing
82
   * the same value.
83
   *
84
   * @param v The value to associate with each key.
85
   * @param k The keys to associate with the given value.
86
   * @return A new {@link Map} with all keys referencing the same value.
87
   */
88
  private static Map<String, String> m( final String v, final String... k ) {
89
    final var map = new HashMap<String, String>();
90
    asList( k ).forEach( ( key ) -> map.put( key, v ) );
91
    return Collections.unmodifiableMap( map );
92
  }
93
94
  /**
95
   * Helper method to instantiate a new {@link Map} having an empty key
96
   * referencing the given value. This provides a default value so that
97
   * an unmapped country code can return a valid script code.
98
   *
99
   * @param v The value to associate with an empty key.
100
   * @return A new {@link Map} with the empty key referencing the given value.
101
   */
102
  private static Map<String, String> m( final String v ) {
103
    return m( v, "" );
104
  }
105
}
1106
A src/main/java/com/keenwrite/preferences/PreferencesController.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
import com.dlsc.formsfx.model.structure.StringField;
5
import com.dlsc.preferencesfx.PreferencesFx;
6
import com.dlsc.preferencesfx.PreferencesFxEvent;
7
import com.dlsc.preferencesfx.model.Category;
8
import com.dlsc.preferencesfx.model.Group;
9
import com.dlsc.preferencesfx.model.Setting;
10
import javafx.beans.property.DoubleProperty;
11
import javafx.beans.property.ObjectProperty;
12
import javafx.beans.property.StringProperty;
13
import javafx.event.EventHandler;
14
import javafx.scene.Node;
15
import javafx.scene.control.Label;
16
17
import java.io.File;
18
19
import static com.keenwrite.Constants.ICON_DIALOG;
20
import static com.keenwrite.Messages.get;
21
import static com.keenwrite.preferences.LocaleProperty.localeListProperty;
22
import static com.keenwrite.preferences.Workspace.*;
23
24
/**
25
 * Provides the ability for users to configure their preferences. This links
26
 * the {@link Workspace} model with the {@link PreferencesFx} view, in MVC.
27
 */
28
@SuppressWarnings( "SameParameterValue" )
29
public class PreferencesController {
30
31
  private final Workspace mWorkspace;
32
  private final PreferencesFx mPreferencesFx;
33
34
  public PreferencesController( final Workspace workspace ) {
35
    mWorkspace = workspace;
36
37
    // All properties must be initialized before creating the dialog.
38
    mPreferencesFx = createPreferencesFx();
39
  }
40
41
  /**
42
   * Display the user preferences settings dialog (non-modal).
43
   */
44
  public void show() {
45
    getPreferencesFx().show( false );
46
  }
47
48
  /**
49
   * Call to persist the settings. Strictly speaking, this could watch on
50
   * all values for external changes then save automatically.
51
   */
52
  public void save() {
53
    getPreferencesFx().saveSettings();
54
  }
55
56
  /**
57
   * Delegates to the {@link PreferencesFx} event handler for monitoring
58
   * save events.
59
   *
60
   * @param eventHandler The handler to call when the preferences are saved.
61
   */
62
  public void addSaveEventHandler(
63
    final EventHandler<? super PreferencesFxEvent> eventHandler ) {
64
    final var eventType = PreferencesFxEvent.EVENT_PREFERENCES_SAVED;
65
    getPreferencesFx().addEventHandler( eventType, eventHandler );
66
  }
67
68
  /**
69
   * Creates the preferences dialog.
70
   * <p>
71
   * TODO: Make this dynamic by iterating over all "Preferences.*" values
72
   * that follow a particular naming pattern.
73
   * </p>
74
   *
75
   * @return A new instance of preferences for users to edit.
76
   */
77
  @SuppressWarnings( "unchecked" )
78
  private PreferencesFx createPreferencesFx() {
79
    final Setting<StringField, StringProperty> scriptSetting =
80
      Setting.of( "Script", stringProperty( KEY_R_SCRIPT ) );
81
    final StringField field = scriptSetting.getElement();
82
    field.multiline( true );
83
84
    return PreferencesFx.of(
85
      new XmlStorageHandler(),
86
      Category.of(
87
        get( KEY_R ),
88
        Group.of(
89
          get( KEY_R_DIR ),
90
          Setting.of( label( KEY_R_DIR,
91
                             stringProperty( KEY_DEF_DELIM_BEGAN ).get(),
92
                             stringProperty( KEY_DEF_DELIM_ENDED ).get() ) ),
93
          Setting.of( title( KEY_R_DIR ), fileProperty( KEY_R_DIR ), true )
94
        ),
95
        Group.of(
96
          get( KEY_R_SCRIPT ),
97
          Setting.of( label( KEY_R_SCRIPT ) ),
98
          scriptSetting
99
        ),
100
        Group.of(
101
          get( KEY_R_DELIM_BEGAN ),
102
          Setting.of( label( KEY_R_DELIM_BEGAN ) ),
103
          Setting.of( title( KEY_R_DELIM_BEGAN ),
104
                      stringProperty( KEY_R_DELIM_BEGAN ) )
105
        ),
106
        Group.of(
107
          get( KEY_R_DELIM_ENDED ),
108
          Setting.of( label( KEY_R_DELIM_ENDED ) ),
109
          Setting.of( title( KEY_R_DELIM_ENDED ),
110
                      stringProperty( KEY_R_DELIM_ENDED ) )
111
        )
112
      ),
113
      Category.of(
114
        get( KEY_IMAGES ),
115
        Group.of(
116
          get( KEY_IMAGES_DIR ),
117
          Setting.of( label( KEY_IMAGES_DIR ) ),
118
          Setting.of( title( KEY_IMAGES_DIR ),
119
                      fileProperty( KEY_IMAGES_DIR ),
120
                      true )
121
        ),
122
        Group.of(
123
          get( KEY_IMAGES_ORDER ),
124
          Setting.of( label( KEY_IMAGES_ORDER ) ),
125
          Setting.of( title( KEY_IMAGES_ORDER ),
126
                      stringProperty( KEY_IMAGES_ORDER ) )
127
        )
128
      ),
129
      Category.of(
130
        get( KEY_DEF ),
131
        Group.of(
132
          get( KEY_DEF_PATH ),
133
          Setting.of( label( KEY_DEF_PATH ) ),
134
          Setting.of( title( KEY_DEF_PATH ),
135
                      fileProperty( KEY_DEF_PATH ),
136
                      false )
137
        ),
138
        Group.of(
139
          get( KEY_DEF_DELIM_BEGAN ),
140
          Setting.of( label( KEY_DEF_DELIM_BEGAN ) ),
141
          Setting.of( title( KEY_DEF_DELIM_BEGAN ),
142
                      stringProperty( KEY_DEF_DELIM_BEGAN ) )
143
        ),
144
        Group.of(
145
          get( KEY_DEF_DELIM_ENDED ),
146
          Setting.of( label( KEY_DEF_DELIM_ENDED ) ),
147
          Setting.of( title( KEY_DEF_DELIM_ENDED ),
148
                      stringProperty( KEY_DEF_DELIM_ENDED ) )
149
        )
150
      ),
151
      Category.of(
152
        get( KEY_UI_FONT ),
153
        Group.of(
154
          get( KEY_UI_FONT_PREVIEW_SIZE ),
155
          Setting.of( label( KEY_UI_FONT_PREVIEW_SIZE ) ),
156
          Setting.of( title( KEY_UI_FONT_PREVIEW_SIZE ),
157
                      doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) )
158
        ),
159
        Group.of(
160
          get( KEY_UI_FONT_EDITOR_SIZE ),
161
          Setting.of( label( KEY_UI_FONT_EDITOR_SIZE ) ),
162
          Setting.of( title( KEY_UI_FONT_EDITOR_SIZE ),
163
                      doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) )
164
        ),
165
        Group.of(
166
          get( KEY_UI_FONT_LOCALE ),
167
          Setting.of( label( KEY_UI_FONT_LOCALE ) ),
168
          Setting.of( title( KEY_UI_FONT_LOCALE ),
169
                      localeListProperty(),
170
                      localeProperty( KEY_UI_FONT_LOCALE ) )
171
        )
172
      )
173
    ).instantPersistent( false ).dialogIcon( ICON_DIALOG );
174
  }
175
176
  /**
177
   * Creates a label for the given key after interpolating its value.
178
   *
179
   * @param key The key to find in the resource bundle.
180
   * @return The value of the key as a label.
181
   */
182
  private Node label( final Key key ) {
183
    return label( key, (String[]) null );
184
  }
185
186
  private Node label( final Key key, final String... values ) {
187
    return new Label( get( key.toString() + ".desc", (Object[]) values ) );
188
  }
189
190
  private String title( final Key key ) {
191
    return get( key.toString() + ".title" );
192
  }
193
194
  private ObjectProperty<File> fileProperty( final Key key ) {
195
    return mWorkspace.fileProperty( key );
196
  }
197
198
  private StringProperty stringProperty( final Key key ) {
199
    return mWorkspace.stringProperty( key );
200
  }
201
202
  @SuppressWarnings( "SameParameterValue" )
203
  private DoubleProperty doubleProperty( final Key key ) {
204
    return mWorkspace.doubleProperty( key );
205
  }
206
207
  private ObjectProperty<String> localeProperty( final Key key ) {
208
    return mWorkspace.localeProperty( key );
209
  }
210
211
  private PreferencesFx getPreferencesFx() {
212
    return mPreferencesFx;
213
  }
214
}
1215
D src/main/java/com/keenwrite/preferences/UserPreferences.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.preferences;
29
30
import com.dlsc.formsfx.model.structure.StringField;
31
import com.dlsc.preferencesfx.PreferencesFx;
32
import com.dlsc.preferencesfx.PreferencesFxEvent;
33
import com.dlsc.preferencesfx.model.Category;
34
import com.dlsc.preferencesfx.model.Group;
35
import com.dlsc.preferencesfx.model.Setting;
36
import javafx.beans.property.*;
37
import javafx.event.EventHandler;
38
import javafx.scene.Node;
39
import javafx.scene.control.Label;
40
41
import java.io.File;
42
import java.nio.file.Path;
43
44
import static com.keenwrite.Constants.*;
45
import static com.keenwrite.Messages.get;
46
47
/**
48
 * Responsible for user preferences that can be changed from the GUI. The
49
 * settings are displayed and persisted using {@link PreferencesFx}.
50
 */
51
public class UserPreferences {
52
  /**
53
   * Implementation of the  initialization-on-demand holder design pattern,
54
   * an for a lazy-loaded singleton. In all versions of Java, the idiom enables
55
   * a safe, highly concurrent lazy initialization of static fields with good
56
   * performance. The implementation relies upon the initialization phase of
57
   * execution within the Java Virtual Machine (JVM) as specified by the Java
58
   * Language Specification. When the class {@link UserPreferencesContainer}
59
   * is loaded, its initialization completes trivially because there are no
60
   * static variables to initialize.
61
   * <p>
62
   * The static class definition {@link UserPreferencesContainer} within the
63
   * {@link UserPreferences} is not initialized until such time that
64
   * {@link UserPreferencesContainer} must be executed. The static
65
   * {@link UserPreferencesContainer} class executes when
66
   * {@link #getInstance} is called. The first call will trigger loading and
67
   * initialization of the {@link UserPreferencesContainer} thereby
68
   * instantiating the {@link #INSTANCE}.
69
   * </p>
70
   * <p>
71
   * This indirection is necessary because the {@link UserPreferences} class
72
   * references {@link PreferencesFx}, which must not be instantiated until the
73
   * UI is ready.
74
   * </p>
75
   */
76
  private static class UserPreferencesContainer {
77
    private static final UserPreferences INSTANCE = new UserPreferences();
78
  }
79
80
  public static UserPreferences getInstance() {
81
    return UserPreferencesContainer.INSTANCE;
82
  }
83
84
  private final PreferencesFx mPreferencesFx;
85
86
  private final ObjectProperty<File> mPropRDirectory;
87
  private final StringProperty mPropRScript;
88
  private final ObjectProperty<File> mPropImagesDirectory;
89
  private final StringProperty mPropImagesOrder;
90
  private final ObjectProperty<File> mPropDefinitionPath;
91
  private final StringProperty mPropRDelimBegan;
92
  private final StringProperty mPropRDelimEnded;
93
  private final StringProperty mPropDefDelimBegan;
94
  private final StringProperty mPropDefDelimEnded;
95
  private final IntegerProperty mPropFontsSizeEditor;
96
97
  private UserPreferences() {
98
    mPropRDirectory = simpleFile( USER_DIRECTORY );
99
    mPropRScript = new SimpleStringProperty( "" );
100
101
    mPropImagesDirectory = simpleFile( USER_DIRECTORY );
102
    mPropImagesOrder = new SimpleStringProperty( PERSIST_IMAGES_DEFAULT );
103
104
    mPropDefinitionPath = simpleFile(
105
        getSetting( "file.definition.default", DEFINITION_NAME )
106
    );
107
108
    mPropDefDelimBegan = new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT );
109
    mPropDefDelimEnded = new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT );
110
111
    mPropRDelimBegan = new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT );
112
    mPropRDelimEnded = new SimpleStringProperty( R_DELIM_ENDED_DEFAULT );
113
114
    mPropFontsSizeEditor = new SimpleIntegerProperty( (int) FONT_SIZE_EDITOR );
115
116
    // All properties must be initialized before creating the dialog.
117
    mPreferencesFx = createPreferencesFx();
118
  }
119
120
  /**
121
   * Display the user preferences settings dialog (non-modal).
122
   */
123
  public void show() {
124
    getPreferencesFx().show( false );
125
  }
126
127
  /**
128
   * Call to persist the settings. Strictly speaking, this could watch on
129
   * all values for external changes then save automatically.
130
   */
131
  public void save() {
132
    getPreferencesFx().saveSettings();
133
  }
134
135
  /**
136
   * Creates the preferences dialog.
137
   * <p>
138
   * TODO: Make this dynamic by iterating over all "Preferences.*" values
139
   * that follow a particular naming pattern.
140
   * </p>
141
   *
142
   * @return A new instance of preferences for users to edit.
143
   */
144
  @SuppressWarnings("unchecked")
145
  private PreferencesFx createPreferencesFx() {
146
    final Setting<StringField, StringProperty> scriptSetting =
147
        Setting.of( "Script", mPropRScript );
148
    final StringField field = scriptSetting.getElement();
149
    field.multiline( true );
150
151
    return PreferencesFx.of(
152
        UserPreferences.class,
153
        Category.of(
154
            get( "Preferences.r" ),
155
            Group.of(
156
                get( "Preferences.r.directory" ),
157
                Setting.of( label( "Preferences.r.directory.desc", false ) ),
158
                Setting.of( "Directory", mPropRDirectory, true )
159
            ),
160
            Group.of(
161
                get( "Preferences.r.script" ),
162
                Setting.of( label( "Preferences.r.script.desc" ) ),
163
                scriptSetting
164
            ),
165
            Group.of(
166
                get( "Preferences.r.delimiter.began" ),
167
                Setting.of( label( "Preferences.r.delimiter.began.desc" ) ),
168
                Setting.of( "Opening", mPropRDelimBegan )
169
            ),
170
            Group.of(
171
                get( "Preferences.r.delimiter.ended" ),
172
                Setting.of( label( "Preferences.r.delimiter.ended.desc" ) ),
173
                Setting.of( "Closing", mPropRDelimEnded )
174
            )
175
        ),
176
        Category.of(
177
            get( "Preferences.images" ),
178
            Group.of(
179
                get( "Preferences.images.directory" ),
180
                Setting.of( label( "Preferences.images.directory.desc" ) ),
181
                Setting.of( "Directory", mPropImagesDirectory, true )
182
            ),
183
            Group.of(
184
                get( "Preferences.images.suffixes" ),
185
                Setting.of( label( "Preferences.images.suffixes.desc" ) ),
186
                Setting.of( "Extensions", mPropImagesOrder )
187
            )
188
        ),
189
        Category.of(
190
            get( "Preferences.definitions" ),
191
            Group.of(
192
                get( "Preferences.definitions.path" ),
193
                Setting.of( label( "Preferences.definitions.path.desc" ) ),
194
                Setting.of( "Path", mPropDefinitionPath, false )
195
            ),
196
            Group.of(
197
                get( "Preferences.definitions.delimiter.began" ),
198
                Setting.of( label(
199
                    "Preferences.definitions.delimiter.began.desc" ) ),
200
                Setting.of( "Opening", mPropDefDelimBegan )
201
            ),
202
            Group.of(
203
                get( "Preferences.definitions.delimiter.ended" ),
204
                Setting.of( label(
205
                    "Preferences.definitions.delimiter.ended.desc" ) ),
206
                Setting.of( "Closing", mPropDefDelimEnded )
207
            )
208
        ),
209
        Category.of(
210
            get( "Preferences.fonts" ),
211
            Group.of(
212
                get( "Preferences.fonts.size_editor" ),
213
                Setting.of( label( "Preferences.fonts.size_editor.desc" ) ),
214
                Setting.of( "Points", mPropFontsSizeEditor )
215
            )
216
        )
217
    ).instantPersistent( false )
218
                        .dialogIcon( ICON_DIALOG );
219
  }
220
221
  /**
222
   * Wraps a {@link File} inside a {@link SimpleObjectProperty}.
223
   *
224
   * @param path The file name to use when constructing the {@link File}.
225
   * @return A new {@link SimpleObjectProperty} instance with a {@link File}
226
   * that references the given {@code path}.
227
   */
228
  private SimpleObjectProperty<File> simpleFile( final String path ) {
229
    return new SimpleObjectProperty<>( new File( path ) );
230
  }
231
232
  /**
233
   * Creates a label for the given key after interpolating its value.
234
   *
235
   * @param key The key to find in the resource bundle.
236
   * @return The value of the key as a label.
237
   */
238
  private Node label( final String key ) {
239
    return new Label( get( key, true ) );
240
  }
241
242
  /**
243
   * Creates a label for the given key.
244
   *
245
   * @param key         The key to find in the resource bundle.
246
   * @param interpolate {@code true} means to interpolate the value.
247
   * @return The value of the key, interpolated if {@code interpolate} is
248
   * {@code true}.
249
   */
250
  @SuppressWarnings("SameParameterValue")
251
  private Node label( final String key, final boolean interpolate ) {
252
    return new Label( get( key, interpolate ) );
253
  }
254
255
  /**
256
   * Delegates to the {@link PreferencesFx} event handler for monitoring
257
   * save events.
258
   *
259
   * @param eventHandler The handler to call when the preferences are saved.
260
   */
261
  public void addSaveEventHandler(
262
      final EventHandler<? super PreferencesFxEvent> eventHandler ) {
263
    final var eventType = PreferencesFxEvent.EVENT_PREFERENCES_SAVED;
264
    getPreferencesFx().addEventHandler( eventType, eventHandler );
265
  }
266
267
  /**
268
   * Returns the value for a key from the settings properties file.
269
   *
270
   * @param key   Key within the settings properties file to find.
271
   * @param value Default value to return if the key is not found.
272
   * @return The value for the given key from the settings file, or the
273
   * given {@code value} if no key found.
274
   */
275
  @SuppressWarnings("SameParameterValue")
276
  private String getSetting( final String key, final String value ) {
277
    return SETTINGS.getSetting( key, value );
278
  }
279
280
  public ObjectProperty<File> definitionPathProperty() {
281
    return mPropDefinitionPath;
282
  }
283
284
  public Path getDefinitionPath() {
285
    return definitionPathProperty().getValue().toPath();
286
  }
287
288
  private StringProperty defDelimiterBegan() {
289
    return mPropDefDelimBegan;
290
  }
291
292
  public String getDefDelimiterBegan() {
293
    return defDelimiterBegan().get();
294
  }
295
296
  private StringProperty defDelimiterEnded() {
297
    return mPropDefDelimEnded;
298
  }
299
300
  public String getDefDelimiterEnded() {
301
    return defDelimiterEnded().get();
302
  }
303
304
  public ObjectProperty<File> rDirectoryProperty() {
305
    return mPropRDirectory;
306
  }
307
308
  public File getRDirectory() {
309
    return rDirectoryProperty().getValue();
310
  }
311
312
  public StringProperty rScriptProperty() {
313
    return mPropRScript;
314
  }
315
316
  public String getRScript() {
317
    return rScriptProperty().getValue();
318
  }
319
320
  private StringProperty rDelimiterBegan() {
321
    return mPropRDelimBegan;
322
  }
323
324
  public String getRDelimiterBegan() {
325
    return rDelimiterBegan().get();
326
  }
327
328
  private StringProperty rDelimiterEnded() {
329
    return mPropRDelimEnded;
330
  }
331
332
  public String getRDelimiterEnded() {
333
    return rDelimiterEnded().get();
334
  }
335
336
  private ObjectProperty<File> imagesDirectoryProperty() {
337
    return mPropImagesDirectory;
338
  }
339
340
  public File getImagesDirectory() {
341
    return imagesDirectoryProperty().getValue();
342
  }
343
344
  private StringProperty imagesOrderProperty() {
345
    return mPropImagesOrder;
346
  }
347
348
  public String getImagesOrder() {
349
    return imagesOrderProperty().getValue();
350
  }
351
352
  public IntegerProperty fontsSizeEditorProperty() {
353
    return mPropFontsSizeEditor;
354
  }
355
356
  /**
357
   * Returns the preferred font size of the text editor.
358
   *
359
   * @return A non-negative integer, in points.
360
   */
361
  public int getFontsSizeEditor() {
362
    return mPropFontsSizeEditor.intValue();
363
  }
364
365
  private PreferencesFx getPreferencesFx() {
366
    return mPreferencesFx;
367
  }
368
}
3691
A src/main/java/com/keenwrite/preferences/Workspace.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
import com.keenwrite.Constants;
5
import com.keenwrite.sigils.Tokens;
6
import javafx.application.Platform;
7
import javafx.beans.property.*;
8
import org.apache.commons.configuration2.XMLConfiguration;
9
import org.apache.commons.configuration2.builder.fluent.Configurations;
10
import org.apache.commons.configuration2.io.FileHandler;
11
12
import java.io.File;
13
import java.util.HashSet;
14
import java.util.LinkedHashSet;
15
import java.util.Map;
16
import java.util.Set;
17
import java.util.function.BiConsumer;
18
import java.util.function.BooleanSupplier;
19
import java.util.function.Consumer;
20
import java.util.function.Function;
21
22
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
23
import static com.keenwrite.Constants.*;
24
import static com.keenwrite.Launcher.getVersion;
25
import static com.keenwrite.StatusBarNotifier.clue;
26
import static com.keenwrite.preferences.Key.key;
27
import static java.util.Map.entry;
28
import static javafx.application.Platform.runLater;
29
import static javafx.collections.FXCollections.observableSet;
30
31
/**
32
 * Responsible for defining behaviours for separate projects. A workspace has
33
 * the ability to save and restore a session, including the window dimensions,
34
 * tab setup, files, and user preferences.
35
 * <p>
36
 * The configuration must support hierarchical (nested) configuration nodes
37
 * to persist the user interface state. Although possible with a flat
38
 * configuration file, it's not nearly as simple or elegant.
39
 * </p>
40
 * <p>
41
 * Neither JSON nor HOCON support schema validation and versioning, which makes
42
 * XML the more suitable configuration file format. Schema validation and
43
 * versioning provide future-proofing and ease of reading and upgrading previous
44
 * versions of the configuration file.
45
 * </p>
46
 * <p>
47
 * Persistent preferences may be set directly by the user or indirectly by
48
 * the act of using the application.
49
 * </p>
50
 * <p>
51
 * Note the following definitions:
52
 * </p>
53
 * <dl>
54
 *   <dt>File</dt>
55
 *   <dd>References a file name (no path), path, or directory.</dd>
56
 *   <dt>Path</dt>
57
 *   <dd>Fully qualified file name, which includes all parent directories.</dd>
58
 *   <dt>Dir</dt>
59
 *   <dd>Directory without a file name ({@link File#isDirectory()} is true)
60
 *   .</dd>
61
 * </dl>
62
 */
63
public class Workspace {
64
  private static final Key KEY_ROOT = key( "workspace" );
65
66
  public static final Key KEY_META = key( KEY_ROOT, "meta" );
67
  public static final Key KEY_META_NAME = key( KEY_META, "name" );
68
  public static final Key KEY_META_VERSION = key( KEY_META, "version" );
69
70
  public static final Key KEY_R = key( KEY_ROOT, "r" );
71
  public static final Key KEY_R_SCRIPT = key( KEY_R, "script" );
72
  public static final Key KEY_R_DIR = key( KEY_R, "dir" );
73
  public static final Key KEY_R_DELIM = key( KEY_R, "delimiter" );
74
  public static final Key KEY_R_DELIM_BEGAN = key( KEY_R_DELIM, "began" );
75
  public static final Key KEY_R_DELIM_ENDED = key( KEY_R_DELIM, "ended" );
76
77
  public static final Key KEY_IMAGES = key( KEY_ROOT, "images" );
78
  public static final Key KEY_IMAGES_DIR = key( KEY_IMAGES, "dir" );
79
  public static final Key KEY_IMAGES_ORDER = key( KEY_IMAGES, "order" );
80
81
  public static final Key KEY_DEF = key( KEY_ROOT, "definition" );
82
  public static final Key KEY_DEF_PATH = key( KEY_DEF, "path" );
83
  public static final Key KEY_DEF_DELIM = key( KEY_DEF, "delimiter" );
84
  public static final Key KEY_DEF_DELIM_BEGAN = key( KEY_DEF_DELIM, "began" );
85
  public static final Key KEY_DEF_DELIM_ENDED = key( KEY_DEF_DELIM, "ended" );
86
87
  //@formatter:off
88
  public static final Key KEY_UI = key( KEY_ROOT, "ui" );
89
90
  public static final Key KEY_UI_RECENT = key( KEY_UI, "recent" );
91
  public static final Key KEY_UI_RECENT_DIR = key( KEY_UI_RECENT, "dir" );
92
  public static final Key KEY_UI_RECENT_DOCUMENT = key( KEY_UI_RECENT,"document" );
93
  public static final Key KEY_UI_RECENT_DEFINITION = key( KEY_UI_RECENT, "definition" );
94
95
  public static final Key KEY_UI_FILES = key( KEY_UI, "files" );
96
  public static final Key KEY_UI_FILES_PATH = key( KEY_UI_FILES, "path" );
97
98
  public static final Key KEY_UI_FONT = key( KEY_UI, "font" );
99
  public static final Key KEY_UI_FONT_LOCALE = key( KEY_UI_FONT, "locale" );
100
  public static final Key KEY_UI_FONT_EDITOR = key( KEY_UI_FONT, "editor" );
101
  public static final Key KEY_UI_FONT_EDITOR_SIZE = key( KEY_UI_FONT_EDITOR, "size" );
102
  public static final Key KEY_UI_FONT_PREVIEW = key( KEY_UI_FONT, "preview" );
103
  public static final Key KEY_UI_FONT_PREVIEW_SIZE = key( KEY_UI_FONT_PREVIEW, "size" );
104
105
  public static final Key KEY_UI_WINDOW = key( KEY_UI, "window" );
106
  public static final Key KEY_UI_WINDOW_X = key( KEY_UI_WINDOW, "x" );
107
  public static final Key KEY_UI_WINDOW_Y = key( KEY_UI_WINDOW, "y" );
108
  public static final Key KEY_UI_WINDOW_W = key( KEY_UI_WINDOW, "width" );
109
  public static final Key KEY_UI_WINDOW_H = key( KEY_UI_WINDOW, "height" );
110
  public static final Key KEY_UI_WINDOW_MAX = key( KEY_UI_WINDOW, "maximized" );
111
  public static final Key KEY_UI_WINDOW_FULL = key( KEY_UI_WINDOW, "full" );
112
113
  private final Map<Key, Property<?>> VALUES = Map.ofEntries(
114
    entry( KEY_META_VERSION, new SimpleStringProperty( getVersion() ) ),
115
    entry( KEY_META_NAME, new SimpleStringProperty( "default" ) ),
116
    
117
    entry( KEY_R_SCRIPT, new SimpleStringProperty( "" ) ),
118
    entry( KEY_R_DIR, new FileProperty( USER_DIRECTORY ) ),
119
    entry( KEY_R_DELIM_BEGAN, new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT ) ),
120
    entry( KEY_R_DELIM_ENDED, new SimpleStringProperty( R_DELIM_ENDED_DEFAULT ) ),
121
    
122
    entry( KEY_IMAGES_DIR, new FileProperty( USER_DIRECTORY ) ),
123
    entry( KEY_IMAGES_ORDER, new SimpleStringProperty( PERSIST_IMAGES_DEFAULT ) ),
124
    
125
    entry( KEY_DEF_PATH, new FileProperty( DEFINITION_DEFAULT ) ),
126
    entry( KEY_DEF_DELIM_BEGAN, new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT ) ),
127
    entry( KEY_DEF_DELIM_ENDED, new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT ) ),
128
    
129
    entry( KEY_UI_RECENT_DIR, new FileProperty( USER_DIRECTORY ) ),
130
    entry( KEY_UI_RECENT_DOCUMENT, new FileProperty( DOCUMENT_DEFAULT ) ),
131
    entry( KEY_UI_RECENT_DEFINITION, new FileProperty( DEFINITION_DEFAULT ) ),
132
    
133
    entry( KEY_UI_FONT_LOCALE, new LocaleProperty( LOCALE_DEFAULT ) ),
134
    entry( KEY_UI_FONT_EDITOR_SIZE, new SimpleDoubleProperty( FONT_SIZE_EDITOR_DEFAULT ) ),
135
    entry( KEY_UI_FONT_PREVIEW_SIZE, new SimpleDoubleProperty( FONT_SIZE_PREVIEW_DEFAULT ) ),
136
    
137
    entry( KEY_UI_WINDOW_X, new SimpleDoubleProperty( WINDOW_X_DEFAULT ) ),
138
    entry( KEY_UI_WINDOW_Y, new SimpleDoubleProperty( WINDOW_Y_DEFAULT ) ),
139
    entry( KEY_UI_WINDOW_W, new SimpleDoubleProperty( WINDOW_W_DEFAULT ) ),
140
    entry( KEY_UI_WINDOW_H, new SimpleDoubleProperty( WINDOW_H_DEFAULT ) ),
141
    entry( KEY_UI_WINDOW_MAX, new SimpleBooleanProperty() ),
142
    entry( KEY_UI_WINDOW_FULL, new SimpleBooleanProperty() )
143
  );
144
  //@formatter:on
145
146
  /**
147
   * Helps instantiate {@link Property} instances for XML configuration items.
148
   */
149
  private static final Map<Class<?>, Function<String, Object>> UNMARSHALL =
150
    Map.of(
151
      LocaleProperty.class, LocaleProperty::parseLocale,
152
      SimpleBooleanProperty.class, Boolean::parseBoolean,
153
      SimpleDoubleProperty.class, Double::parseDouble,
154
      SimpleFloatProperty.class, Float::parseFloat,
155
      FileProperty.class, File::new
156
    );
157
158
  private static final Map<Class<?>, Function<String, Object>> MARSHALL =
159
    Map.of(
160
      LocaleProperty.class, LocaleProperty::toLanguageTag
161
    );
162
163
  private final Map<Key, SetProperty<?>> SETS = Map.ofEntries(
164
    entry(
165
      KEY_UI_FILES_PATH,
166
      new SimpleSetProperty<>( observableSet( new HashSet<>() ) )
167
    )
168
  );
169
170
  /**
171
   * Creates a new {@link Workspace} that will attempt to load a configuration
172
   * file. If the configuration file cannot be loaded, the workspace settings
173
   * will return default values. This allows unit tests to provide an instance
174
   * of {@link Workspace} when necessary without encountering failures.
175
   */
176
  public Workspace() {
177
    load();
178
  }
179
180
  /**
181
   * Returns a value that represents a setting in the application that the user
182
   * may configure, either directly or indirectly.
183
   *
184
   * @param key The reference to the users' preference stored in deference
185
   *            of app reëntrance.
186
   * @return An observable property to be persisted.
187
   */
188
  @SuppressWarnings( "unchecked" )
189
  public <T, U extends Property<T>> U valuesProperty( final Key key ) {
190
    // The type that goes into the map must come out.
191
    return (U) VALUES.get( key );
192
  }
193
194
  /**
195
   * Returns a list of values that represent a setting in the application that
196
   * the user may configure, either directly or indirectly. The property
197
   * returned is backed by a mutable {@link Set}.
198
   *
199
   * @param key The {@link Key} associated with a preference value.
200
   * @return An observable property to be persisted.
201
   */
202
  @SuppressWarnings( "unchecked" )
203
  public <T> SetProperty<T> setsProperty( final Key key ) {
204
    // The type that goes into the map must come out.
205
    return (SetProperty<T>) SETS.get( key );
206
  }
207
208
  /**
209
   * Returns the {@link Boolean} preference value associated with the given
210
   * {@link Key}. The caller must be sure that the given {@link Key} is
211
   * associated with a value that matches the return type.
212
   *
213
   * @param key The {@link Key} associated with a preference value.
214
   * @return The value associated with the given {@link Key}.
215
   */
216
  public boolean toBoolean( final Key key ) {
217
    return (Boolean) valuesProperty( key ).getValue();
218
  }
219
220
  /**
221
   * Returns the {@link Double} preference value associated with the given
222
   * {@link Key}. The caller must be sure that the given {@link Key} is
223
   * associated with a value that matches the return type.
224
   *
225
   * @param key The {@link Key} associated with a preference value.
226
   * @return The value associated with the given {@link Key}.
227
   */
228
  public double toDouble( final Key key ) {
229
    return (Double) valuesProperty( key ).getValue();
230
  }
231
232
  public File toFile( final Key key ) {
233
    return fileProperty( key ).get();
234
  }
235
236
  public String toString( final Key key ) {
237
    return stringProperty( key ).get();
238
  }
239
240
  public Tokens toTokens( final Key began, final Key ended ) {
241
    return new Tokens( stringProperty( began ), stringProperty( ended ) );
242
  }
243
244
  @SuppressWarnings( "SameParameterValue" )
245
  public DoubleProperty doubleProperty( final Key key ) {
246
    return valuesProperty( key );
247
  }
248
249
  /**
250
   * Returns the {@link File} {@link Property} associated with the given
251
   * {@link Key} from the internal list of preference values. The caller
252
   * must be sure that the given {@link Key} is associated with a {@link File}
253
   * {@link Property}.
254
   *
255
   * @param key The {@link Key} associated with a preference value.
256
   * @return The value associated with the given {@link Key}.
257
   */
258
  public ObjectProperty<File> fileProperty( final Key key ) {
259
    return valuesProperty( key );
260
  }
261
262
  public LocaleProperty localeProperty( final Key key ) {
263
    return valuesProperty( key );
264
  }
265
266
  public StringProperty stringProperty( final Key key ) {
267
    return valuesProperty( key );
268
  }
269
270
  public void loadValueKeys( final Consumer<Key> consumer ) {
271
    VALUES.keySet().forEach( consumer );
272
  }
273
274
  public void loadSetKeys( final Consumer<Key> consumer ) {
275
    SETS.keySet().forEach( consumer );
276
  }
277
278
  /**
279
   * Calls the given consumer for all single-value keys. For lists, see
280
   * {@link #saveSets(BiConsumer)}.
281
   *
282
   * @param consumer Called to accept each preference key value.
283
   */
284
  public void saveValues( final BiConsumer<Key, Property<?>> consumer ) {
285
    VALUES.forEach( consumer );
286
  }
287
288
  /**
289
   * Calls the given consumer for all multi-value keys. For single items, see
290
   * {@link #saveValues(BiConsumer)}. Callers are responsible for iterating
291
   * over the list of items retrieved through this method.
292
   *
293
   * @param consumer Called to accept each preference key list.
294
   */
295
  public void saveSets( final BiConsumer<Key, SetProperty<?>> consumer ) {
296
    SETS.forEach( consumer );
297
  }
298
299
  /**
300
   * Delegates to {@link #listen(Key, ReadOnlyProperty, BooleanSupplier)},
301
   * providing a value of {@code true} for the {@link BooleanSupplier} to
302
   * indicate the property changes always take effect.
303
   *
304
   * @param key      The value to bind to the internal key property.
305
   * @param property The external property value that sets the internal value.
306
   */
307
  public <T> void listen( final Key key, final ReadOnlyProperty<T> property ) {
308
    listen( key, property, () -> true );
309
  }
310
311
  /**
312
   * Binds a read-only property to a value in the preferences. This allows
313
   * user interface properties to change and the preferences will be
314
   * synchronized automatically.
315
   * <p>
316
   * This calls {@link Platform#runLater(Runnable)} to ensure that all pending
317
   * application window states are finished before assessing whether property
318
   * changes should be applied. Without this, exiting the application while the
319
   * window is maximized would persist the window's maximum dimensions,
320
   * preventing restoration to its prior, non-maximum size.
321
   * </p>
322
   *
323
   * @param key      The value to bind to the internal key property.
324
   * @param property The external property value that sets the internal value.
325
   * @param enabled  Indicates whether property changes should be applied.
326
   */
327
  public <T> void listen(
328
    final Key key,
329
    final ReadOnlyProperty<T> property,
330
    final BooleanSupplier enabled ) {
331
    property.addListener(
332
      ( c, o, n ) -> runLater( () -> {
333
        if( enabled.getAsBoolean() ) {
334
          valuesProperty( key ).setValue( n );
335
        }
336
      } )
337
    );
338
  }
339
340
  /**
341
   * Saves the current workspace.
342
   */
343
  public void save() {
344
    try {
345
      final var config = new XMLConfiguration();
346
347
      // The root config key can only be set for an empty configuration file.
348
      config.setRootElementName( APP_TITLE_LOWERCASE );
349
350
      saveValues( ( key, property ) ->
351
                    config.setProperty( key.toString(), marshall( property ) )
352
      );
353
354
      saveSets(
355
        ( key, set ) -> {
356
          final var keyName = key.toString();
357
          set.forEach( ( value ) -> config.addProperty( keyName, value ) );
358
        }
359
      );
360
      new FileHandler( config ).save( FILE_PREFERENCES );
361
    } catch( final Exception ex ) {
362
      clue( ex );
363
    }
364
  }
365
366
  /**
367
   * Attempts to load the {@link Constants#FILE_PREFERENCES} configuration file.
368
   * If not found, this will fall back to an empty configuration file, leaving
369
   * the application to fill in default values.
370
   */
371
  private void load() {
372
    try {
373
      final var config = new Configurations().xml( FILE_PREFERENCES );
374
375
      loadValueKeys( ( key ) -> {
376
        final var configValue = config.getProperty( key.toString() );
377
        final var propertyValue = valuesProperty( key );
378
        propertyValue.setValue( unmarshall( propertyValue, configValue ) );
379
      } );
380
381
      loadSetKeys( ( key ) -> {
382
        final var configSet =
383
          new LinkedHashSet<>( config.getList( key.toString() ) );
384
        final var propertySet = setsProperty( key );
385
        propertySet.setValue( observableSet( configSet ) );
386
      } );
387
    } catch( final Exception ex ) {
388
      clue( ex );
389
    }
390
  }
391
392
  private Object unmarshall(
393
    final Property<?> property, final Object configValue ) {
394
    return UNMARSHALL
395
      .getOrDefault( property.getClass(), ( value ) -> value )
396
      .apply( configValue.toString() );
397
  }
398
399
  private Object marshall( final Property<?> property ) {
400
    return MARSHALL
401
      .getOrDefault( property.getClass(), ( v ) -> property.getValue() )
402
      .apply( property.getValue().toString() );
403
  }
404
}
1405
A src/main/java/com/keenwrite/preferences/XmlStorageHandler.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
import com.dlsc.preferencesfx.PreferencesFx;
5
import com.dlsc.preferencesfx.util.StorageHandler;
6
import javafx.collections.ObservableList;
7
8
import java.util.prefs.Preferences;
9
10
/**
11
 * Prevents {@link PreferencesFx} from saving. Saving and loading preferences
12
 * and application window state is accomplished by the {@link Workspace}.
13
 * <p>
14
 * This implies that undo/redo functionality must be disabled because the
15
 * {@link Workspace} does not preserve previous states.
16
 * </p>
17
 */
18
public class XmlStorageHandler implements StorageHandler {
19
  @Override
20
  public void saveSelectedCategory( final String breadcrumb ) { }
21
22
  @Override
23
  public String loadSelectedCategory() {
24
    return "";
25
  }
26
27
  @Override
28
  public void saveDividerPosition( final double dividerPosition ) {
29
  }
30
31
  @Override
32
  public double loadDividerPosition() {
33
    return 0;
34
  }
35
36
  @Override
37
  public void saveWindowWidth( final double windowWidth ) { }
38
39
  @Override
40
  public double loadWindowWidth() {
41
    return 0;
42
  }
43
44
  @Override
45
  public void saveWindowHeight( final double windowHeight ) { }
46
47
  @Override
48
  public double loadWindowHeight() {
49
    return 0;
50
  }
51
52
  @Override
53
  public void saveWindowPosX( final double windowPosX ) { }
54
55
  @Override
56
  public double loadWindowPosX() {
57
    return 0;
58
  }
59
60
  @Override
61
  public void saveWindowPosY( final double windowPosY ) { }
62
63
  @Override
64
  public double loadWindowPosY() {
65
    return 0;
66
  }
67
68
  @Override
69
  public void saveObject( final String breadcrumb, final Object object ) { }
70
71
  @Override
72
  public Object loadObject(
73
    final String breadcrumb, final Object defaultObject ) {
74
    return defaultObject;
75
  }
76
77
  @Override
78
  public <T> T loadObject(
79
    final String breadcrumb, final Class<T> type, final T defaultObject ) {
80
    return defaultObject;
81
  }
82
83
  @Override
84
  @SuppressWarnings("rawtypes")
85
  public ObservableList loadObservableList(
86
    final String breadcrumb, final ObservableList defaultObservableList ) {
87
    return defaultObservableList;
88
  }
89
90
  @Override
91
  public <T> ObservableList<T> loadObservableList(
92
    final String breadcrumb,
93
    final Class<T> type,
94
    final ObservableList<T> defaultObservableList ) {
95
    return defaultObservableList;
96
  }
97
98
  @Override
99
  public boolean clearPreferences() {
100
    return false;
101
  }
102
103
  @Override
104
  public Preferences getPreferences() {
105
    return null;
106
  }
107
}
1108
M src/main/java/com/keenwrite/preview/ChainedReplacedElementFactory.java
1
/*
2
 * Copyright 2006 Patrick Wright
1
/* Copyright 2006 Patrick Wright
32
 * Copyright 2007 Wisconsin Court System
43
 * Copyright 2020 White Magic Software, Ltd.
...
2019
package com.keenwrite.preview;
2120
22
import com.keenwrite.adapters.ReplacedElementAdapter;
21
import com.keenwrite.ui.adapters.ReplacedElementAdapter;
22
import com.keenwrite.util.BoundedCache;
2323
import org.w3c.dom.Element;
2424
import org.xhtmlrenderer.extend.ReplacedElement;
2525
import org.xhtmlrenderer.extend.ReplacedElementFactory;
2626
import org.xhtmlrenderer.extend.UserAgentCallback;
2727
import org.xhtmlrenderer.layout.LayoutContext;
2828
import org.xhtmlrenderer.render.BlockBox;
2929
30
import java.util.HashSet;
30
import java.util.LinkedHashSet;
31
import java.util.Map;
3132
import java.util.Set;
33
34
import static com.keenwrite.preview.SvgReplacedElementFactory.HTML_IMAGE;
35
import static com.keenwrite.preview.SvgReplacedElementFactory.HTML_IMAGE_SRC;
36
import static com.keenwrite.processors.markdown.tex.TexNode.HTML_TEX;
37
import static java.util.Arrays.asList;
3238
39
/**
40
 * Responsible for running one or more factories to perform post-processing on
41
 * the HTML document prior to displaying it.
42
 */
3343
public class ChainedReplacedElementFactory extends ReplacedElementAdapter {
34
  private final Set<ReplacedElementFactory> mFactoryList = new HashSet<>();
44
  /**
45
   * Retain insertion order so that client classes can control the order that
46
   * factories are used to resolve images.
47
   */
48
  private final Set<ReplacedElementFactory> mFactories = new LinkedHashSet<>();
49
50
  /**
51
   * A bounded cache that removes the oldest image if the maximum number of
52
   * cached images has been reached. This constrains the number of images
53
   * loaded into memory.
54
   */
55
  private final Map<String, ReplacedElement> mCache = new BoundedCache<>( 150 );
56
57
  public ChainedReplacedElementFactory(
58
    final ReplacedElementFactory... factories ) {
59
    mFactories.addAll( asList( factories ) );
60
  }
3561
3662
  @Override
3763
  public ReplacedElement createReplacedElement(
38
      final LayoutContext c,
39
      final BlockBox box,
40
      final UserAgentCallback uac,
41
      final int cssWidth,
42
      final int cssHeight ) {
43
    for( final var f : mFactoryList ) {
44
      final var r = f.createReplacedElement(
45
          c, box, uac, cssWidth, cssHeight );
64
    final LayoutContext c,
65
    final BlockBox box,
66
    final UserAgentCallback uac,
67
    final int width,
68
    final int height ) {
69
    for( final var f : mFactories ) {
70
      final var e = box.getElement();
4671
47
      if( r != null ) {
48
        return r;
72
      // Exit early for super-speed.
73
      if( e == null ) {
74
        break;
75
      }
76
77
      // If the source image is cached, don't bother fetching. This optimization
78
      // avoids making multiple HTTP requests for the same URI.
79
      final var node = e.getNodeName();
80
      final var source = switch( node ) {
81
        case HTML_IMAGE -> e.getAttribute( HTML_IMAGE_SRC );
82
        case HTML_TEX -> e.getTextContent();
83
        default -> "";
84
      };
85
86
      // HTML <img> or <tex> elements without source data shall not pass.
87
      if( source.isBlank() ) {
88
        break;
89
      }
90
91
      final var replaced = mCache.computeIfAbsent(
92
        source, k -> f.createReplacedElement( c, box, uac, width, height )
93
      );
94
95
      if( replaced != null ) {
96
        return replaced;
4997
      }
5098
    }
5199
52100
    return null;
53101
  }
54102
55103
  @Override
56104
  public void reset() {
57
    for( final var factory : mFactoryList ) {
105
    for( final var factory : mFactories ) {
58106
      factory.reset();
59107
    }
60108
  }
61109
62110
  @Override
63111
  public void remove( final Element element ) {
64
    for( final var factory : mFactoryList ) {
112
    for( final var factory : mFactories ) {
65113
      factory.remove( element );
66114
    }
67115
  }
68116
69117
  public void addFactory( final ReplacedElementFactory factory ) {
70
    mFactoryList.add( factory );
118
    mFactories.add( factory );
119
  }
120
121
  public void clearCache() {
122
    mCache.clear();
71123
  }
72124
}
D src/main/java/com/keenwrite/preview/CustomImageLoader.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.preview;
29
30
import com.keenwrite.exceptions.MissingFileException;
31
import javafx.beans.property.IntegerProperty;
32
import javafx.beans.property.SimpleIntegerProperty;
33
import org.xhtmlrenderer.extend.FSImage;
34
import org.xhtmlrenderer.resource.ImageResource;
35
import org.xhtmlrenderer.swing.ImageResourceLoader;
36
37
import javax.imageio.ImageIO;
38
import java.net.URI;
39
import java.net.URL;
40
import java.nio.file.Paths;
41
42
import static com.keenwrite.StatusBarNotifier.clue;
43
import static com.keenwrite.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER;
44
import static com.keenwrite.util.ProtocolResolver.getProtocol;
45
import static java.lang.String.valueOf;
46
import static java.nio.file.Files.exists;
47
import static org.xhtmlrenderer.swing.AWTFSImage.createImage;
48
49
/**
50
 * Responsible for loading images. If the image cannot be found, a placeholder
51
 * is used instead.
52
 */
53
public class CustomImageLoader extends ImageResourceLoader {
54
  /**
55
   * Placeholder that's displayed when image cannot be found.
56
   */
57
  private FSImage mBrokenImage;
58
59
  private final IntegerProperty mWidthProperty = new SimpleIntegerProperty();
60
61
  /**
62
   * Gets an {@link IntegerProperty} that represents the maximum width an
63
   * image should be scaled.
64
   *
65
   * @return The maximum width for an image.
66
   */
67
  public IntegerProperty widthProperty() {
68
    return mWidthProperty;
69
  }
70
71
  /**
72
   * Gets an image resolved from the given URI. If the image cannot be found,
73
   * this will return a custom placeholder image indicating the reference
74
   * is broken.
75
   *
76
   * @param uri    Path to the image resource to load.
77
   * @param width  Ignored.
78
   * @param height Ignored.
79
   * @return The scaled image, or a placeholder image if the URI's content
80
   * could not be retrieved.
81
   */
82
  @Override
83
  public synchronized ImageResource get(
84
      final String uri, final int width, final int height ) {
85
    assert uri != null;
86
    assert width >= 0;
87
    assert height >= 0;
88
89
    try {
90
      final var protocol = getProtocol( uri );
91
      final ImageResource imageResource;
92
93
      if( protocol.isFile() ) {
94
        if( exists( Paths.get( new URI( uri ) ) ) ) {
95
          imageResource = super.get( uri, width, height );
96
        }
97
        else {
98
          throw new MissingFileException( uri );
99
        }
100
      }
101
      else if( protocol.isHttp() ) {
102
        // FlyingSaucer will silently swallow any images that fail to load.
103
        // Consequently, the following lines load the resource over HTTP and
104
        // translate errors into a broken image icon.
105
        final var url = new URL( uri );
106
        final var image = ImageIO.read( url );
107
        imageResource = new ImageResource( uri, createImage( image ) );
108
      }
109
      else {
110
        // Caught below to return a broken image; exception is swallowed.
111
        throw new UnsupportedOperationException( valueOf( protocol ) );
112
      }
113
114
      return scale( imageResource );
115
    } catch( final Exception e ) {
116
      clue( e );
117
      return new ImageResource( uri, getBrokenImage() );
118
    }
119
  }
120
121
  /**
122
   * Scales the image found at the given URI.
123
   *
124
   * @param ir {@link ImageResource} of image loaded successfully.
125
   * @return Resource representing the rendered image and path.
126
   */
127
  private ImageResource scale( final ImageResource ir ) {
128
    final var image = ir.getImage();
129
    final var imageWidth = image.getWidth();
130
    final var imageHeight = image.getHeight();
131
132
    int maxWidth = mWidthProperty.get();
133
    int newWidth = imageWidth;
134
    int newHeight = imageHeight;
135
136
    // Maintain aspect ratio while shrinking image to view port bounds.
137
    if( imageWidth > maxWidth ) {
138
      newWidth = maxWidth;
139
      newHeight = (newWidth * imageHeight) / imageWidth;
140
    }
141
142
    image.scale( newWidth, newHeight );
143
    return ir;
144
  }
145
146
  /**
147
   * Lazily initializes the broken image placeholder.
148
   *
149
   * @return The {@link FSImage} that represents a broken image icon.
150
   */
151
  private FSImage getBrokenImage() {
152
    final var image = mBrokenImage;
153
154
    if( image == null ) {
155
      mBrokenImage = createImage( BROKEN_IMAGE_PLACEHOLDER );
156
    }
157
158
    return mBrokenImage;
159
  }
160
}
1611
A src/main/java/com/keenwrite/preview/DomConverter.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import org.jsoup.helper.W3CDom;
5
import org.jsoup.nodes.Node;
6
import org.jsoup.nodes.TextNode;
7
import org.jsoup.select.NodeVisitor;
8
import org.w3c.dom.DOMImplementation;
9
import org.w3c.dom.Document;
10
11
import javax.xml.parsers.DocumentBuilder;
12
import javax.xml.parsers.DocumentBuilderFactory;
13
import java.util.LinkedHashMap;
14
import java.util.Map;
15
16
import static com.keenwrite.StatusBarNotifier.clue;
17
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
18
19
/**
20
 * Responsible for converting JSoup document object model (DOM) to a W3C DOM.
21
 * Provides a lighter implementation than the superclass by overriding the
22
 * {@link #fromJsoup(org.jsoup.nodes.Document)} method to reuse factories,
23
 * builders, and implementations.
24
 */
25
class DomConverter extends W3CDom {
26
  /**
27
   * Retain insertion order using an instance of {@link LinkedHashMap} so
28
   * that ligature substitution uses longer ligatures ahead of shorter
29
   * ligatures. The word "ruffian" should use the "ffi" ligature, not the "ff"
30
   * ligature.
31
   */
32
  private static final Map<String, String> LIGATURES = new LinkedHashMap<>();
33
34
  static {
35
    LIGATURES.put( "ffi", "\uFB03" );
36
    LIGATURES.put( "ffl", "\uFB04" );
37
    LIGATURES.put( "ff", "\uFB00" );
38
    LIGATURES.put( "fi", "\uFB01" );
39
    LIGATURES.put( "fl", "\uFB02" );
40
  }
41
42
  private static final NodeVisitor LIGATURE_VISITOR = new NodeVisitor() {
43
    @Override
44
    public void head( final Node node, final int depth ) {
45
      if( node instanceof TextNode ) {
46
        final var parent = node.parentNode();
47
        final var name = parent == null ? "root" : parent.nodeName();
48
49
        if( !("pre".equalsIgnoreCase( name ) ||
50
          "code".equalsIgnoreCase( name ) ||
51
          "tt".equalsIgnoreCase( name )) ) {
52
          // Calling getWholeText() will return newlines, which must be kept
53
          // to ensure that preformatted text maintains its formatting.
54
          final var textNode = (TextNode) node;
55
          textNode.text( replace( textNode.getWholeText(), LIGATURES ) );
56
        }
57
      }
58
    }
59
60
    @Override
61
    public void tail( final Node node, final int depth ) {
62
    }
63
  };
64
65
  private static final DocumentBuilderFactory DOCUMENT_FACTORY;
66
  private static DocumentBuilder DOCUMENT_BUILDER;
67
  private static DOMImplementation DOM_IMPL;
68
69
  static {
70
    DOCUMENT_FACTORY = DocumentBuilderFactory.newInstance();
71
    DOCUMENT_FACTORY.setNamespaceAware( true );
72
73
    try {
74
      DOCUMENT_BUILDER = DOCUMENT_FACTORY.newDocumentBuilder();
75
      DOM_IMPL = DOCUMENT_BUILDER.getDOMImplementation();
76
    } catch( final Exception ex ) {
77
      clue( ex );
78
    }
79
  }
80
81
  @Override
82
  public Document fromJsoup( final org.jsoup.nodes.Document in ) {
83
    assert in != null;
84
    assert DOCUMENT_BUILDER != null;
85
    assert DOM_IMPL != null;
86
87
    final var out = DOCUMENT_BUILDER.newDocument();
88
    final org.jsoup.nodes.DocumentType doctype = in.documentType();
89
90
    if( doctype != null ) {
91
      out.appendChild(
92
        DOM_IMPL.createDocumentType(
93
          doctype.name(),
94
          doctype.publicId(),
95
          doctype.systemId()
96
        )
97
      );
98
    }
99
100
    out.setXmlStandalone( true );
101
    in.traverse( LIGATURE_VISITOR );
102
    convert( in, out );
103
104
    return out;
105
  }
106
}
1107
D src/main/java/com/keenwrite/preview/HTMLPreviewPane.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.preview;
29
30
import com.keenwrite.adapters.DocumentAdapter;
31
import javafx.beans.property.BooleanProperty;
32
import javafx.beans.property.SimpleBooleanProperty;
33
import javafx.embed.swing.SwingNode;
34
import javafx.scene.Node;
35
import org.jsoup.Jsoup;
36
import org.jsoup.helper.W3CDom;
37
import org.xhtmlrenderer.layout.SharedContext;
38
import org.xhtmlrenderer.render.Box;
39
import org.xhtmlrenderer.simple.XHTMLPanel;
40
import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
41
import org.xhtmlrenderer.swing.*;
42
43
import javax.swing.*;
44
import java.awt.*;
45
import java.awt.event.ComponentAdapter;
46
import java.awt.event.ComponentEvent;
47
import java.net.URI;
48
import java.nio.file.Path;
49
50
import static com.keenwrite.Constants.DEFAULT_DIRECTORY;
51
import static com.keenwrite.Constants.STYLESHEET_PREVIEW;
52
import static com.keenwrite.StatusBarNotifier.clue;
53
import static com.keenwrite.util.ProtocolResolver.getProtocol;
54
import static java.awt.Desktop.Action.BROWSE;
55
import static java.awt.Desktop.getDesktop;
56
import static java.lang.Math.max;
57
import static java.lang.String.format;
58
import static javax.swing.SwingUtilities.invokeLater;
59
import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER;
60
61
/**
62
 * HTML preview pane is responsible for rendering an HTML document.
63
 */
64
public final class HTMLPreviewPane extends SwingNode {
65
  /**
66
   * Suppresses scrolling to the top on every key press.
67
   */
68
  private static class HTMLPanel extends XHTMLPanel {
69
    @Override
70
    public void resetScrollPosition() {
71
    }
72
  }
73
74
  /**
75
   * Suppresses scroll attempts until after the document has loaded.
76
   */
77
  private static final class DocumentEventHandler extends DocumentAdapter {
78
    private final BooleanProperty mReadyProperty = new SimpleBooleanProperty();
79
80
    public BooleanProperty readyProperty() {
81
      return mReadyProperty;
82
    }
83
84
    @Override
85
    public void documentStarted() {
86
      mReadyProperty.setValue( Boolean.FALSE );
87
    }
88
89
    @Override
90
    public void documentLoaded() {
91
      mReadyProperty.setValue( Boolean.TRUE );
92
    }
93
  }
94
95
  /**
96
   * Ensure that images are constrained to the panel width upon resizing.
97
   */
98
  private final class ResizeListener extends ComponentAdapter {
99
    @Override
100
    public void componentResized( final ComponentEvent e ) {
101
      setWidth( e );
102
    }
103
104
    @Override
105
    public void componentShown( final ComponentEvent e ) {
106
      setWidth( e );
107
    }
108
109
    /**
110
     * Sets the width of the {@link HTMLPreviewPane} so that images can be
111
     * scaled to fit. The scale factor is adjusted a bit below the full width
112
     * to prevent the horizontal scrollbar from appearing.
113
     *
114
     * @param event The component that defines the image scaling width.
115
     */
116
    private void setWidth( final ComponentEvent event ) {
117
      final int width = (int) (event.getComponent().getWidth() * .95);
118
      HTMLPreviewPane.this.mImageLoader.widthProperty().set( width );
119
    }
120
  }
121
122
  /**
123
   * Responsible for opening hyperlinks. External hyperlinks are opened in
124
   * the system's default browser; local file system links are opened in the
125
   * editor.
126
   */
127
  private static class HyperlinkListener extends LinkListener {
128
    @Override
129
    public void linkClicked( final BasicPanel panel, final String link ) {
130
      try {
131
        switch( getProtocol( link ) ) {
132
          case HTTP:
133
            final var desktop = getDesktop();
134
135
            if( desktop.isSupported( BROWSE ) ) {
136
              desktop.browse( new URI( link ) );
137
            }
138
            break;
139
          case FILE:
140
            // TODO: #88 -- publish a message to the event bus.
141
            break;
142
        }
143
      } catch( final Exception ex ) {
144
        clue( ex );
145
      }
146
    }
147
  }
148
149
  /**
150
   * Render CSS using points (pt) not pixels (px) to reduce the chance of
151
   * poor rendering.
152
   */
153
  private static final String HTML_PREFIX = format(
154
      "<!DOCTYPE html>"
155
          + "<html lang='en'>"
156
          + "<head><title> </title><meta charset='utf-8'/>"
157
          + "<link rel='stylesheet' href='%s'/>"
158
          + "</head>"
159
          + "<body>",
160
      HTMLPreviewPane.class.getResource( STYLESHEET_PREVIEW )
161
  );
162
163
  private static final String HTML_SUFFIX = "</body></html>";
164
165
  /**
166
   * Used to reset the {@link #mHtmlDocument} buffer so that the
167
   * {@link #HTML_PREFIX} need not be appended all the time.
168
   */
169
  private static final int HTML_PREFIX_LENGTH = HTML_PREFIX.length();
170
171
  private static final W3CDom W3C_DOM = new W3CDom();
172
  private static final XhtmlNamespaceHandler NS_HANDLER =
173
      new XhtmlNamespaceHandler();
174
175
  /**
176
   * The buffer is reused so that previous memory allocations need not repeat.
177
   */
178
  private final StringBuilder mHtmlDocument = new StringBuilder( 65536 );
179
180
  private final HTMLPanel mHtmlRenderer = new HTMLPanel();
181
  private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer );
182
  private final CustomImageLoader mImageLoader = new CustomImageLoader();
183
184
  private Path mPath = DEFAULT_DIRECTORY;
185
186
  /**
187
   * Creates a new preview pane that can scroll to the caret position within the
188
   * document.
189
   */
190
  public HTMLPreviewPane() {
191
    setStyle( "-fx-background-color: white;" );
192
193
    // No need to append same prefix each time the HTML content is updated.
194
    mHtmlDocument.append( HTML_PREFIX );
195
196
    // Inject an SVG renderer that produces high-quality SVG buffered images.
197
    final var factory = new ChainedReplacedElementFactory();
198
    factory.addFactory( new SvgReplacedElementFactory() );
199
    factory.addFactory( new SwingReplacedElementFactory(
200
        NO_OP_REPAINT_LISTENER, mImageLoader ) );
201
202
    final var context = getSharedContext();
203
    final var textRenderer = context.getTextRenderer();
204
    context.setReplacedElementFactory( factory );
205
    textRenderer.setSmoothingThreshold( 0 );
206
207
    setContent( mScrollPane );
208
    mHtmlRenderer.addDocumentListener( new DocumentEventHandler() );
209
    mHtmlRenderer.addComponentListener( new ResizeListener() );
210
211
    // The default mouse click listener attempts navigation within the
212
    // preview panel. We want to usurp that behaviour to open the link in
213
    // a platform-specific browser.
214
    for( final var listener : mHtmlRenderer.getMouseTrackingListeners() ) {
215
      if( !(listener instanceof HoverListener) ) {
216
        mHtmlRenderer.removeMouseTrackingListener( (FSMouseListener) listener );
217
      }
218
    }
219
220
    mHtmlRenderer.addMouseTrackingListener( new HyperlinkListener() );
221
  }
222
223
  /**
224
   * Updates the internal HTML source, loads it into the preview pane, then
225
   * scrolls to the caret position.
226
   *
227
   * @param html The new HTML document to display.
228
   */
229
  public void process( final String html ) {
230
    final var docJsoup = Jsoup.parse( decorate( html ) );
231
    final var docW3c = W3C_DOM.fromJsoup( docJsoup );
232
233
    // Access to a Swing component must occur from the Event Dispatch
234
    // Thread (EDT) according to Swing threading restrictions.
235
    invokeLater(
236
        () -> mHtmlRenderer.setDocument( docW3c, getBaseUrl(), NS_HANDLER )
237
    );
238
  }
239
240
  /**
241
   * Clears the preview pane by rendering an empty string.
242
   */
243
  public void clear() {
244
    process( "" );
245
  }
246
247
  /**
248
   * Scrolls to the closest element matching the given identifier without
249
   * waiting for the document to be ready. Be sure the document is ready
250
   * before calling this method.
251
   *
252
   * @param id Scroll the preview pane to this unique paragraph identifier.
253
   */
254
  public void scrollTo( final String id ) {
255
    scrollTo( getBoxById( id ) );
256
  }
257
258
  /**
259
   * Scrolls to the location specified by the {@link Box} that corresponds
260
   * to a point somewhere in the preview pane. If there is no caret, then
261
   * this will not change the scroll position. Changing the scroll position
262
   * to the top if the {@link Box} instance is {@code null} will result in
263
   * jumping around a lot and inconsistent synchronization issues.
264
   *
265
   * @param box The rectangular region containing the caret, or {@code null}
266
   *            if the HTML does not have a caret.
267
   */
268
  private void scrollTo( final Box box ) {
269
    if( box != null ) {
270
      scrollTo( createPoint( box ) );
271
    }
272
  }
273
274
  private void scrollTo( final Point point ) {
275
    mHtmlRenderer.scrollTo( point );
276
  }
277
278
  private Box getBoxById( final String id ) {
279
    return getSharedContext().getBoxById( id );
280
  }
281
282
  private String decorate( final String html ) {
283
    // Trim the HTML back to only the prefix.
284
    mHtmlDocument.setLength( HTML_PREFIX_LENGTH );
285
286
    // Write the HTML body element followed by closing tags.
287
    return mHtmlDocument.append( html ).append( HTML_SUFFIX ).toString();
288
  }
289
290
  public Path getPath() {
291
    return mPath;
292
  }
293
294
  public void setPath( final Path path ) {
295
    assert path != null;
296
    mPath = path;
297
  }
298
299
  /**
300
   * Content to embed in a panel.
301
   *
302
   * @return The content to display to the user.
303
   */
304
  public Node getNode() {
305
    return this;
306
  }
307
308
  public void repaintScrollPane() {
309
    getScrollPane().repaint();
310
  }
311
312
  public JScrollBar getVerticalScrollBar() {
313
    return getScrollPane().getVerticalScrollBar();
314
  }
315
316
  /**
317
   * Creates a {@link Point} to use as a reference for scrolling to the area
318
   * described by the given {@link Box}. The {@link Box} coordinates are used
319
   * to populate the {@link Point}'s location, with minor adjustments for
320
   * vertical centering.
321
   *
322
   * @param box The {@link Box} that represents a scrolling anchor reference.
323
   * @return A coordinate suitable for scrolling to.
324
   */
325
  private Point createPoint( final Box box ) {
326
    assert box != null;
327
328
    int x = box.getAbsX();
329
330
    // Scroll back up by half the height of the scroll bar to keep the typing
331
    // area within the view port. Otherwise the view port will have jumped too
332
    // high up and the most recently typed letters won't be visible.
333
    int y = max(
334
        box.getAbsY() - (mScrollPane.getVerticalScrollBar().getHeight() / 2),
335
        0 );
336
337
    if( !box.getStyle().isInline() ) {
338
      final var margin = box.getMargin( mHtmlRenderer.getLayoutContext() );
339
      x += margin.left();
340
      y += margin.top();
341
    }
342
343
    return new Point( x, y );
344
  }
345
346
  private JScrollPane getScrollPane() {
347
    return mScrollPane;
348
  }
349
350
  private String getBaseUrl() {
351
    final var basePath = getPath();
352
    final var parent = basePath == null ? null : basePath.getParent();
353
354
    return parent == null ? "" : parent.toUri().toString();
355
  }
356
357
  private SharedContext getSharedContext() {
358
    return mHtmlRenderer.getSharedContext();
359
  }
360
}
3611
A src/main/java/com/keenwrite/preview/HtmlPanel.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import com.keenwrite.ui.adapters.DocumentAdapter;
5
import javafx.beans.property.BooleanProperty;
6
import javafx.beans.property.SimpleBooleanProperty;
7
import org.xhtmlrenderer.layout.SharedContext;
8
import org.xhtmlrenderer.render.Box;
9
import org.xhtmlrenderer.simple.XHTMLPanel;
10
import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
11
import org.xhtmlrenderer.swing.BasicPanel;
12
import org.xhtmlrenderer.swing.FSMouseListener;
13
import org.xhtmlrenderer.swing.HoverListener;
14
import org.xhtmlrenderer.swing.LinkListener;
15
16
import java.awt.event.ComponentAdapter;
17
import java.awt.event.ComponentEvent;
18
import java.net.URI;
19
20
import static com.keenwrite.StatusBarNotifier.clue;
21
import static com.keenwrite.util.ProtocolScheme.getProtocol;
22
import static java.awt.Desktop.Action.BROWSE;
23
import static java.awt.Desktop.getDesktop;
24
import static javax.swing.SwingUtilities.invokeLater;
25
import static org.jsoup.Jsoup.parse;
26
27
/**
28
 * Responsible for configuring FlyingSaucer's {@link XHTMLPanel}.
29
 */
30
public class HtmlPanel extends XHTMLPanel {
31
32
  /**
33
   * Suppresses scroll attempts until after the document has loaded.
34
   */
35
  private static final class DocumentEventHandler extends DocumentAdapter {
36
    private final BooleanProperty mReadyProperty = new SimpleBooleanProperty();
37
38
    @Override
39
    public void documentStarted() {
40
      mReadyProperty.setValue( Boolean.FALSE );
41
    }
42
43
    @Override
44
    public void documentLoaded() {
45
      mReadyProperty.setValue( Boolean.TRUE );
46
    }
47
  }
48
49
  /**
50
   * Ensures that the preview panel fills its container's area completely.
51
   */
52
  private final class ComponentEventHandler extends ComponentAdapter {
53
    /**
54
     * Invoked when the component's size changes.
55
     */
56
    public void componentResized( final ComponentEvent e ) {
57
      setPreferredSize( e.getComponent().getPreferredSize() );
58
    }
59
  }
60
61
  /**
62
   * Responsible for opening hyperlinks. External hyperlinks are opened in
63
   * the system's default browser; local file system links are opened in the
64
   * editor.
65
   */
66
  private static final class HyperlinkListener extends LinkListener {
67
    @Override
68
    public void linkClicked( final BasicPanel panel, final String link ) {
69
      switch( getProtocol( link ) ) {
70
        case HTTP -> {
71
          final var desktop = getDesktop();
72
73
          if( desktop.isSupported( BROWSE ) ) {
74
            try {
75
              desktop.browse( new URI( link ) );
76
            } catch( final Exception ex ) {
77
              clue( ex );
78
            }
79
          }
80
        }
81
        case FILE -> {
82
          // TODO: #88 -- publish a message to the event bus.
83
        }
84
      }
85
    }
86
  }
87
88
  private static final DomConverter CONVERTER = new DomConverter();
89
  private static final XhtmlNamespaceHandler XNH = new XhtmlNamespaceHandler();
90
91
  public HtmlPanel() {
92
    addDocumentListener( new DocumentEventHandler() );
93
    removeMouseTrackingListeners();
94
    addMouseTrackingListener( new HyperlinkListener() );
95
    addComponentListener( new ComponentEventHandler() );
96
  }
97
98
  /**
99
   * Updates the document model displayed by the renderer. Effectively, this
100
   * updates the HTML document to provide new content.
101
   *
102
   * @param html    A complete HTML5 document, including doctype.
103
   * @param baseUri URI to use for finding relative files, such as images.
104
   */
105
  public void render( final String html, final String baseUri ) {
106
    final var doc = CONVERTER.fromJsoup( parse( html ) );
107
108
    // Access to a Swing component must occur from the Event Dispatch
109
    // Thread (EDT) according to Swing threading restrictions. Setting a new
110
    // document invokes a Swing repaint operation.
111
    invokeLater( () -> setDocument( doc, baseUri, XNH ) );
112
  }
113
114
  /**
115
   * Delegates to the {@link SharedContext}.
116
   *
117
   * @param id The HTML element identifier to retrieve in {@link Box} form.
118
   * @return The {@link Box} that corresponds to the given element ID, or
119
   * {@code null} if none found.
120
   */
121
  public Box getBoxById( final String id ) {
122
    return getSharedContext().getBoxById( id );
123
  }
124
125
  /**
126
   * Suppress scrolling to the top on updates.
127
   */
128
  @Override
129
  public void resetScrollPosition() {
130
  }
131
132
  /**
133
   * The default mouse click listener attempts navigation within the preview
134
   * panel. We want to usurp that behaviour to open the link in a
135
   * platform-specific browser.
136
   */
137
  private void removeMouseTrackingListeners() {
138
    for( final var listener : getMouseTrackingListeners() ) {
139
      if( !(listener instanceof HoverListener) ) {
140
        removeMouseTrackingListener( (FSMouseListener) listener );
141
      }
142
    }
143
  }
144
}
1145
A src/main/java/com/keenwrite/preview/HtmlPreview.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import com.keenwrite.preferences.LocaleProperty;
5
import com.keenwrite.preferences.Workspace;
6
import javafx.beans.property.DoubleProperty;
7
import javafx.embed.swing.SwingNode;
8
import org.xhtmlrenderer.render.Box;
9
import org.xhtmlrenderer.swing.SwingReplacedElementFactory;
10
11
import javax.swing.*;
12
import java.awt.*;
13
import java.net.URL;
14
import java.nio.file.Path;
15
import java.util.Locale;
16
17
import static com.keenwrite.Constants.*;
18
import static com.keenwrite.Messages.get;
19
import static com.keenwrite.preferences.Workspace.KEY_UI_FONT_LOCALE;
20
import static com.keenwrite.preferences.Workspace.KEY_UI_FONT_PREVIEW_SIZE;
21
import static java.lang.Math.max;
22
import static java.lang.String.format;
23
import static javafx.scene.CacheHint.SPEED;
24
import static javax.swing.SwingUtilities.invokeLater;
25
26
/**
27
 * Responsible for parsing an HTML document.
28
 */
29
public final class HtmlPreview extends SwingNode {
30
31
  // The order is important: Swing factory will replace SVG images with
32
  // a blank image, which will cause the chained factory to cache the image
33
  // and exit. Instead, the SVG must execute first to rasterize the content.
34
  // Consequently, the chained factory must maintain insertion order.
35
  private static final ChainedReplacedElementFactory FACTORY
36
    = new ChainedReplacedElementFactory(
37
    new SvgReplacedElementFactory(),
38
    new SwingReplacedElementFactory()
39
  );
40
41
  /**
42
   * Render CSS using points (pt) not pixels (px) to reduce the chance of
43
   * poor rendering.
44
   */
45
  private static final String HTML_HEAD =
46
    """
47
      <!DOCTYPE html>
48
      <html lang='%s'><head><title> </title><meta charset='utf-8'>
49
      <link rel='stylesheet' href='%s'>
50
      <link rel='stylesheet' href='%s'>
51
      <style>body{font-size: %spt;}</style>
52
      <base href='%s'>
53
      </head><body>
54
      """;
55
56
  private static final String HTML_TAIL = "</body></html>";
57
58
  private static final URL HTML_STYLE_PREVIEW = toUrl( STYLESHEET_PREVIEW );
59
60
  /**
61
   * The buffer is reused so that previous memory allocations need not repeat.
62
   */
63
  private final StringBuilder mHtmlDocument = new StringBuilder( 65536 );
64
65
  private HtmlPanel mView;
66
  private JScrollPane mScrollPane;
67
  private String mBaseUriPath = "";
68
  private URL mLocaleUrl;
69
  private final Workspace mWorkspace;
70
71
  /**
72
   * Creates a new preview pane that can scroll to the caret position within the
73
   * document.
74
   *
75
   * @param workspace Contains locale and font size information.
76
   */
77
  public HtmlPreview( final Workspace workspace ) {
78
    mWorkspace = workspace;
79
    mLocaleUrl = toUrl( getLocale() );
80
81
    // Attempts to prevent a flash of black un-styled content upon load.
82
    setStyle( "-fx-background-color: white;" );
83
84
    invokeLater( () -> {
85
      mView = new HtmlPanel();
86
      mScrollPane = new JScrollPane( mView );
87
88
      // Enabling the cache attempts to prevent black flashes when resizing.
89
      setCache( true );
90
      setCacheHint( SPEED );
91
      setContent( mScrollPane );
92
93
      final var context = mView.getSharedContext();
94
      final var textRenderer = context.getTextRenderer();
95
      context.setReplacedElementFactory( FACTORY );
96
      textRenderer.setSmoothingThreshold( 0 );
97
98
      localeProperty().addListener( ( c, o, n ) -> {
99
        mLocaleUrl = toUrl( getLocale() );
100
        rerender();
101
      } );
102
103
      fontSizeProperty().addListener( ( c, o, n ) -> rerender() );
104
    } );
105
  }
106
107
  /**
108
   * Updates the internal HTML source shown in the preview pane.
109
   *
110
   * @param html The new HTML document to display.
111
   */
112
  public void render( final String html ) {
113
    mView.render( decorate( html ), getBaseUri() );
114
  }
115
116
  /**
117
   * Clears the caches then rerenders the content.
118
   */
119
  public void refresh() {
120
    FACTORY.clearCache();
121
    rerender();
122
  }
123
124
  private void rerender() {
125
    render( mHtmlDocument.toString() );
126
  }
127
128
  /**
129
   * Attaches the HTML head prefix and HTML tail suffix to the given HTML
130
   * string.
131
   *
132
   * @param html The HTML to adorn with opening and closing tags.
133
   * @return A complete HTML document, ready for rendering.
134
   */
135
  private String decorate( final String html ) {
136
    mHtmlDocument.setLength( 0 );
137
    mHtmlDocument.append( html );
138
    return head() + mHtmlDocument + tail();
139
  }
140
141
  private String head() {
142
    return format(
143
      HTML_HEAD,
144
      getLocale().getLanguage(),
145
      HTML_STYLE_PREVIEW,
146
      mLocaleUrl,
147
      getFontSize(),
148
      mBaseUriPath
149
    );
150
  }
151
152
  private String tail() {
153
    return HTML_TAIL;
154
  }
155
156
  /**
157
   * Clears the preview pane by rendering an empty string.
158
   */
159
  public void clear() {
160
    render( "" );
161
  }
162
163
  /**
164
   * Sets the base URI to the containing directory the file being edited.
165
   *
166
   * @param path The path to the file being edited.
167
   */
168
  public void setBaseUri( final Path path ) {
169
    final var parent = path.getParent();
170
    mBaseUriPath = parent == null ? "" : parent.toUri().toString();
171
  }
172
173
  /**
174
   * Scrolls to the closest element matching the given identifier without
175
   * waiting for the document to be ready. Be sure the document is ready
176
   * before calling this method.
177
   *
178
   * @param id Scroll the preview pane to this unique paragraph identifier.
179
   */
180
  public void scrollTo( final String id ) {
181
    scrollTo( mView.getBoxById( id ) );
182
  }
183
184
  /**
185
   * Scrolls to the location specified by the {@link Box} that corresponds
186
   * to a point somewhere in the preview pane. If there is no caret, then
187
   * this will not change the scroll position. Changing the scroll position
188
   * to the top if the {@link Box} instance is {@code null} will result in
189
   * jumping around a lot and inconsistent synchronization issues.
190
   *
191
   * @param box The rectangular region containing the caret, or {@code null}
192
   *            if the HTML does not have a caret.
193
   */
194
  private void scrollTo( final Box box ) {
195
    if( box != null ) {
196
      scrollTo( createPoint( box ) );
197
    }
198
  }
199
200
  private void scrollTo( final Point point ) {
201
    invokeLater( () -> {
202
      mView.scrollTo( point );
203
      getScrollPane().repaint();
204
    } );
205
  }
206
207
  /**
208
   * Creates a {@link Point} to use as a reference for scrolling to the area
209
   * described by the given {@link Box}. The {@link Box} coordinates are used
210
   * to populate the {@link Point}'s location, with minor adjustments for
211
   * vertical centering.
212
   *
213
   * @param box The {@link Box} that represents a scrolling anchor reference.
214
   * @return A coordinate suitable for scrolling to.
215
   */
216
  private Point createPoint( final Box box ) {
217
    assert box != null;
218
219
    // Scroll back up by half the height of the scroll bar to keep the typing
220
    // area within the view port. Otherwise the view port will have jumped too
221
    // high up and the most recently typed letters won't be visible.
222
    int y = max( box.getAbsY() - getVerticalScrollBarHeight() / 2, 0 );
223
    int x = box.getAbsX();
224
225
    if( !box.getStyle().isInline() ) {
226
      final var margin = box.getMargin( mView.getLayoutContext() );
227
      y += margin.top();
228
      x += margin.left();
229
    }
230
231
    return new Point( x, y );
232
  }
233
234
  private String getBaseUri() {
235
    return mBaseUriPath;
236
  }
237
238
  private JScrollPane getScrollPane() {
239
    return mScrollPane;
240
  }
241
242
  public JScrollBar getVerticalScrollBar() {
243
    return getScrollPane().getVerticalScrollBar();
244
  }
245
246
  private int getVerticalScrollBarHeight() {
247
    return getVerticalScrollBar().getHeight();
248
  }
249
250
  /**
251
   * Returns the ISO 639 alpha-2 or alpha-3 language code followed by a hyphen
252
   * followed by the ISO 15924 alpha-4 script code, followed by an ISO 3166
253
   * alpha-2 country code or UN M.49 numeric-3 area code. For example, this
254
   * could return "en-Latn-CA" for Canadian English written in the Latin
255
   * character set.
256
   *
257
   * @return Unique identifier for language and country.
258
   */
259
  private static URL toUrl( final Locale locale ) {
260
    return toUrl(
261
      get(
262
        sSettings.getSetting( STYLESHEET_PREVIEW_LOCALE, "" ),
263
        locale.getLanguage(),
264
        locale.getScript(),
265
        locale.getCountry()
266
      )
267
    );
268
  }
269
270
  private static URL toUrl( final String path ) {
271
    return HtmlPreview.class.getResource( path );
272
  }
273
274
  private Locale getLocale() {
275
    return localeProperty().toLocale();
276
  }
277
278
  private LocaleProperty localeProperty() {
279
    return mWorkspace.localeProperty( KEY_UI_FONT_LOCALE );
280
  }
281
282
  private double getFontSize() {
283
    return fontSizeProperty().get();
284
  }
285
286
  private DoubleProperty fontSizeProperty() {
287
    return mWorkspace.doubleProperty( KEY_UI_FONT_PREVIEW_SIZE );
288
  }
289
}
1290
M src/main/java/com/keenwrite/preview/MathRenderer.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.preview;
293
...
4014
 */
4115
public class MathRenderer {
16
17
  /**
18
   * Singleton instance for rendering math symbols.
19
   */
20
  public static final MathRenderer MATH_RENDERER = new MathRenderer();
4221
4322
  /**
...
5029
  private final SvgDomGraphics2D mGraphics = createSvgDomGraphics2D();
5130
52
  public MathRenderer() {
31
  private MathRenderer() {
5332
    mGraphics.scale( FONT_SIZE, FONT_SIZE );
5433
  }
M src/main/java/com/keenwrite/preview/RenderingSettings.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.preview;
293
M src/main/java/com/keenwrite/preview/SvgRasterizer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.preview;
293
304
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
315
import org.apache.batik.gvt.renderer.ImageRenderer;
32
import org.apache.batik.transcoder.TranscoderException;
336
import org.apache.batik.transcoder.TranscoderInput;
347
import org.apache.batik.transcoder.TranscoderOutput;
...
4417
import java.awt.*;
4518
import java.awt.image.BufferedImage;
46
import java.io.IOException;
19
import java.io.File;
4720
import java.io.StringReader;
4821
import java.io.StringWriter;
49
import java.net.URL;
22
import java.net.URI;
23
import java.nio.file.Path;
5024
import java.text.NumberFormat;
5125
...
130104
    try {
131105
      image = rasterizeString( BROKEN_IMAGE_SVG, w );
132
    } catch( final Exception e ) {
106
    } catch( final Exception ex ) {
133107
      image = new BufferedImage( w, h, TYPE_INT_RGB );
134108
      final var graphics = (Graphics2D) image.getGraphics();
...
177151
      final RenderingHints hints = renderer.getRenderingHints();
178152
      hints.putAll( RENDERING_HINTS );
179
180153
      renderer.setRenderingHints( hints );
181154
182155
      return renderer;
183
    }
184
  }
185
186
  /**
187
   * Rasterizes the vector graphic file at the given URL. If any exception
188
   * happens, a red circle is returned instead.
189
   *
190
   * @param url   The URL to a vector graphic file, which must include the
191
   *              protocol scheme (such as file:// or https://).
192
   * @param width The number of pixels wide to render the image. The aspect
193
   *              ratio is maintained.
194
   * @return Either the rasterized image upon success or a red circle.
195
   */
196
  public static BufferedImage rasterize( final String url, final int width ) {
197
    try {
198
      return rasterize( new URL( url ), width );
199
    } catch( final Exception ex ) {
200
      clue( ex );
201
      return BROKEN_IMAGE_PLACEHOLDER;
202156
    }
203157
  }
204158
205159
  /**
206160
   * Rasterizes the given document into an image.
207161
   *
208162
   * @param svg   The SVG {@link Document} to rasterize.
209163
   * @param width The rasterized image's width (in pixels).
210164
   * @return The rasterized image.
211
   * @throws TranscoderException Signifies an issue with the input document.
212165
   */
213
  public static BufferedImage rasterize( final Document svg, final int width )
214
      throws TranscoderException {
215
    final var transcoder = new BufferedImageTranscoder();
216
    final var input = new TranscoderInput( svg );
217
218
    transcoder.addTranscodingHint( KEY_WIDTH, (float) width );
219
    transcoder.transcode( input, null );
166
  public static BufferedImage rasterize( final Document svg, final int width ) {
167
    try {
168
      final var transcoder = new BufferedImageTranscoder();
169
      final var input = new TranscoderInput( svg );
220170
221
    return transcoder.getImage();
222
  }
171
      transcoder.addTranscodingHint( KEY_WIDTH, (float) width );
172
      transcoder.transcode( input, null );
173
      return transcoder.getImage();
174
    } catch( final Exception ex ) {
175
      clue( ex );
176
    }
223177
224
  /**
225
   * Converts an SVG drawing into a rasterized image that can be drawn on
226
   * a graphics context.
227
   *
228
   * @param url   The path to the image (can be web address).
229
   * @param width Scale the image width to this size (aspect ratio is
230
   *              maintained).
231
   * @return The vector graphic transcoded into a raster image format.
232
   * @throws IOException         Could not read the vector graphic.
233
   * @throws TranscoderException Could not convert the vector graphic to an
234
   *                             instance of {@link Image}.
235
   */
236
  public static BufferedImage rasterize( final URL url, final int width )
237
      throws IOException, TranscoderException {
238
    return rasterize( FACTORY_DOM.createDocument( url.toString() ), width );
178
    return BROKEN_IMAGE_PLACEHOLDER;
239179
  }
240180
241181
  public static BufferedImage rasterize( final Document document ) {
242182
    try {
243183
      final var root = document.getDocumentElement();
244184
      final var width = root.getAttribute( "width" );
245185
      return rasterize( document, INT_FORMAT.parse( width ).intValue() );
246186
    } catch( final Exception ex ) {
247187
      clue( ex );
248
      return BROKEN_IMAGE_PLACEHOLDER;
249188
    }
189
190
    return BROKEN_IMAGE_PLACEHOLDER;
250191
  }
251192
252193
  /**
253
   * Converts an SVG string into a rasterized image that can be drawn on
194
   * Rasterizes the vector graphic file at the given URI. If any exception
195
   * happens, a broken image icon is returned instead.
196
   *
197
   * @param path  The {@link Path} to a vector graphic file.
198
   * @param width Scale the image to the given width (px); aspect ratio is
199
   *              maintained.
200
   * @return A rasterized image as an instance of {@link BufferedImage}.
201
   */
202
  public static BufferedImage rasterize( final Path path, final int width ) {
203
    return rasterize( path.toUri(), width );
204
  }
205
206
  /**
207
   * Rasterizes the vector graphic file at the given URI. If any exception
208
   * happens, a broken image icon is returned instead.
209
   *
210
   * @param uri   The URI to a vector graphic file, which must include the
211
   *              protocol scheme (such as file:// or https://).
212
   * @param width Scale the image to the given width (px); aspect ratio is
213
   *              maintained.
214
   * @return A rasterized image as an instance of {@link BufferedImage}.
215
   */
216
  public static BufferedImage rasterize( final String uri, final int width ) {
217
    return rasterize( new File( uri ).toURI(), width );
218
  }
219
220
  /**
221
   * Converts an SVG drawing into a rasterized image that can be drawn on
254222
   * a graphics context.
255223
   *
256
   * @param svg The SVG xml document.
257
   * @param w   Scale the image width to this size (aspect ratio is
258
   *            maintained).
224
   * @param uri   The path to the image (can be web address).
225
   * @param width Scale the image to the given width (px); aspect ratio is
226
   *              maintained.
259227
   * @return The vector graphic transcoded into a raster image format.
260
   * @throws TranscoderException Could not convert the vector graphic to an
261
   *                             instance of {@link Image}.
262228
   */
263
  public static BufferedImage rasterizeString( final String svg, final int w )
264
      throws IOException, TranscoderException {
265
    return rasterize( toDocument( svg ), w );
229
  public static BufferedImage rasterize( final URI uri, final int width ) {
230
    try {
231
      return rasterize( FACTORY_DOM.createDocument( uri.toString() ), width );
232
    } catch( final Exception ex ) {
233
      clue( ex );
234
    }
235
236
    return BROKEN_IMAGE_PLACEHOLDER;
266237
  }
267238
...
281252
    } catch( final Exception ex ) {
282253
      clue( ex );
283
      return BROKEN_IMAGE_PLACEHOLDER;
284254
    }
255
256
    return BROKEN_IMAGE_PLACEHOLDER;
285257
  }
286258
287259
  /**
288
   * Converts an SVG XML string into a new {@link Document} instance.
260
   * Converts an SVG string into a rasterized image that can be drawn on
261
   * a graphics context.
289262
   *
290
   * @param xml The XML containing SVG elements.
291
   * @return The SVG contents parsed into a {@link Document} object model.
292
   * @throws IOException Could
263
   * @param svg The SVG xml document.
264
   * @param w   Scale the image width to this size (aspect ratio is
265
   *            maintained).
266
   * @return The vector graphic transcoded into a raster image format.
293267
   */
294
  private static Document toDocument( final String xml ) throws IOException {
295
    try( final var reader = new StringReader( xml ) ) {
296
      return FACTORY_DOM.createSVGDocument(
297
          "http://www.w3.org/2000/svg", reader );
298
    }
268
  public static BufferedImage rasterizeString( final String svg, final int w ) {
269
    return rasterize( toDocument( svg ), w );
299270
  }
300271
...
315286
316287
    return BROKEN_IMAGE_SVG;
288
  }
289
290
  /**
291
   * Converts an SVG XML string into a new {@link Document} instance.
292
   *
293
   * @param xml The XML containing SVG elements.
294
   * @return The SVG contents parsed into a {@link Document} object model.
295
   */
296
  private static Document toDocument( final String xml ) {
297
    try( final var reader = new StringReader( xml ) ) {
298
      return FACTORY_DOM.createSVGDocument(
299
          "http://www.w3.org/2000/svg", reader );
300
    } catch( final Exception ex ) {
301
      throw new IllegalArgumentException( ex );
302
    }
317303
  }
318304
}
M src/main/java/com/keenwrite/preview/SvgReplacedElementFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.preview;
293
30
import com.keenwrite.util.BoundedCache;
31
import org.apache.commons.io.FilenameUtils;
4
import com.keenwrite.io.HttpMediaType;
5
import com.keenwrite.io.MediaType;
6
import com.keenwrite.ui.adapters.ReplacedElementAdapter;
327
import org.w3c.dom.Element;
338
import org.xhtmlrenderer.extend.ReplacedElement;
34
import org.xhtmlrenderer.extend.ReplacedElementFactory;
359
import org.xhtmlrenderer.extend.UserAgentCallback;
3610
import org.xhtmlrenderer.layout.LayoutContext;
3711
import org.xhtmlrenderer.render.BlockBox;
38
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
3912
import org.xhtmlrenderer.swing.ImageReplacedElement;
4013
4114
import java.awt.image.BufferedImage;
42
import java.util.Map;
43
import java.util.function.Function;
15
import java.net.URI;
16
import java.nio.file.Paths;
4417
4518
import static com.keenwrite.StatusBarNotifier.clue;
19
import static com.keenwrite.io.MediaType.*;
20
import static com.keenwrite.preview.MathRenderer.MATH_RENDERER;
21
import static com.keenwrite.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER;
4622
import static com.keenwrite.preview.SvgRasterizer.rasterize;
4723
import static com.keenwrite.processors.markdown.tex.TexNode.HTML_TEX;
24
import static com.keenwrite.util.ProtocolScheme.getProtocol;
4825
4926
/**
5027
 * Responsible for running {@link SvgRasterizer} on SVG images detected within
5128
 * a document to transform them into rasterized versions.
5229
 */
53
public class SvgReplacedElementFactory implements ReplacedElementFactory {
54
55
  /**
56
   * Prevent instantiation until needed.
57
   */
58
  private static class MathRendererContainer {
59
    private static final MathRenderer INSTANCE = new MathRenderer();
60
  }
61
62
  /**
63
   * Returns the singleton instance for rendering math symbols.
64
   *
65
   * @return A non-null instance, loaded, configured, and ready to render math.
66
   */
67
  public static MathRenderer getInstance() {
68
    return MathRendererContainer.INSTANCE;
69
  }
70
71
  /**
72
   * SVG filename extension maps to an SVG image element.
73
   */
74
  private static final String SVG_FILE = "svg";
30
public class SvgReplacedElementFactory extends ReplacedElementAdapter {
7531
76
  private static final String HTML_IMAGE = "img";
77
  private static final String HTML_IMAGE_SRC = "src";
32
  public static final String HTML_IMAGE = "img";
33
  public static final String HTML_IMAGE_SRC = "src";
7834
79
  /**
80
   * A bounded cache that removes the oldest image if the maximum number of
81
   * cached images has been reached. This constrains the number of images
82
   * loaded into memory.
83
   */
84
  private final Map<String, BufferedImage> mImageCache =
85
      new BoundedCache<>( 150 );
35
  private static final ImageReplacedElement BROKEN_IMAGE =
36
    createImageReplacedElement( BROKEN_IMAGE_PLACEHOLDER );
8637
8738
  @Override
8839
  public ReplacedElement createReplacedElement(
89
      final LayoutContext c,
90
      final BlockBox box,
91
      final UserAgentCallback uac,
92
      final int cssWidth,
93
      final int cssHeight ) {
94
    BufferedImage image = null;
40
    final LayoutContext c,
41
    final BlockBox box,
42
    final UserAgentCallback uac,
43
    final int cssWidth,
44
    final int cssHeight ) {
9545
    final var e = box.getElement();
9646
97
    if( e != null ) {
98
      try {
99
        final var nodeName = e.getNodeName();
47
    ImageReplacedElement image = null;
10048
101
        if( HTML_IMAGE.equals( nodeName ) ) {
102
          final var src = e.getAttribute( HTML_IMAGE_SRC );
103
          final var ext = FilenameUtils.getExtension( src );
49
    try {
50
      BufferedImage raster = null;
10451
105
          if( SVG_FILE.equalsIgnoreCase( ext ) ) {
106
            image = getCachedImage(
107
                src, svg -> rasterize( svg, box.getContentWidth() ) );
52
      switch( e.getNodeName() ) {
53
        case HTML_IMAGE -> {
54
          final var source = e.getAttribute( HTML_IMAGE_SRC );
55
          URI uri = null;
56
57
          if( getProtocol( source ).isHttp() ) {
58
            var mediaType = MediaType.valueFrom( source );
59
60
            if( isSvg( mediaType ) || mediaType == UNDEFINED ) {
61
              uri = new URI( source );
62
63
              // Attempt to rasterize SVG depending on URL resource content.
64
              if( !isSvg( HttpMediaType.valueFrom( uri ) ) ) {
65
                uri = null;
66
              }
67
            }
10868
          }
109
        }
110
        else if( HTML_TEX.equals( nodeName ) ) {
111
          // Convert the TeX element to a raster graphic if not yet cached.
112
          final var src = e.getTextContent();
113
          image = getCachedImage(
114
              src, __ -> rasterize( getInstance().render( src ) )
115
          );
69
          else if( isSvg( MediaType.valueFrom( source ) ) ) {
70
            // Attempt to rasterize based on file name.
71
            final var base = new URI( getBaseUri( e ) ).getPath();
72
            uri = Paths.get( base, source ).toUri();
73
          }
74
75
          if( uri != null ) {
76
            raster = rasterize( uri, box.getContentWidth() );
77
          }
11678
        }
117
      } catch( final Exception ex ) {
118
        clue( ex );
79
        case HTML_TEX ->
80
          // Convert the TeX element to a raster graphic.
81
          raster = rasterize( MATH_RENDERER.render( e.getTextContent() ) );
11982
      }
120
    }
121
122
    if( image != null ) {
123
      final var w = image.getWidth( null );
124
      final var h = image.getHeight( null );
12583
126
      return new ImageReplacedElement( image, w, h );
84
      if( raster != null ) {
85
        image = createImageReplacedElement( raster );
86
      }
87
    } catch( final Exception ex ) {
88
      image = BROKEN_IMAGE;
89
      clue( ex );
12790
    }
12891
129
    return null;
92
    return image;
13093
  }
13194
132
  @Override
133
  public void reset() {
134
  }
95
  private String getBaseUri( final Element e ) {
96
    try {
97
      final var doc = e.getOwnerDocument();
98
      final var html = doc.getDocumentElement();
99
      final var head = html.getFirstChild();
100
      final var children = head.getChildNodes();
135101
136
  @Override
137
  public void remove( final Element e ) {
102
      for( int i = children.getLength() - 1; i >= 0; i-- ) {
103
        final var child = children.item( i );
104
        final var name = child.getLocalName();
105
106
        if( "base".equalsIgnoreCase( name ) ) {
107
          final var attrs = child.getAttributes();
108
          final var item = attrs.getNamedItem( "href" );
109
110
          return item.getNodeValue();
111
        }
112
      }
113
    } catch( final Exception ex ) {
114
      clue( ex );
115
    }
116
117
    return "";
138118
  }
139119
140
  @Override
141
  public void setFormSubmissionListener( FormSubmissionListener listener ) {
120
  private static ImageReplacedElement createImageReplacedElement(
121
    final BufferedImage bi ) {
122
    return new ImageReplacedElement( bi, bi.getWidth(), bi.getHeight() );
142123
  }
143124
144
  /**
145
   * Returns an image associated with a string; the string's pre-computed
146
   * hash code is returned as the string value, making this operation very
147
   * quick to return the corresponding {@link BufferedImage}.
148
   *
149
   * @param src        The source used for the key into the image cache.
150
   * @param rasterizer {@link Function} to call to rasterize an image.
151
   * @return The image that corresponds to the given source string.
152
   */
153
  private BufferedImage getCachedImage(
154
      final String src, final Function<String, BufferedImage> rasterizer ) {
155
    return mImageCache.computeIfAbsent( src, __ -> rasterizer.apply( src ) );
125
  private static boolean isSvg( final MediaType mediaType ) {
126
    return mediaType == TEXT_PLAIN || mediaType == IMAGE_SVG_XML;
156127
  }
157128
}
D src/main/java/com/keenwrite/processors/AbstractProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors;
29
30
/**
31
 * Responsible for transforming a document through a variety of chained
32
 * handlers. If there are conditions where this handler should not process the
33
 * entire chain, create a second handler, or split the chain into reusable
34
 * sub-chains.
35
 *
36
 * @param <T> The type of object to process.
37
 */
38
public abstract class AbstractProcessor<T> implements Processor<T> {
39
40
  /**
41
   * Used while processing the entire chain; null to signify no more links.
42
   */
43
  private final Processor<T> mNext;
44
45
  /**
46
   * Constructs a new default handler with no successor.
47
   */
48
  protected AbstractProcessor() {
49
    this( null );
50
  }
51
52
  /**
53
   * Constructs a new default handler with a given successor.
54
   *
55
   * @param successor The next processor in the chain.
56
   */
57
  public AbstractProcessor( final Processor<T> successor ) {
58
    mNext = successor;
59
  }
60
61
  @Override
62
  public Processor<T> next() {
63
    return mNext;
64
  }
65
66
  /**
67
   * This algorithm is incorrect, but works for the one use case of removing
68
   * the ending HTML Preview Processor from the end of the processor chain.
69
   * The processor chain is immutable so this creates a succession of
70
   * delegators that wrap each processor in the chain, except for the one
71
   * to be removed.
72
   * <p>
73
   * An alternative is to update the {@link ProcessorFactory} with the ability
74
   * to create a processor chain devoid of an {@link HtmlPreviewProcessor}.
75
   * </p>
76
   *
77
   * @param removal The {@link Processor} to remove from the chain.
78
   * @return A delegating processor chain starting from this processor
79
   * onwards with the given processor removed from the chain.
80
   */
81
  @Override
82
  public Processor<T> remove( final Class<? extends Processor<T>> removal ) {
83
    Processor<T> p = this;
84
    final ProcessorDelegator<T> head = new ProcessorDelegator<>( p );
85
    ProcessorDelegator<T> result = head;
86
87
    while( p != null ) {
88
      final Processor<T> next = p.next();
89
90
      if( next != null && next.getClass() != removal ) {
91
        final var delegator = new ProcessorDelegator<>( next );
92
93
        result.setNext( delegator );
94
        result = delegator;
95
      }
96
97
      p = p.next();
98
    }
99
100
    return head;
101
  }
102
103
  private static final class ProcessorDelegator<T>
104
      extends AbstractProcessor<T> {
105
    private final Processor<T> mDelegate;
106
    private Processor<T> mNext;
107
108
    public ProcessorDelegator( final Processor<T> delegate ) {
109
      super( delegate );
110
111
      assert delegate != null;
112
113
      mDelegate = delegate;
114
    }
115
116
    @Override
117
    public T apply( T t ) {
118
      return mDelegate.apply( t );
119
    }
120
121
    protected void setNext( final Processor<T> next ) {
122
      mNext = next;
123
    }
124
125
    @Override
126
    public Processor<T> next() {
127
      return mNext;
128
    }
129
  }
130
}
1311
M src/main/java/com/keenwrite/processors/DefinitionProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors;
293
...
3711
 * {@code $variable$}.
3812
 */
39
public class DefinitionProcessor extends AbstractProcessor<String> {
13
public class DefinitionProcessor extends ExecutorProcessor<String> {
4014
4115
  private final Map<String, String> mDefinitions;
4216
17
  /**
18
   * Constructs a processor capable of interpolating string definitions.
19
   *
20
   * @param successor Subsequent link in the processing chain.
21
   * @param context   Contains resolved definitions map.
22
   */
4323
  public DefinitionProcessor(
44
      final Processor<String> successor, final Map<String, String> map ) {
24
      final Processor<String> successor,
25
      final ProcessorContext context ) {
4526
    super( successor );
46
    mDefinitions = map;
27
    mDefinitions = context.getResolvedMap();
4728
  }
4829
A src/main/java/com/keenwrite/processors/ExecutorProcessor.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import java.util.Optional;
5
import java.util.concurrent.atomic.AtomicReference;
6
7
/**
8
 * Responsible for transforming data through a variety of chained handlers.
9
 *
10
 * @param <T> The data type to process.
11
 */
12
public class ExecutorProcessor<T> implements Processor<T> {
13
14
  /**
15
   * The next link in the processing chain.
16
   */
17
  private final Processor<T> mNext;
18
19
  protected ExecutorProcessor() {
20
    this( null );
21
  }
22
23
  /**
24
   * Constructs a new processor having a given successor.
25
   *
26
   * @param successor The next processor in the chain.
27
   */
28
  public ExecutorProcessor( final Processor<T> successor ) {
29
    mNext = successor;
30
  }
31
32
  /**
33
   * Calls every link in the chain to process the given data.
34
   *
35
   * @param data The data to transform.
36
   * @return The data after processing by every link in the chain.
37
   */
38
  @Override
39
  public T apply( final T data ) {
40
    // Start processing using the first processor after the executor.
41
    Optional<Processor<T>> handler = next();
42
    final var result = new MutableReference( data );
43
44
    while( handler.isPresent() ) {
45
      handler = handler.flatMap( p -> {
46
        result.set( p.apply( result.get() ) );
47
        return p.next();
48
      } );
49
    }
50
51
    return result.get();
52
  }
53
54
  @Override
55
  public Optional<Processor<T>> next() {
56
    return Optional.ofNullable( mNext );
57
  }
58
59
  /**
60
   * A minor micro-optimization, since the processors cannot be run in parallel,
61
   * avoid using an {@link AtomicReference} during processor execution. This
62
   * is about twice as fast for the first four processor links in the chain.
63
   */
64
  private final class MutableReference {
65
    private T mObject;
66
67
    MutableReference( final T object ) {
68
      set( object );
69
    }
70
71
    void set( final T object ) {
72
      mObject = object;
73
    }
74
75
    T get() {
76
      return mObject;
77
    }
78
  }
79
}
180
M src/main/java/com/keenwrite/processors/HtmlPreviewProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors;
293
30
import com.keenwrite.preview.HTMLPreviewPane;
4
import com.keenwrite.preview.HtmlPreview;
315
326
/**
337
 * Responsible for notifying the HTMLPreviewPane when the succession chain has
348
 * updated. This decouples knowledge of changes to the editor panel from the
359
 * 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
10
 * final HTML preview is rendered. This is the last link in the processor
3711
 * chain.
3812
 */
39
public class HtmlPreviewProcessor extends AbstractProcessor<String> {
13
public class HtmlPreviewProcessor extends ExecutorProcessor<String> {
4014
41
  // There is only one preview panel.
42
  private static HTMLPreviewPane sHtmlPreviewPane;
15
  /**
16
   * There is only one preview panel.
17
   */
18
  private static HtmlPreview sHtmlPreviewPane;
4319
4420
  /**
4521
   * Constructs the end of a processing chain.
4622
   *
4723
   * @param htmlPreviewPane The pane to update with the post-processed document.
4824
   */
49
  public HtmlPreviewProcessor( final HTMLPreviewPane htmlPreviewPane ) {
25
  public HtmlPreviewProcessor( final HtmlPreview htmlPreviewPane ) {
5026
    sHtmlPreviewPane = htmlPreviewPane;
5127
  }
5228
5329
  /**
5430
   * Update the preview panel using HTML from the succession chain.
5531
   *
5632
   * @param html The document content to render in the preview pane. The HTML
5733
   *             should not contain a doctype, head, or body tag, only
5834
   *             content to render within the body.
59
   * @return {@code null} to indicate no more processors in the chain.
35
   * @return The given {@code html} string.
6036
   */
6137
  @Override
6238
  public String apply( final String html ) {
63
    getHtmlPreviewPane().process( html );
39
    assert html != null;
6440
65
    // No more processing required.
66
    return null;
41
    getHtmlPreviewPane().render( html );
42
    return html;
6743
  }
6844
69
  private HTMLPreviewPane getHtmlPreviewPane() {
45
  private HtmlPreview getHtmlPreviewPane() {
7046
    return sHtmlPreviewPane;
7147
  }
M src/main/java/com/keenwrite/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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors;
293
304
/**
315
 * Responsible for transforming a string into itself. This is used at the
326
 * end of a processing chain when no more processing is required.
337
 */
34
public class IdentityProcessor extends AbstractProcessor<String> {
8
public class IdentityProcessor extends ExecutorProcessor<String> {
359
  public static final IdentityProcessor INSTANCE = new IdentityProcessor();
3610
M src/main/java/com/keenwrite/processors/InlineRProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors;
293
30
import com.keenwrite.StatusBarNotifier;
31
import com.keenwrite.preferences.UserPreferences;
32
import javafx.beans.property.ObjectProperty;
33
import javafx.beans.property.StringProperty;
4
import com.keenwrite.preferences.Workspace;
5
import com.keenwrite.processors.markdown.MarkdownProcessor;
6
import com.vladsch.flexmark.ast.Paragraph;
7
import com.vladsch.flexmark.ast.Text;
8
import javafx.beans.property.Property;
349
3510
import javax.script.ScriptEngine;
...
4217
4318
import static com.keenwrite.Constants.STATUS_PARSE_ERROR;
19
import static com.keenwrite.StatusBarNotifier.clue;
20
import static com.keenwrite.preferences.Workspace.*;
4421
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
4522
import static com.keenwrite.sigils.RSigilOperator.PREFIX;
...
5532
   */
5633
  private static final int MAX_CACHED_R_STATEMENTS = 512;
34
35
  private final MarkdownProcessor mMarkdownProcessor;
5736
5837
  /**
5938
   * Where to put document inline evaluated R expressions.
6039
   */
61
  private final Map<String, Object> mEvalCache = new LinkedHashMap<>() {
40
  private final Map<String, String> mEvalCache = new LinkedHashMap<>() {
6241
    @Override
6342
    protected boolean removeEldestEntry(
64
        final Map.Entry<String, Object> eldest ) {
43
      final Map.Entry<String, String> eldest ) {
6544
      return size() > MAX_CACHED_R_STATEMENTS;
6645
    }
6746
  };
6847
69
  /**
70
   * Only one editor is open at a time.
71
   */
7248
  private static final ScriptEngine ENGINE =
73
      (new ScriptEngineManager()).getEngineByName( "Renjin" );
49
    (new ScriptEngineManager()).getEngineByName( "Renjin" );
7450
7551
  private static final int PREFIX_LENGTH = PREFIX.length();
7652
7753
  private final AtomicBoolean mDirty = new AtomicBoolean( false );
54
55
  private final Workspace mWorkspace;
7856
7957
  /**
8058
   * Constructs a processor capable of evaluating R statements.
8159
   *
8260
   * @param successor Subsequent link in the processing chain.
83
   * @param map       Resolved definitions map.
61
   * @param context   Contains resolved definitions map.
8462
   */
8563
  public InlineRProcessor(
86
      final Processor<String> successor,
87
      final Map<String, String> map ) {
88
    super( successor, map );
64
    final Processor<String> successor,
65
    final ProcessorContext context ) {
66
    super( successor, context );
67
68
    mWorkspace = context.getWorkspace();
69
    mMarkdownProcessor = MarkdownProcessor.create( context );
8970
9071
    bootstrapScriptProperty().addListener(
91
        ( ob, oldScript, newScript ) -> setDirty( true ) );
72
      ( __, oldScript, newScript ) -> setDirty( true ) );
9273
    workingDirectoryProperty().addListener(
93
        ( ob, oldScript, newScript ) -> setDirty( true ) );
74
      ( __, oldScript, newScript ) -> setDirty( true ) );
9475
95
    getUserPreferences().addSaveEventHandler( ( handler ) -> {
96
      if( isDirty() ) {
97
        init();
98
        setDirty( false );
99
      }
100
    } );
76
    // TODO: Watch the "R" property keys in the workspace, directly.
77
78
    // If the user saves the preferences, make sure that any R-related settings
79
    // changes are applied.
80
//    getWorkspace().addSaveEventHandler( ( handler ) -> {
81
//      if( isDirty() ) {
82
//        init();
83
//        setDirty( false );
84
//      }
85
//    } );
10186
10287
    init();
...
115100
      final var dir = wd.toString().replace( '\\', '/' );
116101
      final var map = getDefinitions();
117
      final var prefs = UserPreferences.getInstance();
118
      final var defBegan = prefs.getDefDelimiterBegan();
119
      final var defEnded = prefs.getDefDelimiterEnded();
102
      final var defBegan = mWorkspace.toString( KEY_DEF_DELIM_BEGAN );
103
      final var defEnded = mWorkspace.toString( KEY_DEF_DELIM_ENDED );
120104
121105
      map.put( defBegan + "application.r.working.directory" + defEnded, dir );
...
166150
167151
    while( currIndex >= 0 ) {
168
      // Copy everything up to, but not including, an R statement (`r#).
152
      // Copy everything up to, but not including, the opening token.
169153
      sb.append( text, prevIndex, currIndex );
170154
171155
      // Jump to the start of the R statement.
172156
      prevIndex = currIndex + PREFIX_LENGTH;
173157
174
      // Find the statement ending (`), without indexing past the text boundary.
158
      // Find the closing token, without indexing past the text boundary.
175159
      currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) );
176160
177161
      // Only evaluate inline R statements that have end delimiters.
178162
      if( currIndex > 1 ) {
179163
        // Extract the inline R statement to be evaluated.
180164
        final String r = text.substring( prevIndex, currIndex );
181165
182166
        // Pass the R statement into the R engine for evaluation.
183167
        try {
184
          final Object result = evalText( r );
168
          final var result = evalCached( r );
185169
186170
          // Append the string representation of the result into the text.
187171
          sb.append( result );
188
        } catch( final Exception e ) {
172
        } catch( final Exception ex ) {
173
          // Inform the user that there was a problem.
174
          clue( STATUS_PARSE_ERROR, ex.getMessage(), currIndex );
175
189176
          // If the string couldn't be parsed using R, append the statement
190177
          // that failed to parse, instead of its evaluated value.
191178
          sb.append( PREFIX ).append( r ).append( SUFFIX );
192
193
          // Tell the user that there was a problem.
194
          StatusBarNotifier.clue( STATUS_PARSE_ERROR,
195
                                  e.getMessage(),
196
                                  currIndex );
197179
        }
198180
...
216198
   * @return The object resulting from the evaluation.
217199
   */
218
  private Object evalText( final String r ) {
219
    return mEvalCache.computeIfAbsent( r, v -> eval( r ) );
200
  private String evalCached( final String r ) {
201
    return mEvalCache.computeIfAbsent( r, v -> evalHtml( r ) );
202
  }
203
204
  /**
205
   * Converts the given string to HTML, trimming new lines, and inlining
206
   * the text if it is a paragraph. Otherwise, the resulting HTML is most likely
207
   * complex (e.g., a Markdown table) and should be rendered as its HTML
208
   * equivalent.
209
   *
210
   * @param r The R expression to evaluate then convert to HTML.
211
   * @return The result from the R expression as an HTML element.
212
   */
213
  private String evalHtml( final String r ) {
214
    final var markdown = eval( r );
215
    var node = mMarkdownProcessor.toNode( markdown ).getFirstChild();
216
217
    if( node != null && node.isOrDescendantOfType( Paragraph.class ) ) {
218
      node = new Text( node.getChars() );
219
    }
220
221
    // Trimming prevents displaced commas and unwanted newlines.
222
    return mMarkdownProcessor.toHtml( node ).trim();
220223
  }
221224
222225
  /**
223226
   * Evaluate an R expression and return the resulting object.
224227
   *
225228
   * @param r The expression to evaluate.
226229
   * @return The object resulting from the evaluation.
227230
   */
228
  private Object eval( final String r ) {
231
  private String eval( final String r ) {
229232
    try {
230
      return getScriptEngine().eval( r );
233
      return ENGINE.eval( r ).toString();
231234
    } catch( final Exception ex ) {
232
      final String expr = r.substring( 0, min( r.length(), 30 ) );
233
      StatusBarNotifier.clue( "Main.status.error.r", expr, ex.getMessage() );
235
      final var expr = r.substring( 0, min( r.length(), 30 ) );
236
      clue( "Main.status.error.r", expr, ex.getMessage() );
237
      return "";
234238
    }
235
236
    return "";
237239
  }
238240
239241
  /**
240242
   * Return the given path if not {@code null}, otherwise return the path to
241243
   * the user's directory.
242244
   *
243245
   * @return A non-null path.
244246
   */
245247
  private Path getWorkingDirectory() {
246
    return getUserPreferences().getRDirectory().toPath();
248
    return workingDirectoryProperty().getValue().toPath();
247249
  }
248250
249
  private ObjectProperty<File> workingDirectoryProperty() {
250
    return getUserPreferences().rDirectoryProperty();
251
  private Property<File> workingDirectoryProperty() {
252
    return getWorkspace().fileProperty( KEY_R_DIR );
251253
  }
252254
253255
  /**
254256
   * Loads the R init script from the application's persisted preferences.
255257
   *
256258
   * @return A non-null string, possibly empty.
257259
   */
258260
  private String getBootstrapScript() {
259
    return getUserPreferences().getRScript();
260
  }
261
262
  private StringProperty bootstrapScriptProperty() {
263
    return getUserPreferences().rScriptProperty();
261
    return bootstrapScriptProperty().getValue();
264262
  }
265263
266
  private UserPreferences getUserPreferences() {
267
    return UserPreferences.getInstance();
264
  private Property<String> bootstrapScriptProperty() {
265
    return getWorkspace().valuesProperty( KEY_R_SCRIPT );
268266
  }
269267
270
  private ScriptEngine getScriptEngine() {
271
    return ENGINE;
268
  private Workspace getWorkspace() {
269
    return mWorkspace;
272270
  }
273271
}
M src/main/java/com/keenwrite/processors/PreformattedProcessor.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors;
293
304
/**
31
 * This is the default processor used when an unknown filename extension is
5
 * This is the default processor used when an unknown file name extension is
326
 * encountered. It processes the text by enclosing it in an HTML {@code <pre>}
337
 * element.
348
 */
35
public class PreformattedProcessor extends AbstractProcessor<String> {
9
public class PreformattedProcessor extends ExecutorProcessor<String> {
3610
3711
  /**
M src/main/java/com/keenwrite/processors/Processor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors;
293
4
import java.util.Optional;
305
import java.util.function.UnaryOperator;
316
327
/**
338
 * Responsible for processing documents from one known format to another.
349
 * Processes the given content providing a transformation from one document
35
 * format into another. For example, this could convert from XML to text using
36
 * an XSLT processor, or from markdown to HTML.
10
 * format into another. For example, this could convert Markdown to HTML.
3711
 *
38
 * @param <T> The type of processor to create.
12
 * @param <T> The data type to process.
3913
 */
4014
public interface Processor<T> extends UnaryOperator<T> {
41
42
  /**
43
   * Removes the given processor from the chain, returning a new immutable
44
   * chain equivalent to this chain, but without the given processor.
45
   *
46
   * @param processor The {@link Processor} to remove from the chain.
47
   * @return A delegating processor chain starting from this processor
48
   * onwards with the given processor removed from the chain.
49
   */
50
  Processor<T> remove( Class<? extends Processor<T>> processor );
5115
5216
  /**
53
   * Adds a document processor to call after this processor finishes processing
54
   * the document given to the process method.
17
   * Returns the next link in the processor chain.
5518
   *
56
   * @return The processor that should transform the document after this
57
   * instance has finished processing, or {@code null} if this is the last
58
   * processor in the chain.
19
   * @return The processor intended to transform the data after this instance
20
   * has finished processing, or {@link Optional#empty} if this is the last
21
   * link in the chain.
5922
   */
60
  default Processor<T> next() {
61
    return null;
23
  default Optional<Processor<T>> next() {
24
    return Optional.empty();
6225
  }
6326
}
M src/main/java/com/keenwrite/processors/ProcessorContext.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors;
293
4
import com.keenwrite.Constants;
305
import com.keenwrite.ExportFormat;
31
import com.keenwrite.FileEditorTab;
32
import com.keenwrite.FileType;
33
import com.keenwrite.preview.HTMLPreviewPane;
34
import com.keenwrite.processors.markdown.CaretPosition;
6
import com.keenwrite.io.FileType;
7
import com.keenwrite.preferences.Workspace;
8
import com.keenwrite.preview.HtmlPreview;
9
import com.keenwrite.processors.markdown.Caret;
3510
3611
import java.nio.file.Path;
3712
import java.util.Map;
3813
3914
import static com.keenwrite.AbstractFileFactory.lookup;
15
import static com.keenwrite.Constants.DEFAULT_DIRECTORY;
4016
4117
/**
4218
 * Provides a context for configuring a chain of {@link Processor} instances.
4319
 */
4420
public class ProcessorContext {
45
  private final HTMLPreviewPane mPreviewPane;
21
  private final HtmlPreview mHtmlPreview;
4622
  private final Map<String, String> mResolvedMap;
23
  private final Path mPath;
24
  private final Caret mCaret;
4725
  private final ExportFormat mExportFormat;
48
  private final FileEditorTab mTab;
26
  private final Workspace mWorkspace;
4927
5028
  /**
5129
   * Creates a new context for use by the {@link ProcessorFactory} when
5230
   * instantiating new {@link Processor} instances. Although all the
5331
   * parameters are required, not all {@link Processor} instances will use
5432
   * all parameters.
5533
   *
56
   * @param previewPane Where to display the final (HTML) output.
57
   * @param resolvedMap Fully expanded interpolated strings.
58
   * @param tab         Tab containing path to the document to process.
59
   * @param format      Indicate configuration options for export format.
34
   * @param htmlPreview  Where to display the final (HTML) output.
35
   * @param resolvedMap  Fully expanded interpolated strings.
36
   * @param path         Path to the document to process.
37
   * @param caret        Location of the caret in the edited document, which is
38
   *                     used to synchronize the scrollbars.
39
   * @param exportFormat Indicate configuration options for export format.
6040
   */
6141
  public ProcessorContext(
62
      final HTMLPreviewPane previewPane,
42
      final HtmlPreview htmlPreview,
6343
      final Map<String, String> resolvedMap,
64
      final FileEditorTab tab,
65
      final ExportFormat format ) {
66
    assert previewPane != null;
44
      final Path path,
45
      final Caret caret,
46
      final ExportFormat exportFormat,
47
      final Workspace workspace ) {
48
    assert htmlPreview != null;
6749
    assert resolvedMap != null;
68
    assert tab != null;
69
    assert format != null;
50
    assert path != null;
51
    assert caret != null;
52
    assert exportFormat != null;
53
    assert workspace != null;
7054
71
    mPreviewPane = previewPane;
55
    mHtmlPreview = htmlPreview;
7256
    mResolvedMap = resolvedMap;
73
    mTab = tab;
74
    mExportFormat = format;
57
    mPath = path;
58
    mCaret = caret;
59
    mExportFormat = exportFormat;
60
    mWorkspace = workspace;
7561
  }
7662
7763
  @SuppressWarnings("SameParameterValue")
7864
  boolean isExportFormat( final ExportFormat format ) {
7965
    return mExportFormat == format;
8066
  }
8167
82
  HTMLPreviewPane getPreviewPane() {
83
    return mPreviewPane;
68
  HtmlPreview getPreview() {
69
    return mHtmlPreview;
8470
  }
8571
72
  /**
73
   * Returns the variable map of interpolated definitions.
74
   *
75
   * @return A map to help dereference variables.
76
   */
8677
  Map<String, String> getResolvedMap() {
8778
    return mResolvedMap;
...
9889
   * @return Caret position in the document.
9990
   */
100
  public CaretPosition getCaretPosition() {
101
    return mTab.getCaretPosition();
91
  public Caret getCaret() {
92
    return mCaret;
93
  }
94
95
  /**
96
   * Returns the directory that contains the file being edited.
97
   * When {@link Constants#DOCUMENT_DEFAULT} is created, the parent path is
98
   * {@code null}. This will get absolute path to the file before trying to
99
   * get te parent path, which should always be a valid path. In the unlikely
100
   * event that the base path cannot be determined by the path alone, the
101
   * default user directory is returned. This is necessary for the creation
102
   * of new files.
103
   *
104
   * @return Path to the directory containing a file being edited, or the
105
   * default user directory if the base path cannot be determined.
106
   */
107
  public Path getBasePath() {
108
    final var path = getPath().toAbsolutePath().getParent();
109
    return path == null ? DEFAULT_DIRECTORY : path;
102110
  }
103111
104112
  public Path getPath() {
105
    return mTab.getPath();
113
    return mPath;
106114
  }
107115
108116
  FileType getFileType() {
109117
    return lookup( getPath() );
118
  }
119
120
  public Workspace getWorkspace() {
121
    return mWorkspace;
110122
  }
111123
}
M src/main/java/com/keenwrite/processors/ProcessorFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors;
293
304
import com.keenwrite.AbstractFileFactory;
31
import com.keenwrite.preview.HTMLPreviewPane;
5
import com.keenwrite.preview.HtmlPreview;
326
import com.keenwrite.processors.markdown.MarkdownProcessor;
33
34
import java.nio.file.Path;
35
import java.util.Map;
367
378
import static com.keenwrite.ExportFormat.NONE;
389
3910
/**
4011
 * Responsible for creating processors capable of parsing, transforming,
4112
 * interpolating, and rendering known file types.
4213
 */
4314
public class ProcessorFactory extends AbstractFileFactory {
4415
45
  private final ProcessorContext mProcessorContext;
16
  private final ProcessorContext mContext;
4617
4718
  /**
4819
   * Constructs a factory with the ability to create processors that can perform
4920
   * text and caret processing to generate a final preview.
5021
   *
51
   * @param processorContext Parameters needed to construct various processors.
22
   * @param context Parameters needed to construct various processors.
5223
   */
53
  private ProcessorFactory( final ProcessorContext processorContext ) {
54
    mProcessorContext = processorContext;
24
  private ProcessorFactory( final ProcessorContext context ) {
25
    mContext = context;
5526
  }
5627
5728
  private Processor<String> createProcessor() {
58
    final ProcessorContext context = getProcessorContext();
29
    final var context = getProcessorContext();
5930
6031
    // If the content is not to be exported, then the successor processor
...
6839
    // to SVG. Without conversion would require client-side rendering of
6940
    // math (such as using the JavaScript-based KaTeX engine).
70
    final Processor<String> successor = context.isExportFormat( NONE )
71
        ? createHtmlPreviewProcessor()
72
        : createIdentityProcessor();
41
    final var successor = context.isExportFormat( NONE )
42
      ? createHtmlPreviewProcessor()
43
      : createIdentityProcessor();
7344
74
    return switch( context.getFileType() ) {
45
    final var processor = switch( context.getFileType() ) {
7546
      case RMARKDOWN -> createRProcessor( successor );
7647
      case SOURCE -> createMarkdownProcessor( successor );
7748
      case RXML -> createRXMLProcessor( successor );
7849
      case XML -> createXMLProcessor( successor );
7950
      default -> createPreformattedProcessor( successor );
8051
    };
52
53
    return new ExecutorProcessor<>( processor );
8154
  }
8255
...
8962
   */
9063
  public static Processor<String> createProcessors(
91
      final ProcessorContext context ) {
64
    final ProcessorContext context ) {
9265
    return new ProcessorFactory( context ).createProcessor();
93
  }
94
95
  /**
96
   * Executes the processing chain, operating on the given string.
97
   *
98
   * @param handler The first processor in the chain to call.
99
   * @param text    The initial value of the text to process.
100
   * @return The final value of the text that was processed by the chain.
101
   */
102
  public static String processChain( Processor<String> handler, String text ) {
103
    while( handler != null && text != null ) {
104
      text = handler.apply( text );
105
      handler = handler.next();
106
    }
107
108
    return text;
10966
  }
11067
...
13794
   */
13895
  private Processor<String> createMarkdownProcessor(
139
      final Processor<String> successor ) {
96
    final Processor<String> successor ) {
14097
    final var dp = createDefinitionProcessor( successor );
14198
    return MarkdownProcessor.create( dp, getProcessorContext() );
14299
  }
143100
144101
  private Processor<String> createDefinitionProcessor(
145
      final Processor<String> successor ) {
146
    return new DefinitionProcessor( successor, getResolvedMap() );
102
    final Processor<String> successor ) {
103
    return new DefinitionProcessor( successor, getProcessorContext() );
147104
  }
148105
149106
  private Processor<String> createRProcessor(
150
      final Processor<String> successor ) {
151
    final var irp = new InlineRProcessor( successor, getResolvedMap() );
152
    final var rvp = new RVariableProcessor( irp, getResolvedMap() );
107
    final Processor<String> successor ) {
108
    final var irp = new InlineRProcessor( successor, getProcessorContext() );
109
    final var rvp = new RVariableProcessor( irp, getProcessorContext() );
153110
    return MarkdownProcessor.create( rvp, getProcessorContext() );
154111
  }
155112
156113
  protected Processor<String> createRXMLProcessor(
157
      final Processor<String> successor ) {
158
    final var xmlp = new XmlProcessor( successor, getPath() );
114
    final Processor<String> successor ) {
115
    final var xmlp = new XmlProcessor( successor, getProcessorContext() );
159116
    return createRProcessor( xmlp );
160117
  }
161118
162119
  private Processor<String> createXMLProcessor(
163
      final Processor<String> successor ) {
164
    final var xmlp = new XmlProcessor( successor, getPath() );
120
    final Processor<String> successor ) {
121
    final var xmlp = new XmlProcessor( successor, getProcessorContext() );
165122
    return createDefinitionProcessor( xmlp );
166123
  }
167124
168125
  private Processor<String> createPreformattedProcessor(
169
      final Processor<String> successor ) {
126
    final Processor<String> successor ) {
170127
    return new PreformattedProcessor( successor );
171128
  }
172129
173130
  private ProcessorContext getProcessorContext() {
174
    return mProcessorContext;
175
  }
176
177
  private HTMLPreviewPane getPreviewPane() {
178
    return getProcessorContext().getPreviewPane();
179
  }
180
181
  /**
182
   * Returns the variable map of interpolated definitions.
183
   *
184
   * @return A map to help dereference variables.
185
   */
186
  private Map<String, String> getResolvedMap() {
187
    return getProcessorContext().getResolvedMap();
131
    return mContext;
188132
  }
189133
190
  /**
191
   * Returns the {@link Path} from the {@link ProcessorContext}.
192
   *
193
   * @return A non-null {@link Path} instance.
194
   */
195
  private Path getPath() {
196
    return getProcessorContext().getPath();
134
  private HtmlPreview getPreviewPane() {
135
    return getProcessorContext().getPreview();
197136
  }
198137
}
M src/main/java/com/keenwrite/processors/RVariableProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors;
293
4
import com.keenwrite.preferences.Workspace;
305
import com.keenwrite.sigils.RSigilOperator;
6
import com.keenwrite.sigils.SigilOperator;
7
import com.keenwrite.sigils.YamlSigilOperator;
318
329
import java.util.HashMap;
3310
import java.util.Map;
11
12
import static com.keenwrite.preferences.Workspace.*;
3413
3514
/**
3615
 * Converts the keys of the resolved map from default form to R form, then
3716
 * performs a substitution on the text. The default R variable syntax is
3817
 * {@code v$tree$leaf}.
3918
 */
4019
public class RVariableProcessor extends DefinitionProcessor {
20
21
  private final SigilOperator mSigilOperator;
4122
4223
  public RVariableProcessor(
43
      final Processor<String> rp, final Map<String, String> map ) {
44
    super( rp, map );
24
    final InlineRProcessor irp, final ProcessorContext context ) {
25
    super( irp, context );
26
    mSigilOperator = createSigilOperator( context.getWorkspace() );
4527
  }
4628
...
6648
    for( final var entry : map.entrySet() ) {
6749
      final var key = entry.getKey();
68
      rMap.put( RSigilOperator.entoken( key ), toRValue( map.get( key ) ) );
50
      rMap.put( mSigilOperator.entoken( key ), toRValue( map.get( key ) ) );
6951
    }
7052
...
8668
  @SuppressWarnings("SameParameterValue")
8769
  private String escape(
88
      final String haystack, final char needle, final String thread ) {
70
    final String haystack, final char needle, final String thread ) {
8971
    int end = haystack.indexOf( needle );
9072
...
9779
9880
    // Replace up to 32 occurrences before the string reallocates its buffer.
99
    final StringBuilder sb = new StringBuilder( length + 32 );
81
    final var sb = new StringBuilder( length + 32 );
10082
10183
    while( end >= 0 ) {
10284
      sb.append( haystack, start, end ).append( thread );
10385
      start = end + 1;
10486
      end = haystack.indexOf( needle, start );
10587
    }
10688
10789
    return sb.append( haystack.substring( start ) ).toString();
90
  }
91
92
  private SigilOperator createSigilOperator( final Workspace workspace ) {
93
    final var tokens = workspace.toTokens(
94
      KEY_R_DELIM_BEGAN, KEY_R_DELIM_ENDED );
95
    final var antecedent = createDefinitionOperator( workspace );
96
    return new RSigilOperator( tokens, antecedent );
97
  }
98
99
  private SigilOperator createDefinitionOperator(
100
    final Workspace workspace ) {
101
    final var tokens = workspace.toTokens(
102
      KEY_DEF_DELIM_BEGAN, KEY_DEF_DELIM_ENDED );
103
    return new YamlSigilOperator( tokens );
108104
  }
109105
}
M src/main/java/com/keenwrite/processors/XmlProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors;
293
...
3711
import javax.xml.stream.XMLStreamException;
3812
import javax.xml.stream.events.ProcessingInstruction;
39
import javax.xml.stream.events.XMLEvent;
4013
import javax.xml.transform.*;
4114
import javax.xml.transform.stream.StreamResult;
4215
import javax.xml.transform.stream.StreamSource;
43
import java.io.File;
4416
import java.io.Reader;
4517
import java.io.StringReader;
...
6133
 * </p>
6234
 */
63
public class XmlProcessor extends AbstractProcessor<String>
35
public class XmlProcessor extends ExecutorProcessor<String>
6436
    implements ErrorListener {
6537
66
  private final Snitch snitch = Services.load( Snitch.class );
38
  private final Snitch mSnitch = Services.load( Snitch.class );
6739
68
  private XMLInputFactory xmlInputFactory;
69
  private TransformerFactory transformerFactory;
70
  private Transformer transformer;
40
  private final XMLInputFactory mXmlInputFactory =
41
      XMLInputFactory.newInstance();
42
  private final TransformerFactory mTransformerFactory =
43
      new TransformerFactoryImpl();
44
  private Transformer mTransformer;
7145
72
  private Path path;
46
  private final Path mPath;
7347
7448
  /**
7549
   * Constructs an XML processor that can transform an XML document into another
7650
   * format based on the XSL file specified as a processing instruction. The
7751
   * path must point to the directory where the XSL file is found, which implies
7852
   * that they must be in the same directory.
7953
   *
80
   * @param processor Next link in the processing chain.
81
   * @param path      The path to the XML file content to be processed.
54
   * @param successor Next link in the processing chain.
55
   * @param context   Contains path to the XML file content to be processed.
8256
   */
83
  public XmlProcessor( final Processor<String> processor, final Path path ) {
84
    super( processor );
85
    setPath( path );
57
  public XmlProcessor(
58
      final Processor<String> successor,
59
      final ProcessorContext context ) {
60
    super( successor );
61
    mPath = context.getPath();
62
63
    // Bubble problems up to the user interface, rather than standard error.
64
    mTransformerFactory.setErrorListener( this );
8665
  }
8766
...
11998
12099
      // Listen for external file modification events.
121
      getSnitch().listen( xsl );
100
      mSnitch.listen( xsl );
122101
123102
      getTransformer( xsl ).transform(
...
141120
   *                                           transformer.
142121
   */
143
  private Transformer getTransformer( final Path xsl )
122
  private synchronized Transformer getTransformer( final Path xsl )
144123
      throws TransformerConfigurationException {
145
    if( this.transformer == null ) {
146
      this.transformer = createTransformer( xsl );
124
    if( mTransformer == null ) {
125
      mTransformer = createTransformer( xsl );
147126
    }
148127
149
    return this.transformer;
128
    return mTransformer;
150129
  }
151130
...
160139
  protected Transformer createTransformer( final Path xsl )
161140
      throws TransformerConfigurationException {
162
    final Source xslt = new StreamSource( xsl.toFile() );
141
    final var xslt = new StreamSource( xsl.toFile() );
163142
164143
    return getTransformerFactory().newTransformer( xslt );
165144
  }
166145
167146
  private Path getXslPath( final String filename ) {
168
    final Path xmlPath = getPath();
169
    final File xmlDirectory = xmlPath.toFile().getParentFile();
147
    final var xmlDirectory = mPath.toFile().getParentFile();
170148
171149
    return Paths.get( xmlDirectory.getPath(), filename );
172150
  }
173151
174152
  /**
175153
   * Given XML text, this will use a StAX pull reader to obtain the XML
176154
   * stylesheet processing instruction. This will throw a parse exception if the
177
   * href pseudo-attribute filename value cannot be found.
155
   * href pseudo-attribute file name value cannot be found.
178156
   *
179157
   * @param xml The XML containing an xml-stylesheet processing instruction.
180158
   * @return The href pseudo-attribute value.
181159
   * @throws XMLStreamException Could not parse the XML file.
182160
   */
183161
  private String getXsltFilename( final String xml )
184162
      throws XMLStreamException, XPathException {
185
186163
    String result = "";
187164
188165
    try( final StringReader sr = new StringReader( xml ) ) {
166
      final XMLEventReader reader = createXmlEventReader( sr );
189167
      boolean found = false;
190168
      int count = 0;
191
      final XMLEventReader reader = createXMLEventReader( sr );
192169
193170
      // If the processing instruction wasn't found in the first 10 lines,
194171
      // fail fast. This should iterate twice through the loop.
195172
      while( !found && reader.hasNext() && count++ < 10 ) {
196
        final XMLEvent event = reader.nextEvent();
173
        final var event = reader.nextEvent();
197174
198175
        if( event.isProcessingInstruction() ) {
199
          final ProcessingInstruction pi = (ProcessingInstruction) event;
200
          final String target = pi.getTarget();
176
          final var pi = (ProcessingInstruction) event;
177
          final var target = pi.getTarget();
201178
202179
          if( "xml-stylesheet".equalsIgnoreCase( target ) ) {
...
211188
  }
212189
213
  private XMLEventReader createXMLEventReader( final Reader reader )
190
  private XMLEventReader createXmlEventReader( final Reader reader )
214191
      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();
192
    return mXmlInputFactory.createXMLEventReader( reader );
228193
  }
229194
230195
  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;
196
    return mTransformerFactory;
250197
  }
251198
...
279226
  public void fatalError( final TransformerException ex ) {
280227
    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;
293228
  }
294229
}
A src/main/java/com/keenwrite/processors/markdown/Caret.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown;
3
4
import com.keenwrite.util.GenericBuilder;
5
import javafx.beans.value.ObservableValue;
6
import org.fxmisc.richtext.StyleClassedTextArea;
7
import org.fxmisc.richtext.model.Paragraph;
8
import org.reactfx.collection.LiveList;
9
10
import java.util.Collection;
11
12
import static com.keenwrite.Constants.STATUS_BAR_LINE;
13
import static com.keenwrite.Messages.get;
14
15
/**
16
 * Represents the absolute, relative, and maximum position of the caret. The
17
 * caret position is a character offset into the text.
18
 */
19
public class Caret {
20
21
  public static GenericBuilder<Caret.Mutator, Caret> builder() {
22
    return GenericBuilder.of( Caret.Mutator::new, Caret::new );
23
  }
24
25
  /**
26
   * Used for building a new {@link Caret} instance.
27
   */
28
  public static class Mutator {
29
    /**
30
     * Caret's current paragraph index (i.e., current caret line number).
31
     */
32
    private ObservableValue<Integer> mParagraph;
33
34
    /**
35
     * Used to count the number of lines in the text editor document.
36
     */
37
    private LiveList<Paragraph<Collection<String>, String,
38
        Collection<String>>> mParagraphs;
39
40
    /**
41
     * Caret offset into the full text, represented as a string index.
42
     */
43
    private ObservableValue<Integer> mTextOffset;
44
45
    /**
46
     * Caret offset into the current paragraph, represented as a string index.
47
     */
48
    private ObservableValue<Integer> mParaOffset;
49
50
    /**
51
     * Total number of characters in the document.
52
     */
53
    private ObservableValue<Integer> mTextLength;
54
55
    /**
56
     * Configures this caret position using properties from the given editor.
57
     *
58
     * @param editor The text editor that has a caret with position properties.
59
     */
60
    public void setEditor( final StyleClassedTextArea editor ) {
61
      mParagraph = editor.currentParagraphProperty();
62
      mParagraphs = editor.getParagraphs();
63
      mParaOffset = editor.caretColumnProperty();
64
      mTextOffset = editor.caretPositionProperty();
65
      mTextLength = editor.lengthProperty();
66
    }
67
  }
68
69
  private final Mutator mMutator;
70
71
  /**
72
   * Force using the builder pattern.
73
   */
74
  private Caret( final Mutator mutator ) {
75
    assert mutator != null;
76
77
    mMutator = mutator;
78
  }
79
80
  /**
81
   * Allows observers to be notified when the value of the caret changes.
82
   *
83
   * @return An observer for the caret's document offset.
84
   */
85
  public ObservableValue<Integer> textOffsetProperty() {
86
    return mMutator.mTextOffset;
87
  }
88
89
  /**
90
   * Answers whether the caret's offset into the text is between the given
91
   * offsets.
92
   *
93
   * @param began Starting value compared against the caret's text offset.
94
   * @param ended Ending value compared against the caret's text offset.
95
   * @return {@code true} when the caret's text offset is between the given
96
   * values, inclusively (for either value).
97
   */
98
  public boolean isBetweenText( final int began, final int ended ) {
99
    final var offset = getTextOffset();
100
    return began <= offset && offset <= ended;
101
  }
102
103
  /**
104
   * Answers whether the caret's offset into the paragraph is before the given
105
   * offset.
106
   *
107
   * @param offset Compared against the caret's paragraph offset.
108
   * @return {@code true} the caret's offset is before the given offset.
109
   */
110
  public boolean isBeforeColumn( final int offset ) {
111
    return getParaOffset() < offset;
112
  }
113
114
  /**
115
   * Answers whether the caret's offset into the text is before the given
116
   * text offset.
117
   *
118
   * @param offset Compared against the caret's text offset.
119
   * @return {@code true} the caret's offset is after the given offset.
120
   */
121
  public boolean isAfterColumn( final int offset ) {
122
    return getParaOffset() > offset;
123
  }
124
125
  /**
126
   * Answers whether the caret's offset into the text exceeds the length of
127
   * the text.
128
   *
129
   * @return {@code true} when the caret is at the end of the text boundary.
130
   */
131
  public boolean isAfterText() {
132
    return getTextOffset() >= getTextLength();
133
  }
134
135
  public boolean isAfter( final int offset ) {
136
    return offset >= getTextOffset();
137
  }
138
139
  private int getParagraph() {
140
    return mMutator.mParagraph.getValue();
141
  }
142
143
  /**
144
   * Returns the number of lines in the text editor.
145
   *
146
   * @return The size of the text editor's paragraph list plus one.
147
   */
148
  private int getParagraphCount() {
149
    return mMutator.mParagraphs.size() + 1;
150
  }
151
152
  /**
153
   * Returns the absolute position of the caret within the entire document.
154
   *
155
   * @return A zero-based index of the caret position.
156
   */
157
  private int getTextOffset() {
158
    return mMutator.mTextOffset.getValue();
159
  }
160
161
  /**
162
   * Returns the position of the caret within the current paragraph being
163
   * edited.
164
   *
165
   * @return A zero-based index of the caret position relative to the
166
   * current paragraph.
167
   */
168
  private int getParaOffset() {
169
    return mMutator.mParaOffset.getValue();
170
  }
171
172
  /**
173
   * Returns the total number of characters in the document being edited.
174
   *
175
   * @return A zero-based count of the total characters in the document.
176
   */
177
  private int getTextLength() {
178
    return mMutator.mTextLength.getValue();
179
  }
180
181
  /**
182
   * Returns a human-readable string that shows the current caret position
183
   * within the text. Typically this will include the current line number,
184
   * the number of lines, and the character offset into the text.
185
   *
186
   * @return A string to present to an end user.
187
   */
188
  @Override
189
  public String toString() {
190
    return get( STATUS_BAR_LINE,
191
                getParagraph() + 1,
192
                getParagraphCount(),
193
                getTextOffset() + 1 );
194
  }
195
}
1196
M src/main/java/com/keenwrite/processors/markdown/CaretExtension.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors.markdown;
293
...
4923
 */
5024
public class CaretExtension implements HtmlRendererExtension {
25
26
  private final Caret mCaret;
27
28
  private CaretExtension( final Caret caret ) {
29
    mCaret = caret;
30
  }
31
32
  public static CaretExtension create( final Caret caret ) {
33
    return new CaretExtension( caret );
34
  }
35
36
  @Override
37
  public void extend(
38
      final Builder builder, @NotNull final String rendererType ) {
39
    builder.attributeProviderFactory(
40
        IdAttributeProvider.createFactory( mCaret ) );
41
  }
42
43
  @Override
44
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
45
  }
5146
5247
  /**
5348
   * Responsible for creating the id attribute. This class is instantiated
5449
   * once: for the HTML element containing the {@link Constants#CARET_ID}.
5550
   */
5651
  public static class IdAttributeProvider implements AttributeProvider {
57
    private final CaretPosition mCaret;
52
    private final Caret mCaret;
5853
59
    public IdAttributeProvider( final CaretPosition caret ) {
54
    public IdAttributeProvider( final Caret caret ) {
6055
      mCaret = caret;
6156
    }
6257
6358
    private static AttributeProviderFactory createFactory(
64
        final CaretPosition caret ) {
59
      final Caret caret ) {
6560
      return new IndependentAttributeProviderFactory() {
6661
        @Override
6762
        public @NotNull AttributeProvider apply(
68
            @NotNull final LinkResolverContext context ) {
63
          @NotNull final LinkResolverContext context ) {
6964
          return new IdAttributeProvider( caret );
7065
        }
7166
      };
7267
    }
7368
7469
    @Override
7570
    public void setAttributes( @NotNull Node curr,
7671
                               @NotNull AttributablePart part,
7772
                               @NotNull MutableAttributes attributes ) {
73
      final var outside = mCaret.isAfterText() ? 1 : 0;
7874
      final var began = curr.getStartOffset();
79
      final var ended = curr.getEndOffset();
75
      final var ended = curr.getEndOffset() + outside;
8076
      final var prev = curr.getPrevious();
8177
8278
      // If the caret is within the bounds of the current node or the
8379
      // caret is within the bounds of the end of the previous node and
8480
      // the start of the current node, then mark the current node with
8581
      // a caret indicator.
8682
      if( mCaret.isBetweenText( began, ended ) ||
87
          prev != null && mCaret.isBetweenText( prev.getEndOffset(), began ) ) {
88
89
        // This magic line enables synchronizing the text editor with preview.
83
        prev != null && mCaret.isBetweenText( prev.getEndOffset(), began ) ) {
84
        // This line empowers synchronizing the text editor with the preview.
9085
        attributes.addValue( AttributeImpl.of( "id", CARET_ID ) );
9186
      }
9287
    }
93
  }
94
95
  private final CaretPosition mCaret;
96
97
  private CaretExtension( final CaretPosition caret ) {
98
    mCaret = caret;
99
  }
100
101
  @Override
102
  public void extend(
103
      final Builder builder, @NotNull final String rendererType ) {
104
    builder.attributeProviderFactory(
105
        IdAttributeProvider.createFactory( mCaret ) );
106
  }
107
108
  public static CaretExtension create( final CaretPosition caret ) {
109
    return new CaretExtension( caret );
110
  }
111
112
  @Override
113
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
11488
  }
11589
}
D src/main/java/com/keenwrite/processors/markdown/CaretPosition.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.keenwrite.processors.markdown;
29
30
import com.keenwrite.util.GenericBuilder;
31
import javafx.beans.value.ObservableValue;
32
import org.fxmisc.richtext.model.Paragraph;
33
import org.reactfx.collection.LiveList;
34
35
import java.util.Collection;
36
37
import static com.keenwrite.Constants.STATUS_BAR_LINE;
38
import static com.keenwrite.Messages.get;
39
40
/**
41
 * Represents the absolute, relative, and maximum position of the caret.
42
 * The caret position is a character offset into the text.
43
 */
44
public class CaretPosition {
45
46
  public static GenericBuilder<CaretPosition.Mutator, CaretPosition> builder() {
47
    return GenericBuilder.of( CaretPosition.Mutator::new, CaretPosition::new );
48
  }
49
50
  /**
51
   * Used for building a new {@link CaretPosition} instance.
52
   */
53
  public static class Mutator {
54
    /**
55
     * Caret's current paragraph index (i.e., current caret line number).
56
     */
57
    private ObservableValue<Integer> mParagraph;
58
59
    /**
60
     * Used to count the number of lines in the text editor document.
61
     */
62
    private LiveList<Paragraph<Collection<String>, String,
63
        Collection<String>>> mParagraphs;
64
65
    /**
66
     * Caret offset into the full text, represented as a string index.
67
     */
68
    private ObservableValue<Integer> mTextOffset;
69
70
    /**
71
     * Caret offset into the current paragraph, represented as a string index.
72
     */
73
    private ObservableValue<Integer> mParaOffset;
74
75
    public void setParagraph( final ObservableValue<Integer> paragraph ) {
76
      mParagraph = paragraph;
77
    }
78
79
    public void setParagraphs(
80
        final LiveList<Paragraph<Collection<String>, String,
81
            Collection<String>>> paragraphs ) {
82
      mParagraphs = paragraphs;
83
    }
84
85
    public void setTextOffset( final ObservableValue<Integer> textOffset ) {
86
      mTextOffset = textOffset;
87
    }
88
89
    public void setParaOffset( final ObservableValue<Integer> paraOffset ) {
90
      mParaOffset = paraOffset;
91
    }
92
  }
93
94
  private final Mutator mMutator;
95
96
  /**
97
   * Force using the builder pattern.
98
   */
99
  private CaretPosition( final Mutator mutator ) {
100
    mMutator = mutator;
101
  }
102
103
  /**
104
   * Answers whether the caret's offset into the text is between the given
105
   * offsets.
106
   *
107
   * @param began Starting value compared against the caret's text offset.
108
   * @param ended Ending value compared against the caret's text offset.
109
   * @return {@code true} when the caret's text offset is between the given
110
   * values, inclusively (for either value).
111
   */
112
  public boolean isBetweenText( final int began, final int ended ) {
113
    final int offset = getTextOffset();
114
    return began <= offset && offset <= ended;
115
  }
116
117
  /**
118
   * Answers whether the caret's offset into the paragraph is before the given
119
   * offset.
120
   *
121
   * @param offset Compared against the caret's paragraph offset.
122
   * @return {@code true} the caret's offset is before the given offset.
123
   */
124
  public boolean isBeforeColumn( final int offset ) {
125
    return getParaOffset() < offset;
126
  }
127
128
  /**
129
   * Answers whether the caret's offset into the text is before the given
130
   * text offset.
131
   *
132
   * @param offset Compared against the caret's text offset.
133
   * @return {@code true} the caret's offset is after the given offset.
134
   */
135
  public boolean isAfterColumn( final int offset ) {
136
    return getParaOffset() > offset;
137
  }
138
139
  private int getParagraph() {
140
    return mMutator.mParagraph.getValue();
141
  }
142
143
  /**
144
   * Returns the number of lines in the text editor.
145
   *
146
   * @return The size of the text editor's paragraph list plus one.
147
   */
148
  private int getParagraphCount() {
149
    return mMutator.mParagraphs.size() + 1;
150
  }
151
152
  /**
153
   * Returns the absolute position of the caret within the entire document.
154
   *
155
   * @return A zero-based index of the caret position.
156
   */
157
  private int getTextOffset() {
158
    return mMutator.mTextOffset.getValue();
159
  }
160
161
  /**
162
   * Returns the position of the caret within the current paragraph being
163
   * edited.
164
   *
165
   * @return A zero-based index of the caret position relative to the
166
   * current paragraph.
167
   */
168
  private int getParaOffset() {
169
    return mMutator.mParaOffset.getValue();
170
  }
171
172
  /**
173
   * Returns a human-readable string that shows the current caret position
174
   * within the text. Typically this will include the current line number,
175
   * the number of lines, and the character offset into the text.
176
   *
177
   * @return A string to present to an end user.
178
   */
179
  @Override
180
  public String toString() {
181
    return get( STATUS_BAR_LINE,
182
                getParagraph(),
183
                getParagraphCount(),
184
                getTextOffset() );
185
  }
186
}
1871
A src/main/java/com/keenwrite/processors/markdown/FencedBlockExtension.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown;
3
4
import com.keenwrite.processors.DefinitionProcessor;
5
import com.keenwrite.processors.IdentityProcessor;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.vladsch.flexmark.ast.FencedCodeBlock;
8
import com.vladsch.flexmark.html.renderer.DelegatingNodeRendererFactory;
9
import com.vladsch.flexmark.html.renderer.NodeRenderer;
10
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
11
import com.vladsch.flexmark.util.data.DataHolder;
12
import com.vladsch.flexmark.util.data.MutableDataHolder;
13
import com.vladsch.flexmark.util.sequence.BasedSequence;
14
import org.jetbrains.annotations.NotNull;
15
16
import java.io.ByteArrayOutputStream;
17
import java.util.HashSet;
18
import java.util.Set;
19
import java.util.zip.Deflater;
20
21
import static com.keenwrite.StatusBarNotifier.clue;
22
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
23
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
24
import static com.vladsch.flexmark.html.renderer.LinkType.LINK;
25
import static java.lang.String.format;
26
import static java.util.Base64.getUrlEncoder;
27
import static java.util.zip.Deflater.BEST_COMPRESSION;
28
import static java.util.zip.Deflater.FULL_FLUSH;
29
30
/**
31
 * Responsible for converting textual diagram descriptions into HTML image
32
 * elements.
33
 */
34
public class FencedBlockExtension implements HtmlRendererExtension {
35
  private final static String DIAGRAM_STYLE = "diagram-";
36
  private final static int DIAGRAM_STYLE_LEN = DIAGRAM_STYLE.length();
37
38
  private final DefinitionProcessor mProcessor;
39
40
  public FencedBlockExtension( final ProcessorContext context ) {
41
    assert context != null;
42
    mProcessor = new DefinitionProcessor( IdentityProcessor.INSTANCE, context );
43
  }
44
45
  /**
46
   * Creates a new parser for fenced blocks. This calls out to a web service
47
   * to generate SVG files of text diagrams.
48
   * <p>
49
   * Internally, this creates a {@link DefinitionProcessor} to substitute
50
   * variable definitions. This is necessary because the order of processors
51
   * matters. If the {@link DefinitionProcessor} comes before an instance of
52
   * {@link MarkdownProcessor}, for example, then the caret position in the
53
   * preview pane will not align with the caret position in the editor
54
   * pane. The {@link MarkdownProcessor} must come before all else. However,
55
   * when parsing fenced blocks, the variables within the block must be
56
   * interpolated before being sent to the diagram web service.
57
   * </p>
58
   *
59
   * @param context Used to create a new {@link DefinitionProcessor}.
60
   * @return A new {@link FencedBlockExtension} capable of shunting ASCII
61
   * diagrams to a service for conversion to SVG.
62
   */
63
  public static FencedBlockExtension create( final ProcessorContext context ) {
64
    return new FencedBlockExtension( context );
65
  }
66
67
  @Override
68
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
69
  }
70
71
  @Override
72
  public void extend(
73
    @NotNull final Builder builder, @NotNull final String rendererType ) {
74
    builder.nodeRendererFactory( new Factory() );
75
  }
76
77
  /**
78
   * Converts the given {@link BasedSequence} to a lowercase value.
79
   *
80
   * @param text The character string to convert to lowercase.
81
   * @return The lowercase text value, or the empty string for no text.
82
   */
83
  private static String sanitize( final BasedSequence text ) {
84
    assert text != null;
85
    return text.toString().toLowerCase();
86
  }
87
88
  private class CustomRenderer implements NodeRenderer {
89
90
    @Override
91
    public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
92
      final var set = new HashSet<NodeRenderingHandler<?>>();
93
94
      set.add( new NodeRenderingHandler<>(
95
        FencedCodeBlock.class, ( node, context, html ) -> {
96
        final var style = sanitize( node.getInfo() );
97
98
        if( style.startsWith( DIAGRAM_STYLE ) ) {
99
          final var type = style.substring( DIAGRAM_STYLE_LEN );
100
          final var content = node.getContentChars().normalizeEOL();
101
          final var text = FencedBlockExtension.this.mProcessor.apply( content );
102
          final var encoded = encode( text );
103
          final var source = format(
104
            "https://kroki.io/%s/svg/%s", type, encoded );
105
106
          final var link = context.resolveLink( LINK, source, false );
107
108
          html.attr( "src", source );
109
          html.withAttr( link );
110
          html.tagVoid( "img" );
111
        }
112
        else {
113
          context.delegateRender();
114
        }
115
      } ) );
116
117
      return set;
118
    }
119
120
    private byte[] compress( byte[] source ) {
121
      final var inLen = source.length;
122
      final var result = new byte[ inLen ];
123
      final var deflater = new Deflater( BEST_COMPRESSION );
124
125
      deflater.setInput( source, 0, inLen );
126
      deflater.finish();
127
      final var outLen = deflater.deflate( result, 0, inLen, FULL_FLUSH );
128
      deflater.end();
129
130
      try( final var out = new ByteArrayOutputStream() ) {
131
        out.write( result, 0, outLen );
132
        return out.toByteArray();
133
      } catch( final Exception ex ) {
134
        clue( ex );
135
        throw new RuntimeException( ex );
136
      }
137
    }
138
139
    private String encode( final String decoded ) {
140
      return getUrlEncoder().encodeToString( compress( decoded.getBytes() ) );
141
    }
142
  }
143
144
  private class Factory implements DelegatingNodeRendererFactory {
145
    public Factory() {}
146
147
    @NotNull
148
    @Override
149
    public NodeRenderer apply( @NotNull final DataHolder options ) {
150
      return new CustomRenderer();
151
    }
152
153
    /**
154
     * Return {@code null} to indicate this may delegate to the core renderer.
155
     */
156
    @Override
157
    public Set<Class<?>> getDelegates() {
158
      return null;
159
    }
160
  }
161
}
1162
M src/main/java/com/keenwrite/processors/markdown/ImageLinkExtension.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors.markdown;
293
304
import com.keenwrite.exceptions.MissingFileException;
31
import com.keenwrite.preferences.UserPreferences;
5
import com.keenwrite.preferences.Workspace;
326
import com.vladsch.flexmark.ast.Image;
337
import com.vladsch.flexmark.html.IndependentLinkResolverFactory;
348
import com.vladsch.flexmark.html.LinkResolver;
359
import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
36
import com.vladsch.flexmark.html.renderer.LinkStatus;
3710
import com.vladsch.flexmark.html.renderer.ResolvedLink;
3811
import com.vladsch.flexmark.util.ast.Node;
...
4619
4720
import static com.keenwrite.StatusBarNotifier.clue;
48
import static com.keenwrite.util.ProtocolResolver.getProtocol;
21
import static com.keenwrite.preferences.Workspace.KEY_IMAGES_DIR;
22
import static com.keenwrite.preferences.Workspace.KEY_IMAGES_ORDER;
23
import static com.keenwrite.util.ProtocolScheme.getProtocol;
4924
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
5025
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
26
import static com.vladsch.flexmark.html.renderer.LinkStatus.VALID;
5127
import static java.lang.String.format;
52
import static org.apache.commons.io.FilenameUtils.getExtension;
53
import static org.apache.commons.io.FilenameUtils.removeExtension;
5428
5529
/**
...
6236
   * Creates an extension capable of using a relative path to embed images.
6337
   *
64
   * @param path The {@link Path} to the file being edited; the parent path
65
   *             is the starting location of the relative image directory.
66
   * @return The new {@link ImageLinkExtension}, never {@code null}.
38
   * @param basePath  The directory to search for images, either directly or
39
   *                  through the images directory setting, not {@code null}.
40
   * @param workspace Contains user preferences for image directory and image
41
   *                  file name extension lookup order.
42
   * @return The new {@link ImageLinkExtension}, not {@code null}.
6743
   */
68
  public static ImageLinkExtension create( @NotNull final Path path ) {
69
    return new ImageLinkExtension( path );
44
  public static ImageLinkExtension create(
45
    @NotNull final Path basePath,
46
    @NotNull final Workspace workspace ) {
47
    return new ImageLinkExtension( basePath, workspace );
48
  }
49
50
  private final Path mBasePath;
51
  private final Workspace mWorkspace;
52
53
  private ImageLinkExtension(
54
    @NotNull final Path basePath, @NotNull final Workspace workspace ) {
55
    mBasePath = basePath;
56
    mWorkspace = workspace;
57
  }
58
59
  @Override
60
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
61
  }
62
63
  @Override
64
  public void extend(
65
    @NotNull final Builder builder, @NotNull final String rendererType ) {
66
    builder.linkResolverFactory( new Factory() );
7067
  }
7168
7269
  private class Factory extends IndependentLinkResolverFactory {
7370
    @Override
7471
    public @NotNull LinkResolver apply(
75
        @NotNull final LinkResolverBasicContext context ) {
72
      @NotNull final LinkResolverBasicContext context ) {
7673
      return new ImageLinkResolver();
7774
    }
7875
  }
7976
8077
  private class ImageLinkResolver implements LinkResolver {
81
    private final UserPreferences mUserPref = getUserPreferences();
82
    private final File mImagesUserPrefix = mUserPref.getImagesDirectory();
83
    private final String mImageExtensions = mUserPref.getImagesOrder();
84
8578
    public ImageLinkResolver() {
8679
    }
8780
88
    /**
89
     * You can also set/clear/modify attributes through
90
     * {@link ResolvedLink#getAttributes()} and
91
     * {@link ResolvedLink#getNonNullAttributes()}.
92
     */
9381
    @NotNull
9482
    @Override
9583
    public ResolvedLink resolveLink(
96
        @NotNull final Node node,
97
        @NotNull final LinkResolverBasicContext context,
98
        @NotNull final ResolvedLink link ) {
84
      @NotNull final Node node,
85
      @NotNull final LinkResolverBasicContext context,
86
      @NotNull final ResolvedLink link ) {
9987
      return node instanceof Image ? resolve( link ) : link;
10088
    }
10189
10290
    private ResolvedLink resolve( final ResolvedLink link ) {
103
      var url = link.getUrl();
104
      final var protocol = getProtocol( url );
91
      var uri = link.getUrl();
92
      final var protocol = getProtocol( uri );
10593
106
      try {
107
        if( protocol.isHttp() ) {
108
          return valid( link, url );
109
        }
110
      } catch( final Exception ignored ) {
111
        // Try to resolve the image path, dynamically.
94
      if( protocol.isHttp() ) {
95
        return valid( link, uri );
11296
      }
113
114
      try {
115
        final Path imagePrefix = getImagePrefix().toPath();
11697
117
        // Path to the file being edited.
118
        Path editPath = getEditPath();
98
      // Determine the fully-qualified file name (fqfn).
99
      final var fqfn = Paths.get( getBasePath().toString(), uri ).toFile();
119100
120
        // If there is no parent path to the file, it means the file has not
121
        // been saved. Default to using the value from the user's preferences.
122
        // The user's preferences will be defaulted to a the application's
123
        // starting directory.
124
        editPath = editPath == null
125
            ? imagePrefix
126
            : Path.of( editPath.toString(), imagePrefix.toString() );
101
      if( fqfn.isFile() ) {
102
        return valid( link, uri );
103
      }
127104
128
        final var urlExt = getExtension( url );
129
        url = removeExtension( url );
105
      // At this point either the image directory is qualified or needs to be
106
      // qualified using the image prefix, as set in the user preferences.
107
      try {
108
        final var imagePrefix = getImagePrefix();
109
        final var basePath = getBasePath().resolve( imagePrefix );
130110
131
        final var suffixes = urlExt + ' ' + getImageExtensions();
132
        final var imagePathPrefix = Path.of( editPath.toString(), url );
133
        var suffix = ".*";
111
        final var imagePathPrefix = Path.of( basePath.toString(), uri );
112
        final var suffixes = getImageExtensions();
134113
        boolean missing = true;
135114
136115
        // Iterate over the user's preferred image file type extensions.
137
        for( final String ext : Splitter.on( ' ' ).split( suffixes ) ) {
138
          final String imagePath = format( "%s.%s", imagePathPrefix, ext );
139
          final File file = new File( imagePath );
116
        for( final var ext : Splitter.on( ' ' ).split( suffixes ) ) {
117
          final var imagePath = format( "%s.%s", imagePathPrefix, ext );
118
          final var file = new File( imagePath );
140119
141120
          if( file.exists() ) {
142
            url = file.toString();
121
            uri += '.' + ext;
122
            final var path = Path.of( imagePrefix.toString(), uri );
123
            uri = path.normalize().toString();
143124
            missing = false;
144
            break;
145
          }
146
          else if( !urlExt.isBlank() ) {
147
            // The file is missing because the user specified a prefix.
148
            suffix = urlExt;
149125
            break;
150126
          }
151127
        }
152128
153129
        if( missing ) {
154
          throw new MissingFileException( imagePathPrefix + suffix );
155
        }
156
157
        if( protocol.isFile() ) {
158
          // When generating an HTML document, ensure images use a path that's
159
          // relative to the source document. This handles displaying within
160
          // the application and when exporting to an HTML file.
161
          url = editPath.relativize( Paths.get( url ) ).toString();
130
          throw new MissingFileException( imagePathPrefix + ".*" );
162131
        }
163132
164
        return valid( link, url );
133
        return valid( link, uri );
165134
      } catch( final Exception ex ) {
166135
        clue( ex );
167136
      }
168137
169138
      return link;
170139
    }
171140
172141
    private ResolvedLink valid( final ResolvedLink link, final String url ) {
173
      return link.withStatus( LinkStatus.VALID ).withUrl( url );
142
      return link.withStatus( VALID ).withUrl( url );
174143
    }
175144
176
    private File getImagePrefix() {
177
      return mImagesUserPrefix;
145
    private Path getImagePrefix() {
146
      return mWorkspace.toFile( KEY_IMAGES_DIR ).toPath();
178147
    }
179148
180149
    private String getImageExtensions() {
181
      return mImageExtensions;
150
      return mWorkspace.toString( KEY_IMAGES_ORDER );
182151
    }
183152
184
    private Path getEditPath() {
185
      return mPath.getParent();
153
    private Path getBasePath() {
154
      return mBasePath;
186155
    }
187
  }
188
189
  private final Path mPath;
190
191
  private ImageLinkExtension( @NotNull final Path path ) {
192
    mPath = path;
193
  }
194
195
  @Override
196
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
197
  }
198
199
  @Override
200
  public void extend( @NotNull final Builder builder,
201
                      @NotNull final String rendererType ) {
202
    builder.linkResolverFactory( new Factory() );
203
  }
204
205
  private UserPreferences getUserPreferences() {
206
    return UserPreferences.getInstance();
207156
  }
208157
}
D src/main/java/com/keenwrite/processors/markdown/LigatureExtension.java
1
package com.keenwrite.processors.markdown;
2
3
import com.vladsch.flexmark.ast.Text;
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.html.renderer.NodeRenderer;
6
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
7
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
8
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
9
import com.vladsch.flexmark.util.ast.TextCollectingVisitor;
10
import com.vladsch.flexmark.util.data.DataHolder;
11
import com.vladsch.flexmark.util.data.MutableDataHolder;
12
import org.jetbrains.annotations.NotNull;
13
import org.jetbrains.annotations.Nullable;
14
15
import java.util.LinkedHashMap;
16
import java.util.Map;
17
import java.util.Set;
18
19
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
20
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
21
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
22
23
/**
24
 * Responsible for substituting multi-codepoint glyphs with single codepoint
25
 * glyphs. The text is adorned with ligatures prior to rendering as HTML.
26
 * This requires a font that supports ligatures.
27
 * <p>
28
 * TODO: #81 -- I18N
29
 * </p>
30
 */
31
public class LigatureExtension implements HtmlRendererExtension {
32
  /**
33
   * Retain insertion order so that ligature substitution uses longer ligatures
34
   * ahead of shorter ligatures. The word "ruffian" should use the "ffi"
35
   * ligature, not the "ff" ligature.
36
   */
37
  private static final Map<String, String> LIGATURES = new LinkedHashMap<>();
38
39
  static {
40
    LIGATURES.put( "ffi", "\uFB03" );
41
    LIGATURES.put( "ffl", "\uFB04" );
42
    LIGATURES.put( "ff", "\uFB00" );
43
    LIGATURES.put( "fi", "\uFB01" );
44
    LIGATURES.put( "fl", "\uFB02" );
45
    LIGATURES.put( "ft", "\uFB05" );
46
    LIGATURES.put( "AE", "\u00C6" );
47
    LIGATURES.put( "OE", "\u0152" );
48
//      "ae", "\u00E6",
49
//      "oe", "\u0153",
50
  }
51
52
  private static class LigatureRenderer implements NodeRenderer {
53
    private final TextCollectingVisitor mVisitor = new TextCollectingVisitor();
54
55
    @SuppressWarnings("unused")
56
    public LigatureRenderer( final DataHolder options ) {
57
    }
58
59
    @Override
60
    public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
61
      return Set.of( new NodeRenderingHandler<>(
62
          Text.class, LigatureRenderer.this::render ) );
63
    }
64
65
    /**
66
     * This will pick the fastest string replacement algorithm based on the
67
     * text length. The insertion order of the {@link #LIGATURES} is
68
     * important to give precedence to longer ligatures.
69
     *
70
     * @param textNode The text node containing text to replace with ligatures.
71
     * @param context  Not used.
72
     * @param html     Where to write the text adorned with ligatures.
73
     */
74
    private void render(
75
        @NotNull final Text textNode,
76
        @NotNull final NodeRendererContext context,
77
        @NotNull final HtmlWriter html ) {
78
      final var text = mVisitor.collectAndGetText( textNode );
79
      html.text( replace( text, LIGATURES ) );
80
    }
81
  }
82
83
  private static class Factory implements NodeRendererFactory {
84
    @NotNull
85
    @Override
86
    public NodeRenderer apply( @NotNull DataHolder options ) {
87
      return new LigatureRenderer( options );
88
    }
89
  }
90
91
  private LigatureExtension() {
92
  }
93
94
  @Override
95
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
96
  }
97
98
  @Override
99
  public void extend( @NotNull final Builder builder,
100
                      @NotNull final String rendererType ) {
101
    if( "HTML".equalsIgnoreCase( rendererType ) ) {
102
      builder.nodeRendererFactory( new Factory() );
103
    }
104
  }
105
106
  public static LigatureExtension create() {
107
    return new LigatureExtension();
108
  }
109
}
1101
M src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors.markdown;
293
304
import com.keenwrite.ExportFormat;
5
import com.keenwrite.io.MediaType;
6
import com.keenwrite.preferences.Workspace;
317
import com.keenwrite.processors.*;
328
import com.keenwrite.processors.markdown.r.RExtension;
...
4723
import java.util.HashSet;
4824
49
import static com.keenwrite.AbstractFileFactory.lookup;
50
import static com.keenwrite.Constants.USER_DIRECTORY;
25
import static com.keenwrite.Constants.DEFAULT_DIRECTORY;
5126
import static com.keenwrite.ExportFormat.NONE;
27
import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN;
28
import static com.keenwrite.io.MediaType.TEXT_R_XML;
5229
5330
/**
5431
 * Responsible for parsing a Markdown document and rendering it as HTML.
5532
 */
56
public class MarkdownProcessor extends AbstractProcessor<String> {
33
public class MarkdownProcessor extends ExecutorProcessor<String> {
5734
5835
  private final IParse mParser;
5936
  private final IRender mRenderer;
6037
6138
  private MarkdownProcessor(
62
      final Processor<String> successor,
63
      final Collection<Extension> extensions ) {
39
    final Processor<String> successor,
40
    final Collection<Extension> extensions ) {
6441
    super( successor );
65
66
    // TODO: https://github.com/FAlthausen/Vollkorn-Typeface/issues/38
67
    // TODO: Uncomment when ligatures are fixed.
68
    // extensions.add( LigatureExtension.create() );
6942
7043
    mParser = Parser.builder().extensions( extensions ).build();
7144
    mRenderer = HtmlRenderer.builder().extensions( extensions ).build();
7245
  }
7346
74
  public static MarkdownProcessor create() {
75
    return create( IdentityProcessor.INSTANCE, Path.of( USER_DIRECTORY ) );
47
  public static MarkdownProcessor create( final Workspace workspace ) {
48
    return create( IdentityProcessor.INSTANCE, workspace, DEFAULT_DIRECTORY );
49
  }
50
51
  public static MarkdownProcessor create( final ProcessorContext context ) {
52
    return create( IdentityProcessor.INSTANCE, context );
7653
  }
7754
7855
  public static MarkdownProcessor create(
79
      final Processor<String> successor, final Path path ) {
80
    final var extensions = createExtensions( path, NONE );
56
    final Processor<String> successor,
57
    final Workspace workspace,
58
    final Path dir ) {
59
    final var extensions = createExtensions( NONE, workspace, dir );
8160
    return new MarkdownProcessor( successor, extensions );
8261
  }
8362
8463
  public static MarkdownProcessor create(
85
      final Processor<String> successor, final ProcessorContext context ) {
64
    final Processor<String> successor, final ProcessorContext context ) {
8665
    final var extensions = createExtensions( context );
8766
    return new MarkdownProcessor( successor, extensions );
...
10281
   */
10382
  private static Collection<Extension> createExtensions(
104
      final ProcessorContext context ) {
105
    final var path = context.getPath();
83
    final ProcessorContext context ) {
84
    final var path  = context.getPath();
85
    final var dir = context.getBasePath();
10686
    final var format = context.getExportFormat();
107
    final var extensions = createExtensions( path, format );
87
    final var workspace = context.getWorkspace();
88
    final var extensions = createExtensions( format, workspace, dir );
10889
109
    extensions.add( CaretExtension.create( context.getCaretPosition() ) );
90
    final var mediaType = MediaType.valueFrom( path );
91
    if( mediaType == TEXT_R_MARKDOWN || mediaType == TEXT_R_XML ) {
92
      extensions.add( RExtension.create() );
93
    }
94
95
    extensions.add( FencedBlockExtension.create( context ) );
96
    extensions.add( CaretExtension.create( context.getCaret() ) );
11097
11198
    return extensions;
...
120107
   * R statements.
121108
   *
122
   * @param path   Path name for referencing image files via relative paths
109
   * @param dir    Directory for referencing image files via relative paths
123110
   *               and dynamic file types.
124111
   * @param format TeX export format to use when generating HTMl documents.
125112
   * @return {@link Collection} of extensions invoked when parsing Markdown.
126113
   */
127114
  private static Collection<Extension> createExtensions(
128
      final Path path, final ExportFormat format ) {
115
    final ExportFormat format, final Workspace workspace, final Path dir ) {
129116
    final var extensions = createDefaultExtensions();
130117
131
    extensions.add( ImageLinkExtension.create( path ) );
118
    extensions.add( ImageLinkExtension.create( dir, workspace ) );
132119
    extensions.add( TeXExtension.create( format ) );
133
134
    if( lookup( path ).isR() ) {
135
      extensions.add( RExtension.create() );
136
    }
137120
138121
    return extensions;
...
179162
  public Node toNode( final String markdown ) {
180163
    return parse( markdown );
164
  }
165
166
  /**
167
   * Returns the result of converting the given AST into an HTML string.
168
   *
169
   * @param node The AST {@link Node} to convert to an HTML string.
170
   * @return The given {@link Node} as an HTML string.
171
   */
172
  public String toHtml( final Node node ) {
173
    return getRenderer().render( node );
181174
  }
182175
...
198191
   */
199192
  private String toHtml( final String markdown ) {
200
    return getRenderer().render( parse( markdown ) );
193
    return toHtml( parse( markdown ) );
201194
  }
202195
M src/main/java/com/keenwrite/processors/markdown/TeXExtension.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors.markdown;
293
M src/main/java/com/keenwrite/processors/markdown/r/RExtension.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors.markdown.r;
293
...
3913
import com.vladsch.flexmark.util.data.DataHolder;
4014
import com.vladsch.flexmark.util.data.MutableDataHolder;
41
import com.vladsch.flexmark.util.sequence.BasedSequence;
4215
4316
import java.util.BitSet;
...
5225
 */
5326
public final class RExtension implements Parser.ParserExtension {
54
  private final static InlineParserFactory R_INLINE_PARSER_FACTORY =
55
      RInlineParser::new;
27
  private static final InlineParserFactory FACTORY = CustomParser::new;
28
29
  private RExtension() {
30
  }
31
32
  /**
33
   * Creates an extension capable of intercepting R code blocks and preventing
34
   * them from being converted into HTML {@code <code>} elements.
35
   */
36
  public static RExtension create() {
37
    return new RExtension();
38
  }
39
40
  @Override
41
  public void extend( final Parser.Builder builder ) {
42
    builder.customInlineParserFactory( FACTORY );
43
  }
44
45
  @Override
46
  public void parserOptions( final MutableDataHolder options ) {
47
  }
5648
5749
  /**
...
6759
   * </p>
6860
   */
69
  private static class RInlineParser extends InlineParserImpl {
70
    private RInlineParser(
71
        final DataHolder options,
72
        final BitSet specialCharacters,
73
        final BitSet delimiterCharacters,
74
        final Map<Character, DelimiterProcessor> delimiterProcessors,
75
        final LinkRefProcessorData referenceLinkProcessors,
76
        final List<InlineParserExtensionFactory> inlineParserExtensions ) {
61
  private static class CustomParser extends InlineParserImpl {
62
    private CustomParser(
63
      final DataHolder options,
64
      final BitSet specialCharacters,
65
      final BitSet delimiterCharacters,
66
      final Map<Character, DelimiterProcessor> delimiterProcessors,
67
      final LinkRefProcessorData referenceLinkProcessors,
68
      final List<InlineParserExtensionFactory> inlineParserExtensions ) {
7769
      super( options,
7870
             specialCharacters,
...
8779
     * changes the behaviour to retain R code snippets, identified by
8880
     * {@link RSigilOperator#PREFIX}, so that subsequent processing can
89
     * invoke R. If other languages are added, this {@link RInlineParser} will
81
     * invoke R. If other languages are added, the {@link CustomParser} will
9082
     * have to be rewritten to identify more than merely R.
9183
     *
9284
     * @return The return value from {@link super#parseBackticks()}.
9385
     * @inheritDoc
9486
     */
9587
    @Override
9688
    protected final boolean parseBackticks() {
97
      final var foundCode = super.parseBackticks();
89
      final var foundTicks = super.parseBackticks();
9890
99
      if( foundCode ) {
100
        final var block = getBlock();
101
        final var codeNode = block.getLastChild();
102
        final var code = codeNode == null
103
            ? BasedSequence.of( "" )
104
            : codeNode.getChars();
91
      if( foundTicks ) {
92
        final var blockNode = getBlock();
93
        final var codeNode = blockNode.getLastChild();
10594
106
        if( code.startsWith( RSigilOperator.PREFIX ) ) {
107
          assert codeNode != null;
108
          codeNode.unlink();
109
          block.appendChild( new Text( code ) );
95
        if( codeNode != null ) {
96
          final var code = codeNode.getChars();
97
98
          if( code.startsWith( RSigilOperator.PREFIX ) ) {
99
            codeNode.unlink();
100
            blockNode.appendChild( new Text( code ) );
101
          }
110102
        }
111103
      }
112104
113
      return foundCode;
105
      return foundTicks;
114106
    }
115
  }
116
117
  private RExtension() {
118
  }
119
120
  /**
121
   * Creates an extension capable of intercepting R code blocks and preventing
122
   * them from being converted into HTML {@code <code>} elements.
123
   */
124
  public static RExtension create() {
125
    return new RExtension();
126
  }
127
128
  @Override
129
  public void extend( final Parser.Builder builder ) {
130
    builder.customInlineParserFactory( R_INLINE_PARSER_FACTORY );
131
  }
132
133
  @Override
134
  public void parserOptions( final MutableDataHolder options ) {
135107
  }
136108
}
M src/main/java/com/keenwrite/processors/markdown/tex/TeXInlineDelimiterProcessor.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors.markdown.tex;
293
M src/main/java/com/keenwrite/processors/markdown/tex/TexNode.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors.markdown.tex;
293
M src/main/java/com/keenwrite/processors/markdown/tex/TexNodeRenderer.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors.markdown.tex;
293
304
import com.keenwrite.ExportFormat;
315
import com.keenwrite.preview.SvgRasterizer;
32
import com.keenwrite.preview.SvgReplacedElementFactory;
336
import com.vladsch.flexmark.html.HtmlWriter;
347
import com.vladsch.flexmark.html.renderer.NodeRenderer;
...
4316
import java.util.Set;
4417
18
import static com.keenwrite.preview.MathRenderer.MATH_RENDERER;
4519
import static com.keenwrite.processors.markdown.tex.TexNode.*;
4620
...
10983
                 final NodeRendererContext context,
11084
                 final HtmlWriter html ) {
111
      final var renderer = SvgReplacedElementFactory.getInstance();
11285
      final var tex = node.getText().toStringOrNull();
113
      final var doc = renderer.render( tex == null ? "" : tex );
86
      final var doc = MATH_RENDERER.render( tex == null ? "" : tex );
11487
      final var svg = SvgRasterizer.toSvg( doc.getDocumentElement() );
11588
      html.raw( svg );
M src/main/java/com/keenwrite/processors/text/AbstractTextReplacer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors.text;
293
M src/main/java/com/keenwrite/processors/text/AhoCorasickReplacer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors.text;
293
304
import java.util.Map;
31
import org.ahocorasick.trie.Emit;
32
import org.ahocorasick.trie.Trie.TrieBuilder;
5
336
import static org.ahocorasick.trie.Trie.builder;
347
...
4720
  public String replace( final String text, final Map<String, String> map ) {
4821
    // Create a buffer sufficiently large that re-allocations are minimized.
49
    final StringBuilder sb = new StringBuilder( (int)(text.length() * 1.25) );
50
51
    // The TrieBuilder should only match whole words and ignore overlaps (there
52
    // shouldn't be any).
53
    final TrieBuilder builder = builder().onlyWholeWords().ignoreOverlaps();
22
    final var sb = new StringBuilder( (int)(text.length() * 1.25) );
5423
55
    for( final String key : keys( map ) ) {
56
      builder.addKeyword( key );
57
    }
24
    // Definition names cannot overlap.
25
    final var builder = builder().ignoreOverlaps();
26
    builder.addKeywords( keys( map ) );
5827
5928
    int index = 0;
6029
6130
    // Replace all instances with dereferenced variables.
62
    for( final Emit emit : builder.build().parseText( text ) ) {
31
    for( final var emit : builder.build().parseText( text ) ) {
6332
      sb.append( text, index, emit.getStart() );
6433
      sb.append( map.get( emit.getKeyword() ) );
M src/main/java/com/keenwrite/processors/text/StringUtilsReplacer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors.text;
293
M src/main/java/com/keenwrite/processors/text/TextReplacementFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors.text;
293
...
4721
   */
4822
  public static TextReplacer getTextReplacer( final int length ) {
49
    // After about 1,500 characters, the StringUtils implementation is less
50
    // performant than the Aho-Corsick implementation.
51
    //
52
    // See http://stackoverflow.com/a/40836618/59087
23
    // After about 1,500 characters, the StringUtils implementation is slower
24
    // than the Aho-Corsick algorithm implementation.
5325
    return length < 1500 ? APACHE : AHO_CORASICK;
5426
  }
M src/main/java/com/keenwrite/processors/text/TextReplacer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.processors.text;
293
A src/main/java/com/keenwrite/search/SearchModel.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.search;
3
4
import com.keenwrite.util.CyclicIterator;
5
import javafx.beans.property.ObjectProperty;
6
import javafx.beans.property.SimpleObjectProperty;
7
import javafx.beans.value.ObservableValue;
8
import javafx.scene.control.IndexRange;
9
import org.ahocorasick.trie.Emit;
10
import org.ahocorasick.trie.Trie;
11
12
import java.util.ArrayList;
13
import java.util.List;
14
15
import static org.ahocorasick.trie.Trie.builder;
16
17
/**
18
 * Responsible for finding words in a text document. This implementation uses
19
 * a {@link Trie} for efficiency.
20
 */
21
public class SearchModel {
22
  private final ObjectProperty<IndexRange> mMatchOffset =
23
      new SimpleObjectProperty<>();
24
  private final ObjectProperty<Integer> mMatchCount =
25
      new SimpleObjectProperty<>();
26
  private final ObjectProperty<Integer> mMatchIndex =
27
      new SimpleObjectProperty<>();
28
29
  private CyclicIterator<Emit> mMatches = new CyclicIterator<>( List.of() );
30
31
  private String mNeedle = "";
32
33
  /**
34
   * Creates a new {@link SearchModel} that finds all text string in a
35
   * document simultaneously.
36
   */
37
  public SearchModel() {
38
  }
39
40
  public ObjectProperty<Integer> matchCountProperty() {
41
    return mMatchCount;
42
  }
43
44
  public ObjectProperty<Integer> matchIndexProperty() {
45
    return mMatchIndex;
46
  }
47
48
  /**
49
   * Observers watch this property to be notified when a needle has been
50
   * found in the haystack. Use {@link IndexRange#getStart()} to get the
51
   * absolute offset into the text (zero-based).
52
   *
53
   * @return The {@link IndexRange} property to observe, representing the
54
   * most recently matched text offset into the document.
55
   */
56
  public ObservableValue<IndexRange> matchOffsetProperty() {
57
    return mMatchOffset;
58
  }
59
60
  /**
61
   * Searches the document for text matching the given parameter value. This
62
   * is the main entry point for kicking off text searches.
63
   *
64
   * @param needle   The text string to find in the document, no regex allowed.
65
   * @param haystack The document to search within for a text string.
66
   */
67
  public void search( final String needle, final String haystack ) {
68
    assert needle != null;
69
    assert haystack != null;
70
71
    final var trie = builder()
72
        .ignoreCase()
73
        .ignoreOverlaps()
74
        .addKeyword( needle )
75
        .build();
76
    final var emits = trie.parseText( haystack );
77
78
    mMatches = new CyclicIterator<>( new ArrayList<>( emits ) );
79
    mMatchCount.set( emits.size() );
80
    mNeedle = needle;
81
    advance();
82
  }
83
84
  /**
85
   * Searches the document for the last known needle.
86
   *
87
   * @param haystack The new text to search.
88
   */
89
  public void search( final String haystack ) {
90
    search( mNeedle, haystack );
91
  }
92
93
  /**
94
   * Moves the search iterator to the next match, wrapping as needed.
95
   */
96
  public void advance() {
97
    if( mMatches.hasNext() ) {
98
      setCurrent( mMatches.next() );
99
    }
100
  }
101
102
  /**
103
   * Moves the search iterator to the previous match, wrapping as needed.
104
   */
105
  public void retreat() {
106
    if( mMatches.hasPrevious() ) {
107
      setCurrent( mMatches.previous() );
108
    }
109
  }
110
111
  private void setCurrent( final Emit emit ) {
112
    mMatchOffset.set( new IndexRange( emit.getStart(), emit.getEnd() ) );
113
    mMatchIndex.set( mMatches.getIndex() + 1 );
114
  }
115
}
1116
D src/main/java/com/keenwrite/service/Options.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.service;
29
30
import com.dlsc.preferencesfx.PreferencesFx;
31
32
import java.util.prefs.BackingStoreException;
33
import java.util.prefs.Preferences;
34
35
/**
36
 * Responsible for persisting options that are safe to load before the UI
37
 * is shown. This can include items like window dimensions, last file
38
 * opened, split pane locations, and more. This cannot be used to persist
39
 * options that are user-controlled (i.e., all options available through
40
 * {@link PreferencesFx}).
41
 */
42
public interface Options extends Service {
43
44
  /**
45
   * Returns the {@link Preferences} that persist settings that cannot
46
   * be configured via the user interface.
47
   *
48
   * @return A valid {@link Preferences} instance, never {@code null}.
49
   */
50
  Preferences getState();
51
52
  /**
53
   * Stores the key and value into the user preferences to be loaded the next
54
   * time the application is launched.
55
   *
56
   * @param key   Name of the key to persist along with its value.
57
   * @param value Value to associate with the key.
58
   * @throws BackingStoreException Could not persist the change.
59
   */
60
  void put( String key, String value ) throws BackingStoreException;
61
62
  /**
63
   * Retrieves the value for a key in the user preferences.
64
   *
65
   * @param key          Retrieve the value of this key.
66
   * @param defaultValue The value to return in the event that the given key has
67
   *                     no associated value.
68
   * @return The value associated with the key.
69
   */
70
  String get( String key, String defaultValue );
71
72
  /**
73
   * Retrieves the value for a key in the user preferences. This will return
74
   * the empty string if the value cannot be found.
75
   *
76
   * @param key The key to find in the preferences.
77
   * @return A non-null, possibly empty value for the key.
78
   */
79
  String get( String key );
80
}
811
M src/main/java/com/keenwrite/service/Service.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.service;
293
M src/main/java/com/keenwrite/service/Settings.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.service;
293
M src/main/java/com/keenwrite/service/Snitch.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.service;
293
...
6539
   */
6640
  void ignore( Path file );
41
42
  /**
43
   * Start listening for events on a new thread.
44
   */
45
  void start();
6746
6847
  /**
M src/main/java/com/keenwrite/service/events/Notification.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.service.events;
293
M src/main/java/com/keenwrite/service/events/Notifier.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.service.events;
293
304
import javafx.scene.control.Alert;
315
import javafx.scene.control.ButtonType;
326
import javafx.stage.Window;
7
8
import java.nio.file.Path;
339
3410
/**
...
4218
4319
  /**
44
   * Constructs a default alert message text for a modal alert dialog.
20
   * Constructs an alert message text for a modal alert dialog.
4521
   *
46
   * @param title   The dialog box message title.
47
   * @param message The dialog box message content (needs formatting).
22
   * @param parent     The window responsible for the child dialog.
23
   * @param path       The path to a file that was not actionable.
24
   * @param titleKey   The dialog box message title.
25
   * @param messageKey The dialog box message content (needs formatting).
26
   * @param ex         The problem that requires user attention.
27
   */
28
  void alert(
29
      Window parent,
30
      Path path,
31
      String titleKey,
32
      String messageKey,
33
      Exception ex );
34
35
  /**
36
   * Constructs an alert message text for a modal alert dialog.
37
   *
38
   * @param parent The window responsible for the child dialog.
39
   * @param path   The path to a file that was not actionable.
40
   * @param key    Prefix for both title and message key.
41
   * @param ex     The problem that requires user attention.
42
   */
43
  default void alert(
44
      Window parent,
45
      Path path,
46
      String key,
47
      Exception ex ) {
48
    alert( parent, path, key + ".title", key + ".message", ex );
49
  }
50
51
  /**
52
   * Contains all the information that the user needs to know about a problem.
53
   *
54
   * @param title   The dialog box message title (i.e., the error context).
55
   * @param message The message content (formatted with the given args).
4856
   * @param args    The arguments to the message content that must be formatted.
4957
   * @return The message suitable for building a modal alert dialog.
M src/main/java/com/keenwrite/service/events/impl/ButtonOrderPane.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.service.events.impl;
293
304
import javafx.scene.Node;
315
import javafx.scene.control.ButtonBar;
326
import javafx.scene.control.DialogPane;
337
34
import static com.keenwrite.Constants.SETTINGS;
8
import static com.keenwrite.Constants.sSettings;
359
import static javafx.scene.control.ButtonBar.BUTTON_ORDER_WINDOWS;
3610
...
5529
  @SuppressWarnings("SameParameterValue")
5630
  private String getSetting( final String key, final String defaultValue ) {
57
    return SETTINGS.getSetting( key, defaultValue );
31
    return sSettings.getSetting( key, defaultValue );
5832
  }
5933
}
M src/main/java/com/keenwrite/service/events/impl/DefaultNotification.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.service.events.impl;
293
...
6438
    return this.content;
6539
  }
40
6641
}
6742
M src/main/java/com/keenwrite/service/events/impl/DefaultNotifier.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.service.events.impl;
293
304
import com.keenwrite.service.events.Notification;
315
import com.keenwrite.service.events.Notifier;
326
import javafx.scene.control.Alert;
337
import javafx.scene.control.Alert.AlertType;
348
import javafx.stage.Window;
9
10
import java.nio.file.Path;
3511
12
import static com.keenwrite.Messages.get;
3613
import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
3714
import static javafx.scene.control.Alert.AlertType.ERROR;
3815
3916
/**
4017
 * Provides the ability to notify the user of events that need attention,
4118
 * such as prompting the user to confirm closing when there are unsaved changes.
4219
 */
4320
public final class DefaultNotifier implements Notifier {
4421
45
  /**
46
   * Contains all the information that the user needs to know about a problem.
47
   *
48
   * @param title   The context for the message.
49
   * @param message The message content (formatted with the given args).
50
   * @param args    Parameters for the message content.
51
   * @return A notification instance, never null.
52
   */
5322
  @Override
5423
  public Notification createNotification(
5524
      final String title,
5625
      final String message,
5726
      final Object... args ) {
5827
    return new DefaultNotification( title, message, args );
5928
  }
6029
61
  private Alert createAlertDialog(
30
  @Override
31
  public void alert(
6232
      final Window parent,
63
      final AlertType alertType,
64
      final Notification message ) {
65
66
    final Alert alert = new Alert( alertType );
67
68
    alert.setDialogPane( new ButtonOrderPane() );
69
    alert.setTitle( message.getTitle() );
70
    alert.setHeaderText( null );
71
    alert.setContentText( message.getContent() );
72
    alert.initOwner( parent );
33
      final Path path,
34
      final String titleKey,
35
      final String messageKey,
36
      final Exception ex ) {
37
    final var message = createNotification(
38
        get( titleKey ), get( messageKey ), path, ex.getMessage()
39
    );
7340
74
    return alert;
41
    createError( parent, message ).showAndWait();
7542
  }
7643
7744
  @Override
78
  public Alert createConfirmation( final Window parent,
79
                                   final Notification message ) {
80
    final Alert alert = createAlertDialog( parent, CONFIRMATION, message );
45
  public Alert createConfirmation(
46
      final Window parent, final Notification message ) {
47
    final var alert = createAlertDialog( parent, CONFIRMATION, message );
8148
8249
    alert.getButtonTypes().setAll( YES, NO, CANCEL );
8350
8451
    return alert;
8552
  }
8653
8754
  @Override
8855
  public Alert createError( final Window parent, final Notification message ) {
8956
    return createAlertDialog( parent, ERROR, message );
57
  }
58
59
  private Alert createAlertDialog(
60
      final Window parent,
61
      final AlertType alertType,
62
      final Notification message ) {
63
    final var alert = new Alert( alertType );
64
65
    alert.setDialogPane( new ButtonOrderPane() );
66
    alert.setTitle( message.getTitle() );
67
    alert.setHeaderText( null );
68
    alert.setContentText( message.getContent() );
69
    alert.initOwner( parent );
70
71
    return alert;
9072
  }
9173
}
D src/main/java/com/keenwrite/service/impl/DefaultOptions.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.service.impl;
28
29
import com.keenwrite.service.Options;
30
31
import java.util.prefs.BackingStoreException;
32
import java.util.prefs.Preferences;
33
34
import static com.keenwrite.Constants.PREFS_ROOT;
35
import static com.keenwrite.Constants.PREFS_STATE;
36
import static java.util.prefs.Preferences.userRoot;
37
38
/**
39
 * Persistent options user can change at runtime.
40
 */
41
public class DefaultOptions implements Options {
42
  public DefaultOptions() {
43
  }
44
45
  /**
46
   * This will throw IllegalArgumentException if the value exceeds the maximum
47
   * preferences value length.
48
   *
49
   * @param key   The name of the key to associate with the value.
50
   * @param value The value to persist.
51
   * @throws BackingStoreException New value not persisted.
52
   */
53
  @Override
54
  public void put( final String key, final String value )
55
      throws BackingStoreException {
56
    getState().put( key, value );
57
    getState().flush();
58
  }
59
60
  @Override
61
  public String get( final String key, final String value ) {
62
    return getState().get( key, value );
63
  }
64
65
  @Override
66
  public String get( final String key ) {
67
    return get( key, "" );
68
  }
69
70
  private Preferences getRootPreferences() {
71
    return userRoot().node( PREFS_ROOT );
72
  }
73
74
  @Override
75
  public Preferences getState() {
76
    return getRootPreferences().node( PREFS_STATE );
77
  }
78
}
791
M src/main/java/com/keenwrite/service/impl/DefaultSettings.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.service.impl;
293
...
4418
 * Responsible for loading settings that help avoid hard-coded assumptions.
4519
 */
46
public class DefaultSettings implements Settings {
20
public final class DefaultSettings implements Settings {
4721
4822
  private static final char VALUE_SEPARATOR = ',';
4923
50
  private PropertiesConfiguration mProperties;
24
  private final PropertiesConfiguration mProperties = createProperties();
5125
5226
  public DefaultSettings() {
53
    setProperties( createProperties() );
5427
  }
5528
...
140113
  private URL getPropertySource() {
141114
    return DefaultSettings.class.getResource( PATH_PROPERTIES_SETTINGS );
142
  }
143
144
  private void setProperties( final PropertiesConfiguration properties ) {
145
    mProperties = properties;
146115
  }
147116
M src/main/java/com/keenwrite/service/impl/DefaultSnitch.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.service.impl;
293
...
3913
4014
import static com.keenwrite.Constants.APP_WATCHDOG_TIMEOUT;
15
import static com.keenwrite.StatusBarNotifier.clue;
4116
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
4217
4318
/**
4419
 * Listens for file changes. Other classes can register paths to be monitored
4520
 * and listen for changes to those paths.
4621
 */
4722
public class DefaultSnitch extends Observable implements Snitch {
23
  private final Thread mSnitchThread = new Thread( this );
4824
4925
  /**
...
6844
6945
  public DefaultSnitch() {
46
  }
47
48
  @Override
49
  public void start() {
50
    mSnitchThread.start();
7051
  }
7152
7253
  @Override
7354
  public void stop() {
7455
    setListening( false );
56
57
    try {
58
      mSnitchThread.interrupt();
59
      mSnitchThread.join();
60
    } catch( final Exception ex ) {
61
      clue( ex );
62
    }
7563
  }
7664
...
157145
          ignore( path );
158146
        }
159
      } catch( final IOException | InterruptedException ex ) {
147
      } catch( final Exception ex ) {
160148
        // Stop eavesdropping.
161149
        setListening( false );
M src/main/java/com/keenwrite/sigils/RSigilOperator.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.sigils;
293
...
3913
  public static final char SUFFIX = '`';
4014
41
  private final String mDelimiterBegan =
42
      getUserPreferences().getRDelimiterBegan();
43
  private final String mDelimiterEnded =
44
      getUserPreferences().getRDelimiterEnded();
15
  /**
16
   * Definition variables are inserted into the document before R variables,
17
   * so this is required to reformat the definition variable suitable for R.
18
   */
19
  private final SigilOperator mAntecedent;
20
21
  public RSigilOperator( final Tokens tokens, final SigilOperator antecedent ) {
22
    super( tokens );
23
    mAntecedent = antecedent;
24
  }
4525
4626
  /**
4727
   * Returns the given string R-escaping backticks prepended and appended. This
4828
   * is not null safe. Do not pass null into this method.
4929
   *
5030
   * @param key The string to adorn with R token delimiters.
51
   * @return "`r#" + delimiterBegan + variableName+ delimiterEnded + "`".
31
   * @return PREFIX + delimiterBegan + variableName+ delimiterEnded + SUFFIX.
5232
   */
5333
  @Override
5434
  public String apply( final String key ) {
5535
    assert key != null;
56
57
    return PREFIX
58
        + mDelimiterBegan
59
        + entoken( key )
60
        + mDelimiterEnded
61
        + SUFFIX;
36
    return PREFIX + getBegan() + entoken( key ) + getEnded() + SUFFIX;
6237
  }
6338
6439
  /**
6540
   * Transforms a definition key (bracketed by token delimiters) into the
6641
   * expected format for an R variable key name.
6742
   *
6843
   * @param key The variable name to transform, can be empty but not null.
6944
   * @return The transformed variable name.
7045
   */
71
  public static String entoken( final String key ) {
72
    return "v$" +
73
        YamlSigilOperator.detoken( key )
74
                         .replace( KEY_SEPARATOR_DEF, KEY_SEPARATOR_R );
46
  public String entoken( final String key ) {
47
    return "v$" + mAntecedent.detoken( key )
48
                             .replace( KEY_SEPARATOR_DEF, KEY_SEPARATOR_R );
7549
  }
7650
}
M src/main/java/com/keenwrite/sigils/SigilOperator.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.sigils;
29
30
import com.keenwrite.preferences.UserPreferences;
313
324
import java.util.function.UnaryOperator;
...
3911
 */
4012
public abstract class SigilOperator implements UnaryOperator<String> {
41
  protected static UserPreferences getUserPreferences() {
42
    return UserPreferences.getInstance();
13
  private final Tokens mTokens;
14
15
  SigilOperator( final Tokens tokens ) {
16
    mTokens = tokens;
17
  }
18
19
  /**
20
   * Removes start and stop definition key delimiters from the given key. This
21
   * method does not check for delimiters, only that there are sufficient
22
   * characters to remove from either end of the given key.
23
   *
24
   * @param key The key adorned with start and stop tokens.
25
   * @return The given key with the delimiters removed.
26
   */
27
  String detoken( final String key ) {
28
    return key;
29
  }
30
31
  String getBegan() {
32
    return mTokens.getBegan();
33
  }
34
35
  String getEnded() {
36
    return mTokens.getEnded();
4337
  }
38
39
  /**
40
   * Wraps the given key in the began and ended tokens. This may perform any
41
   * preprocessing necessary to ensure the transformation happens.
42
   *
43
   * @param key The variable name to transform.
44
   * @return The given key with tokens to delimit it (from the edited text).
45
   */
46
  public abstract String entoken( final String key );
4447
}
4548
A src/main/java/com/keenwrite/sigils/Tokens.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.sigils;
3
4
import javafx.beans.property.StringProperty;
5
6
import java.util.AbstractMap.SimpleImmutableEntry;
7
8
/**
9
 * Convenience class for pairing a start and end sigil together.
10
 */
11
public final class Tokens
12
  extends SimpleImmutableEntry<StringProperty, StringProperty> {
13
14
  /**
15
   * Associates a new key-value pair.
16
   *
17
   * @param began The starting sigil.
18
   * @param ended The ending sigil.
19
   */
20
  public Tokens( final StringProperty began, final StringProperty ended ) {
21
    super( began, ended );
22
  }
23
24
  /**
25
   * @return The opening sigil token.
26
   */
27
  public String getBegan() {
28
    return getKey().get();
29
  }
30
31
  /**
32
   * @return The closing sigil token, or the empty string if none set.
33
   */
34
  public String getEnded() {
35
    return getValue().get();
36
  }
37
}
138
M src/main/java/com/keenwrite/sigils/YamlSigilOperator.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.sigils;
29
30
import java.util.regex.Pattern;
31
32
import static java.lang.String.format;
33
import static java.util.regex.Pattern.compile;
34
import static java.util.regex.Pattern.quote;
353
364
/**
375
 * Brackets definition keys with token delimiters.
386
 */
397
public class YamlSigilOperator extends SigilOperator {
408
  public static final char KEY_SEPARATOR_DEF = '.';
41
42
  private static final String mDelimiterBegan =
43
      getUserPreferences().getDefDelimiterBegan();
44
  private static final String mDelimiterEnded =
45
      getUserPreferences().getDefDelimiterEnded();
46
47
  /**
48
   * Non-greedy match of key names delimited by definition tokens.
49
   */
50
  private static final String REGEX =
51
      format( "(%s.*?%s)", quote( mDelimiterBegan ), quote( mDelimiterEnded ) );
529
53
  /**
54
   * Compiled regular expression for matching delimited references.
55
   */
56
  public static final Pattern REGEX_PATTERN = compile( REGEX );
10
  public YamlSigilOperator( final Tokens tokens ) {
11
    super( tokens );
12
  }
5713
5814
  /**
...
7430
   * @return The given key bracketed by definition token symbols.
7531
   */
76
  public static String entoken( final String key ) {
32
  public String entoken( final String key ) {
7733
    assert key != null;
78
    return mDelimiterBegan + key + mDelimiterEnded;
34
    return getBegan() + key + getEnded();
7935
  }
8036
...
8743
   * @return The given key with the delimiters removed.
8844
   */
89
  public static String detoken( final String key ) {
90
    final int beganLen = mDelimiterBegan.length();
91
    final int endedLen = mDelimiterEnded.length();
45
  public String detoken( final String key ) {
46
    final int beganLen = getBegan().length();
47
    final int endedLen = getEnded().length();
9248
9349
    return key.length() > beganLen + endedLen
94
        ? key.substring( beganLen, key.length() - endedLen )
95
        : key;
50
      ? key.substring( beganLen, key.length() - endedLen )
51
      : key;
9652
  }
9753
}
M src/main/java/com/keenwrite/spelling/api/SpellCheckListener.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.spelling.api;
293
M src/main/java/com/keenwrite/spelling/api/SpellChecker.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.spelling.api;
293
A src/main/java/com/keenwrite/spelling/api/package-info.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * Redistribution and use in source and binary forms, with or without
4
 * modification, are permitted provided that the following conditions are met:
5
 *
6
 *  o Redistributions of source code must retain the above copyright
7
 *    notice, this list of conditions and the following disclaimer.
8
 *
9
 *  o Redistributions in binary form must reproduce the above copyright
10
 *    notice, this list of conditions and the following disclaimer in the
11
 *    documentation and/or other materials provided with the distribution.
12
 *
13
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
14
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
15
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
16
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
17
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
18
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
19
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
20
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
21
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
 */
25
26
/**
27
 * This package defines interfaces for spell checking implementations.
28
 */
29
package com.keenwrite.spelling.api;
130
M src/main/java/com/keenwrite/spelling/impl/PermissiveSpeller.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.spelling.impl;
293
M src/main/java/com/keenwrite/spelling/impl/SymSpellSpeller.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.spelling.impl;
293
4
import com.keenwrite.exceptions.MissingFileException;
305
import com.keenwrite.spelling.api.SpellCheckListener;
316
import com.keenwrite.spelling.api.SpellChecker;
327
import io.gitlab.rxp90.jsymspell.SuggestItem;
338
import io.gitlab.rxp90.jsymspell.SymSpell;
349
import io.gitlab.rxp90.jsymspell.SymSpellBuilder;
3510
11
import java.io.BufferedReader;
12
import java.io.InputStreamReader;
3613
import java.text.BreakIterator;
3714
import java.util.ArrayList;
3815
import java.util.Collection;
3916
import java.util.List;
17
import java.util.stream.Collectors;
4018
19
import static com.keenwrite.Constants.LEXICONS_DIRECTORY;
20
import static com.keenwrite.StatusBarNotifier.clue;
4121
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity;
4222
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.ALL;
4323
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.CLOSEST;
4424
import static java.lang.Character.isLetter;
25
import static java.nio.charset.StandardCharsets.UTF_8;
4526
4627
/**
...
5334
5435
  /**
55
   * Creates a new lexicon for the given collection of lexemes.
36
   * Creates a new spellchecker for a lexicon of words found in the specified
37
   * file.
5638
   *
57
   * @param lexiconWords The words in the lexicon to add for spell checking,
58
   *                     must not be empty.
39
   * @param filename Lexicon language file (e.g., "en.txt").
5940
   * @return An instance of {@link SpellChecker} that can check if a word
60
   * is correct and suggest alternatives.
41
   * is correct and suggest alternatives, or {@link PermissiveSpeller} if the
42
   * lexicon cannot be loaded.
6143
   */
62
  public static SpellChecker forLexicon(
44
  public static SpellChecker forLexicon( final String filename ) {
45
    try {
46
      final var lexicon = readLexicon( filename );
47
      return SymSpellSpeller.forLexicon( lexicon );
48
    } catch( final Exception ex ) {
49
      clue( ex );
50
      return new PermissiveSpeller();
51
    }
52
  }
53
54
  private static SpellChecker forLexicon(
6355
      final Collection<String> lexiconWords ) {
6456
    assert lexiconWords != null && !lexiconWords.isEmpty();
6557
66
    final SymSpellBuilder builder = new SymSpellBuilder()
58
    final var builder = new SymSpellBuilder()
6759
        .setLexiconWords( lexiconWords );
6860
...
127119
      previousIndex = boundaryIndex;
128120
      boundaryIndex = mBreakIterator.next();
121
    }
122
  }
123
124
  @SuppressWarnings("SameParameterValue")
125
  private static Collection<String> readLexicon( final String filename )
126
      throws Exception {
127
    final var path = '/' + LEXICONS_DIRECTORY + '/' + filename;
128
129
    try( final var resource =
130
             SymSpellSpeller.class.getResourceAsStream( path ) ) {
131
      if( resource == null ) {
132
        throw new MissingFileException( path );
133
      }
134
135
      try( final var isr = new InputStreamReader( resource, UTF_8 );
136
           final var reader = new BufferedReader( isr ) ) {
137
        return reader.lines().collect( Collectors.toList() );
138
      }
129139
    }
130140
  }
A src/main/java/com/keenwrite/spelling/impl/TextEditorSpeller.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.spelling.impl;
3
4
import com.keenwrite.spelling.api.SpellCheckListener;
5
import com.keenwrite.spelling.api.SpellChecker;
6
import com.vladsch.flexmark.parser.Parser;
7
import com.vladsch.flexmark.util.ast.NodeVisitor;
8
import com.vladsch.flexmark.util.ast.VisitHandler;
9
import org.fxmisc.richtext.StyleClassedTextArea;
10
import org.fxmisc.richtext.model.StyleSpansBuilder;
11
12
import java.util.Collection;
13
import java.util.concurrent.atomic.AtomicInteger;
14
15
import static com.keenwrite.spelling.impl.SymSpellSpeller.forLexicon;
16
import static java.util.Collections.emptyList;
17
import static java.util.Collections.singleton;
18
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
19
20
/**
21
 * Responsible for checking the spelling of a document being edited.
22
 */
23
public class TextEditorSpeller {
24
  /**
25
   * Only load the dictionary into memory once, because it's huge.
26
   */
27
  private static final SpellChecker sSpellChecker = forLexicon( "en.txt" );
28
29
  public TextEditorSpeller() {
30
  }
31
32
  /**
33
   * Delegates to {@link #spellcheck(StyleClassedTextArea, String, int)}.
34
   * call to spell check the entire document.
35
   */
36
  public void checkDocument( final StyleClassedTextArea editor ) {
37
    spellcheck( editor, editor.getText(), -1 );
38
  }
39
40
  /**
41
   * Listen for changes to the any particular paragraph and perform a quick
42
   * spell check upon it. The style classes in the editor will be changed to
43
   * mark any spelling mistakes in the paragraph. The user may then interact
44
   * with any misspelled word (i.e., any piece of text that is marked) to
45
   * revise the spelling.
46
   *
47
   * @param editor The text area containing paragraphs to spellcheck.
48
   */
49
  public void checkParagraphs( final StyleClassedTextArea editor ) {
50
    // Use the plain text changes so that notifications of style changes
51
    // are suppressed. Checking against the identity ensures that only
52
    // new text additions or deletions trigger proofreading.
53
    editor.plainTextChanges()
54
          .filter( p -> !p.isIdentity() ).subscribe( change -> {
55
56
      // Check current paragraph; the whole document was checked upon opening.
57
      final var offset = change.getPosition();
58
      final var position = editor.offsetToPosition( offset, Forward );
59
      final var paraId = position.getMajor();
60
      final var paragraph = editor.getParagraph( paraId );
61
      final var text = paragraph.getText();
62
63
      // Prevent doubling-up styles.
64
      editor.clearStyle( paraId );
65
66
      spellcheck( editor, text, paraId );
67
    } );
68
  }
69
70
  /**
71
   * Spellchecks a subset of the entire document.
72
   *
73
   * @param text   Look up words for this text in the lexicon.
74
   * @param paraId Set to -1 to apply resulting style spans to the entire
75
   *               text.
76
   */
77
  private void spellcheck(
78
    final StyleClassedTextArea editor, final String text, final int paraId ) {
79
    final var builder = new StyleSpansBuilder<Collection<String>>();
80
    final var runningIndex = new AtomicInteger( 0 );
81
82
    // The text nodes must be relayed through a contextual "visitor" that
83
    // can return text in chunks with correlative offsets into the string.
84
    // This allows Markdown, R Markdown, XML, and R XML documents to return
85
    // sets of words to check.
86
87
    final var node = mParser.parse( text );
88
    final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> {
89
      // Treat hyphenated compound words as individual words.
90
      final var check = visited.replace( '-', ' ' );
91
92
      sSpellChecker.proofread( check, ( misspelled, prevIndex, currIndex ) -> {
93
        prevIndex += bIndex;
94
        currIndex += bIndex;
95
96
        // Clear styling between lexiconically absent words.
97
        builder.add( emptyList(), prevIndex - runningIndex.get() );
98
        builder.add( singleton( "spelling" ), currIndex - prevIndex );
99
        runningIndex.set( currIndex );
100
      } );
101
    } );
102
103
    visitor.visit( node );
104
105
    // If the running index was set, at least one word triggered the listener.
106
    if( runningIndex.get() > 0 ) {
107
      // Clear styling after the last lexiconically absent word.
108
      builder.add( emptyList(), text.length() - runningIndex.get() );
109
110
      final var spans = builder.create();
111
112
      if( paraId >= 0 ) {
113
        editor.setStyleSpans( paraId, 0, spans );
114
      }
115
      else {
116
        editor.setStyleSpans( 0, spans );
117
      }
118
    }
119
  }
120
121
  /**
122
   * TODO: #59 -- Replace using Markdown processor instantiated for Markdown
123
   * files.
124
   */
125
  private final Parser mParser = Parser.builder().build();
126
127
  /**
128
   * TODO: #59 -- Replace with generic interface; provide Markdown/XML
129
   * implementations.
130
   */
131
  private static final class TextVisitor {
132
    private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>(
133
      com.vladsch.flexmark.ast.Text.class, this::visit )
134
    );
135
136
    private final SpellCheckListener mConsumer;
137
138
    public TextVisitor( final SpellCheckListener consumer ) {
139
      mConsumer = consumer;
140
    }
141
142
    private void visit( final com.vladsch.flexmark.util.ast.Node node ) {
143
      if( node instanceof com.vladsch.flexmark.ast.Text ) {
144
        mConsumer.accept( node.getChars().toString(),
145
                          node.getStartOffset(),
146
                          node.getEndOffset() );
147
      }
148
149
      mVisitor.visitChildren( node );
150
    }
151
  }
152
}
1153
A src/main/java/com/keenwrite/spelling/impl/package-info.java
1
/* Copyright 2020 White Magic Software, Ltd.
2
 *
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
/**
29
 * This package contains classes for spell checking implementations.
30
 */
31
package com.keenwrite.spelling.impl;
132
A src/main/java/com/keenwrite/ui/actions/Action.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.actions;
3
4
import com.keenwrite.Messages;
5
import com.keenwrite.util.GenericBuilder;
6
import de.jensd.fx.glyphs.GlyphIcons;
7
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
8
import javafx.event.ActionEvent;
9
import javafx.event.EventHandler;
10
import javafx.scene.control.Button;
11
import javafx.scene.control.Menu;
12
import javafx.scene.control.MenuItem;
13
import javafx.scene.control.Tooltip;
14
import javafx.scene.input.KeyCombination;
15
16
import java.util.ArrayList;
17
import java.util.List;
18
19
import static de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory.get;
20
import static javafx.scene.input.KeyCombination.valueOf;
21
22
/**
23
 * Defines actions the user can take through GUI interactions
24
 */
25
public class Action implements MenuAction {
26
  private final String mText;
27
  private final KeyCombination mAccelerator;
28
  private final GlyphIcons mIcon;
29
  private final EventHandler<ActionEvent> mHandler;
30
  private final List<MenuAction> mSubActions = new ArrayList<>();
31
32
  public Action(
33
    final String text,
34
    final String accelerator,
35
    final GlyphIcons icon,
36
    final EventHandler<ActionEvent> handler ) {
37
    assert text != null;
38
    assert handler != null;
39
40
    mText = text;
41
    mAccelerator = accelerator == null ? null : valueOf( accelerator );
42
    mIcon = icon;
43
    mHandler = handler;
44
  }
45
46
  /**
47
   * Runs this action. Most actions are mapped to menu items, but some actions
48
   * (such as the Insert key to toggle overwrite mode) are not.
49
   */
50
  public void execute() {
51
    mHandler.handle( new ActionEvent() );
52
  }
53
54
  @Override
55
  public MenuItem createMenuItem() {
56
    // This will either become a menu or a menu item, depending on whether
57
    // sub-actions are defined.
58
    final MenuItem menuItem;
59
60
    if( mSubActions.isEmpty() ) {
61
      // Regular menu item has no sub-menus.
62
      menuItem = new MenuItem( mText );
63
    }
64
    else {
65
      // Sub-actions are translated into sub-menu items beneath this action.
66
      final var submenu = new Menu( mText );
67
68
      for( final var action : mSubActions ) {
69
        // Recursive call that creates a sub-menu hierarchy.
70
        submenu.getItems().add( action.createMenuItem() );
71
      }
72
73
      menuItem = submenu;
74
    }
75
76
    if( mAccelerator != null ) {
77
      menuItem.setAccelerator( mAccelerator );
78
    }
79
80
    if( mIcon != null ) {
81
      menuItem.setGraphic( get().createIcon( mIcon ) );
82
    }
83
84
    menuItem.setOnAction( mHandler );
85
86
    return menuItem;
87
  }
88
89
  @Override
90
  public Button createToolBarNode() {
91
    final var button = createIconButton();
92
    var tooltip = mText;
93
94
    if( tooltip.endsWith( "..." ) ) {
95
      tooltip = tooltip.substring( 0, tooltip.length() - 3 );
96
    }
97
98
    if( mAccelerator != null ) {
99
      tooltip += " (" + mAccelerator.getDisplayText() + ')';
100
    }
101
102
    button.setTooltip( new Tooltip( tooltip ) );
103
    button.setFocusTraversable( false );
104
    button.setOnAction( mHandler );
105
106
    return button;
107
  }
108
109
  private Button createIconButton() {
110
    final var button = new Button();
111
    button.setGraphic( get().createIcon( mIcon, "1.2em" ) );
112
    return button;
113
  }
114
115
  /**
116
   * Adds subordinate actions to the menu. This is used to establish sub-menu
117
   * relationships. The default behaviour does not wire up any registration;
118
   * subclasses are responsible for handling how actions relate to one another.
119
   *
120
   * @param action Actions that only exist with respect to this action.
121
   */
122
  public MenuAction addSubActions( final MenuAction... action ) {
123
    mSubActions.addAll( List.of( action ) );
124
    return this;
125
  }
126
127
  /**
128
   * TODO: Reuse the {@link GenericBuilder}.
129
   *
130
   * @return The {@link Builder} for an instance of {@link Action}.
131
   */
132
  public static Builder builder() {
133
    return new Builder();
134
  }
135
136
  /**
137
   * Provides a fluent interface around constructing actions so that duplication
138
   * can be avoided.
139
   */
140
  public static class Builder {
141
    private String mText;
142
    private String mAccelerator;
143
    private GlyphIcons mIcon;
144
    private EventHandler<ActionEvent> mHandler;
145
146
    /**
147
     * Sets the text, icon, and accelerator for a given action identifier.
148
     * See the "App.action" entries in the messages properties file for details.
149
     *
150
     * @param id The identifier to look up in the properties file.
151
     * @return An instance of {@link Builder} that can be built into an
152
     * instance of {@link Action}.
153
     */
154
    public Builder setId( final String id ) {
155
      final var prefix = "App.action." + id + ".";
156
      final var text = prefix + "text";
157
      final var icon = prefix + "icon";
158
      final var accelerator = prefix + "accelerator";
159
      final var builder = setText( text ).setIcon( icon );
160
161
      return Messages.containsKey( accelerator )
162
        ? builder.setAccelerator( Messages.get( accelerator ) )
163
        : builder;
164
    }
165
166
    /**
167
     * Sets the action text based on a resource bundle key.
168
     *
169
     * @param key The key to look up in the {@link Messages}.
170
     * @return The corresponding value, or the key name if none found.
171
     */
172
    private Builder setText( final String key ) {
173
      mText = Messages.get( key, key );
174
      return this;
175
    }
176
177
    private Builder setAccelerator( final String accelerator ) {
178
      mAccelerator = accelerator;
179
      return this;
180
    }
181
182
    private Builder setIcon( final GlyphIcons icon ) {
183
      mIcon = icon;
184
      return this;
185
    }
186
187
    private Builder setIcon( final String iconKey ) {
188
      assert iconKey != null;
189
190
      final var iconValue = Messages.get( iconKey );
191
192
      return iconKey.equals( iconValue )
193
        ? this
194
        : setIcon( getIcon( iconValue ) );
195
    }
196
197
    public Builder setHandler( final EventHandler<ActionEvent> handler ) {
198
      mHandler = handler;
199
      return this;
200
    }
201
202
    public Action build() {
203
      return new Action( mText, mAccelerator, mIcon, mHandler );
204
    }
205
206
    private GlyphIcons getIcon( final String name ) {
207
      return FontAwesomeIcon.valueOf( name.toUpperCase() );
208
    }
209
  }
210
}
1211
A src/main/java/com/keenwrite/ui/actions/ApplicationActions.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.actions;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.MainPane;
6
import com.keenwrite.editors.TextDefinition;
7
import com.keenwrite.editors.TextEditor;
8
import com.keenwrite.preferences.PreferencesController;
9
import com.keenwrite.preferences.Workspace;
10
import com.keenwrite.processors.ProcessorContext;
11
import com.keenwrite.search.SearchModel;
12
import com.keenwrite.ui.controls.SearchBar;
13
import javafx.scene.control.Alert;
14
import javafx.scene.image.ImageView;
15
import javafx.stage.Window;
16
import javafx.stage.WindowEvent;
17
18
import static com.keenwrite.Bootstrap.APP_TITLE;
19
import static com.keenwrite.Constants.ICON_DIALOG;
20
import static com.keenwrite.ExportFormat.*;
21
import static com.keenwrite.Messages.get;
22
import static com.keenwrite.StatusBarNotifier.clue;
23
import static com.keenwrite.StatusBarNotifier.getStatusBar;
24
import static com.keenwrite.preferences.Workspace.KEY_UI_RECENT_DIR;
25
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
26
import static java.nio.file.Files.writeString;
27
import static javafx.event.Event.fireEvent;
28
import static javafx.scene.control.Alert.AlertType.INFORMATION;
29
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
30
31
/**
32
 * Responsible for abstracting how functionality is mapped to the application.
33
 * This allows users to customize accelerator keys and will provide pluggable
34
 * functionality so that different text markup languages can change documents
35
 * using their respective syntax.
36
 */
37
@SuppressWarnings("NonAsciiCharacters")
38
public class ApplicationActions {
39
  private static final String STYLE_SEARCH = "search";
40
41
  /**
42
   * When an action is executed, this is one of the recipients.
43
   */
44
  private final MainPane mMainPane;
45
46
  /**
47
   * Tracks finding text in the active document.
48
   */
49
  private final SearchModel mSearchModel;
50
51
  public ApplicationActions( final MainPane mainPane ) {
52
    mMainPane = mainPane;
53
    mSearchModel = new SearchModel();
54
    mSearchModel.matchOffsetProperty().addListener( ( c, o, n ) -> {
55
      final var editor = getActiveTextEditor();
56
57
      // Clear highlighted areas before adding highlighting to a new region.
58
      if( o != null ) {
59
        editor.unstylize( STYLE_SEARCH );
60
      }
61
62
      if( n != null ) {
63
        editor.moveTo( n.getStart() );
64
        editor.stylize( n, STYLE_SEARCH );
65
      }
66
    } );
67
68
    // When the active text editor changes, update the haystack.
69
    mMainPane.activeTextEditorProperty().addListener(
70
      ( c, o, n ) -> mSearchModel.search( getActiveTextEditor().getText() )
71
    );
72
  }
73
74
  public void file‿new() {
75
    getMainPane().newTextEditor();
76
  }
77
78
  public void file‿open() {
79
    getMainPane().open( createFileChooser().openFiles() );
80
  }
81
82
  public void file‿close() {
83
    getMainPane().close();
84
  }
85
86
  public void file‿close_all() {
87
    getMainPane().closeAll();
88
  }
89
90
  public void file‿save() {
91
    getMainPane().save();
92
  }
93
94
  public void file‿save_as() {
95
    final var file = createFileChooser().saveAs();
96
    file.ifPresent( ( f ) -> getMainPane().saveAs( f ) );
97
  }
98
99
  public void file‿save_all() {
100
    getMainPane().saveAll();
101
  }
102
103
  public void file‿export‿html_svg() {
104
    file‿export( HTML_TEX_SVG );
105
  }
106
107
  public void file‿export‿html_tex() {
108
    file‿export( HTML_TEX_DELIMITED );
109
  }
110
111
  public void file‿export‿markdown() {
112
    file‿export( MARKDOWN_PLAIN );
113
  }
114
115
  private void file‿export( final ExportFormat format ) {
116
    final var editor = getActiveTextEditor();
117
    final var context = createProcessorContext( editor );
118
    final var chain = createProcessors( context );
119
    final var doc = editor.getText();
120
    final var export = chain.apply( doc );
121
    final var filename = format.toExportFilename( editor.getPath() );
122
    final var chooser = createFileChooser();
123
    final var file = chooser.exportAs( filename );
124
125
    file.ifPresent( ( f ) -> {
126
      try {
127
        writeString( f.toPath(), export );
128
        final var m = get( "Main.status.export.success", f.toString() );
129
        clue( m );
130
      } catch( final Exception e ) {
131
        clue( e );
132
      }
133
    } );
134
  }
135
136
  private ProcessorContext createProcessorContext( final TextEditor editor ) {
137
    return getMainPane().createProcessorContext( editor );
138
  }
139
140
  public void file‿exit() {
141
    final var window = getWindow();
142
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
143
  }
144
145
  public void edit‿undo() {
146
    getActiveTextEditor().undo();
147
  }
148
149
  public void edit‿redo() {
150
    getActiveTextEditor().redo();
151
  }
152
153
  public void edit‿cut() {
154
    getActiveTextEditor().cut();
155
  }
156
157
  public void edit‿copy() {
158
    getActiveTextEditor().copy();
159
  }
160
161
  public void edit‿paste() {
162
    getActiveTextEditor().paste();
163
  }
164
165
  public void edit‿select_all() {
166
    getActiveTextEditor().selectAll();
167
  }
168
169
  public void edit‿find() {
170
    final var nodes = getStatusBar().getLeftItems();
171
172
    if( nodes.isEmpty() ) {
173
      final var searchBar = new SearchBar();
174
175
      searchBar.matchIndexProperty().bind( mSearchModel.matchIndexProperty() );
176
      searchBar.matchCountProperty().bind( mSearchModel.matchCountProperty() );
177
178
      searchBar.setOnCancelAction( ( event ) -> {
179
        final var editor = getActiveTextEditor();
180
        nodes.remove( searchBar );
181
        editor.unstylize( STYLE_SEARCH );
182
        editor.getNode().requestFocus();
183
      } );
184
185
      searchBar.addInputListener( ( c, o, n ) -> {
186
        if( n != null && !n.isEmpty() ) {
187
          mSearchModel.search( n, getActiveTextEditor().getText() );
188
        }
189
      } );
190
191
      searchBar.setOnNextAction( ( event ) -> edit‿find_next() );
192
      searchBar.setOnPrevAction( ( event ) -> edit‿find_prev() );
193
194
      nodes.add( searchBar );
195
      searchBar.requestFocus();
196
    }
197
    else {
198
      nodes.clear();
199
    }
200
  }
201
202
  public void edit‿find_next() {
203
    mSearchModel.advance();
204
  }
205
206
  public void edit‿find_prev() {
207
    mSearchModel.retreat();
208
  }
209
210
  public void edit‿preferences() {
211
    new PreferencesController( getWorkspace() ).show();
212
  }
213
214
  public void format‿bold() {
215
    getActiveTextEditor().bold();
216
  }
217
218
  public void format‿italic() {
219
    getActiveTextEditor().italic();
220
  }
221
222
  public void format‿superscript() {
223
    getActiveTextEditor().superscript();
224
  }
225
226
  public void format‿subscript() {
227
    getActiveTextEditor().subscript();
228
  }
229
230
  public void format‿strikethrough() {
231
    getActiveTextEditor().strikethrough();
232
  }
233
234
  public void insert‿blockquote() {
235
    getActiveTextEditor().blockquote();
236
  }
237
238
  public void insert‿code() {
239
    getActiveTextEditor().code();
240
  }
241
242
  public void insert‿fenced_code_block() {
243
    getActiveTextEditor().fencedCodeBlock();
244
  }
245
246
  public void insert‿link() {
247
    createMarkdownDialog().insertLink( getActiveTextEditor().getTextArea() );
248
  }
249
250
  public void insert‿image() {
251
    createMarkdownDialog().insertImage( getActiveTextEditor().getTextArea() );
252
  }
253
254
  private MarkdownCommands createMarkdownDialog() {
255
    return new MarkdownCommands(
256
      getMainPane(), getActiveTextEditor().getPath() );
257
  }
258
259
  public void insert‿heading_1() {
260
    insert‿heading( 1 );
261
  }
262
263
  public void insert‿heading_2() {
264
    insert‿heading( 2 );
265
  }
266
267
  public void insert‿heading_3() {
268
    insert‿heading( 3 );
269
  }
270
271
  private void insert‿heading( final int level ) {
272
    getActiveTextEditor().heading( level );
273
  }
274
275
  public void insert‿unordered_list() {
276
    getActiveTextEditor().unorderedList();
277
  }
278
279
  public void insert‿ordered_list() {
280
    getActiveTextEditor().orderedList();
281
  }
282
283
  public void insert‿horizontal_rule() {
284
    getActiveTextEditor().horizontalRule();
285
  }
286
287
  public void definition‿create() {
288
    getActiveTextDefinition().createDefinition();
289
  }
290
291
  public void definition‿rename() {
292
    getActiveTextDefinition().renameDefinition();
293
  }
294
295
  public void definition‿delete() {
296
    getActiveTextDefinition().deleteDefinitions();
297
  }
298
299
  public void definition‿autoinsert() {
300
    getMainPane().autoinsert();
301
  }
302
303
  public void view‿refresh() {
304
    getMainPane().viewRefresh();
305
  }
306
307
  public void view‿preview() {
308
    getMainPane().viewPreview();
309
  }
310
311
  public void help‿about() {
312
    final Alert alert = new Alert( INFORMATION );
313
    alert.setTitle( get( "Dialog.about.title", APP_TITLE ) );
314
    alert.setHeaderText( get( "Dialog.about.header", APP_TITLE ) );
315
    alert.setContentText( get( "Dialog.about.content" ) );
316
    alert.setGraphic( new ImageView( ICON_DIALOG ) );
317
    alert.initOwner( getWindow() );
318
    alert.showAndWait();
319
  }
320
321
  private FileChooserCommand createFileChooser() {
322
    final var dir = getWorkspace().fileProperty( KEY_UI_RECENT_DIR );
323
    return new FileChooserCommand( getWindow(), dir );
324
  }
325
326
  private MainPane getMainPane() {
327
    return mMainPane;
328
  }
329
330
  private TextEditor getActiveTextEditor() {
331
    return getMainPane().getActiveTextEditor();
332
  }
333
334
  private TextDefinition getActiveTextDefinition() {
335
    return getMainPane().getActiveTextDefinition();
336
  }
337
338
  private Workspace getWorkspace() {
339
    return mMainPane.getWorkspace();
340
  }
341
342
  private Window getWindow() {
343
    return getMainPane().getWindow();
344
  }
345
}
1346
A src/main/java/com/keenwrite/ui/actions/ApplicationMenuBar.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.actions;
3
4
import javafx.event.ActionEvent;
5
import javafx.event.EventHandler;
6
import javafx.scene.Node;
7
import javafx.scene.control.Menu;
8
import javafx.scene.control.MenuBar;
9
import javafx.scene.control.MenuItem;
10
import javafx.scene.control.ToolBar;
11
import javafx.scene.layout.VBox;
12
13
import java.util.HashMap;
14
import java.util.Map;
15
16
import static com.keenwrite.Messages.get;
17
18
/**
19
 * Responsible for wiring all application actions to menus, toolbar buttons,
20
 * and keyboard shortcuts.
21
 */
22
public class ApplicationMenuBar {
23
24
  private final Map<String, Action> mMap = new HashMap<>( 64 );
25
26
  /**
27
   * Empty constructor.
28
   */
29
  public ApplicationMenuBar() {
30
  }
31
32
  /**
33
   * Creates the main application affordances.
34
   *
35
   * @param actions The {@link ApplicationActions} that map user interface
36
   *                selections to executable code.
37
   * @return An instance of {@link Node} that contains the menu and toolbar.
38
   */
39
  public Node createMenuBar( final ApplicationActions actions ) {
40
    final var SEPARATOR_ACTION = new SeparatorAction();
41
42
    //@formatter:off
43
    final var menuBar = new MenuBar(
44
    createMenu(
45
      get( "Main.menu.file" ),
46
      addAction( "file.new", e -> actions.file‿new() ),
47
      addAction( "file.open", e -> actions.file‿open() ),
48
      SEPARATOR_ACTION,
49
      addAction( "file.close", e -> actions.file‿close() ),
50
      addAction( "file.close_all", e -> actions.file‿close_all() ),
51
      SEPARATOR_ACTION,
52
      addAction( "file.save", e -> actions.file‿save() ),
53
      addAction( "file.save_as", e -> actions.file‿save_as() ),
54
      addAction( "file.save_all", e -> actions.file‿save_all() ),
55
      SEPARATOR_ACTION,
56
      addAction( "file.export", e -> {} )
57
        .addSubActions(
58
          addAction( "file.export.html_svg", e -> actions.file‿export‿html_svg() ),
59
          addAction( "file.export.html_tex", e -> actions.file‿export‿html_tex() ),
60
          addAction( "file.export.markdown", e -> actions.file‿export‿markdown() )
61
        ),
62
      SEPARATOR_ACTION,
63
      addAction( "file.exit", e -> actions.file‿exit() )
64
    ),
65
    createMenu(
66
      get( "Main.menu.edit" ),
67
      SEPARATOR_ACTION,
68
      addAction( "edit.undo", e -> actions.edit‿undo() ),
69
      addAction( "edit.redo", e -> actions.edit‿redo() ),
70
      SEPARATOR_ACTION,
71
      addAction( "edit.cut", e -> actions.edit‿cut() ),
72
      addAction( "edit.copy", e -> actions.edit‿copy() ),
73
      addAction( "edit.paste", e -> actions.edit‿paste() ),
74
      addAction( "edit.select_all", e -> actions.edit‿select_all() ),
75
      SEPARATOR_ACTION,
76
      addAction( "edit.find", e -> actions.edit‿find() ),
77
      addAction( "edit.find_next", e -> actions.edit‿find_next() ),
78
      addAction( "edit.find_prev", e -> actions.edit‿find_prev() ),
79
      SEPARATOR_ACTION,
80
      addAction( "edit.preferences", e -> actions.edit‿preferences() )
81
    ),
82
    createMenu(
83
      get( "Main.menu.format" ),
84
      addAction( "format.bold", e -> actions.format‿bold() ),
85
      addAction( "format.italic", e -> actions.format‿italic() ),
86
      addAction( "format.superscript", e -> actions.format‿superscript() ),
87
      addAction( "format.subscript", e -> actions.format‿subscript() ),
88
      addAction( "format.strikethrough", e -> actions.format‿strikethrough() )
89
    ),
90
    createMenu(
91
      get( "Main.menu.insert" ),
92
      addAction( "insert.blockquote", e -> actions.insert‿blockquote() ),
93
      addAction( "insert.code", e -> actions.insert‿code() ),
94
      addAction( "insert.fenced_code_block", e -> actions.insert‿fenced_code_block() ),
95
      SEPARATOR_ACTION,
96
      addAction( "insert.link", e -> actions.insert‿link() ),
97
      addAction( "insert.image", e -> actions.insert‿image() ),
98
      SEPARATOR_ACTION,
99
      addAction( "insert.heading_1", e -> actions.insert‿heading_1() ),
100
      addAction( "insert.heading_2", e -> actions.insert‿heading_2() ),
101
      addAction( "insert.heading_3", e -> actions.insert‿heading_3() ),
102
      SEPARATOR_ACTION,
103
      addAction( "insert.unordered_list", e -> actions.insert‿unordered_list() ),
104
      addAction( "insert.ordered_list", e -> actions.insert‿ordered_list() ),
105
      addAction( "insert.horizontal_rule", e -> actions.insert‿horizontal_rule() )
106
    ),
107
    createMenu(
108
      get( "Main.menu.definition" ),
109
      addAction( "definition.insert", e -> actions.definition‿autoinsert() ),
110
      SEPARATOR_ACTION,
111
      addAction( "definition.create", e -> actions.definition‿create() ),
112
      addAction( "definition.rename", e -> actions.definition‿rename() ),
113
      addAction( "definition.delete", e -> actions.definition‿delete() )
114
    ),
115
    createMenu(
116
      get( "Main.menu.view" ),
117
      addAction( "view.refresh", e -> actions.view‿refresh() ),
118
      SEPARATOR_ACTION,
119
      addAction( "view.preview", e -> actions.view‿preview() )
120
    ),
121
    createMenu(
122
      get( "Main.menu.help" ),
123
      addAction( "help.about", e -> actions.help‿about() )
124
    ) );
125
    //@formatter:on
126
127
    //@formatter:off
128
    final var toolBar = createToolBar(
129
      getAction( "file.new" ),
130
      getAction( "file.open" ),
131
      getAction( "file.save" ),
132
      SEPARATOR_ACTION,
133
      getAction( "edit.undo" ),
134
      getAction( "edit.redo" ),
135
      getAction( "edit.cut" ),
136
      getAction( "edit.copy" ),
137
      getAction( "edit.paste" ),
138
      SEPARATOR_ACTION,
139
      getAction( "format.bold" ),
140
      getAction( "format.italic" ),
141
      getAction( "format.superscript" ),
142
      getAction( "format.subscript" ),
143
      getAction( "insert.blockquote" ),
144
      getAction( "insert.code" ),
145
      getAction( "insert.fenced_code_block" ),
146
      SEPARATOR_ACTION,
147
      getAction( "insert.link" ),
148
      getAction( "insert.image" ),
149
      SEPARATOR_ACTION,
150
      getAction( "insert.heading_1" ),
151
      SEPARATOR_ACTION,
152
      getAction( "insert.unordered_list" ),
153
      getAction( "insert.ordered_list" )
154
    );
155
    //@formatter:on
156
157
    return new VBox( menuBar, toolBar );
158
  }
159
160
  /**
161
   * Adds a new action to the list of actions.
162
   *
163
   * @param key     The name of the action to register in {@link #mMap}.
164
   * @param handler Performs the action upon request.
165
   * @return The newly registered action.
166
   */
167
  private Action addAction(
168
    final String key, final EventHandler<ActionEvent> handler ) {
169
    assert key != null;
170
    assert handler != null;
171
172
    final var action = Action
173
      .builder()
174
      .setId( key )
175
      .setHandler( handler )
176
      .build();
177
178
    mMap.put( key, action );
179
180
    return action;
181
  }
182
183
  private Action getAction( final String key ) {
184
    return mMap.get( key );
185
  }
186
187
  public static Menu createMenu(
188
    final String text, final MenuAction... actions ) {
189
    return new Menu( text, null, createMenuItems( actions ) );
190
  }
191
192
  public static MenuItem[] createMenuItems( final MenuAction... actions ) {
193
    final var menuItems = new MenuItem[ actions.length ];
194
195
    for( var i = 0; i < actions.length; i++ ) {
196
      menuItems[ i ] = actions[ i ].createMenuItem();
197
    }
198
199
    return menuItems;
200
  }
201
202
  private static ToolBar createToolBar( final MenuAction... actions ) {
203
    return new ToolBar( createToolBarButtons( actions ) );
204
  }
205
206
  private static Node[] createToolBarButtons( final MenuAction... actions ) {
207
    final var len = actions.length;
208
    final var nodes = new Node[ len ];
209
210
    for( var i = 0; i < len; i++ ) {
211
      nodes[ i ] = actions[ i ].createToolBarNode();
212
    }
213
214
    return nodes;
215
  }
216
}
1217
A src/main/java/com/keenwrite/ui/actions/FileChooserCommand.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.actions;
3
4
import com.keenwrite.Messages;
5
import com.keenwrite.io.FileType;
6
import com.keenwrite.service.Settings;
7
import javafx.beans.property.Property;
8
import javafx.stage.FileChooser;
9
import javafx.stage.FileChooser.ExtensionFilter;
10
import javafx.stage.Window;
11
12
import java.io.File;
13
import java.util.ArrayList;
14
import java.util.List;
15
import java.util.Optional;
16
17
import static com.keenwrite.Constants.*;
18
import static com.keenwrite.Messages.get;
19
import static com.keenwrite.io.FileType.*;
20
import static java.lang.String.format;
21
22
/**
23
 * Responsible for opening a dialog that provides users with the ability to
24
 * select files.
25
 */
26
public class FileChooserCommand {
27
  private static final String FILTER_EXTENSION_TITLES =
28
    "Dialog.file.choose.filter";
29
30
  /**
31
   * Dialog owner.
32
   */
33
  private final Window mParent;
34
35
  /**
36
   * Set to the directory of most recently selected file.
37
   */
38
  private final Property<File> mDirectory;
39
40
  /**
41
   * Constructs a new {@link FileChooserCommand} that will attach to a given
42
   * parent window and update the given property upon a successful selection.
43
   *
44
   * @param parent    The parent window that will own the dialog.
45
   * @param directory The most recently opened file's directory property.
46
   */
47
  public FileChooserCommand(
48
    final Window parent, final Property<File> directory ) {
49
    mParent = parent;
50
    mDirectory = directory;
51
  }
52
53
  /**
54
   * Returns a list of files to be opened.
55
   *
56
   * @return A non-null, possibly empty list of files to open.
57
   */
58
  public List<File> openFiles() {
59
    final var dialog = createFileChooser(
60
      "Dialog.file.choose.open.title" );
61
    final var list = dialog.showOpenMultipleDialog( mParent );
62
    final List<java.io.File> selected = list == null ? List.of() : list;
63
    final var files = new ArrayList<File>( selected.size() );
64
65
    files.addAll( selected );
66
67
    if( !files.isEmpty() ) {
68
      setRecentDirectory( files.get( 0 ) );
69
    }
70
71
    return files;
72
  }
73
74
  /**
75
   * Allows saving the document under a new file name.
76
   *
77
   * @return The new file name.
78
   */
79
  public Optional<File> saveAs() {
80
    final var dialog = createFileChooser( "Dialog.file.choose.save.title" );
81
    return saveOrExportAs( dialog );
82
  }
83
84
  /**
85
   * Allows exporting the document to a new file format.
86
   *
87
   * @return The file name for exporting into.
88
   */
89
  public Optional<File> exportAs( final File filename ) {
90
    final var dialog = createFileChooser( "Dialog.file.choose.export.title" );
91
    dialog.setInitialFileName( filename.getName() );
92
    return saveOrExportAs( dialog );
93
  }
94
95
  /**
96
   * Helper method called when saving or exporting.
97
   *
98
   * @param dialog The {@link FileChooser} to display.
99
   * @return The file selected by the user.
100
   */
101
  private Optional<File> saveOrExportAs( final FileChooser dialog ) {
102
    final var file = dialog.showSaveDialog( mParent );
103
104
    setRecentDirectory( file );
105
106
    return Optional.ofNullable( file );
107
  }
108
109
  /**
110
   * Opens a new {@link FileChooser} at the previously selected directory.
111
   *
112
   * @param key Message key from resource bundle.
113
   * @return {@link FileChooser} GUI allowing the user to pick a file.
114
   */
115
  private FileChooser createFileChooser( final String key ) {
116
    final var chooser = new FileChooser();
117
118
    chooser.setTitle( get( key ) );
119
    chooser.getExtensionFilters().addAll( createExtensionFilters() );
120
    chooser.setInitialDirectory( mDirectory.getValue() );
121
122
    return chooser;
123
  }
124
125
  private List<ExtensionFilter> createExtensionFilters() {
126
    final List<ExtensionFilter> list = new ArrayList<>();
127
128
    // TODO: Return a list of all properties that match the filter prefix.
129
    // This will allow dynamic filters to be added and removed just by
130
    // updating the properties file.
131
    list.add( createExtensionFilter( ALL ) );
132
    list.add( createExtensionFilter( SOURCE ) );
133
    list.add( createExtensionFilter( DEFINITION ) );
134
    list.add( createExtensionFilter( XML ) );
135
136
    return list;
137
  }
138
139
  /**
140
   * Returns a filter for file name extensions recognized by the application
141
   * that can be opened by the user.
142
   *
143
   * @param filetype Used to find the globbing pattern for extensions.
144
   * @return A file name filter suitable for use by a FileDialog instance.
145
   */
146
  private ExtensionFilter createExtensionFilter(
147
    final FileType filetype ) {
148
    final var tKey = format( "%s.title.%s", FILTER_EXTENSION_TITLES, filetype );
149
    final var eKey = format( "%s.%s", GLOB_PREFIX_FILE, filetype );
150
151
    return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
152
  }
153
154
  /**
155
   * Sets the value for the most recent directly selected. This will get the
156
   * parent location from the given file. If the parent is a readable directory
157
   * then this will update the most recent directory property.
158
   *
159
   * @param file A file contained in a directory.
160
   */
161
  private void setRecentDirectory( final File file ) {
162
    if( file != null ) {
163
      final var parent = file.getParentFile();
164
      final var dir = parent == null ? USER_DIRECTORY : parent;
165
166
      if( dir.isDirectory() && dir.canRead() ) {
167
        mDirectory.setValue( dir );
168
      }
169
    }
170
  }
171
172
  private List<String> getExtensions( final String key ) {
173
    return getSettings().getStringSettingList( key );
174
  }
175
176
  private static Settings getSettings() {
177
    return sSettings;
178
  }
179
}
1180
A src/main/java/com/keenwrite/ui/actions/MarkdownCommands.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.actions;
3
4
import com.keenwrite.MainPane;
5
import com.keenwrite.editors.markdown.HyperlinkModel;
6
import com.keenwrite.editors.markdown.LinkVisitor;
7
import com.keenwrite.preferences.Workspace;
8
import com.keenwrite.processors.markdown.MarkdownProcessor;
9
import com.keenwrite.ui.dialogs.ImageDialog;
10
import com.keenwrite.ui.dialogs.LinkDialog;
11
import com.vladsch.flexmark.ast.Link;
12
import javafx.scene.control.Dialog;
13
import javafx.stage.Window;
14
import org.fxmisc.richtext.StyleClassedTextArea;
15
16
import java.nio.file.Path;
17
18
/**
19
 * TODO: Integrate the methods into {@link ApplicationActions}
20
 *
21
 * @deprecated Migrate into {@link ApplicationActions}.
22
 */
23
@Deprecated
24
public class MarkdownCommands {
25
26
  private final MainPane mParent;
27
  private final Path mBase;
28
29
  public MarkdownCommands( final MainPane parent, final Path path ) {
30
    mParent = parent;
31
    mBase = path.getParent();
32
  }
33
34
  public void insertLink( final StyleClassedTextArea textArea ) {
35
    insertObject( createLinkDialog( textArea ), textArea );
36
  }
37
38
  public void insertImage( final StyleClassedTextArea textArea ) {
39
    insertObject( createImageDialog(), textArea );
40
  }
41
42
  /**
43
   * Returns one of: selected text, word under cursor, or parsed hyperlink from
44
   * the markdown AST.
45
   *
46
   * @return An instance containing the link URL and display text.
47
   */
48
  private HyperlinkModel getHyperlink( final StyleClassedTextArea textArea ) {
49
    final var selectedText = textArea.getSelectedText();
50
51
    // Get the current paragraph, convert to Markdown nodes.
52
    final var mp = MarkdownProcessor.create( getWorkspace() );
53
    final var p = textArea.getCurrentParagraph();
54
    final var paragraph = textArea.getText( p );
55
    final var node = mp.toNode( paragraph );
56
    final var visitor = new LinkVisitor( textArea.getCaretColumn() );
57
    final var link = visitor.process( node );
58
59
    if( link != null ) {
60
      textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
61
    }
62
63
    return createHyperlinkModel(
64
      link, selectedText, "https://localhost"
65
    );
66
  }
67
68
  @SuppressWarnings("SameParameterValue")
69
  private HyperlinkModel createHyperlinkModel(
70
    final Link link, final String selection, final String url ) {
71
72
    return link == null
73
      ? new HyperlinkModel( selection, url )
74
      : new HyperlinkModel( link );
75
  }
76
77
  private Dialog<String> createLinkDialog(
78
    final StyleClassedTextArea textArea ) {
79
    return new LinkDialog( getWindow(), getHyperlink( textArea ) );
80
  }
81
82
  private Dialog<String> createImageDialog() {
83
    return new ImageDialog( getWindow(), getParentPath() );
84
  }
85
86
  private void insertObject(
87
    final Dialog<String> dialog, final StyleClassedTextArea textArea ) {
88
    dialog.showAndWait().ifPresent( textArea::replaceSelection );
89
  }
90
91
  private Path getParentPath() {
92
    return mBase;
93
  }
94
95
  private Workspace getWorkspace() {
96
    return mParent.getWorkspace();
97
  }
98
99
  private Window getWindow() {
100
    return mParent.getWindow();
101
  }
102
}
1103
A src/main/java/com/keenwrite/ui/actions/MenuAction.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.actions;
3
4
import javafx.scene.Node;
5
import javafx.scene.control.Button;
6
import javafx.scene.control.MenuItem;
7
import javafx.scene.control.Separator;
8
9
/**
10
 * Implementations are responsible for creating menu items and toolbar buttons.
11
 */
12
public interface MenuAction {
13
  /**
14
   * Creates a menu item based on the {@link Action} parameters.
15
   *
16
   * @return A new {@link MenuItem} instance.
17
   */
18
  MenuItem createMenuItem();
19
20
  /**
21
   * Creates an instance of {@link Button} or {@link Separator} based on the
22
   * {@link Action} parameters.
23
   *
24
   * @return A new {@link Button} or {@link Separator} instance.
25
   */
26
  Node createToolBarNode();
27
}
128
A src/main/java/com/keenwrite/ui/actions/SeparatorAction.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.actions;
3
4
import javafx.scene.Node;
5
import javafx.scene.control.*;
6
7
/**
8
 * Represents a {@link MenuBar} or {@link ToolBar} action that has no
9
 * operation, acting as a placeholder for line separators.
10
 */
11
public class SeparatorAction implements MenuAction {
12
  @Override
13
  public MenuItem createMenuItem() {
14
    return new SeparatorMenuItem();
15
  }
16
17
  @Override
18
  public Node createToolBarNode() {
19
    return new Separator();
20
  }
21
}
122
A src/main/java/com/keenwrite/ui/actions/package-info.java
1
/* Copyright 2020 White Magic Software, Ltd.
2
 *
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
/**
29
 * This package contains classes that define commands as executable actions.
30
 */
31
package com.keenwrite.ui.actions;
132
A src/main/java/com/keenwrite/ui/adapters/DocumentAdapter.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.adapters;
3
4
import org.xhtmlrenderer.event.DocumentListener;
5
6
import static com.keenwrite.StatusBarNotifier.clue;
7
8
/**
9
 * Allows subclasses to implement only specific events of interest.
10
 */
11
public class DocumentAdapter implements DocumentListener {
12
  @Override
13
  public void documentStarted() {
14
  }
15
16
  @Override
17
  public void documentLoaded() {
18
  }
19
20
  @Override
21
  public void onLayoutException( final Throwable t ) {
22
    clue( t );
23
  }
24
25
  @Override
26
  public void onRenderException( final Throwable t ) {
27
    clue( t );
28
  }
29
}
130
A src/main/java/com/keenwrite/ui/adapters/ReplacedElementAdapter.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.adapters;
3
4
import org.w3c.dom.Element;
5
import org.xhtmlrenderer.extend.ReplacedElementFactory;
6
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
7
8
/**
9
 * Allows subclasses to implement only specific events of interest.
10
 */
11
public abstract class ReplacedElementAdapter implements ReplacedElementFactory {
12
  @Override
13
  public void reset() {
14
  }
15
16
  @Override
17
  public void remove( final Element e ) {
18
  }
19
20
  @Override
21
  public void setFormSubmissionListener(
22
      final FormSubmissionListener listener ) {
23
  }
24
}
125
A src/main/java/com/keenwrite/ui/controls/BrowseFileButton.java
1
/*
2
 * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.keenwrite.ui.controls;
29
30
import com.keenwrite.Messages;
31
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
32
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
33
import javafx.beans.property.ObjectProperty;
34
import javafx.beans.property.SimpleObjectProperty;
35
import javafx.event.ActionEvent;
36
import javafx.scene.control.Button;
37
import javafx.scene.control.Tooltip;
38
import javafx.scene.input.KeyCode;
39
import javafx.scene.input.KeyEvent;
40
import javafx.stage.FileChooser;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
43
import java.io.File;
44
import java.nio.file.Path;
45
import java.util.ArrayList;
46
import java.util.List;
47
48
/**
49
 * Button that opens a file chooser to select a local file for a URL.
50
 */
51
public class BrowseFileButton extends Button {
52
53
  private final List<ExtensionFilter> mExtensionFilters = new ArrayList<>();
54
  private final ObjectProperty<Path> mBasePath = new SimpleObjectProperty<>();
55
  private final ObjectProperty<String> mUrl = new SimpleObjectProperty<>();
56
57
  public BrowseFileButton() {
58
    setGraphic(
59
        FontAwesomeIconFactory.get().createIcon( FontAwesomeIcon.FILE_ALT )
60
    );
61
    setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) );
62
    setOnAction( this::browse );
63
64
    disableProperty().bind( mBasePath.isNull() );
65
66
    // workaround for a JavaFX bug:
67
    //   avoid closing the dialog that contains this control when the user
68
    //   closes the FileChooser or DirectoryChooser using the ESC key
69
    addEventHandler( KeyEvent.KEY_RELEASED, e -> {
70
      if( e.getCode() == KeyCode.ESCAPE ) {
71
        e.consume();
72
      }
73
    } );
74
  }
75
76
  public void addExtensionFilter( ExtensionFilter extensionFilter ) {
77
    mExtensionFilters.add( extensionFilter );
78
  }
79
80
  public ObjectProperty<String> urlProperty() {
81
    return mUrl;
82
  }
83
84
  private void browse( ActionEvent e ) {
85
    var fileChooser = new FileChooser();
86
    fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) );
87
    fileChooser.getExtensionFilters().addAll( mExtensionFilters );
88
    fileChooser.getExtensionFilters()
89
               .add( new ExtensionFilter( Messages.get(
90
                   "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) );
91
    fileChooser.setInitialDirectory( getInitialDirectory() );
92
    var result = fileChooser.showOpenDialog( getScene().getWindow() );
93
    if( result != null ) {
94
      updateUrl( result );
95
    }
96
  }
97
98
  private File getInitialDirectory() {
99
    //TODO build initial directory based on current value of 'url' property
100
    return getBasePath().toFile();
101
  }
102
103
  private void updateUrl( File file ) {
104
    String newUrl;
105
    try {
106
      newUrl = getBasePath().relativize( file.toPath() ).toString();
107
    } catch( IllegalArgumentException ex ) {
108
      newUrl = file.toString();
109
    }
110
    mUrl.set( newUrl.replace( '\\', '/' ) );
111
  }
112
113
  public void setBasePath( Path basePath ) {
114
    this.mBasePath.set( basePath );
115
  }
116
117
  private Path getBasePath() {
118
    return mBasePath.get();
119
  }
120
}
1121
A src/main/java/com/keenwrite/ui/controls/EscapeTextField.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.keenwrite.ui.controls;
29
30
import javafx.beans.property.SimpleStringProperty;
31
import javafx.beans.property.StringProperty;
32
import javafx.scene.control.TextField;
33
import javafx.util.StringConverter;
34
35
/**
36
 * Responsible for escaping/unescaping characters for markdown.
37
 */
38
public class EscapeTextField extends TextField {
39
40
  public EscapeTextField() {
41
    escapedText.bindBidirectional(
42
        textProperty(),
43
        new StringConverter<>() {
44
          @Override
45
          public String toString( String object ) {
46
            return escape( object );
47
          }
48
49
          @Override
50
          public String fromString( String string ) {
51
            return unescape( string );
52
          }
53
        }
54
    );
55
    escapeCharacters.addListener(
56
        e -> escapedText.set( escape( textProperty().get() ) )
57
    );
58
  }
59
60
  // 'escapedText' property
61
  private final StringProperty escapedText = new SimpleStringProperty();
62
63
  public StringProperty escapedTextProperty() {
64
    return escapedText;
65
  }
66
67
  // 'escapeCharacters' property
68
  private final StringProperty escapeCharacters = new SimpleStringProperty();
69
70
  public String getEscapeCharacters() {
71
    return escapeCharacters.get();
72
  }
73
74
  public void setEscapeCharacters( String escapeCharacters ) {
75
    this.escapeCharacters.set( escapeCharacters );
76
  }
77
78
  private String escape( final String s ) {
79
    final String escapeChars = getEscapeCharacters();
80
81
    return isEmpty( escapeChars ) ? s :
82
        s.replaceAll( "([" + escapeChars.replaceAll(
83
            "(.)",
84
            "\\\\$1" ) + "])", "\\\\$1" );
85
  }
86
87
  private String unescape( final String s ) {
88
    final String escapeChars = getEscapeCharacters();
89
90
    return isEmpty( escapeChars ) ? s :
91
        s.replaceAll( "\\\\([" + escapeChars
92
            .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" );
93
  }
94
95
  private static boolean isEmpty( final String s ) {
96
    return s == null || s.isEmpty();
97
  }
98
}
199
A src/main/java/com/keenwrite/ui/controls/SearchBar.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.controls;
3
4
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
5
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
6
import javafx.beans.property.IntegerProperty;
7
import javafx.beans.property.SimpleIntegerProperty;
8
import javafx.beans.value.ChangeListener;
9
import javafx.event.ActionEvent;
10
import javafx.event.EventHandler;
11
import javafx.geometry.Orientation;
12
import javafx.geometry.Pos;
13
import javafx.scene.Node;
14
import javafx.scene.control.Button;
15
import javafx.scene.control.Separator;
16
import javafx.scene.control.TextField;
17
import javafx.scene.control.Tooltip;
18
import javafx.scene.layout.HBox;
19
import javafx.scene.layout.Priority;
20
import javafx.scene.layout.Region;
21
import javafx.scene.layout.VBox;
22
import javafx.scene.text.Text;
23
import org.controlsfx.control.textfield.CustomTextField;
24
25
import static com.keenwrite.Messages.get;
26
import static java.lang.StrictMath.max;
27
import static java.lang.String.format;
28
29
/**
30
 * Responsible for presenting user interface options for searching through
31
 * the document.
32
 */
33
public final class SearchBar extends HBox {
34
35
  private static final String MESSAGE_KEY = "Main.search.%s.%s";
36
37
  private final Button mButtonStop = createButtonStop();
38
  private final Button mButtonNext = createButton( "next" );
39
  private final Button mButtonPrev = createButton( "prev" );
40
  private final TextField mFind = createTextField();
41
  private final Text mMatches = new Text();
42
  private final IntegerProperty mMatchIndex = new SimpleIntegerProperty();
43
  private final IntegerProperty mMatchCount = new SimpleIntegerProperty();
44
45
  public SearchBar() {
46
    setAlignment( Pos.CENTER );
47
    addAll(
48
        mButtonStop,
49
        createSpacer( 10 ),
50
        mFind,
51
        createSpacer( 10 ),
52
        mButtonNext,
53
        createSpacer( 10 ),
54
        mButtonPrev,
55
        createSpacer( 10 ),
56
        mMatches,
57
        createSpacer( 10 ),
58
        createSeparatorVertical(),
59
        createSpacer( 5 )
60
    );
61
62
    mMatchIndex.addListener( ( c, o, n ) -> updateMatchText() );
63
    mMatchCount.addListener( ( c, o, n ) -> updateMatchText() );
64
    updateMatchText();
65
  }
66
67
  /**
68
   * Gives focus to the text field.
69
   */
70
  @Override
71
  public void requestFocus() {
72
    mFind.requestFocus();
73
  }
74
75
  /**
76
   * Adds a listener that triggers when the input text field changes.
77
   *
78
   * @param listener The listener to notify of change events.
79
   */
80
  public void addInputListener( final ChangeListener<String> listener ) {
81
    mFind.textProperty().addListener( listener );
82
  }
83
84
  /**
85
   * Sets the {@link EventHandler} to call when the user interface triggers
86
   * finding the next matching search string. This will wrap from the end
87
   * to the beginning.
88
   *
89
   * @param handler The handler requested to perform the find next action.
90
   */
91
  public void setOnNextAction( final EventHandler<ActionEvent> handler ) {
92
    mButtonNext.setOnAction( handler );
93
    mFind.setOnAction( handler );
94
  }
95
96
  /**
97
   * Sets the {@link EventHandler} to call when the user interface triggers
98
   * finding the previous matching search string. This will wrap from the
99
   * beginning to the end.
100
   *
101
   * @param handler The handler requested to perform the find next action.
102
   */
103
  public void setOnPrevAction( final EventHandler<ActionEvent> handler ) {
104
    mButtonPrev.setOnAction( handler );
105
  }
106
107
  /**
108
   * Sets the {@link EventHandler} to call when searching has been terminated.
109
   *
110
   * @param handler The {@link EventHandler} that will perform an action
111
   *                when the searching has stopped (e.g., remove from this
112
   *                widget from status bar).
113
   */
114
  public void setOnCancelAction( final EventHandler<ActionEvent> handler ) {
115
    mButtonStop.setOnAction( handler );
116
  }
117
118
  /**
119
   * When this property value changes, the match text is updated accordingly.
120
   * If the value is less than zero, the text will show zero.
121
   *
122
   * @return The index of the latest search string match.
123
   */
124
  public IntegerProperty matchIndexProperty() {
125
    return mMatchIndex;
126
  }
127
128
  /**
129
   * When this property value changes, the match text is updated accordingly.
130
   * If the value is less than zero, the text will show zero.
131
   *
132
   * @return The total number of items that match the search string.
133
   */
134
  public IntegerProperty matchCountProperty() {
135
    return mMatchCount;
136
  }
137
138
  /**
139
   * Updates the match count.
140
   */
141
  private void updateMatchText() {
142
    final var index = max( 0, mMatchIndex.get() );
143
    final var count = max( 0, mMatchCount.get() );
144
    final var suffix = count == 0 ? "none" : "some";
145
    final var key = getMessageValue( "match", suffix );
146
147
    mMatches.setText( get( key, index, count ) );
148
  }
149
150
  private Button createButton( final String id ) {
151
    final var button = new Button();
152
    final var tooltipText = getMessageValue( id, "tooltip" );
153
154
    button.setMnemonicParsing( false );
155
    button.setGraphic( getIcon( id ) );
156
    button.setTooltip( new Tooltip( tooltipText ) );
157
158
    return button;
159
  }
160
161
  private Button createButtonStop() {
162
    final var button = createButton( "stop" );
163
    button.setCancelButton( true );
164
    return button;
165
  }
166
167
  private TextField createTextField() {
168
    final var textField = new CustomTextField();
169
    textField.setLeft( getIcon( "find" ) );
170
    return textField;
171
  }
172
173
  /**
174
   * Creates a vertical bar, used to divide the search results from the
175
   * application status message.
176
   *
177
   * @return A vertical separator.
178
   */
179
  private Node createSeparatorVertical() {
180
    return new Separator( Orientation.VERTICAL );
181
  }
182
183
  /**
184
   * Breathing room between the search box and the application status message.
185
   * This could also be accomplished by using CSS.
186
   *
187
   * @param width The spacer's width.
188
   * @return A new {@link Node} having about 10px of space.
189
   */
190
  private Node createSpacer( final int width ) {
191
    final var spacer = new Region();
192
    spacer.setPrefWidth( width );
193
    VBox.setVgrow( spacer, Priority.ALWAYS );
194
    return spacer;
195
  }
196
197
  private Node getIcon( final String id ) {
198
    final var name = getMessageValue( id, "icon" );
199
    final var glyph = FontAwesomeIcon.valueOf( name.toUpperCase() );
200
    return FontAwesomeIconFactory.get().createIcon( glyph );
201
  }
202
203
  private String getMessageValue( final String id, final String suffix ) {
204
    return get( format( MESSAGE_KEY, id, suffix ) );
205
  }
206
207
  private void addAll( final Node... nodes ) {
208
    getChildren().addAll( nodes );
209
  }
210
}
1211
A src/main/java/com/keenwrite/ui/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.keenwrite.ui.dialogs;
29
30
import static com.keenwrite.Messages.get;
31
import com.keenwrite.service.events.impl.ButtonOrderPane;
32
import static javafx.scene.control.ButtonType.CANCEL;
33
import static javafx.scene.control.ButtonType.OK;
34
import javafx.scene.control.Dialog;
35
import javafx.stage.Window;
36
37
/**
38
 * Superclass that abstracts common behaviours for all dialogs.
39
 *
40
 * @param <T> The type of dialog to create (usually String).
41
 */
42
public abstract class AbstractDialog<T> extends Dialog<T> {
43
44
  /**
45
   * Ensures that all dialogs can be closed.
46
   *
47
   * @param owner The parent window of this dialog.
48
   * @param title The messages title to display in the title bar.
49
   */
50
  @SuppressWarnings( "OverridableMethodCallInConstructor" )
51
  public AbstractDialog( final Window owner, final String title ) {
52
    setTitle( get( title ) );
53
    setResizable( true );
54
55
    initOwner( owner );
56
    initCloseAction();
57
    initDialogPane();
58
    initDialogButtons();
59
    initComponents();
60
  }
61
62
  /**
63
   * Initialize the component layout.
64
   */
65
  protected abstract void initComponents();
66
67
  /**
68
   * Set the dialog to use a button order pane with an OK and a CANCEL button.
69
   */
70
  protected void initDialogPane() {
71
    setDialogPane( new ButtonOrderPane() );
72
  }
73
  
74
  /**
75
   * Set an OK and CANCEL button on the dialog.
76
   */
77
  protected void initDialogButtons() {
78
    getDialogPane().getButtonTypes().addAll( OK, CANCEL );
79
  }
80
81
  /**
82
   * Attaches a setOnCloseRequest to the dialog's [X] button so that the user
83
   * can always close the window, even if there's an error.
84
   */
85
  protected final void initCloseAction() {
86
    final Window window = getDialogPane().getScene().getWindow();
87
    window.setOnCloseRequest( event -> window.hide() );
88
  }
89
}
190
A src/main/java/com/keenwrite/ui/dialogs/ImageDialog.java
1
/*
2
 * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.ui.dialogs;
28
29
import static com.keenwrite.Messages.get;
30
import com.keenwrite.ui.controls.BrowseFileButton;
31
import com.keenwrite.ui.controls.EscapeTextField;
32
import java.nio.file.Path;
33
import javafx.application.Platform;
34
import javafx.beans.binding.Bindings;
35
import javafx.beans.property.SimpleStringProperty;
36
import javafx.beans.property.StringProperty;
37
import javafx.scene.control.ButtonBar.ButtonData;
38
import static javafx.scene.control.ButtonType.OK;
39
import javafx.scene.control.DialogPane;
40
import javafx.scene.control.Label;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
import javafx.stage.Window;
43
import org.tbee.javafx.scene.layout.fxml.MigPane;
44
45
/**
46
 * Dialog to enter a markdown image.
47
 */
48
public class ImageDialog extends AbstractDialog<String> {
49
50
  private final StringProperty image = new SimpleStringProperty();
51
52
  public ImageDialog( final Window owner, final Path basePath ) {
53
    super(owner, "Dialog.image.title" );
54
    
55
    final DialogPane dialogPane = getDialogPane();
56
    dialogPane.setContent( pane );
57
58
    linkBrowseFileButton.setBasePath( basePath );
59
    linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
60
    linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() );
61
62
    dialogPane.lookupButton( OK ).disableProperty().bind(
63
      urlField.escapedTextProperty().isEmpty()
64
      .or( textField.escapedTextProperty().isEmpty() ) );
65
66
    image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "![%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.format( "![%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) );
69
    previewField.textProperty().bind( image );
70
71
    setResultConverter( dialogButton -> {
72
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
73
      return (data == ButtonData.OK_DONE) ? image.get() : null;
74
    } );
75
76
    Platform.runLater( () -> {
77
      urlField.requestFocus();
78
79
      if( urlField.getText().startsWith( "http://" ) ) {
80
        urlField.selectRange( "http://".length(), urlField.getLength() );
81
      }
82
    } );
83
  }
84
85
  @Override
86
  protected void initComponents() {
87
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
88
    pane = new MigPane();
89
    Label urlLabel = new Label();
90
    urlField = new EscapeTextField();
91
    linkBrowseFileButton = new BrowseFileButton();
92
    Label textLabel = new Label();
93
    textField = new EscapeTextField();
94
    Label titleLabel = new Label();
95
    titleField = new EscapeTextField();
96
    Label previewLabel = new Label();
97
    previewField = new Label();
98
99
    //======== pane ========
100
    {
101
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" );
102
      pane.setRows( "[][][][]" );
103
104
      //---- urlLabel ----
105
      urlLabel.setText( get( "Dialog.image.urlLabel.text" ) );
106
      pane.add( urlLabel, "cell 0 0" );
107
108
      //---- urlField ----
109
      urlField.setEscapeCharacters( "()" );
110
      urlField.setText( "http://yourlink.com" );
111
      urlField.setPromptText( "http://yourlink.com" );
112
      pane.add( urlField, "cell 1 0" );
113
      pane.add( linkBrowseFileButton, "cell 2 0" );
114
115
      //---- textLabel ----
116
      textLabel.setText( get( "Dialog.image.textLabel.text" ) );
117
      pane.add( textLabel, "cell 0 1" );
118
119
      //---- textField ----
120
      textField.setEscapeCharacters( "[]" );
121
      pane.add( textField, "cell 1 1 2 1" );
122
123
      //---- titleLabel ----
124
      titleLabel.setText( get( "Dialog.image.titleLabel.text" ) );
125
      pane.add( titleLabel, "cell 0 2" );
126
      pane.add( titleField, "cell 1 2 2 1" );
127
128
      //---- previewLabel ----
129
      previewLabel.setText( get( "Dialog.image.previewLabel.text" ) );
130
      pane.add( previewLabel, "cell 0 3" );
131
      pane.add( previewField, "cell 1 3 2 1" );
132
    }
133
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
134
  }
135
136
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
137
  private MigPane pane;
138
  private EscapeTextField urlField;
139
  private BrowseFileButton linkBrowseFileButton;
140
  private EscapeTextField textField;
141
  private EscapeTextField titleField;
142
  private Label previewField;
143
	// JFormDesigner - End of variables declaration  //GEN-END:variables
144
}
1145
A src/main/java/com/keenwrite/ui/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/keenwrite/ui/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.keenwrite.ui.dialogs;
29
30
import com.keenwrite.ui.controls.EscapeTextField;
31
import com.keenwrite.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.keenwrite.Messages.get;
43
import static javafx.scene.control.ButtonType.OK;
44
45
/**
46
 * Dialog to enter a markdown link.
47
 */
48
public class LinkDialog extends AbstractDialog<String> {
49
50
  private final StringProperty link = new SimpleStringProperty();
51
52
  public LinkDialog(
53
    final Window owner, final HyperlinkModel hyperlink ) {
54
    super( owner, "Dialog.link.title" );
55
56
    final DialogPane dialogPane = getDialogPane();
57
    dialogPane.setContent( pane );
58
59
    dialogPane.lookupButton( OK ).disableProperty().bind(
60
      urlField.escapedTextProperty().isEmpty() );
61
62
    textField.setText( hyperlink.getText() );
63
    urlField.setText( hyperlink.getUrl() );
64
    titleField.setText( hyperlink.getTitle() );
65
66
    link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() )
69
        .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) )
70
        .otherwise( urlField.escapedTextProperty() ) ) );
71
72
    setResultConverter( dialogButton -> {
73
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
74
      return (data == ButtonData.OK_DONE) ? link.get() : null;
75
    } );
76
77
    Platform.runLater( () -> {
78
      urlField.requestFocus();
79
      urlField.selectRange( 0, urlField.getLength() );
80
    } );
81
  }
82
83
  @Override
84
  protected void initComponents() {
85
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
86
    pane = new MigPane();
87
    Label urlLabel = new Label();
88
    urlField = new EscapeTextField();
89
    Label textLabel = new Label();
90
    textField = new EscapeTextField();
91
    Label titleLabel = new Label();
92
    titleField = new EscapeTextField();
93
94
    //======== pane ========
95
    {
96
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" );
97
      pane.setRows( "[][][][]" );
98
99
      //---- urlLabel ----
100
      urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
101
      pane.add( urlLabel, "cell 0 0" );
102
103
      //---- urlField ----
104
      urlField.setEscapeCharacters( "()" );
105
      pane.add( urlField, "cell 1 0" );
106
107
      //---- textLabel ----
108
      textLabel.setText( get( "Dialog.link.textLabel.text" ) );
109
      pane.add( textLabel, "cell 0 1" );
110
111
      //---- textField ----
112
      textField.setEscapeCharacters( "[]" );
113
      pane.add( textField, "cell 1 1 3 1" );
114
115
      //---- titleLabel ----
116
      titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
117
      pane.add( titleLabel, "cell 0 2" );
118
      pane.add( titleField, "cell 1 2 3 1" );
119
    }
120
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
121
  }
122
123
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
124
  private MigPane pane;
125
  private EscapeTextField urlField;
126
  private EscapeTextField textField;
127
  private EscapeTextField titleField;
128
  // JFormDesigner - End of variables declaration  //GEN-END:variables
129
}
1130
A src/main/java/com/keenwrite/ui/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/keenwrite/ui/listeners/CaretListener.java
1
package com.keenwrite.ui.listeners;
2
3
import com.keenwrite.editors.TextEditor;
4
import com.keenwrite.processors.markdown.Caret;
5
import javafx.beans.property.ReadOnlyObjectProperty;
6
import javafx.beans.value.ChangeListener;
7
import javafx.beans.value.ObservableValue;
8
import javafx.scene.layout.VBox;
9
import javafx.scene.text.Text;
10
11
import static javafx.geometry.Pos.BASELINE_CENTER;
12
13
/**
14
 * Responsible for updating the UI whenever the caret changes position.
15
 * Only one instance of {@link CaretListener} is allowed to prevent duplicate
16
 * adds to the observable property.
17
 */
18
public class CaretListener extends VBox implements ChangeListener<Integer> {
19
20
  private final Text mLineNumberText = new Text();
21
  private volatile Caret mCaret;
22
23
  public CaretListener( final ReadOnlyObjectProperty<TextEditor> editor ) {
24
    assert editor != null;
25
26
    setAlignment( BASELINE_CENTER );
27
    getChildren().add( mLineNumberText );
28
29
    editor.addListener( ( c, o, n ) -> {
30
      if( n != null ) {
31
        updateListener( n.getCaret() );
32
      }
33
    } );
34
35
    updateListener( editor.get().getCaret() );
36
  }
37
38
  /**
39
   * Called whenever the caret position changes.
40
   *
41
   * @param c The caret position property.
42
   * @param o The old caret position offset.
43
   * @param n The new caret position offset.
44
   */
45
  @Override
46
  public void changed(
47
      final ObservableValue<? extends Integer> c,
48
      final Integer o, final Integer n ) {
49
    updateLineNumber();
50
  }
51
52
  private void updateListener( final Caret caret ) {
53
    assert caret != null;
54
55
    final var property = caret.textOffsetProperty();
56
57
    property.removeListener( this );
58
    mCaret = caret;
59
    property.addListener( this );
60
    updateLineNumber();
61
  }
62
63
  private void updateLineNumber() {
64
    mLineNumberText.setText( mCaret.toString() );
65
  }
66
}
167
D src/main/java/com/keenwrite/util/Action.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.keenwrite.util;
29
30
import com.keenwrite.Messages;
31
import de.jensd.fx.glyphs.GlyphIcons;
32
import javafx.beans.value.ObservableBooleanValue;
33
import javafx.event.ActionEvent;
34
import javafx.event.EventHandler;
35
import javafx.scene.Node;
36
import javafx.scene.control.MenuItem;
37
38
/**
39
 * Represents a menu action that can generate {@link MenuItem} instances and
40
 * and {@link Node} instances for a toolbar.
41
 */
42
public abstract class Action {
43
  /**
44
   * TODO: Reuse the {@link GenericBuilder}.
45
   *
46
   * @return The {@link Builder} for an instance of {@link Action}.
47
   */
48
  public static Builder builder() {
49
    return new Builder();
50
  }
51
52
  public abstract MenuItem createMenuItem();
53
54
  public abstract Node createToolBarButton();
55
56
  /**
57
   * Adds subordinate actions to the menu. This is used to establish sub-menu
58
   * relationships. The default behaviour does not wire up any registration;
59
   * subclasses are responsible for handling how actions relate to one another.
60
   *
61
   * @param action Actions that only exist with respect to this action.
62
   */
63
  public void addSubActions( Action... action ) {
64
  }
65
66
  /**
67
   * Provides a fluent interface around constructing actions so that duplication
68
   * can be avoided.
69
   */
70
  public static class Builder {
71
    private String mText;
72
    private String mAccelerator;
73
    private GlyphIcons mIcon;
74
    private EventHandler<ActionEvent> mAction;
75
    private ObservableBooleanValue mDisabled;
76
77
    /**
78
     * Sets the action text based on a resource bundle key.
79
     *
80
     * @param key The key to look up in the {@link Messages}.
81
     * @return The corresponding value, or the key name if none found.
82
     */
83
    public Builder setText( final String key ) {
84
      mText = Messages.get( key, key );
85
      return this;
86
    }
87
88
    public Builder setAccelerator( final String accelerator ) {
89
      mAccelerator = accelerator;
90
      return this;
91
    }
92
93
    public Builder setIcon( final GlyphIcons icon ) {
94
      mIcon = icon;
95
      return this;
96
    }
97
98
    public Builder setAction( final EventHandler<ActionEvent> action ) {
99
      mAction = action;
100
      return this;
101
    }
102
103
    public Builder setDisabled( final ObservableBooleanValue disabled ) {
104
      mDisabled = disabled;
105
      return this;
106
    }
107
108
    public Action build() {
109
      return new MenuAction( mText, mAccelerator, mIcon, mAction, mDisabled );
110
    }
111
  }
112
}
1131
D src/main/java/com/keenwrite/util/ActionUtils.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.util;
28
29
import javafx.scene.Node;
30
import javafx.scene.control.Menu;
31
import javafx.scene.control.MenuItem;
32
import javafx.scene.control.ToolBar;
33
34
/**
35
 * Responsible for creating menu items and toolbar buttons.
36
 */
37
public class ActionUtils {
38
39
  public static Menu createMenu( final String text, final Action... actions ) {
40
    return new Menu( text, null, createMenuItems( actions ) );
41
  }
42
43
  public static MenuItem[] createMenuItems( final Action... actions ) {
44
    final var menuItems = new MenuItem[ actions.length ];
45
46
    for( int i = 0; i < actions.length; i++ ) {
47
      menuItems[ i ] = actions[ i ].createMenuItem();
48
    }
49
50
    return menuItems;
51
  }
52
53
  public static ToolBar createToolBar( final Action... actions ) {
54
    return new ToolBar( createToolBarButtons( actions ) );
55
  }
56
57
  public static Node[] createToolBarButtons( final Action... actions ) {
58
    Node[] buttons = new Node[ actions.length ];
59
60
    for( int i = 0; i < actions.length; i++ ) {
61
      buttons[ i ] = actions[ i ].createToolBarButton();
62
    }
63
64
    return buttons;
65
  }
66
}
671
M src/main/java/com/keenwrite/util/BoundedCache.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.util;
293
...
3812
 * @param <V> The type of value mapped to a key.
3913
 */
40
public class BoundedCache<K, V> extends LinkedHashMap<K, V> {
14
public final class BoundedCache<K, V> extends LinkedHashMap<K, V> {
4115
  private final int mCacheSize;
4216
A src/main/java/com/keenwrite/util/CyclicIterator.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.util.List;
5
import java.util.ListIterator;
6
import java.util.NoSuchElementException;
7
8
/**
9
 * Responsible for iterating over a list either forwards or backwards. When
10
 * the iterator reaches the last element in the list, the next element will
11
 * be the first. When the iterator reaches the first element in the list,
12
 * the previous element will be the last.
13
 * <p>
14
 * Due to the ability to move forwards and backwards through the list, rather
15
 * than force client classes to track the list index independently, this
16
 * iterator provides an accessor to the index. The index is zero-based.
17
 * </p>
18
 *
19
 * @param <T> The type of list to be cycled.
20
 */
21
public class CyclicIterator<T> implements ListIterator<T> {
22
  private final List<T> mList;
23
24
  /**
25
   * Initialize to an invalid index so that the first calls to either
26
   * {@link #previous()} or {@link #next()} will return the starting or ending
27
   * element.
28
   */
29
  private int mIndex = -1;
30
31
  /**
32
   * Creates an iterator that cycles indefinitely through the given list.
33
   *
34
   * @param list The list to cycle through indefinitely.
35
   */
36
  public CyclicIterator( final List<T> list ) {
37
    mList = list;
38
  }
39
40
  /**
41
   * @return {@code true} if there is at least one element.
42
   */
43
  @Override
44
  public boolean hasNext() {
45
    return !mList.isEmpty();
46
  }
47
48
  /**
49
   * @return {@code true} if there is at least one element.
50
   */
51
  @Override
52
  public boolean hasPrevious() {
53
    return !mList.isEmpty();
54
  }
55
56
  @Override
57
  public int nextIndex() {
58
    return computeIndex( +1 );
59
  }
60
61
  @Override
62
  public int previousIndex() {
63
    return computeIndex( -1 );
64
  }
65
66
  @Override
67
  public void remove() {
68
    mList.remove( mIndex );
69
  }
70
71
  @Override
72
  public void set( final T t ) {
73
    mList.set( mIndex, t );
74
  }
75
76
  @Override
77
  public void add( final T t ) {
78
    mList.add( mIndex, t );
79
  }
80
81
  /**
82
   * Returns the next item in the list, which will cycle to the first
83
   * item as necessary.
84
   *
85
   * @return The next item in the list, cycling to the start if needed.
86
   */
87
  @Override
88
  public T next() {
89
    return cycle( +1 );
90
  }
91
92
  /**
93
   * Returns the previous item in the list, which will cycle to the last
94
   * item as necessary.
95
   *
96
   * @return The previous item in the list, cycling to the end if needed.
97
   */
98
  @Override
99
  public T previous() {
100
    return cycle( -1 );
101
  }
102
103
  /**
104
   * Cycles to the next or previous element, depending on the direction value.
105
   *
106
   * @param direction Use -1 for previous, +1 for next.
107
   * @return The next or previous item in the list.
108
   */
109
  private T cycle( final int direction ) {
110
    try {
111
      return mList.get( mIndex = computeIndex( direction ) );
112
    } catch( final Exception ex ) {
113
      throw new NoSuchElementException( ex );
114
    }
115
  }
116
117
  /**
118
   * Returns the index of the value retrieved from the most recent call to
119
   * either {@link #previous()} or {@link #next()}.
120
   *
121
   * @return The list item index or -1 if no calls have been made to retrieve
122
   * an item from the list.
123
   */
124
  public int getIndex() {
125
    return mIndex;
126
  }
127
128
  private int computeIndex( final int direction ) {
129
    final var i = mIndex + direction;
130
    final var size = mList.size();
131
    final var result = i < 0
132
        ? size - 1
133
        : size == 0 ? 0 : i % size;
134
135
    // Ensure the invariant holds.
136
    assert 0 <= result && result < size || size == 0 && result <= 0;
137
138
    return result;
139
  }
140
}
1141
A src/main/java/com/keenwrite/util/FontLoader.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import com.keenwrite.preview.HtmlPreview;
5
6
import java.awt.*;
7
import java.awt.font.TextAttribute;
8
import java.io.FileInputStream;
9
import java.io.IOException;
10
import java.io.InputStream;
11
import java.net.URI;
12
import java.util.Map;
13
14
import static com.keenwrite.Constants.FONT_DIRECTORY;
15
import static com.keenwrite.StatusBarNotifier.clue;
16
import static com.keenwrite.util.ProtocolScheme.valueFrom;
17
import static com.keenwrite.util.ResourceWalker.GLOB_FONTS;
18
import static com.keenwrite.util.ResourceWalker.walk;
19
import static java.awt.Font.TRUETYPE_FONT;
20
import static java.awt.Font.createFont;
21
import static java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment;
22
import static java.awt.font.TextAttribute.*;
23
24
/**
25
 * Responsible for loading fonts into the application's
26
 * {@link GraphicsEnvironment} so that the {@link HtmlPreview} can display
27
 * the text using a non-system font.
28
 */
29
public final class FontLoader {
30
31
  /**
32
   * Walks the resources associated with the application to load all TrueType
33
   * font resources found. This method must run before the windowing system
34
   * kicks in, otherwise the fonts will not be found.
35
   * <p>
36
   * All fonts must be TrueType fonts. No PostScript Type 1 fonts are
37
   * supported.
38
   * </p>
39
   */
40
  @SuppressWarnings("unchecked")
41
  public static void initFonts() {
42
    final var ge = getLocalGraphicsEnvironment();
43
44
    try {
45
      walk(
46
          FONT_DIRECTORY, GLOB_FONTS, path -> {
47
            final var uri = path.toUri();
48
            final var filename = path.toString();
49
50
            try( final var is = openFont( uri, filename ) ) {
51
              final var font = createFont( TRUETYPE_FONT, is );
52
              final var attributes =
53
                  (Map<TextAttribute, Integer>) font.getAttributes();
54
55
              attributes.put( LIGATURES, LIGATURES_ON );
56
              attributes.put( KERNING, KERNING_ON );
57
              ge.registerFont( font.deriveFont( attributes ) );
58
            } catch( final Exception ex ) {
59
              clue( ex );
60
            }
61
          }
62
      );
63
    } catch( final Exception ex ) {
64
      clue( ex );
65
    }
66
  }
67
68
  /**
69
   * Attempts to open a font, regardless of whether the font is a resource in
70
   * a JAR file or somewhere on the file system.
71
   *
72
   * @param uri      Directory or archive containing a font.
73
   * @param filename Name of the font file.
74
   * @return An open file handled to the font.
75
   * @throws IOException Could not open the resource as a stream.
76
   */
77
  private static InputStream openFont( final URI uri, final String filename )
78
      throws IOException {
79
    return valueFrom( uri ).isJar()
80
        ? FontLoader.class.getResourceAsStream( filename )
81
        : new FileInputStream( filename );
82
  }
83
}
184
M src/main/java/com/keenwrite/util/GenericBuilder.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
12
package com.keenwrite.util;
23
...
4445
  protected GenericBuilder(
4546
      final Supplier<MT> mutator, final Function<MT, IT> immutable ) {
47
    assert mutator != null;
48
    assert immutable != null;
49
4650
    mMutable = mutator;
4751
    mImmutable = immutable;
D src/main/java/com/keenwrite/util/MenuAction.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.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.control.Button;
34
import javafx.scene.control.Menu;
35
import javafx.scene.control.MenuItem;
36
import javafx.scene.control.Tooltip;
37
import javafx.scene.input.KeyCombination;
38
39
import java.util.ArrayList;
40
import java.util.List;
41
42
import static de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory.get;
43
import static javafx.scene.input.KeyCombination.valueOf;
44
45
/**
46
 * Defines actions the user can take by interacting with the GUI.
47
 */
48
public class MenuAction extends Action {
49
  private final String mText;
50
  private final KeyCombination mAccelerator;
51
  private final GlyphIcons mIcon;
52
  private final EventHandler<ActionEvent> mAction;
53
  private final ObservableBooleanValue mDisabled;
54
  private final List<Action> mSubActions = new ArrayList<>();
55
56
  public MenuAction(
57
      final String text,
58
      final String accelerator,
59
      final GlyphIcons icon,
60
      final EventHandler<ActionEvent> action,
61
      final ObservableBooleanValue disabled ) {
62
63
    mText = text;
64
    mAccelerator = accelerator == null ? null : valueOf( accelerator );
65
    mIcon = icon;
66
    mAction = action;
67
    mDisabled = disabled;
68
  }
69
70
  @Override
71
  public MenuItem createMenuItem() {
72
    // This will either become a menu or a menu item, depending on whether
73
    // sub-actions are defined.
74
    final MenuItem menuItem;
75
76
    if( mSubActions.isEmpty() ) {
77
      // Regular menu item has no sub-menus.
78
      menuItem = new MenuItem( mText );
79
    }
80
    else {
81
      // Sub-actions are translated into sub-menu items beneath this action.
82
      final var submenu = new Menu( mText );
83
84
      for( final var action : mSubActions ) {
85
        // Recursive call that creates a sub-menu hierarchy.
86
        submenu.getItems().add( action.createMenuItem() );
87
      }
88
89
      menuItem = submenu;
90
    }
91
92
    if( mAccelerator != null ) {
93
      menuItem.setAccelerator( mAccelerator );
94
    }
95
96
    if( mIcon != null ) {
97
      menuItem.setGraphic( get().createIcon( mIcon ) );
98
    }
99
100
    if( mAction != null ) {
101
      menuItem.setOnAction( mAction );
102
    }
103
104
    if( mDisabled != null ) {
105
      menuItem.disableProperty().bind( mDisabled );
106
    }
107
108
    menuItem.setMnemonicParsing( true );
109
110
    return menuItem;
111
  }
112
113
  @Override
114
  public Button createToolBarButton() {
115
    final Button button = new Button();
116
    button.setGraphic(
117
        get().createIcon( mIcon, "1.2em" ) );
118
119
    String tooltip = mText;
120
121
    if( tooltip.endsWith( "..." ) ) {
122
      tooltip = tooltip.substring( 0, tooltip.length() - 3 );
123
    }
124
125
    if( mAccelerator != null ) {
126
      tooltip += " (" + mAccelerator.getDisplayText() + ')';
127
    }
128
129
    button.setTooltip( new Tooltip( tooltip ) );
130
    button.setFocusTraversable( false );
131
    button.setOnAction( mAction );
132
133
    if( mDisabled != null ) {
134
      button.disableProperty().bind( mDisabled );
135
    }
136
137
    return button;
138
  }
139
140
  @Override
141
  public void addSubActions( final Action... action ) {
142
    mSubActions.addAll( List.of( action ) );
143
  }
144
}
1451
D src/main/java/com/keenwrite/util/Pair.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.keenwrite.util;
29
30
import java.util.AbstractMap;
31
import java.util.Map;
32
33
/**
34
 * Convenience class for pairing two objects together; this is a synonym for
35
 * {@link Map.Entry}.
36
 *
37
 * @param <K> The type of key to store in this pair.
38
 * @param <V> The type of value to store in this pair.
39
 */
40
public class Pair<K, V> extends AbstractMap.SimpleImmutableEntry<K, V> {
41
  /**
42
   * Associates a new key-value pair.
43
   *
44
   * @param key   The key for this key-value pairing.
45
   * @param value The value for this key-value pairing.
46
   */
47
  public Pair( final K key, final V value ) {
48
    super( key, value );
49
  }
50
}
511
D src/main/java/com/keenwrite/util/ProtocolResolver.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.keenwrite.util;
29
30
import java.io.File;
31
import java.net.MalformedURLException;
32
import java.net.URI;
33
import java.net.URL;
34
35
import static com.keenwrite.util.ProtocolScheme.UNKNOWN;
36
37
/**
38
 * Responsible for determining the protocol of a resource.
39
 */
40
public class ProtocolResolver {
41
  /**
42
   * Returns the protocol for a given URI or filename.
43
   *
44
   * @param resource Determine the protocol for this URI or filename.
45
   * @return The protocol for the given resource.
46
   */
47
  public static ProtocolScheme getProtocol( final String resource ) {
48
    String protocol;
49
50
    try {
51
      final URI uri = new URI( resource );
52
53
      if( uri.isAbsolute() ) {
54
        protocol = uri.getScheme();
55
      }
56
      else {
57
        final URL url = new URL( resource );
58
        protocol = url.getProtocol();
59
      }
60
    } catch( final Exception e ) {
61
      // Could be HTTP, HTTPS?
62
      if( resource.startsWith( "//" ) ) {
63
        throw new IllegalArgumentException( "Relative context: " + resource );
64
      }
65
      else {
66
        final File file = new File( resource );
67
        protocol = getProtocol( file );
68
      }
69
    }
70
71
    return ProtocolScheme.valueFrom( protocol );
72
  }
73
74
  /**
75
   * Returns the protocol for a given file.
76
   *
77
   * @param file Determine the protocol for this file.
78
   * @return The protocol for the given file, or {@link ProtocolScheme#UNKNOWN}
79
   * if the protocol cannot be determined.
80
   */
81
  private static String getProtocol( final File file ) {
82
    try {
83
      return file.toURI().toURL().getProtocol();
84
    } catch( final MalformedURLException ex ) {
85
      // Return a protocol guaranteed to be undefined.
86
      return UNKNOWN.toString();
87
    }
88
  }
89
}
901
M src/main/java/com/keenwrite/util/ProtocolScheme.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.util;
3
4
import java.io.File;
5
import java.net.URI;
6
import java.net.URL;
297
308
/**
319
 * Represents the type of data encoding scheme used for a universal resource
32
 * indicator.
10
 * indicator. Prefer to use the {@code is*} methods to check equality because
11
 * there are cases where the protocol represents more than one possible type
12
 * (e.g., a Java Archive is a file, so comparing {@link #FILE} directly could
13
 * lead to incorrect results).
3314
 */
3415
public enum ProtocolScheme {
16
  /**
17
   * Denotes a local file.
18
   */
19
  FILE,
3520
  /**
3621
   * Denotes either HTTP or HTTPS.
3722
   */
3823
  HTTP,
3924
  /**
40
   * Denotes a local file.
25
   * Denotes Java archive file.
4126
   */
42
  FILE,
27
  JAR,
4328
  /**
4429
   * Could not determine schema (or is not supported by the application).
4530
   */
4631
  UNKNOWN;
47
48
  /**
49
   * Answers {@code true} if the given protocol is either HTTP or HTTPS.
50
   *
51
   * @return {@code true} the protocol is either HTTP or HTTPS.
52
   */
53
  public boolean isHttp() {
54
    return this == HTTP;
55
  }
5632
5733
  /**
58
   * Answers {@code true} if the given protocol is for a local file.
34
   * Returns the protocol for a given URI or file name.
5935
   *
60
   * @return {@code true} the protocol is for a local file reference.
36
   * @param resource Determine the protocol for this URI or file name.
37
   * @return The protocol for the given resource.
6138
   */
62
  public boolean isFile() {
63
    return this == FILE;
39
  public static ProtocolScheme getProtocol( final String resource ) {
40
    try {
41
      final var uri = new URI( resource );
42
      return uri.isAbsolute()
43
          ? valueFrom( uri )
44
          : valueFrom( new URL( resource ) );
45
    } catch( final Exception ex ) {
46
      // Using double-slashes is a short-hand to instruct the browser to
47
      // reference a resource using the parent URL's security model. This
48
      // is known as a protocol-relative URL.
49
      return resource.startsWith( "//" )
50
          ? HTTP
51
          : valueFrom( new File( resource ) );
52
    }
6453
  }
6554
6655
  /**
6756
   * Determines the protocol scheme for a given string.
6857
   *
6958
   * @param protocol A string representing data encoding protocol scheme.
7059
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
7160
   * valid value from this enumeration.
7261
   */
73
  public static ProtocolScheme valueFrom( String protocol ) {
74
    ProtocolScheme result = UNKNOWN;
75
    protocol = sanitize( protocol );
62
  public static ProtocolScheme valueFrom( final String protocol ) {
63
    final var sanitized = protocol == null ? "" : protocol.toUpperCase();
7664
7765
    for( final var scheme : values() ) {
7866
      // This will match HTTP/HTTPS as well as FILE*, which may be inaccurate.
79
      if( protocol.startsWith( scheme.name() ) ) {
80
        result = scheme;
81
        break;
67
      if( sanitized.startsWith( scheme.name() ) ) {
68
        return scheme;
8269
      }
8370
    }
8471
85
    return result;
72
    return UNKNOWN;
8673
  }
8774
8875
  /**
89
   * Returns an empty string if the given string to sanitize is {@code null},
90
   * otherwise the given string in uppercase. Uppercase is used to align with
91
   * the enum name.
76
   * Determines the protocol scheme for a given {@link File}.
9277
   *
93
   * @param s The string to sanitize, may be {@code null}.
94
   * @return A non-{@code null} string.
78
   * @param file A file having a URI that contains a protocol scheme.
79
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
80
   * valid value from this enumeration.
9581
   */
96
  private static String sanitize( final String s ) {
97
    return s == null ? "" : s.toUpperCase();
82
  public static ProtocolScheme valueFrom( final File file ) {
83
    return valueFrom( file.toURI() );
84
  }
85
86
  /**
87
   * Determines the protocol scheme for a given {@link URI}.
88
   *
89
   * @param uri A URI that contains a protocol scheme.
90
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
91
   * valid value from this enumeration.
92
   */
93
  public static ProtocolScheme valueFrom( final URI uri ) {
94
    try {
95
      return valueFrom( uri.toURL() );
96
    } catch( final Exception ex ) {
97
      return UNKNOWN;
98
    }
99
  }
100
101
  /**
102
   * Determines the protocol scheme for a given {@link URL}.
103
   *
104
   * @param url A {@link URL} that contains a protocol scheme.
105
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
106
   * valid value from this enumeration.
107
   */
108
  public static ProtocolScheme valueFrom( final URL url ) {
109
    return valueFrom( url.getProtocol() );
110
  }
111
112
  /**
113
   * Answers {@code true} if the given protocol is for a local file, which
114
   * includes a JAR file.
115
   *
116
   * @return {@code false} the protocol is not a local file reference.
117
   */
118
  public boolean isFile() {
119
    return this == FILE || this == JAR;
120
  }
121
122
  /**
123
   * Answers {@code true} if the given protocol is either HTTP or HTTPS.
124
   *
125
   * @return {@code true} the protocol is either HTTP or HTTPS.
126
   */
127
  public boolean isHttp() {
128
    return this == HTTP;
129
  }
130
131
  /**
132
   * Answers {@code true} if the given protocol is for a Java archive file.
133
   *
134
   * @return {@code false} the protocol is not a Java archive file.
135
   */
136
  public boolean isJar() {
137
    return this == JAR;
98138
  }
99139
}
M src/main/java/com/keenwrite/util/ResourceWalker.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
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
package com.keenwrite.util;
293
304
import java.io.IOException;
5
import java.net.URI;
316
import java.net.URISyntaxException;
32
import java.nio.file.*;
7
import java.nio.file.Files;
8
import java.nio.file.Path;
9
import java.nio.file.Paths;
3310
import java.util.function.Consumer;
3411
12
import static java.nio.file.FileSystems.getDefault;
3513
import static java.nio.file.FileSystems.newFileSystem;
3614
import static java.util.Collections.emptyMap;
3715
3816
/**
3917
 * Responsible for finding file resources.
4018
 */
4119
public class ResourceWalker {
42
  private static final PathMatcher PATH_MATCHER =
43
      FileSystems.getDefault().getPathMatcher( "glob:**.{ttf,otf}" );
20
  /**
21
   * Globbing pattern to match font names.
22
   */
23
  public static final String GLOB_FONTS = "**.{ttf,otf}";
4424
4525
  /**
46
   * @param dirName The root directory to scan for files matching the glob.
47
   * @param c       The consumer function to call for each matching path found.
26
   * @param directory The root directory to scan for files matching the glob.
27
   * @param c         The consumer function to call for each matching path
28
   *                  found.
4829
   * @throws URISyntaxException Could not convert the resource to a URI.
4930
   * @throws IOException        Could not walk the tree.
5031
   */
51
  public static void walk( final String dirName, final Consumer<Path> c )
32
  public static void walk(
33
      final String directory, final String glob, final Consumer<Path> c )
5234
      throws URISyntaxException, IOException {
53
    final var resource = ResourceWalker.class.getResource( dirName );
35
    final var resource = ResourceWalker.class.getResource( directory );
36
    final var matcher = getDefault().getPathMatcher( "glob:" + glob );
5437
5538
    if( resource != null ) {
5639
      final var uri = resource.toURI();
57
      final var path = uri.getScheme().equals( "jar" )
58
          ? newFileSystem( uri, emptyMap() ).getPath( dirName )
59
          : Paths.get( uri );
60
      final var walk = Files.walk( path, 10 );
40
      final var jar = uri.getScheme().equals( "jar" );
41
      final var path = jar ? toFileSystem( uri, directory ) : Paths.get( uri );
6142
62
      for( final var it = walk.iterator(); it.hasNext(); ) {
63
        final Path p = it.next();
64
        if( PATH_MATCHER.matches( p ) ) {
65
          c.accept( p );
43
      try( final var walk = Files.walk( path, 10 ) ) {
44
        for( final var it = walk.iterator(); it.hasNext(); ) {
45
          final Path p = it.next();
46
          if( matcher.matches( p ) ) {
47
            c.accept( p );
48
          }
6649
        }
6750
      }
51
    }
52
  }
53
54
  private static Path toFileSystem( final URI uri, final String directory )
55
      throws IOException {
56
    try( final var fs = newFileSystem( uri, emptyMap() ) ) {
57
      return fs.getPath( directory );
6858
    }
6959
  }
D src/main/java/com/keenwrite/util/SeparatorAction.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.keenwrite.util;
29
30
import javafx.scene.Node;
31
import javafx.scene.control.MenuItem;
32
import javafx.scene.control.Separator;
33
import javafx.scene.control.SeparatorMenuItem;
34
35
/**
36
 * Represents a menu bar action that has no operation, acting as a placeholder
37
 * for line separators.
38
 */
39
public class SeparatorAction extends Action {
40
  @Override
41
  public MenuItem createMenuItem() {
42
    return new SeparatorMenuItem();
43
  }
44
45
  @Override
46
  public Node createToolBarButton() {
47
    return new Separator();
48
  }
49
}
501
D src/main/java/com/keenwrite/util/StageState.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.util;
28
29
import java.util.prefs.Preferences;
30
31
import javafx.application.Platform;
32
import javafx.scene.shape.Rectangle;
33
import javafx.stage.Stage;
34
import javafx.stage.WindowEvent;
35
36
/**
37
 * Saves and restores Stage state (window bounds, maximized, fullScreen).
38
 */
39
public class StageState {
40
41
  public static final String K_PANE_SPLIT_DEFINITION = "pane.split.definition";
42
  public static final String K_PANE_SPLIT_EDITOR = "pane.split.editor";
43
  public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview";
44
45
  private final Stage mStage;
46
  private final Preferences mState;
47
48
  private Rectangle normalBounds;
49
  private boolean runLaterPending;
50
51
  public StageState( final Stage stage, final Preferences state ) {
52
    mStage = stage;
53
    mState = state;
54
55
    restore();
56
57
    stage.addEventHandler( WindowEvent.WINDOW_HIDING, e -> save() );
58
59
    stage.xProperty().addListener( ( ob, o, n ) -> boundsChanged() );
60
    stage.yProperty().addListener( ( ob, o, n ) -> boundsChanged() );
61
    stage.widthProperty().addListener( ( ob, o, n ) -> boundsChanged() );
62
    stage.heightProperty().addListener( ( ob, o, n ) -> boundsChanged() );
63
  }
64
65
  private void save() {
66
    final Rectangle bounds = isNormalState() ? getStageBounds() : normalBounds;
67
68
    if( bounds != null ) {
69
      mState.putDouble( "windowX", bounds.getX() );
70
      mState.putDouble( "windowY", bounds.getY() );
71
      mState.putDouble( "windowWidth", bounds.getWidth() );
72
      mState.putDouble( "windowHeight", bounds.getHeight() );
73
    }
74
75
    mState.putBoolean( "windowMaximized", mStage.isMaximized() );
76
    mState.putBoolean( "windowFullScreen", mStage.isFullScreen() );
77
  }
78
79
  private void restore() {
80
    final double x = mState.getDouble( "windowX", Double.NaN );
81
    final double y = mState.getDouble( "windowY", Double.NaN );
82
    final double w = mState.getDouble( "windowWidth", Double.NaN );
83
    final double h = mState.getDouble( "windowHeight", Double.NaN );
84
    final boolean maximized = mState.getBoolean( "windowMaximized", false );
85
    final boolean fullScreen = mState.getBoolean( "windowFullScreen", false );
86
87
    if( !Double.isNaN( x ) && !Double.isNaN( y ) ) {
88
      mStage.setX( x );
89
      mStage.setY( y );
90
    } // else: default behavior is center on screen
91
92
    if( !Double.isNaN( w ) && !Double.isNaN( h ) ) {
93
      mStage.setWidth( w );
94
      mStage.setHeight( h );
95
    } // else: default behavior is use scene size
96
97
    if( fullScreen != mStage.isFullScreen() ) {
98
      mStage.setFullScreen( fullScreen );
99
    }
100
101
    if( maximized != mStage.isMaximized() ) {
102
      mStage.setMaximized( maximized );
103
    }
104
  }
105
106
  /**
107
   * Remembers the window bounds when the window is not iconified, maximized or
108
   * in fullScreen.
109
   */
110
  private void boundsChanged() {
111
    // avoid too many (and useless) runLater() invocations
112
    if( runLaterPending ) {
113
      return;
114
    }
115
116
    runLaterPending = true;
117
118
    // must use runLater() to ensure that change of all properties
119
    // (x, y, width, height, iconified, maximized and fullScreen)
120
    // has finished
121
    Platform.runLater( () -> {
122
      runLaterPending = false;
123
124
      if( isNormalState() ) {
125
        normalBounds = getStageBounds();
126
      }
127
    } );
128
  }
129
130
  private boolean isNormalState() {
131
    return !mStage.isIconified() &&
132
        !mStage.isMaximized() &&
133
        !mStage.isFullScreen();
134
  }
135
136
  private Rectangle getStageBounds() {
137
    return new Rectangle(
138
        mStage.getX(),
139
        mStage.getY(),
140
        mStage.getWidth(),
141
        mStage.getHeight()
142
    );
143
  }
144
}
1451
D src/main/java/com/keenwrite/util/Utils.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.util;
28
29
import java.util.ArrayList;
30
import java.util.prefs.Preferences;
31
32
/**
33
 * Responsible for trimming, storing, and retrieving strings.
34
 */
35
public class Utils {
36
37
  public static String ltrim( final String s ) {
38
    int i = 0;
39
40
    while( i < s.length() && Character.isWhitespace( s.charAt( i ) ) ) {
41
      i++;
42
    }
43
44
    return s.substring( i );
45
  }
46
47
  public static String rtrim( final String s ) {
48
    int i = s.length() - 1;
49
50
    while( i >= 0 && Character.isWhitespace( s.charAt( i ) ) ) {
51
      i--;
52
    }
53
54
    return s.substring( 0, i + 1 );
55
  }
56
57
  public static String[] getPrefsStrings( final Preferences prefs,
58
                                          String key ) {
59
    final ArrayList<String> arr = new ArrayList<>( 256 );
60
61
    for( int i = 0; i < 10000; i++ ) {
62
      final String s = prefs.get( key + (i + 1), null );
63
64
      if( s == null ) {
65
        break;
66
      }
67
68
      arr.add( s );
69
    }
70
71
    return arr.toArray( new String[ 0 ] );
72
  }
73
74
  public static void putPrefsStrings( Preferences prefs, String key,
75
                                      String[] strings ) {
76
    for( int i = 0; i < strings.length; i++ ) {
77
      prefs.put( key + (i + 1), strings[ i ] );
78
    }
79
80
    for( int i = strings.length; prefs.get( key + (i + 1),
81
                                            null ) != null; i++ ) {
82
      prefs.remove( key + (i + 1) );
83
    }
84
  }
85
}
861
D src/main/resources/META-INF/services/com.keenwrite.service.Options
1
com.keenwrite.service.impl.DefaultOptions
1
M src/main/resources/com/keenwrite/editor/markdown.css
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
282
29
.markdown-editor {
3
.markdown {
4
  -fx-font-family: 'Noto Sans';
305
  -fx-font-size: 11pt;
6
  -fx-padding: 1em;
317
}
328
339
/* Subtly highlight the current paragraph. */
34
.markdown-editor .paragraph-box:has-caret {
10
.markdown .paragraph-box:has-caret {
3511
  -fx-background-color: #fcfeff;
3612
}
3713
3814
/* Light colour for selection highlight. */
39
.markdown-editor .selection {
15
.markdown .selection {
4016
  -fx-fill: #a6d2ff;
4117
}
4218
4319
/* Decoration for words not found in the lexicon. */
44
.markdown-editor .spelling {
20
.markdown .spelling {
4521
  -rtfx-underline-color: rgba(255, 131, 67, .7);
4622
  -rtfx-underline-dash-array: 4, 2;
4723
  -rtfx-underline-width: 2;
4824
  -rtfx-underline-cap: round;
25
}
26
27
.markdown .search {
28
  -rtfx-background-color: #ffe959;
4929
}
5030
A src/main/resources/com/keenwrite/editor/markdown_en-Latn-AU.css
11
A src/main/resources/com/keenwrite/editor/markdown_en-Latn-CA.css
11
A src/main/resources/com/keenwrite/editor/markdown_en-Latn-GB.css
11
A src/main/resources/com/keenwrite/editor/markdown_en-Latn-NZ.css
11
A src/main/resources/com/keenwrite/editor/markdown_en-Latn-US.css
11
A src/main/resources/com/keenwrite/editor/markdown_en-Latn-ZA.css
11
A src/main/resources/com/keenwrite/editor/markdown_ja-Jpan-JP.css
1
.markdown {
2
  -fx-font-family: 'Noto Sans CJK JP';
3
}
14
A src/main/resources/com/keenwrite/editor/markdown_ko-Kore-KR.css
1
.markdown {
2
  -fx-font-family: 'Noto Sans CJK KR';
3
}
14
A src/main/resources/com/keenwrite/editor/markdown_zh-Hans-CN.css
1
.markdown {
2
  -fx-font-family: 'Noto Sans CJK SC';
3
}
14
A src/main/resources/com/keenwrite/editor/markdown_zh-Hans-SG.css
1
.markdown {
2
  -fx-font-family: 'Noto Sans CJK SC';
3
}
14
A src/main/resources/com/keenwrite/editor/markdown_zh-Hant-HK.css
1
.markdown {
2
  -fx-font-family: 'Noto Sans CJK HK';
3
}
14
A src/main/resources/com/keenwrite/editor/markdown_zh-Hant-TW.css
1
.markdown {
2
  -fx-font-family: 'Noto Sans CJK TC';
3
}
14
D src/main/resources/com/keenwrite/logo-original.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
2
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1280" height="1024" viewBox="0 0 1280 1024" xml:space="preserve">
4
<desc>Created with Fabric.js 3.6.3</desc>
5
<defs>
6
</defs>
7
<g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0153846153846 512.012312418764)" id="background-logo"  >
8
<rect style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  x="-325" y="-260" rx="0" ry="0" width="650" height="520" />
9
</g>
10
<g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0170725174504 420.4016715831266)" id="logo-logo"  >
11
<g style=""  paint-order="stroke"   >
12
		<g transform="matrix(2.537 0 0 -2.537 -86.35385711719567 85.244912)"  >
13
<linearGradient id="SVGID_1_302284" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-24.348526 -27.478867 -27.478867 24.348526 138.479 129.67187)"  x1="0" y1="0" x2="1" y2="0">
14
<stop offset="0%" style="stop-color:rgb(245,132,41);stop-opacity: 1"/>
15
<stop offset="100%" style="stop-color:rgb(251,173,23);stop-opacity: 1"/>
16
</linearGradient>
17
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: url(#SVGID_1_302284); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(-127.92674550729492, -117.16399999999999)" d="m 118.951 124.648 c -9.395 -14.441 -5.243 -20.693 -5.243 -20.693 v 0 c 0 0 6.219 9.126 9.771 5.599 v 0 c 3.051 -3.023 -2.415 -8.668 -2.415 -8.668 v 0 c 0 0 33.24 13.698 17.995 28.872 v 0 c 0 0 -3.203 3.683 -7.932 3.684 v 0 c -3.46 0 -7.736 -1.97 -12.176 -8.794" stroke-linecap="round" />
18
</g>
19
		<g transform="matrix(2.537 0 0 -2.537 -84.52085711719567 70.2729119999999)"  >
20
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(250,220,153); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(11.9895, -1.2609990716440347)" d="m 0 0 c 0 0 -6.501 6.719 -11.093 5.443 c -5.584 -1.545 -12.886 -12.078 -12.886 -12.078 c 0 0 5.98 16.932 15.29 15.731 C -1.19 8.127 0 0 0 0" stroke-linecap="round" />
21
</g>
22
		<g transform="matrix(2.537 0 0 -2.537 -22.327857117195663 48.729911999999956)"  >
23
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(-4.189, -10.432)" d="m 0 0 l -0.87 16.89 l 3.995 3.974 l 6.123 -6.156 z" stroke-linecap="round" />
24
</g>
25
		<g transform="matrix(2.537 0 0 -2.537 -11.3118571171957 24.124911999999966)"  >
26
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(4.0955, -2.037)" d="m 0 0 l -2.081 -2.069 l -6.11 6.143 l 2.081 2.069 z" stroke-linecap="round" />
27
</g>
28
		<g transform="matrix(2.537 0 0 -2.537 46.27614288280432 -57.96708800000005)"  >
29
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(12.070999999999998, 9.599000000000004)" d="m 0 0 c -1.226 0.69 -2.81 0.523 -3.862 -0.524 c -1.275 -1.268 -1.28 -3.33 -0.013 -4.604 l -31.681 -31.501 l -6.11 6.143 c 19.224 19.305 25.369 35.582 25.369 35.582 c 15.857 2.364 27.851 8.624 33.821 12.335 z" stroke-linecap="round" />
30
</g>
31
		<g transform="matrix(2.537 0 0 -2.537 -26.842857117195706 8.501911999999976)"  >
32
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(4.1075, -2.0525)" d="M 0 0 L -2.081 -2.069 L -8.215 4.11 L -6.141 6.174 Z" stroke-linecap="round" />
33
</g>
34
		<g transform="matrix(2.537 0 0 -2.537 -51.495857117195726 19.491911999999985)"  >
35
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(10.434000000000001, -1.0939999999999994)" d="m 0 0 l -3.995 -3.974 l -16.873 0.96 l 14.752 9.176 z" stroke-linecap="round" />
36
</g>
37
		<g transform="matrix(2.537 0 0 -2.537 55.72014288280434 -48.441088000000036)"  >
38
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(9.671499999999998, 11.999499999999998)" d="M 0 0 L 17.536 17.443 C 13.788 11.486 7.47 -0.468 5.021 -16.312 c 0 0 -15.526 -6.982 -35.765 -25.13 l -6.135 6.168 l 31.681 31.5 c 1.273 -1.28 3.33 -1.279 4.604 -0.012 C 0.435 -2.764 0.629 -1.223 0 0" stroke-linecap="round" />
39
</g>
40
</g>
41
</g>
42
<g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 643.7363123827618 766.1975713477327)" id="text-logo-path"  >
43
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(247,149,33); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(-186.83999999999997, 27.08)" d="M 4.47 -6.1 L 4.47 -6.1 L 4.47 -47.5 Q 4.47 -50.27 6.43 -52.23 Q 8.39 -54.19 11.16 -54.19 L 11.16 -54.19 Q 14.01 -54.19 15.95 -52.23 Q 17.89 -50.27 17.89 -47.5 L 17.89 -47.5 L 17.89 -30.09 L 34.95 -51.97 Q 35.74 -52.97 36.94 -53.58 Q 38.13 -54.19 39.42 -54.19 L 39.42 -54.19 Q 41.77 -54.19 43.42 -52.5 Q 45.07 -50.82 45.07 -48.5 L 45.07 -48.5 Q 45.07 -46.46 43.82 -44.93 L 43.82 -44.93 L 32.93 -31.44 L 46.8 -9.81 Q 47.84 -8.11 47.84 -6.27 L 47.84 -6.27 Q 47.84 -3.33 45.9 -1.39 Q 43.96 0.55 41.19 0.55 L 41.19 0.55 Q 39.42 0.55 37.89 -0.29 Q 36.37 -1.14 35.43 -2.57 L 35.43 -2.57 L 23.78 -21.15 L 17.89 -13.9 L 17.89 -6.1 Q 17.89 -3.33 15.93 -1.39 Q 13.97 0.55 11.16 0.55 L 11.16 0.55 Q 8.39 0.55 6.43 -1.39 Q 4.47 -3.33 4.47 -6.1 Z M 50.27 -19.24 L 50.27 -19.24 Q 50.27 -25.13 52.71 -29.78 Q 55.16 -34.43 59.7 -37.06 Q 64.24 -39.69 70.27 -39.69 L 70.27 -39.69 Q 76.37 -39.69 80.78 -37.09 Q 85.18 -34.49 87.43 -30.32 Q 89.69 -26.14 89.69 -21.6 L 89.69 -21.6 Q 89.69 -18.69 88.33 -17.26 Q 86.98 -15.84 83.86 -15.84 L 83.86 -15.84 L 62.89 -15.84 Q 63.23 -12.38 65.38 -10.31 Q 67.53 -8.25 70.86 -8.25 L 70.86 -8.25 Q 72.84 -8.25 74.19 -8.91 Q 75.54 -9.57 76.62 -10.64 L 76.62 -10.64 Q 77.62 -11.58 78.42 -12.03 Q 79.22 -12.48 80.43 -12.48 L 80.43 -12.48 Q 82.61 -12.48 84.19 -10.89 Q 85.77 -9.29 85.77 -7.04 L 85.77 -7.04 Q 85.77 -4.54 83.62 -2.77 L 83.62 -2.77 Q 81.71 -1.14 78.16 -0.03 Q 74.61 1.07 70.58 1.07 L 70.58 1.07 Q 64.76 1.07 60.13 -1.42 Q 55.5 -3.92 52.89 -8.53 Q 50.27 -13.14 50.27 -19.24 Z M 62.96 -23.57 L 62.96 -23.57 L 76.96 -23.57 Q 76.82 -26.97 74.93 -28.97 Q 73.05 -30.96 70.06 -30.96 L 70.06 -30.96 Q 67.08 -30.96 65.21 -28.97 Q 63.34 -26.97 62.96 -23.57 Z M 91.63 -19.24 L 91.63 -19.24 Q 91.63 -25.13 94.07 -29.78 Q 96.52 -34.43 101.06 -37.06 Q 105.6 -39.69 111.63 -39.69 L 111.63 -39.69 Q 117.73 -39.69 122.14 -37.09 Q 126.54 -34.49 128.79 -30.32 Q 131.04 -26.14 131.04 -21.6 L 131.04 -21.6 Q 131.04 -18.69 129.69 -17.26 Q 128.34 -15.84 125.22 -15.84 L 125.22 -15.84 L 104.25 -15.84 Q 104.59 -12.38 106.74 -10.31 Q 108.89 -8.25 112.22 -8.25 L 112.22 -8.25 Q 114.2 -8.25 115.55 -8.91 Q 116.9 -9.57 117.98 -10.64 L 117.98 -10.64 Q 118.98 -11.58 119.78 -12.03 Q 120.58 -12.48 121.79 -12.48 L 121.79 -12.48 Q 123.97 -12.48 125.55 -10.89 Q 127.13 -9.29 127.13 -7.04 L 127.13 -7.04 Q 127.13 -4.54 124.98 -2.77 L 124.98 -2.77 Q 123.07 -1.14 119.52 -0.03 Q 115.96 1.07 111.94 1.07 L 111.94 1.07 Q 106.12 1.07 101.49 -1.42 Q 96.86 -3.92 94.24 -8.53 Q 91.63 -13.14 91.63 -19.24 Z M 104.32 -23.57 L 104.32 -23.57 L 118.32 -23.57 Q 118.18 -26.97 116.29 -28.97 Q 114.4 -30.96 111.42 -30.96 L 111.42 -30.96 Q 108.44 -30.96 106.57 -28.97 Q 104.7 -26.97 104.32 -23.57 Z M 135.03 -6.03 L 135.03 -6.03 L 135.03 -33.14 Q 135.03 -35.64 136.85 -37.46 Q 138.67 -39.28 141.13 -39.28 L 141.13 -39.28 Q 143.7 -39.28 145.52 -37.46 Q 147.34 -35.64 147.34 -33.14 L 147.34 -33.14 L 147.34 -32.17 Q 148.97 -35.36 152.09 -37.42 Q 155.21 -39.49 159.82 -39.49 L 159.82 -39.49 Q 166.93 -39.49 170.19 -35.47 Q 173.44 -31.44 173.44 -24.44 L 173.44 -24.44 L 173.44 -6.03 Q 173.44 -3.33 171.5 -1.39 Q 169.56 0.55 166.86 0.55 L 166.86 0.55 Q 164.15 0.55 162.19 -1.39 Q 160.24 -3.33 160.24 -6.03 L 160.24 -6.03 L 160.24 -22.36 Q 160.24 -26.35 158.54 -27.91 Q 156.84 -29.47 154.65 -29.47 L 154.65 -29.47 Q 152.02 -29.47 150.13 -27.58 Q 148.24 -25.69 148.24 -20.73 L 148.24 -20.73 L 148.24 -6.03 Q 148.24 -3.33 146.3 -1.39 Q 144.36 0.55 141.65 0.55 L 141.65 0.55 Q 138.95 0.55 136.99 -1.39 Q 135.03 -3.33 135.03 -6.03 Z M 177.71 -47.56 L 177.71 -47.56 Q 177.71 -50.34 179.63 -52.26 Q 181.56 -54.19 184.23 -54.19 L 184.23 -54.19 Q 186.58 -54.19 188.39 -52.73 Q 190.19 -51.27 190.71 -48.99 L 190.71 -48.99 L 197.88 -15.12 L 206.52 -48.64 Q 207.07 -51.07 209.12 -52.63 Q 211.16 -54.19 213.69 -54.19 L 213.69 -54.19 Q 216.26 -54.19 218.25 -52.57 Q 220.25 -50.96 220.8 -48.64 L 220.8 -48.64 L 229.4 -15.39 L 236.64 -49.33 Q 237.06 -51.38 238.76 -52.78 Q 240.46 -54.19 242.61 -54.19 L 242.61 -54.19 Q 245.17 -54.19 246.94 -52.4 Q 248.71 -50.62 248.71 -48.05 L 248.71 -48.05 Q 248.71 -47.56 248.57 -46.73 L 248.57 -46.73 L 239.69 -7.38 Q 238.9 -3.99 236.11 -1.72 Q 233.32 0.55 229.68 0.55 L 229.68 0.55 Q 226.14 0.55 223.37 -1.61 Q 220.59 -3.78 219.73 -7.11 L 219.73 -7.11 L 213.07 -33.45 L 206.38 -7.11 Q 205.51 -3.71 202.79 -1.58 Q 200.07 0.55 196.53 0.55 L 196.53 0.55 Q 192.89 0.55 190.17 -1.72 Q 187.45 -3.99 186.65 -7.38 L 186.65 -7.38 L 177.85 -46.14 Q 177.71 -47.15 177.71 -47.56 Z M 253.35 -6.03 L 253.35 -6.03 L 253.35 -33.14 Q 253.35 -35.64 255.17 -37.46 Q 256.99 -39.28 259.46 -39.28 L 259.46 -39.28 Q 262.02 -39.28 263.84 -37.46 Q 265.66 -35.64 265.66 -33.14 L 265.66 -33.14 L 265.66 -31.44 L 265.94 -31.44 Q 266.8 -33.56 268.1 -35.24 Q 269.4 -36.92 270.69 -37.61 L 270.69 -37.61 Q 271.9 -38.24 273.46 -38.27 L 273.46 -38.27 Q 276.65 -38.27 278.14 -36.45 Q 279.63 -34.63 279.63 -32.52 L 279.63 -32.52 Q 279.63 -30.33 278.11 -28.62 Q 276.58 -26.9 274.08 -26.9 L 274.08 -26.9 Q 272.59 -26.9 271.07 -26.26 Q 269.54 -25.62 268.47 -24.34 L 268.47 -24.34 Q 266.56 -21.98 266.56 -17.68 L 266.56 -17.68 L 266.56 -6.03 Q 266.56 -3.33 264.62 -1.39 Q 262.68 0.55 259.98 0.55 L 259.98 0.55 Q 257.27 0.55 255.31 -1.39 Q 253.35 -3.33 253.35 -6.03 Z M 282.41 -49.71 L 282.41 -49.71 Q 282.41 -52 284.03 -53.61 Q 285.66 -55.23 287.95 -55.23 L 287.95 -55.23 L 291.21 -55.23 Q 293.5 -55.23 295.13 -53.6 Q 296.76 -51.97 296.76 -49.71 L 296.76 -49.71 Q 296.76 -47.43 295.11 -45.8 Q 293.46 -44.17 291.21 -44.17 L 291.21 -44.17 L 287.95 -44.17 Q 285.66 -44.17 284.03 -45.8 Q 282.41 -47.43 282.41 -49.71 Z M 282.96 -6.03 L 282.96 -6.03 L 282.96 -32.66 Q 282.96 -35.36 284.92 -37.32 Q 286.88 -39.28 289.58 -39.28 L 289.58 -39.28 Q 292.29 -39.28 294.23 -37.32 Q 296.17 -35.36 296.17 -32.66 L 296.17 -32.66 L 296.17 -6.03 Q 296.17 -3.33 294.21 -1.39 Q 292.25 0.55 289.58 0.55 L 289.58 0.55 Q 286.88 0.55 284.92 -1.39 Q 282.96 -3.33 282.96 -6.03 Z M 299.43 -34.29 L 299.43 -34.29 Q 299.43 -36.12 300.71 -37.41 Q 301.99 -38.69 303.76 -38.69 L 303.76 -38.69 L 306.19 -38.69 L 306.46 -43.96 Q 306.6 -46.32 308.34 -47.98 Q 310.07 -49.64 312.5 -49.64 L 312.5 -49.64 Q 314.99 -49.64 316.76 -47.86 Q 318.53 -46.07 318.53 -43.58 L 318.53 -43.58 L 318.53 -38.69 L 322.72 -38.69 Q 324.49 -38.69 325.77 -37.41 Q 327.06 -36.12 327.06 -34.36 L 327.06 -34.36 Q 327.06 -32.52 325.77 -31.24 Q 324.49 -29.95 322.72 -29.95 L 322.72 -29.95 L 318.81 -29.95 L 318.81 -14.14 Q 318.81 -11.23 320.05 -10.02 Q 321.3 -8.81 323.83 -8.81 L 323.83 -8.81 Q 325.46 -8.46 326.61 -7.14 Q 327.75 -5.82 327.75 -4.06 L 327.75 -4.06 Q 327.75 -2.57 326.94 -1.39 Q 326.12 -0.21 324.84 0.35 L 324.84 0.35 Q 322 0.83 318.11 0.87 L 318.11 0.87 Q 311.28 0.9 308.44 -2.5 L 308.44 -2.5 Q 305.67 -5.79 305.67 -12.65 L 305.67 -12.65 Q 305.67 -12.83 305.67 -13 L 305.67 -13 L 305.74 -29.95 L 303.76 -29.95 Q 301.99 -29.95 300.71 -31.24 Q 299.43 -32.52 299.43 -34.29 Z M 329.8 -19.24 L 329.8 -19.24 Q 329.8 -25.13 332.24 -29.78 Q 334.68 -34.43 339.23 -37.06 Q 343.77 -39.69 349.8 -39.69 L 349.8 -39.69 Q 355.9 -39.69 360.3 -37.09 Q 364.71 -34.49 366.96 -30.32 Q 369.21 -26.14 369.21 -21.6 L 369.21 -21.6 Q 369.21 -18.69 367.86 -17.26 Q 366.51 -15.84 363.39 -15.84 L 363.39 -15.84 L 342.42 -15.84 Q 342.76 -12.38 344.91 -10.31 Q 347.06 -8.25 350.39 -8.25 L 350.39 -8.25 Q 352.37 -8.25 353.72 -8.91 Q 355.07 -9.57 356.14 -10.64 L 356.14 -10.64 Q 357.15 -11.58 357.95 -12.03 Q 358.74 -12.48 359.96 -12.48 L 359.96 -12.48 Q 362.14 -12.48 363.72 -10.89 Q 365.3 -9.29 365.3 -7.04 L 365.3 -7.04 Q 365.3 -4.54 363.15 -2.77 L 363.15 -2.77 Q 361.24 -1.14 357.69 -0.03 Q 354.13 1.07 350.11 1.07 L 350.11 1.07 Q 344.29 1.07 339.66 -1.42 Q 335.03 -3.92 332.41 -8.53 Q 329.8 -13.14 329.8 -19.24 Z M 342.48 -23.57 L 342.48 -23.57 L 356.49 -23.57 Q 356.35 -26.97 354.46 -28.97 Q 352.57 -30.96 349.59 -30.96 L 349.59 -30.96 Q 346.61 -30.96 344.74 -28.97 Q 342.87 -26.97 342.48 -23.57 Z" stroke-linecap="round" />
44
</g>
45
</svg>
1
D src/main/resources/com/keenwrite/logo-text.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
   xmlns:cc="http://creativecommons.org/ns#"
5
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
   xmlns:svg="http://www.w3.org/2000/svg"
7
   xmlns="http://www.w3.org/2000/svg"
8
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
   version="1.1"
11
   width="1280"
12
   height="1024"
13
   viewBox="0 0 1280 1024"
14
   xml:space="preserve"
15
   id="svg52"
16
   sodipodi:docname="logo-text.svg"
17
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"><metadata
18
   id="metadata56"><rdf:RDF><cc:Work
19
       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
20
         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview
21
   inkscape:document-rotation="0"
22
   pagecolor="#ffffff"
23
   bordercolor="#666666"
24
   borderopacity="1"
25
   objecttolerance="10"
26
   gridtolerance="10"
27
   guidetolerance="10"
28
   inkscape:pageopacity="0"
29
   inkscape:pageshadow="2"
30
   inkscape:window-width="640"
31
   inkscape:window-height="480"
32
   id="namedview54"
33
   showgrid="false"
34
   inkscape:zoom="0.78417969"
35
   inkscape:cx="642.50039"
36
   inkscape:cy="508.59942"
37
   inkscape:current-layer="svg52" />
38
<desc
39
   id="desc2">Created with Fabric.js 3.6.3</desc>
40
<defs
41
   id="defs4"><rect
42
   x="114.92139"
43
   y="132.06312"
44
   width="470.12033"
45
   height="175.55822"
46
   id="rect933" />
47
48
49
50
		
51
		
52
		
53
		
54
		
55
		
56
		
57
		
58
59
<linearGradient
60
   y2="-0.049471263"
61
   x2="0.96880889"
62
   y1="-0.044911571"
63
   x1="0.15235768"
64
   gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)"
65
   gradientUnits="userSpaceOnUse"
66
   id="SVGID_1_302284">
67
<stop
68
   id="stop9"
69
   style="stop-color:#ec706a;stop-opacity:1"
70
   offset="0%" />
71
<stop
72
   id="stop11"
73
   style="stop-color:#ecd980;stop-opacity:1"
74
   offset="100%" />
75
</linearGradient>
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
</defs>
92
93
<g
94
   id="g853"><path
95
     style="fill:url(#SVGID_1_302284);fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
96
     paint-order="stroke"
97
     d="m 425.11895,550.88213 c -46.93797,72.14807 -26.19433,103.38343 -26.19433,103.38343 v 0 c 0,0 31.07048,-45.59403 48.81648,-27.97293 v 0 c 15.24298,15.10308 -12.06548,43.30583 -12.06548,43.30583 v 0 c 0,0 166.06898,-68.436 89.90407,-144.24619 v 0 c 0,0 -16.00237,-18.40049 -39.62873,-18.40548 v 0 c -17.28637,0 -38.64951,9.84223 -60.83201,43.93534"
98
     stroke-linecap="round"
99
     id="path14" /><path
100
     style="fill:#126d95;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1"
101
     paint-order="stroke"
102
     d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z"
103
     stroke-linecap="round"
104
     id="path22" /><path
105
     style="fill:#126d95;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1"
106
     paint-order="stroke"
107
     d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z"
108
     stroke-linecap="round"
109
     id="path26" /><path
110
     style="fill:#51a9cf;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1"
111
     paint-order="stroke"
112
     d="m 791.45508,258.2912 c -6.12517,-3.44728 -14.03892,-2.61294 -19.29478,2.61793 -6.36997,6.33501 -6.39495,16.63688 -0.0649,23.00186 L 613.81523,441.29182 583.28931,410.60103 c 96.04423,-96.4489 126.74501,-177.76974 126.74501,-177.76974 79.22249,-11.81068 139.14522,-43.08601 168.97169,-61.62638 z"
113
     stroke-linecap="round"
114
     id="path30" /><path
115
     style="fill:#51a9cf;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1"
116
     paint-order="stroke"
117
     d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z"
118
     stroke-linecap="round"
119
     id="path34" /><path
120
     style="fill:#51a9cf;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1"
121
     paint-order="stroke"
122
     d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z"
123
     stroke-linecap="round"
124
     id="path38" /><path
125
     style="fill:#126d95;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1"
126
     paint-order="stroke"
127
     d="m 798.0649,265.0575 87.61088,-87.14624 c -18.72523,29.76151 -50.29032,89.4844 -62.52567,168.64194 0,0 -77.5688,34.88248 -178.68403,125.55095 L 613.81527,441.28846 772.09539,283.91262 c 6.35998,6.39496 16.63687,6.38996 23.00185,0.06 5.14095,-5.10597 6.11018,-12.8049 2.96766,-18.91508"
128
     stroke-linecap="round"
129
     id="path42" /></g>
130
131
<text
132
   xml:space="preserve"
133
   id="text931"
134
   style="fill:black;fill-opacity:1;stroke:none;font-family:sans-serif;font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect933);" /><text
135
   xml:space="preserve"
136
   style="font-style:italic;font-variant:normal;font-weight:800;font-stretch:normal;font-size:133.333px;line-height:1.25;font-family:'Merriweather Sans';-inkscape-font-specification:'Merriweather Sans, Ultra-Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#51a9cf;fill-opacity:1;stroke:none;"
137
   x="311.87085"
138
   y="820.2641"
139
   id="text939"><tspan
140
     sodipodi:role="line"
141
     id="tspan937"
142
     x="311.87085"
143
     y="820.2641">KeenWrite</tspan></text></svg>
1441
M src/main/resources/com/keenwrite/messages.properties
55
# suppress inspection "UnusedProperty" for whole file
66
7
Main.menu.file=_File
8
Main.menu.file.new=_New
9
Main.menu.file.open=_Open...
10
Main.menu.file.close=_Close
11
Main.menu.file.close_all=Close All
12
Main.menu.file.save=_Save
13
Main.menu.file.save_as=Save _As
14
Main.menu.file.save_all=Save A_ll
15
Main.menu.file.export=_Export As
16
Main.menu.file.export.html_svg=HTML and S_VG
17
Main.menu.file.export.html_tex=HTML and _TeX
18
Main.menu.file.export.markdown=Markdown
19
Main.menu.file.exit=E_xit
7
# ########################################################################
8
# Menu Bar
9
# ########################################################################
2010
11
Main.menu.file=_File
2112
Main.menu.edit=_Edit
22
Main.menu.edit.undo=_Undo
23
Main.menu.edit.redo=_Redo
24
Main.menu.edit.cut=Cu_t
25
Main.menu.edit.copy=_Copy
26
Main.menu.edit.paste=_Paste
27
Main.menu.edit.selectAll=Select _All
28
Main.menu.edit.find=_Find
29
Main.menu.edit.find.next=Find _Next
30
Main.menu.edit.preferences=_Preferences
31
3213
Main.menu.insert=_Insert
33
Main.menu.insert.blockquote=_Blockquote
34
Main.menu.insert.code=Inline _Code
35
Main.menu.insert.fenced_code_block=_Fenced Code Block
36
Main.menu.insert.fenced_code_block.prompt=Enter code here
37
Main.menu.insert.link=_Link...
38
Main.menu.insert.image=_Image...
39
Main.menu.insert.heading.1=Heading _1
40
Main.menu.insert.heading.1.prompt=heading 1
41
Main.menu.insert.heading.2=Heading _2
42
Main.menu.insert.heading.2.prompt=heading 2
43
Main.menu.insert.heading.3=Heading _3
44
Main.menu.insert.heading.3.prompt=heading 3
45
Main.menu.insert.unordered_list=_Unordered List
46
Main.menu.insert.ordered_list=_Ordered List
47
Main.menu.insert.horizontal_rule=_Horizontal Rule
48
4914
Main.menu.format=Forma_t
50
Main.menu.format.bold=_Bold
51
Main.menu.format.italic=_Italic
52
Main.menu.format.superscript=Su_perscript
53
Main.menu.format.subscript=Su_bscript
54
Main.menu.format.strikethrough=Stri_kethrough
55
5615
Main.menu.definition=_Definition
57
Main.menu.definition.create=_Create
58
Main.menu.definition.insert=_Insert
59
16
Main.menu.view=_View
6017
Main.menu.help=_Help
61
Main.menu.help.about=About
18
19
# ########################################################################
20
# Detachable Tabs
21
# ########################################################################
22
23
# {0} is the application title; {1} is a unique window ID.
24
Detach.tab.title={0} - {1}
6225
6326
# ########################################################################
...
7942
Main.status.error.messages.recursion=Lookup depth exceeded, check for loops in ''{0}''
8043
Main.status.error.messages.syntax=Missing ''}'' in ''{0}''
44
45
Main.status.error.undo=Cannot undo; beginning of undo history reached
46
Main.status.error.redo=Cannot redo; end of redo history reached
47
48
Main.status.image.request.init=Initializing HTTP request
49
Main.status.image.request.fetch=Requesting content type from {0}
50
Main.status.image.request.success=Detected content type ''{0}''
8151
8252
# ########################################################################
83
# Preferences
53
# Search Bar
8454
# ########################################################################
8555
86
Preferences.r=R
87
Preferences.r.script=Startup Script
88
Preferences.r.script.desc=Script runs prior to executing R statements within the document.
89
Preferences.r.directory=Working Directory
90
Preferences.r.directory.desc=Value assigned to $application.r.working.directory$ and usable in the startup script.
91
Preferences.r.delimiter.began=Delimiter Prefix
92
Preferences.r.delimiter.began.desc=Prefix of expression that wraps inserted definitions.
93
Preferences.r.delimiter.ended=Delimiter Suffix
94
Preferences.r.delimiter.ended.desc=Suffix of expression that wraps inserted definitions.
56
Main.search.stop.tooltip=Close search bar
57
Main.search.stop.icon=CLOSE
58
Main.search.next.tooltip=Find next match
59
Main.search.next.icon=CHEVRON_DOWN
60
Main.search.prev.tooltip=Find previous match
61
Main.search.prev.icon=CHEVRON_UP
62
Main.search.find.tooltip=Search document for text
63
Main.search.find.icon=SEARCH
64
Main.search.match.none=No matches
65
Main.search.match.some={0} of {1} matches
9566
96
Preferences.images=Images
97
Preferences.images.directory=Relative Directory
98
Preferences.images.directory.desc=Path prepended to embedded images referenced using local file paths.
99
Preferences.images.suffixes=Extensions
100
Preferences.images.suffixes.desc=Preferred order of image file types to embed, separated by spaces.
67
# ########################################################################
68
# Workspace preferences
69
# ########################################################################
10170
102
Preferences.definitions=Definitions
103
Preferences.definitions.path=File name
104
Preferences.definitions.path.desc=Absolute path to interpolated string definitions.
105
Preferences.definitions.delimiter.began=Delimiter Prefix
106
Preferences.definitions.delimiter.began.desc=Indicates when a definition key is starting.
107
Preferences.definitions.delimiter.ended=Delimiter Suffix
108
Preferences.definitions.delimiter.ended.desc=Indicates when a definition key is ending.
71
workspace.r=R
72
workspace.r.script=Startup Script
73
workspace.r.script.desc=Script runs prior to executing R statements within the document.
74
workspace.r.dir=Working Directory
75
workspace.r.dir.desc=Value assigned to {0}application.r.working.directory{1} and usable in the startup script.
76
workspace.r.dir.title=Directory
77
workspace.r.delimiter.began=Delimiter Prefix
78
workspace.r.delimiter.began.desc=Prefix of expression that wraps inserted definitions.
79
workspace.r.delimiter.began.title=Opening
80
workspace.r.delimiter.ended=Delimiter Suffix
81
workspace.r.delimiter.ended.desc=Suffix of expression that wraps inserted definitions.
82
workspace.r.delimiter.ended.title=Closing
10983
110
Preferences.fonts=Editor
111
Preferences.fonts.size_editor=Font Size
112
Preferences.fonts.size_editor.desc=Font size to use for the text editor.
84
workspace.images=Images
85
workspace.images.dir=Relative Directory
86
workspace.images.dir.desc=Path prepended to embedded images referenced using local file paths.
87
workspace.images.dir.title=Directory
88
workspace.images.order=Extensions
89
workspace.images.order.desc=Preferred order of image file types to embed, separated by spaces.
90
workspace.images.order.title=Extensions
91
92
workspace.definition=Definition
93
workspace.definition.path=File name
94
workspace.definition.path.desc=Absolute path to interpolated string definition.
95
workspace.definition.path.title=Path
96
workspace.definition.delimiter.began=Delimiter Prefix
97
workspace.definition.delimiter.began.desc=Indicates when a definition key is starting.
98
workspace.definition.delimiter.began.title=Opening
99
workspace.definition.delimiter.ended=Delimiter Suffix
100
workspace.definition.delimiter.ended.desc=Indicates when a definition key is ending.
101
workspace.definition.delimiter.ended.title=Closing
102
103
workspace.ui.font=Fonts
104
workspace.ui.font.editor.size=Editor Font Size
105
workspace.ui.font.editor.size.desc=Text editor font size.
106
workspace.ui.font.editor.size.title=Points
107
workspace.ui.font.preview.size=Preview Font Size
108
workspace.ui.font.preview.size.desc=Preview pane font size.
109
workspace.ui.font.preview.size.title=Points
110
workspace.ui.font.locale=Locale
111
workspace.ui.font.locale.desc=Character set for editing and previewing.
112
workspace.ui.font.locale.title=Language
113113
114114
# ########################################################################
115115
# Definition Pane and its Tree View
116116
# ########################################################################
117117
118
Definition.menu.create=Create
119
Definition.menu.rename=Rename
120
Definition.menu.remove=Delete
121118
Definition.menu.add.default=Undefined
119
120
# ########################################################################
121
# Definition Pane
122
# ########################################################################
123
124
Pane.definition.node.root.title=Definitions
122125
123126
# ########################################################################
124127
# Failure messages with respect to YAML files.
125128
# ########################################################################
129
126130
yaml.error.open=Could not open YAML file (ensure non-empty file).
127131
yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''.
128132
yaml.error.missing=Empty definition value for key ''{0}''.
129133
yaml.error.tree.form=Unassigned definition near ''{0}''.
130134
131135
# ########################################################################
132
# File Editor
136
# Text Resource
133137
# ########################################################################
134138
135
FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1}
136
FileEditor.loadFailed.title=Load
137
FileEditor.loadFailed.reason.permissions=File must be readable and writable.
138
FileEditor.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
139
FileEditor.saveFailed.title=Save
139
TextResource.load.error.unsaved=The file ''{0}'' is unsaved or does not exist.
140
TextResource.load.error.permissions=The file ''{0}'' must be readable and writable.
141
142
# ########################################################################
143
# Text Resources
144
# ########################################################################
145
146
TextResource.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
147
TextResource.saveFailed.title=Save
140148
141149
# ########################################################################
...
151159
Dialog.file.choose.filter.title.xml=XML Files
152160
Dialog.file.choose.filter.title.all=All Files
153
154
# ########################################################################
155
# Alert Dialog
156
# ########################################################################
157
158
Alert.file.close.title=Close
159
Alert.file.close.text=Save changes to {0}?
160
161
# ########################################################################
162
# Definition Pane
163
# ########################################################################
164
165
Pane.definition.node.root.title=Definitions
166
Pane.definition.button.create.label=_Create
167
Pane.definition.button.rename.label=_Rename
168
Pane.definition.button.delete.label=_Delete
169
Pane.definition.button.create.tooltip=Add new item (Insert)
170
Pane.definition.button.rename.tooltip=Rename selected item (F2)
171
Pane.definition.button.delete.tooltip=Delete selected items (Delete)
172
173
# Controls ###############################################################
174161
175162
# ########################################################################
176163
# Browse File
177164
# ########################################################################
178165
179166
BrowseFileButton.chooser.title=Browse for local file
180167
BrowseFileButton.chooser.allFilesFilter=All Files
181168
BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
182169
183
# Dialogs ################################################################
170
# ########################################################################
171
# Alert Dialog
172
# ########################################################################
173
174
Alert.file.close.title=Close
175
Alert.file.close.text=Save changes to {0}?
184176
185177
# ########################################################################
186
# Image
178
# Image Dialog
187179
# ########################################################################
188180
...
195187
196188
# ########################################################################
197
# Hyperlink
189
# Hyperlink Dialog
198190
# ########################################################################
199191
200192
Dialog.link.title=Link
201193
Dialog.link.previewLabel.text=Markdown Preview\:
202194
Dialog.link.textLabel.text=Link Text\:
203195
Dialog.link.titleLabel.text=Title (tooltip)\:
204196
Dialog.link.urlLabel.text=Link URL\:
205197
206198
# ########################################################################
207
# About
199
# About Dialog
208200
# ########################################################################
209201
210202
Dialog.about.title=About {0}
211203
Dialog.about.header={0}
212204
Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
205
206
# ########################################################################
207
# Application Actions
208
# ########################################################################
209
210
App.action.file.new.description=Create a new file
211
App.action.file.new.accelerator=Shortcut+N
212
App.action.file.new.icon=FILE_ALT
213
App.action.file.new.text=_New
214
215
App.action.file.open.description=Open a new file
216
App.action.file.open.accelerator=Shortcut+O
217
App.action.file.open.text=_Open...
218
App.action.file.open.icon=FOLDER_OPEN_ALT
219
220
App.action.file.close.description=Close the current document
221
App.action.file.close.accelerator=Shortcut+W
222
App.action.file.close.text=_Close
223
224
App.action.file.close_all.description=Close all open documents
225
App.action.file.close_all.accelerator=Ctrl+F4
226
App.action.file.close_all.text=Close All
227
228
App.action.file.save.description=Save the document
229
App.action.file.save.accelerator=Shortcut+S
230
App.action.file.save.text=_Save
231
App.action.file.save.icon=FLOPPY_ALT
232
233
App.action.file.save_as.description=Rename the current document
234
App.action.file.save_as.text=Save _As
235
236
App.action.file.save_all.description=Save all open documents
237
App.action.file.save_all.accelerator=Shortcut+Shift+S
238
App.action.file.save_all.text=Save A_ll
239
240
App.action.file.export.html_svg.description=Export the current document as HTML + SVG
241
App.action.file.export.text=_Export As
242
App.action.file.export.html_svg.text=HTML and S_VG
243
244
App.action.file.export.html_tex.description=Export the current document as HTML + TeX
245
App.action.file.export.html_tex.text=HTML and _TeX
246
247
App.action.file.export.markdown.description=Export the current document as Markdown
248
App.action.file.export.markdown.text=Markdown
249
250
App.action.file.exit.description=Quit the application
251
App.action.file.exit.text=E_xit
252
253
254
App.action.edit.undo.description=Undo the previous edit
255
App.action.edit.undo.accelerator=Shortcut+Z
256
App.action.edit.undo.text=_Undo
257
App.action.edit.undo.icon=UNDO
258
259
App.action.edit.redo.description=Redo the previous edit
260
App.action.edit.redo.accelerator=Shortcut+Y
261
App.action.edit.redo.text=_Redo
262
App.action.edit.redo.icon=REPEAT
263
264
App.action.edit.cut.description=Delete the selected text or line
265
App.action.edit.cut.accelerator=Shortcut+X
266
App.action.edit.cut.text=Cu_t
267
App.action.edit.cut.icon=CUT
268
269
App.action.edit.copy.description=Copy the selected text
270
App.action.edit.copy.accelerator=Shortcut+C
271
App.action.edit.copy.text=_Copy
272
App.action.edit.copy.icon=COPY
273
274
App.action.edit.paste.description=Paste from the clipboard
275
App.action.edit.paste.accelerator=Shortcut+V
276
App.action.edit.paste.text=_Paste
277
App.action.edit.paste.icon=PASTE
278
279
App.action.edit.select_all.description=Highlight the current document text
280
App.action.edit.select_all.accelerator=Shortcut+A
281
App.action.edit.select_all.text=Select _All
282
283
App.action.edit.find.description=Search for text in the document
284
App.action.edit.find.accelerator=Shortcut+F
285
App.action.edit.find.text=_Find
286
App.action.edit.find.icon=SEARCH
287
288
App.action.edit.find_next.description=Find next occurrence
289
App.action.edit.find_next.accelerator=F3
290
App.action.edit.find_next.text=Find _Next
291
292
App.action.edit.find_prev.description=Find previous occurrence
293
App.action.edit.find_prev.accelerator=Shift+F3
294
App.action.edit.find_prev.text=Find _Prev
295
296
App.action.edit.preferences.description=Edit user preferences
297
App.action.edit.preferences.accelerator=Ctrl+Alt+S
298
App.action.edit.preferences.text=_Preferences
299
300
301
App.action.format.bold.description=Insert strong text
302
App.action.format.bold.accelerator=Shortcut+B
303
App.action.format.bold.text=_Bold
304
App.action.format.bold.icon=BOLD
305
306
App.action.format.italic.description=Insert text emphasis
307
App.action.format.italic.accelerator=Shortcut+I
308
App.action.format.italic.text=_Italic
309
App.action.format.italic.icon=ITALIC
310
311
App.action.format.superscript.description=Insert superscript text
312
App.action.format.superscript.accelerator=Shortcut+[
313
App.action.format.superscript.text=Su_perscript
314
App.action.format.superscript.icon=SUPERSCRIPT
315
316
App.action.format.subscript.description=Insert subscript text
317
App.action.format.subscript.accelerator=Shortcut+]
318
App.action.format.subscript.text=Su_bscript
319
App.action.format.subscript.icon=SUBSCRIPT
320
321
App.action.format.strikethrough.description=Insert struck text
322
App.action.format.strikethrough.accelerator=Shortcut+T
323
App.action.format.strikethrough.text=Stri_kethrough
324
App.action.format.strikethrough.icon=STRIKETHROUGH
325
326
327
App.action.insert.blockquote.description=Insert blockquote
328
App.action.insert.blockquote.accelerator=Ctrl+Q
329
App.action.insert.blockquote.text=_Blockquote
330
App.action.insert.blockquote.icon=QUOTE_LEFT
331
332
App.action.insert.code.description=Insert inline code
333
App.action.insert.code.accelerator=Shortcut+K
334
App.action.insert.code.text=Inline _Code
335
App.action.insert.code.icon=CODE
336
337
App.action.insert.fenced_code_block.description=Insert code block
338
App.action.insert.fenced_code_block.accelerator=Shortcut+Shift+K
339
App.action.insert.fenced_code_block.text=_Fenced Code Block
340
App.action.insert.fenced_code_block.prompt.text=Enter code here
341
App.action.insert.fenced_code_block.icon=FILE_CODE_ALT
342
343
App.action.insert.link.description=Insert hyperlink
344
App.action.insert.link.accelerator=Shortcut+L
345
App.action.insert.link.text=_Link...
346
App.action.insert.link.icon=LINK
347
348
App.action.insert.image.description=Insert image
349
App.action.insert.image.accelerator=Shortcut+G
350
App.action.insert.image.text=_Image...
351
App.action.insert.image.icon=PICTURE_ALT
352
353
App.action.insert.heading.description=Insert heading level
354
App.action.insert.heading.accelerator=Shortcut+
355
App.action.insert.heading.icon=HEADER
356
357
App.action.insert.heading_1.description=${App.action.insert.heading.description} 1
358
App.action.insert.heading_1.accelerator=${App.action.insert.heading.accelerator}1
359
App.action.insert.heading_1.text=Heading _1
360
App.action.insert.heading_1.icon=${App.action.insert.heading.icon}
361
362
App.action.insert.heading_2.description=${App.action.insert.heading.description} 2
363
App.action.insert.heading_2.accelerator=${App.action.insert.heading.accelerator}2
364
App.action.insert.heading_2.text=Heading _2
365
App.action.insert.heading_2.icon=${App.action.insert.heading.icon}
366
367
App.action.insert.heading_3.description=${App.action.insert.heading.description} 3
368
App.action.insert.heading_3.accelerator=${App.action.insert.heading.accelerator}3
369
App.action.insert.heading_3.text=Heading _3
370
App.action.insert.heading_3.icon=${App.action.insert.heading.icon}
371
372
App.action.insert.unordered_list.description=Insert bulleted list
373
App.action.insert.unordered_list.accelerator=Shortcut+U
374
App.action.insert.unordered_list.text=_Unordered List
375
App.action.insert.unordered_list.icon=LIST_UL
376
377
App.action.insert.ordered_list.description=Insert enumerated list
378
App.action.insert.ordered_list.accelerator=Shortcut+Shift+O
379
App.action.insert.ordered_list.text=_Ordered List
380
App.action.insert.ordered_list.icon=LIST_OL
381
382
App.action.insert.horizontal_rule.description=Insert horizontal rule
383
App.action.insert.horizontal_rule.accelerator=Shortcut+H
384
App.action.insert.horizontal_rule.text=_Horizontal Rule
385
App.action.insert.horizontal_rule.icon=LIST_OL
386
387
388
App.action.definition.create.description=Create a new variable definition
389
App.action.definition.create.text=_Create
390
App.action.definition.create.icon=TREE
391
App.action.definition.create.tooltip=Add new item (Insert)
392
393
App.action.definition.rename.description=Rename the selected variable definition
394
App.action.definition.rename.text=_Rename
395
App.action.definition.rename.icon=EDIT
396
App.action.definition.rename.tooltip=Rename selected item (F2)
397
398
App.action.definition.delete.description=Delete the selected variable definitions
399
App.action.definition.delete.text=_Delete
400
App.action.definition.delete.icon=TRASH
401
App.action.definition.delete.tooltip=Delete selected items (Delete)
402
403
App.action.definition.insert.description=Insert a definition
404
App.action.definition.insert.accelerator=Ctrl+Space
405
App.action.definition.insert.text=_Insert
406
App.action.definition.insert.icon=STAR
407
408
409
App.action.view.refresh.description=Clear all caches
410
App.action.view.refresh.accelerator=F5
411
App.action.view.refresh.text=Refresh
412
413
App.action.view.preview.description=Open document preview
414
App.action.view.preview.accelerator=F7
415
App.action.view.preview.text=Preview
416
417
App.action.view.outline.description=Open document outline
418
App.action.view.outline.accelerator=F8
419
App.action.view.outline.text=Outline
420
421
422
App.action.view.files.description=Open file system browser
423
App.action.view.files.accelerator=F9
424
App.action.view.files.text=File system
425
426
427
App.action.help.about.description=Show help dialog
428
App.action.help.about.accelerator=F1
429
App.action.help.about.text=About
430
App.action.help.about.icon=INFO
213431
M src/main/resources/com/keenwrite/preview/webview.css
1
/* RESET ***/
21
html{box-sizing:border-box;font-size:12pt}body,h1,h2,h3,h4,h5,h6,ol,p,ul{margin:0;padding:0}img{max-width:100%;height:auto}table{table-collapse:collapse;table-spacing:0;border-spacing:0}
32
4
/* BODY ***/
53
body {
6
  /* Must be bundled in JAR file. */
7
  font-family: "Vollkorn", serif;
4
  /* Noto Serif introduces whitespace on style transitions. */
5
  font-family: 'Source Serif Pro';
6
  font-size: 12pt;
7
88
  background-color: #fff;
99
  margin: 0 auto;
10
  max-width: 960px;
1110
  line-height: 1.6;
1211
  color: #454545;
1312
  padding: 1em;
14
  font-feature-settings: "liga" 1;
13
  font-feature-settings: 'liga' 1;
1514
  font-variant-ligatures: normal;
1615
}
...
132131
pre, code, tt {
133132
  /* Must be bundled in JAR file. */
134
  font-family: "Fira Code", monospace;
133
  font-family: 'Source Code Pro';
135134
  font-size: 10pt;
136135
  background-color: #f8f8f8;
...
207206
img {
208207
  max-width: 100%;
208
209
  /* Tell FlyingSaucer to treat images as block elements.
210
   * See SvgReplacedElementFactory.
211
   */
212
  display: inline-block;
209213
}
210214
211
/* Required for FlyingSaucer to detect the node.
212
 * See SVGReplacedElementFactory for details.
215
/* Tell FlyingSaucer to treat tex elements as nodes.
216
 * See SvgReplacedElementFactory.
213217
 */
214218
tex {
A src/main/resources/com/keenwrite/preview/webview_ja-Jpan-JP.css
1
body {
2
  font-family: 'Noto Serif CJK JP';
3
}
4
5
pre, code, tt {
6
  font-family: 'Noto Sans Mono CJK JP';
7
}
18
A src/main/resources/com/keenwrite/preview/webview_ko-Kore-KR.css
1
body {
2
  font-family: 'Noto Serif CJK KR';
3
}
4
5
pre, code, tt {
6
  font-family: 'Noto Sans Mono CJK KR';
7
}
18
A src/main/resources/com/keenwrite/preview/webview_zh-Hans-CN.css
1
body {
2
  font-family: 'Noto Serif CJK SC';
3
}
4
5
pre, code, tt {
6
  font-family: 'Noto Sans Mono CJK SC';
7
}
18
A src/main/resources/com/keenwrite/preview/webview_zh-Hans-SG.css
1
body {
2
  font-family: 'Noto Serif CJK SC';
3
}
4
5
pre, code, tt {
6
  font-family: 'Noto Sans Mono CJK SC';
7
}
18
A src/main/resources/com/keenwrite/preview/webview_zh-Hant-HK.css
1
body {
2
  font-family: 'Noto Serif CJK SC';
3
}
4
5
pre, code, tt {
6
  font-family: 'Noto Sans Mono CJK HK';
7
}
18
A src/main/resources/com/keenwrite/preview/webview_zh-Hant-TW.css
1
body {
2
  font-family: 'Noto Serif CJK TC';
3
}
4
5
pre, code, tt {
6
  font-family: 'Noto Sans Mono CJK TC';
7
}
18
M src/main/resources/com/keenwrite/scene.css
2626
 */
2727
28
/*---- toolbar ----*/
29
3028
.tool-bar {
3129
	-fx-spacing: 0;
...
4341
.tool-bar .button:armed {
4442
	-fx-color: -fx-pressed-base;
43
}
44
45
/* Definition editor drag and drop target.
46
 */
47
.drop-target {
48
  -fx-border-color: #eea82f;
49
  -fx-border-width: 0 0 2 0;
50
  -fx-padding: 3 3 1 3
4551
}
4652
M src/main/resources/com/keenwrite/settings.properties
2323
# File and Path References
2424
# ########################################################################
25
26
#file.stylesheet.dock=com/panemu/tiwulfx/control/dock/tiwulfx-dock.css
2527
file.stylesheet.scene=${application.package}/scene.css
2628
file.stylesheet.markdown=${application.package}/editor/markdown.css
27
file.stylesheet.preview=webview.css
29
# {0} language code, {1} script code, {2} country code
30
file.stylesheet.markdown.locale=${application.package}/editor/markdown_{0}-{1}-{2}.css
2831
file.stylesheet.xml=${application.package}/xml.css
2932
30
file.logo.16 =${application.package}/logo16.png
31
file.logo.32 =${application.package}/logo32.png
33
# Preview styles are loaded statically through a class's classloader.
34
file.stylesheet.preview=webview.css
35
# {0} language code, {1} script code, {2} country code
36
file.stylesheet.preview.locale=webview_{0}-{1}-{2}.css
37
38
file.logo.16=${application.package}/logo16.png
39
file.logo.32=${application.package}/logo32.png
3240
file.logo.128=${application.package}/logo128.png
3341
file.logo.256=${application.package}/logo256.png
3442
file.logo.512=${application.package}/logo512.png
3543
3644
# Default file name when a new file is created.
3745
# This ensures that the file type can always be
3846
# discerned so that the correct type of variable
3947
# reference can be inserted.
40
file.default=untitled.md
41
file.definition.default=variables.yaml
48
file.default.document=untitled.md
49
file.default.definition=variables.yaml
4250
4351
# ########################################################################
D src/main/resources/com/keenwrite/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
4421
D src/main/resources/fonts/firacode/FiraCode-Bold.ttf
Binary file
D src/main/resources/fonts/firacode/FiraCode-Light.ttf
Binary file
D src/main/resources/fonts/firacode/FiraCode-Medium.ttf
Binary file
D src/main/resources/fonts/firacode/FiraCode-Regular.ttf
Binary file
D src/main/resources/fonts/firacode/FiraCode-Retina.ttf
Binary file
D src/main/resources/fonts/firacode/FiraCode-SemiBold.ttf
Binary file
A src/main/resources/fonts/font-names
1
#!/usr/bin/env bash
2
3
# Writes the name for all OTF files found in the current directory or lower
4
5
find . -type f \( -name "*otf" -o -name "*ttf" \) -exec \
6
  fc-scan --format "%{foundry}: %{family}\n" {} \; | uniq | sort
7
18
A src/main/resources/fonts/noto-sans/NotoSans-Bold.ttf
Binary file
A src/main/resources/fonts/noto-sans/NotoSans-BoldItalic.ttf
Binary file
A src/main/resources/fonts/noto-sans/NotoSans-Italic.ttf
Binary file
A src/main/resources/fonts/noto-sans/NotoSans-Regular.ttf
Binary file
A src/main/resources/fonts/noto-sans-cjk/NotoSansCJKhk-Bold.otf
Binary file
A src/main/resources/fonts/noto-sans-cjk/NotoSansCJKhk-Regular.otf
Binary file
A src/main/resources/fonts/noto-sans-cjk/NotoSansCJKjp-Bold.otf
Binary file
A src/main/resources/fonts/noto-sans-cjk/NotoSansCJKjp-Regular.otf
Binary file
A src/main/resources/fonts/noto-sans-cjk/NotoSansCJKkr-Bold.otf
Binary file
A src/main/resources/fonts/noto-sans-cjk/NotoSansCJKkr-Regular.otf
Binary file
A src/main/resources/fonts/noto-sans-cjk/NotoSansCJKsc-Bold.otf
Binary file
A src/main/resources/fonts/noto-sans-cjk/NotoSansCJKsc-Regular.otf
Binary file
A src/main/resources/fonts/noto-sans-cjk/NotoSansCJKtc-Bold.otf
Binary file
A src/main/resources/fonts/noto-sans-cjk/NotoSansCJKtc-Regular.otf
Binary file
A src/main/resources/fonts/noto-sans-mono-cjk/NotoSansMonoCJKhk-Bold.otf
Binary file
A src/main/resources/fonts/noto-sans-mono-cjk/NotoSansMonoCJKhk-Regular.otf
Binary file
A src/main/resources/fonts/noto-sans-mono-cjk/NotoSansMonoCJKjp-Bold.otf
Binary file
A src/main/resources/fonts/noto-sans-mono-cjk/NotoSansMonoCJKjp-Regular.otf
Binary file
A src/main/resources/fonts/noto-sans-mono-cjk/NotoSansMonoCJKkr-Bold.otf
Binary file
A src/main/resources/fonts/noto-sans-mono-cjk/NotoSansMonoCJKkr-Regular.otf
Binary file
A src/main/resources/fonts/noto-sans-mono-cjk/NotoSansMonoCJKsc-Bold.otf
Binary file
A src/main/resources/fonts/noto-sans-mono-cjk/NotoSansMonoCJKsc-Regular.otf
Binary file
A src/main/resources/fonts/noto-sans-mono-cjk/NotoSansMonoCJKtc-Bold.otf
Binary file
A src/main/resources/fonts/noto-sans-mono-cjk/NotoSansMonoCJKtc-Regular.otf
Binary file
A src/main/resources/fonts/noto-serif-cjk/NotoSerifCJKjp-Bold.otf
Binary file
A src/main/resources/fonts/noto-serif-cjk/NotoSerifCJKjp-Regular.otf
Binary file
A src/main/resources/fonts/noto-serif-cjk/NotoSerifCJKkr-Bold.otf
Binary file
A src/main/resources/fonts/noto-serif-cjk/NotoSerifCJKkr-Regular.otf
Binary file
A src/main/resources/fonts/noto-serif-cjk/NotoSerifCJKsc-Bold.otf
Binary file
A src/main/resources/fonts/noto-serif-cjk/NotoSerifCJKsc-Regular.otf
Binary file
A src/main/resources/fonts/noto-serif-cjk/NotoSerifCJKtc-Bold.otf
Binary file
A src/main/resources/fonts/noto-serif-cjk/NotoSerifCJKtc-Regular.otf
Binary file
A src/main/resources/fonts/source-code-pro/SourceCodePro-Bold.ttf
Binary file
A src/main/resources/fonts/source-code-pro/SourceCodePro-BoldItalic.ttf
Binary file
A src/main/resources/fonts/source-code-pro/SourceCodePro-Italic.ttf
Binary file
A src/main/resources/fonts/source-code-pro/SourceCodePro-Regular.ttf
Binary file
A src/main/resources/fonts/source-serif-pro/SourceSerifPro-Bold.otf
Binary file
A src/main/resources/fonts/source-serif-pro/SourceSerifPro-BoldItalic.otf
Binary file
A src/main/resources/fonts/source-serif-pro/SourceSerifPro-Italic.otf
Binary file
A src/main/resources/fonts/source-serif-pro/SourceSerifPro-Regular.otf
Binary file
D src/main/resources/fonts/vollkorn/Vollkorn-Bold.ttf
Binary file
D src/main/resources/fonts/vollkorn/Vollkorn-BoldItalic.ttf
Binary file
D src/main/resources/fonts/vollkorn/Vollkorn-Italic.ttf
Binary file
D src/main/resources/fonts/vollkorn/Vollkorn-Regular.ttf
Binary file
A src/test/java/com/keenwrite/definition/TreeViewTest.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.definition;
3
4
import com.keenwrite.editors.definition.DefinitionEditor;
5
import com.keenwrite.editors.definition.yaml.YamlTreeTransformer;
6
import com.keenwrite.editors.markdown.MarkdownEditor;
7
import com.keenwrite.preferences.Workspace;
8
import com.keenwrite.preview.HtmlPreview;
9
import com.panemu.tiwulfx.control.dock.DetachableTabPane;
10
import javafx.application.Application;
11
import javafx.beans.property.SimpleObjectProperty;
12
import javafx.event.Event;
13
import javafx.event.EventHandler;
14
import javafx.scene.Node;
15
import javafx.scene.Scene;
16
import javafx.scene.control.ColorPicker;
17
import javafx.scene.control.SplitPane;
18
import javafx.scene.control.Tooltip;
19
import javafx.scene.control.TreeItem;
20
import javafx.stage.Stage;
21
import org.testfx.framework.junit5.Start;
22
23
import static com.keenwrite.util.FontLoader.initFonts;
24
25
//@ExtendWith(ApplicationExtension.class)
26
public class TreeViewTest extends Application {
27
  private final SimpleObjectProperty<Node> mTextEditor =
28
    new SimpleObjectProperty<>();
29
30
  private final EventHandler<TreeItem.TreeModificationEvent<Event>> mTreeHandler =
31
    event -> refresh( mTextEditor.get() );
32
33
  private void refresh( final Node node ) {
34
    throw new RuntimeException( "Derp: " + node );
35
  }
36
37
  public static void main( final String[] args ) {
38
    initFonts();
39
    launch( args );
40
  }
41
42
  public void start( final Stage stage ) {
43
    onStart( stage );
44
  }
45
46
  @Start
47
  private void onStart( final Stage stage ) {
48
    final var workspace = new Workspace();
49
    final var mainPane = new SplitPane();
50
51
    final var transformer = new YamlTreeTransformer();
52
    final var editor = new DefinitionEditor( transformer );
53
54
    final var tabPane1 = new DetachableTabPane();
55
    tabPane1.addTab( "Editor", editor );
56
57
    final var tabPane2 = new DetachableTabPane();
58
    final var tab21 = tabPane2.addTab( "Picker", new ColorPicker() );
59
    final var tab22 = tabPane2.addTab( "Editor",
60
                                       new MarkdownEditor( workspace ) );
61
    tab21.setTooltip( new Tooltip( "Colour Picker" ) );
62
    tab22.setTooltip( new Tooltip( "Text Editor" ) );
63
64
    final var tabPane3 = new DetachableTabPane();
65
    tabPane3.addTab( "Preview", new HtmlPreview( workspace ) );
66
67
    editor.addTreeChangeHandler( mTreeHandler );
68
69
    mainPane.getItems().addAll( tabPane1, tabPane2, tabPane3 );
70
71
    final var scene = new Scene( mainPane );
72
    stage.setScene( scene );
73
74
    stage.show();
75
  }
76
}
177
A src/test/java/com/keenwrite/editors/markdown/MarkdownEditorTest.java
1
package com.keenwrite.editors.markdown;
2
3
import com.keenwrite.preferences.Workspace;
4
import org.junit.jupiter.api.Test;
5
import org.junit.jupiter.api.extension.ExtendWith;
6
import org.testfx.framework.junit5.ApplicationExtension;
7
8
import java.util.regex.Pattern;
9
10
import static java.util.regex.Pattern.compile;
11
import static org.junit.jupiter.api.Assertions.assertEquals;
12
import static org.junit.jupiter.api.Assertions.assertTrue;
13
14
@ExtendWith( ApplicationExtension.class )
15
public class MarkdownEditorTest {
16
  private static final String[] WORDS = new String[]{
17
    "Italicize",
18
    "English's",
19
    "foreign",
20
    "words",
21
    "based",
22
    "on",
23
    "popularity,",
24
    "like",
25
    "_bête_",
26
    "_noire_",
27
    "and",
28
    "_Weltanschauung_",
29
    "but",
30
    "not",
31
    "résumé.",
32
    "Don't",
33
    "omit",
34
    "accented",
35
    "characters!",
36
    "Cœlacanthe",
37
    "L'Haÿ-les-Roses",
38
    "Mühlfeldstraße",
39
    "Da̱nx̱a̱laga̱litła̱n",
40
  };
41
42
  private static final String TEXT = String.join( " ", WORDS );
43
44
  private static final Pattern REGEX = compile(
45
    "[^\\p{Mn}\\p{Me}\\p{L}\\p{N}'-]+" );
46
47
  /**
48
   * Test that the {@link MarkdownEditor} can retrieve a word at the caret
49
   * position, regardless of whether the caret is at the beginning, middle, or
50
   * end of the word.
51
   */
52
  @Test
53
  public void test_CaretWord_GetISO88591Word_WordSelected() {
54
    final var editor = createMarkdownEditor();
55
56
    for( int i = 0; i < WORDS.length; i++ ) {
57
      final var word = WORDS[ i ];
58
      final var len = word.length();
59
      final var expected = REGEX.matcher( word ).replaceAll( "" );
60
61
      for( int j = 0; j < len; j++ ) {
62
        editor.moveTo( offset( i ) + j );
63
        final var actual = editor.getCaretWordText();
64
        assertEquals( expected, actual );
65
      }
66
    }
67
  }
68
69
  /**
70
   * Test that the {@link MarkdownEditor} can make a word bold.
71
   */
72
  @Test
73
  public void test_CaretWord_SetWordBold_WordIsBold() {
74
    final var index = 20;
75
    final var editor = createMarkdownEditor();
76
77
    editor.moveTo( offset( index ) );
78
    editor.bold();
79
    assertTrue( editor.getText().contains( "**" + WORDS[ index ] + "**" ) );
80
  }
81
82
  /**
83
   * Returns the document offset for a string at the given index.
84
   */
85
  private static int offset( final int index ) {
86
    assert 0 <= index && index < WORDS.length;
87
    int offset = 0;
88
89
    for( int i = 0; i < index; i++ ) {
90
      offset += WORDS[ i ].length();
91
    }
92
93
    // Add the index to compensate for one space between words.
94
    return offset + index;
95
  }
96
97
  /**
98
   * Returns an instance of {@link MarkdownEditor} pre-populated with
99
   * {@link #TEXT}.
100
   *
101
   * @return A new {@link MarkdownEditor} instance, ready for unit tests.
102
   */
103
  private MarkdownEditor createMarkdownEditor() {
104
    final var workspace = new Workspace();
105
    final var editor = new MarkdownEditor( workspace );
106
    editor.setText( TEXT );
107
    return editor;
108
  }
109
}
1110
A src/test/java/com/keenwrite/io/MediaTypeTest.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import org.junit.jupiter.api.Test;
5
6
import java.net.URI;
7
import java.util.Map;
8
9
import static com.keenwrite.io.MediaType.*;
10
import static org.junit.jupiter.api.Assertions.*;
11
12
/**
13
 * Test that {@link MediaType} instances can be queried and return reliable
14
 * results.
15
 */
16
public class MediaTypeTest {
17
  /**
18
   * Test that {@link MediaType#equals(String, String)} is case insensitive.
19
   */
20
  @Test
21
  public void test_Equality_IgnoreCase_Success() {
22
    final var mediaType = TEXT_PLAIN;
23
    assertTrue( mediaType.equals( "TeXt", "Plain" ) );
24
    assertEquals( "text/plain", mediaType.toString() );
25
  }
26
27
  /**
28
   * Test that {@link MediaType#valueFrom(String)} can lookup by file name.
29
   */
30
  @Test
31
  public void test_FilenameExtensions_Supported_Success() {
32
    final var map = Map.of(
33
      "jpeg", IMAGE_JPEG,
34
      "png", IMAGE_PNG,
35
      "svg", IMAGE_SVG_XML,
36
      "md", TEXT_MARKDOWN,
37
      "Rmd", TEXT_R_MARKDOWN,
38
      "Rxml", TEXT_R_XML,
39
      "txt", TEXT_PLAIN,
40
      "yml", TEXT_YAML
41
    );
42
43
    map.forEach( ( k, v ) -> assertEquals( v, valueFrom( "f." + k ) ) );
44
  }
45
46
  /**
47
   * Test that {@link HttpMediaType#valueFrom(URI)} will pull and identify the
48
   * type of resource based on the HTTP Content-Type header.
49
   */
50
  @Test
51
  public void test_HttpRequest_Supported_Success() {
52
    //@formatter:off
53
    final var map = Map.of(
54
       "https://stackoverflow.com/robots.txt", TEXT_PLAIN,
55
       "https://placekitten.com/g/400/400", IMAGE_JPEG,
56
       "https://upload.wikimedia.org/wikipedia/commons/9/9f/Vimlogo.svg", IMAGE_SVG_XML,
57
       "https://kroki.io//graphviz/svg/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", TEXT_PLAIN
58
    );
59
    //@formatter:on
60
61
    map.forEach( ( k, v ) -> {
62
      try {
63
        assertEquals( v, HttpMediaType.valueFrom( new URI( k ) ) );
64
      } catch( Exception e ) {
65
        fail();
66
      }
67
    } );
68
  }
69
}
170
A src/test/java/com/keenwrite/processors/markdown/ImageLinkExtensionTest.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown;
3
4
import com.keenwrite.preferences.Workspace;
5
import com.vladsch.flexmark.html.HtmlRenderer;
6
import com.vladsch.flexmark.parser.Parser;
7
import org.junit.jupiter.api.Test;
8
import org.junit.jupiter.api.extension.ExtendWith;
9
import org.testfx.framework.junit5.ApplicationExtension;
10
11
import java.io.File;
12
import java.net.URISyntaxException;
13
import java.net.URL;
14
import java.nio.file.Path;
15
import java.nio.file.Paths;
16
import java.util.HashMap;
17
import java.util.List;
18
import java.util.Map;
19
20
import static java.lang.String.format;
21
import static org.junit.jupiter.api.Assertions.assertEquals;
22
import static org.junit.jupiter.api.Assertions.assertNotNull;
23
24
/**
25
 * Responsible for testing that linked images render into HTML according to
26
 * the {@link ImageLinkExtension} rules.
27
 */
28
@ExtendWith( ApplicationExtension.class )
29
@SuppressWarnings( "SameParameterValue" )
30
public class ImageLinkExtensionTest {
31
32
  private static final Map<String, String> IMAGES = new HashMap<>();
33
34
  private static final String URI_WEB = "placekitten.com/200/200";
35
  private static final String URI_DIRNAME = "images";
36
  private static final String URI_FILENAME = "kitten";
37
38
  /**
39
   * Path to use for testing image file name resolution. Note that resources use
40
   * forward slashes, regardless of OS.
41
   */
42
  private static final String URI_PATH = URI_DIRNAME + '/' + URI_FILENAME;
43
44
  /**
45
   * Extension for the first existing image that matches the preferred image
46
   * extension order.
47
   */
48
  private static final String URI_IMAGE_EXT = ".png";
49
50
  /**
51
   * Relative path to an image that exists.
52
   */
53
  private static final String URI_IMAGE = URI_PATH + URI_IMAGE_EXT;
54
55
  static {
56
    addUri( URI_PATH + ".png" );
57
    addUri( URI_PATH + ".jpg" );
58
    addUri( URI_PATH, URI_PATH + URI_IMAGE_EXT );
59
    addUri( "//" + URI_WEB );
60
    addUri( "http://" + URI_WEB );
61
    addUri( "https://" + URI_WEB );
62
  }
63
64
  private static void addUri( final String uri ) {
65
    addUri( uri, uri );
66
  }
67
68
  private static void addUri( final String uriKey, final String uriValue ) {
69
    IMAGES.put( toMd( uriKey ), toHtml( uriValue ) );
70
  }
71
72
  private static String toMd( final String file ) {
73
    return format( "![Tooltip](%s 'Title')", file );
74
  }
75
76
  private static String toHtml( final String file ) {
77
    return format(
78
      "<p><img src=\"%s\" alt=\"Tooltip\" title=\"Title\" /></p>\n", file );
79
  }
80
81
  /**
82
   * Test that the key URIs present in the {@link #IMAGES} map are rendered
83
   * as the value URIs present in the same map.
84
   */
85
  @Test
86
  void test_LocalImage_RelativePathWithExtension_ResolvedSuccessfully()
87
    throws URISyntaxException {
88
    final var workspace = new Workspace();
89
    final var resource = getPathResource( URI_IMAGE );
90
    final var imagePath = new File( URI_IMAGE ).toPath();
91
    final var subpaths = resource.getNameCount() - imagePath.getNameCount();
92
    final var subpath = resource.subpath( 0, subpaths );
93
94
    // The root component isn't considered part of the path, so add it back.
95
    final var path = resource.getRoot().resolve( subpath );
96
97
    final var extension = ImageLinkExtension.create( path, workspace );
98
    final var extensions = List.of( extension );
99
    final var pBuilder = Parser.builder();
100
    final var hBuilder = HtmlRenderer.builder();
101
    final var parser = pBuilder.extensions( extensions ).build();
102
    final var renderer = hBuilder.extensions( extensions ).build();
103
104
    assertNotNull( parser );
105
    assertNotNull( renderer );
106
107
    // Set a default (fallback) image directory search location.
108
    //getInstance().imagesDirectoryProperty().setValue( new File( "." ) );
109
110
    for( final var entry : IMAGES.entrySet() ) {
111
      final var key = entry.getKey();
112
      final var node = parser.parse( key );
113
      final var expectedHtml = entry.getValue();
114
      final var actualHtml = renderer.render( node );
115
116
      assertEquals( expectedHtml, actualHtml );
117
    }
118
  }
119
120
  private Path getPathResource( final String path )
121
    throws URISyntaxException {
122
    final var url = getResource( path );
123
    assert url != null;
124
125
    final var uri = url.toURI();
126
    return Paths.get( uri );
127
  }
128
129
  private URL getResource( final String path ) {
130
    final var packagePath = getClass().getPackageName().replace( '.', '/' );
131
    final var resourcePath = '/' + packagePath + '/' + path;
132
    return getClass().getResource( resourcePath );
133
  }
134
}
1135
M src/test/java/com/keenwrite/r/PluralizeTest.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
12
package com.keenwrite.r;
23
A src/test/java/com/keenwrite/util/CyclicIteratorTest.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import org.junit.jupiter.api.Test;
5
6
import java.util.List;
7
import java.util.ListIterator;
8
import java.util.NoSuchElementException;
9
10
import static org.junit.jupiter.api.Assertions.*;
11
12
/**
13
 * Tests the {@link CyclicIterator} class.
14
 */
15
public class CyclicIteratorTest {
16
  /**
17
   * Test that the {@link CyclicIterator} can move forwards and backwards
18
   * through a {@link List}.
19
   */
20
  @Test
21
  public void test_Directions_NextPreviousCycles_Success() {
22
    final var list = List.of( 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 );
23
    final var iterator = createCyclicIterator( list );
24
25
    // Test forwards through the iterator.
26
    for( int i = 0; i < list.size(); i++ ) {
27
      assertTrue( iterator.hasNext() );
28
      assertEquals( i, iterator.next() );
29
    }
30
31
    // Loop to the first item.
32
    iterator.next();
33
34
    // Test backwards through the iterator.
35
    for( int i = list.size() - 1; i >= 0; i-- ) {
36
      assertTrue( iterator.hasPrevious() );
37
      assertEquals( i, iterator.previous() );
38
    }
39
  }
40
41
  /**
42
   * Test that the {@link CyclicIterator} returns the last element when
43
   * the very first API call is to {@link ListIterator#previous()}.
44
   */
45
  @Test
46
  public void test_Direction_FirstPrevious_ReturnsLastElement() {
47
    final var list = List.of( 1, 2, 3, 4, 5, 6, 7 );
48
    final var iterator = createCyclicIterator( list );
49
50
    assertEquals( iterator.previous(), list.get( list.size() - 1 ) );
51
  }
52
53
  @Test
54
  public void test_Empty_Next_Exception() {
55
    final var iterator = createCyclicIterator( List.of() );
56
    assertThrows( NoSuchElementException.class, iterator::next );
57
  }
58
59
  @Test
60
  public void test_Empty_Previous_Exception() {
61
    final var iterator = createCyclicIterator( List.of() );
62
    assertThrows( NoSuchElementException.class, iterator::previous );
63
  }
64
65
  private <T> CyclicIterator<T> createCyclicIterator( final List<T> list ) {
66
    return new CyclicIterator<>( list );
67
  }
68
}
169
A src/test/resources/com/keenwrite/processors/markdown/images/kitten.jpg
Binary file
A src/test/resources/com/keenwrite/processors/markdown/images/kitten.png
Binary file