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
# BINARY FILES:
10
#   Disable line ending normalize on checkin.
11
12
*.blend binary
13
14
*.bin binary
15
*.bmp binary
16
*.eps binary
17
*.exe binary
18
*.gif binary
19
*.ico binary
20
*.jar binary
21
*.jpg binary
22
*.mng binary
23
*.png binary
24
*.zip binary
25
*.otf binary
26
*.ttf binary
27
*.ods binary
28
29
bin/linux-x64.warp-packer binary
30
bin/osslsigncode binary
31
bin/warp-packer binary
32
scripts/rcedit-x64.exe binary
33
134
A .gitignore
1
dist
2
*.bin
3
*.exe
4
*.app
5
/*.jar
6
build
7
.gradle
8
video
9
.settings
10
.classpath
11
.idea
12
count/
13
themes/
14
quotes/
15
!src/main/java/com/keenwrite/processors/markdown/extensions/quotes
16
tex/
17
spell/
18
keenwrite.build_artifacts.txt
19
todo
20
tokens/
121
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
* [JDK 23](https://bell-sw.com/pages/downloads) (Full JDK + JavaFX)
10
* [Gradle 8.10.2](https://gradle.org/releases)
11
* [Git 2.46.2](https://git-scm.com/downloads)
12
* [warp v0.4.0-alpha](https://github.com/Reisz/warp/releases/tag/v0.4.0)
13
14
Note: The forked warp packer release fixes a bug in the main branch.
15
16
## Repository
17
18
Clone the repository as follows:
19
20
    git clone https://gitlab.com/DaveJarvis/KeenWrite.git keenwrite
21
22
The repository is cloned.
23
24
# Build
25
26
Build the application überjar as follows:
27
28
    cd keenwrite
29
    gradle clean jar
30
31
The application is built.
32
33
# Run
34
35
After the application is compiled, run it using `keenwrite.sh`.
36
37
# Integrated development environments
38
39
This section describes setup instructions to import and run the application
40
using an integrated development environment (IDE). Running the application
41
should trigger a build.
42
43
## IntelliJ IDEA
44
45
This section describes how to build and run the application using
46
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 within the IDE as follows:
64
65
1. Open **Launcher.java**.
66
1. Click **Run → Launcher**.
67
68
The application is started.
69
70
# Installers
71
72
This section describes how to set up the development environment and build
73
native executables for supported operating systems.
74
75
## Setup
76
77
Follow these one-time setup instructions to begin:
78
79
1. Ensure `$HOME/bin` is set in the `PATH` environment variable.
80
1. Copy `build-template` into `$HOME/bin`.
81
82
Setup is complete.
83
84
## Binaries
85
86
Run the `installer.sh` script to build platform-specific binaries, such as:
87
88
    ./installer.sh -V -o linux
89
90
The `installer.sh` script:
91
92
* downloads a JDK;
93
* generates a run script;
94
* bundles the JDK, run script, and JAR file; and
95
* creates a standalone binary, so no installation required.
96
97
Run `./installer.sh -h` to see all command-line options.
98
99
# Releases
100
101
After installing `scripts/build-template`, build release binaries as follows:
102
103
    git tag -a 4.0.0 -m "Release name"
104
    git push origin --tags
105
    ./release.sh
106
107
When finished, browse to the project releases page to draft a new release.
108
109
# Versioning
110
111
Version numbers are read directly from Git using a plugin. The version
112
number is written to `app.properties` in the `resources` directory. The
113
application reads that file to display version information upon start.
114
1115
A LICENSE.md
1
# License
2
3
Copyright 2023 White Magic Software, Ltd.
4
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
11
  notice, this list of conditions and the following disclaimer.
12
13
* Redistributions in binary form must reproduce the above copyright
14
  notice, this list of conditions and the following disclaimer in the
15
  documentation and/or other materials provided with the distribution.
16
17
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
128
A R/bootstrap.R
1
setwd( v$application$r$working$directory )
2
3
# To reference additional R variables in documents, define them such as:
4
# assign( "variable", v$key$name, envir = .GlobalEnv )
5
6
source( "pluralize.R" )
7
source( "possessive.R" )
8
source( "conversion.R" )
9
source( "csv.R" )
10
111
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
  gsub( " 0", " ", 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 will translate numbers greater than one by truncating to nearest
87
# thousandth, millionth, billionth, etc. regardless of ordinal. If ordinal
88
# is TRUE, this will return the ordinal name. This will not produce ordinals
89
# for numbers greater than 100.
90
#
91
# If scaled is TRUE, this will write large numbers as comma-separated values.
92
# -----------------------------------------------------------------------------
93
cms <- function( n, ordinal = FALSE, scaled = TRUE ) {
94
  n <- x( n )
95
96
  if( n == 0 ) {
97
    if( ordinal ) {
98
      return( "zeroth" )
99
    }
100
101
    return( "zero" )
102
  }
103
104
  # Concatenate this a little later.
105
  if( n < 0 ) {
106
    result = "negative "
107
    n = abs( n )
108
  }
109
110
  if( n > 999 && scaled ) {
111
    scales <- c(
112
      "thousand", "million", "billion", "trillion", "quadrillion",
113
      "quintillion", "sextillion", "septillion", "octillion", "nonillion",
114
      "decillion", "undecillion", "duodecillion", "tredecillion",
115
      "quattuordecillion", "quindecillion", "sexdecillion", "septendecillion",
116
      "octodecillion", "novemdecillion", "vigintillion", "centillion",
117
      "quadrillion", "quitillion", "sextillion"
118
    );
119
120
    d <- round( n / (10 ^ (log10( n ) - log10( n ) %% 3)) );
121
    n <- floor( log10( n ) ) / 3;
122
    return( paste( cms( d ), scales[ n ] ) );
123
  }
124
125
  # Do not spell out numbers greater than one hundred.
126
  if( n > 100 ) {
127
    # Comma-separated numbers.
128
    return( commas( n ) )
129
  }
130
131
  # Don't go beyond 100.
132
  if( n == 100 ) {
133
    if( ordinal ) {
134
      return( "one hundredth" )
135
    }
136
137
    return( "one hundred" )
138
  }
139
140
  # Samuel Langhorne Clemens noted English has too many exceptions.
141
  small = c(
142
    "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
143
    "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
144
    "seventeen", "eighteen", "nineteen"
145
  )
146
147
  ord_small = c(
148
    "first", "second", "third", "fourth", "fifth", "sixth", "seventh",
149
    "eighth", "ninth", "tenth", "eleventh", "twelfth", "thirteenth",
150
    "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth",
151
    "nineteenth", "twentieth"
152
  )
153
154
  # After this, the number (n) is between 20 and 99.
155
  if( n < 20 ) {
156
    if( ordinal ) {
157
      return( .subset( ord_small, n %% 100 ) )
158
    }
159
160
    return( .subset( small, n %% 100 ) )
161
  }
162
163
  tens = c( "",
164
    "twenty", "thirty", "forty", "fifty",
165
    "sixty", "seventy", "eighty", "ninety"
166
  )
167
168
  ord_tens = c( "",
169
    "twentieth", "thirtieth", "fortieth", "fiftieth",
170
    "sixtieth", "seventieth", "eightieth", "ninetieth"
171
  )
172
173
  ones_index = n %% 10
174
  n = n %/% 10
175
176
  # No number in the ones column, so the number must be a multiple of ten.
177
  if( ones_index == 0 ) {
178
    if( ordinal ) {
179
      return( .subset( ord_tens, n ) )
180
    }
181
182
    return( .subset( tens, n ) )
183
  }
184
185
  # Find the value from the ones column.
186
  if( ordinal ) {
187
    unit_1 = .subset( ord_small, ones_index )
188
  }
189
  else {
190
    unit_1 = .subset( small, ones_index )
191
  }
192
193
  # Find the tens column.
194
  unit_10 = .subset( tens, n )
195
196
  # Hyphenate the tens and the ones together.
197
  concat( unit_10, concat( "-", unit_1 ) )
198
}
199
200
# -----------------------------------------------------------------------------
201
# Returns a number as a comma-delimited string. This is a work-around
202
# until Renjin fixes https://github.com/bedatadriven/renjin/issues/338
203
# -----------------------------------------------------------------------------
204
commas <- function( n ) {
205
  n <- x( n )
206
207
  s <- sprintf( "%03.0f", n %% 1000 )
208
  n <- n %/% 1000
209
210
  while( n > 0 ) {
211
    s <- concat( sprintf( "%03.0f", n %% 1000 ), ',', s )
212
    n <- n %/% 1000
213
  }
214
215
  gsub( "^0*", '', s )
216
}
217
218
# -----------------------------------------------------------------------------
219
# Returns a human-readable string that provides the elapsed time between
220
# two numbers in terms of years, months, and days. If any unit value is zero,
221
# the unit is not included. The words (year, month, day) are pluralized
222
# according to English grammar. The numbers are written out according to
223
# Chicago Manual of Style. This applies the serial comma.
224
#
225
# Both numbers are offsets relative to the anchor date.
226
#
227
# If all unit values are zero, this returns s ("same day" by default).
228
#
229
# If the start date (began) is greater than end date (ended), the dates are
230
# swapped before calculations are performed. This allows any two dates
231
# to be compared and positive unit values are always returned.
232
# -----------------------------------------------------------------------------
233
elapsed <- function( began, ended, s = "same day" ) {
234
  began = when( anchor, began )
235
  ended = when( anchor, ended )
236
237
  # Swap the dates if the end date comes before the start date.
238
  if( as.integer( ended - began ) < 0 ) {
239
    tempd = began
240
    began = ended
241
    ended = tempd
242
  }
243
244
  # Calculate number of elapsed years.
245
  years = length( seq( from = began, to = ended, by = "year" ) ) - 1
246
247
  # Move the start date up by the number of elapsed years.
248
  if( years > 0 ) {
249
    began = seq( began, length = 2, by = concat( years, " years" ) )[2]
250
    years = pl.numeric( "year", years )
251
  }
252
  else {
253
    # Zero years.
254
    years = ""
255
  }
256
257
  # Calculate number of elapsed months, excluding years.
258
  months = length( seq( from = began, to = ended, by = "month" ) ) - 1
259
260
  # Move the start date up by the number of elapsed months
261
  if( months > 0 ) {
262
    began = seq( began, length = 2, by = concat( months, " months" ) )[2]
263
    months = pl.numeric( "month", months )
264
  }
265
  else {
266
    # Zero months
267
    months = ""
268
  }
269
270
  # Calculate number of elapsed days, excluding months and years.
271
  days = length( seq( from = began, to = ended, by = "day" ) ) - 1
272
273
  if( days > 0 ) {
274
    days = pl.numeric( "day", days )
275
  }
276
  else {
277
    # Zero days
278
    days = ""
279
  }
280
281
  if( years <= 0 && months <= 0 && days <= 0 ) {
282
    return( s )
283
  }
284
285
  # Put them all in a vector, then remove the empty values.
286
  s <- c( years, months, days )
287
  s <- s[ s != "" ]
288
289
  r <- paste( s, collapse = ", " )
290
291
  # If all three items are present, replace the last comma with ", and".
292
  if( length( s ) > 2 ) {
293
    return( gsub( "(.*),", "\\1, and", r ) )
294
  }
295
296
  # Does nothing if no commas are present.
297
  gsub( "(.*),", "\\1 and", r )
298
}
299
300
# -----------------------------------------------------------------------------
301
# Returns the number (n) in English followed by the plural or singular
302
# form of the given string (s; resumably a noun), if applicable, according
303
# to English grammar. That is, pl.numeric( "wolf", 5 ) will return
304
# "five wolves".
305
# -----------------------------------------------------------------------------
306
pl.numeric <- function( s, n ) {
307
  concat( cms( n ), concat( " ", pluralize( word=s, n=n ) ) )
308
}
309
310
# -----------------------------------------------------------------------------
311
# Pluralize s if n is not equal to 1.
312
# -----------------------------------------------------------------------------
313
pl <- function( s, count=2 ) {
314
  pluralize( word=s, n=count )
315
}
316
317
# -----------------------------------------------------------------------------
318
# Name of the season, starting with an capital letter.
319
# -----------------------------------------------------------------------------
320
season <- function( n, format = "%Y-%m-%d" ) {
321
  WS <- as.Date("2016-12-15", "%Y-%m-%d") # Winter Solstice
322
  SE <- as.Date("2016-03-15", "%Y-%m-%d") # Spring Equinox
323
  SS <- as.Date("2016-06-15", "%Y-%m-%d") # Summer Solstice
324
  AE <- as.Date("2016-09-15", "%Y-%m-%d") # Autumn Equinox
325
326
  d <- when( anchor, n )
327
  d <- as.Date( strftime( d, format="2016-%m-%d" ) )
328
329
  ifelse( d >= WS | d < SE, "Winter",
330
    ifelse( d >= SE & d < SS, "Spring",
331
      ifelse( d >= SS & d < AE, "Summer", "Autumn" )
332
    )
333
  )
334
}
335
336
# -----------------------------------------------------------------------------
337
# Converts the first letter in a string to lowercase
338
# -----------------------------------------------------------------------------
339
lc <- function( s ) {
340
  concat( tolower( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
341
}
342
343
# -----------------------------------------------------------------------------
344
# Converts the entire string to lowercase
345
# -----------------------------------------------------------------------------
346
lower <- tolower
347
348
# -----------------------------------------------------------------------------
349
# Converts the first letter in a string to uppercase
350
# -----------------------------------------------------------------------------
351
uc <- function( s ) {
352
  concat( toupper( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
353
}
354
355
# -----------------------------------------------------------------------------
356
# Returns the number of time periods that have elapsed.
357
# -----------------------------------------------------------------------------
358
time.elapsed <- function( began, ended, by = "year" ) {
359
  began = when( anchor, began )
360
  ended = when( anchor, ended )
361
362
  # Swap the dates if the end date comes before the start date.
363
  if( as.integer( ended - began ) < 0 ) {
364
    tempd = began
365
    began = ended
366
    ended = tempd
367
  }
368
369
  # Calculate the elapsed time period.
370
  length( seq( from = began, to = ended, by = by ) ) - 1
371
}
372
373
# -----------------------------------------------------------------------------
374
# Returns the number of days between the given dates, taking into account
375
# the passage of years.
376
# -----------------------------------------------------------------------------
377
days <- function( d1, d2, format = "%Y-%m-%d" ) {
378
  dates = c( d1, d2 )
379
  dt = strptime( dates, format = format )
380
381
  as.integer( difftime( dates[2], dates[1], units = "days" ) )
382
}
383
384
# -----------------------------------------------------------------------------
385
# Returns the number of elapsed weeks.
386
# -----------------------------------------------------------------------------
387
weeks <- function( began, ended ) {
388
  time.elapsed( began, ended, "weeks" );
389
}
390
391
# -----------------------------------------------------------------------------
392
# Returns the number of elapsed months.
393
# -----------------------------------------------------------------------------
394
months <- function( began, ended ) {
395
  time.elapsed( began, ended, "months" );
396
}
397
398
# -----------------------------------------------------------------------------
399
# Returns the number of elapsed years.
400
# -----------------------------------------------------------------------------
401
years <- function( began, ended ) {
402
  time.elapsed( began, ended, "years" );
403
}
404
405
# -----------------------------------------------------------------------------
406
# Full name of the month, starting with a capital letter.
407
# -----------------------------------------------------------------------------
408
month <- function( n ) {
409
  # Faster than month.name[ x( n ) ]
410
  .subset( month.name, x( n ) )
411
}
412
413
# -----------------------------------------------------------------------------
414
# -----------------------------------------------------------------------------
415
money <- function( n ) {
416
  commas( x( n ) )
417
}
418
419
# -----------------------------------------------------------------------------
420
# -----------------------------------------------------------------------------
421
timeline <- function( n ) {
422
  concat( weekday( n ), ", ", annal( n ), " (", season( n ), ")" )
423
}
424
425
# -----------------------------------------------------------------------------
426
# Rounds to the nearest base value (e.g., round to nearest 10).
427
#
428
# @param base The nearest value to round to.
429
# -----------------------------------------------------------------------------
430
round.up <- function( n, base = 5 ) {
431
  base * round( x( n ) / base )
432
}
433
434
# -----------------------------------------------------------------------------
435
# Rounds the given value to the nearest integer.
436
#
437
# @param n The value round.
438
# -----------------------------------------------------------------------------
439
round.int <- function( n ) {
440
  format( round( n ) )
441
}
442
443
# -----------------------------------------------------------------------------
444
# Removes common accents from letters.
445
#
446
# @param s The string to remove diacritics from.
447
#
448
# @return The given string without diacritics.
449
# -----------------------------------------------------------------------------
450
accentless <- function( s ) {
451
  chartr(
452
    "áéóūáéíóúÁÉÍÓÚýÝàèìòùÀÈÌÒÙâêîôûÂÊÎÔÛãõÃÕñÑäëïöüÄËÏÖÜÿçÇ",
453
    "aeouaeiouAEIOUyYaeiouAEIOUaeiouAEIOUaoAOnNaeiouAEIOUycC",
454
    s );
455
}
456
457
# -----------------------------------------------------------------------------
458
# Computes linear distance between two points using Haversine formula.
459
# Although Earth is an oblate spheroid, this will produce results close
460
# enough for most purposes.
461
#
462
# @param lat1/lon1 The source latitude and longitude.
463
# @param lat2/lon2 The destination latitude and longitude.
464
# @param radius The radius of the sphere.
465
#
466
# @return The distance between the two coordinates in meters.
467
# -----------------------------------------------------------------------------
468
haversine <- function( lat1, lon1, lat2, lon2, radius = 6371 ) {
469
  # Convert decimal degrees to radians
470
  lon1 = lon1 * pi / 180
471
  lon2 = lon2 * pi / 180
472
  lat1 = lat1 * pi / 180
473
  lat2 = lat2 * pi / 180
474
475
  # Haversine formula
476
  dlon = lon2 - lon1
477
  dlat = lat2 - lat1
478
  a = sin( dlat / 2 ) ** 2 + cos( lat1 ) * cos( lat2 ) * sin( dlon / 2 ) ** 2
479
  c = 2 * atan2( sqrt( a ), sqrt( 1-a ) )
480
481
  return( radius * c * 1000 )
482
}
483
1484
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 file name to convert.
31
# @param decimals Rounded decimal places (default 2).
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, caption = "" ) {
36
  # Read the CVS data from the file; ensure strings become characters.
37
  df <- read.table( f, sep=',', header=T, stringsAsFactors=F, check.names=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
    # pluralize 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
  # Use pandoc syntax for table captions.
79
  if( caption != "" ) {
80
    caption <- paste( '\n[', caption, ']\n', sep='' )
81
  }
82
83
  # Create a Markdown version of the data frame.
84
  paste(
85
    '|', paste( names( df ), collapse = '|'), '|', '\n',
86
    '|', dashes, '|', '\n', 
87
    paste(
88
      '|',
89
      Reduce( function( x, y ) {
90
          paste( x, format( y, nsmall = decimals ), sep = '|' )
91
        }, df
92
      ),
93
      collapse = '|\n',sep=''
94
    ),
95
    '|',
96
    caption,
97
    sep=''
98
  )
99
}
100
1101
A R/numeric.R
1
# TODO: Finish the implementation
2
3
# -----------------------------------------------------------------------------
4
# Converts an integer value into English words. Negative numbers are prefixed
5
# with the word minus. This is useful for very large numbers.
6
#
7
# See https://english.stackexchange.com/a/111837/22099
8
#
9
# @param n Any integer value, including zero, and negative numbers.
10
# -----------------------------------------------------------------------------
11
to.words <- function( n ) {
12
  s <- 'zero';
13
14
  if( n > 0 ) {
15
    s <- to.words.nz( n );
16
  }
17
  else if( n < 0 ) {
18
    s <- paste0( 'minus ', to.words.nz( -n ) );
19
  }
20
21
  s
22
}
23
24
# -----------------------------------------------------------------------------
25
# Converts a non-zero number into English words.
26
# -----------------------------------------------------------------------------
27
to.words.nz <- function( n ) {
28
  scales <- c(
29
    "thousand", "million", "billion", "trillion", "quadrillion",
30
    "quintillion", "sextillion", "septillion", "octillion", "nonillion",
31
    "decillion", "undecillion", "duodecillion", "tredecillion",
32
    "quattuordecillion", "quindecillion", "sexdecillion", "septendecillion",
33
    "octodecillion", "novemdecillion", "vigintillion", "centillion",
34
    "quadrillion", "quitillion", "sextillion"
35
  );
36
37
  i <- 0;
38
  s <- "";
39
40
  while( n > 0 ) {
41
    if( !(n %% 1000 == 0) ) {
42
      j <- if( n < 100 ) "," else "";
43
      s <- paste( to.words.help( n %% 1000 ), scales[ i ], j, s );
44
    }
45
46
    n <- floor( n / 1000 );
47
    i <- i + 1;
48
  }
49
50
  s
51
}
52
53
to.words.help <- function( n ) {
54
  low <- c( 
55
    "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
56
    "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
57
    "seventeen", "eighteen", "nineteen"
58
  );
59
60
  tens <- c(
61
    "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"
62
  );
63
64
  if( n < 20 ) {
65
    s <- low[ n ];
66
  }
67
  else if( n < 100 ) {
68
    d <- n %% 10;
69
    j <- if( d > 0 ) "-" else "";
70
    s <- paste0( tens[ (n / 10) - 1 ], j, to.words.help( d ) );
71
  }
72
  else {
73
    d <- (n / 100);
74
    r <- (n %% 100);
75
    j <- if( r > 0 ) "and" else "";
76
    s <- paste( low[ d ], "hundred", j, to.words.help( r ) );
77
  }
78
79
  s
80
}
81
182
A R/pluralize.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2021 Robin Gertenbach.
3
#
4
# Copyright 2021 White Magic Software, Ltd.
5
# 
6
# Permission is hereby granted, free of charge, to any person obtaining
7
# a 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
15
# included in all copies or substantial portions of the Software.
16
# 
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
# -----------------------------------------------------------------------------
25
26
# -----------------------------------------------------------------------------
27
# See Damian Conway's "An Algorithmic Approach to English Pluralization":
28
#   http://goo.gl/oRL4MP
29
# See Oliver Glerke's Evo Inflector: https://github.com/atteo/evo-inflector/
30
# See Shevek's Pluralizer: https://github.com/shevek/linguistics/
31
# See also: http://www.freevectors.net/assets/files/plural.txt
32
# -----------------------------------------------------------------------------
33
34
# -----------------------------------------------------------------------------
35
# Applies all pluralization rules.
36
#
37
# @param word The word to pluralize.
38
# @param method The pluralization approach to apply to the word.
39
# @param n When any other value than 1, the word is pluralized.
40
# -----------------------------------------------------------------------------
41
pluralize <- function( word, method = c( "ac", "ca", "a", "c" ), n = 2 ) {
42
  if( n != 1 ) {
43
    method <- match.arg( method )
44
45
    coalesce( 
46
      pluralize_non_inflecting( word ),
47
      pluralize_pronoun( word ),
48
      pluralize_irregular( word, method ),
49
      pluralize_irregular_inflection_for_common_suffixes( word ),
50
      pluralize_fully_assimilated_classical_inflections( word ),
51
      pluralize_classical_variants_of_modern_inflections( word, method ),
52
      pluralize_ch_sh_ss_suffixes( word ),
53
      pluralize_f_and_fe_suffix( word ),
54
      pluralize_y_suffix( word ),
55
      pluralize_o_suffix( word ),
56
      pluralize_compound_words( word ),
57
      pluralize_regular( word )
58
    )
59
  }
60
  else {
61
    word
62
  }
63
}
64
65
# -----------------------------------------------------------------------------
66
# Rule 1
67
#
68
# Retain non-inflective user-mapped noun as is.
69
#
70
# Rule 2
71
#
72
# Irregular verbs that do not inflect in plural.
73
# -----------------------------------------------------------------------------
74
pluralize_non_inflecting <- function( word ) {
75
  coalesce( 
76
    ifelse( word %in% .uninflected_nouns, word, NA_character_ ),
77
    ifelse( word %in% .singular_nouns, word, NA_character_ ),
78
    ifelse( check_suffix( word, .irregular_patterns ), word, NA_character_ )
79
  )
80
} 
81
82
.check_non_inflecting <- function( word ) {
83
  is_uninflected <- word %in% .uninflected_nouns
84
  is_singular <- word %in% .singular_nouns
85
  is_irregular <- check_suffix( word, .irregular_patterns )
86
87
  is_uninflected | is_singular | is_irregular
88
}
89
90
.uninflected_nouns <- c( 
91
  "adonis",
92
  "anis",
93
  "bison",
94
  "bream",
95
  "breeches",
96
  "britches",
97
  "carp",
98
  "chassis",
99
  "clippers",
100
  "cod",
101
  "contretemps",
102
  "corps",
103
  "debris",
104
  "diabetes",
105
  "djinn",
106
  "eland",
107
  "elk",
108
  "flounder",
109
  "gallows",
110
  "graffiti",
111
  "headquarters",
112
  "herpes",
113
  "high-jinks",
114
  "homework",
115
  "innings",
116
  "jackanapes",
117
  "mackerel",
118
  "measles",
119
  "mews",
120
  "mumps",
121
  "news",
122
  "pants",
123
  "physics",
124
  "pincers",
125
  "pliers",
126
  "proceedings",
127
  "rabies",
128
  "salmon",
129
  "scissors",
130
  "sea-bass",
131
  "series",
132
  "shears",
133
  "species",
134
  "swine",
135
  "trout",
136
  "tuna",
137
  "whiting",
138
  "wildebeest"
139
)
140
141
.singular_nouns <- c( 
142
  "bathos",
143
  "caddis",
144
  "cannabis",
145
  "dais",
146
  "digitalis",
147
  "ethos",
148
  "glottis",
149
  "marquis",
150
  "pathos",
151
  "polis"
152
)
153
154
.irregular_patterns <- c( 
155
  "fish$", "ois$", "sheep$", "deer$", "pox$", "[A-Z].*ese$", "itis$"
156
)
157
158
.prepositions <- c( 
159
  "about",
160
  "above",
161
  "across",
162
  "after",
163
  "among",
164
  "around",
165
  "at",
166
  "athwart",
167
  "before",
168
  "behind",
169
  "below",
170
  "beneath",
171
  "beside",
172
  "besides",
173
  "between",
174
  "betwixt",
175
  "beyond",
176
  "but",
177
  "by",
178
  "during",
179
  "except",
180
  "for",
181
  "from",
182
  "in",
183
  "into",
184
  "near",
185
  "of",
186
  "off",
187
  "on",
188
  "onto",
189
  "out",
190
  "over",
191
  "since",
192
  "till",
193
  "to",
194
  "under",
195
  "until",
196
  "unto",
197
  "upon",
198
  "with"
199
)
200
201
# -----------------------------------------------------------------------------
202
# Rule 3
203
#
204
# Handle pronouns in the nominative, accusative, and dative and propositional
205
# phrases.
206
# -----------------------------------------------------------------------------
207
pluralize_pronoun <- function( word ) {
208
  as.vector( .pluralized_pronouns[word] )
209
}
210
211
.pluralized_pronouns <- c( 
212
  "I" = "we",
213
  "me" = "us",
214
  "myself" = "ourselves",
215
216
  "you" = "you",
217
  "thou" = "ye",
218
  "thee" = "ye",
219
  "yourself" = "yourself",
220
  "thyself" = "yourself",
221
222
  "she" = "they",
223
  "he" = "they",
224
  "it" = "they",
225
  "they" = "they",
226
227
  "her" = "them",
228
  "him" = "them",
229
  "it" = "them",
230
  "them" = "them",
231
232
  "herself" = "themselves",
233
  "himself" = "themselves",
234
  "itself" = "themselves",
235
236
  "themself" = "themselves",
237
  "oneself" = "oneselves"
238
)
239
240
# -----------------------------------------------------------------------------
241
# Rule 4
242
#
243
# Change irregular plurals based on mapping.
244
# -----------------------------------------------------------------------------
245
pluralize_irregular <- function( word, method = c( "ac", "ca", "a", "c" ) ) {
246
  method <- match.arg( method )
247
  plurals <- .irregular_nouns[word]
248
249
  extract_plural <- function( plurals ) {
250
    if( is.null( plurals ) ) {
251
      return( NA_character_ )
252
    }
253
254
    return(
255
      switch( 
256
        method,
257
        "a" = plurals["a"],
258
        "c" = plurals["c"],
259
        "ac" = if.na( plurals["a"], plurals["c"] ),
260
        "ca" = if.na( plurals["c"], plurals["a"] )
261
      )
262
    )
263
  }
264
265
  as.character( lapply( plurals, extract_plural ) )
266
}
267
268
.irregular_nouns <- list( 
269
  "beef"      = c( "a" = "beefs",       "c" = "beeves" ),
270
  "biscotto"  = c( "a" = "biscotti",    "c" = NA_character_ ),
271
  "brother"   = c( "a" = "brothers",    "c" = "brethren" ),
272
  "cactus"    = c( "a" = NA_character_, "c" = "catci" ),
273
  "child"     = c( "a" = NA_character_, "c" = "children" ),
274
  "cherub"    = c( "a" = "cherubim",    "c" = NA_character_ ),
275
  "cow"       = c( "a" = "cows",        "c" = "kine" ),
276
  "crisis"    = c( "a" = NA_character_, "c" = "crises" ),
277
  "data"      = c( "a" = "data",        "c" = "data" ),
278
  "ephemeris" = c( "a" = NA_character_, "c" = "ephemerides" ),
279
  "genie"     = c( "a" = "genies",      "c" = "genii" ),
280
  "graffito"  = c( "a" = "graffiti",    "c" = NA_character_ ),
281
  "matrix"    = c( "a" = NA_character_, "c" = "matrices" ),
282
  "money"     = c( "a" = "moneys",      "c" = "monies" ),
283
  "mongoose"  = c( "a" = "mongooses",   "c" = NA_character_ ),
284
  "minimum"   = c( "a" = "minimums",    "c" = "minima" ),
285
  "mythos"    = c( "a" = NA_character_, "c" = "mythoi" ),
286
  "octopus"   = c( "a" = NA_character_, "c" = "octopodes" ),
287
  "ox"        = c( "a" = NA_character_, "c" = "oxen" ),
288
  "passerby"  = c( "a" = NA_character_, "c" = "passersby" ),
289
  "panino"    = c( "a" = "panini",      "c" = NA_character_ ),
290
  "pieróg"    = c( "a" = "pierogi",     "c" = NA_character_ ),
291
  "pierog"    = c( "a" = "pierogi",     "c" = NA_character_ ),
292
  "radius"    = c( "a" = NA_character_, "c" = "radii" ),
293
  "referendum"= c( "a" = "referendums", "c" = "referenda" ),
294
  "soliloquy" = c( "a" = "soliloquies", "c" = NA_character_ ),
295
  "seraph"    = c( "a" = "seraphim",    "c" = NA_character_ ),
296
  "stadium"   = c( "a" = "stadiums",    "c" = "stadia" ),
297
  "trilby"    = c( "a" = "trilbys",     "c" = NA_character_ ),
298
  "vertex"    = c( "a" = NA_character_, "c" = "vertices" ),
299
  "vortex"    = c( "a" = NA_character_, "c" = "vortices" )
300
)
301
302
# -----------------------------------------------------------------------------
303
# Rule 5
304
#
305
# Handle irregular inflections for common suffixes.
306
# -----------------------------------------------------------------------------
307
pluralize_irregular_inflection_for_common_suffixes <- function( word ) {
308
  output <- sub( "man$", "men", word )
309
  output <- sub( "([ml])(ouse)$", "\\1ice", output )
310
  output <- sub( "tooth$", "teeth", output )
311
  output <- sub( "goose$", "geese", output )
312
  output <- sub( "foot$", "feet", output )
313
  output <- sub( "zoon$", "zoa", output )
314
  output <- sub( "([csx])(is)$", "\\1es", output )
315
316
  ifelse( output == word, NA_character_, output )
317
}
318
319
# -----------------------------------------------------------------------------
320
# Rule 6
321
#
322
# Handle fully assimilated classical inflections.
323
# -----------------------------------------------------------------------------
324
pluralize_fully_assimilated_classical_inflections <- function( word ) {
325
  output <- replace_suffix(
326
    word, "", "e", c( "alumna", "alga", "vertebra" ) )
327
  output <- replace_suffix(
328
    output, "ex", "ices", c( "codex", "murex", "silex" ) )
329
  output <- replace_suffix(
330
    output, "on", "a", c( 
331
      "aphelion",
332
      "asyndeton",
333
      "criterion",
334
      "hyperbaton",
335
      "noumenon",
336
      "organon",
337
      "perihelion",
338
      "phenomenon",
339
      "prolegomenon"
340
    )
341
  )
342
  output <- replace_suffix(
343
    output, "um", "a", c( 
344
      "agendum",
345
      "bacterium",
346
      "candelabrum",
347
      "datum",
348
      "desideratum",
349
      "erratum",
350
      "extremum",
351
      "ovum",
352
      "stratum"
353
    )
354
  )
355
356
  ifelse( output == word, NA_character_, output )
357
}
358
359
# -----------------------------------------------------------------------------
360
# Rule 7
361
#
362
# Classical variants of modern inflections (e.g., stigmata, soprani).
363
#
364
# See tables A.11 to A.13, A.15, A.16, A.18, A.21 to A.25.
365
# -----------------------------------------------------------------------------
366
pluralize_classical_variants_of_modern_inflections <- function( 
367
  word, method = c( "ac", "ca", "a", "c" ) ) {
368
  method <- match.arg( method )
369
370
  # -a to -as (anglicized) or -ae (classical)
371
  a11 <- c( 
372
    "abscissa",
373
    "amoeba",
374
    "antenna",
375
    "aurora",
376
    "formula",
377
    "hydra",
378
    "hyperbola",
379
    "lacuna",
380
    "medusa",
381
    "nebula",
382
    "nova",
383
    "parabola"
384
  )
385
386
  # Table A.12: -a to -as (anglicized) or -ata (classical)
387
  a12 <- c( 
388
    "anathema",
389
    "bema",
390
    "carcinoma",
391
    "charisma",
392
    "diploma",
393
    "dogma",
394
    "drama",
395
    "edema",
396
    "enema",
397
    "enigma",
398
    "gumma",
399
    "lemma",
400
    "lymphoma",
401
    "magma",
402
    "melisma",
403
    "miasma",
404
    "oedema",
405
    "sarcoma",
406
    "schema",
407
    "soma",
408
    "stigma",
409
    "stoma",
410
    "trauma"
411
  )
412
  
413
  # Table A.13: -en to -ens (anglicized) or -ina (classical)
414
  a13 <- c( "stamen", "foramen", "lumen" )
415
  
416
  # Table A.15: -ex to -exes (anglicized) or -ices (classical)
417
  a15 <- c( 
418
    "apex",
419
    "cortex",
420
    "index",
421
    "latex",
422
    "pontifex",
423
    "simplex",
424
    "vertex",
425
    "vortex"
426
  )
427
  
428
  # Table A.16: -is to -ises (anglicized) or -ides (classical)
429
  a16 <- c( "iris", "clitoris" )
430
  
431
  # Table A.18: -o to -os (anglicized) or -i (classical)
432
  a18 <- c( 
433
    "alto",
434
    "basso",
435
    "canto",
436
    "contralto",
437
    "crescendo",
438
    "solo",
439
    "soprano",
440
    "tempo"
441
  )
442
   
443
  # Table A.21: -um to -ums (anglicized) or -a (classical)
444
  a21 <- c( 
445
    "aquarium",
446
    "compendium",
447
    "consortium",
448
    "cranium",
449
    "curriculum",
450
    "dictum",
451
    "emporium",
452
    "enconium",
453
    "gymnasium",
454
    "honorarium",
455
    "interregnum",
456
    "lustrum",
457
    "maximum",
458
    "medium",
459
    "memorandum",
460
    "millennium",
461
    "minimum",
462
    "momentum",
463
    "optimum",
464
    "phylum",
465
    "quantum",
466
    "rostrum",
467
    "spectrum",
468
    "speculum",
469
    "stadium",
470
    "trapezium",
471
    "ultimatum",
472
    "vacuum",
473
    "velum"
474
  )
475
  
476
  # Table A.22: -us to -uses (anglicized) or -i (classical)
477
  a22 <- c( 
478
    "focus",
479
    "fungus",
480
    "genius",
481
    "incubus",
482
    "nimbus",
483
    "nucleolus",
484
    "radius",
485
    "stylus",
486
    "succubus",
487
    "torus",
488
    "umbilicus",
489
    "uterus"
490
  )
491
  
492
  # Table A.23: -us to -uses (anglicized) or -us (classical)
493
  a23 <- c( 
494
    "apparatus",
495
    "cantus",
496
    "coitus",
497
    "hiatus",
498
    "impetus",
499
    "nexus",
500
    "plexus",
501
    "prospectus",
502
    "sinus",
503
    "status"
504
  )
505
  
506
  output <- replace_suffix( word, "", "im", c( "cherub", "goy", "seraph"  ) )
507
  output <- replace_suffix( output, "", "i", c( "afreet", "afrit", "efreet" ) )
508
  
509
  if( method %in% c( "a", "ac" ) ) {
510
    output <- replace_suffix( output, "us", "uses", a23 )
511
    output <- replace_suffix( output, "us", "uses", a22 )
512
    output <- replace_suffix( output, "um", "ums", a21 )
513
    output <- replace_suffix( output, "o", "os", a18 )
514
    output <- replace_suffix( output, "is", "ises", a16 )
515
    output <- replace_suffix( output, "ex", "exes", a15 )
516
    output <- replace_suffix( output, "en", "ens", a13 )
517
    output <- replace_suffix( output, "a", "as", a12 )
518
    output <- replace_suffix( output, "a", "as", a11 )
519
  } else {
520
    output <- replace_suffix( output, "us", "us", a23 )
521
    output <- replace_suffix( output, "us", "i", a22 )
522
    output <- replace_suffix( output, "um", "a", a21 )
523
    output <- replace_suffix( output, "o", "i", a18 )
524
    output <- replace_suffix( output, "is", "ides", a16 )
525
    output <- replace_suffix( output, "ex", "ices", a15 )
526
    output <- replace_suffix( output, "en", "ina", a13 )
527
    output <- replace_suffix( output, "a", "ata", a12 )
528
    output <- replace_suffix( output, "a", "ae", a11 )
529
  }
530
531
  ifelse( 
532
    output == word & (method %in% c( "a", "ac" ) | !word %in% a23), 
533
    NA_character_, 
534
    output
535
  )
536
}
537
538
# -----------------------------------------------------------------------------
539
# Rule 8
540
#
541
# Suffixes -ch, -sh, -ss, -x, and -z take -es as plural (e.g., churches,
542
# classes).
543
# -----------------------------------------------------------------------------
544
pluralize_ch_sh_ss_suffixes <- function( word ) {
545
  output <- sub( "(([cs]h)|(x|z))$", "\\1es", word )
546
  output <- replace_suffix( output, "ss", "sses" )
547
548
  ifelse( output == word, NA_character_, output )
549
}
550
551
# -----------------------------------------------------------------------------
552
# Rule 9
553
#
554
# Certain words ending in -f or -fe take -ves in the plural.
555
# -----------------------------------------------------------------------------
556
pluralize_f_and_fe_suffix <- function( word ) {
557
  output <- sub( "([aeo]l|[^d]ea|ar)f$", "\\1ves", word )
558
  output <- sub( "([nlw]i)fe$", "\\1ves", output )
559
560
  ifelse( output == word, NA_character_, output )
561
}
562
563
# -----------------------------------------------------------------------------
564
# Rule 10
565
#
566
# Words ending in -y take -ies.
567
# -----------------------------------------------------------------------------
568
pluralize_y_suffix <- function( word ) {
569
  output <- sub( "([aeiou]y)$", "\\1s", word )
570
  output <- sub( "([A-Z].*y)$", "\\1s", output )
571
  output <- replace_suffix( output, "y", "ies" )
572
573
  ifelse( output == word, NA_character_, output )
574
}
575
576
# -----------------------------------------------------------------------------
577
# Rule 11
578
#
579
# Some words ending in -o take -os (lassos, solos). See tables A.17 and A.18.
580
# Others take -oes (potatoes, dominoes).
581
# When -o is preceded by a vowel always take -os (folios, bamboos).
582
# -----------------------------------------------------------------------------
583
pluralize_o_suffix <- function( word, method = c( "ac", "ca", "a", "c" ) ) {
584
  method <- match.arg( method )
585
586
  # Table A.17: -o to -os
587
  a17 <- c( 
588
    "albino",
589
    "archipelago",
590
    "armadillo",
591
    "commando",
592
    "ditto",
593
    "dynamo",
594
    "embryo",
595
    "fiasco",
596
    "generalissimo",
597
    "ghetto",
598
    "guano",
599
    "inferno",
600
    "jumbo",
601
    "lingo",
602
    "lumbago",
603
    "magneto",
604
    "manifesto",
605
    "medico",
606
    "octavo",
607
    "photo",
608
    "pro",
609
    "quarto",
610
    "rhino",
611
    "stylo"
612
  )
613
614
  # Table A.18: -o to -os (anglicized) or -i (classical)
615
  a18 <- c( 
616
    "alto",
617
    "basso",
618
    "canto",
619
    "contralto",
620
    "crescendo",
621
    "solo",
622
    "soprano",
623
    "tempo"
624
  )
625
626
  output <- replace_suffix( word, "o", "os", a17 )
627
  replacement <- if( method %in% c( "c", "ca" ) ) "i" else "os"
628
  output <- replace_suffix( output, "o", replacement, a18 )
629
630
  ifelse( output == word, NA_character_, output )
631
}
632
633
# -----------------------------------------------------------------------------
634
# Rule 12
635
#
636
# Compound word pluralization.
637
# -----------------------------------------------------------------------------
638
pluralize_compound_words <- function(
639
  word, method = c( "ac", "ca", "a", "c" ) ) {
640
  method <- match.arg( method )
641
642
  # "General" is pluralized.
643
  military <- c(
644
    "Adjutant",
645
    "Brigadier",
646
    "Lieutenant",
647
    "Major",
648
    "Quartermaster"
649
  )
650
651
  # X of Y -> plural(X) of Y
652
  # X at Y -> plural(X) of Y
653
  # X Y general -> X plural(Y) general
654
  # X-in-Y -> plural(X)-in-Y
655
656
  # Major Generals
657
  # Adjutant Generals
658
  # Lieutenant Generals
659
  # Brigadier Generals
660
  # Quartermaster Generals
661
662
  pluralize_cw <- Vectorize(
663
    function( cw, seps ) {
664
      if( cw[length( cw )] %in% c( "General", "general" ) && 
665
          (!cw[length( cw )] %in% military) ) {
666
        cw[1] <- pluralize( cw[1], method )
667
      } else {
668
        cw[1] <- pluralize( cw[1], method )
669
      }
670
671
      paste( paste0( seps, cw ), collapse = "" )
672
    }
673
  )
674
675
  parts <- strsplit( word, "[- ]+" )
676
  seps <- strsplit( word, "[^ -]+" )
677
  is_compound <- grepl( "[- ]", word )
678
  output <- word
679
  output[!is_compound] <- NA_character_
680
  output[is_compound] <- pluralize_cw( parts[is_compound], seps[is_compound] )
681
682
  output
683
}
684
685
# -----------------------------------------------------------------------------
686
# Rule 13
687
#
688
# Add -es if ending in -s (e.g., tennis, lychnis); otherwise, add -s.
689
# -----------------------------------------------------------------------------
690
pluralize_regular <- function( word ) {
691
  ending <- 's'
692
693
  if( endsWith( word, ending ) ) {
694
    ending <- "es"
695
  }
696
697
  paste0( word, ending )
698
}
699
700
# -----------------------------------------------------------------------------
701
# Determines whether the word ends with one of the given suffixes.
702
# -----------------------------------------------------------------------------
703
check_suffix <- function( x, suffixes ) {
704
  pattern <- paste0( "(", paste( suffixes, collapse = "|" ), ")$" )
705
  grepl( pattern, x, ignore.case = TRUE )
706
}
707
708
# -----------------------------------------------------------------------------
709
# Replaces the suffix of the word.
710
# -----------------------------------------------------------------------------
711
replace_suffix <- function( x, suffix, replacement, eligible = NULL ) {
712
  ifelse( 
713
    is.null( eligible ) | x %in% eligible, 
714
    sub( paste0( suffix, "$" ), replacement, x ),
715
    x
716
  )
717
}
718
719
# -----------------------------------------------------------------------------
720
# Returns y if x is na, otherwise x.
721
# -----------------------------------------------------------------------------
722
if.na <- function( x, y ) {
723
  ifelse( is.na( x ), y, x )
724
}
725
726
# -----------------------------------------------------------------------------
727
# Reduces the given function list.
728
# -----------------------------------------------------------------------------
729
coalesce <- function( ... ) {
730
  args <- list( ... )
731
  Reduce( if.na, args )
732
}
733
1734
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
62
pro.sub <- function( s ) {
63
  if( s == 'm' ) {
64
    s <- 'he'
65
  }
66
  else if( s == 'f' ) {
67
    s <- 'she'
68
  }
69
  else {
70
    s <- 'their'
71
  }
72
73
  s
74
}
75
76
pro.obj <- function( s ) {
77
  if( s == 'm' ) {
78
    s <- 'him'
79
  }
80
  else if( s == 'f' ) {
81
    s <- 'her'
82
  }
83
  else {
84
    s <- 'them'
85
  }
86
87
  s
88
}
89
90
pro.ref <- function( s ) {
91
  if( s == 'm' ) {
92
    s <- 'himself'
93
  }
94
  else if( s == 'f' ) {
95
    s <- 'herself'
96
  }
97
  else {
98
    s <- 'themselves'
99
  }
100
101
  s
102
}
103
104
pro.adj <- function( s ) {
105
  if( s == 'm' ) {
106
    s <- 'his'
107
  }
108
  else if( s == 'f' ) {
109
    s <- 'her'
110
  }
111
  else {
112
    s <- 'their'
113
  }
114
115
  s
116
}
117
118
pro.pos <- function( s ) {
119
  if( s == 'm' ) {
120
    s <- 'his'
121
  }
122
  else if( s == 'f' ) {
123
    s <- 'hers'
124
  }
125
  else {
126
    s <- 'theirs'
127
  }
128
129
  s
130
}
131
132
pro.noun <- function( s ) {
133
  if( s == 'm' ) {
134
    s <- 'man'
135
  }
136
  else if( s == 'f' ) {
137
    s <- 'woman'
138
  }
139
140
  s
141
}
142
1143
A R.zip
Binary file
A README.md
1
# ![KeenWrite](docs/images/app-title.png)
2
3
A free, open-source, cross-platform desktop Markdown editor that can produce beautifully typeset PDFs.
4
5
## Download
6
7
Visit [KeenWrite.com](https://keenwrite.com/) for downloads.
8
9
## Run
10
11
Note that the first time the application runs, it will unpack itself into a local directory. Subsequent starts will be faster.
12
13
### Windows
14
15
Double-click the application to start; give the application permission to run.
16
17
### Linux
18
19
Execute the following commands in a terminal:
20
21
``` bash
22
chmod +x keenwrite.bin
23
./keenwrite.bin
24
```
25
26
### MacOS
27
28
Execute the following commands in a terminal:
29
30
``` bash
31
chmod +x keenwrite.app
32
./keenwrite.app
33
```
34
35
### Java
36
37
Using Java, first follow these one-time setup steps:
38
39
1. Download the *Full version* of the Java Runtime Environment, [JRE 23](https://bell-sw.com/pages/downloads).
40
   * JavaFX, which is bundled with BellSoft's *Full version*, is required.
41
1. Install the JRE (include JRE's `bin` directory in the `PATH` environment variable).
42
1. Open a new terminal.
43
1. Verify the installation: `java -version`
44
1. Download [keenwrite.jar](https://keenwrite.com/downloads/keenwrite.jar).
45
1. Download [keenwrite.sh](https://gitlab.com/DaveJarvis/KeenWrite/-/raw/main/keenwrite.sh?inline=false).
46
1. Place the `.jar` and `.sh` in the same directory.
47
1. Make `keenwrite.sh` executable: `chmod +x keenwrite.sh`
48
49
Start the application as follows:
50
51
1. Open a new terminal.
52
1. Change to the `.jar` and `.sh` directory.
53
1. Run: `./keenwrite.sh`
54
55
The application is started.
56
57
## Features
58
59
The application offers:
60
61
* User-defined interpolated strings
62
* Auto-complete variable names based on variable values
63
* High-quality PDF exports
64
* Real-time spell check
65
* Real-time rendering of math using TeX notation
66
* Real-time document statistics (with CJK word separation)
67
* Diagrams: Mermaid, GraphViz, UML, sequence, timing, and more
68
* Dark, custom, and responsive user interface skins
69
* Integrated file manager
70
* Interactive document outline
71
* Internationalized font support (e.g., Chinese, Japanese, Korean)
72
* Support for Pandoc's fenced div extended attribute syntax
73
* R integration
74
* Customizable user interface having detachable tabs
75
* Platform-independent (Windows, Linux, MacOS)
76
77
## Typesetting
78
79
Typesetting to PDF files requires the following:
80
81
* [Theme Pack](https://gitlab.com/DaveJarvis/keenwrite-themes/-/releases/permalink/latest/downloads/theme-pack.zip)
82
* [ConTeXt](https://wiki.contextgarden.net/Installation)
83
84
## Usage
85
86
Read the [detailed documentation](docs/README.md) for using the application.
87
88
### Skins
89
90
Read the [skins documentation](docs/skins.md) to learn about how to change
91
the user interface appearance.
92
93
## Screenshots
94
95
See [screenshots](docs/screenshots.md) for visuals.
96
97
## License
98
99
This software is licensed under the [BSD 2-Clause License](LICENSE.md) and
100
based on [Markdown-Writer-FX](https://github.com/JFormDesigner/markdown-writer-fx/blob/main/LICENSE).
101
1102
A README.zh-CN.md
1
# ![Logo](docs/images/app-title.zh-CN.png)
2
3
智能写入是一个文本编辑器,它使用插值字符串引用外部定义的值。
4
5
## 下载
6
7
[KeenWrite.com](https://keenwrite.com/)
8
9
## 跑
10
11
在第一次运行期间,应用程序将自身解压到本地目录中。随后的启动会更快。
12
13
### Windows
14
15
双击应用程序以启动。您必须授予应用程序运行权限。 
16
17
升级时,删除以下目录:
18
19
    C:\Users\%USERNAME%\AppData\Local\warp\packages\keenwrite.exe
20
21
### Linux
22
23
执行以下命令:
24
25
``` bash
26
chmod +x keenwrite.bin
27
./keenwrite.bin
28
```
29
30
### MacOS
31
32
执行以下命令:
33
34
``` bash
35
chmod +x keenwrite.app
36
./keenwrite.app
37
```
38
39
### Java
40
41
使用Java,首先按照以下一次性设置步骤进行操作:
42
43
1. 下载Java Runtime Environment(JRE)的*完整版本*,[JRE 21](https://bell-sw.com/pages/downloads)。
44
   * 需要BellSoft的*完整版本*中捆绑的JavaFX。
45
1. 安装JRE(将JRE的`bin`目录包含在`PATH`环境变量中)。
46
1. 打开一个新的终端。
47
1. 验证安装:`java -version`
48
1. 下载[keenwrite.jar](https://keenwrite.com/downloads/keenwrite.jar)。
49
1. 下载[keenwrite.sh](https://gitlab.com/DaveJarvis/KeenWrite/-/raw/main/keenwrite.sh?inline=false)。
50
1. 将`.jar`和`.sh`文件放置在同一个目录中。
51
1. 使`keenwrite.sh`可执行:`chmod +x keenwrite.sh`
52
53
按以下方式启动应用程序:
54
55
1. 打开一个新的终端。
56
1. 切换到`.jar`和`.sh`目录。
57
1. 运行:`./keenwrite.sh`
58
59
应用程序已启动。
60
61
## 特征
62
63
* 用户定义的插值字符串
64
* 带变量替换的实时预览
65
* 基于变量值自动完成变量名
66
* 独立于操作系统
67
* 打字时拼写检查
68
* 使用TeX的子集编写数学公式
69
* 嵌入R语句
70
71
## Typesetting
72
73
排版到 PDF 文件需要以下內容:
74
75
* [Theme Pack](https://gitlab.com/DaveJarvis/keenwrite-themes/-/releases/permalink/latest/downloads/theme-pack.zip)
76
* [ConTeXt](https://wiki.contextgarden.net/Installation)
77
78
## 软件使用
79
80
有關使用該應用程序的信息,請參[閱詳細文檔](docs/README.md)。
81
82
## 截图
83
84
![GraphViz Diagram Screenshot](docs/images/screenshots/01.png)
85
86
![Korean Poem Screenshot](docs/images/screenshots/02.png)
87
88
![TeX Equations Screenshot](docs/images/screenshots/03.png)
89
90
91
## 软件许可证
92
93
This software is licensed under the [BSD 2-Clause License](LICENSE.md) and
94
based on [Markdown-Writer-FX](licenses/MARKDOWN-WRITER-FX.md).
95
196
A bin/LICENSE-linux-x64.warp-packer
1
MIT License
2
3
Copyright (c) 2018 Diego Giagio <diego@giagio.com>
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 bin/LICENSE-osslsigncode
1
OpenSSL based Authenticode signing for PE/MSI/Java CAB files.
2
3
Copyright (C) 2005-2014 Per Allansson <pallansson@gmail.com>
4
Copyright (C) 2018-2022 Michał Trojnara <Michal.Trojnara@stunnel.org>
5
6
This program is free software: you can redistribute it and/or modify
7
it under the terms of the GNU General Public License as published by
8
the Free Software Foundation, either version 3 of the License, or
9
(at your option) any later version.
10
11
This program is distributed in the hope that it will be useful,
12
but WITHOUT ANY WARRANTY; without even the implied warranty of
13
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
GNU General Public License for more details.
15
16
You should have received a copy of the GNU General Public License
17
along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19
In addition, as a special exception, the copyright holders give
20
permission to link the code of portions of this program with the
21
OpenSSL library under certain conditions as described in each
22
individual source file, and distribute linked combinations
23
including the two.
24
You must obey the GNU General Public License in all respects
25
for all of the code used other than OpenSSL.  If you modify
26
file(s) with this exception, you may extend this exception to your
27
version of the file(s), but you are not obligated to do so.  If you
28
do not wish to do so, delete this exception statement from your
29
version.  If you delete this exception statement from all source
30
files in the program, then also delete it here.
131
A bin/LICENSE-warp-packer
1
MIT License
2
3
Copyright (c) 2018 Diego Giagio <diego@giagio.com>
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 bin/linux-x64.warp-packer
Binary file
A bin/osslsigncode
Binary file
A bin/warp-packer
Binary file
A bug-filter.xml
1
<?xml version="1.0" encoding="UTF-8"?>
2
<FindBugsFilter>
3
  <Match>
4
    <Or>
5
      <Bug code="EI, EI2, CT, RV, PI" />
6
    </Or>
7
  </Match>
8
9
  <Match class="com.keenwrite.preview.HighQualityRenderingHints">
10
    <Method name="initializeRenderingHints" />
11
    <Bug code="WMI" />
12
  </Match>
13
14
  <Match class="com.keenwrite.processors.html.HtmlPreviewProcessor">
15
    <Method name="&lt;init&gt;" />
16
    <Bug code="ST" />
17
  </Match>
18
</FindBugsFilter>
119
A build.gradle
1
//file:noinspection SpellCheckingInspection
2
3
buildscript {
4
  repositories {
5
    mavenCentral()
6
    maven {
7
      url = 'https://plugins.gradle.org/m2/'
8
    }
9
  }
10
  dependencies {
11
    classpath 'org.owasp:dependency-check-gradle:10.0.3'
12
    classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.19"
13
  }
14
}
15
16
plugins {
17
  id 'application'
18
  id 'org.openjfx.javafxplugin' version '0.1.0'
19
  id 'com.palantir.git-version' version '3.1.0'
20
  id 'com.github.spotbugs' version '6.0.9'
21
}
22
23
spotbugs {
24
  excludeFilter.set(
25
    file( "${projectDir}/bug-filter.xml" )
26
  )
27
}
28
29
apply plugin: 'org.owasp.dependencycheck'
30
31
repositories {
32
  mavenCentral()
33
34
  maven { url = 'https://oss.sonatype.org/content/repositories/snapshots' }
35
  maven { url = 'https://nexus.bedatadriven.com/content/groups/public' }
36
37
  maven {
38
    url = 'https://css4j.github.io/maven'
39
    mavenContent {
40
      releasesOnly()
41
    }
42
    content {
43
      includeGroupByRegex 'io\\.sf\\..*'
44
    }
45
  }
46
}
47
48
// Assume a cross-platform überjar unless targetOs is set.
49
String[] os = [ 'win', 'mac', 'linux' ]
50
51
if( project.hasProperty( 'targetOs' ) ) {
52
  if( 'windows' == targetOs ) {
53
    os = [ 'win' ]
54
  } else if( 'macos' == targetOs ) {
55
    os = [ 'mac' ]
56
  } else {
57
    os = [ targetOs ]
58
  }
59
}
60
61
def moduleSecurity = [
62
  '--add-opens=javafx.graphics/javafx.scene=ALL-UNNAMED',
63
  '--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED',
64
  '--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED',
65
  '--add-opens=javafx.graphics/javafx.scene.text=ALL-UNNAMED',
66
  '--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED',
67
  '--add-opens=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED',
68
  '--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED',
69
  '--add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED',
70
  '--add-exports=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED',
71
  '--add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED',
72
  '--add-exports=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED',
73
  '--add-exports=javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED',
74
  '--add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED',
75
]
76
77
def javaVersionFull = new File( "java.version" ).text
78
def (javaVersion, javaUpdate) = javaVersionFull.tokenize( '+' )
79
80
java {
81
  sourceCompatibility = javaVersion
82
  targetCompatibility = javaVersion
83
}
84
85
javafx {
86
  version = javaVersion
87
  modules = [ 'javafx.base', 'javafx.controls', 'javafx.graphics', 'javafx.swing' ]
88
  configuration = 'compileOnly'
89
}
90
91
dependencies {
92
  def v_junit = '5.13.4'
93
  def v_platform = '1.13.4'
94
  def v_flexmark = '0.64.8'
95
  def v_jackson = '2.19.2'
96
  def v_echosvg = '2.2'
97
  def v_picocli = '4.7.7'
98
99
  // JavaFX
100
  implementation 'org.controlsfx:controlsfx:11.2.2'
101
  implementation 'org.fxmisc.richtext:richtextfx:0.11.5'
102
  implementation 'org.fxmisc.flowless:flowless:0.7.4'
103
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
104
  implementation 'org.openjfx:javafx-media:26-ea+3'
105
  implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.17.0'
106
  implementation 'com.panemu:tiwulfx-dock:0.5'
107
108
  // Markdown
109
  implementation "com.vladsch.flexmark:flexmark:${v_flexmark}"
110
  implementation "com.vladsch.flexmark:flexmark-ext-definition:${v_flexmark}"
111
  implementation "com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:${v_flexmark}"
112
  implementation "com.vladsch.flexmark:flexmark-ext-superscript:${v_flexmark}"
113
  implementation "com.vladsch.flexmark:flexmark-ext-tables:${v_flexmark}"
114
  implementation "com.vladsch.flexmark:flexmark-ext-typographic:${v_flexmark}"
115
116
  // YAML
117
  implementation 'org.yaml:snakeyaml:2.4'
118
  implementation "com.fasterxml.jackson.core:jackson-core:${v_jackson}"
119
  implementation "com.fasterxml.jackson.core:jackson-databind:${v_jackson}"
120
  implementation "com.fasterxml.jackson.core:jackson-annotations:${v_jackson}"
121
  implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${v_jackson}"
122
123
  // HTML parsing and rendering
124
  implementation 'org.jsoup:jsoup:1.18.3'
125
  implementation 'org.xhtmlrenderer:flying-saucer-core:9.11.2'
126
127
  // R
128
  implementation 'org.apache.commons:commons-compress:1.28.0'
129
  implementation "org.apache.commons:commons-vfs2:2.10.0"
130
  implementation 'org.codehaus.plexus:plexus-utils:4.0.2'
131
  implementation 'org.renjin:renjin-script-engine:3.5-beta76'
132
  implementation 'org.renjin.cran:rjson:0.2.15-renjin-21'
133
134
  // SVG
135
  implementation "io.sf.carte:echosvg-awt-util:${v_echosvg}"
136
  implementation "io.sf.carte:echosvg-bridge:${v_echosvg}"
137
  implementation "io.sf.carte:echosvg-css:${v_echosvg}"
138
  implementation "io.sf.carte:echosvg-dom:${v_echosvg}"
139
  implementation "io.sf.carte:echosvg-ext:${v_echosvg}"
140
  implementation "io.sf.carte:echosvg-gvt:${v_echosvg}"
141
  implementation "io.sf.carte:echosvg-parser:${v_echosvg}"
142
  implementation "io.sf.carte:echosvg-svg-dom:${v_echosvg}"
143
  implementation "io.sf.carte:echosvg-svggen:${v_echosvg}"
144
  implementation "io.sf.carte:echosvg-transcoder:${v_echosvg}"
145
  implementation "io.sf.carte:echosvg-util:${v_echosvg}"
146
  implementation "io.sf.carte:echosvg-xml:${v_echosvg}"
147
148
  // Misc.
149
  implementation 'org.ahocorasick:ahocorasick:0.6.3'
150
  implementation 'com.github.albfernandez:juniversalchardet:2.5.0'
151
  implementation 'jakarta.validation:jakarta.validation-api:3.1.0'
152
  implementation 'org.greenrobot:eventbus-java:3.3.1'
153
154
  // Logging.
155
  implementation 'org.slf4j:slf4j-api:2.1.0-alpha1'
156
  implementation 'org.slf4j:slf4j-nop:2.0.16'
157
158
  // Command-line parsing
159
  implementation "info.picocli:picocli:${v_picocli}"
160
  annotationProcessor "info.picocli:picocli-codegen:${v_picocli}"
161
162
  // KeenQuotes, KeenType, KeenSpell, KeenCount.
163
  implementation fileTree( include: [ '**/*.jar' ], dir: 'libs' )
164
165
  def fx = [ 'controls', 'graphics', 'fxml', 'swing' ]
166
167
  fx.each { fxitem ->
168
    os.each { ositem ->
169
      runtimeOnly "org.openjfx:javafx-${fxitem}:${javafx.version}:${ositem}"
170
    }
171
  }
172
173
  testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${v_junit}"
174
  testRuntimeOnly "org.junit.platform:junit-platform-engine:${v_platform}"
175
  testRuntimeOnly "org.junit.platform:junit-platform-launcher:${v_platform}"
176
177
  testImplementation 'org.junit.jupiter:junit-jupiter:${v_junit}'
178
  testImplementation "org.junit.jupiter:junit-jupiter-api:${v_junit}"
179
  testImplementation "org.junit.jupiter:junit-jupiter-params:${v_junit}"
180
  testImplementation 'org.testfx:testfx-junit5:4.0.18'
181
  testImplementation 'org.assertj:assertj-core:3.27.4'
182
}
183
184
/**
185
 * Mozilla's JavaScript engine, Rhino, is used by the SVG rasterizer.
186
 * The application does not support animated SVG files (the HTML
187
 * renderer won't honour them), so we can eliminate the dependency from
188
 * the binaries.
189
 */
190
configurations {
191
  all*.exclude group: 'org.mozilla', module: 'rhino'
192
}
193
194
sourceSets {
195
  main {
196
    java {
197
      srcDirs 'src/main'
198
    }
199
  }
200
201
  test {
202
    java {
203
      srcDirs 'src/test'
204
    }
205
  }
206
}
207
208
final resourceDir = sourceSets.main.resources.srcDirs[0]
209
final Properties config = new Properties()
210
final File configFile = file( "${resourceDir}/bootstrap.properties" )
211
final FileInputStream configStream = new FileInputStream( configFile )
212
config.load( configStream )
213
configStream.close()
214
215
final String applicationName = config.get( 'application.title' ).toString().toLowerCase()
216
final String applicationPackage = "com.${applicationName}"
217
final String applicationClass = "${applicationPackage}.Launcher"
218
219
compileJava {
220
  options.compilerArgs += [
221
    '-Xlint:unchecked',
222
    '-Xlint:deprecation',
223
    "-Aproject=${applicationPackage}/${applicationName}"
224
  ]
225
}
226
227
application {
228
  mainClass.set( applicationClass )
229
  applicationDefaultJvmArgs = moduleSecurity
230
}
231
232
version = gitVersion()
233
234
final File p = new File( "${resourceDir}/com/${applicationName}/app.properties" )
235
p.write( "application.version=${version}" )
236
237
jar {
238
  duplicatesStrategy = DuplicatesStrategy.EXCLUDE
239
240
  doFirst {
241
    manifest {
242
      attributes 'Main-Class': applicationClass
243
    }
244
  }
245
246
  from {
247
    (configurations.runtimeClasspath.findAll { !it.path.endsWith( '.pom' ) })
248
      .collect { it.isDirectory() ? it : zipTree( it ) }
249
  }
250
251
  archiveFileName = "${applicationName}.jar"
252
253
  exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
254
}
255
256
distributions {
257
  main {
258
    distributionBaseName.set( applicationName )
259
260
    contents {
261
      from { [ 'LICENSE.md', 'README.md' ] }
262
      into( 'images' ) {
263
        from { 'images' }
264
      }
265
    }
266
  }
267
}
268
269
test {
270
  useJUnitPlatform()
271
272
  doFirst { jvmArgs += moduleSecurity }
273
  testLogging { exceptionFormat = 'full' }
274
}
275
276
tasks.withType( JavaCompile ).configureEach {
277
  options.encoding = 'UTF-8'
278
}
1279
A container/.gitignore
1
host-path.txt
12
A container/Containerfile
1
# ########################################################################
2
#
3
# Copyright 2022 White Magic Software, Ltd.
4
#
5
# Creates a container image that can run ConTeXt to typeset documents.
6
#
7
# ########################################################################
8
FROM alpine:latest
9
10
LABEL org.opencontainers.image.description Configures a typesetting system.
11
12
ENV ENV="/etc/profile"
13
ENV PROFILE=/etc/profile
14
15
ENV INSTALL_DIR=/opt
16
ENV SOURCE_DIR=/root/source
17
ENV TARGET_DIR=/root/target
18
ENV IMAGES_DIR=/root/images
19
ENV THEMES_DIR=/root/themes
20
ENV CACHES_DIR=/root/caches
21
ENV FONTS_DIR=/usr/share/fonts/user
22
ENV DOWNLOAD_DIR=/root
23
24
ENV CONTEXT_HOME=$INSTALL_DIR/context
25
ENV CONTEXT_ARCH=linuxmusl-64
26
27
# ########################################################################
28
#
29
# Download all required dependencies
30
#
31
# ########################################################################
32
WORKDIR $DOWNLOAD_DIR
33
34
# Many fonts may be downloaded using Google's download URL. Example:
35
# https://fonts.google.com/download?family=Roboto%20Mono
36
37
# Fonts are repacked with minimal file set, flat directory, and license.
38
ADD "https://fonts.keenwrite.com/download/andada-pro.zip" ./
39
ADD "https://fonts.keenwrite.com/download/archivo-narrow.zip" ./
40
ADD "https://fonts.keenwrite.com/download/carlito.zip" ./
41
ADD "https://fonts.keenwrite.com/download/courier-prime.zip" ./
42
ADD "https://fonts.keenwrite.com/download/inconsolata.zip" ./
43
ADD "https://fonts.keenwrite.com/download/libre-baskerville.zip" ./
44
ADD "https://fonts.keenwrite.com/download/niconne.zip" ./
45
ADD "https://fonts.keenwrite.com/download/nunito.zip" ./
46
ADD "https://fonts.keenwrite.com/download/open-sans-emoji.zip" ./
47
ADD "https://fonts.keenwrite.com/download/pt-mono.zip" ./
48
ADD "https://fonts.keenwrite.com/download/pt-sans.zip" ./
49
ADD "https://fonts.keenwrite.com/download/pt-serif.zip" ./
50
ADD "https://fonts.keenwrite.com/download/roboto.zip" ./
51
ADD "https://fonts.keenwrite.com/download/roboto-mono.zip" ./
52
ADD "https://fonts.keenwrite.com/download/source-serif-4.zip" ./
53
ADD "https://fonts.keenwrite.com/download/underwood.zip" ./
54
55
# Typesetting software
56
ADD "http://lmtx.pragma-ade.nl/install-lmtx/context-$CONTEXT_ARCH.zip" "context.zip"
57
58
# ########################################################################
59
#
60
# Install components, modules, configure system, remove unnecessary files
61
#
62
# ########################################################################
63
WORKDIR $CONTEXT_HOME
64
65
RUN \
66
  apk update && \
67
  apk add -t py3-cssselect && \
68
  apk add -t py3-lxml && \
69
  apk add -t py3-numpy && \
70
  apk --update --no-cache \
71
    add ca-certificates curl fontconfig rsync
72
73
RUN apk --update --no-cache add inkscape
74
75
RUN \
76
  mkdir -p \
77
    "$FONTS_DIR" \
78
    "$INSTALL_DIR" \
79
    "$TARGET_DIR" \
80
    "$SOURCE_DIR" \
81
    "$THEMES_DIR" \
82
    "$IMAGES_DIR" \
83
    "$CACHES_DIR" && \
84
  echo "export CONTEXT_HOME=\"$CONTEXT_HOME\"" >> $PROFILE && \
85
  echo "export PATH=\"\$PATH:\$CONTEXT_HOME/tex/texmf-$CONTEXT_ARCH/bin\"" >> $PROFILE && \
86
  echo "export OSFONTDIR=\"/usr/share/fonts//\"" >> $PROFILE && \
87
  echo "PS1='\\u@typesetter:\\w\\$ '" >> $PROFILE && \
88
  unzip -d $CONTEXT_HOME $DOWNLOAD_DIR/context.zip && \
89
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/andada-pro.zip "*.otf" && \
90
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/archivo-narrow.zip "*.otf" && \
91
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/carlito.zip "*.ttf" && \
92
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/courier-prime.zip "*.ttf" && \
93
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/libre-baskerville.zip "*.ttf" && \
94
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/inconsolata.zip "*.ttf" && \
95
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/niconne.zip "*.ttf" && \
96
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/nunito.zip "*.ttf" && \
97
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/open-sans-emoji.zip "*.ttf" && \
98
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/pt-mono.zip "*.ttf" && \
99
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/pt-sans.zip "*.ttf" && \
100
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/pt-serif.zip "*.ttf" && \
101
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/roboto.zip "*.ttf" && \
102
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/roboto-mono.zip "*.ttf" && \
103
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/source-serif-4.zip "*.otf" && \
104
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/underwood.zip "*.ttf" && \
105
  fc-cache -f -v && \
106
  mkdir -p tex && \
107
  sh install.sh && \
108
  rm -rf \
109
    "modules" \
110
    "/var/cache" \
111
    "/usr/share/icons" \
112
    "/opt/context/tex/texmf-context/source" \
113
    $DOWNLOAD_DIR/*.zip \
114
    $CONTEXT_HOME/tex/texmf-modules/doc \
115
    $CONTEXT_HOME/tex/texmf-context/doc && \
116
  mkdir -p $CONTEXT_HOME/tex/texmf-fonts/tex/context/user && \
117
  ln -s $CONTEXT_HOME/tex/texmf-fonts/tex/context/user $HOME/fonts && \
118
  source $PROFILE \
119
  mtxrun --generate && \
120
  find \
121
    /usr/share/inkscape \
122
    -type f -not -iname \*.xml -exec rm {} \; && \
123
  find \
124
    $CONTEXT_HOME \
125
    -type f \
126
      \( -iname \*.pdf -o -iname \*.txt -o -iname \*.log \) \
127
    -exec rm {} \;
128
129
# ########################################################################
130
#
131
# Ensure login goes to the target directory. ConTeXt prefers to export to
132
# the current working directory.
133
#
134
# ########################################################################
135
WORKDIR $TARGET_DIR
136
1137
A container/README.md
1
# Overview
2
3
This document describes how to maintain the containerized typesetting system.
4
The container is built locally then deployed to a web server capable of
5
serving static web pages.
6
7
## Installation wizard
8
9
The installation wizard is responsible for installing the containerization
10
software and the container image. The container manager class loads the
11
image from a URL. That URL is defined in the `messages.properties` file.
12
13
# Upgrade
14
15
## Themes
16
17
If changes have been made to the themes, upgrade them as follows:
18
19
1. Change to the themes repository directory.
20
21
    cd $HOME/dev/java/keenwrite/themes
22
23
1. Tag the themes, such as:
24
25
    git tag -a 1.11.0 -m "message"
26
    git push --tags
27
28
1. Ensure the personal access token exists:
29
30
    test -f tokens/release.pat && echo "exists"
31
32
1. Create the release and release notes via the web.
33
34
1. Run the release script to upload the release akchive:
35
36
    ./release.sh
37
38
1. Edit `src/main/resources/com/keenwrite/messages.properties`.
39
1. Set `Wizard.typesetter.themes.version` to the version.
40
1. Set `Wizard.typesetter.themes.checksum` to the checksum.
41
42
The themes are released.
43
44
## Container
45
46
Upgrade the containerization software (e.g., podman or docker) as follows:
47
48
1. Download the latest container version.
49
50
    wget -q $(\
51
      wget \
52
      -q -O- \
53
      https://api.github.com/repos/containers/podman/releases/latest | \
54
      jq \
55
      -r '.assets[] | select(.name | contains("exe")) | .browser_download_url')
56
57
1. Compute the SHA:
58
59
    sha256sum *exe | cut -f1 -d' '
60
61
1. Edit `src/main/resources/com/keenwrite/messages.properties`.
62
1. Set `Wizard.typesetter.container.version` to the new container version.
63
1. Set `Wizard.typesetter.container.checksum` to the Windows version checksum.
64
1. Set `Wizard.typesetter.container.image.version` to the new image version.
65
1. Save the file.
66
67
The containerization software version is changed.
68
69
# Publish
70
71
Building the container will pull from the container version in the properties
72
file. Ensure that a personal access token (`token.txt`) exists, then publish
73
the changes to the container image as follows:
74
75
``` bash
76
./manage.sh --verbose --delete --build --export --publish
77
```
78
79
The container image is published.
80
181
A container/manage.sh
1
#!/usr/bin/env bash
2
3
# ---------------------------------------------------------------------------
4
# Copyright 2022 White Magic Software, Ltd.
5
#
6
# This script manages the container configured to run ConTeXt.
7
# ---------------------------------------------------------------------------
8
9
set -euo pipefail  # Exit on error, undefined vars, and pipe failures
10
11
source ../scripts/build-template
12
13
# Reads the value of a property from a properties file.
14
#
15
# $1 - The key name to obtain.
16
function property {
17
  if [[ ! -f "${PROPERTIES}" ]]; then
18
    echo "Error: Properties file ${PROPERTIES} not found" >&2
19
    exit 1
20
  fi
21
  grep "^${1}" "${PROPERTIES}" | cut -d'=' -f2
22
}
23
24
readonly BUILD_DIR=build
25
readonly PROPERTIES="${SCRIPT_DIR}/../src/main/resources/com/keenwrite/messages.properties"
26
27
readonly CONTAINER_EXE=podman
28
readonly CONTAINER_SHORTNAME=$(property Wizard.typesetter.container.image.name)
29
readonly CONTAINER_VERSION=$(property Wizard.typesetter.container.image.version)
30
readonly CONTAINER_NETWORK=host
31
readonly CONTAINER_IMAGE_FILE="${CONTAINER_SHORTNAME}-${CONTAINER_VERSION}"
32
readonly CONTAINER_IMAGE_PATH="${BUILD_DIR}/${CONTAINER_IMAGE_FILE}"
33
readonly CONTAINER_ARCHIVE_FILE="${CONTAINER_IMAGE_FILE}.tar"
34
readonly CONTAINER_ARCHIVE_PATH="${BUILD_DIR}/${CONTAINER_ARCHIVE_FILE}"
35
readonly CONTAINER_COMPRESSED_FILE="${CONTAINER_ARCHIVE_FILE}.gz"
36
readonly CONTAINER_COMPRESSED_PATH="${BUILD_DIR}/${CONTAINER_COMPRESSED_FILE}"
37
readonly CONTAINER_DIR_SOURCE="/root/source"
38
readonly CONTAINER_DIR_TARGET="/root/target"
39
readonly CONTAINER_DIR_IMAGES="/root/images"
40
readonly CONTAINER_DIR_FONTS="/root/fonts"
41
42
ARG_CONTAINER_NAME="${CONTAINER_SHORTNAME}:${CONTAINER_VERSION}"
43
ARG_CONTAINER_COMMAND="context --version"
44
ARG_MOUNTPOINT_SOURCE=""
45
ARG_MOUNTPOINT_TARGET="."
46
ARG_MOUNTPOINT_IMAGES=""
47
ARG_MOUNTPOINT_FONTS="${HOME}/.fonts"
48
49
DEPENDENCIES=(
50
  "podman,https://podman.io"
51
  "tar,https://www.gnu.org/software/tar"
52
  "bzip2,https://gitlab.com/bzip2/bzip2"
53
)
54
55
ARGUMENTS+=(
56
  "b,build,Build container"
57
  "c,connect,Connect to container"
58
  "d,delete,Remove all containers"
59
  "s,source,Set mount point for input document (before typesetting)"
60
  "t,target,Set mount point for output file (after typesetting)"
61
  "i,images,Set mount point for image files (to typeset)"
62
  "f,fonts,Set mount point for font files (during typesetting)"
63
  "l,load,Load container (${CONTAINER_COMPRESSED_PATH})"
64
  "p,publish,Publish the container"
65
  "r,run,Run a command in the container (\"${ARG_CONTAINER_COMMAND}\")"
66
  "x,export,Save container (${CONTAINER_COMPRESSED_PATH})"
67
)
68
69
# ---------------------------------------------------------------------------
70
# Manages the container.
71
# ---------------------------------------------------------------------------
72
execute() {
73
  ${do_delete}
74
  ${do_build}
75
  ${do_export}
76
  ${do_publish}
77
  ${do_load}
78
  ${do_execute}
79
  ${do_connect}
80
81
  return 1
82
}
83
84
# ---------------------------------------------------------------------------
85
# Deletes all containers.
86
# ---------------------------------------------------------------------------
87
utile_delete() {
88
  [[ -f ${CONTAINER_IMAGE_PATH} ]] && \
89
    $log "Deleting ${CONTAINER_IMAGE_PATH}"; \
90
    rm -f "${CONTAINER_IMAGE_PATH}"
91
92
  $log "Deleting all containers"
93
94
  ${CONTAINER_EXE} rmi --all --force > /dev/null || true
95
96
  $log "Containers deleted"
97
}
98
99
# ---------------------------------------------------------------------------
100
# Builds the container file in the current working directory.
101
# ---------------------------------------------------------------------------
102
utile_build() {
103
  $log "Building container version ${CONTAINER_VERSION}"
104
105
  mkdir -p "${ARG_MOUNTPOINT_FONTS}"
106
107
  # Show what commands are run while building, but not the commands' output.
108
  ${CONTAINER_EXE} build \
109
    --network="${CONTAINER_NETWORK}" \
110
    --squash \
111
    -t "${ARG_CONTAINER_NAME}" . | \
112
  grep ^STEP || true
113
}
114
115
# ---------------------------------------------------------------------------
116
# Publishes the container to the repository.
117
# ---------------------------------------------------------------------------
118
utile_publish() {
119
  local -r HOST_PATH="host-path.txt"
120
121
  if [[ -f "${HOST_PATH}" ]]; then
122
    local -r repository=$(cat "${HOST_PATH}")
123
    local -r remote_file="${CONTAINER_SHORTNAME}:${CONTAINER_VERSION}"
124
    local -r remote_path="${repository}/${remote_file}"
125
126
    $log "Publishing ${CONTAINER_IMAGE_PATH} to ${remote_path}"
127
128
    # Path to the repository.
129
    scp -q "${CONTAINER_IMAGE_PATH}" "${remote_path}"
130
  else
131
    error "Create ${HOST_PATH} with path on remote host"
132
  fi
133
}
134
135
# ---------------------------------------------------------------------------
136
# Creates the command-line option for a read-only mountpoint.
137
#
138
# $1 - The host directory.
139
# $2 - The guest (container) directory.
140
# $3 - The file system permissions (set to 1 for read-write).
141
# ---------------------------------------------------------------------------
142
get_mountpoint() {
143
  local result=""
144
  local binding="ro"
145
146
  if [[ -n "${3:-}" ]]; then
147
    binding="Z"
148
  fi
149
150
  if [[ -n "${1:-}" ]]; then
151
    result="-v ${1}:${2}:${binding}"
152
  fi
153
154
  echo "${result}"
155
}
156
157
get_mountpoint_source() {
158
  echo "$(get_mountpoint "${ARG_MOUNTPOINT_SOURCE}" "${CONTAINER_DIR_SOURCE}")"
159
}
160
161
get_mountpoint_target() {
162
  echo "$(get_mountpoint "${ARG_MOUNTPOINT_TARGET}" "${CONTAINER_DIR_TARGET}" 1)"
163
}
164
165
get_mountpoint_images() {
166
  echo "$(get_mountpoint "${ARG_MOUNTPOINT_IMAGES}" "${CONTAINER_DIR_IMAGES}")"
167
}
168
169
get_mountpoint_fonts() {
170
  echo "$(get_mountpoint "${ARG_MOUNTPOINT_FONTS}" "${CONTAINER_DIR_FONTS}")"
171
}
172
173
# ---------------------------------------------------------------------------
174
# Connects to the container.
175
# ---------------------------------------------------------------------------
176
utile_connect() {
177
  $log "Connecting to container"
178
179
  local mount_source
180
  local mount_target
181
  local mount_images
182
  local mount_fonts
183
184
  mount_source=$(get_mountpoint_source)
185
  mount_target=$(get_mountpoint_target)
186
  mount_images=$(get_mountpoint_images)
187
  mount_fonts=$(get_mountpoint_fonts)
188
189
  $log "mount_source = '${mount_source}'"
190
  $log "mount_target = '${mount_target}'"
191
  $log "mount_images = '${mount_images}'"
192
  $log "mount_fonts  = '${mount_fonts}'"
193
194
  # Use array to properly handle empty mount options
195
  local mount_args=()
196
  [[ -n "${mount_source}" ]] && mount_args+=(${mount_source})
197
  [[ -n "${mount_target}" ]] && mount_args+=(${mount_target})
198
  [[ -n "${mount_images}" ]] && mount_args+=(${mount_images})
199
  [[ -n "${mount_fonts}"  ]] && mount_args+=(${mount_fonts})
200
201
  # Ensure directories exist and log creation
202
  for mount in "${mount_args[@]}"; do
203
    # Extract host path from mount string (format: -v /host:/container)
204
    host_path=$(echo "${mount}" | sed 's/^-v \([^:]*\):.*$/\1/')
205
206
    [[ ! -d "$host_path" ]] && \
207
      $log "Create directory: $host_path" && \
208
      mkdir -p "$host_path"
209
  done
210
211
  ${CONTAINER_EXE} run \
212
    --network="${CONTAINER_NETWORK}" \
213
    --rm \
214
    -it \
215
    "${mount_args[@]}" \
216
    "${ARG_CONTAINER_NAME}"
217
}
218
219
# ---------------------------------------------------------------------------
220
# Runs a command in the container.
221
#
222
# Examples:
223
#
224
#   ./manage.sh -r "ls /"
225
#   ./manage.sh -r "context --version"
226
# ---------------------------------------------------------------------------
227
utile_execute() {
228
  $log "Running \"${ARG_CONTAINER_COMMAND}\":"
229
230
  ${CONTAINER_EXE} run \
231
    --network="${CONTAINER_NETWORK}" \
232
    --rm \
233
    -it \
234
    "${ARG_CONTAINER_NAME}" \
235
    /bin/sh --login -c "${ARG_CONTAINER_COMMAND}"
236
}
237
238
# ---------------------------------------------------------------------------
239
# Saves the container to a file.
240
# ---------------------------------------------------------------------------
241
utile_export() {
242
  if [[ -f "${CONTAINER_COMPRESSED_PATH}" ]]; then
243
    warning "${CONTAINER_COMPRESSED_PATH} exists, delete before saving."
244
  else
245
    $log "Saving ${CONTAINER_SHORTNAME} image"
246
247
    mkdir -p "${BUILD_DIR}"
248
249
    ${CONTAINER_EXE} save \
250
      --quiet \
251
      -o "${CONTAINER_ARCHIVE_PATH}" \
252
      "${ARG_CONTAINER_NAME}"
253
254
    $log "Compressing to ${CONTAINER_COMPRESSED_PATH}"
255
    gzip "${CONTAINER_ARCHIVE_PATH}"
256
257
    $log "Renaming to ${CONTAINER_IMAGE_PATH}"
258
    mv "${CONTAINER_COMPRESSED_PATH}" "${CONTAINER_IMAGE_PATH}"
259
260
    $log "Saved ${CONTAINER_IMAGE_PATH} image"
261
  fi
262
}
263
264
# ---------------------------------------------------------------------------
265
# Loads the container from a file.
266
# ---------------------------------------------------------------------------
267
utile_load() {
268
  if [[ -f "${CONTAINER_COMPRESSED_PATH}" ]]; then
269
    $log "Loading ${CONTAINER_SHORTNAME} from ${CONTAINER_COMPRESSED_PATH}"
270
271
    ${CONTAINER_EXE} load \
272
      --quiet \
273
      -i "${CONTAINER_COMPRESSED_PATH}"
274
275
    $log "Loaded ${CONTAINER_SHORTNAME} image"
276
  else
277
    warning "Missing ${CONTAINER_COMPRESSED_PATH}; use build followed by save"
278
  fi
279
}
280
281
argument() {
282
  local consume=1
283
284
  case "$1" in
285
    -b|--build)
286
    do_build=utile_build
287
    ;;
288
    -c|--connect)
289
    do_connect=utile_connect
290
    ;;
291
    -z|--delete)
292
    do_delete=utile_delete
293
    ;;
294
    -l|--load)
295
    do_load=utile_load
296
    ;;
297
    -i|--images)
298
    if [[ -n "${2:-}" ]]; then
299
      ARG_MOUNTPOINT_IMAGES="$2"
300
      consume=2
301
    fi
302
    ;;
303
    -t|--target)
304
    if [[ -n "${2:-}" ]]; then
305
      ARG_MOUNTPOINT_TARGET="$2"
306
      consume=2
307
    fi
308
    ;;
309
    -p|--publish)
310
    do_publish=utile_publish
311
    ;;
312
    -r|--run)
313
    do_execute=utile_execute
314
315
    if [[ -n "${2:-}" ]]; then
316
      ARG_CONTAINER_COMMAND="$2"
317
      consume=2
318
    fi
319
    ;;
320
    -s|--source)
321
    if [[ -n "${2:-}" ]]; then
322
      ARG_MOUNTPOINT_SOURCE="$2"
323
      consume=2
324
    fi
325
    ;;
326
    -x|--export)
327
    do_export=utile_export
328
    ;;
329
  esac
330
331
  return ${consume}
332
}
333
334
do_build=:
335
do_connect=:
336
do_delete=:
337
do_execute=:
338
do_load=:
339
do_publish=:
340
do_export=:
341
342
main "$@"
1343
A docs/README.md
1
# Documentation
2
3
The following documents have additional details about using the editor:
4
5
* [cmd.md](cmd.md) -- Command-line argument usage
6
* [div.md](div.md) -- Syntax for annotated text (fenced divs)
7
* [i18n.md](i18n.md) -- Internationalization features
8
* [r.md](r.md) -- R functions within R Markdown documents
9
* [samples](samples) -- Example documents
10
* [skins.md](skins.md) -- User interface customization
11
* [svg.md](svg.md) -- Resolve issues with some SVG files
12
* [metadata.md](metadata.md) -- Document metadata
13
* [references.md](references.md) -- Captions and cross-references
14
* [typesetting.md](typesetting.md) -- Document typesetting
15
* [variables.md](variables.md) -- Variable definitions and interpolation
16
17
# Contributions
18
19
* [credits.md](credits.md) -- Thanks to authors of contributing projects
20
* [licenses](licenses) -- Third-party licenses
21
122
A docs/cmd.md
1
# Command-line arguments
2
3
The application may be run from the command-line to convert Markdown and
4
R Markdown files to a variety of output formats. Without specifying any
5
command-line arguments, the application will launch a graphical user interface.
6
7
## Common arguments
8
9
The most common command-line arguments to use include:
10
11
* `-h` -- displays all command-line arguments, then exits.
12
* `-i` -- sets the input file name, must be a full path.
13
* `-o` -- sets the output file name, can be a relative path.
14
* `-s` -- sets a variable name and value at build time (dynamic data).
15
16
## Example usage
17
18
On Linux, simple usages include:
19
20
    keenwrite.bin -i $HOME/document/01.md -o document.xhtml
21
22
    keenwrite.bin -i $HOME/document/01.md -o document.md \
23
      -v $HOME/document/variables.yaml
24
25
That command will convert `01.md` into the respective file formats. In
26
the first case, it will become an HTML page. In the second case, it will
27
become a Markdown document with all variables interpolated and replaced.
28
29
A more complex example follows:
30
31
    keenwrite.bin -i $HOME/document/01.Rmd -o document.pdf \
32
      --image-dir=$HOME/document/images -v $HOME/document/variables.yaml \
33
      --metadata="title={{book.title}}" --metadata="author={{book.author}}" \
34
      --r-dir=$HOME/document/r --r-script=$HOME/document/r/bootstrap.R \
35
      --theme-dir=$HOME/document/themes/boschet
36
37
That command will convert `01.Rmd` to `document.pdf` and replace the metadata
38
using values from the variable definitions file.
39
40
Directory names containing spaces must be quoted. For example, on Windows:
41
42
    keenwrite.bin -i "C:\Users\My Documents\01.Rmd" -o document.pdf
43
144
A docs/credits.md
1
# Credits
2
3
Using libraries from:
4
5
* Tomas Mikula: [RichTextFX](https://github.com/TomasMikula/RichTextFX), [WellBehavedFX](https://github.com/TomasMikula/WellBehavedFX), [Flowless](https://github.com/TomasMikula/Flowless), and [UndoFX](https://github.com/TomasMikula/UndoFX)
6
* Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx)
7
* Dieter Holz: [PreferencesFX](https://github.com/dlsc-software-consulting-gmbh/PreferencesFX)
8
* David Croft: [File Preferences](http://www.davidc.net/programming/java/java-preferences-using-file-backing-store)
9
* Alex Bertram: [Renjin](https://www.renjin.org/)
10
* Vladimir Schneider: [flexmark](https://github.com/vsch/flexmark-java)
11
* Alberto Fernández, Shy Shalom, Kohei Taketa: [juniversalchardet](https://github.com/takscape/juniversalchardet)
12
* Morten Nobel-Jørgensen: [Java Image Scaling](https://github.com/mortennobel/java-image-scaling)
13
14
Inspired by:
15
16
* Karl Tauber: [Markdown Writer FX](https://github.com/JFormDesigner/markdown-writer-fx)
117
A docs/development/proguard/README.md
1
From https://github.com/greenrobot/EventBus#r8-proguard
2
3
	-keepattributes *Annotation*
4
	-keepclassmembers class * {
5
			@org.greenrobot.eventbus.Subscribe <methods>;
6
	}
7
	-keep enum org.greenrobot.eventbus.ThreadMode { *; }
8
	 
19
A docs/diagram.md
1
# Introduction
2
3
From a high level, the application architecture for converting Markdown documents is captured in the following figure:
4
5
``` diagram-graphviz
6
digraph {
7
  node [fontname = "Noto Sans" fontsize=6 height=.25 penwidth=.5];
8
  edge [fontname = "Noto Sans" fontsize=6  penwidth=.5 arrowsize=.5];
9
  node [shape=box color="{{keenwrite.palette.primary.light}}" fontcolor="{{keenwrite.palette.primary.dark}}"]
10
  edge [color="{{keenwrite.palette.grayscale.light}}" fontcolor="{{keenwrite.palette.grayscale.dark}}"]
11
12
  {{keenwrite.classes.processors.variable.definition}} ->   {{keenwrite.classes.processors.markdown}} [xlabel="{{keenwrite.graph.label.chain.next}}  "]
13
  {{keenwrite.classes.processors.markdown}} -> {{keenwrite.classes.processors.preview}} [xlabel="{{keenwrite.graph.label.chain.next}}  "]
14
  {{keenwrite.classes.processors.markdown}} -> Extensions [label="  contains"]
15
16
Extensions -> FencedBlockExtension
17
Extensions -> CaretExtension
18
Extensions -> ImageLinkExtension
19
Extensions -> TeXExtension
20
}
21
```
22
23
An extension is an addition to the Markdown parser, flexmark-java, that is used when converting the document's abstract syntax tree into an HTML document. The {{keenwrite.classes.processors.markdown}} contains both prepackaged and custom extensions.
124
A docs/diagram.yaml
1
---
2
keenwrite:
3
  classes:
4
    processors:
5
      markdown: MarkdownProcessor
6
      variable:
7
        definition: DefinitionProcessor
8
      preview: PreviewProcessor
9
  palette:
10
    primary:
11
      light: '#51a9cf'
12
      dark: '#126d95'
13
    secondary:
14
      light: '#ec706a'
15
      dark: '#7e252f'
16
    accent:
17
      light: '#76A786'
18
      dark: '#385742'
19
    grayscale:
20
      light: '#bac2c5'
21
      dark: '#394343'
22
  graph:
23
    label:
24
      chain:
25
        next: successor
126
A docs/div.md
1
# Fenced divs
2
3
This section describes the syntax to generate HTML `div` elements. The
4
syntax is known as a _fenced div_.
5
6
# Basic syntax
7
8
A fenced div has the following basic syntax:
9
10
``` markdown
11
::: name
12
Content
13
:::
14
```
15
16
To start a fenced div, begin a line with at least three colons (`:::`),
17
followed by at least one space, followed by any word. Content may follow
18
immediately on the next line. Terminate the fenced div with at least
19
three colons. The terminating colons needn't match in number to the starting
20
colons, but it's a good idea to maintain symmetry.
21
22
The HTML that is generated from the above fenced div will resemble:
23
24
``` html
25
<div class="name">
26
<p>Content</p>
27
</div>
28
```
29
30
# Extended syntax
31
32
A fenced div may use an extended syntax. The extended syntax can provide
33
a unique identifier, multiple class names, and key/value data pairs. For
34
example:
35
36
``` markdown
37
::: {#poem-01 .stanza author="Emily Dickinson" year=1890}
38
Because I could not stop for Death —
39
He kindly stopped for me —
40
The Carriage held but just Ourselves —
41
And Immortality.
42
:::
43
```
44
45
The above snippet produces:
46
47
``` html
48
<div id="poem-01" class="stanza" data-author="Emily Dickinson" data-year="1890">
49
<p>Because I could not stop for Death —
50
He kindly stopped for me —
51
The Carriage held but just Ourselves —
52
And Immortality.</p>
53
</div>
54
```
55
56
Note that when using the extended syntax, class styles must be prefixed with
57
a period (e.g., `.stanza` in the example).
58
59
# Nested syntax
60
61
Fenced divs may be nested, such as in the following example:
62
63
``` markdown
64
::: poem
65
:::::: stanza
66
Because I could not stop for Death —
67
He kindly stopped for me —
68
The Carriage held but just Ourselves —
69
And Immortality.
70
::::::
71
:::
72
```
73
74
The above example produces:
75
76
``` html
77
<div class="poem"><div class="stanza">
78
<p>Because I could not stop for Death —
79
He kindly stopped for me —
80
The Carriage held but just Ourselves —
81
And Immortality.</p>
82
</div></div>
83
```
84
185
A docs/i18n.md
1
# Internationalization
2
3
The application supports internationalization (I18N). There are multiple
4
components to editing and previewing internationalized text documents.
5
These include:
6
7
* Fonts
8
* Language
9
10
Both fonts and language must be set for non-Latin-based text.
11
12
# Fonts
13
14
The text editors and preview panel have independent font settings. For
15
all Chinese, Japanese, and Korean (CJK) fonts, you may have to type in
16
the font family name directly.
17
18
For example, CJK font families for the editor have the following names:
19
20
* **Noto Sans CJK KR** --- Korean font
21
* **Noto Sans CJK JP** --- Japanese font
22
* **Noto Sans CJK HN** --- Chinese font
23
* **Noto Sans CJK SC** --- Simplified Chinese font
24
25
While CJK font families for the preview have the following names:
26
27
* **Noto Serif CJK KR** --- Korean font
28
* **Noto Serif CJK JP** --- Japanese font
29
* **Noto Serif CJK HN** --- Chinese font
30
* **Noto Serif CJK SC** --- Simplified Chinese font
31
32
## Editor
33
34
Complete the following steps to change the editor font:
35
36
1. Click **Edit → Preferences**.
37
1. Click **Fonts**.
38
1. Click **Change** under **Editor Font**.
39
1. Find the font name by typing or scrolling.
40
1. Click the desired font family.
41
1. Click **OK**.
42
1. Click **Apply**.
43
44
The text editor font is changed.
45
46
Note the following:
47
48
* The font must be installed in the system for this to work.
49
* You may have to edit the font name if it cannot be selected from the list.
50
* Setting the editor font also sets the statistics panel font.
51
52
## Preview
53
54
The preview panel uses regular and monospace fonts.
55
56
### Regular
57
58
Complete the following steps to change the regular preview font:
59
60
1. Click **Edit → Preferences**.
61
1. Click **Fonts**.
62
1. Click **Change** under **Preview Font** for the **Preview pane font name**.
63
1. Find the font name by typing or scrolling.
64
1. Click the desired font family.
65
1. Click **OK**.
66
1. Click **Apply**.
67
68
The regular preview font is changed.
69
70
### Monospace
71
72
Complete the following steps to change the monospace preview font:
73
74
1. Click **Edit → Preferences**.
75
1. Click **Fonts**.
76
1. Click **Change** under **Preview Font** for the **Monospace font name**.
77
1. Find the font name by typing or scrolling.
78
1. Click the desired font family.
79
1. Click **OK**.
80
1. Click **Apply**.
81
82
The monospace font is changed.
83
84
# Language
85
86
Language settings control the locale that the application uses. When using
87
a CJK font, for example, the application must also be instructed to use
88
a particular locale. Change the locale as follows:
89
90
1. Click **Edit → Preferences**.
91
1. Click **Language**.
92
1. Select a value for **Locale**.
93
1. Click **Apply**.
94
95
The language is set.
96
197
A docs/images/app-ide.png
Binary file
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/resolved-text.png
Binary file
A docs/images/screenshots/01.png
Binary file
A docs/images/screenshots/02.png
Binary file
A docs/images/screenshots/03.png
Binary file
A docs/images/screenshots/04.png
Binary file
A docs/images/screenshots/05.png
Binary file
A docs/images/screenshots/06.png
Binary file
A docs/images/screenshots/07.png
Binary file
A docs/images/screenshots/08.png
Binary file
A docs/images/screenshots/09.png
Binary file
A docs/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 docs/licenses/FILE-ICON-IMAGES.md
1
The MIT License (MIT)
2
3
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
19
A docs/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 docs/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 docs/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 docs/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 docs/licenses/JAVA-IMAGE-SCALING.md
1
Java Image Scaling
2
3
Copyright (c) 2013, Morten Nobel-Joergensen
4
All rights reserved.
5
6
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
7
following conditions are met:
8
9
Redistributions of source code must retain the above copyright notice, this list of conditions and the following
10
disclaimer.
11
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
12
disclaimer in the documentation and/or other materials provided with the distribution.
13
Neither the name of the <ORGANIZATION> nor the names of its contributors may be used to endorse or promote products
14
derived from this software without specific prior written permission.
15
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
16
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
18
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
20
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
21
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
122
A docs/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 docs/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
        Alberto Fernández <infjaf@gmail.com>
22
        Shy Shalom <shooshX@gmail.com>
23
        Kohei TAKETA <k-tak@void.in> (Java port)
24
25
Alternatively, the contents of this file may be used under the terms of
26
either the GNU General Public License Version 2 or later (the "GPL"), or
27
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28
in which case the provisions of the GPL or the LGPL are applicable instead
29
of those above. If you wish to allow use of your version of this file only
30
under the terms of either the GPL or the LGPL, and not to allow others to
31
use your version of this file under the terms of the MPL, indicate your
32
decision by deleting the provisions above and replace them with the notice
33
and other provisions required by the GPL or the LGPL. If you do not delete
34
the provisions above, a recipient may use your version of this file under
35
the terms of any one of the MPL, the GPL or the LGPL.
136
A docs/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 docs/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 docs/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 docs/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 docs/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 docs/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 docs/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 docs/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 docs/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 docs/licenses/fonts/SOURCE-SERIF-4.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 docs/logo/font.txt
1
Merriweather Sans ExtraBold Italic
2
3
https://github.com/SorkinType/Merriweather-Sans/blob/master/fonts/otf/MerriweatherSans-ExtraBoldItalic.otf
4
5
Weight 800
6
7
https://fonts.google.com/specimen/Merriweather+Sans
8
19
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
<?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:xlink="http://www.w3.org/1999/xlink"
9
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
10
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
11
   height="197.4767"
12
   viewBox="0 0 695.99768 197.4767"
13
   width="695.99768"
14
   version="1.1"
15
   id="svg37"
16
   sodipodi:docname="new-logo-text.svg"
17
   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
18
  <metadata
19
     id="metadata43">
20
    <rdf:RDF>
21
      <cc:Work
22
         rdf:about="">
23
        <dc:format>image/svg+xml</dc:format>
24
        <dc:type
25
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
26
        <dc:title></dc:title>
27
      </cc:Work>
28
    </rdf:RDF>
29
  </metadata>
30
  <defs
31
     id="defs41">
32
    <linearGradient
33
       id="a"
34
       gradientTransform="matrix(-8.7796153,42.985832,-42.985832,-8.7796153,514.83476,136.06192)"
35
       gradientUnits="userSpaceOnUse"
36
       x1=".152358"
37
       x2=".968809"
38
       y1="-.044912"
39
       y2="-.049471">
40
      <stop
41
         offset="0"
42
         stop-color="#ec706a"
43
         id="stop2" />
44
      <stop
45
         offset="1"
46
         stop-color="#ecd980"
47
         id="stop4" />
48
    </linearGradient>
49
  </defs>
50
  <path
51
     style="fill:url(#a);fill-opacity:1.0;fill-rule:nonzero;stroke:none;stroke-width:1.226;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
52
     paint-order="stroke"
53
     d="m 496.76229,150.80474 c -4.25368,20.68081 3.28191,25.95476 3.28191,25.95476 v 0 c 0,0 3.00963,-13.19543 8.64082,-10.76172 v 0 c 4.83401,2.08299 1.12516,10.97002 1.12516,10.97002 v 0 c 0,0 31.78993,-30.5076 7.60484,-40.99434 v 0 c 0,0 -5.30287,-2.76791 -10.69842,-0.65209 v 0 c -3.94735,1.54891 -7.94375,5.71058 -9.95431,15.48337"
54
     stroke-linecap="round"
55
     id="path14" />
56
  <path
57
     d="m 530.80335,138.63592 -10.99206,-16.95952 1.75995,-6.49966 10.01483,2.71233 z"
58
     fill="#126d95"
59
     id="path9" />
60
  <path
61
     d="m 533.0598,112.36676 -0.91739,3.38458 -9.99361,-2.70665 0.91739,-3.38458 z"
62
     fill="#126d95"
63
     id="path11" />
64
  <g
65
     fill="#51a9cf"
66
     id="g19"
67
     transform="translate(-295.50101,-692.52836)">
68
    <path
69
       d="m 834.01973,741.0381 c -1.68105,0.0185 -3.22054,1.13771 -3.68367,2.84981 -0.56186,2.07405 0.665,4.21099 2.73743,4.77241 l -13.96475,51.52944 -9.99361,-2.70665 c 8.36013,-31.46487 4.99411,-51.98144 4.99411,-51.98144 14.99782,-11.92097 23.67,-25.56577 27.63101,-32.97331 z"
70
       id="path13" />
71
    <path
72
       d="m 818.56767,802.18881 -0.9174,3.38458 -10.03996,-2.72957 0.91314,-3.37522 z"
73
       id="path15" />
74
    <path
75
       d="m 817.07405,807.70594 -1.75995,6.49966 -18.03534,9.08805 9.78412,-18.31044 z"
76
       id="path17" />
77
  </g>
78
  <path
79
     d="m 540.69709,49.12083 7.72577,-28.52932 c -0.3195,8.40427 0.28451,24.55036 7.21678,42.41047 0,0 -11.89603,16.50235 -21.99788,47.3763 l -10.03442,-2.71758 13.96533,-51.5284 c 2.08221,0.56405 4.21039,-0.66603 4.77182,-2.73844 0.45427,-1.67248 -0.26571,-3.38317 -1.64739,-4.27302"
80
     fill="#126d95"
81
     id="path21" />
82
  <text
83
     transform="translate(-295.73751 -689.6407)"
84
     id="text25" />
85
  <g
86
     style="font-style:italic;font-weight:800;font-size:133.333;font-family:Merriweather Sans;letter-spacing:0;word-spacing:0;fill:#51a9cf"
87
     id="g35">
88
    <text
89
       x="16.133343"
90
       y="130.6234"
91
       id="text29"><tspan
92
         x="16.133343"
93
         y="130.6234"
94
         id="tspan27">KeenWr</tspan></text>
95
    <text
96
       x="552.53137"
97
       y="130.6234"
98
       id="text33"><tspan
99
         x="552.53137"
100
         y="130.6234"
101
         id="tspan31">te</tspan></text>
102
  </g>
103
</svg>
1104
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/logo/palette.txt
11
2
Blues
3
  Light - 51a9cf
4
  Dark - 126d95
5
6
Red & Yellow
7
  Light yellow - ecd980
8
  Light red - ec706a
9
  Dark red - 7e252f
10
11
Greens
12
  Light - 76A786
13
  Dark - 385742
14
15
Grayscale
16
  Light - bac2c5
17
  Dark - 394343
18
19
A docs/metadata.md
1
# Document metadata
2
3
Document metadata is information about a document. Metadata often includes
4
a title, author name, copyright date, and keywords.
5
6
# Custom metadata
7
8
The following screenshot shows example metadata preferences:
9
10
![Metadata screenshot](images/screenshots/09.png)
11
12
The **Key** column lists metadata names and the **Value** column lists
13
the metadata content for each corresponding **Key**. The content may
14
include references to variable definitions. When the document is typeset,
15
the values for the variables will be substituted upon export.
16
17
When the document is exported as XHTML, the header will include the
18
keys and values conforming to the XHTML specification. For example:
19
20
``` html
21
<head>
22
  <title>Document Title</title>
23
  <meta content="science, nature" name="keywords"/>
24
  <meta content="Penn Surnom" name="author"/>
25
  <meta content="4311" name="count"/>
26
</head>
27
```
28
29
# Special metadata
30
31
When exporting the document, note the following special metadata:
32
33
* **author** -- Included as PDF metadata
34
* **byline** -- Replaces author in PDF metadata (e.g., for pen names)
35
* **count** -- Total word count in document, automatically included
36
* **keywords** -- Included as PDF metadata
37
* **title** -- Included as a `<title>` tag, rather than a `<meta>` tag
38
139
A docs/quotes.md
1
# Quotation marks
2
3
When converting straight single quotes into curled single quotes, the
4
application offers a variety of entities to use for encoding:
5
6
* **regular** -- Do not encode.
7
* **modifier** -- Encode as \&#x2bc;, the modifier letter apostrophe.
8
* **apos** -- Encode as \&apos;, curled when typeset to PDF.
9
* **aposhex** -- Encode as \&#x27;, the apostrophe's numeric value.
10
* **quote** -- Encode as \&rsquo;, the right single quotation mark, which
11
is typically curled in HTML and XHTML documents by default.
12
* **quotehex** -- Encode \&#8217;, the right single quotation mark's numeric
13
value.
14
15
When typsetting into a PDF document, only the semantically correct value
16
of \&apos; will be curled automatically.
17
18
# History
19
20
Quotation marks trace back to Ancient Greek, later adopted to the diplé (⸖)
21
circa 625 BCE, foreshadowing its later curve. By the seventeenth century,
22
quotation marks grew common. During the nineteenth century, Western Europe
23
turned the convexity of quotation mark pairs outward.
24
25
Early mechanical typewriters, circa 1825, lacked many punctuation marks. As
26
technology improved, additional keys were added while some keys played dual
27
roles (such as I for 1). Straight single and double quotes could be co-opted
28
for quotation marks and apostrophes, feet and inches marks, and primes and
29
double-primes. There wasn't a pressing need to type curled versions because
30
humans excel at understanding from context.
31
32
Eventually straight quotes were codified for computers. Unfortunately, the
33
apostrophe carried with it the baggage from typewriters. That is, burgeoning
34
encoding standards failed to let users capture the nuances of the English
35
language; computers forced users to treat the apostrophe as a straight quote.
36
Standards bodies suggested using the right single quotation mark for an
37
apostrophe instead, shirking off its semantic meaning. Consequently,
38
text containing English quotations, especially British English, is now
39
riddled with ambiguity.
40
41
Consider the sentence:
42
43
> Ambiguity lurks in "'cause the horses'".
44
45
Does `'cause` mean _because_ or _induce_? The answer determines whether
46
an open left single quote is used or an apostrophe, semantically speaking.
47
It's amazing how ancient decisions still affect modern systems.
48
149
A docs/r/INSTALL.md
1
# R Functions
2
3
Import the files in this directory into the application, which include:
4
5
* bootstrap.R
6
* pluralize.R
7
* possessive.R
8
* conversion.R
9
* csv.R
10
11
# bootstrap.R
12
13
Copy the contents of `bootstrap.R` into the R script preferences, shown in the
14
following figure, then restart the application:
15
16
# ![Bootstrap](images/bootstrap.png)
17
18
Setting the **Working Directory** allows the startup script to load files
19
using a relative to said directory.
20
21
# pluralize.R
22
23
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).
24
25
## Usage
26
27
Example usages of the pluralize function include:
28
29
    `r#pluralize( "mouse" )` - mice
30
    `r#pluralize( "buzz" )` - buzzes
31
    `r#pluralize( "bus" )` - buses
32
33
# possessive.R
34
35
This file defines a function that applies possessives to English words.
36
37
## Usage
38
39
Example usages of the possessive function include:
40
41
    `r#pos( "Ross" )` - Ross'
42
    `r#pos( "Ruby" )` - Ruby's
43
    `r#pos( "Lois" )` - Lois'
44
    `r#pos( "my" )` - mine
45
    `r#pos( "Your" )` - Yours
46
147
A docs/r/README.md
1
# R Scripts
2
3
These scripts illustrate how R can perform calculations using variables, to help automate repetitive tasks. Authors are free to write their own scripts.
4
5
## Configuration
6
7
Configure the editor to use 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 docs/r/images/bootstrap.png
Binary file
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( v$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 `v$application$r$working$directory` changes the
110
working directory where the R engine searches for source files.
111
112
# YAML variable 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 **Variable 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 variable 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 variables 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/references.md
1
# Captions and cross-references
2
3
Users may define captions and cross-references to tables, figures,
4
and equations. Unfortunately, at time of writing, the CommonMark
5
specification is frozen. This means cross-references must be implemented
6
as an extension to the Markdown specification, leaving the door open for
7
differing Markdown rendering libraries and applications to diverge with
8
respect to syntax.
9
10
# Syntax
11
12
This section describes how to use captions and cross-references within
13
Markdown documents. The CommonMark standard details different ways to
14
add captions to tables and figures. While those are supported, a more
15
consistent syntax has been implemented.
16
17
## Captions
18
19
Tables, figures, and equations are captioned using the same syntax. In
20
general, a line that starts with a double colon after a blank line will
21
result in a caption added to the item immediately preceding it. The
22
remainder of this section provides examples.
23
24
### Images
25
26
An image caption:
27
28
```
29
![image title](https://loremflickr.com/600/350)
30
31
:: Figure caption text
32
```
33
34
### Table
35
36
A table caption:
37
38
```
39
| a | b | c |
40
|---|---|---|
41
| 1 | 2 | 3 |
42
| 4 | 5 | 6 |
43
| 7 | 8 | 9 |
44
45
:: Table caption text
46
```
47
48
### Equation
49
50
An equation caption:
51
52
```
53
$$E = mc^2$$
54
55
:: Equation caption
56
```
57
58
# Cross-references
59
60
There are two parts to a cross-reference: the anchor name and its references.
61
An anchor name must be uniquely defined. Any number of references may refer
62
to an anchor name. Anchor names can be associated with any item in the
63
document, and are primarily added to captions.
64
65
The general syntax for anchor names and references is:
66
67
* `{#type-name:label}` (anchor name)
68
* `[@type-name:label]` (reference)
69
70
The `type-name` can be any alphanumeric value, starting with a letter or
71
ideogram. Type names are user-defined categories for the item type. Labels
72
are user-defined identifiers that must be unique per item.
73
74
Consider the following example:
75
76
```
77
In [@fig:animal], a cute animal is shown.
78
79
![image title](https://loremflickr.com/600/350)
80
81
:: World's cutest animal {#fig:animal}
82
83
There is no cuter animal than the one in [@fig:animal].
84
```
85
86
The anchor name uniquely defines where an item in a document is located. Any
87
number of references, anywhere in the document, may reference an anchor name.
88
There are few restrictions placed on the possible type names and labels. Here
89
are a few more anchor name examples:
90
91
```
92
{#fig:cats}
93
{#図版:猫}
94
{#eq:mass-energy}
95
{#eqn:laplace}
96
```
97
98
## Type names
99
100
To avoid duplicating writing the label each time (e.g., Figure, Table,
101
Equation), there are a number of predefined labels associated with
102
type names. The following table lists the type names and the label
103
generated by the typesetting system:
104
105
| Type name | English name |
106
|-----------|--------------|
107
| algorithm | Algorithm    |
108
| alg       | Algorithm    |
109
| equation  | Equation     |
110
| eqn       | Equation     |
111
| eq        | Equation     |
112
| figure    | Figure       |
113
| fig       | Figure       |
114
| formula   | Formula      |
115
| listing   | Listing      |
116
| list      | Listing      |
117
| lst       | Listing      |
118
| lyric     | Lyrics       |
119
| music     | Score        |
120
| score     | Score        |
121
| source    | Listing      |
122
| src       | Listing      |
123
| tab       | Table        |
124
| table     | Table        |
125
| tbl       | Table        |
126
127
These values are defined in the theme's `xhtml/xml-references.tex` file.
1128
A docs/samples/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/samples/math.yaml
1
---
2
formula:
3
  sqrt:
4
    value: "420"
5
  quadratic:
6
    a: "25"
7
    b: "84.906"
8
    c: "20"
19
A docs/samples/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/samples/tex.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/screenshots.md
1
# Variables
2
3
Diagrams that include variables:
4
5
![GraphViz diagram screenshot](images/screenshots/01.png)
6
7
![Family tree diagram screenshot](images/screenshots/05.png)
8
9
# PDF themes
10
11
In the background of the following screenshot, the editor shows a novel
12
being edited:
13
14
![PDF themes](images/screenshots/08.png)
15
16
Highlighted items of note:
17
18
* PDF icon in the upper-left
19
* Novel metadata as integrated variables towards the top-left
20
* Theme selection dialog in the upper-middle
21
* Three different styles, including:
22
    * Boschet, based on Baskerville font, nicely styled
23
    * Handrit, based on Courier font, double-spaced, manuscript format
24
    * Tarmes, based on Times Roman font, minimal styling
25
* Variations in page numbers
26
* Manuscript includes word count, automatically
27
* Preferences dialog in the middle
28
29
# Internationalization
30
31
Poem with locale settings:
32
33
![Korean poem screenshot](images/screenshots/02.png)
34
35
# Equations
36
37
TeX equations with detached preview:
38
39
![TeX equations screenshot](images/screenshots/03.png)
40
41
# Dockable tabs
42
43
Document outline opened and docked in bottom-left corner:
44
45
![Document outline](images/screenshots/04.png)
46
147
A docs/skins.md
1
# Skins
2
3
The application provides bundled skins and the ability to add custom
4
skins. This document describes the interplay between bundled skins
5
and building your own look and feel.
6
7
A skin is a set of styles, similar to cascading style sheet classes,
8
that configures the user interface colours, fonts, spacing, highlights,
9
drop-shadows, gradients, and more.
10
11
For more information on CSS, see the [W3C CSS tutorial](https://www.w3.org/Style/Examples/011/firstcss).
12
13
# Order
14
15
The order that stylesheets are applied matters so that stylesheets can
16
override styles defined previously. The application's user interface
17
is made up of the following stylesheets, applied in the order listed:
18
19
* **scene.css** --- Defines toolbar styling.
20
* **markdown.css** --- Defines text editor styling.
21
* **skins/skin_name.css** --- Bundled skin selected in preferences.
22
* **custom.css** --- User-defined file set in preferences.
23
24
# Customization
25
26
Create a custom skin as follows:
27
28
1. Start the application.
29
1. Click **File → New** to create a new file.
30
1. Click **File → Save As** to rename the file.
31
1. Save the file as `custom.css`.
32
1. Change the content to the following:
33
``` css
34
.root {
35
  -fx-base: rgb( 30, 30, 30 );
36
  -fx-background: -fx-base;
37
}
38
```
39
40
Next, apply the skin as follows:
41
42
1. Click **Edit → Preferences** to open the preferences dialog.
43
1. Click **Skins** to view the available options.
44
1. Click **Browse** to select a custom file.
45
1. Browse to and select `custom.css`, created previously.
46
1. Click **Open**.
47
1. Click **Apply**.
48
49
The user interface immediately changes to a dark mode. Continue:
50
51
1. Click **OK** to close the dialog.
52
1. Change the **rgb** numbers in **custom.css** from `30` to `60`.
53
1. Click **File → Save** to save the CSS file.
54
55
The user interface immediately changes colour.
56
57
# Classes
58
59
When creating your own skin, there many classes that can be styled. The
60
previous section showed how to set up a rudimentary skin. Instead, start
61
with a template that already has a number of classes defined so that you
62
can tweak them to your taste. Accomplish this as follows:
63
64
1. Visit the [skin](https://gitlab.com/DaveJarvis/KeenWrite/-/tree/main/src/main/resources/com/keenwrite/skins) repository directory
65
1. Click one of the files (e.g., `haunted_grey.css`).
66
1. Click **Raw**.
67
1. Copy the entire text.
68
1. Return to `custom.css`.
69
1. Delete the contents.
70
1. Paste the copied text.
71
1. Save the file.
72
73
To see how the CSS styles are applied to the text editor, open
74
[markdown.css](https://gitlab.com/DaveJarvis/KeenWrite/-/blob/main/src/main/resources/com/keenwrite/editor/markdown.css), which is also in the repository.
75
76
# Modena
77
78
The basic look used by the application is _Modena Light_. Typically we
79
only need to override a few classes to completely change the application's
80
look and feel. For a full listing of available styles see the OpenJDK's
81
[Modena CSS file](https://github.com/openjdk/jfx/blob/master/modules/javafx.controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/modena.css).
82
83
# JavaFX CSS
84
85
The [Java CSS Reference Guide](https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/doc-files/cssref.html) is exhaustive. In addition to showing many
86
differences between JavaFX CSS and W3C CSS, the guide introduces numerous
87
helpful functions for manipulating colours and gradients using existing
88
colour definitions.
89
90
# RichTextFX
91
92
The application uses RichTextFX to render the text editor. Styling various
93
text editor classes can require using the prefix `-rtfx` instead of the
94
regular JavaFX `-fx`.
95
96
# Submit
97
98
If you have a look that you'd like to contribute to the project, do pass
99
it along. Either open a new issue in the [issue tracker](https://gitlab.com/DaveJarvis/KeenWrite/-/issues) that contains the CSS file or submit a pull request.
100
1101
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/typesetting-custom.md
1
# Overview
2
3
Typesetting PDF files entails the following:
4
5
* Download and install typesetting software
6
* Download a theme pack
7
8
These are described in the subsequent sections. Once the requirements have been met, continue reading to learn how to typeset a document.
9
10
# Download typesetter
11
12
Download the typesetting software as follows:
13
14
1. Start the text editor.
15
1. Click **File → Export As → PDF**.
16
    * Note the following details (e.g., Windows X86 64-bit):
17
        * operating system name;
18
        * instruction set; and
19
        * architecture.
20
1. Click the [link](https://wiki.contextgarden.net/Installation) in the dialog.
21
1. Download the appropriate archive file.
22
23
# Install typesetter
24
25
This section describes the installation steps for various platforms. Follow the steps that apply to the computer's operating system:
26
27
* [Windows](#windows) (includes Windows 7, Windows 10, and similar)
28
* [Unix](#unix) (includes MacOS, FreeBSD, Linux, and similar)
29
30
## Windows
31
32
Proceed with a Windows installation of the typesetting software as follows:
33
34
1. Extract the `.zip` file into `C:\Users\%USERNAME%\AppData\Local\context` (the "root" directory)
35
1. Run **install.bat** to download and install the software.
36
    * If prompted, click **Run anyway** (or click **More info** first).
37
1. Right-click [localpath.bat](https://gitlab.com/DaveJarvis/KeenWrite/-/raw/main/scripts/localpath.bat).
38
1. Select **Save Link As** (or similar).
39
1. Save the file to the typesetting software's "root" directory.
40
1. Rename `localpath.bat.txt` to `localpath.bat`, if necessary.
41
1. Run `localpath.bat` (to set and save the `PATH` environment variable).
42
43
Installation is complete. Verify the installation as follows:
44
45
1. Type: `context --version`
46
1. Press `Enter`.
47
48
If version information is displayed then the software is installed correctly.
49
50
Continue by installing a [theme pack](#theme-pack).
51
52
## Unix
53
54
For Linux, MacOS, FreeBSD, and similar operating systems, proceed as follows:
55
56
1. Create `$HOME/.local/bin/context`
57
1. Extract the `.zip` file within `$HOME/.local/bin/context`
58
1. Run `sh install.sh`
59
1. Add `export PATH=$PATH:$HOME/.local/bin/context/tex/texmf-linux-64/bin` to the login script.
60
61
Installation is complete. Verify the installation as follows:
62
63
1. Open a new terminal (to export the new PATH setting).
64
1. Type: `context --version`
65
1. Press `Enter`.
66
67
If version information is displayed then the software is installed correctly.
68
69
Continue by installing a [theme pack](#theme-pack).
70
71
# Theme pack
72
73
A theme pack is a set of themes that define how documents appear when typeset. Broadly, themes are applied as follows:
74
75
* Install a theme pack
76
* Configure individual themes
77
78
## Install theme pack
79
80
Install and configure the default theme pack as follows:
81
82
1. Download the <a href="https://gitlab.com/DaveJarvis/keenwrite-themes/-/releases/permalink/latest/downloads/theme-pack.zip">theme-pack.zip</a> archive.
83
1. Extract archive into a known location.
84
1. Start the text editor, if not already running.
85
1. Click **Edit → Preferences**.
86
1. Click **Typesetting**.
87
1. Click **Browse** beside **Themes**.
88
1. Navigate to the `themes` directory.
89
1. Click **Open**.
90
1. Click **OK**.
91
92
The theme pack is installed.
93
94
Each theme has its own requirements, described below. 
95
96
## Configure Boschet theme
97
98
Download and install the following font families:
99
100
* [Libre Baskerville](https://fonts.google.com/specimen/Libre+Baskerville)
101
* [Archivo Narrow](https://fonts.google.com/specimen/Archivo+Narrow)
102
* [Inconsolata](https://fonts.google.com/specimen/Inconsolata)
103
104
The theme is configured.
105
106
# Typeset single document
107
108
Typeset a document as follows:
109
110
1. Start the text editor, if not already running.
111
1. Click **File → New** (or type `Ctrl+n`).
112
1. Type in some text.
113
1. Click **File → Export As → PDF** (or type `Ctrl+p`).
114
1. Select a theme from the drop-down list.
115
1. Click **OK** (or press `Enter`).
116
1. Set the **File name** to the PDF file name.
117
1. Click **Save**.
118
119
The document is typeset; open the PDF file in a PDF reader to view the result.
120
121
# Typeset multiple documents
122
123
Typeset multiple documents similar to single documents, with one difference:
124
125
* Click **File → Export As → Joined PDF** (or type `Ctrl+Shift+p`).
126
127
All documents having the same file name extension in the same directory
128
(or sub-directories) as the actively edited file are first concatenated then
129
typeset into a single PDF document. The order that files are concatenated
130
is numeric and alphabetic.
131
132
For example, if `1.Rmd` is a sibling of the following files in the same
133
directory, then all the files will be included in the PDF, as expected:
134
135
    chapter_1.Rmd
136
    chapter_2.Rmd
137
    chapter_2a.Rmd
138
    chapter_2b.Rmd
139
    chapter_3.Rmd
140
    chapter_10.Rmd
141
142
Basically, sorting honours numbers and letters in file names.
143
144
# Background 
145
146
This text editor helps keep content separated from presentation. Plain text documents will remain readable long after proprietary formats have become obsolete. However, we've come to expect much more in what we read than mere text: from hyperlinked tables of contents to indexes, from footers to footnotes, from mathematical expressions to complex graphics, modern documents are nuanced and multifaceted.
147
148
## History
149
150
Before computer-based typesetting, much of mathematics was put to page by hand. Professional typesetters, who were often expensive and usually not mathematicians, would inadvertently introduce typographic errors into equations. Phototypesetting technology improved upon hand-typesetting, but well-known computer scientist Donald Knuth---whose third volume of *The Art of Computer Programming* was phototypeset in 1976---expressed dissatisfaction with its typographic quality. He set himself two goals: let anyone create high-quality books without much effort and provide software that typesets consistently on all capable computers. Two years later, he released a typesetting system and a font description language: TeX and METAFONT, respectively.
151
152
In short, TeX is software that helps typeset plain text documents.
153
154
## ConTeXt
155
156
Programming computers to typeset internationalized text automatically at the level we've become accustomed takes decades of development effort. Many free and open source software solutions can typeset text, including: ConTeXt, LaTeX, Sile, and others. ConTeXt, which builds upon TeX, is ideal for typesetting plain text into beautiful documents because it is developed with a notion of *setups*. These setups can wholly describe how text is to be typeset and---by being external to the text itself---configuring setups provides ample control over the document's final appearance without changing the prose.
157
158
# Further reading
159
160
Here are a few documents that introduce the typesetting system:
161
162
* [What is ConTeXt?](https://www.pragma-ade.com/general/manuals/what-is-context.pdf)
163
* [A not so short introduction to ConTeXt](https://github.com/contextgarden/not-so-short-introduction-to-context)
164
* [Dealing with XML in ConTeXt MKIV](https://pragma-ade.com/general/manuals/xml-mkiv.pdf)
165
* [Typographic Programming](https://www.pragma-ade.com/general/manuals/style.pdf)
166
167
The [documentation library](https://wiki.contextgarden.net/Documentation) includes the following gems:
168
169
* [ConTeXt Manual](https://www.pragma-ade.nl/general/manuals/ma-cb-en.pdf)
170
* [ConTeXt command reference](https://www.pragma-ade.nl/general/qrcs/setup-en.pdf)
171
* [METAFUN Manual](https://www.pragma-ade.nl/general/manuals/metafun-p.pdf)
172
* [It's in the Details](https://www.pragma-ade.nl/general/manuals/details.pdf)
173
* [Fonts out of ConTeXt](https://www.pragma-ade.com/general/manuals/fonts-mkiv.pdf)
174
175
Expert-level documentation includes the [LuaTeX Reference Manual](https://www.pragma-ade.nl/general/manuals/luatex.pdf).
176
1177
A docs/typesetting.md
1
# Typesetting
2
3
The application uses the [ConTeXt](https://contextgarden.net) typesetting
4
system, the [podman](https://podman.io/) container manager, various
5
[themes](https://gitlab.com/DaveJarvis/keenwrite-themes), and numerous
6
fonts to produce high-quality PDF files. The container manager significantly
7
reduces the number of manual steps in the installation process.
8
9
When exporting a document to a PDF file for the first time, a series of
10
semi-automated steps guides users through the installation process. These
11
steps differ depending on the operating system.
12
13
Run the installation wizard as follows:
14
15
1. Start the application.
16
1. Click **File → Export As → PDF**.
17
18
Follow the steps in the wizard to install the requisite software and
19
typesetting themes.
20
121
A docs/variables.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 **Variable Files** to list variable 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 **Variables**.
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 fonts/README.md
1
# Fonts
2
3
For best results, it is recommended that the Noto Font family is installed
4
on the system. The required font families include:
5
6
* Sans-serif --- editor pane
7
* Serif --- preview pane
8
* Serif monospace --- preview pane
9
10
# Chinese, Japanese, and Korean (CJK)
11
12
Download and install from the following font bundles:
13
14
* [Hong Kong](noto-hk.zip)
15
* [Japanese](noto-jp.zip)
16
* [Korean](noto-kr.zip)
17
* [Simplified Chinese](noto-sc.zip)
18
* [Traditional Chinese](noto-tc.zip)
19
20
Except for Hong Kong, each bundle contains all the required font families;
21
Hong Kong must be paired with the Simplified Chinese.
22
23
The [official versions](https://www.google.com/get/noto/) of these fonts
24
are updated regularly at the Noto Fonts
25
[repository](https://github.com/googlefonts/noto-fonts/). If downloading
26
from the original location, be sure to retrieve all font families needed
27
for the application to render text correctly.
28
29
# Internationalization
30
31
Fonts for other languages may work but have not been tested.
32
133
A fonts/install.sh
1
#!/usr/bin/env bash
2
3
readonly FONTS_DIR="/usr/local/share/fonts"
4
readonly DOWNLOAD_DIR=$(mktemp -d)
5
6
cleanup() {
7
  if [ -d "${DOWNLOAD_DIR}" ]; then
8
    rm -rf "${DOWNLOAD_DIR}"
9
  fi
10
}
11
12
trap cleanup EXIT
13
14
if [ ! -d "${FONTS_DIR}" ]; then
15
  echo "ERROR: Create ${FONTS_DIR} and ensure write access."
16
  exit 1
17
fi
18
19
while IFS=',' read -r url extension; do
20
  [[ -n "$url" ]] || continue
21
22
  filename=$(basename "${url}")
23
24
  if [ ! -d "${FONTS_DIR}/${extension}" ]; then
25
    echo "ERROR: Create ${FONTS_DIR}/${extension} and ensure write access."
26
    exit 1
27
  fi
28
29
  echo "Downloading ${url} to ${DOWNLOAD_DIR}"
30
  wget --quiet -P "${DOWNLOAD_DIR}" "${url}"
31
32
  font_dir="${FONTS_DIR}/${extension}"
33
34
  echo "Extracting ${extension} to ${font_dir}"
35
  unzip -j -o -d "${font_dir}" "${DOWNLOAD_DIR}/${filename}" "*.${extension}"
36
done < urls.csv
37
138
A fonts/noto-hk.zip
Binary file
A fonts/noto-jp.zip
Binary file
A fonts/noto-kr.zip
Binary file
A fonts/noto-sc.zip
Binary file
A fonts/noto-tc.zip
Binary file
A fonts/urls.csv
1
https://fonts.keenwrite.com/download/andada-pro.zip,otf
2
https://fonts.keenwrite.com/download/archivo-narrow.zip,otf
3
https://fonts.keenwrite.com/download/carlito.zip,ttf
4
https://fonts.keenwrite.com/download/courier-prime.zip,ttf
5
https://fonts.keenwrite.com/download/inconsolata.zip,ttf
6
https://fonts.keenwrite.com/download/libre-baskerville.zip,ttf
7
https://fonts.keenwrite.com/download/niconne.zip,ttf
8
https://fonts.keenwrite.com/download/nunito.zip,ttf
9
https://fonts.keenwrite.com/download/open-sans-emoji.zip,ttf
10
https://fonts.keenwrite.com/download/pt-mono.zip,ttf
11
https://fonts.keenwrite.com/download/pt-sans.zip,ttf
12
https://fonts.keenwrite.com/download/pt-serif.zip,ttf
13
https://fonts.keenwrite.com/download/roboto.zip,ttf
14
https://fonts.keenwrite.com/download/roboto-mono.zip,ttf
15
https://fonts.keenwrite.com/download/source-serif-4.zip,otf
16
https://fonts.keenwrite.com/download/underwood.zip,ttf
17
118
A gradle.properties
1
org.gradle.jvmargs=-Xmx1G
2
org.gradle.daemon=true
3
org.gradle.parallel=true
14
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 installer.sh
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
# Case sensitive application name.
13
readonly APP_NAME=$(cat \
14
  "${SCRIPT_DIR}/src/main/resources/bootstrap.properties" | \
15
  cut -d'=' -f2
16
)
17
# Lowercase application name.
18
readonly APP_NAME_LC=${APP_NAME,,}
19
readonly FILE_APP_JAR="${APP_NAME_LC}.jar"
20
21
readonly OPT_JAVA=$(cat << END_OF_ARGS
22
-Dprism.order=sw \
23
--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED \
24
--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED \
25
--add-opens=javafx.graphics/javafx.scene.text=ALL-UNNAMED \
26
--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED \
27
--add-opens=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED \
28
--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED \
29
--add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED \
30
--add-exports=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED \
31
--add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED \
32
--add-exports=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED \
33
--add-exports=javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED \
34
--add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED
35
END_OF_ARGS
36
)
37
38
readonly JAVA_VERSION=$(cat java.version)
39
40
ARG_JAVA_OS="linux"
41
ARG_JAVA_ARCH="amd64"
42
ARG_JAVA_VERSION=${JAVA_VERSION%+*}
43
ARG_JAVA_UPDATE=${JAVA_VERSION#*+}
44
45
ARG_JAVA_DIR="java"
46
47
ARG_DIR_DIST="dist"
48
49
FILE_LAUNCHER_SCRIPT="run.sh"
50
51
ARG_PATH_DIST_JAR="${SCRIPT_DIR}/build/libs/${FILE_APP_JAR}"
52
53
DEPENDENCIES=(
54
  "gradle,https://gradle.org"
55
  "tar,https://www.gnu.org/software/tar"
56
  "wine,https://www.winehq.org"
57
  "unzip,http://infozip.sourceforge.net"
58
)
59
60
ARGUMENTS+=(
61
  "a,arch,Target operating system architecture (amd64)"
62
  "o,os,Target operating system (linux, windows, macos)"
63
  "u,update,Java update version number (${ARG_JAVA_UPDATE})"
64
  "v,version,Full Java version (${ARG_JAVA_VERSION})"
65
)
66
67
ARCHIVE_EXT="tar.gz"
68
ARCHIVE_APP="tar xf"
69
APP_EXTENSION="bin"
70
71
# ---------------------------------------------------------------------------
72
# Generates an application binary as a self-extracting installer.
73
# ---------------------------------------------------------------------------
74
execute() {
75
  $do_configure_target
76
  $do_build
77
  $do_clean
78
79
  pushd "${ARG_DIR_DIST}" > /dev/null 2>&1
80
81
  $do_extract_java
82
  $do_create_launch_script
83
  $do_copy_archive
84
85
  popd > /dev/null 2>&1
86
87
  $do_create_launcher
88
89
  $do_brand_windows
90
  $do_sign_windows
91
92
  return 1
93
}
94
95
# ---------------------------------------------------------------------------
96
# Configure platform-specific commands and file names.
97
# ---------------------------------------------------------------------------
98
utile_configure_target() {
99
  if [ "${ARG_JAVA_OS}" = "windows" ]; then
100
    ARCHIVE_EXT="zip"
101
    ARCHIVE_APP="unzip -qq"
102
    FILE_LAUNCHER_SCRIPT="run.bat"
103
    APP_EXTENSION="exe"
104
    do_create_launch_script=utile_create_launch_script_windows
105
    do_brand_windows=utile_brand_windows
106
    do_sign_windows=utile_sign_windows
107
  elif [ "${ARG_JAVA_OS}" = "macos" ]; then
108
    APP_EXTENSION="app"
109
  fi
110
}
111
112
# ---------------------------------------------------------------------------
113
# Build platform-specific überjar.
114
# ---------------------------------------------------------------------------
115
utile_build() {
116
  $log "Delete ${ARG_PATH_DIST_JAR}"
117
  rm -f "${ARG_PATH_DIST_JAR}"
118
119
  $log "Build application for ${ARG_JAVA_OS}"
120
  gradle clean jar -PtargetOs="${ARG_JAVA_OS}"
121
}
122
123
# ---------------------------------------------------------------------------
124
# Purges the existing distribution directory to recreate the launcher.
125
# This refreshes the JRE from the downloaded archive.
126
# ---------------------------------------------------------------------------
127
utile_clean() {
128
  $log "Recreate ${ARG_DIR_DIST}"
129
  rm -rf "${ARG_DIR_DIST}"
130
  mkdir -p "${ARG_DIR_DIST}"
131
}
132
133
# ---------------------------------------------------------------------------
134
# Extract platform-specific Java Runtime Environment. This will download
135
# and cache the required Java Runtime Environment for the target platform.
136
# On subsequent runs, the cached version is used, instead of issuing another
137
# download.
138
# ---------------------------------------------------------------------------
139
utile_extract_java() {
140
  $log "Extract Java"
141
  local -r java_vm="jre"
142
  local -r java_version="${ARG_JAVA_VERSION}+${ARG_JAVA_UPDATE}"
143
144
  java_os="${ARG_JAVA_OS}"
145
  java_arch="${ARG_JAVA_ARCH}"
146
  archive_ext=""
147
148
  if [ "${ARG_JAVA_OS}" = "macos" ]; then
149
    archive_ext=".jre"
150
  fi
151
152
  local -r url_java="https://download.bell-sw.com/java/${java_version}/bellsoft-${java_vm}${java_version}-${java_os}-${java_arch}-full.${ARCHIVE_EXT}"
153
154
  local -r file_java="${java_vm}-${java_version}-${java_os}-${java_arch}.${ARCHIVE_EXT}"
155
  local -r path_java="/tmp/${file_java}"
156
157
  # File must have contents.
158
  if [ ! -s ${path_java} ]; then
159
    $log "Download ${url_java} to ${path_java}"
160
    wget -q "${url_java}" -O "${path_java}"
161
  fi
162
163
  $log "Unpack ${path_java}"
164
  $ARCHIVE_APP "${path_java}"
165
166
  local -r dir_java="${java_vm}-${ARG_JAVA_VERSION}-full${archive_ext}"
167
168
  $log "Rename ${dir_java} to ${ARG_JAVA_DIR}"
169
  mv "${dir_java}" "${ARG_JAVA_DIR}"
170
}
171
172
# ---------------------------------------------------------------------------
173
# Create Linux-specific launch script.
174
# ---------------------------------------------------------------------------
175
utile_create_launch_script_linux() {
176
  $log "Create Linux launch script"
177
178
  cat > "${FILE_LAUNCHER_SCRIPT}" << __EOT
179
#!/usr/bin/env bash
180
181
readonly SCRIPT_SRC="\$(dirname "\${BASH_SOURCE[\${#BASH_SOURCE[@]} - 1]}")"
182
183
"\${SCRIPT_SRC}/${ARG_JAVA_DIR}/bin/java" ${OPT_JAVA} -jar "\${SCRIPT_SRC}/${FILE_APP_JAR}" "\$@"
184
__EOT
185
186
  chmod +x "${FILE_LAUNCHER_SCRIPT}"
187
}
188
189
# ---------------------------------------------------------------------------
190
# Create Windows-specific launch script.
191
# ---------------------------------------------------------------------------
192
utile_create_launch_script_windows() {
193
  $log "Create Windows launch script"
194
195
  cat > "${FILE_LAUNCHER_SCRIPT}" << __EOT
196
@echo off
197
198
set SCRIPT_DIR=%~dp0
199
"%SCRIPT_DIR%\\${ARG_JAVA_DIR}\\bin\\java" ${OPT_JAVA} -jar "%SCRIPT_DIR%\\${FILE_APP_JAR}" %*
200
__EOT
201
202
  # Convert Unix end of line characters (\n) to Windows format (\r\n).
203
  # This avoids any potential line conversion issues with the repository.
204
  sed -i 's/$/\r/' "${FILE_LAUNCHER_SCRIPT}"
205
}
206
207
# ---------------------------------------------------------------------------
208
# Modify the Windows binary to include icon and identifying information.
209
# ---------------------------------------------------------------------------
210
utile_brand_windows() {
211
  local -r BINARY="${APP_NAME}.exe"
212
  local -r BINARY_LC="${APP_NAME_LC}.exe"
213
  local -r VERSION=$(git describe --tags)
214
  local -r COMPANY="White Magic Software, Ltd."
215
  local -r YEAR=$(date +%Y)
216
  local -r DESCRIPTION="Markdown editor with live preview, variables, and math."
217
  local -r SIZE=$(stat --format="%s" ${BINARY_LC})
218
219
  $log "Brand ${BINARY_LC}"
220
  wine "${SCRIPT_DIR}/scripts/rcedit-x64.exe" "${BINARY_LC}" \
221
    --set-icon "scripts/logo.ico" \
222
    --set-version-string "OriginalFilename" "${BINARY}" \
223
    --set-version-string "CompanyName" "${COMPANY}" \
224
    --set-version-string "ProductName" "${APP_NAME}" \
225
    --set-version-string "LegalCopyright" "Copyright ${YEAR} ${COMPANY}" \
226
    --set-version-string "FileDescription" "${DESCRIPTION}" \
227
    --set-version-string "Size" "${DESCRIPTION}" \
228
    --set-product-version "${VERSION}" \
229
    --set-file-version "${VERSION}"
230
231
  $log "Rename ${BINARY_LC} to ${BINARY}"
232
  mv -f "${BINARY_LC}" "${BINARY}"
233
}
234
235
# ---------------------------------------------------------------------------
236
# Modify the Windows binary to include signed certificate information.
237
# ---------------------------------------------------------------------------
238
utile_sign_windows() {
239
  local -r FILE_CERTIFICATE="${SCRIPT_DIR}/tokens/code-sign-cert.pfx"
240
  local -r FILE_BINARY="${APP_NAME}.exe"
241
  local -r FILE_SIGNED_BINARY="signed-${FILE_BINARY}"
242
243
  rm -f "${FILE_SIGNED_BINARY}"
244
245
  $log "Sign ${FILE_BINARY}"
246
  ${SCRIPT_DIR}/bin/osslsigncode sign \
247
    -pkcs12 "${FILE_CERTIFICATE}" \
248
    -askpass \
249
    -n "${APP_NAME}" \
250
    -i "https://www.${APP_NAME_LC}.com" \
251
    -in "${FILE_BINARY}" \
252
    -out "${FILE_SIGNED_BINARY}"
253
254
  $log "Rename ${FILE_SIGNED_BINARY} to ${FILE_BINARY}"
255
  mv -f "${FILE_SIGNED_BINARY}" "${FILE_BINARY}"
256
}
257
258
# ---------------------------------------------------------------------------
259
# Copy application überjar.
260
# ---------------------------------------------------------------------------
261
utile_copy_archive() {
262
  $log "Create copy of ${FILE_APP_JAR}"
263
  cp "${ARG_PATH_DIST_JAR}" "${FILE_APP_JAR}"
264
}
265
266
# ---------------------------------------------------------------------------
267
# Create platform-specific launcher binary.
268
# ---------------------------------------------------------------------------
269
utile_create_launcher() {
270
  packer=${SCRIPT_DIR}/bin/warp-packer
271
  packer_opt_pack="pack"
272
  packer_opt_input="input-dir"
273
274
  local -r FILE_APP_NAME="${APP_NAME_LC}.${APP_EXTENSION}"
275
  $log "Create ${FILE_APP_NAME}"
276
277
  # Warp-packer does not overwrite the file.
278
  rm -f "${FILE_APP_NAME}"
279
280
  # Download uses amd64, but warp-packer differs.
281
  if [ "${ARG_JAVA_ARCH}" = "amd64" ]; then
282
    ARG_JAVA_ARCH="x64"
283
  fi
284
285
  # The warp-packer fork that fixes Windows doesn't support MacOS.
286
  if [ "${ARG_JAVA_OS}" = "macos" ]; then
287
    packer=${SCRIPT_DIR}/bin/linux-x64.warp-packer
288
    packer_opt_pack=""
289
    packer_opt_input="input_dir"
290
  fi
291
292
  ${packer} \
293
    ${packer_opt_pack} \
294
    --arch "${ARG_JAVA_OS}-${ARG_JAVA_ARCH}" \
295
    --${packer_opt_input} "${ARG_DIR_DIST}" \
296
    --exec "${FILE_LAUNCHER_SCRIPT}" \
297
    --output "${FILE_APP_NAME}" > /dev/null
298
299
  chmod +x "${FILE_APP_NAME}"
300
}
301
302
argument() {
303
  local consume=2
304
305
  case "$1" in
306
    -a|--arch)
307
    ARG_JAVA_ARCH="$2"
308
    ;;
309
    -o|--os)
310
    ARG_JAVA_OS="$2"
311
    ;;
312
    -u|--update)
313
    ARG_JAVA_UPDATE="$2"
314
    ;;
315
    -v|--version)
316
    ARG_JAVA_VERSION="$2"
317
    ;;
318
  esac
319
320
  return ${consume}
321
}
322
323
do_configure_target=utile_configure_target
324
do_build=utile_build
325
do_clean=utile_clean
326
do_extract_java=utile_extract_java
327
do_create_launch_script=utile_create_launch_script_linux
328
do_copy_archive=utile_copy_archive
329
do_create_launcher=utile_create_launcher
330
do_brand_windows=:
331
do_sign_windows=:
332
333
main "$@"
334
1335
A java.version
1
23.0.1+13
12
A keenwrite.sh
1
#!/usr/bin/env bash
2
3
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
4
5
java \
6
  -Dprism.order=sw \
7
  --add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED \
8
  --add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED \
9
  --add-opens=javafx.graphics/javafx.scene.text=ALL-UNNAMED \
10
  --add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED \
11
  --add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED \
12
  --add-opens=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED \
13
  --add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED \
14
  --add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED \
15
  --add-exports=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED \
16
  --add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED \
17
  --add-exports=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED \
18
  --add-exports=javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED \
19
  --add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED \
20
  -jar ${SCRIPT_DIR}/keenwrite.jar $@
21
122
A libs/keencount.jar
Binary file
A libs/keenquotes.jar
Binary file
A libs/keenspell.jar
Binary file
A libs/keentype-lib.jar
Binary file
A publish.sh
1
#!/usr/bin/env bash
2
3
# ---------------------------------------------------------------------------
4
# This script uploads the latest application release for all supported
5
# platforms.
6
# ---------------------------------------------------------------------------
7
8
readonly RELEASE=$(git describe --abbrev=0 --tags)
9
readonly APP_NAME=$(cut -d= -f2 ./src/main/resources/bootstrap.properties)
10
readonly APP_NAME_LC=${APP_NAME,,}
11
readonly PATH_TOKEN="tokens/${APP_NAME_LC}.pat"
12
readonly URL=$(cat "tokens/publish.url")
13
readonly FILE_VERSION="version.txt"
14
15
# ---------------------------------------------------------------------------
16
# Adds download URLs to a release.
17
#
18
# $1 - The system (Linux, WIndows, MacOS, Java)
19
# ---------------------------------------------------------------------------
20
release() {
21
  local -r OS="${1}"
22
  local ARCH=" (64-bit, x86)"
23
  local FILE_PREFIX="${APP_NAME_LC}"
24
  local FILE_SUFFIX="bin"
25
26
  case ${OS} in
27
    MacOS)
28
      FILE_SUFFIX="app"
29
    ;;
30
    Windows)
31
      FILE_PREFIX="${APP_NAME}"
32
      FILE_SUFFIX="exe"
33
    ;;
34
    Java)
35
      ARCH=""
36
      FILE_SUFFIX="jar"
37
    ;;
38
    *)
39
      # Linux, others
40
    ;;
41
  esac
42
43
  local -r BINARY="${FILE_PREFIX}.${FILE_SUFFIX}"
44
45
  upload "${BINARY}"
46
47
  glab release upload ${RELEASE} \
48
    --assets-links="[{
49
      \"name\":\"${APP_NAME} for ${OS}${ARCH}\",
50
      \"url\":\"https://${APP_NAME_LC}.com/downloads/${BINARY}\",
51
      \"link_type\":\"other\"
52
    }]"
53
}
54
55
# ---------------------------------------------------------------------------
56
# Uploads a file to the remote host.
57
#
58
# $1 - The relative path to the file to upload.
59
# ---------------------------------------------------------------------------
60
upload() {
61
  local -r FILENAME="${1}"
62
63
  if [ -f "${FILENAME}" ]; then
64
    scp "${FILENAME}" "${URL}"
65
  else
66
    echo "Missing ${FILE_BINARY}, continuing."
67
  fi
68
}
69
70
if [ -f "${PATH_TOKEN}" ]; then
71
  cat "${PATH_TOKEN}" | glab auth login --hostname gitlab.com --stdin
72
73
  glab release create ${RELEASE}
74
75
  release "Windows"
76
  release "MacOS"
77
  release "Linux"
78
  release "Java"
79
80
  echo "${RELEASE}" > "${FILE_VERSION}"
81
  upload "${FILE_VERSION}"
82
  mv "${FILE_VERSION}" "www/downloads"
83
else
84
  echo "Create ${PATH_TOKEN} before publishing the release."
85
fi
86
187
A release.sh
1
#!/usr/bin/env bash
2
3
# ---------------------------------------------------------------------------
4
# This script builds Windows, Linux, and Java archive binaries for a
5
# release.
6
# ---------------------------------------------------------------------------
7
8
source $HOME/bin/build-template
9
10
readonly FILE_PROPERTIES="${SCRIPT_DIR}/src/main/resources/bootstrap.properties"
11
readonly BIN_INSTALLER="${SCRIPT_DIR}/installer.sh"
12
13
DEPENDENCIES=(
14
  "gradle,https://gradle.org"
15
  "zip,http://infozip.sourceforge.net"
16
  "${FILE_PROPERTIES},File containing application name"
17
)
18
19
execute() {
20
  $log "Remove distribution directory"
21
  rm -rf "${SCRIPT_DIR}/dist"
22
23
  $log "Remove stale binaries"
24
  rm -f "${application_title,,}.jar"
25
  rm -f "${application_title,,}.bin"
26
  rm -f "${application_title}.exe"
27
28
  $log "Build Java archive"
29
  gradle clean jar
30
  mv "build/libs/${application_title,,}.jar" .
31
32
  $log "Build Linux installer binary"
33
  ${BIN_INSTALLER} -o linux
34
35
  $log "Build MacOS installer binary"
36
  ${BIN_INSTALLER} -o macos
37
38
  $log "Build Windows installer binary"
39
  ${BIN_INSTALLER} -o windows
40
}
41
42
preprocess() {
43
  while IFS='=' read -r key value; do
44
    if [[ "${key}" = "" || "${key}" = "#"* ]]; then
45
      continue
46
    fi
47
48
    key=$(echo $key | tr '.' '_')
49
    eval ${key}=\${value}
50
  done < "${FILE_PROPERTIES}"
51
52
  application_title="${application_title}"
53
54
  return 1
55
}
56
57
main "$@"
58
159
A scripts/.gitignore
1
code-sign-cert.pfx
12
A scripts/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 scripts/font-names.sh
1
#!/usr/bin/env bash
2
3
# Outputs font names for all font files.
4
5
find src/main/resources/fonts -type f \( -name "*otf" -o -name "*ttf" \) -exec \
6
  fc-scan --format "%{foundry}: %{family}\n" {} \; | uniq | sort
7
18
A scripts/icons.sh
1
#!/bin/bash
2
3
INKSCAPE="/usr/bin/inkscape"
4
PNG_COMPRESS="optipng"
5
PNG_COMPRESS_OPTS="-o9 *png"
6
7
declare -a SIZES=("16" "32" "64" "128" "256" "512")
8
9
for i in "${SIZES[@]}"; do
10
  # -y: export background opacity 0
11
  $INKSCAPE -y 0 -w "${i}" --export-overwrite --export-type=png -o "logo${i}.png" "logo.svg" 
12
done
13
14
# Compess the PNG images.
15
which $PNG_COMPRESS && $PNG_COMPRESS $PNG_COMPRESS_OPTS
16
117
A scripts/jsign-4.2.jar
Binary file
A scripts/localpath.bat
1
@echo off
2
set "OWNPATH=%~dp0"
3
set "PLATFORM=mswin"
4
5
if defined ProgramFiles(x86)                        set "PLATFORM=win64"
6
if "%PROCESSOR_ARCHITECTURE%"=="AMD64"              set "PLATFORM=win64"
7
if exist "%OWNPATH%tex\texmf-mswin\bin\context.exe" set "PLATFORM=mswin"
8
if exist "%OWNPATH%tex\texmf-win64\bin\context.exe" set "PLATFORM=win64"
9
10
set "TeXPath=%OWNPATH%tex\texmf-%PLATFORM%\bin"
11
12
echo %PATH% | findstr "texmf-%PLATFORM%" > nul
13
14
rem Only update the PATH if not previously updated
15
if ERRORLEVEL 1 (
16
  setlocal enabledelayedexpansion
17
  set "Exists=false"
18
  set "Key=HKCU\Environment"
19
  
20
  for /F "USEBACKQ tokens=2*" %%A in (`reg query %%Key%% /v PATH 2^>nul`) do (
21
    if not "%%~B" == "" (
22
      set "Exists=true"
23
24
      rem Preserve the existing PATH
25
      echo %%B > currpath.txt
26
27
      rem Change the PATH environment variable
28
      setx PATH "%%B;%TeXPath%"
29
    )
30
  )
31
32
  rem The user-defined PATH does not exist, create it
33
  if "!Exists!" == "false" (
34
    rem Change the user PATH environment variable
35
    setx PATH "%TeXPath%"
36
  )
37
38
  endlocal
39
  
40
  rem Update the current session
41
  set "PATH=%PATH%;%TeXPath%"
42
)
43
144
A scripts/logo.ico
Binary file
A scripts/rcedit-x64.exe
Binary file
A scripts/squish.sh
1
#!/usr/bin/env bash
2
3
# TODO: This file does not work with Picocli and there are other issues.
4
# TODO: Revisit after replacing Picocli and using FastR instead of Renjin.
5
6
MODULES="${JAVA_HOME}/jmods/"
7
LIBS=$(ls -1 ../libs/*jar | sed 's/\(.*\)/-libraryjars \1/g')
8
9
java -jar ../tex/lib/proguard.jar \
10
  -libraryjars "${MODULES}java.base.jmod/(!**.jar;!module-info.class)" \
11
  -libraryjars "${MODULES}java.desktop.jmod/(!**.jar;!module-info.class)" \
12
  -libraryjars "${MODULES}java.xml.jmod/(!**.jar;!module-info.class)" \
13
  -libraryjars "${MODULES}javafx.controls.jmod/(!**.jar;!module-info.class)" \
14
  -libraryjars "${MODULES}javafx.graphics.jmod/(!**.jar;!module-info.class)" \
15
  ${LIBS} \
16
  -injars ../build/libs/keenwrite.jar \
17
  -outjars ../build/libs/keenwrite-min.jar \
18
  -keep 'class com.keenwrite.** { *; }' \
19
  -keep 'class com.whitemagicsoftware.tex.** { *; }' \
20
  -keep 'class org.renjin.** { *; }' \
21
  -keep 'class picocli.** { *; }' \
22
  -keep 'interface picocli.** { *; }' \
23
  -keep 'class picocli.CommandLine { *; }' \
24
  -keep 'class picocli.CommandLine$* { *; }' \
25
  -keepattributes '*Annotation*, Signature, Exception' \
26
  -keepclassmembers 'class * extends java.util.concurrent.Callable {
27
      public java.lang.Integer call();
28
  }' \
29
  -keepclassmembers 'class * {
30
      @javax.inject.Inject <init>(...);
31
      @picocli.CommandLine$Option *;
32
  }' \
33
  -keepclassmembers 'class * extends java.lang.Enum {
34
      <fields>;
35
      public static **[] values();
36
      public static ** valueOf(java.lang.String);
37
  }' \
38
  -keepnames \
39
    'class org.apache.lucene.analysis.tokenattributes.KeywordAttributeImpl' \
40
  -dontnote \
41
  -dontwarn \
42
  -dontoptimize \
43
  -dontobfuscate
44
145
A settings.gradle
11
A src/main/java/com/keenwrite/AppCommands.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite;
6
7
import com.keenwrite.cmdline.Arguments;
8
import com.keenwrite.commands.ConcatenateCommand;
9
import com.keenwrite.io.SysFile;
10
import com.keenwrite.processors.Processor;
11
import com.keenwrite.processors.ProcessorContext;
12
import com.keenwrite.processors.r.RBootstrapProcessor;
13
14
import java.io.IOException;
15
import java.nio.file.Path;
16
import java.util.concurrent.Callable;
17
import java.util.concurrent.CompletableFuture;
18
import java.util.concurrent.ExecutorService;
19
import java.util.concurrent.Future;
20
21
import static com.keenwrite.events.StatusEvent.clue;
22
import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN;
23
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
24
import static java.nio.charset.StandardCharsets.UTF_8;
25
import static java.nio.file.Files.readString;
26
import static java.nio.file.Files.writeString;
27
import static java.util.concurrent.Executors.newFixedThreadPool;
28
import static org.apache.commons.io.FilenameUtils.getExtension;
29
30
/**
31
 * Responsible for executing common commands. These commands are shared by
32
 * both the graphical and the command-line interfaces.
33
 */
34
public class AppCommands {
35
  private static final ExecutorService sExecutor = newFixedThreadPool( 1 );
36
37
  private AppCommands() {
38
  }
39
40
  public static void run( final Arguments args ) {
41
    final var future = new CompletableFuture<Path>() {
42
      @Override
43
      public boolean complete( final Path path ) {
44
        return super.complete( path );
45
      }
46
47
      @Override
48
      public boolean completeExceptionally( final Throwable ex ) {
49
        clue( ex );
50
51
        return super.completeExceptionally( ex );
52
      }
53
    };
54
55
    file_export( args, future );
56
    sExecutor.shutdown();
57
    future.join();
58
  }
59
60
  /**
61
   * Converts one or more files into the given file format. If {@code dir}
62
   * is set to true, this will first append all files in the same directory
63
   * as the actively edited file.
64
   *
65
   * @param future Indicates whether the export succeeded or failed.
66
   * @return The path to the exported file as a {@link Future}.
67
   */
68
  @SuppressWarnings( "UnusedReturnValue" )
69
  private static Future<Path> file_export(
70
    final Arguments args, final CompletableFuture<Path> future ) {
71
    assert args != null;
72
    assert future != null;
73
74
    final Callable<Path> callableTask = () -> {
75
      try {
76
        final var context = args.createProcessorContext();
77
        final var outputPath = context.getTargetPath();
78
        final var chain = createProcessors( context );
79
        final var processor = createBootstrapProcessor( chain, context );
80
        final var inputDoc = read( context );
81
        final var outputDoc = processor.apply( inputDoc );
82
83
        // Processors can export binary files. In such cases, processors will
84
        // return null to prevent further processing.
85
        final var result = outputDoc == null
86
          ? null
87
          : writeString( outputPath, outputDoc, UTF_8 );
88
89
        future.complete( outputPath );
90
        return result;
91
      } catch( final Throwable ex ) {
92
        future.completeExceptionally( ex );
93
        return null;
94
      }
95
    };
96
97
    // Prevent the application from blocking while the processor executes.
98
    return sExecutor.submit( callableTask );
99
  }
100
101
  private static Processor<String> createBootstrapProcessor(
102
    final Processor<String> chain, final ProcessorContext context ) {
103
104
    return context.getSourceType() == TEXT_R_MARKDOWN
105
      ? new RBootstrapProcessor( chain, context )
106
      : chain;
107
  }
108
109
  /**
110
   * Concatenates all the files in the same directory as the given file into
111
   * a string. The extension is determined by the given file name pattern; the
112
   * order files are concatenated is based on their numeric sort order (this
113
   * avoids lexicographic sorting).
114
   * <p>
115
   * If the parent path to the file being edited in the text editor cannot
116
   * be found then this will return the editor's text, without iterating through
117
   * the parent directory. (Should never happen, but who knows?)
118
   * </p>
119
   * <p>
120
   * New lines are automatically appended to separate each file.
121
   * </p>
122
   *
123
   * @param context The {@link ProcessorContext} containing input path,
124
   *                and other command-line parameters.
125
   * @return All files in the same directory as the file being edited
126
   * concatenated into a single string.
127
   */
128
  private static String read( final ProcessorContext context )
129
    throws IOException {
130
    final var concat = context.getConcatenate();
131
    final var inputPath = context.getSourcePath();
132
    final var parent = inputPath.getParent();
133
    final var filename = SysFile.getFileName( inputPath );
134
    final var extension = getExtension( filename );
135
136
    // Short-circuit because: only one file was requested; there is no parent
137
    // directory to scan for files; or there's no extension for globbing.
138
    if( !concat || parent == null || extension.isBlank() ) {
139
      return readString( inputPath, UTF_8 );
140
    }
141
142
    final var command = new ConcatenateCommand(
143
      parent, extension, context.getChapters() );
144
    return command.call();
145
  }
146
}
1147
A src/main/java/com/keenwrite/Bootstrap.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.constants.Constants;
5
import com.keenwrite.io.UserDataDir;
6
7
import java.io.File;
8
import java.io.InputStream;
9
import java.nio.file.Path;
10
import java.util.Calendar;
11
import java.util.Properties;
12
13
import static com.keenwrite.events.StatusEvent.clue;
14
import static com.keenwrite.io.SysFile.toFile;
15
16
/**
17
 * Responsible for loading the bootstrap.properties file, which is
18
 * tactically located outside the standard resource reverse domain name
19
 * namespace to avoid hard-coding the application name in many places.
20
 * Instead, the application name is located in the bootstrap file, which is
21
 * then used to look up the remaining settings.
22
 * <p>
23
 * See {@link Constants#PATH_PROPERTIES_SETTINGS} for details.
24
 * </p>
25
 */
26
public final class Bootstrap {
27
  private static final String PATH_BOOTSTRAP = "/bootstrap.properties";
28
29
  /**
30
   * Must be populated before deriving the app title (order matters).
31
   */
32
  private static final Properties sP = new Properties();
33
34
  public static final String APP_TITLE;
35
  public static final String APP_VERSION;
36
37
  public static final String APP_TITLE_ABBR = "kwr";
38
  public static final String APP_TITLE_LOWERCASE;
39
  public static final String APP_VERSION_CLEAN;
40
  public static final String APP_YEAR;
41
42
  public static final Path USER_DATA_DIR;
43
  public static final File USER_CACHE_DIR;
44
45
  static {
46
    var appVersion = "0.0.0";
47
    var appTitle = "KeenWrite";
48
49
    try( final var in = openResource( PATH_BOOTSTRAP ) ) {
50
      sP.load( in );
51
52
      appTitle = sP.getProperty( "application.title" );
53
    } catch( final Exception ex ) {
54
      final var fmt = "Unable to load %s resource, applying defaults.%n";
55
      clue( ex, fmt, PATH_BOOTSTRAP );
56
    }
57
58
    APP_TITLE = appTitle;
59
    APP_TITLE_LOWERCASE = APP_TITLE.toLowerCase();
60
61
    try {
62
      appVersion = Launcher.getVersion();
63
    } catch( final Exception ex ) {
64
      final var fmt = "Unable to determine application version.";
65
      clue( ex, fmt );
66
    }
67
68
    APP_VERSION = appVersion;
69
70
    // The plug-in that requests the version from the repository tag will
71
    // add a "dirty" number and indicator suffix. Removing it allows the
72
    // "clean" version to be used to pull a corresponding typesetter container.
73
    APP_VERSION_CLEAN = APP_VERSION.replaceAll( "-.*", "" );
74
    APP_YEAR = getYear();
75
76
    // This also sets the user agent for the SVG rendering library.
77
    System.setProperty( "http.agent", APP_TITLE + " " + APP_VERSION_CLEAN );
78
79
    USER_DATA_DIR = UserDataDir.getAppPath( APP_TITLE_LOWERCASE );
80
    USER_CACHE_DIR = toFile( USER_DATA_DIR.resolve( "cache" ) );
81
82
    if( !USER_CACHE_DIR.exists() && !USER_CACHE_DIR.mkdirs() ) {
83
      clue( "Main.status.error.bootstrap.cache", USER_CACHE_DIR );
84
    }
85
  }
86
87
  @SuppressWarnings( "SameParameterValue" )
88
  private static InputStream openResource( final String path ) {
89
    return Constants.class.getResourceAsStream( path );
90
  }
91
92
  private static String getYear() {
93
    return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) );
94
  }
95
}
196
A src/main/java/com/keenwrite/ExportFormat.java
1
/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite;
6
7
import com.keenwrite.events.StatusEvent;
8
import com.keenwrite.io.MediaType;
9
import com.keenwrite.io.MediaTypeExtension;
10
11
import java.io.File;
12
import java.nio.file.Path;
13
14
import static com.keenwrite.io.SysFile.toFile;
15
import static java.lang.String.format;
16
import static org.apache.commons.io.FilenameUtils.removeExtension;
17
18
/**
19
 * Provides controls for processor behaviour when transforming input documents.
20
 */
21
public enum ExportFormat {
22
23
  /**
24
   * For HTML exports, encode TeX as SVG. Treat image links relatively.
25
   */
26
  HTML_TEX_SVG( ".html" ),
27
28
  /**
29
   * For HTML exports, encode TeX using {@code $} delimiters, suitable for
30
   * rendering by an external TeX typesetting engine (or online with KaTeX).
31
   * Treat image links relatively.
32
   */
33
  HTML_TEX_DELIMITED( ".html" ),
34
35
  /**
36
   * For XHTML exports, encode TeX using {@code $} delimiters.
37
   */
38
  XHTML_TEX( ".xhtml" ),
39
40
  /**
41
   * For XHTML exports, encode TeX as SVG. Treat image links relatively.
42
   */
43
  XHTML_TEX_SVG( ".xhtml" ),
44
45
  /**
46
   * For TEXT exports, encode TeX using {@code $} delimiters.
47
   */
48
  TEXT_TEX( ".txt" ),
49
50
  /**
51
   * Exports as PDF file format.
52
   */
53
  APPLICATION_PDF( ".pdf" ),
54
55
  /**
56
   * Indicates no special export format is to be created. No extension is
57
   * applicable. Image links must use absolute directories.
58
   */
59
  NONE( "" );
60
61
  /**
62
   * Preferred file name extension for the given file type.
63
   */
64
  private final String mExtension;
65
66
  /**
67
   * Looks up the {@link ExportFormat} based on the given path and subtype.
68
   *
69
   * @param path     The type to find.
70
   * @param modifier The subtype to find (for HTML).
71
   * @return An object to control the output file format.
72
   * @throws IllegalArgumentException The type/subtype could not be found.
73
   */
74
  public static ExportFormat valueFrom( final Path path, final String modifier )
75
    throws IllegalArgumentException {
76
    assert path != null;
77
78
    return valueFrom( MediaType.fromFilename( path ), modifier );
79
  }
80
81
  /**
82
   * Looks up the {@link ExportFormat} based on the given path and subtype.
83
   *
84
   * @param extension The type to find.
85
   * @param modifier  The subtype to find (for HTML).
86
   * @return An object to control the output file format.
87
   * @throws IllegalArgumentException The type/subtype could not be found.
88
   */
89
  public static ExportFormat valueFrom(
90
    final String extension, final String modifier )
91
    throws IllegalArgumentException {
92
    assert extension != null;
93
    final var mediaType = MediaTypeExtension.fromExtension( extension );
94
95
    return valueFrom( mediaType, modifier );
96
  }
97
98
  /**
99
   * Looks up the {@link ExportFormat} based on the given path and subtype.
100
   *
101
   * @param type     The media type to find.
102
   * @param modifier The subtype to find (for HTML).
103
   * @return An object to control the output file format.
104
   * @throws IllegalArgumentException The type/subtype could not be found.
105
   */
106
  public static ExportFormat valueFrom(
107
    final MediaType type, final String modifier ) {
108
    final var svg = "svg".equalsIgnoreCase( modifier.trim() );
109
110
    return switch( type ) {
111
      case TEXT_HTML -> svg
112
        ? HTML_TEX_SVG
113
        : HTML_TEX_DELIMITED;
114
      case TEXT_XML, APP_XHTML -> svg
115
        ? XHTML_TEX_SVG
116
        : XHTML_TEX;
117
      case APP_PDF -> APPLICATION_PDF;
118
      default -> throw new IllegalArgumentException( format(
119
        "Unrecognized format type and subtype: '%s' and '%s'", type, modifier
120
      ) );
121
    };
122
  }
123
124
  ExportFormat( final String extension ) {
125
    mExtension = extension;
126
  }
127
128
  /**
129
   * Returns the given {@link File} with its extension replaced by one that
130
   * matches this {@link ExportFormat} extension.
131
   *
132
   * @param file The file to perform an extension swap.
133
   * @return The given file with its extension replaced.
134
   */
135
  public File toExportFilename( final File file ) {
136
    return new File( removeExtension( file.getName() ) + mExtension );
137
  }
138
139
  /**
140
   * Delegates to {@link #toExportFilename(File)} after converting the given
141
   * {@link Path} to an instance of {@link File}.
142
   *
143
   * @param path The {@link Path} to convert to a {@link File}.
144
   * @return The given path with its extension replaced.
145
   */
146
  public File toExportFilename( final Path path ) {
147
    return toExportFilename( toFile( path ) );
148
  }
149
}
1150
A src/main/java/com/keenwrite/GuiApp.java
1
/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite;
6
7
import com.keenwrite.cmdline.HeadlessApp;
8
import com.keenwrite.events.HyperlinkOpenEvent;
9
import com.keenwrite.events.StatusEvent;
10
import com.keenwrite.preferences.Workspace;
11
import com.keenwrite.preview.MathRenderer;
12
import com.keenwrite.spelling.impl.Lexicon;
13
import javafx.application.Application;
14
import javafx.event.Event;
15
import javafx.event.EventType;
16
import javafx.scene.input.KeyCode;
17
import javafx.scene.input.KeyEvent;
18
import javafx.stage.Stage;
19
import org.greenrobot.eventbus.Subscribe;
20
21
import java.util.function.BooleanSupplier;
22
import java.util.logging.FileHandler;
23
import java.util.logging.Level;
24
import java.util.logging.Logger;
25
26
import static com.keenwrite.Bootstrap.APP_TITLE;
27
import static com.keenwrite.constants.GraphicsConstants.LOGOS;
28
import static com.keenwrite.events.Bus.register;
29
import static com.keenwrite.preferences.AppKeys.*;
30
import static com.keenwrite.util.FontLoader.initFonts;
31
import static java.util.logging.Logger.getLogger;
32
import static javafx.scene.input.KeyCode.F11;
33
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
34
import static javafx.scene.input.KeyEvent.KEY_RELEASED;
35
import static javafx.stage.WindowEvent.WINDOW_SHOWN;
36
37
/**
38
 * The application allows users to edit plain text files in a markup notation
39
 * and see a real-time preview of the formatted output.
40
 */
41
public final class GuiApp extends Application {
42
  private Workspace mWorkspace;
43
44
  private final static Logger sLogger =
45
    getLogger( GuiApp.class.getCanonicalName() );
46
47
  /**
48
   * GUI application entry point. See {@link HeadlessApp} for the entry
49
   * point to the command-line application.
50
   *
51
   * @param args Command-line arguments.
52
   */
53
  public static void run( final String[] args ) {
54
    setLogLevel( sLogger, Level.FINE );
55
    launch( args );
56
  }
57
58
  @SuppressWarnings( "SameParameterValue" )
59
  private static void setLogLevel( final Logger logger, final Level level ) {
60
    final var handlers = logger.getHandlers();
61
62
    logger.setLevel( level );
63
64
    for( final var h : handlers ) {
65
      if( h instanceof FileHandler ) {
66
        h.setLevel( level );
67
      }
68
    }
69
  }
70
71
  /**
72
   * Creates an instance of {@link KeyEvent} that represents pressing a key.
73
   *
74
   * @param code  The key to simulate being pressed down.
75
   * @param shift Whether shift key modifier shall modify the key code.
76
   * @return An instance of {@link KeyEvent} that may be used to simulate
77
   * a key being pressed.
78
   */
79
  public static Event keyDown( final KeyCode code, final boolean shift ) {
80
    return keyEvent( KEY_PRESSED, code, shift );
81
  }
82
83
  /**
84
   * Creates an instance of {@link KeyEvent} that represents a key released
85
   * event without any modifier keys held.
86
   *
87
   * @param code The key code representing a key to simulate releasing.
88
   * @return An instance of {@link KeyEvent}.
89
   */
90
  public static Event keyDown( final KeyCode code ) {
91
    return keyDown( code, false );
92
  }
93
94
  /**
95
   * Creates an instance of {@link KeyEvent} that represents releasing a key.
96
   *
97
   * @param code  The key to simulate being released up.
98
   * @param shift Whether shift key modifier shall modify the key code.
99
   * @return An instance of {@link KeyEvent} that may be used to simulate
100
   * a key being released.
101
   */
102
  @SuppressWarnings( "unused" )
103
  public static Event keyUp( final KeyCode code, final boolean shift ) {
104
    return keyEvent( KEY_RELEASED, code, shift );
105
  }
106
107
  private static Event keyEvent(
108
    final EventType<KeyEvent> type, final KeyCode code, final boolean shift ) {
109
    return new KeyEvent(
110
      type, "", "", code, shift, false, false, false
111
    );
112
  }
113
114
  /**
115
   * JavaFX entry point.
116
   *
117
   * @param stage The primary application stage.
118
   */
119
  @Override
120
  public void start( final Stage stage ) {
121
    // Must be instantiated after the UI is initialized (i.e., not in main)
122
    // because it interacts with GUI properties.
123
    mWorkspace = new Workspace();
124
125
    // The locale was already loaded when the workspace was created. This
126
    // ensures that when the locale preference changes, a new spellchecker
127
    // instance will be loaded and applied.
128
    final var property = mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
129
    property.addListener( ( _, _, _ ) -> readLexicon() );
130
131
    initFonts();
132
    initState( stage );
133
    initStage( stage );
134
    initIcons( stage );
135
    initScene( stage );
136
137
    MathRenderer.bindSize( mWorkspace.doubleProperty( KEY_UI_FONT_MATH_SIZE ) );
138
139
    // Load the lexicon and check all the documents after all files are open.
140
    stage.addEventFilter( WINDOW_SHOWN, _ -> readLexicon() );
141
    stage.show();
142
143
    register( this );
144
  }
145
146
  private void initState( final Stage stage ) {
147
    final var enable = createBoundsEnabledSupplier( stage );
148
149
    stage.setX( mWorkspace.getDouble( KEY_UI_WINDOW_X ) );
150
    stage.setY( mWorkspace.getDouble( KEY_UI_WINDOW_Y ) );
151
    stage.setWidth( mWorkspace.getDouble( KEY_UI_WINDOW_W ) );
152
    stage.setHeight( mWorkspace.getDouble( KEY_UI_WINDOW_H ) );
153
    stage.setMaximized( mWorkspace.getBoolean( KEY_UI_WINDOW_MAX ) );
154
    stage.setFullScreen( mWorkspace.getBoolean( KEY_UI_WINDOW_FULL ) );
155
156
    mWorkspace.listen( KEY_UI_WINDOW_X, stage.xProperty(), enable );
157
    mWorkspace.listen( KEY_UI_WINDOW_Y, stage.yProperty(), enable );
158
    mWorkspace.listen( KEY_UI_WINDOW_W, stage.widthProperty(), enable );
159
    mWorkspace.listen( KEY_UI_WINDOW_H, stage.heightProperty(), enable );
160
    mWorkspace.listen( KEY_UI_WINDOW_MAX, stage.maximizedProperty() );
161
    mWorkspace.listen( KEY_UI_WINDOW_FULL, stage.fullScreenProperty() );
162
  }
163
164
  private void initStage( final Stage stage ) {
165
    stage.setTitle( APP_TITLE );
166
    stage.addEventHandler( KEY_PRESSED, event -> {
167
      if( F11.equals( event.getCode() ) ) {
168
        stage.setFullScreen( !stage.isFullScreen() );
169
      }
170
    } );
171
  }
172
173
  private void initIcons( final Stage stage ) {
174
    stage.getIcons().addAll( LOGOS );
175
  }
176
177
  private void initScene( final Stage stage ) {
178
    final var mainScene = new MainScene( mWorkspace );
179
    stage.setScene( mainScene.getScene() );
180
  }
181
182
  /**
183
   * When a hyperlink website URL is clicked, this method is called to launch
184
   * the default browser to the event's location.
185
   *
186
   * @param event The event called when a hyperlink was clicked.
187
   */
188
  @Subscribe
189
  public void handle( final HyperlinkOpenEvent event ) {
190
    getHostServices().showDocument( event.getUri().toString() );
191
  }
192
193
  /**
194
   * When a status message is shown, write it to the console.
195
   *
196
   * @param event The event published when the status changes.
197
   */
198
  @Subscribe
199
  public void handle( final StatusEvent event ) {
200
    assert event != null;
201
    assert sLogger != null;
202
203
    sLogger.info( event.getMessage() );
204
  }
205
206
  /**
207
   * This will load the lexicon for the user's preferred locale and fire
208
   * an event when the all entries in the lexicon have been loaded.
209
   */
210
  private void readLexicon() {
211
    Lexicon.read( mWorkspace.getLocale() );
212
  }
213
214
  /**
215
   * When the window is maximized, full screen, or iconified, prevent updating
216
   * the window bounds. This is used so that if the user exits the application
217
   * when full screen (or maximized), restarting the application will recall
218
   * the previous bounds, allowing for continuity of expected behaviour.
219
   *
220
   * @param stage The window to check for "normal" status.
221
   * @return {@code false} when the bounds must not be changed, ergo
222
   * persisted.
223
   */
224
  private BooleanSupplier createBoundsEnabledSupplier( final Stage stage ) {
225
    return () ->
226
      !(stage.isMaximized() || stage.isFullScreen() || stage.isIconified());
227
  }
228
}
1229
A src/main/java/com/keenwrite/Launcher.java
1
/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite;
6
7
import com.keenwrite.cmdline.Arguments;
8
import com.keenwrite.cmdline.ColourScheme;
9
import com.keenwrite.cmdline.HeadlessApp;
10
import picocli.CommandLine;
11
12
import java.io.IOException;
13
import java.io.InputStream;
14
import java.io.PrintStream;
15
import java.util.Properties;
16
import java.util.function.Consumer;
17
import java.util.logging.LogManager;
18
19
import static com.keenwrite.Bootstrap.*;
20
import static com.keenwrite.security.PermissiveCertificate.installTrustManager;
21
import static java.lang.String.format;
22
import static picocli.CommandLine.IParameterExceptionHandler;
23
import static picocli.CommandLine.ParameterException;
24
import static picocli.CommandLine.UnmatchedArgumentException.printSuggestions;
25
26
/**
27
 * This is the main entry point to the application. The {@link Launcher} class
28
 * is responsible for running the application using either the {@link GuiApp} or
29
 * {@link HeadlessApp} class, depending on whether running with command-line
30
 * arguments.
31
 *
32
 * <p>
33
 * This is required until modules are implemented, which may never happen
34
 * because the application should be ported away from Java and JavaFX.
35
 * </p>
36
 */
37
public final class Launcher implements Consumer<Arguments> {
38
  static {
39
    // We don't care about the logging provider connection message.
40
    System.setProperty( "slf4j.internal.verbosity", "WARN" );
41
  }
42
43
  /**
44
   * Needed for the GUI.
45
   */
46
  private final String[] mArgs;
47
48
  /**
49
   * Where to write error messages.
50
   */
51
  private static final PrintStream ERRORS = System.err;
52
53
  /**
54
   * Responsible for informing the user of an invalid command-line option,
55
   * along with suggestions as to the closest argument name that matches.
56
   */
57
  private static final class ArgHandler implements IParameterExceptionHandler {
58
    /**
59
     * Invoked by the command-line parser when an invalid option is provided.
60
     *
61
     * @param ex   Captures information about the parameter.
62
     * @param args Captures the complete command-line arguments.
63
     * @return The application exit code (non-zero).
64
     */
65
    public int handleParseException(
66
      final ParameterException ex, final String[] args ) {
67
      final var cmd = ex.getCommandLine();
68
      final var writer = cmd.getErr();
69
      final var spec = cmd.getCommandSpec();
70
      final var mapper = cmd.getExitCodeExceptionMapper();
71
72
      writer.println( ex.getMessage() );
73
      printSuggestions( ex, writer );
74
      writer.print( cmd.getHelp().fullSynopsis() );
75
      writer.printf( "Run '%s --help' for details.%n", spec.qualifiedName() );
76
77
      return mapper == null
78
        ? spec.exitCodeOnInvalidInput()
79
        : mapper.getExitCode( ex );
80
    }
81
  }
82
83
  /**
84
   * Returns the application version number retrieved from the application
85
   * properties file. The properties file is generated at build time, which
86
   * keys off the repository.
87
   *
88
   * @return The application version number.
89
   * @throws RuntimeException An {@link IOException} occurred.
90
   */
91
  public static String getVersion() {
92
    try {
93
      final var properties = loadProperties( "app.properties" );
94
      return properties.getProperty( "application.version" );
95
    } catch( final IOException ex ) {
96
      throw new RuntimeException( ex );
97
    }
98
  }
99
100
  /**
101
   * Immediately exits the application.
102
   *
103
   * @param exitCode Code to provide back to the calling shell.
104
   */
105
  private static void terminate( final int exitCode ) {
106
    System.exit( exitCode );
107
  }
108
109
  private static void parse( final String[] args ) {
110
    assert args != null;
111
112
    final var arguments = new Arguments( new Launcher( args ) );
113
    final var parser = new CommandLine( arguments );
114
115
    parser.setColorScheme( ColourScheme.create() );
116
    parser.setParameterExceptionHandler( new ArgHandler() );
117
    parser.setUnmatchedArgumentsAllowed( false );
118
119
    final var parseResult = parser.parseArgs( args );
120
121
    if( parseResult.isVersionHelpRequested() ) {
122
      showAppInfo();
123
    }
124
125
    final var exitCode = parser.execute( args );
126
    terminate( exitCode );
127
  }
128
129
  @SuppressWarnings( "SameParameterValue" )
130
  private static Properties loadProperties( final String resource )
131
    throws IOException {
132
    final var properties = new Properties();
133
    properties.load( getResourceAsStream( getResourceName( resource ) ) );
134
    return properties;
135
  }
136
137
  private static String getResourceName( final String resource ) {
138
    return format( "%s/%s", getPackagePath(), resource );
139
  }
140
141
  private static String getPackagePath() {
142
    return Launcher.class.getPackageName().replace( '.', '/' );
143
  }
144
145
  private static InputStream getResourceAsStream( final String resource ) {
146
    return Launcher.class.getClassLoader().getResourceAsStream( resource );
147
  }
148
149
  /**
150
   * Logs the message of an error to the console.
151
   *
152
   * @param error The fatal error that could not be handled.
153
   */
154
  private static void log( final Throwable error ) {
155
    var message = error.getMessage();
156
157
    if( message != null && message.toLowerCase().contains( "javafx" ) ) {
158
      message = "Run using a Java Runtime Environment that includes JavaFX.";
159
      log( "ERROR: %s", message );
160
    }
161
    else {
162
      error.printStackTrace( ERRORS );
163
    }
164
  }
165
166
  /**
167
   * Suppress writing log messages.
168
   */
169
  private static void disableLogging() {
170
    LogManager.getLogManager().reset();
171
  }
172
173
  /**
174
   * Writes the given placeholder text to standard output with a new line
175
   * appended.
176
   *
177
   * @param message The format string specifier.
178
   * @param args    The arguments to substitute into the format string.
179
   */
180
  private static void log( final String message, final Object... args ) {
181
    ERRORS.printf( format( "%s%n", message ), args );
182
    ERRORS.flush();
183
  }
184
185
  private static void showAppInfo() {
186
    log( "%s version %s", APP_TITLE, APP_VERSION );
187
    log( "Copyright 2016-%s White Magic Software, Ltd.", APP_YEAR );
188
  }
189
190
  /**
191
   * @param args Command-line arguments (passed into the GUI).
192
   */
193
  public Launcher( final String[] args ) {
194
    mArgs = args;
195
  }
196
197
  /**
198
   * Called after the arguments have been parsed.
199
   *
200
   * @param arguments The parsed command-line arguments.
201
   */
202
  @Override
203
  public void accept( final Arguments arguments ) {
204
    assert arguments != null;
205
206
    try {
207
      int argCount = mArgs.length;
208
209
      if( arguments.quiet() ) {
210
        argCount--;
211
      }
212
      else {
213
        showAppInfo();
214
      }
215
216
      if( arguments.debug() ) {
217
        argCount--;
218
        arguments.iterate( null, Launcher::log );
219
      }
220
      else {
221
        disableLogging();
222
      }
223
224
      if( argCount <= 0 ) {
225
        // When no command-line arguments are provided, launch the GUI.
226
        GuiApp.run( mArgs );
227
      }
228
      else {
229
        // When command-line arguments are supplied, run in headless mode.
230
        HeadlessApp.run( arguments, ERRORS );
231
      }
232
    } catch( final Throwable t ) {
233
      log( t );
234
    }
235
  }
236
237
  /**
238
   * Delegates running the application via the command-line argument parser.
239
   * This is the main entry point for the application, regardless of whether
240
   * run from the command-line or as a GUI.
241
   *
242
   * @param args Command-line arguments.
243
   */
244
  public static void main( final String[] args ) {
245
    installTrustManager();
246
    parse( args );
247
  }
248
}
1249
A src/main/java/com/keenwrite/MainPane.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.constants.Constants;
5
import com.keenwrite.editors.TextDefinition;
6
import com.keenwrite.editors.TextEditor;
7
import com.keenwrite.editors.TextResource;
8
import com.keenwrite.editors.common.ScrollEventHandler;
9
import com.keenwrite.editors.common.VariableNameInjector;
10
import com.keenwrite.editors.definition.DefinitionEditor;
11
import com.keenwrite.editors.definition.TreeTransformer;
12
import com.keenwrite.editors.definition.yaml.YamlTreeTransformer;
13
import com.keenwrite.editors.markdown.MarkdownEditor;
14
import com.keenwrite.events.*;
15
import com.keenwrite.events.spelling.LexiconLoadedEvent;
16
import com.keenwrite.io.MediaType;
17
import com.keenwrite.io.MediaTypeExtension;
18
import com.keenwrite.preferences.Workspace;
19
import com.keenwrite.preview.HtmlPreview;
20
import com.keenwrite.processors.Processor;
21
import com.keenwrite.processors.ProcessorContext;
22
import com.keenwrite.processors.ProcessorFactory;
23
import com.keenwrite.processors.html.HtmlPreviewProcessor;
24
import com.keenwrite.processors.r.Engine;
25
import com.keenwrite.processors.r.RBootstrapController;
26
import com.keenwrite.service.events.Notifier;
27
import com.keenwrite.spelling.api.SpellChecker;
28
import com.keenwrite.spelling.impl.PermissiveSpeller;
29
import com.keenwrite.spelling.impl.SymSpellSpeller;
30
import com.keenwrite.typesetting.installer.TypesetterInstaller;
31
import com.keenwrite.ui.explorer.FilePickerFactory;
32
import com.keenwrite.ui.heuristics.DocumentStatistics;
33
import com.keenwrite.ui.outline.DocumentOutline;
34
import com.keenwrite.ui.spelling.TextEditorSpellChecker;
35
import com.keenwrite.util.GenericBuilder;
36
import com.panemu.tiwulfx.control.dock.DetachableTab;
37
import com.panemu.tiwulfx.control.dock.DetachableTabPane;
38
import javafx.beans.property.*;
39
import javafx.collections.ListChangeListener;
40
import javafx.concurrent.Task;
41
import javafx.event.ActionEvent;
42
import javafx.event.Event;
43
import javafx.event.EventHandler;
44
import javafx.scene.Node;
45
import javafx.scene.Scene;
46
import javafx.scene.control.SplitPane;
47
import javafx.scene.control.Tab;
48
import javafx.scene.control.TabPane;
49
import javafx.scene.control.Tooltip;
50
import javafx.scene.control.TreeItem.TreeModificationEvent;
51
import javafx.scene.input.KeyEvent;
52
import javafx.stage.Stage;
53
import javafx.stage.Window;
54
import org.greenrobot.eventbus.Subscribe;
55
56
import java.io.File;
57
import java.io.FileNotFoundException;
58
import java.nio.file.Path;
59
import java.util.*;
60
import java.util.concurrent.ExecutorService;
61
import java.util.concurrent.ScheduledExecutorService;
62
import java.util.concurrent.ScheduledFuture;
63
import java.util.concurrent.atomic.AtomicBoolean;
64
import java.util.concurrent.atomic.AtomicReference;
65
import java.util.function.Consumer;
66
import java.util.function.Function;
67
import java.util.stream.Collectors;
68
69
import static com.keenwrite.ExportFormat.NONE;
70
import static com.keenwrite.Messages.get;
71
import static com.keenwrite.constants.Constants.*;
72
import static com.keenwrite.events.Bus.register;
73
import static com.keenwrite.events.StatusEvent.clue;
74
import static com.keenwrite.io.MediaType.*;
75
import static com.keenwrite.io.MediaType.TypeName.TEXT;
76
import static com.keenwrite.io.SysFile.toFile;
77
import static com.keenwrite.preferences.AppKeys.*;
78
import static com.keenwrite.processors.ProcessorContext.Mutator;
79
import static com.keenwrite.processors.ProcessorContext.builder;
80
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
81
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
82
import static java.awt.Desktop.getDesktop;
83
import static java.lang.String.*;
84
import static java.util.concurrent.Executors.newFixedThreadPool;
85
import static java.util.concurrent.Executors.newScheduledThreadPool;
86
import static java.util.concurrent.TimeUnit.SECONDS;
87
import static java.util.stream.Collectors.groupingBy;
88
import static javafx.application.Platform.exit;
89
import static javafx.application.Platform.runLater;
90
import static javafx.scene.control.ButtonType.NO;
91
import static javafx.scene.control.ButtonType.YES;
92
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
93
import static javafx.scene.input.KeyCode.ENTER;
94
import static javafx.scene.input.KeyCode.SPACE;
95
import static javafx.scene.input.KeyCombination.ALT_DOWN;
96
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
97
import static javafx.util.Duration.millis;
98
import static javax.swing.SwingUtilities.invokeLater;
99
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
100
101
/**
102
 * Responsible for wiring together the main application components for a
103
 * particular {@link Workspace} (project). These include the definition views,
104
 * text editors, and preview pane along with any corresponding controllers.
105
 */
106
public final class MainPane extends SplitPane {
107
108
  private static final ExecutorService sExecutor = newFixedThreadPool( 1 );
109
  private static final Notifier sNotifier = Services.load( Notifier.class );
110
111
  /**
112
   * Used when opening files to determine how each file should be binned and
113
   * therefore what tab pane to be opened within.
114
   */
115
  private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of(
116
    TEXT_MARKDOWN, TEXT_R_MARKDOWN, UNDEFINED
117
  );
118
119
  private final ScheduledExecutorService mSaver = newScheduledThreadPool( 1 );
120
  private final AtomicReference<ScheduledFuture<?>> mSaveTask =
121
    new AtomicReference<>();
122
123
  /**
124
   * Prevents re-instantiation of processing classes.
125
   */
126
  private final Map<TextResource, Processor<String>> mProcessors =
127
    new HashMap<>();
128
129
  private final Workspace mWorkspace;
130
131
  /**
132
   * Groups similar file type tabs together.
133
   */
134
  private final List<TabPane> mTabPanes = new ArrayList<>();
135
136
  /**
137
   * Renders the actively selected plain text editor tab.
138
   */
139
  private final HtmlPreview mPreview;
140
141
  /**
142
   * Provides an interactive document outline.
143
   */
144
  private final DocumentOutline mOutline = new DocumentOutline();
145
146
  /**
147
   * Changing the active editor fires the value changed event. This allows
148
   * refreshes to happen when external definitions are modified and need to
149
   * trigger the processing chain.
150
   */
151
  private final ObjectProperty<TextEditor> mTextEditor =
152
    new SimpleObjectProperty<>();
153
154
  /**
155
   * Changing the active definition editor fires the value changed event. This
156
   * allows refreshes to happen when external definitions are modified and need
157
   * to trigger the processing chain.
158
   */
159
  private final ObjectProperty<TextDefinition> mDefinitionEditor =
160
    new SimpleObjectProperty<>();
161
162
  private final ObjectProperty<SpellChecker> mSpellChecker;
163
164
  private final TextEditorSpellChecker mEditorSpeller;
165
166
  /**
167
   * Called when the definition data is changed.
168
   */
169
  private final EventHandler<TreeModificationEvent<Event>> mTreeHandler =
170
    _ -> {
171
      process( getTextEditor() );
172
      save( getTextDefinition() );
173
    };
174
175
  /**
176
   * Tracks the number of detached tab panels opened into their own windows,
177
   * which allows unique identification of subordinate windows by their title.
178
   * It is doubtful more than 128 windows, much less 256, will be created.
179
   */
180
  private byte mWindowCount;
181
182
  private final VariableNameInjector mVariableNameInjector;
183
184
  private final RBootstrapController mRBootstrapController;
185
186
  private final DocumentStatistics mStatistics;
187
188
  @SuppressWarnings( { "FieldCanBeLocal", "unused" } )
189
  private final TypesetterInstaller mInstallWizard;
190
191
  /**
192
   * Adds all content panels to the main user interface. This will load the
193
   * configuration settings from the workspace to reproduce the settings from
194
   * a previous session.
195
   */
196
  public MainPane( final Workspace workspace ) {
197
    mWorkspace = workspace;
198
    mSpellChecker = createSpellChecker();
199
    mEditorSpeller = createTextEditorSpellChecker( mSpellChecker );
200
    mPreview = new HtmlPreview( workspace );
201
    mStatistics = new DocumentStatistics( workspace );
202
203
    mTextEditor.addListener( ( _, o, n ) -> {
204
      if( o != null ) {
205
        removeProcessor( o );
206
      }
207
208
      if( n != null ) {
209
        mPreview.setBaseUri( n.getPath() );
210
        updateProcessors( n );
211
        process( n );
212
      }
213
    } );
214
215
    mTextEditor.set( createMarkdownEditor( DOCUMENT_DEFAULT ) );
216
    mDefinitionEditor.set( createDefinitionEditor( workspace ) );
217
    mVariableNameInjector = new VariableNameInjector( workspace );
218
    mRBootstrapController = new RBootstrapController(
219
      workspace, mDefinitionEditor.get()::getDefinitions
220
    );
221
222
    // If the user modifies the definitions, re-process the variables.
223
    mDefinitionEditor.addListener( ( _, _, _ ) -> {
224
      final var textEditor = getTextEditor();
225
226
      if( textEditor.isMediaType( TEXT_R_MARKDOWN ) ) {
227
        mRBootstrapController.update();
228
      }
229
230
      process( textEditor );
231
    } );
232
233
    open( collect( getRecentFiles() ) );
234
    viewPreview();
235
    setDividerPositions( calculateDividerPositions() );
236
237
    // Once the main scene's window regains focus, update the active definition
238
    // editor to the currently selected tab.
239
    runLater( () -> getWindow().setOnCloseRequest( event -> {
240
      // Order matters: Open file names must be persisted before closing all.
241
      mWorkspace.save();
242
243
      if( closeAll() ) {
244
        exit();
245
      }
246
247
      event.consume();
248
    } ) );
249
250
    register( this );
251
    initAutosave( workspace );
252
253
    restoreSession();
254
    runLater( this::restoreFocus );
255
256
    mInstallWizard = new TypesetterInstaller( workspace );
257
  }
258
259
  /**
260
   * Called when spellchecking can be run. This will reload the dictionary
261
   * into memory once, and then re-use it for all the existing text editors.
262
   *
263
   * @param event The event to process, having a populated word-frequency map.
264
   */
265
  @Subscribe
266
  public void handle( final LexiconLoadedEvent event ) {
267
    final var lexicon = event.getLexicon();
268
269
    try {
270
      final var checker = SymSpellSpeller.forLexicon( lexicon );
271
      mSpellChecker.set( checker );
272
    } catch( final Exception ex ) {
273
      clue( ex );
274
    }
275
  }
276
277
  @Subscribe
278
  public void handle( final TextEditorFocusEvent event ) {
279
    mTextEditor.set( event.get() );
280
  }
281
282
  @Subscribe
283
  public void handle( final TextDefinitionFocusEvent event ) {
284
    mDefinitionEditor.set( event.get() );
285
  }
286
287
  /**
288
   * Typically called when a file name is clicked in the preview panel.
289
   *
290
   * @param event The event to process, must contain a valid file reference.
291
   */
292
  @Subscribe
293
  public void handle( final FileOpenEvent event ) {
294
    final File eventFile;
295
    final var eventUri = event.getUri();
296
297
    if( eventUri.isAbsolute() ) {
298
      eventFile = new File( eventUri.getPath() );
299
    }
300
    else {
301
      final var activeFile = getTextEditor().getFile();
302
      final var parent = activeFile.getParentFile();
303
304
      if( parent == null ) {
305
        clue( new FileNotFoundException( eventUri.getPath() ) );
306
        return;
307
      }
308
      else {
309
        final var parentPath = parent.getAbsolutePath();
310
        eventFile = toFile( Path.of( parentPath, eventUri.getPath() ) );
311
      }
312
    }
313
314
    final var mediaType = MediaTypeExtension.fromFile( eventFile );
315
316
    runLater( () -> {
317
      // Open text files locally.
318
      if( mediaType.isType( TEXT ) ) {
319
        open( eventFile );
320
      }
321
      else {
322
        try {
323
          // Delegate opening all other file types to the operating system.
324
          getDesktop().open( eventFile );
325
        } catch( final Exception ex ) {
326
          clue( ex );
327
        }
328
      }
329
    } );
330
  }
331
332
  @Subscribe
333
  public void handle( final CaretNavigationEvent event ) {
334
    runLater( () -> {
335
      final var textArea = getTextEditor();
336
      textArea.moveTo( event.getOffset() );
337
      textArea.requestFocus();
338
    } );
339
  }
340
341
  @Subscribe
342
  public void handle( final InsertDefinitionEvent<String> event ) {
343
    final var leaf = event.getLeaf();
344
    final var editor = mTextEditor.get();
345
346
    mVariableNameInjector.insert( editor, leaf );
347
  }
348
349
  private void initAutosave( final Workspace workspace ) {
350
    final var rate = workspace.integerProperty( KEY_EDITOR_AUTOSAVE );
351
352
    rate.addListener(
353
      ( _, _, _ ) -> {
354
        final var taskRef = mSaveTask.get();
355
356
        // Prevent multiple auto-saves from running.
357
        if( taskRef != null ) {
358
          taskRef.cancel( false );
359
        }
360
361
        initAutosave( rate );
362
      }
363
    );
364
365
    // Start the save listener (avoids duplicating some code).
366
    initAutosave( rate );
367
  }
368
369
  private void initAutosave( final IntegerProperty rate ) {
370
    mSaveTask.set(
371
      mSaver.scheduleAtFixedRate(
372
        () -> {
373
          if( getTextEditor().isModified() ) {
374
            // Ensure the modified indicator is cleared by running on EDT.
375
            runLater( this::save );
376
          }
377
        }, 0, rate.intValue(), SECONDS
378
      )
379
    );
380
  }
381
382
  /**
383
   * TODO: Load divider positions from exported settings, see
384
   *   {@link #collect(SetProperty)} comment.
385
   */
386
  private double[] calculateDividerPositions() {
387
    final var ratio = 100f / getItems().size() / 100;
388
    final var positions = getDividerPositions();
389
390
    for( int i = 0; i < positions.length; i++ ) {
391
      positions[ i ] = ratio * i;
392
    }
393
394
    return positions;
395
  }
396
397
  /**
398
   * Opens all the files into the application, provided the paths are unique.
399
   * This may only be called for any type of files that a user can edit
400
   * (i.e., update and persist), such as definitions and text files.
401
   *
402
   * @param files The list of files to open.
403
   */
404
  public void open( final List<File> files ) {
405
    files.forEach( this::open );
406
  }
407
408
  /**
409
   * This opens the given file. Since the preview pane is not a file that
410
   * can be opened, it is safe to add a listener to the detachable pane.
411
   * This will exit early if the given file is not a regular file (i.e., a
412
   * directory).
413
   *
414
   * @param inputFile The file to open.
415
   */
416
  private void open( final File inputFile ) {
417
    // Prevent opening directories (a non-existent "untitled.md" is fine).
418
    if( !inputFile.isFile() && inputFile.exists() ) {
419
      return;
420
    }
421
422
    final var mediaType = fromFilename( inputFile );
423
424
    // Only allow opening text files.
425
    if( !mediaType.isType( TEXT ) ) {
426
      return;
427
    }
428
429
    final var tab = createTab( inputFile );
430
    final var node = tab.getContent();
431
    final var tabPane = obtainTabPane( mediaType );
432
433
    tab.setTooltip( createTooltip( inputFile ) );
434
    tabPane.setFocusTraversable( false );
435
    tabPane.setTabClosingPolicy( ALL_TABS );
436
    tabPane.getTabs().add( tab );
437
438
    // Attach the tab scene factory for new tab panes.
439
    if( !getItems().contains( tabPane ) ) {
440
      addTabPane(
441
        node instanceof TextDefinition ? 0 : getItems().size(), tabPane
442
      );
443
    }
444
445
    if( inputFile.isFile() ) {
446
      getRecentFiles().add( inputFile.getAbsolutePath() );
447
448
      final var dir = inputFile.getParentFile();
449
      mWorkspace.fileProperty( KEY_UI_RECENT_DIR ).setValue( dir );
450
    }
451
  }
452
453
  /**
454
   * Gives focus to the most recently edited document and attempts to move
455
   * the caret to the most recently known offset into said document.
456
   */
457
  private void restoreSession() {
458
    final var workspace = getWorkspace();
459
    final var file = workspace.fileProperty( KEY_UI_RECENT_DOCUMENT );
460
    final var offset = workspace.integerProperty( KEY_UI_RECENT_OFFSET );
461
462
    for( final var pane : mTabPanes ) {
463
      for( final var tab : pane.getTabs() ) {
464
        final var tooltip = tab.getTooltip();
465
466
        if( tooltip != null ) {
467
          final var tabName = tooltip.getText();
468
          final var fileName = file.get().toString();
469
470
          if( tabName.equalsIgnoreCase( fileName ) ) {
471
            final var node = tab.getContent();
472
473
            pane.getSelectionModel().select( tab );
474
            node.requestFocus();
475
476
            if( node instanceof TextEditor editor ) {
477
              runLater( () -> editor.moveTo( offset.getValue() ) );
478
            }
479
480
            break;
481
          }
482
        }
483
      }
484
    }
485
  }
486
487
  /**
488
   * Sets the focus to the middle pane, which contains the text editor tabs.
489
   */
490
  private void restoreFocus() {
491
    // Work around a bug where focusing directly on the middle pane results
492
    // in the R engine not loading variables properly.
493
    mTabPanes.get( 0 ).requestFocus();
494
495
    // This is the only line that should be required.
496
    mTabPanes.get( 1 ).requestFocus();
497
  }
498
499
  /**
500
   * Opens a new text editor document using a document file name that doesn't
501
   * clash with an existing document.
502
   */
503
  public void newTextEditor() {
504
    final String key = "file.default.document.";
505
    final String prefix = Constants.get( format( "%s%s", key, "prefix" ) );
506
    final String suffix = Constants.get( format( "%s%s", key, "suffix" ) );
507
508
    File file = new File( format( "%s.%s", prefix, suffix ) );
509
    int i = 0;
510
511
    while( file.exists() && i++ < 100 ) {
512
      file = new File( format( "%s-%s.%s", prefix, i, suffix ) );
513
    }
514
515
    open( file );
516
  }
517
518
  /**
519
   * Opens a new definition editor document using the default definition
520
   * file name.
521
   */
522
  @SuppressWarnings( "unused" )
523
  public void newDefinitionEditor() {
524
    open( DEFINITION_DEFAULT );
525
  }
526
527
  /**
528
   * Iterates over all tab panes to find all {@link TextEditor}s and request
529
   * that they save themselves.
530
   */
531
  public void saveAll() {
532
    iterateEditors( this::save );
533
  }
534
535
  /**
536
   * Requests that the active {@link TextEditor} saves itself. Don't bother
537
   * checking if modified first because if the user swaps external media from
538
   * an external source (e.g., USB thumb drive), save should not second-guess
539
   * the user: save always re-saves. Also, it's less code.
540
   */
541
  public void save() {
542
    save( getTextEditor() );
543
  }
544
545
  /**
546
   * Saves the active {@link TextEditor} under a new name.
547
   *
548
   * @param files The new active editor {@link File} reference, must contain
549
   *              at least one element.
550
   */
551
  public void saveAs( final List<File> files ) {
552
    assert files != null;
553
    assert !files.isEmpty();
554
    final var editor = getTextEditor();
555
    final var tab = getTab( editor );
556
    final var file = files.getFirst();
557
558
    // If the file type has changed, refresh the processors.
559
    final var mediaType = fromFilename( file );
560
    final var typeChanged = !editor.isMediaType( mediaType );
561
562
    if( typeChanged ) {
563
      removeProcessor( editor );
564
    }
565
566
    editor.rename( file );
567
    tab.ifPresent( t -> {
568
      t.setText( editor.getFilename() );
569
      t.setTooltip( createTooltip( file ) );
570
    } );
571
572
    if( typeChanged ) {
573
      updateProcessors( editor );
574
      process( editor );
575
    }
576
577
    save();
578
  }
579
580
  /**
581
   * Saves the given {@link TextResource} to a file. This is typically used
582
   * to save either an instance of {@link TextEditor} or {@link TextDefinition}.
583
   *
584
   * @param resource The resource to export.
585
   */
586
  private void save( final TextResource resource ) {
587
    try {
588
      resource.save();
589
    } catch( final Exception ex ) {
590
      clue( ex );
591
      sNotifier.alert(
592
        getWindow(), resource.getPath(), "TextResource.saveFailed", ex
593
      );
594
    }
595
  }
596
597
  /**
598
   * Closes all open {@link TextEditor}s; all {@link TextDefinition}s stay open.
599
   *
600
   * @return {@code true} when all editors, modified or otherwise, were
601
   * permitted to close; {@code false} when one or more editors were modified
602
   * and the user requested no closing.
603
   */
604
  public boolean closeAll() {
605
    var closable = true;
606
607
    for( final var tabPane : mTabPanes ) {
608
      final var tabIterator = tabPane.getTabs().iterator();
609
610
      while( tabIterator.hasNext() ) {
611
        final var tab = tabIterator.next();
612
        final var resource = tab.getContent();
613
614
        // The definition panes auto-save, so being specific here prevents
615
        // closing the definitions in the situation where the user wants to
616
        // continue editing (i.e., possibly save unsaved work).
617
        if( !(resource instanceof TextEditor) ) {
618
          continue;
619
        }
620
621
        if( canClose( (TextEditor) resource ) ) {
622
          tabIterator.remove();
623
          close( tab );
624
        }
625
        else {
626
          closable = false;
627
        }
628
      }
629
    }
630
631
    return closable;
632
  }
633
634
  /**
635
   * Calls the tab's {@link Tab#getOnClosed()} handler to carry out a close
636
   * event.
637
   *
638
   * @param tab The {@link Tab} that was closed.
639
   */
640
  private void close( final Tab tab ) {
641
    assert tab != null;
642
643
    final var handler = tab.getOnClosed();
644
645
    if( handler != null ) {
646
      handler.handle( new ActionEvent() );
647
    }
648
  }
649
650
  /**
651
   * Closes the active tab; delegates to {@link #canClose(TextResource)}.
652
   */
653
  public void close() {
654
    final var editor = getTextEditor();
655
656
    if( canClose( editor ) ) {
657
      close( editor );
658
      removeProcessor( editor );
659
    }
660
  }
661
662
  /**
663
   * Closes the given {@link TextResource}. This must not be called from within
664
   * a loop that iterates over the tab panes using {@code forEach}, lest a
665
   * concurrent modification exception be thrown.
666
   *
667
   * @param resource The {@link TextResource} to close, without confirming with
668
   *                 the user.
669
   */
670
  private void close( final TextResource resource ) {
671
    getTab( resource ).ifPresent(
672
      tab -> {
673
        close( tab );
674
        tab.getTabPane().getTabs().remove( tab );
675
      }
676
    );
677
  }
678
679
  /**
680
   * Answers whether the given {@link TextResource} may be closed.
681
   *
682
   * @param editor The {@link TextResource} to try closing.
683
   * @return {@code true} when the editor may be closed; {@code false} when
684
   * the user has requested to keep the editor open.
685
   */
686
  private boolean canClose( final TextResource editor ) {
687
    final var editorTab = getTab( editor );
688
    final var canClose = new AtomicBoolean( true );
689
690
    if( editor.isModified() ) {
691
      final var filename = new StringBuilder();
692
      editorTab.ifPresent( tab -> filename.append( tab.getText() ) );
693
694
      final var message = sNotifier.createNotification(
695
        Messages.get( "Alert.file.close.title" ),
696
        Messages.get( "Alert.file.close.text" ),
697
        filename.toString()
698
      );
699
700
      final var dialog = sNotifier.createConfirmation( getWindow(), message );
701
702
      dialog.showAndWait().ifPresent(
703
        save -> canClose.set( save == YES ? editor.save() : save == NO )
704
      );
705
    }
706
707
    return canClose.get();
708
  }
709
710
  private void iterateEditors( final Consumer<TextEditor> consumer ) {
711
    mTabPanes.forEach(
712
      tp -> tp.getTabs().forEach( tab -> {
713
        final var node = tab.getContent();
714
715
        if( node instanceof final TextEditor editor ) {
716
          consumer.accept( editor );
717
        }
718
      } )
719
    );
720
  }
721
722
  /**
723
   * Adds the HTML preview tab to its own, singular tab pane.
724
   */
725
  public void viewPreview() {
726
    addTab( mPreview, TEXT_HTML, "Pane.preview.title" );
727
  }
728
729
  /**
730
   * Adds the document outline tab to its own, singular tab pane.
731
   */
732
  public void viewOutline() {
733
    addTab( mOutline, APP_DOCUMENT_OUTLINE, "Pane.outline.title" );
734
  }
735
736
  public void viewStatistics() {
737
    addTab( mStatistics, APP_DOCUMENT_STATISTICS, "Pane.statistics.title" );
738
  }
739
740
  public void viewFiles() {
741
    try {
742
      final var factory = new FilePickerFactory( getWorkspace() );
743
      final var fileManager = factory.createModeless();
744
      addTab( fileManager, APP_FILE_MANAGER, "Pane.files.title" );
745
    } catch( final Exception ex ) {
746
      clue( ex );
747
    }
748
  }
749
750
  public void viewRefresh() {
751
    mPreview.refresh();
752
    Engine.clear();
753
    mRBootstrapController.update();
754
  }
755
756
  private void addTab(
757
    final Node node, final MediaType mediaType, final String key ) {
758
    final var tabPane = obtainTabPane( mediaType );
759
760
    for( final var tab : tabPane.getTabs() ) {
761
      if( tab.getContent() == node ) {
762
        return;
763
      }
764
    }
765
766
    tabPane.getTabs().add( createTab( get( key ), node ) );
767
    addTabPane( tabPane );
768
  }
769
770
  /**
771
   * Returns the tab that contains the given {@link TextEditor}.
772
   *
773
   * @param editor The {@link TextEditor} instance to find amongst the tabs.
774
   * @return The first tab having content that matches the given tab.
775
   */
776
  private Optional<Tab> getTab( final TextResource editor ) {
777
    return mTabPanes.stream()
778
                    .flatMap( pane -> pane.getTabs().stream() )
779
                    .filter( tab -> editor.equals( tab.getContent() ) )
780
                    .findFirst();
781
  }
782
783
  private TextDefinition createDefinitionEditor( final File file ) {
784
    final var editor = new DefinitionEditor( file, createTreeTransformer() );
785
786
    editor.addTreeChangeHandler( mTreeHandler );
787
788
    return editor;
789
  }
790
791
  /**
792
   * Creates a new {@link DefinitionEditor} wrapped in a listener that
793
   * is used to detect when the active {@link DefinitionEditor} has changed.
794
   * Upon changing, the variables are interpolated and the active text editor
795
   * is refreshed.
796
   *
797
   * @param workspace Has the most recently edited definitions file name.
798
   * @return A newly configured property that represents the active
799
   * {@link DefinitionEditor}, never {@code null}.
800
   */
801
  private TextDefinition createDefinitionEditor(
802
    final Workspace workspace ) {
803
    final var fileProperty = workspace.fileProperty( KEY_UI_RECENT_DEFINITION );
804
    final var filename = fileProperty.get();
805
    final SetProperty<String> recent = workspace.setsProperty(
806
      KEY_UI_RECENT_OPEN_PATH
807
    );
808
809
    // Open the most recently used YAML definition file.
810
    for( final var recentFile : recent.get() ) {
811
      if( recentFile.endsWith( filename.toString() ) ) {
812
        return createDefinitionEditor( new File( recentFile ) );
813
      }
814
    }
815
816
    return createDefaultDefinitionEditor();
817
  }
818
819
  private TextDefinition createDefaultDefinitionEditor() {
820
    final var transformer = createTreeTransformer();
821
    return new DefinitionEditor( transformer );
822
  }
823
824
  private TreeTransformer createTreeTransformer() {
825
    return new YamlTreeTransformer();
826
  }
827
828
  private Tab createTab( final String filename, final Node node ) {
829
    return new DetachableTab( filename, node );
830
  }
831
832
  private Tab createTab( final File file ) {
833
    final var r = createTextResource( file );
834
    final var filename = r.getFilename();
835
    final var tab = createTab( filename, r.getNode() );
836
837
    r.modifiedProperty().addListener(
838
      ( _, _, n ) -> tab.setText( filename + (n ? "*" : "") )
839
    );
840
841
    // This is called when either the tab is closed by the user clicking on
842
    // the tab's close icon or when closing (all) from the file menu.
843
    tab.setOnClosed(
844
      _ -> getRecentFiles().remove( file.getAbsolutePath() )
845
    );
846
847
    // When closing a tab, give focus to the newly revealed tab.
848
    tab.selectedProperty().addListener( ( _, _, n ) -> {
849
      if( n != null && n ) {
850
        final var pane = tab.getTabPane();
851
852
        if( pane != null ) {
853
          pane.requestFocus();
854
        }
855
      }
856
    } );
857
858
    tab.tabPaneProperty().addListener( ( _, _, nPane ) -> {
859
      if( nPane != null ) {
860
        nPane.focusedProperty().addListener( ( _, _, n ) -> {
861
          if( n != null && n ) {
862
            final var model = nPane.getSelectionModel();
863
864
            if( model != null ) {
865
              final var selected = model.getSelectedItem();
866
867
              if( selected != null ) {
868
                final var node = selected.getContent();
869
                node.requestFocus();
870
              }
871
            }
872
          }
873
        } );
874
      }
875
    } );
876
877
    return tab;
878
  }
879
880
  /**
881
   * Creates bins for the different {@link MediaType}s, which eventually are
882
   * added to the UI as separate tab panes. If ever a general-purpose scene
883
   * exporter is developed to serialize a scene to an FXML file, this could
884
   * be replaced by such a class.
885
   * <p>
886
   * When binning the files, this makes sure that at least one file exists
887
   * for every type. If the user has opted to close a particular type (such
888
   * as the definition pane), the view will suppressed elsewhere.
889
   * </p>
890
   * <p>
891
   * The order that the binned files are returned will be reflected in the
892
   * order that the corresponding panes are rendered in the UI.
893
   * </p>
894
   *
895
   * @param paths The file paths to bin according to their type.
896
   * @return An in-order list of files, first by structured definition files,
897
   * then by plain text documents.
898
   */
899
  private List<File> collect( final SetProperty<String> paths ) {
900
    // Treat all files destined for the text editor as plain text documents
901
    // so that they are added to the same pane. Grouping by TEXT_PLAIN is a
902
    // bit arbitrary, but means explicitly capturing TEXT_PLAIN isn't needed.
903
    final Function<MediaType, MediaType> bin =
904
      m -> PLAIN_TEXT_FORMAT.contains( m ) ? TEXT_PLAIN : m;
905
906
    // Create two groups: YAML files and plain text files. The order that
907
    // the elements are listed in the enumeration for media types determines
908
    // what files are loaded first. Variable definitions come before all other
909
    // plain text documents.
910
    final var bins = paths
911
      .stream()
912
      .collect(
913
        groupingBy(
914
          path -> bin.apply( fromFilename( path ) ),
915
          () -> new TreeMap<>( Enum::compareTo ),
916
          Collectors.toList()
917
        )
918
      );
919
920
    bins.putIfAbsent( TEXT_YAML, List.of( DEFINITION_DEFAULT.toString() ) );
921
    bins.putIfAbsent( TEXT_PLAIN, List.of( DOCUMENT_DEFAULT.toString() ) );
922
923
    final var result = new LinkedList<File>();
924
925
    // Ensure that the same types are listed together (keep insertion order).
926
    bins.forEach( ( _, files ) -> result.addAll(
927
      files.stream().map( File::new ).toList() )
928
    );
929
930
    return result;
931
  }
932
933
  /**
934
   * Force the active editor to update, which will cause the processor
935
   * to re-evaluate the interpolated definition map thereby updating the
936
   * preview pane.
937
   *
938
   * @param editor Contains the source document to update in the preview pane.
939
   */
940
  private void process( final TextEditor editor ) {
941
    // Ensure processing does not run on the JavaFX thread, which frees the
942
    // text editor immediately for caret movement. The preview will have a
943
    // slight delay when catching up to the caret position.
944
    final var task = new Task<Void>() {
945
      @Override
946
      public Void call() {
947
        try {
948
          final var p = mProcessors.getOrDefault( editor, IDENTITY );
949
          p.apply( editor == null ? "" : editor.getText() );
950
        } catch( final Exception ex ) {
951
          clue( ex );
952
        }
953
954
        return null;
955
      }
956
    };
957
958
    // TODO: Each time the editor successfully runs the processor, the task is
959
    //   considered successful. Due to the rapid-fire nature of processing
960
    //   (e.g., keyboard navigation, fast typing), it isn't necessary to
961
    //   scroll each time.
962
    //   The algorithm:
963
    //   1. Peek at the oldest time.
964
    //   2. If the difference between the oldest time and current time exceeds
965
    //      250 milliseconds, then invoke the scrolling.
966
    //   3. Insert the current time into the circular queue.
967
    task.setOnSucceeded(
968
      _ -> invokeLater( () -> mPreview.scrollTo( CARET_ID ) )
969
    );
970
971
    // Prevents multiple process requests from executing simultaneously (due
972
    // to having a restricted queue size).
973
    sExecutor.execute( task );
974
  }
975
976
  /**
977
   * Lazily creates a {@link TabPane} configured to listen for tab select
978
   * events. The tab pane is associated with a given media type so that
979
   * similar files can be grouped together.
980
   *
981
   * @param mediaType The media type to associate with the tab pane.
982
   * @return An instance of {@link TabPane} that will handle tab docking.
983
   */
984
  private TabPane obtainTabPane( final MediaType mediaType ) {
985
    for( final var pane : mTabPanes ) {
986
      for( final var tab : pane.getTabs() ) {
987
        final var node = tab.getContent();
988
989
        if( node instanceof TextResource r && r.supports( mediaType ) ) {
990
          return pane;
991
        }
992
      }
993
    }
994
995
    final var pane = createTabPane();
996
    mTabPanes.add( pane );
997
    return pane;
998
  }
999
1000
  /**
1001
   * Creates an initialized {@link TabPane} instance.
1002
   *
1003
   * @return A new {@link TabPane} with all listeners configured.
1004
   */
1005
  private TabPane createTabPane() {
1006
    final var tabPane = new DetachableTabPane();
1007
1008
    initStageOwnerFactory( tabPane );
1009
    initTabListener( tabPane );
1010
1011
    return tabPane;
1012
  }
1013
1014
  /**
1015
   * When any {@link DetachableTabPane} is detached from the main window,
1016
   * the stage owner factory must be given its parent window, which will
1017
   * own the child window. The parent window is the {@link MainPane}'s
1018
   * {@link Scene}'s {@link Window} instance.
1019
   *
1020
   * <p>
1021
   * This will derives the new title from the main window title, incrementing
1022
   * the window count to help uniquely identify the child windows.
1023
   * </p>
1024
   *
1025
   * @param tabPane A new {@link DetachableTabPane} to configure.
1026
   */
1027
  private void initStageOwnerFactory( final DetachableTabPane tabPane ) {
1028
    tabPane.setStageOwnerFactory( stage -> {
1029
      final var title = get(
1030
        "Detach.tab.title",
1031
        ((Stage) getWindow()).getTitle(), ++mWindowCount
1032
      );
1033
      stage.setTitle( title );
1034
1035
      return getScene().getWindow();
1036
    } );
1037
  }
1038
1039
  /**
1040
   * Responsible for configuring the content of each {@link DetachableTab} when
1041
   * it is added to the given {@link DetachableTabPane} instance.
1042
   * <p>
1043
   * For {@link TextEditor} contents, an instance of {@link ScrollEventHandler}
1044
   * is initialized to perform synchronized scrolling between the editor and
1045
   * its preview window. Additionally, the last tab in the tab pane's list of
1046
   * tabs is given focus.
1047
   * </p>
1048
   * <p>
1049
   * Note that multiple tabs can be added simultaneously.
1050
   * </p>
1051
   *
1052
   * @param tabPane A new {@link TabPane} to configure.
1053
   */
1054
  private void initTabListener( final TabPane tabPane ) {
1055
    tabPane.getTabs().addListener(
1056
      ( final ListChangeListener.Change<? extends Tab> listener ) -> {
1057
        while( listener.next() ) {
1058
          if( listener.wasAdded() ) {
1059
            final var tabs = listener.getAddedSubList();
1060
1061
            tabs.forEach( tab -> {
1062
              final var node = tab.getContent();
1063
1064
              if( node instanceof TextEditor ) {
1065
                initScrollEventListener( tab );
1066
              }
1067
            } );
1068
1069
            // Select and give focus to the last tab opened.
1070
            final var index = tabs.size() - 1;
1071
            if( index >= 0 ) {
1072
              final var tab = tabs.get( index );
1073
              tabPane.getSelectionModel().select( tab );
1074
              tab.getContent().requestFocus();
1075
            }
1076
          }
1077
        }
1078
      }
1079
    );
1080
  }
1081
1082
  /**
1083
   * Synchronizes scrollbar positions between the given {@link Tab} that
1084
   * contains an instance of {@link TextEditor} and {@link HtmlPreview} pane.
1085
   *
1086
   * @param tab The container for an instance of {@link TextEditor}.
1087
   */
1088
  private void initScrollEventListener( final Tab tab ) {
1089
    final var editor = (TextEditor) tab.getContent();
1090
    final var scrollPane = editor.getScrollPane();
1091
    final var scrollBar = mPreview.getVerticalScrollBar();
1092
    final var handler = new ScrollEventHandler( scrollPane, scrollBar );
1093
1094
    handler.enabledProperty().bind( tab.selectedProperty() );
1095
  }
1096
1097
  private void addTabPane( final int index, final TabPane tabPane ) {
1098
    final var items = getItems();
1099
1100
    if( !items.contains( tabPane ) ) {
1101
      items.add( index, tabPane );
1102
    }
1103
  }
1104
1105
  private void addTabPane( final TabPane tabPane ) {
1106
    addTabPane( getItems().size(), tabPane );
1107
  }
1108
1109
  private GenericBuilder<Mutator, ProcessorContext> processorContextBuilder() {
1110
    final var w = getWorkspace();
1111
1112
    return builder()
1113
      .with( Mutator::setDefinitions, this::getDefinitions )
1114
      .with( Mutator::setLocale, w::getLocale )
1115
      .with( Mutator::setMetadata, w::getMetadata )
1116
      .with( Mutator::setThemeDir, w::getThemesPath )
1117
      .with( Mutator::setCacheDir,
1118
             () -> w.getFile( KEY_CACHE_DIR ) )
1119
      .with( Mutator::setImageDir,
1120
             () -> w.getFile( KEY_IMAGE_DIR ) )
1121
      .with( Mutator::setImageOrder,
1122
             () -> w.getString( KEY_IMAGE_ORDER ) )
1123
      .with( Mutator::setImageServer,
1124
             () -> w.getString( KEY_IMAGE_SERVER ) )
1125
      .with( Mutator::setCaret,
1126
             () -> getTextEditor().getCaret() )
1127
      .with( Mutator::setSigilBegan,
1128
             () -> w.getString( KEY_DEF_DELIM_BEGAN ) )
1129
      .with( Mutator::setSigilEnded,
1130
             () -> w.getString( KEY_DEF_DELIM_ENDED ) )
1131
      .with( Mutator::setRScript,
1132
             () -> w.getString( KEY_R_SCRIPT ) )
1133
      .with( Mutator::setRWorkingDir,
1134
             () -> w.getFile( KEY_R_DIR ).toPath() )
1135
      .with( Mutator::setFontDir,
1136
             () -> w.getFile( KEY_TYPESET_CONTEXT_FONTS_DIR ) )
1137
      .with( Mutator::setModesEnabled,
1138
             () -> w.getString( KEY_TYPESET_MODES_ENABLED ) )
1139
      .with( Mutator::setCurlQuotes,
1140
             () -> w.listProperty( KEY_TYPESET_TYPOGRAPHY_QUOTES ).get() )
1141
      .with( Mutator::setAutoRemove,
1142
             () -> w.getBoolean( KEY_TYPESET_CONTEXT_CLEAN ) );
1143
  }
1144
1145
  public ProcessorContext createProcessorContext() {
1146
    return createProcessorContextBuilder( NONE ).build();
1147
  }
1148
1149
  private GenericBuilder<Mutator, ProcessorContext> createProcessorContextBuilder(
1150
    final ExportFormat format ) {
1151
    final var textEditor = getTextEditor();
1152
    final var sourcePath = textEditor.getPath();
1153
1154
    return processorContextBuilder()
1155
      .with( Mutator::setSourcePath, sourcePath )
1156
      .with( Mutator::setExportFormat, format );
1157
  }
1158
1159
  /**
1160
   * @param targetPath Used when exporting to a PDF file (binary).
1161
   * @param format     Used when processors export to a new text format.
1162
   * @return A new {@link ProcessorContext} to use when creating an instance of
1163
   * {@link Processor}.
1164
   */
1165
  public ProcessorContext createProcessorContext(
1166
    final Path targetPath, final ExportFormat format ) {
1167
    assert targetPath != null;
1168
    assert format != null;
1169
1170
    return createProcessorContextBuilder( format )
1171
      .with( Mutator::setTargetPath, targetPath )
1172
      .build();
1173
  }
1174
1175
  /**
1176
   * @param sourcePath Used by {@link ProcessorFactory} to determine
1177
   *                   {@link Processor} type to create based on file type.
1178
   * @return A new {@link ProcessorContext} to use when creating an instance of
1179
   * {@link Processor}.
1180
   */
1181
  private ProcessorContext createProcessorContext( final Path sourcePath ) {
1182
    return processorContextBuilder()
1183
      .with( Mutator::setSourcePath, sourcePath )
1184
      .with( Mutator::setExportFormat, NONE )
1185
      .build();
1186
  }
1187
1188
  private TextResource createTextResource( final File file ) {
1189
    if( fromFilename( file ) == TEXT_YAML ) {
1190
      final var editor = createDefinitionEditor( file );
1191
      mDefinitionEditor.set( editor );
1192
      return editor;
1193
    }
1194
    else {
1195
      final var editor = createMarkdownEditor( file );
1196
      mTextEditor.set( editor );
1197
      return editor;
1198
    }
1199
  }
1200
1201
  /**
1202
   * Creates an instance of {@link MarkdownEditor} that listens for both
1203
   * caret change events and text change events. Text change events must
1204
   * take priority over caret change events because it's possible to change
1205
   * the text without moving the caret (e.g., delete selected text).
1206
   *
1207
   * @param inputFile The file containing contents for the text editor.
1208
   * @return A non-null text editor.
1209
   */
1210
  private MarkdownEditor createMarkdownEditor( final File inputFile ) {
1211
    final var editor = new MarkdownEditor( inputFile, getWorkspace() );
1212
1213
    // Listener for editor modifications or caret position changes.
1214
    editor.addDirtyListener( ( _, _, n ) -> {
1215
      if( n ) {
1216
        // Reset the status bar after changing the text.
1217
        clue();
1218
1219
        // Processing the text may update the status bar.
1220
        process( editor );
1221
1222
        // Update the caret position in the status bar.
1223
        CaretMovedEvent.fire( editor.getCaret() );
1224
      }
1225
    } );
1226
1227
    editor.addEventListener(
1228
      keyPressed( SPACE, CONTROL_DOWN ), this::autoinsert
1229
    );
1230
1231
    editor.addEventListener(
1232
      keyPressed( ENTER, ALT_DOWN ), _ -> mEditorSpeller.autofix( editor )
1233
    );
1234
1235
    final var textArea = editor.getTextArea();
1236
1237
    // Spell check when the paragraph changes.
1238
    textArea
1239
      .plainTextChanges()
1240
      .filter( p -> !p.isIdentity() )
1241
      .subscribe( change -> mEditorSpeller.checkParagraph( textArea, change ) );
1242
1243
    // Store the caret position to restore it after restarting the application.
1244
    textArea.caretPositionProperty().addListener(
1245
      ( _, _, n ) ->
1246
        getWorkspace().integerProperty( KEY_UI_RECENT_OFFSET ).setValue( n )
1247
    );
1248
1249
    // Check the entire document after the spellchecker is initialized (with
1250
    // a valid lexicon) so that only the current paragraph need be scanned
1251
    // while editing. (Technically, only the most recently modified word must
1252
    // be scanned.)
1253
    mSpellChecker.addListener(
1254
      ( _, _, _ ) -> runLater(
1255
        () -> iterateEditors( mEditorSpeller::checkDocument )
1256
      )
1257
    );
1258
1259
    // Check the entire document after it has been loaded.
1260
    mEditorSpeller.checkDocument( editor );
1261
1262
    return editor;
1263
  }
1264
1265
  /**
1266
   * Creates a processor for an editor, provided one doesn't already exist.
1267
   *
1268
   * @param editor The editor that potentially requires an associated processor.
1269
   */
1270
  private void updateProcessors( final TextEditor editor ) {
1271
    final var path = editor.getFile().toPath();
1272
1273
    mProcessors.computeIfAbsent(
1274
      editor, _ -> {
1275
        final var context = createProcessorContext( path );
1276
        final var preview = createHtmlPreviewProcessor( context );
1277
1278
        return createProcessors(
1279
          context,
1280
          preview
1281
        );
1282
      }
1283
    );
1284
  }
1285
1286
  /**
1287
   * Removes a processor for an editor. This is required because a file may
1288
   * change type while editing (e.g., from plain Markdown to R Markdown).
1289
   * In the case that an editor's type changes, its associated processor must
1290
   * be changed accordingly.
1291
   *
1292
   * @param editor The editor that potentially requires an associated processor.
1293
   */
1294
  private void removeProcessor( final TextEditor editor ) {
1295
    mProcessors.remove( editor );
1296
  }
1297
1298
  /**
1299
   * Creates a {@link Processor} capable of rendering an HTML document onto
1300
   * a GUI widget.
1301
   *
1302
   * @return The {@link Processor} for rendering an HTML document.
1303
   */
1304
  private Processor<String> createHtmlPreviewProcessor(
1305
    final ProcessorContext context
1306
  ) {
1307
    return new HtmlPreviewProcessor( context, getPreview() );
1308
  }
1309
1310
  /**
1311
   * Creates a spellchecker that accepts all words as correct. This allows
1312
   * the spellchecker property to be initialized to a known valid value.
1313
   *
1314
   * @return A wrapped {@link PermissiveSpeller}.
1315
   */
1316
  private ObjectProperty<SpellChecker> createSpellChecker() {
1317
    return new SimpleObjectProperty<>( new PermissiveSpeller() );
1318
  }
1319
1320
  private TextEditorSpellChecker createTextEditorSpellChecker(
1321
    final ObjectProperty<SpellChecker> spellChecker ) {
1322
    return new TextEditorSpellChecker( spellChecker );
1323
  }
1324
1325
  /**
1326
   * Delegates to {@link #autoinsert()}.
1327
   *
1328
   * @param ignored Ignored.
1329
   */
1330
  private void autoinsert( final KeyEvent ignored ) {
1331
    autoinsert();
1332
  }
1333
1334
  /**
1335
   * Finds a node that matches the word at the caret, then inserts the
1336
   * corresponding definition. The definition token delimiters depend on
1337
   * the type of file being edited.
1338
   */
1339
  public void autoinsert() {
1340
    mVariableNameInjector.autoinsert( getTextEditor(), getTextDefinition() );
1341
  }
1342
1343
  private Tooltip createTooltip( final File file ) {
1344
    final var path = file.toPath();
1345
    final var tooltip = new Tooltip( path.toString() );
1346
1347
    tooltip.setShowDelay( millis( 200 ) );
1348
1349
    return tooltip;
1350
  }
1351
1352
  public HtmlPreview getPreview() {
1353
    return mPreview;
1354
  }
1355
1356
  /**
1357
   * Returns the active text editor.
1358
   *
1359
   * @return The text editor that currently has focus.
1360
   */
1361
  public TextEditor getTextEditor() {
1362
    return mTextEditor.get();
1363
  }
1364
1365
  /**
1366
   * Returns the active text editor property.
1367
   *
1368
   * @return The property container for the active text editor.
1369
   */
1370
  public ReadOnlyObjectProperty<TextEditor> textEditorProperty() {
1371
    return mTextEditor;
1372
  }
1373
1374
  /**
1375
   * Returns the active text definition editor.
1376
   *
1377
   * @return The property container for the active definition editor.
1378
   */
1379
  public TextDefinition getTextDefinition() {
1380
    return mDefinitionEditor.get();
1381
  }
1382
1383
  /**
1384
   * Returns the active variable definitions, without any interpolation.
1385
   * Interpolation is a responsibility of {@link Processor} instances.
1386
   *
1387
   * @return The key-value pairs, not interpolated.
1388
   */
1389
  private Map<String, String> getDefinitions() {
1390
    return getTextDefinition().getDefinitions();
1391
  }
1392
1393
  public Window getWindow() {
1394
    return getScene().getWindow();
1395
  }
1396
1397
  public Workspace getWorkspace() {
1398
    return mWorkspace;
1399
  }
1400
1401
  /**
1402
   * Returns the set of file names opened in the application. The names must
1403
   * be converted to {@link File} objects.
1404
   *
1405
   * @return A {@link Set} of file names.
1406
   */
1407
  private <E> SetProperty<E> getRecentFiles() {
1408
    return getWorkspace().setsProperty( KEY_UI_RECENT_OPEN_PATH );
1409
  }
1410
}
11411
A src/main/java/com/keenwrite/MainScene.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.io.FileModifiedListener;
5
import com.keenwrite.io.FileWatchService;
6
import com.keenwrite.preferences.Workspace;
7
import com.keenwrite.ui.actions.GuiCommands;
8
import com.keenwrite.ui.listeners.CaretStatus;
9
import javafx.scene.Node;
10
import javafx.scene.Parent;
11
import javafx.scene.Scene;
12
import javafx.scene.control.MenuBar;
13
import javafx.scene.layout.BorderPane;
14
import javafx.scene.layout.VBox;
15
import org.controlsfx.control.StatusBar;
16
17
import java.io.File;
18
import java.text.MessageFormat;
19
20
import static com.keenwrite.constants.Constants.*;
21
import static com.keenwrite.events.ScrollLockEvent.fireScrollLockEvent;
22
import static com.keenwrite.events.StatusEvent.clue;
23
import static com.keenwrite.preferences.AppKeys.KEY_UI_SKIN_CUSTOM;
24
import static com.keenwrite.preferences.AppKeys.KEY_UI_SKIN_SELECTION;
25
import static com.keenwrite.preferences.SkinProperty.toFilename;
26
import static com.keenwrite.ui.actions.ApplicationBars.*;
27
import static javafx.application.Platform.runLater;
28
import static javafx.scene.input.KeyCode.SCROLL_LOCK;
29
import static javafx.scene.input.KeyEvent.KEY_RELEASED;
30
31
/**
32
 * Responsible for creating the bar scene: menu bar, toolbar, and status bar.
33
 */
34
public final class MainScene {
35
36
  private final Scene mScene;
37
  private final MenuBar mMenuBar;
38
  private final Node mToolBar;
39
  private final StatusBar mStatusBar;
40
  private final FileWatchService mFileWatchService = new FileWatchService();
41
  private FileModifiedListener mStylesheetFileListener = event -> {};
42
43
  public MainScene( final Workspace workspace ) {
44
    final var mainPane = createMainPane( workspace );
45
    final var actions = createApplicationActions( mainPane );
46
    final var caretStatus = createCaretStatus();
47
48
    mMenuBar = setManagedLayout( createMenuBar( actions ) );
49
    mToolBar = setManagedLayout( createToolBar() );
50
    mStatusBar = setManagedLayout( createStatusBar() );
51
52
    mStatusBar.getRightItems().add( caretStatus );
53
54
    final var appPane = new BorderPane();
55
    appPane.setTop( new VBox( mMenuBar, mToolBar ) );
56
    appPane.setCenter( mainPane );
57
    appPane.setBottom( mStatusBar );
58
59
    final var fileWatcher = new Thread( mFileWatchService );
60
    fileWatcher.setDaemon( true );
61
    fileWatcher.start();
62
63
    mScene = createScene( appPane );
64
    initStylesheets( mScene, workspace );
65
  }
66
67
  /**
68
   * Called by the {@link GuiApp} to get a handle on the {@link Scene}
69
   * created by an instance of {@link MainScene}.
70
   *
71
   * @return The {@link Scene} created at construction time.
72
   */
73
  public Scene getScene() {
74
    return mScene;
75
  }
76
77
  public void toggleMenuBar() {
78
    final var node = mMenuBar;
79
    node.setVisible( !node.isVisible() );
80
  }
81
82
  public void toggleToolBar() {
83
    final var node = mToolBar;
84
    node.setVisible( !node.isVisible() );
85
  }
86
87
  public void toggleStatusBar() {
88
    final var node = mStatusBar;
89
    node.setVisible( !node.isVisible() );
90
  }
91
92
  public StatusBar getStatusBar() {return mStatusBar;}
93
94
  private void initStylesheets( final Scene scene, final Workspace workspace ) {
95
    final var internal = workspace.listProperty( KEY_UI_SKIN_SELECTION );
96
    final var external = workspace.fileProperty( KEY_UI_SKIN_CUSTOM );
97
    final var inSkin = internal.get();
98
    final var exSkin = external.get();
99
    applyStylesheets( scene, inSkin, exSkin );
100
101
    internal.addListener(
102
      ( c, o, n ) -> {
103
        if( n != null ) {
104
          applyStylesheets( scene, n, exSkin );
105
        }
106
      }
107
    );
108
109
    external.addListener(
110
      ( c, o, n ) -> {
111
        if( o != null ) {
112
          mFileWatchService.unregister( o );
113
        }
114
115
        if( n != null ) {
116
          try {
117
            applyStylesheets( scene, inSkin, n );
118
          } catch( final Exception ex ) {
119
            // Changes to the CSS file won't autoload, which is okay.
120
            clue( ex );
121
          }
122
        }
123
      }
124
    );
125
126
    mFileWatchService.removeListener( mStylesheetFileListener );
127
    mStylesheetFileListener = event ->
128
      runLater( () -> applyStylesheets( scene, inSkin, event.getFile() ) );
129
    mFileWatchService.addListener( mStylesheetFileListener );
130
  }
131
132
  private String getStylesheet( final String filename ) {
133
    return MessageFormat.format( STYLESHEET_APPLICATION_SKIN, filename );
134
  }
135
136
  /**
137
   * Clears then re-applies all the internal stylesheets.
138
   *
139
   * @param scene    The scene to stylize.
140
   * @param internal The CSS file name bundled with the application.
141
   * @param external The (optional) customized CSS file specified by the user.
142
   */
143
  private void applyStylesheets(
144
    final Scene scene, final String internal, final File external ) {
145
    final var stylesheets = scene.getStylesheets();
146
    stylesheets.clear();
147
    stylesheets.add( STYLESHEET_APPLICATION_BASE );
148
    stylesheets.add( STYLESHEET_MARKDOWN );
149
    stylesheets.add( getStylesheet( toFilename( internal ) ) );
150
151
    try {
152
      if( external != null && external.canRead() && !external.isDirectory() ) {
153
        stylesheets.add( external.toURI().toURL().toString() );
154
        mFileWatchService.register( external );
155
      }
156
    } catch( final Exception ex ) {
157
      clue( ex );
158
    }
159
  }
160
161
  private MainPane createMainPane( final Workspace workspace ) {
162
    return new MainPane( workspace );
163
  }
164
165
  private GuiCommands createApplicationActions( final MainPane mainPane ) {
166
    return new GuiCommands( this, mainPane );
167
  }
168
169
  /**
170
   * Creates the class responsible for updating the UI with the caret position
171
   * based on the active text editor.
172
   *
173
   * @return The {@link CaretStatus} responsible for updating the
174
   * {@link StatusBar} whenever the caret changes position.
175
   */
176
  private CaretStatus createCaretStatus() {
177
    return new CaretStatus();
178
  }
179
180
  /**
181
   * Creates a new scene that is attached to the given {@link Parent}.
182
   *
183
   * @param parent The container for the scene.
184
   * @return A scene to capture user interactions, UI styles, etc.
185
   */
186
  private Scene createScene( final Parent parent ) {
187
    final var scene = new Scene( parent );
188
189
    // Update the synchronized scrolling status when user presses scroll lock.
190
    scene.addEventHandler( KEY_RELEASED, event -> {
191
      if( event.getCode() == SCROLL_LOCK ) {
192
        fireScrollLockEvent();
193
      }
194
    } );
195
196
    return scene;
197
  }
198
199
  /**
200
   * Binds the visible property of the node to the managed property so that
201
   * hiding the node also removes the screen real estate that it occupies.
202
   * This allows the user to hide the menu bar, toolbar, etc.
203
   *
204
   * @param node The node to have its real estate bound to visibility.
205
   * @return The given node for fluent-like convenience.
206
   */
207
  private <T extends Node> T setManagedLayout( final T node ) {
208
    node.managedProperty().bind( node.visibleProperty() );
209
    return node;
210
  }
211
}
1212
A src/main/java/com/keenwrite/Messages.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.collections.InterpolatingMap;
5
import com.keenwrite.preferences.Key;
6
import com.keenwrite.sigils.PropertyKeyOperator;
7
import com.keenwrite.sigils.SigilKeyOperator;
8
9
import java.net.URI;
10
import java.text.MessageFormat;
11
import java.util.ResourceBundle;
12
13
import static com.keenwrite.constants.Constants.APP_BUNDLE_NAME;
14
import static java.util.ResourceBundle.getBundle;
15
16
/**
17
 * Recursively resolves message properties. Property values can refer to other
18
 * properties using a <code>${var}</code> syntax.
19
 */
20
public final class Messages {
21
22
  private static final SigilKeyOperator OPERATOR = new PropertyKeyOperator();
23
  private static final InterpolatingMap MAP = new InterpolatingMap( OPERATOR );
24
25
  static {
26
    // Obtains the application resource bundle using the default locale. The
27
    // locale cannot be changed using the application, making interpolation of
28
    // values viable as a one-time operation.
29
    try {
30
      final var bundle = getBundle( APP_BUNDLE_NAME );
31
32
      bundle.keySet().forEach( key -> MAP.put( key, bundle.getString( key ) ) );
33
      MAP.interpolate();
34
    } catch( final Exception ignored ) {
35
      // This is bad, but it'll be extremely apparent when the UI loads. We
36
      // can't log this through regular channels because that'd lead to a
37
      // circular dependency.
38
    }
39
  }
40
41
  /**
42
   * Returns the value for a key from the message bundle. If the value cannot
43
   * be found, this returns the key.
44
   *
45
   * @param key Retrieve the value for this key.
46
   * @return The value for the key, or the key itself if not found.
47
   */
48
  public static String get( final String key ) {
49
    final var v = MAP.get( key );
50
51
    return v == null ? key : v;
52
  }
53
54
  /**
55
   * Returns the value for a key from the message bundle.
56
   *
57
   * @param key Retrieve the value for this key.
58
   * @return The value for the key.
59
   */
60
  public static String get( final Key key ) {
61
    return get( key.toString() );
62
  }
63
64
  /**
65
   * Returns the value for a key from the message bundle with the arguments
66
   * replacing <code>{#}</code> placeholders.
67
   *
68
   * @param key  Retrieve the value for this key.
69
   * @param args The values to substitute for placeholders.
70
   * @return The value for the key.
71
   */
72
  public static String get( final String key, final Object... args ) {
73
    return MessageFormat.format( get( key ), args );
74
  }
75
76
  public static int getInt( final String key, final int defaultValue ) {
77
    try {
78
      return Integer.parseInt( get( key ) );
79
    } catch( final NumberFormatException ignored ) {
80
      return defaultValue;
81
    }
82
  }
83
84
  public static URI getUri( final String key ) {
85
    return URI.create( get( key ) );
86
  }
87
88
  /**
89
   * Answers whether the given key is contained in the application's messages
90
   * properties file.
91
   *
92
   * @param key The key to look for in the {@link ResourceBundle}.
93
   * @return {@code true} when the key exists as an exact match.
94
   */
95
  public static boolean containsKey( final String key ) {
96
    return MAP.containsKey( key );
97
  }
98
99
  private Messages() { }
100
}
1101
A src/main/java/com/keenwrite/Services.java
1
/* Copyright 2020-2021 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/cmdline/Arguments.java
1
/* Copyright 2023-2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.cmdline;
6
7
import com.fasterxml.jackson.databind.JsonNode;
8
import com.fasterxml.jackson.databind.ObjectMapper;
9
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
10
import com.keenwrite.ExportFormat;
11
import com.keenwrite.processors.ProcessorContext;
12
import com.keenwrite.processors.ProcessorContext.Mutator;
13
import picocli.CommandLine;
14
import picocli.CommandLine.ParseResult;
15
16
import java.io.File;
17
import java.io.IOException;
18
import java.nio.file.Files;
19
import java.nio.file.Path;
20
import java.util.HashMap;
21
import java.util.Locale;
22
import java.util.Map;
23
import java.util.Map.Entry;
24
import java.util.concurrent.Callable;
25
import java.util.function.Consumer;
26
27
import static com.keenwrite.constants.Constants.DIAGRAM_SERVER_NAME;
28
import static java.lang.String.format;
29
import static java.nio.charset.StandardCharsets.UTF_8;
30
import static java.nio.file.Files.*;
31
32
/**
33
 * Responsible for mapping command-line arguments to keys that are used by
34
 * the application.
35
 */
36
@CommandLine.Command(
37
  name = "KeenWrite",
38
  mixinStandardHelpOptions = true,
39
  description = "Plain text editor for editing with variables"
40
)
41
@SuppressWarnings( "unused" )
42
public final class Arguments implements Callable<Integer> {
43
  @CommandLine.Spec
44
  CommandLine.Model.CommandSpec mSpecifications;
45
46
  @CommandLine.Option(
47
    names = { "--all" },
48
    description =
49
      "Concatenate files before processing (${DEFAULT-VALUE})",
50
    defaultValue = "false"
51
  )
52
  private boolean mConcatenate;
53
54
  @CommandLine.Option(
55
    names = { "--keep-files" },
56
    description =
57
      "Retain temporary build files (${DEFAULT-VALUE})",
58
    defaultValue = "false"
59
  )
60
  private boolean mKeepFiles;
61
62
  @CommandLine.Option(
63
    names = { "-c", "--chapters" },
64
    description =
65
      "Export chapter ranges, no spaces (e.g., -3,5-9,15-)",
66
    paramLabel = "String"
67
  )
68
  private String mChapters;
69
70
  @CommandLine.Option(
71
    names = { "--curl-quotes" },
72
    description =
73
      "Encode quotation marks (see docs)",
74
    paramLabel = "String",
75
    defaultValue = "regular"
76
  )
77
  private String mCurlQuotes;
78
79
  @CommandLine.Option(
80
    names = { "-d", "--debug" },
81
    description =
82
      "Enable logging to the console (${DEFAULT-VALUE})",
83
    paramLabel = "Boolean",
84
    defaultValue = "false"
85
  )
86
  private boolean mDebug;
87
88
  @CommandLine.Option(
89
    names = { "-i", "--input" },
90
    description =
91
      "Source document path",
92
    paramLabel = "PATH",
93
    defaultValue = "stdin",
94
    required = true
95
  )
96
  private Path mSourcePath;
97
98
  @CommandLine.Option(
99
    names = { "--html-head" },
100
    description =
101
      "Document fragment to append to HTML head element",
102
    paramLabel = "PATH",
103
    defaultValue = "",
104
    required = true
105
  )
106
  private Path mHtmlHeadPath;
107
108
  @CommandLine.Option(
109
    names = { "--html-foot" },
110
    description =
111
      "Document fragment to append to HTML body element",
112
    paramLabel = "PATH",
113
    defaultValue = "",
114
    required = true
115
  )
116
  private Path mHtmlFootPath;
117
118
  @CommandLine.Option(
119
    names = { "--font-dir" },
120
    description =
121
      "Directory to specify additional fonts",
122
    paramLabel = "String"
123
  )
124
  private File mFontDir;
125
126
  @CommandLine.Option(
127
    names = { "--mode" },
128
    description =
129
      "Enable one or more modes when typesetting",
130
    paramLabel = "String"
131
  )
132
  private String mTypesetMode;
133
134
  @CommandLine.Option(
135
    names = { "--format-subtype" },
136
    description =
137
      "Export TeX subtype for HTML formats: svg, delimited",
138
    paramLabel = "String",
139
    defaultValue = "svg"
140
  )
141
  private String mFormatSubtype;
142
143
  @CommandLine.Option(
144
    names = { "--cache-dir" },
145
    description =
146
      "Directory to store remote resources",
147
    paramLabel = "DIR"
148
  )
149
  private File mCachesDir;
150
151
  @CommandLine.Option(
152
    names = { "--image-dir" },
153
    description =
154
      "Directory containing images",
155
    paramLabel = "DIR"
156
  )
157
  private File mImagesDir;
158
159
  @CommandLine.Option(
160
    names = { "--image-order" },
161
    description =
162
      "Comma-separated image order (${DEFAULT-VALUE})",
163
    paramLabel = "String",
164
    defaultValue = "svg,pdf,png,jpg,tiff"
165
  )
166
  private String mImageOrder;
167
168
  @CommandLine.Option(
169
    names = { "--image-server" },
170
    description =
171
      "SVG diagram rendering service (${DEFAULT-VALUE})",
172
    paramLabel = "String",
173
    defaultValue = DIAGRAM_SERVER_NAME
174
  )
175
  private String mImageServer;
176
177
  @CommandLine.Option(
178
    names = { "--locale" },
179
    description =
180
      "Set localization (${DEFAULT-VALUE})",
181
    paramLabel = "String",
182
    defaultValue = "en"
183
  )
184
  private String mLocale;
185
186
  @CommandLine.Option(
187
    names = { "-m", "--metadata" },
188
    description =
189
      "Map metadata keys to values, variable names allowed",
190
    paramLabel = "key=value"
191
  )
192
  private Map<String, String> mMetadata;
193
194
  @CommandLine.Option(
195
    names = { "-o", "--output" },
196
    description =
197
      "Destination document path",
198
    paramLabel = "PATH",
199
    defaultValue = "stdout",
200
    required = true
201
  )
202
  private Path mTargetPath;
203
204
  @CommandLine.Option(
205
    names = { "-q", "--quiet" },
206
    description =
207
      "Suppress all status messages (${DEFAULT-VALUE})",
208
    defaultValue = "false"
209
  )
210
  private boolean mQuiet;
211
212
  @CommandLine.Option(
213
    names = { "--r-dir" },
214
    description =
215
      "R working directory",
216
    paramLabel = "DIR"
217
  )
218
  private Path mRWorkingDir;
219
220
  @CommandLine.Option(
221
    names = { "--r-script" },
222
    description =
223
      "R bootstrap script path",
224
    paramLabel = "PATH"
225
  )
226
  private Path mRScriptPath;
227
228
  @CommandLine.Option(
229
    names = { "-s", "--set" },
230
    description =
231
      "Set (or override) a document variable value",
232
    paramLabel = "key=value"
233
  )
234
  private Map<String, String> mOverrides;
235
236
  @CommandLine.Option(
237
    names = { "--sigil-opening" },
238
    description =
239
      "Starting sigil for variable names (${DEFAULT-VALUE})",
240
    paramLabel = "String",
241
    defaultValue = "{{"
242
  )
243
  private String mSigilBegan;
244
245
  @CommandLine.Option(
246
    names = { "--sigil-closing" },
247
    description =
248
      "Ending sigil for variable names (${DEFAULT-VALUE})",
249
    paramLabel = "String",
250
    defaultValue = "}}"
251
  )
252
  private String mSigilEnded;
253
254
  @CommandLine.Option(
255
    names = { "--theme-dir" },
256
    description =
257
      "Theme directory",
258
    paramLabel = "DIR"
259
  )
260
  private Path mThemesDir;
261
262
  @CommandLine.Option(
263
    names = { "-v", "--variables" },
264
    description =
265
      "Variables path",
266
    paramLabel = "PATH"
267
  )
268
  private Path mPathVariables;
269
270
  private final Consumer<Arguments> mLauncher;
271
272
  public Arguments( final Consumer<Arguments> launcher ) {
273
    mLauncher = launcher;
274
  }
275
276
  public ProcessorContext createProcessorContext()
277
    throws IOException {
278
    final var definitions = parse( mPathVariables );
279
    final var format = ExportFormat.valueFrom( mTargetPath, mFormatSubtype );
280
    final var locale = lookupLocale( mLocale );
281
    final var rScript = read( mRScriptPath );
282
    final var htmlHead = read( mHtmlHeadPath );
283
    final var htmlFoot = read( mHtmlFootPath );
284
285
    return ProcessorContext
286
      .builder()
287
      .with( Mutator::setSourcePath, mSourcePath )
288
      .with( Mutator::setTargetPath, mTargetPath )
289
      .with( Mutator::setHtmlHead, htmlHead )
290
      .with( Mutator::setHtmlFoot, htmlFoot )
291
      .with( Mutator::setThemeDir, () -> mThemesDir )
292
      .with( Mutator::setCacheDir, () -> mCachesDir )
293
      .with( Mutator::setImageDir, () -> mImagesDir )
294
      .with( Mutator::setImageServer, () -> mImageServer )
295
      .with( Mutator::setImageOrder, () -> mImageOrder )
296
      .with( Mutator::setFontDir, () -> mFontDir )
297
      .with( Mutator::setModesEnabled, () -> mTypesetMode )
298
      .with( Mutator::setExportFormat, format )
299
      .with( Mutator::setDefinitions, () -> definitions )
300
      .with( Mutator::setMetadata, () -> mMetadata )
301
      .with( Mutator::setOverrides, () -> mOverrides )
302
      .with( Mutator::setLocale, () -> locale )
303
      .with( Mutator::setConcatenate, () -> mConcatenate )
304
      .with( Mutator::setChapters, () -> mChapters )
305
      .with( Mutator::setSigilBegan, () -> mSigilBegan )
306
      .with( Mutator::setSigilEnded, () -> mSigilEnded )
307
      .with( Mutator::setRScript, () -> rScript )
308
      .with( Mutator::setRWorkingDir, () -> mRWorkingDir )
309
      .with( Mutator::setCurlQuotes, () -> mCurlQuotes )
310
      .with( Mutator::setAutoRemove, () -> !mKeepFiles )
311
      .build();
312
  }
313
314
  public boolean quiet() {
315
    return mQuiet;
316
  }
317
318
  public boolean debug() {
319
    return mDebug;
320
  }
321
322
  /**
323
   * Launches the main application window. This is called when not running
324
   * in headless mode.
325
   *
326
   * @return {@code 0}
327
   * @throws Exception The application encountered an unrecoverable error.
328
   */
329
  @Override
330
  public Integer call() throws Exception {
331
    mLauncher.accept( this );
332
    return 0;
333
  }
334
335
  public void iterate(
336
    final ParseResult parseResult,
337
    final Consumer<String> consumer
338
  ) {
339
    final var options = mSpecifications.options();
340
341
    for( final var opt : options ) {
342
      consumer.accept( format( "%s=%s", opt.longestName(), opt.getValue() ) );
343
    }
344
  }
345
346
  private static String read( final Path path ) throws IOException {
347
    return path == null
348
      ? ""
349
      : canRead( path )
350
      ? readString( path, UTF_8 )
351
      : "";
352
  }
353
354
  private static boolean canRead( final Path path ) {
355
    return exists( path ) && isRegularFile( path ) && isReadable( path );
356
  }
357
358
  /**
359
   * Parses the given YAML document into a map of key-value pairs.
360
   *
361
   * @param vars Variable definition file to read, may be {@code null} if no
362
   *             variables are specified.
363
   * @return A non-interpolated variable map, or an empty map.
364
   * @throws IOException Could not read the variable definition file
365
   */
366
  private static Map<String, String> parse( final Path vars )
367
    throws IOException {
368
    final var map = new HashMap<String, String>();
369
370
    if( vars != null ) {
371
      final var yaml = read( vars );
372
      final var factory = new YAMLFactory();
373
      final var json = new ObjectMapper( factory ).readTree( yaml );
374
375
      parse( json, "", map );
376
    }
377
378
    return map;
379
  }
380
381
  private static void parse(
382
    final JsonNode json, final String parent, final Map<String, String> map ) {
383
    assert json != null;
384
    assert parent != null;
385
    assert map != null;
386
387
    final var fields = json.properties().iterator();
388
389
    fields.forEachRemaining( node -> parse( node, parent, map ) );
390
  }
391
392
  private static void parse(
393
    final Entry<String, JsonNode> node,
394
    final String parent,
395
    final Map<String, String> map ) {
396
    assert node != null;
397
    assert parent != null;
398
    assert map != null;
399
400
    final var jsonNode = node.getValue();
401
    final var keyName = format( "%s.%s", parent, node.getKey() );
402
403
    if( jsonNode.isNull() ) {
404
      map.put( keyName.substring( 1 ), "" );
405
    }
406
    else if( jsonNode.isValueNode() ) {
407
      // Trim the leading period, which is always present.
408
      map.put( keyName.substring( 1 ), node.getValue().asText() );
409
    }
410
    else if( jsonNode.isObject() ) {
411
      parse( jsonNode, keyName, map );
412
    }
413
  }
414
415
  private static Locale lookupLocale( final String locale ) {
416
    try {
417
      return Locale.forLanguageTag( locale );
418
    } catch( final Exception ex ) {
419
      return Locale.ENGLISH;
420
    }
421
  }
422
}
1423
A src/main/java/com/keenwrite/cmdline/ColourScheme.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.cmdline;
6
7
import static picocli.CommandLine.Help.Ansi.Style.*;
8
import static picocli.CommandLine.Help.ColorScheme;
9
import static picocli.CommandLine.Help.ColorScheme.Builder;
10
11
/**
12
 * Responsible for creating the command-line parser's colour scheme.
13
 */
14
public class ColourScheme {
15
16
  /**
17
   * Creates a new color scheme for use with command-line parsing.
18
   *
19
   * @return The new color scheme to apply to the parsesr.
20
   */
21
  public static ColorScheme create() {
22
    return new Builder()
23
      .commands( bold )
24
      .options( fg_blue, bold )
25
      .parameters( fg_blue )
26
      .optionParams( italic )
27
      .errors( fg_red, bold )
28
      .stackTraces( italic )
29
      .build();
30
  }
31
}
132
A src/main/java/com/keenwrite/cmdline/HeadlessApp.java
1
/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.cmdline;
6
7
import com.keenwrite.AppCommands;
8
import com.keenwrite.events.StatusEvent;
9
import org.greenrobot.eventbus.Subscribe;
10
11
import java.io.PrintStream;
12
13
import static com.keenwrite.events.Bus.register;
14
import static java.lang.String.format;
15
16
/**
17
 * Responsible for running the application in headless mode.
18
 */
19
public class HeadlessApp {
20
  /**
21
   * Contains directives that control text file processing.
22
   */
23
  private final Arguments mArgs;
24
25
  /**
26
   * Where to write error messages.
27
   */
28
  private final PrintStream mErrStream;
29
30
  /**
31
   * Creates a new command-line version of the application.
32
   *
33
   * @param args      The post-processed command-line arguments.
34
   * @param errStream Where to write error messages.
35
   */
36
  public HeadlessApp( final Arguments args, final PrintStream errStream ) {
37
    assert args != null;
38
39
    mArgs = args;
40
    mErrStream = errStream;
41
42
    register( this );
43
  }
44
45
  /**
46
   * When a status message is shown, write it to the console, if not in
47
   * quiet mode.
48
   *
49
   * @param event The event published when the status changes.
50
   */
51
  @Subscribe
52
  public void handle( final StatusEvent event ) {
53
    assert event != null;
54
55
    if( !mArgs.quiet() ) {
56
      final var stacktrace = event.getProblem();
57
      final var problem = stacktrace.isBlank()
58
        ? ""
59
        : format( "%n%s", stacktrace );
60
      final var msg = format( "%s%s", event, problem );
61
62
      mErrStream.println( msg );
63
    }
64
  }
65
66
  private void run() {
67
    AppCommands.run( mArgs );
68
  }
69
70
  /**
71
   * Entry point for running the application in headless mode.
72
   *
73
   * @param args      The parsed command-line arguments.
74
   * @param errStream Where to write error messages.
75
   */
76
  public static void run(
77
    final Arguments args,
78
    final PrintStream errStream ) {
79
    final var app = new HeadlessApp( args, errStream );
80
    app.run();
81
  }
82
}
183
A src/main/java/com/keenwrite/collections/BoundedCache.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.collections;
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( final Map.Entry<K, V> eldest ) {
28
    return size() > mCacheSize;
29
  }
30
}
131
A src/main/java/com/keenwrite/collections/CircularQueue.java
1
/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.collections;
3
4
import org.jetbrains.annotations.NotNull;
5
6
import java.util.*;
7
8
import static java.lang.Math.min;
9
10
/**
11
 * Responsible for maintaining a circular queue where newly added items will
12
 * overwrite existing items.
13
 * <p>
14
 * <strong>Warning:</strong> This class is not thread-safe.
15
 * </p>
16
 *
17
 * @param <E> The type of elements to store in this collection.
18
 */
19
@SuppressWarnings( "unchecked" )
20
public class CircularQueue<E>
21
  extends AbstractCollection<E> implements Queue<E> {
22
23
  /**
24
   * Simplifies the code by reusing an existing list implementation.
25
   * Initialized with {@code null} values at construction time.
26
   */
27
  private final Object[] mElements;
28
29
  /**
30
   * Maximum number of elements allowed in the collection before old elements
31
   * are overwritten. Set at construction time.
32
   */
33
  private final int mCapacity;
34
35
  /**
36
   * Insertion position when a new element is added. Starts at zero.
37
   */
38
  private int mProducer;
39
40
  /**
41
   * Retrieval position when the oldest element is removed. Starts at zero.
42
   */
43
  private int mConsumer;
44
45
  /**
46
   * The number of elements in the collection. This cannot delegate to the
47
   * {@link #mElements} list. Starts at zero.
48
   */
49
  private int mSize;
50
51
  /**
52
   * Creates a new circular queue that has a limited number of elements that
53
   * may be added before newly added elements will overwrite the oldest
54
   * elements that were added previously.
55
   * <p>
56
   * <strong>Warning:</strong> Client classes must take care not to exceed
57
   * memory limits imposed by the Java Virtual Machine.
58
   *
59
   * @param capacity Maximum number elements allowed in the list, must be
60
   *                 greater than one.
61
   */
62
  public CircularQueue( final int capacity ) {
63
    assert capacity > 1;
64
65
    mCapacity = capacity;
66
    mElements = new Object[ capacity ];
67
  }
68
69
  /**
70
   * Adds an element to the end of the collection. This overwrites the oldest
71
   * element in the collection when the queue is full. The number of elements,
72
   * reflected by the return value of {@link #size()} will not exceed the
73
   * capacity.
74
   *
75
   * @param element The item to insert into the collection, must not be
76
   *                {@code null}.
77
   * @return {@code true} Non-{@code null} items are always added.
78
   * @throws NullPointerException if the given element is {@code null}.
79
   *                              The iterator requires a consecutive
80
   *                              non-{@code null} range (no gaps).
81
   */
82
  @Override
83
  public boolean add( final E element ) {
84
    if( element == null ) {
85
      throw new NullPointerException();
86
    }
87
88
    mElements[ mProducer++ ] = element;
89
    mProducer %= mCapacity;
90
    mSize = min( mSize + 1, mCapacity );
91
92
    return true;
93
  }
94
95
  /**
96
   * Delegates to {@link #add(E)}.
97
   */
98
  @Override
99
  public boolean offer( final E element ) {
100
    return add( element );
101
  }
102
103
  /**
104
   * Removes the oldest element that was added to the collection.  The number
105
   * of elements reflected by the return value of {@link #size()} will not
106
   * drop below zero.
107
   *
108
   * @return The oldest element.
109
   * @throws NoSuchElementException The collection is empty.
110
   */
111
  @Override
112
  public E remove() {
113
    if( isEmpty() ) {
114
      throw new NoSuchElementException();
115
    }
116
117
    final E element = (E) mElements[ mConsumer ];
118
119
    mElements[ mConsumer++ ] = null;
120
    mConsumer %= mCapacity;
121
    mSize--;
122
123
    return element;
124
  }
125
126
  /**
127
   * Delegates to {@link #remove()}, but does not throw an exception.
128
   *
129
   * @return The oldest element.
130
   */
131
  @Override
132
  public E poll() {
133
    return isEmpty() ? null : remove();
134
  }
135
136
  /**
137
   * Returns the oldest element that was added to the collection.
138
   *
139
   * @return The oldest element.
140
   * @throws NoSuchElementException The collection is empty.
141
   */
142
  @Override
143
  public E element() {
144
    if( isEmpty() ) {
145
      throw new NoSuchElementException();
146
    }
147
148
    return (E) mElements[ mConsumer ];
149
  }
150
151
  /**
152
   * Delegates to {@link #element()}, but does not throw an exception.
153
   *
154
   * @return The oldest element.
155
   */
156
  @Override
157
  public E peek() {
158
    return isEmpty() ? null : element();
159
  }
160
161
  /**
162
   * Answers how many elements are currently in the collection.
163
   *
164
   * @return The number of elements that have been added to but not removed
165
   * from the collection.
166
   */
167
  @Override
168
  public int size() {
169
    return mSize;
170
  }
171
172
  /**
173
   * Returns a facility to visit each of the elements in the
174
   * {@link CircularQueue}. This will start iterating at the oldest element
175
   * and stop when there are no more elements.
176
   * <p>
177
   * The iterator is not thread-safe; concurrent modifications to the number
178
   * of elements in the {@link CircularQueue} will result in undefined
179
   * behaviour.
180
   *
181
   * @return A new {@link Iterator} instance capable of visiting each element.
182
   */
183
  @Override
184
  @NotNull
185
  public Iterator<E> iterator() {
186
    return new Iterator<>() {
187
      private int mIndex = mConsumer;
188
      private boolean mFirst = true;
189
190
      @Override
191
      public boolean hasNext() {
192
        return (mFirst || mIndex != mConsumer) && mElements[ mIndex ] != null;
193
      }
194
195
      @Override
196
      public E next() {
197
        try {
198
          final var element = mElements[ mIndex++ ];
199
          mIndex %= mCapacity;
200
          mFirst = false;
201
202
          return (E) element;
203
        } catch( final IndexOutOfBoundsException ex ) {
204
          throw new NoSuchElementException( "No such element at: " + mIndex );
205
        }
206
      }
207
    };
208
  }
209
210
  @Override
211
  public String toString() {
212
    return Arrays.toString( mElements );
213
  }
214
}
1215
A src/main/java/com/keenwrite/collections/InterpolatingMap.java
1
/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.collections;
3
4
import com.keenwrite.sigils.PropertyKeyOperator;
5
import com.keenwrite.sigils.SigilKeyOperator;
6
7
import java.io.Serial;
8
import java.util.HashMap;
9
import java.util.Map;
10
import java.util.Objects;
11
import java.util.concurrent.ConcurrentHashMap;
12
13
import static java.lang.String.format;
14
import static java.util.regex.Matcher.quoteReplacement;
15
16
/**
17
 * Responsible for interpolating key-value pairs in a map. That is, this will
18
 * iterate over all key-value pairs and replace keys wrapped in sigils
19
 * with corresponding definition value from the same map.
20
 */
21
public class InterpolatingMap extends ConcurrentHashMap<String, String> {
22
  @Serial
23
  private static final long serialVersionUID = -8705400301476113530L;
24
25
  private static final int GROUP_DELIMITED = 1;
26
27
  /**
28
   * Used to override the default initial capacity in {@link HashMap}.
29
   */
30
  private static final int INITIAL_CAPACITY = 1 << 8;
31
32
  private transient final SigilKeyOperator mOperator;
33
34
  /**
35
   * Creates a new interpolating map using the {@link PropertyKeyOperator}.
36
   */
37
  public InterpolatingMap() {
38
    this( new PropertyKeyOperator() );
39
  }
40
41
  /**
42
   * @param operator Contains the opening and closing sigils that mark
43
   *                 where variable names begin and end.
44
   */
45
  public InterpolatingMap( final SigilKeyOperator operator ) {
46
    super( INITIAL_CAPACITY );
47
48
    assert operator != null;
49
    mOperator = operator;
50
  }
51
52
  /**
53
   * @param operator Contains the opening and closing sigils that mark
54
   *                 where variable names begin and end.
55
   * @param m        The initial {@link Map} to copy into this instance.
56
   */
57
  public InterpolatingMap(
58
    final SigilKeyOperator operator, final Map<String, String> m ) {
59
    this( operator );
60
    putAll( m );
61
  }
62
63
  /**
64
   * Interpolates all values in the map that reference other values by way
65
   * of key names. Performs a non-greedy match of key names delimited by
66
   * definition tokens. This operation modifies the map directly.
67
   *
68
   * @return {@code this}
69
   */
70
  public InterpolatingMap interpolate() {
71
    for( final var k : keySet() ) {
72
      replace( k, interpolate( get( k ) ) );
73
    }
74
75
    return this;
76
  }
77
78
  /**
79
   * Given a value with zero or more key references, this will resolve all
80
   * the values, recursively. If a key cannot be de-referenced, the value will
81
   * contain the key name, including the original sigils.
82
   *
83
   * @param value Value containing zero or more key references.
84
   * @return The given value with all embedded key references interpolated.
85
   */
86
  public String interpolate( final String value ) {
87
    assert value != null;
88
89
    final var matcher = mOperator.match( value );
90
    final var sb = new StringBuilder( value.length() << 1 );
91
92
    while( matcher.find() ) {
93
      final var keyName = matcher.group( GROUP_DELIMITED );
94
      final var mapValue = get( keyName );
95
96
      if( mapValue != null ) {
97
        if( mapValue.contains( mOperator.apply( keyName ) ) ) {
98
          throw new IllegalStateException(
99
            format(
100
              "Mapped value '%s' may not contain its key name '%s'",
101
              mapValue,
102
              keyName
103
            )
104
          );
105
        }
106
107
        final var keyValue = interpolate( mapValue );
108
        matcher.appendReplacement( sb, quoteReplacement( keyValue ) );
109
      }
110
    }
111
112
    matcher.appendTail( sb );
113
    return sb.toString();
114
  }
115
116
  @Override
117
  public boolean equals( final Object o ) {
118
    if( this == o ) {
119
      return true;
120
    }
121
122
    if( o == null || getClass() != o.getClass() ) {
123
      return false;
124
    }
125
126
    if( !super.equals( o ) ) {
127
      return false;
128
    }
129
130
    final InterpolatingMap that = (InterpolatingMap) o;
131
    return Objects.equals( mOperator, that.mOperator );
132
  }
133
134
  @Override
135
  public int hashCode() {
136
    return Objects.hash( super.hashCode(), mOperator );
137
  }
138
}
1139
A src/main/java/com/keenwrite/commands/ConcatenateCommand.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.commands;
6
7
import com.keenwrite.io.SysFile;
8
import com.keenwrite.util.AlphanumComparator;
9
import com.keenwrite.util.RangeValidator;
10
11
import java.io.IOException;
12
import java.nio.file.Path;
13
import java.util.ArrayList;
14
import java.util.concurrent.Callable;
15
import java.util.concurrent.atomic.AtomicInteger;
16
17
import static com.keenwrite.events.StatusEvent.clue;
18
import static com.keenwrite.util.FileWalker.walk;
19
import static java.lang.String.format;
20
import static java.lang.System.lineSeparator;
21
import static java.nio.charset.StandardCharsets.UTF_8;
22
import static java.nio.file.Files.readString;
23
24
/**
25
 * Responsible for concatenating files according to user-defined chapter ranges.
26
 */
27
public class ConcatenateCommand implements Callable<String> {
28
  /**
29
   * Sci-fi genres, which are can be longer than other genres, typically fall
30
   * below 150,000 words at 6 chars per word. This reduces re-allocations of
31
   * memory when concatenating files together when exporting novels.
32
   */
33
  private static final int DOCUMENT_LENGTH = 150_000 * 6;
34
35
  private final Path mParent;
36
  private final String mExtension;
37
  private final String mRange;
38
39
  public ConcatenateCommand(
40
    final Path parent,
41
    final String extension,
42
    final String range ) {
43
    assert parent != null;
44
    assert extension != null;
45
    assert range != null;
46
47
    mParent = parent;
48
    mExtension = extension;
49
    mRange = range;
50
  }
51
52
  public static int toDigits( final String filename, final int fallback ) {
53
    final var stripped = filename.replaceAll( "\\D", "" );
54
55
    return stripped.isEmpty() ? fallback : Integer.parseInt( stripped );
56
  }
57
58
  public String call() throws IOException {
59
    final var glob = format( "**/*.%s", mExtension );
60
    final var files = new ArrayList<Path>();
61
    final var text = new StringBuilder( DOCUMENT_LENGTH );
62
    final var chapter = new AtomicInteger();
63
    final var eol = lineSeparator();
64
    final var validator = new RangeValidator( mRange );
65
66
    walk( mParent, glob, files::add );
67
    files.sort( new AlphanumComparator<>() );
68
    files.forEach( file -> {
69
      try {
70
        final var filename = SysFile.getFileName( file );
71
        final var digits = toDigits( filename, chapter.incrementAndGet() );
72
73
        if( validator.test( digits ) ) {
74
          clue( "Main.status.export.concat", file );
75
76
          text.append( readString( file, UTF_8 ) )
77
              .append( eol );
78
        }
79
      } catch( final IOException ex ) {
80
        clue( "Main.status.export.concat.io", file );
81
      }
82
    } );
83
84
    return text.toString();
85
  }
86
}
187
A src/main/java/com/keenwrite/config/PropertiesConfiguration.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.config;
6
7
import com.keenwrite.collections.InterpolatingMap;
8
9
import java.io.IOException;
10
import java.io.Reader;
11
import java.util.*;
12
13
import static java.util.Arrays.*;
14
15
/**
16
 * Responsible for reading and interpolating properties files.
17
 */
18
public class PropertiesConfiguration {
19
  private static final String VALUE_SEPARATOR = ",";
20
21
  private final InterpolatingMap mMap = new InterpolatingMap();
22
23
  public PropertiesConfiguration() {}
24
25
  public void read( final Reader reader ) throws IOException {
26
    final var properties = new Properties();
27
    properties.load( reader );
28
29
    for( final var name : properties.stringPropertyNames() ) {
30
      mMap.put( name, properties.getProperty( name ) );
31
    }
32
33
    mMap.interpolate();
34
  }
35
36
  /**
37
   * Returns the value of a string property.
38
   *
39
   * @param property     The property key.
40
   * @param defaultValue The value to return if no property key has been set.
41
   * @return The property key value, or defaultValue when no key found.
42
   */
43
  public String getString( final String property, final String defaultValue ) {
44
    assert property != null;
45
46
    return mMap.getOrDefault( property, defaultValue );
47
  }
48
49
  /**
50
   * Returns the value of a string property.
51
   *
52
   * @param property     The property key.
53
   * @param defaultValue The value to return if no property key has been set.
54
   * @return The property key value, or defaultValue when no key found.
55
   */
56
  public int getInt( final String property, final int defaultValue ) {
57
    assert property != null;
58
59
    return parse( mMap.get( property ), defaultValue );
60
  }
61
62
  /**
63
   * Convert the generic list of property objects into strings.
64
   *
65
   * @param property The property value to coerce.
66
   * @param defaults The values to use should the property be unset.
67
   * @return The list of properties coerced from objects to strings.
68
   */
69
  public List<String> getList(
70
    final String property, final List<String> defaults ) {
71
    assert property != null;
72
73
    final var value = mMap.get( property );
74
75
    return value == null
76
      ? defaults
77
      : asList( value.split( VALUE_SEPARATOR ) );
78
  }
79
80
  /**
81
   * Returns a list of property names that begin with the given prefix.
82
   * Note that the prefix must be separated from other values with a
83
   * period.
84
   *
85
   * @param prefix The prefix to compare against each property name. When
86
   *               comparing, the prefix value will have a period appended.
87
   * @return The list of property names that have the given prefix.
88
   */
89
  public Iterator<String> getKeys( final String prefix ) {
90
    assert prefix != null;
91
92
    final var result = new HashMap<String, String>();
93
    final var prefixDotted = prefix + '.';
94
95
    for( final var entry : mMap.entrySet() ) {
96
      final var key = entry.getKey();
97
98
      if( key.startsWith( prefixDotted ) ) {
99
        final var value = entry.getValue();
100
        result.put( key, value );
101
      }
102
    }
103
104
    return result.keySet().iterator();
105
  }
106
107
  private static int parse( final String s, final int defaultValue ) {
108
    try {
109
      return s == null || s.isBlank() ? defaultValue : Integer.parseInt( s );
110
    } catch( final NumberFormatException e ) {
111
      return defaultValue;
112
    }
113
  }
114
}
1115
A src/main/java/com/keenwrite/constants/Constants.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.constants;
6
7
import com.keenwrite.Bootstrap;
8
import com.keenwrite.Services;
9
import com.keenwrite.service.Settings;
10
11
import java.io.File;
12
import java.nio.charset.Charset;
13
import java.nio.file.Path;
14
import java.util.Locale;
15
16
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
17
import static com.keenwrite.Bootstrap.USER_DATA_DIR;
18
import static com.keenwrite.io.SysFile.toFile;
19
import static com.keenwrite.io.UserDataDir.getUserHome;
20
import static com.keenwrite.preferences.LocaleScripts.withScript;
21
import static com.keenwrite.util.SystemUtils.*;
22
import static java.io.File.separator;
23
import static java.lang.String.format;
24
import static java.lang.System.getProperty;
25
26
/**
27
 * Defines application-wide default values.
28
 */
29
public final class Constants {
30
31
  /**
32
   * Used by the default settings to load the {@link Settings} service. This
33
   * must come before any attempt is made to create a {@link Settings} object.
34
   * The reference to {@link Bootstrap#APP_TITLE_LOWERCASE} should cause the
35
   * JVM to load {@link Bootstrap} prior to proceeding. Loading that class
36
   * beforehand will read the bootstrap properties file to determine the
37
   * application name, which is then used to locate the settings properties.
38
   */
39
  public static final String PATH_PROPERTIES_SETTINGS =
40
    format( "/com/%s/settings.properties", APP_TITLE_LOWERCASE );
41
42
  /**
43
   * The {@link Settings} uses {@link #PATH_PROPERTIES_SETTINGS}.
44
   */
45
  public static final Settings sSettings = Services.load( Settings.class );
46
47
  public static final double WINDOW_X_DEFAULT = 0;
48
  public static final double WINDOW_Y_DEFAULT = 0;
49
  public static final double WINDOW_W_DEFAULT = 1200;
50
  public static final double WINDOW_H_DEFAULT = 800;
51
52
  public static final File DOCUMENT_DEFAULT = getFile( "document" );
53
  public static final int DOCUMENT_OFFSET = 0;
54
  public static final File DEFINITION_DEFAULT = getFile( "definition" );
55
  public static final File PDF_DEFAULT = getFile( "pdf" );
56
57
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
58
59
  public static final String STYLESHEET_APPLICATION_BASE =
60
    get( "file.stylesheet.application.base" );
61
  public static final String STYLESHEET_APPLICATION_SKIN =
62
    get( "file.stylesheet.application.skin" );
63
  public static final String STYLESHEET_MARKDOWN =
64
    get( "file.stylesheet.markdown" );
65
  public static final String STYLESHEET_MARKDOWN_LOCALE =
66
    "file.stylesheet.markdown.locale";
67
  public static final String STYLESHEET_PREVIEW =
68
    get( "file.stylesheet.preview" );
69
  public static final String STYLESHEET_PREVIEW_LOCALE =
70
    "file.stylesheet.preview.locale";
71
72
  public static final File FILE_PREFERENCES = getPreferencesFile();
73
74
  /**
75
   * Refer to file name extension settings in the configuration file. Do not
76
   * terminate with a period.
77
   */
78
  public static final String GLOB_PREFIX_FILE = "file.ext";
79
80
  /**
81
   * Three parameters: line number, column number, and offset.
82
   */
83
  public static final String STATUS_BAR_LINE = "Main.status.line";
84
85
  public static final String STATUS_BAR_OK = "Main.status.state.default";
86
87
  /**
88
   * Used to show an error while parsing, usually syntactical.
89
   */
90
  public static final String STATUS_PARSE_ERROR = "Main.status.error.parse";
91
  public static final String STATUS_DEFINITION_BLANK =
92
    "Main.status.error.def.blank";
93
  public static final String STATUS_DEFINITION_EMPTY =
94
    "Main.status.error.def.empty";
95
96
  /**
97
   * One parameter: the word under the cursor that could not be found.
98
   */
99
  public static final String STATUS_DEFINITION_MISSING =
100
    "Main.status.error.def.missing";
101
102
  /**
103
   * Default image extension order to use when scanning.
104
   */
105
  public static final String PERSIST_IMAGES_DEFAULT =
106
    get( "file.ext.image.order" );
107
108
  /**
109
   * Default working directory.
110
   */
111
  public static final File USER_DIRECTORY =
112
    new File( System.getProperty( "user.dir" ) );
113
114
  /**
115
   * Location to write temporary files.
116
   */
117
  public static final String TEMPORARY_DIRECTORY =
118
    System.getProperty( "java.io.tmpdir" );
119
120
  public static final String NEWLINE = System.lineSeparator();
121
122
  /**
123
   * Default path to use for an untitled (pathless) file.
124
   */
125
  public static final Path DEFAULT_DIRECTORY = USER_DIRECTORY.toPath();
126
127
  /**
128
   * Default character set to use when reading/writing files.
129
   */
130
  public static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
131
132
  /**
133
   * Default starting delimiter for definition variables. This value must
134
   * not overlap math delimiters, so do not use $ tokens as the first
135
   * delimiter.
136
   */
137
  public static final String DEF_DELIM_BEGAN_DEFAULT = "{{";
138
139
  /**
140
   * Default ending delimiter for definition variables.
141
   */
142
  public static final String DEF_DELIM_ENDED_DEFAULT = "}}";
143
144
  /**
145
   * Default starting delimiter when inserting R variables.
146
   */
147
  public static final String R_DELIM_BEGAN_DEFAULT = "x( ";
148
149
  /**
150
   * Default ending delimiter when inserting R variables.
151
   */
152
  public static final String R_DELIM_ENDED_DEFAULT = " )";
153
154
  /**
155
   * Resource directory where different language lexicons are located.
156
   */
157
  public static final String LEXICONS_DIRECTORY = "lexicons";
158
159
  /**
160
   * Absolute location of true type font files within the Java archive file.
161
   */
162
  public static final String FONT_DIRECTORY = "/fonts";
163
164
  /**
165
   * Default text editor font name.
166
   */
167
  public static final String FONT_NAME_EDITOR_DEFAULT = "Noto Sans Regular";
168
169
  /**
170
   * Default text editor font size, in points.
171
   */
172
  public static final float FONT_SIZE_EDITOR_DEFAULT = 12f;
173
174
  /**
175
   * Default preview font name.
176
   */
177
  public static final String FONT_NAME_PREVIEW_DEFAULT = "Source Serif 4";
178
179
  /**
180
   * Default preview font size, in points.
181
   */
182
  public static final float FONT_SIZE_PREVIEW_DEFAULT = 13f;
183
184
  /**
185
   * Scaling factor for rendering mathematics.
186
   */
187
  public static final double FONT_SIZE_MATH_DEFAULT = 2;
188
189
  /**
190
   * Default monospace preview font name.
191
   */
192
  public static final String FONT_NAME_PREVIEW_MONO_NAME_DEFAULT =
193
    "Source Code Pro";
194
195
  /**
196
   * Default monospace preview font size, in points.
197
   */
198
  public static final float FONT_SIZE_PREVIEW_MONO_SIZE_DEFAULT = 13f;
199
200
  /**
201
   * Default locale for font loading, including ISO 15924 alpha-4 script code.
202
   */
203
  public static final Locale LOCALE_DEFAULT = withScript( Locale.getDefault() );
204
205
  /**
206
   * Default CSS to apply (resolves to a minimal implementation).
207
   */
208
  public static final String SKIN_DEFAULT = "Modena Light";
209
210
  /**
211
   * Custom JavaFX CSS to apply to user interface.
212
   */
213
  public static final File SKIN_CUSTOM_DEFAULT = null;
214
215
  /**
216
   * Custom HTML CSS to apply to HTML preview panel.
217
   */
218
  public static final File PREVIEW_CUSTOM_DEFAULT = null;
219
220
  /**
221
   * Default identifier to use for synchronized scrolling.
222
   */
223
  public static final String CARET_ID = "caret";
224
225
  /**
226
   * Default spacing for UI items (e.g., toolbars).
227
   */
228
  public static final int UI_CONTROL_SPACING = 10;
229
230
  /**
231
   * Default server name for rendering diagrams.
232
   */
233
  public static final String DIAGRAM_SERVER_NAME = "kroki.io";
234
235
  /**
236
   * Application action messages properties prefix.
237
   */
238
  public static final String ACTION_PREFIX = "Action.";
239
240
  /**
241
   * Restrict theme names when displaying.
242
   */
243
  public static final byte THEME_NAME_LENGTH = 30;
244
245
  /**
246
   * The default apostrophe to use when exporting.
247
   */
248
  public static final String APOS_DEFAULT = "apos";
249
250
  /**
251
   * Prevent instantiation.
252
   */
253
  private Constants() {}
254
255
  /**
256
   * Converts from points to pixels because FlyingSaucer cannot handle points
257
   * properly. This is used to convert font sizes.
258
   *
259
   * @param points The points to convert to pixels.
260
   * @return The given number of points in equivalent pixels.
261
   */
262
  public static int toPixels( final double points ) {
263
    return (int) (points * (1 + 1 / 3f));
264
  }
265
266
  public static String get( final String key ) {
267
    return sSettings.getSetting( key, "" );
268
  }
269
270
  /**
271
   * Returns a default {@link File} instance based on the given key suffix.
272
   *
273
   * @param suffix Appended to {@code "file.default."}.
274
   * @return A new {@link File} instance that references the settings file name.
275
   */
276
  private static File getFile( final String suffix ) {
277
    return new File( get( String.format( "file.default.%s", suffix ) ) );
278
  }
279
280
  /**
281
   * Returns the equivalent of {@code $HOME/.filename.xml}.
282
   */
283
  private static File getPreferencesFile() {
284
    return new File( format(
285
      "%s%s.%s.xml",
286
      getProperty( "user.home" ),
287
      separator,
288
      APP_TITLE_LOWERCASE
289
    ) );
290
  }
291
292
  /**
293
   * Tries to get a system-independent path to the user's fonts directory.
294
   */
295
  public static File getFontDirectory() {
296
    final var FONT_PATH = Path.of( "fonts" );
297
    final var USER_HOME = getUserHome();
298
299
    final String fontBase;
300
    final Path fontUser;
301
302
    if( IS_OS_WINDOWS ) {
303
      fontBase = System.getenv( "WINDIR" );
304
      fontUser = FONT_PATH;
305
    }
306
    else if( IS_OS_MAC ) {
307
      fontBase = USER_HOME;
308
      fontUser = Path.of( "Library", "Fonts" );
309
    }
310
    else if( IS_OS_UNIX ) {
311
      fontBase = USER_HOME;
312
      fontUser = Path.of( ".fonts" );
313
    }
314
    else {
315
      fontBase = USER_DATA_DIR.toString();
316
      fontUser = FONT_PATH;
317
    }
318
319
    final var base = fontBase == null
320
      ? USER_DATA_DIR.relativize( fontUser )
321
      : Path.of( fontBase ).resolve( fontUser );
322
323
    return toFile( base );
324
  }
325
}
1326
A src/main/java/com/keenwrite/constants/GraphicsConstants.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.constants;
6
7
import javafx.scene.image.Image;
8
import javafx.scene.image.ImageView;
9
10
import java.util.ArrayList;
11
import java.util.List;
12
13
import static com.keenwrite.constants.Constants.get;
14
15
/**
16
 * Defines application-wide default values for GUI-related items. This helps
17
 * ensure that unit tests that have no graphical dependencies will pass.
18
 */
19
public class GraphicsConstants {
20
  public static final List<Image> LOGOS = createImages(
21
    "file.logo.16",
22
    "file.logo.32",
23
    "file.logo.128",
24
    "file.logo.256",
25
    "file.logo.512"
26
  );
27
28
  public static final Image ICON_DIALOG = LOGOS.get( 1 );
29
30
  public static final ImageView ICON_DIALOG_NODE = new ImageView( ICON_DIALOG );
31
32
  /**
33
   * Converts the given file names to images, such as application icons.
34
   *
35
   * @param keys The file names to convert to images.
36
   * @return The images loaded from the file name references.
37
   */
38
  @SuppressWarnings( "SameParameterValue" )
39
  private static List<Image> createImages( final String... keys ) {
40
    final List<Image> images = new ArrayList<>( keys.length );
41
42
    for( final var key : keys ) {
43
      images.add( new Image( get( key ) ) );
44
    }
45
46
    return images;
47
  }
48
}
149
A src/main/java/com/keenwrite/dom/DocumentConverter.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.dom;
6
7
import org.jetbrains.annotations.NotNull;
8
import org.jsoup.Jsoup;
9
import org.jsoup.helper.W3CDom;
10
import org.jsoup.nodes.Document.OutputSettings.Syntax;
11
import org.jsoup.nodes.Node;
12
import org.jsoup.nodes.TextNode;
13
import org.jsoup.select.NodeVisitor;
14
import org.w3c.dom.Document;
15
16
import java.util.LinkedHashMap;
17
import java.util.Map;
18
19
import static com.keenwrite.dom.DocumentParser.sDomImplementation;
20
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
21
import static java.util.Map.entry;
22
import static java.util.Map.ofEntries;
23
24
/**
25
 * Responsible for converting JSoup document object model (DOM) to a W3C DOM.
26
 * Provides a lighter implementation than the superclass by overriding the
27
 * {@link #fromJsoup(org.jsoup.nodes.Document)} method to reuse factories,
28
 * builders, and implementations.
29
 */
30
public final class DocumentConverter extends W3CDom {
31
  /**
32
   * Retain insertion order using an instance of {@link LinkedHashMap} so
33
   * that ligature substitution uses longer ligatures ahead of shorter
34
   * ligatures. The word "ruffian" should use the "ffi" ligature, not the "ff"
35
   * ligature.
36
   */
37
  private static final Map<String, String> LIGATURES = ofEntries(
38
    entry( "ffi", "ffi" ),
39
    entry( "ffl", "ffl" ),
40
    entry( "ff", "ff" ),
41
    entry( "fi", "fi" ),
42
    entry( "fl", "fl" )
43
  );
44
45
  private static final NodeVisitor LIGATURE_VISITOR = new NodeVisitor() {
46
    @Override
47
    public void head( final @NotNull Node node, final int depth ) {
48
      if( node instanceof final TextNode textNode ) {
49
        final var parent = node.parentNode();
50
        final var name = parent == null ? "root" : parent.nodeName();
51
        final var codeBlock =
52
          "pre".equalsIgnoreCase( name ) ||
53
          "code".equalsIgnoreCase( name ) ||
54
          "kbd".equalsIgnoreCase( name ) ||
55
          "var".equalsIgnoreCase( name ) ||
56
          "tex".equalsIgnoreCase( name ) ||
57
          "tt".equalsIgnoreCase( name );
58
59
        if( !codeBlock ) {
60
          // Obtaining the whole text will return newlines, which must be kept
61
          // to ensure that preformatted text maintains its formatting.
62
          textNode.text( replace( textNode.getWholeText(), LIGATURES ) );
63
        }
64
      }
65
    }
66
67
    @Override
68
    public void tail( final @NotNull Node node, final int depth ) {
69
    }
70
  };
71
72
  @Override
73
  public @NotNull Document fromJsoup( final org.jsoup.nodes.Document in ) {
74
    assert in != null;
75
76
    final var out = DocumentParser.newDocument();
77
    final var doctype = in.documentType();
78
79
    if( doctype != null ) {
80
      out.appendChild(
81
        sDomImplementation.createDocumentType(
82
          doctype.name(),
83
          doctype.publicId(),
84
          doctype.systemId()
85
        )
86
      );
87
    }
88
89
    out.setXmlStandalone( true );
90
    in.traverse( LIGATURE_VISITOR );
91
    convert( in, out );
92
93
    return out;
94
  }
95
96
  /**
97
   * Converts the given non-well-formed HTML document into an XML document
98
   * while preserving whitespace.
99
   *
100
   * @param html The document to convert.
101
   * @return The converted document as an object model.
102
   */
103
  public static org.jsoup.nodes.Document parse( final String html ) {
104
    final var document = Jsoup.parse( html );
105
106
    document
107
      .outputSettings()
108
      .syntax( Syntax.xml )
109
      .prettyPrint( false );
110
111
    return document;
112
  }
113
}
1114
A src/main/java/com/keenwrite/dom/DocumentParser.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.dom;
6
7
import com.keenwrite.util.Strings;
8
import org.w3c.dom.*;
9
import org.xml.sax.InputSource;
10
import org.xml.sax.SAXException;
11
12
import javax.xml.parsers.DocumentBuilder;
13
import javax.xml.parsers.DocumentBuilderFactory;
14
import javax.xml.transform.Transformer;
15
import javax.xml.transform.TransformerException;
16
import javax.xml.transform.TransformerFactory;
17
import javax.xml.transform.dom.DOMSource;
18
import javax.xml.transform.stream.StreamResult;
19
import javax.xml.xpath.XPath;
20
import javax.xml.xpath.XPathExpression;
21
import javax.xml.xpath.XPathExpressionException;
22
import javax.xml.xpath.XPathFactory;
23
import java.io.*;
24
import java.nio.file.Path;
25
import java.util.HashMap;
26
import java.util.Locale;
27
import java.util.Map;
28
import java.util.function.Consumer;
29
30
import static com.keenwrite.events.StatusEvent.clue;
31
import static com.keenwrite.io.SysFile.toFile;
32
import static java.nio.charset.StandardCharsets.UTF_16;
33
import static java.nio.charset.StandardCharsets.UTF_8;
34
import static java.nio.file.Files.write;
35
import static javax.xml.transform.OutputKeys.*;
36
import static javax.xml.xpath.XPathConstants.NODE;
37
import static javax.xml.xpath.XPathConstants.NODESET;
38
39
/**
40
 * Responsible for initializing an XML parser.
41
 */
42
public class DocumentParser {
43
  private static final String LOAD_EXTERNAL_DTD =
44
    "http://apache.org/xml/features/nonvalidating/load-external-dtd";
45
  private static final String INDENT_AMOUNT =
46
    "{http://xml.apache.org/xslt}indent-amount";
47
  private static final String NAMESPACE = "http://www.w3.org/1999/xhtml";
48
49
  private static final XPath XPATH = XPathFactory.newInstance().newXPath();
50
51
  private static final ByteArrayOutputStream sWriter =
52
    new ByteArrayOutputStream( 65536 );
53
  private static final OutputStreamWriter sOutput =
54
    new OutputStreamWriter( sWriter, UTF_8 );
55
56
  /**
57
   * Caches {@link XPathExpression}s to avoid re-compiling.
58
   */
59
  private static final Map<String, XPathExpression> sXpaths = new HashMap<>();
60
61
  private static final DocumentBuilderFactory sDocumentFactory;
62
  private static DocumentBuilder sDocumentBuilder;
63
  private static Transformer sTransformer;
64
  private static final XPath sXpath = XPathFactory.newInstance().newXPath();
65
66
  public static final DOMImplementation sDomImplementation;
67
68
  static {
69
    sDocumentFactory = DocumentBuilderFactory.newInstance();
70
71
    sDocumentFactory.setValidating( false );
72
    sDocumentFactory.setAttribute( LOAD_EXTERNAL_DTD, false );
73
    sDocumentFactory.setNamespaceAware( true );
74
    sDocumentFactory.setIgnoringComments( true );
75
    sDocumentFactory.setIgnoringElementContentWhitespace( true );
76
77
    DOMImplementation domImplementation;
78
79
    try {
80
      sDocumentBuilder = sDocumentFactory.newDocumentBuilder();
81
      domImplementation = sDocumentBuilder.getDOMImplementation();
82
      sTransformer = TransformerFactory.newInstance().newTransformer();
83
84
      // Ensure Unicode characters (emojis) are encoded correctly.
85
      sTransformer.setOutputProperty( ENCODING, UTF_16.toString() );
86
      sTransformer.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
87
      sTransformer.setOutputProperty( METHOD, "xml" );
88
      sTransformer.setOutputProperty( INDENT, "no" );
89
      sTransformer.setOutputProperty( INDENT_AMOUNT, "2" );
90
    }
91
    catch( final Exception ex ) {
92
      clue( ex );
93
      domImplementation = sDocumentBuilder.getDOMImplementation();
94
    }
95
96
    sDomImplementation = domImplementation;
97
  }
98
99
  public static Document newDocument() {
100
    return sDocumentBuilder.newDocument();
101
  }
102
103
  /**
104
   * Creates a new document object model based on the given XML document
105
   * string. This will return an empty document if the document could not
106
   * be parsed.
107
   *
108
   * @param xml The document text to convert into a DOM.
109
   * @return The DOM that represents the given XML data.
110
   */
111
  public static Document parse( final String xml ) {
112
    assert xml != null;
113
114
    if( !xml.isBlank() ) {
115
      try( final var reader = new StringReader( xml ) ) {
116
        final var input = new InputSource();
117
118
        input.setEncoding( UTF_8.toString() );
119
        input.setCharacterStream( reader );
120
121
        return sDocumentBuilder.parse( input );
122
      }
123
      catch( final Throwable t ) {
124
        clue( t );
125
      }
126
    }
127
128
    return sDocumentBuilder.newDocument();
129
  }
130
131
  /**
132
   * Creates a well-formed XHTML document from a standard HTML document.
133
   *
134
   * @param source   The HTML source document to transform.
135
   * @param metadata The metadata contained within the head element.
136
   * @param locale   The localization information for the lang attribute.
137
   * @return The well-formed XHTML document.
138
   */
139
  public static Document create(
140
    final Document source,
141
    final Map<String, String> metadata,
142
    final Locale locale,
143
    final String pageTitle
144
  ) throws XPathExpressionException {
145
    final var target = createXhtmlDocument();
146
    final var html = target.getDocumentElement();
147
    final var sourceHead = evaluate( "//head", source );
148
    final var head = target.importNode( sourceHead, true );
149
150
    html.setAttribute( "lang", locale.getLanguage() );
151
152
    final var encoding = createEncoding( target, "UTF-8" );
153
    head.appendChild( encoding );
154
155
    for( final var entry : metadata.entrySet() ) {
156
      final var node = createMeta( target, entry );
157
      head.appendChild( node );
158
    }
159
160
    final var titleText = Strings.sanitize( pageTitle );
161
162
    // Empty titles result in <title/>, which some browsers cannot parse.
163
    if( !titleText.isBlank() ) {
164
      final var title = createElement( target, "title", titleText );
165
      head.appendChild( title );
166
    }
167
168
    html.appendChild( head );
169
170
    final var body = createElement( target, "body", null );
171
    final var sourceBody = source.getElementsByTagName( "body" ).item( 0 );
172
    final var children = sourceBody.getChildNodes();
173
    final var count = children.getLength();
174
175
    for( var i = 0; i < count; i++ ) {
176
      body.appendChild( importNode( target, children.item( i ) ) );
177
    }
178
179
    html.appendChild( body );
180
181
    return target;
182
  }
183
184
  public static Node evaluate( final String xpath, final Document doc ) throws XPathExpressionException {
185
    return (Node) XPATH.evaluate( xpath, doc, NODE );
186
  }
187
188
  /**
189
   * Parses the given file contents into a document object model.
190
   *
191
   * @param doc The source XML document to parse.
192
   * @return The file as a document object model.
193
   * @throws IOException  Could not open the document.
194
   * @throws SAXException Could not read the XML file content.
195
   */
196
  public static Document parse( final File doc )
197
    throws IOException, SAXException {
198
    assert doc != null;
199
200
    try( final var in = new FileInputStream( doc ) ) {
201
      return parse( in );
202
    }
203
  }
204
205
  /**
206
   * Parses the given file contents into a document object model. Callers
207
   * must close the stream.
208
   *
209
   * @param doc The source XML document to parse.
210
   * @return The {@link InputStream} converted to a document object model.
211
   * @throws IOException  Could not open the document.
212
   * @throws SAXException Could not read the XML file content.
213
   */
214
  public static Document parse( final InputStream doc )
215
    throws IOException, SAXException {
216
    assert doc != null;
217
218
    return sDocumentBuilder.parse( doc );
219
  }
220
221
  /**
222
   * Allows an operation to be applied for every node in the document that
223
   * matches a given tag name pattern.
224
   *
225
   * @param document Document to traverse.
226
   * @param xpath    Document elements to find via {@link XPath} expression.
227
   * @param consumer The consumer to call for each matching document node.
228
   */
229
  public static void visit(
230
    final Document document,
231
    final CharSequence xpath,
232
    final Consumer<Node> consumer ) {
233
    assert document != null;
234
    assert consumer != null;
235
236
    try {
237
      final var expr = compile( xpath );
238
      final var nodeSet = expr.evaluate( document, NODESET );
239
240
      if( nodeSet instanceof NodeList nodes ) {
241
        for( int i = 0, len = nodes.getLength(); i < len; i++ ) {
242
          consumer.accept( nodes.item( i ) );
243
        }
244
      }
245
    }
246
    catch( final Exception ex ) {
247
      clue( ex );
248
    }
249
  }
250
251
  public static Node createMeta(
252
    final Document document, final Map.Entry<String, String> entry ) {
253
    assert document != null;
254
    assert entry != null;
255
256
    final var node = createElement( document, "meta", null );
257
258
    node.setAttribute( "name", entry.getKey() );
259
    node.setAttribute( "content", entry.getValue() );
260
261
    return node;
262
  }
263
264
  public static Node createEncoding(
265
    final Document document, final String encoding
266
  ) {
267
    assert document != null;
268
    assert encoding != null;
269
270
    final var node = createElement( document, "meta", null );
271
272
    node.setAttribute( "http-equiv", "Content-Type" );
273
    node.setAttribute( "content", "text/html; charset=" + encoding );
274
275
    return node;
276
  }
277
278
  public static Element createElement(
279
    final Document document, final String nodeName, final String nodeValue
280
  ) {
281
    assert document != null;
282
    assert nodeName != null;
283
    assert !nodeName.isBlank();
284
285
    final var node = document.createElement( nodeName );
286
287
    if( nodeValue != null ) {
288
      node.setTextContent( nodeValue );
289
    }
290
291
    return node;
292
  }
293
294
  public static String toString( final Node xhtml ) {
295
    assert xhtml != null;
296
297
    String result = "";
298
299
    try( final var writer = new StringWriter() ) {
300
      final var stream = new StreamResult( writer );
301
302
      transform( xhtml, stream );
303
304
      result = writer.toString();
305
    }
306
    catch( final Exception ex ) {
307
      clue( ex );
308
    }
309
310
    return result;
311
  }
312
313
  public static String transform( final Element root )
314
    throws IOException, TransformerException {
315
    assert root != null;
316
317
    try( final var writer = new StringWriter() ) {
318
      transform( root.getOwnerDocument(), new StreamResult( writer ) );
319
320
      return writer.toString();
321
    }
322
  }
323
324
  /**
325
   * Remove whitespace, comments, and XML/DOCTYPE declarations to make
326
   * processing work with ConTeXt.
327
   *
328
   * @param path The SVG file to process.
329
   * @throws Exception The file could not be processed.
330
   */
331
  public static void sanitize( final Path path ) throws Exception {
332
    assert path != null;
333
334
    // Preprocessing the SVG image is a single-threaded operation, no matter
335
    // how many SVG images are in the document to typeset.
336
    sWriter.reset();
337
338
    final var target = new StreamResult( sOutput );
339
    final var source = sDocumentBuilder.parse( toFile( path ) );
340
341
    transform( source, target );
342
    write( path, sWriter.toByteArray() );
343
  }
344
345
  /**
346
   * Converts a string into an {@link XPathExpression}, which may be used to
347
   * extract elements from a {@link Document} object model.
348
   *
349
   * @param cs The string to convert to an {@link XPathExpression}.
350
   * @return {@code null} if there was an error compiling the xpath.
351
   */
352
  public static XPathExpression compile( final CharSequence cs ) {
353
    assert cs != null;
354
355
    final var xpath = cs.toString();
356
357
    return sXpaths.computeIfAbsent(
358
      xpath, _ -> {
359
        try {
360
          return sXpath.compile( xpath );
361
        }
362
        catch( final XPathExpressionException ex ) {
363
          clue( ex );
364
          return null;
365
        }
366
      }
367
    );
368
  }
369
370
  /**
371
   * Merges a source document into a target document. This avoids adding an
372
   * empty XML namespace attribute to elements.
373
   *
374
   * @param target The document to envelop the source document.
375
   * @param source The source document to embed.
376
   * @return The target document with the source document included.
377
   */
378
  private static Node importNode( final Document target, final Node source ) {
379
    assert target != null;
380
    assert source != null;
381
382
    Node result;
383
    final var nodeType = source.getNodeType();
384
385
    if( nodeType == Node.ELEMENT_NODE ) {
386
      final var element = createElement( target, source.getNodeName(), null );
387
      final var attrs = source.getAttributes();
388
389
      if( attrs != null ) {
390
        final var attrLength = attrs.getLength();
391
392
        for( var i = 0; i < attrLength; i++ ) {
393
          final var attr = attrs.item( i );
394
          element.setAttribute( attr.getNodeName(), attr.getNodeValue() );
395
        }
396
      }
397
398
      final var children = source.getChildNodes();
399
      final var childLength = children.getLength();
400
401
      for( var i = 0; i < childLength; i++ ) {
402
        element.appendChild( importNode( target, children.item( i ) ) );
403
      }
404
405
      result = element;
406
    }
407
    else if( nodeType == Node.TEXT_NODE ) {
408
      result = target.createTextNode( source.getNodeValue() );
409
    }
410
    else {
411
      result = target.importNode( source, true );
412
    }
413
414
    return result;
415
  }
416
417
  private static Document createXhtmlDocument() {
418
    return sDomImplementation.createDocument(
419
      NAMESPACE,
420
      "html",
421
      sDomImplementation.createDocumentType(
422
        "html", "-//W3C//DTD XHTML 1.0 Strict//EN",
423
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
424
      )
425
    );
426
  }
427
428
  /**
429
   * Streams an instance of {@link Document} as a plain text XML document.
430
   *
431
   * @param src The source document to transform.
432
   * @param dst The destination location to write the transformed version.
433
   * @throws TransformerException Could not transform the document.
434
   */
435
  private static void transform( final Node src, final StreamResult dst )
436
    throws TransformerException {
437
    sTransformer.transform( new DOMSource( src ), dst );
438
  }
439
440
  /**
441
   * Use the {@code static} constants and methods, not an instance, at least
442
   * until an iterable sub-interface is written.
443
   */
444
  private DocumentParser() {
445
  }
446
}
1447
A src/main/java/com/keenwrite/editors/TextDefinition.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.editors;
6
7
import com.keenwrite.editors.definition.DefinitionEditor;
8
import com.keenwrite.editors.definition.DefinitionTreeItem;
9
import com.keenwrite.editors.markdown.MarkdownEditor;
10
import javafx.scene.control.TreeItem;
11
12
import java.util.Map;
13
14
/**
15
 * Differentiates an instance of {@link TextResource} from an instance of
16
 * {@link DefinitionEditor} or {@link MarkdownEditor}.
17
 */
18
public interface TextDefinition extends TextResource {
19
20
  /**
21
   * Requests all variable definitions.
22
   *
23
   * @return The definition map without interpolation.
24
   */
25
  Map<String, String> getDefinitions();
26
27
  /**
28
   * Requests that the visual representation be expanded to the given node.
29
   *
30
   * @param node Request expansion to this node.
31
   */
32
  <T> void expand( TreeItem<T> node );
33
34
  /**
35
   * Adds a new item to the definition hierarchy.
36
   */
37
  void createDefinition();
38
39
  /**
40
   * Edits the currently selected definition in the hierarchy.
41
   */
42
  void renameDefinition();
43
44
  /**
45
   * Removes the currently selected definition in the hierarchy.
46
   */
47
  void deleteDefinitions();
48
49
  /**
50
   * Finds the definition that exact matches the given text.
51
   *
52
   * @param text The value to find, never {@code null}.
53
   * @return The leaf that contains the given value.
54
   */
55
  DefinitionTreeItem<String> findLeafExact( String text );
56
57
  /**
58
   * Finds the definition that starts with the given text.
59
   *
60
   * @param text The value to find, never {@code null}.
61
   * @return The leaf that starts with the given value.
62
   */
63
  DefinitionTreeItem<String> findLeafStartsWith( String text );
64
65
  /**
66
   * Finds the definition that contains the given text, matching case.
67
   *
68
   * @param text The value to find, never {@code null}.
69
   * @return The leaf that contains the exact given value.
70
   */
71
  DefinitionTreeItem<String> findLeafContains( String text );
72
73
  /**
74
   * Finds the definition that contains the given text, ignoring case.
75
   *
76
   * @param text The value to find, never {@code null}.
77
   * @return The leaf that contains the given value, regardless of case.
78
   */
79
  DefinitionTreeItem<String> findLeafContainsNoCase( String text );
80
81
  /**
82
   * Answers whether there are any definitions written.
83
   *
84
   * @return {@code true} when there are no definitions.
85
   */
86
  boolean isEmpty();
87
}
188
A src/main/java/com/keenwrite/editors/TextEditor.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.editors;
6
7
import com.keenwrite.editors.common.Caret;
8
import javafx.scene.control.IndexRange;
9
import org.fxmisc.flowless.VirtualizedScrollPane;
10
import org.fxmisc.richtext.StyleClassedTextArea;
11
12
/**
13
 * Responsible for differentiating an instance of {@link TextResource} from
14
 * other {@link TextResource} subtypes, such as a {@link TextDefinition}.
15
 * This is primarily used as a marker interface, but also defines a minimal
16
 * set of functionality required by all {@link TextEditor} instances, which
17
 * includes scrolling facilities.
18
 */
19
public interface TextEditor extends TextResource {
20
21
  /**
22
   * Returns the scrollbars associated with the editor's view so that they
23
   * can be moved for synchronized scrolling.
24
   *
25
   * @return The initialized horizontal and vertical scrollbars.
26
   */
27
  VirtualizedScrollPane<StyleClassedTextArea> getScrollPane();
28
29
  StyleClassedTextArea getTextArea();
30
31
  /**
32
   * Delegates requesting focus to the internal {@link StyleClassedTextArea}.
33
   */
34
  default void requestFocus() {
35
    getTextArea().requestFocus();
36
  }
37
38
  /**
39
   * Returns the complete text for the specified paragraph index.
40
   *
41
   * @param paragraph The zero-based paragraph index.
42
   * @throws IndexOutOfBoundsException The paragraph index is less than zero
43
   *                                   or greater than the number of
44
   *                                   paragraphs in the document.
45
   */
46
  String getText( int paragraph ) throws IndexOutOfBoundsException;
47
48
  /**
49
   * Returns the text between the indexes specified by the given
50
   * {@link IndexRange}.
51
   *
52
   * @param indexes The start and end document indexes to reference.
53
   * @return The text between the specified indexes.
54
   * @throws IndexOutOfBoundsException The indexes are invalid.
55
   */
56
  String getText( IndexRange indexes ) throws IndexOutOfBoundsException;
57
58
  /**
59
   * Moves the caret to the given document offset.
60
   *
61
   * @param offset The absolute offset into the document, zero-based.
62
   */
63
  void moveTo( final int offset );
64
65
  /**
66
   * Returns an object that can be used to track the current caret position
67
   * within the document.
68
   *
69
   * @return The caret's position, which is updated continuously.
70
   */
71
  Caret getCaret();
72
73
  /**
74
   * Replaces the text within the given range with the given string.
75
   *
76
   * @param indexes The starting and ending document indexes that represent
77
   *                the range of text to replace.
78
   * @param s       The text to replace, which can be shorter or longer than the
79
   *                specified range.
80
   */
81
  void replaceText( IndexRange indexes, String s );
82
83
  /**
84
   * Returns the starting and ending indexes into the document for the
85
   * word at the current caret position.
86
   * <p>
87
   * Finds the start and end indexes for the word in the current document,
88
   * where the caret is located. There are a few different scenarios, where
89
   * the caret can be at: the start, end, or middle of a word; also, the
90
   * caret can be at the end or beginning of a punctuated word; as well, the
91
   * caret could be at the beginning or end of the line or document.
92
   * </p>
93
   *
94
   * @return The start and ending index into the current document that
95
   * represent the word boundaries of the word under the caret.
96
   */
97
  IndexRange getCaretWord();
98
99
  /**
100
   * Convenience method to get the word at the current caret position.
101
   *
102
   * @return This will return the empty string if the caret is out of bounds.
103
   */
104
  default String getCaretWordText() {
105
    return getText( getCaretWord() );
106
  }
107
108
  /**
109
   * Requests undoing the last text-changing action.
110
   */
111
  void undo();
112
113
  /**
114
   * Requests redoing the last text-changing action that was undone.
115
   */
116
  void redo();
117
118
  /**
119
   * Requests cutting the selected text, or the current line if none selected.
120
   */
121
  void cut();
122
123
  /**
124
   * Requests copying the selected text, or no operation if none selected.
125
   */
126
  void copy();
127
128
  /**
129
   * Requests pasting from the clipboard into the editor. This will replace
130
   * text if selected, otherwise the clipboard contents are inserted at the
131
   * cursor.
132
   */
133
  void paste();
134
135
  /**
136
   * Requests selecting the entire document. This will replace the existing
137
   * selection, if any.
138
   */
139
  void selectAll();
140
141
  /**
142
   * Requests making the selected text, or word at caret, bold.
143
   */
144
  default void bold() {}
145
146
  /**
147
   * Requests making the selected text, or word at caret, italic.
148
   */
149
  default void italic() {}
150
151
  /**
152
   * Requests making the selected text, or word at caret, monospace.
153
   */
154
  default void monospace() {}
155
156
  /**
157
   * Requests making the selected text, or word at caret, a superscript.
158
   */
159
  default void superscript() {}
160
161
  /**
162
   * Requests making the selected text, or word at caret, a subscript.
163
   */
164
  default void subscript() {}
165
166
  /**
167
   * Requests making the selected text, or word at caret, struck.
168
   */
169
  default void strikethrough() {}
170
171
  /**
172
   * Requests making the selected text, or word at caret, a blockquote block.
173
   */
174
  default void blockquote() {}
175
176
  /**
177
   * Requests making the selected text, or word at caret, inline code.
178
   */
179
  default void code() {}
180
181
  /**
182
   * Requests making the selected text, or word at caret, a fenced code block.
183
   */
184
  default void fencedCodeBlock() {}
185
186
  /**
187
   * Requests making the selected text, or word at caret, a heading.
188
   *
189
   * @param level The heading level to apply (typically 1 through 3).
190
   */
191
  default void heading( final int level ) {}
192
193
  /**
194
   * Requests making the selected text, or word at caret, an unordered list
195
   * block.
196
   */
197
  default void unorderedList() {}
198
199
  /**
200
   * Requests making the selected text, or word at caret, an ordered list block.
201
   */
202
  default void orderedList() {}
203
204
  /**
205
   * Requests making the selected text, or inserting at the caret, a
206
   * horizontal rule.
207
   */
208
  default void horizontalRule() {}
209
210
  /**
211
   * Requests that styling be added to the document between the given
212
   * integer values.
213
   *
214
   * @param indexes Document offset where style is to start and end.
215
   * @param style   The style class to apply between the given offset indexes.
216
   */
217
  default void stylize( final IndexRange indexes, final String style ) {}
218
219
  /**
220
   * Requests that the most recent styling for the given style class be
221
   * removed from the document between the given integer values.
222
   */
223
  default void unstylize( final String style ) {}
224
}
1225
A src/main/java/com/keenwrite/editors/TextResource.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.editors;
6
7
import com.keenwrite.io.MediaType;
8
import com.keenwrite.util.EncodingDetector;
9
import javafx.beans.property.ReadOnlyBooleanProperty;
10
import javafx.scene.Node;
11
12
import java.io.File;
13
import java.nio.charset.Charset;
14
import java.nio.file.Path;
15
16
import static com.keenwrite.constants.Constants.DEFAULT_CHARSET;
17
import static com.keenwrite.events.StatusEvent.clue;
18
import static com.keenwrite.io.SysFile.toFile;
19
import static java.nio.file.Files.readAllBytes;
20
import static java.nio.file.Files.write;
21
import static java.util.Arrays.asList;
22
23
/**
24
 * A text resource can be persisted and retrieved from its persisted location.
25
 */
26
public interface TextResource {
27
  /**
28
   * Sets the text string that to be changed through some graphical user
29
   * interface. For example, a YAML document must be parsed from the given
30
   * text string into a tree view with which the user may interact.
31
   *
32
   * @param text The new content for the resource.
33
   */
34
  void setText( String text );
35
36
  /**
37
   * Returns the text string that may have been modified by the user through
38
   * some graphical user interface.
39
   *
40
   * @return The text value, based on the value set from
41
   * {@link #setText(String)}, but possibly mutated.
42
   */
43
  String getText();
44
45
  /**
46
   * Return the character encoding for this file.
47
   *
48
   * @return A non-null character set, primarily detected from file contents.
49
   */
50
  Charset getEncoding();
51
52
  /**
53
   * Renames the current file to the given fully qualified file name.
54
   *
55
   * @param file The new file name.
56
   */
57
  void rename( final File file );
58
59
  /**
60
   * Returns the file name, without any directory components, for this instance.
61
   * Useful for showing as a tab title.
62
   *
63
   * @return The file name value returned from {@link #getFile()}.
64
   */
65
  default String getFilename() {
66
    final var filename = getFile().toPath().getFileName();
67
    return filename == null ? "" : filename.toString();
68
  }
69
70
  /**
71
   * Returns the fully qualified {@link File} to the editable text resource.
72
   * Useful for showing as a tab tooltip, saving the file, or reading it.
73
   *
74
   * @return A non-null {@link File} instance.
75
   */
76
  File getFile();
77
78
  /**
79
   * Returns the {@link MediaType} associated with the file being edited.
80
   *
81
   * @return The {@link MediaType} for the editor's file.
82
   */
83
  default MediaType getMediaType() {
84
    return MediaType.fromFilename( getFile() );
85
  }
86
87
  /**
88
   * Answers whether this instance is an editor for at least one of the given
89
   * {@link MediaType} references.
90
   *
91
   * @param mediaTypes The {@link MediaType} references to compare against.
92
   * @return {@code true} if the given list of media types contains the
93
   * {@link MediaType} for this editor.
94
   */
95
  default boolean isMediaType( final MediaType... mediaTypes ) {
96
    return asList( mediaTypes ).contains( getMediaType() );
97
  }
98
99
  /**
100
   * Returns the fully qualified {@link Path} to the editable text resource.
101
   * This delegates to {@link #getFile()}.
102
   *
103
   * @return A non-null {@link Path} instance.
104
   */
105
  default Path getPath() {
106
    return getFile().toPath();
107
  }
108
109
  /**
110
   * Read the file contents and update the text accordingly. If the file
111
   * cannot be read then no changes will happen to the text. Fails silently.
112
   *
113
   * @param path The fully qualified {@link Path}, including a file name, to
114
   *             fully read into the editor.
115
   * @return The character encoding for the file at the given {@link Path}.
116
   */
117
  default Charset open( final Path path ) {
118
    final var file = toFile( path );
119
    Charset encoding = DEFAULT_CHARSET;
120
121
    try {
122
      if( file.exists() ) {
123
        if( file.canWrite() && file.canRead() ) {
124
          final var bytes = readAllBytes( path );
125
          encoding = detectEncoding( bytes );
126
127
          setText( asString( bytes, encoding ) );
128
        }
129
        else {
130
          clue( "TextResource.load.error.permissions", file.toString() );
131
        }
132
      }
133
      else {
134
        clue( "TextResource.load.error.unsaved", file.toString() );
135
      }
136
    } catch( final Exception ex ) {
137
      clue( ex );
138
    }
139
140
    return encoding;
141
  }
142
143
  /**
144
   * Read the file contents and update the text accordingly. If the file
145
   * cannot be read then no changes will happen to the text. This delegates
146
   * to {@link #open(Path)}.
147
   *
148
   * @param file The {@link File} to fully read into the editor.
149
   * @return The file's character encoding.
150
   */
151
  default Charset open( final File file ) {
152
    return open( file.toPath() );
153
  }
154
155
  /**
156
   * Save the file contents and clear the modified flag. If the file cannot
157
   * be saved, the exception is swallowed and this method returns {@code false}.
158
   *
159
   * @return {@code true} the file was saved; {@code false} if upon exception.
160
   */
161
  default boolean save() {
162
    try {
163
      write( getPath(), asBytes( getText() ) );
164
      clearModifiedProperty();
165
      return true;
166
    } catch( final Exception ex ) {
167
      clue( ex );
168
    }
169
170
    return false;
171
  }
172
173
  /**
174
   * Returns the node associated with this {@link TextResource}.
175
   *
176
   * @return The view component for the {@link TextResource}.
177
   */
178
  Node getNode();
179
180
  /**
181
   * Answers whether the resource has been modified.
182
   *
183
   * @return {@code true} the resource has changed; {@code false} means that
184
   * no changes to the resource have been made.
185
   */
186
  default boolean isModified() {
187
    return modifiedProperty().get();
188
  }
189
190
  /**
191
   * Returns a property that answers whether this text resource has been
192
   * changed from the original text that was opened.
193
   *
194
   * @return A property representing the modified state of this
195
   * {@link TextResource}.
196
   */
197
  ReadOnlyBooleanProperty modifiedProperty();
198
199
  /**
200
   * Lowers the modified flag such that listeners to the modified property
201
   * will be informed that the text that's being edited no longer differs
202
   * from what's persisted.
203
   */
204
  void clearModifiedProperty();
205
206
207
  /**
208
   * Converts the given string to an array of bytes using the encoding that was
209
   * originally detected (if any) and associated with this file.
210
   *
211
   * @param text The text to convert into the original file encoding.
212
   * @return A series of bytes ready for writing to a file.
213
   */
214
  private byte[] asBytes( final String text ) {
215
    return text.getBytes( getEncoding() );
216
  }
217
218
  private Charset detectEncoding( final byte[] bytes ) {
219
    return new EncodingDetector().detect( bytes );
220
  }
221
222
  /**
223
   * Answers whether the given resource are of the same conceptual type. This
224
   * method is intended to be overridden by subclasses.
225
   *
226
   * @param mediaType The type to compare.
227
   * @return {@code true} if the {@link TextResource} is compatible with the
228
   * given {@link MediaType}.
229
   */
230
  default boolean supports( final MediaType mediaType ) {
231
    return isMediaType( mediaType );
232
  }
233
234
  private static String asString( final byte[] text, final Charset encoding ) {
235
    return new String( text, encoding );
236
  }
237
}
1238
A src/main/java/com/keenwrite/editors/common/Caret.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.common;
3
4
import com.keenwrite.util.GenericBuilder;
5
6
import java.util.function.Supplier;
7
8
import static com.keenwrite.Messages.get;
9
import static com.keenwrite.constants.Constants.STATUS_BAR_LINE;
10
11
/**
12
 * Represents the absolute, relative, and maximum position of the caret. The
13
 * caret position is a character offset into the text.
14
 */
15
public class Caret {
16
17
  private final Mutator mMutator;
18
19
  public static GenericBuilder<Caret.Mutator, Caret> builder() {
20
    return GenericBuilder.of( Caret.Mutator::new, Caret::new );
21
  }
22
23
  /**
24
   * Configures a caret.
25
   */
26
  public static class Mutator {
27
    /**
28
     * Caret's current paragraph index (i.e., current caret line number).
29
     */
30
    private Supplier<Integer> mParagraph = () -> 0;
31
32
    /**
33
     * Used to count the number of lines in the text editor document.
34
     */
35
    private Supplier<Integer> mParagraphs = () -> 1;
36
37
    /**
38
     * Caret offset into the current paragraph, represented as a string index.
39
     */
40
    private Supplier<Integer> mParaOffset = () -> 0;
41
42
    /**
43
     * Caret offset into the full text, represented as a string index.
44
     */
45
    private Supplier<Integer> mTextOffset = () -> 0;
46
47
    /**
48
     * Total number of characters in the document.
49
     */
50
    private Supplier<Integer> mTextLength = () -> 0;
51
52
    /**
53
     * Sets the {@link Supplier} for the caret's current paragraph number.
54
     *
55
     * @param paragraph Returns the document caret paragraph index.
56
     */
57
    public void setParagraph( final Supplier<Integer> paragraph ) {
58
      assert paragraph != null;
59
      mParagraph = paragraph;
60
    }
61
62
    /**
63
     * Sets the {@link Supplier} for the total number of document paragraphs.
64
     *
65
     * @param paragraphs Returns the document paragraph count.
66
     */
67
    public void setParagraphs( final Supplier<Integer> paragraphs ) {
68
      assert paragraphs != null;
69
      mParagraphs = paragraphs;
70
    }
71
72
    /**
73
     * Sets the {@link Supplier} for the caret's current character offset
74
     * into the current paragraph.
75
     *
76
     * @param paraOffset Returns the caret's paragraph character index.
77
     */
78
    public void setParaOffset( final Supplier<Integer> paraOffset ) {
79
      assert paraOffset != null;
80
      mParaOffset = paraOffset;
81
    }
82
83
    /**
84
     * Sets the {@link Supplier} for the caret's current document position.
85
     * A value of 0 represents the start of the document.
86
     *
87
     * @param textOffset Returns the text offset into the current document.
88
     */
89
    public void setTextOffset( final Supplier<Integer> textOffset ) {
90
      assert textOffset != null;
91
      mTextOffset = textOffset;
92
    }
93
94
    /**
95
     * Sets the {@link Supplier} for the document's total character count.
96
     *
97
     * @param textLength Returns the total character count in the document.
98
     */
99
    public void setTextLength( final Supplier<Integer> textLength ) {
100
      assert textLength != null;
101
      mTextLength = textLength;
102
    }
103
  }
104
105
  /**
106
   * Force using the builder pattern.
107
   */
108
  private Caret( final Mutator mutator ) {
109
    assert mutator != null;
110
111
    mMutator = mutator;
112
  }
113
114
  /**
115
   * Answers whether the caret's offset into the text is between the given
116
   * offsets.
117
   *
118
   * @param began Starting value compared against the caret's text offset.
119
   * @param ended Ending value compared against the caret's text offset.
120
   * @return {@code true} when the caret's text offset is between the given
121
   * values, inclusively (for either value).
122
   */
123
  public boolean isBetweenText( final int began, final int ended ) {
124
    final var offset = getTextOffset();
125
    return began <= offset && offset <= ended;
126
  }
127
128
  /**
129
   * Answers whether the caret's offset into the paragraph is before the given
130
   * offset.
131
   *
132
   * @param offset Compared against the caret's paragraph offset.
133
   * @return {@code true} the caret's offset is before the given offset.
134
   */
135
  public boolean isBeforeColumn( final int offset ) {
136
    return getParaOffset() < offset;
137
  }
138
139
  /**
140
   * Answers whether the caret's offset into the text is before the given
141
   * text offset.
142
   *
143
   * @param offset Compared against the caret's text offset.
144
   * @return {@code true} the caret's offset is after the given offset.
145
   */
146
  public boolean isAfterColumn( final int offset ) {
147
    return getParaOffset() > offset;
148
  }
149
150
  /**
151
   * Answers whether the caret's offset into the text exceeds the length of
152
   * the text.
153
   *
154
   * @return {@code true} when the caret is at the end of the text boundary.
155
   */
156
  public boolean isAfterText() {
157
    return getTextOffset() >= getTextLength();
158
  }
159
160
  public boolean isAfter( final int offset ) {
161
    return offset >= getTextOffset();
162
  }
163
164
  private int getParagraph() {
165
    return mMutator.mParagraph.get();
166
  }
167
168
  /**
169
   * Returns the number of lines in the text editor.
170
   *
171
   * @return The size of the text editor's paragraph list plus one.
172
   */
173
  private int getParagraphCount() {
174
    return mMutator.mParagraphs.get();
175
  }
176
177
  /**
178
   * Returns the absolute position of the caret within the entire document.
179
   *
180
   * @return A zero-based index of the caret position.
181
   */
182
  private int getTextOffset() {
183
    return mMutator.mTextOffset.get();
184
  }
185
186
  /**
187
   * Returns the position of the caret within the current paragraph being
188
   * edited.
189
   *
190
   * @return A zero-based index of the caret position relative to the
191
   * current paragraph.
192
   */
193
  private int getParaOffset() {
194
    return mMutator.mParaOffset.get();
195
  }
196
197
  /**
198
   * Returns the total number of characters in the document being edited.
199
   *
200
   * @return A zero-based count of the total characters in the document.
201
   */
202
  private int getTextLength() {
203
    return mMutator.mTextLength.get();
204
  }
205
206
  /**
207
   * Returns a human-readable string that shows the current caret position
208
   * within the text. Typically, this will include the current line number,
209
   * the number of lines, and the character offset into the text.
210
   * <p>
211
   * If the {@link Caret} has not been properly built, this will return a
212
   * string for the status bar having all values set to zero. This can happen
213
   * during unit testing.
214
   *
215
   * @return A string to present to an end user.
216
   */
217
  @Override
218
  public String toString() {
219
    try {
220
      return get( STATUS_BAR_LINE,
221
                  getParagraph() + 1,
222
                  getParagraphCount(),
223
                  getTextOffset() + 1 );
224
    } catch( final Exception ex ) {
225
      return get( STATUS_BAR_LINE, 0, 0, 0 );
226
    }
227
  }
228
}
1229
A src/main/java/com/keenwrite/editors/common/ScrollEventHandler.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.editors.common;
6
7
import com.keenwrite.events.ScrollLockEvent;
8
import javafx.beans.property.BooleanProperty;
9
import javafx.beans.property.SimpleBooleanProperty;
10
import javafx.event.Event;
11
import javafx.event.EventHandler;
12
import javafx.scene.control.ScrollBar;
13
import javafx.scene.control.skin.ScrollBarSkin;
14
import javafx.scene.input.MouseEvent;
15
import javafx.scene.input.ScrollEvent;
16
import javafx.scene.layout.StackPane;
17
import org.fxmisc.flowless.VirtualizedScrollPane;
18
import org.fxmisc.richtext.StyleClassedTextArea;
19
import org.greenrobot.eventbus.Subscribe;
20
21
import javax.swing.*;
22
import java.util.function.Consumer;
23
24
import static com.keenwrite.events.Bus.register;
25
import static java.lang.Math.max;
26
import static java.lang.Math.min;
27
import static javafx.geometry.Orientation.VERTICAL;
28
import static javax.swing.SwingUtilities.invokeLater;
29
30
/**
31
 * Converts scroll events from {@link VirtualizedScrollPane} scroll bars to
32
 * an instance of {@link JScrollBar}.
33
 * <p>
34
 * Called to synchronize the scrolling areas for either scrolling with the
35
 * mouse or scrolling using the scrollbar's thumb. Both are required to avoid
36
 * scrolling on the estimatedScrollYProperty that occurs when text events
37
 * fire. Scrolling performed for text events are handled separately to ensure
38
 * the preview panel scrolls to the same position in the Markdown editor,
39
 * taking into account things like images, tables, and other potentially
40
 * long vertical presentation items.
41
 * </p>
42
 */
43
public final class ScrollEventHandler implements EventHandler<Event> {
44
45
  private final class MouseHandler implements EventHandler<MouseEvent> {
46
    private final EventHandler<? super MouseEvent> mOldHandler;
47
48
    /**
49
     * Constructs a new handler for mouse scrolling events.
50
     *
51
     * @param oldHandler Receives the event after scrolling takes place.
52
     */
53
    private MouseHandler( final EventHandler<? super MouseEvent> oldHandler ) {
54
      mOldHandler = oldHandler;
55
    }
56
57
    @Override
58
    public void handle( final MouseEvent event ) {
59
      ScrollEventHandler.this.handle( event );
60
      mOldHandler.handle( event );
61
    }
62
  }
63
64
  private final class ScrollHandler implements EventHandler<ScrollEvent> {
65
    @Override
66
    public void handle( final ScrollEvent event ) {
67
      ScrollEventHandler.this.handle( event );
68
    }
69
  }
70
71
  private final VirtualizedScrollPane<StyleClassedTextArea> mEditorScrollPane;
72
  private final JScrollBar mPreviewScrollBar;
73
  private final BooleanProperty mEnabled = new SimpleBooleanProperty();
74
75
  private boolean mLocked;
76
77
  /**
78
   * @param editorScrollPane Scroll event source (human movement).
79
   * @param previewScrollBar Scroll event destination (corresponding movement).
80
   */
81
  public ScrollEventHandler(
82
    final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane,
83
    final JScrollBar previewScrollBar ) {
84
    mEditorScrollPane = editorScrollPane;
85
    mPreviewScrollBar = previewScrollBar;
86
87
    mEditorScrollPane.addEventFilter( ScrollEvent.ANY, new ScrollHandler() );
88
89
    initVerticalScrollBarThumb(
90
      mEditorScrollPane,
91
      thumb -> {
92
        final var handler = new MouseHandler( thumb.getOnMouseDragged() );
93
        thumb.setOnMouseDragged( handler );
94
      }
95
    );
96
97
    register( this );
98
  }
99
100
  /**
101
   * Gets a property intended to be bound to selected property of the tab being
102
   * scrolled. This is required because there's only one preview pane but
103
   * multiple editor panes. Each editor pane maintains its own scroll position.
104
   *
105
   * @return A {@link BooleanProperty} representing whether the scroll
106
   * events for this tab are to be executed.
107
   */
108
  public BooleanProperty enabledProperty() {
109
    return mEnabled;
110
  }
111
112
  /**
113
   * Scrolls the preview scrollbar relative to the edit scrollbar. Algorithm
114
   * is based on Karl Tauber's ratio calculation.
115
   *
116
   * @param event Unused; either {@link MouseEvent} or {@link ScrollEvent}
117
   */
118
  @Override
119
  public void handle( final Event event ) {
120
    invokeLater( () -> {
121
      if( isEnabled() ) {
122
        // e prefix is for editor pane.
123
        final var eScrollPane = getEditorScrollPane();
124
        final var eScrollY =
125
          eScrollPane.estimatedScrollYProperty().getValue().intValue();
126
        final var eHeight = (int)
127
          (eScrollPane.totalHeightEstimateProperty().getValue().intValue()
128
            - eScrollPane.getHeight());
129
        final var eRatio = eHeight > 0
130
          ? min( max( eScrollY / (float) eHeight, 0 ), 1 ) : 0;
131
132
        // p prefix is for preview pane.
133
        final var pScrollBar = getPreviewScrollBar();
134
        final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight();
135
        final var pScrollY = (int) (pHeight * eRatio);
136
137
        pScrollBar.setValue( pScrollY );
138
        pScrollBar.getParent().repaint();
139
      }
140
    } );
141
  }
142
143
  @Subscribe
144
  public void handle( final ScrollLockEvent event ) {
145
    mLocked = event.isLocked();
146
  }
147
148
  private void initVerticalScrollBarThumb(
149
    final VirtualizedScrollPane<StyleClassedTextArea> pane,
150
    final Consumer<StackPane> consumer ) {
151
    // When the skin property is set, the stack pane is available (not null).
152
    getVerticalScrollBar( pane ).skinProperty().addListener( ( c, o, n ) -> {
153
      for( final var node : ((ScrollBarSkin) n).getChildren() ) {
154
        // Brittle, but what can you do?
155
        if( node.getStyleClass().contains( "thumb" ) ) {
156
          consumer.accept( (StackPane) node );
157
        }
158
      }
159
    } );
160
  }
161
162
  /**
163
   * Returns the vertical {@link ScrollBar} instance associated with the
164
   * given scroll pane. This is {@code null}-safe because the scroll pane
165
   * initializes its vertical {@link ScrollBar} upon construction.
166
   *
167
   * @param pane The scroll pane that contains a vertical {@link ScrollBar}.
168
   * @return The vertical {@link ScrollBar} associated with the scroll pane.
169
   * @throws IllegalStateException Could not obtain the vertical scroll bar.
170
   */
171
  private ScrollBar getVerticalScrollBar(
172
    final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
173
174
    for( final var node : pane.getChildrenUnmodifiable() ) {
175
      if( node instanceof final ScrollBar scrollBar &&
176
        scrollBar.getOrientation() == VERTICAL ) {
177
        return scrollBar;
178
      }
179
    }
180
181
    throw new IllegalStateException( "No vertical scroll bar found." );
182
  }
183
184
  private boolean isEnabled() {
185
    // TODO: As a minor optimization, when this is set to false, it could remove
186
    // the MouseHandler and ScrollHandler so that events only dispatch to one
187
    // object (instead of one per editor tab).
188
    return mEnabled.get() && !mLocked;
189
  }
190
191
  private VirtualizedScrollPane<StyleClassedTextArea> getEditorScrollPane() {
192
    return mEditorScrollPane;
193
  }
194
195
  private JScrollBar getPreviewScrollBar() {
196
    return mPreviewScrollBar;
197
  }
198
}
1199
A src/main/java/com/keenwrite/editors/common/VariableNameInjector.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.editors.common;
6
7
import com.keenwrite.editors.TextDefinition;
8
import com.keenwrite.editors.TextEditor;
9
import com.keenwrite.editors.definition.DefinitionTreeItem;
10
import com.keenwrite.io.MediaType;
11
import com.keenwrite.preferences.Key;
12
import com.keenwrite.preferences.Workspace;
13
import com.keenwrite.processors.r.RInlineEvaluator;
14
import com.keenwrite.sigils.PropertyKeyOperator;
15
import com.keenwrite.sigils.RKeyOperator;
16
17
import java.util.function.UnaryOperator;
18
19
import static com.keenwrite.constants.Constants.*;
20
import static com.keenwrite.events.StatusEvent.clue;
21
import static com.keenwrite.preferences.AppKeys.*;
22
23
/**
24
 * Provides the logic for injecting variable names within the editor.
25
 */
26
public final class VariableNameInjector {
27
  private final Workspace mWorkspace;
28
29
  public VariableNameInjector( final Workspace workspace ) {
30
    assert workspace != null;
31
32
    mWorkspace = workspace;
33
  }
34
35
  /**
36
   * Find a node that matches the current word and substitute the definition
37
   * reference. After calling this method the document being edited will have
38
   * the word under the caret replaced with a corresponding variable name
39
   * bracketed by sigils according to the document's media type.
40
   *
41
   * @param editor      The editor having a caret and a word under that caret.
42
   * @param definitions The list of variable definitions to search for a value
43
   *                    that matches the word under the caret.
44
   */
45
  public void autoinsert(
46
    final TextEditor editor,
47
    final TextDefinition definitions ) {
48
    assert editor != null;
49
    assert definitions != null;
50
51
    try {
52
      if( definitions.isEmpty() ) {
53
        clue( STATUS_DEFINITION_EMPTY );
54
      }
55
      else {
56
        final var indexes = editor.getCaretWord();
57
        final var word = editor.getText( indexes );
58
59
        if( word.isBlank() ) {
60
          clue( STATUS_DEFINITION_BLANK );
61
        }
62
        else {
63
          final var leaf = findLeaf( definitions, word );
64
65
          if( leaf == null ) {
66
            clue( STATUS_DEFINITION_MISSING, word );
67
          }
68
          else {
69
            insert( editor, leaf );
70
            definitions.expand( leaf );
71
          }
72
        }
73
      }
74
    } catch( final Exception ex ) {
75
      clue( STATUS_DEFINITION_BLANK, ex );
76
    }
77
  }
78
79
  public void insert(
80
    final TextEditor editor,
81
    final DefinitionTreeItem<String> leaf ) {
82
    assert editor != null;
83
    assert leaf != null;
84
85
    final var mediaType = editor.getMediaType();
86
    final var operator = createOperator( mediaType );
87
    final var indexes = editor.getCaretWord();
88
89
    editor.replaceText( indexes, operator.apply( leaf.toPath() ) );
90
  }
91
92
  /**
93
   * Creates an instance of {@link UnaryOperator} that can wrap a value with
94
   * sigils.
95
   *
96
   * @param mediaType The type of document with variables to sigilize.
97
   * @return An operator that produces sigilized variable names.
98
   */
99
  private UnaryOperator<String> createOperator( final MediaType mediaType ) {
100
    final String began;
101
    final String ended;
102
    final UnaryOperator<String> operator;
103
104
    switch( mediaType ) {
105
      case TEXT_MARKDOWN -> {
106
        began = getString( KEY_DEF_DELIM_BEGAN );
107
        ended = getString( KEY_DEF_DELIM_ENDED );
108
        operator = s -> s;
109
      }
110
      case TEXT_R_MARKDOWN -> {
111
        began = RInlineEvaluator.PREFIX + getString( KEY_R_DELIM_BEGAN );
112
        ended = getString( KEY_R_DELIM_ENDED ) + RInlineEvaluator.SUFFIX;
113
        operator = new RKeyOperator();
114
      }
115
      case TEXT_PROPERTIES -> {
116
        began = PropertyKeyOperator.BEGAN;
117
        ended = PropertyKeyOperator.ENDED;
118
        operator = s -> s;
119
      }
120
      default -> {
121
        began = "";
122
        ended = "";
123
        operator = s -> s;
124
      }
125
    }
126
127
    return s -> began + operator.apply( s ) + ended;
128
  }
129
130
  private String getString( final Key key ) {
131
    assert key != null;
132
133
    return mWorkspace.getString( key );
134
  }
135
136
  /**
137
   * Looks for the given word, matching first by exact, next by a starts-with
138
   * condition with diacritics replaced, then by containment.
139
   *
140
   * @param word Match the word by: exact, beginning, containment, or other.
141
   */
142
  @SuppressWarnings( "ConstantConditions" )
143
  private static DefinitionTreeItem<String> findLeaf(
144
    final TextDefinition definition, final String word ) {
145
    assert definition != null;
146
    assert word != null;
147
148
    DefinitionTreeItem<String> leaf;
149
150
    leaf = definition.findLeafExact( word );
151
    leaf = leaf == null ? definition.findLeafStartsWith( word ) : leaf;
152
    leaf = leaf == null ? definition.findLeafContains( word ) : leaf;
153
    leaf = leaf == null ? definition.findLeafContainsNoCase( word ) : leaf;
154
155
    return leaf;
156
  }
157
}
1158
A src/main/java/com/keenwrite/editors/definition/DefinitionEditor.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.editors.definition;
6
7
import com.keenwrite.constants.Constants;
8
import com.keenwrite.editors.TextDefinition;
9
import com.keenwrite.events.InsertDefinitionEvent;
10
import com.keenwrite.events.TextDefinitionFocusEvent;
11
import com.keenwrite.processors.r.Engine;
12
import com.keenwrite.ui.tree.AltTreeView;
13
import com.keenwrite.ui.tree.TreeItemConverter;
14
import javafx.beans.property.BooleanProperty;
15
import javafx.beans.property.ReadOnlyBooleanProperty;
16
import javafx.beans.property.SimpleBooleanProperty;
17
import javafx.beans.value.ObservableValue;
18
import javafx.collections.ObservableList;
19
import javafx.event.ActionEvent;
20
import javafx.event.Event;
21
import javafx.event.EventHandler;
22
import javafx.scene.Node;
23
import javafx.scene.control.*;
24
import javafx.scene.input.KeyEvent;
25
import javafx.scene.layout.BorderPane;
26
import javafx.scene.layout.HBox;
27
28
import java.io.File;
29
import java.nio.charset.Charset;
30
import java.util.*;
31
32
import static com.keenwrite.Messages.get;
33
import static com.keenwrite.constants.Constants.*;
34
import static com.keenwrite.events.StatusEvent.clue;
35
import static com.keenwrite.ui.fonts.IconFactory.createGraphic;
36
import static javafx.geometry.Pos.CENTER;
37
import static javafx.geometry.Pos.TOP_CENTER;
38
import static javafx.scene.control.SelectionMode.MULTIPLE;
39
import static javafx.scene.control.TreeItem.childrenModificationEvent;
40
import static javafx.scene.control.TreeItem.valueChangedEvent;
41
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
42
43
/**
44
 * Provides the user interface that holds a {@link TreeView}, which
45
 * allows users to interact with key/value pairs loaded from the
46
 * document parser and adapted using a {@link TreeTransformer}.
47
 */
48
public final class DefinitionEditor extends BorderPane
49
  implements TextDefinition {
50
51
  /**
52
   * Contains the root that is added to the view.
53
   */
54
  private final DefinitionTreeItem<String> mTreeRoot = createRootTreeItem();
55
56
  /**
57
   * Contains a view of the definitions.
58
   */
59
  private final TreeView<String> mTreeView =
60
    new AltTreeView<>( mTreeRoot, new TreeItemConverter() );
61
62
  /**
63
   * Used to adapt the structured document into a {@link TreeView}.
64
   */
65
  private final TreeTransformer mTreeTransformer;
66
67
  /**
68
   * Handlers for key press events.
69
   */
70
  private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
71
    = new HashSet<>();
72
73
  private final Map<String, String> mDefinitions = new HashMap<>();
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
   * File being edited by this editor instance, which may be renamed.
89
   */
90
  private File mFile;
91
92
  /**
93
   * This is provided for unit tests that are not backed by files.
94
   *
95
   * @param treeTransformer Responsible for transforming the definitions into
96
   *                        {@link TreeItem} instances.
97
   */
98
  public DefinitionEditor(
99
    final TreeTransformer treeTransformer ) {
100
    this( DEFINITION_DEFAULT, treeTransformer );
101
  }
102
103
  /**
104
   * Constructs a definition pane with a given tree view root.
105
   *
106
   * @param file The file of definitions to maintain through the UI.
107
   */
108
  public DefinitionEditor(
109
    final File file,
110
    final TreeTransformer treeTransformer ) {
111
    assert file != null;
112
    assert treeTransformer != null;
113
114
    mFile = file;
115
    mTreeTransformer = treeTransformer;
116
117
    mTreeView.setContextMenu( createContextMenu() );
118
    mTreeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
119
    mTreeView.focusedProperty().addListener( this::focused );
120
    getSelectionModel().setSelectionMode( MULTIPLE );
121
122
    final var buttonBar = new HBox();
123
    buttonBar.getChildren().addAll(
124
      createButton( "create", _ -> createDefinition() ),
125
      createButton( "rename", _ -> renameDefinition() ),
126
      createButton( "delete", _ -> deleteDefinitions() )
127
    );
128
    buttonBar.setAlignment( CENTER );
129
    buttonBar.setSpacing( UI_CONTROL_SPACING );
130
    setTop( buttonBar );
131
    setCenter( mTreeView );
132
    setAlignment( buttonBar, TOP_CENTER );
133
134
    mEncoding = open( mFile );
135
    updateDefinitions( getDefinitions(), getTreeView().getRoot() );
136
137
    // After the file is opened, watch for changes, not before. Otherwise,
138
    // upon saving, users will be prompted to save a file that hasn't had
139
    // any modifications (from their perspective).
140
    addTreeChangeHandler( _ -> {
141
      mModified.set( true );
142
      updateDefinitions( getDefinitions(), getTreeView().getRoot() );
143
    } );
144
  }
145
146
  /**
147
   * Replaces the given list of variable definitions with a flat hierarchy
148
   * of the converted {@link TreeView} root.
149
   *
150
   * @param definitions The definition map to update.
151
   * @param root        The values to flatten then insert into the map.
152
   */
153
  private void updateDefinitions(
154
    final Map<String, String> definitions,
155
    final TreeItem<String> root ) {
156
    definitions.clear();
157
    definitions.putAll( TreeItemMapper.convert( root ) );
158
    Engine.clear();
159
  }
160
161
  /**
162
   * Returns the variable definitions.
163
   *
164
   * @return The definition map.
165
   */
166
  @Override
167
  public Map<String, String> getDefinitions() {
168
    return mDefinitions;
169
  }
170
171
  @Override
172
  public void setText( final String document ) {
173
    final var foster = mTreeTransformer.transform( document );
174
    final var biological = getTreeRoot();
175
176
    for( final var child : foster.getChildren() ) {
177
      biological.getChildren().add( child );
178
    }
179
180
    getTreeView().refresh();
181
  }
182
183
  @Override
184
  public String getText() {
185
    final var result = new StringBuilder( 32768 );
186
187
    try {
188
      result.append( mTreeTransformer.transform( getTreeView().getRoot() ) );
189
190
      final var problem = isTreeWellFormed();
191
      problem.ifPresent( node -> clue( "yaml.error.tree.form", node ) );
192
    } catch( final Exception ex ) {
193
      // Catch errors while checking for a well-formed tree (e.g., stack smash).
194
      // Also catch any transformation exceptions (e.g., Json processing).
195
      clue( ex );
196
    }
197
198
    return result.toString();
199
  }
200
201
  @Override
202
  public File getFile() {
203
    return mFile;
204
  }
205
206
  @Override
207
  public void rename( final File file ) {
208
    mFile = file;
209
  }
210
211
  @Override
212
  public Charset getEncoding() {
213
    return mEncoding;
214
  }
215
216
  @Override
217
  public Node getNode() {
218
    return this;
219
  }
220
221
  @Override
222
  public ReadOnlyBooleanProperty modifiedProperty() {
223
    return mModified;
224
  }
225
226
  @Override
227
  public void clearModifiedProperty() {
228
    mModified.setValue( false );
229
  }
230
231
  private Button createButton(
232
    final String msgKey, final EventHandler<ActionEvent> eventHandler ) {
233
    final var keyPrefix = Constants.ACTION_PREFIX + "definition." + msgKey;
234
    final var button = new Button( get( keyPrefix + ".text" ) );
235
    final var graphic = createGraphic( get( keyPrefix + ".icon" ) );
236
237
    button.setOnAction( eventHandler );
238
    button.setGraphic( graphic );
239
    button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) );
240
241
    return button;
242
  }
243
244
  /**
245
   * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
246
   * is modified. The modifications include: item value changes, item additions,
247
   * and item removals.
248
   * <p>
249
   * Safe to call multiple times; if a handler is already registered, the
250
   * old handler is used.
251
   * </p>
252
   *
253
   * @param handler The handler to call whenever any {@link TreeItem} changes.
254
   */
255
  public void addTreeChangeHandler(
256
    final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
257
    final var root = getTreeView().getRoot();
258
    root.addEventHandler( valueChangedEvent(), handler );
259
    root.addEventHandler( childrenModificationEvent(), handler );
260
  }
261
262
  /**
263
   * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
264
   * well-formed for export. A tree is considered well-formed if the following
265
   * conditions are met:
266
   *
267
   * <ul>
268
   *   <li>The root node contains at least one child node having a leaf.</li>
269
   *   <li>There are no leaf nodes with sibling leaf nodes.</li>
270
   * </ul>
271
   *
272
   * @return {@code null} if the document is well-formed, otherwise the
273
   * problematic child {@link TreeItem}.
274
   */
275
  public Optional<TreeItem<String>> isTreeWellFormed() {
276
    final var root = getTreeView().getRoot();
277
278
    for( final var child : root.getChildren() ) {
279
      final var problemChild = isWellFormed( child );
280
281
      if( child.isLeaf() || problemChild != null ) {
282
        return Optional.ofNullable( problemChild );
283
      }
284
    }
285
286
    return Optional.empty();
287
  }
288
289
  /**
290
   * Determines whether the document is well-formed by ensuring that
291
   * child branches do not contain multiple leaves.
292
   *
293
   * @param item The subtree to check for well-formedness.
294
   * @return {@code null} when the tree is well-formed, otherwise the
295
   * problematic {@link TreeItem}.
296
   */
297
  private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
298
    int childLeafs = 0;
299
    int childBranches = 0;
300
301
    for( final var child : item.getChildren() ) {
302
      if( child.isLeaf() ) {
303
        childLeafs++;
304
      }
305
      else {
306
        childBranches++;
307
      }
308
309
      final var problemChild = isWellFormed( child );
310
311
      if( problemChild != null ) {
312
        return problemChild;
313
      }
314
    }
315
316
    return ((childBranches > 0 && childLeafs == 0) ||
317
            (childBranches == 0 && childLeafs <= 1))
318
      ? null
319
      : item;
320
  }
321
322
  @Override
323
  public DefinitionTreeItem<String> findLeafExact( final String text ) {
324
    return getTreeRoot().findLeafExact( text );
325
  }
326
327
  @Override
328
  public DefinitionTreeItem<String> findLeafContains( final String text ) {
329
    return getTreeRoot().findLeafContains( text );
330
  }
331
332
  @Override
333
  public DefinitionTreeItem<String> findLeafContainsNoCase(
334
    final String text ) {
335
    return getTreeRoot().findLeafContainsNoCase( text );
336
  }
337
338
  @Override
339
  public DefinitionTreeItem<String> findLeafStartsWith( final String text ) {
340
    return getTreeRoot().findLeafStartsWith( text );
341
  }
342
343
  public void select( final TreeItem<String> item ) {
344
    getSelectionModel().clearSelection();
345
    getSelectionModel().select( getTreeView().getRow( item ) );
346
  }
347
348
  /**
349
   * Collapses the tree, recursively.
350
   */
351
  public void collapse() {
352
    collapse( getTreeRoot().getChildren() );
353
  }
354
355
  /**
356
   * Collapses the tree, recursively.
357
   *
358
   * @param <T>   The type of tree item to expand (usually String).
359
   * @param nodes The nodes to collapse.
360
   */
361
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
362
    for( final var node : nodes ) {
363
      node.setExpanded( false );
364
      collapse( node.getChildren() );
365
    }
366
  }
367
368
  /**
369
   * @return {@code true} when the user is editing a {@link TreeItem}.
370
   */
371
  private boolean isEditingTreeItem() {
372
    return getTreeView().editingItemProperty().getValue() != null;
373
  }
374
375
  /**
376
   * Changes to edit mode for the selected item.
377
   */
378
  @Override
379
  public void renameDefinition() {
380
    getTreeView().edit( getSelectedItem() );
381
  }
382
383
  /**
384
   * Removes all selected items from the {@link TreeView}.
385
   */
386
  @Override
387
  public void deleteDefinitions() {
388
    for( final var item : getSelectedItems() ) {
389
      final var parent = item.getParent();
390
391
      if( parent != null ) {
392
        parent.getChildren().remove( item );
393
      }
394
    }
395
  }
396
397
  /**
398
   * Deletes the selected item.
399
   */
400
  private void deleteSelectedItem() {
401
    final var c = getSelectedItem();
402
    getSiblings( c ).remove( c );
403
  }
404
405
  private void insertSelectedItem() {
406
    if( getSelectedItem() instanceof DefinitionTreeItem<String> node ) {
407
      if( node.isLeaf() ) {
408
        InsertDefinitionEvent.fire( node );
409
      }
410
    }
411
  }
412
413
  /**
414
   * Adds a new item under the selected item (or root if nothing is selected).
415
   * There are a few conditions to consider: when adding to the root,
416
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
417
   * root must contain two items: a key and a value.
418
   */
419
  @Override
420
  public void createDefinition() {
421
    final var value = createDefinitionTreeItem();
422
    getSelectedItem().getChildren().add( value );
423
    expand( value );
424
    select( value );
425
  }
426
427
  private ContextMenu createContextMenu() {
428
    final var menu = new ContextMenu();
429
    final var items = menu.getItems();
430
431
    addMenuItem( items, ACTION_PREFIX + "definition.create.text" )
432
      .setOnAction( _ -> createDefinition() );
433
    addMenuItem( items, ACTION_PREFIX + "definition.rename.text" )
434
      .setOnAction( _ -> renameDefinition() );
435
    addMenuItem( items, ACTION_PREFIX + "definition.delete.text" )
436
      .setOnAction( _ -> deleteSelectedItem() );
437
    addMenuItem( items, ACTION_PREFIX + "definition.insert.text" )
438
      .setOnAction( _ -> insertSelectedItem() );
439
440
    return menu;
441
  }
442
443
  /**
444
   * Executes hot-keys for edits to the definition tree.
445
   *
446
   * @param event Contains the key code of the key that was pressed.
447
   */
448
  private void keyEventFilter( final KeyEvent event ) {
449
    if( !isEditingTreeItem() ) {
450
      switch( event.getCode() ) {
451
        case ENTER -> {
452
          expand( getSelectedItem() );
453
          event.consume();
454
        }
455
456
        case DELETE -> deleteDefinitions();
457
        case INSERT -> createDefinition();
458
459
        case R -> {
460
          if( event.isControlDown() ) {
461
            renameDefinition();
462
          }
463
        }
464
465
        default -> {}
466
      }
467
468
      for( final var handler : getKeyEventHandlers() ) {
469
        handler.handle( event );
470
      }
471
    }
472
  }
473
474
  /**
475
   * Called when the editor's input focus changes. This will fire an event
476
   * for subscribers.
477
   *
478
   * @param ignored Not used.
479
   * @param o       The old input focus property value.
480
   * @param n       The new input focus property value.
481
   */
482
  private void focused(
483
    final ObservableValue<? extends Boolean> ignored,
484
    final Boolean o,
485
    final Boolean n ) {
486
    if( n != null && n ) {
487
      TextDefinitionFocusEvent.fire( this );
488
    }
489
  }
490
491
  /**
492
   * Adds a menu item to a list of menu items.
493
   *
494
   * @param items    The list of menu items to append to.
495
   * @param labelKey The resource bundle key name for the menu item's label.
496
   * @return The menu item added to the list of menu items.
497
   */
498
  private MenuItem addMenuItem(
499
    final List<MenuItem> items, final String labelKey ) {
500
    final MenuItem menuItem = createMenuItem( labelKey );
501
    items.add( menuItem );
502
    return menuItem;
503
  }
504
505
  private MenuItem createMenuItem( final String labelKey ) {
506
    return new MenuItem( get( labelKey ) );
507
  }
508
509
  /**
510
   * Creates a new {@link TreeItem} that is intended to be the root-level item
511
   * added to the {@link TreeView}. This allows the root item to be
512
   * distinguished from the other items so that reference keys do not include
513
   * "Definition" as part of their name.
514
   *
515
   * @return A new {@link TreeItem}, never {@code null}.
516
   */
517
  private RootTreeItem<String> createRootTreeItem() {
518
    return new RootTreeItem<>( get( "Pane.definition.node.root.title" ) );
519
  }
520
521
  private DefinitionTreeItem<String> createDefinitionTreeItem() {
522
    return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) );
523
  }
524
525
  @Override
526
  public void requestFocus() {
527
    getTreeView().requestFocus();
528
  }
529
530
  /**
531
   * Expands the node to the root, recursively.
532
   *
533
   * @param <T>  The type of tree item to expand (usually String).
534
   * @param node The node to expand.
535
   */
536
  @Override
537
  public <T> void expand( final TreeItem<T> node ) {
538
    if( node != null ) {
539
      expand( node.getParent() );
540
      node.setExpanded( !node.isLeaf() );
541
    }
542
  }
543
544
  /**
545
   * Answers whether there are any definitions in the tree.
546
   *
547
   * @return {@code true} when there are no definitions; {@code false} when
548
   * there's at least one definition.
549
   */
550
  @Override
551
  public boolean isEmpty() {
552
    return getTreeRoot().isEmpty();
553
  }
554
555
  /**
556
   * Returns the actively selected item in the tree.
557
   *
558
   * @return The selected item, or the tree root item if no item is selected.
559
   */
560
  public TreeItem<String> getSelectedItem() {
561
    final var item = getSelectionModel().getSelectedItem();
562
    return item == null ? getTreeRoot() : item;
563
  }
564
565
  /**
566
   * Returns the {@link TreeView} that contains the definition hierarchy.
567
   *
568
   * @return A non-null instance.
569
   */
570
  private TreeView<String> getTreeView() {
571
    return mTreeView;
572
  }
573
574
  /**
575
   * Returns the root of the tree.
576
   *
577
   * @return The first node added to the definition tree.
578
   */
579
  private DefinitionTreeItem<String> getTreeRoot() {
580
    return mTreeRoot;
581
  }
582
583
  private ObservableList<TreeItem<String>> getSiblings(
584
    final TreeItem<String> item ) {
585
    final var root = getTreeView().getRoot();
586
    final var parent = (item == null || item == root)
587
      ? root
588
      : item.getParent();
589
590
    return parent.getChildren();
591
  }
592
593
  private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
594
    return getTreeView().getSelectionModel();
595
  }
596
597
  /**
598
   * Returns a copy of all the selected items.
599
   *
600
   * @return A list, possibly empty, containing all selected items in the
601
   * {@link TreeView}.
602
   */
603
  private List<TreeItem<String>> getSelectedItems() {
604
    return new ArrayList<>( getSelectionModel().getSelectedItems() );
605
  }
606
607
  private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() {
608
    return mKeyEventHandlers;
609
  }
610
}
1611
A src/main/java/com/keenwrite/editors/definition/DefinitionTreeItem.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.editors.definition;
6
7
import com.keenwrite.util.Diacritics;
8
import javafx.scene.control.TreeItem;
9
10
import java.util.Stack;
11
import java.util.function.BiFunction;
12
13
/**
14
 * Provides behaviour afforded to definition keys and corresponding value.
15
 *
16
 * @param <T> The type of {@link TreeItem} (usually string).
17
 */
18
public class DefinitionTreeItem<T> extends TreeItem<T> {
19
20
  /**
21
   * Constructs a new item with a default value.
22
   *
23
   * @param value Passed up to superclass.
24
   */
25
  public DefinitionTreeItem( final T value ) {
26
    super( value );
27
  }
28
29
  /**
30
   * Finds a leaf starting at the current node with text that matches the given
31
   * value. Search is performed case-sensitively.
32
   *
33
   * @param text The text to match against each leaf in the tree.
34
   * @return The leaf that has a value exactly matching the given text.
35
   */
36
  public DefinitionTreeItem<T> findLeafExact( final String text ) {
37
    return findLeaf( text, DefinitionTreeItem::valueEquals );
38
  }
39
40
  /**
41
   * Finds a leaf starting at the current node with text that matches the given
42
   * value. Search is performed case-sensitively.
43
   *
44
   * @param text The text to match against each leaf in the tree.
45
   * @return The leaf that has a value that contains the given text.
46
   */
47
  public DefinitionTreeItem<T> findLeafContains( final String text ) {
48
    return findLeaf( text, DefinitionTreeItem::valueContains );
49
  }
50
51
  /**
52
   * Finds a leaf starting at the current node with text that matches the given
53
   * value. Search is performed case-insensitively.
54
   *
55
   * @param text The text to match against each leaf in the tree.
56
   * @return The leaf that has a value that contains the given text.
57
   */
58
  public DefinitionTreeItem<T> findLeafContainsNoCase( final String text ) {
59
    return findLeaf( text, DefinitionTreeItem::valueContainsNoCase );
60
  }
61
62
  /**
63
   * Finds a leaf starting at the current node with text that matches the given
64
   * value. Search is performed case-sensitively.
65
   *
66
   * @param text The text to match against each leaf in the tree.
67
   * @return The leaf that has a value that starts with the given text.
68
   */
69
  public DefinitionTreeItem<T> findLeafStartsWith( final String text ) {
70
    return findLeaf( text, DefinitionTreeItem::valueStartsWith );
71
  }
72
73
  /**
74
   * Finds a leaf starting at the current node with text that matches the given
75
   * value.
76
   *
77
   * @param text     The text to match against each leaf in the tree.
78
   * @param findMode What algorithm is used to match the given text.
79
   * @return The leaf that has a value starting with the given text, or {@code
80
   * null} if there was no match found.
81
   */
82
  @SuppressWarnings( "AssignmentUsedAsCondition" )
83
  public DefinitionTreeItem<T> findLeaf(
84
    final String text,
85
    final BiFunction<DefinitionTreeItem<T>, String, Boolean> findMode ) {
86
    final var stack = new Stack<DefinitionTreeItem<T>>();
87
    stack.push( this );
88
89
    // Don't hunt for blank (empty) keys.
90
    boolean found = text.isBlank();
91
92
    while( !found && !stack.isEmpty() ) {
93
      final var node = stack.pop();
94
95
      for( final var child : node.getChildren() ) {
96
        final var result = (DefinitionTreeItem<T>) child;
97
98
        if( result.isLeaf() ) {
99
          if( found = findMode.apply( result, text ) ) {
100
            return result;
101
          }
102
        }
103
        else {
104
          stack.push( result );
105
        }
106
      }
107
    }
108
109
    return null;
110
  }
111
112
  /**
113
   * Returns true if this node is a leaf and its value equals the given text.
114
   *
115
   * @param s The text to compare against the node value.
116
   * @return true Node is a leaf and its value equals the given value.
117
   */
118
  private boolean valueEquals( final String s ) {
119
    return isLeaf() && getValue().equals( s );
120
  }
121
122
  /**
123
   * Removes diacritic characters from the given definition item.
124
   *
125
   * @param item The {@link DefinitionTreeItem} to strip of diacritics.
126
   * @param <T>  The type of item contained by {@link DefinitionTreeItem}s.
127
   * @return The given item, without any accented characters.
128
   */
129
  private static <T> String removeAccents( final DefinitionTreeItem<T> item ) {
130
    return Diacritics.remove( item.getValue().toString() );
131
  }
132
133
  /**
134
   * Returns true if this node is a leaf and its value contains the given text.
135
   *
136
   * @param s The text to compare against the node value.
137
   * @return true Node is a leaf and its value contains the given value.
138
   */
139
  private boolean valueContains( final String s ) {
140
    return isLeaf() && removeAccents( this ).contains( s );
141
  }
142
143
  /**
144
   * Returns true if this node is a leaf and its value contains the given text.
145
   *
146
   * @param s The text to compare against the node value.
147
   * @return true Node is a leaf and its value contains the given value.
148
   */
149
  private boolean valueContainsNoCase( final String s ) {
150
    return isLeaf() && removeAccents( this )
151
      .toLowerCase()
152
      .contains( s.toLowerCase() );
153
  }
154
155
  /**
156
   * Returns true if this node is a leaf and its value starts with the given
157
   * text.
158
   *
159
   * @param s The text to compare against the node value.
160
   * @return true Node is a leaf and its value starts with the given value.
161
   */
162
  private boolean valueStartsWith( final String s ) {
163
    return isLeaf() && removeAccents( this ).startsWith( s );
164
  }
165
166
  /**
167
   * Returns the path for this node, with nodes made distinct using the
168
   * separator character. This uses two loops: one for pushing nodes onto a
169
   * stack and one for popping them off to create the path in desired order.
170
   *
171
   * @return A non-null string, possibly empty.
172
   */
173
  public String toPath() {
174
    return TreeItemMapper.toPath( getParent() );
175
  }
176
177
  /**
178
   * Answers whether there are any definitions in this tree.
179
   *
180
   * @return {@code true} when there are no definitions in the tree; {@code
181
   * false} when there is at least one definition present.
182
   */
183
  public boolean isEmpty() {
184
    return getChildren().isEmpty();
185
  }
186
}
1187
A src/main/java/com/keenwrite/editors/definition/RootTreeItem.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.editors.definition;
6
7
import javafx.scene.control.TreeItem;
8
import javafx.scene.control.TreeView;
9
10
/**
11
 * Marker interface for top-most {@link TreeItem}. This class allows the
12
 * {@link TreeItemMapper} to ignore the topmost definition. Such contortions
13
 * are necessary because {@link TreeView} requires a root item that isn't part
14
 * of the user's definition file.
15
 * <p>
16
 * Another approach would be to associate object pairs per {@link TreeItem},
17
 * but that would be a waste of memory since the only "exception" case is
18
 * the root {@link TreeItem}.
19
 * </p>
20
 *
21
 * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}.
22
 */
23
public final class RootTreeItem<T> extends DefinitionTreeItem<T> {
24
  /**
25
   * Default constructor, calls the superclass, no other behaviour.
26
   *
27
   * @param value The {@link TreeItem} node name to construct the superclass.
28
   * @see TreeItemMapper#convert(TreeItem) for details on how this
29
   * class is used.
30
   */
31
  public RootTreeItem( final T value ) {
32
    super( value );
33
  }
34
}
135
A src/main/java/com/keenwrite/editors/definition/TreeItemMapper.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.editors.definition;
6
7
import com.fasterxml.jackson.databind.JsonNode;
8
import javafx.scene.control.TreeItem;
9
10
import java.util.HashMap;
11
import java.util.Iterator;
12
import java.util.Map;
13
import java.util.Stack;
14
15
/**
16
 * Given a {@link TreeItem}, this will generate a flat map with all the
17
 * keys using a dot-separated notation to represent the tree's hierarchy.
18
 *
19
 * <ol>
20
 *   <li>Load YAML file into {@link JsonNode} hierarchy.</li>
21
 *   <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li>
22
 *   <li>Convert the {@link TreeItem} hierarchy into a flat map.</li>
23
 * </ol>
24
 */
25
public final class TreeItemMapper {
26
  /**
27
   * Key name hierarchy separator (i.e., the dots in {@code root.node.var}).
28
   */
29
  public static final String SEPARATOR = ".";
30
31
  /**
32
   * Default buffer length for key names that should be large enough to
33
   * avoid reallocating memory to increase the {@link StringBuilder}'s
34
   * buffer.
35
   */
36
  public static final int DEFAULT_KEY_LENGTH = 64;
37
38
  /**
39
   * In-order traversal of a {@link TreeItem} hierarchy, exposing each item
40
   * as a consecutive list.
41
   */
42
  private static final class TreeIterator
43
    implements Iterator<TreeItem<String>> {
44
    private final Stack<TreeItem<String>> mStack = new Stack<>();
45
46
    public TreeIterator( final TreeItem<String> root ) {
47
      if( root != null ) {
48
        mStack.push( root );
49
      }
50
    }
51
52
    @Override
53
    public boolean hasNext() {
54
      return !mStack.isEmpty();
55
    }
56
57
    @Override
58
    public TreeItem<String> next() {
59
      final var next = mStack.pop();
60
      next.getChildren().forEach( mStack::push );
61
62
      return next;
63
    }
64
  }
65
66
  /**
67
   * Iterate over a given root node (at any level of the tree) and process each
68
   * leaf node into a flat map.
69
   *
70
   * @param root The topmost item in the tree.
71
   */
72
  public static Map<String, String> convert( final TreeItem<String> root ) {
73
    final var map = new HashMap<String, String>();
74
75
    new TreeIterator( root ).forEachRemaining( item -> {
76
      if( item.isLeaf() && item.getParent() != null ) {
77
        map.put( toPath( item.getParent() ), item.getValue() );
78
      }
79
    } );
80
81
    return map;
82
  }
83
84
  /**
85
   * For a given node, this will ascend the tree to generate a key name
86
   * that is associated with the leaf node's value.
87
   *
88
   * @param node Ascendants represent the key to this node's value.
89
   * @param <T>  Data type that the {@link TreeItem} contains.
90
   * @return The string representation of the node's unique key.
91
   */
92
  public static <T> String toPath( TreeItem<T> node ) {
93
    final var key = new StringBuilder( DEFAULT_KEY_LENGTH );
94
    final var stack = new Stack<TreeItem<T>>();
95
96
    while( node != null && !(node instanceof RootTreeItem) ) {
97
      stack.push( node );
98
      node = node.getParent();
99
    }
100
101
    // Gets set at end of first iteration (to avoid an if condition).
102
    var separator = "";
103
104
    while( !stack.empty() ) {
105
      final T subkey = stack.pop().getValue();
106
      key.append( separator );
107
      key.append( subkey );
108
      separator = SEPARATOR;
109
    }
110
111
    return key.toString();
112
  }
113
}
1114
A src/main/java/com/keenwrite/editors/definition/TreeTransformer.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.editors.definition;
6
7
import javafx.scene.control.TreeItem;
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-2021 White Magic Software, Ltd. -- All rights reserved. */
2
3
/**
4
 * This package contains classes that pertain to hierarchical, structured
5
 * data formats, which can be used as interpolated variables.
6
 */
7
package com.keenwrite.editors.definition;
18
A src/main/java/com/keenwrite/editors/definition/yaml/YamlTreeTransformer.java
1
/* Copyright 2020-2021 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.databind.node.ObjectNode;
7
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
8
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
9
import com.keenwrite.editors.definition.DefinitionTreeItem;
10
import com.keenwrite.editors.definition.TreeTransformer;
11
import javafx.scene.control.TreeItem;
12
import javafx.scene.control.TreeView;
13
14
import java.util.Map.Entry;
15
16
import static com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.*;
17
import static com.keenwrite.events.StatusEvent.clue;
18
19
/**
20
 * Transforms a {@link JsonNode} hierarchy into a tree that can be displayed
21
 * in a user interface and vice-versa.
22
 */
23
public final class YamlTreeTransformer implements TreeTransformer {
24
  private static final YAMLFactory sFactory;
25
  private static final YAMLMapper sMapper;
26
27
  static {
28
    sFactory = new YAMLFactory();
29
    sFactory.configure( MINIMIZE_QUOTES, true );
30
    sFactory.configure( SPLIT_LINES, false );
31
    sMapper = new YAMLMapper( sFactory );
32
  }
33
34
  /**
35
   * Constructs a new instance that will use the given path to read the object
36
   * hierarchy from a data source.
37
   */
38
  public YamlTreeTransformer() {
39
  }
40
41
  @Override
42
  public String transform( final TreeItem<String> treeItem ) {
43
    try {
44
      final var root = sMapper.createObjectNode();
45
46
      // Iterate over the root item's children. The root item is used by the
47
      // application to ensure definitions can always be added to a tree, as
48
      // such it is not meant to be exported, only its children.
49
      for( final var child : treeItem.getChildren() ) {
50
        transform( child, root );
51
      }
52
53
      return sMapper.writeValueAsString( root );
54
    } catch( final Exception ex ) {
55
      clue( ex );
56
      throw new RuntimeException( ex );
57
    }
58
  }
59
60
  /**
61
   * Converts a YAML document to a {@link TreeItem} based on the document
62
   * keys.
63
   *
64
   * @param document The YAML document to convert to a hierarchy of
65
   *                 {@link TreeItem} instances.
66
   * @throws StackOverflowError If infinite recursion is encountered.
67
   */
68
  @Override
69
  public TreeItem<String> transform( final String document ) {
70
    final var jsonNode = toJson( document );
71
    final var rootItem = createTreeItem( "root" );
72
73
    transform( jsonNode, rootItem );
74
75
    return rootItem;
76
  }
77
78
  private JsonNode toJson( final String yaml ) {
79
    try {
80
      return new ObjectMapper( sFactory ).readTree( yaml );
81
    } catch( final Exception ex ) {
82
      // Ensure that a document root node exists.
83
      return new ObjectMapper().createObjectNode();
84
    }
85
  }
86
87
  /**
88
   * Recursive method to generate an object hierarchy that represents the
89
   * given {@link TreeItem} hierarchy.
90
   *
91
   * @param item The {@link TreeItem} to reproduce as an object hierarchy.
92
   * @param node The {@link ObjectNode} to update to reflect the
93
   *             {@link TreeItem} hierarchy.
94
   */
95
  private void transform( final TreeItem<String> item, ObjectNode node ) {
96
    final var children = item.getChildren();
97
98
    // If the current item has more than one non-leaf child, it's an
99
    // object node and must become a new nested object.
100
    if( !(children.size() == 1 && children.getFirst().isLeaf()) ) {
101
      node = node.putObject( item.getValue() );
102
    }
103
104
    for( final var child : children ) {
105
      if( child.isLeaf() ) {
106
        node.put( item.getValue(), child.getValue() );
107
      }
108
      else {
109
        transform( child, node );
110
      }
111
    }
112
  }
113
114
  /**
115
   * Iterate over a given root node (at any level of the tree) and adapt each
116
   * leaf node.
117
   *
118
   * @param node A JSON node (YAML node) to adapt.
119
   * @param item The tree item to use as the root when processing the node.
120
   * @throws StackOverflowError If infinite recursion is encountered.
121
   */
122
  private void transform( final JsonNode node, final TreeItem<String> item ) {
123
    final var fields = node.properties().iterator();
124
    fields.forEachRemaining( leaf -> transform( leaf, item ) );
125
  }
126
127
  /**
128
   * Recursively adapt each rootNode to a corresponding rootItem.
129
   *
130
   * @param node The node to adapt.
131
   * @param item The item to adapt using the node's key.
132
   * @throws StackOverflowError If infinite recursion is encountered.
133
   */
134
  private void transform(
135
    final Entry<String, JsonNode> node, final TreeItem<String> item ) {
136
    final var leafNode = node.getValue();
137
    final var key = node.getKey();
138
    final var leaf = createTreeItem( key );
139
140
    if( leafNode.isValueNode() ) {
141
      leaf.getChildren().add( createTreeItem( node.getValue().asText() ) );
142
    }
143
144
    item.getChildren().add( leaf );
145
146
    if( leafNode.isObject() ) {
147
      transform( leafNode, leaf );
148
    }
149
  }
150
151
  /**
152
   * Creates a new {@link TreeItem} that can be added to the {@link TreeView}.
153
   *
154
   * @param value The node's value.
155
   * @return A new {@link TreeItem}, never {@code null}.
156
   */
157
  private TreeItem<String> createTreeItem( final String value ) {
158
    return new DefinitionTreeItem<>( value );
159
  }
160
}
1161
A src/main/java/com/keenwrite/editors/definition/yaml/package-info.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
3
/**
4
 * This package contains classes that can parse YAML documents into a GUI
5
 * representation.
6
 */
7
package com.keenwrite.editors.definition.yaml;
18
A src/main/java/com/keenwrite/editors/markdown/LinkVisitor.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.editors.markdown;
6
7
import com.vladsch.flexmark.ast.Link;
8
import com.vladsch.flexmark.util.ast.Node;
9
import com.vladsch.flexmark.util.ast.NodeVisitor;
10
import com.vladsch.flexmark.util.ast.VisitHandler;
11
12
/**
13
 * Responsible for extracting a hyperlink from the document so that the user
14
 * can edit the link within a dialog.
15
 */
16
public final class LinkVisitor {
17
18
  private NodeVisitor mVisitor;
19
  private Link mLink;
20
  private final int mOffset;
21
22
  /**
23
   * Creates a hyperlink given an offset into a paragraph and the Markdown AST
24
   * link node.
25
   *
26
   * @param index Index into the paragraph that indicates the hyperlink to
27
   *              change.
28
   */
29
  public LinkVisitor( final int index ) {
30
    mOffset = index;
31
  }
32
33
  public Link process( final Node root ) {
34
    getVisitor().visit( root );
35
    return getLink();
36
  }
37
38
  /**
39
   * @param link Not null.
40
   */
41
  private void visit( final Link link ) {
42
    final int began = link.getStartOffset();
43
    final int ended = link.getEndOffset();
44
    final int index = getOffset();
45
46
    if( index >= began && index <= ended ) {
47
      setLink( link );
48
    }
49
  }
50
51
  private synchronized NodeVisitor getVisitor() {
52
    if( mVisitor == null ) {
53
      mVisitor = createVisitor();
54
    }
55
56
    return mVisitor;
57
  }
58
59
  protected NodeVisitor createVisitor() {
60
    return new NodeVisitor(
61
      new VisitHandler<>( Link.class, LinkVisitor.this::visit ) );
62
  }
63
64
  private Link getLink() {
65
    return mLink;
66
  }
67
68
  private void setLink( final Link link ) {
69
    mLink = link;
70
  }
71
72
  public int getOffset() {
73
    return mOffset;
74
  }
75
}
176
A src/main/java/com/keenwrite/editors/markdown/MarkdownEditor.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.editors.markdown;
6
7
import com.keenwrite.constants.Constants;
8
import com.keenwrite.editors.TextEditor;
9
import com.keenwrite.editors.common.Caret;
10
import com.keenwrite.events.TextEditorFocusEvent;
11
import com.keenwrite.io.MediaType;
12
import com.keenwrite.preferences.LocaleProperty;
13
import com.keenwrite.preferences.Workspace;
14
import com.keenwrite.processors.markdown.extensions.caret.CaretExtension;
15
import javafx.beans.binding.Bindings;
16
import javafx.beans.property.*;
17
import javafx.beans.value.ChangeListener;
18
import javafx.event.Event;
19
import javafx.scene.Node;
20
import javafx.scene.control.IndexRange;
21
import javafx.scene.input.KeyEvent;
22
import javafx.scene.layout.BorderPane;
23
import org.fxmisc.flowless.VirtualizedScrollPane;
24
import org.fxmisc.richtext.StyleClassedTextArea;
25
import org.fxmisc.richtext.model.StyleSpans;
26
import org.fxmisc.undo.UndoManager;
27
import org.fxmisc.wellbehaved.event.EventPattern;
28
import org.fxmisc.wellbehaved.event.Nodes;
29
30
import java.io.File;
31
import java.nio.charset.Charset;
32
import java.text.BreakIterator;
33
import java.text.MessageFormat;
34
import java.util.*;
35
import java.util.function.Consumer;
36
import java.util.function.Supplier;
37
import java.util.regex.Pattern;
38
39
import static com.keenwrite.GuiApp.keyDown;
40
import static com.keenwrite.constants.Constants.*;
41
import static com.keenwrite.events.StatusEvent.clue;
42
import static com.keenwrite.io.MediaType.TEXT_MARKDOWN;
43
import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN;
44
import static com.keenwrite.preferences.AppKeys.*;
45
import static com.keenwrite.util.Strings.trimEnd;
46
import static com.keenwrite.util.Strings.trimStart;
47
import static java.lang.Character.isWhitespace;
48
import static java.lang.String.format;
49
import static java.util.Collections.singletonList;
50
import static javafx.application.Platform.runLater;
51
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.ALWAYS;
52
import static javafx.scene.input.KeyCode.*;
53
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
54
import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
55
import static org.fxmisc.richtext.Caret.CaretVisibility.ON;
56
import static org.fxmisc.richtext.model.StyleSpans.singleton;
57
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
58
import static org.fxmisc.wellbehaved.event.InputMap.consume;
59
60
/**
61
 * Responsible for editing Markdown documents.
62
 */
63
public final class MarkdownEditor extends BorderPane implements TextEditor {
64
  /**
65
   * Regular expression that matches the type of markup block. This is used
66
   * when Enter is pressed to continue the block environment.
67
   */
68
  private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile(
69
    "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
70
71
  private final Workspace mWorkspace;
72
73
  /**
74
   * The text editor.
75
   */
76
  private final StyleClassedTextArea mTextArea =
77
    new StyleClassedTextArea( false );
78
79
  /**
80
   * Wraps the text editor in scrollbars.
81
   */
82
  private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
83
    new VirtualizedScrollPane<>( mTextArea );
84
85
  /**
86
   * Tracks where the caret is located in this document. This offers observable
87
   * properties for caret position changes.
88
   */
89
  private final Caret mCaret = createCaret( mTextArea );
90
91
  /**
92
   * File being edited by this editor instance.
93
   */
94
  private File mFile;
95
96
  /**
97
   * Set to {@code true} upon text or caret position changes. Value is {@code
98
   * false} by default.
99
   */
100
  private final BooleanProperty mDirty = new SimpleBooleanProperty();
101
102
  /**
103
   * Opened file's character encoding, or {@link Constants#DEFAULT_CHARSET} if
104
   * either no encoding could be determined or this is a new (empty) file.
105
   */
106
  private final Charset mEncoding;
107
108
  /**
109
   * Tracks whether the in-memory definitions have changed with respect to the
110
   * persisted definitions.
111
   */
112
  private final BooleanProperty mModified = new SimpleBooleanProperty();
113
114
  public MarkdownEditor( final File file, final Workspace workspace ) {
115
    mEncoding = open( mFile = file );
116
    mWorkspace = workspace;
117
118
    initTextArea( mTextArea );
119
    initStyle( mTextArea );
120
    initScrollPane( mScrollPane );
121
    initHotKeys();
122
    initUndoManager();
123
  }
124
125
  @SuppressWarnings( "unused" )
126
  private void initTextArea( final StyleClassedTextArea textArea ) {
127
    textArea.setShowCaret( ON );
128
    textArea.setWrapText( true );
129
    textArea.requestFollowCaret();
130
    textArea.moveTo( 0 );
131
132
    textArea.textProperty().addListener( ( c, o, n ) -> {
133
      // Fire, regardless of whether the caret position has changed.
134
      mDirty.set( false );
135
136
      // Prevent the subsequent caret position change from raising dirty bits.
137
      mDirty.set( true );
138
    } );
139
140
    textArea.caretPositionProperty().addListener( ( c, o, n ) -> {
141
      // Fire when the caret position has changed and the text has not.
142
      mDirty.set( true );
143
      mDirty.set( false );
144
    } );
145
146
    textArea.focusedProperty().addListener( ( c, o, n ) -> {
147
      if( n != null && n ) {
148
        TextEditorFocusEvent.fire( this );
149
      }
150
    } );
151
  }
152
153
  @SuppressWarnings( "unused" )
154
  private void initStyle( final StyleClassedTextArea textArea ) {
155
    textArea.getStyleClass().add( "markdown" );
156
157
    final var stylesheets = textArea.getStylesheets();
158
    stylesheets.add( getStylesheetPath( getLocale() ) );
159
160
    localeProperty().addListener( ( c, o, n ) -> {
161
      if( n != null ) {
162
        stylesheets.clear();
163
        stylesheets.add( getStylesheetPath( getLocale() ) );
164
      }
165
    } );
166
167
    fontNameProperty().addListener(
168
      ( c, o, n ) ->
169
        setFont( mTextArea, getFontName(), getFontSize() )
170
    );
171
172
    fontSizeProperty().addListener(
173
      ( c, o, n ) ->
174
        setFont( mTextArea, getFontName(), getFontSize() )
175
    );
176
177
    setFont( mTextArea, getFontName(), getFontSize() );
178
  }
179
180
  private void initScrollPane(
181
    final VirtualizedScrollPane<StyleClassedTextArea> scrollpane ) {
182
    scrollpane.setVbarPolicy( ALWAYS );
183
    setCenter( scrollpane );
184
  }
185
186
  private void initHotKeys() {
187
    addEventListener( keyPressed( ENTER ), this::onEnterPressed );
188
    addEventListener( keyPressed( X, CONTROL_DOWN ), this::cut );
189
    addEventListener( keyPressed( TAB ), this::tab );
190
    addEventListener( keyPressed( TAB, SHIFT_DOWN ), this::untab );
191
  }
192
193
  private void initUndoManager() {
194
    final var undoManager = getUndoManager();
195
    final var markedPosition = undoManager.atMarkedPositionProperty();
196
197
    undoManager.forgetHistory();
198
    undoManager.mark();
199
    mModified.bind( Bindings.not( markedPosition ) );
200
  }
201
202
  @Override
203
  public void moveTo( final int offset ) {
204
    assert 0 <= offset && offset <= mTextArea.getLength();
205
206
    if( offset <= mTextArea.getLength() ) {
207
      mTextArea.moveTo( offset );
208
      mTextArea.requestFollowCaret();
209
    }
210
  }
211
212
  /**
213
   * Delegate the focus request to the text area itself.
214
   */
215
  @Override
216
  public void requestFocus() {
217
    mTextArea.requestFocus();
218
  }
219
220
  @Override
221
  public void setText( final String text ) {
222
    mTextArea.clear();
223
    mTextArea.appendText( text );
224
    mTextArea.getUndoManager().mark();
225
  }
226
227
  @Override
228
  public String getText() {
229
    return mTextArea.getText();
230
  }
231
232
  @Override
233
  public Charset getEncoding() {
234
    return mEncoding;
235
  }
236
237
  @Override
238
  public File getFile() {
239
    return mFile;
240
  }
241
242
  @Override
243
  public void rename( final File file ) {
244
    mFile = file;
245
  }
246
247
  @Override
248
  public void undo() {
249
    final var manager = getUndoManager();
250
    xxdo( manager::isUndoAvailable, manager::undo, "Main.status.error.undo" );
251
  }
252
253
  @Override
254
  public void redo() {
255
    final var manager = getUndoManager();
256
    xxdo( manager::isRedoAvailable, manager::redo, "Main.status.error.redo" );
257
  }
258
259
  /**
260
   * Performs an undo or redo action, if possible, otherwise displays an error
261
   * message to the user.
262
   *
263
   * @param ready  Answers whether the action can be executed.
264
   * @param action The action to execute.
265
   * @param key    The informational message key having a value to display if
266
   *               the {@link Supplier} is not ready.
267
   */
268
  private void xxdo(
269
    final Supplier<Boolean> ready, final Runnable action, final String key ) {
270
    if( ready.get() ) {
271
      action.run();
272
    }
273
    else {
274
      clue( key );
275
    }
276
  }
277
278
  @Override
279
  public void cut() {
280
    final var selected = mTextArea.getSelectedText();
281
282
    // Emulate selecting the current line by firing Home then Shift+Down Arrow.
283
    if( selected == null || selected.isEmpty() ) {
284
      // Note: mTextArea.selectLine() does not select empty lines.
285
      mTextArea.fireEvent( keyDown( HOME, false ) );
286
      mTextArea.fireEvent( keyDown( DOWN, true ) );
287
    }
288
289
    mTextArea.cut();
290
  }
291
292
  @Override
293
  public void copy() {
294
    mTextArea.copy();
295
  }
296
297
  @Override
298
  public void paste() {
299
    mTextArea.paste();
300
  }
301
302
  @Override
303
  public void selectAll() {
304
    mTextArea.selectAll();
305
  }
306
307
  @Override
308
  public void bold() {
309
    enwrap( "**" );
310
  }
311
312
  @Override
313
  public void italic() {
314
    enwrap( "*" );
315
  }
316
317
  @Override
318
  public void monospace() {
319
    enwrap( "`" );
320
  }
321
322
  @Override
323
  public void superscript() {
324
    enwrap( "^" );
325
  }
326
327
  @Override
328
  public void subscript() {
329
    enwrap( "~" );
330
  }
331
332
  @Override
333
  public void strikethrough() {
334
    enwrap( "~~" );
335
  }
336
337
  @Override
338
  public void blockquote() {
339
    block( "> " );
340
  }
341
342
  @Override
343
  public void code() {
344
    enwrap( "`" );
345
  }
346
347
  @Override
348
  public void fencedCodeBlock() {
349
    enwrap( "\n\n```\n", "\n```\n\n" );
350
  }
351
352
  @Override
353
  public void heading( final int level ) {
354
    final var hashes = new String( new char[ level ] ).replace( "\0", "#" );
355
    block( format( "%s ", hashes ) );
356
  }
357
358
  @Override
359
  public void unorderedList() {
360
    block( "* " );
361
  }
362
363
  @Override
364
  public void orderedList() {
365
    block( "1. " );
366
  }
367
368
  @Override
369
  public void horizontalRule() {
370
    block( format( "---%n%n" ) );
371
  }
372
373
  @Override
374
  public Node getNode() {
375
    return this;
376
  }
377
378
  @Override
379
  public ReadOnlyBooleanProperty modifiedProperty() {
380
    return mModified;
381
  }
382
383
  @Override
384
  public void clearModifiedProperty() {
385
    getUndoManager().mark();
386
  }
387
388
  @Override
389
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
390
    return mScrollPane;
391
  }
392
393
  @Override
394
  public StyleClassedTextArea getTextArea() {
395
    return mTextArea;
396
  }
397
398
  private final Map<String, IndexRange> mStyles = new HashMap<>();
399
400
  @Override
401
  public void stylize( final IndexRange range, final String style ) {
402
    final var began = range.getStart();
403
    final var ended = range.getEnd() + 1;
404
405
    assert 0 <= began && began <= ended;
406
    assert style != null;
407
408
    // TODO: Ensure spell check and find highlights can coexist.
409
//    final var spans = mTextArea.getStyleSpans( range );
410
//    System.out.println( "SPANS: " + spans );
411
412
//    final var spans = mTextArea.getStyleSpans( range );
413
//    mTextArea.setStyleSpans( began, merge( spans, range.getLength(), style
414
//    ) );
415
416
//    final var builder = new StyleSpansBuilder<Collection<String>>();
417
//    builder.add( singleton( style ), range.getLength() + 1 );
418
//    mTextArea.setStyleSpans( began, builder.create() );
419
420
//    final var s = mTextArea.getStyleSpans( began, ended );
421
//    System.out.println( "STYLES: " +s );
422
423
    mStyles.put( style, range );
424
    mTextArea.setStyleClass( began, ended, style );
425
426
    // Ensure that whenever the user interacts with the text that the found
427
    // word will have its highlighting removed. The handler removes itself.
428
    // This won't remove the highlighting if the caret position moves by mouse.
429
    final var handler = mTextArea.getOnKeyPressed();
430
    mTextArea.setOnKeyPressed( event -> {
431
      mTextArea.setOnKeyPressed( handler );
432
      unstylize( style );
433
    } );
434
435
    //mTextArea.setStyleSpans(began, ended, s);
436
  }
437
438
  private static StyleSpans<Collection<String>> merge(
439
    StyleSpans<Collection<String>> spans, int len, String style ) {
440
    spans = spans.overlay(
441
      singleton( singletonList( style ), len ),
442
      ( bottomSpan, list ) -> {
443
        final List<String> l =
444
          new ArrayList<>( bottomSpan.size() + list.size() );
445
        l.addAll( bottomSpan );
446
        l.addAll( list );
447
        return l;
448
      } );
449
450
    return spans;
451
  }
452
453
  @Override
454
  public void unstylize( final String style ) {
455
    final var indexes = mStyles.remove( style );
456
    if( indexes != null ) {
457
      mTextArea.clearStyle( indexes.getStart(), indexes.getEnd() + 1 );
458
    }
459
  }
460
461
  @Override
462
  public Caret getCaret() {
463
    return mCaret;
464
  }
465
466
  /**
467
   * A {@link Caret} instance is not directly coupled ot the GUI because
468
   * document processing does not always require interactive status bar
469
   * updates. This can happen when processing from the command-line. However,
470
   * the processors need the {@link Caret} instance to inject the caret
471
   * position into the document. Making the {@link CaretExtension} optional
472
   * would require more effort than using a {@link Caret} model that is
473
   * decoupled from GUI widgets.
474
   *
475
   * @param editor The text editor containing caret position information.
476
   * @return An instance of {@link Caret} that tracks the GUI caret position.
477
   */
478
  private Caret createCaret( final StyleClassedTextArea editor ) {
479
    return Caret
480
      .builder()
481
      .with( Caret.Mutator::setParagraph,
482
             () -> editor.currentParagraphProperty().getValue() )
483
      .with( Caret.Mutator::setParagraphs,
484
             () -> editor.getParagraphs().size() )
485
      .with( Caret.Mutator::setParaOffset,
486
             () -> editor.caretColumnProperty().getValue() )
487
      .with( Caret.Mutator::setTextOffset,
488
             () -> editor.caretPositionProperty().getValue() )
489
      .with( Caret.Mutator::setTextLength,
490
             () -> editor.lengthProperty().getValue() )
491
      .build();
492
  }
493
494
  /**
495
   * This method adds listeners to editor events.
496
   *
497
   * @param <T>      The event type.
498
   * @param <U>      The consumer type for the given event type.
499
   * @param event    The event of interest.
500
   * @param consumer The method to call when the event happens.
501
   */
502
  public <T extends Event, U extends T> void addEventListener(
503
    final EventPattern<? super T, ? extends U> event,
504
    final Consumer<? super U> consumer ) {
505
    Nodes.addInputMap( mTextArea, consume( event, consumer ) );
506
  }
507
508
  private void onEnterPressed( final KeyEvent ignored ) {
509
    final var currentLine = getCaretParagraph();
510
    final var matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
511
512
    // By default, insert a new line by itself.
513
    String newText = NEWLINE;
514
515
    // If the pattern was matched then determine what block type to continue.
516
    if( matcher.matches() ) {
517
      if( matcher.group( 2 ).isEmpty() ) {
518
        final var pos = mTextArea.getCaretPosition();
519
        mTextArea.selectRange( pos - currentLine.length(), pos );
520
      }
521
      else {
522
        // Indent the new line with the same whitespace characters and
523
        // list markers as current line. This ensures that the indentation
524
        // is propagated.
525
        newText = newText.concat( matcher.group( 1 ) );
526
      }
527
    }
528
529
    mTextArea.replaceSelection( newText );
530
    mTextArea.requestFollowCaret();
531
  }
532
533
  private void cut( final KeyEvent event ) {
534
    cut();
535
  }
536
537
  private void tab( final KeyEvent event ) {
538
    final var range = mTextArea.selectionProperty().getValue();
539
    final var sb = new StringBuilder( 1024 );
540
541
    if( range.getLength() > 0 ) {
542
      final var selection = mTextArea.getSelectedText();
543
544
      selection.lines().forEach(
545
        l -> sb.append( "\t" ).append( l ).append( NEWLINE )
546
      );
547
    }
548
    else {
549
      sb.append( "\t" );
550
    }
551
552
    mTextArea.replaceSelection( sb.toString() );
553
  }
554
555
  private void untab( final KeyEvent event ) {
556
    final var range = mTextArea.selectionProperty().getValue();
557
558
    if( range.getLength() > 0 ) {
559
      final var selection = mTextArea.getSelectedText();
560
      final var sb = new StringBuilder( selection.length() );
561
562
      selection.lines().forEach(
563
        l -> sb.append( l.startsWith( "\t" ) ? l.substring( 1 ) : l )
564
               .append( NEWLINE )
565
      );
566
567
      mTextArea.replaceSelection( sb.toString() );
568
    }
569
    else {
570
      final var p = getCaretParagraph();
571
572
      if( p.startsWith( "\t" ) ) {
573
        mTextArea.selectParagraph();
574
        mTextArea.replaceSelection( p.substring( 1 ) );
575
      }
576
    }
577
  }
578
579
  /**
580
   * Observers may listen for changes to the property returned from this method
581
   * to receive notifications when either the text or caret have changed. This
582
   * should not be used to track whether the text has been modified.
583
   */
584
  public void addDirtyListener( ChangeListener<Boolean> listener ) {
585
    mDirty.addListener( listener );
586
  }
587
588
  /**
589
   * Surrounds the selected text or word under the caret in Markdown markup.
590
   *
591
   * @param token The beginning and ending token for enclosing the text.
592
   */
593
  private void enwrap( final String token ) {
594
    enwrap( token, token );
595
  }
596
597
  /**
598
   * Surrounds the selected text or word under the caret in Markdown markup.
599
   *
600
   * @param began The beginning token for enclosing the text.
601
   * @param ended The ending token for enclosing the text.
602
   */
603
  private void enwrap( final String began, String ended ) {
604
    // Ensure selected text takes precedence over the word at caret position.
605
    final var selected = mTextArea.selectionProperty().getValue();
606
    final var range = selected.getLength() == 0
607
      ? getCaretWord()
608
      : selected;
609
    String text = mTextArea.getText( range );
610
611
    int length = range.getLength();
612
    text = trimStart( text );
613
    final int beganIndex = range.getStart() + length - text.length();
614
615
    length = text.length();
616
    text = trimEnd( text );
617
    final int endedIndex = range.getEnd() - (length - text.length());
618
619
    mTextArea.replaceText( beganIndex, endedIndex, began + text + ended );
620
  }
621
622
  /**
623
   * Inserts the given block-level markup at the current caret position
624
   * within the document. This will prepend two blank lines to ensure that
625
   * the block element begins at the start of a new line.
626
   *
627
   * @param markup The text to insert at the caret.
628
   */
629
  private void block( final String markup ) {
630
    final int pos = mTextArea.getCaretPosition();
631
    mTextArea.insertText( pos, format( "%n%n%s", markup ) );
632
  }
633
634
  /**
635
   * Returns the caret position within the current paragraph.
636
   *
637
   * @return A value from 0 to the length of the current paragraph.
638
   */
639
  private int getCaretColumn() {
640
    return mTextArea.getCaretColumn();
641
  }
642
643
  @Override
644
  public IndexRange getCaretWord() {
645
    final var paragraph = getCaretParagraph()
646
      .replaceAll( "---", "   " )
647
      .replaceAll( "--", "  " )
648
      .replaceAll( "[\\[\\]{}()]", " " );
649
    final var length = paragraph.length();
650
    final var column = getCaretColumn();
651
652
    var began = column;
653
    var ended = column;
654
655
    while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
656
      began--;
657
    }
658
659
    while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
660
      ended++;
661
    }
662
663
    final var iterator = BreakIterator.getWordInstance();
664
    iterator.setText( paragraph );
665
666
    while( began < length && iterator.isBoundary( began + 1 ) ) {
667
      began++;
668
    }
669
670
    while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
671
      ended--;
672
    }
673
674
    final var offset = getCaretDocumentOffset( column );
675
676
    return IndexRange.normalize( began + offset, ended + offset );
677
  }
678
679
  private int getCaretDocumentOffset( final int column ) {
680
    return mTextArea.getCaretPosition() - column;
681
  }
682
683
  /**
684
   * Returns the index of the paragraph where the caret resides.
685
   *
686
   * @return A number greater than or equal to 0.
687
   */
688
  private int getCurrentParagraph() {
689
    return mTextArea.getCurrentParagraph();
690
  }
691
692
  /**
693
   * Returns the text for the paragraph that contains the caret.
694
   *
695
   * @return A non-null string, possibly empty.
696
   */
697
  private String getCaretParagraph() {
698
    return getText( getCurrentParagraph() );
699
  }
700
701
  @Override
702
  public String getText( final int paragraph ) {
703
    return mTextArea.getText( paragraph );
704
  }
705
706
  @Override
707
  public String getText( final IndexRange indexes )
708
    throws IndexOutOfBoundsException {
709
    return mTextArea.getText( indexes.getStart(), indexes.getEnd() );
710
  }
711
712
  @Override
713
  public void replaceText( final IndexRange indexes, final String s ) {
714
    mTextArea.replaceText( indexes, s );
715
  }
716
717
  private UndoManager<?> getUndoManager() {
718
    return mTextArea.getUndoManager();
719
  }
720
721
  /**
722
   * Returns the path to a {@link Locale}-specific stylesheet.
723
   *
724
   * @return A non-null string to inject into the HTML document head.
725
   */
726
  private static String getStylesheetPath( final Locale locale ) {
727
    return MessageFormat.format(
728
      sSettings.getSetting( STYLESHEET_MARKDOWN_LOCALE, "" ),
729
      locale.getLanguage(),
730
      locale.getScript(),
731
      locale.getCountry()
732
    );
733
  }
734
735
  private Locale getLocale() {
736
    return localeProperty().toLocale();
737
  }
738
739
  private LocaleProperty localeProperty() {
740
    return mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
741
  }
742
743
  /**
744
   * Sets the font family name and font size at the same time. When the
745
   * workspace is loaded, the default font values are changed, which results
746
   * in this method being called.
747
   *
748
   * @param area   Change the font settings for this text area.
749
   * @param name   New font family name to apply.
750
   * @param points New font size to apply (in points, not pixels).
751
   */
752
  private void setFont(
753
    final StyleClassedTextArea area, final String name, final double points ) {
754
    runLater( () -> area.setStyle(
755
      format(
756
        "-fx-font-family:'%s';-fx-font-size:%spx;", name, toPixels( points )
757
      )
758
    ) );
759
  }
760
761
  private String getFontName() {
762
    return fontNameProperty().get();
763
  }
764
765
  private StringProperty fontNameProperty() {
766
    return mWorkspace.stringProperty( KEY_UI_FONT_EDITOR_NAME );
767
  }
768
769
  private double getFontSize() {
770
    return fontSizeProperty().get();
771
  }
772
773
  private DoubleProperty fontSizeProperty() {
774
    return mWorkspace.doubleProperty( KEY_UI_FONT_EDITOR_SIZE );
775
  }
776
777
  /**
778
   * Answers whether the given resource is of compatible {@link MediaType}s.
779
   *
780
   * @param mediaType The {@link MediaType} to compare.
781
   * @return {@code true} if the given {@link MediaType} is suitable for
782
   * editing with this type of editor.
783
   */
784
  @Override
785
  public boolean supports( final MediaType mediaType ) {
786
    return isMediaType( mediaType ) ||
787
           mediaType == TEXT_MARKDOWN ||
788
           mediaType == TEXT_R_MARKDOWN;
789
  }
790
}
1791
A src/main/java/com/keenwrite/events/AppEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
import static com.keenwrite.events.Bus.post;
8
9
/**
10
 * Marker interface for all application events.
11
 */
12
public interface AppEvent {
13
14
  /**
15
   * Submits this event to the {@link Bus}.
16
   */
17
  default void publish() {
18
    post( this );
19
  }
20
}
121
A src/main/java/com/keenwrite/events/Bus.java
1
/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
import org.greenrobot.eventbus.EventBus;
8
9
import static org.greenrobot.eventbus.EventBus.builder;
10
11
/**
12
 * Responsible for delegating interactions to the event bus library. This
13
 * class decouples the rest of the application from a particular event bus
14
 * implementation.
15
 */
16
public class Bus {
17
  private static final EventBus sEventBus = builder()
18
    .logNoSubscriberMessages( false )
19
    .installDefaultEventBus();
20
21
  public static <Subscriber> void register( final Subscriber subscriber ) {
22
    sEventBus.register( subscriber );
23
  }
24
25
  public static <Subscriber> void unregister( final Subscriber subscriber ) {
26
    sEventBus.unregister( subscriber );
27
  }
28
29
  public static <Event> void post( final Event event ) {
30
    sEventBus.post( event );
31
  }
32
}
133
A src/main/java/com/keenwrite/events/CaretMovedEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
import com.keenwrite.editors.common.Caret;
8
9
/**
10
 * Responsible for notifying when the caret has moved, which includes giving
11
 * focus to a different editor.
12
 */
13
public class CaretMovedEvent implements AppEvent {
14
  private final Caret mCaret;
15
16
  private CaretMovedEvent( final Caret caret ) {
17
    assert caret != null;
18
    mCaret = caret;
19
  }
20
21
  public static void fire( final Caret caret ) {
22
    new CaretMovedEvent( caret ).publish();
23
  }
24
25
  public Caret getCaret() {
26
    return mCaret;
27
  }
28
}
129
A src/main/java/com/keenwrite/events/CaretNavigationEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
import com.keenwrite.ui.outline.DocumentOutline;
8
9
/**
10
 * Collates information about a caret event, which is typically triggered when
11
 * the user double-clicks in the {@link DocumentOutline}. This is an imperative
12
 * event, meaning that the position of the caret will be changed after this
13
 * event is handled. As opposed to a {@link CaretMovedEvent}, which provides
14
 * information about the caret after it has been moved.
15
 */
16
public class CaretNavigationEvent implements AppEvent {
17
  /**
18
   * Absolute document offset.
19
   */
20
  private final int mOffset;
21
22
  private CaretNavigationEvent( final int offset ) {
23
    mOffset = offset;
24
  }
25
26
  /**
27
   * Publishes an event that requests moving the caret to the given offset.
28
   *
29
   * @param offset Move the caret to this document offset.
30
   */
31
  public static void fire( final int offset ) {
32
    new CaretNavigationEvent( offset ).publish();
33
  }
34
35
  public int getOffset() {
36
    return mOffset;
37
  }
38
}
139
A src/main/java/com/keenwrite/events/DocumentChangedEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
/**
8
 * Collates information about an HTML document that has changed.
9
 */
10
public class DocumentChangedEvent implements AppEvent {
11
  private final String mText;
12
13
  /**
14
   * Hash document (as plain text) so subscribers are notified upon changes.
15
   */
16
  private static int sHash;
17
18
  /**
19
   * Creates an event with the new plain text document, having all variables
20
   * substituted and all markup removed.
21
   *
22
   * @param text The document text that has changed since the last time this
23
   *             type of event was fired.
24
   */
25
  private DocumentChangedEvent( final String text ) {
26
    mText = text;
27
  }
28
29
  /**
30
   * When the given document may have changed. This will only fire a change
31
   * event if the given document has changed from the last time this
32
   * event was fired. The document is first converted to plain text before
33
   * the comparison is made.
34
   *
35
   * @param html The document that may have changed.
36
   */
37
  public static void fire( final String html ) {
38
    // Hashing the document text ignores caret position changes.
39
    final var hash = html.hashCode();
40
41
    if( hash != sHash ) {
42
      sHash = hash;
43
      new DocumentChangedEvent( html ).publish();
44
    }
45
  }
46
47
  /**
48
   * Returns the text that has changed.
49
   *
50
   * @return The new document text.
51
   */
52
  public String getDocument() {
53
    return mText;
54
  }
55
56
  /**
57
   * Returns the document.
58
   *
59
   * @return The value from {@link #getDocument()}.
60
   */
61
  @Override
62
  public String toString() {
63
    return getDocument();
64
  }
65
}
166
A src/main/java/com/keenwrite/events/ExportFailedEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
/**
8
 * Responsible for kicking off an alert message when exporting (e.g., to PDF)
9
 * fails. This can happen when the executable to typeset the document cannot
10
 * be found.
11
 */
12
public class ExportFailedEvent implements AppEvent {
13
  public static void fire() {
14
    new ExportFailedEvent().publish();
15
  }
16
}
117
A src/main/java/com/keenwrite/events/FileOpenEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
import java.net.URI;
8
9
/**
10
 * Collates information about a file requested to be opened. This can be called
11
 * when the user clicks a hyperlink in HTML preview panel.
12
 */
13
public class FileOpenEvent implements AppEvent {
14
  private final URI mUri;
15
16
  private FileOpenEvent( final URI uri ) {
17
    assert uri != null;
18
    mUri = uri;
19
  }
20
21
  /**
22
   * Fires a new file open event using the given {@link URI} instance.
23
   *
24
   * @param uri The instance of {@link URI} to open as a file in a text editor.
25
   */
26
  public static void fire( final URI uri ) {
27
    new FileOpenEvent( uri ).publish();
28
  }
29
30
  /**
31
   * Returns the requested file name to be opened.
32
   *
33
   * @return A file reference that can be opened in a text editor.
34
   */
35
  public URI getUri() {
36
    return mUri;
37
  }
38
}
139
A src/main/java/com/keenwrite/events/FocusEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
/**
8
 * Collates information about an object that has gained focus. This is typically
9
 * used by text resource editors (such as text editors and definition editors).
10
 */
11
public class FocusEvent<T> implements AppEvent {
12
  private final T mNode;
13
14
  protected FocusEvent( final T node ) {
15
    mNode = node;
16
  }
17
18
  /**
19
   * This method is used to help update the UI whenever a component has gained
20
   * input focus.
21
   *
22
   * @return The object that has gained focus.
23
   */
24
  public T get() {
25
    return mNode;
26
  }
27
}
128
A src/main/java/com/keenwrite/events/HyperlinkOpenEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
import java.io.IOException;
8
import java.net.URI;
9
10
import static com.keenwrite.events.StatusEvent.clue;
11
12
/**
13
 * Collates information about a URL requested to be opened.
14
 */
15
public class HyperlinkOpenEvent implements AppEvent {
16
  private final URI mUri;
17
18
  private HyperlinkOpenEvent( final URI uri ) {
19
    mUri = uri;
20
  }
21
22
  /**
23
   * Requests to open the default browser at the given location.
24
   *
25
   * @param uri The location to open.
26
   */
27
  public static void fire( final URI uri )
28
    throws IOException {
29
    new HyperlinkOpenEvent( uri ).publish();
30
  }
31
32
  /**
33
   * Requests to open the default browser at the given location.
34
   *
35
   * @param uri The location to open.
36
   */
37
  public static void fire( final String uri ) {
38
    try {
39
      fire( new URI( uri ) );
40
    } catch( final Exception ex ) {
41
      clue( ex );
42
    }
43
  }
44
45
  /**
46
   * Returns the requested resource to be opened.
47
   *
48
   * @return A reference that can be opened in a web browser.
49
   */
50
  public URI getUri() {
51
    return mUri;
52
  }
53
}
154
A src/main/java/com/keenwrite/events/InsertDefinitionEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
import com.keenwrite.editors.definition.DefinitionTreeItem;
8
9
/**
10
 * Collates information about a request to insert a reference to a
11
 * definition value into the active document.
12
 */
13
public class InsertDefinitionEvent<T> implements AppEvent {
14
15
  private final DefinitionTreeItem<T> mLeaf;
16
17
  private InsertDefinitionEvent( final DefinitionTreeItem<T> leaf ) {
18
    mLeaf = leaf;
19
  }
20
21
  public static <T> void fire( final DefinitionTreeItem<T> leaf ) {
22
    assert leaf != null;
23
    assert leaf.isLeaf();
24
25
    new InsertDefinitionEvent<>( leaf ).publish();
26
  }
27
28
  /**
29
   * Returns the {@link DefinitionTreeItem} that is to be inserted into the
30
   * active document.
31
   *
32
   * @return The item to insert (as a variable).
33
   */
34
  public DefinitionTreeItem<T> getLeaf() {
35
    return mLeaf;
36
  }
37
}
138
A src/main/java/com/keenwrite/events/ParseHeadingEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
import com.keenwrite.processors.Processor;
8
9
/**
10
 * Collates information about a document heading that has been parsed, after
11
 * all pertinent {@link Processor}s applied.
12
 */
13
public class ParseHeadingEvent implements AppEvent {
14
  private static final int NEW_OUTLINE_LEVEL = 0;
15
16
  /**
17
   * The heading text, which may be {@code null} upon creating a new outline.
18
   */
19
  private final String mText;
20
21
  /**
22
   * The heading level, which will be set to {@link #NEW_OUTLINE_LEVEL} if this
23
   * event indicates that the existing outline should be cleared anew.
24
   */
25
  private final int mLevel;
26
27
  /**
28
   * Offset into the text where the heading is found.
29
   */
30
  private final int mOffset;
31
32
  private ParseHeadingEvent(
33
    final int level, final String text, final int offset ) {
34
    mText = text;
35
    mLevel = level;
36
    mOffset = offset;
37
  }
38
39
  /**
40
   * Call to indicate a new outline is to be created.
41
   */
42
  public static void fireNewOutlineEvent() {
43
    new ParseHeadingEvent( NEW_OUTLINE_LEVEL, "Document", 0 ).publish();
44
  }
45
46
  /**
47
   * Call to indicate that a new heading must be added to the document outline.
48
   *
49
   * @param text   The heading text (parsed and processed).
50
   * @param level  A value between 1 and 6.
51
   * @param offset Absolute offset into document where heading is found.
52
   */
53
  public static void fire(
54
    final int level, final String text, final int offset ) {
55
    assert text != null;
56
    assert 1 <= level && level <= 6;
57
    assert 0 <= offset;
58
    new ParseHeadingEvent( level, text, offset ).publish();
59
  }
60
61
  public boolean isNewOutline() {
62
    return getLevel() == NEW_OUTLINE_LEVEL;
63
  }
64
65
  public int getLevel() {
66
    return mLevel;
67
  }
68
69
  /**
70
   * Returns the text description for the heading.
71
   *
72
   * @return The post-parsed and processed heading text from the document.
73
   */
74
  public String getText() {
75
    return mText;
76
  }
77
78
  /**
79
   * Returns an offset into the document where the heading is found.
80
   *
81
   * @return A zero-based document offset.
82
   */
83
  public int getOffset() {
84
    return mOffset;
85
  }
86
87
  @Override
88
  public String toString() {
89
    return getText();
90
  }
91
}
192
A src/main/java/com/keenwrite/events/ScrollLockEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
import static java.awt.Toolkit.getDefaultToolkit;
8
import static java.awt.event.KeyEvent.VK_SCROLL_LOCK;
9
10
/**
11
 * Collates information about the scroll lock status.
12
 */
13
public class ScrollLockEvent implements AppEvent {
14
  private final boolean mLocked;
15
16
  private ScrollLockEvent( final boolean locked ) {
17
    mLocked = locked;
18
  }
19
20
  /**
21
   * Fires a scroll lock event provided that the scroll lock key is in the
22
   * off state.
23
   *
24
   * @param locked The new locked status.
25
   */
26
  public static void fireScrollLockEvent( final boolean locked ) {
27
    // If the scroll lock key is off, allow the status to change.
28
    if( !getScrollLockKeyStatus() ) {
29
      fire( locked );
30
    }
31
  }
32
33
  /**
34
   * Fires a scroll lock event based on the current status of the scroll
35
   * lock key.
36
   */
37
  public static void fireScrollLockEvent() {
38
    fire( getScrollLockKeyStatus() );
39
  }
40
41
  /**
42
   * Answers whether the synchronized scrolling should be locked in place
43
   * (i.e., prevent sync scrolling).
44
   *
45
   * @return {@code true} when the user has locked the scrollbar position.
46
   */
47
  public boolean isLocked() {
48
    return mLocked;
49
  }
50
51
  private static void fire( final boolean locked ) {
52
    new ScrollLockEvent( locked ).publish();
53
  }
54
55
  /**
56
   * Returns the state of the scroll lock key.
57
   *
58
   * @return {@code true} when the scroll lock key is in the on state.
59
   */
60
  private static boolean getScrollLockKeyStatus() {
61
    return getDefaultToolkit().getLockingKeyState( VK_SCROLL_LOCK );
62
  }
63
}
164
A src/main/java/com/keenwrite/events/StatusEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
import java.util.List;
8
9
import static com.keenwrite.Messages.get;
10
import static com.keenwrite.constants.Constants.NEWLINE;
11
import static com.keenwrite.constants.Constants.STATUS_BAR_OK;
12
import static com.keenwrite.util.Strings.sanitize;
13
import static java.lang.String.format;
14
import static java.lang.String.join;
15
import static java.util.Arrays.stream;
16
17
/**
18
 * Collates information about an application issue. The issues can be
19
 * exceptions, state problems, parsing errors, and so forth.
20
 */
21
public final class StatusEvent implements AppEvent {
22
  private static final String ENGLISHIFY =
23
    "(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])";
24
25
  /**
26
   * Detailed information about a problem.
27
   */
28
  private final String mMessage;
29
30
  /**
31
   * Provides stack trace information that isolates the cause.
32
   */
33
  private final Throwable mProblem;
34
35
  /**
36
   * Constructs a new event that contains a problem description to help the
37
   * user resolve an issue encountered while using the application.
38
   *
39
   * @param message The human-readable message, typically displayed on-screen.
40
   */
41
  public StatusEvent( final String message ) {
42
    this( message, null );
43
  }
44
45
  /**
46
   * Constructs a new event that contains information about an unexpected issue.
47
   *
48
   * @param problem The issue encountered by the software, never {@code null}.
49
   */
50
  public StatusEvent( final Throwable problem ) {
51
    this( problem.getMessage(), problem );
52
  }
53
54
  /**
55
   * @param message The human-readable message text.
56
   * @param problem May be {@code null} if no exception was thrown.
57
   */
58
  public StatusEvent( final String message, final Throwable problem ) {
59
    mMessage = sanitize( message );
60
    mProblem = problem;
61
  }
62
63
  /**
64
   * Returns the stack trace information for the issue encountered. This is
65
   * optional because usually a status message isn't an application error.
66
   *
67
   * @return Optional stack trace to pinpoint the problem area in the code.
68
   */
69
  public String getProblem() {
70
    // Arbitrary limit.
71
    final var sb = new StringBuilder( 1024 );
72
    final var trace = mProblem;
73
74
    if( trace != null ) {
75
      stream( trace.getStackTrace() )
76
        .limit( 150 )
77
        .toList()
78
        .forEach( e -> sb.append( e.toString() ).append( NEWLINE ) );
79
    }
80
81
    return sb.toString();
82
  }
83
84
  @Override
85
  public String toString() {
86
    // Not exactly sure how the message can be null, but it happened once!
87
    final var message = mMessage == null ? "UNKNOWN" : mMessage;
88
89
    return format(
90
      "%s%s%s",
91
      message,
92
      message.isBlank() ? "" : " ",
93
      mProblem == null ? "" : toEnglish( mProblem )
94
    );
95
  }
96
97
  /**
98
   * Separates the exception class name from TitleCase into lowercase,
99
   * space-separated words. This makes the exception look a little more like
100
   * English. Any {@link RuntimeException} instances passed into this method
101
   * will have the cause extracted, if possible.
102
   *
103
   * @param problem The exception that triggered the status event change.
104
   * @return A human-readable message with the exception name and the
105
   * exception's message.
106
   */
107
  private static String toEnglish( Throwable problem ) {
108
    assert problem != null;
109
110
    // Subclasses of RuntimeException must be subject to Englishification.
111
    if( problem.getClass().equals( RuntimeException.class ) ) {
112
      final var cause = problem.getCause();
113
      return cause == null ? problem.getMessage() : cause.getMessage();
114
    }
115
116
    final var className = problem.getClass().getSimpleName();
117
    final var words = join( " ", className.split( ENGLISHIFY ) );
118
    return format( "(%s: %s)", words.toLowerCase(), problem.getMessage() );
119
  }
120
121
  /**
122
   * Returns the message used to construct the event.
123
   *
124
   * @return The message for this event.
125
   */
126
  public String getMessage() {
127
    return mMessage;
128
  }
129
130
  /**
131
   * Resets the status bar to a default message. Indicates that there are no
132
   * issues to bring to the user's attention.
133
   */
134
  public static void clue() {
135
    fire( get( STATUS_BAR_OK, "OK" ) );
136
  }
137
138
  /**
139
   * Notifies listeners of a series of messages. This is useful when providing
140
   * users feedback of how third-party executables have failed.
141
   *
142
   * @param messages The lines of text to display.
143
   */
144
  public static void clue( final List<String> messages ) {
145
    messages.forEach( StatusEvent::fire );
146
  }
147
148
  /**
149
   * Notifies listeners of an error.
150
   *
151
   * @param key The message bundle key to look up.
152
   * @param t   The exception that caused the error.
153
   */
154
  public static void clue( final String key, final Throwable t ) {
155
    fire( get( key ), t );
156
  }
157
158
  /**
159
   * Notifies listeners of a custom message.
160
   *
161
   * @param key  The property key having a value to populate with arguments.
162
   * @param args The placeholder values to substitute into the key's value.
163
   */
164
  public static void clue( final String key, final Object... args ) {
165
    fire( get( key, args ) );
166
  }
167
168
  /**
169
   * Notifies listeners of a custom message.
170
   *
171
   * @param ex   The exception that warranted calling this method.
172
   * @param fmt  The string format specifier.
173
   * @param args The arguments to weave into the format specifier.
174
   */
175
  public static void clue(
176
    final Exception ex,
177
    final String fmt,
178
    final Object... args ) {
179
    final var msg = format( fmt, args );
180
    clue( msg, ex );
181
  }
182
183
  /**
184
   * Notifies listeners of an exception occurs that warrants the user's
185
   * attention.
186
   *
187
   * @param problem The exception with a message to display to the user.
188
   */
189
  public static void clue( final Throwable problem ) {
190
    fire( problem );
191
  }
192
193
  public static void fire( final String message ) {
194
    new StatusEvent( message ).publish();
195
  }
196
197
  private static void fire( final Throwable problem ) {
198
    new StatusEvent( problem ).publish();
199
  }
200
201
  private static void fire(
202
    final String message, final Throwable problem ) {
203
    new StatusEvent( message, problem ).publish();
204
  }
205
}
1206
A src/main/java/com/keenwrite/events/TextDefinitionFocusEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
import com.keenwrite.editors.TextDefinition;
8
9
public class TextDefinitionFocusEvent extends FocusEvent<TextDefinition> {
10
  protected TextDefinitionFocusEvent( final TextDefinition editor ) {
11
    super( editor );
12
  }
13
14
  /**
15
   * When the {@link TextDefinition} editor has focus, fire an event so that
16
   * subscribers may perform an action.
17
   *
18
   * @param editor The instance of editor that has gained input focus.
19
   */
20
  public static void fire( final TextDefinition editor ) {
21
    new TextDefinitionFocusEvent( editor ).publish();
22
  }
23
}
124
A src/main/java/com/keenwrite/events/TextEditorFocusEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
import com.keenwrite.editors.TextEditor;
8
9
/**
10
 * Collates information about the text editor that has gained focus.
11
 */
12
public class TextEditorFocusEvent extends FocusEvent<TextEditor> {
13
  protected TextEditorFocusEvent( final TextEditor editor ) {
14
    super( editor );
15
  }
16
17
  /**
18
   * When the {@link TextEditor} has focus, fire an event so that subscribers
19
   * may perform an action---such as parsing and rendering the contents.
20
   *
21
   * @param editor The instance of editor that has gained input focus.
22
   */
23
  public static void fire( final TextEditor editor ) {
24
    new TextEditorFocusEvent( editor ).publish();
25
  }
26
}
127
A src/main/java/com/keenwrite/events/WordCountEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events;
6
7
/**
8
 * Collates information about the word count changing.
9
 */
10
public class WordCountEvent implements AppEvent {
11
  /**
12
   * Number of words in the document.
13
   */
14
  private final int mCount;
15
16
  private WordCountEvent( final int count ) {
17
    mCount = count;
18
  }
19
20
  /**
21
   * Publishes an event that indicates the number of words in the document.
22
   *
23
   * @param count The approximate number of words in the document.
24
   */
25
  public static void fire( final int count ) {
26
    new WordCountEvent( count ).publish();
27
  }
28
29
  public int getCount() {
30
    return mCount;
31
  }
32
}
133
A src/main/java/com/keenwrite/events/spelling/LexiconEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events.spelling;
6
7
import com.keenwrite.events.AppEvent;
8
9
public abstract class LexiconEvent implements AppEvent {
10
}
111
A src/main/java/com/keenwrite/events/spelling/LexiconLoadedEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events.spelling;
6
7
import java.util.Map;
8
9
/**
10
 * Collates information about the lexicon. Fired when the lexicon has been
11
 * fully loaded into memory.
12
 */
13
public class LexiconLoadedEvent extends LexiconEvent {
14
15
  private final Map<String, Long> mLexicon;
16
17
  private LexiconLoadedEvent( final Map<String, Long> lexicon ) {
18
    mLexicon = lexicon;
19
  }
20
21
  public static void fire( final Map<String, Long> lexicon ) {
22
    new LexiconLoadedEvent( lexicon ).publish();
23
  }
24
25
  /**
26
   * Returns a word-frequency map used by the spell checking library.
27
   *
28
   * @return The lexicon that was loaded.
29
   */
30
  public Map<String, Long> getLexicon() {
31
    return mLexicon;
32
  }
33
}
134
A src/main/java/com/keenwrite/events/workspace/WorkspaceEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events.workspace;
6
7
import com.keenwrite.events.AppEvent;
8
9
/**
10
 * Superclass of all events related to the workspace.
11
 */
12
public abstract class WorkspaceEvent implements AppEvent {
13
}
114
A src/main/java/com/keenwrite/events/workspace/WorkspaceLoadedEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.events.workspace;
6
7
import com.keenwrite.preferences.Workspace;
8
9
/**
10
 * Indicates that the {@link Workspace} has been loaded.
11
 */
12
public class WorkspaceLoadedEvent extends WorkspaceEvent {
13
  private final Workspace mWorkspace;
14
15
  private WorkspaceLoadedEvent( final Workspace workspace ) {
16
    assert workspace != null;
17
18
    mWorkspace = workspace;
19
  }
20
21
  /**
22
   * Publishes an event that indicates a new {@link Workspace} has been loaded.
23
   */
24
  public static void fire( final Workspace workspace ) {
25
    new WorkspaceLoadedEvent( workspace ).publish();
26
  }
27
28
  /**
29
   * Returns a reference to the {@link Workspace} that was loaded.
30
   *
31
   * @return The {@link Workspace} that has loaded user preferences.
32
   */
33
  @SuppressWarnings( "unused" )
34
  private Workspace getWorkspace() {
35
    return mWorkspace;
36
  }
37
}
138
A src/main/java/com/keenwrite/exceptions/MissingFileException.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.exceptions;
6
7
import java.io.FileNotFoundException;
8
9
import static com.keenwrite.Messages.get;
10
11
/**
12
 * Responsible for informing the user when a file cannot be found.
13
 * This avoids duplicating the error message prefix.
14
 */
15
public final class MissingFileException extends FileNotFoundException {
16
  /**
17
   * Constructs a new {@link MissingFileException} using the given path.
18
   *
19
   * @param uri The path to the file resource that could not be found.
20
   */
21
  public MissingFileException( final String uri ) {
22
    super( get( "Main.status.error.file.missing", uri ) );
23
  }
24
}
125
A src/main/java/com/keenwrite/heuristics/package-info.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
3
/**
4
 * This package contains classes to help with word count. In logographic,
5
 * or other non-alphabetic languages, word tokenization cannot rely on
6
 * spaces. Instead, we need to employ a more sophisticated approach using
7
 * natural language parsing (NLP).
8
 */
9
package com.keenwrite.heuristics;
110
A src/main/java/com/keenwrite/io/CommandNotFoundException.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import java.io.FileNotFoundException;
5
6
/**
7
 * Indicates a command could not be found to run.
8
 */
9
public class CommandNotFoundException extends FileNotFoundException {
10
  /**
11
   * Creates a new exception indicating that the given command could not be
12
   * found (or executed).
13
   *
14
   * @param command The binary file's command name that could not be run.
15
   */
16
  public CommandNotFoundException( final String command ) {
17
    super( command );
18
  }
19
}
120
A src/main/java/com/keenwrite/io/FileEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import java.io.File;
5
import java.util.EventObject;
6
7
/**
8
 * Responsible for indicating that a file has been modified by the file system.
9
 */
10
public class FileEvent extends EventObject {
11
12
  /**
13
   * Constructs a new event that indicates the source of a file system event.
14
   *
15
   * @param file The {@link File} that has succumbed to a file system event.
16
   */
17
  public FileEvent( final File file ) {
18
    super( file );
19
  }
20
21
  /**
22
   * Returns the source as an instance of {@link File}.
23
   *
24
   * @return The {@link File} being watched.
25
   */
26
  public File getFile() {
27
    return (File) getSource();
28
  }
29
}
130
A src/main/java/com/keenwrite/io/FileModifiedListener.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import java.util.EventListener;
5
import java.util.function.Consumer;
6
7
/**
8
 * Responsible for informing listeners when a file has been modified.
9
 */
10
public interface FileModifiedListener
11
  extends EventListener, Consumer<FileEvent> {
12
}
113
A src/main/java/com/keenwrite/io/FileType.java
1
/* Copyright 2020-2021 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
  CSV( "csv" ),
17
  JSON( "json" ),
18
  TOML( "toml" ),
19
  YAML( "yaml" ),
20
  PROPERTIES( "properties" ),
21
  UNKNOWN( "unknown" );
22
23
  private final String mType;
24
25
  /**
26
   * Default constructor for enumerated file type.
27
   *
28
   * @param type Human-readable name for the file type.
29
   */
30
  FileType( final String type ) {
31
    mType = type;
32
  }
33
34
  /**
35
   * Returns the file type that corresponds to the given string.
36
   *
37
   * @param type The string to compare against this enumeration of file types.
38
   * @return The corresponding File Type for the given string.
39
   * @throws IllegalArgumentException Type not found.
40
   */
41
  public static FileType from( final String type ) {
42
    for( final FileType fileType : FileType.values() ) {
43
      if( fileType.isType( type ) ) {
44
        return fileType;
45
      }
46
    }
47
48
    throw new IllegalArgumentException( type );
49
  }
50
51
  /**
52
   * Answers whether this file type matches the given string, case-insensitive
53
   * comparison.
54
   *
55
   * @param type Presumably a file name extension to check against.
56
   * @return true The given extension corresponds to this enumerated type.
57
   */
58
  public boolean isType( final String type ) {
59
    return getType().equalsIgnoreCase( type );
60
  }
61
62
  /**
63
   * Returns the human-readable name for the file type.
64
   *
65
   * @return A non-null instance.
66
   */
67
  private String getType() {
68
    return mType;
69
  }
70
71
  /**
72
   * Returns the lowercase version of the file name extension.
73
   *
74
   * @return The file name, in lower case.
75
   */
76
  @Override
77
  public String toString() {
78
    return getType();
79
  }
80
}
181
A src/main/java/com/keenwrite/io/FileWatchService.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import org.renjin.repackaged.guava.collect.BiMap;
5
import org.renjin.repackaged.guava.collect.HashBiMap;
6
7
import java.io.File;
8
import java.io.IOException;
9
import java.nio.file.Path;
10
import java.nio.file.WatchKey;
11
import java.nio.file.WatchService;
12
import java.util.Set;
13
import java.util.concurrent.ConcurrentHashMap;
14
15
import static com.keenwrite.io.SysFile.toFile;
16
import static java.nio.file.FileSystems.getDefault;
17
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
18
import static java.util.Collections.newSetFromMap;
19
20
/**
21
 * Responsible for watching when a file has been changed.
22
 */
23
public class FileWatchService implements Runnable {
24
  /**
25
   * Set to {@code false} when {@link #stop()} is called.
26
   */
27
  private volatile boolean mRunning;
28
29
  /**
30
   * Contains the listeners to notify when a given file has changed.
31
   */
32
  private final Set<FileModifiedListener> mListeners =
33
    newSetFromMap( new ConcurrentHashMap<>() );
34
  private final WatchService mWatchService;
35
  private final BiMap<File, WatchKey> mWatched = HashBiMap.create();
36
37
  /**
38
   * Creates a new file system watch service with the given files to watch.
39
   *
40
   * @param files The files to watch for file system events.
41
   */
42
  public FileWatchService( final File... files ) {
43
    mWatchService = createWatchService();
44
45
    try {
46
      for( final var file : files ) {
47
        register( file );
48
      }
49
    } catch( final Exception ex ) {
50
      throw new RuntimeException( ex );
51
    }
52
  }
53
54
  /**
55
   * Runs the event handler until {@link #stop()} is called.
56
   *
57
   * @throws RuntimeException There was an error watching for file events.
58
   */
59
  @Override
60
  public void run() {
61
    mRunning = true;
62
63
    while( mRunning ) {
64
      handleEvents();
65
    }
66
  }
67
68
  private void handleEvents() {
69
    try {
70
      final var watchKey = mWatchService.take();
71
72
      for( final var pollEvent : watchKey.pollEvents() ) {
73
        final var watchable = (Path) watchKey.watchable();
74
        final var context = (Path) pollEvent.context();
75
        final var file = toFile( watchable.resolve( context ) );
76
77
        if( mWatched.containsKey( file ) ) {
78
          final var fileEvent = new FileEvent( file );
79
80
          for( final var listener : mListeners ) {
81
            listener.accept( fileEvent );
82
          }
83
        }
84
      }
85
86
      if( !watchKey.reset() ) {
87
        unregister( watchKey );
88
      }
89
    } catch( final Exception ex ) {
90
      throw new RuntimeException( ex );
91
    }
92
  }
93
94
  /**
95
   * Adds the given {@link File}'s containing directory to the watch list. When
96
   * the given {@link File} is modified, this service will receive a
97
   * notification that the containing directory has been modified, which will
98
   * then be filtered by file name.
99
   * <p>
100
   * This method is idempotent.
101
   * </p>
102
   *
103
   * @param file The {@link File} to watch for modification events.
104
   * @return The {@link File}'s directory watch state.
105
   * @throws IOException              Could not register the directory.
106
   * @throws IllegalArgumentException The {@link File} has no parent directory.
107
   */
108
  public WatchKey register( final File file ) throws IOException {
109
    if( mWatched.containsKey( file ) ) {
110
      return mWatched.get( file );
111
    }
112
113
    final var path = getParentDirectory( file );
114
    final var watchKey = path.register( mWatchService, ENTRY_MODIFY );
115
116
    return mWatched.put( file, watchKey );
117
  }
118
119
  /**
120
   * Removes the given {@link File}'s containing directory from the watch list.
121
   * <p>
122
   * This method is idempotent.
123
   * </p>
124
   *
125
   * @param file The {@link File} to no longer watch.
126
   * @throws IllegalArgumentException The {@link File} has no parent directory.
127
   */
128
  public void unregister( final File file ) {
129
    cancel( file );
130
    mWatched.remove( file );
131
  }
132
133
  /**
134
   * Cancels watching the given file for file system changes.
135
   *
136
   * @param file The {@link File} to watch for file events.
137
   */
138
  private void cancel( final File file ) {
139
    final var watchKey = mWatched.get( file );
140
141
    if( watchKey != null ) {
142
      watchKey.cancel();
143
    }
144
  }
145
146
  /**
147
   * Removes the given {@link WatchKey} from the registration map.
148
   *
149
   * @param watchKey The {@link WatchKey} to remove from the map.
150
   */
151
  private void unregister( final WatchKey watchKey ) {
152
    unregister( mWatched.inverse().get( watchKey ) );
153
  }
154
155
  /**
156
   * Adds a listener to be notified when a file under watch has been modified.
157
   * Listeners are backed by a set.
158
   *
159
   * @param listener The {@link FileModifiedListener} to add to the list.
160
   * @return {@code true} if this set did not already contain listener.
161
   */
162
  public boolean addListener( final FileModifiedListener listener ) {
163
    return mListeners.add( listener );
164
  }
165
166
  /**
167
   * Removes a listener from the notify list.
168
   *
169
   * @param listener The {@link FileModifiedListener} to remove.
170
   */
171
  public void removeListener( final FileModifiedListener listener ) {
172
    mListeners.remove( listener );
173
  }
174
175
  /**
176
   * Shuts down the file watch service and clears both watchers and listeners.
177
   *
178
   * @throws IOException Could not close the watch service.
179
   */
180
  public void stop() throws IOException {
181
    mRunning = false;
182
183
    for( final var file : mWatched.keySet() ) {
184
      cancel( file );
185
    }
186
187
    mWatched.clear();
188
    mListeners.clear();
189
    mWatchService.close();
190
  }
191
192
  /**
193
   * Returns the directory containing the given {@link File} instance.
194
   *
195
   * @param file The {@link File}'s containing directory to watch.
196
   * @return The {@link Path} to the {@link File}'s directory.
197
   * @throws IllegalArgumentException The {@link File} has no parent directory.
198
   */
199
  private Path getParentDirectory( final File file ) {
200
    assert file != null;
201
    assert !file.isDirectory();
202
203
    final var directory = file.getParentFile();
204
205
    if( directory == null ) {
206
      throw new IllegalArgumentException( file.getAbsolutePath() );
207
    }
208
209
    return directory.toPath();
210
  }
211
212
  private WatchService createWatchService() {
213
    try {
214
      return getDefault().newWatchService();
215
    } catch( final Exception ex ) {
216
      // Create a fallback that allows the class to be instantiated and used
217
      // without preventing the application from launching.
218
      return new PollingWatchService();
219
    }
220
  }
221
}
1222
A src/main/java/com/keenwrite/io/MediaType.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import java.io.File;
5
import java.io.IOException;
6
import java.net.URL;
7
import java.nio.file.Path;
8
9
import static com.keenwrite.io.MediaType.TypeName.*;
10
import static com.keenwrite.io.MediaTypeExtension.fromExtension;
11
import static com.keenwrite.io.SysFile.toFile;
12
import static org.apache.commons.io.FilenameUtils.getExtension;
13
14
/**
15
 * Defines various file formats and format contents.
16
 *
17
 * @see
18
 * <a href="https://www.iana.org/assignments/media-types/media-types.xhtml">IANA
19
 * Media Types</a>
20
 */
21
@SuppressWarnings( "SpellCheckingInspection" )
22
public enum MediaType {
23
  APP_DOCUMENT_OUTLINE( APPLICATION, "x-document-outline" ),
24
  APP_DOCUMENT_STATISTICS( APPLICATION, "x-document-statistics" ),
25
  APP_FILE_MANAGER( APPLICATION, "x-file-manager" ),
26
27
  APP_ACAD( APPLICATION, "acad" ),
28
  APP_JAVA_OBJECT( APPLICATION, "x-java-serialized-object" ),
29
  APP_JAVA( APPLICATION, "java" ),
30
  APP_PS( APPLICATION, "postscript" ),
31
  APP_EPS( APPLICATION, "eps" ),
32
  APP_PDF( APPLICATION, "pdf" ),
33
  APP_ZIP( APPLICATION, "zip" ),
34
  APP_XHTML( APPLICATION, "xhtml+xml" ),
35
36
  /*
37
   * Standard font types.
38
   */
39
  FONT_OTF( "otf" ),
40
  FONT_TTF( "ttf" ),
41
42
  /*
43
   * Standard image types.
44
   */
45
  IMAGE_APNG( "apng" ),
46
  IMAGE_ACES( "aces" ),
47
  IMAGE_AVCI( "avci" ),
48
  IMAGE_AVCS( "avcs" ),
49
  IMAGE_BMP( "bmp" ),
50
  IMAGE_CGM( "cgm" ),
51
  IMAGE_DICOM_RLE( "dicom_rle" ),
52
  IMAGE_EMF( "emf" ),
53
  IMAGE_EXAMPLE( "example" ),
54
  IMAGE_FITS( "fits" ),
55
  IMAGE_G3FAX( "g3fax" ),
56
  IMAGE_GIF( "gif" ),
57
  IMAGE_HEIC( "heic" ),
58
  IMAGE_HEIF( "heif" ),
59
  IMAGE_HEJ2K( "hej2k" ),
60
  IMAGE_HSJ2( "hsj2" ),
61
  IMAGE_X_ICON( "x-icon" ),
62
  IMAGE_JLS( "jls" ),
63
  IMAGE_JP2( "jp2" ),
64
  IMAGE_JPEG( "jpeg" ),
65
  IMAGE_JPH( "jph" ),
66
  IMAGE_JPHC( "jphc" ),
67
  IMAGE_JPM( "jpm" ),
68
  IMAGE_JPX( "jpx" ),
69
  IMAGE_JXR( "jxr" ),
70
  IMAGE_JXRA( "jxrA" ),
71
  IMAGE_JXRS( "jxrS" ),
72
  IMAGE_JXS( "jxs" ),
73
  IMAGE_JXSC( "jxsc" ),
74
  IMAGE_JXSI( "jxsi" ),
75
  IMAGE_JXSS( "jxss" ),
76
  IMAGE_KTX( "ktx" ),
77
  IMAGE_KTX2( "ktx2" ),
78
  IMAGE_NAPLPS( "naplps" ),
79
  IMAGE_PNG( "png" ),
80
  IMAGE_PHOTOSHOP( "photoshop" ),
81
  IMAGE_SVG_XML( "svg+xml" ),
82
  IMAGE_T38( "t38" ),
83
  IMAGE_TIFF( "tiff" ),
84
  IMAGE_WEBP( "webp" ),
85
  IMAGE_WMF( "wmf" ),
86
  IMAGE_X_BITMAP( "x-xbitmap" ),
87
  IMAGE_X_PIXMAP( "x-xpixmap" ),
88
89
  /*
90
   * Standard audio types.
91
   */
92
  AUDIO_SIMPLE( AUDIO, "basic" ),
93
  AUDIO_MP3( AUDIO, "mp3" ),
94
  AUDIO_WAV( AUDIO, "x-wav" ),
95
96
  /*
97
   * Standard video types.
98
   */
99
  VIDEO_MNG( VIDEO, "x-mng" ),
100
101
  /*
102
   * Document types for editing or displaying documents, mix of standard and
103
   * application-specific. The order that these are declared reflect in the
104
   * ordinal value used during comparisons.
105
   */
106
  TEXT_YAML( TEXT, "yaml" ),
107
  TEXT_PLAIN( TEXT, "plain" ),
108
  TEXT_MARKDOWN( TEXT, "markdown" ),
109
  TEXT_R_MARKDOWN( TEXT, "R+markdown" ),
110
  TEXT_PROPERTIES( TEXT, "x-java-properties" ),
111
  TEXT_HTML( TEXT, "html" ),
112
  TEXT_XML( TEXT, "xml" ),
113
114
  /*
115
   * When all other lights go out.
116
   */
117
  UNDEFINED( TypeName.UNDEFINED, "undefined" );
118
119
  /**
120
   * The IANA-defined types.
121
   */
122
  public enum TypeName {
123
    APPLICATION,
124
    AUDIO,
125
    IMAGE,
126
    TEXT,
127
    UNDEFINED,
128
    VIDEO
129
  }
130
131
  /**
132
   * The fully qualified IANA-defined media type.
133
   */
134
  private final String mMediaType;
135
136
  /**
137
   * The IANA-defined type name.
138
   */
139
  private final TypeName mTypeName;
140
141
  /**
142
   * The IANA-defined subtype name.
143
   */
144
  private final String mSubtype;
145
146
  /**
147
   * Constructs an instance using the default type name of "image".
148
   *
149
   * @param subtype The image subtype name.
150
   */
151
  MediaType( final String subtype ) {
152
    this( IMAGE, subtype );
153
  }
154
155
  /**
156
   * Constructs an instance using an IANA-defined type and subtype pair.
157
   *
158
   * @param typeName The media type's type name.
159
   * @param subtype  The media type's subtype name.
160
   */
161
  MediaType( final TypeName typeName, final String subtype ) {
162
    mTypeName = typeName;
163
    mSubtype = subtype;
164
    mMediaType = typeName.toString().toLowerCase() + '/' + subtype;
165
  }
166
167
  /**
168
   * Returns the {@link MediaType} associated with the given file.
169
   *
170
   * @param file Has a file name that may contain an extension associated with
171
   *             a known {@link MediaType}.
172
   * @return {@link MediaType#UNDEFINED} if the extension has not been
173
   * assigned, otherwise the {@link MediaType} associated with this
174
   * {@link File}'s file name extension.
175
   */
176
  public static MediaType fromFilename( final File file ) {
177
    assert file != null;
178
    return fromFilename( file.getName() );
179
  }
180
181
  /**
182
   * Returns the {@link MediaType} associated with the given file name.
183
   *
184
   * @param filename The file name that may contain an extension associated
185
   *                 with a known {@link MediaType}.
186
   * @return {@link MediaType#UNDEFINED} if the extension has not been
187
   * assigned, otherwise the {@link MediaType} associated with this
188
   * {@link URL}'s file name extension.
189
   */
190
  public static MediaType fromFilename( final String filename ) {
191
    assert filename != null;
192
    return fromExtension( getExtension( filename ) );
193
  }
194
195
  /**
196
   * Returns the {@link MediaType} associated with the path to a file.
197
   *
198
   * @param path Has a file name that may contain an extension associated with
199
   *             a known {@link MediaType}.
200
   * @return {@link MediaType#UNDEFINED} if the extension has not been
201
   * assigned, otherwise the {@link MediaType} associated with this
202
   * {@link File}'s file name extension.
203
   */
204
  public static MediaType fromFilename( final Path path ) {
205
    assert path != null;
206
    return fromFilename( path.toFile() );
207
  }
208
209
  /**
210
   * Determines the media type an IANA-defined, semi-colon-separated string.
211
   * This is often used after making an HTTP request to extract the type
212
   * and subtype from the content-type.
213
   *
214
   * @param header The content-type header value, may be {@code null}.
215
   * @return The data type for the resource or {@link MediaType#UNDEFINED} if
216
   * unmapped.
217
   */
218
  public static MediaType valueFrom( String header ) {
219
    if( header == null || header.isBlank() ) {
220
      return UNDEFINED;
221
    }
222
223
    // Trim off the character encoding.
224
    var i = header.indexOf( ';' );
225
    header = header.substring( 0, i == -1 ? header.length() : i );
226
227
    // Split the type and subtype.
228
    i = header.indexOf( '/' );
229
    i = i == -1 ? header.length() : i;
230
    final var type = header.substring( 0, i );
231
    final var subtype = header.substring( i + 1 );
232
233
    return valueFrom( type, subtype );
234
  }
235
236
  /**
237
   * Returns the {@link MediaType} for the given type and subtype names.
238
   *
239
   * @param type    The IANA-defined type name.
240
   * @param subtype The IANA-defined subtype name.
241
   * @return {@link MediaType#UNDEFINED} if there is no {@link MediaType} that
242
   * matches the given type and subtype names.
243
   */
244
  public static MediaType valueFrom(
245
    final String type, final String subtype ) {
246
    assert type != null;
247
    assert subtype != null;
248
249
    for( final var mediaType : values() ) {
250
      if( mediaType.equals( type, subtype ) ) {
251
        return mediaType;
252
      }
253
    }
254
255
    return UNDEFINED;
256
  }
257
258
  /**
259
   * Answers whether the given type and subtype names equal this enumerated
260
   * value. This performs a case-insensitive comparison.
261
   *
262
   * @param type    The type name to compare against this {@link MediaType}.
263
   * @param subtype The subtype name to compare against this {@link MediaType}.
264
   * @return {@code true} when the type and subtype name match.
265
   */
266
  public boolean equals( final String type, final String subtype ) {
267
    assert type != null;
268
    assert subtype != null;
269
270
    return mTypeName.name().equalsIgnoreCase( type ) &&
271
      mSubtype.equalsIgnoreCase( subtype );
272
  }
273
274
  /**
275
   * Answers whether the given {@link TypeName} matches this type name.
276
   *
277
   * @param typeName The {@link TypeName} to compare against the internal value.
278
   * @return {@code true} if the given value is the same IANA-defined type name.
279
   */
280
  @SuppressWarnings( "unused" )
281
  public boolean isType( final TypeName typeName ) {
282
    return mTypeName == typeName;
283
  }
284
285
  /**
286
   * Answers whether this instance is a scalable vector graphic.
287
   *
288
   * @return {@code true} if this instance represents an SVG object.
289
   */
290
  public boolean isSvg() {
291
    return equals( IMAGE_SVG_XML );
292
  }
293
294
  /**
295
   * Answers whether this instance is an image, vector or raster.
296
   *
297
   * @return {@code true} if this instance represents any type of image.
298
   */
299
  public boolean isImage() {
300
    return isType( IMAGE );
301
  }
302
303
  public boolean isUndefined() {
304
    return equals( UNDEFINED );
305
  }
306
307
  /**
308
   * Returns the IANA-defined subtype classification. Primarily used by
309
   * {@link MediaTypeExtension} to initialize associations where the subtype
310
   * name and the file name extension have a 1:1 mapping.
311
   *
312
   * @return The IANA subtype value.
313
   */
314
  public String getSubtype() {
315
    return mSubtype;
316
  }
317
318
  /**
319
   * Creates a temporary {@link File} that starts with the given prefix.
320
   *
321
   * @param prefix    The file name begins with this string (empty is allowed).
322
   * @param directory The directory wherein the file is created.
323
   * @return The fully qualified path to the temporary file.
324
   * @throws IOException Could not create the temporary file.
325
   */
326
  public Path createTempFile(
327
    final String prefix,
328
    final Path directory ) throws IOException {
329
    return createTempFile( prefix, directory, false );
330
  }
331
332
  /**
333
   * Creates a temporary {@link File} that starts with the given prefix.
334
   *
335
   * @param prefix    The file name begins with this string (empty is allowed).
336
   * @param directory The directory wherein the file is created.
337
   * @param purge     Set to {@code true} to delete the file on exit.
338
   * @return The fully qualified path to the temporary file.
339
   * @throws IOException Could not create the temporary file.
340
   */
341
  public Path createTempFile(
342
    final String prefix,
343
    final Path directory,
344
    final boolean purge )
345
    throws IOException {
346
    assert prefix != null;
347
348
    final var suffix = '.' + MediaTypeExtension
349
      .valueFrom( this )
350
      .getExtension();
351
352
    final var file = File.createTempFile( prefix, suffix, toFile( directory ) );
353
354
    if( purge ) {
355
      file.deleteOnExit();
356
    }
357
358
    return file.toPath();
359
  }
360
361
  /**
362
   * Returns the IANA-defined type and subtype.
363
   *
364
   * @return The unique media type identifier.
365
   */
366
  @Override
367
  public String toString() {
368
    return mMediaType;
369
  }
370
}
1371
A src/main/java/com/keenwrite/io/MediaTypeExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import org.apache.commons.io.FilenameUtils;
5
6
import java.io.File;
7
import java.nio.file.Path;
8
import java.util.List;
9
10
import static com.keenwrite.io.MediaType.*;
11
import static com.keenwrite.io.SysFile.toFile;
12
import static java.util.List.of;
13
14
/**
15
 * Responsible for associating file extensions with {@link MediaType} instances.
16
 * Insertion order must be maintained because the first element in the list
17
 * represents the file name extension that corresponds to its icon.
18
 */
19
public enum MediaTypeExtension {
20
  MEDIA_APP_ACAD( APP_ACAD, of( "dwg" ) ),
21
  MEDIA_APP_PDF( APP_PDF ),
22
  MEDIA_APP_PS( APP_PS, of( "ps" ) ),
23
  MEDIA_APP_EPS( APP_EPS ),
24
  MEDIA_APP_ZIP( APP_ZIP ),
25
26
  MEDIA_AUDIO_MP3( AUDIO_MP3 ),
27
  MEDIA_AUDIO_SIMPLE( AUDIO_SIMPLE, of( "au" ) ),
28
  MEDIA_AUDIO_WAV( AUDIO_WAV, of( "wav" ) ),
29
30
  MEDIA_FONT_OTF( FONT_OTF ),
31
  MEDIA_FONT_TTF( FONT_TTF ),
32
33
  MEDIA_IMAGE_APNG( IMAGE_APNG ),
34
  MEDIA_IMAGE_BMP( IMAGE_BMP ),
35
  MEDIA_IMAGE_GIF( IMAGE_GIF ),
36
  MEDIA_IMAGE_JPEG( IMAGE_JPEG,
37
    of( "jpg", "jpe", "jpeg", "jfif", "pjpeg", "pjp" ) ),
38
  MEDIA_IMAGE_PNG( IMAGE_PNG ),
39
  MEDIA_IMAGE_PSD( IMAGE_PHOTOSHOP, of( "psd" ) ),
40
  MEDIA_IMAGE_SVG( IMAGE_SVG_XML, of( "svg" ) ),
41
  MEDIA_IMAGE_TIFF( IMAGE_TIFF, of( "tiff", "tif" ) ),
42
  MEDIA_IMAGE_WEBP( IMAGE_WEBP ),
43
  MEDIA_IMAGE_X_BITMAP( IMAGE_X_BITMAP, of( "xbm" ) ),
44
  MEDIA_IMAGE_X_PIXMAP( IMAGE_X_PIXMAP, of( "xpm" ) ),
45
46
  MEDIA_VIDEO_MNG( VIDEO_MNG, of( "mng" ) ),
47
48
  MEDIA_TEXT_MARKDOWN( TEXT_MARKDOWN, of(
49
    "md", "markdown", "mdown", "mdtxt", "mdtext", "mdwn", "mkd", "mkdown",
50
    "mkdn" ) ),
51
  MEDIA_TEXT_PLAIN( TEXT_PLAIN, of( "txt", "asc", "ascii", "text", "utxt" ) ),
52
  MEDIA_TEXT_R_MARKDOWN( TEXT_R_MARKDOWN, of( "Rmd" ) ),
53
  MEDIA_TEXT_PROPERTIES( TEXT_PROPERTIES, of( "properties" ) ),
54
  MEDIA_TEXT_HTML( TEXT_HTML, of( "htm", "html" ) ),
55
  MEDIA_APP_XHTML( APP_XHTML, of( "xhtml" ) ),
56
  MEDIA_TEXT_XML( TEXT_XML ),
57
  MEDIA_TEXT_YAML( TEXT_YAML, of( "yaml", "yml" ) ),
58
59
  MEDIA_UNDEFINED( UNDEFINED, of( "undefined" ) );
60
61
  /**
62
   * Returns the {@link MediaTypeExtension} that matches the given media type.
63
   *
64
   * @param mediaType The media type to find.
65
   * @return The correlated value or {@link #MEDIA_UNDEFINED} if not found.
66
   */
67
  public static MediaTypeExtension valueFrom( final MediaType mediaType ) {
68
    for( final var type : values() ) {
69
      if( type.isMediaType( mediaType ) ) {
70
        return type;
71
      }
72
    }
73
74
    return MEDIA_UNDEFINED;
75
  }
76
77
  /**
78
   * Returns the {@link MediaType} associated with the given file name
79
   * extension. The extension must not contain a period.
80
   *
81
   * @param extension File name extension, case-insensitive, {@code null}-safe.
82
   * @return The associated {@link MediaType} as defined by IANA.
83
   */
84
  public static MediaType fromExtension( final String extension ) {
85
    final var sanitized = sanitize( extension );
86
87
    for( final var mediaType : MediaTypeExtension.values() ) {
88
      if( mediaType.isType( sanitized ) ) {
89
        return mediaType.getMediaType();
90
      }
91
    }
92
93
    return UNDEFINED;
94
  }
95
96
  /**
97
   * Returns the {@link MediaType} associated with the given file.
98
   *
99
   * @param file The file having an extension to map to a {@link MediaType}.
100
   * @return The associated {@link MediaType} as defined by IANA.
101
   */
102
  public static MediaType fromFile( final File file ) {
103
    return fromExtension( FilenameUtils.getExtension( file.getName() ) );
104
  }
105
106
  public static MediaType fromPath( final Path path ) {
107
    return fromFile( toFile( path ) );
108
  }
109
110
  private static String sanitize( final String extension ) {
111
    return extension == null ? "" : extension.toLowerCase();
112
  }
113
114
  private final MediaType mMediaType;
115
  private final List<String> mExtensions;
116
117
  /**
118
   * Several media types have only one corresponding standard file name
119
   * extension; this constructor calls {@link MediaType#getSubtype()} to obtain
120
   * said extension. Some {@link MediaType}s have a single extension but their
121
   * assigned IANA name differs (e.g., {@code svg} maps to {@code svg+xml})
122
   * and thus must not use this constructor.
123
   *
124
   * @param mediaType The {@link MediaType} containing only one extension.
125
   */
126
  MediaTypeExtension( final MediaType mediaType ) {
127
    this( mediaType, of( mediaType.getSubtype() ) );
128
  }
129
130
  /**
131
   * Constructs an association of file name extensions to a single {@link
132
   * MediaType}.
133
   *
134
   * @param mediaType  The {@link MediaType} to associate with the given
135
   *                   file name extensions.
136
   * @param extensions The file name extensions used to lookup a corresponding
137
   *                   {@link MediaType}.
138
   */
139
  MediaTypeExtension(
140
    final MediaType mediaType, final List<String> extensions ) {
141
    assert mediaType != null;
142
    assert extensions != null;
143
    assert !extensions.isEmpty();
144
145
    mMediaType = mediaType;
146
    mExtensions = extensions;
147
  }
148
149
  /**
150
   * Returns the first file name extension in the list of file names given
151
   * at construction time.
152
   *
153
   * @return The one file name to rule them all.
154
   */
155
  public String getExtension() {
156
    return mExtensions.getFirst();
157
  }
158
159
  boolean isMediaType( final MediaType mediaType ) {
160
    return mMediaType == mediaType;
161
  }
162
163
  private boolean isType( final String sanitized ) {
164
    for( final var extension : mExtensions ) {
165
      if( extension.equalsIgnoreCase( sanitized ) ) {
166
        return true;
167
      }
168
    }
169
170
    return false;
171
  }
172
173
  private MediaType getMediaType() {
174
    return mMediaType;
175
  }
176
}
1177
A src/main/java/com/keenwrite/io/MediaTypeSniffer.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.io;
6
7
import java.io.*;
8
import java.util.LinkedHashMap;
9
import java.util.Map;
10
11
import static com.keenwrite.io.MediaType.*;
12
import static java.lang.Math.min;
13
import static java.lang.System.arraycopy;
14
import static java.util.Arrays.fill;
15
16
/**
17
 * Associates file signatures with IANA-defined {@link MediaType}s. See:
18
 * <a href="https://www.garykessler.net/library/file_sigs.html">
19
 * Gary Kessler's List
20
 * </a>,
21
 * <a href="https://en.wikipedia.org/wiki/List_of_file_signatures">
22
 * Wikipedia's List
23
 * </a>, and
24
 * <a href="https://github.com/veniware/Space-Maker/blob/master/FileSignatures.cs">
25
 * Space Maker's List
26
 * </a>
27
 */
28
public class MediaTypeSniffer {
29
  /**
30
   * The maximum buffer size of magic bytes to analyze.
31
   */
32
  private static final int BUFFER = 12;
33
34
  /**
35
   * The media type data can have any value at a corresponding offset.
36
   */
37
  private static final int ANY = -1;
38
39
  /**
40
   * Denotes there are fewer than {@link #BUFFER} bytes to compare.
41
   */
42
  private static final int EOS = -2;
43
44
  private static final Map<int[], MediaType> FORMAT = new LinkedHashMap<>();
45
46
  private static void put( final int[] data, final MediaType mediaType ) {
47
    FORMAT.put( data, mediaType );
48
  }
49
50
  /* The insertion order attempts to approximate the real-world likelihood of
51
   * encountering particular file formats in an application.
52
   */
53
  static {
54
    //@formatter:off
55
    put( ints( 0x3C, 0x73, 0x76, 0x67, 0x20 ), IMAGE_SVG_XML );
56
    put( ints( 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ), IMAGE_PNG );
57
    put( ints( 0xFF, 0xD8, 0xFF, 0xE0 ), IMAGE_JPEG );
58
    put( ints( 0xFF, 0xD8, 0xFF, 0xEE ), IMAGE_JPEG );
59
    put( ints( 0xFF, 0xD8, 0xFF, 0xE1, ANY, ANY, 0x45, 0x78, 0x69, 0x66, 0x00 ), IMAGE_JPEG );
60
    put( ints( 0x3C, 0x21 ), TEXT_HTML );
61
    put( ints( 0x3C, 0x68, 0x74, 0x6D, 0x6C ), TEXT_HTML );
62
    put( ints( 0x3C, 0x68, 0x65, 0x61, 0x64 ), TEXT_HTML );
63
    put( ints( 0x3C, 0x62, 0x6F, 0x64, 0x79 ), TEXT_HTML );
64
    put( ints( 0x3C, 0x48, 0x54, 0x4D, 0x4C ), TEXT_HTML );
65
    put( ints( 0x3C, 0x48, 0x45, 0x41, 0x44 ), TEXT_HTML );
66
    put( ints( 0x3C, 0x42, 0x4F, 0x44, 0x59 ), TEXT_HTML );
67
    put( ints( 0x3C, 0x3F, 0x78, 0x6D, 0x6C, 0x20 ), TEXT_XML );
68
    put( ints( 0xFE, 0xFF, 0x00, 0x3C, 0x00, 0x3f, 0x00, 0x78 ), TEXT_XML );
69
    put( ints( 0xFF, 0xFE, 0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00 ), TEXT_XML );
70
    put( ints( 0x47, 0x49, 0x46, 0x38 ), IMAGE_GIF );
71
    put( ints( 0x42, 0x4D ), IMAGE_BMP );
72
    put( ints( 0x49, 0x49, 0x2A, 0x00 ), IMAGE_TIFF );
73
    put( ints( 0x4D, 0x4D, 0x00, 0x2A ), IMAGE_TIFF );
74
    put( ints( 0x52, 0x49, 0x46, 0x46, ANY, ANY, ANY, ANY, 0x57, 0x45, 0x42, 0x50 ), IMAGE_WEBP );
75
    put( ints( 0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E ), APP_PDF );
76
    put( ints( 0x25, 0x21, 0x50, 0x53, 0x2D, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x2D ), APP_EPS );
77
    put( ints( 0x25, 0x21, 0x50, 0x53 ), APP_PS );
78
    put( ints( 0x38, 0x42, 0x50, 0x53, 0x00, 0x01 ), IMAGE_PHOTOSHOP );
79
    put( ints( 0xFF, 0xFB, 0x30 ), AUDIO_MP3 );
80
    put( ints( 0x49, 0x44, 0x33 ), AUDIO_MP3 );
81
    put( ints( 0x8A, 0x4D, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ), VIDEO_MNG );
82
    put( ints( 0x23, 0x64, 0x65, 0x66 ), IMAGE_X_BITMAP );
83
    put( ints( 0x21, 0x20, 0x58, 0x50, 0x4D, 0x32 ), IMAGE_X_PIXMAP );
84
    put( ints( 0x2E, 0x73, 0x6E, 0x64 ), AUDIO_SIMPLE );
85
    put( ints( 0x64, 0x6E, 0x73, 0x2E ), AUDIO_SIMPLE );
86
    put( ints( 0x52, 0x49, 0x46, 0x46 ), AUDIO_WAV );
87
    put( ints( 0x50, 0x4B ), APP_ZIP );
88
    put( ints( 0x41, 0x43, ANY, ANY, ANY, ANY, 0x00, 0x00, 0x00, 0x00, 0x00 ), APP_ACAD );
89
    put( ints( 0xCA, 0xFE, 0xBA, 0xBE ), APP_JAVA );
90
    put( ints( 0xAC, 0xED ), APP_JAVA_OBJECT );
91
    //@formatter:on
92
  }
93
94
  /**
95
   * Returns the {@link MediaType} for a given set of bytes.
96
   *
97
   * @param data Binary data to compare against the list of known formats.
98
   * @return The IANA-defined {@link MediaType}, or
99
   * {@link MediaType#UNDEFINED} if indeterminate.
100
   */
101
  public static MediaType getMediaType( final byte[] data ) {
102
    assert data != null;
103
    assert data.length > 0;
104
105
    final var source = new int[]{
106
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
107
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
108
      0xFF, 0XFF, EOS
109
    };
110
111
    final int length = min( data.length, source.length );
112
113
    for( int i = 0; i < length; i++ ) {
114
      source[ i ] = data[ i ] & 0xFF;
115
    }
116
117
    for( final var entry : FORMAT.entrySet() ) {
118
      final var key = entry.getKey();
119
120
      int i = -1;
121
      boolean matches = true;
122
123
      while( ++i < BUFFER && key[ i ] != EOS && matches ) {
124
        matches = key[ i ] == source[ i ] || key[ i ] == ANY;
125
      }
126
127
      if( matches ) {
128
        return entry.getValue();
129
      }
130
    }
131
132
    return UNDEFINED;
133
  }
134
135
  /**
136
   * Convenience method to return the probed media type for the given
137
   * {@link SysFile} instance by delegating to
138
   * {@link #getMediaType(InputStream)}.
139
   *
140
   * @param file File to ascertain the {@link MediaType}.
141
   * @return The IANA-defined {@link MediaType}, or
142
   * {@link MediaType#UNDEFINED} if indeterminate.
143
   * @throws IOException Could not read from the {@link File}.
144
   */
145
  public static MediaType getMediaType( final File file )
146
    throws IOException {
147
    try( final var fis = new FileInputStream( file ) ) {
148
      return getMediaType( fis );
149
    }
150
  }
151
152
  /**
153
   * Convenience method to return the probed media type for the given
154
   * {@link BufferedInputStream} instance. <strong>This resets the stream
155
   * pointer</strong> making the call idempotent. Prefer calling this
156
   * method when operating on streams to avoid advancing the stream.
157
   *
158
   * @param bis Data source to ascertain the {@link MediaType}.
159
   * @return The IANA-defined {@link MediaType}, or
160
   * {@link MediaType#UNDEFINED} if indeterminate.
161
   * @throws IOException Could not read from the stream.
162
   */
163
  public static MediaType getMediaType( final BufferedInputStream bis )
164
    throws IOException {
165
    bis.mark( BUFFER );
166
    final var result = getMediaType( (InputStream) bis );
167
    bis.reset();
168
169
    return result;
170
  }
171
172
  /**
173
   * Returns the probed media type for the given {@link InputStream} instance.
174
   * The caller is responsible for closing the stream. <strong>This advances
175
   * the stream.</strong> Use {@link #getMediaType(BufferedInputStream)} to
176
   * perform a non-destructive read.
177
   *
178
   * @param is Data source to ascertain the {@link MediaType}.
179
   * @return The IANA-defined {@link MediaType}, or
180
   * {@link MediaType#UNDEFINED} if indeterminate.
181
   * @throws IOException Could not read from the {@link InputStream}.
182
   */
183
  private static MediaType getMediaType( final InputStream is )
184
    throws IOException {
185
    final var input = new byte[ BUFFER ];
186
    final var count = is.read( input, 0, BUFFER );
187
188
    if( count > 1 ) {
189
      final var available = new byte[ count ];
190
      arraycopy( input, 0, available, 0, count );
191
      return getMediaType( available );
192
    }
193
194
    return UNDEFINED;
195
  }
196
197
  /**
198
   * Creates an integer array from the given data, padded with {@link #EOS}
199
   * values up to {@link #BUFFER} in length.
200
   *
201
   * @param data The input byte values to pad.
202
   * @return The data with padding.
203
   */
204
  private static int[] ints( final int... data ) {
205
    assert data != null;
206
207
    final var magic = new int[ data.length + 1 ];
208
209
    fill( magic, EOS );
210
    arraycopy( data, 0, magic, 0, data.length );
211
212
    return magic;
213
  }
214
}
1215
A src/main/java/com/keenwrite/io/PathScanner.java
1
package com.keenwrite.io;
2
3
import java.io.File;
4
import java.nio.file.Path;
5
import java.util.ArrayList;
6
import java.util.List;
7
import java.util.Optional;
8
9
import static com.keenwrite.util.Strings.sanitize;
10
import static java.lang.System.getenv;
11
import static java.nio.file.Files.isExecutable;
12
import static java.util.Arrays.asList;
13
14
/**
15
 * Responsible for finding the fully qualified PATH to an executable file.
16
 */
17
public class PathScanner {
18
  /**
19
   * For finding executable programs. These are used in an O( n^2 ) search,
20
   * so don't add more entries than necessary.
21
   */
22
  private static final String[] EXECUTABLE_EXTENSIONS = {
23
    "", ".exe", ".bat", ".cmd", ".com", ".msc", ".msi",
24
  };
25
26
  public static List<String> scan() {
27
    final var path = sanitize( getenv( "PATH" ) );
28
    final var directories = path.split( File.pathSeparator );
29
30
    return new ArrayList<>( asList( directories ) );
31
  }
32
33
  public static Optional<Path> scanExtensions(
34
    final String directory,
35
    final String executable
36
  ) {
37
    for( final var ext : EXECUTABLE_EXTENSIONS ) {
38
      final var dir = Path.of( directory );
39
      final var path = dir.resolve( executable + ext );
40
41
      if( isExecutable( path ) ) {
42
        return Optional.of( path );
43
      }
44
    }
45
46
    return Optional.empty();
47
  }
48
}
149
A src/main/java/com/keenwrite/io/PollingWatchService.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import java.nio.file.WatchEvent;
5
import java.nio.file.WatchKey;
6
import java.nio.file.WatchService;
7
import java.nio.file.Watchable;
8
import java.util.List;
9
import java.util.concurrent.TimeUnit;
10
11
/**
12
 * Responsible for polling the file system to see whether a file has been
13
 * updated. This is instantiated when an instance of {@link WatchService}
14
 * cannot be created using the Java API.
15
 * <p>
16
 * This is a skeleton class to avoid {@code null} references. In theory,
17
 * it should never get instantiated. If the application is run on a system
18
 * that does not support file system events, this should eliminate NPEs.
19
 * </p>
20
 */
21
public class PollingWatchService implements WatchService {
22
  private final WatchKey EMPTY_KEY = new WatchKey() {
23
    private final Watchable WATCHABLE = new Watchable() {
24
      @Override
25
      public WatchKey register(
26
        final WatchService watcher,
27
        final WatchEvent.Kind<?>[] events,
28
        final WatchEvent.Modifier... modifiers ) {
29
        return EMPTY_KEY;
30
      }
31
32
      @Override
33
      public WatchKey register(
34
        final WatchService watcher, final WatchEvent.Kind<?>... events ) {
35
        return EMPTY_KEY;
36
      }
37
    };
38
39
    @Override
40
    public boolean isValid() {
41
      return false;
42
    }
43
44
    @Override
45
    public List<WatchEvent<?>> pollEvents() {
46
      return List.of();
47
    }
48
49
    @Override
50
    public boolean reset() {
51
      return false;
52
    }
53
54
    @Override
55
    public void cancel() {
56
    }
57
58
    @Override
59
    public Watchable watchable() {
60
      return WATCHABLE;
61
    }
62
  };
63
64
  @Override
65
  public void close() {
66
  }
67
68
  @Override
69
  public WatchKey poll() {
70
    return EMPTY_KEY;
71
  }
72
73
  @Override
74
  public WatchKey poll( final long timeout, final TimeUnit unit ) {
75
    return EMPTY_KEY;
76
  }
77
78
  @Override
79
  public WatchKey take() {
80
    return EMPTY_KEY;
81
  }
82
}
183
A src/main/java/com/keenwrite/io/StreamGobbler.java
1
package com.keenwrite.io;
2
3
import java.io.BufferedReader;
4
import java.io.IOException;
5
import java.io.InputStream;
6
import java.io.InputStreamReader;
7
import java.util.concurrent.Callable;
8
import java.util.function.Consumer;
9
10
import static java.nio.charset.StandardCharsets.UTF_8;
11
import static java.util.concurrent.Executors.newFixedThreadPool;
12
13
/**
14
 * Consumes the standard output of a {@link Process} created from a
15
 * {@link ProcessBuilder}. Directs the output to a {@link Consumer} of
16
 * strings. This will run on its own thread and close the stream when
17
 * no more data can be processed.
18
 * <p>
19
 * <strong>Warning:</strong> Do not use this with binary data, it is only
20
 * meant for text streams, such as standard out from running command-line
21
 * applications.
22
 * </p>
23
 */
24
public class StreamGobbler implements Callable<Boolean> {
25
  private final InputStream mInput;
26
  private final Consumer<String> mConsumer;
27
28
  /**
29
   * Constructs a new instance of {@link StreamGobbler} that is capable of
30
   * reading an {@link InputStream} and passing each line of textual data from
31
   * that stream over to a string {@link Consumer}.
32
   *
33
   * @param input    The stream having input to pass to the consumer.
34
   * @param consumer The {@link Consumer} that receives each line.
35
   */
36
  private StreamGobbler(
37
    final InputStream input,
38
    final Consumer<String> consumer ) {
39
    assert input != null;
40
    assert consumer != null;
41
42
    mInput = input;
43
    mConsumer = consumer;
44
  }
45
46
  /**
47
   * Consumes the input until no more data is available. Closes the stream.
48
   *
49
   * @return {@link Boolean#TRUE} always.
50
   * @throws IOException Could not read from the stream.
51
   */
52
  @Override
53
  public Boolean call() throws IOException {
54
    try( final var input = new InputStreamReader( mInput, UTF_8 );
55
         final var buffer = new BufferedReader( input ) ) {
56
      buffer.lines().forEach( mConsumer );
57
    }
58
59
    return Boolean.TRUE;
60
  }
61
62
  /**
63
   * Reads the given {@link InputStream} on a separate thread and passes
64
   * each line of text input to the given {@link Consumer}.
65
   *
66
   * @param inputStream The stream having input to pass to the consumer.
67
   * @param consumer    The {@link Consumer} that receives each line.
68
   */
69
  public static void gobble(
70
    final InputStream inputStream, final Consumer<String> consumer ) {
71
    try( final var executor = newFixedThreadPool( 1 ) ) {
72
      executor.submit( new StreamGobbler( inputStream, consumer ) );
73
    }
74
  }
75
}
176
A src/main/java/com/keenwrite/io/SysFile.java
1
/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.io;
6
7
import org.jetbrains.annotations.NotNull;
8
9
import java.io.File;
10
import java.io.FileInputStream;
11
import java.io.IOException;
12
import java.nio.file.Path;
13
import java.security.MessageDigest;
14
import java.security.NoSuchAlgorithmException;
15
import java.util.List;
16
import java.util.Optional;
17
import java.util.function.Function;
18
import java.util.function.Predicate;
19
20
import static com.keenwrite.constants.Constants.USER_DIRECTORY;
21
import static com.keenwrite.events.StatusEvent.clue;
22
import static com.keenwrite.io.PathScanner.scan;
23
import static com.keenwrite.io.PathScanner.scanExtensions;
24
import static com.keenwrite.io.WindowsRegistry.pathsWindows;
25
import static com.keenwrite.util.DataTypeConverter.toHex;
26
import static com.keenwrite.util.SystemUtils.IS_OS_WINDOWS;
27
import static java.lang.System.getenv;
28
import static java.util.regex.Pattern.quote;
29
30
/**
31
 * Responsible for file-related functionality.
32
 */
33
public final class SysFile extends java.io.File {
34
  private static final String WHERE_COMMAND =
35
    IS_OS_WINDOWS ? "where" : "which";
36
37
  /**
38
   * Number of bytes to read at a time when computing this file's checksum.
39
   */
40
  private static final int BUFFER_SIZE = 16384;
41
42
  /**
43
   * Creates a new instance for a given file name.
44
   *
45
   * @param filename Filename to query existence as executable.
46
   */
47
  public SysFile( final String filename ) {
48
    super( filename );
49
  }
50
51
  /**
52
   * Creates a new instance for a given {@link File}. This is useful for
53
   * validating checksums against an existing {@link File} instance that
54
   * may optionally exist in a directory listed in the PATH environment
55
   * variable.
56
   *
57
   * @param file The file to change into a "system file".
58
   */
59
  public SysFile( final File file ) {
60
    super( file.getAbsolutePath() );
61
  }
62
63
  /**
64
   * Answers whether an executable can be found that can be run using a
65
   * {@link ProcessBuilder}.
66
   *
67
   * @return {@code true} if the executable is runnable.
68
   */
69
  public boolean canRun() {
70
    final var path = locate();
71
72
    path.ifPresentOrElse(
73
      _ -> clue( "Wizard.container.executable.run.found", path ),
74
      () -> clue( "Wizard.container.executable.run.missing", path )
75
    );
76
77
    return path.isPresent();
78
  }
79
80
  /**
81
   * For a file name that represents an executable (without an extension)
82
   * file, this determines the first matching executable found in the PATH
83
   * environment variable. This will search the PATH each time the method
84
   * is invoked, triggering a full directory scan for all paths listed in
85
   * the environment variable. The result is not cached, so avoid calling
86
   * this in a critical loop.
87
   * <p>
88
   * After installing software, the software might be located in the PATH,
89
   * but not available to run by its name alone. In such cases, we need the
90
   * absolute path to the executable to run it. This will always return
91
   * the fully qualified path, otherwise an empty result.
92
   *
93
   * @return Fully qualified path to the executable, if found.
94
   */
95
  public Optional<Path> locate() {
96
    final var dirs = scan();
97
    clue( dirs );
98
99
    var path = locate( dirs, "Wizard.container.executable.path" );
100
101
    if( path.isEmpty() ) {
102
      clue();
103
104
      try {
105
        path = where();
106
      } catch( final IOException ex ) {
107
        clue( "Wizard.container.executable.which", ex );
108
      }
109
    }
110
111
    return path.isPresent()
112
      ? path
113
      : locate( System::getenv,
114
                IS_OS_WINDOWS
115
                  ? "Wizard.container.executable.registry"
116
                  : "Wizard.container.executable.path" );
117
  }
118
119
  private Optional<Path> locate( final List<String> dirs, final String msg ) {
120
    final var exe = getName();
121
122
    for( final var dir : dirs ) {
123
      try {
124
        return scanExtensions( dir, exe );
125
      } catch( final Exception ex ) {
126
        clue( ex );
127
      }
128
    }
129
130
    clue( msg );
131
    return Optional.empty();
132
  }
133
134
  private Optional<Path> locate(
135
    final Function<String, String> map, final String msg ) {
136
    final var paths = paths( map ).split( quote( pathSeparator ) );
137
138
    return locate( List.of( paths ), msg );
139
  }
140
141
  /**
142
   * Changes to the PATH environment variable aren't reflected for the
143
   * currently running task. The registry, however, contains the updated
144
   * value. Reading the registry is a hack.
145
   *
146
   * @param map The mapping function of registry variable names to values.
147
   * @return The revised PATH variables as stored in the registry.
148
   */
149
  private static String paths( final Function<String, String> map ) {
150
    return IS_OS_WINDOWS ? pathsWindows( map ) : pathsSane();
151
  }
152
153
  /**
154
   * Answers whether this file's SHA-256 checksum equals the given
155
   * hexadecimal-encoded checksum string.
156
   *
157
   * @param hex The string to compare against the checksum for this file.
158
   * @return {@code true} if the checksums match; {@code false} on any
159
   * error or checksums don't match.
160
   */
161
  public boolean isChecksum( final String hex ) {
162
    assert hex != null;
163
164
    try {
165
      return checksum( "SHA-256" ).equalsIgnoreCase( hex );
166
    } catch( final Exception ex ) {
167
      return false;
168
    }
169
  }
170
171
  /**
172
   * Returns the hash code for this file.
173
   *
174
   * @return The hex-encoded hash code for the file contents.
175
   */
176
  @SuppressWarnings( "SameParameterValue" )
177
  private String checksum( final String algorithm )
178
    throws NoSuchAlgorithmException, IOException {
179
    final var digest = MessageDigest.getInstance( algorithm );
180
181
    try( final var in = new FileInputStream( this ) ) {
182
      final var bytes = new byte[ BUFFER_SIZE ];
183
      int count;
184
185
      while( (count = in.read( bytes )) != -1 ) {
186
        digest.update( bytes, 0, count );
187
      }
188
189
      return toHex( digest.digest() );
190
    }
191
  }
192
193
  /**
194
   * Runs {@code where} or {@code which} to determine the fully qualified path
195
   * to an executable.
196
   *
197
   * @return The path to the executable for this file, if found.
198
   * @throws IOException Could not determine the location of the command.
199
   */
200
  public Optional<Path> where() throws IOException {
201
    // The "where" command on Windows will automatically add the extension.
202
    final var args = new String[]{ WHERE_COMMAND, getName() };
203
    final var output = run( _ -> true, args );
204
    final var result = output.lines().findFirst();
205
206
    return result.map( Path::of );
207
  }
208
209
  /**
210
   * Runs a command and collects standard output into a buffer.
211
   *
212
   * @param filter Provides an injected test to determine whether the line
213
   *               read from the command's standard output is to be added to
214
   *               the result buffer.
215
   * @param args   The command and its arguments to run.
216
   * @return The standard output from the command, filtered.
217
   * @throws IOException Could not run the command.
218
   */
219
  @NotNull
220
  public static String run(
221
    final Predicate<String> filter,
222
    final String[] args ) throws IOException {
223
    final var process = Runtime.getRuntime().exec( args );
224
    final var stream = process.getInputStream();
225
    final var stdout = new StringBuffer( 2048 );
226
227
    StreamGobbler.gobble( stream, text -> {
228
      if( filter.test( text ) ) {
229
        stdout.append( WindowsRegistry.parseRegEntry( text ) );
230
      }
231
    } );
232
233
    try {
234
      process.waitFor();
235
    } catch( final InterruptedException ex ) {
236
      throw new IOException( ex );
237
    } finally {
238
      process.destroy();
239
    }
240
241
    return stdout.toString();
242
  }
243
244
  /**
245
   * Provides {@code null}-safe machinery to get a file name.
246
   *
247
   * @param p The path to the file name to retrieve (may be {@code null}).
248
   * @return The file name or the empty string if the path is not found.
249
   */
250
  public static String getFileName( final Path p ) {
251
    return p == null ? "" : getPathFileName( p );
252
  }
253
254
  /**
255
   * If the path doesn't exist right before typesetting, switch the path
256
   * to the user's home directory to increase the odds of the typesetter
257
   * succeeding. This could help, for example, if the images directory was
258
   * deleted or moved.
259
   *
260
   * @param path The path to verify existence, may be null.
261
   * @return The given path, if it exists, otherwise the user's home directory.
262
   */
263
  public static Path normalize( final Path path ) {
264
    return path == null
265
      ? USER_DIRECTORY.toPath()
266
      : path.toFile().exists()
267
      ? path
268
      : USER_DIRECTORY.toPath();
269
  }
270
271
  public static Path normalize( final File file ) {
272
    return file == null
273
            ? USER_DIRECTORY.toPath()
274
            : normalize( file.toPath() );
275
  }
276
277
  public static File toFile( final Path path ) {
278
    return path == null
279
      ? USER_DIRECTORY
280
      : path.toFile();
281
  }
282
283
  private static String pathsSane() {
284
    return getenv( "PATH" );
285
  }
286
287
  private static String getPathFileName( final Path p ) {
288
    assert p != null;
289
290
    final var f = p.getFileName();
291
292
    return f == null ? "" : f.toString();
293
  }
294
}
1295
A src/main/java/com/keenwrite/io/UserDataDir.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.io;
6
7
import java.nio.file.Path;
8
9
import static com.keenwrite.io.SysFile.toFile;
10
import static com.keenwrite.util.SystemUtils.*;
11
import static java.lang.System.getProperty;
12
import static java.lang.System.getenv;
13
14
/**
15
 * Responsible for determining the directory to write application data, across
16
 * multiple platforms. See also:
17
 *
18
 * <ul>
19
 * <li>
20
 *   <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">
21
 *     Linux: XDG Base Directory Specification
22
 *   </a>
23
 * </li>
24
 * <li>
25
 *   <a href="https://learn.microsoft.com/en-us/windows/deployment/usmt/usmt-recognized-environment-variables">
26
 *     Windows: Recognized environment variables
27
 *   </a>
28
 * </li>
29
 * <li>
30
 *   <a href="https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html">
31
 *     macOS: File System Programming Guide
32
 *   </a>
33
 * </li>
34
 * </ul>
35
 * </p>
36
 */
37
public final class UserDataDir {
38
39
  private static final Path UNDEFINED = Path.of( "/" );
40
41
  private static final String PROP_USER_HOME = getProperty( "user.home" );
42
  private static final String PROP_USER_DIR = getProperty( "user.dir" );
43
  private static final String PROP_OS_VERSION = getProperty( "os.version" );
44
  private static final String ENV_APPDATA = getenv( "AppData" );
45
  private static final String ENV_XDG_DATA_HOME = getenv( "XDG_DATA_HOME" );
46
47
  private UserDataDir() { }
48
49
  public static String getUserHome() {
50
    return PROP_USER_HOME;
51
  }
52
53
  /**
54
   * Makes a valiant attempt at determining where to create application-specific
55
   * files, regardless of operating system.
56
   *
57
   * @param appName The application name that seeks to create files.
58
   * @return A fully qualified {@link Path} to a directory wherein files may
59
   * be created that are user- and application-specific.
60
   */
61
  public static Path getAppPath( final String appName ) {
62
    final var osPath = isWindows()
63
      ? getWinAppPath()
64
      : isMacOs()
65
      ? getMacAppPath()
66
      : isUnix()
67
      ? getUnixAppPath()
68
      : UNDEFINED;
69
70
    final var path = osPath.equals( UNDEFINED )
71
      ? getDefaultAppPath( appName )
72
      : osPath.resolve( appName );
73
74
    final var alternate = Path.of( PROP_USER_DIR, appName );
75
76
    return ensureExists( path )
77
      ? path
78
      : ensureExists( alternate )
79
      ? alternate
80
      : Path.of( PROP_USER_DIR );
81
  }
82
83
  private static Path getWinAppPath() {
84
    return isValid( ENV_APPDATA )
85
      ? Path.of( ENV_APPDATA )
86
      : home( getWinVerAppPath() );
87
  }
88
89
  /**
90
   * Gets the application path with respect to the Windows version.
91
   *
92
   * @return The directory name paths relative to the user's home directory.
93
   */
94
  private static String[] getWinVerAppPath() {
95
    return PROP_OS_VERSION.startsWith( "5." )
96
      ? new String[]{"Application Data"}
97
      : new String[]{"AppData", "Roaming"};
98
  }
99
100
  private static Path getMacAppPath() {
101
    final var path = home( "Library", "Application Support" );
102
103
    return ensureExists( path ) ? path : UNDEFINED;
104
  }
105
106
  private static Path getUnixAppPath() {
107
    // Fallback in case the XDG data directory is undefined.
108
    var path = home( ".local", "share" );
109
110
    if( isValid( ENV_XDG_DATA_HOME ) ) {
111
      final var xdgPath = Path.of( ENV_XDG_DATA_HOME );
112
113
      path = ensureExists( xdgPath ) ? xdgPath : path;
114
    }
115
116
    return path;
117
  }
118
119
  /**
120
   * Returns a hidden directory relative to the user's home directory.
121
   *
122
   * @param appName The application name.
123
   * @return A suitable directory for storing application files.
124
   */
125
  private static Path getDefaultAppPath( final String appName ) {
126
    return home( '.' + appName );
127
  }
128
129
  private static Path home( final String... paths ) {
130
    return Path.of( PROP_USER_HOME, paths );
131
  }
132
133
  /**
134
   * Verifies whether the path exists or was created.
135
   *
136
   * @param path The directory to verify.
137
   * @return {@code true} if the path already exists or was created,
138
   * {@code false} if the directory doesn't exist and couldn't be created.
139
   */
140
  private static boolean ensureExists( final Path path ) {
141
    final var file = toFile( path );
142
    return file.exists() || file.mkdirs();
143
  }
144
145
  /**
146
   * Answers whether the given string contains content.
147
   *
148
   * @param s The string to check, may be {@code null}.
149
   * @return {@code true} if the string is neither {@code null} nor blank.
150
   */
151
  private static boolean isValid( final String s ) {
152
    return !(s == null || s.isBlank());
153
  }
154
155
  private static boolean isWindows() {
156
    return IS_OS_WINDOWS;
157
  }
158
159
  private static boolean isMacOs() {
160
    return IS_OS_MAC;
161
  }
162
163
  private static boolean isUnix() {
164
    return IS_OS_UNIX;
165
  }
166
}
1167
A src/main/java/com/keenwrite/io/WindowsRegistry.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import java.io.IOException;
5
import java.util.function.Function;
6
import java.util.regex.Pattern;
7
8
import static java.io.File.pathSeparator;
9
import static java.lang.System.getenv;
10
import static java.util.regex.Pattern.compile;
11
import static java.util.regex.Pattern.quote;
12
13
/**
14
 * Responsible for obtaining Windows registry key values.
15
 */
16
public class WindowsRegistry {
17
  //@formatter:off
18
  private static final String SYS_KEY =
19
    "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
20
  private static final String USR_KEY =
21
    "HKEY_CURRENT_USER\\Environment";
22
  //@formatter:on
23
24
  /**
25
   * Regular expression pattern for matching %VARIABLE% names.
26
   */
27
  private static final String VAR_REGEX = "%.*?%";
28
  private static final Pattern VAR_PATTERN = compile( VAR_REGEX );
29
30
  private static final String REG_REGEX = "\\s*path\\s+REG_.*SZ\\s+(.*)";
31
  private static final Pattern REG_PATTERN = compile( REG_REGEX );
32
33
  /**
34
   * Returns the value of the Windows PATH registry key.
35
   *
36
   * @return The PATH environment variable if the registry query fails.
37
   */
38
  @SuppressWarnings( "SpellCheckingInspection" )
39
  public static String pathsWindows( final Function<String, String> map ) {
40
    try {
41
      final var hklm = query( SYS_KEY );
42
      final var hkcu = query( USR_KEY );
43
44
      return expand( hklm, map ) + pathSeparator + expand( hkcu, map );
45
    } catch( final IOException ex ) {
46
      return getenv( "PATH" );
47
    }
48
  }
49
50
  /**
51
   * Queries a registry key PATH value.
52
   *
53
   * @param key The registry key name to look up.
54
   * @return The value for the registry key.
55
   */
56
  private static String query( final String key ) throws IOException {
57
    final var registryVarName = "path";
58
    final var args = new String[]{"reg", "query", key, "/v", registryVarName};
59
60
    return SysFile.run( text -> text.contains( registryVarName ), args );
61
  }
62
63
  static String parseRegEntry( final String text ) {
64
    assert text != null;
65
66
    final var matcher = REG_PATTERN.matcher( text );
67
    return matcher.find() ? matcher.group( 1 ) : text.trim();
68
  }
69
70
  /**
71
   * PATH environment variables returned from the registry have unexpanded
72
   * variables of the form %VARIABLE%. This method will expand those values,
73
   * if possible, from the environment. This will only perform a single
74
   * expansion, which should be adequate for most needs.
75
   *
76
   * @param s The %VARIABLE%-encoded value to expand.
77
   * @return The given value with all encoded values expanded.
78
   */
79
  static String expand( final String s, final Function<String, String> map ) {
80
    // Assigned to the unexpanded string, initially.
81
    String expanded = s;
82
83
    final var matcher = VAR_PATTERN.matcher( expanded );
84
85
    while( matcher.find() ) {
86
      final var match = matcher.group( 0 );
87
      String value = map.apply( match );
88
89
      if( value == null ) {
90
        value = "";
91
      }
92
      else {
93
        value = value.replace( "\\", "\\\\" );
94
      }
95
96
      final var subexpression = compile( quote( match ) );
97
      expanded = subexpression.matcher( expanded ).replaceAll( value );
98
    }
99
100
    return expanded;
101
  }
102
}
1103
A src/main/java/com/keenwrite/io/Zip.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.io;
6
7
import java.io.IOException;
8
import java.io.UncheckedIOException;
9
import java.nio.charset.StandardCharsets;
10
import java.nio.file.Files;
11
import java.nio.file.Path;
12
import java.util.concurrent.atomic.AtomicReference;
13
import java.util.function.BiConsumer;
14
import java.util.zip.ZipEntry;
15
import java.util.zip.ZipFile;
16
17
import static com.keenwrite.io.SysFile.toFile;
18
import static java.nio.file.Files.createDirectories;
19
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
20
21
/**
22
 * Responsible for managing zipped archive files. Does not handle archives
23
 * within archives.
24
 */
25
public final class Zip {
26
  /**
27
   * Extracts the contents of the zip archive into its current directory. The
28
   * contents of the archive must be {@link StandardCharsets#UTF_8}. For
29
   * example, if the {@link Path} is <code>/tmp/filename.zip</code>, then
30
   * the contents of the file will be extracted into <code>/tmp</code>.
31
   *
32
   * @param zipPath The {@link Path} to the zip file to extract.
33
   * @throws IOException Could not extract the zip file, zip entries, or find
34
   *                     the parent directory that contains the path to the
35
   *                     zip archive.
36
   */
37
  public static void extract( final Path zipPath ) throws IOException {
38
    final var parent = zipPath.getParent();
39
40
    if( parent == null ) {
41
      throw new IOException( "Path to zip file has no parent." );
42
    }
43
44
    final var path = parent.normalize();
45
46
    iterate( zipPath, ( zipFile, zipEntry ) -> {
47
      // Determine the directory name where the zip archive resides. Files will
48
      // be extracted relative to that directory.
49
      final var zipEntryPath = path.resolve( zipEntry.getName() );
50
51
      // Guard against zip slip.
52
      if( zipEntryPath.normalize().startsWith( path ) ) {
53
        try {
54
          extract( zipFile, zipEntry, zipEntryPath );
55
        } catch( final IOException ex ) {
56
          throw new UncheckedIOException( ex );
57
        }
58
      }
59
    } );
60
  }
61
62
  /**
63
   * Returns the first root-level directory found in the zip archive. Only call
64
   * this function if you know there is exactly one top-level directory in the
65
   * zip archive. If there are multiple top-level directories, one of the
66
   * directories will be returned, albeit indeterminately. No files are
67
   * extracted when calling this function.
68
   *
69
   * @param zipPath The path to the zip archive to process.
70
   * @return The fully qualified root-level directory resolved relatively to
71
   * the zip archive itself.
72
   * @throws IOException Could not process the zip archive.
73
   */
74
  public static Path root( final Path zipPath ) throws IOException {
75
    // Directory that contains the zip archive file.
76
    final var zipParent = zipPath.getParent();
77
78
    if( zipParent == null ) {
79
      throw new IOException( zipPath + " has no parent" );
80
    }
81
82
    final var result = new AtomicReference<>( zipParent );
83
84
    iterate( zipPath, ( zipFile, zipEntry ) -> {
85
      final var zipEntryPath = Path.of( zipEntry.getName() );
86
87
      // The first entry without a parent is considered the root-level entry.
88
      // Return the relative directory path to that entry.
89
      if( zipEntryPath.getParent() == null ) {
90
        result.set( zipParent.resolve( zipEntryPath ) );
91
      }
92
    } );
93
94
    // The zip file doesn't have a sane folder structure, so return the
95
    // directory where the zip file was found.
96
    return result.get();
97
  }
98
99
  /**
100
   * Processes each entry in the zip archive.
101
   *
102
   * @param zipPath  The path to the zip file being processed.
103
   * @param consumer The {@link BiConsumer} that receives each entry in the
104
   *                 zip archive.
105
   * @throws IOException Could not extract zip file entries.
106
   */
107
  private static void iterate(
108
    final Path zipPath,
109
    final BiConsumer<ZipFile, ZipEntry> consumer )
110
    throws IOException {
111
    assert toFile( zipPath ).isFile();
112
113
    try( final var zipFile = new ZipFile( toFile( zipPath ) ) ) {
114
      final var entries = zipFile.entries();
115
116
      while( entries.hasMoreElements() ) {
117
        consumer.accept( zipFile, entries.nextElement() );
118
      }
119
    }
120
  }
121
122
  /**
123
   * Extracts a single entry of a zip file to a given directory. This will
124
   * create the necessary directory path if it doesn't exist. Empty
125
   * directories are not re-created.
126
   *
127
   * @param zipFile      The zip archive to extract.
128
   * @param zipEntry     An entry in the zip archive.
129
   * @param zipEntryPath The file location to write the zip entry.
130
   * @throws IOException Could not extract the zip file entry.
131
   */
132
  private static void extract(
133
    final ZipFile zipFile,
134
    final ZipEntry zipEntry,
135
    final Path zipEntryPath ) throws IOException {
136
    // Only extract files, skip empty directories.
137
    if( !zipEntry.isDirectory() && zipEntryPath != null ) {
138
      final var parent = zipEntryPath.getParent();
139
140
      if( parent != null ) {
141
        createDirectories( parent );
142
143
        try( final var in = zipFile.getInputStream( zipEntry ) ) {
144
          Files.copy( in, zipEntryPath, REPLACE_EXISTING );
145
        }
146
      }
147
    }
148
  }
149
}
1150
A src/main/java/com/keenwrite/io/downloads/DownloadManager.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.io.downloads;
6
7
import com.keenwrite.io.MediaType;
8
import com.keenwrite.io.MediaTypeSniffer;
9
import com.keenwrite.io.SysFile;
10
import javafx.concurrent.Task;
11
12
import java.io.*;
13
import java.net.HttpURLConnection;
14
import java.net.URI;
15
import java.net.URISyntaxException;
16
import java.net.URL;
17
import java.nio.file.Paths;
18
import java.time.Duration;
19
import java.util.concurrent.Callable;
20
import java.util.zip.GZIPInputStream;
21
22
import static java.lang.Math.toIntExact;
23
import static java.lang.String.format;
24
import static java.lang.System.getProperty;
25
import static java.lang.System.setProperty;
26
import static java.net.HttpURLConnection.HTTP_OK;
27
import static java.net.HttpURLConnection.setFollowRedirects;
28
29
/**
30
 * Responsible for downloading files and publishing status updates. This will
31
 * download a resource provided by an instance of {@link URL} into a given
32
 * {@link OutputStream}.
33
 */
34
public final class DownloadManager {
35
  static {
36
    setProperty( "http.keepAlive", "false" );
37
    setFollowRedirects( true );
38
  }
39
40
  /**
41
   * Number of bytes to read at a time.
42
   */
43
  private static final int BUFFER_SIZE = 16384;
44
45
  /**
46
   * HTTP request timeout.
47
   */
48
  private static final Duration TIMEOUT = Duration.ofSeconds( 30 );
49
50
  /**
51
   * Use any of the static methods for opening by URI, URL, or string.
52
   */
53
  private DownloadManager() {}
54
55
  @FunctionalInterface
56
  public interface ProgressListener {
57
    /**
58
     * Called when a chunk of data has been read. This is called synchronously
59
     * when downloading the data; do not execute long-running tasks in this
60
     * method (a few milliseconds is fine).
61
     *
62
     * @param percentage A value between 0 and 100, inclusive, represents the
63
     *                   percentage of bytes downloaded relative to the total.
64
     *                   A value of -1 means the total number of bytes to
65
     *                   download is unknown.
66
     * @param bytes      When {@code percentage} is greater than or equal to
67
     *                   zero, this is the total number of bytes. When {@code
68
     *                   percentage} equals -1, this is the number of bytes
69
     *                   read so far.
70
     */
71
    void update( int percentage, long bytes );
72
  }
73
74
  /**
75
   * Callers may check the value of isSuccessful
76
   */
77
  public static final class DownloadToken implements Closeable {
78
    private final HttpURLConnection mConn;
79
    private final BufferedInputStream mInput;
80
    private final MediaType mMediaType;
81
    private final long mBytesTotal;
82
83
    private DownloadToken(
84
      final HttpURLConnection conn,
85
      final BufferedInputStream input,
86
      final MediaType mediaType
87
    ) {
88
      assert conn != null;
89
      assert input != null;
90
      assert mediaType != null;
91
92
      mConn = conn;
93
      mInput = input;
94
      mMediaType = mediaType;
95
      mBytesTotal = conn.getContentLength();
96
    }
97
98
    /**
99
     * Provides the ability to download remote files asynchronously while
100
     * being updated regarding the download progress. The given {@link File}
101
     * will have the contents of the URL to download upon completion.
102
     *
103
     * @param file     Where to write the file contents.
104
     * @param listener Receives download progress status updates.
105
     * @return A {@link Runnable} task that can be executed in the background
106
     * to download the resource for this {@link DownloadToken}.
107
     */
108
    public Runnable download(
109
      final File file,
110
      final ProgressListener listener ) {
111
      return () -> {
112
        final var buffer = new byte[ BUFFER_SIZE ];
113
        final var stream = getInputStream();
114
        final var bytesTotal = mBytesTotal;
115
116
        long bytesTally = 0;
117
        int bytesRead;
118
119
        try( final var output = new FileOutputStream( file ) ) {
120
          while( (bytesRead = stream.read( buffer )) != -1 ) {
121
            if( Thread.currentThread().isInterrupted() ) {
122
              throw new InterruptedException();
123
            }
124
125
            bytesTally += bytesRead;
126
127
            if( bytesTotal > 0 ) {
128
              listener.update(
129
                toIntExact( bytesTally * 100 / bytesTotal ),
130
                bytesTotal
131
              );
132
            }
133
            else {
134
              listener.update( -1, bytesRead );
135
            }
136
137
            output.write( buffer, 0, bytesRead );
138
          }
139
        } catch( final Exception ex ) {
140
          throw new RuntimeException( ex );
141
        } finally {
142
          close();
143
        }
144
      };
145
    }
146
147
    public void close() {
148
      try {
149
        getInputStream().close();
150
      } catch( final Exception ignored ) {
151
      } finally {
152
        mConn.disconnect();
153
      }
154
    }
155
156
    /**
157
     * Returns the input stream to the resource to download.
158
     *
159
     * @return The stream to read.
160
     */
161
    public BufferedInputStream getInputStream() {
162
      return mInput;
163
    }
164
165
    public MediaType getMediaType() {
166
      return mMediaType;
167
    }
168
169
    /**
170
     * Answers whether the type of content associated with the download stream
171
     * is a scalable vector graphic.
172
     *
173
     * @return {@code true} if the given {@link MediaType} has SVG contents.
174
     */
175
    public boolean isSvg() {
176
      return getMediaType().isSvg();
177
    }
178
  }
179
180
  /**
181
   * Opens the input stream for the resource to download.
182
   *
183
   * @param uri The {@link URI} resource to download.
184
   * @return A token that can be used for downloading the content with
185
   * periodic updates or retrieving the stream for downloading the content.
186
   * @throws IOException        The stream could not be opened.
187
   * @throws URISyntaxException Invalid URI.
188
   */
189
  public static DownloadToken open( final String uri )
190
    throws IOException, URISyntaxException {
191
    // Pass an undefined media type so that any type of file can be retrieved.
192
    return open( new URI( uri ) );
193
  }
194
195
  public static DownloadToken open( final URI uri )
196
    throws IOException {
197
    return open( uri.toURL() );
198
  }
199
200
  /**
201
   * Opens the input stream for the resource to download and verifies that
202
   * the given {@link MediaType} matches the requested type. Callers are
203
   * responsible for closing the {@link DownloadManager} to close the
204
   * underlying stream and the HTTP connection. Connections must be closed by
205
   * callers if {@link DownloadToken#download(File, ProgressListener)}
206
   * isn't called (i.e., {@link DownloadToken#getMediaType()} is called
207
   * after the transport layer's Content-Type is requested but not contents
208
   * are downloaded).
209
   *
210
   * @param url The {@link URL} resource to download.
211
   * @return A token that can be used for downloading the content with
212
   * periodic updates or retrieving the stream for downloading the content.
213
   * @throws IOException The resource could not be downloaded.
214
   */
215
  public static DownloadToken open( final URL url ) throws IOException {
216
    final var conn = connect( url );
217
    final var contentType = conn.getContentType();
218
219
    MediaType remoteType;
220
221
    try {
222
      remoteType = MediaType.valueFrom( contentType );
223
    } catch( final Exception ex ) {
224
      // If the media type couldn't be detected, try using the stream.
225
      remoteType = MediaType.UNDEFINED;
226
    }
227
228
    final var input = open( conn );
229
230
    // Peek at the magic header bytes to determine the media type.
231
    final var magicType = MediaTypeSniffer.getMediaType( input );
232
233
    // If the transport protocol's Content-Type doesn't align with the
234
    // media type for the magic header, defer to the transport protocol (so
235
    // long as the content type was sent from the remote side).
236
    final MediaType mediaType = remoteType.equals( magicType )
237
      ? remoteType
238
      : contentType != null && !contentType.isBlank()
239
      ? remoteType
240
      : magicType.isUndefined()
241
      ? remoteType
242
      : magicType;
243
244
    return new DownloadToken( conn, input, mediaType );
245
  }
246
247
  /**
248
   * Establishes a connection to the remote {@link URL} resource.
249
   *
250
   * @param url The {@link URL} representing a resource to download.
251
   * @return The connection manager for the {@link URL}.
252
   * @throws IOException         Could not establish a connection.
253
   * @throws ArithmeticException Could not compute a timeout value (this
254
   *                             should never happen because the timeout is
255
   *                             less than a minute).
256
   * @see #TIMEOUT
257
   */
258
  private static HttpURLConnection connect( final URL url )
259
    throws IOException, ArithmeticException {
260
    // Both HTTP and HTTPS are covered by this condition.
261
    if( url.openConnection() instanceof HttpURLConnection conn ) {
262
      conn.setUseCaches( false );
263
      conn.setInstanceFollowRedirects( true );
264
      conn.setRequestProperty( "Accept-Encoding", "gzip" );
265
      conn.setRequestProperty( "User-Agent", getProperty( "http.agent" ) );
266
      conn.setRequestMethod( "GET" );
267
      conn.setConnectTimeout( toIntExact( TIMEOUT.toMillis() ) );
268
      conn.setRequestProperty( "connection", "close" );
269
      conn.connect();
270
271
      final var code = conn.getResponseCode();
272
273
      if( code != HTTP_OK ) {
274
        final var message = format(
275
          "%s [HTTP %d: %s]",
276
          url.getFile(),
277
          code,
278
          conn.getResponseMessage()
279
        );
280
281
        throw new IOException( message );
282
      }
283
284
      return conn;
285
    }
286
287
    throw new UnsupportedOperationException( url.toString() );
288
  }
289
290
  /**
291
   * Returns a stream in an open state. Callers are responsible for closing.
292
   *
293
   * @param conn The connection to open, which could be compressed.
294
   * @return The open stream.
295
   * @throws IOException Could not open the stream.
296
   */
297
  private static BufferedInputStream open( final HttpURLConnection conn )
298
    throws IOException {
299
    return open( conn.getContentEncoding(), conn.getInputStream() );
300
  }
301
302
  /**
303
   * Returns a stream in an open state. Callers are responsible for closing.
304
   * The input stream may be compressed.
305
   *
306
   * @param encoding The content encoding for the stream.
307
   * @param is       The stream to wrap with a suitable decoder.
308
   * @return The open stream, with any gzip content-encoding decoded.
309
   * @throws IOException Could not open the stream.
310
   */
311
  private static BufferedInputStream open(
312
    final String encoding, final InputStream is ) throws IOException {
313
    return new BufferedInputStream(
314
      "gzip".equalsIgnoreCase( encoding )
315
        ? new GZIPInputStream( is )
316
        : is
317
    );
318
  }
319
320
  public static <T> Task<T> createTask( final Callable<T> callable ) {
321
    return new Task<>() {
322
      @Override
323
      protected T call() throws Exception {
324
        return callable.call();
325
      }
326
    };
327
  }
328
329
  public static <T> Thread createThread( final Task<T> task ) {
330
    final var thread = new Thread( task );
331
    thread.setDaemon( true );
332
    return thread;
333
  }
334
335
  /**
336
   * Downloads a resource to a local file in a separate {@link Thread}.
337
   *
338
   * @param uri      The resource to download.
339
   * @param file     The destination mTarget for the resource.
340
   * @param listener Receives updates as the download proceeds.
341
   */
342
  public static Task<Void> downloadAsync(
343
    final URI uri,
344
    final File file,
345
    final ProgressListener listener ) {
346
    final Task<Void> task = createTask( () -> {
347
      try( final var token = DownloadManager.open( uri ) ) {
348
        token.download( file, listener ).run();
349
      }
350
351
      return null;
352
    } );
353
354
    createThread( task ).start();
355
    return task;
356
  }
357
358
  public static String toFilename( final URI uri ) {
359
    return toFile( uri ).getName();
360
  }
361
362
  public static File toFile( final URI uri ) {
363
    return SysFile.toFile( Paths.get( uri.getPath() ) );
364
  }
365
}
1366
A src/main/java/com/keenwrite/io/downloads/events/DownloadConnectionFailedEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.io.downloads.events;
6
7
import java.net.URL;
8
9
/**
10
 * Collates information about an HTTP connection that could not be established.
11
 */
12
public class DownloadConnectionFailedEvent extends DownloadEvent {
13
14
  private final Exception mEx;
15
16
  /**
17
   * Constructs a new event that tracks the status of downloading a file.
18
   *
19
   * @param url The {@link URL} that has triggered a download event.
20
   * @param ex  The reason the connection failed.
21
   */
22
  public DownloadConnectionFailedEvent(
23
    final URL url, final Exception ex ) {
24
    super( url );
25
    mEx = ex;
26
  }
27
28
  public static void fire( final URL url, final Exception ex ) {
29
    new DownloadConnectionFailedEvent( url, ex ).publish();
30
  }
31
32
  /**
33
   * Returns the {@link Exception} that caused this event to be published.
34
   *
35
   * @return The {@link Exception} encountered when establishing a connection.
36
   */
37
  public Exception getException() {
38
    return mEx;
39
  }
40
}
141
A src/main/java/com/keenwrite/io/downloads/events/DownloadEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.io.downloads.events;
6
7
import com.keenwrite.events.AppEvent;
8
9
import java.net.URL;
10
import java.time.Instant;
11
12
/**
13
 * The parent class to all download-related status events.
14
 */
15
public class DownloadEvent implements AppEvent {
16
17
  private final Instant mInstant = Instant.now();
18
  private final URL mUrl;
19
20
  /**
21
   * Constructs a new event that tracks the status of downloading a file.
22
   *
23
   * @param url The {@link URL} that has triggered a download event.
24
   */
25
  public DownloadEvent( final URL url ) {
26
    mUrl = url;
27
  }
28
29
  /**
30
   * Returns the download link as an instance of {@link URL}.
31
   *
32
   * @return The {@link URL} being downloaded.
33
   */
34
  public URL getUrl() {
35
    return mUrl;
36
  }
37
38
  /**
39
   * Returns the moment in time that this event was published.
40
   *
41
   * @return The published date and time.
42
   */
43
  public Instant when() {
44
    return mInstant;
45
  }
46
}
147
A src/main/java/com/keenwrite/io/downloads/events/DownloadFailedEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.io.downloads.events;
6
7
import java.net.URL;
8
9
public class DownloadFailedEvent extends DownloadEvent {
10
11
  private final int mResponseCode;
12
13
  /**
14
   * Constructs a new event that indicates downloading a file was not
15
   * successful.
16
   *
17
   * @param url          The {@link URL} that has triggered a download event.
18
   * @param responseCode The HTTP response code associated with the failure.
19
   */
20
  public DownloadFailedEvent( final URL url, final int responseCode ) {
21
    super( url );
22
23
    mResponseCode = responseCode;
24
  }
25
26
  public static void fire( final URL url, final int responseCode ) {
27
    new DownloadFailedEvent( url, responseCode ).publish();
28
  }
29
30
  /**
31
   * Returns the HTTP response code for a failed download.
32
   *
33
   * @return An HTTP response code.
34
   */
35
  public int getResponseCode() {
36
    return mResponseCode;
37
  }
38
}
139
A src/main/java/com/keenwrite/io/downloads/events/DownloadStartedEvent.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.io.downloads.events;
6
7
import java.net.URL;
8
9
/**
10
 * Collates information about a document that has started downloading.
11
 */
12
public class DownloadStartedEvent extends DownloadEvent {
13
14
  public DownloadStartedEvent( final URL url ) {
15
    super( url );
16
  }
17
18
  public static void fire( final URL url ) {
19
    new DownloadStartedEvent( url ).publish();
20
  }
21
}
122
A src/main/java/com/keenwrite/predicates/PredicateFactory.java
1
/* Copyright 2020-2021 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 final 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/AposProperty.java
1
package com.keenwrite.preferences;
2
3
import javafx.beans.property.SimpleObjectProperty;
4
import javafx.collections.ObservableList;
5
6
import java.util.LinkedHashSet;
7
import java.util.Set;
8
9
import static com.keenwrite.constants.Constants.APOS_DEFAULT;
10
import static com.keenwrite.preferences.Workspace.listProperty;
11
12
/**
13
 * Maintains a list of apostrophe encodings the user may select.
14
 */
15
public final class AposProperty extends SimpleObjectProperty<String> {
16
  /**
17
   * Ordered set of available apostrophe encodings.
18
   */
19
  private static final Set<String> sProperties = new LinkedHashSet<>();
20
21
  static {
22
    sProperties.add( "regular" );
23
    sProperties.add( "modifier" );
24
    sProperties.add( APOS_DEFAULT );
25
    sProperties.add( "aposhex" );
26
    sProperties.add( "quote" );
27
    sProperties.add( "quotehex" );
28
  }
29
30
  public AposProperty( final String property ) {
31
    super( property );
32
  }
33
34
  /**
35
   * Returns the list of available apostrophe types to use when encoding.
36
   *
37
   * @return A selection of apostrophes.
38
   */
39
  public static ObservableList<String> aposListProperty() {
40
    assert !sProperties.isEmpty();
41
42
    return listProperty( sProperties );
43
  }
44
45
  /**
46
   * Ensures that the given property name is in the property list.
47
   *
48
   * @param property Property to validate.
49
   * @return The given property was found, otherwise the default property.
50
   */
51
  private static String sanitize( final String property ) {
52
    assert property != null;
53
54
    return sProperties.contains( property ) ? property : APOS_DEFAULT;
55
  }
56
}
157
A src/main/java/com/keenwrite/preferences/AppKeys.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
import static com.keenwrite.preferences.Key.key;
5
6
/**
7
 * Responsible for defining constants used throughout the application that
8
 * represent persisted preferences.
9
 */
10
public final class AppKeys {
11
  //@formatter:off
12
  private static final Key KEY_ROOT = key( "workspace" );
13
14
  public static final Key KEY_META = key( KEY_ROOT, "meta" );
15
  public static final Key KEY_META_NAME = key( KEY_META, "name" );
16
  public static final Key KEY_META_VERSION = key( KEY_META, "version" );
17
18
  public static final Key KEY_DOC = key( KEY_ROOT, "document" );
19
  public static final Key KEY_DOC_META = key( KEY_DOC, "meta" );
20
21
  public static final Key KEY_EDITOR = key( KEY_ROOT, "editor" );
22
  public static final Key KEY_EDITOR_AUTOSAVE = key( KEY_EDITOR, "autosave" );
23
24
  public static final Key KEY_R = key( KEY_ROOT, "r" );
25
  public static final Key KEY_R_SCRIPT = key( KEY_R, "script" );
26
  public static final Key KEY_R_DIR = key( KEY_R, "dir" );
27
  public static final Key KEY_R_DELIM = key( KEY_R, "delimiter" );
28
  public static final Key KEY_R_DELIM_BEGAN = key( KEY_R_DELIM, "began" );
29
  public static final Key KEY_R_DELIM_ENDED = key( KEY_R_DELIM, "ended" );
30
31
32
  public static final Key KEY_IMAGE = key( KEY_ROOT, "images" );
33
  public static final Key KEY_CACHE_DIR = key( KEY_IMAGE, "cache" );
34
  public static final Key KEY_IMAGE_DIR = key( KEY_IMAGE, "dir" );
35
  public static final Key KEY_IMAGE_ORDER = key( KEY_IMAGE, "order" );
36
  public static final Key KEY_IMAGE_RESIZE = key( KEY_IMAGE, "resize" );
37
  public static final Key KEY_IMAGE_SERVER = key( KEY_IMAGE, "server" );
38
39
  public static final Key KEY_DEF = key( KEY_ROOT, "definition" );
40
  public static final Key KEY_DEF_PATH = key( KEY_DEF, "path" );
41
  public static final Key KEY_DEF_DELIM = key( KEY_DEF, "delimiter" );
42
  public static final Key KEY_DEF_DELIM_BEGAN = key( KEY_DEF_DELIM, "began" );
43
  public static final Key KEY_DEF_DELIM_ENDED = key( KEY_DEF_DELIM, "ended" );
44
45
  public static final Key KEY_UI = key( KEY_ROOT, "ui" );
46
47
  public static final Key KEY_UI_RECENT = key( KEY_UI, "recent" );
48
  public static final Key KEY_UI_RECENT_DIR = key( KEY_UI_RECENT, "dir" );
49
  public static final Key KEY_UI_RECENT_DOCUMENT = key( KEY_UI_RECENT, "document" );
50
  public static final Key KEY_UI_RECENT_OFFSET = key( KEY_UI_RECENT, "offset" );
51
  public static final Key KEY_UI_RECENT_DEFINITION = key( KEY_UI_RECENT, "definition" );
52
  public static final Key KEY_UI_RECENT_EXPORT = key( KEY_UI_RECENT, "export" );
53
  public static final Key KEY_UI_RECENT_OPEN = key( KEY_UI_RECENT, "files" );
54
  public static final Key KEY_UI_RECENT_OPEN_PATH = key( KEY_UI_RECENT_OPEN, "path" );
55
56
  public static final Key KEY_UI_FONT = key( KEY_UI, "font" );
57
  public static final Key KEY_UI_FONT_EDITOR = key( KEY_UI_FONT, "editor" );
58
  public static final Key KEY_UI_FONT_EDITOR_NAME = key( KEY_UI_FONT_EDITOR, "name" );
59
  public static final Key KEY_UI_FONT_EDITOR_SIZE = key( KEY_UI_FONT_EDITOR, "size" );
60
  public static final Key KEY_UI_FONT_PREVIEW = key( KEY_UI_FONT, "preview" );
61
  public static final Key KEY_UI_FONT_PREVIEW_NAME = key( KEY_UI_FONT_PREVIEW, "name" );
62
  public static final Key KEY_UI_FONT_PREVIEW_SIZE = key( KEY_UI_FONT_PREVIEW, "size" );
63
  public static final Key KEY_UI_FONT_PREVIEW_MONO = key( KEY_UI_FONT_PREVIEW, "mono" );
64
  public static final Key KEY_UI_FONT_PREVIEW_MONO_NAME = key( KEY_UI_FONT_PREVIEW_MONO, "name" );
65
  public static final Key KEY_UI_FONT_PREVIEW_MONO_SIZE = key( KEY_UI_FONT_PREVIEW_MONO, "size" );
66
  public static final Key KEY_UI_FONT_MATH = key( KEY_UI_FONT, "math" );
67
  public static final Key KEY_UI_FONT_MATH_SIZE = key( KEY_UI_FONT_MATH, "size" );
68
69
  public static final Key KEY_UI_WINDOW = key( KEY_UI, "window" );
70
  public static final Key KEY_UI_WINDOW_X = key( KEY_UI_WINDOW, "x" );
71
  public static final Key KEY_UI_WINDOW_Y = key( KEY_UI_WINDOW, "y" );
72
  public static final Key KEY_UI_WINDOW_W = key( KEY_UI_WINDOW, "width" );
73
  public static final Key KEY_UI_WINDOW_H = key( KEY_UI_WINDOW, "height" );
74
  public static final Key KEY_UI_WINDOW_MAX = key( KEY_UI_WINDOW, "maximized" );
75
  public static final Key KEY_UI_WINDOW_FULL = key( KEY_UI_WINDOW, "full" );
76
77
  public static final Key KEY_UI_SKIN = key( KEY_UI, "skin" );
78
  public static final Key KEY_UI_SKIN_SELECTION = key( KEY_UI_SKIN, "selection" );
79
  public static final Key KEY_UI_SKIN_CUSTOM = key( KEY_UI_SKIN, "custom" );
80
81
  public static final Key KEY_UI_PREVIEW = key( KEY_UI, "preview" );
82
  public static final Key KEY_UI_PREVIEW_STYLESHEET = key( KEY_UI_PREVIEW, "stylesheet" );
83
84
  public static final Key KEY_LANGUAGE = key( KEY_ROOT, "language" );
85
  public static final Key KEY_LANGUAGE_LOCALE = key( KEY_LANGUAGE, "locale" );
86
87
  public static final Key KEY_TYPESET = key( KEY_ROOT, "typeset" );
88
  public static final Key KEY_TYPESET_CONTEXT = key( KEY_TYPESET, "context" );
89
  public static final Key KEY_TYPESET_CONTEXT_FONTS = key( KEY_TYPESET_CONTEXT, "fonts" );
90
  public static final Key KEY_TYPESET_CONTEXT_FONTS_DIR = key( KEY_TYPESET_CONTEXT_FONTS, "dir" );
91
  public static final Key KEY_TYPESET_CONTEXT_THEMES = key( KEY_TYPESET_CONTEXT, "themes" );
92
  public static final Key KEY_TYPESET_CONTEXT_THEMES_PATH = key( KEY_TYPESET_CONTEXT_THEMES, "path" );
93
  public static final Key KEY_TYPESET_CONTEXT_THEME_SELECTION = key( KEY_TYPESET_CONTEXT_THEMES, "selection" );
94
  public static final Key KEY_TYPESET_CONTEXT_CLEAN = key( KEY_TYPESET_CONTEXT, "clean" );
95
  public static final Key KEY_TYPESET_CONTEXT_CHAPTERS = key( KEY_TYPESET_CONTEXT, "chapters" );
96
  public static final Key KEY_TYPESET_TYPOGRAPHY = key( KEY_TYPESET, "typography" );
97
  public static final Key KEY_TYPESET_TYPOGRAPHY_QUOTES = key( KEY_TYPESET_TYPOGRAPHY, "quotes" );
98
  public static final Key KEY_TYPESET_MODES = key( KEY_TYPESET, "modes" );
99
  public static final Key KEY_TYPESET_MODES_ENABLED = key( KEY_TYPESET_MODES, "enabled" );
100
  //@formatter:on
101
102
  /**
103
   * Only for constants, do not instantiate.
104
   */
105
  private AppKeys() { }
106
}
1107
A src/main/java/com/keenwrite/preferences/FileProperty.java
1
/* Copyright 2020-2021 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 final 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-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
import java.util.Stack;
5
import java.util.function.Consumer;
6
7
/**
8
 * Responsible for creating a type hierarchy of preference storage keys.
9
 */
10
public class Key {
11
  private final Key mParent;
12
  private final String mName;
13
14
  /**
15
   * Returns a new key with no parent.
16
   *
17
   * @param name The key name, never {@code null}.
18
   * @return The new {@link Key} instance with a name but no parent.
19
   */
20
  public static Key key( final String name ) {
21
    return key( null, name );
22
  }
23
24
  /**
25
   * Returns a new key with a given parent.
26
   *
27
   * @param parent The parent of this {@link Key}, or {@code null} if this is
28
   *               the topmost key in the chain.
29
   * @param name   The key name, never {@code null}.
30
   * @return The new {@link Key} instance with a name and parent.
31
   */
32
  public static Key key( final Key parent, final String name ) {
33
    return new Key( parent, name );
34
  }
35
36
  private Key( final Key parent, final String name ) {
37
    assert name != null;
38
    assert !name.isBlank();
39
40
    mParent = parent;
41
    mName = name;
42
  }
43
44
  /**
45
   * Answers whether more {@link Key}s exist above this one in the hierarchy.
46
   *
47
   * @return {@code true} means this {@link Key} has a parent {@link Key}.
48
   */
49
  public boolean hasParent() {
50
    return mParent != null;
51
  }
52
53
  /**
54
   * Visits every key in the hierarchy, starting at the topmost {@link Key} and
55
   * ending with the current {@link Key}.
56
   *
57
   * @param consumer  Receives the name of each visited node.
58
   * @param separator Characters to insert between each node.
59
   */
60
  public void walk( final Consumer<String> consumer, final String separator ) {
61
    var key = this;
62
63
    final var stack = new Stack<String>();
64
65
    while( key != null ) {
66
      stack.push( key.name() );
67
      key = key.parent();
68
    }
69
70
    var sep = "";
71
72
    while( !stack.empty() ) {
73
      consumer.accept( sep + stack.pop() );
74
      sep = separator;
75
    }
76
  }
77
78
  public void walk( final Consumer<String> consumer ) {
79
    walk( consumer, "" );
80
  }
81
82
  public Key parent() {
83
    return mParent;
84
  }
85
86
  public String name() {
87
    return mName;
88
  }
89
90
  /**
91
   * Returns a dot-separated path representing the key's name.
92
   *
93
   * @return The dot-separated key name.
94
   */
95
  @Override
96
  public String toString() {
97
    final var sb = new StringBuilder( 128 );
98
99
    walk( sb::append, "." );
100
101
    return sb.toString();
102
  }
103
}
1104
A src/main/java/com/keenwrite/preferences/LocaleProperty.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
import javafx.beans.property.SimpleObjectProperty;
5
import javafx.collections.ObservableList;
6
7
import java.util.LinkedHashMap;
8
import java.util.Locale;
9
import java.util.Map;
10
import java.util.Objects;
11
12
import static com.keenwrite.constants.Constants.LOCALE_DEFAULT;
13
import static com.keenwrite.preferences.Workspace.listProperty;
14
import static java.util.Locale.forLanguageTag;
15
16
/**
17
 * Responsible for providing a list of locales from which the user may pick.
18
 */
19
public final class LocaleProperty extends SimpleObjectProperty<String> {
20
21
  /**
22
   * The {@link Locale}s are used for multiple purposes, including:
23
   *
24
   * <ul>
25
   *   <li>supported text editor font listing in preferences dialog;</li>
26
   *   <li>text editor CSS;</li>
27
   *   <li>preview window CSS; and</li>
28
   *   <li>lexicon to load for spellcheck.</li>
29
   * </ul>
30
   *
31
   * When the Markdown and preview CSS files are loaded, a general file is
32
   * first loaded, then a specific file is loaded according to the locale.
33
   * The specific file overrides font families so that different languages
34
   * may be presented.
35
   * <p>
36
   * Using an instance of {@link LinkedHashMap} preserves display order.
37
   * </p>
38
   * <p>
39
   * See
40
   * <a href="https://www.oracle.com/java/technologies/javase/jdk19-suported-locales.html">
41
   * JDK 19 Supported Locales
42
   * </a> for details.
43
   * </p>
44
   */
45
  private static final Map<String, Locale> sLocales = new LinkedHashMap<>();
46
47
  static {
48
    @SuppressWarnings( "SpellCheckingInspection" )
49
    final String[] tags = {
50
      // English
51
      "en-Latn-AU",
52
      "en-Latn-CA",
53
      "en-Latn-GB",
54
      "en-Latn-NZ",
55
      "en-Latn-US",
56
      "en-Latn-ZA",
57
      // German
58
      "de-Latn-AT",
59
      "de-Latn-DE",
60
      "de-Latn-LU",
61
      "de-Latn-CH",
62
      // Spanish
63
      "es-Latn-AR",
64
      "es-Latn-BO",
65
      "es-Latn-CL",
66
      "es-Latn-CO",
67
      "es-Latn-CR",
68
      "es-Latn-DO",
69
      "es-Latn-EC",
70
      "es-Latn-SV",
71
      "es-Latn-GT",
72
      "es-Latn-HN",
73
      "es-Latn-MX",
74
      "es-Latn-NI",
75
      "es-Latn-PA",
76
      "es-Latn-PY",
77
      "es-Latn-PE",
78
      "es-Latn-PR",
79
      "es-Latn-ES",
80
      "es-Latn-US",
81
      "es-Latn-UY",
82
      "es-Latn-VE",
83
      // French
84
      "fr-Latn-BE",
85
      "fr-Latn-CA",
86
      "fr-Latn-FR",
87
      "fr-Latn-LU",
88
      "fr-Latn-CH",
89
      // Hebrew
90
      //"iw-Hebr-IL",
91
      // Italian
92
      "it-Latn-IT",
93
      "it-Latn-CH",
94
      // Japanese
95
      "ja-Jpan-JP",
96
      // Korean
97
      "ko-Kore-KR",
98
      // Chinese
99
      "zh-Hans-CN",
100
      "zh-Hans-SG",
101
      "zh-Hant-HK",
102
      "zh-Hant-TW",
103
    };
104
105
    for( final var tag : tags ) {
106
      final var locale = forLanguageTag( tag );
107
      sLocales.put( locale.getDisplayName(), locale );
108
    }
109
  }
110
111
  public LocaleProperty( final Locale locale ) {
112
    super( sanitize( locale ).getDisplayName() );
113
  }
114
115
  public static String parseLocale( final String languageTag ) {
116
    final var locale = forLanguageTag( languageTag );
117
    final var key = getKey( sLocales, locale );
118
    return key == null ? LOCALE_DEFAULT.getDisplayName() : key;
119
  }
120
121
  public static String toLanguageTag( final String displayName ) {
122
    return sLocales.getOrDefault( displayName, LOCALE_DEFAULT ).toLanguageTag();
123
  }
124
125
  public Locale toLocale() {
126
    return sLocales.getOrDefault( getValue(), LOCALE_DEFAULT );
127
  }
128
129
  private static Locale sanitize( final Locale locale ) {
130
    // If the language is undefined then use the default locale.
131
    return locale == null || "und".equalsIgnoreCase( locale.toLanguageTag() )
132
      ? LOCALE_DEFAULT
133
      : locale;
134
  }
135
136
  public static ObservableList<String> localeListProperty() {
137
    return listProperty( sLocales.keySet() );
138
  }
139
140
  /**
141
   * Performs an O(n) search through the given map to find the key that is
142
   * mapped to the given value. A bidirectional map would be faster, but
143
   * also introduces additional dependencies. This doesn't need to be fast
144
   * because it happens once, at start up, and there aren't a lot of values.
145
   *
146
   * @param map   The map containing a key to find based on a value.
147
   * @param value The value to find within the map.
148
   * @param <K>   The type of key associated with a value.
149
   * @param <V>   The type of value associated with a key.
150
   * @return The key that corresponds to the given value, or {@code null} if
151
   * the key is not found.
152
   */
153
  @SuppressWarnings( "SameParameterValue" )
154
  private static <K, V> K getKey( final Map<K, V> map, final V value ) {
155
    for( final var entry : map.entrySet() ) {
156
      if( Objects.equals( value, entry.getValue() ) ) {
157
        return entry.getKey();
158
      }
159
    }
160
161
    return null;
162
  }
163
}
1164
A src/main/java/com/keenwrite/preferences/LocaleScripts.java
1
/* Copyright 2020-2021 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 final 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-2021 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.model.Category;
7
import com.dlsc.preferencesfx.model.Group;
8
import com.dlsc.preferencesfx.model.Setting;
9
import com.dlsc.preferencesfx.util.StorageHandler;
10
import com.dlsc.preferencesfx.view.NavigationView;
11
import javafx.beans.property.*;
12
import javafx.scene.Node;
13
import javafx.scene.control.Button;
14
import javafx.scene.control.DialogPane;
15
import javafx.scene.control.Label;
16
import org.controlsfx.control.MasterDetailPane;
17
18
import java.io.File;
19
import java.util.Map.Entry;
20
21
import static com.dlsc.formsfx.model.structure.Field.ofStringType;
22
import static com.dlsc.preferencesfx.PreferencesFxEvent.EVENT_PREFERENCES_SAVED;
23
import static com.keenwrite.Messages.get;
24
import static com.keenwrite.constants.Constants.USER_DIRECTORY;
25
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
26
import static com.keenwrite.preferences.AposProperty.aposListProperty;
27
import static com.keenwrite.preferences.AppKeys.*;
28
import static com.keenwrite.preferences.LocaleProperty.localeListProperty;
29
import static com.keenwrite.preferences.SkinProperty.skinListProperty;
30
import static com.keenwrite.preferences.TableField.ofListType;
31
import static javafx.scene.control.ButtonType.CANCEL;
32
import static javafx.scene.control.ButtonType.OK;
33
34
/**
35
 * Provides the ability for users to configure their preferences. This links
36
 * the {@link Workspace} model with the {@link PreferencesFx} view, in MVC.
37
 */
38
@SuppressWarnings( "SameParameterValue" )
39
public final class PreferencesController {
40
41
  private final Workspace mWorkspace;
42
  private final PreferencesFx mPreferencesFx;
43
44
  public PreferencesController( final Workspace workspace ) {
45
    mWorkspace = workspace;
46
47
    // Order matters: set the workspace before creating the dialog.
48
    mPreferencesFx = createPreferencesFx();
49
50
    initKeyEventHandler( mPreferencesFx );
51
    initSaveEventHandler( mPreferencesFx );
52
  }
53
54
  /**
55
   * Display the user preferences settings dialog (non-modal).
56
   */
57
  public void show() {
58
    mPreferencesFx.show( false );
59
  }
60
61
  private StringField createFontNameField(
62
    final StringProperty fontName, final DoubleProperty fontSize ) {
63
    final var control = new SimpleFontControl( "Change" );
64
65
    control.fontSizeProperty().addListener( ( _, _, n ) -> {
66
      if( n != null ) {
67
        fontSize.set( n.doubleValue() );
68
      }
69
    } );
70
71
    return ofStringType( fontName ).render( control );
72
  }
73
74
  /**
75
   * Convenience method to create a helper class for the user interface. This
76
   * establishes a key-value pair for the view.
77
   *
78
   * @param persist A reference to the values that will be persisted.
79
   * @param <K>     The type of key, usually a string.
80
   * @param <V>     The type of value, usually a string.
81
   * @return UI data model container that may update the persistent state.
82
   */
83
  private <K, V> TableField<Entry<K, V>> createTableField(
84
    final ListProperty<Entry<K, V>> persist ) {
85
    return ofListType( persist ).render( new SimpleTableControl<>() );
86
  }
87
88
  /**
89
   * Creates the preferences dialog using {@link SkeletonStorageHandler} and
90
   * numerous {@link Category} objects.
91
   *
92
   * @return A component for editing preferences.
93
   * @throws RuntimeException Could not construct the {@link PreferencesFx}
94
   *                          object (e.g., illegal access permissions,
95
   *                          unmapped XML resource).
96
   */
97
  private PreferencesFx createPreferencesFx() {
98
    return PreferencesFx.of( createStorageHandler(), createCategories() )
99
                        .instantPersistent( false )
100
                        .dialogIcon( ICON_DIALOG );
101
  }
102
103
  /**
104
   * Override the {@link PreferencesFx} storage handler to perform no actions.
105
   * Persistence is accomplished using the {@link XmlStore}.
106
   *
107
   * @return A no-op {@link StorageHandler} implementation.
108
   */
109
  private StorageHandler createStorageHandler() {
110
    return new SkeletonStorageHandler();
111
  }
112
113
  private Category[] createCategories() {
114
    return new Category[]{
115
      Category.of(
116
        get( KEY_DOC ),
117
        Group.of(
118
          get( KEY_DOC_META ),
119
          Setting.of( label( KEY_DOC_META ) ),
120
          Setting.of( title( KEY_DOC_META ),
121
                      createTableField( listEntryProperty( KEY_DOC_META ) ),
122
                      listEntryProperty( KEY_DOC_META ) )
123
        )
124
      ),
125
      Category.of(
126
        get( KEY_TYPESET ),
127
        Group.of(
128
          get( KEY_TYPESET_CONTEXT ),
129
          Setting.of( label( KEY_TYPESET_CONTEXT_THEMES_PATH ) ),
130
          Setting.of( title( KEY_TYPESET_CONTEXT_THEMES_PATH ),
131
                      directoryProperty( KEY_TYPESET_CONTEXT_THEMES_PATH ),
132
                      true ),
133
          Setting.of( label( KEY_TYPESET_CONTEXT_CLEAN ) ),
134
          Setting.of( title( KEY_TYPESET_CONTEXT_CLEAN ),
135
                      booleanProperty( KEY_TYPESET_CONTEXT_CLEAN ) )
136
        ),
137
        Group.of(
138
          get( KEY_TYPESET_CONTEXT_FONTS ),
139
          Setting.of( label( KEY_TYPESET_CONTEXT_FONTS_DIR ) ),
140
          Setting.of( title( KEY_TYPESET_CONTEXT_FONTS_DIR ),
141
                      directoryProperty( KEY_TYPESET_CONTEXT_FONTS_DIR ),
142
                      true )
143
        ),
144
        Group.of(
145
          get( KEY_TYPESET_TYPOGRAPHY ),
146
          Setting.of( label( KEY_TYPESET_TYPOGRAPHY_QUOTES ) ),
147
          Setting.of( title( KEY_TYPESET_TYPOGRAPHY_QUOTES ),
148
                      aposListProperty(),
149
                      listProperty( KEY_TYPESET_TYPOGRAPHY_QUOTES ) )
150
        ),
151
        Group.of(
152
          get( KEY_TYPESET_MODES ),
153
          Setting.of( label( KEY_TYPESET_MODES_ENABLED ) ),
154
          Setting.of( title( KEY_TYPESET_MODES_ENABLED ),
155
                      stringProperty( KEY_TYPESET_MODES_ENABLED ) )
156
        )
157
      ),
158
      Category.of(
159
        get( KEY_EDITOR ),
160
        Group.of(
161
          get( KEY_EDITOR_AUTOSAVE ),
162
          Setting.of( label( KEY_EDITOR_AUTOSAVE ) ),
163
          Setting.of( title( KEY_EDITOR_AUTOSAVE ),
164
                      integerProperty( KEY_EDITOR_AUTOSAVE ) )
165
        )
166
      ),
167
      Category.of(
168
        get( KEY_R ),
169
        Group.of(
170
          get( KEY_R_DIR ),
171
          Setting.of( label( KEY_R_DIR ) ),
172
          Setting.of( title( KEY_R_DIR ),
173
                      directoryProperty( KEY_R_DIR ),
174
                      true )
175
        ),
176
        Group.of(
177
          get( KEY_R_SCRIPT ),
178
          Setting.of( label( KEY_R_SCRIPT ) ),
179
          createMultilineSetting( "Script", KEY_R_SCRIPT )
180
        ),
181
        Group.of(
182
          get( KEY_R_DELIM_BEGAN ),
183
          Setting.of( label( KEY_R_DELIM_BEGAN ) ),
184
          Setting.of( title( KEY_R_DELIM_BEGAN ),
185
                      stringProperty( KEY_R_DELIM_BEGAN ) )
186
        ),
187
        Group.of(
188
          get( KEY_R_DELIM_ENDED ),
189
          Setting.of( label( KEY_R_DELIM_ENDED ) ),
190
          Setting.of( title( KEY_R_DELIM_ENDED ),
191
                      stringProperty( KEY_R_DELIM_ENDED ) )
192
        )
193
      ),
194
      Category.of(
195
        get( KEY_IMAGE ),
196
        Group.of(
197
          get( KEY_IMAGE_DIR ),
198
          Setting.of( label( KEY_IMAGE_DIR ) ),
199
          Setting.of( title( KEY_IMAGE_DIR ),
200
                      directoryProperty( KEY_IMAGE_DIR ),
201
                      true ),
202
          Setting.of( label( KEY_CACHE_DIR ) ),
203
          Setting.of( title( KEY_CACHE_DIR ),
204
                      directoryProperty( KEY_CACHE_DIR ),
205
                      true )
206
        ),
207
        Group.of(
208
          get( KEY_IMAGE_ORDER ),
209
          Setting.of( label( KEY_IMAGE_ORDER ) ),
210
          Setting.of( title( KEY_IMAGE_ORDER ),
211
                      stringProperty( KEY_IMAGE_ORDER ) )
212
        ),
213
        Group.of(
214
          get( KEY_IMAGE_RESIZE ),
215
          Setting.of( label( KEY_IMAGE_RESIZE ) ),
216
          Setting.of( title( KEY_IMAGE_RESIZE ),
217
                      booleanProperty( KEY_IMAGE_RESIZE ) )
218
        ),
219
        Group.of(
220
          get( KEY_IMAGE_SERVER ),
221
          Setting.of( label( KEY_IMAGE_SERVER ) ),
222
          Setting.of( title( KEY_IMAGE_SERVER ),
223
                      stringProperty( KEY_IMAGE_SERVER ) )
224
        )
225
      ),
226
      Category.of(
227
        get( KEY_DEF ),
228
        Group.of(
229
          get( KEY_DEF_PATH ),
230
          Setting.of( label( KEY_DEF_PATH ) ),
231
          Setting.of( title( KEY_DEF_PATH ),
232
                      fileProperty( KEY_DEF_PATH ), false )
233
        ),
234
        Group.of(
235
          get( KEY_DEF_DELIM_BEGAN ),
236
          Setting.of( label( KEY_DEF_DELIM_BEGAN ) ),
237
          Setting.of( title( KEY_DEF_DELIM_BEGAN ),
238
                      stringProperty( KEY_DEF_DELIM_BEGAN ) )
239
        ),
240
        Group.of(
241
          get( KEY_DEF_DELIM_ENDED ),
242
          Setting.of( label( KEY_DEF_DELIM_ENDED ) ),
243
          Setting.of( title( KEY_DEF_DELIM_ENDED ),
244
                      stringProperty( KEY_DEF_DELIM_ENDED ) )
245
        )
246
      ),
247
      Category.of(
248
        get( KEY_UI_FONT ),
249
        Group.of(
250
          get( KEY_UI_FONT_EDITOR ),
251
          Setting.of( label( KEY_UI_FONT_EDITOR_NAME ) ),
252
          Setting.of( title( KEY_UI_FONT_EDITOR_NAME ),
253
                      createFontNameField(
254
                        stringProperty( KEY_UI_FONT_EDITOR_NAME ),
255
                        doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) ),
256
                      stringProperty( KEY_UI_FONT_EDITOR_NAME ) ),
257
          Setting.of( label( KEY_UI_FONT_EDITOR_SIZE ) ),
258
          Setting.of( title( KEY_UI_FONT_EDITOR_SIZE ),
259
                      doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) )
260
        ),
261
        Group.of(
262
          get( KEY_UI_FONT_PREVIEW ),
263
          Setting.of( label( KEY_UI_FONT_PREVIEW_NAME ) ),
264
          Setting.of( title( KEY_UI_FONT_PREVIEW_NAME ),
265
                      createFontNameField(
266
                        stringProperty( KEY_UI_FONT_PREVIEW_NAME ),
267
                        doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
268
                      stringProperty( KEY_UI_FONT_PREVIEW_NAME ) ),
269
          Setting.of( label( KEY_UI_FONT_PREVIEW_SIZE ) ),
270
          Setting.of( title( KEY_UI_FONT_PREVIEW_SIZE ),
271
                      doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
272
          Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
273
          Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_NAME ),
274
                      createFontNameField(
275
                        stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ),
276
                        doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
277
                      stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
278
          Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
279
          Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_SIZE ),
280
                      doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) )
281
        ),
282
        Group.of(
283
          get( KEY_UI_FONT_MATH ),
284
          Setting.of( title( KEY_UI_FONT_MATH_SIZE ),
285
                      doubleProperty( KEY_UI_FONT_MATH_SIZE ) )
286
        )
287
      ),
288
      Category.of(
289
        get( KEY_UI_SKIN ),
290
        Group.of(
291
          get( KEY_UI_SKIN_SELECTION ),
292
          Setting.of( label( KEY_UI_SKIN_SELECTION ) ),
293
          Setting.of( title( KEY_UI_SKIN_SELECTION ),
294
                      skinListProperty(),
295
                      listProperty( KEY_UI_SKIN_SELECTION ) )
296
        ),
297
        Group.of(
298
          get( KEY_UI_SKIN_CUSTOM ),
299
          Setting.of( label( KEY_UI_SKIN_CUSTOM ) ),
300
          Setting.of( title( KEY_UI_SKIN_CUSTOM ),
301
                      fileProperty( KEY_UI_SKIN_CUSTOM ), false )
302
        )
303
      ),
304
      Category.of(
305
        get( KEY_UI_PREVIEW ),
306
        Group.of(
307
          get( KEY_UI_PREVIEW_STYLESHEET ),
308
          Setting.of( label( KEY_UI_PREVIEW_STYLESHEET ) ),
309
          Setting.of( title( KEY_UI_PREVIEW_STYLESHEET ),
310
                      fileProperty( KEY_UI_PREVIEW_STYLESHEET ), false )
311
        )
312
      ),
313
      Category.of(
314
        get( KEY_LANGUAGE ),
315
        Group.of(
316
          get( KEY_LANGUAGE_LOCALE ),
317
          Setting.of( label( KEY_LANGUAGE_LOCALE ) ),
318
          Setting.of( title( KEY_LANGUAGE_LOCALE ),
319
                      localeListProperty(),
320
                      localeProperty( KEY_LANGUAGE_LOCALE ) )
321
        )
322
      )
323
    };
324
  }
325
326
  @SuppressWarnings( "unchecked" )
327
  private Setting<StringField, StringProperty> createMultilineSetting(
328
    final String description, final Key property ) {
329
    final Setting<StringField, StringProperty> setting =
330
      Setting.of( description, stringProperty( property ) );
331
    final var field = setting.getElement();
332
    field.multiline( true );
333
334
    return setting;
335
  }
336
337
  /**
338
   * Map ENTER and ESCAPE keys to OK and CANCEL buttons, respectively.
339
   */
340
  private void initKeyEventHandler( final PreferencesFx preferences ) {
341
    final var view = preferences.getView();
342
    final var nodes = view.getChildrenUnmodifiable();
343
    final var master = (MasterDetailPane) nodes.getFirst();
344
    final var detail = (NavigationView) master.getDetailNode();
345
    final var pane = (DialogPane) view.getParent();
346
347
    detail.setOnKeyReleased( key -> {
348
      switch( key.getCode() ) {
349
        case ENTER -> ((Button) pane.lookupButton( OK )).fire();
350
        case ESCAPE -> ((Button) pane.lookupButton( CANCEL )).fire();
351
        default -> {}
352
      }
353
    } );
354
  }
355
356
  /**
357
   * Called when the user clicks the APPLY or OK buttons in the dialog.
358
   *
359
   * @param preferences Preferences widget.
360
   */
361
  private void initSaveEventHandler( final PreferencesFx preferences ) {
362
    preferences.addEventHandler(
363
      EVENT_PREFERENCES_SAVED, _ -> mWorkspace.save()
364
    );
365
  }
366
367
  /**
368
   * Creates a label for the given key after interpolating its value.
369
   *
370
   * @param key The key to find in the resource bundle.
371
   * @return The value of the key as a label.
372
   */
373
  private Node label( final Key key ) {
374
    return label( key, (String[]) null );
375
  }
376
377
  private Node label( final Key key, final String... values ) {
378
    return new Label( get( String.format( "%s%s", key.toString(), ".desc" ), (Object[]) values ) );
379
  }
380
381
  private String title( final Key key ) {
382
    return get( String.format( "%s%s", key.toString(), ".title" ) );
383
  }
384
385
  /**
386
   * Screens out non-existent directories to avoid throwing an exception caused
387
   * by
388
   * <a href="https://github.com/dlsc-software-consulting-gmbh/PreferencesFX/issues/441">
389
   * PreferencesFX issue #441
390
   * </a>.
391
   *
392
   * @param key Preference to pre-screen before creating a {@link FileProperty}.
393
   * @return The preferred value or the user's home directory if the directory
394
   * does not exist.
395
   */
396
  private ObjectProperty<File> directoryProperty( final Key key ) {
397
    final var property = mWorkspace.fileProperty( key );
398
    final var file = property.get();
399
400
    if( !file.exists() ) {
401
      property.set( USER_DIRECTORY );
402
    }
403
404
    return property;
405
  }
406
407
  private ObjectProperty<File> fileProperty( final Key key ) {
408
    return mWorkspace.fileProperty( key );
409
  }
410
411
  private StringProperty stringProperty( final Key key ) {
412
    return mWorkspace.stringProperty( key );
413
  }
414
415
  private BooleanProperty booleanProperty( final Key key ) {
416
    return mWorkspace.booleanProperty( key );
417
  }
418
419
  private IntegerProperty integerProperty( final Key key ) {
420
    return mWorkspace.integerProperty( key );
421
  }
422
423
  private DoubleProperty doubleProperty( final Key key ) {
424
    return mWorkspace.doubleProperty( key );
425
  }
426
427
  private ObjectProperty<String> listProperty( final Key key ) {
428
    return mWorkspace.listProperty( key );
429
  }
430
431
  private ObjectProperty<String> localeProperty( final Key key ) {
432
    return mWorkspace.localeProperty( key );
433
  }
434
435
  private <K, V> ListProperty<Entry<K, V>> listEntryProperty( final Key key ) {
436
    return mWorkspace.listsProperty( key );
437
  }
438
}
1439
A src/main/java/com/keenwrite/preferences/SimpleFontControl.java
1
/* Copyright 2021 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.formsfx.view.controls.SimpleControl;
6
import javafx.beans.property.DoubleProperty;
7
import javafx.beans.property.SimpleDoubleProperty;
8
import javafx.scene.control.Button;
9
import javafx.scene.control.ListView;
10
import javafx.scene.control.TextField;
11
import javafx.scene.input.KeyEvent;
12
import javafx.scene.layout.HBox;
13
import javafx.scene.layout.Region;
14
import javafx.scene.layout.StackPane;
15
import javafx.scene.text.Font;
16
import javafx.stage.Stage;
17
import org.controlsfx.dialog.FontSelectorDialog;
18
19
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
20
import static com.keenwrite.events.StatusEvent.clue;
21
import static java.lang.System.currentTimeMillis;
22
import static javafx.geometry.Pos.CENTER_LEFT;
23
import static javafx.scene.control.ButtonType.CANCEL;
24
import static javafx.scene.control.ButtonType.OK;
25
import static javafx.scene.input.KeyCode.ENTER;
26
import static javafx.scene.input.KeyCode.ESCAPE;
27
import static javafx.scene.layout.Priority.ALWAYS;
28
import static javafx.scene.text.Font.font;
29
import static javafx.scene.text.Font.getDefault;
30
31
/**
32
 * Responsible for provide users the ability to select a font using a friendly
33
 * font dialog.
34
 */
35
public class SimpleFontControl extends SimpleControl<StringField, StackPane> {
36
  private final Button mButton = new Button();
37
  private final String mButtonText;
38
  private final DoubleProperty mFontSize = new SimpleDoubleProperty();
39
  private final TextField mFontName = new TextField();
40
41
  public SimpleFontControl( final String buttonText ) {
42
    mButtonText = buttonText;
43
  }
44
45
  @Override
46
  public void initializeParts() {
47
    super.initializeParts();
48
49
    mFontName.setText( field.getValue() );
50
    mFontName.setPromptText( field.placeholderProperty().getValue() );
51
52
    final var fieldProperty = field.valueProperty();
53
    if( fieldProperty.get().equals( "null" ) ) {
54
      fieldProperty.set( "" );
55
    }
56
57
    mButton.setText( mButtonText );
58
    mButton.setOnAction( event -> {
59
      final var selected = !fieldProperty.get().trim().isEmpty();
60
      var initialFont = getDefault();
61
      if( selected ) {
62
        final var previousValue = fieldProperty.get();
63
        initialFont = font( previousValue );
64
      }
65
66
      createFontSelectorDialog( initialFont )
67
        .showAndWait()
68
        .ifPresent( font -> {
69
          mFontName.setText( font.getFamily() );
70
          mFontSize.set( font.getSize() );
71
        } );
72
    } );
73
74
    node = new StackPane();
75
  }
76
77
  @Override
78
  public void layoutParts() {
79
    node.getStyleClass().add( "simple-text-control" );
80
    fieldLabel.getStyleClass().addAll( field.getStyleClass() );
81
    fieldLabel.getStyleClass().add( "read-only-label" );
82
83
    final var box = new HBox();
84
    HBox.setHgrow( mFontName, ALWAYS );
85
    box.setAlignment( CENTER_LEFT );
86
    box.getChildren().addAll( fieldLabel, mFontName, mButton );
87
88
    node.getChildren().add( box );
89
  }
90
91
  @Override
92
  public void setupBindings() {
93
    super.setupBindings();
94
    mFontName.textProperty().bindBidirectional( field.userInputProperty() );
95
  }
96
97
  public DoubleProperty fontSizeProperty() {
98
    return mFontSize;
99
  }
100
101
  /**
102
   * Creates a dialog that displays a list of available font families,
103
   * sizes, and a button for font selection.
104
   *
105
   * @param font The default font to select initially.
106
   * @return A dialog to help the user select a different {@link Font}.
107
   */
108
  private FontSelectorDialog createFontSelectorDialog( final Font font ) {
109
    final var dialog = new FontSelectorDialog( font );
110
    final var pane = dialog.getDialogPane();
111
    final var buttonOk = (Button) pane.lookupButton( OK );
112
    final var buttonCancel = (Button) pane.lookupButton( CANCEL );
113
114
    buttonOk.setDefaultButton( true );
115
    buttonCancel.setCancelButton( true );
116
    pane.setOnKeyReleased( keyEvent -> {
117
      switch( keyEvent.getCode() ) {
118
        case ENTER -> buttonOk.fire();
119
        case ESCAPE -> buttonCancel.fire();
120
        default -> { }
121
      }
122
    } );
123
124
    final var stage = (Stage) pane.getScene().getWindow();
125
    stage.getIcons().add( ICON_DIALOG );
126
127
    final var frontPanel = (Region) pane.getContent();
128
    for( final var node : frontPanel.getChildrenUnmodifiable() ) {
129
      if( node instanceof final ListView<?> listView ) {
130
        final var handler = new ListViewHandler<>( listView );
131
        listView.setOnKeyPressed( handler::handle );
132
      }
133
    }
134
135
    return dialog;
136
  }
137
138
  /**
139
   * Responsible for handling key presses when selecting a font. Based on
140
   * <a href="https://stackoverflow.com/a/43604223/59087">Martin Široký</a>'s
141
   * answer.
142
   *
143
   * @param <T> The type of {@link ListView} to search.
144
   */
145
  private static final class ListViewHandler<T> {
146
    /**
147
     * Amount of time to wait between key presses that typing a subsequent
148
     * key is considered part of the same search, in milliseconds.
149
     */
150
    private static final int RESET_DELAY_MS = 1250;
151
152
    private String mNeedle = "";
153
    private int mSearchSkip = 0;
154
    private long mLastTyped = currentTimeMillis();
155
    private final ListView<T> mHaystack;
156
157
    private ListViewHandler( final ListView<T> listView ) {
158
      mHaystack = listView;
159
    }
160
161
    private void handle( final KeyEvent key ) {
162
      var ch = key.getText();
163
      final var code = key.getCode();
164
165
      if( ch == null || ch.isEmpty() || code == ESCAPE || code == ENTER ) {
166
        return;
167
      }
168
169
      ch = ch.toUpperCase();
170
171
      if( mNeedle.equals( ch ) ) {
172
        mSearchSkip++;
173
      }
174
      else {
175
        mNeedle = currentTimeMillis() - mLastTyped > RESET_DELAY_MS
176
          ? ch : mNeedle + ch;
177
      }
178
179
      mLastTyped = currentTimeMillis();
180
181
      boolean found = false;
182
      int skipped = 0;
183
184
      for( final T item : mHaystack.getItems() ) {
185
        final var straw = item.toString().toUpperCase();
186
187
        if( straw.startsWith( mNeedle ) ) {
188
          if( mSearchSkip > skipped ) {
189
            skipped++;
190
            continue;
191
          }
192
193
          mHaystack.getSelectionModel().select( item );
194
          final int index = mHaystack.getSelectionModel().getSelectedIndex();
195
          mHaystack.getFocusModel().focus( index );
196
          mHaystack.scrollTo( index );
197
          found = true;
198
          break;
199
        }
200
      }
201
202
      if( !found ) {
203
        clue( "Main.status.font.search.missing", mNeedle );
204
        mSearchSkip = 0;
205
      }
206
    }
207
  }
208
}
1209
A src/main/java/com/keenwrite/preferences/SimpleTableControl.java
1
/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
import com.dlsc.preferencesfx.formsfx.view.controls.SimpleControl;
5
import com.keenwrite.ui.cells.AltTableCell;
6
import javafx.beans.property.SimpleObjectProperty;
7
import javafx.event.ActionEvent;
8
import javafx.event.EventHandler;
9
import javafx.geometry.Insets;
10
import javafx.scene.control.Button;
11
import javafx.scene.control.ButtonBar;
12
import javafx.scene.control.TableColumn;
13
import javafx.scene.control.TableColumn.CellEditEvent;
14
import javafx.scene.control.TableView;
15
import javafx.scene.layout.VBox;
16
import javafx.util.StringConverter;
17
18
import java.util.AbstractMap.SimpleEntry;
19
import java.util.ArrayList;
20
import java.util.Map.Entry;
21
import java.util.function.BiFunction;
22
import java.util.function.Function;
23
24
import static com.keenwrite.ui.fonts.IconFactory.createGraphic;
25
import static java.util.Arrays.asList;
26
import static javafx.scene.control.SelectionMode.MULTIPLE;
27
import static javafx.scene.control.TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN;
28
29
public class SimpleTableControl<K, V, F extends TableField<Entry<K, V>>>
30
  extends SimpleControl<F, VBox> {
31
32
  private static long sCounter;
33
34
  public SimpleTableControl() { }
35
36
  @Override
37
  public void initializeParts() {
38
    super.initializeParts();
39
40
    final var field = getField();
41
    final var table = field.createTableView();
42
43
    table.setColumnResizePolicy( CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN );
44
    table.setEditable( true );
45
    table.getColumns().addAll(
46
      asList(
47
        createEditableColumnKey( table ),
48
        createEditableColumnValue( table )
49
      )
50
    );
51
    table.getSelectionModel().setSelectionMode( MULTIPLE );
52
53
    final var buttons = new ButtonBar();
54
    buttons.getButtons().addAll(
55
      createButton(
56
        "Add", "PLUS",
57
        event -> {
58
          sCounter++;
59
60
          field.add( createEntry( "key" + sCounter, "value" + sCounter ) );
61
        }
62
      ),
63
64
      createButton(
65
        "Delete", "TRASH",
66
        event -> {
67
          final var selectionModel = table.getSelectionModel();
68
          final var selection = selectionModel.getSelectedItems();
69
70
          if( selection != null && !selection.isEmpty() ) {
71
            final var items = table.getItems();
72
            final var rows = new ArrayList<>( selection );
73
            rows.forEach( items::remove );
74
75
            selectionModel.clearSelection();
76
          }
77
        }
78
      )
79
    );
80
81
    final var vbox = new VBox();
82
    vbox.setSpacing( 5 );
83
    vbox.setPadding( new Insets( 10, 0, 0, 10 ) );
84
    vbox.getChildren().addAll( table, buttons );
85
86
    super.node = vbox;
87
  }
88
89
  @SuppressWarnings( "unchecked" )
90
  private Entry<K, V> createEntry( final String k, final String v ) {
91
    return new SimpleEntry<>( (K) k, (V) v );
92
  }
93
94
  private Button createButton(
95
    final String label,
96
    final String graphic,
97
    final EventHandler<ActionEvent> handler ) {
98
    assert label != null;
99
    assert !label.isBlank();
100
    assert graphic != null;
101
    assert !graphic.isBlank();
102
    assert handler != null;
103
104
    final var button = new Button( label, createGraphic( graphic ) );
105
    button.setOnAction( handler );
106
    return button;
107
  }
108
109
  private TableColumn<Entry<K, V>, K> createEditableColumnKey(
110
    final TableView<Entry<K, V>> table ) {
111
    return createColumn(
112
      table,
113
      Entry::getKey,
114
      ( e, o ) -> new SimpleEntry<>( e.getNewValue(), o.getValue() ),
115
      "Key",
116
      .2
117
    );
118
  }
119
120
  private TableColumn<Entry<K, V>, V> createEditableColumnValue(
121
    final TableView<Entry<K, V>> table ) {
122
    return createColumn(
123
      table,
124
      Entry::getValue,
125
      ( e, o ) -> new SimpleEntry<>( o.getKey(), e.getNewValue() ),
126
      "Value",
127
      .8
128
    );
129
  }
130
131
  /**
132
   * Creates a table column having cells that be edited.
133
   *
134
   * @param table    The table to which the column belongs.
135
   * @param mapEntry Data model backing the edited text.
136
   * @param label    Column name.
137
   * @param width    Fraction of table width (1 = 100%).
138
   * @param <T>      The return type for the column (i.e., key or value).
139
   * @return The newly configured column.
140
   */
141
  private <T, E extends Entry<K, V>> TableColumn<E, T> createColumn(
142
    final TableView<E> table,
143
    final Function<E, T> mapEntry,
144
    final BiFunction<CellEditEvent<E, T>, E, E> creator,
145
    final String label,
146
    final double width
147
  ) {
148
    final var column = new TableColumn<E, T>( label );
149
150
    column.setEditable( true );
151
    column.setResizable( true );
152
    column.prefWidthProperty().bind( table.widthProperty().multiply( width ) );
153
154
    column.setOnEditCommit( event -> {
155
      final var index = event.getTablePosition().getRow();
156
      final var view = event.getTableView();
157
      final var old = view.getItems().get( index );
158
159
      // Update the data model with the new column value.
160
      view.getItems().set( index, creator.apply( event, old ) );
161
    } );
162
163
    column.setCellValueFactory(
164
      cellData ->
165
        new SimpleObjectProperty<>( mapEntry.apply( cellData.getValue() ) )
166
    );
167
168
    column.setCellFactory(
169
      tableColumn -> new AltTableCell<>(
170
        new StringConverter<>() {
171
          @Override
172
          public String toString( final T object ) {
173
            return object.toString();
174
          }
175
176
          @Override
177
          @SuppressWarnings( "unchecked" )
178
          public T fromString( final String string ) {
179
            return (T) string;
180
          }
181
        }
182
      )
183
    );
184
185
    return column;
186
  }
187
188
  /**
189
   * Calling {@link #initializeParts()} also performs layout because no handles
190
   * are kept to the widgets after initialization.
191
   */
192
  @Override
193
  public void layoutParts() { }
194
}
1195
A src/main/java/com/keenwrite/preferences/SkeletonStorageHandler.java
1
/* Copyright 2020-2021 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}. This
13
 * is required to change the user preferences file location and data format.
14
 *
15
 * @see XmlStore
16
 * @see Workspace
17
 */
18
public final class SkeletonStorageHandler 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
  @Override
31
  public double loadDividerPosition() {
32
    return 0;
33
  }
34
35
  @Override
36
  public void saveWindowWidth( final double windowWidth ) {}
37
38
  @Override
39
  public double loadWindowWidth() {
40
    return 0;
41
  }
42
43
  @Override
44
  public void saveWindowHeight( final double windowHeight ) {}
45
46
  @Override
47
  public double loadWindowHeight() {
48
    return 0;
49
  }
50
51
  @Override
52
  public void saveWindowPosX( final double windowPosX ) {}
53
54
  @Override
55
  public double loadWindowPosX() {
56
    return 0;
57
  }
58
59
  @Override
60
  public void saveWindowPosY( final double windowPosY ) {}
61
62
  @Override
63
  public double loadWindowPosY() {
64
    return 0;
65
  }
66
67
  @Override
68
  public void saveObject( final String breadcrumb, final Object object ) {}
69
70
  @Override
71
  public Object loadObject(
72
    final String breadcrumb, final Object defaultObject ) {
73
    return defaultObject;
74
  }
75
76
  @Override
77
  public <T> T loadObject(
78
    final String breadcrumb, final Class<T> type, final T defaultObject ) {
79
    return defaultObject;
80
  }
81
82
  @Override
83
  @SuppressWarnings( "rawtypes" )
84
  public ObservableList loadObservableList(
85
    final String breadcrumb, final ObservableList defaultObservableList ) {
86
    return defaultObservableList;
87
  }
88
89
  @Override
90
  public <T> ObservableList<T> loadObservableList(
91
    final String breadcrumb,
92
    final Class<T> type,
93
    final ObservableList<T> defaultObservableList ) {
94
    return defaultObservableList;
95
  }
96
97
  @Override
98
  public boolean clearPreferences() {
99
    return false;
100
  }
101
102
  public Preferences getPreferences() {
103
    return null;
104
  }
105
}
1106
A src/main/java/com/keenwrite/preferences/SkinProperty.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
import com.keenwrite.constants.Constants;
5
import javafx.beans.property.SimpleObjectProperty;
6
import javafx.collections.ObservableList;
7
8
import java.util.LinkedHashSet;
9
import java.util.Set;
10
11
import static com.keenwrite.constants.Constants.SKIN_DEFAULT;
12
import static com.keenwrite.preferences.Workspace.listProperty;
13
14
/**
15
 * Maintains a list of look and feels that the user may choose.
16
 */
17
public final class SkinProperty extends SimpleObjectProperty<String> {
18
  /**
19
   * Ordered set of available skins.
20
   */
21
  private static final Set<String> sProperties = new LinkedHashSet<>();
22
23
  static {
24
    sProperties.add( "Count Darcula" );
25
    sProperties.add( "Haunted Grey" );
26
    sProperties.add( "Modena Dark" );
27
    sProperties.add( "Monokai" );
28
    sProperties.add( SKIN_DEFAULT );
29
    sProperties.add( "Silver Cavern" );
30
    sProperties.add( "Solarized Dark" );
31
    sProperties.add( "Vampire Byte" );
32
  }
33
34
  public SkinProperty( final String skin ) {
35
    super( skin );
36
  }
37
38
  /**
39
   * Returns the list of available skin names to change the UI fonts and
40
   * colours.
41
   *
42
   * @return A selection of skins.
43
   */
44
  public static ObservableList<String> skinListProperty() {
45
    assert !sProperties.isEmpty();
46
47
    return listProperty( sProperties );
48
  }
49
50
  /**
51
   * Returns the given skin name as a sanitized file name, which must map
52
   * to a stylesheet file bundled with the application. This does not include
53
   * the path to the stylesheet. If the given name is not known, the file
54
   * name for {@link Constants#SKIN_DEFAULT} is returned. The extension must
55
   * be added separately.
56
   *
57
   * @param property The property name to convert to a file name.
58
   * @return The given property name converted lower case, spaces replaced with
59
   * underscores, without the ".css" extension appended.
60
   */
61
  public static String toFilename( final String property ) {
62
    assert property != null;
63
64
    return sanitize( property ).toLowerCase().replace( ' ', '_' );
65
  }
66
67
  /**
68
   * Ensures that the given property name is in the property list.
69
   *
70
   * @param property Property to validate.
71
   * @return The given property was found, otherwise the default property.
72
   */
73
  private static String sanitize( final String property ) {
74
    assert property != null;
75
76
    return sProperties.contains( property ) ? property : SKIN_DEFAULT;
77
  }
78
}
179
A src/main/java/com/keenwrite/preferences/TableField.java
1
/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
import com.dlsc.formsfx.model.structure.Field;
5
import com.dlsc.formsfx.model.util.BindingMode;
6
import javafx.beans.property.ListProperty;
7
import javafx.beans.property.Property;
8
import javafx.beans.property.SimpleListProperty;
9
import javafx.scene.control.TableView;
10
11
import java.util.ArrayList;
12
13
import static com.dlsc.formsfx.model.util.BindingMode.CONTINUOUS;
14
import static javafx.collections.FXCollections.observableList;
15
16
/**
17
 * Responsible for binding a form field to a map of values that, ultimately,
18
 * users may edit.
19
 *
20
 * @param <P> The type of {@link Property} to store in the list.
21
 */
22
public class TableField<P> extends Field<TableField<P>> {
23
24
  /**
25
   * Create a writeable list as the data model.
26
   */
27
  private final ListProperty<P> mViewProperty = new SimpleListProperty<>(
28
    observableList( new ArrayList<>() )
29
  );
30
31
  /**
32
   * Contains the data model entries to persist.
33
   */
34
  private final ListProperty<P> mSaveProperty;
35
36
  /**
37
   * Creates a new {@link TableField} with a reference to the list that is to
38
   * be persisted.
39
   *
40
   * @param persist A list of items that will be persisted.
41
   * @param <P>     The type of elements in the list to persist.
42
   * @return A new {@link TableField} used to help render a UI widget.
43
   */
44
  public static <P> TableField<P> ofListType( final ListProperty<P> persist ) {
45
    return new TableField<>( persist );
46
  }
47
48
  private TableField( final ListProperty<P> property ) {
49
    mSaveProperty = property;
50
  }
51
52
  public TableView<P> createTableView() {
53
    return new TableView<>( mViewProperty );
54
  }
55
56
  public void add( final P entry ) {
57
    mViewProperty.add( entry );
58
  }
59
60
  /**
61
   * Called when a new UI instance is opened.
62
   *
63
   * @param bindingMode Indicates how the view data model is bound to the
64
   *                    persistence data model.
65
   */
66
  @Override
67
  public void setBindingMode( final BindingMode bindingMode ) {
68
    if( CONTINUOUS.equals( bindingMode ) ) {
69
      mViewProperty.addAll( mSaveProperty );
70
    }
71
  }
72
73
  /**
74
   * Answers whether the user input is valid.
75
   *
76
   * @return {@code true} Users may provide any key or value strings.
77
   */
78
  @Override
79
  protected boolean validate() {
80
    return true;
81
  }
82
83
  /**
84
   * Update the properties to save by copying the properties updated in the
85
   * user interface (i.e., the view). To be clear, the properties are not
86
   * persisted after calling this method, merely moved out of the UI data
87
   * model and into the to-be-saved data model.
88
   */
89
  @Override
90
  public void persist() {
91
    mSaveProperty.clear();
92
    mSaveProperty.addAll( mViewProperty );
93
  }
94
95
  /**
96
   * The {@link TableField} doesn't bind values, as such the reset can be
97
   * a no-op because only {@link #persist()} will update the properties to
98
   * save.
99
   */
100
  @Override
101
  public void reset() {}
102
}
1103
A src/main/java/com/keenwrite/preferences/Workspace.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preferences;
3
4
import com.keenwrite.events.workspace.WorkspaceLoadedEvent;
5
import javafx.application.Platform;
6
import javafx.beans.property.*;
7
import javafx.collections.ObservableList;
8
9
import java.io.File;
10
import java.nio.file.Path;
11
import java.util.*;
12
import java.util.Map.Entry;
13
import java.util.function.BooleanSupplier;
14
import java.util.function.Function;
15
16
import static com.keenwrite.Bootstrap.*;
17
import static com.keenwrite.Launcher.getVersion;
18
import static com.keenwrite.constants.Constants.*;
19
import static com.keenwrite.events.StatusEvent.clue;
20
import static com.keenwrite.preferences.AppKeys.*;
21
import static java.util.Map.entry;
22
import static javafx.application.Platform.runLater;
23
import static javafx.collections.FXCollections.observableArrayList;
24
import static javafx.collections.FXCollections.observableSet;
25
26
/**
27
 * Responsible for defining behaviours for separate projects. A workspace has
28
 * the ability to save and restore a session, including the window dimensions,
29
 * tab setup, files, and user preferences.
30
 * <p>
31
 * The configuration must support hierarchical (nested) configuration nodes
32
 * to persist the user interface state. Although possible with a flat
33
 * configuration file, it's not nearly as simple or elegant.
34
 * </p>
35
 * <p>
36
 * Neither JSON nor HOCON support schema validation and versioning, which makes
37
 * XML the more suitable configuration file format. Schema validation and
38
 * versioning provide future-proofing and ease of reading and upgrading previous
39
 * versions of the configuration file.
40
 * </p>
41
 * <p>
42
 * Persistent preferences may be set directly by the user or indirectly by
43
 * the act of using the application.
44
 * </p>
45
 * <p>
46
 * Note the following definitions:
47
 * </p>
48
 * <dl>
49
 *   <dt>File</dt>
50
 *   <dd>References a file name (no path), path, or directory.</dd>
51
 *   <dt>Path</dt>
52
 *   <dd>Fully qualified file name, which includes all parent directories.</dd>
53
 *   <dt>Dir</dt>
54
 *   <dd>Directory without file name ({@link File#isDirectory()} is true).</dd>
55
 * </dl>
56
 */
57
public final class Workspace {
58
59
  /**
60
   * Main configuration values, single text strings.
61
   */
62
  private final Map<Key, Property<?>> mValues = Map.ofEntries(
63
    entry( KEY_META_VERSION, asStringProperty( getVersion() ) ),
64
    entry( KEY_META_NAME, asStringProperty( "default" ) ),
65
66
    entry( KEY_EDITOR_AUTOSAVE, asIntegerProperty( 30 ) ),
67
68
    entry( KEY_R_SCRIPT, asStringProperty( "" ) ),
69
    entry( KEY_R_DIR, asFileProperty( USER_DIRECTORY ) ),
70
    entry( KEY_R_DELIM_BEGAN, asStringProperty( R_DELIM_BEGAN_DEFAULT ) ),
71
    entry( KEY_R_DELIM_ENDED, asStringProperty( R_DELIM_ENDED_DEFAULT ) ),
72
73
    entry( KEY_CACHE_DIR, asFileProperty( USER_CACHE_DIR ) ),
74
    entry( KEY_IMAGE_DIR, asFileProperty( USER_DIRECTORY ) ),
75
    entry( KEY_IMAGE_ORDER, asStringProperty( PERSIST_IMAGES_DEFAULT ) ),
76
    entry( KEY_IMAGE_RESIZE, asBooleanProperty( true ) ),
77
    entry( KEY_IMAGE_SERVER, asStringProperty( DIAGRAM_SERVER_NAME ) ),
78
79
    entry( KEY_DEF_PATH, asFileProperty( DEFINITION_DEFAULT ) ),
80
    entry( KEY_DEF_DELIM_BEGAN, asStringProperty( DEF_DELIM_BEGAN_DEFAULT ) ),
81
    entry( KEY_DEF_DELIM_ENDED, asStringProperty( DEF_DELIM_ENDED_DEFAULT ) ),
82
83
    entry( KEY_UI_RECENT_DIR, asFileProperty( USER_DIRECTORY ) ),
84
    entry( KEY_UI_RECENT_OFFSET, asIntegerProperty( DOCUMENT_OFFSET ) ),
85
    entry( KEY_UI_RECENT_DOCUMENT, asFileProperty( DOCUMENT_DEFAULT ) ),
86
    entry( KEY_UI_RECENT_DEFINITION, asFileProperty( DEFINITION_DEFAULT ) ),
87
    entry( KEY_UI_RECENT_EXPORT, asFileProperty( PDF_DEFAULT ) ),
88
89
    //@formatter:off
90
    entry(
91
      KEY_UI_FONT_EDITOR_NAME,
92
      asStringProperty( FONT_NAME_EDITOR_DEFAULT )
93
    ),
94
    entry(
95
     KEY_UI_FONT_EDITOR_SIZE,
96
     asDoubleProperty( FONT_SIZE_EDITOR_DEFAULT )
97
    ),
98
    entry(
99
     KEY_UI_FONT_PREVIEW_NAME,
100
     asStringProperty( FONT_NAME_PREVIEW_DEFAULT )
101
    ),
102
    entry(
103
     KEY_UI_FONT_PREVIEW_SIZE,
104
     asDoubleProperty( FONT_SIZE_PREVIEW_DEFAULT )
105
    ),
106
    entry(
107
     KEY_UI_FONT_PREVIEW_MONO_NAME,
108
     asStringProperty( FONT_NAME_PREVIEW_MONO_NAME_DEFAULT )
109
    ),
110
    entry(
111
     KEY_UI_FONT_PREVIEW_MONO_SIZE,
112
     asDoubleProperty( FONT_SIZE_PREVIEW_MONO_SIZE_DEFAULT )
113
    ),
114
    entry(
115
      KEY_UI_FONT_MATH_SIZE,
116
      asDoubleProperty( FONT_SIZE_MATH_DEFAULT )
117
    ),
118
119
    entry( KEY_UI_WINDOW_X, asDoubleProperty( WINDOW_X_DEFAULT ) ),
120
    entry( KEY_UI_WINDOW_Y, asDoubleProperty( WINDOW_Y_DEFAULT ) ),
121
    entry( KEY_UI_WINDOW_W, asDoubleProperty( WINDOW_W_DEFAULT ) ),
122
    entry( KEY_UI_WINDOW_H, asDoubleProperty( WINDOW_H_DEFAULT ) ),
123
    entry( KEY_UI_WINDOW_MAX, asBooleanProperty() ),
124
    entry( KEY_UI_WINDOW_FULL, asBooleanProperty() ),
125
126
    entry( KEY_UI_SKIN_SELECTION, asSkinProperty( SKIN_DEFAULT ) ),
127
    entry( KEY_UI_SKIN_CUSTOM, asFileProperty( SKIN_CUSTOM_DEFAULT ) ),
128
129
    entry(
130
      KEY_UI_PREVIEW_STYLESHEET, asFileProperty( PREVIEW_CUSTOM_DEFAULT )
131
    ),
132
133
    entry( KEY_LANGUAGE_LOCALE, asLocaleProperty( LOCALE_DEFAULT ) ),
134
135
    entry( KEY_TYPESET_CONTEXT_CLEAN, asBooleanProperty( true ) ),
136
    entry( KEY_TYPESET_CONTEXT_FONTS_DIR, asFileProperty( getFontDirectory() ) ),
137
    entry( KEY_TYPESET_CONTEXT_THEMES_PATH, asFileProperty( USER_DIRECTORY ) ),
138
    entry( KEY_TYPESET_CONTEXT_THEME_SELECTION, asStringProperty( "boschet" ) ),
139
    entry( KEY_TYPESET_CONTEXT_CHAPTERS, asStringProperty( "" ) ),
140
    entry( KEY_TYPESET_TYPOGRAPHY_QUOTES, asAposProperty( APOS_DEFAULT ) ),
141
    entry( KEY_TYPESET_MODES_ENABLED, asStringProperty( "" ) )
142
    //@formatter:on
143
  );
144
145
  /**
146
   * Sets of configuration values, all the same type (e.g., file names),
147
   * where the key name doesn't change per set.
148
   */
149
  private final Map<Key, SetProperty<?>> mSets = Map.ofEntries(
150
    entry(
151
      KEY_UI_RECENT_OPEN_PATH,
152
      createSetProperty( new HashSet<String>() )
153
    )
154
  );
155
156
  /**
157
   * Lists of configuration values, such as key-value pairs where both the
158
   * key name and the value must be preserved per list.
159
   */
160
  private final Map<Key, ListProperty<?>> mLists = Map.ofEntries(
161
    entry(
162
      KEY_DOC_META,
163
      createListProperty( new LinkedList<Entry<String, String>>() )
164
    )
165
  );
166
167
  /**
168
   * Helps instantiate {@link Property} instances for XML configuration items.
169
   */
170
  private static final Map<Class<?>, Function<String, Object>> UNMARSHALL =
171
    Map.of(
172
      LocaleProperty.class, LocaleProperty::parseLocale,
173
      SimpleBooleanProperty.class, Boolean::parseBoolean,
174
      SimpleIntegerProperty.class, Integer::parseInt,
175
      SimpleDoubleProperty.class, Double::parseDouble,
176
      SimpleFloatProperty.class, Float::parseFloat,
177
      SimpleStringProperty.class, String::new,
178
      SimpleObjectProperty.class, String::new,
179
      SkinProperty.class, String::new,
180
      FileProperty.class, File::new
181
    );
182
183
  /**
184
   * The asymmetry with respect to {@link #UNMARSHALL} is because most objects
185
   * can simply call {@link Object#toString()} to convert the value to a string.
186
   */
187
  private static final Map<Class<?>, Function<String, Object>> MARSHALL =
188
    Map.of(
189
      LocaleProperty.class, LocaleProperty::toLanguageTag
190
    );
191
192
  /**
193
   * Converts the given {@link Property} value to a string.
194
   *
195
   * @param property The {@link Property} to convert.
196
   * @return A string representation of the given property, or the empty
197
   * string if no conversion was possible.
198
   */
199
  private static String marshall( final Property<?> property ) {
200
    final var v = property.getValue();
201
202
    return v == null
203
      ? ""
204
      : MARSHALL
205
      .getOrDefault( property.getClass(), _ -> property.getValue() )
206
      .apply( v.toString() )
207
      .toString();
208
  }
209
210
  private static Object unmarshall(
211
    final Property<?> property, final Object configValue ) {
212
    final var v = configValue.toString();
213
214
    return UNMARSHALL
215
      .getOrDefault( property.getClass(), _ -> property.getValue() )
216
      .apply( v );
217
  }
218
219
  /**
220
   * Creates an instance of {@link ObservableList} that is based on a
221
   * modifiable observable array list for the given items.
222
   *
223
   * @param items The items to wrap in an observable list.
224
   * @param <E>   The type of items to add to the list.
225
   * @return An observable property that can have its contents modified.
226
   */
227
  public static <E> ObservableList<E> listProperty( final Set<E> items ) {
228
    return new SimpleListProperty<>( observableArrayList( items ) );
229
  }
230
231
  private static <E> SetProperty<E> createSetProperty( final Set<E> set ) {
232
    return new SimpleSetProperty<>( observableSet( set ) );
233
  }
234
235
  private static <E> ListProperty<E> createListProperty( final List<E> list ) {
236
    return new SimpleListProperty<>( observableArrayList( list ) );
237
  }
238
239
  private static StringProperty asStringProperty( final String value ) {
240
    return new SimpleStringProperty( value );
241
  }
242
243
  private static BooleanProperty asBooleanProperty() {
244
    return new SimpleBooleanProperty();
245
  }
246
247
  /**
248
   * @param value Default value.
249
   */
250
  @SuppressWarnings( "SameParameterValue" )
251
  private static BooleanProperty asBooleanProperty( final boolean value ) {
252
    return new SimpleBooleanProperty( value );
253
  }
254
255
  /**
256
   * @param value Default value.
257
   */
258
  @SuppressWarnings( "SameParameterValue" )
259
  private static IntegerProperty asIntegerProperty( final int value ) {
260
    return new SimpleIntegerProperty( value );
261
  }
262
263
  /**
264
   * @param value Default value.
265
   */
266
  private static DoubleProperty asDoubleProperty( final double value ) {
267
    return new SimpleDoubleProperty( value );
268
  }
269
270
  /**
271
   * @param value Default value.
272
   */
273
  private static FileProperty asFileProperty( final File value ) {
274
    return new FileProperty( value );
275
  }
276
277
  /**
278
   * @param value Default value.
279
   */
280
  @SuppressWarnings( "SameParameterValue" )
281
  private static LocaleProperty asLocaleProperty( final Locale value ) {
282
    return new LocaleProperty( value );
283
  }
284
285
  /**
286
   * @param value Default value.
287
   */
288
  @SuppressWarnings( "SameParameterValue" )
289
  private static SkinProperty asSkinProperty( final String value ) {
290
    return new SkinProperty( value );
291
  }
292
293
  /**
294
   * @param value Default value.
295
   */
296
  @SuppressWarnings( "SameParameterValue" )
297
  private static AposProperty asAposProperty( final String value ) {
298
    return new AposProperty( value );
299
  }
300
301
  /**
302
   * Creates a new {@link Workspace} that will attempt to load the users'
303
   * preferences. If the configuration file cannot be loaded, the workspace
304
   * settings returns default values.
305
   */
306
  public Workspace() {
307
    load();
308
  }
309
310
  /**
311
   * Attempts to load the app's configuration file.
312
   */
313
  private void load() {
314
    final var store = createXmlStore();
315
    store.load( FILE_PREFERENCES );
316
317
    mValues.keySet().forEach( key -> {
318
      try {
319
        final var storeValue = store.getValue( key );
320
        final var property = valuesProperty( key );
321
        final var unmarshalled = unmarshall( property, storeValue );
322
323
        property.setValue( unmarshalled );
324
      } catch( final NoSuchElementException ex ) {
325
        // When no configuration (item), use the default value.
326
        clue( ex );
327
      }
328
    } );
329
330
    mSets.keySet().forEach( key -> {
331
      final var set = store.getSet( key );
332
      final SetProperty<String> property = setsProperty( key );
333
334
      property.setValue( observableSet( set ) );
335
    } );
336
337
    mLists.keySet().forEach( key -> {
338
      final var map = store.getMap( key );
339
      final ListProperty<Entry<String, String>> property = listsProperty( key );
340
      final var list = map
341
        .entrySet()
342
        .stream()
343
        .toList();
344
345
      property.setValue( observableArrayList( list ) );
346
    } );
347
348
    WorkspaceLoadedEvent.fire( this );
349
  }
350
351
  /**
352
   * Saves the current workspace.
353
   */
354
  public void save() {
355
    final var store = createXmlStore();
356
357
    try {
358
      // Update the string values to include the application version.
359
      valuesProperty( KEY_META_VERSION ).setValue( getVersion() );
360
361
      mValues.forEach( ( k, v ) -> store.setValue( k, marshall( v ) ) );
362
      mSets.forEach( store::setSet );
363
      mLists.forEach( store::setMap );
364
365
      store.save( FILE_PREFERENCES );
366
    } catch( final Exception ex ) {
367
      clue( ex );
368
    }
369
  }
370
371
  /**
372
   * Returns a value that represents a setting in the application that the user
373
   * may configure, either directly or indirectly.
374
   *
375
   * @param key The reference to the users' preference stored in deference
376
   *            of app reëntrance.
377
   * @return An observable property to be persisted.
378
   */
379
  @SuppressWarnings( "unchecked" )
380
  public <T, U extends Property<T>> U valuesProperty( final Key key ) {
381
    assert key != null;
382
    return (U) mValues.get( key );
383
  }
384
385
  /**
386
   * Returns a set of values that represent a setting in the application that
387
   * the user may configure, either directly or indirectly. The property
388
   * returned is backed by a {@link Set}.
389
   *
390
   * @param key The {@link Key} associated with a preference value.
391
   * @return An observable property to be persisted.
392
   */
393
  @SuppressWarnings( "unchecked" )
394
  public <T> SetProperty<T> setsProperty( final Key key ) {
395
    assert key != null;
396
    return (SetProperty<T>) mSets.get( key );
397
  }
398
399
  /**
400
   * Returns a list of values that represent a setting in the application that
401
   * the user may configure, either directly or indirectly. The property
402
   * returned is backed by a mutable {@link List}.
403
   *
404
   * @param key The {@link Key} associated with a preference value.
405
   * @return An observable property to be persisted.
406
   */
407
  @SuppressWarnings( "unchecked" )
408
  public <K, V> ListProperty<Entry<K, V>> listsProperty( final Key key ) {
409
    assert key != null;
410
    return (ListProperty<Entry<K, V>>) mLists.get( key );
411
  }
412
413
  /**
414
   * Returns the {@link String} {@link Property} associated with the given
415
   * {@link Key} from the internal list of preference values. The caller
416
   * must be sure that the given {@link Key} is associated with a {@link File}
417
   * {@link Property}.
418
   *
419
   * @param key The {@link Key} associated with a preference value.
420
   * @return The value associated with the given {@link Key}.
421
   */
422
  public StringProperty stringProperty( final Key key ) {
423
    assert key != null;
424
    return valuesProperty( key );
425
  }
426
427
  /**
428
   * Returns the {@link Boolean} {@link Property} associated with the given
429
   * {@link Key} from the internal list of preference values. The caller
430
   * must be sure that the given {@link Key} is associated with a {@link File}
431
   * {@link Property}.
432
   *
433
   * @param key The {@link Key} associated with a preference value.
434
   * @return The value associated with the given {@link Key}.
435
   */
436
  public BooleanProperty booleanProperty( final Key key ) {
437
    assert key != null;
438
    return valuesProperty( key );
439
  }
440
441
  /**
442
   * Returns the {@link Integer} {@link Property} associated with the given
443
   * {@link Key} from the internal list of preference values. The caller
444
   * must be sure that the given {@link Key} is associated with a {@link File}
445
   * {@link Property}.
446
   *
447
   * @param key The {@link Key} associated with a preference value.
448
   * @return The value associated with the given {@link Key}.
449
   */
450
  public IntegerProperty integerProperty( final Key key ) {
451
    assert key != null;
452
    return valuesProperty( key );
453
  }
454
455
  /**
456
   * Returns the {@link Double} {@link Property} associated with the given
457
   * {@link Key} from the internal list of preference values. The caller
458
   * must be sure that the given {@link Key} is associated with a {@link File}
459
   * {@link Property}.
460
   *
461
   * @param key The {@link Key} associated with a preference value.
462
   * @return The value associated with the given {@link Key}.
463
   */
464
  public DoubleProperty doubleProperty( final Key key ) {
465
    assert key != null;
466
    return valuesProperty( key );
467
  }
468
469
  /**
470
   * Returns the {@link File} {@link Property} associated with the given
471
   * {@link Key} from the internal list of preference values. The caller
472
   * must be sure that the given {@link Key} is associated with a {@link File}
473
   * {@link Property}.
474
   *
475
   * @param key The {@link Key} associated with a preference value.
476
   * @return The value associated with the given {@link Key}.
477
   */
478
  public ObjectProperty<File> fileProperty( final Key key ) {
479
    assert key != null;
480
    return valuesProperty( key );
481
  }
482
483
  /**
484
   * Returns the {@link Locale} {@link Property} associated with the given
485
   * {@link Key} from the internal list of preference values. The caller
486
   * must be sure that the given {@link Key} is associated with a {@link File}
487
   * {@link Property}.
488
   *
489
   * @param key The {@link Key} associated with a preference value.
490
   * @return The value associated with the given {@link Key}.
491
   */
492
  public LocaleProperty localeProperty( final Key key ) {
493
    assert key != null;
494
    return valuesProperty( key );
495
  }
496
497
  public ObjectProperty<String> listProperty( final Key key ) {
498
    assert key != null;
499
    return valuesProperty( key );
500
  }
501
502
  public String getString( final Key key ) {
503
    assert key != null;
504
    return stringProperty( key ).get();
505
  }
506
507
  /**
508
   * Returns the {@link Boolean} preference value associated with the given
509
   * {@link Key}. The caller must be sure that the given {@link Key} is
510
   * associated with a value that matches the return type.
511
   *
512
   * @param key The {@link Key} associated with a preference value.
513
   * @return The value associated with the given {@link Key}.
514
   */
515
  public boolean getBoolean( final Key key ) {
516
    assert key != null;
517
    return booleanProperty( key ).get();
518
  }
519
520
  /**
521
   * Returns the {@link Integer} preference value associated with the given
522
   * {@link Key}. The caller must be sure that the given {@link Key} is
523
   * associated with a value that matches the return type.
524
   *
525
   * @param key The {@link Key} associated with a preference value.
526
   * @return The value associated with the given {@link Key}.
527
   */
528
  @SuppressWarnings( "unused" )
529
  public int getInteger( final Key key ) {
530
    assert key != null;
531
    return integerProperty( key ).get();
532
  }
533
534
  /**
535
   * Returns the {@link Double} preference value associated with the given
536
   * {@link Key}. The caller must be sure that the given {@link Key} is
537
   * associated with a value that matches the return type.
538
   *
539
   * @param key The {@link Key} associated with a preference value.
540
   * @return The value associated with the given {@link Key}.
541
   */
542
  public double getDouble( final Key key ) {
543
    assert key != null;
544
    return doubleProperty( key ).get();
545
  }
546
547
  /**
548
   * Returns the {@link File} preference value associated with the given
549
   * {@link Key}. The caller must be sure that the given {@link Key} is
550
   * associated with a value that matches the return type.
551
   *
552
   * @param key The {@link Key} associated with a preference value.
553
   * @return The value associated with the given {@link Key}.
554
   */
555
  public File getFile( final Key key ) {
556
    assert key != null;
557
    return fileProperty( key ).get();
558
  }
559
560
  /**
561
   * Returns the language locale setting for the
562
   * {@link AppKeys#KEY_LANGUAGE_LOCALE} key.
563
   *
564
   * @return The user's current locale setting.
565
   */
566
  public Locale getLocale() {
567
    return localeProperty( KEY_LANGUAGE_LOCALE ).toLocale();
568
  }
569
570
  @SuppressWarnings( "unchecked" )
571
  public <K, V> Map<K, V> getMetadata() {
572
    final var metadata = listsProperty( KEY_DOC_META );
573
    final HashMap<K, V> map;
574
575
    if( metadata != null ) {
576
      map = new HashMap<>( metadata.size() );
577
578
      metadata.forEach(
579
        entry -> map.put( (K) entry.getKey(), (V) entry.getValue() )
580
      );
581
    }
582
    else {
583
      map = new HashMap<>();
584
    }
585
586
    return map;
587
  }
588
589
  public Path getThemesPath() {
590
    final var dir = getFile( KEY_TYPESET_CONTEXT_THEMES_PATH );
591
    final var name = getString( KEY_TYPESET_CONTEXT_THEME_SELECTION );
592
593
    return Path.of( dir.toString(), name );
594
  }
595
596
  /**
597
   * Delegates to {@link #listen(Key, ReadOnlyProperty, BooleanSupplier)},
598
   * providing a value of {@code true} for the {@link BooleanSupplier} to
599
   * indicate the property changes always take effect.
600
   *
601
   * @param key      The value to bind to the internal key property.
602
   * @param property The external property value that sets the internal value.
603
   */
604
  public <T> void listen( final Key key, final ReadOnlyProperty<T> property ) {
605
    assert key != null;
606
    assert property != null;
607
608
    listen( key, property, () -> true );
609
  }
610
611
  /**
612
   * Binds a read-only property to a value in the preferences. This allows
613
   * user interface properties to change and the preferences will be
614
   * synchronized automatically.
615
   * <p>
616
   * This calls {@link Platform#runLater(Runnable)} to ensure that all pending
617
   * application window states are finished before assessing whether property
618
   * changes should be applied. Without this, exiting the application while the
619
   * window is maximized would persist the window's maximum dimensions,
620
   * preventing restoration to its prior, non-maximum size.
621
   *
622
   * @param key      The value to bind to the internal key property.
623
   * @param property The external property value that sets the internal value.
624
   * @param enabled  Indicates whether property changes should be applied.
625
   */
626
  public <T> void listen(
627
    final Key key,
628
    final ReadOnlyProperty<T> property,
629
    final BooleanSupplier enabled ) {
630
    assert key != null;
631
    assert property != null;
632
    assert enabled != null;
633
634
    property.addListener(
635
      ( _, _, n ) -> runLater( () -> {
636
        if( enabled.getAsBoolean() ) {
637
          valuesProperty( key ).setValue( n );
638
        }
639
      } )
640
    );
641
  }
642
643
  /**
644
   * Creates a lightweight persistence mechanism for user preferences.
645
   *
646
   * @return The {@link XmlStore} that helps with persisting application state.
647
   */
648
  private XmlStore createXmlStore() {
649
    // Root-level configuration item is the application name.
650
    return new XmlStore( APP_TITLE_LOWERCASE );
651
  }
652
}
1653
A src/main/java/com/keenwrite/preferences/XmlStore.java
1
package com.keenwrite.preferences;
2
3
import com.keenwrite.dom.DocumentParser;
4
import javafx.beans.property.ListProperty;
5
import javafx.beans.property.SetProperty;
6
import org.w3c.dom.Document;
7
import org.w3c.dom.Element;
8
import org.w3c.dom.Node;
9
10
import javax.xml.xpath.XPath;
11
import javax.xml.xpath.XPathExpression;
12
import javax.xml.xpath.XPathExpressionException;
13
import java.io.File;
14
import java.io.FileWriter;
15
import java.io.IOException;
16
import java.util.*;
17
import java.util.Map.Entry;
18
import java.util.function.Consumer;
19
20
import static com.keenwrite.events.StatusEvent.clue;
21
import static java.nio.charset.StandardCharsets.UTF_8;
22
import static javax.xml.xpath.XPathConstants.NODE;
23
24
/**
25
 * Responsible for managing XML documents, which includes reading, writing,
26
 * retrieving, and setting elements. This is an alternative to Apache
27
 * Commons Configuration, JAXB, and Jackson. All of them are heavyweight and
28
 * the latter are difficult to use with dynamic data (because they require
29
 * annotations).
30
 * <p>
31
 * <strong>Note:</strong> It is preferable to use a different instance when
32
 * loading and saving the documents. Otherwise, old and duplicate data will
33
 * persist. Using a new instance ensures that elements removed from the
34
 * user preferences will not persist across XML configuration file versions.
35
 */
36
public class XmlStore {
37
  private static final String SEPARATOR = "/";
38
39
  private final String mRoot;
40
  private Document mDocument = DocumentParser.newDocument();
41
42
  /**
43
   * Constructs a new instance with a blank {@link Document}. Call the
44
   * {@link #load(File)} method to populate the document from a {@link File},
45
   * or {@link #save(File)} to persist the current document state.
46
   *
47
   * @param root The root-level document element.
48
   */
49
  public XmlStore( final String root ) {
50
    assert root != null;
51
52
    mRoot = root;
53
  }
54
55
  /**
56
   * Loads the given configuration file into a document object model.
57
   * Clients of this class can set and retrieve elements via the requisite
58
   * access methods.
59
   *
60
   * @param config File containing persistent user preferences.
61
   */
62
  public void load( final File config ) {
63
    assert config != null;
64
    assert config.isFile();
65
66
    try {
67
      mDocument = DocumentParser.parse( config );
68
    } catch( final Exception ignored ) {
69
      mDocument = DocumentParser.newDocument();
70
    }
71
  }
72
73
  /**
74
   * Returns the document value associated with the given key name.
75
   *
76
   * @param key {@link Key} name to retrieve.
77
   * @return The value associated with the key.
78
   * @throws NoSuchElementException No value could be found for the key.
79
   */
80
  public String getValue( final Key key ) throws NoSuchElementException {
81
    assert key != null;
82
83
    try {
84
      final var node = toNode( key, mDocument );
85
86
      if( node != null ) {
87
        return node.getTextContent();
88
      }
89
    } catch( final XPathExpressionException ignored ) { }
90
91
    throw new NoSuchElementException( key.toString() );
92
  }
93
94
  /**
95
   * Returns a set of document values associated with the given key name. This
96
   * is suitable for basic sets, such as:
97
   * <pre>
98
   *   {@code
99
   *   <recent>
100
   *     <file>/tmp/filename.txt</file>
101
   *     <file>/home/username/document.md</file>
102
   *     <file>/usr/local/share/app/conf/help.Rmd</file>
103
   *   </recent>}
104
   * </pre>
105
   * <p>
106
   * The {@code file} element name can be ignored.
107
   *
108
   * @param key {@link Key} name to retrieve.
109
   * @return The values associated with the key, or an empty set if none found.
110
   */
111
  public Set<String> getSet( final Key key ) {
112
    assert key != null;
113
114
    final var set = new LinkedHashSet<String>();
115
116
    visit( key, node -> set.add( node.getTextContent() ) );
117
118
    return set;
119
  }
120
121
  /**
122
   * Returns a map of name/value pairs associated with the given key name.
123
   * This is suitable for mapped values, such as:
124
   * <pre>
125
   *   {@code
126
   *   <meta>
127
   *     <title>{{book.title}}</title>
128
   *     <author>{{book.author}}</author>
129
   *     <date>{{book.publish.date}}</date>
130
   *   </meta>}
131
   * </pre>
132
   * <p>
133
   * The element names under the {@code meta} node must be preserved along
134
   * with their values. Resolving the values based on the variable definitions
135
   * (in moustache syntax) is not a responsibility of this class.
136
   *
137
   * @param key {@link Key} name to retrieve (e.g., {@code meta}).
138
   * @return A map of element names to element values, or an empty map if
139
   * none found.
140
   */
141
  public Map<String, String> getMap( final Key key ) {
142
    assert key != null;
143
144
    // Create a new key that will match all child nodes under the given key,
145
    // extracting each element as a name/value pair for the resulting map.
146
    final var all = Key.key( key, "*" );
147
    final var map = new LinkedHashMap<String, String>();
148
149
    visit( all, node -> map.put( node.getNodeName(), node.getTextContent() ) );
150
151
    return map;
152
  }
153
154
  /**
155
   * Call to write the user preferences to a file.
156
   *
157
   * @param config The file wherein the preferences are saved.
158
   * @throws IOException Could not write to the file.
159
   */
160
  public void save( final File config ) throws IOException {
161
    assert config != null;
162
163
    try( final var writer = new FileWriter( config, UTF_8 ) ) {
164
      writer.write( DocumentParser.toString( mDocument ) );
165
    }
166
  }
167
168
  public void setValue( final Key key, final String value ) {
169
    assert key != null;
170
    assert value != null;
171
172
    try {
173
      final var node = upsert( key, mDocument );
174
175
      node.setTextContent( value );
176
    } catch( final XPathExpressionException ex ) {
177
      clue( ex );
178
    }
179
  }
180
181
  public void setSet( final Key key, final SetProperty<?> set ) {
182
    assert key != null;
183
    assert set != null;
184
185
    Node node = null;
186
187
    try {
188
      for( final var item : set ) {
189
        if( node == null ) {
190
          node = upsert( key, mDocument );
191
        }
192
        else {
193
          final var doc = node.getOwnerDocument();
194
          final var sibling = doc.createElement( key.name() );
195
          var parent = node.getParentNode();
196
197
          if( parent == null ) {
198
            parent = doc.getDocumentElement();
199
          }
200
201
          parent.appendChild( sibling );
202
          node = sibling;
203
        }
204
205
        node.setTextContent( item.toString() );
206
      }
207
    } catch( final XPathExpressionException ignored ) { }
208
  }
209
210
  /**
211
   * @param key  The application key representing a user preference.
212
   * @param list List of {@link Entry} items.
213
   */
214
  public void setMap( final Key key, final ListProperty<?> list ) {
215
    assert key != null;
216
    assert list != null;
217
218
    for( final var item : list ) {
219
      if( item instanceof Entry<?, ?> entry ) {
220
        try {
221
          final var child = Key.key( key, entry.getKey().toString() );
222
          final var node = upsert( child, mDocument );
223
224
          node.setTextContent( entry.getValue().toString() );
225
        } catch( final XPathExpressionException ignored ) { }
226
      }
227
    }
228
  }
229
230
  private Node toNode( final Key key, final Document doc )
231
    throws XPathExpressionException {
232
    final var xpath = toXPath( key );
233
    final var expr = DocumentParser.compile( xpath );
234
    final var element = expr.evaluate( doc, NODE );
235
236
    return element instanceof Node node ? node : null;
237
  }
238
239
  /**
240
   * Provides the equivalent of update-or-insert behaviour provided by some
241
   * SQL databases. Finds the element in the document represented by the
242
   * given {@link Key}. If no element is found then the full path to the
243
   * element is created. In essence, this method converts a hierarchy of
244
   * {@link Key} names into a hierarchy of {@link Document} {@link Element}s
245
   * (i.e., {@link Node}s).
246
   * <p>
247
   * For example, given a key named {@code workspace.meta.version}, this will
248
   * produce a document structure that, when exported as XML, resembles:
249
   * <pre>{@code
250
   *   <root>
251
   *     <workspace>
252
   *       <meta>
253
   *         <version/>
254
   *       </meta>
255
   *     </workspace>
256
   *   </root>
257
   * }</pre>
258
   * <p>
259
   * The calling code is responsible for populating the {@link Node} returned
260
   * with its particular value. In the example above, the text content of the
261
   * {@link Node} would be filled with the application version number.
262
   *
263
   * @param key The application key representing a user preference.
264
   * @param doc The document that may contain an xpath for the {@link Key}.
265
   * @return The existing or new element.
266
   */
267
  private Node upsert( final Key key, final Document doc )
268
    throws XPathExpressionException {
269
    assert key != null;
270
    assert doc != null;
271
272
    final var missing = new Stack<Key>();
273
    Key visitor = key;
274
    Node parent = null;
275
276
    do {
277
      final var node = toNode( visitor, doc );
278
279
      // If an element exists on the first iteration, return it because there
280
      // is no missing hierarchy to create.
281
      if( node != null ) {
282
        if( missing.isEmpty() ) {
283
          return node;
284
        }
285
286
        parent = node;
287
      }
288
      else {
289
        // Track the number of elements in the hierarchy that don't exist.
290
        missing.push( visitor );
291
292
        // Attempt to find the parent xpath in the document.
293
        visitor = visitor.parent();
294
      }
295
    }
296
    while( visitor != null && parent == null );
297
298
    // If the document is empty, update the top-level document element.
299
    if( parent == null ) {
300
      parent = doc.getDocumentElement();
301
302
      // If there is still no top-level element, then create it.
303
      if( parent == null ) {
304
        parent = doc.createElement( mRoot );
305
        doc.appendChild( parent );
306
      }
307
    }
308
309
    assert parent != null;
310
311
    // Create the hierarchy.
312
    while( !missing.isEmpty() ) {
313
      visitor = missing.pop();
314
315
      final var child = doc.createElement( visitor.name() );
316
      parent.appendChild( child );
317
      parent = child;
318
    }
319
320
    return parent;
321
  }
322
323
  /**
324
   * Abstraction for functionality that requires iterating over multiple
325
   * nodes under a particular xpath.
326
   *
327
   * @param key      {@link #toXPath(Key) Compiled} into an {@link XPath}.
328
   * @param consumer Accepts each node that matches the {@link XPath}.
329
   */
330
  private void visit( final Key key, final Consumer<Node> consumer ) {
331
    assert key != null;
332
    assert consumer != null;
333
334
    try {
335
      final var xpath = toXPath( key );
336
337
      DocumentParser.visit( mDocument, xpath, consumer );
338
    } catch( final XPathExpressionException ignored ) {
339
      // Programming error. Triggered by loading a previous config version?
340
    }
341
  }
342
343
  /**
344
   * Creates an {@link XPathExpression} value based on the given {@link Key}.
345
   *
346
   * @param key The {@link Key} to convert to an xpath string.
347
   * @return The given {@link Key} compiled into an {@link XPathExpression}.
348
   * @throws XPathExpressionException Could not compile the {@link Key}.
349
   */
350
  private StringBuilder toXPath( final Key key )
351
    throws XPathExpressionException {
352
    assert key != null;
353
354
    final var sb = new StringBuilder( 128 );
355
356
    key.walk( sb::append, SEPARATOR );
357
    sb.insert( 0, SEPARATOR );
358
359
    if( !mRoot.isBlank() ) {
360
      sb.insert( 0, SEPARATOR + mRoot );
361
    }
362
363
    return sb;
364
  }
365
366
  /**
367
   * Pretty-prints the XML document into a string. Meant to be used for
368
   * debugging. To save the configuration, see {@link #save(File)}.
369
   *
370
   * @return The document in a well-formed, indented, string format.
371
   */
372
  @Override
373
  public String toString() {
374
    return DocumentParser.toString( mDocument );
375
  }
376
}
1377
A src/main/java/com/keenwrite/preview/ChainedReplacedElementFactory.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import com.keenwrite.collections.BoundedCache;
5
import com.keenwrite.ui.adapters.ReplacedElementAdapter;
6
import org.w3c.dom.Element;
7
import org.xhtmlrenderer.extend.ReplacedElement;
8
import org.xhtmlrenderer.extend.ReplacedElementFactory;
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.util.LinkedHashSet;
15
import java.util.Map;
16
import java.util.Set;
17
18
import static com.keenwrite.preview.ImageReplacedElementFactory.HTML_IMAGE;
19
import static com.keenwrite.preview.ImageReplacedElementFactory.HTML_IMAGE_SRC;
20
import static com.keenwrite.processors.markdown.extensions.tex.TexNode.HTML_TEX;
21
import static java.lang.Math.min;
22
import static java.util.Arrays.asList;
23
24
/**
25
 * Responsible for running one or more factories to perform post-processing on
26
 * the HTML document prior to displaying it.
27
 */
28
public final class ChainedReplacedElementFactory
29
  extends ReplacedElementAdapter {
30
  /**
31
   * Retain insertion order so that client classes can control the order that
32
   * factories are used to resolve images.
33
   */
34
  private final Set<ReplacedElementFactory> mFactories = new LinkedHashSet<>();
35
36
  /**
37
   * A bounded cache that removes the oldest image if the maximum number of
38
   * cached images has been reached. This constrains the number of images
39
   * loaded into memory.
40
   */
41
  private final Map<String, ReplacedElement> mCache = new BoundedCache<>( 150 );
42
43
  public ChainedReplacedElementFactory(
44
    final ReplacedElementFactory... factories ) {
45
    assert factories != null;
46
    assert factories.length > 0;
47
    mFactories.addAll( asList( factories ) );
48
  }
49
50
  @Override
51
  public ReplacedElement createReplacedElement(
52
    final LayoutContext c,
53
    final BlockBox box,
54
    final UserAgentCallback uac,
55
    final int width,
56
    final int height ) {
57
    for( final var f : mFactories ) {
58
      final var e = box.getElement();
59
60
      // Exit early for super-speed.
61
      if( e == null ) {
62
        break;
63
      }
64
65
      // If the source image is cached, don't bother fetching. This optimization
66
      // avoids making multiple HTTP requests for the same URI.
67
      final var node = e.getNodeName();
68
      final var source = switch( node ) {
69
        case HTML_IMAGE -> e.getAttribute( HTML_IMAGE_SRC );
70
        case HTML_TEX -> e.getTextContent();
71
        default -> "";
72
      };
73
74
      // HTML <img> or <tex> elements without source data shall not pass.
75
      if( source.isBlank() ) {
76
        break;
77
      }
78
79
      final var replaced = mCache.computeIfAbsent( source, k -> {
80
        final var r = f.createReplacedElement( c, box, uac, width, height );
81
82
        return r instanceof final ImageReplacedElement ire
83
          ? createImageElement( box, ire )
84
          : r;
85
      } );
86
87
      if( replaced != null ) {
88
        return replaced;
89
      }
90
    }
91
92
    return null;
93
  }
94
95
  @Override
96
  public void reset() {
97
    for( final var factory : mFactories ) {
98
      factory.reset();
99
    }
100
  }
101
102
  @Override
103
  public void remove( final Element element ) {
104
    for( final var factory : mFactories ) {
105
      factory.remove( element );
106
    }
107
  }
108
109
  public void clearCache() {
110
    mCache.clear();
111
  }
112
113
  /**
114
   * Creates a new image that maintains its aspect ratio while fitting into
115
   * the given {@link BlockBox}. If the image is too big, it is scaled down.
116
   *
117
   * @param box The bounding region the image must fit into.
118
   * @param ire The image to resize.
119
   * @return An image that is scaled down to fit, but only if necessary.
120
   */
121
  private SmoothImageReplacedElement createImageElement(
122
    final BlockBox box, final ImageReplacedElement ire ) {
123
    return new SmoothImageReplacedElement(
124
      ire.getImage(), min( ire.getIntrinsicWidth(), box.getWidth() ), -1 );
125
  }
126
}
1127
A src/main/java/com/keenwrite/preview/DiagramUrlGenerator.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import java.nio.charset.StandardCharsets;
5
import java.util.zip.Deflater;
6
7
import static java.lang.String.format;
8
import static java.util.Base64.getUrlEncoder;
9
10
/**
11
 * Responsible for transforming text-based diagram descriptions into URLs
12
 * that the HTML renderer can embed as SVG images.
13
 */
14
public class DiagramUrlGenerator {
15
  private DiagramUrlGenerator() {
16
  }
17
18
  /**
19
   * Returns a URL that can be embedded as the {@code src} attribute to an HTML
20
   * {@code img} tag.
21
   *
22
   * @param server  Name of server to use for diagram conversion.
23
   * @param diagram Diagram type (e.g., Graphviz, Block, PlantUML).
24
   * @param text    Diagram text that conforms to the diagram type.
25
   * @return A secure URL string to use as an image {@code src} attribute.
26
   */
27
  public static String toUrl(
28
    final String server, final String diagram, final String text ) {
29
    return format(
30
      "https://%s/%s/svg/%s", server, diagram, encode( text )
31
    );
32
  }
33
34
  /**
35
   * Convert the plain-text version of the diagram into a URL-encoded value
36
   * suitable for passing to a web server using an HTTP GET request.
37
   *
38
   * @param text The diagram text to encode.
39
   * @return The URL-encoded (and compressed) version of the text.
40
   */
41
  private static String encode( final String text ) {
42
    return getUrlEncoder().encodeToString(
43
      compress( text.getBytes( StandardCharsets.UTF_8 ) )
44
    );
45
  }
46
47
  /**
48
   * Compresses a sequence of bytes using ZLIB format.
49
   *
50
   * @param source The data to compress.
51
   * @return A lossless, compressed sequence of bytes.
52
   */
53
  private static byte[] compress( final byte[] source ) {
54
    final var deflater = new Deflater();
55
    deflater.setInput( source );
56
    deflater.finish();
57
58
    final var compressed = new byte[ Short.MAX_VALUE ];
59
    final var size = deflater.deflate( compressed );
60
    final var result = new byte[ size ];
61
62
    System.arraycopy( compressed, 0, result, 0, size );
63
64
    return result;
65
  }
66
}
167
A src/main/java/com/keenwrite/preview/FlyingSaucerPanel.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import com.keenwrite.events.FileOpenEvent;
5
import com.keenwrite.events.HyperlinkOpenEvent;
6
import com.keenwrite.ui.adapters.DocumentAdapter;
7
import javafx.beans.property.BooleanProperty;
8
import javafx.beans.property.SimpleBooleanProperty;
9
import org.w3c.dom.Document;
10
import org.xhtmlrenderer.layout.SharedContext;
11
import org.xhtmlrenderer.render.Box;
12
import org.xhtmlrenderer.simple.XHTMLPanel;
13
import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
14
import org.xhtmlrenderer.swing.*;
15
16
import javax.swing.*;
17
import java.awt.*;
18
import java.awt.event.ComponentAdapter;
19
import java.awt.event.ComponentEvent;
20
import java.net.URI;
21
22
import static com.keenwrite.events.StatusEvent.clue;
23
import static com.keenwrite.util.ProtocolScheme.getProtocol;
24
import static java.lang.Boolean.FALSE;
25
import static java.lang.Boolean.TRUE;
26
import static java.lang.Math.max;
27
import static java.lang.Thread.sleep;
28
import static javax.swing.SwingUtilities.invokeLater;
29
30
/**
31
 * Responsible for configuring FlyingSaucer's {@link XHTMLPanel}.
32
 */
33
public final class FlyingSaucerPanel extends XHTMLPanel
34
  implements HtmlRenderer {
35
36
  /**
37
   * Suppresses scroll attempts until after the document has loaded.
38
   */
39
  private static final class DocumentEventHandler extends DocumentAdapter {
40
    private final BooleanProperty mReadyProperty = new SimpleBooleanProperty();
41
42
    @Override
43
    public void documentStarted() {
44
      mReadyProperty.setValue( FALSE );
45
    }
46
47
    @Override
48
    public void documentLoaded() {
49
      mReadyProperty.setValue( TRUE );
50
    }
51
  }
52
53
  /**
54
   * Ensures that the preview panel fills its container's area completely.
55
   */
56
  private final class ComponentEventHandler extends ComponentAdapter {
57
    /**
58
     * Invoked when the component's size changes.
59
     */
60
    public void componentResized( final ComponentEvent e ) {
61
      setPreferredSize( e.getComponent().getPreferredSize() );
62
    }
63
  }
64
65
  /**
66
   * Responsible for opening hyperlinks. External hyperlinks are opened in
67
   * the system's default browser; local file system links are opened in the
68
   * editor.
69
   */
70
  private static final class HyperlinkListener extends LinkListener {
71
    @Override
72
    public void linkClicked( final BasicPanel panel, final String link ) {
73
      try {
74
        final var uri = new URI( link );
75
76
        switch( getProtocol( uri ) ) {
77
          case HTTP -> HyperlinkOpenEvent.fire( uri );
78
          case FILE -> FileOpenEvent.fire( uri );
79
          default -> { }
80
        }
81
      } catch( final Exception ex ) {
82
        clue( ex );
83
      }
84
    }
85
  }
86
87
  private static final XhtmlNamespaceHandler XNH = new XhtmlNamespaceHandler();
88
  private final ChainedReplacedElementFactory mFactory;
89
90
  FlyingSaucerPanel() {
91
    // The order is important: SwingReplacedElementFactory replaces SVG images
92
    // with a blank image, which will cause the chained factory to cache the
93
    // image and exit. Instead, the SVG must execute first to rasterize the
94
    // content. Consequently, the chained factory must maintain insertion order.
95
    mFactory = new ChainedReplacedElementFactory(
96
      new ImageReplacedElementFactory(),
97
      new SwingReplacedElementFactory()
98
    );
99
100
    final var context = getSharedContext();
101
    final var textRenderer = context.getTextRenderer();
102
    context.setReplacedElementFactory( mFactory );
103
    textRenderer.setSmoothingThreshold( 0 );
104
105
    addDocumentListener( new DocumentEventHandler() );
106
    removeMouseTrackingListeners();
107
    addMouseTrackingListener( new HyperlinkListener() );
108
    addComponentListener( new ComponentEventHandler() );
109
  }
110
111
  /**
112
   * Updates the document model displayed by the renderer. Effectively, this
113
   * updates the HTML document to provide new content.
114
   *
115
   * @param doc     A complete HTML5 document, including doctype.
116
   * @param baseUri URI to use for finding relative files, such as images.
117
   */
118
  @Override
119
  public void render( final Document doc, final String baseUri ) {
120
    setDocument( doc, baseUri, XNH );
121
  }
122
123
  @Override
124
  public void clearCache() {
125
    mFactory.clearCache();
126
  }
127
128
  @Override
129
  public void scrollTo( final String id, final JScrollPane scrollPane ) {
130
    int iter = 0;
131
    Box box = null;
132
133
    while( iter++ < 3 && ((box = getBoxById( id )) == null) ) {
134
      try {
135
        sleep( 10 );
136
      } catch( final Exception ex ) {
137
        clue( ex );
138
      }
139
    }
140
141
    scrollTo( box, scrollPane );
142
  }
143
144
  /**
145
   * Scrolls to the location specified by the {@link Box} that corresponds
146
   * to a point somewhere in the preview pane. If there is no caret, then
147
   * this will not change the scroll position. Changing the scroll position
148
   * to the top if the {@link Box} instance is {@code null} will result in
149
   * jumping around a lot and inconsistent synchronization issues.
150
   *
151
   * @param box The rectangular region containing the caret, or {@code null}
152
   *            if the HTML does not have a caret.
153
   */
154
  private void scrollTo( final Box box, final JScrollPane scrollPane ) {
155
    if( box != null ) {
156
      invokeLater( () -> {
157
        scrollTo( createPoint( box, scrollPane ) );
158
        scrollPane.repaint();
159
      } );
160
    }
161
  }
162
163
  /**
164
   * Creates a {@link Point} to use as a reference for scrolling to the area
165
   * described by the given {@link Box}. The {@link Box} coordinates are used
166
   * to populate the {@link Point}'s location, with minor adjustments for
167
   * vertical centering.
168
   *
169
   * @param box The {@link Box} that represents a scrolling anchor reference.
170
   * @return A coordinate suitable for scrolling to.
171
   */
172
  private Point createPoint( final Box box, final JScrollPane scrollPane ) {
173
    assert box != null;
174
175
    // Scroll back up by half the height of the scroll bar to keep the typing
176
    // area within the view port; otherwise, the view port will have jumped too
177
    // high up and the most recently typed letters won't be visible.
178
    int y = max( box.getAbsY() - scrollPane.getVerticalScrollBar()
179
                                           .getHeight() / 2, 0 );
180
    int x = box.getAbsX();
181
182
    if( !box.getStyle().isInline() ) {
183
      final var margin = box.getMargin( getLayoutContext() );
184
      y += (int) margin.top();
185
      x += (int) margin.left();
186
    }
187
188
    return new Point( x, y );
189
  }
190
191
  /**
192
   * Delegates to the {@link SharedContext}.
193
   *
194
   * @param id The HTML element identifier to retrieve in {@link Box} form.
195
   * @return The {@link Box} that corresponds to the given element ID, or
196
   * {@code null} if none found.
197
   */
198
  Box getBoxById( final String id ) {
199
    return getSharedContext().getBoxById( id );
200
  }
201
202
  /**
203
   * Suppress scrolling to the top on updates.
204
   */
205
  @Override
206
  public void resetScrollPosition() {
207
  }
208
209
  /**
210
   * The default mouse click listener attempts navigation within the preview
211
   * panel. We want to usurp that behaviour to open the link in a
212
   * platform-specific browser.
213
   */
214
  private void removeMouseTrackingListeners() {
215
    for( final var listener : getMouseTrackingListeners() ) {
216
      if( !(listener instanceof HoverListener) ) {
217
        removeMouseTrackingListener( (FSMouseListener) listener );
218
      }
219
    }
220
  }
221
}
1222
A src/main/java/com/keenwrite/preview/HighQualityRenderingHints.java
1
/* Copyright 2020-2021 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 initializing settings to produce high-quality image
12
 * transformations.
13
 */
14
@SuppressWarnings( "rawtypes" )
15
public class HighQualityRenderingHints {
16
  /**
17
   * Default hints for high-quality rendering that may be changed by
18
   * the system's rendering hints.
19
   */
20
  private static final Map<Object, Object> DEFAULT_HINTS = Map.of(
21
    KEY_ANTIALIASING, VALUE_ANTIALIAS_ON,
22
    KEY_ALPHA_INTERPOLATION, VALUE_ALPHA_INTERPOLATION_QUALITY,
23
    KEY_COLOR_RENDERING, VALUE_COLOR_RENDER_QUALITY,
24
    KEY_DITHERING, VALUE_DITHER_DISABLE,
25
    KEY_FRACTIONALMETRICS, VALUE_FRACTIONALMETRICS_ON,
26
    KEY_INTERPOLATION, VALUE_INTERPOLATION_BICUBIC,
27
    KEY_RENDERING, VALUE_RENDER_QUALITY,
28
    KEY_STROKE_CONTROL, VALUE_STROKE_PURE,
29
    KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON
30
  );
31
32
  /**
33
   * Shared hints for high-quality rendering.
34
   */
35
  static final Map<Object, Object> RENDERING_HINTS = new HashMap<>(
36
    DEFAULT_HINTS
37
  );
38
39
  static {
40
    initializeRenderingHints();
41
  }
42
43
  private static void initializeRenderingHints() {
44
    final var toolkit = getDefaultToolkit();
45
    final var hints = toolkit.getDesktopProperty( "awt.font.desktophints" );
46
47
    if( hints instanceof final Map map ) {
48
      for( final var key : map.keySet() ) {
49
        final var hint = map.get( key );
50
        RENDERING_HINTS.put( key, hint );
51
      }
52
    }
53
  }
54
55
  /**
56
   * Defines a reusable constant, nothing more.
57
   */
58
  private HighQualityRenderingHints() {
59
  }
60
}
161
A src/main/java/com/keenwrite/preview/HtmlPreview.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import com.keenwrite.dom.DocumentConverter;
5
import com.keenwrite.events.DocumentChangedEvent;
6
import com.keenwrite.events.ScrollLockEvent;
7
import com.keenwrite.preferences.LocaleProperty;
8
import com.keenwrite.preferences.Workspace;
9
import javafx.beans.property.DoubleProperty;
10
import javafx.beans.property.StringProperty;
11
import javafx.embed.swing.SwingNode;
12
import org.greenrobot.eventbus.Subscribe;
13
14
import javax.swing.*;
15
import java.awt.*;
16
import java.awt.event.ComponentEvent;
17
import java.awt.event.ComponentListener;
18
import java.net.URL;
19
import java.nio.file.Path;
20
import java.util.Locale;
21
22
import static com.keenwrite.constants.Constants.*;
23
import static com.keenwrite.events.Bus.register;
24
import static com.keenwrite.events.ScrollLockEvent.fireScrollLockEvent;
25
import static com.keenwrite.events.StatusEvent.clue;
26
import static com.keenwrite.preferences.AppKeys.*;
27
import static com.keenwrite.ui.fonts.IconFactory.getIconFont;
28
import static java.awt.BorderLayout.*;
29
import static java.awt.event.KeyEvent.*;
30
import static java.lang.String.format;
31
import static javafx.scene.CacheHint.SPEED;
32
import static javax.swing.JComponent.WHEN_IN_FOCUSED_WINDOW;
33
import static javax.swing.KeyStroke.getKeyStroke;
34
import static javax.swing.SwingUtilities.invokeLater;
35
import static org.controlsfx.glyphfont.FontAwesome.Glyph.LOCK;
36
import static org.controlsfx.glyphfont.FontAwesome.Glyph.UNLOCK_ALT;
37
38
/**
39
 * Responsible for parsing an HTML document.
40
 */
41
public final class HtmlPreview extends SwingNode implements ComponentListener {
42
  /**
43
   * Converts a text string to a structured HTML document.
44
   */
45
  private static final DocumentConverter CONVERTER = new DocumentConverter();
46
47
  /**
48
   * Used to populate the {@link #HTML_HEAD} with stylesheet file references.
49
   */
50
  private static final String HTML_STYLESHEET =
51
    "<link rel='stylesheet' href='%s'/>";
52
53
  private static final String HTML_BASE =
54
    "<base href='%s'/>";
55
56
  /**
57
   * Render CSS using points (pt) not pixels (px) to reduce the chance of
58
   * poor rendering. The {@link #generateHead()} method fills placeholders.
59
   * When the user has not set a locale, only one stylesheet is added to
60
   * the document. In order, the placeholders are as follows:
61
   * <ol>
62
   * <li>%s --- language</li>
63
   * <li>%s --- default stylesheet</li>
64
   * <li>%s --- language-specific stylesheet</li>
65
   * <li>%s --- user-customized stylesheet</li>
66
   * <li>%s --- font family</li>
67
   * <li>%d --- font size (must be pixels, not points due to bug)</li>
68
   * <li>%s --- base href</li>
69
   * </p>
70
   */
71
  private static final String HTML_HEAD = """
72
    <!doctype html>
73
    <html lang='%s'><head><title> </title><meta charset='utf-8'/>
74
    %s%s%s<style>body{font-family:'%s';font-size: %dpx;}</style>%s</head><body>
75
    """;
76
77
  private static final String HTML_TAIL = "</body></html>";
78
79
  private static final URL HTML_STYLE_PREVIEW = toUrl( STYLESHEET_PREVIEW );
80
81
  /**
82
   * Reusing this buffer prevents repetitious memory re-allocations.
83
   */
84
  private final StringBuilder mDocument = new StringBuilder( 65536 );
85
86
  private HtmlRenderer mPreview;
87
  private JScrollPane mScrollPane;
88
  private String mBaseUriPath = "";
89
  private String mHead;
90
91
  private volatile boolean mScrollLocked;
92
  private final JButton mScrollLockButton = new JButton();
93
  private final Workspace mWorkspace;
94
95
  /**
96
   * Creates a new preview pane that can scroll to the caret position within the
97
   * document.
98
   *
99
   * @param workspace Contains locale and font size information.
100
   */
101
  public HtmlPreview( final Workspace workspace ) {
102
    mWorkspace = workspace;
103
    mHead = generateHead();
104
105
    // Attempts to prevent a flash of black un-styled content upon load.
106
    setStyle( "-fx-background-color: white;" );
107
108
    invokeLater( () -> {
109
      mPreview = new FlyingSaucerPanel();
110
      mScrollPane = new JScrollPane( (Component) mPreview );
111
      final var verticalBar = mScrollPane.getVerticalScrollBar();
112
      final var verticalPanel = new JPanel( new BorderLayout() );
113
114
      final var map = verticalBar.getInputMap( WHEN_IN_FOCUSED_WINDOW );
115
      addKeyboardEvents( map );
116
117
      mScrollLockButton.setFont( getIconFont( 14 ) );
118
      mScrollLockButton.setText( getLockText( mScrollLocked ) );
119
      mScrollLockButton.setMargin( new Insets( 1, 0, 0, 0 ) );
120
      mScrollLockButton.addActionListener(
121
        _ -> fireScrollLockEvent( !mScrollLocked )
122
      );
123
124
      verticalPanel.add( verticalBar, CENTER );
125
      verticalPanel.add( mScrollLockButton, PAGE_END );
126
127
      final var wrapper = new JPanel( new BorderLayout() );
128
      wrapper.add( mScrollPane, CENTER );
129
      wrapper.add( verticalPanel, LINE_END );
130
131
      // Enabling the cache attempts to prevent black flashes when resizing.
132
      setCache( true );
133
      setCacheHint( SPEED );
134
      setContent( wrapper );
135
      wrapper.addComponentListener( this );
136
    } );
137
138
    localeProperty().addListener( ( c, o, n ) -> rerender() );
139
    fontFamilyProperty().addListener( ( c, o, n ) -> rerender() );
140
    fontSizeProperty().addListener( ( c, o, n ) -> rerender() );
141
142
    register( this );
143
  }
144
145
  @Subscribe
146
  public void handle( final ScrollLockEvent event ) {
147
    mScrollLocked = event.isLocked();
148
    invokeLater(
149
      () -> mScrollLockButton.setText( getLockText( mScrollLocked ) )
150
    );
151
  }
152
153
  /**
154
   * Updates the internal HTML source shown in the preview pane.
155
   *
156
   * @param html The new HTML document to display.
157
   */
158
  public void render( final String html ) {
159
    final var jsoupDoc = DocumentConverter.parse( decorate( html ) );
160
161
    // Ensure the title (metadata) doesn't appear in the preview panel.
162
    jsoupDoc.select( "title" ).remove();
163
164
    final var doc = CONVERTER.fromJsoup( jsoupDoc );
165
    final var uri = getBaseUri();
166
167
    doc.setDocumentURI( uri );
168
    invokeLater( () -> mPreview.render( doc, uri ) );
169
    DocumentChangedEvent.fire( html );
170
  }
171
172
  /**
173
   * Clears the caches then re-renders the content.
174
   */
175
  public void refresh() {
176
    mPreview.clearCache();
177
    rerender();
178
  }
179
180
  /**
181
   * Recomputes the HTML head then renders the document.
182
   */
183
  private void rerender() {
184
    mHead = generateHead();
185
    render( mDocument.toString() );
186
  }
187
188
  /**
189
   * Attaches the HTML head prefix and HTML tail suffix to the given HTML
190
   * string.
191
   *
192
   * @param html The HTML to adorn with opening and closing tags.
193
   * @return A complete HTML document, ready for rendering.
194
   */
195
  private String decorate( final String html ) {
196
    mDocument.setLength( 0 );
197
    mDocument.append( html );
198
199
    // Head and tail must be separate from document due to re-rendering.
200
    return mHead + mDocument + HTML_TAIL;
201
  }
202
203
  /**
204
   * Called when settings are changed that affect the HTML document preamble.
205
   * This is a minor performance optimization to avoid generating the head
206
   * each time that the document itself changes.
207
   *
208
   * @return A new doctype and HTML {@code head} element.
209
   */
210
  private String generateHead() {
211
    final var locale = getLocale();
212
    final var base = getBaseUri();
213
    final var custom = getCustomStylesheetUrl();
214
215
    // Point sizes are converted to pixels because of a rendering bug.
216
    return format(
217
      HTML_HEAD,
218
      locale.getLanguage(),
219
      toStylesheetString( HTML_STYLE_PREVIEW ),
220
      toStylesheetString( toUrl( locale ) ),
221
      toStylesheetString( custom ),
222
      getFontFamily(),
223
      toPixels( getFontSize() ),
224
      base.isBlank() ? "" : format( HTML_BASE, base )
225
    );
226
  }
227
228
  /**
229
   * Clears the preview pane by rendering an empty string.
230
   */
231
  public void clear() {
232
    render( "" );
233
  }
234
235
  /**
236
   * Sets the base URI to the containing directory the file being edited.
237
   *
238
   * @param path The path to the file being edited.
239
   */
240
  public void setBaseUri( final Path path ) {
241
    final var parent = path.getParent();
242
    mBaseUriPath = parent == null ? "" : parent.toUri().toString();
243
  }
244
245
  /**
246
   * Scrolls to the closest element matching the given identifier without
247
   * waiting for the document to be ready.
248
   *
249
   * @param id Scroll the preview pane to this unique paragraph identifier.
250
   */
251
  public void scrollTo( final String id ) {
252
    if( !mScrollLocked ) {
253
      mPreview.scrollTo( id, mScrollPane );
254
      mScrollPane.repaint();
255
    }
256
  }
257
258
  private String getBaseUri() {
259
    return mBaseUriPath;
260
  }
261
262
  private JScrollPane getScrollPane() {
263
    return mScrollPane;
264
  }
265
266
  public JScrollBar getVerticalScrollBar() {
267
    return getScrollPane().getVerticalScrollBar();
268
  }
269
270
  /**
271
   * Returns the ISO 639 alpha-2 or alpha-3 language code followed by a hyphen
272
   * followed by the ISO 15924 alpha-4 script code, followed by an ISO 3166
273
   * alpha-2 country code or UN M.49 numeric-3 area code. For example, this
274
   * could return {@code en-Latn-CA} for Canadian English written in the Latin
275
   * character set.
276
   *
277
   * @return Unique identifier for language and country.
278
   */
279
  private static URL toUrl( final Locale locale ) {
280
    return toUrl(
281
      String.format(
282
        sSettings.getSetting( STYLESHEET_PREVIEW_LOCALE, "" ),
283
        locale.getLanguage(),
284
        locale.getScript(),
285
        locale.getCountry()
286
      )
287
    );
288
  }
289
290
  private static URL toUrl( final String path ) {
291
    return HtmlPreview.class.getResource( path );
292
  }
293
294
  private Locale getLocale() {
295
    return localeProperty().toLocale();
296
  }
297
298
  private LocaleProperty localeProperty() {
299
    return mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
300
  }
301
302
  private String getFontFamily() {
303
    return fontFamilyProperty().get();
304
  }
305
306
  private StringProperty fontFamilyProperty() {
307
    return mWorkspace.stringProperty( KEY_UI_FONT_PREVIEW_NAME );
308
  }
309
310
  private double getFontSize() {
311
    return fontSizeProperty().get();
312
  }
313
314
  /**
315
   * Returns the font size in points.
316
   *
317
   * @return The user-defined font size (in pt).
318
   */
319
  private DoubleProperty fontSizeProperty() {
320
    return mWorkspace.doubleProperty( KEY_UI_FONT_PREVIEW_SIZE );
321
  }
322
323
  private String getLockText( final boolean locked ) {
324
    return Character.toString( (locked ? LOCK : UNLOCK_ALT).getChar() );
325
  }
326
327
  private URL getCustomStylesheetUrl() {
328
    try {
329
      return mWorkspace.getFile( KEY_UI_PREVIEW_STYLESHEET ).toURI().toURL();
330
    } catch( final Exception ex ) {
331
      clue( ex );
332
      return null;
333
    }
334
  }
335
336
  /**
337
   * Maps keyboard events to scrollbar commands so that users may control
338
   * the {@link HtmlPreview} panel using the keyboard.
339
   *
340
   * @param map The map to update with keyboard events.
341
   */
342
  private void addKeyboardEvents( final InputMap map ) {
343
    map.put( getKeyStroke( VK_DOWN, 0 ), "positiveUnitIncrement" );
344
    map.put( getKeyStroke( VK_UP, 0 ), "negativeUnitIncrement" );
345
    map.put( getKeyStroke( VK_PAGE_DOWN, 0 ), "positiveBlockIncrement" );
346
    map.put( getKeyStroke( VK_PAGE_UP, 0 ), "negativeBlockIncrement" );
347
    map.put( getKeyStroke( VK_HOME, 0 ), "minScroll" );
348
    map.put( getKeyStroke( VK_END, 0 ), "maxScroll" );
349
  }
350
351
  @Override
352
  public void componentResized( final ComponentEvent e ) {
353
    if( mWorkspace.getBoolean( KEY_IMAGE_RESIZE ) ) {
354
      mPreview.clearCache();
355
    }
356
357
    // Force update on the Swing EDT, otherwise the scrollbar and content
358
    // will not be updated correctly on some platforms.
359
    invokeLater( () -> getContent().repaint() );
360
  }
361
362
  @Override
363
  public void componentMoved( final ComponentEvent e ) {}
364
365
  @Override
366
  public void componentShown( final ComponentEvent e ) {}
367
368
  @Override
369
  public void componentHidden( final ComponentEvent e ) {}
370
371
  private static String toStylesheetString( final URL url ) {
372
    return url == null ? "" : format( HTML_STYLESHEET, url );
373
  }
374
}
1375
A src/main/java/com/keenwrite/preview/HtmlRenderer.java
1
package com.keenwrite.preview;
2
3
import org.w3c.dom.Document;
4
5
import javax.swing.*;
6
7
/**
8
 * Denotes the ability to render an HTML document onto a Swing component.
9
 */
10
public interface HtmlRenderer {
11
12
  /**
13
   * Renders an HTML document with respect to a base location.
14
   *
15
   * @param doc     The document to render.
16
   * @param baseUri The document's relative URI.
17
   */
18
  void render( final Document doc, final String baseUri );
19
20
  /**
21
   * Scrolls the given {@link JScrollPane} to the first HTML element that
22
   * has an {@code id} attribute that matches the given identifier.
23
   *
24
   * @param id         The HTML element identifier.
25
   * @param scrollPane The GUI widget that controls scrolling.
26
   */
27
  void scrollTo( final String id, final JScrollPane scrollPane );
28
29
  /**
30
   * Clears the cache (e.g., so that images are re-rendered using updated
31
   * dimensions).
32
   */
33
  void clearCache();
34
}
135
A src/main/java/com/keenwrite/preview/ImageReplacedElementFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.preview;
6
7
import com.keenwrite.io.MediaType;
8
import com.keenwrite.ui.adapters.ReplacedElementAdapter;
9
import io.sf.carte.echosvg.transcoder.TranscoderException;
10
import org.w3c.dom.Element;
11
import org.xhtmlrenderer.extend.ReplacedElement;
12
import org.xhtmlrenderer.extend.UserAgentCallback;
13
import org.xhtmlrenderer.layout.LayoutContext;
14
import org.xhtmlrenderer.render.BlockBox;
15
import org.xhtmlrenderer.swing.ImageReplacedElement;
16
17
import javax.imageio.ImageIO;
18
import java.awt.image.BufferedImage;
19
import java.io.IOException;
20
import java.net.URI;
21
import java.net.URISyntaxException;
22
import java.nio.file.Files;
23
import java.nio.file.Path;
24
import java.nio.file.Paths;
25
import java.text.ParseException;
26
27
import static com.keenwrite.events.StatusEvent.clue;
28
import static com.keenwrite.io.downloads.DownloadManager.open;
29
import static com.keenwrite.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER;
30
import static com.keenwrite.preview.SvgRasterizer.rasterize;
31
import static com.keenwrite.processors.markdown.extensions.tex.TexNode.HTML_TEX;
32
import static com.keenwrite.util.ProtocolScheme.getProtocol;
33
34
/**
35
 * Responsible for running {@link SvgRasterizer} on SVG images detected within
36
 * a document to transform them into rasterized versions. This will fall back
37
 * to loading rasterized images from a file if not detected as SVG.
38
 */
39
public final class ImageReplacedElementFactory extends ReplacedElementAdapter {
40
41
  public static final String HTML_IMAGE = "img";
42
  public static final String HTML_IMAGE_SRC = "src";
43
44
  private static final ImageReplacedElement BROKEN_IMAGE =
45
    createElement( BROKEN_IMAGE_PLACEHOLDER );
46
47
  @Override
48
  public ReplacedElement createReplacedElement(
49
    final LayoutContext c,
50
    final BlockBox box,
51
    final UserAgentCallback uac,
52
    final int cssWidth,
53
    final int cssHeight ) {
54
    final var e = box.getElement();
55
56
    try {
57
      final BufferedImage raster =
58
        switch( e.getNodeName() ) {
59
          case HTML_IMAGE -> createHtmlImage( box, e, uac );
60
          case HTML_TEX -> createTexImage( e );
61
          default -> null;
62
        };
63
64
      return createElement( raster );
65
    } catch( final Exception ex ) {
66
      clue( "Main.status.image.request.error.create", ex );
67
    }
68
69
    return BROKEN_IMAGE;
70
  }
71
72
  /**
73
   * Convert an HTML element to a raster graphic.
74
   */
75
  private static BufferedImage createHtmlImage(
76
    final BlockBox box,
77
    final Element e,
78
    final UserAgentCallback uac )
79
    throws TranscoderException, URISyntaxException, IOException {
80
    final var source = e.getAttribute( HTML_IMAGE_SRC );
81
    final var mediaType = MediaType.fromFilename( source );
82
83
    URI uri = null;
84
    BufferedImage raster = null;
85
86
    final var w = box.getContentWidth();
87
88
    if( getProtocol( source ).isRemote() ) {
89
      try( final var response = open( source );
90
           final var stream = response.getInputStream() ) {
91
92
        // Rasterize SVG from URL resource.
93
        raster = response.isSvg()
94
          ? rasterize( stream, w )
95
          : ImageIO.read( stream );
96
97
        clue( "Main.status.image.request.fetch", source );
98
      }
99
    }
100
    else if( mediaType.isSvg() ) {
101
      uri = resolve( source, uac, e );
102
    }
103
104
    if( uri != null && w > 0 ) {
105
      raster = rasterize( uri, w );
106
    }
107
108
    // Not an SVG, attempt to read a local rasterized image.
109
    if( raster == null && mediaType.isImage() ) {
110
      uri = resolve( source, uac, e );
111
112
      final var path = Paths.get( uri );
113
114
      try( final var stream = Files.newInputStream( path ) ) {
115
        raster = ImageIO.read( stream );
116
      }
117
    }
118
119
    // Image path resolved; image rendered successfully.
120
    clue();
121
122
    return raster;
123
  }
124
125
  /**
126
   * Attempt to rasterize based on file name.
127
   *
128
   * @param source The source URI from the document.
129
   * @param uac    A callback for retrieving the image resource.
130
   * @param e      The HTML element containing a reference to a file.
131
   * @return The resolved URI to the file.
132
   * @throws URISyntaxException Could not resolve URI.
133
   */
134
  private static URI resolve(
135
    final String source,
136
    final UserAgentCallback uac,
137
    final Element e )
138
    throws URISyntaxException {
139
    final var nSource = source.replaceAll( "\\\\", "/" );
140
141
    try {
142
      final var baseUri = new URI( uac.getBaseURL() );
143
      final var resolved = baseUri.resolve( nSource );
144
      final var path = resolved.normalize();
145
      clue( "Main.status.image.request.resolve", path );
146
147
      return path.isAbsolute() ? path : resolve( nSource, e );
148
    } catch( final Exception ex ) {
149
      clue( "Main.status.image.request.error.resolve", nSource );
150
      throw new URISyntaxException( nSource, ex.getMessage() );
151
    }
152
  }
153
154
  private static URI resolve( final String source, final Element e )
155
    throws URISyntaxException {
156
    final var base = new URI( e.getBaseURI() ).getPath();
157
    return Path.of( base, source ).toUri();
158
  }
159
160
  /**
161
   * Convert the TeX element to a raster graphic.
162
   */
163
  private BufferedImage createTexImage( final Element e )
164
    throws TranscoderException, ParseException {
165
    return rasterize( MathRenderer.toString( e.getTextContent() ) );
166
  }
167
168
  private static ImageReplacedElement createElement( final BufferedImage bi ) {
169
    return bi == null
170
      ? BROKEN_IMAGE
171
      : new ImageReplacedElement( bi, bi.getWidth(), bi.getHeight() );
172
  }
173
}
1174
A src/main/java/com/keenwrite/preview/MathRenderer.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import com.keenwrite.dom.DocumentParser;
5
import com.whitemagicsoftware.keentype.lib.KeenType;
6
import javafx.beans.property.DoubleProperty;
7
import javafx.beans.property.SimpleDoubleProperty;
8
import org.w3c.dom.Document;
9
10
import static com.keenwrite.events.StatusEvent.clue;
11
12
/**
13
 * Responsible for rendering formulas as scalable vector graphics (SVG).
14
 */
15
public final class MathRenderer {
16
17
  private static KeenType sTypesetter;
18
19
  static {
20
    try {
21
      sTypesetter = new KeenType( false );
22
    } catch( final Exception e ) {
23
      clue( e );
24
    }
25
  }
26
27
  private static final DoubleProperty sSize = new SimpleDoubleProperty( 2 );
28
29
  private MathRenderer() { }
30
31
  public static void bindSize( final DoubleProperty size ) {
32
    sSize.bind( size );
33
  }
34
35
  /**
36
   * Converts a TeX-based equation into an SVG document.
37
   *
38
   * @param equation A mathematical expression to render, without sigils.
39
   * @return The given string with all formulas transformed into SVG format.
40
   */
41
  public static Document toDocument( final String equation ) {
42
    return DocumentParser.parse( toString( equation ) );
43
  }
44
45
  /**
46
   * Converts a TeX-based equation into an SVG document.
47
   *
48
   * @param equation A mathematical expression to render, without sigils.
49
   * @return The given string with all formulas transformed into SVG format.
50
   */
51
  public static String toString( final String equation ) {
52
    return sTypesetter.toSvg( "$" + equation + "$", sSize.doubleValue() );
53
  }
54
}
155
A src/main/java/com/keenwrite/preview/SmoothImageReplacedElement.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import com.keenwrite.preview.images.Lanczos3Filter;
5
import com.keenwrite.preview.images.ResampleOp;
6
import org.xhtmlrenderer.swing.ImageReplacedElement;
7
8
import java.awt.*;
9
import java.awt.image.BufferedImage;
10
11
/**
12
 * Responsible for scaling an image using a Lanczos-3 filter, typically for
13
 * down-sampling.
14
 */
15
public final class SmoothImageReplacedElement extends ImageReplacedElement {
16
  private static final Lanczos3Filter FILTER = new Lanczos3Filter();
17
18
  /**
19
   * Creates a high-quality rescaled version of the given image. The
20
   * aspect ratio is maintained if either width or height is less than 1.
21
   *
22
   * @param source An instance of {@link BufferedImage} to rescale.
23
   * @param width  Rescale the given image to this width (px).
24
   * @param height Rescale the given image to this height (px).
25
   */
26
  public SmoothImageReplacedElement(
27
    final Image source, final int width, final int height ) {
28
    super._image = rescale( source, width, height );
29
  }
30
31
  private BufferedImage rescale(
32
    final Image source, final int w, final int h ) {
33
    final var bi = (BufferedImage) source;
34
    final var dim = rescaleDimensions( bi, w, h );
35
36
    final var resampleOp = new ResampleOp( FILTER, dim.width, dim.height );
37
    return resampleOp.filter( bi, null );
38
  }
39
40
  /**
41
   * Calculates scaled dimensions while maintaining the image aspect ratio.
42
   */
43
  private Dimension rescaleDimensions(
44
    final BufferedImage bi, final int width, final int height ) {
45
    final var oldW = bi.getWidth();
46
    final var oldH = bi.getHeight();
47
48
    int newW = width;
49
    int newH = height;
50
51
    if( newW <= 0 ) {
52
      newW = (int) (oldW * ((double) newH / oldH));
53
    }
54
55
    if( newH <= 0 ) {
56
      newH = (int) (oldH * ((double) newW / oldW));
57
    }
58
59
    if( newW <= 0 && newH <= 0 ) {
60
      newW = oldW;
61
      newH = oldH;
62
    }
63
64
    return new Dimension( newW, newH );
65
  }
66
}
167
A src/main/java/com/keenwrite/preview/SvgRasterizer.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import io.sf.carte.echosvg.anim.dom.SAXSVGDocumentFactory;
5
import io.sf.carte.echosvg.bridge.BridgeContext;
6
import io.sf.carte.echosvg.bridge.DocumentLoader;
7
import io.sf.carte.echosvg.bridge.UserAgent;
8
import io.sf.carte.echosvg.bridge.UserAgentAdapter;
9
import io.sf.carte.echosvg.gvt.renderer.ImageRenderer;
10
import io.sf.carte.echosvg.transcoder.*;
11
import io.sf.carte.echosvg.transcoder.image.ImageTranscoder;
12
import org.w3c.dom.Document;
13
import org.w3c.dom.Element;
14
15
import java.awt.*;
16
import java.awt.image.BufferedImage;
17
import java.io.File;
18
import java.io.InputStream;
19
import java.io.StringReader;
20
import java.net.URI;
21
import java.nio.file.Path;
22
import java.text.NumberFormat;
23
import java.text.ParseException;
24
import java.util.HashMap;
25
import java.util.Map;
26
27
import static com.keenwrite.dom.DocumentParser.transform;
28
import static com.keenwrite.events.StatusEvent.clue;
29
import static com.keenwrite.preview.HighQualityRenderingHints.RENDERING_HINTS;
30
import static io.sf.carte.echosvg.bridge.UnitProcessor.createContext;
31
import static io.sf.carte.echosvg.bridge.UnitProcessor.svgHorizontalLengthToUserSpace;
32
import static io.sf.carte.echosvg.transcoder.SVGAbstractTranscoder.*;
33
import static io.sf.carte.echosvg.transcoder.TranscodingHints.Key;
34
import static io.sf.carte.echosvg.util.SVGConstants.SVG_HEIGHT_ATTRIBUTE;
35
import static io.sf.carte.echosvg.util.SVGConstants.SVG_WIDTH_ATTRIBUTE;
36
import static java.awt.image.BufferedImage.TYPE_INT_RGB;
37
import static java.text.NumberFormat.getIntegerInstance;
38
39
/**
40
 * Responsible for converting SVG images into rasterized PNG images.
41
 */
42
public final class SvgRasterizer {
43
44
  /**
45
   * Prevent rudely barfing stack traces to the console.
46
   */
47
  private static final class SvgErrorHandler implements ErrorHandler {
48
    @Override
49
    public void error( final TranscoderException ex ) {
50
      clue( ex );
51
    }
52
53
    @Override
54
    public void fatalError( final TranscoderException ex ) {
55
      clue( ex );
56
    }
57
58
    @Override
59
    public void warning( final TranscoderException ex ) {
60
      clue( ex );
61
    }
62
  }
63
64
  private static final UserAgent USER_AGENT = new UserAgentAdapter();
65
  private static final BridgeContext BRIDGE_CONTEXT = new BridgeContext(
66
    USER_AGENT, new DocumentLoader( USER_AGENT )
67
  );
68
  private static final ErrorHandler sErrorHandler = new SvgErrorHandler();
69
70
  private static final SAXSVGDocumentFactory FACTORY_DOM =
71
    new SAXSVGDocumentFactory();
72
73
  private static final NumberFormat INT_FORMAT = getIntegerInstance();
74
75
  public static final BufferedImage BROKEN_IMAGE_PLACEHOLDER;
76
77
  /**
78
   * A FontAwesome camera icon, cleft asunder.
79
   */
80
  public static final String BROKEN_IMAGE_SVG =
81
    "<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www" +
82
      ".w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c" +
83
      ".332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path " +
84
      "d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531" +
85
      ".03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2" +
86
      ".511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-" +
87
      ".367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1" +
88
      ".699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 " +
89
      "2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094" +
90
      ".0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5" +
91
      ".0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-" +
92
      ".195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4" +
93
      ".191406-3.652344.207031-.015625.414063-.015625.617187-.007812l" +
94
      ".933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2" +
95
      ".820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3" +
96
      ".429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3" +
97
      ".429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 " +
98
      ".140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281" +
99
      ".808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2" +
100
      ".472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4" +
101
      ".285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1" +
102
      ".9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-" +
103
      ".667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1" +
104
      ".253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 " +
105
      "0'/></g></svg>";
106
107
  static {
108
    // The width and height cannot be embedded in the SVG above because the
109
    // path element values are relative to the viewBox dimensions.
110
    final int w = 75;
111
    final int h = 75;
112
    BufferedImage image;
113
114
    try {
115
      image = rasterizeImage( BROKEN_IMAGE_SVG, w );
116
    } catch( final Exception ex ) {
117
      image = new BufferedImage( w, h, TYPE_INT_RGB );
118
      final var graphics = (Graphics2D) image.getGraphics();
119
      graphics.setRenderingHints( RENDERING_HINTS );
120
121
      final var offset = (int) ((double) w / 4 / Math.PI);
122
123
      // Fall back to a (\) symbol.
124
      graphics.setColor( new Color( 204, 204, 204 ) );
125
      graphics.fillRect( 0, 0, w, h );
126
      graphics.setColor( new Color( 255, 204, 204 ) );
127
      graphics.setStroke( new BasicStroke( 4 ) );
128
      graphics.drawOval( w / 4, h / 4, w / 2, h / 2 );
129
      graphics.drawLine(
130
        w / 4 + offset,
131
        h / 4 + offset,
132
        w / 2 + w / 4 - offset,
133
        h / 2 + h / 4 - offset
134
      );
135
    }
136
137
    BROKEN_IMAGE_PLACEHOLDER = image;
138
  }
139
140
  /**
141
   * Responsible for creating a new {@link ImageRenderer} implementation that
142
   * can render a DOM as an SVG image.
143
   */
144
  private static class BufferedImageTranscoder extends ImageTranscoder {
145
    private BufferedImage mImage;
146
147
    /**
148
     * Prevent barfing a stack trace when the transcoder encounters problems
149
     * parsing SVG contents.
150
     */
151
    @Override
152
    protected UserAgent createUserAgent() {
153
      return new SVGAbstractTranscoderUserAgent() {
154
        @Override
155
        public void displayError( final Exception ex ) {
156
          clue( ex );
157
        }
158
      };
159
    }
160
161
    @Override
162
    public BufferedImage createImage( final int w, final int h ) {
163
      return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB );
164
    }
165
166
    @Override
167
    public void writeImage(
168
      final BufferedImage image, final TranscoderOutput output ) {
169
      mImage = image;
170
    }
171
172
    public BufferedImage getImage() {
173
      return mImage;
174
    }
175
176
    @Override
177
    protected ImageRenderer createRenderer() {
178
      final ImageRenderer renderer = super.createRenderer();
179
      final RenderingHints hints = renderer.getRenderingHints();
180
      hints.putAll( RENDERING_HINTS );
181
      renderer.setRenderingHints( hints );
182
183
      return renderer;
184
    }
185
  }
186
187
  /**
188
   * Rasterizes the given SVG input stream into an image.
189
   *
190
   * @param svg The SVG data to rasterize, must be closed by caller.
191
   * @return The given input stream converted to a rasterized image.
192
   */
193
  public static BufferedImage rasterize( final String svg )
194
    throws TranscoderException, ParseException {
195
    return rasterize( toDocument( svg ) );
196
  }
197
198
  /**
199
   * Rasterizes the given SVG input stream into an image at 96 DPI.
200
   *
201
   * @param svg The SVG data to rasterize, must be closed by caller.
202
   * @return The given input stream converted to a rasterized image.
203
   */
204
  public static BufferedImage rasterize( final InputStream svg )
205
    throws TranscoderException {
206
    return rasterize( svg, 96 );
207
  }
208
209
  /**
210
   * Rasterizes the given SVG input stream into an image.
211
   *
212
   * @param svg The SVG data to rasterize, must be closed by caller.
213
   * @param dpi Resolution to use when rasterizing (default is 96 DPI).
214
   * @return The given input stream converted to a rasterized image at the
215
   * given resolution.
216
   */
217
  public static BufferedImage rasterize(
218
    final InputStream svg, final float dpi ) throws TranscoderException {
219
    return rasterize(
220
      new TranscoderInput( svg ),
221
      KEY_RESOLUTION_DPI,
222
      1f / dpi * 25.4f
223
    );
224
  }
225
226
  /**
227
   * Rasterizes the given document into an image.
228
   *
229
   * @param svg   The SVG {@link Document} to rasterize.
230
   * @param width The rasterized image's width (in pixels).
231
   * @return The rasterized image.
232
   */
233
  public static BufferedImage rasterize(
234
    final Document svg, final int width ) throws TranscoderException {
235
    return rasterize(
236
      new TranscoderInput( svg ),
237
      KEY_WIDTH,
238
      fit( svg.getDocumentElement(), width )
239
    );
240
  }
241
242
  /**
243
   * Rasterizes the given vector graphic file using the width dimension
244
   * specified by the document's width attribute.
245
   *
246
   * @param document The {@link Document} containing a vector graphic.
247
   * @return A rasterized image as an instance of {@link BufferedImage}, or
248
   * {@link #BROKEN_IMAGE_PLACEHOLDER} if the graphic could not be rasterized.
249
   */
250
  public static BufferedImage rasterize( final Document document )
251
    throws ParseException, TranscoderException {
252
    final var root = document.getDocumentElement();
253
    final var width = root.getAttribute( SVG_WIDTH_ATTRIBUTE );
254
255
    return rasterize( document, INT_FORMAT.parse( width ).intValue() );
256
  }
257
258
  /**
259
   * Rasterizes the vector graphic file at the given URI. If any exception
260
   * happens, a broken image icon is returned instead.
261
   *
262
   * @param path  The {@link Path} to a vector graphic file.
263
   * @param width Scale the image to the given width (px); aspect ratio is
264
   *              maintained.
265
   * @return A rasterized image as an instance of {@link BufferedImage}.
266
   */
267
  public static BufferedImage rasterize( final Path path, final int width ) {
268
    return rasterize( path.toUri(), width );
269
  }
270
271
  /**
272
   * Rasterizes the vector graphic file at the given URI. If any exception
273
   * happens, a broken image icon is returned instead.
274
   *
275
   * @param uri   The URI to a vector graphic file, which must include the
276
   *              protocol scheme (such as <code>file://</code> or
277
   *              <code>https://</code>).
278
   * @param width Scale the image to the given width (px); aspect ratio is
279
   *              maintained.
280
   * @return A rasterized image as an instance of {@link BufferedImage}.
281
   */
282
  public static BufferedImage rasterize( final String uri, final int width ) {
283
    return rasterize( new File( uri ).toURI(), width );
284
  }
285
286
  /**
287
   * Converts an SVG drawing into a rasterized image that can be drawn on
288
   * a graphics context.
289
   *
290
   * @param uri   The path to the image (can be web address).
291
   * @param width Scale the image to the given width (px); aspect ratio is
292
   *              maintained.
293
   * @return The vector graphic transcoded into a raster image format.
294
   */
295
  public static BufferedImage rasterize( final URI uri, final int width ) {
296
    try {
297
      return rasterize( FACTORY_DOM.createDocument( uri.toString() ), width );
298
    } catch( final Exception ex ) {
299
      clue( ex );
300
    }
301
302
    return BROKEN_IMAGE_PLACEHOLDER;
303
  }
304
305
  /**
306
   * Converts an SVG string into a rasterized image that can be drawn on
307
   * a graphics context. The dimensions are determined from the document.
308
   *
309
   * @param svg   The SVG xml document.
310
   * @param scale The scaling factor to apply when transcoding.
311
   * @return The vector graphic transcoded into a raster image format.
312
   */
313
  @SuppressWarnings("unused")
314
  public static BufferedImage rasterizeImage(
315
    final String svg, final double scale )
316
    throws ParseException, TranscoderException {
317
    final var document = toDocument( svg );
318
    final var root = document.getDocumentElement();
319
    final var width = root.getAttribute( SVG_WIDTH_ATTRIBUTE );
320
    final var height = root.getAttribute( SVG_HEIGHT_ATTRIBUTE );
321
    final var w = INT_FORMAT.parse( width ).intValue() * scale;
322
    final var h = INT_FORMAT.parse( height ).intValue() * scale;
323
324
    return rasterize( svg, w, h );
325
  }
326
327
  /**
328
   * Converts an SVG string into a rasterized image that can be drawn on
329
   * a graphics context.
330
   *
331
   * @param svg The SVG xml document.
332
   * @param w   Scale the image width to this size (aspect ratio is
333
   *            maintained).
334
   * @return The vector graphic transcoded into a raster image format.
335
   */
336
  public static BufferedImage rasterizeImage( final String svg, final int w )
337
    throws TranscoderException {
338
    return rasterize( toDocument( svg ), w );
339
  }
340
341
  /**
342
   * Given a document object model (DOM) {@link Element}, this will convert that
343
   * element to a string.
344
   *
345
   * @param root The DOM node to convert to a string.
346
   * @return The DOM node as an escaped, plain text string.
347
   */
348
  public static String toSvg( final Element root ) {
349
    try {
350
      return transform( root ).replaceAll( "xmlns=\"\" ", "" );
351
    } catch( final Exception ex ) {
352
      clue( ex );
353
    }
354
355
    return BROKEN_IMAGE_SVG;
356
  }
357
358
  /**
359
   * Converts an SVG XML string into a new {@link Document} instance.
360
   *
361
   * @param xml The XML containing SVG elements.
362
   * @return The SVG contents parsed into a {@link Document} object model.
363
   */
364
  private static Document toDocument( final String xml ) {
365
    try( final var reader = new StringReader( xml ) ) {
366
      return FACTORY_DOM.createSVGDocument(
367
        "http://www.w3.org/2000/svg", reader );
368
    } catch( final Exception ex ) {
369
      throw new IllegalArgumentException( ex );
370
    }
371
  }
372
373
  /**
374
   * Creates a rasterized image of the given source document.
375
   *
376
   * @param input     The source document to transcode.
377
   * @param hintKey   Transcoding hint key.
378
   * @param hintValue Transcoding hint value.
379
   * @return A new {@link BufferedImageTranscoder} instance with the given
380
   * transcoding hint applied.
381
   */
382
  private static BufferedImage rasterize(
383
    final TranscoderInput input, final Key hintKey, final float hintValue )
384
    throws TranscoderException {
385
    final var hints = new HashMap<Key, Object>();
386
    hints.put( hintKey, hintValue );
387
388
    return rasterize( input, hints );
389
  }
390
391
  private static BufferedImage rasterize(
392
    final String svg, final double w, final double h )
393
    throws TranscoderException {
394
    final var hints = new HashMap<Key, Object>();
395
    hints.put( KEY_WIDTH, (float) w );
396
    hints.put( KEY_HEIGHT, (float) h );
397
398
    return rasterize( new TranscoderInput( toDocument( svg ) ), hints );
399
  }
400
401
  public static BufferedImage rasterize(
402
    final TranscoderInput input,
403
    final Map<TranscodingHints.Key, Object> hints ) throws TranscoderException {
404
    final var transcoder = new BufferedImageTranscoder();
405
406
    for( final var hint : hints.entrySet() ) {
407
      transcoder.addTranscodingHint( hint.getKey(), hint.getValue() );
408
    }
409
410
    transcoder.setErrorHandler( sErrorHandler );
411
    transcoder.transcode( input, null );
412
413
    return transcoder.getImage();
414
  }
415
416
  /**
417
   * Returns either the given element's SVG document width, or the display
418
   * width, whichever is smaller.
419
   *
420
   * @param root  The SVG document's root node.
421
   * @param width The display width (e.g., rendering canvas width).
422
   * @return The lower value of the document's width or the display width.
423
   */
424
  @SuppressWarnings("ConstantValue")
425
  private static float fit( final Element root, final int width ) {
426
    final var w = root.getAttribute( SVG_WIDTH_ATTRIBUTE );
427
428
    return w == null || w.isBlank()
429
      ? width
430
      : fit( root, w, width );
431
  }
432
433
  /**
434
   * Returns the width in user space units (pixels?).
435
   *
436
   * @param root  The element containing the width attribute.
437
   * @param w     The element's width attribute value.
438
   * @param width The rendering canvas width.
439
   * @return Either the rendering canvas width or SVG document width,
440
   * whichever is smaller.
441
   */
442
  private static float fit(
443
    final Element root, final String w, final int width ) {
444
    final var usWidth = svgHorizontalLengthToUserSpace(
445
      w, SVG_WIDTH_ATTRIBUTE, createContext( BRIDGE_CONTEXT, root )
446
    );
447
448
    // If the image is too small, scale it to 1/4 the canvas width.
449
    return Math.min( usWidth < 5 ? width / 4.0f : usWidth, (float) width );
450
  }
451
}
1452
A src/main/java/com/keenwrite/preview/images/AdvancedResizeOp.java
1
/*
2
 * Copyright 2013, Morten Nobel-Joergensen
3
 *
4
 * License: The BSD 3-Clause License
5
 * http://opensource.org/licenses/BSD-3-Clause
6
 */
7
package com.keenwrite.preview.images;
8
9
import java.awt.*;
10
import java.awt.geom.Point2D;
11
import java.awt.geom.Rectangle2D;
12
import java.awt.image.BufferedImage;
13
import java.awt.image.BufferedImageOp;
14
import java.awt.image.ColorModel;
15
16
/**
17
 * @author Morten Nobel-Joergensen
18
 */
19
public abstract class AdvancedResizeOp implements BufferedImageOp {
20
  private final ConstrainedDimension dimensionConstrain;
21
22
  public AdvancedResizeOp( ConstrainedDimension dimensionConstrain ) {
23
    this.dimensionConstrain = dimensionConstrain;
24
  }
25
26
  public final BufferedImage filter( BufferedImage src, BufferedImage dest ) {
27
    Dimension dstDimension = dimensionConstrain.getDimension(
28
      new Dimension( src.getWidth(), src.getHeight() ) );
29
    int dstWidth = dstDimension.width;
30
    int dstHeight = dstDimension.height;
31
32
    return doFilter( src, dest, dstWidth, dstHeight );
33
  }
34
35
  protected abstract BufferedImage doFilter(
36
    BufferedImage src, BufferedImage dest, int dstWidth, int dstHeight );
37
38
  @Override
39
  public final Rectangle2D getBounds2D( BufferedImage src ) {
40
    return new Rectangle( 0, 0, src.getWidth(), src.getHeight() );
41
  }
42
43
  @Override
44
  public final BufferedImage createCompatibleDestImage(
45
    BufferedImage src, ColorModel destCM ) {
46
    if( destCM == null ) {
47
      destCM = src.getColorModel();
48
    }
49
50
    return new BufferedImage(
51
      destCM,
52
      destCM.createCompatibleWritableRaster( src.getWidth(), src.getHeight() ),
53
      destCM.isAlphaPremultiplied(),
54
      null );
55
  }
56
57
  @Override
58
  public final Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) {
59
    return (Point2D) srcPt.clone();
60
  }
61
62
  @Override
63
  public final RenderingHints getRenderingHints() {
64
    return null;
65
  }
66
}
167
A src/main/java/com/keenwrite/preview/images/ConstrainedDimension.java
1
/*
2
 * Copyright 2013, Morten Nobel-Joergensen
3
 *
4
 * SPDX-License-Identifier: BSD-3-Clause
5
 */
6
package com.keenwrite.preview.images;
7
8
import java.awt.*;
9
10
/**
11
 * This class let you create dimension constrains based on an actual image.
12
 */
13
public class ConstrainedDimension {
14
  private ConstrainedDimension() {
15
  }
16
17
  /**
18
   * Will always return a dimension with positive width and height;
19
   *
20
   * @param dimension of the unscaled image
21
   * @return the dimension of the scaled image
22
   */
23
  public Dimension getDimension( Dimension dimension ) {
24
    return dimension;
25
  }
26
27
  /**
28
   * Used when the destination size is fixed. This may not keep the image
29
   * aspect radio.
30
   *
31
   * @param width  destination dimension width
32
   * @param height destination dimension height
33
   * @return destination dimension (width x height)
34
   */
35
  public static ConstrainedDimension createAbsolutionDimension(
36
    final int width, final int height ) {
37
    assert width > 0 : "Width must be positive integer";
38
    assert height > 0 : "Height must be positive integer";
39
40
    return new ConstrainedDimension() {
41
      public Dimension getDimension( Dimension dimension ) {
42
        return new Dimension( width, height );
43
      }
44
    };
45
  }
46
}
147
A src/main/java/com/keenwrite/preview/images/ImageUtils.java
1
/*
2
 * Copyright 2013, Morten Nobel-Joergensen
3
 *
4
 * License: The BSD 3-Clause License
5
 * http://opensource.org/licenses/BSD-3-Clause
6
 */
7
package com.keenwrite.preview.images;
8
9
import java.awt.*;
10
import java.awt.image.BufferedImage;
11
import java.awt.image.Raster;
12
import java.awt.image.WritableRaster;
13
14
import static java.awt.image.BufferedImage.*;
15
16
/**
17
 * @author Heinz Doerr
18
 * @author Morten Nobel-Joergensen
19
 */
20
public final class ImageUtils {
21
  @SuppressWarnings( "DuplicateBranchesInSwitch" )
22
  static int nrChannels( final BufferedImage img ) {
23
    return switch( img.getType() ) {
24
      case TYPE_3BYTE_BGR -> 3;
25
      case TYPE_4BYTE_ABGR -> 4;
26
      case TYPE_BYTE_GRAY -> 1;
27
      case TYPE_INT_BGR -> 3;
28
      case TYPE_INT_ARGB -> 4;
29
      case TYPE_INT_RGB -> 3;
30
      case TYPE_CUSTOM -> 4;
31
      case TYPE_4BYTE_ABGR_PRE -> 4;
32
      case TYPE_INT_ARGB_PRE -> 4;
33
      case TYPE_USHORT_555_RGB -> 3;
34
      case TYPE_USHORT_565_RGB -> 3;
35
      case TYPE_USHORT_GRAY -> 1;
36
      default -> 0;
37
    };
38
  }
39
40
  /**
41
   * returns one row (height == 1) of byte packed image data in BGR or AGBR form
42
   *
43
   * @param temp must be either null or an array with length of w*h
44
   */
45
  static void getPixelsBGR(
46
    BufferedImage img, int y, int w, byte[] array, int[] temp ) {
47
    final int x = 0;
48
    final int h = 1;
49
50
    assert array.length == temp.length * nrChannels( img );
51
    assert temp.length == w;
52
53
    final Raster raster;
54
    switch( img.getType() ) {
55
      case TYPE_3BYTE_BGR, TYPE_4BYTE_ABGR,
56
        TYPE_4BYTE_ABGR_PRE, TYPE_BYTE_GRAY -> {
57
        raster = img.getRaster();
58
        //int ttype= raster.getTransferType();
59
        raster.getDataElements( x, y, w, h, array );
60
      }
61
      case TYPE_INT_BGR -> {
62
        raster = img.getRaster();
63
        raster.getDataElements( x, y, w, h, temp );
64
        ints2bytes( temp, array, 0, 1, 2 );  // bgr -->  bgr
65
      }
66
      case TYPE_INT_RGB -> {
67
        raster = img.getRaster();
68
        raster.getDataElements( x, y, w, h, temp );
69
        ints2bytes( temp, array, 2, 1, 0 );  // rgb -->  bgr
70
      }
71
      case TYPE_INT_ARGB, TYPE_INT_ARGB_PRE -> {
72
        raster = img.getRaster();
73
        raster.getDataElements( x, y, w, h, temp );
74
        ints2bytes( temp, array, 2, 1, 0, 3 );  // argb -->  abgr
75
      }
76
      case TYPE_CUSTOM -> {
77
        // loader, but else ???
78
        img.getRGB( x, y, w, h, temp, 0, w );
79
        ints2bytes( temp, array, 2, 1, 0, 3 );  // argb -->  abgr
80
      }
81
      default -> {
82
        img.getRGB( x, y, w, h, temp, 0, w );
83
        ints2bytes( temp, array, 2, 1, 0 );  // rgb -->  bgr
84
      }
85
    }
86
  }
87
88
  /**
89
   * converts and copies byte packed  BGR or ABGR into the img buffer,
90
   * the img type may vary (e.g. RGB or BGR, int or byte packed)
91
   * but the number of components (w/o alpha, w alpha, gray) must match
92
   * <p>
93
   * does not unmange the image for all (A)RGN and (A)BGR and gray imaged
94
   */
95
  public static void setBGRPixels( byte[] bgrPixels, BufferedImage img, int x,
96
                                   int y, int w, int h ) {
97
    int imageType = img.getType();
98
    WritableRaster raster = img.getRaster();
99
100
    if( imageType == TYPE_3BYTE_BGR ||
101
      imageType == TYPE_4BYTE_ABGR ||
102
      imageType == TYPE_4BYTE_ABGR_PRE ||
103
      imageType == TYPE_BYTE_GRAY ) {
104
      raster.setDataElements( x, y, w, h, bgrPixels );
105
    }
106
    else {
107
      int[] pixels;
108
      if( imageType == TYPE_INT_BGR ) {
109
        pixels = bytes2int( bgrPixels, 2, 1, 0 );  // bgr -->  bgr
110
      }
111
      else if( imageType == TYPE_INT_ARGB ||
112
        imageType == TYPE_INT_ARGB_PRE ) {
113
        pixels = bytes2int( bgrPixels, 3, 0, 1, 2 );  // abgr -->  argb
114
      }
115
      else {
116
        pixels = bytes2int( bgrPixels, 0, 1, 2 );  // bgr -->  rgb
117
      }
118
      if( w == 0 || h == 0 ) {
119
        return;
120
      }
121
      else if( pixels.length < w * h ) {
122
        throw new IllegalArgumentException( "pixels array must have a length" + " >= w*h" );
123
      }
124
      if( imageType == TYPE_INT_ARGB ||
125
        imageType == TYPE_INT_RGB ||
126
        imageType == TYPE_INT_ARGB_PRE ||
127
        imageType == TYPE_INT_BGR ) {
128
        raster.setDataElements( x, y, w, h, pixels );
129
      }
130
      else {
131
        // Unmanages the image
132
        img.setRGB( x, y, w, h, pixels, 0, w );
133
      }
134
    }
135
  }
136
137
  public static void ints2bytes( int[] in, byte[] out, int index1, int index2,
138
                                 int index3 ) {
139
    for( int i = 0; i < in.length; i++ ) {
140
      int index = i * 3;
141
      int value = in[ i ];
142
      out[ index + index1 ] = (byte) value;
143
      value = value >> 8;
144
      out[ index + index2 ] = (byte) value;
145
      value = value >> 8;
146
      out[ index + index3 ] = (byte) value;
147
    }
148
  }
149
150
  public static void ints2bytes( int[] in, byte[] out, int index1, int index2,
151
                                 int index3, int index4 ) {
152
    for( int i = 0; i < in.length; i++ ) {
153
      int index = i * 4;
154
      int value = in[ i ];
155
      out[ index + index1 ] = (byte) value;
156
      value = value >> 8;
157
      out[ index + index2 ] = (byte) value;
158
      value = value >> 8;
159
      out[ index + index3 ] = (byte) value;
160
      value = value >> 8;
161
      out[ index + index4 ] = (byte) value;
162
    }
163
  }
164
165
  public static int[] bytes2int( byte[] in, int index1, int index2,
166
                                 int index3 ) {
167
    int[] out = new int[ in.length / 3 ];
168
    for( int i = 0; i < out.length; i++ ) {
169
      int index = i * 3;
170
      int b1 = (in[ index + index1 ] & 0xff) << 16;
171
      int b2 = (in[ index + index2 ] & 0xff) << 8;
172
      int b3 = in[ index + index3 ] & 0xff;
173
      out[ i ] = b1 | b2 | b3;
174
    }
175
    return out;
176
  }
177
178
  public static int[] bytes2int( byte[] in, int index1, int index2, int index3,
179
                                 int index4 ) {
180
    int[] out = new int[ in.length / 4 ];
181
    for( int i = 0; i < out.length; i++ ) {
182
      int index = i * 4;
183
      int b1 = (in[ index + index1 ] & 0xff) << 24;
184
      int b2 = (in[ index + index2 ] & 0xff) << 16;
185
      int b3 = (in[ index + index3 ] & 0xff) << 8;
186
      int b4 = in[ index + index4 ] & 0xff;
187
      out[ i ] = b1 | b2 | b3 | b4;
188
    }
189
    return out;
190
  }
191
192
  public static BufferedImage convert( BufferedImage src, int bufImgType ) {
193
    BufferedImage img = new BufferedImage( src.getWidth(),
194
                                           src.getHeight(),
195
                                           bufImgType );
196
    Graphics2D g2d = img.createGraphics();
197
    g2d.drawImage( src, 0, 0, null );
198
    g2d.dispose();
199
    return img;
200
  }
201
}
1202
A src/main/java/com/keenwrite/preview/images/Lanczos3.java
1
package com.keenwrite.preview.images;
2
3
import java.awt.image.BufferedImage;
4
5
/**
6
 * Unused. Needs to extract image data from {@link BufferedImage} and create
7
 * down-sampled version.
8
 */
9
public class Lanczos3 {
10
  static double sinc( double x ) {
11
    x *= Math.PI;
12
13
    if( x < 0.01f && x > -0.01f ) {
14
      return 1.0f + x * x * (-1.0f / 6.0f + x * x * 1.0f / 120.0f);
15
    }
16
17
    return Math.sin( x ) / x;
18
  }
19
20
  static float clip( double t ) {
21
    final float eps = .0000125f;
22
23
    if( Math.abs( t ) < eps ) { return 0.0f; }
24
25
    return (float) t;
26
  }
27
28
  static float lancos( float t ) {
29
    if( t < 0.0f ) { t = -t; }
30
31
    if( t < 3.0f ) { return clip( sinc( t ) * sinc( t / 3.0f ) ); }
32
    else { return 0.0f; }
33
  }
34
35
  static float lancos3_resample_x(
36
    int[][] arr, int src_w, int src_h, int y, int x, float xscale ) {
37
    float s = 0;
38
    float coef_sum = 0.0f;
39
    float coef;
40
    float pix;
41
    int i;
42
43
    int l, r;
44
    float c;
45
    float hw;
46
47
    // For the reduction of the situation hw is equivalent to expanding the
48
    // number of pixels in the field, if you do not do this, the final
49
    // reduction of the image effect is not much different from the recent
50
    // field interpolation method, the effect is equivalent to the first
51
    // low-pass filtering, and then interpolate
52
    if( xscale > 1.0f ) { hw = 3.0f; }
53
    else { hw = 3.0f / xscale; }
54
55
    c = (float) x / xscale;
56
    l = (int) Math.floor( c - hw );
57
    r = (int) Math.ceil( c + hw );
58
59
    if( y < 0 ) { y = 0; }
60
    if( y >= src_h ) { y = src_h - 1; }
61
    if( xscale > 1.0f ) { xscale = 1.0f; }
62
    for( i = l; i <= r; i++ ) {
63
      x = Math.max( i, 0 );
64
      if( i >= src_w ) { x = src_w - 1; }
65
      pix = arr[ y ][ x ];
66
      coef = lancos( (c - i) * xscale );
67
      s += pix * coef;
68
      coef_sum += coef;
69
    }
70
    s /= coef_sum;
71
    return s;
72
  }
73
74
  static class uint8_2d {
75
    int[][] arr;
76
    int rows;
77
    int cols;
78
79
    public uint8_2d( final int h1, final int w1 ) {
80
      arr = new int[ h1 ][ w1 ];
81
      rows = h1;
82
      cols = w1;
83
    }
84
  }
85
86
  void img_resize_using_lancos3( uint8_2d src, uint8_2d dst ) {
87
    if( src == null || dst == null ) { return; }
88
89
    int src_rows, src_cols;
90
    int dst_rows, dst_cols;
91
    int i, j;
92
    int[][] src_arr;
93
    int[][] dst_arr;
94
    float xratio;
95
    float yratio;
96
    int val;
97
    int k;
98
    float hw;
99
100
    src_arr = src.arr;
101
    dst_arr = dst.arr;
102
    src_rows = src.rows;
103
    src_cols = src.cols;
104
    dst_rows = dst.rows;
105
    dst_cols = dst.cols;
106
107
    xratio = (float) dst_cols / (float) src_cols;
108
    yratio = (float) dst_rows / (float) src_rows;
109
110
    float scale;
111
112
    if( yratio > 1.0f ) {
113
      hw = 3.0f;
114
      scale = 1.0f;
115
    }
116
    else {
117
      hw = 3.0f / yratio;
118
      scale = yratio;
119
    }
120
121
    for( i = 0; i < dst_rows; i++ ) {
122
      for( j = 0; j < dst_cols; j++ ) {
123
        int t, b;
124
        float c;
125
126
        float s = 0;
127
        float coef_sum = 0.0f;
128
        float coef;
129
        float pix;
130
131
        c = (float) i / yratio;
132
        t = (int) Math.floor( c - hw );
133
        b = (int) Math.ceil( c + hw );
134
        // Interpolate in the x direction first, then interpolate in the y
135
        // direction.
136
        for( k = t; k <= b; k++ ) {
137
          pix = lancos3_resample_x( src_arr, src_cols, src_rows, k, j, xratio );
138
          coef = lancos( (c - k) * scale );
139
          coef_sum += coef;
140
          pix *= coef;
141
          s += pix;
142
        }
143
        val = (int) (s / coef_sum);
144
        if( val < 0 ) { val = 0; }
145
        if( val > 255 ) { val = 255; }
146
        dst_arr[ i ][ j ] = val;
147
      }
148
    }
149
  }
150
151
  BufferedImage test_lancos3_resize( BufferedImage img, float factor ) {
152
    assert img != null;
153
154
    uint8_2d r = null;
155
    uint8_2d g = null;
156
    uint8_2d b = null;
157
158
    BufferedImage out = null;
159
    // TODO: Split buffered image into RGB components.
160
    //split_img_data( img, r, g, b );
161
162
    int w, h;
163
    int w1, h1;
164
    w = img.getWidth();
165
    h = img.getHeight();
166
167
    // TODO: Maintain aspect ratio.
168
    w1 = (int) (factor * w);
169
    h1 = (int) (factor * h);
170
171
    uint8_2d r1 = new uint8_2d( h1, w1 );
172
    uint8_2d g1 = new uint8_2d( h1, w1 );
173
    uint8_2d b1 = new uint8_2d( h1, w1 );
174
175
    img_resize_using_lancos3( r, r1 );
176
    img_resize_using_lancos3( g, g1 );
177
    img_resize_using_lancos3( b, b1 );
178
179
    // TODO: Combine rescaled image into RGB components.
180
    //merge_img_data( r1, g1, b1, out);
181
182
    return out;
183
  }
184
}
1185
A src/main/java/com/keenwrite/preview/images/Lanczos3Filter.java
1
/*
2
 * Copyright 2013, Morten Nobel-Joergensen
3
 *
4
 * License: The BSD 3-Clause License
5
 * http://opensource.org/licenses/BSD-3-Clause
6
 */
7
package com.keenwrite.preview.images;
8
9
public final class Lanczos3Filter implements ResampleFilter {
10
  private static final float PI_FLOAT = (float) Math.PI;
11
12
  private float sincModified( float value ) {
13
    return (float) Math.sin( value ) / value;
14
  }
15
16
  public float apply( float value ) {
17
    if( value == 0 ) {
18
      return 1.0f;
19
    }
20
21
    if( value < 0.0f ) {
22
      value = -value;
23
    }
24
25
    if( value < 3.0f ) {
26
      value *= PI_FLOAT;
27
      return sincModified( value ) * sincModified( value / 3.0f );
28
    }
29
30
    return 0.0f;
31
  }
32
33
  public float getSamplingRadius() {
34
    return 3.0f;
35
  }
36
}
137
A src/main/java/com/keenwrite/preview/images/ResampleFilter.java
1
/*
2
 * Copyright 2013, Morten Nobel-Joergensen
3
 *
4
 * License: The BSD 3-Clause License
5
 * http://opensource.org/licenses/BSD-3-Clause
6
 */
7
package com.keenwrite.preview.images;
8
9
public interface ResampleFilter {
10
  float getSamplingRadius();
11
12
  float apply(float v);
13
}
114
A src/main/java/com/keenwrite/preview/images/ResampleOp.java
1
/*
2
 * Copyright 2013, Morten Nobel-Joergensen
3
 *
4
 * License: The BSD 3-Clause License
5
 * http://opensource.org/licenses/BSD-3-Clause
6
 */
7
package com.keenwrite.preview.images;
8
9
import java.awt.image.BufferedImage;
10
import java.util.concurrent.atomic.AtomicInteger;
11
12
import static com.keenwrite.preview.images.ConstrainedDimension.*;
13
import static java.awt.image.BufferedImage.*;
14
import static java.awt.image.DataBuffer.TYPE_USHORT;
15
import static java.lang.Runtime.getRuntime;
16
import static java.lang.String.format;
17
import static java.lang.Thread.currentThread;
18
19
/**
20
 * Based on <a href="http://schmidt.devlib.org/jiu/">Java Image Util</a>.
21
 * <p>
22
 * Note that the filter method is not thread-safe.
23
 * </p>
24
 *
25
 * @author Morten Nobel-Joergensen
26
 * @author Heinz Doerr
27
 */
28
public class ResampleOp extends AdvancedResizeOp {
29
  private static final int MAX_CHANNEL_VALUE = 255;
30
31
  private int nrChannels;
32
  private int srcWidth;
33
  private int srcHeight;
34
  private int dstWidth;
35
  private int dstHeight;
36
37
  static class SubSamplingData {
38
    // individual - per row or per column - nr of contributions
39
    private final int[] arrN;
40
    // 2Dim: [wid or hei][contrib]
41
    private final int[] arrPixel;
42
    // 2Dim: [wid or hei][contrib]
43
    private final float[] arrWeight;
44
    // the primary index length for the 2Dim arrays : arrPixel and arrWeight
45
    private final int numContributors;
46
47
    private SubSamplingData( int[] arrN, int[] arrPixel, float[] arrWeight,
48
                             int numContributors ) {
49
      this.arrN = arrN;
50
      this.arrPixel = arrPixel;
51
      this.arrWeight = arrWeight;
52
      this.numContributors = numContributors;
53
    }
54
55
    public int[] getArrN() {
56
      return arrN;
57
    }
58
  }
59
60
  private SubSamplingData horizontalSubsamplingData;
61
  private SubSamplingData verticalSubsamplingData;
62
63
  private final int threadCount = getRuntime().availableProcessors();
64
  private final AtomicInteger multipleInvocationLock = new AtomicInteger();
65
  private final ResampleFilter mFilter;
66
67
  public ResampleOp(
68
    final ResampleFilter filter,
69
    final int dstWidth,
70
    final int dstHeight ) {
71
    this( filter, createAbsolutionDimension( dstWidth, dstHeight ) );
72
  }
73
74
  public ResampleOp(
75
    final ResampleFilter filter, ConstrainedDimension dimensionConstrain ) {
76
    super( dimensionConstrain );
77
    mFilter = filter;
78
  }
79
80
  public BufferedImage doFilter(
81
    BufferedImage srcImg, BufferedImage dst, int dstWidth, int dstHeight ) {
82
    this.dstWidth = dstWidth;
83
    this.dstHeight = dstHeight;
84
85
    if( dstWidth < 3 || dstHeight < 3 ) {
86
      throw new IllegalArgumentException( "Target must be at least 3x3." );
87
    }
88
89
    assert multipleInvocationLock.incrementAndGet() == 1 :
90
      "Multiple concurrent invocations detected";
91
92
    final var srcType = srcImg.getType();
93
94
    if( srcType == TYPE_BYTE_BINARY ||
95
      srcType == TYPE_BYTE_INDEXED ||
96
      srcType == TYPE_CUSTOM ) {
97
      srcImg = ImageUtils.convert(
98
        srcImg,
99
        srcImg.getColorModel().hasAlpha() ? TYPE_4BYTE_ABGR : TYPE_3BYTE_BGR );
100
    }
101
102
    this.nrChannels = ImageUtils.nrChannels( srcImg );
103
    assert nrChannels > 0;
104
    this.srcWidth = srcImg.getWidth();
105
    this.srcHeight = srcImg.getHeight();
106
107
    byte[][] workPixels = new byte[ srcHeight ][ dstWidth * nrChannels ];
108
109
    // Pre-calculate  sub-sampling
110
    horizontalSubsamplingData = createSubSampling(
111
      mFilter, srcWidth, dstWidth );
112
    verticalSubsamplingData = createSubSampling(
113
      mFilter, srcHeight, dstHeight );
114
115
    final BufferedImage scrImgCopy = srcImg;
116
    final byte[][] workPixelsCopy = workPixels;
117
    final Thread[] threads = new Thread[ threadCount - 1 ];
118
119
    for( int i = 1; i < threadCount; i++ ) {
120
      final int finalI = i;
121
      threads[ i - 1 ] = new Thread( () -> horizontallyFromSrcToWork(
122
        scrImgCopy, workPixelsCopy, finalI, threadCount ) );
123
      threads[ i - 1 ].start();
124
    }
125
126
    horizontallyFromSrcToWork( scrImgCopy, workPixelsCopy, 0, threadCount );
127
    waitForAllThreads( threads );
128
129
    byte[] outPixels = new byte[ dstWidth * dstHeight * nrChannels ];
130
131
    // --------------------------------------------------
132
    // Apply filter to sample vertically from Work to Dst
133
    // --------------------------------------------------
134
    final byte[] outPixelsCopy = outPixels;
135
    for( int i = 1; i < threadCount; i++ ) {
136
      final int finalI = i;
137
      threads[ i - 1 ] = new Thread( () -> verticalFromWorkToDst(
138
        workPixelsCopy, outPixelsCopy, finalI, threadCount ) );
139
      threads[ i - 1 ].start();
140
    }
141
    verticalFromWorkToDst( workPixelsCopy, outPixelsCopy, 0, threadCount );
142
    waitForAllThreads( threads );
143
144
    // free memory
145
    // noinspection UnusedAssignment
146
    workPixels = null;
147
148
    final BufferedImage out;
149
150
    if( dst != null &&
151
      dstWidth == dst.getWidth() &&
152
      dstHeight == dst.getHeight() ) {
153
      out = dst;
154
      int nrDestChannels = ImageUtils.nrChannels( dst );
155
      if( nrDestChannels != nrChannels ) {
156
        final var errorMgs = format(
157
          "Destination image must be compatible width source image. Source " +
158
            "image had %d channels destination image had %d channels",
159
          nrChannels, nrDestChannels );
160
        throw new RuntimeException( errorMgs );
161
      }
162
    }
163
    else {
164
      out = new BufferedImage(
165
        dstWidth, dstHeight, getResultBufferedImageType( srcImg ) );
166
    }
167
168
    ImageUtils.setBGRPixels( outPixels, out, 0, 0, dstWidth, dstHeight );
169
170
    assert multipleInvocationLock.decrementAndGet() == 0 : "Multiple " +
171
      "concurrent invocations detected";
172
173
    return out;
174
  }
175
176
  private void waitForAllThreads( final Thread[] threads ) {
177
    try {
178
      for( final Thread thread : threads ) {
179
        thread.join( Long.MAX_VALUE );
180
      }
181
    } catch( final InterruptedException e ) {
182
      currentThread().interrupt();
183
      throw new RuntimeException( e );
184
    }
185
  }
186
187
  static SubSamplingData createSubSampling(
188
    ResampleFilter filter, int srcSize, int dstSize ) {
189
    final float scale = (float) dstSize / (float) srcSize;
190
    final int[] arrN = new int[ dstSize ];
191
    final int numContributors;
192
    final float[] arrWeight;
193
    final int[] arrPixel;
194
195
    final float fwidth = filter.getSamplingRadius();
196
197
    float centerOffset = 0.5f / scale;
198
199
    if( scale < 1.0f ) {
200
      final float width = fwidth / scale;
201
      // Add 2 to be safe with the ceiling
202
      numContributors = (int) (width * 2.0f + 2);
203
      arrWeight = new float[ dstSize * numContributors ];
204
      arrPixel = new int[ dstSize * numContributors ];
205
206
      final float fNormFac = (float) (1f / (Math.ceil( width ) / fwidth));
207
208
      for( int i = 0; i < dstSize; i++ ) {
209
        final int subindex = i * numContributors;
210
        float center = i / scale + centerOffset;
211
        int left = (int) Math.floor( center - width );
212
        int right = (int) Math.ceil( center + width );
213
        for( int j = left; j <= right; j++ ) {
214
          float weight;
215
          weight = filter.apply( (center - j) * fNormFac );
216
217
          if( weight == 0.0f ) {
218
            continue;
219
          }
220
          int n;
221
          if( j < 0 ) {
222
            n = -j;
223
          }
224
          else if( j >= srcSize ) {
225
            n = srcSize - j + srcSize - 1;
226
          }
227
          else {
228
            n = j;
229
          }
230
          int k = arrN[ i ];
231
          //assert k == j-left:String.format("%s = %s %s", k,j,left);
232
          arrN[ i ]++;
233
          if( n < 0 || n >= srcSize ) {
234
            weight = 0.0f;// Flag that cell should not be used
235
          }
236
          arrPixel[ subindex + k ] = n;
237
          arrWeight[ subindex + k ] = weight;
238
        }
239
        // normalize the filter's weight's so the sum equals to 1.0, very
240
        // important for avoiding box type of artifacts
241
        final int max = arrN[ i ];
242
        float tot = 0;
243
        for( int k = 0; k < max; k++ ) { tot += arrWeight[ subindex + k ]; }
244
        if( tot != 0f ) { // 0 should never happen except bug in filter
245
          for( int k = 0; k < max; k++ ) { arrWeight[ subindex + k ] /= tot; }
246
        }
247
      }
248
    }
249
    else {
250
      // super-sampling
251
      // Scales from smaller to bigger height
252
      numContributors = (int) (fwidth * 2.0f + 1);
253
      arrWeight = new float[ dstSize * numContributors ];
254
      arrPixel = new int[ dstSize * numContributors ];
255
      //
256
      for( int i = 0; i < dstSize; i++ ) {
257
        final int subindex = i * numContributors;
258
        float center = i / scale + centerOffset;
259
        int left = (int) Math.floor( center - fwidth );
260
        int right = (int) Math.ceil( center + fwidth );
261
        for( int j = left; j <= right; j++ ) {
262
          float weight = filter.apply( center - j );
263
          if( weight == 0.0f ) {
264
            continue;
265
          }
266
          int n;
267
          if( j < 0 ) {
268
            n = -j;
269
          }
270
          else if( j >= srcSize ) {
271
            n = srcSize - j + srcSize - 1;
272
          }
273
          else {
274
            n = j;
275
          }
276
          int k = arrN[ i ];
277
          arrN[ i ]++;
278
          if( n < 0 || n >= srcSize ) {
279
            weight = 0.0f;// Flag that cell should not be used
280
          }
281
          arrPixel[ subindex + k ] = n;
282
          arrWeight[ subindex + k ] = weight;
283
        }
284
        // normalize the filter's weight's so the sum equals to 1.0, very
285
        // important for avoiding box type of artifacts
286
        final int max = arrN[ i ];
287
        float tot = 0;
288
        for( int k = 0; k < max; k++ ) { tot += arrWeight[ subindex + k ]; }
289
        assert tot != 0 : "should never happen except bug in filter";
290
        for( int k = 0; k < max; k++ ) { arrWeight[ subindex + k ] /= tot; }
291
      }
292
    }
293
    return new SubSamplingData( arrN, arrPixel, arrWeight, numContributors );
294
  }
295
296
  private void verticalFromWorkToDst( byte[][] workPixels, byte[] outPixels,
297
                                      int start, int delta ) {
298
    if( nrChannels == 1 ) {
299
      verticalFromWorkToDstGray(
300
        workPixels, outPixels, start, threadCount );
301
      return;
302
    }
303
    boolean useChannel3 = nrChannels > 3;
304
    for( int x = start; x < dstWidth; x += delta ) {
305
      final int xLoc = x * nrChannels;
306
      for( int y = dstHeight - 1; y >= 0; y-- ) {
307
        final int yTimesNumContributors =
308
          y * verticalSubsamplingData.numContributors;
309
        final int max = verticalSubsamplingData.arrN[ y ];
310
        final int sampleLocation = (y * dstWidth + x) * nrChannels;
311
312
        float sample0 = 0.0f;
313
        float sample1 = 0.0f;
314
        float sample2 = 0.0f;
315
        float sample3 = 0.0f;
316
        int index = yTimesNumContributors;
317
        for( int j = max - 1; j >= 0; j-- ) {
318
          int valueLoc = verticalSubsamplingData.arrPixel[ index ];
319
          float arrWeight = verticalSubsamplingData.arrWeight[ index ];
320
          sample0 += (workPixels[ valueLoc ][ xLoc ] & 0xff) * arrWeight;
321
          sample1 += (workPixels[ valueLoc ][ xLoc + 1 ] & 0xff) * arrWeight;
322
          sample2 += (workPixels[ valueLoc ][ xLoc + 2 ] & 0xff) * arrWeight;
323
          if( useChannel3 ) {
324
            sample3 += (workPixels[ valueLoc ][ xLoc + 3 ] & 0xff) * arrWeight;
325
          }
326
327
          index++;
328
        }
329
330
        outPixels[ sampleLocation ] = toByte( sample0 );
331
        outPixels[ sampleLocation + 1 ] = toByte( sample1 );
332
        outPixels[ sampleLocation + 2 ] = toByte( sample2 );
333
334
        if( useChannel3 ) {
335
          outPixels[ sampleLocation + 3 ] = toByte( sample3 );
336
        }
337
      }
338
    }
339
  }
340
341
  private void verticalFromWorkToDstGray(
342
    byte[][] workPixels, byte[] outPixels, int start, int delta ) {
343
    for( int x = start; x < dstWidth; x += delta ) {
344
      for( int y = dstHeight - 1; y >= 0; y-- ) {
345
        final int yTimesNumContributors =
346
          y * verticalSubsamplingData.numContributors;
347
        final int max = verticalSubsamplingData.arrN[ y ];
348
        final int sampleLocation = y * dstWidth + x;
349
        float sample0 = 0.0f;
350
        int index = yTimesNumContributors;
351
352
        for( int j = max - 1; j >= 0; j-- ) {
353
          int valueLocation = verticalSubsamplingData.arrPixel[ index ];
354
          float arrWeight = verticalSubsamplingData.arrWeight[ index ];
355
          sample0 += (workPixels[ valueLocation ][ x ] & 0xff) * arrWeight;
356
357
          index++;
358
        }
359
360
        outPixels[ sampleLocation ] = toByte( sample0 );
361
      }
362
    }
363
  }
364
365
  /**
366
   * Apply filter to sample horizontally from Src to Work
367
   */
368
  private void horizontallyFromSrcToWork(
369
    BufferedImage srcImg, byte[][] workPixels, int start, int delta ) {
370
    if( nrChannels == 1 ) {
371
      horizontallyFromSrcToWorkGray( srcImg, workPixels, start, delta );
372
      return;
373
    }
374
375
    // Used if we work on int based bitmaps, later used to keep channel values
376
    final int[] tempPixels = new int[ srcWidth ];
377
    // create reusable row to minimize memory overhead
378
    final byte[] srcPixels = new byte[ srcWidth * nrChannels ];
379
    final boolean useChannel3 = nrChannels > 3;
380
381
    for( int k = start; k < srcHeight; k = k + delta ) {
382
      ImageUtils.getPixelsBGR( srcImg, k, srcWidth, srcPixels, tempPixels );
383
384
      for( int i = dstWidth - 1; i >= 0; i-- ) {
385
        int sampleLocation = i * nrChannels;
386
        final int max = horizontalSubsamplingData.arrN[ i ];
387
388
        float sample0 = 0.0f;
389
        float sample1 = 0.0f;
390
        float sample2 = 0.0f;
391
        float sample3 = 0.0f;
392
        int index = i * horizontalSubsamplingData.numContributors;
393
        for( int j = max - 1; j >= 0; j-- ) {
394
          float arrWeight = horizontalSubsamplingData.arrWeight[ index ];
395
          int pixelIndex =
396
            horizontalSubsamplingData.arrPixel[ index ] * nrChannels;
397
398
          sample0 += (srcPixels[ pixelIndex ] & 0xff) * arrWeight;
399
          sample1 += (srcPixels[ pixelIndex + 1 ] & 0xff) * arrWeight;
400
          sample2 += (srcPixels[ pixelIndex + 2 ] & 0xff) * arrWeight;
401
          if( useChannel3 ) {
402
            sample3 += (srcPixels[ pixelIndex + 3 ] & 0xff) * arrWeight;
403
          }
404
          index++;
405
        }
406
407
        workPixels[ k ][ sampleLocation ] = toByte( sample0 );
408
        workPixels[ k ][ sampleLocation + 1 ] = toByte( sample1 );
409
        workPixels[ k ][ sampleLocation + 2 ] = toByte( sample2 );
410
        if( useChannel3 ) {
411
          workPixels[ k ][ sampleLocation + 3 ] = toByte( sample3 );
412
        }
413
      }
414
    }
415
  }
416
417
  /**
418
   * Apply filter to sample horizontally from Src to Work
419
   */
420
  private void horizontallyFromSrcToWorkGray(
421
    BufferedImage srcImg, byte[][] workPixels, int start, int delta ) {
422
    // Used if we work on int based bitmaps, later used to keep channel values
423
    final int[] tempPixels = new int[ srcWidth ];
424
    // create reusable row to minimize memory overhead
425
    final byte[] srcPixels = new byte[ srcWidth ];
426
427
    for( int k = start; k < srcHeight; k = k + delta ) {
428
      ImageUtils.getPixelsBGR( srcImg, k, srcWidth, srcPixels, tempPixels );
429
430
      for( int i = dstWidth - 1; i >= 0; i-- ) {
431
        final int max = horizontalSubsamplingData.arrN[ i ];
432
433
        float sample0 = 0.0f;
434
        int index = i * horizontalSubsamplingData.numContributors;
435
        for( int j = max - 1; j >= 0; j-- ) {
436
          float arrWeight = horizontalSubsamplingData.arrWeight[ index ];
437
          int pixelIndex = horizontalSubsamplingData.arrPixel[ index ];
438
439
          sample0 += (srcPixels[ pixelIndex ] & 0xff) * arrWeight;
440
          index++;
441
        }
442
443
        workPixels[ k ][ i ] = toByte( sample0 );
444
      }
445
    }
446
  }
447
448
  private static byte toByte( final float f ) {
449
    if( f < 0 ) {
450
      return 0;
451
    }
452
453
    return (byte) (f > MAX_CHANNEL_VALUE ? MAX_CHANNEL_VALUE : f + 0.5f);
454
  }
455
456
  protected int getResultBufferedImageType( BufferedImage srcImg ) {
457
    return nrChannels == 3
458
      ? TYPE_3BYTE_BGR
459
      : nrChannels == 4
460
      ? TYPE_4BYTE_ABGR
461
      : srcImg.getSampleModel().getDataType() == TYPE_USHORT
462
      ? TYPE_USHORT_GRAY
463
      : TYPE_BYTE_GRAY;
464
  }
465
}
1466
A src/main/java/com/keenwrite/processors/ExecutorProcessor.java
1
/* Copyright 2020-2021 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/Processor.java
1
/* Copyright 2020-2021 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 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors;
6
7
import com.keenwrite.ExportFormat;
8
import com.keenwrite.collections.InterpolatingMap;
9
import com.keenwrite.constants.Constants;
10
import com.keenwrite.editors.common.Caret;
11
import com.keenwrite.io.FileType;
12
import com.keenwrite.io.MediaType;
13
import com.keenwrite.io.MediaTypeExtension;
14
import com.keenwrite.processors.variable.VariableProcessor;
15
import com.keenwrite.sigils.PropertyKeyOperator;
16
import com.keenwrite.sigils.SigilKeyOperator;
17
import com.keenwrite.util.GenericBuilder;
18
import org.renjin.repackaged.guava.base.Splitter;
19
20
import java.io.File;
21
import java.nio.file.Path;
22
import java.util.HashMap;
23
import java.util.Locale;
24
import java.util.Map;
25
import java.util.concurrent.Callable;
26
import java.util.function.Supplier;
27
28
import static com.keenwrite.Bootstrap.USER_CACHE_DIR;
29
import static com.keenwrite.Bootstrap.USER_DATA_DIR;
30
import static com.keenwrite.constants.Constants.*;
31
import static com.keenwrite.io.FileType.UNKNOWN;
32
import static com.keenwrite.io.MediaType.TEXT_PROPERTIES;
33
import static com.keenwrite.io.SysFile.toFile;
34
import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate;
35
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
36
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
37
import static com.keenwrite.util.Strings.sanitize;
38
39
/**
40
 * Provides a context for configuring a chain of {@link Processor} instances.
41
 */
42
public final class ProcessorContext {
43
44
  private final Mutator mMutator;
45
46
  /**
47
   * Determines the file type from the path extension. This should only be
48
   * called when it is known that the file type won't be a definition file
49
   * (e.g., YAML or other definition source), but rather an editable file
50
   * (e.g., Markdown, R Markdown, etc.).
51
   *
52
   * @param path The path with a file name extension.
53
   * @return The FileType for the given path.
54
   */
55
  private static FileType lookup( final Path path ) {
56
    assert path != null;
57
58
    final var prefix = GLOB_PREFIX_FILE;
59
    final var keys = sSettings.getKeys( prefix );
60
61
    var found = false;
62
    var fileType = UNKNOWN;
63
64
    while( keys.hasNext() && !found ) {
65
      final var key = keys.next();
66
      final var patterns = sSettings.getStringSettingList( key );
67
      final var predicate = createFileTypePredicate( patterns );
68
69
      if( predicate.test( toFile( path ) ) ) {
70
        // Remove the EXTENSIONS_PREFIX to get the file name extension mapped
71
        // to a standard name (as defined in the settings.properties file).
72
        final String suffix = key.replace( prefix + '.', "" );
73
        fileType = FileType.from( suffix );
74
        found = true;
75
      }
76
    }
77
78
    return fileType;
79
  }
80
81
  public boolean isExportFormat( final ExportFormat exportFormat ) {
82
    return mMutator.mExportFormat == exportFormat;
83
  }
84
85
  /**
86
   * Responsible for populating the instance variables required by the
87
   * context.
88
   */
89
  public static class Mutator {
90
    private Path mSourcePath;
91
    private Path mTargetPath;
92
    private String mHtmlHead = "";
93
    private String mHtmlFoot = "";
94
    private ExportFormat mExportFormat;
95
    private Supplier<Boolean> mConcatenate = () -> true;
96
    private Supplier<String> mChapters = () -> "";
97
98
    private Supplier<Path> mThemeDir = USER_DIRECTORY::toPath;
99
    private Supplier<Locale> mLocale = () -> Locale.ENGLISH;
100
101
    private Supplier<Map<String, String>> mDefinitions = HashMap::new;
102
    private Supplier<Map<String, String>> mMetadata = HashMap::new;
103
    private Supplier<Caret> mCaret = () -> Caret.builder().build();
104
105
    private Supplier<Path> mImageDir = USER_DIRECTORY::toPath;
106
    private Supplier<String> mImageServer = () -> DIAGRAM_SERVER_NAME;
107
    private Supplier<String> mImageOrder = () -> PERSIST_IMAGES_DEFAULT;
108
    private Supplier<Path> mCacheDir = USER_CACHE_DIR::toPath;
109
    private Supplier<Path> mFontDir = () -> getFontDirectory().toPath();
110
111
    private Supplier<String> mModesEnabled = () -> "";
112
113
    private Supplier<String> mSigilBegan = () -> DEF_DELIM_BEGAN_DEFAULT;
114
    private Supplier<String> mSigilEnded = () -> DEF_DELIM_ENDED_DEFAULT;
115
116
    private Supplier<Path> mRWorkingDir = USER_DIRECTORY::toPath;
117
    private Supplier<String> mRScript = () -> "";
118
119
    private Supplier<String> mCurlQuotes = () -> APOS_DEFAULT;
120
    private Supplier<Boolean> mAutoRemove = () -> true;
121
122
    public void setSourcePath( final Path path ) {
123
      assert path != null;
124
      mSourcePath = path;
125
    }
126
127
    public void setTargetPath( final Path path ) {
128
      assert path != null;
129
      mTargetPath = path;
130
    }
131
132
    public void setHtmlHead( final String text ) {
133
      assert text != null;
134
      mHtmlHead = text;
135
    }
136
137
    public void setHtmlFoot( final String text ) {
138
      assert text != null;
139
      mHtmlFoot = text;
140
    }
141
142
    public void setThemeDir( final Supplier<Path> themeDir ) {
143
      assert themeDir != null;
144
      mThemeDir = themeDir;
145
    }
146
147
    public void setCacheDir( final Supplier<File> cacheDir ) {
148
      assert cacheDir != null;
149
150
      mCacheDir = () -> {
151
        final var dir = cacheDir.get();
152
153
        return (dir == null ? toFile( USER_DATA_DIR ) : dir).toPath();
154
      };
155
    }
156
157
    public void setImageDir( final Supplier<File> imageDir ) {
158
      assert imageDir != null;
159
160
      mImageDir = () -> {
161
        final var dir = imageDir.get();
162
163
        return (dir == null ? USER_DIRECTORY : dir).toPath();
164
      };
165
    }
166
167
    public void setImageOrder( final Supplier<String> imageOrder ) {
168
      assert imageOrder != null;
169
      mImageOrder = imageOrder;
170
    }
171
172
    public void setImageServer( final Supplier<String> imageServer ) {
173
      assert imageServer != null;
174
      mImageServer = imageServer;
175
    }
176
177
    public void setFontDir( final Supplier<File> fontDir ) {
178
      assert fontDir != null;
179
180
      mFontDir = () -> {
181
        final var dir = fontDir.get();
182
183
        return (dir == null ? USER_DIRECTORY : dir).toPath();
184
      };
185
    }
186
187
    public void setModesEnabled( final Supplier<String> modesEnabled ) {
188
      assert modesEnabled != null;
189
      mModesEnabled = modesEnabled;
190
    }
191
192
    public void setExportFormat( final ExportFormat exportFormat ) {
193
      assert exportFormat != null;
194
      mExportFormat = exportFormat;
195
    }
196
197
    public void setConcatenate( final Supplier<Boolean> concatenate ) {
198
      mConcatenate = concatenate;
199
    }
200
201
    public void setChapters( final Supplier<String> chapters ) {
202
      mChapters = chapters;
203
    }
204
205
    public void setLocale( final Supplier<Locale> locale ) {
206
      assert locale != null;
207
      mLocale = locale;
208
    }
209
210
    /**
211
     * Sets the list of fully interpolated key-value pairs to use when
212
     * substituting variable names back into the document as variable values.
213
     * This uses a {@link Callable} reference so that GUI and command-line
214
     * usage can insert their respective behaviours. That is, this method
215
     * prevents coupling the GUI to the CLI.
216
     *
217
     * @param supplier Defines how to retrieve the definitions.
218
     */
219
    public void setDefinitions( final Supplier<Map<String, String>> supplier ) {
220
      assert supplier != null;
221
      mDefinitions = supplier;
222
    }
223
224
    /**
225
     * Sets metadata to use in the document header. These are made available
226
     * to the typesetting engine as {@code \documentvariable} values.
227
     *
228
     * @param metadata The key/value pairs to publish as document metadata.
229
     */
230
    public void setMetadata( final Supplier<Map<String, String>> metadata ) {
231
      assert metadata != null;
232
      mMetadata = metadata.get() == null ? HashMap::new : metadata;
233
    }
234
235
    /**
236
     * Sets document variables to use when building the document. These
237
     * variables will override existing key/value pairs, or be added as
238
     * new key/value pairs if not already defined. This allows users to
239
     * inject variables into the document from the command-line, allowing
240
     * for dynamic assignment of in-text values when building documents.
241
     *
242
     * @param overrides The key/value pairs to add (or override) as variables.
243
     */
244
    public void setOverrides( final Supplier<Map<String, String>> overrides ) {
245
      assert overrides != null;
246
      assert mDefinitions != null;
247
      assert mDefinitions.get() != null;
248
249
      final var map = overrides.get();
250
251
      if( map != null ) {
252
        mDefinitions.get().putAll( map );
253
      }
254
    }
255
256
    /**
257
     * Sets the source for deriving the {@link Caret}. Typically, this is
258
     * the text editor that has focus.
259
     *
260
     * @param caret The source for the currently active caret.
261
     */
262
    public void setCaret( final Supplier<Caret> caret ) {
263
      assert caret != null;
264
      mCaret = caret;
265
    }
266
267
    public void setSigilBegan( final Supplier<String> sigilBegan ) {
268
      assert sigilBegan != null;
269
      mSigilBegan = sigilBegan;
270
    }
271
272
    public void setSigilEnded( final Supplier<String> sigilEnded ) {
273
      assert sigilEnded != null;
274
      mSigilEnded = sigilEnded;
275
    }
276
277
    public void setRWorkingDir( final Supplier<Path> rWorkingDir ) {
278
      assert rWorkingDir != null;
279
      mRWorkingDir = rWorkingDir;
280
    }
281
282
    public void setRScript( final Supplier<String> rScript ) {
283
      assert rScript != null;
284
      mRScript = rScript;
285
    }
286
287
    public void setCurlQuotes( final Supplier<String> encoding ) {
288
      assert encoding != null;
289
      mCurlQuotes = encoding;
290
    }
291
292
    public void setAutoRemove( final Supplier<Boolean> autoRemove ) {
293
      assert autoRemove != null;
294
      mAutoRemove = autoRemove;
295
    }
296
297
    private boolean isExportFormat( final ExportFormat format ) {
298
      return mExportFormat == format;
299
    }
300
  }
301
302
  public static GenericBuilder<Mutator, ProcessorContext> builder() {
303
    return GenericBuilder.of( Mutator::new, ProcessorContext::new );
304
  }
305
306
  /**
307
   * Creates a new context for use by the {@link ProcessorFactory} when
308
   * instantiating new {@link Processor} instances. Although all the
309
   * parameters are required, not all {@link Processor} instances will use
310
   * all parameters.
311
   */
312
  private ProcessorContext( final Mutator mutator ) {
313
    assert mutator != null;
314
315
    mMutator = mutator;
316
  }
317
318
  public Path getSourcePath() {
319
    return mMutator.mSourcePath;
320
  }
321
322
  /**
323
   * Answers what type of input document is to be processed.
324
   *
325
   * @return The input document's {@link MediaType}.
326
   */
327
  public MediaType getSourceType() {
328
    return MediaTypeExtension.fromPath( mMutator.mSourcePath );
329
  }
330
331
  /**
332
   * Fully qualified file name to use when exporting (e.g., document.pdf).
333
   *
334
   * @return Full path to a file name.
335
   */
336
  public Path getTargetPath() {
337
    return mMutator.mTargetPath;
338
  }
339
340
  public String getHtmlHead() {
341
    assert mMutator.mHtmlHead != null;
342
343
    return mMutator.mHtmlHead;
344
  }
345
346
  public String getHtmlFoot() {
347
    assert mMutator.mHtmlFoot != null;
348
349
    return mMutator.mHtmlFoot;
350
  }
351
352
  public ExportFormat getExportFormat() {
353
    return mMutator.mExportFormat;
354
  }
355
356
  public Locale getLocale() {
357
    return mMutator.mLocale.get();
358
  }
359
360
  /**
361
   * Returns the variable map of definitions, without interpolation.
362
   *
363
   * @return A map to help dereference variables.
364
   */
365
  public Map<String, String> getDefinitions() {
366
    return mMutator.mDefinitions.get();
367
  }
368
369
  /**
370
   * Returns the variable map of definitions, with interpolation.
371
   *
372
   * @return A map to help dereference variables.
373
   */
374
  public InterpolatingMap getInterpolatedDefinitions() {
375
    return new InterpolatingMap(
376
      createDefinitionKeyOperator(), getDefinitions()
377
    ).interpolate();
378
  }
379
380
  public Map<String, String> getMetadata() {
381
    return mMutator.mMetadata.get();
382
  }
383
384
  /**
385
   * Returns the current caret position in the document being edited and is
386
   * always up-to-date.
387
   *
388
   * @return Caret position in the document.
389
   */
390
  public Supplier<Caret> getCaret() {
391
    return mMutator.mCaret;
392
  }
393
394
  /**
395
   * Returns the directory that contains the file being edited. When
396
   * {@link Constants#DOCUMENT_DEFAULT} is created, the parent path is
397
   * {@code null}. This will get absolute path to the file before trying to
398
   * get te parent path, which should always be a valid path. In the unlikely
399
   * event that the base path cannot be determined by the path alone, the
400
   * default user directory is returned. This is necessary for the creation
401
   * of new files.
402
   *
403
   * @return Path to the directory containing a file being edited, or the
404
   * default user directory if the base path cannot be determined.
405
   */
406
  public Path getBaseDir() {
407
    final var path = getSourcePath().toAbsolutePath().getParent();
408
    return path == null ? DEFAULT_DIRECTORY : path;
409
  }
410
411
  FileType getSourceFileType() {
412
    return lookup( getSourcePath() );
413
  }
414
415
  public Path getThemeDir() {
416
    return mMutator.mThemeDir.get();
417
  }
418
419
  public Path getImageDir() {
420
    return mMutator.mImageDir.get();
421
  }
422
423
  public Path getCacheDir() {
424
    return mMutator.mCacheDir.get();
425
  }
426
427
  public Iterable<String> getImageOrder() {
428
    assert mMutator.mImageOrder != null;
429
430
    final var order = mMutator.mImageOrder.get();
431
    final var token = order.contains( "," ) ? ',' : ' ';
432
433
    return Splitter.on( token ).split( token + order );
434
  }
435
436
  public String getImageServer() {
437
    return mMutator.mImageServer.get();
438
  }
439
440
  public Path getFontDir() {
441
    return mMutator.mFontDir.get();
442
  }
443
444
  public String getModesEnabled() {
445
    // Force the processor to select particular sigils.
446
    final var processor = new VariableProcessor( IDENTITY, this );
447
    final var needles = processor.getDefinitions();
448
    final var haystack = sanitize( mMutator.mModesEnabled.get() );
449
450
    return needles.containsKey( haystack )
451
      ? replace( haystack, needles )
452
      : processor.hasSigils( haystack )
453
      ? ""
454
      : haystack;
455
  }
456
457
  public boolean getAutoRemove() {
458
    return mMutator.mAutoRemove.get();
459
  }
460
461
  public Path getRWorkingDir() {
462
    return mMutator.mRWorkingDir.get();
463
  }
464
465
  public String getRScript() {
466
    return mMutator.mRScript.get();
467
  }
468
469
  public String getCurlQuotes() {
470
    final var result = mMutator.mCurlQuotes.get();
471
472
    return "true".equalsIgnoreCase( result ) ? APOS_DEFAULT : result;
473
  }
474
475
  /**
476
   * Answers whether to process a single text file or all text files in
477
   * the same directory as a single text file. See {@link #getSourcePath()}
478
   * for the file to process (or all files in its directory).
479
   *
480
   * @return {@code true} means to process all text files, {@code false}
481
   * means to process a single file.
482
   */
483
  public boolean getConcatenate() {
484
    return mMutator.mConcatenate.get();
485
  }
486
487
  public String getChapters() {
488
    return mMutator.mChapters.get();
489
  }
490
491
  public SigilKeyOperator createKeyOperator() {
492
    return createKeyOperator( getSourcePath() );
493
  }
494
495
  /**
496
   * Returns the sigil operator for the given {@link Path}.
497
   *
498
   * @param path The type of file being edited, from its extension.
499
   */
500
  private SigilKeyOperator createKeyOperator( final Path path ) {
501
    assert path != null;
502
503
    return MediaType.fromFilename( path ) == TEXT_PROPERTIES
504
      ? createPropertyKeyOperator()
505
      : createDefinitionKeyOperator();
506
  }
507
508
  private SigilKeyOperator createPropertyKeyOperator() {
509
    return new PropertyKeyOperator();
510
  }
511
512
  private SigilKeyOperator createDefinitionKeyOperator() {
513
    final var began = mMutator.mSigilBegan.get();
514
    final var ended = mMutator.mSigilEnded.get();
515
516
    return new SigilKeyOperator( began, ended );
517
  }
518
}
1519
A src/main/java/com/keenwrite/processors/ProcessorFactory.java
1
/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors;
6
7
import com.keenwrite.processors.html.HtmlProcessor;
8
import com.keenwrite.processors.html.PreformattedProcessor;
9
import com.keenwrite.processors.html.XhtmlProcessor;
10
import com.keenwrite.processors.markdown.MarkdownProcessor;
11
import com.keenwrite.processors.pdf.PdfProcessor;
12
import com.keenwrite.processors.text.TextProcessor;
13
import com.keenwrite.processors.variable.VariableProcessor;
14
15
import static com.keenwrite.ExportFormat.TEXT_TEX;
16
import static com.keenwrite.io.FileType.RMARKDOWN;
17
import static com.keenwrite.io.FileType.SOURCE;
18
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
19
20
/**
21
 * Responsible for creating processors capable of parsing, transforming,
22
 * interpolating, and rendering known file types.
23
 */
24
public final class ProcessorFactory {
25
26
  private ProcessorFactory() {
27
  }
28
29
  public static Processor<String> createProcessors(
30
    final ProcessorContext context ) {
31
    return createProcessors( context, null );
32
  }
33
34
  /**
35
   * Creates a new {@link Processor} chain suitable for parsing and rendering
36
   * the file opened at the given tab.
37
   *
38
   * @param context The tab containing a text editor, path, and caret position.
39
   * @return A processor that can render the given tab's text.
40
   */
41
  public static Processor<String> createProcessors(
42
    final ProcessorContext context, final Processor<String> preview ) {
43
    return createProcessor( context, preview );
44
  }
45
46
  /**
47
   * Constructs processors that chain various processing operations on a
48
   * document to generate a transformed version of the source document.
49
   *
50
   * @param context Parameters needed to construct various processors.
51
   * @param preview The processor to use when no export format is specified.
52
   */
53
  private static Processor<String> createProcessor(
54
    final ProcessorContext context, final Processor<String> preview ) {
55
    // If the content is not to be exported, then the successor processor
56
    // is one that parses Markdown into HTML and passes the string to the
57
    // HTML preview pane.
58
    //
59
    // Otherwise, bolt on a processor that---after the interpolation and
60
    // substitution phase, which includes text strings or R code---will
61
    // generate HTML or plain Markdown. HTML has a few output formats:
62
    // with embedded SVG representing formulas, or without any conversion
63
    // to SVG. Without conversion would require client-side rendering of
64
    // math (such as using the JavaScript-based KaTeX engine).
65
    final var outputType = context.getExportFormat();
66
67
    final var successor = switch( outputType ) {
68
      case NONE -> preview;
69
      case HTML_TEX_DELIMITED, HTML_TEX_SVG -> createHtmlProcessor( context );
70
      case XHTML_TEX, XHTML_TEX_SVG -> createXhtmlProcessor( context );
71
      case TEXT_TEX -> createTextProcessor( context );
72
      case APPLICATION_PDF -> createPdfProcessor( context );
73
      default -> createIdentityProcessor( context );
74
    };
75
    final var inputType = context.getSourceFileType();
76
    final Processor<String> processor;
77
78
    if( preview == null ) {
79
      if( outputType == TEXT_TEX ) {
80
        processor = successor;
81
      }
82
      else {
83
        processor = createMarkdownProcessor( successor, context );
84
      }
85
    }
86
    else {
87
      processor = inputType == SOURCE || inputType == RMARKDOWN
88
        ? createMarkdownProcessor( successor, context )
89
        : createPreformattedProcessor( successor );
90
    }
91
92
    return new ExecutorProcessor<>( processor );
93
  }
94
95
  /**
96
   * Instantiates a new {@link Processor} that has no successor and returns
97
   * the string it was given without modification.
98
   *
99
   * @return An instance of {@link Processor} that performs no processing.
100
   */
101
  @SuppressWarnings( "unused" )
102
  private static Processor<String> createIdentityProcessor(
103
    final ProcessorContext ignored ) {
104
    return IDENTITY;
105
  }
106
107
  /**
108
   * Instantiates a {@link Processor} responsible for parsing Markdown and
109
   * definitions.
110
   *
111
   * @return A chain of {@link Processor}s for processing Markdown and
112
   * definitions.
113
   */
114
  private static Processor<String> createMarkdownProcessor(
115
    final Processor<String> successor,
116
    final ProcessorContext context ) {
117
    final var dp = createVariableProcessor( successor, context );
118
    return MarkdownProcessor.create( dp, context );
119
  }
120
121
  private static Processor<String> createVariableProcessor(
122
    final Processor<String> successor,
123
    final ProcessorContext context ) {
124
    return new VariableProcessor( successor, context );
125
  }
126
127
  /**
128
   * Instantiates a new {@link Processor} that wraps an HTML document into
129
   * its final, well-formed state (including head and body tags). This is
130
   * useful for generating XHTML documents suitable for typesetting (using
131
   * an engine such as LuaTeX).
132
   *
133
   * @return An instance of {@link Processor} that completes an HTML document.
134
   */
135
  private static Processor<String> createXhtmlProcessor(
136
    final ProcessorContext context ) {
137
    return createXhtmlProcessor( IDENTITY, context );
138
  }
139
140
  private static Processor<String> createXhtmlProcessor(
141
    final Processor<String> successor, final ProcessorContext context ) {
142
    return new XhtmlProcessor( successor, context );
143
  }
144
145
  private static Processor<String> createPdfProcessor(
146
    final ProcessorContext context ) {
147
    final var pdfProcessor = new PdfProcessor( context );
148
    return createXhtmlProcessor( pdfProcessor, context );
149
  }
150
151
  private static Processor<String> createHtmlProcessor(
152
    final ProcessorContext context ) {
153
    return new HtmlProcessor( IDENTITY, context );
154
  }
155
156
  private static Processor<String> createTextProcessor(
157
    final ProcessorContext context ) {
158
    return new TextProcessor( IDENTITY, context );
159
  }
160
161
  private static Processor<String> createPreformattedProcessor(
162
    final Processor<String> successor ) {
163
    return new PreformattedProcessor( successor );
164
  }
165
}
1166
A src/main/java/com/keenwrite/processors/html/Configuration.java
1
package com.keenwrite.processors.html;
2
3
import com.whitemagicsoftware.keenquotes.lex.FilterType;
4
import com.whitemagicsoftware.keenquotes.parser.Apostrophe;
5
import com.whitemagicsoftware.keenquotes.parser.Contractions;
6
import com.whitemagicsoftware.keenquotes.parser.Curler;
7
8
import static com.whitemagicsoftware.keenquotes.parser.Contractions.*;
9
10
/**
11
 * Ensures the contractions aren't created multiple times when creating
12
 * a class that changes typographic straight quotes into curly quotes.
13
 */
14
public final class Configuration {
15
  /**
16
   * Creates contracts with a custom set of unambiguous English contractions.
17
   */
18
  private final static Contractions CONTRACTIONS = new Builder().build();
19
20
  public static Curler createCurler(
21
    final FilterType filterType,
22
    final Apostrophe apostrophe ) {
23
    return new Curler( CONTRACTIONS, filterType, apostrophe );
24
  }
25
}
126
A src/main/java/com/keenwrite/processors/html/HtmlPreviewProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.html;
3
4
import com.keenwrite.preview.HtmlPreview;
5
import com.keenwrite.processors.ExecutorProcessor;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.whitemagicsoftware.keenquotes.parser.Apostrophe;
8
import com.whitemagicsoftware.keenquotes.parser.Curler;
9
10
import static com.keenwrite.processors.html.Configuration.createCurler;
11
import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML;
12
13
/**
14
 * Responsible for notifying the {@link HtmlPreview} when the succession
15
 * chain has updated. This decouples knowledge of changes to the editor panel
16
 * from the HTML preview panel as well as any processing that takes place
17
 * before the final HTML preview is rendered. This is the last link in the
18
 * processor chain.
19
 */
20
public final class HtmlPreviewProcessor extends ExecutorProcessor<String> {
21
  /**
22
   * Force the straight quotes to be curled to \&rsquo; in the preview.
23
   */
24
  private final static Curler CURLER = createCurler(
25
    FILTER_XML, Apostrophe.CONVERT_RSQUOTE
26
  );
27
28
  /**
29
   * Allows quote curling in the preview panel.
30
   */
31
  private final ProcessorContext mContext;
32
33
  /**
34
   * There is only one preview panel.
35
   */
36
  private static HtmlPreview sPreview;
37
38
  /**
39
   * Constructs the end of a processing chain.
40
   *
41
   * @param context Typesetting options.
42
   * @param preview The pane to update with the post-processed document.
43
   */
44
  public HtmlPreviewProcessor(
45
    final ProcessorContext context,
46
    final HtmlPreview preview ) {
47
    mContext = context;
48
    sPreview = preview;
49
  }
50
51
  /**
52
   * Update the preview panel using HTML from the succession chain.
53
   *
54
   * @param html The document content to render in the preview pane. The HTML
55
   *             should not contain a doctype, head, or body tag.
56
   * @return The given {@code html} string.
57
   */
58
  @Override
59
  public String apply( final String html ) {
60
    assert html != null;
61
62
    final var apos = mContext.getCurlQuotes();
63
    final var document = apos.isBlank() ? html : CURLER.apply( html );
64
65
    sPreview.render( document );
66
    return html;
67
  }
68
}
169
A src/main/java/com/keenwrite/processors/html/HtmlProcessor.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.html;
6
7
import com.keenwrite.processors.ExecutorProcessor;
8
import com.keenwrite.processors.Processor;
9
import com.keenwrite.processors.ProcessorContext;
10
import com.whitemagicsoftware.keenquotes.parser.Apostrophe;
11
12
import static com.keenwrite.processors.html.Configuration.createCurler;
13
import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML;
14
15
/**
16
 * This is the processor used when an HTML file name extension is encountered.
17
 */
18
public final class HtmlProcessor extends ExecutorProcessor<String> {
19
  private final ProcessorContext mContext;
20
21
  /**
22
   * Constructs an HTML processor capable of curling straight quotes.
23
   *
24
   * @param successor The next chained processor for text processing.
25
   */
26
  public HtmlProcessor(
27
    final Processor<String> successor,
28
    final ProcessorContext context ) {
29
    super( successor );
30
31
    mContext = context;
32
  }
33
34
  /**
35
   * Returns the given string with quotations marks encoded as HTML entities,
36
   * provided the user opted to curl quotation marks.
37
   *
38
   * @param t The string having quotation marks to replace.
39
   * @return The string with quotation marks curled.
40
   */
41
  @Override
42
  public String apply( final String t ) {
43
    final var curl = mContext.getCurlQuotes();
44
    final var curler = createCurler(
45
      FILTER_XML, Apostrophe.fromType( curl )
46
    );
47
48
    return curler.apply( t );
49
  }
50
}
151
A src/main/java/com/keenwrite/processors/html/IdentityProcessor.java
1
/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.html;
3
4
import com.keenwrite.processors.ExecutorProcessor;
5
6
/**
7
 * Responsible for transforming a string into itself. This is used at the
8
 * end of a processing chain when no more processing is required.
9
 */
10
public final class IdentityProcessor extends ExecutorProcessor<String> {
11
  public static final IdentityProcessor IDENTITY = new IdentityProcessor();
12
13
  /**
14
   * Constructs a new instance having no successor (the default successor is
15
   * {@code null}).
16
   */
17
  private IdentityProcessor() {}
18
19
  /**
20
   * Returns the given string without modification.
21
   *
22
   * @param s The string to return.
23
   * @return The value of s.
24
   */
25
  @Override
26
  public String apply( final String s ) {
27
    return s;
28
  }
29
}
130
A src/main/java/com/keenwrite/processors/html/PreformattedProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.html;
6
7
import com.keenwrite.processors.ExecutorProcessor;
8
import com.keenwrite.processors.Processor;
9
10
/**
11
 * This is the default processor used when an unknown file name extension is
12
 * encountered. It processes the text by enclosing it in an HTML {@code <pre>}
13
 * element.
14
 */
15
public final class PreformattedProcessor extends ExecutorProcessor<String> {
16
17
  /**
18
   * Passes the link to the super constructor.
19
   *
20
   * @param successor The next processor in the chain to use for text
21
   *                  processing.
22
   */
23
  public PreformattedProcessor( final Processor<String> successor ) {
24
    super( successor );
25
  }
26
27
  /**
28
   * Returns the given string, modified with "pre" tags.
29
   *
30
   * @param t The string to return, enclosed in "pre" tags.
31
   * @return The value of t wrapped in "pre" tags.
32
   */
33
  @Override
34
  public String apply( final String t ) {
35
    return String.format( "<pre>%s</pre>", t );
36
  }
37
}
138
A src/main/java/com/keenwrite/processors/html/XhtmlProcessor.java
1
/* Copyright 2023-2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.html;
6
7
import com.keenwrite.collections.InterpolatingMap;
8
import com.keenwrite.dom.DocumentConverter;
9
import com.keenwrite.dom.DocumentParser;
10
import com.keenwrite.io.MediaTypeExtension;
11
import com.keenwrite.processors.ExecutorProcessor;
12
import com.keenwrite.processors.Processor;
13
import com.keenwrite.processors.ProcessorContext;
14
import com.keenwrite.ui.heuristics.WordCounter;
15
import com.keenwrite.util.DataTypeConverter;
16
import com.whitemagicsoftware.keenquotes.parser.Apostrophe;
17
import org.w3c.dom.Document;
18
import org.w3c.dom.Node;
19
import org.w3c.dom.NodeList;
20
21
import java.io.File;
22
import java.io.FileNotFoundException;
23
import java.nio.file.Path;
24
import java.util.*;
25
import java.util.function.Consumer;
26
27
import static com.keenwrite.Bootstrap.APP_TITLE_ABBR;
28
import static com.keenwrite.dom.DocumentParser.*;
29
import static com.keenwrite.events.StatusEvent.clue;
30
import static com.keenwrite.io.SysFile.toFile;
31
import static com.keenwrite.io.downloads.DownloadManager.open;
32
import static com.keenwrite.processors.html.Configuration.createCurler;
33
import static com.keenwrite.util.ProtocolScheme.getProtocol;
34
import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML;
35
import static java.lang.String.format;
36
import static java.lang.String.valueOf;
37
import static java.nio.file.Files.copy;
38
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
39
40
/**
41
 * Responsible for making an XHTML document complete by wrapping it with html
42
 * and body elements. This doesn't have to be super-efficient because it's
43
 * not run in real time.
44
 */
45
public final class XhtmlProcessor extends ExecutorProcessor<String> {
46
  private static final String DTD =
47
    "<!DOCTYPE html PUBLIC " +
48
      "\"-//W3C//DTD XHTML 1.0 Transitional//EN\" " +
49
      "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
50
51
  private static final DocumentConverter CONVERTER = new DocumentConverter();
52
53
  private final ProcessorContext mContext;
54
55
  public XhtmlProcessor(
56
    final Processor<String> successor, final ProcessorContext context ) {
57
    super( successor );
58
59
    assert context != null;
60
    mContext = context;
61
  }
62
63
  /**
64
   * Responsible for producing a well-formed XML document complete with
65
   * metadata (title, author, keywords, copyright, and date).
66
   *
67
   * @param html The HTML document to transform into an XHTML document.
68
   * @return The transformed HTML document.
69
   */
70
  @Override
71
  public String apply( final String html ) {
72
    clue( "Main.status.typeset.xhtml" );
73
74
    try {
75
      final var doc = parse( html );
76
77
      visit(
78
        doc, "//img", node -> {
79
          try {
80
            final var attrs = node.getAttributes();
81
            final var attr = attrs.getNamedItem( "src" );
82
83
            if( attr != null ) {
84
              final var src = attr.getTextContent();
85
              final Path location;
86
              final Path imagesDir;
87
88
              // Download into a cache directory, which can be written to
89
              // without any possibility of overwriting local image files.
90
              // Further, the filenames are hashed as a second layer of
91
              // protection.
92
              if( getProtocol( src ).isRemote() ) {
93
                location = downloadImage( src );
94
                imagesDir = getCachesPath();
95
              }
96
              else {
97
                location = resolveImage( src );
98
                imagesDir = getImagesPath();
99
              }
100
101
              final var relative = imagesDir.relativize( location );
102
103
              attr.setTextContent( relative.toString() );
104
            }
105
          }
106
          catch( final Exception ex ) {
107
            clue( ex );
108
          }
109
        }
110
      );
111
112
      final var headText = mContext.getHtmlHead();
113
      final var footText = mContext.getHtmlFoot();
114
115
      append( headText, doc );
116
      append( footText, doc );
117
118
      final var map = mContext.getInterpolatedDefinitions();
119
      final var locale = mContext.getLocale();
120
      final var title = map.get( "document.title" );
121
      final var metadata = createMetaDataMap( doc, map );
122
      final var xhtml = DocumentParser.create( doc, metadata, locale, title );
123
      final var document = DTD + DocumentParser.toString( xhtml );
124
      final var curl = mContext.getCurlQuotes();
125
      final var curler = createCurler(
126
        FILTER_XML, Apostrophe.fromType( curl )
127
      );
128
129
      return curler.apply( document );
130
    }
131
    catch( final Exception ex ) {
132
      clue( ex );
133
    }
134
135
    return html;
136
  }
137
138
  public void append( final String html, final Document target ) {
139
    assert html != null;
140
    assert target != null;
141
142
    try {
143
      final var source = DocumentConverter.parse( html );
144
      final var sourceBody = source.head();
145
      final var sourceDoc = CONVERTER.fromJsoup( sourceBody );
146
      final var sourceRoot = sourceDoc.getDocumentElement();
147
      final var children = sourceRoot.getChildNodes();
148
149
      forEachChild(
150
        children, child -> {
151
          if( child.getNodeType() == Node.ELEMENT_NODE ) {
152
            final var name = child.getNodeName();
153
            final var targetElement = find( target, name );
154
155
            append( child, target, targetElement );
156
          }
157
        }
158
      );
159
160
    }
161
    catch( final Exception ex ) {
162
      clue( ex );
163
    }
164
  }
165
166
  private Node find( final Document document, final String name ) {
167
    assert document != null;
168
    assert name != null;
169
    assert !name.isBlank();
170
171
    try {
172
      // Both documents are well-formed at this point.
173
      return DocumentParser.evaluate( "//" + name, document );
174
    }
175
    catch( final Exception ex ) {
176
      // Implies that the head/body elements are missing from the document.
177
      final var element = createElement( document, name, null );
178
      document.getDocumentElement().appendChild( element );
179
180
      return element;
181
    }
182
  }
183
184
  private void append(
185
    final Node source,
186
    final Document targetDoc,
187
    final Node targetNode
188
  ) {
189
    assert source != null;
190
    assert targetDoc != null;
191
    assert targetNode != null;
192
193
    final var children = source.getChildNodes();
194
195
    forEachChild(
196
      children, child -> {
197
        final var imported = targetDoc.importNode( child, true );
198
        targetNode.appendChild( imported );
199
      }
200
    );
201
  }
202
203
  private void forEachChild(
204
    final NodeList children,
205
    final Consumer<Node> action
206
  ) {
207
    assert children != null;
208
    assert action != null;
209
210
    final var childCount = children.getLength();
211
212
    for( var i = 0; i < childCount; i++ ) {
213
      action.accept( children.item( i ) );
214
    }
215
  }
216
217
  /**
218
   * Generates document metadata, including word count.
219
   *
220
   * @param doc The document containing the text to tally.
221
   * @return A map of metadata key/value pairs.
222
   */
223
  private Map<String, String> createMetaDataMap(
224
    final Document doc, final InterpolatingMap map
225
  ) {
226
    final var result = new LinkedHashMap<String, String>();
227
    final var metadata = getMetadata();
228
229
    metadata.forEach(
230
      ( key, value ) -> {
231
        final var interpolated = map.interpolate( value );
232
233
        if( !interpolated.isEmpty() ) {
234
          result.put( key, interpolated );
235
        }
236
      }
237
    );
238
    result.put( "count", wordCount( doc ) );
239
240
    return result;
241
  }
242
243
  /**
244
   * The metadata is in list form because the user interface for entering the
245
   * key-value pairs is a table, which requires a generic {@link List} rather
246
   * than a generic {@link Map}.
247
   *
248
   * @return The document metadata.
249
   */
250
  private Map<String, String> getMetadata() {
251
    final var result = mContext.getMetadata();
252
    return result == null ? new HashMap<>() : result;
253
  }
254
255
  /**
256
   * Hashes the URL so that the number of files doesn't eat up disk space
257
   * over time. For static resources, a feature could be added to prevent
258
   * downloading the URL if the hashed filename already exists.
259
   *
260
   * @param src The source file's URL to download.
261
   * @return A {@link Path} to the local file containing the URL's contents.
262
   * @throws Exception Could not download or save the file.
263
   */
264
  private Path downloadImage( final String src ) throws Exception {
265
    final Path imagePath;
266
    final File imageFile;
267
    final var cachesPath = getCachesPath();
268
269
    clue( "Main.status.image.xhtml.image.download", src );
270
271
    try( final var response = open( src ) ) {
272
      final var mediaType = response.getMediaType();
273
274
      final var ext = MediaTypeExtension.valueFrom( mediaType ).getExtension();
275
      final var hash = DataTypeConverter.toHex( DataTypeConverter.hash( src ) );
276
      final var id = hash.toLowerCase();
277
278
      imagePath = cachesPath.resolve( APP_TITLE_ABBR + id + '.' + ext );
279
      imageFile = toFile( imagePath );
280
281
      // Preserve image files if auto-remove is turned off.
282
      if( autoRemove() ) {
283
        imageFile.deleteOnExit();
284
      }
285
286
      try( final var image = response.getInputStream() ) {
287
        copy( image, imagePath, REPLACE_EXISTING );
288
      }
289
290
      if( mediaType.isSvg() ) {
291
        sanitize( imagePath );
292
      }
293
    }
294
295
    final var key = imageFile.exists()
296
      ? "Main.status.image.xhtml.image.saved"
297
      : "Main.status.image.xhtml.image.failed";
298
    clue( key, imageFile );
299
300
    return imagePath;
301
  }
302
303
  private Path resolveImage( final String src ) throws Exception {
304
    var imagePath = getImagesPath();
305
    var found = false;
306
307
    Path imageFile = null;
308
309
    clue( "Main.status.image.xhtml.image.resolve", src );
310
311
    for( final var extension : getImageOrder() ) {
312
      final var filename = format(
313
        "%s%s%s", src, extension.isBlank() ? "" : ".", extension );
314
      imageFile = imagePath.resolve( filename );
315
316
      if( toFile( imageFile ).exists() ) {
317
        found = true;
318
        break;
319
      }
320
    }
321
322
    if( !found ) {
323
      imagePath = getDocumentDir();
324
      imageFile = imagePath.resolve( src );
325
326
      if( !toFile( imageFile ).exists() ) {
327
        final var filename = imageFile.toString();
328
        clue( "Main.status.image.xhtml.image.missing", filename );
329
330
        throw new FileNotFoundException( filename );
331
      }
332
    }
333
334
    clue( "Main.status.image.xhtml.image.found", imageFile.toString() );
335
336
    return imageFile;
337
  }
338
339
  private Path getImagesPath() {
340
    return mContext.getImageDir();
341
  }
342
343
  private Path getCachesPath() {
344
    return mContext.getCacheDir();
345
  }
346
347
  /**
348
   * By including an "empty" extension, the first element returned
349
   * will be the empty string. Thus, the first extension to try is the
350
   * file's default extension. Subsequent iterations will try to find
351
   * a file that has a name matching one of the preferred extensions.
352
   *
353
   * @return A list of extensions, including an empty string at the start.
354
   */
355
  private Iterable<String> getImageOrder() {
356
    return mContext.getImageOrder();
357
  }
358
359
  /**
360
   * Returns the absolute path to the document being edited, which can be used
361
   * to find files included using relative paths.
362
   *
363
   * @return The directory containing the edited file.
364
   */
365
  private Path getDocumentDir() {
366
    return mContext.getBaseDir();
367
  }
368
369
  private Locale getLocale() {
370
    return mContext.getLocale();
371
  }
372
373
  private boolean autoRemove() {
374
    return mContext.getAutoRemove();
375
  }
376
377
  private String wordCount( final Document doc ) {
378
    final var sb = new StringBuilder( 65536 * 10 );
379
380
    visit(
381
      doc,
382
      "//*[normalize-space( text() ) != '']",
383
      node -> sb.append( node.getTextContent() )
384
    );
385
386
    return valueOf( WordCounter.create( getLocale() ).count( sb.toString() ) );
387
  }
388
}
1389
A src/main/java/com/keenwrite/processors/markdown/BaseMarkdownProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown;
6
7
import com.keenwrite.dom.DocumentConverter;
8
import com.keenwrite.processors.ExecutorProcessor;
9
import com.keenwrite.processors.Processor;
10
import com.keenwrite.processors.ProcessorContext;
11
import com.keenwrite.processors.markdown.extensions.captions.CaptionExtension;
12
import com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension;
13
import com.keenwrite.processors.markdown.extensions.quotes.EscapedQuotesExtension;
14
import com.keenwrite.processors.markdown.extensions.r.RInlineExtension;
15
import com.keenwrite.processors.markdown.extensions.references.CrossReferenceExtension;
16
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
17
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
18
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
19
import com.vladsch.flexmark.ext.tables.TablesExtension;
20
import com.vladsch.flexmark.html.HtmlRenderer;
21
import com.vladsch.flexmark.parser.Parser;
22
import com.vladsch.flexmark.util.ast.IParse;
23
import com.vladsch.flexmark.util.ast.IRender;
24
import com.vladsch.flexmark.util.ast.Node;
25
import com.vladsch.flexmark.util.data.MutableDataSet;
26
import com.vladsch.flexmark.util.misc.Extension;
27
28
import java.util.ArrayList;
29
import java.util.List;
30
31
/**
32
 * Responsible for parsing and rendering Markdown into HTML. This is required
33
 * to break a circular dependency between the {@link MarkdownProcessor} and
34
 * {@link RInlineExtension}.
35
 */
36
public class BaseMarkdownProcessor extends ExecutorProcessor<String> {
37
  private final IParse mParser;
38
  private final IRender mRenderer;
39
40
  public BaseMarkdownProcessor(
41
    final Processor<String> successor, final ProcessorContext context ) {
42
    super( successor );
43
44
    final var options = new MutableDataSet();
45
    options.set( HtmlRenderer.GENERATE_HEADER_ID, true );
46
    options.set( HtmlRenderer.RENDER_HEADER_ID, true );
47
48
    final var builder = Parser.builder( options );
49
    final var extensions = createExtensions( context );
50
51
    mParser = builder.extensions( extensions ).build();
52
    mRenderer = HtmlRenderer
53
      .builder( options )
54
      .extensions( extensions )
55
      .build();
56
  }
57
58
  /**
59
   * Instantiates a number of extensions to be applied when parsing.
60
   *
61
   * @param context The context that subclasses use to configure custom
62
   *                extension behaviour.
63
   * @return A {@link List} of {@link Extension} instances that change the
64
   * {@link Parser}'s behaviour.
65
   */
66
  List<Extension> createExtensions( final ProcessorContext context ) {
67
    final var extensions = new ArrayList<Extension>();
68
69
    extensions.add( DefinitionExtension.create() );
70
    extensions.add( StrikethroughSubscriptExtension.create() );
71
    extensions.add( SuperscriptExtension.create() );
72
    extensions.add( TablesExtension.create() );
73
    extensions.add( FencedDivExtension.create() );
74
    extensions.add( CrossReferenceExtension.create() );
75
    extensions.add( CaptionExtension.create() );
76
    extensions.add( EscapedQuotesExtension.create() );
77
78
    return extensions;
79
  }
80
81
  /**
82
   * Converts the given Markdown string into HTML, without the doctype, html,
83
   * head, and body tags.
84
   *
85
   * @param markdown The string to convert from Markdown to HTML.
86
   * @return The HTML representation of the Markdown document.
87
   */
88
  @Override
89
  public String apply( final String markdown ) {
90
    return toXhtml( toHtml( toNode( markdown ) ) );
91
  }
92
93
  /**
94
   * Returns the AST in the form of a node for the given Markdown document. This
95
   * can be used, for example, to determine if a hyperlink exists inside a
96
   * paragraph.
97
   *
98
   * @param markdown The Markdown to convert into an AST.
99
   * @return The Markdown AST for the given text (usually a paragraph).
100
   */
101
  public Node toNode( final String markdown ) {
102
    return parse( markdown );
103
  }
104
105
  /**
106
   * Returns the result of converting the given AST into an HTML string.
107
   *
108
   * @param node The AST {@link Node} to convert to an HTML string.
109
   * @return The given {@link Node} as an HTML string.
110
   */
111
  private String toHtml( final Node node ) {
112
    return getRenderer().render( node );
113
  }
114
115
  /**
116
   * Ensures that subsequent processing will receive a well-formed document.
117
   * That is, an XHTML document.
118
   *
119
   * @param html Document to transform (may contain unbalanced HTML tags).
120
   * @return A well-formed (balanced) equivalent HTML document.
121
   */
122
  private String toXhtml( final String html ) {
123
    return DocumentConverter.parse( html ).html();
124
  }
125
126
  /**
127
   * Helper method to create an AST given some Markdown.
128
   *
129
   * @param markdown The Markdown to parse.
130
   * @return The root node of the Markdown tree.
131
   */
132
  private Node parse( final String markdown ) {
133
    return getParser().parse( markdown );
134
  }
135
136
  /**
137
   * Creates the Markdown document processor.
138
   *
139
   * @return An instance of {@link IParse} for building abstract syntax trees.
140
   */
141
  private IParse getParser() {
142
    return mParser;
143
  }
144
145
  private IRender getRenderer() {
146
    return mRenderer;
147
  }
148
}
1149
A src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown;
6
7
import com.keenwrite.ExportFormat;
8
import com.keenwrite.io.MediaType;
9
import com.keenwrite.processors.Processor;
10
import com.keenwrite.processors.ProcessorContext;
11
import com.keenwrite.processors.variable.VariableProcessor;
12
import com.keenwrite.processors.markdown.extensions.caret.CaretExtension;
13
import com.keenwrite.processors.markdown.extensions.fences.ImageBlockExtension;
14
import com.keenwrite.processors.markdown.extensions.images.ImageLinkExtension;
15
import com.keenwrite.processors.markdown.extensions.outline.DocumentOutlineExtension;
16
import com.keenwrite.processors.markdown.extensions.r.RInlineExtension;
17
import com.keenwrite.processors.markdown.extensions.tex.TexExtension;
18
import com.keenwrite.processors.r.RInlineEvaluator;
19
import com.keenwrite.processors.variable.RVariableProcessor;
20
import com.vladsch.flexmark.util.misc.Extension;
21
22
import java.util.ArrayList;
23
import java.util.List;
24
import java.util.function.Function;
25
26
import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN;
27
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
28
29
/**
30
 * Responsible for parsing a Markdown document and rendering it as HTML.
31
 */
32
public final class MarkdownProcessor extends BaseMarkdownProcessor {
33
  private MarkdownProcessor(
34
    final Processor<String> successor, final ProcessorContext context ) {
35
    super( successor, context );
36
  }
37
38
  public static MarkdownProcessor create( final ProcessorContext context ) {
39
    return create( IDENTITY, context );
40
  }
41
42
  public static MarkdownProcessor create(
43
    final Processor<String> successor, final ProcessorContext context ) {
44
    return new MarkdownProcessor( successor, context );
45
  }
46
47
  /**
48
   * Creating extensions based using an instance of {@link ProcessorContext}
49
   * indicates that the {@link CaretExtension} should be used to inject the
50
   * caret position into the final HTML document. This enables the HTML
51
   * preview pane to scroll to the same position, relatively speaking, within
52
   * the main document. Scrolling is developed this way to decouple the
53
   * document being edited from the preview pane so that multiple document
54
   * formats can be edited.
55
   *
56
   * @param context Contains necessary information needed to create
57
   *                extensions used by the Markdown parser.
58
   * @return {@link List} of extensions invoked when parsing Markdown.
59
   */
60
  @Override
61
  List<Extension> createExtensions( final ProcessorContext context ) {
62
    final var inputPath = context.getSourcePath();
63
    final var mediaType = MediaType.fromFilename( inputPath );
64
    final Processor<String> processor;
65
    final Function<String, String> evaluator;
66
    final List<Extension> result = new ArrayList<>();
67
68
    if( mediaType == TEXT_R_MARKDOWN ) {
69
      final var rVarProcessor = new RVariableProcessor( IDENTITY, context );
70
      final var rInlineEvaluator = new RInlineEvaluator( rVarProcessor );
71
      result.add( RInlineExtension.create( rInlineEvaluator, context ) );
72
      processor = rVarProcessor;
73
      evaluator = rInlineEvaluator;
74
    }
75
    else {
76
      processor = new VariableProcessor( IDENTITY, context );
77
      evaluator = processor;
78
    }
79
80
    // Add typographic, table, strikethrough, and similar extensions.
81
    result.addAll( super.createExtensions( context ) );
82
83
    result.add( ImageLinkExtension.create( context ) );
84
    result.add( TexExtension.create( evaluator, context ) );
85
    result.add( ImageBlockExtension.create( processor, evaluator, context ) );
86
87
    if( context.isExportFormat( ExportFormat.NONE ) ) {
88
      result.add( CaretExtension.create( context ) );
89
    }
90
91
    result.add( DocumentOutlineExtension.create( processor ) );
92
    return result;
93
  }
94
}
195
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionBlock.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.vladsch.flexmark.html.HtmlWriter;
8
import com.vladsch.flexmark.parser.InlineParser;
9
import com.vladsch.flexmark.util.ast.Block;
10
import com.vladsch.flexmark.util.sequence.BasedSequence;
11
import org.jetbrains.annotations.NotNull;
12
13
/**
14
 * Responsible for retaining the text node and all child nodes with respect
15
 * to a caption. The caption can be associated with most items, such as
16
 * block quotes, tables, math expressions, and images.
17
 */
18
class CaptionBlock extends Block {
19
  private final BasedSequence mCaption;
20
21
  CaptionBlock( final BasedSequence caption ) {
22
    assert caption != null;
23
24
    mCaption = caption;
25
  }
26
27
  /**
28
   * Opens the caption.
29
   *
30
   * @param writer Where to write the opening tags.
31
   */
32
  void opening( final HtmlWriter writer ) {
33
    writer.raw( "<span class=\"caption\">" );
34
  }
35
36
  /**
37
   * Closes the caption.
38
   *
39
   * @param writer Where to write the closing tags.
40
   */
41
  void closing( final HtmlWriter writer ) {
42
    writer.raw( "</span>" );
43
  }
44
45
  void parse( final InlineParser inlineParser ) {
46
    assert inlineParser != null;
47
48
    inlineParser.parse( mCaption, this );
49
  }
50
51
  @NotNull
52
  @Override
53
  public BasedSequence[] getSegments() {
54
    return BasedSequence.EMPTY_SEGMENTS;
55
  }
56
}
157
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionBlockParserFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.vladsch.flexmark.parser.block.AbstractBlockParserFactory;
8
import com.vladsch.flexmark.parser.block.BlockStart;
9
import com.vladsch.flexmark.parser.block.MatchedBlockParser;
10
import com.vladsch.flexmark.parser.block.ParserState;
11
import com.vladsch.flexmark.util.data.DataHolder;
12
13
class CaptionBlockParserFactory extends AbstractBlockParserFactory {
14
  CaptionBlockParserFactory( final DataHolder options ) {
15
    super( options );
16
  }
17
18
  @Override
19
  public BlockStart tryStart(
20
    final ParserState state,
21
    final MatchedBlockParser matchedBlockParser ) {
22
23
    final var flush = state.getIndent() == 0;
24
    final var index = state.getNextNonSpaceIndex();
25
    final var line = state.getLine();
26
    final var length = line.length();
27
    final var text = line.subSequence( index, length );
28
29
    return flush && CaptionParser.canParse( text )
30
      ? BlockStart.of( new CaptionParser( text ) ).atIndex( length )
31
      : BlockStart.none();
32
  }
33
}
134
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionCustomBlockParserFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownCustomBlockParserFactory;
8
import com.vladsch.flexmark.parser.block.BlockParserFactory;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
11
class CaptionCustomBlockParserFactory extends MarkdownCustomBlockParserFactory {
12
  CaptionCustomBlockParserFactory() {}
13
14
  @Override
15
  public BlockParserFactory createBlockParserFactory(
16
    final DataHolder options ) {
17
    return new CaptionBlockParserFactory( options );
18
  }
19
}
120
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionExtension.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownRendererExtension;
8
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
9
import com.vladsch.flexmark.parser.Parser.Builder;
10
11
/**
12
 * Responsible for parsing and rendering {@link CaptionBlock} instances.
13
 */
14
public final class CaptionExtension extends MarkdownRendererExtension {
15
  /**
16
   * Use {@link #create()}.
17
   */
18
  private CaptionExtension() {}
19
20
  /**
21
   * Returns a new {@link CaptionExtension}.
22
   *
23
   * @return An extension capable of parsing caption syntax.
24
   */
25
  public static CaptionExtension create() {
26
    return new CaptionExtension();
27
  }
28
29
  @Override
30
  public void extend( final Builder builder ) {
31
    builder.customBlockParserFactory( new CaptionCustomBlockParserFactory() );
32
    builder.postProcessorFactory( new CaptionPostProcessorFactory() );
33
  }
34
35
  @Override
36
  protected NodeRendererFactory createNodeRendererFactory() {
37
    return new CaptionNodeRendererFactory();
38
  }
39
}
140
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionNodeRenderer.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.keenwrite.processors.markdown.extensions.references.CrossReferenceNode;
8
import com.vladsch.flexmark.html.HtmlWriter;
9
import com.vladsch.flexmark.html.renderer.CoreNodeRenderer;
10
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
11
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
12
import com.vladsch.flexmark.util.ast.Node;
13
import com.vladsch.flexmark.util.data.DataHolder;
14
15
import java.util.HashSet;
16
import java.util.LinkedList;
17
import java.util.List;
18
import java.util.Set;
19
20
/**
21
 * Responsible for rendering {@link CaptionBlock} instances as HTML (via
22
 * delegation).
23
 */
24
class CaptionNodeRenderer extends CoreNodeRenderer {
25
  CaptionNodeRenderer( final DataHolder options ) {
26
    super( options );
27
  }
28
29
  @Override
30
  public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
31
    return new HashSet<>( List.of(
32
      new NodeRenderingHandler<>( CaptionBlock.class, this::render )
33
    ) );
34
  }
35
36
  private void render(
37
    final CaptionBlock node,
38
    final NodeRendererContext context,
39
    final HtmlWriter html ) {
40
    final var anchors = new LinkedList<Node>();
41
42
    html.raw( "<p>" );
43
    node.opening( html );
44
45
    if( node.hasChildren() ) {
46
      for( final var child : node.getChildren() ) {
47
        if( !child.isOrDescendantOfType( CrossReferenceNode.class ) ) {
48
          context.render( child );
49
        }
50
        else {
51
          anchors.add( child );
52
        }
53
      }
54
    }
55
56
    node.closing( html );
57
58
    for( final var anchor : anchors ) {
59
      context.render( anchor );
60
    }
61
62
    html.raw( "</p>" );
63
  }
64
}
165
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionNodeRendererFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownNodeRendererFactory;
8
import com.vladsch.flexmark.html.renderer.NodeRenderer;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
11
class CaptionNodeRendererFactory extends MarkdownNodeRendererFactory {
12
  @Override
13
  protected NodeRenderer createNodeRenderer( final DataHolder options ) {
14
    return new CaptionNodeRenderer( options );
15
  }
16
}
117
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionParser.java
1
package com.keenwrite.processors.markdown.extensions.captions;
2
3
import com.vladsch.flexmark.parser.InlineParser;
4
import com.vladsch.flexmark.parser.block.AbstractBlockParser;
5
import com.vladsch.flexmark.parser.block.BlockContinue;
6
import com.vladsch.flexmark.parser.block.ParserState;
7
import com.vladsch.flexmark.util.ast.Block;
8
import com.vladsch.flexmark.util.sequence.BasedSequence;
9
10
class CaptionParser extends AbstractBlockParser {
11
  private final CaptionBlock mBlock;
12
13
  CaptionParser( final BasedSequence text ) {
14
    assert text != null;
15
    assert text.isNotEmpty();
16
    assert text.length() > 2;
17
18
    final var caption = text.subSequence( 2 );
19
20
    mBlock = new CaptionBlock( caption.trim() );
21
  }
22
23
  static boolean canParse( final BasedSequence text ) {
24
    return text.length() > 3 &&
25
           text.charAt( 0 ) == ':' &&
26
           text.charAt( 1 ) == ':' &&
27
           text.charAt( 2 ) != ':';
28
  }
29
30
  @Override
31
  public Block getBlock() {
32
    return mBlock;
33
  }
34
35
  @Override
36
  public BlockContinue tryContinue( final ParserState state ) {
37
    return BlockContinue.none();
38
  }
39
40
  @Override
41
  public void parseInlines( final InlineParser inlineParser ) {
42
    assert inlineParser != null;
43
44
    mBlock.parse( inlineParser );
45
  }
46
47
  @Override
48
  public void closeBlock( final ParserState state ) {}
49
}
150
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionPostProcessor.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.keenwrite.processors.markdown.extensions.fences.blocks.ClosingDivBlock;
8
import com.keenwrite.processors.markdown.extensions.fences.blocks.OpeningDivBlock;
9
import com.vladsch.flexmark.parser.block.NodePostProcessor;
10
import com.vladsch.flexmark.util.ast.Node;
11
import com.vladsch.flexmark.util.ast.NodeTracker;
12
import org.jetbrains.annotations.NotNull;
13
14
/**
15
 * Captions are written most naturally <em>after</em>> the element that they
16
 * apply to, regardless of whether they are figures, tables, code listings,
17
 * algorithms, or equations. The typesetting software uses event-based parsing
18
 * of XML elements, meaning the DOM isn't fully loaded into memory. This means
19
 * that captions must come <em>before</em> the item being captioned.
20
 * <p>
21
 * To reconcile this UX conundrum, we swap captions with the previous node.
22
 */
23
class CaptionPostProcessor extends NodePostProcessor {
24
  @Override
25
  public void process(
26
    @NotNull final NodeTracker state,
27
    @NotNull final Node caption ) {
28
29
    var previous = caption.getPrevious();
30
31
    if( previous != null ) {
32
      swap( previous, caption );
33
    }
34
  }
35
36
  private void swap( final Node previous, final Node caption ) {
37
    assert previous != null;
38
    assert caption != null;
39
40
    var swap = previous;
41
    boolean found = true;
42
43
    if( swap.isOrDescendantOfType( ClosingDivBlock.class ) ) {
44
      found = false;
45
46
      while( !found && swap != null ) {
47
        if( swap.isOrDescendantOfType( OpeningDivBlock.class ) ) {
48
          found = true;
49
        }
50
        else {
51
          swap = swap.getPrevious();
52
        }
53
      }
54
    }
55
56
    if( found ) {
57
      swap.insertBefore( caption );
58
    }
59
  }
60
}
161
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionPostProcessorFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownPostProcessorFactory;
8
import com.vladsch.flexmark.parser.block.NodePostProcessor;
9
import com.vladsch.flexmark.util.ast.Document;
10
11
class CaptionPostProcessorFactory extends MarkdownPostProcessorFactory {
12
  CaptionPostProcessorFactory() {
13
    // The argument isn't used by the Markdown parsing library.
14
    super( false );
15
16
    addNodes( CaptionBlock.class );
17
  }
18
19
  @Override
20
  protected NodePostProcessor createPostProcessor( final Document document ) {
21
    return new CaptionPostProcessor();
22
  }
23
}
124
A src/main/java/com/keenwrite/processors/markdown/extensions/caret/CaretExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.caret;
3
4
import com.keenwrite.editors.common.Caret;
5
import com.keenwrite.processors.ProcessorContext;
6
import com.keenwrite.processors.markdown.extensions.common.HtmlRendererAdapter;
7
import org.jetbrains.annotations.NotNull;
8
9
import java.util.function.Supplier;
10
11
import static com.keenwrite.processors.markdown.extensions.caret.IdAttributeProvider.createFactory;
12
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
13
14
/**
15
 * Responsible for giving most block-level elements a unique identifier
16
 * attribute. The identifier is used to coordinate scrolling.
17
 */
18
public class CaretExtension extends HtmlRendererAdapter {
19
20
  private final Supplier<Caret> mCaret;
21
22
  private CaretExtension( final ProcessorContext context ) {
23
    mCaret = context.getCaret();
24
  }
25
26
  public static CaretExtension create( final ProcessorContext context ) {
27
    return new CaretExtension( context );
28
  }
29
30
  @Override
31
  public void extend(
32
    @NotNull final Builder builder,
33
    @NotNull final String rendererType ) {
34
    builder.attributeProviderFactory( createFactory( mCaret ) );
35
  }
36
}
137
A src/main/java/com/keenwrite/processors/markdown/extensions/caret/IdAttributeProvider.java
1
package com.keenwrite.processors.markdown.extensions.caret;
2
3
import com.keenwrite.constants.Constants;
4
import com.keenwrite.editors.common.Caret;
5
import com.vladsch.flexmark.ext.tables.TableBlock;
6
import com.vladsch.flexmark.html.AttributeProvider;
7
import com.vladsch.flexmark.html.AttributeProviderFactory;
8
import com.vladsch.flexmark.html.IndependentAttributeProviderFactory;
9
import com.vladsch.flexmark.html.renderer.AttributablePart;
10
import com.vladsch.flexmark.html.renderer.LinkResolverContext;
11
import com.vladsch.flexmark.util.ast.Node;
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 java.util.function.Supplier;
17
18
import static com.keenwrite.constants.Constants.CARET_ID;
19
import static com.keenwrite.processors.markdown.extensions.common.EmptyNode.EMPTY_NODE;
20
21
/**
22
 * Responsible for creating the id attribute. This class is instantiated
23
 * once: for the HTML element containing the {@link Constants#CARET_ID}.
24
 */
25
final class IdAttributeProvider implements AttributeProvider {
26
  private final Supplier<Caret> mCaret;
27
  private boolean mAdded;
28
29
  public IdAttributeProvider( final Supplier<Caret> caret ) {
30
    mCaret = caret;
31
  }
32
33
  static AttributeProviderFactory createFactory(
34
    final Supplier<Caret> caret ) {
35
    return new IndependentAttributeProviderFactory() {
36
      @Override
37
      public @NotNull AttributeProvider apply(
38
        @NotNull final LinkResolverContext context ) {
39
        return new IdAttributeProvider( caret );
40
      }
41
    };
42
  }
43
44
  @Override
45
  public void setAttributes(
46
    @NotNull final Node curr,
47
    @NotNull final AttributablePart part,
48
    @NotNull final MutableAttributes attributes ) {
49
    // Optimization: if a caret is inserted, don't try to find another.
50
    if( mAdded ) {
51
      return;
52
    }
53
54
    final var caret = mCaret.get();
55
56
    // If a table block has been earmarked with an empty node, it means
57
    // another extension has generated code from an external source. The
58
    // Markdown processor won't be able to determine the caret position
59
    // with any semblance of accuracy, so skip the element. This usually
60
    // happens with tables, but in theory any Markdown generated from an
61
    // external source (e.g., an R script) could produce text that has no
62
    // caret position that can be calculated.
63
    var table = curr;
64
65
    if( !(curr instanceof TableBlock) ) {
66
      table = curr.getAncestorOfType( TableBlock.class );
67
    }
68
69
    // The table was generated outside the document
70
    if( table != null && table.getLastChild() == EMPTY_NODE ) {
71
      return;
72
    }
73
74
    final var outside = caret.isAfterText() ? 1 : 0;
75
    final var began = curr.getStartOffset();
76
    final var ended = curr.getEndOffset() + outside;
77
    final var prev = curr.getPrevious();
78
79
    // If the caret is within the bounds of the current node or the
80
    // caret is within the bounds of the end of the previous node and
81
    // the start of the current node, then mark the current node with
82
    // a caret indicator.
83
    if( caret.isBetweenText( began, ended ) ||
84
        prev != null && caret.isBetweenText( prev.getEndOffset(), began ) ) {
85
      // This line empowers synchronizing the text editor with the preview.
86
      attributes.addValue( AttributeImpl.of( "id", CARET_ID ) );
87
88
      // We're done until the user moves the caret (micro-optimization)
89
      mAdded = true;
90
    }
91
  }
92
}
193
A src/main/java/com/keenwrite/processors/markdown/extensions/common/EmptyNode.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.common;
3
4
import com.vladsch.flexmark.util.ast.Node;
5
import com.vladsch.flexmark.util.sequence.BasedSequence;
6
import org.jetbrains.annotations.NotNull;
7
8
/**
9
 * The singleton is injected into the abstract syntax tree to mark an instance
10
 * of {@link Node} such that it must not be processed normally. Using a wrapper
11
 * for a given {@link Node} cannot work because the class type is used by
12
 * the parsing library for processing.
13
 */
14
public final class EmptyNode extends Node {
15
  public static final Node EMPTY_NODE = new EmptyNode();
16
17
  /**
18
   * Use {@link #EMPTY_NODE}.
19
   */
20
  private EmptyNode() {}
21
22
  @NotNull
23
  @Override
24
  public BasedSequence[] getSegments() {
25
    return BasedSequence.EMPTY_SEGMENTS;
26
  }
27
}
128
A src/main/java/com/keenwrite/processors/markdown/extensions/common/HtmlRendererAdapter.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.common;
3
4
import com.vladsch.flexmark.util.data.MutableDataHolder;
5
import org.jetbrains.annotations.NotNull;
6
7
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
8
9
/**
10
 * Hides the {@link #rendererOptions(MutableDataHolder)} from subclasses
11
 * that would otherwise implement the {@link HtmlRendererExtension} interface.
12
 */
13
public abstract class HtmlRendererAdapter implements HtmlRendererExtension {
14
  /**
15
   * Empty, unused.
16
   *
17
   * @param options Ignored.
18
   */
19
  @Override
20
  public void rendererOptions( @NotNull final MutableDataHolder options ) { }
21
}
122
A src/main/java/com/keenwrite/processors/markdown/extensions/common/MarkdownCustomBlockParserFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.common;
6
7
import com.vladsch.flexmark.parser.block.BlockParserFactory;
8
import com.vladsch.flexmark.parser.block.CustomBlockParserFactory;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
import org.jetbrains.annotations.NotNull;
11
import org.jetbrains.annotations.Nullable;
12
13
import java.util.Set;
14
15
public abstract class MarkdownCustomBlockParserFactory
16
  implements CustomBlockParserFactory {
17
  /**
18
   * Subclasses must return a new {@link BlockParserFactory} instance.
19
   *
20
   * @param options Passed into the new instance constructor.
21
   * @return The new {@link BlockParserFactory} instance.
22
   */
23
  protected abstract BlockParserFactory createBlockParserFactory(
24
    DataHolder options );
25
26
  @NotNull
27
  @Override
28
  public BlockParserFactory apply( @NotNull final DataHolder options ) {
29
    return createBlockParserFactory( options );
30
  }
31
32
  @Override
33
  public @Nullable Set<Class<?>> getAfterDependents() {
34
    return null;
35
  }
36
37
  @Override
38
  public @Nullable Set<Class<?>> getBeforeDependents() {
39
    return null;
40
  }
41
42
  @Override
43
  public boolean affectsGlobalScope() {
44
    return false;
45
  }
46
}
147
A src/main/java/com/keenwrite/processors/markdown/extensions/common/MarkdownNodeRendererFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.common;
6
7
import com.vladsch.flexmark.html.renderer.NodeRenderer;
8
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
import org.jetbrains.annotations.NotNull;
11
12
public abstract class MarkdownNodeRendererFactory
13
  implements NodeRendererFactory {
14
  @NotNull
15
  @Override
16
  public NodeRenderer apply( @NotNull final DataHolder options ) {
17
    return createNodeRenderer( options );
18
  }
19
20
  protected abstract NodeRenderer createNodeRenderer( DataHolder options );
21
}
122
A src/main/java/com/keenwrite/processors/markdown/extensions/common/MarkdownParserExtension.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.common;
6
7
import com.vladsch.flexmark.parser.Parser.ParserExtension;
8
import com.vladsch.flexmark.util.data.MutableDataHolder;
9
10
/**
11
 * Provides a default {@link #parserOptions(MutableDataHolder)} implementation.
12
 */
13
public interface MarkdownParserExtension extends ParserExtension {
14
  @Override
15
  default void parserOptions( final MutableDataHolder options ) {}
16
}
117
A src/main/java/com/keenwrite/processors/markdown/extensions/common/MarkdownPostProcessorFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.common;
6
7
import com.vladsch.flexmark.parser.block.NodePostProcessor;
8
import com.vladsch.flexmark.parser.block.NodePostProcessorFactory;
9
import com.vladsch.flexmark.util.ast.Document;
10
import org.jetbrains.annotations.NotNull;
11
12
public abstract class MarkdownPostProcessorFactory
13
  extends NodePostProcessorFactory {
14
  public MarkdownPostProcessorFactory( final boolean ignored ) {
15
    super( ignored );
16
  }
17
18
  @NotNull
19
  @Override
20
  public NodePostProcessor apply( @NotNull Document document ) {
21
    return createPostProcessor( document );
22
  }
23
24
  protected abstract NodePostProcessor createPostProcessor( Document document );
25
}
126
A src/main/java/com/keenwrite/processors/markdown/extensions/common/MarkdownRendererExtension.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.common;
6
7
import com.vladsch.flexmark.html.HtmlRenderer.Builder;
8
import com.vladsch.flexmark.html.renderer.NodeRenderer;
9
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
10
import org.jetbrains.annotations.NotNull;
11
12
public abstract class MarkdownRendererExtension extends HtmlRendererAdapter
13
  implements MarkdownParserExtension {
14
15
  /**
16
   * Implemented by subclasses to create the {@link NodeRendererFactory} capable
17
   * of converting nodes created by an extension into HTML elements.
18
   *
19
   * @return The {@link NodeRendererFactory} for producing {@link NodeRenderer}
20
   * instances.
21
   */
22
  protected abstract NodeRendererFactory createNodeRendererFactory();
23
24
  /**
25
   * Adds an extension for HTML document export types.
26
   *
27
   * @param builder      The document builder.
28
   * @param rendererType Indicates the document type to be built.
29
   */
30
  @Override
31
  public void extend(
32
    @NotNull final Builder builder,
33
    @NotNull final String rendererType ) {
34
    if( "HTML".equalsIgnoreCase( rendererType ) ) {
35
      builder.nodeRendererFactory( createNodeRendererFactory() );
36
    }
37
  }
38
}
139
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivExtension.java
1
/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences;
3
4
import com.keenwrite.processors.markdown.extensions.common.MarkdownParserExtension;
5
import com.keenwrite.processors.markdown.extensions.common.MarkdownRendererExtension;
6
import com.keenwrite.processors.markdown.extensions.fences.factories.CustomDivBlockParserFactory;
7
import com.keenwrite.processors.markdown.extensions.fences.factories.FencedDivNodeRendererFactory;
8
import com.keenwrite.processors.markdown.extensions.fences.factories.FencedDivPreProcessorFactory;
9
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
10
import com.vladsch.flexmark.parser.Parser.Builder;
11
12
import java.util.regex.Pattern;
13
14
import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS;
15
import static java.util.regex.Pattern.compile;
16
17
/**
18
 * Responsible for parsing div block syntax into HTML div tags. Fenced div
19
 * blocks start with three or more consecutive colons, followed by a space,
20
 * followed by attributes. The attributes can be either a single word, or
21
 * multiple words nested in braces. For example:
22
 *
23
 * <p>
24
 * ::: poem
25
 * Tyger Tyger, burning bright,
26
 * In the forests of the night;
27
 * What immortal hand or eye,
28
 * Could frame thy fearful symmetry?
29
 * :::
30
 * </p>
31
 * <p>
32
 * As well as:
33
 * </p>
34
 * <p>
35
 * ::: {#verse .p .d k=v author="Dickinson"}
36
 * Because I could not stop for Death --
37
 * He kindly stopped for me --
38
 * The Carriage held but just Ourselves --
39
 * And Immortality.
40
 * :::
41
 * </p>
42
 *
43
 * <p>
44
 * The second example produces the following starting {@code div} element:
45
 * </p>
46
 * {@literal <div id="verse" class="p d" data-k="v" data-author="Dickson">}
47
 *
48
 * <p>
49
 * This will parse fenced divs embedded inside of blockquote environments.
50
 * </p>
51
 */
52
public class FencedDivExtension extends MarkdownRendererExtension
53
  implements MarkdownParserExtension {
54
  /**
55
   * Matches any number of colons at start of line. This will match both the
56
   * opening and closing fences, with any number of colons.
57
   */
58
  public static final Pattern FENCE = compile( "^:::+.*" );
59
60
  /**
61
   * After a fenced div is detected, this will match the opening fence.
62
   */
63
  public static final Pattern FENCE_OPENING = compile(
64
    "^:::+\\s+([\\p{Alnum}\\-_]+|\\{.+})\\s*$",
65
    UNICODE_CHARACTER_CLASS );
66
67
  public static FencedDivExtension create() {
68
    return new FencedDivExtension();
69
  }
70
71
  @Override
72
  public void extend( final Builder builder ) {
73
    builder.customBlockParserFactory( new CustomDivBlockParserFactory() );
74
    builder.paragraphPreProcessorFactory( new FencedDivPreProcessorFactory() );
75
  }
76
77
  @Override
78
  protected NodeRendererFactory createNodeRendererFactory() {
79
    return new FencedDivNodeRendererFactory();
80
  }
81
}
182
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/ImageBlockExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences;
3
4
import com.keenwrite.preview.DiagramUrlGenerator;
5
import com.keenwrite.processors.Processor;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.keenwrite.processors.variable.VariableProcessor;
8
import com.keenwrite.processors.markdown.MarkdownProcessor;
9
import com.keenwrite.processors.markdown.extensions.common.HtmlRendererAdapter;
10
import com.keenwrite.processors.r.RChunkEvaluator;
11
import com.keenwrite.processors.variable.RVariableProcessor;
12
import com.vladsch.flexmark.ast.FencedCodeBlock;
13
import com.vladsch.flexmark.html.HtmlRendererOptions;
14
import com.vladsch.flexmark.html.HtmlWriter;
15
import com.vladsch.flexmark.html.renderer.*;
16
import com.vladsch.flexmark.util.data.DataHolder;
17
import com.vladsch.flexmark.util.sequence.BasedSequence;
18
import com.whitemagicsoftware.keenquotes.util.Tuple;
19
import org.jetbrains.annotations.NotNull;
20
21
import java.nio.file.Path;
22
import java.util.HashSet;
23
import java.util.Set;
24
import java.util.function.Function;
25
26
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
27
import static com.keenwrite.constants.Constants.TEMPORARY_DIRECTORY;
28
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
29
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
30
import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT;
31
import static com.vladsch.flexmark.html.renderer.LinkType.LINK;
32
import static java.lang.String.format;
33
34
/**
35
 * Responsible for converting textual diagram descriptions into HTML image
36
 * elements.
37
 */
38
public final class ImageBlockExtension extends HtmlRendererAdapter {
39
  /**
40
   * Ensure that the device is always closed to prevent an out-of-resources
41
   * error, regardless of whether the R expression the user tries to evaluate
42
   * is valid by swallowing errors alongside a {@code finally} block.
43
   */
44
  private static final String R_SVG_EXPORT =
45
    "tryCatch({svg('%s'%s)%n%s%n},finally={dev.off()})%n";
46
47
  private static final String STYLE_DIAGRAM = "diagram-";
48
  private static final int STYLE_DIAGRAM_LEN = STYLE_DIAGRAM.length();
49
50
  private static final String STYLE_R_CHUNK = "{r";
51
52
  private static final class VerbatimRVariableProcessor
53
    extends RVariableProcessor {
54
55
    public VerbatimRVariableProcessor(
56
      final Processor<String> successor, final ProcessorContext context ) {
57
      super( successor, context );
58
    }
59
60
    @Override
61
    protected String processValue( final String value ) {
62
      return value;
63
    }
64
  }
65
66
  private final RChunkEvaluator mRChunkEvaluator;
67
  private final Function<String, String> mInlineEvaluator;
68
69
  private final Processor<String> mRVariableProcessor;
70
  private final ProcessorContext mContext;
71
72
  public ImageBlockExtension(
73
    final Processor<String> processor,
74
    final Function<String, String> evaluator,
75
    final ProcessorContext context ) {
76
    assert processor != null;
77
    assert context != null;
78
    mContext = context;
79
    mRChunkEvaluator = new RChunkEvaluator();
80
    mInlineEvaluator = evaluator;
81
    mRVariableProcessor = new VerbatimRVariableProcessor( IDENTITY, context );
82
  }
83
84
  /**
85
   * Creates a new parser for fenced blocks. This calls out to a web service
86
   * to generate SVG files of text diagrams.
87
   * <p>
88
   * Internally, this creates a {@link VariableProcessor} to substitute
89
   * variable definitions. This is necessary because the order of processors
90
   * matters. If the {@link VariableProcessor} comes before an instance of
91
   * {@link MarkdownProcessor}, for example, then the caret position in the
92
   * preview pane will not align with the caret position in the editor
93
   * pane. The {@link MarkdownProcessor} must come before all else. However,
94
   * when parsing fenced blocks, the variables within the block must be
95
   * interpolated before being sent to the diagram web service.
96
   * </p>
97
   *
98
   * @param processor Used to pre-process the text.
99
   * @return A new {@link ImageBlockExtension} capable of shunting ASCII
100
   * diagrams to a service for conversion to SVG.
101
   */
102
  public static ImageBlockExtension create(
103
    final Processor<String> processor,
104
    final Function<String, String> evaluator,
105
    final ProcessorContext context ) {
106
    assert processor != null;
107
    assert context != null;
108
    return new ImageBlockExtension( processor, evaluator, context );
109
  }
110
111
  @Override
112
  public void extend(
113
    @NotNull final Builder builder, @NotNull final String rendererType ) {
114
    builder.nodeRendererFactory( new Factory() );
115
  }
116
117
  /**
118
   * Converts the given {@link BasedSequence} to a lowercase value.
119
   *
120
   * @param text The character string to convert to lowercase.
121
   * @return The lowercase text value, or the empty string for no text.
122
   */
123
  private static String sanitize( final BasedSequence text ) {
124
    assert text != null;
125
    return text.toString().toLowerCase();
126
  }
127
128
  /**
129
   * Responsible for generating images from a fenced block that contains a
130
   * diagram reference.
131
   */
132
  private class CustomRenderer implements NodeRenderer {
133
134
    @Override
135
    public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
136
      final var set = new HashSet<NodeRenderingHandler<?>>();
137
138
      set.add( new NodeRenderingHandler<>(
139
        FencedCodeBlock.class, ( node, context, html ) -> {
140
        final var style = sanitize( node.getInfo() );
141
        final Tuple<String, ResolvedLink> imagePair;
142
143
        if( style.startsWith( STYLE_DIAGRAM ) ) {
144
          imagePair = importTextDiagram( style, node, context );
145
146
          html.attr( "src", imagePair.item1() );
147
          html.withAttr( imagePair.item2() );
148
          html.tagVoid( "img" );
149
        }
150
        else if( style.startsWith( STYLE_R_CHUNK ) ) {
151
          imagePair = evaluateRChunk( node, context );
152
153
          html.attr( "src", imagePair.item1() );
154
          html.withAttr( imagePair.item2() );
155
          html.tagVoid( "img" );
156
        }
157
        else {
158
          // TODO: Revert to using context.delegateRender() after flexmark
159
          //   is updated to no longer trim blank lines up to the EOL.
160
          render( node, context, html );
161
        }
162
      } ) );
163
164
      return set;
165
    }
166
167
    private Tuple<String, ResolvedLink> importTextDiagram(
168
      final String style,
169
      final FencedCodeBlock node,
170
      final NodeRendererContext context ) {
171
172
      final var type = style.substring( STYLE_DIAGRAM_LEN );
173
      final var content = node.getContentChars().normalizeEOL();
174
      final var text = mInlineEvaluator.apply( content );
175
      final var server = mContext.getImageServer();
176
      final var source = DiagramUrlGenerator.toUrl( server, type, text );
177
      final var link = context.resolveLink( LINK, source, false );
178
179
      return new Tuple<>( source, link );
180
    }
181
182
    /**
183
     * Evaluates an R expression. This will take into consideration any
184
     * key/value pairs passed in from the document, such as width and height
185
     * attributes of the form: <code>{r width=5 height=5}</code>.
186
     *
187
     * @param node    The {@link FencedCodeBlock} to evaluate using R.
188
     * @param context Used to resolve the link that refers to any resulting
189
     *                image produced by the R chunk (such as a plot).
190
     * @return The SVG text string associated with the content produced by
191
     * the chunk (such as a graphical data plot).
192
     */
193
    @SuppressWarnings( "unused" )
194
    private Tuple<String, ResolvedLink> evaluateRChunk(
195
      final FencedCodeBlock node,
196
      final NodeRendererContext context ) {
197
      final var content = node.getContentChars().normalizeEOL().trim();
198
      final var text = mRVariableProcessor.apply( content );
199
      final var hash = Integer.toHexString( text.hashCode() );
200
      final var filename = format( "%s-%s.svg", APP_TITLE_LOWERCASE, hash );
201
202
      // The URI helps convert backslashes to forward slashes.
203
      final var uri = Path.of( TEMPORARY_DIRECTORY, filename ).toUri();
204
      final var svg = uri.getPath();
205
      final var link = context.resolveLink( LINK, svg, false );
206
      final var dimensions = getAttributes( node.getInfo() );
207
      final var r = format( R_SVG_EXPORT, svg, dimensions, text );
208
209
      mRChunkEvaluator.apply( r );
210
211
      return new Tuple<>( svg, link );
212
    }
213
214
    /**
215
     * Splits attributes of the form <code>{r key1=value2 key2=value2}</code>
216
     * into a comma-separated string containing only the key/value pairs,
217
     * such as <code>key1=value1,key2=value2</code>.
218
     *
219
     * @param bs The complete line after the fenced block demarcation.
220
     * @return A comma-separated string of name/value pairs.
221
     */
222
    private String getAttributes( final BasedSequence bs ) {
223
      final var result = new StringBuilder();
224
      final var split = bs.splitList( " " );
225
      final var splits = split.size();
226
227
      for( var i = 1; i < splits; i++ ) {
228
        final var based = split.get( i ).toString();
229
        final var attribute = based.replace( '}', ' ' );
230
231
        // The order of attribute evaluations is in order of performance.
232
        if( !attribute.isBlank() &&
233
          attribute.indexOf( '=' ) > 1 &&
234
          attribute.matches( ".*\\d.*" ) ) {
235
236
          // The comma will do double-duty for separating individual attributes
237
          // as well as being the comma that separates all attributes from the
238
          // SVG image file name.
239
          result.append( ',' ).append( attribute );
240
        }
241
      }
242
243
      return result.toString();
244
    }
245
246
    /**
247
     * This method is a stop-gap because blank lines that contain only
248
     * whitespace are collapsed into lines without any spaces. Consequently,
249
     * the typesetting software does not honour the blank lines, which
250
     * then would otherwise discard blank lines entirely.
251
     * <p>
252
     * Given the following:
253
     *
254
     * <pre>
255
     *   if( bool ) {
256
     *
257
     *
258
     *   }
259
     * </pre>
260
     * <p>
261
     * The typesetter would otherwise render this incorrectly as:
262
     *
263
     * <pre>
264
     *   if( bool ) {
265
     *   }
266
     * </pre>
267
     * <p>
268
     */
269
    private void render(
270
      final FencedCodeBlock node,
271
      final NodeRendererContext context,
272
      final HtmlWriter html ) {
273
      assert node != null;
274
      assert context != null;
275
      assert html != null;
276
277
      html.line();
278
      html.srcPosWithTrailingEOL( node.getChars() )
279
          .withAttr()
280
          .tag( "pre" )
281
          .openPre();
282
283
      final var options = context.getHtmlOptions();
284
      final var languageClass = lookupLanguageClass( node, options );
285
286
      if( !languageClass.isBlank() ) {
287
        html.attr( "class", languageClass );
288
      }
289
290
      html.srcPosWithEOL( node.getContentChars() )
291
          .withAttr( CODE_CONTENT )
292
          .tag( "code" );
293
294
      final var lines = node.getContentLines();
295
296
      for( final var line : lines ) {
297
        if( line.isBlank() ) {
298
          html.text( "    " );
299
        }
300
301
        html.text( line );
302
      }
303
304
      html.tag( "/code" );
305
      html.tag( "/pre" )
306
          .closePre();
307
      html.lineIf( options.htmlBlockCloseTagEol );
308
    }
309
310
    private String lookupLanguageClass(
311
      final FencedCodeBlock node,
312
      final HtmlRendererOptions options ) {
313
      assert node != null;
314
      assert options != null;
315
316
      final var info = node.getInfo();
317
318
      if( info.isNotNull() && !info.isBlank() ) {
319
        final var lang = node
320
          .getInfoDelimitedByAny( options.languageDelimiterSet )
321
          .unescape();
322
        return options
323
          .languageClassMap
324
          .getOrDefault( lang, options.languageClassPrefix + lang );
325
      }
326
327
      return options.noLanguageClass;
328
    }
329
  }
330
331
  private class Factory implements DelegatingNodeRendererFactory {
332
    public Factory() { }
333
334
    @NotNull
335
    @Override
336
    public NodeRenderer apply( @NotNull final DataHolder options ) {
337
      return new CustomRenderer();
338
    }
339
340
    /**
341
     * Return {@code null} to indicate this may delegate to the core renderer.
342
     */
343
    @Override
344
    public Set<Class<?>> getDelegates() {
345
      return null;
346
    }
347
  }
348
}
1349
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/blocks/ClosingDivBlock.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences.blocks;
3
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.util.sequence.BasedSequence;
6
7
/**
8
 * Responsible for helping to generate a closing {@code div} element.
9
 */
10
public final class ClosingDivBlock extends DivBlock {
11
  public ClosingDivBlock( final BasedSequence chars ) {
12
    super( chars );
13
  }
14
15
  @Override
16
  public void write( final HtmlWriter html ) {
17
    html.closeTag( HTML_DIV );
18
  }
19
}
120
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/blocks/DivBlock.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences.blocks;
3
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.util.ast.Block;
6
import com.vladsch.flexmark.util.sequence.BasedSequence;
7
import org.jetbrains.annotations.NotNull;
8
9
public abstract class DivBlock extends Block {
10
  static final CharSequence HTML_DIV = "div";
11
12
  public DivBlock( final BasedSequence chars ) {
13
    super( chars );
14
  }
15
16
  @Override
17
  @NotNull
18
  public BasedSequence[] getSegments() {
19
    return EMPTY_SEGMENTS;
20
  }
21
22
  /**
23
   * Append an opening or closing HTML div element to the given writer.
24
   *
25
   * @param html Builds the HTML document to be written.
26
   */
27
  public abstract void write( HtmlWriter html );
28
}
129
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/blocks/OpeningDivBlock.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences.blocks;
3
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.util.html.Attribute;
6
import com.vladsch.flexmark.util.sequence.BasedSequence;
7
8
import java.util.ArrayList;
9
import java.util.List;
10
11
/**
12
 * Responsible for helping to generate an opening {@code div} element.
13
 */
14
public final class OpeningDivBlock extends DivBlock {
15
  private final List<Attribute> mAttributes = new ArrayList<>();
16
17
  public OpeningDivBlock( final BasedSequence chars,
18
                          final List<Attribute> attrs ) {
19
    super( chars );
20
    assert attrs != null;
21
    mAttributes.addAll( attrs );
22
  }
23
24
  @Override
25
  public void write( final HtmlWriter html ) {
26
    mAttributes.forEach( html::attr );
27
    html.withAttr().tag( HTML_DIV );
28
  }
29
}
130
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/factories/CustomDivBlockParserFactory.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.factories;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownCustomBlockParserFactory;
8
import com.vladsch.flexmark.parser.block.BlockParserFactory;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
11
/**
12
 * Responsible for creating an instance of {@link DivBlockParserFactory}.
13
 */
14
public class CustomDivBlockParserFactory
15
  extends MarkdownCustomBlockParserFactory {
16
  @Override
17
  public BlockParserFactory createBlockParserFactory(
18
    final DataHolder options ) {
19
    return new DivBlockParserFactory( options );
20
  }
21
}
122
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/factories/DivBlockParserFactory.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.factories;
6
7
import com.keenwrite.processors.markdown.extensions.fences.parsers.ClosingParser;
8
import com.keenwrite.processors.markdown.extensions.fences.parsers.OpeningParser;
9
import com.vladsch.flexmark.parser.block.AbstractBlockParserFactory;
10
import com.vladsch.flexmark.parser.block.BlockStart;
11
import com.vladsch.flexmark.parser.block.MatchedBlockParser;
12
import com.vladsch.flexmark.parser.block.ParserState;
13
import com.vladsch.flexmark.util.data.DataHolder;
14
15
import static com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension.FENCE;
16
import static com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension.FENCE_OPENING;
17
18
/**
19
 * Responsible for creating a fenced div parser that is appropriate for the
20
 * type of fenced div encountered: opening or closing.
21
 */
22
public class DivBlockParserFactory extends AbstractBlockParserFactory {
23
  public DivBlockParserFactory( final DataHolder options ) {
24
    super( options );
25
  }
26
27
  /**
28
   * Try to match an opening or closing fenced div.
29
   *
30
   * @param state       Block parser state.
31
   * @param blockParser Last matched open block parser.
32
   * @return Wrapper for the opening or closing parser, upon finding :::.
33
   */
34
  @Override
35
  public BlockStart tryStart(
36
    final ParserState state, final MatchedBlockParser blockParser ) {
37
    return
38
      state.getIndent() == 0 && FENCE.matcher( state.getLine() ).matches()
39
        ? parseFence( state )
40
        : BlockStart.none();
41
  }
42
43
  /**
44
   * After finding a fenced div, this will further disambiguate an opening
45
   * from a closing fence.
46
   *
47
   * @param state Block parser state, contains line to parse.
48
   * @return Wrapper for the opening or closing parser, upon finding :::.
49
   */
50
  private BlockStart parseFence( final ParserState state ) {
51
    final var line = state.getLine();
52
    final var fence = FENCE_OPENING.matcher( line.trim() );
53
54
    return BlockStart.of(
55
      fence.matches()
56
        ? new OpeningParser( fence.group( 1 ) )
57
        : new ClosingParser( line )
58
    ).atIndex( state.getIndex() );
59
  }
60
}
161
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/factories/FencedDivNodeRendererFactory.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences.factories;
3
4
import com.keenwrite.processors.markdown.extensions.common.MarkdownNodeRendererFactory;
5
import com.keenwrite.processors.markdown.extensions.fences.renderers.FencedDivRenderer;
6
import com.vladsch.flexmark.html.renderer.NodeRenderer;
7
import com.vladsch.flexmark.util.data.DataHolder;
8
9
public class FencedDivNodeRendererFactory extends MarkdownNodeRendererFactory {
10
  @Override
11
  protected NodeRenderer createNodeRenderer( final DataHolder options ) {
12
    return new FencedDivRenderer();
13
  }
14
}
115
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/factories/FencedDivPreProcessorFactory.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.factories;
6
7
import com.keenwrite.processors.markdown.extensions.fences.processors.FencedDivParagraphPreProcessor;
8
import com.vladsch.flexmark.parser.block.ParagraphPreProcessor;
9
import com.vladsch.flexmark.parser.block.ParagraphPreProcessorFactory;
10
import com.vladsch.flexmark.parser.block.ParserState;
11
import com.vladsch.flexmark.parser.core.ReferencePreProcessorFactory;
12
import org.jetbrains.annotations.Nullable;
13
14
import java.util.Set;
15
16
public class FencedDivPreProcessorFactory
17
  implements ParagraphPreProcessorFactory {
18
19
  @Override
20
  public ParagraphPreProcessor apply( final ParserState state ) {
21
    return new FencedDivParagraphPreProcessor( state.getProperties() );
22
  }
23
24
  @Override
25
  public @Nullable Set<Class<?>> getBeforeDependents() {
26
    return Set.of();
27
  }
28
29
  @Override
30
  public @Nullable Set<Class<?>> getAfterDependents() {
31
    return Set.of( ReferencePreProcessorFactory.class );
32
  }
33
34
  @Override
35
  public boolean affectsGlobalScope() {
36
    return false;
37
  }
38
39
}
140
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/parsers/ClosingParser.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.parsers;
6
7
import com.keenwrite.processors.markdown.extensions.fences.blocks.ClosingDivBlock;
8
import com.vladsch.flexmark.util.ast.Block;
9
import com.vladsch.flexmark.util.sequence.BasedSequence;
10
11
/**
12
 * Responsible for creating an instance of {@link ClosingDivBlock}.
13
 */
14
public class ClosingParser extends DivBlockParser {
15
  private final ClosingDivBlock mBlock;
16
17
  public ClosingParser( final BasedSequence line ) {
18
    mBlock = new ClosingDivBlock( line );
19
  }
20
21
  @Override
22
  public Block getBlock() {
23
    return mBlock;
24
  }
25
}
126
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/parsers/DivBlockParser.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.parsers;
6
7
import com.vladsch.flexmark.parser.block.AbstractBlockParser;
8
import com.vladsch.flexmark.parser.block.BlockContinue;
9
import com.vladsch.flexmark.parser.block.ParserState;
10
11
/**
12
 * Abstracts common {@link OpeningParser} and
13
 * {@link ClosingParser} methods.
14
 */
15
public abstract class DivBlockParser extends AbstractBlockParser {
16
  @Override
17
  public BlockContinue tryContinue( final ParserState state ) {
18
    return BlockContinue.none();
19
  }
20
21
  @Override
22
  public void closeBlock( final ParserState state ) {}
23
}
124
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/parsers/OpeningParser.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.parsers;
6
7
import com.keenwrite.processors.markdown.extensions.fences.blocks.OpeningDivBlock;
8
import com.vladsch.flexmark.util.ast.Block;
9
import com.vladsch.flexmark.util.html.Attribute;
10
import com.vladsch.flexmark.util.html.AttributeImpl;
11
import com.vladsch.flexmark.util.sequence.BasedSequence;
12
13
import java.util.ArrayList;
14
import java.util.regex.Pattern;
15
16
import static java.lang.String.format;
17
import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS;
18
import static java.util.regex.Pattern.compile;
19
20
/**
21
 * Responsible for creating an instance of {@link OpeningDivBlock}.
22
 */
23
public class OpeningParser extends DivBlockParser {
24
  /**
25
   * Matches either individual CSS definitions (id/class, {@code <d>}) or
26
   * key/value pairs ({@code <k>} and {@link <v>}). The key/value pair
27
   * will match optional quotes.
28
   */
29
  private static final Pattern ATTR_PAIRS = compile(
30
    "\\s*" +
31
    "(?<d>[#.][\\p{Alnum}\\-_]+[^\\s=])|" +
32
    "((?<k>[\\p{Alnum}\\-_]+)=" +
33
    "\"*(?<v>(?<=\")[^\"]+(?=\")|(\\S+))\"*)",
34
    UNICODE_CHARACTER_CLASS );
35
36
  /**
37
   * Matches whether extended syntax is being used.
38
   */
39
  private static final Pattern ATTR_CSS = compile( "\\{(.+)}" );
40
41
  private final OpeningDivBlock mBlock;
42
43
  /**
44
   * Parses the arguments upon construction.
45
   *
46
   * @param args Text after :::, excluding leading/trailing whitespace.
47
   */
48
  public OpeningParser( final String args ) {
49
    final var attrs = new ArrayList<Attribute>();
50
    final var cssMatcher = ATTR_CSS.matcher( args );
51
52
    if( cssMatcher.matches() ) {
53
      // Split the text between braces into tokens and/or key-value pairs.
54
      final var pairMatcher =
55
        ATTR_PAIRS.matcher( cssMatcher.group( 1 ) );
56
57
      while( pairMatcher.find() ) {
58
        final var cssDef = pairMatcher.group( "d" );
59
        String cssAttrKey = "class";
60
        final String cssAttrVal;
61
62
        // When no regular CSS definition (id or class), use key/value pairs.
63
        if( cssDef == null ) {
64
          cssAttrKey = format( "data-%s", pairMatcher.group( "k" ) );
65
          cssAttrVal = pairMatcher.group( "v" );
66
        }
67
        else {
68
          // This will strip the "#" and "." off the start of CSS definition.
69
          var index = 1;
70
71
          // Default CSS attribute name is "class", switch to "id" for #.
72
          if( cssDef.startsWith( "#" ) ) {
73
            cssAttrKey = "id";
74
          }
75
          else if( !cssDef.startsWith( "." ) ) {
76
            index = 0;
77
          }
78
79
          cssAttrVal = cssDef.substring( index );
80
        }
81
82
        attrs.add( AttributeImpl.of( cssAttrKey, cssAttrVal ) );
83
      }
84
    }
85
    else {
86
      attrs.add( AttributeImpl.of( "class", args ) );
87
    }
88
89
    final var chars = BasedSequence.of( args );
90
91
    mBlock = new OpeningDivBlock( chars, attrs );
92
  }
93
94
  @Override
95
  public Block getBlock() {
96
    return mBlock;
97
  }
98
}
199
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/processors/FencedDivParagraphPreProcessor.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.processors;
6
7
import com.keenwrite.processors.markdown.extensions.fences.parsers.ClosingParser;
8
import com.keenwrite.processors.markdown.extensions.fences.parsers.OpeningParser;
9
import com.vladsch.flexmark.ast.Paragraph;
10
import com.vladsch.flexmark.parser.block.ParagraphPreProcessor;
11
import com.vladsch.flexmark.parser.block.ParserState;
12
import com.vladsch.flexmark.util.data.MutableDataHolder;
13
import com.vladsch.flexmark.util.sequence.BasedSequence;
14
15
import java.util.ArrayList;
16
17
import static com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension.FENCE;
18
import static com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension.FENCE_OPENING;
19
20
public class FencedDivParagraphPreProcessor
21
  implements ParagraphPreProcessor {
22
  public FencedDivParagraphPreProcessor( final MutableDataHolder unused ) {}
23
24
  @Override
25
  public int preProcessBlock( final Paragraph block,
26
                              final ParserState state ) {
27
    final var lines = block.getContentLines();
28
29
    // Stores lines matching opening or closing fenced div sigil.
30
    final var sigilLines = new ArrayList<BasedSequence>();
31
32
    for( final var line : lines ) {
33
      // Seeks a :::+ sigil.
34
      final var fence = FENCE.matcher( line );
35
36
      if( fence.find() ) {
37
        // Attributes after the fence are required to detect an open fence.
38
        final var attrs = FENCE_OPENING.matcher( line.trim() );
39
        final var match = attrs.matches();
40
41
        final var parser = match
42
          ? new OpeningParser( attrs.group( 1 ) )
43
          : new ClosingParser( line );
44
        final var divBlock = parser.getBlock();
45
46
        if( match ) {
47
          block.insertBefore( divBlock );
48
        }
49
        else {
50
          block.insertAfter( divBlock );
51
        }
52
53
        state.blockAdded( divBlock );
54
55
        // Schedule the line for removal (because it has been handled).
56
        sigilLines.add( line );
57
      }
58
    }
59
60
    sigilLines.forEach( lines::remove );
61
62
    return 0;
63
  }
64
}
165
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/renderers/FencedDivRenderer.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences.renderers;
3
4
import com.keenwrite.processors.markdown.extensions.fences.blocks.ClosingDivBlock;
5
import com.keenwrite.processors.markdown.extensions.fences.blocks.DivBlock;
6
import com.keenwrite.processors.markdown.extensions.fences.blocks.OpeningDivBlock;
7
import com.vladsch.flexmark.html.HtmlWriter;
8
import com.vladsch.flexmark.html.renderer.NodeRenderer;
9
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
10
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
11
import org.jetbrains.annotations.Nullable;
12
13
import java.util.Set;
14
15
/**
16
 * Responsible for rendering opening and closing fenced div blocks as HTML
17
 * {@code div} elements.
18
 */
19
public class FencedDivRenderer implements NodeRenderer {
20
  @Nullable
21
  @Override
22
  public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
23
    return Set.of(
24
      new NodeRenderingHandler<>( OpeningDivBlock.class, this::render ),
25
      new NodeRenderingHandler<>( ClosingDivBlock.class, this::render )
26
    );
27
  }
28
29
  /**
30
   * Renders the fenced div block as an HTML {@code <div></div>} element.
31
   */
32
  void render(
33
    final DivBlock node,
34
    final NodeRendererContext context,
35
    final HtmlWriter html ) {
36
    node.write( html );
37
  }
38
}
139
A src/main/java/com/keenwrite/processors/markdown/extensions/images/ImageLinkExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.images;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.processors.ProcessorContext;
6
import com.keenwrite.processors.markdown.extensions.common.HtmlRendererAdapter;
7
import com.vladsch.flexmark.ast.Image;
8
import com.vladsch.flexmark.html.IndependentLinkResolverFactory;
9
import com.vladsch.flexmark.html.LinkResolver;
10
import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
11
import com.vladsch.flexmark.html.renderer.ResolvedLink;
12
import com.vladsch.flexmark.util.ast.Node;
13
import org.jetbrains.annotations.NotNull;
14
15
import java.io.File;
16
import java.nio.file.Path;
17
import java.util.Optional;
18
19
import static com.keenwrite.events.StatusEvent.clue;
20
import static com.keenwrite.io.SysFile.toFile;
21
import static com.keenwrite.util.ProtocolScheme.getProtocol;
22
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
23
import static com.vladsch.flexmark.html.renderer.LinkStatus.VALID;
24
25
/**
26
 * Responsible for ensuring that images can be rendered relative to a path.
27
 * This allows images to be located virtually anywhere.
28
 */
29
public class ImageLinkExtension extends HtmlRendererAdapter {
30
31
  private final ProcessorContext mContext;
32
33
  private ImageLinkExtension( @NotNull final ProcessorContext context ) {
34
    mContext = context;
35
  }
36
37
  /**
38
   * Creates an extension capable of using a relative path to embed images.
39
   *
40
   * @param context Contains the base directory to search in for images.
41
   * @return The new {@link ImageLinkExtension}, not {@code null}.
42
   */
43
  public static ImageLinkExtension create(
44
    @NotNull final ProcessorContext context ) {
45
    return new ImageLinkExtension( context );
46
  }
47
48
  @Override
49
  public void extend(
50
    @NotNull final Builder builder,
51
    @NotNull final String rendererType ) {
52
    builder.linkResolverFactory( new ResolverFactory() );
53
  }
54
55
  private final class ResolverFactory extends IndependentLinkResolverFactory {
56
    @NotNull
57
    @Override
58
    public LinkResolver apply(
59
      @NotNull final LinkResolverBasicContext context ) {
60
      return new ImageLinkResolver();
61
    }
62
  }
63
64
  private final class ImageLinkResolver implements LinkResolver {
65
    private ImageLinkResolver() {}
66
67
    @NotNull
68
    @Override
69
    public ResolvedLink resolveLink(
70
      @NotNull final Node node,
71
      @NotNull final LinkResolverBasicContext context,
72
      @NotNull final ResolvedLink link ) {
73
      return node instanceof Image ? forImage( link, node ) : link;
74
    }
75
76
    /**
77
     * Algorithm:
78
     * <ol>
79
     *   <li>Accept remote URLs as valid links.</li>
80
     *   <li>Accept existing readable files as valid links.</li>
81
     *   <li>Accept non-{@link ExportFormat#NONE} exports as valid links.</li>
82
     *   <li>Append the images dir to the edited file's dir (baseDir).</li>
83
     *   <li>Search for images by extension.</li>
84
     * </ol>
85
     *
86
     * @param link The link URL to resolve.
87
     * @param node The document node containing the URL.
88
     * @return The {@link ResolvedLink} instance used to render the link.
89
     */
90
    private ResolvedLink forImage( final ResolvedLink link, final Node node ) {
91
      final var url = link.getUrl();
92
      final var protocolScheme = getProtocol( url );
93
94
      return protocolScheme.isRemote()
95
        ? valid( link, url )
96
        : resolveImageFile( link, node, url );
97
    }
98
99
    private ResolvedLink resolveImageFile(
100
      final ResolvedLink link,
101
      final Node node,
102
      final String url ) {
103
      final var userPath = new File( url );
104
105
      // If the user specified a fully qualified path name, use it verbatim.
106
      return readable( userPath )
107
        ? valid( link, url )
108
        : resolveUnqualifiedImageFile( link, node, url );
109
    }
110
111
    private ResolvedLink resolveUnqualifiedImageFile(
112
      final ResolvedLink link,
113
      final Node node,
114
      final String url ) {
115
      final var baseDir = getBaseDir();
116
      final var fqfn = baseDir.resolve( Path.of( url ) );
117
118
      // If the image can be found relative to the base directory, then
119
      // use the link as is when resolving the path.
120
      return readable( toFile( fqfn ) )
121
        ? valid( link, url )
122
        : resolveExtensionlessImageFile( link, node, url );
123
    }
124
125
    private ResolvedLink resolveExtensionlessImageFile(
126
      final ResolvedLink link,
127
      final Node node,
128
      final String url
129
    ) {
130
      final var imagePath = new File( url );
131
      final var file = resolveImageExtension( imagePath );
132
133
      return file.isPresent() && readable( file.get() )
134
        ? valid( link, file.get().toString() )
135
        : resolveRelativeImageFile( link, node, url );
136
    }
137
138
    private ResolvedLink resolveRelativeImageFile(
139
      final ResolvedLink link,
140
      final Node node,
141
      final String url ) {
142
      final var baseDir = getBaseDir();
143
144
      try {
145
        // Compute the path to the image file. The base directory should
146
        // be an absolute path to the file being edited, without an extension.
147
        final var imagesDir = getImageDir();
148
        final var baseImagesDir = baseDir.resolve( imagesDir );
149
        final var imagePath = baseImagesDir.resolve( url );
150
        final var file = resolveImageExtension( toFile( imagePath ) );
151
152
        if( file.isPresent() ) {
153
          final var resolved = imagesDir.resolve( file.get().toPath() );
154
          final var relative = baseDir.relativize( resolved );
155
          return valid( link, relative.toString() );
156
        }
157
158
        clue( "Main.status.error.file.missing.near",
159
              imagePath + ".*", node.getLineNumber()
160
        );
161
      } catch( final Exception ex ) {
162
        clue( ex );
163
      }
164
165
      return link;
166
    }
167
168
    private Optional<File> resolveImageExtension( final File imagePath ) {
169
      for( final var ext : getImageOrder() ) {
170
        final var file = new File( imagePath.toString() + '.' + ext );
171
172
        if( readable( file ) ) {
173
          return Optional.of( file );
174
        }
175
      }
176
177
      return Optional.empty();
178
    }
179
180
    private ResolvedLink valid( final ResolvedLink link, final String url ) {
181
      return link.withStatus( VALID ).withUrl( url );
182
    }
183
184
    private Path getImageDir() {
185
      return mContext.getImageDir();
186
    }
187
188
    private Iterable<String> getImageOrder() {
189
      return mContext.getImageOrder();
190
    }
191
192
    private Path getBaseDir() {
193
      return mContext.getBaseDir();
194
    }
195
  }
196
197
  private static boolean readable( final File file ) {
198
    return file.isFile() && file.canRead();
199
  }
200
}
1201
A src/main/java/com/keenwrite/processors/markdown/extensions/outline/DocumentOutlineExtension.java
1
package com.keenwrite.processors.markdown.extensions.outline;
2
3
import com.keenwrite.events.ParseHeadingEvent;
4
import com.keenwrite.processors.Processor;
5
import com.keenwrite.processors.markdown.extensions.common.MarkdownParserExtension;
6
import com.vladsch.flexmark.ast.Heading;
7
import com.vladsch.flexmark.parser.Parser.Builder;
8
import com.vladsch.flexmark.parser.block.NodePostProcessor;
9
import com.vladsch.flexmark.parser.block.NodePostProcessorFactory;
10
import com.vladsch.flexmark.util.ast.Document;
11
import com.vladsch.flexmark.util.ast.Node;
12
import com.vladsch.flexmark.util.ast.NodeTracker;
13
import org.jetbrains.annotations.NotNull;
14
15
import java.util.regex.Pattern;
16
17
import static com.keenwrite.events.ParseHeadingEvent.fireNewOutlineEvent;
18
19
public final class DocumentOutlineExtension implements MarkdownParserExtension {
20
  private static final Pattern REGEX = Pattern.compile( "^(#+)" );
21
22
  private final Processor<String> mProcessor;
23
24
  private DocumentOutlineExtension( final Processor<String> processor ) {
25
    mProcessor = processor;
26
  }
27
28
  @Override
29
  public void extend( final Builder builder ) {
30
    builder.postProcessorFactory( new Factory() );
31
  }
32
33
  public static DocumentOutlineExtension create(
34
    final Processor<String> processor ) {
35
    return new DocumentOutlineExtension( processor );
36
  }
37
38
  private class HeadingNodePostProcessor extends NodePostProcessor {
39
    @Override
40
    public void process(
41
      @NotNull final NodeTracker state, @NotNull final Node node ) {
42
      final var heading = mProcessor.apply( node.getChars().toString() );
43
      final var matcher = REGEX.matcher( heading );
44
45
      if( matcher.find() ) {
46
        final var level = matcher.group().length();
47
        final var text = heading.substring( level );
48
        final var offset = node.getStartOffset();
49
        ParseHeadingEvent.fire( level, text, offset );
50
      }
51
    }
52
  }
53
54
  public class Factory extends NodePostProcessorFactory {
55
    public Factory() {
56
      super( false );
57
      addNodes( Heading.class );
58
    }
59
60
    @NotNull
61
    @Override
62
    public NodePostProcessor apply( @NotNull final Document document ) {
63
      fireNewOutlineEvent();
64
      return new HeadingNodePostProcessor();
65
    }
66
  }
67
}
168
A src/main/java/com/keenwrite/processors/markdown/extensions/quotes/EscapedQuoteNodeRenderer.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.quotes;
6
7
import com.vladsch.flexmark.ast.Text;
8
import com.vladsch.flexmark.html.HtmlWriter;
9
import com.vladsch.flexmark.html.renderer.NodeRenderer;
10
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
11
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
12
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
13
import com.vladsch.flexmark.util.data.DataHolder;
14
import org.jetbrains.annotations.NotNull;
15
16
import java.util.HashSet;
17
import java.util.Set;
18
19
/**
20
 * Responsible for preventing escaped quotes from being converted to regular
21
 * quotes.
22
 */
23
public class EscapedQuoteNodeRenderer implements NodeRenderer {
24
  public static class Factory implements NodeRendererFactory {
25
    @Override
26
    public @NotNull NodeRenderer apply( @NotNull DataHolder options ) {
27
      return new EscapedQuoteNodeRenderer();
28
    }
29
  }
30
31
  @Override
32
  public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
33
    final var handlers = new HashSet<NodeRenderingHandler<?>>();
34
35
    handlers.add( new NodeRenderingHandler<>( Text.class, this::renderText ) );
36
37
    return handlers;
38
  }
39
40
  private void renderText(
41
    final Text node,
42
    final NodeRendererContext context,
43
    final HtmlWriter html ) {
44
    // By default, rendering will unescape escaped quotation marks. Overriding
45
    // how text is produced with the verbatim characters ensures the escape
46
    // sequence is retained (i.e., \").
47
    html.text( node.getChars().toString() );
48
  }
49
}
150
A src/main/java/com/keenwrite/processors/markdown/extensions/quotes/EscapedQuotesExtension.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.quotes;
6
7
import com.vladsch.flexmark.util.data.MutableDataHolder;
8
import org.jetbrains.annotations.NotNull;
9
10
import static com.keenwrite.processors.markdown.extensions.quotes.EscapedQuoteNodeRenderer.Factory;
11
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
12
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
13
14
/**
15
 * Responsible for extending the Markdown library with the ability to
16
 * retain escaped characters, rather than unescape the text. This is needed
17
 * so that the character sequence {@code \"} can be interpreted as a straight
18
 * quotation mark when output into the final document.
19
 */
20
public class EscapedQuotesExtension
21
  implements HtmlRendererExtension {
22
23
  @Override
24
  public void rendererOptions( @NotNull final MutableDataHolder options ) {}
25
26
  @Override
27
  public void extend(
28
    final Builder builder,
29
    @NotNull final String rendererType ) {
30
    builder.nodeRendererFactory( new Factory() );
31
  }
32
33
  public static EscapedQuotesExtension create() {
34
    return new EscapedQuotesExtension();
35
  }
36
}
137
A src/main/java/com/keenwrite/processors/markdown/extensions/r/RInlineExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.r;
3
4
import com.keenwrite.processors.Processor;
5
import com.keenwrite.processors.ProcessorContext;
6
import com.keenwrite.processors.markdown.BaseMarkdownProcessor;
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownParserExtension;
8
import com.keenwrite.processors.r.RInlineEvaluator;
9
import com.vladsch.flexmark.ast.Paragraph;
10
import com.vladsch.flexmark.parser.InlineParserExtensionFactory;
11
import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
12
import com.vladsch.flexmark.parser.internal.InlineParserImpl;
13
import com.vladsch.flexmark.parser.internal.LinkRefProcessorData;
14
import com.vladsch.flexmark.util.data.DataHolder;
15
16
import java.util.BitSet;
17
import java.util.List;
18
import java.util.Map;
19
20
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
21
import static com.vladsch.flexmark.parser.Parser.Builder;
22
23
/**
24
 * Responsible for processing inline R statements (denoted using the
25
 * {@link RInlineEvaluator#PREFIX}) to prevent them from being converted to
26
 * HTML {@code <code>} elements and stop them from interfering with TeX
27
 * statements. Note that TeX statements are processed using a Markdown
28
 * extension, rather than an implementation of {@link Processor}. For this
29
 * reason, some pre-conversion is necessary.
30
 */
31
public final class RInlineExtension implements MarkdownParserExtension {
32
  private final RInlineEvaluator mEvaluator;
33
  private final BaseMarkdownProcessor mMarkdownProcessor;
34
35
  private RInlineExtension(
36
    final RInlineEvaluator evaluator,
37
    final ProcessorContext context ) {
38
    mEvaluator = evaluator;
39
    mMarkdownProcessor = new BaseMarkdownProcessor( IDENTITY, context );
40
  }
41
42
  /**
43
   * Creates an extension capable of intercepting R code blocks and preventing
44
   * them from being converted into HTML {@code <code>} elements.
45
   */
46
  public static RInlineExtension create(
47
    final RInlineEvaluator evaluator,
48
    final ProcessorContext context ) {
49
    return new RInlineExtension( evaluator, context );
50
  }
51
52
  @Override
53
  public void extend( final Builder builder ) {
54
    builder.customInlineParserFactory( InlineParser::new );
55
  }
56
57
  /**
58
   * Prevents rendering {@code `r} statements as inline HTML {@code <code>}
59
   * blocks, which allows the {@link RInlineEvaluator} to post-process the
60
   * text prior to display in the preview pane. This intervention assists
61
   * with decoupling the caret from the Markdown content so that the two
62
   * can vary independently in the architecture while permitting synchronization
63
   * of the editor and preview pane.
64
   * <p>
65
   * The text is therefore processed twice: once by flexmark-java and once by
66
   * {@link RInlineEvaluator}.
67
   * </p>
68
   */
69
  private final class InlineParser extends InlineParserImpl {
70
    private InlineParser(
71
      final DataHolder options,
72
      final BitSet specialCharacters,
73
      final BitSet delimiterCharacters,
74
      final Map<Character, DelimiterProcessor> delimiterProcessors,
75
      final LinkRefProcessorData referenceLinkProcessors,
76
      final List<InlineParserExtensionFactory> inlineParserExtensions ) {
77
      super(
78
        options,
79
        specialCharacters,
80
        delimiterCharacters,
81
        delimiterProcessors,
82
        referenceLinkProcessors,
83
        inlineParserExtensions
84
      );
85
    }
86
87
    /**
88
     * The superclass handles a number backtick parsing edge cases; this method
89
     * changes the behaviour to retain R code snippets, identified by
90
     * {@link RInlineEvaluator#PREFIX}, so that subsequent processing can
91
     * invoke R. If other languages are added, the {@link InlineParser} will
92
     * have to be rewritten to identify more than merely R.
93
     *
94
     * @return The return value from {@link super#parseBackticks()}.
95
     * @inheritDoc
96
     */
97
    @Override
98
    protected boolean parseBackticks() {
99
      final var foundTicks = super.parseBackticks();
100
101
      if( foundTicks ) {
102
        final var blockNode = getBlock();
103
        final var codeNode = blockNode.getLastChild();
104
105
        if( codeNode != null ) {
106
          final var code = codeNode.getChars().toString();
107
108
          if( mEvaluator.test( code ) ) {
109
            codeNode.unlink();
110
111
            final var rText = mEvaluator.apply( code );
112
            var node = mMarkdownProcessor.toNode( rText );
113
114
            if( node.getFirstChild() instanceof Paragraph paragraph ) {
115
              node = paragraph.getFirstChild();
116
            }
117
118
            if( node != null ) {
119
              blockNode.appendChild( node );
120
            }
121
          }
122
        }
123
      }
124
125
      return foundTicks;
126
    }
127
  }
128
}
1129
A src/main/java/com/keenwrite/processors/markdown/extensions/references/AnchorNameDelimiterProcessor.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.parser.InlineParser;
8
import com.vladsch.flexmark.parser.core.delimiter.Delimiter;
9
import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
10
import com.vladsch.flexmark.parser.delimiter.DelimiterRun;
11
import com.vladsch.flexmark.util.ast.Node;
12
13
import static com.keenwrite.constants.Constants.DEF_DELIM_BEGAN_DEFAULT;
14
15
/**
16
 * Responsible for processing {@code {@type:id}} anchors.
17
 */
18
class AnchorNameDelimiterProcessor implements DelimiterProcessor {
19
20
  @Override
21
  public void process(
22
    final Delimiter opener,
23
    final Delimiter closer,
24
    final int delimitersUsed ) {
25
    final var node = new AnchorNameNode();
26
    opener.moveNodesBetweenDelimitersTo( node, closer );
27
  }
28
29
  @Override
30
  public char getOpeningCharacter() {
31
    return '{';
32
  }
33
34
  @Override
35
  public char getClosingCharacter() {
36
    return '}';
37
  }
38
39
  @Override
40
  public int getMinLength() {
41
    return 1;
42
  }
43
44
  @Override
45
  public int getDelimiterUse(
46
    final DelimiterRun opener,
47
    final DelimiterRun closer ) {
48
    final var text = opener.getNode();
49
50
    // Ensure that the default delimiters are respected (not clobbered by
51
    // transforming them into anchor links).
52
    return text.getChars().toString().equals( DEF_DELIM_BEGAN_DEFAULT ) ? 0 : 1;
53
  }
54
55
  @Override
56
  public Node unmatchedDelimiterNode(
57
    final InlineParser inlineParser,
58
    final DelimiterRun delimiter ) {
59
    return null;
60
  }
61
62
  @Override
63
  public boolean canBeOpener(
64
    final String before,
65
    final String after,
66
    final boolean leftFlanking,
67
    final boolean rightFlanking,
68
    final boolean beforeIsPunctuation,
69
    final boolean afterIsPunctuation,
70
    final boolean beforeIsWhitespace,
71
    final boolean afterIsWhiteSpace ) {
72
    return leftFlanking;
73
  }
74
75
  @Override
76
  public boolean canBeCloser(
77
    final String before,
78
    final String after,
79
    final boolean leftFlanking,
80
    final boolean rightFlanking,
81
    final boolean beforeIsPunctuation,
82
    final boolean afterIsPunctuation,
83
    final boolean beforeIsWhitespace,
84
    final boolean afterIsWhiteSpace ) {
85
    return rightFlanking;
86
  }
87
88
  @Override
89
  public boolean skipNonOpenerCloser() {
90
    return false;
91
  }
92
}
193
A src/main/java/com/keenwrite/processors/markdown/extensions/references/AnchorNameNode.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.ast.DelimitedNodeImpl;
8
import com.vladsch.flexmark.util.sequence.BasedSequence;
9
import org.jetbrains.annotations.NotNull;
10
11
/**
12
 * Responsible for writing HTML anchor names in the form
13
 * {@code <a data-type="..." name="name" />}, where {@code name} can be
14
 * referred to by a cross-reference.
15
 *
16
 * @see AnchorXrefNode
17
 */
18
public class AnchorNameNode extends DelimitedNodeImpl implements CrossReferenceNode {
19
20
  private BasedSequence mOpeningMarker = BasedSequence.EMPTY;
21
  private BasedSequence mClosingMarker = BasedSequence.EMPTY;
22
23
  private BasedSequenceNameParser mParser;
24
25
  public AnchorNameNode() {}
26
27
  @Override
28
  public String getTypeName() {
29
    return mParser.getTypeName();
30
  }
31
32
  @Override
33
  public String getIdName() {
34
    return mParser.getIdName();
35
  }
36
37
  @Override
38
  public String getRefAttrName() {
39
    return "name";
40
  }
41
42
  @Override
43
  public BasedSequence getOpeningMarker() {
44
    return mOpeningMarker;
45
  }
46
47
  @NotNull
48
  @Override
49
  public BasedSequence getChars() {
50
    return BasedSequence.EMPTY;
51
  }
52
53
  @Override
54
  public void setOpeningMarker( final BasedSequence openingMarker ) {
55
    mOpeningMarker = openingMarker;
56
  }
57
58
  @Override
59
  public BasedSequence getText() {
60
    return BasedSequence.EMPTY;
61
  }
62
63
  @Override
64
  public void setText( final BasedSequence text ) {
65
    mParser = BasedSequenceNameParser.parse( text );
66
  }
67
68
  @Override
69
  public BasedSequence getClosingMarker() {
70
    return mClosingMarker;
71
  }
72
73
  @Override
74
  public void setClosingMarker( final BasedSequence closingMarker ) {
75
    mClosingMarker = closingMarker;
76
  }
77
}
178
A src/main/java/com/keenwrite/processors/markdown/extensions/references/AnchorXrefNode.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.util.ast.Node;
8
import com.vladsch.flexmark.util.sequence.BasedSequence;
9
import org.jetbrains.annotations.NotNull;
10
11
import static java.lang.String.format;
12
13
/**
14
 * Responsible for writing HTML anchor cross-references in the form
15
 * {@code <a data-type="..." href="#name" />} where {@code name} refers
16
 * to an anchor name.
17
 *
18
 * @see AnchorNameNode
19
 */
20
public class AnchorXrefNode extends Node implements CrossReferenceNode {
21
  private final String mTypeName;
22
  private final String mIdName;
23
24
  AnchorXrefNode( final String type, final String id ) {
25
    mTypeName = type;
26
    mIdName = format( "#%s", id );
27
  }
28
29
  @Override
30
  public String getTypeName() {
31
    return mTypeName;
32
  }
33
34
  @Override
35
  public String getIdName() {
36
    return mIdName;
37
  }
38
39
  @Override
40
  public String getRefAttrName() {
41
    return "href";
42
  }
43
44
  @NotNull
45
  @Override
46
  public BasedSequence[] getSegments() {
47
    return BasedSequence.EMPTY_SEGMENTS;
48
  }
49
}
150
A src/main/java/com/keenwrite/processors/markdown/extensions/references/AnchorXrefProcessorFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.parser.LinkRefProcessor;
8
import com.vladsch.flexmark.parser.LinkRefProcessorFactory;
9
import com.vladsch.flexmark.util.ast.Document;
10
import com.vladsch.flexmark.util.ast.Node;
11
import com.vladsch.flexmark.util.data.DataHolder;
12
import com.vladsch.flexmark.util.sequence.BasedSequence;
13
import org.jetbrains.annotations.NotNull;
14
15
/**
16
 * Responsible for processing {@code [@type:id]} anchors.
17
 */
18
class AnchorXrefProcessorFactory implements LinkRefProcessorFactory {
19
  private final LinkRefProcessor mProcessor = new AnchorLinkRefProcessor();
20
21
  @Override
22
  public boolean getWantExclamationPrefix( @NotNull final DataHolder options ) {
23
    return false;
24
  }
25
26
  @Override
27
  public int getBracketNestingLevel( @NotNull final DataHolder options ) {
28
    return 0;
29
  }
30
31
  @NotNull
32
  @Override
33
  public LinkRefProcessor apply( @NotNull final Document document ) {
34
    return mProcessor;
35
  }
36
37
  private static class AnchorLinkRefProcessor implements LinkRefProcessor {
38
39
    @Override
40
    public boolean getWantExclamationPrefix() {
41
      return false;
42
    }
43
44
    @Override
45
    public int getBracketNestingLevel() {
46
      return 0;
47
    }
48
49
    @Override
50
    public boolean isMatch( @NotNull final BasedSequence nodeChars ) {
51
      return nodeChars.indexOf( '@' ) == 1;
52
    }
53
54
    @NotNull
55
    @Override
56
    public Node createNode( @NotNull final BasedSequence nodeChars ) {
57
      return BasedSequenceXrefParser.parse( nodeChars ).toNode();
58
    }
59
60
    @NotNull
61
    @Override
62
    public BasedSequence adjustInlineText(
63
      @NotNull final Document document,
64
      @NotNull final Node node ) {
65
      return BasedSequence.EMPTY;
66
    }
67
68
    @Override
69
    public boolean allowDelimiters(
70
      @NotNull final BasedSequence chars,
71
      @NotNull final Document document,
72
      @NotNull final Node node ) {
73
      return false;
74
    }
75
76
    @Override
77
    public void updateNodeElements(
78
      @NotNull final Document document,
79
      @NotNull final Node node ) {}
80
  }
81
}
182
A src/main/java/com/keenwrite/processors/markdown/extensions/references/BasedSequenceNameParser.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.util.sequence.BasedSequence;
8
9
import java.util.regex.Pattern;
10
11
import static java.lang.String.format;
12
13
class BasedSequenceNameParser extends BasedSequenceParser {
14
  private static final String REGEX = format( "#%s", REGEX_INNER );
15
  private static final Pattern PATTERN = asPattern( REGEX );
16
17
  private BasedSequenceNameParser( final String text ) {
18
    super( text );
19
  }
20
21
  static BasedSequenceNameParser parse( final BasedSequence chars ) {
22
    return new BasedSequenceNameParser( chars.toString() );
23
  }
24
25
  @Override
26
  Pattern getPattern() {
27
    return PATTERN;
28
  }
29
}
130
A src/main/java/com/keenwrite/processors/markdown/extensions/references/BasedSequenceParser.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import java.util.regex.Matcher;
8
import java.util.regex.Pattern;
9
10
import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS;
11
12
abstract class BasedSequenceParser {
13
  /**
14
   * Shared syntax between subclasses: a letter followed by zero or more
15
   * alphanumeric characters.
16
   */
17
  static final String REGEX_INNER =
18
    "(\\p{Alpha}[\\p{Alnum}-_]+):(\\p{Alpha}[\\p{Alnum}-_]+)";
19
20
  private final String mTypeName;
21
  private final String mIdName;
22
23
  BasedSequenceParser( final String text ) {
24
    final var matcher = createMatcher( text );
25
26
    if( matcher.find() ) {
27
      mTypeName = matcher.group( 1 );
28
      mIdName = matcher.group( 2 );
29
    }
30
    else {
31
      mTypeName = null;
32
      mIdName = null;
33
    }
34
  }
35
36
  static Pattern asPattern( final String regex ) {
37
    return Pattern.compile( regex, UNICODE_CHARACTER_CLASS );
38
  }
39
40
  abstract Pattern getPattern();
41
42
  /**
43
   * Creates a regular expression pattern matcher that can extract the
44
   * reference elements from text.
45
   *
46
   * @param text The text containing an anchor or cross-reference to an anchor.
47
   * @return The {@link Matcher} to use when extracting the text elements.
48
   */
49
  Matcher createMatcher( final String text ) {
50
    return getPattern().matcher( text );
51
  }
52
53
  String getTypeName() {
54
    return mTypeName;
55
  }
56
57
  String getIdName() {
58
    return mIdName;
59
  }
60
}
161
A src/main/java/com/keenwrite/processors/markdown/extensions/references/BasedSequenceXrefParser.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.util.ast.Node;
8
import com.vladsch.flexmark.util.sequence.BasedSequence;
9
10
import java.util.regex.Pattern;
11
12
import static com.keenwrite.processors.markdown.extensions.common.EmptyNode.EMPTY_NODE;
13
import static java.lang.String.format;
14
15
class BasedSequenceXrefParser extends BasedSequenceParser {
16
  private static final String REGEX = format( "\\[@%s]", REGEX_INNER );
17
  private static final Pattern PATTERN = asPattern( REGEX );
18
19
  private BasedSequenceXrefParser( final String text ) {
20
    super( text );
21
  }
22
23
  static BasedSequenceXrefParser parse( final BasedSequence chars ) {
24
    return new BasedSequenceXrefParser( chars.toString() );
25
  }
26
27
  @Override
28
  Pattern getPattern() {
29
    return PATTERN;
30
  }
31
32
  Node toNode() {
33
    final var typeName = getTypeName();
34
    final var idName = getIdName();
35
36
    return typeName == null || idName == null
37
      ? EMPTY_NODE
38
      : new AnchorXrefNode( typeName, idName );
39
  }
40
}
141
A src/main/java/com/keenwrite/processors/markdown/extensions/references/CrossReferenceExtension.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownRendererExtension;
8
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
9
import com.vladsch.flexmark.parser.Parser.Builder;
10
11
/**
12
 * Responsible for processing {@code {@type:id}} anchors and their corresponding
13
 * {@code [@type:id]} cross-references.
14
 */
15
public final class CrossReferenceExtension extends MarkdownRendererExtension {
16
  /**
17
   * Use {@link #create()}.
18
   */
19
  private CrossReferenceExtension() {}
20
21
  /**
22
   * Returns a new {@link CrossReferenceExtension}.
23
   *
24
   * @return An extension capable of parsing cross-reference syntax.
25
   */
26
  public static CrossReferenceExtension create() {
27
    return new CrossReferenceExtension();
28
  }
29
30
  @Override
31
  public void extend( final Builder builder ) {
32
    builder.linkRefProcessorFactory( new AnchorXrefProcessorFactory() );
33
    builder.customDelimiterProcessor( new AnchorNameDelimiterProcessor() );
34
  }
35
36
  @Override
37
  protected NodeRendererFactory createNodeRendererFactory() {
38
    return new CrossReferencesNodeRendererFactory();
39
  }
40
}
141
A src/main/java/com/keenwrite/processors/markdown/extensions/references/CrossReferenceNode.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.html.HtmlWriter;
8
9
import static java.lang.String.format;
10
11
/**
12
 * Responsible for generating anchor links, either named or cross-referenced.
13
 */
14
public interface CrossReferenceNode {
15
  String getTypeName();
16
17
  String getIdName();
18
19
  String getRefAttrName();
20
21
  /**
22
   * Writes the HTML representation for this cross-reference node.
23
   *
24
   * @param html The HTML tag is written to the {@link HtmlWriter}.
25
   */
26
  default void write( final HtmlWriter html ) {
27
    final var type = getTypeName();
28
    final var id = getIdName();
29
    final var attr = getRefAttrName();
30
31
    final var clazz = format( "class=\"%s\"", attr );
32
    final var dataType = format( "data-type=\"%s\"", type );
33
    final var refId = format( "%s=\"%s\"", attr, id );
34
35
    html.raw( format( "<a %s %s %s />", clazz, dataType, refId ) );
36
  }
37
}
138
A src/main/java/com/keenwrite/processors/markdown/extensions/references/CrossReferencesNodeRenderer.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.html.HtmlWriter;
8
import com.vladsch.flexmark.html.renderer.NodeRenderer;
9
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
10
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
11
12
import java.util.Arrays;
13
import java.util.HashSet;
14
import java.util.Set;
15
16
/**
17
 * Responsible for rendering HTML elements that correspond to cross-references.
18
 */
19
class CrossReferencesNodeRenderer implements NodeRenderer {
20
  @Override
21
  public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
22
    return new HashSet<>( Arrays.asList(
23
      new NodeRenderingHandler<>( AnchorNameNode.class, this::render ),
24
      new NodeRenderingHandler<>( AnchorXrefNode.class, this::render )
25
    ) );
26
  }
27
28
  private void render(
29
    final CrossReferenceNode node,
30
    final NodeRendererContext context,
31
    final HtmlWriter html ) {
32
    node.write( html );
33
  }
34
}
135
A src/main/java/com/keenwrite/processors/markdown/extensions/references/CrossReferencesNodeRendererFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownNodeRendererFactory;
8
import com.vladsch.flexmark.html.renderer.NodeRenderer;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
11
class CrossReferencesNodeRendererFactory extends MarkdownNodeRendererFactory {
12
  @Override
13
  protected NodeRenderer createNodeRenderer( final DataHolder options ) {
14
    return new CrossReferencesNodeRenderer();
15
  }
16
}
117
A src/main/java/com/keenwrite/processors/markdown/extensions/tex/TexExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.tex;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.processors.ProcessorContext;
6
import com.keenwrite.processors.markdown.extensions.common.MarkdownRendererExtension;
7
import com.keenwrite.processors.markdown.extensions.tex.TexNodeRenderer.TexNodeRendererFactory;
8
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
9
import com.vladsch.flexmark.parser.Parser;
10
11
import java.util.function.Function;
12
13
/**
14
 * Responsible for wrapping delimited TeX code in Markdown into an XML element
15
 * that the HTML renderer can handle. For example, {@code $E=mc^2$} becomes
16
 * {@code <tex>E=mc^2</tex>} when passed to HTML renderer. The HTML renderer
17
 * is responsible for converting the TeX code for display. This avoids inserting
18
 * SVG code into the Markdown document, which the parser would then have to
19
 * iterate---a <em>very</em> wasteful operation that impacts front-end
20
 * performance.
21
 */
22
public class TexExtension extends MarkdownRendererExtension {
23
  /**
24
   * Responsible for pre-parsing the input.
25
   */
26
  private final Function<String, String> mEvaluator;
27
28
  /**
29
   * Controls how the node renderer produces TeX code within HTML output.
30
   */
31
  private final ExportFormat mExportFormat;
32
33
  private TexExtension(
34
    final Function<String, String> evaluator,
35
    final ProcessorContext context ) {
36
    mEvaluator = evaluator;
37
    mExportFormat = context.getExportFormat();
38
  }
39
40
  /**
41
   * Creates an extension capable of handling delimited TeX code in Markdown.
42
   *
43
   * @return The new {@link TexExtension}, never {@code null}.
44
   */
45
  public static TexExtension create(
46
    final Function<String, String> evaluator, final ProcessorContext context ) {
47
    return new TexExtension( evaluator, context );
48
  }
49
50
  /**
51
   * Creates the TeX {@link NodeRendererFactory} for HTML document export types.
52
   */
53
  @Override
54
  public NodeRendererFactory createNodeRendererFactory() {
55
    return new TexNodeRendererFactory( mExportFormat, mEvaluator );
56
  }
57
58
  @Override
59
  public void extend( final Parser.Builder builder ) {
60
    builder.customDelimiterProcessor( new TexInlineDelimiterProcessor() );
61
  }
62
}
163
A src/main/java/com/keenwrite/processors/markdown/extensions/tex/TexInlineDelimiterProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.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
class TexInlineDelimiterProcessor implements DelimiterProcessor {
11
12
  @Override
13
  public void process(
14
    final Delimiter opener,
15
    final Delimiter closer,
16
    final int delimitersUsed ) {
17
    final var node = new TexNode( opener, closer );
18
    opener.moveNodesBetweenDelimitersTo( node, closer );
19
  }
20
21
  @Override
22
  public char getOpeningCharacter() {
23
    return '$';
24
  }
25
26
  @Override
27
  public char getClosingCharacter() {
28
    return '$';
29
  }
30
31
  @Override
32
  public int getMinLength() {
33
    return 1;
34
  }
35
36
  /**
37
   * Allow for $ or $$.
38
   *
39
   * @param opener One or more opening delimiter characters.
40
   * @param closer One or more closing delimiter characters.
41
   * @return The number of delimiters to use to determine whether a valid
42
   * opening delimiter expression is found.
43
   */
44
  @Override
45
  public int getDelimiterUse(
46
    final DelimiterRun opener,
47
    final DelimiterRun closer ) {
48
    return 1;
49
  }
50
51
  @Override
52
  public boolean canBeOpener(
53
    final String before,
54
    final String after,
55
    final boolean leftFlanking,
56
    final boolean rightFlanking,
57
    final boolean beforeIsPunctuation,
58
    final boolean afterIsPunctuation,
59
    final boolean beforeIsWhitespace,
60
    final boolean afterIsWhiteSpace ) {
61
    return leftFlanking;
62
  }
63
64
  @Override
65
  public boolean canBeCloser(
66
    final String before,
67
    final String after,
68
    final boolean leftFlanking,
69
    final boolean rightFlanking,
70
    final boolean beforeIsPunctuation,
71
    final boolean afterIsPunctuation,
72
    final boolean beforeIsWhitespace,
73
    final boolean afterIsWhiteSpace ) {
74
    return rightFlanking;
75
  }
76
77
  @Override
78
  public Node unmatchedDelimiterNode(
79
    final InlineParser inlineParser,
80
    final DelimiterRun delimiter ) {
81
    return null;
82
  }
83
84
  @Override
85
  public boolean skipNonOpenerCloser() {
86
    return false;
87
  }
88
}
189
A src/main/java/com/keenwrite/processors/markdown/extensions/tex/TexNode.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.tex;
3
4
import com.vladsch.flexmark.ast.DelimitedNodeImpl;
5
import com.vladsch.flexmark.parser.core.delimiter.Delimiter;
6
7
public class TexNode extends DelimitedNodeImpl {
8
  /**
9
   * A TeX expression wrapped in a {@code <tex>} element.
10
   */
11
  public static final String HTML_TEX = "tex";
12
13
  static final String TOKEN_OPEN = "$";
14
  static final String TOKEN_CLOSE = "$";
15
16
  private final String mOpener;
17
  private final String mCloser;
18
19
  /**
20
   * Creates a new TeX node representation that can distinguish between '$'
21
   * and '$$' as opening/closing delimiters. The '$' is used for inline
22
   * TeX statements and '$$' is used for multi-line statements.
23
   *
24
   * @param opener The opening delimiter.
25
   * @param closer The closing delimiter.
26
   */
27
  public TexNode( final Delimiter opener, final Delimiter closer ) {
28
    mOpener = getDelimiter( opener );
29
    mCloser = getDelimiter( closer );
30
  }
31
32
  /**
33
   * @return Either '$' or '$$'.
34
   */
35
  public String getOpeningDelimiter() {
36
    return mOpener;
37
  }
38
39
  /**
40
   * @return Either '$' or '$$'.
41
   */
42
  public String getClosingDelimiter() {
43
    return mCloser;
44
  }
45
46
  private String getDelimiter( final Delimiter delimiter ) {
47
    return delimiter.getInput().subSequence(
48
      delimiter.getStartIndex(), delimiter.getEndIndex()
49
    ).toString();
50
  }
51
}
152
A src/main/java/com/keenwrite/processors/markdown/extensions/tex/TexNodeRenderer.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.tex;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.preview.MathRenderer;
6
import com.keenwrite.preview.SvgRasterizer;
7
import com.vladsch.flexmark.html.HtmlWriter;
8
import com.vladsch.flexmark.html.renderer.NodeRenderer;
9
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
10
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
11
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
12
import com.vladsch.flexmark.util.ast.Node;
13
import com.vladsch.flexmark.util.data.DataHolder;
14
import org.jetbrains.annotations.NotNull;
15
import org.jetbrains.annotations.Nullable;
16
17
import java.util.Map;
18
import java.util.Set;
19
import java.util.function.Function;
20
21
import static com.keenwrite.ExportFormat.*;
22
import static com.keenwrite.processors.markdown.extensions.tex.TexNode.*;
23
24
public class TexNodeRenderer {
25
  private static final RendererFacade RENDERER =
26
    new TexElementNodeRenderer( false );
27
28
  private static final Map<ExportFormat, RendererFacade> EXPORT_RENDERERS =
29
    Map.of(
30
      APPLICATION_PDF, new TexElementNodeRenderer( true ),
31
      HTML_TEX_SVG, new TexSvgNodeRenderer(),
32
      HTML_TEX_DELIMITED, new TexDelimitedNodeRenderer(),
33
      XHTML_TEX, new TexElementNodeRenderer( true ),
34
      TEXT_TEX, new TexElementNodeRenderer( true ),
35
      NONE, RENDERER
36
    );
37
38
  public static class TexNodeRendererFactory implements NodeRendererFactory {
39
    private final RendererFacade mNodeRenderer;
40
41
    public TexNodeRendererFactory(
42
      final ExportFormat exportFormat,
43
      final Function<String, String> evaluator ) {
44
      final var format = exportFormat == null ? NONE : exportFormat;
45
46
      mNodeRenderer = EXPORT_RENDERERS.getOrDefault( format, RENDERER );
47
      mNodeRenderer.setEvaluator( evaluator );
48
    }
49
50
    @NotNull
51
    @Override
52
    public NodeRenderer apply( @NotNull final DataHolder options ) {
53
      return mNodeRenderer;
54
    }
55
  }
56
57
  private static abstract class RendererFacade
58
    implements NodeRenderer {
59
    private Function<String, String> mEvaluator;
60
61
    @Override
62
    public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
63
      return Set.of(
64
        new NodeRenderingHandler<>( TexNode.class, this::render )
65
      );
66
    }
67
68
    /**
69
     * Subclasses implement this method to render the content of {@link TexNode}
70
     * instances as per their associated {@link ExportFormat}.
71
     *
72
     * @param node    {@link Node} containing text content of a math formula.
73
     * @param context Configuration information (unused).
74
     * @param html    Where to write the rendered output.
75
     */
76
    abstract void render( final TexNode node,
77
                          final NodeRendererContext context,
78
                          final HtmlWriter html );
79
80
    private void setEvaluator( final Function<String, String> evaluator ) {
81
      mEvaluator = evaluator;
82
    }
83
84
    Function<String, String> getEvaluator() {
85
      return mEvaluator;
86
    }
87
  }
88
89
  /**
90
   * Responsible for rendering a TeX node as an HTML {@code <tex>}
91
   * element. This is the default behaviour.
92
   */
93
  private static class TexElementNodeRenderer extends RendererFacade {
94
    private final boolean mIncludeDelimiter;
95
96
    private TexElementNodeRenderer( final boolean includeDelimiter ) {
97
      mIncludeDelimiter = includeDelimiter;
98
    }
99
100
    void render( final TexNode node,
101
                 final NodeRendererContext context,
102
                 final HtmlWriter html ) {
103
      final var text = getEvaluator().apply( node.getText().toString() );
104
      final var content =
105
        mIncludeDelimiter
106
          ? node.getOpeningDelimiter() + text + node.getClosingDelimiter()
107
          : text;
108
      html.tag( HTML_TEX );
109
      html.raw( content );
110
      html.closeTag( HTML_TEX );
111
    }
112
  }
113
114
  /**
115
   * Responsible for rendering a TeX node as an HTML {@code <svg>}
116
   * element.
117
   */
118
  private static class TexSvgNodeRenderer extends RendererFacade {
119
    void render( final TexNode node,
120
                 final NodeRendererContext context,
121
                 final HtmlWriter html ) {
122
      final var tex = node.getText().toStringOrNull();
123
      final var doc = MathRenderer.toDocument(
124
        tex == null ? "" : getEvaluator().apply( tex )
125
      );
126
      final var svg = SvgRasterizer.toSvg( doc.getDocumentElement() );
127
      html.raw( svg );
128
    }
129
  }
130
131
  /**
132
   * Responsible for rendering a TeX node as text bracketed by $ tokens.
133
   */
134
  private static class TexDelimitedNodeRenderer extends RendererFacade {
135
    void render( final TexNode node,
136
                 final NodeRendererContext context,
137
                 final HtmlWriter html ) {
138
      html.raw( TOKEN_OPEN );
139
      html.raw( getEvaluator().apply( node.getText().toString() ) );
140
      html.raw( TOKEN_CLOSE );
141
    }
142
  }
143
}
1144
A src/main/java/com/keenwrite/processors/pdf/PdfProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.pdf;
3
4
import com.keenwrite.processors.ExecutorProcessor;
5
import com.keenwrite.processors.ProcessorContext;
6
import com.keenwrite.typesetting.Typesetter;
7
8
import static com.keenwrite.Bootstrap.APP_TITLE_ABBR;
9
import static com.keenwrite.events.StatusEvent.clue;
10
import static com.keenwrite.io.MediaType.TEXT_XML;
11
import static com.keenwrite.io.SysFile.normalize;
12
import static com.keenwrite.typesetting.Typesetter.Mutator;
13
import static com.keenwrite.util.Strings.sanitize;
14
import static java.nio.charset.StandardCharsets.UTF_8;
15
import static java.nio.file.Files.deleteIfExists;
16
import static java.nio.file.Files.writeString;
17
18
/**
19
 * Responsible for using a typesetting engine to convert an XHTML document
20
 * into a PDF file. This must not be run from the JavaFX thread.
21
 */
22
public final class PdfProcessor extends ExecutorProcessor<String> {
23
  private final ProcessorContext mProcessorContext;
24
25
  public PdfProcessor( final ProcessorContext context ) {
26
    assert context != null;
27
    mProcessorContext = context;
28
  }
29
30
  /**
31
   * Converts a document by calling a third-party application to typeset the
32
   * given XHTML document.
33
   *
34
   * @param xhtml The document to convert to a PDF file.
35
   * @return {@code null} because there is no valid return value from generating
36
   * a PDF file.
37
   */
38
  public String apply( final String xhtml ) {
39
    try {
40
      clue( "Main.status.typeset.create" );
41
42
      final var context = mProcessorContext;
43
      final var targetPath = context.getTargetPath();
44
      clue( "Main.status.typeset.setting", "target", targetPath );
45
46
      final var parent = normalize( targetPath.toAbsolutePath().getParent() );
47
48
      final var document = TEXT_XML.createTempFile( APP_TITLE_ABBR, parent );
49
      final var sourcePath = writeString( document, xhtml, UTF_8 );
50
      clue( "Main.status.typeset.setting", "source", sourcePath );
51
52
      final var themeDir = normalize( context.getThemeDir() );
53
      clue( "Main.status.typeset.setting", "themes", themeDir );
54
55
      final var imageDir = normalize( context.getImageDir() );
56
      clue( "Main.status.typeset.setting", "images", imageDir );
57
58
      final var imageOrder = context.getImageOrder();
59
      clue( "Main.status.typeset.setting", "order", imageOrder );
60
61
      final var cacheDir = normalize( context.getCacheDir() );
62
      clue( "Main.status.typeset.setting", "caches", cacheDir );
63
64
      final var fontDir = normalize( context.getFontDir() );
65
      clue( "Main.status.typeset.setting", "fonts", fontDir );
66
67
      final var rWorkDir = normalize( context.getRWorkingDir() );
68
      clue( "Main.status.typeset.setting", "r-work", rWorkDir );
69
70
      final var modesEnabled = sanitize( context.getModesEnabled() );
71
      clue( "Main.status.typeset.setting", "mode", modesEnabled );
72
73
      final var autoRemove = context.getAutoRemove();
74
      clue( "Main.status.typeset.setting", "purge", autoRemove );
75
76
      final var typesetter = Typesetter
77
        .builder()
78
        .with( Mutator::setTargetPath, targetPath )
79
        .with( Mutator::setSourcePath, sourcePath )
80
        .with( Mutator::setThemeDir, themeDir )
81
        .with( Mutator::setImageDir, imageDir )
82
        .with( Mutator::setCacheDir, cacheDir )
83
        .with( Mutator::setFontDir, fontDir )
84
        .with( Mutator::setModesEnabled, modesEnabled )
85
        .with( Mutator::setAutoRemove, autoRemove )
86
        .build();
87
88
      try {
89
        typesetter.typeset();
90
      }
91
      finally {
92
        // Smote the temporary file after typesetting the document.
93
        if( typesetter.autoRemove() ) {
94
          deleteIfExists( document );
95
        }
96
      }
97
    } catch( final Exception ex ) {
98
      // Typesetter runtime exceptions will pass up the call stack.
99
      clue( "Main.status.typeset.failed", ex );
100
    }
101
102
    // Do not continue processing (the document was typeset into a binary).
103
    return null;
104
  }
105
}
1106
A src/main/java/com/keenwrite/processors/r/Engine.java
1
/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.r;
3
4
import com.keenwrite.collections.BoundedCache;
5
6
import javax.script.ScriptEngine;
7
import javax.script.ScriptEngineManager;
8
import java.util.Map;
9
10
import static com.keenwrite.Messages.get;
11
import static com.keenwrite.events.StatusEvent.clue;
12
import static java.lang.Math.min;
13
14
/**
15
 * Responsible for executing R statements, which can also update the engine's
16
 * state.
17
 */
18
public final class Engine {
19
  /**
20
   * Inline R expressions that have already been evaluated.
21
   */
22
  private static final Map<String, String> sCache =
23
    new BoundedCache<>( 768 );
24
25
  /**
26
   * Engine for evaluating R expressions.
27
   */
28
  private static final ScriptEngine sEngine =
29
    new ScriptEngineManager().getEngineByName( "Renjin" );
30
31
  /**
32
   * Empties the cache.
33
   */
34
  public static void clear() {
35
    sCache.clear();
36
  }
37
38
  /**
39
   * Look up an R expression from the cache then return the resulting object.
40
   * If the R expression hasn't been cached, it'll first be evaluated.
41
   *
42
   * @param r R expression to evaluate.
43
   * @return The object resulting from the evaluation.
44
   */
45
  public static String eval( final String r ) {
46
    return sCache.computeIfAbsent( r, _ -> evaluate( r ) );
47
  }
48
49
  /**
50
   * Returns the result of an R expression as an object converted to string.
51
   *
52
   * @param r R expression to evaluate.
53
   * @return The object resulting from the evaluation.
54
   */
55
  private static String evaluate( final String r ) {
56
    try {
57
      return sEngine.eval( r ).toString();
58
    } catch( final Exception ex ) {
59
      final var expr = r.substring( 0, min( r.length(), 50 ) );
60
      clue( get( "Main.status.error.r", expr, ex.getMessage() ), ex );
61
      throw new IllegalArgumentException( r );
62
    }
63
  }
64
}
165
A src/main/java/com/keenwrite/processors/r/RBootstrapController.java
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.r;
6
7
import com.keenwrite.preferences.Workspace;
8
import com.keenwrite.sigils.RKeyOperator;
9
10
import java.util.HashMap;
11
import java.util.Map;
12
import java.util.function.Supplier;
13
14
import static com.keenwrite.events.StatusEvent.clue;
15
import static com.keenwrite.preferences.AppKeys.KEY_R_DIR;
16
import static com.keenwrite.preferences.AppKeys.KEY_R_SCRIPT;
17
import static com.keenwrite.processors.variable.RVariableProcessor.escape;
18
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
19
20
/**
21
 * Transforms a document containing R statements into Markdown.
22
 */
23
public final class RBootstrapController {
24
25
  private static final RKeyOperator KEY_OPERATOR = new RKeyOperator();
26
27
  private final Workspace mWorkspace;
28
  private final Supplier<Map<String, String>> mSupplier;
29
30
  public RBootstrapController(
31
    final Workspace workspace,
32
    final Supplier<Map<String, String>> supplier ) {
33
    assert workspace != null;
34
    assert supplier != null;
35
36
    mWorkspace = workspace;
37
    mSupplier = supplier;
38
39
    mWorkspace.stringProperty( KEY_R_SCRIPT )
40
              .addListener( ( c, o, n ) -> update() );
41
    mWorkspace.fileProperty( KEY_R_DIR )
42
              .addListener( ( c, o, n ) -> update() );
43
44
    // Add the definitions immediately upon loading them.
45
    update();
46
  }
47
48
  /**
49
   * Updates the R code so that R can find imported libraries. Note that
50
   * any existing R functionality will not be overwritten if this method is
51
   * called multiple times.
52
   */
53
  public void update() {
54
    final var bootstrap = getRScript();
55
56
    if( !bootstrap.isBlank() ) {
57
      final var dir = getRWorkingDirectory();
58
      final var definitions = mSupplier.get();
59
60
      update( bootstrap, dir, definitions );
61
    }
62
  }
63
64
  public static void update(
65
    final String bootstrap,
66
    final String workingDir,
67
    final Map<String, String> definitions ) {
68
69
    if( !bootstrap.isBlank() ) {
70
      final Map<String, String> map;
71
72
      if( definitions == null ) {
73
        map = new HashMap<>();
74
      }
75
      else {
76
        map = new HashMap<>( definitions.size() + 1 );
77
        definitions.forEach(
78
          ( k, v ) -> map.put( KEY_OPERATOR.apply( k ), escape( v ) )
79
        );
80
      }
81
82
      map.put(
83
        KEY_OPERATOR.apply( "application.r.working.directory" ),
84
        escape( workingDir )
85
      );
86
87
      try {
88
        Engine.eval( replace( bootstrap, map ) );
89
      } catch( final Exception ex ) {
90
        clue( ex );
91
      }
92
    }
93
  }
94
95
  private String getRScript() {
96
    return mWorkspace.getString( KEY_R_SCRIPT );
97
  }
98
99
  private String getRWorkingDirectory() {
100
    final var wd = mWorkspace.getFile( KEY_R_DIR );
101
    return wd.toString().replace( '\\', '/' );
102
  }
103
}
1104
A src/main/java/com/keenwrite/processors/r/RBootstrapProcessor.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.r;
6
7
import com.keenwrite.processors.ExecutorProcessor;
8
import com.keenwrite.processors.Processor;
9
import com.keenwrite.processors.ProcessorContext;
10
11
public class RBootstrapProcessor extends ExecutorProcessor<String> {
12
  private final Processor<String> mSuccessor;
13
  private final ProcessorContext mContext;
14
15
  public RBootstrapProcessor(
16
    final Processor<String> successor,
17
    final ProcessorContext context ) {
18
    assert successor != null;
19
    assert context != null;
20
21
    mSuccessor = successor;
22
    mContext = context;
23
  }
24
25
  /**
26
   * Processes the given text document by replacing variables with their values.
27
   *
28
   * @param text The document text that includes variables that should be
29
   *             replaced with values when rendered as HTML.
30
   * @return The text with all variables replaced.
31
   */
32
  @Override
33
  public String apply( final String text ) {
34
    assert text != null;
35
36
    final var bootstrap = mContext.getRScript();
37
    final var workingDir = mContext.getRWorkingDir().toString();
38
    final var definitions = mContext.getDefinitions();
39
40
    RBootstrapController.update( bootstrap, workingDir, definitions );
41
42
    return mSuccessor.apply( text );
43
  }
44
}
145
A src/main/java/com/keenwrite/processors/r/RChunkEvaluator.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.r;
3
4
import java.util.function.Function;
5
6
import static com.keenwrite.constants.Constants.STATUS_PARSE_ERROR;
7
import static com.keenwrite.events.StatusEvent.clue;
8
9
/**
10
 * Transforms a document containing R statements into Markdown. The statements
11
 * are part of an R chunk, <code>```{r}</code>.
12
 */
13
public final class RChunkEvaluator implements Function<String, String> {
14
15
  /**
16
   * Constructs an evaluator capable of executing R statements.
17
   */
18
  public RChunkEvaluator() {}
19
20
  /**
21
   * Evaluates the given R statements and returns the result as a string.
22
   * If an image was produced, the calling code is responsible for persisting
23
   * and making the file embeddable into the document.
24
   *
25
   * @param r The R statements to evaluate.
26
   * @return The output from the final R statement.
27
   */
28
  @Override
29
  public String apply( final String r ) {
30
    try {
31
      return Engine.eval( r );
32
    } catch( final Exception ex ) {
33
      clue( STATUS_PARSE_ERROR, ex.getMessage() );
34
35
      return r;
36
    }
37
  }
38
}
139
A src/main/java/com/keenwrite/processors/r/RInlineEvaluator.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.r;
3
4
import com.keenwrite.processors.Processor;
5
import com.keenwrite.processors.variable.RVariableProcessor;
6
7
import java.util.function.Function;
8
import java.util.function.Predicate;
9
10
import static com.keenwrite.constants.Constants.STATUS_PARSE_ERROR;
11
import static com.keenwrite.events.StatusEvent.clue;
12
13
/**
14
 * Evaluates inline R statements.
15
 */
16
public final class RInlineEvaluator
17
  implements Function<String, String>, Predicate<String> {
18
  public static final String PREFIX = "`r#";
19
  public static final String SUFFIX = "`";
20
21
  private static final int PREFIX_LENGTH = PREFIX.length();
22
23
  private final Processor<String> mProcessor;
24
25
  /**
26
   * Constructs an evaluator capable of executing R statements.
27
   */
28
  public RInlineEvaluator( final RVariableProcessor processor ) {
29
    mProcessor = processor;
30
  }
31
32
  /**
33
   * Evaluates all R statements in the source document and inserts the
34
   * calculated value into the generated document.
35
   *
36
   * @param text The document text that includes variables that should be
37
   *             replaced with values when rendered as HTML.
38
   * @return The generated document with output from all R statements
39
   * substituted with value returned from their execution.
40
   */
41
  @Override
42
  public String apply( final String text ) {
43
    try {
44
      final var buffer = new StringBuilder( text.length() );
45
46
      int index = 0;
47
      int began;
48
      int ended = 0;
49
50
      while( (began = text.indexOf( PREFIX, index )) >= 0 && ended > -1 ) {
51
        buffer.append( text, index, began );
52
53
        // If the R expression has no definite end, this returns -1.
54
        ended = text.indexOf( SUFFIX, began + 1 );
55
56
        if( ended > began ) {
57
          final var r = mProcessor.apply(
58
            text.substring( began + PREFIX_LENGTH, ended )
59
          );
60
61
          // Return the evaluated R expression for insertion back into the text.
62
          buffer.append( Engine.eval( r ) );
63
64
          index = ended + 1;
65
        }
66
      }
67
68
      buffer.append( text.substring( index ) );
69
70
      return buffer.toString();
71
    } catch( final Exception ex ) {
72
      clue( STATUS_PARSE_ERROR, ex.getMessage() );
73
74
      // If the string couldn't be parsed using R, append the statement
75
      // that failed to parse, instead of its evaluated value.
76
      return text;
77
    }
78
  }
79
80
  /**
81
   * Answers whether the engine associated with this evaluator may attempt to
82
   * evaluate the given source code statement.
83
   *
84
   * @param code The source code to verify.
85
   * @return {@code true} if the code may be evaluated.
86
   */
87
  @Override
88
  public boolean test( final String code ) {
89
    return code.startsWith( PREFIX );
90
  }
91
}
192
A src/main/java/com/keenwrite/processors/text/AbstractTextReplacer.java
1
/* Copyright 2020-2021 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
   * Optimization: Cache keys until the map changes.
12
   */
13
  private String[] mKeys = new String[ 0 ];
14
15
  /**
16
   * Optimization: Cache values until the map changes.
17
   */
18
  private String[] mValues = new String[ 0 ];
19
20
  /**
21
   * Optimization: Detect when the map changes.
22
   */
23
  private int mMapHash;
24
25
  private final Object mMutex = new Object();
26
27
  /**
28
   * Default (empty) constructor.
29
   */
30
  protected AbstractTextReplacer() { }
31
32
  protected String[] keys( final Map<String, String> map ) {
33
    synchronized( mMutex ) {
34
      updateCache( map );
35
      return mKeys;
36
    }
37
  }
38
39
  protected String[] values( final Map<String, String> map ) {
40
    synchronized( mMutex ) {
41
      updateCache( map );
42
      return mValues;
43
    }
44
  }
45
46
  private void updateCache( final Map<String, String> map ) {
47
    synchronized( mMutex ) {
48
      if( map.hashCode() != mMapHash ) {
49
        mKeys = map.keySet().toArray( new String[ 0 ] );
50
        mValues = map.values().toArray( new String[ 0 ] );
51
        mMapHash = map.hashCode();
52
      }
53
    }
54
  }
55
}
156
A src/main/java/com/keenwrite/processors/text/AhoCorasickReplacer.java
1
/* Copyright 2020-2021 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
  @Override
19
  public String replace( final String text, final Map<String, String> map ) {
20
    assert text != null;
21
    assert map != null;
22
23
    // Create a buffer sufficiently large that re-allocations are minimized.
24
    final var sb = new StringBuilder( (int) (text.length() * 1.25) );
25
26
    // Definition names cannot overlap.
27
    final var builder = builder().ignoreOverlaps();
28
    final var keySet = keys( map );
29
30
    if( keySet != null ) {
31
      builder.addKeywords( keys( map ) );
32
    }
33
34
    int index = 0;
35
36
    // Replace all instances with dereferenced variables.
37
    for( final var emit : builder.build().parseText( text ) ) {
38
      sb.append( text, index, emit.getStart() );
39
      sb.append( map.get( emit.getKeyword() ) );
40
      index = emit.getEnd() + 1;
41
    }
42
43
    // Add the remainder of the string (contains no more matches).
44
    sb.append( text.substring( index ) );
45
46
    return sb.toString();
47
  }
48
}
149
A src/main/java/com/keenwrite/processors/text/StringUtilsReplacer.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.text;
3
4
import java.util.Map;
5
6
import static com.keenwrite.util.Strings.replaceEach;
7
8
/**
9
 * Replaces text using a brute-force replacement method.
10
 */
11
public class StringUtilsReplacer extends AbstractTextReplacer {
12
13
  /**
14
   * Default (empty) constructor.
15
   */
16
  protected StringUtilsReplacer() {}
17
18
  @Override
19
  public String replace( final String text, final Map<String, String> map ) {
20
    return replaceEach( text, keys( map ), values( map ) );
21
  }
22
}
123
A src/main/java/com/keenwrite/processors/text/TextProcessor.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.text;
6
7
import com.keenwrite.io.MediaType;
8
import com.keenwrite.processors.ExecutorProcessor;
9
import com.keenwrite.processors.Processor;
10
import com.keenwrite.processors.ProcessorContext;
11
import com.keenwrite.processors.r.RInlineEvaluator;
12
import com.keenwrite.processors.variable.RVariableProcessor;
13
import com.keenwrite.processors.variable.VariableProcessor;
14
15
import java.util.function.Function;
16
17
import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN;
18
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
19
20
/**
21
 * Responsible for converting documents to plain text files. This will
22
 * perform interpolated variable substitutions and execute R commands
23
 * as necessary.
24
 */
25
public class TextProcessor extends ExecutorProcessor<String> {
26
  private final Function<String, String> mEvaluator;
27
28
  public TextProcessor(
29
    final Processor<String> successor,
30
    final ProcessorContext context ) {
31
    super( successor );
32
33
    final var inputPath = context.getSourcePath();
34
    final var mediaType = MediaType.fromFilename( inputPath );
35
36
    if( mediaType == TEXT_R_MARKDOWN ) {
37
      final var rVarProcessor = new RVariableProcessor( IDENTITY, context );
38
      mEvaluator = new RInlineEvaluator( rVarProcessor );
39
    }
40
    else {
41
      mEvaluator = new VariableProcessor( IDENTITY, context );
42
    }
43
  }
44
45
  @Override
46
  public String apply( final String document ) {
47
    return mEvaluator.apply( document );
48
  }
49
}
150
A src/main/java/com/keenwrite/processors/text/TextReplacementFactory.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.text;
3
4
import com.keenwrite.util.Strings;
5
6
import java.util.Map;
7
8
/**
9
 * Used to generate a class capable of efficiently replacing variable
10
 * definitions with their values.
11
 */
12
public final class TextReplacementFactory {
13
14
  private static final TextReplacer APACHE = new StringUtilsReplacer();
15
  private static final TextReplacer AHO_CORASICK = new AhoCorasickReplacer();
16
17
  /**
18
   * Returns a text search/replacement instance that is reasonably optimal for
19
   * the given length of text.
20
   *
21
   * @param length The length of text that requires some search and replacing.
22
   * @return A class that can search and replace text with utmost expediency.
23
   */
24
  public static TextReplacer getTextReplacer( final int length ) {
25
    // After about 1,500 characters, the Aho-Corsick algorithm is faster.
26
    return length < 1500 ? APACHE : AHO_CORASICK;
27
  }
28
29
  /**
30
   * Convenience method to instantiate a suitable text replacer algorithm and
31
   * perform a replacement using the given map. At this point, the values should
32
   * be already dereferenced and ready to be substituted verbatim; any
33
   * recursively defined values must have been interpolated previously.
34
   *
35
   * @param text    The text containing zero or more variables to replace.
36
   * @param needles The map of variables to their dereferenced values.
37
   * @return The text with all variables replaced.
38
   */
39
  public static String replace(
40
    final String text, final Map<String, String> needles ) {
41
    final String haystack = Strings.sanitize( text );
42
    assert needles != null;
43
44
    return getTextReplacer( haystack.length() ).replace( haystack, needles );
45
  }
46
}
147
A src/main/java/com/keenwrite/processors/text/TextReplacer.java
1
/* Copyright 2020-2021 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/processors/variable/RVariableProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.variable;
6
7
import com.keenwrite.processors.Processor;
8
import com.keenwrite.processors.ProcessorContext;
9
import com.keenwrite.sigils.RKeyOperator;
10
import com.keenwrite.sigils.SigilKeyOperator;
11
12
/**
13
 * Converts the keys of the resolved map from default form to R form, then
14
 * performs a substitution on the text. The default R variable syntax is
15
 * <pre>v$tree$leaf</pre>.
16
 */
17
public class RVariableProcessor extends VariableProcessor {
18
  public RVariableProcessor(
19
    final Processor<String> successor, final ProcessorContext context ) {
20
    super( successor, context );
21
  }
22
23
  @Override
24
  protected SigilKeyOperator createKeyOperator(
25
    final ProcessorContext context ) {
26
    return new RKeyOperator();
27
  }
28
29
  @Override
30
  protected String processValue( final String value ) {
31
    assert value != null;
32
33
    return escape( value );
34
  }
35
36
  /**
37
   * In R, single quotes and double quotes are interchangeable. Using single
38
   * quotes is simpler to code.
39
   *
40
   * @param value The text to convert into a valid quoted R string.
41
   * @return The quoted value with embedded quotes escaped as necessary.
42
   */
43
  public static String escape( final String value ) {
44
    return '\'' + escape( value, '\'', "\\'" ) + '\'';
45
  }
46
47
  /**
48
   * TODO: Make generic method for replacing text.
49
   *
50
   * @param haystack Search this string for the needle, must not be null.
51
   * @param needle   The character to find in the haystack.
52
   * @param thread   Replace the needle with this text, if the needle is found.
53
   * @return The haystack with the all instances of needle replaced with thread.
54
   */
55
  @SuppressWarnings( "SameParameterValue" )
56
  private static String escape(
57
    final String haystack, final char needle, final String thread ) {
58
    assert haystack != null;
59
    assert thread != null;
60
61
    int end = haystack.indexOf( needle );
62
63
    if( end < 0 ) {
64
      return haystack;
65
    }
66
67
    int start = 0;
68
69
    // Replace up to 32 occurrences before reallocating the internal buffer.
70
    final var sb = new StringBuilder( haystack.length() + 32 );
71
72
    while( end >= 0 ) {
73
      sb.append( haystack, start, end ).append( thread );
74
      start = end + 1;
75
      end = haystack.indexOf( needle, start );
76
    }
77
78
    return sb.append( haystack.substring( start ) ).toString();
79
  }
80
}
181
A src/main/java/com/keenwrite/processors/variable/VariableProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.variable;
3
4
import com.keenwrite.processors.ExecutorProcessor;
5
import com.keenwrite.processors.Processor;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.keenwrite.sigils.SigilKeyOperator;
8
9
import java.util.HashMap;
10
import java.util.Map;
11
import java.util.function.Function;
12
13
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
14
15
/**
16
 * Processes interpolated string definitions in the document and inserts
17
 * their values into the post-processed text. The default variable syntax is
18
 * <pre>{{variable}}</pre> (a.k.a., moustache syntax).
19
 */
20
public class VariableProcessor
21
  extends ExecutorProcessor<String> implements Function<String, String> {
22
23
  private final ProcessorContext mContext;
24
  private final SigilKeyOperator mSigilOperator;
25
26
  /**
27
   * Constructs a processor capable of interpolating string definitions.
28
   *
29
   * @param successor Subsequent link in the processing chain.
30
   * @param context   Contains resolved definitions map.
31
   */
32
  public VariableProcessor(
33
    final Processor<String> successor,
34
    final ProcessorContext context ) {
35
    super( successor );
36
37
    mContext = context;
38
    mSigilOperator = createKeyOperator( context );
39
  }
40
41
  /**
42
   * Subclasses may change the type of operation performed on keys, such as
43
   * wrapping key names in sigils.
44
   *
45
   * @param context Provides the name of the file being edited.
46
   * @return An operator for transforming key names.
47
   */
48
  protected SigilKeyOperator createKeyOperator(
49
    final ProcessorContext context ) {
50
    return context.createKeyOperator();
51
  }
52
53
  /**
54
   * Returns the map to use for variable substitution.
55
   *
56
   * @return A map of variable names to values, with keys wrapped in sigils.
57
   */
58
  public Map<String, String> getDefinitions() {
59
    return entoken( mContext.getInterpolatedDefinitions() );
60
  }
61
62
  /**
63
   * Subclasses may override this method to change how keys are wrapped
64
   * in sigils.
65
   *
66
   * @param key The key to enwrap.
67
   * @return The wrapped key.
68
   */
69
  protected String processKey( final String key ) {
70
    return mSigilOperator.apply( key );
71
  }
72
73
  /**
74
   * Subclasses may override this method to modify values prior to use. This
75
   * can be used, for example, to escape values prior to evaluating by a
76
   * scripting engine.
77
   *
78
   * @param value The value to process.
79
   * @return The processed value.
80
   */
81
  protected String processValue( final String value ) {
82
    return value;
83
  }
84
85
  /**
86
   * Answers whether the given key is wrapped in sigil tokens.
87
   *
88
   * @param key The key to analyze.
89
   * @return {@code true} if the key is wrapped in sigils.
90
   */
91
  public boolean hasSigils( final String key ) {
92
    return mSigilOperator.match( key ).find();
93
  }
94
95
  /**
96
   * Processes the given text document by replacing variables with their values.
97
   *
98
   * @param text The document text that includes variables that should be
99
   *             replaced with values when rendered as HTML.
100
   * @return The text with all variables replaced.
101
   */
102
  @Override
103
  public String apply( final String text ) {
104
    assert text != null;
105
106
    return replace( text, getDefinitions() );
107
  }
108
109
  /**
110
   * Converts the given map from regular variables to processor-specific
111
   * variables.
112
   *
113
   * @param map Map of variable names to values.
114
   * @return Map of variables with the keys and values subjected to
115
   * post-processing.
116
   */
117
  protected Map<String, String> entoken( final Map<String, String> map ) {
118
    assert map != null;
119
120
    final var result = new HashMap<String, String>( map.size() );
121
122
    map.forEach( ( k, v ) -> result.put( processKey( k ), processValue( v ) ) );
123
124
    return result;
125
  }
126
}
1127
A src/main/java/com/keenwrite/search/SearchModel.java
1
/* Copyright 2020-2021 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.List;
13
14
import static org.ahocorasick.trie.Trie.builder;
15
16
/**
17
 * Responsible for finding words in a text document. This implementation uses
18
 * a {@link Trie} for efficiency.
19
 */
20
public final class SearchModel {
21
  private final ObjectProperty<IndexRange> mMatchOffset =
22
      new SimpleObjectProperty<>();
23
  private final ObjectProperty<Integer> mMatchCount =
24
      new SimpleObjectProperty<>();
25
  private final ObjectProperty<Integer> mMatchIndex =
26
      new SimpleObjectProperty<>();
27
28
  private CyclicIterator<Emit> mMatches = new CyclicIterator<>( List.of() );
29
30
  private String mNeedle = "";
31
32
  /**
33
   * Creates a new {@link SearchModel} that finds all text string in a
34
   * document simultaneously.
35
   */
36
  public SearchModel() {
37
  }
38
39
  public ObjectProperty<Integer> matchCountProperty() {
40
    return mMatchCount;
41
  }
42
43
  public ObjectProperty<Integer> matchIndexProperty() {
44
    return mMatchIndex;
45
  }
46
47
  /**
48
   * Observers watch this property to be notified when a needle has been
49
   * found in the haystack. Use {@link IndexRange#getStart()} to get the
50
   * absolute offset into the text (zero-based).
51
   *
52
   * @return The {@link IndexRange} property to observe, representing the
53
   * most recently matched text offset into the document.
54
   */
55
  public ObservableValue<IndexRange> matchOffsetProperty() {
56
    return mMatchOffset;
57
  }
58
59
  /**
60
   * Searches the document for text matching the given parameter value. This
61
   * is the main entry point for kicking off text searches.
62
   *
63
   * @param needle   The text string to find in the document, no regex allowed.
64
   * @param haystack The document to search within for a text string.
65
   */
66
  public void search( final String needle, final String haystack ) {
67
    assert needle != null;
68
    assert haystack != null;
69
70
    final var trie = builder()
71
        .ignoreCase()
72
        .ignoreOverlaps()
73
        .addKeyword( needle )
74
        .build();
75
    final var emits = trie.parseText( haystack );
76
77
    mMatches = new CyclicIterator<>( emits );
78
    mMatchCount.set( emits.size() );
79
    mNeedle = needle;
80
    advance();
81
  }
82
83
  /**
84
   * Searches the document for the last known needle.
85
   *
86
   * @param haystack The new text to search.
87
   */
88
  public void search( final String haystack ) {
89
    search( mNeedle, haystack );
90
  }
91
92
  /**
93
   * Moves the search iterator to the next match, wrapping as needed.
94
   */
95
  public void advance() {
96
    if( mMatches.hasNext() ) {
97
      setCurrent( mMatches.next() );
98
    }
99
  }
100
101
  /**
102
   * Moves the search iterator to the previous match, wrapping as needed.
103
   */
104
  public void retreat() {
105
    if( mMatches.hasPrevious() ) {
106
      setCurrent( mMatches.previous() );
107
    }
108
  }
109
110
  private void setCurrent( final Emit emit ) {
111
    mMatchOffset.set( new IndexRange( emit.getStart(), emit.getEnd() ) );
112
    mMatchIndex.set( mMatches.getIndex() + 1 );
113
  }
114
}
1115
A src/main/java/com/keenwrite/security/PermissiveCertificate.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.security;
3
4
import javax.net.ssl.*;
5
import java.security.KeyManagementException;
6
import java.security.NoSuchAlgorithmException;
7
import java.security.SecureRandom;
8
import java.security.cert.X509Certificate;
9
10
import static javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier;
11
import static javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory;
12
13
/**
14
 * Responsible for trusting all certificate chains. The purpose of this class
15
 * is to work-around certificate issues caused by software that blocks
16
 * HTTP requests. For example, Zscaler may block HTTP requests to kroki.io
17
 * when generating diagrams.
18
 */
19
public final class PermissiveCertificate {
20
  /**
21
   * Create a trust manager that does not validate certificate chains.
22
   */
23
  private static final TrustManager[] TRUST_ALL_CERTS = new TrustManager[]{
24
    new X509TrustManager() {
25
      @Override
26
      public X509Certificate[] getAcceptedIssuers() {
27
        return new X509Certificate[ 0 ];
28
      }
29
30
      @Override
31
      public void checkClientTrusted(
32
        X509Certificate[] certs, String authType ) {
33
      }
34
35
      @Override
36
      public void checkServerTrusted(
37
        X509Certificate[] certs, String authType ) {
38
      }
39
    }
40
  };
41
42
  /**
43
   * Responsible for permitting all hostnames for making HTTP requests.
44
   */
45
  private static class PermissiveHostNameVerifier implements HostnameVerifier {
46
    @Override
47
    public boolean verify( final String hostname, final SSLSession session ) {
48
      return true;
49
    }
50
  }
51
52
  /**
53
   * Install the all-trusting trust manager. If this fails it means that in
54
   * certain situations the HTML preview may fail to render diagrams. A way
55
   * to work around the issue is to install a local server for generating
56
   * diagrams.
57
   */
58
  public static boolean installTrustManager() {
59
    try {
60
      final var context = SSLContext.getInstance( "SSL" );
61
      context.init( null, TRUST_ALL_CERTS, new SecureRandom() );
62
      setDefaultSSLSocketFactory( context.getSocketFactory() );
63
      setDefaultHostnameVerifier( new PermissiveHostNameVerifier() );
64
      return true;
65
    } catch( NoSuchAlgorithmException | KeyManagementException e ) {
66
      return false;
67
    }
68
  }
69
70
  /**
71
   * Use {@link #installTrustManager()}.
72
   */
73
  private PermissiveCertificate() {}
74
}
175
A src/main/java/com/keenwrite/service/Service.java
1
/* Copyright 2020-2021 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-2021 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/events/Notification.java
1
/* Copyright 2020-2021 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-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.service.events;
3
4
import javafx.scene.control.Alert;
5
import javafx.stage.Window;
6
7
import java.nio.file.Path;
8
9
/**
10
 * Provides the application with a uniform way to notify the user of events.
11
 */
12
public interface Notifier {
13
14
  /**
15
   * Constructs an alert message text for a modal alert dialog.
16
   *
17
   * @param parent     The window responsible for the child dialog.
18
   * @param path       The path to a file that was not actionable.
19
   * @param titleKey   The dialog box message title.
20
   * @param messageKey The dialog box message content (needs formatting).
21
   * @param ex         The problem that requires user attention.
22
   */
23
  void alert(
24
    Window parent,
25
    Path path,
26
    String titleKey,
27
    String messageKey,
28
    Exception ex );
29
30
  /**
31
   * Constructs an alert message text for a modal alert dialog.
32
   *
33
   * @param parent The window responsible for the child dialog.
34
   * @param path   The path to a file that was not actionable.
35
   * @param key    Prefix for both title and message key.
36
   * @param ex     The problem that requires user attention.
37
   */
38
  default void alert(
39
    Window parent,
40
    Path path,
41
    String key,
42
    Exception ex ) {
43
    alert( parent, path, key + ".title", key + ".message", ex );
44
  }
45
46
  /**
47
   * Contains all the information that the user needs to know about a problem.
48
   *
49
   * @param title   The dialog box message title (i.e., the error context).
50
   * @param message The message content (formatted with the given args).
51
   * @param args    The arguments to the message content that must be formatted.
52
   * @return The message suitable for building a modal alert dialog.
53
   */
54
  Notification createNotification(
55
    String title,
56
    String message,
57
    Object... args );
58
59
  /**
60
   * Creates an alert of alert type error with a message showing the cause of
61
   * the error.
62
   *
63
   * @param parent  Dialog box owner (for modal purposes).
64
   * @param message The error message, title, and possibly more details.
65
   * @return A modal alert dialog box ready to display using showAndWait.
66
   */
67
  Alert createError( Window parent, Notification message );
68
69
  /**
70
   * Creates an alert of alert type confirmation with Yes/No/Cancel buttons.
71
   *
72
   * @param parent  Dialog box owner (for modal purposes).
73
   * @param message The message, title, and possibly more details.
74
   * @return A modal alert dialog box ready to display using showAndWait.
75
   */
76
  Alert createConfirmation( Window parent, Notification message );
77
}
178
A src/main/java/com/keenwrite/service/events/impl/ButtonOrderPane.java
1
/* Copyright 2020-2021 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.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
  public ButtonOrderPane() {
17
  }
18
19
  @Override
20
  protected Node createButtonBar() {
21
    final var node = (ButtonBar) super.createButtonBar();
22
    node.setButtonOrder( getButtonOrder() );
23
    return node;
24
  }
25
26
  private String getButtonOrder() {
27
    return sSettings.getSetting(
28
      "dialog.alert.button.order.windows", BUTTON_ORDER_WINDOWS );
29
  }
30
}
131
A src/main/java/com/keenwrite/service/events/impl/DefaultNotification.java
1
/* Copyright 2020-2021 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 mTitle;
14
  private final String mContent;
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
    mTitle = title;
28
    mContent = MessageFormat.format( message, args );
29
  }
30
31
  @Override
32
  public String getTitle() {
33
    return mTitle;
34
  }
35
36
  @Override
37
  public String getContent() {
38
    return mContent;
39
  }
40
41
}
142
A src/main/java/com/keenwrite/service/events/impl/DefaultNotifier.java
1
/* Copyright 2020-2021 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.constants.GraphicsConstants.ICON_DIALOG_NODE;
13
import static com.keenwrite.Messages.get;
14
import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
15
import static javafx.scene.control.Alert.AlertType.ERROR;
16
import static javafx.scene.control.ButtonType.*;
17
18
/**
19
 * Provides the ability to notify the user of events that need attention,
20
 * such as prompting the user to confirm closing when there are unsaved changes.
21
 */
22
public final class DefaultNotifier implements Notifier {
23
24
  @Override
25
  public Notification createNotification(
26
      final String title,
27
      final String message,
28
      final Object... args ) {
29
    return new DefaultNotification( title, message, args );
30
  }
31
32
  @Override
33
  public void alert(
34
      final Window parent,
35
      final Path path,
36
      final String titleKey,
37
      final String messageKey,
38
      final Exception ex ) {
39
    final var message = createNotification(
40
        get( titleKey ), get( messageKey ), path, ex.getMessage()
41
    );
42
43
    createError( parent, message ).showAndWait();
44
  }
45
46
  @Override
47
  public Alert createConfirmation(
48
      final Window parent, final Notification message ) {
49
    final var alert = createAlertDialog( parent, CONFIRMATION, message );
50
51
    alert.getButtonTypes().setAll( YES, NO, CANCEL );
52
53
    return alert;
54
  }
55
56
  @Override
57
  public Alert createError( final Window parent, final Notification message ) {
58
    return createAlertDialog( parent, ERROR, message );
59
  }
60
61
  private Alert createAlertDialog(
62
      final Window parent,
63
      final AlertType alertType,
64
      final Notification message ) {
65
    final var alert = new Alert( alertType );
66
67
    alert.setDialogPane( new ButtonOrderPane() );
68
    alert.setTitle( message.getTitle() );
69
    alert.setHeaderText( null );
70
    alert.setContentText( message.getContent() );
71
    alert.initOwner( parent );
72
    alert.setGraphic( ICON_DIALOG_NODE );
73
74
    return alert;
75
  }
76
}
177
A src/main/java/com/keenwrite/service/impl/DefaultSettings.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.service.impl;
3
4
import com.keenwrite.config.PropertiesConfiguration;
5
import com.keenwrite.service.Settings;
6
7
import java.io.InputStreamReader;
8
import java.net.URL;
9
import java.nio.charset.Charset;
10
import java.util.Iterator;
11
import java.util.List;
12
13
import static com.keenwrite.constants.Constants.PATH_PROPERTIES_SETTINGS;
14
15
/**
16
 * Responsible for loading settings that help avoid hard-coded assumptions.
17
 */
18
public final class DefaultSettings implements Settings {
19
20
  private final PropertiesConfiguration mProperties = loadProperties();
21
22
  public DefaultSettings() {}
23
24
  /**
25
   * Returns the value of a string property.
26
   *
27
   * @param property     The property key.
28
   * @param defaultValue The value to return if no property key has been set.
29
   * @return The property key value, or defaultValue when no key found.
30
   */
31
  @Override
32
  public String getSetting( final String property, final String defaultValue ) {
33
    return getSettings().getString( property, defaultValue );
34
  }
35
36
  /**
37
   * Returns the value of a string property.
38
   *
39
   * @param property     The property key.
40
   * @param defaultValue The value to return if no property key has been set.
41
   * @return The property key value, or defaultValue when no key found.
42
   */
43
  @Override
44
  public int getSetting( final String property, final int defaultValue ) {
45
    return getSettings().getInt( property, defaultValue );
46
  }
47
48
  /**
49
   * Convert the generic list of property objects into strings.
50
   *
51
   * @param property The property value to coerce.
52
   * @param defaults The values to use should the property be unset.
53
   * @return The list of properties coerced from objects to strings.
54
   */
55
  @Override
56
  public List<String> getStringSettingList(
57
    final String property, final List<String> defaults ) {
58
    return getSettings().getList( property, defaults );
59
  }
60
61
  /**
62
   * Convert a list of property objects into strings, with no default value.
63
   *
64
   * @param property The property value to coerce.
65
   * @return The list of properties coerced from objects to strings.
66
   */
67
  @Override
68
  public List<String> getStringSettingList( final String property ) {
69
    return getStringSettingList( property, null );
70
  }
71
72
  /**
73
   * Returns a list of property names that begin with the given prefix.
74
   *
75
   * @param prefix The prefix to compare against each property name.
76
   * @return The list of property names that have the given prefix.
77
   */
78
  @Override
79
  public Iterator<String> getKeys( final String prefix ) {
80
    return getSettings().getKeys( prefix );
81
  }
82
83
  private PropertiesConfiguration loadProperties() {
84
    final var url = getPropertySource();
85
    final var configuration = new PropertiesConfiguration();
86
    final var encoding = getDefaultEncoding();
87
88
    if( url != null ) {
89
      try( final var reader = new InputStreamReader(
90
        url.openStream(), encoding ) ) {
91
        configuration.read( reader );
92
      } catch( final Exception ex ) {
93
        throw new RuntimeException( ex );
94
      }
95
    }
96
97
    return configuration;
98
  }
99
100
  private Charset getDefaultEncoding() {
101
    return Charset.defaultCharset();
102
  }
103
104
  private URL getPropertySource() {
105
    return DefaultSettings.class.getResource( PATH_PROPERTIES_SETTINGS );
106
  }
107
108
  private PropertiesConfiguration getSettings() {
109
    return mProperties;
110
  }
111
}
1112
A src/main/java/com/keenwrite/sigils/PropertyKeyOperator.java
1
/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.sigils;
3
4
/**
5
 * Responsible for defining sigils used within property files.
6
 */
7
public class PropertyKeyOperator extends SigilKeyOperator {
8
  public static final String BEGAN = "${";
9
  public static final String ENDED = "}";
10
11
  /**
12
   * Constructs a new {@link SigilKeyOperator} subclass with <code>${</code>
13
   * and <code>}</code> used for the beginning and ending sigils.
14
   */
15
  public PropertyKeyOperator() {
16
    super( BEGAN, ENDED );
17
  }
18
}
119
A src/main/java/com/keenwrite/sigils/RKeyOperator.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.sigils;
3
4
import com.keenwrite.collections.BoundedCache;
5
6
/**
7
 * Converts dot-separated variable names into names compatible with R. That is,
8
 * {@code variable.name.qualified} becomes {@code v$variable$name$qualified}.
9
 */
10
public final class RKeyOperator extends SigilKeyOperator {
11
  private static final char KEY_SEPARATOR_DEF = '.';
12
  private static final char KEY_SEPARATOR_R = '$';
13
14
  /** Minor optimization to avoid recreating an object. */
15
  private final StringBuilder mVarName = new StringBuilder( 128 );
16
17
  /** Optimization to avoid re-converting variable names into R format. */
18
  private final BoundedCache<String, String> mVariables = new BoundedCache<>(
19
    2048
20
  );
21
22
  /**
23
   * Constructs a new instance capable of converting dot-separated variable
24
   * names into R's dollar-symbol-separated names.
25
   */
26
  public RKeyOperator() {
27
    // The keys are not delimited.
28
    super( "", "" );
29
  }
30
31
  /**
32
   * Transforms a definition key name into the expected format for an R
33
   * variable key name.
34
   * <p>
35
   * This algorithm is faster than {@link String#replace(char, char)}. Faster
36
   * still would be to cache the values, but that would mean managing the
37
   * cache when the user changes the beginning and ending of the R delimiters.
38
   * This code gives about a 2% performance boost when scrolling using
39
   * cursor keys. After the JIT warms up, this super-minor bottleneck vanishes.
40
   *
41
   * @param key The variable name to transform, neither blank nor {@code null}.
42
   * @return The transformed variable name.
43
   */
44
  @Override
45
  public String apply( final String key ) {
46
    assert key != null;
47
    assert !key.isBlank();
48
49
    return mVariables.computeIfAbsent( key, this::convert );
50
  }
51
52
  private String convert( final String key ) {
53
    mVarName.setLength( 0 );
54
    mVarName.append( "v" );
55
    mVarName.append( KEY_SEPARATOR_R );
56
    mVarName.append( key );
57
58
    // The 3 is for v$ + first char, which cannot be a separator.
59
    for( int i = mVarName.length() - 1; i >= 3; i-- ) {
60
      if( mVarName.charAt( i ) == KEY_SEPARATOR_DEF ) {
61
        mVarName.setCharAt( i, KEY_SEPARATOR_R );
62
      }
63
    }
64
65
    return mVarName.toString();
66
  }
67
}
168
A src/main/java/com/keenwrite/sigils/SigilKeyOperator.java
1
/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.sigils;
3
4
import java.util.function.UnaryOperator;
5
import java.util.regex.Matcher;
6
import java.util.regex.Pattern;
7
8
import static java.lang.String.format;
9
import static java.util.regex.Pattern.compile;
10
import static java.util.regex.Pattern.quote;
11
12
/**
13
 * Responsible for bracketing definition keys with token delimiters.
14
 */
15
public class SigilKeyOperator implements UnaryOperator<String> {
16
  private final String mBegan;
17
  private final String mEnded;
18
  private final Pattern mPattern;
19
20
  public SigilKeyOperator( final String began, final String ended ) {
21
    assert began != null;
22
    assert ended != null;
23
24
    mBegan = began;
25
    mEnded = ended;
26
    mPattern = compile( format( "%s(.*?)%s", quote( began ), quote( ended ) ) );
27
  }
28
29
  @Override
30
  public String apply( final String key ) {
31
    assert key != null;
32
    assert !key.startsWith( mBegan );
33
    assert !key.endsWith( mEnded );
34
35
    return mBegan + key + mEnded;
36
  }
37
38
  public Matcher match( final String text ) {
39
    return mPattern.matcher( text );
40
  }
41
}
142
A src/main/java/com/keenwrite/spelling/api/SpellCheckListener.java
1
/* Copyright 2020-2021 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-2021 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-2021 White Magic Software, Ltd. -- All rights reserved. */
2
3
/**
4
 * This package contains interfaces for spell checking implementations.
5
 */
6
package com.keenwrite.spelling.api;
17
A src/main/java/com/keenwrite/spelling/impl/Lexicon.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.spelling.impl;
6
7
import com.keenwrite.events.spelling.LexiconLoadedEvent;
8
import com.keenwrite.exceptions.MissingFileException;
9
10
import java.io.BufferedReader;
11
import java.io.InputStream;
12
import java.io.InputStreamReader;
13
import java.util.HashMap;
14
import java.util.Locale;
15
16
import static com.keenwrite.constants.Constants.LEXICONS_DIRECTORY;
17
import static com.keenwrite.events.StatusEvent.clue;
18
import static java.lang.String.format;
19
import static java.nio.charset.StandardCharsets.UTF_8;
20
21
/**
22
 * Responsible for loading a set of single words, asynchronously.
23
 */
24
public final class Lexicon {
25
  /**
26
   * Most lexicons have 100,000 words.
27
   */
28
  private static final int LEXICON_CAPACITY = 100_000;
29
30
  /**
31
   * The word-frequency entries are tab-delimited.
32
   */
33
  private static final char DELIMITER = '\t';
34
35
  /**
36
   * Load the lexicon into memory then fire an event indicating that the
37
   * word-frequency pairs are available to use for spellchecking. This
38
   * happens asynchronously so that the UI can load faster.
39
   *
40
   * @param locale The locale having a corresponding lexicon to load.
41
   */
42
  public static void read( final Locale locale ) {
43
    assert locale != null;
44
45
    new Thread( read( toResourcePath( locale ) ) ).start();
46
  }
47
48
  private static Runnable read( final String path ) {
49
    return () -> {
50
      try( final var resource = openResource( path ) ) {
51
        read( resource );
52
      } catch( final Exception ex ) {
53
        clue( ex );
54
      }
55
    };
56
  }
57
58
  private static void read( final InputStream resource ) {
59
    try( final var input = new InputStreamReader( resource, UTF_8 );
60
         final var reader = new BufferedReader( input ) ) {
61
      read( reader );
62
    } catch( final Exception ex ) {
63
      clue( ex );
64
    }
65
  }
66
67
  private static void read( final BufferedReader reader ) {
68
    try {
69
      long count = 0;
70
      final var lexicon = new HashMap<String, Long>( LEXICON_CAPACITY );
71
      String line;
72
73
      while( (line = reader.readLine()) != null ) {
74
        final var index = line.indexOf( DELIMITER );
75
        final var word = line.substring( 0, index == -1 ? 0 : index );
76
        final var frequency = parse( line.substring( index + 1 ) );
77
78
        lexicon.put( word, frequency );
79
80
        // Slower machines may benefit users by showing a loading message.
81
        if( ++count % 25_000 == 0 ) {
82
          status( "loading", count );
83
        }
84
      }
85
86
      // Indicate that loading the lexicon is finished.
87
      status( "loaded", count );
88
      LexiconLoadedEvent.fire( lexicon );
89
    } catch( final Exception ex ) {
90
      clue( ex );
91
    }
92
  }
93
94
  /**
95
   * Prevents autoboxing and uses cached values when possible. A return value
96
   * of 0L means that the word will receive the lowest priority. If there's
97
   * an error (i.e., data corruption) parsing the number, the spell checker
98
   * will still work, but be suboptimal for all erroneous entries.
99
   *
100
   * @param number The numeric value to parse into a long object.
101
   * @return The parsed value, or 0L if the number couldn't be parsed.
102
   */
103
  private static Long parse( final String number ) {
104
    try {
105
      return Long.valueOf( number );
106
    } catch( final NumberFormatException ex ) {
107
      clue( ex );
108
      return 0L;
109
    }
110
  }
111
112
  private static InputStream openResource( final String path )
113
    throws MissingFileException {
114
    final var resource = Lexicon.class.getResourceAsStream( path );
115
116
    if( resource == null ) {
117
      throw new MissingFileException( path );
118
    }
119
120
    return resource;
121
  }
122
123
  /**
124
   * Convert a {@link Locale} into a path that can be loaded as a resource.
125
   *
126
   * @param locale The {@link Locale} to convert to a resource.
127
   * @return The slash-separated path to a lexicon resource file.
128
   */
129
  private static String toResourcePath( final Locale locale ) {
130
    final var language = locale.getLanguage();
131
    return format( "/%s/%s.txt", LEXICONS_DIRECTORY, language );
132
  }
133
134
  private static void status( final String s, final long count ) {
135
    clue( format( "Main.status.lexicon.%s", s ), count );
136
  }
137
}
1138
A src/main/java/com/keenwrite/spelling/impl/PermissiveSpeller.java
1
/* Copyright 2020-2021 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 final 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-2021 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 io.gitlab.rxp90.jsymspell.SymSpell;
7
import io.gitlab.rxp90.jsymspell.SymSpellBuilder;
8
import io.gitlab.rxp90.jsymspell.Verbosity;
9
import io.gitlab.rxp90.jsymspell.api.SuggestItem;
10
import io.gitlab.rxp90.jsymspell.exceptions.NotInitializedException;
11
12
import java.text.BreakIterator;
13
import java.util.ArrayList;
14
import java.util.List;
15
import java.util.Map;
16
17
import static io.gitlab.rxp90.jsymspell.Verbosity.ALL;
18
import static io.gitlab.rxp90.jsymspell.Verbosity.CLOSEST;
19
import static java.lang.Character.isLetter;
20
21
/**
22
 * Responsible for spell checking using {@link SymSpell}.
23
 */
24
public class SymSpellSpeller implements SpellChecker {
25
  private final BreakIterator mBreakIterator = BreakIterator.getWordInstance();
26
  private final SymSpell mSymSpell;
27
28
  /**
29
   * Creates a new spellchecker for a lexicon of words in the specified file.
30
   *
31
   * @param lexicon The word-frequency map.
32
   * @return An instance of {@link SpellChecker} that can check if a word
33
   * is correct and suggest alternatives, or {@link PermissiveSpeller} if the
34
   * lexicon cannot be loaded.
35
   */
36
  public static SpellChecker forLexicon( final Map<String, Long> lexicon )
37
    throws NotInitializedException {
38
    assert lexicon != null;
39
    assert !lexicon.isEmpty();
40
41
    final var symSpell = new SymSpellBuilder()
42
      .setUnigramLexicon( lexicon )
43
      .build();
44
45
    return new SymSpellSpeller( symSpell );
46
  }
47
48
  /**
49
   * Prevent direct instantiation so that only the {@link SpellChecker}
50
   * interface is available.
51
   *
52
   * @param symSpell The implementation-specific spell checker.
53
   */
54
  private SymSpellSpeller( final SymSpell symSpell ) {
55
    assert symSpell != null;
56
57
    mSymSpell = symSpell;
58
  }
59
60
  /**
61
   * This expensive operation is only called for viable words, not for
62
   * single punctuation characters or whitespace.
63
   *
64
   * @param lexeme The word to check for correctness.
65
   * @return {@code false} if the word is not in the lexicon.
66
   */
67
  @Override
68
  public boolean inLexicon( final String lexeme ) {
69
    assert lexeme != null;
70
    assert !lexeme.isEmpty();
71
72
    final var words = lookup( lexeme, CLOSEST );
73
    return !words.isEmpty() && lexeme.equals( words.get( 0 ).getSuggestion() );
74
  }
75
76
  @Override
77
  public List<String> suggestions( final String lexeme, int count ) {
78
    assert lexeme != null;
79
    assert !lexeme.isEmpty();
80
81
    final List<String> result = new ArrayList<>( count );
82
83
    for( final var item : lookup( lexeme, ALL ) ) {
84
      if( count-- > 0 ) {
85
        result.add( item.getSuggestion() );
86
      }
87
      else {
88
        break;
89
      }
90
    }
91
92
    return result;
93
  }
94
95
  @Override
96
  public void proofread(
97
    final String text,
98
    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 =
109
        text.substring( previousIndex, boundaryIndex ).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
  /**
125
   * Answers whether the given string is likely a word by checking the first
126
   * character.
127
   *
128
   * @param word The word to check.
129
   * @return {@code true} if the word begins with a letter.
130
   */
131
  private boolean isWord( final String word ) {
132
    assert word != null;
133
134
    return !word.isBlank() && isLetter( word.charAt( 0 ) );
135
  }
136
137
  /**
138
   * Returns a list of {@link SuggestItem} instances that provide alternative
139
   * spellings for the given lexeme.
140
   *
141
   * @param lexeme A word to look up in the lexicon.
142
   * @param v      Influences the number of results returned.
143
   * @return Alternative lexemes.
144
   */
145
  private List<SuggestItem> lookup( final String lexeme, final Verbosity v ) {
146
    assert lexeme != null;
147
    assert v != null;
148
149
    return mSymSpell.lookup( lexeme, v );
150
  }
151
}
1152
A src/main/java/com/keenwrite/spelling/impl/package-info.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
3
/**
4
 * This package contains classes for spell checking implementations.
5
 */
6
package com.keenwrite.spelling.impl;
17
A src/main/java/com/keenwrite/typesetting/GuestTypesetter.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting;
3
4
import com.keenwrite.io.CommandNotFoundException;
5
import com.keenwrite.io.StreamGobbler;
6
import com.keenwrite.io.SysFile;
7
import com.keenwrite.typesetting.containerization.Podman;
8
import org.apache.commons.io.FilenameUtils;
9
10
import java.nio.file.Path;
11
import java.util.LinkedList;
12
import java.util.concurrent.Callable;
13
14
import static com.keenwrite.events.StatusEvent.clue;
15
import static com.keenwrite.io.StreamGobbler.gobble;
16
import static com.keenwrite.io.SysFile.normalize;
17
import static java.lang.String.format;
18
19
/**
20
 * Responsible for invoking a typesetter installed inside a container.
21
 */
22
public final class GuestTypesetter extends Typesetter
23
  implements Callable<Boolean> {
24
  private static final String SOURCE = "/root/source";
25
  private static final String TARGET = "/root/target";
26
  private static final String THEMES = "/root/themes";
27
  private static final String IMAGES = "/root/images";
28
  private static final String CACHES = "/root/caches";
29
  private static final String FONTS = "/root/fonts";
30
31
  private static final boolean READONLY = true;
32
  private static final boolean READWRITE = false;
33
34
  private static final String TYPESETTER_VERSION =
35
    String.format( "%s%s", TYPESETTER_EXE, " --version > /dev/null" );
36
37
  public GuestTypesetter( final Mutator mutator ) {
38
    super( mutator );
39
  }
40
41
  @Override
42
  public Boolean call() throws Exception {
43
    final var sourcePath = getSourcePath();
44
    final var targetPath = getTargetPath();
45
    final var themesPath = getThemeDir();
46
47
    final var sourceDir = normalize( sourcePath.getParent() );
48
    final var targetDir = normalize( targetPath.getParent() );
49
    final var themesDir = normalize( themesPath.getParent() );
50
    final var imagesDir = normalize( getImageDir() );
51
    final var cachesDir = normalize( getCacheDir() );
52
    final var fontsDir = normalize( getFontDir() );
53
54
    final var sourceFile = sourcePath.getFileName();
55
    final var targetFile = targetPath.getFileName();
56
    final var themesFile = themesPath.getFileName();
57
58
    final var manager = new Podman();
59
    manager.mount( sourceDir, SOURCE, READONLY );
60
    manager.mount( targetDir, TARGET, READWRITE );
61
    manager.mount( themesDir, THEMES, READONLY );
62
    manager.mount( imagesDir, IMAGES, READONLY );
63
    manager.mount( cachesDir, CACHES, READWRITE );
64
    manager.mount( fontsDir, FONTS, READONLY );
65
66
    final var args = new LinkedList<String>();
67
    args.add( TYPESETTER_EXE );
68
    args.addAll( commonOptions() );
69
    args.add( format(
70
      "--arguments=themesdir=%s/%s,imagesdir=%s,cachesdir=%s",
71
      THEMES, themesFile, IMAGES, CACHES
72
    ) );
73
    args.add( format( "--path='%s/%s'", THEMES, themesFile ) );
74
    args.add( format( "--result='%s'", removeExtension( targetFile ) ) );
75
    args.add( format( "%s/%s", SOURCE, sourceFile ) );
76
77
    final var listener = new PaginationListener();
78
    final var command = String.join( " ", args );
79
80
    manager.run( in -> StreamGobbler.gobble( in, listener ), command );
81
82
    return true;
83
  }
84
85
  static String removeExtension( final Path path ) {
86
    return FilenameUtils.removeExtension( SysFile.getFileName( path ) );
87
  }
88
89
  /**
90
   * @return {@code true} indicates that the containerized typesetter is
91
   * installed, properly configured, and ready to typeset documents.
92
   */
93
  static boolean isReady() {
94
    if( Podman.canRun() ) {
95
      final var exitCode = new StringBuilder();
96
      final var manager = new Podman();
97
98
      try {
99
        // Running blocks until the command completes.
100
        manager.run(
101
          input -> gobble( input, s -> exitCode.append( s.trim() ) ),
102
          String.format( "%s%s", TYPESETTER_VERSION, "; echo $?" )
103
        );
104
105
        // If the typesetter ran with an exit code of 0, it is available.
106
        return exitCode.indexOf( "0" ) == 0;
107
      } catch( final CommandNotFoundException ex ) {
108
        clue( ex );
109
      }
110
    }
111
112
    return false;
113
  }
114
}
1115
A src/main/java/com/keenwrite/typesetting/HostTypesetter.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting;
6
7
import com.keenwrite.collections.CircularQueue;
8
import com.keenwrite.io.StreamGobbler;
9
import com.keenwrite.io.SysFile;
10
11
import java.io.FileNotFoundException;
12
import java.io.IOException;
13
import java.nio.file.NoSuchFileException;
14
import java.nio.file.Path;
15
import java.util.ArrayList;
16
import java.util.List;
17
import java.util.concurrent.Callable;
18
19
import static com.keenwrite.constants.Constants.DEFAULT_DIRECTORY;
20
import static com.keenwrite.constants.Constants.TEMPORARY_DIRECTORY;
21
import static com.keenwrite.events.StatusEvent.clue;
22
import static com.keenwrite.io.SysFile.toFile;
23
import static java.lang.ProcessBuilder.Redirect.DISCARD;
24
import static java.nio.charset.StandardCharsets.UTF_8;
25
import static java.nio.file.Files.*;
26
import static java.util.Arrays.asList;
27
import static java.util.concurrent.TimeUnit.SECONDS;
28
import static org.apache.commons.io.FilenameUtils.removeExtension;
29
30
/**
31
 * Responsible for invoking an executable to typeset text. This will
32
 * construct suitable command-line arguments to invoke the typesetting engine.
33
 * This uses a version of the typesetter installed on the host system.
34
 */
35
public final class HostTypesetter extends Typesetter
36
  implements Callable<Boolean> {
37
  private static final SysFile TYPESETTER = new SysFile( TYPESETTER_EXE );
38
39
  HostTypesetter( final Mutator mutator ) {
40
    super( mutator );
41
  }
42
43
  /**
44
   * Answers whether the typesetting software is installed locally.
45
   *
46
   * @return {@code true} if the typesetting software is installed on the host.
47
   */
48
  public static boolean isReady() {
49
    return TYPESETTER.canRun();
50
  }
51
52
  /**
53
   * Launches a task to typeset a document.
54
   */
55
  private class TypesetTask implements Callable<Boolean> {
56
    private final List<String> mArgs = new ArrayList<>();
57
58
    /**
59
     * Working directory must be set because ConTeXt cannot write the
60
     * result to an arbitrary location.
61
     */
62
    private final Path mDirectory;
63
64
    private TypesetTask() {
65
      final var parentDir = getTargetPath().getParent();
66
      mDirectory = parentDir == null ? DEFAULT_DIRECTORY : parentDir;
67
    }
68
69
    /**
70
     * Initializes ConTeXt, which means creating the cache directory if it
71
     * doesn't already exist. The theme entry point must be named 'main.tex'.
72
     *
73
     * @return {@code true} if the cache directory exists.
74
     */
75
    private boolean reinitialize() {
76
      final var cacheExists = !isEmpty( getCacheDir().toPath() );
77
78
      // Ensure invoking multiple times will load the correct arguments.
79
      mArgs.clear();
80
      mArgs.add( TYPESETTER_EXE );
81
82
      if( cacheExists ) {
83
        mArgs.addAll( options() );
84
85
        final var sb = new StringBuilder( 128 );
86
        mArgs.forEach( arg -> sb.append( arg ).append( " " ) );
87
        clue( sb.toString() );
88
      }
89
      else {
90
        mArgs.add( "--generate" );
91
      }
92
93
      return cacheExists;
94
    }
95
96
    /**
97
     * Setting {@code TEXMFCACHE} when run on a fresh system fails on the first
98
     * try. If the cache directory doesn't exist, attempt to create it, then
99
     * call ConTeXt to generate the PDF. This is brittle because if the
100
     * directory is empty, or not populated with cached data, a false positive
101
     * will be returned, resulting in no PDF being created.
102
     *
103
     * @return {@code true} if the document was typeset successfully.
104
     * @throws IOException          If the process could not be started.
105
     * @throws InterruptedException If the process was killed.
106
     */
107
    private boolean typeset() throws IOException, InterruptedException {
108
      return reinitialize() ? call() : call() && reinitialize() && call();
109
    }
110
111
    @Override
112
    public Boolean call() throws IOException, InterruptedException {
113
      final var stdout = new CircularQueue<String>( 150 );
114
      final var builder = new ProcessBuilder( mArgs );
115
      builder.directory( toFile( mDirectory ) );
116
      builder.environment().put( "TEXMFCACHE", getCacheDir().toString() );
117
118
      // Without redirecting (or draining) stderr, the command may not
119
      // terminate successfully.
120
      builder.redirectError( DISCARD );
121
122
      final var process = builder.start();
123
      final var listener = new PaginationListener();
124
125
      // Slurp page numbers in a separate thread while typesetting.
126
      StreamGobbler.gobble( process.getInputStream(), line -> {
127
        listener.accept( line );
128
        stdout.add( line );
129
      } );
130
131
      // Even though the process has completed, there may be incomplete I/O.
132
      process.waitFor();
133
134
      // Allow time for any incomplete I/O to take place.
135
      process.waitFor( 1, SECONDS );
136
137
      final var exit = process.exitValue();
138
      process.destroy();
139
140
      // If there was an error, the typesetter will leave behind log, pdf, and
141
      // error files.
142
      if( exit > 0 ) {
143
        final var xmlName = SysFile.getFileName( getSourcePath() );
144
        final var srcName = SysFile.getFileName( getTargetPath() );
145
        final var logName = newExtension( xmlName, ".log" );
146
        final var errName = newExtension( xmlName, "-error.log" );
147
        final var pdfName = newExtension( xmlName, ".pdf" );
148
        final var tuaName = newExtension( xmlName, ".tua" );
149
        final var badName = newExtension( srcName, ".log" );
150
151
        log( badName );
152
        log( logName );
153
        log( errName );
154
        log( stdout.stream().toList() );
155
156
        // Users may opt to keep these files around for debugging purposes.
157
        if( autoRemove() ) {
158
          deleteIfExists( logName );
159
          deleteIfExists( errName );
160
          deleteIfExists( pdfName );
161
          deleteIfExists( badName );
162
          deleteIfExists( tuaName );
163
        }
164
      }
165
166
      // Exit value for a successful invocation of the typesetter. This value
167
      // is returned when creating the cache on the first run as well as
168
      // creating PDFs on subsequent runs (after the cache has been created).
169
      // Users don't care about exit codes, only whether the PDF was generated.
170
      return exit == 0;
171
    }
172
173
    private Path newExtension( final String baseName, final String ext ) {
174
      final var path = getTargetPath();
175
      return path.resolveSibling( removeExtension( baseName ) + ext );
176
    }
177
178
    /**
179
     * Fires a status message for each line in the given file. The file format
180
     * is somewhat machine-readable, but no effort beyond line splitting is
181
     * made to parse the text.
182
     *
183
     * @param path Path to the file containing error messages.
184
     */
185
    private void log( final Path path ) throws IOException {
186
      if( exists( path ) ) {
187
        log( readAllLines( path, UTF_8 ) );
188
      }
189
    }
190
191
    private void log( final List<String> lines ) {
192
      final var splits = new ArrayList<String>( lines.size() * 2 );
193
194
      for( final var line : lines ) {
195
        splits.addAll( asList( line.split( "\\\\n" ) ) );
196
      }
197
198
      clue( splits );
199
    }
200
201
    /**
202
     * Returns the location of the cache directory.
203
     *
204
     * @return A fully qualified path to the location to store temporary
205
     * files between typesetting runs.
206
     */
207
    @SuppressWarnings( "SpellCheckingInspection" )
208
    private java.io.File getCacheDir() {
209
      final var cache = Path.of( TEMPORARY_DIRECTORY, "luatex-cache" );
210
      return toFile( cache );
211
    }
212
213
    /**
214
     * Answers whether the given directory is empty. The typesetting software
215
     * creates a non-empty directory by default. The return value from this
216
     * method is a proxy to answering whether the typesetter has been run for
217
     * the first time or not.
218
     *
219
     * @param path The directory to check for emptiness.
220
     * @return {@code true} if the directory is empty.
221
     */
222
    private boolean isEmpty( final Path path ) {
223
      try( final var stream = newDirectoryStream( path ) ) {
224
        return !stream.iterator().hasNext();
225
      } catch( final NoSuchFileException | FileNotFoundException ex ) {
226
        // A missing directory means it doesn't exist, ergo is empty.
227
        return true;
228
      } catch( final IOException ex ) {
229
        throw new RuntimeException( ex );
230
      }
231
    }
232
  }
233
234
  /**
235
   * This will typeset the document using a new process. The return value only
236
   * indicates whether the typesetter exists, not whether the typesetting was
237
   * successful. The typesetter must be known to exist prior to calling this
238
   * method.
239
   *
240
   * @throws IOException                 If the process could not be started.
241
   * @throws InterruptedException        If the process was killed.
242
   * @throws TypesetterNotFoundException When no typesetter is along the PATH.
243
   */
244
  @Override
245
  public Boolean call()
246
    throws IOException, InterruptedException, TypesetterNotFoundException {
247
    final var task = new HostTypesetter.TypesetTask();
248
    return task.typeset();
249
  }
250
}
1251
A src/main/java/com/keenwrite/typesetting/PaginationListener.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting;
3
4
import java.util.Scanner;
5
import java.util.function.Consumer;
6
import java.util.regex.Pattern;
7
8
import static com.keenwrite.events.StatusEvent.clue;
9
10
/**
11
 * Responsible for parsing the output from the typesetting engine and
12
 * updating the status bar to provide assurance that typesetting is
13
 * executing.
14
 *
15
 * <p>
16
 * Example lines written to standard output:
17
 * </p>
18
 * <pre>{@code
19
 * pages           > flushing realpage 15, userpage 15, subpage 15
20
 * pages           > flushing realpage 16, userpage 16, subpage 16
21
 * pages           > flushing realpage 1, userpage 1, subpage 1
22
 * pages           > flushing realpage 2, userpage 2, subpage 2
23
 * }</pre>
24
 * <p>
25
 * The lines are parsed; the first number is displayed as a status bar
26
 * message.
27
 * </p>
28
 */
29
class PaginationListener implements Consumer<String> {
30
  private static final Pattern DIGITS = Pattern.compile( "\\D+" );
31
32
  private int mPageCount = 1;
33
  private int mPassCount = 1;
34
  private int mPageTotal = 0;
35
36
  public PaginationListener() { }
37
38
  @Override
39
  public void accept( final String line ) {
40
    if( line.startsWith( "pages" ) ) {
41
      final var scanner = new Scanner( line ).useDelimiter( DIGITS );
42
      final var digits = scanner.next();
43
      final var page = Integer.parseInt( digits );
44
45
      // If the page number is less than the previous page count, it
46
      // means that the typesetting engine has started another pass.
47
      if( page < mPageCount ) {
48
        mPassCount++;
49
        mPageTotal = mPageCount;
50
      }
51
52
      mPageCount = page;
53
54
      // Inform the user of pages being typeset.
55
      clue( "Main.status.typeset.page",
56
            mPageCount, mPageTotal < 1 ? "?" : mPageTotal, mPassCount
57
      );
58
    }
59
  }
60
}
161
A src/main/java/com/keenwrite/typesetting/Typesetter.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting;
6
7
import com.keenwrite.util.GenericBuilder;
8
import com.keenwrite.util.Time;
9
10
import java.nio.file.Path;
11
import java.time.Duration;
12
import java.util.LinkedList;
13
import java.util.List;
14
import java.util.concurrent.Callable;
15
16
import static com.keenwrite.Bootstrap.USER_CACHE_DIR;
17
import static com.keenwrite.constants.Constants.USER_DIRECTORY;
18
import static com.keenwrite.constants.Constants.getFontDirectory;
19
import static com.keenwrite.events.StatusEvent.clue;
20
import static com.keenwrite.util.Time.toElapsedTime;
21
import static java.lang.String.format;
22
import static java.lang.System.currentTimeMillis;
23
import static java.time.Duration.ofMillis;
24
25
/**
26
 * Responsible for typesetting a document using either a typesetter installed
27
 * on the computer ({@link HostTypesetter} or installed within a container
28
 * ({@link GuestTypesetter}).
29
 */
30
@SuppressWarnings( "SpellCheckingInspection" )
31
public class Typesetter {
32
  /**
33
   * Name of the executable program that can typeset documents.
34
   */
35
  static final String TYPESETTER_EXE = "mtxrun";
36
37
  public static GenericBuilder<Mutator, Typesetter> builder() {
38
    return GenericBuilder.of( Mutator::new, Typesetter::new );
39
  }
40
41
  public static final class Mutator {
42
    private Path mSourcePath;
43
    private Path mTargetPath;
44
    private Path mThemeDir = USER_DIRECTORY.toPath();
45
    private Path mImageDir = USER_DIRECTORY.toPath();
46
    private Path mCacheDir = USER_CACHE_DIR.toPath();
47
    private Path mFontDir = getFontDirectory().toPath();
48
    private String mModesEnabled = "";
49
    private boolean mAutoRemove;
50
51
    /**
52
     * @param inputPath The input document to typeset.
53
     */
54
    public void setSourcePath( final Path inputPath ) {
55
      mSourcePath = inputPath;
56
    }
57
58
    /**
59
     * @param outputPath Path to the finished typeset document to create.
60
     */
61
    public void setTargetPath( final Path outputPath ) {
62
      mTargetPath = outputPath;
63
    }
64
65
    /**
66
     * @param themeDir Fully qualified path to the theme directory, which
67
     *                 ends with the selected theme name.
68
     */
69
    public void setThemeDir( final Path themeDir ) {
70
      mThemeDir = themeDir;
71
    }
72
73
    /**
74
     * @param imageDir Fully qualified path to the "images" directory.
75
     */
76
    public void setImageDir( final Path imageDir ) {
77
      mImageDir = imageDir;
78
    }
79
80
    /**
81
     * @param cacheDir Fully qualified path to the "caches" directory.
82
     */
83
    public void setCacheDir( final Path cacheDir ) {
84
      mCacheDir = cacheDir;
85
    }
86
87
    /**
88
     * @param fontDir Fully qualified path to the "fonts" directory.
89
     */
90
    public void setFontDir( final Path fontDir ) {
91
      mFontDir = fontDir;
92
    }
93
94
    public void setModesEnabled( final String modesEnabled ) {
95
      mModesEnabled = modesEnabled;
96
    }
97
98
    /**
99
     * @param remove {@code true} to remove all temporary files after the
100
     *               typesetter produces a PDF file.
101
     */
102
    public void setAutoRemove( final boolean remove ) {
103
      mAutoRemove = remove;
104
    }
105
106
    public Path getSourcePath() {
107
      return mSourcePath;
108
    }
109
110
    public Path getTargetPath() {
111
      return mTargetPath;
112
    }
113
114
    public Path getThemeDir() {
115
      return mThemeDir;
116
    }
117
118
    public Path getImageDir() {
119
      return mImageDir;
120
    }
121
122
    public Path getCacheDir() {
123
      return mCacheDir;
124
    }
125
126
    public Path getFontDir() {
127
      return mFontDir;
128
    }
129
130
    public String getModesEnabled() {
131
      return mModesEnabled;
132
    }
133
134
    public boolean isAutoRemove() {
135
      return mAutoRemove;
136
    }
137
  }
138
139
  private final Mutator mMutator;
140
141
  /**
142
   * Creates a new {@link Typesetter} instance capable of configuring the
143
   * typesetter used to generate a typeset document.
144
   */
145
  Typesetter( final Mutator mutator ) {
146
    assert mutator != null;
147
148
    mMutator = mutator;
149
  }
150
151
  public void typeset() throws Exception {
152
    final Callable<Boolean> typesetter;
153
154
    if( HostTypesetter.isReady() ) {
155
      typesetter = new HostTypesetter( mMutator );
156
    }
157
    else if( GuestTypesetter.isReady() ) {
158
      typesetter = new GuestTypesetter( mMutator );
159
    }
160
    else {
161
      throw new TypesetterNotFoundException( TYPESETTER_EXE );
162
    }
163
164
    final var outputPath = getTargetPath();
165
    final var prefix = "Main.status.typeset.";
166
167
    clue( format( "%s%s", prefix, "began" ), outputPath );
168
169
    final var time = currentTimeMillis();
170
    final var success = typesetter.call();
171
    final var suffix = success ? "success" : "failure";
172
173
    clue( format( "%sended.%s", prefix, suffix ), outputPath, since( time ) );
174
  }
175
176
  /**
177
   * Generates command-line arguments used to invoke the typesetter.
178
   */
179
  @SuppressWarnings( "SpellCheckingInspection" )
180
  List<String> options() {
181
    final var args = commonOptions();
182
183
    final var sourcePath = getSourcePath().toString();
184
    final var targetPath = getTargetPath().getFileName();
185
    final var themesPath = getThemeDir();
186
    final var imagesPath = getImageDir();
187
    final var cachesPath = getCacheDir();
188
189
    args.add(
190
      format(
191
        "--arguments=themesdir=%s,imagesdir=%s,cachesdir=%s",
192
        themesPath, imagesPath, cachesPath
193
      )
194
    );
195
    args.add( format( "--path='%s'", themesPath ) );
196
    args.add( format( "--result='%s'", targetPath ) );
197
    args.add( sourcePath );
198
199
    final var modesEnabled = getModesEnabled();
200
201
    if( !modesEnabled.isBlank() ) {
202
      args.add( format( "--mode=%s", modesEnabled ) );
203
    }
204
205
    return args;
206
  }
207
208
  List<String> commonOptions() {
209
    final var args = new LinkedList<String>();
210
211
    args.add( "--autogenerate" );
212
    args.add( "--script" );
213
    args.add( "mtx-context" );
214
    args.add( "--batchmode" );
215
    args.add( "--nonstopmode" );
216
    args.add( "--purgeall" );
217
    args.add( "--environment='main'" );
218
219
    return args;
220
  }
221
222
  protected Path getSourcePath() {
223
    return mMutator.getSourcePath();
224
  }
225
226
  protected Path getTargetPath() {
227
    return mMutator.getTargetPath();
228
  }
229
230
  protected Path getThemeDir() {
231
    return mMutator.getThemeDir();
232
  }
233
234
  protected Path getImageDir() {
235
    return mMutator.getImageDir();
236
  }
237
238
  protected Path getCacheDir() {
239
    return mMutator.getCacheDir();
240
  }
241
242
  protected Path getFontDir() {
243
    return mMutator.getFontDir();
244
  }
245
246
  protected String getModesEnabled() {
247
    return mMutator.getModesEnabled();
248
  }
249
250
  /**
251
   * Answers whether logs and other files should be deleted upon error. The
252
   * log files are useful for debugging.
253
   *
254
   * @return {@code true} to delete generated files.
255
   */
256
  public boolean autoRemove() {
257
    return mMutator.isAutoRemove();
258
  }
259
260
  public static boolean canRun() {
261
    return hostCanRun() || guestCanRun();
262
  }
263
264
  private static boolean hostCanRun() {
265
    return HostTypesetter.isReady();
266
  }
267
268
  private static boolean guestCanRun() {
269
    return GuestTypesetter.isReady();
270
  }
271
272
  /**
273
   * Calculates the time that has elapsed from the current time to the
274
   * given moment in time.
275
   *
276
   * @param start The starting time, which should be before the current time.
277
   * @return A human-readable formatted time.
278
   * @see Time#toElapsedTime(Duration)
279
   */
280
  private static String since( final long start ) {
281
    return toElapsedTime( ofMillis( currentTimeMillis() - start ) );
282
  }
283
}
1284
A src/main/java/com/keenwrite/typesetting/TypesetterNotFoundException.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting;
3
4
/**
5
 * Responsible for creating an alternate execution path when a typesetter
6
 * cannot be found.
7
 */
8
public class TypesetterNotFoundException extends RuntimeException {
9
  /**
10
   * Constructs a new exception that indicates the typesetting engine cannot
11
   * be found anywhere along the PATH.
12
   *
13
   * @param name Typesetter executable file name.
14
   */
15
  public TypesetterNotFoundException( final String name ) {
16
    super( name );
17
  }
18
}
119
A src/main/java/com/keenwrite/typesetting/containerization/ContainerManager.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.containerization;
6
7
import com.keenwrite.io.CommandNotFoundException;
8
9
import java.io.File;
10
import java.io.IOException;
11
import java.util.List;
12
13
public interface ContainerManager {
14
  /**
15
   * Installs the container software, in quiet and headless mode if possible.
16
   *
17
   * @param exe The installer binary to run.
18
   * @return The exit code from the installer program, or -1 on failure.
19
   * @throws IOException The container installer could not be run.
20
   */
21
  int install( final File exe )
22
    throws IOException;
23
24
  /**
25
   * Runs preliminary commands against the container before starting.
26
   *
27
   * @param processor Processes the command output (in a separate thread).
28
   * @throws CommandNotFoundException The container executable was not found.
29
   */
30
  void start( StreamProcessor processor ) throws CommandNotFoundException;
31
32
  /**
33
   * Requests that the container manager load an image into the container.
34
   *
35
   * @param processor Processes the command output (in a separate thread).
36
   * @throws CommandNotFoundException The container executable was not found.
37
   */
38
  void load( StreamProcessor processor )
39
    throws CommandNotFoundException;
40
41
  /**
42
   * Runs a command using the container manager.
43
   *
44
   * @param processor Processes the command output (in a separate thread).
45
   * @param args      The command and arguments to run.
46
   * @return The exit code returned by the installer program.
47
   * @throws CommandNotFoundException The container executable was not found.
48
   */
49
  int run( StreamProcessor processor, String... args )
50
    throws CommandNotFoundException;
51
52
  /**
53
   * Convenience method to run a command using the container manager.
54
   *
55
   * @see #run(StreamProcessor, String...)
56
   */
57
  default int run( final StreamProcessor listener, final List<String> args )
58
    throws CommandNotFoundException {
59
    return run( listener, toArray( args ) );
60
  }
61
62
  /**
63
   * Convenience method to convert a {@link List} into an array.
64
   *
65
   * @param list The elements to convert to an array.
66
   * @return The converted {@link List}.
67
   */
68
  default String[] toArray( final List<String> list ) {
69
    return list.toArray( new String[ 0 ] );
70
  }
71
}
172
A src/main/java/com/keenwrite/typesetting/containerization/Podman.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.containerization;
6
7
import com.keenwrite.Messages;
8
import com.keenwrite.io.CommandNotFoundException;
9
import com.keenwrite.io.SysFile;
10
11
import java.io.File;
12
import java.nio.file.Files;
13
import java.nio.file.Path;
14
import java.util.LinkedList;
15
import java.util.List;
16
17
import static com.keenwrite.events.StatusEvent.clue;
18
import static com.keenwrite.io.SysFile.toFile;
19
import static com.keenwrite.util.SystemUtils.IS_OS_WINDOWS;
20
import static java.lang.String.format;
21
import static java.lang.String.join;
22
import static java.lang.System.arraycopy;
23
import static java.util.Arrays.copyOf;
24
25
/**
26
 * Provides facilities for interacting with a container environment.
27
 */
28
public final class Podman implements ContainerManager {
29
  private static final String BINARY = "podman";
30
  private static final Path BINARY_PATH =
31
    Path.of(
32
      format( IS_OS_WINDOWS
33
                ? "C:\\Program Files\\RedHat\\Podman\\%s.exe"
34
                : "/usr/bin/%s",
35
              BINARY
36
      )
37
    );
38
  private static final SysFile MANAGER = new SysFile( BINARY );
39
40
  private final List<String> mMountPoints = new LinkedList<>();
41
42
  public Podman() { }
43
44
  /**
45
   * Answers whether the container is installed and runnable on the host.
46
   *
47
   * @return {@code true} if the container is available.
48
   */
49
  public static boolean canRun() {
50
    try {
51
      return toFile( getExecutable() ).isFile();
52
    } catch( final Exception ex ) {
53
      clue( "Wizard.container.executable.run.error", ex );
54
55
      // If the binary couldn't be found, then indicate that it cannot run.
56
      return false;
57
    }
58
  }
59
60
  private static Path getExecutable() {
61
    final var executable = Files.isExecutable( BINARY_PATH );
62
63
    clue( "Wizard.container.executable.run.scan", BINARY_PATH, executable );
64
65
    return executable
66
      ? BINARY_PATH
67
      : MANAGER.locate().orElseThrow();
68
  }
69
70
  @Override
71
  public int install( final File exe ) {
72
    // This monstrosity runs the installer in the background without displaying
73
    // a secondary command window, while blocking until the installer completes
74
    // and an exit code can be determined. I hate Windows.
75
    final var cmd = format(
76
      "start /b /high /wait cmd /c %s /quiet /install & exit ^!errorlevel^!",
77
      exe.getAbsolutePath()
78
    );
79
80
    clue( "Wizard.container.install.command", cmd );
81
82
    final var builder = processBuilder( "cmd", "/c", cmd );
83
84
    try {
85
      clue( "Wizard.container.install.await", cmd );
86
87
      // Wait for installation to finish (successfully or not).
88
      return wait( builder.start() );
89
    } catch( final Exception ignored ) {
90
      return -1;
91
    }
92
  }
93
94
  @Override
95
  public void start( final StreamProcessor processor )
96
    throws CommandNotFoundException {
97
    machine( processor, "stop" );
98
    podman( processor, "system", "prune", "--force" );
99
    machine( processor, "rm", "--force" );
100
    machine( processor, "init" );
101
    machine( processor, "start" );
102
  }
103
104
  @Override
105
  public void load( final StreamProcessor processor )
106
    throws CommandNotFoundException {
107
    final var url = Messages.get( "Wizard.typesetter.container.image.url" );
108
109
    podman( processor, "load", "-i", url );
110
  }
111
112
  /**
113
   * Runs:
114
   * <p>
115
   * <code>podman run --network=host --rm -t IMAGE /bin/sh -lc</code>
116
   * </p>
117
   * followed by the given arguments.
118
   *
119
   * @param args The command and arguments to run against the container.
120
   * @return The exit code from running the container manager (not the
121
   * exit code from running the command).
122
   * @throws CommandNotFoundException Container manager couldn't be found.
123
   */
124
  @Override
125
  public int run(
126
    final StreamProcessor processor,
127
    final String... args ) throws CommandNotFoundException {
128
    final var tag = Messages.get( "Wizard.typesetter.container.image.tag" );
129
130
    final var options = new LinkedList<String>();
131
    options.add( "run" );
132
    options.add( "--rm" );
133
    options.add( "--network=host" );
134
    options.addAll( mMountPoints );
135
    options.add( "-t" );
136
    options.add( tag );
137
    options.add( "/bin/sh" );
138
    options.add( "-lc" );
139
140
    final var command = toArray( toArray( options ), args );
141
    return podman( processor, command );
142
  }
143
144
  /**
145
   * Generates a command-line argument representing a mount point between
146
   * the host and guest systems.
147
   *
148
   * @param hostDir  The host directory to mount in the container.
149
   * @param guestDir The guest directory to map from the container to host.
150
   * @param readonly Set {@code true} to make the mount point read-only.
151
   */
152
  public void mount(
153
    final Path hostDir, final String guestDir, final boolean readonly ) {
154
    assert hostDir != null;
155
    assert guestDir != null;
156
    assert !guestDir.isBlank();
157
    assert toFile( hostDir ).isDirectory();
158
159
    mMountPoints.add(
160
      format( "-v%s:%s:%s", hostDir, guestDir, readonly ? "ro" : "Z" )
161
    );
162
  }
163
164
  private static void machine(
165
    final StreamProcessor processor,
166
    final String... args )
167
    throws CommandNotFoundException {
168
    podman( processor, toArray( "machine", args ) );
169
  }
170
171
  private static int podman(
172
    final StreamProcessor processor, final String... args )
173
    throws CommandNotFoundException {
174
    try {
175
      final var path = getExecutable();
176
      final var joined = join( ",", args );
177
178
      clue( "Wizard.container.process.enter", path, joined );
179
180
      final var builder = processBuilder( path, args );
181
      final var process = builder.start();
182
183
      processor.start( process.getInputStream() );
184
185
      return wait( process );
186
    } catch( final Exception ex ) {
187
      clue( ex );
188
      throw new CommandNotFoundException( MANAGER.toString() );
189
    }
190
  }
191
192
  /**
193
   * Performs a blocking wait until the {@link Process} completes.
194
   *
195
   * @param process The {@link Process} to await completion.
196
   * @return The exit code from running a command.
197
   * @throws InterruptedException The {@link Process} was interrupted.
198
   */
199
  private static int wait( final Process process ) throws InterruptedException {
200
    final var exitCode = process.waitFor();
201
202
    clue( "Wizard.container.process.exit", exitCode );
203
204
    process.destroy();
205
206
    return exitCode;
207
  }
208
209
  private static ProcessBuilder processBuilder( final String... args ) {
210
    final var builder = new ProcessBuilder( args );
211
    builder.redirectErrorStream( true );
212
213
    return builder;
214
  }
215
216
  private static ProcessBuilder processBuilder(
217
    final File file, final String... s ) {
218
    return processBuilder( toArray( file.getAbsolutePath(), s ) );
219
  }
220
221
  private static ProcessBuilder processBuilder(
222
    final Path path, final String... s ) {
223
    return processBuilder( toFile( path ), s );
224
  }
225
226
  /**
227
   * Merges two arrays into a single array.
228
   *
229
   * @param first  The first array to merge before the second array.
230
   * @param second The second array to merge after the first array.
231
   * @param <T>    The type of arrays to merge.
232
   * @return The merged arrays, with the first array elements preceding the
233
   * second array's elements.
234
   */
235
  private static <T> T[] toArray( final T[] first, final T[] second ) {
236
    assert first != null;
237
    assert second != null;
238
    assert first.length > 0;
239
    assert second.length > 0;
240
241
    final var merged = copyOf( first, first.length + second.length );
242
    arraycopy( second, 0, merged, first.length, second.length );
243
    return merged;
244
  }
245
246
  /**
247
   * Convenience method to merge a single string with an array of strings.
248
   *
249
   * @param first  The first item to prepend to the secondary items.
250
   * @param second The second item to combine with the first item.
251
   * @return A new array with the first element at index 0 and the second
252
   * elements starting at index 1.
253
   */
254
  private static String[] toArray( final String first, String... second ) {
255
    assert first != null;
256
    assert second != null;
257
    assert second.length > 0;
258
259
    return toArray( new String[]{first}, second );
260
  }
261
}
1262
A src/main/java/com/keenwrite/typesetting/containerization/StreamProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.containerization;
3
4
import java.io.InputStream;
5
import java.io.PipedInputStream;
6
import java.io.PipedOutputStream;
7
8
/**
9
 * Implementations receive an {@link InputStream} for reading, which happens
10
 * on a separate thread. Implementations are responsible for starting the
11
 * thread. This class helps avoid relying on {@link PipedInputStream} and
12
 * {@link PipedOutputStream} to connect the {@link InputStream} from an
13
 * instance of {@link ProcessBuilder} to process standard output and standard
14
 * error for a running command.
15
 */
16
@FunctionalInterface
17
public interface StreamProcessor {
18
  /**
19
   * Processes the given {@link InputStream} on a separate thread.
20
   */
21
  void start( InputStream in );
22
}
123
A src/main/java/com/keenwrite/typesetting/installer/TypesetterInstaller.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.installer;
6
7
import com.keenwrite.events.ExportFailedEvent;
8
import com.keenwrite.preferences.AppKeys;
9
import com.keenwrite.preferences.Workspace;
10
import com.keenwrite.typesetting.installer.panes.*;
11
import org.controlsfx.dialog.Wizard;
12
import org.greenrobot.eventbus.Subscribe;
13
14
import java.util.LinkedList;
15
16
import static com.keenwrite.Messages.get;
17
import static com.keenwrite.events.Bus.register;
18
import static com.keenwrite.util.SystemUtils.*;
19
20
/**
21
 * Responsible for installing the typesetting system and all its requirements.
22
 */
23
public final class TypesetterInstaller {
24
  private final Workspace mWorkspace;
25
26
  /**
27
   * Registers for the {@link ExportFailedEvent}, which, when received,
28
   * indicates that the typesetting software must be installed.
29
   *
30
   * @param workspace To set {@link AppKeys#KEY_TYPESET_CONTEXT_THEMES_PATH} via
31
   *                  {@link TypesetterThemesDownloadPane}.
32
   */
33
  public TypesetterInstaller( final Workspace workspace ) {
34
    assert workspace != null;
35
36
    mWorkspace = workspace;
37
38
    register( this );
39
  }
40
41
  @Subscribe
42
  @SuppressWarnings( "unused" )
43
  public void handle( final ExportFailedEvent failedEvent ) {
44
    final var wizard = wizard();
45
46
    wizard.showAndWait();
47
  }
48
49
  private Wizard wizard() {
50
    final var title = get( "Wizard.typesetter.all.1.install.title" );
51
    final var wizard = new Wizard( this, title );
52
    final var wizardFlow = wizardFlow();
53
54
    wizard.setFlow( wizardFlow );
55
56
    return wizard;
57
  }
58
59
  private Wizard.Flow wizardFlow() {
60
    final var panels = wizardPanes();
61
    return new Wizard.LinearFlow( panels );
62
  }
63
64
  private InstallerPane[] wizardPanes() {
65
    final var panes = new LinkedList<InstallerPane>();
66
67
    // STEP 1: Introduction panel (all)
68
    panes.add( new IntroductionPane() );
69
70
    if( IS_OS_WINDOWS ) {
71
      // STEP 2 a: Download container (Windows)
72
      panes.add( new WindowsManagerDownloadPane() );
73
      // STEP 2 b: Install container (Windows)
74
      panes.add( new WindowsManagerInstallPane() );
75
    }
76
    else if( IS_OS_UNIX ) {
77
      // STEP 2: Install container (Unix)
78
      panes.add( new UnixManagerInstallPane() );
79
    }
80
    else {
81
      // STEP 2: Install container (other)
82
      panes.add( new UniversalManagerInstallPane() );
83
    }
84
85
    if( !IS_OS_LINUX ) {
86
      // STEP 3: Initialize container (all except Linux)
87
      panes.add( new ManagerInitializationPane() );
88
    }
89
90
    // STEP 4: Install typesetter container image (all)
91
    panes.add( new TypesetterImageDownloadPane() );
92
93
    // STEP 5: Download and install typesetter themes (all)
94
    panes.add( new TypesetterThemesDownloadPane( mWorkspace ) );
95
96
    return panes.toArray( InstallerPane[]::new );
97
  }
98
}
199
A src/main/java/com/keenwrite/typesetting/installer/panes/AbstractDownloadPane.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.installer.panes;
6
7
import com.keenwrite.io.SysFile;
8
import javafx.collections.ObservableMap;
9
import javafx.concurrent.Task;
10
import javafx.scene.control.Label;
11
import javafx.scene.layout.BorderPane;
12
import org.controlsfx.dialog.Wizard;
13
14
import java.io.File;
15
import java.net.URI;
16
17
import static com.keenwrite.Bootstrap.USER_DATA_DIR;
18
import static com.keenwrite.Messages.get;
19
import static com.keenwrite.Messages.getUri;
20
import static com.keenwrite.events.StatusEvent.clue;
21
import static com.keenwrite.io.SysFile.toFile;
22
import static com.keenwrite.io.downloads.DownloadManager.downloadAsync;
23
import static com.keenwrite.io.downloads.DownloadManager.toFilename;
24
import static java.lang.String.*;
25
26
/**
27
 * Responsible for asynchronous downloads.
28
 */
29
public abstract class AbstractDownloadPane extends InstallerPane {
30
  private static final String STATUS = ".status";
31
32
  private final Label mStatus;
33
  private final File mTarget;
34
  private final String mFilename;
35
  private final URI mUri;
36
37
  public AbstractDownloadPane() {
38
    mUri = getUri( format( "%s%s", getPrefix(), ".download.link.url" ) );
39
    mFilename = toFilename( mUri );
40
    final var directory = USER_DATA_DIR;
41
    mTarget = toFile( directory.resolve( mFilename ) );
42
    final var source = labelf(
43
      format( "%s%s", getPrefix(), ".paths" ), mFilename, directory
44
    );
45
    mStatus = labelf( format( "%s%s.progress", getPrefix(), STATUS ), 0, 0 );
46
47
    final var border = new BorderPane();
48
    border.setTop( source );
49
    border.setCenter( spacer() );
50
    border.setBottom( mStatus );
51
52
    setContent( border );
53
  }
54
55
  @Override
56
  public void onEnteringPage( final Wizard wizard ) {
57
    disableNext( true );
58
59
    final var threadName = getClass().getCanonicalName();
60
    final var properties = wizard.getProperties();
61
    final var thread = properties.get( threadName );
62
63
    if( thread instanceof Task<?> downloader && downloader.isRunning() ) {
64
      clue( "Wizard.container.install.download.running" );
65
      return;
66
    }
67
68
    updateProperties( properties );
69
70
    final var target = getTarget();
71
    final var sysFile = new SysFile( target );
72
    final var checksum = getChecksum();
73
74
    if( sysFile.exists() ) {
75
      final var checksumOk = sysFile.isChecksum( checksum );
76
      final var suffix = checksumOk ? ".ok" : ".no";
77
78
      updateStatus( format( "%s.checksum%s", STATUS, suffix ), mFilename );
79
      disableNext( !checksumOk );
80
    }
81
    else {
82
      clue( "Wizard.container.install.download.started", mUri );
83
84
      final var task = downloadAsync( mUri, target, ( progress, bytes ) -> {
85
        final var suffix = progress < 0 ? ".bytes" : ".progress";
86
87
        updateStatus( STATUS + suffix, progress, bytes );
88
      } );
89
90
      properties.put( threadName, task );
91
92
      task.setOnSucceeded( _ -> onDownloadSucceeded( threadName, properties ) );
93
      task.setOnFailed( _ -> onDownloadFailed( threadName, properties ) );
94
      task.setOnCancelled( _ -> onDownloadFailed( threadName, properties ) );
95
    }
96
  }
97
98
  protected void updateProperties(
99
    final ObservableMap<Object, Object> properties ) {
100
  }
101
102
  @Override
103
  protected String getHeaderKey() {
104
    return format( "%s%s", getPrefix(), ".header" );
105
  }
106
107
  protected File getTarget() {
108
    return mTarget;
109
  }
110
111
  protected abstract String getChecksum();
112
113
  protected abstract String getPrefix();
114
115
  protected void onDownloadSucceeded(
116
    final String threadName, final ObservableMap<Object, Object> properties ) {
117
    updateStatus( format( "%s%s", STATUS, ".success" ) );
118
    properties.remove( threadName );
119
    disableNext( false );
120
  }
121
122
  protected void onDownloadFailed(
123
    final String threadName, final ObservableMap<Object, Object> properties ) {
124
    updateStatus( format( "%s%s", STATUS, ".failure" ) );
125
    properties.remove( threadName );
126
  }
127
128
  protected void updateStatus( final String suffix, final Object... args ) {
129
    update( mStatus, get( getPrefix() + suffix, args ) );
130
  }
131
132
  protected void deleteTarget() {
133
    if( !getTarget().delete() ) {
134
      clue( "Main.status.error.file.delete", getTarget() );
135
    }
136
  }
137
}
1138
A src/main/java/com/keenwrite/typesetting/installer/panes/InstallerPane.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.installer.panes;
6
7
import com.keenwrite.events.HyperlinkOpenEvent;
8
import com.keenwrite.typesetting.containerization.ContainerManager;
9
import com.keenwrite.typesetting.containerization.Podman;
10
import javafx.animation.Animation;
11
import javafx.animation.RotateTransition;
12
import javafx.geometry.Insets;
13
import javafx.scene.Node;
14
import javafx.scene.control.*;
15
import javafx.scene.image.ImageView;
16
import javafx.scene.layout.BorderPane;
17
import javafx.scene.layout.FlowPane;
18
import javafx.scene.layout.Pane;
19
import org.controlsfx.dialog.Wizard;
20
import org.controlsfx.dialog.WizardPane;
21
22
import static com.keenwrite.Messages.get;
23
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
24
import static com.keenwrite.io.downloads.DownloadManager.createTask;
25
import static com.keenwrite.io.downloads.DownloadManager.createThread;
26
import static java.lang.System.lineSeparator;
27
import static javafx.animation.Interpolator.LINEAR;
28
import static javafx.application.Platform.runLater;
29
import static javafx.scene.control.ButtonBar.ButtonData.NEXT_FORWARD;
30
import static javafx.scene.control.ContentDisplay.RIGHT;
31
import static javafx.util.Duration.seconds;
32
33
/**
34
 * Responsible for creating a {@link WizardPane} with a common header for all
35
 * subclasses.
36
 */
37
public abstract class InstallerPane extends WizardPane {
38
  /**
39
   * Unique key name to store the animation object so that it can be stopped.
40
   */
41
  private static final String PROP_ROTATION = "Wizard.typesetter.next.animate";
42
43
  /**
44
   * Defines amount of spacing between the installer's UI widgets, in pixels.
45
   */
46
  static final int PAD = 10;
47
48
  private static final double HEADER_FONT_SCALE = 1.25;
49
50
  public InstallerPane() {
51
    setHeader( createHeader() );
52
  }
53
54
  /**
55
   * When leaving the page, stop the animation. This is idempotent.
56
   *
57
   * @param wizard The wizard controlling the installer steps.
58
   */
59
  @Override
60
  public void onExitingPage( final Wizard wizard ) {
61
    super.onExitingPage( wizard );
62
    runLater( () -> stopAnimation( getNextButton() ) );
63
  }
64
65
  /**
66
   * Returns the property bundle key representing the dialog box title.
67
   */
68
  protected abstract String getHeaderKey();
69
70
  private BorderPane createHeader() {
71
    final var headerLabel = label( getHeaderKey() );
72
    headerLabel.setScaleX( HEADER_FONT_SCALE );
73
    headerLabel.setScaleY( HEADER_FONT_SCALE );
74
75
    final var separator = new Separator();
76
    separator.setPadding( new Insets( PAD, 0, 0, 0 ) );
77
78
    final var header = new BorderPane();
79
    header.setCenter( headerLabel );
80
    header.setRight( new ImageView( ICON_DIALOG ) );
81
    header.setBottom( separator );
82
    header.setPadding( new Insets( PAD, PAD, 0, PAD ) );
83
84
    return header;
85
  }
86
87
  /**
88
   * Disables the "Next" button during the installer. Normally disabling UI
89
   * elements is an anti-pattern (along with modal dialogs); however, in this
90
   * case, installation cannot proceed until each step is successfully
91
   * completed. Further, there may be "misleading" success messages shown
92
   * in the output panel, which the user may take as the step being complete.
93
   *
94
   * @param disable Set to {@code true} to disable the button.
95
   */
96
  void disableNext( final boolean disable ) {
97
    runLater( () -> {
98
      final var button = getNextButton();
99
100
      button.setDisable( disable );
101
102
      if( disable ) {
103
        startAnimation( button );
104
      }
105
      else {
106
        stopAnimation( button );
107
      }
108
    } );
109
  }
110
111
  /**
112
   * Returns the {@link Button} for advancing the wizard to the next pane.
113
   *
114
   * @return The Next button, if present, otherwise a new {@link Button}
115
   * instance so that API calls will succeed, despite not affecting the UI.
116
   */
117
  private Button getNextButton() {
118
    for( final var buttonType : getButtonTypes() ) {
119
      final var buttonData = buttonType.getButtonData();
120
121
      if( buttonData.equals( NEXT_FORWARD ) &&
122
          lookupButton( buttonType ) instanceof Button button ) {
123
        return button;
124
      }
125
    }
126
127
    // If there's no Next button, return a fake button.
128
    return new Button();
129
  }
130
131
  private void startAnimation( final Button button ) {
132
    // Create an image that is slightly taller than the button's font.
133
    final var graphic = new ImageView( ICON_DIALOG );
134
    graphic.setFitHeight( button.getFont().getSize() + 2 );
135
    graphic.setPreserveRatio( true );
136
    graphic.setSmooth( true );
137
138
    button.setGraphic( graphic );
139
    button.setGraphicTextGap( PAD );
140
    button.setContentDisplay( RIGHT );
141
142
    final var rotation = new RotateTransition( seconds( 1 ), graphic );
143
    getProperties().put( PROP_ROTATION, rotation );
144
145
    rotation.setCycleCount( Animation.INDEFINITE );
146
    rotation.setByAngle( 360 );
147
    rotation.setInterpolator( LINEAR );
148
    rotation.play();
149
  }
150
151
  private void stopAnimation( final Button button ) {
152
    final var animation = getProperties().get( PROP_ROTATION );
153
154
    if( animation instanceof RotateTransition rotation ) {
155
      rotation.stop();
156
      button.setGraphic( null );
157
      getProperties().remove( PROP_ROTATION );
158
    }
159
  }
160
161
  static TitledPane titledPane( final String title, final Node child ) {
162
    final var pane = new TitledPane( title, child );
163
    pane.setAnimated( false );
164
    pane.setCollapsible( false );
165
    pane.setExpanded( true );
166
167
    return pane;
168
  }
169
170
  static TextArea textArea( final int rows, final int cols ) {
171
    final var textarea = new TextArea();
172
    textarea.setEditable( false );
173
    textarea.setWrapText( true );
174
    textarea.setPrefRowCount( rows );
175
    textarea.setPrefColumnCount( cols );
176
177
    return textarea;
178
  }
179
180
  static Label label( final String key ) {
181
    return new Label( get( key ) );
182
  }
183
184
  /**
185
   * Like printf for labels.
186
   *
187
   * @param key    The property key to look up.
188
   * @param values The values to insert at the placeholders.
189
   * @return The formatted text with values replaced.
190
   */
191
  @SuppressWarnings( "SpellCheckingInspection" )
192
  static Label labelf( final String key, final Object... values ) {
193
    return new Label( get( key, values ) );
194
  }
195
196
  @SuppressWarnings( "SameParameterValue" )
197
  static Button button( final String key ) {
198
    return new Button( get( key ) );
199
  }
200
201
  static Node flowPane( final Node... nodes ) {
202
    return new FlowPane( nodes );
203
  }
204
205
  /**
206
   * Provides vertical spacing between {@link Node}s.
207
   *
208
   * @return A new empty vertical gap widget.
209
   */
210
  static Node spacer() {
211
    final var spacer = new Pane();
212
    spacer.setPadding( new Insets( PAD, 0, 0, 0 ) );
213
214
    return spacer;
215
  }
216
217
  static Hyperlink hyperlink( final String prefix ) {
218
    final var label = get( String.format( "%s%s", prefix, ".lbl" ) );
219
    final var url = get( String.format( "%s%s", prefix, ".url" ) );
220
    final var link = new Hyperlink( label );
221
222
    link.setOnAction( _ -> browse( url ) );
223
    link.setTooltip( new Tooltip( url ) );
224
225
    return link;
226
  }
227
228
  /**
229
   * Opens a browser window off of the JavaFX main execution thread. This
230
   * is necessary so that the links open immediately, instead of being blocked
231
   * by any modal dialog (i.e., the {@link Wizard} instance).
232
   *
233
   * @param property The property key name associated with a hyperlink URL.
234
   */
235
  static void browse( final String property ) {
236
    final var url = get( property );
237
    final var task = createTask( () -> {
238
      HyperlinkOpenEvent.fire( url );
239
      return null;
240
    } );
241
    final var thread = createThread( task );
242
243
    thread.start();
244
  }
245
246
  /**
247
   * Creates a container that can have its standard output read as an input
248
   * stream that's piped directly to a {@link TextArea}.
249
   *
250
   * @return An object that can perform tasks against a container.
251
   */
252
  static ContainerManager createContainer() {
253
    return new Podman();
254
  }
255
256
  static void update( final Label node, final String text ) {
257
    runLater( () -> node.setText( text ) );
258
  }
259
260
  static void append( final TextArea node, final String text ) {
261
    runLater( () -> {
262
      node.appendText( text );
263
      node.appendText( lineSeparator() );
264
    } );
265
  }
266
}
1267
A src/main/java/com/keenwrite/typesetting/installer/panes/IntroductionPane.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.installer.panes;
6
7
/**
8
 * Responsible for informing the user as to what will happen next.
9
 */
10
public final class IntroductionPane extends InstallerPane {
11
  private static final String PREFIX = "Wizard.typesetter.all.1.install";
12
13
  public IntroductionPane() {
14
    setContent( flowPane(
15
      hyperlink( PREFIX + ".about.container.link" ),
16
      label( PREFIX + ".about.text.1" ),
17
      hyperlink( PREFIX + ".about.typesetter.link" ),
18
      label( PREFIX + ".about.text.2" )
19
    ) );
20
  }
21
22
  @Override
23
  protected String getHeaderKey() {
24
    return PREFIX + ".header";
25
  }
26
}
127
A src/main/java/com/keenwrite/typesetting/installer/panes/ManagerInitializationPane.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.installer.panes;
6
7
import com.keenwrite.typesetting.containerization.ContainerManager;
8
9
/**
10
 * Responsible for initializing the container manager on all platforms except
11
 * for Linux.
12
 */
13
public final class ManagerInitializationPane extends ManagerOutputPane {
14
15
  private static final String PREFIX =
16
    "Wizard.typesetter.all.3.install.container";
17
18
  public ManagerInitializationPane() {
19
    super(
20
      PREFIX + ".correct",
21
      PREFIX + ".missing",
22
      ContainerManager::start,
23
      35
24
    );
25
  }
26
27
  @Override
28
  public String getHeaderKey() {
29
    return PREFIX + ".header";
30
  }
31
}
132
A src/main/java/com/keenwrite/typesetting/installer/panes/ManagerOutputPane.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.installer.panes;
6
7
import com.keenwrite.io.CommandNotFoundException;
8
import com.keenwrite.io.downloads.DownloadManager;
9
import com.keenwrite.typesetting.containerization.ContainerManager;
10
import com.keenwrite.typesetting.containerization.StreamProcessor;
11
import com.keenwrite.util.FailableBiConsumer;
12
import javafx.collections.ObservableMap;
13
import javafx.concurrent.Task;
14
import javafx.scene.control.TextArea;
15
import javafx.scene.layout.BorderPane;
16
import org.controlsfx.dialog.Wizard;
17
18
import static com.keenwrite.Messages.get;
19
import static com.keenwrite.io.StreamGobbler.gobble;
20
import static com.keenwrite.io.downloads.DownloadManager.createThread;
21
22
/**
23
 * Responsible for showing the output from running commands against a container
24
 * manager. There are a few installation steps that run different commands
25
 * against the installer, which are platform-specific and cannot be merged.
26
 * Common functionality between them is codified in this class.
27
 */
28
public abstract class ManagerOutputPane extends InstallerPane {
29
  private static final String PROP_EXECUTOR =
30
    ManagerOutputPane.class.getCanonicalName();
31
32
  private final String mCorrectKey;
33
  private final String mMissingKey;
34
  private final FailableBiConsumer
35
    <ContainerManager, StreamProcessor, CommandNotFoundException> mFc;
36
  private final ContainerManager mContainer;
37
  private final TextArea mTextArea;
38
39
  public ManagerOutputPane(
40
    final String correctKey,
41
    final String missingKey,
42
    final FailableBiConsumer
43
      <ContainerManager, StreamProcessor, CommandNotFoundException> fc,
44
    final int cols
45
  ) {
46
    mFc = fc;
47
    mCorrectKey = correctKey;
48
    mMissingKey = missingKey;
49
    mTextArea = textArea( 5, cols );
50
    mContainer = createContainer();
51
52
    final var borderPane = new BorderPane();
53
    final var titledPane = titledPane( "Output", mTextArea );
54
55
    borderPane.setBottom( titledPane );
56
    setContent( borderPane );
57
  }
58
59
  @Override
60
  public void onEnteringPage( final Wizard wizard ) {
61
    disableNext( true );
62
63
    try {
64
      final var properties = wizard.getProperties();
65
      final var thread = properties.get( PROP_EXECUTOR );
66
67
      if( thread instanceof Thread executor && executor.isAlive() ) {
68
        return;
69
      }
70
71
      final var task = createTask( properties, thread );
72
      final var executor = createThread( task );
73
74
      properties.put( PROP_EXECUTOR, executor );
75
      executor.start();
76
    } catch( final Exception e ) {
77
      throw new RuntimeException( e );
78
    }
79
  }
80
81
  private Task<Void> createTask(
82
    final ObservableMap<Object, Object> properties,
83
    final Object thread ) {
84
    final Task<Void> task = DownloadManager.createTask( () -> {
85
      mFc.accept(
86
        mContainer,
87
        input -> gobble( input, line -> append( mTextArea, line ) )
88
      );
89
      properties.remove( thread );
90
      return null;
91
    } );
92
93
    task.setOnSucceeded( _ -> {
94
      append( mTextArea, get( mCorrectKey ) );
95
      properties.remove( thread );
96
      disableNext( false );
97
    } );
98
    task.setOnFailed( _ -> append( mTextArea, get( mMissingKey ) ) );
99
    task.setOnCancelled( _ -> append( mTextArea, get( mMissingKey ) ) );
100
    return task;
101
  }
102
}
1103
A src/main/java/com/keenwrite/typesetting/installer/panes/TypesetterImageDownloadPane.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.installer.panes;
6
7
import com.keenwrite.typesetting.containerization.ContainerManager;
8
9
/**
10
 * Responsible for installing the typesetter's image via the container manager.
11
 */
12
public final class TypesetterImageDownloadPane extends ManagerOutputPane {
13
  private static final String PREFIX =
14
    "Wizard.typesetter.all.4.download.image";
15
16
  public TypesetterImageDownloadPane() {
17
    super(
18
      PREFIX + ".correct",
19
      PREFIX + ".missing",
20
      ContainerManager::load,
21
      45
22
    );
23
  }
24
25
  @Override
26
  public String getHeaderKey() {
27
    return PREFIX + ".header";
28
  }
29
}
130
A src/main/java/com/keenwrite/typesetting/installer/panes/TypesetterThemesDownloadPane.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.installer.panes;
6
7
import com.keenwrite.io.UserDataDir;
8
import com.keenwrite.io.Zip;
9
import com.keenwrite.preferences.Workspace;
10
import javafx.collections.ObservableMap;
11
import org.controlsfx.dialog.Wizard;
12
13
import java.io.File;
14
import java.io.IOException;
15
16
import static com.keenwrite.Messages.get;
17
import static com.keenwrite.events.StatusEvent.clue;
18
import static com.keenwrite.io.SysFile.toFile;
19
import static com.keenwrite.preferences.AppKeys.KEY_TYPESET_CONTEXT_THEMES_PATH;
20
21
/**
22
 * Responsible for downloading themes into the application's data directory.
23
 * The data directory differs between platforms, which is handled
24
 * transparently by the {@link UserDataDir} class.
25
 */
26
public class TypesetterThemesDownloadPane extends AbstractDownloadPane {
27
  private static final String PREFIX =
28
    "Wizard.typesetter.all.5.download.themes";
29
30
  private final Workspace mWorkspace;
31
32
  public TypesetterThemesDownloadPane( final Workspace workspace ) {
33
    assert workspace != null;
34
    mWorkspace = workspace;
35
  }
36
37
  @Override
38
  public void onEnteringPage( final Wizard wizard ) {
39
    // Delete the target themes file to force re-download so that unzipping
40
    // the file takes place. This side-steps checksum validation, which would
41
    // be best implemented after downloading.
42
    deleteTarget();
43
    super.onEnteringPage( wizard );
44
  }
45
46
  @Override
47
  protected void onDownloadSucceeded(
48
    final String threadName, final ObservableMap<Object, Object> properties ) {
49
    super.onDownloadSucceeded( threadName, properties );
50
51
    try {
52
      process( getTarget() );
53
    } catch( final Exception ex ) {
54
      clue( ex );
55
    }
56
  }
57
58
  private void process( final File target ) throws IOException {
59
    Zip.extract( target.toPath() );
60
61
    // Replace the default themes directory with the downloaded version.
62
    final var root = toFile( Zip.root( target.toPath() ) );
63
64
    // Make sure the typesetter will know where to find the themes.
65
    mWorkspace.fileProperty( KEY_TYPESET_CONTEXT_THEMES_PATH ).set( root );
66
    mWorkspace.save();
67
68
    // The themes pack is no longer needed.
69
    deleteTarget();
70
  }
71
72
  @Override
73
  protected String getPrefix() {
74
    return PREFIX;
75
  }
76
77
  @Override
78
  protected String getChecksum() {
79
    return get( "Wizard.typesetter.themes.checksum" );
80
  }
81
}
182
A src/main/java/com/keenwrite/typesetting/installer/panes/UniversalManagerInstallPane.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.installer.panes;
6
7
/**
8
 * Responsible for installing the container manager for any operating system
9
 * that was not explicitly detected.
10
 */
11
public final class UniversalManagerInstallPane extends InstallerPane {
12
  private static final String PREFIX =
13
    "Wizard.typesetter.all.2.install.container";
14
15
  public UniversalManagerInstallPane() { }
16
17
  @Override
18
  protected String getHeaderKey() {
19
    return PREFIX + ".header";
20
  }
21
}
122
A src/main/java/com/keenwrite/typesetting/installer/panes/UnixManagerInstallPane.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.installer.panes;
6
7
import com.keenwrite.ui.clipboard.SystemClipboard;
8
import javafx.geometry.Insets;
9
import javafx.scene.Node;
10
import javafx.scene.control.ButtonBar;
11
import javafx.scene.control.ComboBox;
12
import javafx.scene.control.TextArea;
13
import javafx.scene.layout.BorderPane;
14
import javafx.scene.layout.HBox;
15
import javafx.scene.layout.VBox;
16
17
import static com.keenwrite.Messages.get;
18
import static com.keenwrite.Messages.getInt;
19
import static com.keenwrite.util.SystemUtils.IS_OS_MAC;
20
import static java.lang.String.format;
21
22
public final class UnixManagerInstallPane extends InstallerPane {
23
  private static final String PREFIX =
24
    "Wizard.typesetter.unix.2.install.container";
25
26
  private final TextArea mCommands = textArea( 2, 40 );
27
28
  public UnixManagerInstallPane() {
29
    final var titledPane = titledPane( "Run", mCommands );
30
    final var comboBox = createUnixOsCommandMap();
31
    final var selection = comboBox.getSelectionModel();
32
    selection
33
      .selectedItemProperty()
34
      .addListener( ( c, o, n ) -> mCommands.setText( n.command() ) );
35
36
    // Auto-select if running on macOS.
37
    if( IS_OS_MAC ) {
38
      final var items = comboBox.getItems();
39
40
      for( final var item : items ) {
41
        if( "macOS".equalsIgnoreCase( item.name ) ) {
42
          selection.select( item );
43
          break;
44
        }
45
      }
46
    }
47
    else {
48
      selection.select( 0 );
49
    }
50
51
    final var distro = label( PREFIX + ".os" );
52
    distro.setText( distro.getText() + ":" );
53
    distro.setPadding( new Insets( PAD / 2.0, PAD, 0, 0 ) );
54
55
    final var hbox = new HBox();
56
    hbox.getChildren().add( distro );
57
    hbox.getChildren().add( comboBox );
58
    hbox.setPadding( new Insets( 0, 0, PAD, 0 ) );
59
60
    final var stepsPane = new VBox();
61
    final var steps = stepsPane.getChildren();
62
    steps.add( label( PREFIX + ".step.0" ) );
63
    steps.add( spacer() );
64
    steps.add( label( PREFIX + ".step.1" ) );
65
    steps.add( label( PREFIX + ".step.2" ) );
66
    steps.add( label( PREFIX + ".step.3" ) );
67
    steps.add( label( PREFIX + ".step.4" ) );
68
    steps.add( spacer() );
69
70
    steps.add( flowPane(
71
      label( PREFIX + ".details.prefix" ),
72
      hyperlink( PREFIX + ".details.link" ),
73
      label( PREFIX + ".details.suffix" )
74
    ) );
75
    steps.add( spacer() );
76
77
    final var border = new BorderPane();
78
    border.setTop( stepsPane );
79
    border.setCenter( hbox );
80
    border.setBottom( titledPane );
81
82
    setContent( border );
83
  }
84
85
  @Override
86
  public Node createButtonBar() {
87
    final var node = super.createButtonBar();
88
    final var layout = new BorderPane();
89
    final var copyButton = button( format( "%s%s", PREFIX, ".copy.began" ) );
90
91
    // Change the label to indicate clipboard is updated.
92
    copyButton.setOnAction( _ -> {
93
      SystemClipboard.write( mCommands.getText() );
94
      copyButton.setText( get( format( "%s%s", PREFIX, ".copy.ended" ) ) );
95
    } );
96
97
    if( node instanceof ButtonBar buttonBar ) {
98
      copyButton.setMinWidth( buttonBar.getButtonMinWidth() );
99
    }
100
101
    layout.setPadding( new Insets( PAD, PAD, PAD, PAD ) );
102
    layout.setLeft( copyButton );
103
    layout.setRight( node );
104
105
    return layout;
106
  }
107
108
  @Override
109
  protected String getHeaderKey() {
110
    return format( "%s%s", PREFIX, ".header" );
111
  }
112
113
  private record UnixOsCommand( String name, String command )
114
    implements Comparable<UnixOsCommand> {
115
    @Override
116
    public int compareTo( final UnixOsCommand other ) {
117
      return toString().compareToIgnoreCase( other.toString() );
118
    }
119
120
    @Override
121
    public String toString() {
122
      return name;
123
    }
124
  }
125
126
  /**
127
   * Creates a collection of *nix distributions mapped to instructions for users
128
   * to run in a terminal.
129
   *
130
   * @return A map of *nix to instructions.
131
   */
132
  private static ComboBox<UnixOsCommand> createUnixOsCommandMap() {
133
    new ComboBox<UnixOsCommand>();
134
    final var comboBox = new ComboBox<UnixOsCommand>();
135
    final var items = comboBox.getItems();
136
    final var prefix = format( "%s%s", PREFIX, ".command" );
137
    final var distros = getInt( format( "%s%s", prefix, ".distros" ), 14 );
138
139
    for( int i = 1; i <= distros; i++ ) {
140
      final var suffix = format( ".%02d", i );
141
      final var name = get( format( "%s.os.name%s", prefix, suffix ) );
142
      final var command = get( format( "%s.os.text%s", prefix, suffix ) );
143
144
      items.add( new UnixOsCommand( name, command ) );
145
    }
146
147
    items.sort( UnixOsCommand::compareTo );
148
149
    return comboBox;
150
  }
151
}
1152
A src/main/java/com/keenwrite/typesetting/installer/panes/WindowsManagerDownloadPane.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.installer.panes;
6
7
import javafx.collections.ObservableMap;
8
9
import static com.keenwrite.Messages.get;
10
import static com.keenwrite.typesetting.installer.panes.WindowsManagerInstallPane.WIN_BIN;
11
12
/**
13
 * Responsible for downloading the container manager software on Windows.
14
 */
15
public final class WindowsManagerDownloadPane extends AbstractDownloadPane {
16
  private static final String PREFIX =
17
    "Wizard.typesetter.win.2.download.container";
18
19
  @Override
20
  protected void updateProperties(
21
    final ObservableMap<Object, Object> properties ) {
22
    properties.put( WIN_BIN, getTarget() );
23
  }
24
25
  @Override
26
  protected String getPrefix() {
27
    return PREFIX;
28
  }
29
30
  @Override
31
  protected String getChecksum() {
32
    return get( "Wizard.typesetter.container.checksum" );
33
  }
34
}
135
A src/main/java/com/keenwrite/typesetting/installer/panes/WindowsManagerInstallPane.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.typesetting.installer.panes;
6
7
import com.keenwrite.typesetting.containerization.ContainerManager;
8
import javafx.scene.control.TextArea;
9
import javafx.scene.layout.BorderPane;
10
import javafx.scene.layout.VBox;
11
import org.controlsfx.dialog.Wizard;
12
13
import java.io.File;
14
15
import static com.keenwrite.Messages.get;
16
import static com.keenwrite.io.downloads.DownloadManager.createTask;
17
import static com.keenwrite.io.downloads.DownloadManager.createThread;
18
19
/**
20
 * Responsible for installing the container manager on Windows.
21
 */
22
public final class WindowsManagerInstallPane extends InstallerPane {
23
  /**
24
   * Property for the installation thread to help with reentrancy.
25
   */
26
  private static final String WIN_INSTALLER = "windows.container.installer";
27
28
  /**
29
   * Shared property to track name of container manager binary file.
30
   */
31
  static final String WIN_BIN = "windows.container.binary";
32
33
  private static final String PREFIX =
34
    "Wizard.typesetter.win.2.install.container";
35
36
  private final ContainerManager mContainer;
37
  private final TextArea mCommands;
38
39
  public WindowsManagerInstallPane() {
40
    mCommands = textArea( 2, 55 );
41
42
    final var titledPane = titledPane( "Output", mCommands );
43
    append( mCommands, get( String.format( "%s%s", PREFIX, ".status.running" ) ) );
44
45
    final var stepsPane = new VBox();
46
    final var steps = stepsPane.getChildren();
47
    steps.add( label( String.format( "%s%s", PREFIX, ".step.0" ) ) );
48
    steps.add( spacer() );
49
    steps.add( label( String.format( "%s%s", PREFIX, ".step.1" ) ) );
50
    steps.add( label( String.format( "%s%s", PREFIX, ".step.2" ) ) );
51
    steps.add( label( String.format( "%s%s", PREFIX, ".step.3" ) ) );
52
    steps.add( spacer() );
53
    steps.add( titledPane );
54
55
    final var border = new BorderPane();
56
    border.setTop( stepsPane );
57
58
    mContainer = createContainer();
59
  }
60
61
  @Override
62
  public void onEnteringPage( final Wizard wizard ) {
63
    disableNext( true );
64
65
    // Pull the fully qualified installer path from the properties.
66
    final var properties = wizard.getProperties();
67
    final var thread = properties.get( WIN_INSTALLER );
68
69
    if( thread instanceof Thread installer && installer.isAlive() ) {
70
      return;
71
    }
72
73
    final var binary = properties.get( WIN_BIN );
74
    final var key = String.format( "%s%s", PREFIX, ".status" );
75
76
    if( binary instanceof File exe ) {
77
      final var task = createTask( () -> {
78
        final var exit = mContainer.install( exe );
79
80
        // Remove the installer after installation is finished.
81
        properties.remove( thread );
82
83
        final var msg = exit == 0
84
          ? get( String.format( "%s%s", key, ".success" ) )
85
          : get( String.format( "%s%s", key, ".failure" ), exit );
86
87
        append( mCommands, msg );
88
        disableNext( exit != 0 );
89
90
        return null;
91
      } );
92
93
      final var installer = createThread( task );
94
      properties.put( WIN_INSTALLER, installer );
95
      installer.start();
96
    }
97
    else {
98
      append( mCommands, get( String.format( "%s%s", PREFIX, ".unknown" ), binary ) );
99
    }
100
  }
101
102
  @Override
103
  public String getHeaderKey() {
104
    return String.format( "%s%s", PREFIX, ".header" );
105
  }
106
}
1107
A src/main/java/com/keenwrite/ui/actions/Action.java
1
/* Copyright 2020-2021 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 javafx.event.ActionEvent;
7
import javafx.event.EventHandler;
8
import javafx.scene.control.Button;
9
import javafx.scene.control.Menu;
10
import javafx.scene.control.MenuItem;
11
import javafx.scene.control.Tooltip;
12
import javafx.scene.input.KeyCombination;
13
14
import java.util.ArrayList;
15
import java.util.List;
16
17
import static com.keenwrite.constants.Constants.ACTION_PREFIX;
18
import static com.keenwrite.ui.fonts.IconFactory.createGraphic;
19
import static javafx.scene.input.KeyCombination.valueOf;
20
21
/**
22
 * Defines actions the user can take through GUI interactions
23
 */
24
public final class Action implements MenuAction {
25
  private final String mText;
26
  private final KeyCombination mAccelerator;
27
  private final String mIcon;
28
  private final EventHandler<ActionEvent> mHandler;
29
  private final List<MenuAction> mSubActions = new ArrayList<>();
30
31
  /**
32
   * Provides a fluent interface around constructing actions so that duplication
33
   * can be avoided.
34
   */
35
  public static class Builder {
36
    private String mText;
37
    private String mAccelerator;
38
    private String mIcon;
39
    private EventHandler<ActionEvent> mHandler;
40
41
    /**
42
     * Sets the text, icon, and accelerator for a given action identifier.
43
     * See the messages properties file for details.
44
     *
45
     * @param id The identifier to look up in the properties file.
46
     * @return An instance of {@link Builder} that can be built into an
47
     * instance of {@link Action}.
48
     */
49
    public Builder setId( final String id ) {
50
      final var prefix = String.format( "%s%s.", ACTION_PREFIX, id );
51
      final var text = String.format( "%s%s", prefix, "text" );
52
      final var icon = String.format( "%s%s", prefix, "icon" );
53
      final var accelerator = String.format( "%s%s", prefix, "accelerator" );
54
      final var builder = setText( text ).setIcon( icon );
55
56
      return Messages.containsKey( accelerator )
57
        ? builder.setAccelerator( Messages.get( accelerator ) )
58
        : builder;
59
    }
60
61
    /**
62
     * Sets the action text based on a resource bundle key.
63
     *
64
     * @param key The key to look up in the {@link Messages}.
65
     * @return The corresponding value, or the key name if none found.
66
     */
67
    private Builder setText( final String key ) {
68
      mText = Messages.get( key, key );
69
      return this;
70
    }
71
72
    private Builder setAccelerator( final String accelerator ) {
73
      mAccelerator = accelerator;
74
      return this;
75
    }
76
77
    private Builder setIcon( final String iconKey ) {
78
      assert iconKey != null;
79
80
      // If there's no icon associated with the icon key name, don't attempt
81
      // to create a graphic for the icon, because it won't exist.
82
      final var iconName = Messages.get( iconKey );
83
      mIcon = iconKey.equals( iconName ) ? "" : iconName;
84
85
      return this;
86
    }
87
88
    public Builder setHandler( final EventHandler<ActionEvent> handler ) {
89
      mHandler = handler;
90
      return this;
91
    }
92
93
    public Action build() {
94
      return new Action( mText, mAccelerator, mIcon, mHandler );
95
    }
96
  }
97
98
  /**
99
   * TODO: Reuse the {@link GenericBuilder}.
100
   *
101
   * @return The {@link Builder} for an instance of {@link Action}.
102
   */
103
  public static Builder builder() {
104
    return new Builder();
105
  }
106
107
  private static Button createIconButton( final String icon ) {
108
    return new Button( null, createGraphic( icon ) );
109
  }
110
111
  public Action(
112
    final String text,
113
    final String accelerator,
114
    final String icon,
115
    final EventHandler<ActionEvent> handler ) {
116
    assert text != null;
117
    assert handler != null;
118
119
    mText = text;
120
    mAccelerator = accelerator == null ? null : valueOf( accelerator );
121
    mIcon = icon;
122
    mHandler = handler;
123
  }
124
125
  @Override
126
  public MenuItem createMenuItem() {
127
    // This will either become a menu or a menu item, depending on whether
128
    // sub-actions are defined.
129
    final MenuItem menuItem;
130
131
    if( mSubActions.isEmpty() ) {
132
      // Regular menu item has no sub-menus.
133
      menuItem = new MenuItem( mText );
134
    }
135
    else {
136
      // Sub-actions are translated into sub-menu items beneath this action.
137
      final var submenu = new Menu( mText );
138
139
      for( final var action : mSubActions ) {
140
        // Recursive call that creates a sub-menu hierarchy.
141
        submenu.getItems().add( action.createMenuItem() );
142
      }
143
144
      menuItem = submenu;
145
    }
146
147
    if( mAccelerator != null ) {
148
      menuItem.setAccelerator( mAccelerator );
149
    }
150
151
    if( mIcon != null ) {
152
      menuItem.setGraphic( createGraphic( mIcon ) );
153
    }
154
155
    menuItem.setOnAction( mHandler );
156
157
    return menuItem;
158
  }
159
160
  @Override
161
  public Button createToolBarNode() {
162
    final var button = createIconButton( mIcon );
163
    var tooltip = mText;
164
165
    if( tooltip.endsWith( "..." ) ) {
166
      tooltip = tooltip.substring( 0, tooltip.length() - 3 );
167
    }
168
169
    // Do not display mnemonic accelerator character in tooltip text.
170
    // The accelerator key will still be available, this is display-only.
171
    tooltip = tooltip.replace( "_", "" );
172
173
    if( mAccelerator != null ) {
174
      tooltip += String.format( " (%s)", mAccelerator.getDisplayText() );
175
    }
176
177
    button.setTooltip( new Tooltip( tooltip ) );
178
    button.setFocusTraversable( false );
179
    button.setOnAction( mHandler );
180
181
    return button;
182
  }
183
184
  /**
185
   * Adds subordinate actions to the menu. This is used to establish sub-menu
186
   * relationships. The default behaviour does not wire up any registration;
187
   * subclasses are responsible for handling how actions relate to one another.
188
   *
189
   * @param action Actions that only exist with respect to this action.
190
   */
191
  public MenuAction addSubActions( final MenuAction... action ) {
192
    mSubActions.addAll( List.of( action ) );
193
    return this;
194
  }
195
}
1196
A src/main/java/com/keenwrite/ui/actions/ApplicationBars.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.actions;
6
7
import com.keenwrite.ui.controls.EventedStatusBar;
8
import javafx.event.ActionEvent;
9
import javafx.event.EventHandler;
10
import javafx.scene.Node;
11
import javafx.scene.control.Menu;
12
import javafx.scene.control.MenuBar;
13
import javafx.scene.control.MenuItem;
14
import javafx.scene.control.ToolBar;
15
import org.controlsfx.control.StatusBar;
16
import org.jetbrains.annotations.NotNull;
17
18
import java.util.HashMap;
19
import java.util.Map;
20
21
import static com.keenwrite.Messages.get;
22
23
/**
24
 * Responsible for wiring all application actions to menus, toolbar buttons,
25
 * and keyboard shortcuts.
26
 */
27
public final class ApplicationBars {
28
29
  private static final Map<String, Action> sMap = new HashMap<>( 64 );
30
31
  /**
32
   * Empty constructor.
33
   */
34
  public ApplicationBars() {
35
  }
36
37
  /**
38
   * Creates the main application affordances.
39
   *
40
   * @param actions The {@link GuiCommands} that map user interface
41
   *                selections to executable code.
42
   * @return An instance of {@link MenuBar} that contains the menu.
43
   */
44
  public static MenuBar createMenuBar( final GuiCommands actions ) {
45
    final var SEPARATOR = new SeparatorAction();
46
47
    return new MenuBar(
48
      createMenuFile( actions, SEPARATOR ),
49
      createMenuEdit( actions, SEPARATOR ),
50
      createMenuFormat( actions ),
51
      createMenuInsert( actions, SEPARATOR ),
52
      createMenuVariable( actions, SEPARATOR ),
53
      createMenuView( actions, SEPARATOR ),
54
      createMenuHelp( actions )
55
    );
56
  }
57
58
  @NotNull
59
  private static Menu createMenuFile(
60
    final GuiCommands actions, final SeparatorAction SEPARATOR ) {
61
    // @formatter:off
62
    return createMenu(
63
      get( "Main.menu.file" ),
64
      addAction( "file.new", _ -> actions.file_new() ),
65
      addAction( "file.open", _ -> actions.file_open() ),
66
      addAction( "file.open_url", _ -> actions.file_open_url() ),
67
      SEPARATOR,
68
      addAction( "file.close", _ -> actions.file_close() ),
69
      addAction( "file.close_all", _ -> actions.file_close_all() ),
70
      SEPARATOR,
71
      addAction( "file.save", _ -> actions.file_save() ),
72
      addAction( "file.save_as", _ -> actions.file_save_as() ),
73
      addAction( "file.save_all", _ -> actions.file_save_all() ),
74
      SEPARATOR,
75
      addAction( "file.export", _ -> { } )
76
        .addSubActions(
77
          addAction( "file.export.pdf", _ -> actions.file_export_pdf() ),
78
          addAction( "file.export.pdf.dir", _ -> actions.file_export_pdf_dir() ),
79
          addAction( "file.export.pdf.repeat", _ -> actions.file_export_repeat() ),
80
          addAction( "file.export.html.dir", _ -> actions.file_export_html_dir() ),
81
          addAction( "file.export.text_tex.dir", _ -> actions.file_export_text_tex_dir() ),
82
          addAction( "file.export.html_svg", _ -> actions.file_export_html_svg() ),
83
          addAction( "file.export.html_tex", _ -> actions.file_export_html_tex() ),
84
          addAction( "file.export.text_tex", _ -> actions.file_export_text_tex() ),
85
          addAction( "file.export.xhtml_tex", _ -> actions.file_export_xhtml_tex() )
86
        ),
87
      SEPARATOR,
88
      addAction( "file.exit", _ -> actions.file_exit() )
89
    );
90
    // @formatter:on
91
  }
92
93
  @NotNull
94
  private static Menu createMenuEdit(
95
    final GuiCommands actions, final SeparatorAction SEPARATOR ) {
96
    return createMenu(
97
      get( "Main.menu.edit" ),
98
      SEPARATOR,
99
      addAction( "edit.undo", _ -> actions.edit_undo() ),
100
      addAction( "edit.redo", _ -> actions.edit_redo() ),
101
      SEPARATOR,
102
      addAction( "edit.cut", _ -> actions.edit_cut() ),
103
      addAction( "edit.copy", _ -> actions.edit_copy() ),
104
      addAction( "edit.paste", _ -> actions.edit_paste() ),
105
      addAction( "edit.select_all", _ -> actions.edit_select_all() ),
106
      SEPARATOR,
107
      addAction( "edit.find", _ -> actions.edit_find() ),
108
      addAction( "edit.find_next", _ -> actions.edit_find_next() ),
109
      addAction( "edit.find_prev", _ -> actions.edit_find_prev() ),
110
      SEPARATOR,
111
      addAction( "edit.preferences", _ -> actions.edit_preferences() )
112
    );
113
  }
114
115
  @NotNull
116
  private static Menu createMenuFormat( final GuiCommands actions ) {
117
    return createMenu(
118
      get( "Main.menu.format" ),
119
      addAction( "format.bold", _ -> actions.format_bold() ),
120
      addAction( "format.italic", _ -> actions.format_italic() ),
121
      addAction( "format.monospace", _ -> actions.format_monospace() ),
122
      addAction( "format.superscript", _ -> actions.format_superscript() ),
123
      addAction( "format.subscript", _ -> actions.format_subscript() ),
124
      addAction( "format.strikethrough", _ -> actions.format_strikethrough() )
125
    );
126
  }
127
128
  @NotNull
129
  private static Menu createMenuInsert(
130
    final GuiCommands actions,
131
    final SeparatorAction SEPARATOR ) {
132
    // @formatter:off
133
    return createMenu(
134
      get( "Main.menu.insert" ),
135
      addAction( "insert.blockquote", _ -> actions.insert_blockquote() ),
136
      addAction( "insert.code", _ -> actions.insert_code() ),
137
      addAction( "insert.fenced_code_block", _ -> actions.insert_fenced_code_block() ),
138
      SEPARATOR,
139
      addAction( "insert.link", _ -> actions.insert_link() ),
140
      addAction( "insert.image", _ -> actions.insert_image() ),
141
      SEPARATOR,
142
      addAction( "insert.heading_1", _ -> actions.insert_heading_1() ),
143
      addAction( "insert.heading_2", _ -> actions.insert_heading_2() ),
144
      addAction( "insert.heading_3", _ -> actions.insert_heading_3() ),
145
      SEPARATOR,
146
      addAction( "insert.unordered_list", _ -> actions.insert_unordered_list() ),
147
      addAction( "insert.ordered_list", _ -> actions.insert_ordered_list() ),
148
      addAction( "insert.horizontal_rule", _ -> actions.insert_horizontal_rule() )
149
    );
150
    // @formatter:on
151
  }
152
153
  @NotNull
154
  private static Menu createMenuVariable(
155
    final GuiCommands actions, final SeparatorAction SEPARATOR ) {
156
    return createMenu(
157
      get( "Main.menu.definition" ),
158
      addAction( "definition.insert", _ -> actions.definition_autoinsert() ),
159
      SEPARATOR,
160
      addAction( "definition.create", _ -> actions.definition_create() ),
161
      addAction( "definition.rename", _ -> actions.definition_rename() ),
162
      addAction( "definition.delete", _ -> actions.definition_delete() )
163
    );
164
  }
165
166
  @NotNull
167
  private static Menu createMenuView(
168
    final GuiCommands actions, final SeparatorAction SEPARATOR ) {
169
    return createMenu(
170
      get( "Main.menu.view" ),
171
      addAction( "view.refresh", _ -> actions.view_refresh() ),
172
      SEPARATOR,
173
      addAction( "view.preview", _ -> actions.view_preview() ),
174
      addAction( "view.outline", _ -> actions.view_outline() ),
175
      addAction( "view.statistics", _ -> actions.view_statistics() ),
176
      addAction( "view.files", _ -> actions.view_files() ),
177
      SEPARATOR,
178
      addAction( "view.menubar", _ -> actions.view_menubar() ),
179
      addAction( "view.toolbar", _ -> actions.view_toolbar() ),
180
      addAction( "view.statusbar", _ -> actions.view_statusbar() ),
181
      SEPARATOR,
182
      addAction( "view.log", _ -> actions.view_log() )
183
    );
184
  }
185
186
  @NotNull
187
  private static Menu createMenuHelp( final GuiCommands actions ) {
188
    return createMenu(
189
      get( "Main.menu.help" ),
190
      addAction( "help.about", _ -> actions.help_about() )
191
    );
192
  }
193
194
  public static Node createToolBar() {
195
    final var SEPARATOR = new SeparatorAction();
196
197
    return createToolBar(
198
      getAction( "file.new" ),
199
      getAction( "file.open" ),
200
      getAction( "file.save" ),
201
      SEPARATOR,
202
      getAction( "file.export.pdf" ),
203
      SEPARATOR,
204
      getAction( "edit.undo" ),
205
      getAction( "edit.redo" ),
206
      getAction( "edit.cut" ),
207
      getAction( "edit.copy" ),
208
      getAction( "edit.paste" ),
209
      SEPARATOR,
210
      getAction( "format.bold" ),
211
      getAction( "format.italic" ),
212
      getAction( "format.superscript" ),
213
      getAction( "format.subscript" ),
214
      getAction( "insert.blockquote" ),
215
      getAction( "insert.code" ),
216
      getAction( "insert.fenced_code_block" ),
217
      SEPARATOR,
218
      getAction( "insert.link" ),
219
      getAction( "insert.image" ),
220
      SEPARATOR,
221
      getAction( "insert.heading_1" ),
222
      SEPARATOR,
223
      getAction( "insert.unordered_list" ),
224
      getAction( "insert.ordered_list" )
225
    );
226
  }
227
228
  public static StatusBar createStatusBar() {
229
    return new EventedStatusBar();
230
  }
231
232
  /**
233
   * Adds a new action to the list of actions.
234
   *
235
   * @param key     The name of the action to register in {@link #sMap}.
236
   * @param handler Performs the action upon request.
237
   * @return The newly registered action.
238
   */
239
  private static Action addAction(
240
    final String key, final EventHandler<ActionEvent> handler ) {
241
    assert key != null;
242
    assert handler != null;
243
244
    final var action = Action
245
      .builder()
246
      .setId( key )
247
      .setHandler( handler )
248
      .build();
249
250
    sMap.put( key, action );
251
252
    return action;
253
  }
254
255
  private static Action getAction( final String key ) {
256
    return sMap.get( key );
257
  }
258
259
  public static Menu createMenu(
260
    final String text, final MenuAction... actions ) {
261
    return new Menu( text, null, createMenuItems( actions ) );
262
  }
263
264
  public static MenuItem[] createMenuItems( final MenuAction... actions ) {
265
    final var menuItems = new MenuItem[ actions.length ];
266
267
    for( var i = 0; i < actions.length; i++ ) {
268
      menuItems[ i ] = actions[ i ].createMenuItem();
269
    }
270
271
    return menuItems;
272
  }
273
274
  private static ToolBar createToolBar( final MenuAction... actions ) {
275
    return new ToolBar( createToolBarButtons( actions ) );
276
  }
277
278
  private static Node[] createToolBarButtons( final MenuAction... actions ) {
279
    final var len = actions.length;
280
    final var nodes = new Node[ len ];
281
282
    for( var i = 0; i < len; i++ ) {
283
      nodes[ i ] = actions[ i ].createToolBarNode();
284
    }
285
286
    return nodes;
287
  }
288
}
1289
A src/main/java/com/keenwrite/ui/actions/GuiCommands.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.actions;
6
7
import com.keenwrite.ExportFormat;
8
import com.keenwrite.MainPane;
9
import com.keenwrite.MainScene;
10
import com.keenwrite.commands.ConcatenateCommand;
11
import com.keenwrite.editors.TextDefinition;
12
import com.keenwrite.editors.TextEditor;
13
import com.keenwrite.editors.markdown.LinkVisitor;
14
import com.keenwrite.events.CaretMovedEvent;
15
import com.keenwrite.events.ExportFailedEvent;
16
import com.keenwrite.io.SysFile;
17
import com.keenwrite.preferences.Key;
18
import com.keenwrite.preferences.PreferencesController;
19
import com.keenwrite.preferences.Workspace;
20
import com.keenwrite.processors.markdown.MarkdownProcessor;
21
import com.keenwrite.search.SearchModel;
22
import com.keenwrite.typesetting.Typesetter;
23
import com.keenwrite.ui.controls.SearchBar;
24
import com.keenwrite.ui.dialogs.*;
25
import com.keenwrite.ui.explorer.FilePicker;
26
import com.keenwrite.ui.explorer.FilePickerFactory;
27
import com.keenwrite.ui.logging.LogView;
28
import com.keenwrite.ui.models.HyperlinkModel;
29
import com.keenwrite.ui.models.ImageModel;
30
import com.vladsch.flexmark.ast.Link;
31
import javafx.concurrent.Service;
32
import javafx.concurrent.Task;
33
import javafx.scene.control.Alert;
34
import javafx.scene.control.Dialog;
35
import javafx.stage.Window;
36
import javafx.stage.WindowEvent;
37
38
import java.io.File;
39
import java.nio.file.Path;
40
import java.util.List;
41
import java.util.Optional;
42
43
import static com.keenwrite.Bootstrap.*;
44
import static com.keenwrite.ExportFormat.*;
45
import static com.keenwrite.Messages.get;
46
import static com.keenwrite.constants.Constants.USER_DIRECTORY;
47
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE;
48
import static com.keenwrite.events.StatusEvent.clue;
49
import static com.keenwrite.preferences.AppKeys.*;
50
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
51
import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType;
52
import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType.*;
53
import static java.nio.charset.StandardCharsets.UTF_8;
54
import static java.nio.file.Files.writeString;
55
import static javafx.application.Platform.runLater;
56
import static javafx.event.Event.fireEvent;
57
import static javafx.scene.control.Alert.AlertType.INFORMATION;
58
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
59
import static org.apache.commons.io.FilenameUtils.getExtension;
60
61
/**
62
 * Responsible for abstracting how functionality is mapped to the application.
63
 * This allows users to customize accelerator keys and will provide pluggable
64
 * functionality so that different text markup languages can change documents
65
 * using their respective syntax.
66
 */
67
public final class GuiCommands {
68
  private static final String STYLE_SEARCH = "search";
69
70
  /**
71
   * When an action is executed, this is one of the recipients.
72
   */
73
  private final MainPane mMainPane;
74
75
  private final MainScene mMainScene;
76
77
  private final LogView mLogView;
78
79
  /**
80
   * Tracks finding text in the active document.
81
   */
82
  private final SearchModel mSearchModel;
83
84
  private boolean mCanTypeset;
85
86
  /**
87
   * A {@link Task} can only be run once, so wrap it in a {@link Service} to
88
   * allow re-launching the typesetting task repeatedly.
89
   */
90
  private Service<Path> mTypesetService;
91
92
  /**
93
   * Prevent a race-condition between checking to see if the typesetting task
94
   * is running and restarting the task itself.
95
   */
96
  private final Object mMutex = new Object();
97
98
  public GuiCommands( final MainScene scene, final MainPane pane ) {
99
    mMainScene = scene;
100
    mMainPane = pane;
101
    mLogView = new LogView();
102
    mSearchModel = new SearchModel();
103
    mSearchModel.matchOffsetProperty().addListener( ( _, o, n ) -> {
104
      final var editor = getActiveTextEditor();
105
106
      // Clear highlighted areas before highlighting a new region.
107
      if( o != null ) {
108
        editor.unstylize( STYLE_SEARCH );
109
      }
110
111
      if( n != null ) {
112
        editor.moveTo( n.getStart() );
113
        editor.stylize( n, STYLE_SEARCH );
114
      }
115
    } );
116
117
    // When the active text editor changes ...
118
    mMainPane.textEditorProperty().addListener(
119
      ( _, _, n ) -> {
120
        // ... update the haystack.
121
        mSearchModel.search( getActiveTextEditor().getText() );
122
123
        // ... update the status bar with the current caret position.
124
        if( n != null ) {
125
          final var w = getWorkspace();
126
          final var recentDoc = w.fileProperty( KEY_UI_RECENT_DOCUMENT );
127
128
          // ... preserve the most recent document.
129
          recentDoc.setValue( n.getFile() );
130
          CaretMovedEvent.fire( n.getCaret() );
131
        }
132
      }
133
    );
134
  }
135
136
  public void file_new() {
137
    getMainPane().newTextEditor();
138
  }
139
140
  public void file_open() {
141
    pickFiles( FILE_OPEN_MULTIPLE ).ifPresent( l -> getMainPane().open( l ) );
142
  }
143
144
  public void file_open_url() {
145
    pickFile().ifPresent( l -> getMainPane().open( List.of( l ) ) );
146
  }
147
148
  public void file_close() {
149
    getMainPane().close();
150
  }
151
152
  public void file_close_all() {
153
    getMainPane().closeAll();
154
  }
155
156
  public void file_save() {
157
    getMainPane().save();
158
  }
159
160
  public void file_save_as() {
161
    pickFiles( FILE_SAVE_AS ).ifPresent( l -> getMainPane().saveAs( l ) );
162
  }
163
164
  public void file_save_all() {
165
    getMainPane().saveAll();
166
  }
167
168
  /**
169
   * Converts the actively edited file in the given file format.
170
   *
171
   * @param format The destination file format.
172
   */
173
  private void file_export( final ExportFormat format ) {
174
    file_export( format, false );
175
  }
176
177
  /**
178
   * Converts one or more files into the given file format. If {@code dir}
179
   * is set to true, this will first append all files in the same directory
180
   * as the actively edited file.
181
   *
182
   * @param format The destination file format.
183
   * @param dir    Export all files in the actively edited file's directory.
184
   */
185
  private void file_export( final ExportFormat format, final boolean dir ) {
186
    final var editor = getMainPane().getTextEditor();
187
    final var exported = getWorkspace().fileProperty( KEY_UI_RECENT_EXPORT );
188
    final var exportParent = exported.get().toPath().getParent();
189
    final var editorParent = editor.getPath().getParent();
190
    final var userHomeParent = USER_DIRECTORY.toPath();
191
    final var exportPath = exportParent != null
192
      ? exportParent
193
      : editorParent != null
194
      ? editorParent
195
      : userHomeParent;
196
197
    final var filename = format.toExportFilename( exported.get() );
198
199
    final var selection = pickFile(
200
      filename,
201
      exportPath,
202
      FILE_EXPORT
203
    );
204
205
    selection.ifPresent( files -> file_export( editor, format, files, dir ) );
206
  }
207
208
  private void file_export(
209
    final TextEditor editor,
210
    final ExportFormat format,
211
    final List<File> files,
212
    final boolean dir ) {
213
    editor.save();
214
    final var main = getMainPane();
215
    final var exported = getWorkspace().fileProperty( KEY_UI_RECENT_EXPORT );
216
217
    final var sourceFile = files.getFirst();
218
    final var sourcePath = sourceFile.toPath();
219
    final var document = dir ? append( editor ) : editor.getText();
220
    final var context = main.createProcessorContext( sourcePath, format );
221
222
    final var service = new Service<Path>() {
223
      @Override
224
      protected Task<Path> createTask() {
225
        final var task = new Task<Path>() {
226
          @Override
227
          protected Path call() throws Exception {
228
            final var chain = createProcessors( context );
229
            final var export = chain.apply( document );
230
231
            // Processors can export binary files. In such cases, processors
232
            // return null to prevent further processing.
233
            return export == null
234
              ? null
235
              : writeString( sourcePath, export, UTF_8 );
236
          }
237
        };
238
239
        task.setOnSucceeded(
240
          _ -> {
241
            // Remember the exported file name for next time.
242
            exported.setValue( sourceFile );
243
244
            final var result = task.getValue();
245
246
            // Binary formats must notify users of success independently.
247
            if( result != null ) {
248
              clue( "Main.status.export.success", result );
249
            }
250
          }
251
        );
252
253
        task.setOnFailed( _ -> {
254
          final var ex = task.getException();
255
          clue( ex );
256
257
          if( ex instanceof TypeNotPresentException ) {
258
            fireExportFailedEvent();
259
          }
260
        } );
261
262
        return task;
263
      }
264
    };
265
266
    mTypesetService = service;
267
    typeset( service );
268
  }
269
270
  /**
271
   * @param dir {@code true} means to export all files in the active file
272
   *            editor's directory; {@code false} means to export only the
273
   *            actively edited file.
274
   */
275
  private void file_export_pdf( final boolean dir ) {
276
    // Don't re-validate the typesetter installation each time. If the
277
    // user mucks up the typesetter installation, it'll get caught the
278
    // next time the application is started. Don't use |= because it
279
    // won't short-circuit.
280
    mCanTypeset = mCanTypeset || Typesetter.canRun();
281
282
    if( mCanTypeset ) {
283
      final var workspace = getWorkspace();
284
      final var theme = workspace.stringProperty(
285
        KEY_TYPESET_CONTEXT_THEME_SELECTION
286
      );
287
      final var chapters = workspace.stringProperty(
288
        KEY_TYPESET_CONTEXT_CHAPTERS
289
      );
290
291
      final var settings = ExportSettings
292
        .builder()
293
        .with( ExportSettings.Mutator::setTheme, theme )
294
        .with( ExportSettings.Mutator::setChapters, chapters )
295
        .build();
296
297
      final var themes = workspace.getFile(
298
        KEY_TYPESET_CONTEXT_THEMES_PATH
299
      );
300
301
      // If the typesetter is installed, allow the user to select a theme. If
302
      // the themes aren't installed, a status message will appear.
303
      if( ExportDialog.choose( getWindow(), themes, settings, dir ) ) {
304
        file_export( APPLICATION_PDF, dir );
305
      }
306
    }
307
    else {
308
      fireExportFailedEvent();
309
    }
310
  }
311
312
  public void file_export_pdf() {
313
    file_export_pdf( false );
314
  }
315
316
  public void file_export_pdf_dir() {
317
    file_export_pdf( true );
318
  }
319
320
  public void file_export_html_dir() {
321
    file_export( XHTML_TEX, true );
322
  }
323
324
  public void file_export_repeat() {
325
    typeset( mTypesetService );
326
  }
327
328
  public void file_export_html_svg() {
329
    file_export( HTML_TEX_SVG );
330
  }
331
332
  public void file_export_html_tex() {
333
    file_export( HTML_TEX_DELIMITED );
334
  }
335
336
  public void file_export_text_tex() {
337
    file_export( TEXT_TEX, false );
338
  }
339
340
  public void file_export_text_tex_dir() {
341
    file_export( TEXT_TEX, true );
342
  }
343
344
  public void file_export_xhtml_tex() {
345
    file_export( XHTML_TEX );
346
  }
347
348
  private void fireExportFailedEvent() {
349
    runLater( ExportFailedEvent::fire );
350
  }
351
352
  public void file_exit() {
353
    final var window = getWindow();
354
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
355
  }
356
357
  public void edit_undo() {
358
    getActiveTextEditor().undo();
359
  }
360
361
  public void edit_redo() {
362
    getActiveTextEditor().redo();
363
  }
364
365
  public void edit_cut() {
366
    getActiveTextEditor().cut();
367
  }
368
369
  public void edit_copy() {
370
    getActiveTextEditor().copy();
371
  }
372
373
  public void edit_paste() {
374
    getActiveTextEditor().paste();
375
  }
376
377
  public void edit_select_all() {
378
    getActiveTextEditor().selectAll();
379
  }
380
381
  public void edit_find() {
382
    final var nodes = getMainScene().getStatusBar().getLeftItems();
383
384
    if( nodes.isEmpty() ) {
385
      final var searchBar = new SearchBar();
386
387
      searchBar.matchIndexProperty().bind( mSearchModel.matchIndexProperty() );
388
      searchBar.matchCountProperty().bind( mSearchModel.matchCountProperty() );
389
390
      searchBar.setOnCancelAction( _ -> {
391
        final var editor = getActiveTextEditor();
392
        nodes.remove( searchBar );
393
        editor.unstylize( STYLE_SEARCH );
394
        editor.getNode().requestFocus();
395
      } );
396
397
      searchBar.addInputListener( ( _, _, n ) -> {
398
        if( n != null && !n.isEmpty() ) {
399
          mSearchModel.search( n, getActiveTextEditor().getText() );
400
        }
401
      } );
402
403
      searchBar.setOnNextAction( _ -> edit_find_next() );
404
      searchBar.setOnPrevAction( _ -> edit_find_prev() );
405
406
      nodes.add( searchBar );
407
      searchBar.requestFocus();
408
    }
409
  }
410
411
  public void edit_find_next() {
412
    mSearchModel.advance();
413
  }
414
415
  public void edit_find_prev() {
416
    mSearchModel.retreat();
417
  }
418
419
  public void edit_preferences() {
420
    try {
421
      new PreferencesController( getWorkspace() ).show();
422
    } catch( final Exception ex ) {
423
      clue( ex );
424
    }
425
  }
426
427
  public void format_bold() {
428
    getActiveTextEditor().bold();
429
  }
430
431
  public void format_italic() {
432
    getActiveTextEditor().italic();
433
  }
434
435
  public void format_monospace() {
436
    getActiveTextEditor().monospace();
437
  }
438
439
  public void format_superscript() {
440
    getActiveTextEditor().superscript();
441
  }
442
443
  public void format_subscript() {
444
    getActiveTextEditor().subscript();
445
  }
446
447
  public void format_strikethrough() {
448
    getActiveTextEditor().strikethrough();
449
  }
450
451
  public void insert_blockquote() {
452
    getActiveTextEditor().blockquote();
453
  }
454
455
  public void insert_code() {
456
    getActiveTextEditor().code();
457
  }
458
459
  public void insert_fenced_code_block() {
460
    getActiveTextEditor().fencedCodeBlock();
461
  }
462
463
  public void insert_link() {
464
    insertObject( createLinkDialog() );
465
  }
466
467
  public void insert_image() {
468
    insertObject( createImageDialog() );
469
  }
470
471
  private void insertObject( final Dialog<String> dialog ) {
472
    final var textArea = getActiveTextEditor().getTextArea();
473
    dialog.showAndWait().ifPresent( textArea::replaceSelection );
474
  }
475
476
  private Dialog<String> createLinkDialog() {
477
    return new HyperlinkDialog( getWindow(), createHyperlinkModel() );
478
  }
479
480
  private Dialog<String> createImageDialog() {
481
    return new ImageDialog( getWindow(), createImageModel() );
482
  }
483
484
  /**
485
   * Returns one of: selected text, word under cursor, or parsed hyperlink from
486
   * the Markdown AST. When a user opts to insert a hyperlink, this will
487
   * populate the insert hyperlink dialog with data from the document, thereby
488
   * allowing a user to edit an existing link.
489
   *
490
   * @return An instance containing the link URL and display text.
491
   */
492
  private HyperlinkModel createHyperlinkModel() {
493
    final var context = getMainPane().createProcessorContext();
494
    final var editor = getActiveTextEditor();
495
    final var textArea = editor.getTextArea();
496
    final var selectedText = textArea.getSelectedText();
497
498
    // Convert current paragraph to Markdown nodes.
499
    final var mp = MarkdownProcessor.create( context );
500
    final var p = textArea.getCurrentParagraph();
501
    final var paragraph = textArea.getText( p );
502
    final var node = mp.toNode( paragraph );
503
    final var visitor = new LinkVisitor( textArea.getCaretColumn() );
504
    final var link = visitor.process( node );
505
506
    if( link != null ) {
507
      textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
508
    }
509
510
    return createHyperlinkModel( link, selectedText );
511
  }
512
513
  private HyperlinkModel createHyperlinkModel(
514
    final Link link, final String selection ) {
515
516
    return link == null
517
      ? new HyperlinkModel( selection )
518
      : new HyperlinkModel( link );
519
  }
520
521
  private ImageModel createImageModel() {
522
    return new ImageModel( "" );
523
  }
524
525
  public void insert_heading_1() {
526
    insert_heading( 1 );
527
  }
528
529
  public void insert_heading_2() {
530
    insert_heading( 2 );
531
  }
532
533
  public void insert_heading_3() {
534
    insert_heading( 3 );
535
  }
536
537
  private void insert_heading( final int level ) {
538
    getActiveTextEditor().heading( level );
539
  }
540
541
  public void insert_unordered_list() {
542
    getActiveTextEditor().unorderedList();
543
  }
544
545
  public void insert_ordered_list() {
546
    getActiveTextEditor().orderedList();
547
  }
548
549
  public void insert_horizontal_rule() {
550
    getActiveTextEditor().horizontalRule();
551
  }
552
553
  public void definition_create() {
554
    getActiveTextDefinition().createDefinition();
555
  }
556
557
  public void definition_rename() {
558
    getActiveTextDefinition().renameDefinition();
559
  }
560
561
  public void definition_delete() {
562
    getActiveTextDefinition().deleteDefinitions();
563
  }
564
565
  public void definition_autoinsert() {
566
    getMainPane().autoinsert();
567
  }
568
569
  public void view_refresh() {
570
    getMainPane().viewRefresh();
571
  }
572
573
  public void view_preview() {
574
    getMainPane().viewPreview();
575
  }
576
577
  public void view_outline() {
578
    getMainPane().viewOutline();
579
  }
580
581
  public void view_files() {getMainPane().viewFiles();}
582
583
  public void view_statistics() {
584
    getMainPane().viewStatistics();
585
  }
586
587
  public void view_menubar() {
588
    getMainScene().toggleMenuBar();
589
  }
590
591
  public void view_toolbar() {
592
    getMainScene().toggleToolBar();
593
  }
594
595
  public void view_statusbar() {
596
    getMainScene().toggleStatusBar();
597
  }
598
599
  public void view_log() {
600
    mLogView.view();
601
  }
602
603
  public void help_about() {
604
    final var alert = new Alert( INFORMATION );
605
    final var prefix = "Dialog.about.";
606
    alert.setTitle( get( String.format( "%s%s", prefix, "title" ), APP_TITLE ) );
607
    alert.setHeaderText( get( String.format( "%s%s", prefix, "header" ), APP_TITLE ) );
608
    alert.setContentText( get( String.format( "%s%s", prefix, "content" ),
609
                               APP_YEAR,
610
                               APP_VERSION ) );
611
    alert.setGraphic( ICON_DIALOG_NODE );
612
    alert.initOwner( getWindow() );
613
    alert.showAndWait();
614
  }
615
616
  private <T> void typeset( final Service<T> service ) {
617
    synchronized( mMutex ) {
618
      if( service != null && !service.isRunning() ) {
619
        service.reset();
620
        service.start();
621
      }
622
    }
623
  }
624
625
  /**
626
   * Concatenates all the files in the same directory as the given file into
627
   * a string. The extension is determined by the given file name pattern; the
628
   * order files are concatenated is based on their numeric sort order (this
629
   * avoids lexicographic sorting).
630
   * <p>
631
   * If the parent path to the file being edited in the text editor cannot
632
   * be found then this will return the editor's text, without iterating through
633
   * the parent directory. (Should never happen, but who knows?)
634
   * </p>
635
   * <p>
636
   * New lines are automatically appended to separate each file.
637
   * </p>
638
   *
639
   * @param editor The text editor containing
640
   * @return All files in the same directory as the file being edited
641
   * concatenated into a single string.
642
   */
643
  private String append( final TextEditor editor ) {
644
    final var pattern = editor.getPath();
645
    final var parent = pattern.getParent();
646
647
    // Short-circuit because nothing else can be done.
648
    if( parent == null ) {
649
      clue( "Main.status.export.concat.parent", pattern );
650
      return editor.getText();
651
    }
652
653
    final var filename = SysFile.getFileName( pattern );
654
    final var extension = getExtension( filename );
655
656
    if( extension.isBlank() ) {
657
      clue( "Main.status.export.concat.extension", filename );
658
      return editor.getText();
659
    }
660
661
    try {
662
      final var command = new ConcatenateCommand(
663
        parent, extension, getString( KEY_TYPESET_CONTEXT_CHAPTERS ) );
664
      return command.call();
665
    } catch( final Throwable t ) {
666
      clue( t );
667
      return editor.getText();
668
    }
669
  }
670
671
  private Optional<File> pickFile() {
672
    final var editor = getActiveTextEditor();
673
    final var file = editor == null ? USER_DIRECTORY : editor.getFile();
674
    final var path = SysFile.toFile( file.toPath() );
675
    final var parent = Path.of( path.getParent() );
676
677
    return new OpenUrlDialog( getWindow(), parent ).showAndWait();
678
  }
679
680
  private Optional<List<File>> pickFiles( final SelectionType type ) {
681
    return createPicker( type ).choose();
682
  }
683
684
  @SuppressWarnings( "SameParameterValue" )
685
  private Optional<List<File>> pickFile(
686
    final File file,
687
    final Path directory,
688
    final SelectionType type ) {
689
    final var picker = createPicker( type );
690
    picker.setInitialFilename( file );
691
    picker.setInitialDirectory( directory );
692
    return picker.choose();
693
  }
694
695
  private FilePicker createPicker( final SelectionType type ) {
696
    final var factory = new FilePickerFactory( getWorkspace() );
697
    return factory.createModal( getWindow(), type );
698
  }
699
700
  private TextEditor getActiveTextEditor() {
701
    return getMainPane().getTextEditor();
702
  }
703
704
  private TextDefinition getActiveTextDefinition() {
705
    return getMainPane().getTextDefinition();
706
  }
707
708
  private MainScene getMainScene() {
709
    return mMainScene;
710
  }
711
712
  private MainPane getMainPane() {
713
    return mMainPane;
714
  }
715
716
  private Workspace getWorkspace() {
717
    return mMainPane.getWorkspace();
718
  }
719
720
  @SuppressWarnings( "SameParameterValue" )
721
  private String getString( final Key key ) {
722
    return getWorkspace().getString( key );
723
  }
724
725
  private Window getWindow() {
726
    return getMainPane().getWindow();
727
  }
728
}
1729
A src/main/java/com/keenwrite/ui/actions/Keyboard.java
1
package com.keenwrite.ui.actions;
2
3
import javafx.scene.input.KeyCodeCombination;
4
import javafx.scene.input.KeyEvent;
5
6
import static javafx.scene.input.KeyCode.C;
7
import static javafx.scene.input.KeyCode.INSERT;
8
import static javafx.scene.input.KeyCombination.CONTROL_ANY;
9
10
public class Keyboard {
11
  public static final KeyCodeCombination CTRL_C =
12
    new KeyCodeCombination( C, CONTROL_ANY );
13
  public static final KeyCodeCombination CTRL_INSERT =
14
    new KeyCodeCombination( INSERT, CONTROL_ANY );
15
16
  /**
17
   * Answers whether the user issued a copy request via the keyboard.
18
   *
19
   * @param event The keyboard event to examine.
20
   * @return {@code true} if the user pressed Ctrl+C or Ctrl+Insert.
21
   */
22
  public static boolean isCopy( final KeyEvent event ) {
23
    return CTRL_C.match( event ) || CTRL_INSERT.match( event );
24
  }
25
}
126
A src/main/java/com/keenwrite/ui/actions/MenuAction.java
1
/* Copyright 2020-2021 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-2021 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 final 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-2021 White Magic Software, Ltd. -- All rights reserved. */
2
3
/**
4
 * This package contains classes that define commands as executable actions.
5
 */
6
package com.keenwrite.ui.actions;
17
A src/main/java/com/keenwrite/ui/adapters/DocumentAdapter.java
1
/* Copyright 2020-2021 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.events.StatusEvent.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-2021 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/cells/AltTableCell.java
1
package com.keenwrite.ui.cells;
2
3
import javafx.scene.control.cell.TextFieldTableCell;
4
import javafx.util.StringConverter;
5
6
public class AltTableCell<S, T> extends TextFieldTableCell<S, T> {
7
  public AltTableCell( final StringConverter<T> converter ) {
8
    super( converter );
9
10
    assert converter != null;
11
12
    new CellEditor(
13
      input -> commitEdit( getConverter().fromString( input ) ),
14
      graphicProperty()
15
    );
16
  }
17
}
118
A src/main/java/com/keenwrite/ui/cells/AltTreeCell.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.cells;
3
4
import javafx.scene.control.cell.TextFieldTreeCell;
5
import javafx.util.StringConverter;
6
7
/**
8
 * Responsible for enhancing the existing cell behaviour with fairly common
9
 * functionality, including commit on focus loss and Enter to commit.
10
 *
11
 * @param <T> The type of data stored by the tree.
12
 */
13
public class AltTreeCell<T> extends TextFieldTreeCell<T> {
14
  public AltTreeCell( final StringConverter<T> converter ) {
15
    super( converter );
16
17
    assert converter != null;
18
19
    new CellEditor(
20
      input -> commitEdit( getConverter().fromString( input ) ),
21
      graphicProperty()
22
    );
23
  }
24
}
125
A src/main/java/com/keenwrite/ui/cells/CellEditor.java
1
package com.keenwrite.ui.cells;
2
3
import javafx.beans.property.ObjectProperty;
4
import javafx.beans.property.Property;
5
import javafx.beans.property.SimpleStringProperty;
6
import javafx.beans.value.ChangeListener;
7
import javafx.beans.value.ObservableValue;
8
import javafx.event.EventHandler;
9
import javafx.scene.Node;
10
import javafx.scene.control.TableCell;
11
import javafx.scene.control.TextField;
12
import javafx.scene.control.TreeCell;
13
import javafx.scene.input.KeyEvent;
14
15
import java.util.function.Consumer;
16
17
import static javafx.application.Platform.runLater;
18
import static javafx.scene.input.KeyCode.ENTER;
19
import static javafx.scene.input.KeyCode.TAB;
20
import static javafx.scene.input.KeyEvent.KEY_RELEASED;
21
22
public class CellEditor {
23
  private FocusListener mFocusListener;
24
  private final KeyHandler mKeyHandler = new KeyHandler();
25
  private final Property<String> mInputText = new SimpleStringProperty();
26
  private final Consumer<String> mConsumer;
27
28
  /**
29
   * Responsible for accepting the text when users press the Enter or Tab key.
30
   */
31
  private class KeyHandler implements EventHandler<KeyEvent> {
32
    @Override
33
    public void handle( final KeyEvent event ) {
34
      if( event.getCode() == ENTER || event.getCode() == TAB ) {
35
        commitEdit();
36
        event.consume();
37
      }
38
    }
39
  }
40
41
  /**
42
   * Responsible for committing edits when focus is lost. This will also
43
   * deselect the input field when focus is gained so that typing text won't
44
   * overwrite the entire existing text.
45
   */
46
  private class FocusListener implements ChangeListener<Boolean> {
47
    private final TextField mInput;
48
49
    private FocusListener( final TextField input ) {
50
      mInput = input;
51
    }
52
53
    @Override
54
    public void changed(
55
      final ObservableValue<? extends Boolean> c,
56
      final Boolean endedFocus, final Boolean beganFocus ) {
57
58
      if( beganFocus ) {
59
        runLater( mInput::deselect );
60
      }
61
      else if( endedFocus ) {
62
        commitEdit();
63
      }
64
    }
65
  }
66
67
  /**
68
   * Generalized cell editor suitable for use with {@link TableCell} or
69
   * {@link TreeCell} instances.
70
   *
71
   * @param consumer        Converts the field input text to the required
72
   *                        data type.
73
   * @param graphicProperty Defines the graphical user input field.
74
   */
75
  public CellEditor(
76
    final Consumer<String> consumer,
77
    final ObjectProperty<Node> graphicProperty ) {
78
    assert consumer != null;
79
    mConsumer = consumer;
80
81
    init( graphicProperty );
82
  }
83
84
  private void init( final ObjectProperty<Node> graphicProperty ) {
85
    // When the text field is added as the graphics context, we hook into
86
    // the changed value to get a handle on the text field. From there it is
87
    // possible to add change the keyboard and focus behaviours.
88
    graphicProperty.addListener( ( c, o, n ) -> {
89
      if( o instanceof TextField ) {
90
        o.removeEventHandler( KEY_RELEASED, mKeyHandler );
91
        o.focusedProperty().removeListener( mFocusListener );
92
      }
93
94
      if( n instanceof final TextField input ) {
95
        n.addEventFilter( KEY_RELEASED, mKeyHandler );
96
        mInputText.bind( input.textProperty() );
97
        mFocusListener = new FocusListener( input );
98
        n.focusedProperty().addListener( mFocusListener );
99
      }
100
    } );
101
  }
102
103
  private void commitEdit() {
104
    mConsumer.accept( mInputText.getValue() );
105
  }
106
}
1107
A src/main/java/com/keenwrite/ui/clipboard/SystemClipboard.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.clipboard;
6
7
import javafx.scene.control.TableView;
8
import javafx.scene.input.ClipboardContent;
9
10
import java.util.TreeSet;
11
12
import static javafx.scene.input.Clipboard.getSystemClipboard;
13
14
/**
15
 * Responsible for pasting into the computer's clipboard.
16
 */
17
public class SystemClipboard {
18
  /**
19
   * Copies the given text into the clipboard, overwriting all data.
20
   *
21
   * @param text The text to insert into the clipboard.
22
   */
23
  public static void write( final String text ) {
24
    final var contents = new ClipboardContent();
25
    contents.putString( text );
26
    getSystemClipboard().setContent( contents );
27
  }
28
29
  /**
30
   * Delegates to {@link #write(String)}.
31
   *
32
   * @see #write(String)
33
   */
34
  public static void write( final StringBuilder text ) {
35
    write( text.toString() );
36
  }
37
38
  /**
39
   * Copies the contents of the selected rows into the clipboard; code is from
40
   * <a href="https://stackoverflow.com/a/48126059/59087">StackOverflow</a>.
41
   *
42
   * @param table The {@link TableView} having selected rows to copy.
43
   */
44
  public static <T> void write( final TableView<T> table ) {
45
    final var sb = new StringBuilder( 2048 );
46
    final var rows = new TreeSet<Integer>();
47
    final var cols = table.getColumns();
48
49
    for( final var position : table.getSelectionModel().getSelectedCells() ) {
50
      rows.add( position.getRow() );
51
    }
52
53
    String rSep = "";
54
55
    for( final var row : rows ) {
56
      sb.append( rSep );
57
58
      String cSep = "";
59
60
      for( final var column : cols ) {
61
        sb.append( cSep );
62
63
        final var data = column.getCellData( row );
64
        sb.append( data == null ? "" : data.toString() );
65
66
        cSep = "\t";
67
      }
68
69
      rSep = "\n";
70
    }
71
72
    write( sb );
73
  }
74
}
175
A src/main/java/com/keenwrite/ui/controls/BrowseButton.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.controls;
6
7
import com.keenwrite.Messages;
8
import javafx.event.ActionEvent;
9
import javafx.scene.control.Button;
10
import javafx.stage.DirectoryChooser;
11
12
import java.io.File;
13
import java.util.function.Consumer;
14
15
import static com.keenwrite.ui.fonts.IconFactory.createGraphic;
16
import static org.controlsfx.glyphfont.FontAwesome.Glyph.FILE_ALT;
17
18
/**
19
 * Responsible for browsing directories.
20
 */
21
public class BrowseButton extends Button {
22
  /**
23
   * Initial directory.
24
   */
25
  private final File mDirectory;
26
27
  /**
28
   * Called when the user accepts a directory.
29
   */
30
  private final Consumer<File> mConsumer;
31
32
  public BrowseButton( final File directory, final Consumer<File> consumer ) {
33
    assert directory != null;
34
    assert consumer != null;
35
36
    mDirectory = directory;
37
    mConsumer = consumer;
38
39
    setGraphic( createGraphic( FILE_ALT ) );
40
    setOnAction( this::browse );
41
  }
42
43
  public void browse( final ActionEvent ignored ) {
44
    final var chooser = new DirectoryChooser();
45
    chooser.setTitle( Messages.get( "BrowseDirectoryButton.chooser.title" ) );
46
    chooser.setInitialDirectory( mDirectory );
47
48
    final var result = chooser.showDialog( getScene().getWindow() );
49
50
    if( result != null ) {
51
      mConsumer.accept( result );
52
    }
53
  }
54
}
155
A src/main/java/com/keenwrite/ui/controls/EventedStatusBar.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.controls;
3
4
import com.keenwrite.events.StatusEvent;
5
import org.controlsfx.control.StatusBar;
6
import org.greenrobot.eventbus.Subscribe;
7
8
import static com.keenwrite.events.Bus.register;
9
import static javafx.application.Platform.isFxApplicationThread;
10
import static javafx.application.Platform.runLater;
11
12
/**
13
 * Responsible for handling application status events.
14
 */
15
public class EventedStatusBar extends StatusBar {
16
  public EventedStatusBar() {
17
    register( this );
18
  }
19
20
  /**
21
   * Called when an application problem is encountered. Updates the status
22
   * bar to show the first line of the given message. This method is
23
   * idempotent (if the message text is already set to the text from the
24
   * given message, no update is performed).
25
   *
26
   * @param event The event containing information about the problem.
27
   */
28
  @Subscribe
29
  public void handle( final StatusEvent event ) {
30
    final var m = event.toString();
31
32
    // Don't burden the repaint thread if there's no status bar change.
33
    if( !getText().equals( m ) ) {
34
      final var i = m.indexOf( '\n' );
35
36
      final Runnable update =
37
        () -> setText( m.substring( 0, i > 0 ? i : m.length() ) );
38
39
      if( isFxApplicationThread() ) {
40
        update.run();
41
      }
42
      else {
43
        runLater( update );
44
      }
45
    }
46
  }
47
}
148
A src/main/java/com/keenwrite/ui/controls/SearchBar.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.controls;
3
4
import javafx.beans.property.IntegerProperty;
5
import javafx.beans.property.SimpleIntegerProperty;
6
import javafx.beans.value.ChangeListener;
7
import javafx.event.ActionEvent;
8
import javafx.event.EventHandler;
9
import javafx.geometry.Pos;
10
import javafx.scene.Node;
11
import javafx.scene.control.*;
12
import javafx.scene.layout.HBox;
13
import javafx.scene.layout.Region;
14
import javafx.scene.layout.VBox;
15
import org.controlsfx.control.textfield.CustomTextField;
16
17
import static com.keenwrite.Messages.get;
18
import static com.keenwrite.ui.fonts.IconFactory.createGraphic;
19
import static java.lang.StrictMath.max;
20
import static java.lang.String.format;
21
import static javafx.geometry.Orientation.VERTICAL;
22
import static javafx.scene.layout.Priority.ALWAYS;
23
24
/**
25
 * Responsible for presenting user interface options for searching through
26
 * the document.
27
 */
28
public final class SearchBar extends HBox {
29
30
  private static final String MESSAGE_KEY = "Main.search.%s.%s";
31
32
  private final Button mButtonStop = createButtonStop();
33
  private final Button mButtonNext = createButton( "next" );
34
  private final Button mButtonPrev = createButton( "prev" );
35
  private final TextField mFind = createTextField();
36
  private final Label mMatches = new Label();
37
  private final IntegerProperty mMatchIndex = new SimpleIntegerProperty();
38
  private final IntegerProperty mMatchCount = new SimpleIntegerProperty();
39
40
  public SearchBar() {
41
    setAlignment( Pos.CENTER );
42
    addAll(
43
      mButtonStop,
44
      createSpacer( 10 ),
45
      mFind,
46
      createSpacer( 10 ),
47
      mButtonNext,
48
      createSpacer( 10 ),
49
      mButtonPrev,
50
      createSpacer( 10 ),
51
      mMatches,
52
      createSpacer( 10 ),
53
      createSeparatorVertical(),
54
      createSpacer( 5 )
55
    );
56
57
    mMatchIndex.addListener( ( c, o, n ) -> updateMatchText() );
58
    mMatchCount.addListener( ( c, o, n ) -> updateMatchText() );
59
    updateMatchText();
60
  }
61
62
  /**
63
   * Gives focus to the text field.
64
   */
65
  @Override
66
  public void requestFocus() {
67
    mFind.requestFocus();
68
  }
69
70
  /**
71
   * Adds a listener that triggers when the input text field changes.
72
   *
73
   * @param listener The listener to notify of change events.
74
   */
75
  public void addInputListener( final ChangeListener<String> listener ) {
76
    mFind.textProperty().addListener( listener );
77
  }
78
79
  /**
80
   * Sets the {@link EventHandler} to call when the user interface triggers
81
   * finding the next matching search string. This will wrap from the end
82
   * to the beginning.
83
   *
84
   * @param handler The handler requested to perform the find next action.
85
   */
86
  public void setOnNextAction( final EventHandler<ActionEvent> handler ) {
87
    mButtonNext.setOnAction( handler );
88
    mFind.setOnAction( handler );
89
  }
90
91
  /**
92
   * Sets the {@link EventHandler} to call when the user interface triggers
93
   * finding the previous matching search string. This will wrap from the
94
   * beginning to the end.
95
   *
96
   * @param handler The handler requested to perform the find next action.
97
   */
98
  public void setOnPrevAction( final EventHandler<ActionEvent> handler ) {
99
    mButtonPrev.setOnAction( handler );
100
  }
101
102
  /**
103
   * Sets the {@link EventHandler} to call when searching has been terminated.
104
   *
105
   * @param handler The {@link EventHandler} that will perform an action
106
   *                when the searching has stopped (e.g., remove from this
107
   *                widget from status bar).
108
   */
109
  public void setOnCancelAction( final EventHandler<ActionEvent> handler ) {
110
    mButtonStop.setOnAction( handler );
111
  }
112
113
  /**
114
   * When this property value changes, the match text is updated accordingly.
115
   * If the value is less than zero, the text will show zero.
116
   *
117
   * @return The index of the latest search string match.
118
   */
119
  public IntegerProperty matchIndexProperty() {
120
    return mMatchIndex;
121
  }
122
123
  /**
124
   * When this property value changes, the match text is updated accordingly.
125
   * If the value is less than zero, the text will show zero.
126
   *
127
   * @return The total number of items that match the search string.
128
   */
129
  public IntegerProperty matchCountProperty() {
130
    return mMatchCount;
131
  }
132
133
  /**
134
   * Updates the match count.
135
   */
136
  private void updateMatchText() {
137
    final var index = max( 0, mMatchIndex.get() );
138
    final var count = max( 0, mMatchCount.get() );
139
    final var suffix = count == 0 ? "none" : "some";
140
    final var key = getMessageValue( "match", suffix );
141
142
    mMatches.setText( get( key, index, count ) );
143
  }
144
145
  private Button createButton( final String id ) {
146
    final var button = new Button();
147
    final var tooltipText = getMessageValue( id, "tooltip" );
148
149
    button.setMnemonicParsing( false );
150
    button.setGraphic( getIcon( id ) );
151
    button.setTooltip( new Tooltip( tooltipText ) );
152
153
    return button;
154
  }
155
156
  private Button createButtonStop() {
157
    final var button = createButton( "stop" );
158
    button.setCancelButton( true );
159
    return button;
160
  }
161
162
  private TextField createTextField() {
163
    final var textField = new CustomTextField();
164
    textField.setLeft( getIcon( "find" ) );
165
    return textField;
166
  }
167
168
  private void addAll( final Node... nodes ) {
169
    getChildren().addAll( nodes );
170
  }
171
172
  /**
173
   * Creates a vertical bar, used to divide the search results from the
174
   * application status message.
175
   *
176
   * @return A vertical separator.
177
   */
178
  private static Node createSeparatorVertical() {
179
    return new Separator( VERTICAL );
180
  }
181
182
  /**
183
   * Breathing room between the search box and the application status message.
184
   * This could also be accomplished by using CSS.
185
   *
186
   * @param width The spacer's width.
187
   * @return A new {@link Node} having about 10px of space.
188
   */
189
  private static Node createSpacer( final int width ) {
190
    final var spacer = new Region();
191
    spacer.setPrefWidth( width );
192
    VBox.setVgrow( spacer, ALWAYS );
193
    return spacer;
194
  }
195
196
  private static Node getIcon( final String id ) {
197
    return createGraphic( getMessageValue( id, "icon" ) );
198
  }
199
200
  private static String getMessageValue( final String id,
201
                                         final String suffix ) {
202
    return get( format( MESSAGE_KEY, id, suffix ) );
203
  }
204
}
1205
A src/main/java/com/keenwrite/ui/dialogs/AbstractDialog.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.dialogs;
6
7
import com.keenwrite.service.events.impl.ButtonOrderPane;
8
import javafx.scene.control.Dialog;
9
import javafx.stage.Stage;
10
import javafx.stage.Window;
11
12
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
13
import static com.keenwrite.Messages.get;
14
import static com.keenwrite.util.Strings.validate;
15
import static javafx.scene.control.ButtonType.CANCEL;
16
import static javafx.scene.control.ButtonType.OK;
17
18
/**
19
 * Superclass that abstracts common behaviours for all dialogs.
20
 *
21
 * @param <T> The type of dialog to create (usually String).
22
 */
23
public abstract class AbstractDialog<T> extends Dialog<T> {
24
25
  /**
26
   * Ensures that all dialogs can be closed.
27
   *
28
   * @param owner The parent window of this dialog.
29
   * @param title The messages title to display in the title bar.
30
   */
31
  public AbstractDialog( final Window owner, final String title ) {
32
    assert owner != null;
33
    assert validate( title );
34
35
    setTitle( get( title ) );
36
    setResizable( true );
37
38
    initOwner( owner );
39
    initCloseAction();
40
    initDialogPane();
41
    initDialogButtons();
42
    initComponents();
43
44
    if( owner instanceof Stage stage ) {
45
      initIcon( stage );
46
    }
47
  }
48
49
  /**
50
   * Initialize the component layout.
51
   */
52
  protected abstract void initComponents();
53
54
  /**
55
   * Set the dialog to use a button order pane with an OK and a CANCEL button.
56
   */
57
  protected void initDialogPane() {
58
    setDialogPane( new ButtonOrderPane() );
59
  }
60
61
  /**
62
   * Set an OK and CANCEL button on the dialog.
63
   */
64
  protected void initDialogButtons() {
65
    getDialogPane().getButtonTypes().addAll( OK, CANCEL );
66
  }
67
68
  /**
69
   * Attaches a close request to the dialog's [X] button so that the user
70
   * can always close the window, even if there's an error.
71
   */
72
  protected final void initCloseAction() {
73
    final var window = getDialogPane().getScene().getWindow();
74
    window.setOnCloseRequest( _ -> window.hide() );
75
  }
76
77
  private void initIcon( final Stage owner ) {
78
    owner.getIcons().add( ICON_DIALOG );
79
  }
80
}
181
A src/main/java/com/keenwrite/ui/dialogs/CustomDialog.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.dialogs;
6
7
import com.keenwrite.Messages;
8
import com.keenwrite.service.events.impl.ButtonOrderPane;
9
import javafx.application.Platform;
10
import javafx.beans.value.ChangeListener;
11
import javafx.geometry.Insets;
12
import javafx.scene.control.ButtonBar.ButtonData;
13
import javafx.scene.control.Dialog;
14
import javafx.scene.control.Label;
15
import javafx.scene.control.TextField;
16
import javafx.scene.layout.ColumnConstraints;
17
import javafx.scene.layout.GridPane;
18
import javafx.stage.Window;
19
20
import java.util.LinkedList;
21
import java.util.List;
22
23
import static com.keenwrite.Messages.get;
24
import static com.keenwrite.util.Strings.validate;
25
import static javafx.scene.control.ButtonType.CANCEL;
26
import static javafx.scene.control.ButtonType.OK;
27
import static javafx.scene.layout.Priority.ALWAYS;
28
import static javafx.scene.layout.Priority.NEVER;
29
30
/**
31
 * TODO: This class could be combined with {@link AbstractDialog}, either
32
 *   directly or through inheritance.
33
 *
34
 * @param <T> The type of data returned from the dialog upon acceptance.
35
 */
36
public abstract class CustomDialog<T> extends Dialog<T> {
37
  private final GridPane mContentPane = new GridPane( 10, 10 );
38
  private final List<TextField> mInputFields = new LinkedList<>();
39
40
  public CustomDialog( final Window owner, final String title ) {
41
    assert owner != null;
42
    assert validate( title );
43
44
    initOwner( owner );
45
    setTitle( get( title ) );
46
    setResizable( true );
47
  }
48
49
  /**
50
   * Allows for late binding so that input fields can be populated after
51
   * the constructor is called.
52
   */
53
  protected void initialize() {
54
    initDialogPane();
55
    initDialogButtons();
56
    initInputFields();
57
    initContentPane();
58
59
    assert !mInputFields.isEmpty();
60
61
    final var first = mInputFields.getFirst();
62
    assert first != null;
63
64
    Platform.runLater( first::requestFocus );
65
66
    setResultConverter( button -> {
67
      final ButtonData data = button == null ? null : button.getButtonData();
68
      return data == ButtonData.OK_DONE ? handleAccept() : null;
69
    } );
70
  }
71
72
  /**
73
   * Invoked when the user selects the OK button to confirm the input values.
74
   *
75
   * @return The type of data provided by using the dialog.
76
   */
77
  protected abstract T handleAccept();
78
79
  /**
80
   * Subclasses must call this method at least once.
81
   *
82
   * @param id     The unique identifier for the input field.
83
   * @param label  The input field's label property key.
84
   * @param prompt The prompt property key, which provides context.
85
   * @param value  The initial value to provide for the field.
86
   * @see Messages#get(String)
87
   */
88
  protected void addInputField(
89
    final String id,
90
    final String label,
91
    final String prompt,
92
    final String value,
93
    final ChangeListener<String> listener ) {
94
    assert validate( id );
95
    assert validate( label );
96
    assert validate( prompt );
97
    assert validate( value );
98
99
    final int row = mInputFields.size();
100
    final Label fieldLabel = new Label( get( label ) );
101
    final TextField fieldInput = new TextField();
102
103
    fieldInput.setPromptText( get( prompt ) );
104
    fieldInput.setId( id );
105
    fieldInput.textProperty().addListener( listener );
106
    fieldInput.setText( value );
107
108
    mContentPane.add( fieldLabel, 0, row );
109
    mContentPane.add( fieldInput, 1, row );
110
    mInputFields.add( fieldInput );
111
  }
112
113
  /**
114
   * Subclasses must add at least one input field.
115
   */
116
  protected abstract void initInputFields();
117
118
  /**
119
   * Set the dialog to use a button order pane with an OK and a CANCEL button.
120
   */
121
  protected void initDialogPane() {
122
    setDialogPane( new ButtonOrderPane() );
123
  }
124
125
  /**
126
   * Set an OK and CANCEL button on the dialog.
127
   */
128
  protected void initDialogButtons() {
129
    getDialogPane().getButtonTypes().addAll( OK, CANCEL );
130
  }
131
132
  /**
133
   * Called after the input fields have been added. This adds the input
134
   * fields to the main dialog pane.
135
   */
136
  protected void initContentPane() {
137
    mContentPane.setPadding( new Insets( 20, 10, 10, 10 ) );
138
139
    final var cc1 = new ColumnConstraints();
140
    final var cc2 = new ColumnConstraints();
141
142
    cc1.setHgrow( NEVER );
143
    cc2.setHgrow( ALWAYS );
144
    cc2.setMinWidth( 250 );
145
    mContentPane.getColumnConstraints().addAll( cc1, cc2 );
146
147
    getDialogPane().setContent( mContentPane );
148
  }
149
}
1150
A src/main/java/com/keenwrite/ui/dialogs/ExportDialog.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.dialogs;
6
7
import com.keenwrite.events.ExportFailedEvent;
8
import com.keenwrite.io.SysFile;
9
import com.keenwrite.util.Diacritics;
10
import com.keenwrite.util.FileWalker;
11
import com.keenwrite.util.RangeValidator;
12
import com.keenwrite.util.ResourceWalker;
13
import javafx.geometry.Insets;
14
import javafx.scene.Node;
15
import javafx.scene.control.ComboBox;
16
import javafx.scene.control.Label;
17
import javafx.scene.control.TextField;
18
import javafx.scene.image.Image;
19
import javafx.scene.input.KeyCode;
20
import javafx.scene.layout.GridPane;
21
import javafx.scene.text.Font;
22
import javafx.stage.Stage;
23
import javafx.stage.Window;
24
25
import java.io.File;
26
import java.io.FileInputStream;
27
import java.io.IOException;
28
import java.io.InputStreamReader;
29
import java.nio.charset.StandardCharsets;
30
import java.nio.file.Path;
31
import java.util.Collections;
32
import java.util.LinkedList;
33
import java.util.List;
34
import java.util.Properties;
35
import java.util.concurrent.atomic.AtomicReference;
36
37
import static com.keenwrite.Messages.get;
38
import static com.keenwrite.constants.Constants.THEME_NAME_LENGTH;
39
import static com.keenwrite.constants.Constants.UI_CONTROL_SPACING;
40
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
41
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE;
42
import static com.keenwrite.events.StatusEvent.clue;
43
import static com.keenwrite.io.SysFile.toFile;
44
import static com.keenwrite.util.FileWalker.walk;
45
import static com.keenwrite.util.Strings.abbreviate;
46
import static java.lang.Math.max;
47
import static java.nio.charset.StandardCharsets.UTF_8;
48
import static javafx.application.Platform.runLater;
49
import static javafx.geometry.Pos.CENTER;
50
import static javafx.scene.control.ButtonType.OK;
51
52
/**
53
 * Provides controls for exporting to PDF, such as selecting a theme and
54
 * creating a subset of chapter numbers.
55
 */
56
public final class ExportDialog extends AbstractDialog<ExportSettings> {
57
  private record Theme( Path path, String name ) implements Comparable<Theme> {
58
    /**
59
     * Answers whether the given theme directory name matches the theme name
60
     * that the user selected.
61
     *
62
     * @param themeDir The user-selected directory to compare with the
63
     *                 corresponding path of this {@link Theme}.
64
     * @return {@code true} if the given directory matches the ending portion
65
     * of the {@link Path} associated with this {@link Theme} instance.
66
     */
67
    public boolean matches( final String themeDir ) {
68
      final var f = SysFile.getFileName( path() );
69
70
      return f.equalsIgnoreCase( Diacritics.remove( themeDir ) );
71
    }
72
73
    /**
74
     * Returns the theme's display name.
75
     *
76
     * @return The name of the theme presented to users.
77
     */
78
    @Override
79
    public String toString() {
80
      return abbreviate( name(), THEME_NAME_LENGTH );
81
    }
82
83
    @Override
84
    public int compareTo( final Theme o ) {
85
      assert o != null;
86
87
      return name().compareTo( o.name() );
88
    }
89
  }
90
91
  private final File mThemes;
92
  private final ExportSettings mSettings;
93
  private GridPane mPane;
94
  private ComboBox<Theme> mComboBox;
95
  private TextField mChapters;
96
  private final boolean mMissingThemes;
97
98
  /**
99
   * Construction must use static method to allow caching themes in the
100
   * future, if needed.
101
   */
102
  private ExportDialog(
103
    final Window owner,
104
    final File themesDir,
105
    final ExportSettings settings,
106
    final boolean multiple
107
  ) {
108
    super( owner, get( "Dialog.typesetting.settings.title" ) );
109
110
    assert themesDir != null;
111
    assert settings != null;
112
113
    mThemes = themesDir;
114
    mSettings = settings;
115
116
    setResultConverter( button -> button == OK ? settings : null );
117
118
    final var themes = readThemes( themesDir );
119
120
    mMissingThemes = themes.isEmpty();
121
122
    // Typesetting installation has been corrupted. This is probably due
123
    // to the user's settings file gone missing. Rather than force users
124
    // to find the "themes" directory location, re-install the typesetter,
125
    if( mMissingThemes ) {
126
      clue( "Dialog.typesetting.settings.themes.missing",
127
            themesDir.getAbsolutePath() );
128
      ExportFailedEvent.fire();
129
      return;
130
    }
131
132
    final var previousTheme = mSettings.themeProperty().get();
133
134
    initComboBox( mComboBox, previousTheme, themes );
135
136
    mPane.add( createLabel( "Dialog.typesetting.settings.theme" ), 0, 1 );
137
    mPane.add( mComboBox, 1, 1 );
138
139
    var title = "Dialog.typesetting.settings.header.";
140
    final var focusNode = new AtomicReference<Node>( mComboBox );
141
142
    if( multiple ) {
143
      mPane.add( createLabel( "Dialog.typesetting.settings.chapters" ), 0, 2 );
144
      mPane.add( mChapters, 1, 2 );
145
146
      focusNode.set( mChapters );
147
      title += "multiple";
148
    }
149
    else {
150
      title += "single";
151
    }
152
153
    // Remember the chapter range regardless of text field visibility.
154
    mChapters.textProperty().bindBidirectional( mSettings.chaptersProperty() );
155
156
    setHeaderText( get( title ) );
157
158
    final var dialogPane = getDialogPane();
159
    dialogPane.setContent( mPane );
160
161
    runLater( () -> focusNode.get().requestFocus() );
162
  }
163
164
  /**
165
   * Prompts a user to select a theme, answering {@code false} if no theme
166
   * was selected. The themes must be on the native file system; using the
167
   * {@link FileWalker} is a little more optimal than {@link ResourceWalker}.
168
   *
169
   * @param owner    The parent {@link Window} responsible for the dialog.
170
   * @param themes   Theme directory root.
171
   * @param settings Configuration preferences to use when exporting.
172
   * @param multiple Pass {@code true} to input a chapter number subset.
173
   * @return {@code true} if the user accepted or selected a theme.
174
   */
175
  public static boolean choose(
176
    final Window owner,
177
    final File themes,
178
    final ExportSettings settings,
179
    final boolean multiple
180
  ) {
181
    assert themes != null;
182
    assert settings != null;
183
184
    return new ExportDialog( owner, themes, settings, multiple ).pick();
185
  }
186
187
  /**
188
   * @return {@code true} if the user accepted or selected a theme.
189
   * @see #choose(Window, File, ExportSettings, boolean)
190
   */
191
  private boolean pick() {
192
    try {
193
      if( !mMissingThemes ) {
194
        final var result = showAndWait();
195
196
        // The result will only be set if the OK button is pressed.
197
        if( result.isPresent() ) {
198
          final var theme = mComboBox.getSelectionModel().getSelectedItem();
199
          final var path = theme.path();
200
          final var filename = SysFile.getFileName( path.getFileName() );
201
202
          mSettings.themeProperty().setValue( filename );
203
204
          return true;
205
        }
206
      }
207
    } catch( final Exception ex ) {
208
      clue( get( "Main.status.error.theme.missing", mThemes ), ex );
209
    }
210
211
    return false;
212
  }
213
214
  @Override
215
  protected void initComponents() {
216
    initIcon();
217
    setResizable( true );
218
219
    mPane = createContentPane();
220
    mComboBox = createComboBox();
221
    mComboBox.setOnKeyPressed( event -> {
222
      // When the user presses the down arrow, open the drop-down. This
223
      // prevents navigating to the cancel button.
224
      if( event.getCode() == KeyCode.DOWN && !mComboBox.isShowing() ) {
225
        mComboBox.show();
226
        event.consume();
227
      }
228
    } );
229
230
    mChapters = createNumericTextField();
231
  }
232
233
  private void initIcon() {
234
    setGraphic( ICON_DIALOG_NODE );
235
    setStageGraphic( ICON_DIALOG );
236
  }
237
238
  @SuppressWarnings( "SameParameterValue" )
239
  private void setStageGraphic( final Image icon ) {
240
    if( getDialogPane().getScene().getWindow() instanceof final Stage stage ) {
241
      stage.getIcons().add( icon );
242
    }
243
  }
244
245
  private void initComboBox(
246
    final ComboBox<Theme> comboBox,
247
    final String previousTheme,
248
    final List<Theme> choices
249
  ) {
250
    assert comboBox != null;
251
    assert previousTheme != null;
252
    assert choices != null;
253
254
    final var items = comboBox.getItems();
255
    items.clear();
256
    items.addAll( choices );
257
258
    // Set the selected item to user's settings value.
259
    for( final var choice : choices ) {
260
      if( choice.matches( previousTheme ) ) {
261
        comboBox.getSelectionModel().select(
262
          items.get( max( items.indexOf( choice ), 0 ) )
263
        );
264
265
        break;
266
      }
267
    }
268
  }
269
270
  private List<Theme> readThemes( final File themesDir ) {
271
    try {
272
      // List themes in alphabetical order (human-readable by directory name).
273
      final var choices = new LinkedList<Theme>();
274
275
      // Populate the choices with themes detected on the system.
276
      walk( themesDir.toPath(), "**/theme.properties", path -> {
277
        try {
278
          final var themeName = readThemeName( path );
279
          final var themePath = path.getParent();
280
          choices.add( new Theme( themePath, themeName ) );
281
        } catch( final Exception ex ) {
282
          clue( "Main.status.error.theme.name", path );
283
        }
284
      } );
285
286
      Collections.sort( choices );
287
288
      return choices;
289
    } catch( final Exception ex ) {
290
      clue( ex );
291
    }
292
293
    return Collections.emptyList();
294
  }
295
296
  private ComboBox<Theme> createComboBox() {
297
    return new ComboBox<>();
298
  }
299
300
  private GridPane createContentPane() {
301
    final var grid = new GridPane();
302
303
    grid.setAlignment( CENTER );
304
    grid.setHgap( UI_CONTROL_SPACING );
305
    grid.setVgap( UI_CONTROL_SPACING );
306
    grid.setPadding( new Insets( 25, 25, 25, 25 ) );
307
308
    return grid;
309
  }
310
311
  /**
312
   * Creates an input field that only accepts whole numbers. This allows users
313
   * to enter in chapter ranges such as: <code>1-5, 7, 9-10</code>.
314
   *
315
   * @return A {@link TextField} that censors non-conforming characters.
316
   */
317
  private TextField createNumericTextField() {
318
    final var textField = new TextField();
319
320
    textField.textProperty().addListener(
321
      ( _, _, n ) -> textField.setText( RangeValidator.normalize( n ) )
322
    );
323
324
    return textField;
325
  }
326
327
  private Label createLabel( final String key ) {
328
    final var label = new Label( String.format( "%s%s", get( key ), ":" ) );
329
    final var font = label.getFont();
330
    final var upscale = new Font( font.getName(), 14 );
331
332
    label.setFont( upscale );
333
334
    return label;
335
  }
336
337
  /**
338
   * Returns the theme's human-friendly name from a file conforming to
339
   * {@link Properties}.
340
   *
341
   * @param file A fully qualified file name readable using {@link Properties}.
342
   * @return The human-friendly theme name.
343
   * @throws IOException          The {@link Properties} file cannot be read.
344
   * @throws NullPointerException The name field is not defined.
345
   */
346
  private String readThemeName( final Path file ) throws Exception {
347
    return read( file ).get( "name" ).toString();
348
  }
349
350
  /**
351
   * Reads an instance of {@link Properties} from the given {@link Path} using
352
   * {@link StandardCharsets#UTF_8} encoding.
353
   *
354
   * @param path The fully qualified path to the file.
355
   * @return The path to the file to read.
356
   * @throws IOException Could not open the file for reading.
357
   */
358
  private Properties read( final Path path ) throws IOException {
359
    final var properties = new Properties();
360
361
    try(
362
      final var f = new FileInputStream( toFile( path ) );
363
      final var in = new InputStreamReader( f, UTF_8 )
364
    ) {
365
      properties.load( in );
366
    }
367
368
    return properties;
369
  }
370
}
1371
A src/main/java/com/keenwrite/ui/dialogs/ExportSettings.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.dialogs;
6
7
import com.keenwrite.util.GenericBuilder;
8
import javafx.beans.property.StringProperty;
9
10
/**
11
 * Provides export settings such as the selected theme and chapter numbers
12
 * to include.
13
 */
14
public class ExportSettings {
15
  private final Mutator mMutator;
16
17
  public static class Mutator {
18
    private StringProperty mThemeProperty;
19
    private StringProperty mChaptersProperty;
20
21
    public void setTheme( final StringProperty theme ) {
22
      assert theme != null;
23
      mThemeProperty = theme;
24
    }
25
26
    public void setChapters( final StringProperty chapters ) {
27
      assert chapters != null;
28
      mChaptersProperty = chapters;
29
    }
30
  }
31
32
  /**
33
   * Force using the builder pattern.
34
   */
35
  private ExportSettings( final Mutator mutator ) {
36
    assert mutator != null;
37
38
    mMutator = mutator;
39
  }
40
41
  public static GenericBuilder<Mutator, ExportSettings> builder() {
42
    return GenericBuilder.of(
43
      ExportSettings.Mutator::new, ExportSettings::new
44
    );
45
  }
46
47
  public StringProperty themeProperty() {
48
    return mMutator.mThemeProperty;
49
  }
50
51
  public StringProperty chaptersProperty() {
52
    return mMutator.mChaptersProperty;
53
  }
54
}
55
156
A src/main/java/com/keenwrite/ui/dialogs/HyperlinkDialog.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.dialogs;
6
7
import com.keenwrite.ui.models.HyperlinkModel;
8
import javafx.stage.Window;
9
10
/**
11
 * Dialog to insert or edit a Markdown link.
12
 */
13
public final class HyperlinkDialog extends CustomDialog<String> {
14
  private static final String PREFIX = "Dialog.link.";
15
16
  /**
17
   * Contains information about the hyperlink at the caret position in the
18
   * document, if a hyperlink is present at that location. This allows users
19
   * to edit existing hyperlinks using this {@link HyperlinkDialog}.
20
   */
21
  private final HyperlinkModel mModel;
22
23
  /**
24
   * @param owner {@link Window} responsible for the dialog resource.
25
   * @param model Existing hyperlink data, or blank for a new link.
26
   */
27
  public HyperlinkDialog( final Window owner, final HyperlinkModel model ) {
28
    super( owner, String.format( "%s%s", PREFIX, "title" ) );
29
30
    mModel = model;
31
32
    super.initialize();
33
  }
34
35
  @Override
36
  protected void initInputFields() {
37
    addInputField(
38
      "text",
39
      String.format( "%s%s", PREFIX, "label.text" ), String.format( "%s%s", PREFIX, "prompt.text" ),
40
      mModel.getText(),
41
      ( _, _, n ) -> mModel.setText( n )
42
    );
43
    addInputField(
44
      "url",
45
      String.format( "%s%s", PREFIX, "label.url" ), String.format( "%s%s", PREFIX, "prompt.url" ),
46
      mModel.getUrl(),
47
      ( _, _, n ) -> mModel.setUrl( n )
48
    );
49
    addInputField(
50
      "title",
51
      String.format( "%s%s", PREFIX, "label.title" ), String.format( "%s%s", PREFIX, "prompt.title" ),
52
      mModel.getTitle(),
53
      ( _, _, n ) -> mModel.setTitle( n )
54
    );
55
  }
56
57
  @Override
58
  protected String handleAccept() {
59
    return mModel.toString();
60
  }
61
}
162
A src/main/java/com/keenwrite/ui/dialogs/ImageDialog.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.dialogs;
6
7
import com.keenwrite.ui.models.ImageModel;
8
import javafx.stage.Window;
9
10
/**
11
 * Dialog to enter a Markdown image.
12
 */
13
public class ImageDialog extends CustomDialog<String> {
14
  private static final String PREFIX = "Dialog.image.";
15
16
  private final ImageModel mModel;
17
18
  public ImageDialog( final Window owner, final ImageModel model ) {
19
    super( owner, String.format( "%s%s", PREFIX, "title" ) );
20
21
    mModel = model;
22
23
    super.initialize();
24
  }
25
26
  @Override
27
  protected void initInputFields() {
28
    addInputField(
29
      "url",
30
      String.format( "%s%s", PREFIX, "label.url" ),
31
      String.format( "%s%s", PREFIX, "prompt.url" ),
32
      mModel.getUrl(),
33
      ( _, _, n ) -> mModel.setUrl( n )
34
    );
35
    addInputField(
36
      "text",
37
      String.format( "%s%s", PREFIX, "label.text" ),
38
      String.format( "%s%s", PREFIX, "prompt.text" ),
39
      mModel.getText(),
40
      ( _, _, n ) -> mModel.setText( n )
41
    );
42
    addInputField(
43
      "title",
44
      String.format( "%s%s", PREFIX, "label.title" ),
45
      String.format( "%s%s", PREFIX, "prompt.title" ),
46
      mModel.getTitle(),
47
      ( _, _, n ) -> mModel.setTitle( n )
48
    );
49
  }
50
51
  @Override
52
  protected String handleAccept() {
53
    return mModel.toString();
54
  }
55
}
156
A src/main/java/com/keenwrite/ui/dialogs/OpenUrlDialog.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.dialogs;
6
7
import com.keenwrite.events.FileOpenEvent;
8
import javafx.stage.Window;
9
10
import java.io.File;
11
import java.net.URI;
12
import java.nio.file.Path;
13
14
import static com.keenwrite.events.StatusEvent.clue;
15
import static com.keenwrite.io.downloads.DownloadManager.*;
16
import static com.keenwrite.util.Strings.sanitize;
17
18
/**
19
 * Dialog to open a remote Markdown file.
20
 */
21
public final class OpenUrlDialog extends CustomDialog<File> {
22
  private static final String PREFIX = "Dialog.open_url.";
23
  private static final String DOWNLOAD = "Main.status.url.request.";
24
  private static final String STATUS = String.format(
25
    "%s%s", DOWNLOAD, "status."
26
  );
27
28
  private final Path mParent;
29
  private String mUrl = "";
30
31
  /**
32
   * Ensures that all dialogs can be closed.
33
   *
34
   * @param owner  The parent window of this dialog.
35
   * @param parent Directory to store downloaded file.
36
   */
37
  public OpenUrlDialog( final Window owner, final Path parent ) {
38
    super( owner, String.format( "%s%s", PREFIX, "title" ) );
39
40
    mParent = parent;
41
42
    super.initialize();
43
  }
44
45
  @Override
46
  protected void initInputFields() {
47
    addInputField(
48
      "url",
49
      String.format( "%s%s", PREFIX, "label.url" ),
50
      String.format( "%s%s", PREFIX, "prompt.url" ),
51
      mUrl,
52
      ( _, _, n ) -> mUrl = sanitize( n )
53
    );
54
  }
55
56
  @Override
57
  protected File handleAccept() {
58
    return mUrl.isBlank() ? null : download( mUrl );
59
  }
60
61
  private File download( final String reference ) {
62
    try {
63
      clue( String.format( "%s%s", DOWNLOAD, "fetch" ), reference );
64
65
      final var uri = new URI( reference );
66
      final var path = toFile( uri );
67
      final var basedir = path.getName();
68
      final var file = mParent.resolve( basedir ).toFile();
69
70
      if( file.exists() ) {
71
        clue( String.format( "%s%s", DOWNLOAD, "exists" ), file );
72
      }
73
      else {
74
        final var task = downloadAsync( uri, file, ( progress, bytes ) -> {
75
          final var suffix = progress < 0 ? "bytes" : "progress";
76
77
          clue( String.format( "%s%s", STATUS, suffix ), progress, bytes );
78
        } );
79
80
        task.setOnSucceeded( _ -> {
81
          clue( String.format( "%s%s", DOWNLOAD, "success" ), file );
82
83
          // Only after the download succeeds can we open the file.
84
          FileOpenEvent.fire( file.toURI() );
85
        } );
86
        task.setOnFailed( _ -> clue(
87
          String.format( "%s%s", DOWNLOAD, "failure" ), uri )
88
        );
89
      }
90
91
      // The return value isn't used because the download happens
92
      // asynchronously. If the download succeeds, an event is fired.
93
      return null;
94
    } catch( final Exception e ) {
95
      throw new RuntimeException( e );
96
    }
97
  }
98
}
199
A src/main/java/com/keenwrite/ui/explorer/FilePicker.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.explorer;
3
4
import java.io.File;
5
import java.nio.file.Path;
6
import java.util.List;
7
import java.util.Optional;
8
9
/**
10
 * Responsible for providing the user with a way to select a file.
11
 */
12
public interface FilePicker {
13
14
  /**
15
   * Establishes the default file name to use when the UI is displayed. The
16
   * path portion of the file, if any, is ignored.
17
   *
18
   * @param file The initial {@link File} to choose when prompting the user
19
   *             to select a file.
20
   */
21
  void setInitialFilename( File file );
22
23
  /**
24
   * Establishes the directory to browse when the UI is displayed.
25
   *
26
   * @param path The initial {@link Path} to use when navigating the system.
27
   */
28
  default void setInitialDirectory( Path path ) {}
29
30
  /**
31
   * Sets the list of file names to display. For example, a single call to
32
   * this method with values of ("**.pdf", "Portable Document Format (PDF)")
33
   * would display only a file listing of PDF files.
34
   *
35
   * @param glob Pattern that allows matching file names to be listed.
36
   * @param text Human-readable description of the pattern.
37
   */
38
  default void addIncludeFileFilter( String glob, String text ) {}
39
40
  /**
41
   * Sets the list of file names to suppress. For example, a single call to
42
   * this method with values of (".*") would prevent listing files that begin
43
   * with a period.
44
   *
45
   * @param glob Pattern that allows matching file names to be suppressed.
46
   */
47
  default void addExcludeFileFilter( String glob ) {}
48
49
  /**
50
   * Returns the list of {@link File} objects selected by the user.
51
   *
52
   * @return A list of {@link File} objects, empty when nothing was selected.
53
   */
54
  Optional<List<File>> choose();
55
}
156
A src/main/java/com/keenwrite/ui/explorer/FilePickerFactory.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.explorer;
3
4
import com.keenwrite.Messages;
5
import com.keenwrite.io.SysFile;
6
import com.keenwrite.preferences.Workspace;
7
import javafx.beans.property.ObjectProperty;
8
import javafx.scene.Node;
9
import javafx.stage.FileChooser;
10
import javafx.stage.Window;
11
12
import java.io.File;
13
import java.nio.file.Path;
14
import java.util.List;
15
import java.util.Locale;
16
import java.util.Optional;
17
18
import static com.keenwrite.io.SysFile.toFile;
19
import static com.keenwrite.io.UserDataDir.getUserHome;
20
import static com.keenwrite.preferences.AppKeys.KEY_UI_RECENT_DIR;
21
import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType.*;
22
import static java.lang.String.format;
23
24
/**
25
 * Shim for a {@link FilePicker} instance that is implemented in pure Java.
26
 * This particular picker is added to avoid using the bug-ridden JavaFX
27
 * {@link FileChooser} that invokes the native file chooser.
28
 */
29
public class FilePickerFactory {
30
  public enum SelectionType {
31
    DIRECTORY_OPEN( "open" ),
32
    FILE_IMPORT( "import" ),
33
    FILE_EXPORT( "export" ),
34
    FILE_OPEN_SINGLE( "open" ),
35
    FILE_OPEN_MULTIPLE( "open" ),
36
    FILE_OPEN_NEW( "open" ),
37
    FILE_SAVE_AS( "save" );
38
39
    private final String mTitle;
40
41
    SelectionType( final String title ) {
42
      assert title != null;
43
      mTitle = Messages.get( format( "Dialog.file.choose.%s.title", title ) );
44
    }
45
46
    public String getTitle() {
47
      return mTitle;
48
    }
49
  }
50
51
  private final ObjectProperty<File> mDirectory;
52
  private final Locale mLocale;
53
54
  public FilePickerFactory( final Workspace workspace ) {
55
    mDirectory = workspace.fileProperty( KEY_UI_RECENT_DIR );
56
    mLocale = workspace.getLocale();
57
  }
58
59
  public FilePicker createModal(
60
    final Window owner, final SelectionType options ) {
61
    final var picker = new NativeFilePicker( owner, options );
62
    final var directory = SysFile.normalize( mDirectory.get() );
63
64
    picker.setInitialDirectory( directory );
65
66
    return picker;
67
  }
68
69
  public Node createModeless() {
70
    return new FilesView( mDirectory, mLocale );
71
  }
72
73
  /**
74
   * Operating system's file selection dialog.
75
   */
76
  private static final class NativeFilePicker implements FilePicker {
77
    private final FileChooser mChooser = new FileChooser();
78
    private final Window mOwner;
79
    private final SelectionType mType;
80
81
    public NativeFilePicker( final Window owner, final SelectionType type ) {
82
      assert owner != null;
83
      assert type != null;
84
85
      mOwner = owner;
86
      mType = type;
87
    }
88
89
    @Override
90
    public void setInitialFilename( final File file ) {
91
      assert file != null;
92
93
      mChooser.setInitialFileName( file.getName() );
94
    }
95
96
    @Override
97
    public void setInitialDirectory( final Path path ) {
98
      final var directory = toFile( path );
99
100
      mChooser.setInitialDirectory(
101
        directory.exists() ? directory : new File( getUserHome() )
102
      );
103
    }
104
105
    @Override
106
    public Optional<List<File>> choose() {
107
      if( mType == FILE_OPEN_MULTIPLE ) {
108
        return Optional.ofNullable( mChooser.showOpenMultipleDialog( mOwner ) );
109
      }
110
111
      final File file = mType == FILE_EXPORT || mType == FILE_SAVE_AS
112
        ? mChooser.showSaveDialog( mOwner )
113
        : mChooser.showOpenDialog( mOwner );
114
115
      return file == null ? Optional.empty() : Optional.of( List.of( file ) );
116
    }
117
  }
118
}
1119
A src/main/java/com/keenwrite/ui/explorer/FilesView.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.explorer;
3
4
import com.keenwrite.events.FileOpenEvent;
5
import com.keenwrite.io.SysFile;
6
import com.keenwrite.ui.controls.BrowseButton;
7
import javafx.beans.property.*;
8
import javafx.collections.ObservableList;
9
import javafx.collections.transformation.SortedList;
10
import javafx.scene.control.*;
11
import javafx.scene.layout.BorderPane;
12
import javafx.scene.layout.HBox;
13
import javafx.stage.FileChooser;
14
import javafx.util.Callback;
15
16
import java.io.File;
17
import java.io.IOException;
18
import java.nio.file.Path;
19
import java.nio.file.Paths;
20
import java.time.Instant;
21
import java.time.format.DateTimeFormatter;
22
import java.util.List;
23
import java.util.Locale;
24
import java.util.Optional;
25
26
import static com.keenwrite.constants.Constants.UI_CONTROL_SPACING;
27
import static com.keenwrite.events.StatusEvent.clue;
28
import static com.keenwrite.io.SysFile.toFile;
29
import static com.keenwrite.ui.fonts.IconFactory.createFileIcon;
30
import static java.nio.file.Files.size;
31
import static java.time.Instant.ofEpochMilli;
32
import static java.time.ZoneId.systemDefault;
33
import static java.time.format.DateTimeFormatter.ofPattern;
34
import static java.util.Comparator.comparing;
35
import static javafx.collections.FXCollections.observableArrayList;
36
import static javafx.scene.control.TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN;
37
import static javafx.scene.input.KeyCode.ENTER;
38
import static javafx.scene.layout.Priority.ALWAYS;
39
import static org.apache.commons.io.FilenameUtils.getExtension;
40
41
/**
42
 * Responsible for browsing files.
43
 */
44
public class FilesView extends BorderPane implements FilePicker {
45
  /**
46
   * When this directory changes, the input field will update accordingly.
47
   */
48
  private final ObjectProperty<File> mDirectory;
49
50
  /**
51
   * Data model for the file list shown in tabular format.
52
   */
53
  private final ObservableList<PathEntry> mItems = observableArrayList();
54
55
  /**
56
   * Used to format a file's date string from a {@code long} value.
57
   */
58
  private final DateTimeFormatter mDateFormatter;
59
60
  /**
61
   * Used to format a file's time string from a {@code long} value.
62
   */
63
  private final DateTimeFormatter mTimeFormatter;
64
65
  /**
66
   * Constructs a new view of a directory, listing all the files contained
67
   * therein. This will update the recent directory so that it will be
68
   * restored upon restart.
69
   *
70
   * @param recent Contains the initial (recent) directory.
71
   * @param locale Contains the language settings.
72
   */
73
  public FilesView(
74
    final ObjectProperty<File> recent, final Locale locale ) {
75
    mDirectory = recent;
76
    mDateFormatter = createFormatter( "yyyy-MMM-dd", locale );
77
    mTimeFormatter = createFormatter( "HH:mm:ss", locale );
78
79
    final var browse = createDirectoryChooser();
80
    final var table = createFileTable();
81
82
    final var sortedItems = new SortedList<>( mItems );
83
    sortedItems.comparatorProperty().bind( table.comparatorProperty() );
84
    table.setItems( sortedItems );
85
86
    setTop( browse );
87
    setCenter( table );
88
89
    mDirectory.addListener( ( c, o, n ) -> updateListing( n ) );
90
    updateListing( mDirectory.get() );
91
  }
92
93
  @Override
94
  public void setInitialFilename( final File file ) { }
95
96
  @Override
97
  public Optional<List<File>> choose() {
98
    return Optional.empty();
99
  }
100
101
  private void updateListing( final File directory ) {
102
    if( directory != null ) {
103
      mItems.clear();
104
105
      try {
106
        if( directory.getParent() != null ) {
107
          // Allow traversal to parent-directory.
108
          mItems.add( pathEntry( Paths.get( ".." ) ) );
109
        }
110
111
        final var list = directory.list();
112
113
        if( list != null ) {
114
          for( final var f : list ) {
115
            if( !f.startsWith( "." ) ) {
116
              mItems.add( pathEntry( Paths.get( directory.toString(), f ) ) );
117
            }
118
          }
119
        }
120
      } catch( final Exception ex ) {
121
        clue( ex );
122
      }
123
    }
124
  }
125
126
  /**
127
   * Allows the user to use an instance of {@link FileChooser} to change the
128
   * directory.
129
   *
130
   * @return The browse button and input field.
131
   */
132
  private HBox createDirectoryChooser() {
133
    final var dirProperty = directoryProperty();
134
    final var directory = dirProperty.get();
135
    final var hbox = new HBox();
136
    final var field = new TextField();
137
138
    mDirectory.addListener( ( c, o, n ) -> {
139
      if( n != null ) { field.setText( n.getAbsolutePath() ); }
140
    } );
141
142
    field.setOnKeyPressed( event -> {
143
      if( event.getCode() == ENTER ) {
144
        mDirectory.set( new File( field.getText() ) );
145
      }
146
    } );
147
148
    final var button = new BrowseButton( directory, mDirectory::set );
149
150
    hbox.getChildren().add( button );
151
    hbox.getChildren().add( field );
152
    hbox.setSpacing( UI_CONTROL_SPACING );
153
    HBox.setHgrow( field, ALWAYS );
154
155
    return hbox;
156
  }
157
158
  @SuppressWarnings( "unchecked" )
159
  private TableView<FilesView.PathEntry> createFileTable() {
160
    final var style = "-fx-alignment: BASELINE_LEFT;";
161
    final var table = new TableView<FilesView.PathEntry>();
162
    table.setColumnResizePolicy( CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN );
163
164
    table.setRowFactory( tv -> {
165
      final var row = new TableRow<PathEntry>();
166
167
      row.setOnMouseClicked( event -> {
168
        if( event.getClickCount() == 2 && !row.isEmpty() ) {
169
          final var entry = row.getItem();
170
          final var dir = mDirectory.get();
171
          final var filename = entry.nameProperty().get();
172
          final var path = Path.of( dir.toString(), filename );
173
          final var file = toFile( path );
174
175
          if( file.isFile() ) {
176
            FileOpenEvent.fire( path.toUri() );
177
          }
178
          else if( file.isDirectory() ) {
179
            mDirectory.set( toFile( path.normalize() ) );
180
          }
181
        }
182
      } );
183
184
      return row;
185
    } );
186
187
    final TableColumn<PathEntry, Path> colType = createColumn( "Type" );
188
    final TableColumn<PathEntry, String> colName = createColumn( "Name" );
189
    final TableColumn<PathEntry, Number> colSize = createColumn( "Size" );
190
    final TableColumn<PathEntry, String> colDate = createColumn( "Date" );
191
    final TableColumn<PathEntry, String> colTime = createColumn( "Modified" );
192
193
    colType.setCellFactory( new FileCell<>() );
194
195
    colType.setCellValueFactory( stat -> stat.getValue().typeProperty() );
196
    colName.setCellValueFactory( stat -> stat.getValue().nameProperty() );
197
    colSize.setCellValueFactory( stat -> stat.getValue().sizeProperty() );
198
    colDate.setCellValueFactory( stat -> stat.getValue().dateProperty() );
199
    colTime.setCellValueFactory( stat -> stat.getValue().timeProperty() );
200
201
    colType.setStyle( style );
202
    colName.setStyle( style );
203
    colSize.setStyle( style );
204
    colDate.setStyle( style );
205
    colTime.setStyle( style );
206
207
    final var columns = table.getColumns();
208
    columns.add( colType );
209
    columns.add( colName );
210
    columns.add( colSize );
211
    columns.add( colDate );
212
    columns.add( colTime );
213
214
    table.getSortOrder().setAll( colName, colDate, colTime );
215
216
    colType.setComparator(
217
      comparing( p -> getExtension( p.getFileName().toString() ) )
218
    );
219
220
    return table;
221
  }
222
223
  public ObjectProperty<File> directoryProperty() {
224
    return mDirectory;
225
  }
226
227
  private static DateTimeFormatter createFormatter(
228
    final String format, final Locale locale ) {
229
    return ofPattern( format, locale ).withZone( systemDefault() );
230
  }
231
232
  public PathEntry pathEntry( final Path path ) throws IOException {
233
    return new PathEntry( path );
234
  }
235
236
  /**
237
   * Responsible for rendering file system objects as image icons.
238
   *
239
   * @param <T> The data model type associated with a fully qualified path.
240
   * @param <P> Simplifies swapping {@link Path} for {@link File}.
241
   */
242
  private static class FileCell<T, P extends Path> extends TableCell<T, P>
243
    implements Callback<TableColumn<T, P>, TableCell<T, P>> {
244
    @Override
245
    public TableCell<T, P> call( final TableColumn<T, P> param ) {
246
      return new TableCell<>() {
247
        @Override
248
        protected void updateItem( final P path, final boolean empty ) {
249
          super.updateItem( path, empty );
250
          setText( null );
251
252
          try {
253
            setGraphic( empty || path == null ? null : createFileIcon( path ) );
254
          } catch( final Exception ex ) {
255
            clue( ex );
256
          }
257
        }
258
      };
259
    }
260
  }
261
262
  protected final class PathEntry {
263
    private final ObjectProperty<Path> mType;
264
    private final StringProperty mName;
265
    private final LongProperty mSize;
266
    private final StringProperty mDate;
267
    private final StringProperty mTime;
268
269
    private PathEntry( final Path path ) throws IOException {
270
      this(
271
        path,
272
        SysFile.getFileName( path ),
273
        size( path ),
274
        ofEpochMilli( toFile( path ).lastModified() )
275
      );
276
    }
277
278
    public PathEntry(
279
      final Path type,
280
      final String name,
281
      final long size,
282
      final Instant modified ) {
283
      this(
284
        new SimpleObjectProperty<>( type ),
285
        new SimpleStringProperty( name ),
286
        new SimpleLongProperty( size ),
287
        new SimpleStringProperty( mDateFormatter.format( modified ) ),
288
        new SimpleStringProperty( mTimeFormatter.format( modified ) )
289
      );
290
    }
291
292
    private PathEntry(
293
      final ObjectProperty<Path> type,
294
      final StringProperty name,
295
      final LongProperty size,
296
      final StringProperty date,
297
      final StringProperty time ) {
298
      mType = type;
299
      mName = name;
300
      mSize = size;
301
      mDate = date;
302
      mTime = time;
303
    }
304
305
    private ObjectProperty<Path> typeProperty() {
306
      return mType;
307
    }
308
309
    private StringProperty nameProperty() {
310
      return mName;
311
    }
312
313
    private LongProperty sizeProperty() {
314
      return mSize;
315
    }
316
317
    private StringProperty dateProperty() {
318
      return mDate;
319
    }
320
321
    private StringProperty timeProperty() {
322
      return mTime;
323
    }
324
  }
325
326
  private <E, T> TableColumn<E, T> createColumn( final String key ) {
327
    return new TableColumn<>( key );
328
  }
329
}
1330
A src/main/java/com/keenwrite/ui/fonts/IconFactory.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.fonts;
3
4
import com.keenwrite.io.MediaType;
5
import com.keenwrite.io.MediaTypeExtension;
6
import com.keenwrite.io.SysFile;
7
import javafx.scene.Node;
8
import javafx.scene.image.Image;
9
import javafx.scene.image.ImageView;
10
import org.controlsfx.glyphfont.FontAwesome;
11
import org.controlsfx.glyphfont.Glyph;
12
13
import java.awt.*;
14
import java.awt.image.BufferedImage;
15
import java.io.IOException;
16
import java.io.InputStream;
17
import java.nio.file.Path;
18
import java.nio.file.attribute.BasicFileAttributes;
19
import java.util.HashMap;
20
import java.util.Map;
21
22
import static com.keenwrite.events.StatusEvent.clue;
23
import static com.keenwrite.io.MediaTypeExtension.MEDIA_UNDEFINED;
24
import static com.keenwrite.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER;
25
import static com.keenwrite.preview.SvgRasterizer.rasterize;
26
import static java.awt.Font.*;
27
import static java.nio.file.Files.readAttributes;
28
import static javafx.embed.swing.SwingFXUtils.toFXImage;
29
import static org.apache.commons.io.FilenameUtils.getExtension;
30
import static org.controlsfx.glyphfont.FontAwesome.Glyph.valueOf;
31
32
/**
33
 * Responsible for creating FontAwesome glyphs and graphics.
34
 */
35
public class IconFactory {
36
  /**
37
   * File icon height, in pixels.
38
   */
39
  private static final int ICON_HEIGHT = 16;
40
41
  /**
42
   * Singleton to prevent re-loading the TTF file.
43
   */
44
  private static final FontAwesome FONT_AWESOME = new FontAwesome();
45
46
  /**
47
   * Caches file type icons encountered.
48
   */
49
  private static final Map<String, Image> ICONS = new HashMap<>();
50
51
  /**
52
   * Create a {@link Node} representation for the given icon name.
53
   *
54
   * @param icon Name of icon to convert to a UI object (case-insensitive).
55
   * @return A UI object suitable for display.
56
   */
57
  public static Node createGraphic( final String icon ) {
58
    assert icon != null;
59
60
    // Return a label glyph.
61
    return icon.isEmpty()
62
      ? new Glyph()
63
      : createGlyph( icon );
64
  }
65
66
  /**
67
   * Create a {@link Node} representation for the given FontAwesome glyph.
68
   *
69
   * @param glyph The glyph to convert to a {@link Node}.
70
   * @return The given glyph as a text label.
71
   */
72
  public static Node createGraphic( final FontAwesome.Glyph glyph ) {
73
    return FONT_AWESOME.create( glyph );
74
  }
75
76
  /**
77
   * Creates a suitable {@link Node} icon representation for the given file.
78
   * This will first look up the {@link MediaType} before matching based on
79
   * the file name extension.
80
   *
81
   * @param path The file to represent graphically.
82
   * @return An icon representation for the given file.
83
   */
84
  public static ImageView createFileIcon( final Path path ) throws IOException {
85
    final var attrs = readAttributes( path, BasicFileAttributes.class );
86
    final var filename = SysFile.getFileName( path );
87
    String extension;
88
89
    if( "..".equals( filename ) ) {
90
      extension = "folder-up";
91
    }
92
    else if( attrs.isDirectory() ) {
93
      extension = "folder";
94
    }
95
    else if( attrs.isSymbolicLink() ) {
96
      extension = "folder-link";
97
    }
98
    else {
99
      final var mediaType = MediaType.fromFilename( path );
100
      final var mte = MediaTypeExtension.valueFrom( mediaType );
101
102
      // if the file extension is not known to the app, try loading an icon
103
      // that corresponds to the extension directly.
104
      extension = mte == MEDIA_UNDEFINED
105
        ? getExtension( filename )
106
        : mte.getExtension();
107
    }
108
109
    if( extension == null ) {
110
      extension = "";
111
    }
112
    else {
113
      extension = extension.toLowerCase();
114
    }
115
116
    // Each cell in the table must have a distinct parent, so the image views
117
    // cannot be reused. The underlying buffered image can be cached, though.
118
    final var image =
119
      ICONS.computeIfAbsent( extension, IconFactory::createFxImage );
120
    final var imageView = new ImageView();
121
    imageView.setPreserveRatio( true );
122
    imageView.setFitHeight( ICON_HEIGHT );
123
    imageView.setImage( image );
124
125
    return imageView;
126
  }
127
128
  private static Image createFxImage( final String extension ) {
129
    return toFXImage( createImage( extension ), null );
130
  }
131
132
  private static BufferedImage createImage( final String extension ) {
133
    try( final var icon = open( "icons/" + extension + ".svg" ) ) {
134
      if( icon == null ) {
135
        throw new IllegalArgumentException( extension );
136
      }
137
138
      return rasterize( icon );
139
    } catch( final Exception ex ) {
140
      clue( ex );
141
142
      // If the extension was unknown, fall back to a blank icon, falling
143
      // back again to a broken image if blank cannot be found (to avoid
144
      // infinite recursion).
145
      return "blank".equals( extension )
146
        ? BROKEN_IMAGE_PLACEHOLDER
147
        : createImage( "blank" );
148
    }
149
  }
150
151
  private static InputStream open( final String resource ) {
152
    return IconFactory.class.getResourceAsStream( resource );
153
  }
154
155
  /**
156
   * Returns the font to use when adding icons to the UI.
157
   *
158
   * @param size The font size to use when drawing the icon.
159
   * @return A font containing numerous icons.
160
   */
161
  public static Font getIconFont( final int size ) {
162
    try( final var fontStream = openFont() ) {
163
      final var font = createFont( TRUETYPE_FONT, fontStream );
164
      return font.deriveFont( PLAIN, size );
165
    } catch( final Exception e ) {
166
      // This doesn't actually work, seemingly after an upgrade to ControlsFX.
167
      // As such, creating the font and deriving it will work.
168
      return new Font( FONT_AWESOME.getName(), PLAIN, size );
169
    }
170
  }
171
172
  /**
173
   * This re-reads the {@link FontAwesome} font TTF resource. For a reason
174
   * not yet investigated, the font doesn't appear to be accessible to the
175
   * application. This may have happened during an upgrade to ControlsFX.
176
   * Callers are responsible for closing the stream.
177
   *
178
   * @return A stream containing font TrueType glyph information.
179
   */
180
  private static InputStream openFont() {
181
    return FontAwesome.class.getResourceAsStream( "fontawesome-webfont.ttf" );
182
  }
183
184
  private static Node createGlyph( final String icon ) {
185
    return createGraphic( valueOf( icon.toUpperCase() ) );
186
  }
187
188
  /**
189
   * Prevent instantiation. Use the {@link #createGraphic(String)} method to
190
   * create an icon for display.
191
   */
192
  private IconFactory() { }
193
}
1194
A src/main/java/com/keenwrite/ui/heuristics/DocumentStatistics.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.heuristics;
3
4
import com.keenwrite.events.DocumentChangedEvent;
5
import com.keenwrite.events.WordCountEvent;
6
import com.keenwrite.preferences.Workspace;
7
import com.keenwrite.ui.actions.Keyboard;
8
import com.keenwrite.ui.clipboard.SystemClipboard;
9
import com.whitemagicsoftware.keencount.TokenizerException;
10
import javafx.beans.property.IntegerProperty;
11
import javafx.beans.property.SimpleIntegerProperty;
12
import javafx.beans.property.SimpleStringProperty;
13
import javafx.beans.property.StringProperty;
14
import javafx.collections.ObservableList;
15
import javafx.collections.transformation.SortedList;
16
import javafx.scene.control.TableColumn;
17
import javafx.scene.control.TableView;
18
import org.greenrobot.eventbus.Subscribe;
19
20
import static com.keenwrite.events.Bus.register;
21
import static com.keenwrite.events.StatusEvent.clue;
22
import static com.keenwrite.preferences.AppKeys.KEY_LANGUAGE_LOCALE;
23
import static com.keenwrite.preferences.AppKeys.KEY_UI_FONT_EDITOR_NAME;
24
import static com.keenwrite.ui.heuristics.DocumentStatistics.StatEntry;
25
import static java.lang.String.format;
26
import static javafx.application.Platform.runLater;
27
import static javafx.collections.FXCollections.observableArrayList;
28
import static javafx.scene.control.SelectionMode.MULTIPLE;
29
30
/**
31
 * Responsible for displaying document statistics, such as word count and
32
 * word frequency.
33
 */
34
public final class DocumentStatistics extends TableView<StatEntry> {
35
36
  private WordCounter mWordCounter;
37
  private final ObservableList<StatEntry> mItems = observableArrayList();
38
39
  /**
40
   * Creates a new observer of document change events that will gather and
41
   * display document statistics (e.g., word counts).
42
   *
43
   * @param workspace Settings used to configure the statistics engine.
44
   */
45
  public DocumentStatistics( final Workspace workspace ) {
46
    mWordCounter = WordCounter.create( workspace.getLocale() );
47
48
    final var sortedItems = new SortedList<>( mItems );
49
    sortedItems.comparatorProperty().bind( comparatorProperty() );
50
    setItems( sortedItems );
51
52
    initView();
53
    initListeners( workspace );
54
    register( this );
55
  }
56
57
  /**
58
   * Called when the hash code for the current document changes. This happens
59
   * when non-collapsable-whitespace is added to the document. When the
60
   * document is sent for rendering, the parsed document is converted to text.
61
   * If that text differs in its hash code, then this method is called. The
62
   * implication is that all variables and executable statements have been
63
   * replaced. An event bus subscriber is used so that text processing occurs
64
   * outside the UI processing threads.
65
   *
66
   * @param event Container for the document text that has changed.
67
   */
68
  @Subscribe
69
  public void handle( final DocumentChangedEvent event ) {
70
    try {
71
      runLater( () -> {
72
        mItems.clear();
73
        final var document = event.getDocument();
74
        final var wordCount = mWordCounter.count(
75
          document, ( k, count ) ->
76
            mItems.add( new StatEntry( k, count ) )
77
        );
78
79
        WordCountEvent.fire( wordCount );
80
      } );
81
    } catch( final TokenizerException ex ) {
82
      clue( ex );
83
    }
84
  }
85
86
  @SuppressWarnings( "unchecked" )
87
  private void initView() {
88
    final TableColumn<StatEntry, String> colWord = createColumn( "Word" );
89
    final TableColumn<StatEntry, Number> colCount = createColumn( "Count" );
90
91
    colWord.setCellValueFactory( stat -> stat.getValue().wordProperty() );
92
    colCount.setCellValueFactory( stat -> stat.getValue().tallyProperty() );
93
    colCount.setComparator( colCount.getComparator().reversed() );
94
95
    final var columns = getColumns();
96
    columns.add( colWord );
97
    columns.add( colCount );
98
99
    setMaxWidth( Double.MAX_VALUE );
100
    setPrefWidth( 128 );
101
    setColumnResizePolicy( CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN );
102
    getSortOrder().setAll( colCount, colWord );
103
104
    getStyleClass().add( "" );
105
  }
106
107
  private void initListeners( final Workspace workspace ) {
108
    initLocaleListener( workspace );
109
    initFontListener( workspace );
110
    initKeyboardListener();
111
  }
112
113
  private void initLocaleListener( final Workspace workspace ) {
114
    final var property = workspace.localeProperty( KEY_LANGUAGE_LOCALE );
115
    property.addListener(
116
      ( c, o, n ) -> mWordCounter = WordCounter.create( property.toLocale() )
117
    );
118
  }
119
120
  private void initFontListener( final Workspace workspace ) {
121
    final var fontName = workspace.stringProperty( KEY_UI_FONT_EDITOR_NAME );
122
123
    fontName.addListener(
124
      ( c, o, n ) -> {
125
        if( n != null ) {
126
          setFontFamily( n );
127
        }
128
      }
129
    );
130
131
    setFontFamily( fontName.getValue() );
132
  }
133
134
  private void initKeyboardListener() {
135
    getSelectionModel().setSelectionMode( MULTIPLE );
136
    setOnKeyPressed( event -> {
137
      if( Keyboard.isCopy( event ) ) {
138
        SystemClipboard.write( this );
139
      }
140
    } );
141
  }
142
143
  private <E, T> TableColumn<E, T> createColumn( final String key ) {
144
    return new TableColumn<>( key );
145
  }
146
147
  private void setFontFamily( final String value ) {
148
    runLater( () -> setStyle( format( "-fx-font-family:'%s';", value ) ) );
149
  }
150
151
  /**
152
   * Represents the number of times a word appears in a document.
153
   */
154
  protected static final class StatEntry {
155
    private final StringProperty mWord;
156
    private final IntegerProperty mTally;
157
158
    public StatEntry( final String word, final int tally ) {
159
      mWord = new SimpleStringProperty( word );
160
      mTally = new SimpleIntegerProperty( tally );
161
    }
162
163
    private StringProperty wordProperty() {
164
      return mWord;
165
    }
166
167
    private IntegerProperty tallyProperty() {
168
      return mTally;
169
    }
170
  }
171
}
1172
A src/main/java/com/keenwrite/ui/heuristics/WordCounter.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.heuristics;
3
4
import com.whitemagicsoftware.keencount.Tokenizer;
5
import com.whitemagicsoftware.keencount.TokenizerFactory;
6
7
import java.util.Locale;
8
import java.util.function.BiConsumer;
9
10
/**
11
 * Responsible for counting unique words as well as total words in a document.
12
 */
13
public class WordCounter {
14
  /**
15
   * Parses documents into word counts.
16
   */
17
  private final Tokenizer mTokenizer;
18
19
  /**
20
   * Constructs a new {@link WordCounter} instance using the given tokenizer.
21
   *
22
   * @param tokenizer The class responsible for parsing a document into unique
23
   *                  and total word counts.
24
   */
25
  private WordCounter( final Tokenizer tokenizer ) {
26
    mTokenizer = tokenizer;
27
  }
28
29
  /**
30
   * Counts the number of unique words in the document.
31
   *
32
   * @param document The document to tally.
33
   * @return The total number of words in the document.
34
   */
35
  public int count( final String document ) {
36
    return count( document, ( k, count ) -> {} );
37
  }
38
39
  /**
40
   * Counts the number of unique words in the document.
41
   *
42
   * @param document The document to tally.
43
   * @param consumer The action to take for each unique word/count pair.
44
   * @return The total number of words in the document.
45
   */
46
  public int count(
47
    final String document, final BiConsumer<String, Integer> consumer ) {
48
    final var tokens = mTokenizer.tokenize( document );
49
    final var sum = new int[]{0};
50
51
    tokens.forEach( ( k, v ) -> {
52
      final var count = v[ 0 ];
53
      consumer.accept( k, count );
54
      sum[ 0 ] += count;
55
    } );
56
57
    return sum[ 0 ];
58
  }
59
60
  /**
61
   * Constructs a new {@link WordCounter} capable of tokenizing a document
62
   * into words using the given {@link Locale}.
63
   *
64
   * @param locale The {@link Tokenizer}'s language settings.
65
   */
66
  public static WordCounter create( final Locale locale ) {
67
    return new WordCounter( createTokenizer( locale ) );
68
  }
69
70
  /**
71
   * Creates a tokenizer for English text (can handle most Latin languages).
72
   *
73
   * @return An English-based tokenizer for counting words.
74
   */
75
  private static Tokenizer createTokenizer( final Locale language ) {
76
    return TokenizerFactory.create( language );
77
  }
78
}
179
A src/main/java/com/keenwrite/ui/listeners/CaretStatus.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.listeners;
3
4
import com.keenwrite.editors.common.Caret;
5
import com.keenwrite.events.CaretMovedEvent;
6
import com.keenwrite.events.WordCountEvent;
7
import javafx.scene.control.Label;
8
import javafx.scene.layout.VBox;
9
import org.greenrobot.eventbus.Subscribe;
10
11
import static com.keenwrite.events.Bus.register;
12
import static javafx.application.Platform.runLater;
13
import static javafx.geometry.Pos.BASELINE_CENTER;
14
15
/**
16
 * Responsible for updating the UI whenever the caret changes position.
17
 * Only one instance of {@link CaretStatus} is allowed, which prevents
18
 * duplicate adds to the observable property.
19
 */
20
public class CaretStatus extends VBox {
21
22
  /**
23
   * Use an instance of {@link Label} for its built-in CSS style class.
24
   */
25
  private final Label mStatusText = new Label();
26
27
  /**
28
   * Contains caret position information within an editor.
29
   */
30
  private volatile Caret mCaret = Caret.builder().build();
31
32
  /**
33
   * Approximate number of words in the document.
34
   */
35
  private volatile int mCount;
36
37
  public CaretStatus() {
38
    setAlignment( BASELINE_CENTER );
39
    getChildren().add( mStatusText );
40
    register( this );
41
  }
42
43
  @Subscribe
44
  public void handle( final WordCountEvent event ) {
45
    mCount = event.getCount();
46
    updateStatus( mCaret, mCount );
47
  }
48
49
  @Subscribe
50
  public void handle( final CaretMovedEvent event ) {
51
    mCaret = event.getCaret();
52
    updateStatus( mCaret, mCount );
53
  }
54
55
  private void updateStatus( final Caret caret, final int count ) {
56
    assert caret != null;
57
    runLater( () -> mStatusText.setText( caret + " | " + count ) );
58
  }
59
}
160
A src/main/java/com/keenwrite/ui/logging/LogView.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.logging;
3
4
import com.keenwrite.events.StatusEvent;
5
import com.keenwrite.ui.actions.Keyboard;
6
import com.keenwrite.ui.clipboard.SystemClipboard;
7
import javafx.beans.property.SimpleStringProperty;
8
import javafx.beans.property.StringProperty;
9
import javafx.collections.ObservableList;
10
import javafx.scene.control.*;
11
import javafx.stage.Stage;
12
import org.greenrobot.eventbus.Subscribe;
13
14
import java.time.LocalDateTime;
15
import java.util.Objects;
16
17
import static com.keenwrite.Messages.get;
18
import static com.keenwrite.constants.Constants.ACTION_PREFIX;
19
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
20
import static com.keenwrite.events.Bus.register;
21
import static com.keenwrite.events.StatusEvent.clue;
22
import static java.time.LocalDateTime.now;
23
import static java.time.format.DateTimeFormatter.ofPattern;
24
import static javafx.application.Platform.runLater;
25
import static javafx.collections.FXCollections.observableArrayList;
26
import static javafx.event.ActionEvent.ACTION;
27
import static javafx.scene.control.Alert.AlertType.INFORMATION;
28
import static javafx.scene.control.ButtonType.OK;
29
import static javafx.scene.control.SelectionMode.MULTIPLE;
30
import static javafx.stage.Modality.NONE;
31
32
/**
33
 * Responsible for logging application issues to {@link TableView} entries.
34
 */
35
public final class LogView extends Alert {
36
  /**
37
   * Number of error messages to retain in the {@link TableView}; must be
38
   * greater than zero. Typesetting the document can cause many page number
39
   * messages to be logged.
40
   */
41
  private static final int CACHE_SIZE = 500;
42
43
  private final ObservableList<LogEntry> mItems = observableArrayList();
44
  private final TableView<LogEntry> mTable = new TableView<>( mItems );
45
46
  public LogView() {
47
    super( INFORMATION );
48
    setTitle( get( ACTION_PREFIX + "view.log.text" ) );
49
    initModality( NONE );
50
    initTableView();
51
    setResizable( true );
52
    initButtons();
53
    initIcon();
54
    initActions();
55
    register( this );
56
  }
57
58
  @Subscribe
59
  public void log( final StatusEvent event ) {
60
    runLater( () -> {
61
      final var logEntry = new LogEntry( event );
62
63
      if( !mItems.contains( logEntry ) ) {
64
        mItems.add( logEntry );
65
66
        while( mItems.size() > CACHE_SIZE ) {
67
          mItems.removeFirst();
68
        }
69
70
        mTable.scrollTo( logEntry );
71
      }
72
    } );
73
  }
74
75
  /**
76
   * Brings the dialog to the foreground, showing it if needed.
77
   */
78
  public void view() {
79
    super.show();
80
    getStage().toFront();
81
  }
82
83
  /**
84
   * Removes all the entries from the list.
85
   */
86
  public void clear() {
87
    mItems.clear();
88
    clue();
89
  }
90
91
  private void initTableView() {
92
    final var colDate = new TableColumn<LogEntry, String>( "Timestamp" );
93
    final var colMessage = new TableColumn<LogEntry, String>( "Message" );
94
    final var colTrace = new TableColumn<LogEntry, String>( "Trace" );
95
96
    colDate.setCellValueFactory( log -> log.getValue().dateProperty() );
97
    colMessage.setCellValueFactory( log -> log.getValue().messageProperty() );
98
    colTrace.setCellValueFactory( log -> log.getValue().traceProperty() );
99
100
    final var columns = mTable.getColumns();
101
    columns.add( colDate );
102
    columns.add( colMessage );
103
    columns.add( colTrace );
104
105
    // Display the entire date by default.
106
    colDate.setPrefWidth( 135 );
107
108
    // Display most of the message by default.
109
    colMessage.setPrefWidth( 425 );
110
111
    // Display a large portion of the stack trace.
112
    colTrace.setPrefWidth( 600 );
113
114
    mTable.setMaxWidth( Double.MAX_VALUE );
115
    mTable.setPrefWidth( 1200 );
116
    mTable.getSelectionModel().setSelectionMode( MULTIPLE );
117
    mTable.setOnKeyPressed( event -> {
118
      if( Keyboard.isCopy( event ) ) {
119
        SystemClipboard.write( mTable );
120
      }
121
    } );
122
123
    final var pane = getDialogPane();
124
    pane.setContent( mTable );
125
  }
126
127
  private void initButtons() {
128
    final var pane = getDialogPane();
129
    final var CLEAR = new ButtonType( "CLEAR" );
130
    pane.getButtonTypes().add( CLEAR );
131
132
    final var buttonOk = (Button) pane.lookupButton( OK );
133
    final var buttonClear = (Button) pane.lookupButton( CLEAR );
134
135
    buttonOk.setDefaultButton( true );
136
    buttonClear.addEventFilter( ACTION, event -> {
137
      clear();
138
      event.consume();
139
    } );
140
141
    pane.setOnKeyReleased( t -> {
142
      switch( t.getCode() ) {
143
        case ENTER, ESCAPE -> buttonOk.fire();
144
        default -> { }
145
      }
146
    } );
147
  }
148
149
  private void initIcon() {
150
    getStage().getIcons().add( ICON_DIALOG );
151
  }
152
153
  private void initActions() {
154
    final var stage = getStage();
155
    stage.setOnCloseRequest( _ -> stage.hide() );
156
  }
157
158
  private Stage getStage() {
159
    return (Stage) getDialogPane().getScene().getWindow();
160
  }
161
162
  private static final class LogEntry {
163
    private final StringProperty mDate;
164
    private final StringProperty mMessage;
165
    private final StringProperty mTrace;
166
167
    /**
168
     * Constructs a new {@link LogEntry} for the current time.
169
     */
170
    public LogEntry( final StatusEvent event ) {
171
      mDate = new SimpleStringProperty( toString( now() ) );
172
      mMessage = new SimpleStringProperty( event.getMessage() );
173
      mTrace = new SimpleStringProperty( event.getProblem() );
174
    }
175
176
    private StringProperty messageProperty() {
177
      return mMessage;
178
    }
179
180
    private StringProperty dateProperty() {
181
      return mDate;
182
    }
183
184
    private StringProperty traceProperty() {
185
      return mTrace;
186
    }
187
188
    @Override
189
    public boolean equals( final Object o ) {
190
      if( this == o ) { return true; }
191
      if( o == null || getClass() != o.getClass() ) { return false; }
192
193
      return Objects.equals( mMessage.get(), ((LogEntry) o).mMessage.get() );
194
    }
195
196
    @Override
197
    public int hashCode() {
198
      return mMessage != null ? mMessage.hashCode() : 0;
199
    }
200
201
    @Override
202
    public String toString() {
203
      final var date = mDate == null ? "" : mDate.get();
204
      final var message = mMessage == null ? "" : mMessage.get();
205
      final var trace = mTrace == null ? "" : mTrace.get();
206
207
      return getClass().getSimpleName() + "{" +
208
        "mDate=" + (date == null ? "''" : date) +
209
        ", mMessage=" + (message == null ? "''" : message) +
210
        ", mTrace=" + (trace == null ? "''" : trace) +
211
        '}';
212
    }
213
214
    private String toString( final LocalDateTime date ) {
215
      return date.format( ofPattern( "d MMM u HH:mm:ss" ) );
216
    }
217
  }
218
}
1219
A src/main/java/com/keenwrite/ui/models/HyperlinkModel.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.models;
6
7
import com.vladsch.flexmark.ast.Link;
8
import org.renjin.gnur.format;
9
10
/**
11
 * Represents the model for a hyperlink: text, url, and title.
12
 */
13
public final class HyperlinkModel extends ObjectModel {
14
15
  /**
16
   * Constructs a new hyperlink model in Markdown format by default with no
17
   * title (i.e., tooltip).
18
   *
19
   * @param text The hyperlink text displayed (e.g., displayed to the user).
20
   */
21
  public HyperlinkModel( final String text ) {
22
    super( text );
23
  }
24
25
  /**
26
   * Constructs a new hyperlink model in Markdown format by default.
27
   *
28
   * @param text  The hyperlink text displayed (e.g., displayed to the user).
29
   * @param url   The destination URL (e.g., when clicked).
30
   * @param title The hyperlink title (e.g., shown as a tooltip).
31
   */
32
  public HyperlinkModel(
33
    final String text, final String url, final String title ) {
34
    super( text, url, title );
35
  }
36
37
  /**
38
   * Constructs a new hyperlink model for the given AST link.
39
   *
40
   * @param link A Markdown link.
41
   */
42
  public HyperlinkModel( final Link link ) {
43
    this(
44
      link.getText().toString(),
45
      link.getUrl().toString(),
46
      link.getTitle().toString()
47
    );
48
  }
49
50
  /**
51
   * Returns the string in Markdown format by default.
52
   *
53
   * @return A Markdown version of the hyperlink.
54
   */
55
  @Override
56
  public String toString() {
57
    final String format = hasText()
58
      ? "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)")
59
      : "%s%s%s";
60
61
    // Becomes ""+URL+"" if no text is set.
62
    // Becomes [TITLE]+(URL)+"" if no title is set.
63
    // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
64
    return String.format( format, getText(), getUrl(), getTitle() );
65
  }
66
}
167
A src/main/java/com/keenwrite/ui/models/ImageModel.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.models;
6
7
import org.renjin.gnur.format;
8
9
/**
10
 * Represents the model for an image: text, url, and title.
11
 */
12
public final class ImageModel extends ObjectModel {
13
14
  /**
15
   * Constructs a new image model in Markdown format by default with no
16
   * title (i.e., tooltip).
17
   *
18
   * @param text The alternate text (e.g., displayed to the user).
19
   */
20
  public ImageModel( final String text ) {
21
    super( text );
22
  }
23
24
  /**
25
   * Returns the string in Markdown format by default.
26
   *
27
   * @return An image reference using Markdown syntax.
28
   */
29
  @Override
30
  public String toString() {
31
    final String format = hasText()
32
      ? "![%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)")
33
      : "![%s](%s)%s";
34
35
    return String.format( format, getText(), getUrl(), getTitle() );
36
  }
37
}
138
A src/main/java/com/keenwrite/ui/models/ObjectModel.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.models;
6
7
import static com.keenwrite.util.Strings.sanitize;
8
9
/**
10
 * Represents the model for an object containing text, url, and title.
11
 */
12
class ObjectModel {
13
  private String mText;
14
  private String mUrl;
15
  private String mTitle;
16
17
  /**
18
   * Constructs a new object model in Markdown format by default with no
19
   * title (i.e., tooltip).
20
   *
21
   * @param text The hyperlink text displayed (e.g., displayed to the user).
22
   */
23
  public ObjectModel( final String text ) {
24
    this( text, null, null );
25
  }
26
27
  /**
28
   * Constructs a new object model in Markdown format by default.
29
   *
30
   * @param text  The text displayed (e.g., to the user).
31
   * @param url   The destination URL (e.g., when clicked).
32
   * @param title The text title (e.g., shown as a tooltip).
33
   */
34
  public ObjectModel(
35
    final String text, final String url, final String title ) {
36
    setText( text );
37
    setUrl( url );
38
    setTitle( title );
39
  }
40
41
  public void setText( final String text ) {
42
    mText = sanitize( text );
43
  }
44
45
  public void setUrl( final String url ) {
46
    mUrl = sanitize( url );
47
  }
48
49
  public void setTitle( final String title ) {
50
    mTitle = sanitize( title );
51
  }
52
53
  /**
54
   * Answers whether text has been set for the model.
55
   *
56
   * @return true The text description is set.
57
   */
58
  public boolean hasText() {
59
    return !getText().isEmpty();
60
  }
61
62
  /**
63
   * Answers whether a title (tooltip) has been set for the model.
64
   *
65
   * @return true The title is set.
66
   */
67
  public boolean hasTitle() {
68
    return !getTitle().isEmpty();
69
  }
70
71
  public String getText() {
72
    return mText;
73
  }
74
75
  public String getUrl() {
76
    return mUrl;
77
  }
78
79
  public String getTitle() {
80
    return mTitle;
81
  }
82
}
183
A src/main/java/com/keenwrite/ui/outline/DocumentOutline.java
1
package com.keenwrite.ui.outline;
2
3
import com.keenwrite.events.Bus;
4
import com.keenwrite.events.CaretNavigationEvent;
5
import com.keenwrite.events.ParseHeadingEvent;
6
import javafx.scene.Node;
7
import javafx.scene.control.TreeCell;
8
import javafx.scene.control.TreeItem;
9
import javafx.scene.control.TreeView;
10
import javafx.util.Callback;
11
import org.greenrobot.eventbus.Subscribe;
12
13
import static com.keenwrite.events.Bus.register;
14
import static com.keenwrite.ui.fonts.IconFactory.createGraphic;
15
import static javafx.application.Platform.runLater;
16
import static javafx.scene.input.MouseButton.PRIMARY;
17
import static javafx.scene.input.MouseEvent.MOUSE_PRESSED;
18
19
public class DocumentOutline extends TreeView<ParseHeadingEvent> {
20
  private TreeItem<ParseHeadingEvent> mCurrent;
21
22
  /**
23
   * Registers with the {@link Bus}.
24
   */
25
  public DocumentOutline() {
26
    // Override double-click to issue a caret navigation event.
27
    setCellFactory( new Callback<>() {
28
      @Override
29
      public TreeCell<ParseHeadingEvent> call(
30
        TreeView<ParseHeadingEvent> treeView ) {
31
        TreeCell<ParseHeadingEvent> cell = new TreeCell<>() {
32
          @Override
33
          protected void updateItem( ParseHeadingEvent item, boolean empty ) {
34
            super.updateItem( item, empty );
35
            if( empty || item == null ) {
36
              setText( null );
37
              setGraphic( null );
38
            }
39
            else {
40
              setText( item.toString() );
41
              setGraphic( createIcon() );
42
            }
43
          }
44
        };
45
46
        cell.addEventFilter( MOUSE_PRESSED, event -> {
47
          if( event.getButton() == PRIMARY && event.getClickCount() % 2 == 0 ) {
48
            CaretNavigationEvent.fire( cell.getItem().getOffset() );
49
            event.consume();
50
          }
51
        } );
52
53
        return cell;
54
      }
55
    } );
56
57
    register( this );
58
  }
59
60
  /**
61
   * Updates the {@link TreeView} with the given event data. This method will
62
   * track the most recently added {@link TreeItem} so that the nesting
63
   * hierarchy reflects the document hierarchy.
64
   *
65
   * @param event Represents a document heading to add to the tree.
66
   */
67
  @Subscribe
68
  public void handle( final ParseHeadingEvent event ) {
69
    runLater(
70
      () -> mCurrent = event.isNewOutline() ? clear( event ) : addItem( event )
71
    );
72
  }
73
74
  private TreeItem<ParseHeadingEvent> clear( final ParseHeadingEvent event ) {
75
    final var root = createTreeItem( event );
76
    setRoot( root );
77
    setShowRoot( false );
78
    return root;
79
  }
80
81
  /**
82
   * This method is called once for every heading in the document. The event
83
   * data directly corresponds to the sequence of headings in the document.
84
   * The given event data contains a level that is relative to the last
85
   * item in the tree.
86
   *
87
   * @param next Contains a level value to indicate heading depth.
88
   */
89
  private TreeItem<ParseHeadingEvent> addItem( final ParseHeadingEvent next ) {
90
    var parent = mCurrent;
91
    final var item = createTreeItem( next );
92
    final var curr = parent.getValue();
93
    final var currLevel = curr.getLevel();
94
    final var nextLevel = next.getLevel();
95
    var deltaLevel = currLevel - nextLevel + 1;
96
97
    while( deltaLevel > 0 && parent != null ) {
98
      parent = parent.getParent();
99
      deltaLevel--;
100
    }
101
102
    if( parent == null ) {
103
      parent = getRoot();
104
    }
105
106
    parent.getChildren().add( item );
107
108
    return item;
109
  }
110
111
  private TreeItem<ParseHeadingEvent> createTreeItem(
112
    final ParseHeadingEvent event ) {
113
    final var item = new TreeItem<>( event, createIcon() );
114
    item.setExpanded( true );
115
    return item;
116
  }
117
118
  private Node createIcon() {
119
    return createGraphic( "BOOKMARK" );
120
  }
121
}
1122
A src/main/java/com/keenwrite/ui/spelling/TextEditorSpellChecker.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.spelling;
3
4
import com.keenwrite.editors.TextEditor;
5
import com.keenwrite.spelling.api.SpellCheckListener;
6
import com.keenwrite.spelling.api.SpellChecker;
7
import com.vladsch.flexmark.parser.Parser;
8
import com.vladsch.flexmark.util.ast.NodeVisitor;
9
import com.vladsch.flexmark.util.ast.VisitHandler;
10
import javafx.beans.property.ObjectProperty;
11
import javafx.scene.control.ContextMenu;
12
import javafx.scene.control.IndexRange;
13
import javafx.scene.control.MenuItem;
14
import org.fxmisc.richtext.StyleClassedTextArea;
15
import org.fxmisc.richtext.model.PlainTextChange;
16
import org.fxmisc.richtext.model.StyleSpansBuilder;
17
18
import java.util.Collection;
19
import java.util.List;
20
import java.util.concurrent.atomic.AtomicInteger;
21
22
import static com.keenwrite.events.StatusEvent.clue;
23
import static java.util.Collections.emptyList;
24
import static java.util.Collections.singleton;
25
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
26
27
/**
28
 * Responsible for checking the spelling of a document being edited.
29
 */
30
public final class TextEditorSpellChecker {
31
  private final ObjectProperty<SpellChecker> mSpellChecker;
32
  private final Parser mParser = Parser.builder().build();
33
34
  /**
35
   * Create a new spellchecker that can highlight spelling mistakes within a
36
   * {@link StyleClassedTextArea}. The given {@link SpellChecker} is wrapped
37
   * in a mutable {@link ObjectProperty} because the user may swap languages
38
   * at runtime.
39
   *
40
   * @param checker The spellchecker to use when scanning for spelling errors.
41
   */
42
  public TextEditorSpellChecker( final ObjectProperty<SpellChecker> checker ) {
43
    assert checker != null;
44
45
    mSpellChecker = checker;
46
  }
47
48
  /**
49
   * Call to spellcheck the entire document.
50
   */
51
  public void checkDocument( final TextEditor editor ) {
52
    spellcheck( editor.getTextArea(), editor.getText(), -1 );
53
  }
54
55
  /**
56
   * Listen for changes to any particular paragraph and perform a quick
57
   * spell check upon it. The style classes in the editor will be changed to
58
   * mark any spelling mistakes in the paragraph. The user may then interact
59
   * with any misspelled word (i.e., any piece of text that is marked) to
60
   * revise the spelling.
61
   * <p>
62
   * Use {@link PlainTextChange} so that notifications of style changes
63
   * are suppressed. Checking against the identity ensures that only
64
   * new text additions or deletions trigger proofreading.
65
   */
66
  public void checkParagraph(
67
    final StyleClassedTextArea editor,
68
    final PlainTextChange change ) {
69
    // Check current paragraph; the document was checked when opened.
70
    final var offset = change.getPosition();
71
    final var position = editor.offsetToPosition( offset, Forward );
72
    var paraId = position.getMajor();
73
    var paragraph = editor.getParagraph( paraId );
74
    var text = paragraph.getText();
75
76
    // If the current paragraph is blank, it may mean the caret is at the
77
    // start of a new paragraph (i.e., a blank line). Spellcheck the "next"
78
    // paragraph, instead.
79
    if( text.isBlank() ) {
80
      final var paragraphs = editor.getParagraphs();
81
      final var count = paragraphs == null ? 0 : paragraphs.size();
82
83
      paraId = Math.min( paraId + 1, count - 1 );
84
      paragraph = editor.getParagraph( paraId );
85
      text = paragraph.getText();
86
    }
87
88
    // Prevent doubling-up styles.
89
    editor.clearStyle( paraId );
90
91
    spellcheck( editor, text, paraId );
92
  }
93
94
  /**
95
   * Spellchecks a subset of the entire document.
96
   *
97
   * @param editor The document (or portions thereof) to spellcheck.
98
   * @param text   Look up words for this text in the lexicon.
99
   * @param paraId Set to -1 to apply resulting style spans to the entire text.
100
   */
101
  private void spellcheck(
102
    final StyleClassedTextArea editor, final String text, final int paraId ) {
103
    final var builder = new StyleSpansBuilder<Collection<String>>();
104
    final var runningIndex = new AtomicInteger( 0 );
105
106
    // The text nodes must be relayed through a contextual "visitor" that
107
    // can return text in chunks with correlative offsets into the string.
108
    // This allows Markdown and R Markdown documents to return sets of
109
    // words to check.
110
    final var node = mParser.parse( text );
111
    final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> {
112
      // Treat hyphenated compound words as individual words.
113
      final var check = visited.replace( '-', ' ' );
114
      final var checker = getSpellChecker();
115
116
      checker.proofread( check, ( misspelled, prevIndex, currIndex ) -> {
117
        prevIndex += bIndex;
118
        currIndex += bIndex;
119
120
        // Clear styling between lexiconically absent words.
121
        builder.add( emptyList(), prevIndex - runningIndex.get() );
122
        builder.add( singleton( "spelling" ), currIndex - prevIndex );
123
        runningIndex.set( currIndex );
124
      } );
125
    } );
126
127
    visitor.visit( node );
128
129
    // If the running index was set, at least one word triggered the listener.
130
    if( runningIndex.get() > 0 ) {
131
      // Clear styling after the last lexiconically absent word.
132
      builder.add( emptyList(), text.length() - runningIndex.get() );
133
134
      final var spans = builder.create();
135
136
      if( paraId >= 0 ) {
137
        editor.setStyleSpans( paraId, 0, spans );
138
      }
139
      else {
140
        editor.setStyleSpans( 0, spans );
141
      }
142
    }
143
  }
144
145
  /**
146
   * Called to display a pop-up with a list of spelling corrections. When the
147
   * user selects an item from the list, the word at the caret position is
148
   * replaced (with the selected item).
149
   */
150
  public void autofix( final TextEditor editor ) {
151
    final var caretWord = editor.getCaretWord();
152
    final var textArea = editor.getTextArea();
153
    final var word = textArea.getText( caretWord );
154
    final var suggestions = checkWord( word, 10 );
155
156
    if( suggestions.isEmpty() ) {
157
      clue( "Editor.spelling.check.matches.none", word );
158
    }
159
    else if( !suggestions.contains( word ) ) {
160
      final var menu = createSuggestionsPopup( textArea );
161
      final var items = menu.getItems();
162
      textArea.setContextMenu( menu );
163
164
      for( final var correction : suggestions ) {
165
        items.add( createSuggestedItem( textArea, caretWord, correction ) );
166
      }
167
168
      textArea.getCaretBounds().ifPresent(
169
        bounds -> {
170
          menu.setOnShown( event -> menu.requestFocus() );
171
          menu.show( textArea, bounds.getCenterX(), bounds.getCenterY() );
172
        }
173
      );
174
    }
175
    else {
176
      clue( "Editor.spelling.check.matches.okay", word );
177
    }
178
  }
179
180
  private ContextMenu createSuggestionsPopup(
181
    final StyleClassedTextArea textArea ) {
182
    final var menu = new ContextMenu();
183
184
    menu.setAutoHide( true );
185
    menu.setHideOnEscape( true );
186
    menu.setOnHidden( event -> textArea.setContextMenu( null ) );
187
188
    return menu;
189
  }
190
191
  /**
192
   * Creates a menu item capable of replacing a word under the cursor.
193
   *
194
   * @param textArea The text upon which this action will replace.
195
   * @param i        The beginning and ending text offset to replace.
196
   * @param s        The text to replace at the given offset.
197
   * @return The menu item that, if actioned, will replace the text.
198
   */
199
  private MenuItem createSuggestedItem(
200
    final StyleClassedTextArea textArea,
201
    final IndexRange i,
202
    final String s ) {
203
    final var menuItem = new MenuItem( s );
204
205
    menuItem.setOnAction( event -> textArea.replaceText( i, s ) );
206
207
    return menuItem;
208
  }
209
210
  /**
211
   * Returns a list of suggests for the given word. This is typically used to
212
   * check for suitable replacements of the word at the caret position.
213
   *
214
   * @param word  The word to spellcheck.
215
   * @param count The maximum number of suggested alternatives to return.
216
   * @return A list of recommended spellings for the given word.
217
   */
218
  public List<String> checkWord( final String word, final int count ) {
219
    return getSpellChecker().suggestions( word, count );
220
  }
221
222
  private SpellChecker getSpellChecker() {
223
    return mSpellChecker.get();
224
  }
225
226
  private static final class TextVisitor {
227
    private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>(
228
      com.vladsch.flexmark.ast.Text.class, this::visit )
229
    );
230
231
    private final SpellCheckListener mConsumer;
232
233
    public TextVisitor( final SpellCheckListener consumer ) {
234
      mConsumer = consumer;
235
    }
236
237
    private void visit( final com.vladsch.flexmark.util.ast.Node node ) {
238
      if( node instanceof com.vladsch.flexmark.ast.Text ) {
239
        mConsumer.accept( node.getChars().toString(),
240
                          node.getStartOffset(),
241
                          node.getEndOffset() );
242
      }
243
244
      mVisitor.visitChildren( node );
245
    }
246
  }
247
}
1248
A src/main/java/com/keenwrite/ui/tree/AltTreeView.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.tree;
3
4
import com.keenwrite.ui.cells.AltTreeCell;
5
import javafx.scene.control.TreeItem;
6
import javafx.scene.control.TreeView;
7
import javafx.util.StringConverter;
8
9
/**
10
 * Responsible for allowing users to edit items in the tree as well as
11
 * drag and drop. The goal is to be a drop-in replacement for the regular
12
 * JavaFX {@link TreeView}, which does not offer editing and moving {@link
13
 * TreeItem} instances.
14
 *
15
 * @param <T> The type of data to edit.
16
 */
17
public class AltTreeView<T> extends TreeView<T> {
18
  public AltTreeView(
19
    final TreeItem<T> root, final StringConverter<T> converter ) {
20
    super( root );
21
22
    assert root != null;
23
    assert converter != null;
24
25
    setEditable( true );
26
    setCellFactory( _ -> new AltTreeCell<>( converter ) );
27
    setShowRoot( false );
28
29
    // When focus is lost while not editing, deselect all items.
30
    focusedProperty().addListener( ( _, o, _ ) -> {
31
      if( o && getEditingItem() == null ) {
32
        getSelectionModel().clearSelection();
33
      }
34
    } );
35
  }
36
}
137
A src/main/java/com/keenwrite/ui/tree/TreeItemConverter.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.tree;
3
4
import javafx.util.StringConverter;
5
6
import static com.keenwrite.util.Strings.sanitize;
7
8
/**
9
 * Responsible for converting objects to and from string instances. The
10
 * tree items contain only strings, so this effectively is a string-to-string
11
 * converter, which allows the implementation to retain its generics.
12
 */
13
public class TreeItemConverter extends StringConverter<String> {
14
15
  @Override
16
  public String toString( final String object ) {
17
    return sanitize( object );
18
  }
19
20
  @Override
21
  public String fromString( final String string ) {
22
    return sanitize( string );
23
  }
24
}
125
A src/main/java/com/keenwrite/util/AlphanumComparator.java
1
/*
2
 * The Alphanum Algorithm is an improved sorting algorithm for strings
3
 * containing numbers. Rather than sort numbers in ASCII order like
4
 * a standard sort, this algorithm sorts numbers in numeric order.
5
 *
6
 * The Alphanum Algorithm is discussed at http://www.DaveKoelle.com
7
 *
8
 * Released under the MIT License - https://opensource.org/licenses/MIT
9
 *
10
 * Copyright 2007-2017 David Koelle
11
 *
12
 * Permission is hereby granted, free of charge, to any person obtaining
13
 * a copy of this software and associated documentation files (the "Software"),
14
 * to deal in the Software without restriction, including without limitation
15
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
16
 * and/or sell copies of the Software, and to permit persons to whom the
17
 * Software is furnished to do so, subject to the following conditions:
18
 *
19
 * The above copyright notice and this permission notice shall be included
20
 * in all copies or substantial portions of the Software.
21
 *
22
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
25
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
26
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
27
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
28
 * USE OR OTHER DEALINGS IN THE SOFTWARE.
29
 */
30
package com.keenwrite.util;
31
32
import java.io.Serializable;
33
import java.util.Comparator;
34
35
import static java.lang.Character.isDigit;
36
37
/**
38
 * Responsible for sorting lists that may contain numeric values. Usage:
39
 * <pre>
40
 *   Collections.sort(list, new AlphanumComparator());
41
 * </pre>
42
 * <p>
43
 * Where "list" is the list to sort alphanumerically, not lexicographically.
44
 * </p>
45
 */
46
public final class AlphanumComparator<T> implements
47
  Comparator<T>, Serializable {
48
49
  /**
50
   * Returns a chunk of text that is continuous with respect to digits or
51
   * non-digits.
52
   *
53
   * @param s      The string to compare.
54
   * @param length The string length, for improved efficiency.
55
   * @param marker The current index into a subset of the given string.
56
   * @return The substring {@code s} that is a continuous text chunk of the
57
   * same character type.
58
   */
59
  private StringBuilder chunk( final String s, final int length, int marker ) {
60
    assert s != null;
61
    assert length >= 0;
62
    assert marker < length;
63
64
    // Prevent any possible memory re-allocations by using the length.
65
    final var chunk = new StringBuilder( length );
66
    var c = s.charAt( marker );
67
    final var chunkType = isDigit( c );
68
69
    // While the character at the current position is the same type (numeric or
70
    // alphabetic), append the character to the current chunk.
71
    while( marker < length &&
72
      isDigit( c = s.charAt( marker++ ) ) == chunkType ) {
73
      chunk.append( c );
74
    }
75
76
    return chunk;
77
  }
78
79
  /**
80
   * Performs an alphanumeric comparison of two strings, sorting numerically
81
   * first when numbers are found within the string. If either argument is
82
   * {@code null}, this will return zero.
83
   *
84
   * @param o1 The object to compare against {@code s2}, converted to string.
85
   * @param o2 The object to compare against {@code s1}, converted to string.
86
   * @return a negative integer, zero, or a positive integer if the first
87
   * argument is less than, equal to, or greater than the second, respectively.
88
   */
89
  @Override
90
  public int compare( final T o1, final T o2 ) {
91
    if( o1 == null || o2 == null ) {
92
      return 0;
93
    }
94
95
    final var s1 = o1.toString();
96
    final var s2 = o2.toString();
97
    final var s1Length = s1.length();
98
    final var s2Length = s2.length();
99
100
    var thisMarker = 0;
101
    var thatMarker = 0;
102
103
    while( thisMarker < s1Length && thatMarker < s2Length ) {
104
      final var thisChunk = chunk( s1, s1Length, thisMarker );
105
      final var thisChunkLength = thisChunk.length();
106
      thisMarker += thisChunkLength;
107
      final var thatChunk = chunk( s2, s2Length, thatMarker );
108
      final var thatChunkLength = thatChunk.length();
109
      thatMarker += thatChunkLength;
110
111
      // If both chunks contain numeric characters, sort them numerically
112
      int result;
113
114
      if( isDigit( thisChunk.charAt( 0 ) ) &&
115
        isDigit( thatChunk.charAt( 0 ) ) ) {
116
        // If equal, the first different number counts
117
        if( (result = thisChunkLength - thatChunkLength) == 0 ) {
118
          for( var i = 0; i < thisChunkLength; i++ ) {
119
            result = thisChunk.charAt( i ) - thatChunk.charAt( i );
120
121
            if( result != 0 ) {
122
              return result;
123
            }
124
          }
125
        }
126
      }
127
      else {
128
        result = thisChunk.compareTo( thatChunk );
129
      }
130
131
      if( result != 0 ) {
132
        return result;
133
      }
134
    }
135
136
    return s1Length - s2Length;
137
  }
138
}
1139
A src/main/java/com/keenwrite/util/CyclicIterator.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.util;
6
7
import java.util.*;
8
9
/**
10
 * Responsible for iterating over a list either forwards or backwards. When
11
 * the iterator reaches the last element in the list, the next element will
12
 * be the first. When the iterator reaches the first element in the list,
13
 * the previous element will be the last.
14
 * <p>
15
 * Due to the ability to move forwards and backwards through the list, rather
16
 * than force client classes to track the list index independently, this
17
 * iterator provides an accessor to the index. The index is zero-based.
18
 * </p>
19
 *
20
 * @param <T> The type of list to be cycled.
21
 */
22
public final class CyclicIterator<T> implements ListIterator<T> {
23
  private final List<T> mList;
24
25
  /**
26
   * Initialize to an invalid index so that the first calls to either
27
   * {@link #previous()} or {@link #next()} will return the starting or ending
28
   * element.
29
   */
30
  private int mIndex = -1;
31
32
  /**
33
   * Creates an iterator that cycles indefinitely through the given list.
34
   *
35
   * @param list The list to cycle through indefinitely.
36
   */
37
  public CyclicIterator( final Collection<T> list ) {
38
    mList = new ArrayList<>( list );
39
  }
40
41
  /**
42
   * @return {@code true} if there is at least one element.
43
   */
44
  @Override
45
  public boolean hasNext() {
46
    return !mList.isEmpty();
47
  }
48
49
  /**
50
   * @return {@code true} if there is at least one element.
51
   */
52
  @Override
53
  public boolean hasPrevious() {
54
    return !mList.isEmpty();
55
  }
56
57
  @Override
58
  public int nextIndex() {
59
    return computeIndex( +1 );
60
  }
61
62
  @Override
63
  public int previousIndex() {
64
    return computeIndex( -1 );
65
  }
66
67
  @Override
68
  public void remove() {
69
    mList.remove( mIndex );
70
  }
71
72
  @Override
73
  public void set( final T t ) {
74
    mList.set( mIndex, t );
75
  }
76
77
  @Override
78
  public void add( final T t ) {
79
    mList.add( mIndex, t );
80
  }
81
82
  /**
83
   * Returns the next item in the list, which will cycle to the first
84
   * item as necessary.
85
   *
86
   * @return The next item in the list, cycling to the start if needed.
87
   */
88
  @Override
89
  public T next() {
90
    return cycle( +1 );
91
  }
92
93
  /**
94
   * Returns the previous item in the list, which will cycle to the last
95
   * item as necessary.
96
   *
97
   * @return The previous item in the list, cycling to the end if needed.
98
   */
99
  @Override
100
  public T previous() {
101
    return cycle( -1 );
102
  }
103
104
  /**
105
   * Cycles to the next or previous element, depending on the direction value.
106
   *
107
   * @param direction Use -1 for previous, +1 for next.
108
   * @return The next or previous item in the list.
109
   */
110
  private T cycle( final int direction ) {
111
    try {
112
      return mList.get( mIndex = computeIndex( direction ) );
113
    } catch( final Exception ex ) {
114
      throw new NoSuchElementException( ex );
115
    }
116
  }
117
118
  /**
119
   * Returns the index of the value retrieved from the most recent call to
120
   * either {@link #previous()} or {@link #next()}.
121
   *
122
   * @return The list item index or -1 if no calls have been made to retrieve
123
   * an item from the list.
124
   */
125
  public int getIndex() {
126
    return mIndex;
127
  }
128
129
  private int computeIndex( final int direction ) {
130
    final var i = mIndex + direction;
131
    final var size = mList.size();
132
    final var result = i < 0
133
        ? size - 1
134
        : size == 0 ? 0 : i % size;
135
136
    // Ensure the invariant holds.
137
    assert 0 <= result && result < size || size == 0;
138
139
    return result;
140
  }
141
}
1142
A src/main/java/com/keenwrite/util/DataTypeConverter.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.security.MessageDigest;
5
import java.security.NoSuchAlgorithmException;
6
7
import static java.nio.charset.StandardCharsets.US_ASCII;
8
import static java.nio.charset.StandardCharsets.UTF_8;
9
10
/**
11
 * Responsible for converting various data types into other representations.
12
 */
13
public final class DataTypeConverter {
14
  private static final byte[] HEX = "0123456789ABCDEF".getBytes( US_ASCII );
15
16
  /**
17
   * Returns a hexadecimal number that represents the bit sequences provided
18
   * in the given array of bytes.
19
   *
20
   * @param bytes The bytes to convert to a hexadecimal string.
21
   * @return An uppercase-encoded hexadecimal number.
22
   */
23
  public static String toHex( final byte[] bytes ) {
24
    final var hexChars = new byte[ bytes.length * 2 ];
25
    final var len = bytes.length;
26
27
    for( var i = 0; i < len; i++ ) {
28
      final var digit = bytes[ i ] & 0xFF;
29
30
      hexChars[ (i << 1) ] = HEX[ digit >>> 4 ];
31
      hexChars[ (i << 1) + 1 ] = HEX[ digit & 0x0F ];
32
    }
33
34
    return new String( hexChars, UTF_8 );
35
  }
36
37
  /**
38
   * Hashes a string using the SHA-1 algorithm.
39
   *
40
   * @param s The string to has.
41
   * @return The hashed string.
42
   * @throws NoSuchAlgorithmException Could not find the SHA-1 algorithm.
43
   */
44
  public static byte[] hash( final String s ) throws NoSuchAlgorithmException {
45
    final var digest = MessageDigest.getInstance( "SHA-1" );
46
    return digest.digest( s.getBytes( UTF_8 ) );
47
  }
48
}
149
A src/main/java/com/keenwrite/util/Diacritics.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import static java.text.Normalizer.Form.NFD;
5
import static java.text.Normalizer.normalize;
6
7
/**
8
 * Responsible for modifying diacritics.
9
 */
10
public class Diacritics {
11
  private static final String UNCRITIC = "\\p{M}+";
12
13
  /**
14
   * Returns the value of the string without diacritic marks.
15
   *
16
   * @param text The text to normalize.
17
   * @return A non-null, possibly empty string.
18
   */
19
  public static String remove( final String text ) {
20
    return normalize( text, NFD ).replaceAll( UNCRITIC, "" );
21
  }
22
}
123
A src/main/java/com/keenwrite/util/EncodingDetector.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.util;
6
7
import org.mozilla.universalchardet.UniversalDetector;
8
9
import java.nio.charset.Charset;
10
11
import static com.keenwrite.constants.Constants.DEFAULT_CHARSET;
12
import static java.nio.charset.Charset.forName;
13
import static java.util.Locale.ENGLISH;
14
15
/**
16
 * Wraps the {@link UniversalDetector} with to provide enhanced abilities
17
 * and bug fixes (if needed).
18
 */
19
public class EncodingDetector {
20
21
  private final UniversalDetector mDetector;
22
23
  public EncodingDetector() {
24
    mDetector = new UniversalDetector( null );
25
  }
26
27
  /**
28
   * Returns the character set for the constructed input. This will coerce
29
   * both US-ASCII and TIS620 to UTF-8.
30
   *
31
   * @param bytes The textual content having an as yet unknown encoding.
32
   * @return The character encoding for the given bytes.
33
   */
34
  public Charset detect( final byte[] bytes ) {
35
    mDetector.handleData( bytes, 0, bytes.length );
36
    mDetector.dataEnd();
37
38
    final String detectedCharset = mDetector.getDetectedCharset();
39
40
    // TODO: Revert when the issue has been fixed.
41
    // https://github.com/albfernandez/juniversalchardet/issues/35
42
    return switch( detectedCharset ) {
43
      case null -> DEFAULT_CHARSET;
44
      case "US-ASCII", "TIS620" -> DEFAULT_CHARSET;
45
      default -> forName( detectedCharset.toUpperCase( ENGLISH ) );
46
    };
47
  }
48
}
149
A src/main/java/com/keenwrite/util/FailableBiConsumer.java
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one or more
3
 * contributor license agreements.  See the NOTICE file distributed with
4
 * this work for additional information regarding copyright ownership.
5
 * The ASF licenses this file to You under the Apache License, Version 2.0
6
 * (the "License"); you may not use this file except in compliance with
7
 * the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
package com.keenwrite.util;
18
19
import java.util.function.BiConsumer;
20
21
/**
22
 * A functional interface like {@link BiConsumer} that declares a {@link Throwable}.
23
 *
24
 * @param <T> Consumed type 1.
25
 * @param <U> Consumed type 2.
26
 * @param <E> The kind of thrown exception or error.
27
 */
28
@FunctionalInterface
29
public interface FailableBiConsumer<T, U, E extends Throwable> {
30
31
  /**
32
   * Accepts the given arguments.
33
   *
34
   * @param t the first parameter for the consumable to accept
35
   * @param u the second parameter for the consumable to accept
36
   * @throws E Thrown when the consumer fails.
37
   */
38
  void accept(T t, U u) throws E;
39
}
140
A src/main/java/com/keenwrite/util/FileWalker.java
1
/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.io.IOException;
5
import java.nio.file.Files;
6
import java.nio.file.Path;
7
import java.util.function.Consumer;
8
9
import static java.lang.String.format;
10
import static java.nio.file.FileSystems.getDefault;
11
12
/**
13
 * Responsible for finding files in a file system that match a particular
14
 * globbing file name pattern.
15
 *
16
 * @see ResourceWalker#walk(String, String, Consumer)
17
 */
18
public class FileWalker {
19
  /**
20
   * Walks the given directory hierarchy for files that match the given
21
   * globbing file name pattern. This will search to a depth of 10 directories
22
   * deep (to avoid infinite recursion).
23
   *
24
   * @param path Root directory to scan for files matching the glob.
25
   * @param glob Only files matching the pattern will be consumed.
26
   * @param c    Function to call for each matching path found.
27
   * @throws IOException Could not walk the tree.
28
   */
29
  public static void walk(
30
    final Path path, final String glob, final Consumer<Path> c )
31
    throws IOException {
32
    final var pattern = format( "glob:%s", glob );
33
    final var matcher = getDefault().getPathMatcher( pattern );
34
35
    try( final var walk = Files.walk( path, 10 ) ) {
36
      for( final var it = walk.iterator(); it.hasNext(); ) {
37
        final var p = it.next();
38
        if( matcher.matches( p ) ) {
39
          c.accept( p );
40
        }
41
      }
42
    }
43
  }
44
}
145
A src/main/java/com/keenwrite/util/FontLoader.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.awt.*;
5
import java.awt.font.TextAttribute;
6
import java.io.FileInputStream;
7
import java.io.IOException;
8
import java.io.InputStream;
9
import java.net.URI;
10
import java.util.Map;
11
12
import static com.keenwrite.constants.Constants.FONT_DIRECTORY;
13
import static com.keenwrite.events.StatusEvent.clue;
14
import static com.keenwrite.util.ProtocolScheme.valueFrom;
15
import static com.keenwrite.util.ResourceWalker.walk;
16
import static java.awt.Font.TRUETYPE_FONT;
17
import static java.awt.Font.createFont;
18
import static java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment;
19
import static java.awt.font.TextAttribute.*;
20
21
/**
22
 * Loads fonts into the application's {@link GraphicsEnvironment} so that
23
 * preview can display text using non-system fonts.
24
 */
25
public final class FontLoader {
26
  /**
27
   * Globbing pattern to match font names.
28
   */
29
  public static final String GLOB_FONTS = "**.{ttf,otf}";
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
  public static void initFonts() {
41
    // Editor, preview, and TeX fonts
42
    initFonts( FONT_DIRECTORY );
43
44
    // FontAwesome font
45
    initFonts( "/org" );
46
  }
47
48
  @SuppressWarnings( "unchecked" )
49
  private static void initFonts( final String directory ) {
50
    try {
51
      final var ge = getLocalGraphicsEnvironment();
52
      walk(
53
        directory, GLOB_FONTS, path -> {
54
          final var uri = path.toUri();
55
          final var filename = path.toString();
56
57
          try( final var is = openFont( uri, filename ) ) {
58
            final var font = createFont( TRUETYPE_FONT, is );
59
            final var attributes =
60
              (Map<TextAttribute, Integer>) font.getAttributes();
61
62
            attributes.put( LIGATURES, LIGATURES_ON );
63
            attributes.put( KERNING, KERNING_ON );
64
            ge.registerFont( font.deriveFont( attributes ) );
65
          } catch( final Exception ex ) {
66
            clue( ex );
67
          }
68
        }
69
      );
70
    } catch( final Exception ex ) {
71
      clue( ex );
72
    }
73
  }
74
75
  /**
76
   * Attempts to open a font, regardless of whether the font is a resource in
77
   * a JAR file or somewhere on the file system.
78
   *
79
   * @param uri      Directory or archive containing a font.
80
   * @param filename Name of the font file.
81
   * @return An open file handled to the font.
82
   * @throws IOException Could not open the resource as a stream.
83
   */
84
  private static InputStream openFont( final URI uri, final String filename )
85
    throws IOException {
86
    return valueFrom( uri ).isJar()
87
      ? FontLoader.class.getResourceAsStream( filename )
88
      : new FileInputStream( filename );
89
  }
90
}
191
A src/main/java/com/keenwrite/util/GenericBuilder.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.util.LinkedList;
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
 *
18
 * @param <MT> The mutable definition for the type of object to build.
19
 * @param <IT> The immutable definition for the type of object to build.
20
 */
21
public class GenericBuilder<MT, IT> {
22
  /**
23
   * Provides the methods to use for setting object properties.
24
   */
25
  private final Supplier<MT> mMutable;
26
27
  /**
28
   * Calling {@link #build()} will instantiate the immutable instance using
29
   * the mutator.
30
   */
31
  private final Function<MT, IT> mImmutable;
32
33
  /**
34
   * Adds a modifier to call when building an instance.
35
   */
36
  private final List<Consumer<MT>> mModifiers = new LinkedList<>();
37
38
  /**
39
   * Starting point for building an instance of a particular class.
40
   *
41
   * @param supplier Returns the instance to build.
42
   * @param <MT>     The type of class to build.
43
   * @return A new {@link GenericBuilder} capable of populating data for an
44
   * instance of the class provided by the {@link Supplier}.
45
   */
46
  public static <MT, IT> GenericBuilder<MT, IT> of(
47
    final Supplier<MT> supplier, final Function<MT, IT> immutable ) {
48
    return new GenericBuilder<>( supplier, immutable );
49
  }
50
51
  /**
52
   * Constructs a new builder instance that is capable of populating values for
53
   * any type of object.
54
   *
55
   * @param mutator Provides methods to use for setting object properties.
56
   */
57
  protected GenericBuilder(
58
    final Supplier<MT> mutator, final Function<MT, IT> immutable ) {
59
    assert mutator != null;
60
    assert immutable != null;
61
62
    mMutable = mutator;
63
    mImmutable = immutable;
64
  }
65
66
  /**
67
   * Registers a new value with the builder.
68
   *
69
   * @param consumer Accepts a value to be set upon the built object.
70
   * @param value    The value to use when building.
71
   * @param <V>      The type of value used when building.
72
   * @return This {@link GenericBuilder} instance.
73
   */
74
  public <V> GenericBuilder<MT, IT> with(
75
    final BiConsumer<MT, V> consumer, final V value ) {
76
    assert consumer != null;
77
78
    mModifiers.add( instance -> consumer.accept( instance, value ) );
79
80
    return this;
81
  }
82
83
  /**
84
   * Instantiates then populates the immutable object to build.
85
   *
86
   * @return The newly built object.
87
   */
88
  public IT build() {
89
    final var value = mMutable.get();
90
91
    mModifiers.forEach( modifier -> modifier.accept( value ) );
92
    mModifiers.clear();
93
94
    return mImmutable.apply( value );
95
  }
96
}
197
A src/main/java/com/keenwrite/util/ProtocolScheme.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.io.File;
5
import java.net.MalformedURLException;
6
import java.net.URI;
7
import java.net.URL;
8
9
import static com.keenwrite.events.StatusEvent.clue;
10
11
/**
12
 * Represents the type of data encoding scheme used for a universal resource
13
 * indicator. Prefer to use the {@code is*} methods to check equality because
14
 * there are cases where the protocol represents more than one possible type
15
 * (e.g., a Java Archive is a file, so comparing {@link #FILE} directly could
16
 * lead to incorrect results).
17
 */
18
public enum ProtocolScheme {
19
  /**
20
   * Denotes a local file.
21
   */
22
  FILE( "file" ),
23
  /**
24
   * Denotes either HTTP or HTTPS.
25
   */
26
  HTTP( "http" ),
27
  /**
28
   * Denotes the File Transfer Protocol.
29
   */
30
  FTP( "ftp" ),
31
  /**
32
   * Denotes Java archive file.
33
   */
34
  JAR( "jar" ),
35
  /**
36
   * Could not determine scheme (or is not supported by the application).
37
   */
38
  UNKNOWN( "unknown" );
39
40
  private final String mPrefix;
41
42
  ProtocolScheme( final String prefix ) {
43
    mPrefix = prefix;
44
  }
45
46
  /**
47
   * Returns the protocol for a given URI or file name.
48
   *
49
   * @param uri Determine the protocol for this URI or file name.
50
   * @return The protocol for the given resource.
51
   */
52
  public static ProtocolScheme getProtocol( final String uri ) {
53
    try {
54
      return getProtocol( new URI( uri ) );
55
    } catch( final Exception ex ) {
56
      // Using double-slashes is a shorthand to instruct the browser to
57
      // reference a resource using the parent URL's security model. This
58
      // is known as a protocol-relative URL.
59
      return uri.startsWith( "//" ) ? HTTP : valueFrom( new File( uri ) );
60
    }
61
  }
62
63
  /**
64
   * Returns the protocol for a given URI or file name.
65
   *
66
   * @param uri Determine the protocol for this URI or file name.
67
   * @return The protocol for the given resource.
68
   */
69
  public static ProtocolScheme getProtocol( final URI uri )
70
    throws MalformedURLException {
71
    return uri.isAbsolute()
72
      ? valueFrom( uri )
73
      : valueFrom( uri.toURL() );
74
  }
75
76
  /**
77
   * Determines the protocol scheme for a given string.
78
   *
79
   * @param protocol A string representing data encoding protocol scheme.
80
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
81
   * valid value from this enumeration.
82
   */
83
  public static ProtocolScheme valueFrom( final String protocol ) {
84
    final var sanitized = protocol == null ? "" : protocol.toUpperCase();
85
86
    for( final var scheme : values() ) {
87
      // This will match HTTP/HTTPS as well as FILE*, which may be inaccurate.
88
      if( sanitized.startsWith( scheme.name() ) ) {
89
        return scheme;
90
      }
91
    }
92
93
    return UNKNOWN;
94
  }
95
96
  /**
97
   * Determines the protocol scheme for a given {@link File}.
98
   *
99
   * @param file A file having a URI that contains a protocol scheme.
100
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
101
   * valid value from this enumeration.
102
   */
103
  public static ProtocolScheme valueFrom( final File file ) {
104
    return valueFrom( file.toURI() );
105
  }
106
107
  /**
108
   * Determines the protocol scheme for a given {@link URI}.
109
   *
110
   * @param uri A URI that contains a protocol scheme.
111
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
112
   * valid value from this enumeration.
113
   */
114
  public static ProtocolScheme valueFrom( final URI uri ) {
115
    try {
116
      return valueFrom( uri.toURL() );
117
    } catch( final Exception ex ) {
118
      clue( ex );
119
      return UNKNOWN;
120
    }
121
  }
122
123
  /**
124
   * Determines the protocol scheme for a given {@link URL}.
125
   *
126
   * @param url The {@link URL} containing a protocol scheme.
127
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
128
   * valid value from this enumeration.
129
   */
130
  public static ProtocolScheme valueFrom( final URL url ) {
131
    return valueFrom( url.getProtocol() );
132
  }
133
134
  /**
135
   * Answers whether the given {@link URL} points to a remote resource.
136
   *
137
   * @param url The {@link URL} containing a protocol scheme.
138
   * @return {@link true} if the protocol must be fetched via HTTP or FTP.
139
   */
140
  @SuppressWarnings( "unused" )
141
  public static boolean isRemote( final URL url ) {
142
    return valueFrom( url ).isRemote();
143
  }
144
145
  /**
146
   * Answers {@code true} if the given protocol is for a local file, which
147
   * includes a JAR file.
148
   *
149
   * @return {@code false} the protocol is not a local file reference.
150
   */
151
  public boolean isFile() {
152
    return this == FILE || this == JAR;
153
  }
154
155
  /**
156
   * Answers whether the given protocol is HTTP or HTTPS.
157
   *
158
   * @return {@code true} the protocol is either HTTP or HTTPS.
159
   */
160
  public boolean isHttp() {
161
    return this == HTTP;
162
  }
163
164
  /**
165
   * Answers whether the given protocol is FTP.
166
   *
167
   * @return {@code true} the protocol is FTP.
168
   */
169
  public boolean isFtp() {
170
    return this == FTP;
171
  }
172
173
  /**
174
   * Answers whether the given protocol represents a remote resource.
175
   *
176
   * @return {@code true} the protocol is HTTP or FTP.
177
   */
178
  public boolean isRemote() {
179
    return isHttp() || isFtp();
180
  }
181
182
  /**
183
   * Answers {@code true} if the given protocol is for a Java archive file.
184
   *
185
   * @return {@code false} the protocol is not a Java archive file.
186
   */
187
  public boolean isJar() {
188
    return this == JAR;
189
  }
190
191
  /**
192
   * Prepends the protocol scheme to the given path, without a host name.
193
   *
194
   * @param path The path to decorate as a URI, including the scheme.
195
   * @return The
196
   */
197
  public String decorate( final String path ) {
198
    return getPrefix() + "://" + path;
199
  }
200
201
  private String getPrefix() {
202
    return mPrefix;
203
  }
204
}
1205
A src/main/java/com/keenwrite/util/RangeValidator.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.util;
6
7
import java.util.ArrayList;
8
import java.util.List;
9
import java.util.function.Predicate;
10
11
/**
12
 * Responsible for answering whether a given integer value falls within a
13
 * set of range specifiers. For example, if the range is "1-3, 5, 7-9, 11-",
14
 * then values of 0, 4, and 10 return {@code false} while values of 2, 5,
15
 * and 37 would return {@code true}.
16
 */
17
public final class RangeValidator implements Predicate<Integer> {
18
19
  /**
20
   * Container for a pair of integer values that can answer whether a given
21
   * value is included within the bounds provided by the pair.
22
   */
23
  private static class Range {
24
    private final int mLo;
25
    private final int mHi;
26
27
    private Range( final int lo, final int hi ) {
28
      assert lo <= hi;
29
30
      mLo = lo;
31
      mHi = hi;
32
    }
33
34
    private boolean includes( final int i ) {
35
      return mLo <= i && i <= mHi || mLo == -1 && mHi == -1;
36
    }
37
  }
38
39
  private final List<Range> mRanges = new ArrayList<>();
40
41
  /**
42
   * Creates an instance of {@link RangeValidator} that can verify whether
43
   * an integer value will fall within one of the numeric ranges in the
44
   * given listing.
45
   *
46
   * @param range The listing of ranges to validate against.
47
   */
48
  public RangeValidator( final String range ) {
49
    assert normalize( range ).equals( range );
50
51
    parse( range );
52
  }
53
54
  @Override
55
  public boolean test( final Integer integer ) {
56
    for( final var range : mRanges ) {
57
      if( range.includes( integer ) ) {
58
        return true;
59
      }
60
    }
61
62
    return false;
63
  }
64
65
  /**
66
   * Given a string meant to represent a comma-separated range of numbers,
67
   * this will ensure that the range meets the formatting requirements.
68
   *
69
   * @param range The sequences to validate (can be {@code null}).
70
   * @return The given range with all non-conforming characters removed, or
71
   * the empty string if {@code null} was provided.
72
   */
73
  public static String normalize( final String range ) {
74
    return range == null
75
      ? ""
76
      : range.matches( "^\\d+(-\\d+)?(?:,\\d+(?:-\\d+)?)*+$" )
77
      ? range
78
      : range.replaceAll( "[^-,\\d\\s]", "" );
79
  }
80
81
  /**
82
   * Populates the internal list of {@link Range} instances.
83
   *
84
   * @param s The string containing zero or more comma-separated integer
85
   *          ranges, themselves separated by hyphens.
86
   */
87
  private void parse( final String s ) {
88
    for( final var commaRange : normalize( s ).split( "," ) ) {
89
      final var hyphenRanges = commaRange.split( "-" );
90
      final Range range;
91
92
      if( hyphenRanges.length == 2 ) {
93
        final var hrlo = hyphenRanges[ 0 ].trim();
94
        final var hrhi = hyphenRanges[ 1 ].trim();
95
96
        if( hrlo.isEmpty() ) {
97
          range = new Range( 1, Integer.parseInt( hrhi ) );
98
        }
99
        else {
100
          final var lo = Integer.parseInt( hrlo );
101
          final var hi = Integer.parseInt( hrhi );
102
103
          range = new Range( lo, hi );
104
        }
105
      }
106
      else if( hyphenRanges.length == 1 ) {
107
        final var hri = hyphenRanges[ 0 ].trim();
108
109
        if( hri.isEmpty() ) {
110
          // Special case for all numbers being valid.
111
          range = new Range( -1, -1 );
112
        }
113
        else {
114
          final var i = Integer.parseInt( hyphenRanges[ 0 ].trim() );
115
          final var index = commaRange.trim().indexOf( '-' );
116
117
          // If the hyphen is to the left of the number, the range is bounded
118
          // from 0 to the number. Otherwise, the range is "unbounded" starting
119
          // at the number.
120
          if( index == -1 ) {
121
            range = new Range( i, i );
122
          }
123
          else if( index == 0 ) {
124
            range = new Range( 1, i );
125
          }
126
          else {
127
            range = new Range( i, Integer.MAX_VALUE );
128
          }
129
        }
130
      }
131
      else {
132
        // Ignore the range.
133
        range = new Range( 0, 0 );
134
      }
135
136
      mRanges.add( range );
137
    }
138
  }
139
}
1140
A src/main/java/com/keenwrite/util/ResourceWalker.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.io.IOException;
5
import java.net.URISyntaxException;
6
import java.nio.file.FileSystem;
7
import java.nio.file.Path;
8
import java.nio.file.Paths;
9
import java.util.function.Consumer;
10
11
import static com.keenwrite.util.ProtocolScheme.JAR;
12
import static com.keenwrite.util.ProtocolScheme.valueFrom;
13
import static java.nio.file.FileSystems.newFileSystem;
14
import static java.util.Collections.emptyMap;
15
16
/**
17
 * Responsible for finding file resources, regardless if they exist within
18
 * a Java Archive (.jar) file or on the native file system.
19
 *
20
 * @see FileWalker#walk(Path, String, Consumer)
21
 */
22
public final class ResourceWalker {
23
24
  /**
25
   * Walks the given directory hierarchy for files that match the given
26
   * globbing file name pattern.
27
   *
28
   * @param directory Root directory to scan for files matching the glob.
29
   * @param glob      Only files matching the pattern will be consumed.
30
   * @param c         Function to call for each matching path found.
31
   * @throws IOException        Could not walk the tree.
32
   * @throws URISyntaxException Could not convert the resource to a URI.
33
   */
34
  public static void walk(
35
    final String directory, final String glob, final Consumer<Path> c )
36
    throws URISyntaxException, IOException {
37
    final var resource = ResourceWalker.class.getResource( directory );
38
39
    if( resource != null ) {
40
      final var uri = resource.toURI();
41
      final Path path;
42
      FileSystem fs = null;
43
44
      if( valueFrom( uri ) == JAR ) {
45
        fs = newFileSystem( uri, emptyMap() );
46
        path = fs.getPath( directory );
47
      }
48
      else {
49
        path = Paths.get( uri );
50
      }
51
52
      try {
53
        FileWalker.walk( path, glob, c );
54
      } finally {
55
        if( fs != null ) { fs.close(); }
56
      }
57
    }
58
  }
59
}
160
A src/main/java/com/keenwrite/util/Strings.java
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one or more
3
 * contributor license agreements.  See the NOTICE file distributed with
4
 * this work for additional information regarding copyright ownership.
5
 * The ASF licenses this file to You under the Apache License, Version 2.0
6
 * (the "License"); you may not use this file except in compliance with
7
 * the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
package com.keenwrite.util;
18
19
import java.lang.reflect.Array;
20
import java.util.Arrays;
21
import java.util.HashSet;
22
import java.util.Set;
23
24
import static java.lang.Character.isWhitespace;
25
import static java.lang.String.format;
26
27
/**
28
 * Java doesn't allow adding behaviour to its {@link String} class, so these
29
 * functions have no alternative home. They are duplicated here to eliminate
30
 * the dependency on an Apache library. Extracting the methods that only
31
 * the application uses may have some small performance gains, as well,
32
 * because numerous if clauses have been removed and other code simplified.
33
 */
34
public class Strings {
35
  /**
36
   * The empty String {@code ""}.
37
   */
38
  private static final String EMPTY = "";
39
40
  /**
41
   * Abbreviates a String using ellipses. This will turn
42
   * "Now is the time for all good men" into "Now is the time for..."
43
   *
44
   * @param str   the String to check, may be {@code null}.
45
   * @param width maximum length of result String, must be at least 4.
46
   * @return abbreviated String, {@code null} if {@code null} String input.
47
   * @throws IllegalArgumentException if the width is too small.
48
   */
49
  public static String abbreviate( final String str, final int width ) {
50
    return abbreviate( str, "...", 0, width );
51
  }
52
53
  /**
54
   * Abbreviates a String using another given String as replacement marker.
55
   * This will turn"Now is the time for all good men" into "Now is the time
56
   * for..." if "..." was defined as the replacement marker.
57
   *
58
   * @param str        the String to check, may be {@code null}.
59
   * @param abbrMarker the String used as replacement marker.
60
   * @param width      maximum length of result String, must be at least
61
   *                   {@code abbrMarker.length + 1}.
62
   * @return abbreviated String, {@code null} if {@code null} String input.
63
   * @throws IllegalArgumentException if the width is too small.
64
   */
65
  public static String abbreviate(
66
    final String str,
67
    final String abbrMarker,
68
    final int width ) {
69
    return abbreviate( str, abbrMarker, 0, width );
70
  }
71
72
  /**
73
   * Abbreviates a String using a given replacement marker. This will turn
74
   * "Now is the time for all good men" into "...is the time for..." if "..."
75
   * was defined as the replacement marker.
76
   *
77
   * @param str        the String to check, may be {@code null}.
78
   * @param abbrMarker the String used as replacement marker.
79
   * @param offset     left edge of source String.
80
   * @param width      maximum length of result String, must be at least 4.
81
   * @return abbreviated String, {@code null} if {@code null} String input.
82
   * @throws IllegalArgumentException if the width is too small.
83
   */
84
  public static String abbreviate(
85
    final String str,
86
    final String abbrMarker,
87
    int offset,
88
    final int width ) {
89
    if( !isEmpty( str ) && EMPTY.equals( abbrMarker ) && width > 0 ) {
90
      return substring( str, width );
91
    }
92
93
    if( isAnyEmpty( str, abbrMarker ) ) {
94
      return str;
95
    }
96
97
    final int abbrMarkerLen = abbrMarker.length();
98
    final int minAbbrWidth = abbrMarkerLen + 1;
99
    final int minAbbrWidthOffset = abbrMarkerLen + abbrMarkerLen + 1;
100
101
    if( width < minAbbrWidth ) {
102
      final String msg = format( "Min abbreviation width: %d", minAbbrWidth );
103
      throw new IllegalArgumentException( msg );
104
    }
105
106
    final int strLen = str.length();
107
108
    if( strLen <= width ) {
109
      return str;
110
    }
111
112
    if( offset > strLen ) {
113
      offset = strLen;
114
    }
115
116
    if( strLen - offset < width - abbrMarkerLen ) {
117
      offset = strLen - (width - abbrMarkerLen);
118
    }
119
120
    if( offset <= abbrMarkerLen + 1 ) {
121
      return str.substring( 0, width - abbrMarkerLen ) + abbrMarker;
122
    }
123
124
    if( width < minAbbrWidthOffset ) {
125
      final String msg = format(
126
        "Min abbreviation width with offset: %d",
127
        minAbbrWidthOffset
128
      );
129
      throw new IllegalArgumentException( msg );
130
    }
131
132
    if( offset + width - abbrMarkerLen < strLen ) {
133
      return abbrMarker + abbreviate(
134
        str.substring( offset ),
135
        abbrMarker,
136
        width - abbrMarkerLen
137
      );
138
    }
139
140
    return abbrMarker + str.substring( strLen - (width - abbrMarkerLen) );
141
  }
142
143
  /**
144
   * Strips whitespace characters from the end of a String.
145
   *
146
   * <p>A {@code null} input String returns {@code null}.
147
   * An empty string ("") input returns the empty string.</p>
148
   *
149
   * @param str the String to remove characters from, may be {@code null}.
150
   * @return the stripped String, {@code null} if {@code null} input.
151
   */
152
  public static String trimEnd( final String str ) {
153
    int end = length( str );
154
155
    if( end == 0 ) {
156
      return str;
157
    }
158
159
    while( end != 0 && isWhitespace( str.charAt( end - 1 ) ) ) {
160
      end--;
161
    }
162
163
    return str.substring( 0, end );
164
  }
165
166
  /**
167
   * Strips whitespace characters from the start of a String.
168
   *
169
   * <p>A {@code null} input returns {@code null}.
170
   * An empty string ("") input returns the empty string.</p>
171
   *
172
   * @param str the String to remove characters from, may be {@code null}.
173
   * @return the stripped String, {@code null} if {@code null} input.
174
   */
175
  public static String trimStart( final String str ) {
176
    final int strLen = length( str );
177
178
    if( strLen == 0 ) {
179
      return str;
180
    }
181
182
    int start = 0;
183
184
    while( start != strLen && isWhitespace( str.charAt( start ) ) ) {
185
      start++;
186
    }
187
188
    return str.substring( start );
189
  }
190
191
  /**
192
   * Replaces all occurrences of Strings within another String.
193
   *
194
   * @param text            the haystack, no-op if {@code null}.
195
   * @param searchList      the needles, no-op if {@code null}.
196
   * @param replacementList the new needles, no-op if {@code null}.
197
   * @return the text with any replacements processed, {@code null} if
198
   * {@code null} String input.
199
   * @throws IllegalArgumentException if the lengths of the arrays are not
200
   *                                  the same ({@code null}  is ok, and/or
201
   *                                  size 0).
202
   */
203
  public static String replaceEach( final String text,
204
                                    final String[] searchList,
205
                                    final String[] replacementList ) {
206
    return replaceEach( text, searchList, replacementList, 0 );
207
  }
208
209
  /**
210
   * Replace all occurrences of Strings within another String.
211
   *
212
   * @param text            the haystack, no-op if {@code null}.
213
   * @param searchList      the needles, no-op if {@code null}.
214
   * @param replacementList the new needles, no-op if {@code null}.
215
   * @param timeToLive      if less than 0 then there is a circular reference
216
   *                        and endless loop
217
   * @return the text with any replacements processed, {@code null} if
218
   * {@code null} String input.
219
   * @throws IllegalStateException    if the search is repeating and there is
220
   *                                  an endless loop due to outputs of one
221
   *                                  being inputs to another
222
   * @throws IllegalArgumentException if the lengths of the arrays are not
223
   *                                  the same ({@code null} is ok, and/or
224
   *                                  size 0)
225
   */
226
  private static String replaceEach(
227
    final String text,
228
    final String[] searchList,
229
    final String[] replacementList,
230
    final int timeToLive
231
  ) {
232
    // If in a recursive call, this shouldn't be less than zero.
233
    if( timeToLive < 0 ) {
234
      final Set<String> searchSet =
235
        new HashSet<>( Arrays.asList( searchList ) );
236
      final Set<String> replacementSet = new HashSet<>( Arrays.asList(
237
        replacementList ) );
238
      searchSet.retainAll( replacementSet );
239
      if( !searchSet.isEmpty() ) {
240
        throw new IllegalStateException(
241
          "Aborting to protect against StackOverflowError - " +
242
          "output of one loop is the input of another" );
243
      }
244
    }
245
246
    if( isEmpty( text ) ||
247
        isEmpty( searchList ) ||
248
        isEmpty( replacementList ) ||
249
        isNotEmpty( searchList ) &&
250
        timeToLive == -1 ) {
251
      return text;
252
    }
253
254
    final int searchLength = searchList.length;
255
    final int replacementLength = replacementList.length;
256
257
    // make sure lengths are ok, these need to be equal
258
    if( searchLength != replacementLength ) {
259
      final String msg = format(
260
        "Search and Replace array lengths don't match: %d vs %d",
261
        searchLength,
262
        replacementLength
263
      );
264
      throw new IllegalArgumentException( msg );
265
    }
266
267
    // keep track of which still have matches
268
    final boolean[] noMoreMatchesForReplIndex = new boolean[ searchLength ];
269
270
    // index on index that the match was found
271
    int textIndex = -1;
272
    int replaceIndex = -1;
273
    int tempIndex;
274
275
    // index of replace array that will replace the search string found
276
    // NOTE: logic duplicated below START
277
    for( int i = 0; i < searchLength; i++ ) {
278
      if( noMoreMatchesForReplIndex[ i ] || isEmpty( searchList[ i ] ) || replacementList[ i ] == null ) {
279
        continue;
280
      }
281
      tempIndex = text.indexOf( searchList[ i ] );
282
283
      // see if we need to keep searching for this
284
      if( tempIndex == -1 ) {
285
        noMoreMatchesForReplIndex[ i ] = true;
286
      }
287
      else if( textIndex == -1 || tempIndex < textIndex ) {
288
        textIndex = tempIndex;
289
        replaceIndex = i;
290
      }
291
    }
292
    // NOTE: logic mostly below END
293
294
    // no search strings found, we are done
295
    if( textIndex == -1 ) {
296
      return text;
297
    }
298
299
    int start = 0;
300
301
    // Guess the result buffer size, to prevent doubling capacity.
302
    final StringBuilder buf = createStringBuilder(
303
      text, searchList, replacementList
304
    );
305
306
    while( textIndex != -1 ) {
307
      for( int i = start; i < textIndex; i++ ) {
308
        buf.append( text.charAt( i ) );
309
      }
310
311
      buf.append( replacementList[ replaceIndex ] );
312
313
      start = textIndex + searchList[ replaceIndex ].length();
314
315
      textIndex = -1;
316
      replaceIndex = -1;
317
318
      // find the next earliest match
319
      // NOTE: logic mostly duplicated above START
320
      for( int i = 0; i < searchLength; i++ ) {
321
        if( noMoreMatchesForReplIndex[ i ] || isEmpty( searchList[ i ] ) || replacementList[ i ] == null ) {
322
          continue;
323
        }
324
        tempIndex = text.indexOf( searchList[ i ], start );
325
326
        // see if we need to keep searching for this
327
        if( tempIndex == -1 ) {
328
          noMoreMatchesForReplIndex[ i ] = true;
329
        }
330
        else if( textIndex == -1 || tempIndex < textIndex ) {
331
          textIndex = tempIndex;
332
          replaceIndex = i;
333
        }
334
      }
335
336
      // NOTE: logic duplicated above END
337
    }
338
339
    final int textLength = text.length();
340
    for( int i = start; i < textLength; i++ ) {
341
      buf.append( text.charAt( i ) );
342
    }
343
344
    return replaceEach(
345
      buf.toString(),
346
      searchList,
347
      replacementList,
348
      timeToLive - 1
349
    );
350
  }
351
352
  private static StringBuilder createStringBuilder(
353
    final String text,
354
    final String[] searchList,
355
    final String[] replacementList ) {
356
    int increase = 0;
357
358
    // count the replacement text elements that are larger than their
359
    // corresponding text being replaced
360
    for( int i = 0; i < searchList.length; i++ ) {
361
      if( searchList[ i ] == null || replacementList[ i ] == null ) {
362
        continue;
363
      }
364
      final int greater =
365
        replacementList[ i ].length() - searchList[ i ].length();
366
      if( greater > 0 ) {
367
        increase += 3 * greater; // assume 3 matches
368
      }
369
    }
370
371
    // have upper-bound at 20% increase, then let Java take over
372
    increase = Math.min( increase, text.length() / 5 );
373
374
    return new StringBuilder( text.length() + increase );
375
  }
376
377
  /**
378
   * Gets a {@link CharSequence} length or {@code 0} if the
379
   * {@link CharSequence} is {@code null}.
380
   *
381
   * @param cs a {@link CharSequence} or {@code null}.
382
   * @return {@link CharSequence} length or {@code 0} if the
383
   * {@link CharSequence} is {@code null}.
384
   */
385
  private static int length( final CharSequence cs ) {
386
    return cs == null ? 0 : cs.length();
387
  }
388
389
  /**
390
   * Checks if a {@link CharSequence} is empty ("") or {@code null}.
391
   *
392
   * @param cs the {@link CharSequence} to check, may be {@code null}.
393
   * @return {@code true} if the {@link CharSequence} is empty or {@code null}.
394
   */
395
  public static boolean isEmpty( final CharSequence cs ) {
396
    return cs == null || cs.isEmpty();
397
  }
398
399
  private static boolean isEmpty( final Object[] array ) {
400
    return array == null || Array.getLength( array ) == 0;
401
  }
402
403
  private static boolean isNotEmpty( final Object[] array ) {
404
    return array != null && Array.getLength( array ) > 0;
405
  }
406
407
  private static boolean isAnyEmpty( final CharSequence... css ) {
408
    if( isNotEmpty( css ) ) {
409
      for( final CharSequence cs : css ) {
410
        if( isEmpty( cs ) ) {
411
          return true;
412
        }
413
      }
414
    }
415
416
    return false;
417
  }
418
419
  /**
420
   * Gets a substring from the specified String avoiding exceptions.
421
   *
422
   * <p>A negative start position can be used to start/end {@code n}
423
   * characters from the end of the String.</p>
424
   *
425
   * <p>The returned substring starts with the character in the {@code start}
426
   * position and ends before the {@code end} position. All position counting
427
   * is zero-based -- i.e., to start at the beginning of the string use
428
   * {@code start = 0}. Negative start and end positions can be used to
429
   * specify offsets relative to the end of the String.</p>
430
   *
431
   * <p>If {@code start} is not strictly to the left of {@code end}, ""
432
   * is returned.</p>
433
   *
434
   * @param str the String to get the substring from, may be {@code null}.
435
   * @param end the position to end at (exclusive), negative means
436
   *            count back from the end of the String by this many characters
437
   * @return substring from start position to end position, {@code null} if
438
   * {@code null} String input
439
   */
440
  private static String substring( final String str, int end ) {
441
    if( str == null ) {
442
      return null;
443
    }
444
445
    final int len = str.length();
446
447
    if( end < 0 ) {
448
      end = len + end;
449
    }
450
451
    if( end > len ) {
452
      end = len;
453
    }
454
455
    final int start = 0;
456
457
    if( start > end ) {
458
      return EMPTY;
459
    }
460
461
    return str.substring( start, end );
462
  }
463
464
  public static boolean validate( final String s ) {
465
    assert s != null;
466
    assert !s.isBlank();
467
468
    return true;
469
  }
470
471
  public static String sanitize( final String s ) {
472
    return s == null ? "" : s;
473
  }
474
}
1475
A src/main/java/com/keenwrite/util/SystemUtils.java
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one or more
3
 * contributor license agreements.  See the NOTICE file distributed with
4
 * this work for additional information regarding copyright ownership.
5
 * The ASF licenses this file to You under the Apache License, Version 2.0
6
 * (the "License"); you may not use this file except in compliance with
7
 * the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
package com.keenwrite.util;
18
19
import java.util.Properties;
20
21
import static com.keenwrite.util.Strings.isEmpty;
22
23
/**
24
 * Helpers for {@code java.lang.System}.
25
 */
26
public class SystemUtils {
27
28
  // System property constants
29
  // -----------------------------------------------------------------------
30
  // These MUST be declared first. Other constants depend on this.
31
32
  /**
33
   * The System property name {@value}.
34
   */
35
  public static final String PROPERTY_OS_NAME = "os.name";
36
37
  /**
38
   * Gets the current value from the system properties map.
39
   * <p>
40
   * Returns {@code null} if the property cannot be read due to a
41
   * {@link SecurityException}.
42
   * </p>
43
   *
44
   * @return the current value from the system properties map.
45
   */
46
  @SuppressWarnings( "ConstantValue" )
47
  private static String getOsName() {
48
    assert PROPERTY_OS_NAME != null;
49
    assert !PROPERTY_OS_NAME.isBlank();
50
51
    try {
52
      final String value = System.getProperty( PROPERTY_OS_NAME );
53
54
      return isEmpty( value ) ? "" : value;
55
    } catch( final SecurityException ignore ) {}
56
57
    return "";
58
  }
59
60
  /**
61
   * The Operating System name, derived from Java's system properties.
62
   *
63
   * <p>
64
   * Defaults to empty if the runtime does not have security access to
65
   * read this property or the property does not exist.
66
   * </p>
67
   * <p>
68
   * This value is initialized when the class is loaded. If
69
   * {@link System#setProperty(String, String)} or
70
   * {@link System#setProperties(Properties)} is called after this
71
   * class is loaded, the value will be out of sync with that System property.
72
   * </p>
73
   */
74
  public static final String OS_NAME = getOsName();
75
76
  /**
77
   * Is {@code true} if this is AIX.
78
   *
79
   * <p>
80
   * The field will return {@code false} if {@code OS_NAME} is {@code null}.
81
   * </p>
82
   */
83
  public static final boolean IS_OS_AIX = osNameMatches( "AIX" );
84
85
  /**
86
   * Is {@code true} if this is HP-UX.
87
   *
88
   * <p>
89
   * The field will return {@code false} if {@code OS_NAME} is {@code null}.
90
   * </p>
91
   */
92
  public static final boolean IS_OS_HP_UX = osNameMatches( "HP-UX" );
93
94
  /**
95
   * Is {@code true} if this is Irix.
96
   *
97
   * <p>
98
   * The field will return {@code false} if {@code OS_NAME} is {@code null}.
99
   * </p>
100
   */
101
  public static final boolean IS_OS_IRIX = osNameMatches( "Irix" );
102
103
  /**
104
   * Is {@code true} if this is Linux.
105
   *
106
   * <p>
107
   * The field will return {@code false} if {@code OS_NAME} is {@code null}.
108
   * </p>
109
   */
110
  public static final boolean IS_OS_LINUX =
111
    osNameMatches( "Linux" ) ||
112
    osNameMatches( "LINUX" );
113
114
  /**
115
   * Is {@code true} if this is Mac.
116
   *
117
   * <p>
118
   * The field will return {@code false} if {@code OS_NAME} is {@code null}.
119
   * </p>
120
   */
121
  public static final boolean IS_OS_MAC = osNameMatches( "Mac" );
122
123
  /**
124
   * Is {@code true} if this is Mac.
125
   *
126
   * <p>
127
   * The field will return {@code false} if {@code OS_NAME} is {@code null}.
128
   * </p>
129
   */
130
  public static final boolean IS_OS_MAC_OSX = osNameMatches( "Mac OS X" );
131
132
  /**
133
   * Is {@code true} if this is FreeBSD.
134
   *
135
   * <p>
136
   * The field will return {@code false} if {@code OS_NAME} is {@code null}.
137
   * </p>
138
   */
139
  public static final boolean IS_OS_FREE_BSD = osNameMatches( "FreeBSD" );
140
141
  /**
142
   * Is {@code true} if this is OpenBSD.
143
   *
144
   * <p>
145
   * The field will return {@code false} if {@code OS_NAME} is {@code null}.
146
   * </p>
147
   */
148
  public static final boolean IS_OS_OPEN_BSD = osNameMatches( "OpenBSD" );
149
150
  /**
151
   * Is {@code true} if this is NetBSD.
152
   *
153
   * <p>
154
   * The field will return {@code false} if {@code OS_NAME} is {@code null}.
155
   * </p>
156
   */
157
  public static final boolean IS_OS_NET_BSD = osNameMatches( "NetBSD" );
158
159
  /**
160
   * Is {@code true} if this is Solaris.
161
   *
162
   * <p>
163
   * The field will return {@code false} if {@code OS_NAME} is {@code null}.
164
   * </p>
165
   */
166
  public static final boolean IS_OS_SOLARIS = osNameMatches( "Solaris" );
167
168
  /**
169
   * Is {@code true} if this is SunOS.
170
   *
171
   * <p>
172
   * The field will return {@code false} if {@code OS_NAME} is {@code null}.
173
   * </p>
174
   */
175
  public static final boolean IS_OS_SUN_OS = osNameMatches( "SunOS" );
176
177
  /**
178
   * Is {@code true} if this is a UNIX like system, as in any of AIX, HP-UX,
179
   * Irix, Linux, MacOSX, Solaris or SUN OS.
180
   *
181
   * <p>
182
   * The field will return {@code false} if {@code OS_NAME} is {@code null}.
183
   * </p>
184
   */
185
  public static final boolean IS_OS_UNIX =
186
    IS_OS_AIX ||
187
    IS_OS_HP_UX ||
188
    IS_OS_IRIX ||
189
    IS_OS_LINUX ||
190
    IS_OS_MAC_OSX ||
191
    IS_OS_SOLARIS ||
192
    IS_OS_SUN_OS ||
193
    IS_OS_FREE_BSD ||
194
    IS_OS_OPEN_BSD ||
195
    IS_OS_NET_BSD;
196
197
  /**
198
   * The prefix String for all Windows OS.
199
   */
200
  private static final String OS_NAME_WINDOWS_PREFIX = "Windows";
201
202
  /**
203
   * Is {@code true} if this is Windows.
204
   *
205
   * <p>
206
   * The field will return {@code false} if {@code OS_NAME} is {@code null}.
207
   * </p>
208
   */
209
  public static final boolean IS_OS_WINDOWS =
210
    osNameMatches( OS_NAME_WINDOWS_PREFIX );
211
212
  /**
213
   * Decides if the operating system matches.
214
   * <p>
215
   * This method is package private instead of private to support unit test
216
   * invocation.
217
   * </p>
218
   *
219
   * @param prefix the prefix for the expected OS name
220
   * @return true if matches, or false if not or can't determine
221
   */
222
  private static boolean osNameMatches( final String prefix ) {
223
    return OS_NAME.startsWith( prefix );
224
  }
225
}
1226
A src/main/java/com/keenwrite/util/Time.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.time.Duration;
5
6
import static java.lang.String.format;
7
import static java.util.concurrent.TimeUnit.*;
8
9
/**
10
 * Responsible for time-related functionality.
11
 */
12
public final class Time {
13
  /**
14
   * Converts an elapsed time to a human-readable format (hours, minutes,
15
   * seconds, and milliseconds).
16
   *
17
   * @param duration An elapsed time.
18
   * @return Human-readable elapsed time.
19
   */
20
  public static String toElapsedTime( final Duration duration ) {
21
    final var elapsed = duration.toMillis();
22
    final var hours = MILLISECONDS.toHours( elapsed );
23
    final var eHours = elapsed - HOURS.toMillis( hours );
24
    final var minutes = MILLISECONDS.toMinutes( eHours );
25
    final var eMinutes = eHours - MINUTES.toMillis( minutes );
26
    final var seconds = MILLISECONDS.toSeconds( eMinutes );
27
    final var eSeconds = eMinutes - SECONDS.toMillis( seconds );
28
    final var milliseconds = MILLISECONDS.toMillis( eSeconds );
29
30
    return format( "%02d:%02d:%02d.%03d",
31
                   hours, minutes, seconds, milliseconds );
32
  }
33
}
134
A src/main/module-info.txt
1
module keenwrite.main {
2
  requires java.desktop;
3
  requires java.prefs;
4
  requires java.scripting;
5
  requires java.xml;
6
  requires javafx.graphics;
7
  requires javafx.controls;
8
  requires javafx.swing;
9
10
  requires annotations;
11
12
  requires echosvg.anim;
13
  requires echosvg.bridge;
14
  requires echosvg.css;
15
  requires echosvg.gvt;
16
  requires echosvg.transcoder;
17
  requires echosvg.util;
18
19
  requires com.dlsc.formsfx;
20
  requires transitive com.dlsc.preferencesfx;
21
  requires com.fasterxml.jackson.databind;
22
  requires transitive com.fasterxml.jackson.dataformat.yaml;
23
24
  requires flexmark;
25
  requires flexmark.util.data;
26
  requires flexmark.util.sequence;
27
28
  requires keenquotes;
29
  requires keentex;
30
  requires tokenize;
31
32
  requires org.apache.commons.lang3;
33
  requires org.jsoup;
34
  requires org.controlsfx.controls;
35
  requires org.fxmisc.flowless;
36
  requires org.fxmisc.richtext;
37
  requires org.fxmisc.undo;
38
39
  requires commons.io;
40
  requires eventbus.java;
41
  requires flying.saucer.core;
42
  requires info.picocli;
43
  requires jsymspell;
44
  requires tiwulfx.dock;
45
  requires wellbehavedfx;
46
  requires xml.apis.ext;
47
  requires java.logging;
48
}
149
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.events.Notifier
1
1
com.keenwrite.service.events.impl.DefaultNotifier
A src/main/resources/bootstrap.properties
1
application.title=KeenWrite
12
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/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: .25em;
7
}
8
9
/* Editor background color */
10
.styled-text-area {
11
  -fx-background-color: -fx-control-inner-background;
12
}
13
14
/* Text foreground colour */
15
.styled-text-area .text {
16
  -fx-fill: -fx-text-foreground;
17
}
18
19
/* Subtly highlight the current paragraph. */
20
.markdown .paragraph-box:has-caret {
21
  -fx-background-color: -fx-text-background;
22
}
23
24
/* Light colour for selection highlight. */
25
.markdown .selection {
26
  -fx-fill: -fx-text-selection;
27
}
28
29
/* Decoration for words not found in the lexicon. */
30
.markdown .spelling {
31
  -rtfx-underline-color: rgba( 255, 131, 67, .7 );
32
  -rtfx-underline-dash-array: 4, 2;
33
  -rtfx-underline-width: 2;
34
  -rtfx-underline-cap: round;
35
}
36
37
.markdown .search {
38
  -rtfx-background-color: #ffe959;
39
}
140
A src/main/resources/com/keenwrite/editor/markdown_de-Latn-AT.css
11
A src/main/resources/com/keenwrite/editor/markdown_de-Latn-CH.css
11
A src/main/resources/com/keenwrite/editor/markdown_de-Latn-DE.css
11
A src/main/resources/com/keenwrite/editor/markdown_de-Latn-LU.css
11
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_es-Latn-AR.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-BO.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-CL.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-CO.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-CR.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-DO.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-EC.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-ES.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-GT.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-HN.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-MX.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-NI.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-PA.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-PE.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-PR.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-PY.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-SV.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-US.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-UY.css
11
A src/main/resources/com/keenwrite/editor/markdown_es-Latn-VE.css
11
A src/main/resources/com/keenwrite/editor/markdown_fr-Latn-BE.css
11
A src/main/resources/com/keenwrite/editor/markdown_fr-Latn-CA.css
11
A src/main/resources/com/keenwrite/editor/markdown_fr-Latn-CH.css
11
A src/main/resources/com/keenwrite/editor/markdown_fr-Latn-FR.css
11
A src/main/resources/com/keenwrite/editor/markdown_fr-Latn-LU.css
11
A src/main/resources/com/keenwrite/editor/markdown_it-Latn-CH.css
11
A src/main/resources/com/keenwrite/editor/markdown_it-Latn-IT.css
11
A src/main/resources/com/keenwrite/editor/markdown_iw-Hebr-IL.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
# Workspace preferences
9
# ########################################################################
10
11
workspace.document=Document
12
13
workspace.document.meta=Document Metadata
14
workspace.document.meta.desc=Keys must be alphabetic, values may use variables (e.g., '{{'document.title'}}').
15
workspace.document.meta.title=Pairs
16
17
workspace.editor=Editor
18
workspace.editor.autosave=Autosave
19
workspace.editor.autosave.desc=Amount of time to wait between saves, in seconds (0 means disabled).
20
workspace.editor.autosave.title=Timeout
21
22
workspace.typeset=Typesetting
23
workspace.typeset.context=ConTeXt
24
workspace.typeset.context.themes.path=Paths
25
workspace.typeset.context.themes.path.desc=Directory containing theme subdirectories.
26
workspace.typeset.context.themes.path.title=Themes
27
workspace.typeset.context.clean=Clean
28
workspace.typeset.context.clean.desc=Delete ancillary files after an unsuccessful export.
29
workspace.typeset.context.clean.title=Purge
30
workspace.typeset.context.fonts=Fonts
31
workspace.typeset.context.fonts.dir=Directory
32
workspace.typeset.context.fonts.dir.desc=Directory containing additional font files (OTF and TTF).
33
workspace.typeset.context.fonts.dir.title=Path
34
workspace.typeset.typography=Typography
35
workspace.typeset.typography.quotes=Quotation Marks
36
workspace.typeset.typography.quotes.desc=Defines encoding for apostrophes when curling quotation marks (regular means none; apos is typical).
37
workspace.typeset.typography.quotes.title=Curl
38
workspace.typeset.modes=Modes
39
workspace.typeset.modes.enabled=Enabled
40
workspace.typeset.modes.enabled.desc=Enable typesetting modes, separated by commas; values may use variables (e.g., '{{'document.category'}}').
41
workspace.typeset.modes.enabled.title=Enable
42
43
workspace.r=R
44
workspace.r.script=Startup Script
45
workspace.r.script.desc=Script runs prior to executing R statements within the document.
46
workspace.r.dir=Working Directory
47
workspace.r.dir.desc=Value assigned to v$application$r$working$directory and usable in the startup script.
48
workspace.r.dir.title=Directory
49
workspace.r.delimiter.began=Delimiter Prefix
50
workspace.r.delimiter.began.desc=Prefix of expression that wraps inserted variables.
51
workspace.r.delimiter.began.title=Opening
52
workspace.r.delimiter.ended=Delimiter Suffix
53
workspace.r.delimiter.ended.desc=Suffix of expression that wraps inserted variables.
54
workspace.r.delimiter.ended.title=Closing
55
56
workspace.images=Images
57
workspace.images.dir=Absolute Directory
58
workspace.images.dir.desc=Path to search for local file system images.
59
workspace.images.dir.title=Directory
60
workspace.images.cache.desc=Path to store remotely retrieved images.
61
workspace.images.cache.title=Directory
62
workspace.images.order=Extensions
63
workspace.images.order.desc=Preferred order of image file types to embed, separated by spaces.
64
workspace.images.order.title=Extensions
65
workspace.images.resize=Resize
66
workspace.images.resize.desc=Scale images to fit the preview panel when resizing, automatically.
67
workspace.images.resize.title=Resize
68
workspace.images.server=Diagram Server
69
workspace.images.server.desc=Server used to generate diagrams (e.g., kroki.io).
70
workspace.images.server.title=Name
71
72
workspace.definition=Variable
73
workspace.definition.path=File name
74
workspace.definition.path.desc=Absolute path to interpolated string variables.
75
workspace.definition.path.title=Path
76
workspace.definition.delimiter.began=Delimiter Prefix
77
workspace.definition.delimiter.began.desc=Indicates when a variable name is starting.
78
workspace.definition.delimiter.began.title=Opening
79
workspace.definition.delimiter.ended=Delimiter Suffix
80
workspace.definition.delimiter.ended.desc=Indicates when a variable name is ending.
81
workspace.definition.delimiter.ended.title=Closing
82
83
workspace.ui.skin=Skins
84
workspace.ui.skin.selection=Bundled
85
workspace.ui.skin.selection.desc=Pre-packaged application style (default: Modena Light).
86
workspace.ui.skin.selection.title=Name
87
workspace.ui.skin.custom=Custom
88
workspace.ui.skin.custom.desc=User-defined JavaFX cascading stylesheet file.
89
workspace.ui.skin.custom.title=Path
90
91
workspace.ui.preview=Preview
92
workspace.ui.preview.stylesheet=Stylesheet
93
workspace.ui.preview.stylesheet.desc=User-defined HTML cascading stylesheet file.
94
workspace.ui.preview.stylesheet.title=Path
95
96
workspace.ui.font=Fonts
97
workspace.ui.font.editor=Editor Font
98
workspace.ui.font.editor.name=Name
99
workspace.ui.font.editor.name.desc=Text editor font name (sans-serif font recommended).
100
workspace.ui.font.editor.name.title=Family
101
workspace.ui.font.editor.size=Size
102
workspace.ui.font.editor.size.desc=Font size.
103
workspace.ui.font.editor.size.title=Points
104
workspace.ui.font.preview=Preview Font
105
workspace.ui.font.preview.name=Name
106
workspace.ui.font.preview.name.desc=Preview pane font name (must support ligatures, serif font recommended).
107
workspace.ui.font.preview.name.title=Family
108
workspace.ui.font.preview.size=Size
109
workspace.ui.font.preview.size.desc=Font size.
110
workspace.ui.font.preview.size.title=Points
111
workspace.ui.font.preview.mono.name=Name
112
workspace.ui.font.preview.mono.name.desc=Monospace font name.
113
workspace.ui.font.preview.mono.name.title=Family
114
workspace.ui.font.preview.mono.size=Size
115
workspace.ui.font.preview.mono.size.desc=Monospace font size.
116
workspace.ui.font.preview.mono.size.title=Points
117
workspace.ui.font.math=Math Font
118
workspace.ui.font.math.size.title=Scale
119
120
workspace.language=Language
121
workspace.language.locale=Internationalization
122
workspace.language.locale.desc=Language for application and HTML export.
123
workspace.language.locale.title=Locale
124
125
# ########################################################################
126
# Editor actions
127
# ########################################################################
128
129
Editor.spelling.check.matches.none=No suggestions for ''{0}'' found.
130
Editor.spelling.check.matches.okay=The spelling for ''{0}'' appears to be correct.
131
132
# ########################################################################
133
# Menu Bar
134
# ########################################################################
135
136
Main.menu.file=_File
137
Main.menu.edit=_Edit
138
Main.menu.insert=_Insert
139
Main.menu.format=Forma_t
140
Main.menu.definition=_Variable
141
Main.menu.view=Vie_w
142
Main.menu.help=_Help
143
144
# ########################################################################
145
# Detachable Tabs
146
# ########################################################################
147
148
# {0} is the application title; {1} is a unique window ID.
149
Detach.tab.title={0} - {1}
150
151
# ########################################################################
152
# Status Bar
153
# ########################################################################
154
155
Main.status.text.offset=offset
156
Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2}
157
Main.status.state.default=OK
158
Main.status.export.success=Saved as ''{0}''
159
160
Main.status.error.bootstrap.eval=Note: Bootstrap variable of ''{0}'' not found
161
Main.status.error.bootstrap.cache=Could not create cache directory ''{0}''
162
163
Main.status.error.parse=Evaluation error: {0}
164
Main.status.error.def.blank=Move the caret to a word before inserting a variable
165
Main.status.error.def.empty=Create a variable before inserting one
166
Main.status.error.def.missing=No variable value found for ''{0}''
167
Main.status.error.r=Error with [{0}...]: {1}
168
169
Main.status.error.file.missing=Not found: ''{0}''
170
Main.status.error.file.missing.near=Not found: ''{0}'' near line {1}
171
Main.status.error.file.delete=Failed to delete ''{0}''
172
173
Main.status.error.messages.recursion=Lookup depth exceeded, check for loops in ''{0}''
174
Main.status.error.messages.syntax=Missing ''}'' in ''{0}''
175
176
Main.status.error.undo=Cannot undo; beginning of undo history reached
177
Main.status.error.redo=Cannot redo; end of redo history reached
178
179
Main.status.error.theme.missing=Install themes before exporting (no themes found at ''{0}'')
180
Main.status.error.theme.name=Cannot find theme name for ''{0}''
181
182
Main.status.image.request.init=Initializing HTTP request
183
Main.status.image.request.fetch=Downloaded image ''{0}''
184
Main.status.image.request.success=Determined content type ''{0}''
185
Main.status.image.request.resolve=Resolved image path: ''{0}''
186
Main.status.image.request.error.media=No media type for ''{0}''
187
Main.status.image.request.error.cert=Could not accept certificate for ''{0}''
188
Main.status.image.request.error.create=Could not create image for preview document
189
Main.status.image.request.error.resolve=Could not resolve image path: ''{0}''
190
191
Main.status.image.xhtml.image.download=Downloading ''{0}''
192
Main.status.image.xhtml.image.resolve=Qualify path for ''{0}''
193
Main.status.image.xhtml.image.found=Found image ''{0}''
194
Main.status.image.xhtml.image.missing=Missing image ''{0}''
195
Main.status.image.xhtml.image.saved=Saved image ''{0}''
196
Main.status.image.xhtml.image.failed=Cannot save image ''{0}''
197
198
Main.status.url.request.fetch=Download Markdown file from: ''{0}''
199
Main.status.url.request.success=Downloaded Markdown file ''{0}''
200
Main.status.url.request.failure=Could not save Markdown file to: ''{0}''
201
Main.status.url.request.exists=Download aborted; file exists: ''{0}''
202
# suppress inspection "UnusedMessageFormatParameter"
203
Main.status.url.request.status.bytes=Downloaded {1} bytes (size unknown).
204
Main.status.url.request.status.progress=Downloaded {0} % of {1} bytes.
205
206
Main.status.font.search.missing=No font name starting with ''{0}'' was found
207
208
Main.status.export.concat=Concatenating ''{0}''
209
Main.status.export.concat.parent=No parent directory found for ''{0}''
210
Main.status.export.concat.extension=File name must have an extension ''{0}''
211
Main.status.export.concat.io=Could not read from ''{0}''
212
213
Main.status.typeset.create=Creating typesetter
214
Main.status.typeset.xhtml=Export document as XHTML
215
Main.status.typeset.began=Started typesetting ''{0}''
216
Main.status.typeset.failed=Could not generate PDF file
217
Main.status.typeset.page=Typesetting page {0} of {1} (pass {2})
218
Main.status.typeset.ended.success=Finished typesetting ''{0}'' ({1} elapsed)
219
Main.status.typeset.ended.failure=Failed to typeset ''{0}'' ({1} elapsed)
220
Main.status.typeset.setting=Set {0} to ''{1}''
221
222
Main.status.lexicon.loading=Loading lexicon: {0} words
223
Main.status.lexicon.loaded=Loaded lexicon: {0} words
224
225
# ########################################################################
226
# Search Bar
227
# ########################################################################
228
229
Main.search.stop.tooltip=Close search bar
230
Main.search.stop.icon=CLOSE
231
Main.search.next.tooltip=Find next match
232
Main.search.next.icon=CHEVRON_DOWN
233
Main.search.prev.tooltip=Find previous match
234
Main.search.prev.icon=CHEVRON_UP
235
Main.search.find.tooltip=Search document for text
236
Main.search.find.icon=SEARCH
237
Main.search.match.none=No matches
238
Main.search.match.some={0} of {1} matches
239
240
# ########################################################################
241
# Definition Pane and its Tree View
242
# ########################################################################
243
244
Definition.menu.add.default=Undefined
245
246
# ########################################################################
247
# Variable Definitions Pane
248
# ########################################################################
249
250
Pane.definition.node.root.title=Variables
251
252
# ########################################################################
253
# HTML Preview Pane
254
# ########################################################################
255
256
Pane.preview.title=Preview
257
258
# ########################################################################
259
# Document Outline Pane
260
# ########################################################################
261
262
Pane.outline.title=Outline
263
264
# ########################################################################
265
# File Manager Pane
266
# ########################################################################
267
268
Pane.files.title=Files
269
270
# ########################################################################
271
# Document Outline Pane
272
# ########################################################################
273
274
Pane.statistics.title=Statistics
275
276
# ########################################################################
277
# Failure messages with respect to YAML files.
278
# ########################################################################
279
280
yaml.error.open=Could not open YAML file (ensure non-empty file).
281
yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''.
282
yaml.error.missing=Empty variable value for key ''{0}''.
283
yaml.error.tree.form=Unassigned variable near ''{0}''.
284
285
# ########################################################################
286
# Text Resource
287
# ########################################################################
288
289
TextResource.load.error.unsaved=The file ''{0}'' is unsaved or does not exist.
290
TextResource.load.error.permissions=The file ''{0}'' must be readable and writable.
291
292
TextResource.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
293
TextResource.saveFailed.title=Save
294
295
# ########################################################################
296
# File Open
297
# ########################################################################
298
299
Dialog.file.choose.open.title=Open File
300
Dialog.file.choose.save.title=Save File
301
Dialog.file.choose.export.title=Export File
302
Dialog.file.choose.import.title=Import File
303
304
Dialog.file.choose.filter.title.source=Source Files
305
Dialog.file.choose.filter.title.definition=Variable Files
306
Dialog.file.choose.filter.title.xml=XML Files
307
Dialog.file.choose.filter.title.all=All Files
308
309
# ########################################################################
310
# Browse Directory
311
# ########################################################################
312
313
BrowseDirectoryButton.chooser.title=Open local directory
314
BrowseDirectoryButton.tooltip=${BrowseDirectoryButton.chooser.title}
315
316
# ########################################################################
317
# Alert Dialog
318
# ########################################################################
319
320
Alert.file.close.title=Close
321
Alert.file.close.text=Save changes to {0}?
322
323
# ########################################################################
324
# Typesetter Installation Wizard
325
# ########################################################################
326
327
Wizard.typesetter.name=ConTeXt
328
Wizard.typesetter.container.name=Podman
329
Wizard.typesetter.container.version=5.6.0
330
Wizard.typesetter.container.checksum=fc8960481e6165b5d1ef05970a11b691b13d434d1f97ceb29b8be6f3902ba86c
331
Wizard.typesetter.container.image.name=typesetter
332
Wizard.typesetter.container.image.version=3.3.0
333
Wizard.typesetter.container.image.tag=${Wizard.typesetter.container.image.name}:${Wizard.typesetter.container.image.version}
334
Wizard.typesetter.container.image.url=https://repository.keenwrite.com/containers/${Wizard.typesetter.container.image.tag}
335
Wizard.typesetter.themes.version=1.11.0
336
Wizard.typesetter.themes.checksum=87e258329d2a4c2802135c4828c2b095a92d62adf7e02925dba24ec4cc1fee6e
337
338
Wizard.container.install.command=Installing container using: ''{0}''
339
Wizard.container.install.await=Waiting for installer to finish
340
Wizard.container.install.download.started=Download ''{0}'' started
341
Wizard.container.install.download.running=Download in progress, please wait
342
Wizard.container.process.enter=Running ''{0}'' ''{1}''
343
Wizard.container.process.exit=Process exit code (zero means success): {0}
344
Wizard.container.executable.run.scan=''{0}'' is executable: {1}
345
Wizard.container.executable.run.missing=Cannot find executable: ''{0}''
346
Wizard.container.executable.run.found=Found executable: ''{0}''
347
Wizard.container.executable.run.error=Cannot run container
348
Wizard.container.executable.which=Cannot find container using search command
349
Wizard.container.executable.path=Cannot find container using PATH variable
350
Wizard.container.executable.registry=Cannot find container using registry
351
352
# STEP 1: Introduction panel (all)
353
Wizard.typesetter.all.1.install.title=Install typesetting system
354
Wizard.typesetter.all.1.install.header=Install typesetting system
355
Wizard.typesetter.all.1.install.about.container.link.lbl=${Wizard.typesetter.container.name}
356
Wizard.typesetter.all.1.install.about.container.link.url=https://podman.io
357
Wizard.typesetter.all.1.install.about.text.1=manages the container for the extensive
358
Wizard.typesetter.all.1.install.about.typesetter.link.lbl=${Wizard.typesetter.name}
359
Wizard.typesetter.all.1.install.about.typesetter.link.url=https://contextgarden.net
360
Wizard.typesetter.all.1.install.about.text.2=\
361
  typesetting software, which generates PDF files. This wizard\n\
362
  will guide you through the installation process. After each\n\
363
  step, you'll be prompted to click a button. Click Next to begin.
364
365
# STEP 2: Install container manager (Unix)
366
# Append steps to keep numbers stable; sorted programmatically.
367
Wizard.typesetter.unix.2.install.container.header=Install ${Wizard.typesetter.container.name} for Linux / macOS / Unix
368
# Copy button states
369
Wizard.typesetter.unix.2.install.container.copy.began=Copy
370
Wizard.typesetter.unix.2.install.container.copy.ended=Copied
371
Wizard.typesetter.unix.2.install.container.os=Operating System
372
Wizard.typesetter.unix.2.install.container.step.0=Complete the following steps to install ${Wizard.typesetter.container.name}:
373
Wizard.typesetter.unix.2.install.container.step.1=\t1. Select this computer's ${Wizard.typesetter.unix.2.install.container.os}.
374
Wizard.typesetter.unix.2.install.container.step.2=\t2. Open a new terminal.
375
Wizard.typesetter.unix.2.install.container.step.3=\t3. Run the commands provided below in the terminal.
376
Wizard.typesetter.unix.2.install.container.step.4=\t4. Click Next to continue.
377
Wizard.typesetter.unix.2.install.container.details.prefix=See
378
Wizard.typesetter.unix.2.install.container.details.link.lbl=${Wizard.typesetter.container.name}'s instructions
379
Wizard.typesetter.unix.2.install.container.details.link.url=https://podman.io/getting-started/installation
380
Wizard.typesetter.unix.2.install.container.details.suffix=for more details.
381
Wizard.typesetter.unix.2.install.container.command.distros=14
382
Wizard.typesetter.unix.2.install.container.command.os.name.01=Arch Linux & Manjaro Linux
383
Wizard.typesetter.unix.2.install.container.command.os.text.01=sudo pacman -S podman
384
Wizard.typesetter.unix.2.install.container.command.os.name.02=Alpine Linux
385
Wizard.typesetter.unix.2.install.container.command.os.text.02=sudo apk add podman
386
Wizard.typesetter.unix.2.install.container.command.os.name.03=CentOS
387
Wizard.typesetter.unix.2.install.container.command.os.text.03=sudo yum -y install podman
388
Wizard.typesetter.unix.2.install.container.command.os.name.04=Debian
389
Wizard.typesetter.unix.2.install.container.command.os.text.04=sudo apt-get -y install podman
390
Wizard.typesetter.unix.2.install.container.command.os.name.05=Fedora
391
Wizard.typesetter.unix.2.install.container.command.os.text.05=sudo dnf -y install podman
392
Wizard.typesetter.unix.2.install.container.command.os.name.06=Gentoo
393
Wizard.typesetter.unix.2.install.container.command.os.text.06=sudo emerge app-containers/podman
394
Wizard.typesetter.unix.2.install.container.command.os.name.07=OpenEmbedded
395
Wizard.typesetter.unix.2.install.container.command.os.text.07=bitbake podman
396
Wizard.typesetter.unix.2.install.container.command.os.name.08=openSUSE
397
Wizard.typesetter.unix.2.install.container.command.os.text.08=sudo zypper install podman
398
Wizard.typesetter.unix.2.install.container.command.os.name.09=RHEL7
399
Wizard.typesetter.unix.2.install.container.command.os.text.09=\
400
  sudo subscription-manager repos \
401
    --enable=rhel-7-server-extras-rpms\n\
402
  sudo yum -y install podman
403
Wizard.typesetter.unix.2.install.container.command.os.name.10=RHEL8
404
Wizard.typesetter.unix.2.install.container.command.os.text.10=\
405
  sudo yum module enable -y container-tools:rhel8\n\
406
  sudo yum module install -y container-tools:rhel8
407
Wizard.typesetter.unix.2.install.container.command.os.name.11=Ubuntu 20.10+
408
Wizard.typesetter.unix.2.install.container.command.os.text.11=\
409
  sudo apt-get -y update\n\
410
  sudo apt-get -y install podman
411
Wizard.typesetter.unix.2.install.container.command.os.name.12=Linuxmint
412
Wizard.typesetter.unix.2.install.container.command.os.text.12=${Wizard.typesetter.unix.2.install.container.command.os.text.11}
413
Wizard.typesetter.unix.2.install.container.command.os.name.13=Linuxmint LMDE
414
Wizard.typesetter.unix.2.install.container.command.os.text.13=${Wizard.typesetter.unix.2.install.container.command.os.text.04}
415
Wizard.typesetter.unix.2.install.container.command.os.name.14=macOS
416
Wizard.typesetter.unix.2.install.container.command.os.text.14=\
417
  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \n\
418
  brew install podman
419
420
# STEP 2 a: Download container manager (Windows)
421
Wizard.typesetter.win.2.download.container.header=Download ${Wizard.typesetter.container.name} for Windows
422
Wizard.typesetter.win.2.download.container.homepage.link.lbl=${Wizard.typesetter.container.name}
423
Wizard.typesetter.win.2.download.container.homepage.link.url=https://podman.io
424
Wizard.typesetter.win.2.download.container.download.link.lbl=repository
425
Wizard.typesetter.win.2.download.container.download.link.url=https://github.com/containers/podman/releases/download/v${Wizard.typesetter.container.version}/podman-${Wizard.typesetter.container.version}-setup.exe
426
Wizard.typesetter.win.2.download.container.paths=Downloading {0} into {1}.
427
# suppress inspection "UnusedMessageFormatParameter"
428
Wizard.typesetter.win.2.download.container.status.bytes=Downloaded {1} bytes (size unknown).
429
Wizard.typesetter.win.2.download.container.status.progress=Downloaded {0} % of {1} bytes.
430
Wizard.typesetter.win.2.download.container.status.checksum.ok=File {0} exists. Click Next to continue.
431
Wizard.typesetter.win.2.download.container.status.checksum.no=Integrity check failed, {0} may be corrupt.
432
Wizard.typesetter.win.2.download.container.status.success=Download successful. Click Next to continue.
433
Wizard.typesetter.win.2.download.container.status.failure=Download failed. Check network then click Previous to try again.
434
435
# STEP 2 b: Install container manager (Windows)
436
Wizard.typesetter.win.2.install.container.header=Install ${Wizard.typesetter.container.name} for Windows
437
Wizard.typesetter.win.2.install.container.step.0=Complete the following steps to install ${Wizard.typesetter.container.name}:
438
Wizard.typesetter.win.2.install.container.step.1=\t1. Open the task bar.
439
Wizard.typesetter.win.2.install.container.step.2=\t2. Click the shield icon to grant permissions.
440
Wizard.typesetter.win.2.install.container.step.3=\t3. Click Yes in the User Account Control dialog to install.
441
Wizard.typesetter.win.2.install.container.status.running=Installing ...
442
Wizard.typesetter.win.2.install.container.status.success=Installation successful.\nClick Next to continue.
443
Wizard.typesetter.win.2.install.container.status.failure=Installation failed with exit code {0}.
444
Wizard.typesetter.win.2.install.container.status.unknown=Could not determine installer file type: {0}
445
446
# STEP 2: Install container manager (Universal, undetected operating system)
447
Wizard.typesetter.all.2.install.container.header=Install ${Wizard.typesetter.container.name}
448
Wizard.typesetter.all.2.install.container.homepage.lbl=${Wizard.typesetter.container.name}
449
Wizard.typesetter.all.2.install.container.homepage.url=https://podman.io
450
451
# STEP 3: Initialize container manager (all except Linux)
452
Wizard.typesetter.all.3.install.container.header=Initialize ${Wizard.typesetter.container.name}
453
Wizard.typesetter.all.3.install.container.correct=${Wizard.typesetter.container.name} initialized.\nClick Next to continue.
454
Wizard.typesetter.all.3.install.container.missing=Install ${Wizard.typesetter.container.name} before continuing.
455
456
# STEP 4: Install typesetter container image (all)
457
Wizard.typesetter.all.4.download.image.header=Download ${Wizard.typesetter.name} image
458
Wizard.typesetter.all.4.download.image.correct=Download successful.\nClick Next to continue.
459
Wizard.typesetter.all.4.download.image.missing=Install ${Wizard.typesetter.container.name} before continuing.
460
461
# STEP 5: Download typesetter themes (all)
462
Wizard.typesetter.all.5.download.themes.header=Download ${Wizard.typesetter.name} themes
463
Wizard.typesetter.all.5.download.themes.download.link.lbl=repository
464
Wizard.typesetter.all.5.download.themes.download.link.url=https://gitlab.com/DaveJarvis/keenwrite-themes/-/releases/${Wizard.typesetter.themes.version}/downloads/theme-pack.zip
465
Wizard.typesetter.all.5.download.themes.paths=Downloading {0} into {1}.
466
Wizard.typesetter.all.5.download.themes.status.bytes=Downloaded {0} bytes (size unknown).
467
Wizard.typesetter.all.5.download.themes.status.progress=Downloaded {0} % of {1} bytes.
468
Wizard.typesetter.all.5.download.themes.status.checksum.ok=File {0} exists. Click Finish to continue.
469
Wizard.typesetter.all.5.download.themes.status.checksum.no=Integrity check failed, {0} may be corrupt.
470
Wizard.typesetter.all.5.download.themes.status.success=Download successful. Click Finish to continue.
471
Wizard.typesetter.all.5.download.themes.status.failure=Download failed. Check network then click Previous to try again.
472
473
# ########################################################################
474
# Open URL dialog
475
# ########################################################################
476
477
Dialog.open_url.title=Open URL
478
Dialog.open_url.label.url=URL\:
479
Dialog.open_url.prompt.url=https://example.com/filename.md
480
481
# ########################################################################
482
# Insert image dialog
483
# ########################################################################
484
485
Dialog.image.title=Insert image
486
Dialog.image.label.url=File or URL\:
487
Dialog.image.label.text=Alternate text\:
488
Dialog.image.label.title=Title\:
489
Dialog.image.prompt.url=Image resource
490
Dialog.image.prompt.text=Image description
491
Dialog.image.prompt.title=Image tooltip
492
493
# ########################################################################
494
# Insert hyperlink dialog
495
# ########################################################################
496
497
Dialog.link.title=Insert hyperlink
498
Dialog.link.label.text=Text\:
499
Dialog.link.label.url=URL\:
500
Dialog.link.label.title=Title\:
501
Dialog.link.prompt.text=Hyperlink text
502
Dialog.link.prompt.url=https://example.com/index.html
503
Dialog.link.prompt.title=Hyperlink tooltip
504
505
# ########################################################################
506
# Typesetting settings dialog
507
# ########################################################################
508
509
Dialog.typesetting.settings.title=Typesetting export settings
510
Dialog.typesetting.settings.header.single=Export current document
511
Dialog.typesetting.settings.theme=Theme
512
Dialog.typesetting.settings.themes.missing=Install themes into {0}.
513
514
Dialog.typesetting.settings.header.multiple=Export multiple documents
515
Dialog.typesetting.settings.chapters=Chapters (e.g., 1-3, 5, 7-)
516
517
# ########################################################################
518
# About dialog
519
# ########################################################################
520
521
Dialog.about.title=About {0}
522
Dialog.about.header={0}
523
Dialog.about.content=Copyright 2016-{0} White Magic Software, Ltd.\n\nVersion: {1}
524
525
# ########################################################################
526
# Application Actions
527
# ########################################################################
528
529
Action.file.new.description=Create a new file
530
Action.file.new.accelerator=Ctrl+N
531
Action.file.new.icon=FILE_ALT
532
Action.file.new.text=_New
533
534
Action.file.open.description=Open a new file
535
Action.file.open.accelerator=Ctrl+O
536
Action.file.open.text=_Open...
537
Action.file.open.icon=FOLDER_OPEN_ALT
538
539
Action.file.open_url.description=Open a URL
540
Action.file.open_url.accelerator=Ctrl+Alt+O
541
Action.file.open_url.text=Open _URL...
542
Action.file.open_url.icon=FOLDER_OPEN_ALT
543
544
Action.file.close.description=Close the current document
545
Action.file.close.accelerator=Ctrl+W
546
Action.file.close.text=_Close
547
548
Action.file.close_all.description=Close all open documents
549
Action.file.close_all.accelerator=Ctrl+F4
550
Action.file.close_all.text=Close All
551
552
Action.file.save.description=Save the document
553
Action.file.save.accelerator=Ctrl+S
554
Action.file.save.text=_Save
555
Action.file.save.icon=FLOPPY_ALT
556
557
Action.file.save_as.description=Rename the current document
558
Action.file.save_as.text=Save _As
559
560
Action.file.save_all.description=Save all open documents
561
Action.file.save_all.accelerator=Ctrl+Shift+S
562
Action.file.save_all.text=Save A_ll
563
564
Action.file.export.pdf.description=Typeset the document
565
Action.file.export.pdf.accelerator=Ctrl+P
566
Action.file.export.pdf.text=_PDF
567
Action.file.export.pdf.icon=FILE_PDF_ALT
568
569
Action.file.export.pdf.dir.description=Typeset files in document directory
570
Action.file.export.pdf.dir.accelerator=Ctrl+Shift+P
571
Action.file.export.pdf.dir.text=_Joined PDF
572
Action.file.export.pdf.dir.icon=FILE_PDF_ALT
573
574
Action.file.export.text_tex.dir.description=Convert files in document directory
575
Action.file.export.text_tex.dir.text=Joined Text
576
Action.file.export.text_tex.dir.icon=FILE_TEXT_ALT
577
578
Action.file.export.pdf.repeat.description=Repeat previous typesetting command
579
Action.file.export.pdf.repeat.accelerator=Ctrl+Shift+E
580
Action.file.export.pdf.repeat.text=_Repeat Export
581
Action.file.export.pdf.repeat.icon=FILE_PDF_ALT
582
583
Action.file.export.html.dir.description=Export files in document directory as HTML
584
Action.file.export.html.dir.accelerator=Ctrl+Shift+H
585
Action.file.export.html.dir.text=Joined _HTML
586
Action.file.export.html.dir.icon=HTML5
587
588
Action.file.export.html_svg.description=Export the current document as HTML + SVG
589
Action.file.export.text=_Export As
590
Action.file.export.html_svg.text=HTML and S_VG
591
592
Action.file.export.html_tex.description=Export the current document as HTML + TeX
593
Action.file.export.html_tex.text=HTML and _TeX
594
595
Action.file.export.text_tex.description=Export the current document as text + TeX
596
Action.file.export.text_tex.text=Text and TeX
597
598
Action.file.export.xhtml_tex.description=Export as XHTML + TeX
599
Action.file.export.xhtml_tex.text=_XHTML and TeX
600
601
Action.file.export.markdown.description=Export the current document as Markdown
602
Action.file.export.markdown.text=Markdown
603
604
Action.file.exit.description=Quit the application
605
Action.file.exit.text=E_xit
606
607
608
Action.edit.undo.description=Undo the previous edit
609
Action.edit.undo.accelerator=Ctrl+Z
610
Action.edit.undo.text=_Undo
611
Action.edit.undo.icon=UNDO
612
613
Action.edit.redo.description=Redo the previous edit
614
Action.edit.redo.accelerator=Ctrl+Y
615
Action.edit.redo.text=_Redo
616
Action.edit.redo.icon=REPEAT
617
618
Action.edit.cut.description=Delete the selected text or line
619
Action.edit.cut.accelerator=Ctrl+X
620
Action.edit.cut.text=Cu_t
621
Action.edit.cut.icon=CUT
622
623
Action.edit.copy.description=Copy the selected text
624
Action.edit.copy.accelerator=Ctrl+C
625
Action.edit.copy.text=_Copy
626
Action.edit.copy.icon=COPY
627
628
Action.edit.paste.description=Paste from the clipboard
629
Action.edit.paste.accelerator=Ctrl+V
630
Action.edit.paste.text=_Paste
631
Action.edit.paste.icon=PASTE
632
633
Action.edit.select_all.description=Highlight the current document text
634
Action.edit.select_all.accelerator=Ctrl+A
635
Action.edit.select_all.text=Select _All
636
637
Action.edit.find.description=Search for text in the document
638
Action.edit.find.accelerator=Ctrl+F
639
Action.edit.find.text=_Find
640
Action.edit.find.icon=SEARCH
641
642
Action.edit.find_next.description=Find next occurrence
643
Action.edit.find_next.accelerator=F3
644
Action.edit.find_next.text=Find _Next
645
646
Action.edit.find_prev.description=Find previous occurrence
647
Action.edit.find_prev.accelerator=Shift+F3
648
Action.edit.find_prev.text=Find _Prev
649
650
Action.edit.preferences.description=Edit user preferences
651
Action.edit.preferences.accelerator=Ctrl+Alt+S
652
Action.edit.preferences.text=_Preferences
653
654
655
Action.format.bold.description=Insert strong text
656
Action.format.bold.accelerator=Ctrl+B
657
Action.format.bold.text=_Bold
658
Action.format.bold.icon=BOLD
659
660
Action.format.italic.description=Insert text emphasis
661
Action.format.italic.accelerator=Ctrl+I
662
Action.format.italic.text=_Italic
663
Action.format.italic.icon=ITALIC
664
665
Action.format.monospace.description=Insert monospace text
666
Action.format.monospace.accelerator=Ctrl+`
667
Action.format.monospace.text=_Monospace
668
669
Action.format.superscript.description=Insert superscript text
670
Action.format.superscript.accelerator=Ctrl+[
671
Action.format.superscript.text=Su_perscript
672
Action.format.superscript.icon=SUPERSCRIPT
673
674
Action.format.subscript.description=Insert subscript text
675
Action.format.subscript.accelerator=Ctrl+]
676
Action.format.subscript.text=Su_bscript
677
Action.format.subscript.icon=SUBSCRIPT
678
679
Action.format.strikethrough.description=Insert struck text
680
Action.format.strikethrough.accelerator=Ctrl+T
681
Action.format.strikethrough.text=Stri_kethrough
682
Action.format.strikethrough.icon=STRIKETHROUGH
683
684
685
Action.insert.blockquote.description=Insert blockquote
686
Action.insert.blockquote.accelerator=Ctrl+Q
687
Action.insert.blockquote.text=_Blockquote
688
Action.insert.blockquote.icon=QUOTE_LEFT
689
690
Action.insert.code.description=Insert inline code
691
Action.insert.code.accelerator=Ctrl+K
692
Action.insert.code.text=Inline _Code
693
Action.insert.code.icon=CODE
694
695
Action.insert.fenced_code_block.description=Insert code block
696
Action.insert.fenced_code_block.accelerator=Ctrl+Shift+K
697
Action.insert.fenced_code_block.text=_Fenced Code Block
698
Action.insert.fenced_code_block.prompt.text=Enter code here
699
Action.insert.fenced_code_block.icon=FILE_CODE_ALT
700
701
Action.insert.link.description=Insert hyperlink
702
Action.insert.link.accelerator=Ctrl+L
703
Action.insert.link.text=_Link...
704
Action.insert.link.icon=LINK
705
706
Action.insert.image.description=Insert image
707
Action.insert.image.accelerator=Ctrl+G
708
Action.insert.image.text=_Image...
709
Action.insert.image.icon=PICTURE_ALT
710
711
Action.insert.heading.description=Insert heading level
712
Action.insert.heading.accelerator=Ctrl+
713
Action.insert.heading.icon=HEADER
714
715
Action.insert.heading_1.description=${Action.insert.heading.description} 1
716
Action.insert.heading_1.accelerator=${Action.insert.heading.accelerator}1
717
Action.insert.heading_1.text=Heading _1
718
Action.insert.heading_1.icon=${Action.insert.heading.icon}
719
720
Action.insert.heading_2.description=${Action.insert.heading.description} 2
721
Action.insert.heading_2.accelerator=${Action.insert.heading.accelerator}2
722
Action.insert.heading_2.text=Heading _2
723
Action.insert.heading_2.icon=${Action.insert.heading.icon}
724
725
Action.insert.heading_3.description=${Action.insert.heading.description} 3
726
Action.insert.heading_3.accelerator=${Action.insert.heading.accelerator}3
727
Action.insert.heading_3.text=Heading _3
728
Action.insert.heading_3.icon=${Action.insert.heading.icon}
729
730
Action.insert.unordered_list.description=Insert bulleted list
731
Action.insert.unordered_list.accelerator=Ctrl+U
732
Action.insert.unordered_list.text=_Unordered List
733
Action.insert.unordered_list.icon=LIST_UL
734
735
Action.insert.ordered_list.description=Insert enumerated list
736
Action.insert.ordered_list.accelerator=Ctrl+Shift+O
737
Action.insert.ordered_list.text=_Ordered List
738
Action.insert.ordered_list.icon=LIST_OL
739
740
Action.insert.horizontal_rule.description=Insert horizontal rule
741
Action.insert.horizontal_rule.accelerator=Ctrl+H
742
Action.insert.horizontal_rule.text=_Horizontal Rule
743
Action.insert.horizontal_rule.icon=LIST_OL
744
745
746
Action.definition.create.description=Create a new variable
747
Action.definition.create.text=_Create
748
Action.definition.create.icon=TREE
749
Action.definition.create.tooltip=Add new item (Insert)
750
751
Action.definition.rename.description=Rename the selected variable
752
Action.definition.rename.text=_Rename
753
Action.definition.rename.icon=EDIT
754
Action.definition.rename.tooltip=Rename selected item (F2)
755
756
Action.definition.delete.description=Delete the selected variables
757
Action.definition.delete.text=De_lete
758
Action.definition.delete.icon=TRASH
759
Action.definition.delete.tooltip=Delete selected items (Delete)
760
761
Action.definition.insert.description=Insert a variable
762
Action.definition.insert.accelerator=Ctrl+Space
763
Action.definition.insert.text=_Insert
764
Action.definition.insert.icon=STAR
765
766
767
Action.view.refresh.description=Clear all caches
768
Action.view.refresh.accelerator=F5
769
Action.view.refresh.text=Refresh
770
771
Action.view.preview.description=Open document preview
772
Action.view.preview.accelerator=F6
773
Action.view.preview.text=Preview
774
775
Action.view.outline.description=Open document outline
776
Action.view.outline.accelerator=F7
777
Action.view.outline.text=Outline
778
779
Action.view.statistics.description=Open document word counts
780
Action.view.statistics.accelerator=F8
781
Action.view.statistics.text=Statistics
782
783
Action.view.files.description=Open file manager
784
Action.view.files.accelerator=Ctrl+F8
785
Action.view.files.text=Files
786
787
Action.view.menubar.description=Toggle menu bar
788
Action.view.menubar.accelerator=Ctrl+F9
789
Action.view.menubar.text=Menu bar
790
791
Action.view.toolbar.description=Toggle toolbar
792
Action.view.toolbar.accelerator=Ctrl+Shift+F9
793
Action.view.toolbar.text=Toolbar
794
795
Action.view.statusbar.description=Toggle status bar
796
Action.view.statusbar.accelerator=Ctrl+Shift+Alt+F9
797
Action.view.statusbar.text=Status bar
798
799
Action.view.log.description=Open document issues
800
Action.view.log.accelerator=F12
801
Action.view.log.text=Log
802
803
804
Action.help.about.description=Show help dialog
805
Action.help.about.accelerator=F1
806
Action.help.about.text=About
807
Action.help.about.icon=INFO
1808
A src/main/resources/com/keenwrite/preview/webview.css
1
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
/* Do not use points (pt): FlyingSaucer on Debian fails to render. */
4
body {
5
  color: #454545;
6
  background-color: #fff;
7
  margin: 0 auto;
8
  padding: .5em;
9
  line-height: 1.6;
10
  font-feature-settings: 'liga' 1;
11
  font-variant-ligatures: normal;
12
}
13
14
body>*:first-child {
15
  margin-top: 0 !important;
16
}
17
18
body>*:last-child {
19
  margin-bottom: 0 !important;
20
}
21
22
#caret {
23
  background: #fcfeff;
24
}
25
26
p, blockquote, ul, ol, dl, table, pre {
27
  margin: 1em 0;
28
}
29
30
/* HEADERS ***/
31
h1, h2, h3, h4, h5, h6 {
32
  font-weight: bold;
33
  margin: 1em 0 .5em;
34
}
35
36
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code,
37
h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
38
  font-size: inherit;
39
}
40
41
h1 {
42
  font-size: 28px;
43
}
44
45
h2 {
46
  font-size: 24px;
47
  border-bottom: 1px solid #ccc;
48
}
49
50
h3 {
51
  font-size: 20px;
52
}
53
54
h4 {
55
  font-size: 18px;
56
}
57
58
h5 {
59
  font-size: 16px;
60
}
61
62
h6 {
63
  font-size: 14px;
64
}
65
66
h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
67
  margin-top: .5em;
68
}
69
70
/* LINKS ***/
71
a {
72
  color: #0077aa;
73
  text-decoration: none;
74
}
75
76
a:hover {
77
  text-decoration: underline;
78
}
79
80
/* CROSS-REFERENCES ***/
81
a.href::after {
82
  content: " (" attr(data-type) " reference)";
83
}
84
85
/* ITEMIZED LISTS ***/
86
ol, ul {
87
  margin: 0 0 0 2em;
88
}
89
90
ol { list-style-type: decimal; }
91
ol ol { list-style-type: lower-alpha; }
92
ol ol ol { list-style-type: lower-roman; }
93
ol ol ol ol { list-style-type: upper-alpha; }
94
ol ol ol ol ol { list-style-type: upper-roman; }
95
ol ol ol ol ol ol { list-style-type: lower-greek; }
96
97
ul { list-style-type: disc; }
98
ul ul { list-style-type: circle; }
99
ul ul ul { list-style-type: square; }
100
ul ul ul ul { list-style-type: disc; }
101
ul ul ul ul ul { list-style-type: circle; }
102
ul ul ul ul ul ul { list-style-type: square; }
103
104
/* DEFINITION LISTS ***/
105
dl {
106
  /** Horizontal scroll bar will appear if set to 100%. */
107
  width: 99%;
108
  overflow: hidden;
109
  padding-left: 1em;
110
}
111
112
dl dt {
113
  font-weight: bold;
114
  float: left;
115
  width: 20%;
116
  clear: both;
117
  position: relative;
118
}
119
120
dl dd {
121
  float: right;
122
  width: 79%;
123
  padding-bottom: .5em;
124
  margin-left: 0;
125
}
126
127
/* PREFORMATTED CODE ***/
128
pre, code, tt {
129
  font-family: 'Source Code Pro';
130
  font-size: 13px;
131
  background-color: #f8f8f8;
132
  text-decoration: none;
133
  white-space: pre-wrap;
134
  word-wrap: break-word;
135
  overflow-wrap: anywhere;
136
  border-radius: .125em;
137
}
138
139
code, tt {
140
  padding: .25em;
141
}
142
143
pre > code {
144
  padding: 0;
145
  border: none;
146
  background: transparent;
147
}
148
149
pre {
150
  border: .125em solid #ccc;
151
  overflow: auto;
152
  padding: .25em .5em;
153
}
154
155
pre code, pre tt {
156
  background-color: transparent;
157
  border: none;
158
}
159
160
/* BLOCKQUOTES ***/
161
blockquote {
162
  border-left: .25em solid #ccc;
163
  padding: 0 1em;
164
  color: #777;
165
}
166
167
blockquote>:first-child {
168
  margin-top: 0;
169
}
170
171
blockquote>:last-child {
172
  margin-bottom: 0;
173
}
174
175
/* TABLES ***/
176
table {
177
  width: 100%;
178
}
179
180
tr:nth-child(odd) {
181
  background-color: #eee;
182
}
183
184
th {
185
  background-color: #454545;
186
  color: #fff;
187
}
188
189
th, td {
190
  text-align: left;
191
  padding: 0 1em;
192
}
193
194
/* IMAGES ***/
195
img {
196
  max-width: 100%;
197
198
  /* Tell FlyingSaucer to treat images as block elements.
199
   * See SvgReplacedElementFactory.
200
   */
201
  display: inline-block;
202
}
203
204
/* TEX ***/
205
206
/* Tell FlyingSaucer to treat tex elements as nodes.
207
 * See SvgReplacedElementFactory.
208
 */
209
tex {
210
  /* Ensure the formulas can be inlined with text. */
211
  display: inline-block;
212
}
213
214
/* Without a robust typesetting engine, there's no
215
 * nice-looking way to automatically typeset equations.
216
 * Sometimes baseline is appropriate, sometimes the
217
 * descender must be considered, and sometimes vertical
218
 * alignment to the middle looks best.
219
 */
220
p tex {
221
  vertical-align: baseline;
222
}
223
224
/* RULES ***/
225
hr {
226
  clear: both;
227
  margin: 1.5em 0 1.5em;
228
  height: 0;
229
  overflow: hidden;
230
  border: none;
231
  background: transparent;
232
  border-bottom: .125em solid #ccc;
233
}
234
235
/* EMAIL ***/
236
div.email {
237
  padding: 0 1.5em;
238
  text-align: left;
239
  text-indent: 0;
240
  border-style: solid;
241
  border-width: 0.05em;
242
  border-radius: .25em;
243
  background-color: #f8f8f8;
244
}
245
246
/* TO DO ***/
247
div.todo:before {
248
  content: "TODO";
249
  color: #c00;
250
  font-weight: bold;
251
  display: block;
252
  width: 100%;
253
  text-align: center;
254
  padding: 0;
255
  margin: 0;
256
}
257
258
div.todo {
259
  border-color: #c00;
260
  background-color: #f8f8f8;
261
}
262
263
div.todo, div.terminal {
264
  padding: .5em;
265
  padding-top: .25em;
266
  padding-bottom: .25em;
267
  border-style: solid;
268
  border-width: 0.05em;
269
  border-radius: .25em;
270
}
271
272
/* TERMINAL ***/
273
div.terminal {
274
  font-family: 'Source Code Pro';
275
  font-size: 90%;
276
  border-color: #222;
277
}
278
279
/* SIMULTANEOUS ACTION ***/
280
div.concurrent {
281
  border-top: .125em solid #ccc;
282
  border-left: .125em solid #ccc;
283
  border-radius: .25em;
284
  padding-left: .5em;
285
}
286
287
/* SPEECH BUBBLE ***/
288
div.bubblerx, div.bubbletx {
289
  display: table;
290
  padding: .5em;
291
  padding-top: .25em;
292
  padding-bottom: .25em;
293
  margin: 1em;
294
  position: relative;
295
  border-radius: .25em;
296
  background-color: #ccc;
297
298
  font-family: 'OpenSansEmoji', sans-serif;
299
  font-size: 95%;
300
}
301
302
/* Transmit bubble on the right. */
303
div.bubbletx {
304
  margin-left: auto;
305
}
306
307
div.bubblerx::after, div.bubbletx::after {
308
  content: "";
309
  position: absolute;
310
  width: 0;
311
  height: 0;
312
  top: .5em;
313
  border-top: 1em solid transparent;
314
  border-bottom: 1em solid transparent;
315
}
316
317
div.bubblerx::after {
318
  left: -1em;
319
  right: auto;
320
  border-right: 1em solid #ccc;
321
  border-left: none;
322
}
323
324
div.bubbletx::after {
325
  right: -1em;
326
  border-left: 1em solid #ccc;
327
}
328
329
/* LYRICS ***/
330
div.lyrics p {
331
  margin: 0;
332
  padding: 0;
333
  white-space: pre-line;
334
  font-style: italic;
335
}
336
337
div.lyrics:first-line p {
338
  line-height: 0;
339
}
340
341
/* TYPEWRITER ***/
342
div.typewritten {
343
  font-family: monospace;
344
  font-size: 16px;
345
  font-weight: bold;
346
}
1347
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/settings.properties
1
# suppress inspection "UnusedProperty" for whole file
2
3
# ########################################################################
4
# Application
5
# ########################################################################
6
7
application.title=keenwrite
8
application.package=com/${application.title}
9
application.messages= com.${application.title}.messages
10
11
# Suppress multiple file modified notifications for one logical modification.
12
# Given in milliseconds.
13
application.watchdog.timeout=50
14
15
# ########################################################################
16
# Preferences
17
# ########################################################################
18
19
preferences.root=com.${application.title}
20
preferences.root.state=state
21
preferences.root.options=options
22
preferences.root.definition.source=definition.source
23
24
# ########################################################################
25
# File and Path References
26
# ########################################################################
27
28
file.stylesheet.application.dir=${application.package}/skins
29
file.stylesheet.application.base=${file.stylesheet.application.dir}/scene.css
30
file.stylesheet.application.skin=${file.stylesheet.application.dir}/{0}.css
31
file.stylesheet.markdown=${application.package}/editor/markdown.css
32
# {0} language code, {1} script code, {2} country code
33
file.stylesheet.markdown.locale=${application.package}/editor/markdown_{0}-{1}-{2}.css
34
file.stylesheet.xml=${application.package}/xml.css
35
36
# Preview styles are loaded statically through a class's classloader.
37
file.stylesheet.preview=webview.css
38
# {0} language code, {1} script code, {2} country code
39
file.stylesheet.preview.locale=webview_{0}-{1}-{2}.css
40
41
file.logo.16=${application.package}/logo16.png
42
file.logo.32=${application.package}/logo32.png
43
file.logo.128=${application.package}/logo128.png
44
file.logo.256=${application.package}/logo256.png
45
file.logo.512=${application.package}/logo512.png
46
47
# Default file name when a new file is created.
48
# This ensures that the file type can always be
49
# discerned so that the correct type of variable
50
# reference can be inserted.
51
file.default.document.prefix=untitled
52
file.default.document.suffix=md
53
file.default.document=${file.default.document.prefix}.${file.default.document.suffix}
54
file.default.definition=variables.yaml
55
56
# Default file name to be replaced by the most
57
# recently exported file name.
58
file.default.pdf=untitled.pdf
59
60
# ########################################################################
61
# File name Extensions
62
# ########################################################################
63
64
# Comma-separated list of definition file name extensions.
65
definition.file.ext.json=*.json
66
definition.file.ext.toml=*.toml
67
definition.file.ext.yaml=*.yml,*.yaml
68
definition.file.ext.properties=*.properties,*.props
69
70
# Comma-separated list of file name extensions.
71
file.ext.rmarkdown=*.Rmd
72
file.ext.rxml=*.Rxml
73
file.ext.source=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown},${file.ext.rxml}
74
file.ext.definition=${definition.file.ext.yaml}
75
file.ext.xml=*.xml,${file.ext.rxml}
76
file.ext.all=*.*
77
78
# File name extension search order for images.
79
file.ext.image.order=svg pdf png jpg tiff
80
81
# ########################################################################
82
# Variable Name Editor
83
# ########################################################################
84
85
# Maximum number of characters for a variable name. A variable is defined
86
# as one or more non-whitespace characters up to this maximum length.
87
editor.variable.maxLength=256
88
89
# ########################################################################
90
# Dialog Preferences
91
# ########################################################################
92
93
dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R
94
dialog.alert.button.order.linux=L_HE+UNYACBXIO_R
95
dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R
96
97
# Ensures a consistent button order for alert dialogs across platforms (because
98
# the default button order on Linux defies all logic).
99
dialog.alert.button.order=${dialog.alert.button.order.windows}
1100
A src/main/resources/com/keenwrite/skins/count_darcula.css
1
.root {
2
  -fx-base: rgb( 43, 43, 43 );
3
  -fx-background: -fx-base;
4
  -fx-control-inner-background: -fx-base;
5
6
  -fx-light-text-color: rgb( 187, 187, 187 );
7
  -fx-mid-text-color: derive( -fx-base, 100% );
8
  -fx-dark-text-color: derive( -fx-base, 25% );
9
  -fx-text-foreground: -fx-light-text-color;
10
  -fx-text-background: derive( -fx-control-inner-background, 7.5% );
11
  -fx-text-selection: derive( -fx-control-inner-background, 45% );
12
13
  /* Make controls ( buttons, thumb, etc. ) slightly lighter */
14
  -fx-color: derive( -fx-base, 20% );
15
}
16
17
.caret {
18
  -fx-stroke: -fx-accent;
19
}
20
21
.glyph-icon {
22
  -fx-text-fill: -fx-light-text-color;
23
  -fx-fill: -fx-light-text-color;
24
}
25
26
.glyph-icon:hover {
27
  -fx-effect: dropshadow( three-pass-box, rgba( 0, 0, 0, 0.2 ), 4, 0, 0, 0 );
28
}
29
30
/* Fix derived prompt color for text fields */
31
.text-input {
32
  -fx-prompt-text-fill: derive( -fx-control-inner-background, +50% );
33
}
34
35
/* Keep prompt invisible when focused ( above color fix overrides it ) */
36
.text-input:focused {
37
  -fx-prompt-text-fill: transparent;
38
}
39
40
/* Fix scroll bar buttons arrows colors */
41
.scroll-bar > .increment-button > .increment-arrow,
42
.scroll-bar > .decrement-button > .decrement-arrow {
43
  -fx-background-color: -fx-mark-highlight-color,  -fx-light-text-color;
44
}
45
46
.scroll-bar > .increment-button:hover > .increment-arrow,
47
.scroll-bar > .decrement-button:hover > .decrement-arrow {
48
  -fx-background-color: -fx-mark-highlight-color, rgb( 240, 240, 240 );
49
}
50
51
.scroll-bar > .increment-button:pressed > .increment-arrow,
52
.scroll-bar > .decrement-button:pressed > .decrement-arrow {
53
  -fx-background-color: -fx-mark-highlight-color, rgb( 255, 255, 255 );
54
}
155
A src/main/resources/com/keenwrite/skins/haunted_grey.css
1
/* https://stackoverflow.com/a/58441758/59087
2
 */
3
.root { 
4
  -fx-accent: #1e74c6;
5
  -fx-focus-color: -fx-accent;
6
  -fx-base: #373e43;
7
  -fx-control-inner-background: derive( -fx-base, 35% );
8
  -fx-control-inner-background-alt: -fx-control-inner-background;
9
10
  -fx-light-text-color: derive( -fx-base, 150% );
11
  -fx-mid-text-color: derive( -fx-base, 100% );
12
  -fx-dark-text-color: derive( -fx-base, 25% );
13
  -fx-text-foreground: -fx-light-text-color;
14
  -fx-text-background: derive( -fx-control-inner-background, 7.5% );
15
  -fx-text-selection: derive( -fx-control-inner-background, 45% );
16
}
17
18
.glyph-icon {
19
  -fx-text-fill: -fx-light-text-color;
20
  -fx-fill: -fx-light-text-color;
21
}
22
23
.glyph-icon:hover {
24
  -fx-effect: dropshadow( three-pass-box, rgba( 0, 0, 0, 0.2 ), 4, 0, 0, 0 );
25
}
26
27
.label {
28
  -fx-text-fill: -fx-light-text-color;
29
}
30
31
.text-field {
32
  -fx-prompt-text-fill: gray;
33
}
34
35
.button {
36
  -fx-focus-traversable: false;
37
}
38
39
.button:hover {
40
  -fx-text-fill: white;
41
}
42
43
.separator *.line { 
44
  -fx-background-color: #3C3C3C;
45
  -fx-border-style: solid;
46
  -fx-border-width: 1px;
47
}
48
49
.scroll-bar {
50
  -fx-background-color: derive( -fx-base, 45% );
51
}
52
53
.button:default {
54
  -fx-base: -fx-accent;
55
} 
56
57
.table-view {
58
  -fx-selection-bar-non-focused: derive( -fx-base, 50% );
59
}
60
61
.table-view .column-header .label {
62
  -fx-alignment: CENTER_LEFT;
63
  -fx-font-weight: none;
64
}
65
66
.list-cell:even,
67
.list-cell:odd,
68
.table-row-cell:even,
69
.table-row-cell:odd {  
70
  -fx-control-inner-background: derive( -fx-base, 15% );
71
}
72
73
.list-cell:empty,
74
.table-row-cell:empty {
75
  -fx-background-color: transparent;
76
}
77
78
.list-cell,
79
.table-row-cell {
80
  -fx-border-color: transparent;
81
  -fx-table-cell-border-color: transparent;
82
}
83
84
/* Avoid clipping text descenders in statistics table row. */
85
.table-row-cell {
86
  -fx-cell-size: 30px;
87
}
188
A src/main/resources/com/keenwrite/skins/modena_dark.css
1
/* https://github.com/joffrey-bion/javafx-themes/blob/master/css/modena_dark.css
2
 */
3
.root {
4
  -fx-base: rgb( 50, 50, 50 );
5
  -fx-background: -fx-base;
6
7
  /* Make controls ( buttons, thumb, etc. ) slightly lighter */
8
  -fx-color: derive( -fx-base, 10% );
9
10
  /* Text fields and table rows background */
11
  -fx-control-inner-background: rgb( 20, 20, 20 );
12
  /* Version of -fx-control-inner-background for alternative rows */
13
  -fx-control-inner-background-alt: derive( -fx-control-inner-background, 2.5% );
14
15
  /* Text colors depending on background's brightness */
16
  -fx-light-text-color: rgb( 220, 220, 220 );
17
  -fx-mid-text-color: rgb( 100, 100, 100 );
18
  -fx-dark-text-color: rgb( 20, 20, 20 );
19
  -fx-text-foreground: -fx-light-text-color;
20
  -fx-text-background: derive( -fx-control-inner-background, 7.5% );
21
  -fx-text-selection: derive( -fx-control-inner-background, 45% );
22
23
  /* A bright blue for highlighting/accenting objects.  For example: selected
24
   * text; selected items in menus, lists, trees, and tables; progress bars */
25
  -fx-accent: rgb( 0, 80, 100 );
26
27
  /* Color of non-focused yet selected elements */
28
  -fx-selection-bar-non-focused: rgb( 50, 50, 50 );
29
}
30
31
.glyph-icon {
32
  -fx-text-fill: -fx-light-text-color;
33
  -fx-fill: -fx-light-text-color;
34
}
35
36
.glyph-icon:hover {
37
  -fx-effect: dropshadow( three-pass-box, rgba( 0, 0, 0, 0.2 ), 4, 0, 0, 0 );
38
}
39
40
/* Fix derived prompt color for text fields */
41
.text-input {
42
  -fx-prompt-text-fill: derive( -fx-control-inner-background, +50% );
43
}
44
45
/* Keep prompt invisible when focused ( above color fix overrides it ) */
46
.text-input:focused {
47
  -fx-prompt-text-fill: transparent;
48
}
49
50
/* Fix scroll bar buttons arrows colors */
51
.scroll-bar > .increment-button > .increment-arrow,
52
.scroll-bar > .decrement-button > .decrement-arrow {
53
  -fx-background-color: -fx-mark-highlight-color, rgb( 220, 220, 220 );
54
}
55
56
.scroll-bar > .increment-button:hover > .increment-arrow,
57
.scroll-bar > .decrement-button:hover > .decrement-arrow {
58
  -fx-background-color: -fx-mark-highlight-color, rgb( 240, 240, 240 );
59
}
60
61
.scroll-bar > .increment-button:pressed > .increment-arrow,
62
.scroll-bar > .decrement-button:pressed > .decrement-arrow {
63
  -fx-background-color: -fx-mark-highlight-color, rgb( 255, 255, 255 );
64
}
165
A src/main/resources/com/keenwrite/skins/modena_light.css
1
.root {
2
  -fx-text-foreground: -fx-dark-text-color;
3
  -fx-text-background: derive( -fx-accent, 124% );
4
  -fx-text-selection: #a6d2ff;
5
}
16
A src/main/resources/com/keenwrite/skins/monokai.css
1
/*
2
 * Theme contributed by mery6299
3
 *
4
 * https://github.com/mery6299
5
 */
6
.root { 
7
  -fx-accent: #75715E;
8
  -fx-focus-color: -fx-accent;
9
  -fx-base: #262626;
10
  -fx-control-inner-background: -fx-base;
11
  -fx-control-inner-background-alt: -fx-control-inner-background;
12
13
  -theme-text-selection: #78dce8;
14
  -theme-search-selection: #ffd866;
15
16
  -fx-light-text-color: derive( -fx-base, 150% );
17
  -fx-mid-text-color: derive( -fx-base, 100% );
18
  -fx-dark-text-color: derive( -fx-base, 25% );
19
  -fx-text-foreground: -fx-light-text-color;
20
  -fx-text-background: derive( -fx-control-inner-background, 25% );
21
  -fx-text-selection: derive( -theme-text-selection, -50% );
22
}
23
24
/* Caret colour */
25
.styled-text-area .caret {
26
  -fx-stroke: white;
27
}
28
29
/* Spelling errors */
30
.markdown .spelling {
31
  -rtfx-underline-color: #fc9867;
32
}
33
34
/* Search result */
35
.markdown .search {
36
  -rtfx-background-color: derive( -theme-search-selection, -25% );
37
}
38
39
.glyph-icon {
40
  -fx-text-fill: -fx-light-text-color;
41
  -fx-fill: -fx-light-text-color;
42
}
43
44
.glyph-icon:hover {
45
  -fx-effect: dropshadow( three-pass-box, rgba( 0, 0, 0, 0.2 ), 4, 0, 0, 0 );
46
}
47
48
.label {
49
  -fx-text-fill: -fx-light-text-color;
50
}
51
52
.text-field {
53
  -fx-prompt-text-fill: gray;
54
}
55
56
.button {
57
  -fx-focus-traversable: false;
58
}
59
60
.button:hover {
61
  -fx-text-fill: white;
62
}
63
64
.separator *.line { 
65
  -fx-background-color: #3C3C3C;
66
  -fx-border-style: solid;
67
  -fx-border-width: 1px;
68
}
69
70
.scroll-bar {
71
  -fx-background-color: derive( -fx-base, 15% );
72
}
73
74
.button:default {
75
  -fx-base: derive( -fx-accent, -25% );
76
} 
77
78
.table-view {
79
  -fx-selection-bar-non-focused: derive( -fx-base, 50% );
80
}
81
82
.table-view .column-header .label {
83
  -fx-alignment: CENTER_LEFT;
84
  -fx-font-weight: none;
85
}
86
87
.list-cell:even,
88
.list-cell:odd,
89
.table-row-cell:even,
90
.table-row-cell:odd {  
91
  -fx-control-inner-background: derive( -fx-base, 15% );
92
}
93
94
.list-cell:empty,
95
.table-row-cell:empty {
96
  -fx-background-color: transparent;
97
}
98
99
.list-cell,
100
.table-row-cell {
101
  -fx-border-color: transparent;
102
  -fx-table-cell-border-color: transparent;
103
}
104
105
/* Avoid clipping text descenders in statistics table row. */
106
.table-row-cell {
107
  -fx-cell-size: 30px;
108
}
109
110
/* Toolbar */
111
.tool-bar .button:hover {
112
  -fx-background-color: derive( -fx-accent, -25% );
113
  -fx-color: -fx-hover-base;
114
}
115
116
/* Tabs */
117
.tab-pane *.tab-header-background {
118
	-fx-background-color: -fx-base;
119
}
120
121
.tab:selected {
122
	-fx-background-color: derive( #A9DC76, -30% );
123
}
124
1125
A src/main/resources/com/keenwrite/skins/scene.css
1
.tool-bar {
2
  -fx-spacing: 0;
3
}
4
5
.tool-bar .button {
6
  -fx-background-color: transparent;
7
}
8
9
.tool-bar .button:hover {
10
  -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
11
  -fx-color: -fx-hover-base;
12
}
13
14
.tool-bar .button:armed {
15
  -fx-color: -fx-pressed-base;
16
}
17
18
/* Definition editor drag and drop target.
19
 */
20
.drop-target {
21
  -fx-border-color: #eea82f;
22
  -fx-border-width: 0 0 2 0;
23
  -fx-padding: 3 3 1 3
24
}
125
A src/main/resources/com/keenwrite/skins/silver_cavern.css
1
/* https://toedter.com/2011/10/26/java-fx-2-0-css-styling/
2
 */
3
.root {
4
  -fx-base: rgb( 50, 50, 50 );
5
  -fx-background: -fx-base;
6
  -fx-control-inner-background: -fx-base;
7
8
  -fx-light-text-color: derive( -fx-base, 150% );
9
  -fx-mid-text-color: derive( -fx-base, 100% );
10
  -fx-dark-text-color: derive( -fx-base, 25% );
11
  -fx-text-foreground: -fx-light-text-color;
12
  -fx-text-background: derive( -fx-control-inner-background, 7.5% );
13
  -fx-text-selection: derive( -fx-control-inner-background, 45% );
14
}
15
16
.glyph-icon {
17
  -fx-text-fill: -fx-light-text-color;
18
  -fx-fill: -fx-light-text-color;
19
}
20
21
.glyph-icon:hover {
22
  -fx-effect: dropshadow( three-pass-box, rgba( 0, 0, 0, 0.2 ), 4, 0, 0, 0 );
23
}
24
 
25
.tab {
26
  -fx-background-color: linear-gradient( to top, -fx-base, derive( -fx-base, 30% ) );
27
}
28
29
.menu-bar {
30
  -fx-background-color: linear-gradient( to bottom, -fx-base, derive( -fx-base, 30% ) );
31
}
32
 
33
.tool-bar:horizontal {
34
  -fx-background-color: linear-gradient( to bottom, derive( -fx-base, +50% ), derive( -fx-base, -40% ), derive( -fx-base, -20% ) );
35
}
36
 
37
.button {
38
  -fx-background-color: transparent;
39
}
40
 
41
.button:hover {
42
  -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
43
  -fx-color: -fx-hover-base;
44
}
45
 
46
.table-view {
47
  -fx-table-cell-border-color:derive( -fx-base, +10% );
48
  -fx-table-header-border-color:derive( -fx-base, +20% );
49
}
50
 
51
.split-pane:horizontal > * > .split-pane-divider {
52
  -fx-border-color: transparent -fx-base transparent -fx-base;
53
  -fx-background-color: transparent, derive( -fx-base, 20% );
54
  -fx-background-insets: 0, 0 1 0 1;
55
}
56
57
.separator-label {
58
  -fx-text-fill: orange;
59
}
160
A src/main/resources/com/keenwrite/skins/solarized_dark.css
1
/* https://ethanschoonover.com/solarized
2
 */
3
.root {
4
  /* Solarized: base03 */
5
  -fx-base: rgb( 0, 43, 54 );
6
  -fx-background: -fx-base;
7
8
  /* Brighten controls */
9
  -fx-color: derive( -fx-base, -40% );
10
11
  -fx-control-inner-background: -fx-base;
12
  -fx-control-inner-background-alt: derive( -fx-control-inner-background, 2.5% );
13
14
  /* Text colors */
15
  /* Solarized: base0 */
16
  -fx-light-text-color: rgb( 131, 148, 150 );
17
  -fx-mid-text-color: derive( -fx-light-text-color, 50% );
18
  -fx-dark-text-color: derive( -fx-light-text-color, 25% );
19
  -fx-text-foreground: -fx-light-text-color;
20
  -fx-text-background: derive( -fx-control-inner-background, 7.5% );
21
  -fx-text-selection: derive( -fx-control-inner-background, 45% );
22
23
  -fx-mid-text-color: derive( -fx-base, 100% );
24
  -fx-dark-text-color: derive( -fx-base, 25% );
25
  -fx-text-foreground: -fx-light-text-color;
26
  -fx-text-background: derive( -fx-control-inner-background, 7.5% );
27
  -fx-text-selection: derive( -fx-control-inner-background, 45% );
28
29
  /* Accent colors */
30
  -fx-accent: rgb( 38, 139, 210 );
31
  -fx-focus-color: rgb( 253, 246, 227 );
32
33
  /* Non-focused-selected elements */
34
  -fx-selection-bar-non-focused: rgb( 0, 43, 54 );
35
}
36
37
.glyph-icon {
38
  -fx-text-fill: -fx-light-text-color;
39
  -fx-fill: -fx-light-text-color;
40
}
41
42
.glyph-icon:hover {
43
  -fx-effect: dropshadow( three-pass-box, rgba( 0, 0, 0, 0.2 ), 4, 0, 0, 0 );
44
}
45
46
.scroll-bar {
47
  -fx-background-color: derive( -fx-base, 45% );
48
}
49
50
.caret {
51
  -fx-stroke: -fx-accent;
52
}
53
154
A src/main/resources/com/keenwrite/skins/vampire_byte.css
1
/* https://github.com/Col-E/Recaf/blob/master/src/main/resources/style/ui-dark.css
2
 */
3
.root {
4
  -fx-base: rgb( 45, 45, 46 );
5
  -fx-background: -fx-base;
6
7
  /* Brighten controls */
8
  -fx-color: derive( -fx-base, -40% );
9
10
  /* Control background */
11
  -fx-control-inner-background: rgb( 46, 46, 47 );
12
13
  /* Alternative control background ( rows ) */
14
  -fx-control-inner-background-alt: derive( -fx-control-inner-background, 2.5% );
15
16
  /* Text colors */
17
  -fx-light-text-color: rgb( 220, 220, 220 );
18
  -fx-mid-text-color: rgb( 100, 100, 100 );
19
  -fx-dark-text-color: rgb( 20, 20, 20 );
20
  -fx-text-foreground: -fx-light-text-color;
21
  -fx-text-background: derive( -fx-control-inner-background, 7.5% );
22
  -fx-text-selection: derive( -fx-control-inner-background, 45% );
23
24
  /* Accent colors */
25
  -fx-accent: rgb( 51, 51, 52 );
26
  -fx-focus-color: rgb( 51, 51, 52 );
27
28
  /* Non-focused-selected elements */
29
  -fx-selection-bar-non-focused: rgb( 45, 45, 46 );
30
}
31
32
.glyph-icon {
33
  -fx-text-fill: -fx-light-text-color;
34
  -fx-fill: -fx-light-text-color;
35
}
36
37
.glyph-icon:hover {
38
  -fx-effect: dropshadow( three-pass-box, rgba( 0, 0, 0, 0.2 ), 4, 0, 0, 0 );
39
}
40
41
* {
42
  -fx-highlight-fill: rgba( 0, 180, 255, 0.4 );
43
}
44
45
/* Scroll */
46
.scroll-bar {
47
  -fx-background-color: rgb( 61,61,62 );
48
}
49
.scroll-bar .thumb {
50
  -fx-background-color: rgb( 91,91,92 );
51
  -fx-background-radius: 0;
52
}
53
.scroll-bar .thumb:hover,
54
.scroll-bar .thumb:pressed {
55
  -fx-background-color: rgb( 141,141,142 );
56
}
57
.scroll-bar .increment-button .increment-arrow,
58
.scroll-bar .decrement-button .decrement-arrow {
59
  -fx-background-color: rgb( 200,200,200 );
60
}
61
.corner {
62
  -fx-background-color: rgb( 61,61,62 );
63
}
64
65
/* Menu */
66
.menu-bar {
67
  -fx-background-color: rgb( 45, 45, 48 );
68
}
69
.menu {
70
  -fx-padding: 6 14 6 14;
71
  -fx-background-insets: -1;
72
}
73
.menu-item {
74
  -fx-padding: 5 11 5 11;
75
  -fx-background-insets: -1;
76
}
77
.menu:hover {
78
  -fx-background-color: rgb( 61, 61, 62 );
79
}
80
.context-menu,
81
.menu:showing {
82
  -fx-background-color: rgb( 27, 27, 28 );
83
  -fx-border-insets: -1;
84
  -fx-border-width: 1;
85
  -fx-border-color: black;
86
}
87
.context-menu {
88
  -fx-min-width: 80px;
89
  -fx-background-insets: -1;
90
  -fx-border-insets: -1;
91
  -fx-border-width: 1;
92
  -fx-border-color: black;
93
}
94
.context-menu .menu-item:focused {
95
  -fx-background-color: rgb( 61, 61, 62 );
96
}
97
.context-menu-header {
98
  /* TODO: Find a way to disable hover coloring on the menu header */
99
  -fx-opacity: 1.0;
100
  -fx-background-color: rgb( 24, 50, 95 );
101
}
102
.context-menu-header .label {
103
  -fx-opacity: 1.0;
104
}
105
106
/* Tabs */
107
.tab-pane {
108
  -fx-tab-min-width: 100px;
109
}
110
.tab-pane *.tab-header-background {
111
  -fx-background-color: rgb( 29, 29, 31 );
112
  -fx-border-width: 0 0 1 0;
113
  -fx-border-color: black;
114
}
115
.headers-region {
116
  -fx-background-color: rgb( 75, 75, 76 );
117
}
118
.tab {
119
  -fx-background-color: rgb( 36,36,37 );
120
  -fx-background-insets: 2 -1 -1 -1;
121
  -fx-background-radius: 0;
122
  -fx-padding: 2 2 1 2;
123
  -fx-border-insets: 0;
124
  -fx-border-width: 1 1 1 1;
125
  -fx-border-color: black;
126
}
127
.tab:selected {
128
  -fx-background-color: rgb( 45, 45, 46 );
129
  -fx-background-insets: 2 -1 -1 -1;
130
  -fx-padding: 2;
131
  -fx-border-insets: 0;
132
  -fx-border-width: 1 1 0 1;
133
  -fx-border-color: black;
134
}
135
.tab:selected .focus-indicator {
136
  -fx-border-color: transparent;
137
}
138
139
/* Table */
140
.table-view {
141
  -fx-selection-bar: rgb( 50, 71, 77 );
142
  -fx-selection-bar-non-focused: rgb( 46, 56, 59 );
143
  -fx-background-color: rgb( 36,36,37 );
144
  -fx-background-insets: 2 -1 -1 -1;
145
  -fx-background-radius: 0;
146
  -fx-padding: -1;
147
  -fx-border-width: 0 1 1 1;
148
  -fx-border-color: rgb( 22, 22, 23 );
149
}
150
.table-view .filler,
151
.table-view .show-hide-columns-button,
152
.column-overlay {
153
  -fx-background-color: transparent;
154
}
155
.column-header-background {
156
  -fx-background-color: rgb( 36,36,37 );
157
  -fx-background-insets: 2 -1 -1 -1;
158
  -fx-padding: -1;
159
  -fx-border-insets: 0;
160
  -fx-border-width: 0 1 0 1;
161
  -fx-border-color: rgb( 22, 22, 23 );
162
}
163
.column-header {
164
  -fx-background-color: rgb( 45, 45, 46 );
165
  -fx-background-insets: -1 -0 -1 0;
166
  -fx-padding: 2;
167
  -fx-border-insets: 1 -1 1 0;
168
  -fx-border-width: 1;
169
  -fx-border-color: rgb( 22, 22, 23 );
170
}
171
172
/* Splitpane */
173
.split-pane-divider {
174
  -fx-background-color: black;
175
  -fx-padding: 0;
176
  -fx-background-insets: -5;
177
}
178
179
/* Tree */
180
.tree-table-view,
181
.tree-view {
182
  -fx-background-color: rgb( 29, 29, 31 );
183
  -fx-background-insets: 0;
184
  -fx-border-width: 0 1 0 0;
185
  -fx-border-color: black;
186
}
187
.tree-table-cell,
188
.tree-cell {
189
  -fx-background-color: rgb( 29, 29, 31 );
190
}
191
.tree-cell:selected {
192
  -fx-background-color: rgb( 44, 48, 55 );
193
}
194
195
/* Buttons */
196
.box,
197
.button,
198
.combo-box,
199
.slider .thumb {
200
  -fx-background-radius: 0;
201
  -fx-background-color: rgb( 63, 63, 70 );
202
  -fx-background-insets: 0;
203
  -fx-border-width: 1;
204
  -fx-border-color: rgb( 85, 85, 85 );
205
}
206
.check-box:hover .box,
207
.button:hover,
208
.combo-box:hover,
209
.slider .thumb:hover {
210
  -fx-background-color: rgb( 80, 80, 85 );
211
  -fx-border-color: rgb( 0, 122, 205 );
212
}
213
.check-box:pressed .box,
214
.button:pressed,
215
.combo-box:pressed,
216
.slider .thumb:pressed {
217
  -fx-background-color: rgb( 0, 122, 205 );
218
  -fx-border-color: rgb( 0, 162, 245 );
219
}
220
.combo-box:showing {
221
  -fx-background-color: rgb( 27, 27, 28 );
222
  -fx-border-width: 1 1 0 1;
223
  -fx-border-color: black;
224
}
225
.combo-box .combo-box-popup .list-cell {
226
  -fx-background-color: rgb( 27, 27, 28 );
227
}
228
.combo-box .combo-box-popup .list-cell:hover {
229
  -fx-background-color: rgb( 61, 61, 62 );
230
}
231
.combo-box .combo-box-popup .list-view {
232
  -fx-background-color: rgb( 27, 27, 28 );
233
  -fx-border-width: 0 1 1 1;
234
  -fx-border-color: black;
235
}
236
.hyperlink {
237
  -fx-text-fill: rgb( 30, 132, 250 );
238
}
239
hyperlink:visited {
240
  -fx-text-fill: rgb( 98, 59, 217 );
241
}
242
243
/* slider */
244
.slider .track {
245
  -fx-background-radius: 0;
246
  -fx-background-color: rgb( 29, 29, 31 );
247
  -fx-background-insets: 0;
248
  -fx-border-width: 1;
249
  -fx-border-color: rgb( 65, 65, 65 );
250
}
251
.slider .thumb {
252
  -fx-padding: 5;
253
}
254
.axis-tick-mark {
255
  -fx-stroke: rgb( 100, 100, 100 );
256
}
257
258
/* Text */
259
.text-area .content,
260
.text-field {
261
  -fx-background-radius: 0;
262
  -fx-background-color: rgb( 63, 63, 70 );
263
  -fx-background-insets: 0;
264
  -fx-border-width: 1;
265
  -fx-border-color: rgb( 85, 85, 85 );
266
}
267
.text-area {
268
  -fx-background-radius: 0;
269
  -fx-background-color: rgb( 63, 63, 70 );
270
  -fx-background-insets: 0;
271
  -fx-border-width: 1;
272
  -fx-border-color: rgb( 85, 85, 85 );
273
}
274
.text-area .content {
275
  -fx-border-width: 0;
276
}
277
278
/* Popup */
279
.tooltip {
280
  -fx-background-radius: 0;
281
  -fx-background-color: rgb( 40, 40, 42 );
282
  -fx-background-insets: 0;
283
  -fx-border-width: 1;
284
  -fx-border-color: rgb( 70, 70, 72 );
285
}
1286
A src/main/resources/com/keenwrite/ui/fonts/icons/3g2.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/3ga.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/3gp.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/7z.svg
1
1
<svg height="63" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M53.344 18.172H44.02V8.965zM28.309 8.965v33.437h25.199V20.434H41.727V8.964zm3.93-8.723H4.417v6.461h10.965l-6.875 5.332v5.652l10.148-7.753V6.867H54V4.281zM18.655 14.297 8.508 22.05v5.652l10.148-7.754zM8.344 37.559l10.148-7.754v-5.657L8.344 31.902zm10.312 2.261v-5.656L8.508 41.918v2.91h-4.09v6.461h6.219v4.523H7.035c-.652-1.132-1.797-1.937-3.273-1.937C1.637 53.875 0 55.488 0 57.59c0 2.097 1.637 3.715 3.762 3.715 1.476 0 2.62-.809 3.273-1.938h3.602v3.39h5.562v-3.39h3.602c.652 1.13 1.8 1.938 3.273 1.938 2.125 0 3.762-1.618 3.762-3.715 0-2.102-1.637-3.715-3.762-3.715-1.472 0-2.62.805-3.273 1.938h-3.602v-4.524h15.875l21.762-3.879v-2.582H11.78zm0 0" fill="#90c"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/aa.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/aac.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ac.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M56.633 44.984V63.27c0 .363-.367.73-.735.73H1.102c-.368 0-.735-.367-.735-.73V61.62c0-.363.367-.73.735-.73h2.39l28.5-28.344L2.391 3.109c-.184-.183-.184-.367-.184-.55V.73c0-.363.367-.73.734-.73h52.957c.368 0 .735.367.735.73v18.106c0 .363-.367.73-.735.73h-2.023c-.367 0-.734-.367-.734-.73 0-7.684-4.598-14.082-12.688-14.082H19.121l24.09 24.137c.367.367.367.73 0 1.097L19.676 53.395h20.777c5.516 0 10.297-3.473 12.133-8.594.184-.367.367-.551.738-.551h2.758c.367 0 .55.367.55.734zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/accdb.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M66.824 40.465c-.176 1.59-2.097 2.824-5.945 3.707-3.852.879-8.75 1.41-14.52 1.41h-4.898v9.172c1.574.176 3.324.176 4.898.176 5.77 0 10.668-.528 14.52-1.586 3.848-1.059 5.945-2.293 6.121-3.707-.176-.352-.176-9.172-.176-9.172zm-20.64-6.7c-1.75 0-3.325 0-4.899-.18v9.352h4.899c5.773 0 10.671-.53 14.52-1.59s5.944-2.292 5.944-3.702v-8.997c-.171 1.586-2.097 2.825-6.12 3.704-3.673 1.058-8.571 1.59-14.344 1.414zm0-11.468c-1.75 0-3.325 0-4.899-.176v9.352c1.574.175 3.324.175 4.899.175 5.773 0 10.671-.53 14.695-1.59C64.727 29 66.824 27.767 67 26.356V17.36c-.176 1.59-2.098 2.825-6.121 3.704-4.024.707-8.922 1.234-14.695 1.234zm0-13.05c-1.75 0-3.325 0-4.899.175v10.406c1.574.176 3.324.176 4.899.176 5.773 0 10.671-.527 14.695-1.586 3.848-1.059 5.945-2.293 6.121-3.703-.176-1.59-2.098-2.824-6.121-3.883-4.024-1.055-8.922-1.41-14.695-1.586zM18.02 23.886c-.176.527-.528 2.293-1.227 5.293l-1.223 5.113h5.07l-1.222-5.113c-.7-3-1.227-4.766-1.227-5.293zM0 7.129v49.918l37.785 6.527V.426zm23.09 37.219-1.399-5.645-6.996-.176-1.398 5.29-4.375-.352 6.648-23.813 5.07-.351 7.348 25.222zm0 0" fill="#a03537"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/accdt.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M66.824 40.465c-.176 1.59-2.097 2.824-5.945 3.707-3.852.879-8.75 1.41-14.52 1.41h-4.898v9.172c1.574.176 3.324.176 4.898.176 5.77 0 10.668-.528 14.52-1.586 3.848-1.059 5.945-2.293 6.121-3.707-.176-.352-.176-9.172-.176-9.172zm-20.64-6.7c-1.75 0-3.325 0-4.899-.18v9.352h4.899c5.773 0 10.671-.53 14.52-1.59s5.944-2.292 5.944-3.702v-8.997c-.171 1.586-2.097 2.825-6.12 3.704-3.673 1.058-8.571 1.59-14.344 1.414zm0-11.468c-1.75 0-3.325 0-4.899-.176v9.352c1.574.175 3.324.175 4.899.175 5.773 0 10.671-.53 14.695-1.59C64.727 29 66.824 27.767 67 26.356V17.36c-.176 1.59-2.098 2.825-6.121 3.704-4.024.707-8.922 1.234-14.695 1.234zm0-13.05c-1.75 0-3.325 0-4.899.175v10.406c1.574.176 3.324.176 4.899.176 5.773 0 10.671-.527 14.695-1.586 3.848-1.059 5.945-2.293 6.121-3.703-.176-1.59-2.098-2.824-6.121-3.883-4.024-1.055-8.922-1.41-14.695-1.586zM18.02 23.886c-.176.527-.528 2.293-1.227 5.293l-1.223 5.113h5.07l-1.222-5.113c-.7-3-1.227-4.766-1.227-5.293zM0 7.129v49.918l37.785 6.527V.426zm23.09 37.219-1.399-5.645-6.996-.176-1.398 5.29-4.375-.352 6.648-23.813 5.07-.351 7.348 25.222zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ace.svg
1
1
<svg height="63" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M53.344 18.172H44.02V8.965zM28.309 8.965v33.437h25.199V20.434H41.727V8.964zm3.93-8.723H4.417v6.461h10.965l-6.875 5.332v5.652l10.148-7.753V6.867H54V4.281zM18.655 14.297 8.508 22.05v5.652l10.148-7.754zM8.344 37.559l10.148-7.754v-5.657L8.344 31.902zm10.312 2.261v-5.656L8.508 41.918v2.91h-4.09v6.461h6.219v4.523H7.035c-.652-1.132-1.797-1.937-3.273-1.937C1.637 53.875 0 55.488 0 57.59c0 2.097 1.637 3.715 3.762 3.715 1.476 0 2.62-.809 3.273-1.938h3.602v3.39h5.562v-3.39h3.602c.652 1.13 1.8 1.938 3.273 1.938 2.125 0 3.762-1.618 3.762-3.715 0-2.102-1.637-3.715-3.762-3.715-1.472 0-2.62.805-3.273 1.938h-3.602v-4.524h15.875l21.762-3.879v-2.582H11.78zm0 0" fill="#90c"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/adn.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M66.824 40.465c-.176 1.59-2.097 2.824-5.945 3.707-3.852.879-8.75 1.41-14.52 1.41h-4.898v9.172c1.574.176 3.324.176 4.898.176 5.77 0 10.668-.528 14.52-1.586 3.848-1.059 5.945-2.293 6.121-3.707-.176-.352-.176-9.172-.176-9.172zm-20.64-6.7c-1.75 0-3.325 0-4.899-.18v9.352h4.899c5.773 0 10.671-.53 14.52-1.59s5.944-2.292 5.944-3.702v-8.997c-.171 1.586-2.097 2.825-6.12 3.704-3.673 1.058-8.571 1.59-14.344 1.414zm0-11.468c-1.75 0-3.325 0-4.899-.176v9.352c1.574.175 3.324.175 4.899.175 5.773 0 10.671-.53 14.695-1.59C64.727 29 66.824 27.767 67 26.356V17.36c-.176 1.59-2.098 2.825-6.121 3.704-4.024.707-8.922 1.234-14.695 1.234zm0-13.05c-1.75 0-3.325 0-4.899.175v10.406c1.574.176 3.324.176 4.899.176 5.773 0 10.671-.527 14.695-1.586 3.848-1.059 5.945-2.293 6.121-3.703-.176-1.59-2.098-2.824-6.121-3.883-4.024-1.055-8.922-1.41-14.695-1.586zM18.02 23.886c-.176.527-.528 2.293-1.227 5.293l-1.223 5.113h5.07l-1.222-5.113c-.7-3-1.227-4.766-1.227-5.293zM0 7.129v49.918l37.785 6.527V.426zm23.09 37.219-1.399-5.645-6.996-.176-1.398 5.29-4.375-.352 6.648-23.813 5.07-.351 7.348 25.222zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ai.svg
1
1
<svg height="64" width="66" xmlns="http://www.w3.org/2000/svg"><path d="M40.266 62.762 34.922 45.55h-19.98l-5.34 17.21h-7.32L21.468 5.59h7.32l19.39 57.172zM26.813 19.438c-.594-2.176-1.387-5.145-1.583-7.32h-.199c-.394 1.98-.988 4.945-1.781 7.32L16.523 41H33.54zm33.039-9.891c-2.375 0-4.356-1.781-4.356-4.156s1.98-4.153 4.356-4.153c2.37 0 4.351 1.778 4.351 4.153 0 2.375-1.98 4.156-4.351 4.156zm-3.563 53.215V18.05h7.32v44.71zm0 0" fill="#fea500" stroke="#fea500" stroke-miterlimit="10" stroke-width="2.47295"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/aif.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/aifc.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/aiff.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ait.svg
1
1
<svg height="64" width="66" xmlns="http://www.w3.org/2000/svg"><path d="M40.266 62.762 34.922 45.55h-19.98l-5.34 17.21h-7.32L21.468 5.59h7.32l19.39 57.172zM26.813 19.438c-.594-2.176-1.387-5.145-1.583-7.32h-.199c-.394 1.98-.988 4.945-1.781 7.32L16.523 41H33.54zm33.039-9.891c-2.375 0-4.356-1.781-4.356-4.156s1.98-4.153 4.356-4.153c2.37 0 4.351 1.778 4.351 4.153 0 2.375-1.98 4.156-4.351 4.156zm-3.563 53.215V18.05h7.32v44.71zm0 0" fill="#fea500" stroke="#fea500" stroke-miterlimit="10" stroke-width="2.47295"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/amr.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ani.svg
1
1
<svg height="64" width="53" xmlns="http://www.w3.org/2000/svg"><path d="M49.023.375H3.977C1.703.375 0 2.07 0 4.328 0 6.59 1.703 8.281 3.977 8.281h.187v3.953c0 8.286 5.11 15.625 12.684 18.637.379.188.757.563.757.942v.375c0 .378-.378.753-.757.94C9.086 36.142 3.977 43.48 4.164 51.767v3.953h-.187C1.703 55.719 0 57.41 0 59.672c0 2.258 1.703 3.953 3.977 3.953h45.046c2.274 0 3.977-1.695 3.977-3.953 0-2.262-1.703-3.953-3.977-3.953h-.187v-3.953c0-8.286-5.11-15.625-12.684-18.637-.379-.188-.757-.563-.757-.941v-.376c0-.378.378-.753.757-.94 7.762-3.013 12.871-10.352 12.684-18.638V8.281h.187C51.297 8.281 53 6.59 53 4.328 53 2.07 51.297.375 49.023.375zm-5.488 11.86c0 6.023-3.785 11.484-9.465 13.742-2.46.941-4.164 3.199-4.164 5.836v.375c0 2.636 1.703 4.894 4.164 5.835 5.68 2.258 9.465 7.72 9.465 13.743v3.953H9.465v-3.953c0-6.024 3.785-11.485 9.465-13.743 2.46-.941 4.164-3.199 4.164-5.836v-.374c0-2.637-1.703-4.895-4.164-5.836-5.68-2.258-9.465-7.72-9.465-13.743V8.281h34.07zm-28.77 6.777c-.378-.567-.19-1.317.38-1.696.187-.187.375-.187.753-.187H37.29c.566 0 1.137.566 1.137 1.129 0 .187 0 .566-.192.754-1.324 1.883-3.027 3.199-5.109 3.953-2.27.754-3.977 2.445-5.11 4.515-.378.754-1.328 1.133-2.081.567-.192-.188-.57-.375-.57-.567-1.134-2.07-2.84-3.761-5.11-4.515-2.274-.942-4.164-2.258-5.488-3.953zM29.907 42.73a6.64 6.64 0 0 0 4.164 1.504c2.84 0 5.301 1.883 5.868 4.707v.188c.19.566.19 1.129.19 1.883s-.565 1.316-1.323 1.316H14.008c-.758 0-1.324-.562-1.324-1.316 0-.567.187-1.317.187-1.883v-.188c.758-2.636 3.219-4.52 6.059-4.52a6.64 6.64 0 0 0 4.164-1.503c.758-.754 1.511-1.508 1.89-2.45.38-.562.95-.937 1.703-.75.57.188.95.376 1.137.75.57.755 1.137 1.509 2.082 2.262zm0 0" fill="#8ed200"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/apk.svg
1
1
<svg height="64" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M10.867 48.152c0 1.551 1.04 2.582 2.602 2.582h2.773v9.305c0 2.066 1.91 3.961 3.992 3.961s3.989-1.895 3.989-3.96v-9.306h5.379v9.305c0 2.066 1.91 3.961 3.992 3.961s3.988-1.895 3.988-3.96v-9.306h2.602c1.562 0 2.605-1.03 2.605-2.757V21.449H10.867zM4.097 21.45C2.017 21.45.11 23.344.11 25.41v18.606c0 2.066 1.907 3.96 3.989 3.96s3.992-1.894 3.992-3.96V25.41c0-2.066-1.735-3.96-3.992-3.96zm45.805 0c-2.082 0-3.992 1.895-3.992 3.961v18.606c0 2.066 1.91 3.96 3.992 3.96s3.989-1.894 3.989-3.96V25.41c0-2.066-1.907-3.96-3.989-3.96zM36.367 5.945l3.473-3.449c.52-.516.52-1.375 0-1.894a1.373 1.373 0 0 0-1.91 0L33.94 4.566c-1.91-1.379-4.34-1.894-6.941-1.894-2.777 0-5.031.515-7.285 1.55L15.898.259c-.523-.344-1.562-.344-2.082 0-.347.515-.347 1.55 0 2.066l3.47 3.446c-3.817 2.93-6.419 7.41-6.419 12.75h32.266c0-5.168-2.602-9.82-6.766-12.575zm-14.746 7.407h-2.773v-2.586h2.773zm13.531 0H32.38v-2.586h2.773zm0 0" fill="#a4ca39"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/app.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M62.887 38.266c-2.684-.84-4.532-3.36-4.532-6.215 0-2.852 1.848-5.371 4.532-6.211.84-.336 1.343-1.172 1.008-2.012-.84-3.023-1.848-5.707-3.524-8.394-.504-.84-1.344-1.008-2.184-.672-1.007.504-2.015.84-3.19.84-3.692 0-6.548-3.024-6.548-6.547 0-1.176.336-2.184.84-3.188.504-.84.168-1.68-.672-2.183a40.47 40.47 0 0 0-8.39-3.528c-.84-.168-1.68.168-2.016 1.008C37.37 3.852 34.855 5.7 32 5.7s-5.371-1.847-6.21-4.535C25.452.324 24.612-.18 23.772.156c-3.02.84-5.707 1.848-8.39 3.528-.84.503-1.008 1.343-.672 2.183.504 1.004.84 2.012.84 3.188 0 3.691-3.024 6.547-6.547 6.547-1.176 0-2.184-.336-3.191-.84-.84-.504-1.68-.168-2.184.672a40.699 40.699 0 0 0-3.524 8.394c-.167.84.168 1.676 1.008 2.012 2.684.84 4.532 3.36 4.532 6.21 0 2.856-1.848 5.376-4.532 6.216-.84.332-1.343 1.172-1.008 2.011.84 3.024 1.848 5.707 3.524 8.395.504.84 1.344 1.008 2.184.672 1.007-.504 2.015-.84 3.19-.84 3.692 0 6.548 3.02 6.548 6.547 0 1.176-.336 2.183-.84 3.187-.504.84-.168 1.68.672 2.184a40.47 40.47 0 0 0 8.39 3.527h.336c.672 0 1.344-.504 1.512-1.176.84-2.687 3.356-4.535 6.211-4.535s5.371 1.848 6.211 4.535c.336.84 1.176 1.344 2.016 1.008 3.02-.84 5.707-1.847 8.39-3.527.84-.504 1.008-1.344.672-2.184-.504-1.004-.84-2.011-.84-3.187 0-3.692 3.024-6.547 6.547-6.547 1.176 0 2.184.336 3.192.84.84.504 1.68.168 2.183-.672a40.698 40.698 0 0 0 3.524-8.395c.503-.672 0-1.511-.84-1.843zm-30.719 3.691c-5.371 0-9.902-4.363-9.902-9.906 0-5.371 4.363-9.903 9.902-9.903 5.371 0 9.902 4.364 9.902 9.903 0 5.375-4.53 9.906-9.902 9.906zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/applescript.svg
1
1
<svg height="64" width="68" xmlns="http://www.w3.org/2000/svg"><g fill="#999"><path d="M51.6 33H28.3c-3.2 0-5.9 2.601-5.9 5.9v26.3h-5.9c0 3.2 2.599 5.9 5.9 5.9h23.4c3.2 0 5.9-2.601 5.9-5.9V41.7h5.9v-2.9a5.98 5.98 0 0 0-6-5.8zm-2.9 31.6c0 1.9-1.5 3.4-3.4 3.4H23.9c1.399-1 1.399-2.9 1.399-2.9V38.9c0-1.6 1.3-2.9 2.902-2.9 1.598 0 2.899 1.3 2.899 2.9v2.901h17.6zM34.1 38.9V36h17.6c2.7 0 2.9 1.7 2.9 2.9zm0 0" stroke="#999" stroke-miterlimit="10" stroke-width=".75" transform="matrix(1.62485 0 0 1.6469 -26.2 -53.722)"/><path d="M28.719 46.082c-.489-.332-.973-.824-1.461-1.32-.488-.492-.813-1.153-1.137-1.645-.812-1.316-1.625-2.637-1.953-4.117-.484-1.648-.813-3.293-.813-4.941 0-1.813.329-3.293 1.141-4.61.484-.988 1.297-1.976 2.438-2.472a5.777 5.777 0 0 1 3.246-.989c.328 0 .812 0 1.3.164.325.164.653.164 1.137.496.653.164.977.329 1.14.493.325.164.65.164.973.164.165 0 .489 0 .653-.164.16 0 .484-.164.972-.328s.813-.329 1.137-.329c.488-.168.813-.168 1.137-.332.488 0 .812-.164 1.3 0 .813 0 1.466.164 2.278.496 1.137.493 2.11 1.153 2.762 2.305-.324.164-.489.328-.813.66-.488.492-.976 1.153-1.465 1.645-.484.824-.812 1.812-.648 2.965 0 1.316.324 2.304.977 3.293.484.66.972 1.32 1.785 1.812l.976.496c-.164.492-.324.82-.488 1.317-.324.988-.813 1.812-1.461 2.632-.488.66-.812 1.32-1.14 1.649l-1.297 1.316a3.095 3.095 0 0 1-1.625.496c-.329 0-.813 0-1.141-.164-.324-.164-.649-.164-.973-.332-.324-.164-.648-.328-.976-.328-.485-.164-.813-.164-1.297-.164-.488 0-.977 0-1.301.164-.324.164-.652.164-.977.328-.488.168-.812.332-.972.332-.328.164-.813.164-1.14.164-1.298-.66-1.786-.824-2.274-1.152zm7.636-21.246c-.812.328-1.46.492-2.273.492-.164-.656 0-1.48.324-2.305.324-.656.649-1.316 1.137-1.976a5.093 5.093 0 0 1 1.789-1.48c.813-.329 1.461-.66 2.11-.66.163.823 0 1.484-.325 2.304-.324.66-.648 1.484-1.137 1.977-.324.824-.812 1.32-1.625 1.648zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/asax.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#d5006e"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/asc.svg
1
1
<svg height="64" width="80" xmlns="http://www.w3.org/2000/svg"><g fill="#bababa"><path d="M0 61.766V2.234C0 1.004 1 0 2.223 0h53.332c.668 0 1.222.223 1.668.781l22.222 24.922c.332.445.555.89.555 1.45v34.613C80 62.996 79 64 77.777 64H2.223A2.234 2.234 0 0 1 0 61.766zm75.555-33.72-21-23.577H4.445V59.53h71.11zm0 0"/><path d="M53.332 29.055V4.469c0-1.227 1-2.235 2.223-2.235a2.236 2.236 0 0 1 2.222 2.235V26.82h17.778a2.234 2.234 0 0 1 0 4.47h-20a2.236 2.236 0 0 1-2.223-2.235zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ascx.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#d5006e"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/asf.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ash.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M13.875 13.874h10.9v2.701h-10.9zm0 5.4h10.9v2.701h-10.9zm0 5.5h10.9v2.702h-10.9zm19-24.399H11.177c-3 0-5.402 2.4-5.402 5.4v24.4h-5.4c0 3 2.402 5.4 5.4 5.4h21.7c3 0 5.402-2.4 5.402-5.4v-21.7h5.4v-2.7c0-3-2.402-5.4-5.4-5.4zm-2.7 29.3c0 1.801-1.4 3.2-3.2 3.2h-19.9c1.3-.9 1.3-2.7 1.3-2.7v-24.4c0-1.5 1.2-2.7 2.7-2.7 1.501 0 2.7 1.2 2.7 2.7v2.7h16.3zm-13.6-23.9v-2.7h16.3c2.501 0 2.7 1.6 2.7 2.7zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".75" transform="matrix(1.7717 0 0 1.78025 .262 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ashx.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#d5006e"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/asm.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="stroke:none;fill-rule:nonzero;fill:#999;fill-opacity:1" d="M.125 0h69.586v8.184H.125zm13.164 18.273h69.586v8.18H13.289zM.125 36.543h69.586v8.184H.125zm13.164 18.273h69.586V63H13.289zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/asmx.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#d5006e"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/asp.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#c33"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/aspx.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#d5006e"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/asx.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/au.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/aup.svg
1
1
<svg height="64" width="68" xmlns="http://www.w3.org/2000/svg"><path d="M34 0c-7.133 0-13.672 1.98-18.625 5.547S7.051 14.266 7.051 20.21v8.719C3.09 31.902.316 38.242.316 45.574.316 55.68 5.664 64 12.203 64c1.586 0 3.371-.594 4.953-1.586V28.73c-.988-.593-2.18-1.187-3.367-1.386V20.21c0-3.566 1.785-6.738 5.352-9.113C22.707 8.52 28.055 6.738 34 6.738s11.098 1.782 14.86 4.36c3.566 2.574 5.35 5.746 5.35 9.113v6.934c-1.187.398-2.378.793-3.366 1.386v33.29c1.582.992 3.367 1.585 4.953 1.585 6.34 0 11.887-8.324 11.887-18.43 0-6.933-2.774-13.273-6.735-16.246v-8.52c0-5.944-3.37-11.292-8.324-14.663C47.672 2.18 41.133 0 34 0zm0 29.723-7.332 14.465-4.555-5.946-1.586 2.38v11.093l1.586-1.98 5.746 7.331L34 44.582l6.14 12.484 5.747-7.332 1.586 1.98V40.622l-1.586-2.379-4.356 6.14zm0 0" fill="#1493f6"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/avi.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/axd.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#d5006e"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/aze.svg
1
1
<svg height="64" width="52" xmlns="http://www.w3.org/2000/svg"><path d="M34.625 63.98c14.355-12.652 1.95-28.816-1.773-30.574.53 1.934.355 5.27-1.594 7.203-.887-4.918-4.785-11.07-10.278-13.18.883 7.032-3.19 11.95-4.964 14.41-1.418 2.286-9.922 14.06-1.418 22.141-20.38-6.328-15.239-26.535-9.391-35.496C11.41 19.172 18.676 11.617 17.078.02c9.926 3.515 16.66 13.882 18.434 21.789 3.367-3.164 3.898-8.786 3.011-11.95 6.914 2.813 28.536 41.47-3.898 54.121zm0 0" fill="#ff9800"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/bak.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#bababa"><path d="M0 0v36.57h13.715V13.715H36.57V0zm0 0"/><path d="M18.285 18.285V64H64V18.285zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/bash.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M13.875 13.874h10.9v2.701h-10.9zm0 5.4h10.9v2.701h-10.9zm0 5.5h10.9v2.702h-10.9zm19-24.399H11.177c-3 0-5.402 2.4-5.402 5.4v24.4h-5.4c0 3 2.402 5.4 5.4 5.4h21.7c3 0 5.402-2.4 5.402-5.4v-21.7h5.4v-2.7c0-3-2.402-5.4-5.4-5.4zm-2.7 29.3c0 1.801-1.4 3.2-3.2 3.2h-19.9c1.3-.9 1.3-2.7 1.3-2.7v-24.4c0-1.5 1.2-2.7 2.7-2.7 1.501 0 2.7 1.2 2.7 2.7v2.7h16.3zm-13.6-23.9v-2.7h16.3c2.501 0 2.7 1.6 2.7 2.7zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".75" transform="matrix(1.7717 0 0 1.78025 .262 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/bat.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#999"><path d="M64 0H0v64h64zM12.8 12.633H6.399V6.23h6.403zm44.802 0h-38.57V6.23h38.57zm0 44.797H6.23V19.2h51.372zm0 0"/><path d="m16.336 24.59-4.547 4.547 7.41 7.41-7.41 7.242 4.547 4.547 11.957-11.79zm10.613 21.558h12.797v6.399H26.95zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/bin.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M17.8.5c-2.9 0-5.4 2.801-5.4 6.2 0 3.4 2.4 6.2 5.4 6.2 2.9 0 5.399-2.8 5.399-6.2C23.199 3.302 20.8.5 17.8.5zm0 10.1c-1.6 0-3-1.7-3-3.9 0-2.1 1.3-3.9 3-3.9s3 1.7 3 3.9-1.3 3.9-3 3.9zM7 11.8V1.7C7 1 6.5.5 5.8.5S4.6 1 4.6 1.7v10.1c0 .7.5 1.2 1.2 1.2S7 12.4 7 11.8zm-1.1 6.9C3 18.7.5 21.5.5 24.9s2.4 6.2 5.4 6.2 5.401-2.8 5.401-6.2c-.102-3.3-2.5-6.2-5.4-6.2zm0 10.2c-1.6 0-3-1.699-3-3.9 0-2.1 1.3-3.9 3-3.9s3 1.7 3 3.9c-.1 2.1-1.4 3.9-3 3.9zM19 30V19.9c0-.7-.5-1.2-1.2-1.2s-1.2.5-1.2 1.2V30c0 .7.5 1.2 1.2 1.2S19 30.7 19 30zM31.3 12.7V2.6c0-.7-.499-1.2-1.2-1.2-.7 0-1.1.5-1.1 1.2v10.099c0 .701.5 1.2 1.2 1.2s1.1-.6 1.1-1.2zm-1.2 6.9c-2.9 0-5.401 2.8-5.401 6.2 0 3.4 2.4 6.202 5.4 6.202 2.901 0 5.402-2.802 5.402-6.202S33.1 19.6 30.1 19.6zm0 10.102c-1.6 0-3-1.7-3-3.902 0-2.099 1.3-3.9 3-3.9s3 1.7 3 3.9c0 2.202-1.3 3.902-3 3.902zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" transform="matrix(1.91667 0 0 1.9394 0 .485)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/blank.svg
1
1
<svg height="64" width="80" xmlns="http://www.w3.org/2000/svg"><g fill="#bababa"><path d="M0 61.766V2.234C0 1.004 1 0 2.223 0h53.332c.668 0 1.222.223 1.668.781l22.222 24.922c.332.445.555.89.555 1.45v34.613C80 62.996 79 64 77.777 64H2.223A2.234 2.234 0 0 1 0 61.766zm75.555-33.72-21-23.577H4.445V59.53h71.11zm0 0"/><path d="M53.332 29.055V4.469c0-1.227 1-2.235 2.223-2.235a2.236 2.236 0 0 1 2.222 2.235V26.82h17.778a2.234 2.234 0 0 1 0 4.47h-20a2.236 2.236 0 0 1-2.223-2.235zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/bmp.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/bowerrc.svg
1
1
<svg height="64" width="72" xmlns="http://www.w3.org/2000/svg"><filter id="a" height="100%" width="100%" x="0%" y="0%"><feColorMatrix in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter><mask id="c"><g filter="url(#a)"><path d="M0 0h72v64H0z" fill-opacity=".65"/></g></mask><clipPath id="b"><path d="M0 0h72v64H0z"/></clipPath><g clip-path="url(#b)" mask="url(#c)"><path d="M25.281.082h4.356l.168.168c1.34.34 2.843.508 4.183 1.18 4.524 1.855 7.203 5.39 8.875 9.937 0 .168.168.508.336.676 1.004.336 2.008.504 3.016.84l.164-.168c2.68-5.39 7.035-8.422 12.894-9.43 1.508-.34 3.016-.172 4.524.336-.168.168-.168.336-.336.336-2.012 2.191-3.516 4.547-3.852 7.41-.168 1.012-.336 1.852-.504 2.863-.5 2.528-1.34 5.055-3.347 6.907C53.246 23.497 50.062 24 46.883 24c-.168 0-.504-.168-.504-.504-.332-.508-.5-1.012-.668-1.687-2.176.507-2.848 1.011-3.016 2.863.672 0 1.34.172 2.012.172 5.691.672 11.387 1.515 16.91 2.691 2.68.676 5.192 1.348 7.703 2.528 1.34.675 2.012 1.683 2.344 3.199 0 .34.168.675.336.843v2.02c-.168.168-.168.508-.336.676-.332 1.011-.836 1.851-1.672 2.527-1.008.84-2.18 1.176-3.52 1.348.337 1.683-.667 2.691-1.84 3.367-.835.504-1.675.84-2.51 1.012-1.34.168-2.513 0-3.852 0-1.004 1.851-2.68 2.187-4.52 2.355-1.844.168-3.52-.336-5.023-1.347-.672.843-1.34 1.347-2.512 1.347-2.012.336-3.684-.168-5.36-.84l-.167-.168.167.168c1.172 2.188 2.344 4.211 3.348 6.399.336.676.336 1.683.336 2.36 0 1.515-.836 2.523-2.008 2.862-.168 1.18-.84 2.188-1.676 2.864-1.171.84-2.511 1.007-4.02 1.007-.167 0-.667-.167-.667 0-.672 1.012-1.844 1.348-2.848 1.856h-1.507c-1.004-.168-1.84-1.18-3.016-.34-.164.172-.5.172-.668.172-1.172 0-3.348-.676-3.852-2.36s-1.34-3.538-2.007-5.222l-.168.168c-1.508 3.035-4.856 4.21-8.04 2.695a14.392 14.392 0 0 1-4.351-3.367C1.67 46.402-1.508 35.621.668 23.496 2.676 12.043 11.551 3.286 22.77.926 23.44.422 24.445.25 25.28.082zm13.73 19.035c1.676.168 3.52.168 5.192-.676 1.508-.671 3.184-.671 4.688-.504 1.007.168 2.18.504 3.183.672-.504-.504-1.004-1.011-1.676-1.347-2.68-2.02-5.691-3.367-9.207-3.703-.336 0-.84 0-.84.168-1.003 1.515-1.671 3.03-1.671 4.882-.168 1.18.164 2.36.668 3.371 1.843-.843 3.851-1.687 5.691-2.527v-.508c-1.004.172-1.84.508-2.844.676-1.172.168-2.343.336-3.183-.504zM56.427 7.664c-2.176 2.356-3.684 5.05-5.024 7.746l3.684 3.2c1.176-1.516 1.508-3.2 2.012-4.883.668-3.032.836-6.235 3.18-8.59-4.352-.168-10.715 4.379-12.055 8.422l2.175 1.175c.168-.168.168-.504.336-.671.668-1.18 1.34-2.36 2.176-3.368 1.004-1.18 2.012-2.36 3.516-3.03zm-7.867 24.59c1.34.168 2.68.336 4.015.336l12.395 1.347c.668 0 1.172.168 1.84.168-.336-1.011-.836-1.851-1.676-2.19-.836-.337-1.84-.505-2.676-.84-3.684-.677-7.203-1.516-10.887-2.192-3.515-.504-7.199-.84-10.715-1.348-.167 0-.503 0-.671.172l-2.008 2.02c-3.684 2.695-7.703 3.367-12.055 3.367a25.933 25.933 0 0 0 3.348-1.18c3.515-1.344 6.531-3.2 8.707-6.23.336-.504.336-.84.168-1.348a12.356 12.356 0 0 1-1.508-5.895c0-2.527.836-4.714 2.68-6.566.336-.34.336-.676.168-1.012-1.172-2.695-2.848-4.715-5.36-6.234-3.015-1.852-6.363-2.02-9.543-1.516-7.535 1.18-13.23 5.055-17.25 11.285-3.683 5.895-4.855 12.297-4.015 19.204.332 2.187.836 4.21 1.84 6.062.167.504.335 1.012.671 1.516 4.52 8.254 11.551 12.968 20.93 13.64 5.527.34 10.383-2.695 12.055-7.914.336-1.18.84-2.36.168-3.539 1.172 1.012 2.68 1.516 4.355 1.688 1.672.168 3.012-.34 3.684-1.688.168 0 .332.172.5.172 1.34.672 2.68.84 4.187.672 1.844-.168 2.68-1.012 2.848-2.695.836.168 1.84.336 2.68.168 2.675 0 3.851-1.516 3.347-3.704 1.004 0 2.176 0 3.18-.167 1.008-.172 2.012-.676 2.68-1.856l-10.047-2.02-10.047-2.023c-.332.34 2.012.34 2.012.34zm-1.676-13.645c.336 1.012.504 1.852.668 2.696.168.504.336.504.84.504 1.672-.168 3.18-.504 4.52-1.176.167-.172.335-.172.839-.508l-4.02-1.008-.503.84h-.168v-1.012zm0 0" fill="#543828"/><path d="M5.02 40c-1.004-1.852-1.504-4.043-1.84-6.063-1.004-6.906.168-13.304 4.02-19.203 4.015-6.398 9.878-10.273 17.245-11.28 3.348-.509 6.532-.34 9.543 1.515 2.512 1.515 4.188 3.535 5.36 6.23.168.336.168.676-.168 1.012-1.672 1.851-2.512 4.043-2.68 6.566 0 2.024.504 4.043 1.508 5.895.336.508.168.844-.168 1.348-2.176 3.03-5.192 5.054-8.707 6.234-1.004.336-2.176.672-3.348 1.176 4.352 0 8.54-.672 12.055-3.368-.836 1.348-1.34 2.696-2.344 3.875-4.351 5.727-10.047 8.254-17.414 6.231-.668-.168-1.34-.336-2.008.336l-1.008.508c-3.18.84-6.695.504-10.046-1.012zm22.605-29.813c-3.516 0-6.195 2.696-6.195 6.403 0 3.367 2.847 6.23 6.363 6.23s6.195-2.863 6.195-6.398c0-3.54-2.843-6.402-6.363-6.235zm0 0" fill="#e95927"/><path d="M5.02 40c3.351 1.516 6.867 1.684 10.382.676.336-.172.836-.172 1.004-.508.672-.504 1.34-.504 2.012-.336 7.367 2.02 13.059-.504 17.414-6.23.836-1.18 1.508-2.528 2.344-3.875l2.008-2.02c.168-.172.504-.172.671-.172 3.516.508 7.2.844 10.715 1.348 3.684.676 7.203 1.347 10.887 2.191-.168 0-.336.168-.336.168-4.523.336-9.043.672-13.394 1.012h-2.348l10.047 2.02c-.164.335-.164 1.011-.332 1.18-.504.503-1.172 1.01-1.844 1.179-.836.168-1.676 0-2.512 0 .168.84 0 1.683-.668 2.355-.504.34-1.007.844-1.675 1.012-1.508.676-3.016.676-4.52-.168-.336 1.516-1.676 2.02-2.848 2.188-1.34.171-2.511 0-3.683-.84l-.168.168 1.004 2.359c.672 1.176.168 2.355-.168 3.535-1.672 5.223-6.528 8.422-12.055 7.918-9.379-.676-16.41-5.39-20.93-13.644-.672-.504-.84-1.012-1.007-1.516zm0 0" fill="#fbcd00"/><path d="M51.402 36.8c.84 0 1.844.169 2.512 0 .672-.167 1.34-.675 1.844-1.179.336-.168.336-.844.336-1.18l10.047 2.024c-.672 1.008-1.508 1.683-2.68 1.851-1.004.168-2.176.168-3.184.168.504 2.192-.668 3.707-3.347 3.707-.836 0-1.844-.171-2.68-.171-.168 1.687-1.004 2.527-2.848 2.695-1.504.168-2.843 0-4.183-.672 0-.336-.168-.336-.336-.336-.668 1.348-2.008 1.852-3.684 1.684s-3.015-.676-4.355-1.684l-1.004-2.36.168-.167c1.172.84 2.344 1.18 3.683.84 1.34-.168 2.512-.672 2.848-2.188 1.508.844 3.012.844 4.52.168.504-.168 1.171-.672 1.675-1.012.668-.504.836-1.347.668-2.187zm0 0" fill="#3daf00"/><path d="M56.426 7.664c-1.504.672-2.676 1.852-3.684 3.2-.836 1.011-1.504 2.187-2.176 3.366-.168.168-.168.336-.336.676l-2.175-1.18c1.34-4.21 7.703-8.59 12.054-8.421-2.343 2.527-2.343 5.726-3.18 8.59-.335 1.683-.835 3.367-2.007 4.882l-3.684-3.199c1.504-2.863 3.18-5.558 5.188-7.914.336-.172 0 0 0 0zm0 0" fill="#25a7f0"/><path d="M38.68 18.777c0-1.851.5-3.535 1.672-4.882.168-.168.671-.168.84-.168 3.515.168 6.359 1.683 9.206 3.703.504.34 1.172.843 1.676 1.347-1.004-.168-2.176-.504-3.183-.672-1.672-.335-3.18-.167-4.688.504-1.672.676-3.348.676-5.191.676zm0 0" fill="#cbcbca"/><path d="M48.559 32.254c4.52-.34 9.039-.676 13.394-1.012.168 0 .336 0 .336-.168.836.336 1.84.504 2.68.84 1.004.34 1.34 1.18 1.672 2.191-.668 0-1.34-.167-1.84-.167l-12.227-1.18zm0 0" fill="#3eae00"/><path d="m46.883 18.61 2.176.335v1.012h.168l.503-1.012 4.02 1.012c-.336.168-.504.336-.84.504-1.508.676-3.012 1.012-4.52 1.18-.335 0-.671 0-.84-.504-.163-.676-.5-1.516-.667-2.528zm0 0" fill="#25a5ec"/><path d="m38.68 18.777.164.34c1.008.84 2.18.84 3.183.672 1.004-.168 1.84-.504 2.848-.672v.504l-5.695 2.527c-.168-1.011-.5-2.191-.5-3.37zm0 0" fill="#c9c8c7"/><path d="M27.625 10.188c3.52 0 6.363 2.695 6.363 6.234 0 3.535-2.843 6.398-6.195 6.398-3.348 0-6.363-2.863-6.363-6.23 0-3.707 2.68-6.403 6.195-6.403zm3.684 6.234c0-2.192-1.672-3.875-3.684-3.875-2.008 0-3.684 1.683-3.852 3.875 0 2.02 1.676 3.871 3.688 3.871 2.344-.168 3.848-1.684 3.848-3.871zm0 0" fill="#fbcb00"/><path d="M56.426 7.664c.168.168 0 0 0 0zm0 0" fill="#543828"/><path d="M31.309 16.422c0 2.187-1.672 3.703-3.848 3.703-2.18 0-3.852-1.684-3.688-3.871 0-2.024 1.676-3.875 3.852-3.875 2.18.168 3.684 1.851 3.684 4.043zm-3.684-.168a8.37 8.37 0 0 0 1.844-.676c.668-.336.668-1.18 0-1.683-.836-.676-2.68-.676-3.516 0-.672.503-.672 1.347 0 1.683.5.336 1.004.504 1.672.676zm0 0" fill="#553928"/><path d="M27.625 16.254c-.668-.172-1.172-.34-1.672-.676-.672-.336-.672-1.18 0-1.683.836-.676 2.68-.676 3.516 0 .668.503.668 1.347 0 1.683a8.37 8.37 0 0 1-1.844.676zm0 0" fill="#fff"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/bpg.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/bz2.svg
1
1
<svg height="63" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M53.344 18.172H44.02V8.965zM28.309 8.965v33.437h25.199V20.434H41.727V8.964zm3.93-8.723H4.417v6.461h10.965l-6.875 5.332v5.652l10.148-7.753V6.867H54V4.281zM18.655 14.297 8.508 22.05v5.652l10.148-7.754zM8.344 37.559l10.148-7.754v-5.657L8.344 31.902zm10.312 2.261v-5.656L8.508 41.918v2.91h-4.09v6.461h6.219v4.523H7.035c-.652-1.132-1.797-1.937-3.273-1.937C1.637 53.875 0 55.488 0 57.59c0 2.097 1.637 3.715 3.762 3.715 1.476 0 2.62-.809 3.273-1.938h3.602v3.39h5.562v-3.39h3.602c.652 1.13 1.8 1.938 3.273 1.938 2.125 0 3.762-1.618 3.762-3.715 0-2.102-1.637-3.715-3.762-3.715-1.472 0-2.62.805-3.273 1.938h-3.602v-4.524h15.875l21.762-3.879v-2.582H11.78zm0 0" fill="#90c"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/bzempty.svg
1
1
<svg height="64" width="52" xmlns="http://www.w3.org/2000/svg"><path d="M47.734 32c8.914 12.195 3.325 28-11.785 31.46C28.695 52.927 40.33 43.59 47.734 32zM8.45 60.3l.453.302a18.118 18.118 0 0 0 9.215 3.011c-2.867-5.87-2.265-12.945 1.363-18.367l10.575-15.355c5.742-8.578 3.476-20.32-5.137-26.043l-.605-.45A18.106 18.106 0 0 0 15.098.387c2.87 6.02 2.265 12.945-1.36 18.367L3.16 34.109c-5.742 8.73-3.472 20.473 5.29 26.192zm27.047-17.76s3.02-4.067 4.531-6.474l4.383-6.476c5.438-7.977-4.082-14.3-5.289-14.45 1.207 2.407 0 7.376-1.512 9.786l-4.382 6.472c-2.414 3.762-1.508 8.73 2.27 11.141zm0 0" fill="#bababa"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/c.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="stroke:none;fill-rule:nonzero;fill:#999;fill-opacity:1" d="M.125 0h69.586v8.184H.125zm13.164 18.273h69.586v8.18H13.289zM.125 36.543h69.586v8.184H.125zm13.164 18.273h69.586V63H13.289zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cab.svg
1
1
<svg height="63" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M53.344 18.172H44.02V8.965zM28.309 8.965v33.437h25.199V20.434H41.727V8.964zm3.93-8.723H4.417v6.461h10.965l-6.875 5.332v5.652l10.148-7.753V6.867H54V4.281zM18.655 14.297 8.508 22.05v5.652l10.148-7.754zM8.344 37.559l10.148-7.754v-5.657L8.344 31.902zm10.312 2.261v-5.656L8.508 41.918v2.91h-4.09v6.461h6.219v4.523H7.035c-.652-1.132-1.797-1.937-3.273-1.937C1.637 53.875 0 55.488 0 57.59c0 2.097 1.637 3.715 3.762 3.715 1.476 0 2.62-.809 3.273-1.938h3.602v3.39h5.562v-3.39h3.602c.652 1.13 1.8 1.938 3.273 1.938 2.125 0 3.762-1.618 3.762-3.715 0-2.102-1.637-3.715-3.762-3.715-1.472 0-2.62.805-3.273 1.938h-3.602v-4.524h15.875l21.762-3.879v-2.582H11.78zm0 0" fill="#4d1b9b"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cad.svg
1
1
<svg height="63" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M33.325 19.55c-.498-.2-1.1 0-1.299.5-1.1 2.5-2.901 4.7-5.1 6.4l-6.7-13.601c1-.8 1.6-1.999 1.6-3.4 0-2.099-1.501-3.899-3.501-4.3v-3.4a1 1 0 0 0-2 0v3.4c-2 .401-3.5 2.201-3.5 4.3 0 1.401.6 2.6 1.601 3.4l-6.7 13.602c-2.201-1.7-4-3.801-5.1-6.401-.201-.5-.8-.7-1.301-.5-.499.199-.7.8-.499 1.3 1.299 3 3.4 5.4 6 7.3l-4 8c-.2.5 0 1.1.4 1.3.098 0 .3.1.4.1.3 0 .7-.2.9-.5l3.8-7.8c2.7 1.5 5.6 2.2 8.7 2.2 3.1 0 6-.8 8.699-2.2l3.8 7.8c.1.3.501.5.9.5.1 0 .3 0 .4-.1.5-.2.7-.8.4-1.3l-3.9-8c2.6-1.8 4.701-4.4 6-7.3.6-.5.401-1.101 0-1.3zM17.326 6.95c1.4 0 2.5 1.1 2.5 2.499 0 1.401-1.1 2.502-2.5 2.502s-2.5-1.1-2.5-2.502c0-1.4 1.199-2.5 2.5-2.5zm0 22.6c-2.8 0-5.4-.7-7.801-2l6.8-13.7c.3.1.701.1 1.1.1.402 0 .701 0 1.1-.1l6.8 13.7c-2.5 1.3-5.199 2-7.999 2zm0 0" fill="#369" stroke="#369" stroke-miterlimit="10" stroke-width="1.5" transform="matrix(1.6544 0 0 1.63607 0 .154)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/caf.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cal.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M5.102 59.219v-10.11h11.605v10.11zm14.066 0v-10.11h12.836v10.11zM5.102 46.805V35.457h11.605v11.348zm14.066 0V35.457h12.836v11.348zM5.102 33.152V23.047h11.605v10.105zM34.645 59.22v-10.11H47.48v10.11zM19.168 33.152V23.047h12.836v10.105zm30.95 26.067v-10.11h11.605v10.11zM34.644 46.805V35.457H47.48v11.348zm-14.07-30.496c0 .53-.528 1.062-1.231 1.062h-2.637c-.703 0-1.23-.531-1.23-1.062V6.203c0-.535.527-1.066 1.23-1.066h2.461c.703 0 1.23.531 1.23 1.066V16.31zm29.542 30.496V35.457h11.606v11.348zM34.645 33.152V23.047H47.48v10.105zm15.472 0V23.047h11.606v10.105zm1.406-16.843c0 .53-.527 1.062-1.23 1.062h-2.637c-.703 0-1.23-.531-1.23-1.062V6.203c0-.535.527-1.066 1.23-1.066h2.637c.703 0 1.23.531 1.23 1.066zM67 14.004c0-2.484-2.285-4.434-5.102-4.434h-5.097V6.203c0-3.016-2.813-5.676-6.508-5.676h-2.637c-3.515 0-6.508 2.48-6.508 5.676V9.57H25.676V6.203c0-3.016-2.817-5.676-6.508-5.676h-2.637c-3.52 0-6.508 2.48-6.508 5.676V9.57H5.102C2.285 9.57 0 11.7 0 14.004v45.035c0 2.484 2.285 4.434 5.102 4.434h56.62c2.817 0 5.102-2.13 5.102-4.434V14.004zm0 0" fill="#111"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cdda.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M43.195 53.328v-24l13.93-2.308C55.008 15.11 44.605 6.043 32.09 6.043c-14.106 0-25.563 11.555-25.563 25.773 0 14.223 11.457 25.778 25.563 25.778 1.41 0 2.644-.18 4.055-.356 1.058-1.777 2.82-3.023 5.113-3.734.703-.176 1.41-.176 1.937-.176zM36.848 8.176l-2.82 12.09h-2.645l-1.938-12.09c2.82-1.422 7.403 0 7.403 0zM32.09 41.418c-5.29 0-9.52-4.266-9.52-9.602 0-5.332 4.23-9.597 9.52-9.597 5.289 0 9.52 4.265 9.52 9.597 0 5.336-4.231 9.602-9.52 9.602zm0-16.887c-4.055 0-7.227 3.2-7.227 7.285 0 4.09 3.172 7.29 7.227 7.29s7.226-3.2 7.226-7.29c0-4.086-3.171-7.285-7.226-7.285zm0 12.442c-2.82 0-5.113-2.309-5.113-5.157 0-2.843 2.293-5.152 5.113-5.152 2.82 0 5.113 2.309 5.113 5.152.176 2.848-2.293 5.157-5.113 5.157zm3.347 24.707c.18.71.356 1.246.708 1.957-1.41.175-2.645.355-4.055.355C14.637 63.992.355 49.594.355 31.996S14.637 0 32.09 0c15.512 0 28.558 11.375 31.203 26.129l-2.996.535C57.828 13.332 46.19 3.024 32.09 3.024 16.398 3.023 3.53 15.995 3.53 31.815c0 15.82 12.867 28.797 28.559 28.797 1.058 0 2.117 0 3.172-.175 0 .355 0 .886.175 1.242zm31.208-33.95v27.73c0 2.31-1.766 4.087-4.41 4.798-2.82.71-5.641-.531-6.344-2.664-.532-2.313 1.41-4.621 4.23-5.332 1.234-.356 2.645-.18 3.703.175V35.73l-14.808 2.665V59.19c0 1.957-1.766 3.91-4.235 4.621-2.82.711-5.816-.71-6.168-2.664-.531-2.312 1.41-4.62 4.23-5.332 1.235-.355 2.645-.18 3.704.176V31.105zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cer.svg
1
1
<svg height="64" width="52" xmlns="http://www.w3.org/2000/svg"><path d="M48.793 26.879h-4.629V18.05C44.164 7.988 35.973.043 26 .043S7.836 8.164 7.836 18.051v8.828H3.207A3.181 3.181 0 0 0 0 30.059V60.78c0 1.762 1.426 3.176 3.207 3.176h45.586c1.781 0 3.207-1.414 3.207-3.176V29.883c0-1.59-1.426-3.004-3.207-3.004zM29.918 52.305c.355 1.058-.535 1.941-1.602 1.941h-4.808c-1.07 0-1.781-1.059-1.606-1.941l1.426-5.649c-1.781-.883-3.027-2.648-3.027-4.945 0-3 2.492-5.473 5.52-5.473 3.027 0 5.523 2.473 5.523 5.473 0 2.117-1.246 4.062-3.028 4.945zm5.164-25.426H16.918V18.05c0-4.942 4.098-9.004 9.082-9.004s9.082 4.062 9.082 9.004zm0 0" fill="#696"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cfg.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M41.266 22.992c0-3.945-2.403-7.035-5.664-8.406V3.262C35.602 1.372 34.23 0 32.344 0s-3.262 1.371-3.262 3.262v11.324c-3.43 1.2-5.66 4.46-5.66 8.406 0 3.945 2.402 7.035 5.66 8.406 0 .172-.172.516-.172.688V60.57c0 1.887 1.375 3.258 3.262 3.258s3.258-1.371 3.258-3.258V31.914c0-.344 0-.516-.168-.687 3.601-1.028 6.004-4.29 6.004-8.235zm-9.094 2.574c-1.371 0-2.402-1.03-2.402-2.402 0-1.375 1.03-2.402 2.402-2.402s2.402 1.027 2.402 2.402c.172 1.2-1.031 2.402-2.402 2.402zM58.254 3.602c0-1.887-1.375-3.258-3.262-3.258s-3.262 1.37-3.262 3.258v26.597c-3.43 1.2-5.66 4.461-5.66 8.406 0 3.946 2.403 7.036 5.66 8.407 0 .172-.171.515-.171.687v13.04c0 1.89 1.375 3.261 3.261 3.261 1.887 0 3.262-1.371 3.262-3.262V47.7c0-.344 0-.515-.172-.687 3.43-1.2 5.66-4.461 5.66-8.407 0-3.945-2.402-7.035-5.66-8.406V3.602zm-3.262 37.406c-1.37 0-2.402-1.028-2.402-2.403 0-1.37 1.031-2.402 2.402-2.402 1.371 0 2.403 1.031 2.403 2.402 0 1.375-1.032 2.403-2.403 2.403zm-48.73 19.39c0 1.887 1.375 3.258 3.261 3.258 1.887 0 3.258-1.37 3.258-3.258V47.355c0-.343 0-.511-.172-.683 3.434-1.203 5.664-4.461 5.664-8.41 0-3.946-2.402-7.035-5.664-8.407V3.602c0-1.887-1.37-3.258-3.257-3.258S6.09 1.714 6.09 3.602v26.597C2.66 31.4.43 34.66.43 38.605c0 3.946 2.402 7.036 5.66 8.407 0 .172-.172.515-.172.687v13.04c0-.34.344-.34.344-.34zm3.261-24.367c1.372 0 2.403 1.032 2.403 2.403 0 1.375-1.031 2.402-2.403 2.402-1.375 0-2.402-1.027-2.402-2.402 0-1.371 1.027-2.403 2.402-2.403zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cfm.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M11.427 34.125c-7.399 0-10.8-7.1-10.8-14.9-.099-7.7 3.3-14.9 10.8-14.9 2.3 0 4.2.6 5.7 1.6l-1 2.4c-1.1-.7-2.6-1.2-4.2-1.2-5.2 0-7.3 6-7.3 12.1 0 6 2.1 12 7.2 12 1.6 0 3.2-.5 4.2-1.2l1 2.5c-1.5 1-3.3 1.6-5.6 1.6zm14.901-20.8v20.3h-3.701v-20.3h-2.599v-2.3h2.599v-3.2c0-4.3 2.4-7.2 6.9-7.2h.8v2.4h-.3c-2 0-3.699 1-3.699 4.6v3.3h3.899v2.4zm0 0" fill="#679eb2" stroke="#679eb2" stroke-miterlimit="10" stroke-width="1.25" transform="matrix(1.84155 0 0 1.8314 0 .18)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cfml.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M11.427 34.125c-7.399 0-10.8-7.1-10.8-14.9-.099-7.7 3.3-14.9 10.8-14.9 2.3 0 4.2.6 5.7 1.6l-1 2.4c-1.1-.7-2.6-1.2-4.2-1.2-5.2 0-7.3 6-7.3 12.1 0 6 2.1 12 7.2 12 1.6 0 3.2-.5 4.2-1.2l1 2.5c-1.5 1-3.3 1.6-5.6 1.6zm14.901-20.8v20.3h-3.701v-20.3h-2.599v-2.3h2.599v-3.2c0-4.3 2.4-7.2 6.9-7.2h.8v2.4h-.3c-2 0-3.699 1-3.699 4.6v3.3h3.899v2.4zm0 0" fill="#679eb2" stroke="#679eb2" stroke-miterlimit="10" stroke-width="1.25" transform="matrix(1.84155 0 0 1.8314 0 .18)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cgi.svg
1
1
<svg height="64" width="73" xmlns="http://www.w3.org/2000/svg"><path d="M.184 46.813 26.828 64V51.383h45.625v-9.145H26.828V29.805zm45.988-34.196H.547v9.145h45.625v12.617l26.644-17.191L46.172 0zm0 0" fill="#666"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/chm.svg
1
1
<svg height="64" width="59" xmlns="http://www.w3.org/2000/svg"><path d="M59 0H13.41C6.613 0 0 2.668 0 10.668V64h48.273V10.668H6.613c0-3.914 2.684-5.336 5.367-5.336h41.477v53.336l5.363-5.336V0zm0 0" fill="#c93"/><path d="M21.992 40.18c0-5.512 6.434-6.403 6.434-10.493 0-1.777-1.61-3.199-3.754-3.199-2.324.18-4.11 1.778-4.11 1.778L17.88 24.89s2.683-2.848 7.332-2.848c4.289 0 8.402 2.668 8.402 7.289 0 6.402-6.797 7.113-6.797 11.203v1.422h-4.824zm0 5.152h4.824v4.445h-4.824zm0 0" fill="#fff"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/class.svg
1
1
<svg height="64" width="48" xmlns="http://www.w3.org/2000/svg"><g fill="#f60" stroke="#f60" stroke-miterlimit="10" stroke-width=".5"><path d="M44.2 75.3c7.2-3.701 3.9-7.3 1.5-6.799-.6.099-.801.2-.801.2s.2-.3.601-.5C50.1 66.6 53.6 73 44 75.5zM37.8 64.8c1.801 2.1-.5 4-.5 4s4.7-2.4 2.5-5.5c-2-2.8-3.6-4.2 4.8-9.101 0 .101-13.1 3.401-6.8 10.6" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M39.8 46.499s3.999 4-3.8 10.102c-6.2 4.898-1.4 7.7 0 10.899-3.601-3.3-6.3-6.2-4.5-8.8 2.7-4 9.9-5.9 8.3-12.201M31 76.8s-1.5.9 1 1.1c3 .299 4.6.299 7.9-.3 0 0 .9.599 2.1 1-7.4 3.3-16.901-.1-11-1.8m-.9-4.2s-1.6 1.199.9 1.5c3.2.3 5.8.4 10.2-.5 0 0 .6.6 1.599 1-9.1 2.6-19.199.2-12.698-2" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M47.7 79.9s1.1.9-1.2 1.599c-4.3 1.302-18 1.702-21.8.101-1.4-.6 1.2-1.4 2-1.6.8-.2 1.3-.1 1.3-.1-1.5-1.1-9.8 2.1-4.2 3 15.3 2.4 27.9-1.199 23.9-3M31.7 68.3s-7 1.702-2.499 2.301c1.9.301 5.699.2 9.2-.101 2.9-.2 5.799-.8 5.799-.8s-1 .4-1.8.901c-7.1 1.9-20.7.999-16.8-.9 3.4-1.6 6.1-1.401 6.1-1.401" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M32.399 85.4c6.901.4 17.502-.2 17.7-3.5 0 0-.499 1.2-5.699 2.2-5.899 1.1-13.101 1-17.5.3.1 0 1 .7 5.499 1" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cmd.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#999"><path d="M64 0H0v64h64zM12.8 12.633H6.399V6.23h6.403zm44.802 0h-38.57V6.23h38.57zm0 44.797H6.23V19.2h51.372zm0 0"/><path d="m16.336 24.59-4.547 4.547 7.41 7.41-7.41 7.242 4.547 4.547 11.957-11.79zm10.613 21.558h12.797v6.399H26.95zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/coffee.svg
1
1
<svg height="64" width="79" xmlns="http://www.w3.org/2000/svg"><path d="M30.86 9.86c7.523-.567 9.59-5.458 18.43-6.212 4.323-.375 6.956.567 7.331 2.07.188 1.317-1.879 2.446-4.512 2.634-3.574.378-5.078-.938-5.453-2.258-2.633.187-3.008 1.32-3.008 2.258.188 1.507 3.57 3.011 9.024 2.449 6.207-.567 8.277-3.012 7.898-5.457-.562-3.2-5.453-5.836-14.101-5.082-11.098.941-11.098 6.02-18.43 6.773-3.008.188-4.89-.375-5.078-1.691-.188-1.13 1.316-1.883 3.008-1.883 1.695-.188 3.574.187 4.703.562.754-.375.941-.75.754-1.128-.192-1.13-2.633-1.692-5.457-1.317-5.64.567-5.64 3.012-5.453 4.14.754 2.634 4.89 4.516 10.343 4.141zM68.28 22.468c-6.957 1.691-15.797 2.633-26.328 2.633-10.906 0-19.742-1.13-26.512-2.633-6.207-1.696-9.402-3.39-10.718-5.082.562 3.953 1.691 7.902 3.007 11.668-1.503.941-3.007 2.257-4.324 3.761C.961 35.828-.168 39.402.02 42.98c.187 3.575 1.882 6.399 4.703 8.657 2.82 2.258 6.015 2.824 9.402 2.258 1.316-.188 2.82-.942 4.137-1.317-2.82 0-5.266-.941-7.711-2.824-2.633-1.883-4.512-4.703-4.89-7.902-.563-3.012 0-6.024 1.694-8.47.375-.566.75-.94 1.125-1.316.942 2.446 2.07 4.704 3.387 6.961 2.633 3.953 5.266 7.528 7.899 11.293 1.129 2.258 1.879 4.516 2.445 6.586 1.691 2.446 4.137 4.14 7.332 5.082 3.762 1.317 7.71 1.88 11.848 1.88h.375c3.949 0 8.273-.563 12.222-1.88a14.826 14.826 0 0 0 7.149-5.082h.187a27.312 27.312 0 0 1 2.258-6.586c2.629-3.765 5.262-7.34 7.895-11.293 3.574-6.398 6.02-13.738 7.335-21.64-1.128 2.07-4.515 3.761-10.53 5.082zm-52.84-5.457c6.957 1.691 15.793 2.633 26.325 2.633 10.906 0 19.558-.942 26.328-2.633C75.426 15.316 79 13.059 79 10.8c0-1.696-1.691-3.012-4.512-4.14.563.374 1.125 1.128 1.125 1.882 0 2.258-3.383 3.95-9.965 5.457-6.207 1.316-14.101 2.258-23.695 2.258-9.21 0-17.488-.942-23.504-2.258-6.394-1.695-9.777-3.387-9.777-5.457 0-.941.375-1.695 1.691-2.637-3.949 1.696-6.207 2.824-6.207 4.895.188 2.258 3.762 4.515 11.285 6.21zm0 0" fill="#28334c"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/coffeelintignore.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M54.633 9.367C42.145-3.12 21.855-3.12 9.367 9.367s-12.488 32.778 0 45.266 32.778 12.488 45.266 0 12.488-32.778 0-45.266zM12.176 44.801c-5.934-9.211-4.84-21.543 3.12-29.504s20.294-9.055 29.505-3.121zm7.023 7.023L51.824 19.2c5.934 9.211 4.84 21.543-3.12 29.504s-20.294 9.055-29.505 3.121zm0 0" fill="#bababa"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/com.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#999"><path d="M64 0H0v64h64zM12.8 12.633H6.399V6.23h6.403zm44.802 0h-38.57V6.23h38.57zm0 44.797H6.23V19.2h51.372zm0 0"/><path d="m16.336 24.59-4.547 4.547 7.41 7.41-7.41 7.242 4.547 4.547 11.957-11.79zm10.613 21.558h12.797v6.399H26.95zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/compile.svg
1
1
<svg height="64" width="68" xmlns="http://www.w3.org/2000/svg"><g fill="#bababa"><path d="M59.906 48.902H0m60.715 7.387V19.035H68v44.8H1.617v-44.8h7.29V56.29zM16.675.164h36.106L34.648 18.543C28.82 12.637 22.668 6.398 16.676.164zm0 0"/><path d="M23.8 33.805v-7.383h7.286v7.383zm22.02 0h-7.285v-7.383h7.285zm-29.468 7.55h7.285v7.383h-7.285zm29.628 7.383v-7.383h7.286v7.383zm-7.609-7.383v7.383h-7.285v-7.383zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/conf.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M41.266 22.992c0-3.945-2.403-7.035-5.664-8.406V3.262C35.602 1.372 34.23 0 32.344 0s-3.262 1.371-3.262 3.262v11.324c-3.43 1.2-5.66 4.46-5.66 8.406 0 3.945 2.402 7.035 5.66 8.406 0 .172-.172.516-.172.688V60.57c0 1.887 1.375 3.258 3.262 3.258s3.258-1.371 3.258-3.258V31.914c0-.344 0-.516-.168-.687 3.601-1.028 6.004-4.29 6.004-8.235zm-9.094 2.574c-1.371 0-2.402-1.03-2.402-2.402 0-1.375 1.03-2.402 2.402-2.402s2.402 1.027 2.402 2.402c.172 1.2-1.031 2.402-2.402 2.402zM58.254 3.602c0-1.887-1.375-3.258-3.262-3.258s-3.262 1.37-3.262 3.258v26.597c-3.43 1.2-5.66 4.461-5.66 8.406 0 3.946 2.403 7.036 5.66 8.407 0 .172-.171.515-.171.687v13.04c0 1.89 1.375 3.261 3.261 3.261 1.887 0 3.262-1.371 3.262-3.262V47.7c0-.344 0-.515-.172-.687 3.43-1.2 5.66-4.461 5.66-8.407 0-3.945-2.402-7.035-5.66-8.406V3.602zm-3.262 37.406c-1.37 0-2.402-1.028-2.402-2.403 0-1.37 1.031-2.402 2.402-2.402 1.371 0 2.403 1.031 2.403 2.402 0 1.375-1.032 2.403-2.403 2.403zm-48.73 19.39c0 1.887 1.375 3.258 3.261 3.258 1.887 0 3.258-1.37 3.258-3.258V47.355c0-.343 0-.511-.172-.683 3.434-1.203 5.664-4.461 5.664-8.41 0-3.946-2.402-7.035-5.664-8.407V3.602c0-1.887-1.37-3.258-3.257-3.258S6.09 1.714 6.09 3.602v26.597C2.66 31.4.43 34.66.43 38.605c0 3.946 2.402 7.036 5.66 8.407 0 .172-.172.515-.172.687v13.04c0-.34.344-.34.344-.34zm3.261-24.367c1.372 0 2.403 1.032 2.403 2.403 0 1.375-1.031 2.402-2.403 2.402-1.375 0-2.402-1.027-2.402-2.402 0-1.371 1.027-2.403 2.402-2.403zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/config.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M41.266 22.992c0-3.945-2.403-7.035-5.664-8.406V3.262C35.602 1.372 34.23 0 32.344 0s-3.262 1.371-3.262 3.262v11.324c-3.43 1.2-5.66 4.46-5.66 8.406 0 3.945 2.402 7.035 5.66 8.406 0 .172-.172.516-.172.688V60.57c0 1.887 1.375 3.258 3.262 3.258s3.258-1.371 3.258-3.258V31.914c0-.344 0-.516-.168-.687 3.601-1.028 6.004-4.29 6.004-8.235zm-9.094 2.574c-1.371 0-2.402-1.03-2.402-2.402 0-1.375 1.03-2.402 2.402-2.402s2.402 1.027 2.402 2.402c.172 1.2-1.031 2.402-2.402 2.402zM58.254 3.602c0-1.887-1.375-3.258-3.262-3.258s-3.262 1.37-3.262 3.258v26.597c-3.43 1.2-5.66 4.461-5.66 8.406 0 3.946 2.403 7.036 5.66 8.407 0 .172-.171.515-.171.687v13.04c0 1.89 1.375 3.261 3.261 3.261 1.887 0 3.262-1.371 3.262-3.262V47.7c0-.344 0-.515-.172-.687 3.43-1.2 5.66-4.461 5.66-8.407 0-3.945-2.402-7.035-5.66-8.406V3.602zm-3.262 37.406c-1.37 0-2.402-1.028-2.402-2.403 0-1.37 1.031-2.402 2.402-2.402 1.371 0 2.403 1.031 2.403 2.402 0 1.375-1.032 2.403-2.403 2.403zm-48.73 19.39c0 1.887 1.375 3.258 3.261 3.258 1.887 0 3.258-1.37 3.258-3.258V47.355c0-.343 0-.511-.172-.683 3.434-1.203 5.664-4.461 5.664-8.41 0-3.946-2.402-7.035-5.664-8.407V3.602c0-1.887-1.37-3.258-3.257-3.258S6.09 1.714 6.09 3.602v26.597C2.66 31.4.43 34.66.43 38.605c0 3.946 2.402 7.036 5.66 8.407 0 .172-.172.515-.172.687v13.04c0-.34.344-.34.344-.34zm3.261-24.367c1.372 0 2.403 1.032 2.403 2.403 0 1.375-1.031 2.402-2.403 2.402-1.375 0-2.402-1.027-2.402-2.402 0-1.371 1.027-2.403 2.402-2.403zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cpp.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="43"><path style="stroke:none;fill-rule:nonzero;fill:#999;fill-opacity:1" d="M21.02 0c9.93 0 14.718 5.578 14.718 5.578l-4.433 9.715s-3.903-3.957-9.399-3.957c-6.738 0-9.93 4.676-9.93 10.074 0 5.395 3.368 10.434 9.93 10.434 6.207 0 9.754-4.856 9.754-4.856l5.32 9.356S31.836 43 21.195 43C8.605 43 .273 34.004.273 21.59.093 9.355 8.605 0 21.02 0zm19.152 18.531h7.094v-8.093h5.851v8.093h7.094v6.117h-7.094v8.098h-5.851v-8.098h-7.094zm22.523 0h7.094v-8.093h5.852v8.093h7.093v6.117h-7.093v8.098h-6.032v-8.098h-7.093zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cptx.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M11.9 28C4.5 28 1 21.501 1 14.5 1 7.5 4.4 1 11.9 1c2.3 0 4.2.6 5.6 1.4l-1 2.1c-1-.6-2.7-1.1-4.2-1.1-5.2 0-7.2 5.5-7.2 11 0 5.4 2.101 10.9 7.2 10.9 1.6 0 3.1-.5 4.2-1.1l1 2.3c-1.4 1.1-3.2 1.5-5.6 1.5zM29.1 28c-1.302 0-2.702-.2-3.5-.501v8.4H22V8.2c1.9-1 4.4-1.4 7-1.4 6.5 0 10 4 10 10.3C39 23.9 35.1 28 29.1 28zM28.8 8.8c-1.1 0-2.4.199-3.2.601v16.1c.7.198 1.601.4 2.799.4 4.601 0 7.002-3.102 7.002-8.6-.102-5.201-1.9-8.5-6.601-8.5zm0 0" fill="#63b763" stroke="#63b763" stroke-miterlimit="10" stroke-width="2" transform="matrix(1.725 0 0 1.72973 0 .086)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cr2.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/crt.svg
1
1
<svg height="64" width="52" xmlns="http://www.w3.org/2000/svg"><path d="M48.793 26.879h-4.629V18.05C44.164 7.988 35.973.043 26 .043S7.836 8.164 7.836 18.051v8.828H3.207A3.181 3.181 0 0 0 0 30.059V60.78c0 1.762 1.426 3.176 3.207 3.176h45.586c1.781 0 3.207-1.414 3.207-3.176V29.883c0-1.59-1.426-3.004-3.207-3.004zM29.918 52.305c.355 1.058-.535 1.941-1.602 1.941h-4.808c-1.07 0-1.781-1.059-1.606-1.941l1.426-5.649c-1.781-.883-3.027-2.648-3.027-4.945 0-3 2.492-5.473 5.52-5.473 3.027 0 5.523 2.473 5.523 5.473 0 2.117-1.246 4.062-3.028 4.945zm5.164-25.426H16.918V18.05c0-4.942 4.098-9.004 9.082-9.004s9.082 4.062 9.082 9.004zm0 0" fill="#696"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/crypt.svg
1
1
<svg height="64" width="52" xmlns="http://www.w3.org/2000/svg"><path d="M48.793 26.879h-4.629V18.05C44.164 7.988 35.973.043 26 .043S7.836 8.164 7.836 18.051v8.828H3.207A3.181 3.181 0 0 0 0 30.059V60.78c0 1.762 1.426 3.176 3.207 3.176h45.586c1.781 0 3.207-1.414 3.207-3.176V29.883c0-1.59-1.426-3.004-3.207-3.004zM29.918 52.305c.355 1.058-.535 1.941-1.602 1.941h-4.808c-1.07 0-1.781-1.059-1.606-1.941l1.426-5.649c-1.781-.883-3.027-2.648-3.027-4.945 0-3 2.492-5.473 5.52-5.473 3.027 0 5.523 2.473 5.523 5.473 0 2.117-1.246 4.062-3.028 4.945zm5.164-25.426H16.918V18.05c0-4.942 4.098-9.004 9.082-9.004s9.082 4.062 9.082 9.004zm0 0" fill="#a03537"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cs.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#d5006e"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/csh.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M13.875 13.874h10.9v2.701h-10.9zm0 5.4h10.9v2.701h-10.9zm0 5.5h10.9v2.702h-10.9zm19-24.399H11.177c-3 0-5.402 2.4-5.402 5.4v24.4h-5.4c0 3 2.402 5.4 5.4 5.4h21.7c3 0 5.402-2.4 5.402-5.4v-21.7h5.4v-2.7c0-3-2.402-5.4-5.4-5.4zm-2.7 29.3c0 1.801-1.4 3.2-3.2 3.2h-19.9c1.3-.9 1.3-2.7 1.3-2.7v-24.4c0-1.5 1.2-2.7 2.7-2.7 1.501 0 2.7 1.2 2.7 2.7v2.7h16.3zm-13.6-23.9v-2.7h16.3c2.501 0 2.7 1.6 2.7 2.7zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".75" transform="matrix(1.7717 0 0 1.78025 .262 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cson.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="stroke:none;fill-rule:nonzero;fill:#999;fill-opacity:1" d="M.125 0h69.586v8.184H.125zm13.164 18.273h69.586v8.18H13.289zM.125 36.543h69.586v8.184H.125zm13.164 18.273h69.586V63H13.289zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/css.svg
1
1
<svg height="64" width="75" xmlns="http://www.w3.org/2000/svg"><path d="M.5 19v-4.1c.9-.1 1.6-.2 2-.4.4-.2.8-.6 1.2-1.001.4-.5.5-1.1.7-1.9.1-.6.2-1.499.2-2.799 0-2.201.1-3.7.4-4.6.2-.8.6-1.6 1.2-2 .5-.5 1.4-.9 2.5-1.2.7-.2 1.9-.4 3.5-.4h.9v3.9c-1.3 0-2.2.1-2.6.3-.4.2-.6.4-.9.6-.2.3-.3.7-.3 1.501 0 .8-.1 2-.2 4.099-.101 1.2-.2 2-.4 2.801-.301.6-.6 1.2-1 1.8-.4.4-1 .9-1.8 1.399.7.4 1.3.8 1.8 1.3s.8 1.2 1.1 1.899c.3.702.4 1.802.4 3.001.1 1.9.1 3.1.1 3.599 0 .702.1 1.202.3 1.602.2.4.5.5.9.6.4.2 1.2.3 2.6.3v4.098h-1c-1.6 0-2.9-.1-3.701-.4-.9-.3-1.6-.6-2.2-1.2-.6-.6-.999-1.2-1.2-1.999-.198-.8-.299-2.1-.299-4 0-2-.1-3.5-.3-4.1-.3-.9-.7-1.601-1.201-2-.698-.5-1.5-.7-2.7-.7zm39.1 0c-.9.1-1.6.2-2 .4s-.8.6-1.2 1.001c-.4.5-.5 1.1-.7 1.9-.099.6-.2 1.499-.2 2.799 0 2.201-.1 3.7-.4 4.6-.2.9-.6 1.6-1.2 2-.5.5-1.4.9-2.5 1.2-.7.2-1.9.4-3.5.4h-.999v-4.1c1.298 0 2.1-.1 2.599-.3s.7-.4.899-.6c.2-.3.301-.7.301-1.501 0-.6.1-2 .2-3.999.099-1.2.3-2.1.5-2.8.3-.7.6-1.3 1.1-1.9.4-.5 1-.9 1.7-1.3-.901-.6-1.6-1.1-2-1.6-.5-.7-1-1.801-1.201-2.8-.199-.8-.299-2.6-.299-5.2 0-.8-.1-1.4-.301-1.8-.199-.3-.4-.5-.799-.6-.2-.3-1-.3-2.5-.3v-4h.999c1.602 0 2.9.1 3.7.4.902.3 1.6.6 2.2 1.2.6.6 1.002 1.2 1.2 2 .201.8.402 2.1.402 4 0 2 .098 3.4.299 4.1.299.9.7 1.601 1.2 1.9.5.4 1.401.6 2.5.6.1.1 0 4.3 0 4.3zm0 0" fill="#72a536" stroke="#72a536" stroke-miterlimit="10" transform="matrix(1.86825 0 0 1.87558 0 .209)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/csv.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="52"><path style="fill:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1f7244;stroke-opacity:1;stroke-miterlimit:10" d="M0 1.5h8.5m3.3 0h8.5m3.4 0h8.5m3.3 0H44M0 7.4h8.5m3.3 0h8.5m3.4 0h8.5m3.3 0H44M0 13.3h8.5m3.3 0h8.5m3.4 0h8.5m3.3 0H44M0 19.2h8.5m3.3 0h8.5m3.4 0h8.5m3.3 0H44M0 25.1h8.5m3.3 0h8.5m3.4 0h8.5m3.3 0H44" transform="matrix(1.9091 0 0 1.92593 0 .385)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cue.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#eab41b"><path d="M28.023 32c0 1.04.344 2.074 1.211 2.766 1.555 1.558 4.149 1.558 5.707 0 .692-.692 1.211-1.727 1.211-2.766s-.347-2.074-1.21-2.766c-.692-.695-1.731-1.21-2.77-1.21-1.035 0-2.074.343-2.766 1.21-1.039.692-1.383 1.727-1.383 2.766zm0 0"/><path d="M9.34 9.34c-12.453 12.453-12.453 32.691 0 45.32 12.453 12.453 32.691 12.453 45.32 0 12.453-12.453 12.453-32.691 0-45.32-12.453-12.453-32.867-12.453-45.32 0zm47.394 36.152c-1.21 2.074-2.765 4.153-4.496 5.88-1.73 1.73-3.804 3.288-5.883 4.5l-7.437-14.184s.691-.176 2.078-1.56c1.383-1.382 1.727-2.073 1.727-2.073zM37.707 26.293c1.559 1.555 2.422 3.633 2.422 5.707s-.863 4.152-2.422 5.707a7.933 7.933 0 0 1-11.242 0c-1.559-1.555-2.422-3.633-2.422-5.707s.691-4.152 2.422-5.707c2.941-3.113 8.129-3.113 11.242 0zm-10.895-5.535s-1.558.863-2.769 2.246c-1.211 1.387-1.211 1.558-1.73 2.25l-14.184-7.61c1.21-2.078 2.77-4.152 4.5-5.882 1.902-1.73 3.805-3.285 5.879-4.496zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/cur.svg
1
1
<svg height="64" width="55" xmlns="http://www.w3.org/2000/svg"><path d="M54.652 53.883 41.801 64 27.289 46.172l-9.3 11.351L.347 0l53.07 29.219-13.277 6.836zm0 0" fill="#8ed200"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dart.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="m10.98 2.23 12.672 10.356c-.355-3.75-1.96-7.856-4.46-10.356-1.786-1.785-3.391-2.5-5-2.14-1.426.18-2.5 1.07-3.212 2.14zM2.23 19.191c2.68 2.676 6.786 4.106 10.536 4.461L2.41 10.98c-1.25.891-2.14 1.786-2.32 3.211-.36 1.61.355 3.215 2.14 5zm51.06 22.672L41.862 53.29c1.43 1.43 3.75 2.676 6.07 3.035.715.18 1.25.18 1.965.18 1.07 0 2.141-.18 3.036-.715l7.675 7.676c.356.355.891.535 1.426.535s1.07-.18 1.43-.535c.715-.715.715-2.145 0-2.856l-7.676-7.675c1.606-3.575 0-8.57-2.5-11.07zM4.91 7.766l34.274 42.488 11.07-11.07L7.766 4.91c-.715-.715-1.965-.535-2.68.176-.89.715-.89 1.789-.176 2.68zm0 0" fill="#0091ea"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dat.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M17.8.5c-2.9 0-5.4 2.801-5.4 6.2 0 3.4 2.4 6.2 5.4 6.2 2.9 0 5.399-2.8 5.399-6.2C23.199 3.302 20.8.5 17.8.5zm0 10.1c-1.6 0-3-1.7-3-3.9 0-2.1 1.3-3.9 3-3.9s3 1.7 3 3.9-1.3 3.9-3 3.9zM7 11.8V1.7C7 1 6.5.5 5.8.5S4.6 1 4.6 1.7v10.1c0 .7.5 1.2 1.2 1.2S7 12.4 7 11.8zm-1.1 6.9C3 18.7.5 21.5.5 24.9s2.4 6.2 5.4 6.2 5.401-2.8 5.401-6.2c-.102-3.3-2.5-6.2-5.4-6.2zm0 10.2c-1.6 0-3-1.699-3-3.9 0-2.1 1.3-3.9 3-3.9s3 1.7 3 3.9c-.1 2.1-1.4 3.9-3 3.9zM19 30V19.9c0-.7-.5-1.2-1.2-1.2s-1.2.5-1.2 1.2V30c0 .7.5 1.2 1.2 1.2S19 30.7 19 30zM31.3 12.7V2.6c0-.7-.499-1.2-1.2-1.2-.7 0-1.1.5-1.1 1.2v10.099c0 .701.5 1.2 1.2 1.2s1.1-.6 1.1-1.2zm-1.2 6.9c-2.9 0-5.401 2.8-5.401 6.2 0 3.4 2.4 6.202 5.4 6.202 2.901 0 5.402-2.802 5.402-6.202S33.1 19.6 30.1 19.6zm0 10.102c-1.6 0-3-1.7-3-3.902 0-2.099 1.3-3.9 3-3.9s3 1.7 3 3.9c0 2.202-1.3 3.902-3 3.902zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" transform="matrix(1.91667 0 0 1.9394 0 .485)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/data.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M17.8.5c-2.9 0-5.4 2.801-5.4 6.2 0 3.4 2.4 6.2 5.4 6.2 2.9 0 5.399-2.8 5.399-6.2C23.199 3.302 20.8.5 17.8.5zm0 10.1c-1.6 0-3-1.7-3-3.9 0-2.1 1.3-3.9 3-3.9s3 1.7 3 3.9-1.3 3.9-3 3.9zM7 11.8V1.7C7 1 6.5.5 5.8.5S4.6 1 4.6 1.7v10.1c0 .7.5 1.2 1.2 1.2S7 12.4 7 11.8zm-1.1 6.9C3 18.7.5 21.5.5 24.9s2.4 6.2 5.4 6.2 5.401-2.8 5.401-6.2c-.102-3.3-2.5-6.2-5.4-6.2zm0 10.2c-1.6 0-3-1.699-3-3.9 0-2.1 1.3-3.9 3-3.9s3 1.7 3 3.9c-.1 2.1-1.4 3.9-3 3.9zM19 30V19.9c0-.7-.5-1.2-1.2-1.2s-1.2.5-1.2 1.2V30c0 .7.5 1.2 1.2 1.2S19 30.7 19 30zM31.3 12.7V2.6c0-.7-.499-1.2-1.2-1.2-.7 0-1.1.5-1.1 1.2v10.099c0 .701.5 1.2 1.2 1.2s1.1-.6 1.1-1.2zm-1.2 6.9c-2.9 0-5.401 2.8-5.401 6.2 0 3.4 2.4 6.202 5.4 6.202 2.901 0 5.402-2.802 5.402-6.202S33.1 19.6 30.1 19.6zm0 10.102c-1.6 0-3-1.7-3-3.902 0-2.099 1.3-3.9 3-3.9s3 1.7 3 3.9c0 2.202-1.3 3.902-3 3.902zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" transform="matrix(1.91667 0 0 1.9394 0 .485)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/db.svg
1
1
<svg height="64" width="51" xmlns="http://www.w3.org/2000/svg"><path d="M23.023 63.957c-8.199-.34-15.543-2.875-19.468-6.77-1.196-1.011-2.39-2.535-2.903-3.55L.31 52.96v-7.617c0-7.614 0-7.614.171-6.934.34 1.692 1.368 3.383 2.903 4.735 1.023.847 3.074 2.37 4.781 3.214 2.906 1.524 6.66 2.54 10.59 3.047 2.39.34 3.246.34 6.66.34 3.418 0 4.27 0 6.66-.34 3.93-.508 7.516-1.691 10.59-3.047 1.707-.843 3.758-2.199 4.781-3.214 1.368-1.352 2.563-3.043 2.903-4.735.172-.508.172-.508.172 6.934v7.445l-.34.68c-1.196 2.367-3.246 4.398-5.98 6.09-5.294 3.046-13.321 4.738-21.177 4.398zm0-18.95c-7.171-.339-13.832-2.37-18.101-5.413-1.027-.68-2.39-2.032-2.906-2.707-.512-.68-1.024-1.524-1.364-2.371L.31 33.84v-7.445c0-7.446 0-7.446.171-6.938.34 1.184.852 2.54 1.88 3.555.511.675 1.367 1.523 1.878 1.86.168.171.684.339 1.024.679 3.414 2.367 8.199 4.058 13.664 4.906 2.39.336 3.242.336 6.66.336 3.414 0 4.27 0 6.66-.336 3.93-.508 7.516-1.691 10.59-3.047 1.707-.847 3.758-2.2 4.781-3.215 1.367-1.351 2.39-3.047 2.903-4.738.171-.508.171-.508.171 6.938v7.445l-.511 1.015c-.856 1.524-1.368 2.368-2.39 3.383-1.028 1.016-2.052 1.864-3.419 2.54-5.465 3.046-13.492 4.738-21.348 4.23zm-.511-18.78c-4.782-.34-8.54-1.184-12.125-2.54-4.27-1.69-7.344-3.89-8.883-6.597a5.594 5.594 0 0 1-.852-2.031C.48 14.383.31 12.69.48 11.676 1.504 6.262 8.848 1.859 18.754.34 21.144 0 22 0 25.414 0c3.418 0 4.27 0 6.66.34 3.93.508 7.516 1.691 10.59 3.043 4.441 2.199 7.172 5.078 7.684 8.12.172.849.172 2.708-.168 3.388-.512 1.691-1.196 2.707-2.563 4.058-3.586 3.723-9.906 6.094-17.762 6.938-1.023.34-6.32.34-7.343.34zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dbf.svg
1
1
<svg height="64" width="51" xmlns="http://www.w3.org/2000/svg"><path d="M23.023 63.957c-8.199-.34-15.543-2.875-19.468-6.77-1.196-1.011-2.39-2.535-2.903-3.55L.31 52.96v-7.617c0-7.614 0-7.614.171-6.934.34 1.692 1.368 3.383 2.903 4.735 1.023.847 3.074 2.37 4.781 3.214 2.906 1.524 6.66 2.54 10.59 3.047 2.39.34 3.246.34 6.66.34 3.418 0 4.27 0 6.66-.34 3.93-.508 7.516-1.691 10.59-3.047 1.707-.843 3.758-2.199 4.781-3.214 1.368-1.352 2.563-3.043 2.903-4.735.172-.508.172-.508.172 6.934v7.445l-.34.68c-1.196 2.367-3.246 4.398-5.98 6.09-5.294 3.046-13.321 4.738-21.177 4.398zm0-18.95c-7.171-.339-13.832-2.37-18.101-5.413-1.027-.68-2.39-2.032-2.906-2.707-.512-.68-1.024-1.524-1.364-2.371L.31 33.84v-7.445c0-7.446 0-7.446.171-6.938.34 1.184.852 2.54 1.88 3.555.511.675 1.367 1.523 1.878 1.86.168.171.684.339 1.024.679 3.414 2.367 8.199 4.058 13.664 4.906 2.39.336 3.242.336 6.66.336 3.414 0 4.27 0 6.66-.336 3.93-.508 7.516-1.691 10.59-3.047 1.707-.847 3.758-2.2 4.781-3.215 1.367-1.351 2.39-3.047 2.903-4.738.171-.508.171-.508.171 6.938v7.445l-.511 1.015c-.856 1.524-1.368 2.368-2.39 3.383-1.028 1.016-2.052 1.864-3.419 2.54-5.465 3.046-13.492 4.738-21.348 4.23zm-.511-18.78c-4.782-.34-8.54-1.184-12.125-2.54-4.27-1.69-7.344-3.89-8.883-6.597a5.594 5.594 0 0 1-.852-2.031C.48 14.383.31 12.69.48 11.676 1.504 6.262 8.848 1.859 18.754.34 21.144 0 22 0 25.414 0c3.418 0 4.27 0 6.66.34 3.93.508 7.516 1.691 10.59 3.043 4.441 2.199 7.172 5.078 7.684 8.12.172.849.172 2.708-.168 3.388-.512 1.691-1.196 2.707-2.563 4.058-3.586 3.723-9.906 6.094-17.762 6.938-1.023.34-6.32.34-7.343.34zm0 0" fill="#a03537"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/deb.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><path d="m49.332 34.941-12.25-5.714L61.75 17.633 74 23.348l-12.25 5.879zM61.75 6.207 49.5.492 37.25 6.207l24.5 11.594L74 12.086zm-37.082 17.14-12.25-5.714-12.25 5.715L24.836 34.94l12.246-5.714zm0-11.429 12.25-5.711L24.668.492 0 12.086 12.25 17.8zM61.75 32.59l-11.074 5.039-1.344.672-1.34-.672-11.074-5.04-11.078 5.04-1.34.672-1.344-.672-11.074-5.04v17.977L36.75 63.508l25-12.942zm0 0" fill="#4d1b9b"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dgn.svg
1
1
<svg height="63" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M33.325 19.55c-.498-.2-1.1 0-1.299.5-1.1 2.5-2.901 4.7-5.1 6.4l-6.7-13.601c1-.8 1.6-1.999 1.6-3.4 0-2.099-1.501-3.899-3.501-4.3v-3.4a1 1 0 0 0-2 0v3.4c-2 .401-3.5 2.201-3.5 4.3 0 1.401.6 2.6 1.601 3.4l-6.7 13.602c-2.201-1.7-4-3.801-5.1-6.401-.201-.5-.8-.7-1.301-.5-.499.199-.7.8-.499 1.3 1.299 3 3.4 5.4 6 7.3l-4 8c-.2.5 0 1.1.4 1.3.098 0 .3.1.4.1.3 0 .7-.2.9-.5l3.8-7.8c2.7 1.5 5.6 2.2 8.7 2.2 3.1 0 6-.8 8.699-2.2l3.8 7.8c.1.3.501.5.9.5.1 0 .3 0 .4-.1.5-.2.7-.8.4-1.3l-3.9-8c2.6-1.8 4.701-4.4 6-7.3.6-.5.401-1.101 0-1.3zM17.326 6.95c1.4 0 2.5 1.1 2.5 2.499 0 1.401-1.1 2.502-2.5 2.502s-2.5-1.1-2.5-2.502c0-1.4 1.199-2.5 2.5-2.5zm0 22.6c-2.8 0-5.4-.7-7.801-2l6.8-13.7c.3.1.701.1 1.1.1.402 0 .701 0 1.1-.1l6.8 13.7c-2.5 1.3-5.199 2-7.999 2zm0 0" fill="#369" stroke="#369" stroke-miterlimit="10" stroke-width="1.5" transform="matrix(1.6544 0 0 1.63607 0 .154)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dist.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="54"><path style="fill-rule:nonzero;fill:#999;fill-opacity:1;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#999;stroke-opacity:1;stroke-miterlimit:10" d="M11.242 25.867c-.5 0-1.1-.2-1.498-.6l-8.4-8.401c-.802-.799-.802-2.099 0-3l8.4-8.398c.8-.8 2.098-.8 2.999 0 .8.8.8 2.098 0 2.999l-6.9 6.9 6.9 6.9c.8.801.8 2.1 0 3-.401.4-1 .6-1.5.6zm25.1 0c-.499 0-1.099-.2-1.5-.6-.8-.8-.8-2.099 0-3.002l6.9-6.898-6.9-6.9c-.8-.8-.8-2.101 0-3 .8-.8 2.1-.8 3.001 0l8.398 8.4c.802.8.802 2.1 0 3l-8.398 8.4c-.4.4-1.001.6-1.5.6zm-16.7 4.1c-.199 0-.398 0-.698-.1-1.102-.401-1.702-1.5-1.301-2.6l8.398-25.1c.4-1.1 1.5-1.699 2.6-1.301 1.102.4 1.702 1.5 1.301 2.601l-8.398 25.1c-.202.899-1.102 1.4-1.901 1.4zm0 0" transform="matrix(1.74425 0 0 1.75713 0 .013)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/diz.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="60"><path style="stroke:none;fill-rule:nonzero;fill:#bababa;fill-opacity:1" d="M72.277.21c-4.906 0-3.586 4.978-4.53 7.083-.376.77-.946 2.297-1.509 3.64l.375-.765c-.375.762-.566.953-1.32 1.527 0 0-3.398 2.11-6.422 3.832C55.285 9.211 48.863 5.57 41.875 5.57c-6.984 0-13.594 3.828-16.992 9.957-3.207-1.914-6.61-3.832-6.61-3.832-.753-.574-1.128-.765-1.32-1.527l.375.762c-.566-1.34-1.129-2.68-1.508-3.637C15.066 5 16.383.211 11.29.211c-4.915 0-3.97 6.7-5.477 9.379-.946 1.726-3.778 3.445-5.098 5.363-.192.188-.192.379-.192.574-.566.957-.753 2.297.192 3.637 2.453 4.211 6.039.766 8.305.383.945-.192 2.27-.192 3.777-.574l-.945.191c.757-.191 1.136 0 2.078.574 0 0 3.59 2.106 8.308 4.785v1.149c0 3.64.946 7.469 3.024 10.527a656.505 656.505 0 0 0-11.895 7.278c-.758.574-1.137.765-1.89.574h.753-3.585c-2.27 0-5.102-2.68-7.93.957-2.649 3.637 2.828 5.36 3.965 7.469 1.133 2.105-.38 7.843 4.34 7.27 4.535-.571 3.777-4.403 4.91-6.505.566-.765 1.136-1.918 1.699-3.062l-.375.765c.375-.578.754-.765 1.508-1.343 0 0 5.289-2.868 11.332-6.508v8.804c1.699.766 3.398 1.153 5.097 1.532v-4.785l2.454.574v4.785c1.699.387 3.402.578 5.101.578v-4.789h2.266v4.598c1.695 0 3.398-.196 5.097-.578v-4.786l2.453-.574v4.977c1.7-.38 3.399-.766 5.098-1.532v-8.804a396.942 396.942 0 0 1 11.332 6.508c.754.578 1.133.765 1.512 1.343.566.762.941 1.72 1.32 2.297.946 1.914.375 5.934 4.91 6.504 4.536.578 3.211-5.164 4.344-7.27 1.133-2.109 6.61-3.64 3.965-7.468-2.645-3.637-5.664-1.149-7.93-.957h-3.59.754c-.754 0-.941 0-1.886-.574 0 0-5.477-3.446-11.899-7.278 2.078-3.25 3.024-6.695 3.024-10.527v-1.149c4.722-2.68 8.308-4.785 8.308-4.785.754-.574 1.32-.574 2.078-.574l-.57-.191c1.516.191 2.836.382 3.781.574 2.266.383 5.852 3.828 8.309-.383 2.453-4.21-3.59-6.89-5.098-9.574-.945-1.719-.758-5.164-2.078-7.27 0-.195-.191-.386-.191-.386C74.735.785 73.793.402 72.277.21zM34.324 23.376c.567 0 .758 0 1.32.191 2.645.766 3.403 3.254 2.645 6.125-.754 2.684-3.586 4.403-6.23 3.637-2.645-.766-3.59-3.445-2.645-6.121.57-2.3 2.645-3.832 4.91-3.832zm14.73 0c2.266 0 4.532 1.531 5.098 3.637.754 2.68 0 5.172-2.644 6.129-2.645.761-5.477-.77-6.23-3.641-.755-2.68 0-5.168 2.644-6.125a1.19 1.19 0 0 0 1.133 0zm-7.363 12.824c.754 0 3.961 4.215 3.586 4.79-.379.574-6.797.574-7.175 0-.38-.575 3.02-4.79 3.59-4.79zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dll.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M67 .445 36.203 4.63v24.55H67zM30.797 5.172 0 9.355V29.18h30.797zM.18 34.82v19.825l30.797 4.183V35zm36.023 0v24.551L67 63.555V34.82zm0 0" fill="#666"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dmg.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M57.602 41.688c2.488 0 4.62 1.066 6.218 2.492L58.488 5.422C58.133 2.222 55.29.09 52.266.09H11.734c-3.199 0-5.867 2.133-6.402 5.332L0 44.18c1.777-1.426 3.91-2.493 6.398-2.493zm0 3.023H6.398A6.372 6.372 0 0 0 0 51.109v6.403a6.372 6.372 0 0 0 6.398 6.398h51.204A6.372 6.372 0 0 0 64 57.512v-6.403a6.372 6.372 0 0 0-6.398-6.398zM52.09 56.977h-3.91a1.97 1.97 0 0 1-1.957-1.954c0-1.066.886-1.957 1.957-1.957h3.91c1.066 0 1.953.891 1.953 1.957a1.97 1.97 0 0 1-1.953 1.954zm5.156 0a1.972 1.972 0 0 1-1.957-1.954c0-1.066.89-1.957 1.957-1.957s1.953.891 1.953 1.957c.18 1.067-.71 1.954-1.953 1.954zm0 0" fill="#4d1b9b"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dng.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/doc.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="60"><path style="stroke:none;fill-rule:nonzero;fill:#1a75ce;fill-opacity:1" d="m12.762 33.262-8.39-26.09c-.349-1.059-.524-1.41-.7-1.41-.176-.176-.352-.176-.527-.352l-2.97-.883L0 .824h15.734l.348 3.703-2.973.883v.352c0 .351.176 1.058.528 1.761L16.78 17.57 22.38.824 26.57.648l5.07 16.747 3.497-10.051c.175-.703.527-1.41.527-1.762V5.41l-2.621-.707-.176-3.879h12.235l.351 3.703-3.32 1.059c-.176 0-.352.176-.528.176 0 .176-.347.351-.523 1.234l-9.266 25.91-4.37.356-4.716-16.043-5.597 15.687zm0 0"/><path style="fill-rule:nonzero;fill:#1a75ce;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1a75ce;stroke-opacity:1;stroke-miterlimit:10" d="M42.4 48.6H60v2.8H42.4zm0 7.401H60V58.8H42.4zm0 7.7H60V66.5H42.4zm-29.4 7.8h47v2.798H13zm0 7.598h47v2.8H13zm0 0" transform="matrix(1.74818 0 0 1.76287 -21.328 -85.027)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/docb.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="60"><path style="stroke:none;fill-rule:nonzero;fill:#1a75ce;fill-opacity:1" d="m12.762 33.262-8.39-26.09c-.349-1.059-.524-1.41-.7-1.41-.176-.176-.352-.176-.527-.352l-2.97-.883L0 .824h15.734l.348 3.703-2.973.883v.352c0 .351.176 1.058.528 1.761L16.78 17.57 22.38.824 26.57.648l5.07 16.747 3.497-10.051c.175-.703.527-1.41.527-1.762V5.41l-2.621-.707-.176-3.879h12.235l.351 3.703-3.32 1.059c-.176 0-.352.176-.528.176 0 .176-.347.351-.523 1.234l-9.266 25.91-4.37.356-4.716-16.043-5.597 15.687zm0 0"/><path style="fill-rule:nonzero;fill:#1a75ce;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1a75ce;stroke-opacity:1;stroke-miterlimit:10" d="M42.4 48.6H60v2.8H42.4zm0 7.401H60V58.8H42.4zm0 7.7H60V66.5H42.4zm-29.4 7.8h47v2.798H13zm0 7.598h47v2.8H13zm0 0" transform="matrix(1.74818 0 0 1.76287 -21.328 -85.027)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/docm.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="60"><path style="stroke:none;fill-rule:nonzero;fill:#1a75ce;fill-opacity:1" d="m12.762 33.262-8.39-26.09c-.349-1.059-.524-1.41-.7-1.41-.176-.176-.352-.176-.527-.352l-2.97-.883L0 .824h15.734l.348 3.703-2.973.883v.352c0 .351.176 1.058.528 1.761L16.78 17.57 22.38.824 26.57.648l5.07 16.747 3.497-10.051c.175-.703.527-1.41.527-1.762V5.41l-2.621-.707-.176-3.879h12.235l.351 3.703-3.32 1.059c-.176 0-.352.176-.528.176 0 .176-.347.351-.523 1.234l-9.266 25.91-4.37.356-4.716-16.043-5.597 15.687zm0 0"/><path style="fill-rule:nonzero;fill:#1a75ce;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1a75ce;stroke-opacity:1;stroke-miterlimit:10" d="M42.4 48.6H60v2.8H42.4zm0 7.401H60V58.8H42.4zm0 7.7H60V66.5H42.4zm-29.4 7.8h47v2.798H13zm0 7.598h47v2.8H13zm0 0" transform="matrix(1.74818 0 0 1.76287 -21.328 -85.027)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/docx.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="60"><path style="stroke:none;fill-rule:nonzero;fill:#1a75ce;fill-opacity:1" d="m12.762 33.262-8.39-26.09c-.349-1.059-.524-1.41-.7-1.41-.176-.176-.352-.176-.527-.352l-2.97-.883L0 .824h15.734l.348 3.703-2.973.883v.352c0 .351.176 1.058.528 1.761L16.78 17.57 22.38.824 26.57.648l5.07 16.747 3.497-10.051c.175-.703.527-1.41.527-1.762V5.41l-2.621-.707-.176-3.879h12.235l.351 3.703-3.32 1.059c-.176 0-.352.176-.528.176 0 .176-.347.351-.523 1.234l-9.266 25.91-4.37.356-4.716-16.043-5.597 15.687zm0 0"/><path style="fill-rule:nonzero;fill:#1a75ce;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1a75ce;stroke-opacity:1;stroke-miterlimit:10" d="M42.4 48.6H60v2.8H42.4zm0 7.401H60V58.8H42.4zm0 7.7H60V66.5H42.4zm-29.4 7.8h47v2.798H13zm0 7.598h47v2.8H13zm0 0" transform="matrix(1.74818 0 0 1.76287 -21.328 -85.027)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dot.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="60"><path style="stroke:none;fill-rule:nonzero;fill:#1a75ce;fill-opacity:1" d="m12.762 33.262-8.39-26.09c-.349-1.059-.524-1.41-.7-1.41-.176-.176-.352-.176-.527-.352l-2.97-.883L0 .824h15.734l.348 3.703-2.973.883v.352c0 .351.176 1.058.528 1.761L16.78 17.57 22.38.824 26.57.648l5.07 16.747 3.497-10.051c.175-.703.527-1.41.527-1.762V5.41l-2.621-.707-.176-3.879h12.235l.351 3.703-3.32 1.059c-.176 0-.352.176-.528.176 0 .176-.347.351-.523 1.234l-9.266 25.91-4.37.356-4.716-16.043-5.597 15.687zm0 0"/><path style="fill-rule:nonzero;fill:#1a75ce;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1a75ce;stroke-opacity:1;stroke-miterlimit:10" d="M42.4 48.6H60v2.8H42.4zm0 7.401H60V58.8H42.4zm0 7.7H60V66.5H42.4zm-29.4 7.8h47v2.798H13zm0 7.598h47v2.8H13zm0 0" transform="matrix(1.74818 0 0 1.76287 -21.328 -85.027)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dotm.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="60"><path style="stroke:none;fill-rule:nonzero;fill:#1a75ce;fill-opacity:1" d="m12.762 33.262-8.39-26.09c-.349-1.059-.524-1.41-.7-1.41-.176-.176-.352-.176-.527-.352l-2.97-.883L0 .824h15.734l.348 3.703-2.973.883v.352c0 .351.176 1.058.528 1.761L16.78 17.57 22.38.824 26.57.648l5.07 16.747 3.497-10.051c.175-.703.527-1.41.527-1.762V5.41l-2.621-.707-.176-3.879h12.235l.351 3.703-3.32 1.059c-.176 0-.352.176-.528.176 0 .176-.347.351-.523 1.234l-9.266 25.91-4.37.356-4.716-16.043-5.597 15.687zm0 0"/><path style="fill-rule:nonzero;fill:#1a75ce;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1a75ce;stroke-opacity:1;stroke-miterlimit:10" d="M42.4 48.6H60v2.8H42.4zm0 7.401H60V58.8H42.4zm0 7.7H60V66.5H42.4zm-29.4 7.8h47v2.798H13zm0 7.598h47v2.8H13zm0 0" transform="matrix(1.74818 0 0 1.76287 -21.328 -85.027)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dotx.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="60"><path style="stroke:none;fill-rule:nonzero;fill:#1a75ce;fill-opacity:1" d="m12.762 33.262-8.39-26.09c-.349-1.059-.524-1.41-.7-1.41-.176-.176-.352-.176-.527-.352l-2.97-.883L0 .824h15.734l.348 3.703-2.973.883v.352c0 .351.176 1.058.528 1.761L16.78 17.57 22.38.824 26.57.648l5.07 16.747 3.497-10.051c.175-.703.527-1.41.527-1.762V5.41l-2.621-.707-.176-3.879h12.235l.351 3.703-3.32 1.059c-.176 0-.352.176-.528.176 0 .176-.347.351-.523 1.234l-9.266 25.91-4.37.356-4.716-16.043-5.597 15.687zm0 0"/><path style="fill-rule:nonzero;fill:#1a75ce;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1a75ce;stroke-opacity:1;stroke-miterlimit:10" d="M42.4 48.6H60v2.8H42.4zm0 7.401H60V58.8H42.4zm0 7.7H60V66.5H42.4zm-29.4 7.8h47v2.798H13zm0 7.598h47v2.8H13zm0 0" transform="matrix(1.74818 0 0 1.76287 -21.328 -85.027)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/download.svg
1
1
<svg height="64" width="72" xmlns="http://www.w3.org/2000/svg"><filter id="a" height="100%" width="100%" x="0%" y="0%"><feColorMatrix in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter><mask id="c"><g filter="url(#a)"><path d="M0 0h72v64H0z" fill-opacity=".6"/></g></mask><clipPath id="b"><path d="M0 0h72v64H0z"/></clipPath><mask id="e"><g filter="url(#a)"><path d="M0 0h72v64H0z" fill-opacity=".6"/></g></mask><clipPath id="d"><path d="M0 0h72v64H0z"/></clipPath><g clip-path="url(#b)" mask="url(#c)"><path d="M67.418 37.824C70.199 40.454 72 44.391 72 48.656c0 8.207-6.71 14.934-14.89 14.934-8.184 0-14.891-6.727-14.891-14.934 0-.492 0-1.148.164-1.64.816-7.387 7.199-13.293 14.89-13.293 3.926-.164 7.528 1.64 10.145 4.101zm0 0" fill="#ef806f"/></g><g clip-path="url(#d)" mask="url(#e)"><path d="M68.563 32.082c0 1.148-.165 2.297-.325 3.61-3.11-2.626-7.039-4.102-11.129-4.102-8.672 0-16.035 6.562-17.02 15.262H11.782C5.238 46.852 0 41.602 0 35.035c0-5.086 3.273-9.515 7.691-11.16v-.656c0-6.07 4.91-10.992 10.965-10.992 1.961 0 3.762.492 5.399 1.312C25.69 6.152 32.398.41 40.418.41c9.328 0 16.855 7.547 16.855 16.902v.329c6.543 1.476 11.29 7.386 11.29 14.441zm0 0" fill="#1ea6c6"/></g><path d="m64.965 48.82-7.528 7.715-7.69-7.715h4.581V38.152h6.055V48.82zm0 0" fill="#fff"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dpj.svg
1
1
<svg height="63" width="49" xmlns="http://www.w3.org/2000/svg"><path d="M24.842 21.014c2.2-.7 4.4.7 4.202 2.8-.2 2.4-3.302 3.601-5.1 4.2l.1-.099-.1-.101c1.3-1 3.898-2.199 3.7-4.6-.1-1.2-1.001-2.098-2.702-2v-.2zm-16.099.401.1-.101c-1 0-1.901.4-2.701.7-.798.3-1.8.401-2.198 1.3.399.7 1.4.7 2.398.802 3.4.5 8.302.398 11.701 0 1.799-.201 3.4-.401 4.2-1.201l-.1-.101.1-.1c-3.4.402-7.8 1-11.9.8-1.301-.099-3-.099-3.7-.8.4-.7 1.4-.9 2.1-1.299zm19.9 14.099v-.1c-5.1 2.5-13.201 2.8-20.5 2.201l.1.1-.1.2c2.999.5 6.9.7 10.7.398 3.7-.198 8.199-.698 9.9-2.698zm-14.4-15.398h.1c-.8-1.802-2.3-2.602-2.499-4.7-.2-1.902.7-3.102 1.598-4 1.101-1.201 2.702-2.201 3.901-3.5 1.6-1.803 3.4-4.5 1.9-7.102l-.101.101-.299-.101c.4 2.5-.6 4.101-1.901 5.4-.999 1.201-2.6 2.201-4 3.3-1.6 1.3-3.7 2.901-3.1 5.3.502 2.302 2.8 3.9 4.102 5.4zm8-11.602-.1-.099c-2.7 1-6.701 2.6-7.1 5.698-.1 1.503.399 2.602.9 3.401.4.602 1.1 1 1.3 1.901.2.8 0 1.6-.2 2.2h.1l.1.1c1.099-.8 2.2-1.901 1.899-3.401-.198-1.5-1.9-2.5-2.1-3.899-.1-.802.1-1.5.401-1.9 1.1-1.701 3.5-2.902 4.8-4zm-13.8 17.401-.101-.101c-.5.301-1.5.4-1.4 1.2.1.8 1.5 1 2.2 1.2 3.7.8 9.2.3 11.902-.599l-.1-.101.1-.099c-.301-.101-.7-.7-1.3-.7-.502-.1-1.6.299-2.602.4-1.6.199-3.299.3-4.799.199-1.101-.1-4.5-.1-3.9-1.399zm.9 4.099.1-.1c-.6.201-1.3.4-1.3 1.1 0 .601 1.2 1 1.9 1.3 3.299 1 8.5.4 10.9-.699-.2-.302-.6-.4-.9-.601-.4-.1-.7-.3-1.1-.5-2.001.5-5.1.7-7.5.4-.7-.1-1.701-.1-1.9-.799zm17.699 3.2-.1-.099c-.098 1-1.3 1.1-2.1 1.3-.898.2-1.9.398-2.998.5-4.902.599-11.5.898-16.302 0-.898-.102-2.2-.401-2.499-1.102.4-.698 1.5-.8 2.399-1.198l-.098-.101.098-.1c-1.2.1-2.1.4-2.998.701-.7.3-1.701.698-1.902 1.5.6.8 1.801.8 2.8 1 6.6 1 15.7 1.198 21.402-.7.998-.401 3.098-1 2.1-1.901zm-3.7-5.099c.2 0 .4-.101.702-.2m.898-6.8c-.198 0-.399.1-.7.1m-2.2 1.7c.1 0 .2-.101.401-.101m-12.5-1.6c-.4.1-.8.1-1.3.201m-2.2 15.898c.499.2 1.099.2 1.7.401m20.5-2.2c.1-.1.2-.2.3-.399M19.043.814c0-.099-.098-.3-.098-.399m-4.702 19.7c.1.1.301.4.402.5m2.298 1.199c.1-.1.2-.198.3-.399m5.7-13.2c-.3.099-.499.2-.7.398m-1.198 18.802h.198m-12.6-1.8c0 .1-.2.1-.2.199m.9 4.2.101-.1m-2.8 2.701c-.4 0-.7.1-1 .1m21.399.5c0-.1-.1-.1-.1-.1h-.098" fill="#666" stroke="#666" stroke-miterlimit="10" transform="matrix(1.63519 0 0 1.61722 .336 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ds_store.svg
1
1
<svg height="64" width="72" xmlns="http://www.w3.org/2000/svg"><path d="M67.566 0H4.56C2.125 0 .339 1.953.339 4.234V48.04c0 2.445 1.946 4.234 4.22 4.234h62.843c2.438 0 4.223-1.953 4.223-4.234V4.4C71.949 1.952 70.004 0 67.566 0zm0 0" fill="#ced2d8"/><path d="M4.559 10.586h63.007v37.453H4.56zm0 0" fill="#f2f2f2"/><path d="M17.55 5.7a1.462 1.462 0 1 1-2.921 0 1.462 1.462 0 1 1 2.922 0zm0 0" fill="#54b845"/><path d="M12.516 5.7c0 .808-.653 1.464-1.461 1.464a1.465 1.465 0 0 1 0-2.93c.808 0 1.46.657 1.46 1.465zm0 0" fill="#fbd303"/><path d="M7.809 5.7c0 .808-.657 1.464-1.461 1.464-.809 0-1.461-.656-1.461-1.465 0-.808.652-1.465 1.46-1.465.805 0 1.462.657 1.462 1.465zm0 0" fill="#f0582f"/><path d="m57.5 38.594-4.223-1.137c-.324-1.793-1.136-3.422-1.949-4.887l2.11-3.582c.324-.328.164-.976-.16-1.304L50.19 24.59c-.324-.324-.812-.324-1.3-.164l-3.57 2.117c-1.462-.813-3.087-1.625-4.872-1.953l-1.136-4.07c-.165-.489-.489-.817-.977-.817h-4.223c-.484 0-.808.328-.972.817l-1.301 4.07c-1.785.328-3.41 1.14-4.871 1.953l-3.735-1.953c-.324-.324-.972-.164-1.297.164l-3.085 3.094c-.325.324-.325.812-.164 1.3l2.113 3.586c-.813 1.465-1.625 3.094-1.95 4.883l-4.062.977c-.484.164-.809.488-.809.98v4.23c0 .493.325.817.81.981l4.222 1.137c.324 1.793 1.136 3.422 1.949 4.887l-2.11 3.746c-.324.324-.164.976.16 1.304l3.087 3.094c.324.324.812.324 1.3.16l3.57-2.117c1.462.816 3.087 1.629 4.872 1.957l1.137 4.234c.164.489.488.813.976.813h4.223c.484 0 .812-.324.972-.813l1.137-4.234c1.785-.328 3.41-1.14 4.871-1.957l3.574 2.117c.325.328.973.164 1.297-.16l3.086-3.094c.325-.328.325-.816.164-1.304l-2.113-3.582c.813-1.465 1.625-3.094 1.95-4.887l4.058-.977c.488-.164.812-.488.812-.976v-4.399c.164-.488 0-.812-.484-1.14zM36.062 49.832c-4.382 0-7.957-3.582-7.957-7.98 0-4.395 3.575-7.98 7.957-7.98 4.387 0 7.958 3.585 7.958 7.98 0 4.398-3.57 7.98-7.958 7.98zm0 0" fill="#6eb1e1"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dsn.svg
1
1
<svg height="63" width="62" xmlns="http://www.w3.org/2000/svg"><g fill="#999"><path d="m11.426 26.762 27.386-4.489-1.007-4.32-27.387 4.488zm4.203 18.453 1.008 4.652 11.086-2.16v-4.652zm-1.516-6.985 13.61-2.492v-3.656c0-.332 0-.664.168-.996l-14.786 2.656zm13.61 16.625L13.945 57.68l-9.07-40.89 35.617-5.653 2.856 12.468c.672 0 1.343 0 1.847-.168L39.82 0 .34 5.652 12.434 60.34l15.457-3.156v-.168zm18.312-7.148c-8.566 0-15.625-2.824-15.625-6.148v6.148c0 3.492 7.059 6.152 15.625 6.152 8.57 0 15.625-2.828 15.625-6.152v-6.148c0 3.324-7.055 6.148-15.625 6.148zm0 9.14c-8.566 0-15.625-2.824-15.625-6.148v6.149C30.41 60.34 37.47 63 46.035 63c8.57 0 15.625-2.824 15.625-6.152v-6.149c0 3.492-7.055 6.149-15.625 6.149zm0-30.917c-8.566 0-15.625 2.828-15.625 6.152v6.316c0 3.493 7.059 6.149 15.625 6.149 8.57 0 15.625-2.824 15.625-6.149v-6.152c0-3.488-7.055-6.316-15.625-6.316zm0 0"/><path d="M46.035 36.902c-8.566 0-14.11-2.824-14.11-4.656 0-1.828 5.544-4.652 14.11-4.652 8.57 0 14.113 2.824 14.113 4.652 0 1.832-5.543 4.656-14.113 4.656zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dtd.svg
1
1
<svg height="64" width="76" xmlns="http://www.w3.org/2000/svg"><path d="m14.974 16.374-11-5.5v-.1l11-5.398V1.574L.374 9.375v3l14.6 7.7zm7.502-3.2L23.174.376h-4.798l.698 12.8zm-1.701 8.002c1.6 0 2.701-1.401 2.701-3.102 0-1.898-1.102-3.1-2.701-3.1s-2.701 1.3-2.701 3.1c-.1 1.7 1 3.102 2.701 3.102zm5.8-19.602v3.802l11.2 5.399v.1l-11.2 5.5v3.7l14.6-7.7v-3.1zm-24 23.401h36.5v2.5h-36.5zm0 7.1h36.5v2.5h-36.5zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".75" transform="matrix(1.81211 0 0 1.83119 .353 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dwg.svg
1
1
<svg height="63" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M33.325 19.55c-.498-.2-1.1 0-1.299.5-1.1 2.5-2.901 4.7-5.1 6.4l-6.7-13.601c1-.8 1.6-1.999 1.6-3.4 0-2.099-1.501-3.899-3.501-4.3v-3.4a1 1 0 0 0-2 0v3.4c-2 .401-3.5 2.201-3.5 4.3 0 1.401.6 2.6 1.601 3.4l-6.7 13.602c-2.201-1.7-4-3.801-5.1-6.401-.201-.5-.8-.7-1.301-.5-.499.199-.7.8-.499 1.3 1.299 3 3.4 5.4 6 7.3l-4 8c-.2.5 0 1.1.4 1.3.098 0 .3.1.4.1.3 0 .7-.2.9-.5l3.8-7.8c2.7 1.5 5.6 2.2 8.7 2.2 3.1 0 6-.8 8.699-2.2l3.8 7.8c.1.3.501.5.9.5.1 0 .3 0 .4-.1.5-.2.7-.8.4-1.3l-3.9-8c2.6-1.8 4.701-4.4 6-7.3.6-.5.401-1.101 0-1.3zM17.326 6.95c1.4 0 2.5 1.1 2.5 2.499 0 1.401-1.1 2.502-2.5 2.502s-2.5-1.1-2.5-2.502c0-1.4 1.199-2.5 2.5-2.5zm0 22.6c-2.8 0-5.4-.7-7.801-2l6.8-13.7c.3.1.701.1 1.1.1.402 0 .701 0 1.1-.1l6.8 13.7c-2.5 1.3-5.199 2-7.999 2zm0 0" fill="#369" stroke="#369" stroke-miterlimit="10" stroke-width="1.5" transform="matrix(1.6544 0 0 1.63607 0 .154)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/dxf.svg
1
1
<svg height="63" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M33.325 19.55c-.498-.2-1.1 0-1.299.5-1.1 2.5-2.901 4.7-5.1 6.4l-6.7-13.601c1-.8 1.6-1.999 1.6-3.4 0-2.099-1.501-3.899-3.501-4.3v-3.4a1 1 0 0 0-2 0v3.4c-2 .401-3.5 2.201-3.5 4.3 0 1.401.6 2.6 1.601 3.4l-6.7 13.602c-2.201-1.7-4-3.801-5.1-6.401-.201-.5-.8-.7-1.301-.5-.499.199-.7.8-.499 1.3 1.299 3 3.4 5.4 6 7.3l-4 8c-.2.5 0 1.1.4 1.3.098 0 .3.1.4.1.3 0 .7-.2.9-.5l3.8-7.8c2.7 1.5 5.6 2.2 8.7 2.2 3.1 0 6-.8 8.699-2.2l3.8 7.8c.1.3.501.5.9.5.1 0 .3 0 .4-.1.5-.2.7-.8.4-1.3l-3.9-8c2.6-1.8 4.701-4.4 6-7.3.6-.5.401-1.101 0-1.3zM17.326 6.95c1.4 0 2.5 1.1 2.5 2.499 0 1.401-1.1 2.502-2.5 2.502s-2.5-1.1-2.5-2.502c0-1.4 1.199-2.5 2.5-2.5zm0 22.6c-2.8 0-5.4-.7-7.801-2l6.8-13.7c.3.1.701.1 1.1.1.402 0 .701 0 1.1-.1l6.8 13.7c-2.5 1.3-5.199 2-7.999 2zm0 0" fill="#369" stroke="#369" stroke-miterlimit="10" stroke-width="1.5" transform="matrix(1.6544 0 0 1.63607 0 .154)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/el.svg
1
1
<svg height="63" width="45" xmlns="http://www.w3.org/2000/svg"><path d="M22.686 26.4h.8c0 2.401-.4 4.2-1.2 5.3-.801 1.1-1.8 1.7-3 1.7-1.001 0-1.9-.4-2.8-1.1-.9-.699-1.701-2.7-2.4-5.9l-2-8.9-6.902 15.599h-4.4l9.901-21.2c-.5-2.698-1.2-4.799-1.899-6.098-.701-1.3-1.7-2.002-2.7-2.002-.902 0-1.601.301-2.3 1-.6.7-1 1.701-1.1 3.1h-.8c0-2.299.5-4.1 1.4-5.4.899-1.3 1.898-2 3.2-2 .8 0 1.599.302 2.3 1.002.7.699 1.4 1.799 1.9 3.499.599 1.7 1.4 5.1 2.6 10.3l1.599 7.3c.701 3 1.4 5 2.1 6.1.7 1 1.6 1.5 2.6 1.5 1.9-.1 2.901-1.3 3.101-3.8zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" transform="matrix(1.87615 0 0 1.85407 0 .073)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/elf.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M68.633.102H16.39v7.226H5.738v2.274H0v2.062h5.738v2.684h10.653V26.94H5.738v2.477H0v2.066h5.738v2.27h10.653v13.625H5.738v2.48H0v2.063h5.738v2.273h10.653v9.703h52.242v-9.703h9.629v-2.48H84v-2.063h-5.738v-2.476h-9.63v-13.63h9.63v-2.062H84v-2.066h-5.738v-2.684h-9.63V14.141h9.63v-2.684H84V9.395h-5.738V7.12h-9.63zm-10.04 17.136c-2.253 0-4.097-1.86-4.097-4.129S56.34 8.98 58.594 8.98s4.097 1.86 4.097 4.13c0 2.476-1.843 4.128-4.097 4.128zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/eml.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="60"><path style="fill-rule:nonzero;fill:#7e57c2;fill-opacity:1;stroke-width:.75;stroke-linecap:butt;stroke-linejoin:miter;stroke:#7e57c2;stroke-opacity:1;stroke-miterlimit:10" d="M6.274 25.574h28.3l-9.698-9.3-4.501 3.802-4.5-3.802zm34.1-25.2v28.002H.376V.374zM26.976 14.576l10.7 10.298v-19.3zm-24.2 10.298 10.7-10.298-10.7-9.002zm1.4-21.7 15.9 13.4 15.9-13.4zm0 0" transform="matrix(2.06135 0 0 2.08166 0 .076)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/enc.svg
1
1
<svg height="64" width="52" xmlns="http://www.w3.org/2000/svg"><path d="M48.793 26.879h-4.629V18.05C44.164 7.988 35.973.043 26 .043S7.836 8.164 7.836 18.051v8.828H3.207A3.181 3.181 0 0 0 0 30.059V60.78c0 1.762 1.426 3.176 3.207 3.176h45.586c1.781 0 3.207-1.414 3.207-3.176V29.883c0-1.59-1.426-3.004-3.207-3.004zM29.918 52.305c.355 1.058-.535 1.941-1.602 1.941h-4.808c-1.07 0-1.781-1.059-1.606-1.941l1.426-5.649c-1.781-.883-3.027-2.648-3.027-4.945 0-3 2.492-5.473 5.52-5.473 3.027 0 5.523 2.473 5.523 5.473 0 2.117-1.246 4.062-3.028 4.945zm5.164-25.426H16.918V18.05c0-4.942 4.098-9.004 9.082-9.004s9.082 4.062 9.082 9.004zm0 0" fill="#696"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/eot.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="48"><path style="stroke:none;fill-rule:nonzero;fill:#7291a1;fill-opacity:1" d="M28.621 33.172h-16.32l-2.012 4.45c-.55 1.483-.918 2.593-.918 3.706 0 1.297.547 2.223 1.649 2.781.55.371 2.203.555 4.582.743v1.293H.203v-1.293c1.652-.188 2.934-.93 4.035-2.04 1.098-1.113 2.383-3.34 3.848-6.859L24.586 0h.73L42 36.879c1.648 3.52 2.934 5.746 3.852 6.672.73.742 1.832 1.113 3.296 1.113v1.297h-22.18v-1.297h.919c1.832 0 3.113-.184 3.847-.742.551-.371.735-.926.735-1.48 0-.372 0-.743-.184-1.301 0-.184-.367-1.11-1.101-2.407zm-1.101-2.406-6.786-15.57-7.148 15.57zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#36454d;fill-opacity:1" d="m83.797 16.309-6.602 22.054-.734 2.778c0 .375-.184.558-.184.742 0 .187.184.558.371.742.184.188.368.371.547.371.551 0 1.102-.371 2.016-1.113.371-.367 1.102-1.297 2.387-2.965l1.097.559c-1.648 2.964-3.3 5.003-5.132 6.3-1.833 1.297-3.852 2.04-5.864 2.04-1.285 0-2.203-.372-2.933-.93-.735-.742-1.102-1.485-1.102-2.407 0-.93.367-2.41 1.102-4.82l.73-2.781c-2.562 4.45-5.133 7.601-7.516 9.453C60.516 47.442 59.05 48 57.582 48c-2.016 0-3.668-.926-4.582-2.594-.918-1.668-1.465-3.523-1.465-5.746 0-3.152.914-6.672 2.934-10.75 2.011-4.074 4.582-7.226 7.695-9.82 2.566-2.04 5.133-2.965 7.332-2.965 1.285 0 2.203.367 3.121 1.11.73.742 1.281 2.038 1.649 3.89l1.28-4.074zM72.98 22.797c0-1.856-.367-3.152-.918-3.895-.367-.554-.914-.742-1.648-.742-.734 0-1.469.375-2.2.93-1.464 1.297-3.116 4.074-4.948 8.336-1.832 4.265-2.57 7.785-2.57 10.937 0 1.11.183 2.035.554 2.594.363.559.914.742 1.281.742 1.098 0 2.016-.558 3.117-1.668 1.465-1.668 2.934-3.707 4.032-5.93 2.199-4.449 3.3-8.156 3.3-11.304zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/eps.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M16.223 21.805.09 55.844l3.012 3.015 20.035-20.035c-.711-1.594-.532-3.543.886-4.96 1.774-1.774 4.43-1.774 6.204 0 1.773 1.769 1.773 4.429 0 6.202-1.243 1.243-3.368 1.594-4.965.887L5.23 60.984 8.242 64l34.04-16.133L49.73 27.48 36.61 14.36zm46.625-4.075L46.184 1.062c-1.418-1.417-3.547-1.417-4.965 0L37.32 4.966c-1.422 1.418-1.422 3.543 0 4.965l16.664 16.664c1.418 1.418 3.543 1.418 4.965 0l3.899-3.903c1.418-1.418 1.418-3.543 0-4.96zm0 0" fill="#fea500"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/epub.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><path d="M69.723 24.898c-.336-.851-1.012-1.535-1.688-2.222-.168.687-.336 1.37-.844 2.054L46.098 57.723c-.844 1.199-2.532 1.539-3.88 1.199l-33.75-9.574c-2.023-.512-4.386-1.711-4.554-4.106 0-.851 0-1.195.504-1.535.508-.344 1.016-.344 1.52-.172l31.726 8.89c4.555 1.368 5.902.34 9.277-4.788l19.239-30.09a5.83 5.83 0 0 0 .675-4.957c-.507-1.54-1.855-2.735-3.543-3.246L35.47 1.48c-.676-.171-1.352-.171-2.024-.171v-.172c-4.218-2.563-5.906 2.222-8.101 4.101-.844.684-1.856 1.2-2.196 1.883-.336.684-.168 1.367-.336 1.879-.843 1.883-3.207 4.957-4.386 5.813-.676.515-1.688.683-2.196 1.539-.335.511-.335 1.539-.503 2.222-.676 1.711-2.872 4.617-4.387 5.985-.508.511-1.352.855-1.688 1.539-.34.511-.172 1.539-.675 2.05-1.012 1.711-3.04 4.446-4.559 5.985-.844.855-1.856 1.195-2.191 2.05-.168.34 0 1.028-.168 1.54-.34.855-.676 1.539-.844 2.222C.37 41.141-.137 42.852.03 44.56c.34 4.105 3.375 8.207 7.09 9.234l33.746 9.574c3.207.852 7.09-.683 8.778-3.422l19.402-30.258c1.016-1.367 1.183-3.25.676-4.789zm-38.98-10.941 1.35-2.05c.337-.512 1.18-.856 1.856-.684l22.274 6.324c.675.172.843.855.507 1.371l-1.351 2.05c-.336.512-1.18.856-1.856.684L31.25 15.328c-.676-.172-.844-.687-.508-1.371zm-5.567 8.55 1.347-2.054c.34-.512 1.184-.851 1.86-.683l22.273 6.328c.676.172.844.855.504 1.367l-1.347 2.05c-.34.512-1.184.856-1.856.684L25.68 23.875c-.672-.172-1.012-.855-.504-1.367zm0 0" fill="#963"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/eslintignore.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M54.633 9.367C42.145-3.12 21.855-3.12 9.367 9.367s-12.488 32.778 0 45.266 32.778 12.488 45.266 0 12.488-32.778 0-45.266zM12.176 44.801c-5.934-9.211-4.84-21.543 3.12-29.504s20.294-9.055 29.505-3.121zm7.023 7.023L51.824 19.2c5.934 9.211 4.84 21.543-3.12 29.504s-20.294 9.055-29.505 3.121zm0 0" fill="#bababa"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/exe.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M62.887 38.266c-2.684-.84-4.532-3.36-4.532-6.215 0-2.852 1.848-5.371 4.532-6.211.84-.336 1.343-1.172 1.008-2.012-.84-3.023-1.848-5.707-3.524-8.394-.504-.84-1.344-1.008-2.184-.672-1.007.504-2.015.84-3.19.84-3.692 0-6.548-3.024-6.548-6.547 0-1.176.336-2.184.84-3.188.504-.84.168-1.68-.672-2.183a40.47 40.47 0 0 0-8.39-3.528c-.84-.168-1.68.168-2.016 1.008C37.37 3.852 34.855 5.7 32 5.7s-5.371-1.847-6.21-4.535C25.452.324 24.612-.18 23.772.156c-3.02.84-5.707 1.848-8.39 3.528-.84.503-1.008 1.343-.672 2.183.504 1.004.84 2.012.84 3.188 0 3.691-3.024 6.547-6.547 6.547-1.176 0-2.184-.336-3.191-.84-.84-.504-1.68-.168-2.184.672a40.699 40.699 0 0 0-3.524 8.394c-.167.84.168 1.676 1.008 2.012 2.684.84 4.532 3.36 4.532 6.21 0 2.856-1.848 5.376-4.532 6.216-.84.332-1.343 1.172-1.008 2.011.84 3.024 1.848 5.707 3.524 8.395.504.84 1.344 1.008 2.184.672 1.007-.504 2.015-.84 3.19-.84 3.692 0 6.548 3.02 6.548 6.547 0 1.176-.336 2.183-.84 3.187-.504.84-.168 1.68.672 2.184a40.47 40.47 0 0 0 8.39 3.527h.336c.672 0 1.344-.504 1.512-1.176.84-2.687 3.356-4.535 6.211-4.535s5.371 1.848 6.211 4.535c.336.84 1.176 1.344 2.016 1.008 3.02-.84 5.707-1.847 8.39-3.527.84-.504 1.008-1.344.672-2.184-.504-1.004-.84-2.011-.84-3.187 0-3.692 3.024-6.547 6.547-6.547 1.176 0 2.184.336 3.192.84.84.504 1.68.168 2.183-.672a40.698 40.698 0 0 0 3.524-8.395c.503-.672 0-1.511-.84-1.843zm-30.719 3.691c-5.371 0-9.902-4.363-9.902-9.906 0-5.371 4.363-9.903 9.902-9.903 5.371 0 9.902 4.364 9.902 9.903 0 5.375-4.53 9.906-9.902 9.906zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/f4v.svg
1
1
<svg height="64" width="52" xmlns="http://www.w3.org/2000/svg"><path d="M46.168 13.516c1.793-.711 3.766-.891 5.738-.891V.008c-8.605-.18-16.851 3.554-22.23 10.308-2.153 2.844-4.125 5.864-5.559 9.243l-4.12 10.128c-1.079 3.024-2.333 6.223-3.767 9.067a31.916 31.916 0 0 1-3.945 6.754c-1.254 1.777-3.047 3.199-5.02 4.09-2.152 1.066-4.66 1.597-7.171 1.597v12.797c8.605.18 16.851-3.554 22.23-10.308 1.613-2.309 3.227-4.797 4.485-7.286l3.406-8h14.879v-12.62h-9.86c.715-1.954 1.793-3.731 3.047-5.508.895-1.602 2.153-2.844 3.407-3.91 1.613-1.422 3.046-2.313 4.48-2.844zm0 0" fill="#d10407"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/fax.svg
1
1
<svg height="63" width="60" xmlns="http://www.w3.org/2000/svg"><path d="M21.516 22.047V3.832c0-1.91 1.574-3.297 3.324-3.297h20.816l10.672 10.582v10.754c0 .348-.351.695-.703.695h-3.672c-.351 0-.7-.347-.7-.695v-7.285H44.43c-1.399 0-2.446-1.04-2.446-2.43v-6.59H26.59v16.305c0 .348-.352.695-.7.695h-3.675c-.348.172-.7-.171-.7-.52zm0 39.203V28.984c0-.695.523-1.214 1.226-1.214h36.035c.7 0 1.223.52 1.223 1.214v27.063c0 3.469-2.973 6.418-6.473 6.418H22.914c-.875 0-1.398-.52-1.398-1.215zm25.363-24.98c0 1.562 1.226 2.601 2.625 2.601 1.398 0 2.625-1.215 2.625-2.601 0-1.56-1.227-2.602-2.625-2.602-1.399-.172-2.625 1.043-2.625 2.602zm0 8.847c0 1.563 1.226 2.602 2.625 2.602 1.574 0 2.625-1.215 2.625-2.602 0-1.387-1.227-2.601-2.625-2.773-1.399 0-2.625 1.21-2.625 2.773zM37.96 36.27c0 1.386 1.223 2.601 2.621 2.601 1.402 0 2.625-1.215 2.625-2.601s-1.223-2.602-2.625-2.602c-1.398-.172-2.621 1.043-2.621 2.602zm0 8.847c0 1.387 1.223 2.602 2.621 2.602 1.574 0 2.625-1.215 2.625-2.602 0-1.562-1.223-2.601-2.625-2.773-1.398 0-2.621 1.21-2.621 2.773zm0 8.848c0 1.387 1.223 2.601 2.621 2.601 1.574 0 2.625-1.214 2.625-2.601s-1.223-2.602-2.625-2.602c-1.398 0-2.621 1.215-2.621 2.602zM29.039 36.27c0 1.562 1.223 2.601 2.621 2.601 1.403 0 2.625-1.215 2.625-2.601 0-1.56-1.222-2.602-2.625-2.602-1.574-.172-2.62 1.043-2.62 2.602zm0 8.847c0 1.563 1.223 2.602 2.621 2.602 1.574 0 2.625-1.215 2.625-2.602 0-1.562-1.222-2.601-2.625-2.773-1.398 0-2.62 1.21-2.62 2.773zm0 8.848c0 1.387 1.223 2.601 2.621 2.601 1.574 0 2.625-1.214 2.625-2.601s-1.222-2.602-2.625-2.602c-1.574 0-2.62 1.215-2.62 2.602zm-22.566 8.5h8.57c.7 0 1.227-.52 1.227-1.215V20.832c0-.695-.528-1.215-1.227-1.215h-6.82C3.672 19.617 0 23.262 0 27.77v28.449c0 3.297 2.8 6.246 6.473 6.246zm0 0" fill="#fea500"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/fb2.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><path d="M69.723 24.898c-.336-.851-1.012-1.535-1.688-2.222-.168.687-.336 1.37-.844 2.054L46.098 57.723c-.844 1.199-2.532 1.539-3.88 1.199l-33.75-9.574c-2.023-.512-4.386-1.711-4.554-4.106 0-.851 0-1.195.504-1.535.508-.344 1.016-.344 1.52-.172l31.726 8.89c4.555 1.368 5.902.34 9.277-4.788l19.239-30.09a5.83 5.83 0 0 0 .675-4.957c-.507-1.54-1.855-2.735-3.543-3.246L35.47 1.48c-.676-.171-1.352-.171-2.024-.171v-.172c-4.218-2.563-5.906 2.222-8.101 4.101-.844.684-1.856 1.2-2.196 1.883-.336.684-.168 1.367-.336 1.879-.843 1.883-3.207 4.957-4.386 5.813-.676.515-1.688.683-2.196 1.539-.335.511-.335 1.539-.503 2.222-.676 1.711-2.872 4.617-4.387 5.985-.508.511-1.352.855-1.688 1.539-.34.511-.172 1.539-.675 2.05-1.012 1.711-3.04 4.446-4.559 5.985-.844.855-1.856 1.195-2.191 2.05-.168.34 0 1.028-.168 1.54-.34.855-.676 1.539-.844 2.222C.37 41.141-.137 42.852.03 44.56c.34 4.105 3.375 8.207 7.09 9.234l33.746 9.574c3.207.852 7.09-.683 8.778-3.422l19.402-30.258c1.016-1.367 1.183-3.25.676-4.789zm-38.98-10.941 1.35-2.05c.337-.512 1.18-.856 1.856-.684l22.274 6.324c.675.172.843.855.507 1.371l-1.351 2.05c-.336.512-1.18.856-1.856.684L31.25 15.328c-.676-.172-.844-.687-.508-1.371zm-5.567 8.55 1.347-2.054c.34-.512 1.184-.851 1.86-.683l22.273 6.328c.676.172.844.855.504 1.367l-1.347 2.05c-.34.512-1.184.856-1.856.684L25.68 23.875c-.672-.172-1.012-.855-.504-1.367zm0 0" fill="#963"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/fla.svg
1
1
<svg height="63" width="49" xmlns="http://www.w3.org/2000/svg"><path d="M4.524 3.224v10.102h8.5v2.598h-8.5v13.7h-3.9V.626h13.301v2.598zm14.402 26.3V.826h3.7v28.7zm0 0" fill="#d10407" stroke="#d10407" stroke-miterlimit="10" stroke-width="1.25" transform="matrix(2.10753 0 0 2.07742 0 .079)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/flac.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/flv.svg
1
1
<svg height="64" width="52" xmlns="http://www.w3.org/2000/svg"><path d="M46.168 13.516c1.793-.711 3.766-.891 5.738-.891V.008c-8.605-.18-16.851 3.554-22.23 10.308-2.153 2.844-4.125 5.864-5.559 9.243l-4.12 10.128c-1.079 3.024-2.333 6.223-3.767 9.067a31.916 31.916 0 0 1-3.945 6.754c-1.254 1.777-3.047 3.199-5.02 4.09-2.152 1.066-4.66 1.597-7.171 1.597v12.797c8.605.18 16.851-3.554 22.23-10.308 1.613-2.309 3.227-4.797 4.485-7.286l3.406-8h14.879v-12.62h-9.86c.715-1.954 1.793-3.731 3.047-5.508.895-1.602 2.153-2.844 3.407-3.91 1.613-1.422 3.046-2.313 4.48-2.844zm0 0" fill="#d10407"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/fnt.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="48"><path style="stroke:none;fill-rule:nonzero;fill:#7291a1;fill-opacity:1" d="M28.621 33.172h-16.32l-2.012 4.45c-.55 1.483-.918 2.593-.918 3.706 0 1.297.547 2.223 1.649 2.781.55.371 2.203.555 4.582.743v1.293H.203v-1.293c1.652-.188 2.934-.93 4.035-2.04 1.098-1.113 2.383-3.34 3.848-6.859L24.586 0h.73L42 36.879c1.648 3.52 2.934 5.746 3.852 6.672.73.742 1.832 1.113 3.296 1.113v1.297h-22.18v-1.297h.919c1.832 0 3.113-.184 3.847-.742.551-.371.735-.926.735-1.48 0-.372 0-.743-.184-1.301 0-.184-.367-1.11-1.101-2.407zm-1.101-2.406-6.786-15.57-7.148 15.57zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#36454d;fill-opacity:1" d="m83.797 16.309-6.602 22.054-.734 2.778c0 .375-.184.558-.184.742 0 .187.184.558.371.742.184.188.368.371.547.371.551 0 1.102-.371 2.016-1.113.371-.367 1.102-1.297 2.387-2.965l1.097.559c-1.648 2.964-3.3 5.003-5.132 6.3-1.833 1.297-3.852 2.04-5.864 2.04-1.285 0-2.203-.372-2.933-.93-.735-.742-1.102-1.485-1.102-2.407 0-.93.367-2.41 1.102-4.82l.73-2.781c-2.562 4.45-5.133 7.601-7.516 9.453C60.516 47.442 59.05 48 57.582 48c-2.016 0-3.668-.926-4.582-2.594-.918-1.668-1.465-3.523-1.465-5.746 0-3.152.914-6.672 2.934-10.75 2.011-4.074 4.582-7.226 7.695-9.82 2.566-2.04 5.133-2.965 7.332-2.965 1.285 0 2.203.367 3.121 1.11.73.742 1.281 2.038 1.649 3.89l1.28-4.074zM72.98 22.797c0-1.856-.367-3.152-.918-3.895-.367-.554-.914-.742-1.648-.742-.734 0-1.469.375-2.2.93-1.464 1.297-3.116 4.074-4.948 8.336-1.832 4.265-2.57 7.785-2.57 10.937 0 1.11.183 2.035.554 2.594.363.559.914.742 1.281.742 1.098 0 2.016-.558 3.117-1.668 1.465-1.668 2.934-3.707 4.032-5.93 2.199-4.449 3.3-8.156 3.3-11.304zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/folder-link.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M81.227 64H2.773a2.514 2.514 0 0 1-2.535-2.54V21.333h83.524v40.129A2.514 2.514 0 0 1 81.227 64zm0 0" fill="#efce4a"/><path d="M33.008 10.059v-7.52A2.514 2.514 0 0 0 30.468 0H2.774A2.514 2.514 0 0 0 .238 2.54v18.792h83.524v-8.734a2.514 2.514 0 0 0-2.535-2.54zm0 0" fill="#ebba16"/><path d="m53.059 42.668-10.754-9.754v6.5H30.94v6.504h11.364v6.5zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/folder-up.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M81.227 64H2.773a2.514 2.514 0 0 1-2.535-2.54V21.333h83.524v40.129A2.514 2.514 0 0 1 81.227 64zm0 0" fill="#efce4a"/><path d="M33.008 10.059v-7.52A2.514 2.514 0 0 0 30.468 0H2.774A2.514 2.514 0 0 0 .238 2.54v18.792h83.524v-8.734a2.514 2.514 0 0 0-2.535-2.54zm0 0" fill="#ebba16"/><path d="m42 31.594-9.738 10.77h6.492v11.374h6.492V42.363h6.492zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/folder.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M81.227 64H2.773a2.514 2.514 0 0 1-2.535-2.54V21.333h83.524v40.129A2.514 2.514 0 0 1 81.227 64zm0 0" fill="#efce4a"/><path d="M33.008 10.059v-7.52A2.514 2.514 0 0 0 30.468 0H2.774A2.514 2.514 0 0 0 .238 2.54v18.792h83.524v-8.734a2.514 2.514 0 0 0-2.535-2.54zm0 0" fill="#ebba16"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/fon.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="48"><path style="stroke:none;fill-rule:nonzero;fill:#7291a1;fill-opacity:1" d="M28.621 33.172h-16.32l-2.012 4.45c-.55 1.483-.918 2.593-.918 3.706 0 1.297.547 2.223 1.649 2.781.55.371 2.203.555 4.582.743v1.293H.203v-1.293c1.652-.188 2.934-.93 4.035-2.04 1.098-1.113 2.383-3.34 3.848-6.859L24.586 0h.73L42 36.879c1.648 3.52 2.934 5.746 3.852 6.672.73.742 1.832 1.113 3.296 1.113v1.297h-22.18v-1.297h.919c1.832 0 3.113-.184 3.847-.742.551-.371.735-.926.735-1.48 0-.372 0-.743-.184-1.301 0-.184-.367-1.11-1.101-2.407zm-1.101-2.406-6.786-15.57-7.148 15.57zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#36454d;fill-opacity:1" d="m83.797 16.309-6.602 22.054-.734 2.778c0 .375-.184.558-.184.742 0 .187.184.558.371.742.184.188.368.371.547.371.551 0 1.102-.371 2.016-1.113.371-.367 1.102-1.297 2.387-2.965l1.097.559c-1.648 2.964-3.3 5.003-5.132 6.3-1.833 1.297-3.852 2.04-5.864 2.04-1.285 0-2.203-.372-2.933-.93-.735-.742-1.102-1.485-1.102-2.407 0-.93.367-2.41 1.102-4.82l.73-2.781c-2.562 4.45-5.133 7.601-7.516 9.453C60.516 47.442 59.05 48 57.582 48c-2.016 0-3.668-.926-4.582-2.594-.918-1.668-1.465-3.523-1.465-5.746 0-3.152.914-6.672 2.934-10.75 2.011-4.074 4.582-7.226 7.695-9.82 2.566-2.04 5.133-2.965 7.332-2.965 1.285 0 2.203.367 3.121 1.11.73.742 1.281 2.038 1.649 3.89l1.28-4.074zM72.98 22.797c0-1.856-.367-3.152-.918-3.895-.367-.554-.914-.742-1.648-.742-.734 0-1.469.375-2.2.93-1.464 1.297-3.116 4.074-4.948 8.336-1.832 4.265-2.57 7.785-2.57 10.937 0 1.11.183 2.035.554 2.594.363.559.914.742 1.281.742 1.098 0 2.016-.558 3.117-1.668 1.465-1.668 2.934-3.707 4.032-5.93 2.199-4.449 3.3-8.156 3.3-11.304zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/gdp.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M43.8 55.613H11.036c-3.586 0-6.488-2.906-6.488-6.5s2.902-6.504 6.488-6.504h12.797c5.8 0 10.582-4.793 10.582-10.609s-4.781-10.61-10.582-10.61h-6.996c.172.684.172 1.368.172 2.055 0 .684 0 1.368-.172 2.051h6.996c3.586 0 6.484 2.91 6.484 6.504s-2.898 6.504-6.484 6.504H11.035C5.23 38.504.453 43.293.453 49.114c0 5.816 4.777 10.609 10.582 10.609H43.97c-.168-.684-.168-1.371-.168-2.055zm10.067-4.277c-3.414 0-6.312 2.738-6.312 6.332S50.285 64 53.867 64c3.586 0 6.317-2.738 6.317-6.332s-2.73-6.332-6.317-6.332zM19.567 0H6.765C5.742 0 4.719.855 4.719 2.055v15.398C2.16 18.31.453 20.707.453 23.445c0 3.422 2.73 6.332 6.313 6.332 3.586 0 6.316-2.738 6.316-6.332 0-2.738-1.707-5.136-4.266-5.992v-4.789h10.75c1.024 0 2.047-.855 2.047-2.055V2.055C21.441 1.027 20.59 0 19.566 0zm34.3 4.277c-8.191 0-14.847 6.676-14.847 14.887 0 4.45 1.878 8.559 5.292 11.297l7.68 15.23c.68 1.54 2.899 1.54 3.582 0l7.68-15.23c3.414-2.91 5.289-7.016 5.289-11.297.172-8.21-6.484-14.887-14.676-14.887zm0 21.22c-3.414 0-6.312-2.74-6.312-6.333s2.73-6.328 6.312-6.328c3.586 0 6.317 2.734 6.317 6.328s-2.73 6.332-6.317 6.332zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/gem.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M61.988 2.012v59.976L46.996 17.004zM2.012 61.988h59.976L17.004 46.996zm14.992-14.992 44.984 14.992L32 32zM32 32l29.988 29.988-14.992-44.984zM2.012 46.996v14.992l14.992-14.992zM32 32H17.004v14.996zm14.996-14.996H32V32zM61.988 2.012H46.996v14.992zM17.004 32 2.012 46.996h14.992zM32 17.004 17.004 32H32zM46.996 2.012 32 17.004h14.996zm0 0" fill="#666" stroke="#fff" stroke-width="1.66605"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/gif.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/gitattributes.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M62.773 29.176 34.824 1.226c-1.636-1.636-4.164-1.636-5.797 0L23.23 7.024l7.282 7.286c1.636-.594 3.718-.149 5.054 1.191 1.34 1.336 1.786 3.418 1.192 5.055l7.137 7.133a4.905 4.905 0 0 1 5.054 1.19 4.942 4.942 0 0 1-6.988 6.99c-1.488-1.49-1.785-3.571-1.043-5.356l-6.54-6.54v17.395l1.337.89a4.935 4.935 0 0 1 0 6.99 4.937 4.937 0 0 1-6.985 0c-1.933-1.934-2.082-5.056-.148-6.99.445-.444.89-.89 1.484-1.038V23.527c-.445-.297-1.039-.597-1.484-1.043-1.488-1.484-1.785-3.566-1.043-5.351l-7.137-7.285-19.175 19.18c-1.637 1.632-1.637 4.16 0 5.796l27.949 27.95c1.636 1.636 4.164 1.636 5.797 0l27.8-27.801c1.637-1.633 1.637-4.313 0-5.797zm0 0" fill="#bababa"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/gitignore.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M54.633 9.367C42.145-3.12 21.855-3.12 9.367 9.367s-12.488 32.778 0 45.266 32.778 12.488 45.266 0 12.488-32.778 0-45.266zM12.176 44.801c-5.934-9.211-4.84-21.543 3.12-29.504s20.294-9.055 29.505-3.121zm7.023 7.023L51.824 19.2c5.934 9.211 4.84 21.543-3.12 29.504s-20.294 9.055-29.505 3.121zm0 0" fill="#bababa"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/go.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="42"><path style="stroke:none;fill-rule:nonzero;fill:#2dbcaf;fill-opacity:1" d="M41.156 11.465c-3.449.906-5.804 1.633-9.254 2.539-.906.184-.906.184-1.632-.36-.907-.91-1.27-1.636-2.54-2-3.449-1.812-6.714-1.085-9.98.91-3.809 2.54-5.625 6.169-5.625 10.344 0 4.172 3.086 8.165 7.441 8.528 3.809.363 6.711-.906 9.254-3.63.36-.726.907-1.273 1.633-1.995H19.746c-1.09 0-1.27-.727-1.09-1.633.727-1.816 1.996-4.535 2.723-6.168.183-.184.363-.91 1.27-.91h20.14c0 1.633 0 2.902-.18 4.535-.726 3.996-1.996 7.621-4.535 10.887-3.992 5.265-9.074 8.347-15.61 9.258-5.44.722-10.339-.184-14.694-3.63C3.777 35.056 1.422 30.88.695 25.98c-.73-6.168 1.086-11.246 4.715-15.968C9.223 4.754 14.484 1.668 20.832.578c5.266-.906 10.164-.18 14.7 2.723 2.902 1.996 4.898 4.535 6.35 7.62 0 0 0 .18-.726.544zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#2dbcaf;fill-opacity:1" d="M59.117 41.766c-4.898 0-9.433-1.633-13.242-4.715-3.086-2.723-5.262-6.352-5.809-10.707-.906-6.352.727-11.793 4.54-16.692 3.988-5.265 8.89-8.168 15.421-9.254 5.625-.91 10.887-.363 15.606 2.723 4.351 2.902 7.074 7.074 7.62 12.156.907 7.438-1.089 13.61-6.35 18.688-3.63 3.629-8.165 6.168-13.063 7.078-1.633.363-3.266.723-4.723.723zm13.25-22.133c0-.727 0-1.274-.183-1.817-.907-5.445-6.168-8.527-11.25-7.257-5.262 1.086-8.348 4.351-9.797 9.433-.907 4.356 1.09 8.528 4.898 10.344 2.903 1.27 6.172 1.09 9.07-.184 4.54-2.175 6.899-5.804 7.262-10.52zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/gpg.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M31.816 6.078c5.094 5.094 6.547 12.184 4.73 18.547l26.907 26.91.547 12-15.09-1.273v-7.637h-7.637v-7.637h-7.457L24 37.172c-6.363 1.816-13.637.363-18.547-4.73-7.27-7.27-7.27-19.27 0-26.544a18.494 18.494 0 0 1 26.363.18zM18 11.172c-2.184-2.184-5.453-2.184-7.637 0-2.18 2.18-2.18 5.453 0 7.637 2.184 2.18 5.453 2.18 7.637 0 2.184-2.184 2.184-5.637 0-7.637zm0 0" fill="#696"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/gpl.svg
1
1
<svg height="64" width="65" xmlns="http://www.w3.org/2000/svg"><path d="M21.61 63.91c-.176 0-.352 0-.528-.176C8.434 58.934 0 46.488 0 32.977 0 14.844 14.582.09 32.5.09S65 14.844 65 32.977c0 13.511-8.434 25.957-21.082 30.757a1.12 1.12 0 0 1-1.055 0c-.351-.18-.527-.355-.699-.71l-7.027-18.669c-.352-.71.175-1.601.875-1.777 3.867-1.422 6.324-5.156 6.324-9.422 0-5.511-4.39-9.957-9.836-9.957s-9.836 4.446-9.836 9.957c0 4.09 2.633 7.82 6.324 9.422.7.356 1.051 1.067.875 1.777l-7.027 18.668c-.172.356-.348.711-.7.711 0 .176-.175.176-.527.176zm0 0" fill="#af7931"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/gradle.svg
1
1
<svg height="64" width="81" xmlns="http://www.w3.org/2000/svg"><path d="M78.398 6.79C75.508 1.55 70.492 0 66.828 0c-4.437 0-8.101 2.328-7.328 4.074.191.387.965 2.133 1.543 2.906.77 1.165 2.121.196 2.508 0 1.347-.773 2.89-.968 4.433-.773 1.543.191 3.664 1.164 5.207 3.879 3.278 6.398-6.941 19.781-19.863 10.473-13.113-9.118-25.652-6.207-31.437-4.27-5.786 1.941-8.293 3.688-5.977 7.953 3.086 5.82 2.121 4.074 5.012 8.922 4.629 7.758 15.043-3.492 15.043-3.492-7.711 11.441-14.27 8.726-16.778 4.656-2.312-3.492-4.05-7.758-4.05-7.758C-4.336 33.55.87 64 .87 64h9.64C13.02 52.75 21.7 53.14 23.243 64h7.328c6.559-21.914 22.95 0 22.95 0h9.644c-2.7-14.934 5.398-19.586 10.414-28.316 5.399-8.922 10.223-19.586 4.82-28.895zM53.52 35.683c-5.012-1.746-3.278-6.786-3.278-6.786s4.434 1.356 10.414 3.489c-.191 1.36-3.277 4.46-7.136 3.297zm0 0" fill="#02303a"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/gz.svg
1
1
<svg height="63" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M53.344 18.172H44.02V8.965zM28.309 8.965v33.437h25.199V20.434H41.727V8.964zm3.93-8.723H4.417v6.461h10.965l-6.875 5.332v5.652l10.148-7.753V6.867H54V4.281zM18.655 14.297 8.508 22.05v5.652l10.148-7.754zM8.344 37.559l10.148-7.754v-5.657L8.344 31.902zm10.312 2.261v-5.656L8.508 41.918v2.91h-4.09v6.461h6.219v4.523H7.035c-.652-1.132-1.797-1.937-3.273-1.937C1.637 53.875 0 55.488 0 57.59c0 2.097 1.637 3.715 3.762 3.715 1.476 0 2.62-.809 3.273-1.938h3.602v3.39h5.562v-3.39h3.602c.652 1.13 1.8 1.938 3.273 1.938 2.125 0 3.762-1.618 3.762-3.715 0-2.102-1.637-3.715-3.762-3.715-1.472 0-2.62.805-3.273 1.938h-3.602v-4.524h15.875l21.762-3.879v-2.582H11.78zm0 0" fill="#90c"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/h.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".25"><path d="M63.773.227H.227v63.546h63.546zM60.324 60.14H4.04V12.574h56.285zm0 0" stroke-width=".4539"/><path d="M9.305 18.203h45.39v6.352H9.305zm7.445 10.348h27.777v2.543H16.75zm3.992 7.808h27.781v2.723h-27.78zm-3.992 8.168h27.777v2.723H16.75zm3.992 8.352h27.781v2.723h-27.78zm0 0" stroke-width=".4539"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/handlebars.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="51"><path style="stroke:none;fill-rule:nonzero;fill:#c19770;fill-opacity:1" d="M30.605 30.332c2.782-.18 5.735.879 7.993 2.824 1.039.883 2.086 2.121 2.777 3.535 1.39-2.296 3.477-4.242 6.082-5.3a15.304 15.304 0 0 1 9.73 0 20.188 20.188 0 0 1 7.293 4.77 62.33 62.33 0 0 0 5.04 4.593c.867.883 1.91 1.41 2.953 1.766 1.562.53 3.297 0 4.343-1.06.868-1.058.868-2.827-.175-4.062-.696-.53-1.563-.53-2.258.176 0 0 0 .176-.176.176-.348.707-.348 1.59.176 2.121-.867-.531-1.563-1.59-1.738-2.828-.172-1.414.87-2.648 2.085-3.004 2.43-.883 5.387-.176 6.774 1.945 1.219 2.118 1.738 4.59 1.39 6.887-.347 2.3-1.566 4.242-3.472 5.656-2.606 1.766-5.734 2.649-9.035 2.297-2.953-.176-6.078-.707-8.688-1.945-4.687-1.941-9.031-4.414-13.722-6.008-1.563-.351-3.473-.879-5.04-1.059h-3.82c-1.562.18-3.125.532-4.515 1.06-4.864 1.593-9.207 4.241-13.899 6.187-3.996 1.59-8.336 2.296-12.504 1.414-2.433-.356-4.691-1.594-6.258-3.535a7.888 7.888 0 0 1-1.906-5.829c-.176-2.12.344-4.066 1.563-5.656 1.214-1.59 3.129-2.297 5.035-2.121 1.215 0 2.262.531 3.129 1.59.52.887.695 1.77.347 2.828-.347.883-.87 1.59-1.566 1.945.352-.53.523-1.414.172-2.12-.516-.708-1.559-.884-2.254-.356-.176 0-.176.18-.348.18-.87.882-.87 2.296-.347 3.355.695 1.059 1.738 1.766 2.949 1.945 1.742 0 3.48-.886 4.691-2.125 2.606-2.648 5.387-4.945 8.34-7.242 2.781-1.941 5.906-2.828 8.86-3zm0-30.215c2.782-.176 5.735.887 7.993 2.828 1.039.883 2.086 2.118 2.777 3.532 1.39-2.297 3.477-4.239 6.082-5.297a15.25 15.25 0 0 1 9.73 0 20.22 20.22 0 0 1 7.293 4.77 62.33 62.33 0 0 0 5.04 4.593c.867.883 1.91 1.414 2.953 1.766 1.562.53 3.297 0 4.343-1.059.868-1.063.868-2.828-.175-4.066-.696-.528-1.563-.528-2.258.18 0 0 0 .175-.176.175-.348.707-.348 1.59.176 2.121-.867-.531-1.563-1.59-1.738-2.828-.172-1.414.87-2.652 2.085-3.004 2.43-.883 5.387-.176 6.778 1.942 1.215 2.12 1.734 4.597 1.387 6.894-.348 2.297-1.563 4.238-3.473 5.652-2.606 1.77-5.734 2.649-9.035 2.297-2.953-.175-6.078-.707-8.684-1.941-4.691-1.945-9.035-4.418-13.723-6.008-1.562-.355-3.476-.887-5.039-1.062h-3.82c-1.566.175-3.129.53-4.52 1.062-4.863 1.59-9.206 4.238-13.898 6.18-3.992 1.593-8.336 2.297-12.504 1.414-2.433-.352-4.691-1.586-6.258-3.531a7.913 7.913 0 0 1-1.906-5.832C-.14 8.773.38 6.832 1.598 5.242c1.214-1.59 3.129-2.297 5.035-2.12 1.215 0 2.262.53 3.129 1.589.52.887.695 1.766.347 2.828-.347.883-.87 1.59-1.566 1.941.352-.527.523-1.41.172-2.117-.516-.707-1.559-.886-2.254-.351-.176 0-.176.172-.348.172-.87.882-.87 2.296-.347 3.359.695 1.059 1.738 1.766 2.949 1.941 1.742 0 3.48-.882 4.691-2.117 2.606-2.648 5.387-4.949 8.34-7.246 2.781-1.941 5.91-2.824 8.86-3.004zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/hbs.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="51"><path style="stroke:none;fill-rule:nonzero;fill:#c19770;fill-opacity:1" d="M30.605 30.332c2.782-.18 5.735.879 7.993 2.824 1.039.883 2.086 2.121 2.777 3.535 1.39-2.296 3.477-4.242 6.082-5.3a15.304 15.304 0 0 1 9.73 0 20.188 20.188 0 0 1 7.293 4.77 62.33 62.33 0 0 0 5.04 4.593c.867.883 1.91 1.41 2.953 1.766 1.562.53 3.297 0 4.343-1.06.868-1.058.868-2.827-.175-4.062-.696-.53-1.563-.53-2.258.176 0 0 0 .176-.176.176-.348.707-.348 1.59.176 2.121-.867-.531-1.563-1.59-1.738-2.828-.172-1.414.87-2.648 2.085-3.004 2.43-.883 5.387-.176 6.774 1.945 1.219 2.118 1.738 4.59 1.39 6.887-.347 2.3-1.566 4.242-3.472 5.656-2.606 1.766-5.734 2.649-9.035 2.297-2.953-.176-6.078-.707-8.688-1.945-4.687-1.941-9.031-4.414-13.722-6.008-1.563-.351-3.473-.879-5.04-1.059h-3.82c-1.562.18-3.125.532-4.515 1.06-4.864 1.593-9.207 4.241-13.899 6.187-3.996 1.59-8.336 2.296-12.504 1.414-2.433-.356-4.691-1.594-6.258-3.535a7.888 7.888 0 0 1-1.906-5.829c-.176-2.12.344-4.066 1.563-5.656 1.214-1.59 3.129-2.297 5.035-2.121 1.215 0 2.262.531 3.129 1.59.52.887.695 1.77.347 2.828-.347.883-.87 1.59-1.566 1.945.352-.53.523-1.414.172-2.12-.516-.708-1.559-.884-2.254-.356-.176 0-.176.18-.348.18-.87.882-.87 2.296-.347 3.355.695 1.059 1.738 1.766 2.949 1.945 1.742 0 3.48-.886 4.691-2.125 2.606-2.648 5.387-4.945 8.34-7.242 2.781-1.766 5.906-2.828 8.86-3zm0-30.215c2.782-.176 5.735.887 7.993 2.828 1.039.883 2.086 2.118 2.777 3.532 1.39-2.297 3.477-4.239 6.082-5.297a15.25 15.25 0 0 1 9.73 0 20.22 20.22 0 0 1 7.293 4.77 62.33 62.33 0 0 0 5.04 4.593c.867.883 1.91 1.414 2.953 1.766 1.562.53 3.297 0 4.343-1.059.868-1.063.868-2.828-.175-4.066-.696-.528-1.563-.528-2.258.18 0 0 0 .175-.176.175-.348.707-.348 1.59.176 2.121-.867-.531-1.563-1.59-1.738-2.828-.172-1.414.87-2.652 2.085-3.004 2.43-.883 5.387-.176 6.778 1.942 1.215 2.12 1.734 4.597 1.387 6.894-.348 2.297-1.563 4.238-3.473 5.652-2.606 1.77-5.734 2.649-9.035 2.297-2.953-.175-6.078-.707-8.684-1.941-4.691-1.945-9.035-4.418-13.723-6.008-1.562-.355-3.476-.887-5.039-1.062h-3.82c-1.566.175-3.129.53-4.52 1.062-4.863 1.59-9.206 4.238-13.898 6.18-3.992 1.593-8.336 2.297-12.504 1.414-2.433-.352-4.691-1.586-6.258-3.531a7.913 7.913 0 0 1-1.906-5.832C-.14 8.773.38 6.832 1.598 5.242c1.214-1.59 3.129-2.297 5.035-2.12 1.215 0 2.262.53 3.129 1.589.52.887.695 1.766.347 2.828-.347.883-.87 1.59-1.566 1.941.352-.527.523-1.41.172-2.117-.516-.707-1.559-.886-2.254-.351-.176 0-.176.172-.348.172-.87.882-.87 2.296-.347 3.359.695 1.059 1.738 1.766 2.949 1.941 1.742 0 3.48-.882 4.691-2.117 2.606-2.648 5.387-4.949 8.34-7.246 2.781-1.766 5.91-2.824 8.86-3.004zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/heic.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/hlp.svg
1
1
<svg height="64" width="59" xmlns="http://www.w3.org/2000/svg"><path d="M59 0H13.41C6.613 0 0 2.668 0 10.668V64h48.273V10.668H6.613c0-3.914 2.684-5.336 5.367-5.336h41.477v53.336l5.363-5.336V0zm0 0" fill="#c93"/><path d="M21.992 40.18c0-5.512 6.434-6.403 6.434-10.493 0-1.777-1.61-3.199-3.754-3.199-2.324.18-4.11 1.778-4.11 1.778L17.88 24.89s2.683-2.848 7.332-2.848c4.289 0 8.402 2.668 8.402 7.289 0 6.402-6.797 7.113-6.797 11.203v1.422h-4.824zm0 5.152h4.824v4.445h-4.824zm0 0" fill="#fff"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/hs.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="59"><path style="stroke:none;fill-rule:nonzero;fill:#8f4e8b;fill-opacity:1" d="m.469 59 19.367-29.5L.469 0h14.379l19.367 29.5L14.848 59zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#8f4e8b;fill-opacity:1" d="m19.836 59 19.363-29.5L19.836 0h14.379l38.73 59h-14.57L46.293 40.633 34.215 59zm46.59-17.191-6.328-9.965H82.53v9.965zm-9.59-14.653-6.516-9.965h32.211v9.965zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/hsl.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="59"><path style="stroke:none;fill-rule:nonzero;fill:#8f4e8b;fill-opacity:1" d="m.469 59 19.367-29.5L.469 0h14.379l19.367 29.5L14.848 59zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#8f4e8b;fill-opacity:1" d="m19.836 59 19.363-29.5L19.836 0h14.379l38.73 59h-14.57L46.293 40.633 34.215 59zm46.59-17.191-6.328-9.965H82.53v9.965zm-9.59-14.653-6.516-9.965h32.211v9.965zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/htm.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="55"><path style="fill-rule:nonzero;fill:#d75b26;fill-opacity:1;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#d75b26;stroke-opacity:1;stroke-miterlimit:10" d="M11.241 25.867c-.498 0-1.1-.2-1.5-.6l-8.398-8.4c-.8-.799-.8-2.1 0-3.001l8.398-8.4c.8-.798 2.101-.798 3.002 0 .8.801.8 2.1 0 3.001l-7 6.9 6.9 6.9c.8.8.8 2.098 0 3-.4.4-.9.6-1.402.6zm25 0c-.5 0-1.099-.2-1.499-.6-.8-.8-.8-2.1 0-3l6.901-6.9-6.9-6.9c-.8-.8-.8-2.1 0-3 .8-.8 2.1-.8 2.998 0l8.4 8.399c.801.8.801 2.102 0 3l-8.4 8.3c-.4.5-.898.7-1.5.7zm-16.698 4.1c-.2 0-.402 0-.7-.1-1.1-.399-1.7-1.5-1.3-2.599l8.399-25.1c.402-1.101 1.5-1.702 2.6-1.302 1.1.4 1.7 1.502 1.3 2.6l-8.4 25.101c-.198.901-1 1.4-1.899 1.4zm0 0" transform="matrix(1.74792 0 0 1.75607 0 .53)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/html.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="55"><path style="fill-rule:nonzero;fill:#d75b26;fill-opacity:1;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#d75b26;stroke-opacity:1;stroke-miterlimit:10" d="M11.241 25.867c-.498 0-1.1-.2-1.5-.6l-8.398-8.4c-.8-.799-.8-2.1 0-3.001l8.398-8.4c.8-.798 2.101-.798 3.002 0 .8.801.8 2.1 0 3.001l-7 6.9 6.9 6.9c.8.8.8 2.098 0 3-.4.4-.9.6-1.402.6zm25 0c-.5 0-1.099-.2-1.499-.6-.8-.8-.8-2.1 0-3l6.901-6.9-6.9-6.9c-.8-.8-.8-2.1 0-3 .8-.8 2.1-.8 2.998 0l8.4 8.399c.801.8.801 2.102 0 3l-8.4 8.3c-.4.5-.898.7-1.5.7zm-16.698 4.1c-.2 0-.402 0-.7-.1-1.1-.399-1.7-1.5-1.3-2.599l8.399-25.1c.402-1.101 1.5-1.702 2.6-1.302 1.1.4 1.7 1.502 1.3 2.6l-8.4 25.101c-.198.901-1 1.4-1.899 1.4zm0 0" transform="matrix(1.74792 0 0 1.75607 0 .53)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ibooks.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><path d="M69.723 24.898c-.336-.851-1.012-1.535-1.688-2.222-.168.687-.336 1.37-.844 2.054L46.098 57.723c-.844 1.199-2.532 1.539-3.88 1.199l-33.75-9.574c-2.023-.512-4.386-1.711-4.554-4.106 0-.851 0-1.195.504-1.535.508-.344 1.016-.344 1.52-.172l31.726 8.89c4.555 1.368 5.902.34 9.277-4.788l19.239-30.09a5.83 5.83 0 0 0 .675-4.957c-.507-1.54-1.855-2.735-3.543-3.246L35.47 1.48c-.676-.171-1.352-.171-2.024-.171v-.172c-4.218-2.563-5.906 2.222-8.101 4.101-.844.684-1.856 1.2-2.196 1.883-.336.684-.168 1.367-.336 1.879-.843 1.883-3.207 4.957-4.386 5.813-.676.515-1.688.683-2.196 1.539-.335.511-.335 1.539-.503 2.222-.676 1.711-2.872 4.617-4.387 5.985-.508.511-1.352.855-1.688 1.539-.34.511-.172 1.539-.675 2.05-1.012 1.711-3.04 4.446-4.559 5.985-.844.855-1.856 1.195-2.191 2.05-.168.34 0 1.028-.168 1.54-.34.855-.676 1.539-.844 2.222C.37 41.141-.137 42.852.03 44.56c.34 4.105 3.375 8.207 7.09 9.234l33.746 9.574c3.207.852 7.09-.683 8.778-3.422l19.402-30.258c1.016-1.367 1.183-3.25.676-4.789zm-38.98-10.941 1.35-2.05c.337-.512 1.18-.856 1.856-.684l22.274 6.324c.675.172.843.855.507 1.371l-1.351 2.05c-.336.512-1.18.856-1.856.684L31.25 15.328c-.676-.172-.844-.687-.508-1.371zm-5.567 8.55 1.347-2.054c.34-.512 1.184-.851 1.86-.683l22.273 6.328c.676.172.844.855.504 1.367l-1.347 2.05c-.34.512-1.184.856-1.856.684L25.68 23.875c-.672-.172-1.012-.855-.504-1.367zm0 0" fill="#963"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/icns.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M33.383.531c.176-.707.527-.707.699 0l7.176 22.582c.176.707.875 1.235 1.574 1.235h23.45c.698 0 .698.351.175.707L47.383 38.816c-.524.528-.7 1.235-.524 1.942l7.172 22.582c.176.707 0 .883-.523.351L34.434 49.754c-.524-.352-1.399-.352-1.926 0L13.438 63.69c-.528.356-.876.176-.528-.351l7.176-22.582c.176-.707 0-1.414-.527-1.942L.489 24.88c-.528-.356-.352-.707.175-.707H24.11c.7 0 1.399-.531 1.575-1.235zm0 0" fill="#8ed200"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ico.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M33.383.531c.176-.707.527-.707.699 0l7.176 22.582c.176.707.875 1.235 1.574 1.235h23.45c.698 0 .698.351.175.707L47.383 38.816c-.524.528-.7 1.235-.524 1.942l7.172 22.582c.176.707 0 .883-.523.351L34.434 49.754c-.524-.352-1.399-.352-1.926 0L13.438 63.69c-.528.356-.876.176-.528-.351l7.176-22.582c.176-.707 0-1.414-.527-1.942L.489 24.88c-.528-.356-.352-.707.175-.707H24.11c.7 0 1.399-.531 1.575-1.235zm0 0" fill="#8ed200"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ics.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M5.102 59.219v-10.11h11.605v10.11zm14.066 0v-10.11h12.836v10.11zM5.102 46.805V35.457h11.605v11.348zm14.066 0V35.457h12.836v11.348zM5.102 33.152V23.047h11.605v10.105zM34.645 59.22v-10.11H47.48v10.11zM19.168 33.152V23.047h12.836v10.105zm30.95 26.067v-10.11h11.605v10.11zM34.644 46.805V35.457H47.48v11.348zm-14.07-30.496c0 .53-.528 1.062-1.231 1.062h-2.637c-.703 0-1.23-.531-1.23-1.062V6.203c0-.535.527-1.066 1.23-1.066h2.461c.703 0 1.23.531 1.23 1.066V16.31zm29.542 30.496V35.457h11.606v11.348zM34.645 33.152V23.047H47.48v10.105zm15.472 0V23.047h11.606v10.105zm1.406-16.843c0 .53-.527 1.062-1.23 1.062h-2.637c-.703 0-1.23-.531-1.23-1.062V6.203c0-.535.527-1.066 1.23-1.066h2.637c.703 0 1.23.531 1.23 1.066zM67 14.004c0-2.484-2.285-4.434-5.102-4.434h-5.097V6.203c0-3.016-2.813-5.676-6.508-5.676h-2.637c-3.515 0-6.508 2.48-6.508 5.676V9.57H25.676V6.203c0-3.016-2.817-5.676-6.508-5.676h-2.637c-3.52 0-6.508 2.48-6.508 5.676V9.57H5.102C2.285 9.57 0 11.7 0 14.004v45.035c0 2.484 2.285 4.434 5.102 4.434h56.62c2.817 0 5.102-2.13 5.102-4.434V14.004zm0 0" fill="#111"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/idx.svg
1
1
<svg height="63" width="48" xmlns="http://www.w3.org/2000/svg"><path d="M47.66 38.852c0 3.89-2.894 17.074-2.894 17.074-.68 3.21-3.575 6.594-7.317 6.594H19.75c-2.555 0-4.938-1.524-5.957-3.891 0 0-8.68-15.887-12.082-21.637-2.383-4.054-2.383-4.054.68-5.746a2.905 2.905 0 0 1 1.703-.508c1.191 0 2.039.676 2.89 1.692l5.278 6.086 1.531 2.027V3.863c0-1.86 1.703-3.383 3.742-3.383 1.875 0 3.406 1.524 3.406 3.383l.68 23.664h1.531l.34-4.054c0-1.86 1.531-3.383 3.406-3.383 1.872 0 3.403 1.523 3.403 3.383l.34 4.898h1.53l.34-3.21c0-1.86 1.532-3.38 3.407-3.38 1.871 0 3.402 1.52 3.402 3.38l.34 3.21v.848h1.192l.34-1.692c0-1.859 1.53-3.379 3.406-3.379 1.87 0 3.402 1.52 3.402 3.38-.34 0-.34 7.437-.34 11.324zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/iff.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ifo.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M28.023 32c0 1.04.344 2.074 1.211 2.766 1.555 1.558 4.149 1.558 5.707 0 .692-.692 1.211-1.727 1.211-2.766s-.347-2.074-1.21-2.766c-.692-.695-1.731-1.21-2.77-1.21-1.035 0-2.074.343-2.766 1.21-1.039.692-1.383 1.727-1.383 2.766zm0 0" fill="#bababa"/><path d="M9.34 9.34c-12.453 12.453-12.453 32.691 0 45.32 12.453 12.453 32.691 12.453 45.32 0 12.453-12.453 12.453-32.691 0-45.32-12.453-12.453-32.867-12.453-45.32 0zm47.394 36.152c-1.21 2.074-2.765 4.153-4.496 5.88-1.73 1.73-3.804 3.288-5.883 4.5l-7.437-14.184s.691-.176 2.078-1.56c1.383-1.382 1.727-2.073 1.727-2.073zM37.707 26.293c1.559 1.555 2.422 3.633 2.422 5.707s-.863 4.152-2.422 5.707a7.933 7.933 0 0 1-11.242 0c-1.559-1.555-2.422-3.633-2.422-5.707s.691-4.152 2.422-5.707c2.941-3.113 8.129-3.113 11.242 0zm-10.895-5.535s-1.558.863-2.769 2.246c-1.211 1.387-1.211 1.558-1.73 2.25l-14.184-7.61c1.21-2.078 2.77-4.152 4.5-5.882 1.902-1.73 3.805-3.285 5.879-4.496zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/image.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M64 32C64 14.43 49.57 0 32 0S0 14.43 0 32s14.43 32 32 32c17.57-.629 32-14.43 32-32zm0 0" fill="#3c3"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/img.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#eab41b"><path d="M28.023 32c0 1.04.344 2.074 1.211 2.766 1.555 1.558 4.149 1.558 5.707 0 .692-.692 1.211-1.727 1.211-2.766s-.347-2.074-1.21-2.766c-.692-.695-1.731-1.21-2.77-1.21-1.035 0-2.074.343-2.766 1.21-1.039.692-1.383 1.727-1.383 2.766zm0 0"/><path d="M9.34 9.34c-12.453 12.453-12.453 32.691 0 45.32 12.453 12.453 32.691 12.453 45.32 0 12.453-12.453 12.453-32.691 0-45.32-12.453-12.453-32.867-12.453-45.32 0zm47.394 36.152c-1.21 2.074-2.765 4.153-4.496 5.88-1.73 1.73-3.804 3.288-5.883 4.5l-7.437-14.184s.691-.176 2.078-1.56c1.383-1.382 1.727-2.073 1.727-2.073zM37.707 26.293c1.559 1.555 2.422 3.633 2.422 5.707s-.863 4.152-2.422 5.707a7.933 7.933 0 0 1-11.242 0c-1.559-1.555-2.422-3.633-2.422-5.707s.691-4.152 2.422-5.707c2.941-3.113 8.129-3.113 11.242 0zm-10.895-5.535s-1.558.863-2.769 2.246c-1.211 1.387-1.211 1.558-1.73 2.25l-14.184-7.61c1.21-2.078 2.77-4.152 4.5-5.882 1.902-1.73 3.805-3.285 5.879-4.496zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/iml.svg
1
1
<svg height="64" width="48" xmlns="http://www.w3.org/2000/svg"><g fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".5"><path d="M44.2 75.3c7.2-3.701 3.9-7.3 1.5-6.799-.6.099-.801.2-.801.2s.2-.3.601-.5C50.1 66.6 53.6 73 44 75.5zm-6.4-10.5c1.801 2.1-.5 4-.5 4s4.7-2.4 2.5-5.5c-2-2.8-3.6-4.2 4.8-9.101 0 .101-13.1 3.401-6.8 10.6" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M39.8 46.499s3.999 4-3.8 10.102c-6.2 4.898-1.4 7.7 0 10.899-3.601-3.3-6.3-6.2-4.5-8.8 2.7-4 9.9-5.9 8.3-12.201M31 76.8s-1.5.9 1 1.1c3 .299 4.6.299 7.9-.3 0 0 .9.599 2.1 1-7.4 3.3-16.901-.1-11-1.8m-.9-4.2s-1.6 1.199.9 1.5c3.2.3 5.8.4 10.2-.5 0 0 .6.6 1.599 1-9.1 2.6-19.199.2-12.698-2" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M47.7 79.9s1.1.9-1.2 1.599c-4.3 1.302-18 1.702-21.8.101-1.4-.6 1.2-1.4 2-1.6.8-.2 1.3-.1 1.3-.1-1.5-1.1-9.8 2.1-4.2 3 15.3 2.4 27.9-1.199 23.9-3M31.7 68.3s-7 1.702-2.499 2.301c1.9.301 5.699.2 9.2-.101 2.9-.2 5.799-.8 5.799-.8s-1 .4-1.8.901c-7.1 1.9-20.7.999-16.8-.9 3.4-1.6 6.1-1.401 6.1-1.401" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M32.399 85.4c6.901.4 17.502-.2 17.7-3.5 0 0-.499 1.2-5.699 2.2-5.899 1.1-13.101 1-17.5.3.1 0 1 .7 5.499 1" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/inc.svg
1
1
<svg height="64" width="71" xmlns="http://www.w3.org/2000/svg"><path d="m12.225 17.126-7.7 7.598c-.9 1.001-1.4 2.201-1.4 3.501 0 1.2.5 2.5 1.4 3.4l.1.1c.9.9 2.2 1.4 3.4 1.4 1.3 0 2.5-.5 3.5-1.4l8.6-8.6 9.3-9.3c.4-.4.6-1 .6-1.5s-.2-1.101-.6-1.5-1-.6-1.5-.6-1.1.2-1.5.6l-13.4 13.3c-.6.6-1.5.6-2.2 0-.6-.6-.6-1.6 0-2.2l13.3-13.299c1-1 2.3-1.502 3.7-1.502 1.3 0 2.7.501 3.7 1.502 1 .998 1.5 2.299 1.5 3.698 0 1.3-.5 2.7-1.5 3.701l-9.3 9.3-8.6 8.6c-1.5 1.5-3.6 2.3-5.6 2.3-2 0-4-.7-5.499-2.2l-.1-.1c-1.5-1.5-2.3-3.501-2.3-5.6 0-2.001.8-4.1 2.3-5.6l8.6-8.599 10.899-11c2-2.001 4.6-3.002 7.3-3.002s5.3 1 7.3 3.002c1.999 1.999 2.999 4.6 2.999 7.3 0 2.6-1 5.3-3 7.299l-14.9 14.9c-.6.599-1.599.599-2.199 0-.6-.6-.6-1.6 0-2.201l14.9-14.898c1.4-1.4 2.1-3.301 2.1-5.1s-.7-3.701-2.1-5.101-3.3-2.1-5.2-2.1c-1.9 0-3.7.7-5.1 2.1zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".25" transform="matrix(1.7579 0 0 1.76066 .65 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/indd.svg
1
1
<svg height="64" width="58" xmlns="http://www.w3.org/2000/svg"><path d="M.624 29.625V.725h3.9v28.9zm19.3.4c-6.5 0-9.899-4-9.899-9.9s3.4-10.6 9.9-10.6c1.1 0 2.3.1 3.5.4v-9.3h3.7v28c-1.6.8-4.2 1.4-7.2 1.4zm3.501-18.2c-.9-.2-1.9-.4-2.9-.4-5.1 0-6.8 4-6.8 8.3 0 4.7 1.8 8.1 6.4 8.1 1.5 0 2.5-.2 3.3-.6zm0 0" fill="#db007b" stroke="#db007b" stroke-miterlimit="10" stroke-width="1.25" transform="matrix(2.09009 0 0 2.08311 0 .076)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/inf.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M41.266 22.992c0-3.945-2.403-7.035-5.664-8.406V3.262C35.602 1.372 34.23 0 32.344 0s-3.262 1.371-3.262 3.262v11.324c-3.43 1.2-5.66 4.46-5.66 8.406 0 3.945 2.402 7.035 5.66 8.406 0 .172-.172.516-.172.688V60.57c0 1.887 1.375 3.258 3.262 3.258s3.258-1.371 3.258-3.258V31.914c0-.344 0-.516-.168-.687 3.601-1.028 6.004-4.29 6.004-8.235zm-9.094 2.574c-1.371 0-2.402-1.03-2.402-2.402 0-1.375 1.03-2.402 2.402-2.402s2.402 1.027 2.402 2.402c.172 1.2-1.031 2.402-2.402 2.402zM58.254 3.602c0-1.887-1.375-3.258-3.262-3.258s-3.262 1.37-3.262 3.258v26.597c-3.43 1.2-5.66 4.461-5.66 8.406 0 3.946 2.403 7.036 5.66 8.407 0 .172-.171.515-.171.687v13.04c0 1.89 1.375 3.261 3.261 3.261 1.887 0 3.262-1.371 3.262-3.262V47.7c0-.344 0-.515-.172-.687 3.43-1.2 5.66-4.461 5.66-8.407 0-3.945-2.402-7.035-5.66-8.406V3.602zm-3.262 37.406c-1.37 0-2.402-1.028-2.402-2.403 0-1.37 1.031-2.402 2.402-2.402 1.371 0 2.403 1.031 2.403 2.402 0 1.375-1.032 2.403-2.403 2.403zm-48.73 19.39c0 1.887 1.375 3.258 3.261 3.258 1.887 0 3.258-1.37 3.258-3.258V47.355c0-.343 0-.511-.172-.683 3.434-1.203 5.664-4.461 5.664-8.41 0-3.946-2.402-7.035-5.664-8.407V3.602c0-1.887-1.37-3.258-3.257-3.258S6.09 1.714 6.09 3.602v26.597C2.66 31.4.43 34.66.43 38.605c0 3.946 2.402 7.036 5.66 8.407 0 .172-.172.515-.172.687v13.04c0-.34.344-.34.344-.34zm3.261-24.367c1.372 0 2.403 1.032 2.403 2.403 0 1.375-1.031 2.402-2.403 2.402-1.375 0-2.402-1.027-2.402-2.402 0-1.371 1.027-2.403 2.402-2.403zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/info.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#999"><path d="M35.2 13.332c-1.067 0-1.954.355-2.665 1.066-.715.711-1.07 1.602-1.07 2.668s.355 1.957 1.07 2.668c.711.711 1.598 1.067 2.664 1.067 1.067 0 1.957-.356 2.668-1.067.711-.71 1.067-1.601 1.067-2.668s-.356-1.957-1.067-2.668c-.535-.71-1.422-1.066-2.668-1.066zm1.777 12.09-.176.355h.176zm-.176.355c-3.555.535-6.934.711-10.489 1.246l-.355 1.598h.887c.535 0 1.066.18 1.422.535s.535.711.535 1.067c0 .53-.18.886-.535 2.132l-3.73 12.622C24.18 46.043 24 46.754 24 47.465c0 1.07.355 1.781 1.066 2.492.711.711 2.844.887 3.91.887 3.024 0 8-1.598 10.669-6.223l-2.133-1.242c-1.067 1.777-3.024 3.02-4.09 3.555-1.067.53-1.602.355-1.777.355-.18 0-.356 0-.536-.18-.175-.175-.175-.355-.175-.53 0-.356.175-1.067.53-2.134zm0 0"/><path d="M32 1.777C15.29 1.777 1.777 15.29 1.777 32S15.29 62.223 32 62.223 62.223 48.71 62.223 32 48.71 1.777 32 1.777zm0 3.38c14.934 0 26.844 12.09 26.844 26.843 0 14.934-12.09 26.844-26.844 26.844S5.156 46.754 5.156 32C5.156 17.066 17.066 5.156 32 5.156zm0 0" stroke="#999" stroke-miterlimit="10" stroke-width="3.55556"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ini.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M41.266 22.992c0-3.945-2.403-7.035-5.664-8.406V3.262C35.602 1.372 34.23 0 32.344 0s-3.262 1.371-3.262 3.262v11.324c-3.43 1.2-5.66 4.46-5.66 8.406 0 3.945 2.402 7.035 5.66 8.406 0 .172-.172.516-.172.688V60.57c0 1.887 1.375 3.258 3.262 3.258s3.258-1.371 3.258-3.258V31.914c0-.344 0-.516-.168-.687 3.601-1.028 6.004-4.29 6.004-8.235zm-9.094 2.574c-1.371 0-2.402-1.03-2.402-2.402 0-1.375 1.03-2.402 2.402-2.402s2.402 1.027 2.402 2.402c.172 1.2-1.031 2.402-2.402 2.402zM58.254 3.602c0-1.887-1.375-3.258-3.262-3.258s-3.262 1.37-3.262 3.258v26.597c-3.43 1.2-5.66 4.461-5.66 8.406 0 3.946 2.403 7.036 5.66 8.407 0 .172-.171.515-.171.687v13.04c0 1.89 1.375 3.261 3.261 3.261 1.887 0 3.262-1.371 3.262-3.262V47.7c0-.344 0-.515-.172-.687 3.43-1.2 5.66-4.461 5.66-8.407 0-3.945-2.402-7.035-5.66-8.406V3.602zm-3.262 37.406c-1.37 0-2.402-1.028-2.402-2.403 0-1.37 1.031-2.402 2.402-2.402 1.371 0 2.403 1.031 2.403 2.402 0 1.375-1.032 2.403-2.403 2.403zm-48.73 19.39c0 1.887 1.375 3.258 3.261 3.258 1.887 0 3.258-1.37 3.258-3.258V47.355c0-.343 0-.511-.172-.683 3.434-1.203 5.664-4.461 5.664-8.41 0-3.946-2.402-7.035-5.664-8.407V3.602c0-1.887-1.37-3.258-3.257-3.258S6.09 1.714 6.09 3.602v26.597C2.66 31.4.43 34.66.43 38.605c0 3.946 2.402 7.036 5.66 8.407 0 .172-.172.515-.172.687v13.04c0-.34.344-.34.344-.34zm3.261-24.367c1.372 0 2.403 1.032 2.403 2.403 0 1.375-1.031 2.402-2.403 2.402-1.375 0-2.402-1.027-2.402-2.402 0-1.371 1.027-2.403 2.402-2.403zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/inv.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M47.914 26.813V9.688L31.828 0 15.914 9.512v17.3L0 35.462v19.2L16.434 64 32 55.004 47.566 64 64 54.66V35.633zm-2.941 0-11.59 6.398V20.066l11.59-6.746zM31.828 3.633l11.414 6.918-11.414 6.746-11.07-6.918zM4.844 36.324l12.8-6.746 11.243 6.399-12.453 7.265zm12.972 9.512 12.625-7.262v13.492l-12.625 7.438zm17.47-9.86 11.245-6.398 12.797 6.918-11.762 6.746zm25.6 16.782-11.761 6.746V45.836l11.762-6.742zm0 0" fill="#938886"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/iso.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#eab41b"><path d="M28.023 32c0 1.04.344 2.074 1.211 2.766 1.555 1.558 4.149 1.558 5.707 0 .692-.692 1.211-1.727 1.211-2.766s-.347-2.074-1.21-2.766c-.692-.695-1.731-1.21-2.77-1.21-1.035 0-2.074.343-2.766 1.21-1.039.692-1.383 1.727-1.383 2.766zm0 0"/><path d="M9.34 9.34c-12.453 12.453-12.453 32.691 0 45.32 12.453 12.453 32.691 12.453 45.32 0 12.453-12.453 12.453-32.691 0-45.32-12.453-12.453-32.867-12.453-45.32 0zm47.394 36.152c-1.21 2.074-2.765 4.153-4.496 5.88-1.73 1.73-3.804 3.288-5.883 4.5l-7.437-14.184s.691-.176 2.078-1.56c1.383-1.382 1.727-2.073 1.727-2.073zM37.707 26.293c1.559 1.555 2.422 3.633 2.422 5.707s-.863 4.152-2.422 5.707a7.933 7.933 0 0 1-11.242 0c-1.559-1.555-2.422-3.633-2.422-5.707s.691-4.152 2.422-5.707c2.941-3.113 8.129-3.113 11.242 0zm-10.895-5.535s-1.558.863-2.769 2.246c-1.211 1.387-1.211 1.558-1.73 2.25l-14.184-7.61c1.21-2.078 2.77-4.152 4.5-5.882 1.902-1.73 3.805-3.285 5.879-4.496zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/j2.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M39.06.14s-7.599 3.599-13.7 4.4c-6.1.798-17.9 1.6-20.2 1-2.3-.601-4-1.201-4-1.201l.501 1.6-1.5 1.3.3.5h.6l1.9.201.3.799 1.1.1.501 1.8s2.2.2 2.9.2c.698 0 1.999-.101 1.999-.101v.7l.501.101v.801l-1.1.9h.3v.298s-2.4.201-3.302 0c-1-.098-1.098-.098-1.098-.098l-.101.098v.402h.2l.101 1.798 4.899-.198-.499 6.6v.798l-4.101-.198v-1.7h.9l.1-.701.801-.2.099-.2-3.2-.7-2.4.7.4.3h.4v.6h.8v1.6l-.899.2.2.4.2.1v1.301h.6v5.9l-.901.1.101 1.7.6.098-.101 4h2.5l-.499-3.8 3.798-.1-.299 2.1-.4 1.502h3.801v-3.702l2.599-.198-.099 2.398-.1 1.401h2.198l-.099-3.8h.301l.299-1.9h-.4l-.1-.8-.2-2.2.1-2.5h.501v-1.5h-.602l.101-1.8.701-.1v-.601l.4-.098.4-.301-2.5-.5-2.5.6.2.4h.6l.098.7h.701v1.6l-3-.1.1-1.7.101-1.5v-2l.1-2.099 5.999-.301 7.1-.4.101 1.299-.301 3.202-.099 2.999h-2.6v-2.1h.999v-.6l.4-.1v-.101h.4l.2-.299-3-.7-2.9.6.2.399h.3v.2h.4v.701h.9v1.9h-.9v.4h.3v1.6h.6l-.099 6.4-.802.2v.3h.301v1.6h.501l-.2 2.3-.2 1.9 3.1.101-.101-1.7-.2-1.401v-1.2h2.601l-.101 1.9v2l1.5.1 2-.1.6-.2-.4-1.1-.1-1.7-.2-1.2 2.5-.1-.1 1.5v2.3l1 .1h1.001l.499-.2-.3-2.2-.1-1.6h.4v-1.5h.1v-.299h-.5l-.099-1.1V24.74h.4v-1.6h.2v-.301h-.6V21.04l.898-.1-.098-.601h.4v-.1l.498-.3-2.7-.6-2.5.6.2.3h.402v.1h.4v.7h.898l.102 1.7H28.06l-.4-2.6-.299-1.4-.2-2.501.1-1.399 6.3-.5v-2.1l.298-.1v-.301l-.2-.098s-3.5.5-4.6.6c-.298 0-.298 0-.498.098v-.4l-1.3-.7v-.799h.5v-1.1s2.7-.2 3.7-.399c.999-.2 2.398-.502 2.398-.502l.701-1.798 1.3-.402.1-.299 3-.801.3-.299-1.1-2.499.1-.6.6-.301.4-1.299zM24.662 9.638v.901h.699v.9l-1.3.901-.1.299h.501v.2l-4.2.2v-.6l.2-.3v-.6l.099-.299v-.402l.2-.698zm-8.502.801-.098.6.298.4v.7l.301.5-.1.5.2.301-4.199.199v-.1l.499-.099v-.2l-1.099-.601-.1-.9h.5v-1.001zm6.101 14.1 2.5.2-.099 3.4.299 2.8-2.7-.1zm-16.9.1 4 .2-.3 2.3v2l.3 1.1-4 .2zm25.9 0v5.8l-2.6.2-.1-3.1-.3-2.8zm-19.5.1h3v5.4h-2.799l-.1-1.7v-1.7zm0 0" fill="#b41717" stroke="#b41717" stroke-miterlimit="10" stroke-width=".25" transform="matrix(1.7 0 0 1.71166 0 .105)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/jar.svg
1
1
<svg height="64" width="48" xmlns="http://www.w3.org/2000/svg"><g stroke-miterlimit="10" stroke-width=".5"><path d="M44.2 75.3c7.2-3.701 3.9-7.3 1.5-6.799-.6.099-.801.2-.801.2s.2-.3.601-.5C50.1 66.6 53.6 73 44 75.5zm0 0" fill="#265db4" stroke="#265db4" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M37.8 64.8c1.801 2.1-.5 4-.5 4s4.7-2.4 2.5-5.5c-2-2.8-3.6-4.2 4.8-9.101 0 .101-13.1 3.401-6.8 10.6" fill="#c00" stroke="#c00" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M39.8 46.499s3.999 4-3.8 10.102c-6.2 4.898-1.4 7.7 0 10.899-3.601-3.3-6.3-6.2-4.5-8.8 2.7-4 9.9-5.9 8.3-12.201" fill="#c00" stroke="#c00" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><g fill="#265db4" stroke="#265db4"><path d="M31 76.8s-1.5.9 1 1.1c3 .299 4.6.299 7.9-.3 0 0 .9.599 2.1 1-7.4 3.3-16.901-.1-11-1.8m-.9-4.2s-1.6 1.199.9 1.5c3.2.3 5.8.4 10.2-.5 0 0 .6.6 1.599 1-9.1 2.6-19.199.2-12.698-2" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M47.7 79.9s1.1.9-1.2 1.599c-4.3 1.302-18 1.702-21.8.101-1.4-.6 1.2-1.4 2-1.6.8-.2 1.3-.1 1.3-.1-1.5-1.1-9.8 2.1-4.2 3 15.3 2.4 27.9-1.199 23.9-3M31.7 68.3s-7 1.702-2.499 2.301c1.9.301 5.699.2 9.2-.101 2.9-.2 5.799-.8 5.799-.8s-1 .4-1.8.901c-7.1 1.9-20.7.999-16.8-.9 3.4-1.6 6.1-1.401 6.1-1.401" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M32.399 85.4c6.901.4 17.502-.2 17.7-3.5 0 0-.499 1.2-5.699 2.2-5.899 1.1-13.101 1-17.5.3.1 0 1 .7 5.499 1" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/></g></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/java.svg
1
1
<svg height="64" width="48" xmlns="http://www.w3.org/2000/svg"><g stroke-miterlimit="10" stroke-width=".5"><path d="M44.2 75.3c7.2-3.701 3.9-7.3 1.5-6.799-.6.099-.801.2-.801.2s.2-.3.601-.5C50.1 66.6 53.6 73 44 75.5zm0 0" fill="#265db4" stroke="#265db4" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M37.8 64.8c1.801 2.1-.5 4-.5 4s4.7-2.4 2.5-5.5c-2-2.8-3.6-4.2 4.8-9.101 0 .101-13.1 3.401-6.8 10.6" fill="#c00" stroke="#c00" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M39.8 46.499s3.999 4-3.8 10.102c-6.2 4.898-1.4 7.7 0 10.899-3.601-3.3-6.3-6.2-4.5-8.8 2.7-4 9.9-5.9 8.3-12.201" fill="#c00" stroke="#c00" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><g fill="#265db4" stroke="#265db4"><path d="M31 76.8s-1.5.9 1 1.1c3 .299 4.6.299 7.9-.3 0 0 .9.599 2.1 1-7.4 3.3-16.901-.1-11-1.8m-.9-4.2s-1.6 1.199.9 1.5c3.2.3 5.8.4 10.2-.5 0 0 .6.6 1.599 1-9.1 2.6-19.199.2-12.698-2" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M47.7 79.9s1.1.9-1.2 1.599c-4.3 1.302-18 1.702-21.8.101-1.4-.6 1.2-1.4 2-1.6.8-.2 1.3-.1 1.3-.1-1.5-1.1-9.8 2.1-4.2 3 15.3 2.4 27.9-1.199 23.9-3M31.7 68.3s-7 1.702-2.499 2.301c1.9.301 5.699.2 9.2-.101 2.9-.2 5.799-.8 5.799-.8s-1 .4-1.8.901c-7.1 1.9-20.7.999-16.8-.9 3.4-1.6 6.1-1.401 6.1-1.401" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M32.399 85.4c6.901.4 17.502-.2 17.7-3.5 0 0-.499 1.2-5.699 2.2-5.899 1.1-13.101 1-17.5.3.1 0 1 .7 5.499 1" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/></g></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/jpg.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/js.svg
1
1
<svg height="64" width="75" xmlns="http://www.w3.org/2000/svg"><path d="M.5 19v-4.1c.9-.1 1.6-.2 2-.4.4-.2.8-.6 1.2-1.001.4-.5.5-1.1.7-1.9.1-.6.2-1.499.2-2.799 0-2.201.1-3.7.4-4.6.2-.8.6-1.6 1.2-2 .5-.5 1.4-.9 2.5-1.2.7-.2 1.9-.4 3.5-.4h.9v3.9c-1.3 0-2.2.1-2.6.3-.4.2-.6.4-.9.6-.2.3-.3.7-.3 1.501 0 .8-.1 2-.2 4.099-.101 1.2-.2 2-.4 2.801-.301.6-.6 1.2-1 1.8-.4.4-1 .9-1.8 1.399.7.4 1.3.8 1.8 1.3s.8 1.2 1.1 1.899c.3.702.4 1.802.4 3.001.1 1.9.1 3.1.1 3.599 0 .702.1 1.202.3 1.602.2.4.5.5.9.6.4.2 1.2.3 2.6.3v4.098h-1c-1.6 0-2.9-.1-3.701-.4-.9-.3-1.6-.6-2.2-1.2-.6-.6-.999-1.2-1.2-1.999-.198-.8-.299-2.1-.299-4 0-2-.1-3.5-.3-4.1-.3-.9-.7-1.601-1.201-2-.698-.5-1.5-.7-2.7-.7zm39.1 0c-.9.1-1.6.2-2 .4s-.8.6-1.2 1.001c-.4.5-.5 1.1-.7 1.9-.099.6-.2 1.499-.2 2.799 0 2.201-.1 3.7-.4 4.6-.2.9-.6 1.6-1.2 2-.5.5-1.4.9-2.5 1.2-.7.2-1.9.4-3.5.4h-.999v-4.1c1.298 0 2.1-.1 2.599-.3s.7-.4.899-.6c.2-.3.301-.7.301-1.501 0-.6.1-2 .2-3.999.099-1.2.3-2.1.5-2.8.3-.7.6-1.3 1.1-1.9.4-.5 1-.9 1.7-1.3-.901-.6-1.6-1.1-2-1.6-.5-.7-1-1.801-1.201-2.8-.199-.8-.299-2.6-.299-5.2 0-.8-.1-1.4-.301-1.8-.199-.3-.4-.5-.799-.6-.2-.3-1-.3-2.5-.3v-4h.999c1.602 0 2.9.1 3.7.4.902.3 1.6.6 2.2 1.2.6.6 1.002 1.2 1.2 2 .201.8.402 2.1.402 4 0 2 .098 3.4.299 4.1.299.9.7 1.601 1.2 1.9.5.4 1.401.6 2.5.6.1.1 0 4.3 0 4.3zm0 0" fill="#307ac6" stroke="#307ac6" stroke-miterlimit="10" transform="matrix(1.86825 0 0 1.87558 0 .209)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/json.svg
1
1
<svg height="64" width="77" xmlns="http://www.w3.org/2000/svg"><g fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".5"><path d="M16.4 67.5v-4.1c.901-.1 1.6-.2 2-.4.4-.2.8-.6 1.2-1 .4-.5.5-1.1.7-1.9.1-.6.2-1.5.2-2.8 0-2.2.1-3.7.4-4.6.2-.8.599-1.6 1.2-2 .5-.5 1.4-.9 2.5-1.2.7-.2 1.9-.4 3.5-.4h.9V53c-1.3 0-2.2.1-2.6.3-.4.2-.6.4-.9.6-.2.3-.3.7-.3 1.5 0 .801-.1 2-.2 4.1-.1 1.2-.2 2-.4 2.8-.3.6-.6 1.2-1 1.8-.4.4-1 .9-1.8 1.401.7.4 1.301.8 1.8 1.299.5.499.801 1.2 1.1 1.9.3.7.4 1.8.4 3 .1 1.9.1 3.1.1 3.6 0 .7.1 1.2.3 1.6.199.4.5.5.9.6.4.2 1.2.3 2.6.3v4.1h-1c-1.6 0-2.9-.1-3.7-.4-.9-.3-1.6-.6-2.2-1.199-.601-.601-1-1.2-1.2-2.002-.2-.799-.3-2.1-.3-4 0-2-.1-3.5-.3-4.1-.3-.898-.7-1.6-1.2-1.999-.7-.5-1.5-.7-2.7-.7zm39.1 0c-.9.1-1.6.2-2 .4-.401.2-.8.6-1.2 1-.4.5-.499 1.1-.7 1.9-.1.6-.2 1.5-.2 2.8 0 2.2-.1 3.7-.4 4.6-.2.9-.6 1.6-1.2 2-.5.5-1.4.9-2.501 1.2-.698.2-1.9.4-3.5.4h-1v-4.1c1.3 0 2.101-.1 2.6-.3.501-.2.7-.4.902-.6.2-.3.3-.7.3-1.5 0-.601.098-2 .199-4 .1-1.2.3-2.099.499-2.8.302-.7.602-1.3 1.1-1.9.401-.5 1.001-.9 1.701-1.3-.9-.6-1.6-1.1-2-1.6-.5-.7-1-1.8-1.2-2.8-.2-.8-.3-2.6-.3-5.2 0-.8-.1-1.401-.3-1.8-.2-.3-.4-.5-.8-.6-.2-.3-1-.3-2.5-.3v-4h1c1.6 0 2.9.1 3.7.4.9.3 1.6.6 2.2 1.199.6.601 1 1.2 1.2 2.002.2.799.4 2.1.4 4 0 2 .1 3.4.301 4.1.3.898.698 1.6 1.2 1.9.499.399 1.398.598 2.499.598.1.1 0 4.302 0 4.302zm0 0" transform="matrix(1.90195 0 0 1.91617 -29.917 -93.413)"/><path d="M44.1 67.1c-.7-.3-1.2-.9-1.2-1.599 0-.701.5-1.4 1.2-1.6.299-.1.4-.3.299-.502-.3-.799-.499-1.598-.998-2.2-.1-.3-.4-.3-.602-.2-.2.1-.499.3-.799.3-1 0-1.7-.799-1.7-1.7 0-.3.1-.599.3-.799.1-.3 0-.4-.2-.6-.7-.4-1.499-.7-2.2-1-.3-.1-.4.1-.5.3-.3.7-.9 1.2-1.6 1.2s-1.4-.5-1.6-1.2c-.101-.3-.3-.4-.5-.3-.8.3-1.6.5-2.2 1-.301.1-.301.4-.2.6.2.3.3.5.3.8 0 1-.8 1.7-1.699 1.7-.302 0-.602-.1-.801-.3-.3-.1-.4 0-.6.2-.4.7-.7 1.5-1 2.2-.1.3.1.4.3.5.7.3 1.2.9 1.2 1.601 0 .7-.5 1.398-1.2 1.598-.3.1-.4.302-.3.502.3.799.5 1.6 1 2.2.1.299.4.299.6.2.3-.2.5-.3.801-.3.998 0 1.699.799 1.699 1.7 0 .3-.1.599-.3.799-.101.3 0 .4.2.6.7.4 1.5.7 2.2 1 .2 0 .399-.1.399-.3.302-.7.902-1.2 1.602-1.2.698 0 1.399.5 1.6 1.2.098.3.3.4.499.3.801-.3 1.6-.5 2.2-1 .3-.1.3-.4.2-.6-.1-.3-.301-.5-.301-.8 0-1 .801-1.7 1.7-1.7.3 0 .6.1.802.3.3.1.4 0 .6-.2.4-.701.7-1.5 1-2.2.199-.1.098-.4-.202-.5zm-8.3 1c-1.5 0-2.699-1.2-2.699-2.701 0-1.498 1.2-2.699 2.699-2.699 1.499 0 2.7 1.2 2.7 2.699.1 1.5-1.201 2.701-2.7 2.701zm0 0" transform="matrix(1.90195 0 0 1.91617 -29.917 -93.413)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/jsp.svg
1
1
<svg height="63" width="49" xmlns="http://www.w3.org/2000/svg"><path d="M24.842 21.014c2.2-.7 4.4.7 4.202 2.8-.2 2.4-3.302 3.601-5.1 4.2l.1-.099-.1-.101c1.3-1 3.898-2.199 3.7-4.6-.1-1.2-1.001-2.098-2.702-2v-.2zm-16.099.401.1-.101c-1 0-1.901.4-2.701.7-.798.3-1.8.401-2.198 1.3.399.7 1.4.7 2.398.802 3.4.5 8.302.398 11.701 0 1.799-.201 3.4-.401 4.2-1.201l-.1-.101.1-.1c-3.4.402-7.8 1-11.9.8-1.301-.099-3-.099-3.7-.8.4-.7 1.4-.9 2.1-1.299zm19.9 14.099v-.1c-5.1 2.5-13.201 2.8-20.5 2.201l.1.1-.1.2c2.999.5 6.9.7 10.7.398 3.7-.198 8.199-.698 9.9-2.698zm-14.4-15.398h.1c-.8-1.802-2.3-2.602-2.499-4.7-.2-1.902.7-3.102 1.598-4 1.101-1.201 2.702-2.201 3.901-3.5 1.6-1.803 3.4-4.5 1.9-7.102l-.101.101-.299-.101c.4 2.5-.6 4.101-1.901 5.4-.999 1.201-2.6 2.201-4 3.3-1.6 1.3-3.7 2.901-3.1 5.3.502 2.302 2.8 3.9 4.102 5.4zm8-11.602-.1-.099c-2.7 1-6.701 2.6-7.1 5.698-.1 1.503.399 2.602.9 3.401.4.602 1.1 1 1.3 1.901.2.8 0 1.6-.2 2.2h.1l.1.1c1.099-.8 2.2-1.901 1.899-3.401-.198-1.5-1.9-2.5-2.1-3.899-.1-.802.1-1.5.401-1.9 1.1-1.701 3.5-2.902 4.8-4zm-13.8 17.401-.101-.101c-.5.301-1.5.4-1.4 1.2.1.8 1.5 1 2.2 1.2 3.7.8 9.2.3 11.902-.599l-.1-.101.1-.099c-.301-.101-.7-.7-1.3-.7-.502-.1-1.6.299-2.602.4-1.6.199-3.299.3-4.799.199-1.101-.1-4.5-.1-3.9-1.399zm.9 4.099.1-.1c-.6.201-1.3.4-1.3 1.1 0 .601 1.2 1 1.9 1.3 3.299 1 8.5.4 10.9-.699-.2-.302-.6-.4-.9-.601-.4-.1-.7-.3-1.1-.5-2.001.5-5.1.7-7.5.4-.7-.1-1.701-.1-1.9-.799zm17.699 3.2-.1-.099c-.098 1-1.3 1.1-2.1 1.3-.898.2-1.9.398-2.998.5-4.902.599-11.5.898-16.302 0-.898-.102-2.2-.401-2.499-1.102.4-.698 1.5-.8 2.399-1.198l-.098-.101.098-.1c-1.2.1-2.1.4-2.998.701-.7.3-1.701.698-1.902 1.5.6.8 1.801.8 2.8 1 6.6 1 15.7 1.198 21.402-.7.998-.401 3.098-1 2.1-1.901zm-3.7-5.099c.2 0 .4-.101.702-.2m.898-6.8c-.198 0-.399.1-.7.1m-2.2 1.7c.1 0 .2-.101.401-.101m-12.5-1.6c-.4.1-.8.1-1.3.201m-2.2 15.898c.499.2 1.099.2 1.7.401m20.5-2.2c.1-.1.2-.2.3-.399M19.043.814c0-.099-.098-.3-.098-.399m-4.702 19.7c.1.1.301.4.402.5m2.298 1.199c.1-.1.2-.198.3-.399m5.7-13.2c-.3.099-.499.2-.7.398m-1.198 18.802h.198m-12.6-1.8c0 .1-.2.1-.2.199m.9 4.2.101-.1m-2.8 2.701c-.4 0-.7.1-1 .1m21.399.5c0-.1-.1-.1-.1-.1h-.098" fill="#666" stroke="#666" stroke-miterlimit="10" transform="matrix(1.63519 0 0 1.61722 .336 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/jsx.svg
1
1
<svg height="64" width="72" xmlns="http://www.w3.org/2000/svg"><g fill="#72bed3" stroke="#72bed3" stroke-miterlimit="10" stroke-width=".75"><path d="M56.4 65.6c0-2.7-3.4-5.3-8.5-6.9 1.2-5.3.7-9.5-1.7-10.9-.6-.3-1.2-.4-1.9-.4v1.9c.4 0 .7.1.9.201 1.1.7 1.7 3.098 1.2 6.299-.1.8-.2 1.6-.4 2.4-1.7-.401-3.4-.7-5.3-.9-1.099-1.6-2.3-2.9-3.4-4.2 2.8-2.5 5.3-3.9 7-3.9V47.3c-2.3 0-5.3 1.7-8.301 4.5-3.098-2.8-6.098-4.4-8.3-4.4V49.3c1.802 0 4.201 1.3 7 3.9-1.198 1.2-2.3 2.6-3.4 4.2-1.9.2-3.698.399-5.3.9-.098-.9-.298-1.7-.4-2.4-.4-3.1.102-5.7 1.201-6.301.2-.098.6-.198.9-.198v-1.902c-.7 0-1.3.1-1.9.402-2.3 1.3-2.8 5.498-1.7 10.899-5.3 1.6-8.7 4.199-8.7 6.9 0 2.7 3.4 5.3 8.5 6.9-1.2 5.301-.7 9.5 1.7 10.9.6.3 1.2.401 1.9.401 2.3 0 5.3-1.7 8.301-4.5 3.098 2.8 6.098 4.4 8.3 4.4.698 0 1.299-.1 1.9-.401C48.3 82.1 48.8 77.9 47.7 72.5c5.4-1.7 8.7-4.2 8.7-6.9zm-10.8-5.601c-.3 1.1-.7 2.202-1.1 3.3-.3-.699-.701-1.299-1.099-1.998-.4-.7-.8-1.302-1.2-2.002 1.2.2 2.3.401 3.399.7zM41.8 68.9c-.7 1.099-1.3 2.2-1.999 3.098-1.2.1-2.401.2-3.7.2s-2.501-.1-3.8-.1c-.7-.998-1.3-1.999-2.002-3.1-.7-1.099-1.198-2.2-1.799-3.299.6-1.1 1.1-2.2 1.799-3.3.702-1.1 1.302-2.2 2.001-3.101 1.2-.098 2.5-.098 3.8-.098s2.5.098 3.8.098c.7 1 1.3 2.002 1.999 3.1.701 1.101 1.2 2.2 1.801 3.301-.7 1.001-1.3 2.1-1.9 3.2zm2.7-1.101c.4 1.1.9 2.2 1.199 3.3-1.1.2-2.298.4-3.4.7.4-.7.8-1.301 1.2-2 .3-.6.601-1.3 1.001-2zM36 76.7C35.2 75.9 34.4 74.9 33.7 74c.798 0 1.5.1 2.298.1.8 0 1.6 0 2.3-.1-.7.999-1.5 1.9-2.3 2.7zm-6.2-4.9c-1.2-.1-2.3-.4-3.399-.7.3-1.1.7-2.199 1.1-3.3.301.7.699 1.3 1.1 2 .4.699.8 1.3 1.2 2zM36 54.5c.8.8 1.6 1.799 2.3 2.7-.798 0-1.5-.1-2.3-.1-.798 0-1.599 0-2.298.1.7-1 1.5-1.9 2.298-2.7zm-6.2 4.9c-.4.7-.8 1.301-1.2 2-.4.7-.7 1.3-1.1 2-.4-1.101-.899-2.2-1.2-3.3 1.2-.4 2.3-.6 3.5-.7zm-7.5 10.4c-3-1.2-4.9-2.9-4.9-4.2 0-1.3 1.9-3 4.9-4.2.701-.3 1.502-.601 2.3-.9.4 1.701 1.102 3.3 1.9 5.1a46.22 46.22 0 0 0-1.9 5c-.798-.199-1.599-.4-2.3-.8zm4.4 11.899c-1.1-.7-1.7-3.098-1.198-6.299.099-.8.2-1.6.4-2.4 1.7.401 3.4.7 5.3.9 1.1 1.6 2.3 2.9 3.4 4.2-2.8 2.5-5.3 3.9-7 3.9-.2-.1-.6-.1-.902-.301zM46.5 75.4c.4 3.1-.099 5.7-1.198 6.299-.2.1-.6.2-.902.2-1.799 0-4.2-1.299-7-3.9 1.201-1.199 2.3-2.599 3.4-4.2 1.9-.2 3.701-.398 5.3-.9.2.8.301 1.701.4 2.501zm3.2-5.6c-.7.3-1.5.601-2.298.9-.4-1.701-1.102-3.3-1.9-5.1a46.22 46.22 0 0 0 1.9-5c.798.199 1.599.6 2.298.899C52.7 62.7 54.6 64.4 54.6 65.7c-.102 1.2-2.002 2.9-4.902 4.1zm0 0" transform="matrix(1.69822 0 0 1.71352 -24.966 -80.407)"/><path d="M39.801 65.6a3.8 3.8 0 1 1-7.601 0 3.8 3.8 0 0 1 7.601 0zm0 0" transform="matrix(1.69822 0 0 1.71352 -24.966 -80.407)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/key.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M31.816 6.078c5.094 5.094 6.547 12.184 4.73 18.547l26.907 26.91.547 12-15.09-1.273v-7.637h-7.637v-7.637h-7.457L24 37.172c-6.363 1.816-13.637.363-18.547-4.73-7.27-7.27-7.27-19.27 0-26.544a18.494 18.494 0 0 1 26.363.18zM18 11.172c-2.184-2.184-5.453-2.184-7.637 0-2.18 2.18-2.18 5.453 0 7.637 2.184 2.18 5.453 2.18 7.637 0 2.184-2.184 2.184-5.637 0-7.637zm0 0" fill="#696"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/kf8.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><path d="M69.723 24.898c-.336-.851-1.012-1.535-1.688-2.222-.168.687-.336 1.37-.844 2.054L46.098 57.723c-.844 1.199-2.532 1.539-3.88 1.199l-33.75-9.574c-2.023-.512-4.386-1.711-4.554-4.106 0-.851 0-1.195.504-1.535.508-.344 1.016-.344 1.52-.172l31.726 8.89c4.555 1.368 5.902.34 9.277-4.788l19.239-30.09a5.83 5.83 0 0 0 .675-4.957c-.507-1.54-1.855-2.735-3.543-3.246L35.47 1.48c-.676-.171-1.352-.171-2.024-.171v-.172c-4.218-2.563-5.906 2.222-8.101 4.101-.844.684-1.856 1.2-2.196 1.883-.336.684-.168 1.367-.336 1.879-.843 1.883-3.207 4.957-4.386 5.813-.676.515-1.688.683-2.196 1.539-.335.511-.335 1.539-.503 2.222-.676 1.711-2.872 4.617-4.387 5.985-.508.511-1.352.855-1.688 1.539-.34.511-.172 1.539-.675 2.05-1.012 1.711-3.04 4.446-4.559 5.985-.844.855-1.856 1.195-2.191 2.05-.168.34 0 1.028-.168 1.54-.34.855-.676 1.539-.844 2.222C.37 41.141-.137 42.852.03 44.56c.34 4.105 3.375 8.207 7.09 9.234l33.746 9.574c3.207.852 7.09-.683 8.778-3.422l19.402-30.258c1.016-1.367 1.183-3.25.676-4.789zm-38.98-10.941 1.35-2.05c.337-.512 1.18-.856 1.856-.684l22.274 6.324c.675.172.843.855.507 1.371l-1.351 2.05c-.336.512-1.18.856-1.856.684L31.25 15.328c-.676-.172-.844-.687-.508-1.371zm-5.567 8.55 1.347-2.054c.34-.512 1.184-.851 1.86-.683l22.273 6.328c.676.172.844.855.504 1.367l-1.347 2.05c-.34.512-1.184.856-1.856.684L25.68 23.875c-.672-.172-1.012-.855-.504-1.367zm0 0" fill="#963"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/kmk.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><g fill="#1f385e"><path d="M73.734 51.555c0-2.844-2.289-5.157-5.109-5.157H5.375c-2.82 0-5.11 2.313-5.11 5.157v7.289c0 2.843 2.29 5.156 5.11 5.156h63.25c2.82 0 5.11-2.313 5.11-5.156zm-27.308 6.757a2.985 2.985 0 0 1-2.996-3.023 2.985 2.985 0 0 1 2.996-3.023 2.985 2.985 0 0 1 2.996 3.023c0 1.777-1.234 3.023-2.996 3.023zm8.984 0a2.984 2.984 0 0 1-2.992-3.023c0-1.777 1.23-3.023 2.992-3.023a2.985 2.985 0 0 1 2.996 3.023 2.985 2.985 0 0 1-2.996 3.023zm8.813 0a2.985 2.985 0 0 1-2.996-3.023c0-1.777 1.234-3.023 2.996-3.023a2.981 2.981 0 0 1 2.992 3.023 2.981 2.981 0 0 1-2.992 3.023zM5.375 43.38h63.25c1.41 0 2.82.355 3.879 1.066l-6.168-12.98c-1.762-3.73-4.582-5.153-7.398-5.153h-6.876L42.2 36.623c-.707.71-1.586 1.245-2.469 1.6-.878.356-1.937.532-2.82.532-1.055 0-1.937-.176-2.816-.531h-.352c-.707-.356-1.41-.891-2.117-1.422l-9.867-10.668h-6.871c-2.817 0-5.461 1.601-7.399 5.156L1.32 44.266c1.235-.532 2.47-.887 4.055-.887zm0 0"/><path d="M51.71 21.332c.352-.355.532-.71.884-1.242.176-.535.351-.89.351-1.602 0-.531-.175-1.066-.351-1.422-.176-.53-.532-.886-.883-1.246a5.273 5.273 0 0 0-1.23-.886c-.356-.18-.883-.356-1.41-.356-.532 0-1.06.176-1.41.356-.528.175-.884.53-1.235.886l-5.637 5.692V3.734c0-.535-.176-1.066-.352-1.421-.18-.536-.53-.891-.882-1.247-.352-.355-.703-.71-1.235-.886C37.97 0 37.441 0 36.91 0c-.527 0-1.055 0-1.406.18-.531.175-.883.53-1.234.886-.352.356-.708.711-.883 1.246-.176.532-.352.887-.352 1.422v17.953L27.398 16c-.351-.355-.707-.71-1.234-.89-.352-.176-.879-.356-1.41-.356-.527 0-1.055.18-1.41.355-.352.18-.88.536-1.23.891-.356.355-.708.71-.884 1.246-.175.531-.351.887-.351 1.422 0 .531.176 1.066.351 1.598.176.535.528.89.883 1.246L34.27 33.957c.351.355.703.711 1.234.887.351.18.879.355 1.406.355.531 0 1.059-.176 1.41-.355.532-.176.883-.532 1.235-.887zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ksh.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M13.875 13.874h10.9v2.701h-10.9zm0 5.4h10.9v2.701h-10.9zm0 5.5h10.9v2.702h-10.9zm19-24.399H11.177c-3 0-5.402 2.4-5.402 5.4v24.4h-5.4c0 3 2.402 5.4 5.4 5.4h21.7c3 0 5.402-2.4 5.402-5.4v-21.7h5.4v-2.7c0-3-2.402-5.4-5.4-5.4zm-2.7 29.3c0 1.801-1.4 3.2-3.2 3.2h-19.9c1.3-.9 1.3-2.7 1.3-2.7v-24.4c0-1.5 1.2-2.7 2.7-2.7 1.501 0 2.7 1.2 2.7 2.7v2.7h16.3zm-13.6-23.9v-2.7h16.3c2.501 0 2.7 1.6 2.7 2.7zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".75" transform="matrix(1.7717 0 0 1.78025 .262 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/kt.svg
1
1
<svg height="63" width="60" xmlns="http://www.w3.org/2000/svg"><path d="M.125 47.379V28.586c0-1.125.379-1.879 1.523-2.441 14.29-7.707 28.579-15.598 42.868-23.493 2.097-.937 4.004-2.254 6.48-2.254 4 .188 7.43 2.442 8.57 6.204 1.145 3.757 0 8.078-3.238 10.144-3.617 2.258-7.621 4.324-11.43 6.578C30.61 31.031 16.32 38.922 1.84 46.816c-.574.188-.953.375-1.715.563zm0 0" fill="#e88e3d"/><path d="M22.797 40.426c.57-.375.953-.563 1.144-.938 4.762-2.633 9.524-5.074 13.907-7.52.953-.562 1.715-.562 2.668.188 5.336 4.887 10.859 9.399 16.004 14.285 3.046 2.63 3.812 6.012 2.667 9.582-.953 3.57-3.62 5.641-7.43 6.204-2.476.375-4.952-.375-6.859-1.88-7.242-6.577-14.48-13.156-22.101-19.921.191.375.191.187 0 0zM.125 22.008c0-4.695-.383-9.207.191-13.528C.886 3.406 5.84.398 11.367.96c4.57.567 8.383 5.263 8 9.774 0 .563-.379 1.13-.953 1.317-5.906 3.195-11.812 6.578-17.91 9.77.191.187 0 0-.379.187zm19.242 20.297c0 4.324.192 8.082 0 12.027-.379 4.7-4.762 8.27-9.336 8.27-4.57 0-8.953-3.383-9.715-7.891-.191-1.316 0-2.258 1.332-3.008 5.336-3.008 10.86-5.828 16.196-8.832.379 0 .761-.187 1.523-.566zm0 0" fill="#5171a5"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/kts.svg
1
1
<svg height="63" width="60" xmlns="http://www.w3.org/2000/svg"><path d="M.125 47.379V28.586c0-1.125.379-1.879 1.523-2.441 14.29-7.707 28.579-15.598 42.868-23.493 2.097-.937 4.004-2.254 6.48-2.254 4 .188 7.43 2.442 8.57 6.204 1.145 3.757 0 8.078-3.238 10.144-3.617 2.258-7.621 4.324-11.43 6.578C30.61 31.031 16.32 38.922 1.84 46.816c-.574.188-.953.375-1.715.563zm0 0" fill="#e88e3d"/><path d="M22.797 40.426c.57-.375.953-.563 1.144-.938 4.762-2.633 9.524-5.074 13.907-7.52.953-.562 1.715-.562 2.668.188 5.336 4.887 10.859 9.399 16.004 14.285 3.046 2.63 3.812 6.012 2.667 9.582-.953 3.57-3.62 5.641-7.43 6.204-2.476.375-4.952-.375-6.859-1.88-7.242-6.577-14.48-13.156-22.101-19.921.191.375.191.187 0 0zM.125 22.008c0-4.695-.383-9.207.191-13.528C.886 3.406 5.84.398 11.367.96c4.57.567 8.383 5.263 8 9.774 0 .563-.379 1.13-.953 1.317-5.906 3.195-11.812 6.578-17.91 9.77.191.187 0 0-.379.187zm19.242 20.297c0 4.324.192 8.082 0 12.027-.379 4.7-4.762 8.27-9.336 8.27-4.57 0-8.953-3.383-9.715-7.891-.191-1.316 0-2.258 1.332-3.008 5.336-3.008 10.86-5.828 16.196-8.832.379 0 .761-.187 1.523-.566zm0 0" fill="#5171a5"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/kup.svg
1
1
<svg height="64" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M32.707 50.086c-4.18-4.148-9.402.348-13.93 4.324-5.742 5.184-9.746 8.125-13.402 4.668-2.613-2.594-2.438-5.707 0-8.992l2.438 2.25a1.847 1.847 0 0 0 2.261 0L37.406 27.27 26.09 16.03.848 43.344c-.524.691-.524 1.558 0 2.25l2.437 2.418c-4.527 5.36-2.96 10.547 0 13.484 5.918 5.879 12.535.172 17.758-4.496 4.7-4.148 7.66-6.395 9.574-4.492.695.52 1.738.52 2.262-.172.348-.691.348-1.559-.172-2.25zm-8.008-19.188c-.699.692-1.57.692-2.265 0-.696-.691-.696-1.554 0-2.246l2.265-2.246c.696-.691 1.567-.691 2.262 0 .695.692.695 1.555 0 2.246zm14.797-5.875c.348.344.695.516 1.043.516 2.262 0 4.7-.516 6.617-1.727L29.57 6.352c-1.218 2.074-1.738 4.324-1.738 6.57 0 .344.172.863.52 1.035zm10.27-21.261c-5.047-5.016-13.23-5.016-18.278 0L49.766 21.91a12.77 12.77 0 0 0 0-18.148zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/less.svg
1
1
<svg height="64" width="75" xmlns="http://www.w3.org/2000/svg"><path d="M.5 19v-4.1c.9-.1 1.6-.2 2-.4.4-.2.8-.6 1.2-1.001.4-.5.5-1.1.7-1.9.1-.6.2-1.499.2-2.799 0-2.201.1-3.7.4-4.6.2-.8.6-1.6 1.2-2 .5-.5 1.4-.9 2.5-1.2.7-.2 1.9-.4 3.5-.4h.9v3.9c-1.3 0-2.2.1-2.6.3-.4.2-.6.4-.9.6-.2.3-.3.7-.3 1.501 0 .8-.1 2-.2 4.099-.101 1.2-.2 2-.4 2.801-.301.6-.6 1.2-1 1.8-.4.4-1 .9-1.8 1.399.7.4 1.3.8 1.8 1.3s.8 1.2 1.1 1.899c.3.702.4 1.802.4 3.001.1 1.9.1 3.1.1 3.599 0 .702.1 1.202.3 1.602.2.4.5.5.9.6.4.2 1.2.3 2.6.3v4.098h-1c-1.6 0-2.9-.1-3.701-.4-.9-.3-1.6-.6-2.2-1.2-.6-.6-.999-1.2-1.2-1.999-.198-.8-.299-2.1-.299-4 0-2-.1-3.5-.3-4.1-.3-.9-.7-1.601-1.201-2-.698-.5-1.5-.7-2.7-.7zm39.1 0c-.9.1-1.6.2-2 .4s-.8.6-1.2 1.001c-.4.5-.5 1.1-.7 1.9-.099.6-.2 1.499-.2 2.799 0 2.201-.1 3.7-.4 4.6-.2.9-.6 1.6-1.2 2-.5.5-1.4.9-2.5 1.2-.7.2-1.9.4-3.5.4h-.999v-4.1c1.298 0 2.1-.1 2.599-.3s.7-.4.899-.6c.2-.3.301-.7.301-1.501 0-.6.1-2 .2-3.999.099-1.2.3-2.1.5-2.8.3-.7.6-1.3 1.1-1.9.4-.5 1-.9 1.7-1.3-.901-.6-1.6-1.1-2-1.6-.5-.7-1-1.801-1.201-2.8-.199-.8-.299-2.6-.299-5.2 0-.8-.1-1.4-.301-1.8-.199-.3-.4-.5-.799-.6-.2-.3-1-.3-2.5-.3v-4h.999c1.602 0 2.9.1 3.7.4.902.3 1.6.6 2.2 1.2.6.6 1.002 1.2 1.2 2 .201.8.402 2.1.402 4 0 2 .098 3.4.299 4.1.299.9.7 1.601 1.2 1.9.5.4 1.401.6 2.5.6.1.1 0 4.3 0 4.3zm0 0" fill="#7058c6" stroke="#7058c6" stroke-miterlimit="10" transform="matrix(1.86825 0 0 1.87558 0 .209)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/lex.svg
1
1
<svg height="64" width="80" xmlns="http://www.w3.org/2000/svg"><path d="M38.027 37.414c-5.011-4.812-9.425-9.223-12.03-19.25H43.64v-7.219H26.195V1.121h-7.617v10.024H.93v7.421h18.047s-.2 1.403-.399 2.606C15.968 30.996 13.164 37.215.93 43.23l2.61 7.418c11.429-6.015 17.444-13.835 20.05-22.257 2.605 6.418 6.816 11.629 11.629 16.441zM61.29 13.352H51.262L33.617 62.879h7.617l5.016-14.836H66.3l5.013 14.836h7.62zm-12.434 27.27 7.622-19.65 7.617 19.852zm0 0" fill="#c93" stroke="#c93" stroke-miterlimit="10" stroke-width="1.5039150000000001"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/licx.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/lisp.svg
1
1
<svg height="63" width="45" xmlns="http://www.w3.org/2000/svg"><path d="M22.686 26.4h.8c0 2.401-.4 4.2-1.2 5.3-.801 1.1-1.8 1.7-3 1.7-1.001 0-1.9-.4-2.8-1.1-.9-.699-1.701-2.7-2.4-5.9l-2-8.9-6.902 15.599h-4.4l9.901-21.2c-.5-2.698-1.2-4.799-1.899-6.098-.701-1.3-1.7-2.002-2.7-2.002-.902 0-1.601.301-2.3 1-.6.7-1 1.701-1.1 3.1h-.8c0-2.299.5-4.1 1.4-5.4.899-1.3 1.898-2 3.2-2 .8 0 1.599.302 2.3 1.002.7.699 1.4 1.799 1.9 3.499.599 1.7 1.4 5.1 2.6 10.3l1.599 7.3c.701 3 1.4 5 2.1 6.1.7 1 1.6 1.5 2.6 1.5 1.9-.1 2.901-1.3 3.101-3.8zm0 0" fill="#066" stroke="#066" stroke-miterlimit="10" transform="matrix(1.87615 0 0 1.85407 0 .073)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/lit.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><path d="M69.723 24.898c-.336-.851-1.012-1.535-1.688-2.222-.168.687-.336 1.37-.844 2.054L46.098 57.723c-.844 1.199-2.532 1.539-3.88 1.199l-33.75-9.574c-2.023-.512-4.386-1.711-4.554-4.106 0-.851 0-1.195.504-1.535.508-.344 1.016-.344 1.52-.172l31.726 8.89c4.555 1.368 5.902.34 9.277-4.788l19.239-30.09a5.83 5.83 0 0 0 .675-4.957c-.507-1.54-1.855-2.735-3.543-3.246L35.47 1.48c-.676-.171-1.352-.171-2.024-.171v-.172c-4.218-2.563-5.906 2.222-8.101 4.101-.844.684-1.856 1.2-2.196 1.883-.336.684-.168 1.367-.336 1.879-.843 1.883-3.207 4.957-4.386 5.813-.676.515-1.688.683-2.196 1.539-.335.511-.335 1.539-.503 2.222-.676 1.711-2.872 4.617-4.387 5.985-.508.511-1.352.855-1.688 1.539-.34.511-.172 1.539-.675 2.05-1.012 1.711-3.04 4.446-4.559 5.985-.844.855-1.856 1.195-2.191 2.05-.168.34 0 1.028-.168 1.54-.34.855-.676 1.539-.844 2.222C.37 41.141-.137 42.852.03 44.56c.34 4.105 3.375 8.207 7.09 9.234l33.746 9.574c3.207.852 7.09-.683 8.778-3.422l19.402-30.258c1.016-1.367 1.183-3.25.676-4.789zm-38.98-10.941 1.35-2.05c.337-.512 1.18-.856 1.856-.684l22.274 6.324c.675.172.843.855.507 1.371l-1.351 2.05c-.336.512-1.18.856-1.856.684L31.25 15.328c-.676-.172-.844-.687-.508-1.371zm-5.567 8.55 1.347-2.054c.34-.512 1.184-.851 1.86-.683l22.273 6.328c.676.172.844.855.504 1.367l-1.347 2.05c-.34.512-1.184.856-1.856.684L25.68 23.875c-.672-.172-1.012-.855-.504-1.367zm0 0" fill="#963"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/lnk.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#bababa"><path d="M34.84 4.973 23.66 16.156a19.478 19.478 0 0 1 10.266 0c.55.184.914.364 1.281.364l5.684-5.68c3.3-3.3 8.8-3.3 12.097 0 3.301 3.297 3.301 8.797 0 12.098L41.074 34.852c-1.101 1.101-2.383 1.836-3.851 2.199-2.746.734-6.047 0-8.246-2.2-1.47-1.464-2.383-3.48-2.383-5.316-.735.367-1.285.735-1.836 1.102l-5.317 5.316c.735 1.832 2.02 3.484 3.485 4.95 2.199 2.199 4.765 3.667 7.699 4.398 4.398 1.101 9.164.55 13.016-1.832 1.28-.735 2.382-1.649 3.3-2.567L59.04 28.805c6.598-6.602 6.598-17.418 0-24.016a17.443 17.443 0 0 0-24.2.184zm0 0"/><path d="M40.156 47.867c-3.847 1.102-7.883.918-11.73-.367l-5.5 5.5c-3.301 3.3-8.801 3.3-12.098 0-3.3-3.297-3.3-8.797 0-12.098l12.098-12.097c1.101-1.102 2.383-1.836 3.851-2.2 2.746-.734 6.047 0 8.246 2.2 1.47 1.465 2.383 3.48 2.383 5.5.551-.368 1.285-.735 1.836-1.102l5.317-5.316c-.735-1.832-2.02-3.485-3.485-4.95-2.199-2.199-4.765-3.667-7.699-4.398-4.398-1.102-9.164-.55-13.016 1.832-1.28.734-2.382 1.649-3.3 2.567L4.96 35.035c-6.598 6.602-6.598 17.414 0 24.016 6.598 6.597 17.414 6.597 24.016 0zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/lock.svg
1
1
<svg height="64" width="52" xmlns="http://www.w3.org/2000/svg"><path d="M48.793 26.879h-4.629V18.05C44.164 7.988 35.973.043 26 .043S7.836 8.164 7.836 18.051v8.828H3.207A3.181 3.181 0 0 0 0 30.059V60.78c0 1.762 1.426 3.176 3.207 3.176h45.586c1.781 0 3.207-1.414 3.207-3.176V29.883c0-1.59-1.426-3.004-3.207-3.004zM29.918 52.305c.355 1.058-.535 1.941-1.602 1.941h-4.808c-1.07 0-1.781-1.059-1.606-1.941l1.426-5.649c-1.781-.883-3.027-2.648-3.027-4.945 0-3 2.492-5.473 5.52-5.473 3.027 0 5.523 2.473 5.523 5.473 0 2.117-1.246 4.062-3.028 4.945zm5.164-25.426H16.918V18.05c0-4.942 4.098-9.004 9.082-9.004s9.082 4.062 9.082 9.004zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/log.svg
1
1
<svg height="64" width="76" xmlns="http://www.w3.org/2000/svg"><path d="M.176 52.977h75.648V64H.176zm0-26.309h75.648v11.02H.176zM.176 0h75.648v11.023H.176zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/lua.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M27.91 64A27.846 27.846 0 0 1 0 36.09C0 20.62 12.445 8 28.445 8.18c15.11.355 27.2 12.441 27.2 27.91C55.645 51.555 43.199 64 27.91 64zm11.38-47.645c-4.446 0-8.356 3.91-8.356 8.356 0 4.445 3.554 8.355 8.355 8.355 4.445 0 8.356-3.554 8.356-8.355 0-4.621-3.555-8.356-8.356-8.356zm16.355 0c-4.446 0-8.356-3.554-8-8.355 0-4.445 3.554-8 8.355-8 4.445 0 8 3.91 8 8.355 0 4.446-3.91 8-8.355 8zm0 0" fill="navy"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/m.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="stroke:none;fill-rule:nonzero;fill:#999;fill-opacity:1" d="M.125 0h69.586v8.184H.125zm13.164 18.273h69.586v8.18H13.289zM.125 36.543h69.586v8.184H.125zm13.164 18.273h69.586V63H13.289zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/m2v.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/m3u.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="stroke:none;fill-rule:nonzero;fill:#039;fill-opacity:1" d="M4.059 10.39h40.254c2.109 0 3.69-1.613 3.69-3.761 0-2.149-1.581-3.758-3.69-3.758H4.059c-2.11 0-3.692 1.61-3.692 3.758 0 2.152 1.582 3.762 3.692 3.762zm0 19.891h40.254c2.109 0 3.69-1.613 3.69-3.765 0-2.149-1.581-3.762-3.69-3.762H4.059c-2.11 0-3.692 1.613-3.692 3.762 0 2.148 1.582 3.765 3.692 3.765zm19.336 10.57H4.059c-2.11 0-3.692 1.614-3.692 3.762 0 2.149 1.582 3.766 3.692 3.766h19.336c2.109 0 3.69-1.617 3.69-3.766 0-2.148-1.581-3.761-3.69-3.761zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#039;fill-opacity:1" d="M70.68 9.496c-2.813-1.434-6.504-3.582-7.91-6.629C62.77 1.254 61.54 0 59.957 0c-1.582 0-2.812 1.254-2.812 2.867v38.52c-2.989-1.614-8.614-1.075-12.833 1.433-6.68 3.766-9.492 10.93-6.68 15.766 2.813 4.84 10.723 5.914 17.4 2.152 4.573-2.687 7.738-6.988 7.913-11.289V16.305c9.492 0 15.29 3.941 13.18 13.437-.352 1.793-1.05 3.403-1.754 5.195-.355.54-.355 1.254.176 1.793.527.536 1.402.356 2.11-.359 3.515-3.582 5.796-8.242 5.976-13.437-.18-6.805-6.508-10.75-11.953-13.438zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/m3u8.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="stroke:none;fill-rule:nonzero;fill:#039;fill-opacity:1" d="M4.059 10.39h40.254c2.109 0 3.69-1.613 3.69-3.761 0-2.149-1.581-3.758-3.69-3.758H4.059c-2.11 0-3.692 1.61-3.692 3.758 0 2.152 1.582 3.762 3.692 3.762zm0 19.891h40.254c2.109 0 3.69-1.613 3.69-3.765 0-2.149-1.581-3.762-3.69-3.762H4.059c-2.11 0-3.692 1.613-3.692 3.762 0 2.148 1.582 3.765 3.692 3.765zm19.336 10.57H4.059c-2.11 0-3.692 1.614-3.692 3.762 0 2.149 1.582 3.766 3.692 3.766h19.336c2.109 0 3.69-1.617 3.69-3.766 0-2.148-1.581-3.761-3.69-3.761zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#039;fill-opacity:1" d="M70.68 9.496c-2.813-1.434-6.504-3.582-7.91-6.629C62.77 1.254 61.54 0 59.957 0c-1.582 0-2.812 1.254-2.812 2.867v38.52c-2.989-1.614-8.614-1.075-12.833 1.433-6.68 3.766-9.492 10.93-6.68 15.766 2.813 4.84 10.723 5.914 17.4 2.152 4.573-2.687 7.738-6.988 7.913-11.289V16.305c9.492 0 15.29 3.941 13.18 13.437-.352 1.793-1.05 3.403-1.754 5.195-.355.54-.355 1.254.176 1.793.527.536 1.402.356 2.11-.359 3.515-3.582 5.796-8.242 5.976-13.437-.18-6.805-6.508-10.75-11.953-13.438zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/m4.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="m60.137 40.719-.184-.36-.36-.359c0-5.395-3.234-10.066-7.73-12.406-.539-1.977-.18-2.696-.18-2.696.18-.18.18-.539.364-.718h4.672c1.078 0 2.156-.36 2.875-1.258 2.52-2.516 3.777-5.754 3.777-9.348 0-6.652-4.676-12.047-11.144-12.945-.364 0-.723-.18-1.082-.18h-37.57C6.382.45.448 6.383.448 13.574c0 .54 0 1.078.18 1.797.36 12.223 9.527 22.113 14.383 26.606H5.664c-1.437 0-2.879.718-3.418 2.16C1.168 46.113.45 48.27.45 50.426c0 7.191 5.934 13.125 13.125 13.125h37.93c6.832-.719 12.047-6.473 12.047-13.125-.18-3.414-1.438-7.192-3.414-9.707M51.145 4.586c4.675.539 8.449 4.312 8.449 9.348 0 2.695-1.078 4.851-2.875 6.652H22.563c1.437-1.98 2.335-4.137 2.335-6.652 0-3.778-1.976-7.192-4.671-9.348zM4.227 50.426c0-1.617.539-3.235 1.257-4.313h15.82c.72 1.438 1.259 2.875 1.259 4.313 0 5.035-4.137 9.168-9.348 9.168-5.215 0-8.988-4.313-8.988-9.168m46.918 9.168H19.863c3.059-2.156 4.856-5.39 4.856-9.348 0-3.773-1.977-7.191-4.672-9.348h.18S4.766 29.395 4.586 14.832c0-.539-.18-.898-.18-1.437 0-5.036 4.133-9.348 9.348-9.348 5.21 0 9.348 4.133 9.348 9.348v.539c0 .898-.18 1.796-.54 2.515-.359 1.078-.898 1.977-1.617 2.875l-2.34 3.239H48.63c0 .18-.18.359-.36.539-.539 1.078-.718 2.156-.718 3.234-.54 0-1.258-.18-1.977-.18-7.55 0-13.844 6.114-13.844 13.844s6.114 13.844 13.844 13.844c5.57 0 10.426-3.239 12.582-8.27.719 1.617 1.258 3.235 1.258 4.852.18 4.676-3.594 8.808-8.27 9.168M51.685 40l-9.168 6.832V32.988zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".898875"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/m4a.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/m4r.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#039"><path d="M.324 12.07C-.777 14.828 1.43 23.102 1.43 23.102c4.226 11.77 14.156 21.882 14.156 21.882 9.746 10.114 19.492 16.364 28.133 18.387 8.64 2.02 10.297-1.473 10.297-1.473s7.168-6.988 9.191-9.375c2.023-2.574-.55-4.046-.55-4.046s-12.505-7.54-14.524-8.274c-2.024-.918-3.13.55-4.414 1.656-1.29 1.102-3.864 3.493-3.864 3.493-1.468.183-4.226-.918-8.64-4.414C26.8 37.444 21.469 30.823 20 28.434c-1.473-2.204-1.473-4.594-1.473-4.594s1.84-1.473 3.68-3.496c1.836-2.02 1.285-3.86 1.285-3.86l-5.699-10.48C14.301-1.352 13.379.12 13.379.12c-2.39.918-4.41 2.758-5.7 4.043-.917.922-5.882 5.149-7.355 7.906zM49.97 27.7c1.472 0 2.758-1.102 2.758-2.759 0-8.09-6.618-15.078-15.075-15.078-1.472 0-2.761 1.106-2.761 2.758 0 1.473 1.105 2.762 2.761 2.762 5.145 0 9.375 4.226 9.375 9.375 0 1.656 1.473 2.941 2.942 2.941zm0 0"/><path d="M38.938 1.223c-1.473 0-2.758 1.105-2.758 2.757 0 1.473 1.101 2.758 2.757 2.758a16.87 16.87 0 0 1 16.915 16.918c0 1.469 1.105 2.758 2.757 2.758 1.657 0 2.762-1.105 2.762-2.758 0-12.32-10.113-22.433-22.434-22.433zm-3.676 16.363c-1.473 0-2.758 1.105-2.758 2.758 0 1.656 1.101 2.758 2.758 2.758 2.39 0 4.41 2.023 4.41 4.414 0 1.472 1.105 2.757 2.758 2.757 1.472 0 2.757-1.101 2.757-2.757-.183-5.516-4.593-9.93-9.925-9.93zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/m4v.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/map.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><g fill="#999"><path d="M56.797 45.254c-.356-.531-.711-.707-1.242-.707H38.91c-.531 1.238-1.238 2.297-1.77 3.535-1.417 2.828-3.011 5.832-4.425 8.305v.18c-.887 1.413-2.305 2.472-4.074 2.472s-3.188-.883-4.07-2.473c-.532-.886-2.305-4.242-4.43-8.484-.707-1.238-1.239-2.473-1.77-3.71H9.34c-.531 0-1.063.35-1.414.882L.133 61.69c-.176.528-.176 1.059 0 1.414.355.528.707.708 1.238.708h46.215c.531 0 1.062-.356 1.418-.887l7.793-16.258c.351-.352.176-1.059 0-1.414zm0 0"/><path d="M28.465.188c-9.387 0-17.176 7.601-17.176 17.144 0 5.656 6.195 19.086 11.332 29.512 2.48 4.773 4.426 8.308 4.426 8.484.355.531.71.883 1.418.883.707 0 1.062-.352 1.414-.883 0 0 1.95-3.535 4.43-8.484 5.132-10.25 11.332-23.68 11.332-29.512C45.64 7.789 37.848.187 28.465.187zm0 27.57c-4.25 0-7.969-3.356-8.324-7.598v-.883c0-4.597 3.718-8.308 8.324-8.308 4.25 0 7.969 3.36 8.32 7.422v.886c0 4.594-3.719 8.48-8.32 8.48zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mc.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M64 32c0 17.645-14.355 32-32 32S0 49.645 0 32 14.355 0 32 0s32 14.355 32 32" fill="#999"/><g fill="#fff"><path d="M54.313 34.422c-1.036-6.746-8.13-11.242-13.493-14.531-2.246-1.383-6.054-3.286-6.57-6.399-.176-1.21-.176-2.594-.176-3.805V8.13c0-.692-.691.172-1.035-.344-.867-1.387-.52.344-.52 1.211.172 1.727.52 3.457.52 5.188 0 3.285-.52 6.574-1.387 9.687-1.902 7.438-3.457 15.223-1.554 22.832a24.518 24.518 0 0 0 1.554 4.668c.176.52.52 1.73 1.211 1.906 2.078.516 3.633.692 5.192 2.246 1.039.868 1.73.348 2.941 0 3.633-1.382 6.746-3.285 9.34-6.226 3.285-4.496 4.844-9.512 3.977-14.875m-3.805 6.746c-.344 2.766-2.074 5.363-3.805 7.437-1.383 1.56-3.113 3.461-5.016 4.153-.69.172.172-1.211.172-1.211.52-.867 1.383-1.73 2.075-2.594 1.039-1.21 1.902-2.598 2.421-3.98 1.903-5.016 1.56-10.899-1.382-15.395-1.555-2.422-3.805-4.496-5.88-6.398-1.038-.868-2.077-1.73-2.94-2.77-.176-.172-2.079-2.594-1.387-2.941.175-.172 4.152 3.98 4.5 4.324 1.554 1.21 3.285 2.422 4.843 3.809 2.075 1.902 4.149 3.976 5.36 6.57 1.21 2.77 1.386 6.055 1.039 8.996"/><path d="M30.79.863c.519.348.69 2.77.69 4.844 0 2.078.172 11.246-.52 13.664-.69 2.422-2.245 5.192-3.804 7.613-1.73 2.422-3.633 7.438-3.457 10.551 0 3.113 1.903 8.13 3.285 10.38 1.383 2.073 3.805 5.015 3.286 5.706-.864 1.211-4.668-2.941-6.747-5.363-1.902-2.422-3.976-7.262-3.976-11.07 0-3.805 2.074-7.262 3.633-9.34 1.554-2.075 4.496-5.707 5.36-7.438.866-1.73 1.73-3.457 1.901-5.707.348-2.25 0-10.55 0-10.55S30.27.52 30.79.862"/><path d="M29.234 4.844c.516.343.692 1.039.692 1.73 0 .692-.176 3.633-.348 6.57-.172 2.942-2.594 5.364-4.152 7.094-1.727 1.73-6.746 7.09-8.473 9.688-1.906 2.594-2.77 6.05-2.598 8.992.176 2.941.868 5.883 3.633 8.996 2.77 3.113 4.672 4.496 6.227 5.363 1.387.692 2.941 1.211 2.597 1.903-.347.691-1.73.172-3.289-.348-1.554-.52-6.746-2.594-9.687-6.055-2.938-3.457-4.496-7.957-4.324-12.105.175-4.324 1.386-6.055 3.289-8.824 1.902-2.766 7.437-6.918 9.168-7.957 1.73-1.036 3.976-2.766 5.187-4.325 1.211-1.382 1.73-2.593 1.73-4.668 0-1.902.173-3.804 0-4.5-.171-.515-.171-1.902.348-1.554m.172 51.89c.344 0 .172 1.211-.347 1.73-.52.52-1.211.864-1.383.692s.52-.343 1.039-.863c.52-.691.344-1.559.691-1.559m5.36-.172c-.344 0-.172 1.211.347 1.731s1.211.863 1.383.691c.176-.172-.52-.347-1.035-.867-.52-.515-.348-1.554-.695-1.554m-2.418 1.382c0 1.04 0 1.903-.176 1.903-.344 0-.172-.864-.172-1.903 0-1.039-.172-1.902.172-1.902.348 0 .176.863.176 1.902"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/md.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="39"><path style="stroke:none;fill-rule:nonzero;fill:#999;fill-opacity:1" d="M0 38.824V.176h11.2l11.198 14.183L33.602.176H44.8v38.648H33.6v-22.16l-11.203 14.18-11.195-14.18v22.16zm67.2 0L50.397 20.031h11.204V.176h11.195V20.03H84zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mdb.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M66.824 40.465c-.176 1.59-2.097 2.824-5.945 3.707-3.852.879-8.75 1.41-14.52 1.41h-4.898v9.172c1.574.176 3.324.176 4.898.176 5.77 0 10.668-.528 14.52-1.586 3.848-1.059 5.945-2.293 6.121-3.707-.176-.352-.176-9.172-.176-9.172zm-20.64-6.7c-1.75 0-3.325 0-4.899-.18v9.352h4.899c5.773 0 10.671-.53 14.52-1.59s5.944-2.292 5.944-3.702v-8.997c-.171 1.586-2.097 2.825-6.12 3.704-3.673 1.058-8.571 1.59-14.344 1.414zm0-11.468c-1.75 0-3.325 0-4.899-.176v9.352c1.574.175 3.324.175 4.899.175 5.773 0 10.671-.53 14.695-1.59C64.727 29 66.824 27.767 67 26.356V17.36c-.176 1.59-2.098 2.825-6.121 3.704-4.024.707-8.922 1.234-14.695 1.234zm0-13.05c-1.75 0-3.325 0-4.899.175v10.406c1.574.176 3.324.176 4.899.176 5.773 0 10.671-.527 14.695-1.586 3.848-1.059 5.945-2.293 6.121-3.703-.176-1.59-2.098-2.824-6.121-3.883-4.024-1.055-8.922-1.41-14.695-1.586zM18.02 23.886c-.176.527-.528 2.293-1.227 5.293l-1.223 5.113h5.07l-1.222-5.113c-.7-3-1.227-4.766-1.227-5.293zM0 7.129v49.918l37.785 6.527V.426zm23.09 37.219-1.399-5.645-6.996-.176-1.398 5.29-4.375-.352 6.648-23.813 5.07-.351 7.348 25.222zm0 0" fill="#a03537"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mdf.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#d5006e"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/me.svg
1
1
<svg height="64" width="76" xmlns="http://www.w3.org/2000/svg"><path d="M.176 52.977h75.648V64H.176zm0-26.309h75.648v11.02H.176zM.176 0h75.648v11.023H.176zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mi.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M64 32c0 17.645-14.355 32-32 32S0 49.645 0 32 14.355 0 32 0s32 14.355 32 32" fill="#999"/><g fill="#fff"><path d="M54.313 34.422c-1.036-6.746-8.13-11.242-13.493-14.531-2.246-1.383-6.054-3.286-6.57-6.399-.176-1.21-.176-2.594-.176-3.805V8.13c0-.692-.691.172-1.035-.344-.867-1.387-.52.344-.52 1.211.172 1.727.52 3.457.52 5.188 0 3.285-.52 6.574-1.387 9.687-1.902 7.438-3.457 15.223-1.554 22.832a24.518 24.518 0 0 0 1.554 4.668c.176.52.52 1.73 1.211 1.906 2.078.516 3.633.692 5.192 2.246 1.039.868 1.73.348 2.941 0 3.633-1.382 6.746-3.285 9.34-6.226 3.285-4.496 4.844-9.512 3.977-14.875m-3.805 6.746c-.344 2.766-2.074 5.363-3.805 7.437-1.383 1.56-3.113 3.461-5.016 4.153-.69.172.172-1.211.172-1.211.52-.867 1.383-1.73 2.075-2.594 1.039-1.21 1.902-2.598 2.421-3.98 1.903-5.016 1.56-10.899-1.382-15.395-1.555-2.422-3.805-4.496-5.88-6.398-1.038-.868-2.077-1.73-2.94-2.77-.176-.172-2.079-2.594-1.387-2.941.175-.172 4.152 3.98 4.5 4.324 1.554 1.21 3.285 2.422 4.843 3.809 2.075 1.902 4.149 3.976 5.36 6.57 1.21 2.77 1.386 6.055 1.039 8.996"/><path d="M30.79.863c.519.348.69 2.77.69 4.844 0 2.078.172 11.246-.52 13.664-.69 2.422-2.245 5.192-3.804 7.613-1.73 2.422-3.633 7.438-3.457 10.551 0 3.113 1.903 8.13 3.285 10.38 1.383 2.073 3.805 5.015 3.286 5.706-.864 1.211-4.668-2.941-6.747-5.363-1.902-2.422-3.976-7.262-3.976-11.07 0-3.805 2.074-7.262 3.633-9.34 1.554-2.075 4.496-5.707 5.36-7.438.866-1.73 1.73-3.457 1.901-5.707.348-2.25 0-10.55 0-10.55S30.27.52 30.79.862"/><path d="M29.234 4.844c.516.343.692 1.039.692 1.73 0 .692-.176 3.633-.348 6.57-.172 2.942-2.594 5.364-4.152 7.094-1.727 1.73-6.746 7.09-8.473 9.688-1.906 2.594-2.77 6.05-2.598 8.992.176 2.941.868 5.883 3.633 8.996 2.77 3.113 4.672 4.496 6.227 5.363 1.387.692 2.941 1.211 2.597 1.903-.347.691-1.73.172-3.289-.348-1.554-.52-6.746-2.594-9.687-6.055-2.938-3.457-4.496-7.957-4.324-12.105.175-4.324 1.386-6.055 3.289-8.824 1.902-2.766 7.437-6.918 9.168-7.957 1.73-1.036 3.976-2.766 5.187-4.325 1.211-1.382 1.73-2.593 1.73-4.668 0-1.902.173-3.804 0-4.5-.171-.515-.171-1.902.348-1.554m.172 51.89c.344 0 .172 1.211-.347 1.73-.52.52-1.211.864-1.383.692s.52-.343 1.039-.863c.52-.691.344-1.559.691-1.559m5.36-.172c-.344 0-.172 1.211.347 1.731s1.211.863 1.383.691c.176-.172-.52-.347-1.035-.867-.52-.515-.348-1.554-.695-1.554m-2.418 1.382c0 1.04 0 1.903-.176 1.903-.344 0-.172-.864-.172-1.903 0-1.039-.172-1.902.172-1.902.348 0 .176.863.176 1.902"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mid.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/midi.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mk.svg
1
1
<svg height="64" width="79" xmlns="http://www.w3.org/2000/svg"><path d="m42.852 35.445 4.695 1.344 2.851-10.418-4.695-1.344c0-1.68-.168-3.359-.672-5.039l4.192-2.52-5.364-9.405-4.359 2.519a18.036 18.036 0 0 0-4.023-3.023l1.34-4.704L26.421 0l-1.34 4.703c-1.676 0-3.352.168-5.027.672l-2.516-4.2-9.387 5.376 2.512 4.199a18.053 18.053 0 0 0-3.016 4.031l-4.695-1.343L.105 23.852l4.692 1.343c0 1.68.168 3.36.672 5.04l-4.192 2.523 5.364 9.406 4.191-2.52a18.126 18.126 0 0 0 4.023 3.024l-1.34 4.703 10.395 2.856 1.34-4.704c1.676 0 3.352-.168 5.031-.671l2.512 4.199 9.39-5.375-2.515-4.2c1.172-1.175 2.348-2.519 3.184-4.03zm-25.985-5.547c-2.68-4.535-1.004-10.414 3.52-13.101 4.527-2.688 10.394-1.008 13.078 3.527 2.683 4.535 1.004 10.414-3.52 13.106-4.527 2.687-10.394 1.175-13.078-3.532zm50.63 33.262 6.034-3.527-1.676-2.856c.84-.84 1.508-1.68 2.012-2.687l3.184.84 1.844-6.887-3.184-.84c0-1.176-.168-2.183-.504-3.36l2.852-1.679-3.52-6.047-2.852 1.68c-.84-.84-1.675-1.512-2.683-2.016l.84-3.191-6.875-1.852-.836 3.196c-1.176 0-2.18.168-3.356.504l-1.675-2.86-5.7 3.7 1.676 2.855c-.836.84-1.508 1.68-2.012 2.687l-3.183-1.007-1.844 6.886 3.184.84c0 1.176.168 2.184.504 3.36l-2.852 1.68 3.523 6.046 2.848-1.68c.84.84 1.676 1.512 2.684 2.016l-.84 3.191L61.965 64l.836-3.191c1.176 0 2.18-.168 3.355-.504-.336 0 1.34 2.855 1.34 2.855zM57.101 50.563c-1.676-3.024-.668-6.887 2.347-8.567 3.02-1.68 6.875-.672 8.551 2.352 1.676 3.023.672 6.886-2.348 8.566-3.015 1.68-6.875.672-8.55-2.352zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mkv.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mm.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="stroke:none;fill-rule:nonzero;fill:#999;fill-opacity:1" d="M.125 0h69.586v8.184H.125zm13.164 18.273h69.586v8.18H13.289zM.125 36.543h69.586v8.184H.125zm13.164 18.273h69.586V63H13.289zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mng.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M62.14 63.773H1.86a1.627 1.627 0 0 1-1.633-1.632V1.859C.227.953.953.227 1.859.227h60.282c.906 0 1.632.726 1.632 1.632v60.282c0 .906-.726 1.632-1.632 1.632zM3.314 59.777c0 .547.363.727.726.727h55.559c.543 0 .726-.363.726-.727V45.434c0-.543-.363-.723-.726-.723H4.223c-.547 0-.727.363-.727.723v14.343zm56.464-56.28H4.223c-.547 0-.727.362-.727.726v36.492c0 .18 0 .363.18.363l11.8-14.707 11.985 7.082 13.434-15.976 19.793 15.43V4.222c0-.547-.364-.727-.91-.727zm-48.476 44.3c2.543 0 4.722 2.176 4.722 4.719s-2.18 4.722-4.722 4.722c-2.54 0-4.719-2-4.719-4.539 0-2.543 2.18-4.902 4.719-4.902zm8.715 3.266h36.496c.543 0 .726.363.726.726v1.637c0 .543-.363.722-.726.722H20.016c-.543 0-.727-.359-.727-.722v-1.637c0-.363.363-.726.727-.726zm0 0" fill="#3c3" stroke="#3c3" stroke-miterlimit="10" stroke-width=".4539"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mo.svg
1
1
<svg height="64" width="80" xmlns="http://www.w3.org/2000/svg"><path d="M38.027 37.414c-5.011-4.812-9.425-9.223-12.03-19.25H43.64v-7.219H26.195V1.121h-7.617v10.024H.93v7.421h18.047s-.2 1.403-.399 2.606C15.968 30.996 13.164 37.215.93 43.23l2.61 7.418c11.429-6.015 17.444-13.835 20.05-22.257 2.605 6.418 6.816 11.629 11.629 16.441zM61.29 13.352H51.262L33.617 62.879h7.617l5.016-14.836H66.3l5.013 14.836h7.62zm-12.434 27.27 7.622-19.65 7.617 19.852zm0 0" fill="#a87c2d" stroke="#a87c2d" stroke-miterlimit="10" stroke-width="1.5039150000000001"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mobi.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><path d="M69.723 24.898c-.336-.851-1.012-1.535-1.688-2.222-.168.687-.336 1.37-.844 2.054L46.098 57.723c-.844 1.199-2.532 1.539-3.88 1.199l-33.75-9.574c-2.023-.512-4.386-1.711-4.554-4.106 0-.851 0-1.195.504-1.535.508-.344 1.016-.344 1.52-.172l31.726 8.89c4.555 1.368 5.902.34 9.277-4.788l19.239-30.09a5.83 5.83 0 0 0 .675-4.957c-.507-1.54-1.855-2.735-3.543-3.246L35.47 1.48c-.676-.171-1.352-.171-2.024-.171v-.172c-4.218-2.563-5.906 2.222-8.101 4.101-.844.684-1.856 1.2-2.196 1.883-.336.684-.168 1.367-.336 1.879-.843 1.883-3.207 4.957-4.386 5.813-.676.515-1.688.683-2.196 1.539-.335.511-.335 1.539-.503 2.222-.676 1.711-2.872 4.617-4.387 5.985-.508.511-1.352.855-1.688 1.539-.34.511-.172 1.539-.675 2.05-1.012 1.711-3.04 4.446-4.559 5.985-.844.855-1.856 1.195-2.191 2.05-.168.34 0 1.028-.168 1.54-.34.855-.676 1.539-.844 2.222C.37 41.141-.137 42.852.03 44.56c.34 4.105 3.375 8.207 7.09 9.234l33.746 9.574c3.207.852 7.09-.683 8.778-3.422l19.402-30.258c1.016-1.367 1.183-3.25.676-4.789zm-38.98-10.941 1.35-2.05c.337-.512 1.18-.856 1.856-.684l22.274 6.324c.675.172.843.855.507 1.371l-1.351 2.05c-.336.512-1.18.856-1.856.684L31.25 15.328c-.676-.172-.844-.687-.508-1.371zm-5.567 8.55 1.347-2.054c.34-.512 1.184-.851 1.86-.683l22.273 6.328c.676.172.844.855.504 1.367l-1.347 2.05c-.34.512-1.184.856-1.856.684L25.68 23.875c-.672-.172-1.012-.855-.504-1.367zm0 0" fill="#963"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mod.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mov.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mp2.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mp3.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mp4.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mpa.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mpd.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><g fill="#999"><path d="M0 7.336 37.754.008v63.984L0 57.02zm0 0"/><path d="M64.164 57.734H32.082c-1.594 0-2.836-1.25-2.836-2.859V9.305c0-1.61 1.242-2.864 2.836-2.864h32.082C65.758 6.441 67 7.695 67 9.305v45.57c0 1.61-1.242 2.86-2.836 2.86zM32.082 9.125c-.176 0-.355.18-.355.355v45.575c0 .18.18.36.355.36h32.082c.176 0 .356-.18.356-.36V9.305c0-.18-.18-.36-.356-.36 0 .18-32.082.18-32.082.18zm0 0"/><path d="M59.555 34.324H55.3V19.313H35.629v-4.29h23.926zm0 0"/><path d="m57.25 38.078-7.621-8.402h15.066zM37.719 42.578l8.144-8.215 8.149 8.215-8.149 8.215zm0 0"/></g><path d="M23.574 22.348c-.71-.715-1.418-1.07-2.48-1.43-.887-.355-2.13-.176-3.192-.176-2.129 0-5.847.356-5.847.356l-.18 20.73 3.898.36v-7.329s2.305.36 4.254-.18c1.067-.355 2.13-.89 2.66-1.429.711-.715 1.243-1.43 1.594-2.145.535-1.07.711-2.144.711-3.753.356-1.965-.176-3.75-1.418-5.004zm-3.012 7.148c-.71 1.61-2.66 1.61-2.66 1.61h-2.129v-6.614s1.418-.176 2.485 0c.531.18 1.062.36 1.418.54 1.062.89 1.594 3.034.886 4.464zm0 0" fill="#fff"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mpe.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mpeg.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mpg.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mpga.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mpp.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><g fill="#2d7136"><path d="M0 7.336 37.754.008v63.984L0 57.02zm0 0"/><path d="M64.164 57.734H32.082c-1.594 0-2.836-1.25-2.836-2.859V9.305c0-1.61 1.242-2.864 2.836-2.864h32.082C65.758 6.441 67 7.695 67 9.305v45.57c0 1.61-1.242 2.86-2.836 2.86zM32.082 9.125c-.176 0-.355.18-.355.355v45.575c0 .18.18.36.355.36h32.082c.176 0 .356-.18.356-.36V9.305c0-.18-.18-.36-.356-.36 0 .18-32.082.18-32.082.18zm0 0"/><path d="M59.555 34.324H55.3V19.313H35.629v-4.29h23.926zm0 0"/><path d="m57.25 38.078-7.621-8.402h15.066zM37.719 42.578l8.144-8.215 8.149 8.215-8.149 8.215zm0 0"/></g><path d="M23.574 22.348c-.71-.715-1.418-1.07-2.48-1.43-.887-.355-2.13-.176-3.192-.176-2.129 0-5.847.356-5.847.356l-.18 20.73 3.898.36v-7.329s2.305.36 4.254-.18c1.067-.355 2.13-.89 2.66-1.429.711-.715 1.243-1.43 1.594-2.145.535-1.07.711-2.144.711-3.753.356-1.965-.176-3.75-1.418-5.004zm-3.012 7.148c-.71 1.61-2.66 1.61-2.66 1.61h-2.129v-6.614s1.418-.176 2.485 0c.531.18 1.062.36 1.418.54 1.062.89 1.594 3.034.886 4.464zm0 0" fill="#fff"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/mpt.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><g fill="#2d7136"><path d="M0 7.336 37.754.008v63.984L0 57.02zm0 0"/><path d="M64.164 57.734H32.082c-1.594 0-2.836-1.25-2.836-2.859V9.305c0-1.61 1.242-2.864 2.836-2.864h32.082C65.758 6.441 67 7.695 67 9.305v45.57c0 1.61-1.242 2.86-2.836 2.86zM32.082 9.125c-.176 0-.355.18-.355.355v45.575c0 .18.18.36.355.36h32.082c.176 0 .356-.18.356-.36V9.305c0-.18-.18-.36-.356-.36 0 .18-32.082.18-32.082.18zm0 0"/><path d="M59.555 34.324H55.3V19.313H35.629v-4.29h23.926zm0 0"/><path d="m57.25 38.078-7.621-8.402h15.066zM37.719 42.578l8.144-8.215 8.149 8.215-8.149 8.215zm0 0"/></g><path d="M23.574 22.348c-.71-.715-1.418-1.07-2.48-1.43-.887-.355-2.13-.176-3.192-.176-2.129 0-5.847.356-5.847.356l-.18 20.73 3.898.36v-7.329s2.305.36 4.254-.18c1.067-.355 2.13-.89 2.66-1.429.711-.715 1.243-1.43 1.594-2.145.535-1.07.711-2.144.711-3.753.356-1.965-.176-3.75-1.418-5.004zm-3.012 7.148c-.71 1.61-2.66 1.61-2.66 1.61h-2.129v-6.614s1.418-.176 2.485 0c.531.18 1.062.36 1.418.54 1.062.89 1.594 3.034.886 4.464zm0 0" fill="#fff"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/msg.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="60"><path style="fill-rule:nonzero;fill:#1d6fb5;fill-opacity:1;stroke-width:.75;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1d6fb5;stroke-opacity:1;stroke-miterlimit:10" d="M6.274 25.574h28.3l-9.698-9.3-4.501 3.802-4.5-3.802zm34.1-25.2v28.002H.376V.374zM26.976 14.576l10.7 10.298v-19.3zm-24.2 10.298 10.7-10.298-10.7-9.002zm1.4-21.7 15.9 13.4 15.9-13.4zm0 0" transform="matrix(2.06135 0 0 2.08166 0 .076)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/msi.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><path d="m49.332 34.941-12.25-5.714L61.75 17.633 74 23.348l-12.25 5.879zM61.75 6.207 49.5.492 37.25 6.207l24.5 11.594L74 12.086zm-37.082 17.14-12.25-5.714-12.25 5.715L24.836 34.94l12.246-5.714zm0-11.429 12.25-5.711L24.668.492 0 12.086 12.25 17.8zM61.75 32.59l-11.074 5.039-1.344.672-1.34-.672-11.074-5.04-11.078 5.04-1.34.672-1.344-.672-11.074-5.04v17.977L36.75 63.508l25-12.942zm0 0" fill="#4d1b9b"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/msu.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><path d="m49.332 34.941-12.25-5.714L61.75 17.633 74 23.348l-12.25 5.879zM61.75 6.207 49.5.492 37.25 6.207l24.5 11.594L74 12.086zm-37.082 17.14-12.25-5.714-12.25 5.715L24.836 34.94l12.246-5.714zm0-11.429 12.25-5.711L24.668.492 0 12.086 12.25 17.8zM61.75 32.59l-11.074 5.039-1.344.672-1.34-.672-11.074-5.04-11.078 5.04-1.34.672-1.344-.672-11.074-5.04v17.977L36.75 63.508l25-12.942zm0 0" fill="#55486d"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/nef.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/nes.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#ff0021"><path d="M49.453.188h-14.45V64h14.45c7.883 0 14.453-6.383 14.453-14.453V14.453C63.906 6.57 57.523.188 49.453.188zm0 41.289c-4.129 0-7.508-3.375-7.508-7.508 0-4.13 3.38-7.504 7.508-7.504s7.508 3.375 7.508 7.504c0 4.133-3.379 7.508-7.508 7.508zM31.437 64h-16.89C6.664 64 .094 57.617.094 49.547V14.453C.094 6.383 6.477 0 14.547 0h16.89zM14.547 3.941c-5.82 0-10.7 4.692-10.7 10.7v35.093c0 5.82 4.692 10.7 10.7 10.7H27.87V3.94zm0 0"/><path d="M15.672 26.465c-4.129 0-7.508-3.38-7.508-7.508 0-4.129 3.379-7.508 7.508-7.508s7.508 3.38 7.508 7.508c0 4.129-3.38 7.508-7.508 7.508zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/nfo.svg
1
1
<svg height="64" width="66" xmlns="http://www.w3.org/2000/svg"><g fill="#bababa" stroke-miterlimit="10"><path d="M58 47.3v4.2H14.4v-4.2m43.2 10.1V61.6H14V57.4M57.6 68v4.1H14V68M57.6 78.5v4.2H14v-4.2" stroke="#bababa" transform="matrix(1.46667 0 0 1.48837 -19.8 -64.744)"/><path d="M29.8 60.9v-1.8c.5-.2 1.2-.399 2.2-.7.9-.3 1.8-.4 2.799-.599 1.001-.202 2-.302 2.9-.402.9-.1 1.8-.2 2.602-.2l.898.602-4.8 22.8h3.7v1.9c-.4.298-.999.6-1.598.9-.602.299-1.3.498-2 .8-.7.3-1.401.399-2.102.499-.7.1-1.398.199-2 .199-1.398 0-2.2-.3-2.799-.798-.4-.501-.6-1.102-.6-1.7 0-.701.1-1.402.2-2.1.099-.7.299-1.402.4-2.202L33.2 61.7zm4.5-11.999c0-1.202.4-2.202 1.2-2.8.801-.7 1.8-1 3.1-1 1.4 0 2.4.3 3.2 1 .8.698 1.2 1.598 1.2 2.8 0 1.1-.4 2.1-1.2 2.698-.8.701-1.9 1-3.2 1-1.2 0-2.2-.299-3.1-1-.701-.598-1.2-1.498-1.2-2.698zm0 0" stroke="#fff" stroke-width="3" transform="matrix(1.46667 0 0 1.48837 -19.8 -64.744)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/nix.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="stroke:none;fill-rule:nonzero;fill:#666;fill-opacity:1" d="M.125 0h69.586v8.184H.125zm13.164 18.273h69.586v8.18H13.289zM.125 36.543h69.586v8.184H.125zm13.164 18.273h69.586V63H13.289zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/npmignore.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M54.633 9.367C42.145-3.12 21.855-3.12 9.367 9.367s-12.488 32.778 0 45.266 32.778 12.488 45.266 0 12.488-32.778 0-45.266zM12.176 44.801c-5.934-9.211-4.84-21.543 3.12-29.504s20.294-9.055 29.505-3.121zm7.023 7.023L51.824 19.2c5.934 9.211 4.84 21.543-3.12 29.504s-20.294 9.055-29.505 3.121zm0 0" fill="#bababa"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ocx.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M60.887 19.578c2.941-7.27 3.285-13.152-.348-16.789C56.56-1.19 46.867.02 36.656 4.867h-1.21c-7.27 0-14.192 2.598-19.383 7.27-4.325 3.98-7.614 9.172-9 15.054 1.039-1.21 6.578-7.789 12.98-11.421.172 0 1.73-1.04 1.73-1.04-.171 0-3.289 2.942-3.808 3.637C3.949 32.73-4.184 54.535 2.219 60.937c4.152 4.153 11.765 3.29 20.593-1.558 3.81 1.73 7.961 2.598 12.633 2.598 6.059 0 11.594-1.559 16.442-4.848 5.02-3.285 8.652-8.133 10.73-14.016H47.04c-2.074 3.809-6.574 6.403-11.422 6.403-6.746 0-12.457-5.54-12.633-11.942v-.52h40.844v-.863c0-1.039.172-2.25.172-2.941 0-4.848-1.04-9.52-3.113-13.672zM6.719 59.555c-3.29-3.29-2.25-9.348 1.554-16.79 1.735 5.02 4.848 9.348 8.657 12.633 1.21 1.04 2.593 2.079 3.98 2.77-6.406 3.46-11.598 3.98-14.191 1.387zm41.015-30.461H23.332v-.172c.344-6.23 6.23-11.594 12.98-11.594 6.403 0 11.594 5.02 11.938 11.594v.172zM59.848 18.02c-1.211-2.079-2.77-3.98-4.672-5.54a29.6 29.6 0 0 0-9.692-6.054c6.403-2.942 11.766-3.461 14.536-.52 2.421 2.422 2.25 6.75-.172 12.114 0 .171 0 .171 0 0zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/odb.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><g fill="#a03537"><path d="M38.684 63.957c-8.098-.34-15.356-2.875-19.239-6.77-1.183-1.011-2.363-2.535-2.87-3.55l-.337-.676v-7.617c0-7.614 0-7.614.168-6.934.34 1.692 1.352 3.383 2.871 4.735 1.012.847 3.035 2.37 4.723 3.214 2.871 1.524 6.586 2.54 10.465 3.047 2.363.34 3.207.34 6.582.34s4.223 0 6.582-.34c3.883-.508 7.43-1.691 10.465-3.047 1.687-.843 3.715-2.199 4.726-3.214 1.352-1.352 2.532-3.043 2.871-4.735.168-.508.168-.508.168 6.934v7.445l-.34.68c-1.18 2.367-3.207 4.398-5.906 6.09-5.23 3.046-13.164 4.738-20.93 4.398zm0-18.95c-7.086-.339-13.668-2.37-17.887-5.413-1.016-.68-2.363-2.032-2.871-2.707a10.877 10.877 0 0 1-1.352-2.371l-.336-.676v-7.445c0-7.446 0-7.446.168-6.938.34 1.184.844 2.54 1.856 3.555.508.675 1.351 1.523 1.86 1.86.167.171.675.339 1.01.679 3.376 2.367 8.102 4.058 13.5 4.906 2.364.336 3.208.336 6.587.336 3.375 0 4.218 0 6.582-.336 3.879-.508 7.426-1.691 10.46-3.047 1.692-.847 3.716-2.2 4.727-3.215 1.352-1.351 2.364-3.047 2.871-4.738.168-.508.168-.508.168 6.938v7.445l-.507 1.015c-.844 1.524-1.348 2.368-2.364 3.383-1.011 1.016-2.023 1.864-3.375 2.54-5.398 3.046-13.332 4.738-21.097 4.23zm-.504-18.78c-4.727-.34-8.438-1.184-11.985-2.54-4.218-1.69-7.257-3.89-8.777-6.597a5.733 5.733 0 0 1-.844-2.031c-.168-.676-.336-2.368-.168-3.383C17.418 6.262 24.676 1.859 34.465.34 36.828 0 37.672 0 41.047 0s4.223 0 6.582.34c3.883.508 7.43 1.691 10.465 3.043 4.39 2.199 7.09 5.078 7.597 8.12.168.849.168 2.708-.171 3.388-.504 1.691-1.18 2.707-2.532 4.058-3.543 3.723-9.789 6.094-17.55 6.938-1.016.34-6.247.34-7.258.34zm0 0"/><path d="M38.5 55.7h1.7c2.501.2 4.5.8 6.5 1.6 3.699-1.7 9.1-.399 12.399.9-4.298-.4-9.3 0-12.2 1.7-2.9-2.4-8.498-2.999-13.699-2.4 1.4-1 3.1-1.5 5.3-1.8zm-1.3 6.601c-3 .199-5.5 1.198-7.2 2.6-5-2.302-13.7-1.3-17 1.798-.299.201-.6.402-.5.702 2.8-.9 6.3-1.6 9.901-1.302 3.5.3 6.198 1.5 8.2 3.1 3.6-3.299 8.999-5.1 15.898-5-2.4-1.4-5.8-2.3-9.3-1.898zm0 0" stroke="#fff" stroke-miterlimit="10" stroke-width=".75" transform="matrix(1.68776 0 0 1.692 -20.28 -79.405)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ods.svg
1
1
<svg height="64" width="83" xmlns="http://www.w3.org/2000/svg"><path d="M62.797 9.066H82.93v13.512H62.797zm0 20.801H82.93V43.38H62.797zm0 20.621H82.93V64H62.797zm-27.371 0h20.308V64H35.426zm-27.196 0h20.13V64H8.23zM43.371 0h2.824c4.239.355 7.594 1.422 10.95 2.668 6.359-2.848 15.187-.711 20.84 1.422-7.063-.711-15.54 0-20.485 2.844-4.945-4.09-14.305-5.157-22.957-4.09A22.506 22.506 0 0 1 43.371 0zM41.43 10.844c-5.121.355-9.36 1.957-12.008 4.265C20.945 11.2 6.465 12.977.988 18.133c-.531.355-1.058.71-.883 1.246 4.77-1.422 10.598-2.668 16.602-2.133 6.004.531 10.418 2.488 13.773 5.152 6.18-5.507 15.188-8.71 26.665-8.53-4.06-1.778-9.887-3.38-15.715-3.024zm-5.828 19.199h20.132v13.512H35.602zm-27.196 0H28.54v13.512H8.406zm0 0" fill="#1f7244"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/odt.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="fill-rule:nonzero;fill:#1a75ce;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1a75ce;stroke-opacity:1;stroke-miterlimit:10" d="M43.399 59.6h15.802v2.498H43.399zm-30.9 6.902h46.702V69.3H12.499zm0 6.799h46.702v2.798H12.499zm0 0" transform="matrix(1.7529 0 0 1.7867 -21.473 -85.752)"/><path style="stroke:none;fill-rule:nonzero;fill:#1a75ce;fill-opacity:1" d="M46.363.363h2.985c4.554.36 8.058 1.434 11.566 2.864C67.574.187 77.043 2.512 83 4.832c-7.54-.715-16.477 0-21.738 3.04-5.258-4.286-15.075-5.54-24.364-4.286 2.63-1.79 5.786-2.863 9.465-3.223zM44.262 11.98c-5.434.36-9.82 2.145-12.797 4.649-8.942-4.113-24.367-2.324-30.149 3.21-.527.36-1.054.72-.879 1.25 5.086-1.605 11.22-2.855 17.528-2.32 6.312.536 11.047 2.68 14.55 5.54 6.485-5.899 16.13-9.29 28.223-9.114-4.207-1.965-10.343-3.57-16.476-3.215zm0 0"/><path style="fill-rule:nonzero;fill:#1a75ce;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1a75ce;stroke-opacity:1;stroke-miterlimit:10" d="M12.5 80h46.7v2.8H12.5zm0 0" transform="matrix(1.7529 0 0 1.7867 -21.473 -85.752)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ogg.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ogv.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ost.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><g fill="#1d6fb5"><path d="M18.754 25.742c-2.668.36-4.8 3.219-4.8 6.258s2.132 6.078 4.8 6.258c2.668.355 4.805-2.504 4.805-6.258s-2.137-6.613-4.805-6.258zm0 0"/><path d="M.074 7.508v49.52L38.504 64V0zm18.68 34.683c-4.27-.539-7.649-5.187-7.649-10.191 0-5.184 3.38-9.652 7.649-10.191 4.27-.536 7.652 4.113 7.652 10.191 0 6.258-3.383 10.727-7.652 10.191zm50.172-27.175L47.754 32.715l-5.691-4.649v-14.66h26.863zm0 0"/><path d="m68.926 18.414-21.172 17.7-5.691-4.65V51.31h26.863zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/otf.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="48"><path style="stroke:none;fill-rule:nonzero;fill:#7291a1;fill-opacity:1" d="M28.621 33.172h-16.32l-2.012 4.45c-.55 1.483-.918 2.593-.918 3.706 0 1.297.547 2.223 1.649 2.781.55.371 2.203.555 4.582.743v1.293H.203v-1.293c1.652-.188 2.934-.93 4.035-2.04 1.098-1.113 2.383-3.34 3.848-6.859L24.586 0h.73L42 36.879c1.648 3.52 2.934 5.746 3.852 6.672.73.742 1.832 1.113 3.296 1.113v1.297h-22.18v-1.297h.919c1.832 0 3.113-.184 3.847-.742.551-.371.735-.926.735-1.48 0-.372 0-.743-.184-1.301 0-.184-.367-1.11-1.101-2.407zm-1.101-2.406-6.786-15.57-7.148 15.57zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#36454d;fill-opacity:1" d="m83.797 16.309-6.602 22.054-.734 2.778c0 .375-.184.558-.184.742 0 .187.184.558.371.742.184.188.368.371.547.371.551 0 1.102-.371 2.016-1.113.371-.367 1.102-1.297 2.387-2.965l1.097.559c-1.648 2.964-3.3 5.003-5.132 6.3-1.833 1.297-3.852 2.04-5.864 2.04-1.285 0-2.203-.372-2.933-.93-.735-.742-1.102-1.485-1.102-2.407 0-.93.367-2.41 1.102-4.82l.73-2.781c-2.562 4.45-5.133 7.601-7.516 9.453C60.516 47.442 59.05 48 57.582 48c-2.016 0-3.668-.926-4.582-2.594-.918-1.668-1.465-3.523-1.465-5.746 0-3.152.914-6.672 2.934-10.75 2.011-4.074 4.582-7.226 7.695-9.82 2.566-2.04 5.133-2.965 7.332-2.965 1.285 0 2.203.367 3.121 1.11.73.742 1.281 2.038 1.649 3.89l1.28-4.074zM72.98 22.797c0-1.856-.367-3.152-.918-3.895-.367-.554-.914-.742-1.648-.742-.734 0-1.469.375-2.2.93-1.464 1.297-3.116 4.074-4.948 8.336-1.832 4.265-2.57 7.785-2.57 10.937 0 1.11.183 2.035.554 2.594.363.559.914.742 1.281.742 1.098 0 2.016-.558 3.117-1.668 1.465-1.668 2.934-3.707 4.032-5.93 2.199-4.449 3.3-8.156 3.3-11.304zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ott.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="fill-rule:nonzero;fill:#1a75ce;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1a75ce;stroke-opacity:1;stroke-miterlimit:10" d="M43.399 59.6h15.802v2.498H43.399zm-30.9 6.902h46.702V69.3H12.499zm0 6.799h46.702v2.798H12.499zm0 0" transform="matrix(1.7529 0 0 1.7867 -21.473 -85.752)"/><path style="stroke:none;fill-rule:nonzero;fill:#1a75ce;fill-opacity:1" d="M46.363.363h2.985c4.554.36 8.058 1.434 11.566 2.864C67.574.187 77.043 2.512 83 4.832c-7.54-.715-16.477 0-21.738 3.04-5.258-4.286-15.075-5.54-24.364-4.286 2.63-1.79 5.786-2.863 9.465-3.223zM44.262 11.98c-5.434.36-9.82 2.145-12.797 4.649-8.942-4.113-24.367-2.324-30.149 3.21-.527.36-1.054.72-.879 1.25 5.086-1.605 11.22-2.855 17.528-2.32 6.312.536 11.047 2.68 14.55 5.54 6.485-5.899 16.13-9.29 28.223-9.114-4.207-1.965-10.343-3.57-16.476-3.215zm0 0"/><path style="fill-rule:nonzero;fill:#1a75ce;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1a75ce;stroke-opacity:1;stroke-miterlimit:10" d="M12.5 80h46.7v2.8H12.5zm0 0" transform="matrix(1.7529 0 0 1.7867 -21.473 -85.752)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ova.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><g fill="#4d1b9b"><path d="M73.734 51.555c0-2.844-2.289-5.157-5.109-5.157H5.375c-2.82 0-5.11 2.313-5.11 5.157v7.289c0 2.843 2.29 5.156 5.11 5.156h63.25c2.82 0 5.11-2.313 5.11-5.156zm-27.308 6.757a2.985 2.985 0 0 1-2.996-3.023 2.985 2.985 0 0 1 2.996-3.023 2.985 2.985 0 0 1 2.996 3.023c0 1.777-1.234 3.023-2.996 3.023zm8.984 0a2.984 2.984 0 0 1-2.992-3.023c0-1.777 1.23-3.023 2.992-3.023a2.985 2.985 0 0 1 2.996 3.023 2.985 2.985 0 0 1-2.996 3.023zm8.813 0a2.985 2.985 0 0 1-2.996-3.023c0-1.777 1.234-3.023 2.996-3.023a2.981 2.981 0 0 1 2.992 3.023 2.981 2.981 0 0 1-2.992 3.023zM5.375 43.38h63.25c1.41 0 2.82.355 3.879 1.066l-6.168-12.98c-1.762-3.73-4.582-5.153-7.398-5.153h-6.876L42.2 36.623c-.707.71-1.586 1.245-2.469 1.6-.878.356-1.937.532-2.82.532-1.055 0-1.937-.176-2.816-.531h-.352c-.707-.356-1.41-.891-2.117-1.422l-9.867-10.668h-6.871c-2.817 0-5.461 1.601-7.399 5.156L1.32 44.266c1.235-.532 2.47-.887 4.055-.887zm0 0"/><path d="M51.71 21.332c.352-.355.532-.71.884-1.242.176-.535.351-.89.351-1.602 0-.531-.175-1.066-.351-1.422-.176-.53-.532-.886-.883-1.246a5.273 5.273 0 0 0-1.23-.886c-.356-.18-.883-.356-1.41-.356-.532 0-1.06.176-1.41.356-.528.175-.884.53-1.235.886l-5.637 5.692V3.734c0-.535-.176-1.066-.352-1.421-.18-.536-.53-.891-.882-1.247-.352-.355-.703-.71-1.235-.886C37.97 0 37.441 0 36.91 0c-.527 0-1.055 0-1.406.18-.531.175-.883.53-1.234.886-.352.356-.708.711-.883 1.246-.176.532-.352.887-.352 1.422v17.953L27.398 16c-.351-.355-.707-.71-1.234-.89-.352-.176-.879-.356-1.41-.356-.527 0-1.055.18-1.41.355-.352.18-.88.536-1.23.891-.356.355-.708.71-.884 1.246-.175.531-.351.887-.351 1.422 0 .531.176 1.066.351 1.598.176.535.528.89.883 1.246L34.27 33.957c.351.355.703.711 1.234.887.351.18.879.355 1.406.355.531 0 1.059-.176 1.41-.355.532-.176.883-.532 1.235-.887zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ovf.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><g fill="#4d1b9b"><path d="M73.734 51.555c0-2.844-2.289-5.157-5.109-5.157H5.375c-2.82 0-5.11 2.313-5.11 5.157v7.289c0 2.843 2.29 5.156 5.11 5.156h63.25c2.82 0 5.11-2.313 5.11-5.156zm-27.308 6.757a2.985 2.985 0 0 1-2.996-3.023 2.985 2.985 0 0 1 2.996-3.023 2.985 2.985 0 0 1 2.996 3.023c0 1.777-1.234 3.023-2.996 3.023zm8.984 0a2.984 2.984 0 0 1-2.992-3.023c0-1.777 1.23-3.023 2.992-3.023a2.985 2.985 0 0 1 2.996 3.023 2.985 2.985 0 0 1-2.996 3.023zm8.813 0a2.985 2.985 0 0 1-2.996-3.023c0-1.777 1.234-3.023 2.996-3.023a2.981 2.981 0 0 1 2.992 3.023 2.981 2.981 0 0 1-2.992 3.023zM5.375 43.38h63.25c1.41 0 2.82.355 3.879 1.066l-6.168-12.98c-1.762-3.73-4.582-5.153-7.398-5.153h-6.876L42.2 36.623c-.707.71-1.586 1.245-2.469 1.6-.878.356-1.937.532-2.82.532-1.055 0-1.937-.176-2.816-.531h-.352c-.707-.356-1.41-.891-2.117-1.422l-9.867-10.668h-6.871c-2.817 0-5.461 1.601-7.399 5.156L1.32 44.266c1.235-.532 2.47-.887 4.055-.887zm0 0"/><path d="M51.71 21.332c.352-.355.532-.71.884-1.242.176-.535.351-.89.351-1.602 0-.531-.175-1.066-.351-1.422-.176-.53-.532-.886-.883-1.246a5.273 5.273 0 0 0-1.23-.886c-.356-.18-.883-.356-1.41-.356-.532 0-1.06.176-1.41.356-.528.175-.884.53-1.235.886l-5.637 5.692V3.734c0-.535-.176-1.066-.352-1.421-.18-.536-.53-.891-.882-1.247-.352-.355-.703-.71-1.235-.886C37.97 0 37.441 0 36.91 0c-.527 0-1.055 0-1.406.18-.531.175-.883.53-1.234.886-.352.356-.708.711-.883 1.246-.176.532-.352.887-.352 1.422v17.953L27.398 16c-.351-.355-.707-.71-1.234-.89-.352-.176-.879-.356-1.41-.356-.527 0-1.055.18-1.41.355-.352.18-.88.536-1.23.891-.356.355-.708.71-.884 1.246-.175.531-.351.887-.351 1.422 0 .531.176 1.066.351 1.598.176.535.528.89.883 1.246L34.27 33.957c.351.355.703.711 1.234.887.351.18.879.355 1.406.355.531 0 1.059-.176 1.41-.355.532-.176.883-.532 1.235-.887zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/p12.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M31.816 6.078c5.094 5.094 6.547 12.184 4.73 18.547l26.907 26.91.547 12-15.09-1.273v-7.637h-7.637v-7.637h-7.457L24 37.172c-6.363 1.816-13.637.363-18.547-4.73-7.27-7.27-7.27-19.27 0-26.544a18.494 18.494 0 0 1 26.363.18zM18 11.172c-2.184-2.184-5.453-2.184-7.637 0-2.18 2.18-2.18 5.453 0 7.637 2.184 2.18 5.453 2.18 7.637 0 2.184-2.184 2.184-5.637 0-7.637zm0 0" fill="#696"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/p7b.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M31.816 6.078c5.094 5.094 6.547 12.184 4.73 18.547l26.907 26.91.547 12-15.09-1.273v-7.637h-7.637v-7.637h-7.457L24 37.172c-6.363 1.816-13.637.363-18.547-4.73-7.27-7.27-7.27-19.27 0-26.544a18.494 18.494 0 0 1 26.363.18zM18 11.172c-2.184-2.184-5.453-2.184-7.637 0-2.18 2.18-2.18 5.453 0 7.637 2.184 2.18 5.453 2.18 7.637 0 2.184-2.184 2.184-5.637 0-7.637zm0 0" fill="#696"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/part.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="63"><path style="stroke:none;fill-rule:nonzero;fill:#bababa;fill-opacity:1" d="M78.953 17.746c-.969-.191-1.742-.387-2.707-.191-2.125.191-4.441 1.36-6.566 1.746-1.543.199-2.895-.973-3.09-2.719a102.076 102.076 0 0 1 0-15.379c0-.777-.578-1.168-1.156-1.168C59.64-.156 54.039.425 48.05 1.79c-1.547.387-2.7 1.754-2.7 3.113.188 3.118 2.313 6.618 1.348 9.926-.773 2.918-3.09 5.059-6.18 5.645-2.894.586-5.988-.778-7.53-3.114-1.93-2.726-.966-6.62-1.739-9.925-.383-1.559-1.93-2.34-3.473-2.145a62.499 62.499 0 0 0-16.804 4.09c-.77.195-1.157.973-.77 1.555 2.125 4.671 3.664 9.539 5.02 14.597.386 1.559-.578 3.117-2.125 3.504-2.125.586-4.637.195-6.758.777-.969.196-1.738.586-2.508 1.168-2.707 1.754-3.867 4.672-3.48 7.59.39 2.727 2.507 5.063 5.02 6.23 3.284 1.165 6.183-.972 9.464-1.167 1.543-.192 2.898.972 3.09 2.726.387 5.059.387 10.313 0 15.371 0 .782.578 1.168 1.16 1.168 5.789.391 11.586-.386 17.379-1.75 1.543-.39 2.703-1.75 2.703-3.113-.195-3.308-2.508-6.617-1.543-10.12.77-2.727 3.473-5.06 6.18-5.454 2.699-.387 5.988.781 7.53 3.117 2.122 2.727.966 6.813 1.74 10.121.382 1.555 1.929 2.336 3.472 2.14 5.797-.585 11.59-1.753 16.805-4.089.77-.195 1.156-.973.77-1.555-2.126-4.672-3.669-9.535-5.02-14.597-.387-1.559.578-3.114 2.125-3.5 3.09-.782 6.566.195 9.46-1.95 2.126-1.75 3.477-4.671 2.895-7.394.195-3.695-2.121-6.227-4.629-7.008zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pdb.svg
1
1
<svg height="64" width="51" xmlns="http://www.w3.org/2000/svg"><path d="M23.023 63.957c-8.199-.34-15.543-2.875-19.468-6.77-1.196-1.011-2.39-2.535-2.903-3.55L.31 52.96v-7.617c0-7.614 0-7.614.171-6.934.34 1.692 1.368 3.383 2.903 4.735 1.023.847 3.074 2.37 4.781 3.214 2.906 1.524 6.66 2.54 10.59 3.047 2.39.34 3.246.34 6.66.34 3.418 0 4.27 0 6.66-.34 3.93-.508 7.516-1.691 10.59-3.047 1.707-.843 3.758-2.199 4.781-3.214 1.368-1.352 2.563-3.043 2.903-4.735.172-.508.172-.508.172 6.934v7.445l-.34.68c-1.196 2.367-3.246 4.398-5.98 6.09-5.294 3.046-13.321 4.738-21.177 4.398zm0-18.95c-7.171-.339-13.832-2.37-18.101-5.413-1.027-.68-2.39-2.032-2.906-2.707-.512-.68-1.024-1.524-1.364-2.371L.31 33.84v-7.445c0-7.446 0-7.446.171-6.938.34 1.184.852 2.54 1.88 3.555.511.675 1.367 1.523 1.878 1.86.168.171.684.339 1.024.679 3.414 2.367 8.199 4.058 13.664 4.906 2.39.336 3.242.336 6.66.336 3.414 0 4.27 0 6.66-.336 3.93-.508 7.516-1.691 10.59-3.047 1.707-.847 3.758-2.2 4.781-3.215 1.367-1.351 2.39-3.047 2.903-4.738.171-.508.171-.508.171 6.938v7.445l-.511 1.015c-.856 1.524-1.368 2.368-2.39 3.383-1.028 1.016-2.052 1.864-3.419 2.54-5.465 3.046-13.492 4.738-21.348 4.23zm-.511-18.78c-4.782-.34-8.54-1.184-12.125-2.54-4.27-1.69-7.344-3.89-8.883-6.597a5.594 5.594 0 0 1-.852-2.031C.48 14.383.31 12.69.48 11.676 1.504 6.262 8.848 1.859 18.754.34 21.144 0 22 0 25.414 0c3.418 0 4.27 0 6.66.34 3.93.508 7.516 1.691 10.59 3.043 4.441 2.199 7.172 5.078 7.684 8.12.172.849.172 2.708-.168 3.388-.512 1.691-1.196 2.707-2.563 4.058-3.586 3.723-9.906 6.094-17.762 6.938-1.023.34-6.32.34-7.343.34zm0 0" fill="#a03537"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pdf.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M27.762.637c-3.899 0-6.95 3.218-6.95 6.949 0 4.746 2.711 10.68 5.422 16.277-2.203 6.782-4.574 14.07-7.797 20.172-6.44 2.547-12.207 4.41-15.597 7.293l-.168.168C1.484 52.852.637 54.546.637 56.414c0 3.898 3.218 6.95 6.949 6.95 1.867 0 3.73-.677 4.918-2.036 0 0 .168 0 .168-.168 2.543-3.05 5.594-8.644 8.308-13.562 6.102-2.375 12.715-4.918 18.817-6.442 4.578 3.73 11.191 6.102 16.617 6.102 3.898 0 6.95-3.223 6.95-6.95 0-3.902-3.22-6.953-6.95-6.953-4.41 0-10.68 1.528-15.43 3.223a56.197 56.197 0 0 1-10.172-13.223c1.868-5.765 4.07-11.359 4.07-15.77-.171-3.898-3.222-6.948-7.12-6.948zm0 4.066c1.527 0 2.71 1.188 2.71 2.715 0 2.035-1.183 5.934-2.37 10-1.696-4.066-3.223-7.965-3.223-10 .172-1.356 1.355-2.715 2.883-2.715zm1.187 23.906c2.035 3.391 4.578 6.442 7.29 9.157a171.201 171.201 0 0 0-12.208 4.066c2.035-4.238 3.563-8.816 4.918-13.223zm27.465 8.985a2.679 2.679 0 0 1 2.711 2.715 2.678 2.678 0 0 1-2.71 2.71c-3.224 0-7.63-1.355-11.192-3.39 4.07-1.016 8.648-2.035 11.191-2.035zM14.875 49.973c-2.031 3.558-3.898 6.78-5.254 8.476-.508.508-1.016.676-1.863.676a2.679 2.679 0 0 1-2.715-2.71c0-.68.34-1.528.68-1.868 1.523-1.356 5.086-2.879 9.152-4.574zm0 0" fill="#c11e07" stroke="#c11e07" stroke-miterlimit="10" stroke-width="1.27152"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pem.svg
1
1
<svg height="64" width="52" xmlns="http://www.w3.org/2000/svg"><path d="M48.793 26.879h-4.629V18.05C44.164 7.988 35.973.043 26 .043S7.836 8.164 7.836 18.051v8.828H3.207A3.181 3.181 0 0 0 0 30.059V60.78c0 1.762 1.426 3.176 3.207 3.176h45.586c1.781 0 3.207-1.414 3.207-3.176V29.883c0-1.59-1.426-3.004-3.207-3.004zM29.918 52.305c.355 1.058-.535 1.941-1.602 1.941h-4.808c-1.07 0-1.781-1.059-1.606-1.941l1.426-5.649c-1.781-.883-3.027-2.648-3.027-4.945 0-3 2.492-5.473 5.52-5.473 3.027 0 5.523 2.473 5.523 5.473 0 2.117-1.246 4.062-3.028 4.945zm5.164-25.426H16.918V18.05c0-4.942 4.098-9.004 9.082-9.004s9.082 4.062 9.082 9.004zm0 0" fill="#696"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pfx.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M31.816 6.078c5.094 5.094 6.547 12.184 4.73 18.547l26.907 26.91.547 12-15.09-1.273v-7.637h-7.637v-7.637h-7.457L24 37.172c-6.363 1.816-13.637.363-18.547-4.73-7.27-7.27-7.27-19.27 0-26.544a18.494 18.494 0 0 1 26.363.18zM18 11.172c-2.184-2.184-5.453-2.184-7.637 0-2.18 2.18-2.18 5.453 0 7.637 2.184 2.18 5.453 2.18 7.637 0 2.184-2.184 2.184-5.637 0-7.637zm0 0" fill="#696"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pgp.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M31.816 6.078c5.094 5.094 6.547 12.184 4.73 18.547l26.907 26.91.547 12-15.09-1.273v-7.637h-7.637v-7.637h-7.457L24 37.172c-6.363 1.816-13.637.363-18.547-4.73-7.27-7.27-7.27-19.27 0-26.544a18.494 18.494 0 0 1 26.363.18zM18 11.172c-2.184-2.184-5.453-2.184-7.637 0-2.18 2.18-2.18 5.453 0 7.637 2.184 2.18 5.453 2.18 7.637 0 2.184-2.184 2.184-5.637 0-7.637zm0 0" fill="#696"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ph.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M64 32c0 17.645-14.355 32-32 32S0 49.645 0 32 14.355 0 32 0s32 14.355 32 32" fill="#999"/><g fill="#fff"><path d="M54.313 34.422c-1.036-6.746-8.13-11.242-13.493-14.531-2.246-1.383-6.054-3.286-6.57-6.399-.176-1.21-.176-2.594-.176-3.805V8.13c0-.692-.691.172-1.035-.344-.867-1.387-.52.344-.52 1.211.172 1.727.52 3.457.52 5.188 0 3.285-.52 6.574-1.387 9.687-1.902 7.438-3.457 15.223-1.554 22.832a24.518 24.518 0 0 0 1.554 4.668c.176.52.52 1.73 1.211 1.906 2.078.516 3.633.692 5.192 2.246 1.039.868 1.73.348 2.941 0 3.633-1.382 6.746-3.285 9.34-6.226 3.285-4.496 4.844-9.512 3.977-14.875m-3.805 6.746c-.344 2.766-2.074 5.363-3.805 7.437-1.383 1.56-3.113 3.461-5.016 4.153-.69.172.172-1.211.172-1.211.52-.867 1.383-1.73 2.075-2.594 1.039-1.21 1.902-2.598 2.421-3.98 1.903-5.016 1.56-10.899-1.382-15.395-1.555-2.422-3.805-4.496-5.88-6.398-1.038-.868-2.077-1.73-2.94-2.77-.176-.172-2.079-2.594-1.387-2.941.175-.172 4.152 3.98 4.5 4.324 1.554 1.21 3.285 2.422 4.843 3.809 2.075 1.902 4.149 3.976 5.36 6.57 1.21 2.77 1.386 6.055 1.039 8.996"/><path d="M30.79.863c.519.348.69 2.77.69 4.844 0 2.078.172 11.246-.52 13.664-.69 2.422-2.245 5.192-3.804 7.613-1.73 2.422-3.633 7.438-3.457 10.551 0 3.113 1.903 8.13 3.285 10.38 1.383 2.073 3.805 5.015 3.286 5.706-.864 1.211-4.668-2.941-6.747-5.363-1.902-2.422-3.976-7.262-3.976-11.07 0-3.805 2.074-7.262 3.633-9.34 1.554-2.075 4.496-5.707 5.36-7.438.866-1.73 1.73-3.457 1.901-5.707.348-2.25 0-10.55 0-10.55S30.27.52 30.79.862"/><path d="M29.234 4.844c.516.343.692 1.039.692 1.73 0 .692-.176 3.633-.348 6.57-.172 2.942-2.594 5.364-4.152 7.094-1.727 1.73-6.746 7.09-8.473 9.688-1.906 2.594-2.77 6.05-2.598 8.992.176 2.941.868 5.883 3.633 8.996 2.77 3.113 4.672 4.496 6.227 5.363 1.387.692 2.941 1.211 2.597 1.903-.347.691-1.73.172-3.289-.348-1.554-.52-6.746-2.594-9.687-6.055-2.938-3.457-4.496-7.957-4.324-12.105.175-4.324 1.386-6.055 3.289-8.824 1.902-2.766 7.437-6.918 9.168-7.957 1.73-1.036 3.976-2.766 5.187-4.325 1.211-1.382 1.73-2.593 1.73-4.668 0-1.902.173-3.804 0-4.5-.171-.515-.171-1.902.348-1.554m.172 51.89c.344 0 .172 1.211-.347 1.73-.52.52-1.211.864-1.383.692s.52-.343 1.039-.863c.52-.691.344-1.559.691-1.559m5.36-.172c-.344 0-.172 1.211.347 1.731s1.211.863 1.383.691c.176-.172-.52-.347-1.035-.867-.52-.515-.348-1.554-.695-1.554m-2.418 1.382c0 1.04 0 1.903-.176 1.903-.344 0-.172-.864-.172-1.903 0-1.039-.172-1.902.172-1.902.348 0 .176.863.176 1.902"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/phar.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M65.66 13.137 33.5.168 20.77 5.391l32.16 12.8zM12.73 8.59 1.34 13.137 33.5 26.105l11.39-4.546zM0 50.695l32.496 13.137V27.789L0 14.652zm54.27-30.82V29.98l-1.34-.843-1.34 1.851-1.34-.672-1.34 1.852-1.34-.84-1.34 1.852V23.074L34.504 27.79v36.043L67 50.695V14.652zm0 0" fill="#6781b2"/><path d="M7.371 36.21c.668.337 1.34.337 1.676.169.332-.168.836-.504 1.172-1.012.332-.504.5-1.008.332-1.176-.164-.171-.5-.675-1.172-.843l-1.004-.676-1.844 3.035zm-5.195 2.528c-.164-.168-.164-.168 0-.34l4.86-8.082.167-.168 3.516 1.348c1.172.504 1.844 1.008 2.18 1.852.335.843.167 1.683-.504 2.695-.168.336-.504.84-.836 1.008-.336.34-.672.508-1.172.676-.504.168-1.008.336-1.508.336-.504 0-1.176-.168-1.844-.504l-1.508-.508-1.34 2.023-.167.168zm14.238 2.864c-.168-.172-.168-.172 0-.34l2.18-3.535c.168-.336.332-.676.168-.676 0-.168-.168-.336-.672-.504l-1.34-.504-2.68 4.379-.168.168-1.843-.676s-.168 0-.168-.168v-.168l4.859-8.082.168-.168 1.844.672s.164 0 .164.168v.168l-1.172 2.023 1.34.504c1.008.336 1.676.844 2.011 1.516.336.504.168 1.348-.335 2.02l-2.344 3.706-.168.168zm7.707 1.007c.668.34 1.34.34 1.84.168.504-.168.84-.504 1.176-1.007.332-.508.5-1.012.332-1.18-.168-.336-.5-.676-1.172-.844l-1.172-.504-1.844 3.031zm-5.36 2.528c-.167-.168-.167-.168-.167-.336l5.023-8.086s.168-.168.336 0l3.684 1.347c1.172.508 1.843 1.012 2.18 1.852.331.844.167 1.688-.504 2.695-.168.34-.504.844-.836 1.012-.336.336-.672.504-1.176.672-.5.172-1.172.34-1.672.34-.504 0-1.176-.168-2.012-.508l-1.34-.336-1.34 2.02s-.167.171-.335 0zm0 0" fill="#fff"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/php.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="52"><path style="stroke:none;fill-rule:nonzero;fill:#6781b2;fill-opacity:1" d="M36.18 38.57c3.308 1.235 8.703 1.063 12.535.528.7 1.593-.348 3.715.347 5.48.352.887 1.395 1.594 3.137 2.477.348.18.867.18 1.219.355l.348.352c.695.531 2.09 1.418 2.437 1.418.524.172 1.047.172 1.39.172h1.747c2.433-.172 4.87-.883 5.57-1.766 1.219-1.59.524-5.125.348-8.309-.176-2.652-.524-6.187 0-8.308.176-.707.87-1.418 1.215-2.301 1.394-3.004 2.789-7.957 2.27-12.73-.352-2.122-1.395-4.063-1.571-5.653 3.308.352 6.441-.355 9.226 0 1.747.172 3.137 1.41 4.704 1.235.351-.883 1.39-1.415 1.39-2.473.176-1.238-.344-2.656-.867-3.54-2.266-.35-4.004 1.77-6.266 1.95-.703 0-1.57-.18-2.441-.355-2.613-.18-6.266.527-8.531 0-1.567-.356-2.961-2.122-4.528-2.829-.347-.18-1.043 0-1.394-.355-.52-.176-.871-.531-1.215-.531-1.742-.703-3.66-1.414-5.402-1.59C48.543.91 44.016.91 39.836 1.266c-1.39.18-2.61.882-4.004.53C34.785 1.618 34.613.91 33.918.56c-2.965-1.414-5.922.175-7.84 1.238-1.39.707-3.129 1.766-4.527 1.945-1.39.352-3.48 0-4.7 0-1.566 0-3.656.352-5.398.531-1.566.352-3.828.528-4.7 1.235-2.437 1.414-3.132 7.957-4.007 11.847-.344 1.415-.867 2.829-1.215 4.243-.523 3.18-.875 6.543-.875 9.547-.172 6.187-.867 14.851 2.27 17.148.695.531 2.957 1.238 3.652.887.176 0 1.047-.887 1.219-1.239.176-.53-.344-1.238-.344-1.945 0-1.418-.351-3.184-.351-4.598 0-3.71.695-7.777 1.566-9.37 0-.176.523-.176.523-.352.176-.352 0-.707.352-.887.695-.703 1.738-1.414 2.434-1.59 2.09-.883 3.308.176 4.18 1.414 1.741 2.301 2.09 6.188 2.265 9.903v2.297c0 .882-.352 1.765-.352 2.296.524 1.414 2.961 2.125 4.008 2.832 0 .528.176 1.239.52 1.59.523.887 1.394 1.414 1.918 1.766 2.609 1.418 9.226.531 10.449-1.234.172-.18.344-.356.344-.708.171-.53.523-1.062.523-1.414 1.047-3.183-.172-6.011.348-9.37zM32.523 6.922c-.171.18-.171.18 0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pid.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M62.887 38.266c-2.684-.84-4.532-3.36-4.532-6.215 0-2.852 1.848-5.371 4.532-6.211.84-.336 1.343-1.172 1.008-2.012-.84-3.023-1.848-5.707-3.524-8.394-.504-.84-1.344-1.008-2.184-.672-1.007.504-2.015.84-3.19.84-3.692 0-6.548-3.024-6.548-6.547 0-1.176.336-2.184.84-3.188.504-.84.168-1.68-.672-2.183a40.47 40.47 0 0 0-8.39-3.528c-.84-.168-1.68.168-2.016 1.008C37.37 3.852 34.855 5.7 32 5.7s-5.371-1.847-6.21-4.535C25.452.324 24.612-.18 23.772.156c-3.02.84-5.707 1.848-8.39 3.528-.84.503-1.008 1.343-.672 2.183.504 1.004.84 2.012.84 3.188 0 3.691-3.024 6.547-6.547 6.547-1.176 0-2.184-.336-3.191-.84-.84-.504-1.68-.168-2.184.672a40.699 40.699 0 0 0-3.524 8.394c-.167.84.168 1.676 1.008 2.012 2.684.84 4.532 3.36 4.532 6.21 0 2.856-1.848 5.376-4.532 6.216-.84.332-1.343 1.172-1.008 2.011.84 3.024 1.848 5.707 3.524 8.395.504.84 1.344 1.008 2.184.672 1.007-.504 2.015-.84 3.19-.84 3.692 0 6.548 3.02 6.548 6.547 0 1.176-.336 2.183-.84 3.187-.504.84-.168 1.68.672 2.184a40.47 40.47 0 0 0 8.39 3.527h.336c.672 0 1.344-.504 1.512-1.176.84-2.687 3.356-4.535 6.211-4.535s5.371 1.848 6.211 4.535c.336.84 1.176 1.344 2.016 1.008 3.02-.84 5.707-1.847 8.39-3.527.84-.504 1.008-1.344.672-2.184-.504-1.004-.84-2.011-.84-3.187 0-3.692 3.024-6.547 6.547-6.547 1.176 0 2.184.336 3.192.84.84.504 1.68.168 2.183-.672a40.698 40.698 0 0 0 3.524-8.395c.503-.672 0-1.511-.84-1.843zm-30.719 3.691c-5.371 0-9.902-4.363-9.902-9.906 0-5.371 4.363-9.903 9.902-9.903 5.371 0 9.902 4.364 9.902 9.903 0 5.375-4.53 9.906-9.902 9.906zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pkg.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><path d="m49.332 34.941-12.25-5.714L61.75 17.633 74 23.348l-12.25 5.879zM61.75 6.207 49.5.492 37.25 6.207l24.5 11.594L74 12.086zm-37.082 17.14-12.25-5.714-12.25 5.715L24.836 34.94l12.246-5.714zm0-11.429 12.25-5.711L24.668.492 0 12.086 12.25 17.8zM61.75 32.59l-11.074 5.039-1.344.672-1.34-.672-11.074-5.04-11.078 5.04-1.34.672-1.344-.672-11.074-5.04v17.977L36.75 63.508l25-12.942zm0 0" fill="#4d1b9b"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pl.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M64 32c0 17.645-14.355 32-32 32S0 49.645 0 32 14.355 0 32 0s32 14.355 32 32" fill="#3a3c5b"/><g fill="#fff"><path d="M54.313 34.422c-1.036-6.746-8.13-11.242-13.493-14.531-2.246-1.383-6.054-3.286-6.57-6.399-.176-1.21-.176-2.594-.176-3.805V8.13c0-.692-.691.172-1.035-.344-.867-1.387-.52.344-.52 1.211.172 1.727.52 3.457.52 5.188 0 3.285-.52 6.574-1.387 9.687-1.902 7.438-3.457 15.223-1.554 22.832a24.518 24.518 0 0 0 1.554 4.668c.176.52.52 1.73 1.211 1.906 2.078.516 3.633.692 5.192 2.246 1.039.868 1.73.348 2.941 0 3.633-1.382 6.746-3.285 9.34-6.226 3.285-4.496 4.844-9.512 3.977-14.875m-3.805 6.746c-.344 2.766-2.074 5.363-3.805 7.437-1.383 1.56-3.113 3.461-5.016 4.153-.69.172.172-1.211.172-1.211.52-.867 1.383-1.73 2.075-2.594 1.039-1.21 1.902-2.598 2.421-3.98 1.903-5.016 1.56-10.899-1.382-15.395-1.555-2.422-3.805-4.496-5.88-6.398-1.038-.868-2.077-1.73-2.94-2.77-.176-.172-2.079-2.594-1.387-2.941.175-.172 4.152 3.98 4.5 4.324 1.554 1.21 3.285 2.422 4.843 3.809 2.075 1.902 4.149 3.976 5.36 6.57 1.21 2.77 1.386 6.055 1.039 8.996"/><path d="M30.79.863c.519.348.69 2.77.69 4.844 0 2.078.172 11.246-.52 13.664-.69 2.422-2.245 5.192-3.804 7.613-1.73 2.422-3.633 7.438-3.457 10.551 0 3.113 1.903 8.13 3.285 10.38 1.383 2.073 3.805 5.015 3.286 5.706-.864 1.211-4.668-2.941-6.747-5.363-1.902-2.422-3.976-7.262-3.976-11.07 0-3.805 2.074-7.262 3.633-9.34 1.554-2.075 4.496-5.707 5.36-7.438.866-1.73 1.73-3.457 1.901-5.707.348-2.25 0-10.55 0-10.55S30.27.52 30.79.862"/><path d="M29.234 4.844c.516.343.692 1.039.692 1.73 0 .692-.176 3.633-.348 6.57-.172 2.942-2.594 5.364-4.152 7.094-1.727 1.73-6.746 7.09-8.473 9.688-1.906 2.594-2.77 6.05-2.598 8.992.176 2.941.868 5.883 3.633 8.996 2.77 3.113 4.672 4.496 6.227 5.363 1.387.692 2.941 1.211 2.597 1.903-.347.691-1.73.172-3.289-.348-1.554-.52-6.746-2.594-9.687-6.055-2.938-3.457-4.496-7.957-4.324-12.105.175-4.324 1.386-6.055 3.289-8.824 1.902-2.766 7.437-6.918 9.168-7.957 1.73-1.036 3.976-2.766 5.187-4.325 1.211-1.382 1.73-2.593 1.73-4.668 0-1.902.173-3.804 0-4.5-.171-.515-.171-1.902.348-1.554m.172 51.89c.344 0 .172 1.211-.347 1.73-.52.52-1.211.864-1.383.692s.52-.343 1.039-.863c.52-.691.344-1.559.691-1.559m5.36-.172c-.344 0-.172 1.211.347 1.731s1.211.863 1.383.691c.176-.172-.52-.347-1.035-.867-.52-.515-.348-1.554-.695-1.554m-2.418 1.382c0 1.04 0 1.903-.176 1.903-.344 0-.172-.864-.172-1.903 0-1.039-.172-1.902.172-1.902.348 0 .176.863.176 1.902"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/plist.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><path d="M69.5 0H4.633C2.125 0 .285 2.008.285 4.352V49.39c0 2.511 2.004 4.351 4.348 4.351h64.699c2.508 0 4.348-2.008 4.348-4.351V4.52C74.016 2.008 72.008 0 69.5 0zm0 0" fill="#ced2d8"/><path d="M4.633 10.883H69.5V49.39H4.633zm0 0" fill="#f2f2f2"/><path d="M18.008 5.86c0 .831-.676 1.507-1.508 1.507a1.508 1.508 0 0 1 0-3.015c.832 0 1.508.675 1.508 1.507zm0 0" fill="#54b845"/><path d="M12.824 5.86c0 .831-.676 1.507-1.504 1.507a1.509 1.509 0 0 1 0-3.015c.828 0 1.504.675 1.504 1.507zm0 0" fill="#fbd303"/><path d="M7.977 5.86c0 .831-.676 1.507-1.508 1.507a1.508 1.508 0 0 1 0-3.015c.832 0 1.508.675 1.508 1.507zm0 0" fill="#f0582f"/><path d="M27.703 63.285c-.836-.668-1.672-1.336-2.34-2.176-.668-.836-1.34-1.84-2.008-2.68-1.503-2.175-2.507-4.519-3.343-6.863-1.004-2.843-1.336-5.523-1.336-8.203 0-3.015.668-5.523 1.84-7.703 1-1.672 2.34-3.18 4.011-4.183 1.672-1.004 3.512-1.508 5.348-1.676.672 0 1.34.168 2.176.336.5.168 1.168.336 2.004.668 1.004.336 1.672.672 1.84.672.667.168 1.171.335 1.503.335.336 0 .672-.167 1.172-.167.336-.168.836-.336 1.504-.672.668-.332 1.34-.5 1.84-.668l2.008-.504c.668-.168 1.504-.168 2.172-.168 1.336.168 2.508.336 3.68.84 2.003.836 3.507 2.007 4.68 3.683-.5.332-1.005.668-1.337 1.004-1.004.836-1.672 1.84-2.344 2.844-.836 1.508-1.168 3.184-1.168 4.855 0 2.012.5 3.852 1.672 5.36a7.757 7.757 0 0 0 2.84 2.847c.672.332 1.172.668 1.672.836-.164.668-.5 1.34-.836 2.008-.668 1.508-1.504 3.016-2.34 4.356-.836 1.172-1.504 2.007-2.004 2.675-.836.84-1.507 1.676-2.175 2.012-.836.5-1.84.836-2.676.836-.668 0-1.336 0-2.004-.168-.504-.168-1.172-.336-1.672-.668-.504-.336-1.172-.504-1.672-.672-.668-.168-1.504-.332-2.176-.332-.836 0-1.504.164-2.172.332s-1.171.336-1.671.672c-.836.332-1.336.5-1.672.668-.668.168-1.172.336-1.84.336-1.336.168-2.34-.168-3.176-.672zm13.04-34.992c-1.337.672-2.509.84-3.677.84-.168-1.172 0-2.512.5-3.852.504-1.172 1.004-2.176 1.84-3.18.836-1.007 1.84-1.843 3.008-2.343 1.172-.672 2.508-1.008 3.512-1.008.168 1.34 0 2.512-.5 3.852-.504 1.171-1.004 2.343-1.84 3.347-.836.84-1.84 1.676-2.844 2.344zm0 0" fill="#c6a8e5"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pm.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M64 32c0 17.645-14.355 32-32 32S0 49.645 0 32 14.355 0 32 0s32 14.355 32 32" fill="#999"/><g fill="#fff"><path d="M54.313 34.422c-1.036-6.746-8.13-11.242-13.493-14.531-2.246-1.383-6.054-3.286-6.57-6.399-.176-1.21-.176-2.594-.176-3.805V8.13c0-.692-.691.172-1.035-.344-.867-1.387-.52.344-.52 1.211.172 1.727.52 3.457.52 5.188 0 3.285-.52 6.574-1.387 9.687-1.902 7.438-3.457 15.223-1.554 22.832a24.518 24.518 0 0 0 1.554 4.668c.176.52.52 1.73 1.211 1.906 2.078.516 3.633.692 5.192 2.246 1.039.868 1.73.348 2.941 0 3.633-1.382 6.746-3.285 9.34-6.226 3.285-4.496 4.844-9.512 3.977-14.875m-3.805 6.746c-.344 2.766-2.074 5.363-3.805 7.437-1.383 1.56-3.113 3.461-5.016 4.153-.69.172.172-1.211.172-1.211.52-.867 1.383-1.73 2.075-2.594 1.039-1.21 1.902-2.598 2.421-3.98 1.903-5.016 1.56-10.899-1.382-15.395-1.555-2.422-3.805-4.496-5.88-6.398-1.038-.868-2.077-1.73-2.94-2.77-.176-.172-2.079-2.594-1.387-2.941.175-.172 4.152 3.98 4.5 4.324 1.554 1.21 3.285 2.422 4.843 3.809 2.075 1.902 4.149 3.976 5.36 6.57 1.21 2.77 1.386 6.055 1.039 8.996"/><path d="M30.79.863c.519.348.69 2.77.69 4.844 0 2.078.172 11.246-.52 13.664-.69 2.422-2.245 5.192-3.804 7.613-1.73 2.422-3.633 7.438-3.457 10.551 0 3.113 1.903 8.13 3.285 10.38 1.383 2.073 3.805 5.015 3.286 5.706-.864 1.211-4.668-2.941-6.747-5.363-1.902-2.422-3.976-7.262-3.976-11.07 0-3.805 2.074-7.262 3.633-9.34 1.554-2.075 4.496-5.707 5.36-7.438.866-1.73 1.73-3.457 1.901-5.707.348-2.25 0-10.55 0-10.55S30.27.52 30.79.862"/><path d="M29.234 4.844c.516.343.692 1.039.692 1.73 0 .692-.176 3.633-.348 6.57-.172 2.942-2.594 5.364-4.152 7.094-1.727 1.73-6.746 7.09-8.473 9.688-1.906 2.594-2.77 6.05-2.598 8.992.176 2.941.868 5.883 3.633 8.996 2.77 3.113 4.672 4.496 6.227 5.363 1.387.692 2.941 1.211 2.597 1.903-.347.691-1.73.172-3.289-.348-1.554-.52-6.746-2.594-9.687-6.055-2.938-3.457-4.496-7.957-4.324-12.105.175-4.324 1.386-6.055 3.289-8.824 1.902-2.766 7.437-6.918 9.168-7.957 1.73-1.036 3.976-2.766 5.187-4.325 1.211-1.382 1.73-2.593 1.73-4.668 0-1.902.173-3.804 0-4.5-.171-.515-.171-1.902.348-1.554m.172 51.89c.344 0 .172 1.211-.347 1.73-.52.52-1.211.864-1.383.692s.52-.343 1.039-.863c.52-.691.344-1.559.691-1.559m5.36-.172c-.344 0-.172 1.211.347 1.731s1.211.863 1.383.691c.176-.172-.52-.347-1.035-.867-.52-.515-.348-1.554-.695-1.554m-2.418 1.382c0 1.04 0 1.903-.176 1.903-.344 0-.172-.864-.172-1.903 0-1.039-.172-1.902.172-1.902.348 0 .176.863.176 1.902"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/png.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/po.svg
1
1
<svg height="64" width="80" xmlns="http://www.w3.org/2000/svg"><path d="M38.027 37.414c-5.011-4.812-9.425-9.223-12.03-19.25H43.64v-7.219H26.195V1.121h-7.617v10.024H.93v7.421h18.047s-.2 1.403-.399 2.606C15.968 30.996 13.164 37.215.93 43.23l2.61 7.418c11.429-6.015 17.444-13.835 20.05-22.257 2.605 6.418 6.816 11.629 11.629 16.441zM61.29 13.352H51.262L33.617 62.879h7.617l5.016-14.836H66.3l5.013 14.836h7.62zm-12.434 27.27 7.622-19.65 7.617 19.852zm0 0" fill="#a87c2d" stroke="#a87c2d" stroke-miterlimit="10" stroke-width="1.5039150000000001"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pom.svg
1
1
<svg height="63" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M55.809.504S31.965.832 16.73 19.992c-.828 1.145-1.656 2.13-2.484 3.274-.496.656-.996 7.207-.996 7.207s-.66-.493-.992-1.309c-.496-.984-.664-2.129-.664-2.129C2.984 40.953 5.469 48.16 5.469 48.16c-.664 1.637-1.989 2.617-3.809 6.387C-.16 58.313.004 61.914.004 61.914c0 .656.164.82.496.164 0 0 1.988-3.766 3.477-6.223.996-1.636 3.976-5.402 3.976-5.402s4.969.16 10.93-1.312c-.496-.164-2.153-.657-3.313-1.145-1.16-.492-1.82-1.312-1.82-1.312l21.36-4.91c2.98-1.801 5.628-3.93 7.785-6.551 11.257-13.266 14.074-33.57 14.074-33.57.164-.657-.332-1.15-1.16-1.15zM34.613 21.629s-9.937 8.68-14.902 13.266C14.742 39.48 8.117 50.453 8.117 50.453L5.633 48.16s1.824-4.422 9.11-13.265c7.12-8.68 19.538-14.083 19.538-14.083 1.492-.656 1.657-.328.332.817zm0 0" fill="#ef712f"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pot.svg
1
1
<svg height="64" width="80" xmlns="http://www.w3.org/2000/svg"><path d="M38.027 37.414c-5.011-4.812-9.425-9.223-12.03-19.25H43.64v-7.219H26.195V1.121h-7.617v10.024H.93v7.421h18.047s-.2 1.403-.399 2.606C15.968 30.996 13.164 37.215.93 43.23l2.61 7.418c11.429-6.015 17.444-13.835 20.05-22.257 2.605 6.418 6.816 11.629 11.629 16.441zM61.29 13.352H51.262L33.617 62.879h7.617l5.016-14.836H66.3l5.013 14.836h7.62zm-12.434 27.27 7.622-19.65 7.617 19.852zm0 0" fill="#c93" stroke="#c93" stroke-miterlimit="10" stroke-width="1.5039150000000001"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/potx.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><g fill="#f57e00" stroke="#f57e00" stroke-miterlimit="10"><path d="M53 47.7H19a3.116 3.116 0 0 0-3.101 3.1v21.8c0 1.7 1.4 3.1 3.101 3.1h14.2L31.8 81h-1.6c-.7 0-1.2.501-1.2 1.2s.5 1.2 1.2 1.2h11.5c.7 0 1.2-.501 1.2-1.2s-.5-1.2-1.2-1.2H40l-1.4-5.4H53c1.7 0 3.101-1.4 3.101-3.1V50.8c0-1.7-1.4-3.1-3.101-3.1zm.3 25.1H18.7V50.6h34.5zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/><path d="M27.6 63.9h2.8v5.6h-2.8zm4.7-1.8h2.801v7.4h-2.8zM36.9 60.2h2.8v9.302h-2.8zm4.6-1.8h2.8v11.102h-2.8zm-.2-4.098L36.4 56.999l-3.6-1.198-6.1 3.3.9.998 5.4-2.798 3.6 1.198 5.6-3.1.9 1.001 2.101-3.5-4.8.3zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pps.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><g fill="#f57e00" stroke="#f57e00" stroke-miterlimit="10"><path d="M53 47.7H19a3.116 3.116 0 0 0-3.101 3.1v21.8c0 1.7 1.4 3.1 3.101 3.1h14.2L31.8 81h-1.6c-.7 0-1.2.501-1.2 1.2s.5 1.2 1.2 1.2h11.5c.7 0 1.2-.501 1.2-1.2s-.5-1.2-1.2-1.2H40l-1.4-5.4H53c1.7 0 3.101-1.4 3.101-3.1V50.8c0-1.7-1.4-3.1-3.101-3.1zm.3 25.1H18.7V50.6h34.5zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/><path d="M27.6 63.9h2.8v5.6h-2.8zm4.7-1.8h2.801v7.4h-2.8zM36.9 60.2h2.8v9.302h-2.8zm4.6-1.8h2.8v11.102h-2.8zm-.2-4.098L36.4 56.999l-3.6-1.198-6.1 3.3.9.998 5.4-2.798 3.6 1.198 5.6-3.1.9 1.001 2.101-3.5-4.8.3zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ppsx.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><g fill="#f57e00" stroke="#f57e00" stroke-miterlimit="10"><path d="M53 47.7H19a3.116 3.116 0 0 0-3.101 3.1v21.8c0 1.7 1.4 3.1 3.101 3.1h14.2L31.8 81h-1.6c-.7 0-1.2.501-1.2 1.2s.5 1.2 1.2 1.2h11.5c.7 0 1.2-.501 1.2-1.2s-.5-1.2-1.2-1.2H40l-1.4-5.4H53c1.7 0 3.101-1.4 3.101-3.1V50.8c0-1.7-1.4-3.1-3.101-3.1zm.3 25.1H18.7V50.6h34.5zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/><path d="M27.6 63.9h2.8v5.6h-2.8zm4.7-1.8h2.801v7.4h-2.8zM36.9 60.2h2.8v9.302h-2.8zm4.6-1.8h2.8v11.102h-2.8zm-.2-4.098L36.4 56.999l-3.6-1.198-6.1 3.3.9.998 5.4-2.798 3.6 1.198 5.6-3.1.9 1.001 2.101-3.5-4.8.3zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ppt.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><g fill="#f57e00" stroke="#f57e00" stroke-miterlimit="10"><path d="M53 47.7H19a3.116 3.116 0 0 0-3.101 3.1v21.8c0 1.7 1.4 3.1 3.101 3.1h14.2L31.8 81h-1.6c-.7 0-1.2.501-1.2 1.2s.5 1.2 1.2 1.2h11.5c.7 0 1.2-.501 1.2-1.2s-.5-1.2-1.2-1.2H40l-1.4-5.4H53c1.7 0 3.101-1.4 3.101-3.1V50.8c0-1.7-1.4-3.1-3.101-3.1zm.3 25.1H18.7V50.6h34.5zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/><path d="M27.6 63.9h2.8v5.6h-2.8zm4.7-1.8h2.801v7.4h-2.8zM36.9 60.2h2.8v9.302h-2.8zm4.6-1.8h2.8v11.102h-2.8zm-.2-4.098L36.4 56.999l-3.6-1.198-6.1 3.3.9.998 5.4-2.798 3.6 1.198 5.6-3.1.9 1.001 2.101-3.5-4.8.3zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pptm.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><g fill="#f57e00" stroke="#f57e00" stroke-miterlimit="10"><path d="M53 47.7H19a3.116 3.116 0 0 0-3.101 3.1v21.8c0 1.7 1.4 3.1 3.101 3.1h14.2L31.8 81h-1.6c-.7 0-1.2.501-1.2 1.2s.5 1.2 1.2 1.2h11.5c.7 0 1.2-.501 1.2-1.2s-.5-1.2-1.2-1.2H40l-1.4-5.4H53c1.7 0 3.101-1.4 3.101-3.1V50.8c0-1.7-1.4-3.1-3.101-3.1zm.3 25.1H18.7V50.6h34.5zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/><path d="M27.6 63.9h2.8v5.6h-2.8zm4.7-1.8h2.801v7.4h-2.8zM36.9 60.2h2.8v9.302h-2.8zm4.6-1.8h2.8v11.102h-2.8zm-.2-4.098L36.4 56.999l-3.6-1.198-6.1 3.3.9.998 5.4-2.798 3.6 1.198 5.6-3.1.9 1.001 2.101-3.5-4.8.3zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pptx.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><g fill="#f57e00" stroke="#f57e00" stroke-miterlimit="10"><path d="M53 47.7H19a3.116 3.116 0 0 0-3.101 3.1v21.8c0 1.7 1.4 3.1 3.101 3.1h14.2L31.8 81h-1.6c-.7 0-1.2.501-1.2 1.2s.5 1.2 1.2 1.2h11.5c.7 0 1.2-.501 1.2-1.2s-.5-1.2-1.2-1.2H40l-1.4-5.4H53c1.7 0 3.101-1.4 3.101-3.1V50.8c0-1.7-1.4-3.1-3.101-3.1zm.3 25.1H18.7V50.6h34.5zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/><path d="M27.6 63.9h2.8v5.6h-2.8zm4.7-1.8h2.801v7.4h-2.8zM36.9 60.2h2.8v9.302h-2.8zm4.6-1.8h2.8v11.102h-2.8zm-.2-4.098L36.4 56.999l-3.6-1.198-6.1 3.3.9.998 5.4-2.798 3.6 1.198 5.6-3.1.9 1.001 2.101-3.5-4.8.3zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/prop.svg
1
1
<svg height="64" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M10.867 48.152c0 1.551 1.04 2.582 2.602 2.582h2.773v9.305c0 2.066 1.91 3.961 3.992 3.961s3.989-1.895 3.989-3.96v-9.306h5.379v9.305c0 2.066 1.91 3.961 3.992 3.961s3.988-1.895 3.988-3.96v-9.306h2.602c1.562 0 2.605-1.03 2.605-2.757V21.449H10.867zM4.097 21.45C2.017 21.45.11 23.344.11 25.41v18.606c0 2.066 1.907 3.96 3.989 3.96s3.992-1.894 3.992-3.96V25.41c0-2.066-1.735-3.96-3.992-3.96zm45.805 0c-2.082 0-3.992 1.895-3.992 3.961v18.606c0 2.066 1.91 3.96 3.992 3.96s3.989-1.894 3.989-3.96V25.41c0-2.066-1.907-3.96-3.989-3.96zM36.367 5.945l3.473-3.449c.52-.516.52-1.375 0-1.894a1.373 1.373 0 0 0-1.91 0L33.94 4.566c-1.91-1.379-4.34-1.894-6.941-1.894-2.777 0-5.031.515-7.285 1.55L15.898.259c-.523-.344-1.562-.344-2.082 0-.347.515-.347 1.55 0 2.066l3.47 3.446c-3.817 2.93-6.419 7.41-6.419 12.75h32.266c0-5.168-2.602-9.82-6.766-12.575zm-14.746 7.407h-2.773v-2.586h2.773zm13.531 0H32.38v-2.586h2.773zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ps.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M16.223 21.805.09 55.844l3.012 3.015 20.035-20.035c-.711-1.594-.532-3.543.886-4.96 1.774-1.774 4.43-1.774 6.204 0 1.773 1.769 1.773 4.429 0 6.202-1.243 1.243-3.368 1.594-4.965.887L5.23 60.984 8.242 64l34.04-16.133L49.73 27.48 36.61 14.36zm46.625-4.075L46.184 1.062c-1.418-1.417-3.547-1.417-4.965 0L37.32 4.966c-1.422 1.418-1.422 3.543 0 4.965l16.664 16.664c1.418 1.418 3.543 1.418 4.965 0l3.899-3.903c1.418-1.418 1.418-3.543 0-4.96zm0 0" fill="#fea500"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ps1.svg
1
1
<svg height="63" width="51" xmlns="http://www.w3.org/2000/svg"><g fill="#737373"><path d="M28.21 1.082 6.696 13.367v24.414l21.516 12.285 21.516-12.285V13.367zm0 0"/><path d="m6.695 13.367 21.516 12.285 21.516-12.285L28.21 1.082zM6.695 37.781l21.516 12.285 21.516-12.285L28.21 25.652zm0 0"/></g><path d="M6.055 39.04 27.57 51.167c.16.156.48.156.64.156.157 0 .477-.156.638-.156l21.515-12.285c.477-.156.637-.63.637-1.102V13.367c0-.472-.16-.785-.637-1.101L28.848.136c-.48-.156-.797-.156-1.278 0L6.055 12.423c-.477.16-.637.633-.637 1.101v24.57c0 .16.16.634.637.946zM28.21 2.5l18.965 10.867L28.21 24.395 9.242 13.367zm2.547 23.152 17.691-10.078v21.574L29.484 48.02V26.44M7.97 35.574v-20L25.66 25.652l1.274.79V48.02L7.969 37.148" fill="#fff"/><path d="M11.953 35.102.637 41.559v12.757l11.316 6.457 11.156-6.457V41.56zm0 0" fill="#fff"/><path d="m.637 41.559 11.316 6.3 11.156-6.3-11.156-6.457zm0 12.757 11.316 6.457 11.156-6.457-11.156-6.457zm0 0" fill="#fff"/><path d="m.32 54.95 11.313 6.456c.16.157.16.157.32.157s.16 0 .32-.157l11.313-6.457c.16-.16.32-.316.32-.472V41.559c0-.157-.16-.473-.32-.473l-11.313-6.457a.496.496 0 0 0-.64 0L.32 41.086c-.16.16-.32.316-.32.473v12.757c0 .16.16.473.32.633zm11.633-19.06 9.883 5.669-9.883 5.828-9.883-5.828zm1.274 11.97 9.246-5.196v11.34l-9.883 5.512v-11.34M1.434 53.215v-10.55l9.246 5.194.636.473v11.34l-9.882-5.668" fill="#444"/><path d="m24.383 45.656-7.328 4.094v8.348l7.328 4.254 7.332-4.254V49.75zm0 0" fill="#517889"/><path d="m17.055 49.75 7.328 4.094 7.332-4.094-7.332-4.094zm0 8.348 7.328 4.254 7.332-4.254-7.332-4.254zm0 0" fill="#517889"/><path d="m16.895 58.57 7.332 4.254c.156 0 .156.156.156.156s.16 0 .16-.156l7.332-4.254c.16-.156.16-.156.16-.472V49.75c0-.156-.16-.316-.16-.473l-7.332-4.254c-.16-.156-.316-.156-.477 0l-7.332 4.254c-.16.157-.16.157-.16.473v8.348c.16.156.16.316.32.472zm7.488-12.441 6.535 3.621-6.535 3.621-6.531-3.777zm.957 7.715 5.898-3.465v7.402l-6.535 3.625V54.16m-7.172 3.149v-6.77l5.899 3.465.476.156v7.246l-6.535-3.625" fill="#fff"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/psd.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><path d="M7.225 18.125c-1 0-2-.1-2.8-.3v12.1h-3.8v-27.7c1.9-.9 4.5-1.6 7.2-1.6 6.8 0 9.9 3.6 9.9 8.7 0 4.9-3.2 8.8-10.5 8.8zm.6-15.2c-1.3 0-2.5.3-3.4.6v12.1c.8.1 1.8.2 2.8.2 5 0 6.8-2.9 6.8-6.5 0-3.9-1.8-6.4-6.2-6.4zm19.5 27.5c-2.2 0-4.7-.6-6.1-1.6l1-2.4c1.3.8 3.1 1.3 4.7 1.3 2.6 0 4.4-1.7 4.4-3.9 0-5.5-9.6-3.3-9.6-10.8 0-3.5 2.8-6.2 7-6.2 2.2 0 4.2.5 5.7 1.6l-1 2.2c-1.2-.8-2.7-1.3-4.2-1.3-2.6 0-3.8 1.6-3.8 3.6 0 5.2 9.7 3.1 9.7 10.8-.1 3.6-3.2 6.7-7.8 6.7zm0 0" fill="#03c" stroke="#03c" stroke-miterlimit="10" stroke-width="1.25" transform="matrix(2.05225 0 0 2.0612 .316 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/psp.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pst.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><g fill="#1d6fb5"><path d="M18.754 25.742c-2.668.36-4.8 3.219-4.8 6.258s2.132 6.078 4.8 6.258c2.668.355 4.805-2.504 4.805-6.258s-2.137-6.613-4.805-6.258zm0 0"/><path d="M.074 7.508v49.52L38.504 64V0zm18.68 34.683c-4.27-.539-7.649-5.187-7.649-10.191 0-5.184 3.38-9.652 7.649-10.191 4.27-.536 7.652 4.113 7.652 10.191 0 6.258-3.383 10.727-7.652 10.191zm50.172-27.175L47.754 32.715l-5.691-4.649v-14.66h26.863zm0 0"/><path d="m68.926 18.414-21.172 17.7-5.691-4.65V51.31h26.863zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pub.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#077265"><path d="M23.91 25.406c0-4.746-2.285-7.031-5.976-7.031-1.407 0-2.461.176-3.164.527V32.97c.703.351 1.582.527 2.636.527 4.219-.351 6.504-2.992 6.504-8.09zm0 0"/><path d="m0 53.715 38.68 9.844V.44L0 10.285zm11.605-38.508c1.582-.527 3.692-.703 6.153-.703 3.34 0 5.625 1.055 7.035 2.992 1.406 1.758 2.285 4.219 2.285 7.559s-.703 5.976-1.933 7.738c-1.762 2.637-4.575 4.043-7.739 4.043-1.054 0-1.933 0-2.636-.352v14.418h-3.34V15.207zm30.418-6.855v3.867h13.008V23.12H42.023v4.043h13.008v4.746H42.023v3.871h13.008v4.922H42.023v4.219h13.008v4.926H42.023v8.086H64V8.352zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/py.svg
1
1
<svg height="63" width="62" xmlns="http://www.w3.org/2000/svg"><path d="M29.816 62.316c-7.492-.168-12.261-2.523-13.453-6.398l-.172-.504V47.84c0-8.758 0-8.926.512-10.27.852-2.527 2.895-4.21 5.961-4.882l.68-.168h8.687c6.301 0 8.852 0 9.196-.168 1.87-.34 2.894-.676 3.914-1.516 1.363-1.012 2.386-2.695 2.726-4.379.34-1.348.34-1.18.34-5.89v-4.212h6.129l.684.168c3.234 1.012 5.617 3.872 6.64 8.586.34 1.852.34 1.852.34 6.399 0 4.379 0 4.379-.34 5.89-.34 1.348-.683 2.696-1.363 3.875-1.192 2.356-3.067 4.04-5.277 4.715-1.364.504-.172.336-12.774.504h-11.41v2.02h14.988v7.746c-.172.504-.172 1.008-.683 1.683-.508.672-1.36 1.512-1.871 2.02-2.043 1.348-5.11 2.187-9.196 2.355zm10.387-4.714c1.363-.168 2.555-1.684 2.215-3.028-.172-1.18-1.023-2.023-2.215-2.191-1.871-.168-3.406 1.347-3.066 3.031.172 1.348 1.363 2.188 2.726 2.188zM7.848 46.66c-1.703-.336-3.403-1.347-4.598-2.863C1.04 41.105-.152 36.39.016 30.668c.172-3.54.68-6.23 1.875-8.59 1.359-3.027 3.574-4.543 6.468-5.219.684-.168.684-.168 11.75-.168h11.239c.172 0 .172-.168.172-1.007v-1.012H16.535V10.8c0-4.211 0-4.211.34-5.051 1.363-2.695 5.11-4.379 11.066-4.883.512 0 1.536-.168 2.727-.168 6.64-.168 11.578 1.008 14.133 3.367l.851.84c.34.508.852 1.348 1.024 2.192l.168.504v8.082c0 7.406 0 8.078-.168 8.586-.172.671-.512 1.683-.684 2.187-1.02 1.852-2.722 3.031-4.937 3.703-1.364.336-.852.336-10.387.508-9.535 0-9.027 0-10.387.336-2.726.672-4.597 2.691-5.281 5.555-.34 1.347-.34 1.18-.34 5.89v4.38h-2.894c-2.895 0-3.746 0-3.918-.169zm16.007-36.027c1.024-.508 1.875-1.852 1.532-2.863-.34-1.012-1.02-1.852-1.871-2.188-1.532-.508-3.235.504-3.407 2.188-.168 1.347.512 2.695 1.875 3.03.168.169.508.169 1.02.169.34-.168.34-.168.851-.336zm0 0" fill="#fed142"/><path d="M7.848 46.66c-1.703-.336-3.403-1.347-4.598-2.863C1.04 41.105-.152 36.39.016 30.668c.171-3.54.68-6.23 1.875-8.59 1.359-3.027 3.574-4.543 6.468-5.219.684-.168.684-.168 11.75-.168h11.239c.172 0 .172-.168.172-1.007v-1.012H16.535V10.8c0-4.211 0-4.211.34-5.051 1.363-2.695 5.11-4.379 11.066-4.883.512 0 1.536-.168 2.727-.168 6.64-.168 11.578 1.008 14.133 3.367l.851.84c.34.508.852 1.348 1.024 2.192l.168.504v8.082c0 7.406 0 8.078-.168 8.586-.172.671-.512 1.683-.684 2.187-1.02 1.852-2.722 3.031-4.937 3.703-1.364.336-.852.336-10.387.508-9.535 0-9.027 0-10.387.336-2.726.672-4.597 2.691-5.281 5.555-.34 1.347-.34 1.18-.34 5.89v4.38h-2.894c-2.895 0-3.746 0-3.918-.169zm16.007-36.027c1.024-.508 1.875-1.852 1.532-2.863-.34-1.012-1.02-1.852-1.871-2.188-1.532-.508-3.235.504-3.407 2.188-.168 1.347.512 2.695 1.875 3.03.168.169.508.169 1.02.169.34-.168.34-.168.851-.336zm0 0" fill="#3571a3"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/pyc.svg
1
1
<svg height="63" width="62" xmlns="http://www.w3.org/2000/svg"><g fill="#666"><path d="M29.816 62.316c-7.492-.168-12.261-2.523-13.453-6.398l-.172-.504V47.84c0-8.758 0-8.926.512-10.27.852-2.527 2.895-4.21 5.961-4.882l.68-.168h8.687c6.301 0 8.852 0 9.196-.168 1.87-.34 2.894-.676 3.914-1.516 1.363-1.012 2.386-2.695 2.726-4.379.34-1.348.34-1.18.34-5.89v-4.212h6.129l.684.168c3.234 1.012 5.617 3.872 6.64 8.586.34 1.852.34 1.852.34 6.399 0 4.379 0 4.379-.34 5.89-.34 1.348-.683 2.696-1.363 3.875-1.192 2.356-3.067 4.04-5.277 4.715-1.364.504-.172.336-12.774.504h-11.41v2.02h14.988v7.746c-.172.504-.172 1.008-.683 1.683-.508.672-1.36 1.512-1.871 2.02-2.043 1.348-5.11 2.187-9.196 2.355zm10.387-4.714c1.363-.168 2.555-1.684 2.215-3.028-.172-1.18-1.023-2.023-2.215-2.191-1.871-.168-3.406 1.347-3.066 3.031.172 1.348 1.363 2.188 2.726 2.188zM7.848 46.66c-1.703-.336-3.403-1.347-4.598-2.863C1.04 41.105-.152 36.39.016 30.668c.172-3.54.68-6.23 1.875-8.59 1.359-3.027 3.574-4.543 6.468-5.219.684-.168.684-.168 11.75-.168h11.239c.172 0 .172-.168.172-1.007v-1.012H16.535V10.8c0-4.211 0-4.211.34-5.051 1.363-2.695 5.11-4.379 11.066-4.883.512 0 1.536-.168 2.727-.168 6.64-.168 11.578 1.008 14.133 3.367l.851.84c.34.508.852 1.348 1.024 2.192l.168.504v8.082c0 7.406 0 8.078-.168 8.586-.172.671-.512 1.683-.684 2.187-1.02 1.852-2.722 3.031-4.937 3.703-1.364.336-.852.336-10.387.508-9.535 0-9.027 0-10.387.336-2.726.672-4.597 2.691-5.281 5.555-.34 1.347-.34 1.18-.34 5.89v4.38h-2.894c-2.895 0-3.746 0-3.918-.169zm16.007-36.027c1.024-.508 1.875-1.852 1.532-2.863-.34-1.012-1.02-1.852-1.871-2.188-1.532-.508-3.235.504-3.407 2.188-.168 1.347.512 2.695 1.875 3.03.168.169.508.169 1.02.169.34-.168.34-.168.851-.336zm0 0"/><path d="M7.848 46.66c-1.703-.336-3.403-1.347-4.598-2.863C1.04 41.105-.152 36.39.016 30.668c.171-3.54.68-6.23 1.875-8.59 1.359-3.027 3.574-4.543 6.468-5.219.684-.168.684-.168 11.75-.168h11.239c.172 0 .172-.168.172-1.007v-1.012H16.535V10.8c0-4.211 0-4.211.34-5.051 1.363-2.695 5.11-4.379 11.066-4.883.512 0 1.536-.168 2.727-.168 6.64-.168 11.578 1.008 14.133 3.367l.851.84c.34.508.852 1.348 1.024 2.192l.168.504v8.082c0 7.406 0 8.078-.168 8.586-.172.671-.512 1.683-.684 2.187-1.02 1.852-2.722 3.031-4.937 3.703-1.364.336-.852.336-10.387.508-9.535 0-9.027 0-10.387.336-2.726.672-4.597 2.691-5.281 5.555-.34 1.347-.34 1.18-.34 5.89v4.38h-2.894c-2.895 0-3.746 0-3.918-.169zm16.007-36.027c1.024-.508 1.875-1.852 1.532-2.863-.34-1.012-1.02-1.852-1.871-2.188-1.532-.508-3.235.504-3.407 2.188-.168 1.347.512 2.695 1.875 3.03.168.169.508.169 1.02.169.34-.168.34-.168.851-.336zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/qt.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/r.svg
1
1
<svg height="64" width="83" xmlns="http://www.w3.org/2000/svg"><linearGradient id="a" gradientTransform="matrix(.14059 0 0 .09419 -.088 -.345)" gradientUnits="userSpaceOnUse" x1=".741" x2="590.86" y1="3.666" y2="593.79"><stop offset="0" stop-color="#cbced0"/><stop offset="1" stop-color="#84838b"/></linearGradient><linearGradient id="b" gradientTransform="matrix(.1139 0 0 .11627 -.088 -.345)" gradientUnits="userSpaceOnUse" x1="301.03" x2="703.07" y1="151.4" y2="553.44"><stop offset="0" stop-color="#276dc3"/><stop offset="1" stop-color="#165caa"/></linearGradient><g fill-rule="evenodd"><path d="M41.5 55.586C18.59 55.586.016 43.14.016 27.793.016 12.441 18.59 0 41.5 0s41.484 12.441 41.484 27.793c0 15.348-18.574 27.793-41.484 27.793zm6.352-44.719c-17.414 0-31.536 8.504-31.536 19 0 10.492 14.122 19 31.536 19s30.265-5.816 30.265-19c0-13.18-12.851-19-30.265-19zm0 0" fill="url(#a)"/><path d="M63.195 43.047s2.508.758 3.97 1.496c.503.254 1.382.766 2.01 1.437.622.657.923 1.325.923 1.325l9.894 16.687L64 64l-7.48-14.047s-1.532-2.633-2.473-3.398c-.785-.637-1.121-.864-1.899-.864h-3.8l.004 18.297-14.153.008V17.258h28.418s12.946.23 12.946 12.55-12.368 13.235-12.368 13.235zM57.04 27.395l-8.566-.004-.008 7.945 8.574-.004s3.969-.012 3.969-4.039c0-4.113-3.969-3.898-3.969-3.898zm0 0" fill="url(#b)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ra.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ram.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="54"><path style="fill-rule:nonzero;fill:#039;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#039;stroke-opacity:1;stroke-miterlimit:10" d="m10.45 23.48-7.7-8.5v-.1l7.7-8.5V.68L.25 12.681v4.5l10.2 11.898zM36.25.679V6.38l7.9 8.5v.1l-7.9 8.5v5.6l10.2-11.7v-4.9zM28.549 5.08c-1.299-.6-3-1.6-3.598-3 0-.7-.602-1.3-1.301-1.3-.7 0-1.299.6-1.299 1.3v17.3c-1.3-.7-3.9-.5-5.9.6-3.002 1.7-4.3 4.899-3.002 7.2 1.301 2.2 5.002 2.7 8 1 2.1-1.2 3.502-3.2 3.601-5.1v-15c4.4 0 7 1.8 6 6-.199.8-.401 1.6-.8 2.299-.2.302-.2.5.1.802.2.198.6.198 1-.2 1.7-1.6 2.7-3.7 2.7-6.001 0-2.9-2.9-4.8-5.501-5.9zm0 0" transform="matrix(1.79872 0 0 1.81157 0 .047)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rar.svg
1
1
<svg height="63" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M53.344 18.172H44.02V8.965zM28.309 8.965v33.437h25.199V20.434H41.727V8.964zm3.93-8.723H4.417v6.461h10.965l-6.875 5.332v5.652l10.148-7.753V6.867H54V4.281zM18.655 14.297 8.508 22.05v5.652l10.148-7.754zM8.344 37.559l10.148-7.754v-5.657L8.344 31.902zm10.312 2.261v-5.656L8.508 41.918v2.91h-4.09v6.461h6.219v4.523H7.035c-.652-1.132-1.797-1.937-3.273-1.937C1.637 53.875 0 55.488 0 57.59c0 2.097 1.637 3.715 3.762 3.715 1.476 0 2.62-.809 3.273-1.938h3.602v3.39h5.562v-3.39h3.602c.652 1.13 1.8 1.938 3.273 1.938 2.125 0 3.762-1.618 3.762-3.715 0-2.102-1.637-3.715-3.762-3.715-1.472 0-2.62.805-3.273 1.938h-3.602v-4.524h15.875l21.762-3.879v-2.582H11.78zm0 0" fill="#90c"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/raw.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rb.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M61.988 2.012v59.976L46.996 17.004zM2.012 61.988h59.976L17.004 46.996zm14.992-14.992 44.984 14.992L32 32zM32 32l29.988 29.988-14.992-44.984zM2.012 46.996v14.992l14.992-14.992zM32 32H17.004v14.996zm14.996-14.996H32V32zM61.988 2.012H46.996v14.992zM17.004 32 2.012 46.996h14.992zM32 17.004 17.004 32H32zM46.996 2.012 32 17.004h14.996zm0 0" fill="#992315" stroke="#fff" stroke-width="1.66605"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rdf.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="54"><path style="fill-rule:nonzero;fill:#999;fill-opacity:1;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#999;stroke-opacity:1;stroke-miterlimit:10" d="M11.242 25.867c-.5 0-1.1-.2-1.498-.6l-8.4-8.401c-.802-.799-.802-2.099 0-3l8.4-8.398c.8-.8 2.098-.8 2.999 0 .8.8.8 2.098 0 2.999l-6.9 6.9 6.9 6.9c.8.801.8 2.1 0 3-.401.4-1 .6-1.5.6zm25.1 0c-.499 0-1.099-.2-1.5-.6-.8-.8-.8-2.099 0-3.002l6.9-6.898-6.9-6.9c-.8-.8-.8-2.101 0-3 .8-.8 2.1-.8 3.001 0l8.398 8.4c.802.8.802 2.1 0 3l-8.398 8.4c-.4.4-1.001.6-1.5.6zm-16.7 4.1c-.199 0-.398 0-.698-.1-1.102-.401-1.702-1.5-1.301-2.6l8.398-25.1c.4-1.1 1.5-1.699 2.6-1.301 1.102.4 1.702 1.5 1.301 2.601l-8.398 25.1c-.202.899-1.102 1.4-1.901 1.4zm0 0" transform="matrix(1.74425 0 0 1.75713 0 .013)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rdl.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="54"><path style="fill-rule:nonzero;fill:#999;fill-opacity:1;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#999;stroke-opacity:1;stroke-miterlimit:10" d="M11.242 25.867c-.5 0-1.1-.2-1.498-.6l-8.4-8.401c-.802-.799-.802-2.099 0-3l8.4-8.398c.8-.8 2.098-.8 2.999 0 .8.8.8 2.098 0 2.999l-6.9 6.9 6.9 6.9c.8.801.8 2.1 0 3-.401.4-1 .6-1.5.6zm25.1 0c-.499 0-1.099-.2-1.5-.6-.8-.8-.8-2.099 0-3.002l6.9-6.898-6.9-6.9c-.8-.8-.8-2.101 0-3 .8-.8 2.1-.8 3.001 0l8.398 8.4c.802.8.802 2.1 0 3l-8.398 8.4c-.4.4-1.001.6-1.5.6zm-16.7 4.1c-.199 0-.398 0-.698-.1-1.102-.401-1.702-1.5-1.301-2.6l8.398-25.1c.4-1.1 1.5-1.699 2.6-1.301 1.102.4 1.702 1.5 1.301 2.601l-8.398 25.1c-.202.899-1.102 1.4-1.901 1.4zm0 0" transform="matrix(1.74425 0 0 1.75713 0 .013)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/reg.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M0 56.355v-7.644h15.29V64H0zm22.754 0v-7.644h15.289V64h-15.29zm22.933 0v-7.644h15.29V64h-15.29zM48 38.934l-5.332-5.332L48 28.266l5.332-5.332 5.336 5.332L64 33.602l-5.332 5.332a125.106 125.106 0 0 1-5.336 5.332zM0 33.602v-7.645h15.29v15.29H0zm22.754 0v-7.645h15.289v15.29h-15.29zM25.066 16l-5.332-5.332 5.332-5.336L30.398 0l5.336 5.332 5.332 5.336L35.734 16c-2.843 3.023-5.336 5.332-5.336 5.332zM0 10.668V3.023h15.29v15.29H0zm0 0" fill="#3a898d"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/resx.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#d5006e"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/retry.svg
1
1
<svg height="64" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M54 36.781c0 14.856-12.04 26.813-27 26.813S0 51.637 0 36.78c0-5.12 1.547-9.906 4.129-14.004l8.77 4.953a16.11 16.11 0 0 0-2.75 9.051c0 9.223 7.566 16.735 16.851 16.735s16.852-7.512 16.852-16.735c0-7.683-5.329-14.172-12.551-16.222v8.539L6.19 14.754 31.301.406v9.906C44.199 12.364 54 23.462 54 36.782zm0 0" fill="#bababa"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rm.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rmd.svg
1
1
<svg height="64" width="76" xmlns="http://www.w3.org/2000/svg"><linearGradient id="a" gradientTransform="matrix(.08235 0 0 .05573 -.06 .108)" gradientUnits="userSpaceOnUse" x1=".741" x2="590.86" y1="3.666" y2="593.79"><stop offset="0" stop-color="#cbced0"/><stop offset="1" stop-color="#84838b"/></linearGradient><linearGradient id="b" gradientTransform="matrix(.06671 0 0 .0688 -.06 .108)" gradientUnits="userSpaceOnUse" x1="301.03" x2="703.07" y1="151.4" y2="553.44"><stop offset="0" stop-color="#276dc3"/><stop offset="1" stop-color="#165caa"/></linearGradient><path d="M27.406 63.688V41.383h6.48l6.477 8.187 6.48-8.187h6.481v22.304h-6.48V50.898l-6.48 8.184-6.477-8.184v12.79zm38.875 0-9.719-10.844h6.481V41.383h6.477v11.46H76zm0 0" fill="#999"/><g fill-rule="evenodd"><path d="M24.297 33.2C10.879 33.2 0 25.84 0 16.757S10.879.312 24.297.312c13.422 0 24.3 7.364 24.3 16.446s-10.878 16.441-24.3 16.441zm3.719-26.458c-10.2 0-18.47 5.031-18.47 11.242 0 6.207 8.27 11.243 18.47 11.243s17.73-3.442 17.73-11.243c0-7.797-7.531-11.242-17.73-11.242zm0 0" fill="url(#a)"/><path d="M37.004 25.781s1.473.45 2.324.887c.297.152.813.453 1.18.852.363.386.539.78.539.78l5.797 9.876-9.367.004-4.38-8.313s-.898-1.555-1.449-2.008c-.46-.375-.66-.511-1.113-.511H28.31l.003 10.828-8.289.004V10.523h16.645s7.582.137 7.582 7.426c0 7.29-7.246 7.832-7.246 7.832zm-3.606-9.258-5.015-.003-.004 4.703 5.02-.004s2.324-.008 2.324-2.39c0-2.434-2.325-2.306-2.325-2.306zm0 0" fill="url(#b)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rom.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M68.633.102H16.39v7.226H5.738v2.274H0v2.062h5.738v2.684h10.653V26.94H5.738v2.477H0v2.066h5.738v2.27h10.653v13.625H5.738v2.48H0v2.063h5.738v2.273h10.653v9.703h52.242v-9.703h9.629v-2.48H84v-2.063h-5.738v-2.476h-9.63v-13.63h9.63v-2.062H84v-2.066h-5.738v-2.684h-9.63V14.141h9.63v-2.684H84V9.395h-5.738V7.12h-9.63zm-10.04 17.136c-2.253 0-4.097-1.86-4.097-4.129S56.34 8.98 58.594 8.98s4.097 1.86 4.097 4.13c0 2.476-1.843 4.128-4.097 4.128zm0 0" fill="#099"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rpm.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><path d="m49.332 34.941-12.25-5.714L61.75 17.633 74 23.348l-12.25 5.879zM61.75 6.207 49.5.492 37.25 6.207l24.5 11.594L74 12.086zm-37.082 17.14-12.25-5.714-12.25 5.715L24.836 34.94l12.246-5.714zm0-11.429 12.25-5.711L24.668.492 0 12.086 12.25 17.8zM61.75 32.59l-11.074 5.039-1.344.672-1.34-.672-11.074-5.04-11.078 5.04-1.34.672-1.344-.672-11.074-5.04v17.977L36.75 63.508l25-12.942zm0 0" fill="#55486d"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rpt.svg
1
1
<svg height="63" width="47" xmlns="http://www.w3.org/2000/svg"><path d="M10.62.6c-.398 0-.8.3-.998.7l-1 1.901h-6.8c-.7 0-1.302.6-1.302 1.2-.098 10.6.2 21.299 0 31.899 0 .7.602 1.3 1.302 1.3H26.02c.7 0 1.302-.6 1.302-1.3V4.4c0-.6-.602-1.302-1.302-1.302h-6.799l-1-1.899c-.2-.4-.7-.7-1.2-.7-2.2.1-4.501.1-6.4.1zm0 0" fill="#666" stroke="#666" stroke-miterlimit="10" transform="matrix(1.67417 0 0 1.65354 .211 0)"/><path d="M5.438 9.426h7.53c0 2.148.169 4.133 2.18 4.133h17.075c2.175 0 2.175-2.149 2.175-4.133h7.536v48.449H5.605c-.168-16.207-.168-32.41-.168-48.45zm0 0" fill="#fff"/><path d="M10.793 21H36.41v4.3H10.793zm0 8.434H36.41v4.296H10.793zm0 8.433H36.41V42H10.793zm0 8.434h17.078v4.297H10.793zm0 0" fill="#666"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rsa.svg
1
1
<svg height="64" width="52" xmlns="http://www.w3.org/2000/svg"><path d="M48.793 26.879h-4.629V18.05C44.164 7.988 35.973.043 26 .043S7.836 8.164 7.836 18.051v8.828H3.207A3.181 3.181 0 0 0 0 30.059V60.78c0 1.762 1.426 3.176 3.207 3.176h45.586c1.781 0 3.207-1.414 3.207-3.176V29.883c0-1.59-1.426-3.004-3.207-3.004zM29.918 52.305c.355 1.058-.535 1.941-1.602 1.941h-4.808c-1.07 0-1.781-1.059-1.606-1.941l1.426-5.649c-1.781-.883-3.027-2.648-3.027-4.945 0-3 2.492-5.473 5.52-5.473 3.027 0 5.523 2.473 5.523 5.473 0 2.117-1.246 4.062-3.028 4.945zm5.164-25.426H16.918V18.05c0-4.942 4.098-9.004 9.082-9.004s9.082 4.062 9.082 9.004zm0 0" fill="#696"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rss.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M14.54 49.375c1.593 1.594 2.48 3.723 2.48 6.027 0 2.305-.887 4.43-2.48 6.028a8.46 8.46 0 0 1-6.032 2.48 8.451 8.451 0 0 1-6.028-2.48C.887 59.832 0 57.883 0 55.402c0-2.304.887-4.433 2.48-6.027a8.439 8.439 0 0 1 6.028-2.484c2.484 0 4.433 1.066 6.031 2.484zM.175 21.719v12.23c7.98 0 15.426 3.192 21.097 8.688 5.676 5.672 8.688 13.12 8.688 21.097h12.234c0-11.523-4.789-22.16-12.41-29.785C22.16 26.504 11.7 21.72.175 21.72zm0-21.63V12.32c28.367 0 51.59 23.227 51.59 51.59H64c0-17.55-7.09-33.504-18.793-45.027C33.684 7.18 17.73.09.176.09zm0 0" fill="#dd7d36"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rst.svg
1
1
<svg height="64" width="76" xmlns="http://www.w3.org/2000/svg"><path d="M.176 52.977h75.648V64H.176zm0-26.309h75.648v11.02H.176zM.176 0h75.648v11.023H.176zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rtf.svg
1
1
<svg height="64" width="76" xmlns="http://www.w3.org/2000/svg"><path d="M.176 52.977h75.648V64H.176zm0-26.309h75.648v11.02H.176zM.176 0h75.648v11.023H.176zm0 0" fill="#666"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ru.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M61.988 2.012v59.976L46.996 17.004zM2.012 61.988h59.976L17.004 46.996zm14.992-14.992 44.984 14.992L32 32zM32 32l29.988 29.988-14.992-44.984zM2.012 46.996v14.992l14.992-14.992zM32 32H17.004v14.996zm14.996-14.996H32V32zM61.988 2.012H46.996v14.992zM17.004 32 2.012 46.996h14.992zM32 17.004 17.004 32H32zM46.996 2.012 32 17.004h14.996zm0 0" fill="#666" stroke="#fff" stroke-width="1.66605"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rub.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M61.988 2.012v59.976L46.996 17.004zM2.012 61.988h59.976L17.004 46.996zm14.992-14.992 44.984 14.992L32 32zM32 32l29.988 29.988-14.992-44.984zM2.012 46.996v14.992l14.992-14.992zM32 32H17.004v14.996zm14.996-14.996H32V32zM61.988 2.012H46.996v14.992zM17.004 32 2.012 46.996h14.992zM32 17.004 17.004 32H32zM46.996 2.012 32 17.004h14.996zm0 0" fill="#992315" stroke="#fff" stroke-width="1.66605"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/rxml.svg
1
1
<svg height="64" width="71" xmlns="http://www.w3.org/2000/svg"><linearGradient id="a" gradientTransform="matrix(.07693 0 0 .05184 -.057 .13)" gradientUnits="userSpaceOnUse" x1=".741" x2="590.86" y1="3.666" y2="593.79"><stop offset="0" stop-color="#cbced0"/><stop offset="1" stop-color="#84838b"/></linearGradient><linearGradient id="b" gradientTransform="matrix(.06233 0 0 .064 -.057 .13)" gradientUnits="userSpaceOnUse" x1="301.03" x2="703.07" y1="151.4" y2="553.44"><stop offset="0" stop-color="#276dc3"/><stop offset="1" stop-color="#165caa"/></linearGradient><path d="M22.7 30.914C10.163 30.914 0 24.066 0 15.617 0 7.168 10.164.32 22.7.32c12.534 0 22.698 6.848 22.698 15.297 0 8.45-10.16 15.297-22.699 15.297zm3.476-24.613c-9.531 0-17.254 4.683-17.254 10.457 0 5.777 7.723 10.46 17.254 10.46 9.527 0 16.558-3.202 16.558-10.46 0-7.254-7.03-10.457-16.558-10.457zm0 0" fill="url(#a)" fill-rule="evenodd"/><path d="M34.57 24.016s1.375.414 2.172.82c.278.144.758.426 1.102.793.34.363.504.73.504.73l5.414 9.184-8.75.004-4.094-7.73s-.836-1.45-1.352-1.872c-.43-.347-.613-.472-1.039-.472H26.45v10.07l-7.742.004V9.82h15.55s7.083.13 7.083 6.907c0 6.78-6.77 7.285-6.77 7.285zm-3.367-8.618h-4.687l-.004 4.372h4.691s2.172-.008 2.172-2.227c0-2.262-2.172-2.145-2.172-2.145zm0 0" fill="url(#b)" fill-rule="evenodd"/><path d="M-24.46 120.549a1.43 1.43 0 0 1-.97-.386l-5.364-5.43c-.517-.517-.517-1.357 0-1.94l5.43-5.43c.515-.517 1.357-.517 1.938 0 .518.517.518 1.36 0 1.94l-4.46 4.461 4.46 4.46c.518.517.518 1.356 0 1.939-.322.257-.71.386-1.035.386zm16.222 0a1.43 1.43 0 0 1-.97-.386c-.517-.517-.517-1.357 0-1.94l4.461-4.459-4.46-4.461c-.518-.517-.518-1.357 0-1.94.517-.517 1.357-.517 1.94 0l5.428 5.43c.517.517.517 1.357 0 1.94l-5.427 5.43a1.432 1.432 0 0 1-.972.386zm-10.793 2.65c-.13 0-.26 0-.452-.062-.71-.26-1.098-.971-.84-1.683l5.427-16.222c.26-.711.972-1.1 1.683-.84.71.257 1.098.969.84 1.68l-5.43 16.222c-.13.583-.71.906-1.228.906zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".97" transform="matrix(1.47916 0 0 1.48836 72.435 -120.409)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sass.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="fill-rule:nonzero;fill:#cd6799;fill-opacity:1;stroke-width:.25;stroke-linecap:butt;stroke-linejoin:miter;stroke:#cd6799;stroke-opacity:1;stroke-miterlimit:10" d="M40.764 20.63c-1.602 0-3.101.4-4.3 1-.4-.9-.9-1.601-1.001-2.198-.1-.7-.2-1.001-.1-1.8.1-.804.599-1.904.599-2.003 0-.098-.1-.498-1-.498-.998 0-1.797.099-1.797.4-.101.197-.302.799-.4 1.398-.102.9-1.903 3.901-2.903 5.502-.3-.7-.6-1.201-.7-1.6-.1-.701-.2-1.002-.1-1.8.1-.802.6-1.902.6-2.001 0-.101-.1-.498-1-.498s-1.801.098-1.801.4c-.096.3-.197.798-.398 1.4-.2.597-2.5 5.598-3.1 6.999-.302.698-.6 1.2-.7 1.598v.101c-.099.301-.3.5-.3.5-.1.2-.3.4-.4.4-.101 0-.101-.599 0-1.5.4-1.8 1.199-4.5 1.199-4.6 0-.1.1-.5-.5-.8-.7-.2-.9.101-.997.101-.103 0-.103.099-.103.099s.7-3.1-1.399-3.1c-1.3 0-3.2 1.5-4.2 2.801-.6.299-1.8.997-3.2 1.697-.499.301-1 .602-1.5.802l-.1-.101c-2.598-2.8-7.5-4.8-7.3-8.6.099-1.398.5-4.999 9.3-9.3 7.202-3.598 13-2.6 13.9-.4 1.4 3.2-3.1 9.002-10.6 9.799-2.901.303-4.3-.797-4.7-1.199-.4-.4-.4-.4-.6-.4-.2.102-.098.501 0 .701.2.6 1.202 1.6 2.699 2.101 1.4.397 4.7.698 8.702-.9 4.498-1.8 8.099-6.602 6.998-10.7-1-4.102-7.9-5.5-14.4-3.202-3.898 1.401-8.098 3.5-11.1 6.4-3.6 3.4-4.099 6.2-3.9 7.502.801 4.299 6.8 7.1 9.202 9.199-.1.1-.201.1-.302.1-1.2.6-5.7 2.901-6.897 5.5-1.3 2.9.197 4.898 1.2 5.1 3.099.898 6.198-.7 7.897-3.2 1.7-2.501 1.501-5.8.702-7.3v-.101c.3-.1.7-.4.998-.499.6-.4 1.2-.7 1.7-1-.299.8-.5 1.8-.599 3.2-.098 1.6.5 3.7 1.399 4.5.4.4.8.4 1.103.4.998 0 1.497-.801 1.998-1.8.6-1.2 1.2-2.6 1.2-2.6s-.7 3.799 1.2 3.799c.7 0 1.4-.9 1.7-1.3l.1-.099.1-.1c.299-.501.9-1.5 1.8-3.401 1.2-2.3 2.3-5.298 2.3-5.298s.1.698.4 1.899c.2.7.7 1.5 1 2.2-.3.4-.401.6-.401.6-.2.3-.398.599-.698.998-1 1.1-2.101 2.402-2.2 2.802-.1.4-.1.7.197 1 .201.2.703.2 1.202.2.8-.1 1.499-.3 1.698-.4.4-.1 1-.4 1.502-.8 1-.7 1.499-1.601 1.4-2.9 0-.701-.3-1.4-.5-2.1.1-.1.1-.2.2-.4 1.5-2.1 2.6-4.5 2.6-4.5s.1.7.4 1.9c.1.6.5 1.2.8 1.9-1.4 1.1-2.202 2.4-2.5 3.2-.5 1.501-.1 2.3.699 2.399.4.1.9-.101 1.202-.299.499-.1 1-.4 1.6-.801 1-.698 1.799-1.6 1.799-2.898 0-.602-.1-1.203-.4-1.701 1.2-.501 2.602-.7 4.5-.501 4.099.5 4.9 3.001 4.8 4.101-.1 1.1-1 1.7-1.3 1.8-.3.1-.4.2-.4.4s.1.1.4.1c.4-.1 2.1-.9 2.199-2.9.201-2.2-2.2-4.9-6.4-4.9zm-31.5 10.701c-1.3 1.5-3.203 2.099-4.002 1.5-.9-.501-.499-2.7 1.101-4.301.998-1 2.3-1.8 3.2-2.299.2-.1.5-.3.799-.5.1 0 .1-.101.1-.101.101-.101.101-.101.202-.101.6 2.2 0 4.1-1.4 5.802zm9.9-6.801c-.401 1.2-1.5 4.1-2.1 4-.501-.101-.801-2.301-.1-4.5.4-1.1 1.199-2.4 1.6-2.901.7-.8 1.499-1.1 1.799-.7.2.202-.9 3.4-1.2 4.1zm8.1 3.899c-.202.1-.403.1-.403.1l.1-.1s1-1.1 1.402-1.599c.199-.298.5-.7.8-1v.1c-.1 1.199-1.4 2.1-1.9 2.5zm6.2-1.401c-.1-.099-.1-.4.399-1.5.2-.399.7-1.1 1.4-1.8.1.301.1.503.1.802 0 1.6-1.102 2.2-1.9 2.498zm0 0" transform="matrix(1.7456 0 0 1.77926 .217 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/scss.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="fill-rule:nonzero;fill:#cd6799;fill-opacity:1;stroke-width:.25;stroke-linecap:butt;stroke-linejoin:miter;stroke:#cd6799;stroke-opacity:1;stroke-miterlimit:10" d="M40.764 20.63c-1.602 0-3.101.4-4.3 1-.4-.9-.9-1.601-1.001-2.198-.1-.7-.2-1.001-.1-1.8.1-.804.599-1.904.599-2.003 0-.098-.1-.498-1-.498-.998 0-1.797.099-1.797.4-.101.197-.302.799-.4 1.398-.102.9-1.903 3.901-2.903 5.502-.3-.7-.6-1.201-.7-1.6-.1-.701-.2-1.002-.1-1.8.1-.802.6-1.902.6-2.001 0-.101-.1-.498-1-.498s-1.801.098-1.801.4c-.096.3-.197.798-.398 1.4-.2.597-2.5 5.598-3.1 6.999-.302.698-.6 1.2-.7 1.598v.101c-.099.301-.3.5-.3.5-.1.2-.3.4-.4.4-.101 0-.101-.599 0-1.5.4-1.8 1.199-4.5 1.199-4.6 0-.1.1-.5-.5-.8-.7-.2-.9.101-.997.101-.103 0-.103.099-.103.099s.7-3.1-1.399-3.1c-1.3 0-3.2 1.5-4.2 2.801-.6.299-1.8.997-3.2 1.697-.499.301-1 .602-1.5.802l-.1-.101c-2.598-2.8-7.5-4.8-7.3-8.6.099-1.398.5-4.999 9.3-9.3 7.202-3.598 13-2.6 13.9-.4 1.4 3.2-3.1 9.002-10.6 9.799-2.901.303-4.3-.797-4.7-1.199-.4-.4-.4-.4-.6-.4-.2.102-.098.501 0 .701.2.6 1.202 1.6 2.699 2.101 1.4.397 4.7.698 8.702-.9 4.498-1.8 8.099-6.602 6.998-10.7-1-4.102-7.9-5.5-14.4-3.202-3.898 1.401-8.098 3.5-11.1 6.4-3.6 3.4-4.099 6.2-3.9 7.502.801 4.299 6.8 7.1 9.202 9.199-.1.1-.201.1-.302.1-1.2.6-5.7 2.901-6.897 5.5-1.3 2.9.197 4.898 1.2 5.1 3.099.898 6.198-.7 7.897-3.2 1.7-2.501 1.501-5.8.702-7.3v-.101c.3-.1.7-.4.998-.499.6-.4 1.2-.7 1.7-1-.299.8-.5 1.8-.599 3.2-.098 1.6.5 3.7 1.399 4.5.4.4.8.4 1.103.4.998 0 1.497-.801 1.998-1.8.6-1.2 1.2-2.6 1.2-2.6s-.7 3.799 1.2 3.799c.7 0 1.4-.9 1.7-1.3l.1-.099.1-.1c.299-.501.9-1.5 1.8-3.401 1.2-2.3 2.3-5.298 2.3-5.298s.1.698.4 1.899c.2.7.7 1.5 1 2.2-.3.4-.401.6-.401.6-.2.3-.398.599-.698.998-1 1.1-2.101 2.402-2.2 2.802-.1.4-.1.7.197 1 .201.2.703.2 1.202.2.8-.1 1.499-.3 1.698-.4.4-.1 1-.4 1.502-.8 1-.7 1.499-1.601 1.4-2.9 0-.701-.3-1.4-.5-2.1.1-.1.1-.2.2-.4 1.5-2.1 2.6-4.5 2.6-4.5s.1.7.4 1.9c.1.6.5 1.2.8 1.9-1.4 1.1-2.202 2.4-2.5 3.2-.5 1.501-.1 2.3.699 2.399.4.1.9-.101 1.202-.299.499-.1 1-.4 1.6-.801 1-.698 1.799-1.6 1.799-2.898 0-.602-.1-1.203-.4-1.701 1.2-.501 2.602-.7 4.5-.501 4.099.5 4.9 3.001 4.8 4.101-.1 1.1-1 1.7-1.3 1.8-.3.1-.4.2-.4.4s.1.1.4.1c.4-.1 2.1-.9 2.199-2.9.201-2.2-2.2-4.9-6.4-4.9zm-31.5 10.701c-1.3 1.5-3.203 2.099-4.002 1.5-.9-.501-.499-2.7 1.101-4.301.998-1 2.3-1.8 3.2-2.299.2-.1.5-.3.799-.5.1 0 .1-.101.1-.101.101-.101.101-.101.202-.101.6 2.2 0 4.1-1.4 5.802zm9.9-6.801c-.401 1.2-1.5 4.1-2.1 4-.501-.101-.801-2.301-.1-4.5.4-1.1 1.199-2.4 1.6-2.901.7-.8 1.499-1.1 1.799-.7.2.202-.9 3.4-1.2 4.1zm8.1 3.899c-.202.1-.403.1-.403.1l.1-.1s1-1.1 1.402-1.599c.199-.298.5-.7.8-1v.1c-.1 1.199-1.4 2.1-1.9 2.5zm6.2-1.401c-.1-.099-.1-.4.399-1.5.2-.399.7-1.1 1.4-1.8.1.301.1.503.1.802 0 1.6-1.102 2.2-1.9 2.498zm0 0" transform="matrix(1.7456 0 0 1.77926 .217 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sdf.svg
1
1
<svg height="64" width="51" xmlns="http://www.w3.org/2000/svg"><path d="M23.023 63.957c-8.199-.34-15.543-2.875-19.468-6.77-1.196-1.011-2.39-2.535-2.903-3.55L.31 52.96v-7.617c0-7.614 0-7.614.171-6.934.34 1.692 1.368 3.383 2.903 4.735 1.023.847 3.074 2.37 4.781 3.214 2.906 1.524 6.66 2.54 10.59 3.047 2.39.34 3.246.34 6.66.34 3.418 0 4.27 0 6.66-.34 3.93-.508 7.516-1.691 10.59-3.047 1.707-.843 3.758-2.199 4.781-3.214 1.368-1.352 2.563-3.043 2.903-4.735.172-.508.172-.508.172 6.934v7.445l-.34.68c-1.196 2.367-3.246 4.398-5.98 6.09-5.294 3.046-13.321 4.738-21.177 4.398zm0-18.95c-7.171-.339-13.832-2.37-18.101-5.413-1.027-.68-2.39-2.032-2.906-2.707-.512-.68-1.024-1.524-1.364-2.371L.31 33.84v-7.445c0-7.446 0-7.446.171-6.938.34 1.184.852 2.54 1.88 3.555.511.675 1.367 1.523 1.878 1.86.168.171.684.339 1.024.679 3.414 2.367 8.199 4.058 13.664 4.906 2.39.336 3.242.336 6.66.336 3.414 0 4.27 0 6.66-.336 3.93-.508 7.516-1.691 10.59-3.047 1.707-.847 3.758-2.2 4.781-3.215 1.367-1.351 2.39-3.047 2.903-4.738.171-.508.171-.508.171 6.938v7.445l-.511 1.015c-.856 1.524-1.368 2.368-2.39 3.383-1.028 1.016-2.052 1.864-3.419 2.54-5.465 3.046-13.492 4.738-21.348 4.23zm-.511-18.78c-4.782-.34-8.54-1.184-12.125-2.54-4.27-1.69-7.344-3.89-8.883-6.597a5.594 5.594 0 0 1-.852-2.031C.48 14.383.31 12.69.48 11.676 1.504 6.262 8.848 1.859 18.754.34 21.144 0 22 0 25.414 0c3.418 0 4.27 0 6.66.34 3.93.508 7.516 1.691 10.59 3.043 4.441 2.199 7.172 5.078 7.684 8.12.172.849.172 2.708-.168 3.388-.512 1.691-1.196 2.707-2.563 4.058-3.586 3.723-9.906 6.094-17.762 6.938-1.023.34-6.32.34-7.343.34zm0 0" fill="#a03537"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sed.svg
1
1
<svg height="64" width="79" xmlns="http://www.w3.org/2000/svg"><path d="m42.852 35.445 4.695 1.344 2.851-10.418-4.695-1.344c0-1.68-.168-3.359-.672-5.039l4.192-2.52-5.364-9.405-4.359 2.519a18.036 18.036 0 0 0-4.023-3.023l1.34-4.704L26.421 0l-1.34 4.703c-1.676 0-3.352.168-5.027.672l-2.516-4.2-9.387 5.376 2.512 4.199a18.053 18.053 0 0 0-3.016 4.031l-4.695-1.343L.105 23.852l4.692 1.343c0 1.68.168 3.36.672 5.04l-4.192 2.523 5.364 9.406 4.191-2.52a18.126 18.126 0 0 0 4.023 3.024l-1.34 4.703 10.395 2.856 1.34-4.704c1.676 0 3.352-.168 5.031-.671l2.512 4.199 9.39-5.375-2.515-4.2c1.172-1.175 2.348-2.519 3.184-4.03zm-25.985-5.547c-2.68-4.535-1.004-10.414 3.52-13.101 4.527-2.688 10.394-1.008 13.078 3.527 2.683 4.535 1.004 10.414-3.52 13.106-4.527 2.687-10.394 1.175-13.078-3.532zm50.63 33.262 6.034-3.527-1.676-2.856c.84-.84 1.508-1.68 2.012-2.687l3.184.84 1.844-6.887-3.184-.84c0-1.176-.168-2.183-.504-3.36l2.852-1.679-3.52-6.047-2.852 1.68c-.84-.84-1.675-1.512-2.683-2.016l.84-3.191-6.875-1.852-.836 3.196c-1.176 0-2.18.168-3.356.504l-1.675-2.86-5.7 3.7 1.676 2.855c-.836.84-1.508 1.68-2.012 2.687l-3.183-1.007-1.844 6.886 3.184.84c0 1.176.168 2.184.504 3.36l-2.852 1.68 3.523 6.046 2.848-1.68c.84.84 1.676 1.512 2.684 2.016l-.84 3.191L61.965 64l.836-3.191c1.176 0 2.18-.168 3.355-.504-.336 0 1.34 2.855 1.34 2.855zM57.101 50.563c-1.676-3.024-.668-6.887 2.347-8.567 3.02-1.68 6.875-.672 8.551 2.352 1.676 3.023.672 6.886-2.348 8.566-3.015 1.68-6.875.672-8.55-2.352zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sh.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M13.875 13.874h10.9v2.701h-10.9zm0 5.4h10.9v2.701h-10.9zm0 5.5h10.9v2.702h-10.9zm19-24.399H11.177c-3 0-5.402 2.4-5.402 5.4v24.4h-5.4c0 3 2.402 5.4 5.4 5.4h21.7c3 0 5.402-2.4 5.402-5.4v-21.7h5.4v-2.7c0-3-2.402-5.4-5.4-5.4zm-2.7 29.3c0 1.801-1.4 3.2-3.2 3.2h-19.9c1.3-.9 1.3-2.7 1.3-2.7v-24.4c0-1.5 1.2-2.7 2.7-2.7 1.501 0 2.7 1.2 2.7 2.7v2.7h16.3zm-13.6-23.9v-2.7h16.3c2.501 0 2.7 1.6 2.7 2.7zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".75" transform="matrix(1.7717 0 0 1.78025 .262 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sit.svg
1
1
<svg height="63" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M53.344 18.172H44.02V8.965zM28.309 8.965v33.437h25.199V20.434H41.727V8.964zm3.93-8.723H4.417v6.461h10.965l-6.875 5.332v5.652l10.148-7.753V6.867H54V4.281zM18.655 14.297 8.508 22.05v5.652l10.148-7.754zM8.344 37.559l10.148-7.754v-5.657L8.344 31.902zm10.312 2.261v-5.656L8.508 41.918v2.91h-4.09v6.461h6.219v4.523H7.035c-.652-1.132-1.797-1.937-3.273-1.937C1.637 53.875 0 55.488 0 57.59c0 2.097 1.637 3.715 3.762 3.715 1.476 0 2.62-.809 3.273-1.938h3.602v3.39h5.562v-3.39h3.602c.652 1.13 1.8 1.938 3.273 1.938 2.125 0 3.762-1.618 3.762-3.715 0-2.102-1.637-3.715-3.762-3.715-1.472 0-2.62.805-3.273 1.938h-3.602v-4.524h15.875l21.762-3.879v-2.582H11.78zm0 0" fill="#90c"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sitemap.svg
1
1
<svg height="63" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M56.645 44.492c-.348-.512-.864-.68-1.38-.68H38.728c-.516 1.188-1.204 2.376-1.723 3.567-1.375 2.715-2.926 5.773-4.305 8.32v.168c-.863 1.528-2.41 2.38-4.133 2.38s-3.273-.852-4.136-2.38c-.516-.847-2.239-4.074-4.477-8.32-.691-1.188-1.207-2.379-1.723-3.567H9.27c-.516 0-1.204.34-1.375.852L.14 60.793a2.327 2.327 0 0 0 0 1.527c.343.512.863.68 1.379.68h46.34c.515 0 1.203-.34 1.374-.848l7.754-15.965c0-.68 0-1.187-.343-1.695zm0 0" fill="#039"/><path d="M28.39 0c-9.472 0-17.222 7.64-17.222 16.98 0 5.606 6.2 18.852 11.367 29.04 2.41 4.753 4.309 8.32 4.48 8.32.344.508.86.847 1.376.847.52 0 1.035-.34 1.379-.847 0 0 1.894-3.567 4.48-8.32 5.168-10.188 11.367-23.434 11.367-29.04C45.617 7.64 37.867 0 28.391 0zm0 27.168c-4.304 0-7.921-3.223-8.265-7.469v-.851c0-4.582 3.79-8.32 8.441-8.32 4.305 0 7.922 3.226 8.266 7.472v.848c0 4.586-3.789 8.32-8.441 8.32zm0 0" fill="#efce4a"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/skin.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#d5006e"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sldm.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><g fill="#c60" stroke="#c60" stroke-miterlimit="10"><path d="M53 47.7H19a3.116 3.116 0 0 0-3.101 3.1v21.8c0 1.7 1.4 3.1 3.101 3.1h14.2L31.8 81h-1.6c-.7 0-1.2.501-1.2 1.2s.5 1.2 1.2 1.2h11.5c.7 0 1.2-.501 1.2-1.2s-.5-1.2-1.2-1.2H40l-1.4-5.4H53c1.7 0 3.101-1.4 3.101-3.1V50.8c0-1.7-1.4-3.1-3.101-3.1zm.3 25.1H18.7V50.6h34.5zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/><path d="M27.6 63.9h2.8v5.6h-2.8zm4.7-1.8h2.801v7.4h-2.8zM36.9 60.2h2.8v9.302h-2.8zm4.6-1.8h2.8v11.102h-2.8zm-.2-4.098L36.4 56.999l-3.6-1.198-6.1 3.3.9.998 5.4-2.798 3.6 1.198 5.6-3.1.9 1.001 2.101-3.5-4.8.3zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sldx.svg
1
1
<svg height="64" width="70" xmlns="http://www.w3.org/2000/svg"><g fill="#c60" stroke="#c60" stroke-miterlimit="10"><path d="M53 47.7H19a3.116 3.116 0 0 0-3.101 3.1v21.8c0 1.7 1.4 3.1 3.101 3.1h14.2L31.8 81h-1.6c-.7 0-1.2.501-1.2 1.2s.5 1.2 1.2 1.2h11.5c.7 0 1.2-.501 1.2-1.2s-.5-1.2-1.2-1.2H40l-1.4-5.4H53c1.7 0 3.101-1.4 3.101-3.1V50.8c0-1.7-1.4-3.1-3.101-3.1zm.3 25.1H18.7V50.6h34.5zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/><path d="M27.6 63.9h2.8v5.6h-2.8zm4.7-1.8h2.801v7.4h-2.8zM36.9 60.2h2.8v9.302h-2.8zm4.6-1.8h2.8v11.102h-2.8zm-.2-4.098L36.4 56.999l-3.6-1.198-6.1 3.3.9.998 5.4-2.798 3.6 1.198 5.6-3.1.9 1.001 2.101-3.5-4.8.3zm0 0" transform="matrix(1.69903 0 0 1.72133 -26.165 -80.833)"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sln.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#d5006e"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sol.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M30.871 11.793h2.602c.867 0 1.39-.691 1.39-1.387v-9.02C34.863.52 34.168 0 33.473 0H30.87c-.867 0-1.387.695-1.387 1.387v8.847c-.172.868.52 1.559 1.387 1.559zm21.336 18.906v2.602c0 .867.691 1.386 1.387 1.386h9.02c.866 0 1.386-.69 1.386-1.386v-2.602c0-.867-.695-1.387-1.387-1.387h-9.02c-.695-.175-1.386.52-1.386 1.387zM33.301 64c.867 0 1.386-.695 1.386-1.387v-9.02c0-.866-.69-1.386-1.386-1.386h-2.602c-.867 0-1.387.691-1.387 1.387v9.02c0 .866.692 1.386 1.387 1.386zM1.387 34.687h9.02c.866 0 1.386-.69 1.386-1.386v-2.602c0-.867-.691-1.387-1.387-1.387h-9.02C.52 29.313 0 30.005 0 30.7v2.602c0 .695.695 1.386 1.387 1.386zM47.176 18.56c.52.52 1.562.52 2.082 0l6.418-6.418c.52-.52.52-1.563 0-2.082L53.94 8.324c-.52-.52-1.562-.52-2.082 0l-6.418 6.418c-.52.52-.52 1.563 0 2.082zM53.94 55.5l1.735-1.734c.52-.52.52-1.559 0-2.078l-6.418-6.418c-.52-.524-1.563-.524-2.082 0l-1.735 1.734c-.52.52-.52 1.559 0 2.082L51.86 55.5c.52.523 1.387.523 2.082 0zm-41.629 0 6.418-6.414c.524-.523.524-1.563 0-2.082l-1.734-1.734c-.52-.524-1.558-.524-2.082 0L8.5 51.687c-.523.52-.523 1.56 0 2.079l1.734 1.734c.692.523 1.559.523 2.079 0zm2.602-36.941c.523.52 1.563.52 2.082 0l1.734-1.735c.524-.52.524-1.562 0-2.082l-6.418-6.418c-.519-.52-1.558-.52-2.078 0L8.5 10.06c-.523.52-.523 1.562 0 2.082zm0 0" fill="#fea500"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sphinx.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#bababa"><path d="M30.906 0h3.746v2.496h-3.746zm31.532 26.066c.156-2.027 0-4.058-.465-5.93h-8.899v-3.745h7.18a15.572 15.572 0 0 0-2.031-2.496l-8.895-8.899-1.406 14.047c-.313 3.59-2.027 6.871-4.527 9.367v6.242h4.996c2.808 0 5.308 1.094 7.18 2.657 2.808-1.72 4.84-4.371 6.09-7.336h-8.587v-3.75h9.364zM34.651 2.652V7.18h-3.746V2.652H19.98l1.563 15.922c.312 2.813 1.559 5.465 3.746 7.34 2.027 1.871 4.836 2.965 7.648 2.965 2.81 0 5.461-1.094 7.649-2.965 2.027-1.875 3.434-4.527 3.746-7.34l1.563-15.922zm5.153 12.176h-4.996v6.09h-3.746v-6.09h-4.997v-3.746h13.739zm0 0"/><path d="M48.234 38.242h-8.742v-7.336c-2.027 1.094-4.37 1.563-6.71 1.563-2.344 0-4.684-.625-6.716-1.563v7.336h-8.738a7.33 7.33 0 0 0-7.336 7.34v4.68c5.774.937 10.145 5.933 10.145 11.863V64h23.57v-1.875c0-6.555 5.309-11.863 11.863-12.02v-4.523a7.33 7.33 0 0 0-7.336-7.34zm0 0"/><path d="M55.727 53.855c-4.528 0-8.274 3.747-8.274 8.27V64h6.402v-4.996h3.747V64H64v-1.875c0-4.68-3.746-8.27-8.273-8.27zm-37.93-34.968L16.39 4.84l-8.899 8.898a15.342 15.342 0 0 0-2.027 2.496h7.18v3.746H3.746c-.625 1.875-.781 3.903-.469 5.934h9.524v3.746H4.215c1.094 2.965 3.277 5.617 6.086 7.336 1.875-1.719 4.527-2.656 7.183-2.656h4.993v-6.242a13.914 13.914 0 0 1-4.68-9.211zM8.273 53.855C3.746 53.855 0 57.602 0 62.125V64h6.398v-4.996h3.747V64h6.402v-1.875c0-4.68-3.746-8.27-8.274-8.27zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sql.svg
1
1
<svg height="64" width="51" xmlns="http://www.w3.org/2000/svg"><path d="M23.023 63.957c-8.199-.34-15.543-2.875-19.468-6.77-1.196-1.011-2.39-2.535-2.903-3.55L.31 52.96v-7.617c0-7.614 0-7.614.171-6.934.34 1.692 1.368 3.383 2.903 4.735 1.023.847 3.074 2.37 4.781 3.214 2.906 1.524 6.66 2.54 10.59 3.047 2.39.34 3.246.34 6.66.34 3.418 0 4.27 0 6.66-.34 3.93-.508 7.516-1.691 10.59-3.047 1.707-.843 3.758-2.199 4.781-3.214 1.368-1.352 2.563-3.043 2.903-4.735.172-.508.172-.508.172 6.934v7.445l-.34.68c-1.196 2.367-3.246 4.398-5.98 6.09-5.294 3.046-13.321 4.738-21.177 4.398zm0-18.95c-7.171-.339-13.832-2.37-18.101-5.413-1.027-.68-2.39-2.032-2.906-2.707-.512-.68-1.024-1.524-1.364-2.371L.31 33.84v-7.445c0-7.446 0-7.446.171-6.938.34 1.184.852 2.54 1.88 3.555.511.675 1.367 1.523 1.878 1.86.168.171.684.339 1.024.679 3.414 2.367 8.199 4.058 13.664 4.906 2.39.336 3.242.336 6.66.336 3.414 0 4.27 0 6.66-.336 3.93-.508 7.516-1.691 10.59-3.047 1.707-.847 3.758-2.2 4.781-3.215 1.367-1.351 2.39-3.047 2.903-4.738.171-.508.171-.508.171 6.938v7.445l-.511 1.015c-.856 1.524-1.368 2.368-2.39 3.383-1.028 1.016-2.052 1.864-3.419 2.54-5.465 3.046-13.492 4.738-21.348 4.23zm-.511-18.78c-4.782-.34-8.54-1.184-12.125-2.54-4.27-1.69-7.344-3.89-8.883-6.597a5.594 5.594 0 0 1-.852-2.031C.48 14.383.31 12.69.48 11.676 1.504 6.262 8.848 1.859 18.754.34 21.144 0 22 0 25.414 0c3.418 0 4.27 0 6.66.34 3.93.508 7.516 1.691 10.59 3.043 4.441 2.199 7.172 5.078 7.684 8.12.172.849.172 2.708-.168 3.388-.512 1.691-1.196 2.707-2.563 4.058-3.586 3.723-9.906 6.094-17.762 6.938-1.023.34-6.32.34-7.343.34zm0 0" fill="#a03537"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sqlite.svg
1
1
<svg height="64" width="51" xmlns="http://www.w3.org/2000/svg"><path d="M23.023 63.957c-8.199-.34-15.543-2.875-19.468-6.77-1.196-1.011-2.39-2.535-2.903-3.55L.31 52.96v-7.617c0-7.614 0-7.614.171-6.934.34 1.692 1.368 3.383 2.903 4.735 1.023.847 3.074 2.37 4.781 3.214 2.906 1.524 6.66 2.54 10.59 3.047 2.39.34 3.246.34 6.66.34 3.418 0 4.27 0 6.66-.34 3.93-.508 7.516-1.691 10.59-3.047 1.707-.843 3.758-2.199 4.781-3.214 1.368-1.352 2.563-3.043 2.903-4.735.172-.508.172-.508.172 6.934v7.445l-.34.68c-1.196 2.367-3.246 4.398-5.98 6.09-5.294 3.046-13.321 4.738-21.177 4.398zm0-18.95c-7.171-.339-13.832-2.37-18.101-5.413-1.027-.68-2.39-2.032-2.906-2.707-.512-.68-1.024-1.524-1.364-2.371L.31 33.84v-7.445c0-7.446 0-7.446.171-6.938.34 1.184.852 2.54 1.88 3.555.511.675 1.367 1.523 1.878 1.86.168.171.684.339 1.024.679 3.414 2.367 8.199 4.058 13.664 4.906 2.39.336 3.242.336 6.66.336 3.414 0 4.27 0 6.66-.336 3.93-.508 7.516-1.691 10.59-3.047 1.707-.847 3.758-2.2 4.781-3.215 1.367-1.351 2.39-3.047 2.903-4.738.171-.508.171-.508.171 6.938v7.445l-.511 1.015c-.856 1.524-1.368 2.368-2.39 3.383-1.028 1.016-2.052 1.864-3.419 2.54-5.465 3.046-13.492 4.738-21.348 4.23zm-.511-18.78c-4.782-.34-8.54-1.184-12.125-2.54-4.27-1.69-7.344-3.89-8.883-6.597a5.594 5.594 0 0 1-.852-2.031C.48 14.383.31 12.69.48 11.676 1.504 6.262 8.848 1.859 18.754.34 21.144 0 22 0 25.414 0c3.418 0 4.27 0 6.66.34 3.93.508 7.516 1.691 10.59 3.043 4.441 2.199 7.172 5.078 7.684 8.12.172.849.172 2.708-.168 3.388-.512 1.691-1.196 2.707-2.563 4.058-3.586 3.723-9.906 6.094-17.762 6.938-1.023.34-6.32.34-7.343.34zm0 0" fill="#369"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/step.svg
1
1
<svg height="63" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M33.325 19.55c-.498-.2-1.1 0-1.299.5-1.1 2.5-2.901 4.7-5.1 6.4l-6.7-13.601c1-.8 1.6-1.999 1.6-3.4 0-2.099-1.501-3.899-3.501-4.3v-3.4a1 1 0 0 0-2 0v3.4c-2 .401-3.5 2.201-3.5 4.3 0 1.401.6 2.6 1.601 3.4l-6.7 13.602c-2.201-1.7-4-3.801-5.1-6.401-.201-.5-.8-.7-1.301-.5-.499.199-.7.8-.499 1.3 1.299 3 3.4 5.4 6 7.3l-4 8c-.2.5 0 1.1.4 1.3.098 0 .3.1.4.1.3 0 .7-.2.9-.5l3.8-7.8c2.7 1.5 5.6 2.2 8.7 2.2 3.1 0 6-.8 8.699-2.2l3.8 7.8c.1.3.501.5.9.5.1 0 .3 0 .4-.1.5-.2.7-.8.4-1.3l-3.9-8c2.6-1.8 4.701-4.4 6-7.3.6-.5.401-1.101 0-1.3zM17.326 6.95c1.4 0 2.5 1.1 2.5 2.499 0 1.401-1.1 2.502-2.5 2.502s-2.5-1.1-2.5-2.502c0-1.4 1.199-2.5 2.5-2.5zm0 22.6c-2.8 0-5.4-.7-7.801-2l6.8-13.7c.3.1.701.1 1.1.1.402 0 .701 0 1.1-.1l6.8 13.7c-2.5 1.3-5.199 2-7.999 2zm0 0" fill="#369" stroke="#369" stroke-miterlimit="10" stroke-width="1.5" transform="matrix(1.6544 0 0 1.63607 0 .154)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/stl.svg
1
1
<svg height="63" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M33.325 19.55c-.498-.2-1.1 0-1.299.5-1.1 2.5-2.901 4.7-5.1 6.4l-6.7-13.601c1-.8 1.6-1.999 1.6-3.4 0-2.099-1.501-3.899-3.501-4.3v-3.4a1 1 0 0 0-2 0v3.4c-2 .401-3.5 2.201-3.5 4.3 0 1.401.6 2.6 1.601 3.4l-6.7 13.602c-2.201-1.7-4-3.801-5.1-6.401-.201-.5-.8-.7-1.301-.5-.499.199-.7.8-.499 1.3 1.299 3 3.4 5.4 6 7.3l-4 8c-.2.5 0 1.1.4 1.3.098 0 .3.1.4.1.3 0 .7-.2.9-.5l3.8-7.8c2.7 1.5 5.6 2.2 8.7 2.2 3.1 0 6-.8 8.699-2.2l3.8 7.8c.1.3.501.5.9.5.1 0 .3 0 .4-.1.5-.2.7-.8.4-1.3l-3.9-8c2.6-1.8 4.701-4.4 6-7.3.6-.5.401-1.101 0-1.3zM17.326 6.95c1.4 0 2.5 1.1 2.5 2.499 0 1.401-1.1 2.502-2.5 2.502s-2.5-1.1-2.5-2.502c0-1.4 1.199-2.5 2.5-2.5zm0 22.6c-2.8 0-5.4-.7-7.801-2l6.8-13.7c.3.1.701.1 1.1.1.402 0 .701 0 1.1-.1l6.8 13.7c-2.5 1.3-5.199 2-7.999 2zm0 0" fill="#369" stroke="#369" stroke-miterlimit="10" stroke-width="1.5" transform="matrix(1.6544 0 0 1.63607 0 .154)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/svg.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M16.223 21.805.09 55.844l3.012 3.015 20.035-20.035c-.711-1.594-.532-3.543.886-4.96 1.774-1.774 4.43-1.774 6.204 0 1.773 1.769 1.773 4.429 0 6.202-1.243 1.243-3.368 1.594-4.965.887L5.23 60.984 8.242 64l34.04-16.133L49.73 27.48 36.61 14.36zm46.625-4.075L46.184 1.062c-1.418-1.417-3.547-1.417-4.965 0L37.32 4.966c-1.422 1.418-1.422 3.543 0 4.965l16.664 16.664c1.418 1.418 3.543 1.418 4.965 0l3.899-3.903c1.418-1.418 1.418-3.543 0-4.96zm0 0" fill="#fea500"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/swd.svg
1
1
<svg height="64" width="52" xmlns="http://www.w3.org/2000/svg"><path d="M46.168 13.516c1.793-.711 3.766-.891 5.738-.891V.008c-8.605-.18-16.851 3.554-22.23 10.308-2.153 2.844-4.125 5.864-5.559 9.243l-4.12 10.128c-1.079 3.024-2.333 6.223-3.767 9.067a31.916 31.916 0 0 1-3.945 6.754c-1.254 1.777-3.047 3.199-5.02 4.09-2.152 1.066-4.66 1.597-7.171 1.597v12.797c8.605.18 16.851-3.554 22.23-10.308 1.613-2.309 3.227-4.797 4.485-7.286l3.406-8h14.879v-12.62h-9.86c.715-1.954 1.793-3.731 3.047-5.508.895-1.602 2.153-2.844 3.407-3.91 1.613-1.422 3.046-2.313 4.48-2.844zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/swf.svg
1
1
<svg height="64" width="52" xmlns="http://www.w3.org/2000/svg"><path d="M46.168 13.516c1.793-.711 3.766-.891 5.738-.891V.008c-8.605-.18-16.851 3.554-22.23 10.308-2.153 2.844-4.125 5.864-5.559 9.243l-4.12 10.128c-1.079 3.024-2.333 6.223-3.767 9.067a31.916 31.916 0 0 1-3.945 6.754c-1.254 1.777-3.047 3.199-5.02 4.09-2.152 1.066-4.66 1.597-7.171 1.597v12.797c8.605.18 16.851-3.554 22.23-10.308 1.613-2.309 3.227-4.797 4.485-7.286l3.406-8h14.879v-12.62h-9.86c.715-1.954 1.793-3.731 3.047-5.508.895-1.602 2.153-2.844 3.407-3.91 1.613-1.422 3.046-2.313 4.48-2.844zm0 0" fill="#d10407"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/swift.svg
1
1
<svg height="64" width="72" xmlns="http://www.w3.org/2000/svg"><path d="M65.75 44.957S76.094 21.352 44.36.051c0 0 12.972 16.152 6.312 34.433 0 0-23.32-16.328-35.067-28.222 0 0 14.727 20.945 19.989 25.207 0 0-8.77-4.438-28.93-21.657 0 0 23.316 30.176 34.188 36.387 0 0-16.657 11.184-40.852-4.613 0 0 12.8 22.363 39.625 22.363 12.098 0 15.605-6.21 21.566-6.21 6.137 0 9.993 6.21 9.993 6.21 3.507-8.875-5.434-18.992-5.434-18.992zm0 0" fill="#fa2a22"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/swp.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><g fill="#bababa"><path d="M73.734 51.555c0-2.844-2.289-5.157-5.109-5.157H5.375c-2.82 0-5.11 2.313-5.11 5.157v7.289c0 2.843 2.29 5.156 5.11 5.156h63.25c2.82 0 5.11-2.313 5.11-5.156zm-27.308 6.757a2.985 2.985 0 0 1-2.996-3.023 2.985 2.985 0 0 1 2.996-3.023 2.985 2.985 0 0 1 2.996 3.023c0 1.777-1.234 3.023-2.996 3.023zm8.984 0a2.984 2.984 0 0 1-2.992-3.023c0-1.777 1.23-3.023 2.992-3.023a2.985 2.985 0 0 1 2.996 3.023 2.985 2.985 0 0 1-2.996 3.023zm8.813 0a2.985 2.985 0 0 1-2.996-3.023c0-1.777 1.234-3.023 2.996-3.023a2.981 2.981 0 0 1 2.992 3.023 2.981 2.981 0 0 1-2.992 3.023zM5.375 43.38h63.25c1.41 0 2.82.355 3.879 1.066l-6.168-12.98c-1.762-3.73-4.582-5.153-7.398-5.153h-6.876L42.2 36.623c-.707.71-1.586 1.245-2.469 1.6-.878.356-1.937.532-2.82.532-1.055 0-1.937-.176-2.816-.531h-.352c-.707-.356-1.41-.891-2.117-1.422l-9.867-10.668h-6.871c-2.817 0-5.461 1.601-7.399 5.156L1.32 44.266c1.235-.532 2.47-.887 4.055-.887zm0 0"/><path d="M51.71 21.332c.352-.355.532-.71.884-1.242.176-.535.351-.89.351-1.602 0-.531-.175-1.066-.351-1.422-.176-.53-.532-.886-.883-1.246a5.273 5.273 0 0 0-1.23-.886c-.356-.18-.883-.356-1.41-.356-.532 0-1.06.176-1.41.356-.528.175-.884.53-1.235.886l-5.637 5.692V3.734c0-.535-.176-1.066-.352-1.421-.18-.536-.53-.891-.882-1.247-.352-.355-.703-.71-1.235-.886C37.97 0 37.441 0 36.91 0c-.527 0-1.055 0-1.406.18-.531.175-.883.53-1.234.886-.352.356-.708.711-.883 1.246-.176.532-.352.887-.352 1.422v17.953L27.398 16c-.351-.355-.707-.71-1.234-.89-.352-.176-.879-.356-1.41-.356-.527 0-1.055.18-1.41.355-.352.18-.88.536-1.23.891-.356.355-.708.71-.884 1.246-.175.531-.351.887-.351 1.422 0 .531.176 1.066.351 1.598.176.535.528.89.883 1.246L34.27 33.957c.351.355.703.711 1.234.887.351.18.879.355 1.406.355.531 0 1.059-.176 1.41-.355.532-.176.883-.532 1.235-.887zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/sys.svg
1
1
<svg height="63" width="60" xmlns="http://www.w3.org/2000/svg"><path d="M57.96 53.09 37.532 32.832l-.527-.523 3.7-3.844.526-.524c4.754 1.747 10.391.875 14.266-2.968 2.816-2.793 4.227-6.637 3.875-10.653 0-.523-.355-.875-.707-1.047-.352-.175-.879 0-1.23.348l-6.516 6.461-6.871-1.57-1.758-6.813 6.516-6.46c.351-.348.351-.872.351-1.223-.176-.348-.527-.7-1.058-.7-4.051-.347-7.922 1.051-10.743 3.844-3.87 3.844-4.93 9.43-2.992 14.145l-.527.523-5.285 5.067-9.86-9.782-.355-.347c.176-.176.176-.352.355-.7 2.114.348 5.809-3.668 8.98-6.812L18.337 0c-4.227 4.191-7.219 6.984-6.867 8.906-.88.524-1.762 1.047-2.465 1.57L7.77 11.7c-.88.875-1.407 1.922-1.582 2.969-.176.176-.176.352-.352.523l-.531 1.051v.172l-.528.7c-.351.35-.527.698-.703 1.222a.378.378 0 0 1-.351.352l-.18.171c-.176.524-.527.875-.703 1.399-.176.523-.527 1.222-.703 1.922v.347c0 .176-.176.524-.176.7l-.176.875c-.176.523-.176 1.046-.176 1.57v3.144l.176.696v.351c0 .348.176.524.176.871l.527 1.575c.176.523.703.87 1.235.87.351 0 .527-.171.703-.347s.351-.352.351-.7l.176-1.745c0-.176 0-.348.176-.524 0-.175 0-.351.176-.351l.175-.696s0-.175.176-.175c0 0 0-.176.18-.176 0-.172.176-.348.176-.348.175-.351.175-.523.351-.699 0-.176.176-.176.176-.348.176-.351.352-.527.527-.875l.352-.523c0-.176.176-.176.352-.352l.18-.172c.175-.351.527-.523.878-.875l.176-.171c.176-.176.527-.352.703-.528.176 0 .176-.172.355-.172.176 0 .176-.175.352-.175.176-.176.352-.348.703-.348l.528-.352.53-.175c.177-.172.352-.172.528-.172s.176 0 .352-.176c0 0 .175 0 .175-.176.176 0 .176 0 .352-.172h.176c.176.172.351.524.531.696l9.684 9.605L2.488 51.52c-2.64 2.617-2.992 6.285-.351 8.906 2.64 2.617 6.164 1.918 8.629-.7l18.136-19.035.356.348 20.426 20.258a5.883 5.883 0 0 0 8.277 0 5.763 5.763 0 0 0 0-8.207zM7.95 57.629c-.884.875-2.47.875-3.348 0-.88-.871-.88-2.445 0-3.316.878-.876 2.464-.876 3.347 0a2.134 2.134 0 0 1 0 3.316zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/tar.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><path d="m49.332 34.941-12.25-5.714L61.75 17.633 74 23.348l-12.25 5.879zM61.75 6.207 49.5.492 37.25 6.207l24.5 11.594L74 12.086zm-37.082 17.14-12.25-5.714-12.25 5.715L24.836 34.94l12.246-5.714zm0-11.429 12.25-5.711L24.668.492 0 12.086 12.25 17.8zM61.75 32.59l-11.074 5.039-1.344.672-1.34-.672-11.074-5.04-11.078 5.04-1.34.672-1.344-.672-11.074-5.04v17.977L36.75 63.508l25-12.942zm0 0" fill="#4d1b9b"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/tax.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M12.726.224c-2.801 0-5.202 2.302-5.202 5.2 0 2.8 2.302 5.2 5.202 5.2.9 0 1.698-.199 2.4-.599l2.099 2.101-6 6c-.999.999-.999 2.7 0 3.698l.2.202L23.524 9.925c.702.4 1.602.601 2.4.601 2.801 0 5.202-2.302 5.202-5.2 0-2.8-2.302-5.2-5.202-5.2-2.798 0-5.199 2.3-5.199 5.2 0 .898.2 1.698.601 2.4l-2.101 2.098-2.1-2.098c.399-.702.6-1.6.6-2.4.2-2.8-2.2-5.102-5-5.102zm0 3.001c1.199 0 2.2 1 2.2 2.2 0 1.2-1.001 2.2-2.2 2.2a2.22 2.22 0 0 1-2.2-2.2c0-1.3.998-2.2 2.2-2.2zm13.3 0c1.199 0 2.2 1 2.2 2.2 0 1.2-1.001 2.2-2.2 2.2a2.22 2.22 0 0 1-2.2-2.2c0-1.3.998-2.2 2.2-2.2zm-22.902 10.3c-.198 0-.398 0-.598.1-.2 0-.4.101-.601.2-.2.1-.3.2-.5.301-.1.098-.301.199-.4.3-.099.1-.2.299-.3.4-.1.1-.2.299-.3.5-.1.199-.1.3-.2.498v.1c0 .202-.1.4-.1.602v17.8c0 .198 0 .4.1.598 0 .201.1.4.2.601.1.2.2.3.3.501.1.098.201.3.3.4.099.098.3.199.4.3.1.1.3.198.5.299.2.1.299.1.5.201h.1c.2 0 .401.098.599.098h32.602c.198 0 .398 0 .598-.098.2 0 .4-.1.601-.201.2-.1.3-.199.5-.3.1-.1.301-.2.4-.299.099-.1.2-.302.3-.4.1-.1.2-.302.3-.5.1-.202.1-.3.2-.501v-.1c0-.2.1-.4.1-.6V16.526c0-.201 0-.4-.1-.601 0-.199-.1-.4-.2-.599-.1-.201-.2-.302-.3-.5-.1-.101-.201-.3-.3-.4-.099-.101-.3-.202-.4-.3-.1-.1-.3-.201-.5-.302-.2-.098-.299-.098-.5-.199h-.1c-.2 0-.401-.1-.599-.1h-11.9l-3.002 3.001h11.802c0 1.6 1.298 2.999 2.999 2.999v11.8c-1.6 0-2.999 1.3-2.999 3h-26.6c0-1.6-1.3-3-3.001-3v-11.9c1.6 0 3.001-1.299 3.001-3h3.4l2.998-3zm16.301 5.9c-3.3 0-5.9 3-5.9 6.699 0 2.1.899 4 2.2 5.2h7.3c1.401-1.2 2.2-3.1 2.2-5.2.1-3.698-2.601-6.7-5.8-6.7zm-11.9 4.5c-.8 0-1.499.7-1.499 1.5s.7 1.499 1.498 1.499c.801 0 1.5-.7 1.5-1.5s-.699-1.499-1.5-1.499zm23.7 0c-.8 0-1.501.7-1.501 1.5s.702 1.499 1.5 1.499c.801 0 1.501-.7 1.501-1.5s-.7-1.499-1.5-1.499zm0 0" fill="#83ad51" stroke="#83ad51" stroke-miterlimit="10" stroke-width=".25" transform="matrix(1.6973 0 0 1.70894 .53 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/tcsh.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M13.875 13.874h10.9v2.701h-10.9zm0 5.4h10.9v2.701h-10.9zm0 5.5h10.9v2.702h-10.9zm19-24.399H11.177c-3 0-5.402 2.4-5.402 5.4v24.4h-5.4c0 3 2.402 5.4 5.4 5.4h21.7c3 0 5.402-2.4 5.402-5.4v-21.7h5.4v-2.7c0-3-2.402-5.4-5.4-5.4zm-2.7 29.3c0 1.801-1.4 3.2-3.2 3.2h-19.9c1.3-.9 1.3-2.7 1.3-2.7v-24.4c0-1.5 1.2-2.7 2.7-2.7 1.501 0 2.7 1.2 2.7 2.7v2.7h16.3zm-13.6-23.9v-2.7h16.3c2.501 0 2.7 1.6 2.7 2.7zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".75" transform="matrix(1.7717 0 0 1.78025 .262 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/tfignore.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M54.633 9.367C42.145-3.12 21.855-3.12 9.367 9.367s-12.488 32.778 0 45.266 32.778 12.488 45.266 0 12.488-32.778 0-45.266zM12.176 44.801c-5.934-9.211-4.84-21.543 3.12-29.504s20.294-9.055 29.505-3.121zm7.023 7.023L51.824 19.2c5.934 9.211 4.84 21.543-3.12 29.504s-20.294 9.055-29.505 3.121zm0 0" fill="#bababa"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/tga.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/tgz.svg
1
1
<svg height="63" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M53.344 18.172H44.02V8.965zM28.309 8.965v33.437h25.199V20.434H41.727V8.964zm3.93-8.723H4.417v6.461h10.965l-6.875 5.332v5.652l10.148-7.753V6.867H54V4.281zM18.655 14.297 8.508 22.05v5.652l10.148-7.754zM8.344 37.559l10.148-7.754v-5.657L8.344 31.902zm10.312 2.261v-5.656L8.508 41.918v2.91h-4.09v6.461h6.219v4.523H7.035c-.652-1.132-1.797-1.937-3.273-1.937C1.637 53.875 0 55.488 0 57.59c0 2.097 1.637 3.715 3.762 3.715 1.476 0 2.62-.809 3.273-1.938h3.602v3.39h5.562v-3.39h3.602c.652 1.13 1.8 1.938 3.273 1.938 2.125 0 3.762-1.618 3.762-3.715 0-2.102-1.637-3.715-3.762-3.715-1.472 0-2.62.805-3.273 1.938h-3.602v-4.524h15.875l21.762-3.879v-2.582H11.78zm0 0" fill="#90c"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/tiff.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/tmp.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><g fill="#bababa"><path d="M51.227 30.453c.687.688 1.89.688 2.574-.172.687-.691.687-1.722.172-2.582l-4.805-5.676a1.33 1.33 0 0 1-.34-.863l-.687-10.664c0-1.035-.86-1.723-1.887-1.723-1.031 0-1.719.688-1.887 1.723v.172l-.687 11.7v.171c0 .688.172 1.375.855 1.89l.344.344zm0 0"/><path d="M46.426 0C36.645 0 28.41 6.367 25.32 15.14c1.887.172 3.774.688 5.664 1.204 2.403-6.192 8.407-10.668 15.442-10.668 9.094 0 16.469 7.398 16.469 16.52 0 8.257-6.004 15.136-13.899 16.343.172 1.031.172 2.234.172 3.266 0 .863 0 1.722-.172 2.582 11.152-1.203 19.734-10.668 19.734-22.02C68.73 10.152 58.777 0 46.426 0zm0 0"/><path d="M42.648 38.71h-2.914c-.515-2.41-1.375-4.644-2.746-6.71l2.059-2.066c.687-.688.687-1.891 0-2.579l-2.059-2.066c-.687-.687-1.886-.687-2.574 0l-2.059 2.066c-2.058-1.378-4.289-2.41-6.69-2.753v-2.926c0-1.031-.86-1.89-1.888-1.89H20.86c-1.027 0-1.886.859-1.886 1.89v2.926c-2.403.515-4.633 1.375-6.692 2.753l-1.886-2.238c-.688-.687-1.887-.687-2.575 0l-2.058 2.067c-.688.687-.688 1.89 0 2.578l1.886 2.066c-1.37 2.063-2.402 4.3-2.746 6.711H2.16c-1.031 0-1.89.86-1.89 1.89v2.926c0 1.032.859 1.891 1.89 1.891h2.742c.516 2.41 1.375 4.645 2.746 6.711l-1.886 1.89c-.688.692-.688 1.895 0 2.583l2.058 2.066c.688.688 1.887.688 2.575 0l2.058-1.894c2.059 1.375 4.29 2.41 6.692 2.753v2.754c0 1.032.859 1.891 1.886 1.891h2.918c1.028 0 1.887-.86 1.887-1.89v-2.755c2.402-.515 4.633-1.378 6.691-2.753l1.887 2.066c.688.687 1.887.687 2.574 0l2.059-2.066c.687-.688.687-1.891 0-2.579l-2.059-2.066c1.371-2.066 2.403-4.3 2.746-6.71h2.914c1.032 0 1.887-.86 1.887-1.892V40.43c0-.86-.855-1.72-1.887-1.72zM29.094 48.86c-3.774 3.785-9.95 3.785-13.723 0-3.777-3.786-3.777-9.977 0-13.762 3.774-3.785 9.95-3.785 13.723 0 3.949 3.785 3.777 9.976 0 13.761zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/tmx.svg
1
1
<svg height="64" width="80" xmlns="http://www.w3.org/2000/svg"><path d="M38.027 37.414c-5.011-4.812-9.425-9.223-12.03-19.25H43.64v-7.219H26.195V1.121h-7.617v10.024H.93v7.421h18.047s-.2 1.403-.399 2.606C15.968 30.996 13.164 37.215.93 43.23l2.61 7.418c11.429-6.015 17.444-13.835 20.05-22.257 2.605 6.418 6.816 11.629 11.629 16.441zM61.29 13.352H51.262L33.617 62.879h7.617l5.016-14.836H66.3l5.013 14.836h7.62zm-12.434 27.27 7.622-19.65 7.617 19.852zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width="1.5039150000000001"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/torrent.svg
1
1
<svg height="64" width="58" xmlns="http://www.w3.org/2000/svg"><path d="M57.938 50.598c-.325 1.437-1.29 2.394-2.415 3.511-14.785 15.325-40.34 12.45-51.109-5.906-2.89-4.95-4.5-10.055-4.34-15.8.324-7.184 2.735-13.407 7.235-18.997C12.613 6.703 19.363 2.234 28.043.796 30.129.48 32.219.32 34.309 0v5.906l-4.34.477C17.754 8.14 7.469 18.355 6.664 30.168c-.48 7.34 1.45 14.047 6.43 19.633 1.93 2.394 4.18 4.469 6.91 5.746.805.32 1.77.476 2.574.637-7.715-4.47-12.215-11.172-13.664-19.793-.965-5.746.16-11.332 3.375-16.278C20.49 7.5 34.793 5.426 44.594 9.578c-.801 1.754-1.606 3.512-2.41 5.106-1.926-.317-3.856-.957-5.786-.957-5.625-.32-10.605.957-14.785 4.949-10.61 9.734-7.875 24.738 2.41 31.445 3.215 2.234 6.75 3.828 10.606 4.625.965.32 1.93 0 3.055-.156-.16-.16-.32-.16-.48-.16-4.825-.957-9.325-2.555-13.18-5.907-3.86-3.351-6.75-7.503-7.235-12.77-.48-7.503 2.414-13.566 8.84-17.398 5.625-3.511 11.574-3.511 17.52-.636 3.374 1.593 5.785 4.148 7.714 7.34-1.765.957-3.375 1.757-4.98 2.554-1.45-1.437-2.735-3.031-4.34-3.992-7.555-5.105-18.164-.316-18.965 9.102-.324 4.629 1.606 8.14 4.82 11.332 3.856 3.511 8.52 4.789 13.66 5.425 4.985.641 9.965.48 14.95-.16.965-.16 1.445.48 1.93 1.278 0-.16 0 0 0 0zm0 0" fill="#bababa"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/tpl.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#999"><path d="M60.89 0H10.974C10.238 0 9.69.547 9.69 1.281v3.84c0 .73.73 1.277 1.282 1.277h46.629v46.813c0 .55.73 1.281 1.277 1.281h3.84A1.25 1.25 0 0 0 64 53.211V3.109C64 1.281 62.719 0 60.89 0zm0 0"/><path d="M49.922 12.8H1.282C.546 12.8 0 13.349 0 14.079v48.64C0 63.27.73 64 1.281 64h48.64c.548 0 1.278-.547 1.278-1.281v-48.64c0-.731-.547-1.278-1.277-1.278zm-27.43 43.52c0 .547-.73 1.282-1.281 1.282H7.863c-.73 0-1.281-.551-1.281-1.282v-3.84c0-.55.73-1.28 1.281-1.28h13.164c.735 0 1.282.55 1.282 1.28v3.84zm0-12.8c0 .55-.73 1.28-1.281 1.28H7.863c-.73 0-1.281-.55-1.281-1.28v-3.84c0-.73.73-1.282 1.281-1.282h13.164c.735 0 1.282.551 1.282 1.282v3.84zm22.309 12.8c0 .547-.551 1.282-1.281 1.282H30.172c-.73 0-1.281-.551-1.281-1.282v-3.84c0-.55.55-1.28 1.28-1.28H43.52c.55 0 1.28.55 1.28 1.28zm0-12.8c0 .55-.551 1.28-1.281 1.28H30.172c-.73 0-1.281-.55-1.281-1.28v-3.84c0-.73.55-1.282 1.28-1.282H43.52c.55 0 1.28.551 1.28 1.282zm0-12.801c0 .55-.551 1.281-1.281 1.281H7.68a1.25 1.25 0 0 1-1.282-1.281V20.48c0-.73.735-1.28 1.282-1.28h35.84c.73 0 1.28.55 1.28 1.28zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ts.svg
1
1
<svg height="64" width="37" xmlns="http://www.w3.org/2000/svg"><path d="M21.914 13.86V0h-9.039c-.187.547-.371 1.094-.371 1.824-.184.363-.184.547-.184.91-.922 5.106-3.691 8.754-8.3 10.758-1.293.547-2.582.73-3.875.547v11.125h6.64c.184 15.68.184 23.883.184 24.25v.91c.926 6.93 4.43 10.942 10.886 12.582 2.583.73 5.348 1.094 8.301 1.094 3.688-.184 7.196-.73 10.7-1.824v-13.13a101.367 101.367 0 0 0-5.536 1.645c-3.136.91-5.902.364-8.117-1.824-.183-.367-.55-.73-.55-1.094a23.898 23.898 0 0 1-.555-5.105V25.164h14.386V14.04h-14.57zm0 0" fill="#4065aa"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/tsv.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="52"><path style="fill:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke:#1f7244;stroke-opacity:1;stroke-miterlimit:10" d="M0 1.5h8.5m3.3 0h8.5m3.4 0h8.5m3.3 0H44M0 7.4h8.5m3.3 0h8.5m3.4 0h8.5m3.3 0H44M0 13.3h8.5m3.3 0h8.5m3.4 0h8.5m3.3 0H44M0 19.2h8.5m3.3 0h8.5m3.4 0h8.5m3.3 0H44M0 25.1h8.5m3.3 0h8.5m3.4 0h8.5m3.3 0H44" transform="matrix(1.9091 0 0 1.92593 0 .385)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/ttf.svg
1
1
<svg height="64" width="65" xmlns="http://www.w3.org/2000/svg"><path d="M43.223.492V13.66h-1.32c-.825-3-1.65-5.168-2.473-6.5-.825-1.332-2.145-2.336-3.797-3.168-.824-.5-2.473-.668-4.781-.668h-3.63v37.512c0 2.5.165 4 .329 4.668.328.668.824 1.168 1.648 1.668s1.817.664 3.301.664h1.648v1.336H9.074v-1.336h1.649c1.32 0 2.476-.332 3.3-.832.66-.332 1.153-.832 1.485-1.668.328-.5.328-2 .328-4.5V3.324h-3.461c-3.3 0-5.61.668-7.098 2.004-1.976 2-3.297 4.664-3.957 8.332H0V.492zm0 0" fill="#7291a1"/><path d="M65 14.828V28h-1.32c-.825-3-1.649-5.168-2.473-6.504-.828-1.332-2.145-2.332-3.797-3.168-.824-.5-2.472-.664-4.785-.664h-3.629v37.508c0 2.5.168 4.004.332 4.668.328.668.824 1.168 1.649 1.668.824.5 1.816.668 3.3.668h1.649v1.332H30.684v-1.332h1.652c1.32 0 2.473-.336 3.297-.836.66-.332 1.156-.832 1.488-1.664.328-.5.328-2.004.328-4.504V17.664h-3.465c-3.3 0-5.609.664-7.093 2-1.98 2-3.301 4.668-3.961 8.336h-1.317V14.828zm0 0" fill="#36454d"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/twig.svg
1
1
<svg height="64" width="72" xmlns="http://www.w3.org/2000/svg"><path d="M66.387 64c-4.18-36.844 4.875-26.973 4.875-26.973-9.926-11.988-14.973.528-17.414 12.164C52.628 30.504 46.883-.523 23.898.008c12.364 0 13.059 25.383 11.84 43.719-10.797-19.391-35-17.98-35-17.98s18.11-.708 18.11 38.077h47.539zm0 0" fill="#7faf4a"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/txt.svg
1
1
<svg height="64" width="76" xmlns="http://www.w3.org/2000/svg"><path d="M.176 52.977h75.648V64H.176zm0-26.309h75.648v11.02H.176zM.176 0h75.648v11.023H.176zm0 0" fill="#666"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/udf.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#eab41b"><path d="M28.023 32c0 1.04.344 2.074 1.211 2.766 1.555 1.558 4.149 1.558 5.707 0 .692-.692 1.211-1.727 1.211-2.766s-.347-2.074-1.21-2.766c-.692-.695-1.731-1.21-2.77-1.21-1.035 0-2.074.343-2.766 1.21-1.039.692-1.383 1.727-1.383 2.766zm0 0"/><path d="M9.34 9.34c-12.453 12.453-12.453 32.691 0 45.32 12.453 12.453 32.691 12.453 45.32 0 12.453-12.453 12.453-32.691 0-45.32-12.453-12.453-32.867-12.453-45.32 0zm47.394 36.152c-1.21 2.074-2.765 4.153-4.496 5.88-1.73 1.73-3.804 3.288-5.883 4.5l-7.437-14.184s.691-.176 2.078-1.56c1.383-1.382 1.727-2.073 1.727-2.073zM37.707 26.293c1.559 1.555 2.422 3.633 2.422 5.707s-.863 4.152-2.422 5.707a7.933 7.933 0 0 1-11.242 0c-1.559-1.555-2.422-3.633-2.422-5.707s.691-4.152 2.422-5.707c2.941-3.113 8.129-3.113 11.242 0zm-10.895-5.535s-1.558.863-2.769 2.246c-1.211 1.387-1.211 1.558-1.73 2.25l-14.184-7.61c1.21-2.078 2.77-4.152 4.5-5.882 1.902-1.73 3.805-3.285 5.879-4.496zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/vb.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M21.344 36.297c-1.871 1.527-3.739 2.887-5.61 4.246L4.52 49.211c-.512.34-.852.508-1.36.168A15.884 15.884 0 0 0 .781 48.19c-.511-.171-.68-.511-.68-1.02V17.267c0-.34.34-.852.508-1.02.852-.512 1.7-.851 2.72-1.36.51-.171.85 0 1.19.169 3.06 2.379 6.118 4.757 9.348 7.136 2.547 1.872 5.098 3.91 7.645 5.778l.511-.508C31.367 18.453 40.543 9.449 49.891.44c.507-.507.847-.507 1.527-.34 3.91 1.532 7.816 3.231 11.727 4.758.34.172.507.512.68.852.167.168 0 .508 0 .68v51.316c0 1.188 0 1.188-1.192 1.7-3.738 1.527-7.477 2.886-11.215 4.417-.68.34-1.02.168-1.527-.34-9.348-8.496-18.524-17.504-27.868-26.34-.171-.34-.34-.507-.68-.847zm26.676 8.156V19.984L31.707 32.22zM13.867 32.22c-2.719-2.38-5.437-4.758-8.16-7.309v14.613c2.723-2.378 5.441-4.757 8.16-7.304zm0 0" fill="#d5006e"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/vbs.svg
1
1
<svg height="64" width="80" xmlns="http://www.w3.org/2000/svg"><path d="M3.121 5.453v2.363L.227 8.91V6.547zm0 23.637v2.363L.227 32.727v-2.364zm0 24v2.547l-2.894 1.09v-2.364zm-.363-39.817v2.18L.406 16.547v-2.184zm0 8v2.18L.406 24.547v-2.184zm0 16v2.18L.406 40.547v-2.184zm0 8v2.18L.406 48.547v-2.184zm7.414-38.546v2.91L6.375 11.09V8.363zm0 24v2.726L6.375 34.91v-2.726zm0 24.183v2.727l-3.797 1.636v-2.726zm-.184-39.82v2.547l-3.07 1.273v-2.547zm0 8v2.547l-3.07 1.273v-2.726zm0 16v2.547l-3.07 1.273v-2.726zm0 8v2.547l-3.07 1.273v-2.547zm7.414-38.543V12l-5.062 2v-3.273zm0 24V36l-5.062 2v-3.273zm0 23.816v3.453l-5.062 2v-3.453zm-.18-39.453V20l-4.16 1.637v-2.91zm0 8v2.906l-4.16 1.637v-2.906zm0 16v2.906l-4.16 1.637v-2.906zm0 7.637v3.09l-4.16 1.636v-2.91zm8.133-40.184v4.547l-6.144 2.543V11.09zm0 24v4.547l-6.144 2.363v-4.546zm0 23.82v4.544l-6.144 2.546V58.91zm-.359-39.456v4.183l-5.242 2.18v-4.18zm0 8v4l-5.242 2.183v-4.183zm0 16v4.183l-5.242 2v-4.183zm0 7.636v4.184l-5.242 2v-4zm8.496-40.726v5.816l-6.87 2.73v-5.82zm0 24v5.816l-6.87 2.73v-5.82zm0 23.816v5.82L26.622 64v-5.816zm-.363-39.09v4.91l-5.785 2.364v-4.91zm0 7.637v4.91l-5.785 2.363v-4.91zm0 16.363v4.91l-5.965 2.18v-4.906zm0 7.637v4.91l-5.785 2.363v-4.91zm8.68-43.273v8l-7.414 3.09V8.362c2.53-1.636 5.062-2.726 7.414-3.636zm0 8.726v6.91l-7.414 3.09v-6.906zm0 7.82v6.91l-7.414 3.09v-6.91zm0 7.817V36l-7.414 3.09v-6.906zm0 7.82V44l-7.414 3.09V40zm0 8v6.906l-7.414 3.274V48zm0 7.817v7.457c-2.895 1.09-5.426 2.18-7.414 3.27V56zM79.773 4.91v56c-4.699-3.094-10.668-4.726-17.535-4.726-5.785 0-12.297 1.27-19.527 3.632v-7.632c3.797-1.457 7.957-2.547 12.656-3.274V30.727c-3.797.546-8.137 1.82-12.656 4v-5.274a48.751 48.751 0 0 1 12.656-3.816V7.817C51.391 8.546 47.051 10 42.711 12V4.184C49.039 1.454 55.367 0 61.51 0c6.512.184 12.657 1.816 18.262 4.91zM72.36 9.816c-3.07-1.632-6.687-2.363-10.847-2.363h-1.446v18.184h1.63c3.613 0 7.23.547 10.663 1.816zm0 22.73c-3.254-1.456-6.867-2.183-10.664-2.183h-1.629v18.184h1.63c3.976 0 7.41.543 10.663 1.453zm4.883 30.727V62H76.7v-.184h1.266V62h-.543v1.273zm.903 0v-1.457h.363l.543 1.094.543-1.094h.18v1.457h-.18V62l-.543 1.09h-.184L78.328 62v1.273zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/vcd.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><g fill="#eab41b"><path d="M28.023 32c0 1.04.344 2.074 1.211 2.766 1.555 1.558 4.149 1.558 5.707 0 .692-.692 1.211-1.727 1.211-2.766s-.347-2.074-1.21-2.766c-.692-.695-1.731-1.21-2.77-1.21-1.035 0-2.074.343-2.766 1.21-1.039.692-1.383 1.727-1.383 2.766zm0 0"/><path d="M9.34 9.34c-12.453 12.453-12.453 32.691 0 45.32 12.453 12.453 32.691 12.453 45.32 0 12.453-12.453 12.453-32.691 0-45.32-12.453-12.453-32.867-12.453-45.32 0zm47.394 36.152c-1.21 2.074-2.765 4.153-4.496 5.88-1.73 1.73-3.804 3.288-5.883 4.5l-7.437-14.184s.691-.176 2.078-1.56c1.383-1.382 1.727-2.073 1.727-2.073zM37.707 26.293c1.559 1.555 2.422 3.633 2.422 5.707s-.863 4.152-2.422 5.707a7.933 7.933 0 0 1-11.242 0c-1.559-1.555-2.422-3.633-2.422-5.707s.691-4.152 2.422-5.707c2.941-3.113 8.129-3.113 11.242 0zm-10.895-5.535s-1.558.863-2.769 2.246c-1.211 1.387-1.211 1.558-1.73 2.25l-14.184-7.61c1.21-2.078 2.77-4.152 4.5-5.882 1.902-1.73 3.805-3.285 5.879-4.496zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/vcf.svg
1
1
<svg height="64" width="71" xmlns="http://www.w3.org/2000/svg"><path d="M42.602 10.79V7.132C42.602 3.293 39.324 0 35.5 0s-7.102 3.293-7.102 7.133v3.656H.184V64h70.632V10.79zm21.117 46.077H7.28V17.738h21.117v3.473h14.204v-3.473h21.117zm0 0"/><path d="M24.941 32c0 2.02-1.628 3.656-3.64 3.656S17.66 34.02 17.66 32s1.628-3.656 3.64-3.656S24.94 29.98 24.94 32zM21.3 39.133c-3.823 0-7.1 3.289-7.1 7.129v3.66h14.198v-3.66c0-4.024-3.093-7.13-7.097-7.13zm17.84-10.79H56.8v7.13H39.14zm0 14.446H56.8v7.133H39.14zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/vcs.svg
1
1
<svg height="64" width="71" xmlns="http://www.w3.org/2000/svg"><path d="M42.602 10.79V7.132C42.602 3.293 39.324 0 35.5 0s-7.102 3.293-7.102 7.133v3.656H.184V64h70.632V10.79zm21.117 46.077H7.28V17.738h21.117v3.473h14.204v-3.473h21.117zm0 0"/><path d="M24.941 32c0 2.02-1.628 3.656-3.64 3.656S17.66 34.02 17.66 32s1.628-3.656 3.64-3.656S24.94 29.98 24.94 32zM21.3 39.133c-3.823 0-7.1 3.289-7.1 7.129v3.66h14.198v-3.66c0-4.024-3.093-7.13-7.097-7.13zm17.84-10.79H56.8v7.13H39.14zm0 14.446H56.8v7.133H39.14zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/vdi.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><g fill="#4d1b9b"><path d="M73.734 51.555c0-2.844-2.289-5.157-5.109-5.157H5.375c-2.82 0-5.11 2.313-5.11 5.157v7.289c0 2.843 2.29 5.156 5.11 5.156h63.25c2.82 0 5.11-2.313 5.11-5.156zm-27.308 6.757a2.985 2.985 0 0 1-2.996-3.023 2.985 2.985 0 0 1 2.996-3.023 2.985 2.985 0 0 1 2.996 3.023c0 1.777-1.234 3.023-2.996 3.023zm8.984 0a2.984 2.984 0 0 1-2.992-3.023c0-1.777 1.23-3.023 2.992-3.023a2.985 2.985 0 0 1 2.996 3.023 2.985 2.985 0 0 1-2.996 3.023zm8.813 0a2.985 2.985 0 0 1-2.996-3.023c0-1.777 1.234-3.023 2.996-3.023a2.981 2.981 0 0 1 2.992 3.023 2.981 2.981 0 0 1-2.992 3.023zM5.375 43.38h63.25c1.41 0 2.82.355 3.879 1.066l-6.168-12.98c-1.762-3.73-4.582-5.153-7.398-5.153h-6.876L42.2 36.623c-.707.71-1.586 1.245-2.469 1.6-.878.356-1.937.532-2.82.532-1.055 0-1.937-.176-2.816-.531h-.352c-.707-.356-1.41-.891-2.117-1.422l-9.867-10.668h-6.871c-2.817 0-5.461 1.601-7.399 5.156L1.32 44.266c1.235-.532 2.47-.887 4.055-.887zm0 0"/><path d="M51.71 21.332c.352-.355.532-.71.884-1.242.176-.535.351-.89.351-1.602 0-.531-.175-1.066-.351-1.422-.176-.53-.532-.886-.883-1.246a5.273 5.273 0 0 0-1.23-.886c-.356-.18-.883-.356-1.41-.356-.532 0-1.06.176-1.41.356-.528.175-.884.53-1.235.886l-5.637 5.692V3.734c0-.535-.176-1.066-.352-1.421-.18-.536-.53-.891-.882-1.247-.352-.355-.703-.71-1.235-.886C37.97 0 37.441 0 36.91 0c-.527 0-1.055 0-1.406.18-.531.175-.883.53-1.234.886-.352.356-.708.711-.883 1.246-.176.532-.352.887-.352 1.422v17.953L27.398 16c-.351-.355-.707-.71-1.234-.89-.352-.176-.879-.356-1.41-.356-.527 0-1.055.18-1.41.355-.352.18-.88.536-1.23.891-.356.355-.708.71-.884 1.246-.175.531-.351.887-.351 1.422 0 .531.176 1.066.351 1.598.176.535.528.89.883 1.246L34.27 33.957c.351.355.703.711 1.234.887.351.18.879.355 1.406.355.531 0 1.059-.176 1.41-.355.532-.176.883-.532 1.235-.887zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/vmdk.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><g fill="#4d1b9b"><path d="M73.734 51.555c0-2.844-2.289-5.157-5.109-5.157H5.375c-2.82 0-5.11 2.313-5.11 5.157v7.289c0 2.843 2.29 5.156 5.11 5.156h63.25c2.82 0 5.11-2.313 5.11-5.156zm-27.308 6.757a2.985 2.985 0 0 1-2.996-3.023 2.985 2.985 0 0 1 2.996-3.023 2.985 2.985 0 0 1 2.996 3.023c0 1.777-1.234 3.023-2.996 3.023zm8.984 0a2.984 2.984 0 0 1-2.992-3.023c0-1.777 1.23-3.023 2.992-3.023a2.985 2.985 0 0 1 2.996 3.023 2.985 2.985 0 0 1-2.996 3.023zm8.813 0a2.985 2.985 0 0 1-2.996-3.023c0-1.777 1.234-3.023 2.996-3.023a2.981 2.981 0 0 1 2.992 3.023 2.981 2.981 0 0 1-2.992 3.023zM5.375 43.38h63.25c1.41 0 2.82.355 3.879 1.066l-6.168-12.98c-1.762-3.73-4.582-5.153-7.398-5.153h-6.876L42.2 36.623c-.707.71-1.586 1.245-2.469 1.6-.878.356-1.937.532-2.82.532-1.055 0-1.937-.176-2.816-.531h-.352c-.707-.356-1.41-.891-2.117-1.422l-9.867-10.668h-6.871c-2.817 0-5.461 1.601-7.399 5.156L1.32 44.266c1.235-.532 2.47-.887 4.055-.887zm0 0"/><path d="M51.71 21.332c.352-.355.532-.71.884-1.242.176-.535.351-.89.351-1.602 0-.531-.175-1.066-.351-1.422-.176-.53-.532-.886-.883-1.246a5.273 5.273 0 0 0-1.23-.886c-.356-.18-.883-.356-1.41-.356-.532 0-1.06.176-1.41.356-.528.175-.884.53-1.235.886l-5.637 5.692V3.734c0-.535-.176-1.066-.352-1.421-.18-.536-.53-.891-.882-1.247-.352-.355-.703-.71-1.235-.886C37.97 0 37.441 0 36.91 0c-.527 0-1.055 0-1.406.18-.531.175-.883.53-1.234.886-.352.356-.708.711-.883 1.246-.176.532-.352.887-.352 1.422v17.953L27.398 16c-.351-.355-.707-.71-1.234-.89-.352-.176-.879-.356-1.41-.356-.527 0-1.055.18-1.41.355-.352.18-.88.536-1.23.891-.356.355-.708.71-.884 1.246-.175.531-.351.887-.351 1.422 0 .531.176 1.066.351 1.598.176.535.528.89.883 1.246L34.27 33.957c.351.355.703.711 1.234.887.351.18.879.355 1.406.355.531 0 1.059-.176 1.41-.355.532-.176.883-.532 1.235-.887zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/vob.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/vox.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><g fill="#039"><path d="m31.793 20.445 11.852 8.008V.246L31.793 8.254V4.25c0-2.184-1.797-4.004-3.953-4.004H3.953C1.797.246 0 2.066 0 4.25v20.2c0 2.183 1.797 4.003 3.953 4.003H27.84c2.156 0 3.953-1.82 3.953-4.004zm18.32 7.278v12.011c0 4.368 3.59 8.008 7.903 8.008 4.308 0 7.902-3.64 7.902-8.008V27.723c0-4.368-3.594-8.004-7.902-8.004-4.313 0-7.903 3.636-7.903 8.004zm0 0"/><path d="M70.047 39.734c0 6.73-5.387 12.008-11.852 12.008-6.648 0-11.855-5.457-11.855-12.008h-3.953A15.96 15.96 0 0 0 54.242 55.2v8.555h7.903v-8.555A15.955 15.955 0 0 0 74 39.734zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/vscodeignore.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M54.633 9.367C42.145-3.12 21.855-3.12 9.367 9.367s-12.488 32.778 0 45.266 32.778 12.488 45.266 0 12.488-32.778 0-45.266zM12.176 44.801c-5.934-9.211-4.84-21.543 3.12-29.504s20.294-9.055 29.505-3.121zm7.023 7.023L51.824 19.2c5.934 9.211 4.84 21.543-3.12 29.504s-20.294 9.055-29.505 3.121zm0 0" fill="#bababa"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/war.svg
1
1
<svg height="64" width="48" xmlns="http://www.w3.org/2000/svg"><g stroke-miterlimit="10" stroke-width=".5"><path d="M44.2 75.3c7.2-3.701 3.9-7.3 1.5-6.799-.6.099-.801.2-.801.2s.2-.3.601-.5C50.1 66.6 53.6 73 44 75.5zm0 0" fill="#265db4" stroke="#265db4" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M37.8 64.8c1.801 2.1-.5 4-.5 4s4.7-2.4 2.5-5.5c-2-2.8-3.6-4.2 4.8-9.101 0 .101-13.1 3.401-6.8 10.6" fill="#c00" stroke="#c00" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M39.8 46.499s3.999 4-3.8 10.102c-6.2 4.898-1.4 7.7 0 10.899-3.601-3.3-6.3-6.2-4.5-8.8 2.7-4 9.9-5.9 8.3-12.201" fill="#c00" stroke="#c00" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><g fill="#265db4" stroke="#265db4"><path d="M31 76.8s-1.5.9 1 1.1c3 .299 4.6.299 7.9-.3 0 0 .9.599 2.1 1-7.4 3.3-16.901-.1-11-1.8m-.9-4.2s-1.6 1.199.9 1.5c3.2.3 5.8.4 10.2-.5 0 0 .6.6 1.599 1-9.1 2.6-19.199.2-12.698-2" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M47.7 79.9s1.1.9-1.2 1.599c-4.3 1.302-18 1.702-21.8.101-1.4-.6 1.2-1.4 2-1.6.8-.2 1.3-.1 1.3-.1-1.5-1.1-9.8 2.1-4.2 3 15.3 2.4 27.9-1.199 23.9-3M31.7 68.3s-7 1.702-2.499 2.301c1.9.301 5.699.2 9.2-.101 2.9-.2 5.799-.8 5.799-.8s-1 .4-1.8.901c-7.1 1.9-20.7.999-16.8-.9 3.4-1.6 6.1-1.401 6.1-1.401" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/><path d="M32.399 85.4c6.901.4 17.502-.2 17.7-3.5 0 0-.499 1.2-5.699 2.2-5.899 1.1-13.101 1-17.5.3.1 0 1 .7 5.499 1" transform="matrix(1.63687 0 0 1.62288 -34.986 -75.177)"/></g></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/wav.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/wbk.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="60"><path style="stroke:none;fill-rule:nonzero;fill:#999;fill-opacity:1" d="m12.762 33.262-8.39-26.09c-.349-1.059-.524-1.41-.7-1.41-.176-.176-.352-.176-.527-.352l-2.97-.883L0 .824h15.734l.348 3.703-2.973.883v.352c0 .351.176 1.058.528 1.761L16.78 17.57 22.38.824 26.57.648l5.07 16.747 3.497-10.051c.175-.703.527-1.41.527-1.762V5.41l-2.621-.707-.176-3.879h12.235l.351 3.703-3.32 1.059c-.176 0-.352.176-.528.176 0 .176-.347.351-.523 1.234l-9.266 25.91-4.37.356-4.716-16.043-5.597 15.687zm0 0"/><path style="fill-rule:nonzero;fill:#999;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#999;stroke-opacity:1;stroke-miterlimit:10" d="M42.4 48.6H60v2.8H42.4zm0 7.401H60V58.8H42.4zm0 7.7H60V66.5H42.4zm-29.4 7.8h47v2.798H13zm0 7.598h47v2.8H13zm0 0" transform="matrix(1.74818 0 0 1.76287 -21.328 -85.027)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/webinfo.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M18.785 46.805c3.504-1.43 7.164-2.387 10.985-2.547V34.07H16.398c.16 4.297.954 8.754 2.387 12.735zM11.461 7.64c1.594 1.593 3.504 2.867 5.254 3.98 1.594-3.66 3.664-7.164 6.21-10.348-4.3 1.274-8.12 3.344-11.464 6.368zm33.434 9.554c-3.5 1.43-7.165 2.387-10.985 2.547V29.93h13.375c-.16-4.297-.957-8.754-2.39-12.735zM29.93 19.582c-3.82-.316-7.64-1.113-10.985-2.547A43.883 43.883 0 0 0 16.56 29.93h13.37zm-9.551-6.207c3.023 1.273 6.207 1.91 9.55 2.227V0h-.316L27.86 1.91c-3.343 3.344-5.73 7.324-7.48 11.465zM47.285 33.91H33.91v10.188c3.82.32 7.64 1.117 10.985 2.55 1.433-3.824 2.23-8.28 2.39-12.738zM33.75 15.762c3.344-.32 6.527-.957 9.555-2.23-1.91-4.298-4.457-8.118-7.485-11.622L34.547.16h-.637zm18.629-8.278c-3.344-2.867-7.324-5.097-11.621-6.21a45.734 45.734 0 0 1 6.207 10.347c1.91-1.113 3.66-2.387 5.414-4.137zm-22.45 40.754c-3.343.32-6.527.957-9.55 2.23 1.91 4.137 4.297 8.118 7.324 11.462l1.59 1.75h.477zM12.419 30.09c.16-5.094 1.273-9.871 2.707-14.649-2.39-1.273-4.457-2.863-6.687-4.933l-.16-.16C3.503 15.602.32 22.449 0 30.09zm38.848 3.82c-.16 5.094-1.278 9.871-2.707 14.649 2.386 1.273 4.457 2.863 6.683 4.933l.32.32C60.34 48.56 63.523 41.712 64 34.07c-.32-.16-12.734-.16-12.734-.16zm3.82-23.402c-1.91 1.91-4.3 3.504-6.688 4.933 1.75 4.618 2.707 9.555 2.707 14.649H63.68c-.477-7.64-3.5-14.488-8.438-19.742zm-2.867 45.851c-1.594-1.593-3.504-2.867-5.254-3.98a45.734 45.734 0 0 1-6.207 10.348c4.297-1.274 8.277-3.344 11.46-6.368zM8.598 53.492c1.91-1.91 4.297-3.504 6.527-4.933-1.75-4.618-2.707-9.555-2.867-14.649H0c.477 7.64 3.504 14.488 8.277 19.742zm8.117-1.113c-1.91 1.113-3.66 2.547-5.254 3.98 3.344 2.864 7.324 5.094 11.625 6.207-2.707-3.023-4.777-6.367-6.371-10.187zm26.59-1.754c-3.028-1.273-6.211-1.91-9.555-2.227V64h.48l1.75-1.91c3.184-3.344 5.57-7.485 7.325-11.465zm0 0" fill="#bababa"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/webm.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/webp.svg
1
1
<svg height="64" width="82" xmlns="http://www.w3.org/2000/svg"><g fill="#3c3"><path d="M0 .094v63.812h82V.094zM77.34 4.8v27.86L57.773 17.6 36.34 39.247l-15.094-8.469L4.844 43.953V4.801zm0 0"/><path d="M22.55 17.223c0 3.222-2.585 5.836-5.777 5.836-3.191 0-5.777-2.614-5.777-5.836s2.586-5.836 5.777-5.836c3.192 0 5.778 2.613 5.778 5.836zm0 0"/></g></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/wma.svg
1
1
<svg height="64" width="57" xmlns="http://www.w3.org/2000/svg"><path d="M49.766 32.992c-5.68-2.187-12.278.547-14.66 6.196-2.2 5.652.55 12.21 6.23 14.582 1.102.546 2.383.73 3.668.73 6.414.18 11.726-4.742 11.726-10.938V0c-.363 0-.546.184-.914.184-12.464 3.46-24.742 7.105-37.207 10.57-1.097.363-1.097.91-1.097 1.824V43.2c-.918-.367-1.285-.547-2.016-.73-4.582-1.64-8.617-.73-11.914 2.55-3.3 3.098-4.215 7.84-2.383 12.032 2.383 5.648 8.98 8.383 14.66 6.195 4.22-1.82 6.965-5.832 6.965-10.387V24.426c0-1.278.367-1.64 1.653-2.008 6.046-1.64 12.093-3.461 18.328-5.102l8.797-2.55v18.953c-.918-.364-1.286-.547-1.836-.727zm0 0" fill="#039"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/wmf.svg
1
1
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg"><path d="M16.223 21.805.09 55.844l3.012 3.015 20.035-20.035c-.711-1.594-.532-3.543.886-4.96 1.774-1.774 4.43-1.774 6.204 0 1.773 1.769 1.773 4.429 0 6.202-1.243 1.243-3.368 1.594-4.965.887L5.23 60.984 8.242 64l34.04-16.133L49.73 27.48 36.61 14.36zm46.625-4.075L46.184 1.062c-1.418-1.417-3.547-1.417-4.965 0L37.32 4.966c-1.422 1.418-1.422 3.543 0 4.965l16.664 16.664c1.418 1.418 3.543 1.418 4.965 0l3.899-3.903c1.418-1.418 1.418-3.543 0-4.96zm0 0" fill="#fea500"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/wmv.svg
1
1
<svg height="64" width="84" xmlns="http://www.w3.org/2000/svg"><path d="M.238 64h83.524V0H.238zm16.16-4.91H6.41V52h9.988zm30.688 0h-9.988V52h9.988zM67.602 4.363h9.988v7.274h-9.988zm0 47.453h9.988v7.094h-9.988zM52.168 4.363h9.805v7.09h-9.985v-7.09zm0 47.453h9.805v7.094h-9.985zM36.914 4.363h9.988v7.09h-9.988zM35.464 22l13.071 7.453c2.363 1.457 2.363 3.637 0 5.094L35.465 42c-2.363 1.453-4.36.184-4.36-2.547v-15.09c0-2.547 1.997-3.816 4.36-2.363zM21.485 4.363h9.805v7.09h-9.805zm0 47.453h9.805v7.094h-9.805zM6.41 4.363h9.988v7.274H6.41zm0 0" fill="#f60"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/woff.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="48"><path style="stroke:none;fill-rule:nonzero;fill:#7291a1;fill-opacity:1" d="M28.621 33.172h-16.32l-2.012 4.45c-.55 1.483-.918 2.593-.918 3.706 0 1.297.547 2.223 1.649 2.781.55.371 2.203.555 4.582.743v1.293H.203v-1.293c1.652-.188 2.934-.93 4.035-2.04 1.098-1.113 2.383-3.34 3.848-6.859L24.586 0h.73L42 36.879c1.648 3.52 2.934 5.746 3.852 6.672.73.742 1.832 1.113 3.296 1.113v1.297h-22.18v-1.297h.919c1.832 0 3.113-.184 3.847-.742.551-.371.735-.926.735-1.48 0-.372 0-.743-.184-1.301 0-.184-.367-1.11-1.101-2.407zm-1.101-2.406-6.786-15.57-7.148 15.57zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#36454d;fill-opacity:1" d="m83.797 16.309-6.602 22.054-.734 2.778c0 .375-.184.558-.184.742 0 .187.184.558.371.742.184.188.368.371.547.371.551 0 1.102-.371 2.016-1.113.371-.367 1.102-1.297 2.387-2.965l1.097.559c-1.648 2.964-3.3 5.003-5.132 6.3-1.833 1.297-3.852 2.04-5.864 2.04-1.285 0-2.203-.372-2.933-.93-.735-.742-1.102-1.485-1.102-2.407 0-.93.367-2.41 1.102-4.82l.73-2.781c-2.562 4.45-5.133 7.601-7.516 9.453C60.516 47.442 59.05 48 57.582 48c-2.016 0-3.668-.926-4.582-2.594-.918-1.668-1.465-3.523-1.465-5.746 0-3.152.914-6.672 2.934-10.75 2.011-4.074 4.582-7.226 7.695-9.82 2.566-2.04 5.133-2.965 7.332-2.965 1.285 0 2.203.367 3.121 1.11.73.742 1.281 2.038 1.649 3.89l1.28-4.074zM72.98 22.797c0-1.856-.367-3.152-.918-3.895-.367-.554-.914-.742-1.648-.742-.734 0-1.469.375-2.2.93-1.464 1.297-3.116 4.074-4.948 8.336-1.832 4.265-2.57 7.785-2.57 10.937 0 1.11.183 2.035.554 2.594.363.559.914.742 1.281.742 1.098 0 2.016-.558 3.117-1.668 1.465-1.668 2.934-3.707 4.032-5.93 2.199-4.449 3.3-8.156 3.3-11.304zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/woff2.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="48"><path style="stroke:none;fill-rule:nonzero;fill:#7291a1;fill-opacity:1" d="M28.621 33.172h-16.32l-2.012 4.45c-.55 1.483-.918 2.593-.918 3.706 0 1.297.547 2.223 1.649 2.781.55.371 2.203.555 4.582.743v1.293H.203v-1.293c1.652-.188 2.934-.93 4.035-2.04 1.098-1.113 2.383-3.34 3.848-6.859L24.586 0h.73L42 36.879c1.648 3.52 2.934 5.746 3.852 6.672.73.742 1.832 1.113 3.296 1.113v1.297h-22.18v-1.297h.919c1.832 0 3.113-.184 3.847-.742.551-.371.735-.926.735-1.48 0-.372 0-.743-.184-1.301 0-.184-.367-1.11-1.101-2.407zm-1.101-2.406-6.786-15.57-7.148 15.57zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#36454d;fill-opacity:1" d="m83.797 16.309-6.602 22.054-.734 2.778c0 .375-.184.558-.184.742 0 .187.184.558.371.742.184.188.368.371.547.371.551 0 1.102-.371 2.016-1.113.371-.367 1.102-1.297 2.387-2.965l1.097.559c-1.648 2.964-3.3 5.003-5.132 6.3-1.833 1.297-3.852 2.04-5.864 2.04-1.285 0-2.203-.372-2.933-.93-.735-.742-1.102-1.485-1.102-2.407 0-.93.367-2.41 1.102-4.82l.73-2.781c-2.562 4.45-5.133 7.601-7.516 9.453C60.516 47.442 59.05 48 57.582 48c-2.016 0-3.668-.926-4.582-2.594-.918-1.668-1.465-3.523-1.465-5.746 0-3.152.914-6.672 2.934-10.75 2.011-4.074 4.582-7.226 7.695-9.82 2.566-2.04 5.133-2.965 7.332-2.965 1.285 0 2.203.367 3.121 1.11.73.742 1.281 2.038 1.649 3.89l1.28-4.074zM72.98 22.797c0-1.856-.367-3.152-.918-3.895-.367-.554-.914-.742-1.648-.742-.734 0-1.469.375-2.2.93-1.464 1.297-3.116 4.074-4.948 8.336-1.832 4.265-2.57 7.785-2.57 10.937 0 1.11.183 2.035.554 2.594.363.559.914.742 1.281.742 1.098 0 2.016-.558 3.117-1.668 1.465-1.668 2.934-3.707 4.032-5.93 2.199-4.449 3.3-8.156 3.3-11.304zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/wps.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="60"><path style="stroke:none;fill-rule:nonzero;fill:#6190aa;fill-opacity:1" d="m12.762 33.262-8.39-26.09c-.349-1.059-.524-1.41-.7-1.41-.176-.176-.352-.176-.527-.352l-2.97-.883L0 .824h15.734l.348 3.703-2.973.883v.352c0 .351.176 1.058.528 1.761L16.78 17.57 22.38.824 26.57.648l5.07 16.747 3.497-10.051c.175-.703.527-1.41.527-1.762V5.41l-2.621-.707-.176-3.879h12.235l.351 3.703-3.32 1.059c-.176 0-.352.176-.528.176 0 .176-.347.351-.523 1.234l-9.266 25.91-4.37.356-4.716-16.043-5.597 15.687zm0 0"/><path style="fill-rule:nonzero;fill:#6190aa;fill-opacity:1;stroke-width:.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#6190aa;stroke-opacity:1;stroke-miterlimit:10" d="M42.4 48.6H60v2.8H42.4zm0 7.401H60V58.8H42.4zm0 7.7H60V66.5H42.4zm-29.4 7.8h47v2.798H13zm0 7.598h47v2.8H13zm0 0" transform="matrix(1.74818 0 0 1.76287 -21.328 -85.027)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/wsf.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M13.875 13.874h10.9v2.701h-10.9zm0 5.4h10.9v2.701h-10.9zm0 5.5h10.9v2.702h-10.9zm19-24.399H11.177c-3 0-5.402 2.4-5.402 5.4v24.4h-5.4c0 3 2.402 5.4 5.4 5.4h21.7c3 0 5.402-2.4 5.402-5.4v-21.7h5.4v-2.7c0-3-2.402-5.4-5.4-5.4zm-2.7 29.3c0 1.801-1.4 3.2-3.2 3.2h-19.9c1.3-.9 1.3-2.7 1.3-2.7v-24.4c0-1.5 1.2-2.7 2.7-2.7 1.501 0 2.7 1.2 2.7 2.7v2.7h16.3zm-13.6-23.9v-2.7h16.3c2.501 0 2.7 1.6 2.7 2.7zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".75" transform="matrix(1.7717 0 0 1.78025 .262 0)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xaml.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="55"><path style="fill-rule:nonzero;fill:#999;fill-opacity:1;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#999;stroke-opacity:1;stroke-miterlimit:10" d="M11.143 25.867c-.5 0-1.1-.2-1.502-.6l-8.298-8.4c-.8-.799-.8-2.1 0-3.001l8.398-8.4c.8-.798 2.101-.798 3.002 0 .8.801.8 2.1 0 3.001l-6.901 6.9 6.9 6.9c.8.8.8 2.098 0 3-.5.4-1.1.6-1.6.6zm25.101 0c-.503 0-1.102-.2-1.502-.6-.8-.8-.8-2.1 0-3l6.901-6.9-6.9-6.9c-.8-.8-.8-2.1 0-3 .8-.8 2.1-.8 2.998 0l8.4 8.399c.801.8.801 2.102 0 3l-8.4 8.4c-.4.4-.999.6-1.5.6zm-16.7 4.1c-.202 0-.403 0-.7-.1-1.102-.399-1.7-1.5-1.3-2.599l8.398-25.1c.402-1.101 1.5-1.702 2.6-1.302 1.1.4 1.7 1.502 1.3 2.6l-8.4 25.101c-.198.901-1.1 1.4-1.899 1.4zm0 0" transform="matrix(1.74792 0 0 1.75607 0 .53)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xcf.svg
1
1
<svg height="64" width="78" xmlns="http://www.w3.org/2000/svg"><path d="M60.387 42.926c.734.367 1.648.738 2.383 1.293 2.382 1.48 4.585 2.957 6.785 4.437.367.184.734.371 1.101.184 2.567-.555 5.317.926 6.051 3.512.55 1.664.734 3.328.918 5.18.184 2.03.184 4.25.367 6.468-3.668-.926-7.152-2.035-9.719-4.805-.734-.742-.918-1.851-1.468-2.773-.184-.188-.368-.555-.551-.555A61.383 61.383 0 0 1 52.5 50.875c-.184-.188-.734-.188-.918 0-4.398 2.219-9.168 3.14-14.121 2.773-6.602-.37-12.652-2.59-18.52-5.363-.918-.555-2.02-1.11-2.937-1.664 4.586-2.219 6.605-5.914 6.605-10.906-1.101-.184-2.203-.184-3.12-.371-.184.926-.184 2.035-.368 3.144-1.098 4.067-4.95 6.285-9.168 5.176C4.453 42.184-.5 35.714.051 29.43c.367-4.621 4.402-7.578 8.8-6.47.735.185 1.47.368 2.204.74.734.366 1.28.738 2.015 1.109.551-.926.918-2.036 1.47-2.957.183-.188 0-.555 0-.743V6.32h.183c.183.184.367.371.55.739 1.649 2.406 3.118 4.808 5.133 6.843 2.383 2.215 4.77 4.067 7.887 4.805.918.184 1.648.184 2.566-.371 3.485-2.219 6.97-2.402 10.637-.555.184.188.55.188.918.188 5.133-1.297 9.902-3.512 13.754-7.395 1.652-1.664 2.934-3.699 3.852-6.101.367-1.11.917-2.035 1.468-3.145.547-.922 1.282-1.476 2.2-1.293 1.101.184 1.648.926 1.832 1.848.367 1.851.734 3.699.917 5.547.184 3.328.184 6.656.184 9.984-.184 6.84-.918 13.68-3.3 20.149-1.102 1.296-2.016 3.328-2.934 5.363zm-9.172-.371c.734-1.11.734-2.22.183-3.328-.914-1.664-2.382-2.957-4.214-3.696H47l.547 1.11c.183.367.367.738.367 1.293.367 1.48.184 2.218-1.281 2.773-.918.371-1.836.555-2.57.738-4.399.739-8.985.739-13.387.555h-1.281c0 .184.183.184.183.184 4.399 1.48 8.8 2.59 13.387 2.96 1.648.184 3.484.184 5.133-.37.55-.184.918 0 1.285.183 4.953 3.328 10.27 5.73 16.137 7.211 1.101.371 1.101.371 1.652-.738.183-.188.183-.371.367-.555-4.953-3.7-10.453-6.285-16.324-8.32zm-15.586-17.38c-.184 2.22.184 3.145 1.465 3.516 1.101.368 2.386 0 2.754-.925.367-.555.367-1.293.367-2.036-.184-.554-.735-1.109-1.102-1.664 2.2.188 3.485 1.48 4.219 3.516.547-1.852-.367-5.547-3.117-7.395-2.938-1.851-6.973-1.48-9.356 1.11-2.382 2.586-2.566 6.469-.734 9.426 1.836 2.773 5.504 3.699 7.152 2.957-2.015-.739-3.3-2.215-3.484-4.434 0-1.664.55-2.96 1.836-4.07zm-13.387 7.028c-3.3-1.848-4.035-5.363-1.652-7.027-.367 1.48 0 2.406 1.101 2.96.735.368 2.016.184 2.383-.741.367-.555.551-1.293.367-1.848s-.734-.926-1.101-1.664c.918.37 1.652.555 2.387.926.546-1.664.546-1.664 0-2.403-1.653-1.48-3.485-2.035-5.504-1.48-2.2.554-3.485 2.035-4.035 4.25-.184.926-.368 1.851.183 2.773l1.652 2.774c.184.187.184.554.551.554.914.743 2.2.926 3.668.926zM2.984 29.984c0 .926.368 2.036 1.102 2.957 1.098 1.293 2.934 1.293 4.219 0 1.832-1.847 1.648-4.992-.367-6.656-.551-.555-1.286-.738-2.02-.555-1.832.188-3.117 1.852-2.934 4.254zm0 0" fill="#3c3"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xfl.svg
1
1
<svg height="63" width="49" xmlns="http://www.w3.org/2000/svg"><path d="M4.524 3.224v10.102h8.5v2.598h-8.5v13.7h-3.9V.626h13.301v2.598zm14.402 26.3V.826h3.7v28.7zm0 0" fill="#d10407" stroke="#d10407" stroke-miterlimit="10" stroke-width="1.25" transform="matrix(2.10753 0 0 2.07742 0 .079)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xlm.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="62"><path style="stroke:none;fill-rule:nonzero;fill:#1f7244;fill-opacity:1" d="M61.328.137H84v15.152H61.328zm0 23.383H84v15.152H61.328zm0 23.19H84v15.153H61.328zm-30.664 0h22.672v15.153H30.664zM0 46.71h22.672v15.153H0zm50.363-8.98L35.496 18.84 49.06 1.633H35.684l-7.067 9.351-6.5-9.347H8.18l13.746 17.578L7.434 37.73H21l7.617-10.285 7.621 10.285zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xls.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="62"><path style="stroke:none;fill-rule:nonzero;fill:#1f7244;fill-opacity:1" d="M61.328.137H84v15.152H61.328zm0 23.383H84v15.152H61.328zm0 23.19H84v15.153H61.328zm-30.664 0h22.672v15.153H30.664zM0 46.71h22.672v15.153H0zm50.363-8.98L35.496 18.84 49.06 1.633H35.684l-7.067 9.351-6.5-9.347H8.18l13.746 17.578L7.434 37.73H21l7.617-10.285 7.621 10.285zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xlsm.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="62"><path style="stroke:none;fill-rule:nonzero;fill:#1f7244;fill-opacity:1" d="M61.328.137H84v15.152H61.328zm0 23.383H84v15.152H61.328zm0 23.19H84v15.153H61.328zm-30.664 0h22.672v15.153H30.664zM0 46.71h22.672v15.153H0zm50.363-8.98L35.496 18.84 49.06 1.633H35.684l-7.067 9.351-6.5-9.347H8.18l13.746 17.578L7.434 37.73H21l7.617-10.285 7.621 10.285zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xlsx.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="62"><path style="stroke:none;fill-rule:nonzero;fill:#1f7244;fill-opacity:1" d="M61.328.137H84v15.152H61.328zm0 23.383H84v15.152H61.328zm0 23.19H84v15.153H61.328zm-30.664 0h22.672v15.153H30.664zM0 46.71h22.672v15.153H0zm50.363-8.98L35.496 18.84 49.06 1.633H35.684l-7.067 9.351-6.5-9.347H8.18l13.746 17.578L7.434 37.73H21l7.617-10.285 7.621 10.285zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xlt.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="62"><path style="stroke:none;fill-rule:nonzero;fill:#1f7244;fill-opacity:1" d="M61.328.137H84v15.152H61.328zm0 23.383H84v15.152H61.328zm0 23.19H84v15.153H61.328zm-30.664 0h22.672v15.153H30.664zM0 46.71h22.672v15.153H0zm50.363-8.98L35.496 18.84 49.06 1.633H35.684l-7.067 9.351-6.5-9.347H8.18l13.746 17.578L7.434 37.73H21l7.617-10.285 7.621 10.285zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xltm.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="62"><path style="stroke:none;fill-rule:nonzero;fill:#1f7244;fill-opacity:1" d="M61.328.137H84v15.152H61.328zm0 23.383H84v15.152H61.328zm0 23.19H84v15.153H61.328zm-30.664 0h22.672v15.153H30.664zM0 46.71h22.672v15.153H0zm50.363-8.98L35.496 18.84 49.06 1.633H35.684l-7.067 9.351-6.5-9.347H8.18l13.746 17.578L7.434 37.73H21l7.617-10.285 7.621 10.285zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xltx.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="62"><path style="stroke:none;fill-rule:nonzero;fill:#1f7244;fill-opacity:1" d="M61.328.137H84v15.152H61.328zm0 23.383H84v15.152H61.328zm0 23.19H84v15.153H61.328zm-30.664 0h22.672v15.153H30.664zM0 46.71h22.672v15.153H0zm50.363-8.98L35.496 18.84 49.06 1.633H35.684l-7.067 9.351-6.5-9.347H8.18l13.746 17.578L7.434 37.73H21l7.617-10.285 7.621 10.285zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xml.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="55"><path style="fill-rule:nonzero;fill:#666;fill-opacity:1;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#666;stroke-opacity:1;stroke-miterlimit:10" d="M11.241 25.867c-.498 0-1.1-.2-1.5-.6l-8.398-8.4c-.8-.799-.8-2.1 0-3.001l8.398-8.4c.8-.798 2.101-.798 3.002 0 .8.801.8 2.1 0 3.001l-6.901 6.9 6.9 6.9c.8.8.8 2.098 0 3-.5.4-.998.6-1.499.6zm25 0c-.5 0-1.099-.2-1.499-.6-.8-.8-.8-2.1 0-3l6.901-6.9-6.9-6.9c-.8-.8-.8-2.1 0-3 .8-.8 2.1-.8 2.998 0l8.4 8.399c.801.8.801 2.102 0 3l-8.4 8.4c-.4.4-.898.6-1.5.6zm-16.698 4.1c-.2 0-.402 0-.7-.1-1.1-.399-1.7-1.5-1.3-2.599l8.399-25.1c.402-1.101 1.5-1.702 2.6-1.302 1.1.4 1.7 1.502 1.3 2.6l-8.4 25.101c-.198.901-1 1.4-1.899 1.4zm0 0" transform="matrix(1.74792 0 0 1.75607 0 .53)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xpi.svg
1
1
<svg height="64" width="67" xmlns="http://www.w3.org/2000/svg"><path d="M46.406 0c6.367 4.457 10.078 16.043 10.078 24.781 0 2.317-.351 4.457-.882 6.418-.532-5.172-3.008-9.629-6.895-12.48 2.121 2.851 3.36 6.418 3.36 10.34 0 9.625-7.782 17.468-17.329 17.468-3.89 0-5.836-.71-8.664-2.851 5.836 0 9.547-5.883 14.5-5.883 0 0-.71-2.852-4.422-2.852-3.715 0-1.945 2.852-8.664 2.852S17.41 33.695 17.41 30.484c0-3.207 4.774-5.527 5.836-4.457 1.059-1.07 0-2.851 0-2.851l8.664-5.883h-2.832c-12.906 0-5.48-9.094-2.828-11.59-4.598 0-7.426 4.281-8.664 5.883-.707-.356-5.832-.356-7.246 0-.707.18-1.594-1.066-2.3-2.492-1.06-1.961-1.946-4.637-1.946-6.242-3.711 3.746-3.004 9.27-1.59 11.41l-.176.18C1.676 19.253.262 24.601.262 30.483.262 49.024 15.113 64 33.5 64s33.238-13.547 33.238-32.09V29.06C66.563 11.766 55.602 2.852 46.406 0zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xps.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="55"><path style="fill-rule:nonzero;fill:#999;fill-opacity:1;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#999;stroke-opacity:1;stroke-miterlimit:10" d="M11.143 25.867c-.5 0-1.1-.2-1.502-.6l-8.298-8.4c-.8-.799-.8-2.1 0-3.001l8.398-8.4c.8-.798 2.101-.798 3.002 0 .8.801.8 2.1 0 3.001l-6.901 6.9 6.9 6.9c.8.8.8 2.098 0 3-.5.4-1.1.6-1.6.6zm25.101 0c-.503 0-1.102-.2-1.502-.6-.8-.8-.8-2.1 0-3l6.901-6.9-6.9-6.9c-.8-.8-.8-2.1 0-3 .8-.8 2.1-.8 2.998 0l8.4 8.399c.801.8.801 2.102 0 3l-8.4 8.4c-.4.4-.999.6-1.5.6zm-16.7 4.1c-.202 0-.403 0-.7-.1-1.102-.399-1.7-1.5-1.3-2.599l8.398-25.1c.402-1.101 1.5-1.702 2.6-1.302 1.1.4 1.7 1.502 1.3 2.6l-8.4 25.101c-.198.901-1.1 1.4-1.899 1.4zm0 0" transform="matrix(1.74792 0 0 1.75607 0 .53)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xrb.svg
1
1
<svg height="64" width="74" xmlns="http://www.w3.org/2000/svg"><path d="m49.332 34.941-12.25-5.714L61.75 17.633 74 23.348l-12.25 5.879zM61.75 6.207 49.5.492 37.25 6.207l24.5 11.594L74 12.086zm-37.082 17.14-12.25-5.714-12.25 5.715L24.836 34.94l12.246-5.714zm0-11.429 12.25-5.711L24.668.492 0 12.086 12.25 17.8zM61.75 32.59l-11.074 5.039-1.344.672-1.34-.672-11.074-5.04-11.078 5.04-1.34.672-1.344-.672-11.074-5.04v17.977L36.75 63.508l25-12.942zm0 0" fill="#55486d"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xsd.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M26.555 0h15.89v16h-15.89zm0 48h15.89v16h-15.89zM.015 48h15.887v16H.016zm53.083 0h15.886v16H53.098zM37.207 29.273V18.727h-5.414v10.546H5.25v16h5.418V34.727h21.125v10.546h5.414V34.727h21.125v10.546h5.418v-16zm0 0" fill="#999"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xsl.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="55"><path style="fill-rule:nonzero;fill:#999;fill-opacity:1;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:#999;stroke-opacity:1;stroke-miterlimit:10" d="M11.143 25.867c-.5 0-1.1-.2-1.502-.6l-8.298-8.4c-.8-.799-.8-2.1 0-3.001l8.398-8.4c.8-.798 2.101-.798 3.002 0 .8.801.8 2.1 0 3.001l-6.901 6.9 6.9 6.9c.8.8.8 2.098 0 3-.5.4-1.1.6-1.6.6zm25.101 0c-.503 0-1.102-.2-1.502-.6-.8-.8-.8-2.1 0-3l6.901-6.9-6.9-6.9c-.8-.8-.8-2.1 0-3 .8-.8 2.1-.8 2.998 0l8.4 8.399c.801.8.801 2.102 0 3l-8.4 8.4c-.4.4-.999.6-1.5.6zm-16.7 4.1c-.202 0-.403 0-.7-.1-1.102-.399-1.7-1.5-1.3-2.599l8.398-25.1c.402-1.101 1.5-1.702 2.6-1.302 1.1.4 1.7 1.502 1.3 2.6l-8.4 25.101c-.198.901-1.1 1.4-1.899 1.4zm0 0" transform="matrix(1.74792 0 0 1.75607 0 .53)"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xspf.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="stroke:none;fill-rule:nonzero;fill:#039;fill-opacity:1" d="M4.059 10.39h40.254c2.109 0 3.69-1.613 3.69-3.761 0-2.149-1.581-3.758-3.69-3.758H4.059c-2.11 0-3.692 1.61-3.692 3.758 0 2.152 1.582 3.762 3.692 3.762zm0 19.891h40.254c2.109 0 3.69-1.613 3.69-3.765 0-2.149-1.581-3.762-3.69-3.762H4.059c-2.11 0-3.692 1.613-3.692 3.762 0 2.148 1.582 3.765 3.692 3.765zm19.336 10.57H4.059c-2.11 0-3.692 1.614-3.692 3.762 0 2.149 1.582 3.766 3.692 3.766h19.336c2.109 0 3.69-1.617 3.69-3.766 0-2.148-1.581-3.761-3.69-3.761zm0 0"/><path style="stroke:none;fill-rule:nonzero;fill:#039;fill-opacity:1" d="M70.68 9.496c-2.813-1.434-6.504-3.582-7.91-6.629C62.77 1.254 61.54 0 59.957 0c-1.582 0-2.812 1.254-2.812 2.867v38.52c-2.989-1.614-8.614-1.075-12.833 1.433-6.68 3.766-9.492 10.93-6.68 15.766 2.813 4.84 10.723 5.914 17.4 2.152 4.573-2.687 7.738-6.988 7.913-11.289V16.305c9.492 0 15.29 3.941 13.18 13.437-.352 1.793-1.05 3.403-1.754 5.195-.355.54-.355 1.254.176 1.793.527.536 1.402.356 2.11-.359 3.515-3.582 5.796-8.242 5.976-13.437-.18-6.805-6.508-10.75-11.953-13.438zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/xz.svg
1
1
<svg height="63" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M53.344 18.172H44.02V8.965zM28.309 8.965v33.437h25.199V20.434H41.727V8.964zm3.93-8.723H4.417v6.461h10.965l-6.875 5.332v5.652l10.148-7.753V6.867H54V4.281zM18.655 14.297 8.508 22.05v5.652l10.148-7.754zM8.344 37.559l10.148-7.754v-5.657L8.344 31.902zm10.312 2.261v-5.656L8.508 41.918v2.91h-4.09v6.461h6.219v4.523H7.035c-.652-1.132-1.797-1.937-3.273-1.937C1.637 53.875 0 55.488 0 57.59c0 2.097 1.637 3.715 3.762 3.715 1.476 0 2.62-.809 3.273-1.938h3.602v3.39h5.562v-3.39h3.602c.652 1.13 1.8 1.938 3.273 1.938 2.125 0 3.762-1.618 3.762-3.715 0-2.102-1.637-3.715-3.762-3.715-1.472 0-2.62.805-3.273 1.938h-3.602v-4.524h15.875l21.762-3.879v-2.582H11.78zm0 0" fill="#90c"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/yaml.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="83" height="63"><path style="stroke:none;fill-rule:nonzero;fill:#999;fill-opacity:1" d="M.125 0h69.586v8.184H.125zm13.164 18.273h69.586v8.18H13.289zM.125 36.543h69.586v8.184H.125zm13.164 18.273h69.586V63H13.289zm0 0"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/z.svg
1
<svg height="63" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M53.344 18.172H44.02V8.965zM28.309 8.965v33.437h25.199V20.434H41.727V8.964zm3.93-8.723H4.417v6.461h10.965l-6.875 5.332v5.652l10.148-7.753V6.867H54V4.281zM18.655 14.297 8.508 22.05v5.652l10.148-7.754zM8.344 37.559l10.148-7.754v-5.657L8.344 31.902zm10.312 2.261v-5.656L8.508 41.918v2.91h-4.09v6.461h6.219v4.523H7.035c-.652-1.132-1.797-1.937-3.273-1.937C1.637 53.875 0 55.488 0 57.59c0 2.097 1.637 3.715 3.762 3.715 1.476 0 2.62-.809 3.273-1.938h3.602v3.39h5.562v-3.39h3.602c.652 1.13 1.8 1.938 3.273 1.938 2.125 0 3.762-1.618 3.762-3.715 0-2.102-1.637-3.715-3.762-3.715-1.472 0-2.62.805-3.273 1.938h-3.602v-4.524h15.875l21.762-3.879v-2.582H11.78zm0 0" fill="#90c"/></svg>
12
A src/main/resources/com/keenwrite/ui/fonts/icons/zip.svg
1
1
<svg height="63" width="54" xmlns="http://www.w3.org/2000/svg"><path d="M53.344 18.172H44.02V8.965zM28.309 8.965v33.437h25.199V20.434H41.727V8.964zm3.93-8.723H4.417v6.461h10.965l-6.875 5.332v5.652l10.148-7.753V6.867H54V4.281zM18.655 14.297 8.508 22.05v5.652l10.148-7.754zM8.344 37.559l10.148-7.754v-5.657L8.344 31.902zm10.312 2.261v-5.656L8.508 41.918v2.91h-4.09v6.461h6.219v4.523H7.035c-.652-1.132-1.797-1.937-3.273-1.937C1.637 53.875 0 55.488 0 57.59c0 2.097 1.637 3.715 3.762 3.715 1.476 0 2.62-.809 3.273-1.938h3.602v3.39h5.562v-3.39h3.602c.652 1.13 1.8 1.938 3.273 1.938 2.125 0 3.762-1.618 3.762-3.715 0-2.102-1.637-3.715-3.762-3.715-1.472 0-2.62.805-3.273 1.938h-3.602v-4.524h15.875l21.762-3.879v-2.582H11.78zm0 0" fill="#90c"/></svg>
A src/main/resources/com/keenwrite/ui/fonts/icons/zsh.svg
1
1
<svg height="64" width="69" xmlns="http://www.w3.org/2000/svg"><path d="M13.875 13.874h10.9v2.701h-10.9zm0 5.4h10.9v2.701h-10.9zm0 5.5h10.9v2.702h-10.9zm19-24.399H11.177c-3 0-5.402 2.4-5.402 5.4v24.4h-5.4c0 3 2.402 5.4 5.4 5.4h21.7c3 0 5.402-2.4 5.402-5.4v-21.7h5.4v-2.7c0-3-2.402-5.4-5.4-5.4zm-2.7 29.3c0 1.801-1.4 3.2-3.2 3.2h-19.9c1.3-.9 1.3-2.7 1.3-2.7v-24.4c0-1.5 1.2-2.7 2.7-2.7 1.501 0 2.7 1.2 2.7 2.7v2.7h16.3zm-13.6-23.9v-2.7h16.3c2.501 0 2.7 1.6 2.7 2.7zm0 0" fill="#999" stroke="#999" stroke-miterlimit="10" stroke-width=".75" transform="matrix(1.7717 0 0 1.78025 .262 0)"/></svg>
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/emoji/OpenSansEmoji-Regular.ttf
Binary file
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/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-4/SourceSerif4-Bold.otf
Binary file
A src/main/resources/fonts/source-serif-4/SourceSerif4-BoldItalic.otf
Binary file
A src/main/resources/fonts/source-serif-4/SourceSerif4-Italic.otf
Binary file
A src/main/resources/fonts/source-serif-4/SourceSerif4-Regular.otf
Binary file
A src/main/resources/lexicons/README.md
1
# Lexicons
2
3
This directory contains lexicons used for spell checking. Each lexicon
4
file contains tab-delimited word-frequency pairs.
5
6
Compiling a high-quality list of correctly spelled words requires the
7
following steps:
8
9
1. Download a unigram frequency list for all words for a given language.
10
1. Download a high-quality source list of correctly spelled words.
11
1. Filter the unigram frequency list using all words in the source list.
12
1. Sort the filtered list by the frequency in descending order.
13
14
The latter steps can be accomplished as follows:
15
16
    # Extract unigram and frequency based on existence in source lexicon.
17
    for i in $(cat source-lexicon.txt); do
18
      grep -m 1 "^$i"$'\t' unigram-frequencies.txt;
19
    done > filtered.txt
20
21
    # Sort numerically (-n) using column two (-k2) in reverse order (-r).
22
    sort -n -k2 -r filtered.txt > en.txt
23
24
There may be more efficient ways to filter the data, which takes a few hours
25
to complete (on modern hardware).
26
27
# Resources
28
29
There are numerous sources of word and frequency lists available, including:
30
31
* https://storage.googleapis.com/books/ngrams/books/datasetsv3.html
32
* https://github.com/hermitdave/FrequencyWords/
33
* https://github.com/neilk/wordfrequencies
34
135
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
1
'aight
2
ain't
3
amn't
4
aren't
5
can't
6
'cause
7
couldn't
8
couldn't've
9
could've
10
daren't
11
daresn't
12
dasn't
13
didn't
14
doesn't
15
don't
16
dunno
17
d'ye
18
e'er
19
everybody's
20
everyone's
21
g'day
22
gimme
23
giv'n
24
gonna
25
gon't
26
gotta
27
hadn't
28
had've
29
hasn't
30
haven't
31
he'd
32
he'll
33
he's
34
he've
35
how'd
36
howdy
37
how'll
38
how're
39
how's
40
how've
41
i'd
42
i'dn't've
43
i'd've
44
i'll
45
i'm
46
i'm'a
47
imma
48
innit
49
isn't
50
it'd
51
it'll
52
it's
53
i've
54
let's
55
ma'am
56
mayn't
57
may've
58
methinks
59
mightn't
60
might've
61
mustn't
62
mustn't've
63
must've
64
needn't
65
ne'er
66
o'clock
67
o'er
68
ol'
69
oughtn't
70
shalln't
71
shan't
72
she'd
73
she'll
74
she's
75
shouldn't
76
shouldn't've
77
should've
78
somebody's
79
someone's
80
something's
81
so're
82
that'd
83
that'll
84
that're
85
that's
86
there'd
87
there'll
88
there're
89
there's
90
these'd
91
these'll
92
these're
93
these've
94
they'd
95
they'll
96
they're
97
they've
98
this's
99
those're
100
those've
101
'tis
102
to've
103
'twas
104
'twouldn't
105
wanna
106
wasn't
107
we'd
108
we'd've
109
we'll
110
we're
111
weren't
112
we've
113
what'd
114
what'll
115
what're
116
what's
117
what've
118
when'd
119
when'll
120
when's
121
where'd
122
where'll
123
where're
124
where's
125
where've
126
which'd
127
which'll
128
which're
129
which's
130
which've
131
who'd
132
who'd've
133
who'll
134
who're
135
who's
136
who've
137
why'd
138
why'll
139
why're
140
why's
141
willn't
142
won't
143
wouldn't
144
wouldn't've
145
would've
146
y'all
147
y'all'd've
148
y'all're
149
you'd
150
you'dn't've
151
you'll
152
you're
153
you've
1154
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/AwaitFxExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import javafx.embed.swing.JFXPanel;
5
import org.junit.jupiter.api.extension.BeforeAllCallback;
6
import org.junit.jupiter.api.extension.ExtensionContext;
7
import org.testfx.osgi.service.TestFx;
8
9
import java.util.concurrent.Semaphore;
10
11
import static javafx.application.Platform.runLater;
12
import static javax.swing.SwingUtilities.invokeLater;
13
14
/**
15
 * Blocks all unit tests until JavaFX is ready.
16
 */
17
public class AwaitFxExtension implements BeforeAllCallback {
18
  /**
19
   * Prevent {@link RuntimeException} for internal graphics not initialized yet.
20
   *
21
   * @param context Provided by the {@link TestFx} framework.
22
   * @throws InterruptedException Could not acquire semaphore.
23
   */
24
  @Override
25
  public void beforeAll( final ExtensionContext context )
26
    throws InterruptedException {
27
    final var semaphore = new Semaphore( 0 );
28
29
    invokeLater( () -> {
30
      // Prepare JavaFX toolkit and environment.
31
      new JFXPanel();
32
      runLater( semaphore::release );
33
    } );
34
35
    semaphore.acquire();
36
  }
37
}
138
A src/test/java/com/keenwrite/definition/TreeViewTest.java
1
/* Copyright 2020-2021 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.assertj.core.util.Files;
22
import org.testfx.framework.junit5.Start;
23
24
import static com.keenwrite.util.FontLoader.initFonts;
25
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
    final var transformer = new YamlTreeTransformer();
51
    final var editor = new DefinitionEditor( transformer );
52
    final var file = Files.newTemporaryFile();
53
54
    final var tabPane1 = new DetachableTabPane();
55
    tabPane1.addTab( "Editor", editor );
56
57
    final var tabPane2 = new DetachableTabPane();
58
    final var tab21 =
59
      tabPane2.addTab( "Picker", new ColorPicker() );
60
    final var tab22 =
61
      tabPane2.addTab( "Editor", new MarkdownEditor( file, workspace ) );
62
    tab21.setTooltip( new Tooltip( "Colour Picker" ) );
63
    tab22.setTooltip( new Tooltip( "Text Editor" ) );
64
65
    final var tabPane3 = new DetachableTabPane();
66
    tabPane3.addTab( "Preview", new HtmlPreview( workspace ) );
67
68
    editor.addTreeChangeHandler( mTreeHandler );
69
70
    mainPane.getItems().addAll( tabPane1, tabPane2, tabPane3 );
71
72
    stage.setScene( new Scene( mainPane ) );
73
    stage.show();
74
  }
75
}
176
A src/test/java/com/keenwrite/editors/markdown/MarkdownEditorTest.java
1
package com.keenwrite.editors.markdown;
2
3
import com.keenwrite.AwaitFxExtension;
4
import com.keenwrite.preferences.Workspace;
5
import org.assertj.core.util.Files;
6
import org.junit.jupiter.api.Test;
7
import org.junit.jupiter.api.extension.ExtendWith;
8
import org.testfx.framework.junit5.ApplicationExtension;
9
10
import java.io.File;
11
import java.util.regex.Pattern;
12
13
import static java.util.regex.Pattern.compile;
14
import static javafx.application.Platform.runLater;
15
import static org.junit.jupiter.api.Assertions.assertEquals;
16
import static org.junit.jupiter.api.Assertions.assertTrue;
17
18
@ExtendWith( {ApplicationExtension.class, AwaitFxExtension.class} )
19
public class MarkdownEditorTest {
20
  private static final File TEMP_FILE = Files.newTemporaryFile();
21
22
  private static final String[] WORDS = new String[]{
23
    "Italicize",
24
    "English's",
25
    "foreign",
26
    "words",
27
    "based",
28
    "on",
29
    "popularity,",
30
    "like",
31
    "_bête_",
32
    "_noire_",
33
    "and",
34
    "_Weltanschauung_",
35
    "but",
36
    "not",
37
    "résumé.",
38
    "Don't",
39
    "omit",
40
    "accented",
41
    "characters!",
42
    "Cœlacanthe",
43
    "L'Haÿ-les-Roses",
44
    "Mühlfeldstraße",
45
    "Da̱nx̱a̱laga̱litła̱n",
46
  };
47
48
  private static final String TEXT = String.join( " ", WORDS );
49
50
  private static final Pattern REGEX = compile(
51
    "[^\\p{Mn}\\p{Me}\\p{L}\\p{N}'-]+" );
52
53
  /**
54
   * Test that the {@link MarkdownEditor} can retrieve a word at the caret
55
   * position, regardless of whether the caret is at the beginning, middle, or
56
   * end of the word.
57
   */
58
  @Test
59
  public void test_CaretWord_GetISO88591Word_WordSelected() {
60
    runLater( () -> {
61
      final var editor = createMarkdownEditor();
62
63
      for( int i = 0; i < WORDS.length; i++ ) {
64
        final var word = WORDS[ i ];
65
        final var len = word.length();
66
        final var expected = REGEX.matcher( word ).replaceAll( "" );
67
68
        for( int j = 0; j < len; j++ ) {
69
          editor.moveTo( offset( i ) + j );
70
          final var actual = editor.getCaretWordText();
71
          assertEquals( expected, actual );
72
        }
73
      }
74
    } );
75
  }
76
77
  /**
78
   * Test that the {@link MarkdownEditor} can make a word bold.
79
   */
80
  @Test
81
  public void test_CaretWord_SetWordBold_WordIsBold() {
82
    final var index = 20;
83
    final var editor = createMarkdownEditor();
84
85
    editor.moveTo( offset( index ) );
86
    editor.bold();
87
    assertTrue( editor.getText().contains( "**" + WORDS[ index ] + "**" ) );
88
  }
89
90
  /**
91
   * Returns the document offset for a string at the given index.
92
   */
93
  private static int offset( final int index ) {
94
    assert 0 <= index && index < WORDS.length;
95
    int offset = 0;
96
97
    for( int i = 0; i < index; i++ ) {
98
      offset += WORDS[ i ].length();
99
    }
100
101
    // Add the index to compensate for one space between words.
102
    return offset + index;
103
  }
104
105
  /**
106
   * Returns an instance of {@link MarkdownEditor} pre-populated with
107
   * {@link #TEXT}.
108
   *
109
   * @return A new {@link MarkdownEditor} instance, ready for unit tests.
110
   */
111
  private MarkdownEditor createMarkdownEditor() {
112
    final var workspace = new Workspace();
113
    final var editor = new MarkdownEditor( TEMP_FILE, workspace );
114
    editor.setText( TEXT );
115
    return editor;
116
  }
117
}
1118
A src/test/java/com/keenwrite/encoding/EncodingTest.java
1
package com.keenwrite.encoding;
2
3
import com.keenwrite.util.EncodingDetector;
4
import org.junit.jupiter.api.Test;
5
6
import java.nio.charset.Charset;
7
import java.nio.charset.StandardCharsets;
8
9
import static java.nio.charset.StandardCharsets.*;
10
import static org.junit.jupiter.api.Assertions.assertEquals;
11
12
public class EncodingTest {
13
  @Test
14
  @SuppressWarnings( "UnnecessaryLocalVariable" )
15
  public void test_Encoding_UTF8_UTF8() {
16
    final var bytes = testBytes();
17
    final var detector = new EncodingDetector();
18
    final var expectedCharset = UTF_8;
19
    final var actualCharset = detector.detect( bytes );
20
21
    assertEquals( expectedCharset, actualCharset );
22
  }
23
24
  private static byte[] testBytes() {
25
    return
26
      """
27
        One humid afternoon during the harrowing heatwave of 2060, Renato
28
        Salvatierra, a man with blood sausage fingers and a footfall that
29
        silenced rooms, received a box at his police station. Taped to the
30
        box was a ransom note; within were his wife's eyes. By year's end,
31
        a supermax prison overflowed with felons, owing to Salvatierra's
32
        efforts to find his beloved. Soon after, he flipped profession into
33
        an entry-level land management position that, his wife insisted,
34
        would be, in her words, *infinitamente más relajante*---infinitely
35
        more relaxing.
36
        """
37
        .getBytes( UTF_8 );
38
  }
39
}
140
A src/test/java/com/keenwrite/flexmark/ParserTest.java
1
/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.flexmark;
3
4
import com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension;
5
import com.keenwrite.processors.markdown.extensions.references.CrossReferenceExtension;
6
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
7
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
8
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
9
import com.vladsch.flexmark.ext.tables.TablesExtension;
10
import com.vladsch.flexmark.html.HtmlRenderer;
11
import com.vladsch.flexmark.parser.Parser;
12
import com.vladsch.flexmark.util.data.MutableDataSet;
13
import com.vladsch.flexmark.util.misc.Extension;
14
import org.junit.jupiter.params.ParameterizedTest;
15
import org.junit.jupiter.params.provider.Arguments;
16
import org.junit.jupiter.params.provider.MethodSource;
17
18
import java.util.ArrayList;
19
import java.util.List;
20
import java.util.stream.Stream;
21
22
import static org.junit.jupiter.api.Assertions.assertEquals;
23
24
/**
25
 * Test that basic styles for conversion exports as expected.
26
 */
27
public class ParserTest {
28
29
  @ParameterizedTest
30
  @MethodSource( "markdownParameters" )
31
  void test_Conversion_Markdown_Html( final String md, final String expected ) {
32
    final var extensions = createExtensions();
33
    final var options = new MutableDataSet();
34
    final var parser = Parser
35
      .builder( options )
36
      .extensions( extensions )
37
      .build();
38
    final var renderer = HtmlRenderer
39
      .builder( options )
40
      .extensions( extensions )
41
      .build();
42
43
    final var document = parser.parse( md );
44
    final var actual = renderer.render( document );
45
46
    assertEquals( expected, actual );
47
  }
48
49
  private List<Extension> createExtensions() {
50
    final var extensions = new ArrayList<Extension>();
51
52
    extensions.add( DefinitionExtension.create() );
53
    extensions.add( StrikethroughSubscriptExtension.create() );
54
    extensions.add( SuperscriptExtension.create() );
55
    extensions.add( TablesExtension.create() );
56
    extensions.add( FencedDivExtension.create() );
57
    extensions.add( CrossReferenceExtension.create() );
58
59
    return extensions;
60
  }
61
62
  private static Stream<Arguments> markdownParameters() {
63
    return Stream.of(
64
      Arguments.of(
65
        "*emphasis* _emphasis_ **strong**",
66
        "<p><em>emphasis</em> <em>emphasis</em> <strong>strong</strong></p>\n"
67
      ),
68
      Arguments.of(
69
        "the \uD83D\uDC4D emoji",
70
        "<p>the \uD83D\uDC4D emoji</p>\n"
71
      )
72
    );
73
  }
74
}
175
A src/test/java/com/keenwrite/io/FileObjectTest.java
1
package com.keenwrite.io;
2
3
import org.apache.commons.vfs2.FileSystemException;
4
import org.junit.jupiter.api.Disabled;
5
import org.renjin.eval.SessionBuilder;
6
7
import java.io.IOException;
8
import java.io.OutputStreamWriter;
9
10
import static com.keenwrite.constants.Constants.TEMPORARY_DIRECTORY;
11
import static java.io.File.separator;
12
import static java.lang.String.format;
13
import static java.nio.charset.StandardCharsets.UTF_8;
14
15
/**
16
 * Tests file resource allocation.
17
 */
18
public class FileObjectTest {
19
  /**
20
   * Test that resources are not exhausted.
21
   * <p>
22
   * Disabled because no issue was found and this test thrashes the I/O.
23
   * </p>
24
   */
25
  @Disabled
26
  void test_Open_MultipleFiles_NoResourcesExpire() throws FileSystemException {
27
    final var builder = new SessionBuilder();
28
    final var session = builder.build();
29
30
    for( int i = 0; i < 10000; i++ ) {
31
      final var filename = format(
32
        "%s%s%d.txt", TEMPORARY_DIRECTORY, separator, i
33
      );
34
      final var fileObject = session
35
        .getFileSystemManager()
36
        .resolveFile( filename );
37
38
      try(
39
        final var stream = fileObject.getContent().getOutputStream();
40
        final var writer = new OutputStreamWriter( stream, UTF_8 ) ) {
41
        writer.write( "contents" );
42
      } catch( final IOException e ) {
43
        throw new FileSystemException( e );
44
      }
45
46
      fileObject.delete();
47
    }
48
  }
49
}
150
A src/test/java/com/keenwrite/io/FileWatchServiceTest.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import org.junit.jupiter.api.Test;
5
import org.junit.jupiter.api.Timeout;
6
7
import java.io.File;
8
import java.io.IOException;
9
import java.nio.file.Files;
10
import java.util.concurrent.Semaphore;
11
import java.util.function.Consumer;
12
13
import static java.io.File.createTempFile;
14
import static java.nio.charset.StandardCharsets.UTF_8;
15
import static java.nio.file.StandardOpenOption.APPEND;
16
import static java.nio.file.StandardOpenOption.CREATE;
17
import static java.util.concurrent.TimeUnit.SECONDS;
18
import static org.junit.jupiter.api.Assertions.assertEquals;
19
20
/**
21
 * Responsible for testing that the {@link FileWatchService} fires the
22
 * expected {@link FileEvent} when the system raises state changes.
23
 */
24
class FileWatchServiceTest {
25
  /**
26
   * Test that modifying a file produces a {@link FileEvent}.
27
   *
28
   * @throws IOException          Could not create watcher service.
29
   * @throws InterruptedException Could not join on watcher service thread.
30
   */
31
  @Test
32
  @Timeout( value = 5, unit = SECONDS )
33
  void test_SingleFile_Write_Notified() throws
34
    IOException, InterruptedException {
35
    final var text = "arbitrary text to write";
36
    final var file = createTemporaryFile();
37
    final var service = new FileWatchService( file );
38
    final var thread = new Thread( service );
39
    final var semaphor = new Semaphore( 0 );
40
    final var listener = createListener( f -> {
41
      semaphor.release();
42
      assertEquals( file, f );
43
    } );
44
45
    thread.start();
46
    service.addListener( listener );
47
    Files.writeString( file.toPath(), text, UTF_8, CREATE, APPEND );
48
    semaphor.acquire();
49
    service.stop();
50
    thread.join();
51
  }
52
53
  private FileModifiedListener createListener( final Consumer<File> action ) {
54
    return fileEvent -> action.accept( fileEvent.getFile() );
55
  }
56
57
  private File createTemporaryFile() throws IOException {
58
    final var prefix = getClass().getPackageName();
59
    final var file = createTempFile( prefix, null, null );
60
    file.deleteOnExit();
61
    return file;
62
  }
63
}
164
A src/test/java/com/keenwrite/io/MediaTypeSnifferTest.java
1
package com.keenwrite.io;
2
3
import org.junit.jupiter.api.Test;
4
5
import java.io.File;
6
7
import static com.keenwrite.io.MediaTypeExtension.valueFrom;
8
import static org.apache.commons.io.FilenameUtils.getExtension;
9
import static org.junit.jupiter.api.Assertions.*;
10
11
/**
12
 * Responsible for testing that {@link MediaTypeSniffer} can return the
13
 * correct IANA-defined {@link MediaType} for known file types.
14
 */
15
class MediaTypeSnifferTest {
16
  @Test
17
  void test_Read_KnownFileTypes_MediaTypeReturned()
18
    throws Exception {
19
    final var clazz = getClass();
20
    final var pkgName = clazz.getPackageName();
21
    final var dir = pkgName.replace( '.', '/' );
22
23
    final var urls = clazz.getClassLoader().getResources( String.format( "%s%s", dir, "/images" ) );
24
    assertTrue( urls.hasMoreElements() );
25
26
    while( urls.hasMoreElements() ) {
27
      final var url = urls.nextElement();
28
      final var path = new File( url.toURI().getPath() );
29
      final var files = path.listFiles();
30
      assertNotNull( files );
31
32
      for( final var image : files ) {
33
        final var media = MediaTypeSniffer.getMediaType( image );
34
        final var actualExtension = valueFrom( media ).getExtension();
35
        final var expectedExtension = getExtension( image.toString() );
36
        System.out.printf( "%s -> %s%n", image, media );
37
38
        assertEquals( expectedExtension, actualExtension );
39
      }
40
    }
41
  }
42
}
143
A src/test/java/com/keenwrite/io/MediaTypeTest.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import org.junit.jupiter.api.Test;
5
6
import java.util.Map;
7
8
import static com.keenwrite.io.MediaType.*;
9
import static com.keenwrite.io.downloads.DownloadManager.open;
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#fromFilename(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
      "txt", TEXT_PLAIN,
39
      "yml", TEXT_YAML
40
    );
41
42
    map.forEach( ( k, v ) -> assertEquals( v, fromFilename( "f." + k ) ) );
43
  }
44
45
  /**
46
   * Test that remote fetches will pull and identify the type of resource
47
   * based on the HTTP Content-Type header (or shallow decoding).
48
   */
49
  @Test
50
  public void test_HttpRequest_Supported_Success() {
51
    //@formatter:off
52
    final var map = Map.of(
53
       "https://placehold.co/robots.txt", TEXT_PLAIN,
54
       "https://placehold.co/600x400.gif", IMAGE_GIF,
55
       "https://placehold.co/600x400.jpg", IMAGE_JPEG,
56
       "https://placehold.co/600x400.svg", IMAGE_SVG_XML,
57
       "https://kroki.io//graphviz/svg/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", IMAGE_SVG_XML
58
    );
59
    //@formatter:on
60
61
    map.forEach( ( k, v ) -> {
62
      try( var response = open( k ) ) {
63
        System.out.printf( "%s => %s%n", k, v );
64
        assertEquals( v, response.getMediaType() );
65
      } catch( final Exception e ) {
66
        throw new RuntimeException( e );
67
      }
68
    } );
69
  }
70
}
171
A src/test/java/com/keenwrite/io/SysFileTest.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import org.junit.jupiter.api.Test;
5
6
import java.io.IOException;
7
import java.nio.file.Path;
8
import java.util.Optional;
9
import java.util.concurrent.atomic.AtomicBoolean;
10
import java.util.function.Function;
11
12
import static org.junit.jupiter.api.Assertions.*;
13
14
class SysFileTest {
15
16
  @Test
17
  void test_Locate_ExistingExecutable_PathFound() {
18
    testFunction( SysFile::locate, "env", "/usr/bin/env" );
19
  }
20
21
  @Test
22
  void test_Where_ExistingExecutable_PathFound() {
23
    testFunction( sysFile -> {
24
      try {
25
        return sysFile.where();
26
      } catch( final IOException e ) {
27
        throw new RuntimeException( e );
28
      }
29
    }, "which", "/usr/bin/which" );
30
  }
31
32
  void testFunction( final Function<SysFile, Optional<Path>> consumer,
33
                     final String command,
34
                     final String expected ) {
35
    final var file = new SysFile( command );
36
    final var path = consumer.apply( file );
37
    final var failed = new AtomicBoolean( false );
38
39
    assertTrue( file.canRun() );
40
41
    path.ifPresentOrElse(
42
      location -> {
43
        final var actual = location.toAbsolutePath().toString();
44
45
        assertEquals( expected, actual );
46
      },
47
      () -> failed.set( true )
48
    );
49
50
    assertFalse( failed.get() );
51
  }
52
}
153
A src/test/java/com/keenwrite/io/UserDataDirTest.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.io;
6
7
import org.junit.jupiter.api.Test;
8
9
import static org.junit.jupiter.api.Assertions.assertFalse;
10
import static org.junit.jupiter.api.Assertions.assertTrue;
11
12
class UserDataDirTest {
13
  @Test
14
  void test_Unix_GetAppDirectory_DirectoryExists() {
15
    final var path = UserDataDir.getAppPath( "test" );
16
    final var file = path.toFile();
17
18
    assertTrue( file.exists() );
19
    assertTrue( file.delete() );
20
    assertFalse( file.exists() );
21
  }
22
}
123
A src/test/java/com/keenwrite/io/WindowsRegistryTest.java
1
package com.keenwrite.io;
2
3
import org.junit.jupiter.api.Test;
4
5
import static com.keenwrite.io.WindowsRegistry.*;
6
import static org.junit.jupiter.api.Assertions.*;
7
8
class WindowsRegistryTest {
9
  private static final String REG_PATH_PREFIX =
10
    "%USERPROFILE%";
11
  private static final String REG_PATH_SUFFIX =
12
    "\\AppData\\Local\\Microsoft\\WindowsApps;";
13
  private static final String REG_PATH = REG_PATH_PREFIX + REG_PATH_SUFFIX;
14
15
  @Test
16
  void test_Parse_RegistryEntry_ValueObtained() {
17
    final var expected = REG_PATH;
18
    final var actual = parseRegEntry(
19
      "    path    REG_EXPAND_SZ    " + expected
20
    );
21
22
    assertEquals( expected, actual );
23
  }
24
25
  @Test
26
  void test_Expand_RegistryEntry_VariablesExpanded() {
27
    final var value = "UserProfile";
28
    final var expected = value + REG_PATH_SUFFIX;
29
    final var actual = expand( REG_PATH, s -> value );
30
31
    assertEquals( expected, actual );
32
  }
33
}
134
A src/test/java/com/keenwrite/io/downloads/DownloadManagerTest.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.io.downloads;
6
7
import org.junit.jupiter.api.Test;
8
9
import java.io.File;
10
import java.io.IOException;
11
import java.net.URISyntaxException;
12
import java.util.concurrent.ExecutionException;
13
import java.util.concurrent.Executors;
14
import java.util.concurrent.atomic.AtomicInteger;
15
import java.util.concurrent.atomic.AtomicLong;
16
17
import static com.keenwrite.io.downloads.DownloadManager.ProgressListener;
18
import static com.keenwrite.io.downloads.DownloadManager.open;
19
import static java.lang.System.setProperty;
20
import static org.junit.jupiter.api.Assertions.*;
21
22
class DownloadManagerTest {
23
24
  static {
25
    // By default, this returns null, which is not a valid user agent.
26
    setProperty( "http.agent", DownloadManager.class.getCanonicalName() );
27
  }
28
29
  private static final String URL_BINARY =
30
    "https://keenwrite.com/downloads/KeenWrite.exe";
31
32
  @Test
33
  void test_Async_DownloadRequested_DownloadCompletes()
34
    throws IOException, InterruptedException,
35
    ExecutionException, URISyntaxException {
36
    final var complete = new AtomicInteger();
37
    final var transferred = new AtomicLong();
38
39
    final ProgressListener listener = ( percentage, bytes ) -> {
40
      complete.set( percentage );
41
      transferred.set( bytes );
42
    };
43
44
    final var file = File.createTempFile( "kw-", "test" );
45
    file.deleteOnExit();
46
47
    final var token = open( URL_BINARY );
48
    final var executor = Executors.newFixedThreadPool( 1 );
49
    final var result = token.download( file, listener );
50
    final var future = executor.submit( result );
51
52
    assertFalse( future.isDone() );
53
    assertTrue( complete.get() < 100 );
54
    assertNull( future.get() );
55
    assertTrue( future.isDone() );
56
    assertEquals( 100, complete.get() );
57
    assertTrue( transferred.get() > 100_000 );
58
59
    token.close();
60
  }
61
}
162
A src/test/java/com/keenwrite/preferences/KeyTest.java
1
package com.keenwrite.preferences;
2
3
import org.junit.jupiter.api.Test;
4
5
import static com.keenwrite.preferences.Key.key;
6
import static org.junit.jupiter.api.Assertions.assertEquals;
7
8
/**
9
 * Test that {@link Key} hierarchies can be transformed into alternate data
10
 * models.
11
 */
12
class KeyTest {
13
  @Test
14
  public void test_String_ParentHierarchy_DotNotation() {
15
    final var keyRoot = key( "root" );
16
    final var keyMeta = key( keyRoot, "meta" );
17
    final var keyDate = key( keyMeta, "date" );
18
19
    final var expected = "root.meta.date";
20
    final var actual = keyDate.toString();
21
22
    assertEquals( expected, actual );
23
  }
24
}
125
A src/test/java/com/keenwrite/preview/DiagramUrlGeneratorTest.java
1
package com.keenwrite.preview;
2
3
import org.junit.jupiter.api.Test;
4
5
import static com.keenwrite.constants.Constants.DIAGRAM_SERVER_NAME;
6
import static com.keenwrite.preview.DiagramUrlGenerator.toUrl;
7
import static org.junit.jupiter.api.Assertions.assertEquals;
8
9
/**
10
 * Responsible for testing that images sent to the diagram server will render.
11
 */
12
class DiagramUrlGeneratorTest {
13
  // @formatter:off
14
  private static final String[] DIAGRAMS = new String[]{
15
    "graphviz",
16
    "digraph G {Hello->World; World->Hello;}",
17
    "https://kroki.io/graphviz/svg/eJxLyUwvSizIUHBXqPZIzcnJ17ULzy_KSbFWAFO6dmBB61oAE9kNww==",
18
19
    "blockdiag",
20
    """
21
      blockdiag {
22
        Kroki -> generates -> "Block diagrams";
23
        Kroki -> is -> "very easy!";
24
25
        Kroki [color = "greenyellow"];
26
        "Block diagrams" [color = "pink"];
27
        "very easy!" [color = "orange"];
28
      }
29
      """,
30
    "https://kroki.io/blockdiag/svg/eJxdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="
31
  };
32
  // @formatter:on
33
34
  /**
35
   * Test that URL encoding works with Kroki's server.
36
   */
37
  @Test
38
  public void test_Generation_TextDiagram_UrlEncoded() {
39
    // Use a map of pairs if this test needs more complexity.
40
    for( int i = 0; i < DIAGRAMS.length / 3; i += 3 ) {
41
      final var name = DIAGRAMS[ i ];
42
      final var text = DIAGRAMS[ i + 1 ];
43
      final var expected = DIAGRAMS[ i + 2 ];
44
      final var actual = toUrl( DIAGRAM_SERVER_NAME, name, text );
45
46
      assertEquals( expected, actual );
47
    }
48
  }
49
}
150
A src/test/java/com/keenwrite/processors/html/XhtmlProcessorTest.java
1
package com.keenwrite.processors.html;
2
3
import com.keenwrite.ExportFormat;
4
import com.keenwrite.editors.common.Caret;
5
import com.keenwrite.processors.ProcessorContext;
6
import org.junit.jupiter.params.ParameterizedTest;
7
import org.junit.jupiter.params.provider.Arguments;
8
import org.junit.jupiter.params.provider.MethodSource;
9
10
import java.io.File;
11
import java.nio.file.Path;
12
import java.util.HashMap;
13
import java.util.stream.Stream;
14
15
import static com.keenwrite.ExportFormat.HTML_TEX_DELIMITED;
16
import static com.keenwrite.ExportFormat.XHTML_TEX;
17
import static com.keenwrite.constants.Constants.APOS_DEFAULT;
18
import static com.keenwrite.processors.ProcessorContext.builder;
19
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
20
import static com.keenwrite.processors.markdown.extensions.tex.TexNode.HTML_TEX;
21
import static java.util.Locale.ENGLISH;
22
import static org.junit.jupiter.api.Assertions.assertEquals;
23
24
public class XhtmlProcessorTest {
25
26
  /**
27
   * Contains the thumbs up emoji.
28
   */
29
  private static final String EMOJI_MARKDOWN = "the \uD83D\uDC4D emoji";
30
31
  @ParameterizedTest
32
  @MethodSource( "formatParameters" )
33
  void test_Conversion_EmojiInput_EncodedEmoji(
34
    final ExportFormat format, final String expected ) {
35
    final var context = createProcessorContext( format );
36
    final var processor = createProcessors( context );
37
    final var actual = processor.apply( EMOJI_MARKDOWN );
38
39
    assertEquals( expected, actual );
40
  }
41
42
  private static ProcessorContext createProcessorContext(
43
    final ExportFormat format ) {
44
    final var caret = Caret.builder().build();
45
    return builder()
46
      .with( ProcessorContext.Mutator::setExportFormat, format )
47
      .with( ProcessorContext.Mutator::setSourcePath, Path.of( "f.md" ) )
48
      .with( ProcessorContext.Mutator::setDefinitions, HashMap::new )
49
      .with( ProcessorContext.Mutator::setLocale, () -> ENGLISH )
50
      .with( ProcessorContext.Mutator::setMetadata, HashMap::new )
51
      .with( ProcessorContext.Mutator::setThemeDir, () -> Path.of( "b" ) )
52
      .with( ProcessorContext.Mutator::setCaret, () -> caret )
53
      .with( ProcessorContext.Mutator::setImageDir, () -> new File( "i" ) )
54
      .with( ProcessorContext.Mutator::setImageOrder, () -> "" )
55
      .with( ProcessorContext.Mutator::setImageServer, () -> "" )
56
      .with( ProcessorContext.Mutator::setSigilBegan, () -> "" )
57
      .with( ProcessorContext.Mutator::setSigilEnded, () -> "" )
58
      .with( ProcessorContext.Mutator::setRScript, () -> "" )
59
      .with( ProcessorContext.Mutator::setRWorkingDir, () -> Path.of( "r" ) )
60
      .with( ProcessorContext.Mutator::setCurlQuotes, () -> APOS_DEFAULT )
61
      .with( ProcessorContext.Mutator::setAutoRemove, () -> true )
62
      .build();
63
  }
64
65
  private static Stream<Arguments> formatParameters() {
66
    return Stream.of(
67
      Arguments.of(
68
        HTML_TEX_DELIMITED,
69
        """
70
          <html><head></head><body><p>the 👍 emoji</p>
71
          </body></html>"""
72
      ),
73
      Arguments.of(
74
        XHTML_TEX,
75
        """
76
          <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html lang="en" xmlns="http://www.w3.org/1999/xhtml"><head><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta content="2" name="count"/></head><body><p>the 👍 emoji</p>
77
          </body></html>"""
78
      )
79
    );
80
  }
81
}
182
A src/test/java/com/keenwrite/processors/markdown/extensions/ExtensionTest.java
1
package com.keenwrite.processors.markdown.extensions;
2
3
import com.vladsch.flexmark.html.HtmlRenderer;
4
import com.vladsch.flexmark.parser.Parser;
5
import com.vladsch.flexmark.parser.Parser.ParserExtension;
6
import org.junit.jupiter.api.TestInstance;
7
import org.junit.jupiter.params.ParameterizedTest;
8
import org.junit.jupiter.params.provider.Arguments;
9
import org.junit.jupiter.params.provider.MethodSource;
10
11
import java.util.LinkedList;
12
import java.util.List;
13
import java.util.stream.Stream;
14
15
import static org.junit.jupiter.api.Assertions.assertEquals;
16
17
@TestInstance( TestInstance.Lifecycle.PER_CLASS )
18
public abstract class ExtensionTest {
19
  private final List<ParserExtension> mExtensions = new LinkedList<>();
20
21
  @ParameterizedTest
22
  @MethodSource( "getDocuments" )
23
  public void test_Extensions_Markdown_Html(
24
    final String input, final String expected
25
  ) {
26
    final var pBuilder = Parser.builder();
27
    final var hBuilder = HtmlRenderer.builder();
28
    final var parser = pBuilder.extensions( mExtensions ).build();
29
    final var renderer = hBuilder.extensions( mExtensions ).build();
30
31
    final var document = parser.parse( input );
32
    final var actual = renderer.render( document );
33
34
    assertEquals( expected, actual );
35
  }
36
37
  protected void addExtension( final ParserExtension extension ) {
38
    mExtensions.add( extension );
39
  }
40
41
  protected Arguments args( final String in, final String out ) {
42
    return Arguments.of( in, out );
43
  }
44
45
  protected abstract Stream<Arguments> getDocuments();
46
}
147
A src/test/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivExtensionTest.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences;
6
7
import com.keenwrite.processors.markdown.extensions.ExtensionTest;
8
import org.junit.jupiter.api.BeforeAll;
9
import org.junit.jupiter.params.provider.Arguments;
10
11
import java.util.stream.Stream;
12
13
public class FencedDivExtensionTest extends ExtensionTest {
14
15
  @BeforeAll
16
  protected void setup() {
17
    addExtension( FencedDivExtension.create() );
18
  }
19
20
  @Override
21
  protected Stream<Arguments> getDocuments() {
22
    return Stream.of(
23
      args(
24
        """
25
          >
26
          > ::: {.concurrent title="3:58"}
27
          > Line 1
28
          > :::
29
          >
30
          > ::: {.concurrent title="3:59"}
31
          > Line 2
32
          > :::
33
          >
34
          > ::: {.concurrent title="4:00"}
35
          > Line 3
36
          > :::
37
          >
38
          """,
39
        """
40
          <blockquote>
41
          <div class="concurrent" data-title="3:58">
42
          <p>Line 1</p>
43
          </div><div class="concurrent" data-title="3:59">
44
          <p>Line 2</p>
45
          </div><div class="concurrent" data-title="4:00">
46
          <p>Line 3</p>
47
          </div>
48
          </blockquote>
49
          """
50
      ),
51
      args(
52
        """
53
          > Hello
54
          >
55
          > ::: world
56
          > Adventures
57
          >
58
          > in **absolute**
59
          >
60
          > nesting.
61
          > :::
62
          >
63
          > Goodbye
64
          """,
65
        """
66
          <blockquote>
67
          <p>Hello</p>
68
          <div class="world">
69
          <p>Adventures</p>
70
          <p>in <strong>absolute</strong></p>
71
          <p>nesting.</p>
72
          </div>
73
          <p>Goodbye</p>
74
          </blockquote>
75
          """
76
      )
77
    );
78
  }
79
}
180
A src/test/java/com/keenwrite/processors/markdown/extensions/images/ImageLinkExtensionTest.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.images;
3
4
import com.keenwrite.editors.common.Caret;
5
import com.keenwrite.processors.Processor;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.vladsch.flexmark.html.HtmlRenderer;
8
import com.vladsch.flexmark.parser.Parser;
9
import org.junit.jupiter.api.Test;
10
11
import java.io.File;
12
import java.io.IOException;
13
import java.net.URI;
14
import java.nio.file.Path;
15
import java.nio.file.Paths;
16
import java.util.LinkedHashMap;
17
import java.util.List;
18
import java.util.Map;
19
20
import static com.keenwrite.ExportFormat.XHTML_TEX;
21
import static com.keenwrite.constants.Constants.DOCUMENT_DEFAULT;
22
import static java.lang.String.format;
23
import static org.junit.jupiter.api.Assertions.*;
24
25
/**
26
 * Responsible for testing that linked images render into HTML according to
27
 * the {@link ImageLinkExtension} rules.
28
 */
29
@SuppressWarnings( "SameParameterValue" )
30
public class ImageLinkExtensionTest {
31
  private static final String UIR_DIR = "images";
32
  private static final String URI_FILE = "kitten";
33
  private static final String URI_PATH = UIR_DIR + '/' + URI_FILE;
34
  private static final String PATH_KITTEN_PNG = String.format(
35
    "%s%s", URI_PATH, ".png" );
36
  private static final String PATH_KITTEN_JPG = String.format(
37
    "%s%s", URI_PATH, ".jpg" );
38
39
  /**
40
   * Web server that doles out images.
41
   */
42
  private static final String PLACEHOLDER = "loremflickr.com";
43
44
  private static final Map<String, String> IMAGES = new LinkedHashMap<>();
45
46
  static {
47
    add( PATH_KITTEN_PNG, URI_PATH );
48
    add( PATH_KITTEN_PNG, URI_FILE );
49
    add( PATH_KITTEN_PNG, PATH_KITTEN_PNG );
50
    add( PATH_KITTEN_JPG, PATH_KITTEN_JPG );
51
    add( format( "//%s/200/200", PLACEHOLDER ),
52
         format( "//%s/200/200", PLACEHOLDER ) );
53
    add( format( "ftp://%s/200/200", PLACEHOLDER ),
54
         format( "ftp://%s/200/200", PLACEHOLDER ) );
55
    add( format( "http://%s/200/200", PLACEHOLDER ),
56
         format( "http://%s/200/200", PLACEHOLDER ) );
57
    add( format( "https://%s/200/200", PLACEHOLDER ),
58
         format( "https://%s/200/200", PLACEHOLDER ) );
59
  }
60
61
  private static void add( final String expected, final String actual ) {
62
    IMAGES.put( toMd( actual ), toHtml( expected ) );
63
  }
64
65
  private static String toMd( final String resource ) {
66
    return format( "![Tooltip](%s 'Title')", resource );
67
  }
68
69
  private static String toHtml( final String url ) {
70
    return format(
71
      "<p><img src=\"%s\" alt=\"Tooltip\" title=\"Title\" /></p>%n", url );
72
  }
73
74
  /**
75
   * Test that the key URIs present in the {@link #IMAGES} map are rendered
76
   * as the value URIs present in the same map.
77
   */
78
  @Test
79
  void test_ImageLookup_RelativePathWithExtension_ResolvedSuccessfully()
80
    throws IOException {
81
    final var resource = getResourcePath( PATH_KITTEN_PNG );
82
    final var imagePath = new File( PATH_KITTEN_PNG ).toPath();
83
    final var subpaths = resource.getNameCount() - imagePath.getNameCount();
84
    final var subpath = resource.subpath( 0, subpaths );
85
86
    final var root = resource.getRoot();
87
    assertNotNull( root );
88
89
    final var resolved = root.resolve( subpath );
90
    final var doc = resolved.toString();
91
92
    // The root component isn't considered part of the path, so add it back.
93
    final var documentPath = Path.of( doc, DOCUMENT_DEFAULT.getName() );
94
    final var imagesDir = Path.of( "images" );
95
    final var context = createProcessorContext( documentPath, imagesDir );
96
    final var extension = ImageLinkExtension.create( context );
97
    final var extensions = List.of( extension );
98
    final var pBuilder = Parser.builder();
99
    final var hBuilder = HtmlRenderer.builder();
100
    final var parser = pBuilder.extensions( extensions ).build();
101
    final var renderer = hBuilder.extensions( extensions ).build();
102
    final var imageFile = resolved.resolve( imagePath ).toFile();
103
    imageFile.deleteOnExit();
104
105
    assertTrue( imageFile.createNewFile() );
106
    assertNotNull( parser );
107
    assertNotNull( renderer );
108
109
    for( final var entry : IMAGES.entrySet() ) {
110
      final var key = entry.getKey();
111
      final var node = parser.parse( key );
112
      final var expectedHtml = entry.getValue();
113
      final var actualHtml = renderer.render( node );
114
115
      assertEquals( expectedHtml, actualHtml );
116
    }
117
118
    assertTrue( imageFile.delete() );
119
  }
120
121
  /**
122
   * Creates a new {@link ProcessorContext} for the given file name path.
123
   *
124
   * @param inputPath Fully qualified path to the file name.
125
   * @return A context used for creating new {@link Processor} instances.
126
   */
127
  private ProcessorContext createProcessorContext(
128
    final Path inputPath, final Path imagesDir ) {
129
    return ProcessorContext
130
      .builder()
131
      .with( ProcessorContext.Mutator::setSourcePath, inputPath )
132
      .with( ProcessorContext.Mutator::setExportFormat, XHTML_TEX )
133
      .with( ProcessorContext.Mutator::setCaret, () -> Caret.builder().build() )
134
      .with( ProcessorContext.Mutator::setImageDir, imagesDir::toFile )
135
      .build();
136
  }
137
138
  private static URI toUri( final String path ) {
139
    try {
140
      return Path.of( path ).toUri();
141
    } catch( final Exception ex ) {
142
      throw new RuntimeException( ex );
143
    }
144
  }
145
146
  private static Path getResourcePath( final String path ) {
147
    return Paths.get( toUri( path ) );
148
  }
149
}
1150
A src/test/java/com/keenwrite/processors/markdown/extensions/references/CaptionsAndCrossReferencesExtensionTest.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.keenwrite.processors.markdown.extensions.ExtensionTest;
8
import com.keenwrite.processors.markdown.extensions.captions.CaptionExtension;
9
import com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension;
10
import com.keenwrite.processors.markdown.extensions.tex.TexExtension;
11
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
12
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
13
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
14
import com.vladsch.flexmark.ext.tables.TablesExtension;
15
import org.junit.jupiter.api.BeforeAll;
16
import org.junit.jupiter.params.provider.Arguments;
17
18
import java.util.stream.Stream;
19
20
import static com.keenwrite.ExportFormat.XHTML_TEX;
21
import static com.keenwrite.processors.ProcessorContext.Mutator;
22
import static com.keenwrite.processors.ProcessorContext.builder;
23
24
@SuppressWarnings( "SpellCheckingInspection" )
25
public class CaptionsAndCrossReferencesExtensionTest extends ExtensionTest {
26
  protected Stream<Arguments> getDocuments() {
27
    return Stream.of(
28
      args(
29
        """
30
          {#fig:cats} [@fig:cats]
31
          {#table:dogs} [@table:dogs]
32
          {#ocean:whale-01} [@ocean:whale-02]
33
          """,
34
        """
35
          <p><a class="name" data-type="fig" name="cats" /> <a class="href" data-type="fig" href="#cats" />
36
          <a class="name" data-type="table" name="dogs" /> <a class="href" data-type="table" href="#dogs" />
37
          <a class="name" data-type="ocean" name="whale-01" /> <a class="href" data-type="ocean" href="#whale-02" /></p>
38
          """
39
      ),
40
      args(
41
        """
42
          {#日本:w0mbatß}
43
          [@日本:w0mbatß]
44
          """,
45
        """
46
          <p><a class="name" data-type="日本" name="w0mbatß" />
47
          <a class="href" data-type="日本" href="#w0mbatß" /></p>
48
          """
49
      ),
50
      args(
51
        """
52
          Lorem ipsum dolor sit amet, consectetur adipiscing elit.
53
          {#fig:cats} Sed do eiusmod tempor incididunt ut
54
          labore et dolore magna aliqua. Ut enim ad minim veniam,
55
          quis nostrud exercitation ullamco laboris nisi ut aliquip
56
          ex ea commodo consequat. [@fig:cats]
57
          """,
58
        """
59
          <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
60
          <a class="name" data-type="fig" name="cats" /> Sed do eiusmod tempor incididunt ut
61
          labore et dolore magna aliqua. Ut enim ad minim veniam,
62
          quis nostrud exercitation ullamco laboris nisi ut aliquip
63
          ex ea commodo consequat. <a class="href" data-type="fig" href="#cats" /></p>
64
          """
65
      ),
66
      args(
67
        """
68
          {#note:advancement} Advancement isn't
69
          measured by the ingenuity of inventions, but by humanity's ability
70
          to anticipate and forfend dire aftermaths *before* using them.
71
          
72
          [@note:advancement]
73
          
74
          To what end?
75
          """,
76
        """
77
          <p><a class="name" data-type="note" name="advancement" /> Advancement isn't
78
          measured by the ingenuity of inventions, but by humanity's ability
79
          to anticipate and forfend dire aftermaths <em>before</em> using them.</p>
80
          <p><a class="href" data-type="note" href="#advancement" /></p>
81
          <p>To what end?</p>
82
          """
83
      ),
84
      args(
85
        """
86
          $E=mc^2$ {#eq:label}
87
          """,
88
        """
89
          <p><tex>$E=mc^2$</tex> <a class="name" data-type="eq" name="label" /></p>
90
          """
91
      ),
92
      args(
93
        """
94
          $$E=mc^2$$ {#eq:label}
95
          """,
96
        """
97
          <p><tex>$$E=mc^2$$</tex> <a class="name" data-type="eq" name="label" /></p>
98
          """
99
      ),
100
      args(
101
        """
102
          $$E=mc^2$$
103
          
104
          :: Caption {#eqn:energy}
105
          """,
106
        """
107
          <p><span class="caption">Caption </span><a class="name" data-type="eqn" name="energy" /></p>
108
          <p><tex>$$E=mc^2$$</tex></p>
109
          """
110
      ),
111
      args(
112
        """
113
          ``` haskell
114
          main :: IO ()
115
          ```
116
          
117
          :: Source code caption {#listing:haskell1}
118
          """,
119
        """
120
          <p><span class="caption">Source code caption </span><a class="name" data-type="listing" name="haskell1" /></p>
121
          <pre><code class="language-haskell">main :: IO ()
122
          </code></pre>
123
          """
124
      ),
125
      args(
126
        """
127
          ::: warning
128
          Do not eat processed **sugar**.
129
          
130
          Seriously.
131
          :::
132
          
133
          :: Caption {#warning:sugar}
134
          """,
135
        """
136
          <p><span class="caption">Caption </span><a class="name" data-type="warning" name="sugar" /></p><div class="warning">
137
          <p>Do not eat processed <strong>sugar</strong>.</p>
138
          <p>Seriously.</p>
139
          </div>
140
          """
141
      ),
142
      args(
143
        """
144
          ![alt text](tunnel)
145
          
146
          :: Caption {#fig:label}
147
          """,
148
        """
149
          <p><span class="caption">Caption </span><a class="name" data-type="fig" name="label" /></p>
150
          <p><img src="tunnel" alt="alt text" /></p>
151
          """
152
      ),
153
      args(
154
        """
155
          ![kitteh](kitten)
156
          
157
          :: Caption **bold** {#fig:label} *italics*
158
          """,
159
        """
160
          <p><span class="caption">Caption <strong>bold</strong>  <em>italics</em></span><a class="name" data-type="fig" name="label" /></p>
161
          <p><img src="kitten" alt="kitteh" /></p>
162
          """
163
      ),
164
      args(
165
        """
166
          > I'd like to be the lucky devil who gets to burn with you.
167
          >
168
          > Well, I'm no angel, my wings have been clipped;
169
          >
170
          > I've traded my halo for horns and a whip.
171
          
172
          :: Meschiya Lake - Lucky Devil {#lyrics:blues}
173
          """,
174
        """
175
          <p><span class="caption">Meschiya Lake - Lucky Devil </span><a class="name" data-type="lyrics" name="blues" /></p>
176
          <blockquote>
177
          <p>I'd like to be the lucky devil who gets to burn with you.</p>
178
          <p>Well, I'm no angel, my wings have been clipped;</p>
179
          <p>I've traded my halo for horns and a whip.</p>
180
          </blockquote>
181
          """
182
      ),
183
      args(
184
        """
185
          | a | b | c |
186
          |---|---|---|
187
          | 1 | 2 | 3 |
188
          | 4 | 5 | 6 |
189
          
190
          :: Caption {#tbl:label}
191
          """,
192
        """
193
          <p><span class="caption">Caption </span><a class="name" data-type="tbl" name="label" /></p>
194
          <table>
195
          <thead>
196
          <tr><th>a</th><th>b</th><th>c</th></tr>
197
          </thead>
198
          <tbody>
199
          <tr><td>1</td><td>2</td><td>3</td></tr>
200
          <tr><td>4</td><td>5</td><td>6</td></tr>
201
          </tbody>
202
          </table>
203
          """
204
      ),
205
      args(
206
        """
207
          ``` diagram-plantuml
208
          @startuml
209
          Alice -> Bob: Request
210
          Bob --> Alice: Response
211
          @enduml
212
          ```
213
          
214
          :: Diagram {#dia:seq1}
215
          """,
216
        """
217
          <p><span class="caption">Diagram </span><a class="name" data-type="dia" name="seq1" /></p>
218
          <pre><code class="language-diagram-plantuml">@startuml
219
          Alice -&gt; Bob: Request
220
          Bob --&gt; Alice: Response
221
          @enduml
222
          </code></pre>
223
          """
224
      ),
225
      args(
226
        """
227
          ::: lyrics
228
          Weather hit, meltin' road.
229
          Our mama's gone, six feet cold.
230
          Gas on down to future town,
231
          Make prophecy take hold.
232
          
233
          Warnin' sign, cent'ry old:
234
          When buyin' coal, air is sold.
235
          Aim our toil, ten figure oil;
236
          Trade life on Earth for gold.
237
          :::
238
          """,
239
        """
240
          <div class="lyrics">
241
          <p>Weather hit, meltin' road.
242
          Our mama's gone, six feet cold.
243
          Gas on down to future town,
244
          Make prophecy take hold.</p>
245
          <p>Warnin' sign, cent'ry old:
246
          When buyin' coal, air is sold.
247
          Aim our toil, ten figure oil;
248
          Trade life on Earth for gold.</p>
249
          </div>
250
          """
251
      )
252
    );
253
  }
254
255
  @BeforeAll
256
  protected void setup() {
257
    final var context = builder()
258
      .with( Mutator::setExportFormat, XHTML_TEX )
259
      .build();
260
261
    addExtension( TexExtension.create( s -> s, context ) );
262
    addExtension( DefinitionExtension.create() );
263
    addExtension( StrikethroughSubscriptExtension.create() );
264
    addExtension( SuperscriptExtension.create() );
265
    addExtension( TablesExtension.create() );
266
    addExtension( FencedDivExtension.create() );
267
    addExtension( CrossReferenceExtension.create() );
268
    addExtension( CaptionExtension.create() );
269
  }
270
}
1271
A src/test/java/com/keenwrite/r/PluralizeTest.java
1
/* Copyright 2020-2021 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", "octopodes" ),
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
  public void test_Pluralize_SingularForms_PluralForms()
49
    throws ScriptException {
50
51
    for( final var entry : PLURAL_MAP.entrySet() ) {
52
      final var expectedSingular = entry.getKey();
53
      final var expectedPlural = entry.getValue();
54
      final var actualSingular = pluralize( expectedSingular, 1 );
55
      final var actualPlural = pluralize( expectedSingular, 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
    final var stmt = format( "pluralize( word='%s', n=%d );", word, count );
65
    return r( stmt ).toString();
66
  }
67
68
  private static Object r( final String code ) throws ScriptException {
69
    return ENGINE.eval( code );
70
  }
71
}
172
A src/test/java/com/keenwrite/richtext/StyleClassedTextAreaTest.java
1
package com.keenwrite.richtext;
2
3
import javafx.application.Application;
4
import javafx.scene.Scene;
5
import javafx.scene.layout.StackPane;
6
import javafx.stage.Stage;
7
import org.fxmisc.flowless.VirtualizedScrollPane;
8
import org.fxmisc.richtext.StyleClassedTextArea;
9
10
import java.net.URISyntaxException;
11
12
/**
13
 * Scaffolding for creating one-off tests, not run as part of test suite.
14
 */
15
public class StyleClassedTextAreaTest extends Application {
16
  private final StyleClassedTextArea mTextArea =
17
    new StyleClassedTextArea( false );
18
19
  private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
20
    new VirtualizedScrollPane<>( mTextArea );
21
22
  public static void main( final String[] args ) {
23
    launch( args );
24
  }
25
26
  @Override
27
  public void start( final Stage stage ) throws URISyntaxException {
28
    final var pane = new StackPane( mScrollPane );
29
    final var scene = new Scene( pane, 800, 600 );
30
31
    final var stylesheets = scene.getStylesheets();
32
    stylesheets.clear();
33
    stylesheets.add( getStylesheet( "skins/scene.css" ) );
34
    stylesheets.add( getStylesheet( "editor/markdown.css" ) );
35
    stylesheets.add( getStylesheet( "skins/monokai.css" ) );
36
37
    mTextArea.getStyleClass().add( "markdown" );
38
    mTextArea.insertText( 0, TEXT + TEXT + TEXT + TEXT );
39
    mTextArea.setStyle( "-fx-font-size: 13pt" );
40
41
    mTextArea.requestFollowCaret();
42
    mTextArea.moveTo( 4375 );
43
44
    stage.setScene( scene );
45
    stage.show();
46
  }
47
48
  private String getStylesheet( final String suffix )
49
    throws URISyntaxException {
50
    final var url = getClass().getResource( "/com/keenwrite/" + suffix );
51
    return url == null ? "" : url.toURI().toString();
52
  }
53
54
  private static final String TEXT = """
55
    In my younger and more vulnerable years my father gave me some advice
56
    that I’ve been turning over in my mind ever since.
57
                                                                                    
58
    “Whenever you feel like criticizing anyone,” he told me, “just
59
    remember that all the people in this world haven’t had the advantages
60
    that you’ve had.”
61
                                                                                    
62
    He didn’t say any more, but we’ve always been unusually communicative
63
    in a reserved way, and I understood that he meant a great deal more
64
    than that. In consequence, I’m inclined to reserve all judgements, a
65
    habit that has opened up many curious natures to me and also made me
66
    the victim of not a few veteran bores. The abnormal mind is quick to
67
    detect and attach itself to this quality when it appears in a normal
68
    person, and so it came about that in college I was unjustly accused of
69
    being a politician, because I was privy to the secret griefs of wild,
70
    unknown men. Most of the confidences were unsought—frequently I have
71
    feigned sleep, preoccupation, or a hostile levity when I realized by
72
    some unmistakable sign that an intimate revelation was quivering on
73
    the horizon; for the intimate revelations of young men, or at least
74
    the terms in which they express them, are usually plagiaristic and
75
    marred by obvious suppressions. Reserving judgements is a matter of
76
    infinite hope. I am still a little afraid of missing something if I
77
    forget that, as my father snobbishly suggested, and I snobbishly
78
    repeat, a sense of the fundamental decencies is parcelled out
79
    unequally at birth.""";
80
}
181
A src/test/java/com/keenwrite/sigils/RKeyOperatorTest.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.sigils;
3
4
import org.junit.jupiter.api.Test;
5
6
import static org.junit.jupiter.api.Assertions.assertEquals;
7
8
/**
9
 * Responsible for simulating R variable injection.
10
 */
11
class RKeyOperatorTest {
12
13
  /**
14
   * Test that a key name becomes an R variable.
15
   */
16
  @Test
17
  void test_Process_KeyName_Processed() {
18
    final var mOperator = new RKeyOperator();
19
    final var expected = "v$a$b$c$d";
20
    final var actual = mOperator.apply( "a.b.c.d" );
21
22
    assertEquals( expected, actual );
23
  }
24
}
125
A src/test/java/com/keenwrite/util/AlphanumComparatorTest.java
1
/* Copyright 2020-2021 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.ArrayList;
7
import java.util.Arrays;
8
import java.util.Collections;
9
10
import static org.junit.jupiter.api.Assertions.assertEquals;
11
12
/**
13
 * Responsible for testing the http://www.davekoelle.com/alphanum.html
14
 * implementation.
15
 */
16
class AlphanumComparatorTest {
17
18
  /**
19
   * Test that a randomly sorted list containing a mix of alphanumeric
20
   * characters ("chunks") will be sorted according to numeric and alphabetic
21
   * order.
22
   */
23
  @Test
24
  public void test_Sort_UnsortedList_SortedAlphanumerically() {
25
    final var expected = Arrays.asList(
26
      "10X Radonius",
27
      "20X Radonius",
28
      "20X Radonius Prime",
29
      "30X Radonius",
30
      "40X Radonius",
31
      "200X Radonius",
32
      "1000X Radonius Maximus",
33
      "Allegia 6R Clasteron",
34
      "Allegia 50 Clasteron",
35
      "Allegia 50B Clasteron",
36
      "Allegia 51 Clasteron",
37
      "Allegia 500 Clasteron",
38
      "Alpha 2",
39
      "Alpha 2A",
40
      "Alpha 2A-900",
41
      "Alpha 2A-8000",
42
      "Alpha 100",
43
      "Alpha 200",
44
      "Callisto Morphamax",
45
      "Callisto Morphamax 500",
46
      "Callisto Morphamax 600",
47
      "Callisto Morphamax 700",
48
      "Callisto Morphamax 5000",
49
      "Callisto Morphamax 6000 SE",
50
      "Callisto Morphamax 6000 SE2",
51
      "Callisto Morphamax 7000",
52
      "Xiph Xlater 5",
53
      "Xiph Xlater 40",
54
      "Xiph Xlater 50",
55
      "Xiph Xlater 58",
56
      "Xiph Xlater 300",
57
      "Xiph Xlater 500",
58
      "Xiph Xlater 2000",
59
      "Xiph Xlater 5000",
60
      "Xiph Xlater 10000"
61
    );
62
    final var actual = new ArrayList<>( expected );
63
64
    Collections.shuffle( actual );
65
    actual.sort( new AlphanumComparator<>() );
66
    assertEquals( expected, actual );
67
  }
68
}
169
A src/test/java/com/keenwrite/util/CircularQueueTest.java
1
package com.keenwrite.util;
2
3
import com.keenwrite.collections.CircularQueue;
4
import org.junit.jupiter.api.Test;
5
6
import static org.junit.jupiter.api.Assertions.assertEquals;
7
8
/**
9
 * Tests the {@link CircularQueue} class.
10
 */
11
public class CircularQueueTest {
12
13
  /**
14
   * Exercises the circularity aspect of the {@link CircularQueue}.
15
   * Confirms that the elements added can be subsequently overwritten.
16
   * This also checks that peek and remove functionality work as expected.
17
   */
18
  @Test
19
  public void test_Add_ExceedMaxCapacity_FirstElementOverwritten() {
20
    final var CAPACITY = 5;
21
    final var OVERWRITE = 17;
22
    final var ELEMENTS = CAPACITY + OVERWRITE;
23
    final var queue = createQueue( CAPACITY, ELEMENTS );
24
25
    assertEquals( CAPACITY, queue.size() );
26
27
    for( int i = 0; i < CAPACITY; i++ ) {
28
      final var expected =
29
        ELEMENTS - ((((OVERWRITE - CAPACITY - 1) - i) % CAPACITY) + 1);
30
31
      assertEquals( expected, queue.peek() );
32
      assertEquals( expected, queue.remove() );
33
    }
34
  }
35
36
  /**
37
   * Tests iterating over all elements in the {@link CircularQueue}.
38
   */
39
  @Test
40
  public void test_Iterate_FullQueue_AllElementsNavigated() {
41
    final var CAPACITY = 101;
42
    final var queue = createQueue( CAPACITY, CAPACITY );
43
    int actualCount = 0;
44
45
    for( final var ignored : queue ) {
46
      actualCount++;
47
    }
48
49
    assertEquals( CAPACITY, actualCount );
50
  }
51
52
  /**
53
   * Tests iterating over {@link CircularQueue} where some elements,
54
   * starting at an arbitrary offset, have been removed.
55
   */
56
  @Test
57
  public void test_Iterate_PartialQueue_AllElementsNavigated() {
58
    final var CAPACITY = 31;
59
    final var OVERWRITE = CAPACITY / 2;
60
    final var queue = createQueue( CAPACITY, CAPACITY + OVERWRITE );
61
    var actualCount = 0;
62
63
    for( int i = 0; i < OVERWRITE; i++ ) {
64
      queue.remove();
65
    }
66
67
    for( final var ignored : queue ) {
68
      actualCount++;
69
    }
70
71
    assertEquals( CAPACITY - OVERWRITE, actualCount );
72
  }
73
74
  /**
75
   * Creates a new, pre-populated {@link CircularQueue} instance.
76
   *
77
   * @param capacity The maximum number of elements before overwriting.
78
   * @param count    The number of elements to pre-populate the queue.
79
   * @return A new {@link CircularQueue} pre-populated with ascending,
80
   * consecutive values.
81
   */
82
  private static CircularQueue<Integer> createQueue(
83
    final int capacity, final int count ) {
84
    final var queue = new CircularQueue<Integer>( capacity );
85
86
    for( int i = 0; i < count; i++ ) {
87
      queue.add( i );
88
    }
89
90
    return queue;
91
  }
92
}
193
A src/test/java/com/keenwrite/util/CyclicIteratorTest.java
1
/* Copyright 2020-2021 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/java/com/keenwrite/util/RangeValidatorTest.java
1
package com.keenwrite.util;
2
3
import org.junit.jupiter.api.Test;
4
5
import static org.junit.jupiter.api.Assertions.*;
6
7
/**
8
 * Tests that the range format specifiers correctly identify integer values
9
 * inside and outside the range.
10
 */
11
class RangeValidatorTest {
12
  @Test
13
  void test_Validation_SingleRange_Valid() {
14
    // Arbitrary start and end.
15
    final var lo = 1;
16
    final var hi = 5;
17
    final var validator = new RangeValidator( lo + "-" + hi );
18
19
    for( int i = lo; i < hi; i++ ) {
20
      assertTrue( validator.test( i ) );
21
    }
22
23
    // Arbitrary bounds checks.
24
    assertFalse( validator.test( lo - 1 ) );
25
    assertFalse( validator.test( lo - 11 ) );
26
    assertFalse( validator.test( hi + 1 ) );
27
    assertFalse( validator.test( hi + 11 ) );
28
  }
29
30
  @Test
31
  void test_Validation_SingleValue_Valid() {
32
    // Arbitrary.
33
    final var i = 7;
34
    final var validator = new RangeValidator( Integer.toString( i ) );
35
36
    assertTrue( validator.test( i ) );
37
  }
38
39
  @Test
40
  void test_Validation_UnboundedMaxIntegerRange_Valid() {
41
    // Arbitrary.
42
    final var lo = 11;
43
    final var validator = new RangeValidator( lo + "-" );
44
45
    // Arbitrary end value.
46
    for( int i = lo; i < lo + 101; i++ ) {
47
      assertTrue( validator.test( i ) );
48
    }
49
50
    assertFalse( validator.test( 10 ) );
51
  }
52
53
  @Test
54
  void test_Validation_UnboundedMinIntegerRange_Valid() {
55
    // Arbitrary.
56
    final var hi = 5;
57
    final var validator = new RangeValidator( "-" + hi );
58
59
    for( int i = 1; i < hi; i++ ) {
60
      assertTrue( validator.test( i ) );
61
    }
62
63
    assertFalse( validator.test( 0 ) );
64
    assertFalse( validator.test( -1 ) );
65
  }
66
67
  @Test
68
  void test_Validation_MultipleRanges_Valid() {
69
    // Arbitrary.
70
    final var validator = new RangeValidator( "-5, 7-11, 13, 15-20, 30-" );
71
72
    assertTrue( validator.test( 1 ) );
73
    assertTrue( validator.test( 5 ) );
74
    assertTrue( validator.test( 7 ) );
75
    assertTrue( validator.test( 11 ) );
76
    assertTrue( validator.test( 13 ) );
77
    assertTrue( validator.test( 15 ) );
78
    assertTrue( validator.test( 20 ) );
79
    assertTrue( validator.test( 30 ) );
80
    assertTrue( validator.test( 101 ) );
81
82
    assertFalse( validator.test( -1 ) );
83
    assertFalse( validator.test( 0 ) );
84
    assertFalse( validator.test( 6 ) );
85
    assertFalse( validator.test( 12 ) );
86
    assertFalse( validator.test( 14 ) );
87
    assertFalse( validator.test( 21 ) );
88
    assertFalse( validator.test( 29 ) );
89
  }
90
91
  @Test
92
  void test_Validation_EmptyRange_AllValid() {
93
    final var validator = new RangeValidator( "" );
94
95
    assertTrue( validator.test( 0 ) );
96
    assertTrue( validator.test( 1 ) );
97
    assertTrue( validator.test( 2 ) );
98
    assertTrue( validator.test( Integer.MAX_VALUE ) );
99
  }
100
}
1101
A src/test/resources/com/keenwrite/io/images/example.bmp
Binary file
A src/test/resources/com/keenwrite/io/images/example.eps
Binary file
A src/test/resources/com/keenwrite/io/images/example.gif
Binary file
A src/test/resources/com/keenwrite/io/images/example.jpg
Binary file
A src/test/resources/com/keenwrite/io/images/example.png
Binary file
A src/test/resources/com/keenwrite/io/images/example.svg
1
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 34" width="68px" height="34px" fill="#005a9c">
2
<desc>W3C</desc>
3
<path d="m16.117 1.006 5.759 19.58 5.759-19.58h4.17 11.444v1.946l-5.879 10.128c2.065.663 3.627 1.868 4.686 3.615 1.059 1.748 1.589 3.799 1.589 6.155 0 2.914-.775 5.363-2.324 7.348s-3.555 2.978-6.017 2.978c-1.854 0-3.469-.589-4.845-1.767-1.377-1.178-2.396-2.773-3.058-4.786l3.256-1.35c.477 1.218 1.106 2.178 1.887 2.879.781.702 1.701 1.052 2.76 1.052 1.112 0 2.052-.622 2.82-1.866.768-1.245 1.152-2.74 1.152-4.489 0-1.933-.411-3.429-1.231-4.488-.954-1.244-2.45-1.867-4.489-1.867h-1.588v-1.906l5.56-9.612h-6.712l-.382.65-8.163 27.548h-.397l-5.958-19.937-5.957 19.937h-.397l-9.53-32.168h4.17l5.759 19.58 3.892-13.185-1.906-6.395z"/>
4
<path d="m64.92 1.006c-.819 0-1.554.295-2.111.861-.591.6-.92 1.376-.92 2.178s.313 1.545.887 2.128c.583.591 1.334.912 2.145.912.793 0 1.562-.321 2.161-.903.574-.557.887-1.3.887-2.136 0-.811-.321-1.57-.878-2.136-.584-.592-1.344-.904-2.171-.904zm2.643 3.065c0 .701-.271 1.351-.768 1.832-.524.507-1.174.777-1.892.777-.675 0-1.342-.278-1.84-.785s-.777-1.157-.777-1.849.287-1.368.802-1.891c.481-.49 1.131-.751 1.84-.751.726 0 1.376.271 1.883.785.49.489.752 1.147.752 1.882zm-2.559-1.807h-1.3v3.445h.65v-1.469h.642l.701 1.469h.726l-.769-1.57c.498-.102.785-.439.785-.929 0-.625-.472-.946-1.435-.946zm-.118.422c.608 0 .886.169.886.591 0 .405-.278.549-.87.549h-.549v-1.14z"/>
5
<path d="m59.807.825.676 4.107-2.391 4.575s-.918-1.941-2.443-3.015c-1.285-.905-2.122-1.102-3.431-.832-1.681.347-3.587 2.357-4.419 4.835-.995 2.965-1.005 4.4-1.04 5.718-.056 2.113.277 3.362.277 3.362s-1.452-2.686-1.438-6.62c.009-2.808.451-5.354 1.75-7.867 1.143-2.209 2.842-3.535 4.35-3.691 1.559-.161 2.791.59 3.743 1.403 1 .854 2.01 2.721 2.01 2.721z"/>
6
<path d="m60.102 24.063s-1.057 1.889-1.715 2.617c-.659.728-1.837 2.01-3.292 2.651s-2.218.762-3.656.624c-1.437-.138-2.772-.97-3.24-1.317s-1.664-1.369-2.34-2.322-1.733-2.859-1.733-2.859.589 1.91.958 2.721c.212.467.864 1.894 1.789 3.136.863 1.159 2.539 3.154 5.086 3.604 2.547.451 4.297-.693 4.73-.97s1.346-1.042 1.924-1.66c.603-.645 1.174-1.468 1.49-1.962.231-.36.607-1.092.607-1.092z"/>
7
</svg>
18
A src/test/resources/com/keenwrite/io/images/example.xbm
1
#define 1617524430813_width 72
2
#define 1617524430813_height 48
3
static char 1617524430813_bits[] = {
4
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 
5
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
6
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
7
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 
8
  0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
9
  0x00, 0x00, 0xA8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1A, 
10
  0x10, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0xAA, 0x78, 0x80, 0x07, 
11
  0xC0, 0xFF, 0x3F, 0xE0, 0x41, 0x44, 0x78, 0x80, 0x0F, 0xC0, 0xFF, 0x3F, 
12
  0xF8, 0x67, 0x18, 0xF0, 0x80, 0x07, 0xE0, 0xFF, 0x3F, 0xF8, 0xE7, 0x00, 
13
  0xE0, 0x00, 0x0F, 0xE0, 0xFF, 0x1F, 0xFC, 0xFF, 0x00, 0xF0, 0x01, 0x0F, 
14
  0xE0, 0x01, 0x1D, 0xFC, 0x7F, 0x00, 0xF0, 0x01, 0x1F, 0xE0, 0x81, 0x0F, 
15
  0x3E, 0x7C, 0x00, 0xE0, 0x01, 0x1E, 0xE0, 0x81, 0x07, 0x0E, 0x38, 0x00, 
16
  0xE0, 0x01, 0x17, 0xF0, 0xC0, 0x07, 0x0F, 0x30, 0x00, 0xE0, 0x03, 0x3F, 
17
  0xF0, 0xC0, 0x03, 0x07, 0x00, 0x00, 0xC0, 0x03, 0x3F, 0xF8, 0xE0, 0x03, 
18
  0x07, 0x00, 0x00, 0xC0, 0x83, 0x3F, 0x78, 0xF0, 0x01, 0x07, 0x00, 0x00, 
19
  0xC0, 0x83, 0x3F, 0x78, 0xB0, 0x00, 0x03, 0x00, 0x00, 0x80, 0x87, 0x7F, 
20
  0x78, 0xF8, 0x03, 0x03, 0x00, 0x00, 0x80, 0x87, 0x7B, 0x78, 0xFC, 0x07, 
21
  0x03, 0x00, 0x00, 0x80, 0xCF, 0x7B, 0x3C, 0xF8, 0x0F, 0x03, 0x00, 0x00, 
22
  0x80, 0xC7, 0xE3, 0x3E, 0xC8, 0x1F, 0x02, 0x00, 0x00, 0x00, 0xEF, 0xF2, 
23
  0x3C, 0x00, 0x1F, 0x02, 0x00, 0x00, 0x00, 0xEF, 0xF1, 0x1C, 0x00, 0x3E, 
24
  0x02, 0x00, 0x00, 0x00, 0xEF, 0xB1, 0x1F, 0x00, 0x3C, 0x00, 0x00, 0x00, 
25
  0x00, 0xFE, 0xE1, 0x1F, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xF6, 0xE0, 
26
  0x17, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xE0, 0x0F, 0x00, 0x3C, 
27
  0x00, 0x00, 0x00, 0x00, 0x7E, 0xC0, 0x0F, 0x00, 0x7C, 0x00, 0x00, 0x00, 
28
  0x00, 0xFC, 0xC0, 0x0F, 0x00, 0xFC, 0x00, 0x40, 0x00, 0x00, 0x7C, 0x80, 
29
  0x07, 0x03, 0xBC, 0x00, 0x60, 0x00, 0x00, 0x7C, 0xC0, 0xC7, 0x01, 0x3E, 
30
  0x03, 0xE0, 0x00, 0x00, 0x18, 0x80, 0xC7, 0x07, 0x1E, 0x03, 0x70, 0x00, 
31
  0x00, 0x38, 0x80, 0x82, 0x87, 0x1F, 0x0E, 0x7C, 0x00, 0x00, 0x38, 0x00, 
32
  0x03, 0xFF, 0x1D, 0xBC, 0x3F, 0x00, 0x00, 0x30, 0x00, 0x03, 0xFF, 0x0F, 
33
  0xFC, 0x1F, 0x00, 0x00, 0x10, 0x00, 0x03, 0xFC, 0x07, 0xF0, 0x1F, 0x00, 
34
  0x00, 0x10, 0x00, 0x01, 0xF8, 0x01, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 
35
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
36
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
37
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
38
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
39
  0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
40
  };
141
A src/test/resources/com/keenwrite/io/images/example_2.bmp
Binary file
A src/test/resources/com/keenwrite/io/images/example_2.gif
Binary file
A src/test/resources/com/keenwrite/io/images/example_2.jpg
Binary file
A src/test/resources/com/keenwrite/io/images/example_2.png
Binary file
A src/test/resources/com/keenwrite/io/images/example_256.bmp
Binary file
A src/test/resources/com/keenwrite/io/images/example_256.gif
Binary file
A src/test/resources/com/keenwrite/io/images/example_256.jpg
Binary file
A src/test/resources/com/keenwrite/io/images/example_256.png
Binary file
A src/test/resources/com/keenwrite/io/images/example_animation.gif
Binary file
A src/test/resources/com/keenwrite/io/images/example_animation.mng
Binary file
A src/test/resources/com/keenwrite/io/images/example_gray.bmp
Binary file
A src/test/resources/com/keenwrite/io/images/example_gray.gif
Binary file
A src/test/resources/com/keenwrite/io/images/example_gray.jpg
Binary file
A src/test/resources/com/keenwrite/io/images/example_gray.png
Binary file
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 src/test/resources/com/keenwrite/processors/markdown/workspace.xml
1
<?xml version="1.0" encoding="UTF-8"?>
2
<keenwrite>
3
   <workspace>
4
      <images>
5
         <order>svg pdf png jpg tiff</order>
6
         <dir></dir>
7
      </images>
8
   </workspace>
9
</keenwrite>
110
A src/test/sikuli/.gitignore
1
*.class
12
A src/test/sikuli/README.md
1
Sikuli is used for the following purposes:
2
3
* Create application videos.
4
* Create integration tests.
5
16
A src/test/sikuli/demo.sikuli/1594187265140.png
Binary file
A src/test/sikuli/demo.sikuli/1594592396134.png
Binary file
A src/test/sikuli/demo.sikuli/1594593710440.png
Binary file
A src/test/sikuli/demo.sikuli/1594593794335.png
Binary file
A src/test/sikuli/demo.sikuli/1594594984108.png
Binary file
A src/test/sikuli/demo.sikuli/1594689573764.png
Binary file
A src/test/sikuli/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 src/test/sikuli/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 src/test/sikuli/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 src/test/sikuli/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 src/test/sikuli/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 src/test/sikuli/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 src/test/sikuli/editor.sikuli/1594187923258.png
Binary file
A src/test/sikuli/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