Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
A .gitattributes
1
# always use LF line endings
2
3
# ALL FILES:
4
#   Normalize line endings to LF on checkin and
5
#   prevent conversion to CRLF when the file is checked out.
6
7
* text eol=lf
8
9
10
# BINARY FILES:
11
#   Disable line ending normalize on checkin.
12
13
*.gif binary
14
*.jar binary
15
*.png binary
16
*.zip binary
17
*.ttf binary
18
*.otf binary
19
*.blend binary
20
121
A .github/ISSUE_TEMPLATE/bug_report.md
1
---
2
name: Bug report
3
about: Create a report to help us improve
4
title: ''
5
labels: bug
6
assignees: ''
7
8
---
9
10
**Description**
11
A concise problem description.
12
13
**Replicate**
14
Exact and complete steps to reproduce the problem 100% of the time:
15
16
1. Open '...'
17
1. Click '....'
18
1. Click '....'
19
20
**Expected**
21
Describe the expected behaviour.
22
23
**Actual**
24
Describe the actual behaviour.
25
26
**Screenshots**
27
Add screenshots to show the problem, if applicable.
28
29
**Environment**
30
 - Operating System: (Windows, Linux, Mac)
31
 - Application: e.g., 1.7.16
32
33
**Details**
34
Add additional information, if applicable.
135
A .gitignore
1
dist
2
*.bin
3
*.exe
4
/*.jar
5
build
6
.gradle
7
contacted.csv
8
video
9
.settings
10
.classpath
11
.idea
112
A .project
1
<?xml version="1.0" encoding="UTF-8"?>
2
<projectDescription>
3
	<name>Markdown Writer FX</name>
4
	<comment></comment>
5
	<projects>
6
	</projects>
7
	<buildSpec>
8
		<buildCommand>
9
			<name>org.eclipse.jdt.core.javabuilder</name>
10
			<arguments>
11
			</arguments>
12
		</buildCommand>
13
		<buildCommand>
14
			<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
15
			<arguments>
16
			</arguments>
17
		</buildCommand>
18
	</buildSpec>
19
	<natures>
20
		<nature>org.eclipse.jdt.core.javanature</nature>
21
		<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
22
	</natures>
23
	<filteredResources>
24
		<filter>
25
			<id>1438449113801</id>
26
			<name></name>
27
			<type>26</type>
28
			<matcher>
29
				<id>org.eclipse.ui.ide.multiFilter</id>
30
				<arguments>1.0-projectRelativePath-matches-false-false-build</arguments>
31
			</matcher>
32
		</filter>
33
		<filter>
34
			<id>1438449113801</id>
35
			<name></name>
36
			<type>26</type>
37
			<matcher>
38
				<id>org.eclipse.ui.ide.multiFilter</id>
39
				<arguments>1.0-projectRelativePath-matches-false-false-.gradle</arguments>
40
			</matcher>
41
		</filter>
42
	</filteredResources>
43
</projectDescription>
144
A BUILD.md
1
# Introduction
2
3
This document describes how to build the application and platform binaries.
4
5
# Requirements
6
7
Download and install the following software packages:
8
9
* [OpenJDK 14.0.2](https://openjdk.java.net) (full JDK, including JavaFX)
10
* [Gradle 6.4](https://gradle.org/releases)
11
* [Git 2.28.0](https://git-scm.com/downloads)
12
13
## Repository
14
15
Clone the repository as follows:
16
17
    git clone https://github.com/DaveJarvis/keenwrite.git
18
19
The repository is cloned.
20
21
# Build
22
23
Build the application überjar as follows:
24
25
    cd keenwrite
26
    gradle clean jar
27
28
The application is built.
29
30
# Run
31
32
After the application is compiled, run it as follows:
33
34
    java -jar build/libs/keenwrite.jar
35
36
On Windows:
37
38
    java -jar build\libs\keenwrite.jar
39
40
# Integrated development environments
41
42
This section describes setup instructions to import and run the application using an integrated development environment (IDE). Running the application should trigger a build.
43
44
## IntelliJ IDEA
45
46
This section describes how to build and run the application using IntellIJ's IDEA.
47
48
### Import
49
50
Complete the following steps to import the application:
51
52
1. Start the IDE.
53
1. Click **File → New → Project from Existing Sources**.
54
1. Browse to the directory containing `keenwrite`.
55
1. Click **OK**.
56
1. Select **Gradle** as the external model.
57
1. Click **Finish**.
58
59
The project is imported into the IDE.
60
61
### Run
62
63
Run the application as follows:
64
65
1. Ensure the **Project** is open.
66
1. Expand **src → main → java → com.keenwrite**.
67
1. Open **Launcher**.
68
1. Run **main**.
69
70
The application is launched.
71
72
# Installers
73
74
This section describes how to set up the development environment and build native executables for supported operating systems.
75
76
## Setup
77
78
Follow these one-time setup instructions to begin:
79
80
1. Ensure `$HOME/bin` is set in the `PATH` environment variable.
81
1. Copy `build-template` into `$HOME/bin`.
82
83
Setup is complete.
84
85
## Binaries
86
87
Run the `installer` script to build platform-specific binaries, such as:
88
89
    ./installer -V -o linux
90
91
The `installer` script:
92
93
* downloads a JDK;
94
* generates a run script;
95
* bundles the JDK, run script, and JAR file; and
96
* creates a standalone binary, so no installation required.
97
98
Run `./installer -h` to see all command-line options.
99
100
# Versioning
101
102
Version numbers are read directly from Git using a plugin. The version number is written to `app.properties` in the `resources` directory. The application reads that file to display version information upon start.
103
1104
A CODE_OF_CONDUCT.md
1
# Contributor Covenant Code of Conduct
2
3
## Our Pledge
4
5
In the interest of fostering an open and welcoming environment, we as
6
contributors and maintainers pledge to making participation in our project and
7
our community a harassment-free experience for everyone, regardless of age, body
8
size, disability, ethnicity, sex characteristics, gender identity and expression,
9
level of experience, education, socio-economic status, nationality, personal
10
appearance, race, religion, or sexual identity and orientation.
11
12
## Our Standards
13
14
Examples of behavior that contributes to creating a positive environment
15
include:
16
17
* Using welcoming and inclusive language
18
* Being respectful of differing viewpoints and experiences
19
* Gracefully accepting constructive criticism
20
* Focusing on what is best for the community
21
* Showing empathy towards other community members
22
23
Examples of unacceptable behavior by participants include:
24
25
* The use of sexualized language or imagery and unwelcome sexual attention or
26
 advances
27
* Trolling, insulting/derogatory comments, and personal or political attacks
28
* Public or private harassment
29
* Publishing others' private information, such as a physical or electronic
30
 address, without explicit permission
31
* Other conduct which could reasonably be considered inappropriate in a
32
 professional setting
33
34
## Our Responsibilities
35
36
Project maintainers are responsible for clarifying the standards of acceptable
37
behavior and are expected to take appropriate and fair corrective action in
38
response to any instances of unacceptable behavior.
39
40
Project maintainers have the right and responsibility to remove, edit, or
41
reject comments, commits, code, wiki edits, issues, and other contributions
42
that are not aligned to this Code of Conduct, or to ban temporarily or
43
permanently any contributor for other behaviors that they deem inappropriate,
44
threatening, offensive, or harmful.
45
46
## Scope
47
48
This Code of Conduct applies both within project spaces and in public spaces
49
when an individual is representing the project or its community. Examples of
50
representing a project or community include using an official project e-mail
51
address, posting via an official social media account, or acting as an appointed
52
representative at an online or offline event. Representation of a project may be
53
further defined and clarified by project maintainers.
54
55
## Enforcement
56
57
Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
reported by contacting the project team at Dave.Jarvis@gmail.com. All
59
complaints will be reviewed and investigated and will result in a response that
60
is deemed necessary and appropriate to the circumstances. The project team is
61
obligated to maintain confidentiality with regard to the reporter of an incident.
62
Further details of specific enforcement policies may be posted separately.
63
64
Project maintainers who do not follow or enforce the Code of Conduct in good
65
faith may face temporary or permanent repercussions as determined by other
66
members of the project's leadership.
67
68
## Attribution
69
70
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72
73
[homepage]: https://www.contributor-covenant.org
74
75
For answers to common questions about this code of conduct, see
76
https://www.contributor-covenant.org/faq
177
A LICENSE.md
1
# License
2
3
Copyright 2020 White Magic Software, Ltd.
4
5
Copyright 2015 Karl Tauber
6
7
All rights reserved.
8
9
Redistribution and use in source and binary forms, with or without
10
modification, are permitted provided that the following conditions are met:
11
12
* Redistributions of source code must retain the above copyright
13
  notice, this list of conditions and the following disclaimer.
14
15
* Redistributions in binary form must reproduce the above copyright
16
  notice, this list of conditions and the following disclaimer in the
17
  documentation and/or other materials provided with the distribution.
18
19
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
130
A R/README.md
1
# R Functions
2
3
Import the files in this directory into the application, which include:
4
5
* pluralize.R
6
* possessive.R
7
* conversion.R
8
* csv.R
9
10
# pluralize.R
11
12
This file defines a function that implements most of Damian Conway's [An Algorithmic Approach to English Pluralization](http://blob.perl.org/tpc/1998/User_Applications/Algorithmic%20Approach%20Plurals/Algorithmic_Plurals.html).
13
14
## Usage
15
16
Example usages of the pluralize function include:
17
18
    `r#pluralize( 'mouse' )` - mice
19
    `r#pluralize( 'buzz' )` - buzzes
20
    `r#pluralize( 'bus' )` - busses
21
22
# possessive.R
23
24
This file defines a function that applies possessives to English words.
25
26
## Usage
27
28
Example usages of the possessive function include:
29
30
    `r#pos( 'Ross' )` - Ross'
31
    `r#pos( 'Ruby' )` - Ruby's
32
    `r#pos( 'Lois' )` - Lois'
33
    `r#pos( 'my' )` - mine
34
    `r#pos( 'Your' )` - Yours
35
136
A R/bootstrap.R
1
setwd( '$application.r.working.directory$' )
2
assign( "anchor", '$date.anchor$', envir = .GlobalEnv )
3
4
source( 'pluralize.R' )
5
source( 'possessive.R' )
6
source( 'conversion.R' )
7
source( 'csv.R' )
8
19
A R/conversion.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# Substitute R expressions in a document with their evaluated value. The
26
# anchor variable must be set for functions that use relative dates.
27
# -----------------------------------------------------------------------------
28
29
# -----------------------------------------------------------------------------
30
# Evaluates an expression; writes s if there is no expression.
31
# -----------------------------------------------------------------------------
32
x <- function( s ) {
33
  tryCatch( {
34
    r = eval( parse( text = s ) )
35
36
    # If the result isn't primitive, then it was probably parsed into
37
    # an unprintable object (e.g., "gray" becomes a colour). In those
38
    # cases, return the original text string. Otherwise, an atomic
39
    # value means a primitive type (string, integer, etc.) that can be
40
    # written directly into the document.
41
    ifelse( is.atomic( r ), r, s );
42
  },
43
  warning = function( w ) { s },
44
  error = function( e ) { s } )
45
}
46
47
# -----------------------------------------------------------------------------
48
# Returns a date offset by a given number of days, relative to the given
49
# date (d). This does not use the anchor, but is used to get the anchor's
50
# value as a date.
51
# -----------------------------------------------------------------------------
52
when <- function( d, n = 0, format = "%Y-%m-%d" ) {
53
  as.Date( d, format = format ) + x( n )
54
}
55
56
# -----------------------------------------------------------------------------
57
# Full date (s) offset by an optional number of days before or after.
58
# This will remove leading zeros (applying leading spaces instead, which
59
# are ignored by any worthwhile typesetting engine).
60
# -----------------------------------------------------------------------------
61
annal <- function( days = 0, format = "%Y-%m-%d", oformat = "%B %d, %Y" ) {
62
  format( when( anchor, days ), format = oformat )
63
}
64
65
# -----------------------------------------------------------------------------
66
# Extracts the year from a date string.
67
# -----------------------------------------------------------------------------
68
year <- function( days = 0, format = "%Y-%m-%d" ) {
69
  annal( days, format, "%Y" )
70
}
71
72
# -----------------------------------------------------------------------------
73
# Day of the week (in days since the anchor date).
74
# -----------------------------------------------------------------------------
75
weekday <- function( n ) {
76
  weekdays( when( anchor, n ) )
77
}
78
79
# -----------------------------------------------------------------------------
80
# String concatenate function alias because paste0 is a terrible name.
81
# -----------------------------------------------------------------------------
82
concat <- paste0
83
84
# -----------------------------------------------------------------------------
85
# Translates a number from digits to words using Chicago Manual of Style.
86
# This does not translate numbers greater than one hundred. If ordinal
87
# is TRUE, this will return the ordinal name. This will not produce ordinals
88
# for numbers greater than 100.
89
# -----------------------------------------------------------------------------
90
cms <- function( n, ordinal = FALSE ) {
91
  n <- x( n )
92
93
  if( n == 0 ) {
94
    if( ordinal ) {
95
      return( "zeroth" )
96
    }
97
98
    return( "zero" )
99
  }
100
101
  # Concatenate this a little later.
102
  if( n < 0 ) {
103
    result = "negative "
104
    n = abs( n )
105
  }
106
107
  # Do not spell out numbers greater than one hundred.
108
  if( n > 100 ) {
109
    # Comma-separated numbers.
110
    return( commas( n ) )
111
  }
112
113
  # Don't go beyond 100.
114
  if( n == 100 ) {
115
    if( ordinal ) {
116
      return( "one hundredth" )
117
    }
118
119
    return( "one hundred" )
120
  }
121
122
  # Samuel Langhorne Clemens noted English has too many exceptions.
123
  small = c(
124
    "one", "two", "three", "four", "five",
125
    "six", "seven", "eight", "nine", "ten",
126
    "eleven", "twelve", "thirteen", "fourteen", "fifteen",
127
    "sixteen", "seventeen", "eighteen", "nineteen"
128
  )
129
130
  ord_small = c(
131
    "first", "second", "third", "fourth", "fifth",
132
    "sixth", "seventh", "eighth", "ninth", "tenth",
133
    "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth",
134
    "sixteenth", "seventeenth", "eighteenth", "nineteenth", "twentieth"
135
  )
136
137
  # After this, the number (n) is between 20 and 99.
138
  if( n < 20 ) {
139
    if( ordinal ) {
140
      return( .subset( ord_small, n %% 100 ) )
141
    }
142
143
    return( .subset( small, n %% 100 ) )
144
  }
145
146
  tens = c( "",
147
    "twenty", "thirty", "forty", "fifty",
148
    "sixty", "seventy", "eighty", "ninety"
149
  )
150
151
  ord_tens = c( "",
152
    "twentieth", "thirtieth", "fortieth", "fiftieth",
153
    "sixtieth", "seventieth", "eightieth", "ninetieth"
154
  )
155
156
  ones_index = n %% 10
157
  n = n %/% 10
158
159
  # No number in the ones column, so the number must be a multiple of ten.
160
  if( ones_index == 0 ) {
161
    if( ordinal ) {
162
      return( .subset( ord_tens, n ) )
163
    }
164
165
    return( .subset( tens, n ) )
166
  }
167
168
  # Find the value from the ones column.
169
  if( ordinal ) {
170
    unit_1 = .subset( ord_small, ones_index )
171
  }
172
  else {
173
    unit_1 = .subset( small, ones_index )
174
  }
175
176
  # Find the tens column.
177
  unit_10 = .subset( tens, n )
178
179
  # Hyphenate the tens and the ones together.
180
  concat( unit_10, concat( "-", unit_1 ) )
181
}
182
183
# -----------------------------------------------------------------------------
184
# Returns a number as a comma-delimited string. This is a work-around
185
# until Renjin fixes https://github.com/bedatadriven/renjin/issues/338
186
# -----------------------------------------------------------------------------
187
commas <- function( n ) {
188
  n <- x( n )
189
190
  s <- sprintf( "%03.0f", n %% 1000 )
191
  n <- n %/% 1000
192
193
  while( n > 0 ) {
194
    s <- concat( sprintf( "%03.0f", n %% 1000 ), ',', s )
195
    n <- n %/% 1000
196
  }
197
198
  gsub( '^0*', '', s )
199
}
200
201
# -----------------------------------------------------------------------------
202
# Returns a human-readable string that provides the elapsed time between
203
# two numbers in terms of years, months, and days. If any unit value is zero,
204
# the unit is not included. The words (year, month, day) are pluralized
205
# according to English grammar. The numbers are written out according to
206
# Chicago Manual of Style. This applies the serial comma.
207
#
208
# Both numbers are offsets relative to the anchor date.
209
#
210
# If all unit values are zero, this returns s ("same day" by default).
211
#
212
# If the start date (began) is greater than end date (ended), the dates are
213
# swapped before calculations are performed. This allows any two dates
214
# to be compared and positive unit values are always returned.
215
# -----------------------------------------------------------------------------
216
elapsed <- function( began, ended, s = "same day" ) {
217
  began = when( anchor, began )
218
  ended = when( anchor, ended )
219
220
  # Swap the dates if the end date comes before the start date.
221
  if( as.integer( ended - began ) < 0 ) {
222
    tempd = began
223
    began = ended
224
    ended = tempd
225
  }
226
227
  # Calculate number of elapsed years.
228
  years = length( seq( from = began, to = ended, by = 'year' ) ) - 1
229
230
  # Move the start date up by the number of elapsed years.
231
  if( years > 0 ) {
232
    began = seq( began, length = 2, by = concat( years, " years" ) )[2]
233
    years = pl.numeric( "year", years )
234
  }
235
  else {
236
    # Zero years.
237
    years = ""
238
  }
239
240
  # Calculate number of elapsed months, excluding years.
241
  months = length( seq( from = began, to = ended, by = 'month' ) ) - 1
242
243
  # Move the start date up by the number of elapsed months
244
  if( months > 0 ) {
245
    began = seq( began, length = 2, by = concat( months, " months" ) )[2]
246
    months = pl.numeric( "month", months )
247
  }
248
  else {
249
    # Zero months
250
    months = ""
251
  }
252
253
  # Calculate number of elapsed days, excluding months and years.
254
  days = length( seq( from = began, to = ended, by = 'day' ) ) - 1
255
256
  if( days > 0 ) {
257
    days = pl.numeric( "day", days )
258
  }
259
  else {
260
    # Zero days
261
    days = ""
262
  }
263
264
  if( years <= 0 && months <= 0 && days <= 0 ) {
265
    return( s )
266
  }
267
268
  # Put them all in a vector, then remove the empty values.
269
  s <- c( years, months, days )
270
  s <- s[ s != "" ]
271
272
  r <- paste( s, collapse = ", " )
273
274
  # If all three items are present, replace the last comma with ", and".
275
  if( length( s ) > 2 ) {
276
    return( gsub( "(.*),", "\\1, and", r ) )
277
  }
278
279
  # Does nothing if no commas are present.
280
  gsub( "(.*),", "\\1 and", r )
281
}
282
283
# -----------------------------------------------------------------------------
284
# Returns the number (n) in English followed by the plural or singular
285
# form of the given string (s; resumably a noun), if applicable, according
286
# to English grammar. That is, pl.numeric( "wolf", 5 ) will return
287
# "five wolves".
288
# -----------------------------------------------------------------------------
289
pl.numeric <- function( s, n ) {
290
  concat( cms( n ), concat( " ", pluralize( s, n ) ) )
291
}
292
293
# -----------------------------------------------------------------------------
294
# Pluralize s if n is not equal to 1.
295
# -----------------------------------------------------------------------------
296
pl <- function( s, n=2 ) {
297
  pluralize( s, x( n ) )
298
}
299
300
# -----------------------------------------------------------------------------
301
# Name of the season, starting with an capital letter.
302
# -----------------------------------------------------------------------------
303
season <- function( n, format = "%Y-%m-%d" ) {
304
  WS <- as.Date("2016-12-15", "%Y-%m-%d") # Winter Solstice
305
  SE <- as.Date("2016-03-15", "%Y-%m-%d") # Spring Equinox
306
  SS <- as.Date("2016-06-15", "%Y-%m-%d") # Summer Solstice
307
  AE <- as.Date("2016-09-15", "%Y-%m-%d") # Autumn Equinox
308
309
  d <- when( anchor, n )
310
  d <- as.Date( strftime( d, format="2016-%m-%d" ) )
311
312
  ifelse( d >= WS | d < SE, "Winter",
313
    ifelse( d >= SE & d < SS, "Spring",
314
      ifelse( d >= SS & d < AE, "Summer", "Autumn" )
315
    )
316
  )
317
}
318
319
# -----------------------------------------------------------------------------
320
# Converts the first letter in a string to lowercase
321
# -----------------------------------------------------------------------------
322
lc <- function( s ) {
323
  concat( tolower( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
324
}
325
326
# -----------------------------------------------------------------------------
327
# Converts the entire string to lowercase
328
# -----------------------------------------------------------------------------
329
lower <- tolower
330
331
# -----------------------------------------------------------------------------
332
# Converts the first letter in a string to uppercase
333
# -----------------------------------------------------------------------------
334
uc <- function( s ) {
335
  concat( toupper( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
336
}
337
338
# -----------------------------------------------------------------------------
339
# Returns the number of days between the given dates.
340
# -----------------------------------------------------------------------------
341
days <- function( d1, d2, format = "%Y-%m-%d" ) {
342
  dates = c( d1, d2 )
343
  dt = strptime( dates, format = format )
344
  as.integer( difftime( dates[2], dates[1], units = "days" ) )
345
}
346
347
# -----------------------------------------------------------------------------
348
# Returns the number of years elapsed.
349
# -----------------------------------------------------------------------------
350
years <- function( began, ended ) {
351
  began = when( anchor, began )
352
  ended = when( anchor, ended )
353
354
  # Swap the dates if the end date comes before the start date.
355
  if( as.integer( ended - began ) < 0 ) {
356
    tempd = began
357
    began = ended
358
    ended = tempd
359
  }
360
361
  # Calculate number of elapsed years.
362
  length( seq( from = began, to = ended, by = 'year' ) ) - 1
363
}
364
365
# -----------------------------------------------------------------------------
366
# Full name of the month, starting with a capital letter.
367
# -----------------------------------------------------------------------------
368
month <- function( n ) {
369
  # Faster than month.name[ x( n ) ]
370
  .subset( month.name, x( n ) )
371
}
372
373
# -----------------------------------------------------------------------------
374
# -----------------------------------------------------------------------------
375
money <- function( n ) {
376
  commas( x( n ) )
377
}
378
379
# -----------------------------------------------------------------------------
380
# -----------------------------------------------------------------------------
381
timeline <- function( n ) {
382
  concat( weekday( n ), ", ", annal( n ), " (", season( n ), ")" )
383
}
384
385
# -----------------------------------------------------------------------------
386
# Rounds to the nearest base value (e.g., round to nearest 10).
387
#
388
# @param base The nearest value to round to.
389
# -----------------------------------------------------------------------------
390
round.up <- function( n, base = 5 ) {
391
  base * round( x( n ) / base )
392
}
393
394
# -----------------------------------------------------------------------------
395
# Computes linear distance between two points using Haversine formula.
396
# Although Earth is an oblate spheroid, this will produce results close
397
# enough for most purposes.
398
#
399
# @param lat1/lon1 The source latitude and longitude.
400
# @param lat2/lon2 The destination latitude and longitude.
401
# @param radius The radius of the sphere.
402
#
403
# @return The distance between the two coordinates in meters.
404
# -----------------------------------------------------------------------------
405
haversine <- function( lat1, lon1, lat2, lon2, radius = 6371 ) {
406
  # Convert decimal degrees to radians
407
  lon1 = lon1 * pi / 180
408
  lon2 = lon2 * pi / 180
409
  lat1 = lat1 * pi / 180
410
  lat2 = lat2 * pi / 180
411
412
  # Haversine formula
413
  dlon = lon2 - lon1
414
  dlat = lat2 - lat1
415
  a = sin( dlat / 2 ) ** 2 + cos( lat1 ) * cos( lat2 ) * sin( dlon / 2 ) ** 2
416
  c = 2 * atan2( sqrt( a ), sqrt( 1-a ) )
417
418
  return( radius * c * 1000 )
419
}
420
1421
A R/csv.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# Converts CSV to Markdown.
26
#
27
# Reads a CSV file and converts the contents to a Markdown table. The
28
# file must be in the working directory as specified by setwd.
29
#
30
# @param f The filename to convert.
31
# @param decimals Rounded decimal places (default 1).
32
# @param totals Include total sums (default TRUE).
33
# @param align Right-align numbers (default TRUE).
34
# -----------------------------------------------------------------------------
35
csv2md <- function( f, decimals = 2, totals = T, align = T ) {
36
  # Read the CVS data from the file; ensure strings become characters.
37
  df <- read.table( f, sep=',', header=T, stringsAsFactors=F )
38
39
  if( totals ) {
40
    # Determine what columns can be summed.
41
    number <- which( unlist( lapply( df, is.numeric ) ) )
42
43
    # Use colSums when more than one summable column exists.
44
    if( length( number ) > 1 ) {
45
      f.sum <- colSums
46
    }
47
    else {
48
      f.sum <- sum
49
    }
50
51
    # Calculate the sum of all the summable columns and insert the
52
    # results back into the data frame.
53
    df[ (nrow( df ) + 1), number ] <- f.sum( df[, number], na.rm=TRUE )
54
55
    # pluralise would be heavyweight here.
56
    if( length( number ) > 1 ) {
57
      t <- "**Totals**"
58
    }
59
    else {
60
      t <- "**Total**"
61
    }
62
63
    # Change the first column of the last line to "Total(s)".
64
    df[ nrow( df ), 1 ] <- t
65
66
    # Don't clutter the output with "NA" text.
67
    df[ is.na( df ) ] <- ""
68
  }
69
70
  if( align ) {
71
    is.char <- vapply( df, is.character, logical( 1 ) )
72
    dashes <- paste( ifelse( is.char, ':---', '---:' ), collapse='|' )
73
  }
74
  else {
75
    dashes <- paste( rep( '---', length( df ) ), collapse = '|' )
76
  }
77
78
  # Create a Markdown version of the data frame.
79
  paste(
80
    paste( names( df ), collapse = '|'), '\n',
81
    dashes, '\n', 
82
    paste(
83
      Reduce( function( x, y ) {
84
          paste( x, format( y, digits = decimals ), sep = '|' )
85
        }, df
86
      ),
87
      collapse = '|\n', sep=''
88
    )
89
  )
90
}
91
192
A R/pluralize.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# See Damian Conway's "An Algorithmic Approach to English Pluralization":
26
#   http://goo.gl/oRL4MP
27
# See Oliver Glerke's Evo Inflector: https://github.com/atteo/evo-inflector/
28
# See Shevek's Pluralizer: https://github.com/shevek/linguistics/
29
# See also: http://www.freevectors.net/assets/files/plural.txt
30
# -----------------------------------------------------------------------------
31
pluralize <- function( s, n ) {
32
  result <- s
33
34
  # Partial implementation of Conway's algorithm for nouns.
35
  if( n != 1 ) {
36
    if( pl.noninflective( s ) ||
37
        pl.suffix( "es", s ) ||
38
        pl.suffix( "fish", s ) ||
39
        pl.suffix( "ois", s ) ||
40
        pl.suffix( "sheep", s ) ||
41
        pl.suffix( "deer", s ) ||
42
        pl.suffix( "pox", s ) ||
43
        pl.suffix( "[A-Z].*ese", s ) ||
44
        pl.suffix( "itis", s ) ) {
45
      # 1. Retain non-inflective user-mapped noun as is.
46
      # 2. Retain non-inflective plural as is.
47
      result <- s
48
    }
49
    else if( pl.is.irregular.pl( s ) ) {
50
      # 4. Change irregular plurals based on mapping.
51
      result <- pl.irregular.pl( s )
52
    }
53
    else if( pl.is.irregular.es( s ) ) {
54
      # x. From Shevek's
55
      result <- pl.inflect( s, "", "es" )
56
    }
57
    else if( pl.suffix( "man", s ) ) {
58
      # 5. For -man, change -an to -en
59
      result <- pl.inflect( s, "an", "en" )
60
    }
61
    else if( pl.suffix( "[lm]ouse", s ) ) {
62
      # 5. For [lm]ouse, change -ouse to -ice
63
      result <- pl.inflect( s, "ouse", "ice" )
64
    }
65
    else if( pl.suffix( "tooth", s ) ) {
66
      # 5. For -tooth, change -ooth to -eeth
67
      result <- pl.inflect( s, "ooth", "eeth" )
68
    }
69
    else if( pl.suffix( "goose", s ) ) {
70
      # 5. For -goose, change -oose to -eese
71
      result <- pl.inflect( s, "oose", "eese" )
72
    }
73
    else if( pl.suffix( "foot", s ) ) {
74
      # 5. For -foot, change -oot to -eet
75
      result <- pl.inflect( s, "oot", "eet" )
76
    }
77
    else if( pl.suffix( "zoon", s ) ) {
78
      # 5. For -zoon, change -on to -a
79
      result <- pl.inflect( s, "on", "a" )
80
    }
81
    else if( pl.suffix( "[csx]is", s ) ) {
82
      # 5. Change -cis, -sis, -xis to -es
83
      result <- pl.inflect( s, "is", "es" )
84
    }
85
    else if( pl.suffix( "([cs]h|ss|zz|x|s)", s ) ) {
86
      # 8. Change -ch, -sh, -ss, -zz, -x, -s to -es
87
      result <- pl.inflect( s, "", "es" )
88
    }
89
    else if( pl.suffix( "([aeo]lf|[^d]eaf|arf)", s ) ) {
90
      # 9. Change -f to -ves
91
      result <- pl.inflect( s, "f", "ves" )
92
    }
93
    else if( pl.suffix( "[nlw]ife", s ) ) {
94
      # 10. Change -fe to -ves
95
      result <- pl.inflect( s, "fe", "ves" )
96
    }
97
    else if( pl.suffix( "[aeiou]y", s ) ) {
98
      # 11. Change -[aeiou]y to -ys
99
      result <- pl.inflect( s, "", "s" )
100
    }
101
    else if( pl.suffix( "y", s ) ) {
102
      # 12. Change -y to -ies
103
      result <- pl.inflect( s, "y", "ies" )
104
    }
105
    else if( pl.suffix( "z", s ) ) {
106
      # x. Change -z to -zzes
107
      result <- pl.inflect( s, "", "zes" )
108
    }
109
    else {
110
      # 13. Default plural: add -s
111
      result <- pl.inflect( s, "", "s" )
112
    }
113
  }
114
115
  result
116
}
117
118
# -----------------------------------------------------------------------------
119
# Returns the given string (s) with its suffix replaced by r.
120
# -----------------------------------------------------------------------------
121
pl.inflect <- function( s, suffix, r ) {
122
  gsub( paste( suffix, "$", sep="" ), r, s )
123
}
124
125
# -----------------------------------------------------------------------------
126
# Answers whether the given string (s) has the given ending.
127
# -----------------------------------------------------------------------------
128
pl.suffix <- function( ending, s ) {
129
  grepl( paste( ending, "$", sep="" ), s )
130
}
131
132
# -----------------------------------------------------------------------------
133
# Answers whether the given string (s) is a noninflective noun.
134
# -----------------------------------------------------------------------------
135
pl.noninflective <- function( s ) {
136
  v <- c(
137
    "aircraft",
138
    "Bhutanese",
139
    "bison",
140
    "bream",
141
    "Burmese",
142
    "carp",
143
    "chassis",
144
    "Chinese",
145
    "clippers",
146
    "cod",
147
    "contretemps",
148
    "corps",
149
    "debris",
150
    "djinn",
151
    "eland",
152
    "elk",
153
    "flounder",
154
    "fracas",
155
    "gallows",
156
    "graffiti",
157
    "headquarters",
158
    "high-jinks",
159
    "homework",
160
    "hovercraft",
161
    "innings",
162
    "Japanese",
163
    "Lebanese",
164
    "mackerel",
165
    "means",
166
    "mews",
167
    "mice",
168
    "mumps",
169
    "news",
170
    "pincers",
171
    "pliers",
172
    "Portuguese",
173
    "proceedings",
174
    "salmon",
175
    "scissors",
176
    "sea-bass",
177
    "Senegalese",
178
    "shears",
179
    "Siamese",
180
    "Sinhalese",
181
    "spacecraft",
182
    "swine",
183
    "trout",
184
    "tuna",
185
    "Vietnamese",
186
    "watercraft",
187
    "whiting",
188
    "wildebeest"
189
  )
190
191
  is.element( s, v )
192
}
193
194
# -----------------------------------------------------------------------------
195
# Answers whether the given string (s) is an irregular plural.
196
# -----------------------------------------------------------------------------
197
pl.is.irregular.pl <- function( s ) {
198
  # Could be refactored with pl.irregular.pl...
199
  v <- c(
200
    "beef", "brother", "child", "cow", "ephemeris", "genie", "money",
201
    "mongoose", "mythos", "octopus", "ox", "soliloquy", "trilby"
202
  )
203
204
  is.element( s, v )
205
}
206
207
# -----------------------------------------------------------------------------
208
# Call to pluralize an irregular noun. Only call after confirming
209
# the noun is irregular via pl.is.irregular.pl.
210
# -----------------------------------------------------------------------------
211
pl.irregular.pl <- function( s ) {
212
  v <- list(
213
    "beef" = "beefs",
214
    "brother" = "brothers",
215
    "child" = "children",
216
    "cow" = "cows",
217
    "ephemeris" = "ephemerides",
218
    "genie" = "genies",
219
    "money" = "moneys",
220
    "mongoose" = "mongooses",
221
    "mythos" = "mythoi",
222
    "octopus" = "octopuses",
223
    "ox" = "oxen",
224
    "soliloquy" = "soliloquies",
225
    "trilby" = "trilbys"
226
  )
227
228
  # Faster version of v[[ s ]]
229
  .subset2( v, s )
230
}
231
232
# -----------------------------------------------------------------------------
233
# Answers whether the given string (s) pluralizes with -es.
234
# -----------------------------------------------------------------------------
235
pl.is.irregular.es <- function( s ) {
236
  v <- c(
237
    "acropolis", "aegis", "alias", "asbestos", "bathos", "bias", "bronchitis",
238
    "bursitis", "caddis", "cannabis", "canvas", "chaos", "cosmos", "dais",
239
    "digitalis", "epidermis", "ethos", "eyas", "gas", "glottis", "hubris",
240
    "ibis", "lens", "mantis", "marquis", "metropolis", "pathos", "pelvis",
241
    "polis", "rhinoceros", "sassafrass", "trellis"
242
  )
243
244
  is.element( s, v )
245
}
246
1247
A R/possessive.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# Returns leftmost n characters of s.
26
# -----------------------------------------------------------------------------
27
lstr <- function( s, n = 1 ) {
28
  substr( s, 0, n )
29
}
30
31
# -----------------------------------------------------------------------------
32
# Returns rightmost n characters of s.
33
# -----------------------------------------------------------------------------
34
rstr <- function( s, n = 1 ) {
35
  l <- nchar( s )
36
  substr( s, l - n + 1, l )
37
}
38
39
# -----------------------------------------------------------------------------
40
# Returns the possessive form of the given word, s.
41
# -----------------------------------------------------------------------------
42
pos <- function( s ) {
43
  lcs <- tolower( s )
44
  pronouns <- c( 'your', 'our', 'her', 'it', 'their' )
45
46
  if( lcs == 'my' ) {
47
    # Change "[Mm]y" to "[Mm]ine".
48
    s <- paste0( lstr( s, 1 ), "ine" )
49
  }
50
  else if( lcs %in% pronouns ) {
51
    # Append an s to most pronouns.
52
    s <- paste0( s, 's' )
53
  }
54
  else if( lcs != 'his' ) {
55
    # Possessive for all other words except 'his'.
56
    s <- paste0( s, ifelse( rstr( s, 1 ) == 's', "'" ,"'s" ) )
57
  }
58
59
  s
60
}
61
162
A README.md
1
# ![Logo](docs/images/app-title.png)
2
3
A text editor that uses [interpolated strings](https://en.wikipedia.org/wiki/String_interpolation) to reference externally defined values.
4
5
## Download
6
7
Download one of the following editions:
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
## Run
14
15
Note that the first time the application runs, it will unpack itself into a local directory. Subsequent starts will be faster.
16
17
### Windows
18
19
Double-click the application to start; give the application permission to run.
20
21
When upgrading to a new version, delete the following directory:
22
23
    C:\Users\%USERNAME%\AppData\Local\warp\packages\keenwrite.exe
24
25
### Linux
26
27
Execute the following commands in a terminal:
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
## Features
43
44
The application offers:
45
46
* User-defined interpolated strings
47
* Auto-complete variable names based on variable values
48
* Real-time spell check
49
* Real-time rendering of math using TeX notation
50
* Diagrams: Mermaid, GraphViz, UML, sequence, timing, DITAA, and more!
51
* R integration
52
* XML transformation using XSLT3 or older
53
* Customizable GUI having detachable tabs
54
* Platform independent (Windows, Linux, MacOS)
55
56
## Usage
57
58
See the [detailed documentation](docs/README.md) for information about
59
using the application.
60
61
## Screenshot
62
63
![Screenshot with Formulas](docs/images/equations.png)
64
65
## License
66
67
This software is licensed under the [BSD 2-Clause License](LICENSE.md) and
68
based on [Markdown-Writer-FX](licenses/MARKDOWN-WRITER-FX.md).
69
170
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
A build-template
1
#!/usr/bin/env bash
2
3
# -----------------------------------------------------------------------------
4
# Copyright 2020 Dave Jarvis
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining a
7
# copy of this software and associated documentation files (the
8
# "Software"), to deal in the Software without restriction, including
9
# without limitation the rights to use, copy, modify, merge, publish,
10
# distribute, sublicense, and/or sell copies of the Software, and to
11
# permit persons to whom the Software is furnished to do so, subject to
12
# the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included
15
# in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
18
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
# -----------------------------------------------------------------------------
25
26
set -o errexit
27
set -o nounset
28
29
readonly SCRIPT_SRC="$(dirname "${BASH_SOURCE[${#BASH_SOURCE[@]} - 1]}")"
30
readonly SCRIPT_DIR="$(cd "${SCRIPT_SRC}" >/dev/null 2>&1 && pwd)"
31
readonly SCRIPT_NAME=$(basename "$0")
32
33
# -----------------------------------------------------------------------------
34
# The main entry point is responsible for parsing command-line arguments,
35
# changing to the appropriate directory, and running all commands requested
36
# by the user.
37
#
38
# $@ - Command-line arguments
39
# -----------------------------------------------------------------------------
40
main() {
41
  arguments "$@"
42
43
  $usage       && terminate 3
44
  requirements && terminate 4
45
  traps        && terminate 5
46
47
  directory    && terminate 6
48
  preprocess   && terminate 7
49
  execute      && terminate 8
50
  postprocess  && terminate 9
51
52
  terminate 0
53
}
54
55
# -----------------------------------------------------------------------------
56
# Perform all commands that the script requires.
57
#
58
# @return 0 - Indicate to terminate the script with non-zero exit level
59
# @return 1 - All tasks completed successfully (default)
60
# -----------------------------------------------------------------------------
61
execute() {
62
  return 1
63
}
64
65
# -----------------------------------------------------------------------------
66
# Changes to the script's working directory, provided it exists.
67
#
68
# @return 0 - Change directory failed
69
# @return 1 - Change directory succeeded
70
# -----------------------------------------------------------------------------
71
directory() {
72
  $log "Change directory"
73
  local result=1
74
75
  # Track whether change directory failed.
76
  cd "${SCRIPT_DIR}" > /dev/null 2>&1 || result=0
77
78
  return "${result}"
79
}
80
81
# -----------------------------------------------------------------------------
82
# Perform any initialization required prior to executing tasks.
83
#
84
# @return 0 - Preprocessing failed
85
# @return 1 - Preprocessing succeeded
86
# -----------------------------------------------------------------------------
87
preprocess() {
88
  $log "Preprocess"
89
90
  return 1
91
}
92
93
# -----------------------------------------------------------------------------
94
# Perform any clean up required prior to executing tasks.
95
#
96
# @return 0 - Postprocessing failed
97
# @return 1 - Postprocessing succeeded
98
# -----------------------------------------------------------------------------
99
postprocess() {
100
  $log "Postprocess"
101
102
  return 1
103
}
104
105
# -----------------------------------------------------------------------------
106
# Check that all required commands are available.
107
#
108
# @return 0 - At least one command is missing
109
# @return 1 - All commands are available
110
# -----------------------------------------------------------------------------
111
requirements() {
112
  $log "Verify requirements"
113
  local -r expected_count=${#DEPENDENCIES[@]}
114
  local total_count=0
115
116
  # Verify that each command exists.
117
  for dependency in "${DEPENDENCIES[@]}"; do
118
    # Extract the command name [0] and URL [1].
119
    IFS=',' read -ra dependent <<< "${dependency}"
120
121
    required "${dependent[0]}" "${dependent[1]}"
122
    total_count=$(( total_count + $? ))
123
  done
124
125
  unset IFS
126
127
  # Total dependencies found must match the expected number.
128
  # Integer-only division rounds down.
129
  return $(( total_count / expected_count ))
130
}
131
132
# -----------------------------------------------------------------------------
133
# Called before terminating the script.
134
# -----------------------------------------------------------------------------
135
cleanup() {
136
  $log "Cleanup"
137
}
138
139
# -----------------------------------------------------------------------------
140
# Terminates the program immediately.
141
# -----------------------------------------------------------------------------
142
trap_control_c() {
143
  $log "Interrupted"
144
  cleanup
145
  error "⯃"
146
  terminate 1
147
}
148
149
# -----------------------------------------------------------------------------
150
# Configure signal traps.
151
#
152
# @return 1 - Signal traps are set.
153
# -----------------------------------------------------------------------------
154
traps() {
155
  # Suppress echoing ^C if pressed.
156
  stty -echoctl
157
  trap trap_control_c INT
158
159
  return 1
160
}
161
162
# -----------------------------------------------------------------------------
163
# Check for a required command.
164
#
165
# $1 - Command or file to check for existence
166
# $2 - Command's website (e.g., download for binaries and source code)
167
#
168
# @return 0 - Command is missing
169
# @return 1 - Command exists
170
# -----------------------------------------------------------------------------
171
required() {
172
  local result=0
173
174
  test -f "$1" || \
175
  command -v "$1" > /dev/null 2>&1 && result=1 || \
176
    warning "Missing: $1 ($2)"
177
178
  return ${result}
179
}
180
181
# -----------------------------------------------------------------------------
182
# Show acceptable command-line arguments.
183
#
184
# @return 0 - Indicate script may not continue
185
# -----------------------------------------------------------------------------
186
utile_usage() {
187
  printf "Usage: %s [OPTIONS...]\n\n" "${SCRIPT_NAME}" >&2
188
189
  # Number of spaces to pad after the longest long argument.
190
  local -r PADDING=2
191
192
  # Determine the longest long argument to adjust spacing.
193
  local -r LEN=$(printf '%s\n' "${ARGUMENTS[@]}" | \
194
    awk -F"," '{print length($2)+'${PADDING}'}' | sort -n | tail -1)
195
196
  local duplicates
197
198
  for argument in "${ARGUMENTS[@]}"; do
199
    # Extract the short [0] and long [1] arguments and description [2].
200
    arg=("$(echo ${argument} | cut -d ',' -f1)" \
201
         "$(echo ${argument} | cut -d ',' -f2)" \
202
         "$(echo ${argument} | cut -d ',' -f3-)")
203
204
    duplicates+=("${arg[0]}")
205
206
    printf "  -%s, --%-${LEN}s%s\n" "${arg[0]}" "${arg[1]}" "${arg[2]}" >&2
207
  done
208
209
  # Sort the arguments to make sure no duplicates exist.
210
  duplicates=$(echo "${duplicates[@]}" | tr ' ' '\n' | sort | uniq -c -d)
211
212
  # Warn the developer that there's a duplicate command-line option.
213
  if [ -n "${duplicates}" ]; then
214
    # Trim all the whitespaces
215
    duplicates=$(echo "${duplicates}" | xargs echo -n)
216
    error "Duplicate command-line argument exists: ${duplicates}"
217
  fi
218
219
  return 0
220
}
221
222
# -----------------------------------------------------------------------------
223
# Write coloured text to standard output.
224
#
225
# $1 - Text to write
226
# $2 - Text's colour
227
# -----------------------------------------------------------------------------
228
coloured_text() {
229
  printf "%b%s%b\n" "$2" "$1" "${COLOUR_OFF}"
230
}
231
232
# -----------------------------------------------------------------------------
233
# Write a warning message to standard output.
234
#
235
# $1 - Text to write
236
# -----------------------------------------------------------------------------
237
warning() {
238
  coloured_text "$1" "${COLOUR_WARNING}"
239
}
240
241
# -----------------------------------------------------------------------------
242
# Write an error message to standard output.
243
#
244
# $1 - Text to write
245
# -----------------------------------------------------------------------------
246
error() {
247
  coloured_text "$1" "${COLOUR_ERROR}"
248
}
249
250
# -----------------------------------------------------------------------------
251
# Write a timestamp and message to standard output.
252
#
253
# $1 - Text to write
254
# -----------------------------------------------------------------------------
255
utile_log() {
256
  printf "[%s] " "$(date +%H:%M:%S.%4N)"
257
  coloured_text "$1" "${COLOUR_LOGGING}"
258
}
259
260
# -----------------------------------------------------------------------------
261
# Perform no operations.
262
#
263
# return 1 - Success
264
# -----------------------------------------------------------------------------
265
noop() {
266
  return 1
267
}
268
269
# -----------------------------------------------------------------------------
270
# Exit the program with a given exit code.
271
#
272
# $1 - Exit code
273
# -----------------------------------------------------------------------------
274
terminate() {
275
  exit "$1"
276
}
277
278
# -----------------------------------------------------------------------------
279
# Set global variables from command-line arguments.
280
# -----------------------------------------------------------------------------
281
arguments() {
282
  while [ "$#" -gt "0" ]; do
283
    local consume=1
284
285
    case "$1" in
286
      -V|--verbose)
287
        log=utile_log
288
      ;;
289
      -h|-\?|--help)
290
        usage=utile_usage
291
      ;;
292
      *)
293
        set +e
294
        argument "$@"
295
        consume=$?
296
        set -e
297
      ;;
298
    esac
299
300
    shift ${consume}
301
  done
302
}
303
304
# -----------------------------------------------------------------------------
305
# Parses a single command-line argument. This must return a value greater
306
# than or equal to 1, otherwise parsing the command-line arguments will
307
# loop indefinitely.
308
#
309
# @return The number of arguments to consume (1 by default).
310
# -----------------------------------------------------------------------------
311
argument() {
312
  return 1
313
}
314
315
# ANSI colour escape sequences.
316
readonly COLOUR_BLUE='\033[1;34m'
317
readonly COLOUR_PINK='\033[1;35m'
318
readonly COLOUR_DKGRAY='\033[30m'
319
readonly COLOUR_DKRED='\033[31m'
320
readonly COLOUR_LTRED='\033[1;31m'
321
readonly COLOUR_YELLOW='\033[1;33m'
322
readonly COLOUR_OFF='\033[0m'
323
324
# Colour definitions used by script.
325
COLOUR_LOGGING=${COLOUR_BLUE}
326
COLOUR_WARNING=${COLOUR_YELLOW}
327
COLOUR_ERROR=${COLOUR_LTRED}
328
329
# Define required commands to check when script starts.
330
DEPENDENCIES=(
331
  "awk,https://www.gnu.org/software/gawk/manual/gawk.html"
332
  "cut,https://www.gnu.org/software/coreutils"
333
)
334
335
# Define help for command-line arguments.
336
ARGUMENTS=(
337
  "V,verbose,Log messages while processing"
338
  "h,help,Show this help message then exit"
339
)
340
341
# These functions may be set to utile delegates while parsing arguments.
342
usage=noop
343
log=noop
344
1345
A build.gradle
1
plugins {
2
  id 'application'
3
  id 'org.openjfx.javafxplugin' version '0.0.9'
4
  id 'com.palantir.git-version' version '0.12.3'
5
}
6
7
repositories {
8
  mavenCentral()
9
  jcenter()
10
11
  maven {
12
    url 'https://oss.sonatype.org/content/repositories/snapshots/'
13
  }
14
15
  maven {
16
    url "https://nexus.bedatadriven.com/content/groups/public"
17
  }
18
}
19
20
// Assume a cross-platform überjar unless targetOs is set.
21
String[] os = ["win", "mac", "linux"]
22
23
if (project.hasProperty('targetOs')) {
24
  if ("windows" == targetOs) {
25
    os = ["win"]
26
  } else {
27
    os = [targetOs]
28
  }
29
}
30
31
javafx {
32
  version = "15"
33
  modules = ['javafx.controls', 'javafx.swing']
34
  configuration = 'compileOnly'
35
}
36
37
dependencies {
38
  def v_junit = '5.5.1'
39
  def v_flexmark = '0.62.2'
40
  def v_jackson = '2.12.0'
41
  def v_batik = '1.13'
42
43
  // JavaFX
44
  implementation 'org.controlsfx:controlsfx:11.0.3'
45
  implementation 'org.fxmisc.richtext:richtextfx:0.10.5'
46
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
47
  implementation 'com.miglayout:miglayout-javafx:5.2'
48
  implementation('com.dlsc.preferencesfx:preferencesfx-core:11.7.0') {
49
    exclude group: 'org.openjfx'
50
  }
51
  implementation('de.jensd:fontawesomefx-commons:11.0') {
52
    exclude group: 'org.openjfx'
53
  }
54
  implementation('de.jensd:fontawesomefx-fontawesome:4.7.0-11') {
55
    exclude group: 'org.openjfx'
56
  }
57
58
  // Markdown
59
  implementation "com.vladsch.flexmark:flexmark:${v_flexmark}"
60
  implementation "com.vladsch.flexmark:flexmark-ext-definition:${v_flexmark}"
61
  implementation "com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:${v_flexmark}"
62
  implementation "com.vladsch.flexmark:flexmark-ext-superscript:${v_flexmark}"
63
  implementation "com.vladsch.flexmark:flexmark-ext-tables:${v_flexmark}"
64
  implementation "com.vladsch.flexmark:flexmark-ext-typographic:${v_flexmark}"
65
66
  // YAML
67
  implementation "com.fasterxml.jackson.core:jackson-core:${v_jackson}"
68
  implementation "com.fasterxml.jackson.core:jackson-databind:${v_jackson}"
69
  implementation "com.fasterxml.jackson.core:jackson-annotations:${v_jackson}"
70
  implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${v_jackson}"
71
  implementation 'org.yaml:snakeyaml:1.27'
72
73
  // XML and XSL
74
  implementation 'com.ximpleware:vtd-xml:2.13.4'
75
  implementation 'net.sf.saxon:Saxon-HE:10.1'
76
  implementation 'xalan:xalan:2.7.2'
77
78
  // HTML parsing and rendering
79
  implementation 'org.jsoup:jsoup:1.13.1'
80
  implementation 'org.xhtmlrenderer:flying-saucer-core:9.1.20'
81
82
  // R
83
  implementation 'org.renjin:renjin-script-engine:3.5-beta76'
84
85
  // SVG
86
  implementation "org.apache.xmlgraphics:batik-anim:${v_batik}"
87
  implementation "org.apache.xmlgraphics:batik-awt-util:${v_batik}"
88
  implementation "org.apache.xmlgraphics:batik-bridge:${v_batik}"
89
  implementation "org.apache.xmlgraphics:batik-css:${v_batik}"
90
  implementation "org.apache.xmlgraphics:batik-dom:${v_batik}"
91
  implementation "org.apache.xmlgraphics:batik-ext:${v_batik}"
92
  implementation "org.apache.xmlgraphics:batik-gvt:${v_batik}"
93
  implementation "org.apache.xmlgraphics:batik-parser:${v_batik}"
94
  implementation "org.apache.xmlgraphics:batik-script:${v_batik}"
95
  implementation "org.apache.xmlgraphics:batik-svg-dom:${v_batik}"
96
  implementation "org.apache.xmlgraphics:batik-svggen:${v_batik}"
97
  implementation "org.apache.xmlgraphics:batik-transcoder:${v_batik}"
98
  implementation "org.apache.xmlgraphics:batik-util:${v_batik}"
99
  implementation "org.apache.xmlgraphics:batik-xml:${v_batik}"
100
101
  // Misc.
102
  implementation 'org.ahocorasick:ahocorasick:0.4.0'
103
  implementation 'org.apache.commons:commons-configuration2:2.7'
104
  implementation 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
105
  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')
113
114
  def fx = ['controls', 'graphics', 'fxml', 'swing']
115
116
  fx.each { fxitem ->
117
    os.each { ositem ->
118
      println "org.openjfx:javafx-${fxitem}:${javafx.version}:${ositem}"
119
120
      runtimeOnly "org.openjfx:javafx-${fxitem}:${javafx.version}:${ositem}"
121
    }
122
  }
123
124
  testImplementation "org.junit.jupiter:junit-jupiter-engine:${v_junit}"
125
  testImplementation "org.junit.jupiter:junit-jupiter-api:${v_junit}"
126
  testImplementation "org.testfx:testfx-junit5:4.0.16-alpha"
127
}
128
129
compileJava {
130
  options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
131
}
132
133
def resourceDir = sourceSets.main.resources.srcDirs[0]
134
135
def config = new Properties()
136
file("${resourceDir}/bootstrap.properties").withInputStream {
137
  config.load(it)
138
}
139
140
application {
141
  applicationName = config["application.title"].toLowerCase()
142
  mainClassName = "com.${applicationName}.Main"
143
144
  applicationDefaultJvmArgs = [
145
      "--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED",
146
      "--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED",
147
      "--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED",
148
  ]
149
}
150
151
version = gitVersion()
152
153
def launcherClassName = "com.${applicationName}.Launcher"
154
155
def propertiesFile = new File("${resourceDir}/com/${applicationName}/app.properties")
156
propertiesFile.write("application.version=${version}")
157
158
jar {
159
  duplicatesStrategy = DuplicatesStrategy.EXCLUDE
160
161
  manifest {
162
    attributes 'Main-Class': launcherClassName
163
  }
164
165
  from {
166
    (configurations.runtimeClasspath.findAll { !it.path.endsWith(".pom") }).collect {
167
      it.isDirectory() ? it : zipTree(it)
168
    }
169
  }
170
171
  archiveFileName = "${applicationName}.jar"
172
173
  exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
174
}
175
176
distributions {
177
  main {
178
    distributionBaseName = applicationName
179
    contents {
180
      from { ['LICENSE.md', 'README.md'] }
181
      into('images') {
182
        from { 'images' }
183
      }
184
    }
185
  }
186
}
187
188
test {
189
  useJUnitPlatform()
190
}
1191
A docs/README.md
1
## Documents
2
3
See the following documents for more information:
4
5
* [definitions.md](definitions.md) -- Definitions and interpolation
6
* [r.md](r.md) -- Call R functions within R Markdown documents
7
* [texample.Rmd](texample.Rmd) -- Numerous examples of formulas
8
* [svg.md](svg.md) -- Fix known issues with displaying SVG files
9
* [credits.md](credits.md) -- Thanks to authors of contributing projects
10
111
A docs/credits.md
1
# Credits
2
3
* Karl Tauber: [Markdown Writer FX](https://github.com/JFormDesigner/markdown-writer-fx)
4
* Tomas Mikula: [RichTextFX](https://github.com/TomasMikula/RichTextFX), [ReactFX](https://github.com/TomasMikula/ReactFX), [WellBehavedFX](https://github.com/TomasMikula/WellBehavedFX), [Flowless](https://github.com/TomasMikula/Flowless), and [UndoFX](https://github.com/TomasMikula/UndoFX)
5
* Mikael Grev: [MigLayout](http://www.miglayout.com/)
6
* Tom Eugelink: [MigPane](https://github.com/mikaelgrev/miglayout/blob/master/javafx/src/main/java/org/tbee/javafx/scene/layout/fxml/MigPane.java)
7
* Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx)
8
* Dieter Holz, [PreferencesFX](https://github.com/dlsc-software-consulting-gmbh/PreferencesFX)
9
* David Croft, [File Preferences](http://www.davidc.net/programming/java/java-preferences-using-file-backing-store)
10
* Alex Bertram, [Renjin](https://www.renjin.org/)
11
* Vladimir Schneider: [flexmark](https://github.com/vsch/flexmark-java)
12
* Michael Kay, [XSLT Processor](http://www.saxonica.com/)
13
* Shy Shalom, Kohei Taketa: [juniversalchardet](https://github.com/takscape/juniversalchardet)
14
115
A docs/definitions.md
1
# Introduction
2
3
This document describes how to use the application.
4
5
# Variable definitions
6
7
Variable definitions provide a way to insert key names having associated values into a document. The variable names and values are declared inside an external file using the [YAML](http://www.yaml.org/) file format. Simply put, variables are written in the file as follows:
8
9
```
10
key: value
11
```
12
13
Any number of variables can be defined, in any order:
14
15
```
16
key_1: Value 1
17
key_2: Value 2
18
```
19
20
Variables can reference other variables by bookending the key name within symbols:
21
22
```
23
key: Value
24
key_1: {{key}} 1
25
key_2: {{key}} 2
26
```
27
28
Variables can use a nested structure to help group related information:
29
30
```
31
novel:
32
  title: Book Title
33
  author: Author Name
34
  isbn: 978-3-16-148410-0
35
```
36
37
Use a period to reference nested keys, such as:
38
39
```
40
novel:
41
  author: Author Name
42
copyright:
43
  owner: {{novel.author}}
44
```
45
46
Save the variable definitions in a file having an extension of `.yaml` or `.yml`.
47
48
# Document editing
49
50
The application's purpose is to completely separate the document's content from its presentation. To achieve this, documents are composed using a [plain text](http://spec.commonmark.org/0.28/) format.
51
52
## Create document
53
54
Start a new document as follows:
55
56
1. Start the application.
57
1. Click **File → New** to create an empty document to edit.
58
1. Click **File → Open** to open a variable definition file.
59
1. Change **Source Files** to **Definition Files** to list definition files.
60
1. Browse to and select a file saved with a `.yaml` or `.yml` extension.
61
1. Click **Open**.
62
63
The variable definitions appear in the variable definition pane under the heading of **Definitions**.
64
65
## Edit document
66
67
Edit the document as normal. Notice how the preview pane updates as new content is added. The toolbar shows various icons that perform different formatting operations. Try them to see how they appear in the preview pane. Other operations not shown on the toolbar include:
68
69
* Struck text (enclose the words within `~~` and `~~`)
70
* Horizontal rule (use `---` on an otherwise empty line).
71
72
The preview pane shows one way to interpret and format the document, but many other presentations are possible.
73
74
## Insert variable
75
76
Let's assume that the variable definitions loaded into the application include:
77
78
```
79
novel:
80
  title: Diary of {{novel.author}}
81
  author: Anne Frank
82
```
83
84
To reference a variable, type in the key name enclosed within double braces, such as:
85
86
```
87
The novel "{{novel.title}}" is one of the most widely read books in the world.
88
```
89
90
The preview pane shows:
91
92
> The novel "Diary of Anne Frank" is one of the most widely read books in the world.
93
94
As it is laborious to type in variable names, it is possible to inject the variable name using autocomplete. Accomplish this as follows:
95
96
1. Create a new file.
97
1. Type in a partial variable value, such as **Dia**.
98
1. Press `Ctrl+Space` (hold down the `Control` key and tap the spacebar).
99
100
The editor shows:
101
102
```
103
{{novel.title}}
104
```
105
106
The preview pane shows:
107
108
```
109
Diary of Anne Frank
110
```
111
112
The variable name is inserted into the document and the preview pane shows the variable's value.
113
1114
A docs/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
A docs/images/app-title.png
Binary file
A docs/images/app-title.zh-CN.png
Binary file
A docs/images/architecture/architecture.png
Binary file
A docs/images/architecture/architecture.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
   xmlns:cc="http://creativecommons.org/ns#"
5
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
   xmlns:svg="http://www.w3.org/2000/svg"
7
   xmlns="http://www.w3.org/2000/svg"
8
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
   inkscape:export-ydpi="150.0097"
11
   inkscape:export-xdpi="150.0097"
12
   sodipodi:docname="architecture.svg"
13
   viewBox="0 0 764.4414 811.46748"
14
   height="811.46747"
15
   width="764.44141"
16
   id="svg4610"
17
   version="1.2"
18
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)">
19
  <metadata
20
     id="metadata4616">
21
    <rdf:RDF>
22
      <cc:Work
23
         rdf:about="">
24
        <dc:format>image/svg+xml</dc:format>
25
        <dc:type
26
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
27
        <dc:title />
28
      </cc:Work>
29
    </rdf:RDF>
30
  </metadata>
31
  <defs
32
     id="defs4614">
33
    <marker
34
       inkscape:stockid="Arrow1Mend"
35
       orient="auto"
36
       refY="0"
37
       refX="0"
38
       id="marker10933"
39
       style="overflow:visible"
40
       inkscape:isstock="true">
41
      <path
42
         id="path10931"
43
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
44
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
45
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
46
         inkscape:connector-curvature="0" />
47
    </marker>
48
    <marker
49
       inkscape:stockid="Arrow1Mend"
50
       orient="auto"
51
       refY="0"
52
       refX="0"
53
       id="marker9893"
54
       style="overflow:visible"
55
       inkscape:isstock="true">
56
      <path
57
         id="path9891"
58
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
59
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
60
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
61
         inkscape:connector-curvature="0" />
62
    </marker>
63
    <marker
64
       inkscape:collect="always"
65
       inkscape:isstock="true"
66
       style="overflow:visible"
67
       id="marker9767"
68
       refX="0"
69
       refY="0"
70
       orient="auto"
71
       inkscape:stockid="Arrow1Mend">
72
      <path
73
         inkscape:connector-curvature="0"
74
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
75
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
76
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
77
         id="path9765" />
78
    </marker>
79
    <marker
80
       inkscape:collect="always"
81
       inkscape:stockid="Arrow1Mend"
82
       orient="auto"
83
       refY="0"
84
       refX="0"
85
       id="marker9761"
86
       style="overflow:visible"
87
       inkscape:isstock="true">
88
      <path
89
         id="path9759"
90
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
91
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
92
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
93
         inkscape:connector-curvature="0" />
94
    </marker>
95
    <marker
96
       inkscape:isstock="true"
97
       style="overflow:visible"
98
       id="marker9750"
99
       refX="0"
100
       refY="0"
101
       orient="auto"
102
       inkscape:stockid="Arrow1Mend">
103
      <path
104
         inkscape:connector-curvature="0"
105
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
106
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
107
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
108
         id="path9748" />
109
    </marker>
110
    <marker
111
       inkscape:isstock="true"
112
       style="overflow:visible"
113
       id="marker9715"
114
       refX="0"
115
       refY="0"
116
       orient="auto"
117
       inkscape:stockid="Arrow1Mend">
118
      <path
119
         inkscape:connector-curvature="0"
120
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
121
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
122
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
123
         id="path9713" />
124
    </marker>
125
    <marker
126
       inkscape:collect="always"
127
       inkscape:stockid="Arrow1Mend"
128
       orient="auto"
129
       refY="0"
130
       refX="0"
131
       id="marker9685"
132
       style="overflow:visible"
133
       inkscape:isstock="true">
134
      <path
135
         id="path9683"
136
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
137
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
138
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
139
         inkscape:connector-curvature="0" />
140
    </marker>
141
    <marker
142
       inkscape:collect="always"
143
       inkscape:stockid="Arrow1Mend"
144
       orient="auto"
145
       refY="0"
146
       refX="0"
147
       id="marker9679"
148
       style="overflow:visible"
149
       inkscape:isstock="true">
150
      <path
151
         id="path9677"
152
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
153
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
154
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
155
         inkscape:connector-curvature="0" />
156
    </marker>
157
    <marker
158
       inkscape:collect="always"
159
       inkscape:isstock="true"
160
       style="overflow:visible"
161
       id="marker9640"
162
       refX="0"
163
       refY="0"
164
       orient="auto"
165
       inkscape:stockid="Arrow1Mend">
166
      <path
167
         inkscape:connector-curvature="0"
168
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
169
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
170
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
171
         id="path9638" />
172
    </marker>
173
    <marker
174
       inkscape:collect="always"
175
       inkscape:isstock="true"
176
       style="overflow:visible"
177
       id="marker9513"
178
       refX="0"
179
       refY="0"
180
       orient="auto"
181
       inkscape:stockid="Arrow1Mend">
182
      <path
183
         inkscape:connector-curvature="0"
184
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
185
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
186
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
187
         id="path9511" />
188
    </marker>
189
    <marker
190
       inkscape:stockid="Arrow1Mend"
191
       orient="auto"
192
       refY="0"
193
       refX="0"
194
       id="marker9509"
195
       style="overflow:visible"
196
       inkscape:isstock="true">
197
      <path
198
         id="path9507"
199
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
200
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
201
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
202
         inkscape:connector-curvature="0" />
203
    </marker>
204
    <marker
205
       inkscape:isstock="true"
206
       style="overflow:visible"
207
       id="marker9505"
208
       refX="0"
209
       refY="0"
210
       orient="auto"
211
       inkscape:stockid="Arrow1Mend">
212
      <path
213
         inkscape:connector-curvature="0"
214
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
215
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
216
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
217
         id="path9503" />
218
    </marker>
219
    <marker
220
       inkscape:collect="always"
221
       inkscape:stockid="Arrow1Mend"
222
       orient="auto"
223
       refY="0"
224
       refX="0"
225
       id="marker9479"
226
       style="overflow:visible"
227
       inkscape:isstock="true">
228
      <path
229
         id="path9477"
230
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
231
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
232
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
233
         inkscape:connector-curvature="0" />
234
    </marker>
235
    <clipPath
236
       id="ID000001">
237
      <rect
238
         id="rect6"
239
         height="961.125"
240
         width="1381.6169"
241
         y="-43.688"
242
         x="-62.683998" />
243
    </clipPath>
244
    <filter
245
       id="filter2842"
246
       inkscape:label="Drop Shadow"
247
       style="color-interpolation-filters:sRGB;">
248
      <feFlood
249
         id="feFlood2832"
250
         result="flood"
251
         flood-color="rgb(0,0,0)"
252
         flood-opacity="0.498039" />
253
      <feComposite
254
         id="feComposite2834"
255
         result="composite1"
256
         operator="in"
257
         in2="SourceGraphic"
258
         in="flood" />
259
      <feGaussianBlur
260
         id="feGaussianBlur2836"
261
         result="blur"
262
         stdDeviation="2"
263
         in="composite1" />
264
      <feOffset
265
         id="feOffset2838"
266
         result="offset"
267
         dy="3"
268
         dx="3" />
269
      <feComposite
270
         id="feComposite2840"
271
         result="composite2"
272
         operator="over"
273
         in2="offset"
274
         in="SourceGraphic" />
275
    </filter>
276
    <filter
277
       id="filter2854"
278
       inkscape:label="Drop Shadow"
279
       style="color-interpolation-filters:sRGB;">
280
      <feFlood
281
         id="feFlood2844"
282
         result="flood"
283
         flood-color="rgb(0,0,0)"
284
         flood-opacity="0.498039" />
285
      <feComposite
286
         id="feComposite2846"
287
         result="composite1"
288
         operator="in"
289
         in2="SourceGraphic"
290
         in="flood" />
291
      <feGaussianBlur
292
         id="feGaussianBlur2848"
293
         result="blur"
294
         stdDeviation="2"
295
         in="composite1" />
296
      <feOffset
297
         id="feOffset2850"
298
         result="offset"
299
         dy="3"
300
         dx="3" />
301
      <feComposite
302
         id="feComposite2852"
303
         result="composite2"
304
         operator="over"
305
         in2="offset"
306
         in="SourceGraphic" />
307
    </filter>
308
    <filter
309
       id="filter2866"
310
       inkscape:label="Drop Shadow"
311
       style="color-interpolation-filters:sRGB;">
312
      <feFlood
313
         id="feFlood2856"
314
         result="flood"
315
         flood-color="rgb(0,0,0)"
316
         flood-opacity="0.498039" />
317
      <feComposite
318
         id="feComposite2858"
319
         result="composite1"
320
         operator="in"
321
         in2="SourceGraphic"
322
         in="flood" />
323
      <feGaussianBlur
324
         id="feGaussianBlur2860"
325
         result="blur"
326
         stdDeviation="2"
327
         in="composite1" />
328
      <feOffset
329
         id="feOffset2862"
330
         result="offset"
331
         dy="3"
332
         dx="3" />
333
      <feComposite
334
         id="feComposite2864"
335
         result="composite2"
336
         operator="over"
337
         in2="offset"
338
         in="SourceGraphic" />
339
    </filter>
340
    <filter
341
       id="filter2878"
342
       inkscape:label="Drop Shadow"
343
       style="color-interpolation-filters:sRGB;">
344
      <feFlood
345
         id="feFlood2868"
346
         result="flood"
347
         flood-color="rgb(0,0,0)"
348
         flood-opacity="0.498039" />
349
      <feComposite
350
         id="feComposite2870"
351
         result="composite1"
352
         operator="in"
353
         in2="SourceGraphic"
354
         in="flood" />
355
      <feGaussianBlur
356
         id="feGaussianBlur2872"
357
         result="blur"
358
         stdDeviation="2"
359
         in="composite1" />
360
      <feOffset
361
         id="feOffset2874"
362
         result="offset"
363
         dy="3"
364
         dx="3" />
365
      <feComposite
366
         id="feComposite2876"
367
         result="composite2"
368
         operator="over"
369
         in2="offset"
370
         in="SourceGraphic" />
371
    </filter>
372
    <filter
373
       id="filter2890"
374
       inkscape:label="Drop Shadow"
375
       style="color-interpolation-filters:sRGB;">
376
      <feFlood
377
         id="feFlood2880"
378
         result="flood"
379
         flood-color="rgb(0,0,0)"
380
         flood-opacity="0.498039" />
381
      <feComposite
382
         id="feComposite2882"
383
         result="composite1"
384
         operator="in"
385
         in2="SourceGraphic"
386
         in="flood" />
387
      <feGaussianBlur
388
         id="feGaussianBlur2884"
389
         result="blur"
390
         stdDeviation="2"
391
         in="composite1" />
392
      <feOffset
393
         id="feOffset2886"
394
         result="offset"
395
         dy="3"
396
         dx="3" />
397
      <feComposite
398
         id="feComposite2888"
399
         result="composite2"
400
         operator="over"
401
         in2="offset"
402
         in="SourceGraphic" />
403
    </filter>
404
    <filter
405
       id="filter2902"
406
       inkscape:label="Drop Shadow"
407
       style="color-interpolation-filters:sRGB;">
408
      <feFlood
409
         id="feFlood2892"
410
         result="flood"
411
         flood-color="rgb(0,0,0)"
412
         flood-opacity="0.498039" />
413
      <feComposite
414
         id="feComposite2894"
415
         result="composite1"
416
         operator="in"
417
         in2="SourceGraphic"
418
         in="flood" />
419
      <feGaussianBlur
420
         id="feGaussianBlur2896"
421
         result="blur"
422
         stdDeviation="2"
423
         in="composite1" />
424
      <feOffset
425
         id="feOffset2898"
426
         result="offset"
427
         dy="3"
428
         dx="3" />
429
      <feComposite
430
         id="feComposite2900"
431
         result="composite2"
432
         operator="over"
433
         in2="offset"
434
         in="SourceGraphic" />
435
    </filter>
436
    <filter
437
       id="filter2914"
438
       inkscape:label="Drop Shadow"
439
       style="color-interpolation-filters:sRGB;">
440
      <feFlood
441
         id="feFlood2904"
442
         result="flood"
443
         flood-color="rgb(0,0,0)"
444
         flood-opacity="0.498039" />
445
      <feComposite
446
         id="feComposite2906"
447
         result="composite1"
448
         operator="in"
449
         in2="SourceGraphic"
450
         in="flood" />
451
      <feGaussianBlur
452
         id="feGaussianBlur2908"
453
         result="blur"
454
         stdDeviation="2"
455
         in="composite1" />
456
      <feOffset
457
         id="feOffset2910"
458
         result="offset"
459
         dy="3"
460
         dx="3" />
461
      <feComposite
462
         id="feComposite2912"
463
         result="composite2"
464
         operator="over"
465
         in2="offset"
466
         in="SourceGraphic" />
467
    </filter>
468
    <filter
469
       id="filter2926"
470
       inkscape:label="Drop Shadow"
471
       style="color-interpolation-filters:sRGB;">
472
      <feFlood
473
         id="feFlood2916"
474
         result="flood"
475
         flood-color="rgb(0,0,0)"
476
         flood-opacity="0.498039" />
477
      <feComposite
478
         id="feComposite2918"
479
         result="composite1"
480
         operator="in"
481
         in2="SourceGraphic"
482
         in="flood" />
483
      <feGaussianBlur
484
         id="feGaussianBlur2920"
485
         result="blur"
486
         stdDeviation="2"
487
         in="composite1" />
488
      <feOffset
489
         id="feOffset2922"
490
         result="offset"
491
         dy="3"
492
         dx="3" />
493
      <feComposite
494
         id="feComposite2924"
495
         result="composite2"
496
         operator="over"
497
         in2="offset"
498
         in="SourceGraphic" />
499
    </filter>
500
    <filter
501
       id="filter2938"
502
       inkscape:label="Drop Shadow"
503
       style="color-interpolation-filters:sRGB;">
504
      <feFlood
505
         id="feFlood2928"
506
         result="flood"
507
         flood-color="rgb(0,0,0)"
508
         flood-opacity="0.498039" />
509
      <feComposite
510
         id="feComposite2930"
511
         result="composite1"
512
         operator="in"
513
         in2="SourceGraphic"
514
         in="flood" />
515
      <feGaussianBlur
516
         id="feGaussianBlur2932"
517
         result="blur"
518
         stdDeviation="2"
519
         in="composite1" />
520
      <feOffset
521
         id="feOffset2934"
522
         result="offset"
523
         dy="3"
524
         dx="3" />
525
      <feComposite
526
         id="feComposite2936"
527
         result="composite2"
528
         operator="over"
529
         in2="offset"
530
         in="SourceGraphic" />
531
    </filter>
532
    <filter
533
       id="filter2950"
534
       inkscape:label="Drop Shadow"
535
       style="color-interpolation-filters:sRGB;">
536
      <feFlood
537
         id="feFlood2940"
538
         result="flood"
539
         flood-color="rgb(0,0,0)"
540
         flood-opacity="0.498039" />
541
      <feComposite
542
         id="feComposite2942"
543
         result="composite1"
544
         operator="in"
545
         in2="SourceGraphic"
546
         in="flood" />
547
      <feGaussianBlur
548
         id="feGaussianBlur2944"
549
         result="blur"
550
         stdDeviation="2"
551
         in="composite1" />
552
      <feOffset
553
         id="feOffset2946"
554
         result="offset"
555
         dy="3"
556
         dx="3" />
557
      <feComposite
558
         id="feComposite2948"
559
         result="composite2"
560
         operator="over"
561
         in2="offset"
562
         in="SourceGraphic" />
563
    </filter>
564
    <filter
565
       id="filter2962"
566
       inkscape:label="Drop Shadow"
567
       style="color-interpolation-filters:sRGB;">
568
      <feFlood
569
         id="feFlood2952"
570
         result="flood"
571
         flood-color="rgb(0,0,0)"
572
         flood-opacity="0.498039" />
573
      <feComposite
574
         id="feComposite2954"
575
         result="composite1"
576
         operator="in"
577
         in2="SourceGraphic"
578
         in="flood" />
579
      <feGaussianBlur
580
         id="feGaussianBlur2956"
581
         result="blur"
582
         stdDeviation="2"
583
         in="composite1" />
584
      <feOffset
585
         id="feOffset2958"
586
         result="offset"
587
         dy="3"
588
         dx="3" />
589
      <feComposite
590
         id="feComposite2960"
591
         result="composite2"
592
         operator="over"
593
         in2="offset"
594
         in="SourceGraphic" />
595
    </filter>
596
    <filter
597
       id="filter2974"
598
       inkscape:label="Drop Shadow"
599
       style="color-interpolation-filters:sRGB;">
600
      <feFlood
601
         id="feFlood2964"
602
         result="flood"
603
         flood-color="rgb(0,0,0)"
604
         flood-opacity="0.498039" />
605
      <feComposite
606
         id="feComposite2966"
607
         result="composite1"
608
         operator="in"
609
         in2="SourceGraphic"
610
         in="flood" />
611
      <feGaussianBlur
612
         id="feGaussianBlur2968"
613
         result="blur"
614
         stdDeviation="2"
615
         in="composite1" />
616
      <feOffset
617
         id="feOffset2970"
618
         result="offset"
619
         dy="3"
620
         dx="3" />
621
      <feComposite
622
         id="feComposite2972"
623
         result="composite2"
624
         operator="over"
625
         in2="offset"
626
         in="SourceGraphic" />
627
    </filter>
628
    <filter
629
       id="filter2986"
630
       inkscape:label="Drop Shadow"
631
       style="color-interpolation-filters:sRGB;">
632
      <feFlood
633
         id="feFlood2976"
634
         result="flood"
635
         flood-color="rgb(0,0,0)"
636
         flood-opacity="0.498039" />
637
      <feComposite
638
         id="feComposite2978"
639
         result="composite1"
640
         operator="in"
641
         in2="SourceGraphic"
642
         in="flood" />
643
      <feGaussianBlur
644
         id="feGaussianBlur2980"
645
         result="blur"
646
         stdDeviation="2"
647
         in="composite1" />
648
      <feOffset
649
         id="feOffset2982"
650
         result="offset"
651
         dy="3"
652
         dx="3" />
653
      <feComposite
654
         id="feComposite2984"
655
         result="composite2"
656
         operator="over"
657
         in2="offset"
658
         in="SourceGraphic" />
659
    </filter>
660
  </defs>
661
  <sodipodi:namedview
662
     inkscape:snap-text-baseline="false"
663
     inkscape:document-rotation="0"
664
     fit-margin-bottom="20"
665
     fit-margin-right="20"
666
     fit-margin-left="20"
667
     fit-margin-top="20"
668
     inkscape:current-layer="svg4610"
669
     inkscape:cy="370.55742"
670
     inkscape:cx="398.61418"
671
     inkscape:zoom="1.3753763"
672
     showgrid="false"
673
     id="namedview4612"
674
     inkscape:window-height="1280"
675
     inkscape:window-width="2055"
676
     inkscape:pageshadow="2"
677
     inkscape:pageopacity="1"
678
     guidetolerance="10"
679
     gridtolerance="10"
680
     objecttolerance="10"
681
     borderopacity="1"
682
     bordercolor="#666666"
683
     pagecolor="#ffffff"
684
     inkscape:window-x="215"
685
     inkscape:window-y="26"
686
     inkscape:window-maximized="0" />
687
  <path
688
     sodipodi:nodetypes="ccssssc"
689
     inkscape:connector-curvature="0"
690
     style="fill:#333333;fill-opacity:0.0666667;fill-rule:nonzero;stroke:#df4d65;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
691
     d="M 53.35547,445.11522 V 790.96744 H 741.0332 c 1.6112,0 2.90821,-1.29701 2.90821,-2.9082 V 448.02342 c 0,-1.6112 -1.297,-2.9082 -2.90821,-2.9082 z"
692
     id="path9961" />
693
  <path
694
     sodipodi:nodetypes="sssccssss"
695
     id="path9940"
696
     d="m 20.5,787.82486 c 0,0.87013 0.35019,1.65683 0.91797,2.22461 0.56778,0.56778 1.35253,0.91797 2.22265,0.91797 H 53.35547 V 445.11522 H 23.64062 c -0.87012,0 -1.65487,0.35019 -2.22265,0.91797 -0.56778,0.56778 -0.91797,1.35254 -0.91797,2.22266 z"
697
     style="fill:#df4d65;fill-opacity:1;fill-rule:nonzero;stroke:#df4d65;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
698
     inkscape:connector-curvature="0" />
699
  <path
700
     sodipodi:nodetypes="sssccssss"
701
     id="path11125"
702
     d="m 20.5,423.31014 c 0,0.87013 0.35019,1.65683 0.91797,2.22461 0.56778,0.56778 1.354494,0.9764 2.22265,0.91797 H 53.35547 V 210.6005 H 23.64062 c -0.87012,0 -1.65487,0.3502 -2.22265,0.918 C 20.85019,212.08629 20.5,212.871 20.5,213.74109 Z"
703
     style="fill:#3e3e3e;fill-opacity:1;fill-rule:nonzero;stroke:#3e3e3e;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
704
     inkscape:connector-curvature="0" />
705
  <path
706
     sodipodi:nodetypes="ccssssc"
707
     inkscape:connector-curvature="0"
708
     style="fill:#333333;fill-opacity:0.0666667;fill-rule:nonzero;stroke:#3e3e3e;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
709
     d="m 53.35547,210.6005 v 215.85222 h 687.67774 c 1.6112,0 2.9082,-1.29701 2.9082,-2.9082 V 213.5087 c 0,-1.6112 -1.29701,-2.90352 -2.9082,-2.9082 z"
710
     id="path11123" />
711
  <path
712
     id="path6150"
713
     d="m 557.756,222.53493 c -0.87012,0 -1.65683,0.35019 -2.22461,0.91797 -0.56778,0.56778 -0.91797,1.35253 -0.91797,2.22265 v 29.71485 h 165.6211 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91797,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z"
714
     style="fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2926)"
715
     inkscape:connector-curvature="0"
716
     sodipodi:nodetypes="sssccssss" />
717
  <path
718
     sodipodi:nodetypes="ccssssc"
719
     id="path6134"
720
     d="m 720.75716,255.39041 h -165.6211 v 152.63392 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 h 159.80469 c 1.6112,0 2.9082,-1.29701 2.9082,-2.90821 z"
721
     style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2926)"
722
     inkscape:connector-curvature="0" />
723
  <path
724
     id="path6082"
725
     d="m 317.13559,222.53494 c -0.87011,0 -1.65683,0.35019 -2.2246,0.91797 -0.56779,0.56778 -0.91798,1.35253 -0.91798,2.22265 v 29.71485 h 165.62111 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91798,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z"
726
     style="fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2938)"
727
     inkscape:connector-curvature="0"
728
     sodipodi:nodetypes="sssccssss" />
729
  <path
730
     sodipodi:nodetypes="ccssssc"
731
     id="path6080"
732
     d="M 479.61412,255.39041 H 313.99301 v 152.63392 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 h 159.80469 c 1.6112,0 2.90821,-1.29701 2.90821,-2.90821 z"
733
     style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2938)"
734
     inkscape:connector-curvature="0" />
735
  <path
736
     id="path10980"
737
     d="M 53.35547,20.500012 V 188.35224 h 687.67774 c 1.6112,0 2.9082,-1.29701 2.9082,-2.9082 V 23.408212 c 0,-1.6112 -1.29701,-2.912886 -2.9082,-2.9082 z"
738
     style="fill:#333333;fill-opacity:0.0666667;fill-rule:nonzero;stroke:#3e3e3e;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
739
     inkscape:connector-curvature="0"
740
     sodipodi:nodetypes="ccssssc" />
741
  <path
742
     inkscape:connector-curvature="0"
743
     style="fill:#3e3e3e;fill-opacity:1;fill-rule:nonzero;stroke:#3e3e3e;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
744
     d="m 20.5,185.20966 c 0,0.87013 0.35019,1.65683 0.91797,2.22461 0.56778,0.56778 1.35253,0.91797 2.22265,0.91797 H 53.35547 V 20.500012 H 23.64062 c -0.87012,0 -1.65487,0.350201 -2.22265,0.918 -0.56778,0.5678 -0.91797,1.3525 -0.91797,2.2226 z"
745
     id="path10982"
746
     sodipodi:nodetypes="sssccssss" />
747
  <path
748
     sodipodi:nodetypes="sssccssss"
749
     inkscape:connector-curvature="0"
750
     style="fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2962)"
751
     d="m 557.75599,36.704447 c -0.87012,0 -1.65683,0.35019 -2.22461,0.91797 -0.56778,0.56778 -0.91797,1.35253 -0.91797,2.22265 v 29.71485 h 165.6211 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91797,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z"
752
     id="path4857" />
753
  <path
754
     inkscape:connector-curvature="0"
755
     style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2962)"
756
     d="M 720.23451,69.559917 H 554.61341 V 169.2396 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 h 159.80469 c 1.6112,0 2.9082,-1.29701 2.9082,-2.90821 z"
757
     id="path4853" />
758
  <path
759
     sodipodi:nodetypes="sssccssss"
760
     inkscape:connector-curvature="0"
761
     style="fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2974)"
762
     d="m 317.13558,36.704447 c -0.87011,0 -1.65683,0.35019 -2.2246,0.91797 -0.56779,0.56778 -0.91798,1.35253 -0.91798,2.22265 v 29.71485 h 165.62111 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91798,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z"
763
     id="path5726" />
764
  <path
765
     inkscape:connector-curvature="0"
766
     style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2974)"
767
     d="M 479.61411,69.559917 H 313.993 V 169.2396 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 H 476.7059 c 1.6112,0 2.90821,-1.29701 2.90821,-2.90821 z"
768
     id="path5724" />
769
  <path
770
     id="path4721"
771
     d="m 235.85308,44.704447 c 0.87012,0 1.65488,0.35019 2.22266,0.91797 0.56778,0.56778 0.91797,1.35253 0.91797,2.22265 v -0.23242 c 0,-1.6112 -1.297,-2.9082 -2.9082,-2.9082 z"
772
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
773
     inkscape:connector-curvature="0" />
774
  <path
775
     sodipodi:nodetypes="sssccssss"
776
     id="path4719"
777
     d="m 76.515197,36.704447 c -0.870125,0 -1.656831,0.35019 -2.22461,0.91797 -0.567778,0.56778 -0.917968,1.35253 -0.917968,2.22265 v 29.71485 H 238.99371 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91797,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z"
778
     style="fill:#46c7f0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2986)"
779
     inkscape:connector-curvature="0" />
780
  <path
781
     id="path4723"
782
     d="M 238.99372,69.559917 H 73.372613 V 169.2396 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 H 236.08552 c 1.6112,0 2.9082,-1.29701 2.9082,-2.90821 z"
783
     style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2986)"
784
     inkscape:connector-curvature="0" />
785
  <path
786
     id="rect4622"
787
     d="m 76.280822,44.704447 c -1.611195,0 -2.908203,1.297 -2.908203,2.9082 v 0.23242 c 0,-0.87012 0.35019,-1.65487 0.917968,-2.22265 0.567779,-0.56778 1.354485,-0.91797 2.22461,-0.91797 z"
788
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
789
     inkscape:connector-curvature="0" />
790
  <path
791
     sodipodi:nodetypes="cc"
792
     inkscape:connector-curvature="0"
793
     id="path9889"
794
     d="m 397.61301,500.62068 -0.50618,32.59418"
795
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:1.9694221;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9893)" />
796
  <path
797
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9715)"
798
     d="m 554.61351,648.83688 -69.6817,47.69253"
799
     id="path9711"
800
     inkscape:connector-curvature="0"
801
     sodipodi:nodetypes="cc" />
802
  <path
803
     sodipodi:nodetypes="cc"
804
     inkscape:connector-curvature="0"
805
     id="path9675"
806
     d="M 554.61351,567.95047 484.93181,615.643"
807
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9679)" />
808
  <rect
809
     ry="3.9839513"
810
     rx="3.9205718"
811
     y="537.09552"
812
     x="554.61353"
813
     height="32.855"
814
     width="165.621"
815
     id="rect9618"
816
     style="opacity:1;fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2842)" />
817
  <rect
818
     ry="3.9839513"
819
     rx="3.9205718"
820
     y="537.09552"
821
     x="73.372665"
822
     height="32.855"
823
     width="165.621"
824
     id="rect9614"
825
     style="opacity:1;fill:#46c7f0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2914)" />
826
  <path
827
     inkscape:connector-curvature="0"
828
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
829
     d="m 235.85308,545.09525 c 0.87012,0 1.65488,0.35019 2.22266,0.91797 0.56778,0.56778 0.91797,1.35253 0.91797,2.22265 v -0.23242 c 0,-1.6112 -1.297,-2.9082 -2.9082,-2.9082 z"
830
     id="path9323" />
831
  <path
832
     inkscape:connector-curvature="0"
833
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
834
     d="m 76.280823,545.09525 c -1.611195,0 -2.908203,1.297 -2.908203,2.9082 v 0.23242 c 0,-0.87012 0.35019,-1.65487 0.917968,-2.22265 0.567779,-0.56778 1.354485,-0.91797 2.22461,-0.91797 z"
835
     id="path9327" />
836
  <rect
837
     style="opacity:1;fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2902)"
838
     id="rect9616"
839
     width="165.621"
840
     height="32.855"
841
     x="313.99307"
842
     y="537.09552"
843
     rx="3.9205718"
844
     ry="3.9839513" />
845
  <path
846
     sodipodi:nodetypes="cc"
847
     inkscape:connector-curvature="0"
848
     id="path9491"
849
     d="m 240.99257,554.11276 65.23376,-1.01307"
850
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9513)" />
851
  <path
852
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9640)"
853
     d="m 481.61298,554.11276 65.23376,-1.01307"
854
     id="path9501"
855
     inkscape:connector-curvature="0"
856
     sodipodi:nodetypes="cc" />
857
  <rect
858
     ry="3.9839513"
859
     rx="3.9205718"
860
     y="617.79578"
861
     x="313.99307"
862
     height="32.855"
863
     width="165.621"
864
     id="rect9620"
865
     style="opacity:1;fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2890)" />
866
  <path
867
     sodipodi:nodetypes="cc"
868
     inkscape:connector-curvature="0"
869
     id="path9681"
870
     d="m 481.61298,634.81299 65.23376,-1.01307"
871
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9685)" />
872
  <rect
873
     style="opacity:1;fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2854)"
874
     id="rect9687"
875
     width="165.621"
876
     height="32.855"
877
     x="554.61353"
878
     y="617.79578"
879
     rx="3.9205718"
880
     ry="3.9839513" />
881
  <path
882
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9750)"
883
     d="m 481.61298,715.51321 65.23376,-1.01307"
884
     id="path9734"
885
     inkscape:connector-curvature="0"
886
     sodipodi:nodetypes="cc" />
887
  <rect
888
     ry="3.9839513"
889
     rx="3.9205718"
890
     y="698.49591"
891
     x="554.61353"
892
     height="32.855"
893
     width="165.621"
894
     id="rect9736"
895
     style="opacity:1;fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2866)" />
896
  <path
897
     id="path9830"
898
     d="m 356.40451,489.45323 c -0.80426,0 -1.45167,0.64741 -1.45167,1.45166 v 0.11602 c 0,-0.43433 0.1748,-0.82605 0.45822,-1.10946 0.28341,-0.28342 0.6761,-0.45822 1.11043,-0.45822 z"
899
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.10902636;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
900
     inkscape:connector-curvature="0" />
901
  <rect
902
     style="opacity:1;fill:#ffb73a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.9391377;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
903
     id="rect9826"
904
     width="120.98324"
905
     height="24"
906
     x="336.82672"
907
     y="477.86002"
908
     rx="2.8639088"
909
     ry="2.9102066" />
910
  <path
911
     id="path10514"
912
     d="m 235.85301,637.23875 c 0.87012,0 1.65488,0.35019 2.22266,0.91797 0.56778,0.56778 0.91797,1.35253 0.91797,2.22265 v -0.23242 c 0,-1.6112 -1.297,-2.9082 -2.9082,-2.9082 z"
913
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
914
     inkscape:connector-curvature="0" />
915
  <rect
916
     style="opacity:1;fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2878)"
917
     id="rect9717"
918
     width="165.621"
919
     height="32.855"
920
     x="313.99307"
921
     y="698.49591"
922
     rx="3.9205718"
923
     ry="3.9839513" />
924
  <path
925
     id="path10537"
926
     d="M 238.99366,636.97465 H 73.372671 V 729.175 c 0,1.2055 0.970418,2.17592 2.175911,2.17592 H 236.81776 c 1.20549,0 2.1759,-0.97042 2.1759,-2.17592 z"
927
     style="opacity:1;fill:#333333;fill-opacity:0.93333333;fill-rule:nonzero;stroke:none;stroke-width:0.16342013;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
928
     inkscape:connector-curvature="0"
929
     sodipodi:nodetypes="ccssssc" />
930
  <path
931
     sodipodi:nodetypes="sssccssss"
932
     id="path10516"
933
     d="m 75.723937,612.39226 c -0.651025,0 -1.239637,0.26201 -1.664447,0.68682 -0.424811,0.42482 -0.686822,1.01196 -0.686822,1.66299 v 22.23258 H 238.99366 v -22.23258 c 0,-0.65103 -0.26201,-1.23817 -0.68682,-1.66299 -0.42481,-0.42481 -1.01197,-0.68682 -1.66299,-0.68682 z"
934
     style="opacity:1;fill:#ffb73a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.16342013;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
935
     inkscape:connector-curvature="0" />
936
  <path
937
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker10933)"
938
     d="m 240.99257,715.51318 65.23376,-1.01307"
939
     id="path10929"
940
     inkscape:connector-curvature="0"
941
     sodipodi:nodetypes="cc" />
942
  <path
943
     style="fill:#df4d65;fill-opacity:1;fill-rule:evenodd;stroke-width:0.05250736"
944
     d="m 399.47357,99.155037 -0.12716,0.0127 -0.12716,0.0141 -0.12574,0.0141 -0.12716,0.0155 -0.61883,0.967813 -0.29106,0.9325 -0.17943,0.91977 -0.1215,1.09073 -0.30943,0.0636 -0.308,0.0678 -0.308,0.0735 -0.30801,0.0763 -0.58069,-0.93108 -0.5609,-0.75023 -0.66687,-0.71349 -0.97912,-0.6033 -0.12009,0.0409 -0.1201,0.0424 -0.12009,0.0424 -0.12009,0.0424 -0.38854,1.08085 -0.0763,0.97487 0.0297,0.93673 0.12575,1.08932 -0.28823,0.1314 -0.28681,0.13563 -0.28398,0.13987 -0.28117,0.14271 -0.77425,-0.77849 -0.7149,-0.60612 -0.80816,-0.54819 -1.0879,-0.37017 -0.10879,0.0678 -0.10738,0.0679 -0.10738,0.0678 -0.10738,0.0678 -0.13846,1.14018 0.14129,0.9664 0.23735,0.90706 0.36594,1.03563 -0.25149,0.19215 -0.24865,0.19497 -0.24585,0.19922 -0.24442,0.20204 -0.92825,-0.58634 -0.83077,-0.43234 -0.90988,-0.35463 -1.14301,-0.11868 -0.0904,0.0904 -0.0904,0.089 -0.089,0.0904 -0.0904,0.0904 0.11868,1.14301 0.35462,0.90988 0.43234,0.83218 0.58634,0.92684 -0.20204,0.24442 -0.19921,0.24584 -0.19498,0.24867 -0.19215,0.25148 -1.03563,-0.36593 -0.90705,-0.23736 -0.96641,-0.14128 -1.14018,0.13845 -0.0678,0.10738 -0.0678,0.10738 -0.0678,0.10738 -0.0678,0.10879 0.37016,1.0879 0.5482,0.80816 0.60612,0.71491 0.77848,0.77425 -0.1427,0.28117 -0.13987,0.28398 -0.13564,0.28681 -0.13139,0.28823 -1.09073,-0.12575 -0.93532,-0.0297 -0.97487,0.0763 -1.08084,0.38854 -0.0424,0.12009 -0.0424,0.12009 -0.0424,0.1201 -0.0409,0.12009 0.6033,0.97912 0.7135,0.66686 0.75023,0.56091 0.93107,0.58069 -0.0763,0.30801 -0.0735,0.308 -0.0678,0.308 -0.0636,0.30942 -1.09073,0.1215 -0.91977,0.17944 -0.9325,0.29105 -0.96781,0.61883 -0.0156,0.12717 -0.0141,0.12574 -0.0141,0.12716 -0.0127,0.12716 0.80533,0.81804 0.84348,0.49168 0.85619,0.38006 1.03704,0.36028 -0.006,0.31648 -0.003,0.31648 0.003,0.31648 0.006,0.31648 -1.03704,0.35887 -0.85619,0.38006 -0.84348,0.49168 -0.80533,0.81946 0.0127,0.12716 0.0141,0.12574 0.0141,0.12716 0.0156,0.12574 0.96781,0.62026 0.9325,0.29104 0.91977,0.17944 1.09073,0.12009 0.0636,0.30942 0.0678,0.30941 0.0735,0.30801 0.0763,0.30659 -0.93107,0.5821 -0.75023,0.56091 -0.7135,0.66687 -0.6033,0.97771 0.0409,0.12008 0.0424,0.12151 0.0424,0.1201 0.0424,0.11868 1.08084,0.38995 0.97487,0.0763 0.93532,-0.0297 1.09073,-0.12574 0.13139,0.28822 0.13564,0.2854 0.13987,0.28399 0.1427,0.28257 -0.77848,0.77425 -0.60612,0.7135 -0.5482,0.80957 -0.37016,1.08791 0.0678,0.10737 0.0678,0.10879 0.0678,0.10738 0.0678,0.10738 1.14018,0.13846 0.96641,-0.1427 0.90705,-0.23736 1.03563,-0.36452 0.19215,0.25149 0.19498,0.24866 0.19921,0.24584 0.20204,0.24302 -0.58634,0.92825 -0.43234,0.83076 -0.35462,0.9113 -0.11868,1.14159 0.0904,0.0918 0.089,0.089 0.0904,0.0904 0.0904,0.089 1.14301,-0.11868 0.90988,-0.35321 0.83077,-0.43375 0.92825,-0.58493 0.24442,0.20204 0.24585,0.19921 0.24865,0.19497 0.25149,0.19216 -0.36594,1.03563 -0.23735,0.90564 -0.14129,0.9664 0.13846,1.14018 0.10738,0.0692 0.10738,0.0678 0.10738,0.0678 0.10879,0.0664 1.0879,-0.37017 0.80816,-0.54677 0.7149,-0.60754 0.77425,-0.77708 0.28117,0.14271 0.28398,0.13987 0.28681,0.13422 0.28823,0.13139 -0.12575,1.09074 -0.0297,0.93673 0.0763,0.97346 0.38854,1.08084 0.12009,0.0438 0.12009,0.0424 0.1201,0.041 0.12009,0.0409 0.97912,-0.60188 0.66687,-0.71349 0.5609,-0.75165 0.58069,-0.93108 0.30801,0.0777 0.308,0.072 0.308,0.0692 0.30943,0.0636 0.1215,1.09073 0.17943,0.91978 0.29106,0.93249 0.61883,0.9664 0.12716,0.0156 0.12574,0.0141 0.12716,0.0141 0.12716,0.0141 0.81806,-0.80533 0.49167,-0.8449 0.38006,-0.85619 0.36028,-1.03704 0.31648,0.007 0.31648,0.003 0.31649,-0.003 0.31648,-0.007 0.36028,1.03704 0.37865,0.85619 0.49167,0.8449 0.81947,0.80533 0.12715,-0.0141 0.12574,-0.0141 0.12717,-0.0141 0.12574,-0.0156 0.62025,-0.9664 0.29104,-0.93249 0.17944,-0.91978 0.12009,-1.09073 0.30942,-0.0636 0.30942,-0.0692 0.30799,-0.072 0.3066,-0.0777 0.58211,0.93108 0.5609,0.75165 0.66687,0.71349 0.97771,0.60188 0.12009,-0.0409 0.1215,-0.041 0.1201,-0.0424 0.11868,-0.0438 0.38995,-1.08084 0.0763,-0.97346 -0.0297,-0.93673 -0.12574,-1.09074 0.28822,-0.13139 0.2854,-0.13422 0.28398,-0.13987 0.28258,-0.14271 0.77424,0.77708 0.7135,0.60754 0.80957,0.54677 1.08791,0.37017 0.10737,-0.0664 0.10879,-0.0678 0.10738,-0.0678 0.10738,-0.0692 0.13847,-1.14018 -0.14271,-0.9664 -0.23737,-0.90564 -0.36452,-1.03563 0.25149,-0.19216 0.24866,-0.19497 0.24585,-0.19921 0.24301,-0.20204 0.92825,0.58493 0.83077,0.43375 0.91129,0.35321 1.1416,0.11868 0.0904,-0.089 0.0904,-0.0904 0.0904,-0.089 0.089,-0.0918 -0.11868,-1.14159 -0.35321,-0.9113 -0.43375,-0.83076 -0.58492,-0.92825 0.20203,-0.24302 0.19921,-0.24584 0.19498,-0.24866 0.19215,-0.25149 1.03563,0.36452 0.90564,0.23736 0.9664,0.1427 1.14018,-0.13846 0.0692,-0.10738 0.0678,-0.10738 0.0678,-0.10879 0.0664,-0.10737 -0.37017,-1.08791 -0.54677,-0.80957 -0.60754,-0.7135 -0.77706,-0.77425 0.1427,-0.28257 0.13986,-0.28399 0.13423,-0.2854 0.13139,-0.28822 1.09073,0.12574 0.93674,0.0297 0.97345,-0.0763 1.08085,-0.38995 0.0438,-0.11868 0.0424,-0.1201 0.0409,-0.12151 0.041,-0.12008 -0.6019,-0.97771 -0.71349,-0.66687 -0.75164,-0.56091 -0.93108,-0.5821 0.0777,-0.30659 0.072,-0.30801 0.0692,-0.30941 0.0636,-0.30942 1.09073,-0.12009 0.91978,-0.17944 0.93249,-0.29104 0.9664,-0.62026 0.0155,-0.12574 0.0141,-0.12716 0.0141,-0.12574 0.0141,-0.12716 -0.80533,-0.81946 -0.8449,-0.49168 -0.85619,-0.38006 -1.03704,-0.35887 0.007,-0.31648 0.003,-0.31648 -0.003,-0.31648 -0.007,-0.31648 1.03704,-0.36028 0.85619,-0.38006 0.8449,-0.49168 0.80533,-0.81804 -0.0141,-0.12716 -0.0141,-0.12716 -0.0141,-0.12574 -0.0155,-0.12717 -0.9664,-0.61883 -0.93249,-0.29105 -0.91978,-0.17944 -1.09073,-0.1215 -0.0636,-0.30942 -0.0692,-0.308 -0.072,-0.308 -0.0777,-0.30801 0.93108,-0.58069 0.75164,-0.56091 0.71349,-0.66686 0.6019,-0.97912 -0.041,-0.12009 -0.0409,-0.1201 -0.0424,-0.12009 -0.0438,-0.12009 -1.08085,-0.38854 -0.97345,-0.0763 -0.93674,0.0297 -1.09073,0.12575 -0.13139,-0.28823 -0.13423,-0.28681 -0.13986,-0.28398 -0.1427,-0.28117 0.77706,-0.77425 0.60754,-0.71491 0.54677,-0.80816 0.37017,-1.0879 -0.0664,-0.10879 -0.0678,-0.10738 -0.0678,-0.10738 -0.0692,-0.10738 -1.14018,-0.13845 -0.9664,0.14128 -0.90564,0.23736 -1.03563,0.36593 -0.19215,-0.25148 -0.19498,-0.24867 -0.19921,-0.24584 -0.20203,-0.24442 0.58492,-0.92684 0.43375,-0.83218 0.35321,-0.90988 0.11868,-1.14301 -0.089,-0.0904 -0.0904,-0.0904 -0.0904,-0.089 -0.0904,-0.0904 -1.1416,0.11868 -0.91129,0.35463 -0.83077,0.43234 -0.92825,0.58634 -0.24301,-0.20204 -0.24585,-0.19922 -0.24866,-0.19497 -0.25149,-0.19215 0.36452,-1.03563 0.23737,-0.90706 0.14271,-0.9664 -0.13847,-1.14018 -0.10738,-0.0678 -0.10738,-0.0678 -0.10879,-0.0679 -0.10737,-0.0678 -1.08791,0.37017 -0.80957,0.54819 -0.7135,0.60612 -0.77424,0.77849 -0.28258,-0.14271 -0.28398,-0.13987 -0.2854,-0.13563 -0.28822,-0.1314 0.12574,-1.08932 0.0297,-0.93673 -0.0763,-0.97487 -0.38995,-1.08085 -0.11868,-0.0424 -0.1201,-0.0424 -0.1215,-0.0424 -0.12009,-0.0409 -0.97771,0.6033 -0.66687,0.71349 -0.5609,0.75023 -0.58211,0.93108 -0.3066,-0.0763 -0.30799,-0.0735 -0.30942,-0.0678 -0.30942,-0.0636 -0.12009,-1.09073 -0.17944,-0.91977 -0.29104,-0.9325 -0.62025,-0.967813 -0.12574,-0.0155 -0.12717,-0.0141 -0.12574,-0.0141 -0.12715,-0.0127 -0.81947,0.80533 -0.49167,0.843483 -0.37865,0.8562 -0.36028,1.03704 -0.31648,-0.006 -0.31649,-0.003 -0.31648,0.003 -0.31648,0.006 -0.36028,-1.03704 -0.38006,-0.8562 -0.49167,-0.843483 z m 2.68302,20.688573 a 5.3990039,5.3990039 0 0 1 5.39856,5.39997 5.3990039,5.3990039 0 0 1 -5.39856,5.39855 5.3990039,5.3990039 0 0 1 -5.39996,-5.39855 5.3990039,5.3990039 0 0 1 5.39996,-5.39997 z"
945
     id="path5693"
946
     inkscape:connector-curvature="0" />
947
  <path
948
     inkscape:connector-curvature="0"
949
     d="m 380.9529,101.31918 a 4.37599,4.37599 0 0 1 -4.37599,4.37599 4.37599,4.37599 0 0 1 -4.37599,-4.37599 4.37599,4.37599 0 0 1 4.37599,-4.375983 4.37599,4.37599 0 0 1 4.37599,4.375983 z m 4.63493,-1.27213 c -0.32212,-0.118873 -0.95326,0.0926 -0.92258,-0.401293 -0.13877,-0.39635 -0.21401,-0.74537 0.27363,-0.88946 0.78055,-0.47633 1.45123,-1.16128 1.74461,-2.04171 0.15411,-0.39145 -0.3432,-0.48754 -0.63657,-0.53536 -0.91614,-0.25589 -1.86519,0.0578 -2.73328,0.35995 -0.11023,-0.31345 -0.69059,-0.56868 -0.47901,-0.88657 0.56405,-0.84324 0.99162,-1.8335 0.85012,-2.86709 -0.031,-0.41955 -0.52073,-0.29038 -0.8058,-0.20618 -0.93646,0.16692 -1.65537,0.86143 -2.30642,1.51029 -0.23528,-0.23464 -0.86896,-0.21274 -0.81624,-0.59097 0.14241,-1.00446 0.0978,-2.08217 -0.47806,-2.95201 -0.20995,-0.36459 -0.59514,-0.0357 -0.81544,0.16385 -0.7713,0.55671 -1.11768,1.49434 -1.42274,2.36143 -0.3138,-0.10927 -0.87519,0.18536 -0.99181,-0.17826 -0.30757,-0.96675 -0.81528,-1.91841 -1.71153,-2.45229 -0.34734,-0.23737 -0.55176,0.22606 -0.66364,0.50149 -0.45331,0.83622 -0.35865,1.83127 -0.25723,2.74482 -0.33012,0.0378 -0.70817,0.54672 -0.97098,0.26977 -0.69651,-0.73763 -1.56687,-1.37476 -2.60601,-1.46682 -0.41593,-0.0631 -0.39904,0.443 -0.38034,0.73971 -0.0456,0.95009 0.47143,1.80554 0.95918,2.58463 -0.28104,0.17725 -0.40076,0.79984 -0.75777,0.6643 -0.94759,-0.36236 -2.00818,-0.55864 -2.98437,-0.1908 -0.40215,0.12357 -0.16723,0.57227 -0.0217,0.83147 0.37114,0.8758 1.2081,1.42221 1.98561,1.91248 -0.17632,0.28163 -0.0141,0.89453 -0.39449,0.92732 -1.01097,0.0847 -2.05173,0.367983 -2.77166,1.122963 -0.3087,0.2858 0.0976,0.58816 0.34122,0.75853 0.71439,0.62804 1.70558,0.75716 2.6188,0.8616 -0.0367,0.33025 0.37548,0.81204 0.0469,1.00665 -0.87415,0.51488 -1.68886,1.22174 -2.00994,2.2143 -0.15411,0.39144 0.3432,0.48753 0.63657,0.53535 0.91612,0.25589 1.86516,-0.0578 2.73323,-0.35995 0.11029,0.31341 0.69063,0.56869 0.47905,0.88657 -0.56412,0.8432 -0.99155,1.83352 -0.85015,2.86709 0.031,0.41952 0.52075,0.2904 0.80584,0.20618 0.93644,-0.16692 1.65537,-0.86139 2.30637,-1.51029 0.2353,0.23464 0.86901,0.21272 0.81629,0.59098 -0.14241,1.00446 -0.0978,2.08215 0.47802,2.95202 0.20997,0.36455 0.59517,0.0357 0.81548,-0.16387 0.77125,-0.55674 1.11768,-1.49435 1.42274,-2.36142 0.31379,0.10926 0.8752,-0.18537 0.99181,0.17824 0.30754,0.96678 0.81527,1.91842 1.71153,2.45229 0.34733,0.23738 0.55172,-0.22608 0.66362,-0.50146 0.45335,-0.83621 0.35866,-1.83128 0.25725,-2.74484 0.33011,-0.0378 0.70812,-0.54672 0.97093,-0.26977 0.69656,0.7376 1.5669,1.37477 2.60606,1.46683 0.41593,0.0632 0.39897,-0.44304 0.38032,-0.73972 0.0457,-0.95011 -0.4715,-1.8055 -0.95916,-2.58463 0.28105,-0.17722 0.40074,-0.79983 0.75772,-0.6643 0.94761,0.36234 2.00821,0.55865 2.98442,0.1908 0.40215,-0.12357 0.16723,-0.57228 0.0217,-0.83146 -0.37116,-0.87579 -1.20814,-1.42218 -1.98561,-1.91249 0.17632,-0.28163 0.0141,-0.89453 0.39449,-0.92732 1.01097,-0.0847 2.05173,-0.36799 2.77166,-1.12295 0.3087,-0.28581 -0.0976,-0.58817 -0.34122,-0.75854 -0.47483,-0.43652 -1.13407,-0.61787 -1.75144,-0.75008 z"
950
     style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke-width:0.04014921"
951
     id="path5687" />
952
  <path
953
     style="fill:#05556e;fill-opacity:1;stroke-width:0.11881336"
954
     id="path4816"
955
     d="m 619.92265,90.37586 h 2.72717 v 2.71445 h 2.51374 v -2.71445 h 2.72716 v 8.21477 h -2.72716 v -2.73825 h -2.49003 v 2.73825 h -2.75088 m 11.57268,-5.47651 h -2.40702 v -2.73826 h 7.55307 v 2.73826 h -2.41888 v 5.47651 h -2.72717 m 6.34363,-8.21477 h 2.8576 l 1.75487,2.89303 1.75487,-2.89303 h 2.8576 v 8.21477 h -2.72717 v -4.07167 l -1.90901,2.95256 -1.90902,-2.95256 v 4.07167 h -2.67974 m 10.57667,-8.21477 h 2.72717 v 5.50033 h 3.86546 v 2.71444 h -6.59263"
956
     inkscape:connector-curvature="0" />
957
  <path
958
     id="path4818"
959
     d="m 619.82779,146.45062 -3.91289,-44.09786 h 43.01811 l -3.91289,44.07405 -17.63174,4.90505"
960
     inkscape:connector-curvature="0"
961
     style="fill:#e44d26;stroke-width:0.11881336" />
962
  <path
963
     id="path4820"
964
     d="m 637.42396,147.58164 v -41.60962 h 17.5843 l -3.3556,37.62129"
965
     inkscape:connector-curvature="0"
966
     style="fill:#f16529;stroke-width:0.11881336" />
967
  <path
968
     id="path4822"
969
     d="m 623.90669,111.3652 h 13.51727 v 5.40508 h -7.61236 l 0.498,5.53605 h 7.11436 v 5.39318 h -12.04697 m 0.23714,2.71444 h 5.40691 l 0.37943,4.32169 6.02349,1.61914 v 5.64319 L 626.373,138.90255"
970
     inkscape:connector-curvature="0"
971
     style="fill:#ebebeb;stroke-width:0.11881336" />
972
  <path
973
     id="path4824"
974
     d="m 650.89379,111.3652 h -13.49355 v 5.40508 h 12.99555 m -0.48615,5.53605 h -12.5094 v 5.40508 h 6.64006 l -0.62843,7.02423 -6.01163,1.61914 v 5.61938 l 11.02724,-3.07161"
975
     inkscape:connector-curvature="0"
976
     style="fill:#ffffff;stroke-width:0.11881336" />
977
  <path
978
     sodipodi:nodetypes="cc"
979
     inkscape:connector-curvature="0"
980
     id="path5804"
981
     d="m 240.99252,105.07517 65.2338,-1.01308"
982
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9479)" />
983
  <path
984
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9505)"
985
     d="m 481.61302,105.07517 65.2337,-1.01308"
986
     id="path9497"
987
     inkscape:connector-curvature="0"
988
     sodipodi:nodetypes="cc" />
989
  <path
990
     inkscape:connector-curvature="0"
991
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
992
     d="m 235.85308,230.53494 c 0.87012,0 1.65488,0.35019 2.22266,0.91797 0.56778,0.56778 0.91797,1.35253 0.91797,2.22265 v -0.23242 c 0,-1.6112 -1.297,-2.9082 -2.9082,-2.9082 z"
993
     id="path6102" />
994
  <path
995
     inkscape:connector-curvature="0"
996
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
997
     d="m 76.280823,230.53494 c -1.611195,0 -2.908203,1.297 -2.908203,2.9082 v 0.23242 c 0,-0.87012 0.35019,-1.65487 0.917968,-2.22265 0.567779,-0.56778 1.354485,-0.91797 2.22461,-0.91797 z"
998
     id="path6106" />
999
  <path
1000
     inkscape:connector-curvature="0"
1001
     style="fill:#46c7f0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2950)"
1002
     d="m 76.515198,222.53494 c -0.870125,0 -1.656831,0.35019 -2.22461,0.91797 -0.567778,0.56778 -0.917968,1.35253 -0.917968,2.22265 v 29.71485 h 165.62109 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91797,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z"
1003
     id="path6104"
1004
     sodipodi:nodetypes="sssccssss" />
1005
  <path
1006
     sodipodi:nodetypes="ccssssc"
1007
     inkscape:connector-curvature="0"
1008
     style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2950)"
1009
     d="M 238.99371,255.39041 H 73.37262 v 152.63392 c 0,1.6112 1.297008,2.90821 2.908203,2.90821 H 236.08551 c 1.6112,0 2.9082,-1.29701 2.9082,-2.90821 z"
1010
     id="path6100" />
1011
  <path
1012
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9509)"
1013
     d="m 240.99257,328.95043 65.23376,-1.01307"
1014
     id="path9485"
1015
     inkscape:connector-curvature="0"
1016
     sodipodi:nodetypes="cc" />
1017
  <path
1018
     sodipodi:nodetypes="cc"
1019
     inkscape:connector-curvature="0"
1020
     id="path9757"
1021
     d="m 481.61298,300.08996 65.23376,-1.01307"
1022
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9761)" />
1023
  <path
1024
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9767)"
1025
     d="M 552.61456,372.04139 487.3808,371.02832"
1026
     id="path9763"
1027
     inkscape:connector-curvature="0"
1028
     sodipodi:nodetypes="cc" />
1029
  <text
1030
     id="text2269"
1031
     y="62.149761"
1032
     x="115.43707"
1033
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1034
     xml:space="preserve"><tspan
1035
       y="62.149761"
1036
       x="115.43707"
1037
       id="tspan2267"
1038
       sodipodi:role="line">Text Edit</tspan></text>
1039
  <text
1040
     transform="rotate(-90)"
1041
     id="text2273"
1042
     y="43.507812"
1043
     x="-132.24059"
1044
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1045
     xml:space="preserve"><tspan
1046
       y="43.507812"
1047
       x="-132.24059"
1048
       id="tspan2271"
1049
       sodipodi:role="line">Today</tspan></text>
1050
  <text
1051
     id="text2277"
1052
     y="61.540386"
1053
     x="358.88168"
1054
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1055
     xml:space="preserve"><tspan
1056
       y="61.540386"
1057
       x="358.88168"
1058
       id="tspan2275"
1059
       sodipodi:role="line">Process</tspan></text>
1060
  <text
1061
     id="text2281"
1062
     y="59.34898"
1063
     x="605.30872"
1064
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1065
     xml:space="preserve"><tspan
1066
       y="59.34898"
1067
       x="605.30872"
1068
       id="tspan2279"
1069
       sodipodi:role="line">Output</tspan></text>
1070
  <text
1071
     id="text2285"
1072
     y="245.17946"
1073
     x="605.30872"
1074
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1075
     xml:space="preserve"><tspan
1076
       y="245.17946"
1077
       x="605.30872"
1078
       id="tspan2283"
1079
       sodipodi:role="line">Output</tspan></text>
1080
  <text
1081
     id="text2289"
1082
     y="247.37088"
1083
     x="358.88168"
1084
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1085
     xml:space="preserve"><tspan
1086
       y="247.37088"
1087
       x="358.88168"
1088
       id="tspan2287"
1089
       sodipodi:role="line">Process</tspan></text>
1090
  <text
1091
     id="text2293"
1092
     y="247.98026"
1093
     x="115.43707"
1094
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1095
     xml:space="preserve"><tspan
1096
       y="247.98026"
1097
       x="115.43707"
1098
       id="tspan2291"
1099
       sodipodi:role="line">Text Edit</tspan></text>
1100
  <text
1101
     transform="rotate(-90)"
1102
     id="text2297"
1103
     y="43.630859"
1104
     x="-363.15442"
1105
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1106
     xml:space="preserve"><tspan
1107
       y="43.630859"
1108
       x="-363.15442"
1109
       id="tspan2295"
1110
       sodipodi:role="line">Proposed</tspan></text>
1111
  <text
1112
     id="text2301"
1113
     y="314.01108"
1114
     x="98.034729"
1115
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1116
     xml:space="preserve"><tspan
1117
       id="tspan2299"
1118
       sodipodi:role="line"
1119
       x="98.034729"
1120
       y="314.01108">R Markdown</tspan></text>
1121
  <text
1122
     id="text2305"
1123
     y="285.84311"
1124
     x="107.43903"
1125
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1126
     xml:space="preserve"><tspan
1127
       id="tspan2303"
1128
       sodipodi:role="line"
1129
       x="107.43903"
1130
       y="285.84311">Markdown</tspan></text>
1131
  <text
1132
     id="text2309"
1133
     y="342.91147"
1134
     x="134.3277"
1135
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1136
     xml:space="preserve"><tspan
1137
       id="tspan2307"
1138
       sodipodi:role="line"
1139
       x="134.3277"
1140
       y="342.91147">XML</tspan></text>
1141
  <text
1142
     id="text2313"
1143
     y="370.34702"
1144
     x="113.56207"
1145
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1146
     xml:space="preserve"><tspan
1147
       id="tspan2311"
1148
       sodipodi:role="line"
1149
       x="113.56207"
1150
       y="370.34702">DocBook</tspan></text>
1151
  <text
1152
     id="text2317"
1153
     y="398.51498"
1154
     x="114.3526"
1155
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1156
     xml:space="preserve"><tspan
1157
       id="tspan2315"
1158
       sodipodi:role="line"
1159
       x="114.3526"
1160
       y="398.51498">AsciiDoc</tspan></text>
1161
  <text
1162
     transform="rotate(-90)"
1163
     id="text2329"
1164
     y="43.507812"
1165
     x="-774.87335"
1166
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1167
     xml:space="preserve"><tspan
1168
       y="43.507812"
1169
       x="-774.87335"
1170
       id="tspan2327"
1171
       sodipodi:role="line">Example Processing Combination</tspan></text>
1172
  <text
1173
     id="text2333"
1174
     y="562.05426"
1175
     x="135.31207"
1176
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#f3fbfe;fill-opacity:1;stroke:none"
1177
     xml:space="preserve"><tspan
1178
       y="562.05426"
1179
       x="135.31207"
1180
       id="tspan2331"
1181
       sodipodi:role="line">XML</tspan></text>
1182
  <text
1183
     id="text2337"
1184
     y="495.6918"
1185
     x="381.64142"
1186
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#333333;fill-opacity:0.866667;stroke:none"
1187
     xml:space="preserve"><tspan
1188
       y="495.6918"
1189
       x="381.64142"
1190
       id="tspan2335"
1191
       sodipodi:role="line">XSLT</tspan></text>
1192
  <text
1193
     id="text2341"
1194
     y="562.05426"
1195
     x="323.97742"
1196
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1197
     xml:space="preserve"><tspan
1198
       y="562.05426"
1199
       x="323.97742"
1200
       id="tspan2339"
1201
       sodipodi:role="line">XSLT Processor</tspan></text>
1202
  <text
1203
     id="text2345"
1204
     y="562.54059"
1205
     x="579.27557"
1206
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1207
     xml:space="preserve"><tspan
1208
       y="562.54059"
1209
       x="579.27557"
1210
       id="tspan2343"
1211
       sodipodi:role="line">R Markdown</tspan></text>
1212
  <text
1213
     id="text2349"
1214
     y="643.24084"
1215
     x="588.75018"
1216
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1217
     xml:space="preserve"><tspan
1218
       y="643.24084"
1219
       x="588.75018"
1220
       id="tspan2347"
1221
       sodipodi:role="line">Markdown</tspan></text>
1222
  <text
1223
     id="text2353"
1224
     y="642.63147"
1225
     x="339.61023"
1226
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1227
     xml:space="preserve"><tspan
1228
       y="642.63147"
1229
       x="339.61023"
1230
       id="tspan2351"
1231
       sodipodi:role="line">R Processor</tspan></text>
1232
  <text
1233
     id="text2357"
1234
     y="722.93903"
1235
     x="318.43912"
1236
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:21.3333px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1237
     xml:space="preserve"><tspan
1238
       y="722.93903"
1239
       x="318.43912"
1240
       id="tspan2355"
1241
       sodipodi:role="line">Variable Processor</tspan></text>
1242
  <text
1243
     id="text2361"
1244
     y="723.3316"
1245
     x="604.07831"
1246
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1247
     xml:space="preserve"><tspan
1248
       y="723.3316"
1249
       x="604.07831"
1250
       id="tspan2359"
1251
       sodipodi:role="line">HTML5</tspan></text>
1252
  <text
1253
     id="text2365"
1254
     y="630.84766"
1255
     x="81.211723"
1256
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#333333;fill-opacity:0.866667;stroke:none"
1257
     xml:space="preserve"><tspan
1258
       y="630.84766"
1259
       x="81.211723"
1260
       id="tspan2363"
1261
       sodipodi:role="line">Structured Data Source</tspan></text>
1262
  <text
1263
     id="text2369"
1264
     y="756.39404"
1265
     x="215.65826"
1266
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1267
     xml:space="preserve"><tspan
1268
       y="756.39404"
1269
       x="215.65826"
1270
       id="tspan2367"
1271
       sodipodi:role="line">interpolated values</tspan></text>
1272
  <g
1273
     transform="translate(-0.25585322,11.831789)"
1274
     id="g2523">
1275
    <text
1276
       xml:space="preserve"
1277
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1278
       x="156.49219"
1279
       y="708.2467"
1280
       id="text2373"><tspan
1281
         sodipodi:role="line"
1282
         id="tspan2371"
1283
         x="156.49219"
1284
         y="708.2467">CSON</tspan></text>
1285
    <text
1286
       xml:space="preserve"
1287
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1288
       x="156.49219"
1289
       y="688.41504"
1290
       id="text2377"><tspan
1291
         sodipodi:role="line"
1292
         id="tspan2375"
1293
         x="156.49219"
1294
         y="688.41504">JSONNET</tspan></text>
1295
    <text
1296
       xml:space="preserve"
1297
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1298
       x="156.49219"
1299
       y="668.24695"
1300
       id="text2381"><tspan
1301
         sodipodi:role="line"
1302
         id="tspan2379"
1303
         x="156.49219"
1304
         y="668.24695">JSON5</tspan></text>
1305
    <text
1306
       xml:space="preserve"
1307
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1308
       x="156.49219"
1309
       y="648.07886"
1310
       id="text2385"><tspan
1311
         sodipodi:role="line"
1312
         id="tspan2383"
1313
         x="156.49219"
1314
         y="648.07886">JSON</tspan></text>
1315
    <text
1316
       xml:space="preserve"
1317
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1318
       x="94.110725"
1319
       y="648.41534"
1320
       id="text2389"><tspan
1321
         sodipodi:role="line"
1322
         id="tspan2387"
1323
         x="94.110725"
1324
         y="648.41534">YAML</tspan></text>
1325
    <text
1326
       xml:space="preserve"
1327
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1328
       x="94.110725"
1329
       y="668.24695"
1330
       id="text2393"><tspan
1331
         sodipodi:role="line"
1332
         id="tspan2391"
1333
         x="94.110725"
1334
         y="668.24695">TOML</tspan></text>
1335
    <text
1336
       xml:space="preserve"
1337
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1338
       x="94.110725"
1339
       y="688.41504"
1340
       id="text2397"><tspan
1341
         sodipodi:role="line"
1342
         id="tspan2395"
1343
         x="94.110725"
1344
         y="688.41504">XML</tspan></text>
1345
  </g>
1346
  <g
1347
     transform="translate(-1.2304677,-0.85937628)"
1348
     id="g2593">
1349
    <g
1350
       id="g2532">
1351
      <rect
1352
         id="rect4698"
1353
         ry="2.7292624"
1354
         y="91.740654"
1355
         x="129.16347"
1356
         height="32.205296"
1357
         width="54.039394"
1358
         style="fill:none;stroke:#05556e;stroke-width:2.72926;stroke-opacity:1" />
1359
      <path
1360
         style="fill:#05556e;fill-opacity:1;stroke-width:0.272926"
1361
         id="path4700"
1362
         d="M 135.98663,117.12279 V 98.56381 h 5.45852 l 5.45853,6.82315 5.45852,-6.82315 h 5.45853 v 18.55898 h -5.45853 v -10.64412 l -5.45852,6.82315 -5.45853,-6.82315 v 10.64412 z m 34.11578,0 -8.18779,-9.00657 h 5.45852 v -9.55241 h 5.45853 v 9.55241 h 5.45852 z"
1363
         inkscape:connector-curvature="0" />
1364
    </g>
1365
    <text
1366
       xml:space="preserve"
1367
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1368
       x="108.73981"
1369
       y="152.80437"
1370
       id="text2407"><tspan
1371
         sodipodi:role="line"
1372
         id="tspan2405"
1373
         x="108.73981"
1374
         y="152.80437">Markdown</tspan></text>
1375
  </g>
1376
  <path
1377
     inkscape:connector-curvature="0"
1378
     d="m 417.86562,272.90923 c -2.81873,0.35302 -5.58858,1.78683 -7.90222,4.10047 -1.79226,1.78682 -3.43787,4.20365 -5.01832,7.35911 -1.28173,2.56347 -2.29191,5.21927 -2.90019,7.59265 l -0.1738,0.68975 -0.68975,0.35302 c -0.96673,0.49423 -1.81398,1.01561 -2.77528,1.69993 -3.29666,2.35709 -6.15341,5.19211 -8.53222,8.46705 -0.23354,0.32586 -0.45621,0.58656 -0.49966,0.58656 -0.038,0 -0.33673,-0.0435 -0.65716,-0.0923 -0.73863,-0.11949 -3.19891,-0.13578 -4.11676,-0.0272 -3.79633,0.46164 -7.25593,1.57502 -11.41613,3.68228 -3.00339,1.5207 -4.93685,2.87304 -6.8323,4.77391 -2.37881,2.37882 -3.80176,5.01832 -4.21452,7.82076 -0.0978,0.62457 -0.0978,2.39511 0,3.0414 0.51052,3.55193 2.55804,6.94636 5.27358,8.74404 3.15003,2.08554 7.40256,2.6558 12.27424,1.65105 3.62253,-0.75492 7.20161,-2.14527 10.77526,-4.19822 3.47046,-1.99321 5.87643,-4.18193 7.57093,-6.87575 0.27155,-0.43449 0.35845,-0.52682 0.53224,-0.59199 2.79701,-1.01018 4.74677,-2.05295 6.96265,-3.72572 2.02036,-1.5207 3.43244,-2.85675 6.0991,-5.77324 0.68432,-0.74949 0.8038,-0.91785 0.84182,-1.16225 0.0326,-0.17379 0.0543,-0.20095 0.15207,-0.17922 0.51595,0.10319 2.20502,0.11948 2.94908,0.0272 2.08553,-0.25526 4.05701,-1.10251 6.01763,-2.57976 2.61778,-1.97691 5.06177,-5.27901 6.78885,-9.17853 2.59606,-5.86556 3.57908,-10.80785 3.01425,-15.19073 -0.14121,-1.12423 -0.28241,-1.74881 -0.59742,-2.71554 -0.42905,-1.29803 -1.08621,-2.55804 -1.89001,-3.62796 -0.43449,-0.57026 -1.57502,-1.70536 -2.14528,-2.12898 -1.59131,-1.17855 -3.93753,-2.13442 -6.03936,-2.46028 -0.66259,-0.10319 -2.29735,-0.14664 -2.85132,-0.0815 z m 2.44399,7.82076 c 1.94433,0.46707 3.2152,2.04751 3.5302,4.39917 0.0815,0.58656 0.0815,2.10183 0,2.7427 -0.32043,2.62864 -1.26544,5.70263 -2.61235,8.48878 -1.01561,2.10725 -1.79226,3.34011 -2.88933,4.58383 -0.32587,0.36931 -1.38493,1.31975 -1.42838,1.2763 -0.005,-0.005 0.0706,-0.34216 0.1738,-0.74406 0.24983,-0.97759 0.34215,-1.56958 0.3856,-2.41683 0.0706,-1.58044 -0.27155,-3.09571 -0.98302,-4.30684 -1.20027,-2.05295 -3.17175,-3.41072 -5.47453,-3.78547 -0.11405,-0.0163 -0.20638,-0.0489 -0.20638,-0.076 0,-0.0217 0.19552,-0.53768 0.42905,-1.15139 1.41752,-3.67684 2.66666,-5.83298 4.30142,-7.40799 1.0482,-1.01562 1.70536,-1.40665 2.73726,-1.62933 0.51596,-0.11405 1.49355,-0.0978 2.03666,0.0272 z m -10.34078,17.93885 c 0.52139,0.54311 0.56483,0.76579 0.46164,2.25933 l -0.0326,0.51596 -0.14121,-0.21725 c -0.22811,-0.34215 -0.40733,-0.72233 -0.52682,-1.1188 -0.0652,-0.20095 -0.15207,-0.43992 -0.20095,-0.53224 -0.0706,-0.13035 -0.17922,-0.91243 -0.19008,-1.34691 0,-0.11949 0.29871,0.0923 0.63,0.43991 z m -7.36997,3.01425 c 0.3856,2.28649 1.18397,4.05159 2.44941,5.40393 l 0.45078,0.47793 -0.13577,0.14664 c -0.0706,0.0815 -0.46165,0.51052 -0.86355,0.9613 -1.55328,1.73795 -2.81873,2.98167 -4.05158,3.97012 -0.41819,0.34216 -0.78208,0.61915 -0.79837,0.61915 -0.0163,0 -0.0435,-0.0923 -0.0652,-0.20638 -0.076,-0.4019 -0.46708,-1.4664 -0.8038,-2.15614 -0.54311,-1.12424 -1.14596,-2.0095 -2.08554,-3.0577 l -0.45621,-0.50509 0.41276,-0.50509 c 1.19484,-1.47182 2.92192,-3.26951 4.43177,-4.62728 0.85811,-0.76578 1.37949,-1.21656 1.39578,-1.20027 0.005,0.005 0.0597,0.315 0.11949,0.67888 z m -16.52135,9.77052 c -0.0163,0.11405 -0.0815,0.54311 -0.14664,0.9613 -0.22267,1.47182 -0.23353,3.57365 -0.0272,4.78478 0.19008,1.10251 0.57569,2.11812 1.08078,2.81873 0.27699,0.38018 0.87441,0.97759 1.22199,1.20027 l 0.23354,0.1575 -0.15207,0.12492 c -0.60285,0.48879 -2.54174,1.58044 -4.18193,2.34622 -2.4114,1.12967 -4.36659,1.7651 -6.62049,2.16157 -0.77664,0.13578 -0.99932,0.15207 -2.09096,0.15207 -0.98846,0 -1.30889,-0.0217 -1.67278,-0.0978 -1.5207,-0.33672 -2.53088,-0.97216 -3.1989,-2.0095 -0.53225,-0.82552 -0.72234,-1.48268 -0.72777,-2.43855 0,-1.56415 0.57027,-2.68296 2.17244,-4.27969 1.78682,-1.77597 3.93753,-3.05227 7.72299,-4.5784 2.01493,-0.81467 4.20366,-1.37407 5.75151,-1.4664 0.74406,-0.0434 0.66803,-0.0652 0.63544,0.16294 z m 6.13712,3.5302 c -0.0163,0.0543 -0.0272,0.0109 -0.0272,-0.0923 0,-0.10319 0.0109,-0.14664 0.0272,-0.0978 0.0109,0.0543 0.0109,0.14121 0,0.19009 z"
1379
     id="path8164"
1380
     style="fill:#df4d65;fill-opacity:1;stroke:none;stroke-width:0.00543108" />
1381
  <g
1382
     transform="translate(1.378418e-5,1.0193503)"
1383
     id="g1168">
1384
    <text
1385
       id="text1158"
1386
       y="364.17905"
1387
       x="349.05551"
1388
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1389
       xml:space="preserve"><tspan
1390
         id="tspan1156"
1391
         sodipodi:role="line"
1392
         x="349.05551"
1393
         y="364.17905">Processor</tspan></text>
1394
    <text
1395
       xml:space="preserve"
1396
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1397
       x="370.40707"
1398
       y="392.17905"
1399
       id="text1162"><tspan
1400
         y="392.17905"
1401
         x="370.40707"
1402
         sodipodi:role="line"
1403
         id="tspan1160">Chain</tspan></text>
1404
  </g>
1405
  <g
1406
     transform="translate(0,-2.3144459)"
1407
     id="g1206">
1408
    <text
1409
       xml:space="preserve"
1410
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1411
       x="586.44855"
1412
       y="327.56967"
1413
       id="text1190"><tspan
1414
         y="327.56967"
1415
         x="586.44855"
1416
         sodipodi:role="line"
1417
         id="tspan1188">Processor-</tspan></text>
1418
    <text
1419
       id="text1194"
1420
       y="355.56967"
1421
       x="588.43488"
1422
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1423
       xml:space="preserve"><tspan
1424
         id="tspan1192"
1425
         sodipodi:role="line"
1426
         x="588.43488"
1427
         y="355.56967">dependent</tspan></text>
1428
  </g>
1429
</svg>
11430
A docs/images/architecture/logos/html5.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
2
	<title>HTML5 Logo</title>
3
	<path d="M108.4 0h23v22.8h21.2V0h23v69h-23V46h-21v23h-23.2M206 23h-20.3V0h63.7v23H229v46h-23M259.5 0h24.1l14.8 24.3L313.2 0h24.1v69h-23V34.8l-16.1 24.8l-16.1-24.8v34.2h-22.6M348.7 0h23v46.2h32.6V69h-55.6"/>
4
	<path fill="#e44d26" d="M107.6 471l-33-370.4h362.8l-33 370.2L255.7 512"/>
5
	<path fill="#f16529" d="M256 480.5V131H404.3L376 447"/>
6
	<path fill="#ebebeb" d="M142 176.3h114v45.4h-64.2l4.2 46.5h60v45.3H154.4M156.4 336.3H202l3.2 36.3 50.8 13.6v47.4l-93.2-26"/>
7
	<path fill="#fff" d="M369.6 176.3H255.8v45.4h109.6M361.3 268.2H255.8v45.4h56l-5.3 59-50.7 13.6v47.2l93-25.8"/>
8
</svg>
A docs/images/architecture/logos/links.svg
1
<?xml version="1.0" standalone="no"?>
2
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
3
 "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
4
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
5
 width="1280.000000pt" height="1123.000000pt" viewBox="0 0 1280.000000 1123.000000"
6
 preserveAspectRatio="xMidYMid meet">
7
<metadata>
8
Created by potrace 1.15, written by Peter Selinger 2001-2017
9
</metadata>
10
<g transform="translate(0.000000,1123.000000) scale(0.100000,-0.100000)"
11
fill="#000000" stroke="none">
12
<path d="M10280 11220 c-519 -65 -1029 -329 -1455 -755 -330 -329 -633 -774
13
-924 -1355 -236 -472 -422 -961 -534 -1398 l-32 -127 -127 -65 c-178 -91 -334
14
-187 -511 -313 -607 -434 -1133 -956 -1571 -1559 -43 -60 -84 -108 -92 -108
15
-7 0 -62 8 -121 17 -136 22 -589 25 -758 5 -699 -85 -1336 -290 -2102 -678
16
-553 -280 -909 -529 -1258 -879 -438 -438 -700 -924 -776 -1440 -18 -115 -18
17
-441 0 -560 94 -654 471 -1279 971 -1610 580 -384 1363 -489 2260 -304 667
18
139 1326 395 1984 773 639 367 1082 770 1394 1266 50 80 66 97 98 109 515 186
19
874 378 1282 686 372 280 632 526 1123 1063 126 138 148 169 155 214 6 32 10
20
37 28 33 95 -19 406 -22 543 -5 384 47 747 203 1108 475 482 364 932 972 1250
21
1690 478 1080 659 1990 555 2797 -26 207 -52 322 -110 500 -79 239 -200 471
22
-348 668 -80 105 -290 314 -395 392 -293 217 -725 393 -1112 453 -122 19 -423
23
27 -525 15z m450 -1440 c358 -86 592 -377 650 -810 15 -108 15 -387 0 -505
24
-59 -484 -233 -1050 -481 -1563 -187 -388 -330 -615 -532 -844 -60 -68 -255
25
-243 -263 -235 -1 1 13 63 32 137 46 180 63 289 71 445 13 291 -50 570 -181
26
793 -221 378 -584 628 -1008 697 -21 3 -38 9 -38 14 0 4 36 99 79 212 261 677
27
491 1074 792 1364 193 187 314 259 504 300 95 21 275 18 375 -5z m-1904 -3303
28
c96 -100 104 -141 85 -416 l-6 -95 -26 40 c-42 63 -75 133 -97 206 -12 37 -28
29
81 -37 98 -13 24 -33 168 -35 248 0 22 55 -17 116 -81z m-1357 -555 c71 -421
30
218 -746 451 -995 l83 -88 -25 -27 c-13 -15 -85 -94 -159 -177 -286 -320 -519
31
-549 -746 -731 -77 -63 -144 -114 -147 -114 -3 0 -8 17 -12 38 -14 74 -86 270
32
-148 397 -100 207 -211 370 -384 563 l-84 93 76 93 c220 271 538 602 816 852
33
158 141 254 224 257 221 1 -1 11 -58 22 -125z m-3042 -1799 c-3 -21 -15 -100
34
-27 -177 -41 -271 -43 -658 -5 -881 35 -203 106 -390 199 -519 51 -70 161
35
-180 225 -221 l43 -29 -28 -23 c-111 -90 -468 -291 -770 -432 -444 -208 -804
36
-325 -1219 -398 -143 -25 -184 -28 -385 -28 -182 0 -241 4 -308 18 -280 62
37
-466 179 -589 370 -98 152 -133 273 -134 449 0 288 105 494 400 788 329 327
38
725 562 1422 843 371 150 774 253 1059 270 137 8 123 12 117 -30z m1130 -650
39
c-3 -10 -5 -2 -5 17 0 19 2 27 5 18 2 -10 2 -26 0 -35z"/>
40
</g>
41
</svg>
142
A docs/images/architecture/logos/markdown.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="208" height="128" viewBox="0 0 208 128"><rect width="198" height="118" x="5" y="5" ry="10" stroke="#000" stroke-width="10" fill="none"/><path d="M30 98V30h20l20 25 20-25h20v68H90V59L70 84 50 59v39zm125 0l-30-33h20V30h20v35h20z"/></svg>
A docs/images/black-text.png
Binary file
A docs/images/blocked-text.png
Binary file
A docs/images/equations.png
Binary file
A docs/images/resolved-text.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>
A docs/quadratic.Rmd
1
![Logo](images/app-title)
2
3
Given the quadratic formula:
4
5
$x = \frac{-b \pm \sqrt{b^2 -4ac}}{2a}$
6
7
Formatted in an R Markdown document as follows:
8
9
    $x = \frac{-b \pm \sqrt{b^2 -4ac}}{2a}$
10
11
We can substitute the following values:
12
13
$a = `r# x(v$formula$quadratic$a)`, b = `r# x(v$formula$quadratic$b)`, c = `r# x(v$formula$quadratic$c)`$
14
15
`r# -x(v$formula$quadratic$b) + sqrt( v$formula$quadratic$b^2  - 4 * v$formula$quadratic$a * v$formula$quadratic$c )`
16
17
To arrive at two solutions:
18
19
$x = \frac{-b + \sqrt{b^2 -4ac}}{2a} = `r# (-x(v$formula$quadratic$b) + sqrt( x(v$formula$quadratic$b)^2  - 4 * x(v$formula$quadratic$a) * x(v$formula$quadratic$c) )) / (2 * x(v$formula$quadratic$a))`$
20
21
$x = \frac{-b - \sqrt{b^2 -4ac}}{2a} = `r# (-x(v$formula$quadratic$b) - sqrt( x(v$formula$quadratic$b)^2  - 4 * x(v$formula$quadratic$a) * x(v$formula$quadratic$c) )) / (2 * x(v$formula$quadratic$a))`$
22
23
Changing the variable values is reflected in the output immediately.
124
A docs/r.md
1
# Introduction
2
3
This document describes how to use the [R](https://www.r-project.org/)
4
programming language from within the application. The application uses an
5
interpreter known as [Renjin](https://www.renjin.org/) to integrate with R.
6
7
# Hello world
8
9
Complete the following steps to see R in action:
10
11
1. Start the application.
12
1. Click **File → New** to create a new file.
13
1. Click **File → Save As**.
14
1. Set **Name** to: `addition.Rmd`
15
1. Click **Save**.
16
17
Setting the file name extension tells the application what processor to
18
use when transforming the contents for display in the preview pane. Continue
19
by typing in the following text, including the backticks:
20
21
```r
22
`r#1 + 1`
23
```
24
25
The preview pane shows the result of `1` plus `1`:
26
27
```
28
2.0
29
```
30
31
# Bootstrap script
32
33
Being able to run R code while editing an R Markdown document is convenient.
34
Having the ability to call functions is where the power of R can be
35
leveraged.
36
37
Complete the following steps to call an R function from your own library:
38
39
1. Click **File → New** to create a new file.
40
1. Click **File → Save As**.
41
1. Browse to your home directory.
42
1. Set **Name** to: `library.R`.
43
1. Click **Save**.
44
1. Set the contents to:
45
    ``` r
46
    sum <- function( a, b ) {
47
      a + b
48
    }
49
    ```
50
1. Click the **Save** icon.
51
1. Click **R → Script**.
52
1. Set the **R Startup Script** contents to:
53
    ``` r
54
    source( 'library.R' );
55
    ```
56
1. Click **OK**.
57
1. Create a new file.
58
1. Set the contents to:
59
    ``` r
60
    `r#sum( 5, 5 )`
61
    ```
62
1. Save the file as `sum.R`.
63
64
The preview panel shows the result of calling the `sum` function:
65
66
```
67
10.0
68
```
69
70
This shows how the bootstrap script can load `library.R`, which defines
71
a `sum` function that is called by name in the Markdown document.
72
73
# Working directory
74
75
R files may be sourced from any directory, not just the user's home
76
directory. Accomplish this as follows:
77
78
1. Click **R → Directory**.
79
1. Set **Directory** to a different directory.
80
1. Click **OK**.
81
1. Create the directory if it does not exist.
82
1. Move `library.R` into the directory.
83
1. Append a new function to `library.R` as follows:
84
    ``` r
85
    mul <- function( a, b ) {
86
      a * b
87
    }
88
    ```
89
1. Click **R → Script**.
90
1. Set the **R Startup Script** contents to:
91
    ``` r
92
    setwd( '{{application.r.working.directory}}' );
93
    source( 'library.R' );
94
    ```
95
1. Change `sum.Rmd` to:
96
    ``` r
97
    `r#mul( 5, 5 )`
98
    ```
99
1. Close the file `sum.Rmd`.
100
1. Confirm saving the file when prompted.
101
1. Re-open `sum.Rmd`.
102
103
The preview panel shows:
104
105
```
106
25.0
107
```
108
109
Calling `setwd` using `'{{application.r.working.directory}}'` changes the
110
working directory where the R engine searches for source files.
111
112
# YAML definitions
113
114
To see how variable definitions work in R, try the following:
115
116
1. Create a new file.
117
1. Change the contents to (use spaces not tabs):
118
    ``` yaml
119
    project:
120
      title: Project Title
121
      author: Author Name
122
    ```
123
1. Save the file as `definitions.yaml`.
124
1. Click **File → Open**.
125
1. Set **Source Files** to **Definition Files**.
126
1. Select `definitions.yaml`.
127
1. Click **Open**.
128
1. Open `sum.Rmd` if it is not already open.
129
1. Type: `je`
130
1. Press `Ctrl+Space`
131
132
The editor inserts the following text (matches `je` against Pro**je**ct):
133
134
``` r
135
`r#x( v$project$title )`
136
```
137
138
The preview panel shows:
139
140
```
141
r#x( 'Project Title' )
142
```
143
144
This is because the application inserts definition reference names based
145
on the type of file being edited. By default, the R engine does not have
146
a function named `x` defined.
147
148
Continue as follows:
149
150
1. Click **R → Script**.
151
1. Append the following:
152
    ``` r
153
    x <- function( s ) {
154
      tryCatch( {
155
        r = eval( parse( text = s ) )
156
157
        ifelse( is.atomic( r ), r, s );
158
      },
159
      warning = function( w ) { s },
160
      error = function( e ) { s } )
161
    }
162
    ```
163
1. Click **OK**.
164
1. Close and re-open `sum.Rmd`.
165
166
The preview panel shows:
167
168
```
169
25.0
170
171
Project Title
172
```
173
174
The `x` function attempts to evaluate the expression defined by the YAML
175
variable. This means that the YAML definitions can also include expressions
176
that R is capable of evaluating.
177
178
While the `x` function can be defined within the R Startup Script, it is
179
better practice to put it into its own library so that it can be reused
180
outside of the application.
181
1182
A docs/svg.md
1
# Introduction
2
3
The Scalable Vector Graphics (SVG) drawing software---[Batik](https://xmlgraphics.apache.org/batik/)---that's used by the application may be unable to read certain SVG files produced by [Inkscape](https://inkscape.org/). The result is that embedding the vector graphics files may trigger the following issues:
4
5
* Unable to create nested element
6
* Black blocks, no text displayed
7
* Black text instead of coloured
8
9
The remainder of this document explains these problems and how to fix them.
10
11
# Nested element
12
13
When referencing a vector graphic using Markdown, the status bar may show the following error:
14
15
> The current document is unable to create an element of the requested type (namespace: http://www.w3.org/2000/svg, name: flowRoot).
16
17
This error is due to a version mismatch of the `flowRoot` element that Inkscape creates.
18
19
## Fix
20
21
Resolve the issue by changing the SVG version number as follows:
22
23
1. Edit the vector graphics file using any text editor.
24
1. Find `version="1.1"` and change it to `version="1.2"`.
25
1. Save the file.
26
27
The SVG will now appear inside the application; however, the text may appear as black blocks.
28
29
# Black blocks
30
31
Depending on how text is added to a vector graphic in Inkscape, the text may be inserted within an element called a `flowRoot`. Although Batik recognizes `flowRoot` for SVG version 1.2, it cannot fully interpret the contents. Black blocks are drawn instead of the text, such as those depicted in the following figure:
32
33
![Missing text](images/blocked-text.png)
34
35
## Fix
36
37
Resolve the issue by "unflowing" all text elements as follows:
38
39
1. Start Inkscape.
40
1. Load the SVG file.
41
1. Select all the text elements.
42
1. Click **Text → Unflow**.
43
44
The text may change size and position; recreate the text without dragging using the text tool. After all the text areas have been recreated, continue as follows:
45
46
1. Click **Edit → XML Editor**.
47
1. Expand the **XML Editor** to see more elements.
48
1. Delete all elements named `svg:flowRoot`.
49
1. Save the file.
50
51
When the illustration is reloaded, the black blocks will have disappeared, but the text elements ignore any assigned colour.
52
53
# Black text
54
55
When an SVG `style` attribute contains a reference to `-inkscape-font-specification`, Batik ignores all values that follow said reference. This results in black text, such as:
56
57
![Black text](images/black-text.png)
58
59
## Fix
60
61
Resolve the issue of colourless text as follows:
62
63
1. Open the SVG file in a plain text editor.
64
1. Remove all references `-inkscape-font-specification:'<FONT>';`, including the trailing (or leading) semicolon.
65
1. Save the file.
66
67
When the illustration is reloaded, the colours will have reappeared, such as:
68
69
![Resolved text](images/resolved-text.png)
70
171
A docs/texample.Rmd
1
# ![Logo](images/app-title.png)
2
3
# Real-time equation rendering
4
5
Interpolated variables within R calculations, formatted as an equation:
6
7
$\sqrt{`r#x( v$formula$sqrt$value)`} = \pm `r# round(sqrt(x( v$formula$sqrt$value )),5)`$
8
9
# Maxwell's equations
10
11
$rot \vec{E} = \frac{1}{c} \frac{\partial{\vec{B}}}{\partial t}, div \vec{B} = 0$
12
13
$rot \vec{B} = \frac{1}{c} \frac{\partial{\vec{E}}}{\partial t} + \frac{4\pi}{c} \vec{j}, div \vec{E} = 4 \pi \rho_{\varepsilon}$
14
15
# Time-dependent Schrödinger equation
16
17
$- \frac{{\hbar ^2 }}{{2m}}\frac{{\partial ^2 \psi (x,t)}}{{\partial x^2 }} + U(x)\psi (x,t) = i\hbar \frac{{\partial \psi (x,t)}}{{\partial t}}$
18
19
# Discrete-time Fourier transforms
20
21
Unit step function: $u(n) \Leftrightarrow \frac{1}{1-e^{-jw}} + \sum_{k=-\infty}^{\infty} \pi \delta (\omega + 2\pi k)$
22
23
Shifted delta: $\delta (n - n_o ) \Leftrightarrow e^{ - j\omega n_o }$
24
25
# Faraday's Law
26
27
$\oint_C {E \cdot d\ell  =  - \frac{d}{{dt}}} \int_S {B_n dA}$
28
29
# Infinite series
30
31
$sin(x) = \sum_{n = 1}^{\infty}  {\frac{{( { - 1})^{n - 1} x^{2n - 1} }}{{( {2n - 1})!}}}$
32
33
# Magnetic flux
34
35
$\phi _m  = \int_S {N{{B}} \cdot {{\hat n}}dA = } \int_S {NB_n dA}$
36
37
# Driven oscillation amplitude
38
39
$A = \frac{{F_0 }}{{\sqrt {m^2 ( {\omega _0^2  - \omega ^2 } )^2  + b^2 \omega ^2 } }}$
40
41
# Optics
42
43
$\phi  = \frac{{2\pi }}{\lambda }a sin(\theta)$
144
A docs/variables.yaml
1
---
2
formula:
3
  sqrt:
4
    value: "420"
5
  quadratic:
6
    a: "25"
7
    b: "84.906"
8
    c: "20"
19
A gradle.properties
1
org.gradle.jvmargs=-Xmx1G -XX:MaxPermSize=512m
2
13
A images/broken-camera.svg
1
<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www.w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c.332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531.03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2.511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-.367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1.699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094.0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5.0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-.195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4.191406-3.652344.207031-.015625.414063-.015625.617187-.007812l.933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2.820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3.429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3.429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 .140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281.808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2.472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4.285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1.9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-.667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1.253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 0'/></g></svg>
12
A images/logo64.png
Binary file
A installer
1
#!/usr/bin/env bash
2
3
# ---------------------------------------------------------------------------
4
# This script cross-compiles application launchers for different platforms.
5
#
6
# The application binaries are self-contained launchers that do not need
7
# to be installed.
8
# ---------------------------------------------------------------------------
9
10
source $HOME/bin/build-template
11
12
readonly APP_NAME=$(find "${SCRIPT_DIR}/src" -type f -name "settings.properties" -exec cat {} \; | grep "application.title=" | cut -d'=' -f2)
13
readonly FILE_APP_JAR="${APP_NAME}.jar"
14
15
ARG_JAVA_OS="linux"
16
ARG_JAVA_ARCH="amd64"
17
ARG_JAVA_VERSION="15.0.1"
18
ARG_JAVA_UPDATE="9"
19
ARG_JAVA_DIR="java"
20
21
ARG_DIR_DIST="dist"
22
23
FILE_DIST_EXEC="run.sh"
24
25
ARG_PATH_DIST_JAR="${SCRIPT_DIR}/build/libs/${FILE_APP_JAR}"
26
27
DEPENDENCIES=(
28
  "gradle,https://gradle.org"
29
  "warp-packer,https://github.com/dgiagio/warp"
30
  "tar,https://www.gnu.org/software/tar"
31
  "unzip,http://infozip.sourceforge.net"
32
)
33
34
ARGUMENTS+=(
35
  "a,arch,Target operating system architecture (amd64)"
36
  "o,os,Target operating system (linux, windows, mac)"
37
  "u,update,Java update version number (${ARG_JAVA_UPDATE})"
38
  "v,version,Full Java version (${ARG_JAVA_VERSION})"
39
)
40
41
ARCHIVE_EXT="tar.gz"
42
ARCHIVE_APP="tar xf"
43
APP_EXTENSION="bin"
44
45
# ---------------------------------------------------------------------------
46
# Generates
47
# ---------------------------------------------------------------------------
48
execute() {
49
  $do_configure_target
50
  $do_build
51
  $do_clean
52
53
  pushd "${ARG_DIR_DIST}" > /dev/null 2>&1
54
55
  $do_extract_java
56
  $do_create_launch_script
57
  $do_copy_archive
58
59
  popd > /dev/null 2>&1
60
61
  $do_create_launcher
62
63
  return 1
64
}
65
66
# ---------------------------------------------------------------------------
67
# Configure platform-specific commands and file names.
68
# ---------------------------------------------------------------------------
69
utile_configure_target() {
70
  if [ "${ARG_JAVA_OS}" = "windows" ]; then
71
    ARCHIVE_EXT="zip"
72
    ARCHIVE_APP="unzip -qq"
73
    FILE_DIST_EXEC="run.bat"
74
    APP_EXTENSION="exe"
75
    do_create_launch_script=utile_create_launch_script_windows
76
  fi
77
}
78
79
# ---------------------------------------------------------------------------
80
# Build platform-specific überjar.
81
# ---------------------------------------------------------------------------
82
utile_build() {
83
  $log "Delete ${ARG_PATH_DIST_JAR}"
84
  rm -f "${ARG_PATH_DIST_JAR}"
85
86
  $log "Build application for ${ARG_JAVA_OS}"
87
  gradle clean jar -PtargetOs="${ARG_JAVA_OS}"
88
}
89
90
# ---------------------------------------------------------------------------
91
# Purges the existing distribution directory to recreate the launcher.
92
# This refreshes the JRE from the downloaded archive.
93
# ---------------------------------------------------------------------------
94
utile_clean() {
95
  $log "Recreate ${ARG_DIR_DIST}"
96
  rm -rf "${ARG_DIR_DIST}"
97
  mkdir -p "${ARG_DIR_DIST}"
98
}
99
100
# ---------------------------------------------------------------------------
101
# Extract platform-specific Java Runtime Environment. This will download
102
# and cache the required Java Runtime Environment for the target platform.
103
# On subsequent runs, the cached version is used, instead of issuing another
104
# download.
105
# ---------------------------------------------------------------------------
106
utile_extract_java() {
107
  $log "Extract Java"
108
  local -r java_vm="jre"
109
  local -r java_version="${ARG_JAVA_VERSION}+${ARG_JAVA_UPDATE}"
110
  local -r url_java="https://download.bell-sw.com/java/${java_version}/bellsoft-${java_vm}${java_version}-${ARG_JAVA_OS}-${ARG_JAVA_ARCH}-full.${ARCHIVE_EXT}"
111
112
  local -r file_java="${java_vm}-${java_version}-${ARG_JAVA_OS}-${ARG_JAVA_ARCH}.${ARCHIVE_EXT}"
113
  local -r path_java="/tmp/${file_java}"
114
115
  # File must have contents.
116
  if [ ! -s ${path_java} ]; then
117
    $log "Download ${url_java} to ${path_java}"
118
    wget -q "${url_java}" -O "${path_java}"
119
  fi
120
121
  $log "Unpack ${path_java}"
122
  $ARCHIVE_APP "${path_java}"
123
124
  local -r dir_java="${java_vm}-${ARG_JAVA_VERSION}-full"
125
126
  $log "Rename ${dir_java} to ${ARG_JAVA_DIR}"
127
  mv "${dir_java}" "${ARG_JAVA_DIR}"
128
}
129
130
# ---------------------------------------------------------------------------
131
# Create Linux-specific launch script.
132
# ---------------------------------------------------------------------------
133
utile_create_launch_script_linux() {
134
  $log "Create Linux launch script"
135
136
  cat > "${FILE_DIST_EXEC}" << __EOT
137
#!/usr/bin/env bash
138
139
readonly SCRIPT_SRC="\$(dirname "\${BASH_SOURCE[\${#BASH_SOURCE[@]} - 1]}")"
140
141
"\${SCRIPT_SRC}/${ARG_JAVA_DIR}/bin/java" -jar "\${SCRIPT_SRC}/${FILE_APP_JAR}" "\$@" 2>&1 >/dev/null &
142
__EOT
143
144
  chmod +x "${FILE_DIST_EXEC}"
145
}
146
147
# ---------------------------------------------------------------------------
148
# Create Windows-specific launch script.
149
# ---------------------------------------------------------------------------
150
utile_create_launch_script_windows() {
151
  $log "Create Windows launch script"
152
153
  cat > "${FILE_DIST_EXEC}" << __EOT
154
@echo off
155
156
set SCRIPT_DIR=%~dp0
157
"%SCRIPT_DIR%\\${ARG_JAVA_DIR}\\bin\\java" -jar "%SCRIPT_DIR%\\${APP_NAME}.jar" %*
158
__EOT
159
160
  # Convert Unix end of line characters (\n) to Windows format (\r\n).
161
  # This avoids any potential line conversion issues with the repository.
162
  sed -i 's/$/\r/' "${FILE_DIST_EXEC}"
163
}
164
165
# ---------------------------------------------------------------------------
166
# Copy application überjar.
167
# ---------------------------------------------------------------------------
168
utile_copy_archive() {
169
  $log "Create copy of ${FILE_APP_JAR}"
170
  cp "${ARG_PATH_DIST_JAR}" "${FILE_APP_JAR}"
171
}
172
173
# ---------------------------------------------------------------------------
174
# Create platform-specific launcher binary.
175
# ---------------------------------------------------------------------------
176
utile_create_launcher() {
177
  local -r FILE_APP_NAME="${APP_NAME}.${APP_EXTENSION}"
178
  $log "Create ${FILE_APP_NAME}"
179
180
  # Warp-packer does not seem to overwrite the file.
181
  rm -f "${FILE_APP_NAME}"
182
183
  # Download uses amd64, but warp-packer differs.
184
  if [ "${ARG_JAVA_ARCH}" = "amd64" ]; then
185
    ARG_JAVA_ARCH="x64"
186
  fi
187
188
  warp-packer \
189
    --arch "${ARG_JAVA_OS}-${ARG_JAVA_ARCH}" \
190
    --input_dir "${ARG_DIR_DIST}" \
191
    --exec "${FILE_DIST_EXEC}" \
192
    --output "${FILE_APP_NAME}" > /dev/null
193
194
  chmod +x "${FILE_APP_NAME}"
195
}
196
197
argument() {
198
  local consume=2
199
200
  case "$1" in
201
    -a|--arch)
202
    ARG_JAVA_ARCH="$2"
203
    ;;
204
    -o|--os)
205
    ARG_JAVA_OS="$2"
206
    ;;
207
    -u|--update)
208
    ARG_JAVA_UPDATE="$2"
209
    ;;
210
    -v|--version)
211
    ARG_JAVA_VERSION="$2"
212
    ;;
213
  esac
214
215
  return ${consume}
216
}
217
218
do_configure_target=utile_configure_target
219
do_build=utile_build
220
do_clean=utile_clean
221
do_extract_java=utile_extract_java
222
do_create_launch_script=utile_create_launch_script_linux
223
do_copy_archive=utile_copy_archive
224
do_create_launcher=utile_create_launcher
225
226
main "$@"
227
1228
A libs/jmathtex.jar
Binary file
A libs/jsymspell/jsymspell-core-1.0-SNAPSHOT-javadoc.jar
Binary file
A libs/jsymspell/jsymspell-core-1.0-SNAPSHOT-sources.jar
Binary file
A libs/jsymspell/jsymspell-core-1.0-SNAPSHOT.jar
Binary file
A 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
A licenses/FILE-PREFERENCES.md
1
Released into the Public Domain by David Croft.
2
3
http://www.davidc.net/programming/java/java-preferences-using-file-backing-store
4
http://creativecommons.org/publicdomain/zero/1.0/
5
6
CC0 1.0 Universal (CC0 1.0)
7
8
Public Domain Dedication
9
10
This is a human-readable summary of the Legal Code (read the full text).
11
12
Disclaimer
13
14
No Copyright
15
16
* The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law.
17
18
* You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission. See Other Information below.
19
20
This license is acceptable for Free Cultural Works.
21
22
Other Information
23
24
* In no way are the patent or trademark rights of any person affected by CC0, nor are the rights that other persons may have in the work or in how the work is used, such as publicity or privacy rights.
25
* Unless expressly stated otherwise, the person who associated a work with this deed makes no warranties about the work, and disclaims liability for all uses of the work, to the fullest extent permitted by applicable law.
26
* When using or citing the work, you should not imply endorsement by the author or the affirmer.
27
128
A licenses/FLEXMARK.md
1
Copyright (c) 2015-2016, Atlassian Pty Ltd
2
All rights reserved.
3
4
Copyright (c) 2016-2018, Vladimir Schneider,
5
All rights reserved.
6
7
Redistribution and use in source and binary forms, with or without
8
modification, are permitted provided that the following conditions are met:
9
10
* Redistributions of source code must retain the above copyright notice, this
11
  list of conditions and the following disclaimer.
12
13
* Redistributions in binary form must reproduce the above copyright notice,
14
  this list of conditions and the following disclaimer in the documentation
15
  and/or other materials provided with the distribution.
16
17
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
127
A licenses/FLOWLESS.md
1
Copyright (c) 2014, TomasMikula
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without
5
modification, are permitted provided that the following conditions are met:
6
7
* Redistributions of source code must retain the above copyright notice, this
8
  list of conditions and the following disclaimer.
9
10
* Redistributions in binary form must reproduce the above copyright notice,
11
  this list of conditions and the following disclaimer in the documentation
12
  and/or other materials provided with the distribution.
13
14
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
124
A licenses/FONT-AWESOME-FX.txt
11
2
                                 Apache License
3
                           Version 2.0, January 2004
4
                        http://www.apache.org/licenses/
5
6
   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
8
   1. Definitions.
9
10
      "License" shall mean the terms and conditions for use, reproduction,
11
      and distribution as defined by Sections 1 through 9 of this document.
12
13
      "Licensor" shall mean the copyright owner or entity authorized by
14
      the copyright owner that is granting the License.
15
16
      "Legal Entity" shall mean the union of the acting entity and all
17
      other entities that control, are controlled by, or are under common
18
      control with that entity. For the purposes of this definition,
19
      "control" means (i) the power, direct or indirect, to cause the
20
      direction or management of such entity, whether by contract or
21
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
      outstanding shares, or (iii) beneficial ownership of such entity.
23
24
      "You" (or "Your") shall mean an individual or Legal Entity
25
      exercising permissions granted by this License.
26
27
      "Source" form shall mean the preferred form for making modifications,
28
      including but not limited to software source code, documentation
29
      source, and configuration files.
30
31
      "Object" form shall mean any form resulting from mechanical
32
      transformation or translation of a Source form, including but
33
      not limited to compiled object code, generated documentation,
34
      and conversions to other media types.
35
36
      "Work" shall mean the work of authorship, whether in Source or
37
      Object form, made available under the License, as indicated by a
38
      copyright notice that is included in or attached to the work
39
      (an example is provided in the Appendix below).
40
41
      "Derivative Works" shall mean any work, whether in Source or Object
42
      form, that is based on (or derived from) the Work and for which the
43
      editorial revisions, annotations, elaborations, or other modifications
44
      represent, as a whole, an original work of authorship. For the purposes
45
      of this License, Derivative Works shall not include works that remain
46
      separable from, or merely link (or bind by name) to the interfaces of,
47
      the Work and Derivative Works thereof.
48
49
      "Contribution" shall mean any work of authorship, including
50
      the original version of the Work and any modifications or additions
51
      to that Work or Derivative Works thereof, that is intentionally
52
      submitted to Licensor for inclusion in the Work by the copyright owner
53
      or by an individual or Legal Entity authorized to submit on behalf of
54
      the copyright owner. For the purposes of this definition, "submitted"
55
      means any form of electronic, verbal, or written communication sent
56
      to the Licensor or its representatives, including but not limited to
57
      communication on electronic mailing lists, source code control systems,
58
      and issue tracking systems that are managed by, or on behalf of, the
59
      Licensor for the purpose of discussing and improving the Work, but
60
      excluding communication that is conspicuously marked or otherwise
61
      designated in writing by the copyright owner as "Not a Contribution."
62
63
      "Contributor" shall mean Licensor and any individual or Legal Entity
64
      on behalf of whom a Contribution has been received by Licensor and
65
      subsequently incorporated within the Work.
66
67
   2. Grant of Copyright License. Subject to the terms and conditions of
68
      this License, each Contributor hereby grants to You a perpetual,
69
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
      copyright license to reproduce, prepare Derivative Works of,
71
      publicly display, publicly perform, sublicense, and distribute the
72
      Work and such Derivative Works in Source or Object form.
73
74
   3. Grant of Patent License. Subject to the terms and conditions of
75
      this License, each Contributor hereby grants to You a perpetual,
76
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
      (except as stated in this section) patent license to make, have made,
78
      use, offer to sell, sell, import, and otherwise transfer the Work,
79
      where such license applies only to those patent claims licensable
80
      by such Contributor that are necessarily infringed by their
81
      Contribution(s) alone or by combination of their Contribution(s)
82
      with the Work to which such Contribution(s) was submitted. If You
83
      institute patent litigation against any entity (including a
84
      cross-claim or counterclaim in a lawsuit) alleging that the Work
85
      or a Contribution incorporated within the Work constitutes direct
86
      or contributory patent infringement, then any patent licenses
87
      granted to You under this License for that Work shall terminate
88
      as of the date such litigation is filed.
89
90
   4. Redistribution. You may reproduce and distribute copies of the
91
      Work or Derivative Works thereof in any medium, with or without
92
      modifications, and in Source or Object form, provided that You
93
      meet the following conditions:
94
95
      (a) You must give any other recipients of the Work or
96
          Derivative Works a copy of this License; and
97
98
      (b) You must cause any modified files to carry prominent notices
99
          stating that You changed the files; and
100
101
      (c) You must retain, in the Source form of any Derivative Works
102
          that You distribute, all copyright, patent, trademark, and
103
          attribution notices from the Source form of the Work,
104
          excluding those notices that do not pertain to any part of
105
          the Derivative Works; and
106
107
      (d) If the Work includes a "NOTICE" text file as part of its
108
          distribution, then any Derivative Works that You distribute must
109
          include a readable copy of the attribution notices contained
110
          within such NOTICE file, excluding those notices that do not
111
          pertain to any part of the Derivative Works, in at least one
112
          of the following places: within a NOTICE text file distributed
113
          as part of the Derivative Works; within the Source form or
114
          documentation, if provided along with the Derivative Works; or,
115
          within a display generated by the Derivative Works, if and
116
          wherever such third-party notices normally appear. The contents
117
          of the NOTICE file are for informational purposes only and
118
          do not modify the License. You may add Your own attribution
119
          notices within Derivative Works that You distribute, alongside
120
          or as an addendum to the NOTICE text from the Work, provided
121
          that such additional attribution notices cannot be construed
122
          as modifying the License.
123
124
      You may add Your own copyright statement to Your modifications and
125
      may provide additional or different license terms and conditions
126
      for use, reproduction, or distribution of Your modifications, or
127
      for any such Derivative Works as a whole, provided Your use,
128
      reproduction, and distribution of the Work otherwise complies with
129
      the conditions stated in this License.
130
131
   5. Submission of Contributions. Unless You explicitly state otherwise,
132
      any Contribution intentionally submitted for inclusion in the Work
133
      by You to the Licensor shall be under the terms and conditions of
134
      this License, without any additional terms or conditions.
135
      Notwithstanding the above, nothing herein shall supersede or modify
136
      the terms of any separate license agreement you may have executed
137
      with Licensor regarding such Contributions.
138
139
   6. Trademarks. This License does not grant permission to use the trade
140
      names, trademarks, service marks, or product names of the Licensor,
141
      except as required for reasonable and customary use in describing the
142
      origin of the Work and reproducing the content of the NOTICE file.
143
144
   7. Disclaimer of Warranty. Unless required by applicable law or
145
      agreed to in writing, Licensor provides the Work (and each
146
      Contributor provides its Contributions) on an "AS IS" BASIS,
147
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
      implied, including, without limitation, any warranties or conditions
149
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
      PARTICULAR PURPOSE. You are solely responsible for determining the
151
      appropriateness of using or redistributing the Work and assume any
152
      risks associated with Your exercise of permissions under this License.
153
154
   8. Limitation of Liability. In no event and under no legal theory,
155
      whether in tort (including negligence), contract, or otherwise,
156
      unless required by applicable law (such as deliberate and grossly
157
      negligent acts) or agreed to in writing, shall any Contributor be
158
      liable to You for damages, including any direct, indirect, special,
159
      incidental, or consequential damages of any character arising as a
160
      result of this License or out of the use or inability to use the
161
      Work (including but not limited to damages for loss of goodwill,
162
      work stoppage, computer failure or malfunction, or any and all
163
      other commercial damages or losses), even if such Contributor
164
      has been advised of the possibility of such damages.
165
166
   9. Accepting Warranty or Additional Liability. While redistributing
167
      the Work or Derivative Works thereof, You may choose to offer,
168
      and charge a fee for, acceptance of support, warranty, indemnity,
169
      or other liability obligations and/or rights consistent with this
170
      License. However, in accepting such obligations, You may act only
171
      on Your own behalf and on Your sole responsibility, not on behalf
172
      of any other Contributor, and only if You agree to indemnify,
173
      defend, and hold each Contributor harmless for any liability
174
      incurred by, or claims asserted against, such Contributor by reason
175
      of your accepting any such warranty or additional liability.
176
177
   END OF TERMS AND CONDITIONS
178
179
   APPENDIX: How to apply the Apache License to your work.
180
181
      To apply the Apache License to your work, attach the following
182
      boilerplate notice, with the fields enclosed by brackets "[]"
183
      replaced with your own identifying information. (Don't include
184
      the brackets!)  The text should be enclosed in the appropriate
185
      comment syntax for the file format. We also recommend that a
186
      file or class name and description of purpose be included on the
187
      same "printed page" as the copyright notice for easier
188
      identification within third-party archives.
189
190
   Copyright [yyyy] [name of copyright owner]
191
192
   Licensed under the Apache License, Version 2.0 (the "License");
193
   you may not use this file except in compliance with the License.
194
   You may obtain a copy of the License at
195
196
       http://www.apache.org/licenses/LICENSE-2.0
197
198
   Unless required by applicable law or agreed to in writing, software
199
   distributed under the License is distributed on an "AS IS" BASIS,
200
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
   See the License for the specific language governing permissions and
202
   limitations under the License.
203
A licenses/JSYMSPELL.md
1
MIT License
2
3
Copyright (c) 2019 Raul Garcia
4
5
Permission is hereby granted, free of charge, to any person obtaining a copy
6
of this software and associated documentation files (the "Software"), to deal
7
in the Software without restriction, including without limitation the rights
8
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
copies of the Software, and to permit persons to whom the Software is
10
furnished to do so, subject to the following conditions:
11
12
The above copyright notice and this permission notice shall be included in all
13
copies or substantial portions of the Software.
14
15
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
SOFTWARE.
122
A licenses/JUNIVERSAL-CHARDET.md
1
Version: MPL 1.1/GPL 2.0/LGPL 2.1
2
3
The contents of this file are subject to the Mozilla Public License Version
4
1.1 (the "License"); you may not use this file except in compliance with
5
the License. You may obtain a copy of the License at
6
http://www.mozilla.org/MPL/
7
8
Software distributed under the License is distributed on an "AS IS" basis,
9
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
10
for the specific language governing rights and limitations under the
11
License.
12
13
The Original Code is Mozilla Universal charset detector code.
14
15
The Initial Developer of the Original Code is
16
Netscape Communications Corporation.
17
Portions created by the Initial Developer are Copyright (C) 2001
18
the Initial Developer. All Rights Reserved.
19
20
Contributor(s):
21
        Shy Shalom <shooshX@gmail.com>
22
        Kohei TAKETA <k-tak@void.in> (Java port)
23
24
Alternatively, the contents of this file may be used under the terms of
25
either the GNU General Public License Version 2 or later (the "GPL"), or
26
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27
in which case the provisions of the GPL or the LGPL are applicable instead
28
of those above. If you wish to allow use of your version of this file only
29
under the terms of either the GPL or the LGPL, and not to allow others to
30
use your version of this file under the terms of the MPL, indicate your
31
decision by deleting the provisions above and replace them with the notice
32
and other provisions required by the GPL or the LGPL. If you do not delete
33
the provisions above, a recipient may use your version of this file under
34
the terms of any one of the MPL, the GPL or the LGPL.
35
136
A licenses/MARKDOWN-WRITER-FX.md
1
Copyright (c) 2015 Karl Tauber <karl@jformdesigner.com>
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without
5
modification, are permitted provided that the following conditions are met:
6
7
* Redistributions of source code must retain the above copyright
8
  notice, this list of conditions and the following disclaimer.
9
10
* Redistributions in binary form must reproduce the above copyright
11
  notice, this list of conditions and the following disclaimer in the
12
  documentation and/or other materials provided with the distribution.
13
14
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
125
A licenses/MIG-LAYOUT.md
1
Copyright (c) 2000 Mikael Grev
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without
5
modification, are permitted provided that the following conditions
6
are met:
7
1. Redistributions of source code must retain the above copyright
8
   notice, this list of conditions and the following disclaimer.
9
2. Redistributions in binary form must reproduce the above copyright
10
   notice, this list of conditions and the following disclaimer in the
11
   documentation and/or other materials provided with the distribution.
12
3. The name of the author may not be used to endorse or promote products
13
   derived from this software without specific prior written permission.
14
15
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
16
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
126
A licenses/PREFERENCES-FX.txt
1
                                 Apache License
2
                           Version 2.0, January 2004
3
                        http://www.apache.org/licenses/
4
5
   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
7
   1. Definitions.
8
9
      "License" shall mean the terms and conditions for use, reproduction,
10
      and distribution as defined by Sections 1 through 9 of this document.
11
12
      "Licensor" shall mean the copyright owner or entity authorized by
13
      the copyright owner that is granting the License.
14
15
      "Legal Entity" shall mean the union of the acting entity and all
16
      other entities that control, are controlled by, or are under common
17
      control with that entity. For the purposes of this definition,
18
      "control" means (i) the power, direct or indirect, to cause the
19
      direction or management of such entity, whether by contract or
20
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
      outstanding shares, or (iii) beneficial ownership of such entity.
22
23
      "You" (or "Your") shall mean an individual or Legal Entity
24
      exercising permissions granted by this License.
25
26
      "Source" form shall mean the preferred form for making modifications,
27
      including but not limited to software source code, documentation
28
      source, and configuration files.
29
30
      "Object" form shall mean any form resulting from mechanical
31
      transformation or translation of a Source form, including but
32
      not limited to compiled object code, generated documentation,
33
      and conversions to other media types.
34
35
      "Work" shall mean the work of authorship, whether in Source or
36
      Object form, made available under the License, as indicated by a
37
      copyright notice that is included in or attached to the work
38
      (an example is provided in the Appendix below).
39
40
      "Derivative Works" shall mean any work, whether in Source or Object
41
      form, that is based on (or derived from) the Work and for which the
42
      editorial revisions, annotations, elaborations, or other modifications
43
      represent, as a whole, an original work of authorship. For the purposes
44
      of this License, Derivative Works shall not include works that remain
45
      separable from, or merely link (or bind by name) to the interfaces of,
46
      the Work and Derivative Works thereof.
47
48
      "Contribution" shall mean any work of authorship, including
49
      the original version of the Work and any modifications or additions
50
      to that Work or Derivative Works thereof, that is intentionally
51
      submitted to Licensor for inclusion in the Work by the copyright owner
52
      or by an individual or Legal Entity authorized to submit on behalf of
53
      the copyright owner. For the purposes of this definition, "submitted"
54
      means any form of electronic, verbal, or written communication sent
55
      to the Licensor or its representatives, including but not limited to
56
      communication on electronic mailing lists, source code control systems,
57
      and issue tracking systems that are managed by, or on behalf of, the
58
      Licensor for the purpose of discussing and improving the Work, but
59
      excluding communication that is conspicuously marked or otherwise
60
      designated in writing by the copyright owner as "Not a Contribution."
61
62
      "Contributor" shall mean Licensor and any individual or Legal Entity
63
      on behalf of whom a Contribution has been received by Licensor and
64
      subsequently incorporated within the Work.
65
66
   2. Grant of Copyright License. Subject to the terms and conditions of
67
      this License, each Contributor hereby grants to You a perpetual,
68
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
      copyright license to reproduce, prepare Derivative Works of,
70
      publicly display, publicly perform, sublicense, and distribute the
71
      Work and such Derivative Works in Source or Object form.
72
73
   3. Grant of Patent License. Subject to the terms and conditions of
74
      this License, each Contributor hereby grants to You a perpetual,
75
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
      (except as stated in this section) patent license to make, have made,
77
      use, offer to sell, sell, import, and otherwise transfer the Work,
78
      where such license applies only to those patent claims licensable
79
      by such Contributor that are necessarily infringed by their
80
      Contribution(s) alone or by combination of their Contribution(s)
81
      with the Work to which such Contribution(s) was submitted. If You
82
      institute patent litigation against any entity (including a
83
      cross-claim or counterclaim in a lawsuit) alleging that the Work
84
      or a Contribution incorporated within the Work constitutes direct
85
      or contributory patent infringement, then any patent licenses
86
      granted to You under this License for that Work shall terminate
87
      as of the date such litigation is filed.
88
89
   4. Redistribution. You may reproduce and distribute copies of the
90
      Work or Derivative Works thereof in any medium, with or without
91
      modifications, and in Source or Object form, provided that You
92
      meet the following conditions:
93
94
      (a) You must give any other recipients of the Work or
95
          Derivative Works a copy of this License; and
96
97
      (b) You must cause any modified files to carry prominent notices
98
          stating that You changed the files; and
99
100
      (c) You must retain, in the Source form of any Derivative Works
101
          that You distribute, all copyright, patent, trademark, and
102
          attribution notices from the Source form of the Work,
103
          excluding those notices that do not pertain to any part of
104
          the Derivative Works; and
105
106
      (d) If the Work includes a "NOTICE" text file as part of its
107
          distribution, then any Derivative Works that You distribute must
108
          include a readable copy of the attribution notices contained
109
          within such NOTICE file, excluding those notices that do not
110
          pertain to any part of the Derivative Works, in at least one
111
          of the following places: within a NOTICE text file distributed
112
          as part of the Derivative Works; within the Source form or
113
          documentation, if provided along with the Derivative Works; or,
114
          within a display generated by the Derivative Works, if and
115
          wherever such third-party notices normally appear. The contents
116
          of the NOTICE file are for informational purposes only and
117
          do not modify the License. You may add Your own attribution
118
          notices within Derivative Works that You distribute, alongside
119
          or as an addendum to the NOTICE text from the Work, provided
120
          that such additional attribution notices cannot be construed
121
          as modifying the License.
122
123
      You may add Your own copyright statement to Your modifications and
124
      may provide additional or different license terms and conditions
125
      for use, reproduction, or distribution of Your modifications, or
126
      for any such Derivative Works as a whole, provided Your use,
127
      reproduction, and distribution of the Work otherwise complies with
128
      the conditions stated in this License.
129
130
   5. Submission of Contributions. Unless You explicitly state otherwise,
131
      any Contribution intentionally submitted for inclusion in the Work
132
      by You to the Licensor shall be under the terms and conditions of
133
      this License, without any additional terms or conditions.
134
      Notwithstanding the above, nothing herein shall supersede or modify
135
      the terms of any separate license agreement you may have executed
136
      with Licensor regarding such Contributions.
137
138
   6. Trademarks. This License does not grant permission to use the trade
139
      names, trademarks, service marks, or product names of the Licensor,
140
      except as required for reasonable and customary use in describing the
141
      origin of the Work and reproducing the content of the NOTICE file.
142
143
   7. Disclaimer of Warranty. Unless required by applicable law or
144
      agreed to in writing, Licensor provides the Work (and each
145
      Contributor provides its Contributions) on an "AS IS" BASIS,
146
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
      implied, including, without limitation, any warranties or conditions
148
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
      PARTICULAR PURPOSE. You are solely responsible for determining the
150
      appropriateness of using or redistributing the Work and assume any
151
      risks associated with Your exercise of permissions under this License.
152
153
   8. Limitation of Liability. In no event and under no legal theory,
154
      whether in tort (including negligence), contract, or otherwise,
155
      unless required by applicable law (such as deliberate and grossly
156
      negligent acts) or agreed to in writing, shall any Contributor be
157
      liable to You for damages, including any direct, indirect, special,
158
      incidental, or consequential damages of any character arising as a
159
      result of this License or out of the use or inability to use the
160
      Work (including but not limited to damages for loss of goodwill,
161
      work stoppage, computer failure or malfunction, or any and all
162
      other commercial damages or losses), even if such Contributor
163
      has been advised of the possibility of such damages.
164
165
   9. Accepting Warranty or Additional Liability. While redistributing
166
      the Work or Derivative Works thereof, You may choose to offer,
167
      and charge a fee for, acceptance of support, warranty, indemnity,
168
      or other liability obligations and/or rights consistent with this
169
      License. However, in accepting such obligations, You may act only
170
      on Your own behalf and on Your sole responsibility, not on behalf
171
      of any other Contributor, and only if You agree to indemnify,
172
      defend, and hold each Contributor harmless for any liability
173
      incurred by, or claims asserted against, such Contributor by reason
174
      of your accepting any such warranty or additional liability.
175
176
   END OF TERMS AND CONDITIONS
177
178
   APPENDIX: How to apply the Apache License to your work.
179
180
      To apply the Apache License to your work, attach the following
181
      boilerplate notice, with the fields enclosed by brackets "{}"
182
      replaced with your own identifying information. (Don't include
183
      the brackets!)  The text should be enclosed in the appropriate
184
      comment syntax for the file format. We also recommend that a
185
      file or class name and description of purpose be included on the
186
      same "printed page" as the copyright notice for easier
187
      identification within third-party archives.
188
189
   Copyright {yyyy} {name of copyright owner}
190
191
   Licensed under the Apache License, Version 2.0 (the "License");
192
   you may not use this file except in compliance with the License.
193
   You may obtain a copy of the License at
194
195
       http://www.apache.org/licenses/LICENSE-2.0
196
197
   Unless required by applicable law or agreed to in writing, software
198
   distributed under the License is distributed on an "AS IS" BASIS,
199
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
   See the License for the specific language governing permissions and
201
   limitations under the License.
1202
A licenses/REACT-FX.md
1
Copyright (c) 2013-2014, Tomas Mikula
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
6
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
8
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
10
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
111
A licenses/RENJIN.txt
1
		    GNU GENERAL PUBLIC LICENSE
2
		       Version 2, June 1991
3
4
 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
5
                       51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
6
 Everyone is permitted to copy and distribute verbatim copies
7
 of this license document, but changing it is not allowed.
8
9
			    Preamble
10
11
  The licenses for most software are designed to take away your
12
freedom to share and change it.  By contrast, the GNU General Public
13
License is intended to guarantee your freedom to share and change free
14
software--to make sure the software is free for all its users.  This
15
General Public License applies to most of the Free Software
16
Foundation's software and to any other program whose authors commit to
17
using it.  (Some other Free Software Foundation software is covered by
18
the GNU Library General Public License instead.)  You can apply it to
19
your programs, too.
20
21
  When we speak of free software, we are referring to freedom, not
22
price.  Our General Public Licenses are designed to make sure that you
23
have the freedom to distribute copies of free software (and charge for
24
this service if you wish), that you receive source code or can get it
25
if you want it, that you can change the software or use pieces of it
26
in new free programs; and that you know you can do these things.
27
28
  To protect your rights, we need to make restrictions that forbid
29
anyone to deny you these rights or to ask you to surrender the rights.
30
These restrictions translate to certain responsibilities for you if you
31
distribute copies of the software, or if you modify it.
32
33
  For example, if you distribute copies of such a program, whether
34
gratis or for a fee, you must give the recipients all the rights that
35
you have.  You must make sure that they, too, receive or can get the
36
source code.  And you must show them these terms so they know their
37
rights.
38
39
  We protect your rights with two steps: (1) copyright the software, and
40
(2) offer you this license which gives you legal permission to copy,
41
distribute and/or modify the software.
42
43
  Also, for each author's protection and ours, we want to make certain
44
that everyone understands that there is no warranty for this free
45
software.  If the software is modified by someone else and passed on, we
46
want its recipients to know that what they have is not the original, so
47
that any problems introduced by others will not reflect on the original
48
authors' reputations.
49
50
  Finally, any free program is threatened constantly by software
51
patents.  We wish to avoid the danger that redistributors of a free
52
program will individually obtain patent licenses, in effect making the
53
program proprietary.  To prevent this, we have made it clear that any
54
patent must be licensed for everyone's free use or not licensed at all.
55
56
  The precise terms and conditions for copying, distribution and
57
modification follow.
58

59
		    GNU GENERAL PUBLIC LICENSE
60
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61
62
  0. This License applies to any program or other work which contains
63
a notice placed by the copyright holder saying it may be distributed
64
under the terms of this General Public License.  The "Program", below,
65
refers to any such program or work, and a "work based on the Program"
66
means either the Program or any derivative work under copyright law:
67
that is to say, a work containing the Program or a portion of it,
68
either verbatim or with modifications and/or translated into another
69
language.  (Hereinafter, translation is included without limitation in
70
the term "modification".)  Each licensee is addressed as "you".
71
72
Activities other than copying, distribution and modification are not
73
covered by this License; they are outside its scope.  The act of
74
running the Program is not restricted, and the output from the Program
75
is covered only if its contents constitute a work based on the
76
Program (independent of having been made by running the Program).
77
Whether that is true depends on what the Program does.
78
79
  1. You may copy and distribute verbatim copies of the Program's
80
source code as you receive it, in any medium, provided that you
81
conspicuously and appropriately publish on each copy an appropriate
82
copyright notice and disclaimer of warranty; keep intact all the
83
notices that refer to this License and to the absence of any warranty;
84
and give any other recipients of the Program a copy of this License
85
along with the Program.
86
87
You may charge a fee for the physical act of transferring a copy, and
88
you may at your option offer warranty protection in exchange for a fee.
89
90
  2. You may modify your copy or copies of the Program or any portion
91
of it, thus forming a work based on the Program, and copy and
92
distribute such modifications or work under the terms of Section 1
93
above, provided that you also meet all of these conditions:
94
95
    a) You must cause the modified files to carry prominent notices
96
    stating that you changed the files and the date of any change.
97
98
    b) You must cause any work that you distribute or publish, that in
99
    whole or in part contains or is derived from the Program or any
100
    part thereof, to be licensed as a whole at no charge to all third
101
    parties under the terms of this License.
102
103
    c) If the modified program normally reads commands interactively
104
    when run, you must cause it, when started running for such
105
    interactive use in the most ordinary way, to print or display an
106
    announcement including an appropriate copyright notice and a
107
    notice that there is no warranty (or else, saying that you provide
108
    a warranty) and that users may redistribute the program under
109
    these conditions, and telling the user how to view a copy of this
110
    License.  (Exception: if the Program itself is interactive but
111
    does not normally print such an announcement, your work based on
112
    the Program is not required to print an announcement.)
113

114
These requirements apply to the modified work as a whole.  If
115
identifiable sections of that work are not derived from the Program,
116
and can be reasonably considered independent and separate works in
117
themselves, then this License, and its terms, do not apply to those
118
sections when you distribute them as separate works.  But when you
119
distribute the same sections as part of a whole which is a work based
120
on the Program, the distribution of the whole must be on the terms of
121
this License, whose permissions for other licensees extend to the
122
entire whole, and thus to each and every part regardless of who wrote it.
123
124
Thus, it is not the intent of this section to claim rights or contest
125
your rights to work written entirely by you; rather, the intent is to
126
exercise the right to control the distribution of derivative or
127
collective works based on the Program.
128
129
In addition, mere aggregation of another work not based on the Program
130
with the Program (or with a work based on the Program) on a volume of
131
a storage or distribution medium does not bring the other work under
132
the scope of this License.
133
134
  3. You may copy and distribute the Program (or a work based on it,
135
under Section 2) in object code or executable form under the terms of
136
Sections 1 and 2 above provided that you also do one of the following:
137
138
    a) Accompany it with the complete corresponding machine-readable
139
    source code, which must be distributed under the terms of Sections
140
    1 and 2 above on a medium customarily used for software interchange; or,
141
142
    b) Accompany it with a written offer, valid for at least three
143
    years, to give any third party, for a charge no more than your
144
    cost of physically performing source distribution, a complete
145
    machine-readable copy of the corresponding source code, to be
146
    distributed under the terms of Sections 1 and 2 above on a medium
147
    customarily used for software interchange; or,
148
149
    c) Accompany it with the information you received as to the offer
150
    to distribute corresponding source code.  (This alternative is
151
    allowed only for noncommercial distribution and only if you
152
    received the program in object code or executable form with such
153
    an offer, in accord with Subsection b above.)
154
155
The source code for a work means the preferred form of the work for
156
making modifications to it.  For an executable work, complete source
157
code means all the source code for all modules it contains, plus any
158
associated interface definition files, plus the scripts used to
159
control compilation and installation of the executable.  However, as a
160
special exception, the source code distributed need not include
161
anything that is normally distributed (in either source or binary
162
form) with the major components (compiler, kernel, and so on) of the
163
operating system on which the executable runs, unless that component
164
itself accompanies the executable.
165
166
If distribution of executable or object code is made by offering
167
access to copy from a designated place, then offering equivalent
168
access to copy the source code from the same place counts as
169
distribution of the source code, even though third parties are not
170
compelled to copy the source along with the object code.
171

172
  4. You may not copy, modify, sublicense, or distribute the Program
173
except as expressly provided under this License.  Any attempt
174
otherwise to copy, modify, sublicense or distribute the Program is
175
void, and will automatically terminate your rights under this License.
176
However, parties who have received copies, or rights, from you under
177
this License will not have their licenses terminated so long as such
178
parties remain in full compliance.
179
180
  5. You are not required to accept this License, since you have not
181
signed it.  However, nothing else grants you permission to modify or
182
distribute the Program or its derivative works.  These actions are
183
prohibited by law if you do not accept this License.  Therefore, by
184
modifying or distributing the Program (or any work based on the
185
Program), you indicate your acceptance of this License to do so, and
186
all its terms and conditions for copying, distributing or modifying
187
the Program or works based on it.
188
189
  6. Each time you redistribute the Program (or any work based on the
190
Program), the recipient automatically receives a license from the
191
original licensor to copy, distribute or modify the Program subject to
192
these terms and conditions.  You may not impose any further
193
restrictions on the recipients' exercise of the rights granted herein.
194
You are not responsible for enforcing compliance by third parties to
195
this License.
196
197
  7. If, as a consequence of a court judgment or allegation of patent
198
infringement or for any other reason (not limited to patent issues),
199
conditions are imposed on you (whether by court order, agreement or
200
otherwise) that contradict the conditions of this License, they do not
201
excuse you from the conditions of this License.  If you cannot
202
distribute so as to satisfy simultaneously your obligations under this
203
License and any other pertinent obligations, then as a consequence you
204
may not distribute the Program at all.  For example, if a patent
205
license would not permit royalty-free redistribution of the Program by
206
all those who receive copies directly or indirectly through you, then
207
the only way you could satisfy both it and this License would be to
208
refrain entirely from distribution of the Program.
209
210
If any portion of this section is held invalid or unenforceable under
211
any particular circumstance, the balance of the section is intended to
212
apply and the section as a whole is intended to apply in other
213
circumstances.
214
215
It is not the purpose of this section to induce you to infringe any
216
patents or other property right claims or to contest validity of any
217
such claims; this section has the sole purpose of protecting the
218
integrity of the free software distribution system, which is
219
implemented by public license practices.  Many people have made
220
generous contributions to the wide range of software distributed
221
through that system in reliance on consistent application of that
222
system; it is up to the author/donor to decide if he or she is willing
223
to distribute software through any other system and a licensee cannot
224
impose that choice.
225
226
This section is intended to make thoroughly clear what is believed to
227
be a consequence of the rest of this License.
228

229
  8. If the distribution and/or use of the Program is restricted in
230
certain countries either by patents or by copyrighted interfaces, the
231
original copyright holder who places the Program under this License
232
may add an explicit geographical distribution limitation excluding
233
those countries, so that distribution is permitted only in or among
234
countries not thus excluded.  In such case, this License incorporates
235
the limitation as if written in the body of this License.
236
237
  9. The Free Software Foundation may publish revised and/or new versions
238
of the General Public License from time to time.  Such new versions will
239
be similar in spirit to the present version, but may differ in detail to
240
address new problems or concerns.
241
242
Each version is given a distinguishing version number.  If the Program
243
specifies a version number of this License which applies to it and "any
244
later version", you have the option of following the terms and conditions
245
either of that version or of any later version published by the Free
246
Software Foundation.  If the Program does not specify a version number of
247
this License, you may choose any version ever published by the Free Software
248
Foundation.
249
250
  10. If you wish to incorporate parts of the Program into other free
251
programs whose distribution conditions are different, write to the author
252
to ask for permission.  For software which is copyrighted by the Free
253
Software Foundation, write to the Free Software Foundation; we sometimes
254
make exceptions for this.  Our decision will be guided by the two goals
255
of preserving the free status of all derivatives of our free software and
256
of promoting the sharing and reuse of software generally.
257
258
			    NO WARRANTY
259
260
  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
262
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
266
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
267
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268
REPAIR OR CORRECTION.
269
270
  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278
POSSIBILITY OF SUCH DAMAGES.
279
280
		     END OF TERMS AND CONDITIONS
281

282
	    How to Apply These Terms to Your New Programs
283
284
  If you develop a new program, and you want it to be of the greatest
285
possible use to the public, the best way to achieve this is to make it
286
free software which everyone can redistribute and change under these terms.
287
288
  To do so, attach the following notices to the program.  It is safest
289
to attach them to the start of each source file to most effectively
290
convey the exclusion of warranty; and each file should have at least
291
the "copyright" line and a pointer to where the full notice is found.
292
293
    <one line to give the program's name and a brief idea of what it does.>
294
    Copyright (C) <year>  <name of author>
295
296
    This program is free software; you can redistribute it and/or modify
297
    it under the terms of the GNU General Public License as published by
298
    the Free Software Foundation; either version 2 of the License, or
299
    (at your option) any later version.
300
301
    This program is distributed in the hope that it will be useful,
302
    but WITHOUT ANY WARRANTY; without even the implied warranty of
303
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
304
    GNU General Public License for more details.
305
306
    You should have received a copy of the GNU General Public License
307
    along with this program; if not, write to the Free Software
308
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
309
310
311
Also add information on how to contact you by electronic and paper mail.
312
313
If the program is interactive, make it output a short notice like this
314
when it starts in an interactive mode:
315
316
    Gnomovision version 69, Copyright (C) year name of author
317
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
318
    This is free software, and you are welcome to redistribute it
319
    under certain conditions; type `show c' for details.
320
321
The hypothetical commands `show w' and `show c' should show the appropriate
322
parts of the General Public License.  Of course, the commands you use may
323
be called something other than `show w' and `show c'; they could even be
324
mouse-clicks or menu items--whatever suits your program.
325
326
You should also get your employer (if you work as a programmer) or your
327
school, if any, to sign a "copyright disclaimer" for the program, if
328
necessary.  Here is a sample; alter the names:
329
330
  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
331
  `Gnomovision' (which makes passes at compilers) written by James Hacker.
332
333
  <signature of Ty Coon>, 1 April 1989
334
  Ty Coon, President of Vice
335
336
This General Public License does not permit incorporating your program into
337
proprietary programs.  If your program is a subroutine library, you may
338
consider it more useful to permit linking proprietary applications with the
339
library.  If this is what you want to do, use the GNU Library General
340
Public License instead of this License.
1341
A licenses/RICH-TEXT-FX.md
1
Copyright (c) 2013-2017, Tomas Mikula and contributors
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
6
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
8
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
10
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
111
A licenses/SAXON-HE.txt
1
                             Mozilla Public License
2
                                  Version 2.0
3
4
1. Definitions
5
6
   1.1. “Contributor”
7
          means each individual or legal entity that creates, contributes
8
          to the creation of, or owns Covered Software.
9
10
   1.2. “Contributor Version”
11
          means the combination of the Contributions of others (if any)
12
          used by a Contributor and that particular Contributor’s
13
          Contribution.
14
15
   1.3. “Contribution”
16
          means Covered Software of a particular Contributor.
17
18
   1.4. “Covered Software”
19
          means Source Code Form to which the initial Contributor has
20
          attached the notice in Exhibit A, the Executable Form of such
21
          Source Code Form, and Modifications of such Source Code Form, in
22
          each case including portions thereof.
23
24
   1.5. “Incompatible With Secondary Licenses”
25
          means
26
27
         a. that the initial Contributor has attached the notice described
28
            in Exhibit B to the Covered Software; or
29
         b. that the Covered Software was made available under the terms
30
            of version 1.1 or earlier of the License, but not also under
31
            the terms of a Secondary License.
32
33
   1.6. “Executable Form”
34
          means any form of the work other than Source Code Form.
35
36
   1.7. “Larger Work”
37
          means a work that combines Covered Software with other material,
38
          in a separate file or files, that is not Covered Software.
39
40
   1.8. “License”
41
          means this document.
42
43
   1.9. “Licensable”
44
          means having the right to grant, to the maximum extent possible,
45
          whether at the time of the initial grant or subsequently, any
46
          and all of the rights conveyed by this License.
47
48
   1.10. “Modifications”
49
          means any of the following:
50
51
         a. any file in Source Code Form that results from an addition to,
52
            deletion from, or modification of the contents of Covered
53
            Software; or
54
         b. any new file in Source Code Form that contains any Covered
55
            Software.
56
57
   1.11. “Patent Claims” of a Contributor
58
          means any patent claim(s), including without limitation, method,
59
          process, and apparatus claims, in any patent Licensable by such
60
          Contributor that would be infringed, but for the grant of the
61
          License, by the making, using, selling, offering for sale,
62
          having made, import, or transfer of either its Contributions or
63
          its Contributor Version.
64
65
   1.12. “Secondary License”
66
          means either the GNU General Public License, Version 2.0, the
67
          GNU Lesser General Public License, Version 2.1, the GNU Affero
68
          General Public License, Version 3.0, or any later versions of
69
          those licenses.
70
71
   1.13. “Source Code Form”
72
          means the form of the work preferred for making modifications.
73
74
   1.14. “You” (or “Your”)
75
          means an individual or a legal entity exercising rights under
76
          this License. For legal entities, “You” includes any entity that
77
          controls, is controlled by, or is under common control with You.
78
          For purposes of this definition, “control” means (a) the power,
79
          direct or indirect, to cause the direction or management of such
80
          entity, whether by contract or otherwise, or (b) ownership of
81
          more than fifty percent (50%) of the outstanding shares or
82
          beneficial ownership of such entity.
83
84
2. License Grants and Conditions
85
86
  2.1. Grants
87
88
   Each Contributor hereby grants You a world-wide, royalty-free,
89
   non-exclusive license:
90
    a. under intellectual property rights (other than patent or trademark)
91
       Licensable by such Contributor to use, reproduce, make available,
92
       modify, display, perform, distribute, and otherwise exploit its
93
       Contributions, either on an unmodified basis, with Modifications,
94
       or as part of a Larger Work; and
95
    b. under Patent Claims of such Contributor to make, use, sell, offer
96
       for sale, have made, import, and otherwise transfer either its
97
       Contributions or its Contributor Version.
98
99
  2.2. Effective Date
100
101
   The licenses granted in Section 2.1 with respect to any Contribution
102
   become effective for each Contribution on the date the Contributor
103
   first distributes such Contribution.
104
105
  2.3. Limitations on Grant Scope
106
107
   The licenses granted in this Section 2 are the only rights granted
108
   under this License. No additional rights or licenses will be implied
109
   from the distribution or licensing of Covered Software under this
110
   License. Notwithstanding Section 2.1(b) above, no patent license is
111
   granted by a Contributor:
112
    a. for any code that a Contributor has removed from Covered Software;
113
       or
114
    b. for infringements caused by: (i) Your and any other third party’s
115
       modifications of Covered Software, or (ii) the combination of its
116
       Contributions with other software (except as part of its
117
       Contributor Version); or
118
    c. under Patent Claims infringed by Covered Software in the absence of
119
       its Contributions.
120
121
   This License does not grant any rights in the trademarks, service
122
   marks, or logos of any Contributor (except as may be necessary to
123
   comply with the notice requirements in Section 3.4).
124
125
  2.4. Subsequent Licenses
126
127
   No Contributor makes additional grants as a result of Your choice to
128
   distribute the Covered Software under a subsequent version of this
129
   License (see Section 10.2) or under the terms of a Secondary License
130
   (if permitted under the terms of Section 3.3).
131
132
  2.5. Representation
133
134
   Each Contributor represents that the Contributor believes its
135
   Contributions are its original creation(s) or it has sufficient rights
136
   to grant the rights to its Contributions conveyed by this License.
137
138
  2.6. Fair Use
139
140
   This License is not intended to limit any rights You have under
141
   applicable copyright doctrines of fair use, fair dealing, or other
142
   equivalents.
143
144
  2.7. Conditions
145
146
   Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
147
   in Section 2.1.
148
149
3. Responsibilities
150
151
  3.1. Distribution of Source Form
152
153
   All distribution of Covered Software in Source Code Form, including any
154
   Modifications that You create or to which You contribute, must be under
155
   the terms of this License. You must inform recipients that the Source
156
   Code Form of the Covered Software is governed by the terms of this
157
   License, and how they can obtain a copy of this License. You may not
158
   attempt to alter or restrict the recipients’ rights in the Source Code
159
   Form.
160
161
  3.2. Distribution of Executable Form
162
163
   If You distribute Covered Software in Executable Form then:
164
    a. such Covered Software must also be made available in Source Code
165
       Form, as described in Section 3.1, and You must inform recipients
166
       of the Executable Form how they can obtain a copy of such Source
167
       Code Form by reasonable means in a timely manner, at a charge no
168
       more than the cost of distribution to the recipient; and
169
    b. You may distribute such Executable Form under the terms of this
170
       License, or sublicense it under different terms, provided that the
171
       license for the Executable Form does not attempt to limit or alter
172
       the recipients’ rights in the Source Code Form under this License.
173
174
  3.3. Distribution of a Larger Work
175
176
   You may create and distribute a Larger Work under terms of Your choice,
177
   provided that You also comply with the requirements of this License for
178
   the Covered Software. If the Larger Work is a combination of Covered
179
   Software with a work governed by one or more Secondary Licenses, and
180
   the Covered Software is not Incompatible With Secondary Licenses, this
181
   License permits You to additionally distribute such Covered Software
182
   under the terms of such Secondary License(s), so that the recipient of
183
   the Larger Work may, at their option, further distribute the Covered
184
   Software under the terms of either this License or such Secondary
185
   License(s).
186
187
  3.4. Notices
188
189
   You may not remove or alter the substance of any license notices
190
   (including copyright notices, patent notices, disclaimers of warranty,
191
   or limitations of liability) contained within the Source Code Form of
192
   the Covered Software, except that You may alter any license notices to
193
   the extent required to remedy known factual inaccuracies.
194
195
  3.5. Application of Additional Terms
196
197
   You may choose to offer, and to charge a fee for, warranty, support,
198
   indemnity or liability obligations to one or more recipients of Covered
199
   Software. However, You may do so only on Your own behalf, and not on
200
   behalf of any Contributor. You must make it absolutely clear that any
201
   such warranty, support, indemnity, or liability obligation is offered
202
   by You alone, and You hereby agree to indemnify every Contributor for
203
   any liability incurred by such Contributor as a result of warranty,
204
   support, indemnity or liability terms You offer. You may include
205
   additional disclaimers of warranty and limitations of liability
206
   specific to any jurisdiction.
207
208
4. Inability to Comply Due to Statute or Regulation
209
210
   If it is impossible for You to comply with any of the terms of this
211
   License with respect to some or all of the Covered Software due to
212
   statute, judicial order, or regulation then You must: (a) comply with
213
   the terms of this License to the maximum extent possible; and (b)
214
   describe the limitations and the code they affect. Such description
215
   must be placed in a text file included with all distributions of the
216
   Covered Software under this License. Except to the extent prohibited by
217
   statute or regulation, such description must be sufficiently detailed
218
   for a recipient of ordinary skill to be able to understand it.
219
220
5. Termination
221
222
   5.1. The rights granted under this License will terminate automatically
223
   if You fail to comply with any of its terms. However, if You become
224
   compliant, then the rights granted under this License from a particular
225
   Contributor are reinstated (a) provisionally, unless and until such
226
   Contributor explicitly and finally terminates Your grants, and (b) on
227
   an ongoing basis, if such Contributor fails to notify You of the
228
   non-compliance by some reasonable means prior to 60 days after You have
229
   come back into compliance. Moreover, Your grants from a particular
230
   Contributor are reinstated on an ongoing basis if such Contributor
231
   notifies You of the non-compliance by some reasonable means, this is
232
   the first time You have received notice of non-compliance with this
233
   License from such Contributor, and You become compliant prior to 30
234
   days after Your receipt of the notice.
235
236
   5.2. If You initiate litigation against any entity by asserting a
237
   patent infringement claim (excluding declaratory judgment actions,
238
   counter-claims, and cross-claims) alleging that a Contributor Version
239
   directly or indirectly infringes any patent, then the rights granted to
240
   You by any and all Contributors for the Covered Software under
241
   Section 2.1 of this License shall terminate.
242
243
   5.3. In the event of termination under Sections 5.1 or 5.2 above, all
244
   end user license agreements (excluding distributors and resellers)
245
   which have been validly granted by You or Your distributors under this
246
   License prior to termination shall survive termination.
247
248
6. Disclaimer of Warranty
249
250
   Covered Software is provided under this License on an “as is” basis,
251
   without warranty of any kind, either expressed, implied, or statutory,
252
   including, without limitation, warranties that the Covered Software is
253
   free of defects, merchantable, fit for a particular purpose or
254
   non-infringing. The entire risk as to the quality and performance of
255
   the Covered Software is with You. Should any Covered Software prove
256
   defective in any respect, You (not any Contributor) assume the cost of
257
   any necessary servicing, repair, or correction. This disclaimer of
258
   warranty constitutes an essential part of this License. No use of any
259
   Covered Software is authorized under this License except under this
260
   disclaimer.
261
262
7. Limitation of Liability
263
264
   Under no circumstances and under no legal theory, whether tort
265
   (including negligence), contract, or otherwise, shall any Contributor,
266
   or anyone who distributes Covered Software as permitted above, be
267
   liable to You for any direct, indirect, special, incidental, or
268
   consequential damages of any character including, without limitation,
269
   damages for lost profits, loss of goodwill, work stoppage, computer
270
   failure or malfunction, or any and all other commercial damages or
271
   losses, even if such party shall have been informed of the possibility
272
   of such damages. This limitation of liability shall not apply to
273
   liability for death or personal injury resulting from such party’s
274
   negligence to the extent applicable law prohibits such limitation. Some
275
   jurisdictions do not allow the exclusion or limitation of incidental or
276
   consequential damages, so this exclusion and limitation may not apply
277
   to You.
278
279
8. Litigation
280
281
   Any litigation relating to this License may be brought only in the
282
   courts of a jurisdiction where the defendant maintains its principal
283
   place of business and such litigation shall be governed by laws of that
284
   jurisdiction, without reference to its conflict-of-law provisions.
285
   Nothing in this Section shall prevent a party’s ability to bring
286
   cross-claims or counter-claims.
287
288
9. Miscellaneous
289
290
   This License represents the complete agreement concerning the subject
291
   matter hereof. If any provision of this License is held to be
292
   unenforceable, such provision shall be reformed only to the extent
293
   necessary to make it enforceable. Any law or regulation which provides
294
   that the language of a contract shall be construed against the drafter
295
   shall not be used to construe this License against a Contributor.
296
297
10. Versions of the License
298
299
  10.1. New Versions
300
301
   Mozilla Foundation is the license steward. Except as provided in
302
   Section 10.3, no one other than the license steward has the right to
303
   modify or publish new versions of this License. Each version will be
304
   given a distinguishing version number.
305
306
  10.2. Effect of New Versions
307
308
   You may distribute the Covered Software under the terms of the version
309
   of the License under which You originally received the Covered
310
   Software, or under the terms of any subsequent version published by the
311
   license steward.
312
313
  10.3. Modified Versions
314
315
   If you create software not governed by this License, and you want to
316
   create a new license for such software, you may create and use a
317
   modified version of this License if you rename the license and remove
318
   any references to the name of the license steward (except to note that
319
   such modified license differs from this License).
320
321
  10.4. Distributing Source Code Form that is Incompatible With Secondary
322
  Licenses
323
324
   If You choose to distribute Source Code Form that is Incompatible With
325
   Secondary Licenses under the terms of this version of the License, the
326
   notice described in Exhibit B of this License must be attached.
327
328
Exhibit A - Source Code Form License Notice
329
330
     This Source Code Form is subject to the terms of the Mozilla Public
331
     License, v. 2.0. If a copy of the MPL was not distributed with this
332
     file, You can obtain one at https://mozilla.org/MPL/2.0/.
333
334
   If it is not possible or desirable to put the notice in a particular
335
   file, then You may include the notice in a location (such as a LICENSE
336
   file in a relevant directory) where a recipient would be likely to look
337
   for such a notice.
338
339
   You may add additional accurate notices of copyright ownership.
340
341
Exhibit B - “Incompatible With Secondary Licenses” Notice
342
343
     This Source Code Form is “Incompatible With Secondary Licenses”, as
344
     defined by the Mozilla Public License, v. 2.0.
1345
A licenses/UNDO-FX.md
1
Copyright (c) 2014, TomasMikula
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without modification,
5
are permitted provided that the following conditions are met:
6
7
* Redistributions of source code must retain the above copyright notice, this
8
  list of conditions and the following disclaimer.
9
10
* Redistributions in binary form must reproduce the above copyright notice, this
11
  list of conditions and the following disclaimer in the documentation and/or
12
  other materials provided with the distribution.
113
14
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
18
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
A licenses/WELL-BEHAVED-FX.md
1
Copyright (c) 2014, TomasMikula
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without
5
modification, are permitted provided that the following conditions are met:
6
7
* Redistributions of source code must retain the above copyright notice, this
8
  list of conditions and the following disclaimer.
9
10
* Redistributions in binary form must reproduce the above copyright notice,
11
  this list of conditions and the following disclaimer in the documentation
12
  and/or other materials provided with the distribution.
13
14
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
125
A licenses/fonts/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
A logging/Main.java
1
package com.github.javaparser;
2
3
import com.github.javaparser.ast.CompilationUnit;
4
import com.github.javaparser.ast.body.MethodDeclaration;
5
import com.github.javaparser.ast.body.TypeDeclaration;
6
import com.github.javaparser.ast.stmt.BlockStmt;
7
import com.github.javaparser.ast.stmt.Statement;
8
9
import java.io.File;
10
import java.io.FileNotFoundException;
11
import java.util.List;
12
import java.util.Optional;
13
14
import static com.github.javaparser.StaticJavaParser.parseStatement;
15
import static java.lang.String.format;
16
17
public class Main {
18
  public static void main( final String[] args ) throws FileNotFoundException {
19
    final File sourceFile = new File( args[ 0 ] );
20
    final JavaParser parser = new JavaParser();
21
    final ParseResult<CompilationUnit> pr = parser.parse( sourceFile );
22
    final Optional<CompilationUnit> ocu = pr.getResult();
23
24
    if( ocu.isPresent() ) {
25
      final CompilationUnit cu = ocu.get();
26
      final List<TypeDeclaration<?>> types = cu.getTypes();
27
28
      for( final TypeDeclaration<?> type : types ) {
29
        final List<MethodDeclaration> methods = type.getMethods();
30
31
        for( final MethodDeclaration method : methods ) {
32
          final Optional<BlockStmt> body = method.getBody();
33
          final String m = format( "%s::%s( %s )",
34
                                   type.getNameAsString(),
35
                                   method.getNameAsString(),
36
                                   method.getParameters().toString() );
37
38
          final String mBegan = format(
39
              "System.out.println(\"BEGAN %s\");", m );
40
          final String mEnded = format(
41
              "System.out.println(\"ENDED %s\");", m );
42
43
          final Statement sBegan = parseStatement( mBegan );
44
          final Statement sEnded = parseStatement( mEnded );
45
46
          body.ifPresent( ( b ) -> {
47
            final int i = b.getStatements().size();
48
49
            b.addStatement( 0, sBegan );
50
51
            // Insert before any "return" statement.
52
            b.addStatement( i, sEnded );
53
          } );
54
        }
55
56
        System.out.println( cu.toString() );
57
      }
58
    }
59
  }
60
}
161
A logging/README.md
1
# Logging
2
3
The files in this directory can be used to log the entry/exit to every
4
method for debugging purposes. These changes are not meant to be pushed
5
onto the mainline branch (i.e., not for production use).
6
7
The instructions are relative to the directory containing these instructions.
8
9
# Build
10
11
If modifications to the existing JAR are needed, rebuild the changes
12
as follows:
13
14
    git clone https://github.com/javaparser/javaparser
15
    cd javaparser
16
    cp Main.java ./javaparser-core/src/main/java/com/github/javaparser/.
17
    mvn package -Dmaven.test.skip=true
18
    cp javaparser-core/target/javaparser-core-3.16.2-SNAPSHOT.jar jp.jar
19
20
The file `jp.jar` is built with `Main.class`.
21
22
# Usage
23
24
Run the `inject` script to replace the original files with the logging
25
versions.
26
27
# Revert
28
29
When finished building a debug version of the application, reset the repo
30
as follows:
31
32
    git reset --hard HEAD
33
134
A logging/inject
1
#!/usr/bin/env bash
2
3
echo "Parsing"
4
find ../src/main/java -type f -name "*.java" -exec \
5
  sh -c 'echo {}; java -cp jp.jar com.github.javaparser.Main {} > {}.jp' \;
6
7
echo "Renaming"
8
# The +10c ensures that files without code are skipped.
9
find ../src/main/java -type f -name "*.jp" -size +10c -exec \
10
  sh -c 'echo {}; mv {} $(dirname {})/$(basename {} .jp)' \;
11
112
A logging/jp.jar
Binary file
A settings.gradle
11
A src/main/java/com/keenwrite/AbstractFileFactory.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.io.FileType;
5
6
import java.nio.file.Path;
7
8
import static com.keenwrite.Constants.GLOB_PREFIX_FILE;
9
import static com.keenwrite.Constants.sSettings;
10
import static com.keenwrite.io.FileType.UNKNOWN;
11
import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate;
12
13
/**
14
 * Provides common behaviours for factories that instantiate classes based on
15
 * file type.
16
 */
17
public abstract class AbstractFileFactory {
18
19
  /**
20
   * Determines the file type from the path extension. This should only be
21
   * called when it is known that the file type won't be a definition file
22
   * (e.g., YAML or other definition source), but rather an editable file
23
   * (e.g., Markdown, XML, etc.).
24
   *
25
   * @param path The path with a file name extension.
26
   * @return The FileType for the given path.
27
   */
28
  public static FileType lookup( final Path path ) {
29
    assert path != null;
30
31
    return lookup( path, GLOB_PREFIX_FILE );
32
  }
33
34
  /**
35
   * Creates a file type that corresponds to the given path.
36
   *
37
   * @param path   Reference to a variable definition file.
38
   * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE.
39
   * @return The file type that corresponds to the given path.
40
   */
41
  protected static FileType lookup( final Path path, final String prefix ) {
42
    assert path != null;
43
    assert prefix != null;
44
45
    final var keys = sSettings.getKeys( prefix );
46
47
    var found = false;
48
    var fileType = UNKNOWN;
49
50
    while( keys.hasNext() && !found ) {
51
      final var key = keys.next();
52
      final var patterns = sSettings.getStringSettingList( key );
53
      final var predicate = createFileTypePredicate( patterns );
54
55
      if( found = predicate.test( path.toFile() ) ) {
56
        // Remove the EXTENSIONS_PREFIX to get the file name extension mapped
57
        // to a standard name (as defined in the settings.properties file).
58
        final String suffix = key.replace( prefix + '.', "" );
59
        fileType = FileType.from( suffix );
60
      }
61
    }
62
63
    return fileType;
64
  }
65
}
166
A src/main/java/com/keenwrite/Bootstrap.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import java.util.Properties;
5
6
/**
7
 * Responsible for loading the bootstrap.properties file, which is
8
 * tactically located outside of the standard resource reverse domain name
9
 * namespace to avoid hard-coding the application name in many places.
10
 * Instead, the application name is located in the bootstrap file, which is
11
 * then used to look-up the remaining settings.
12
 * <p>
13
 * See {@link Constants#PATH_PROPERTIES_SETTINGS} for details.
14
 * </p>
15
 */
16
public class Bootstrap {
17
  private static final Properties BOOTSTRAP = new Properties();
18
19
  static {
20
    try( final var stream =
21
             Constants.class.getResourceAsStream( "/bootstrap.properties" ) ) {
22
      BOOTSTRAP.load( stream );
23
    } catch( final Exception ignored ) {
24
      // Bootstrap properties cannot be found, throw in the towel.
25
    }
26
  }
27
28
  public static final String APP_TITLE =
29
      BOOTSTRAP.getProperty( "application.title" );
30
  public static final String APP_TITLE_LOWERCASE = APP_TITLE.toLowerCase();
31
}
132
A src/main/java/com/keenwrite/Constants.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.service.Settings;
5
import javafx.scene.image.Image;
6
7
import java.io.File;
8
import java.nio.charset.Charset;
9
import java.nio.file.Path;
10
import java.util.ArrayList;
11
import java.util.List;
12
import java.util.Locale;
13
14
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
15
import static com.keenwrite.preferences.LocaleScripts.withScript;
16
import static java.io.File.separator;
17
import static java.lang.String.format;
18
import static java.lang.System.getProperty;
19
20
/**
21
 * Defines application-wide default values.
22
 */
23
public class Constants {
24
25
  /**
26
   * Used by the default settings to load the {@link Settings} service. This
27
   * must come before any attempt is made to create a {@link Settings} object.
28
   * The reference to {@link Bootstrap#APP_TITLE_LOWERCASE} should cause the
29
   * JVM to load {@link Bootstrap} prior to proceeding. Loading that class
30
   * beforehand will read the bootstrap properties file to determine the
31
   * application name, which is then used to locate the settings properties.
32
   */
33
  public static final String PATH_PROPERTIES_SETTINGS =
34
    format( "/com/%s/settings.properties", APP_TITLE_LOWERCASE );
35
36
  /**
37
   * The {@link Settings} uses {@link #PATH_PROPERTIES_SETTINGS}.
38
   */
39
  public static final Settings sSettings = Services.load( Settings.class );
40
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" );
48
49
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
50
51
  /**
52
   * Prevent double events when updating files on Linux (save and timestamp).
53
   */
54
  public static final int APP_WATCHDOG_TIMEOUT = get(
55
    "application.watchdog.timeout", 200 );
56
57
  public static final String STYLESHEET_MARKDOWN = get(
58
    "file.stylesheet.markdown" );
59
  public static final String STYLESHEET_MARKDOWN_LOCALE =
60
    "file.stylesheet.markdown.locale";
61
  public static final String STYLESHEET_PREVIEW = get(
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" );
66
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
  );
74
75
  public static final Image ICON_DIALOG = LOGOS.get( 1 );
76
77
  public static final String FILE_PREFERENCES = getPreferencesFilename();
78
79
  /**
80
   * Refer to file name extension settings in the configuration file. Do not
81
   * terminate with a period.
82
   */
83
  public static final String GLOB_PREFIX_FILE = "file.ext";
84
85
  /**
86
   * Three parameters: line number, column number, and offset.
87
   */
88
  public static final String STATUS_BAR_LINE = "Main.status.line";
89
90
  public static final String STATUS_BAR_OK = "Main.status.state.default";
91
92
  /**
93
   * Used to show an error while parsing, usually syntactical.
94
   */
95
  public static final String STATUS_PARSE_ERROR = "Main.status.error.parse";
96
  public static final String STATUS_DEFINITION_BLANK =
97
    "Main.status.error.def.blank";
98
  public static final String STATUS_DEFINITION_EMPTY =
99
    "Main.status.error.def.empty";
100
101
  /**
102
   * One parameter: the word under the cursor that could not be found.
103
   */
104
  public static final String STATUS_DEFINITION_MISSING =
105
    "Main.status.error.def.missing";
106
107
  /**
108
   * Used when creating flat maps relating to resolved variables.
109
   */
110
  public static final int MAP_SIZE_DEFAULT = 128;
111
112
  /**
113
   * Default image extension order to use when scanning.
114
   */
115
  public static final String PERSIST_IMAGES_DEFAULT =
116
    get( "file.ext.image.order" );
117
118
  /**
119
   * Default working directory to use for R startup script.
120
   */
121
  public static final File USER_DIRECTORY =
122
    new File( System.getProperty( "user.dir" ) );
123
124
  /**
125
   * Default path to use for an untitled (pathless) file.
126
   */
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();
133
134
  /**
135
   * Default starting delimiter for definition variables. This value must
136
   * not overlap math delimiters, so do not use $ tokens as the first
137
   * delimiter.
138
   */
139
  public static final String DEF_DELIM_BEGAN_DEFAULT = "{{";
140
141
  /**
142
   * Default ending delimiter for definition variables.
143
   */
144
  public static final String DEF_DELIM_ENDED_DEFAULT = "}}";
145
146
  /**
147
   * Default starting delimiter when inserting R variables.
148
   */
149
  public static final String R_DELIM_BEGAN_DEFAULT = "x( ";
150
151
  /**
152
   * Default ending delimiter when inserting R variables.
153
   */
154
  public static final String R_DELIM_ENDED_DEFAULT = " )";
155
156
  /**
157
   * Resource directory where different language lexicons are located.
158
   */
159
  public static final String LEXICONS_DIRECTORY = "lexicons";
160
161
  /**
162
   * Absolute location of true type font files within the Java archive file.
163
   */
164
  public static final String FONT_DIRECTORY = "/fonts";
165
166
  /**
167
   * Default text editor font size, in points.
168
   */
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() );
180
181
  /**
182
   * Default identifier to use for synchronized scrolling.
183
   */
184
  public static final String CARET_ID = "caret";
185
186
  /**
187
   * Prevent instantiation.
188
   */
189
  private Constants() {
190
  }
191
192
  private static String get( final String key ) {
193
    return sSettings.getSetting( key, "" );
194
  }
195
196
  @SuppressWarnings( "SameParameterValue" )
197
  private static int get( final String key, final int 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;
237
  }
238
}
1239
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
A src/main/java/com/keenwrite/ExportFormat.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import java.io.File;
5
import java.nio.file.Path;
6
7
import static org.apache.commons.io.FilenameUtils.removeExtension;
8
9
/**
10
 * Provides controls for processor behaviour when transforming input documents.
11
 */
12
public enum ExportFormat {
13
14
  /**
15
   * For HTML exports, encode TeX as SVG.
16
   */
17
  HTML_TEX_SVG( ".html" ),
18
19
  /**
20
   * For HTML exports, encode TeX using {@code $} delimiters, suitable for
21
   * rendering by an external TeX typesetting engine (or online with KaTeX).
22
   */
23
  HTML_TEX_DELIMITED( ".html" ),
24
25
  /**
26
   * Indicates that the processors should export to a Markdown format.
27
   */
28
  MARKDOWN_PLAIN( ".out.md" ),
29
30
  /**
31
   * Indicates no special export format is to be created. No extension is
32
   * applicable.
33
   */
34
  NONE( "" );
35
36
  /**
37
   * Preferred file name extension for the given file type.
38
   */
39
  private final String mExtension;
40
41
  ExportFormat( final String extension ) {
42
    mExtension = extension;
43
  }
44
45
  /**
46
   * Returns the given {@link File} with its extension replaced by one that
47
   * matches this {@link ExportFormat} extension.
48
   *
49
   * @param file The file to perform an extension swap.
50
   * @return The given file with its extension replaced.
51
   */
52
  public File toExportFilename( final File file ) {
53
    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() );
65
  }
66
}
167
A src/main/java/com/keenwrite/Launcher.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import java.io.IOException;
5
import java.io.InputStream;
6
import java.util.Calendar;
7
import java.util.Properties;
8
9
import static com.keenwrite.Bootstrap.APP_TITLE;
10
import static java.lang.String.format;
11
12
/**
13
 * Launches the application using the {@link MainApp} class.
14
 *
15
 * <p>
16
 * This is required until modules are implemented, which may never happen
17
 * because the application should be ported away from Java and JavaFX.
18
 * </p>
19
 */
20
public class Launcher {
21
  /**
22
   * Delegates to the application entry point.
23
   *
24
   * @param args Command-line arguments.
25
   */
26
  public static void main( final String[] args ) {
27
    showAppInfo();
28
    MainApp.main( args );
29
  }
30
31
  @SuppressWarnings("RedundantStringFormatCall")
32
  private static void showAppInfo() {
33
    out( format( "%s version %s", APP_TITLE, getVersion() ) );
34
    out( format( "Copyright 2016-%s White Magic Software, Ltd.", getYear() ) );
35
    out( format( "Portions copyright 2015-2020 Karl Tauber." ) );
36
  }
37
38
  private static void out( final String s ) {
39
    System.out.println( s );
40
  }
41
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
    }
57
  }
58
59
  private static String getYear() {
60
    return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) );
61
  }
62
63
  @SuppressWarnings("SameParameterValue")
64
  private static Properties loadProperties( final String resource )
65
    throws IOException {
66
    final var properties = new Properties();
67
    properties.load( getResourceAsStream( getResourceName( resource ) ) );
68
    return properties;
69
  }
70
71
  private static String getResourceName( final String resource ) {
72
    return format( "%s/%s", getPackagePath(), resource );
73
  }
74
75
  private static String getPackagePath() {
76
    return Launcher.class.getPackageName().replace( '.', '/' );
77
  }
78
79
  private static InputStream getResourceAsStream( final String resource ) {
80
    return Launcher.class.getClassLoader().getResourceAsStream( resource );
81
  }
82
}
183
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
A src/main/java/com/keenwrite/Messages.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.preferences.Key;
5
6
import java.text.MessageFormat;
7
import java.util.Enumeration;
8
import java.util.ResourceBundle;
9
import java.util.Stack;
10
11
import static com.keenwrite.Constants.APP_BUNDLE_NAME;
12
import static java.util.ResourceBundle.getBundle;
13
14
/**
15
 * Recursively resolves message properties. Property values can refer to other
16
 * properties using a <code>${var}</code> syntax.
17
 */
18
public class Messages {
19
20
  private static final ResourceBundle RESOURCE_BUNDLE =
21
      getBundle( APP_BUNDLE_NAME );
22
23
  private Messages() {
24
  }
25
26
  /**
27
   * Return the value of a resource bundle value after having resolved any
28
   * references to other bundle variables.
29
   *
30
   * @param props The bundle containing resolvable properties.
31
   * @param s     The value for a key to resolve.
32
   * @return The value of the key with all references recursively dereferenced.
33
   */
34
  @SuppressWarnings("SameParameterValue")
35
  private static String resolve( final ResourceBundle props, final String s ) {
36
    final int len = s.length();
37
    final Stack<StringBuilder> stack = new Stack<>();
38
39
    StringBuilder sb = new StringBuilder( 256 );
40
    boolean open = false;
41
42
    for( int i = 0; i < len; i++ ) {
43
      final char c = s.charAt( i );
44
45
      switch( c ) {
46
        case '$': {
47
          if( i + 1 < len && s.charAt( i + 1 ) == '{' ) {
48
            stack.push( sb );
49
50
            if( stack.size() > 20 ) {
51
              final var m = get( "Main.status.error.messages.recursion", s );
52
              throw new IllegalArgumentException( m );
53
            }
54
55
            sb = new StringBuilder( 256 );
56
            i++;
57
            open = true;
58
          }
59
60
          break;
61
        }
62
63
        case '}': {
64
          if( open ) {
65
            open = false;
66
            final String name = sb.toString();
67
68
            sb = stack.pop();
69
            sb.append( props.getString( name ) );
70
            break;
71
          }
72
        }
73
74
        default: {
75
          sb.append( c );
76
          break;
77
        }
78
      }
79
    }
80
81
    if( open ) {
82
      final var m = get( "Main.status.error.messages.syntax", s );
83
      throw new IllegalArgumentException( m );
84
    }
85
86
    return sb.toString();
87
  }
88
89
  /**
90
   * Returns the value for a key from the message bundle.
91
   *
92
   * @param key Retrieve the value for this key.
93
   * @return The value for the key.
94
   */
95
  public static String get( final String key ) {
96
    try {
97
      return resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) );
98
    } catch( final Exception ex ) {
99
      return key;
100
    }
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() );
111
  }
112
113
  public static String getLiteral( final String key ) {
114
    return RESOURCE_BUNDLE.getString( key );
115
  }
116
117
  public static String get( final String key, final boolean interpolate ) {
118
    return interpolate ? get( key ) : getLiteral( key );
119
  }
120
121
  /**
122
   * Returns the value for a key from the message bundle with the arguments
123
   * replacing <code>{#}</code> place holders.
124
   *
125
   * @param key  Retrieve the value for this key.
126
   * @param args The values to substitute for place holders.
127
   * @return The value for the key.
128
   */
129
  public static String get( final String key, final Object... args ) {
130
    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();
152
  }
153
}
1154
A src/main/java/com/keenwrite/ScrollEventHandler.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import javafx.beans.property.BooleanProperty;
5
import javafx.beans.property.SimpleBooleanProperty;
6
import javafx.event.Event;
7
import javafx.event.EventHandler;
8
import javafx.scene.control.ScrollBar;
9
import javafx.scene.control.skin.ScrollBarSkin;
10
import javafx.scene.input.MouseEvent;
11
import javafx.scene.input.ScrollEvent;
12
import javafx.scene.layout.StackPane;
13
import org.fxmisc.flowless.VirtualizedScrollPane;
14
import org.fxmisc.richtext.StyleClassedTextArea;
15
16
import javax.swing.*;
17
import java.util.function.Consumer;
18
19
import static javafx.geometry.Orientation.VERTICAL;
20
21
/**
22
 * Converts scroll events from {@link VirtualizedScrollPane} scroll bars to
23
 * an instance of {@link JScrollBar}.
24
 * <p>
25
 * Called to synchronize the scrolling areas for either scrolling with the
26
 * mouse or scrolling using the scrollbar's thumb. Both are required to avoid
27
 * scrolling on the estimatedScrollYProperty that occurs when text events
28
 * fire. Scrolling performed for text events are handled separately to ensure
29
 * the preview panel scrolls to the same position in the Markdown editor,
30
 * taking into account things like images, tables, and other potentially
31
 * long vertical presentation items.
32
 * </p>
33
 */
34
public final class ScrollEventHandler implements EventHandler<Event> {
35
36
  private final class MouseHandler implements EventHandler<MouseEvent> {
37
    private final EventHandler<? super MouseEvent> mOldHandler;
38
39
    /**
40
     * Constructs a new handler for mouse scrolling events.
41
     *
42
     * @param oldHandler Receives the event after scrolling takes place.
43
     */
44
    private MouseHandler( final EventHandler<? super MouseEvent> oldHandler ) {
45
      mOldHandler = oldHandler;
46
    }
47
48
    @Override
49
    public void handle( final MouseEvent event ) {
50
      ScrollEventHandler.this.handle( event );
51
      mOldHandler.handle( event );
52
    }
53
  }
54
55
  private final class ScrollHandler implements EventHandler<ScrollEvent> {
56
    @Override
57
    public void handle( final ScrollEvent event ) {
58
      ScrollEventHandler.this.handle( event );
59
    }
60
  }
61
62
  private final VirtualizedScrollPane<StyleClassedTextArea> mEditorScrollPane;
63
  private final JScrollBar mPreviewScrollBar;
64
  private final BooleanProperty mEnabled = new SimpleBooleanProperty();
65
66
  /**
67
   * @param editorScrollPane Scroll event source (human movement).
68
   * @param previewScrollBar Scroll event destination (corresponding movement).
69
   */
70
  public ScrollEventHandler(
71
      final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane,
72
      final JScrollBar previewScrollBar ) {
73
    mEditorScrollPane = editorScrollPane;
74
    mPreviewScrollBar = previewScrollBar;
75
76
    mEditorScrollPane.addEventFilter( ScrollEvent.ANY, new ScrollHandler() );
77
78
    initVerticalScrollBarThumb(
79
        mEditorScrollPane,
80
        thumb -> {
81
          final var handler = new MouseHandler( thumb.getOnMouseDragged() );
82
          thumb.setOnMouseDragged( handler );
83
        }
84
    );
85
  }
86
87
  /**
88
   * Gets a property intended to be bound to selected property of the tab being
89
   * scrolled. This is required because there's only one preview pane but
90
   * multiple editor panes. Each editor pane maintains its own scroll position.
91
   *
92
   * @return A {@link BooleanProperty} representing whether the scroll
93
   * events for this tab are to be executed.
94
   */
95
  public BooleanProperty enabledProperty() {
96
    return mEnabled;
97
  }
98
99
  /**
100
   * Scrolls the preview scrollbar relative to the edit scrollbar. Algorithm
101
   * is based on Karl Tauber's ratio calculation.
102
   *
103
   * @param event Unused; either {@link MouseEvent} or {@link ScrollEvent}
104
   */
105
  @Override
106
  public void handle( final Event event ) {
107
    if( isEnabled() ) {
108
      final var eScrollPane = getEditorScrollPane();
109
      final var eScrollY =
110
          eScrollPane.estimatedScrollYProperty().getValue().intValue();
111
      final var eHeight = (int)
112
          (eScrollPane.totalHeightEstimateProperty().getValue().intValue()
113
              - eScrollPane.getHeight());
114
      final var eRatio = eHeight > 0
115
          ? Math.min( Math.max( eScrollY / (float) eHeight, 0 ), 1 ) : 0;
116
117
      final var pScrollBar = getPreviewScrollBar();
118
      final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight();
119
      final var pScrollY = (int) (pHeight * eRatio);
120
121
      pScrollBar.setValue( pScrollY );
122
      pScrollBar.getParent().repaint();
123
    }
124
  }
125
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
        }
136
      }
137
    } );
138
  }
139
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
   */
149
  private ScrollBar getVerticalScrollBar(
150
      final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
151
152
    for( final var node : pane.getChildrenUnmodifiable() ) {
153
      if( node instanceof ScrollBar ) {
154
        final var scrollBar = (ScrollBar) node;
155
156
        if( scrollBar.getOrientation() == VERTICAL ) {
157
          return scrollBar;
158
        }
159
      }
160
    }
161
162
    throw new IllegalStateException( "No vertical scroll bar found." );
163
  }
164
165
  private boolean isEnabled() {
166
    // TODO: As a minor optimization, when this is set to false, it could remove
167
    // the MouseHandler and ScrollHandler so that events only dispatch to one
168
    // object (instead of one per editor tab).
169
    return mEnabled.get();
170
  }
171
172
  private VirtualizedScrollPane<StyleClassedTextArea> getEditorScrollPane() {
173
    return mEditorScrollPane;
174
  }
175
176
  private JScrollBar getPreviewScrollBar() {
177
    return mPreviewScrollBar;
178
  }
179
}
1180
A src/main/java/com/keenwrite/Services.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import java.util.HashMap;
5
import java.util.Map;
6
import java.util.ServiceLoader;
7
8
/**
9
 * Responsible for loading services. The services are treated as singleton
10
 * instances.
11
 */
12
public class Services {
13
14
  @SuppressWarnings("rawtypes")
15
  private static final Map<Class, Object> SINGLETONS = new HashMap<>();
16
17
  /**
18
   * Loads a service based on its interface definition. This will return an
19
   * existing instance if the class has already been instantiated.
20
   *
21
   * @param <T> The service to load.
22
   * @param api The interface definition for the service.
23
   * @return A class that implements the interface.
24
   */
25
  @SuppressWarnings("unchecked")
26
  public static <T> T load( final Class<T> api ) {
27
    final T o = (T) get( api );
28
29
    return o == null ? newInstance( api ) : o;
30
  }
31
32
  private static <T> T newInstance( final Class<T> api ) {
33
    final ServiceLoader<T> services = ServiceLoader.load( api );
34
35
    for( final T service : services ) {
36
      if( service != null ) {
37
        // Re-use the same instance the next time the class is loaded.
38
        put( api, service );
39
        return service;
40
      }
41
    }
42
43
    throw new RuntimeException( "No implementation for: " + api );
44
  }
45
46
  @SuppressWarnings("rawtypes")
47
  private static void put( final Class key, Object value ) {
48
    SINGLETONS.put( key, value );
49
  }
50
51
  @SuppressWarnings("rawtypes")
52
  private static Object get( final Class api ) {
53
    return SINGLETONS.get( api );
54
  }
55
}
156
A src/main/java/com/keenwrite/StatusBarNotifier.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.service.events.Notifier;
5
import org.controlsfx.control.StatusBar;
6
7
import static com.keenwrite.Constants.STATUS_BAR_OK;
8
import static com.keenwrite.Messages.get;
9
import static javafx.application.Platform.runLater;
10
11
/**
12
 * Responsible for passing notifications about exceptions (or other error
13
 * messages) through the application. Once the Event Bus is implemented, this
14
 * class can go away.
15
 */
16
public class StatusBarNotifier {
17
  private static final String OK = get( STATUS_BAR_OK, "OK" );
18
19
  private static final Notifier sNotifier = Services.load( Notifier.class );
20
  private static final StatusBar sStatusBar = new StatusBar();
21
22
  /**
23
   * Resets the status bar to a default message.
24
   */
25
  public static void clue() {
26
    // Don't burden the repaint thread if there's no status bar change.
27
    if( !OK.equals( sStatusBar.getText() ) ) {
28
      update( OK );
29
    }
30
  }
31
32
  /**
33
   * Updates the status bar with a custom message.
34
   *
35
   * @param key The resource bundle key associated with a message (typically
36
   *            to inform the user about an error).
37
   */
38
  public static void clue( final String key ) {
39
    update( get( key ) );
40
  }
41
42
  /**
43
   * Updates the status bar with a custom message.
44
   *
45
   * @param key  The property key having a value to populate with arguments.
46
   * @param args The placeholder values to substitute into the key's value.
47
   */
48
  public static void clue( final String key, final Object... args ) {
49
    update( get( key, args ) );
50
  }
51
52
  /**
53
   * Called when an exception occurs that warrants the user's attention.
54
   *
55
   * @param t The exception with a message that the user should know about.
56
   */
57
  public static void clue( final Throwable t ) {
58
    update( t.getMessage() );
59
  }
60
61
  /**
62
   * Returns the global {@link Notifier} instance that can be used for opening
63
   * pop-up alert messages.
64
   *
65
   * @return The pop-up {@link Notifier} dispatcher.
66
   */
67
  public static Notifier getNotifier() {
68
    return sNotifier;
69
  }
70
71
  public static StatusBar getStatusBar() {
72
    return sStatusBar;
73
  }
74
75
  /**
76
   * Updates the status bar to show the first line of the given message.
77
   *
78
   * @param message The message to show in the status bar.
79
   */
80
  private static void update( final String message ) {
81
    runLater(
82
        () -> {
83
          final var s = message == null ? "" : message;
84
          final var i = s.indexOf( '\n' );
85
          sStatusBar.setText( s.substring( 0, i > 0 ? i : s.length() ) );
86
        }
87
    );
88
  }
89
}
190
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/HyperlinkModel.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.editors.markdown;
29
30
import com.vladsch.flexmark.ast.Link;
31
32
/**
33
 * Represents the model for a hyperlink: text, url, and title.
34
 */
35
public class HyperlinkModel {
36
37
  private String text;
38
  private String url;
39
  private String title;
40
41
  /**
42
   * Constructs a new hyperlink model in Markdown format by default with no
43
   * title (i.e., tooltip).
44
   *
45
   * @param text The hyperlink text displayed (e.g., displayed to the user).
46
   * @param url  The destination URL (e.g., when clicked).
47
   */
48
  public HyperlinkModel( final String text, final String url ) {
49
    this( text, url, null );
50
  }
51
52
  /**
53
   * Constructs a new hyperlink model for the given AST link.
54
   *
55
   * @param link A markdown link.
56
   */
57
  public HyperlinkModel( final Link link ) {
58
    this(
59
        link.getText().toString(),
60
        link.getUrl().toString(),
61
        link.getTitle().toString()
62
    );
63
  }
64
65
  /**
66
   * Constructs a new hyperlink model in Markdown format by default.
67
   *
68
   * @param text  The hyperlink text displayed (e.g., displayed to the user).
69
   * @param url   The destination URL (e.g., when clicked).
70
   * @param title The hyperlink title (e.g., shown as a tooltip).
71
   */
72
  public HyperlinkModel( final String text, final String url,
73
                         final String title ) {
74
    setText( text );
75
    setUrl( url );
76
    setTitle( title );
77
  }
78
79
  /**
80
   * Returns the string in Markdown format by default.
81
   *
82
   * @return A markdown version of the hyperlink.
83
   */
84
  @Override
85
  public String toString() {
86
    String format = "%s%s%s";
87
88
    if( hasText() ) {
89
      format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)");
90
    }
91
92
    // Becomes ""+URL+"" if no text is set.
93
    // Becomes [TITLE]+(URL)+"" if no title is set.
94
    // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
95
    return String.format( format, getText(), getUrl(), getTitle() );
96
  }
97
98
  public final void setText( final String text ) {
99
    this.text = nullSafe( text );
100
  }
101
102
  public final void setUrl( final String url ) {
103
    this.url = nullSafe( url );
104
  }
105
106
  public final void setTitle( final String title ) {
107
    this.title = nullSafe( title );
108
  }
109
110
  /**
111
   * Answers whether text has been set for the hyperlink.
112
   *
113
   * @return true This is a text link.
114
   */
115
  public boolean hasText() {
116
    return !getText().isEmpty();
117
  }
118
119
  /**
120
   * Answers whether a title (tooltip) has been set for the hyperlink.
121
   *
122
   * @return true There is a title.
123
   */
124
  public boolean hasTitle() {
125
    return !getTitle().isEmpty();
126
  }
127
128
  public String getText() {
129
    return this.text;
130
  }
131
132
  public String getUrl() {
133
    return this.url;
134
  }
135
136
  public String getTitle() {
137
    return this.title;
138
  }
139
140
  private String nullSafe( final String s ) {
141
    return s == null ? "" : s;
142
  }
143
}
1144
A src/main/java/com/keenwrite/editors/markdown/LinkVisitor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.editors.markdown;
29
30
import com.vladsch.flexmark.ast.Link;
31
import com.vladsch.flexmark.util.ast.Node;
32
import com.vladsch.flexmark.util.ast.NodeVisitor;
33
import com.vladsch.flexmark.util.ast.VisitHandler;
34
35
/**
36
 * Responsible for extracting a hyperlink from the document so that the user
37
 * can edit the link within a dialog.
38
 */
39
public class LinkVisitor {
40
41
  private NodeVisitor visitor;
42
  private Link link;
43
  private final int offset;
44
45
  /**
46
   * Creates a hyperlink given an offset into a paragraph and the markdown AST
47
   * link node.
48
   *
49
   * @param index Index into the paragraph that indicates the hyperlink to
50
   *              change.
51
   */
52
  public LinkVisitor( final int index ) {
53
    this.offset = index;
54
  }
55
56
  public Link process( final Node root ) {
57
    getVisitor().visit( root );
58
    return getLink();
59
  }
60
61
  /**
62
   * @param link Not null.
63
   */
64
  private void visit( final Link link ) {
65
    final int began = link.getStartOffset();
66
    final int ended = link.getEndOffset();
67
    final int index = getOffset();
68
69
    if( index >= began && index <= ended ) {
70
      setLink( link );
71
    }
72
  }
73
74
  private synchronized NodeVisitor getVisitor() {
75
    if( this.visitor == null ) {
76
      this.visitor = createVisitor();
77
    }
78
79
    return this.visitor;
80
  }
81
82
  protected NodeVisitor createVisitor() {
83
    return new NodeVisitor(
84
        new VisitHandler<>( Link.class, LinkVisitor.this::visit ) );
85
  }
86
87
  private Link getLink() {
88
    return this.link;
89
  }
90
91
  private void setLink( final Link link ) {
92
    this.link = link;
93
  }
94
95
  public int getOffset() {
96
    return this.offset;
97
  }
98
}
199
A src/main/java/com/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
A src/main/java/com/keenwrite/exceptions/MissingFileException.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.exceptions;
3
4
import java.io.FileNotFoundException;
5
6
import static com.keenwrite.Messages.get;
7
8
/**
9
 * Responsible for informing the user when a file cannot be found.
10
 * This avoids duplicating the error message prefix.
11
 */
12
public class MissingFileException extends FileNotFoundException {
13
  /**
14
   * Constructs a new {@link MissingFileException} using the given path.
15
   *
16
   * @param uri The path to the file resource that could not be found.
17
   */
18
  public MissingFileException( final String uri ) {
19
    super( get( "Main.status.error.file.missing", uri ) );
20
  }
21
}
122
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
A src/main/java/com/keenwrite/predicates/PredicateFactory.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.predicates;
3
4
import java.io.File;
5
import java.util.Collection;
6
import java.util.function.Predicate;
7
8
import static java.lang.String.join;
9
import static java.nio.file.FileSystems.getDefault;
10
11
/**
12
 * Provides a number of simple {@link Predicate} instances for various types
13
 * of string comparisons, including basic strings and file name strings.
14
 */
15
public class PredicateFactory {
16
  /**
17
   * Creates an instance of {@link Predicate} that matches a globbed file
18
   * name pattern.
19
   *
20
   * @param pattern The file name pattern to match.
21
   * @return A {@link Predicate} that can answer whether a given file name
22
   * matches the given glob pattern.
23
   */
24
  public static Predicate<File> createFileTypePredicate(
25
      final String pattern ) {
26
    final var matcher = getDefault().getPathMatcher(
27
        "glob:**{" + pattern + "}"
28
    );
29
30
    return file -> matcher.matches( file.toPath() );
31
  }
32
33
  /**
34
   * Creates an instance of {@link Predicate} that matches any file name from
35
   * a {@link Collection} of file name patterns. The given patterns are joined
36
   * with commas into a single comma-separated list.
37
   *
38
   * @param patterns The file name patterns to be matched.
39
   * @return A {@link Predicate} that can answer whether a given file name
40
   * matches the given glob patterns.
41
   */
42
  public static Predicate<File> createFileTypePredicate(
43
      final Collection<String> patterns ) {
44
    return createFileTypePredicate( join( ",", patterns ) );
45
  }
46
47
  /**
48
   * Creates an instance of {@link Predicate} that compares whether the given
49
   * {@code reference} string is contained by the comparator. Comparison is
50
   * case-insensitive. The test will also pass if the comparate is empty.
51
   *
52
   * @param comparator The string to check as being contained.
53
   * @return A {@link Predicate} that can answer whether the given string
54
   * is contained within the comparator, or the comparate is empty.
55
   */
56
  public static Predicate<String> createStringContainsPredicate(
57
      final String comparator ) {
58
    return comparate -> comparate.isEmpty() ||
59
        comparate.toLowerCase().contains( comparator.toLowerCase() );
60
  }
161
62
  /**
63
   * Creates an instance of {@link Predicate} that compares whether the given
64
   * {@code reference} string is starts with the comparator. Comparison is
65
   * case-insensitive.
66
   *
67
   * @param comparator The string to check as being contained.
68
   * @return A {@link Predicate} that can answer whether the given string
69
   * is contained within the comparator.
70
   */
71
  public static Predicate<String> createStringStartsPredicate(
72
      final String comparator ) {
73
    return comparate ->
74
        comparate.toLowerCase().startsWith( comparator.toLowerCase() );
75
  }
76
}
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
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
A src/main/java/com/keenwrite/preview/ChainedReplacedElementFactory.java
1
/* Copyright 2006 Patrick Wright
2
 * Copyright 2007 Wisconsin Court System
3
 * Copyright 2020 White Magic Software, Ltd.
4
 *
5
 * This program is free software; you can redistribute it and/or
6
 * modify it under the terms of the GNU Lesser General Public License
7
 * as published by the Free Software Foundation; either version 2.1
8
 * of the License, or (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
13
 * GNU Lesser General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU Lesser General Public License
16
 * along with this program; if not, write to the Free Software
17
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18
 */
19
package com.keenwrite.preview;
20
21
import com.keenwrite.ui.adapters.ReplacedElementAdapter;
22
import com.keenwrite.util.BoundedCache;
23
import org.w3c.dom.Element;
24
import org.xhtmlrenderer.extend.ReplacedElement;
25
import org.xhtmlrenderer.extend.ReplacedElementFactory;
26
import org.xhtmlrenderer.extend.UserAgentCallback;
27
import org.xhtmlrenderer.layout.LayoutContext;
28
import org.xhtmlrenderer.render.BlockBox;
29
30
import java.util.LinkedHashSet;
31
import java.util.Map;
32
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;
38
39
/**
40
 * Responsible for running one or more factories to perform post-processing on
41
 * the HTML document prior to displaying it.
42
 */
43
public class ChainedReplacedElementFactory extends ReplacedElementAdapter {
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
  }
61
62
  @Override
63
  public ReplacedElement createReplacedElement(
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();
71
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;
97
      }
98
    }
99
100
    return null;
101
  }
102
103
  @Override
104
  public void reset() {
105
    for( final var factory : mFactories ) {
106
      factory.reset();
107
    }
108
  }
109
110
  @Override
111
  public void remove( final Element element ) {
112
    for( final var factory : mFactories ) {
113
      factory.remove( element );
114
    }
115
  }
116
117
  public void addFactory( final ReplacedElementFactory factory ) {
118
    mFactories.add( factory );
119
  }
120
121
  public void clearCache() {
122
    mCache.clear();
123
  }
124
}
1125
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
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
A src/main/java/com/keenwrite/preview/MathRenderer.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import com.whitemagicsoftware.tex.*;
5
import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
6
import org.w3c.dom.Document;
7
8
import java.util.function.Supplier;
9
10
import static com.keenwrite.StatusBarNotifier.clue;
11
12
/**
13
 * Responsible for rendering formulas as scalable vector graphics (SVG).
14
 */
15
public class MathRenderer {
16
17
  /**
18
   * Singleton instance for rendering math symbols.
19
   */
20
  public static final MathRenderer MATH_RENDERER = new MathRenderer();
21
22
  /**
23
   * Default font size in points.
24
   */
25
  private static final float FONT_SIZE = 20f;
26
27
  private final TeXFont mTeXFont = createDefaultTeXFont( FONT_SIZE );
28
  private final TeXEnvironment mEnvironment = createTeXEnvironment( mTeXFont );
29
  private final SvgDomGraphics2D mGraphics = createSvgDomGraphics2D();
30
31
  private MathRenderer() {
32
    mGraphics.scale( FONT_SIZE, FONT_SIZE );
33
  }
34
35
  /**
36
   * This method only takes a few seconds to generate
37
   *
38
   * @param equation A mathematical expression to render.
39
   * @return The given string with all formulas transformed into SVG format.
40
   */
41
  public Document render( final String equation ) {
42
    final var formula = new TeXFormula( equation );
43
    final var box = formula.createBox( mEnvironment );
44
    final var l = new TeXLayout( box, FONT_SIZE );
45
46
    mGraphics.initialize( l.getWidth(), l.getHeight() );
47
    box.draw( mGraphics, l.getX(), l.getY() );
48
    return mGraphics.toDom();
49
  }
50
51
  @SuppressWarnings("SameParameterValue")
52
  private TeXFont createDefaultTeXFont( final float fontSize ) {
53
    return create( () -> new DefaultTeXFont( fontSize ) );
54
  }
55
56
  private TeXEnvironment createTeXEnvironment( final TeXFont texFont ) {
57
    return create( () -> new TeXEnvironment( texFont ) );
58
  }
59
60
  private SvgDomGraphics2D createSvgDomGraphics2D() {
61
    return create( SvgDomGraphics2D::new );
62
  }
63
64
  /**
65
   * Tries to instantiate a given object, returning {@code null} on failure.
66
   * The failure message is bubbled up to to the user interface.
67
   *
68
   * @param supplier Creates an instance.
69
   * @param <T>      The type of instance being created.
70
   * @return An instance of the parameterized type or {@code null} upon error.
71
   */
72
  private <T> T create( final Supplier<T> supplier ) {
73
    try {
74
      return supplier.get();
75
    } catch( final Exception ex ) {
76
      clue( ex );
77
      return null;
78
    }
79
  }
80
}
181
A src/main/java/com/keenwrite/preview/RenderingSettings.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import java.util.HashMap;
5
import java.util.Map;
6
7
import static java.awt.RenderingHints.*;
8
import static java.awt.Toolkit.getDefaultToolkit;
9
10
/**
11
 * Responsible for supplying consistent rendering hints throughout the
12
 * application, such as image rendering for {@link SvgRasterizer}.
13
 */
14
@SuppressWarnings("rawtypes")
15
public class RenderingSettings {
16
17
  /**
18
   * Default hints for high-quality rendering that may be changed by
19
   * the system's rendering hints.
20
   */
21
  private static final Map<Object, Object> DEFAULT_HINTS = Map.of(
22
      KEY_ANTIALIASING,
23
      VALUE_ANTIALIAS_ON,
24
      KEY_ALPHA_INTERPOLATION,
25
      VALUE_ALPHA_INTERPOLATION_QUALITY,
26
      KEY_COLOR_RENDERING,
27
      VALUE_COLOR_RENDER_QUALITY,
28
      KEY_DITHERING,
29
      VALUE_DITHER_DISABLE,
30
      KEY_FRACTIONALMETRICS,
31
      VALUE_FRACTIONALMETRICS_ON,
32
      KEY_INTERPOLATION,
33
      VALUE_INTERPOLATION_BICUBIC,
34
      KEY_RENDERING,
35
      VALUE_RENDER_QUALITY,
36
      KEY_STROKE_CONTROL,
37
      VALUE_STROKE_PURE,
38
      KEY_TEXT_ANTIALIASING,
39
      VALUE_TEXT_ANTIALIAS_ON
40
  );
41
42
  /**
43
   * Shared hints for high-quality rendering.
44
   */
45
  public static final Map<Object, Object> RENDERING_HINTS = new HashMap<>(
46
      DEFAULT_HINTS
47
  );
48
49
  static {
50
    final var toolkit = getDefaultToolkit();
51
    final var hints = toolkit.getDesktopProperty( "awt.font.desktophints" );
52
53
    if( hints instanceof Map ) {
54
      final var map = (Map) hints;
55
      for( final var key : map.keySet() ) {
56
        final var hint = map.get( key );
57
        RENDERING_HINTS.put( key, hint );
58
      }
59
    }
60
  }
61
62
  /**
63
   * Prevent instantiation as per Joshua Bloch's recommendation.
64
   */
65
  private RenderingSettings() {
66
  }
67
}
168
A src/main/java/com/keenwrite/preview/SvgRasterizer.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
5
import org.apache.batik.gvt.renderer.ImageRenderer;
6
import org.apache.batik.transcoder.TranscoderInput;
7
import org.apache.batik.transcoder.TranscoderOutput;
8
import org.apache.batik.transcoder.image.ImageTranscoder;
9
import org.w3c.dom.Document;
10
import org.w3c.dom.Element;
11
12
import javax.xml.transform.Transformer;
13
import javax.xml.transform.TransformerConfigurationException;
14
import javax.xml.transform.TransformerFactory;
15
import javax.xml.transform.dom.DOMSource;
16
import javax.xml.transform.stream.StreamResult;
17
import java.awt.*;
18
import java.awt.image.BufferedImage;
19
import java.io.File;
20
import java.io.StringReader;
21
import java.io.StringWriter;
22
import java.net.URI;
23
import java.nio.file.Path;
24
import java.text.NumberFormat;
25
26
import static com.keenwrite.StatusBarNotifier.clue;
27
import static com.keenwrite.preview.RenderingSettings.RENDERING_HINTS;
28
import static java.awt.image.BufferedImage.TYPE_INT_RGB;
29
import static java.nio.charset.StandardCharsets.UTF_8;
30
import static java.text.NumberFormat.getIntegerInstance;
31
import static javax.xml.transform.OutputKeys.*;
32
import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH;
33
import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName;
34
35
/**
36
 * Responsible for converting SVG images into rasterized PNG images.
37
 */
38
public class SvgRasterizer {
39
  private static final SAXSVGDocumentFactory FACTORY_DOM =
40
      new SAXSVGDocumentFactory( getXMLParserClassName() );
41
42
  private static final TransformerFactory FACTORY_TRANSFORM =
43
      TransformerFactory.newInstance();
44
45
  private static final Transformer sTransformer;
46
47
  static {
48
    Transformer t;
49
50
    try {
51
      t = FACTORY_TRANSFORM.newTransformer();
52
      t.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
53
      t.setOutputProperty( METHOD, "xml" );
54
      t.setOutputProperty( INDENT, "no" );
55
      t.setOutputProperty( ENCODING, UTF_8.name() );
56
    } catch( final TransformerConfigurationException e ) {
57
      t = null;
58
    }
59
60
    sTransformer = t;
61
  }
62
63
  private static final NumberFormat INT_FORMAT = getIntegerInstance();
64
65
  public static final BufferedImage BROKEN_IMAGE_PLACEHOLDER;
66
67
  /**
68
   * A FontAwesome camera icon, cleft asunder.
69
   */
70
  public static final String BROKEN_IMAGE_SVG =
71
      "<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www" +
72
          ".w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c" +
73
          ".332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path " +
74
          "d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531" +
75
          ".03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2" +
76
          ".511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-" +
77
          ".367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1" +
78
          ".699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 " +
79
          "2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094" +
80
          ".0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5" +
81
          ".0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-" +
82
          ".195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4" +
83
          ".191406-3.652344.207031-.015625.414063-.015625.617187-.007812l" +
84
          ".933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2" +
85
          ".820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3" +
86
          ".429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3" +
87
          ".429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 " +
88
          ".140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281" +
89
          ".808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2" +
90
          ".472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4" +
91
          ".285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1" +
92
          ".9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-" +
93
          ".667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1" +
94
          ".253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 " +
95
          "0'/></g></svg>";
96
97
  static {
98
    // The width and height cannot be embedded in the SVG above because the
99
    // path element values are relative to the viewBox dimensions.
100
    final int w = 75;
101
    final int h = 75;
102
    BufferedImage image;
103
104
    try {
105
      image = rasterizeString( BROKEN_IMAGE_SVG, w );
106
    } catch( final Exception ex ) {
107
      image = new BufferedImage( w, h, TYPE_INT_RGB );
108
      final var graphics = (Graphics2D) image.getGraphics();
109
      graphics.setRenderingHints( RENDERING_HINTS );
110
111
      // Fall back to a (\) symbol.
112
      graphics.setColor( new Color( 204, 204, 204 ) );
113
      graphics.fillRect( 0, 0, w, h );
114
      graphics.setColor( new Color( 255, 204, 204 ) );
115
      graphics.setStroke( new BasicStroke( 4 ) );
116
      graphics.drawOval( w / 4, h / 4, w / 2, h / 2 );
117
      graphics.drawLine( w / 4 + (int) (w / 4 / Math.PI),
118
                         h / 4 + (int) (w / 4 / Math.PI),
119
                         w / 2 + w / 4 - (int) (w / 4 / Math.PI),
120
                         h / 2 + h / 4 - (int) (w / 4 / Math.PI) );
121
    }
122
123
    BROKEN_IMAGE_PLACEHOLDER = image;
124
  }
125
126
  /**
127
   * Responsible for creating a new {@link ImageRenderer} implementation that
128
   * can render a DOM as an SVG image.
129
   */
130
  private static class BufferedImageTranscoder extends ImageTranscoder {
131
    private BufferedImage mImage;
132
133
    @Override
134
    public BufferedImage createImage( final int w, final int h ) {
135
      return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB );
136
    }
137
138
    @Override
139
    public void writeImage(
140
        final BufferedImage image, final TranscoderOutput output ) {
141
      mImage = image;
142
    }
143
144
    public BufferedImage getImage() {
145
      return mImage;
146
    }
147
148
    @Override
149
    protected ImageRenderer createRenderer() {
150
      final ImageRenderer renderer = super.createRenderer();
151
      final RenderingHints hints = renderer.getRenderingHints();
152
      hints.putAll( RENDERING_HINTS );
153
      renderer.setRenderingHints( hints );
154
155
      return renderer;
156
    }
157
  }
158
159
  /**
160
   * Rasterizes the given document into an image.
161
   *
162
   * @param svg   The SVG {@link Document} to rasterize.
163
   * @param width The rasterized image's width (in pixels).
164
   * @return The rasterized image.
165
   */
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 );
170
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
    }
177
178
    return BROKEN_IMAGE_PLACEHOLDER;
179
  }
180
181
  public static BufferedImage rasterize( final Document document ) {
182
    try {
183
      final var root = document.getDocumentElement();
184
      final var width = root.getAttribute( "width" );
185
      return rasterize( document, INT_FORMAT.parse( width ).intValue() );
186
    } catch( final Exception ex ) {
187
      clue( ex );
188
    }
189
190
    return BROKEN_IMAGE_PLACEHOLDER;
191
  }
192
193
  /**
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
222
   * a graphics context.
223
   *
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.
227
   * @return The vector graphic transcoded into a raster image format.
228
   */
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;
237
  }
238
239
  /**
240
   * Converts an SVG string into a rasterized image that can be drawn on
241
   * a graphics context. The dimensions are determined from the document.
242
   *
243
   * @param xml The SVG xml document.
244
   * @return The vector graphic transcoded into a raster image format.
245
   */
246
  public static BufferedImage rasterizeString( final String xml ) {
247
    try {
248
      final var document = toDocument( xml );
249
      final var root = document.getDocumentElement();
250
      final var width = root.getAttribute( "width" );
251
      return rasterizeString( xml, INT_FORMAT.parse( width ).intValue() );
252
    } catch( final Exception ex ) {
253
      clue( ex );
254
    }
255
256
    return BROKEN_IMAGE_PLACEHOLDER;
257
  }
258
259
  /**
260
   * Converts an SVG string into a rasterized image that can be drawn on
261
   * a graphics context.
262
   *
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.
267
   */
268
  public static BufferedImage rasterizeString( final String svg, final int w ) {
269
    return rasterize( toDocument( svg ), w );
270
  }
271
272
  /**
273
   * Given a document object model (DOM) {@link Element}, this will convert that
274
   * element to a string.
275
   *
276
   * @param e The DOM node to convert to a string.
277
   * @return The DOM node as an escaped, plain text string.
278
   */
279
  public static String toSvg( final Element e ) {
280
    try( final var writer = new StringWriter() ) {
281
      sTransformer.transform( new DOMSource( e ), new StreamResult( writer ) );
282
      return writer.toString().replaceAll( "xmlns=\"\" ", "" );
283
    } catch( final Exception ex ) {
284
      clue( ex );
285
    }
286
287
    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
    }
303
  }
304
}
1305
A src/main/java/com/keenwrite/preview/SvgReplacedElementFactory.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import com.keenwrite.io.HttpMediaType;
5
import com.keenwrite.io.MediaType;
6
import com.keenwrite.ui.adapters.ReplacedElementAdapter;
7
import org.w3c.dom.Element;
8
import org.xhtmlrenderer.extend.ReplacedElement;
9
import org.xhtmlrenderer.extend.UserAgentCallback;
10
import org.xhtmlrenderer.layout.LayoutContext;
11
import org.xhtmlrenderer.render.BlockBox;
12
import org.xhtmlrenderer.swing.ImageReplacedElement;
13
14
import java.awt.image.BufferedImage;
15
import java.net.URI;
16
import java.nio.file.Paths;
17
18
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;
22
import static com.keenwrite.preview.SvgRasterizer.rasterize;
23
import static com.keenwrite.processors.markdown.tex.TexNode.HTML_TEX;
24
import static com.keenwrite.util.ProtocolScheme.getProtocol;
25
26
/**
27
 * Responsible for running {@link SvgRasterizer} on SVG images detected within
28
 * a document to transform them into rasterized versions.
29
 */
30
public class SvgReplacedElementFactory extends ReplacedElementAdapter {
31
32
  public static final String HTML_IMAGE = "img";
33
  public static final String HTML_IMAGE_SRC = "src";
34
35
  private static final ImageReplacedElement BROKEN_IMAGE =
36
    createImageReplacedElement( BROKEN_IMAGE_PLACEHOLDER );
37
38
  @Override
39
  public ReplacedElement createReplacedElement(
40
    final LayoutContext c,
41
    final BlockBox box,
42
    final UserAgentCallback uac,
43
    final int cssWidth,
44
    final int cssHeight ) {
45
    final var e = box.getElement();
46
47
    ImageReplacedElement image = null;
48
49
    try {
50
      BufferedImage raster = null;
51
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
            }
68
          }
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
          }
78
        }
79
        case HTML_TEX ->
80
          // Convert the TeX element to a raster graphic.
81
          raster = rasterize( MATH_RENDERER.render( e.getTextContent() ) );
82
      }
83
84
      if( raster != null ) {
85
        image = createImageReplacedElement( raster );
86
      }
87
    } catch( final Exception ex ) {
88
      image = BROKEN_IMAGE;
89
      clue( ex );
90
    }
91
92
    return image;
93
  }
94
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();
101
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 "";
118
  }
119
120
  private static ImageReplacedElement createImageReplacedElement(
121
    final BufferedImage bi ) {
122
    return new ImageReplacedElement( bi, bi.getWidth(), bi.getHeight() );
123
  }
124
125
  private static boolean isSvg( final MediaType mediaType ) {
126
    return mediaType == TEXT_PLAIN || mediaType == IMAGE_SVG_XML;
127
  }
128
}
1129
A src/main/java/com/keenwrite/processors/DefinitionProcessor.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import java.util.Map;
5
6
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
7
8
/**
9
 * Processes interpolated string definitions in the document and inserts
10
 * their values into the post-processed text. The default variable syntax is
11
 * {@code $variable$}.
12
 */
13
public class DefinitionProcessor extends ExecutorProcessor<String> {
14
15
  private final Map<String, String> mDefinitions;
16
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
   */
23
  public DefinitionProcessor(
24
      final Processor<String> successor,
25
      final ProcessorContext context ) {
26
    super( successor );
27
    mDefinitions = context.getResolvedMap();
28
  }
29
30
  /**
31
   * Processes the given text document by replacing variables with their values.
32
   *
33
   * @param text The document text that includes variables that should be
34
   *             replaced with values when rendered as HTML.
35
   * @return The text with all variables replaced.
36
   */
37
  @Override
38
  public String apply( final String text ) {
39
    return replace( text, getDefinitions() );
40
  }
41
42
  /**
43
   * Returns the map to use for variable substitution.
44
   *
45
   * @return A map of variable names to values.
46
   */
47
  protected Map<String, String> getDefinitions() {
48
    return mDefinitions;
49
  }
50
}
151
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
A src/main/java/com/keenwrite/processors/HtmlPreviewProcessor.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import com.keenwrite.preview.HtmlPreview;
5
6
/**
7
 * Responsible for notifying the HTMLPreviewPane when the succession chain has
8
 * updated. This decouples knowledge of changes to the editor panel from the
9
 * HTML preview panel as well as any processing that takes place before the
10
 * final HTML preview is rendered. This is the last link in the processor
11
 * chain.
12
 */
13
public class HtmlPreviewProcessor extends ExecutorProcessor<String> {
14
15
  /**
16
   * There is only one preview panel.
17
   */
18
  private static HtmlPreview sHtmlPreviewPane;
19
20
  /**
21
   * Constructs the end of a processing chain.
22
   *
23
   * @param htmlPreviewPane The pane to update with the post-processed document.
24
   */
25
  public HtmlPreviewProcessor( final HtmlPreview htmlPreviewPane ) {
26
    sHtmlPreviewPane = htmlPreviewPane;
27
  }
28
29
  /**
30
   * Update the preview panel using HTML from the succession chain.
31
   *
32
   * @param html The document content to render in the preview pane. The HTML
33
   *             should not contain a doctype, head, or body tag, only
34
   *             content to render within the body.
35
   * @return The given {@code html} string.
36
   */
37
  @Override
38
  public String apply( final String html ) {
39
    assert html != null;
40
41
    getHtmlPreviewPane().render( html );
42
    return html;
43
  }
44
45
  private HtmlPreview getHtmlPreviewPane() {
46
    return sHtmlPreviewPane;
47
  }
48
}
149
A src/main/java/com/keenwrite/processors/IdentityProcessor.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
/**
5
 * Responsible for transforming a string into itself. This is used at the
6
 * end of a processing chain when no more processing is required.
7
 */
8
public class IdentityProcessor extends ExecutorProcessor<String> {
9
  public static final IdentityProcessor INSTANCE = new IdentityProcessor();
10
11
  /**
12
   * Constructs a new instance having no successor (the default successor is
13
   * {@code null}).
14
   */
15
  private IdentityProcessor() {
16
  }
17
18
  /**
19
   * Returns the given string without modification.
20
   *
21
   * @param s The string to return.
22
   * @return The value of s.
23
   */
24
  @Override
25
  public String apply( final String s ) {
26
    return s;
27
  }
28
}
129
A src/main/java/com/keenwrite/processors/InlineRProcessor.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import com.keenwrite.preferences.Workspace;
5
import com.keenwrite.processors.markdown.MarkdownProcessor;
6
import com.vladsch.flexmark.ast.Paragraph;
7
import com.vladsch.flexmark.ast.Text;
8
import javafx.beans.property.Property;
9
10
import javax.script.ScriptEngine;
11
import javax.script.ScriptEngineManager;
12
import java.io.File;
13
import java.nio.file.Path;
14
import java.util.LinkedHashMap;
15
import java.util.Map;
16
import java.util.concurrent.atomic.AtomicBoolean;
17
18
import static com.keenwrite.Constants.STATUS_PARSE_ERROR;
19
import static com.keenwrite.StatusBarNotifier.clue;
20
import static com.keenwrite.preferences.Workspace.*;
21
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
22
import static com.keenwrite.sigils.RSigilOperator.PREFIX;
23
import static com.keenwrite.sigils.RSigilOperator.SUFFIX;
24
import static java.lang.Math.min;
25
26
/**
27
 * Transforms a document containing R statements into Markdown.
28
 */
29
public final class InlineRProcessor extends DefinitionProcessor {
30
  /**
31
   * Constrain memory when typing new R expressions into the document.
32
   */
33
  private static final int MAX_CACHED_R_STATEMENTS = 512;
34
35
  private final MarkdownProcessor mMarkdownProcessor;
36
37
  /**
38
   * Where to put document inline evaluated R expressions.
39
   */
40
  private final Map<String, String> mEvalCache = new LinkedHashMap<>() {
41
    @Override
42
    protected boolean removeEldestEntry(
43
      final Map.Entry<String, String> eldest ) {
44
      return size() > MAX_CACHED_R_STATEMENTS;
45
    }
46
  };
47
48
  private static final ScriptEngine ENGINE =
49
    (new ScriptEngineManager()).getEngineByName( "Renjin" );
50
51
  private static final int PREFIX_LENGTH = PREFIX.length();
52
53
  private final AtomicBoolean mDirty = new AtomicBoolean( false );
54
55
  private final Workspace mWorkspace;
56
57
  /**
58
   * Constructs a processor capable of evaluating R statements.
59
   *
60
   * @param successor Subsequent link in the processing chain.
61
   * @param context   Contains resolved definitions map.
62
   */
63
  public InlineRProcessor(
64
    final Processor<String> successor,
65
    final ProcessorContext context ) {
66
    super( successor, context );
67
68
    mWorkspace = context.getWorkspace();
69
    mMarkdownProcessor = MarkdownProcessor.create( context );
70
71
    bootstrapScriptProperty().addListener(
72
      ( __, oldScript, newScript ) -> setDirty( true ) );
73
    workingDirectoryProperty().addListener(
74
      ( __, oldScript, newScript ) -> setDirty( true ) );
75
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
//    } );
86
87
    init();
88
  }
89
90
  /**
91
   * Initialises the R code so that R can find imported libraries. Note that
92
   * any existing R functionality will not be overwritten if this method is
93
   * called multiple times.
94
   */
95
  private void init() {
96
    final var bootstrap = getBootstrapScript();
97
98
    if( !bootstrap.isBlank() ) {
99
      final var wd = getWorkingDirectory();
100
      final var dir = wd.toString().replace( '\\', '/' );
101
      final var map = getDefinitions();
102
      final var defBegan = mWorkspace.toString( KEY_DEF_DELIM_BEGAN );
103
      final var defEnded = mWorkspace.toString( KEY_DEF_DELIM_ENDED );
104
105
      map.put( defBegan + "application.r.working.directory" + defEnded, dir );
106
107
      eval( replace( bootstrap, map ) );
108
    }
109
  }
110
111
  /**
112
   * Sets the dirty flag to indicate that the bootstrap script or working
113
   * directory has been modified. Upon saving the preferences, if this flag
114
   * is true, then {@link #init()} will be called to reload the R environment.
115
   *
116
   * @param dirty Set to true to reload changes upon closing preferences.
117
   */
118
  private void setDirty( final boolean dirty ) {
119
    mDirty.set( dirty );
120
  }
121
122
  /**
123
   * Answers whether R-related settings have been modified.
124
   *
125
   * @return {@code true} when the settings have changed.
126
   */
127
  private boolean isDirty() {
128
    return mDirty.get();
129
  }
130
131
  /**
132
   * Evaluates all R statements in the source document and inserts the
133
   * calculated value into the generated document.
134
   *
135
   * @param text The document text that includes variables that should be
136
   *             replaced with values when rendered as HTML.
137
   * @return The generated document with output from all R statements
138
   * substituted with value returned from their execution.
139
   */
140
  @Override
141
  public String apply( final String text ) {
142
    final int length = text.length();
143
144
    // The * 2 is a wild guess at the ratio of R statements to the length
145
    // of text produced by those statements.
146
    final StringBuilder sb = new StringBuilder( length * 2 );
147
148
    int prevIndex = 0;
149
    int currIndex = text.indexOf( PREFIX );
150
151
    while( currIndex >= 0 ) {
152
      // Copy everything up to, but not including, the opening token.
153
      sb.append( text, prevIndex, currIndex );
154
155
      // Jump to the start of the R statement.
156
      prevIndex = currIndex + PREFIX_LENGTH;
157
158
      // Find the closing token, without indexing past the text boundary.
159
      currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) );
160
161
      // Only evaluate inline R statements that have end delimiters.
162
      if( currIndex > 1 ) {
163
        // Extract the inline R statement to be evaluated.
164
        final String r = text.substring( prevIndex, currIndex );
165
166
        // Pass the R statement into the R engine for evaluation.
167
        try {
168
          final var result = evalCached( r );
169
170
          // Append the string representation of the result into the text.
171
          sb.append( result );
172
        } catch( final Exception ex ) {
173
          // Inform the user that there was a problem.
174
          clue( STATUS_PARSE_ERROR, ex.getMessage(), currIndex );
175
176
          // If the string couldn't be parsed using R, append the statement
177
          // that failed to parse, instead of its evaluated value.
178
          sb.append( PREFIX ).append( r ).append( SUFFIX );
179
        }
180
181
        // Retain the R statement's ending position in the text.
182
        prevIndex = currIndex + 1;
183
      }
184
185
      // Find the start of the next inline R statement.
186
      currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) );
187
    }
188
189
    // Copy from the previous index to the end of the string.
190
    return sb.append( text.substring( min( prevIndex, length ) ) ).toString();
191
  }
192
193
  /**
194
   * Look up an R expression from the cache then return the resulting object.
195
   * If the R expression hasn't been cached, it'll first be evaluated.
196
   *
197
   * @param r The expression to evaluate.
198
   * @return The object resulting from the evaluation.
199
   */
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();
223
  }
224
225
  /**
226
   * Evaluate an R expression and return the resulting object.
227
   *
228
   * @param r The expression to evaluate.
229
   * @return The object resulting from the evaluation.
230
   */
231
  private String eval( final String r ) {
232
    try {
233
      return ENGINE.eval( r ).toString();
234
    } catch( final Exception ex ) {
235
      final var expr = r.substring( 0, min( r.length(), 30 ) );
236
      clue( "Main.status.error.r", expr, ex.getMessage() );
237
      return "";
238
    }
239
  }
240
241
  /**
242
   * Return the given path if not {@code null}, otherwise return the path to
243
   * the user's directory.
244
   *
245
   * @return A non-null path.
246
   */
247
  private Path getWorkingDirectory() {
248
    return workingDirectoryProperty().getValue().toPath();
249
  }
250
251
  private Property<File> workingDirectoryProperty() {
252
    return getWorkspace().fileProperty( KEY_R_DIR );
253
  }
254
255
  /**
256
   * Loads the R init script from the application's persisted preferences.
257
   *
258
   * @return A non-null string, possibly empty.
259
   */
260
  private String getBootstrapScript() {
261
    return bootstrapScriptProperty().getValue();
262
  }
263
264
  private Property<String> bootstrapScriptProperty() {
265
    return getWorkspace().valuesProperty( KEY_R_SCRIPT );
266
  }
267
268
  private Workspace getWorkspace() {
269
    return mWorkspace;
270
  }
271
}
1272
A src/main/java/com/keenwrite/processors/PreformattedProcessor.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
/**
5
 * This is the default processor used when an unknown file name extension is
6
 * encountered. It processes the text by enclosing it in an HTML {@code <pre>}
7
 * element.
8
 */
9
public class PreformattedProcessor extends ExecutorProcessor<String> {
10
11
  /**
12
   * Passes the link to the super constructor.
13
   *
14
   * @param successor The next processor in the chain to use for text
15
   *                  processing.
16
   */
17
  public PreformattedProcessor( final Processor<String> successor ) {
18
    super( successor );
19
  }
20
21
  /**
22
   * Returns the given string, modified with "pre" tags.
23
   *
24
   * @param t The string to return, enclosed in "pre" tags.
25
   * @return The value of t wrapped in "pre" tags.
26
   */
27
  @Override
28
  public String apply( final String t ) {
29
    return "<pre>" + t + "</pre>";
30
  }
31
}
132
A src/main/java/com/keenwrite/processors/Processor.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.function.UnaryOperator;
6
7
/**
8
 * Responsible for processing documents from one known format to another.
9
 * Processes the given content providing a transformation from one document
10
 * format into another. For example, this could convert Markdown to HTML.
11
 *
12
 * @param <T> The data type to process.
13
 */
14
public interface Processor<T> extends UnaryOperator<T> {
15
16
  /**
17
   * Returns the next link in the processor chain.
18
   *
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.
22
   */
23
  default Optional<Processor<T>> next() {
24
    return Optional.empty();
25
  }
26
}
127
A src/main/java/com/keenwrite/processors/ProcessorContext.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import com.keenwrite.Constants;
5
import com.keenwrite.ExportFormat;
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;
10
11
import java.nio.file.Path;
12
import java.util.Map;
13
14
import static com.keenwrite.AbstractFileFactory.lookup;
15
import static com.keenwrite.Constants.DEFAULT_DIRECTORY;
16
17
/**
18
 * Provides a context for configuring a chain of {@link Processor} instances.
19
 */
20
public class ProcessorContext {
21
  private final HtmlPreview mHtmlPreview;
22
  private final Map<String, String> mResolvedMap;
23
  private final Path mPath;
24
  private final Caret mCaret;
25
  private final ExportFormat mExportFormat;
26
  private final Workspace mWorkspace;
27
28
  /**
29
   * Creates a new context for use by the {@link ProcessorFactory} when
30
   * instantiating new {@link Processor} instances. Although all the
31
   * parameters are required, not all {@link Processor} instances will use
32
   * all parameters.
33
   *
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.
40
   */
41
  public ProcessorContext(
42
      final HtmlPreview htmlPreview,
43
      final Map<String, String> resolvedMap,
44
      final Path path,
45
      final Caret caret,
46
      final ExportFormat exportFormat,
47
      final Workspace workspace ) {
48
    assert htmlPreview != null;
49
    assert resolvedMap != null;
50
    assert path != null;
51
    assert caret != null;
52
    assert exportFormat != null;
53
    assert workspace != null;
54
55
    mHtmlPreview = htmlPreview;
56
    mResolvedMap = resolvedMap;
57
    mPath = path;
58
    mCaret = caret;
59
    mExportFormat = exportFormat;
60
    mWorkspace = workspace;
61
  }
62
63
  @SuppressWarnings("SameParameterValue")
64
  boolean isExportFormat( final ExportFormat format ) {
65
    return mExportFormat == format;
66
  }
67
68
  HtmlPreview getPreview() {
69
    return mHtmlPreview;
70
  }
71
72
  /**
73
   * Returns the variable map of interpolated definitions.
74
   *
75
   * @return A map to help dereference variables.
76
   */
77
  Map<String, String> getResolvedMap() {
78
    return mResolvedMap;
79
  }
80
81
  public ExportFormat getExportFormat() {
82
    return mExportFormat;
83
  }
84
85
  /**
86
   * Returns the current caret position in the document being edited and is
87
   * always up-to-date.
88
   *
89
   * @return Caret position in the document.
90
   */
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;
110
  }
111
112
  public Path getPath() {
113
    return mPath;
114
  }
115
116
  FileType getFileType() {
117
    return lookup( getPath() );
118
  }
119
120
  public Workspace getWorkspace() {
121
    return mWorkspace;
122
  }
123
}
1124
A src/main/java/com/keenwrite/processors/ProcessorFactory.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import com.keenwrite.AbstractFileFactory;
5
import com.keenwrite.preview.HtmlPreview;
6
import com.keenwrite.processors.markdown.MarkdownProcessor;
7
8
import static com.keenwrite.ExportFormat.NONE;
9
10
/**
11
 * Responsible for creating processors capable of parsing, transforming,
12
 * interpolating, and rendering known file types.
13
 */
14
public class ProcessorFactory extends AbstractFileFactory {
15
16
  private final ProcessorContext mContext;
17
18
  /**
19
   * Constructs a factory with the ability to create processors that can perform
20
   * text and caret processing to generate a final preview.
21
   *
22
   * @param context Parameters needed to construct various processors.
23
   */
24
  private ProcessorFactory( final ProcessorContext context ) {
25
    mContext = context;
26
  }
27
28
  private Processor<String> createProcessor() {
29
    final var context = getProcessorContext();
30
31
    // If the content is not to be exported, then the successor processor
32
    // is one that parses Markdown into HTML and passes the string to the
33
    // HTML preview pane.
34
    //
35
    // Otherwise, bolt on a processor that--after the interpolation and
36
    // substitution phase, which includes text strings or R code---will
37
    // generate HTML or plain Markdown. HTML has a few output formats:
38
    // with embedded SVG representing formulas, or without any conversion
39
    // to SVG. Without conversion would require client-side rendering of
40
    // math (such as using the JavaScript-based KaTeX engine).
41
    final var successor = context.isExportFormat( NONE )
42
      ? createHtmlPreviewProcessor()
43
      : createIdentityProcessor();
44
45
    final var processor = switch( context.getFileType() ) {
46
      case RMARKDOWN -> createRProcessor( successor );
47
      case SOURCE -> createMarkdownProcessor( successor );
48
      case RXML -> createRXMLProcessor( successor );
49
      case XML -> createXMLProcessor( successor );
50
      default -> createPreformattedProcessor( successor );
51
    };
52
53
    return new ExecutorProcessor<>( processor );
54
  }
55
56
  /**
57
   * Creates a processor chain suitable for parsing and rendering the file
58
   * opened at the given tab.
59
   *
60
   * @param context The tab containing a text editor, path, and caret position.
61
   * @return A processor that can render the given tab's text.
62
   */
63
  public static Processor<String> createProcessors(
64
    final ProcessorContext context ) {
65
    return new ProcessorFactory( context ).createProcessor();
66
  }
67
68
  /**
69
   * Instantiates a new {@link Processor} that has no successor and returns
70
   * the string it was given without modification.
71
   *
72
   * @return An instance of {@link Processor} that performs no processing.
73
   */
74
  private Processor<String> createIdentityProcessor() {
75
    return IdentityProcessor.INSTANCE;
76
  }
77
78
  /**
79
   * Instantiates a new {@link Processor} that passes an incoming HTML
80
   * string to a user interface widget that can render HTML as a web page.
81
   *
82
   * @return An instance of {@link Processor} that forwards HTML for display.
83
   */
84
  private Processor<String> createHtmlPreviewProcessor() {
85
    return new HtmlPreviewProcessor( getPreviewPane() );
86
  }
87
88
  /**
89
   * Instantiates a {@link Processor} responsible for parsing Markdown and
90
   * definitions.
91
   *
92
   * @return A chain of {@link Processor}s for processing Markdown and
93
   * definitions.
94
   */
95
  private Processor<String> createMarkdownProcessor(
96
    final Processor<String> successor ) {
97
    final var dp = createDefinitionProcessor( successor );
98
    return MarkdownProcessor.create( dp, getProcessorContext() );
99
  }
100
101
  private Processor<String> createDefinitionProcessor(
102
    final Processor<String> successor ) {
103
    return new DefinitionProcessor( successor, getProcessorContext() );
104
  }
105
106
  private Processor<String> createRProcessor(
107
    final Processor<String> successor ) {
108
    final var irp = new InlineRProcessor( successor, getProcessorContext() );
109
    final var rvp = new RVariableProcessor( irp, getProcessorContext() );
110
    return MarkdownProcessor.create( rvp, getProcessorContext() );
111
  }
112
113
  protected Processor<String> createRXMLProcessor(
114
    final Processor<String> successor ) {
115
    final var xmlp = new XmlProcessor( successor, getProcessorContext() );
116
    return createRProcessor( xmlp );
117
  }
118
119
  private Processor<String> createXMLProcessor(
120
    final Processor<String> successor ) {
121
    final var xmlp = new XmlProcessor( successor, getProcessorContext() );
122
    return createDefinitionProcessor( xmlp );
123
  }
124
125
  private Processor<String> createPreformattedProcessor(
126
    final Processor<String> successor ) {
127
    return new PreformattedProcessor( successor );
128
  }
129
130
  private ProcessorContext getProcessorContext() {
131
    return mContext;
132
  }
133
134
  private HtmlPreview getPreviewPane() {
135
    return getProcessorContext().getPreview();
136
  }
137
}
1138
A src/main/java/com/keenwrite/processors/RVariableProcessor.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import com.keenwrite.preferences.Workspace;
5
import com.keenwrite.sigils.RSigilOperator;
6
import com.keenwrite.sigils.SigilOperator;
7
import com.keenwrite.sigils.YamlSigilOperator;
8
9
import java.util.HashMap;
10
import java.util.Map;
11
12
import static com.keenwrite.preferences.Workspace.*;
13
14
/**
15
 * Converts the keys of the resolved map from default form to R form, then
16
 * performs a substitution on the text. The default R variable syntax is
17
 * {@code v$tree$leaf}.
18
 */
19
public class RVariableProcessor extends DefinitionProcessor {
20
21
  private final SigilOperator mSigilOperator;
22
23
  public RVariableProcessor(
24
    final InlineRProcessor irp, final ProcessorContext context ) {
25
    super( irp, context );
26
    mSigilOperator = createSigilOperator( context.getWorkspace() );
27
  }
28
29
  /**
30
   * Returns the R-based version of the interpolated variable definitions.
31
   *
32
   * @return Variable names transmogrified from the default syntax to R syntax.
33
   */
34
  @Override
35
  protected Map<String, String> getDefinitions() {
36
    return toR( super.getDefinitions() );
37
  }
38
39
  /**
40
   * Converts the given map from regular variables to R variables.
41
   *
42
   * @param map Map of variable names to values.
43
   * @return Map of R variables.
44
   */
45
  private Map<String, String> toR( final Map<String, String> map ) {
46
    final var rMap = new HashMap<String, String>( map.size() );
47
48
    for( final var entry : map.entrySet() ) {
49
      final var key = entry.getKey();
50
      rMap.put( mSigilOperator.entoken( key ), toRValue( map.get( key ) ) );
51
    }
52
53
    return rMap;
54
  }
55
56
  private String toRValue( final String value ) {
57
    return '\'' + escape( value, '\'', "\\'" ) + '\'';
58
  }
59
60
  /**
61
   * TODO: Make generic method for replacing text.
62
   *
63
   * @param haystack Search this string for the needle, must not be null.
64
   * @param needle   The character to find in the haystack.
65
   * @param thread   Replace the needle with this text, if the needle is found.
66
   * @return The haystack with the all instances of needle replaced with thread.
67
   */
68
  @SuppressWarnings("SameParameterValue")
69
  private String escape(
70
    final String haystack, final char needle, final String thread ) {
71
    int end = haystack.indexOf( needle );
72
73
    if( end < 0 ) {
74
      return haystack;
75
    }
76
77
    final int length = haystack.length();
78
    int start = 0;
79
80
    // Replace up to 32 occurrences before the string reallocates its buffer.
81
    final var sb = new StringBuilder( length + 32 );
82
83
    while( end >= 0 ) {
84
      sb.append( haystack, start, end ).append( thread );
85
      start = end + 1;
86
      end = haystack.indexOf( needle, start );
87
    }
88
89
    return sb.append( haystack.substring( start ) ).toString();
90
  }
91
92
  private SigilOperator createSigilOperator( final Workspace workspace ) {
93
    final var tokens = workspace.toTokens(
94
      KEY_R_DELIM_BEGAN, KEY_R_DELIM_ENDED );
95
    final var antecedent = createDefinitionOperator( workspace );
96
    return new RSigilOperator( tokens, antecedent );
97
  }
98
99
  private SigilOperator createDefinitionOperator(
100
    final Workspace workspace ) {
101
    final var tokens = workspace.toTokens(
102
      KEY_DEF_DELIM_BEGAN, KEY_DEF_DELIM_ENDED );
103
    return new YamlSigilOperator( tokens );
104
  }
105
}
1106
A src/main/java/com/keenwrite/processors/XmlProcessor.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import com.keenwrite.Services;
5
import com.keenwrite.service.Snitch;
6
import net.sf.saxon.TransformerFactoryImpl;
7
import net.sf.saxon.trans.XPathException;
8
9
import javax.xml.stream.XMLEventReader;
10
import javax.xml.stream.XMLInputFactory;
11
import javax.xml.stream.XMLStreamException;
12
import javax.xml.stream.events.ProcessingInstruction;
13
import javax.xml.transform.*;
14
import javax.xml.transform.stream.StreamResult;
15
import javax.xml.transform.stream.StreamSource;
16
import java.io.Reader;
17
import java.io.StringReader;
18
import java.io.StringWriter;
19
import java.nio.file.Path;
20
import java.nio.file.Paths;
21
22
import static net.sf.saxon.tree.util.ProcInstParser.getPseudoAttribute;
23
24
/**
25
 * Transforms an XML document. The XML document must have a stylesheet specified
26
 * as part of its processing instructions, such as:
27
 * <p>
28
 * {@code xml-stylesheet type="text/xsl" href="markdown.xsl"}
29
 * </p>
30
 * <p>
31
 * The XSL must transform the XML document into Markdown, or another format
32
 * recognized by the next link on the chain.
33
 * </p>
34
 */
35
public class XmlProcessor extends ExecutorProcessor<String>
36
    implements ErrorListener {
37
38
  private final Snitch mSnitch = Services.load( Snitch.class );
39
40
  private final XMLInputFactory mXmlInputFactory =
41
      XMLInputFactory.newInstance();
42
  private final TransformerFactory mTransformerFactory =
43
      new TransformerFactoryImpl();
44
  private Transformer mTransformer;
45
46
  private final Path mPath;
47
48
  /**
49
   * Constructs an XML processor that can transform an XML document into another
50
   * format based on the XSL file specified as a processing instruction. The
51
   * path must point to the directory where the XSL file is found, which implies
52
   * that they must be in the same directory.
53
   *
54
   * @param successor Next link in the processing chain.
55
   * @param context   Contains path to the XML file content to be processed.
56
   */
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 );
65
  }
66
67
  /**
68
   * Transforms the given XML text into another form (typically Markdown).
69
   *
70
   * @param text The text to transform, can be empty, cannot be null.
71
   * @return The transformed text, or empty if text is empty.
72
   */
73
  @Override
74
  public String apply( final String text ) {
75
    try {
76
      return text.isEmpty() ? text : transform( text );
77
    } catch( final Exception ex ) {
78
      throw new RuntimeException( ex );
79
    }
80
  }
81
82
  /**
83
   * Performs an XSL transformation on the given XML text. The XML text must
84
   * have a processing instruction that points to the XSL template file to use
85
   * for the transformation.
86
   *
87
   * @param text The text to transform.
88
   * @return The transformed text.
89
   */
90
  private String transform( final String text ) throws Exception {
91
    // Extract the XML stylesheet processing instruction.
92
    final String template = getXsltFilename( text );
93
    final Path xsl = getXslPath( template );
94
95
    try(
96
        final StringWriter output = new StringWriter( text.length() );
97
        final StringReader input = new StringReader( text ) ) {
98
99
      // Listen for external file modification events.
100
      mSnitch.listen( xsl );
101
102
      getTransformer( xsl ).transform(
103
          new StreamSource( input ),
104
          new StreamResult( output )
105
      );
106
107
      return output.toString();
108
    }
109
  }
110
111
  /**
112
   * Returns an XSL transformer ready to transform an XML document using the
113
   * XSLT file specified by the given path. If the path is already known then
114
   * this will return the associated transformer.
115
   *
116
   * @param xsl The path to an XSLT file.
117
   * @return A transformer that will transform XML documents using the given
118
   * XSLT file.
119
   * @throws TransformerConfigurationException Could not instantiate the
120
   *                                           transformer.
121
   */
122
  private synchronized Transformer getTransformer( final Path xsl )
123
      throws TransformerConfigurationException {
124
    if( mTransformer == null ) {
125
      mTransformer = createTransformer( xsl );
126
    }
127
128
    return mTransformer;
129
  }
130
131
  /**
132
   * Creates a configured transformer ready to run.
133
   *
134
   * @param xsl The stylesheet to use for transforming XML documents.
135
   * @return The edited XML document transformed into another format (usually
136
   * markdown).
137
   * @throws TransformerConfigurationException Could not create the transformer.
138
   */
139
  protected Transformer createTransformer( final Path xsl )
140
      throws TransformerConfigurationException {
141
    final var xslt = new StreamSource( xsl.toFile() );
142
143
    return getTransformerFactory().newTransformer( xslt );
144
  }
145
146
  private Path getXslPath( final String filename ) {
147
    final var xmlDirectory = mPath.toFile().getParentFile();
148
149
    return Paths.get( xmlDirectory.getPath(), filename );
150
  }
151
152
  /**
153
   * Given XML text, this will use a StAX pull reader to obtain the XML
154
   * stylesheet processing instruction. This will throw a parse exception if the
155
   * href pseudo-attribute file name value cannot be found.
156
   *
157
   * @param xml The XML containing an xml-stylesheet processing instruction.
158
   * @return The href pseudo-attribute value.
159
   * @throws XMLStreamException Could not parse the XML file.
160
   */
161
  private String getXsltFilename( final String xml )
162
      throws XMLStreamException, XPathException {
163
    String result = "";
164
165
    try( final StringReader sr = new StringReader( xml ) ) {
166
      final XMLEventReader reader = createXmlEventReader( sr );
167
      boolean found = false;
168
      int count = 0;
169
170
      // If the processing instruction wasn't found in the first 10 lines,
171
      // fail fast. This should iterate twice through the loop.
172
      while( !found && reader.hasNext() && count++ < 10 ) {
173
        final var event = reader.nextEvent();
174
175
        if( event.isProcessingInstruction() ) {
176
          final var pi = (ProcessingInstruction) event;
177
          final var target = pi.getTarget();
178
179
          if( "xml-stylesheet".equalsIgnoreCase( target ) ) {
180
            result = getPseudoAttribute( pi.getData(), "href" );
181
            found = true;
182
          }
183
        }
184
      }
185
    }
186
187
    return result;
188
  }
189
190
  private XMLEventReader createXmlEventReader( final Reader reader )
191
      throws XMLStreamException {
192
    return mXmlInputFactory.createXMLEventReader( reader );
193
  }
194
195
  private synchronized TransformerFactory getTransformerFactory() {
196
    return mTransformerFactory;
197
  }
198
199
  /**
200
   * Called when the XSL transformer issues a warning.
201
   *
202
   * @param ex The problem the transformer encountered.
203
   */
204
  @Override
205
  public void warning( final TransformerException ex ) {
206
    throw new RuntimeException( ex );
207
  }
208
209
  /**
210
   * Called when the XSL transformer issues an error.
211
   *
212
   * @param ex The problem the transformer encountered.
213
   */
214
  @Override
215
  public void error( final TransformerException ex ) {
216
    throw new RuntimeException( ex );
217
  }
218
219
  /**
220
   * Called when the XSL transformer issues a fatal error, which is probably
221
   * a bit over-dramatic a method name.
222
   *
223
   * @param ex The problem the transformer encountered.
224
   */
225
  @Override
226
  public void fatalError( final TransformerException ex ) {
227
    throw new RuntimeException( ex );
228
  }
229
}
1230
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
A src/main/java/com/keenwrite/processors/markdown/CaretExtension.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown;
3
4
import com.keenwrite.Constants;
5
import com.vladsch.flexmark.html.AttributeProvider;
6
import com.vladsch.flexmark.html.AttributeProviderFactory;
7
import com.vladsch.flexmark.html.IndependentAttributeProviderFactory;
8
import com.vladsch.flexmark.html.renderer.AttributablePart;
9
import com.vladsch.flexmark.html.renderer.LinkResolverContext;
10
import com.vladsch.flexmark.util.ast.Node;
11
import com.vladsch.flexmark.util.data.MutableDataHolder;
12
import com.vladsch.flexmark.util.html.AttributeImpl;
13
import com.vladsch.flexmark.util.html.MutableAttributes;
14
import org.jetbrains.annotations.NotNull;
15
16
import static com.keenwrite.Constants.CARET_ID;
17
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
18
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
19
20
/**
21
 * Responsible for giving most block-level elements a unique identifier
22
 * attribute. The identifier is used to coordinate scrolling.
23
 */
24
public class 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
  }
46
47
  /**
48
   * Responsible for creating the id attribute. This class is instantiated
49
   * once: for the HTML element containing the {@link Constants#CARET_ID}.
50
   */
51
  public static class IdAttributeProvider implements AttributeProvider {
52
    private final Caret mCaret;
53
54
    public IdAttributeProvider( final Caret caret ) {
55
      mCaret = caret;
56
    }
57
58
    private static AttributeProviderFactory createFactory(
59
      final Caret caret ) {
60
      return new IndependentAttributeProviderFactory() {
61
        @Override
62
        public @NotNull AttributeProvider apply(
63
          @NotNull final LinkResolverContext context ) {
64
          return new IdAttributeProvider( caret );
65
        }
66
      };
67
    }
68
69
    @Override
70
    public void setAttributes( @NotNull Node curr,
71
                               @NotNull AttributablePart part,
72
                               @NotNull MutableAttributes attributes ) {
73
      final var outside = mCaret.isAfterText() ? 1 : 0;
74
      final var began = curr.getStartOffset();
75
      final var ended = curr.getEndOffset() + outside;
76
      final var prev = curr.getPrevious();
77
78
      // If the caret is within the bounds of the current node or the
79
      // caret is within the bounds of the end of the previous node and
80
      // the start of the current node, then mark the current node with
81
      // a caret indicator.
82
      if( mCaret.isBetweenText( began, ended ) ||
83
        prev != null && mCaret.isBetweenText( prev.getEndOffset(), began ) ) {
84
        // This line empowers synchronizing the text editor with the preview.
85
        attributes.addValue( AttributeImpl.of( "id", CARET_ID ) );
86
      }
87
    }
88
  }
89
}
190
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
A src/main/java/com/keenwrite/processors/markdown/ImageLinkExtension.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown;
3
4
import com.keenwrite.exceptions.MissingFileException;
5
import com.keenwrite.preferences.Workspace;
6
import com.vladsch.flexmark.ast.Image;
7
import com.vladsch.flexmark.html.IndependentLinkResolverFactory;
8
import com.vladsch.flexmark.html.LinkResolver;
9
import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
10
import com.vladsch.flexmark.html.renderer.ResolvedLink;
11
import com.vladsch.flexmark.util.ast.Node;
12
import com.vladsch.flexmark.util.data.MutableDataHolder;
13
import org.jetbrains.annotations.NotNull;
14
import org.renjin.repackaged.guava.base.Splitter;
15
16
import java.io.File;
17
import java.nio.file.Path;
18
import java.nio.file.Paths;
19
20
import static com.keenwrite.StatusBarNotifier.clue;
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;
24
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
25
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
26
import static com.vladsch.flexmark.html.renderer.LinkStatus.VALID;
27
import static java.lang.String.format;
28
29
/**
30
 * Responsible for ensuring that images can be rendered relative to a path.
31
 * This allows images to be located virtually anywhere.
32
 */
33
public class ImageLinkExtension implements HtmlRendererExtension {
34
35
  /**
36
   * Creates an extension capable of using a relative path to embed images.
37
   *
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}.
43
   */
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() );
67
  }
68
69
  private class Factory extends IndependentLinkResolverFactory {
70
    @Override
71
    public @NotNull LinkResolver apply(
72
      @NotNull final LinkResolverBasicContext context ) {
73
      return new ImageLinkResolver();
74
    }
75
  }
76
77
  private class ImageLinkResolver implements LinkResolver {
78
    public ImageLinkResolver() {
79
    }
80
81
    @NotNull
82
    @Override
83
    public ResolvedLink resolveLink(
84
      @NotNull final Node node,
85
      @NotNull final LinkResolverBasicContext context,
86
      @NotNull final ResolvedLink link ) {
87
      return node instanceof Image ? resolve( link ) : link;
88
    }
89
90
    private ResolvedLink resolve( final ResolvedLink link ) {
91
      var uri = link.getUrl();
92
      final var protocol = getProtocol( uri );
93
94
      if( protocol.isHttp() ) {
95
        return valid( link, uri );
96
      }
97
98
      // Determine the fully-qualified file name (fqfn).
99
      final var fqfn = Paths.get( getBasePath().toString(), uri ).toFile();
100
101
      if( fqfn.isFile() ) {
102
        return valid( link, uri );
103
      }
104
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 );
110
111
        final var imagePathPrefix = Path.of( basePath.toString(), uri );
112
        final var suffixes = getImageExtensions();
113
        boolean missing = true;
114
115
        // Iterate over the user's preferred image file type extensions.
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 );
119
120
          if( file.exists() ) {
121
            uri += '.' + ext;
122
            final var path = Path.of( imagePrefix.toString(), uri );
123
            uri = path.normalize().toString();
124
            missing = false;
125
            break;
126
          }
127
        }
128
129
        if( missing ) {
130
          throw new MissingFileException( imagePathPrefix + ".*" );
131
        }
132
133
        return valid( link, uri );
134
      } catch( final Exception ex ) {
135
        clue( ex );
136
      }
137
138
      return link;
139
    }
140
141
    private ResolvedLink valid( final ResolvedLink link, final String url ) {
142
      return link.withStatus( VALID ).withUrl( url );
143
    }
144
145
    private Path getImagePrefix() {
146
      return mWorkspace.toFile( KEY_IMAGES_DIR ).toPath();
147
    }
148
149
    private String getImageExtensions() {
150
      return mWorkspace.toString( KEY_IMAGES_ORDER );
151
    }
152
153
    private Path getBasePath() {
154
      return mBasePath;
155
    }
156
  }
157
}
1158
A src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.io.MediaType;
6
import com.keenwrite.preferences.Workspace;
7
import com.keenwrite.processors.*;
8
import com.keenwrite.processors.markdown.r.RExtension;
9
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
10
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
11
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
12
import com.vladsch.flexmark.ext.tables.TablesExtension;
13
import com.vladsch.flexmark.ext.typographic.TypographicExtension;
14
import com.vladsch.flexmark.html.HtmlRenderer;
15
import com.vladsch.flexmark.parser.Parser;
16
import com.vladsch.flexmark.util.ast.IParse;
17
import com.vladsch.flexmark.util.ast.IRender;
18
import com.vladsch.flexmark.util.ast.Node;
19
import com.vladsch.flexmark.util.misc.Extension;
20
21
import java.nio.file.Path;
22
import java.util.Collection;
23
import java.util.HashSet;
24
25
import static com.keenwrite.Constants.DEFAULT_DIRECTORY;
26
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;
29
30
/**
31
 * Responsible for parsing a Markdown document and rendering it as HTML.
32
 */
33
public class MarkdownProcessor extends ExecutorProcessor<String> {
34
35
  private final IParse mParser;
36
  private final IRender mRenderer;
37
38
  private MarkdownProcessor(
39
    final Processor<String> successor,
40
    final Collection<Extension> extensions ) {
41
    super( successor );
42
43
    mParser = Parser.builder().extensions( extensions ).build();
44
    mRenderer = HtmlRenderer.builder().extensions( extensions ).build();
45
  }
46
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 );
53
  }
54
55
  public static MarkdownProcessor create(
56
    final Processor<String> successor,
57
    final Workspace workspace,
58
    final Path dir ) {
59
    final var extensions = createExtensions( NONE, workspace, dir );
60
    return new MarkdownProcessor( successor, extensions );
61
  }
62
63
  public static MarkdownProcessor create(
64
    final Processor<String> successor, final ProcessorContext context ) {
65
    final var extensions = createExtensions( context );
66
    return new MarkdownProcessor( successor, extensions );
67
  }
68
69
  /**
70
   * Creating extensions based using an instance of {@link ProcessorContext}
71
   * indicates that the {@link CaretExtension} should be used to inject the
72
   * caret position into the final HTML document. This enables the HTML
73
   * preview pane to scroll to the same position, relatively speaking, within
74
   * the main document. Scrolling is developed this way to decouple the
75
   * document being edited from the preview pane so that multiple document
76
   * formats can be edited.
77
   *
78
   * @param context Contains necessary information needed to create extensions
79
   *                used by the Markdown parser.
80
   * @return {@link Collection} of extensions invoked when parsing Markdown.
81
   */
82
  private static Collection<Extension> createExtensions(
83
    final ProcessorContext context ) {
84
    final var path  = context.getPath();
85
    final var dir = context.getBasePath();
86
    final var format = context.getExportFormat();
87
    final var workspace = context.getWorkspace();
88
    final var extensions = createExtensions( format, workspace, dir );
89
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() ) );
97
98
    return extensions;
99
  }
100
101
  /**
102
   * Creates parser extensions that tweak the parsing engine based on various
103
   * conditions. For example, this will add a new {@link TeXExtension} that
104
   * can export TeX as either SVG or TeX macros. The tweak also includes the
105
   * ability to keep inline R statements, rather than convert them to inline
106
   * code elements, so that the {@link InlineRProcessor} can interpret the
107
   * R statements.
108
   *
109
   * @param dir    Directory for referencing image files via relative paths
110
   *               and dynamic file types.
111
   * @param format TeX export format to use when generating HTMl documents.
112
   * @return {@link Collection} of extensions invoked when parsing Markdown.
113
   */
114
  private static Collection<Extension> createExtensions(
115
    final ExportFormat format, final Workspace workspace, final Path dir ) {
116
    final var extensions = createDefaultExtensions();
117
118
    extensions.add( ImageLinkExtension.create( dir, workspace ) );
119
    extensions.add( TeXExtension.create( format ) );
120
121
    return extensions;
122
  }
123
124
  /**
125
   * Instantiates a number of extensions to be applied when parsing. These
126
   * are typically typographic extensions that convert characters into
127
   * HTML entities.
128
   *
129
   * @return A {@link Collection} of {@link Extension} instances that
130
   * change the {@link Parser}'s behaviour.
131
   */
132
  private static Collection<Extension> createDefaultExtensions() {
133
    final var extensions = new HashSet<Extension>();
134
    extensions.add( DefinitionExtension.create() );
135
    extensions.add( StrikethroughSubscriptExtension.create() );
136
    extensions.add( SuperscriptExtension.create() );
137
    extensions.add( TablesExtension.create() );
138
    extensions.add( TypographicExtension.create() );
139
    return extensions;
140
  }
141
142
  /**
143
   * Converts the given Markdown string into HTML, without the doctype, html,
144
   * head, and body tags.
145
   *
146
   * @param markdown The string to convert from Markdown to HTML.
147
   * @return The HTML representation of the Markdown document.
148
   */
149
  @Override
150
  public String apply( final String markdown ) {
151
    return toHtml( markdown );
152
  }
153
154
  /**
155
   * Returns the AST in the form of a node for the given markdown document. This
156
   * can be used, for example, to determine if a hyperlink exists inside of a
157
   * paragraph.
158
   *
159
   * @param markdown The markdown to convert into an AST.
160
   * @return The markdown AST for the given text (usually a paragraph).
161
   */
162
  public Node toNode( final String markdown ) {
163
    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 );
174
  }
175
176
  /**
177
   * Helper method to create an AST given some markdown.
178
   *
179
   * @param markdown The markdown to parse.
180
   * @return The root node of the markdown tree.
181
   */
182
  private Node parse( final String markdown ) {
183
    return getParser().parse( markdown );
184
  }
185
186
  /**
187
   * Converts a string of markdown into HTML.
188
   *
189
   * @param markdown The markdown text to convert to HTML, must not be null.
190
   * @return The markdown rendered as an HTML document.
191
   */
192
  private String toHtml( final String markdown ) {
193
    return toHtml( parse( markdown ) );
194
  }
195
196
  /**
197
   * Creates the Markdown document processor.
198
   *
199
   * @return An instance of {@link IParse} for building abstract syntax trees.
200
   */
201
  private IParse getParser() {
202
    return mParser;
203
  }
204
205
  private IRender getRenderer() {
206
    return mRenderer;
207
  }
208
}
1209
A src/main/java/com/keenwrite/processors/markdown/TeXExtension.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.processors.markdown.tex.TeXInlineDelimiterProcessor;
6
import com.keenwrite.processors.markdown.tex.TexNodeRenderer.Factory;
7
import com.vladsch.flexmark.html.HtmlRenderer;
8
import com.vladsch.flexmark.parser.Parser;
9
import com.vladsch.flexmark.util.data.MutableDataHolder;
10
import com.vladsch.flexmark.util.misc.Extension;
11
import org.jetbrains.annotations.NotNull;
12
13
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
14
import static com.vladsch.flexmark.parser.Parser.ParserExtension;
15
16
/**
17
 * Responsible for wrapping delimited TeX code in Markdown into an XML element
18
 * that the HTML renderer can handle. For example, {@code $E=mc^2$} becomes
19
 * {@code <tex>E=mc^2</tex>} when passed to HTML renderer. The HTML renderer
20
 * is responsible for converting the TeX code for display. This avoids inserting
21
 * SVG code into the Markdown document, which the parser would then have to
22
 * iterate---a <em>very</em> wasteful operation that impacts front-end
23
 * performance.
24
 */
25
public class TeXExtension implements ParserExtension, HtmlRendererExtension {
26
  /**
27
   * Controls how the node renderer produces TeX code within HTML output.
28
   */
29
  private final ExportFormat mExportFormat;
30
31
  /**
32
   * Creates an extension capable of handling delimited TeX code in Markdown.
33
   *
34
   * @return The new {@link TeXExtension}, never {@code null}.
35
   */
36
  public static TeXExtension create( final ExportFormat format ) {
37
    return new TeXExtension( format );
38
  }
39
40
  /**
41
   * Force using the {@link #create(ExportFormat)} method for consistency with
42
   * the other {@link Extension} creation invocations.
43
   */
44
  private TeXExtension( final ExportFormat exportFormat ) {
45
    mExportFormat = exportFormat;
46
  }
47
48
  /**
49
   * Adds the TeX extension for HTML document export types.
50
   *
51
   * @param builder      The document builder.
52
   * @param rendererType Indicates the document type to be built.
53
   */
54
  @Override
55
  public void extend( @NotNull final HtmlRenderer.Builder builder,
56
                      @NotNull final String rendererType ) {
57
    if( "HTML".equalsIgnoreCase( rendererType ) ) {
58
      builder.nodeRendererFactory( new Factory( mExportFormat ) );
59
    }
60
  }
61
62
  @Override
63
  public void extend( final Parser.Builder builder ) {
64
    builder.customDelimiterProcessor( new TeXInlineDelimiterProcessor() );
65
  }
66
67
  @Override
68
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
69
  }
70
71
  @Override
72
  public void parserOptions( final MutableDataHolder options ) {
73
  }
74
}
175
A src/main/java/com/keenwrite/processors/markdown/r/RExtension.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.r;
3
4
import com.keenwrite.processors.InlineRProcessor;
5
import com.keenwrite.sigils.RSigilOperator;
6
import com.vladsch.flexmark.ast.Text;
7
import com.vladsch.flexmark.parser.InlineParserExtensionFactory;
8
import com.vladsch.flexmark.parser.InlineParserFactory;
9
import com.vladsch.flexmark.parser.Parser;
10
import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
11
import com.vladsch.flexmark.parser.internal.InlineParserImpl;
12
import com.vladsch.flexmark.parser.internal.LinkRefProcessorData;
13
import com.vladsch.flexmark.util.data.DataHolder;
14
import com.vladsch.flexmark.util.data.MutableDataHolder;
15
16
import java.util.BitSet;
17
import java.util.List;
18
import java.util.Map;
19
20
/**
21
 * Responsible for preventing the Markdown engine from interpreting inline
22
 * backticks as inline code elements. This is required so that inline R code
23
 * can be executed after conversion of Markdown to HTML but before the HTML
24
 * is previewed (or exported).
25
 */
26
public final class RExtension implements Parser.ParserExtension {
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
  }
48
49
  /**
50
   * Prevents rendering {@code `r} statements as inline HTML {@code <code>}
51
   * blocks, which allows the {@link InlineRProcessor} to post-process the
52
   * text prior to display in the preview pane. This intervention assists
53
   * with decoupling the caret from the Markdown content so that the two
54
   * can vary independently in the architecture while permitting synchronization
55
   * of the editor and preview pane.
56
   * <p>
57
   * The text is therefore processed twice: once by flexmark-java and once by
58
   * {@link InlineRProcessor}.
59
   * </p>
60
   */
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 ) {
69
      super( options,
70
             specialCharacters,
71
             delimiterCharacters,
72
             delimiterProcessors,
73
             referenceLinkProcessors,
74
             inlineParserExtensions );
75
    }
76
77
    /**
78
     * The superclass handles a number backtick parsing edge cases; this method
79
     * changes the behaviour to retain R code snippets, identified by
80
     * {@link RSigilOperator#PREFIX}, so that subsequent processing can
81
     * invoke R. If other languages are added, the {@link CustomParser} will
82
     * have to be rewritten to identify more than merely R.
83
     *
84
     * @return The return value from {@link super#parseBackticks()}.
85
     * @inheritDoc
86
     */
87
    @Override
88
    protected final boolean parseBackticks() {
89
      final var foundTicks = super.parseBackticks();
90
91
      if( foundTicks ) {
92
        final var blockNode = getBlock();
93
        final var codeNode = blockNode.getLastChild();
94
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
          }
102
        }
103
      }
104
105
      return foundTicks;
106
    }
107
  }
108
}
1109
A src/main/java/com/keenwrite/processors/markdown/tex/TeXInlineDelimiterProcessor.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.tex;
3
4
import com.vladsch.flexmark.parser.InlineParser;
5
import com.vladsch.flexmark.parser.core.delimiter.Delimiter;
6
import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
7
import com.vladsch.flexmark.parser.delimiter.DelimiterRun;
8
import com.vladsch.flexmark.util.ast.Node;
9
10
public class TeXInlineDelimiterProcessor implements DelimiterProcessor {
11
12
  @Override
13
  public void process( final Delimiter opener,
14
                       final Delimiter closer,
15
                       final int delimitersUsed ) {
16
    final var node = new TexNode();
17
    opener.moveNodesBetweenDelimitersTo( node, closer );
18
  }
19
20
  @Override
21
  public char getOpeningCharacter() {
22
    return '$';
23
  }
24
25
  @Override
26
  public char getClosingCharacter() {
27
    return '$';
28
  }
29
30
  @Override
31
  public int getMinLength() {
32
    return 1;
33
  }
34
35
  /**
36
   * Allow for $ or $$.
37
   *
38
   * @param opener One or more opening delimiter characters.
39
   * @param closer One or more closing delimiter characters.
40
   * @return The number of delimiters to use to determine whether a valid
41
   * opening delimiter expression is found.
42
   */
43
  @Override
44
  public int getDelimiterUse(
45
      final DelimiterRun opener, final DelimiterRun closer ) {
46
    return 1;
47
  }
48
49
  @Override
50
  public boolean canBeOpener( final String before,
51
                              final String after,
52
                              final boolean leftFlanking,
53
                              final boolean rightFlanking,
54
                              final boolean beforeIsPunctuation,
55
                              final boolean afterIsPunctuation,
56
                              final boolean beforeIsWhitespace,
57
                              final boolean afterIsWhiteSpace ) {
58
    return leftFlanking;
59
  }
60
61
  @Override
62
  public boolean canBeCloser( final String before,
63
                              final String after,
64
                              final boolean leftFlanking,
65
                              final boolean rightFlanking,
66
                              final boolean beforeIsPunctuation,
67
                              final boolean afterIsPunctuation,
68
                              final boolean beforeIsWhitespace,
69
                              final boolean afterIsWhiteSpace ) {
70
    return rightFlanking;
71
  }
72
73
  @Override
74
  public Node unmatchedDelimiterNode(
75
      final InlineParser inlineParser, final DelimiterRun delimiter ) {
76
    return null;
77
  }
78
79
  @Override
80
  public boolean skipNonOpenerCloser() {
81
    return false;
82
  }
83
}
184
A src/main/java/com/keenwrite/processors/markdown/tex/TexNode.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.tex;
3
4
import com.vladsch.flexmark.ast.DelimitedNodeImpl;
5
6
public class TexNode extends DelimitedNodeImpl {
7
  /**
8
   * TeX expression wrapped in a {@code <tex>} element.
9
   */
10
  public static final String HTML_TEX = "tex";
11
12
  public static final String TOKEN_OPEN = "$";
13
  public static final String TOKEN_CLOSE = "$";
14
15
  public TexNode() {
16
  }
17
}
118
A src/main/java/com/keenwrite/processors/markdown/tex/TexNodeRenderer.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.tex;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.preview.SvgRasterizer;
6
import com.vladsch.flexmark.html.HtmlWriter;
7
import com.vladsch.flexmark.html.renderer.NodeRenderer;
8
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
9
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
10
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
11
import com.vladsch.flexmark.util.ast.Node;
12
import com.vladsch.flexmark.util.data.DataHolder;
13
import org.jetbrains.annotations.NotNull;
14
import org.jetbrains.annotations.Nullable;
15
16
import java.util.Set;
17
18
import static com.keenwrite.preview.MathRenderer.MATH_RENDERER;
19
import static com.keenwrite.processors.markdown.tex.TexNode.*;
20
21
public class TexNodeRenderer {
22
23
  public static class Factory implements NodeRendererFactory {
24
    private final ExportFormat mExportFormat;
25
26
    public Factory( final ExportFormat exportFormat ) {
27
      mExportFormat = exportFormat;
28
    }
29
30
    @NotNull
31
    @Override
32
    public NodeRenderer apply( @NotNull DataHolder options ) {
33
      return switch( mExportFormat ) {
34
        case HTML_TEX_SVG -> new TexSvgNodeRenderer();
35
        case HTML_TEX_DELIMITED, MARKDOWN_PLAIN -> new TexDelimNodeRenderer();
36
        case NONE -> new TexElementNodeRenderer();
37
      };
38
    }
39
  }
40
41
  private static abstract class AbstractTexNodeRenderer
42
      implements NodeRenderer {
43
44
    @Override
45
    public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
46
      final var h = new NodeRenderingHandler<>( TexNode.class, this::render );
47
      return Set.of( h );
48
    }
49
50
    /**
51
     * Subclasses implement this method to render the content of {@link TexNode}
52
     * instances as per their associated {@link ExportFormat}.
53
     *
54
     * @param node    {@link Node} containing text content of a math formula.
55
     * @param context Configuration information (unused).
56
     * @param html    Where to write the rendered output.
57
     */
58
    abstract void render( final TexNode node,
59
                          final NodeRendererContext context,
60
                          final HtmlWriter html );
61
  }
62
63
  /**
64
   * Responsible for rendering a TeX node as an HTML {@code <tex>}
65
   * element. This is the default behaviour.
66
   */
67
  private static class TexElementNodeRenderer extends AbstractTexNodeRenderer {
68
    void render( final TexNode node,
69
                 final NodeRendererContext context,
70
                 final HtmlWriter html ) {
71
      html.tag( HTML_TEX );
72
      html.raw( node.getText() );
73
      html.closeTag( HTML_TEX );
74
    }
75
  }
76
77
  /**
78
   * Responsible for rendering a TeX node as an HTML {@code <svg>}
79
   * element.
80
   */
81
  private static class TexSvgNodeRenderer extends AbstractTexNodeRenderer {
82
    void render( final TexNode node,
83
                 final NodeRendererContext context,
84
                 final HtmlWriter html ) {
85
      final var tex = node.getText().toStringOrNull();
86
      final var doc = MATH_RENDERER.render( tex == null ? "" : tex );
87
      final var svg = SvgRasterizer.toSvg( doc.getDocumentElement() );
88
      html.raw( svg );
89
    }
90
  }
91
92
  /**
93
   * Responsible for rendering a TeX node as text bracketed by $ tokens.
94
   */
95
  private static class TexDelimNodeRenderer extends AbstractTexNodeRenderer {
96
    void render( final TexNode node,
97
                 final NodeRendererContext context,
98
                 final HtmlWriter html ) {
99
      html.raw( TOKEN_OPEN );
100
      html.raw( node.getText() );
101
      html.raw( TOKEN_CLOSE );
102
    }
103
  }
104
}
1105
A src/main/java/com/keenwrite/processors/text/AbstractTextReplacer.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.text;
3
4
import java.util.Map;
5
6
/**
7
 * Responsible for common behaviour across all text replacer implementations.
8
 */
9
public abstract class AbstractTextReplacer implements TextReplacer {
10
11
  /**
12
   * Default (empty) constructor.
13
   */
14
  protected AbstractTextReplacer() {
15
  }
16
17
  protected String[] keys( final Map<String, String> map ) {
18
    return map.keySet().toArray( new String[ 0 ] );
19
  }
20
21
  protected String[] values( final Map<String, String> map ) {
22
    return map.values().toArray( new String[ 0 ] );
23
  }
24
}
125
A src/main/java/com/keenwrite/processors/text/AhoCorasickReplacer.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.text;
3
4
import java.util.Map;
5
6
import static org.ahocorasick.trie.Trie.builder;
7
8
/**
9
 * Replaces text using an Aho-Corasick algorithm.
10
 */
11
public class AhoCorasickReplacer extends AbstractTextReplacer {
12
13
  /**
14
   * Default (empty) constructor.
15
   */
16
  protected AhoCorasickReplacer() {
17
  }
18
19
  @Override
20
  public String replace( final String text, final Map<String, String> map ) {
21
    // Create a buffer sufficiently large that re-allocations are minimized.
22
    final var sb = new StringBuilder( (int)(text.length() * 1.25) );
23
24
    // Definition names cannot overlap.
25
    final var builder = builder().ignoreOverlaps();
26
    builder.addKeywords( keys( map ) );
27
28
    int index = 0;
29
30
    // Replace all instances with dereferenced variables.
31
    for( final var emit : builder.build().parseText( text ) ) {
32
      sb.append( text, index, emit.getStart() );
33
      sb.append( map.get( emit.getKeyword() ) );
34
      index = emit.getEnd() + 1;
35
    }
36
37
    // Add the remainder of the string (contains no more matches).
38
    sb.append( text.substring( index ) );
39
40
    return sb.toString();
41
  }
42
}
143
A src/main/java/com/keenwrite/processors/text/StringUtilsReplacer.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.text;
3
4
import java.util.Map;
5
6
import static org.apache.commons.lang3.StringUtils.replaceEach;
7
8
/**
9
 * Replaces text using Apache's StringUtils.replaceEach method.
10
 */
11
public class StringUtilsReplacer extends AbstractTextReplacer {
12
13
  /**
14
   * Default (empty) constructor.
15
   */
16
  protected StringUtilsReplacer() {
17
  }
18
19
  @Override
20
  public String replace( final String text, final Map<String, String> map ) {
21
    return replaceEach( text, keys( map ), values( map ) );
22
  }
23
}
124
A src/main/java/com/keenwrite/processors/text/TextReplacementFactory.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.text;
3
4
import java.util.Map;
5
6
/**
7
 * Used to generate a class capable of efficiently replacing variable
8
 * definitions with their values.
9
 */
10
public final class TextReplacementFactory {
11
12
  private static final TextReplacer APACHE = new StringUtilsReplacer();
13
  private static final TextReplacer AHO_CORASICK = new AhoCorasickReplacer();
14
15
  /**
16
   * Returns a text search/replacement instance that is reasonably optimal for
17
   * the given length of text.
18
   *
19
   * @param length The length of text that requires some search and replacing.
20
   * @return A class that can search and replace text with utmost expediency.
21
   */
22
  public static TextReplacer getTextReplacer( final int length ) {
23
    // After about 1,500 characters, the StringUtils implementation is slower
24
    // than the Aho-Corsick algorithm implementation.
25
    return length < 1500 ? APACHE : AHO_CORASICK;
26
  }
27
28
  /**
29
   * Convenience method to instantiate a suitable text replacer algorithm and
30
   * perform a replacement using the given map. At this point, the values should
31
   * be already dereferenced and ready to be substituted verbatim; any
32
   * recursively defined values must have been interpolated previously.
33
   *
34
   * @param text The text containing zero or more variables to replace.
35
   * @param map  The map of variables to their dereferenced values.
36
   * @return The text with all variables replaced.
37
   */
38
  public static String replace(
39
      final String text, final Map<String, String> map ) {
40
    return getTextReplacer( text.length() ).replace( text, map );
41
  }
42
}
143
A src/main/java/com/keenwrite/processors/text/TextReplacer.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.text;
3
4
import java.util.Map;
5
6
/**
7
 * Defines the ability to replace text given a set of keys and values.
8
 */
9
public interface TextReplacer {
10
11
  /**
12
   * Searches through the given text for any of the keys given in the map and
13
   * replaces the keys that appear in the text with the key's corresponding
14
   * value.
15
   *
16
   * @param text The text that contains zero or more keys.
17
   * @param map  The set of keys mapped to replacement values.
18
   * @return The given text with all keys replaced with corresponding values.
19
   */
20
  String replace( String text, Map<String, String> map );
21
}
122
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
A src/main/java/com/keenwrite/service/Service.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.service;
3
4
/**
5
 * All services inherit from this one.
6
 */
7
public interface Service {
8
}
19
A src/main/java/com/keenwrite/service/Settings.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.service;
3
4
import java.util.Iterator;
5
import java.util.List;
6
7
/**
8
 * Defines how settings and options can be retrieved.
9
 */
10
public interface Settings extends Service {
11
12
  /**
13
   * Returns a setting property or its default value.
14
   *
15
   * @param property     The property key name to obtain its value.
16
   * @param defaultValue The default value to return iff the property cannot be
17
   *                     found.
18
   * @return The property value for the given property key.
19
   */
20
  String getSetting( String property, String defaultValue );
21
22
  /**
23
   * Returns a setting property or its default value.
24
   *
25
   * @param property     The property key name to obtain its value.
26
   * @param defaultValue The default value to return iff the property cannot be
27
   *                     found.
28
   * @return The property value for the given property key.
29
   */
30
  int getSetting( String property, int defaultValue );
31
32
  /**
33
   * Returns a list of property names that begin with the given prefix. The
34
   * prefix is included in any matching results. This will return keys that
35
   * either match the prefix or start with the prefix followed by a dot ('.').
36
   * For example, a prefix value of <code>the.property.name</code> will likely
37
   * return the expected results, but <code>the.property.name.</code> (note the
38
   * extraneous period) will probably not.
39
   *
40
   * @param prefix The prefix to compare against each property name.
41
   * @return The list of property names that have the given prefix.
42
   */
43
  Iterator<String> getKeys( final String prefix );
44
45
  /**
46
   * Convert the generic list of property objects into strings.
47
   *
48
   * @param property The property value to coerce.
49
   * @param defaults The defaults values to use should the property be unset.
50
   * @return The list of properties coerced from objects to strings.
51
   */
52
  List<String> getStringSettingList( String property, List<String> defaults );
53
54
  /**
55
   * Converts the generic list of property objects into strings.
56
   *
57
   * @param property The property value to coerce.
58
   * @return The list of properties coerced from objects to strings.
59
   */
60
  List<String> getStringSettingList( String property );
61
}
162
A src/main/java/com/keenwrite/service/Snitch.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.service;
3
4
import java.io.IOException;
5
import java.nio.file.Path;
6
import java.util.Observer;
7
8
/**
9
 * Listens for changes to file system files and directories.
10
 */
11
public interface Snitch extends Service, Runnable {
12
13
  /**
14
   * Adds an observer to the set of observers for this object, provided that it
15
   * is not the same as some observer already in the set. The order in which
16
   * notifications will be delivered to multiple observers is not specified.
17
   *
18
   * @param o The object to receive changed events for when monitored files
19
   *          are changed.
20
   */
21
  void addObserver( Observer o );
22
23
  /**
24
   * Listens for changes to the path. If the path specifies a file, then only
25
   * notifications pertaining to that file are sent. Otherwise, change events
26
   * for the directory that contains the file are sent. This method must allow
27
   * for multiple calls to the same file without incurring additional listeners
28
   * or events.
29
   *
30
   * @param file Send notifications when this file changes, can be null.
31
   * @throws IOException Couldn't create a watcher for the given file.
32
   */
33
  void listen( Path file ) throws IOException;
34
35
  /**
36
   * Removes the given file from the notifications list.
37
   *
38
   * @param file The file to stop monitoring for any changes, can be null.
39
   */
40
  void ignore( Path file );
41
42
  /**
43
   * Start listening for events on a new thread.
44
   */
45
  void start();
46
47
  /**
48
   * Stop listening for events.
49
   */
50
  void stop();
51
}
152
A src/main/java/com/keenwrite/service/events/Notification.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.service.events;
3
4
/**
5
 * Represents a message that contains a title and content.
6
 */
7
public interface Notification {
8
9
  /**
10
   * Alert title.
11
   *
12
   * @return A non-null string to use as alert message title.
13
   */
14
  String getTitle();
15
16
  /**
17
   * Alert message content.
18
   *
19
   * @return A non-null string that contains information for the user.
20
   */
21
  String getContent();
22
}
123
A src/main/java/com/keenwrite/service/events/Notifier.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.service.events;
3
4
import javafx.scene.control.Alert;
5
import javafx.scene.control.ButtonType;
6
import javafx.stage.Window;
7
8
import java.nio.file.Path;
9
10
/**
11
 * Provides the application with a uniform way to notify the user of events.
12
 */
13
public interface Notifier {
14
15
  ButtonType YES = ButtonType.YES;
16
  ButtonType NO = ButtonType.NO;
17
  ButtonType CANCEL = ButtonType.CANCEL;
18
19
  /**
20
   * Constructs an alert message text for a modal alert dialog.
21
   *
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).
56
   * @param args    The arguments to the message content that must be formatted.
57
   * @return The message suitable for building a modal alert dialog.
58
   */
59
  Notification createNotification(
60
      String title,
61
      String message,
62
      Object... args );
63
64
  /**
65
   * Creates an alert of alert type error with a message showing the cause of
66
   * the error.
67
   *
68
   * @param parent  Dialog box owner (for modal purposes).
69
   * @param message The error message, title, and possibly more details.
70
   * @return A modal alert dialog box ready to display using showAndWait.
71
   */
72
  Alert createError( Window parent, Notification message );
73
74
  /**
75
   * Creates an alert of alert type confirmation with Yes/No/Cancel buttons.
76
   *
77
   * @param parent  Dialog box owner (for modal purposes).
78
   * @param message The message, title, and possibly more details.
79
   * @return A modal alert dialog box ready to display using showAndWait.
80
   */
81
  Alert createConfirmation( Window parent, Notification message );
82
}
183
A src/main/java/com/keenwrite/service/events/impl/ButtonOrderPane.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.service.events.impl;
3
4
import javafx.scene.Node;
5
import javafx.scene.control.ButtonBar;
6
import javafx.scene.control.DialogPane;
7
8
import static com.keenwrite.Constants.sSettings;
9
import static javafx.scene.control.ButtonBar.BUTTON_ORDER_WINDOWS;
10
11
/**
12
 * Ensures a consistent button order for alert dialogs across platforms (because
13
 * the default button order on Linux defies all logic).
14
 */
15
public class ButtonOrderPane extends DialogPane {
16
17
  @Override
18
  protected Node createButtonBar() {
19
    final var node = (ButtonBar) super.createButtonBar();
20
    node.setButtonOrder( getButtonOrder() );
21
    return node;
22
  }
23
24
  private String getButtonOrder() {
25
    return getSetting( "dialog.alert.button.order.windows",
26
                       BUTTON_ORDER_WINDOWS );
27
  }
28
29
  @SuppressWarnings("SameParameterValue")
30
  private String getSetting( final String key, final String defaultValue ) {
31
    return sSettings.getSetting( key, defaultValue );
32
  }
33
}
134
A src/main/java/com/keenwrite/service/events/impl/DefaultNotification.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.service.events.impl;
3
4
import com.keenwrite.service.events.Notification;
5
6
import java.text.MessageFormat;
7
8
/**
9
 * Responsible for alerting the user to prominent information.
10
 */
11
public class DefaultNotification implements Notification {
12
13
  private final String title;
14
  private final String content;
15
16
  /**
17
   * Constructs default message text for a notification.
18
   *
19
   * @param title   The message title.
20
   * @param message The message content (needs formatting).
21
   * @param args    The arguments to the message content that must be formatted.
22
   */
23
  public DefaultNotification(
24
      final String title,
25
      final String message,
26
      final Object... args ) {
27
    this.title = title;
28
    this.content = MessageFormat.format( message, args );
29
  }
30
31
  @Override
32
  public String getTitle() {
33
    return this.title;
34
  }
35
36
  @Override
37
  public String getContent() {
38
    return this.content;
39
  }
40
41
}
142
A src/main/java/com/keenwrite/service/events/impl/DefaultNotifier.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.service.events.impl;
3
4
import com.keenwrite.service.events.Notification;
5
import com.keenwrite.service.events.Notifier;
6
import javafx.scene.control.Alert;
7
import javafx.scene.control.Alert.AlertType;
8
import javafx.stage.Window;
9
10
import java.nio.file.Path;
11
12
import static com.keenwrite.Messages.get;
13
import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
14
import static javafx.scene.control.Alert.AlertType.ERROR;
15
16
/**
17
 * Provides the ability to notify the user of events that need attention,
18
 * such as prompting the user to confirm closing when there are unsaved changes.
19
 */
20
public final class DefaultNotifier implements Notifier {
21
22
  @Override
23
  public Notification createNotification(
24
      final String title,
25
      final String message,
26
      final Object... args ) {
27
    return new DefaultNotification( title, message, args );
28
  }
29
30
  @Override
31
  public void alert(
32
      final Window 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
    );
40
41
    createError( parent, message ).showAndWait();
42
  }
43
44
  @Override
45
  public Alert createConfirmation(
46
      final Window parent, final Notification message ) {
47
    final var alert = createAlertDialog( parent, CONFIRMATION, message );
48
49
    alert.getButtonTypes().setAll( YES, NO, CANCEL );
50
51
    return alert;
52
  }
53
54
  @Override
55
  public Alert createError( final Window parent, final Notification message ) {
56
    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;
72
  }
73
}
174
A src/main/java/com/keenwrite/service/impl/DefaultSettings.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.service.impl;
3
4
import com.keenwrite.service.Settings;
5
import org.apache.commons.configuration2.PropertiesConfiguration;
6
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
7
import org.apache.commons.configuration2.convert.ListDelimiterHandler;
8
9
import java.io.InputStreamReader;
10
import java.net.URL;
11
import java.nio.charset.Charset;
12
import java.util.Iterator;
13
import java.util.List;
14
15
import static com.keenwrite.Constants.PATH_PROPERTIES_SETTINGS;
16
17
/**
18
 * Responsible for loading settings that help avoid hard-coded assumptions.
19
 */
20
public final class DefaultSettings implements Settings {
21
22
  private static final char VALUE_SEPARATOR = ',';
23
24
  private final PropertiesConfiguration mProperties = createProperties();
25
26
  public DefaultSettings() {
27
  }
28
29
  /**
30
   * Returns the value of a string property.
31
   *
32
   * @param property     The property key.
33
   * @param defaultValue The value to return if no property key has been set.
34
   * @return The property key value, or defaultValue when no key found.
35
   */
36
  @Override
37
  public String getSetting( final String property, final String defaultValue ) {
38
    return getSettings().getString( property, defaultValue );
39
  }
40
41
  /**
42
   * Returns the value of a string property.
43
   *
44
   * @param property     The property key.
45
   * @param defaultValue The value to return if no property key has been set.
46
   * @return The property key value, or defaultValue when no key found.
47
   */
48
  @Override
49
  public int getSetting( final String property, final int defaultValue ) {
50
    return getSettings().getInt( property, defaultValue );
51
  }
52
53
  /**
54
   * Convert the generic list of property objects into strings.
55
   *
56
   * @param property The property value to coerce.
57
   * @param defaults The defaults values to use should the property be unset.
58
   * @return The list of properties coerced from objects to strings.
59
   */
60
  @Override
61
  public List<String> getStringSettingList(
62
      final String property, final List<String> defaults ) {
63
    return getSettings().getList( String.class, property, defaults );
64
  }
65
66
  /**
67
   * Convert a list of property objects into strings, with no default value.
68
   *
69
   * @param property The property value to coerce.
70
   * @return The list of properties coerced from objects to strings.
71
   */
72
  @Override
73
  public List<String> getStringSettingList( final String property ) {
74
    return getStringSettingList( property, null );
75
  }
76
77
  /**
78
   * Returns a list of property names that begin with the given prefix.
79
   *
80
   * @param prefix The prefix to compare against each property name.
81
   * @return The list of property names that have the given prefix.
82
   */
83
  @Override
84
  public Iterator<String> getKeys( final String prefix ) {
85
    return getSettings().getKeys( prefix );
86
  }
87
88
  private PropertiesConfiguration createProperties() {
89
    final var url = getPropertySource();
90
    final var configuration = new PropertiesConfiguration();
91
92
    if( url != null ) {
93
      try( final var reader = new InputStreamReader(
94
          url.openStream(), getDefaultEncoding() ) ) {
95
        configuration.setListDelimiterHandler( createListDelimiterHandler() );
96
        configuration.read( reader );
97
      } catch( final Exception ex ) {
98
        throw new RuntimeException( ex );
99
      }
100
    }
101
102
    return configuration;
103
  }
104
105
  protected Charset getDefaultEncoding() {
106
    return Charset.defaultCharset();
107
  }
108
109
  protected ListDelimiterHandler createListDelimiterHandler() {
110
    return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
111
  }
112
113
  private URL getPropertySource() {
114
    return DefaultSettings.class.getResource( PATH_PROPERTIES_SETTINGS );
115
  }
116
117
  private PropertiesConfiguration getSettings() {
118
    return mProperties;
119
  }
120
}
1121
A src/main/java/com/keenwrite/service/impl/DefaultSnitch.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.service.impl;
3
4
import com.keenwrite.service.Snitch;
5
6
import java.io.IOException;
7
import java.nio.file.*;
8
import java.util.Collections;
9
import java.util.Map;
10
import java.util.Observable;
11
import java.util.Set;
12
import java.util.concurrent.ConcurrentHashMap;
13
14
import static com.keenwrite.Constants.APP_WATCHDOG_TIMEOUT;
15
import static com.keenwrite.StatusBarNotifier.clue;
16
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
17
18
/**
19
 * Listens for file changes. Other classes can register paths to be monitored
20
 * and listen for changes to those paths.
21
 */
22
public class DefaultSnitch extends Observable implements Snitch {
23
  private final Thread mSnitchThread = new Thread( this );
24
25
  /**
26
   * Service for listening to directories for modifications.
27
   */
28
  private WatchService watchService;
29
30
  /**
31
   * Directories being monitored for changes.
32
   */
33
  private Map<WatchKey, Path> keys;
34
35
  /**
36
   * Files that will kick off notification events if modified.
37
   */
38
  private Set<Path> eavesdropped;
39
40
  /**
41
   * Set to true when running; set to false to stop listening.
42
   */
43
  private volatile boolean listening;
44
45
  public DefaultSnitch() {
46
  }
47
48
  @Override
49
  public void start() {
50
    mSnitchThread.start();
51
  }
52
53
  @Override
54
  public void stop() {
55
    setListening( false );
56
57
    try {
58
      mSnitchThread.interrupt();
59
      mSnitchThread.join();
60
    } catch( final Exception ex ) {
61
      clue( ex );
62
    }
63
  }
64
65
  /**
66
   * Adds a listener to the list of files to watch for changes. If the file is
67
   * already in the monitored list, this will return immediately.
68
   *
69
   * @param file Path to a file to watch for changes.
70
   * @throws IOException The file could not be monitored.
71
   */
72
  @Override
73
  public void listen( final Path file ) throws IOException {
74
    if( file != null && getEavesdropped().add( file ) ) {
75
      final Path dir = toDirectory( file );
76
      final WatchKey key = dir.register( getWatchService(), ENTRY_MODIFY );
77
78
      getWatchMap().put( key, dir );
79
    }
80
  }
81
82
  /**
83
   * Returns the given path to a file (or directory) as a directory. If the
84
   * given path is already a directory, it is returned. Otherwise, this returns
85
   * the directory that contains the file. This will fail if the file is stored
86
   * in the root folder.
87
   *
88
   * @param path The file to return as a directory, which should always be the
89
   *             case.
90
   * @return The given path as a directory, if a file, otherwise the path
91
   * itself.
92
   */
93
  private Path toDirectory( final Path path ) {
94
    return Files.isDirectory( path )
95
        ? path
96
        : path.toFile().getParentFile().toPath();
97
  }
98
99
  /**
100
   * Stop listening to the given file for change events. This fails silently.
101
   *
102
   * @param file The file to no longer monitor for changes.
103
   */
104
  @Override
105
  public void ignore( final Path file ) {
106
    if( file != null ) {
107
      final Path directory = toDirectory( file );
108
109
      // Remove all occurrences (there should be only one).
110
      getWatchMap().values().removeAll( Collections.singleton( directory ) );
111
112
      // Remove all occurrences (there can be only one).
113
      getEavesdropped().remove( file );
114
    }
115
  }
116
117
  /**
118
   * Loops until stop is called, or the application is terminated.
119
   */
120
  @Override
121
  @SuppressWarnings("BusyWait")
122
  public void run() {
123
    setListening( true );
124
125
    while( isListening() ) {
126
      try {
127
        final WatchKey key = getWatchService().take();
128
        final Path path = get( key );
129
130
        // Prevent receiving two separate ENTRY_MODIFY events: file modified
131
        // and timestamp updated. Instead, receive one ENTRY_MODIFY event
132
        // with two counts.
133
        Thread.sleep( APP_WATCHDOG_TIMEOUT );
134
135
        for( final WatchEvent<?> event : key.pollEvents() ) {
136
          final Path changed = path.resolve( (Path) event.context() );
137
138
          if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
139
            setChanged();
140
            notifyObservers( changed );
141
          }
142
        }
143
144
        if( !key.reset() ) {
145
          ignore( path );
146
        }
147
      } catch( final Exception ex ) {
148
        // Stop eavesdropping.
149
        setListening( false );
150
      }
151
    }
152
  }
153
154
  /**
155
   * Returns true if the list of files being listened to for changes contains
156
   * the given file.
157
   *
158
   * @param file Path to a system file.
159
   * @return true The given file is being monitored for changes.
160
   */
161
  private boolean isListening( final Path file ) {
162
    return getEavesdropped().contains( file );
163
  }
164
165
  /**
166
   * Returns a path for a given watch key.
167
   *
168
   * @param key The key to lookup its corresponding path.
169
   * @return The path for the given key.
170
   */
171
  private Path get( final WatchKey key ) {
172
    return getWatchMap().get( key );
173
  }
174
175
  private synchronized Map<WatchKey, Path> getWatchMap() {
176
    if( this.keys == null ) {
177
      this.keys = createWatchKeys();
178
    }
179
180
    return this.keys;
181
  }
182
183
  protected Map<WatchKey, Path> createWatchKeys() {
184
    return new ConcurrentHashMap<>();
185
  }
186
187
  /**
188
   * Returns a list of files that, when changed, will kick off a notification.
189
   *
190
   * @return A non-null, possibly empty, list of files.
191
   */
192
  private synchronized Set<Path> getEavesdropped() {
193
    if( this.eavesdropped == null ) {
194
      this.eavesdropped = createEavesdropped();
195
    }
196
197
    return this.eavesdropped;
198
  }
199
200
  protected Set<Path> createEavesdropped() {
201
    return ConcurrentHashMap.newKeySet();
202
  }
203
204
  /**
205
   * The existing watch service, or a new instance if null.
206
   *
207
   * @return A valid WatchService instance, never null.
208
   * @throws IOException Could not create a new watch service.
209
   */
210
  private synchronized WatchService getWatchService() throws IOException {
211
    if( this.watchService == null ) {
212
      this.watchService = createWatchService();
213
    }
214
215
    return this.watchService;
216
  }
217
218
  protected WatchService createWatchService() throws IOException {
219
    final FileSystem fileSystem = FileSystems.getDefault();
220
    return fileSystem.newWatchService();
221
  }
222
223
  /**
224
   * Answers whether the loop should continue executing.
225
   *
226
   * @return true The internal listening loop should continue listening for file
227
   * modification events.
228
   */
229
  protected boolean isListening() {
230
    return this.listening;
231
  }
232
233
  /**
234
   * Requests the snitch to stop eavesdropping on file changes.
235
   *
236
   * @param listening Use true to indicate the service should stop running.
237
   */
238
  private void setListening( final boolean listening ) {
239
    this.listening = listening;
240
  }
241
}
1242
A src/main/java/com/keenwrite/sigils/RSigilOperator.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.sigils;
3
4
import static com.keenwrite.sigils.YamlSigilOperator.KEY_SEPARATOR_DEF;
5
6
/**
7
 * Brackets variable names between {@link #PREFIX} and {@link #SUFFIX} sigils.
8
 */
9
public class RSigilOperator extends SigilOperator {
10
  public static final char KEY_SEPARATOR_R = '$';
11
12
  public static final String PREFIX = "`r#";
13
  public static final char SUFFIX = '`';
14
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
  }
25
26
  /**
27
   * Returns the given string R-escaping backticks prepended and appended. This
28
   * is not null safe. Do not pass null into this method.
29
   *
30
   * @param key The string to adorn with R token delimiters.
31
   * @return PREFIX + delimiterBegan + variableName+ delimiterEnded + SUFFIX.
32
   */
33
  @Override
34
  public String apply( final String key ) {
35
    assert key != null;
36
    return PREFIX + getBegan() + entoken( key ) + getEnded() + SUFFIX;
37
  }
38
39
  /**
40
   * Transforms a definition key (bracketed by token delimiters) into the
41
   * expected format for an R variable key name.
42
   *
43
   * @param key The variable name to transform, can be empty but not null.
44
   * @return The transformed variable name.
45
   */
46
  public String entoken( final String key ) {
47
    return "v$" + mAntecedent.detoken( key )
48
                             .replace( KEY_SEPARATOR_DEF, KEY_SEPARATOR_R );
49
  }
50
}
151
A src/main/java/com/keenwrite/sigils/SigilOperator.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.sigils;
3
4
import java.util.function.UnaryOperator;
5
6
/**
7
 * Responsible for updating definition keys to use a machine-readable format
8
 * corresponding to the type of file being edited. This changes a definition
9
 * key name based on some criteria determined by the factory that creates
10
 * implementations of this interface.
11
 */
12
public abstract class SigilOperator implements UnaryOperator<String> {
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();
37
  }
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 );
47
}
148
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
A src/main/java/com/keenwrite/sigils/YamlSigilOperator.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.sigils;
3
4
/**
5
 * Brackets definition keys with token delimiters.
6
 */
7
public class YamlSigilOperator extends SigilOperator {
8
  public static final char KEY_SEPARATOR_DEF = '.';
9
10
  public YamlSigilOperator( final Tokens tokens ) {
11
    super( tokens );
12
  }
13
14
  /**
15
   * Returns the given {@link String} verbatim because variables in YAML
16
   * documents and plain Markdown documents already have the appropriate
17
   * tokenizable syntax wrapped around the text.
18
   *
19
   * @param key Returned verbatim.
20
   */
21
  @Override
22
  public String apply( final String key ) {
23
    return key;
24
  }
25
26
  /**
27
   * Adds delimiters to the given key.
28
   *
29
   * @param key The key to adorn with start and stop definition tokens.
30
   * @return The given key bracketed by definition token symbols.
31
   */
32
  public String entoken( final String key ) {
33
    assert key != null;
34
    return getBegan() + key + getEnded();
35
  }
36
37
  /**
38
   * Removes start and stop definition key delimiters from the given key. This
39
   * method does not check for delimiters, only that there are sufficient
40
   * characters to remove from either end of the given key.
41
   *
42
   * @param key The key adorned with start and stop definition tokens.
43
   * @return The given key with the delimiters removed.
44
   */
45
  public String detoken( final String key ) {
46
    final int beganLen = getBegan().length();
47
    final int endedLen = getEnded().length();
48
49
    return key.length() > beganLen + endedLen
50
      ? key.substring( beganLen, key.length() - endedLen )
51
      : key;
52
  }
53
}
154
A src/main/java/com/keenwrite/spelling/api/SpellCheckListener.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.spelling.api;
3
4
import java.util.function.BiConsumer;
5
6
/**
7
 * Represents an operation that accepts two input arguments and returns no
8
 * result. Unlike most other functional interfaces, this class is expected to
9
 * operate via side-effects.
10
 * <p>
11
 * This is used instead of a {@link BiConsumer} to avoid autoboxing.
12
 * </p>
13
 */
14
@FunctionalInterface
15
public interface SpellCheckListener {
16
17
  /**
18
   * Performs an operation on the given arguments.
19
   *
20
   * @param text        The text associated with a beginning and ending offset.
21
   * @param beganOffset A starting offset, used as an index into a string.
22
   * @param endedOffset An ending offset, which should equal text.length() +
23
   *                    beganOffset.
24
   */
25
  void accept( String text, int beganOffset, int endedOffset );
26
}
127
A src/main/java/com/keenwrite/spelling/api/SpellChecker.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.spelling.api;
3
4
import java.util.List;
5
6
/**
7
 * Defines the responsibilities for a spell checking API. The intention is
8
 * to allow different spell checking implementations to be used by the
9
 * application, such as SymSpell and LinSpell.
10
 */
11
public interface SpellChecker {
12
13
  /**
14
   * Answers whether the given lexeme, in whole, is found in the lexicon. The
15
   * lexicon lookup is performed case-insensitively. This method should be
16
   * used instead of {@link #suggestions(String, int)} for performance reasons.
17
   *
18
   * @param lexeme The word to check for correctness.
19
   * @return {@code true} if the lexeme is in the lexicon.
20
   */
21
  boolean inLexicon( String lexeme );
22
23
  /**
24
   * Gets a list of spelling corrections for the given lexeme.
25
   *
26
   * @param lexeme A word to check for correctness that's not in the lexicon.
27
   * @param count  The maximum number of alternatives to return.
28
   * @return A list of words in the lexicon that are similar to the given
29
   * lexeme.
30
   */
31
  List<String> suggestions( String lexeme, int count );
32
33
  /**
34
   * Iterates over the given text, emitting starting and ending offsets into
35
   * the text for every word that is missing from the lexicon.
36
   *
37
   * @param text     The text to check for words missing from the lexicon.
38
   * @param consumer Every missing word emits a message with the starting
39
   *                 and ending offset into the text where said word is found.
40
   */
41
  void proofread( String text, SpellCheckListener consumer );
42
}
143
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
A src/main/java/com/keenwrite/spelling/impl/PermissiveSpeller.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
7
import java.util.List;
8
9
/**
10
 * Responsible for spell checking in the event that a real spell checking
11
 * implementation cannot be created (for any reason). Does not perform any
12
 * spell checking and indicates that any given lexeme is in the lexicon.
13
 */
14
public class PermissiveSpeller implements SpellChecker {
15
  /**
16
   * Returns {@code true}, ignoring the given word.
17
   *
18
   * @param ignored Unused.
19
   * @return {@code true}
20
   */
21
  @Override
22
  public boolean inLexicon( final String ignored ) {
23
    return true;
24
  }
25
26
  /**
27
   * Returns an array with the given lexeme.
28
   *
29
   * @param lexeme  The word to return.
30
   * @param ignored Unused.
31
   * @return A suggestion list containing the given lexeme.
32
   */
33
  @Override
34
  public List<String> suggestions( final String lexeme, final int ignored ) {
35
    return List.of( lexeme );
36
  }
37
38
  /**
39
   * Performs no action.
40
   *
41
   * @param text    Unused.
42
   * @param ignored Uncalled.
43
   */
44
  @Override
45
  public void proofread(
46
      final String text, final SpellCheckListener ignored ) {
47
  }
48
}
149
A src/main/java/com/keenwrite/spelling/impl/SymSpellSpeller.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.spelling.impl;
3
4
import com.keenwrite.exceptions.MissingFileException;
5
import com.keenwrite.spelling.api.SpellCheckListener;
6
import com.keenwrite.spelling.api.SpellChecker;
7
import io.gitlab.rxp90.jsymspell.SuggestItem;
8
import io.gitlab.rxp90.jsymspell.SymSpell;
9
import io.gitlab.rxp90.jsymspell.SymSpellBuilder;
10
11
import java.io.BufferedReader;
12
import java.io.InputStreamReader;
13
import java.text.BreakIterator;
14
import java.util.ArrayList;
15
import java.util.Collection;
16
import java.util.List;
17
import java.util.stream.Collectors;
18
19
import static com.keenwrite.Constants.LEXICONS_DIRECTORY;
20
import static com.keenwrite.StatusBarNotifier.clue;
21
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity;
22
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.ALL;
23
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.CLOSEST;
24
import static java.lang.Character.isLetter;
25
import static java.nio.charset.StandardCharsets.UTF_8;
26
27
/**
28
 * Responsible for spell checking using {@link SymSpell}.
29
 */
30
public class SymSpellSpeller implements SpellChecker {
31
  private final BreakIterator mBreakIterator = BreakIterator.getWordInstance();
32
33
  private final SymSpell mSymSpell;
34
35
  /**
36
   * Creates a new spellchecker for a lexicon of words found in the specified
37
   * file.
38
   *
39
   * @param filename Lexicon language file (e.g., "en.txt").
40
   * @return An instance of {@link SpellChecker} that can check if a word
41
   * is correct and suggest alternatives, or {@link PermissiveSpeller} if the
42
   * lexicon cannot be loaded.
43
   */
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(
55
      final Collection<String> lexiconWords ) {
56
    assert lexiconWords != null && !lexiconWords.isEmpty();
57
58
    final var builder = new SymSpellBuilder()
59
        .setLexiconWords( lexiconWords );
60
61
    return new SymSpellSpeller( builder.build() );
62
  }
63
64
  /**
65
   * Prevent direct instantiation so that only the {@link SpellChecker}
66
   * interface
67
   * is available.
68
   *
69
   * @param symSpell The implementation-specific spell checker.
70
   */
71
  private SymSpellSpeller( final SymSpell symSpell ) {
72
    mSymSpell = symSpell;
73
  }
74
75
  @Override
76
  public boolean inLexicon( final String lexeme ) {
77
    return lookup( lexeme, CLOSEST ).size() == 1;
78
  }
79
80
  @Override
81
  public List<String> suggestions( final String lexeme, int count ) {
82
    final List<String> result = new ArrayList<>( count );
83
84
    for( final var item : lookup( lexeme, ALL ) ) {
85
      if( count-- > 0 ) {
86
        result.add( item.getSuggestion() );
87
      }
88
      else {
89
        break;
90
      }
91
    }
92
93
    return result;
94
  }
95
96
  @Override
97
  public void proofread(
98
      final String text, final SpellCheckListener consumer ) {
99
    assert text != null;
100
    assert consumer != null;
101
102
    mBreakIterator.setText( text );
103
104
    int boundaryIndex = mBreakIterator.first();
105
    int previousIndex = 0;
106
107
    while( boundaryIndex != BreakIterator.DONE ) {
108
      final var lex = text.substring( previousIndex, boundaryIndex )
109
                          .toLowerCase();
110
111
      // Get the lexeme for the possessive.
112
      final var pos = lex.endsWith( "'s" ) || lex.endsWith( "’s" );
113
      final var lexeme = pos ? lex.substring( 0, lex.length() - 2 ) : lex;
114
115
      if( isWord( lexeme ) && !inLexicon( lexeme ) ) {
116
        consumer.accept( lex, previousIndex, boundaryIndex );
117
      }
118
119
      previousIndex = boundaryIndex;
120
      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
      }
139
    }
140
  }
141
142
  /**
143
   * Answers whether the given string is likely a word by checking the first
144
   * character.
145
   *
146
   * @param word The word to check.
147
   * @return {@code true} if the word begins with a letter.
148
   */
149
  private boolean isWord( final String word ) {
150
    return !word.isEmpty() && isLetter( word.charAt( 0 ) );
151
  }
152
153
  /**
154
   * Returns a list of {@link SuggestItem} instances that provide alternative
155
   * spellings for the given lexeme.
156
   *
157
   * @param lexeme A word to look up in the lexicon.
158
   * @param v      Influences the number of results returned.
159
   * @return Alternative lexemes.
160
   */
161
  private List<SuggestItem> lookup( final String lexeme, final Verbosity v ) {
162
    return getSpeller().lookup( lexeme, v );
163
  }
164
165
  private SymSpell getSpeller() {
166
    return mSymSpell;
167
  }
168
}
1169
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
A src/main/java/com/keenwrite/util/BoundedCache.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.util.LinkedHashMap;
5
import java.util.Map;
6
7
/**
8
 * A map that removes the oldest entry once its capacity (cache size) has
9
 * been reached.
10
 *
11
 * @param <K> The type of key mapped to a value.
12
 * @param <V> The type of value mapped to a key.
13
 */
14
public final class BoundedCache<K, V> extends LinkedHashMap<K, V> {
15
  private final int mCacheSize;
16
17
  /**
18
   * Constructs a new instance having a finite size.
19
   *
20
   * @param cacheSize The maximum number of entries.
21
   */
22
  public BoundedCache( final int cacheSize ) {
23
    mCacheSize = cacheSize;
24
  }
25
26
  @Override
27
  protected boolean removeEldestEntry(
28
      final Map.Entry<K, V> eldest ) {
29
    return size() > mCacheSize;
30
  }
31
}
132
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
A src/main/java/com/keenwrite/util/GenericBuilder.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.util.ArrayList;
5
import java.util.List;
6
import java.util.function.BiConsumer;
7
import java.util.function.Consumer;
8
import java.util.function.Function;
9
import java.util.function.Supplier;
10
11
/**
12
 * Responsible for constructing objects that would otherwise require
13
 * a long list of constructor parameters.
14
 * <p>
15
 * See <a href="https://stackoverflow.com/a/31754787/59087">source</a> for
16
 * details.
17
 * </p>
18
 *
19
 * @param <MT> The mutable definition for the type of object to build.
20
 * @param <IT> The immutable definition for the type of object to build.
21
 */
22
public class GenericBuilder<MT, IT> {
23
  /**
24
   * Provides the methods to use for setting object properties.
25
   */
26
  private final Supplier<MT> mMutable;
27
28
  /**
29
   * Calling {@link #build()} will instantiate the immutable instance using
30
   * the mutator.
31
   */
32
  private final Function<MT, IT> mImmutable;
33
34
  /**
35
   * Adds a modifier to call when building an instance.
36
   */
37
  private final List<Consumer<MT>> mModifiers = new ArrayList<>();
38
39
  /**
40
   * Constructs a new builder instance that is capable of populating values for
41
   * any type of object.
42
   *
43
   * @param mutator Provides methods to use for setting object properties.
44
   */
45
  protected GenericBuilder(
46
      final Supplier<MT> mutator, final Function<MT, IT> immutable ) {
47
    assert mutator != null;
48
    assert immutable != null;
49
50
    mMutable = mutator;
51
    mImmutable = immutable;
52
  }
53
54
  /**
55
   * Starting point for building an instance of a particular class.
56
   *
57
   * @param supplier Returns the instance to build.
58
   * @param <MT>     The type of class to build.
59
   * @return A new {@link GenericBuilder} capable of populating data for an
60
   * instance of the class provided by the {@link Supplier}.
61
   */
62
  public static <MT, IT> GenericBuilder<MT, IT> of(
63
      final Supplier<MT> supplier, final Function<MT, IT> immutable ) {
64
    return new GenericBuilder<>( supplier, immutable );
65
  }
66
67
  /**
68
   * Registers a new value with the builder.
69
   *
70
   * @param consumer Accepts a value to be set upon the built object.
71
   * @param value    The value to use when building.
72
   * @param <V>      The type of value used when building.
73
   * @return This {@link GenericBuilder} instance.
74
   */
75
  public <V> GenericBuilder<MT, IT> with(
76
      final BiConsumer<MT, V> consumer, final V value ) {
77
    mModifiers.add( instance -> consumer.accept( instance, value ) );
78
    return this;
79
  }
80
81
  /**
82
   * Instantiates then populates the immutable object to build.
83
   *
84
   * @return The newly built object.
85
   */
86
  public IT build() {
87
    final var value = mMutable.get();
88
    mModifiers.forEach( modifier -> modifier.accept( value ) );
89
    mModifiers.clear();
90
    return mImmutable.apply( value );
91
  }
92
}
193
A src/main/java/com/keenwrite/util/ProtocolScheme.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.io.File;
5
import java.net.URI;
6
import java.net.URL;
7
8
/**
9
 * Represents the type of data encoding scheme used for a universal resource
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).
14
 */
15
public enum ProtocolScheme {
16
  /**
17
   * Denotes a local file.
18
   */
19
  FILE,
20
  /**
21
   * Denotes either HTTP or HTTPS.
22
   */
23
  HTTP,
24
  /**
25
   * Denotes Java archive file.
26
   */
27
  JAR,
28
  /**
29
   * Could not determine schema (or is not supported by the application).
30
   */
31
  UNKNOWN;
32
33
  /**
34
   * Returns the protocol for a given URI or file name.
35
   *
36
   * @param resource Determine the protocol for this URI or file name.
37
   * @return The protocol for the given resource.
38
   */
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
    }
53
  }
54
55
  /**
56
   * Determines the protocol scheme for a given string.
57
   *
58
   * @param protocol A string representing data encoding protocol scheme.
59
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
60
   * valid value from this enumeration.
61
   */
62
  public static ProtocolScheme valueFrom( final String protocol ) {
63
    final var sanitized = protocol == null ? "" : protocol.toUpperCase();
64
65
    for( final var scheme : values() ) {
66
      // This will match HTTP/HTTPS as well as FILE*, which may be inaccurate.
67
      if( sanitized.startsWith( scheme.name() ) ) {
68
        return scheme;
69
      }
70
    }
71
72
    return UNKNOWN;
73
  }
74
75
  /**
76
   * Determines the protocol scheme for a given {@link File}.
77
   *
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.
81
   */
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;
138
  }
139
}
1140
A src/main/java/com/keenwrite/util/ResourceWalker.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.io.IOException;
5
import java.net.URI;
6
import java.net.URISyntaxException;
7
import java.nio.file.Files;
8
import java.nio.file.Path;
9
import java.nio.file.Paths;
10
import java.util.function.Consumer;
11
12
import static java.nio.file.FileSystems.getDefault;
13
import static java.nio.file.FileSystems.newFileSystem;
14
import static java.util.Collections.emptyMap;
15
16
/**
17
 * Responsible for finding file resources.
18
 */
19
public class ResourceWalker {
20
  /**
21
   * Globbing pattern to match font names.
22
   */
23
  public static final String GLOB_FONTS = "**.{ttf,otf}";
24
25
  /**
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.
29
   * @throws URISyntaxException Could not convert the resource to a URI.
30
   * @throws IOException        Could not walk the tree.
31
   */
32
  public static void walk(
33
      final String directory, final String glob, final Consumer<Path> c )
34
      throws URISyntaxException, IOException {
35
    final var resource = ResourceWalker.class.getResource( directory );
36
    final var matcher = getDefault().getPathMatcher( "glob:" + glob );
37
38
    if( resource != null ) {
39
      final var uri = resource.toURI();
40
      final var jar = uri.getScheme().equals( "jar" );
41
      final var path = jar ? toFileSystem( uri, directory ) : Paths.get( uri );
42
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
          }
49
        }
50
      }
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 );
58
    }
59
  }
60
}
161
A src/main/r/README.md
1
# R Scripts
2
3
These R scripts illustrate how R can be used within an application to perform calculations using variables. Authors are free to write their own scripts, of course. These scripts serve as an example of how to automate certain tasks while writing.
4
5
## Configuration
6
7
Configure the editor to use the R scripts as follows:
8
9
1. Copy the R scripts into same directory as your Markdown files.
10
1. Start the editor.
11
1. Click **Tools → R Script**.
12
1. Copy and paste the following:
13
14
        assign( 'anchor', as.Date( '$date.anchor$', format='%Y-%m-%d' ), envir = .GlobalEnv );
15
        setwd( '$application.r.working.directory$' );
16
        source( 'pluralize.R' );
17
        source( 'csv.R' );
18
        source( 'conversion.R' );
19
20
1. Click **File → New** to create a new file.
21
1. Click **File → Save As** to set a filename.
22
1. Set **Name** to: `variables.yaml`
23
1. Click **OK**.
24
1. Paste the following definitions:
25
26
        date:
27
          anchor: 2017-01-01
28
        editor:
29
          examples:
30
            season: 2017-09-02
31
            math:
32
              x: 1
33
              y: $editor.examples.math.x$ + 1
34
              z: $editor.examples.math.y$ + 1
35
            name:
36
              given: Josephene
37
38
1. Save and close the file.
39
1. Click **File → Open**
40
1. Change **Markdown Files** to **Definition Files**.
41
1. Select `variables.yaml`.
42
1. Click **Open**.
43
44
R functionality is configured.
45
46
## Definitions
47
48
The variables definitions within `variables.yaml` are available to R using the R syntax. An additional variable, `application.r.working.directory` is added to the list of variables. The value is set to the working directory of the file being edited. Hover the mouse cursor over the file tab in the editor to see the full path to the file.
49
50
## Examples
51
52
This section demonstrates how to use the R functions when editing. Complete the following steps to begin:
53
54
1. Click **File → New** to create a new file.
55
1. Click **File → Save As** to set a filename.
56
1. Set **Name** to: `example.Rmd`
57
1. Click **OK**.
58
59
The examples are ready for use within the editor.
60
61
### Arithmetic
62
63
Type the following to perform a simple calculation:
64
65
    `r# 1+1`
66
67
The preview pane shows `2.0`.
68
69
### Functions
70
71
Call the [format](https://stat.ethz.ch/R-manual/R-devel/library/base/html/format.html) function to truncate unwanted decimal places as follows:
72
73
    `r# format(1+1,digits=1)`
74
75
The preview pane shows `2`.
76
77
### Pluralize
78
79
Many English words can be pluralized as follows:
80
81
    `r# pl('wolf',2)`
82
83
The preview pane shows `wolves`. The `pluralize.R` file contains a partial implementation of Damian Conway's algorithmic approach to English pluralization.
84
85
### Chicago Manual of Style
86
87
Apply the Chicago Manual of Style for words less than one-hundred as follows:
88
89
       `r# cms(1)` `r# cms(99)` `r# cms(101)`
90
91
The preview pane shows numbers written out as `one` and `ninety-nine`, followed by the digits 101.
92
93
### Data Import
94
95
Import and display information from a CSV file as follows:
96
97
1. Click **File → New** to create a new file.
98
1. Click **File → Save As** to rename the file.
99
1. Set the filename to: `data.csv`
100
1. Paste the following into `data.csv`:
101
102
        Animal,Quantity,Country
103
        Aardwolf,1,Africa
104
        Keel-billed toucan,1,Belize
105
        Beaver,2,Canada
106
        Mute swan,3,Denmark
107
        Lion,5,Ethiopia
108
        Brown bear,8,Finland
109
        Dolphin,13,Greece
110
        Turul,21,Hungary
111
        Gyrfalcon,34,Iceland
112
        Red-billed streamertail,55,Jamaica
113
114
1. Click the `example.Rmd` tab.
115
1. Type the following:
116
117
       `r# csv2md('data.csv',total=F)`
118
119
1. Type the following to calculate a total for all numeric columns:
120
121
       `r# csv2md('data.csv')`
122
123
This imports the data from an external file and formats the information into a table, automatically. Update the data as follows:
124
125
1. Click the `data.csv` tab to edit the data.
126
1. Change the data by adding a new row.
127
1. Save the file.
128
1. Click the `example.Rmd` tab.
129
130
The preview pane shows the revised contents.
131
132
### Elapsed Time
133
134
The duration of a timeline, given in numbers of days, can be computed into English as follows:
135
136
    `r# elapsed(1,1)`
137
138
The preview pane shows `same day`. Change the expression to:
139
140
    `r# elapsed(1,2)`
141
142
The preview pane shows `one day`. Change the expression to:
143
144
    `r# elapsed(1,112358)`
145
146
The preview pane shows `307 years, seven months, and sixteen days`, combined using the Chicago Manual of Style, the pluralization function, and a [serial comma](https://www.behance.net/gallery/19417363/The-Oxford-Comma).
147
148
### Variable Syntax
149
150
The syntax for a variable changes when using an R Markdown file (denoted by the `.Rmd` filename extension), as opposed to a regular Markdown file (`.md`). Return to the example file and type the following:
151
152
    `r# v$date$anchor`
153
154
The preview pane shows the date.
155
156
### Autocomplete
157
158
Automatically insert a variable reference into the text as follows:
159
160
1. Type: `Jos`
161
    * Note the capital letter, matches are case sensitive.
162
1. Hold down the `Control` key.
163
1. Tap the `Spacebar`
164
165
The editor shows:
166
167
    `r#x( v$editor$examples$name$given )`
168
169
The preview pane shows:
170
171
    Josephine
172
173
Here, the `x` function evaluates its parameter as an expression. This allows variables to include expressions in their definition.
174
175
### Variable Definition Expressions
176
177
Definition file variables are have the ability to reference other definitions. Try the following:
178
179
    x = `r#x( v$editor$examples$math$x )`;
180
    y = `r#x( v$editor$examples$math$y )`;
181
    z = `r#x( v$editor$examples$math$z )`
182
183
The preview pane shows:
184
185
    x = 1.0; y = 2.0; z = 3.0
186
187
### Case
188
189
Ensure words begin with a lowercase letter as follows:
190
191
    `r#lc( v$editor$examples$name$given )`
192
193
The preview pane shows:
194
195
    josephine
196
197
Similarly, ensure an uppercase letter as follows:
198
199
    `r#uc( 'hello, world!' )`
200
201
The preview pane shows:
202
203
    Hello, world!
204
205
### Month
206
207
Display the month name given a month number as follows:
208
209
    `r# month( 1 )`
210
211
The preview pane shows:
212
213
    January
214
215
## Summary
216
217
Authors can inline R statements into documents, directly, so long as those statements generate text. Plots, graphs, and images must be referenced as external image files or URLs.
1218
A src/main/r/conversion.R
1
# ########################################################################
2
#
3
# Substitute R expressions in a document with their evaluated value. The
4
# anchor variable must be set for functions that use relative dates.
5
#
6
# ########################################################################
7
8
# Evaluates an expression; writes s if there is no expression.
9
x <- function( s ) {
10
  return(
11
    tryCatch({
12
      r = eval( parse( text=s ) )
13
14
      # If the result isn't primitive, then it was probably parsed into
15
      # an unprintable object (e.g., "gray" becomes a colour). In those
16
      # cases, return the original text string. Otherwise, an atomic
17
      # value means a primitive type (string, integer, etc.) that can be
18
      # written directly into the document.
19
      #
20
      # See: http://stackoverflow.com/a/19501276/59087
21
      if( is.atomic( r ) ) {
22
        r
23
      }
24
      else {
25
        s
26
      }
27
    },
28
    warning = function( w ) {
29
      s
30
    },
31
    error = function( e ) {
32
      s
33
    })
34
  )
35
}
36
37
# Returns a date offset by a given number of days, relative to the given
38
# date (d). This does not use the anchor, but is used to get the anchor's
39
# value as a date.
40
when <- function( d, n = 0, format = "%Y-%m-%d" ) {
41
  as.Date( d, format = format ) + x( n )
42
}
43
44
# Full date (s) offset by an optional number of days before or after.
45
# This will remove leading zeros (applying leading spaces instead, which
46
# are ignored by any worthwhile typesetting engine).
47
annal <- function( days = 0, format = "%Y-%m-%d", oformat = "%B %d, %Y" ) {
48
  format( when( anchor, days ), format = oformat )
49
}
50
51
# Extracts the year from a date string.
52
year <- function( days = 0, format = "%Y-%m-%d" ) {
53
  annal( days, format, "%Y" )
54
}
55
56
# Day of the week (in days since the anchor date).
57
weekday <- function( n ) {
58
  weekdays( when( anchor, n ) )
59
}
60
61
# String concatenate function alias because paste0 is a terrible name.
62
concat <- paste0
63
64
# Translates a number from digits to words using Chicago Manual of Style.
65
# This does not translate numbers greater than one hundred. If ordinal
66
# is TRUE, this will return the ordinal name. This will not produce ordinals
67
# for numbers greater than 100
68
cms <- function( n, ordinal = FALSE ) {
69
  n <- x( n )
70
71
  # We're done here.
72
  if( n == 0 ) {
73
    if( ordinal ) {
74
      return( "zeroth" )
75
    }
76
77
    return( "zero" )
78
  }
79
80
  # Concatenate this a little later.
81
  if( n < 0 ) {
82
    result = "negative "
83
    n = abs( n )
84
  }
85
86
  # Do not spell out numbers greater than one hundred.
87
  if( n > 100 ) {
88
    # Comma-separated numbers.
89
    return( format( n, big.mark=",", trim=TRUE, scientific=FALSE ) )
90
  }
91
92
  # Don't go beyond 100.
93
  if( n == 100 ) {
94
    if( ordinal ) {
95
      return( "one hundredth" )
96
    }
97
98
    return( "one hundred" )
99
  }
100
101
  # Samuel Langhorne Clemens noted English has too many exceptions.
102
  small = c(
103
    "one", "two", "three", "four", "five",
104
    "six", "seven", "eight", "nine", "ten",
105
    "eleven", "twelve", "thirteen", "fourteen", "fifteen",
106
    "sixteen", "seventeen", "eighteen", "nineteen"
107
  )
108
109
  ord_small = c(
110
    "first", "second", "third", "fourth", "fifth",
111
    "sixth", "seventh", "eighth", "ninth", "tenth",
112
    "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth",
113
    "sixteenth", "seventeenth", "eighteenth", "nineteenth", "twentieth"
114
  )
115
116
  # After this, the number (n) is between 20 and 99.
117
  if( n < 20 ) {
118
    if( ordinal ) {
119
      return( .subset( ord_small, n %% 100 ) )
120
    }
121
122
    return( .subset( small, n %% 100 ) )
123
  }
124
125
  tens = c( "",
126
    "twenty", "thirty", "forty", "fifty",
127
    "sixty", "seventy", "eighty", "ninety"
128
  )
129
130
  ord_tens = c( "",
131
    "twentieth", "thirtieth", "fortieth", "fiftieth",
132
    "sixtieth", "seventieth", "eightieth", "ninetieth"
133
  )
134
135
  ones_index = n %% 10
136
  n = n %/% 10
137
138
  # No number in the ones column, so the number must be a multiple of ten.
139
  if( ones_index == 0 ) {
140
    if( ordinal ) {
141
      return( .subset( ord_tens, n ) )
142
    }
143
144
    return( .subset( tens, n ) )
145
  }
146
147
  # Find the value from the ones column.
148
  if( ordinal ) {
149
    unit_1 = .subset( ord_small, ones_index )
150
  }
151
  else {
152
    unit_1 = .subset( small, ones_index )
153
  }
154
155
  # Find the tens column.
156
  unit_10 = .subset( tens, n )
157
158
  # Hyphenate the tens and the ones together.
159
  concat( unit_10, concat( "-", unit_1 ) )
160
}
161
162
# Returns a human-readable string that provides the elapsed time between
163
# two numbers in terms of years, months, and days. If any unit value is zero,
164
# the unit is not included. The words (year, month, day) are pluralized
165
# according to English grammar. The numbers are written out according to
166
# Chicago Manual of Style. This applies the serial comma.
167
#
168
# Both numbers are offsets relative to the anchor date.
169
#
170
# If all unit values are zero, this returns s ("same day" by default).
171
#
172
# If the start date (began) is greater than end date (ended), the dates are
173
# swapped before calculations are performed. This allows any two dates
174
# to be compared and positive unit values are always returned.
175
#
176
elapsed <- function( began, ended, s = "same day" ) {
177
  began = when( anchor, began )
178
  ended = when( anchor, ended )
179
180
  # Swap the dates if the end date comes before the start date.
181
  if( as.integer( ended - began ) < 0 ) {
182
    tempd = began
183
    began = ended
184
    ended = tempd
185
  }
186
187
  # Calculate number of elapsed years.
188
  years = length( seq( from = began, to = ended, by = 'year' ) ) - 1
189
190
  # Move the start date up by the number of elapsed years.
191
  if( years > 0 ) {
192
    began = seq( began, length = 2, by = concat( years, " years" ) )[2]
193
    years = pl.numeric( "year", years )
194
  }
195
  else {
196
    # Zero years.
197
    years = ""
198
  }
199
200
  # Calculate number of elapsed months, excluding years.
201
  months = length( seq( from = began, to = ended, by = 'month' ) ) - 1
202
203
  # Move the start date up by the number of elapsed months
204
  if( months > 0 ) {
205
    began = seq( began, length = 2, by = concat( months, " months" ) )[2]
206
    months = pl.numeric( "month", months )
207
  }
208
  else {
209
    # Zero months
210
    months = ""
211
  }
212
213
  # Calculate number of elapsed days, excluding months and years.
214
  days = length( seq( from = began, to = ended, by = 'day' ) ) - 1
215
216
  if( days > 0 ) {
217
    days = pl.numeric( "day", days )
218
  }
219
  else {
220
    # Zero days
221
    days = ""
222
  }
223
224
  if( years <= 0 && months <= 0 && days <= 0 ) {
225
    return( s )
226
  }
227
228
  # Put them all in a vector, then remove the empty values.
229
  s <- c( years, months, days )
230
  s <- s[ s != "" ]
231
232
  r <- paste( s, collapse = ", " )
233
234
  # If all three items are present, replace the last comma with ", and".
235
  if( length( s ) > 2 ) {
236
    return( gsub( "(.*),", "\\1, and", r ) )
237
  }
238
239
  # Does nothing if no commas are present.
240
  gsub( "(.*),", "\\1 and", r )
241
}
242
243
# Returns the number (n) in English followed by the plural or singular
244
# form of the given string (s; resumably a noun), if applicable, according
245
# to English grammar. That is, pl.numeric( "wolf", 5 ) will return
246
# "five wolves".
247
pl.numeric <- function( s, n ) {
248
  concat( cms( n ), concat( " ", pluralize( s, n ) ) )
249
}
250
251
# Name of the season, starting with an capital letter.
252
season <- function( n, format = "%Y-%m-%d" ) {
253
  WS <- as.Date("2016-12-15", "%Y-%m-%d") # Winter Solstice
254
  SE <- as.Date("2016-03-15", "%Y-%m-%d") # Spring Equinox
255
  SS <- as.Date("2016-06-15", "%Y-%m-%d") # Summer Solstice
256
  AE <- as.Date("2016-09-15", "%Y-%m-%d") # Autumn Equinox
257
258
  d <- when( anchor, n )
259
  d <- as.Date( strftime( d, format="2016-%m-%d" ) )
260
261
  ifelse( d >= WS | d < SE, "Winter",
262
    ifelse( d >= SE & d < SS, "Spring",
263
      ifelse( d >= SS & d < AE, "Summer", "Autumn" )
264
    )
265
  )
266
}
267
268
# Converts the first letter in a string to lowercase
269
lc <- function( s ) {
270
  concat( tolower( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
271
}
272
273
# Converts the first letter in a string to uppercase
274
uc <- function( s ) {
275
  concat( toupper( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
276
}
277
278
# Returns the number of days between the given dates.
279
days <- function( d1, d2, format = "%Y-%m-%d" ) {
280
  dates = c( d1, d2 )
281
  dt = strptime( dates, format = format )
282
  as.integer( difftime( dates[2], dates[1], units = "days" ) )
283
}
284
285
# Returns the number of years elapsed.
286
years <- function( began, ended ) {
287
  began = when( anchor, began )
288
  ended = when( anchor, ended )
289
290
  # Swap the dates if the end date comes before the start date.
291
  if( as.integer( ended - began ) < 0 ) {
292
    tempd = began
293
    began = ended
294
    ended = tempd
295
  }
296
297
  # Calculate number of elapsed years.
298
  length( seq( from = began, to = ended, by = 'year' ) ) - 1
299
}
300
301
# Full name of the month, starting with a capital letter.
302
month <- function( n ) {
303
  # Faster than month.name[ x( n ) ]
304
  .subset( month.name, x( n ) )
305
}
306
307
money <- function( n ) {
308
  formatC( x( n ), format="d" )
309
}
1310
A src/main/r/csv.R
1
# ######################################################################
2
#
3
# Copyright 2016, White Magic Software, Ltd.
4
# 
5
# Permission is hereby granted, free of charge, to any person obtaining
6
# a copy of this software and associated documentation files (the
7
# "Software"), to deal in the Software without restriction, including
8
# without limitation the rights to use, copy, modify, merge, publish,
9
# distribute, sublicense, and/or sell copies of the Software, and to
10
# permit persons to whom the Software is furnished to do so, subject to
11
# the following conditions:
12
# 
13
# The above copyright notice and this permission notice shall be
14
# included in all copies or substantial portions of the Software.
15
# 
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
#
24
# ######################################################################
25
26
# ######################################################################
27
#
28
# Converts CSV to Markdown.
29
#
30
# ######################################################################
31
32
# Reads a CSV file and converts the contents to a Markdown table. The
33
# file must be in the working directory as specified by setwd.
34
#
35
# @param f The filename to convert.
36
# @param decimals Rounded decimal places (default 1).
37
# @param totals Include total sums (default TRUE).
38
# @param align Right-align numbers (default TRUE).
39
csv2md <- function( f, decimals = 1, totals = T, align = T ) {
40
  # Read the CVS data from the file; ensure strings become characters.
41
  df <- read.table( f, sep=',', header=T, stringsAsFactors=F )
42
43
  if( totals ) {
44
    # Determine what columns can be summed.
45
    number <- which( unlist( lapply( df, is.numeric ) ) )
46
47
    # Use colSums when more than one summable column exists.
48
    if( length( number ) > 1 ) {
49
      f.sum <- colSums
50
    }
51
    else {
52
      f.sum <- sum
53
    }
54
55
    # Calculate the sum of all the summable columns and insert the
56
    # results back into the data frame.
57
    df[ (nrow( df ) + 1), number ] <- f.sum( df[, number], na.rm=TRUE )
58
59
    # pluralize would be heavyweight here.
60
    if( length( number ) > 1 ) {
61
      t <- "**Totals**"
62
    }
63
    else {
64
      t <- "**Total**"
65
    }
66
67
    # Change the first column of the last line to "Total(s)".
68
    df[ nrow( df ), 1 ] <- t
69
70
    # Don't clutter the output with "NA" text.
71
    df[ is.na( df ) ] <- ""
72
  }
73
74
  if( align ) {
75
    is.char <- vapply( df, is.character, logical( 1 ) )
76
    dashes <- paste( ifelse( is.char, ':---', '---:' ), collapse='|' )
77
  }
78
  else {
79
    dashes <- paste( rep( '---', length( df ) ), collapse = '|')
80
  }
81
82
  # Create a Markdown version of the data frame.
83
  paste(
84
    paste( names( df ), collapse = '|'), '\n',
85
    dashes, '\n', 
86
    paste(
87
      Reduce( function( x, y ) {
88
          paste( x, format( y, digits = decimals ), sep = '|' )
89
        }, df
90
      ),
91
      collapse = '|\n', sep=''
92
    )
93
  )
94
}
95
196
A src/main/r/pluralize.R
1
# ######################################################################
2
#
3
# Copyright 2016, White Magic Software, Ltd.
4
# 
5
# Permission is hereby granted, free of charge, to any person obtaining
6
# a copy of this software and associated documentation files (the
7
# "Software"), to deal in the Software without restriction, including
8
# without limitation the rights to use, copy, modify, merge, publish,
9
# distribute, sublicense, and/or sell copies of the Software, and to
10
# permit persons to whom the Software is furnished to do so, subject to
11
# the following conditions:
12
# 
13
# The above copyright notice and this permission notice shall be
14
# included in all copies or substantial portions of the Software.
15
# 
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
#
24
# ######################################################################
25
26
# ######################################################################
27
#
28
# See Damian Conway's "An Algorithmic Approach to English Pluralization":
29
#   http://goo.gl/oRL4MP
30
# See Oliver Glerke's Evo Inflector: https://github.com/atteo/evo-inflector/
31
# See Shevek's Pluralizer: https://github.com/shevek/linguistics/
32
# See also: http://www.freevectors.net/assets/files/plural.txt
33
#
34
# ######################################################################
35
36
pluralize <- function( s, n ) {
37
  result <- s
38
39
  # Partial implementation of Conway's algorithm for nouns.
40
  if( n != 1 ) {
41
    if( pl.noninflective( s ) ||
42
        pl.suffix( "fish", s ) ||
43
        pl.suffix( "ois", s ) ||
44
        pl.suffix( "sheep", s ) ||
45
        pl.suffix( "deer", s ) ||
46
        pl.suffix( "pox", s ) ||
47
        pl.suffix( "[A-Z].*ese", s ) ||
48
        pl.suffix( "itis", s ) ) {
49
      # 1. Retain non-inflective user-mapped noun as is.
50
      # 2. Retain non-inflective plural as is.
51
      result <- s
52
    }
53
    else if( pl.is.irregular.pl( s ) ) {
54
      # 4. Change irregular plurals based on mapping.
55
      result <- pl.irregular.pl( s )
56
    }
57
    else if( pl.is.irregular.es( s ) ) {
58
      # x. From Shevek's Pluralizer
59
      result <- pl.inflect( s, "", "es" )
60
    }
61
    else if( pl.suffix( "man", s ) ) {
62
      # 5. For -man, change -an to -en
63
      result <- pl.inflect( s, "an", "en" )
64
    }
65
    else if( pl.suffix( "[lm]ouse", s ) ) {
66
      # 5. For [lm]ouse, change -ouse to -ice
67
      result <- pl.inflect( s, "ouse", "ice" )
68
    }
69
    else if( pl.suffix( "tooth", s ) ) {
70
      # 5. For -tooth, change -ooth to -eeth
71
      result <- pl.inflect( s, "ooth", "eeth" )
72
    }
73
    else if( pl.suffix( "goose", s ) ) {
74
      # 5. For -goose, change -oose to -eese
75
      result <- pl.inflect( s, "oose", "eese" )
76
    }
77
    else if( pl.suffix( "foot", s ) ) {
78
      # 5. For -foot, change -oot to -eet
79
      result <- pl.inflect( s, "oot", "eet" )
80
    }
81
    else if( pl.suffix( "zoon", s ) ) {
82
      # 5. For -zoon, change -on to -a
83
      result <- pl.inflect( s, "on", "a" )
84
    }
85
    else if( pl.suffix( "[csx]is", s ) ) {
86
      # 5. Change -cis, -sis, -xis to -es
87
      result <- pl.inflect( s, "is", "es" )
88
    }
89
    else if( pl.suffix( "([cs]h|ss)", s ) ) {
90
      # 8. Change -ch, -sh, -ss to -es
91
      result <- pl.inflect( s, "", "es" )
92
    }
93
    else if( pl.suffix( "([aeo]lf|[^d]eaf|arf)", s ) ) {
94
      # 9. Change -f to -ves
95
      result <- pl.inflect( s, "f", "ves" )
96
    }
97
    else if( pl.suffix( "[nlw]ife", s ) ) {
98
      # 9. Change -fe to -ves
99
      result <- pl.inflect( s, "fe", "ves" )
100
    }
101
    else if( pl.suffix( "([aeiou]y|[A-Z].*y)", s ) ) {
102
      # 10. Change -y to -ys.
103
      result <- pl.inflect( s, "", "s" )
104
    }
105
    else if( pl.suffix( "y", s ) ) {
106
      # 10. Change -y to -ies.
107
      result <- pl.inflect( s, "y", "ies" )
108
    }
109
    else {
110
      # 13. Default plural: add -s.
111
      result <- pl.inflect( s, "", "s" )
112
    }
113
  }
114
115
  result
116
}
117
118
# Pluralize s if n is not equal to 1.
119
pl <- function( s, n ) {
120
  pluralize( s, x( n ) )
121
}
122
123
# Returns the given string (s) with its suffix replaced by r.
124
pl.inflect <- function( s, suffix, r ) {
125
  gsub( paste( suffix, "$", sep="" ), r, s )
126
}
127
128
# Answers whether the given string (s) has the given ending.
129
pl.suffix <- function( ending, s ) {
130
  grepl( paste( ending, "$", sep="" ), s )
131
}
132
133
# Answers whether the given string (s) is a noninflective noun.
134
pl.noninflective <- function( s ) {
135
  v <- c(
136
    "aircraft", "Bhutanese", "bison", "bream", "breeches", "britches",
137
    "Burmese", "carp", "chassis", "Chinese", "clippers", "cod", "contretemps",
138
    "corps", "debris", "diabetes", "djinn", "eland", "elk", "flounder",
139
    "fracas", "gallows", "graffiti", "headquarters", "herpes", "high-jinks",
140
    "homework", "hovercraft", "innings", "jackanapes", "Japanese",
141
    "Lebanese", "mackerel", "means", "measles", "mews", "mumps", "news",
142
    "pincers", "pliers", "Portuguese", "proceedings", "rabies", "salmon",
143
    "scissors", "sea-bass", "Senegalese", "series", "shears", "Siamese",
144
    "Sinhalese", "spacecraft", "species", "swine", "trout", "tuna",
145
    "Vietnamese", "watercraft", "whiting", "wildebeest"
146
  )
147
148
  is.element( s, v )
149
}
150
151
# Answers whether the given string (s) is an irregular plural.
152
pl.is.irregular.pl <- function( s ) {
153
  # Could be refactored with pl.irregular.pl...
154
  v <- c(
155
    "beef", "brother", "child", "cow", "ephemeris", "genie", "money",
156
    "mongoose", "mythos", "octopus", "ox", "soliloquy", "trilby"
157
  )
158
159
  is.element( s, v )
160
}
161
162
# Call to pluralize an irregular noun. Only call after confirming
163
# the noun is irregular via pl.is.irregular.pl.
164
pl.irregular.pl <- function( s ) {
165
  v <- list(
166
    "beef" = "beefs",
167
    "brother" = "brothers",
168
    "child" = "children",
169
    "cow" = "cows",
170
    "ephemeris" = "ephemerides",
171
    "genie" = "genies",
172
    "money" = "moneys",
173
    "mongoose" = "mongooses",
174
    "mythos" = "mythoi",
175
    "octopus" = "octopuses",
176
    "ox" = "oxen",
177
    "soliloquy" = "soliloquies",
178
    "trilby" = "trilbys"
179
  )
180
181
  # Faster version of v[[ s ]]
182
  .subset2( v, s )
183
}
184
185
# Answers whether the given string (s) pluralizes with -es.
186
pl.is.irregular.es <- function( s ) {
187
  v <- c(
188
    "acropolis", "aegis", "alias", "asbestos", "bathos", "bias", "bronchitis",
189
    "bursitis", "caddis", "cannabis", "canvas", "chaos", "cosmos", "dais",
190
    "digitalis", "epidermis", "ethos", "eyas", "gas", "glottis", "hubris",
191
    "ibis", "lens", "mantis", "marquis", "metropolis", "pathos", "pelvis",
192
    "polis", "rhinoceros", "sassafrass", "trellis"
193
  )
194
195
  is.element( s, v )
196
}
197
1198
A src/main/resources/META-INF/services/com.keenwrite.service.Settings
1
1
com.keenwrite.service.impl.DefaultSettings
A src/main/resources/META-INF/services/com.keenwrite.service.Snitch
1
1
com.keenwrite.service.impl.DefaultSnitch
A src/main/resources/META-INF/services/com.keenwrite.service.events.Notifier
1
1
com.keenwrite.service.events.impl.DefaultNotifier
A src/main/resources/bootstrap.properties
1
# Used by the Gradle build script and the application.
2
application.title=KeenWrite
3
14
A src/main/resources/com/keenwrite/.gitignore
1
app.properties
12
A src/main/resources/com/keenwrite/app-title.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="971.53119"
12
   height="498.39355"
13
   viewBox="0 0 971.53119 498.39354"
14
   xml:space="preserve"
15
   id="svg52"
16
   sodipodi:docname="app-title.svg"
17
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
18
   inkscape:export-filename="/home/jarvisd/dev/java/scrivenvar/docs/images/app-title.png"
19
   inkscape:export-xdpi="24.66"
20
   inkscape:export-ydpi="24.66"><metadata
21
   id="metadata56"><rdf:RDF><cc:Work
22
       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
23
         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
24
   inkscape:document-rotation="0"
25
   pagecolor="#ffffff"
26
   bordercolor="#666666"
27
   borderopacity="1"
28
   objecttolerance="10"
29
   gridtolerance="10"
30
   guidetolerance="10"
31
   inkscape:pageopacity="0"
32
   inkscape:pageshadow="2"
33
   inkscape:window-width="640"
34
   inkscape:window-height="480"
35
   id="namedview54"
36
   showgrid="false"
37
   inkscape:zoom="0.78417969"
38
   inkscape:cx="455.5775"
39
   inkscape:cy="347.59625"
40
   inkscape:current-layer="svg52"
41
   fit-margin-top="0"
42
   fit-margin-left="0"
43
   fit-margin-right="0"
44
   fit-margin-bottom="0" />
45
<desc
46
   id="desc2">Created with Fabric.js 3.6.3</desc>
47
<defs
48
   id="defs4"><rect
49
   x="114.92139"
50
   y="132.06313"
51
   width="470.12033"
52
   height="175.55823"
53
   id="rect933" />
54
55
56
57
		
58
		
59
		
60
		
61
		
62
		
63
		
64
		
65
66
<linearGradient
67
   y2="-0.049471263"
68
   x2="0.96880889"
69
   y1="-0.044911571"
70
   x1="0.15235768"
71
   gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)"
72
   gradientUnits="userSpaceOnUse"
73
   id="SVGID_1_302284">
74
<stop
75
   id="stop9"
76
   style="stop-color:#ec706a;stop-opacity:1"
77
   offset="0%" />
78
<stop
79
   id="stop11"
80
   style="stop-color:#ecd980;stop-opacity:1"
81
   offset="100%" />
82
</linearGradient>
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
</defs>
99
100
<g
101
   id="g853"
102
   transform="translate(-394.35834,-171.20491)"><path
103
     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"
104
     paint-order="stroke"
105
     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"
106
     stroke-linecap="round"
107
     id="path14" /><path
108
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
109
     paint-order="stroke"
110
     d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z"
111
     stroke-linecap="round"
112
     id="path22" /><path
113
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
114
     paint-order="stroke"
115
     d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z"
116
     stroke-linecap="round"
117
     id="path26" /><path
118
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
119
     paint-order="stroke"
120
     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"
121
     stroke-linecap="round"
122
     id="path30" /><path
123
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
124
     paint-order="stroke"
125
     d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z"
126
     stroke-linecap="round"
127
     id="path34" /><path
128
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
129
     paint-order="stroke"
130
     d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z"
131
     stroke-linecap="round"
132
     id="path38" /><path
133
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
134
     paint-order="stroke"
135
     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"
136
     stroke-linecap="round"
137
     id="path42" /></g>
138
139
<text
140
   xml:space="preserve"
141
   id="text931"
142
   style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect933);fill:#000000;fill-opacity:1;stroke:none;"
143
   transform="translate(-394.35834,-171.20491)" /><text
144
   xml:space="preserve"
145
   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"
146
   x="311.66693"
147
   y="402.20627"
148
   id="text939"><tspan
149
     sodipodi:role="line"
150
     id="tspan937"
151
     x="311.66693"
152
     y="402.20627">KeenWrite</tspan></text></svg>
1153
A src/main/resources/com/keenwrite/build.sh
1
#!/bin/bash
2
3
INKSCAPE="/usr/bin/inkscape"
4
PNG_COMPRESS="optipng"
5
PNG_COMPRESS_OPTS="-o9 *png"
6
ICO_TOOL="icotool"
7
ICO_TOOL_OPTS="-c -o ../../../../../icons/logo.ico logo64.png"
8
9
declare -a SIZES=("16" "32" "64" "128" "256" "512")
10
11
for i in "${SIZES[@]}"; do
12
  # -y: export background opacity 0
13
  $INKSCAPE -y 0 -w "${i}" --export-overwrite --export-type=png -o "logo${i}.png" "logo.svg" 
14
done
15
16
# Compess the PNG images.
17
which $PNG_COMPRESS && $PNG_COMPRESS $PNG_COMPRESS_OPTS
18
19
# Generate an ICO file.
20
which $ICO_TOOL && $ICO_TOOL $ICO_TOOL_OPTS
21
122
A src/main/resources/com/keenwrite/editor/markdown.css
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
3
.markdown {
4
  -fx-font-family: 'Noto Sans';
5
  -fx-font-size: 11pt;
6
  -fx-padding: 1em;
7
}
8
9
/* Subtly highlight the current paragraph. */
10
.markdown .paragraph-box:has-caret {
11
  -fx-background-color: #fcfeff;
12
}
13
14
/* Light colour for selection highlight. */
15
.markdown .selection {
16
  -fx-fill: #a6d2ff;
17
}
18
19
/* Decoration for words not found in the lexicon. */
20
.markdown .spelling {
21
  -rtfx-underline-color: rgba(255, 131, 67, .7);
22
  -rtfx-underline-dash-array: 4, 2;
23
  -rtfx-underline-width: 2;
24
  -rtfx-underline-cap: round;
25
}
26
27
.markdown .search {
28
  -rtfx-background-color: #ffe959;
29
}
130
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
A src/main/resources/com/keenwrite/logo.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
   xmlns:cc="http://creativecommons.org/ns#"
5
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
   xmlns:svg="http://www.w3.org/2000/svg"
7
   xmlns="http://www.w3.org/2000/svg"
8
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
11
   sodipodi:docname="icon.svg"
12
   id="svg52"
13
   xml:space="preserve"
14
   viewBox="0 0 512 512"
15
   height="512"
16
   width="512"
17
   version="1.1"><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:current-layer="svg52"
22
   inkscape:cy="369.17559"
23
   inkscape:cx="343.24925"
24
   inkscape:zoom="0.78417969"
25
   showgrid="false"
26
   id="namedview54"
27
   inkscape:window-height="480"
28
   inkscape:window-width="640"
29
   inkscape:pageshadow="2"
30
   inkscape:pageopacity="0"
31
   guidetolerance="10"
32
   gridtolerance="10"
33
   objecttolerance="10"
34
   borderopacity="1"
35
   bordercolor="#666666"
36
   pagecolor="#ffffff"
37
   inkscape:document-rotation="0" />
38
<desc
39
   id="desc2">Created with Fabric.js 3.6.3</desc>
40
<defs
41
   id="defs4"><rect
42
   id="rect933"
43
   height="175.55823"
44
   width="470.12033"
45
   y="132.06313"
46
   x="114.92139" />
47
48
49
50
		
51
		
52
		
53
		
54
		
55
		
56
		
57
		
58
59
<linearGradient
60
   id="SVGID_1_302284"
61
   gradientUnits="userSpaceOnUse"
62
   gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)"
63
   x1="0.15235768"
64
   y1="-0.044911571"
65
   x2="0.96880889"
66
   y2="-0.049471263">
67
<stop
68
   offset="0%"
69
   style="stop-color:#ec706a;stop-opacity:1"
70
   id="stop9" />
71
<stop
72
   offset="100%"
73
   style="stop-color:#ecd980;stop-opacity:1"
74
   id="stop11" />
75
</linearGradient>
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
</defs>
92
93
<g
94
   transform="translate(-384.01706,-164.40168)"
95
   id="g853"><path
96
     id="path14"
97
     stroke-linecap="round"
98
     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"
99
     paint-order="stroke"
100
     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" /><path
101
     id="path22"
102
     stroke-linecap="round"
103
     d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z"
104
     paint-order="stroke"
105
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
106
     id="path26"
107
     stroke-linecap="round"
108
     d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z"
109
     paint-order="stroke"
110
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
111
     id="path30"
112
     stroke-linecap="round"
113
     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"
114
     paint-order="stroke"
115
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
116
     id="path34"
117
     stroke-linecap="round"
118
     d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z"
119
     paint-order="stroke"
120
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
121
     id="path38"
122
     stroke-linecap="round"
123
     d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z"
124
     paint-order="stroke"
125
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
126
     id="path42"
127
     stroke-linecap="round"
128
     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"
129
     paint-order="stroke"
130
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /></g>
131
132
<text
133
   style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect933);fill:#000000;fill-opacity:1;stroke:none;"
134
   id="text931"
135
   xml:space="preserve" /></svg>
1136
A src/main/resources/com/keenwrite/logo128.png
Binary file
A src/main/resources/com/keenwrite/logo16.png
Binary file
A src/main/resources/com/keenwrite/logo256.png
Binary file
A src/main/resources/com/keenwrite/logo32.png
Binary file
A src/main/resources/com/keenwrite/logo512.png
Binary file
A src/main/resources/com/keenwrite/logo64.png
Binary file
A src/main/resources/com/keenwrite/messages.properties
1
# ########################################################################
2
# Main Application Window
3
# ########################################################################
4
5
# suppress inspection "UnusedProperty" for whole file
6
7
# ########################################################################
8
# Menu Bar
9
# ########################################################################
10
11
Main.menu.file=_File
12
Main.menu.edit=_Edit
13
Main.menu.insert=_Insert
14
Main.menu.format=Forma_t
15
Main.menu.definition=_Definition
16
Main.menu.view=_View
17
Main.menu.help=_Help
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}
25
26
# ########################################################################
27
# Status Bar
28
# ########################################################################
29
30
Main.status.text.offset=offset
31
Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2}
32
Main.status.state.default=OK
33
Main.status.export.success=Saved as {0}
34
35
Main.status.error.parse={0} (near ${Main.status.text.offset} {1})
36
Main.status.error.def.blank=Move the caret to a word before inserting a definition
37
Main.status.error.def.empty=Create a definition before inserting a definition
38
Main.status.error.def.missing=No definition value found for ''{0}''
39
Main.status.error.r=Error with [{0}...]: {1}
40
Main.status.error.file.missing=Not found: {0}
41
42
Main.status.error.messages.recursion=Lookup depth exceeded, check for loops in ''{0}''
43
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}''
51
52
# ########################################################################
53
# Search Bar
54
# ########################################################################
55
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
66
67
# ########################################################################
68
# Workspace preferences
69
# ########################################################################
70
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
83
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
113
114
# ########################################################################
115
# Definition Pane and its Tree View
116
# ########################################################################
117
118
Definition.menu.add.default=Undefined
119
120
# ########################################################################
121
# Definition Pane
122
# ########################################################################
123
124
Pane.definition.node.root.title=Definitions
125
126
# ########################################################################
127
# Failure messages with respect to YAML files.
128
# ########################################################################
129
130
yaml.error.open=Could not open YAML file (ensure non-empty file).
131
yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''.
132
yaml.error.missing=Empty definition value for key ''{0}''.
133
yaml.error.tree.form=Unassigned definition near ''{0}''.
134
135
# ########################################################################
136
# Text Resource
137
# ########################################################################
138
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
148
149
# ########################################################################
150
# File Open
151
# ########################################################################
152
153
Dialog.file.choose.open.title=Open File
154
Dialog.file.choose.save.title=Save File
155
Dialog.file.choose.export.title=Export File
156
157
Dialog.file.choose.filter.title.source=Source Files
158
Dialog.file.choose.filter.title.definition=Definition Files
159
Dialog.file.choose.filter.title.xml=XML Files
160
Dialog.file.choose.filter.title.all=All Files
161
162
# ########################################################################
163
# Browse File
164
# ########################################################################
165
166
BrowseFileButton.chooser.title=Browse for local file
167
BrowseFileButton.chooser.allFilesFilter=All Files
168
BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
169
170
# ########################################################################
171
# Alert Dialog
172
# ########################################################################
173
174
Alert.file.close.title=Close
175
Alert.file.close.text=Save changes to {0}?
176
177
# ########################################################################
178
# Image Dialog
179
# ########################################################################
180
181
Dialog.image.title=Image
182
Dialog.image.chooser.imagesFilter=Images
183
Dialog.image.previewLabel.text=Markdown Preview\:
184
Dialog.image.textLabel.text=Alternate Text\:
185
Dialog.image.titleLabel.text=Title (tooltip)\:
186
Dialog.image.urlLabel.text=Image URL\:
187
188
# ########################################################################
189
# Hyperlink Dialog
190
# ########################################################################
191
192
Dialog.link.title=Link
193
Dialog.link.previewLabel.text=Markdown Preview\:
194
Dialog.link.textLabel.text=Link Text\:
195
Dialog.link.titleLabel.text=Title (tooltip)\:
196
Dialog.link.urlLabel.text=Link URL\:
197
198
# ########################################################################
199
# About Dialog
200
# ########################################################################
201
202
Dialog.about.title=About {0}
203
Dialog.about.header={0}
204
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
1431
A src/main/resources/com/keenwrite/preview/webview.css
1
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}
2
3
body {
4
  /* Noto Serif introduces whitespace on style transitions. */
5
  font-family: 'Source Serif Pro';
6
  font-size: 12pt;
7
8
  background-color: #fff;
9
  margin: 0 auto;
10
  line-height: 1.6;
11
  color: #454545;
12
  padding: 1em;
13
  font-feature-settings: 'liga' 1;
14
  font-variant-ligatures: normal;
15
}
16
17
body>*:first-child {
18
  margin-top: 0 !important;
19
}
20
21
body>*:last-child {
22
  margin-bottom: 0 !important;
23
}
24
25
#caret {
26
  background: #fcfeff;
27
}
28
29
p, blockquote, ul, ol, dl, table, pre {
30
  margin: 1em 0;
31
}
32
33
h1, h2, h3, h4, h5, h6 {
34
  font-weight: bold;
35
  margin: 1em 0 .5em;
36
}
37
38
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code,
39
h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
40
  font-size: inherit;
41
}
42
43
h1 {
44
  font-size: 21pt;
45
}
46
47
h2 {
48
  font-size: 18pt;
49
  border-bottom: 1px solid #ccc;
50
}
51
52
h3 {
53
  font-size: 15pt;
54
}
55
56
h4 {
57
  font-size: 13.5pt;
58
}
59
60
h5 {
61
  font-size: 12pt;
62
}
63
64
h6 {
65
  font-size: 10.5pt;
66
}
67
68
h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
69
  margin-top: .5em;
70
}
71
72
/* LINKS ***/
73
a {
74
  color: #0077aa;
75
  text-decoration: none;
76
}
77
78
a:hover {
79
  text-decoration: underline;
80
}
81
82
/* BULLET LISTS ***/
83
ul, ol {
84
  display: block;
85
  list-style: disc outside none;
86
  margin: 1em 0;
87
  padding: 0 0 0 2em;
88
}
89
90
ol {
91
  list-style-type: decimal;
92
}
93
94
ul ul, ol ul,
95
ol ol, ul ol {
96
  list-style-position: inside;
97
  margin-left: 1em;
98
}
99
100
ul ul, ol ul {
101
  list-style-type: circle;
102
}
103
104
ol ol, ul ol {
105
  list-style-type: lower-latin;
106
}
107
108
/* DEFINITION LISTS ***/
109
dl {
110
  /** Horizontal scroll bar will appear if set to 100%. */
111
  width: 99%;
112
  overflow: hidden;
113
  padding-left: 1em;
114
}
115
116
dl dt {
117
  font-weight: bold;
118
  float: left;
119
  width: 20%;
120
  clear: both;
121
  position: relative;
122
}
123
124
dl dd {
125
  float: right;
126
  width: 79%;
127
  padding-bottom: .5em;
128
  margin-left: 0;
129
}
130
131
pre, code, tt {
132
  /* Must be bundled in JAR file. */
133
  font-family: 'Source Code Pro';
134
  font-size: 10pt;
135
  background-color: #f8f8f8;
136
  text-decoration: none;
137
  white-space: pre-wrap;
138
  word-wrap: break-word;
139
  overflow-wrap: anywhere;
140
  border-radius: .125em;
141
}
142
143
code, tt {
144
  padding: .25em;
145
}
146
147
pre > code {
148
  padding: 0;
149
  border: none;
150
  background: transparent;
151
}
152
153
pre {
154
  border: .125em solid #ccc;
155
  overflow: auto;
156
  padding: .25em .5em;
157
}
158
159
pre code, pre tt {
160
  background-color: transparent;
161
  border: none;
162
}
163
164
blockquote {
165
  border-left: .25em solid #ccc;
166
  padding: 0 1em;
167
  color: #777;
168
}
169
170
blockquote>:first-child {
171
  margin-top: 0;
172
}
173
174
blockquote>:last-child {
175
  margin-bottom: 0;
176
}
177
178
hr {
179
  clear: both;
180
  margin: 1.5em 0 1.5em;
181
  height: 0;
182
  overflow: hidden;
183
  border: none;
184
  background: transparent;
185
  border-bottom: .125em solid #ccc;
186
}
187
188
table {
189
  width: 100%;
190
}
191
192
tr:nth-child(odd) {
193
  background-color: #eee;
194
}
195
196
th {
197
  background-color: #454545;
198
  color: #fff;
199
}
200
201
th, td {
202
  text-align: left;
203
  padding: 0 1em;
204
}
205
206
img {
207
  max-width: 100%;
208
209
  /* Tell FlyingSaucer to treat images as block elements.
210
   * See SvgReplacedElementFactory.
211
   */
212
  display: inline-block;
213
}
214
215
/* Tell FlyingSaucer to treat tex elements as nodes.
216
 * See SvgReplacedElementFactory.
217
 */
218
tex {
219
  /* Ensure the formulas can be inlined with text. */
220
  display: inline-block;
221
}
222
223
/* Without a robust typesetting engine, there's no
224
 * nice-looking way to automatically typeset equations.
225
 * Sometimes baseline is appropriate, sometimes the
226
 * descender must be considered, and sometimes vertical
227
 * alignment to the middle looks best.
228
 */
229
p tex {
230
  vertical-align: baseline;
231
}
1232
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
A src/main/resources/com/keenwrite/scene.css
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
.tool-bar {
29
	-fx-spacing: 0;
30
}
31
32
.tool-bar .button {
33
	-fx-background-color: transparent;
34
}
35
36
.tool-bar .button:hover {
37
	-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
38
	-fx-color: -fx-hover-base;
39
}
40
41
.tool-bar .button:armed {
42
	-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
51
}
152
A src/main/resources/com/keenwrite/settings.properties
1
# ########################################################################
2
# Application
3
# ########################################################################
4
5
application.title=keenwrite
6
application.package=com/${application.title}
7
application.messages= com.${application.title}.messages
8
9
# Suppress multiple file modified notifications for one logical modification.
10
# Given in milliseconds.
11
application.watchdog.timeout=50
12
13
# ########################################################################
14
# Preferences
15
# ########################################################################
16
17
preferences.root=com.${application.title}
18
preferences.root.state=state
19
preferences.root.options=options
20
preferences.root.definition.source=definition.source
21
22
# ########################################################################
23
# File and Path References
24
# ########################################################################
25
26
#file.stylesheet.dock=com/panemu/tiwulfx/control/dock/tiwulfx-dock.css
27
file.stylesheet.scene=${application.package}/scene.css
28
file.stylesheet.markdown=${application.package}/editor/markdown.css
29
# {0} language code, {1} script code, {2} country code
30
file.stylesheet.markdown.locale=${application.package}/editor/markdown_{0}-{1}-{2}.css
31
file.stylesheet.xml=${application.package}/xml.css
32
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
40
file.logo.128=${application.package}/logo128.png
41
file.logo.256=${application.package}/logo256.png
42
file.logo.512=${application.package}/logo512.png
43
44
# Default file name when a new file is created.
45
# This ensures that the file type can always be
46
# discerned so that the correct type of variable
47
# reference can be inserted.
48
file.default.document=untitled.md
49
file.default.definition=variables.yaml
50
51
# ########################################################################
52
# File name Extensions
53
# ########################################################################
54
55
# Comma-separated list of definition file name extensions.
56
definition.file.ext.json=*.json
57
definition.file.ext.toml=*.toml
58
definition.file.ext.yaml=*.yml,*.yaml
59
definition.file.ext.properties=*.properties,*.props
60
61
# Comma-separated list of file name extensions.
62
file.ext.rmarkdown=*.Rmd
63
file.ext.rxml=*.Rxml
64
file.ext.source=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown},${file.ext.rxml}
65
file.ext.definition=${definition.file.ext.yaml}
66
file.ext.xml=*.xml,${file.ext.rxml}
67
file.ext.all=*.*
68
69
# File name extension search order for images.
70
file.ext.image.order=svg pdf png jpg tiff
71
72
# ########################################################################
73
# Variable Name Editor
74
# ########################################################################
75
76
# Maximum number of characters for a variable name. A variable is defined
77
# as one or more non-whitespace characters up to this maximum length.
78
editor.variable.maxLength=256
79
80
# ########################################################################
81
# Dialog Preferences
82
# ########################################################################
83
84
dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R
85
dialog.alert.button.order.linux=L_HE+UNYACBXIO_R
86
dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R
87
88
# Ensures a consistent button order for alert dialogs across platforms (because
89
# the default button order on Linux defies all logic).
90
dialog.alert.button.order=${dialog.alert.button.order.windows}
191
A src/main/resources/com/keenwrite/xml.css
1
.tagmark {
2
    -fx-fill: gray;
3
}
4
.anytag {
5
    -fx-fill: crimson;
6
}
7
.paren {
8
    -fx-fill: firebrick;
9
    -fx-font-weight: bold;
10
}
11
.attribute {
12
    -fx-fill: darkviolet;
13
}
14
.avalue {
15
    -fx-fill: black;
16
}
117
18
.comment {
19
	-fx-fill: teal;
20
}
A src/main/resources/fonts/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
A src/main/resources/lexicons/README.md
1
# Building
2
3
The lexicon files are retrieved from SymSpell in the parent directory:
4
5
svn export \
6
  https://github.com/wolfgarbe/SymSpell/trunk/SymSpell.FrequencyDictionary/ lexicons
7
8
The lexicons and bigrams are both space-separated, but parsing a
9
tab-delimited file is easier, so change them to tab-separated files.
110
A src/main/resources/lexicons/de.txt
Binary file
A src/main/resources/lexicons/en.txt
Binary file
A src/main/resources/lexicons/es.txt
Binary file
A src/main/resources/lexicons/ext/README.md
1
# Overview
2
3
Lexicons in this directory are meant to relate to a particular subject
4
(medicine, chemistry, math, sports, and such), extend the main lexicon,
5
or not be in common use.
6
17
A src/main/resources/lexicons/ext/contractions.txt
11
2
'aight
3
ain't
4
amn't
5
aren't
6
can't
7
'cause
8
couldn't
9
couldn't've
10
could've
11
daren't
12
daresn't
13
dasn't
14
didn't
15
doesn't
16
don't
17
dunno
18
d'ye
19
e'er
20
everybody's
21
everyone's
22
g'day
23
gimme
24
giv'n
25
gonna
26
gon't
27
gotta
28
hadn't
29
had've
30
hasn't
31
haven't
32
he'd
33
he'll
34
he's
35
he've
36
how'd
37
howdy
38
how'll
39
how're
40
how's
41
how've
42
i'd
43
i'dn't've
44
i'd've
45
i'll
46
i'm
47
i'm'a
48
imma
49
innit
50
isn't
51
it'd
52
it'll
53
it's
54
i've
55
let's
56
ma'am
57
mayn't
58
may've
59
methinks
60
mightn't
61
might've
62
mustn't
63
mustn't've
64
must've
65
needn't
66
ne'er
67
o'clock
68
o'er
69
ol'
70
oughtn't
71
shalln't
72
shan't
73
she'd
74
she'll
75
she's
76
shouldn't
77
shouldn't've
78
should've
79
somebody's
80
someone's
81
something's
82
so're
83
that'd
84
that'll
85
that're
86
that's
87
there'd
88
there'll
89
there're
90
there's
91
these'd
92
these'll
93
these're
94
these've
95
they'd
96
they'll
97
they're
98
they've
99
this's
100
those're
101
those've
102
'tis
103
to've
104
'twas
105
'twouldn't
106
wanna
107
wasn't
108
we'd
109
we'd've
110
we'll
111
we're
112
weren't
113
we've
114
what'd
115
what'll
116
what're
117
what's
118
what've
119
when'd
120
when'll
121
when's
122
where'd
123
where'll
124
where're
125
where's
126
where've
127
which'd
128
which'll
129
which're
130
which's
131
which've
132
who'd
133
who'd've
134
who'll
135
who're
136
who's
137
who've
138
why'd
139
why'll
140
why're
141
why's
142
willn't
143
won't
144
wouldn't
145
wouldn't've
146
would've
147
y'all
148
y'all'd've
149
y'all're
150
you'd
151
you'dn't've
152
you'll
153
you're
154
you've
155
A src/main/resources/lexicons/ext/tech.txt
1
analytics	130337
2
hotspot	130022
3
instantiation	130000
4
onboarding	129953
5
biometric	129795
6
anamorphic	129777
7
benchmarking	129772
8
cybersecurity	129769
9
barcode	129757
10
splitter	129755
11
keychain	129719
12
crowdfunding	129696
13
polymorphism	129688
14
automata	129666
15
shockwave	129658
16
profiler	129648
17
kerning	129646
18
nanometer	129630
19
meridiem	129624
20
influencer	129618
21
passcode	129617
22
sexting	129607
23
cryptology	129606
24
biometrics	129606
25
bitcoin	129599
26
specular	129598
27
accelerometer	129588
28
googolplex	129583
29
grayscale	129576
30
ascender	129571
31
pixelated	129569
32
rockstar	129565
33
ragdoll	129564
34
cyberattack	129564
35
cryptanalysis	129562
36
ransomware	129553
37
crowdsourcing	129552
38
hackathon	129551
39
audiobook	129544
40
degauss	129543
41
attenuator	129540
42
jetpack	129538
43
packrat	129536
44
backlight	129535
45
bootable	129530
46
octothorpe	129529
47
newsfeed	129525
48
extranet	129523
49
failover	129516
50
cyberbullying	129516
51
neumann	129515
52
capacitive	129514
53
backlit	129511
54
millimicron	129507
55
inductor	129505
56
workgroup	129502
57
journaling	129500
58
middleware	129499
59
spooler	129497
60
clamshell	129495
61
wireframe	129494
62
modularity	129493
63
strikethrough	129489
64
petabyte	129487
65
jughead	129482
66
acyclic	129482
67
gearhead	129478
68
stateful	129473
69
submenu	129467
70
pseudorandom	129463
71
earbuds	129461
72
narrowband	129460
73
recordable	129457
74
unallocated	129455
75
mappable	129455
76
chipset	129454
77
multicast	129447
78
loopback	129444
79
pixelate	129441
80
cryptographic	129441
81
pixelation	129438
82
autocorrect	129438
83
teraflop	129437
84
digitizer	129436
85
tunnelling	129434
86
deduplication	129434
87
subwoofer	129433
88
touchpad	129429
89
namespace	129428
90
microcontroller	129428
91
geolocation	129428
92
telepresence	129427
93
driverless	129426
94
photolithography	129425
95
multiphase	129425
96
verifier	129424
97
robocall	129424
98
autofocus	129424
99
kilobit	129422
100
hacktivist	129419
101
geocache	129415
102
rasterize	129412
103
plaintext	129411
104
pipelining	129411
105
technobabble	129409
106
defragment	129409
107
connectionless	129409
108
homomorphic	129407
109
demodulator	129406
110
datagram	129406
111
activex	129406
112
normalisation	129404
113
blackhole	129402
114
cyberstalker	129401
115
multifunction	129400
116
undirected	129397
117
ciphertext	129397
118
superspeed	129396
119
spacebar	129395
120
cyberwar	129395
121
borderless	129395
122
transcode	129393
123
cyberbully	129393
124
multimeter	129392
125
dropship	129391
126
yottabyte	129390
127
infector	129390
128
superclass	129389
129
tooltip	129388
130
dereference	129387
131
combinator	129386
132
milliwatt	129385
133
cyberstalking	129384
134
subfolder	129383
135
wideband	129382
136
noncontiguous	129382
137
ferroelectric	129382
138
cybersquatting	129378
139
autofill	129378
140
trackpad	129376
141
associatively	129376
142
luggable	129374
143
seamonkey	129373
144
defragmentation	129373
145
starcraft	129371
146
obliquing	129371
147
leadless	129371
148
greeking	129371
149
upgradeable	129370
150
radiosity	129370
151
transcoding	129369
152
quintillionth	129369
153
bitmapped	129369
154
subdirectory	129368
155
degausser	129368
156
curtiss	129368
157
scunthorpe	129367
158
undelete	129365
159
gigaflops	129365
160
darknet	129365
161
zettabyte	129364
162
topologies	129363
163
spidering	129363
164
photorealism	129363
165
multithreading	129363
166
deallocate	129363
167
mersenne	129362
168
machinima	129361
169
satisfiable	129360
170
laserjet	129360
171
multicore	129359
172
microblog	129359
173
megaflops	129359
174
homeomorphic	129359
175
microblogging	129358
176
kilobaud	129358
177
cyberwarfare	129358
178
microarchitecture	129357
179
autosave	129357
180
wirelessly	129356
181
sneakernet	129355
182
textbox	129354
183
obfuscator	129354
184
microkernel	129353
185
substring	129352
186
macroinstruction	129352
187
endianness	129352
188
indexable	129351
189
backtick	129351
190
unshielded	129350
191
cleartext	129350
192
autocomplete	129349
193
abandonware	129349
194
hacktivism	129348
195
antikythera	129348
196
stereolithography	129347
197
photorealistic	129347
198
macrovision	129347
199
greasemonkey	129347
200
geotagging	129347
201
disassembler	129346
202
spacewar	129345
203
pluggable	129345
204
kilobits	129345
205
webcomic	129344
206
unfollow	129344
207
photosensor	129344
208
petaflop	129344
209
garageband	129344
210
truetype	129343
211
subnetwork	129342
212
backpropagation	129342
213
supercomputing	129340
214
smartwatch	129340
215
unbundled	129339
216
smilies	129339
217
milliamp	129339
218
bytecode	129339
219
trackpoint	129337
220
slipstreaming	129337
221
monospace	129337
222
memoization	129337
223
scaleable	129336
224
respawn	129335
225
multicasting	129335
226
geocacher	129335
227
workgroups	129334
228
ferrofluid	129334
229
smartdrive	129333
230
subsampling	129332
231
rasterization	129332
232
guiltware	129332
233
defragger	129332
234
satisfiability	129331
235
activision	129331
236
subdirectories	129330
237
segfault	129330
238
flamebait	129330
239
framebuffer	129329
240
defragging	129329
241
decompiler	129329
242
unshift	129328
243
memristor	129328
244
zebibyte	129327
245
semiprime	129327
246
rotoscoping	129327
247
hypertransport	129327
248
smartmedia	129326
249
grayware	129326
250
defragmenting	129326
251
defragmenter	129326
252
repagination	129325
253
subnetting	129324
254
skeuomorphism	129324
255
screencast	129324
256
stylesheet	129323
257
superintelligence	129322
258
multitenancy	129322
259
datastore	129322
260
autoplay	129322
261
repaginate	129321
262
macbook	129321
263
geotagged	129321
264
baudrate	129321
265
transmeta	129320
266
screwless	129320
267
nameserver	129320
268
interexchange	129320
269
geocoding	129319
270
downloader	129319
271
autodiscovery	129319
272
extortion	65752
273
emoji	65684
274
googol	65618
275
backside	65388
276
fibre	65387
277
metre	65333
278
royale	65173
279
radix	65093
280
hotdog	65091
281
lecher	65062
282
uptime	65009
283
unbound	64979
284
eniac	64975
285
synaptic	64966
286
voxel	64926
287
selfie	64917
288
uplink	64887
289
fanboy	64857
290
defrag	64849
291
nondisclosure	64839
292
qubit	64828
293
yippie	64821
294
gearhead	64819
295
subnet	64818
296
endian	64798
297
bezier	64797
298
reallocation	64796
299
telephonic	64789
300
mosfet	64777
301
mutex	64775
302
inkjet	64772
303
gobbing	64768
304
shader	64766
305
ultralight	64755
306
hackers	64746
307
pacman	64742
308
unlink	64741
309
undock	64740
310
understroke	64738
311
beginners	64736
312
photoscope	64731
313
gantt	64725
314
programmers	64722
315
todays	64720
316
moores	64716
317
fullscreen	64715
318
moveless	64708
319
reformatted	64704
320
deallocate	64704
321
laserdisc	64702
322
macos	64700
323
nonactive	64697
324
nonadjacent	64696
325
hotfix	64695
326
keylogger	64694
327
geotag	64691
328
oreilly	64681
329
exabit	64678
330
jailbroken	64677
331
fuzzer	64676
332
noninteractive	64673
333
multifactor	64672
334
letterspacing	64671
335
preinstall	64669
336
multiboot	64666
337
runescape	64665
338
micropayment	64664
339
numpad	64663
340
preinstalled	64661
341
jailbreaking	64660
342
attend	2158
343
withstand	1809
344
transpire	1116
345
reading	1110
346
texture	1065
347
capitalize	832
348
calling	779
349
unfold	767
350
starboard	679
351
commode	625
352
doing	594
353
textbook	499
354
unease	378
355
unpack	358
356
keycard	231
357
mainspring	207
358
grr	180
359
geocaching	167
360
microbus	160
361
mp3	147
362
svg	139
363
shifted	128
364
texted	127
365
towheaded	118
366
mineshaft	115
367
nonparty	95
368
crossbite	80
369
resignedness	69
370
msrp	61
371
inbreak	53
372
nanocomposite	44
373
md5	44
374
neomorphic	41
375
superstrain	28
376
lifers	27
377
multination	26
378
smartwatch	22
379
antilibration	22
380
zapf	20
381
mp4	20
1382
A src/main/resources/lexicons/fr.txt
Binary file
A src/main/resources/lexicons/he.txt
Binary file
A src/main/resources/lexicons/it.txt
Binary file
A src/main/resources/lexicons/ru.txt
Binary file
A src/main/resources/lexicons/zh.txt
Binary file
A src/test/java/com/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
A src/test/java/com/keenwrite/r/PluralizeTest.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.r;
3
4
import org.junit.jupiter.api.BeforeAll;
5
import org.junit.jupiter.api.Test;
6
7
import javax.script.ScriptEngine;
8
import javax.script.ScriptEngineManager;
9
import javax.script.ScriptException;
10
import java.util.Map;
11
12
import static java.lang.String.format;
13
import static java.util.Map.entry;
14
import static java.util.Map.ofEntries;
15
import static org.junit.jupiter.api.Assertions.assertEquals;
16
17
/**
18
 * Test that English pluralization rules produce expected values.
19
 */
20
public class PluralizeTest {
21
  private static final ScriptEngine ENGINE =
22
      (new ScriptEngineManager()).getEngineByName( "Renjin" );
23
24
  private static final Map<String, String> PLURAL_MAP = ofEntries(
25
      entry( "beef", "beefs" ),
26
      entry( "brother", "brothers" ),
27
      entry( "child", "children" ),
28
      entry( "cow", "cows" ),
29
      entry( "ephemeris", "ephemerides" ),
30
      entry( "genie", "genies" ),
31
      entry( "money", "moneys" ),
32
      entry( "mongoose", "mongooses" ),
33
      entry( "mythos", "mythoi" ),
34
      entry( "octopus", "octopuses" ),
35
      entry( "ox", "oxen" ),
36
      entry( "soliloquy", "soliloquies" ),
37
      entry( "trilby", "trilbys" ),
38
      entry( "wolf", "wolves" )
39
  );
40
41
  @BeforeAll
42
  static void setup() throws ScriptException {
43
    r( "setwd( 'R' );" );
44
    r( "source( 'pluralize.R' );" );
45
  }
46
47
  @Test
48
  @SuppressWarnings("UnnecessaryLocalVariable")
49
  public void test_Pluralize_SingularForms_PluralForms()
50
      throws ScriptException {
51
    for( final var key : PLURAL_MAP.keySet() ) {
52
      final var expectedSingular = key;
53
      final var expectedPlural = PLURAL_MAP.get( key );
54
      final var actualSingular = pluralize( key, 1 );
55
      final var actualPlural = pluralize( key, 2 );
56
57
      assertEquals( expectedSingular, actualSingular );
58
      assertEquals( expectedPlural, actualPlural );
59
    }
60
  }
61
62
  private String pluralize( final String word, final int count )
63
      throws ScriptException {
64
    return r( format( "pluralize( '%s', %d );", word, count ) ).toString();
65
  }
66
67
  private static Object r( final String code ) throws ScriptException {
68
    return ENGINE.eval( code );
69
  }
70
}
171
A src/test/java/com/keenwrite/tex/TeXRasterization.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.tex;
29
30
import com.whitemagicsoftware.tex.DefaultTeXFont;
31
import com.whitemagicsoftware.tex.TeXEnvironment;
32
import com.whitemagicsoftware.tex.TeXFormula;
33
import com.whitemagicsoftware.tex.TeXLayout;
34
import com.whitemagicsoftware.tex.graphics.AbstractGraphics2D;
35
import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
36
import com.whitemagicsoftware.tex.graphics.SvgGraphics2D;
37
import org.junit.jupiter.api.Test;
38
import org.xml.sax.SAXException;
39
40
import javax.imageio.ImageIO;
41
import javax.xml.parsers.DocumentBuilderFactory;
42
import javax.xml.parsers.ParserConfigurationException;
43
import java.awt.image.BufferedImage;
44
import java.io.ByteArrayInputStream;
45
import java.io.File;
46
import java.io.IOException;
47
import java.nio.file.Path;
48
49
import static com.keenwrite.preview.SvgRasterizer.*;
50
import static java.lang.System.getProperty;
51
import static org.junit.jupiter.api.Assertions.assertEquals;
52
53
/**
54
 * Test that TeX rasterization produces a readable image.
55
 */
56
public class TeXRasterization {
57
  private static final String LOAD_EXTERNAL_DTD =
58
      "http://apache.org/xml/features/nonvalidating/load-external-dtd";
59
60
  private static final String EQUATION =
61
      "G_{\\mu \\nu} = \\frac{8 \\pi G}{c^4} T_{{\\mu \\nu}}";
62
63
  private static final String DIR_TEMP = getProperty( "java.io.tmpdir" );
64
65
  private static final long FILESIZE = 12364;
66
67
  /**
68
   * Test that an equation can be converted to a raster image and the
69
   * final raster image size corresponds to the input equation. This is
70
   * a simple way to verify that the rasterization process is correct,
71
   * albeit if any aspect of the SVG algorithm changes (such as padding
72
   * around the equation), it will cause this test to fail, which is a bit
73
   * misleading.
74
   */
75
  @Test
76
  public void test_Rasterize_SimpleFormula_CorrectImageSize()
77
      throws IOException {
78
    final var g = new SvgGraphics2D();
79
    drawGraphics( g );
80
    verifyImage( rasterizeString( g.toString() ) );
81
  }
82
83
  /**
84
   * Test that an SVG document object model can be parsed and rasterized into
85
   * an image.
86
   */
87
  @Test
88
  public void getTest_SvgDomGraphics2D_InputElement_OutputRasterizedImage()
89
      throws ParserConfigurationException, IOException, SAXException {
90
    final var g = new SvgGraphics2D();
91
    drawGraphics( g );
92
93
    final var expectedSvg = g.toString();
94
    final var bytes = expectedSvg.getBytes();
95
96
    final var dbf = DocumentBuilderFactory.newInstance();
97
    dbf.setFeature( LOAD_EXTERNAL_DTD, false );
98
    dbf.setNamespaceAware( false );
99
    final var builder = dbf.newDocumentBuilder();
100
101
    final var doc = builder.parse( new ByteArrayInputStream( bytes ) );
102
    final var actualSvg = toSvg( doc.getDocumentElement() );
103
104
    verifyImage( rasterizeString( actualSvg ) );
105
  }
106
107
  /**
108
   * Test that an SVG image from a DOM element can be rasterized.
109
   *
110
   * @throws IOException Could not write the image.
111
   */
112
  @Test
113
  public void test_SvgDomGraphics2D_InputDom_OutputRasterizedImage()
114
      throws IOException {
115
    final var g = new SvgDomGraphics2D();
116
    drawGraphics( g );
117
118
    final var dom = g.toDom();
119
120
    verifyImage( rasterize( dom ) );
121
  }
122
123
  /**
124
   * Asserts that the given image matches an expected file size.
125
   *
126
   * @param image The image to check against the file size.
127
   * @throws IOException Could not write the image.
128
   */
129
  private void verifyImage( final BufferedImage image ) throws IOException {
130
    final var file = export( image, "dom.png" );
131
    assertEquals( FILESIZE, file.length() );
132
  }
133
134
  /**
135
   * Creates an SVG string for the default equation and font size.
136
   */
137
  private void drawGraphics( final AbstractGraphics2D g ) {
138
    final var size = 100f;
139
    final var texFont = new DefaultTeXFont( size );
140
    final var env = new TeXEnvironment( texFont );
141
    g.scale( size, size );
142
143
    final var formula = new TeXFormula( EQUATION );
144
    final var box = formula.createBox( env );
145
    final var layout = new TeXLayout( box, size );
146
147
    g.initialize( layout.getWidth(), layout.getHeight() );
148
    box.draw( g, layout.getX(), layout.getY() );
149
  }
150
151
  @SuppressWarnings("SameParameterValue")
152
  private File export( final BufferedImage image, final String filename )
153
      throws IOException {
154
    final var path = Path.of( DIR_TEMP, filename );
155
    final var file = path.toFile();
156
    ImageIO.write( image, "png", file );
157
    file.deleteOnExit();
158
    return file;
159
  }
160
}
1161
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
A testing/.gitignore
1
*.class
12
A testing/demo.sikuli/1594187265140.png
Binary file
A testing/demo.sikuli/1594592396134.png
Binary file
A testing/demo.sikuli/1594593710440.png
Binary file
A testing/demo.sikuli/1594593794335.png
Binary file
A testing/demo.sikuli/1594594984108.png
Binary file
A testing/demo.sikuli/1594689573764.png
Binary file
A testing/demo.sikuli/demo.py
1
# -----------------------------------------------------------------------------
2
# Copyright 2020 White Magic Software, Ltd.
3
#
4
# Permission is hereby granted, free of charge, to any person obtaining a
5
# copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
#
12
# The above copyright notice and this permission notice shall be included
13
# in all copies or substantial portions of the Software.
14
#
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# Runs all scripts
26
# -----------------------------------------------------------------------------
27
28
import s01
29
import s02
30
import s03
31
import s04
132
A testing/demo.sikuli/s01.py
1
# -----------------------------------------------------------------------------
2
# Copyright 2020 White Magic Software, Ltd.
3
#
4
# Permission is hereby granted, free of charge, to any person obtaining a
5
# copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
#
12
# The above copyright notice and this permission notice shall be included
13
# in all copies or substantial portions of the Software.
14
#
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# This script introduces the editor and its purpose.
26
# -----------------------------------------------------------------------------
27
from sikuli import *
28
import sys
29
30
if not "../editor.sikuli" in sys.path:
31
    sys.path.append( "../editor.sikuli" )
32
33
from editor import *
34
35
# ---------------------------------------------------------------
36
# Fresh start
37
# ---------------------------------------------------------------
38
rm( app_home + "/variables.yaml" )
39
rm( app_home + "/untitled.md" )
40
rm( dir_home + "/.scrivenvar" )
41
42
# ---------------------------------------------------------------
43
# Wait for application to launch
44
# ---------------------------------------------------------------
45
openApp( "java -jar " + app_bin )
46
47
wait("1594187265140.png", 30)
48
49
# Breathing room for video recording.
50
wait( 4 )
51
52
# ---------------------------------------------------------------
53
# Introduction
54
# ---------------------------------------------------------------
55
set_typing_speed( 240 )
56
57
heading( "What is this application?" )
58
typer( "Well, this application is a text editor that supports interpolated definitions, ")
59
typer( "a few different text formats, real-time preview, spell check ") 
60
typer( "as you tipe" ) 
61
wait( 0.5 )
62
recur( 3, backspace )
63
typer( "ype, and R statements." )
64
paragraph()
65
wait( 1 )
66
67
# ---------------------------------------------------------------
68
# Definition demo
69
# ---------------------------------------------------------------
70
heading( "What are definitions?" )
71
typer( "Watch. " )
72
wait( .5 )
73
74
# Focus the definition editor.
75
click_create()
76
recur( 4, tab )
77
78
wait( .5 )
79
rename_definition( "application" )
80
81
insert()
82
rename_definition( "title" )
83
84
insert()
85
rename_definition( "Scrivenvar" )
86
87
# Set focus to the text editor.
88
tab()
89
90
typer( "The left-hand pane contains a nested, folder-like structure of names " )
91
typer( "and values that are called *definitions*. " )
92
wait( .5 )
93
typer( "Such definitions can simplify updating documents. " )
94
wait( 1 )
95
96
edit_find( "this application" )
97
typer( "$application.title$" )
98
99
edit_find_next()
100
typer( "$application.title$" )
101
102
type( Key.END, Key.CTRL )
103
104
typer( "The right-hand pane shows the result after having substituted definition " )
105
typer( "values into the document." ) 
106
107
paragraph()
108
typer( "Now nobody wants to type definition names all the time. Instead, type any " )
109
typer( "partial definition value followed by `Ctrl+Space`, such as: scr" )
110
wait( 0.5 )
111
autoinsert()
112
wait( 1 )
113
typer( ". *Much* better!" )
114
paragraph()
115
116
heading( "What is interpolation?" )
117
typer( "Definition values can reference definition names. " )
118
wait( .5 )
119
typer( "The definition names act as placeholders. Substituting placeholders with " )
120
typer( "their definition value is called *interpolation*. Let's see how it works." )
121
wait( 2 )
1122
A testing/demo.sikuli/s02.py
1
# -----------------------------------------------------------------------------
2
# Copyright 2020 White Magic Software, Ltd.
3
#
4
# Permission is hereby granted, free of charge, to any person obtaining a
5
# copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
#
12
# The above copyright notice and this permission notice shall be included
13
# in all copies or substantial portions of the Software.
14
#
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# This script demonstrates how to use interpolated strings.
26
# -----------------------------------------------------------------------------
27
from sikuli import *
28
import sys
29
30
if not "../editor.sikuli" in sys.path:
31
    sys.path.append( "../editor.sikuli" )
32
33
from editor import *
34
35
# -----------------------------------------------------------------------------
36
# Open sample chapter.
37
# -----------------------------------------------------------------------------
38
file_open()
39
type( Key.UP, Key.ALT )
40
wait( 1 )
41
typer( Key.END )
42
wait( 1 )
43
enter()
44
wait( 0.5 )
45
enter()
46
wait( 1 )
47
48
# -----------------------------------------------------------------------------
49
# Open the corresponding definition file.
50
# -----------------------------------------------------------------------------
51
file_open()
52
recur( 2, down )
53
wait( 1 )
54
enter()
55
wait( 1 )
56
57
# -----------------------------------------------------------------------------
58
# Edit the sample document.
59
# -----------------------------------------------------------------------------
60
set_typing_speed( 80 )
61
62
type( Key.HOME, Key.CTRL )
63
recur( 2, down )
64
65
# Grey
66
recur( 3, skip_right )
67
autoinsert()
68
69
# 34
70
recur( 4, skip_right )
71
autoinsert()
72
73
# Central
74
recur( 10, skip_right )
75
autoinsert()
76
77
# London
78
skip_right()
79
autoinsert()
80
81
# Hatchery
82
skip_right()
83
autoinsert()
84
85
# and Conditioning
86
recur( 2, select_word_right )
87
delete()
88
89
# Centre
90
skip_right()
91
autoinsert()
92
93
set_typing_speed( 220 )
94
95
typer( " Let's interpolate those four definitions instead!" )
96
wait( 4 )
97
recur( 13, type, Key.BACKSPACE, Key.CTRL )
98
recur( 9, backspace )
99
100
set_typing_speed( 60 )
101
102
typer( "name$" )
103
wait( 2 )
104
105
# Collapse all definitions
106
tab()
107
recur( 8, typer, Key.LEFT )
108
109
# Expand to city
110
recur( 4, typer, Key.RIGHT )
111
112
# Jump to name
113
recur( 2, down )
114
recur( 2, typer, Key.RIGHT )
115
116
# Open the text field to show the full value
117
typer( Key.F2 )
118
119
# Traverse the text field
120
home()
121
recur( 16, type, Key.RIGHT, Key.CTRL )
122
esc()
123
124
restore_typing_speed()
125
126
tab()
127
type( Key.HOME, Key.CTRL )
128
edit_find( "Director" )
129
autoinsert()
130
131
edit_find_next()
132
autoinsert()
133
134
edit_find_next()
135
typer( Key.RIGHT )
136
recur( 2, delete )
137
autoinsert()
138
typer( "'s" )
139
140
edit_find( "Hatcheries" )
141
autoinsert()
142
143
# and Conditioning
144
recur( 2, select_word_right )
145
delete()
146
147
edit_find( "Central" )
148
autoinsert()
149
150
skip_right()
151
autoinsert()
152
153
typer( " How about a different city?" )
154
wait( 2 )
155
recur( 5, type, Key.BACKSPACE, Key.CTRL )
156
wait( 1 )
157
tab()
158
typer( Key.F2 )
159
typer( "Seattle" )
160
enter()
161
tab()
162
wait( 2 )
163
164
type( Key.END, Key.CTRL )
165
paragraph()
166
typer( "No?" )
167
paragraph()
168
169
tab()
170
typer( Key.F2 )
171
typer( "London" )
172
enter()
173
174
tab()
175
typer( "Organizing definitions is left to your ")
176
typer( "doub" )
177
autoinsert()
178
typer( " Good imagination." )
179
tab()
180
181
# Jump to "char" definition
182
home()
183
184
# Jump to "char.a.primary.name" definition
185
recur( 6, typer, Key.RIGHT )
186
187
# Jump to "char.a.primary.caste" definition
188
down()
189
typer( Key.RIGHT )
190
191
# Jump to root-level "caste" definition
192
recur( 7, down )
193
194
# Reselect "super"
195
recur( 5, typer, Key.RIGHT )
196
wait( 2 )
197
198
# Close the window, no save
199
type( "w", Key.CTRL )
200
wait( 0.5 )
201
tab()
202
wait( 0.5 )
203
typer( Key.SPACE )
204
wait( 1 )
1205
A testing/demo.sikuli/s03.py
1
# -----------------------------------------------------------------------------
2
# Copyright 2020 White Magic Software, Ltd.
3
#
4
# Permission is hereby granted, free of charge, to any person obtaining a
5
# copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
#
12
# The above copyright notice and this permission notice shall be included
13
# in all copies or substantial portions of the Software.
14
#
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# This script introduces images and R.
26
# -----------------------------------------------------------------------------
27
from sikuli import *
28
import sys
29
30
if not "../editor.sikuli" in sys.path:
31
    sys.path.append( "../editor.sikuli" )
32
33
from editor import *
34
35
set_typing_speed( 80 )
36
37
file_open()
38
type( Key.UP, Key.ALT )
39
wait( 0.5 )
40
home()
41
wait( 0.25 )
42
enter()
43
wait( 1 )
44
end()
45
wait( 0.25 )
46
enter()
47
wait( 1 )
48
49
set_typing_speed( 200 )
50
51
paragraph()
52
heading( "What text formats are supported?" )
53
54
typer( "Scr" )
55
autoinsert()
56
typer( " supports Markdown, R Markdown, XML, and R XML; however, the software " )
57
typer( "architecture enables it to easily add new formats. The following figure " )
58
typer( "depicts the overall architecture: " )
59
paragraph()
60
typer( "![](../writing/images/architecture)" )
61
paragraph()
62
typer( "Many text editors can only open one type of plain text markup format that is " )
63
typer( "only output as HTML. With a little more effort, text editors could support " )
64
typer( "multiple input and output formats. Scr" )
65
autoinsert()
66
typer( " does so and goes one step further by introducing interpolated definitions." )
67
paragraph()
68
typer( "Kitten interlude:" )
69
paragraph()
70
typer( "![](https://i.imgur.com/jboueQH.jpg)" )
71
paragraph()
72
73
heading( "What is R?" )
74
typer( "R is a programming language. You might have noticed a few potential grammar " )
75
typer( "problems with direct substitution. Rules for possessive forms, numbers, and " )
76
typer( "other quirks can be tackled using R." )
77
78
# -----------------------------------------------------------------------------
79
# Demo bootstrapping
80
# -----------------------------------------------------------------------------
81
82
# Jump to the end
83
type( Key.END, Key.CTRL )
84
paragraph()
85
86
set_typing_speed( 300 )
87
heading( "How is R used?" )
88
typer( "R must be instructed where to find script files and what ones to load. The " )
89
typer( "*working directory* is the full path to those R files; the *startup script* " )
90
typer( "defines what R files to load. Both preferences must be changed before prose " )
91
typer( "may be processed. Preferences can be opened using either the " )
92
typeln( "**Edit > Preferences** menu or by pressing `Ctrl+Alt+s`. Here goes!" ) 
93
wait( 2 )
94
95
# -----------------------------------------------------------------------------
96
# Select the R script directory
97
# -----------------------------------------------------------------------------
98
99
# Change the working directory by clicking "Browse"
100
type( "s", Key.CTRL + Key.ALT )
101
wait("1594592396134.png", 1)
102
click("1594592396134.png")
103
wait( 0.5 )
104
105
# Navigate to and select the "r" directory
106
type( Key.UP, Key.ALT )
107
wait( 0.5 )
108
end()
109
wait( 0.5 )
110
enter()
111
wait( 0.5 )
112
end()
113
wait( 0.5 )
114
type( Key.UP )
115
wait( 0.5 )
116
recur( 2, tab )
117
wait( 0.5 )
118
enter()
119
wait( 1 )
120
121
# -----------------------------------------------------------------------------
122
# Set the R startup script instructions
123
# -----------------------------------------------------------------------------
124
125
wait("1594593710440.png", 5)
126
click("1594593710440.png")
127
128
set_typing_speed( 440 )
129
130
typeln( "setwd( '$application.r.working.directory$' )" )
131
typeln( "assign( 'anchor', '$date.anchor$', envir = .GlobalEnv )" )
132
typeln( "source( 'pluralize.R' )" )
133
typeln( "source( 'possessive.R' )" )
134
typeln( "source( 'conversion.R' )" )
135
typeln( "source( 'csv.R' )" )
136
137
wait("1594593794335.png", 3)
138
click("1594593794335.png")
139
140
paragraph()
141
set_typing_speed( 220 )
142
143
typer( "R is now configured. The startup script and other R " )
144
typer( "files can be found in the " )
145
typer( "[repository](https://github.com/DaveJarvis/scrivenvar/tree/master/R). " )
146
wait( 1.5 )
147
148
# Wait for the browser to appear.
149
wait("1594594984108.png", 5)
150
click("1594594984108.png")
151
152
wait( 5 )
153
click("1594689573764.png")
154
155
paragraph()
156
typer( "Next, we'll see how definitions and R can work together." )
157
wait( 2 )
1158
A testing/demo.sikuli/s04.py
1
# -----------------------------------------------------------------------------
2
# Copyright 2020 White Magic Software, Ltd.
3
#
4
# Permission is hereby granted, free of charge, to any person obtaining a
5
# copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
#
12
# The above copyright notice and this permission notice shall be included
13
# in all copies or substantial portions of the Software.
14
#
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# This script demonstrates using R.
26
# -----------------------------------------------------------------------------
27
from sikuli import *
28
import sys
29
30
if not "../editor.sikuli" in sys.path:
31
    sys.path.append( "../editor.sikuli" )
32
33
from editor import *
34
35
set_typing_speed( 220 )
36
37
# -----------------------------------------------------------------------------
38
# Open the demo text.
39
# -----------------------------------------------------------------------------
40
file_open()
41
type( Key.UP, Key.ALT )
42
wait( 0.5 )
43
end()
44
wait( 0.5 )
45
enter()
46
wait( 0.5 )
47
down()
48
wait( 0.5 )
49
enter()
50
wait( 2 )
51
52
# -----------------------------------------------------------------------------
53
# Re-open the corresponding definition file.
54
# -----------------------------------------------------------------------------
55
file_open()
56
recur( 2, down )
57
wait( 1 )
58
enter()
59
wait( 2 )
60
61
# -----------------------------------------------------------------------------
62
# Brief introduction to R
63
# -----------------------------------------------------------------------------
64
type( Key.HOME, Key.CTRL )
65
end()
66
paragraph()
67
68
typer( "## Using R" )
69
paragraph()
70
typer( "Insert R code into documents as follows: `r# 1+1`. " )
71
wait( 1.5 )
72
typer( "Notice how the right-hand pane shows the computed result. I'll wait. " )
73
wait( 3 )
74
typer( "The syntax is: open backtick, r#, *computable expression*, close " )
75
typer( "backtick. That expression can be any valid R statement. The status bar " ) 
76
typer( "will provide clues when an R expression cannot be computed by the " )
77
typer( "editor. `r# glitch`" )
78
wait( 4 )
79
recur( 11, backspace )
80
typer( "Let's swap 34 storeys for a definition value and replace the number " )
81
typer( "according to the Chicago Manual of Style (cms) rules." )
82
83
# -----------------------------------------------------------------------------
84
# Demo pluralization
85
# -----------------------------------------------------------------------------
86
set_typing_speed( 80 )
87
88
edit_find( "34" )
89
autoinsert()
90
91
edit_find( "x(" )
92
typer( "cms(" )
93
94
edit_find( "storeys." )
95
typer( "34." )
96
autoinsert()
97
edit_find( "x(" )
98
typer( "pl( 'storey'," )
99
wait( 4 )
100
101
tab()
102
rename_definition( "1" )
103
wait( 4 )
104
rename_definition( "142" )
105
wait( 4 )
106
rename_definition( "34" )
107
wait( 4 )
108
tab()
109
110
# -----------------------------------------------------------------------------
111
# Demo possessives (it, her, his, Director)
112
# -----------------------------------------------------------------------------
113
type( Key.HOME, Key.CTRL )
114
edit_find( "Director" )
115
autoinsert()
116
edit_find_next()
117
autoinsert()
118
edit_find_next()
119
autoinsert()
120
type( Key.RIGHT )
121
recur( 2, delete )
122
autoinsert()
123
home()
124
edit_find( "x(" )
125
typer( "pos(" )
126
wait( 2 )
127
128
tab()
129
rename_definition( "Headmistress" )
130
wait( 4 )
131
rename_definition( "Director" )
132
wait( 2 )
133
tab()
134
135
type( Key.END, Key.CTRL )
136
paragraph()
137
typer( "Other possessives: `r# pos( 'it' )`, `r# pos( 'her' )`, `r# pos( 'his' )`, " )
138
typer( "and `r# pos( 'my' )`." )
139
140
# -----------------------------------------------------------------------------
141
# Demo conversion, including ordinal numbers
142
# -----------------------------------------------------------------------------
143
set_typing_speed( 160 )
144
145
paragraph()
146
heading( "Date Conversions" )
147
typer( "Mixing R code with definitions invites endless possibilities. " )
148
typer( "Imagine someone racing to the " ) 
149
typer( "`r#cms( v$location$breeder$storeys, ordinal=TRUE )` floor, whereby that " )
150
typer( "ordinal stems from the Hatchery's storeys' definition. Or how about " )
151
typer( "a complex timeline where dates are expressed in days relative to one " )
152
typer( "point in time. Let's call this the *anchor date* and define it." )
153
154
tab()
155
home()
156
typer( Key.SPACE )
157
insert()
158
rename_definition( "date" )
159
insert()
160
rename_definition( "anchor" )
161
insert()
162
rename_definition( "1969-10-29" )
163
tab()
164
165
paragraph()
166
typer( "Next, set an R variable named `now` to the current date" )
167
typer( "`r# now = format( Sys.time(), '%Y-%m-%d' ); ''`--- the empty single quotes " )
168
typer( "prevent the date from appearing in the output document. " )
169
170
paragraph()
171
typer( "We set the anchor date to `r# annal()`, which was " )
172
typer( "`r# elapsed( 0, days( v$date$anchor, format( Sys.time(), '%Y-%m-%d' ) ) )` " )
173
typer( "ago from `r# format( as.Date( now ), '%B %d, %Y' )`. " )
174
175
# -----------------------------------------------------------------------------
176
# Demo CSV file import
177
# -----------------------------------------------------------------------------
178
paragraph()
179
heading( "Tabular Data" )
180
typer( "The following table shows average Canadian lifespans by birth " )
181
typer( "year and sex:" )
182
paragraph()
183
typer( "`r# csv2md( '../data.csv', total=FALSE )`" )
184
paragraph()
185
typer( "Calling `csv2md` converts the comma-separated values in the spreadsheet " )
186
typer( "to a table formatted using Markdown. The HTML preview pane changes the " )
187
typer( "appearance of the resulting table. Using `../data.csv` instructs R to " )
188
typer( "open `data.csv` from one directory above the *working directory*." )
189
190
# -----------------------------------------------------------------------------
191
# Demo HTML export
192
# -----------------------------------------------------------------------------
193
paragraph()
194
heading( "Export" )
195
typer( "Retrieve the output HTML by using the **Edit > Copy HTML** menu. Let's " )
196
typer( "peek at the output." )
197
wait( 2 )
198
199
type( "e", Key.ALT )
200
wait( 0.5 )
201
down()
202
wait( 0.25 )
203
enter()
204
wait( 0.25 )
205
206
type( "a", Key.CTRL )
207
wait( 0.25 )
208
type( "v", Key.CTRL )
209
wait( 5 )
210
211
set_typing_speed( 40 )
212
213
# Jump to page bottom (should already be there, but just in case)
214
type( Key.END, Key.CTRL )
215
recur( 3, typer, Key.PAGE_UP )
216
type( Key.HOME, Key.CTRL )
217
wait( 3 )
218
219
set_typing_speed( 220 )
220
type( "z", Key.CTRL )
221
type( Key.END, Key.CTRL )
222
223
paragraph()
224
typer( "That's all for now, thank you!" )
225
wait( 5 )
226
227
# Delete the anchor date.
228
tab()
229
end()
230
recur( 2, type, Key.UP )
231
delete()
232
tab()
1233
A testing/demo.sikuli/test.py
1
from sikuli import *
2
3
import sys
4
import os
5
6
def set_class_path():
7
    path_script = getBundlePath()
8
    dir_script = os.path.dirname( path_script )
9
    path_lib = dir_script + "/keycast/build/libs/keycast.jar"
10
    
11
    sys.path.append( path_lib )
12
13
def launch():
14
    from com.whitemagicsoftware.keycast import KeyCast
15
    kc = KeyCast()
16
    kc.show()
17
18
def main():
19
    set_class_path()
20
    launch()
21
   
22
23
if __name__ == "__main__":
24
    main()
125
A testing/editor.sikuli/1594187923258.png
Binary file
A testing/editor.sikuli/editor.py
1
# -----------------------------------------------------------------------------
2
# Copyright 2020 White Magic Software, Ltd.
3
#
4
# Permission is hereby granted, free of charge, to any person obtaining a
5
# copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
#
12
# The above copyright notice and this permission notice shall be included
13
# in all copies or substantial portions of the Software.
14
#
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# This script contains helper functions used by the other scripts.
26
#
27
# Do not run this script.
28
# -----------------------------------------------------------------------------
29
30
from sikuli import *
31
import sys
32
import os
33
from os.path import expanduser
34
35
dir_home = expanduser( "~" )
36
app_home = dir_home + "/bin"
37
app_bin = app_home + "/scrivenvar.jar"
38
39
wpm_typing_speed = 80
40
41
# -----------------------------------------------------------------------------
42
# Try to delete the file pointed to by the path variable. If there is no such
43
# file, this will silently ignore the exception.
44
# -----------------------------------------------------------------------------
45
def rm( path ):
46
    try:
47
        os.remove( path )
48
    except:
49
        print "Ignored"
50
51
# -----------------------------------------------------------------------------
52
# Changes the current typing speed, where speed is given in words per minute.
53
# -----------------------------------------------------------------------------
54
def set_typing_speed( wpm ):
55
    global wpm_typing_speed
56
    wpm_typing_speed = wpm
57
58
# -----------------------------------------------------------------------------
59
# Creates a delay between keystrokes to emulate typing at a particular speed.
60
# -----------------------------------------------------------------------------
61
def random_wait():
62
    from time import sleep
63
    from random import uniform
64
    cpm = wpm_typing_speed * 5.1
65
    cps = cpm / 60.0
66
    ms_per_char = 1000.0 / cps
67
    ms_per_stroke = ms_per_char / 2.0
68
69
    noise = uniform( 0, ms_per_stroke / 2 )
70
    duration = (ms_per_stroke + noise ) / 1000
71
    
72
    sleep( duration )
73
74
# -----------------------------------------------------------------------------
75
# Repeats a function call, f, n times.
76
# -----------------------------------------------------------------------------
77
def recur( n, f, *args ):
78
    for i in range( n ):
79
        f( *args )
80
        random_wait()
81
82
# -----------------------------------------------------------------------------
83
# Emulate a typist who is typing in the given text.
84
# -----------------------------------------------------------------------------
85
def typer( text ):
86
    for c in text:
87
        type( c )
88
        random_wait()
89
90
# -----------------------------------------------------------------------------
91
# Type a line of text followed by typing the ENTER key.
92
# -----------------------------------------------------------------------------
93
def typeln( text ):
94
    typer( text )
95
    enter()
96
97
# -----------------------------------------------------------------------------
98
# Injects a definition.
99
# -----------------------------------------------------------------------------
100
def autoinsert():
101
    type( Key.SPACE, Key.CTRL )
102
    random_wait()
103
104
# -----------------------------------------------------------------------------
105
# Types the TAB key.
106
# -----------------------------------------------------------------------------
107
def tab():
108
    typer( Key.TAB )
109
110
# -----------------------------------------------------------------------------
111
# Types the ENTER key.
112
# -----------------------------------------------------------------------------
113
def enter():
114
    typer( Key.ENTER )
115
116
# -----------------------------------------------------------------------------
117
# Types the ESC key.
118
# -----------------------------------------------------------------------------
119
def esc():
120
    typer( Key.ESC )
121
122
# -----------------------------------------------------------------------------
123
# Types the DOWN arrow key.
124
# -----------------------------------------------------------------------------
125
def down():
126
    typer( Key.DOWN )
127
128
# -----------------------------------------------------------------------------
129
# Types the HOME key.
130
# -----------------------------------------------------------------------------
131
def home():
132
    typer( Key.HOME )
133
134
# -----------------------------------------------------------------------------
135
# Types the END key.
136
# -----------------------------------------------------------------------------
137
def end():
138
    typer( Key.END )
139
140
# -----------------------------------------------------------------------------
141
# Types the BACKSPACE key.
142
# -----------------------------------------------------------------------------
143
def backspace():
144
    typer( Key.BACKSPACE )
145
146
# -----------------------------------------------------------------------------
147
# Types the INSERT key, often to insert a new definition.
148
# -----------------------------------------------------------------------------
149
def insert():
150
    typer( Key.INSERT )
151
152
# -----------------------------------------------------------------------------
153
# Types the DELETE key, often to remove selected text.
154
# -----------------------------------------------------------------------------
155
def delete():
156
    typer( Key.DELETE )
157
158
# -----------------------------------------------------------------------------
159
# Moves the cursor one word to the right.
160
# -----------------------------------------------------------------------------
161
def skip_right():
162
    type( Key.RIGHT, Key.CTRL )
163
    random_wait()
164
165
def select_word_right():
166
    type( Key.RIGHT, Key.CTRL + Key.SHIFT )
167
    random_wait()
168
169
# -----------------------------------------------------------------------------
170
# Types ENTER twice to begin a new paragraph.
171
# -----------------------------------------------------------------------------
172
def paragraph():
173
    recur( 2, enter )
174
    wait( 1.5 )
175
176
# -----------------------------------------------------------------------------
177
# Writes a heading to the document using the given text value as the content.
178
# -----------------------------------------------------------------------------
179
def heading( text ):
180
    typer( "# " + text )
181
    paragraph()
182
183
# -----------------------------------------------------------------------------
184
# Clicks the "Create" button to add a new definition.
185
# -----------------------------------------------------------------------------
186
def click_create():
187
    click("1594187923258.png")
188
    wait( .5 )
189
190
# -----------------------------------------------------------------------------
191
# Changes the text for the actively selected definition.
192
# -----------------------------------------------------------------------------
193
def rename_definition( text ):
194
    typer( Key.F2 )
195
    typer( text )
196
    enter()
197
    wait( .5 )
198
199
# -----------------------------------------------------------------------------
200
# Searches for the given text within the document.
201
# -----------------------------------------------------------------------------
202
def edit_find( text ):
203
    type( "f", Key.CTRL )
204
    typer( text )
205
    enter()
206
    wait( .25 )
207
    esc()
208
    wait( .5 )
209
210
# -----------------------------------------------------------------------------
211
# Searches for the next occurrence of the previous search term.
212
# -----------------------------------------------------------------------------
213
def edit_find_next():
214
    typer( Key.F3 )
215
    wait( .5 )
216
217
# -----------------------------------------------------------------------------
218
# Opens a dialog for selecting a file.
219
# -----------------------------------------------------------------------------
220
def file_open():
221
    type( "o", Key.CTRL )
222
    wait( 1 )
1223
A video/.gitignore
1
*.avi
2
*.wav
3
*.png
4
*.mp4
5
*.mp3
6
17
A video/title.blend
Binary file
A video/traced-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
   width="211.87125mm"
11
   height="56.576mm"
12
   viewBox="0 0 211.87125 56.576"
13
   version="1.1"
14
   id="svg8"
15
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
16
   sodipodi:docname="traced-text.svg">
17
  <defs
18
     id="defs2" />
19
  <sodipodi:namedview
20
     id="base"
21
     pagecolor="#ffffff"
22
     bordercolor="#666666"
23
     borderopacity="1.0"
24
     inkscape:pageopacity="0.0"
25
     inkscape:pageshadow="2"
26
     inkscape:zoom="1.4142136"
27
     inkscape:cx="367.6429"
28
     inkscape:cy="129.23348"
29
     inkscape:document-units="mm"
30
     inkscape:current-layer="layer1"
31
     inkscape:document-rotation="0"
32
     showgrid="false"
33
     fit-margin-top="10"
34
     fit-margin-left="10"
35
     fit-margin-right="10"
36
     fit-margin-bottom="10" />
37
  <metadata
38
     id="metadata5">
39
    <rdf:RDF>
40
      <cc:Work
41
         rdf:about="">
42
        <dc:format>image/svg+xml</dc:format>
43
        <dc:type
44
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
45
        <dc:title></dc:title>
46
      </cc:Work>
47
    </rdf:RDF>
48
  </metadata>
49
  <g
50
     inkscape:label="Layer 1"
51
     inkscape:groupmode="layer"
52
     id="layer1"
53
     transform="translate(-1.4263456,-106.05539)">
54
    <text
55
       xml:space="preserve"
56
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;line-height:1.25;font-family:'Alex Brush';-inkscape-font-specification:'Alex Brush, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
57
       x="12.289946"
58
       y="147.80539"
59
       id="text835"><tspan
60
         sodipodi:role="line"
61
         id="tspan833"
62
         x="12.289946"
63
         y="147.80539"
64
         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Alex Brush';-inkscape-font-specification:'Alex Brush, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583">Scrivenvar</tspan></text>
65
    <path
66
       sodipodi:nodetypes="cssssc"
67
       id="path859"
68
       d="m 47.37594,126.25759 c 5.878995,0.58684 8.108819,-2.8906 6.991897,-5.39049 -4.163299,-9.31827 -26.104298,-1.57165 -26.47428,4.67958 -0.290066,4.90098 4.329286,5.69691 9.138161,6.81221 4.75698,1.10326 9.980125,1.72503 10.138085,4.5281 0.511551,9.07772 -11.28247,13.50974 -21.577969,13.14767"
69
       style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#4eb059;stroke-width:0.132292;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
70
    <path
71
       sodipodi:nodetypes="cssc"
72
       id="path861"
73
       d="m 61.538159,137.91416 c 8.229745,-12.05206 -9.227635,-1.22793 -10.272792,5.40306 -0.929347,5.89623 4.566953,5.63307 9.024721,2.11036 5.095939,-4.02702 8.706628,-8.11599 12.031905,-13.9409"
74
       style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#4eb059;stroke-width:0.132292;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
75
    <path
76
       sodipodi:nodetypes="ccssc"
77
       id="path863"
78
       d="m 72.321991,131.48668 c 3.834665,-5.91801 -1.131419,0.83402 0.75311,2.48796 2.189872,1.94816 6.580549,-2.11016 5.400159,-0.72958 -0.854851,0.99983 -9.857527,10.41157 -5.126492,13.80621 2.461609,1.76627 8.936925,-2.58857 11.751532,-5.5313"
79
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
80
    <path
81
       sodipodi:nodetypes="csssc"
82
       id="path963"
83
       d="m 85.1003,141.51997 c 0,0 6.754775,-9.24626 6.743495,-8.01563 -0.01328,1.44899 -5.040946,6.68411 -6.63123,10.08427 -0.90584,1.93677 -0.626402,4.68995 2.447111,4.25184 1.468017,-0.20926 5.212094,-2.44913 10.029682,-7.66684"
84
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
85
    <path
86
       sodipodi:nodetypes="csccc"
87
       id="path965"
88
       d="m 97.689357,140.17361 c 0,0 3.797813,-8.42805 4.594353,-7.95573 0.58723,0.34822 -6.526154,13.32545 -5.477472,14.50806 2.435753,1.7862 19.064212,-11.51107 15.563042,-16.73913 -0.73409,-1.34256 -3.18033,-1.99148 -3.18033,-1.99148"
89
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
90
    <path
91
       sodipodi:nodetypes="csssc"
92
       d="m 113.37707,141.34636 c 4.23091,0.29831 11.94363,-4.90618 10.94354,-7.7799 -1.29105,-3.70978 -8.05529,1.78774 -9.69006,3.68511 -4.97668,5.77609 -4.11733,10.31478 -0.92228,10.61275 3.436,0.32045 8.83724,-3.13085 13.69698,-9.62574"
93
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
94
       id="path967" />
95
    <path
96
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
97
       d="m 146.49943,140.17361 c 0,0 3.79781,-8.42805 4.59435,-7.95573 0.58723,0.34822 -6.52616,13.32545 -5.47747,14.50806 2.43575,1.7862 19.06421,-11.51107 15.56304,-16.73913 -0.73409,-1.34256 -3.10123,-1.96263 -3.10123,-1.96263"
98
       id="path970"
99
       sodipodi:nodetypes="csccc" />
100
    <path
101
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
102
       d="m 188.80833,131.36316 c 3.83466,-5.91801 -1.13142,0.83402 0.75311,2.48796 2.18987,1.94816 6.58055,-2.11016 5.40016,-0.72958 -0.85485,0.99983 -9.98962,10.60367 -5.12649,13.80621 2.8329,1.86556 9.63808,-2.25455 13.61435,-8.05051"
103
       id="path987"
104
       sodipodi:nodetypes="ccssc" />
105
    <path
106
       sodipodi:nodetypes="ccsssccc"
107
       d="m 127.40525,138.23858 c 1.53961,-1.23511 5.06979,-6.4876 5.94375,-5.82833 -1.7832,2.5949 -8.95273,13.68991 -7.1105,13.94503 1.19011,0.16482 7.25976,-8.00422 10.87675,-10.901 1.83151,-1.46682 4.35069,-3.49971 5.94917,-3.73267 1.66376,-0.24247 -1.93803,2.90472 -3.80099,5.77097 -1.36327,2.14988 -4.92421,8.02816 -2.69839,9.35481 3.0826,1.21137 7.35116,-4.27566 9.93439,-6.67382"
108
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
109
       id="path989" />
110
    <path
111
       sodipodi:nodetypes="csscsc"
112
       id="path992"
113
       d="m 176.85645,132.78853 c -3.26879,-6.24001 -16.43513,7.99373 -16.14879,12.14556 0.1378,1.99804 2.16776,3.14653 3.8818,2.44798 4.44909,-1.8132 11.93103,-13.58278 13.4413,-14.18515 -6.97685,9.84354 -7.04537,13.29844 -4.02229,13.83262 2.49715,0.44125 8.94275,-6.11484 14.79986,-15.66638"
114
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
115
  </g>
116
</svg>
1117