Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
A .gitattributes
1
# always use LF line endings
2
3
# ALL FILES:
4
#   Normalize line endings to LF on checkin and
5
#   prevent conversion to CRLF when the file is checked out.
6
7
* text eol=lf
8
9
10
# BINARY FILES:
11
#   Disable line ending normalize on checkin.
12
13
*.blend binary
14
15
*.bin binary
16
*.bmp binary
17
*.eps binary
18
*.exe binary
19
*.gif binary
20
*.ico binary
21
*.jar binary
22
*.jpg binary
23
*.mng binary
24
*.png binary
25
*.zip binary
26
*.otf binary
27
*.ttf binary
28
129
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
tex
16
spell
17
keenwrite.build_artifacts.txt
18
todo
119
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 21](https://bell-sw.com/pages/downloads) (Full JDK + JavaFX)
10
* [Gradle 8.3](https://gradle.org/releases)
11
* [Git 2.40.1](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
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` script to build platform-specific binaries, such as:
87
88
    ./installer -V -o linux
89
90
The `installer` 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 -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 2020 White Magic Software, Ltd.
4
5
Copyright 2015 Karl Tauber
6
7
All rights reserved.
8
9
Redistribution and use in source and binary forms, with or without
10
modification, are permitted provided that the following conditions are met:
11
12
* Redistributions of source code must retain the above copyright
13
  notice, this list of conditions and the following disclaimer.
14
15
* Redistributions in binary form must reproduce the above copyright
16
  notice, this list of conditions and the following disclaimer in the
17
  documentation and/or other materials provided with the distribution.
18
19
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
130
A R/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 filename to convert.
31
# @param decimals Rounded decimal places (default 1).
32
# @param totals Include total sums (default TRUE).
33
# @param align Right-align numbers (default TRUE).
34
# -----------------------------------------------------------------------------
35
csv2md <- function( f, decimals = 2, totals = T, align = T, caption = "" ) {
36
  # Read the CVS data from the file; ensure strings become characters.
37
  df <- read.table( f, sep=',', header=T, stringsAsFactors=F )
38
39
  if( totals ) {
40
    # Determine what columns can be summed.
41
    number <- which( unlist( lapply( df, is.numeric ) ) )
42
43
    # Use colSums when more than one summable column exists.
44
    if( length( number ) > 1 ) {
45
      f.sum <- colSums
46
    }
47
    else {
48
      f.sum <- sum
49
    }
50
51
    # Calculate the sum of all the summable columns and insert the
52
    # results back into the data frame.
53
    df[ (nrow( df ) + 1), number ] <- f.sum( df[, number], na.rm=TRUE )
54
55
    # 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
# ![Logo](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
Download one of the following editions:
8
9
* [Windows](https://github.com/DaveJarvis/keenwrite/releases/latest/download/keenwrite.exe)
10
* [Linux](https://github.com/DaveJarvis/keenwrite/releases/latest/download/keenwrite.bin)
11
* [Java Archive](https://github.com/DaveJarvis/keenwrite/releases/latest/download/keenwrite.jar)
12
13
## Run
14
15
Note that the first time the application runs, it will unpack itself into a local directory. Subsequent starts will be faster.
16
17
### Windows
18
19
Double-click the application to start; give the application permission to run.
20
21
### Linux
22
23
Execute the following commands in a terminal:
24
25
``` bash
26
chmod +x keenwrite.bin
27
./keenwrite.bin
28
```
29
30
### Other
31
32
On other platforms, such as MacOS, start the application as follows:
33
34
1. Download the *Full version* of the Java Runtime Environment, [JRE 21](https://bell-sw.com/pages/downloads).
35
   * JavaFX, which is bundled with BellSoft's *Full version*, is required.
36
1. Install the JRE (include JRE's `bin` directory in the `PATH` environment variable).
37
1. Open a new terminal.
38
1. Verify the installation: `java -version`
39
1. Download [keenwrite.jar](https://github.com/DaveJarvis/keenwrite/releases/latest/download/keenwrite.jar).
40
1. Download [keenwrite.sh](https://raw.githubusercontent.com/DaveJarvis/keenwrite/master/keenwrite.sh).
41
1. Place the `.jar` and `.sh` in the same directory.
42
1. Make `keenwrite.sh` executable: `chmod +x keenwrite.sh`
43
1. Run: `./keenwrite.sh`
44
45
The application is started.
46
47
## Features
48
49
The application offers:
50
51
* User-defined interpolated strings
52
* Auto-complete variable names based on variable values
53
* High-quality PDF exports
54
* Real-time spell check
55
* Real-time rendering of math using TeX notation
56
* Real-time document statistics (with CJK word separation)
57
* Diagrams: Mermaid, GraphViz, UML, sequence, timing, and more
58
* Dark, custom, and responsive user interface skins
59
* Integrated file manager
60
* Interactive document outline
61
* Internationalized font support (e.g., Chinese, Japanese, Korean, etc.)
62
* Support for Pandoc's fenced div extended attribute syntax
63
* R integration
64
* Customizable user interface having detachable tabs
65
* Platform-independent (Windows, Linux, MacOS)
66
67
## Typesetting
68
69
Typesetting to PDF files requires the following:
70
71
* [Theme Pack](https://github.com/DaveJarvis/keenwrite-themes/releases/latest/download/theme-pack.zip)
72
* [ConTeXt](https://wiki.contextgarden.net/Installation)
73
74
## Usage
75
76
Read the [detailed documentation](docs/README.md) for using the application.
77
78
### Skins
79
80
Read the [skins documentation](docs/skins.md) to learn about how to change
81
the user interface appearance.
82
83
## Screenshots
84
85
See [screenshots](docs/screenshots.md) for visuals.
86
87
## License
88
89
This software is licensed under the [BSD 2-Clause License](LICENSE.md) and
90
based on [Markdown-Writer-FX](https://github.com/JFormDesigner/markdown-writer-fx/blob/main/LICENSE).
91
192
A README.zh-CN.md
1
# ![Logo](docs/images/app-title.zh-CN.png)
2
3
智能写入是一个文本编辑器,它使用插值字符串引用外部定义的值。
4
5
## 下载
6
7
下载以下版本之一:
8
9
* [Windows](https://github.com/DaveJarvis/keenwrite/releases/latest/download/keenwrite.exe)
10
* [Linux](https://github.com/DaveJarvis/keenwrite/releases/latest/download/keenwrite.bin)
11
* [Java Archive](https://github.com/DaveJarvis/keenwrite/releases/latest/download/keenwrite.jar)
12
13
## 跑
14
15
在第一次运行期间,应用程序将自身解压到本地目录中。随后的启动会更快。
16
17
### Windows
18
19
双击应用程序以启动。您必须授予应用程序运行权限。 
20
21
升级时,删除以下目录:
22
23
    C:\Users\%USERNAME%\AppData\Local\warp\packages\keenwrite.exe
24
25
### Linux
26
27
执行以下命令:
28
29
``` bash
30
chmod +x keenwrite.bin
31
./keenwrite.bin
32
```
33
34
### Other
35
36
Download and install a full version of [OpenJDK 20](https://bell-sw.com/pages/downloads) that includes JavaFX module support, then run:
37
38
``` bash
39
java -jar keenwrite.jar
40
```
41
42
## 特征
43
44
* 用户定义的插值字符串
45
* 带变量替换的实时预览
46
* 基于变量值自动完成变量名
47
* 独立于操作系统
48
* 打字时拼写检查
49
* 使用TeX的子集编写数学公式
50
* 嵌入R语句
51
52
## Typesetting
53
54
排版到 PDF 文件需要以下內容:
55
56
* [Theme Pack](https://github.com/DaveJarvis/keenwrite-themes/releases/latest/download/theme-pack.zip)
57
* [ConTeXt](https://wiki.contextgarden.net/Installation)
58
59
## 软件使用
60
61
有關使用該應用程序的信息,請參[閱詳細文檔](docs/README.md)。
62
63
## 截图
64
65
![GraphViz Diagram Screenshot](docs/images/screenshots/01.png)
66
67
![Korean Poem Screenshot](docs/images/screenshots/02.png)
68
69
![TeX Equations Screenshot](docs/images/screenshots/03.png)
70
71
72
## 软件许可证
73
74
This software is licensed under the [BSD 2-Clause License](LICENSE.md) and
75
based on [Markdown-Writer-FX](licenses/MARKDOWN-WRITER-FX.md).
76
177
A bug-filter.xml
1
<?xml version="1.0" encoding="UTF-8"?>
2
<FindBugsFilter>
3
  <Match>
4
    <Or>
5
      <Bug code="EI, EI2" />
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.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:8.2.1'
12
    classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.14"
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.0.0'
20
  id "com.github.spotbugs" version "5.1.3"
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
      includeGroup 'com.github.css4j'
44
      includeGroup 'io.sf.carte'
45
      includeGroup 'io.sf.jclf'
46
    }
47
  }
48
}
49
50
// Assume a cross-platform überjar unless targetOs is set.
51
String[] os = ['win', 'mac', 'linux']
52
53
if (project.hasProperty( 'targetOs' )) {
54
  if ('windows' == targetOs) {
55
    os = ["win"]
56
  } else if ('macos' == targetOs) {
57
    os = ["mac"]
58
  } else {
59
    os = [targetOs]
60
  }
61
}
62
63
def moduleSecurity = [
64
    '--add-opens=javafx.graphics/javafx.scene=ALL-UNNAMED',
65
    '--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED',
66
    '--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED',
67
    '--add-opens=javafx.graphics/javafx.scene.text=ALL-UNNAMED',
68
    '--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED',
69
    '--add-opens=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED',
70
    '--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED',
71
    '--add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED',
72
    '--add-exports=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED',
73
    '--add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED',
74
    '--add-exports=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED',
75
    '--add-exports=javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED',
76
    '--add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED',
77
]
78
79
java {
80
  sourceCompatibility = 21
81
  targetCompatibility = 21
82
}
83
84
javafx {
85
  version = '21'
86
  modules = ['javafx.base', 'javafx.controls', 'javafx.graphics', 'javafx.swing']
87
  configuration = 'compileOnly'
88
}
89
90
dependencies {
91
  def v_junit = '5.9.3'
92
  def v_flexmark = '0.64.8'
93
  def v_jackson = '2.15.2'
94
  def v_echosvg = '1.0'
95
  def v_picocli = '4.7.5'
96
97
  // JavaFX
98
  implementation 'org.controlsfx:controlsfx:11.1.2'
99
  implementation 'org.fxmisc.richtext:richtextfx:0.11.1'
100
  implementation 'org.fxmisc.flowless:flowless:0.7.1'
101
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
102
  implementation 'com.miglayout:miglayout-javafx:11.1'
103
  implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.16.0'
104
  implementation 'com.panemu:tiwulfx-dock:0.2'
105
106
  // Markdown
107
  implementation "com.vladsch.flexmark:flexmark:${v_flexmark}"
108
  implementation "com.vladsch.flexmark:flexmark-ext-definition:${v_flexmark}"
109
  implementation "com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:${v_flexmark}"
110
  implementation "com.vladsch.flexmark:flexmark-ext-superscript:${v_flexmark}"
111
  implementation "com.vladsch.flexmark:flexmark-ext-tables:${v_flexmark}"
112
  implementation "com.vladsch.flexmark:flexmark-ext-typographic:${v_flexmark}"
113
114
  // YAML
115
  implementation 'org.yaml:snakeyaml:2.2'
116
  implementation "com.fasterxml.jackson.core:jackson-core:${v_jackson}"
117
  implementation "com.fasterxml.jackson.core:jackson-databind:${v_jackson}"
118
  implementation "com.fasterxml.jackson.core:jackson-annotations:${v_jackson}"
119
  implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${v_jackson}"
120
121
  // HTML parsing and rendering
122
  implementation 'org.jsoup:jsoup:1.16.1'
123
  implementation 'org.xhtmlrenderer:flying-saucer-core:9.3.1'
124
125
  // R
126
  implementation 'org.apache.commons:commons-compress:1.24.0'
127
  implementation 'org.codehaus.plexus:plexus-utils:4.0.0'
128
  implementation 'org.renjin:renjin-script-engine:3.5-beta76'
129
  implementation 'org.renjin.cran:rjson:0.2.15-renjin-21'
130
131
  // SVG
132
  implementation "io.sf.carte:echosvg-awt-util:${v_echosvg}"
133
  implementation "io.sf.carte:echosvg-bridge:${v_echosvg}"
134
  implementation "io.sf.carte:echosvg-css:${v_echosvg}"
135
  implementation "io.sf.carte:echosvg-dom:${v_echosvg}"
136
  implementation "io.sf.carte:echosvg-ext:${v_echosvg}"
137
  implementation "io.sf.carte:echosvg-gvt:${v_echosvg}"
138
  implementation "io.sf.carte:echosvg-parser:${v_echosvg}"
139
  implementation "io.sf.carte:echosvg-script:${v_echosvg}"
140
  implementation "io.sf.carte:echosvg-svg-dom:${v_echosvg}"
141
  implementation "io.sf.carte:echosvg-svggen:${v_echosvg}"
142
  implementation "io.sf.carte:echosvg-transcoder:${v_echosvg}"
143
  implementation "io.sf.carte:echosvg-util:${v_echosvg}"
144
  implementation "io.sf.carte:echosvg-xml:${v_echosvg}"
145
146
  // Misc.
147
  implementation 'org.ahocorasick:ahocorasick:0.6.3'
148
  implementation 'org.apache.commons:commons-configuration2:2.9.0'
149
  implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
150
  implementation 'javax.validation:validation-api:2.0.1.Final'
151
  implementation 'org.greenrobot:eventbus-java:3.3.1'
152
153
  // Command-line parsing
154
  implementation "info.picocli:picocli:${v_picocli}"
155
  annotationProcessor "info.picocli:picocli-codegen:${v_picocli}"
156
157
  // KeenQuotes, KeenType, KeenSpell, word split.
158
  implementation fileTree( include: ['**/*.jar'], dir: 'libs' )
159
160
  def fx = ['controls', 'graphics', 'fxml', 'swing']
161
162
  fx.each { fxitem ->
163
    os.each { ositem ->
164
      runtimeOnly "org.openjfx:javafx-${fxitem}:${javafx.version}:${ositem}"
165
    }
166
  }
167
168
  testImplementation 'org.testfx:testfx-junit5:4.0.17'
169
  testImplementation "org.junit.jupiter:junit-jupiter-api:${v_junit}"
170
  testImplementation "org.junit.jupiter:junit-jupiter-params:${v_junit}"
171
  testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
172
}
173
174
sourceSets {
175
  main {
176
    java {
177
      srcDirs 'src/main'
178
    }
179
  }
180
181
  test {
182
    java {
183
      srcDirs 'src/test'
184
    }
185
  }
186
}
187
188
final resourceDir = sourceSets.main.resources.srcDirs[0]
189
final Properties config = new Properties()
190
final File configFile = file( "${resourceDir}/bootstrap.properties" )
191
final FileInputStream configStream = new FileInputStream( configFile )
192
config.load( configStream )
193
configStream.close()
194
195
final String applicationName = config.get( 'application.title' ).toString().toLowerCase()
196
final String applicationPackage = "com.${applicationName}"
197
final String applicationClass = "${applicationPackage}.Launcher"
198
199
compileJava {
200
  options.compilerArgs += [
201
      "-Xlint:unchecked",
202
      "-Xlint:deprecation",
203
      "-Aproject=${applicationPackage}/${applicationName}"
204
  ]
205
}
206
207
application {
208
  mainClass.set( applicationClass )
209
  applicationDefaultJvmArgs = moduleSecurity
210
}
211
212
version = gitVersion()
213
214
final File p = new File( "${resourceDir}/com/${applicationName}/app.properties" )
215
p.write( "application.version=${version}" )
216
217
jar {
218
  duplicatesStrategy = DuplicatesStrategy.EXCLUDE
219
220
  doFirst {
221
    manifest {
222
      attributes 'Main-Class': applicationClass
223
    }
224
  }
225
226
  from {
227
    (configurations.runtimeClasspath.findAll { !it.path.endsWith( ".pom" ) })
228
        .collect { it.isDirectory() ? it : zipTree( it ) }
229
  }
230
231
  archiveFileName = "${applicationName}.jar"
232
233
  exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
234
}
235
236
distributions {
237
  main {
238
    distributionBaseName.set( applicationName )
239
240
    contents {
241
      from { ['LICENSE.md', 'README.md'] }
242
      into( 'images' ) {
243
        from { 'images' }
244
      }
245
    }
246
  }
247
}
248
249
test {
250
  useJUnitPlatform()
251
252
  doFirst { jvmArgs = moduleSecurity }
253
  testLogging { exceptionFormat = 'full' }
254
}
1255
A container/.gitignore
1
token.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
9
LABEL org.opencontainers.image.description Configures a typesetting system.
10
11
FROM alpine:latest
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
26
# ########################################################################
27
#
28
# Download all required dependencies
29
#
30
# ########################################################################
31
WORKDIR $DOWNLOAD_DIR
32
33
# Carlito (Calibri replacement)
34
ADD "https://github.com/googlefonts/carlito/raw/main/fonts/ttf/Carlito-Regular.ttf" "Carlito-Regular.ttf"
35
ADD "https://github.com/googlefonts/carlito/raw/main/fonts/ttf/Carlito-Bold.ttf" "Carlito-Bold.ttf"
36
ADD "https://github.com/googlefonts/carlito/raw/main/fonts/ttf/Carlito-Italic.ttf" "Carlito-Italic.ttf"
37
ADD "https://github.com/googlefonts/carlito/raw/main/fonts/ttf/Carlito-BoldItalic.ttf" "Carlito-BoldItalic.ttf"
38
39
# Open Sans Emoji
40
ADD "https://github.com/MorbZ/OpenSansEmoji/raw/master/OpenSansEmoji.ttf" "OpenSansEmoji.ttf"
41
42
# Underwood Quiet Tab
43
ADD "https://site.xavier.edu/polt/typewriters/Underwood_Quiet_Tab.ttf" "Underwood_Quiet_Tab.ttf"
44
45
# Archives
46
ADD "https://www.omnibus-type.com/wp-content/uploads/Archivo-Narrow.zip" "archivo-narrow.zip"
47
ADD "https://fonts.google.com/download?family=Courier%20Prime" "courier-prime.zip"
48
ADD "https://fonts.google.com/download?family=Inconsolata" "inconsolata.zip"
49
ADD "https://fonts.google.com/download?family=Libre%20Baskerville" "libre-baskerville.zip"
50
ADD "https://fonts.google.com/download?family=Niconne" "niconne.zip"
51
ADD "https://fonts.google.com/download?family=Nunito" "nunito.zip"
52
ADD "https://fonts.google.com/download?family=Roboto" "roboto.zip"
53
ADD "https://fonts.google.com/download?family=Roboto%20Mono" "roboto-mono.zip"
54
ADD "https://github.com/adobe-fonts/source-serif/releases/download/4.004R/source-serif-4.004.zip" "source-serif.zip"
55
56
# Typesetting software
57
ADD "http://lmtx.pragma-ade.nl/install-lmtx/context-linuxmusl.zip" "context.zip"
58
59
# ########################################################################
60
#
61
# Install components, modules, configure system, remove unnecessary files
62
#
63
# ########################################################################
64
WORKDIR $CONTEXT_HOME
65
66
RUN \
67
  apk --update --no-cache \
68
    add ca-certificates curl fontconfig inkscape rsync && \
69
  mkdir -p \
70
    "$FONTS_DIR" "$INSTALL_DIR" \
71
    "$TARGET_DIR" "$SOURCE_DIR" "$THEMES_DIR" "$IMAGES_DIR" "$CACHES_DIR" && \
72
  echo "export CONTEXT_HOME=\"$CONTEXT_HOME\"" >> $PROFILE && \
73
  echo "export PATH=\"\$PATH:\$CONTEXT_HOME/tex/texmf-linuxmusl/bin\"" >> $PROFILE && \
74
  echo "export OSFONTDIR=\"/usr/share/fonts//\"" >> $PROFILE && \
75
  echo "PS1='\\u@typesetter:\\w\\$ '" >> $PROFILE && \
76
  unzip -d $CONTEXT_HOME $DOWNLOAD_DIR/context.zip && \
77
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/archivo-narrow.zip "Archivo-Narrow/otf/*.otf" && \
78
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/courier-prime.zip "*.ttf" && \
79
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/libre-baskerville.zip "*.ttf" && \
80
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/inconsolata.zip "**/Inconsolata/*.ttf" && \
81
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/niconne.zip "*.ttf" && \
82
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/nunito.zip "static/*.ttf" && \
83
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/roboto.zip "*.ttf" && \
84
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/roboto-mono.zip "static/*.ttf" && \
85
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/source-serif.zip "source-serif-4.004/OTF/SourceSerif4-*.otf" && \
86
  mv $DOWNLOAD_DIR/*tf $FONTS_DIR && \
87
  fc-cache -f -v && \
88
  mkdir -p tex && \
89
  #rsync \
90
    #--recursive --links --times \
91
    #--info=progress2,remove,symsafe,flist,del \
92
    #--human-readable --del \
93
    #rsync://contextgarden.net/minimals/current/modules/ modules && \
94
  #rsync \
95
    #-rlt --exclude=/VERSION --del modules/*/ tex/texmf-modules && \
96
  sh install.sh && \
97
  rm -f $DOWNLOAD_DIR/*.zip && \
98
  rm -rf \
99
    "modules" \
100
    "/var/cache" \
101
    "/usr/share/icons" \
102
    $CONTEXT_HOME/tex/texmf-modules/doc \
103
    $CONTEXT_HOME/tex/texmf-context/doc && \
104
  mkdir -p $CONTEXT_HOME/tex/texmf-fonts/tex/context/user && \
105
  ln -s $CONTEXT_HOME/tex/texmf-fonts/tex/context/user $HOME/fonts && \
106
  source $PROFILE && \
107
  mtxrun --generate && \
108
  find \
109
    /usr/share/inkscape \
110
    -type f -not -iname \*.xml -exec rm {} \; && \
111
  find \
112
    $CONTEXT_HOME \
113
    -type f \
114
      \( -iname \*.pdf -o -iname \*.txt -o -iname \*.log \) \
115
    -exec rm {} \;
116
117
# ########################################################################
118
#
119
# Ensure login goes to the target directory. ConTeXt prefers to export to
120
# the current working directory.
121
#
122
# ########################################################################
123
WORKDIR $TARGET_DIR
124
1125
A container/README.md
1
# Overview
2
3
This document describes how to maintain the containerized typesetting system.
4
Broadly, the container is built locally then deployed to a web server capable
5
of 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
Upgrade the containerization software as follows:
16
17
1. Edit `src/main/resources/com/keenwrite/messages.properties`.
18
1. Set `Wizard.typesetter.container.version` to the latest version.
19
1. Set `Wizard.typesetter.container.checksum` to the Windows version checksum.
20
1. Set `Wizard.typesetter.container.image.version` to the latest image version.
21
1. Save the file.
22
23
The containerization software versions are changed.
24
25
# Publish
26
27
Publish the changes to the container image as follows:
28
29
``` bash
30
./manage.sh --build
31
./manage.sh --export
32
./manage.sh --publish
33
```
34
35
The container image is published.
36
137
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
source ../scripts/build-template
10
11
# Reads the value of a property from a properties file.
12
#
13
# $1 - The key name to obtain.
14
function property {
15
  grep "^${1}" "${PROPERTIES}" | cut -d'=' -f2
16
}
17
18
readonly BUILD_DIR=build
19
readonly PROPERTIES="${SCRIPT_DIR}/../src/main/resources/com/keenwrite/messages.properties"
20
21
readonly CONTAINER_EXE=podman
22
readonly CONTAINER_SHORTNAME=$(property Wizard.typesetter.container.image.name)
23
readonly CONTAINER_VERSION=$(property Wizard.typesetter.container.image.version)
24
readonly CONTAINER_NETWORK=host
25
readonly CONTAINER_FILE="${CONTAINER_SHORTNAME}-${CONTAINER_VERSION}"
26
readonly CONTAINER_ARCHIVE_FILE="${CONTAINER_FILE}.tar"
27
readonly CONTAINER_ARCHIVE_PATH="${BUILD_DIR}/${CONTAINER_ARCHIVE_FILE}"
28
readonly CONTAINER_COMPRESSED_FILE="${CONTAINER_ARCHIVE_FILE}.gz"
29
readonly CONTAINER_COMPRESSED_PATH="${BUILD_DIR}/${CONTAINER_ARCHIVE_FILE}.gz"
30
readonly CONTAINER_IMAGE_FILE="${BUILD_DIR}/${CONTAINER_FILE}"
31
readonly CONTAINER_DIR_SOURCE="/root/source"
32
readonly CONTAINER_DIR_TARGET="/root/target"
33
readonly CONTAINER_DIR_IMAGES="/root/images"
34
readonly CONTAINER_DIR_FONTS="/root/fonts"
35
36
ARG_CONTAINER_NAME="${CONTAINER_SHORTNAME}:${CONTAINER_VERSION}"
37
ARG_CONTAINER_COMMAND="context --version"
38
ARG_MOUNTPOINT_SOURCE=""
39
ARG_MOUNTPOINT_TARGET="."
40
ARG_MOUNTPOINT_IMAGES=""
41
ARG_MOUNTPOINT_FONTS="${HOME}/.fonts"
42
43
DEPENDENCIES=(
44
  "podman,https://podman.io"
45
  "tar,https://www.gnu.org/software/tar"
46
  "bzip2,https://gitlab.com/bzip2/bzip2"
47
)
48
49
ARGUMENTS+=(
50
  "b,build,Build container"
51
  "c,connect,Connect to container"
52
  "d,delete,Remove all containers"
53
  "s,source,Set mount point for input document (before typesetting)"
54
  "t,target,Set mount point for output file (after typesetting)"
55
  "i,images,Set mount point for image files (to typeset)"
56
  "f,fonts,Set mount point for font files (during typesetting)"
57
  "l,load,Load container (${CONTAINER_COMPRESSED_PATH})"
58
  "p,publish,Publish the container"
59
  "r,run,Run a command in the container (\"${ARG_CONTAINER_COMMAND}\")"
60
  "x,export,Save container (${CONTAINER_COMPRESSED_PATH})"
61
)
62
63
# ---------------------------------------------------------------------------
64
# Manages the container.
65
# ---------------------------------------------------------------------------
66
execute() {
67
  $do_delete
68
  $do_build
69
  $do_publish
70
  $do_export
71
  $do_load
72
  $do_execute
73
  $do_connect
74
75
  return 1
76
}
77
78
# ---------------------------------------------------------------------------
79
# Deletes all containers.
80
# ---------------------------------------------------------------------------
81
utile_delete() {
82
  $log "Deleting all containers"
83
84
  ${CONTAINER_EXE} rmi --all --force > /dev/null
85
86
  $log "Containers deleted"
87
}
88
89
# ---------------------------------------------------------------------------
90
# Builds the container file in the current working directory.
91
# ---------------------------------------------------------------------------
92
utile_build() {
93
  $log "Building"
94
95
  # Show what commands are run while building, but not the commands' output.
96
  ${CONTAINER_EXE} build \
97
    --network="${CONTAINER_NETWORK}" \
98
    --squash \
99
    -t "${ARG_CONTAINER_NAME}" . | \
100
  grep ^STEP
101
}
102
103
# ---------------------------------------------------------------------------
104
# Publishes the container to the repository.
105
# ---------------------------------------------------------------------------
106
utile_publish() {
107
  local -r TOKEN_FILE="token.txt"
108
109
  if [[ -f "${TOKEN_FILE}" ]]; then
110
    local -r repository=$(cat ${TOKEN_FILE})
111
    local -r remote_file="${CONTAINER_SHORTNAME}:${CONTAINER_VERSION}"
112
    local -r remote_path="${repository}/${remote_file}"
113
114
    $log "Publishing to ${remote_path}"
115
116
    # Path to the repository.
117
    scp -q "${CONTAINER_IMAGE_FILE}" "${remote_path}"
118
  else
119
    error "Create ${TOKEN_FILE} with publish credentials"
120
  fi
121
}
122
123
# ---------------------------------------------------------------------------
124
# Creates the command-line option for a read-only mountpoint.
125
#
126
# $1 - The host directory.
127
# $2 - The guest (container) directory.
128
# $3 - The file system permissions (set to 1 for read-write).
129
# ---------------------------------------------------------------------------
130
get_mountpoint() {
131
  local result=""
132
  local binding="ro"
133
134
  if [ ! -z "${3+x}" ]; then
135
    binding="Z"
136
  fi
137
138
  if [ ! -z "${1}" ]; then
139
    result="-v ${1}:${2}:${binding}"
140
  fi
141
142
  echo "${result}"
143
}
144
145
get_mountpoint_source() {
146
  echo $(get_mountpoint "${ARG_MOUNTPOINT_SOURCE}" "${CONTAINER_DIR_SOURCE}")
147
}
148
149
get_mountpoint_target() {
150
  echo $(get_mountpoint "${ARG_MOUNTPOINT_TARGET}" "${CONTAINER_DIR_TARGET}" 1)
151
}
152
153
get_mountpoint_images() {
154
  echo $(get_mountpoint "${ARG_MOUNTPOINT_IMAGES}" "${CONTAINER_DIR_IMAGES}")
155
}
156
157
get_mountpoint_fonts() {
158
  echo $(get_mountpoint "${ARG_MOUNTPOINT_FONTS}" "${CONTAINER_DIR_FONTS}")
159
}
160
161
# ---------------------------------------------------------------------------
162
# Connects to the container.
163
# ---------------------------------------------------------------------------
164
utile_connect() {
165
  $log "Connecting to container"
166
167
  declare -r mount_source=$(get_mountpoint_source)
168
  declare -r mount_target=$(get_mountpoint_target)
169
  declare -r mount_images=$(get_mountpoint_images)
170
  declare -r mount_fonts=$(get_mountpoint_fonts)
171
172
  $log "mount_source = '${mount_source}'"
173
  $log "mount_target = '${mount_target}'"
174
  $log "mount_images = '${mount_images}'"
175
  $log "mount_fonts = '${mount_fonts}'"
176
177
  ${CONTAINER_EXE} run \
178
    --network="${CONTAINER_NETWORK}" \
179
    --rm \
180
    -it \
181
    ${mount_source} \
182
    ${mount_target} \
183
    ${mount_images} \
184
    ${mount_fonts} \
185
    "${ARG_CONTAINER_NAME}"
186
}
187
188
# ---------------------------------------------------------------------------
189
# Runs a command in the container.
190
#
191
# Examples:
192
#
193
#   ./manage.sh -r "ls /"
194
#   ./manage.sh -r "context --version"
195
# ---------------------------------------------------------------------------
196
utile_execute() {
197
  $log "Running \"${ARG_CONTAINER_COMMAND}\":"
198
199
  ${CONTAINER_EXE} run \
200
    --network=${CONTAINER_NETWORK} \
201
    --rm \
202
    -i \
203
    -t "${ARG_CONTAINER_NAME}" \
204
    /bin/sh --login -c "${ARG_CONTAINER_COMMAND}"
205
}
206
207
# ---------------------------------------------------------------------------
208
# Saves the container to a file.
209
# ---------------------------------------------------------------------------
210
utile_export() {
211
  if [[ -f "${CONTAINER_COMPRESSED_PATH}" ]]; then
212
    warning "${CONTAINER_COMPRESSED_PATH} exists, delete before saving."
213
  else
214
    $log "Saving ${CONTAINER_SHORTNAME} image"
215
216
    mkdir -p "${BUILD_DIR}"
217
218
    ${CONTAINER_EXE} save \
219
      --quiet \
220
      -o "${BUILD_DIR}/${CONTAINER_ARCHIVE_FILE}" \
221
      "${ARG_CONTAINER_NAME}"
222
223
    $log "Compressing to ${CONTAINER_COMPRESSED_PATH}"
224
    gzip "${CONTAINER_ARCHIVE_PATH}"
225
226
    $log "Renaming to ${CONTAINER_IMAGE_FILE}"
227
    mv "${CONTAINER_COMPRESSED_PATH}" "${CONTAINER_IMAGE_FILE}"
228
229
    $log "Saved ${CONTAINER_IMAGE_FILE} image"
230
  fi
231
}
232
233
# ---------------------------------------------------------------------------
234
# Loads the container from a file.
235
# ---------------------------------------------------------------------------
236
utile_load() {
237
  if [[ -f "${CONTAINER_COMPRESSED_PATH}" ]]; then
238
    $log "Loading ${CONTAINER_SHORTNAME} from ${CONTAINER_COMPRESSED_PATH}"
239
240
    ${CONTAINER_EXE} load \
241
      --quiet \
242
      -i "${CONTAINER_COMPRESSED_PATH}"
243
244
    $log "Loaded ${CONTAINER_SHORTNAME} image"
245
  else
246
    warning "Missing ${CONTAINER_COMPRESSED_PATH}; use build followed by save"
247
  fi
248
}
249
250
argument() {
251
  local consume=1
252
253
  case "$1" in
254
    -b|--build)
255
    do_build=utile_build
256
    ;;
257
    -c|--connect)
258
    do_connect=utile_connect
259
    ;;
260
    -d|--delete)
261
    do_delete=utile_delete
262
    ;;
263
    -l|--load)
264
    do_load=utile_load
265
    ;;
266
    -i|--images)
267
    if [ ! -z "${2+x}" ]; then
268
      ARG_MOUNTPOINT_IMAGES="$2"
269
      consume=2
270
    fi
271
    ;;
272
    -t|--target)
273
    if [ ! -z "${2+x}" ]; then
274
      ARG_MOUNTPOINT_TARGET="$2"
275
      consume=2
276
    fi
277
    ;;
278
    -p|--publish)
279
    do_publish=utile_publish
280
    ;;
281
    -r|--run)
282
    do_execute=utile_execute
283
284
    if [ ! -z "${2+x}" ]; then
285
      ARG_CONTAINER_COMMAND="$2"
286
      consume=2
287
    fi
288
    ;;
289
    -s|--source)
290
    if [ ! -z "${2+x}" ]; then
291
      ARG_MOUNTPOINT_SOURCE="$2"
292
      consume=2
293
    fi
294
    ;;
295
    -x|--export)
296
    do_export=utile_export
297
    ;;
298
  esac
299
300
  return ${consume}
301
}
302
303
do_build=:
304
do_connect=:
305
do_delete=:
306
do_execute=:
307
do_load=:
308
do_publish=:
309
do_export=:
310
311
main "$@"
312
1313
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
* [typesetting.md](typesetting.md) -- Document typesetting
14
* [variables.md](variables.md) -- Variable definitions and interpolation
15
16
# Contributions
17
18
* [credits.md](credits.md) -- Thanks to authors of contributing projects
19
* [licenses](licenses) -- Third-party licenses
20
121
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
* Karl Tauber: [Markdown Writer FX](https://github.com/JFormDesigner/markdown-writer-fx)
4
* Tomas Mikula: [RichTextFX](https://github.com/TomasMikula/RichTextFX), [ReactFX](https://github.com/TomasMikula/ReactFX), [WellBehavedFX](https://github.com/TomasMikula/WellBehavedFX), [Flowless](https://github.com/TomasMikula/Flowless), and [UndoFX](https://github.com/TomasMikula/UndoFX)
5
* Mikael Grev: [MigLayout](http://www.miglayout.com/)
6
* Tom Eugelink: [MigPane](https://github.com/mikaelgrev/miglayout/blob/master/javafx/src/main/java/org/tbee/javafx/scene/layout/fxml/MigPane.java)
7
* Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx)
8
* Dieter Holz, [PreferencesFX](https://github.com/dlsc-software-consulting-gmbh/PreferencesFX)
9
* David Croft, [File Preferences](http://www.davidc.net/programming/java/java-preferences-using-file-backing-store)
10
* Alex Bertram, [Renjin](https://www.renjin.org/)
11
* Vladimir Schneider: [flexmark](https://github.com/vsch/flexmark-java)
12
* Shy Shalom, Kohei Taketa: [juniversalchardet](https://github.com/takscape/juniversalchardet)
13
114
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 following conditions are met:
7
8
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.  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
Neither the name of the <ORGANIZATION> nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.  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
11
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12
113
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
        Shy Shalom <shooshX@gmail.com>
22
        Kohei TAKETA <k-tak@void.in> (Java port)
23
24
Alternatively, the contents of this file may be used under the terms of
25
either the GNU General Public License Version 2 or later (the "GPL"), or
26
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27
in which case the provisions of the GPL or the LGPL are applicable instead
28
of those above. If you wish to allow use of your version of this file only
29
under the terms of either the GPL or the LGPL, and not to allow others to
30
use your version of this file under the terms of the MPL, indicate your
31
decision by deleting the provisions above and replace them with the notice
32
and other provisions required by the GPL or the LGPL. If you do not delete
33
the provisions above, a recipient may use your version of this file under
34
the terms of any one of the MPL, the GPL or the LGPL.
35
136
A docs/licenses/JWHEATSHEAF.md
1
Copyright © 2020 Mark Raynsford <code@io7m.com> http://io7m.com
2
3
Permission to use, copy, modify, and/or distribute this software for any
4
purpose with or without fee is hereby granted, provided that the above
5
copyright notice and this permission notice appear in all copies.
6
7
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
114
A docs/licenses/MARKDOWN-WRITER-FX.md
1
Copyright (c) 2015 Karl Tauber <karl@jformdesigner.com>
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without
5
modification, are permitted provided that the following conditions are met:
6
7
* Redistributions of source code must retain the above copyright
8
  notice, this list of conditions and the following disclaimer.
9
10
* Redistributions in binary form must reproduce the above copyright
11
  notice, this list of conditions and the following disclaimer in the
12
  documentation and/or other materials provided with the distribution.
13
14
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
125
A docs/licenses/MIG-LAYOUT.md
1
Copyright (c) 2000 Mikael Grev
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without
5
modification, are permitted provided that the following conditions
6
are met:
7
1. Redistributions of source code must retain the above copyright
8
   notice, this list of conditions and the following disclaimer.
9
2. Redistributions in binary form must reproduce the above copyright
10
   notice, this list of conditions and the following disclaimer in the
11
   documentation and/or other materials provided with the distribution.
12
3. The name of the author may not be used to endorse or promote products
13
   derived from this software without specific prior written permission.
14
15
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
16
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
126
A 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/REACT-FX.md
1
Copyright (c) 2013-2014, Tomas Mikula
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
6
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
8
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
10
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
111
A 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/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/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://github.com/DaveJarvis/keenwrite/tree/master/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://github.com/DaveJarvis/keenwrite/blob/master/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/11/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://github.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 <a href="https://github.com/DaveJarvis/keenwrite/raw/master/scripts/localpath.bat">localpath.bat</a>.
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://gitreleases.dev/gh/DaveJarvis/keenwrite-themes/latest/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?* ([English](https://www.pragma-ade.com/general/manuals/what-is-context.pdf))
163
* *A not so short introduction to ConTeXt* ([English](https://github.com/contextgarden/not-so-short-introduction-to-context/raw/main/en/introCTX_eng.pdf) or [Spanish](https://raw.githubusercontent.com/contextgarden/not-so-short-introduction-to-context/main/es/introCTX_esp.pdf))
164
* *Dealing with XML in ConTeXt MKIV* ([English](https://pragma-ade.com/general/manuals/xml-mkiv.pdf))
165
* *Typographic Programming* ([English](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://github.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
A wizard appears.
19
20
## Windows
21
22
23
## Linux
24
25
26
## macOS
27
128
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/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 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
readonly APP_NAME=$(find "${SCRIPT_DIR}/src" -type f -name "settings.properties" -exec cat {} \; | grep "application.title=" | cut -d'=' -f2)
13
readonly FILE_APP_JAR="${APP_NAME}.jar"
14
15
readonly OPT_JAVA=$(cat << END_OF_ARGS
16
--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED \
17
--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED \
18
--add-opens=javafx.graphics/javafx.scene.text=ALL-UNNAMED \
19
--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED \
20
--add-opens=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED \
21
--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED \
22
--add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED \
23
--add-exports=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED \
24
--add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED \
25
--add-exports=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED \
26
--add-exports=javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED \
27
--add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED
28
END_OF_ARGS
29
)
30
31
ARG_JAVA_OS="linux"
32
ARG_JAVA_ARCH="amd64"
33
ARG_JAVA_VERSION="21"
34
ARG_JAVA_UPDATE="37"
35
ARG_JAVA_DIR="java"
36
37
ARG_DIR_DIST="dist"
38
39
FILE_DIST_EXEC="run.sh"
40
41
ARG_PATH_DIST_JAR="${SCRIPT_DIR}/build/libs/${FILE_APP_JAR}"
42
43
DEPENDENCIES=(
44
  "gradle,https://gradle.org"
45
  "warp-packer,https://github.com/Reisz/warp/releases"
46
  "linux-x64.warp-packer,https://github.com/dgiagio/warp/releases"
47
  "tar,https://www.gnu.org/software/tar"
48
  "wine,https://www.winehq.org"
49
  "unzip,http://infozip.sourceforge.net"
50
)
51
52
ARGUMENTS+=(
53
  "a,arch,Target operating system architecture (amd64)"
54
  "o,os,Target operating system (linux, windows, macos)"
55
  "u,update,Java update version number (${ARG_JAVA_UPDATE})"
56
  "v,version,Full Java version (${ARG_JAVA_VERSION})"
57
)
58
59
ARCHIVE_EXT="tar.gz"
60
ARCHIVE_APP="tar xf"
61
APP_EXTENSION="bin"
62
63
# ---------------------------------------------------------------------------
64
# Generates
65
# ---------------------------------------------------------------------------
66
execute() {
67
  $do_configure_target
68
  $do_build
69
  $do_clean
70
71
  pushd "${ARG_DIR_DIST}" > /dev/null 2>&1
72
73
  $do_extract_java
74
  $do_create_launch_script
75
  $do_copy_archive
76
77
  popd > /dev/null 2>&1
78
79
  $do_create_launcher
80
81
  $do_brand_windows
82
83
  return 1
84
}
85
86
# ---------------------------------------------------------------------------
87
# Configure platform-specific commands and file names.
88
# ---------------------------------------------------------------------------
89
utile_configure_target() {
90
  if [ "${ARG_JAVA_OS}" = "windows" ]; then
91
    ARCHIVE_EXT="zip"
92
    ARCHIVE_APP="unzip -qq"
93
    FILE_DIST_EXEC="run.bat"
94
    APP_EXTENSION="exe"
95
    do_create_launch_script=utile_create_launch_script_windows
96
    do_brand_windows=utile_brand_windows
97
  elif [ "${ARG_JAVA_OS}" = "macos" ]; then
98
    APP_EXTENSION="app"
99
  fi
100
}
101
102
# ---------------------------------------------------------------------------
103
# Build platform-specific überjar.
104
# ---------------------------------------------------------------------------
105
utile_build() {
106
  $log "Delete ${ARG_PATH_DIST_JAR}"
107
  rm -f "${ARG_PATH_DIST_JAR}"
108
109
  $log "Build application for ${ARG_JAVA_OS}"
110
  gradle clean jar -PtargetOs="${ARG_JAVA_OS}"
111
}
112
113
# ---------------------------------------------------------------------------
114
# Purges the existing distribution directory to recreate the launcher.
115
# This refreshes the JRE from the downloaded archive.
116
# ---------------------------------------------------------------------------
117
utile_clean() {
118
  $log "Recreate ${ARG_DIR_DIST}"
119
  rm -rf "${ARG_DIR_DIST}"
120
  mkdir -p "${ARG_DIR_DIST}"
121
}
122
123
# ---------------------------------------------------------------------------
124
# Extract platform-specific Java Runtime Environment. This will download
125
# and cache the required Java Runtime Environment for the target platform.
126
# On subsequent runs, the cached version is used, instead of issuing another
127
# download.
128
# ---------------------------------------------------------------------------
129
utile_extract_java() {
130
  $log "Extract Java"
131
  local -r java_vm="jre"
132
  local -r java_version="${ARG_JAVA_VERSION}+${ARG_JAVA_UPDATE}"
133
134
  java_os="${ARG_JAVA_OS}"
135
  java_arch="${ARG_JAVA_ARCH}"
136
  archive_ext=""
137
138
  if [ "${ARG_JAVA_OS}" = "macos" ]; then
139
    archive_ext=".jre"
140
  fi
141
142
  local -r url_java="https://download.bell-sw.com/java/${java_version}/bellsoft-${java_vm}${java_version}-${java_os}-${java_arch}-full.${ARCHIVE_EXT}"
143
144
  local -r file_java="${java_vm}-${java_version}-${java_os}-${java_arch}.${ARCHIVE_EXT}"
145
  local -r path_java="/tmp/${file_java}"
146
147
  # File must have contents.
148
  if [ ! -s ${path_java} ]; then
149
    $log "Download ${url_java} to ${path_java}"
150
    wget -q "${url_java}" -O "${path_java}"
151
  fi
152
153
  $log "Unpack ${path_java}"
154
  $ARCHIVE_APP "${path_java}"
155
156
  local -r dir_java="${java_vm}-${ARG_JAVA_VERSION}-full${archive_ext}"
157
158
  $log "Rename ${dir_java} to ${ARG_JAVA_DIR}"
159
  mv "${dir_java}" "${ARG_JAVA_DIR}"
160
}
161
162
# ---------------------------------------------------------------------------
163
# Create Linux-specific launch script.
164
# ---------------------------------------------------------------------------
165
utile_create_launch_script_linux() {
166
  $log "Create Linux launch script"
167
168
  cat > "${FILE_DIST_EXEC}" << __EOT
169
#!/usr/bin/env bash
170
171
readonly SCRIPT_SRC="\$(dirname "\${BASH_SOURCE[\${#BASH_SOURCE[@]} - 1]}")"
172
173
"\${SCRIPT_SRC}/${ARG_JAVA_DIR}/bin/java" ${OPT_JAVA} -jar "\${SCRIPT_SRC}/${FILE_APP_JAR}" "\$@" 2>/dev/null
174
__EOT
175
176
  chmod +x "${FILE_DIST_EXEC}"
177
}
178
179
# ---------------------------------------------------------------------------
180
# Create Windows-specific launch script.
181
# ---------------------------------------------------------------------------
182
utile_create_launch_script_windows() {
183
  $log "Create Windows launch script"
184
185
  cat > "${FILE_DIST_EXEC}" << __EOT
186
@echo off
187
188
set SCRIPT_DIR=%~dp0
189
"%SCRIPT_DIR%\\${ARG_JAVA_DIR}\\bin\\java" ${OPT_JAVA} -jar "%SCRIPT_DIR%\\${APP_NAME}.jar" %* 2>nul
190
__EOT
191
192
  # Convert Unix end of line characters (\n) to Windows format (\r\n).
193
  # This avoids any potential line conversion issues with the repository.
194
  sed -i 's/$/\r/' "${FILE_DIST_EXEC}"
195
}
196
197
# ---------------------------------------------------------------------------
198
# Modify the binary to include icon and identifying information.
199
# ---------------------------------------------------------------------------
200
utile_brand_windows() {
201
  # Read the properties file to get the application name (case sensitvely).
202
  while IFS='=' read -r key value
203
  do
204
    key=$(echo $key | tr '.' '_')
205
    eval ${key}=\${value}
206
  done < "src/main/resources/bootstrap.properties"
207
208
  readonly BINARY="${APP_NAME}.exe"
209
  readonly VERSION=$(git describe --tags)
210
  readonly COMPANY="White Magic Software, Ltd."
211
  readonly YEAR=$(date +%Y)
212
  readonly DESCRIPTION="Markdown editor with live preview, variables, and math."
213
  readonly SIZE=$(stat --format="%s" ${BINARY})
214
215
  wine ${SCRIPT_DIR}/scripts/rcedit-x64.exe "${BINARY}" \
216
    --set-icon "scripts/logo.ico" \
217
    --set-version-string "OriginalFilename" "${application_title}.exe" \
218
    --set-version-string "CompanyName" "${COMPANY}" \
219
    --set-version-string "ProductName" "${application_title}" \
220
    --set-version-string "LegalCopyright" "Copyright ${YEAR} ${COMPANY}" \
221
    --set-version-string "FileDescription" "${DESCRIPTION}" \
222
    --set-version-string "Size" "${DESCRIPTION}" \
223
    --set-product-version "${VERSION}" \
224
    --set-file-version "${VERSION}"
225
226
  mv -f "${BINARY}" "${application_title}.exe"
227
}
228
229
# ---------------------------------------------------------------------------
230
# Copy application überjar.
231
# ---------------------------------------------------------------------------
232
utile_copy_archive() {
233
  $log "Create copy of ${FILE_APP_JAR}"
234
  cp "${ARG_PATH_DIST_JAR}" "${FILE_APP_JAR}"
235
}
236
237
# ---------------------------------------------------------------------------
238
# Create platform-specific launcher binary.
239
# ---------------------------------------------------------------------------
240
utile_create_launcher() {
241
  packer=warp-packer
242
  packer_opt_pack="pack"
243
  packer_opt_input="input-dir"
244
245
  local -r FILE_APP_NAME="${APP_NAME}.${APP_EXTENSION}"
246
  $log "Create ${FILE_APP_NAME}"
247
248
  # Warp-packer does not overwrite the file.
249
  rm -f "${FILE_APP_NAME}"
250
251
  # Download uses amd64, but warp-packer differs.
252
  if [ "${ARG_JAVA_ARCH}" = "amd64" ]; then
253
    ARG_JAVA_ARCH="x64"
254
  fi
255
256
  # The warp-packer fork that fixes Windows doesn't support MacOS.
257
  if [ "${ARG_JAVA_OS}" = "macos" ]; then
258
    packer=linux-x64.warp-packer
259
    packer_opt_pack=""
260
    packer_opt_input="input_dir"
261
  fi
262
263
  ${packer} \
264
    ${packer_opt_pack} \
265
    --arch "${ARG_JAVA_OS}-${ARG_JAVA_ARCH}" \
266
    --${packer_opt_input} "${ARG_DIR_DIST}" \
267
    --exec "${FILE_DIST_EXEC}" \
268
    --output "${FILE_APP_NAME}" > /dev/null
269
270
  chmod +x "${FILE_APP_NAME}"
271
}
272
273
argument() {
274
  local consume=2
275
276
  case "$1" in
277
    -a|--arch)
278
    ARG_JAVA_ARCH="$2"
279
    ;;
280
    -o|--os)
281
    ARG_JAVA_OS="$2"
282
    ;;
283
    -u|--update)
284
    ARG_JAVA_UPDATE="$2"
285
    ;;
286
    -v|--version)
287
    ARG_JAVA_VERSION="$2"
288
    ;;
289
  esac
290
291
  return ${consume}
292
}
293
294
do_configure_target=utile_configure_target
295
do_build=utile_build
296
do_clean=utile_clean
297
do_extract_java=utile_extract_java
298
do_create_launch_script=utile_create_launch_script_linux
299
do_copy_archive=utile_copy_archive
300
do_create_launcher=utile_create_launcher
301
do_brand_windows=:
302
303
main "$@"
304
1305
A libs/keencount-min.jar
Binary file
A libs/keenquotes.jar
Binary file
A libs/keenspell.jar
Binary file
A libs/keentype-lib.jar
Binary file
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
sign.sh
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.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
import java.util.concurrent.atomic.AtomicInteger;
21
22
import static com.keenwrite.Launcher.terminate;
23
import static com.keenwrite.events.StatusEvent.clue;
24
import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN;
25
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
26
import static java.nio.file.Files.readString;
27
import static java.nio.file.Files.writeString;
28
import static java.util.concurrent.Executors.newFixedThreadPool;
29
import static org.apache.commons.io.FilenameUtils.getExtension;
30
31
/**
32
 * Responsible for executing common commands. These commands are shared by
33
 * both the graphical and the command-line interfaces.
34
 */
35
public class AppCommands {
36
  private static final ExecutorService sExecutor = newFixedThreadPool( 1 );
37
38
  private AppCommands() {
39
  }
40
41
  public static void run( final Arguments args ) {
42
    final var exitCode = new AtomicInteger();
43
44
    final var future = new CompletableFuture<Path>() {
45
      @Override
46
      public boolean complete( final Path path ) {
47
        return super.complete( path );
48
      }
49
50
      @Override
51
      public boolean completeExceptionally( final Throwable ex ) {
52
        clue( ex );
53
        exitCode.set( 1 );
54
55
        return super.completeExceptionally( ex );
56
      }
57
    };
58
59
    file_export( args, future );
60
    sExecutor.shutdown();
61
    future.join();
62
    terminate( exitCode.get() );
63
  }
64
65
  /**
66
   * Converts one or more files into the given file format. If {@code dir}
67
   * is set to true, this will first append all files in the same directory
68
   * as the actively edited file.
69
   *
70
   * @param future Indicates whether the export succeeded or failed.
71
   * @return The path to the exported file as a {@link Future}.
72
   */
73
  @SuppressWarnings( "UnusedReturnValue" )
74
  private static Future<Path> file_export(
75
    final Arguments args, final CompletableFuture<Path> future ) {
76
    assert args != null;
77
    assert future != null;
78
79
    final Callable<Path> callableTask = () -> {
80
      try {
81
        final var context = args.createProcessorContext();
82
        final var outputPath = context.getTargetPath();
83
        final var chain = createProcessors( context );
84
        final var processor = createBootstrapProcessor( chain, context );
85
        final var inputDoc = read( context );
86
        final var outputDoc = processor.apply( inputDoc );
87
88
        // Processors can export binary files. In such cases, processors will
89
        // return null to prevent further processing.
90
        final var result =
91
          outputDoc == null ? null : writeString( outputPath, outputDoc );
92
93
        future.complete( outputPath );
94
        return result;
95
      } catch( final Throwable ex ) {
96
        future.completeExceptionally( ex );
97
        return null;
98
      }
99
    };
100
101
    // Prevent the application from blocking while the processor executes.
102
    return sExecutor.submit( callableTask );
103
  }
104
105
  private static Processor<String> createBootstrapProcessor(
106
    final Processor<String> chain, final ProcessorContext context ) {
107
108
    return context.getSourceType() == TEXT_R_MARKDOWN
109
      ? new RBootstrapProcessor( chain, context )
110
      : chain;
111
  }
112
113
  /**
114
   * Concatenates all the files in the same directory as the given file into
115
   * a string. The extension is determined by the given file name pattern; the
116
   * order files are concatenated is based on their numeric sort order (this
117
   * avoids lexicographic sorting).
118
   * <p>
119
   * If the parent path to the file being edited in the text editor cannot
120
   * be found then this will return the editor's text, without iterating through
121
   * the parent directory. (Should never happen, but who knows?)
122
   * </p>
123
   * <p>
124
   * New lines are automatically appended to separate each file.
125
   * </p>
126
   *
127
   * @param context The {@link ProcessorContext} containing input path,
128
   *                and other command-line parameters.
129
   * @return All files in the same directory as the file being edited
130
   * concatenated into a single string.
131
   */
132
  private static String read( final ProcessorContext context )
133
    throws IOException {
134
    final var concat = context.getConcatenate();
135
    final var inputPath = context.getSourcePath();
136
    final var parent = inputPath.getParent();
137
    final var filename = SysFile.getFileName( inputPath );
138
    final var extension = getExtension( filename );
139
140
    // Short-circuit because: only one file was requested; there is no parent
141
    // directory to scan for files; or there's no extension for globbing.
142
    if( !concat || parent == null || extension.isBlank() ) {
143
      return readString( inputPath );
144
    }
145
146
    final var command = new ConcatenateCommand(
147
      parent, extension, context.getChapters() );
148
    return command.call();
149
  }
150
}
1151
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-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.io.MediaType;
5
import com.keenwrite.io.MediaTypeExtension;
6
7
import java.io.File;
8
import java.nio.file.Path;
9
10
import static com.keenwrite.io.SysFile.toFile;
11
import static java.lang.String.format;
12
import static org.apache.commons.io.FilenameUtils.removeExtension;
13
14
/**
15
 * Provides controls for processor behaviour when transforming input documents.
16
 */
17
public enum ExportFormat {
18
19
  /**
20
   * For HTML exports, encode TeX as SVG. Treat image links relatively.
21
   */
22
  HTML_TEX_SVG( ".html" ),
23
24
  /**
25
   * For HTML exports, encode TeX using {@code $} delimiters, suitable for
26
   * rendering by an external TeX typesetting engine (or online with KaTeX).
27
   * Treat image links relatively.
28
   */
29
  HTML_TEX_DELIMITED( ".html" ),
30
31
  /**
32
   * For XHTML exports, encode TeX using {@code $} delimiters.
33
   */
34
  XHTML_TEX( ".xml" ),
35
36
  /**
37
   * Exports as PDF file format.
38
   */
39
  APPLICATION_PDF( ".pdf" ),
40
41
  /**
42
   * Indicates no special export format is to be created. No extension is
43
   * applicable. Image links must use absolute directories.
44
   */
45
  NONE( "" );
46
47
  /**
48
   * Preferred file name extension for the given file type.
49
   */
50
  private final String mExtension;
51
52
  /**
53
   * Looks up the {@link ExportFormat} based on the given path and subtype.
54
   *
55
   * @param path     The type to find.
56
   * @param modifier The subtype to find (for HTML).
57
   * @return An object to control the output file format.
58
   * @throws IllegalArgumentException The type/subtype could not be found.
59
   */
60
  public static ExportFormat valueFrom( final Path path, final String modifier )
61
    throws IllegalArgumentException {
62
    assert path != null;
63
64
    return valueFrom( MediaType.fromFilename( path ), modifier );
65
  }
66
67
  /**
68
   * Looks up the {@link ExportFormat} based on the given path and subtype.
69
   *
70
   * @param extension The type to find.
71
   * @param modifier  The subtype to find (for HTML).
72
   * @return An object to control the output file format.
73
   * @throws IllegalArgumentException The type/subtype could not be found.
74
   */
75
  public static ExportFormat valueFrom(
76
    final String extension, final String modifier )
77
    throws IllegalArgumentException {
78
    assert extension != null;
79
80
    return valueFrom( MediaTypeExtension.fromExtension( extension ), modifier );
81
  }
82
83
  /**
84
   * Looks up the {@link ExportFormat} based on the given path and subtype.
85
   *
86
   * @param type     The media type to find.
87
   * @param modifier The subtype to find (for HTML).
88
   * @return An object to control the output file format.
89
   * @throws IllegalArgumentException The type/subtype could not be found.
90
   */
91
  public static ExportFormat valueFrom(
92
    final MediaType type, final String modifier ) {
93
    return switch( type ) {
94
      case TEXT_HTML, TEXT_XHTML -> "svg".equalsIgnoreCase( modifier.trim() )
95
        ? HTML_TEX_SVG
96
        : HTML_TEX_DELIMITED;
97
      case APP_PDF -> APPLICATION_PDF;
98
      case TEXT_XML -> XHTML_TEX;
99
      default -> throw new IllegalArgumentException( format(
100
        "Unrecognized format type and subtype: '%s' and '%s'", type, modifier
101
      ) );
102
    };
103
  }
104
105
  ExportFormat( final String extension ) {
106
    mExtension = extension;
107
  }
108
109
  /**
110
   * Returns the given {@link File} with its extension replaced by one that
111
   * matches this {@link ExportFormat} extension.
112
   *
113
   * @param file The file to perform an extension swap.
114
   * @return The given file with its extension replaced.
115
   */
116
  public File toExportFilename( final File file ) {
117
    return new File( removeExtension( file.getName() ) + mExtension );
118
  }
119
120
  /**
121
   * Delegates to {@link #toExportFilename(File)} after converting the given
122
   * {@link Path} to an instance of {@link File}.
123
   *
124
   * @param path The {@link Path} to convert to a {@link File}.
125
   * @return The given path with its extension replaced.
126
   */
127
  public File toExportFilename( final Path path ) {
128
    return toExportFilename( toFile( path ) );
129
  }
130
}
1131
A src/main/java/com/keenwrite/Launcher.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.cmdline.Arguments;
5
import com.keenwrite.cmdline.ColourScheme;
6
import com.keenwrite.cmdline.HeadlessApp;
7
import picocli.CommandLine;
8
9
import java.io.IOException;
10
import java.io.InputStream;
11
import java.util.Properties;
12
import java.util.function.Consumer;
13
import java.util.logging.LogManager;
14
15
import static com.keenwrite.Bootstrap.*;
16
import static com.keenwrite.security.PermissiveCertificate.installTrustManager;
17
import static java.lang.String.format;
18
import static picocli.CommandLine.IParameterExceptionHandler;
19
import static picocli.CommandLine.ParameterException;
20
import static picocli.CommandLine.UnmatchedArgumentException.printSuggestions;
21
22
/**
23
 * Launches the application using the {@link MainApp} class.
24
 *
25
 * <p>
26
 * This is required until modules are implemented, which may never happen
27
 * because the application should be ported away from Java and JavaFX.
28
 * </p>
29
 */
30
public final class Launcher implements Consumer<Arguments> {
31
32
  /**
33
   * Needed for the GUI.
34
   */
35
  private final String[] mArgs;
36
37
  /**
38
   * Responsible for informing the user of an invalid command-line option,
39
   * along with suggestions as to the closest argument name that matches.
40
   */
41
  private static final class ArgHandler implements IParameterExceptionHandler {
42
    /**
43
     * Invoked by the command-line parser when an invalid option is provided.
44
     *
45
     * @param ex   Captures information about the parameter.
46
     * @param args Captures the complete command-line arguments.
47
     * @return The application exit code (non-zero).
48
     */
49
    public int handleParseException(
50
      final ParameterException ex, final String[] args ) {
51
      final var cmd = ex.getCommandLine();
52
      final var writer = cmd.getErr();
53
      final var spec = cmd.getCommandSpec();
54
      final var mapper = cmd.getExitCodeExceptionMapper();
55
56
      writer.println( ex.getMessage() );
57
      printSuggestions( ex, writer );
58
      writer.print( cmd.getHelp().fullSynopsis() );
59
      writer.printf( "Run '%s --help' for details.%n", spec.qualifiedName() );
60
61
      return mapper == null
62
        ? spec.exitCodeOnInvalidInput()
63
        : mapper.getExitCode( ex );
64
    }
65
  }
66
67
  /**
68
   * Returns the application version number retrieved from the application
69
   * properties file. The properties file is generated at build time, which
70
   * keys off the repository.
71
   *
72
   * @return The application version number.
73
   * @throws RuntimeException An {@link IOException} occurred.
74
   */
75
  public static String getVersion() {
76
    try {
77
      final var properties = loadProperties( "app.properties" );
78
      return properties.getProperty( "application.version" );
79
    } catch( final IOException ex ) {
80
      throw new RuntimeException( ex );
81
    }
82
  }
83
84
  /**
85
   * Immediately exits the application.
86
   *
87
   * @param exitCode Code to provide back to the calling shell.
88
   */
89
  public static void terminate( final int exitCode ) {
90
    System.exit( exitCode );
91
  }
92
93
  private static void parse( final String[] args ) {
94
    assert args != null;
95
96
    final var arguments = new Arguments( new Launcher( args ) );
97
    final var parser = new CommandLine( arguments );
98
99
    parser.setColorScheme( ColourScheme.create() );
100
    parser.setParameterExceptionHandler( new ArgHandler() );
101
    parser.setUnmatchedArgumentsAllowed( false );
102
103
    final var exitCode = parser.execute( args );
104
    final var parseResult = parser.getParseResult();
105
106
    if( parseResult.isUsageHelpRequested() ) {
107
      terminate( exitCode );
108
    }
109
    else if( parseResult.isVersionHelpRequested() ) {
110
      showAppInfo();
111
      terminate( exitCode );
112
    }
113
  }
114
115
  @SuppressWarnings( "SameParameterValue" )
116
  private static Properties loadProperties( final String resource )
117
    throws IOException {
118
    final var properties = new Properties();
119
    properties.load( getResourceAsStream( getResourceName( resource ) ) );
120
    return properties;
121
  }
122
123
  private static String getResourceName( final String resource ) {
124
    return format( "%s/%s", getPackagePath(), resource );
125
  }
126
127
  private static String getPackagePath() {
128
    return Launcher.class.getPackageName().replace( '.', '/' );
129
  }
130
131
  private static InputStream getResourceAsStream( final String resource ) {
132
    return Launcher.class.getClassLoader().getResourceAsStream( resource );
133
  }
134
135
  /**
136
   * Logs the message of an error to the console.
137
   *
138
   * @param error The fatal error that could not be handled.
139
   */
140
  private static void log( final Throwable error ) {
141
    var message = error.getMessage();
142
143
    if( message != null && message.toLowerCase().contains( "javafx" ) ) {
144
      message = "Run using a Java Runtime Environment that includes JavaFX.";
145
      out( "ERROR: %s", message );
146
    }
147
    else {
148
      error.printStackTrace( System.err );
149
    }
150
  }
151
152
  /**
153
   * Suppress writing to standard error, suppresses writing log messages.
154
   */
155
  private static void disableLogging() {
156
    LogManager.getLogManager().reset();
157
    // TODO: Delete this after JavaFX/GTK 3 no longer barfs useless warnings.
158
    System.err.close();
159
  }
160
161
  /**
162
   * Writes the given placeholder text to standard output with a new line
163
   * appended.
164
   *
165
   * @param message The format string specifier.
166
   * @param args    The arguments to substitute into the format string.
167
   */
168
  private static void out( final String message, final Object... args ) {
169
    System.out.printf( format( "%s%n", message ), args );
170
  }
171
172
  private static void showAppInfo() {
173
    out( "%n%s version %s", APP_TITLE, APP_VERSION );
174
    out( "Copyright 2016-%s White Magic Software, Ltd.", APP_YEAR );
175
    out( "Portions copyright 2015-2020 Karl Tauber.%n" );
176
  }
177
178
  /**
179
   * Delegates running the application via the command-line argument parser.
180
   * This is the main entry point for the application, regardless of whether
181
   * run from the command-line or as a GUI.
182
   *
183
   * @param args Command-line arguments.
184
   */
185
  public static void main( final String[] args ) {
186
    installTrustManager();
187
    parse( args );
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 args The parsed command-line arguments.
201
   */
202
  @Override
203
  public void accept( final Arguments args ) {
204
    assert args != null;
205
206
    try {
207
      int argCount = mArgs.length;
208
209
      if( args.quiet() ) {
210
        argCount--;
211
      }
212
      else {
213
        showAppInfo();
214
      }
215
216
      if( args.debug() ) {
217
        argCount--;
218
      }
219
      else {
220
        disableLogging();
221
      }
222
223
      if( argCount <= 0 ) {
224
        // When no command-line arguments are provided, launch the GUI.
225
        MainApp.main( mArgs );
226
      }
227
      else {
228
        // When command-line arguments are supplied, run in headless mode.
229
        HeadlessApp.main( args );
230
      }
231
    } catch( final Throwable t ) {
232
      log( t );
233
    }
234
  }
235
}
1236
A src/main/java/com/keenwrite/MainApp.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.cmdline.HeadlessApp;
5
import com.keenwrite.events.HyperlinkOpenEvent;
6
import com.keenwrite.preferences.Workspace;
7
import com.keenwrite.preview.MathRenderer;
8
import com.keenwrite.spelling.impl.Lexicon;
9
import javafx.application.Application;
10
import javafx.event.Event;
11
import javafx.event.EventType;
12
import javafx.scene.input.KeyCode;
13
import javafx.scene.input.KeyEvent;
14
import javafx.stage.Stage;
15
import org.greenrobot.eventbus.Subscribe;
16
17
import java.io.PrintStream;
18
import java.util.function.BooleanSupplier;
19
20
import static com.keenwrite.Bootstrap.APP_TITLE;
21
import static com.keenwrite.constants.GraphicsConstants.LOGOS;
22
import static com.keenwrite.events.Bus.register;
23
import static com.keenwrite.preferences.AppKeys.*;
24
import static com.keenwrite.util.FontLoader.initFonts;
25
import static javafx.scene.input.KeyCode.ESCAPE;
26
import static javafx.scene.input.KeyCode.F11;
27
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
28
import static javafx.scene.input.KeyEvent.KEY_RELEASED;
29
import static javafx.stage.WindowEvent.WINDOW_SHOWN;
30
31
/**
32
 * Application entry point. The application allows users to edit plain text
33
 * files in a markup notation and see a real-time preview of the formatted
34
 * output.
35
 */
36
public final class MainApp extends Application {
37
38
  private Workspace mWorkspace;
39
  private MainScene mMainScene;
40
41
  /**
42
   * TODO: Delete this after JavaFX/GTK 3 no longer barfs useless warnings.
43
   */
44
  @SuppressWarnings( "SameParameterValue" )
45
  private static void stderrRedirect( final PrintStream stream ) {
46
    System.setErr( stream );
47
  }
48
49
  /**
50
   * GUI application entry point. See {@link HeadlessApp} for the entry
51
   * point to the command-line application.
52
   *
53
   * @param args Command-line arguments.
54
   */
55
  public static void main( final String[] args ) {
56
    launch( args );
57
  }
58
59
  /**
60
   * Creates an instance of {@link KeyEvent} that represents pressing a key.
61
   *
62
   * @param code  The key to simulate being pressed down.
63
   * @param shift Whether shift key modifier shall modify the key code.
64
   * @return An instance of {@link KeyEvent} that may be used to simulate
65
   * a key being pressed.
66
   */
67
  public static Event keyDown( final KeyCode code, final boolean shift ) {
68
    return keyEvent( KEY_PRESSED, code, shift );
69
  }
70
71
  /**
72
   * Creates an instance of {@link KeyEvent} that represents a key released
73
   * event without any modifier keys held.
74
   *
75
   * @param code The key code representing a key to simulate releasing.
76
   * @return An instance of {@link KeyEvent}.
77
   */
78
  public static Event keyDown( final KeyCode code ) {
79
    return keyDown( code, false );
80
  }
81
82
  /**
83
   * Creates an instance of {@link KeyEvent} that represents releasing a key.
84
   *
85
   * @param code  The key to simulate being released up.
86
   * @param shift Whether shift key modifier shall modify the key code.
87
   * @return An instance of {@link KeyEvent} that may be used to simulate
88
   * a key being released.
89
   */
90
  @SuppressWarnings( "unused" )
91
  public static Event keyUp( final KeyCode code, final boolean shift ) {
92
    return keyEvent( KEY_RELEASED, code, shift );
93
  }
94
95
  private static Event keyEvent(
96
    final EventType<KeyEvent> type, final KeyCode code, final boolean shift ) {
97
    return new KeyEvent(
98
      type, "", "", code, shift, false, false, false
99
    );
100
  }
101
102
  /**
103
   * JavaFX entry point.
104
   *
105
   * @param stage The primary application stage.
106
   */
107
  @Override
108
  public void start( final Stage stage ) {
109
    // Must be instantiated after the UI is initialized (i.e., not in main)
110
    // because it interacts with GUI properties.
111
    mWorkspace = new Workspace();
112
113
    // The locale was already loaded when the workspace was created. This
114
    // ensures that when the locale preference changes, a new spellchecker
115
    // instance will be loaded and applied.
116
    final var property = mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
117
    property.addListener( ( c, o, n ) -> readLexicon() );
118
119
    initFonts();
120
    initState( stage );
121
    initStage( stage );
122
    initIcons( stage );
123
    initScene( stage );
124
125
    MathRenderer.bindSize( mWorkspace.doubleProperty( KEY_UI_FONT_MATH_SIZE ) );
126
127
    // Load the lexicon and check all the documents after all files are open.
128
    stage.addEventFilter( WINDOW_SHOWN, event -> readLexicon() );
129
    stage.show();
130
131
    stderrRedirect( System.out );
132
133
    register( this );
134
  }
135
136
  private void initState( final Stage stage ) {
137
    final var enable = createBoundsEnabledSupplier( stage );
138
139
    stage.setX( mWorkspace.getDouble( KEY_UI_WINDOW_X ) );
140
    stage.setY( mWorkspace.getDouble( KEY_UI_WINDOW_Y ) );
141
    stage.setWidth( mWorkspace.getDouble( KEY_UI_WINDOW_W ) );
142
    stage.setHeight( mWorkspace.getDouble( KEY_UI_WINDOW_H ) );
143
    stage.setMaximized( mWorkspace.getBoolean( KEY_UI_WINDOW_MAX ) );
144
    stage.setFullScreen( mWorkspace.getBoolean( KEY_UI_WINDOW_FULL ) );
145
146
    mWorkspace.listen( KEY_UI_WINDOW_X, stage.xProperty(), enable );
147
    mWorkspace.listen( KEY_UI_WINDOW_Y, stage.yProperty(), enable );
148
    mWorkspace.listen( KEY_UI_WINDOW_W, stage.widthProperty(), enable );
149
    mWorkspace.listen( KEY_UI_WINDOW_H, stage.heightProperty(), enable );
150
    mWorkspace.listen( KEY_UI_WINDOW_MAX, stage.maximizedProperty() );
151
    mWorkspace.listen( KEY_UI_WINDOW_FULL, stage.fullScreenProperty() );
152
  }
153
154
  private void initStage( final Stage stage ) {
155
    stage.setTitle( APP_TITLE );
156
    stage.addEventHandler( KEY_PRESSED, event -> {
157
      if( F11.equals( event.getCode() ) ) {
158
        stage.setFullScreen( !stage.isFullScreen() );
159
      }
160
    } );
161
162
    // After the app loses focus, when the user switches back using Alt+Tab,
163
    // the menu is engaged on Windows. Simulate an ESC keypress to the menu
164
    // to disable the menu, giving focus back to the application proper.
165
    //
166
    // JavaFX Bug: https://bugs.openjdk.java.net/browse/JDK-8090647
167
    stage.focusedProperty().addListener( ( c, lost, found ) -> {
168
      if( found ) {
169
        mMainScene.getMenuBar().fireEvent( keyDown( ESCAPE ) );
170
      }
171
    } );
172
  }
173
174
  private void initIcons( final Stage stage ) {
175
    stage.getIcons().addAll( LOGOS );
176
  }
177
178
  private void initScene( final Stage stage ) {
179
    mMainScene = new MainScene( mWorkspace );
180
    stage.setScene( mMainScene.getScene() );
181
  }
182
183
  /**
184
   * When a hyperlink website URL is clicked, this method is called to launch
185
   * the default browser to the event's location.
186
   *
187
   * @param event The event called when a hyperlink was clicked.
188
   */
189
  @Subscribe
190
  public void handle( final HyperlinkOpenEvent event ) {
191
    getHostServices().showDocument( event.getUri().toString() );
192
  }
193
194
  /**
195
   * This will load the lexicon for the user's preferred locale and fire
196
   * an event when the all entries in the lexicon have been loaded.
197
   */
198
  private void readLexicon() {
199
    Lexicon.read( mWorkspace.getLocale() );
200
  }
201
202
  /**
203
   * When the window is maximized, full screen, or iconified, prevent updating
204
   * the window bounds. This is used so that if the user exits the application
205
   * when full screen (or maximized), restarting the application will recall
206
   * the previous bounds, allowing for continuity of expected behaviour.
207
   *
208
   * @param stage The window to check for "normal" status.
209
   * @return {@code false} when the bounds must not be changed, ergo persisted.
210
   */
211
  private BooleanSupplier createBoundsEnabledSupplier( final Stage stage ) {
212
    return () ->
213
      !(stage.isMaximized() || stage.isFullScreen() || stage.isIconified());
214
  }
215
}
1216
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.editors.TextDefinition;
5
import com.keenwrite.editors.TextEditor;
6
import com.keenwrite.editors.TextResource;
7
import com.keenwrite.editors.common.ScrollEventHandler;
8
import com.keenwrite.editors.common.VariableNameInjector;
9
import com.keenwrite.editors.definition.DefinitionEditor;
10
import com.keenwrite.editors.definition.TreeTransformer;
11
import com.keenwrite.editors.definition.yaml.YamlTreeTransformer;
12
import com.keenwrite.editors.markdown.MarkdownEditor;
13
import com.keenwrite.events.*;
14
import com.keenwrite.events.spelling.LexiconLoadedEvent;
15
import com.keenwrite.io.MediaType;
16
import com.keenwrite.io.MediaTypeExtension;
17
import com.keenwrite.preferences.Workspace;
18
import com.keenwrite.preview.HtmlPreview;
19
import com.keenwrite.processors.HtmlPreviewProcessor;
20
import com.keenwrite.processors.Processor;
21
import com.keenwrite.processors.ProcessorContext;
22
import com.keenwrite.processors.ProcessorFactory;
23
import com.keenwrite.processors.r.Engine;
24
import com.keenwrite.processors.r.RBootstrapController;
25
import com.keenwrite.service.events.Notifier;
26
import com.keenwrite.spelling.api.SpellChecker;
27
import com.keenwrite.spelling.impl.PermissiveSpeller;
28
import com.keenwrite.spelling.impl.SymSpellSpeller;
29
import com.keenwrite.typesetting.installer.TypesetterInstaller;
30
import com.keenwrite.ui.explorer.FilePickerFactory;
31
import com.keenwrite.ui.heuristics.DocumentStatistics;
32
import com.keenwrite.ui.outline.DocumentOutline;
33
import com.keenwrite.ui.spelling.TextEditorSpellChecker;
34
import com.keenwrite.util.GenericBuilder;
35
import com.panemu.tiwulfx.control.dock.DetachableTab;
36
import com.panemu.tiwulfx.control.dock.DetachableTabPane;
37
import javafx.beans.property.*;
38
import javafx.collections.ListChangeListener;
39
import javafx.concurrent.Task;
40
import javafx.event.ActionEvent;
41
import javafx.event.Event;
42
import javafx.event.EventHandler;
43
import javafx.scene.Node;
44
import javafx.scene.Scene;
45
import javafx.scene.control.SplitPane;
46
import javafx.scene.control.Tab;
47
import javafx.scene.control.TabPane;
48
import javafx.scene.control.Tooltip;
49
import javafx.scene.control.TreeItem.TreeModificationEvent;
50
import javafx.scene.input.KeyEvent;
51
import javafx.stage.Stage;
52
import javafx.stage.Window;
53
import org.greenrobot.eventbus.Subscribe;
54
55
import java.io.File;
56
import java.io.FileNotFoundException;
57
import java.nio.file.Path;
58
import java.util.*;
59
import java.util.concurrent.ExecutorService;
60
import java.util.concurrent.ScheduledExecutorService;
61
import java.util.concurrent.ScheduledFuture;
62
import java.util.concurrent.atomic.AtomicBoolean;
63
import java.util.concurrent.atomic.AtomicReference;
64
import java.util.function.Consumer;
65
import java.util.function.Function;
66
import java.util.stream.Collectors;
67
68
import static com.keenwrite.ExportFormat.NONE;
69
import static com.keenwrite.Launcher.terminate;
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.IdentityProcessor.IDENTITY;
79
import static com.keenwrite.processors.ProcessorContext.Mutator;
80
import static com.keenwrite.processors.ProcessorContext.builder;
81
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
82
import static java.awt.Desktop.getDesktop;
83
import static java.util.concurrent.Executors.newFixedThreadPool;
84
import static java.util.concurrent.Executors.newScheduledThreadPool;
85
import static java.util.concurrent.TimeUnit.SECONDS;
86
import static java.util.stream.Collectors.groupingBy;
87
import static javafx.application.Platform.exit;
88
import static javafx.application.Platform.runLater;
89
import static javafx.scene.control.ButtonType.NO;
90
import static javafx.scene.control.ButtonType.YES;
91
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
92
import static javafx.scene.input.KeyCode.ENTER;
93
import static javafx.scene.input.KeyCode.SPACE;
94
import static javafx.scene.input.KeyCombination.ALT_DOWN;
95
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
96
import static javafx.util.Duration.millis;
97
import static javax.swing.SwingUtilities.invokeLater;
98
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
99
100
/**
101
 * Responsible for wiring together the main application components for a
102
 * particular {@link Workspace} (project). These include the definition views,
103
 * text editors, and preview pane along with any corresponding controllers.
104
 */
105
public final class MainPane extends SplitPane {
106
107
  private static final ExecutorService sExecutor = newFixedThreadPool( 1 );
108
  private static final Notifier sNotifier = Services.load( Notifier.class );
109
110
  /**
111
   * Used when opening files to determine how each file should be binned and
112
   * therefore what tab pane to be opened within.
113
   */
114
  private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of(
115
    TEXT_MARKDOWN, TEXT_R_MARKDOWN, UNDEFINED
116
  );
117
118
  private final ScheduledExecutorService mSaver = newScheduledThreadPool( 1 );
119
  private final AtomicReference<ScheduledFuture<?>> mSaveTask =
120
    new AtomicReference<>();
121
122
  /**
123
   * Prevents re-instantiation of processing classes.
124
   */
125
  private final Map<TextResource, Processor<String>> mProcessors =
126
    new HashMap<>();
127
128
  private final Workspace mWorkspace;
129
130
  /**
131
   * Groups similar file type tabs together.
132
   */
133
  private final List<TabPane> mTabPanes = new ArrayList<>();
134
135
  /**
136
   * Renders the actively selected plain text editor tab.
137
   */
138
  private final HtmlPreview mPreview;
139
140
  /**
141
   * Provides an interactive document outline.
142
   */
143
  private final DocumentOutline mOutline = new DocumentOutline();
144
145
  /**
146
   * Changing the active editor fires the value changed event. This allows
147
   * refreshes to happen when external definitions are modified and need to
148
   * trigger the processing chain.
149
   */
150
  private final ObjectProperty<TextEditor> mTextEditor =
151
    new SimpleObjectProperty<>();
152
153
  /**
154
   * Changing the active definition editor fires the value changed event. This
155
   * allows refreshes to happen when external definitions are modified and need
156
   * to trigger the processing chain.
157
   */
158
  private final ObjectProperty<TextDefinition> mDefinitionEditor =
159
    new SimpleObjectProperty<>();
160
161
  private final ObjectProperty<SpellChecker> mSpellChecker;
162
163
  private final TextEditorSpellChecker mEditorSpeller;
164
165
  /**
166
   * Called when the definition data is changed.
167
   */
168
  private final EventHandler<TreeModificationEvent<Event>> mTreeHandler =
169
    event -> {
170
      process( getTextEditor() );
171
      save( getTextDefinition() );
172
    };
173
174
  /**
175
   * Tracks the number of detached tab panels opened into their own windows,
176
   * which allows unique identification of subordinate windows by their title.
177
   * It is doubtful more than 128 windows, much less 256, will be created.
178
   */
179
  private byte mWindowCount;
180
181
  private final VariableNameInjector mVariableNameInjector;
182
183
  private final RBootstrapController mRBootstrapController;
184
185
  private final DocumentStatistics mStatistics;
186
187
  @SuppressWarnings( {"FieldCanBeLocal", "unused"} )
188
  private final TypesetterInstaller mInstallWizard;
189
190
  /**
191
   * Adds all content panels to the main user interface. This will load the
192
   * configuration settings from the workspace to reproduce the settings from
193
   * a previous session.
194
   */
195
  public MainPane( final Workspace workspace ) {
196
    mWorkspace = workspace;
197
    mSpellChecker = createSpellChecker();
198
    mEditorSpeller = createTextEditorSpellChecker( mSpellChecker );
199
    mPreview = new HtmlPreview( workspace );
200
    mStatistics = new DocumentStatistics( workspace );
201
202
    mTextEditor.addListener( ( c, o, n ) -> {
203
      if( o != null ) {
204
        removeProcessor( o );
205
      }
206
207
      if( n != null ) {
208
        mPreview.setBaseUri( n.getPath() );
209
        updateProcessors( n );
210
        process( n );
211
      }
212
    } );
213
214
    mTextEditor.set( createMarkdownEditor( DOCUMENT_DEFAULT ) );
215
    mDefinitionEditor.set( createDefinitionEditor( workspace ) );
216
    mVariableNameInjector = new VariableNameInjector( workspace );
217
    mRBootstrapController = new RBootstrapController(
218
      workspace, mDefinitionEditor.get()::getDefinitions
219
    );
220
221
    // If the user modifies the definitions, re-process the variables.
222
    mDefinitionEditor.addListener( ( c, o, n ) -> {
223
      final var textEditor = getTextEditor();
224
225
      if( textEditor.isMediaType( TEXT_R_MARKDOWN ) ) {
226
        mRBootstrapController.update();
227
      }
228
229
      process( textEditor );
230
    } );
231
232
    open( collect( getRecentFiles() ) );
233
    viewPreview();
234
    setDividerPositions( calculateDividerPositions() );
235
236
    // Once the main scene's window regains focus, update the active definition
237
    // editor to the currently selected tab.
238
    runLater( () -> getWindow().setOnCloseRequest( event -> {
239
      // Order matters: Open file names must be persisted before closing all.
240
      mWorkspace.save();
241
242
      if( closeAll() ) {
243
        exit();
244
        terminate( 0 );
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
      ( c, o, n ) -> {
354
        final var taskRef = mSaveTask.get();
355
356
        // Prevent multiple autosaves 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
  }
449
450
  /**
451
   * Gives focus to the most recently edited document and attempts to move
452
   * the caret to the most recently known offset into said document.
453
   */
454
  private void restoreSession() {
455
    final var workspace = getWorkspace();
456
    final var file = workspace.fileProperty( KEY_UI_RECENT_DOCUMENT );
457
    final var offset = workspace.integerProperty( KEY_UI_RECENT_OFFSET );
458
459
    for( final var pane : mTabPanes ) {
460
      for( final var tab : pane.getTabs() ) {
461
        final var tooltip = tab.getTooltip();
462
463
        if( tooltip != null ) {
464
          final var tabName = tooltip.getText();
465
          final var fileName = file.get().toString();
466
467
          if( tabName.equalsIgnoreCase( fileName ) ) {
468
            final var node = tab.getContent();
469
470
            pane.getSelectionModel().select( tab );
471
            node.requestFocus();
472
473
            if( node instanceof TextEditor editor ) {
474
              runLater( () -> editor.moveTo( offset.getValue() ) );
475
            }
476
477
            break;
478
          }
479
        }
480
      }
481
    }
482
  }
483
484
  /**
485
   * Sets the focus to the middle pane, which contains the text editor tabs.
486
   */
487
  private void restoreFocus() {
488
    // Work around a bug where focusing directly on the middle pane results
489
    // in the R engine not loading variables properly.
490
    mTabPanes.get( 0 ).requestFocus();
491
492
    // This is the only line that should be required.
493
    mTabPanes.get( 1 ).requestFocus();
494
  }
495
496
  /**
497
   * Opens a new text editor document using the default document file name.
498
   */
499
  public void newTextEditor() {
500
    open( DOCUMENT_DEFAULT );
501
  }
502
503
  /**
504
   * Opens a new definition editor document using the default definition
505
   * file name.
506
   */
507
  @SuppressWarnings( "unused" )
508
  public void newDefinitionEditor() {
509
    open( DEFINITION_DEFAULT );
510
  }
511
512
  /**
513
   * Iterates over all tab panes to find all {@link TextEditor}s and request
514
   * that they save themselves.
515
   */
516
  public void saveAll() {
517
    iterateEditors( this::save );
518
  }
519
520
  /**
521
   * Requests that the active {@link TextEditor} saves itself. Don't bother
522
   * checking if modified first because if the user swaps external media from
523
   * an external source (e.g., USB thumb drive), save should not second-guess
524
   * the user: save always re-saves. Also, it's less code.
525
   */
526
  public void save() {
527
    save( getTextEditor() );
528
  }
529
530
  /**
531
   * Saves the active {@link TextEditor} under a new name.
532
   *
533
   * @param files The new active editor {@link File} reference, must contain
534
   *              at least one element.
535
   */
536
  public void saveAs( final List<File> files ) {
537
    assert files != null;
538
    assert !files.isEmpty();
539
    final var editor = getTextEditor();
540
    final var tab = getTab( editor );
541
    final var file = files.get( 0 );
542
543
    // If the file type has changed, refresh the processors.
544
    final var mediaType = fromFilename( file );
545
    final var typeChanged = !editor.isMediaType( mediaType );
546
547
    if( typeChanged ) {
548
      removeProcessor( editor );
549
    }
550
551
    editor.rename( file );
552
    tab.ifPresent( t -> {
553
      t.setText( editor.getFilename() );
554
      t.setTooltip( createTooltip( file ) );
555
    } );
556
557
    if( typeChanged ) {
558
      updateProcessors( editor );
559
      process( editor );
560
    }
561
562
    save();
563
  }
564
565
  /**
566
   * Saves the given {@link TextResource} to a file. This is typically used
567
   * to save either an instance of {@link TextEditor} or {@link TextDefinition}.
568
   *
569
   * @param resource The resource to export.
570
   */
571
  private void save( final TextResource resource ) {
572
    try {
573
      resource.save();
574
    } catch( final Exception ex ) {
575
      clue( ex );
576
      sNotifier.alert(
577
        getWindow(), resource.getPath(), "TextResource.saveFailed", ex
578
      );
579
    }
580
  }
581
582
  /**
583
   * Closes all open {@link TextEditor}s; all {@link TextDefinition}s stay open.
584
   *
585
   * @return {@code true} when all editors, modified or otherwise, were
586
   * permitted to close; {@code false} when one or more editors were modified
587
   * and the user requested no closing.
588
   */
589
  public boolean closeAll() {
590
    var closable = true;
591
592
    for( final var tabPane : mTabPanes ) {
593
      final var tabIterator = tabPane.getTabs().iterator();
594
595
      while( tabIterator.hasNext() ) {
596
        final var tab = tabIterator.next();
597
        final var resource = tab.getContent();
598
599
        // The definition panes auto-save, so being specific here prevents
600
        // closing the definitions in the situation where the user wants to
601
        // continue editing (i.e., possibly save unsaved work).
602
        if( !(resource instanceof TextEditor) ) {
603
          continue;
604
        }
605
606
        if( canClose( (TextEditor) resource ) ) {
607
          tabIterator.remove();
608
          close( tab );
609
        }
610
        else {
611
          closable = false;
612
        }
613
      }
614
    }
615
616
    return closable;
617
  }
618
619
  /**
620
   * Calls the tab's {@link Tab#getOnClosed()} handler to carry out a close
621
   * event.
622
   *
623
   * @param tab The {@link Tab} that was closed.
624
   */
625
  private void close( final Tab tab ) {
626
    assert tab != null;
627
628
    final var handler = tab.getOnClosed();
629
630
    if( handler != null ) {
631
      handler.handle( new ActionEvent() );
632
    }
633
  }
634
635
  /**
636
   * Closes the active tab; delegates to {@link #canClose(TextResource)}.
637
   */
638
  public void close() {
639
    final var editor = getTextEditor();
640
641
    if( canClose( editor ) ) {
642
      close( editor );
643
      removeProcessor( editor );
644
    }
645
  }
646
647
  /**
648
   * Closes the given {@link TextResource}. This must not be called from within
649
   * a loop that iterates over the tab panes using {@code forEach}, lest a
650
   * concurrent modification exception be thrown.
651
   *
652
   * @param resource The {@link TextResource} to close, without confirming with
653
   *                 the user.
654
   */
655
  private void close( final TextResource resource ) {
656
    getTab( resource ).ifPresent(
657
      tab -> {
658
        close( tab );
659
        tab.getTabPane().getTabs().remove( tab );
660
      }
661
    );
662
  }
663
664
  /**
665
   * Answers whether the given {@link TextResource} may be closed.
666
   *
667
   * @param editor The {@link TextResource} to try closing.
668
   * @return {@code true} when the editor may be closed; {@code false} when
669
   * the user has requested to keep the editor open.
670
   */
671
  private boolean canClose( final TextResource editor ) {
672
    final var editorTab = getTab( editor );
673
    final var canClose = new AtomicBoolean( true );
674
675
    if( editor.isModified() ) {
676
      final var filename = new StringBuilder();
677
      editorTab.ifPresent( tab -> filename.append( tab.getText() ) );
678
679
      final var message = sNotifier.createNotification(
680
        Messages.get( "Alert.file.close.title" ),
681
        Messages.get( "Alert.file.close.text" ),
682
        filename.toString()
683
      );
684
685
      final var dialog = sNotifier.createConfirmation( getWindow(), message );
686
687
      dialog.showAndWait().ifPresent(
688
        save -> canClose.set( save == YES ? editor.save() : save == NO )
689
      );
690
    }
691
692
    return canClose.get();
693
  }
694
695
  private void iterateEditors( final Consumer<TextEditor> consumer ) {
696
    mTabPanes.forEach(
697
      tp -> tp.getTabs().forEach( tab -> {
698
        final var node = tab.getContent();
699
700
        if( node instanceof final TextEditor editor ) {
701
          consumer.accept( editor );
702
        }
703
      } )
704
    );
705
  }
706
707
  /**
708
   * Adds the HTML preview tab to its own, singular tab pane.
709
   */
710
  public void viewPreview() {
711
    addTab( mPreview, TEXT_HTML, "Pane.preview.title" );
712
  }
713
714
  /**
715
   * Adds the document outline tab to its own, singular tab pane.
716
   */
717
  public void viewOutline() {
718
    addTab( mOutline, APP_DOCUMENT_OUTLINE, "Pane.outline.title" );
719
  }
720
721
  public void viewStatistics() {
722
    addTab( mStatistics, APP_DOCUMENT_STATISTICS, "Pane.statistics.title" );
723
  }
724
725
  public void viewFiles() {
726
    try {
727
      final var factory = new FilePickerFactory( getWorkspace() );
728
      final var fileManager = factory.createModeless();
729
      addTab( fileManager, APP_FILE_MANAGER, "Pane.files.title" );
730
    } catch( final Exception ex ) {
731
      clue( ex );
732
    }
733
  }
734
735
  public void viewRefresh() {
736
    mPreview.refresh();
737
    Engine.clear();
738
    mRBootstrapController.update();
739
  }
740
741
  private void addTab(
742
    final Node node, final MediaType mediaType, final String key ) {
743
    final var tabPane = obtainTabPane( mediaType );
744
745
    for( final var tab : tabPane.getTabs() ) {
746
      if( tab.getContent() == node ) {
747
        return;
748
      }
749
    }
750
751
    tabPane.getTabs().add( createTab( get( key ), node ) );
752
    addTabPane( tabPane );
753
  }
754
755
  /**
756
   * Returns the tab that contains the given {@link TextEditor}.
757
   *
758
   * @param editor The {@link TextEditor} instance to find amongst the tabs.
759
   * @return The first tab having content that matches the given tab.
760
   */
761
  private Optional<Tab> getTab( final TextResource editor ) {
762
    return mTabPanes.stream()
763
                    .flatMap( pane -> pane.getTabs().stream() )
764
                    .filter( tab -> editor.equals( tab.getContent() ) )
765
                    .findFirst();
766
  }
767
768
  private TextDefinition createDefinitionEditor( final File file ) {
769
    final var editor = new DefinitionEditor( file, createTreeTransformer() );
770
771
    editor.addTreeChangeHandler( mTreeHandler );
772
773
    return editor;
774
  }
775
776
  /**
777
   * Creates a new {@link DefinitionEditor} wrapped in a listener that
778
   * is used to detect when the active {@link DefinitionEditor} has changed.
779
   * Upon changing, the variables are interpolated and the active text editor
780
   * is refreshed.
781
   *
782
   * @param workspace Has the most recently edited definitions file name.
783
   * @return A newly configured property that represents the active
784
   * {@link DefinitionEditor}, never {@code null}.
785
   */
786
  private TextDefinition createDefinitionEditor(
787
    final Workspace workspace ) {
788
    final var fileProperty = workspace.fileProperty( KEY_UI_RECENT_DEFINITION );
789
    final var filename = fileProperty.get();
790
    final SetProperty<String> recent = workspace.setsProperty(
791
      KEY_UI_RECENT_OPEN_PATH
792
    );
793
794
    // Open the most recently used YAML definition file.
795
    for( final var recentFile : recent.get() ) {
796
      if( recentFile.endsWith( filename.toString() ) ) {
797
        return createDefinitionEditor( new File( recentFile ) );
798
      }
799
    }
800
801
    return createDefaultDefinitionEditor();
802
  }
803
804
  private TextDefinition createDefaultDefinitionEditor() {
805
    final var transformer = createTreeTransformer();
806
    return new DefinitionEditor( transformer );
807
  }
808
809
  private TreeTransformer createTreeTransformer() {
810
    return new YamlTreeTransformer();
811
  }
812
813
  private Tab createTab( final String filename, final Node node ) {
814
    return new DetachableTab( filename, node );
815
  }
816
817
  private Tab createTab( final File file ) {
818
    final var r = createTextResource( file );
819
    final var filename = r.getFilename();
820
    final var tab = createTab( filename, r.getNode() );
821
822
    r.modifiedProperty().addListener(
823
      ( c, o, n ) -> tab.setText( filename + (n ? "*" : "") )
824
    );
825
826
    // This is called when either the tab is closed by the user clicking on
827
    // the tab's close icon or when closing (all) from the file menu.
828
    tab.setOnClosed(
829
      __ -> getRecentFiles().remove( file.getAbsolutePath() )
830
    );
831
832
    // When closing a tab, give focus to the newly revealed tab.
833
    tab.selectedProperty().addListener( ( c, o, n ) -> {
834
      if( n != null && n ) {
835
        final var pane = tab.getTabPane();
836
837
        if( pane != null ) {
838
          pane.requestFocus();
839
        }
840
      }
841
    } );
842
843
    tab.tabPaneProperty().addListener( ( cPane, oPane, nPane ) -> {
844
      if( nPane != null ) {
845
        nPane.focusedProperty().addListener( ( c, o, n ) -> {
846
          if( n != null && n ) {
847
            final var selected = nPane.getSelectionModel().getSelectedItem();
848
            final var node = selected.getContent();
849
            node.requestFocus();
850
          }
851
        } );
852
      }
853
    } );
854
855
    return tab;
856
  }
857
858
  /**
859
   * Creates bins for the different {@link MediaType}s, which eventually are
860
   * added to the UI as separate tab panes. If ever a general-purpose scene
861
   * exporter is developed to serialize a scene to an FXML file, this could
862
   * be replaced by such a class.
863
   * <p>
864
   * When binning the files, this makes sure that at least one file exists
865
   * for every type. If the user has opted to close a particular type (such
866
   * as the definition pane), the view will suppressed elsewhere.
867
   * </p>
868
   * <p>
869
   * The order that the binned files are returned will be reflected in the
870
   * order that the corresponding panes are rendered in the UI.
871
   * </p>
872
   *
873
   * @param paths The file paths to bin according to their type.
874
   * @return An in-order list of files, first by structured definition files,
875
   * then by plain text documents.
876
   */
877
  private List<File> collect( final SetProperty<String> paths ) {
878
    // Treat all files destined for the text editor as plain text documents
879
    // so that they are added to the same pane. Grouping by TEXT_PLAIN is a
880
    // bit arbitrary, but means explicitly capturing TEXT_PLAIN isn't needed.
881
    final Function<MediaType, MediaType> bin =
882
      m -> PLAIN_TEXT_FORMAT.contains( m ) ? TEXT_PLAIN : m;
883
884
    // Create two groups: YAML files and plain text files. The order that
885
    // the elements are listed in the enumeration for media types determines
886
    // what files are loaded first. Variable definitions come before all other
887
    // plain text documents.
888
    final var bins = paths
889
      .stream()
890
      .collect(
891
        groupingBy(
892
          path -> bin.apply( fromFilename( path ) ),
893
          () -> new TreeMap<>( Enum::compareTo ),
894
          Collectors.toList()
895
        )
896
      );
897
898
    bins.putIfAbsent( TEXT_YAML, List.of( DEFINITION_DEFAULT.toString() ) );
899
    bins.putIfAbsent( TEXT_PLAIN, List.of( DOCUMENT_DEFAULT.toString() ) );
900
901
    final var result = new LinkedList<File>();
902
903
    // Ensure that the same types are listed together (keep insertion order).
904
    bins.forEach( ( mediaType, files ) -> result.addAll(
905
      files.stream().map( File::new ).toList() )
906
    );
907
908
    return result;
909
  }
910
911
  /**
912
   * Force the active editor to update, which will cause the processor
913
   * to re-evaluate the interpolated definition map thereby updating the
914
   * preview pane.
915
   *
916
   * @param editor Contains the source document to update in the preview pane.
917
   */
918
  private void process( final TextEditor editor ) {
919
    // Ensure processing does not run on the JavaFX thread, which frees the
920
    // text editor immediately for caret movement. The preview will have a
921
    // slight delay when catching up to the caret position.
922
    final var task = new Task<Void>() {
923
      @Override
924
      public Void call() {
925
        try {
926
          final var p = mProcessors.getOrDefault( editor, IDENTITY );
927
          p.apply( editor == null ? "" : editor.getText() );
928
        } catch( final Exception ex ) {
929
          clue( ex );
930
        }
931
932
        return null;
933
      }
934
    };
935
936
    // TODO: Each time the editor successfully runs the processor, the task is
937
    //   considered successful. Due to the rapid-fire nature of processing
938
    //   (e.g., keyboard navigation, fast typing), it isn't necessary to
939
    //   scroll each time.
940
    //   The algorithm:
941
    //   1. Peek at the oldest time.
942
    //   2. If the difference between the oldest time and current time exceeds
943
    //      250 milliseconds, then invoke the scrolling.
944
    //   3. Insert the current time into the circular queue.
945
    task.setOnSucceeded(
946
      e -> invokeLater( () -> mPreview.scrollTo( CARET_ID ) )
947
    );
948
949
    // Prevents multiple process requests from executing simultaneously (due
950
    // to having a restricted queue size).
951
    sExecutor.execute( task );
952
  }
953
954
  /**
955
   * Lazily creates a {@link TabPane} configured to listen for tab select
956
   * events. The tab pane is associated with a given media type so that
957
   * similar files can be grouped together.
958
   *
959
   * @param mediaType The media type to associate with the tab pane.
960
   * @return An instance of {@link TabPane} that will handle tab docking.
961
   */
962
  private TabPane obtainTabPane( final MediaType mediaType ) {
963
    for( final var pane : mTabPanes ) {
964
      for( final var tab : pane.getTabs() ) {
965
        final var node = tab.getContent();
966
967
        if( node instanceof TextResource r && r.supports( mediaType ) ) {
968
          return pane;
969
        }
970
      }
971
    }
972
973
    final var pane = createTabPane();
974
    mTabPanes.add( pane );
975
    return pane;
976
  }
977
978
  /**
979
   * Creates an initialized {@link TabPane} instance.
980
   *
981
   * @return A new {@link TabPane} with all listeners configured.
982
   */
983
  private TabPane createTabPane() {
984
    final var tabPane = new DetachableTabPane();
985
986
    initStageOwnerFactory( tabPane );
987
    initTabListener( tabPane );
988
989
    return tabPane;
990
  }
991
992
  /**
993
   * When any {@link DetachableTabPane} is detached from the main window,
994
   * the stage owner factory must be given its parent window, which will
995
   * own the child window. The parent window is the {@link MainPane}'s
996
   * {@link Scene}'s {@link Window} instance.
997
   *
998
   * <p>
999
   * This will derives the new title from the main window title, incrementing
1000
   * the window count to help uniquely identify the child windows.
1001
   * </p>
1002
   *
1003
   * @param tabPane A new {@link DetachableTabPane} to configure.
1004
   */
1005
  private void initStageOwnerFactory( final DetachableTabPane tabPane ) {
1006
    tabPane.setStageOwnerFactory( stage -> {
1007
      final var title = get(
1008
        "Detach.tab.title",
1009
        ((Stage) getWindow()).getTitle(), ++mWindowCount
1010
      );
1011
      stage.setTitle( title );
1012
1013
      return getScene().getWindow();
1014
    } );
1015
  }
1016
1017
  /**
1018
   * Responsible for configuring the content of each {@link DetachableTab} when
1019
   * it is added to the given {@link DetachableTabPane} instance.
1020
   * <p>
1021
   * For {@link TextEditor} contents, an instance of {@link ScrollEventHandler}
1022
   * is initialized to perform synchronized scrolling between the editor and
1023
   * its preview window. Additionally, the last tab in the tab pane's list of
1024
   * tabs is given focus.
1025
   * </p>
1026
   * <p>
1027
   * Note that multiple tabs can be added simultaneously.
1028
   * </p>
1029
   *
1030
   * @param tabPane A new {@link TabPane} to configure.
1031
   */
1032
  private void initTabListener( final TabPane tabPane ) {
1033
    tabPane.getTabs().addListener(
1034
      ( final ListChangeListener.Change<? extends Tab> listener ) -> {
1035
        while( listener.next() ) {
1036
          if( listener.wasAdded() ) {
1037
            final var tabs = listener.getAddedSubList();
1038
1039
            tabs.forEach( tab -> {
1040
              final var node = tab.getContent();
1041
1042
              if( node instanceof TextEditor ) {
1043
                initScrollEventListener( tab );
1044
              }
1045
            } );
1046
1047
            // Select and give focus to the last tab opened.
1048
            final var index = tabs.size() - 1;
1049
            if( index >= 0 ) {
1050
              final var tab = tabs.get( index );
1051
              tabPane.getSelectionModel().select( tab );
1052
              tab.getContent().requestFocus();
1053
            }
1054
          }
1055
        }
1056
      }
1057
    );
1058
  }
1059
1060
  /**
1061
   * Synchronizes scrollbar positions between the given {@link Tab} that
1062
   * contains an instance of {@link TextEditor} and {@link HtmlPreview} pane.
1063
   *
1064
   * @param tab The container for an instance of {@link TextEditor}.
1065
   */
1066
  private void initScrollEventListener( final Tab tab ) {
1067
    final var editor = (TextEditor) tab.getContent();
1068
    final var scrollPane = editor.getScrollPane();
1069
    final var scrollBar = mPreview.getVerticalScrollBar();
1070
    final var handler = new ScrollEventHandler( scrollPane, scrollBar );
1071
1072
    handler.enabledProperty().bind( tab.selectedProperty() );
1073
  }
1074
1075
  private void addTabPane( final int index, final TabPane tabPane ) {
1076
    final var items = getItems();
1077
1078
    if( !items.contains( tabPane ) ) {
1079
      items.add( index, tabPane );
1080
    }
1081
  }
1082
1083
  private void addTabPane( final TabPane tabPane ) {
1084
    addTabPane( getItems().size(), tabPane );
1085
  }
1086
1087
  private GenericBuilder<Mutator, ProcessorContext> processorContextBuilder() {
1088
    final var w = getWorkspace();
1089
1090
    return builder()
1091
      .with( Mutator::setDefinitions, this::getDefinitions )
1092
      .with( Mutator::setLocale, w::getLocale )
1093
      .with( Mutator::setMetadata, w::getMetadata )
1094
      .with( Mutator::setThemeDir, w::getThemesPath )
1095
      .with( Mutator::setCacheDir,
1096
             () -> w.getFile( KEY_CACHE_DIR ) )
1097
      .with( Mutator::setImageDir,
1098
             () -> w.getFile( KEY_IMAGE_DIR ) )
1099
      .with( Mutator::setImageOrder,
1100
             () -> w.getString( KEY_IMAGE_ORDER ) )
1101
      .with( Mutator::setImageServer,
1102
             () -> w.getString( KEY_IMAGE_SERVER ) )
1103
      .with( Mutator::setFontDir,
1104
             () -> w.getFile( KEY_TYPESET_CONTEXT_FONTS_DIR ) )
1105
      .with( Mutator::setCaret,
1106
             () -> getTextEditor().getCaret() )
1107
      .with( Mutator::setSigilBegan,
1108
             () -> w.getString( KEY_DEF_DELIM_BEGAN ) )
1109
      .with( Mutator::setSigilEnded,
1110
             () -> w.getString( KEY_DEF_DELIM_ENDED ) )
1111
      .with( Mutator::setRScript,
1112
             () -> w.getString( KEY_R_SCRIPT ) )
1113
      .with( Mutator::setRWorkingDir,
1114
             () -> w.getFile( KEY_R_DIR ).toPath() )
1115
      .with( Mutator::setCurlQuotes,
1116
             () -> w.getBoolean( KEY_TYPESET_TYPOGRAPHY_QUOTES ) )
1117
      .with( Mutator::setAutoRemove,
1118
             () -> w.getBoolean( KEY_TYPESET_CONTEXT_CLEAN ) );
1119
  }
1120
1121
  public ProcessorContext createProcessorContext() {
1122
    return createProcessorContextBuilder( NONE ).build();
1123
  }
1124
1125
  private GenericBuilder<Mutator, ProcessorContext> createProcessorContextBuilder(
1126
    final ExportFormat format ) {
1127
    final var textEditor = getTextEditor();
1128
    final var sourcePath = textEditor.getPath();
1129
1130
    return processorContextBuilder()
1131
      .with( Mutator::setSourcePath, sourcePath )
1132
      .with( Mutator::setExportFormat, format );
1133
  }
1134
1135
  /**
1136
   * @param targetPath Used when exporting to a PDF file (binary).
1137
   * @param format     Used when processors export to a new text format.
1138
   * @return A new {@link ProcessorContext} to use when creating an instance of
1139
   * {@link Processor}.
1140
   */
1141
  public ProcessorContext createProcessorContext(
1142
    final Path targetPath, final ExportFormat format ) {
1143
    assert targetPath != null;
1144
    assert format != null;
1145
1146
    return createProcessorContextBuilder( format )
1147
      .with( Mutator::setTargetPath, targetPath )
1148
      .build();
1149
  }
1150
1151
  /**
1152
   * @param sourcePath Used by {@link ProcessorFactory} to determine
1153
   *                   {@link Processor} type to create based on file type.
1154
   * @return A new {@link ProcessorContext} to use when creating an instance of
1155
   * {@link Processor}.
1156
   */
1157
  private ProcessorContext createProcessorContext( final Path sourcePath ) {
1158
    return processorContextBuilder()
1159
      .with( Mutator::setSourcePath, sourcePath )
1160
      .with( Mutator::setExportFormat, NONE )
1161
      .build();
1162
  }
1163
1164
  private TextResource createTextResource( final File file ) {
1165
    if( fromFilename( file ) == TEXT_YAML ) {
1166
      final var editor = createDefinitionEditor( file );
1167
      mDefinitionEditor.set( editor );
1168
      return editor;
1169
    }
1170
    else {
1171
      final var editor = createMarkdownEditor( file );
1172
      mTextEditor.set( editor );
1173
      return editor;
1174
    }
1175
  }
1176
1177
  /**
1178
   * Creates an instance of {@link MarkdownEditor} that listens for both
1179
   * caret change events and text change events. Text change events must
1180
   * take priority over caret change events because it's possible to change
1181
   * the text without moving the caret (e.g., delete selected text).
1182
   *
1183
   * @param inputFile The file containing contents for the text editor.
1184
   * @return A non-null text editor.
1185
   */
1186
  private MarkdownEditor createMarkdownEditor( final File inputFile ) {
1187
    final var editor = new MarkdownEditor( inputFile, getWorkspace() );
1188
1189
    // Listener for editor modifications or caret position changes.
1190
    editor.addDirtyListener( ( c, o, n ) -> {
1191
      if( n ) {
1192
        // Reset the status bar after changing the text.
1193
        clue();
1194
1195
        // Processing the text may update the status bar.
1196
        process( editor );
1197
1198
        // Update the caret position in the status bar.
1199
        CaretMovedEvent.fire( editor.getCaret() );
1200
      }
1201
    } );
1202
1203
    editor.addEventListener(
1204
      keyPressed( SPACE, CONTROL_DOWN ), this::autoinsert
1205
    );
1206
1207
    editor.addEventListener(
1208
      keyPressed( ENTER, ALT_DOWN ), event -> mEditorSpeller.autofix( editor )
1209
    );
1210
1211
    final var textArea = editor.getTextArea();
1212
1213
    // Spell check when the paragraph changes.
1214
    textArea
1215
      .plainTextChanges()
1216
      .filter( p -> !p.isIdentity() )
1217
      .subscribe( change -> mEditorSpeller.checkParagraph( textArea, change ) );
1218
1219
    // Store the caret position to restore it after restarting the application.
1220
    textArea.caretPositionProperty().addListener(
1221
      ( c, o, n ) ->
1222
        getWorkspace().integerProperty( KEY_UI_RECENT_OFFSET ).setValue( n )
1223
    );
1224
1225
    // Check the entire document after the spellchecker is initialized (with
1226
    // a valid lexicon) so that only the current paragraph need be scanned
1227
    // while editing. (Technically, only the most recently modified word must
1228
    // be scanned.)
1229
    mSpellChecker.addListener(
1230
      ( c, o, n ) -> runLater(
1231
        () -> iterateEditors( mEditorSpeller::checkDocument )
1232
      )
1233
    );
1234
1235
    // Check the entire document after it has been loaded.
1236
    mEditorSpeller.checkDocument( editor );
1237
1238
    return editor;
1239
  }
1240
1241
  /**
1242
   * Creates a processor for an editor, provided one doesn't already exist.
1243
   *
1244
   * @param editor The editor that potentially requires an associated processor.
1245
   */
1246
  private void updateProcessors( final TextEditor editor ) {
1247
    final var path = editor.getFile().toPath();
1248
1249
    mProcessors.computeIfAbsent(
1250
      editor, p -> createProcessors(
1251
        createProcessorContext( path ),
1252
        createHtmlPreviewProcessor()
1253
      )
1254
    );
1255
  }
1256
1257
  /**
1258
   * Removes a processor for an editor. This is required because a file may
1259
   * change type while editing (e.g., from plain Markdown to R Markdown).
1260
   * In the case that an editor's type changes, its associated processor must
1261
   * be changed accordingly.
1262
   *
1263
   * @param editor The editor that potentially requires an associated processor.
1264
   */
1265
  private void removeProcessor( final TextEditor editor ) {
1266
    mProcessors.remove( editor );
1267
  }
1268
1269
  /**
1270
   * Creates a {@link Processor} capable of rendering an HTML document onto
1271
   * a GUI widget.
1272
   *
1273
   * @return The {@link Processor} for rendering an HTML document.
1274
   */
1275
  private Processor<String> createHtmlPreviewProcessor() {
1276
    return new HtmlPreviewProcessor( getPreview() );
1277
  }
1278
1279
  /**
1280
   * Creates a spellchecker that accepts all words as correct. This allows
1281
   * the spellchecker property to be initialized to a known valid value.
1282
   *
1283
   * @return A wrapped {@link PermissiveSpeller}.
1284
   */
1285
  private ObjectProperty<SpellChecker> createSpellChecker() {
1286
    return new SimpleObjectProperty<>( new PermissiveSpeller() );
1287
  }
1288
1289
  private TextEditorSpellChecker createTextEditorSpellChecker(
1290
    final ObjectProperty<SpellChecker> spellChecker ) {
1291
    return new TextEditorSpellChecker( spellChecker );
1292
  }
1293
1294
  /**
1295
   * Delegates to {@link #autoinsert()}.
1296
   *
1297
   * @param keyEvent Ignored.
1298
   */
1299
  private void autoinsert( final KeyEvent keyEvent ) {
1300
    autoinsert();
1301
  }
1302
1303
  /**
1304
   * Finds a node that matches the word at the caret, then inserts the
1305
   * corresponding definition. The definition token delimiters depend on
1306
   * the type of file being edited.
1307
   */
1308
  public void autoinsert() {
1309
    mVariableNameInjector.autoinsert( getTextEditor(), getTextDefinition() );
1310
  }
1311
1312
  private Tooltip createTooltip( final File file ) {
1313
    final var path = file.toPath();
1314
    final var tooltip = new Tooltip( path.toString() );
1315
1316
    tooltip.setShowDelay( millis( 200 ) );
1317
1318
    return tooltip;
1319
  }
1320
1321
  public HtmlPreview getPreview() {
1322
    return mPreview;
1323
  }
1324
1325
  /**
1326
   * Returns the active text editor.
1327
   *
1328
   * @return The text editor that currently has focus.
1329
   */
1330
  public TextEditor getTextEditor() {
1331
    return mTextEditor.get();
1332
  }
1333
1334
  /**
1335
   * Returns the active text editor property.
1336
   *
1337
   * @return The property container for the active text editor.
1338
   */
1339
  public ReadOnlyObjectProperty<TextEditor> textEditorProperty() {
1340
    return mTextEditor;
1341
  }
1342
1343
  /**
1344
   * Returns the active text definition editor.
1345
   *
1346
   * @return The property container for the active definition editor.
1347
   */
1348
  public TextDefinition getTextDefinition() {
1349
    return mDefinitionEditor.get();
1350
  }
1351
1352
  /**
1353
   * Returns the active variable definitions, without any interpolation.
1354
   * Interpolation is a responsibility of {@link Processor} instances.
1355
   *
1356
   * @return The key-value pairs, not interpolated.
1357
   */
1358
  private Map<String, String> getDefinitions() {
1359
    return getTextDefinition().getDefinitions();
1360
  }
1361
1362
  public Window getWindow() {
1363
    return getScene().getWindow();
1364
  }
1365
1366
  public Workspace getWorkspace() {
1367
    return mWorkspace;
1368
  }
1369
1370
  /**
1371
   * Returns the set of file names opened in the application. The names must
1372
   * be converted to {@link File} objects.
1373
   *
1374
   * @return A {@link Set} of file names.
1375
   */
1376
  private <E> SetProperty<E> getRecentFiles() {
1377
    return getWorkspace().setsProperty( KEY_UI_RECENT_OPEN_PATH );
1378
  }
1379
}
11380
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 MainApp} 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
  MenuBar getMenuBar() {
93
    return mMenuBar;
94
  }
95
96
  public StatusBar getStatusBar() {return mStatusBar;}
97
98
  private void initStylesheets( final Scene scene, final Workspace workspace ) {
99
    final var internal = workspace.skinProperty( KEY_UI_SKIN_SELECTION );
100
    final var external = workspace.fileProperty( KEY_UI_SKIN_CUSTOM );
101
    final var inSkin = internal.get();
102
    final var exSkin = external.get();
103
    applyStylesheets( scene, inSkin, exSkin );
104
105
    internal.addListener(
106
      ( c, o, n ) -> {
107
        if( n != null ) {
108
          applyStylesheets( scene, n, exSkin );
109
        }
110
      }
111
    );
112
113
    external.addListener(
114
      ( c, o, n ) -> {
115
        if( o != null ) {
116
          mFileWatchService.unregister( o );
117
        }
118
119
        if( n != null ) {
120
          try {
121
            applyStylesheets( scene, inSkin, n );
122
          } catch( final Exception ex ) {
123
            // Changes to the CSS file won't autoload, which is okay.
124
            clue( ex );
125
          }
126
        }
127
      }
128
    );
129
130
    mFileWatchService.removeListener( mStylesheetFileListener );
131
    mStylesheetFileListener = event ->
132
      runLater( () -> applyStylesheets( scene, inSkin, event.getFile() ) );
133
    mFileWatchService.addListener( mStylesheetFileListener );
134
  }
135
136
  private String getStylesheet( final String filename ) {
137
    return MessageFormat.format( STYLESHEET_APPLICATION_SKIN, filename );
138
  }
139
140
  /**
141
   * Clears then re-applies all the internal stylesheets.
142
   *
143
   * @param scene    The scene to stylize.
144
   * @param internal The CSS file name bundled with the application.
145
   * @param external The (optional) customized CSS file specified by the user.
146
   */
147
  private void applyStylesheets(
148
    final Scene scene, final String internal, final File external ) {
149
    final var stylesheets = scene.getStylesheets();
150
    stylesheets.clear();
151
    stylesheets.add( STYLESHEET_APPLICATION_BASE );
152
    stylesheets.add( STYLESHEET_MARKDOWN );
153
    stylesheets.add( getStylesheet( toFilename( internal ) ) );
154
155
    try {
156
      if( external != null && external.canRead() && !external.isDirectory() ) {
157
        stylesheets.add( external.toURI().toURL().toString() );
158
        mFileWatchService.register( external );
159
      }
160
    } catch( final Exception ex ) {
161
      clue( ex );
162
    }
163
  }
164
165
  private MainPane createMainPane( final Workspace workspace ) {
166
    return new MainPane( workspace );
167
  }
168
169
  private GuiCommands createApplicationActions( final MainPane mainPane ) {
170
    return new GuiCommands( this, mainPane );
171
  }
172
173
  /**
174
   * Creates the class responsible for updating the UI with the caret position
175
   * based on the active text editor.
176
   *
177
   * @return The {@link CaretStatus} responsible for updating the
178
   * {@link StatusBar} whenever the caret changes position.
179
   */
180
  private CaretStatus createCaretStatus() {
181
    return new CaretStatus();
182
  }
183
184
  /**
185
   * Creates a new scene that is attached to the given {@link Parent}.
186
   *
187
   * @param parent The container for the scene.
188
   * @return A scene to capture user interactions, UI styles, etc.
189
   */
190
  private Scene createScene( final Parent parent ) {
191
    final var scene = new Scene( parent );
192
193
    // Update the synchronized scrolling status when user presses scroll lock.
194
    scene.addEventHandler( KEY_RELEASED, event -> {
195
      if( event.getCode() == SCROLL_LOCK ) {
196
        fireScrollLockEvent();
197
      }
198
    } );
199
200
    return scene;
201
  }
202
203
  /**
204
   * Binds the visible property of the node to the managed property so that
205
   * hiding the node also removes the screen real estate that it occupies.
206
   * This allows the user to hide the menu bar, toolbar, etc.
207
   *
208
   * @param node The node to have its real estate bound to visibility.
209
   * @return The given node for fluent-like convenience.
210
   */
211
  private <T extends Node> T setManagedLayout( final T node ) {
212
    node.managedProperty().bind( node.visibleProperty() );
213
    return node;
214
  }
215
}
1216
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 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
15
import java.io.File;
16
import java.io.IOException;
17
import java.nio.file.Files;
18
import java.nio.file.Path;
19
import java.util.HashMap;
20
import java.util.Locale;
21
import java.util.Map;
22
import java.util.Map.Entry;
23
import java.util.concurrent.Callable;
24
import java.util.function.Consumer;
25
26
import static com.keenwrite.constants.Constants.DIAGRAM_SERVER_NAME;
27
28
/**
29
 * Responsible for mapping command-line arguments to keys that are used by
30
 * the application.
31
 */
32
@CommandLine.Command(
33
  name = "KeenWrite",
34
  mixinStandardHelpOptions = true,
35
  description = "Plain text editor for editing with variables"
36
)
37
@SuppressWarnings( "unused" )
38
public final class Arguments implements Callable<Integer> {
39
  @CommandLine.Option(
40
    names = {"--all"},
41
    description =
42
      "Concatenate files before processing (${DEFAULT-VALUE})",
43
    defaultValue = "false"
44
  )
45
  private boolean mConcatenate;
46
47
  @CommandLine.Option(
48
    names = {"--keep-files"},
49
    description =
50
      "Retain temporary build files (${DEFAULT-VALUE})",
51
    defaultValue = "false"
52
  )
53
  private boolean mKeepFiles;
54
55
  @CommandLine.Option(
56
    names = {"-c", "--chapters"},
57
    description =
58
      "Export chapter ranges, no spaces (e.g., -3,5-9,15-)",
59
    paramLabel = "String"
60
  )
61
  private String mChapters;
62
63
  @CommandLine.Option(
64
    names = {"--curl-quotes"},
65
    description =
66
      "Replace straight quotes with curly quotes (${DEFAULT-VALUE})",
67
    defaultValue = "true"
68
  )
69
  private boolean mCurlQuotes;
70
71
  @CommandLine.Option(
72
    names = {"-d", "--debug"},
73
    description =
74
      "Enable logging to the console (${DEFAULT-VALUE})",
75
    paramLabel = "Boolean",
76
    defaultValue = "false"
77
  )
78
  private boolean mDebug;
79
80
  @CommandLine.Option(
81
    names = {"-i", "--input"},
82
    description =
83
      "Source document file path",
84
    paramLabel = "PATH",
85
    defaultValue = "stdin",
86
    required = true
87
  )
88
  private Path mSourcePath;
89
90
  @CommandLine.Option(
91
    names = {"--font-dir"},
92
    description =
93
      "Directory to specify additional fonts",
94
    paramLabel = "String"
95
  )
96
  private File mFontDir;
97
98
  @CommandLine.Option(
99
    names = {"--format-subtype"},
100
    description =
101
      "Export TeX subtype for HTML formats: svg, delimited",
102
    paramLabel = "String",
103
    defaultValue = "svg"
104
  )
105
  private String mFormatSubtype;
106
107
  @CommandLine.Option(
108
    names = {"--cache-dir"},
109
    description =
110
      "Directory to store remote resources",
111
    paramLabel = "DIR"
112
  )
113
  private File mCachesDir;
114
115
  @CommandLine.Option(
116
    names = {"--image-dir"},
117
    description =
118
      "Directory containing images",
119
    paramLabel = "DIR"
120
  )
121
  private File mImagesDir;
122
123
  @CommandLine.Option(
124
    names = {"--image-order"},
125
    description =
126
      "Comma-separated image order (${DEFAULT-VALUE})",
127
    paramLabel = "String",
128
    defaultValue = "svg,pdf,png,jpg,tiff"
129
  )
130
  private String mImageOrder;
131
132
  @CommandLine.Option(
133
    names = {"--image-server"},
134
    description =
135
      "SVG diagram rendering service (${DEFAULT-VALUE})",
136
    paramLabel = "String",
137
    defaultValue = DIAGRAM_SERVER_NAME
138
  )
139
  private String mImageServer;
140
141
  @CommandLine.Option(
142
    names = {"--locale"},
143
    description =
144
      "Set localization (${DEFAULT-VALUE})",
145
    paramLabel = "String",
146
    defaultValue = "en"
147
  )
148
  private String mLocale;
149
150
  @CommandLine.Option(
151
    names = {"-m", "--metadata"},
152
    description =
153
      "Map metadata keys to values, variable names allowed",
154
    paramLabel = "key=value"
155
  )
156
  private Map<String, String> mMetadata;
157
158
  @CommandLine.Option(
159
    names = {"-o", "--output"},
160
    description =
161
      "Destination document file path",
162
    paramLabel = "PATH",
163
    defaultValue = "stdout",
164
    required = true
165
  )
166
  private Path mTargetPath;
167
168
  @CommandLine.Option(
169
    names = {"-q", "--quiet"},
170
    description =
171
      "Suppress all status messages (${DEFAULT-VALUE})",
172
    defaultValue = "false"
173
  )
174
  private boolean mQuiet;
175
176
  @CommandLine.Option(
177
    names = {"--r-dir"},
178
    description =
179
      "R working directory",
180
    paramLabel = "DIR"
181
  )
182
  private Path mRWorkingDir;
183
184
  @CommandLine.Option(
185
    names = {"--r-script"},
186
    description =
187
      "R bootstrap script file path",
188
    paramLabel = "PATH"
189
  )
190
  private Path mRScriptPath;
191
192
  @CommandLine.Option(
193
    names = {"-s", "--set"},
194
    description =
195
      "Set (or override) a document variable value",
196
    paramLabel = "key=value"
197
  )
198
  private Map<String, String> mOverrides;
199
200
  @CommandLine.Option(
201
    names = {"--sigil-opening"},
202
    description =
203
      "Starting sigil for variable names (${DEFAULT-VALUE})",
204
    paramLabel = "String",
205
    defaultValue = "{{"
206
  )
207
  private String mSigilBegan;
208
209
  @CommandLine.Option(
210
    names = {"--sigil-closing"},
211
    description =
212
      "Ending sigil for variable names (${DEFAULT-VALUE})",
213
    paramLabel = "String",
214
    defaultValue = "}}"
215
  )
216
  private String mSigilEnded;
217
218
  @CommandLine.Option(
219
    names = {"--theme-dir"},
220
    description =
221
      "Theme directory",
222
    paramLabel = "DIR"
223
  )
224
  private Path mThemesDir;
225
226
  @CommandLine.Option(
227
    names = {"-v", "--variables"},
228
    description =
229
      "Variables file path",
230
    paramLabel = "PATH"
231
  )
232
  private Path mPathVariables;
233
234
  private final Consumer<Arguments> mLauncher;
235
236
  public Arguments( final Consumer<Arguments> launcher ) {
237
    mLauncher = launcher;
238
  }
239
240
  public ProcessorContext createProcessorContext()
241
    throws IOException {
242
    final var definitions = parse( mPathVariables );
243
    final var format = ExportFormat.valueFrom( mTargetPath, mFormatSubtype );
244
    final var locale = lookupLocale( mLocale );
245
    final var rScript = read( mRScriptPath );
246
247
    return ProcessorContext
248
      .builder()
249
      .with( Mutator::setSourcePath, mSourcePath )
250
      .with( Mutator::setTargetPath, mTargetPath )
251
      .with( Mutator::setThemeDir, () -> mThemesDir )
252
      .with( Mutator::setCacheDir, () -> mCachesDir )
253
      .with( Mutator::setImageDir, () -> mImagesDir )
254
      .with( Mutator::setImageServer, () -> mImageServer )
255
      .with( Mutator::setImageOrder, () -> mImageOrder )
256
      .with( Mutator::setFontDir, () -> mFontDir )
257
      .with( Mutator::setExportFormat, format )
258
      .with( Mutator::setDefinitions, () -> definitions )
259
      .with( Mutator::setMetadata, () -> mMetadata )
260
      .with( Mutator::setOverrides, () -> mOverrides )
261
      .with( Mutator::setLocale, () -> locale )
262
      .with( Mutator::setConcatenate, () -> mConcatenate )
263
      .with( Mutator::setChapters, () -> mChapters )
264
      .with( Mutator::setSigilBegan, () -> mSigilBegan )
265
      .with( Mutator::setSigilEnded, () -> mSigilEnded )
266
      .with( Mutator::setRScript, () -> rScript )
267
      .with( Mutator::setRWorkingDir, () -> mRWorkingDir )
268
      .with( Mutator::setCurlQuotes, () -> mCurlQuotes )
269
      .with( Mutator::setAutoRemove, () -> !mKeepFiles )
270
      .build();
271
  }
272
273
  public boolean quiet() {
274
    return mQuiet;
275
  }
276
277
  public boolean debug() {
278
    return mDebug;
279
  }
280
281
  /**
282
   * Launches the main application window. This is called when not running
283
   * in headless mode.
284
   *
285
   * @return {@code 0}
286
   * @throws Exception The application encountered an unrecoverable error.
287
   */
288
  @Override
289
  public Integer call() throws Exception {
290
    mLauncher.accept( this );
291
    return 0;
292
  }
293
294
  private static String read( final Path path ) throws IOException {
295
    return path == null ? "" : Files.readString( path );
296
  }
297
298
  /**
299
   * Parses the given YAML document into a map of key-value pairs.
300
   *
301
   * @param vars Variable definition file to read, may be {@code null} if no
302
   *             variables are specified.
303
   * @return A non-interpolated variable map, or an empty map.
304
   * @throws IOException Could not read the variable definition file
305
   */
306
  private static Map<String, String> parse( final Path vars )
307
    throws IOException {
308
    final var map = new HashMap<String, String>();
309
310
    if( vars != null ) {
311
      final var yaml = read( vars );
312
      final var factory = new YAMLFactory();
313
      final var json = new ObjectMapper( factory ).readTree( yaml );
314
315
      parse( json, "", map );
316
    }
317
318
    return map;
319
  }
320
321
  private static void parse(
322
    final JsonNode json, final String parent, final Map<String, String> map ) {
323
    assert json != null;
324
    assert parent != null;
325
    assert map != null;
326
327
    json.fields().forEachRemaining( node -> parse( node, parent, map ) );
328
  }
329
330
  private static void parse(
331
    final Entry<String, JsonNode> node,
332
    final String parent,
333
    final Map<String, String> map ) {
334
    assert node != null;
335
    assert parent != null;
336
    assert map != null;
337
338
    final var jsonNode = node.getValue();
339
    final var keyName = parent + "." + node.getKey();
340
341
    if( jsonNode.isValueNode() ) {
342
      // Trim the leading period, which is always present.
343
      map.put( keyName.substring( 1 ), node.getValue().asText() );
344
    }
345
    else if( jsonNode.isObject() ) {
346
      parse( jsonNode, keyName, map );
347
    }
348
  }
349
350
  private static Locale lookupLocale( final String locale ) {
351
    try {
352
      return Locale.forLanguageTag( locale );
353
    } catch( final Exception ex ) {
354
      return Locale.ENGLISH;
355
    }
356
  }
357
}
1358
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
package com.keenwrite.cmdline;
2
3
import com.keenwrite.AppCommands;
4
import com.keenwrite.events.StatusEvent;
5
import org.greenrobot.eventbus.Subscribe;
6
7
import static com.keenwrite.events.Bus.register;
8
import static java.lang.String.format;
9
10
/**
11
 * Responsible for running the application in headless mode.
12
 */
13
public class HeadlessApp {
14
15
  /**
16
   * Contains directives that control text file processing.
17
   */
18
  private final Arguments mArgs;
19
20
  /**
21
   * Creates a new command-line version of the application.
22
   *
23
   * @param args The post-processed command-line arguments.
24
   */
25
  public HeadlessApp( final Arguments args ) {
26
    assert args != null;
27
28
    mArgs = args;
29
30
    register( this );
31
    AppCommands.run( mArgs );
32
  }
33
34
  /**
35
   * When a status message is shown, write it to the console, if not in
36
   * quiet mode.
37
   *
38
   * @param event The event published when the status changes.
39
   */
40
  @Subscribe
41
  public void handle( final StatusEvent event ) {
42
    assert event != null;
43
44
    if( !mArgs.quiet() ) {
45
      final var stacktrace = event.getProblem();
46
      final var problem = stacktrace.isBlank()
47
        ? ""
48
        : format( "%n%s", stacktrace );
49
      final var msg = format( "%s%s", event, problem );
50
51
      System.out.println( msg );
52
    }
53
  }
54
55
  /**
56
   * Entry point for running the application in headless mode.
57
   *
58
   * @param args The parsed command-line arguments.
59
   */
60
  @SuppressWarnings( "ConfusingMainMethod" )
61
  public static void main( final Arguments args ) {
62
    new HeadlessApp( args );
63
  }
64
}
165
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.SigilKeyOperator;
5
6
import java.io.Serial;
7
import java.util.HashMap;
8
import java.util.Map;
9
import java.util.Objects;
10
import java.util.concurrent.ConcurrentHashMap;
11
12
/**
13
 * Responsible for interpolating key-value pairs in a map. That is, this will
14
 * iterate over all key-value pairs and replace keys wrapped in sigils
15
 * with corresponding definition value from the same map.
16
 */
17
public class InterpolatingMap extends ConcurrentHashMap<String, String> {
18
  @Serial
19
  private static final long serialVersionUID = -8705400301476113530L;
20
21
  private static final int GROUP_DELIMITED = 1;
22
23
  /**
24
   * Used to override the default initial capacity in {@link HashMap}.
25
   */
26
  private static final int INITIAL_CAPACITY = 1 << 8;
27
28
  private transient final SigilKeyOperator mOperator;
29
30
  /**
31
   * @param operator Contains the opening and closing sigils that mark
32
   *                 where variable names begin and end.
33
   */
34
  public InterpolatingMap( final SigilKeyOperator operator ) {
35
    super( INITIAL_CAPACITY );
36
37
    assert operator != null;
38
    mOperator = operator;
39
  }
40
41
  /**
42
   * @param operator Contains the opening and closing sigils that mark
43
   *                 where variable names begin and end.
44
   * @param m        The initial {@link Map} to copy into this instance.
45
   */
46
  public InterpolatingMap(
47
    final SigilKeyOperator operator, final Map<String, String> m ) {
48
    this( operator );
49
    putAll( m );
50
  }
51
52
  /**
53
   * Interpolates all values in the map that reference other values by way
54
   * of key names. Performs a non-greedy match of key names delimited by
55
   * definition tokens. This operation modifies the map directly.
56
   *
57
   * @return {@code this}
58
   */
59
  public InterpolatingMap interpolate() {
60
    for( final var k : keySet() ) {
61
      replace( k, interpolate( get( k ) ) );
62
    }
63
64
    return this;
65
  }
66
67
  /**
68
   * Given a value with zero or more key references, this will resolve all
69
   * the values, recursively. If a key cannot be de-referenced, the value will
70
   * contain the key name, including the original sigils.
71
   *
72
   * @param value Value containing zero or more key references.
73
   * @return The given value with all embedded key references interpolated.
74
   */
75
  public String interpolate( String value ) {
76
    assert value != null;
77
78
    final var matcher = mOperator.match( value );
79
80
    while( matcher.find() ) {
81
      final var keyName = matcher.group( GROUP_DELIMITED );
82
      final var mapValue = get( keyName );
83
84
      if( mapValue != null ) {
85
        final var keyValue = interpolate( mapValue );
86
        value = value.replace( mOperator.apply( keyName ), keyValue );
87
      }
88
    }
89
90
    return value;
91
  }
92
93
  @Override
94
  public boolean equals( final Object o ) {
95
    if( this == o ) { return true; }
96
    if( o == null || getClass() != o.getClass() ) { return false; }
97
    if( !super.equals( o ) ) { return false; }
98
    final InterpolatingMap that = (InterpolatingMap) o;
99
    return Objects.equals( mOperator, that.mOperator );
100
  }
101
102
  @Override
103
  public int hashCode() {
104
    return Objects.hash( super.hashCode(), mOperator );
105
  }
106
}
1107
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.util.AlphanumComparator;
8
import com.keenwrite.util.RangeValidator;
9
10
import java.io.IOException;
11
import java.nio.file.Path;
12
import java.util.ArrayList;
13
import java.util.concurrent.Callable;
14
import java.util.concurrent.atomic.AtomicInteger;
15
16
import static com.keenwrite.events.StatusEvent.clue;
17
import static com.keenwrite.util.FileWalker.walk;
18
import static java.lang.System.lineSeparator;
19
import static java.nio.file.Files.readString;
20
21
/**
22
 * Responsible for concatenating files according to user-defined chapter ranges.
23
 */
24
public class ConcatenateCommand implements Callable<String> {
25
  /**
26
   * Sci-fi genres, which are can be longer than other genres, typically fall
27
   * below 150,000 words at 6 chars per word. This reduces re-allocations of
28
   * memory when concatenating files together when exporting novels.
29
   */
30
  private static final int DOCUMENT_LENGTH = 150_000 * 6;
31
32
  private final Path mParent;
33
  private final String mExtension;
34
  private final String mRange;
35
36
  public ConcatenateCommand(
37
    final Path parent,
38
    final String extension,
39
    final String range ) {
40
    assert parent != null;
41
    assert extension != null;
42
    assert range != null;
43
44
    mParent = parent;
45
    mExtension = extension;
46
    mRange = range;
47
  }
48
49
  public String call() throws IOException {
50
    final var glob = "**/*." + mExtension;
51
    final var files = new ArrayList<Path>();
52
    final var text = new StringBuilder( DOCUMENT_LENGTH );
53
    final var chapter = new AtomicInteger();
54
    final var eol = lineSeparator();
55
56
    final var validator = new RangeValidator( mRange );
57
58
    walk( mParent, glob, files::add );
59
    files.sort( new AlphanumComparator<>() );
60
    files.forEach( file -> {
61
      try {
62
        if( validator.test( chapter.incrementAndGet() ) ) {
63
          clue( "Main.status.export.concat", file );
64
65
          text.append( readString( file ) )
66
              .append( eol );
67
        }
68
      } catch( final IOException ex ) {
69
        clue( "Main.status.export.concat.io", file );
70
      }
71
    } );
72
73
    return text.toString();
74
  }
75
}
176
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.preferences.LocaleScripts.withScript;
20
import static java.io.File.separator;
21
import static java.lang.String.format;
22
import static java.lang.System.getProperty;
23
import static org.apache.commons.lang3.SystemUtils.*;
24
25
/**
26
 * Defines application-wide default values.
27
 */
28
public final class Constants {
29
30
  /**
31
   * Used by the default settings to load the {@link Settings} service. This
32
   * must come before any attempt is made to create a {@link Settings} object.
33
   * The reference to {@link Bootstrap#APP_TITLE_LOWERCASE} should cause the
34
   * JVM to load {@link Bootstrap} prior to proceeding. Loading that class
35
   * beforehand will read the bootstrap properties file to determine the
36
   * application name, which is then used to locate the settings properties.
37
   */
38
  public static final String PATH_PROPERTIES_SETTINGS =
39
    format( "/com/%s/settings.properties", APP_TITLE_LOWERCASE );
40
41
  /**
42
   * The {@link Settings} uses {@link #PATH_PROPERTIES_SETTINGS}.
43
   */
44
  public static final Settings sSettings = Services.load( Settings.class );
45
46
  public static final double WINDOW_X_DEFAULT = 0;
47
  public static final double WINDOW_Y_DEFAULT = 0;
48
  public static final double WINDOW_W_DEFAULT = 1200;
49
  public static final double WINDOW_H_DEFAULT = 800;
50
51
  public static final File DOCUMENT_DEFAULT = getFile( "document" );
52
  public static final int DOCUMENT_OFFSET = 0;
53
  public static final File DEFINITION_DEFAULT = getFile( "definition" );
54
  public static final File PDF_DEFAULT = getFile( "pdf" );
55
56
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
57
58
  public static final String STYLESHEET_APPLICATION_BASE =
59
    get( "file.stylesheet.application.base" );
60
  public static final String STYLESHEET_APPLICATION_SKIN =
61
    get( "file.stylesheet.application.skin" );
62
  public static final String STYLESHEET_MARKDOWN =
63
    get( "file.stylesheet.markdown" );
64
  public static final String STYLESHEET_MARKDOWN_LOCALE =
65
    "file.stylesheet.markdown.locale";
66
  public static final String STYLESHEET_PREVIEW =
67
    get( "file.stylesheet.preview" );
68
  public static final String STYLESHEET_PREVIEW_LOCALE =
69
    "file.stylesheet.preview.locale";
70
71
  public static final File FILE_PREFERENCES = getPreferencesFile();
72
73
  /**
74
   * Refer to file name extension settings in the configuration file. Do not
75
   * terminate with a period.
76
   */
77
  public static final String GLOB_PREFIX_FILE = "file.ext";
78
79
  /**
80
   * Three parameters: line number, column number, and offset.
81
   */
82
  public static final String STATUS_BAR_LINE = "Main.status.line";
83
84
  public static final String STATUS_BAR_OK = "Main.status.state.default";
85
86
  /**
87
   * Used to show an error while parsing, usually syntactical.
88
   */
89
  public static final String STATUS_PARSE_ERROR = "Main.status.error.parse";
90
  public static final String STATUS_DEFINITION_BLANK =
91
    "Main.status.error.def.blank";
92
  public static final String STATUS_DEFINITION_EMPTY =
93
    "Main.status.error.def.empty";
94
95
  /**
96
   * One parameter: the word under the cursor that could not be found.
97
   */
98
  public static final String STATUS_DEFINITION_MISSING =
99
    "Main.status.error.def.missing";
100
101
  /**
102
   * Default image extension order to use when scanning.
103
   */
104
  public static final String PERSIST_IMAGES_DEFAULT =
105
    get( "file.ext.image.order" );
106
107
  /**
108
   * Default working directory.
109
   */
110
  public static final File USER_DIRECTORY =
111
    new File( System.getProperty( "user.dir" ) );
112
113
  /**
114
   * Location to write temporary files.
115
   */
116
  public static final String TEMPORARY_DIRECTORY =
117
    System.getProperty( "java.io.tmpdir" );
118
119
  public static final String NEWLINE = System.lineSeparator();
120
121
  /**
122
   * Default path to use for an untitled (pathless) file.
123
   */
124
  public static final Path DEFAULT_DIRECTORY = USER_DIRECTORY.toPath();
125
126
  /**
127
   * Default character set to use when reading/writing files.
128
   */
129
  public static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
130
131
  /**
132
   * Default starting delimiter for definition variables. This value must
133
   * not overlap math delimiters, so do not use $ tokens as the first
134
   * delimiter.
135
   */
136
  public static final String DEF_DELIM_BEGAN_DEFAULT = "{{";
137
138
  /**
139
   * Default ending delimiter for definition variables.
140
   */
141
  public static final String DEF_DELIM_ENDED_DEFAULT = "}}";
142
143
  /**
144
   * Default starting delimiter when inserting R variables.
145
   */
146
  public static final String R_DELIM_BEGAN_DEFAULT = "x( ";
147
148
  /**
149
   * Default ending delimiter when inserting R variables.
150
   */
151
  public static final String R_DELIM_ENDED_DEFAULT = " )";
152
153
  /**
154
   * Resource directory where different language lexicons are located.
155
   */
156
  public static final String LEXICONS_DIRECTORY = "lexicons";
157
158
  /**
159
   * Absolute location of true type font files within the Java archive file.
160
   */
161
  public static final String FONT_DIRECTORY = "/fonts";
162
163
  /**
164
   * Default text editor font name.
165
   */
166
  public static final String FONT_NAME_EDITOR_DEFAULT = "Noto Sans Regular";
167
168
  /**
169
   * Default text editor font size, in points.
170
   */
171
  public static final float FONT_SIZE_EDITOR_DEFAULT = 12f;
172
173
  /**
174
   * Default preview font name.
175
   */
176
  public static final String FONT_NAME_PREVIEW_DEFAULT = "Source Serif 4";
177
178
  /**
179
   * Default preview font size, in points.
180
   */
181
  public static final float FONT_SIZE_PREVIEW_DEFAULT = 13f;
182
183
  /**
184
   * Scaling factor for rendering mathematics.
185
   */
186
  public static final double FONT_SIZE_MATH_DEFAULT = 2;
187
188
  /**
189
   * Default monospace preview font name.
190
   */
191
  public static final String FONT_NAME_PREVIEW_MONO_NAME_DEFAULT =
192
    "Source Code Pro";
193
194
  /**
195
   * Default monospace preview font size, in points.
196
   */
197
  public static final float FONT_SIZE_PREVIEW_MONO_SIZE_DEFAULT = 13f;
198
199
  /**
200
   * Default locale for font loading, including ISO 15924 alpha-4 script code.
201
   */
202
  public static final Locale LOCALE_DEFAULT = withScript( Locale.getDefault() );
203
204
  /**
205
   * Default CSS to apply (resolves to a minimal implementation).
206
   */
207
  public static final String SKIN_DEFAULT = "Modena Light";
208
209
  /**
210
   * Custom JavaFX CSS to apply to user interface.
211
   */
212
  public static final File SKIN_CUSTOM_DEFAULT = null;
213
214
  /**
215
   * Custom HTML CSS to apply to HTML preview panel.
216
   */
217
  public static final File PREVIEW_CUSTOM_DEFAULT = null;
218
219
  /**
220
   * Default identifier to use for synchronized scrolling.
221
   */
222
  public static final String CARET_ID = "caret";
223
224
  /**
225
   * Default spacing for UI items (e.g., toolbars).
226
   */
227
  public static final int UI_CONTROL_SPACING = 10;
228
229
  /**
230
   * Default server name for rendering diagrams.
231
   */
232
  public static final String DIAGRAM_SERVER_NAME = "kroki.io";
233
234
  /**
235
   * Application action messages properties prefix.
236
   */
237
  public static final String ACTION_PREFIX = "Action.";
238
239
  /**
240
   * Restrict theme names when displaying.
241
   */
242
  public static final byte THEME_NAME_LENGTH = 30;
243
244
  /**
245
   * Prevent instantiation.
246
   */
247
  private Constants() {
248
  }
249
250
  /**
251
   * Converts from points to pixels because FlyingSaucer cannot handle points
252
   * properly. This is used to convert font sizes.
253
   *
254
   * @param points The points to convert to pixels.
255
   * @return The given number of points in equivalent pixels.
256
   */
257
  public static int toPixels( final double points ) {
258
    return (int) (points * (1 + 1 / 3f));
259
  }
260
261
  static String get( final String key ) {
262
    return sSettings.getSetting( key, "" );
263
  }
264
265
  /**
266
   * Returns a default {@link File} instance based on the given key suffix.
267
   *
268
   * @param suffix Appended to {@code "file.default."}.
269
   * @return A new {@link File} instance that references the settings file name.
270
   */
271
  private static File getFile( final String suffix ) {
272
    return new File( get( "file.default." + suffix ) );
273
  }
274
275
  /**
276
   * Returns the equivalent of {@code $HOME/.filename.xml}.
277
   */
278
  private static File getPreferencesFile() {
279
    return new File( format(
280
      "%s%s.%s.xml",
281
      getProperty( "user.home" ),
282
      separator,
283
      APP_TITLE_LOWERCASE
284
    ) );
285
  }
286
287
  /**
288
   * Tries to get a system-independent path to the user's fonts directory.
289
   */
290
  public static File getFontDirectory() {
291
    final var FONT_PATH = Path.of( "fonts" );
292
    final var USER_HOME = System.getProperty( "user.home" );
293
294
    final String fontBase;
295
    final Path fontUser;
296
297
    if( IS_OS_WINDOWS ) {
298
      fontBase = System.getenv( "WINDIR" );
299
      fontUser = FONT_PATH;
300
    }
301
    else if( IS_OS_MAC ) {
302
      fontBase = USER_HOME;
303
      fontUser = Path.of( "Library", "Fonts" );
304
    }
305
    else if( IS_OS_UNIX ) {
306
      fontBase = USER_HOME;
307
      fontUser = Path.of( ".fonts" );
308
    }
309
    else {
310
      fontBase = USER_DATA_DIR.toString();
311
      fontUser = FONT_PATH;
312
    }
313
314
    final var base = fontBase == null
315
      ? USER_DATA_DIR.relativize( fontUser )
316
      : Path.of( fontBase ).resolve( fontUser );
317
318
    return toFile( base );
319
  }
320
}
1321
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.*;
22
23
/**
24
 * Responsible for converting JSoup document object model (DOM) to a W3C DOM.
25
 * Provides a lighter implementation than the superclass by overriding the
26
 * {@link #fromJsoup(org.jsoup.nodes.Document)} method to reuse factories,
27
 * builders, and implementations.
28
 */
29
public final class DocumentConverter extends W3CDom {
30
  /**
31
   * Retain insertion order using an instance of {@link LinkedHashMap} so
32
   * that ligature substitution uses longer ligatures ahead of shorter
33
   * ligatures. The word "ruffian" should use the "ffi" ligature, not the "ff"
34
   * ligature.
35
   */
36
  private static final Map<String, String> LIGATURES = ofEntries(
37
    entry( "ffi", "ffi" ),
38
    entry( "ffl", "ffl" ),
39
    entry( "ff", "ff" ),
40
    entry( "fi", "fi" ),
41
    entry( "fl", "fl" )
42
  );
43
44
  private static final NodeVisitor LIGATURE_VISITOR = new NodeVisitor() {
45
    @Override
46
    public void head( final @NotNull Node node, final int depth ) {
47
      if( node instanceof final TextNode textNode ) {
48
        final var parent = node.parentNode();
49
        final var name = parent == null ? "root" : parent.nodeName();
50
51
        if( !("pre".equalsIgnoreCase( name ) ||
52
          "code".equalsIgnoreCase( name ) ||
53
          "kbd".equalsIgnoreCase( name ) ||
54
          "var".equalsIgnoreCase( name ) ||
55
          "tt".equalsIgnoreCase( name )) ) {
56
          // Calling getWholeText() will return newlines, which must be kept
57
          // to ensure that preformatted text maintains its formatting.
58
          textNode.text( replace( textNode.getWholeText(), LIGATURES ) );
59
        }
60
      }
61
    }
62
63
    @Override
64
    public void tail( final @NotNull Node node, final int depth ) { }
65
  };
66
67
  @Override
68
  public @NotNull Document fromJsoup( final org.jsoup.nodes.Document in ) {
69
    assert in != null;
70
71
    final var out = DocumentParser.newDocument();
72
    final var doctype = in.documentType();
73
74
    if( doctype != null ) {
75
      out.appendChild(
76
        sDomImplementation.createDocumentType(
77
          doctype.name(),
78
          doctype.publicId(),
79
          doctype.systemId()
80
        )
81
      );
82
    }
83
84
    out.setXmlStandalone( true );
85
    in.traverse( LIGATURE_VISITOR );
86
    convert( in, out );
87
88
    return out;
89
  }
90
91
  /**
92
   * Converts the given non-well-formed HTML document into an XML document
93
   * while preserving whitespace.
94
   *
95
   * @param html The document to convert.
96
   * @return The converted document as an object model.
97
   */
98
  public static org.jsoup.nodes.Document parse( final String html ) {
99
    final var document = Jsoup.parse( html );
100
101
    document
102
      .outputSettings()
103
      .syntax( Syntax.xml )
104
      .prettyPrint( false );
105
106
    return document;
107
  }
108
}
1109
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 org.w3c.dom.*;
8
import org.xml.sax.InputSource;
9
import org.xml.sax.SAXException;
10
11
import javax.xml.parsers.DocumentBuilder;
12
import javax.xml.parsers.DocumentBuilderFactory;
13
import javax.xml.transform.Transformer;
14
import javax.xml.transform.TransformerException;
15
import javax.xml.transform.TransformerFactory;
16
import javax.xml.transform.dom.DOMSource;
17
import javax.xml.transform.stream.StreamResult;
18
import javax.xml.xpath.XPath;
19
import javax.xml.xpath.XPathExpression;
20
import javax.xml.xpath.XPathExpressionException;
21
import javax.xml.xpath.XPathFactory;
22
import java.io.*;
23
import java.nio.file.Path;
24
import java.util.HashMap;
25
import java.util.Map;
26
import java.util.function.Consumer;
27
28
import static com.keenwrite.events.StatusEvent.clue;
29
import static com.keenwrite.io.SysFile.toFile;
30
import static java.nio.charset.StandardCharsets.UTF_16;
31
import static java.nio.charset.StandardCharsets.UTF_8;
32
import static java.nio.file.Files.write;
33
import static javax.xml.transform.OutputKeys.*;
34
import static javax.xml.xpath.XPathConstants.NODESET;
35
36
/**
37
 * Responsible for initializing an XML parser.
38
 */
39
public class DocumentParser {
40
  private static final String LOAD_EXTERNAL_DTD =
41
    "http://apache.org/xml/features/nonvalidating/load-external-dtd";
42
  private static final String INDENT_AMOUNT =
43
    "{http://xml.apache.org/xslt}indent-amount";
44
45
  private static final ByteArrayOutputStream sWriter =
46
    new ByteArrayOutputStream( 65536 );
47
  private static final OutputStreamWriter sOutput =
48
    new OutputStreamWriter( sWriter, UTF_8 );
49
50
  /**
51
   * Caches {@link XPathExpression}s to avoid re-compiling.
52
   */
53
  private static final Map<String, XPathExpression> sXpaths = new HashMap<>();
54
55
  private static final DocumentBuilderFactory sDocumentFactory;
56
  private static DocumentBuilder sDocumentBuilder;
57
  private static Transformer sTransformer;
58
  private static final XPath sXpath = XPathFactory.newInstance().newXPath();
59
60
  public static final DOMImplementation sDomImplementation;
61
62
  static {
63
    sDocumentFactory = DocumentBuilderFactory.newInstance();
64
65
    sDocumentFactory.setValidating( false );
66
    sDocumentFactory.setAttribute( LOAD_EXTERNAL_DTD, false );
67
    sDocumentFactory.setNamespaceAware( true );
68
    sDocumentFactory.setIgnoringComments( true );
69
    sDocumentFactory.setIgnoringElementContentWhitespace( true );
70
71
    DOMImplementation domImplementation;
72
73
    try {
74
      sDocumentBuilder = sDocumentFactory.newDocumentBuilder();
75
      domImplementation = sDocumentBuilder.getDOMImplementation();
76
      sTransformer = TransformerFactory.newInstance().newTransformer();
77
78
      // Ensure Unicode characters (emojis) are encoded correctly.
79
      sTransformer.setOutputProperty( ENCODING, UTF_16.toString() );
80
      sTransformer.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
81
      sTransformer.setOutputProperty( METHOD, "xml" );
82
      sTransformer.setOutputProperty( INDENT, "no" );
83
      sTransformer.setOutputProperty( INDENT_AMOUNT, "2" );
84
    } catch( final Exception ex ) {
85
      clue( ex );
86
      domImplementation = sDocumentBuilder.getDOMImplementation();
87
    }
88
89
    sDomImplementation = domImplementation;
90
  }
91
92
  public static Document newDocument() {
93
    return sDocumentBuilder.newDocument();
94
  }
95
96
  /**
97
   * Creates a new document object model based on the given XML document
98
   * string. This will return an empty document if the document could not
99
   * be parsed.
100
   *
101
   * @param xml The document text to convert into a DOM.
102
   * @return The DOM that represents the given XML data.
103
   */
104
  public static Document parse( final String xml ) {
105
    assert xml != null;
106
107
    final var input = new InputSource();
108
109
    try( final var reader = new StringReader( xml ) ) {
110
      input.setEncoding( UTF_8.toString() );
111
      input.setCharacterStream( reader );
112
113
      return sDocumentBuilder.parse( input );
114
    } catch( final Throwable t ) {
115
      clue( t );
116
117
      return sDocumentBuilder.newDocument();
118
    }
119
  }
120
121
  /**
122
   * Parses the given file contents into a document object model.
123
   *
124
   * @param doc The source XML document to parse.
125
   * @return The file as a document object model.
126
   * @throws IOException  Could not open the document.
127
   * @throws SAXException Could not read the XML file content.
128
   */
129
  public static Document parse( final File doc )
130
    throws IOException, SAXException {
131
    assert doc != null;
132
133
    try( final var in = new FileInputStream( doc ) ) {
134
      return parse( in );
135
    }
136
  }
137
138
  /**
139
   * Parses the given file contents into a document object model. Callers
140
   * must close the stream.
141
   *
142
   * @param doc The source XML document to parse.
143
   * @return The {@link InputStream} converted to a document object model.
144
   * @throws IOException  Could not open the document.
145
   * @throws SAXException Could not read the XML file content.
146
   */
147
  public static Document parse( final InputStream doc )
148
    throws IOException, SAXException {
149
    assert doc != null;
150
151
    return sDocumentBuilder.parse( doc );
152
  }
153
154
  /**
155
   * Allows an operation to be applied for every node in the document that
156
   * matches a given tag name pattern.
157
   *
158
   * @param document Document to traverse.
159
   * @param xpath    Document elements to find via {@link XPath} expression.
160
   * @param consumer The consumer to call for each matching document node.
161
   */
162
  public static void visit(
163
    final Document document,
164
    final CharSequence xpath,
165
    final Consumer<Node> consumer ) {
166
    assert document != null;
167
    assert consumer != null;
168
169
    try {
170
      final var expr = compile( xpath );
171
      final var nodeSet = expr.evaluate( document, NODESET );
172
173
      if( nodeSet instanceof NodeList nodes ) {
174
        for( int i = 0, len = nodes.getLength(); i < len; i++ ) {
175
          consumer.accept( nodes.item( i ) );
176
        }
177
      }
178
    } catch( final Exception ex ) {
179
      clue( ex );
180
    }
181
  }
182
183
  public static Node createMeta(
184
    final Document document, final Map.Entry<String, String> entry ) {
185
    assert document != null;
186
    assert entry != null;
187
188
    final var node = document.createElement( "meta" );
189
190
    node.setAttribute( "name", entry.getKey() );
191
    node.setAttribute( "content", entry.getValue() );
192
193
    return node;
194
  }
195
196
  public static Node createElement(
197
    final Document doc, final String nodeName, final String nodeValue ) {
198
    assert doc != null;
199
    assert nodeName != null;
200
    assert !nodeName.isBlank();
201
202
    final var node = doc.createElement( nodeName );
203
204
    if( nodeValue != null ) {
205
      node.setTextContent( nodeValue );
206
    }
207
208
    return node;
209
  }
210
211
  public static String toString( final Document xhtml ) {
212
    assert xhtml != null;
213
214
    try( final var writer = new StringWriter() ) {
215
      final var result = new StreamResult( writer );
216
217
      transform( xhtml, result );
218
219
      return writer.toString();
220
    } catch( final Exception ex ) {
221
      clue( ex );
222
      return "";
223
    }
224
  }
225
226
  public static String transform( final Element root )
227
    throws IOException, TransformerException {
228
    assert root != null;
229
230
    try( final var writer = new StringWriter() ) {
231
      transform( root.getOwnerDocument(), new StreamResult( writer ) );
232
233
      return writer.toString();
234
    }
235
  }
236
237
  /**
238
   * Remove whitespace, comments, and XML/DOCTYPE declarations to make
239
   * processing work with ConTeXt.
240
   *
241
   * @param path The SVG file to process.
242
   * @throws Exception The file could not be processed.
243
   */
244
  public static void sanitize( final Path path ) throws Exception {
245
    assert path != null;
246
247
    // Preprocessing the SVG image is a single-threaded operation, no matter
248
    // how many SVG images are in the document to typeset.
249
    sWriter.reset();
250
251
    final var target = new StreamResult( sOutput );
252
    final var source = sDocumentBuilder.parse( toFile( path ) );
253
254
    transform( source, target );
255
    write( path, sWriter.toByteArray() );
256
  }
257
258
  /**
259
   * Converts a string into an {@link XPathExpression}, which may be used to
260
   * extract elements from a {@link Document} object model.
261
   *
262
   * @param cs The string to convert to an {@link XPathExpression}.
263
   * @return {@code null} if there was an error compiling the xpath.
264
   */
265
  public static XPathExpression compile( final CharSequence cs ) {
266
    assert cs != null;
267
268
    final var xpath = cs.toString();
269
270
    return sXpaths.computeIfAbsent( xpath, k -> {
271
      try {
272
        return sXpath.compile( xpath );
273
      } catch( final XPathExpressionException ex ) {
274
        clue( ex );
275
        return null;
276
      }
277
    } );
278
  }
279
280
  /**
281
   * Streams an instance of {@link Document} as a plain text XML document.
282
   *
283
   * @param src The source document to transform.
284
   * @param dst The destination location to write the transformed version.
285
   * @throws TransformerException Could not transform the document.
286
   */
287
  private static void transform( final Document src, final StreamResult dst )
288
    throws TransformerException {
289
    sTransformer.transform( new DOMSource( src ), dst );
290
  }
291
292
  /**
293
   * Use the {@code static} constants and methods, not an instance, at least
294
   * until an iterable sub-interface is written.
295
   */
296
  private DocumentParser() { }
297
}
1298
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 javafx.beans.property.ReadOnlyBooleanProperty;
9
import javafx.scene.Node;
10
import org.mozilla.universalchardet.UniversalDetector;
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.charset.Charset.forName;
20
import static java.nio.file.Files.readAllBytes;
21
import static java.nio.file.Files.write;
22
import static java.util.Arrays.asList;
23
import static java.util.Locale.ENGLISH;
24
25
/**
26
 * A text resource can be persisted and retrieved from its persisted location.
27
 */
28
public interface TextResource {
29
  /**
30
   * Sets the text string that to be changed through some graphical user
31
   * interface. For example, a YAML document must be parsed from the given
32
   * text string into a tree view with which the user may interact.
33
   *
34
   * @param text The new content for the resource.
35
   */
36
  void setText( String text );
37
38
  /**
39
   * Returns the text string that may have been modified by the user through
40
   * some graphical user interface.
41
   *
42
   * @return The text value, based on the value set from
43
   * {@link #setText(String)}, but possibly mutated.
44
   */
45
  String getText();
46
47
  /**
48
   * Return the character encoding for this file.
49
   *
50
   * @return A non-null character set, primarily detected from file contents.
51
   */
52
  Charset getEncoding();
53
54
  /**
55
   * Renames the current file to the given fully qualified file name.
56
   *
57
   * @param file The new file name.
58
   */
59
  void rename( final File file );
60
61
  /**
62
   * Returns the file name, without any directory components, for this instance.
63
   * Useful for showing as a tab title.
64
   *
65
   * @return The file name value returned from {@link #getFile()}.
66
   */
67
  default String getFilename() {
68
    final var filename = getFile().toPath().getFileName();
69
    return filename == null ? "" : filename.toString();
70
  }
71
72
  /**
73
   * Returns the fully qualified {@link File} to the editable text resource.
74
   * Useful for showing as a tab tooltip, saving the file, or reading it.
75
   *
76
   * @return A non-null {@link File} instance.
77
   */
78
  File getFile();
79
80
  /**
81
   * Returns the {@link MediaType} associated with the file being edited.
82
   *
83
   * @return The {@link MediaType} for the editor's file.
84
   */
85
  default MediaType getMediaType() {
86
    return MediaType.fromFilename( getFile() );
87
  }
88
89
  /**
90
   * Answers whether this instance is an editor for at least one of the given
91
   * {@link MediaType} references.
92
   *
93
   * @param mediaTypes The {@link MediaType} references to compare against.
94
   * @return {@code true} if the given list of media types contains the
95
   * {@link MediaType} for this editor.
96
   */
97
  default boolean isMediaType( final MediaType... mediaTypes ) {
98
    return asList( mediaTypes ).contains( getMediaType() );
99
  }
100
101
  /**
102
   * Returns the fully qualified {@link Path} to the editable text resource.
103
   * This delegates to {@link #getFile()}.
104
   *
105
   * @return A non-null {@link Path} instance.
106
   */
107
  default Path getPath() {
108
    return getFile().toPath();
109
  }
110
111
  /**
112
   * Read the file contents and update the text accordingly. If the file
113
   * cannot be read then no changes will happen to the text. Fails silently.
114
   *
115
   * @param path The fully qualified {@link Path}, including a file name, to
116
   *             fully read into the editor.
117
   * @return The character encoding for the file at the given {@link Path}.
118
   */
119
  default Charset open( final Path path ) {
120
    final var file = toFile( path );
121
    Charset encoding = DEFAULT_CHARSET;
122
123
    try {
124
      if( file.exists() ) {
125
        if( file.canWrite() && file.canRead() ) {
126
          final var bytes = readAllBytes( path );
127
          encoding = detectEncoding( bytes );
128
129
          setText( asString( bytes, encoding ) );
130
        }
131
        else {
132
          clue( "TextResource.load.error.permissions", file.toString() );
133
        }
134
      }
135
      else {
136
        clue( "TextResource.load.error.unsaved", file.toString() );
137
      }
138
    } catch( final Exception ex ) {
139
      clue( ex );
140
    }
141
142
    return encoding;
143
  }
144
145
  /**
146
   * Read the file contents and update the text accordingly. If the file
147
   * cannot be read then no changes will happen to the text. This delegates
148
   * to {@link #open(Path)}.
149
   *
150
   * @param file The {@link File} to fully read into the editor.
151
   * @return The file's character encoding.
152
   */
153
  default Charset open( final File file ) {
154
    return open( file.toPath() );
155
  }
156
157
  /**
158
   * Save the file contents and clear the modified flag. If the file cannot
159
   * be saved, the exception is swallowed and this method returns {@code false}.
160
   *
161
   * @return {@code true} the file was saved; {@code false} if upon exception.
162
   */
163
  default boolean save() {
164
    try {
165
      write( getPath(), asBytes( getText() ) );
166
      clearModifiedProperty();
167
      return true;
168
    } catch( final Exception ex ) {
169
      clue( ex );
170
    }
171
172
    return false;
173
  }
174
175
  /**
176
   * Returns the node associated with this {@link TextResource}.
177
   *
178
   * @return The view component for the {@link TextResource}.
179
   */
180
  Node getNode();
181
182
  /**
183
   * Answers whether the resource has been modified.
184
   *
185
   * @return {@code true} the resource has changed; {@code false} means that
186
   * no changes to the resource have been made.
187
   */
188
  default boolean isModified() {
189
    return modifiedProperty().get();
190
  }
191
192
  /**
193
   * Returns a property that answers whether this text resource has been
194
   * changed from the original text that was opened.
195
   *
196
   * @return A property representing the modified state of this
197
   * {@link TextResource}.
198
   */
199
  ReadOnlyBooleanProperty modifiedProperty();
200
201
  /**
202
   * Lowers the modified flag such that listeners to the modified property
203
   * will be informed that the text that's being edited no longer differs
204
   * from what's persisted.
205
   */
206
  void clearModifiedProperty();
207
208
  private String asString( final byte[] text, final Charset encoding ) {
209
    return new String( text, encoding );
210
  }
211
212
  /**
213
   * Converts the given string to an array of bytes using the encoding that was
214
   * originally detected (if any) and associated with this file.
215
   *
216
   * @param text The text to convert into the original file encoding.
217
   * @return A series of bytes ready for writing to a file.
218
   */
219
  private byte[] asBytes( final String text ) {
220
    return text.getBytes( getEncoding() );
221
  }
222
223
  private Charset detectEncoding( final byte[] bytes ) {
224
    final var detector = new UniversalDetector( null );
225
    detector.handleData( bytes, 0, bytes.length );
226
    detector.dataEnd();
227
228
    final var charset = detector.getDetectedCharset();
229
230
    return charset == null
231
      ? DEFAULT_CHARSET
232
      : forName( charset.toUpperCase( ENGLISH ) );
233
  }
234
235
  /**
236
   * Answers whether the given resource are of the same conceptual type. This
237
   * method is intended to be overridden by subclasses.
238
   *
239
   * @param mediaType The type to compare.
240
   * @return {@code true} if the {@link TextResource} is compatible with the
241
   * given {@link MediaType}.
242
   */
243
  default boolean supports( final MediaType mediaType ) {
244
    return isMediaType( mediaType );
245
  }
246
}
1247
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", e -> createDefinition() ),
125
      createButton( "rename", e -> renameDefinition() ),
126
      createButton( "delete", e -> 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( event -> {
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)) ? null : item;
318
  }
319
320
  @Override
321
  public DefinitionTreeItem<String> findLeafExact( final String text ) {
322
    return getTreeRoot().findLeafExact( text );
323
  }
324
325
  @Override
326
  public DefinitionTreeItem<String> findLeafContains( final String text ) {
327
    return getTreeRoot().findLeafContains( text );
328
  }
329
330
  @Override
331
  public DefinitionTreeItem<String> findLeafContainsNoCase(
332
    final String text ) {
333
    return getTreeRoot().findLeafContainsNoCase( text );
334
  }
335
336
  @Override
337
  public DefinitionTreeItem<String> findLeafStartsWith( final String text ) {
338
    return getTreeRoot().findLeafStartsWith( text );
339
  }
340
341
  public void select( final TreeItem<String> item ) {
342
    getSelectionModel().clearSelection();
343
    getSelectionModel().select( getTreeView().getRow( item ) );
344
  }
345
346
  /**
347
   * Collapses the tree, recursively.
348
   */
349
  public void collapse() {
350
    collapse( getTreeRoot().getChildren() );
351
  }
352
353
  /**
354
   * Collapses the tree, recursively.
355
   *
356
   * @param <T>   The type of tree item to expand (usually String).
357
   * @param nodes The nodes to collapse.
358
   */
359
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
360
    for( final var node : nodes ) {
361
      node.setExpanded( false );
362
      collapse( node.getChildren() );
363
    }
364
  }
365
366
  /**
367
   * @return {@code true} when the user is editing a {@link TreeItem}.
368
   */
369
  private boolean isEditingTreeItem() {
370
    return getTreeView().editingItemProperty().getValue() != null;
371
  }
372
373
  /**
374
   * Changes to edit mode for the selected item.
375
   */
376
  @Override
377
  public void renameDefinition() {
378
    getTreeView().edit( getSelectedItem() );
379
  }
380
381
  /**
382
   * Removes all selected items from the {@link TreeView}.
383
   */
384
  @Override
385
  public void deleteDefinitions() {
386
    for( final var item : getSelectedItems() ) {
387
      final var parent = item.getParent();
388
389
      if( parent != null ) {
390
        parent.getChildren().remove( item );
391
      }
392
    }
393
  }
394
395
  /**
396
   * Deletes the selected item.
397
   */
398
  private void deleteSelectedItem() {
399
    final var c = getSelectedItem();
400
    getSiblings( c ).remove( c );
401
  }
402
403
  private void insertSelectedItem() {
404
    if( getSelectedItem() instanceof DefinitionTreeItem<String> node ) {
405
      if( node.isLeaf() ) {
406
        InsertDefinitionEvent.fire( node );
407
      }
408
    }
409
  }
410
411
  /**
412
   * Adds a new item under the selected item (or root if nothing is selected).
413
   * There are a few conditions to consider: when adding to the root,
414
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
415
   * root must contain two items: a key and a value.
416
   */
417
  @Override
418
  public void createDefinition() {
419
    final var value = createDefinitionTreeItem();
420
    getSelectedItem().getChildren().add( value );
421
    expand( value );
422
    select( value );
423
  }
424
425
  private ContextMenu createContextMenu() {
426
    final var menu = new ContextMenu();
427
    final var items = menu.getItems();
428
429
    addMenuItem( items, ACTION_PREFIX + "definition.create.text" )
430
      .setOnAction( e -> createDefinition() );
431
    addMenuItem( items, ACTION_PREFIX + "definition.rename.text" )
432
      .setOnAction( e -> renameDefinition() );
433
    addMenuItem( items, ACTION_PREFIX + "definition.delete.text" )
434
      .setOnAction( e -> deleteSelectedItem() );
435
    addMenuItem( items, ACTION_PREFIX + "definition.insert.text" )
436
      .setOnAction( e -> insertSelectedItem() );
437
438
    return menu;
439
  }
440
441
  /**
442
   * Executes hot-keys for edits to the definition tree.
443
   *
444
   * @param event Contains the key code of the key that was pressed.
445
   */
446
  private void keyEventFilter( final KeyEvent event ) {
447
    if( !isEditingTreeItem() ) {
448
      switch( event.getCode() ) {
449
        case ENTER -> {
450
          expand( getSelectedItem() );
451
          event.consume();
452
        }
453
454
        case DELETE -> deleteDefinitions();
455
        case INSERT -> createDefinition();
456
457
        case R -> {
458
          if( event.isControlDown() ) {
459
            renameDefinition();
460
          }
461
        }
462
463
        default -> { }
464
      }
465
466
      for( final var handler : getKeyEventHandlers() ) {
467
        handler.handle( event );
468
      }
469
    }
470
  }
471
472
  /**
473
   * Called when the editor's input focus changes. This will fire an event
474
   * for subscribers.
475
   *
476
   * @param ignored Not used.
477
   * @param o       The old input focus property value.
478
   * @param n       The new input focus property value.
479
   */
480
  private void focused(
481
    final ObservableValue<? extends Boolean> ignored,
482
    final Boolean o,
483
    final Boolean n ) {
484
    if( n != null && n ) {
485
      TextDefinitionFocusEvent.fire( this );
486
    }
487
  }
488
489
  /**
490
   * Adds a menu item to a list of menu items.
491
   *
492
   * @param items    The list of menu items to append to.
493
   * @param labelKey The resource bundle key name for the menu item's label.
494
   * @return The menu item added to the list of menu items.
495
   */
496
  private MenuItem addMenuItem(
497
    final List<MenuItem> items, final String labelKey ) {
498
    final MenuItem menuItem = createMenuItem( labelKey );
499
    items.add( menuItem );
500
    return menuItem;
501
  }
502
503
  private MenuItem createMenuItem( final String labelKey ) {
504
    return new MenuItem( get( labelKey ) );
505
  }
506
507
  /**
508
   * Creates a new {@link TreeItem} that is intended to be the root-level item
509
   * added to the {@link TreeView}. This allows the root item to be
510
   * distinguished from the other items so that reference keys do not include
511
   * "Definition" as part of their name.
512
   *
513
   * @return A new {@link TreeItem}, never {@code null}.
514
   */
515
  private RootTreeItem<String> createRootTreeItem() {
516
    return new RootTreeItem<>( get( "Pane.definition.node.root.title" ) );
517
  }
518
519
  private DefinitionTreeItem<String> createDefinitionTreeItem() {
520
    return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) );
521
  }
522
523
  @Override
524
  public void requestFocus() {
525
    getTreeView().requestFocus();
526
  }
527
528
  /**
529
   * Expands the node to the root, recursively.
530
   *
531
   * @param <T>  The type of tree item to expand (usually String).
532
   * @param node The node to expand.
533
   */
534
  @Override
535
  public <T> void expand( final TreeItem<T> node ) {
536
    if( node != null ) {
537
      expand( node.getParent() );
538
      node.setExpanded( !node.isLeaf() );
539
    }
540
  }
541
542
  /**
543
   * Answers whether there are any definitions in the tree.
544
   *
545
   * @return {@code true} when there are no definitions; {@code false} when
546
   * there's at least one definition.
547
   */
548
  @Override
549
  public boolean isEmpty() {
550
    return getTreeRoot().isEmpty();
551
  }
552
553
  /**
554
   * Returns the actively selected item in the tree.
555
   *
556
   * @return The selected item, or the tree root item if no item is selected.
557
   */
558
  public TreeItem<String> getSelectedItem() {
559
    final var item = getSelectionModel().getSelectedItem();
560
    return item == null ? getTreeRoot() : item;
561
  }
562
563
  /**
564
   * Returns the {@link TreeView} that contains the definition hierarchy.
565
   *
566
   * @return A non-null instance.
567
   */
568
  private TreeView<String> getTreeView() {
569
    return mTreeView;
570
  }
571
572
  /**
573
   * Returns the root of the tree.
574
   *
575
   * @return The first node added to the definition tree.
576
   */
577
  private DefinitionTreeItem<String> getTreeRoot() {
578
    return mTreeRoot;
579
  }
580
581
  private ObservableList<TreeItem<String>> getSiblings(
582
    final TreeItem<String> item ) {
583
    final var root = getTreeView().getRoot();
584
    final var parent = (item == null || item == root) ? root : item.getParent();
585
586
    return parent.getChildren();
587
  }
588
589
  private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
590
    return getTreeView().getSelectionModel();
591
  }
592
593
  /**
594
   * Returns a copy of all the selected items.
595
   *
596
   * @return A list, possibly empty, containing all selected items in the
597
   * {@link TreeView}.
598
   */
599
  private List<TreeItem<String>> getSelectedItems() {
600
    return new ArrayList<>( getSelectionModel().getSelectedItems() );
601
  }
602
603
  private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() {
604
    return mKeyEventHandlers;
605
  }
606
}
1607
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 JsonNode hierarchy into a tree that can be displayed in a user
21
 * 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.get( 0 ).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
    node.fields().forEachRemaining( leaf -> transform( leaf, item ) );
124
  }
125
126
  /**
127
   * Recursively adapt each rootNode to a corresponding rootItem.
128
   *
129
   * @param node The node to adapt.
130
   * @param item The item to adapt using the node's key.
131
   * @throws StackOverflowError If infinite recursion is encountered.
132
   */
133
  private void transform(
134
    final Entry<String, JsonNode> node, final TreeItem<String> item ) {
135
    final var leafNode = node.getValue();
136
    final var key = node.getKey();
137
    final var leaf = createTreeItem( key );
138
139
    if( leafNode.isValueNode() ) {
140
      leaf.getChildren().add( createTreeItem( node.getValue().asText() ) );
141
    }
142
143
    item.getChildren().add( leaf );
144
145
    if( leafNode.isObject() ) {
146
      transform( leafNode, leaf );
147
    }
148
  }
149
150
  /**
151
   * Creates a new {@link TreeItem} that can be added to the {@link TreeView}.
152
   *
153
   * @param value The node's value.
154
   * @return A new {@link TreeItem}, never {@code null}.
155
   */
156
  private TreeItem<String> createTreeItem( final String value ) {
157
    return new DefinitionTreeItem<>( value );
158
  }
159
}
1160
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/HyperlinkModel.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
9
/**
10
 * Represents the model for a hyperlink: text, url, and title.
11
 */
12
public final class HyperlinkModel {
13
14
  private String text;
15
  private String url;
16
  private String title;
17
18
  /**
19
   * Constructs a new hyperlink model in Markdown format by default with no
20
   * title (i.e., tooltip).
21
   *
22
   * @param text The hyperlink text displayed (e.g., displayed to the user).
23
   * @param url  The destination URL (e.g., when clicked).
24
   */
25
  public HyperlinkModel( final String text, final String url ) {
26
    this( text, url, null );
27
  }
28
29
  /**
30
   * Constructs a new hyperlink model for the given AST link.
31
   *
32
   * @param link A Markdown link.
33
   */
34
  public HyperlinkModel( final Link link ) {
35
    this(
36
      link.getText().toString(),
37
      link.getUrl().toString(),
38
      link.getTitle().toString()
39
    );
40
  }
41
42
  /**
43
   * Constructs a new hyperlink model in Markdown format by default.
44
   *
45
   * @param text  The hyperlink text displayed (e.g., displayed to the user).
46
   * @param url   The destination URL (e.g., when clicked).
47
   * @param title The hyperlink title (e.g., shown as a tooltip).
48
   */
49
  public HyperlinkModel(
50
    final String text, final String url, final String title ) {
51
    setText( text );
52
    setUrl( url );
53
    setTitle( title );
54
  }
55
56
  /**
57
   * Returns the string in Markdown format by default.
58
   *
59
   * @return A Markdown version of the hyperlink.
60
   */
61
  @Override
62
  public String toString() {
63
    String format = "%s%s%s";
64
65
    if( hasText() ) {
66
      format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)");
67
    }
68
69
    // Becomes ""+URL+"" if no text is set.
70
    // Becomes [TITLE]+(URL)+"" if no title is set.
71
    // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
72
    return String.format( format, getText(), getUrl(), getTitle() );
73
  }
74
75
  public void setText( final String text ) {
76
    this.text = sanitize( text );
77
  }
78
79
  public void setUrl( final String url ) {
80
    this.url = sanitize( url );
81
  }
82
83
  public void setTitle( final String title ) {
84
    this.title = sanitize( title );
85
  }
86
87
  /**
88
   * Answers whether text has been set for the hyperlink.
89
   *
90
   * @return true This is a text link.
91
   */
92
  public boolean hasText() {
93
    return !getText().isEmpty();
94
  }
95
96
  /**
97
   * Answers whether a title (tooltip) has been set for the hyperlink.
98
   *
99
   * @return true There is a title.
100
   */
101
  public boolean hasTitle() {
102
    return !getTitle().isEmpty();
103
  }
104
105
  public String getText() {
106
    return this.text;
107
  }
108
109
  public String getUrl() {
110
    return this.url;
111
  }
112
113
  public String getTitle() {
114
    return this.title;
115
  }
116
117
  private String sanitize( final String s ) {
118
    return s == null ? "" : s;
119
  }
120
}
1121
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.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.MainApp.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 java.lang.Character.isWhitespace;
46
import static java.lang.String.format;
47
import static java.util.Collections.singletonList;
48
import static javafx.application.Platform.runLater;
49
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.ALWAYS;
50
import static javafx.scene.input.KeyCode.*;
51
import static javafx.scene.input.KeyCombination.*;
52
import static org.apache.commons.lang3.StringUtils.stripEnd;
53
import static org.apache.commons.lang3.StringUtils.stripStart;
54
import static org.fxmisc.richtext.Caret.CaretVisibility.ON;
55
import static org.fxmisc.richtext.model.StyleSpans.singleton;
56
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
57
import static org.fxmisc.wellbehaved.event.InputMap.consume;
58
59
/**
60
 * Responsible for editing Markdown documents.
61
 */
62
public final class MarkdownEditor extends BorderPane implements TextEditor {
63
  /**
64
   * Regular expression that matches the type of markup block. This is used
65
   * when Enter is pressed to continue the block environment.
66
   */
67
  private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile(
68
    "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
69
70
  private final Workspace mWorkspace;
71
72
  /**
73
   * The text editor.
74
   */
75
  private final StyleClassedTextArea mTextArea =
76
    new StyleClassedTextArea( false );
77
78
  /**
79
   * Wraps the text editor in scrollbars.
80
   */
81
  private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
82
    new VirtualizedScrollPane<>( mTextArea );
83
84
  /**
85
   * Tracks where the caret is located in this document. This offers observable
86
   * properties for caret position changes.
87
   */
88
  private final Caret mCaret = createCaret( mTextArea );
89
90
  /**
91
   * File being edited by this editor instance.
92
   */
93
  private File mFile;
94
95
  /**
96
   * Set to {@code true} upon text or caret position changes. Value is {@code
97
   * false} by default.
98
   */
99
  private final BooleanProperty mDirty = new SimpleBooleanProperty();
100
101
  /**
102
   * Opened file's character encoding, or {@link Constants#DEFAULT_CHARSET} if
103
   * either no encoding could be determined or this is a new (empty) file.
104
   */
105
  private final Charset mEncoding;
106
107
  /**
108
   * Tracks whether the in-memory definitions have changed with respect to the
109
   * persisted definitions.
110
   */
111
  private final BooleanProperty mModified = new SimpleBooleanProperty();
112
113
  public MarkdownEditor( final File file, final Workspace workspace ) {
114
    mEncoding = open( mFile = file );
115
    mWorkspace = workspace;
116
117
    initTextArea( mTextArea );
118
    initStyle( mTextArea );
119
    initScrollPane( mScrollPane );
120
    initHotKeys();
121
    initUndoManager();
122
  }
123
124
  private void initTextArea( final StyleClassedTextArea textArea ) {
125
    textArea.setShowCaret( ON );
126
    textArea.setWrapText( true );
127
    textArea.requestFollowCaret();
128
    textArea.moveTo( 0 );
129
130
    textArea.textProperty().addListener( ( c, o, n ) -> {
131
      // Fire, regardless of whether the caret position has changed.
132
      mDirty.set( false );
133
134
      // Prevent the subsequent caret position change from raising dirty bits.
135
      mDirty.set( true );
136
    } );
137
138
    textArea.caretPositionProperty().addListener( ( c, o, n ) -> {
139
      // Fire when the caret position has changed and the text has not.
140
      mDirty.set( true );
141
      mDirty.set( false );
142
    } );
143
144
    textArea.focusedProperty().addListener( ( c, o, n ) -> {
145
      if( n != null && n ) {
146
        TextEditorFocusEvent.fire( this );
147
      }
148
    } );
149
  }
150
151
  private void initStyle( final StyleClassedTextArea textArea ) {
152
    textArea.getStyleClass().add( "markdown" );
153
154
    final var stylesheets = textArea.getStylesheets();
155
    stylesheets.add( getStylesheetPath( getLocale() ) );
156
157
    localeProperty().addListener( ( c, o, n ) -> {
158
      if( n != null ) {
159
        stylesheets.clear();
160
        stylesheets.add( getStylesheetPath( getLocale() ) );
161
      }
162
    } );
163
164
    fontNameProperty().addListener(
165
      ( c, o, n ) ->
166
        setFont( mTextArea, getFontName(), getFontSize() )
167
    );
168
169
    fontSizeProperty().addListener(
170
      ( c, o, n ) ->
171
        setFont( mTextArea, getFontName(), getFontSize() )
172
    );
173
174
    setFont( mTextArea, getFontName(), getFontSize() );
175
  }
176
177
  private void initScrollPane(
178
    final VirtualizedScrollPane<StyleClassedTextArea> scrollpane ) {
179
    scrollpane.setVbarPolicy( ALWAYS );
180
    setCenter( scrollpane );
181
  }
182
183
  private void initHotKeys() {
184
    addEventListener( keyPressed( ENTER ), this::onEnterPressed );
185
    addEventListener( keyPressed( X, CONTROL_DOWN ), this::cut );
186
    addEventListener( keyPressed( TAB ), this::tab );
187
    addEventListener( keyPressed( TAB, SHIFT_DOWN ), this::untab );
188
  }
189
190
  private void initUndoManager() {
191
    final var undoManager = getUndoManager();
192
    final var markedPosition = undoManager.atMarkedPositionProperty();
193
194
    undoManager.forgetHistory();
195
    undoManager.mark();
196
    mModified.bind( Bindings.not( markedPosition ) );
197
  }
198
199
  @Override
200
  public void moveTo( final int offset ) {
201
    assert 0 <= offset && offset <= mTextArea.getLength();
202
203
    if( offset <= mTextArea.getLength() ) {
204
      mTextArea.moveTo( offset );
205
      mTextArea.requestFollowCaret();
206
    }
207
  }
208
209
  /**
210
   * Delegate the focus request to the text area itself.
211
   */
212
  @Override
213
  public void requestFocus() {
214
    mTextArea.requestFocus();
215
  }
216
217
  @Override
218
  public void setText( final String text ) {
219
    mTextArea.clear();
220
    mTextArea.appendText( text );
221
    mTextArea.getUndoManager().mark();
222
  }
223
224
  @Override
225
  public String getText() {
226
    return mTextArea.getText();
227
  }
228
229
  @Override
230
  public Charset getEncoding() {
231
    return mEncoding;
232
  }
233
234
  @Override
235
  public File getFile() {
236
    return mFile;
237
  }
238
239
  @Override
240
  public void rename( final File file ) {
241
    mFile = file;
242
  }
243
244
  @Override
245
  public void undo() {
246
    final var manager = getUndoManager();
247
    xxdo( manager::isUndoAvailable, manager::undo, "Main.status.error.undo" );
248
  }
249
250
  @Override
251
  public void redo() {
252
    final var manager = getUndoManager();
253
    xxdo( manager::isRedoAvailable, manager::redo, "Main.status.error.redo" );
254
  }
255
256
  /**
257
   * Performs an undo or redo action, if possible, otherwise displays an error
258
   * message to the user.
259
   *
260
   * @param ready  Answers whether the action can be executed.
261
   * @param action The action to execute.
262
   * @param key    The informational message key having a value to display if
263
   *               the {@link Supplier} is not ready.
264
   */
265
  private void xxdo(
266
    final Supplier<Boolean> ready, final Runnable action, final String key ) {
267
    if( ready.get() ) {
268
      action.run();
269
    }
270
    else {
271
      clue( key );
272
    }
273
  }
274
275
  @Override
276
  public void cut() {
277
    final var selected = mTextArea.getSelectedText();
278
279
    // Emulate selecting the current line by firing Home then Shift+Down Arrow.
280
    if( selected == null || selected.isEmpty() ) {
281
      // Note: mTextArea.selectLine() does not select empty lines.
282
      mTextArea.fireEvent( keyDown( HOME, false ) );
283
      mTextArea.fireEvent( keyDown( DOWN, true ) );
284
    }
285
286
    mTextArea.cut();
287
  }
288
289
  @Override
290
  public void copy() {
291
    mTextArea.copy();
292
  }
293
294
  @Override
295
  public void paste() {
296
    mTextArea.paste();
297
  }
298
299
  @Override
300
  public void selectAll() {
301
    mTextArea.selectAll();
302
  }
303
304
  @Override
305
  public void bold() {
306
    enwrap( "**" );
307
  }
308
309
  @Override
310
  public void italic() {
311
    enwrap( "*" );
312
  }
313
314
  @Override
315
  public void monospace() {
316
    enwrap( "`" );
317
  }
318
319
  @Override
320
  public void superscript() {
321
    enwrap( "^" );
322
  }
323
324
  @Override
325
  public void subscript() {
326
    enwrap( "~" );
327
  }
328
329
  @Override
330
  public void strikethrough() {
331
    enwrap( "~~" );
332
  }
333
334
  @Override
335
  public void blockquote() {
336
    block( "> " );
337
  }
338
339
  @Override
340
  public void code() {
341
    enwrap( "`" );
342
  }
343
344
  @Override
345
  public void fencedCodeBlock() {
346
    enwrap( "\n\n```\n", "\n```\n\n" );
347
  }
348
349
  @Override
350
  public void heading( final int level ) {
351
    final var hashes = new String( new char[ level ] ).replace( "\0", "#" );
352
    block( format( "%s ", hashes ) );
353
  }
354
355
  @Override
356
  public void unorderedList() {
357
    block( "* " );
358
  }
359
360
  @Override
361
  public void orderedList() {
362
    block( "1. " );
363
  }
364
365
  @Override
366
  public void horizontalRule() {
367
    block( format( "---%n%n" ) );
368
  }
369
370
  @Override
371
  public Node getNode() {
372
    return this;
373
  }
374
375
  @Override
376
  public ReadOnlyBooleanProperty modifiedProperty() {
377
    return mModified;
378
  }
379
380
  @Override
381
  public void clearModifiedProperty() {
382
    getUndoManager().mark();
383
  }
384
385
  @Override
386
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
387
    return mScrollPane;
388
  }
389
390
  @Override
391
  public StyleClassedTextArea getTextArea() {
392
    return mTextArea;
393
  }
394
395
  private final Map<String, IndexRange> mStyles = new HashMap<>();
396
397
  @Override
398
  public void stylize( final IndexRange range, final String style ) {
399
    final var began = range.getStart();
400
    final var ended = range.getEnd() + 1;
401
402
    assert 0 <= began && began <= ended;
403
    assert style != null;
404
405
    // TODO: Ensure spell check and find highlights can coexist.
406
//    final var spans = mTextArea.getStyleSpans( range );
407
//    System.out.println( "SPANS: " + spans );
408
409
//    final var spans = mTextArea.getStyleSpans( range );
410
//    mTextArea.setStyleSpans( began, merge( spans, range.getLength(), style
411
//    ) );
412
413
//    final var builder = new StyleSpansBuilder<Collection<String>>();
414
//    builder.add( singleton( style ), range.getLength() + 1 );
415
//    mTextArea.setStyleSpans( began, builder.create() );
416
417
//    final var s = mTextArea.getStyleSpans( began, ended );
418
//    System.out.println( "STYLES: " +s );
419
420
    mStyles.put( style, range );
421
    mTextArea.setStyleClass( began, ended, style );
422
423
    // Ensure that whenever the user interacts with the text that the found
424
    // word will have its highlighting removed. The handler removes itself.
425
    // This won't remove the highlighting if the caret position moves by mouse.
426
    final var handler = mTextArea.getOnKeyPressed();
427
    mTextArea.setOnKeyPressed( event -> {
428
      mTextArea.setOnKeyPressed( handler );
429
      unstylize( style );
430
    } );
431
432
    //mTextArea.setStyleSpans(began, ended, s);
433
  }
434
435
  private static StyleSpans<Collection<String>> merge(
436
    StyleSpans<Collection<String>> spans, int len, String style ) {
437
    spans = spans.overlay(
438
      singleton( singletonList( style ), len ),
439
      ( bottomSpan, list ) -> {
440
        final List<String> l =
441
          new ArrayList<>( bottomSpan.size() + list.size() );
442
        l.addAll( bottomSpan );
443
        l.addAll( list );
444
        return l;
445
      } );
446
447
    return spans;
448
  }
449
450
  @Override
451
  public void unstylize( final String style ) {
452
    final var indexes = mStyles.remove( style );
453
    if( indexes != null ) {
454
      mTextArea.clearStyle( indexes.getStart(), indexes.getEnd() + 1 );
455
    }
456
  }
457
458
  @Override
459
  public Caret getCaret() {
460
    return mCaret;
461
  }
462
463
  /**
464
   * A {@link Caret} instance is not directly coupled ot the GUI because
465
   * document processing does not always require interactive status bar
466
   * updates. This can happen when processing from the command-line. However,
467
   * the processors need the {@link Caret} instance to inject the caret
468
   * position into the document. Making the {@link CaretExtension} optional
469
   * would require more effort than using a {@link Caret} model that is
470
   * decoupled from GUI widgets.
471
   *
472
   * @param editor The text editor containing caret position information.
473
   * @return An instance of {@link Caret} that tracks the GUI caret position.
474
   */
475
  private Caret createCaret( final StyleClassedTextArea editor ) {
476
    return Caret
477
      .builder()
478
      .with( Caret.Mutator::setParagraph,
479
             () -> editor.currentParagraphProperty().getValue() )
480
      .with( Caret.Mutator::setParagraphs,
481
             () -> editor.getParagraphs().size() )
482
      .with( Caret.Mutator::setParaOffset,
483
             () -> editor.caretColumnProperty().getValue() )
484
      .with( Caret.Mutator::setTextOffset,
485
             () -> editor.caretPositionProperty().getValue() )
486
      .with( Caret.Mutator::setTextLength,
487
             () -> editor.lengthProperty().getValue() )
488
      .build();
489
  }
490
491
  /**
492
   * This method adds listeners to editor events.
493
   *
494
   * @param <T>      The event type.
495
   * @param <U>      The consumer type for the given event type.
496
   * @param event    The event of interest.
497
   * @param consumer The method to call when the event happens.
498
   */
499
  public <T extends Event, U extends T> void addEventListener(
500
    final EventPattern<? super T, ? extends U> event,
501
    final Consumer<? super U> consumer ) {
502
    Nodes.addInputMap( mTextArea, consume( event, consumer ) );
503
  }
504
505
  private void onEnterPressed( final KeyEvent ignored ) {
506
    final var currentLine = getCaretParagraph();
507
    final var matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
508
509
    // By default, insert a new line by itself.
510
    String newText = NEWLINE;
511
512
    // If the pattern was matched then determine what block type to continue.
513
    if( matcher.matches() ) {
514
      if( matcher.group( 2 ).isEmpty() ) {
515
        final var pos = mTextArea.getCaretPosition();
516
        mTextArea.selectRange( pos - currentLine.length(), pos );
517
      }
518
      else {
519
        // Indent the new line with the same whitespace characters and
520
        // list markers as current line. This ensures that the indentation
521
        // is propagated.
522
        newText = newText.concat( matcher.group( 1 ) );
523
      }
524
    }
525
526
    mTextArea.replaceSelection( newText );
527
    mTextArea.requestFollowCaret();
528
  }
529
530
  private void cut( final KeyEvent event ) {
531
    cut();
532
  }
533
534
  private void tab( final KeyEvent event ) {
535
    final var range = mTextArea.selectionProperty().getValue();
536
    final var sb = new StringBuilder( 1024 );
537
538
    if( range.getLength() > 0 ) {
539
      final var selection = mTextArea.getSelectedText();
540
541
      selection.lines().forEach(
542
        l -> sb.append( "\t" ).append( l ).append( NEWLINE )
543
      );
544
    }
545
    else {
546
      sb.append( "\t" );
547
    }
548
549
    mTextArea.replaceSelection( sb.toString() );
550
  }
551
552
  private void untab( final KeyEvent event ) {
553
    final var range = mTextArea.selectionProperty().getValue();
554
555
    if( range.getLength() > 0 ) {
556
      final var selection = mTextArea.getSelectedText();
557
      final var sb = new StringBuilder( selection.length() );
558
559
      selection.lines().forEach(
560
        l -> sb.append( l.startsWith( "\t" ) ? l.substring( 1 ) : l )
561
               .append( NEWLINE )
562
      );
563
564
      mTextArea.replaceSelection( sb.toString() );
565
    }
566
    else {
567
      final var p = getCaretParagraph();
568
569
      if( p.startsWith( "\t" ) ) {
570
        mTextArea.selectParagraph();
571
        mTextArea.replaceSelection( p.substring( 1 ) );
572
      }
573
    }
574
  }
575
576
  /**
577
   * Observers may listen for changes to the property returned from this method
578
   * to receive notifications when either the text or caret have changed. This
579
   * should not be used to track whether the text has been modified.
580
   */
581
  public void addDirtyListener( ChangeListener<Boolean> listener ) {
582
    mDirty.addListener( listener );
583
  }
584
585
  /**
586
   * Surrounds the selected text or word under the caret in Markdown markup.
587
   *
588
   * @param token The beginning and ending token for enclosing the text.
589
   */
590
  private void enwrap( final String token ) {
591
    enwrap( token, token );
592
  }
593
594
  /**
595
   * Surrounds the selected text or word under the caret in Markdown markup.
596
   *
597
   * @param began The beginning token for enclosing the text.
598
   * @param ended The ending token for enclosing the text.
599
   */
600
  private void enwrap( final String began, String ended ) {
601
    // Ensure selected text takes precedence over the word at caret position.
602
    final var selected = mTextArea.selectionProperty().getValue();
603
    final var range = selected.getLength() == 0
604
      ? getCaretWord()
605
      : selected;
606
    String text = mTextArea.getText( range );
607
608
    int length = range.getLength();
609
    text = stripStart( text, null );
610
    final int beganIndex = range.getStart() + length - text.length();
611
612
    length = text.length();
613
    text = stripEnd( text, null );
614
    final int endedIndex = range.getEnd() - (length - text.length());
615
616
    mTextArea.replaceText( beganIndex, endedIndex, began + text + ended );
617
  }
618
619
  /**
620
   * Inserts the given block-level markup at the current caret position
621
   * within the document. This will prepend two blank lines to ensure that
622
   * the block element begins at the start of a new line.
623
   *
624
   * @param markup The text to insert at the caret.
625
   */
626
  private void block( final String markup ) {
627
    final int pos = mTextArea.getCaretPosition();
628
    mTextArea.insertText( pos, format( "%n%n%s", markup ) );
629
  }
630
631
  /**
632
   * Returns the caret position within the current paragraph.
633
   *
634
   * @return A value from 0 to the length of the current paragraph.
635
   */
636
  private int getCaretColumn() {
637
    return mTextArea.getCaretColumn();
638
  }
639
640
  @Override
641
  public IndexRange getCaretWord() {
642
    final var paragraph = getCaretParagraph()
643
      .replaceAll( "---", "   " )
644
      .replaceAll( "--", "  " )
645
      .replaceAll( "[\\[\\]{}()]", " " );
646
    final var length = paragraph.length();
647
    final var column = getCaretColumn();
648
649
    var began = column;
650
    var ended = column;
651
652
    while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
653
      began--;
654
    }
655
656
    while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
657
      ended++;
658
    }
659
660
    final var iterator = BreakIterator.getWordInstance();
661
    iterator.setText( paragraph );
662
663
    while( began < length && iterator.isBoundary( began + 1 ) ) {
664
      began++;
665
    }
666
667
    while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
668
      ended--;
669
    }
670
671
    final var offset = getCaretDocumentOffset( column );
672
673
    return IndexRange.normalize( began + offset, ended + offset );
674
  }
675
676
  private int getCaretDocumentOffset( final int column ) {
677
    return mTextArea.getCaretPosition() - column;
678
  }
679
680
  /**
681
   * Returns the index of the paragraph where the caret resides.
682
   *
683
   * @return A number greater than or equal to 0.
684
   */
685
  private int getCurrentParagraph() {
686
    return mTextArea.getCurrentParagraph();
687
  }
688
689
  /**
690
   * Returns the text for the paragraph that contains the caret.
691
   *
692
   * @return A non-null string, possibly empty.
693
   */
694
  private String getCaretParagraph() {
695
    return getText( getCurrentParagraph() );
696
  }
697
698
  @Override
699
  public String getText( final int paragraph ) {
700
    return mTextArea.getText( paragraph );
701
  }
702
703
  @Override
704
  public String getText( final IndexRange indexes )
705
    throws IndexOutOfBoundsException {
706
    return mTextArea.getText( indexes.getStart(), indexes.getEnd() );
707
  }
708
709
  @Override
710
  public void replaceText( final IndexRange indexes, final String s ) {
711
    mTextArea.replaceText( indexes, s );
712
  }
713
714
  private UndoManager<?> getUndoManager() {
715
    return mTextArea.getUndoManager();
716
  }
717
718
  /**
719
   * Returns the path to a {@link Locale}-specific stylesheet.
720
   *
721
   * @return A non-null string to inject into the HTML document head.
722
   */
723
  private static String getStylesheetPath( final Locale locale ) {
724
    return MessageFormat.format(
725
      sSettings.getSetting( STYLESHEET_MARKDOWN_LOCALE, "" ),
726
      locale.getLanguage(),
727
      locale.getScript(),
728
      locale.getCountry()
729
    );
730
  }
731
732
  private Locale getLocale() {
733
    return localeProperty().toLocale();
734
  }
735
736
  private LocaleProperty localeProperty() {
737
    return mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
738
  }
739
740
  /**
741
   * Sets the font family name and font size at the same time. When the
742
   * workspace is loaded, the default font values are changed, which results
743
   * in this method being called.
744
   *
745
   * @param area   Change the font settings for this text area.
746
   * @param name   New font family name to apply.
747
   * @param points New font size to apply (in points, not pixels).
748
   */
749
  private void setFont(
750
    final StyleClassedTextArea area, final String name, final double points ) {
751
    runLater( () -> area.setStyle(
752
      format(
753
        "-fx-font-family:'%s';-fx-font-size:%spx;", name, toPixels( points )
754
      )
755
    ) );
756
  }
757
758
  private String getFontName() {
759
    return fontNameProperty().get();
760
  }
761
762
  private StringProperty fontNameProperty() {
763
    return mWorkspace.stringProperty( KEY_UI_FONT_EDITOR_NAME );
764
  }
765
766
  private double getFontSize() {
767
    return fontSizeProperty().get();
768
  }
769
770
  private DoubleProperty fontSizeProperty() {
771
    return mWorkspace.doubleProperty( KEY_UI_FONT_EDITOR_SIZE );
772
  }
773
774
  /**
775
   * Answers whether the given resource is of compatible {@link MediaType}s.
776
   *
777
   * @param mediaType The {@link MediaType} to compare.
778
   * @return {@code true} if the given {@link MediaType} is suitable for
779
   * editing with this type of editor.
780
   */
781
  @Override
782
  public boolean supports( final MediaType mediaType ) {
783
    return isMediaType( mediaType ) ||
784
      mediaType == TEXT_MARKDOWN ||
785
      mediaType == TEXT_R_MARKDOWN;
786
  }
787
}
1788
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-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
import org.greenrobot.eventbus.EventBus;
5
6
/**
7
 * Responsible for delegating interactions to the event bus library. This
8
 * class decouples the rest of the application from a particular event bus
9
 * implementation.
10
 */
11
public class Bus {
12
  private static final EventBus sEventBus = EventBus
13
    .builder().logNoSubscriberMessages( false ).installDefaultEventBus();
14
15
  public static <Subscriber> void register( final Subscriber subscriber ) {
16
    sEventBus.register( subscriber );
17
  }
18
19
  public static <Subscriber> void unregister( final Subscriber subscriber ) {
20
    sEventBus.unregister( subscriber );
21
  }
22
23
  public static <Event> void post( final Event event ) {
24
    sEventBus.post( event );
25
  }
26
}
127
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 java.lang.String.format;
13
import static java.lang.String.join;
14
import static java.util.Arrays.stream;
15
16
/**
17
 * Collates information about an application issue. The issues can be
18
 * exceptions, state problems, parsing errors, and so forth.
19
 */
20
public final class StatusEvent implements AppEvent {
21
  private static final String ENGLISHIFY =
22
    "(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])";
23
24
  /**
25
   * Detailed information about a problem.
26
   */
27
  private final String mMessage;
28
29
  /**
30
   * Provides stack trace information that isolates the cause.
31
   */
32
  private final Throwable mProblem;
33
34
  /**
35
   * Constructs a new event that contains a problem description to help the
36
   * user resolve an issue encountered while using the application.
37
   *
38
   * @param message The human-readable message, typically displayed on-screen.
39
   */
40
  public StatusEvent( final String message ) {
41
    this( message, null );
42
  }
43
44
  /**
45
   * Constructs a new event that contains information about an unexpected issue.
46
   *
47
   * @param problem The issue encountered by the software, never {@code null}.
48
   */
49
  public StatusEvent( final Throwable problem ) {
50
    this( problem.getMessage(), problem );
51
  }
52
53
  /**
54
   * @param message The human-readable message text.
55
   * @param problem May be {@code null} if no exception was thrown.
56
   */
57
  public StatusEvent( final String message, final Throwable problem ) {
58
    mMessage = message == null ? "" : message;
59
    mProblem = problem;
60
  }
61
62
  /**
63
   * Returns the stack trace information for the issue encountered. This is
64
   * optional because usually a status message isn't an application error.
65
   *
66
   * @return Optional stack trace to pinpoint the problem area in the code.
67
   */
68
  public String getProblem() {
69
    // 256 is arbitrary; stack traces shouldn't be much larger.
70
    final var sb = new StringBuilder( 256 );
71
    final var trace = mProblem;
72
73
    if( trace != null ) {
74
      stream( trace.getStackTrace() )
75
        .takeWhile( StatusEvent::filter )
76
        .limit( 15 )
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( "%s%s%s",
90
                   message,
91
                   message.isBlank() ? "" : " ",
92
                   mProblem == null ? "" : toEnglish( mProblem ) );
93
  }
94
95
  /**
96
   * Returns {@code true} to allow the {@link StackTraceElement} to pass
97
   * through the filter.
98
   *
99
   * @param e The element to check against the filter.
100
   */
101
  private static boolean filter( final StackTraceElement e ) {
102
    final var clazz = e.getClassName();
103
    return !(clazz.contains( "org.renjin." ) ||
104
      clazz.contains( "sun." ) ||
105
      clazz.contains( "flexmark." ) ||
106
      clazz.contains( "java." )
107
    );
108
  }
109
110
  /**
111
   * Separates the exception class name from TitleCase into lowercase,
112
   * space-separated words. This makes the exception look a little more like
113
   * English. Any {@link RuntimeException} instances passed into this method
114
   * will have the cause extracted, if possible.
115
   *
116
   * @param problem The exception that triggered the status event change.
117
   * @return A human-readable message with the exception name and the
118
   * exception's message.
119
   */
120
  private static String toEnglish( Throwable problem ) {
121
    assert problem != null;
122
123
    // Subclasses of RuntimeException must be subject to Englishification.
124
    if( problem.getClass().equals( RuntimeException.class ) ) {
125
      final var cause = problem.getCause();
126
      return cause == null ? problem.getMessage() : cause.getMessage();
127
    }
128
129
    final var className = problem.getClass().getSimpleName();
130
    final var words = join( " ", className.split( ENGLISHIFY ) );
131
    return format( "(%s: %s)", words.toLowerCase(), problem.getMessage() );
132
  }
133
134
  /**
135
   * Returns the message used to construct the event.
136
   *
137
   * @return The message for this event.
138
   */
139
  public String getMessage() {
140
    return mMessage;
141
  }
142
143
  /**
144
   * Resets the status bar to a default message. Indicates that there are no
145
   * issues to bring to the user's attention.
146
   */
147
  public static void clue() {
148
    fire( get( STATUS_BAR_OK, "OK" ) );
149
  }
150
151
  /**
152
   * Notifies listeners of a series of messages. This is useful when providing
153
   * users feedback of how third-party executables have failed.
154
   *
155
   * @param messages The lines of text to display.
156
   */
157
  public static void clue( final List<String> messages ) {
158
    messages.forEach( StatusEvent::fire );
159
  }
160
161
  /**
162
   * Notifies listeners of an error.
163
   *
164
   * @param key The message bundle key to look up.
165
   * @param t   The exception that caused the error.
166
   */
167
  public static void clue( final String key, final Throwable t ) {
168
    fire( get( key ), t );
169
  }
170
171
  /**
172
   * Notifies listeners of a custom message.
173
   *
174
   * @param key  The property key having a value to populate with arguments.
175
   * @param args The placeholder values to substitute into the key's value.
176
   */
177
  public static void clue( final String key, final Object... args ) {
178
    fire( get( key, args ) );
179
  }
180
181
  /**
182
   * Notifies listeners of a custom message.
183
   *
184
   * @param ex   The exception that warranted calling this method.
185
   * @param fmt  The string format specifier.
186
   * @param args The arguments to weave into the format specifier.
187
   */
188
  public static void clue(
189
    final Exception ex,
190
    final String fmt,
191
    final Object... args ) {
192
    final var msg = format( fmt, args );
193
    clue( msg, ex );
194
  }
195
196
  /**
197
   * Notifies listeners of an exception occurs that warrants the user's
198
   * attention.
199
   *
200
   * @param problem The exception with a message to display to the user.
201
   */
202
  public static void clue( final Throwable problem ) {
203
    fire( problem );
204
  }
205
206
  private static void fire( final String message ) {
207
    new StatusEvent( message ).publish();
208
  }
209
210
  private static void fire( final Throwable problem ) {
211
    new StatusEvent( problem ).publish();
212
  }
213
214
  private static void fire(
215
    final String message, final Throwable problem ) {
216
    new StatusEvent( message, problem ).publish();
217
  }
218
}
1219
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
35
  /*
36
   * Standard font types.
37
   */
38
  FONT_OTF( "otf" ),
39
  FONT_TTF( "ttf" ),
40
41
  /*
42
   * Standard image types.
43
   */
44
  IMAGE_APNG( "apng" ),
45
  IMAGE_ACES( "aces" ),
46
  IMAGE_AVCI( "avci" ),
47
  IMAGE_AVCS( "avcs" ),
48
  IMAGE_BMP( "bmp" ),
49
  IMAGE_CGM( "cgm" ),
50
  IMAGE_DICOM_RLE( "dicom_rle" ),
51
  IMAGE_EMF( "emf" ),
52
  IMAGE_EXAMPLE( "example" ),
53
  IMAGE_FITS( "fits" ),
54
  IMAGE_G3FAX( "g3fax" ),
55
  IMAGE_GIF( "gif" ),
56
  IMAGE_HEIC( "heic" ),
57
  IMAGE_HEIF( "heif" ),
58
  IMAGE_HEJ2K( "hej2k" ),
59
  IMAGE_HSJ2( "hsj2" ),
60
  IMAGE_X_ICON( "x-icon" ),
61
  IMAGE_JLS( "jls" ),
62
  IMAGE_JP2( "jp2" ),
63
  IMAGE_JPEG( "jpeg" ),
64
  IMAGE_JPH( "jph" ),
65
  IMAGE_JPHC( "jphc" ),
66
  IMAGE_JPM( "jpm" ),
67
  IMAGE_JPX( "jpx" ),
68
  IMAGE_JXR( "jxr" ),
69
  IMAGE_JXRA( "jxrA" ),
70
  IMAGE_JXRS( "jxrS" ),
71
  IMAGE_JXS( "jxs" ),
72
  IMAGE_JXSC( "jxsc" ),
73
  IMAGE_JXSI( "jxsi" ),
74
  IMAGE_JXSS( "jxss" ),
75
  IMAGE_KTX( "ktx" ),
76
  IMAGE_KTX2( "ktx2" ),
77
  IMAGE_NAPLPS( "naplps" ),
78
  IMAGE_PNG( "png" ),
79
  IMAGE_PHOTOSHOP( "photoshop" ),
80
  IMAGE_SVG_XML( "svg+xml" ),
81
  IMAGE_T38( "t38" ),
82
  IMAGE_TIFF( "tiff" ),
83
  IMAGE_WEBP( "webp" ),
84
  IMAGE_WMF( "wmf" ),
85
  IMAGE_X_BITMAP( "x-xbitmap" ),
86
  IMAGE_X_PIXMAP( "x-xpixmap" ),
87
88
  /*
89
   * Standard audio types.
90
   */
91
  AUDIO_SIMPLE( AUDIO, "basic" ),
92
  AUDIO_MP3( AUDIO, "mp3" ),
93
  AUDIO_WAV( AUDIO, "x-wav" ),
94
95
  /*
96
   * Standard video types.
97
   */
98
  VIDEO_MNG( VIDEO, "x-mng" ),
99
100
  /*
101
   * Document types for editing or displaying documents, mix of standard and
102
   * application-specific. The order that these are declared reflect in the
103
   * ordinal value used during comparisons.
104
   */
105
  TEXT_YAML( TEXT, "yaml" ),
106
  TEXT_PLAIN( TEXT, "plain" ),
107
  TEXT_MARKDOWN( TEXT, "markdown" ),
108
  TEXT_R_MARKDOWN( TEXT, "R+markdown" ),
109
  TEXT_PROPERTIES( TEXT, "x-java-properties" ),
110
  TEXT_HTML( TEXT, "html" ),
111
  TEXT_XHTML( TEXT, "xhtml+xml" ),
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_XHTML( TEXT_XHTML, of( "htm", "html", "xhtml" ) ),
55
  MEDIA_TEXT_XML( TEXT_XML ),
56
  MEDIA_TEXT_YAML( TEXT_YAML, of( "yaml", "yml" ) ),
57
58
  MEDIA_UNDEFINED( UNDEFINED, of( "undefined" ) );
59
60
  /**
61
   * Returns the {@link MediaTypeExtension} that matches the given media type.
62
   *
63
   * @param mediaType The media type to find.
64
   * @return The correlated value or {@link #MEDIA_UNDEFINED} if not found.
65
   */
66
  public static MediaTypeExtension valueFrom( final MediaType mediaType ) {
67
    for( final var type : values() ) {
68
      if( type.isMediaType( mediaType ) ) {
69
        return type;
70
      }
71
    }
72
73
    return MEDIA_UNDEFINED;
74
  }
75
76
  /**
77
   * Returns the {@link MediaType} associated with the given file name
78
   * extension. The extension must not contain a period.
79
   *
80
   * @param extension File name extension, case-insensitive, {@code null}-safe.
81
   * @return The associated {@link MediaType} as defined by IANA.
82
   */
83
  public static MediaType fromExtension( final String extension ) {
84
    final var sanitized = sanitize( extension );
85
86
    for( final var mediaType : MediaTypeExtension.values() ) {
87
      if( mediaType.isType( sanitized ) ) {
88
        return mediaType.getMediaType();
89
      }
90
    }
91
92
    return UNDEFINED;
93
  }
94
95
  /**
96
   * Returns the {@link MediaType} associated with the given file.
97
   *
98
   * @param file The file having an extension to map to a {@link MediaType}.
99
   * @return The associated {@link MediaType} as defined by IANA.
100
   */
101
  public static MediaType fromFile( final File file ) {
102
    return fromExtension( FilenameUtils.getExtension( file.getName() ) );
103
  }
104
105
  public static MediaType fromPath( final Path path ) {
106
    return fromFile( toFile( path ) );
107
  }
108
109
  private static String sanitize( final String extension ) {
110
    return extension == null ? "" : extension.toLowerCase();
111
  }
112
113
  private final MediaType mMediaType;
114
  private final List<String> mExtensions;
115
116
  /**
117
   * Several media types have only one corresponding standard file name
118
   * extension; this constructor calls {@link MediaType#getSubtype()} to obtain
119
   * said extension. Some {@link MediaType}s have a single extension but their
120
   * assigned IANA name differs (e.g., {@code svg} maps to {@code svg+xml})
121
   * and thus must not use this constructor.
122
   *
123
   * @param mediaType The {@link MediaType} containing only one extension.
124
   */
125
  MediaTypeExtension( final MediaType mediaType ) {
126
    this( mediaType, of( mediaType.getSubtype() ) );
127
  }
128
129
  /**
130
   * Constructs an association of file name extensions to a single {@link
131
   * MediaType}.
132
   *
133
   * @param mediaType  The {@link MediaType} to associate with the given
134
   *                   file name extensions.
135
   * @param extensions The file name extensions used to lookup a corresponding
136
   *                   {@link MediaType}.
137
   */
138
  MediaTypeExtension(
139
    final MediaType mediaType, final List<String> extensions ) {
140
    assert mediaType != null;
141
    assert extensions != null;
142
    assert !extensions.isEmpty();
143
144
    mMediaType = mediaType;
145
    mExtensions = extensions;
146
  }
147
148
  /**
149
   * Returns the first file name extension in the list of file names given
150
   * at construction time.
151
   *
152
   * @return The one file name to rule them all.
153
   */
154
  public String getExtension() {
155
    return mExtensions.get( 0 );
156
  }
157
158
  boolean isMediaType( final MediaType mediaType ) {
159
    return mMediaType == mediaType;
160
  }
161
162
  private boolean isType( final String sanitized ) {
163
    for( final var extension : mExtensions ) {
164
      if( extension.equalsIgnoreCase( sanitized ) ) {
165
        return true;
166
      }
167
    }
168
169
    return false;
170
  }
171
172
  private MediaType getMediaType() {
173
    return mMediaType;
174
  }
175
}
1176
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.System.arraycopy;
13
import static java.util.Arrays.fill;
14
15
/**
16
 * Associates file signatures with IANA-defined {@link MediaType}s. See:
17
 * <a href="https://www.garykessler.net/library/file_sigs.html">
18
 * Gary Kessler's List
19
 * </a>,
20
 * <a href="https://en.wikipedia.org/wiki/List_of_file_signatures">
21
 * Wikipedia's List
22
 * </a>, and
23
 * <a href="https://github.com/veniware/Space-Maker/blob/master/FileSignatures.cs">
24
 * Space Maker's List
25
 * </a>
26
 */
27
public class MediaTypeSniffer {
28
  /**
29
   * The maximum buffer size of magic bytes to analyze.
30
   */
31
  private static final int BUFFER = 12;
32
33
  /**
34
   * The media type data can have any value at a corresponding offset.
35
   */
36
  private static final int ANY = -1;
37
38
  /**
39
   * Denotes there are fewer than {@link #BUFFER} bytes to compare.
40
   */
41
  private static final int EOS = -2;
42
43
  private static final Map<int[], MediaType> FORMAT = new LinkedHashMap<>();
44
45
  private static void put( final int[] data, final MediaType mediaType ) {
46
    FORMAT.put( data, mediaType );
47
  }
48
49
  /* The insertion order attempts to approximate the real-world likelihood of
50
   * encountering particular file formats in an application.
51
   */
52
  static {
53
    //@formatter:off
54
    put( ints( 0x3C, 0x73, 0x76, 0x67, 0x20 ), IMAGE_SVG_XML );
55
    put( ints( 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ), IMAGE_PNG );
56
    put( ints( 0xFF, 0xD8, 0xFF, 0xE0 ), IMAGE_JPEG );
57
    put( ints( 0xFF, 0xD8, 0xFF, 0xEE ), IMAGE_JPEG );
58
    put( ints( 0xFF, 0xD8, 0xFF, 0xE1, ANY, ANY, 0x45, 0x78, 0x69, 0x66, 0x00 ), IMAGE_JPEG );
59
    put( ints( 0x3C, 0x21 ), TEXT_HTML );
60
    put( ints( 0x3C, 0x68, 0x74, 0x6D, 0x6C ), TEXT_HTML );
61
    put( ints( 0x3C, 0x68, 0x65, 0x61, 0x64 ), TEXT_HTML );
62
    put( ints( 0x3C, 0x62, 0x6F, 0x64, 0x79 ), TEXT_HTML );
63
    put( ints( 0x3C, 0x48, 0x54, 0x4D, 0x4C ), TEXT_HTML );
64
    put( ints( 0x3C, 0x48, 0x45, 0x41, 0x44 ), TEXT_HTML );
65
    put( ints( 0x3C, 0x42, 0x4F, 0x44, 0x59 ), TEXT_HTML );
66
    put( ints( 0x3C, 0x3F, 0x78, 0x6D, 0x6C, 0x20 ), TEXT_XML );
67
    put( ints( 0xFE, 0xFF, 0x00, 0x3C, 0x00, 0x3f, 0x00, 0x78 ), TEXT_XML );
68
    put( ints( 0xFF, 0xFE, 0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00 ), TEXT_XML );
69
    put( ints( 0x47, 0x49, 0x46, 0x38 ), IMAGE_GIF );
70
    put( ints( 0x42, 0x4D ), IMAGE_BMP );
71
    put( ints( 0x49, 0x49, 0x2A, 0x00 ), IMAGE_TIFF );
72
    put( ints( 0x4D, 0x4D, 0x00, 0x2A ), IMAGE_TIFF );
73
    put( ints( 0x52, 0x49, 0x46, 0x46, ANY, ANY, ANY, ANY, 0x57, 0x45, 0x42, 0x50 ), IMAGE_WEBP );
74
    put( ints( 0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E ), APP_PDF );
75
    put( ints( 0x25, 0x21, 0x50, 0x53, 0x2D, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x2D ), APP_EPS );
76
    put( ints( 0x25, 0x21, 0x50, 0x53 ), APP_PS );
77
    put( ints( 0x38, 0x42, 0x50, 0x53, 0x00, 0x01 ), IMAGE_PHOTOSHOP );
78
    put( ints( 0xFF, 0xFB, 0x30 ), AUDIO_MP3 );
79
    put( ints( 0x49, 0x44, 0x33 ), AUDIO_MP3 );
80
    put( ints( 0x8A, 0x4D, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ), VIDEO_MNG );
81
    put( ints( 0x23, 0x64, 0x65, 0x66 ), IMAGE_X_BITMAP );
82
    put( ints( 0x21, 0x20, 0x58, 0x50, 0x4D, 0x32 ), IMAGE_X_PIXMAP );
83
    put( ints( 0x2E, 0x73, 0x6E, 0x64 ), AUDIO_SIMPLE );
84
    put( ints( 0x64, 0x6E, 0x73, 0x2E ), AUDIO_SIMPLE );
85
    put( ints( 0x52, 0x49, 0x46, 0x46 ), AUDIO_WAV );
86
    put( ints( 0x50, 0x4B ), APP_ZIP );
87
    put( ints( 0x41, 0x43, ANY, ANY, ANY, ANY, 0x00, 0x00, 0x00, 0x00, 0x00 ), APP_ACAD );
88
    put( ints( 0xCA, 0xFE, 0xBA, 0xBE ), APP_JAVA );
89
    put( ints( 0xAC, 0xED ), APP_JAVA_OBJECT );
90
    //@formatter:on
91
  }
92
93
  /**
94
   * Returns the {@link MediaType} for a given set of bytes.
95
   *
96
   * @param data Binary data to compare against the list of known formats.
97
   * @return The IANA-defined {@link MediaType}, or
98
   * {@link MediaType#UNDEFINED} if indeterminate.
99
   */
100
  public static MediaType getMediaType( final byte[] data ) {
101
    assert data != null;
102
    assert data.length > 0;
103
104
    final var source = new int[]{
105
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
106
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
107
      0xFF, 0XFF, EOS
108
    };
109
110
    for( int i = 0; i < Math.min( data.length, source.length ); i++ ) {
111
      source[ i ] = data[ i ] & 0xFF;
112
    }
113
114
    for( final var entry : FORMAT.entrySet() ) {
115
      final var key = entry.getKey();
116
117
      int i = -1;
118
      boolean matches = true;
119
120
      while( ++i < BUFFER && key[ i ] != EOS && matches ) {
121
        matches = key[ i ] == source[ i ] || key[ i ] == ANY;
122
      }
123
124
      if( matches ) {
125
        return entry.getValue();
126
      }
127
    }
128
129
    return UNDEFINED;
130
  }
131
132
  /**
133
   * Convenience method to return the probed media type for the given
134
   * {@link SysFile} instance by delegating to
135
   * {@link #getMediaType(InputStream)}.
136
   *
137
   * @param file File to ascertain the {@link MediaType}.
138
   * @return The IANA-defined {@link MediaType}, or
139
   * {@link MediaType#UNDEFINED} if indeterminate.
140
   * @throws IOException Could not read from the {@link File}.
141
   */
142
  public static MediaType getMediaType( final File file )
143
    throws IOException {
144
    try( final var fis = new FileInputStream( file ) ) {
145
      return getMediaType( fis );
146
    }
147
  }
148
149
  /**
150
   * Convenience method to return the probed media type for the given
151
   * {@link BufferedInputStream} instance. <strong>This resets the stream
152
   * pointer</strong> making the call idempotent. Prefer calling this
153
   * method when operating on streams to avoid advancing the stream.
154
   *
155
   * @param bis Data source to ascertain the {@link MediaType}.
156
   * @return The IANA-defined {@link MediaType}, or
157
   * {@link MediaType#UNDEFINED} if indeterminate.
158
   * @throws IOException Could not read from the stream.
159
   */
160
  public static MediaType getMediaType( final BufferedInputStream bis )
161
    throws IOException {
162
    bis.mark( BUFFER );
163
    final var result = getMediaType( (InputStream) bis );
164
    bis.reset();
165
166
    return result;
167
  }
168
169
  /**
170
   * Returns the probed media type for the given {@link InputStream} instance.
171
   * The caller is responsible for closing the stream. <strong>This advances
172
   * the stream.</strong> Use {@link #getMediaType(BufferedInputStream)} to
173
   * perform a non-destructive read.
174
   *
175
   * @param is Data source to ascertain the {@link MediaType}.
176
   * @return The IANA-defined {@link MediaType}, or
177
   * {@link MediaType#UNDEFINED} if indeterminate.
178
   * @throws IOException Could not read from the {@link InputStream}.
179
   */
180
  private static MediaType getMediaType( final InputStream is )
181
    throws IOException {
182
    final var input = new byte[ BUFFER ];
183
    final var count = is.read( input, 0, BUFFER );
184
185
    if( count > 1 ) {
186
      final var available = new byte[ count ];
187
      arraycopy( input, 0, available, 0, count );
188
      return getMediaType( available );
189
    }
190
191
    return UNDEFINED;
192
  }
193
194
  /**
195
   * Creates an integer array from the given data, padded with {@link #EOS}
196
   * values up to {@link #BUFFER} in length.
197
   *
198
   * @param data The input byte values to pad.
199
   * @return The data with padding.
200
   */
201
  private static int[] ints( final int... data ) {
202
    assert data != null;
203
204
    final var magic = new int[ data.length + 1 ];
205
206
    fill( magic, EOS );
207
    arraycopy( data, 0, magic, 0, data.length );
208
209
    return magic;
210
  }
211
}
1212
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-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import org.jetbrains.annotations.NotNull;
5
6
import java.io.File;
7
import java.io.FileInputStream;
8
import java.io.IOException;
9
import java.nio.file.Path;
10
import java.security.MessageDigest;
11
import java.security.NoSuchAlgorithmException;
12
import java.util.ArrayList;
13
import java.util.Optional;
14
import java.util.function.Function;
15
import java.util.function.Predicate;
16
17
import static com.keenwrite.constants.Constants.USER_DIRECTORY;
18
import static com.keenwrite.events.StatusEvent.clue;
19
import static com.keenwrite.io.WindowsRegistry.pathsWindows;
20
import static com.keenwrite.util.DataTypeConverter.toHex;
21
import static java.lang.System.getenv;
22
import static java.nio.file.Files.isExecutable;
23
import static java.util.regex.Pattern.quote;
24
import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
25
26
/**
27
 * Responsible for file-related functionality.
28
 */
29
public final class SysFile extends java.io.File {
30
  /**
31
   * For finding executable programs. These are used in an O( n^2 ) search,
32
   * so don't add more entries than necessary.
33
   */
34
  private static final String[] EXTENSIONS = new String[]
35
    {"", ".exe", ".bat", ".cmd", ".msi", ".com"};
36
37
  private static final String WHERE_COMMAND =
38
    IS_OS_WINDOWS ? "where" : "which";
39
40
  /**
41
   * Number of bytes to read at a time when computing this file's checksum.
42
   */
43
  private static final int BUFFER_SIZE = 16384;
44
45
  /**
46
   * Creates a new instance for a given file name.
47
   *
48
   * @param filename Filename to query existence as executable.
49
   */
50
  public SysFile( final String filename ) {
51
    super( filename );
52
  }
53
54
  /**
55
   * Creates a new instance for a given {@link File}. This is useful for
56
   * validating checksums against an existing {@link File} instance that
57
   * may optionally exist in a directory listed in the PATH environment
58
   * variable.
59
   *
60
   * @param file The file to change into a "system file".
61
   */
62
  public SysFile( final File file ) {
63
    super( file.getAbsolutePath() );
64
  }
65
66
  /**
67
   * Answers whether an executable can be found that can be run using a
68
   * {@link ProcessBuilder}.
69
   *
70
   * @return {@code true} if the executable is runnable.
71
   */
72
  public boolean canRun() {
73
    return locate().isPresent();
74
  }
75
76
  /**
77
   * For a file name that represents an executable (without an extension)
78
   * file, this determines the first matching executable found in the PATH
79
   * environment variable. This will search the PATH each time the method
80
   * is invoked, triggering a full directory scan for all paths listed in
81
   * the environment variable. The result is not cached, so avoid calling
82
   * this in a critical loop.
83
   * <p>
84
   * After installing software, the software might be located in the PATH,
85
   * but not available to run by its name alone. In such cases, we need the
86
   * absolute path to the executable to run it. This will always return
87
   * the fully qualified path, otherwise an empty result.
88
   *
89
   * @return Fully qualified path to the executable, if found.
90
   */
91
  public Optional<Path> locate() {
92
    final var dirList = new ArrayList<String>();
93
    final var paths = pathsSane();
94
    int began = 0;
95
    int ended;
96
97
    while( (ended = paths.indexOf( pathSeparatorChar, began )) != -1 ) {
98
      final var dir = paths.substring( began, ended );
99
      began = ended + 1;
100
101
      dirList.add( dir );
102
    }
103
104
    final var dirs = dirList.toArray( new String[]{} );
105
    var path = locate( dirs, "Wizard.container.executable.path" );
106
107
    if( path.isEmpty() ) {
108
      clue();
109
110
      try {
111
        path = where();
112
      } catch( final IOException ex ) {
113
        clue( "Wizard.container.executable.which", ex );
114
      }
115
    }
116
117
    return path.isPresent()
118
      ? path
119
      : locate( System::getenv,
120
                IS_OS_WINDOWS
121
                  ? "Wizard.container.executable.registry"
122
                  : "Wizard.container.executable.path" );
123
  }
124
125
  private Optional<Path> locate( final String[] dirs, final String msg ) {
126
    final var exe = getName();
127
128
    for( final var dir : dirs ) {
129
      Path p;
130
131
      try {
132
        p = Path.of( dir ).resolve( exe );
133
      } catch( final Exception ex ) {
134
        clue( ex );
135
        continue;
136
      }
137
138
      for( final var extension : EXTENSIONS ) {
139
        final var filename = Path.of( p + extension );
140
141
        if( isExecutable( filename ) ) {
142
          return Optional.of( filename );
143
        }
144
      }
145
    }
146
147
    clue( msg );
148
    return Optional.empty();
149
  }
150
151
  private Optional<Path> locate(
152
    final Function<String, String> map, final String msg ) {
153
    final var paths = paths( map ).split( quote( pathSeparator ) );
154
155
    return locate( paths, msg );
156
  }
157
158
  /**
159
   * Runs {@code where} or {@code which} to determine the fully qualified path
160
   * to an executable.
161
   *
162
   * @return The path to the executable for this file, if found.
163
   * @throws IOException Could not determine the location of the command.
164
   */
165
  public Optional<Path> where() throws IOException {
166
    // The "where" command on Windows will automatically add the extension.
167
    final var args = new String[]{WHERE_COMMAND, getName()};
168
    final var output = run( text -> true, args );
169
    final var result = output.lines().findFirst();
170
171
    return result.map( Path::of );
172
  }
173
174
  /**
175
   * Changes to the PATH environment variable aren't reflected for the
176
   * currently running task. The registry, however, contains the updated
177
   * value. Reading the registry is a hack.
178
   *
179
   * @param map The mapping function of registry variable names to values.
180
   * @return The revised PATH variables as stored in the registry.
181
   */
182
  private static String paths( final Function<String, String> map ) {
183
    return IS_OS_WINDOWS ? pathsWindows( map ) : pathsSane();
184
  }
185
186
  /**
187
   * Answers whether this file's SHA-256 checksum equals the given
188
   * hexadecimal-encoded checksum string.
189
   *
190
   * @param hex The string to compare against the checksum for this file.
191
   * @return {@code true} if the checksums match; {@code false} on any
192
   * error or checksums don't match.
193
   */
194
  public boolean isChecksum( final String hex ) {
195
    assert hex != null;
196
197
    try {
198
      return checksum( "SHA-256" ).equalsIgnoreCase( hex );
199
    } catch( final Exception ex ) {
200
      return false;
201
    }
202
  }
203
204
  /**
205
   * Returns the hash code for this file.
206
   *
207
   * @return The hex-encoded hash code for the file contents.
208
   */
209
  @SuppressWarnings( "SameParameterValue" )
210
  private String checksum( final String algorithm )
211
    throws NoSuchAlgorithmException, IOException {
212
    final var digest = MessageDigest.getInstance( algorithm );
213
214
    try( final var in = new FileInputStream( this ) ) {
215
      final var bytes = new byte[ BUFFER_SIZE ];
216
      int count;
217
218
      while( (count = in.read( bytes )) != -1 ) {
219
        digest.update( bytes, 0, count );
220
      }
221
222
      return toHex( digest.digest() );
223
    }
224
  }
225
226
  /**
227
   * Runs a command and collects standard output into a buffer.
228
   *
229
   * @param filter Provides an injected test to determine whether the line
230
   *               read from the command's standard output is to be added to
231
   *               the result buffer.
232
   * @param args   The command and its arguments to run.
233
   * @return The standard output from the command, filtered.
234
   * @throws IOException Could not run the command.
235
   */
236
  @NotNull
237
  public static String run( final Predicate<String> filter,
238
                            final String[] args ) throws IOException {
239
    final var process = Runtime.getRuntime().exec( args );
240
    final var stream = process.getInputStream();
241
    final var stdout = new StringBuffer( 2048 );
242
243
    StreamGobbler.gobble( stream, text -> {
244
      if( filter.test( text ) ) {
245
        stdout.append( WindowsRegistry.parseRegEntry( text ) );
246
      }
247
    } );
248
249
    try {
250
      process.waitFor();
251
    } catch( final InterruptedException ex ) {
252
      throw new IOException( ex );
253
    } finally {
254
      process.destroy();
255
    }
256
257
    return stdout.toString();
258
  }
259
260
  /**
261
   * Provides {@code null}-safe machinery to get a file name.
262
   *
263
   * @param p The path to the file name to retrieve (may be {@code null}).
264
   * @return The file name or the empty string if the path is not found.
265
   */
266
  public static String getFileName( final Path p ) {
267
    return p == null ? "" : getPathFileName( p );
268
  }
269
270
  /**
271
   * If the path doesn't exist right before typesetting, switch the path
272
   * to the user's home directory to increase the odds of the typesetter
273
   * succeeding. This could help, for example, if the images directory was
274
   * deleted or moved.
275
   *
276
   * @param path The path to verify existence, may be null.
277
   * @return The given path, if it exists, otherwise the user's home directory.
278
   */
279
  public static Path normalize( final Path path ) {
280
    return path == null
281
      ? USER_DIRECTORY.toPath()
282
      : path.toFile().exists()
283
      ? path
284
      : USER_DIRECTORY.toPath();
285
  }
286
287
  public static File toFile( final Path path ) {
288
    return path == null
289
      ? USER_DIRECTORY
290
      : path.toFile();
291
  }
292
293
  private static String pathsSane() {
294
    return getenv( "PATH" );
295
  }
296
297
  private static String getPathFileName( final Path p ) {
298
    assert p != null;
299
300
    final var f = p.getFileName();
301
302
    return f == null ? "" : f.toString();
303
  }
304
}
1305
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 java.lang.System.getProperty;
11
import static java.lang.System.getenv;
12
import static org.apache.commons.lang3.SystemUtils.*;
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
  /**
50
   * Makes a valiant attempt at determining where to create application-specific
51
   * files, regardless of operating system.
52
   *
53
   * @param appName The application name that seeks to create files.
54
   * @return A fully qualified {@link Path} to a directory wherein files may
55
   * be created that are user- and application-specific.
56
   */
57
  public static Path getAppPath( final String appName ) {
58
    final var osPath = isWindows()
59
      ? getWinAppPath()
60
      : isMacOs()
61
      ? getMacAppPath()
62
      : isUnix()
63
      ? getUnixAppPath()
64
      : UNDEFINED;
65
66
    final var path = osPath.equals( UNDEFINED )
67
      ? getDefaultAppPath( appName )
68
      : osPath.resolve( appName );
69
70
    final var alternate = Path.of( PROP_USER_DIR, appName );
71
72
    return ensureExists( path )
73
      ? path
74
      : ensureExists( alternate )
75
      ? alternate
76
      : Path.of( PROP_USER_DIR );
77
  }
78
79
  private static Path getWinAppPath() {
80
    return isValid( ENV_APPDATA )
81
      ? Path.of( ENV_APPDATA )
82
      : home( getWinVerAppPath() );
83
  }
84
85
  /**
86
   * Gets the application path with respect to the Windows version.
87
   *
88
   * @return The directory name paths relative to the user's home directory.
89
   */
90
  private static String[] getWinVerAppPath() {
91
    return PROP_OS_VERSION.startsWith( "5." )
92
      ? new String[]{"Application Data"}
93
      : new String[]{"AppData", "Roaming"};
94
  }
95
96
  private static Path getMacAppPath() {
97
    final var path = home( "Library", "Application Support" );
98
99
    return ensureExists( path ) ? path : UNDEFINED;
100
  }
101
102
  private static Path getUnixAppPath() {
103
    // Fallback in case the XDG data directory is undefined.
104
    var path = home( ".local", "share" );
105
106
    if( isValid( ENV_XDG_DATA_HOME ) ) {
107
      final var xdgPath = Path.of( ENV_XDG_DATA_HOME );
108
109
      path = ensureExists( xdgPath ) ? xdgPath : path;
110
    }
111
112
    return path;
113
  }
114
115
  /**
116
   * Returns a hidden directory relative to the user's home directory.
117
   *
118
   * @param appName The application name.
119
   * @return A suitable directory for storing application files.
120
   */
121
  private static Path getDefaultAppPath( final String appName ) {
122
    return home( '.' + appName );
123
  }
124
125
  private static Path home( final String... paths ) {
126
    return Path.of( PROP_USER_HOME, paths );
127
  }
128
129
  /**
130
   * Verifies whether the path exists or was created.
131
   *
132
   * @param path The directory to verify.
133
   * @return {@code true} if the path already exists or was created,
134
   * {@code false} if the directory doesn't exist and couldn't be created.
135
   */
136
  private static boolean ensureExists( final Path path ) {
137
    final var file = toFile( path );
138
    return file.exists() || file.mkdirs();
139
  }
140
141
  /**
142
   * Answers whether the given string contains content.
143
   *
144
   * @param s The string to check, may be {@code null}.
145
   * @return {@code true} if the string is neither {@code null} nor blank.
146
   */
147
  private static boolean isValid( final String s ) {
148
    return !(s == null || s.isBlank());
149
  }
150
151
  private static boolean isWindows() {
152
    return IS_OS_WINDOWS;
153
  }
154
155
  private static boolean isMacOs() {
156
    return IS_OS_MAC;
157
  }
158
159
  private static boolean isUnix() {
160
    return IS_OS_UNIX;
161
  }
162
}
1163
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
10
import java.io.*;
11
import java.net.HttpURLConnection;
12
import java.net.URI;
13
import java.net.URISyntaxException;
14
import java.net.URL;
15
import java.time.Duration;
16
import java.util.zip.GZIPInputStream;
17
18
import static java.lang.Math.toIntExact;
19
import static java.lang.String.format;
20
import static java.lang.System.getProperty;
21
import static java.lang.System.setProperty;
22
import static java.net.HttpURLConnection.HTTP_OK;
23
import static java.net.HttpURLConnection.setFollowRedirects;
24
25
/**
26
 * Responsible for downloading files and publishing status updates. This will
27
 * download a resource provided by an instance of {@link URL} into a given
28
 * {@link OutputStream}.
29
 */
30
public final class DownloadManager {
31
  static {
32
    setProperty( "http.keepAlive", "false" );
33
    setFollowRedirects( true );
34
  }
35
36
  /**
37
   * Number of bytes to read at a time.
38
   */
39
  private static final int BUFFER_SIZE = 16384;
40
41
  /**
42
   * HTTP request timeout.
43
   */
44
  private static final Duration TIMEOUT = Duration.ofSeconds( 30 );
45
46
  @FunctionalInterface
47
  public interface ProgressListener {
48
    /**
49
     * Called when a chunk of data has been read. This is called synchronously
50
     * when downloading the data; do not execute long-running tasks in this
51
     * method (a few milliseconds is fine).
52
     *
53
     * @param percentage A value between 0 and 100, inclusive, represents the
54
     *                   percentage of bytes downloaded relative to the total.
55
     *                   A value of -1 means the total number of bytes to
56
     *                   download is unknown.
57
     * @param bytes      When {@code percentage} is greater than or equal to
58
     *                   zero, this is the total number of bytes. When {@code
59
     *                   percentage} equals -1, this is the number of bytes
60
     *                   read so far.
61
     */
62
    void update( int percentage, long bytes );
63
  }
64
65
  /**
66
   * Callers may check the value of isSuccessful
67
   */
68
  public static final class DownloadToken implements Closeable {
69
    private final HttpURLConnection mConn;
70
    private final BufferedInputStream mInput;
71
    private final MediaType mMediaType;
72
    private final long mBytesTotal;
73
74
    private DownloadToken(
75
      final HttpURLConnection conn,
76
      final BufferedInputStream input,
77
      final MediaType mediaType
78
    ) {
79
      assert conn != null;
80
      assert input != null;
81
      assert mediaType != null;
82
83
      mConn = conn;
84
      mInput = input;
85
      mMediaType = mediaType;
86
      mBytesTotal = conn.getContentLength();
87
    }
88
89
    /**
90
     * Provides the ability to download remote files asynchronously while
91
     * being updated regarding the download progress. The given
92
     * {@link OutputStream} will be closed after downloading is complete.
93
     *
94
     * @param file   Where to write the file contents.
95
     * @param listener Receives download progress status updates.
96
     * @return A {@link Runnable} task that can be executed in the background
97
     * to download the resource for this {@link DownloadToken}.
98
     */
99
    public Runnable download(
100
      final File file,
101
      final ProgressListener listener ) {
102
      return () -> {
103
        final var buffer = new byte[ BUFFER_SIZE ];
104
        final var stream = getInputStream();
105
        final var bytesTotal = mBytesTotal;
106
107
        long bytesTally = 0;
108
        int bytesRead;
109
110
        try( final var output = new FileOutputStream( file ) ) {
111
          while( (bytesRead = stream.read( buffer )) != -1 ) {
112
            if( Thread.currentThread().isInterrupted() ) {
113
              throw new InterruptedException();
114
            }
115
116
            bytesTally += bytesRead;
117
118
            if( bytesTotal > 0 ) {
119
              listener.update(
120
                toIntExact( bytesTally * 100 / bytesTotal ),
121
                bytesTotal
122
              );
123
            }
124
            else {
125
              listener.update( -1, bytesRead );
126
            }
127
128
            output.write( buffer, 0, bytesRead );
129
          }
130
        } catch( final Exception ex ) {
131
          throw new RuntimeException( ex );
132
        } finally {
133
          close();
134
        }
135
      };
136
    }
137
138
    public void close() {
139
      try {
140
        getInputStream().close();
141
      } catch( final Exception ignored ) {
142
      } finally {
143
        mConn.disconnect();
144
      }
145
    }
146
147
    /**
148
     * Returns the input stream to the resource to download.
149
     *
150
     * @return The stream to read.
151
     */
152
    public BufferedInputStream getInputStream() {
153
      return mInput;
154
    }
155
156
    public MediaType getMediaType() {
157
      return mMediaType;
158
    }
159
160
    /**
161
     * Answers whether the type of content associated with the download stream
162
     * is a scalable vector graphic.
163
     *
164
     * @return {@code true} if the given {@link MediaType} has SVG contents.
165
     */
166
    public boolean isSvg() {
167
      return getMediaType().isSvg();
168
    }
169
  }
170
171
  /**
172
   * Opens the input stream for the resource to download.
173
   *
174
   * @param uri The {@link URI} resource to download.
175
   * @return A token that can be used for downloading the content with
176
   * periodic updates or retrieving the stream for downloading the content.
177
   * @throws IOException        The stream could not be opened.
178
   * @throws URISyntaxException Invalid URI.
179
   */
180
  public static DownloadToken open( final String uri )
181
    throws IOException, URISyntaxException {
182
    // Pass an undefined media type so that any type of file can be retrieved.
183
    return open( new URI( uri ) );
184
  }
185
186
  public static DownloadToken open( final URI uri )
187
    throws IOException {
188
    return open( uri.toURL() );
189
  }
190
191
  /**
192
   * Opens the input stream for the resource to download and verifies that
193
   * the given {@link MediaType} matches the requested type. Callers are
194
   * responsible for closing the {@link DownloadManager} to close the
195
   * underlying stream and the HTTP connection. Connections must be closed by
196
   * callers if {@link DownloadToken#download(File, ProgressListener)}
197
   * isn't called (i.e., {@link DownloadToken#getMediaType()} is called
198
   * after the transport layer's Content-Type is requested but not contents
199
   * are downloaded).
200
   *
201
   * @param url The {@link URL} resource to download.
202
   * @return A token that can be used for downloading the content with
203
   * periodic updates or retrieving the stream for downloading the content.
204
   * @throws IOException The resource could not be downloaded.
205
   */
206
  public static DownloadToken open( final URL url ) throws IOException {
207
    final var conn = connect( url );
208
    final var contentType = conn.getContentType();
209
210
    MediaType remoteType;
211
212
    try {
213
      remoteType = MediaType.valueFrom( contentType );
214
    } catch( final Exception ex ) {
215
      // If the media type couldn't be detected, try using the stream.
216
      remoteType = MediaType.UNDEFINED;
217
    }
218
219
    final var input = open( conn );
220
221
    // Peek at the magic header bytes to determine the media type.
222
    final var magicType = MediaTypeSniffer.getMediaType( input );
223
224
    // If the transport protocol's Content-Type doesn't align with the
225
    // media type for the magic header, defer to the transport protocol (so
226
    // long as the content type was sent from the remote side).
227
    final MediaType mediaType = remoteType.equals( magicType )
228
      ? remoteType
229
      : contentType != null && !contentType.isBlank()
230
      ? remoteType
231
      : magicType.isUndefined()
232
      ? remoteType
233
      : magicType;
234
235
    return new DownloadToken( conn, input, mediaType );
236
  }
237
238
  /**
239
   * Establishes a connection to the remote {@link URL} resource.
240
   *
241
   * @param url The {@link URL} representing a resource to download.
242
   * @return The connection manager for the {@link URL}.
243
   * @throws IOException         Could not establish a connection.
244
   * @throws ArithmeticException Could not compute a timeout value (this
245
   *                             should never happen because the timeout is
246
   *                             less than a minute).
247
   * @see #TIMEOUT
248
   */
249
  private static HttpURLConnection connect( final URL url )
250
    throws IOException, ArithmeticException {
251
    // Both HTTP and HTTPS are covered by this condition.
252
    if( url.openConnection() instanceof HttpURLConnection conn ) {
253
      conn.setUseCaches( false );
254
      conn.setInstanceFollowRedirects( true );
255
      conn.setRequestProperty( "Accept-Encoding", "gzip" );
256
      conn.setRequestProperty( "User-Agent", getProperty( "http.agent" ) );
257
      conn.setRequestMethod( "GET" );
258
      conn.setConnectTimeout( toIntExact( TIMEOUT.toMillis() ) );
259
      conn.setRequestProperty( "connection", "close" );
260
      conn.connect();
261
262
      final var code = conn.getResponseCode();
263
264
      if( code != HTTP_OK ) {
265
        final var message = format(
266
          "%s [HTTP %d: %s]",
267
          url.getFile(),
268
          code,
269
          conn.getResponseMessage()
270
        );
271
272
        throw new IOException( message );
273
      }
274
275
      return conn;
276
    }
277
278
    throw new UnsupportedOperationException( url.toString() );
279
  }
280
281
  /**
282
   * Returns a stream in an open state. Callers are responsible for closing.
283
   *
284
   * @param conn The connection to open, which could be compressed.
285
   * @return The open stream.
286
   * @throws IOException Could not open the stream.
287
   */
288
  private static BufferedInputStream open( final HttpURLConnection conn )
289
    throws IOException {
290
    return open( conn.getContentEncoding(), conn.getInputStream() );
291
  }
292
293
  /**
294
   * Returns a stream in an open state. Callers are responsible for closing.
295
   * The input stream may be compressed.
296
   *
297
   * @param encoding The content encoding for the stream.
298
   * @param is       The stream to wrap with a suitable decoder.
299
   * @return The open stream, with any gzip content-encoding decoded.
300
   * @throws IOException Could not open the stream.
301
   */
302
  private static BufferedInputStream open(
303
    final String encoding, final InputStream is ) throws IOException {
304
    return new BufferedInputStream(
305
      "gzip".equalsIgnoreCase( encoding )
306
        ? new GZIPInputStream( is )
307
        : is
308
    );
309
  }
310
}
1311
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/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
  //@formatter:on
99
100
  /**
101
   * Only for constants, do not instantiate.
102
   */
103
  private AppKeys() { }
104
}
1105
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.AppKeys.*;
27
import static com.keenwrite.preferences.LocaleProperty.localeListProperty;
28
import static com.keenwrite.preferences.SkinProperty.skinListProperty;
29
import static com.keenwrite.preferences.TableField.ofListType;
30
import static javafx.scene.control.ButtonType.CANCEL;
31
import static javafx.scene.control.ButtonType.OK;
32
33
/**
34
 * Provides the ability for users to configure their preferences. This links
35
 * the {@link Workspace} model with the {@link PreferencesFx} view, in MVC.
36
 */
37
@SuppressWarnings( "SameParameterValue" )
38
public final class PreferencesController {
39
40
  private final Workspace mWorkspace;
41
  private final PreferencesFx mPreferencesFx;
42
43
  public PreferencesController( final Workspace workspace ) {
44
    mWorkspace = workspace;
45
46
    // Order matters: set the workspace before creating the dialog.
47
    mPreferencesFx = createPreferencesFx();
48
49
    initKeyEventHandler( mPreferencesFx );
50
    initSaveEventHandler( mPreferencesFx );
51
  }
52
53
  /**
54
   * Display the user preferences settings dialog (non-modal).
55
   */
56
  public void show() {
57
    mPreferencesFx.show( false );
58
  }
59
60
  private StringField createFontNameField(
61
    final StringProperty fontName, final DoubleProperty fontSize ) {
62
    final var control = new SimpleFontControl( "Change" );
63
64
    control.fontSizeProperty().addListener( ( c, o, n ) -> {
65
      if( n != null ) {
66
        fontSize.set( n.doubleValue() );
67
      }
68
    } );
69
70
    return ofStringType( fontName ).render( control );
71
  }
72
73
  /**
74
   * Convenience method to create a helper class for the user interface. This
75
   * establishes a key-value pair for the view.
76
   *
77
   * @param persist A reference to the values that will be persisted.
78
   * @param <K>     The type of key, usually a string.
79
   * @param <V>     The type of value, usually a string.
80
   * @return UI data model container that may update the persistent state.
81
   */
82
  private <K, V> TableField<Entry<K, V>> createTableField(
83
    final ListProperty<Entry<K, V>> persist ) {
84
    return ofListType( persist ).render( new SimpleTableControl<>() );
85
  }
86
87
  /**
88
   * Creates the preferences dialog using {@link SkeletonStorageHandler} and
89
   * numerous {@link Category} objects.
90
   *
91
   * @return A component for editing preferences.
92
   * @throws RuntimeException Could not construct the {@link PreferencesFx}
93
   *                          object (e.g., illegal access permissions,
94
   *                          unmapped XML resource).
95
   */
96
  private PreferencesFx createPreferencesFx() {
97
    return PreferencesFx.of( createStorageHandler(), createCategories() )
98
                        .instantPersistent( false )
99
                        .dialogIcon( ICON_DIALOG );
100
  }
101
102
  /**
103
   * Override the {@link PreferencesFx} storage handler to perform no actions.
104
   * Persistence is accomplished using the {@link XmlStore}.
105
   *
106
   * @return A no-op {@link StorageHandler} implementation.
107
   */
108
  private StorageHandler createStorageHandler() {
109
    return new SkeletonStorageHandler();
110
  }
111
112
  private Category[] createCategories() {
113
    return new Category[]{
114
      Category.of(
115
        get( KEY_DOC ),
116
        Group.of(
117
          get( KEY_DOC_META ),
118
          Setting.of( label( KEY_DOC_META ) ),
119
          Setting.of( title( KEY_DOC_META ),
120
                      createTableField( listEntryProperty( KEY_DOC_META ) ),
121
                      listEntryProperty( KEY_DOC_META ) )
122
        )
123
      ),
124
      Category.of(
125
        get( KEY_TYPESET ),
126
        Group.of(
127
          get( KEY_TYPESET_CONTEXT ),
128
          Setting.of( label( KEY_TYPESET_CONTEXT_THEMES_PATH ) ),
129
          Setting.of( title( KEY_TYPESET_CONTEXT_THEMES_PATH ),
130
                      directoryProperty( KEY_TYPESET_CONTEXT_THEMES_PATH ),
131
                      true ),
132
          Setting.of( label( KEY_TYPESET_CONTEXT_CLEAN ) ),
133
          Setting.of( title( KEY_TYPESET_CONTEXT_CLEAN ),
134
                      booleanProperty( KEY_TYPESET_CONTEXT_CLEAN ) )
135
        ),
136
        Group.of(
137
          get( KEY_TYPESET_CONTEXT_FONTS ),
138
          Setting.of( label( KEY_TYPESET_CONTEXT_FONTS_DIR ) ),
139
          Setting.of( title( KEY_TYPESET_CONTEXT_FONTS_DIR ),
140
                      directoryProperty( KEY_TYPESET_CONTEXT_FONTS_DIR ),
141
                      true )
142
        ),
143
        Group.of(
144
          get( KEY_TYPESET_TYPOGRAPHY ),
145
          Setting.of( label( KEY_TYPESET_TYPOGRAPHY_QUOTES ) ),
146
          Setting.of( title( KEY_TYPESET_TYPOGRAPHY_QUOTES ),
147
                      booleanProperty( KEY_TYPESET_TYPOGRAPHY_QUOTES ) )
148
        )
149
      ),
150
      Category.of(
151
        get( KEY_EDITOR ),
152
        Group.of(
153
          get( KEY_EDITOR_AUTOSAVE ),
154
          Setting.of( label( KEY_EDITOR_AUTOSAVE ) ),
155
          Setting.of( title( KEY_EDITOR_AUTOSAVE ),
156
                      integerProperty( KEY_EDITOR_AUTOSAVE ) )
157
        )
158
      ),
159
      Category.of(
160
        get( KEY_R ),
161
        Group.of(
162
          get( KEY_R_DIR ),
163
          Setting.of( label( KEY_R_DIR ) ),
164
          Setting.of( title( KEY_R_DIR ),
165
                      directoryProperty( KEY_R_DIR ),
166
                      true )
167
        ),
168
        Group.of(
169
          get( KEY_R_SCRIPT ),
170
          Setting.of( label( KEY_R_SCRIPT ) ),
171
          createMultilineSetting( "Script", KEY_R_SCRIPT )
172
        ),
173
        Group.of(
174
          get( KEY_R_DELIM_BEGAN ),
175
          Setting.of( label( KEY_R_DELIM_BEGAN ) ),
176
          Setting.of( title( KEY_R_DELIM_BEGAN ),
177
                      stringProperty( KEY_R_DELIM_BEGAN ) )
178
        ),
179
        Group.of(
180
          get( KEY_R_DELIM_ENDED ),
181
          Setting.of( label( KEY_R_DELIM_ENDED ) ),
182
          Setting.of( title( KEY_R_DELIM_ENDED ),
183
                      stringProperty( KEY_R_DELIM_ENDED ) )
184
        )
185
      ),
186
      Category.of(
187
        get( KEY_IMAGE ),
188
        Group.of(
189
          get( KEY_IMAGE_DIR ),
190
          Setting.of( label( KEY_IMAGE_DIR ) ),
191
          Setting.of( title( KEY_IMAGE_DIR ),
192
                      directoryProperty( KEY_IMAGE_DIR ),
193
                      true ),
194
          Setting.of( label( KEY_CACHE_DIR ) ),
195
          Setting.of( title( KEY_CACHE_DIR ),
196
                      directoryProperty( KEY_CACHE_DIR ),
197
                      true )
198
        ),
199
        Group.of(
200
          get( KEY_IMAGE_ORDER ),
201
          Setting.of( label( KEY_IMAGE_ORDER ) ),
202
          Setting.of( title( KEY_IMAGE_ORDER ),
203
                      stringProperty( KEY_IMAGE_ORDER ) )
204
        ),
205
        Group.of(
206
          get( KEY_IMAGE_RESIZE ),
207
          Setting.of( label( KEY_IMAGE_RESIZE ) ),
208
          Setting.of( title( KEY_IMAGE_RESIZE ),
209
                      booleanProperty( KEY_IMAGE_RESIZE ) )
210
        ),
211
        Group.of(
212
          get( KEY_IMAGE_SERVER ),
213
          Setting.of( label( KEY_IMAGE_SERVER ) ),
214
          Setting.of( title( KEY_IMAGE_SERVER ),
215
                      stringProperty( KEY_IMAGE_SERVER ) )
216
        )
217
      ),
218
      Category.of(
219
        get( KEY_DEF ),
220
        Group.of(
221
          get( KEY_DEF_PATH ),
222
          Setting.of( label( KEY_DEF_PATH ) ),
223
          Setting.of( title( KEY_DEF_PATH ),
224
                      fileProperty( KEY_DEF_PATH ), false )
225
        ),
226
        Group.of(
227
          get( KEY_DEF_DELIM_BEGAN ),
228
          Setting.of( label( KEY_DEF_DELIM_BEGAN ) ),
229
          Setting.of( title( KEY_DEF_DELIM_BEGAN ),
230
                      stringProperty( KEY_DEF_DELIM_BEGAN ) )
231
        ),
232
        Group.of(
233
          get( KEY_DEF_DELIM_ENDED ),
234
          Setting.of( label( KEY_DEF_DELIM_ENDED ) ),
235
          Setting.of( title( KEY_DEF_DELIM_ENDED ),
236
                      stringProperty( KEY_DEF_DELIM_ENDED ) )
237
        )
238
      ),
239
      Category.of(
240
        get( KEY_UI_FONT ),
241
        Group.of(
242
          get( KEY_UI_FONT_EDITOR ),
243
          Setting.of( label( KEY_UI_FONT_EDITOR_NAME ) ),
244
          Setting.of( title( KEY_UI_FONT_EDITOR_NAME ),
245
                      createFontNameField(
246
                        stringProperty( KEY_UI_FONT_EDITOR_NAME ),
247
                        doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) ),
248
                      stringProperty( KEY_UI_FONT_EDITOR_NAME ) ),
249
          Setting.of( label( KEY_UI_FONT_EDITOR_SIZE ) ),
250
          Setting.of( title( KEY_UI_FONT_EDITOR_SIZE ),
251
                      doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) )
252
        ),
253
        Group.of(
254
          get( KEY_UI_FONT_PREVIEW ),
255
          Setting.of( label( KEY_UI_FONT_PREVIEW_NAME ) ),
256
          Setting.of( title( KEY_UI_FONT_PREVIEW_NAME ),
257
                      createFontNameField(
258
                        stringProperty( KEY_UI_FONT_PREVIEW_NAME ),
259
                        doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
260
                      stringProperty( KEY_UI_FONT_PREVIEW_NAME ) ),
261
          Setting.of( label( KEY_UI_FONT_PREVIEW_SIZE ) ),
262
          Setting.of( title( KEY_UI_FONT_PREVIEW_SIZE ),
263
                      doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
264
          Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
265
          Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_NAME ),
266
                      createFontNameField(
267
                        stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ),
268
                        doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
269
                      stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
270
          Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
271
          Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_SIZE ),
272
                      doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) )
273
        ),
274
        Group.of(
275
          get( KEY_UI_FONT_MATH ),
276
          Setting.of( title( KEY_UI_FONT_MATH_SIZE ),
277
                      doubleProperty( KEY_UI_FONT_MATH_SIZE ) )
278
        )
279
      ),
280
      Category.of(
281
        get( KEY_UI_SKIN ),
282
        Group.of(
283
          get( KEY_UI_SKIN_SELECTION ),
284
          Setting.of( label( KEY_UI_SKIN_SELECTION ) ),
285
          Setting.of( title( KEY_UI_SKIN_SELECTION ),
286
                      skinListProperty(),
287
                      skinProperty( KEY_UI_SKIN_SELECTION ) )
288
        ),
289
        Group.of(
290
          get( KEY_UI_SKIN_CUSTOM ),
291
          Setting.of( label( KEY_UI_SKIN_CUSTOM ) ),
292
          Setting.of( title( KEY_UI_SKIN_CUSTOM ),
293
                      fileProperty( KEY_UI_SKIN_CUSTOM ), false )
294
        )
295
      ),
296
      Category.of(
297
        get( KEY_UI_PREVIEW ),
298
        Group.of(
299
          get( KEY_UI_PREVIEW_STYLESHEET ),
300
          Setting.of( label( KEY_UI_PREVIEW_STYLESHEET ) ),
301
          Setting.of( title( KEY_UI_PREVIEW_STYLESHEET ),
302
                      fileProperty( KEY_UI_PREVIEW_STYLESHEET ), false )
303
        )
304
      ),
305
      Category.of(
306
        get( KEY_LANGUAGE ),
307
        Group.of(
308
          get( KEY_LANGUAGE_LOCALE ),
309
          Setting.of( label( KEY_LANGUAGE_LOCALE ) ),
310
          Setting.of( title( KEY_LANGUAGE_LOCALE ),
311
                      localeListProperty(),
312
                      localeProperty( KEY_LANGUAGE_LOCALE ) )
313
        )
314
      )
315
    };
316
  }
317
318
  @SuppressWarnings( "unchecked" )
319
  private Setting<StringField, StringProperty> createMultilineSetting(
320
    final String description, final Key property ) {
321
    final Setting<StringField, StringProperty> setting =
322
      Setting.of( description, stringProperty( property ) );
323
    final var field = setting.getElement();
324
    field.multiline( true );
325
326
    return setting;
327
  }
328
329
  /**
330
   * Map ENTER and ESCAPE keys to OK and CANCEL buttons, respectively.
331
   */
332
  private void initKeyEventHandler( final PreferencesFx preferences ) {
333
    final var view = preferences.getView();
334
    final var nodes = view.getChildrenUnmodifiable();
335
    final var master = (MasterDetailPane) nodes.get( 0 );
336
    final var detail = (NavigationView) master.getDetailNode();
337
    final var pane = (DialogPane) view.getParent();
338
339
    detail.setOnKeyReleased( key -> {
340
      switch( key.getCode() ) {
341
        case ENTER -> ((Button) pane.lookupButton( OK )).fire();
342
        case ESCAPE -> ((Button) pane.lookupButton( CANCEL )).fire();
343
        default -> { }
344
      }
345
    } );
346
  }
347
348
  /**
349
   * Called when the user clicks the APPLY or OK buttons in the dialog.
350
   *
351
   * @param preferences Preferences widget.
352
   */
353
  private void initSaveEventHandler( final PreferencesFx preferences ) {
354
    preferences.addEventHandler(
355
      EVENT_PREFERENCES_SAVED, event -> mWorkspace.save()
356
    );
357
  }
358
359
  /**
360
   * Creates a label for the given key after interpolating its value.
361
   *
362
   * @param key The key to find in the resource bundle.
363
   * @return The value of the key as a label.
364
   */
365
  private Node label( final Key key ) {
366
    return label( key, (String[]) null );
367
  }
368
369
  private Node label( final Key key, final String... values ) {
370
    return new Label( get( key.toString() + ".desc", (Object[]) values ) );
371
  }
372
373
  private String title( final Key key ) {
374
    return get( key.toString() + ".title" );
375
  }
376
377
  /**
378
   * Screens out non-existent directories to avoid throwing an exception caused
379
   * by
380
   * <a href="https://github.com/dlsc-software-consulting-gmbh/PreferencesFX/issues/441">
381
   * PreferencesFX issue #441
382
   * </a>.
383
   *
384
   * @param key Preference to pre-screen before creating a {@link FileProperty}.
385
   * @return The preferred value or the user's home directory if the directory
386
   * does not exist.
387
   */
388
  private ObjectProperty<File> directoryProperty( final Key key ) {
389
    final var property = mWorkspace.fileProperty( key );
390
    final var file = property.get();
391
392
    if( !file.exists() ) {
393
      property.set( USER_DIRECTORY );
394
    }
395
396
    return property;
397
  }
398
399
  private ObjectProperty<File> fileProperty( final Key key ) {
400
    return mWorkspace.fileProperty( key );
401
  }
402
403
  private StringProperty stringProperty( final Key key ) {
404
    return mWorkspace.stringProperty( key );
405
  }
406
407
  private BooleanProperty booleanProperty( final Key key ) {
408
    return mWorkspace.booleanProperty( key );
409
  }
410
411
  private IntegerProperty integerProperty( final Key key ) {
412
    return mWorkspace.integerProperty( key );
413
  }
414
415
  private DoubleProperty doubleProperty( final Key key ) {
416
    return mWorkspace.doubleProperty( key );
417
  }
418
419
  private ObjectProperty<String> skinProperty( final Key key ) {
420
    return mWorkspace.skinProperty( key );
421
  }
422
423
  private ObjectProperty<String> localeProperty( final Key key ) {
424
    return mWorkspace.localeProperty( key );
425
  }
426
427
  private <K, V> ListProperty<Entry<K, V>> listEntryProperty( final Key key ) {
428
    return mWorkspace.listsProperty( key );
429
  }
430
}
1431
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> sSkins = new LinkedHashSet<>();
22
23
  static {
24
    sSkins.add( "Count Darcula" );
25
    sSkins.add( "Haunted Grey" );
26
    sSkins.add( "Modena Dark" );
27
    sSkins.add( "Monokai" );
28
    sSkins.add( SKIN_DEFAULT );
29
    sSkins.add( "Silver Cavern" );
30
    sSkins.add( "Solarized Dark" );
31
    sSkins.add( "Vampire Byte" );
32
  }
33
34
  /**
35
   * Returns the list of available skin names to change the UI fonts and
36
   * colours.
37
   *
38
   * @return A selection of skins.
39
   */
40
  public static ObservableList<String> skinListProperty() {
41
    assert !sSkins.isEmpty();
42
43
    return listProperty( sSkins );
44
  }
45
46
  /**
47
   * Returns the given skin name as a sanitized file name, which must map
48
   * to a stylesheet file bundled with the application. This does not include
49
   * the path to the stylesheet. If the given name is not known, the file
50
   * name for {@link Constants#SKIN_DEFAULT} is returned. The extension must
51
   * be added separately.
52
   *
53
   * @param skin The name to convert to a file name.
54
   * @return The given name converted lower case, spaces replaced with
55
   * underscores, without the ".css" extension appended.
56
   */
57
  public static String toFilename( final String skin ) {
58
    assert skin != null;
59
60
    return sanitize( skin ).toLowerCase().replace( ' ', '_' );
61
  }
62
63
  /**
64
   * Ensures that the given name is in the list of known skins.
65
   *
66
   * @param skin Validate this name's existence.
67
   * @return The given name, if valid, otherwise the default skin.
68
   */
69
  private static String sanitize( final String skin ) {
70
    assert skin != null;
71
72
    return sSkins.contains( skin ) ? skin : SKIN_DEFAULT;
73
  }
74
75
  public SkinProperty( final String skin ) {
76
    super( skin );
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, asBooleanProperty( true ) )
141
    //@formatter:on
142
  );
143
144
  /**
145
   * Sets of configuration values, all the same type (e.g., file names),
146
   * where the key name doesn't change per set.
147
   */
148
  private final Map<Key, SetProperty<?>> mSets = Map.ofEntries(
149
    entry(
150
      KEY_UI_RECENT_OPEN_PATH,
151
      createSetProperty( new HashSet<String>() )
152
    )
153
  );
154
155
  /**
156
   * Lists of configuration values, such as key-value pairs where both the
157
   * key name and the value must be preserved per list.
158
   */
159
  private final Map<Key, ListProperty<?>> mLists = Map.ofEntries(
160
    entry(
161
      KEY_DOC_META,
162
      createListProperty( new LinkedList<Entry<String, String>>() )
163
    )
164
  );
165
166
  /**
167
   * Helps instantiate {@link Property} instances for XML configuration items.
168
   */
169
  private static final Map<Class<?>, Function<String, Object>> UNMARSHALL =
170
    Map.of(
171
      LocaleProperty.class, LocaleProperty::parseLocale,
172
      SimpleBooleanProperty.class, Boolean::parseBoolean,
173
      SimpleIntegerProperty.class, Integer::parseInt,
174
      SimpleDoubleProperty.class, Double::parseDouble,
175
      SimpleFloatProperty.class, Float::parseFloat,
176
      SimpleStringProperty.class, String::new,
177
      SimpleObjectProperty.class, String::new,
178
      SkinProperty.class, String::new,
179
      FileProperty.class, File::new
180
    );
181
182
  /**
183
   * The asymmetry with respect to {@link #UNMARSHALL} is because most objects
184
   * can simply call {@link Object#toString()} to convert the value to a string.
185
   */
186
  private static final Map<Class<?>, Function<String, Object>> MARSHALL =
187
    Map.of(
188
      LocaleProperty.class, LocaleProperty::toLanguageTag
189
    );
190
191
  /**
192
   * Converts the given {@link Property} value to a string.
193
   *
194
   * @param property The {@link Property} to convert.
195
   * @return A string representation of the given property, or the empty
196
   * string if no conversion was possible.
197
   */
198
  private static String marshall( final Property<?> property ) {
199
    final var v = property.getValue();
200
201
    return v == null
202
      ? ""
203
      : MARSHALL
204
      .getOrDefault( property.getClass(), __ -> property.getValue() )
205
      .apply( v.toString() )
206
      .toString();
207
  }
208
209
  private static Object unmarshall(
210
    final Property<?> property, final Object configValue ) {
211
    final var v = configValue.toString();
212
213
    return UNMARSHALL
214
      .getOrDefault( property.getClass(), value -> property.getValue() )
215
      .apply( v );
216
  }
217
218
  /**
219
   * Creates an instance of {@link ObservableList} that is based on a
220
   * modifiable observable array list for the given items.
221
   *
222
   * @param items The items to wrap in an observable list.
223
   * @param <E>   The type of items to add to the list.
224
   * @return An observable property that can have its contents modified.
225
   */
226
  public static <E> ObservableList<E> listProperty( final Set<E> items ) {
227
    return new SimpleListProperty<>( observableArrayList( items ) );
228
  }
229
230
  private static <E> SetProperty<E> createSetProperty( final Set<E> set ) {
231
    return new SimpleSetProperty<>( observableSet( set ) );
232
  }
233
234
  private static <E> ListProperty<E> createListProperty( final List<E> list ) {
235
    return new SimpleListProperty<>( observableArrayList( list ) );
236
  }
237
238
  private static StringProperty asStringProperty( final String value ) {
239
    return new SimpleStringProperty( value );
240
  }
241
242
  private static BooleanProperty asBooleanProperty() {
243
    return new SimpleBooleanProperty();
244
  }
245
246
  /**
247
   * @param value Default value.
248
   */
249
  @SuppressWarnings( "SameParameterValue" )
250
  private static BooleanProperty asBooleanProperty( final boolean value ) {
251
    return new SimpleBooleanProperty( value );
252
  }
253
254
  /**
255
   * @param value Default value.
256
   */
257
  @SuppressWarnings( "SameParameterValue" )
258
  private static IntegerProperty asIntegerProperty( final int value ) {
259
    return new SimpleIntegerProperty( value );
260
  }
261
262
  /**
263
   * @param value Default value.
264
   */
265
  private static DoubleProperty asDoubleProperty( final double value ) {
266
    return new SimpleDoubleProperty( value );
267
  }
268
269
  /**
270
   * @param value Default value.
271
   */
272
  private static FileProperty asFileProperty( final File value ) {
273
    return new FileProperty( value );
274
  }
275
276
  /**
277
   * @param value Default value.
278
   */
279
  @SuppressWarnings( "SameParameterValue" )
280
  private static LocaleProperty asLocaleProperty( final Locale value ) {
281
    return new LocaleProperty( value );
282
  }
283
284
  /**
285
   * @param value Default value.
286
   */
287
  @SuppressWarnings( "SameParameterValue" )
288
  private static SkinProperty asSkinProperty( final String value ) {
289
    return new SkinProperty( value );
290
  }
291
292
  /**
293
   * Creates a new {@link Workspace} that will attempt to load the users'
294
   * preferences. If the configuration file cannot be loaded, the workspace
295
   * settings returns default values.
296
   */
297
  public Workspace() {
298
    load();
299
  }
300
301
  /**
302
   * Attempts to load the app's configuration file.
303
   */
304
  private void load() {
305
    final var store = createXmlStore();
306
    store.load( FILE_PREFERENCES );
307
308
    mValues.keySet().forEach( key -> {
309
      try {
310
        final var storeValue = store.getValue( key );
311
        final var property = valuesProperty( key );
312
        final var unmarshalled = unmarshall( property, storeValue );
313
314
        property.setValue( unmarshalled );
315
      } catch( final NoSuchElementException ex ) {
316
        // When no configuration (item), use the default value.
317
        clue( ex );
318
      }
319
    } );
320
321
    mSets.keySet().forEach( key -> {
322
      final var set = store.getSet( key );
323
      final SetProperty<String> property = setsProperty( key );
324
325
      property.setValue( observableSet( set ) );
326
    } );
327
328
    mLists.keySet().forEach( key -> {
329
      final var map = store.getMap( key );
330
      final ListProperty<Entry<String, String>> property = listsProperty( key );
331
      final var list = map
332
        .entrySet()
333
        .stream()
334
        .toList();
335
336
      property.setValue( observableArrayList( list ) );
337
    } );
338
339
    WorkspaceLoadedEvent.fire( this );
340
  }
341
342
  /**
343
   * Saves the current workspace.
344
   */
345
  public void save() {
346
    final var store = createXmlStore();
347
348
    try {
349
      // Update the string values to include the application version.
350
      valuesProperty( KEY_META_VERSION ).setValue( getVersion() );
351
352
      mValues.forEach( ( k, v ) -> store.setValue( k, marshall( v ) ) );
353
      mSets.forEach( store::setSet );
354
      mLists.forEach( store::setMap );
355
356
      store.save( FILE_PREFERENCES );
357
    } catch( final Exception ex ) {
358
      clue( ex );
359
    }
360
  }
361
362
  /**
363
   * Returns a value that represents a setting in the application that the user
364
   * may configure, either directly or indirectly.
365
   *
366
   * @param key The reference to the users' preference stored in deference
367
   *            of app reëntrance.
368
   * @return An observable property to be persisted.
369
   */
370
  @SuppressWarnings( "unchecked" )
371
  public <T, U extends Property<T>> U valuesProperty( final Key key ) {
372
    assert key != null;
373
    return (U) mValues.get( key );
374
  }
375
376
  /**
377
   * Returns a set of values that represent a setting in the application that
378
   * the user may configure, either directly or indirectly. The property
379
   * returned is backed by a {@link Set}.
380
   *
381
   * @param key The {@link Key} associated with a preference value.
382
   * @return An observable property to be persisted.
383
   */
384
  @SuppressWarnings( "unchecked" )
385
  public <T> SetProperty<T> setsProperty( final Key key ) {
386
    assert key != null;
387
    return (SetProperty<T>) mSets.get( key );
388
  }
389
390
  /**
391
   * Returns a list of values that represent a setting in the application that
392
   * the user may configure, either directly or indirectly. The property
393
   * returned is backed by a mutable {@link List}.
394
   *
395
   * @param key The {@link Key} associated with a preference value.
396
   * @return An observable property to be persisted.
397
   */
398
  @SuppressWarnings( "unchecked" )
399
  public <K, V> ListProperty<Entry<K, V>> listsProperty( final Key key ) {
400
    assert key != null;
401
    return (ListProperty<Entry<K, V>>) mLists.get( key );
402
  }
403
404
  /**
405
   * Returns the {@link String} {@link Property} associated with the given
406
   * {@link Key} from the internal list of preference values. The caller
407
   * must be sure that the given {@link Key} is associated with a {@link File}
408
   * {@link Property}.
409
   *
410
   * @param key The {@link Key} associated with a preference value.
411
   * @return The value associated with the given {@link Key}.
412
   */
413
  public StringProperty stringProperty( final Key key ) {
414
    assert key != null;
415
    return valuesProperty( key );
416
  }
417
418
  /**
419
   * Returns the {@link Boolean} {@link Property} associated with the given
420
   * {@link Key} from the internal list of preference values. The caller
421
   * must be sure that the given {@link Key} is associated with a {@link File}
422
   * {@link Property}.
423
   *
424
   * @param key The {@link Key} associated with a preference value.
425
   * @return The value associated with the given {@link Key}.
426
   */
427
  public BooleanProperty booleanProperty( final Key key ) {
428
    assert key != null;
429
    return valuesProperty( key );
430
  }
431
432
  /**
433
   * Returns the {@link Integer} {@link Property} associated with the given
434
   * {@link Key} from the internal list of preference values. The caller
435
   * must be sure that the given {@link Key} is associated with a {@link File}
436
   * {@link Property}.
437
   *
438
   * @param key The {@link Key} associated with a preference value.
439
   * @return The value associated with the given {@link Key}.
440
   */
441
  public IntegerProperty integerProperty( final Key key ) {
442
    assert key != null;
443
    return valuesProperty( key );
444
  }
445
446
  /**
447
   * Returns the {@link Double} {@link Property} associated with the given
448
   * {@link Key} from the internal list of preference values. The caller
449
   * must be sure that the given {@link Key} is associated with a {@link File}
450
   * {@link Property}.
451
   *
452
   * @param key The {@link Key} associated with a preference value.
453
   * @return The value associated with the given {@link Key}.
454
   */
455
  public DoubleProperty doubleProperty( final Key key ) {
456
    assert key != null;
457
    return valuesProperty( key );
458
  }
459
460
  /**
461
   * Returns the {@link File} {@link Property} associated with the given
462
   * {@link Key} from the internal list of preference values. The caller
463
   * must be sure that the given {@link Key} is associated with a {@link File}
464
   * {@link Property}.
465
   *
466
   * @param key The {@link Key} associated with a preference value.
467
   * @return The value associated with the given {@link Key}.
468
   */
469
  public ObjectProperty<File> fileProperty( final Key key ) {
470
    assert key != null;
471
    return valuesProperty( key );
472
  }
473
474
  /**
475
   * Returns the {@link Locale} {@link Property} associated with the given
476
   * {@link Key} from the internal list of preference values. The caller
477
   * must be sure that the given {@link Key} is associated with a {@link File}
478
   * {@link Property}.
479
   *
480
   * @param key The {@link Key} associated with a preference value.
481
   * @return The value associated with the given {@link Key}.
482
   */
483
  public LocaleProperty localeProperty( final Key key ) {
484
    assert key != null;
485
    return valuesProperty( key );
486
  }
487
488
  public ObjectProperty<String> skinProperty( final Key key ) {
489
    assert key != null;
490
    return valuesProperty( key );
491
  }
492
493
  public String getString( final Key key ) {
494
    assert key != null;
495
    return stringProperty( key ).get();
496
  }
497
498
  /**
499
   * Returns the {@link Boolean} preference value associated with the given
500
   * {@link Key}. The caller must be sure that the given {@link Key} is
501
   * associated with a value that matches the return type.
502
   *
503
   * @param key The {@link Key} associated with a preference value.
504
   * @return The value associated with the given {@link Key}.
505
   */
506
  public boolean getBoolean( final Key key ) {
507
    assert key != null;
508
    return booleanProperty( key ).get();
509
  }
510
511
  /**
512
   * Returns the {@link Integer} preference value associated with the given
513
   * {@link Key}. The caller must be sure that the given {@link Key} is
514
   * associated with a value that matches the return type.
515
   *
516
   * @param key The {@link Key} associated with a preference value.
517
   * @return The value associated with the given {@link Key}.
518
   */
519
  @SuppressWarnings( "unused" )
520
  public int getInteger( final Key key ) {
521
    assert key != null;
522
    return integerProperty( key ).get();
523
  }
524
525
  /**
526
   * Returns the {@link Double} preference value associated with the given
527
   * {@link Key}. The caller must be sure that the given {@link Key} is
528
   * associated with a value that matches the return type.
529
   *
530
   * @param key The {@link Key} associated with a preference value.
531
   * @return The value associated with the given {@link Key}.
532
   */
533
  public double getDouble( final Key key ) {
534
    assert key != null;
535
    return doubleProperty( key ).get();
536
  }
537
538
  /**
539
   * Returns the {@link File} preference value associated with the given
540
   * {@link Key}. The caller must be sure that the given {@link Key} is
541
   * associated with a value that matches the return type.
542
   *
543
   * @param key The {@link Key} associated with a preference value.
544
   * @return The value associated with the given {@link Key}.
545
   */
546
  public File getFile( final Key key ) {
547
    assert key != null;
548
    return fileProperty( key ).get();
549
  }
550
551
  /**
552
   * Returns the language locale setting for the
553
   * {@link AppKeys#KEY_LANGUAGE_LOCALE} key.
554
   *
555
   * @return The user's current locale setting.
556
   */
557
  public Locale getLocale() {
558
    return localeProperty( KEY_LANGUAGE_LOCALE ).toLocale();
559
  }
560
561
  @SuppressWarnings( "unchecked" )
562
  public <K, V> Map<K, V> getMetadata() {
563
    final var metadata = listsProperty( KEY_DOC_META );
564
    final HashMap<K, V> map;
565
566
    if( metadata != null ) {
567
      map = new HashMap<>( metadata.size() );
568
569
      metadata.forEach(
570
        entry -> map.put( (K) entry.getKey(), (V) entry.getValue() )
571
      );
572
    }
573
    else {
574
      map = new HashMap<>();
575
    }
576
577
    return map;
578
  }
579
580
  public Path getThemesPath() {
581
    final var dir = getFile( KEY_TYPESET_CONTEXT_THEMES_PATH );
582
    final var name = getString( KEY_TYPESET_CONTEXT_THEME_SELECTION );
583
584
    return Path.of( dir.toString(), name );
585
  }
586
587
  /**
588
   * Delegates to {@link #listen(Key, ReadOnlyProperty, BooleanSupplier)},
589
   * providing a value of {@code true} for the {@link BooleanSupplier} to
590
   * indicate the property changes always take effect.
591
   *
592
   * @param key      The value to bind to the internal key property.
593
   * @param property The external property value that sets the internal value.
594
   */
595
  public <T> void listen( final Key key, final ReadOnlyProperty<T> property ) {
596
    assert key != null;
597
    assert property != null;
598
599
    listen( key, property, () -> true );
600
  }
601
602
  /**
603
   * Binds a read-only property to a value in the preferences. This allows
604
   * user interface properties to change and the preferences will be
605
   * synchronized automatically.
606
   * <p>
607
   * This calls {@link Platform#runLater(Runnable)} to ensure that all pending
608
   * application window states are finished before assessing whether property
609
   * changes should be applied. Without this, exiting the application while the
610
   * window is maximized would persist the window's maximum dimensions,
611
   * preventing restoration to its prior, non-maximum size.
612
   *
613
   * @param key      The value to bind to the internal key property.
614
   * @param property The external property value that sets the internal value.
615
   * @param enabled  Indicates whether property changes should be applied.
616
   */
617
  public <T> void listen(
618
    final Key key,
619
    final ReadOnlyProperty<T> property,
620
    final BooleanSupplier enabled ) {
621
    assert key != null;
622
    assert property != null;
623
    assert enabled != null;
624
625
    property.addListener(
626
      ( c, o, n ) -> runLater( () -> {
627
        if( enabled.getAsBoolean() ) {
628
          valuesProperty( key ).setValue( n );
629
        }
630
      } )
631
    );
632
  }
633
634
  /**
635
   * Creates a lightweight persistence mechanism for user preferences.
636
   *
637
   * @return The {@link XmlStore} that helps with persisting application state.
638
   */
639
  private XmlStore createXmlStore() {
640
    // Root-level configuration item is the application name.
641
    return new XmlStore( APP_TITLE_LOWERCASE );
642
  }
643
}
1644
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( 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 += margin.top();
185
      x += 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
        e -> 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
    final var doc = CONVERTER.fromJsoup( jsoupDoc );
161
    final var uri = getBaseUri();
162
163
    doc.setDocumentURI( uri );
164
    invokeLater( () -> mPreview.render( doc, uri ) );
165
    DocumentChangedEvent.fire( html );
166
  }
167
168
  /**
169
   * Clears the caches then re-renders the content.
170
   */
171
  public void refresh() {
172
    mPreview.clearCache();
173
    rerender();
174
  }
175
176
  /**
177
   * Recomputes the HTML head then renders the document.
178
   */
179
  private void rerender() {
180
    mHead = generateHead();
181
    render( mDocument.toString() );
182
  }
183
184
  /**
185
   * Attaches the HTML head prefix and HTML tail suffix to the given HTML
186
   * string.
187
   *
188
   * @param html The HTML to adorn with opening and closing tags.
189
   * @return A complete HTML document, ready for rendering.
190
   */
191
  private String decorate( final String html ) {
192
    mDocument.setLength( 0 );
193
    mDocument.append( html );
194
195
    // Head and tail must be separate from document due to re-rendering.
196
    return mHead + mDocument + HTML_TAIL;
197
  }
198
199
  /**
200
   * Called when settings are changed that affect the HTML document preamble.
201
   * This is a minor performance optimization to avoid generating the head
202
   * each time that the document itself changes.
203
   *
204
   * @return A new doctype and HTML {@code head} element.
205
   */
206
  private String generateHead() {
207
    final var locale = getLocale();
208
    final var base = getBaseUri();
209
    final var custom = getCustomStylesheetUrl();
210
211
    // Point sizes are converted to pixels because of a rendering bug.
212
    return format(
213
      HTML_HEAD,
214
      locale.getLanguage(),
215
      toStylesheetString( HTML_STYLE_PREVIEW ),
216
      toStylesheetString( toUrl( locale ) ),
217
      toStylesheetString( custom ),
218
      getFontFamily(),
219
      toPixels( getFontSize() ),
220
      base.isBlank() ? "" : format( HTML_BASE, base )
221
    );
222
  }
223
224
  /**
225
   * Clears the preview pane by rendering an empty string.
226
   */
227
  public void clear() {
228
    render( "" );
229
  }
230
231
  /**
232
   * Sets the base URI to the containing directory the file being edited.
233
   *
234
   * @param path The path to the file being edited.
235
   */
236
  public void setBaseUri( final Path path ) {
237
    final var parent = path.getParent();
238
    mBaseUriPath = parent == null ? "" : parent.toUri().toString();
239
  }
240
241
  /**
242
   * Scrolls to the closest element matching the given identifier without
243
   * waiting for the document to be ready.
244
   *
245
   * @param id Scroll the preview pane to this unique paragraph identifier.
246
   */
247
  public void scrollTo( final String id ) {
248
    if( !mScrollLocked ) {
249
      mPreview.scrollTo( id, mScrollPane );
250
      mScrollPane.repaint();
251
    }
252
  }
253
254
  private String getBaseUri() {
255
    return mBaseUriPath;
256
  }
257
258
  private JScrollPane getScrollPane() {
259
    return mScrollPane;
260
  }
261
262
  public JScrollBar getVerticalScrollBar() {
263
    return getScrollPane().getVerticalScrollBar();
264
  }
265
266
  /**
267
   * Returns the ISO 639 alpha-2 or alpha-3 language code followed by a hyphen
268
   * followed by the ISO 15924 alpha-4 script code, followed by an ISO 3166
269
   * alpha-2 country code or UN M.49 numeric-3 area code. For example, this
270
   * could return {@code en-Latn-CA} for Canadian English written in the Latin
271
   * character set.
272
   *
273
   * @return Unique identifier for language and country.
274
   */
275
  private static URL toUrl( final Locale locale ) {
276
    return toUrl(
277
      String.format(
278
        sSettings.getSetting( STYLESHEET_PREVIEW_LOCALE, "" ),
279
        locale.getLanguage(),
280
        locale.getScript(),
281
        locale.getCountry()
282
      )
283
    );
284
  }
285
286
  private static URL toUrl( final String path ) {
287
    return HtmlPreview.class.getResource( path );
288
  }
289
290
  private Locale getLocale() {
291
    return localeProperty().toLocale();
292
  }
293
294
  private LocaleProperty localeProperty() {
295
    return mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
296
  }
297
298
  private String getFontFamily() {
299
    return fontFamilyProperty().get();
300
  }
301
302
  private StringProperty fontFamilyProperty() {
303
    return mWorkspace.stringProperty( KEY_UI_FONT_PREVIEW_NAME );
304
  }
305
306
  private double getFontSize() {
307
    return fontSizeProperty().get();
308
  }
309
310
  /**
311
   * Returns the font size in points.
312
   *
313
   * @return The user-defined font size (in pt).
314
   */
315
  private DoubleProperty fontSizeProperty() {
316
    return mWorkspace.doubleProperty( KEY_UI_FONT_PREVIEW_SIZE );
317
  }
318
319
  private String getLockText( final boolean locked ) {
320
    return Character.toString( (locked ? LOCK : UNLOCK_ALT).getChar() );
321
  }
322
323
  private URL getCustomStylesheetUrl() {
324
    try {
325
      return mWorkspace.getFile( KEY_UI_PREVIEW_STYLESHEET ).toURI().toURL();
326
    } catch( final Exception ex ) {
327
      clue( ex );
328
      return null;
329
    }
330
  }
331
332
  /**
333
   * Maps keyboard events to scrollbar commands so that users may control
334
   * the {@link HtmlPreview} panel using the keyboard.
335
   *
336
   * @param map The map to update with keyboard events.
337
   */
338
  private void addKeyboardEvents( final InputMap map ) {
339
    map.put( getKeyStroke( VK_DOWN, 0 ), "positiveUnitIncrement" );
340
    map.put( getKeyStroke( VK_UP, 0 ), "negativeUnitIncrement" );
341
    map.put( getKeyStroke( VK_PAGE_DOWN, 0 ), "positiveBlockIncrement" );
342
    map.put( getKeyStroke( VK_PAGE_UP, 0 ), "negativeBlockIncrement" );
343
    map.put( getKeyStroke( VK_HOME, 0 ), "minScroll" );
344
    map.put( getKeyStroke( VK_END, 0 ), "maxScroll" );
345
  }
346
347
  @Override
348
  public void componentResized( final ComponentEvent e ) {
349
    if( mWorkspace.getBoolean( KEY_IMAGE_RESIZE ) ) {
350
      mPreview.clearCache();
351
    }
352
353
    // Force update on the Swing EDT, otherwise the scrollbar and content
354
    // will not be updated correctly on some platforms.
355
    invokeLater( () -> getContent().repaint() );
356
  }
357
358
  @Override
359
  public void componentMoved( final ComponentEvent e ) { }
360
361
  @Override
362
  public void componentShown( final ComponentEvent e ) { }
363
364
  @Override
365
  public void componentHidden( final ComponentEvent e ) { }
366
367
  private static String toStylesheetString( final URL url ) {
368
    return url == null ? "" : format( HTML_STYLESHEET, url );
369
  }
370
}
1371
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.text.ParseException;
25
26
import static com.keenwrite.events.StatusEvent.clue;
27
import static com.keenwrite.io.downloads.DownloadManager.open;
28
import static com.keenwrite.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER;
29
import static com.keenwrite.preview.SvgRasterizer.rasterize;
30
import static com.keenwrite.processors.markdown.extensions.tex.TexNode.HTML_TEX;
31
import static com.keenwrite.util.ProtocolScheme.getProtocol;
32
33
/**
34
 * Responsible for running {@link SvgRasterizer} on SVG images detected within
35
 * a document to transform them into rasterized versions. This will fall back
36
 * to loading rasterized images from a file if not detected as SVG.
37
 */
38
public final class ImageReplacedElementFactory extends ReplacedElementAdapter {
39
40
  public static final String HTML_IMAGE = "img";
41
  public static final String HTML_IMAGE_SRC = "src";
42
43
  private static final ImageReplacedElement BROKEN_IMAGE =
44
    createElement( BROKEN_IMAGE_PLACEHOLDER );
45
46
  @Override
47
  public ReplacedElement createReplacedElement(
48
    final LayoutContext c,
49
    final BlockBox box,
50
    final UserAgentCallback uac,
51
    final int cssWidth,
52
    final int cssHeight ) {
53
    final var e = box.getElement();
54
55
    try {
56
      final BufferedImage raster =
57
        switch( e.getNodeName() ) {
58
          case HTML_IMAGE -> createHtmlImage( box, e, uac );
59
          case HTML_TEX -> createTexImage( e );
60
          default -> null;
61
        };
62
63
      return createElement( raster );
64
    } catch( final Exception ex ) {
65
      clue( "Main.status.image.request.error.rasterize", ex );
66
    }
67
68
    return BROKEN_IMAGE;
69
  }
70
71
  /**
72
   * Convert an HTML element to a raster graphic.
73
   */
74
  private static BufferedImage createHtmlImage(
75
    final BlockBox box,
76
    final Element e,
77
    final UserAgentCallback uac )
78
    throws TranscoderException, URISyntaxException, IOException {
79
    final var source = e.getAttribute( HTML_IMAGE_SRC );
80
    final var mediaType = MediaType.fromFilename( source );
81
82
    URI uri = null;
83
    BufferedImage raster = null;
84
85
    final var w = box.getContentWidth();
86
87
    if( getProtocol( source ).isRemote() ) {
88
      try( final var response = open( source );
89
           final var stream = response.getInputStream() ) {
90
91
        // Rasterize SVG from URL resource.
92
        raster = response.isSvg()
93
          ? rasterize( stream, w )
94
          : ImageIO.read( stream );
95
96
        clue( "Main.status.image.request.fetch", source );
97
      }
98
    }
99
    else if( mediaType.isSvg() ) {
100
      uri = resolve( source, uac, e );
101
    }
102
103
    if( uri != null && w > 0 ) {
104
      raster = rasterize( uri, w );
105
    }
106
107
    // Not an SVG, attempt to read a local rasterized image.
108
    if( raster == null && mediaType.isImage() ) {
109
      uri = resolve( source, uac, e );
110
      final var path = Path.of( uri.getPath() );
111
112
      try( final var stream = Files.newInputStream( path ) ) {
113
        raster = ImageIO.read( stream );
114
      }
115
    }
116
117
    return raster;
118
  }
119
120
  private static URI resolve(
121
    final String source,
122
    final UserAgentCallback uac,
123
    final Element e )
124
    throws URISyntaxException {
125
    // Attempt to rasterize based on file name.
126
    final var baseUri = new URI( uac.getBaseURL() );
127
    final var path = baseUri.resolve( source ).normalize();
128
129
    if( path.isAbsolute() ) {
130
      return path;
131
    }
132
    else {
133
      final var base = new URI( e.getBaseURI() ).getPath();
134
      return Path.of( base, source ).toUri();
135
    }
136
  }
137
138
  /**
139
   * Convert the TeX element to a raster graphic.
140
   */
141
  private BufferedImage createTexImage( final Element e )
142
    throws TranscoderException, ParseException {
143
    return rasterize( MathRenderer.toString( e.getTextContent() ) );
144
  }
145
146
  private static ImageReplacedElement createElement( final BufferedImage bi ) {
147
    return bi == null
148
      ? BROKEN_IMAGE
149
      : new ImageReplacedElement( bi, bi.getWidth(), bi.getHeight() );
150
  }
151
}
1152
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.KEY_HEIGHT;
33
import static io.sf.carte.echosvg.transcoder.SVGAbstractTranscoder.KEY_WIDTH;
34
import static io.sf.carte.echosvg.transcoder.TranscodingHints.Key;
35
import static io.sf.carte.echosvg.transcoder.image.ImageTranscoder.KEY_PIXEL_UNIT_TO_MILLIMETER;
36
import static io.sf.carte.echosvg.util.SVGConstants.SVG_HEIGHT_ATTRIBUTE;
37
import static io.sf.carte.echosvg.util.SVGConstants.SVG_WIDTH_ATTRIBUTE;
38
import static java.awt.image.BufferedImage.TYPE_INT_RGB;
39
import static java.text.NumberFormat.getIntegerInstance;
40
41
/**
42
 * Responsible for converting SVG images into rasterized PNG images.
43
 */
44
public final class SvgRasterizer {
45
46
  /**
47
   * Prevent rudely barfing stack traces to the console.
48
   */
49
  private static final class SvgErrorHandler implements ErrorHandler {
50
    @Override
51
    public void error( final TranscoderException ex ) {
52
      clue( ex );
53
    }
54
55
    @Override
56
    public void fatalError( final TranscoderException ex ) {
57
      clue( ex );
58
    }
59
60
    @Override
61
    public void warning( final TranscoderException ex ) {
62
      clue( ex );
63
    }
64
  }
65
66
  private static final UserAgent USER_AGENT = new UserAgentAdapter();
67
  private static final BridgeContext BRIDGE_CONTEXT = new BridgeContext(
68
    USER_AGENT, new DocumentLoader( USER_AGENT )
69
  );
70
  private static final ErrorHandler sErrorHandler = new SvgErrorHandler();
71
72
  private static final SAXSVGDocumentFactory FACTORY_DOM =
73
    new SAXSVGDocumentFactory();
74
75
  private static final NumberFormat INT_FORMAT = getIntegerInstance();
76
77
  public static final BufferedImage BROKEN_IMAGE_PLACEHOLDER;
78
79
  /**
80
   * A FontAwesome camera icon, cleft asunder.
81
   */
82
  public static final String BROKEN_IMAGE_SVG =
83
    "<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www" +
84
      ".w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c" +
85
      ".332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path " +
86
      "d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531" +
87
      ".03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2" +
88
      ".511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-" +
89
      ".367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1" +
90
      ".699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 " +
91
      "2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094" +
92
      ".0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5" +
93
      ".0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-" +
94
      ".195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4" +
95
      ".191406-3.652344.207031-.015625.414063-.015625.617187-.007812l" +
96
      ".933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2" +
97
      ".820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3" +
98
      ".429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3" +
99
      ".429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 " +
100
      ".140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281" +
101
      ".808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2" +
102
      ".472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4" +
103
      ".285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1" +
104
      ".9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-" +
105
      ".667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1" +
106
      ".253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 " +
107
      "0'/></g></svg>";
108
109
  static {
110
    // The width and height cannot be embedded in the SVG above because the
111
    // path element values are relative to the viewBox dimensions.
112
    final int w = 75;
113
    final int h = 75;
114
    BufferedImage image;
115
116
    try {
117
      image = rasterizeImage( BROKEN_IMAGE_SVG, w );
118
    } catch( final Exception ex ) {
119
      image = new BufferedImage( w, h, TYPE_INT_RGB );
120
      final var graphics = (Graphics2D) image.getGraphics();
121
      graphics.setRenderingHints( RENDERING_HINTS );
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( w / 4 + (int) (w / 4 / Math.PI),
130
                         h / 4 + (int) (w / 4 / Math.PI),
131
                         w / 2 + w / 4 - (int) (w / 4 / Math.PI),
132
                         h / 2 + h / 4 - (int) (w / 4 / Math.PI) );
133
    }
134
135
    BROKEN_IMAGE_PLACEHOLDER = image;
136
  }
137
138
  /**
139
   * Responsible for creating a new {@link ImageRenderer} implementation that
140
   * can render a DOM as an SVG image.
141
   */
142
  private static class BufferedImageTranscoder extends ImageTranscoder {
143
    private BufferedImage mImage;
144
145
    /**
146
     * Prevent barfing a stack trace when the transcoder encounters problems
147
     * parsing SVG contents.
148
     */
149
    @Override
150
    protected UserAgent createUserAgent() {
151
      return new SVGAbstractTranscoderUserAgent() {
152
        @Override
153
        public void displayError( final Exception ex ) {
154
          clue( ex );
155
        }
156
      };
157
    }
158
159
    @Override
160
    public BufferedImage createImage( final int w, final int h ) {
161
      return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB );
162
    }
163
164
    @Override
165
    public void writeImage(
166
      final BufferedImage image, final TranscoderOutput output ) {
167
      mImage = image;
168
    }
169
170
    public BufferedImage getImage() {
171
      return mImage;
172
    }
173
174
    @Override
175
    protected ImageRenderer createRenderer() {
176
      final ImageRenderer renderer = super.createRenderer();
177
      final RenderingHints hints = renderer.getRenderingHints();
178
      hints.putAll( RENDERING_HINTS );
179
      renderer.setRenderingHints( hints );
180
181
      return renderer;
182
    }
183
  }
184
185
  /**
186
   * Rasterizes the given SVG input stream into an image.
187
   *
188
   * @param svg The SVG data to rasterize, must be closed by caller.
189
   * @return The given input stream converted to a rasterized image.
190
   */
191
  public static BufferedImage rasterize( final String svg )
192
    throws TranscoderException, ParseException {
193
    return rasterize( toDocument( svg ) );
194
  }
195
196
  /**
197
   * Rasterizes the given SVG input stream into an image at 96 DPI.
198
   *
199
   * @param svg The SVG data to rasterize, must be closed by caller.
200
   * @return The given input stream converted to a rasterized image.
201
   */
202
  public static BufferedImage rasterize( final InputStream svg )
203
    throws TranscoderException {
204
    return rasterize( svg, 96 );
205
  }
206
207
  /**
208
   * Rasterizes the given SVG input stream into an image.
209
   *
210
   * @param svg The SVG data to rasterize, must be closed by caller.
211
   * @param dpi Resolution to use when rasterizing (default is 96 DPI).
212
   * @return The given input stream converted to a rasterized image at the
213
   * given resolution.
214
   */
215
  public static BufferedImage rasterize(
216
    final InputStream svg, final float dpi ) throws TranscoderException {
217
    return rasterize(
218
      new TranscoderInput( svg ),
219
      KEY_PIXEL_UNIT_TO_MILLIMETER,
220
      1f / dpi * 25.4f
221
    );
222
  }
223
224
  /**
225
   * Rasterizes the given document into an image.
226
   *
227
   * @param svg   The SVG {@link Document} to rasterize.
228
   * @param width The rasterized image's width (in pixels).
229
   * @return The rasterized image.
230
   */
231
  public static BufferedImage rasterize(
232
    final Document svg, final int width ) throws TranscoderException {
233
    return rasterize(
234
      new TranscoderInput( svg ),
235
      KEY_WIDTH,
236
      fit( svg.getDocumentElement(), width )
237
    );
238
  }
239
240
  /**
241
   * Rasterizes the given vector graphic file using the width dimension
242
   * specified by the document's width attribute.
243
   *
244
   * @param document The {@link Document} containing a vector graphic.
245
   * @return A rasterized image as an instance of {@link BufferedImage}, or
246
   * {@link #BROKEN_IMAGE_PLACEHOLDER} if the graphic could not be rasterized.
247
   */
248
  public static BufferedImage rasterize( final Document document )
249
    throws ParseException, TranscoderException {
250
    final var root = document.getDocumentElement();
251
    final var width = root.getAttribute( SVG_WIDTH_ATTRIBUTE );
252
253
    return rasterize( document, INT_FORMAT.parse( width ).intValue() );
254
  }
255
256
  /**
257
   * Rasterizes the vector graphic file at the given URI. If any exception
258
   * happens, a broken image icon is returned instead.
259
   *
260
   * @param path  The {@link Path} to a vector graphic file.
261
   * @param width Scale the image to the given width (px); aspect ratio is
262
   *              maintained.
263
   * @return A rasterized image as an instance of {@link BufferedImage}.
264
   */
265
  public static BufferedImage rasterize( final Path path, final int width ) {
266
    return rasterize( path.toUri(), width );
267
  }
268
269
  /**
270
   * Rasterizes the vector graphic file at the given URI. If any exception
271
   * happens, a broken image icon is returned instead.
272
   *
273
   * @param uri   The URI to a vector graphic file, which must include the
274
   *              protocol scheme (such as <code>file://</code> or
275
   *              <code>https://</code>).
276
   * @param width Scale the image to the given width (px); aspect ratio is
277
   *              maintained.
278
   * @return A rasterized image as an instance of {@link BufferedImage}.
279
   */
280
  public static BufferedImage rasterize( final String uri, final int width ) {
281
    return rasterize( new File( uri ).toURI(), width );
282
  }
283
284
  /**
285
   * Converts an SVG drawing into a rasterized image that can be drawn on
286
   * a graphics context.
287
   *
288
   * @param uri   The path to the image (can be web address).
289
   * @param width Scale the image to the given width (px); aspect ratio is
290
   *              maintained.
291
   * @return The vector graphic transcoded into a raster image format.
292
   */
293
  public static BufferedImage rasterize( final URI uri, final int width ) {
294
    try {
295
      return rasterize( FACTORY_DOM.createDocument( uri.toString() ), width );
296
    } catch( final Exception ex ) {
297
      clue( ex );
298
    }
299
300
    return BROKEN_IMAGE_PLACEHOLDER;
301
  }
302
303
  /**
304
   * Converts an SVG string into a rasterized image that can be drawn on
305
   * a graphics context. The dimensions are determined from the document.
306
   *
307
   * @param svg   The SVG xml document.
308
   * @param scale The scaling factor to apply when transcoding.
309
   * @return The vector graphic transcoded into a raster image format.
310
   */
311
  @SuppressWarnings( "unused" )
312
  public static BufferedImage rasterizeImage(
313
    final String svg, final double scale )
314
    throws ParseException, TranscoderException {
315
    final var document = toDocument( svg );
316
    final var root = document.getDocumentElement();
317
    final var width = root.getAttribute( SVG_WIDTH_ATTRIBUTE );
318
    final var height = root.getAttribute( SVG_HEIGHT_ATTRIBUTE );
319
    final var w = INT_FORMAT.parse( width ).intValue() * scale;
320
    final var h = INT_FORMAT.parse( height ).intValue() * scale;
321
322
    return rasterize( svg, w, h );
323
  }
324
325
  /**
326
   * Converts an SVG string into a rasterized image that can be drawn on
327
   * a graphics context.
328
   *
329
   * @param svg The SVG xml document.
330
   * @param w   Scale the image width to this size (aspect ratio is
331
   *            maintained).
332
   * @return The vector graphic transcoded into a raster image format.
333
   */
334
  public static BufferedImage rasterizeImage( final String svg, final int w )
335
    throws TranscoderException {
336
    return rasterize( toDocument( svg ), w );
337
  }
338
339
  /**
340
   * Given a document object model (DOM) {@link Element}, this will convert that
341
   * element to a string.
342
   *
343
   * @param root The DOM node to convert to a string.
344
   * @return The DOM node as an escaped, plain text string.
345
   */
346
  public static String toSvg( final Element root ) {
347
    try {
348
      return transform( root ).replaceAll( "xmlns=\"\" ", "" );
349
    } catch( final Exception ex ) {
350
      clue( ex );
351
    }
352
353
    return BROKEN_IMAGE_SVG;
354
  }
355
356
  /**
357
   * Converts an SVG XML string into a new {@link Document} instance.
358
   *
359
   * @param xml The XML containing SVG elements.
360
   * @return The SVG contents parsed into a {@link Document} object model.
361
   */
362
  private static Document toDocument( final String xml ) {
363
    try( final var reader = new StringReader( xml ) ) {
364
      return FACTORY_DOM.createSVGDocument(
365
        "http://www.w3.org/2000/svg", reader );
366
    } catch( final Exception ex ) {
367
      throw new IllegalArgumentException( ex );
368
    }
369
  }
370
371
  /**
372
   * Creates a rasterized image of the given source document.
373
   *
374
   * @param input     The source document to transcode.
375
   * @param hintKey   Transcoding hint key.
376
   * @param hintValue Transcoding hint value.
377
   * @return A new {@link BufferedImageTranscoder} instance with the given
378
   * transcoding hint applied.
379
   */
380
  private static BufferedImage rasterize(
381
    final TranscoderInput input, final Key hintKey, final float hintValue )
382
    throws TranscoderException {
383
    final var hints = new HashMap<Key, Object>();
384
    hints.put( hintKey, hintValue );
385
386
    return rasterize( input, hints );
387
  }
388
389
  private static BufferedImage rasterize(
390
    final String svg, final double w, final double h )
391
    throws TranscoderException {
392
    final var hints = new HashMap<Key, Object>();
393
    hints.put( KEY_WIDTH, (float) w );
394
    hints.put( KEY_HEIGHT, (float) h );
395
396
    return rasterize( new TranscoderInput( toDocument( svg ) ), hints );
397
  }
398
399
  public static BufferedImage rasterize(
400
    final TranscoderInput input,
401
    final Map<TranscodingHints.Key, Object> hints ) throws TranscoderException {
402
    final var transcoder = new BufferedImageTranscoder();
403
404
    for( final var hint : hints.entrySet() ) {
405
      transcoder.addTranscodingHint( hint.getKey(), hint.getValue() );
406
    }
407
408
    transcoder.setErrorHandler( sErrorHandler );
409
    transcoder.transcode( input, null );
410
411
    return transcoder.getImage();
412
  }
413
414
  /**
415
   * Returns either the given element's SVG document width, or the display
416
   * width, whichever is smaller.
417
   *
418
   * @param root  The SVG document's root node.
419
   * @param width The display width (e.g., rendering canvas width).
420
   * @return The lower value of the document's width or the display width.
421
   */
422
  @SuppressWarnings( "ConstantValue" )
423
  private static float fit( final Element root, final int width ) {
424
    final var w = root.getAttribute( SVG_WIDTH_ATTRIBUTE );
425
426
    return w == null || w.isBlank()
427
      ? width
428
      : fit( root, w, width );
429
  }
430
431
  /**
432
   * Returns the width in user space units (pixels?).
433
   *
434
   * @param root  The element containing the width attribute.
435
   * @param w     The element's width attribute value.
436
   * @param width The rendering canvas width.
437
   * @return Either the rendering canvas width or SVG document width,
438
   * whichever is smaller.
439
   */
440
  private static float fit(
441
    final Element root, final String w, final int width ) {
442
    final var usWidth = svgHorizontalLengthToUserSpace(
443
      w, SVG_WIDTH_ATTRIBUTE, createContext( BRIDGE_CONTEXT, root )
444
    );
445
446
    // If the image is too small, scale it to 1/4 the canvas width.
447
    return Math.min( usWidth < 5 ? width / 4.0f : usWidth, (float) width );
448
  }
449
}
1450
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/HtmlPreviewProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import com.keenwrite.preview.HtmlPreview;
5
6
/**
7
 * Responsible for notifying the {@link HtmlPreview} when the succession
8
 * chain has updated. This decouples knowledge of changes to the editor panel
9
 * from the HTML preview panel as well as any processing that takes place
10
 * before the final HTML preview is rendered. This is the last link in the
11
 * processor chain.
12
 */
13
public final class HtmlPreviewProcessor extends ExecutorProcessor<String> {
14
  /**
15
   * There is only one preview panel.
16
   */
17
  private static HtmlPreview sHtmlPreview;
18
19
  /**
20
   * Constructs the end of a processing chain.
21
   *
22
   * @param htmlPreview The pane to update with the post-processed document.
23
   */
24
  public HtmlPreviewProcessor( final HtmlPreview htmlPreview ) {
25
    sHtmlPreview = htmlPreview;
26
  }
27
28
  /**
29
   * Update the preview panel using HTML from the succession chain.
30
   *
31
   * @param html The document content to render in the preview pane. The HTML
32
   *             should not contain a doctype, head, or body tag.
33
   * @return The given {@code html} string.
34
   */
35
  @Override
36
  public String apply( final String html ) {
37
    assert html != null;
38
39
    sHtmlPreview.render( html );
40
    return html;
41
  }
42
}
143
A src/main/java/com/keenwrite/processors/IdentityProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
/**
5
 * Responsible for transforming a string into itself. This is used at the
6
 * end of a processing chain when no more processing is required.
7
 */
8
public final class IdentityProcessor extends ExecutorProcessor<String> {
9
  public static final IdentityProcessor IDENTITY = new IdentityProcessor();
10
11
  /**
12
   * Constructs a new instance having no successor (the default successor is
13
   * {@code null}).
14
   */
15
  private IdentityProcessor() {
16
  }
17
18
  /**
19
   * Returns the given string without modification.
20
   *
21
   * @param s The string to return.
22
   * @return The value of s.
23
   */
24
  @Override
25
  public String apply( final String s ) {
26
    return s;
27
  }
28
}
129
A src/main/java/com/keenwrite/processors/PdfProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import com.keenwrite.typesetting.Typesetter;
5
6
import static com.keenwrite.Bootstrap.APP_TITLE_ABBR;
7
import static com.keenwrite.events.StatusEvent.clue;
8
import static com.keenwrite.io.MediaType.TEXT_XML;
9
import static com.keenwrite.io.SysFile.normalize;
10
import static com.keenwrite.typesetting.Typesetter.Mutator;
11
import static java.nio.file.Files.deleteIfExists;
12
import static java.nio.file.Files.writeString;
13
14
/**
15
 * Responsible for using a typesetting engine to convert an XHTML document
16
 * into a PDF file. This must not be run from the JavaFX thread.
17
 */
18
public final class PdfProcessor extends ExecutorProcessor<String> {
19
  private final ProcessorContext mProcessorContext;
20
21
  public PdfProcessor( final ProcessorContext context ) {
22
    assert context != null;
23
    mProcessorContext = context;
24
  }
25
26
  /**
27
   * Converts a document by calling a third-party application to typeset the
28
   * given XHTML document.
29
   *
30
   * @param xhtml The document to convert to a PDF file.
31
   * @return {@code null} because there is no valid return value from generating
32
   * a PDF file.
33
   */
34
  public String apply( final String xhtml ) {
35
    try {
36
      clue( "Main.status.typeset.create" );
37
38
      final var context = mProcessorContext;
39
      final var targetPath = context.getTargetPath();
40
      clue( "Main.status.typeset.setting", "target", targetPath );
41
42
      final var parent = normalize( targetPath.toAbsolutePath().getParent() );
43
44
      final var document = TEXT_XML.createTempFile( APP_TITLE_ABBR, parent );
45
      final var sourcePath = writeString( document, xhtml );
46
      clue( "Main.status.typeset.setting", "source", sourcePath );
47
48
      final var themeDir = normalize( context.getThemeDir() );
49
      clue( "Main.status.typeset.setting", "themes", themeDir );
50
51
      final var imageDir = normalize( context.getImageDir() );
52
      clue( "Main.status.typeset.setting", "images", imageDir );
53
54
      final var imageOrder = context.getImageOrder();
55
      clue( "Main.status.typeset.setting", "order", imageOrder );
56
57
      final var cacheDir = normalize( context.getCacheDir() );
58
      clue( "Main.status.typeset.setting", "caches", cacheDir );
59
60
      final var fontDir = normalize( context.getFontDir() );
61
      clue( "Main.status.typeset.setting", "fonts", fontDir );
62
63
      final var rWorkDir = normalize( context.getRWorkingDir() );
64
      clue( "Main.status.typeset.setting", "r-work", rWorkDir );
65
66
      final var autoRemove = context.getAutoRemove();
67
      clue( "Main.status.typeset.setting", "purge", autoRemove );
68
69
      final var typesetter = Typesetter
70
        .builder()
71
        .with( Mutator::setTargetPath, targetPath )
72
        .with( Mutator::setSourcePath, sourcePath )
73
        .with( Mutator::setThemeDir, themeDir )
74
        .with( Mutator::setImageDir, imageDir )
75
        .with( Mutator::setCacheDir, cacheDir )
76
        .with( Mutator::setFontDir, fontDir )
77
        .with( Mutator::setAutoRemove, autoRemove )
78
        .build();
79
80
      typesetter.typeset();
81
82
      // Smote the temporary file after typesetting the document.
83
      if( typesetter.autoRemove() ) {
84
        deleteIfExists( document );
85
      }
86
    } catch( final Exception ex ) {
87
      // Typesetter runtime exceptions will pass up the call stack.
88
      clue( "Main.status.typeset.failed", ex );
89
    }
90
91
    // Do not continue processing (the document was typeset into a binary).
92
    return null;
93
  }
94
}
195
A src/main/java/com/keenwrite/processors/PreformattedProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
/**
5
 * This is the default processor used when an unknown file name extension is
6
 * encountered. It processes the text by enclosing it in an HTML {@code <pre>}
7
 * element.
8
 */
9
public final class PreformattedProcessor extends ExecutorProcessor<String> {
10
11
  /**
12
   * Passes the link to the super constructor.
13
   *
14
   * @param successor The next processor in the chain to use for text
15
   *                  processing.
16
   */
17
  public PreformattedProcessor( final Processor<String> successor ) {
18
    super( successor );
19
  }
20
21
  /**
22
   * Returns the given string, modified with "pre" tags.
23
   *
24
   * @param t The string to return, enclosed in "pre" tags.
25
   * @return The value of t wrapped in "pre" tags.
26
   */
27
  @Override
28
  public String apply( final String t ) {
29
    return "<pre>" + t + "</pre>";
30
  }
31
}
132
A src/main/java/com/keenwrite/processors/Processor.java
1
/* Copyright 2020-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.sigils.PropertyKeyOperator;
15
import com.keenwrite.sigils.SigilKeyOperator;
16
import com.keenwrite.util.GenericBuilder;
17
import org.renjin.repackaged.guava.base.Splitter;
18
19
import java.io.File;
20
import java.nio.file.Path;
21
import java.util.HashMap;
22
import java.util.Locale;
23
import java.util.Map;
24
import java.util.concurrent.Callable;
25
import java.util.function.Supplier;
26
27
import static com.keenwrite.Bootstrap.USER_CACHE_DIR;
28
import static com.keenwrite.Bootstrap.USER_DATA_DIR;
29
import static com.keenwrite.constants.Constants.*;
30
import static com.keenwrite.io.FileType.UNKNOWN;
31
import static com.keenwrite.io.MediaType.TEXT_PROPERTIES;
32
import static com.keenwrite.io.SysFile.toFile;
33
import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate;
34
35
/**
36
 * Provides a context for configuring a chain of {@link Processor} instances.
37
 */
38
public final class ProcessorContext {
39
40
  private final Mutator mMutator;
41
42
  /**
43
   * Determines the file type from the path extension. This should only be
44
   * called when it is known that the file type won't be a definition file
45
   * (e.g., YAML or other definition source), but rather an editable file
46
   * (e.g., Markdown, R Markdown, etc.).
47
   *
48
   * @param path The path with a file name extension.
49
   * @return The FileType for the given path.
50
   */
51
  private static FileType lookup( final Path path ) {
52
    assert path != null;
53
54
    final var prefix = GLOB_PREFIX_FILE;
55
    final var keys = sSettings.getKeys( prefix );
56
57
    var found = false;
58
    var fileType = UNKNOWN;
59
60
    while( keys.hasNext() && !found ) {
61
      final var key = keys.next();
62
      final var patterns = sSettings.getStringSettingList( key );
63
      final var predicate = createFileTypePredicate( patterns );
64
65
      if( predicate.test( toFile( path ) ) ) {
66
        // Remove the EXTENSIONS_PREFIX to get the file name extension mapped
67
        // to a standard name (as defined in the settings.properties file).
68
        final String suffix = key.replace( prefix + '.', "" );
69
        fileType = FileType.from( suffix );
70
        found = true;
71
      }
72
    }
73
74
    return fileType;
75
  }
76
77
  public boolean isExportFormat( final ExportFormat exportFormat ) {
78
    return mMutator.mExportFormat == exportFormat;
79
  }
80
81
  /**
82
   * Responsible for populating the instance variables required by the
83
   * context.
84
   */
85
  public static class Mutator {
86
    private Path mSourcePath;
87
    private Path mTargetPath;
88
    private ExportFormat mExportFormat;
89
    private Supplier<Boolean> mConcatenate = () -> true;
90
    private Supplier<String> mChapters = () -> "";
91
92
    private Supplier<Path> mThemeDir = USER_DIRECTORY::toPath;
93
    private Supplier<Locale> mLocale = () -> Locale.ENGLISH;
94
95
    private Supplier<Map<String, String>> mDefinitions = HashMap::new;
96
    private Supplier<Map<String, String>> mMetadata = HashMap::new;
97
    private Supplier<Caret> mCaret = () -> Caret.builder().build();
98
99
    private Supplier<Path> mFontDir = () -> getFontDirectory().toPath();
100
101
    private Supplier<Path> mImageDir = USER_DIRECTORY::toPath;
102
    private Supplier<String> mImageServer = () -> DIAGRAM_SERVER_NAME;
103
    private Supplier<String> mImageOrder = () -> PERSIST_IMAGES_DEFAULT;
104
105
    private Supplier<Path> mCacheDir = USER_CACHE_DIR::toPath;
106
107
    private Supplier<String> mSigilBegan = () -> DEF_DELIM_BEGAN_DEFAULT;
108
    private Supplier<String> mSigilEnded = () -> DEF_DELIM_ENDED_DEFAULT;
109
110
    private Supplier<Path> mRWorkingDir = USER_DIRECTORY::toPath;
111
    private Supplier<String> mRScript = () -> "";
112
113
    private Supplier<Boolean> mCurlQuotes = () -> true;
114
    private Supplier<Boolean> mAutoRemove = () -> true;
115
116
    public void setSourcePath( final Path sourcePath ) {
117
      assert sourcePath != null;
118
      mSourcePath = sourcePath;
119
    }
120
121
    public void setTargetPath( final Path outputPath ) {
122
      assert outputPath != null;
123
      mTargetPath = outputPath;
124
    }
125
126
    public void setThemeDir( final Supplier<Path> themeDir ) {
127
      assert themeDir != null;
128
      mThemeDir = themeDir;
129
    }
130
131
    public void setCacheDir( final Supplier<File> cacheDir ) {
132
      assert cacheDir != null;
133
134
      mCacheDir = () -> {
135
        final var dir = cacheDir.get();
136
137
        return (dir == null ? toFile( USER_DATA_DIR ) : dir).toPath();
138
      };
139
    }
140
141
    public void setImageDir( final Supplier<File> imageDir ) {
142
      assert imageDir != null;
143
144
      mImageDir = () -> {
145
        final var dir = imageDir.get();
146
147
        return (dir == null ? USER_DIRECTORY : dir).toPath();
148
      };
149
    }
150
151
    public void setImageOrder( final Supplier<String> imageOrder ) {
152
      assert imageOrder != null;
153
      mImageOrder = imageOrder;
154
    }
155
156
    public void setImageServer( final Supplier<String> imageServer ) {
157
      assert imageServer != null;
158
      mImageServer = imageServer;
159
    }
160
161
    public void setFontDir( final Supplier<File> fontDir ) {
162
      assert fontDir != null;
163
164
      mFontDir = () -> {
165
        final var dir = fontDir.get();
166
167
        return (dir == null ? USER_DIRECTORY : dir).toPath();
168
      };
169
    }
170
171
    public void setExportFormat( final ExportFormat exportFormat ) {
172
      assert exportFormat != null;
173
      mExportFormat = exportFormat;
174
    }
175
176
    public void setConcatenate( final Supplier<Boolean> concatenate ) {
177
      mConcatenate = concatenate;
178
    }
179
180
    public void setChapters( final Supplier<String> chapters ) {
181
      mChapters = chapters;
182
    }
183
184
    public void setLocale( final Supplier<Locale> locale ) {
185
      assert locale != null;
186
      mLocale = locale;
187
    }
188
189
    /**
190
     * Sets the list of fully interpolated key-value pairs to use when
191
     * substituting variable names back into the document as variable values.
192
     * This uses a {@link Callable} reference so that GUI and command-line
193
     * usage can insert their respective behaviours. That is, this method
194
     * prevents coupling the GUI to the CLI.
195
     *
196
     * @param supplier Defines how to retrieve the definitions.
197
     */
198
    public void setDefinitions( final Supplier<Map<String, String>> supplier ) {
199
      assert supplier != null;
200
      mDefinitions = supplier;
201
    }
202
203
    /**
204
     * Sets metadata to use in the document header. These are made available
205
     * to the typesetting engine as {@code \documentvariable} values.
206
     *
207
     * @param metadata The key/value pairs to publish as document metadata.
208
     */
209
    public void setMetadata( final Supplier<Map<String, String>> metadata ) {
210
      assert metadata != null;
211
      mMetadata = metadata.get() == null ? HashMap::new : metadata;
212
    }
213
214
    /**
215
     * Sets document variables to use when building the document. These
216
     * variables will override existing key/value pairs, or be added as
217
     * new key/value pairs if not already defined. This allows users to
218
     * inject variables into the document from the command-line, allowing
219
     * for dynamic assignment of in-text values when building documents.
220
     *
221
     * @param overrides The key/value pairs to add (or override) as variables.
222
     */
223
    public void setOverrides( final Supplier<Map<String, String>> overrides ) {
224
      assert overrides != null;
225
      assert mDefinitions != null;
226
      assert mDefinitions.get() != null;
227
228
      final var map = overrides.get();
229
230
      if( map != null ) {
231
        mDefinitions.get().putAll( map );
232
      }
233
    }
234
235
    /**
236
     * Sets the source for deriving the {@link Caret}. Typically, this is
237
     * the text editor that has focus.
238
     *
239
     * @param caret The source for the currently active caret.
240
     */
241
    public void setCaret( final Supplier<Caret> caret ) {
242
      assert caret != null;
243
      mCaret = caret;
244
    }
245
246
    public void setSigilBegan( final Supplier<String> sigilBegan ) {
247
      assert sigilBegan != null;
248
      mSigilBegan = sigilBegan;
249
    }
250
251
    public void setSigilEnded( final Supplier<String> sigilEnded ) {
252
      assert sigilEnded != null;
253
      mSigilEnded = sigilEnded;
254
    }
255
256
    public void setRWorkingDir( final Supplier<Path> rWorkingDir ) {
257
      assert rWorkingDir != null;
258
259
      mRWorkingDir = rWorkingDir;
260
    }
261
262
    public void setRScript( final Supplier<String> rScript ) {
263
      assert rScript != null;
264
      mRScript = rScript;
265
    }
266
267
    public void setCurlQuotes( final Supplier<Boolean> curlQuotes ) {
268
      assert curlQuotes != null;
269
      mCurlQuotes = curlQuotes;
270
    }
271
272
    public void setAutoRemove( final Supplier<Boolean> autoRemove ) {
273
      assert autoRemove != null;
274
      mAutoRemove = autoRemove;
275
    }
276
277
    private boolean isExportFormat( final ExportFormat format ) {
278
      return mExportFormat == format;
279
    }
280
  }
281
282
  public static GenericBuilder<Mutator, ProcessorContext> builder() {
283
    return GenericBuilder.of( Mutator::new, ProcessorContext::new );
284
  }
285
286
  /**
287
   * Creates a new context for use by the {@link ProcessorFactory} when
288
   * instantiating new {@link Processor} instances. Although all the
289
   * parameters are required, not all {@link Processor} instances will use
290
   * all parameters.
291
   */
292
  private ProcessorContext( final Mutator mutator ) {
293
    assert mutator != null;
294
295
    mMutator = mutator;
296
  }
297
298
  public Path getSourcePath() {
299
    return mMutator.mSourcePath;
300
  }
301
302
  /**
303
   * Answers what type of input document is to be processed.
304
   *
305
   * @return The input document's {@link MediaType}.
306
   */
307
  public MediaType getSourceType() {
308
    return MediaTypeExtension.fromPath( mMutator.mSourcePath );
309
  }
310
311
  /**
312
   * Fully qualified file name to use when exporting (e.g., document.pdf).
313
   *
314
   * @return Full path to a file name.
315
   */
316
  public Path getTargetPath() {
317
    return mMutator.mTargetPath;
318
  }
319
320
  public ExportFormat getExportFormat() {
321
    return mMutator.mExportFormat;
322
  }
323
324
  public Locale getLocale() {
325
    return mMutator.mLocale.get();
326
  }
327
328
  /**
329
   * Returns the variable map of definitions, without interpolation.
330
   *
331
   * @return A map to help dereference variables.
332
   */
333
  public Map<String, String> getDefinitions() {
334
    return mMutator.mDefinitions.get();
335
  }
336
337
  /**
338
   * Returns the variable map of definitions, with interpolation.
339
   *
340
   * @return A map to help dereference variables.
341
   */
342
  public InterpolatingMap getInterpolatedDefinitions() {
343
    return new InterpolatingMap(
344
      createDefinitionKeyOperator(), getDefinitions()
345
    ).interpolate();
346
  }
347
348
  public Map<String, String> getMetadata() {
349
    return mMutator.mMetadata.get();
350
  }
351
352
  /**
353
   * Returns the current caret position in the document being edited and is
354
   * always up-to-date.
355
   *
356
   * @return Caret position in the document.
357
   */
358
  public Supplier<Caret> getCaret() {
359
    return mMutator.mCaret;
360
  }
361
362
  /**
363
   * Returns the directory that contains the file being edited. When
364
   * {@link Constants#DOCUMENT_DEFAULT} is created, the parent path is
365
   * {@code null}. This will get absolute path to the file before trying to
366
   * get te parent path, which should always be a valid path. In the unlikely
367
   * event that the base path cannot be determined by the path alone, the
368
   * default user directory is returned. This is necessary for the creation
369
   * of new files.
370
   *
371
   * @return Path to the directory containing a file being edited, or the
372
   * default user directory if the base path cannot be determined.
373
   */
374
  public Path getBaseDir() {
375
    final var path = getSourcePath().toAbsolutePath().getParent();
376
    return path == null ? DEFAULT_DIRECTORY : path;
377
  }
378
379
  FileType getSourceFileType() {
380
    return lookup( getSourcePath() );
381
  }
382
383
  public Path getThemeDir() {
384
    return mMutator.mThemeDir.get();
385
  }
386
387
  public Path getImageDir() {
388
    return mMutator.mImageDir.get();
389
  }
390
391
  public Path getCacheDir() {
392
    return mMutator.mCacheDir.get();
393
  }
394
395
  public Iterable<String> getImageOrder() {
396
    assert mMutator.mImageOrder != null;
397
398
    final var order = mMutator.mImageOrder.get();
399
    final var token = order.contains( "," ) ? ',' : ' ';
400
401
    return Splitter.on( token ).split( token + order );
402
  }
403
404
  public String getImageServer() {
405
    return mMutator.mImageServer.get();
406
  }
407
408
  public Path getFontDir() {
409
    return mMutator.mFontDir.get();
410
  }
411
412
  public boolean getAutoRemove() {
413
    return mMutator.mAutoRemove.get();
414
  }
415
416
  public Path getRWorkingDir() {
417
    return mMutator.mRWorkingDir.get();
418
  }
419
420
  public String getRScript() {
421
    return mMutator.mRScript.get();
422
  }
423
424
  public boolean getCurlQuotes() {
425
    return mMutator.mCurlQuotes.get();
426
  }
427
428
  /**
429
   * Answers whether to process a single text file or all text files in
430
   * the same directory as a single text file. See {@link #getSourcePath()}
431
   * for the file to process (or all files in its directory).
432
   *
433
   * @return {@code true} means to process all text files, {@code false}
434
   * means to process a single file.
435
   */
436
  public boolean getConcatenate() {
437
    return mMutator.mConcatenate.get();
438
  }
439
440
  public String getChapters() {
441
    return mMutator.mChapters.get();
442
  }
443
444
  public SigilKeyOperator createKeyOperator() {
445
    return createKeyOperator( getSourcePath() );
446
  }
447
448
  /**
449
   * Returns the sigil operator for the given {@link Path}.
450
   *
451
   * @param path The type of file being edited, from its extension.
452
   */
453
  private SigilKeyOperator createKeyOperator( final Path path ) {
454
    assert path != null;
455
456
    return MediaType.fromFilename( path ) == TEXT_PROPERTIES
457
      ? createPropertyKeyOperator()
458
      : createDefinitionKeyOperator();
459
  }
460
461
  private SigilKeyOperator createPropertyKeyOperator() {
462
    return new PropertyKeyOperator();
463
  }
464
465
  private SigilKeyOperator createDefinitionKeyOperator() {
466
    final var began = mMutator.mSigilBegan.get();
467
    final var ended = mMutator.mSigilEnded.get();
468
469
    return new SigilKeyOperator( began, ended );
470
  }
471
}
1472
A src/main/java/com/keenwrite/processors/ProcessorFactory.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import com.keenwrite.processors.markdown.MarkdownProcessor;
5
6
import static com.keenwrite.io.FileType.RMARKDOWN;
7
import static com.keenwrite.io.FileType.SOURCE;
8
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
9
10
/**
11
 * Responsible for creating processors capable of parsing, transforming,
12
 * interpolating, and rendering known file types.
13
 */
14
public final class ProcessorFactory {
15
16
  private ProcessorFactory() {
17
  }
18
19
  public static Processor<String> createProcessors(
20
    final ProcessorContext context ) {
21
    return createProcessors( context, null );
22
  }
23
24
  /**
25
   * Creates a new {@link Processor} chain suitable for parsing and rendering
26
   * the file opened at the given tab.
27
   *
28
   * @param context The tab containing a text editor, path, and caret position.
29
   * @return A processor that can render the given tab's text.
30
   */
31
  public static Processor<String> createProcessors(
32
    final ProcessorContext context, final Processor<String> preview ) {
33
    return createProcessor( context, preview );
34
  }
35
36
  /**
37
   * Constructs processors that chain various processing operations on a
38
   * document to generate a transformed version of the source document.
39
   *
40
   * @param context Parameters needed to construct various processors.
41
   * @param preview The processor to use when no export format is specified.
42
   */
43
  private static Processor<String> createProcessor(
44
    final ProcessorContext context, final Processor<String> preview ) {
45
    // If the content is not to be exported, then the successor processor
46
    // is one that parses Markdown into HTML and passes the string to the
47
    // HTML preview pane.
48
    //
49
    // Otherwise, bolt on a processor that---after the interpolation and
50
    // substitution phase, which includes text strings or R code---will
51
    // generate HTML or plain Markdown. HTML has a few output formats:
52
    // with embedded SVG representing formulas, or without any conversion
53
    // to SVG. Without conversion would require client-side rendering of
54
    // math (such as using the JavaScript-based KaTeX engine).
55
    final var outputType = context.getExportFormat();
56
57
    final var successor = switch( outputType ) {
58
      case NONE -> preview;
59
      case XHTML_TEX -> createXhtmlProcessor( context );
60
      case APPLICATION_PDF -> createPdfProcessor( context );
61
      default -> createIdentityProcessor( context );
62
    };
63
64
    final var inputType = context.getSourceFileType();
65
    final Processor<String> processor;
66
67
    // When there's no preview, convert to HTML.
68
    if( preview == null ) {
69
      processor = createMarkdownProcessor( successor, context );
70
    }
71
    else {
72
      processor = inputType == SOURCE || inputType == RMARKDOWN
73
        ? createMarkdownProcessor( successor, context )
74
        : createPreformattedProcessor( successor );
75
    }
76
77
    return new ExecutorProcessor<>( processor );
78
  }
79
80
  /**
81
   * Instantiates a new {@link Processor} that has no successor and returns
82
   * the string it was given without modification.
83
   *
84
   * @return An instance of {@link Processor} that performs no processing.
85
   */
86
  @SuppressWarnings( "unused" )
87
  private static Processor<String> createIdentityProcessor(
88
    final ProcessorContext ignored ) {
89
    return IDENTITY;
90
  }
91
92
  /**
93
   * Instantiates a {@link Processor} responsible for parsing Markdown and
94
   * definitions.
95
   *
96
   * @return A chain of {@link Processor}s for processing Markdown and
97
   * definitions.
98
   */
99
  private static Processor<String> createMarkdownProcessor(
100
    final Processor<String> successor,
101
    final ProcessorContext context ) {
102
    final var dp = createVariableProcessor( successor, context );
103
    return MarkdownProcessor.create( dp, context );
104
  }
105
106
  private static Processor<String> createVariableProcessor(
107
    final Processor<String> successor,
108
    final ProcessorContext context ) {
109
    return new VariableProcessor( successor, context );
110
  }
111
112
  /**
113
   * Instantiates a new {@link Processor} that wraps an HTML document into
114
   * its final, well-formed state (including head and body tags). This is
115
   * useful for generating XHTML documents suitable for typesetting (using
116
   * an engine such as LuaTeX).
117
   *
118
   * @return An instance of {@link Processor} that completes an HTML document.
119
   */
120
  private static Processor<String> createXhtmlProcessor(
121
    final ProcessorContext context ) {
122
    return createXhtmlProcessor( IDENTITY, context );
123
  }
124
125
  private static Processor<String> createXhtmlProcessor(
126
    final Processor<String> successor, final ProcessorContext context ) {
127
    return new XhtmlProcessor( successor, context );
128
  }
129
130
  private static Processor<String> createPdfProcessor(
131
    final ProcessorContext context ) {
132
    final var pdfp = new PdfProcessor( context );
133
    return createXhtmlProcessor( pdfp, context );
134
  }
135
136
  private static Processor<String> createPreformattedProcessor(
137
    final Processor<String> successor ) {
138
    return new PreformattedProcessor( successor );
139
  }
140
}
1141
A src/main/java/com/keenwrite/processors/RBootstrapProcessor.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.processors.r.RBootstrapController;
8
9
public class RBootstrapProcessor extends ExecutorProcessor<String> {
10
  private final Processor<String> mSuccessor;
11
  private final ProcessorContext mContext;
12
13
  public RBootstrapProcessor(
14
    final Processor<String> successor,
15
    final ProcessorContext context ) {
16
    assert successor != null;
17
    assert context != null;
18
19
    mSuccessor = successor;
20
    mContext = context;
21
  }
22
23
  /**
24
   * Processes the given text document by replacing variables with their values.
25
   *
26
   * @param text The document text that includes variables that should be
27
   *             replaced with values when rendered as HTML.
28
   * @return The text with all variables replaced.
29
   */
30
  @Override
31
  public String apply( final String text ) {
32
    assert text != null;
33
34
    final var bootstrap = mContext.getRScript();
35
    final var workingDir = mContext.getRWorkingDir().toString();
36
    final var definitions = mContext.getDefinitions();
37
38
    RBootstrapController.update( bootstrap, workingDir, definitions );
39
40
    return mSuccessor.apply( text );
41
  }
42
}
143
A src/main/java/com/keenwrite/processors/VariableProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import java.util.HashMap;
5
import java.util.Map;
6
import java.util.function.Function;
7
import java.util.function.UnaryOperator;
8
9
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
10
11
/**
12
 * Processes interpolated string definitions in the document and inserts
13
 * their values into the post-processed text. The default variable syntax is
14
 * <pre>{{variable}}</pre> (a.k.a., moustache syntax).
15
 */
16
public class VariableProcessor
17
  extends ExecutorProcessor<String> implements Function<String, String> {
18
19
  private final ProcessorContext mContext;
20
  private final UnaryOperator<String> mSigilOperator;
21
22
  /**
23
   * Constructs a processor capable of interpolating string definitions.
24
   *
25
   * @param successor Subsequent link in the processing chain.
26
   * @param context   Contains resolved definitions map.
27
   */
28
  public VariableProcessor(
29
    final Processor<String> successor,
30
    final ProcessorContext context ) {
31
    super( successor );
32
33
    mContext = context;
34
    mSigilOperator = createKeyOperator( context );
35
  }
36
37
  /**
38
   * Subclasses may change the type of operation performed on keys, such as
39
   * wrapping key names in sigils.
40
   *
41
   * @param context Provides the name of the file being edited.
42
   * @return An operator for transforming key names.
43
   */
44
  protected UnaryOperator<String> createKeyOperator(
45
    final ProcessorContext context ) {
46
    return context.createKeyOperator();
47
  }
48
49
  /**
50
   * Returns the map to use for variable substitution.
51
   *
52
   * @return A map of variable names to values, with keys wrapped in sigils.
53
   */
54
  protected Map<String, String> getDefinitions() {
55
    return entoken( mContext.getInterpolatedDefinitions() );
56
  }
57
58
  /**
59
   * Subclasses may override this method to change how keys are wrapped
60
   * in sigils.
61
   *
62
   * @param key The key to enwrap.
63
   * @return The wrapped key.
64
   */
65
  protected String processKey( final String key ) {
66
    return mSigilOperator.apply( key );
67
  }
68
69
  /**
70
   * Subclasses may override this method to modify values prior to use. This
71
   * can be used, for example, to escape values prior to evaluating by a
72
   * scripting engine.
73
   *
74
   * @param value The value to process.
75
   * @return The processed value.
76
   */
77
  protected String processValue( final String value ) {
78
    return value;
79
  }
80
81
  /**
82
   * Processes the given text document by replacing variables with their values.
83
   *
84
   * @param text The document text that includes variables that should be
85
   *             replaced with values when rendered as HTML.
86
   * @return The text with all variables replaced.
87
   */
88
  @Override
89
  public String apply( final String text ) {
90
    assert text != null;
91
92
    return replace( text, getDefinitions() );
93
  }
94
95
  /**
96
   * Converts the given map from regular variables to processor-specific
97
   * variables.
98
   *
99
   * @param map Map of variable names to values.
100
   * @return Map of variables with the keys and values subjected to
101
   * post-processing.
102
   */
103
  protected Map<String, String> entoken( final Map<String, String> map ) {
104
    assert map != null;
105
106
    final var result = new HashMap<String, String>( map.size() );
107
108
    map.forEach( ( k, v ) -> result.put( processKey( k ), processValue( v ) ) );
109
110
    return result;
111
  }
112
}
1113
A src/main/java/com/keenwrite/processors/XhtmlProcessor.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.dom.DocumentParser;
8
import com.keenwrite.io.MediaTypeExtension;
9
import com.keenwrite.ui.heuristics.WordCounter;
10
import com.keenwrite.util.DataTypeConverter;
11
import com.whitemagicsoftware.keenquotes.parser.Contractions;
12
import com.whitemagicsoftware.keenquotes.parser.Curler;
13
import org.w3c.dom.Document;
14
15
import java.io.File;
16
import java.io.FileNotFoundException;
17
import java.nio.file.Path;
18
import java.util.*;
19
20
import static com.keenwrite.Bootstrap.APP_TITLE_ABBR;
21
import static com.keenwrite.dom.DocumentParser.*;
22
import static com.keenwrite.events.StatusEvent.clue;
23
import static com.keenwrite.io.SysFile.toFile;
24
import static com.keenwrite.io.downloads.DownloadManager.open;
25
import static com.keenwrite.util.ProtocolScheme.getProtocol;
26
import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML;
27
import static java.lang.String.format;
28
import static java.lang.String.valueOf;
29
import static java.nio.file.Files.copy;
30
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
31
32
/**
33
 * Responsible for making an XHTML document complete by wrapping it with html
34
 * and body elements. This doesn't have to be super-efficient because it's
35
 * not run in real-time.
36
 */
37
public final class XhtmlProcessor extends ExecutorProcessor<String> {
38
  private static final Curler sTypographer =
39
    new Curler( createContractions(), FILTER_XML, true );
40
41
  private final ProcessorContext mContext;
42
43
  public XhtmlProcessor(
44
    final Processor<String> successor, final ProcessorContext context ) {
45
    super( successor );
46
47
    assert context != null;
48
    mContext = context;
49
  }
50
51
  /**
52
   * Responsible for producing a well-formed XML document complete with
53
   * metadata (title, author, keywords, copyright, and date).
54
   *
55
   * @param html The HTML document to transform into an XHTML document.
56
   * @return The transformed HTML document.
57
   */
58
  @Override
59
  public String apply( final String html ) {
60
    clue( "Main.status.typeset.xhtml" );
61
62
    try {
63
      final var doc = parse( html );
64
      setMetaData( doc );
65
66
      visit( doc, "//img", node -> {
67
        try {
68
          final var attrs = node.getAttributes();
69
          final var attr = attrs.getNamedItem( "src" );
70
71
          if( attr != null ) {
72
            final var src = attr.getTextContent();
73
            final Path location;
74
            final Path imagesDir;
75
76
            // Download into a cache directory, which can be written to without
77
            // any possibility of overwriting local image files. Further, the
78
            // filenames are hashed as a second layer of protection.
79
            if( getProtocol( src ).isRemote() ) {
80
              location = downloadImage( src );
81
              imagesDir = getCachesPath();
82
            }
83
            else {
84
              location = resolveImage( src );
85
              imagesDir = getImagesPath();
86
            }
87
88
            final var relative = imagesDir.relativize( location );
89
90
            attr.setTextContent( relative.toString() );
91
          }
92
        } catch( final Exception ex ) {
93
          clue( ex );
94
        }
95
      } );
96
97
      final var document = DocumentParser.toString( doc );
98
      final var curl = mContext.getCurlQuotes();
99
100
      return curl ? sTypographer.apply( document ) : document;
101
    } catch( final Exception ex ) {
102
      clue( ex );
103
    }
104
105
    return html;
106
  }
107
108
  /**
109
   * Applies the metadata fields to the document.
110
   *
111
   * @param doc The document to adorn with metadata.
112
   */
113
  private void setMetaData( final Document doc ) {
114
    final var metadata = createMetaDataMap( doc );
115
    final var title = metadata.get( "title" );
116
117
    visit( doc, "/html/head", node -> {
118
      // Insert <title>text</title> inside <head>.
119
      node.appendChild( createElement( doc, "title", title ) );
120
121
      // Insert each <meta name=x content=y /> inside <head>.
122
      metadata.entrySet().forEach(
123
        entry -> node.appendChild( createMeta( doc, entry ) )
124
      );
125
    } );
126
  }
127
128
  /**
129
   * Generates document metadata, including word count.
130
   *
131
   * @param doc The document containing the text to tally.
132
   * @return A map of metadata key/value pairs.
133
   */
134
  private Map<String, String> createMetaDataMap( final Document doc ) {
135
    final var result = new LinkedHashMap<String, String>();
136
    final var map = mContext.getInterpolatedDefinitions();
137
    final var metadata = getMetadata();
138
139
    metadata.forEach(
140
      ( key, value ) -> {
141
        final var interpolated = map.interpolate( value );
142
143
        if( !interpolated.isEmpty() ) {
144
          result.put( key, interpolated );
145
        }
146
      }
147
    );
148
    result.put( "count", wordCount( doc ) );
149
150
    return result;
151
  }
152
153
  /**
154
   * The metadata is in list form because the user interface for entering the
155
   * key-value pairs is a table, which requires a generic {@link List} rather
156
   * than a generic {@link Map}.
157
   *
158
   * @return The document metadata.
159
   */
160
  private Map<String, String> getMetadata() {
161
    final var result = mContext.getMetadata();
162
    return result == null ? new HashMap<>() : result;
163
  }
164
165
  /**
166
   * Hashes the URL so that the number of files doesn't eat up disk space
167
   * over time. For static resources, a feature could be added to prevent
168
   * downloading the URL if the hashed filename already exists.
169
   *
170
   * @param src The source file's URL to download.
171
   * @return A {@link Path} to the local file containing the URL's contents.
172
   * @throws Exception Could not download or save the file.
173
   */
174
  private Path downloadImage( final String src ) throws Exception {
175
    final Path imagePath;
176
    final File imageFile;
177
    final var cachesPath = getCachesPath();
178
179
    clue( "Main.status.image.xhtml.image.download", src );
180
181
    try( final var response = open( src ) ) {
182
      final var mediaType = response.getMediaType();
183
184
      final var ext = MediaTypeExtension.valueFrom( mediaType ).getExtension();
185
      final var hash = DataTypeConverter.toHex( DataTypeConverter.hash( src ) );
186
      final var id = hash.toLowerCase();
187
188
      imagePath = cachesPath.resolve( APP_TITLE_ABBR + id + '.' + ext );
189
      imageFile = toFile( imagePath );
190
191
      // Preserve image files if auto-remove is turned off.
192
      if( autoRemove() ) {
193
        imageFile.deleteOnExit();
194
      }
195
196
      try( final var image = response.getInputStream() ) {
197
        copy( image, imagePath, REPLACE_EXISTING );
198
      }
199
200
      if( mediaType.isSvg() ) {
201
        sanitize( imagePath );
202
      }
203
    }
204
205
    final var key = imageFile.exists()
206
      ? "Main.status.image.xhtml.image.saved"
207
      : "Main.status.image.xhtml.image.failed";
208
    clue( key, imageFile );
209
210
    return imagePath;
211
  }
212
213
  private Path resolveImage( final String src ) throws Exception {
214
    var imagePath = getImagesPath();
215
    var found = false;
216
217
    Path imageFile = null;
218
219
    clue( "Main.status.image.xhtml.image.resolve", src );
220
221
    for( final var extension : getImageOrder() ) {
222
      final var filename = format(
223
        "%s%s%s", src, extension.isBlank() ? "" : ".", extension );
224
      imageFile = imagePath.resolve( filename );
225
226
      if( toFile( imageFile ).exists() ) {
227
        found = true;
228
        break;
229
      }
230
    }
231
232
    if( !found ) {
233
      imagePath = getDocumentDir();
234
      imageFile = imagePath.resolve( src );
235
236
      if( !toFile( imageFile ).exists() ) {
237
        final var filename = imageFile.toString();
238
        clue( "Main.status.image.xhtml.image.missing", filename );
239
240
        throw new FileNotFoundException( filename );
241
      }
242
    }
243
244
    clue( "Main.status.image.xhtml.image.found", imageFile.toString() );
245
246
    return imageFile;
247
  }
248
249
  private Path getImagesPath() {
250
    return mContext.getImageDir();
251
  }
252
253
  private Path getCachesPath() {
254
    return mContext.getCacheDir();
255
  }
256
257
  /**
258
   * By including an "empty" extension, the first element returned
259
   * will be the empty string. Thus, the first extension to try is the
260
   * file's default extension. Subsequent iterations will try to find
261
   * a file that has a name matching one of the preferred extensions.
262
   *
263
   * @return A list of extensions, including an empty string at the start.
264
   */
265
  private Iterable<String> getImageOrder() {
266
    return mContext.getImageOrder();
267
  }
268
269
  /**
270
   * Returns the absolute path to the document being edited, which can be used
271
   * to find files included using relative paths.
272
   *
273
   * @return The directory containing the edited file.
274
   */
275
  private Path getDocumentDir() {
276
    return mContext.getBaseDir();
277
  }
278
279
  private Locale getLocale() {
280
    return mContext.getLocale();
281
  }
282
283
  private boolean autoRemove() {
284
    return mContext.getAutoRemove();
285
  }
286
287
  private String wordCount( final Document doc ) {
288
    final var sb = new StringBuilder( 65536 * 10 );
289
290
    visit(
291
      doc,
292
      "//*[normalize-space( text() ) != '']",
293
      node -> sb.append( node.getTextContent() )
294
    );
295
296
    return valueOf( WordCounter.create( getLocale() ).count( sb.toString() ) );
297
  }
298
299
  /**
300
   * Creates contracts with a custom set of unambiguous strings.
301
   *
302
   * @return List of contractions to use for curling straight quotes.
303
   */
304
  private static Contractions createContractions() {
305
    return new Contractions.Builder().build();
306
  }
307
}
1308
A src/main/java/com/keenwrite/processors/markdown/BaseMarkdownProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown;
3
4
import com.keenwrite.dom.DocumentConverter;
5
import com.keenwrite.processors.ExecutorProcessor;
6
import com.keenwrite.processors.Processor;
7
import com.keenwrite.processors.ProcessorContext;
8
import com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension;
9
import com.keenwrite.processors.markdown.extensions.r.RInlineExtension;
10
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
11
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
12
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
13
import com.vladsch.flexmark.ext.tables.TablesExtension;
14
import com.vladsch.flexmark.html.HtmlRenderer;
15
import com.vladsch.flexmark.parser.Parser;
16
import com.vladsch.flexmark.util.ast.IParse;
17
import com.vladsch.flexmark.util.ast.IRender;
18
import com.vladsch.flexmark.util.ast.Node;
19
import com.vladsch.flexmark.util.data.MutableDataSet;
20
import com.vladsch.flexmark.util.misc.Extension;
21
22
import java.util.ArrayList;
23
import java.util.List;
24
25
/**
26
 * Responsible for parsing and rendering Markdown into HTML. This is required
27
 * to break a circular dependency between the {@link MarkdownProcessor} and
28
 * {@link RInlineExtension}.
29
 */
30
public class BaseMarkdownProcessor extends ExecutorProcessor<String> {
31
32
  private final IParse mParser;
33
  private final IRender mRenderer;
34
35
  public BaseMarkdownProcessor(
36
    final Processor<String> successor, final ProcessorContext context ) {
37
    super( successor );
38
39
    final var options = new MutableDataSet();
40
    options.set( HtmlRenderer.GENERATE_HEADER_ID, true );
41
    options.set( HtmlRenderer.RENDER_HEADER_ID, true );
42
43
    final var builder = Parser.builder( options );
44
    final var extensions = createExtensions( context );
45
46
    mParser = builder.extensions( extensions ).build();
47
    mRenderer = HtmlRenderer
48
      .builder( options )
49
      .extensions( extensions )
50
      .build();
51
  }
52
53
  /**
54
   * Instantiates a number of extensions to be applied when parsing.
55
   *
56
   * @param context The context that subclasses use to configure custom
57
   *                extension behaviour.
58
   * @return A {@link List} of {@link Extension} instances that change the
59
   * {@link Parser}'s behaviour.
60
   */
61
  List<Extension> createExtensions( final ProcessorContext context ) {
62
    final var extensions = new ArrayList<Extension>();
63
64
    extensions.add( DefinitionExtension.create() );
65
    extensions.add( StrikethroughSubscriptExtension.create() );
66
    extensions.add( SuperscriptExtension.create() );
67
    extensions.add( TablesExtension.create() );
68
    extensions.add( FencedDivExtension.create() );
69
70
    return extensions;
71
  }
72
73
  /**
74
   * Converts the given Markdown string into HTML, without the doctype, html,
75
   * head, and body tags.
76
   *
77
   * @param markdown The string to convert from Markdown to HTML.
78
   * @return The HTML representation of the Markdown document.
79
   */
80
  @Override
81
  public String apply( final String markdown ) {
82
    return toXhtml( toHtml( toNode( markdown ) ) );
83
  }
84
85
  /**
86
   * Returns the AST in the form of a node for the given Markdown document. This
87
   * can be used, for example, to determine if a hyperlink exists inside a
88
   * paragraph.
89
   *
90
   * @param markdown The Markdown to convert into an AST.
91
   * @return The Markdown AST for the given text (usually a paragraph).
92
   */
93
  public Node toNode( final String markdown ) {
94
    return parse( markdown );
95
  }
96
97
  /**
98
   * Returns the result of converting the given AST into an HTML string.
99
   *
100
   * @param node The AST {@link Node} to convert to an HTML string.
101
   * @return The given {@link Node} as an HTML string.
102
   */
103
  private String toHtml( final Node node ) {
104
    return getRenderer().render( node );
105
  }
106
107
  /**
108
   * Ensures that subsequent processing will receive a well-formed document.
109
   * That is, an XHTML document.
110
   *
111
   * @param html Document to transform (may contain unbalanced HTML tags).
112
   * @return A well-formed (balanced) equivalent HTML document.
113
   */
114
  private String toXhtml( final String html ) {
115
    return DocumentConverter.parse( html ).html();
116
  }
117
118
  /**
119
   * Helper method to create an AST given some Markdown.
120
   *
121
   * @param markdown The Markdown to parse.
122
   * @return The root node of the Markdown tree.
123
   */
124
  private Node parse( final String markdown ) {
125
    return getParser().parse( markdown );
126
  }
127
128
  /**
129
   * Creates the Markdown document processor.
130
   *
131
   * @return An instance of {@link IParse} for building abstract syntax trees.
132
   */
133
  private IParse getParser() {
134
    return mParser;
135
  }
136
137
  private IRender getRenderer() {
138
    return mRenderer;
139
  }
140
}
1141
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.VariableProcessor;
12
import com.keenwrite.processors.markdown.extensions.CaretExtension;
13
import com.keenwrite.processors.markdown.extensions.DocumentOutlineExtension;
14
import com.keenwrite.processors.markdown.extensions.ImageLinkExtension;
15
import com.keenwrite.processors.markdown.extensions.fences.FencedBlockExtension;
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.r.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.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
34
  private MarkdownProcessor(
35
    final Processor<String> successor, final ProcessorContext context ) {
36
    super( successor, context );
37
  }
38
39
  public static MarkdownProcessor create( final ProcessorContext context ) {
40
    return create( IDENTITY, context );
41
  }
42
43
  public static MarkdownProcessor create(
44
    final Processor<String> successor, final ProcessorContext context ) {
45
    return new MarkdownProcessor( successor, context );
46
  }
47
48
  /**
49
   * Creating extensions based using an instance of {@link ProcessorContext}
50
   * indicates that the {@link CaretExtension} should be used to inject the
51
   * caret position into the final HTML document. This enables the HTML
52
   * preview pane to scroll to the same position, relatively speaking, within
53
   * the main document. Scrolling is developed this way to decouple the
54
   * document being edited from the preview pane so that multiple document
55
   * formats can be edited.
56
   *
57
   * @param context Contains necessary information needed to create
58
   *                extensions used by the Markdown parser.
59
   * @return {@link List} of extensions invoked when parsing Markdown.
60
   */
61
  @Override
62
  List<Extension> createExtensions( final ProcessorContext context ) {
63
    final var inputPath = context.getSourcePath();
64
    final var mediaType = MediaType.fromFilename( inputPath );
65
    final Processor<String> processor;
66
    final Function<String, String> evaluator;
67
    final List<Extension> result = new ArrayList<>();
68
69
    if( mediaType == TEXT_R_MARKDOWN ) {
70
      final var rVarProcessor = new RVariableProcessor( IDENTITY, context );
71
      final var rInlineEvaluator = new RInlineEvaluator( rVarProcessor );
72
      result.add( RInlineExtension.create( rInlineEvaluator, context ) );
73
      processor = rVarProcessor;
74
      evaluator = rInlineEvaluator;
75
    }
76
    else {
77
      processor = new VariableProcessor( IDENTITY, context );
78
      evaluator = processor;
79
    }
80
81
    // Add typographic, table, strikethrough, and similar extensions.
82
    result.addAll( super.createExtensions( context ) );
83
84
    result.add( ImageLinkExtension.create( context ) );
85
    result.add( TeXExtension.create( evaluator, context ) );
86
    result.add( FencedBlockExtension.create( processor, evaluator, context ) );
87
88
    if( context.isExportFormat( ExportFormat.NONE ) ) {
89
      result.add( CaretExtension.create( context ) );
90
    }
91
92
    result.add( DocumentOutlineExtension.create( processor ) );
93
    return result;
94
  }
95
}
196
A src/main/java/com/keenwrite/processors/markdown/extensions/CaretExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions;
3
4
import com.keenwrite.editors.common.Caret;
5
import com.keenwrite.constants.Constants;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.vladsch.flexmark.ext.tables.TableBlock;
8
import com.vladsch.flexmark.html.AttributeProvider;
9
import com.vladsch.flexmark.html.AttributeProviderFactory;
10
import com.vladsch.flexmark.html.IndependentAttributeProviderFactory;
11
import com.vladsch.flexmark.html.renderer.AttributablePart;
12
import com.vladsch.flexmark.html.renderer.LinkResolverContext;
13
import com.vladsch.flexmark.util.ast.Node;
14
import com.vladsch.flexmark.util.html.AttributeImpl;
15
import com.vladsch.flexmark.util.html.MutableAttributes;
16
import org.jetbrains.annotations.NotNull;
17
18
import java.util.function.Supplier;
19
20
import static com.keenwrite.constants.Constants.CARET_ID;
21
import static com.keenwrite.processors.markdown.extensions.EmptyNode.EMPTY_NODE;
22
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
23
24
/**
25
 * Responsible for giving most block-level elements a unique identifier
26
 * attribute. The identifier is used to coordinate scrolling.
27
 */
28
public class CaretExtension extends HtmlRendererAdapter {
29
30
  private final Supplier<Caret> mCaret;
31
32
  private CaretExtension( final ProcessorContext context ) {
33
    mCaret = context.getCaret();
34
  }
35
36
  public static CaretExtension create( final ProcessorContext context ) {
37
    return new CaretExtension( context );
38
  }
39
40
  @Override
41
  public void extend( @NotNull final Builder builder,
42
                      @NotNull final String rendererType ) {
43
    builder.attributeProviderFactory(
44
      IdAttributeProvider.createFactory( mCaret ) );
45
  }
46
47
  /**
48
   * Responsible for creating the id attribute. This class is instantiated
49
   * once: for the HTML element containing the {@link Constants#CARET_ID}.
50
   */
51
  public static class IdAttributeProvider implements AttributeProvider {
52
    private final Supplier<Caret> mCaret;
53
    private boolean mAdded;
54
55
    public IdAttributeProvider( final Supplier<Caret> caret ) {
56
      mCaret = caret;
57
    }
58
59
    private static AttributeProviderFactory createFactory(
60
      final Supplier<Caret> caret ) {
61
      return new IndependentAttributeProviderFactory() {
62
        @Override
63
        public @NotNull AttributeProvider apply(
64
          @NotNull final LinkResolverContext context ) {
65
          return new IdAttributeProvider( caret );
66
        }
67
      };
68
    }
69
70
    @Override
71
    public void setAttributes( @NotNull Node curr,
72
                               @NotNull AttributablePart part,
73
                               @NotNull MutableAttributes attributes ) {
74
      // Optimization: if a caret is inserted, don't try to find another.
75
      if( mAdded ) {
76
        return;
77
      }
78
79
      final var caret = mCaret.get();
80
81
      // If a table block has been earmarked with an empty node, it means
82
      // another extension has generated code from an external source. The
83
      // Markdown processor won't be able to determine the caret position
84
      // with any semblance of accuracy, so skip the element. This usually
85
      // happens with tables, but in theory any Markdown generated from an
86
      // external source (e.g., an R script) could produce text that has no
87
      // caret position that can be calculated.
88
      var table = curr;
89
90
      if( !(curr instanceof TableBlock) ) {
91
        table = curr.getAncestorOfType( TableBlock.class );
92
      }
93
94
      // The table was generated outside the document
95
      if( table != null && table.getLastChild() == EMPTY_NODE ) {
96
        return;
97
      }
98
99
      final var outside = caret.isAfterText() ? 1 : 0;
100
      final var began = curr.getStartOffset();
101
      final var ended = curr.getEndOffset() + outside;
102
      final var prev = curr.getPrevious();
103
104
      // If the caret is within the bounds of the current node or the
105
      // caret is within the bounds of the end of the previous node and
106
      // the start of the current node, then mark the current node with
107
      // a caret indicator.
108
      if( caret.isBetweenText( began, ended ) ||
109
        prev != null && caret.isBetweenText( prev.getEndOffset(), began ) ) {
110
        // This line empowers synchronizing the text editor with the preview.
111
        attributes.addValue( AttributeImpl.of( "id", CARET_ID ) );
112
113
        // We're done until the user moves the caret (micro-optimization)
114
        mAdded = true;
115
      }
116
    }
117
  }
118
}
1119
A src/main/java/com/keenwrite/processors/markdown/extensions/DocumentOutlineExtension.java
1
package com.keenwrite.processors.markdown.extensions;
2
3
import com.keenwrite.events.ParseHeadingEvent;
4
import com.keenwrite.processors.Processor;
5
import com.vladsch.flexmark.ast.Heading;
6
import com.vladsch.flexmark.parser.Parser.Builder;
7
import com.vladsch.flexmark.parser.Parser.ParserExtension;
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 com.vladsch.flexmark.util.data.MutableDataHolder;
14
import org.jetbrains.annotations.NotNull;
15
16
import java.util.regex.Pattern;
17
18
import static com.keenwrite.events.ParseHeadingEvent.fireNewOutlineEvent;
19
20
public final class DocumentOutlineExtension implements ParserExtension {
21
  private static final Pattern sRegex = Pattern.compile( "^(#+)" );
22
23
  private final Processor<String> mProcessor;
24
25
  private DocumentOutlineExtension( final Processor<String> processor ) {
26
    mProcessor = processor;
27
  }
28
29
  @Override
30
  public void parserOptions( final MutableDataHolder options ) {}
31
32
  @Override
33
  public void extend( final Builder builder ) {
34
    builder.postProcessorFactory( new Factory() );
35
  }
36
37
  public static DocumentOutlineExtension create(
38
    final Processor<String> processor ) {
39
    return new DocumentOutlineExtension( processor );
40
  }
41
42
  private class HeadingNodePostProcessor extends NodePostProcessor {
43
    @Override
44
    public void process(
45
      @NotNull final NodeTracker state, @NotNull final Node node ) {
46
      final var heading = mProcessor.apply( node.getChars().toString() );
47
      final var matcher = sRegex.matcher( heading );
48
49
      if( matcher.find() ) {
50
        final var level = matcher.group().length();
51
        final var text = heading.substring( level );
52
        final var offset = node.getStartOffset();
53
        ParseHeadingEvent.fire( level, text, offset );
54
      }
55
    }
56
  }
57
58
  public class Factory extends NodePostProcessorFactory {
59
    public Factory() {
60
      super( false );
61
      addNodes( Heading.class );
62
    }
63
64
    @NotNull
65
    @Override
66
    public NodePostProcessor apply( @NotNull final Document document ) {
67
      fireNewOutlineEvent();
68
      return new HeadingNodePostProcessor();
69
    }
70
  }
71
}
172
A src/main/java/com/keenwrite/processors/markdown/extensions/EmptyNode.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions;
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
  private static final BasedSequence[] BASE_SEQ = new BasedSequence[ 0 ];
18
19
  private EmptyNode() {
20
  }
21
22
  @Override
23
  public @NotNull BasedSequence[] getSegments() {
24
    return BASE_SEQ;
25
  }
26
}
127
A src/main/java/com/keenwrite/processors/markdown/extensions/HtmlRendererAdapter.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions;
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/ImageLinkExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.processors.ProcessorContext;
6
import com.vladsch.flexmark.ast.Image;
7
import com.vladsch.flexmark.html.IndependentLinkResolverFactory;
8
import com.vladsch.flexmark.html.LinkResolver;
9
import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
10
import com.vladsch.flexmark.html.renderer.ResolvedLink;
11
import com.vladsch.flexmark.util.ast.Node;
12
import org.jetbrains.annotations.NotNull;
13
14
import java.io.File;
15
import java.nio.file.Path;
16
import java.util.Optional;
17
18
import static com.keenwrite.events.StatusEvent.clue;
19
import static com.keenwrite.io.SysFile.toFile;
20
import static com.keenwrite.util.ProtocolScheme.getProtocol;
21
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
22
import static com.vladsch.flexmark.html.renderer.LinkStatus.VALID;
23
24
/**
25
 * Responsible for ensuring that images can be rendered relative to a path.
26
 * This allows images to be located virtually anywhere.
27
 */
28
public class ImageLinkExtension extends HtmlRendererAdapter {
29
30
  private final ProcessorContext mContext;
31
32
  private ImageLinkExtension( @NotNull final ProcessorContext context ) {
33
    mContext = context;
34
  }
35
36
  /**
37
   * Creates an extension capable of using a relative path to embed images.
38
   *
39
   * @param context Contains the base directory to search in for images.
40
   * @return The new {@link ImageLinkExtension}, not {@code null}.
41
   */
42
  public static ImageLinkExtension create(
43
    @NotNull final ProcessorContext context ) {
44
    return new ImageLinkExtension( context );
45
  }
46
47
  @Override
48
  public void extend( @NotNull final Builder builder,
49
                      @NotNull final String rendererType ) {
50
    builder.linkResolverFactory( new ResolverFactory() );
51
  }
52
53
  private final class ResolverFactory extends IndependentLinkResolverFactory {
54
    @Override
55
    public @NotNull LinkResolver apply(
56
      @NotNull final LinkResolverBasicContext context ) {
57
      return new ImageLinkResolver();
58
    }
59
  }
60
61
  private class ImageLinkResolver implements LinkResolver {
62
    public ImageLinkResolver() {
63
    }
64
65
    @NotNull
66
    @Override
67
    public ResolvedLink resolveLink(
68
      @NotNull final Node node,
69
      @NotNull final LinkResolverBasicContext context,
70
      @NotNull final ResolvedLink link ) {
71
      return node instanceof Image ? forImage( link, node ) : link;
72
    }
73
74
    /**
75
     * Algorithm:
76
     * <ol>
77
     *   <li>Accept remote URLs as valid links.</li>
78
     *   <li>Accept existing readable files as valid links.</li>
79
     *   <li>Accept non-{@link ExportFormat#NONE} exports as valid links.</li>
80
     *   <li>Append the images dir to the edited file's dir (baseDir).</li>
81
     *   <li>Search for images by extension.</li>
82
     * </ol>
83
     *
84
     * @param link The link URL to resolve.
85
     * @param node The document node containing the URL.
86
     * @return The {@link ResolvedLink} instance used to render the link.
87
     */
88
    private ResolvedLink forImage( final ResolvedLink link, final Node node ) {
89
      final var url = link.getUrl();
90
      final var protocolScheme = getProtocol( url );
91
92
      return protocolScheme.isRemote()
93
        ? valid( link, url )
94
        : resolveImageFile( link, node, url );
95
    }
96
97
    private ResolvedLink resolveImageFile(
98
      final ResolvedLink link,
99
      final Node node,
100
      final String url ) {
101
      final var userPath = new File( url );
102
103
      // If the user specified a fully qualified path name, use it verbatim.
104
      return readable( userPath )
105
        ? valid( link, url )
106
        : resolveUnqualifiedImageFile( link, node, url );
107
    }
108
109
    private ResolvedLink resolveUnqualifiedImageFile(
110
      final ResolvedLink link,
111
      final Node node,
112
      final String url ) {
113
      final var baseDir = getBaseDir();
114
      final var fqfn = baseDir.resolve( Path.of( url ) );
115
116
      // If the image can be found relative to the base directory, then
117
      // use the link as is when resolving the path.
118
      return readable( toFile( fqfn ) )
119
        ? valid( link, url )
120
        : resolveExtensionlessImageFile( link, node, url );
121
    }
122
123
    private ResolvedLink resolveExtensionlessImageFile(
124
      final ResolvedLink link,
125
      final Node node,
126
      final String url
127
    ) {
128
      final var imagePath = new File( url );
129
      final var file = resolveImageExtension( imagePath );
130
131
      return file.isPresent() && readable( file.get() )
132
        ? valid( link, file.get().toString() )
133
        : resolveRelativeImageFile( link, node, url );
134
    }
135
136
    private ResolvedLink resolveRelativeImageFile(
137
      final ResolvedLink link,
138
      final Node node,
139
      final String url ) {
140
      final var baseDir = getBaseDir();
141
142
      try {
143
        // Compute the path to the image file. The base directory should
144
        // be an absolute path to the file being edited, without an extension.
145
        final var imagesDir = getImageDir();
146
        final var baseImagesDir = baseDir.resolve( imagesDir );
147
        final var imagePath = baseImagesDir.resolve( url );
148
        final var file = resolveImageExtension( toFile( imagePath ) );
149
150
        if( file.isPresent() ) {
151
          final var resolved = imagesDir.resolve( file.get().toPath() );
152
          final var relative = baseDir.relativize( resolved );
153
          return valid( link, relative.toString() );
154
        }
155
156
        clue( "Main.status.error.file.missing.near",
157
              imagePath + ".*", node.getLineNumber()
158
        );
159
      } catch( final Exception ex ) {
160
        clue( ex );
161
      }
162
163
      return link;
164
    }
165
166
    private Optional<File> resolveImageExtension( final File imagePath ) {
167
      for( final var ext : getImageOrder() ) {
168
        final var file = new File( imagePath.toString() + '.' + ext );
169
170
        if( readable( file ) ) {
171
          return Optional.of( file );
172
        }
173
      }
174
175
      return Optional.empty();
176
    }
177
178
    private ResolvedLink valid( final ResolvedLink link, final String url ) {
179
      return link.withStatus( VALID ).withUrl( url );
180
    }
181
182
    private Path getImageDir() {
183
      return mContext.getImageDir();
184
    }
185
186
    private Iterable<String> getImageOrder() {
187
      return mContext.getImageOrder();
188
    }
189
190
    private Path getBaseDir() {
191
      return mContext.getBaseDir();
192
    }
193
  }
194
195
  private static boolean readable( final File file ) {
196
    return file.isFile() && file.canRead();
197
  }
198
}
1199
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/ClosingDivBlock.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences;
3
4
import com.vladsch.flexmark.html.HtmlWriter;
5
6
/**
7
 * Responsible for helping to generate a closing div element.
8
 */
9
class ClosingDivBlock extends DivBlock {
10
  @Override
11
  void export( final HtmlWriter html ) {
12
    html.closeTag( HTML_DIV );
13
  }
14
}
115
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/DivBlock.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences;
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
abstract class DivBlock extends Block {
10
  static final CharSequence HTML_DIV = "div";
11
12
  @Override
13
  @NotNull
14
  public BasedSequence[] getSegments() {
15
    return EMPTY_SEGMENTS;
16
  }
17
18
  /**
19
   * Append an opening or closing HTML div element to the given writer.
20
   *
21
   * @param html Builds the HTML document to be written.
22
   */
23
  abstract void export( HtmlWriter html );
24
}
125
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedBlockExtension.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.VariableProcessor;
8
import com.keenwrite.processors.markdown.MarkdownProcessor;
9
import com.keenwrite.processors.markdown.extensions.HtmlRendererAdapter;
10
import com.keenwrite.processors.r.RChunkEvaluator;
11
import com.keenwrite.processors.r.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.processors.IdentityProcessor.IDENTITY;
28
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
29
import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT;
30
import static com.vladsch.flexmark.html.renderer.LinkType.LINK;
31
import static java.lang.String.format;
32
33
/**
34
 * Responsible for converting textual diagram descriptions into HTML image
35
 * elements.
36
 */
37
public final class FencedBlockExtension extends HtmlRendererAdapter {
38
  private static final String TEMP_DIR = System.getProperty( "java.io.tmpdir" );
39
40
  /**
41
   * Ensure that the device is always closed to prevent an out-of-resources
42
   * error, regardless of whether the R expression the user tries to evaluate
43
   * is valid by swallowing errors alongside a {@code finally} block.
44
   */
45
  private static final String R_SVG_EXPORT =
46
    "tryCatch({svg('%s'%s)%n%s%n},finally={dev.off()})%n";
47
48
  private static final String STYLE_DIAGRAM = "diagram-";
49
  private static final int STYLE_DIAGRAM_LEN = STYLE_DIAGRAM.length();
50
51
  private static final String STYLE_R_CHUNK = "{r";
52
53
  private static final class VerbatimRVariableProcessor
54
    extends RVariableProcessor {
55
56
    public VerbatimRVariableProcessor(
57
      final Processor<String> successor, final ProcessorContext context ) {
58
      super( successor, context );
59
    }
60
61
    @Override
62
    protected String processValue( final String value ) {
63
      return value;
64
    }
65
  }
66
67
  private final RChunkEvaluator mRChunkEvaluator;
68
  private final Function<String, String> mInlineEvaluator;
69
70
  private final Processor<String> mRVariableProcessor;
71
  private final ProcessorContext mContext;
72
73
  public FencedBlockExtension(
74
    final Processor<String> processor,
75
    final Function<String, String> evaluator,
76
    final ProcessorContext context ) {
77
    assert processor != null;
78
    assert context != null;
79
    mContext = context;
80
    mRChunkEvaluator = new RChunkEvaluator();
81
    mInlineEvaluator = evaluator;
82
    mRVariableProcessor = new VerbatimRVariableProcessor( IDENTITY, context );
83
  }
84
85
  /**
86
   * Creates a new parser for fenced blocks. This calls out to a web service
87
   * to generate SVG files of text diagrams.
88
   * <p>
89
   * Internally, this creates a {@link VariableProcessor} to substitute
90
   * variable definitions. This is necessary because the order of processors
91
   * matters. If the {@link VariableProcessor} comes before an instance of
92
   * {@link MarkdownProcessor}, for example, then the caret position in the
93
   * preview pane will not align with the caret position in the editor
94
   * pane. The {@link MarkdownProcessor} must come before all else. However,
95
   * when parsing fenced blocks, the variables within the block must be
96
   * interpolated before being sent to the diagram web service.
97
   * </p>
98
   *
99
   * @param processor Used to pre-process the text.
100
   * @return A new {@link FencedBlockExtension} capable of shunting ASCII
101
   * diagrams to a service for conversion to SVG.
102
   */
103
  public static FencedBlockExtension create(
104
    final Processor<String> processor,
105
    final Function<String, String> evaluator,
106
    final ProcessorContext context ) {
107
    assert processor != null;
108
    assert context != null;
109
    return new FencedBlockExtension( processor, evaluator, context );
110
  }
111
112
  @Override
113
  public void extend(
114
    @NotNull final Builder builder, @NotNull final String rendererType ) {
115
    builder.nodeRendererFactory( new Factory() );
116
  }
117
118
  /**
119
   * Converts the given {@link BasedSequence} to a lowercase value.
120
   *
121
   * @param text The character string to convert to lowercase.
122
   * @return The lowercase text value, or the empty string for no text.
123
   */
124
  private static String sanitize( final BasedSequence text ) {
125
    assert text != null;
126
    return text.toString().toLowerCase();
127
  }
128
129
  /**
130
   * Responsible for generating images from a fenced block that contains a
131
   * diagram reference.
132
   */
133
  private class CustomRenderer implements NodeRenderer {
134
135
    @Override
136
    public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
137
      final var set = new HashSet<NodeRenderingHandler<?>>();
138
139
      set.add( new NodeRenderingHandler<>(
140
        FencedCodeBlock.class, ( node, context, html ) -> {
141
        final var style = sanitize( node.getInfo() );
142
        final Tuple<String, ResolvedLink> imagePair;
143
144
        if( style.startsWith( STYLE_DIAGRAM ) ) {
145
          imagePair = importTextDiagram( style, node, context );
146
147
          html.attr( "src", imagePair.item1() );
148
          html.withAttr( imagePair.item2() );
149
          html.tagVoid( "img" );
150
        }
151
        else if( style.startsWith( STYLE_R_CHUNK ) ) {
152
          imagePair = evaluateRChunk( node, context );
153
154
          html.attr( "src", imagePair.item1() );
155
          html.withAttr( imagePair.item2() );
156
          html.tagVoid( "img" );
157
        }
158
        else {
159
          // TODO: Revert to using context.delegateRender() after flexmark
160
          //   is updated to no longer trim blank lines up to the EOL.
161
          render( node, context, html );
162
        }
163
      } ) );
164
165
      return set;
166
    }
167
168
    private Tuple<String, ResolvedLink> importTextDiagram(
169
      final String style,
170
      final FencedCodeBlock node,
171
      final NodeRendererContext context ) {
172
173
      final var type = style.substring( STYLE_DIAGRAM_LEN );
174
      final var content = node.getContentChars().normalizeEOL();
175
      final var text = mInlineEvaluator.apply( content );
176
      final var server = mContext.getImageServer();
177
      final var source = DiagramUrlGenerator.toUrl( server, type, text );
178
      final var link = context.resolveLink( LINK, source, false );
179
180
      return new Tuple<>( source, link );
181
    }
182
183
    /**
184
     * Evaluates an R expression. This will take into consideration any
185
     * key/value pairs passed in from the document, such as width and height
186
     * attributes of the form: <code>{r width=5 height=5}</code>.
187
     *
188
     * @param node    The {@link FencedCodeBlock} to evaluate using R.
189
     * @param context Used to resolve the link that refers to any resulting
190
     *                image produced by the R chunk (such as a plot).
191
     * @return The SVG text string associated with the content produced by
192
     * the chunk (such as a graphical data plot).
193
     */
194
    @SuppressWarnings( "unused" )
195
    private Tuple<String, ResolvedLink> evaluateRChunk(
196
      final FencedCodeBlock node,
197
      final NodeRendererContext context ) {
198
      final var content = node.getContentChars().normalizeEOL().trim();
199
      final var text = mRVariableProcessor.apply( content );
200
      final var hash = Integer.toHexString( text.hashCode() );
201
      final var filename = format( "%s-%s.svg", APP_TITLE_LOWERCASE, hash );
202
203
      // The URI helps convert backslashes to forward slashes.
204
      final var uri = Path.of( TEMP_DIR, filename ).toUri();
205
      final var svg = uri.getPath();
206
      final var link = context.resolveLink( LINK, svg, false );
207
      final var dimensions = getAttributes( node.getInfo() );
208
      final var r = format( R_SVG_EXPORT, svg, dimensions, text );
209
210
      mRChunkEvaluator.apply( r );
211
212
      return new Tuple<>( svg, link );
213
    }
214
215
    /**
216
     * Splits attributes of the form <code>{r key1=value2 key2=value2}</code>
217
     * into a comma-separated string containing only the key/value pairs,
218
     * such as <code>key1=value1,key2=value2</code>.
219
     *
220
     * @param bs The complete line after the fenced block demarcation.
221
     * @return A comma-separated string of name/value pairs.
222
     */
223
    private String getAttributes( final BasedSequence bs ) {
224
      final var result = new StringBuilder();
225
      final var split = bs.splitList( " " );
226
      final var splits = split.size();
227
228
      for( var i = 1; i < splits; i++ ) {
229
        final var based = split.get( i ).toString();
230
        final var attribute = based.replace( '}', ' ' );
231
232
        // The order of attribute evaluations is in order of performance.
233
        if( !attribute.isBlank() &&
234
          attribute.indexOf( '=' ) > 1 &&
235
          attribute.matches( ".*\\d.*" ) ) {
236
237
          // The comma will do double-duty for separating individual attributes
238
          // as well as being the comma that separates all attributes from the
239
          // SVG image file name.
240
          result.append( ',' ).append( attribute );
241
        }
242
      }
243
244
      return result.toString();
245
    }
246
247
    /**
248
     * This method is a stop-gap because blank lines that contain only
249
     * whitespace are collapsed into lines without any spaces. Consequently,
250
     * the typesetting software does not honour the blank lines, which
251
     * then would otherwise discard blank lines entirely.
252
     * <p>
253
     * Given the following:
254
     *
255
     * <pre>
256
     *   if( bool ) {
257
     *
258
     *
259
     *   }
260
     * </pre>
261
     * <p>
262
     * The typesetter would otherwise render this incorrectly as:
263
     *
264
     * <pre>
265
     *   if( bool ) {
266
     *   }
267
     * </pre>
268
     * <p>
269
     */
270
    private void render(
271
      final FencedCodeBlock node,
272
      final NodeRendererContext context,
273
      final HtmlWriter html ) {
274
      assert node != null;
275
      assert context != null;
276
      assert html != null;
277
278
      html.line();
279
      html.srcPosWithTrailingEOL( node.getChars() )
280
          .withAttr()
281
          .tag( "pre" )
282
          .openPre();
283
284
      final var options = context.getHtmlOptions();
285
      final var languageClass = lookupLanguageClass( node, options );
286
287
      if( !languageClass.isBlank() ) {
288
        html.attr( "class", languageClass );
289
      }
290
291
      html.srcPosWithEOL( node.getContentChars() )
292
          .withAttr( CODE_CONTENT )
293
          .tag( "code" );
294
295
      final var lines = node.getContentLines();
296
297
      for( final var line : lines ) {
298
        if( line.isBlank() ) {
299
          html.text( "    " );
300
        }
301
302
        html.text( line );
303
      }
304
305
      html.tag( "/code" );
306
      html.tag( "/pre" )
307
          .closePre();
308
      html.lineIf( options.htmlBlockCloseTagEol );
309
    }
310
311
    private String lookupLanguageClass(
312
      final FencedCodeBlock node,
313
      final HtmlRendererOptions options ) {
314
      assert node != null;
315
      assert options != null;
316
317
      final var info = node.getInfo();
318
319
      if( info.isNotNull() && !info.isBlank() ) {
320
        final var lang = node
321
          .getInfoDelimitedByAny( options.languageDelimiterSet )
322
          .unescape();
323
        return options
324
          .languageClassMap
325
          .getOrDefault( lang, options.languageClassPrefix + lang );
326
      }
327
328
      return options.noLanguageClass;
329
    }
330
  }
331
332
  private class Factory implements DelegatingNodeRendererFactory {
333
    public Factory() { }
334
335
    @NotNull
336
    @Override
337
    public NodeRenderer apply( @NotNull final DataHolder options ) {
338
      return new CustomRenderer();
339
    }
340
341
    /**
342
     * Return {@code null} to indicate this may delegate to the core renderer.
343
     */
344
    @Override
345
    public Set<Class<?>> getDelegates() {
346
      return null;
347
    }
348
  }
349
}
1350
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivExtension.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.processors.markdown.extensions.HtmlRendererAdapter;
5
import com.vladsch.flexmark.html.HtmlRenderer;
6
import com.vladsch.flexmark.parser.Parser;
7
import com.vladsch.flexmark.parser.block.*;
8
import com.vladsch.flexmark.util.ast.Block;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
import com.vladsch.flexmark.util.data.MutableDataHolder;
11
import com.vladsch.flexmark.util.html.Attribute;
12
import com.vladsch.flexmark.util.html.AttributeImpl;
13
import org.jetbrains.annotations.NotNull;
14
import org.jetbrains.annotations.Nullable;
15
16
import java.util.ArrayList;
17
import java.util.Set;
18
import java.util.regex.Pattern;
19
20
import static com.vladsch.flexmark.parser.Parser.ParserExtension;
21
22
/**
23
 * Responsible for parsing div block syntax into HTML div tags. Fenced div
24
 * blocks start with three or more consecutive colons, followed by a space,
25
 * followed by attributes. The attributes can be either a single word, or
26
 * multiple words nested in braces. For example:
27
 *
28
 * <p>
29
 * ::: poem
30
 * Tyger Tyger, burning bright,
31
 * In the forests of the night;
32
 * What immortal hand or eye,
33
 * Could frame thy fearful symmetry?
34
 * :::
35
 * </p>
36
 * <p>
37
 * As well as:
38
 * </p>
39
 * <p>
40
 * ::: {#verse .p .d k=v author="Emily Dickinson"}
41
 * Because I could not stop for Death --
42
 * He kindly stopped for me --
43
 * The Carriage held but just Ourselves --
44
 * And Immortality.
45
 * :::
46
 * </p>
47
 *
48
 * <p>
49
 * The second example produces the following starting {@code div} element:
50
 * </p>
51
 * <p>
52
 * &lt;div id="verse" class="p d" data-k="v" data-author="Emily Dickson"&gt;
53
 * </p>
54
 */
55
public class FencedDivExtension extends HtmlRendererAdapter
56
  implements ParserExtension {
57
  /**
58
   * Matches any number of colons at start of line. This will match both the
59
   * opening and closing fences, with any number of colons.
60
   */
61
  private static final Pattern FENCE = Pattern.compile( "^:::.*" );
62
63
  /**
64
   * After a fenced div is detected, this will match the opening fence.
65
   */
66
  private static final Pattern FENCE_OPENING = Pattern.compile(
67
    "^:::+\\s+([\\p{IsAlphabetic}\\p{IsDigit}-_]+|\\{.+})\\s*$" );
68
69
  /**
70
   * Matches whether extended syntax is being used.
71
   */
72
  private static final Pattern ATTR_CSS = Pattern.compile( "\\{(.+)}" );
73
74
  /**
75
   * Matches either individual CSS definitions (id/class, {@code <d>}) or
76
   * key/value pairs ({@code <k>} and {@link <v>}). The key/value pair
77
   * will match optional quotes.
78
   */
79
  private static final Pattern ATTR_PAIRS = Pattern.compile(
80
    "\\s*" +
81
      "(?<d>[#.][\\p{IsAlphabetic}\\p{IsDigit}-_]+[^\\s=])|" +
82
      "((?<k>[\\p{IsAlphabetic}\\p{IsDigit}-_]+)=" +
83
      "\"*(?<v>(?<=\")[^\"]+(?=\")|([^\\s]+))\"*)" );
84
85
  public static FencedDivExtension create() {
86
    return new FencedDivExtension();
87
  }
88
89
  @Override
90
  public void parserOptions( final MutableDataHolder options ) {
91
  }
92
93
  @Override
94
  public void extend( final Parser.Builder builder ) {
95
    builder.customBlockParserFactory( new Factory() );
96
  }
97
98
  /**
99
   * Creates a renderer that can generate HTML div elements.
100
   *
101
   * @param builder      The document builder.
102
   * @param rendererType Indicates the document type to be built.
103
   */
104
  @Override
105
  public void extend( @NotNull final HtmlRenderer.Builder builder,
106
                      @NotNull final String rendererType ) {
107
    if( "HTML".equalsIgnoreCase( rendererType ) ) {
108
      builder.nodeRendererFactory( new FencedDivRenderer.Factory() );
109
    }
110
  }
111
112
  /**
113
   * Responsible for creating an instance of {@link ParserFactory}.
114
   */
115
  private static class Factory implements CustomBlockParserFactory {
116
    @Override
117
    public @NotNull BlockParserFactory apply(
118
      @NotNull final DataHolder options ) {
119
      return new ParserFactory( options );
120
    }
121
122
    @Override
123
    public @Nullable Set<Class<?>> getAfterDependents() { return null; }
124
125
    @Override
126
    public @Nullable Set<Class<?>> getBeforeDependents() { return null; }
127
128
    @Override
129
    public boolean affectsGlobalScope() { return false; }
130
  }
131
132
  /**
133
   * Responsible for creating a fenced div parser that is appropriate for the
134
   * type of fenced div encountered: opening or closing.
135
   */
136
  private static class ParserFactory extends AbstractBlockParserFactory {
137
    public ParserFactory( final DataHolder options ) {
138
      super( options );
139
    }
140
141
    /**
142
     * Try to match an opening or closing fenced div.
143
     *
144
     * @param state              Block parser state.
145
     * @param matchedBlockParser Last matched open block parser.
146
     * @return Wrapper for the opening or closing parser, upon finding :::.
147
     */
148
    @Override
149
    public BlockStart tryStart(
150
      final ParserState state, final MatchedBlockParser matchedBlockParser ) {
151
      return
152
        state.getIndent() == 0 && FENCE.matcher( state.getLine() ).matches()
153
          ? parseFence( state )
154
          : BlockStart.none();
155
    }
156
157
    /**
158
     * After finding a fenced div, this will further disambiguate an opening
159
     * from a closing fence.
160
     *
161
     * @param state Block parser state, contains line to parse.
162
     * @return Wrapper for the opening or closing parser, upon finding :::.
163
     */
164
    private BlockStart parseFence( final ParserState state ) {
165
      final var fence = FENCE_OPENING.matcher( state.getLine() );
166
167
      return BlockStart.of(
168
        fence.matches()
169
          ? new OpeningParser( fence.group( 1 ) )
170
          : new ClosingParser()
171
      ).atIndex( state.getIndex() );
172
    }
173
  }
174
175
  /**
176
   * Abstracts common {@link OpeningParser} and {@link ClosingParser} methods.
177
   */
178
  private static abstract class DivBlockParser extends AbstractBlockParser {
179
    @Override
180
    public BlockContinue tryContinue( final ParserState state ) {
181
      return BlockContinue.none();
182
    }
183
184
    @Override
185
    public void closeBlock( final ParserState state ) {}
186
  }
187
188
  /**
189
   * Responsible for creating an instance of {@link OpeningDivBlock}.
190
   */
191
  private static class OpeningParser extends DivBlockParser {
192
    private final OpeningDivBlock mBlock;
193
194
    /**
195
     * Parses the arguments upon construction.
196
     *
197
     * @param args Text after :::, excluding leading/trailing whitespace.
198
     */
199
    public OpeningParser( final String args ) {
200
      final var attrs = new ArrayList<Attribute>();
201
      final var cssMatcher = ATTR_CSS.matcher( args );
202
203
      if( cssMatcher.matches() ) {
204
        // Split the text between braces into tokens and/or key-value pairs.
205
        final var pairMatcher = ATTR_PAIRS.matcher( cssMatcher.group( 1 ) );
206
207
        while( pairMatcher.find() ) {
208
          final var cssDef = pairMatcher.group( "d" );
209
          String cssAttrKey = "class";
210
          String cssAttrVal;
211
212
          // When no regular CSS definition (id or class), use key/value pairs.
213
          if( cssDef == null ) {
214
            cssAttrKey = "data-" + pairMatcher.group( "k" );
215
            cssAttrVal = pairMatcher.group( "v" );
216
          }
217
          else {
218
            // This will strip the "#" and "." off the start of CSS definition.
219
            var index = 1;
220
221
            // Default CSS attribute name is "class", switch to "id" for #.
222
            if( cssDef.startsWith( "#" ) ) {
223
              cssAttrKey = "id";
224
            }
225
            else if( !cssDef.startsWith( "." ) ) {
226
              index = 0;
227
            }
228
229
            cssAttrVal = cssDef.substring( index );
230
          }
231
232
          attrs.add( AttributeImpl.of( cssAttrKey, cssAttrVal ) );
233
        }
234
      }
235
      else {
236
        attrs.add( AttributeImpl.of( "class", args ) );
237
      }
238
239
      mBlock = new OpeningDivBlock( attrs );
240
    }
241
242
    @Override
243
    public Block getBlock() {
244
      return mBlock;
245
    }
246
  }
247
248
  /**
249
   * Responsible for creating an instance of {@link ClosingDivBlock}.
250
   */
251
  private static class ClosingParser extends DivBlockParser {
252
    private final ClosingDivBlock mBlock = new ClosingDivBlock();
253
254
    @Override
255
    public Block getBlock() {
256
      return mBlock;
257
    }
258
  }
259
}
1260
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivRenderer.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences;
3
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.html.renderer.NodeRenderer;
6
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
7
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
8
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
import org.jetbrains.annotations.NotNull;
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
 * div elements.
18
 */
19
class FencedDivRenderer implements NodeRenderer {
20
  @Override
21
  public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
22
    return Set.of(
23
      new NodeRenderingHandler<>( OpeningDivBlock.class, this::render ),
24
      new NodeRenderingHandler<>( ClosingDivBlock.class, this::render )
25
    );
26
  }
27
28
  /**
29
   * Renders the opening fenced div block as an HTML {@code <div>} element.
30
   */
31
  void render( final OpeningDivBlock node,
32
               final NodeRendererContext context,
33
               final HtmlWriter html ) {
34
    node.export( html );
35
  }
36
37
  /**
38
   * Renders the closing fenced div block as an HTML {@code </div>} element.
39
   */
40
  void render( final ClosingDivBlock node,
41
               final NodeRendererContext context,
42
               final HtmlWriter html ) {
43
    node.export( html );
44
  }
45
46
  static class Factory implements NodeRendererFactory {
47
    @Override
48
    public @NotNull NodeRenderer apply( @NotNull final DataHolder options ) {
49
      return new FencedDivRenderer();
50
    }
51
  }
52
}
153
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/OpeningDivBlock.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences;
3
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.util.html.Attribute;
6
7
import java.util.ArrayList;
8
import java.util.List;
9
10
/**
11
 * Responsible for helping to generate an opening div element.
12
 */
13
class OpeningDivBlock extends DivBlock {
14
  private final List<Attribute> mAttributes = new ArrayList<>();
15
16
  OpeningDivBlock( final List<Attribute> attributes ) {
17
    assert attributes != null;
18
    mAttributes.addAll( attributes );
19
  }
20
21
  void export( final HtmlWriter html ) {
22
    mAttributes.forEach( html::attr );
23
    html.withAttr().tag( HTML_DIV );
24
  }
25
}
126
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.r.RInlineEvaluator;
8
import com.vladsch.flexmark.ast.Paragraph;
9
import com.vladsch.flexmark.parser.InlineParserExtensionFactory;
10
import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
11
import com.vladsch.flexmark.parser.internal.InlineParserImpl;
12
import com.vladsch.flexmark.parser.internal.LinkRefProcessorData;
13
import com.vladsch.flexmark.util.data.DataHolder;
14
import com.vladsch.flexmark.util.data.MutableDataHolder;
15
16
import java.util.BitSet;
17
import java.util.List;
18
import java.util.Map;
19
20
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
21
import static com.vladsch.flexmark.parser.Parser.Builder;
22
import static com.vladsch.flexmark.parser.Parser.ParserExtension;
23
24
/**
25
 * Responsible for processing inline R statements (denoted using the
26
 * {@link RInlineEvaluator#PREFIX}) to prevent them from being converted to
27
 * HTML {@code <code>} elements and stop them from interfering with TeX
28
 * statements. Note that TeX statements are processed using a Markdown
29
 * extension, rather than an implementation of {@link Processor}. For this
30
 * reason, some pre-conversion is necessary.
31
 */
32
public final class RInlineExtension implements ParserExtension {
33
  private final RInlineEvaluator mEvaluator;
34
  private final BaseMarkdownProcessor mMarkdownProcessor;
35
36
  private RInlineExtension(
37
    final RInlineEvaluator evaluator,
38
    final ProcessorContext context ) {
39
    mEvaluator = evaluator;
40
    mMarkdownProcessor = new BaseMarkdownProcessor( IDENTITY, context );
41
  }
42
43
  /**
44
   * Creates an extension capable of intercepting R code blocks and preventing
45
   * them from being converted into HTML {@code <code>} elements.
46
   */
47
  public static RInlineExtension create(
48
    final RInlineEvaluator evaluator,
49
    final ProcessorContext context ) {
50
    return new RInlineExtension( evaluator, context );
51
  }
52
53
  @Override
54
  public void extend( final Builder builder ) {
55
    builder.customInlineParserFactory( InlineParser::new );
56
  }
57
58
  @Override
59
  public void parserOptions( final MutableDataHolder options ) {}
60
61
  /**
62
   * Prevents rendering {@code `r} statements as inline HTML {@code <code>}
63
   * blocks, which allows the {@link RInlineEvaluator} to post-process the
64
   * text prior to display in the preview pane. This intervention assists
65
   * with decoupling the caret from the Markdown content so that the two
66
   * can vary independently in the architecture while permitting synchronization
67
   * of the editor and preview pane.
68
   * <p>
69
   * The text is therefore processed twice: once by flexmark-java and once by
70
   * {@link RInlineEvaluator}.
71
   * </p>
72
   */
73
  private final class InlineParser extends InlineParserImpl {
74
    private InlineParser(
75
      final DataHolder options,
76
      final BitSet specialCharacters,
77
      final BitSet delimiterCharacters,
78
      final Map<Character, DelimiterProcessor> delimiterProcessors,
79
      final LinkRefProcessorData referenceLinkProcessors,
80
      final List<InlineParserExtensionFactory> inlineParserExtensions ) {
81
      super(
82
        options,
83
        specialCharacters,
84
        delimiterCharacters,
85
        delimiterProcessors,
86
        referenceLinkProcessors,
87
        inlineParserExtensions
88
      );
89
    }
90
91
    /**
92
     * The superclass handles a number backtick parsing edge cases; this method
93
     * changes the behaviour to retain R code snippets, identified by
94
     * {@link RInlineEvaluator#PREFIX}, so that subsequent processing can
95
     * invoke R. If other languages are added, the {@link InlineParser} will
96
     * have to be rewritten to identify more than merely R.
97
     *
98
     * @return The return value from {@link super#parseBackticks()}.
99
     * @inheritDoc
100
     */
101
    @Override
102
    protected boolean parseBackticks() {
103
      final var foundTicks = super.parseBackticks();
104
105
      if( foundTicks ) {
106
        final var blockNode = getBlock();
107
        final var codeNode = blockNode.getLastChild();
108
109
        if( codeNode != null ) {
110
          final var code = codeNode.getChars().toString();
111
112
          if( mEvaluator.test( code ) ) {
113
            codeNode.unlink();
114
115
            final var rText = mEvaluator.apply( code );
116
            var node = mMarkdownProcessor.toNode( rText );
117
118
            if( node.getFirstChild() instanceof Paragraph paragraph ) {
119
              node = paragraph.getFirstChild();
120
            }
121
122
            if( node != null ) {
123
              blockNode.appendChild( node );
124
            }
125
          }
126
        }
127
      }
128
129
      return foundTicks;
130
    }
131
  }
132
}
1133
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.HtmlRendererAdapter;
7
import com.keenwrite.processors.markdown.extensions.tex.TexNodeRenderer.Factory;
8
import com.vladsch.flexmark.html.HtmlRenderer;
9
import com.vladsch.flexmark.parser.Parser;
10
import com.vladsch.flexmark.util.data.MutableDataHolder;
11
import org.jetbrains.annotations.NotNull;
12
13
import java.util.function.Function;
14
15
import static com.vladsch.flexmark.parser.Parser.ParserExtension;
16
17
/**
18
 * Responsible for wrapping delimited TeX code in Markdown into an XML element
19
 * that the HTML renderer can handle. For example, {@code $E=mc^2$} becomes
20
 * {@code <tex>E=mc^2</tex>} when passed to HTML renderer. The HTML renderer
21
 * is responsible for converting the TeX code for display. This avoids inserting
22
 * SVG code into the Markdown document, which the parser would then have to
23
 * iterate---a <em>very</em> wasteful operation that impacts front-end
24
 * performance.
25
 */
26
public class TeXExtension extends HtmlRendererAdapter
27
  implements ParserExtension {
28
29
  /**
30
   * Responsible for pre-parsing the input.
31
   */
32
  private final Function<String, String> mEvaluator;
33
34
  /**
35
   * Controls how the node renderer produces TeX code within HTML output.
36
   */
37
  private final ExportFormat mExportFormat;
38
39
  private TeXExtension(
40
    final Function<String, String> evaluator, final ProcessorContext context  ) {
41
    mEvaluator = evaluator;
42
    mExportFormat = context.getExportFormat();
43
  }
44
45
  /**
46
   * Creates an extension capable of handling delimited TeX code in Markdown.
47
   *
48
   * @return The new {@link TeXExtension}, never {@code null}.
49
   */
50
  public static TeXExtension create(
51
    final Function<String, String> evaluator, final ProcessorContext context  ) {
52
    return new TeXExtension( evaluator, context );
53
  }
54
55
  /**
56
   * Adds the TeX extension for HTML document export types.
57
   *
58
   * @param builder      The document builder.
59
   * @param rendererType Indicates the document type to be built.
60
   */
61
  @Override
62
  public void extend( @NotNull final HtmlRenderer.Builder builder,
63
                      @NotNull final String rendererType ) {
64
    if( "HTML".equalsIgnoreCase( rendererType ) ) {
65
      builder.nodeRendererFactory( new Factory( mExportFormat, mEvaluator ) );
66
    }
67
  }
68
69
  @Override
70
  public void extend( final Parser.Builder builder ) {
71
    builder.customDelimiterProcessor( new TeXInlineDelimiterProcessor() );
72
  }
73
74
  @Override
75
  public void parserOptions( final MutableDataHolder options ) {
76
  }
77
}
178
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
public class TeXInlineDelimiterProcessor implements DelimiterProcessor {
11
12
  @Override
13
  public void process( final Delimiter opener,
14
                       final Delimiter closer,
15
                       final int delimitersUsed ) {
16
    final var node = new TexNode( opener, closer );
17
    opener.moveNodesBetweenDelimitersTo( node, closer );
18
  }
19
20
  @Override
21
  public char getOpeningCharacter() {
22
    return '$';
23
  }
24
25
  @Override
26
  public char getClosingCharacter() {
27
    return '$';
28
  }
29
30
  @Override
31
  public int getMinLength() {
32
    return 1;
33
  }
34
35
  /**
36
   * Allow for $ or $$.
37
   *
38
   * @param opener One or more opening delimiter characters.
39
   * @param closer One or more closing delimiter characters.
40
   * @return The number of delimiters to use to determine whether a valid
41
   * opening delimiter expression is found.
42
   */
43
  @Override
44
  public int getDelimiterUse(
45
    final DelimiterRun opener, final DelimiterRun closer ) {
46
    return 1;
47
  }
48
49
  @Override
50
  public boolean canBeOpener( final String before,
51
                              final String after,
52
                              final boolean leftFlanking,
53
                              final boolean rightFlanking,
54
                              final boolean beforeIsPunctuation,
55
                              final boolean afterIsPunctuation,
56
                              final boolean beforeIsWhitespace,
57
                              final boolean afterIsWhiteSpace ) {
58
    return leftFlanking;
59
  }
60
61
  @Override
62
  public boolean canBeCloser( final String before,
63
                              final String after,
64
                              final boolean leftFlanking,
65
                              final boolean rightFlanking,
66
                              final boolean beforeIsPunctuation,
67
                              final boolean afterIsPunctuation,
68
                              final boolean beforeIsWhitespace,
69
                              final boolean afterIsWhiteSpace ) {
70
    return rightFlanking;
71
  }
72
73
  @Override
74
  public Node unmatchedDelimiterNode(
75
    final InlineParser inlineParser, final DelimiterRun delimiter ) {
76
    return null;
77
  }
78
79
  @Override
80
  public boolean skipNonOpenerCloser() {
81
    return false;
82
  }
83
}
184
A src/main/java/com/keenwrite/processors/markdown/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
   * TeX expression wrapped in a {@code <tex>} element.
10
   */
11
  public static final String HTML_TEX = "tex";
12
13
  public static final String TOKEN_OPEN = "$";
14
  public 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() { return mOpener; }
36
37
  /**
38
   * @return Either '$' or '$$'.
39
   */
40
  public String getClosingDelimiter() { return mCloser; }
41
42
  private String getDelimiter( final Delimiter delimiter ) {
43
    return delimiter.getInput().subSequence(
44
      delimiter.getStartIndex(), delimiter.getEndIndex()
45
    ).toString();
46
  }
47
}
148
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 TexDelimNodeRenderer(),
33
      XHTML_TEX, new TexElementNodeRenderer( true ),
34
      NONE, RENDERER
35
    );
36
37
  public static class Factory implements NodeRendererFactory {
38
    private final RendererFacade mNodeRenderer;
39
40
    public Factory(
41
      final ExportFormat exportFormat,
42
      final Function<String, String> evaluator ) {
43
      mNodeRenderer = EXPORT_RENDERERS.getOrDefault( exportFormat, RENDERER );
44
      mNodeRenderer.setEvaluator( evaluator );
45
    }
46
47
    @NotNull
48
    @Override
49
    public NodeRenderer apply( @NotNull final DataHolder options ) {
50
      return mNodeRenderer;
51
    }
52
  }
53
54
  private static abstract class RendererFacade
55
    implements NodeRenderer {
56
    private Function<String, String> mEvaluator;
57
58
    @Override
59
    public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
60
      return Set.of(
61
        new NodeRenderingHandler<>( TexNode.class, this::render )
62
      );
63
    }
64
65
    /**
66
     * Subclasses implement this method to render the content of {@link TexNode}
67
     * instances as per their associated {@link ExportFormat}.
68
     *
69
     * @param node    {@link Node} containing text content of a math formula.
70
     * @param context Configuration information (unused).
71
     * @param html    Where to write the rendered output.
72
     */
73
    abstract void render( final TexNode node,
74
                          final NodeRendererContext context,
75
                          final HtmlWriter html );
76
77
    private void setEvaluator( final Function<String, String> evaluator ) {
78
      mEvaluator = evaluator;
79
    }
80
81
    Function<String, String> getEvaluator() {
82
      return mEvaluator;
83
    }
84
  }
85
86
  /**
87
   * Responsible for rendering a TeX node as an HTML {@code <tex>}
88
   * element. This is the default behaviour.
89
   */
90
  private static class TexElementNodeRenderer extends RendererFacade {
91
    private final boolean mIncludeDelimiter;
92
93
    private TexElementNodeRenderer( final boolean includeDelimiter ) {
94
      mIncludeDelimiter = includeDelimiter;
95
    }
96
97
    void render( final TexNode node,
98
                 final NodeRendererContext context,
99
                 final HtmlWriter html ) {
100
      final var text = getEvaluator().apply( node.getText().toString() );
101
      final var content =
102
        mIncludeDelimiter
103
          ? node.getOpeningDelimiter() + text + node.getClosingDelimiter()
104
          : text;
105
      html.tag( HTML_TEX );
106
      html.raw( content );
107
      html.closeTag( HTML_TEX );
108
    }
109
  }
110
111
  /**
112
   * Responsible for rendering a TeX node as an HTML {@code <svg>}
113
   * element.
114
   */
115
  private static class TexSvgNodeRenderer extends RendererFacade {
116
    void render( final TexNode node,
117
                 final NodeRendererContext context,
118
                 final HtmlWriter html ) {
119
      final var tex = node.getText().toStringOrNull();
120
      final var doc = MathRenderer.toDocument(
121
        tex == null ? "" : getEvaluator().apply( tex )
122
      );
123
      final var svg = SvgRasterizer.toSvg( doc.getDocumentElement() );
124
      html.raw( svg );
125
    }
126
  }
127
128
  /**
129
   * Responsible for rendering a TeX node as text bracketed by $ tokens.
130
   */
131
  private static class TexDelimNodeRenderer extends RendererFacade {
132
    void render( final TexNode node,
133
                 final NodeRendererContext context,
134
                 final HtmlWriter html ) {
135
      html.raw( TOKEN_OPEN );
136
      html.raw( getEvaluator().apply( node.getText().toString() ) );
137
      html.raw( TOKEN_CLOSE );
138
    }
139
  }
140
}
1141
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.r.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/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
6
import java.util.function.Function;
7
import java.util.function.Predicate;
8
9
import static com.keenwrite.constants.Constants.STATUS_PARSE_ERROR;
10
import static com.keenwrite.events.StatusEvent.clue;
11
12
/**
13
 * Evaluates inline R statements.
14
 */
15
public final class RInlineEvaluator
16
  implements Function<String, String>, Predicate<String> {
17
  public static final String PREFIX = "`r#";
18
  public static final String SUFFIX = "`";
19
20
  private static final int PREFIX_LENGTH = PREFIX.length();
21
22
  private final Processor<String> mProcessor;
23
24
  /**
25
   * Constructs an evaluator capable of executing R statements.
26
   */
27
  public RInlineEvaluator( final RVariableProcessor processor ) {
28
    mProcessor = processor;
29
  }
30
31
  /**
32
   * Evaluates all R statements in the source document and inserts the
33
   * calculated value into the generated document.
34
   *
35
   * @param text The document text that includes variables that should be
36
   *             replaced with values when rendered as HTML.
37
   * @return The generated document with output from all R statements
38
   * substituted with value returned from their execution.
39
   */
40
  @Override
41
  public String apply( final String text ) {
42
    try {
43
      final var buffer = new StringBuilder( text.length() );
44
45
      int index = 0;
46
      int began;
47
      int ended = 0;
48
49
      while( (began = text.indexOf( PREFIX, index )) >= 0 && ended > -1 ) {
50
        buffer.append( text, index, began );
51
52
        // If the R expression has no definite end, this returns -1.
53
        ended = text.indexOf( SUFFIX, began + 1 );
54
55
        if( ended > began ) {
56
          final var r = mProcessor.apply(
57
            text.substring( began + PREFIX_LENGTH, ended )
58
          );
59
60
          // Return the evaluated R expression for insertion back into the text.
61
          buffer.append( Engine.eval( r ) );
62
63
          index = ended + 1;
64
        }
65
      }
66
67
      buffer.append( text.substring( index ) );
68
69
      return buffer.toString();
70
    } catch( final Exception ex ) {
71
      clue( STATUS_PARSE_ERROR, ex.getMessage() );
72
73
      // If the string couldn't be parsed using R, append the statement
74
      // that failed to parse, instead of its evaluated value.
75
      return text;
76
    }
77
  }
78
79
  /**
80
   * Answers whether the engine associated with this evaluator may attempt to
81
   * evaluate the given source code statement.
82
   *
83
   * @param code The source code to verify.
84
   * @return {@code true} if the code may be evaluated.
85
   */
86
  @Override
87
  public boolean test( final String code ) {
88
    return code.startsWith( PREFIX );
89
  }
90
}
191
A src/main/java/com/keenwrite/processors/r/RVariableProcessor.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.ProcessorContext;
6
import com.keenwrite.processors.VariableProcessor;
7
import com.keenwrite.sigils.RKeyOperator;
8
9
import java.util.function.UnaryOperator;
10
11
/**
12
 * Converts the keys of the resolved map from default form to R form, then
13
 * performs a substitution on the text. The default R variable syntax is
14
 * <pre>v$tree$leaf</pre>.
15
 */
16
public class RVariableProcessor extends VariableProcessor {
17
  public RVariableProcessor(
18
    final Processor<String> successor, final ProcessorContext context ) {
19
    super( successor, context );
20
  }
21
22
  @Override
23
  protected UnaryOperator<String> createKeyOperator(
24
    final ProcessorContext context ) {
25
    return new RKeyOperator();
26
  }
27
28
  @Override
29
  protected String processValue( final String value ) {
30
    assert value != null;
31
32
    return escape( value );
33
  }
34
35
  /**
36
   * In R, single quotes and double quotes are interchangeable. Using single
37
   * quotes is simpler to code.
38
   *
39
   * @param value The text to convert into a valid quoted R string.
40
   * @return The quoted value with embedded quotes escaped as necessary.
41
   */
42
  public static String escape( final String value ) {
43
    return '\'' + escape( value, '\'', "\\'" ) + '\'';
44
  }
45
46
  /**
47
   * TODO: Make generic method for replacing text.
48
   *
49
   * @param haystack Search this string for the needle, must not be null.
50
   * @param needle   The character to find in the haystack.
51
   * @param thread   Replace the needle with this text, if the needle is found.
52
   * @return The haystack with the all instances of needle replaced with thread.
53
   */
54
  @SuppressWarnings( "SameParameterValue" )
55
  private static String escape(
56
    final String haystack, final char needle, final String thread ) {
57
    assert haystack != null;
58
    assert thread != null;
59
60
    int end = haystack.indexOf( needle );
61
62
    if( end < 0 ) {
63
      return haystack;
64
    }
65
66
    int start = 0;
67
68
    // Replace up to 32 occurrences before reallocating the internal buffer.
69
    final var sb = new StringBuilder( haystack.length() + 32 );
70
71
    while( end >= 0 ) {
72
      sb.append( haystack, start, end ).append( thread );
73
      start = end + 1;
74
      end = haystack.indexOf( needle, start );
75
    }
76
77
    return sb.append( haystack.substring( start ) ).toString();
78
  }
79
}
180
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 org.apache.commons.lang3.StringUtils;
5
6
import java.util.Map;
7
8
import static org.apache.commons.lang3.StringUtils.replaceEach;
9
10
/**
11
 * Replaces text using a brute-force
12
 * {@link StringUtils#replaceEach(String, String[], String[])}} method.
13
 */
14
public class StringUtilsReplacer extends AbstractTextReplacer {
15
16
  /**
17
   * Default (empty) constructor.
18
   */
19
  protected StringUtilsReplacer() { }
20
21
  @Override
22
  public String replace( final String text, final Map<String, String> map ) {
23
    return replaceEach( text, keys( map ), values( map ) );
24
  }
25
}
126
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 java.util.Map;
5
6
/**
7
 * Used to generate a class capable of efficiently replacing variable
8
 * definitions with their values.
9
 */
10
public final class TextReplacementFactory {
11
12
  private static final TextReplacer APACHE = new StringUtilsReplacer();
13
  private static final TextReplacer AHO_CORASICK = new AhoCorasickReplacer();
14
15
  /**
16
   * Returns a text search/replacement instance that is reasonably optimal for
17
   * the given length of text.
18
   *
19
   * @param length The length of text that requires some search and replacing.
20
   * @return A class that can search and replace text with utmost expediency.
21
   */
22
  public static TextReplacer getTextReplacer( final int length ) {
23
    // After about 1,500 characters, the Aho-Corsick algorithm is faster.
24
    return length < 1500 ? APACHE : AHO_CORASICK;
25
  }
26
27
  /**
28
   * Convenience method to instantiate a suitable text replacer algorithm and
29
   * perform a replacement using the given map. At this point, the values should
30
   * be already dereferenced and ready to be substituted verbatim; any
31
   * recursively defined values must have been interpolated previously.
32
   *
33
   * @param haystack The text containing zero or more variables to replace.
34
   * @param needles  The map of variables to their dereferenced values.
35
   * @return The text with all variables replaced.
36
   */
37
  public static String replace(
38
    final String haystack, final Map<String, String> needles ) {
39
    assert haystack != null;
40
    assert needles != null;
41
42
    return getTextReplacer( haystack.length() ).replace( haystack, needles );
43
  }
44
}
145
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/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
  }
75
}
176
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 title;
14
  private final String content;
15
16
  /**
17
   * Constructs default message text for a notification.
18
   *
19
   * @param title   The message title.
20
   * @param message The message content (needs formatting).
21
   * @param args    The arguments to the message content that must be formatted.
22
   */
23
  public DefaultNotification(
24
      final String title,
25
      final String message,
26
      final Object... args ) {
27
    this.title = title;
28
    this.content = MessageFormat.format( message, args );
29
  }
30
31
  @Override
32
  public String getTitle() {
33
    return this.title;
34
  }
35
36
  @Override
37
  public String getContent() {
38
    return this.content;
39
  }
40
41
}
142
A src/main/java/com/keenwrite/service/events/impl/DefaultNotifier.java
1
/* Copyright 2020-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.service.Settings;
5
import org.apache.commons.configuration2.PropertiesConfiguration;
6
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
7
import org.apache.commons.configuration2.convert.ListDelimiterHandler;
8
9
import java.io.InputStreamReader;
10
import java.net.URL;
11
import java.nio.charset.Charset;
12
import java.util.Iterator;
13
import java.util.List;
14
15
import static com.keenwrite.constants.Constants.PATH_PROPERTIES_SETTINGS;
16
17
/**
18
 * Responsible for loading settings that help avoid hard-coded assumptions.
19
 */
20
public final class DefaultSettings implements Settings {
21
22
  private static final char VALUE_SEPARATOR = ',';
23
24
  private final PropertiesConfiguration mProperties = loadProperties();
25
26
  public DefaultSettings() {
27
  }
28
29
  /**
30
   * Returns the value of a string property.
31
   *
32
   * @param property     The property key.
33
   * @param defaultValue The value to return if no property key has been set.
34
   * @return The property key value, or defaultValue when no key found.
35
   */
36
  @Override
37
  public String getSetting( final String property, final String defaultValue ) {
38
    return getSettings().getString( property, defaultValue );
39
  }
40
41
  /**
42
   * Returns the value of a string property.
43
   *
44
   * @param property     The property key.
45
   * @param defaultValue The value to return if no property key has been set.
46
   * @return The property key value, or defaultValue when no key found.
47
   */
48
  @Override
49
  public int getSetting( final String property, final int defaultValue ) {
50
    return getSettings().getInt( property, defaultValue );
51
  }
52
53
  /**
54
   * Convert the generic list of property objects into strings.
55
   *
56
   * @param property The property value to coerce.
57
   * @param defaults The defaults values to use should the property be unset.
58
   * @return The list of properties coerced from objects to strings.
59
   */
60
  @Override
61
  public List<String> getStringSettingList(
62
      final String property, final List<String> defaults ) {
63
    return getSettings().getList( String.class, property, defaults );
64
  }
65
66
  /**
67
   * Convert a list of property objects into strings, with no default value.
68
   *
69
   * @param property The property value to coerce.
70
   * @return The list of properties coerced from objects to strings.
71
   */
72
  @Override
73
  public List<String> getStringSettingList( final String property ) {
74
    return getStringSettingList( property, null );
75
  }
76
77
  /**
78
   * Returns a list of property names that begin with the given prefix.
79
   *
80
   * @param prefix The prefix to compare against each property name.
81
   * @return The list of property names that have the given prefix.
82
   */
83
  @Override
84
  public Iterator<String> getKeys( final String prefix ) {
85
    return getSettings().getKeys( prefix );
86
  }
87
88
  private PropertiesConfiguration loadProperties() {
89
    final var url = getPropertySource();
90
    final var configuration = new PropertiesConfiguration();
91
92
    if( url != null ) {
93
      try( final var reader = new InputStreamReader(
94
          url.openStream(), getDefaultEncoding() ) ) {
95
        configuration.setListDelimiterHandler( createListDelimiterHandler() );
96
        configuration.read( reader );
97
      } catch( final Exception ex ) {
98
        throw new RuntimeException( ex );
99
      }
100
    }
101
102
    return configuration;
103
  }
104
105
  private Charset getDefaultEncoding() {
106
    return Charset.defaultCharset();
107
  }
108
109
  private ListDelimiterHandler createListDelimiterHandler() {
110
    return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
111
  }
112
113
  private URL getPropertySource() {
114
    return DefaultSettings.class.getResource( PATH_PROPERTIES_SETTINGS );
115
  }
116
117
  private PropertiesConfiguration getSettings() {
118
    return mProperties;
119
  }
120
}
1121
A src/main/java/com/keenwrite/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
import java.util.function.UnaryOperator;
7
8
/**
9
 * Converts dot-separated variable names into names compatible with R. That is,
10
 * {@code variable.name.qualified} becomes {@code v$variable$name$qualified}.
11
 */
12
public final class RKeyOperator implements UnaryOperator<String> {
13
  private static final char KEY_SEPARATOR_DEF = '.';
14
  private static final char KEY_SEPARATOR_R = '$';
15
16
  /** Minor optimization to avoid recreating an object. */
17
  private final StringBuilder mVarName = new StringBuilder( 128 );
18
19
  /** Optimization to avoid re-converting variable names into R format. */
20
  private final BoundedCache<String, String> mVariables = new BoundedCache<>(
21
    2048
22
  );
23
24
  /**
25
   * Constructs a new instance capable of converting dot-separated variable
26
   * names into R's dollar-symbol-separated names.
27
   */
28
  public RKeyOperator() { }
29
30
  /**
31
   * Transforms a definition key name into the expected format for an R
32
   * variable key name.
33
   * <p>
34
   * This algorithm is faster than {@link String#replace(char, char)}. Faster
35
   * still would be to cache the values, but that would mean managing the
36
   * cache when the user changes the beginning and ending of the R delimiters.
37
   * This code gives about a 2% performance boost when scrolling using
38
   * cursor keys. After the JIT warms up, this super-minor bottleneck vanishes.
39
   *
40
   * @param key The variable name to transform, neither blank nor {@code null}.
41
   * @return The transformed variable name.
42
   */
43
  @Override
44
  public String apply( final String key ) {
45
    assert key != null;
46
    assert key.length() > 0;
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( "Main.status.lexicon." + 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
    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
          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.file.Files.*;
25
import static java.util.Arrays.asList;
26
import static java.util.concurrent.TimeUnit.SECONDS;
27
import static org.apache.commons.io.FilenameUtils.removeExtension;
28
29
/**
30
 * Responsible for invoking an executable to typeset text. This will
31
 * construct suitable command-line arguments to invoke the typesetting engine.
32
 * This uses a version of the typesetter installed on the host system.
33
 */
34
public final class HostTypesetter extends Typesetter
35
  implements Callable<Boolean> {
36
  private static final SysFile TYPESETTER = new SysFile( TYPESETTER_EXE );
37
38
  HostTypesetter( final Mutator mutator ) {
39
    super( mutator );
40
  }
41
42
  /**
43
   * Answers whether the typesetting software is installed locally.
44
   *
45
   * @return {@code true} if the typesetting software is installed on the host.
46
   */
47
  public static boolean isReady() {
48
    return TYPESETTER.canRun();
49
  }
50
51
  /**
52
   * Launches a task to typeset a document.
53
   */
54
  private class TypesetTask implements Callable<Boolean> {
55
    private final List<String> mArgs = new ArrayList<>();
56
57
    /**
58
     * Working directory must be set because ConTeXt cannot write the
59
     * result to an arbitrary location.
60
     */
61
    private final Path mDirectory;
62
63
    private TypesetTask() {
64
      final var parentDir = getTargetPath().getParent();
65
      mDirectory = parentDir == null ? DEFAULT_DIRECTORY : parentDir;
66
    }
67
68
    /**
69
     * Initializes ConTeXt, which means creating the cache directory if it
70
     * doesn't already exist. The theme entry point must be named 'main.tex'.
71
     *
72
     * @return {@code true} if the cache directory exists.
73
     */
74
    private boolean reinitialize() {
75
      final var cacheExists = !isEmpty( getCacheDir().toPath() );
76
77
      // Ensure invoking multiple times will load the correct arguments.
78
      mArgs.clear();
79
      mArgs.add( TYPESETTER_EXE );
80
81
      if( cacheExists ) {
82
        mArgs.addAll( options() );
83
84
        final var sb = new StringBuilder( 128 );
85
        mArgs.forEach( arg -> sb.append( arg ).append( " " ) );
86
        clue( sb.toString() );
87
      }
88
      else {
89
        mArgs.add( "--generate" );
90
      }
91
92
      return cacheExists;
93
    }
94
95
    /**
96
     * Setting {@code TEXMFCACHE} when run on a fresh system fails on the first
97
     * try. If the cache directory doesn't exist, attempt to create it, then
98
     * call ConTeXt to generate the PDF. This is brittle because if the
99
     * directory is empty, or not populated with cached data, a false positive
100
     * will be returned, resulting in no PDF being created.
101
     *
102
     * @return {@code true} if the document was typeset successfully.
103
     * @throws IOException          If the process could not be started.
104
     * @throws InterruptedException If the process was killed.
105
     */
106
    private boolean typeset() throws IOException, InterruptedException {
107
      return reinitialize() ? call() : call() && reinitialize() && call();
108
    }
109
110
    @Override
111
    public Boolean call() throws IOException, InterruptedException {
112
      final var stdout = new CircularQueue<String>( 150 );
113
      final var builder = new ProcessBuilder( mArgs );
114
      builder.directory( toFile( mDirectory ) );
115
      builder.environment().put( "TEXMFCACHE", getCacheDir().toString() );
116
117
      // Without redirecting (or draining) stderr, the command may not
118
      // terminate successfully.
119
      builder.redirectError( DISCARD );
120
121
      final var process = builder.start();
122
      final var listener = new PaginationListener();
123
124
      // Slurp page numbers in a separate thread while typesetting.
125
      StreamGobbler.gobble( process.getInputStream(), line -> {
126
        listener.accept( line );
127
        stdout.add( line );
128
      } );
129
130
      // Even though the process has completed, there may be incomplete I/O.
131
      process.waitFor();
132
133
      // Allow time for any incomplete I/O to take place.
134
      process.waitFor( 1, SECONDS );
135
136
      final var exit = process.exitValue();
137
      process.destroy();
138
139
      // If there was an error, the typesetter will leave behind log, pdf, and
140
      // error files.
141
      if( exit > 0 ) {
142
        final var xmlName = SysFile.getFileName( getSourcePath() );
143
        final var srcName = SysFile.getFileName( getTargetPath() );
144
        final var logName = newExtension( xmlName, ".log" );
145
        final var errName = newExtension( xmlName, "-error.log" );
146
        final var pdfName = newExtension( xmlName, ".pdf" );
147
        final var tuaName = newExtension( xmlName, ".tua" );
148
        final var badName = newExtension( srcName, ".log" );
149
150
        log( badName );
151
        log( logName );
152
        log( errName );
153
        log( stdout.stream().toList() );
154
155
        // Users may opt to keep these files around for debugging purposes.
156
        if( autoRemove() ) {
157
          deleteIfExists( logName );
158
          deleteIfExists( errName );
159
          deleteIfExists( pdfName );
160
          deleteIfExists( badName );
161
          deleteIfExists( tuaName );
162
        }
163
      }
164
165
      // Exit value for a successful invocation of the typesetter. This value
166
      // is returned when creating the cache on the first run as well as
167
      // creating PDFs on subsequent runs (after the cache has been created).
168
      // Users don't care about exit codes, only whether the PDF was generated.
169
      return exit == 0;
170
    }
171
172
    private Path newExtension( final String baseName, final String ext ) {
173
      final var path = getTargetPath();
174
      return path.resolveSibling( removeExtension( baseName ) + ext );
175
    }
176
177
    /**
178
     * Fires a status message for each line in the given file. The file format
179
     * is somewhat machine-readable, but no effort beyond line splitting is
180
     * made to parse the text.
181
     *
182
     * @param path Path to the file containing error messages.
183
     */
184
    private void log( final Path path ) throws IOException {
185
      if( exists( path ) ) {
186
        log( readAllLines( path ) );
187
      }
188
    }
189
190
    private void log( final List<String> lines ) {
191
      final var splits = new ArrayList<String>( lines.size() * 2 );
192
193
      for( final var line : lines ) {
194
        splits.addAll( asList( line.split( "\\\\n" ) ) );
195
      }
196
197
      clue( splits );
198
    }
199
200
    /**
201
     * Returns the location of the cache directory.
202
     *
203
     * @return A fully qualified path to the location to store temporary
204
     * files between typesetting runs.
205
     */
206
    @SuppressWarnings( "SpellCheckingInspection" )
207
    private java.io.File getCacheDir() {
208
      final var cache = Path.of( TEMPORARY_DIRECTORY, "luatex-cache" );
209
      return toFile( cache );
210
    }
211
212
    /**
213
     * Answers whether the given directory is empty. The typesetting software
214
     * creates a non-empty directory by default. The return value from this
215
     * method is a proxy to answering whether the typesetter has been run for
216
     * the first time or not.
217
     *
218
     * @param path The directory to check for emptiness.
219
     * @return {@code true} if the directory is empty.
220
     */
221
    private boolean isEmpty( final Path path ) {
222
      try( final var stream = newDirectoryStream( path ) ) {
223
        return !stream.iterator().hasNext();
224
      } catch( final NoSuchFileException | FileNotFoundException ex ) {
225
        // A missing directory means it doesn't exist, ergo is empty.
226
        return true;
227
      } catch( final IOException ex ) {
228
        throw new RuntimeException( ex );
229
      }
230
    }
231
  }
232
233
  /**
234
   * This will typeset the document using a new process. The return value only
235
   * indicates whether the typesetter exists, not whether the typesetting was
236
   * successful. The typesetter must be known to exist prior to calling this
237
   * method.
238
   *
239
   * @throws IOException                 If the process could not be started.
240
   * @throws InterruptedException        If the process was killed.
241
   * @throws TypesetterNotFoundException When no typesetter is along the PATH.
242
   */
243
  @Override
244
  public Boolean call()
245
    throws IOException, InterruptedException, TypesetterNotFoundException {
246
    final var task = new HostTypesetter.TypesetTask();
247
    return task.typeset();
248
  }
249
}
1250
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
public class Typesetter {
31
  /**
32
   * Name of the executable program that can typeset documents.
33
   */
34
  static final String TYPESETTER_EXE = "mtxrun";
35
36
  public static GenericBuilder<Mutator, Typesetter> builder() {
37
    return GenericBuilder.of( Mutator::new, Typesetter::new );
38
  }
39
40
  public static final class Mutator {
41
    private Path mSourcePath;
42
    private Path mTargetPath;
43
    private Path mThemeDir = USER_DIRECTORY.toPath();
44
    private Path mImageDir = USER_DIRECTORY.toPath();
45
    private Path mCacheDir = USER_CACHE_DIR.toPath();
46
    private Path mFontDir = getFontDirectory().toPath();
47
    private boolean mAutoRemove;
48
49
    /**
50
     * @param inputPath The input document to typeset.
51
     */
52
    public void setSourcePath( final Path inputPath ) {
53
      mSourcePath = inputPath;
54
    }
55
56
    /**
57
     * @param outputPath Path to the finished typeset document to create.
58
     */
59
    public void setTargetPath( final Path outputPath ) {
60
      mTargetPath = outputPath;
61
    }
62
63
    /**
64
     * @param themeDir Fully qualified path to the theme directory, which
65
     *                 ends with the selected theme name.
66
     */
67
    public void setThemeDir( final Path themeDir ) {
68
      mThemeDir = themeDir;
69
    }
70
71
    /**
72
     * @param imageDir Fully qualified path to the "images" directory.
73
     */
74
    public void setImageDir( final Path imageDir ) {
75
      mImageDir = imageDir;
76
    }
77
78
    /**
79
     * @param cacheDir Fully qualified path to the "caches" directory.
80
     */
81
    public void setCacheDir( final Path cacheDir ) {
82
      mCacheDir = cacheDir;
83
    }
84
85
    /**
86
     * @param fontDir Fully qualified path to the "fonts" directory.
87
     */
88
    public void setFontDir( final Path fontDir ) {
89
      mFontDir = fontDir;
90
    }
91
92
    /**
93
     * @param remove {@code true} to remove all temporary files after the
94
     *               typesetter produces a PDF file.
95
     */
96
    public void setAutoRemove( final boolean remove ) {
97
      mAutoRemove = remove;
98
    }
99
100
    public Path getSourcePath() {
101
      return mSourcePath;
102
    }
103
104
    public Path getTargetPath() {
105
      return mTargetPath;
106
    }
107
108
    public Path getThemeDir() {
109
      return mThemeDir;
110
    }
111
112
    public Path getImageDir() {
113
      return mImageDir;
114
    }
115
116
    public Path getCacheDir() {
117
      return mCacheDir;
118
    }
119
120
    public Path getFontDir() {
121
      return mFontDir;
122
    }
123
124
    public boolean isAutoRemove() {
125
      return mAutoRemove;
126
    }
127
  }
128
129
  private final Mutator mMutator;
130
131
  /**
132
   * Creates a new {@link Typesetter} instance capable of configuring the
133
   * typesetter used to generate a typeset document.
134
   */
135
  Typesetter( final Mutator mutator ) {
136
    assert mutator != null;
137
138
    mMutator = mutator;
139
  }
140
141
  public void typeset() throws Exception {
142
    final Callable<Boolean> typesetter;
143
144
    if( HostTypesetter.isReady() ) {
145
      typesetter = new HostTypesetter( mMutator );
146
    }
147
    else if( GuestTypesetter.isReady() ) {
148
      typesetter = new GuestTypesetter( mMutator );
149
    }
150
    else {
151
      throw new TypesetterNotFoundException( TYPESETTER_EXE );
152
    }
153
154
    final var outputPath = getTargetPath();
155
    final var prefix = "Main.status.typeset";
156
157
    clue( prefix + ".began", outputPath );
158
159
    final var time = currentTimeMillis();
160
    final var success = typesetter.call();
161
    final var suffix = success ? ".success" : ".failure";
162
163
    clue( prefix + ".ended" + suffix, outputPath, since( time ) );
164
  }
165
166
  /**
167
   * Generates the command-line arguments used to invoke the typesetter.
168
   */
169
  @SuppressWarnings( "SpellCheckingInspection" )
170
  List<String> options() {
171
    final var args = commonOptions();
172
173
    final var sourcePath = getSourcePath().toString();
174
    final var targetPath = getTargetPath().getFileName();
175
    final var themesPath = getThemeDir();
176
    final var imagesPath = getImageDir();
177
    final var cachesPath = getCacheDir();
178
179
    args.add(
180
      format( "--arguments=themesdir=%s,imagesdir=%s,cachesdir=%s",
181
              themesPath, imagesPath, cachesPath )
182
    );
183
    args.add( format( "--path='%s'", themesPath ) );
184
    args.add( format( "--result='%s'", targetPath ) );
185
    args.add( sourcePath );
186
187
    return args;
188
  }
189
190
  @SuppressWarnings( "SpellCheckingInspection" )
191
  List<String> commonOptions() {
192
    final var args = new LinkedList<String>();
193
194
    args.add( "--autogenerate" );
195
    args.add( "--script" );
196
    args.add( "mtx-context" );
197
    args.add( "--batchmode" );
198
    args.add( "--nonstopmode" );
199
    args.add( "--purgeall" );
200
    args.add( "--environment='main'" );
201
202
    return args;
203
  }
204
205
  protected Path getSourcePath() {
206
    return mMutator.getSourcePath();
207
  }
208
209
  protected Path getTargetPath() {
210
    return mMutator.getTargetPath();
211
  }
212
213
  protected Path getThemeDir() {
214
    return mMutator.getThemeDir();
215
  }
216
217
  protected Path getImageDir() {
218
    return mMutator.getImageDir();
219
  }
220
221
  protected Path getCacheDir() {
222
    return mMutator.getCacheDir();
223
  }
224
225
  protected Path getFontDir() {
226
    return mMutator.getFontDir();
227
  }
228
229
  /**
230
   * Answers whether logs and other files should be deleted upon error. The
231
   * log files are useful for debugging.
232
   *
233
   * @return {@code true} to delete generated files.
234
   */
235
  public boolean autoRemove() {
236
    return mMutator.isAutoRemove();
237
  }
238
239
  public static boolean canRun() {
240
    return hostCanRun() || guestCanRun();
241
  }
242
243
  private static boolean hostCanRun() {
244
    return HostTypesetter.isReady();
245
  }
246
247
  private static boolean guestCanRun() {
248
    return GuestTypesetter.isReady();
249
  }
250
251
  /**
252
   * Calculates the time that has elapsed from the current time to the
253
   * given moment in time.
254
   *
255
   * @param start The starting time, which should be before the current time.
256
   * @return A human-readable formatted time.
257
   * @see Time#toElapsedTime(Duration)
258
   */
259
  private static String since( final long start ) {
260
    return toElapsedTime( ofMillis( currentTimeMillis() - start ) );
261
  }
262
}
1263
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 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.containerization;
3
4
import com.keenwrite.Messages;
5
import com.keenwrite.io.CommandNotFoundException;
6
import com.keenwrite.io.SysFile;
7
8
import java.io.File;
9
import java.nio.file.Files;
10
import java.nio.file.Path;
11
import java.util.LinkedList;
12
import java.util.List;
13
14
import static com.keenwrite.events.StatusEvent.clue;
15
import static com.keenwrite.io.SysFile.toFile;
16
import static java.lang.String.format;
17
import static java.lang.String.join;
18
import static java.lang.System.arraycopy;
19
import static java.util.Arrays.copyOf;
20
import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
21
22
/**
23
 * Provides facilities for interacting with a container environment.
24
 */
25
public final class Podman implements ContainerManager {
26
  private static final String BINARY = "podman";
27
  private static final Path BINARY_PATH =
28
    Path.of(
29
      format( IS_OS_WINDOWS
30
                ? "C:\\Program Files\\RedHat\\Podman\\%s.exe"
31
                : "/usr/bin/%s",
32
              BINARY
33
      )
34
    );
35
  private static final SysFile MANAGER = new SysFile( BINARY );
36
37
  private final List<String> mMountPoints = new LinkedList<>();
38
39
  public Podman() { }
40
41
  /**
42
   * Answers whether the container is installed and runnable on the host.
43
   *
44
   * @return {@code true} if the container is available.
45
   */
46
  public static boolean canRun() {
47
    try {
48
      return toFile( getExecutable() ).isFile();
49
    } catch( final Exception ex ) {
50
      clue( "Wizard.container.executable.run.error", ex );
51
52
      // If the binary couldn't be found, then indicate that it cannot run.
53
      return false;
54
    }
55
  }
56
57
  private static Path getExecutable() {
58
    final var executable = Files.isExecutable( BINARY_PATH );
59
60
    clue( "Wizard.container.executable.run.scan", BINARY_PATH, executable );
61
62
    return executable
63
      ? BINARY_PATH
64
      : MANAGER.locate().orElseThrow();
65
  }
66
67
  @Override
68
  public int install( final File exe ) {
69
    // This monstrosity runs the installer in the background without displaying
70
    // a secondary command window, while blocking until the installer completes
71
    // and an exit code can be determined. I hate Windows.
72
    final var cmd = format(
73
      "start /b /high /wait cmd /c %s /quiet /install & exit ^!errorlevel^!",
74
      exe.getAbsolutePath()
75
    );
76
77
    clue( "Wizard.container.install.command", cmd );
78
79
    final var builder = processBuilder( "cmd", "/c", cmd );
80
81
    try {
82
      clue( "Wizard.container.install.await", cmd );
83
84
      // Wait for installation to finish (successfully or not).
85
      return wait( builder.start() );
86
    } catch( final Exception ignored ) {
87
      return -1;
88
    }
89
  }
90
91
  @Override
92
  public void start( final StreamProcessor processor )
93
    throws CommandNotFoundException {
94
    machine( processor, "stop" );
95
    podman( processor, "system", "prune", "--force" );
96
    machine( processor, "rm", "--force" );
97
    machine( processor, "init" );
98
    machine( processor, "start" );
99
  }
100
101
  @Override
102
  public void load( final StreamProcessor processor )
103
    throws CommandNotFoundException {
104
    final var url = Messages.get( "Wizard.typesetter.container.image.url" );
105
106
    podman( processor, "load", "-i", url );
107
  }
108
109
  /**
110
   * Runs:
111
   * <p>
112
   * <code>podman run --network=host --rm -t IMAGE /bin/sh -lc</code>
113
   * </p>
114
   * followed by the given arguments.
115
   *
116
   * @param args The command and arguments to run against the container.
117
   * @return The exit code from running the container manager (not the
118
   * exit code from running the command).
119
   * @throws CommandNotFoundException Container manager couldn't be found.
120
   */
121
  @Override
122
  public int run(
123
    final StreamProcessor processor,
124
    final String... args ) throws CommandNotFoundException {
125
    final var tag = Messages.get( "Wizard.typesetter.container.image.tag" );
126
127
    final var options = new LinkedList<String>();
128
    options.add( "run" );
129
    options.add( "--rm" );
130
    options.add( "--network=host" );
131
    options.addAll( mMountPoints );
132
    options.add( "-t" );
133
    options.add( tag );
134
    options.add( "/bin/sh" );
135
    options.add( "-lc" );
136
137
    final var command = toArray( toArray( options ), args );
138
    return podman( processor, command );
139
  }
140
141
  /**
142
   * Generates a command-line argument representing a mount point between
143
   * the host and guest systems.
144
   *
145
   * @param hostDir  The host directory to mount in the container.
146
   * @param guestDir The guest directory to map from the container to host.
147
   * @param readonly Set {@code true} to make the mount point read-only.
148
   */
149
  public void mount(
150
    final Path hostDir, final String guestDir, final boolean readonly ) {
151
    assert hostDir != null;
152
    assert guestDir != null;
153
    assert !guestDir.isBlank();
154
    assert toFile( hostDir ).isDirectory();
155
156
    mMountPoints.add(
157
      format( "-v%s:%s:%s", hostDir, guestDir, readonly ? "ro" : "Z" )
158
    );
159
  }
160
161
  private static void machine(
162
    final StreamProcessor processor,
163
    final String... args )
164
    throws CommandNotFoundException {
165
    podman( processor, toArray( "machine", args ) );
166
  }
167
168
  private static int podman(
169
    final StreamProcessor processor, final String... args )
170
    throws CommandNotFoundException {
171
    try {
172
      final var path = getExecutable();
173
      final var joined = join( ",", args );
174
175
      clue( "Wizard.container.process.enter", path, joined );
176
177
      final var builder = processBuilder( path, args );
178
      final var process = builder.start();
179
180
      processor.start( process.getInputStream() );
181
182
      return wait( process );
183
    } catch( final Exception ex ) {
184
      clue( ex );
185
      throw new CommandNotFoundException( MANAGER.toString() );
186
    }
187
  }
188
189
  /**
190
   * Performs a blocking wait until the {@link Process} completes.
191
   *
192
   * @param process The {@link Process} to await completion.
193
   * @return The exit code from running a command.
194
   * @throws InterruptedException The {@link Process} was interrupted.
195
   */
196
  private static int wait( final Process process ) throws InterruptedException {
197
    final var exitCode = process.waitFor();
198
199
    clue( "Wizard.container.process.exit", exitCode );
200
201
    process.destroy();
202
203
    return exitCode;
204
  }
205
206
  private static ProcessBuilder processBuilder( final String... args ) {
207
    final var builder = new ProcessBuilder( args );
208
    builder.redirectErrorStream( true );
209
210
    return builder;
211
  }
212
213
  private static ProcessBuilder processBuilder(
214
    final File file, final String... s ) {
215
    return processBuilder( toArray( file.getAbsolutePath(), s ) );
216
  }
217
218
  private static ProcessBuilder processBuilder(
219
    final Path path, final String... s ) {
220
    return processBuilder( toFile( path ), s );
221
  }
222
223
  /**
224
   * Merges two arrays into a single array.
225
   *
226
   * @param first  The first array to merge before the second array.
227
   * @param second The second array to merge after the first array.
228
   * @param <T>    The type of arrays to merge.
229
   * @return The merged arrays, with the first array elements preceding the
230
   * second array's elements.
231
   */
232
  private static <T> T[] toArray( final T[] first, final T[] second ) {
233
    assert first != null;
234
    assert second != null;
235
    assert first.length > 0;
236
    assert second.length > 0;
237
238
    final var merged = copyOf( first, first.length + second.length );
239
    arraycopy( second, 0, merged, first.length, second.length );
240
    return merged;
241
  }
242
243
  /**
244
   * Convenience method to merge a single string with an array of strings.
245
   *
246
   * @param first  The first item to prepend to the secondary items.
247
   * @param second The second item to combine with the first item.
248
   * @return A new array with the first element at index 0 and the second
249
   * elements starting at index 1.
250
   */
251
  private static String[] toArray( final String first, String... second ) {
252
    assert first != null;
253
    assert second != null;
254
    assert second.length > 0;
255
256
    return toArray( new String[]{first}, second );
257
  }
258
}
1259
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 org.apache.commons.lang3.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
23
/**
24
 * Responsible for asynchronous downloads.
25
 */
26
public abstract class AbstractDownloadPane extends InstallerPane {
27
  private static final String STATUS = ".status";
28
29
  private final Label mStatus;
30
  private final File mTarget;
31
  private final String mFilename;
32
  private final URI mUri;
33
34
  public AbstractDownloadPane() {
35
    mUri = getUri( getPrefix() + ".download.link.url" );
36
    mFilename = toFilename( mUri );
37
    final var directory = USER_DATA_DIR;
38
    mTarget = toFile( directory.resolve( mFilename ) );
39
    final var source = labelf( getPrefix() + ".paths", mFilename, directory );
40
    mStatus = labelf( getPrefix() + STATUS + ".progress", 0, 0 );
41
42
    final var border = new BorderPane();
43
    border.setTop( source );
44
    border.setCenter( spacer() );
45
    border.setBottom( mStatus );
46
47
    setContent( border );
48
  }
49
50
  @Override
51
  public void onEnteringPage( final Wizard wizard ) {
52
    disableNext( true );
53
54
    final var threadName = getClass().getCanonicalName();
55
    final var properties = wizard.getProperties();
56
    final var thread = properties.get( threadName );
57
58
    if( thread instanceof Task<?> downloader && downloader.isRunning() ) {
59
      clue( "Wizard.container.install.download.running" );
60
      return;
61
    }
62
63
    updateProperties( properties );
64
65
    final var target = getTarget();
66
    final var sysFile = new SysFile( target );
67
    final var checksum = getChecksum();
68
69
    if( sysFile.exists() ) {
70
      final var checksumOk = sysFile.isChecksum( checksum );
71
      final var suffix = checksumOk ? ".ok" : ".no";
72
73
      updateStatus( STATUS + ".checksum" + suffix, mFilename );
74
      disableNext( !checksumOk );
75
    }
76
    else {
77
      clue( "Wizard.container.install.download.started", mUri );
78
79
      final var task = downloadAsync( mUri, target, ( progress, bytes ) -> {
80
        final var suffix = progress < 0 ? ".bytes" : ".progress";
81
82
        updateStatus( STATUS + suffix, progress, bytes );
83
      } );
84
85
      properties.put( threadName, task );
86
87
      task.setOnSucceeded( e -> onDownloadSucceeded( threadName, properties ) );
88
      task.setOnFailed( e -> onDownloadFailed( threadName, properties ) );
89
      task.setOnCancelled( e -> onDownloadFailed( threadName, properties ) );
90
    }
91
  }
92
93
  protected void updateProperties(
94
    final ObservableMap<Object, Object> properties ) {
95
  }
96
97
  @Override
98
  protected String getHeaderKey() {
99
    return getPrefix() + ".header";
100
  }
101
102
  protected File getTarget() {
103
    return mTarget;
104
  }
105
106
  protected abstract String getChecksum();
107
108
  protected abstract String getPrefix();
109
110
  protected void onDownloadSucceeded(
111
    final String threadName, final ObservableMap<Object, Object> properties ) {
112
    updateStatus( STATUS + ".success" );
113
    properties.remove( threadName );
114
    disableNext( false );
115
  }
116
117
  protected void onDownloadFailed(
118
    final String threadName, final ObservableMap<Object, Object> properties ) {
119
    updateStatus( STATUS + ".failure" );
120
    properties.remove( threadName );
121
  }
122
123
  protected void updateStatus( final String suffix, final Object... args ) {
124
    update( mStatus, get( getPrefix() + suffix, args ) );
125
  }
126
127
  protected void deleteTarget() {
128
    if( !getTarget().delete() ) {
129
      clue( "Main.status.error.file.delete", getTarget() );
130
    }
131
  }
132
}
1133
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.io.downloads.DownloadManager;
9
import com.keenwrite.io.downloads.DownloadManager.ProgressListener;
10
import com.keenwrite.typesetting.containerization.ContainerManager;
11
import com.keenwrite.typesetting.containerization.Podman;
12
import javafx.animation.Animation;
13
import javafx.animation.RotateTransition;
14
import javafx.concurrent.Task;
15
import javafx.geometry.Insets;
16
import javafx.scene.Node;
17
import javafx.scene.control.*;
18
import javafx.scene.image.ImageView;
19
import javafx.scene.layout.BorderPane;
20
import javafx.scene.layout.FlowPane;
21
import javafx.scene.layout.Pane;
22
import org.controlsfx.dialog.Wizard;
23
import org.controlsfx.dialog.WizardPane;
24
25
import java.io.File;
26
import java.net.URI;
27
import java.nio.file.Paths;
28
import java.util.concurrent.Callable;
29
30
import static com.keenwrite.Messages.get;
31
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
32
import static com.keenwrite.io.SysFile.toFile;
33
import static java.lang.System.lineSeparator;
34
import static javafx.animation.Interpolator.LINEAR;
35
import static javafx.application.Platform.runLater;
36
import static javafx.scene.control.ButtonBar.ButtonData.NEXT_FORWARD;
37
import static javafx.scene.control.ContentDisplay.RIGHT;
38
import static javafx.util.Duration.seconds;
39
40
/**
41
 * Responsible for creating a {@link WizardPane} with a common header for all
42
 * subclasses.
43
 */
44
public abstract class InstallerPane extends WizardPane {
45
  /**
46
   * Unique key name to store the animation object so that it can be stopped.
47
   */
48
  private static final String PROP_ROTATION = "Wizard.typesetter.next.animate";
49
50
  /**
51
   * Defines amount of spacing between the installer's UI widgets, in pixels.
52
   */
53
  static final int PAD = 10;
54
55
  private static final double HEADER_FONT_SCALE = 1.25;
56
57
  public InstallerPane() {
58
    setHeader( createHeader() );
59
  }
60
61
  /**
62
   * When leaving the page, stop the animation. This is idempotent.
63
   *
64
   * @param wizard The wizard controlling the installer steps.
65
   */
66
  @Override
67
  public void onExitingPage( final Wizard wizard ) {
68
    super.onExitingPage( wizard );
69
    runLater( () -> stopAnimation( getNextButton() ) );
70
  }
71
72
  /**
73
   * Returns the property bundle key representing the dialog box title.
74
   */
75
  protected abstract String getHeaderKey();
76
77
  private BorderPane createHeader() {
78
    final var headerLabel = label( getHeaderKey() );
79
    headerLabel.setScaleX( HEADER_FONT_SCALE );
80
    headerLabel.setScaleY( HEADER_FONT_SCALE );
81
82
    final var separator = new Separator();
83
    separator.setPadding( new Insets( PAD, 0, 0, 0 ) );
84
85
    final var header = new BorderPane();
86
    header.setCenter( headerLabel );
87
    header.setRight( new ImageView( ICON_DIALOG ) );
88
    header.setBottom( separator );
89
    header.setPadding( new Insets( PAD, PAD, 0, PAD ) );
90
91
    return header;
92
  }
93
94
  /**
95
   * Disables the "Next" button during the installer. Normally disabling UI
96
   * elements is an anti-pattern (along with modal dialogs); however, in this
97
   * case, installation cannot proceed until each step is successfully
98
   * completed. Further, there may be "misleading" success messages shown
99
   * in the output panel, which the user may take as the step being complete.
100
   *
101
   * @param disable Set to {@code true} to disable the button.
102
   */
103
  void disableNext( final boolean disable ) {
104
    runLater( () -> {
105
      final var button = getNextButton();
106
107
      button.setDisable( disable );
108
109
      if( disable ) {
110
        startAnimation( button );
111
      }
112
      else {
113
        stopAnimation( button );
114
      }
115
    } );
116
  }
117
118
  /**
119
   * Returns the {@link Button} for advancing the wizard to the next pane.
120
   *
121
   * @return The Next button, if present, otherwise a new {@link Button}
122
   * instance so that API calls will succeed, despite not affecting the UI.
123
   */
124
  private Button getNextButton() {
125
    for( final var buttonType : getButtonTypes() ) {
126
      final var buttonData = buttonType.getButtonData();
127
128
      if( buttonData.equals( NEXT_FORWARD ) &&
129
        lookupButton( buttonType ) instanceof Button button ) {
130
        return button;
131
      }
132
    }
133
134
    // If there's no Next button, return a fake button.
135
    return new Button();
136
  }
137
138
  private void startAnimation( final Button button ) {
139
    // Create an image that is slightly taller than the button's font.
140
    final var graphic = new ImageView( ICON_DIALOG );
141
    graphic.setFitHeight( button.getFont().getSize() + 2 );
142
    graphic.setPreserveRatio( true );
143
    graphic.setSmooth( true );
144
145
    button.setGraphic( graphic );
146
    button.setGraphicTextGap( PAD );
147
    button.setContentDisplay( RIGHT );
148
149
    final var rotation = new RotateTransition( seconds( 1 ), graphic );
150
    getProperties().put( PROP_ROTATION, rotation );
151
152
    rotation.setCycleCount( Animation.INDEFINITE );
153
    rotation.setByAngle( 360 );
154
    rotation.setInterpolator( LINEAR );
155
    rotation.play();
156
  }
157
158
  private void stopAnimation( final Button button ) {
159
    final var animation = getProperties().get( PROP_ROTATION );
160
161
    if( animation instanceof RotateTransition rotation ) {
162
      rotation.stop();
163
      button.setGraphic( null );
164
      getProperties().remove( PROP_ROTATION );
165
    }
166
  }
167
168
  static TitledPane titledPane( final String title, final Node child ) {
169
    final var pane = new TitledPane( title, child );
170
    pane.setAnimated( false );
171
    pane.setCollapsible( false );
172
    pane.setExpanded( true );
173
174
    return pane;
175
  }
176
177
  static TextArea textArea( final int rows, final int cols ) {
178
    final var textarea = new TextArea();
179
    textarea.setEditable( false );
180
    textarea.setWrapText( true );
181
    textarea.setPrefRowCount( rows );
182
    textarea.setPrefColumnCount( cols );
183
184
    return textarea;
185
  }
186
187
  static Label label( final String key ) {
188
    return new Label( get( key ) );
189
  }
190
191
  /**
192
   * Like printf for labels.
193
   *
194
   * @param key    The property key to look up.
195
   * @param values The values to insert at the placeholders.
196
   * @return The formatted text with values replaced.
197
   */
198
  @SuppressWarnings( "SpellCheckingInspection" )
199
  static Label labelf( final String key, final Object... values ) {
200
    return new Label( get( key, values ) );
201
  }
202
203
  @SuppressWarnings( "SameParameterValue" )
204
  static Button button( final String key ) {
205
    return new Button( get( key ) );
206
  }
207
208
  static Node flowPane( final Node... nodes ) {
209
    return new FlowPane( nodes );
210
  }
211
212
  /**
213
   * Provides vertical spacing between {@link Node}s.
214
   *
215
   * @return A new empty vertical gap widget.
216
   */
217
  static Node spacer() {
218
    final var spacer = new Pane();
219
    spacer.setPadding( new Insets( PAD, 0, 0, 0 ) );
220
221
    return spacer;
222
  }
223
224
  static Hyperlink hyperlink( final String prefix ) {
225
    final var label = get( prefix + ".lbl" );
226
    final var url = get( prefix + ".url" );
227
    final var link = new Hyperlink( label );
228
229
    link.setOnAction( e -> browse( url ) );
230
    link.setTooltip( new Tooltip( url ) );
231
232
    return link;
233
  }
234
235
  /**
236
   * Opens a browser window off of the JavaFX main execution thread. This
237
   * is necessary so that the links open immediately, instead of being blocked
238
   * by any modal dialog (i.e., the {@link Wizard} instance).
239
   *
240
   * @param property The property key name associated with a hyperlink URL.
241
   */
242
  static void browse( final String property ) {
243
    final var url = get( property );
244
    final var task = createTask( () -> {
245
      HyperlinkOpenEvent.fire( url );
246
      return null;
247
    } );
248
    final var thread = createThread( task );
249
250
    thread.start();
251
  }
252
253
  static <T> Task<T> createTask( final Callable<T> callable ) {
254
    return new Task<>() {
255
      @Override
256
      protected T call() throws Exception {
257
        return callable.call();
258
      }
259
    };
260
  }
261
262
  static <T> Thread createThread( final Task<T> task ) {
263
    final var thread = new Thread( task );
264
    thread.setDaemon( true );
265
    return thread;
266
  }
267
268
  /**
269
   * Creates a container that can have its standard output read as an input
270
   * stream that's piped directly to a {@link TextArea}.
271
   *
272
   * @return An object that can perform tasks against a container.
273
   */
274
  static ContainerManager createContainer() {
275
    return new Podman();
276
  }
277
278
  static void update( final Label node, final String text ) {
279
    runLater( () -> node.setText( text ) );
280
  }
281
282
  static void append( final TextArea node, final String text ) {
283
    runLater( () -> {
284
      node.appendText( text );
285
      node.appendText( lineSeparator() );
286
    } );
287
  }
288
289
  /**
290
   * Downloads a resource to a local file in a separate {@link Thread}.
291
   *
292
   * @param uri      The resource to download.
293
   * @param file     The destination mTarget for the resource.
294
   * @param listener Receives updates as the download proceeds.
295
   */
296
  static Task<Void> downloadAsync(
297
    final URI uri,
298
    final File file,
299
    final ProgressListener listener ) {
300
    final Task<Void> task = createTask( () -> {
301
      try( final var token = DownloadManager.open( uri ) ) {
302
        token.download( file, listener ).run();
303
      }
304
305
      return null;
306
    } );
307
308
    createThread( task ).start();
309
    return task;
310
  }
311
312
  static String toFilename( final URI uri ) {
313
    return toFile( Paths.get( uri.getPath() ) ).getName();
314
  }
315
}
1316
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
package com.keenwrite.typesetting.installer.panes;
2
3
import com.keenwrite.io.CommandNotFoundException;
4
import com.keenwrite.typesetting.containerization.ContainerManager;
5
import com.keenwrite.typesetting.containerization.StreamProcessor;
6
import javafx.concurrent.Task;
7
import javafx.scene.control.TextArea;
8
import javafx.scene.layout.BorderPane;
9
import org.apache.commons.lang3.function.FailableBiConsumer;
10
import org.controlsfx.dialog.Wizard;
11
12
import static com.keenwrite.Messages.get;
13
import static com.keenwrite.io.StreamGobbler.gobble;
14
15
/**
16
 * Responsible for showing the output from running commands against a container
17
 * manager. There are a few installation steps that run different commands
18
 * against the installer, which are platform-specific and cannot be merged.
19
 * Common functionality between them is codified in this class.
20
 */
21
public abstract class ManagerOutputPane extends InstallerPane {
22
  private final String PROP_EXECUTOR = getClass().getCanonicalName();
23
24
  private final String mCorrectKey;
25
  private final String mMissingKey;
26
  private final FailableBiConsumer
27
    <ContainerManager, StreamProcessor, CommandNotFoundException> mFc;
28
  private final ContainerManager mContainer;
29
  private final TextArea mTextArea;
30
31
  public ManagerOutputPane(
32
    final String correctKey,
33
    final String missingKey,
34
    final FailableBiConsumer
35
      <ContainerManager, StreamProcessor, CommandNotFoundException> fc,
36
    final int cols
37
  ) {
38
    mFc = fc;
39
    mCorrectKey = correctKey;
40
    mMissingKey = missingKey;
41
    mTextArea = textArea( 5, cols );
42
    mContainer = createContainer();
43
44
    final var borderPane = new BorderPane();
45
    final var titledPane = titledPane( "Output", mTextArea );
46
47
    borderPane.setBottom( titledPane );
48
    setContent( borderPane );
49
  }
50
51
  @Override
52
  public void onEnteringPage( final Wizard wizard ) {
53
    disableNext( true );
54
55
    try {
56
      final var properties = wizard.getProperties();
57
      final var thread = properties.get( PROP_EXECUTOR );
58
59
      if( thread instanceof Thread executor && executor.isAlive() ) {
60
        return;
61
      }
62
63
      final Task<Void> task = createTask( () -> {
64
        mFc.accept(
65
          mContainer,
66
          input -> gobble( input, line -> append( mTextArea, line ) )
67
        );
68
        properties.remove( thread );
69
        return null;
70
      } );
71
72
      task.setOnSucceeded( event -> {
73
        append( mTextArea, get( mCorrectKey ) );
74
        properties.remove( thread );
75
        disableNext( false );
76
      } );
77
      task.setOnFailed( event -> append( mTextArea, get( mMissingKey ) ) );
78
      task.setOnCancelled( event -> append( mTextArea, get( mMissingKey ) ) );
79
80
      final var executor = createThread( task );
81
      properties.put( PROP_EXECUTOR, executor );
82
      executor.start();
83
    } catch( final Exception e ) {
84
      throw new RuntimeException( e );
85
    }
86
  }
87
}
188
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.Clipboard;
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
import org.jetbrains.annotations.NotNull;
17
18
import static com.keenwrite.Messages.get;
19
import static com.keenwrite.Messages.getInt;
20
import static java.lang.String.format;
21
import static org.apache.commons.lang3.SystemUtils.IS_OS_MAC;
22
23
public final class UnixManagerInstallPane extends InstallerPane {
24
  private static final String PREFIX =
25
    "Wizard.typesetter.unix.2.install.container";
26
27
  private final TextArea mCommands = textArea( 2, 40 );
28
29
  public UnixManagerInstallPane() {
30
    final var titledPane = titledPane( "Run", mCommands );
31
    final var comboBox = createUnixOsCommandMap();
32
    final var selection = comboBox.getSelectionModel();
33
    selection
34
      .selectedItemProperty()
35
      .addListener( ( c, o, n ) -> mCommands.setText( n.command() ) );
36
37
    // Auto-select if running on macOS.
38
    if( IS_OS_MAC ) {
39
      final var items = comboBox.getItems();
40
41
      for( final var item : items ) {
42
        if( "macOS".equalsIgnoreCase( item.name ) ) {
43
          selection.select( item );
44
          break;
45
        }
46
      }
47
    }
48
    else {
49
      selection.select( 0 );
50
    }
51
52
    final var distro = label( PREFIX + ".os" );
53
    distro.setText( distro.getText() + ":" );
54
    distro.setPadding( new Insets( PAD / 2.0, PAD, 0, 0 ) );
55
56
    final var hbox = new HBox();
57
    hbox.getChildren().add( distro );
58
    hbox.getChildren().add( comboBox );
59
    hbox.setPadding( new Insets( 0, 0, PAD, 0 ) );
60
61
    final var stepsPane = new VBox();
62
    final var steps = stepsPane.getChildren();
63
    steps.add( label( PREFIX + ".step.0" ) );
64
    steps.add( spacer() );
65
    steps.add( label( PREFIX + ".step.1" ) );
66
    steps.add( label( PREFIX + ".step.2" ) );
67
    steps.add( label( PREFIX + ".step.3" ) );
68
    steps.add( label( PREFIX + ".step.4" ) );
69
    steps.add( spacer() );
70
71
    steps.add( flowPane(
72
      label( PREFIX + ".details.prefix" ),
73
      hyperlink( PREFIX + ".details.link" ),
74
      label( PREFIX + ".details.suffix" )
75
    ) );
76
    steps.add( spacer() );
77
78
    final var border = new BorderPane();
79
    border.setTop( stepsPane );
80
    border.setCenter( hbox );
81
    border.setBottom( titledPane );
82
83
    setContent( border );
84
  }
85
86
  @Override
87
  public Node createButtonBar() {
88
    final var node = super.createButtonBar();
89
    final var layout = new BorderPane();
90
    final var copyButton = button( PREFIX + ".copy.began" );
91
92
    // Change the label to indicate clipboard is updated.
93
    copyButton.setOnAction( event -> {
94
      Clipboard.write( mCommands.getText() );
95
      copyButton.setText( get( PREFIX + ".copy.ended" ) );
96
    } );
97
98
    if( node instanceof ButtonBar buttonBar ) {
99
      copyButton.setMinWidth( buttonBar.getButtonMinWidth() );
100
    }
101
102
    layout.setPadding( new Insets( PAD, PAD, PAD, PAD ) );
103
    layout.setLeft( copyButton );
104
    layout.setRight( node );
105
106
    return layout;
107
  }
108
109
  @Override
110
  protected String getHeaderKey() {
111
    return PREFIX + ".header";
112
  }
113
114
  private record UnixOsCommand( String name, String command )
115
    implements Comparable<UnixOsCommand> {
116
    @Override
117
    public int compareTo(
118
      final @NotNull UnixOsCommand other ) {
119
      return toString().compareToIgnoreCase( other.toString() );
120
    }
121
122
    @Override
123
    public String toString() {
124
      return name;
125
    }
126
  }
127
128
  /**
129
   * Creates a collection of *nix distributions mapped to instructions for users
130
   * to run in a terminal.
131
   *
132
   * @return A map of *nix to instructions.
133
   */
134
  private static ComboBox<UnixOsCommand> createUnixOsCommandMap() {
135
    new ComboBox<UnixOsCommand>();
136
    final var comboBox = new ComboBox<UnixOsCommand>();
137
    final var items = comboBox.getItems();
138
    final var prefix = PREFIX + ".command";
139
    final var distros = getInt( prefix + ".distros", 14 );
140
141
    for( int i = 1; i <= distros; i++ ) {
142
      final var suffix = format( ".%02d", i );
143
      final var name = get( prefix + ".os.name" + suffix );
144
      final var command = get( prefix + ".os.text" + suffix );
145
146
      items.add( new UnixOsCommand( name, command ) );
147
    }
148
149
    items.sort( UnixOsCommand::compareTo );
150
151
    return comboBox;
152
  }
153
}
1154
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
17
/**
18
 * Responsible for installing the container manager on Windows.
19
 */
20
public final class WindowsManagerInstallPane extends InstallerPane {
21
  /**
22
   * Property for the installation thread to help with reentrancy.
23
   */
24
  private static final String WIN_INSTALLER = "windows.container.installer";
25
26
  /**
27
   * Shared property to track name of container manager binary file.
28
   */
29
  static final String WIN_BIN = "windows.container.binary";
30
31
  private static final String PREFIX =
32
    "Wizard.typesetter.win.2.install.container";
33
34
  private final ContainerManager mContainer;
35
  private final TextArea mCommands;
36
37
  public WindowsManagerInstallPane() {
38
    mCommands = textArea( 2, 55 );
39
40
    final var titledPane = titledPane( "Output", mCommands );
41
    append( mCommands, get( PREFIX + ".status.running" ) );
42
43
    final var stepsPane = new VBox();
44
    final var steps = stepsPane.getChildren();
45
    steps.add( label( PREFIX + ".step.0" ) );
46
    steps.add( spacer() );
47
    steps.add( label( PREFIX + ".step.1" ) );
48
    steps.add( label( PREFIX + ".step.2" ) );
49
    steps.add( label( PREFIX + ".step.3" ) );
50
    steps.add( spacer() );
51
    steps.add( titledPane );
52
53
    final var border = new BorderPane();
54
    border.setTop( stepsPane );
55
56
    mContainer = createContainer();
57
  }
58
59
  @Override
60
  public void onEnteringPage( final Wizard wizard ) {
61
    disableNext( true );
62
63
    // Pull the fully qualified installer path from the properties.
64
    final var properties = wizard.getProperties();
65
    final var thread = properties.get( WIN_INSTALLER );
66
67
    if( thread instanceof Thread installer && installer.isAlive() ) {
68
      return;
69
    }
70
71
    final var binary = properties.get( WIN_BIN );
72
    final var key = PREFIX + ".status";
73
74
    if( binary instanceof File exe ) {
75
      final var task = createTask( () -> {
76
        final var exit = mContainer.install( exe );
77
78
        // Remove the installer after installation is finished.
79
        properties.remove( thread );
80
81
        final var msg = exit == 0
82
          ? get( key + ".success" )
83
          : get( key + ".failure", exit );
84
85
        append( mCommands, msg );
86
        disableNext( exit != 0 );
87
88
        return null;
89
      } );
90
91
      final var installer = createThread( task );
92
      properties.put( WIN_INSTALLER, installer );
93
      installer.start();
94
    }
95
    else {
96
      append( mCommands, get( PREFIX + ".unknown", binary ) );
97
    }
98
  }
99
100
  @Override
101
  public String getHeaderKey() {
102
    return PREFIX + ".header";
103
  }
104
}
1105
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 = ACTION_PREFIX + id + ".";
51
      final var text = prefix + "text";
52
      final var icon = prefix + "icon";
53
      final var accelerator = 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 += " (" + 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 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.actions;
3
4
import com.keenwrite.ui.controls.EventedStatusBar;
5
import javafx.event.ActionEvent;
6
import javafx.event.EventHandler;
7
import javafx.scene.Node;
8
import javafx.scene.control.Menu;
9
import javafx.scene.control.MenuBar;
10
import javafx.scene.control.MenuItem;
11
import javafx.scene.control.ToolBar;
12
import org.controlsfx.control.StatusBar;
13
import org.jetbrains.annotations.NotNull;
14
15
import java.util.HashMap;
16
import java.util.Map;
17
18
import static com.keenwrite.Messages.get;
19
20
/**
21
 * Responsible for wiring all application actions to menus, toolbar buttons,
22
 * and keyboard shortcuts.
23
 */
24
public final class ApplicationBars {
25
26
  private static final Map<String, Action> sMap = new HashMap<>( 64 );
27
28
  /**
29
   * Empty constructor.
30
   */
31
  public ApplicationBars() {
32
  }
33
34
  /**
35
   * Creates the main application affordances.
36
   *
37
   * @param actions The {@link GuiCommands} that map user interface
38
   *                selections to executable code.
39
   * @return An instance of {@link MenuBar} that contains the menu.
40
   */
41
  public static MenuBar createMenuBar( final GuiCommands actions ) {
42
    final var SEPARATOR = new SeparatorAction();
43
44
    return new MenuBar(
45
      createMenuFile( actions, SEPARATOR ),
46
      createMenuEdit( actions, SEPARATOR ),
47
      createMenuFormat( actions ),
48
      createMenuInsert( actions, SEPARATOR ),
49
      createMenuVariable( actions, SEPARATOR ),
50
      createMenuView( actions, SEPARATOR ),
51
      createMenuHelp( actions )
52
    );
53
  }
54
55
  @NotNull
56
  private static Menu createMenuFile(
57
    final GuiCommands actions, final SeparatorAction SEPARATOR ) {
58
    // @formatter:off
59
    return createMenu(
60
      get( "Main.menu.file" ),
61
      addAction( "file.new", e -> actions.file_new() ),
62
      addAction( "file.open", e -> actions.file_open() ),
63
      SEPARATOR,
64
      addAction( "file.close", e -> actions.file_close() ),
65
      addAction( "file.close_all", e -> actions.file_close_all() ),
66
      SEPARATOR,
67
      addAction( "file.save", e -> actions.file_save() ),
68
      addAction( "file.save_as", e -> actions.file_save_as() ),
69
      addAction( "file.save_all", e -> actions.file_save_all() ),
70
      SEPARATOR,
71
      addAction( "file.export", e -> { } )
72
        .addSubActions(
73
          addAction( "file.export.pdf", e -> actions.file_export_pdf() ),
74
          addAction( "file.export.pdf.dir", e -> actions.file_export_pdf_dir() ),
75
          addAction( "file.export.pdf.repeat", e -> actions.file_export_repeat() ),
76
          addAction( "file.export.html.dir", e -> actions.file_export_html_dir() ),
77
          addAction( "file.export.html_svg", e -> actions.file_export_html_svg() ),
78
          addAction( "file.export.html_tex", e -> actions.file_export_html_tex() ),
79
          addAction( "file.export.xhtml_tex", e -> actions.file_export_xhtml_tex() )
80
        ),
81
      SEPARATOR,
82
      addAction( "file.exit", e -> actions.file_exit() )
83
    );
84
    // @formatter:on
85
  }
86
87
  @NotNull
88
  private static Menu createMenuEdit(
89
    final GuiCommands actions, final SeparatorAction SEPARATOR ) {
90
    return createMenu(
91
      get( "Main.menu.edit" ),
92
      SEPARATOR,
93
      addAction( "edit.undo", e -> actions.edit_undo() ),
94
      addAction( "edit.redo", e -> actions.edit_redo() ),
95
      SEPARATOR,
96
      addAction( "edit.cut", e -> actions.edit_cut() ),
97
      addAction( "edit.copy", e -> actions.edit_copy() ),
98
      addAction( "edit.paste", e -> actions.edit_paste() ),
99
      addAction( "edit.select_all", e -> actions.edit_select_all() ),
100
      SEPARATOR,
101
      addAction( "edit.find", e -> actions.edit_find() ),
102
      addAction( "edit.find_next", e -> actions.edit_find_next() ),
103
      addAction( "edit.find_prev", e -> actions.edit_find_prev() ),
104
      SEPARATOR,
105
      addAction( "edit.preferences", e -> actions.edit_preferences() )
106
    );
107
  }
108
109
  @NotNull
110
  private static Menu createMenuFormat( final GuiCommands actions ) {
111
    return createMenu(
112
      get( "Main.menu.format" ),
113
      addAction( "format.bold", e -> actions.format_bold() ),
114
      addAction( "format.italic", e -> actions.format_italic() ),
115
      addAction( "format.monospace", e -> actions.format_monospace() ),
116
      addAction( "format.superscript", e -> actions.format_superscript() ),
117
      addAction( "format.subscript", e -> actions.format_subscript() ),
118
      addAction( "format.strikethrough", e -> actions.format_strikethrough() )
119
    );
120
  }
121
122
  @NotNull
123
  private static Menu createMenuInsert(
124
    final GuiCommands actions,
125
    final SeparatorAction SEPARATOR ) {
126
    // @formatter:off
127
    return createMenu(
128
      get( "Main.menu.insert" ),
129
      addAction( "insert.blockquote", e -> actions.insert_blockquote() ),
130
      addAction( "insert.code", e -> actions.insert_code() ),
131
      addAction( "insert.fenced_code_block", e -> actions.insert_fenced_code_block() ),
132
      SEPARATOR,
133
      addAction( "insert.link", e -> actions.insert_link() ),
134
      addAction( "insert.image", e -> actions.insert_image() ),
135
      SEPARATOR,
136
      addAction( "insert.heading_1", e -> actions.insert_heading_1() ),
137
      addAction( "insert.heading_2", e -> actions.insert_heading_2() ),
138
      addAction( "insert.heading_3", e -> actions.insert_heading_3() ),
139
      SEPARATOR,
140
      addAction( "insert.unordered_list", e -> actions.insert_unordered_list() ),
141
      addAction( "insert.ordered_list", e -> actions.insert_ordered_list() ),
142
      addAction( "insert.horizontal_rule", e -> actions.insert_horizontal_rule() )
143
    );
144
    // @formatter:on
145
  }
146
147
  @NotNull
148
  private static Menu createMenuVariable(
149
    final GuiCommands actions, final SeparatorAction SEPARATOR ) {
150
    return createMenu(
151
      get( "Main.menu.definition" ),
152
      addAction( "definition.insert", e -> actions.definition_autoinsert() ),
153
      SEPARATOR,
154
      addAction( "definition.create", e -> actions.definition_create() ),
155
      addAction( "definition.rename", e -> actions.definition_rename() ),
156
      addAction( "definition.delete", e -> actions.definition_delete() )
157
    );
158
  }
159
160
  @NotNull
161
  private static Menu createMenuView(
162
    final GuiCommands actions, final SeparatorAction SEPARATOR ) {
163
    return createMenu(
164
      get( "Main.menu.view" ),
165
      addAction( "view.refresh", e -> actions.view_refresh() ),
166
      SEPARATOR,
167
      addAction( "view.preview", e -> actions.view_preview() ),
168
      addAction( "view.outline", e -> actions.view_outline() ),
169
      addAction( "view.statistics", e -> actions.view_statistics() ),
170
      addAction( "view.files", e -> actions.view_files() ),
171
      SEPARATOR,
172
      addAction( "view.menubar", e -> actions.view_menubar() ),
173
      addAction( "view.toolbar", e -> actions.view_toolbar() ),
174
      addAction( "view.statusbar", e -> actions.view_statusbar() ),
175
      SEPARATOR,
176
      addAction( "view.log", e -> actions.view_log() )
177
    );
178
  }
179
180
  @NotNull
181
  private static Menu createMenuHelp( final GuiCommands actions ) {
182
    return createMenu(
183
      get( "Main.menu.help" ),
184
      addAction( "help.about", e -> actions.help_about() )
185
    );
186
  }
187
188
  public static Node createToolBar() {
189
    final var SEPARATOR = new SeparatorAction();
190
191
    return createToolBar(
192
      getAction( "file.new" ),
193
      getAction( "file.open" ),
194
      getAction( "file.save" ),
195
      SEPARATOR,
196
      getAction( "file.export.pdf" ),
197
      SEPARATOR,
198
      getAction( "edit.undo" ),
199
      getAction( "edit.redo" ),
200
      getAction( "edit.cut" ),
201
      getAction( "edit.copy" ),
202
      getAction( "edit.paste" ),
203
      SEPARATOR,
204
      getAction( "format.bold" ),
205
      getAction( "format.italic" ),
206
      getAction( "format.superscript" ),
207
      getAction( "format.subscript" ),
208
      getAction( "insert.blockquote" ),
209
      getAction( "insert.code" ),
210
      getAction( "insert.fenced_code_block" ),
211
      SEPARATOR,
212
      getAction( "insert.link" ),
213
      getAction( "insert.image" ),
214
      SEPARATOR,
215
      getAction( "insert.heading_1" ),
216
      SEPARATOR,
217
      getAction( "insert.unordered_list" ),
218
      getAction( "insert.ordered_list" )
219
    );
220
  }
221
222
  public static StatusBar createStatusBar() {
223
    return new EventedStatusBar();
224
  }
225
226
  /**
227
   * Adds a new action to the list of actions.
228
   *
229
   * @param key     The name of the action to register in {@link #sMap}.
230
   * @param handler Performs the action upon request.
231
   * @return The newly registered action.
232
   */
233
  private static Action addAction(
234
    final String key, final EventHandler<ActionEvent> handler ) {
235
    assert key != null;
236
    assert handler != null;
237
238
    final var action = Action
239
      .builder()
240
      .setId( key )
241
      .setHandler( handler )
242
      .build();
243
244
    sMap.put( key, action );
245
246
    return action;
247
  }
248
249
  private static Action getAction( final String key ) {
250
    return sMap.get( key );
251
  }
252
253
  public static Menu createMenu(
254
    final String text, final MenuAction... actions ) {
255
    return new Menu( text, null, createMenuItems( actions ) );
256
  }
257
258
  public static MenuItem[] createMenuItems( final MenuAction... actions ) {
259
    final var menuItems = new MenuItem[ actions.length ];
260
261
    for( var i = 0; i < actions.length; i++ ) {
262
      menuItems[ i ] = actions[ i ].createMenuItem();
263
    }
264
265
    return menuItems;
266
  }
267
268
  private static ToolBar createToolBar( final MenuAction... actions ) {
269
    return new ToolBar( createToolBarButtons( actions ) );
270
  }
271
272
  private static Node[] createToolBarButtons( final MenuAction... actions ) {
273
    final var len = actions.length;
274
    final var nodes = new Node[ len ];
275
276
    for( var i = 0; i < len; i++ ) {
277
      nodes[ i ] = actions[ i ].createToolBarNode();
278
    }
279
280
    return nodes;
281
  }
282
}
1283
A src/main/java/com/keenwrite/ui/actions/GuiCommands.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.actions;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.MainPane;
6
import com.keenwrite.MainScene;
7
import com.keenwrite.commands.ConcatenateCommand;
8
import com.keenwrite.editors.TextDefinition;
9
import com.keenwrite.editors.TextEditor;
10
import com.keenwrite.editors.markdown.HyperlinkModel;
11
import com.keenwrite.editors.markdown.LinkVisitor;
12
import com.keenwrite.events.CaretMovedEvent;
13
import com.keenwrite.events.ExportFailedEvent;
14
import com.keenwrite.io.SysFile;
15
import com.keenwrite.preferences.Key;
16
import com.keenwrite.preferences.PreferencesController;
17
import com.keenwrite.preferences.Workspace;
18
import com.keenwrite.processors.markdown.MarkdownProcessor;
19
import com.keenwrite.search.SearchModel;
20
import com.keenwrite.typesetting.Typesetter;
21
import com.keenwrite.ui.controls.SearchBar;
22
import com.keenwrite.ui.dialogs.ExportDialog;
23
import com.keenwrite.ui.dialogs.ExportSettings;
24
import com.keenwrite.ui.dialogs.ImageDialog;
25
import com.keenwrite.ui.dialogs.LinkDialog;
26
import com.keenwrite.ui.explorer.FilePicker;
27
import com.keenwrite.ui.explorer.FilePickerFactory;
28
import com.keenwrite.ui.logging.LogView;
29
import com.vladsch.flexmark.ast.Link;
30
import javafx.concurrent.Service;
31
import javafx.concurrent.Task;
32
import javafx.scene.control.Alert;
33
import javafx.scene.control.Dialog;
34
import javafx.stage.Window;
35
import javafx.stage.WindowEvent;
36
37
import java.io.File;
38
import java.nio.file.Path;
39
import java.util.List;
40
import java.util.Optional;
41
42
import static com.keenwrite.Bootstrap.*;
43
import static com.keenwrite.ExportFormat.*;
44
import static com.keenwrite.Messages.get;
45
import static com.keenwrite.constants.Constants.PDF_DEFAULT;
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.file.Files.writeString;
54
import static javafx.application.Platform.runLater;
55
import static javafx.event.Event.fireEvent;
56
import static javafx.scene.control.Alert.AlertType.INFORMATION;
57
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
58
import static org.apache.commons.io.FilenameUtils.getExtension;
59
60
/**
61
 * Responsible for abstracting how functionality is mapped to the application.
62
 * This allows users to customize accelerator keys and will provide pluggable
63
 * functionality so that different text markup languages can change documents
64
 * using their respective syntax.
65
 */
66
public final class GuiCommands {
67
  private static final String STYLE_SEARCH = "search";
68
69
  /**
70
   * When an action is executed, this is one of the recipients.
71
   */
72
  private final MainPane mMainPane;
73
74
  private final MainScene mMainScene;
75
76
  private final LogView mLogView;
77
78
  /**
79
   * Tracks finding text in the active document.
80
   */
81
  private final SearchModel mSearchModel;
82
83
  private boolean mCanTypeset;
84
85
  /**
86
   * A {@link Task} can only be run once, so wrap it in a {@link Service} to
87
   * allow re-launching the typesetting task repeatedly.
88
   */
89
  private Service<Path> mTypesetService;
90
91
  /**
92
   * Prevent a race-condition between checking to see if the typesetting task
93
   * is running and restarting the task itself.
94
   */
95
  private final Object mMutex = new Object();
96
97
  public GuiCommands( final MainScene scene, final MainPane pane ) {
98
    mMainScene = scene;
99
    mMainPane = pane;
100
    mLogView = new LogView();
101
    mSearchModel = new SearchModel();
102
    mSearchModel.matchOffsetProperty().addListener( ( c, o, n ) -> {
103
      final var editor = getActiveTextEditor();
104
105
      // Clear highlighted areas before highlighting a new region.
106
      if( o != null ) {
107
        editor.unstylize( STYLE_SEARCH );
108
      }
109
110
      if( n != null ) {
111
        editor.moveTo( n.getStart() );
112
        editor.stylize( n, STYLE_SEARCH );
113
      }
114
    } );
115
116
    // When the active text editor changes ...
117
    mMainPane.textEditorProperty().addListener(
118
      ( c, o, n ) -> {
119
        // ... update the haystack.
120
        mSearchModel.search( getActiveTextEditor().getText() );
121
122
        // ... update the status bar with the current caret position.
123
        if( n != null ) {
124
          final var w = getWorkspace();
125
          final var recentDoc = w.fileProperty( KEY_UI_RECENT_DOCUMENT );
126
127
          // ... preserve the most recent document.
128
          recentDoc.setValue( n.getFile() );
129
          CaretMovedEvent.fire( n.getCaret() );
130
        }
131
      }
132
    );
133
  }
134
135
  public void file_new() {
136
    getMainPane().newTextEditor();
137
  }
138
139
  public void file_open() {
140
    pickFiles( FILE_OPEN_MULTIPLE ).ifPresent( l -> getMainPane().open( l ) );
141
  }
142
143
  public void file_close() {
144
    getMainPane().close();
145
  }
146
147
  public void file_close_all() {
148
    getMainPane().closeAll();
149
  }
150
151
  public void file_save() {
152
    getMainPane().save();
153
  }
154
155
  public void file_save_as() {
156
    pickFiles( FILE_SAVE_AS ).ifPresent( l -> getMainPane().saveAs( l ) );
157
  }
158
159
  public void file_save_all() {
160
    getMainPane().saveAll();
161
  }
162
163
  /**
164
   * Converts the actively edited file in the given file format.
165
   *
166
   * @param format The destination file format.
167
   */
168
  private void file_export( final ExportFormat format ) {
169
    file_export( format, false );
170
  }
171
172
  /**
173
   * Converts one or more files into the given file format. If {@code dir}
174
   * is set to true, this will first append all files in the same directory
175
   * as the actively edited file.
176
   *
177
   * @param format The destination file format.
178
   * @param dir    Export all files in the actively edited file's directory.
179
   */
180
  private void file_export( final ExportFormat format, final boolean dir ) {
181
    final var editor = getMainPane().getTextEditor();
182
    final var exported = getWorkspace().fileProperty( KEY_UI_RECENT_EXPORT );
183
    final var exportParent = exported.get().toPath().getParent();
184
    final var editorParent = editor.getPath().getParent();
185
    final var userHomeParent = USER_DIRECTORY.toPath();
186
    final var exportPath = exportParent != null
187
      ? exportParent
188
      : editorParent != null
189
      ? editorParent
190
      : userHomeParent;
191
192
    final var filename = format.toExportFilename( editor.getPath() );
193
    final var selected = PDF_DEFAULT
194
      .getName()
195
      .equals( exported.get().getName() );
196
    final var selection = pickFile(
197
      selected
198
        ? filename
199
        : exported.get(),
200
      exportPath,
201
      FILE_EXPORT
202
    );
203
204
    selection.ifPresent( files -> file_export( editor, format, files, dir ) );
205
  }
206
207
  private void file_export(
208
    final TextEditor editor,
209
    final ExportFormat format,
210
    final List<File> files,
211
    final boolean dir ) {
212
    editor.save();
213
    final var main = getMainPane();
214
    final var exported = getWorkspace().fileProperty( KEY_UI_RECENT_EXPORT );
215
216
    final var sourceFile = files.get( 0 );
217
    final var sourcePath = sourceFile.toPath();
218
    final var document = dir ? append( editor ) : editor.getText();
219
    final var context = main.createProcessorContext( sourcePath, format );
220
221
    final var service = new Service<Path>() {
222
      @Override
223
      protected Task<Path> createTask() {
224
        final var task = new Task<Path>() {
225
          @Override
226
          protected Path call() throws Exception {
227
            final var chain = createProcessors( context );
228
            final var export = chain.apply( document );
229
230
            // Processors can export binary files. In such cases, processors
231
            // return null to prevent further processing.
232
            return export == null ? null : writeString( sourcePath, export );
233
          }
234
        };
235
236
        task.setOnSucceeded(
237
          e -> {
238
            // Remember the exported file name for next time.
239
            exported.setValue( sourceFile );
240
241
            final var result = task.getValue();
242
243
            // Binary formats must notify users of success independently.
244
            if( result != null ) {
245
              clue( "Main.status.export.success", result );
246
            }
247
          }
248
        );
249
250
        task.setOnFailed( e -> {
251
          final var ex = task.getException();
252
          clue( ex );
253
254
          if( ex instanceof TypeNotPresentException ) {
255
            fireExportFailedEvent();
256
          }
257
        } );
258
259
        return task;
260
      }
261
    };
262
263
    mTypesetService = service;
264
    typeset( service );
265
  }
266
267
  /**
268
   * @param dir {@code true} means to export all files in the active file
269
   *            editor's directory; {@code false} means to export only the
270
   *            actively edited file.
271
   */
272
  private void file_export_pdf( final boolean dir ) {
273
    // Don't re-validate the typesetter installation each time. If the
274
    // user mucks up the typesetter installation, it'll get caught the
275
    // next time the application is started. Don't use |= because it
276
    // won't short-circuit.
277
    mCanTypeset = mCanTypeset || Typesetter.canRun();
278
279
    if( mCanTypeset ) {
280
      final var workspace = getWorkspace();
281
      final var theme = workspace.stringProperty(
282
        KEY_TYPESET_CONTEXT_THEME_SELECTION
283
      );
284
      final var chapters = workspace.stringProperty(
285
        KEY_TYPESET_CONTEXT_CHAPTERS
286
      );
287
288
      final var settings = ExportSettings
289
        .builder()
290
        .with( ExportSettings.Mutator::setTheme, theme )
291
        .with( ExportSettings.Mutator::setChapters, chapters )
292
        .build();
293
294
      final var themes = workspace.getFile(
295
        KEY_TYPESET_CONTEXT_THEMES_PATH
296
      );
297
298
      // If the typesetter is installed, allow the user to select a theme. If
299
      // the themes aren't installed, a status message will appear.
300
      if( ExportDialog.choose( getWindow(), themes, settings, dir ) ) {
301
        file_export( APPLICATION_PDF, dir );
302
      }
303
    }
304
    else {
305
      fireExportFailedEvent();
306
    }
307
  }
308
309
  public void file_export_pdf() {
310
    file_export_pdf( false );
311
  }
312
313
  public void file_export_pdf_dir() {
314
    file_export_pdf( true );
315
  }
316
317
  public void file_export_html_dir() {
318
    file_export( XHTML_TEX, true );
319
  }
320
321
  public void file_export_repeat() {
322
    typeset( mTypesetService );
323
  }
324
325
  public void file_export_html_svg() {
326
    file_export( HTML_TEX_SVG );
327
  }
328
329
  public void file_export_html_tex() {
330
    file_export( HTML_TEX_DELIMITED );
331
  }
332
333
  public void file_export_xhtml_tex() {
334
    file_export( XHTML_TEX );
335
  }
336
337
  private void fireExportFailedEvent() {
338
    runLater( ExportFailedEvent::fire );
339
  }
340
341
  public void file_exit() {
342
    final var window = getWindow();
343
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
344
  }
345
346
  public void edit_undo() {
347
    getActiveTextEditor().undo();
348
  }
349
350
  public void edit_redo() {
351
    getActiveTextEditor().redo();
352
  }
353
354
  public void edit_cut() {
355
    getActiveTextEditor().cut();
356
  }
357
358
  public void edit_copy() {
359
    getActiveTextEditor().copy();
360
  }
361
362
  public void edit_paste() {
363
    getActiveTextEditor().paste();
364
  }
365
366
  public void edit_select_all() {
367
    getActiveTextEditor().selectAll();
368
  }
369
370
  public void edit_find() {
371
    final var nodes = getMainScene().getStatusBar().getLeftItems();
372
373
    if( nodes.isEmpty() ) {
374
      final var searchBar = new SearchBar();
375
376
      searchBar.matchIndexProperty().bind( mSearchModel.matchIndexProperty() );
377
      searchBar.matchCountProperty().bind( mSearchModel.matchCountProperty() );
378
379
      searchBar.setOnCancelAction( event -> {
380
        final var editor = getActiveTextEditor();
381
        nodes.remove( searchBar );
382
        editor.unstylize( STYLE_SEARCH );
383
        editor.getNode().requestFocus();
384
      } );
385
386
      searchBar.addInputListener( ( c, o, n ) -> {
387
        if( n != null && !n.isEmpty() ) {
388
          mSearchModel.search( n, getActiveTextEditor().getText() );
389
        }
390
      } );
391
392
      searchBar.setOnNextAction( event -> edit_find_next() );
393
      searchBar.setOnPrevAction( event -> edit_find_prev() );
394
395
      nodes.add( searchBar );
396
      searchBar.requestFocus();
397
    }
398
  }
399
400
  public void edit_find_next() {
401
    mSearchModel.advance();
402
  }
403
404
  public void edit_find_prev() {
405
    mSearchModel.retreat();
406
  }
407
408
  public void edit_preferences() {
409
    try {
410
      new PreferencesController( getWorkspace() ).show();
411
    } catch( final Exception ex ) {
412
      clue( ex );
413
    }
414
  }
415
416
  public void format_bold() {
417
    getActiveTextEditor().bold();
418
  }
419
420
  public void format_italic() {
421
    getActiveTextEditor().italic();
422
  }
423
424
  public void format_monospace() {
425
    getActiveTextEditor().monospace();
426
  }
427
428
  public void format_superscript() {
429
    getActiveTextEditor().superscript();
430
  }
431
432
  public void format_subscript() {
433
    getActiveTextEditor().subscript();
434
  }
435
436
  public void format_strikethrough() {
437
    getActiveTextEditor().strikethrough();
438
  }
439
440
  public void insert_blockquote() {
441
    getActiveTextEditor().blockquote();
442
  }
443
444
  public void insert_code() {
445
    getActiveTextEditor().code();
446
  }
447
448
  public void insert_fenced_code_block() {
449
    getActiveTextEditor().fencedCodeBlock();
450
  }
451
452
  public void insert_link() {
453
    insertObject( createLinkDialog() );
454
  }
455
456
  public void insert_image() {
457
    insertObject( createImageDialog() );
458
  }
459
460
  private void insertObject( final Dialog<String> dialog ) {
461
    final var textArea = getActiveTextEditor().getTextArea();
462
    dialog.showAndWait().ifPresent( textArea::replaceSelection );
463
  }
464
465
  private Dialog<String> createLinkDialog() {
466
    return new LinkDialog( getWindow(), createHyperlinkModel() );
467
  }
468
469
  private Dialog<String> createImageDialog() {
470
    final var path = getActiveTextEditor().getPath();
471
    final var parentDir = path.getParent();
472
    return new ImageDialog( getWindow(), parentDir );
473
  }
474
475
  /**
476
   * Returns one of: selected text, word under cursor, or parsed hyperlink from
477
   * the Markdown AST.
478
   *
479
   * @return An instance containing the link URL and display text.
480
   */
481
  private HyperlinkModel createHyperlinkModel() {
482
    final var context = getMainPane().createProcessorContext();
483
    final var editor = getActiveTextEditor();
484
    final var textArea = editor.getTextArea();
485
    final var selectedText = textArea.getSelectedText();
486
487
    // Convert current paragraph to Markdown nodes.
488
    final var mp = MarkdownProcessor.create( context );
489
    final var p = textArea.getCurrentParagraph();
490
    final var paragraph = textArea.getText( p );
491
    final var node = mp.toNode( paragraph );
492
    final var visitor = new LinkVisitor( textArea.getCaretColumn() );
493
    final var link = visitor.process( node );
494
495
    if( link != null ) {
496
      textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
497
    }
498
499
    return createHyperlinkModel( link, selectedText );
500
  }
501
502
  private HyperlinkModel createHyperlinkModel(
503
    final Link link, final String selection ) {
504
505
    return link == null
506
      ? new HyperlinkModel( selection, "https://localhost" )
507
      : new HyperlinkModel( link );
508
  }
509
510
  public void insert_heading_1() {
511
    insert_heading( 1 );
512
  }
513
514
  public void insert_heading_2() {
515
    insert_heading( 2 );
516
  }
517
518
  public void insert_heading_3() {
519
    insert_heading( 3 );
520
  }
521
522
  private void insert_heading( final int level ) {
523
    getActiveTextEditor().heading( level );
524
  }
525
526
  public void insert_unordered_list() {
527
    getActiveTextEditor().unorderedList();
528
  }
529
530
  public void insert_ordered_list() {
531
    getActiveTextEditor().orderedList();
532
  }
533
534
  public void insert_horizontal_rule() {
535
    getActiveTextEditor().horizontalRule();
536
  }
537
538
  public void definition_create() {
539
    getActiveTextDefinition().createDefinition();
540
  }
541
542
  public void definition_rename() {
543
    getActiveTextDefinition().renameDefinition();
544
  }
545
546
  public void definition_delete() {
547
    getActiveTextDefinition().deleteDefinitions();
548
  }
549
550
  public void definition_autoinsert() {
551
    getMainPane().autoinsert();
552
  }
553
554
  public void view_refresh() {
555
    getMainPane().viewRefresh();
556
  }
557
558
  public void view_preview() {
559
    getMainPane().viewPreview();
560
  }
561
562
  public void view_outline() {
563
    getMainPane().viewOutline();
564
  }
565
566
  public void view_files() { getMainPane().viewFiles(); }
567
568
  public void view_statistics() {
569
    getMainPane().viewStatistics();
570
  }
571
572
  public void view_menubar() {
573
    getMainScene().toggleMenuBar();
574
  }
575
576
  public void view_toolbar() {
577
    getMainScene().toggleToolBar();
578
  }
579
580
  public void view_statusbar() {
581
    getMainScene().toggleStatusBar();
582
  }
583
584
  public void view_log() {
585
    mLogView.view();
586
  }
587
588
  public void help_about() {
589
    final var alert = new Alert( INFORMATION );
590
    final var prefix = "Dialog.about.";
591
    alert.setTitle( get( prefix + "title", APP_TITLE ) );
592
    alert.setHeaderText( get( prefix + "header", APP_TITLE ) );
593
    alert.setContentText( get( prefix + "content", APP_YEAR, APP_VERSION ) );
594
    alert.setGraphic( ICON_DIALOG_NODE );
595
    alert.initOwner( getWindow() );
596
    alert.showAndWait();
597
  }
598
599
  private <T> void typeset( final Service<T> service ) {
600
    synchronized( mMutex ) {
601
      if( service != null && !service.isRunning() ) {
602
        service.reset();
603
        service.start();
604
      }
605
    }
606
  }
607
608
  /**
609
   * Concatenates all the files in the same directory as the given file into
610
   * a string. The extension is determined by the given file name pattern; the
611
   * order files are concatenated is based on their numeric sort order (this
612
   * avoids lexicographic sorting).
613
   * <p>
614
   * If the parent path to the file being edited in the text editor cannot
615
   * be found then this will return the editor's text, without iterating through
616
   * the parent directory. (Should never happen, but who knows?)
617
   * </p>
618
   * <p>
619
   * New lines are automatically appended to separate each file.
620
   * </p>
621
   *
622
   * @param editor The text editor containing
623
   * @return All files in the same directory as the file being edited
624
   * concatenated into a single string.
625
   */
626
  private String append( final TextEditor editor ) {
627
    final var pattern = editor.getPath();
628
    final var parent = pattern.getParent();
629
630
    // Short-circuit because nothing else can be done.
631
    if( parent == null ) {
632
      clue( "Main.status.export.concat.parent", pattern );
633
      return editor.getText();
634
    }
635
636
    final var filename = SysFile.getFileName( pattern );
637
    final var extension = getExtension( filename );
638
639
    if( extension.isBlank() ) {
640
      clue( "Main.status.export.concat.extension", filename );
641
      return editor.getText();
642
    }
643
644
    try {
645
      final var command = new ConcatenateCommand(
646
        parent, extension, getString( KEY_TYPESET_CONTEXT_CHAPTERS ) );
647
      return command.call();
648
    } catch( final Throwable t ) {
649
      clue( t );
650
      return editor.getText();
651
    }
652
  }
653
654
  private Optional<List<File>> pickFiles( final SelectionType type ) {
655
    return createPicker( type ).choose();
656
  }
657
658
  @SuppressWarnings( "SameParameterValue" )
659
  private Optional<List<File>> pickFile(
660
    final File file,
661
    final Path directory,
662
    final SelectionType type ) {
663
    final var picker = createPicker( type );
664
    picker.setInitialFilename( file );
665
    picker.setInitialDirectory( directory );
666
    return picker.choose();
667
  }
668
669
  private FilePicker createPicker( final SelectionType type ) {
670
    final var factory = new FilePickerFactory( getWorkspace() );
671
    return factory.createModal( getWindow(), type );
672
  }
673
674
  private TextEditor getActiveTextEditor() {
675
    return getMainPane().getTextEditor();
676
  }
677
678
  private TextDefinition getActiveTextDefinition() {
679
    return getMainPane().getTextDefinition();
680
  }
681
682
  private MainScene getMainScene() {
683
    return mMainScene;
684
  }
685
686
  private MainPane getMainPane() {
687
    return mMainPane;
688
  }
689
690
  private Workspace getWorkspace() {
691
    return mMainPane.getWorkspace();
692
  }
693
694
  @SuppressWarnings( "SameParameterValue" )
695
  private String getString( final Key key ) {
696
    return getWorkspace().getString( key );
697
  }
698
699
  private Window getWindow() {
700
    return getMainPane().getWindow();
701
  }
702
}
1703
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/Clipboard.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 Clipboard {
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 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.controls;
3
4
import com.keenwrite.Messages;
5
import javafx.event.ActionEvent;
6
import javafx.scene.control.Button;
7
import javafx.stage.DirectoryChooser;
8
9
import java.io.File;
10
import java.util.function.Consumer;
11
12
import static com.keenwrite.ui.fonts.IconFactory.createGraphic;
13
import static org.controlsfx.glyphfont.FontAwesome.Glyph.FILE_ALT;
14
15
/**
16
 * Responsible for browsing directories.
17
 */
18
public class BrowseButton extends Button {
19
  /**
20
   * Initial directory.
21
   */
22
  private final File mDirectory;
23
24
  /**
25
   * Called when the user accepts a directory.
26
   */
27
  private final Consumer<File> mConsumer;
28
29
  public BrowseButton( final File directory, final Consumer<File> consumer ) {
30
    assert directory != null;
31
    assert consumer != null;
32
33
    mDirectory = directory;
34
    mConsumer = consumer;
35
36
    setGraphic( createGraphic( FILE_ALT ) );
37
    setOnAction( this::browse );
38
  }
39
40
  public void browse( final ActionEvent ignored ) {
41
    final var chooser = new DirectoryChooser();
42
    chooser.setTitle( Messages.get( "BrowseDirectoryButton.chooser.title" ) );
43
    chooser.setInitialDirectory( mDirectory );
44
45
    final var result = chooser.showDialog( getScene().getWindow() );
46
47
    if( result != null ) {
48
      mConsumer.accept( result );
49
    }
50
  }
51
}
152
A src/main/java/com/keenwrite/ui/controls/BrowseFileButton.java
1
/*
2
 * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.keenwrite.ui.controls;
29
30
import com.keenwrite.Messages;
31
import javafx.beans.property.ObjectProperty;
32
import javafx.beans.property.SimpleObjectProperty;
33
import javafx.event.ActionEvent;
34
import javafx.scene.control.Button;
35
import javafx.scene.control.Tooltip;
36
import javafx.scene.input.KeyCode;
37
import javafx.scene.input.KeyEvent;
38
import javafx.stage.FileChooser;
39
import javafx.stage.FileChooser.ExtensionFilter;
40
41
import java.io.File;
42
import java.nio.file.Path;
43
import java.util.ArrayList;
44
import java.util.List;
45
46
import static com.keenwrite.io.SysFile.toFile;
47
import static com.keenwrite.ui.fonts.IconFactory.createGraphic;
48
import static org.controlsfx.glyphfont.FontAwesome.Glyph.FILE_ALT;
49
50
/**
51
 * Button that opens a file chooser to select a local file for a URL.
52
 */
53
public class BrowseFileButton extends Button {
54
55
  private final List<ExtensionFilter> mExtensionFilters = new ArrayList<>();
56
  private final ObjectProperty<Path> mBasePath = new SimpleObjectProperty<>();
57
  private final ObjectProperty<String> mUrl = new SimpleObjectProperty<>();
58
59
  public BrowseFileButton() {
60
    setGraphic( createGraphic( FILE_ALT ) );
61
    setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) );
62
    setOnAction( this::browse );
63
64
    disableProperty().bind( mBasePath.isNull() );
65
66
    // workaround for a JavaFX bug:
67
    //   avoid closing the dialog that contains this control when the user
68
    //   closes the FileChooser or DirectoryChooser using the ESC key
69
    addEventHandler( KeyEvent.KEY_RELEASED, e -> {
70
      if( e.getCode() == KeyCode.ESCAPE ) {
71
        e.consume();
72
      }
73
    } );
74
  }
75
76
  public void addExtensionFilter( ExtensionFilter extensionFilter ) {
77
    mExtensionFilters.add( extensionFilter );
78
  }
79
80
  public ObjectProperty<String> urlProperty() {
81
    return mUrl;
82
  }
83
84
  private void browse( ActionEvent e ) {
85
    var fileChooser = new FileChooser();
86
    fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) );
87
    fileChooser.getExtensionFilters().addAll( mExtensionFilters );
88
    fileChooser.getExtensionFilters()
89
               .add( new ExtensionFilter( Messages.get(
90
                 "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) );
91
    fileChooser.setInitialDirectory( getInitialDirectory() );
92
    var result = fileChooser.showOpenDialog( getScene().getWindow() );
93
    if( result != null ) {
94
      updateUrl( result );
95
    }
96
  }
97
98
  private File getInitialDirectory() {
99
    //TODO build initial directory based on current value of 'url' property
100
    return toFile( getBasePath() );
101
  }
102
103
  private void updateUrl( File file ) {
104
    String newUrl;
105
    try {
106
      newUrl = getBasePath().relativize( file.toPath() ).toString();
107
    } catch( final Exception ex ) {
108
      newUrl = file.toString();
109
    }
110
    mUrl.set( newUrl.replace( '\\', '/' ) );
111
  }
112
113
  public void setBasePath( Path basePath ) {
114
    this.mBasePath.set( basePath );
115
  }
116
117
  private Path getBasePath() {
118
    return mBasePath.get();
119
  }
120
}
1121
A src/main/java/com/keenwrite/ui/controls/EscapeTextField.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.keenwrite.ui.controls;
29
30
import javafx.beans.property.SimpleStringProperty;
31
import javafx.beans.property.StringProperty;
32
import javafx.scene.control.TextField;
33
import javafx.util.StringConverter;
34
35
/**
36
 * Responsible for escaping/unescaping characters for Markdown.
37
 */
38
public class EscapeTextField extends TextField {
39
40
  public EscapeTextField() {
41
    escapedText.bindBidirectional(
42
        textProperty(),
43
        new StringConverter<>() {
44
          @Override
45
          public String toString( String object ) {
46
            return escape( object );
47
          }
48
49
          @Override
50
          public String fromString( String string ) {
51
            return unescape( string );
52
          }
53
        }
54
    );
55
    escapeCharacters.addListener(
56
        e -> escapedText.set( escape( textProperty().get() ) )
57
    );
58
  }
59
60
  // 'escapedText' property
61
  private final StringProperty escapedText = new SimpleStringProperty();
62
63
  public StringProperty escapedTextProperty() {
64
    return escapedText;
65
  }
66
67
  // 'escapeCharacters' property
68
  private final StringProperty escapeCharacters = new SimpleStringProperty();
69
70
  public String getEscapeCharacters() {
71
    return escapeCharacters.get();
72
  }
73
74
  public void setEscapeCharacters( String escapeCharacters ) {
75
    this.escapeCharacters.set( escapeCharacters );
76
  }
77
78
  private String escape( final String s ) {
79
    final String escapeChars = getEscapeCharacters();
80
81
    return isEmpty( escapeChars ) ? s :
82
        s.replaceAll( "([" + escapeChars.replaceAll(
83
            "(.)",
84
            "\\\\$1" ) + "])", "\\\\$1" );
85
  }
86
87
  private String unescape( final String s ) {
88
    final String escapeChars = getEscapeCharacters();
89
90
    return isEmpty( escapeChars ) ? s :
91
        s.replaceAll( "\\\\([" + escapeChars
92
            .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" );
93
  }
94
95
  private static boolean isEmpty( final String s ) {
96
    return s == null || s.isEmpty();
97
  }
98
}
199
A src/main/java/com/keenwrite/ui/controls/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 2017-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.dialogs;
3
4
import com.keenwrite.service.events.impl.ButtonOrderPane;
5
import javafx.scene.control.Dialog;
6
import javafx.stage.Stage;
7
import javafx.stage.Window;
8
9
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
10
import static com.keenwrite.Messages.get;
11
import static javafx.scene.control.ButtonType.CANCEL;
12
import static javafx.scene.control.ButtonType.OK;
13
14
/**
15
 * Superclass that abstracts common behaviours for all dialogs.
16
 *
17
 * @param <T> The type of dialog to create (usually String).
18
 */
19
public abstract class AbstractDialog<T> extends Dialog<T> {
20
21
  /**
22
   * Ensures that all dialogs can be closed.
23
   *
24
   * @param owner The parent window of this dialog.
25
   * @param title The messages title to display in the title bar.
26
   */
27
  @SuppressWarnings( "OverridableMethodCallInConstructor" )
28
  public AbstractDialog( final Window owner, final String title ) {
29
    setTitle( get( title ) );
30
    setResizable( true );
31
32
    initOwner( owner );
33
    initCloseAction();
34
    initDialogPane();
35
    initDialogButtons();
36
    initComponents();
37
38
    if( owner instanceof Stage stage ) {
39
      initIcon( stage );
40
    }
41
  }
42
43
  /**
44
   * Initialize the component layout.
45
   */
46
  protected abstract void initComponents();
47
48
  /**
49
   * Set the dialog to use a button order pane with an OK and a CANCEL button.
50
   */
51
  protected void initDialogPane() {
52
    setDialogPane( new ButtonOrderPane() );
53
  }
54
55
  /**
56
   * Set an OK and CANCEL button on the dialog.
57
   */
58
  protected void initDialogButtons() {
59
    getDialogPane().getButtonTypes().addAll( OK, CANCEL );
60
  }
61
62
  /**
63
   * Attaches a close request to the dialog's [X] button so that the user
64
   * can always close the window, even if there's an error.
65
   */
66
  protected final void initCloseAction() {
67
    final var window = getDialogPane().getScene().getWindow();
68
    window.setOnCloseRequest( event -> window.hide() );
69
  }
70
71
  private void initIcon( final Stage owner ) {
72
    owner.getIcons().add( ICON_DIALOG );
73
  }
74
}
175
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 java.lang.Math.max;
46
import static java.nio.charset.StandardCharsets.UTF_8;
47
import static javafx.application.Platform.runLater;
48
import static javafx.geometry.Pos.CENTER;
49
import static javafx.scene.control.ButtonType.OK;
50
import static org.apache.commons.lang3.StringUtils.abbreviate;
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
      ( c, o, 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( 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/ImageDialog.java
1
/*
2
 * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.ui.dialogs;
28
29
import static com.keenwrite.Messages.get;
30
import com.keenwrite.ui.controls.BrowseFileButton;
31
import com.keenwrite.ui.controls.EscapeTextField;
32
import java.nio.file.Path;
33
import javafx.application.Platform;
34
import javafx.beans.binding.Bindings;
35
import javafx.beans.property.SimpleStringProperty;
36
import javafx.beans.property.StringProperty;
37
import javafx.scene.control.ButtonBar.ButtonData;
38
import static javafx.scene.control.ButtonType.OK;
39
import javafx.scene.control.DialogPane;
40
import javafx.scene.control.Label;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
import javafx.stage.Window;
43
import org.tbee.javafx.scene.layout.fxml.MigPane;
44
45
/**
46
 * Dialog to enter a Markdown image.
47
 */
48
public class ImageDialog extends AbstractDialog<String> {
49
50
  private final StringProperty image = new SimpleStringProperty();
51
52
  public ImageDialog( final Window owner, final Path basePath ) {
53
    super(owner, "Dialog.image.title" );
54
    
55
    final DialogPane dialogPane = getDialogPane();
56
    dialogPane.setContent( pane );
57
58
    linkBrowseFileButton.setBasePath( basePath );
59
    linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
60
    linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() );
61
62
    dialogPane.lookupButton( OK ).disableProperty().bind(
63
      urlField.escapedTextProperty().isEmpty()
64
      .or( textField.escapedTextProperty().isEmpty() ) );
65
66
    image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "![%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.format( "![%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) );
69
    previewField.textProperty().bind( image );
70
71
    setResultConverter( dialogButton -> {
72
      ButtonData data = dialogButton != null ? dialogButton.getButtonData() : null;
73
      return data == ButtonData.OK_DONE ? image.get() : null;
74
    } );
75
76
    Platform.runLater( () -> {
77
      urlField.requestFocus();
78
79
      if( urlField.getText().startsWith( "http://" ) ) {
80
        urlField.selectRange( "http://".length(), urlField.getLength() );
81
      }
82
    } );
83
  }
84
85
  @Override
86
  protected void initComponents() {
87
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
88
    pane = new MigPane();
89
    Label urlLabel = new Label();
90
    urlField = new EscapeTextField();
91
    linkBrowseFileButton = new BrowseFileButton();
92
    Label textLabel = new Label();
93
    textField = new EscapeTextField();
94
    Label titleLabel = new Label();
95
    titleField = new EscapeTextField();
96
    Label previewLabel = new Label();
97
    previewField = new Label();
98
99
    //======== pane ========
100
    {
101
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" );
102
      pane.setRows( "[][][][]" );
103
104
      //---- urlLabel ----
105
      urlLabel.setText( get( "Dialog.image.urlLabel.text" ) );
106
      pane.add( urlLabel, "cell 0 0" );
107
108
      //---- urlField ----
109
      urlField.setEscapeCharacters( "()" );
110
      urlField.setText( "https://yourlink.com" );
111
      urlField.setPromptText( "https://yourlink.com" );
112
      pane.add( urlField, "cell 1 0" );
113
      pane.add( linkBrowseFileButton, "cell 2 0" );
114
115
      //---- textLabel ----
116
      textLabel.setText( get( "Dialog.image.textLabel.text" ) );
117
      pane.add( textLabel, "cell 0 1" );
118
119
      //---- textField ----
120
      textField.setEscapeCharacters( "[]" );
121
      pane.add( textField, "cell 1 1 2 1" );
122
123
      //---- titleLabel ----
124
      titleLabel.setText( get( "Dialog.image.titleLabel.text" ) );
125
      pane.add( titleLabel, "cell 0 2" );
126
      pane.add( titleField, "cell 1 2 2 1" );
127
128
      //---- previewLabel ----
129
      previewLabel.setText( get( "Dialog.image.previewLabel.text" ) );
130
      pane.add( previewLabel, "cell 0 3" );
131
      pane.add( previewField, "cell 1 3 2 1" );
132
    }
133
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
134
  }
135
136
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
137
  private MigPane pane;
138
  private EscapeTextField urlField;
139
  private BrowseFileButton linkBrowseFileButton;
140
  private EscapeTextField textField;
141
  private EscapeTextField titleField;
142
  private Label previewField;
143
	// JFormDesigner - End of variables declaration  //GEN-END:variables
144
}
1145
A src/main/java/com/keenwrite/ui/dialogs/ImageDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "ImageDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "ImageDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
34
				name: "linkBrowseFileButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "javafx.scene.control.Label" ) {
39
				name: "textLabel"
40
				"text": new FormMessage( null, "ImageDialog.textLabel.text" )
41
				auxiliary() {
42
					"JavaCodeGenerator.variableLocal": true
43
				}
44
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
45
				"value": "cell 0 1"
46
			} )
47
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
48
				name: "textField"
49
				"escapeCharacters": "[]"
50
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
51
				"value": "cell 1 1 2 1"
52
			} )
53
			add( new FormComponent( "javafx.scene.control.Label" ) {
54
				name: "titleLabel"
55
				"text": new FormMessage( null, "ImageDialog.titleLabel.text" )
56
				auxiliary() {
57
					"JavaCodeGenerator.variableLocal": true
58
				}
59
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
60
				"value": "cell 0 2"
61
			} )
62
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
63
				name: "titleField"
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 1 2 2 1"
66
			} )
67
			add( new FormComponent( "javafx.scene.control.Label" ) {
68
				name: "previewLabel"
69
				"text": new FormMessage( null, "ImageDialog.previewLabel.text" )
70
				auxiliary() {
71
					"JavaCodeGenerator.variableLocal": true
72
				}
73
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
74
				"value": "cell 0 3"
75
			} )
76
			add( new FormComponent( "javafx.scene.control.Label" ) {
77
				name: "previewField"
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 1 3 2 1"
80
			} )
81
		}, new FormLayoutConstraints( null ) {
82
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
83
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
84
		} )
85
	}
86
}
187
A src/main/java/com/keenwrite/ui/dialogs/LinkDialog.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.ui.dialogs;
29
30
import com.keenwrite.ui.controls.EscapeTextField;
31
import com.keenwrite.editors.markdown.HyperlinkModel;
32
import javafx.application.Platform;
33
import javafx.beans.binding.Bindings;
34
import javafx.beans.property.SimpleStringProperty;
35
import javafx.beans.property.StringProperty;
36
import javafx.scene.control.ButtonBar.ButtonData;
37
import javafx.scene.control.DialogPane;
38
import javafx.scene.control.Label;
39
import javafx.stage.Window;
40
import org.tbee.javafx.scene.layout.fxml.MigPane;
41
42
import static com.keenwrite.Messages.get;
43
import static javafx.scene.control.ButtonType.OK;
44
45
/**
46
 * Dialog to enter a Markdown link.
47
 */
48
public class LinkDialog extends AbstractDialog<String> {
49
50
  private final StringProperty link = new SimpleStringProperty();
51
52
  public LinkDialog(
53
    final Window owner, final HyperlinkModel hyperlink ) {
54
    super( owner, "Dialog.link.title" );
55
56
    final DialogPane dialogPane = getDialogPane();
57
    dialogPane.setContent( pane );
58
59
    dialogPane.lookupButton( OK ).disableProperty().bind(
60
      urlField.escapedTextProperty().isEmpty() );
61
62
    textField.setText( hyperlink.getText() );
63
    urlField.setText( hyperlink.getUrl() );
64
    titleField.setText( hyperlink.getTitle() );
65
66
    link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() )
69
        .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) )
70
        .otherwise( urlField.escapedTextProperty() ) ) );
71
72
    setResultConverter( dialogButton -> {
73
      ButtonData data = dialogButton != null ? dialogButton.getButtonData() : null;
74
      return data == ButtonData.OK_DONE ? link.get() : null;
75
    } );
76
77
    Platform.runLater( () -> {
78
      urlField.requestFocus();
79
      urlField.selectRange( 0, urlField.getLength() );
80
    } );
81
  }
82
83
  @Override
84
  protected void initComponents() {
85
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
86
    pane = new MigPane();
87
    Label urlLabel = new Label();
88
    urlField = new EscapeTextField();
89
    Label textLabel = new Label();
90
    textField = new EscapeTextField();
91
    Label titleLabel = new Label();
92
    titleField = new EscapeTextField();
93
94
    //======== pane ========
95
    {
96
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" );
97
      pane.setRows( "[][][][]" );
98
99
      //---- urlLabel ----
100
      urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
101
      pane.add( urlLabel, "cell 0 0" );
102
103
      //---- urlField ----
104
      urlField.setEscapeCharacters( "()" );
105
      pane.add( urlField, "cell 1 0" );
106
107
      //---- textLabel ----
108
      textLabel.setText( get( "Dialog.link.textLabel.text" ) );
109
      pane.add( textLabel, "cell 0 1" );
110
111
      //---- textField ----
112
      textField.setEscapeCharacters( "[]" );
113
      pane.add( textField, "cell 1 1 3 1" );
114
115
      //---- titleLabel ----
116
      titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
117
      pane.add( titleLabel, "cell 0 2" );
118
      pane.add( titleField, "cell 1 2 3 1" );
119
    }
120
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
121
  }
122
123
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
124
  private MigPane pane;
125
  private EscapeTextField urlField;
126
  private EscapeTextField textField;
127
  private EscapeTextField titleField;
128
  // JFormDesigner - End of variables declaration  //GEN-END:variables
129
}
1130
A src/main/java/com/keenwrite/ui/dialogs/LinkDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "LinkDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "LinkDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) {
34
				name: "linkBrowseDirectoyButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
39
				name: "linkBrowseFileButton"
40
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
41
				"value": "cell 3 0"
42
			} )
43
			add( new FormComponent( "javafx.scene.control.Label" ) {
44
				name: "textLabel"
45
				"text": new FormMessage( null, "LinkDialog.textLabel.text" )
46
				auxiliary() {
47
					"JavaCodeGenerator.variableLocal": true
48
				}
49
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
50
				"value": "cell 0 1"
51
			} )
52
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
53
				name: "textField"
54
				"escapeCharacters": "[]"
55
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
56
				"value": "cell 1 1 3 1"
57
			} )
58
			add( new FormComponent( "javafx.scene.control.Label" ) {
59
				name: "titleLabel"
60
				"text": new FormMessage( null, "LinkDialog.titleLabel.text" )
61
				auxiliary() {
62
					"JavaCodeGenerator.variableLocal": true
63
				}
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 0 2"
66
			} )
67
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
68
				name: "titleField"
69
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
70
				"value": "cell 1 2 3 1"
71
			} )
72
			add( new FormComponent( "javafx.scene.control.Label" ) {
73
				name: "previewLabel"
74
				"text": new FormMessage( null, "LinkDialog.previewLabel.text" )
75
				auxiliary() {
76
					"JavaCodeGenerator.variableLocal": true
77
				}
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 0 3"
80
			} )
81
			add( new FormComponent( "javafx.scene.control.Label" ) {
82
				name: "previewField"
83
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
84
				"value": "cell 1 3 3 1"
85
			} )
86
		}, new FormLayoutConstraints( null ) {
87
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
88
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
89
		} )
90
	}
91
}
192
A src/main/java/com/keenwrite/ui/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.preferences.Workspace;
6
import javafx.beans.property.ObjectProperty;
7
import javafx.scene.Node;
8
import javafx.stage.FileChooser;
9
import javafx.stage.Window;
10
11
import java.io.File;
12
import java.nio.file.Path;
13
import java.util.List;
14
import java.util.Locale;
15
import java.util.Optional;
16
17
import static com.keenwrite.io.SysFile.toFile;
18
import static com.keenwrite.preferences.AppKeys.KEY_UI_RECENT_DIR;
19
import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType.*;
20
import static java.lang.String.format;
21
22
/**
23
 * Shim for a {@link FilePicker} instance that is implemented in pure Java.
24
 * This particular picker is added to avoid using the bug-ridden JavaFX
25
 * {@link FileChooser} that invokes the native file chooser.
26
 */
27
public class FilePickerFactory {
28
  public enum SelectionType {
29
    DIRECTORY_OPEN( "open" ),
30
    FILE_IMPORT( "import" ),
31
    FILE_EXPORT( "export" ),
32
    FILE_OPEN_SINGLE( "open" ),
33
    FILE_OPEN_MULTIPLE( "open" ),
34
    FILE_OPEN_NEW( "open" ),
35
    FILE_SAVE_AS( "save" );
36
37
    private final String mTitle;
38
39
    SelectionType( final String title ) {
40
      assert title != null;
41
      mTitle = Messages.get( format( "Dialog.file.choose.%s.title", title ) );
42
    }
43
44
    public String getTitle() {
45
      return mTitle;
46
    }
47
  }
48
49
  private final ObjectProperty<File> mDirectory;
50
  private final Locale mLocale;
51
52
  public FilePickerFactory( final Workspace workspace ) {
53
    mDirectory = workspace.fileProperty( KEY_UI_RECENT_DIR );
54
    mLocale = workspace.getLocale();
55
  }
56
57
  public FilePicker createModal(
58
    final Window owner, final SelectionType options ) {
59
    final var picker = new NativeFilePicker( owner, options );
60
61
    picker.setInitialDirectory( mDirectory.get().toPath() );
62
63
    return picker;
64
  }
65
66
  public Node createModeless() {
67
    return new FilesView( mDirectory, mLocale );
68
  }
69
70
  /**
71
   * Operating system's file selection dialog.
72
   */
73
  private static final class NativeFilePicker implements FilePicker {
74
    private final FileChooser mChooser = new FileChooser();
75
    private final Window mOwner;
76
    private final SelectionType mType;
77
78
    public NativeFilePicker( final Window owner, final SelectionType type ) {
79
      assert owner != null;
80
      assert type != null;
81
82
      mOwner = owner;
83
      mType = type;
84
    }
85
86
    @Override
87
    public void setInitialFilename( final File file ) {
88
      assert file != null;
89
90
      mChooser.setInitialFileName( file.getName() );
91
    }
92
93
    @Override
94
    public void setInitialDirectory( final Path path ) {
95
      final var file = toFile( path );
96
97
      mChooser.setInitialDirectory(
98
        file.exists() ? file : new File( System.getProperty( "user.home" ) )
99
      );
100
    }
101
102
    @Override
103
    public Optional<List<File>> choose() {
104
      if( mType == FILE_OPEN_MULTIPLE ) {
105
        return Optional.ofNullable( mChooser.showOpenMultipleDialog( mOwner ) );
106
      }
107
108
      final File file = mType == FILE_EXPORT || mType == FILE_SAVE_AS
109
        ? mChooser.showSaveDialog( mOwner )
110
        : mChooser.showOpenDialog( mOwner );
111
112
      return file == null ? Optional.empty() : Optional.of( List.of( file ) );
113
    }
114
  }
115
}
1116
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.Clipboard;
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
        Clipboard.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.Clipboard;
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.remove( 0 );
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
        Clipboard.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( event -> 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/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( treeView -> new AltTreeCell<>( converter ) );
27
    setShowRoot( false );
28
29
    // When focus is lost while not editing, deselect all items.
30
    focusedProperty().addListener( ( c, o, n ) -> {
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
/**
7
 * Responsible for converting objects to and from string instances. The
8
 * tree items contain only strings, so this effectively is a string-to-string
9
 * converter, which allows the implementation to retain its generics.
10
 */
11
public class TreeItemConverter extends StringConverter<String> {
12
13
  @Override
14
  public String toString( final String object ) {
15
    return sanitize( object );
16
  }
17
18
  @Override
19
  public String fromString( final String string ) {
20
    return sanitize( string );
21
  }
22
23
  private String sanitize( final String string ) {
24
    return string == null ? "" : string;
25
  }
26
}
127
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 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.util.*;
5
6
/**
7
 * Responsible for iterating over a list either forwards or backwards. When
8
 * the iterator reaches the last element in the list, the next element will
9
 * be the first. When the iterator reaches the first element in the list,
10
 * the previous element will be the last.
11
 * <p>
12
 * Due to the ability to move forwards and backwards through the list, rather
13
 * than force client classes to track the list index independently, this
14
 * iterator provides an accessor to the index. The index is zero-based.
15
 * </p>
16
 *
17
 * @param <T> The type of list to be cycled.
18
 */
19
public final class CyclicIterator<T> implements ListIterator<T> {
20
  private final List<T> mList;
21
22
  /**
23
   * Initialize to an invalid index so that the first calls to either
24
   * {@link #previous()} or {@link #next()} will return the starting or ending
25
   * element.
26
   */
27
  private int mIndex = -1;
28
29
  /**
30
   * Creates an iterator that cycles indefinitely through the given list.
31
   *
32
   * @param list The list to cycle through indefinitely.
33
   */
34
  public CyclicIterator( final Collection<T> list ) {
35
    mList = new ArrayList<>( list );
36
  }
37
38
  /**
39
   * @return {@code true} if there is at least one element.
40
   */
41
  @Override
42
  public boolean hasNext() {
43
    return !mList.isEmpty();
44
  }
45
46
  /**
47
   * @return {@code true} if there is at least one element.
48
   */
49
  @Override
50
  public boolean hasPrevious() {
51
    return !mList.isEmpty();
52
  }
53
54
  @Override
55
  public int nextIndex() {
56
    return computeIndex( +1 );
57
  }
58
59
  @Override
60
  public int previousIndex() {
61
    return computeIndex( -1 );
62
  }
63
64
  @Override
65
  public void remove() {
66
    mList.remove( mIndex );
67
  }
68
69
  @Override
70
  public void set( final T t ) {
71
    mList.set( mIndex, t );
72
  }
73
74
  @Override
75
  public void add( final T t ) {
76
    mList.add( mIndex, t );
77
  }
78
79
  /**
80
   * Returns the next item in the list, which will cycle to the first
81
   * item as necessary.
82
   *
83
   * @return The next item in the list, cycling to the start if needed.
84
   */
85
  @Override
86
  public T next() {
87
    return cycle( +1 );
88
  }
89
90
  /**
91
   * Returns the previous item in the list, which will cycle to the last
92
   * item as necessary.
93
   *
94
   * @return The previous item in the list, cycling to the end if needed.
95
   */
96
  @Override
97
  public T previous() {
98
    return cycle( -1 );
99
  }
100
101
  /**
102
   * Cycles to the next or previous element, depending on the direction value.
103
   *
104
   * @param direction Use -1 for previous, +1 for next.
105
   * @return The next or previous item in the list.
106
   */
107
  private T cycle( final int direction ) {
108
    try {
109
      return mList.get( mIndex = computeIndex( direction ) );
110
    } catch( final Exception ex ) {
111
      throw new NoSuchElementException( ex );
112
    }
113
  }
114
115
  /**
116
   * Returns the index of the value retrieved from the most recent call to
117
   * either {@link #previous()} or {@link #next()}.
118
   *
119
   * @return The list item index or -1 if no calls have been made to retrieve
120
   * an item from the list.
121
   */
122
  public int getIndex() {
123
    return mIndex;
124
  }
125
126
  private int computeIndex( final int direction ) {
127
    final var i = mIndex + direction;
128
    final var size = mList.size();
129
    final var result = i < 0
130
        ? size - 1
131
        : size == 0 ? 0 : i % size;
132
133
    // Ensure the invariant holds.
134
    assert 0 <= result && result < size || size == 0;
135
136
    return result;
137
  }
138
}
1139
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/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.nio.file.FileSystems.getDefault;
10
11
/**
12
 * Responsible for finding files in a file system that match a particular
13
 * globbing file name pattern.
14
 *
15
 * @see ResourceWalker#walk(String, String, Consumer)
16
 */
17
public class FileWalker {
18
  /**
19
   * Walks the given directory hierarchy for files that match the given
20
   * globbing file name pattern. This will search to a depth of 10 directories
21
   * deep (to avoid infinite recursion).
22
   *
23
   * @param path Root directory to scan for files matching the glob.
24
   * @param glob Only files matching the pattern will be consumed.
25
   * @param c    Function to call for each matching path found.
26
   * @throws IOException Could not walk the tree.
27
   */
28
  public static void walk(
29
    final Path path, final String glob, final Consumer<Path> c )
30
    throws IOException {
31
    final var matcher = getDefault().getPathMatcher( "glob:" + glob );
32
33
    try( final var walk = Files.walk( path, 10 ) ) {
34
      for( final var it = walk.iterator(); it.hasNext(); ) {
35
        final var p = it.next();
36
        if( matcher.matches( p ) ) {
37
          c.accept( p );
38
        }
39
      }
40
    }
41
  }
42
}
143
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/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., '{{'book.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=Export straight quotes and apostrophes as curled equivalents.
37
workspace.typeset.typography.quotes.title=Curl
38
39
workspace.r=R
40
workspace.r.script=Startup Script
41
workspace.r.script.desc=Script runs prior to executing R statements within the document.
42
workspace.r.dir=Working Directory
43
workspace.r.dir.desc=Value assigned to v$application$r$working$directory and usable in the startup script.
44
workspace.r.dir.title=Directory
45
workspace.r.delimiter.began=Delimiter Prefix
46
workspace.r.delimiter.began.desc=Prefix of expression that wraps inserted variables.
47
workspace.r.delimiter.began.title=Opening
48
workspace.r.delimiter.ended=Delimiter Suffix
49
workspace.r.delimiter.ended.desc=Suffix of expression that wraps inserted variables.
50
workspace.r.delimiter.ended.title=Closing
51
52
workspace.images=Images
53
workspace.images.dir=Absolute Directory
54
workspace.images.dir.desc=Path to search for local file system images.
55
workspace.images.dir.title=Directory
56
workspace.images.cache.desc=Path to store remotely retrieved images.
57
workspace.images.cache.title=Directory
58
workspace.images.order=Extensions
59
workspace.images.order.desc=Preferred order of image file types to embed, separated by spaces.
60
workspace.images.order.title=Extensions
61
workspace.images.resize=Resize
62
workspace.images.resize.desc=Scale images to fit the preview panel when resizing, automatically.
63
workspace.images.resize.title=Resize
64
workspace.images.server=Diagram Server
65
workspace.images.server.desc=Server used to generate diagrams (e.g., kroki.io).
66
workspace.images.server.title=Name
67
68
workspace.definition=Variable
69
workspace.definition.path=File name
70
workspace.definition.path.desc=Absolute path to interpolated string variables.
71
workspace.definition.path.title=Path
72
workspace.definition.delimiter.began=Delimiter Prefix
73
workspace.definition.delimiter.began.desc=Indicates when a variable name is starting.
74
workspace.definition.delimiter.began.title=Opening
75
workspace.definition.delimiter.ended=Delimiter Suffix
76
workspace.definition.delimiter.ended.desc=Indicates when a variable name is ending.
77
workspace.definition.delimiter.ended.title=Closing
78
79
workspace.ui.skin=Skins
80
workspace.ui.skin.selection=Bundled
81
workspace.ui.skin.selection.desc=Pre-packaged application style (default: Modena Light).
82
workspace.ui.skin.selection.title=Name
83
workspace.ui.skin.custom=Custom
84
workspace.ui.skin.custom.desc=User-defined JavaFX cascading stylesheet file.
85
workspace.ui.skin.custom.title=Path
86
87
workspace.ui.preview=Preview
88
workspace.ui.preview.stylesheet=Stylesheet
89
workspace.ui.preview.stylesheet.desc=User-defined HTML cascading stylesheet file.
90
workspace.ui.preview.stylesheet.title=Path
91
92
workspace.ui.font=Fonts
93
workspace.ui.font.editor=Editor Font
94
workspace.ui.font.editor.name=Name
95
workspace.ui.font.editor.name.desc=Text editor font name (sans-serif font recommended).
96
workspace.ui.font.editor.name.title=Family
97
workspace.ui.font.editor.size=Size
98
workspace.ui.font.editor.size.desc=Font size.
99
workspace.ui.font.editor.size.title=Points
100
workspace.ui.font.preview=Preview Font
101
workspace.ui.font.preview.name=Name
102
workspace.ui.font.preview.name.desc=Preview pane font name (must support ligatures, serif font recommended).
103
workspace.ui.font.preview.name.title=Family
104
workspace.ui.font.preview.size=Size
105
workspace.ui.font.preview.size.desc=Font size.
106
workspace.ui.font.preview.size.title=Points
107
workspace.ui.font.preview.mono.name=Name
108
workspace.ui.font.preview.mono.name.desc=Monospace font name.
109
workspace.ui.font.preview.mono.name.title=Family
110
workspace.ui.font.preview.mono.size=Size
111
workspace.ui.font.preview.mono.size.desc=Monospace font size.
112
workspace.ui.font.preview.mono.size.title=Points
113
workspace.ui.font.math=Math Font
114
workspace.ui.font.math.size.title=Scale
115
116
workspace.language=Language
117
workspace.language.locale=Internationalization
118
workspace.language.locale.desc=Language for application and HTML export.
119
workspace.language.locale.title=Locale
120
121
# ########################################################################
122
# Editor actions
123
# ########################################################################
124
125
Editor.spelling.check.matches.none=No suggestions for ''{0}'' found.
126
Editor.spelling.check.matches.okay=The spelling for ''{0}'' appears to be correct.
127
128
# ########################################################################
129
# Menu Bar
130
# ########################################################################
131
132
Main.menu.file=_File
133
Main.menu.edit=_Edit
134
Main.menu.insert=_Insert
135
Main.menu.format=Forma_t
136
Main.menu.definition=_Variable
137
Main.menu.view=Vie_w
138
Main.menu.help=_Help
139
140
# ########################################################################
141
# Detachable Tabs
142
# ########################################################################
143
144
# {0} is the application title; {1} is a unique window ID.
145
Detach.tab.title={0} - {1}
146
147
# ########################################################################
148
# Status Bar
149
# ########################################################################
150
151
Main.status.text.offset=offset
152
Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2}
153
Main.status.state.default=OK
154
Main.status.export.success=Saved as ''{0}''
155
156
Main.status.error.bootstrap.eval=Note: Bootstrap variable of ''{0}'' not found
157
Main.status.error.bootstrap.cache=Could not create cache directory ''{0}''
158
159
Main.status.error.parse=Evaluation error: {0}
160
Main.status.error.def.blank=Move the caret to a word before inserting a variable
161
Main.status.error.def.empty=Create a variable before inserting one
162
Main.status.error.def.missing=No variable value found for ''{0}''
163
Main.status.error.r=Error with [{0}...]: {1}
164
165
Main.status.error.file.missing=Not found: ''{0}''
166
Main.status.error.file.missing.near=Not found: ''{0}'' near line {1}
167
Main.status.error.file.delete=Failed to delete ''{0}''
168
169
Main.status.error.messages.recursion=Lookup depth exceeded, check for loops in ''{0}''
170
Main.status.error.messages.syntax=Missing ''}'' in ''{0}''
171
172
Main.status.error.undo=Cannot undo; beginning of undo history reached
173
Main.status.error.redo=Cannot redo; end of redo history reached
174
175
Main.status.error.theme.missing=Install themes before exporting (no themes found at ''{0}'')
176
Main.status.error.theme.name=Cannot find theme name for ''{0}''
177
178
Main.status.image.request.init=Initializing HTTP request
179
Main.status.image.request.fetch=Downloaded image ''{0}''
180
Main.status.image.request.success=Determined content type ''{0}''
181
Main.status.image.request.error.media=No media type for ''{0}''
182
Main.status.image.request.error.cert=Could not accept certificate for ''{0}''
183
Main.status.image.request.error.rasterize=Rasterizer could not parse SVG image
184
185
Main.status.image.xhtml.image.download=Downloading ''{0}''
186
Main.status.image.xhtml.image.resolve=Qualify path for ''{0}''
187
Main.status.image.xhtml.image.found=Found image ''{0}''
188
Main.status.image.xhtml.image.missing=Missing image ''{0}''
189
Main.status.image.xhtml.image.saved=Saved image ''{0}''
190
Main.status.image.xhtml.image.failed=Cannot save image ''{0}''
191
192
Main.status.font.search.missing=No font name starting with ''{0}'' was found
193
194
Main.status.export.concat=Concatenating ''{0}''
195
Main.status.export.concat.parent=No parent directory found for ''{0}''
196
Main.status.export.concat.extension=File name must have an extension ''{0}''
197
Main.status.export.concat.io=Could not read from ''{0}''
198
199
Main.status.typeset.create=Creating typesetter
200
Main.status.typeset.xhtml=Export document as XHTML
201
Main.status.typeset.began=Started typesetting ''{0}''
202
Main.status.typeset.failed=Could not generate PDF file
203
Main.status.typeset.page=Typesetting page {0} of {1} (pass {2})
204
Main.status.typeset.ended.success=Finished typesetting ''{0}'' ({1} elapsed)
205
Main.status.typeset.ended.failure=Failed to typeset ''{0}'' ({1} elapsed)
206
Main.status.typeset.setting=Set {0} to ''{1}''
207
208
Main.status.lexicon.loading=Loading lexicon: {0} words
209
Main.status.lexicon.loaded=Loaded lexicon: {0} words
210
211
# ########################################################################
212
# Search Bar
213
# ########################################################################
214
215
Main.search.stop.tooltip=Close search bar
216
Main.search.stop.icon=CLOSE
217
Main.search.next.tooltip=Find next match
218
Main.search.next.icon=CHEVRON_DOWN
219
Main.search.prev.tooltip=Find previous match
220
Main.search.prev.icon=CHEVRON_UP
221
Main.search.find.tooltip=Search document for text
222
Main.search.find.icon=SEARCH
223
Main.search.match.none=No matches
224
Main.search.match.some={0} of {1} matches
225
226
# ########################################################################
227
# Definition Pane and its Tree View
228
# ########################################################################
229
230
Definition.menu.add.default=Undefined
231
232
# ########################################################################
233
# Variable Definitions Pane
234
# ########################################################################
235
236
Pane.definition.node.root.title=Variables
237
238
# ########################################################################
239
# HTML Preview Pane
240
# ########################################################################
241
242
Pane.preview.title=Preview
243
244
# ########################################################################
245
# Document Outline Pane
246
# ########################################################################
247
248
Pane.outline.title=Outline
249
250
# ########################################################################
251
# File Manager Pane
252
# ########################################################################
253
254
Pane.files.title=Files
255
256
# ########################################################################
257
# Document Outline Pane
258
# ########################################################################
259
260
Pane.statistics.title=Statistics
261
262
# ########################################################################
263
# Failure messages with respect to YAML files.
264
# ########################################################################
265
266
yaml.error.open=Could not open YAML file (ensure non-empty file).
267
yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''.
268
yaml.error.missing=Empty variable value for key ''{0}''.
269
yaml.error.tree.form=Unassigned variable near ''{0}''.
270
271
# ########################################################################
272
# Text Resource
273
# ########################################################################
274
275
TextResource.load.error.unsaved=The file ''{0}'' is unsaved or does not exist.
276
TextResource.load.error.permissions=The file ''{0}'' must be readable and writable.
277
278
TextResource.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
279
TextResource.saveFailed.title=Save
280
281
# ########################################################################
282
# File Open
283
# ########################################################################
284
285
Dialog.file.choose.open.title=Open File
286
Dialog.file.choose.save.title=Save File
287
Dialog.file.choose.export.title=Export File
288
Dialog.file.choose.import.title=Import File
289
290
Dialog.file.choose.filter.title.source=Source Files
291
Dialog.file.choose.filter.title.definition=Variable Files
292
Dialog.file.choose.filter.title.xml=XML Files
293
Dialog.file.choose.filter.title.all=All Files
294
295
# ########################################################################
296
# Browse File
297
# ########################################################################
298
299
BrowseFileButton.chooser.title=Open local file
300
BrowseFileButton.chooser.allFilesFilter=All Files
301
BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
302
303
# ########################################################################
304
# Browse Directory
305
# ########################################################################
306
307
BrowseDirectoryButton.chooser.title=Open local directory
308
BrowseDirectoryButton.tooltip=${BrowseDirectoryButton.chooser.title}
309
310
# ########################################################################
311
# Alert Dialog
312
# ########################################################################
313
314
Alert.file.close.title=Close
315
Alert.file.close.text=Save changes to {0}?
316
317
# ########################################################################
318
# Typesetter Installation Wizard
319
# ########################################################################
320
321
Wizard.typesetter.name=ConTeXt
322
Wizard.typesetter.container.name=Podman
323
Wizard.typesetter.container.version=4.6.2
324
Wizard.typesetter.container.checksum=a51acef00b17cce83dd4d364817af32dd5e541db8d2d13063ae73742744ba3ad
325
Wizard.typesetter.container.image.name=typesetter
326
Wizard.typesetter.container.image.version=3.0.1
327
Wizard.typesetter.container.image.tag=${Wizard.typesetter.container.image.name}:${Wizard.typesetter.container.image.version}
328
Wizard.typesetter.container.image.url=https://repository.keenwrite.com/containers/${Wizard.typesetter.container.image.tag}
329
Wizard.typesetter.themes.version=1.8.4
330
Wizard.typesetter.themes.checksum=b30dd800df89c1d06b2446469e62c12252434946f950a874e1ca225719ba4efc
331
332
Wizard.container.install.command=Installing container using: ''{0}''
333
Wizard.container.install.await=Waiting for installer to finish
334
Wizard.container.install.download.started=Download ''{0}'' started
335
Wizard.container.install.download.running=Download in progress, please wait
336
Wizard.container.process.enter=Running ''{0}'' ''{1}''
337
Wizard.container.process.exit=Process exit code (zero means success): {0}
338
Wizard.container.executable.run.scan=''{0}'' is executable: {1}
339
Wizard.container.executable.run.error=Cannot run container
340
Wizard.container.executable.which=Cannot find container using search command
341
Wizard.container.executable.path=Cannot find container using PATH variable
342
Wizard.container.executable.registry=Cannot find container using registry
343
344
# STEP 1: Introduction panel (all)
345
Wizard.typesetter.all.1.install.title=Install typesetting system
346
Wizard.typesetter.all.1.install.header=Install typesetting system
347
Wizard.typesetter.all.1.install.about.container.link.lbl=${Wizard.typesetter.container.name}
348
Wizard.typesetter.all.1.install.about.container.link.url=https://podman.io
349
Wizard.typesetter.all.1.install.about.text.1=manages the container for the extensive
350
Wizard.typesetter.all.1.install.about.typesetter.link.lbl=${Wizard.typesetter.name}
351
Wizard.typesetter.all.1.install.about.typesetter.link.url=https://contextgarden.net
352
Wizard.typesetter.all.1.install.about.text.2=\
353
  typesetting software, which generates PDF files. This wizard\n\
354
  will guide you through the installation process. After each\n\
355
  step, you'll be prompted to click a button. Click Next to begin.
356
357
# STEP 2: Install container manager (Unix)
358
# Append steps to keep numbers stable; sorted programmatically.
359
Wizard.typesetter.unix.2.install.container.header=Install ${Wizard.typesetter.container.name} for Linux / macOS / Unix
360
# Copy button states
361
Wizard.typesetter.unix.2.install.container.copy.began=Copy
362
Wizard.typesetter.unix.2.install.container.copy.ended=Copied
363
Wizard.typesetter.unix.2.install.container.os=Operating System
364
Wizard.typesetter.unix.2.install.container.step.0=Complete the following steps to install ${Wizard.typesetter.container.name}:
365
Wizard.typesetter.unix.2.install.container.step.1=\t1. Select this computer's ${Wizard.typesetter.unix.2.install.container.os}.
366
Wizard.typesetter.unix.2.install.container.step.2=\t2. Open a new terminal.
367
Wizard.typesetter.unix.2.install.container.step.3=\t3. Run the commands provided below in the terminal.
368
Wizard.typesetter.unix.2.install.container.step.4=\t4. Click Next to continue.
369
Wizard.typesetter.unix.2.install.container.details.prefix=See
370
Wizard.typesetter.unix.2.install.container.details.link.lbl=${Wizard.typesetter.container.name}'s instructions
371
Wizard.typesetter.unix.2.install.container.details.link.url=https://podman.io/getting-started/installation
372
Wizard.typesetter.unix.2.install.container.details.suffix=for more details.
373
Wizard.typesetter.unix.2.install.container.command.distros=14
374
Wizard.typesetter.unix.2.install.container.command.os.name.01=Arch Linux & Manjaro Linux
375
Wizard.typesetter.unix.2.install.container.command.os.text.01=sudo pacman -S podman
376
Wizard.typesetter.unix.2.install.container.command.os.name.02=Alpine Linux
377
Wizard.typesetter.unix.2.install.container.command.os.text.02=sudo apk add podman
378
Wizard.typesetter.unix.2.install.container.command.os.name.03=CentOS
379
Wizard.typesetter.unix.2.install.container.command.os.text.03=sudo yum -y install podman
380
Wizard.typesetter.unix.2.install.container.command.os.name.04=Debian
381
Wizard.typesetter.unix.2.install.container.command.os.text.04=sudo apt-get -y install podman
382
Wizard.typesetter.unix.2.install.container.command.os.name.05=Fedora
383
Wizard.typesetter.unix.2.install.container.command.os.text.05=sudo dnf -y install podman
384
Wizard.typesetter.unix.2.install.container.command.os.name.06=Gentoo
385
Wizard.typesetter.unix.2.install.container.command.os.text.06=sudo emerge app-containers/podman
386
Wizard.typesetter.unix.2.install.container.command.os.name.07=OpenEmbedded
387
Wizard.typesetter.unix.2.install.container.command.os.text.07=bitbake podman
388
Wizard.typesetter.unix.2.install.container.command.os.name.08=openSUSE
389
Wizard.typesetter.unix.2.install.container.command.os.text.08=sudo zypper install podman
390
Wizard.typesetter.unix.2.install.container.command.os.name.09=RHEL7
391
Wizard.typesetter.unix.2.install.container.command.os.text.09=\
392
  sudo subscription-manager repos \
393
    --enable=rhel-7-server-extras-rpms\n\
394
  sudo yum -y install podman
395
Wizard.typesetter.unix.2.install.container.command.os.name.10=RHEL8
396
Wizard.typesetter.unix.2.install.container.command.os.text.10=\
397
  sudo yum module enable -y container-tools:rhel8\n\
398
  sudo yum module install -y container-tools:rhel8
399
Wizard.typesetter.unix.2.install.container.command.os.name.11=Ubuntu 20.10+
400
Wizard.typesetter.unix.2.install.container.command.os.text.11=\
401
  sudo apt-get -y update\n\
402
  sudo apt-get -y install podman
403
Wizard.typesetter.unix.2.install.container.command.os.name.12=Linuxmint
404
Wizard.typesetter.unix.2.install.container.command.os.text.12=${Wizard.typesetter.unix.2.install.container.command.os.text.11}
405
Wizard.typesetter.unix.2.install.container.command.os.name.13=Linuxmint LMDE
406
Wizard.typesetter.unix.2.install.container.command.os.text.13=${Wizard.typesetter.unix.2.install.container.command.os.text.04}
407
Wizard.typesetter.unix.2.install.container.command.os.name.14=macOS
408
Wizard.typesetter.unix.2.install.container.command.os.text.14=\
409
  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \n\
410
  brew install podman
411
412
# STEP 2 a: Download container manager (Windows)
413
Wizard.typesetter.win.2.download.container.header=Download ${Wizard.typesetter.container.name} for Windows
414
Wizard.typesetter.win.2.download.container.homepage.link.lbl=${Wizard.typesetter.container.name}
415
Wizard.typesetter.win.2.download.container.homepage.link.url=https://podman.io
416
Wizard.typesetter.win.2.download.container.download.link.lbl=repository
417
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
418
Wizard.typesetter.win.2.download.container.paths=Downloading {0} into {1}.
419
# suppress inspection "UnusedMessageFormatParameter"
420
Wizard.typesetter.win.2.download.container.status.bytes=Downloaded {1} bytes (size unknown).
421
Wizard.typesetter.win.2.download.container.status.progress=Downloaded {0} % of {1} bytes.
422
Wizard.typesetter.win.2.download.container.status.checksum.ok=File {0} exists. Click Next to continue.
423
Wizard.typesetter.win.2.download.container.status.checksum.no=Integrity check failed, {0} may be corrupt.
424
Wizard.typesetter.win.2.download.container.status.success=Download successful. Click Next to continue.
425
Wizard.typesetter.win.2.download.container.status.failure=Download failed. Check network then click Previous to try again.
426
427
# STEP 2 b: Install container manager (Windows)
428
Wizard.typesetter.win.2.install.container.header=Install ${Wizard.typesetter.container.name} for Windows
429
Wizard.typesetter.win.2.install.container.step.0=Complete the following steps to install ${Wizard.typesetter.container.name}:
430
Wizard.typesetter.win.2.install.container.step.1=\t1. Open the task bar.
431
Wizard.typesetter.win.2.install.container.step.2=\t2. Click the shield icon to grant permissions.
432
Wizard.typesetter.win.2.install.container.step.3=\t3. Click Yes in the User Account Control dialog to install.
433
Wizard.typesetter.win.2.install.container.status.running=Installing ...
434
Wizard.typesetter.win.2.install.container.status.success=Installation successful.\nClick Next to continue.
435
Wizard.typesetter.win.2.install.container.status.failure=Installation failed with exit code {0}.
436
Wizard.typesetter.win.2.install.container.status.unknown=Could not determine installer file type: {0}
437
438
# STEP 2: Install container manager (Universal, undetected operating system)
439
Wizard.typesetter.all.2.install.container.header=Install ${Wizard.typesetter.container.name}
440
Wizard.typesetter.all.2.install.container.homepage.lbl=${Wizard.typesetter.container.name}
441
Wizard.typesetter.all.2.install.container.homepage.url=https://podman.io
442
443
# STEP 3: Initialize container manager (all except Linux)
444
Wizard.typesetter.all.3.install.container.header=Initialize ${Wizard.typesetter.container.name}
445
Wizard.typesetter.all.3.install.container.correct=${Wizard.typesetter.container.name} initialized.\nClick Next to continue.
446
Wizard.typesetter.all.3.install.container.missing=Install ${Wizard.typesetter.container.name} before continuing.
447
448
# STEP 4: Install typesetter container image (all)
449
Wizard.typesetter.all.4.download.image.header=Download ${Wizard.typesetter.name} image
450
Wizard.typesetter.all.4.download.image.correct=Download successful.\nClick Next to continue.
451
Wizard.typesetter.all.4.download.image.missing=Install ${Wizard.typesetter.container.name} before continuing.
452
453
# STEP 5: Download typesetter themes (all)
454
Wizard.typesetter.all.5.download.themes.header=Download ${Wizard.typesetter.name} themes
455
Wizard.typesetter.all.5.download.themes.download.link.lbl=repository
456
Wizard.typesetter.all.5.download.themes.download.link.url=https://gitlab.com/DaveJarvis/keenwrite-themes/-/releases/${Wizard.typesetter.themes.version}/downloads/theme-pack.zip
457
Wizard.typesetter.all.5.download.themes.paths=Downloading {0} into {1}.
458
Wizard.typesetter.all.5.download.themes.status.bytes=Downloaded {0} bytes (size unknown).
459
Wizard.typesetter.all.5.download.themes.status.progress=Downloaded {0} % of {1} bytes.
460
Wizard.typesetter.all.5.download.themes.status.checksum.ok=File {0} exists. Click Finish to continue.
461
Wizard.typesetter.all.5.download.themes.status.checksum.no=Integrity check failed, {0} may be corrupt.
462
Wizard.typesetter.all.5.download.themes.status.success=Download successful. Click Finish to continue.
463
Wizard.typesetter.all.5.download.themes.status.failure=Download failed. Check network then click Previous to try again.
464
465
# ########################################################################
466
# Image Dialog
467
# ########################################################################
468
469
Dialog.image.title=Image
470
Dialog.image.chooser.imagesFilter=Images
471
Dialog.image.previewLabel.text=Markdown Preview\:
472
Dialog.image.textLabel.text=Alternate Text\:
473
Dialog.image.titleLabel.text=Title (tooltip)\:
474
Dialog.image.urlLabel.text=Image URL\:
475
476
# ########################################################################
477
# Hyperlink Dialog
478
# ########################################################################
479
480
Dialog.link.title=Link
481
Dialog.link.previewLabel.text=Markdown Preview\:
482
Dialog.link.textLabel.text=Link Text\:
483
Dialog.link.titleLabel.text=Title (tooltip)\:
484
Dialog.link.urlLabel.text=Link URL\:
485
486
# ########################################################################
487
# Typesetting Settings Dialog
488
# ########################################################################
489
490
Dialog.typesetting.settings.title=Typesetting export settings
491
Dialog.typesetting.settings.header.single=Export current document
492
Dialog.typesetting.settings.theme=Theme
493
Dialog.typesetting.settings.themes.missing=Install themes into {0}.
494
495
Dialog.typesetting.settings.header.multiple=Export multiple documents
496
Dialog.typesetting.settings.chapters=Chapters (e.g., 1-3, 5, 7-)
497
498
# ########################################################################
499
# About Dialog
500
# ########################################################################
501
502
Dialog.about.title=About {0}
503
Dialog.about.header={0}
504
Dialog.about.content=Copyright 2016-{0} White Magic Software, Ltd.\n\nVersion: {1}
505
506
# ########################################################################
507
# Application Actions
508
# ########################################################################
509
510
Action.file.new.description=Create a new file
511
Action.file.new.accelerator=Shortcut+N
512
Action.file.new.icon=FILE_ALT
513
Action.file.new.text=_New
514
515
Action.file.open.description=Open a new file
516
Action.file.open.accelerator=Shortcut+O
517
Action.file.open.text=_Open...
518
Action.file.open.icon=FOLDER_OPEN_ALT
519
520
Action.file.close.description=Close the current document
521
Action.file.close.accelerator=Shortcut+W
522
Action.file.close.text=_Close
523
524
Action.file.close_all.description=Close all open documents
525
Action.file.close_all.accelerator=Ctrl+F4
526
Action.file.close_all.text=Close All
527
528
Action.file.save.description=Save the document
529
Action.file.save.accelerator=Shortcut+S
530
Action.file.save.text=_Save
531
Action.file.save.icon=FLOPPY_ALT
532
533
Action.file.save_as.description=Rename the current document
534
Action.file.save_as.text=Save _As
535
536
Action.file.save_all.description=Save all open documents
537
Action.file.save_all.accelerator=Shortcut+Shift+S
538
Action.file.save_all.text=Save A_ll
539
540
Action.file.export.pdf.description=Typeset the document
541
Action.file.export.pdf.accelerator=Shortcut+P
542
Action.file.export.pdf.text=_PDF
543
Action.file.export.pdf.icon=FILE_PDF_ALT
544
545
Action.file.export.pdf.dir.description=Typeset files in document directory
546
Action.file.export.pdf.dir.accelerator=Shortcut+Shift+P
547
Action.file.export.pdf.dir.text=_Joined PDF
548
Action.file.export.pdf.dir.icon=FILE_PDF_ALT
549
550
Action.file.export.pdf.repeat.description=Repeat previous typesetting command
551
Action.file.export.pdf.repeat.accelerator=Shortcut+Shift+E
552
Action.file.export.pdf.repeat.text=_Repeat Export
553
Action.file.export.pdf.repeat.icon=FILE_PDF_ALT
554
555
Action.file.export.html.dir.description=Export files in document directory as HTML
556
Action.file.export.html.dir.accelerator=Shortcut+Shift+H
557
Action.file.export.html.dir.text=Joined _HTML
558
Action.file.export.html.dir.icon=HTML5
559
560
Action.file.export.html_svg.description=Export the current document as HTML + SVG
561
Action.file.export.text=_Export As
562
Action.file.export.html_svg.text=HTML and S_VG
563
564
Action.file.export.html_tex.description=Export the current document as HTML + TeX
565
Action.file.export.html_tex.text=HTML and _TeX
566
567
Action.file.export.xhtml_tex.description=Export as XHTML + TeX
568
Action.file.export.xhtml_tex.text=_XHTML and TeX
569
570
Action.file.export.markdown.description=Export the current document as Markdown
571
Action.file.export.markdown.text=Markdown
572
573
Action.file.exit.description=Quit the application
574
Action.file.exit.text=E_xit
575
576
577
Action.edit.undo.description=Undo the previous edit
578
Action.edit.undo.accelerator=Shortcut+Z
579
Action.edit.undo.text=_Undo
580
Action.edit.undo.icon=UNDO
581
582
Action.edit.redo.description=Redo the previous edit
583
Action.edit.redo.accelerator=Shortcut+Y
584
Action.edit.redo.text=_Redo
585
Action.edit.redo.icon=REPEAT
586
587
Action.edit.cut.description=Delete the selected text or line
588
Action.edit.cut.accelerator=Shortcut+X
589
Action.edit.cut.text=Cu_t
590
Action.edit.cut.icon=CUT
591
592
Action.edit.copy.description=Copy the selected text
593
Action.edit.copy.accelerator=Shortcut+C
594
Action.edit.copy.text=_Copy
595
Action.edit.copy.icon=COPY
596
597
Action.edit.paste.description=Paste from the clipboard
598
Action.edit.paste.accelerator=Shortcut+V
599
Action.edit.paste.text=_Paste
600
Action.edit.paste.icon=PASTE
601
602
Action.edit.select_all.description=Highlight the current document text
603
Action.edit.select_all.accelerator=Shortcut+A
604
Action.edit.select_all.text=Select _All
605
606
Action.edit.find.description=Search for text in the document
607
Action.edit.find.accelerator=Shortcut+F
608
Action.edit.find.text=_Find
609
Action.edit.find.icon=SEARCH
610
611
Action.edit.find_next.description=Find next occurrence
612
Action.edit.find_next.accelerator=F3
613
Action.edit.find_next.text=Find _Next
614
615
Action.edit.find_prev.description=Find previous occurrence
616
Action.edit.find_prev.accelerator=Shift+F3
617
Action.edit.find_prev.text=Find _Prev
618
619
Action.edit.preferences.description=Edit user preferences
620
Action.edit.preferences.accelerator=Ctrl+Alt+S
621
Action.edit.preferences.text=_Preferences
622
623
624
Action.format.bold.description=Insert strong text
625
Action.format.bold.accelerator=Shortcut+B
626
Action.format.bold.text=_Bold
627
Action.format.bold.icon=BOLD
628
629
Action.format.italic.description=Insert text emphasis
630
Action.format.italic.accelerator=Shortcut+I
631
Action.format.italic.text=_Italic
632
Action.format.italic.icon=ITALIC
633
634
Action.format.monospace.description=Insert monospace text
635
Action.format.monospace.accelerator=Shortcut+`
636
Action.format.monospace.text=_Monospace
637
638
Action.format.superscript.description=Insert superscript text
639
Action.format.superscript.accelerator=Shortcut+[
640
Action.format.superscript.text=Su_perscript
641
Action.format.superscript.icon=SUPERSCRIPT
642
643
Action.format.subscript.description=Insert subscript text
644
Action.format.subscript.accelerator=Shortcut+]
645
Action.format.subscript.text=Su_bscript
646
Action.format.subscript.icon=SUBSCRIPT
647
648
Action.format.strikethrough.description=Insert struck text
649
Action.format.strikethrough.accelerator=Shortcut+T
650
Action.format.strikethrough.text=Stri_kethrough
651
Action.format.strikethrough.icon=STRIKETHROUGH
652
653
654
Action.insert.blockquote.description=Insert blockquote
655
Action.insert.blockquote.accelerator=Ctrl+Q
656
Action.insert.blockquote.text=_Blockquote
657
Action.insert.blockquote.icon=QUOTE_LEFT
658
659
Action.insert.code.description=Insert inline code
660
Action.insert.code.accelerator=Shortcut+K
661
Action.insert.code.text=Inline _Code
662
Action.insert.code.icon=CODE
663
664
Action.insert.fenced_code_block.description=Insert code block
665
Action.insert.fenced_code_block.accelerator=Shortcut+Shift+K
666
Action.insert.fenced_code_block.text=_Fenced Code Block
667
Action.insert.fenced_code_block.prompt.text=Enter code here
668
Action.insert.fenced_code_block.icon=FILE_CODE_ALT
669
670
Action.insert.link.description=Insert hyperlink
671
Action.insert.link.accelerator=Shortcut+L
672
Action.insert.link.text=_Link...
673
Action.insert.link.icon=LINK
674
675
Action.insert.image.description=Insert image
676
Action.insert.image.accelerator=Shortcut+G
677
Action.insert.image.text=_Image...
678
Action.insert.image.icon=PICTURE_ALT
679
680
Action.insert.heading.description=Insert heading level
681
Action.insert.heading.accelerator=Shortcut+
682
Action.insert.heading.icon=HEADER
683
684
Action.insert.heading_1.description=${Action.insert.heading.description} 1
685
Action.insert.heading_1.accelerator=${Action.insert.heading.accelerator}1
686
Action.insert.heading_1.text=Heading _1
687
Action.insert.heading_1.icon=${Action.insert.heading.icon}
688
689
Action.insert.heading_2.description=${Action.insert.heading.description} 2
690
Action.insert.heading_2.accelerator=${Action.insert.heading.accelerator}2
691
Action.insert.heading_2.text=Heading _2
692
Action.insert.heading_2.icon=${Action.insert.heading.icon}
693
694
Action.insert.heading_3.description=${Action.insert.heading.description} 3
695
Action.insert.heading_3.accelerator=${Action.insert.heading.accelerator}3
696
Action.insert.heading_3.text=Heading _3
697
Action.insert.heading_3.icon=${Action.insert.heading.icon}
698
699
Action.insert.unordered_list.description=Insert bulleted list
700
Action.insert.unordered_list.accelerator=Shortcut+U
701
Action.insert.unordered_list.text=_Unordered List
702
Action.insert.unordered_list.icon=LIST_UL
703
704
Action.insert.ordered_list.description=Insert enumerated list
705
Action.insert.ordered_list.accelerator=Shortcut+Shift+O
706
Action.insert.ordered_list.text=_Ordered List
707
Action.insert.ordered_list.icon=LIST_OL
708
709
Action.insert.horizontal_rule.description=Insert horizontal rule
710
Action.insert.horizontal_rule.accelerator=Shortcut+H
711
Action.insert.horizontal_rule.text=_Horizontal Rule
712
Action.insert.horizontal_rule.icon=LIST_OL
713
714
715
Action.definition.create.description=Create a new variable
716
Action.definition.create.text=_Create
717
Action.definition.create.icon=TREE
718
Action.definition.create.tooltip=Add new item (Insert)
719
720
Action.definition.rename.description=Rename the selected variable
721
Action.definition.rename.text=_Rename
722
Action.definition.rename.icon=EDIT
723
Action.definition.rename.tooltip=Rename selected item (F2)
724
725
Action.definition.delete.description=Delete the selected variables
726
Action.definition.delete.text=De_lete
727
Action.definition.delete.icon=TRASH
728
Action.definition.delete.tooltip=Delete selected items (Delete)
729
730
Action.definition.insert.description=Insert a variable
731
Action.definition.insert.accelerator=Ctrl+Space
732
Action.definition.insert.text=_Insert
733
Action.definition.insert.icon=STAR
734
735
736
Action.view.refresh.description=Clear all caches
737
Action.view.refresh.accelerator=F5
738
Action.view.refresh.text=Refresh
739
740
Action.view.preview.description=Open document preview
741
Action.view.preview.accelerator=F6
742
Action.view.preview.text=Preview
743
744
Action.view.outline.description=Open document outline
745
Action.view.outline.accelerator=F7
746
Action.view.outline.text=Outline
747
748
Action.view.statistics.description=Open document word counts
749
Action.view.statistics.accelerator=F8
750
Action.view.statistics.text=Statistics
751
752
Action.view.files.description=Open file manager
753
Action.view.files.accelerator=Ctrl+F8
754
Action.view.files.text=Files
755
756
Action.view.menubar.description=Toggle menu bar
757
Action.view.menubar.accelerator=Ctrl+F9
758
Action.view.menubar.text=Menu bar
759
760
Action.view.toolbar.description=Toggle toolbar
761
Action.view.toolbar.accelerator=Ctrl+Shift+F9
762
Action.view.toolbar.text=Toolbar
763
764
Action.view.statusbar.description=Toggle status bar
765
Action.view.statusbar.accelerator=Ctrl+Shift+Alt+F9
766
Action.view.statusbar.text=Status bar
767
768
Action.view.log.description=Open document issues
769
Action.view.log.accelerator=F12
770
Action.view.log.text=Log
771
772
773
Action.help.about.description=Show help dialog
774
Action.help.about.accelerator=F1
775
Action.help.about.text=About
776
Action.help.about.icon=INFO
1777
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
/* ITEMIZED LISTS ***/
81
ol, ul {
82
  margin: 0 0 0 2em;
83
}
84
85
ol { list-style-type: decimal; }
86
ol ol { list-style-type: lower-alpha; }
87
ol ol ol { list-style-type: lower-roman; }
88
ol ol ol ol { list-style-type: upper-alpha; }
89
ol ol ol ol ol { list-style-type: upper-roman; }
90
ol ol ol ol ol ol { list-style-type: lower-greek; }
91
92
ul { list-style-type: disc; }
93
ul ul { list-style-type: circle; }
94
ul ul ul { list-style-type: square; }
95
ul ul ul ul { list-style-type: disc; }
96
ul ul ul ul ul { list-style-type: circle; }
97
ul ul ul ul ul ul { list-style-type: square; }
98
99
/* DEFINITION LISTS ***/
100
dl {
101
  /** Horizontal scroll bar will appear if set to 100%. */
102
  width: 99%;
103
  overflow: hidden;
104
  padding-left: 1em;
105
}
106
107
dl dt {
108
  font-weight: bold;
109
  float: left;
110
  width: 20%;
111
  clear: both;
112
  position: relative;
113
}
114
115
dl dd {
116
  float: right;
117
  width: 79%;
118
  padding-bottom: .5em;
119
  margin-left: 0;
120
}
121
122
/* PREFORMATTED CODE ***/
123
pre, code, tt {
124
  font-family: 'Source Code Pro';
125
  font-size: 13px;
126
  background-color: #f8f8f8;
127
  text-decoration: none;
128
  white-space: pre-wrap;
129
  word-wrap: break-word;
130
  overflow-wrap: anywhere;
131
  border-radius: .125em;
132
}
133
134
code, tt {
135
  padding: .25em;
136
}
137
138
pre > code {
139
  padding: 0;
140
  border: none;
141
  background: transparent;
142
}
143
144
pre {
145
  border: .125em solid #ccc;
146
  overflow: auto;
147
  padding: .25em .5em;
148
}
149
150
pre code, pre tt {
151
  background-color: transparent;
152
  border: none;
153
}
154
155
/* BLOCKQUOTES ***/
156
blockquote {
157
  border-left: .25em solid #ccc;
158
  padding: 0 1em;
159
  color: #777;
160
}
161
162
blockquote>:first-child {
163
  margin-top: 0;
164
}
165
166
blockquote>:last-child {
167
  margin-bottom: 0;
168
}
169
170
/* TABLES ***/
171
table {
172
  width: 100%;
173
}
174
175
tr:nth-child(odd) {
176
  background-color: #eee;
177
}
178
179
th {
180
  background-color: #454545;
181
  color: #fff;
182
}
183
184
th, td {
185
  text-align: left;
186
  padding: 0 1em;
187
}
188
189
/* IMAGES ***/
190
img {
191
  max-width: 100%;
192
193
  /* Tell FlyingSaucer to treat images as block elements.
194
   * See SvgReplacedElementFactory.
195
   */
196
  display: inline-block;
197
}
198
199
/* TEX ***/
200
201
/* Tell FlyingSaucer to treat tex elements as nodes.
202
 * See SvgReplacedElementFactory.
203
 */
204
tex {
205
  /* Ensure the formulas can be inlined with text. */
206
  display: inline-block;
207
}
208
209
/* Without a robust typesetting engine, there's no
210
 * nice-looking way to automatically typeset equations.
211
 * Sometimes baseline is appropriate, sometimes the
212
 * descender must be considered, and sometimes vertical
213
 * alignment to the middle looks best.
214
 */
215
p tex {
216
  vertical-align: baseline;
217
}
218
219
/* RULES ***/
220
hr {
221
  clear: both;
222
  margin: 1.5em 0 1.5em;
223
  height: 0;
224
  overflow: hidden;
225
  border: none;
226
  background: transparent;
227
  border-bottom: .125em solid #ccc;
228
}
229
230
/* EMAIL ***/
231
div.email {
232
  padding: 0 1.5em;
233
  text-align: left;
234
  text-indent: 0;
235
  border-style: solid;
236
  border-width: 0.05em;
237
  border-radius: .25em;
238
  background-color: #f8f8f8;
239
}
240
241
/* TO DO ***/
242
div.todo:before {
243
  content: "TODO";
244
  color: #c00;
245
  font-weight: bold;
246
  display: block;
247
  width: 100%;
248
  text-align: center;
249
  padding: 0;
250
  margin: 0;
251
}
252
253
div.todo {
254
  border-color: #c00;
255
  background-color: #f8f8f8;
256
}
257
258
div.todo, div.terminal {
259
  padding: .5em;
260
  padding-top: .25em;
261
  padding-bottom: .25em;
262
  border-style: solid;
263
  border-width: 0.05em;
264
  border-radius: .25em;
265
}
266
267
/* TERMINAL ***/
268
div.terminal {
269
  font-family: 'Source Code Pro';
270
  font-size: 90%;
271
  border-color: #222;
272
}
273
274
/* SPEECH BUBBLE ***/
275
div.bubblerx, div.bubbletx {
276
  display: table;
277
  padding: .5em;
278
  padding-top: .25em;
279
  padding-bottom: .25em;
280
  margin: 1em;
281
  position: relative;
282
  border-radius: .25em;
283
  background-color: #ccc;
284
285
  font-family: 'OpenSansEmoji', sans-serif;
286
  font-size: 95%;
287
}
288
289
/* Transmit bubble on the right. */
290
div.bubbletx {
291
  margin-left: auto;
292
}
293
294
div.bubblerx:after, div.bubbletx:after {
295
  content: "";
296
  position: absolute;
297
  width: 0;
298
  height: 0;
299
  top: .5em;
300
  border-top: 1em solid transparent;
301
  border-bottom: 1em solid transparent;
302
}
303
304
div.bubblerx::after {
305
  left: -1em;
306
  right: auto;
307
  border-right: 1em solid #ccc;
308
  border-left: none;
309
}
310
311
div.bubbletx:after {
312
  right: -1em;
313
  border-left: 1em solid #ccc;
314
}
315
316
/* LYRICS ***/
317
div.lyrics p {
318
  margin: 0;
319
  padding: 0;
320
  white-space: pre-line;
321
  font-style: italic;
322
}
323
324
div.lyrics:first-line p {
325
  line-height: 0;
326
}
327
328
div.typewritten {
329
  font-family: monospace;
330
  font-size: 16px;
331
  font-weight: bold;
332
}
333
1334
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=untitled.md
52
file.default.definition=variables.yaml
53
54
# Default file name to be replaced by the most
55
# recently exported file name.
56
file.default.pdf=untitled.pdf
57
58
# ########################################################################
59
# File name Extensions
60
# ########################################################################
61
62
# Comma-separated list of definition file name extensions.
63
definition.file.ext.json=*.json
64
definition.file.ext.toml=*.toml
65
definition.file.ext.yaml=*.yml,*.yaml
66
definition.file.ext.properties=*.properties,*.props
67
68
# Comma-separated list of file name extensions.
69
file.ext.rmarkdown=*.Rmd
70
file.ext.rxml=*.Rxml
71
file.ext.source=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown},${file.ext.rxml}
72
file.ext.definition=${definition.file.ext.yaml}
73
file.ext.xml=*.xml,${file.ext.rxml}
74
file.ext.all=*.*
75
76
# File name extension search order for images.
77
file.ext.image.order=svg pdf png jpg tiff
78
79
# ########################################################################
80
# Variable Name Editor
81
# ########################################################################
82
83
# Maximum number of characters for a variable name. A variable is defined
84
# as one or more non-whitespace characters up to this maximum length.
85
editor.variable.maxLength=256
86
87
# ########################################################################
88
# Dialog Preferences
89
# ########################################################################
90
91
dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R
92
dialog.alert.button.order.linux=L_HE+UNYACBXIO_R
93
dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R
94
95
# Ensures a consistent button order for alert dialogs across platforms (because
96
# the default button order on Linux defies all logic).
97
dialog.alert.button.order=${dialog.alert.button.order.windows}
198
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
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
.tool-bar {
29
  -fx-spacing: 0;
30
}
31
32
.tool-bar .button {
33
  -fx-background-color: transparent;
34
}
35
36
.tool-bar .button:hover {
37
  -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
38
  -fx-color: -fx-hover-base;
39
}
40
41
.tool-bar .button:armed {
42
  -fx-color: -fx-pressed-base;
43
}
44
45
/* Definition editor drag and drop target.
46
 */
47
.drop-target {
48
  -fx-border-color: #eea82f;
49
  -fx-border-width: 0 0 2 0;
50
  -fx-padding: 3 3 1 3
51
}
152
A src/main/resources/com/keenwrite/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/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.vladsch.flexmark.ext.definition.DefinitionExtension;
6
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
7
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
8
import com.vladsch.flexmark.ext.tables.TablesExtension;
9
import com.vladsch.flexmark.html.HtmlRenderer;
10
import com.vladsch.flexmark.parser.Parser;
11
import com.vladsch.flexmark.util.data.MutableDataSet;
12
import com.vladsch.flexmark.util.misc.Extension;
13
import org.junit.jupiter.params.ParameterizedTest;
14
import org.junit.jupiter.params.provider.Arguments;
15
import org.junit.jupiter.params.provider.MethodSource;
16
17
import java.util.ArrayList;
18
import java.util.List;
19
import java.util.stream.Stream;
20
21
import static org.junit.jupiter.api.Assertions.assertEquals;
22
23
/**
24
 * Test that basic styles for conversion exports as expected.
25
 */
26
public class ParserTest {
27
28
  @ParameterizedTest
29
  @MethodSource( "markdownParameters" )
30
  void test_Conversion_Markdown_Html( final String md, final String expected ) {
31
    final var extensions = createExtensions();
32
    final var options = new MutableDataSet();
33
    final var parser = Parser
34
      .builder( options )
35
      .extensions( extensions )
36
      .build();
37
    final var renderer = HtmlRenderer
38
      .builder( options )
39
      .extensions( extensions )
40
      .build();
41
42
    final var document = parser.parse( md );
43
    final var actual = renderer.render( document );
44
45
    assertEquals( expected, actual );
46
  }
47
48
  private List<Extension> createExtensions() {
49
    final var extensions = new ArrayList<Extension>();
50
51
    extensions.add( DefinitionExtension.create() );
52
    extensions.add( StrikethroughSubscriptExtension.create() );
53
    extensions.add( SuperscriptExtension.create() );
54
    extensions.add( TablesExtension.create() );
55
    extensions.add( FencedDivExtension.create() );
56
57
    return extensions;
58
  }
59
60
  private static Stream<Arguments> markdownParameters() {
61
    return Stream.of(
62
      Arguments.of(
63
        "*emphasis* _emphasis_ **strong**",
64
        "<p><em>emphasis</em> <em>emphasis</em> <strong>strong</strong></p>\n"
65
      ),
66
      Arguments.of(
67
        "the \uD83D\uDC4D emoji",
68
        "<p>the \uD83D\uDC4D emoji</p>\n"
69
      )
70
    );
71
  }
72
}
173
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 java.io.File.separator;
11
import static java.lang.String.format;
12
import static java.nio.charset.StandardCharsets.UTF_8;
13
14
/**
15
 * Tests file resource allocation.
16
 */
17
public class FileObjectTest {
18
  private static final String TEMP_DIR = System.getProperty( "java.io.tmpdir" );
19
20
  /**
21
   * Test that resources are not exhausted.
22
   * <p>
23
   * Disabled because no issue was found and this test thrashes the I/O.
24
   * </p>
25
   */
26
  @Disabled
27
  void test_Open_MultipleFiles_NoResourcesExpire() throws FileSystemException {
28
    final var builder = new SessionBuilder();
29
    final var session = builder.build();
30
31
    for( int i = 0; i < 10000; i++ ) {
32
      final var filename = format( "%s%s%d.txt", TEMP_DIR, separator, i );
33
      final var fileObject = session
34
        .getFileSystemManager()
35
        .resolveFile( filename );
36
37
      try(
38
        final var stream = fileObject.getContent().getOutputStream();
39
        final var writer = new OutputStreamWriter( stream, UTF_8 ) ) {
40
        writer.write( "contents" );
41
      } catch( final IOException e ) {
42
        throw new FileSystemException( e );
43
      }
44
45
      fileObject.delete();
46
    }
47
  }
48
}
149
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.file.StandardOpenOption.APPEND;
15
import static java.nio.file.StandardOpenOption.CREATE;
16
import static java.util.concurrent.TimeUnit.SECONDS;
17
import static org.junit.jupiter.api.Assertions.assertEquals;
18
19
/**
20
 * Responsible for testing that the {@link FileWatchService} fires the
21
 * expected {@link FileEvent} when the system raises state changes.
22
 */
23
class FileWatchServiceTest {
24
  /**
25
   * Test that modifying a file produces a {@link FileEvent}.
26
   *
27
   * @throws IOException          Could not create watcher service.
28
   * @throws InterruptedException Could not join on watcher service thread.
29
   */
30
  @Test
31
  @Timeout( value = 5, unit = SECONDS )
32
  void test_SingleFile_Write_Notified() throws
33
    IOException, InterruptedException {
34
    final var text = "arbitrary text to write";
35
    final var file = createTemporaryFile();
36
    final var service = new FileWatchService( file );
37
    final var thread = new Thread( service );
38
    final var semaphor = new Semaphore( 0 );
39
    final var listener = createListener( f -> {
40
      semaphor.release();
41
      assertEquals( file, f );
42
    } );
43
44
    thread.start();
45
    service.addListener( listener );
46
    Files.writeString( file.toPath(), text, CREATE, APPEND );
47
    semaphor.acquire();
48
    service.stop();
49
    thread.join();
50
  }
51
52
  private FileModifiedListener createListener( final Consumer<File> action ) {
53
    return fileEvent -> action.accept( fileEvent.getFile() );
54
  }
55
56
  private File createTemporaryFile() throws IOException {
57
    final var prefix = getClass().getPackageName();
58
    final var file = createTempFile( prefix, null, null );
59
    file.deleteOnExit();
60
    return file;
61
  }
62
}
163
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
17
  @Test
18
  void test_Read_KnownFileTypes_MediaTypeReturned()
19
    throws Exception {
20
    final var clazz = getClass();
21
    final var pkgName = clazz.getPackageName();
22
    final var dir = pkgName.replace( '.', '/' );
23
24
    final var urls = clazz.getClassLoader().getResources( dir + "/images" );
25
    assertTrue( urls.hasMoreElements() );
26
27
    while( urls.hasMoreElements() ) {
28
      final var url = urls.nextElement();
29
      final var path = new File( url.toURI().getPath() );
30
      final var files = path.listFiles();
31
      assertNotNull( files );
32
33
      for( final var image : files ) {
34
        final var media = MediaTypeSniffer.getMediaType( image );
35
        final var actualExtension = valueFrom( media ).getExtension();
36
        final var expectedExtension = getExtension( image.toString() );
37
        assertEquals( expectedExtension, actualExtension );
38
      }
39
    }
40
  }
41
}
142
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://kroki.io/robots.txt", TEXT_PLAIN,
54
       "https://place-hold.it/300x500", IMAGE_GIF,
55
       "https://placekitten.com/g/200/300", IMAGE_JPEG,
56
       "https://upload.wikimedia.org/wikipedia/commons/9/9f/Vimlogo.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
        assertEquals( v, response.getMediaType() );
64
      } catch( final Exception e ) {
65
        throw new RuntimeException( e );
66
      }
67
    } );
68
  }
69
}
170
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, "ls", "/usr/bin/ls" );
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 SITE = "https://github.com/";
30
  private static final String URL
31
    = SITE + "DaveJarvis/keenwrite/releases/latest/download/keenwrite.exe";
32
33
  @Test
34
  void test_Async_DownloadRequested_DownloadCompletes()
35
    throws IOException, InterruptedException,
36
    ExecutionException, URISyntaxException {
37
    final var complete = new AtomicInteger();
38
    final var transferred = new AtomicLong();
39
40
    final ProgressListener listener = ( percentage, bytes ) -> {
41
      complete.set( percentage );
42
      transferred.set( bytes );
43
    };
44
45
    final var file = File.createTempFile("kw-", "test");
46
    file.deleteOnExit();
47
48
    final var token = open( URL );
49
    final var executor = Executors.newFixedThreadPool( 1 );
50
    final var result = token.download( file, listener );
51
    final var future = executor.submit( result );
52
53
    assertFalse( future.isDone() );
54
    assertTrue( complete.get() < 100 );
55
    assertNull( future.get() );
56
    assertTrue( future.isDone() );
57
    assertEquals( 100, complete.get() );
58
    assertTrue( transferred.get() > 100_000 );
59
60
    token.close();
61
  }
62
}
163
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.processors.ProcessorContext.builder;
18
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
19
import static java.util.Locale.ENGLISH;
20
import static org.junit.jupiter.api.Assertions.assertEquals;
21
22
public class XhtmlProcessorTest {
23
24
  /**
25
   * Contains the thumbs up emoji.
26
   */
27
  private static final String EMOJI_MARKDOWN = "the \uD83D\uDC4D emoji";
28
29
  @ParameterizedTest
30
  @MethodSource( "formatParameters" )
31
  void test_Conversion_EmojiInput_EncodedEmoji(
32
    final ExportFormat format, final String expected ) {
33
    final var context = createProcessorContext( format );
34
    final var processor = createProcessors( context );
35
    final var actual = processor.apply( EMOJI_MARKDOWN );
36
37
    assertEquals( expected, actual );
38
  }
39
40
  private static ProcessorContext createProcessorContext(
41
    final ExportFormat format ) {
42
    final var caret = Caret.builder().build();
43
    return builder()
44
      .with( ProcessorContext.Mutator::setExportFormat, format )
45
      .with( ProcessorContext.Mutator::setSourcePath, Path.of( "f.md" ) )
46
      .with( ProcessorContext.Mutator::setDefinitions, HashMap::new )
47
      .with( ProcessorContext.Mutator::setLocale, () -> ENGLISH )
48
      .with( ProcessorContext.Mutator::setMetadata, HashMap::new )
49
      .with( ProcessorContext.Mutator::setThemeDir, () -> Path.of( "b" ) )
50
      .with( ProcessorContext.Mutator::setCaret, () -> caret )
51
      .with( ProcessorContext.Mutator::setImageDir, () -> new File( "i" ) )
52
      .with( ProcessorContext.Mutator::setImageOrder, () -> "" )
53
      .with( ProcessorContext.Mutator::setImageServer, () -> "" )
54
      .with( ProcessorContext.Mutator::setSigilBegan, () -> "" )
55
      .with( ProcessorContext.Mutator::setSigilEnded, () -> "" )
56
      .with( ProcessorContext.Mutator::setRScript, () -> "" )
57
      .with( ProcessorContext.Mutator::setRWorkingDir, () -> Path.of( "r" ) )
58
      .with( ProcessorContext.Mutator::setCurlQuotes, () -> true )
59
      .with( ProcessorContext.Mutator::setAutoRemove, () -> true )
60
      .build();
61
  }
62
63
  private static Stream<Arguments> formatParameters() {
64
    return Stream.of(
65
      Arguments.of(
66
        HTML_TEX_DELIMITED,
67
        """
68
          <html><head></head><body><p>the 👍 emoji</p>
69
          </body></html>"""
70
      ),
71
      Arguments.of(
72
        XHTML_TEX,
73
        """
74
          <html><head><title/><meta content="2" name="count"/></head><body><p>the 👍 emoji</p>
75
          </body></html>"""
76
      )
77
    );
78
  }
79
}
180
A src/test/java/com/keenwrite/processors/markdown/ImageLinkExtensionTest.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown;
3
4
import com.keenwrite.editors.common.Caret;
5
import com.keenwrite.processors.Processor;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.keenwrite.processors.markdown.extensions.ImageLinkExtension;
8
import com.vladsch.flexmark.html.HtmlRenderer;
9
import com.vladsch.flexmark.parser.Parser;
10
import org.junit.jupiter.api.Test;
11
12
import java.io.File;
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.assertEquals;
24
import static org.junit.jupiter.api.Assertions.assertNotNull;
25
26
/**
27
 * Responsible for testing that linked images render into HTML according to
28
 * the {@link ImageLinkExtension} rules.
29
 */
30
@SuppressWarnings( "SameParameterValue" )
31
public class ImageLinkExtensionTest {
32
  private static final String UIR_DIR = "images";
33
  private static final String URI_FILE = "kitten";
34
  private static final String URI_PATH = UIR_DIR + '/' + URI_FILE;
35
  private static final String PATH_KITTEN_JPG = URI_PATH + ".jpg";
36
  private static final String PATH_KITTEN_PNG = URI_PATH + ".png";
37
38
  private static final Map<String, String> IMAGES = new LinkedHashMap<>();
39
40
  static {
41
    add( PATH_KITTEN_PNG, URI_FILE );
42
    add( PATH_KITTEN_PNG, URI_PATH );
43
    add( PATH_KITTEN_PNG, PATH_KITTEN_PNG );
44
    add( PATH_KITTEN_JPG, PATH_KITTEN_JPG );
45
    add( "//placekitten.com/200/200", "//placekitten.com/200/200" );
46
    add( "ftp://placekitten.com/200/200", "ftp://placekitten.com/200/200" );
47
    add( "http://placekitten.com/200/200", "http://placekitten.com/200/200" );
48
    add( "https://placekitten.com/200/200", "https://placekitten.com/200/200" );
49
  }
50
51
  private static void add( final String expected, final String actual ) {
52
    IMAGES.put( toMd( actual ), toHtml( expected ) );
53
  }
54
55
  private static String toMd( final String resource ) {
56
    return format( "![Tooltip](%s 'Title')", resource );
57
  }
58
59
  private static String toHtml( final String url ) {
60
    return format(
61
      "<p><img src=\"%s\" alt=\"Tooltip\" title=\"Title\" /></p>%n", url );
62
  }
63
64
  /**
65
   * Test that the key URIs present in the {@link #IMAGES} map are rendered
66
   * as the value URIs present in the same map.
67
   */
68
  @Test
69
  void test_ImageLookup_RelativePathWithExtension_ResolvedSuccessfully() {
70
    final var resource = getResourcePath( PATH_KITTEN_PNG );
71
    final var imagePath = new File( PATH_KITTEN_PNG ).toPath();
72
    final var subpaths = resource.getNameCount() - imagePath.getNameCount();
73
    final var subpath = resource.subpath( 0, subpaths );
74
75
    final var root = resource.getRoot();
76
    assertNotNull( root );
77
78
    final var resolved = root.resolve( subpath );
79
    final var doc = resolved.toString();
80
81
    // The root component isn't considered part of the path, so add it back.
82
    final var documentPath = Path.of( doc, DOCUMENT_DEFAULT.getName() );
83
    final var imagesDir = Path.of( "images" );
84
    final var context = createProcessorContext( documentPath, imagesDir );
85
    final var extension = ImageLinkExtension.create( context );
86
    final var extensions = List.of( extension );
87
    final var pBuilder = Parser.builder();
88
    final var hBuilder = HtmlRenderer.builder();
89
    final var parser = pBuilder.extensions( extensions ).build();
90
    final var renderer = hBuilder.extensions( extensions ).build();
91
92
    assertNotNull( parser );
93
    assertNotNull( renderer );
94
95
    for( final var entry : IMAGES.entrySet() ) {
96
      final var key = entry.getKey();
97
      final var node = parser.parse( key );
98
      final var expectedHtml = entry.getValue();
99
      final var actualHtml = renderer.render( node );
100
101
      assertEquals( expectedHtml, actualHtml );
102
    }
103
  }
104
105
  /**
106
   * Creates a new {@link ProcessorContext} for the given file name path.
107
   *
108
   * @param inputPath Fully qualified path to the file name.
109
   * @return A context used for creating new {@link Processor} instances.
110
   */
111
  private ProcessorContext createProcessorContext(
112
    final Path inputPath, final Path imagesDir ) {
113
    return ProcessorContext
114
      .builder()
115
      .with( ProcessorContext.Mutator::setSourcePath, inputPath )
116
      .with( ProcessorContext.Mutator::setExportFormat, XHTML_TEX )
117
      .with( ProcessorContext.Mutator::setCaret, () -> Caret.builder().build() )
118
      .with( ProcessorContext.Mutator::setImageDir, imagesDir::toFile )
119
      .build();
120
  }
121
122
  private static URI toUri( final String path ) {
123
    try {
124
      return Path.of( path ).toUri();
125
    } catch( final Exception ex ) {
126
      throw new RuntimeException( ex );
127
    }
128
  }
129
130
  private static Path getResourcePath( final String path ) {
131
    return Paths.get( toUri( path ) );
132
  }
133
}
1134
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
A www/LICENSE
1
MIT License
2
3
Copyright (c) 2022 KeenWrite
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 www/README.md
11
2
![KeenWrite logo](images/logo/icon.png)
3
4
A www/images/icons/apple.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   viewBox="0 0 157.33104 75"
4
   version="1.1"
5
   id="svg1"
6
   width="157.33104"
7
   height="75"
8
   xmlns="http://www.w3.org/2000/svg"
9
   xmlns:svg="http://www.w3.org/2000/svg">
10
  <defs
11
     id="defs1" />
12
  <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
13
  <path
14
     d="m 56.86155,15.62503 c 0,-1.72852 -1.39648,-3.125 -3.125,-3.125 -1.72852,0 -3.125,1.39648 -3.125,3.125 V 39.3262 l -7.16797,-7.16797 c -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 -1.2207,1.22071 -1.2207,3.20313 0,4.42383 l 12.5,12.5 c 1.22071,1.2207 3.20313,1.2207 4.42383,0 l 12.5,-12.5 c 1.2207,-1.2207 1.2207,-3.20312 0,-4.42383 -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 l -7.1582,7.16797 z m -21.875,31.25 c -3.44727,0 -6.25,2.80273 -6.25,6.25 v 3.125 c 0,3.44727 2.80273,6.25 6.25,6.25 h 37.5 c 3.44727,0 6.25,-2.80273 6.25,-6.25 v -3.125 c 0,-3.44727 -2.80273,-6.25 -6.25,-6.25 h -9.91211 l -4.42383,4.42383 c -2.4414,2.4414 -6.39648,2.4414 -8.83789,0 l -4.41406,-4.42383 z m 35.9375,10.15625 c -1.29883,0 -2.34375,-1.04492 -2.34375,-2.34375 0,-1.29883 1.04492,-2.34375 2.34375,-2.34375 1.29883,0 2.34375,1.04492 2.34375,2.34375 0,1.29883 -1.04492,2.34375 -2.34375,2.34375 z"
15
     id="path16323"
16
     style="fill:#000000;fill-opacity:1;stroke-width:0.0976562" />
17
  <path
18
     d="m 121.70706,38.92224 c -0.0223,-4.09673 1.83069,-7.18881 5.58138,-9.46601 -2.0986,-3.00277 -5.26882,-4.65486 -9.45485,-4.97858 -3.96277,-0.31256 -8.29392,2.31069 -9.87903,2.31069 -1.67441,0 -5.51439,-2.19906 -8.52833,-2.19906 -6.22882,0.10046 -12.84832,4.96742 -12.84832,14.86877 q 0,4.38696 1.60743,9.06415 c 1.42883,4.09673 6.58602,14.1432 11.96646,13.97576 2.81303,-0.067 4.79998,-1.99813 8.46136,-1.99813 3.54976,0 5.39161,1.99813 8.52834,1.99813 5.4251,-0.0781 10.09112,-9.20927 11.45298,-13.31716 -7.27811,-3.42696 -6.88742,-10.04647 -6.88742,-10.25856 z m -6.31811,-18.32922 c 3.04743,-3.61673 2.76836,-6.90974 2.67906,-8.09299 -2.69022,0.15627 -5.80463,1.83069 -7.5795,3.89579 -1.95348,2.21023 -3.10325,4.9451 -2.85767,8.02602 2.91348,0.22325 5.57021,-1.27256 7.75811,-3.82882 z"
19
     id="path5811"
20
     style="fill:#000000;fill-opacity:1;stroke-width:0.111627" />
21
</svg>
122
A www/images/icons/java.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   viewBox="0 0 157.33104 75"
4
   version="1.1"
5
   id="svg1"
6
   width="157.33104"
7
   height="75"
8
   xmlns="http://www.w3.org/2000/svg"
9
   xmlns:svg="http://www.w3.org/2000/svg">
10
  <defs
11
     id="defs1" />
12
  <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
13
  <path
14
     d="m 53.68516,15.625 c 0,-1.72852 -1.39648,-3.125 -3.125,-3.125 -1.72852,0 -3.125,1.39648 -3.125,3.125 V 39.32617 L 40.26719,32.1582 c -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 -1.2207,1.22071 -1.2207,3.20313 0,4.42383 l 12.5,12.5 c 1.22071,1.2207 3.20313,1.2207 4.42383,0 l 12.5,-12.5 c 1.2207,-1.2207 1.2207,-3.20312 0,-4.42383 -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 l -7.1582,7.16797 z m -21.875,31.25 c -3.44727,0 -6.25,2.80273 -6.25,6.25 v 3.125 c 0,3.44727 2.80273,6.25 6.25,6.25 h 37.5 c 3.44727,0 6.25,-2.80273 6.25,-6.25 v -3.125 c 0,-3.44727 -2.80273,-6.25 -6.25,-6.25 h -9.91211 l -4.42383,4.42383 c -2.4414,2.4414 -6.39648,2.4414 -8.83789,0 L 41.72227,46.875 Z m 35.9375,10.15625 c -1.29883,0 -2.34375,-1.04492 -2.34375,-2.34375 0,-1.29883 1.04492,-2.34375 2.34375,-2.34375 1.29883,0 2.34375,1.04492 2.34375,2.34375 0,1.29883 -1.04492,2.34375 -2.34375,2.34375 z"
15
     id="path16474"
16
     style="fill:#000000;fill-opacity:1;stroke-width:0.0976562" />
17
  <path
18
     d="m 121.74171,43.05624 c 0.95702,-0.65429 2.28513,-1.22069 2.28513,-1.22069 0,0 -3.77925,0.68359 -7.53897,0.99608 -4.59954,0.38086 -9.54088,0.45898 -12.02132,0.12695 -5.86906,-0.78124 3.22261,-2.93941 3.22261,-2.93941 0,0 -3.52534,-0.23437 -7.87099,1.85544 -5.12688,2.48044 12.69515,3.61324 21.92354,1.18163 z m -8.33973,-3.13473 c -1.85545,-4.16986 -8.11513,-7.83192 0,-14.23809 C 123.52489,17.69525 118.33355,12.5 118.33355,12.5 c 2.09959,8.25185 -7.38271,10.75181 -10.8104,15.8787 -2.33395,3.50581 1.14256,7.26553 5.87883,11.54281 z m 11.19126,-17.2068 c 0.01,0 -17.10915,4.27729 -8.93543,13.69123 2.41208,2.7734 -0.63476,5.27336 -0.63476,5.27336 0,0 6.12297,-3.16402 3.3105,-7.11904 -2.62691,-3.69136 -4.63861,-5.52727 6.25969,-11.84555 z m -0.5957,26.41567 a 1.190414,1.190414 0 0 1 -0.19531,0.2539 c 12.52914,-3.29097 7.91982,-11.61117 1.93357,-9.50183 a 1.6923604,1.6923604 0 0 0 -0.80077,0.61523 6.879792,6.879792 0 0 1 1.0742,-0.29297 c 3.02731,-0.63475 7.37296,4.05268 -2.01169,8.92567 z m 4.60541,6.0839 c 0,0 1.416,1.1621 -1.55272,2.07029 -5.65422,1.70896 -23.51531,2.22653 -28.47618,0.0684 -1.78709,-0.77147 1.56248,-1.85544 2.61715,-2.08005 1.09374,-0.23437 1.72849,-0.19531 1.72849,-0.19531 -1.98239,-1.39646 -12.82209,2.74411 -5.50773,3.92573 19.94504,3.24215 36.3667,-1.45506 31.19099,-3.78901 z m -21.83174,-4.04291 c -7.68545,2.1484 4.67767,6.58194 14.4627,2.39254 a 18.15308,18.15308 0 0 1 -2.75387,-1.34764 c -4.36518,0.83007 -6.38664,0.88866 -10.35143,0.43945 -3.27144,-0.37109 -1.3574,-1.48435 -1.3574,-1.48435 z m 17.55836,9.49206 c -7.68545,1.44529 -17.16774,1.27928 -22.7829,0.35156 0,-0.01 1.15232,0.94725 7.07022,1.3281 9.00378,0.57617 22.83173,-0.32226 23.15399,-4.58001 0,0 -0.62499,1.6113 -7.44131,2.90035 z M 120.07181,46.9722 c -5.78117,1.11327 -9.13074,1.08397 -13.3592,0.64453 -3.27144,-0.3418 -1.13279,-1.92381 -1.13279,-1.92381 -8.47645,2.81247 4.70696,5.99602 16.55251,2.52927 a 5.89543,5.89543 0 0 1 -2.06052,-1.24999 z"
19
     id="path5936"
20
     style="fill:#000000;fill-opacity:1;stroke-width:0.097655" />
21
</svg>
122
A www/images/icons/linux.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   viewBox="0 0 157.33104 75"
4
   version="1.1"
5
   id="svg1"
6
   width="157.33104"
7
   height="75"
8
   xmlns="http://www.w3.org/2000/svg"
9
   xmlns:svg="http://www.w3.org/2000/svg">
10
  <defs
11
     id="defs1" />
12
  <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
13
  <path
14
     d="m 111.9104,24.539926 c 0.0976,0.0488 0.17577,0.166 0.29295,0.166 0.10741,0 0.27341,-0.039 0.28318,-0.14647 0.0196,-0.13671 -0.18553,-0.22459 -0.31248,-0.28318 -0.166,-0.0684 -0.38082,-0.0976 -0.53706,-0.01 -0.039,0.0195 -0.0781,0.0684 -0.0586,0.10741 0.0293,0.12695 0.22459,0.10742 0.332,0.166 z m -2.13849,0.166 c 0.11718,0 0.1953,-0.11718 0.29294,-0.166 0.10742,-0.0586 0.30271,-0.039 0.34177,-0.15624 0.0195,-0.0391 -0.0195,-0.0879 -0.0586,-0.10741 -0.15623,-0.0879 -0.37106,-0.0586 -0.53706,0.01 -0.12694,0.0586 -0.33201,0.14647 -0.31248,0.28318 0.01,0.0976 0.17577,0.14647 0.27342,0.13671 z m 21.59,27.22431 c -0.35153,-0.3906 -0.51753,-1.13273 -0.70306,-1.92368 -0.17577,-0.79095 -0.38083,-1.64048 -1.02531,-2.18731 -0.12694,-0.10742 -0.25388,-0.20506 -0.39059,-0.28318 -0.12694,-0.0781 -0.26365,-0.14648 -0.40036,-0.1953 0.89836,-2.66579 0.54683,-5.32182 -0.3613,-7.72397 -1.11318,-2.9392 -3.05638,-5.50735 -4.54063,-7.26502 -1.66979,-2.09944 -3.29075,-4.09146 -3.26146,-7.03067 0.0488,-4.48205 0.49801,-12.81143 -7.40172,-12.8212 -9.99918,-0.0196 -7.49938,10.09682 -7.6068,13.20203 -0.166,2.28497 -0.62495,4.08169 -2.19708,6.31784 -1.84555,2.19708 -4.442986,5.74171 -5.673356,9.44257 -0.58588,1.7479 -0.8593,3.5251 -0.60541,5.20465 -0.63472,0.56636 -1.11319,1.43543 -1.62096,1.97249 -0.41012,0.41989 -1.00578,0.57612 -1.66002,0.81048 -0.65424,0.23436 -1.36708,0.58589 -1.8065,1.4159 -0.20506,0.38083 -0.27341,0.79095 -0.27341,1.21084 0,0.38083 0.0586,0.77142 0.11718,1.15225 0.11717,0.79095 0.24412,1.53307 0.0781,2.03108 -0.50777,1.40613 -0.57613,2.38261 -0.21483,3.09544 0.37106,0.71283 1.11319,1.02531 1.96273,1.20108 1.68931,0.35153 3.98405,0.26365 5.79054,1.2206 1.933426,1.01554 3.896156,1.37684 5.458526,1.01554 1.13271,-0.25389 2.06037,-0.93742 2.52908,-1.97249 1.22061,-0.01 2.56815,-0.5273 4.71641,-0.64448 1.45496,-0.11718 3.28098,0.51753 5.38041,0.40035 0.0586,0.2246 0.13671,0.44919 0.24412,0.65425 v 0.01 c 0.81048,1.63072 2.32403,2.37285 3.93522,2.2459 1.62097,-0.12694 3.32981,-1.07413 4.71641,-2.72438 1.32802,-1.60143 3.51533,-2.26544 4.97029,-3.14427 0.7226,-0.43941 1.30849,-0.98624 1.35731,-1.78696 0.039,-0.80071 -0.42965,-1.68931 -1.51355,-2.90015 z m -19.16833,-30.90565 c 0.95695,-2.16779 3.33957,-2.12873 4.29652,-0.0391 0.63471,1.38661 0.35153,3.01733 -0.41989,3.94499 -0.15623,-0.0781 -0.57612,-0.25388 -1.23036,-0.47847 0.10741,-0.11718 0.30271,-0.26365 0.38082,-0.44919 0.46872,-1.15224 -0.0195,-2.6365 -0.88859,-2.66579 -0.71284,-0.0488 -1.35731,1.0546 -1.15225,2.24591 -0.40036,-0.1953 -0.91789,-0.34177 -1.26943,-0.42965 -0.0976,-0.67378 -0.0293,-1.42567 0.28318,-2.12874 z m -3.97428,-1.12295 c 0.98625,0 2.03108,1.3866 1.86508,3.27122 -0.34177,0.0976 -0.6933,0.24412 -0.99601,0.44918 0.11718,-0.86907 -0.32224,-1.96274 -0.93742,-1.91391 -0.82025,0.0684 -0.95696,2.07014 -0.17577,2.74392 0.0977,0.0781 0.18553,-0.0195 -0.57613,0.53706 -1.52331,-1.42566 -1.0253,-5.08747 0.82025,-5.08747 z m -1.32802,5.92724 c 0.60542,-0.44918 1.32802,-0.97648 1.37685,-1.0253 0.45894,-0.42965 1.31825,-1.3866 2.72438,-1.3866 0.6933,0 1.52331,0.22459 2.52909,0.86906 0.61518,0.40036 1.10342,0.42966 2.20684,0.90813 0.82025,0.34177 1.33778,0.94719 1.02531,1.7772 -0.25389,0.6933 -1.07413,1.40613 -2.21661,1.76743 -1.0839,0.35153 -1.93344,1.56237 -3.73016,1.45496 -0.38083,-0.0196 -0.68354,-0.0977 -0.93742,-0.20507 -0.78119,-0.34176 -1.19131,-1.01554 -1.95297,-1.46472 -0.83977,-0.46871 -1.28896,-1.01554 -1.43543,-1.49401 -0.13671,-0.47848 0,-0.87884 0.41012,-1.20108 z m 0.32224,32.61449 c -0.26365,3.42745 -4.28675,3.35909 -7.352896,1.75766 -2.91968,-1.54284 -6.69866,-0.63471 -7.47009,-2.13849 -0.23435,-0.45895 -0.23435,-1.24013 0.25389,-2.57791 v -0.0195 c 0.23435,-0.74213 0.0586,-1.56237 -0.0586,-2.33379 -0.11718,-0.76166 -0.17577,-1.46472 0.0879,-1.95297 0.34177,-0.65424 0.83002,-0.8886 1.4452,-1.10342 1.00578,-0.3613 1.15225,-0.33201 1.9139,-0.96672 0.53707,-0.55659 0.92766,-1.25966 1.39637,-1.75766 0.49801,-0.53707 0.97648,-0.79095 1.72837,-0.67378 0.79095,0.11718 1.474486,0.66401 2.138496,1.56237 l 1.9139,3.47628 c 0.92766,1.9432 4.20863,4.72617 4.00357,6.72796 z m -0.1367,-2.52909 c -0.40036,-0.64448 -0.93743,-1.32801 -1.40614,-1.9139 0.6933,0 1.38661,-0.21483 1.63073,-0.86907 0.22459,-0.60542 0,-1.45496 -0.7226,-2.43144 -1.31825,-1.7772 -3.73992,-3.17357 -3.73992,-3.17357 -1.31825,-0.82024 -2.06038,-1.82602 -2.40215,-2.91968 -0.34176,-1.09366 -0.29294,-2.2752 -0.0293,-3.43721 0.50777,-2.23614 1.81626,-4.4137 2.65603,-5.78077 0.22459,-0.16601 0.0781,0.31247 -0.84954,2.03108 -0.83,1.57214 -2.382606,5.20465 -0.25388,8.04621 0.0586,-2.02132 0.53706,-4.08169 1.34754,-6.00536 1.17178,-2.67556 3.64228,-7.31385 3.83757,-11.00495 0.10742,0.0781 0.44919,0.31247 0.60543,0.40036 0.44918,0.26365 0.79095,0.65424 1.23036,1.00577 1.21084,0.97648 2.78298,0.89836 4.14028,0.11718 0.60542,-0.34177 1.09366,-0.73236 1.55261,-0.87883 0.96672,-0.30271 1.73814,-0.83978 2.17755,-1.46473 0.75189,2.96851 2.50956,7.25526 3.63251,9.34493 0.59566,1.11319 1.78697,3.46651 2.3045,6.30807 0.32224,-0.01 0.68354,0.039 1.06437,0.13671 1.34754,-3.48604 -1.14249,-7.24549 -2.27521,-8.29033 -0.45894,-0.44918 -0.47847,-0.64448 -0.25388,-0.63471 1.23037,1.09366 2.85133,3.29074 3.43722,5.76124 0.27341,1.13272 0.32223,2.31426 0.039,3.48604 1.60143,0.66401 3.50557,1.7479 2.9978,3.39815 -0.21483,-0.01 -0.31248,0 -0.41013,0 0.31248,-0.98624 -0.38082,-1.7186 -2.22637,-2.54861 -1.91391,-0.83978 -3.51534,-0.83978 -3.73993,1.2206 -1.18154,0.41012 -1.78696,1.43543 -2.08967,2.66579 -0.27341,1.09366 -0.35153,2.41191 -0.42965,3.89617 -0.0488,0.75189 -0.35154,1.75766 -0.66401,2.83179 -3.1345,2.23615 -7.48961,3.21263 -11.16118,0.70307 z m 25.13463,-1.12295 c -0.0879,1.64049 -4.0231,1.94319 -6.17136,4.54063 -1.28895,1.53308 -2.87085,2.38262 -4.25746,2.49003 -1.38661,0.10741 -2.58768,-0.46871 -3.29074,-1.88461 -0.45895,-1.08389 -0.23436,-2.25567 0.10741,-3.54462 0.3613,-1.38661 0.89836,-2.81227 0.96672,-3.96452 0.0781,-1.48425 0.166,-2.78297 0.41012,-3.77898 0.25388,-1.00578 0.64448,-1.67955 1.33778,-2.06038 0.0293,-0.0195 0.0684,-0.0293 0.0976,-0.0488 0.0781,1.28895 0.71284,2.59744 1.83579,2.88062 1.23037,0.32223 2.9978,-0.73237 3.74969,-1.59167 0.87884,-0.0293 1.53308,-0.0879 2.20685,0.49801 0.96672,0.83001 0.6933,2.95874 1.66978,4.06216 1.03507,1.13272 1.36708,1.90414 1.33778,2.40215 z m -24.93934,-27.77114 c 0.1953,0.18553 0.45895,0.43942 0.78119,0.6933 0.64448,0.50777 1.54284,1.03507 2.6658,1.03507 1.13271,0 2.19708,-0.57612 3.10521,-1.0546 0.47847,-0.25388 1.06436,-0.68353 1.44519,-1.01554 0.38083,-0.332 0.57612,-0.61518 0.30271,-0.64447 -0.27342,-0.0293 -0.25389,0.25388 -0.58589,0.498 -0.42965,0.31248 -0.94719,0.7226 -1.35731,0.95695 -0.7226,0.41012 -1.90414,0.99601 -2.91968,0.99601 -1.01554,0 -1.82602,-0.46871 -2.43144,-0.94718 -0.30271,-0.24412 -0.55659,-0.48824 -0.75189,-0.67378 -0.14647,-0.1367 -0.18553,-0.44918 -0.41989,-0.47847 -0.13671,-0.01 -0.17577,0.3613 0.166,0.63471 z"
15
     id="path6061"
16
     style="fill:#000000;fill-opacity:1;stroke-width:0.097648" />
17
  <path
18
     d="m 52.578354,15.625006 c 0,-1.72852 -1.39648,-3.125 -3.125,-3.125 -1.72852,0 -3.125,1.39648 -3.125,3.125 v 23.70117 l -7.16797,-7.16797 c -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 -1.2207,1.22071 -1.2207,3.20313 0,4.42383 l 12.5,12.5 c 1.22071,1.2207 3.20313,1.2207 4.42383,0 l 12.5,-12.5 c 1.2207,-1.2207 1.2207,-3.20312 0,-4.42383 -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 l -7.1582,7.16797 z m -21.875,31.25 c -3.44727,0 -6.25,2.80273 -6.25,6.25 v 3.125 c 0,3.44727 2.80273,6.25 6.25,6.25 h 37.5 c 3.44727,0 6.25,-2.80273 6.25,-6.25 v -3.125 c 0,-3.44727 -2.80273,-6.25 -6.25,-6.25 h -9.91211 l -4.42383,4.42383 c -2.4414,2.4414 -6.39648,2.4414 -8.83789,0 l -4.41406,-4.42383 z m 35.9375,10.15625 c -1.29883,0 -2.34375,-1.04492 -2.34375,-2.34375 0,-1.29883 1.04492,-2.34375 2.34375,-2.34375 1.29883,0 2.34375,1.04492 2.34375,2.34375 0,1.29883 -1.04492,2.34375 -2.34375,2.34375 z"
19
     id="path15448"
20
     style="fill:#000000;fill-opacity:1;stroke-width:0.0976562" />
21
</svg>
122
A www/images/icons/windows.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   viewBox="0 0 157.33104 75"
4
   version="1.1"
5
   id="svg1"
6
   width="157.33104"
7
   height="75"
8
   xmlns="http://www.w3.org/2000/svg"
9
   xmlns:svg="http://www.w3.org/2000/svg">
10
  <defs
11
     id="defs1" />
12
  <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
13
  <path
14
     d="M 83.66552,19.38616 104.15659,16.5625 V 36.36161 H 83.66552 Z m 0,36.22768 20.49107,2.82366 V 38.88393 H 83.66552 Z m 22.74554,3.125 L 133.66552,62.5 V 38.88393 h -27.25446 z m 0,-42.47768 v 20.10045 h 27.25446 V 12.5 Z"
15
     id="path6186"
16
     style="fill:#000000;fill-opacity:1;stroke-width:0.111607" />
17
  <path
18
     d="m 51.79052,15.625 c 0,-1.72852 -1.39648,-3.125 -3.125,-3.125 -1.72852,0 -3.125,1.39648 -3.125,3.125 V 39.32617 L 38.37255,32.1582 c -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 -1.2207,1.22071 -1.2207,3.20313 0,4.42383 l 12.5,12.5 c 1.22071,1.2207 3.20313,1.2207 4.42383,0 l 12.5,-12.5 c 1.2207,-1.2207 1.2207,-3.20312 0,-4.42383 -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 l -7.1582,7.16797 z m -21.875,31.25 c -3.44727,0 -6.25,2.80273 -6.25,6.25 v 3.125 c 0,3.44727 2.80273,6.25 6.25,6.25 h 37.5 c 3.44727,0 6.25,-2.80273 6.25,-6.25 v -3.125 c 0,-3.44727 -2.80273,-6.25 -6.25,-6.25 h -9.91211 l -4.42383,4.42383 c -2.4414,2.4414 -6.39648,2.4414 -8.83789,0 L 39.82763,46.875 Z m 35.9375,10.15625 c -1.29883,0 -2.34375,-1.04492 -2.34375,-2.34375 0,-1.29883 1.04492,-2.34375 2.34375,-2.34375 1.29883,0 2.34375,1.04492 2.34375,2.34375 0,1.29883 -1.04492,2.34375 -2.34375,2.34375 z"
19
     id="path16036"
20
     style="fill:#000000;fill-opacity:1;stroke-width:0.0976562" />
21
</svg>
122
A www/images/logo/icon.png
Binary file
A www/images/logo/icon.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   height="493.14542"
4
   viewBox="0 0 500.05118 493.14542"
5
   width="500.05118"
6
   version="1.1"
7
   id="svg37"
8
   sodipodi:docname="logo-icon.svg"
9
   inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
10
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
11
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
12
   xmlns="http://www.w3.org/2000/svg"
13
   xmlns:svg="http://www.w3.org/2000/svg"
14
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
15
   xmlns:cc="http://creativecommons.org/ns#"
16
   xmlns:dc="http://purl.org/dc/elements/1.1/">
17
  <sodipodi:namedview
18
     id="namedview18"
19
     pagecolor="#ffffff"
20
     bordercolor="#666666"
21
     borderopacity="1.0"
22
     inkscape:showpageshadow="2"
23
     inkscape:pageopacity="0.0"
24
     inkscape:pagecheckerboard="0"
25
     inkscape:deskcolor="#d1d1d1"
26
     showgrid="false"
27
     inkscape:zoom="2.1352728"
28
     inkscape:cx="250.3193"
29
     inkscape:cy="246.57271"
30
     inkscape:current-layer="svg37" />
31
  <metadata
32
     id="metadata43">
33
    <rdf:RDF>
34
      <cc:Work
35
         rdf:about="">
36
        <dc:format>image/svg+xml</dc:format>
37
        <dc:type
38
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
39
      </cc:Work>
40
    </rdf:RDF>
41
  </metadata>
42
  <defs
43
     id="defs41">
44
    <linearGradient
45
       id="a"
46
       gradientTransform="matrix(-8.7796153,42.985832,-42.985832,-8.7796153,514.83476,136.06192)"
47
       gradientUnits="userSpaceOnUse"
48
       x1="0.152358"
49
       x2="0.96880901"
50
       y1="-0.044911999"
51
       y2="-0.049470998">
52
      <stop
53
         offset="0"
54
         stop-color="#ec706a"
55
         id="stop2" />
56
      <stop
57
         offset="1"
58
         stop-color="#ecd980"
59
         id="stop4" />
60
    </linearGradient>
61
  </defs>
62
  <g
63
     id="g485"
64
     transform="matrix(2.5605898,1.4612315,-1.4612315,2.5605898,-947.38048,-777.17055)">
65
    <path
66
       style="fill:url(#a);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.226;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
67
       paint-order="stroke"
68
       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"
69
       stroke-linecap="round"
70
       id="path14-3" />
71
    <path
72
       d="m 530.80335,138.63592 -10.99206,-16.95952 1.75995,-6.49966 10.01483,2.71233 z"
73
       fill="#126d95"
74
       id="path9" />
75
    <path
76
       d="m 533.0598,112.36676 -0.91739,3.38458 -9.99361,-2.70665 0.91739,-3.38458 z"
77
       fill="#126d95"
78
       id="path11" />
79
    <g
80
       fill="#51a9cf"
81
       id="g19"
82
       transform="translate(-295.50101,-692.52836)">
83
      <path
84
         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"
85
         id="path13" />
86
      <path
87
         d="m 818.56767,802.18881 -0.9174,3.38458 -10.03996,-2.72957 0.91314,-3.37522 z"
88
         id="path15" />
89
      <path
90
         d="m 817.07405,807.70594 -1.75995,6.49966 -18.03534,9.08805 9.78412,-18.31044 z"
91
         id="path17" />
92
    </g>
93
    <path
94
       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"
95
       fill="#126d95"
96
       id="path21" />
97
    <text
98
       transform="translate(-295.73751,-689.6407)"
99
       id="text25" />
100
  </g>
101
</svg>
1102
A www/images/logo/title.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4
<svg
5
   width="300"
6
   height="71.784302"
7
   viewBox="0 0 79.374996 18.99293"
8
   version="1.1"
9
   id="svg1"
10
   xmlns:xlink="http://www.w3.org/1999/xlink"
11
   xmlns="http://www.w3.org/2000/svg"
12
   xmlns:svg="http://www.w3.org/2000/svg">
13
  <defs
14
     id="defs1">
15
    <linearGradient
16
       xlink:href="#a"
17
       id="linearGradient347"
18
       gradientUnits="userSpaceOnUse"
19
       gradientTransform="matrix(-5.2145621,25.530992,-25.530992,-5.2145621,762.29957,221.94944)"
20
       x1="0.152358"
21
       y1="-0.044911999"
22
       x2="0.96880901"
23
       y2="-0.049470998" />
24
    <linearGradient
25
       id="a"
26
       gradientTransform="matrix(-5.2145621,25.530992,-25.530992,-5.2145621,762.29957,221.94944)"
27
       gradientUnits="userSpaceOnUse"
28
       x1="0.152358"
29
       x2="0.96880901"
30
       y1="-0.044911999"
31
       y2="-0.049470998">
32
      <stop
33
         offset="0"
34
         stop-color="#ec706a"
35
         id="stop2" />
36
      <stop
37
         offset="1"
38
         stop-color="#ecd980"
39
         id="stop4" />
40
    </linearGradient>
41
  </defs>
42
  <g
43
     id="layer1"
44
     transform="matrix(0.76997495,0,0,0.76997495,-41.151955,-104.91711)">
45
    <g
46
       id="g3650"
47
       transform="matrix(0.26458333,0,0,0.26458333,-70.48435,95.775023)">
48
      <path
49
         style="fill:url(#linearGradient347);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.72817;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
50
         paint-order="stroke"
51
         d="m 751.56561,230.70578 c -2.52643,12.28316 1.94926,15.41557 1.94926,15.41557 v 0 c 0,0 1.78754,-7.83729 5.13213,-6.39182 v 0 c 2.87111,1.23717 0.66827,6.51553 0.66827,6.51553 v 0 c 0,0 18.8813,-18.11967 4.51682,-24.34816 v 0 c 0,0 -3.14959,-1.64397 -6.35422,-0.3873 v 0 c -2.34448,0.91995 -4.71811,3.39174 -5.91226,9.19618"
52
         stroke-linecap="round"
53
         id="path14" />
54
      <path
55
         d="m 771.78395,223.47824 -6.52862,-10.07293 1.0453,-3.86041 5.94821,1.61096 z"
56
         fill="#126d95"
57
         id="path9"
58
         style="stroke-width:0.59394" />
59
      <path
60
         d="m 773.12415,207.87594 -0.54488,2.01024 -5.9356,-1.60759 0.54487,-2.01024 z"
61
         fill="#126d95"
62
         id="path11"
63
         style="stroke-width:0.59394" />
64
      <g
65
         fill="#51a9cf"
66
         id="g19"
67
         transform="matrix(0.5939397,0,0,0.5939397,281.00899,-270.18322)">
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 777.66024,170.31167 4.58864,-16.94469 c -0.18977,4.99163 0.16898,14.58143 4.28633,25.18926 0,0 -7.06553,9.8014 -13.06542,28.13866 l -5.95984,-1.61407 8.29457,-30.60477 c 1.23671,0.33502 2.50072,-0.39558 2.83417,-1.62646 0.26981,-0.99336 -0.15781,-2.0094 -0.97845,-2.53792"
80
         fill="#126d95"
81
         id="path21"
82
         style="stroke-width:0.59394" />
83
    </g>
84
    <g
85
       style="font-style:italic;font-weight:800;font-size:133.333px;font-family:'Merriweather Sans';letter-spacing:0;word-spacing:0;fill:#51a9cf"
86
       id="g35"
87
       transform="matrix(0.15714654,0,0,0.15714654,50.302906,133.11748)">
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
  </g>
104
</svg>
1105
A www/index.html
1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
  <title>KeenWrite</title>
5
  <meta charset="UTF-8">
6
  <meta name="description" content="cross-platform, open-source desktop editor">
7
  <meta name="keywords" content="markdown, text, editor, software">
8
  <meta name="robots" content="index, follow">
9
  <style>
10
/* ************************************************************************ 
11
 *
12
 * Page
13
 *
14
 * ************************************************************************ */
15
:root {
16
  --accent-colour: #ec706a;
17
  --link-colour: #8cc6de;
18
}
19
20
body {
21
  /* Ensure the page doesn't extend full screen on large monitors. */
22
	max-width: 1000px; 
23
	margin: 0 auto;
24
25
  background: #363636;
26
  color: #eaeaea;
27
}
28
29
/* Text alignment. */
30
header, nav, footer {
31
  text-align: center;
32
}
33
34
/* ************************************************************************ 
35
 * Header
36
 * ************************************************************************ */
37
38
header {
39
  /* Avoid being flush with top of page, put space between the title and
40
   * the download buttons, ensure any text won't be flush with edges.
41
   */
42
  margin: 2em;
43
}
44
45
header p {
46
  line-height: 1.5em;
47
}
48
49
/* Ensure the application title is large enough. */
50
header > img.title {
51
  width: 100%;
52
  height: 72pt;
53
}
54
55
/* ************************************************************************ 
56
 * Download buttons
57
 * ************************************************************************ */
58
59
main {
60
  /* Arrange the buttons in a responsive, 2 x 2 grid. */
61
  display: grid;
62
  grid-template-rows: 1fr 1fr;
63
  grid-template-columns: max-content max-content;
64
  justify-content: center;
65
}
66
67
/* Make hyperlinks resemble buttons. */
68
a.download {
69
  display: inline-block;
70
71
  /* Separate the buttons from one another. */
72
  margin-top: 2em;
73
  margin-left: 1em;
74
  margin-right: 1em;
75
76
  /* Fancy buttons. */
77
  border-radius: 1em;
78
  background: var( --accent-colour );
79
}
80
81
a.download:hover {
82
  background: var( --link-colour );
83
}
84
85
img.download {
86
  /* Replace icon black with another colour. */
87
  filter: invert(6%)
88
    sepia(58%) saturate(857%) hue-rotate(158deg) brightness(91%) contrast(91%);
89
}
90
91
/* ************************************************************************ 
92
 * Navigation
93
 * ************************************************************************ */
94
95
nav {
96
  /* Don't crowd navigation links against the download buttons. */
97
  margin-top: 4em;
98
}
99
100
nav ul {
101
  /* Remove the bullets */
102
  list-style: none;
103
  padding: 0;
104
  margin: 0;
105
}
106
107
nav li {
108
  /* Put navigation items along a single line. */
109
  display: inline;
110
}
111
112
nav li:not(:last-child)::after {
113
  /* Separate navigation items with a bar. */
114
  content: " | ";
115
}
116
117
nav a, nav a:visited {
118
  color: var( --link-colour );
119
}
120
121
nav a:link:hover, nav a:visited:hover {
122
  color: var( --accent-colour );
123
}
124
125
/* ************************************************************************ 
126
 * Footer
127
 * ************************************************************************ */
128
129
/* Align and center footer along bottom of page. */
130
footer {
131
  position: fixed;
132
  bottom: 1em;
133
  left: 50%;
134
  transform: translateX(-50%);
135
  width: 100%;
136
}
137
  </style>
138
</head>
139
<body>
140
<header>
141
  <img src="images/logo/title.svg" alt="KeenWrite" class="title">
142
  <p>
143
  A free, cross-platform, open-source desktop text editor.
144
  </p>
145
</header>
146
<main>
147
  <a href="https://gitlab.com/DaveJarvis/KeenWrite/-/releases/permalink/latest/keenwrite.bin"
148
     class="download"
149
     title="Download for 64-bit Linux (x86)"
150
     aria-label="Download for Linux"><img
151
       src="images/icons/linux.svg"
152
       alt="Download for Linux"
153
       class="download"></a>
154
  <a href="https://gitlab.com/DaveJarvis/KeenWrite/-/releases/permalink/latest/keenwrite.jar"
155
     class="download"
156
     title="Download for Java virtual machine"
157
     aria-label="Download for Java"><img
158
       src="images/icons/java.svg"
159
       alt="Download for Java"
160
       class="download"></a>
161
  <a href="https://gitlab.com/DaveJarvis/KeenWrite/-/releases/permalink/latest/keenwrite.exe"
162
     class="download"
163
     title="Download for 64-bit Windows (x86)"
164
     aria-label="Download for Windows"><img
165
       src="images/icons/windows.svg"
166
       alt="Download for Windows"
167
       class="download"></a>
168
  <a href="https://gitlab.com/DaveJarvis/KeenWrite/-/releases/permalink/latest/keenwrite.app"
169
     class="download"
170
     title="Download for 64-bit MacOS (x86)"
171
     aria-label="Download for MacOS"><img
172
       src="images/icons/apple.svg"
173
       alt="Download for MacOS"
174
       class="download"></a>
175
</main>
176
<nav>
177
  <ul>
178
    <li><a href="https://www.youtube.com/playlist?list=PLB-WIt1cZYLm1MMx2FBG9KWzPIoWZMKu_">Tutorials</a></li>
179
    <li><a href="https://gitlab.com/DaveJarvis/KeenWrite">Sources</a></li>
180
    <li><a href="https://gitlab.com/DaveJarvis/KeenWrite/issues">Issues</a></li>
181
    <li><a href="https://gitlab.com/DaveJarvis/KeenWrite/-/blob/main/docs/README.md">Documentation</a></li>
182
  </ul>
183
</nav>
184
<footer>
185
  &copy; 2023, White Magic Software, Ltd.
186
</footer>
187
</body>
188
</html>
189
1190