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 .github/ISSUE_TEMPLATE/bug_report.md
1
---
2
name: Bug report
3
about: Create a report to help us improve
4
title: ''
5
labels: bug
6
assignees: ''
7
8
---
9
10
**Description**
11
A concise problem description.
12
13
**Replicate**
14
Exact and complete steps to reproduce the problem 100% of the time:
15
16
1. Open '...'
17
1. Click '....'
18
1. Click '....'
19
20
**Expected**
21
Describe the expected behaviour.
22
23
**Actual**
24
Describe the actual behaviour.
25
26
**Screenshots**
27
Add screenshots to show the problem, if applicable.
28
29
**Environment**
30
 - Operating System: (Windows, Linux, Mac)
31
 - Application: e.g., 1.7.16
32
33
**Details**
34
Add additional information, if applicable.
135
A .gitignore
1
dist
2
*.bin
3
*.exe
4
/*.jar
5
build
6
.gradle
7
video
8
.settings
9
.classpath
10
.idea
11
themes
12
quotes
13
tex
14
spell
15
keenwrite.github.io
16
keenwrite.build_artifacts.txt
17
todo
118
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 19](https://bell-sw.com/pages/downloads) (Full JDK + JavaFX)
10
* [Gradle 7.6-rc-1](https://services.gradle.org/distributions/gradle-7.6-rc-1-bin.zip)
11
* [Git 2.38.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://github.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 2.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 oridinal. 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
cms.big <- function( n ) {
201
}
202
203
204
# -----------------------------------------------------------------------------
205
# Returns a number as a comma-delimited string. This is a work-around
206
# until Renjin fixes https://github.com/bedatadriven/renjin/issues/338
207
# -----------------------------------------------------------------------------
208
commas <- function( n ) {
209
  n <- x( n )
210
211
  s <- sprintf( "%03.0f", n %% 1000 )
212
  n <- n %/% 1000
213
214
  while( n > 0 ) {
215
    s <- concat( sprintf( "%03.0f", n %% 1000 ), ',', s )
216
    n <- n %/% 1000
217
  }
218
219
  gsub( "^0*", '', s )
220
}
221
222
# -----------------------------------------------------------------------------
223
# Returns a human-readable string that provides the elapsed time between
224
# two numbers in terms of years, months, and days. If any unit value is zero,
225
# the unit is not included. The words (year, month, day) are pluralized
226
# according to English grammar. The numbers are written out according to
227
# Chicago Manual of Style. This applies the serial comma.
228
#
229
# Both numbers are offsets relative to the anchor date.
230
#
231
# If all unit values are zero, this returns s ("same day" by default).
232
#
233
# If the start date (began) is greater than end date (ended), the dates are
234
# swapped before calculations are performed. This allows any two dates
235
# to be compared and positive unit values are always returned.
236
# -----------------------------------------------------------------------------
237
elapsed <- function( began, ended, s = "same day" ) {
238
  began = when( anchor, began )
239
  ended = when( anchor, ended )
240
241
  # Swap the dates if the end date comes before the start date.
242
  if( as.integer( ended - began ) < 0 ) {
243
    tempd = began
244
    began = ended
245
    ended = tempd
246
  }
247
248
  # Calculate number of elapsed years.
249
  years = length( seq( from = began, to = ended, by = "year" ) ) - 1
250
251
  # Move the start date up by the number of elapsed years.
252
  if( years > 0 ) {
253
    began = seq( began, length = 2, by = concat( years, " years" ) )[2]
254
    years = pl.numeric( "year", years )
255
  }
256
  else {
257
    # Zero years.
258
    years = ""
259
  }
260
261
  # Calculate number of elapsed months, excluding years.
262
  months = length( seq( from = began, to = ended, by = "month" ) ) - 1
263
264
  # Move the start date up by the number of elapsed months
265
  if( months > 0 ) {
266
    began = seq( began, length = 2, by = concat( months, " months" ) )[2]
267
    months = pl.numeric( "month", months )
268
  }
269
  else {
270
    # Zero months
271
    months = ""
272
  }
273
274
  # Calculate number of elapsed days, excluding months and years.
275
  days = length( seq( from = began, to = ended, by = "day" ) ) - 1
276
277
  if( days > 0 ) {
278
    days = pl.numeric( "day", days )
279
  }
280
  else {
281
    # Zero days
282
    days = ""
283
  }
284
285
  if( years <= 0 && months <= 0 && days <= 0 ) {
286
    return( s )
287
  }
288
289
  # Put them all in a vector, then remove the empty values.
290
  s <- c( years, months, days )
291
  s <- s[ s != "" ]
292
293
  r <- paste( s, collapse = ", " )
294
295
  # If all three items are present, replace the last comma with ", and".
296
  if( length( s ) > 2 ) {
297
    return( gsub( "(.*),", "\\1, and", r ) )
298
  }
299
300
  # Does nothing if no commas are present.
301
  gsub( "(.*),", "\\1 and", r )
302
}
303
304
# -----------------------------------------------------------------------------
305
# Returns the number (n) in English followed by the plural or singular
306
# form of the given string (s; resumably a noun), if applicable, according
307
# to English grammar. That is, pl.numeric( "wolf", 5 ) will return
308
# "five wolves".
309
# -----------------------------------------------------------------------------
310
pl.numeric <- function( s, n ) {
311
  concat( cms( n ), concat( " ", pluralize( word=s, n=n ) ) )
312
}
313
314
# -----------------------------------------------------------------------------
315
# Pluralize s if n is not equal to 1.
316
# -----------------------------------------------------------------------------
317
pl <- function( s, count=2 ) {
318
  pluralize( word=s, n=count )
319
}
320
321
# -----------------------------------------------------------------------------
322
# Name of the season, starting with an capital letter.
323
# -----------------------------------------------------------------------------
324
season <- function( n, format = "%Y-%m-%d" ) {
325
  WS <- as.Date("2016-12-15", "%Y-%m-%d") # Winter Solstice
326
  SE <- as.Date("2016-03-15", "%Y-%m-%d") # Spring Equinox
327
  SS <- as.Date("2016-06-15", "%Y-%m-%d") # Summer Solstice
328
  AE <- as.Date("2016-09-15", "%Y-%m-%d") # Autumn Equinox
329
330
  d <- when( anchor, n )
331
  d <- as.Date( strftime( d, format="2016-%m-%d" ) )
332
333
  ifelse( d >= WS | d < SE, "Winter",
334
    ifelse( d >= SE & d < SS, "Spring",
335
      ifelse( d >= SS & d < AE, "Summer", "Autumn" )
336
    )
337
  )
338
}
339
340
# -----------------------------------------------------------------------------
341
# Converts the first letter in a string to lowercase
342
# -----------------------------------------------------------------------------
343
lc <- function( s ) {
344
  concat( tolower( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
345
}
346
347
# -----------------------------------------------------------------------------
348
# Converts the entire string to lowercase
349
# -----------------------------------------------------------------------------
350
lower <- tolower
351
352
# -----------------------------------------------------------------------------
353
# Converts the first letter in a string to uppercase
354
# -----------------------------------------------------------------------------
355
uc <- function( s ) {
356
  concat( toupper( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
357
}
358
359
# -----------------------------------------------------------------------------
360
# Returns the number of days between the given dates.
361
# -----------------------------------------------------------------------------
362
days <- function( d1, d2, format = "%Y-%m-%d" ) {
363
  dates = c( d1, d2 )
364
  dt = strptime( dates, format = format )
365
  as.integer( difftime( dates[2], dates[1], units = "days" ) )
366
}
367
368
weeks <- function( began, ended ) {
369
  began = when( anchor, began )
370
  ended = when( anchor, ended )
371
372
  if( as.integer( ended - began ) < 0 ) {
373
    tempd = began
374
    began = ended
375
    ended = tempd
376
  }
377
378
  # Calculate number of elapsed weeks.
379
  length( seq( from = began, to = ended, by = "weeks" ) ) - 1
380
}
381
382
# -----------------------------------------------------------------------------
383
# Returns the number of years elapsed.
384
# -----------------------------------------------------------------------------
385
years <- function( began, ended ) {
386
  began = when( anchor, began )
387
  ended = when( anchor, ended )
388
389
  # Swap the dates if the end date comes before the start date.
390
  if( as.integer( ended - began ) < 0 ) {
391
    tempd = began
392
    began = ended
393
    ended = tempd
394
  }
395
396
  # Calculate number of elapsed years.
397
  length( seq( from = began, to = ended, by = "year" ) ) - 1
398
}
399
400
# -----------------------------------------------------------------------------
401
# Full name of the month, starting with a capital letter.
402
# -----------------------------------------------------------------------------
403
month <- function( n ) {
404
  # Faster than month.name[ x( n ) ]
405
  .subset( month.name, x( n ) )
406
}
407
408
# -----------------------------------------------------------------------------
409
# -----------------------------------------------------------------------------
410
money <- function( n ) {
411
  commas( x( n ) )
412
}
413
414
# -----------------------------------------------------------------------------
415
# -----------------------------------------------------------------------------
416
timeline <- function( n ) {
417
  concat( weekday( n ), ", ", annal( n ), " (", season( n ), ")" )
418
}
419
420
# -----------------------------------------------------------------------------
421
# Rounds to the nearest base value (e.g., round to nearest 10).
422
#
423
# @param base The nearest value to round to.
424
# -----------------------------------------------------------------------------
425
round.up <- function( n, base = 5 ) {
426
  base * round( x( n ) / base )
427
}
428
429
# -----------------------------------------------------------------------------
430
# Computes linear distance between two points using Haversine formula.
431
# Although Earth is an oblate spheroid, this will produce results close
432
# enough for most purposes.
433
#
434
# @param lat1/lon1 The source latitude and longitude.
435
# @param lat2/lon2 The destination latitude and longitude.
436
# @param radius The radius of the sphere.
437
#
438
# @return The distance between the two coordinates in meters.
439
# -----------------------------------------------------------------------------
440
haversine <- function( lat1, lon1, lat2, lon2, radius = 6371 ) {
441
  # Convert decimal degrees to radians
442
  lon1 = lon1 * pi / 180
443
  lon2 = lon2 * pi / 180
444
  lat1 = lat1 * pi / 180
445
  lat2 = lat2 * pi / 180
446
447
  # Haversine formula
448
  dlon = lon2 - lon1
449
  dlat = lat2 - lat1
450
  a = sin( dlat / 2 ) ** 2 + cos( lat1 ) * cos( lat2 ) * sin( dlon / 2 ) ** 2
451
  c = 2 * atan2( sqrt( a ), sqrt( 1-a ) )
452
453
  return( radius * c * 1000 )
454
}
455
1456
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
  "brother"   = c( "a" = "brothers",    "c" = "brethren" ),
271
  "child"     = c( "a" = NA_character_, "c" = "children" ),
272
  "cherub"    = c( "a" = "cherubim",    "c" = NA_character_ ),
273
  "cow"       = c( "a" = "cows",        "c" = "kine" ),
274
  "ephemeris" = c( "a" = NA_character_, "c" = "ephemerides" ),
275
  "genie"     = c( "a" = "genies",      "c" = "genii" ),
276
  "matrix"    = c( "a" = NA_character_, "c" = "matrices" ),
277
  "money"     = c( "a" = "moneys",      "c" = "monies" ),
278
  "mongoose"  = c( "a" = "mongooses",   "c" = NA_character_ ),
279
  "mythos"    = c( "a" = NA_character_, "c" = "mythoi" ),
280
  "octopus"   = c( "a" = "octopuses",   "c" = "octopodes" ),
281
  "ox"        = c( "a" = NA_character_, "c" = "oxen" ),
282
  "passerby"  = c( "a" = NA_character_, "c" = "passersby" ),
283
  "soliloquy" = c( "a" = "soliloquies", "c" = NA_character_ ),
284
  "seraph"    = c( "a" = "seraphim",    "c" = NA_character_ ),
285
  "trilby"    = c( "a" = "trilbys",     "c" = NA_character_ ),
286
  "vertex"    = c( "a" = NA_character_, "c" = "vertices" ),
287
  "vortex"    = c( "a" = NA_character_, "c" = "vortices" )
288
)
289
290
# -----------------------------------------------------------------------------
291
# Rule 5
292
#
293
# Handle irregular inflections for common suffixes.
294
# -----------------------------------------------------------------------------
295
pluralize_irregular_inflection_for_common_suffixes <- function( word ) {
296
  output <- sub( "man$", "men", word )
297
  output <- sub( "([ml])(ouse)$", "\\1ice", output )
298
  output <- sub( "tooth$", "teeth", output )
299
  output <- sub( "goose$", "geese", output )
300
  output <- sub( "foot$", "feet", output )
301
  output <- sub( "zoon$", "zoa", output )
302
  output <- sub( "([csx])(is)$", "\\1es", output )
303
304
  ifelse( output == word, NA_character_, output )
305
}
306
307
# -----------------------------------------------------------------------------
308
# Rule 6
309
#
310
# Handle fully assimilated classical inflections.
311
# -----------------------------------------------------------------------------
312
pluralize_fully_assimilated_classical_inflections <- function( word ) {
313
  output <- replace_suffix(
314
    word, "", "e", c( "alumna", "alga", "vertebra" ) )
315
  output <- replace_suffix(
316
    output, "ex", "ices", c( "codex", "murex", "silex" ) )
317
  output <- replace_suffix(
318
    output, "on", "a", c( 
319
      "aphelion",
320
      "asyndeton",
321
      "criterion",
322
      "hyperbaton",
323
      "noumenon",
324
      "organon",
325
      "perihelion",
326
      "phenomenon",
327
      "prolegomenon"
328
    )
329
  )
330
  output <- replace_suffix(
331
    output, "um", "a", c( 
332
      "agendum",
333
      "bacterium",
334
      "candelabrum",
335
      "datum",
336
      "desideratum",
337
      "erratum",
338
      "extremum",
339
      "ovum",
340
      "stratum"
341
    )
342
  )
343
344
  ifelse( output == word, NA_character_, output )
345
}
346
347
# -----------------------------------------------------------------------------
348
# Rule 7
349
#
350
# Classical variants of modern inflections (e.g., stigmata, soprani).
351
#
352
# See tables A.11 to A.13, A.15, A.16, A.18, A.21 to A.25.
353
# -----------------------------------------------------------------------------
354
pluralize_classical_variants_of_modern_inflections <- function( 
355
  word, method = c( "ac", "ca", "a", "c" ) ) {
356
  method <- match.arg( method )
357
358
  # -a to -as (anglicized) or -ae (classical)
359
  a11 <- c( 
360
    "abscissa",
361
    "amoeba",
362
    "antenna",
363
    "aurora",
364
    "formula",
365
    "hydra",
366
    "hyperbola",
367
    "lacuna",
368
    "medusa",
369
    "nebula",
370
    "nova",
371
    "parabola"
372
  )
373
374
  # Table A.12: -a to -as (anglicized) or -ata (classical)
375
  a12 <- c( 
376
    "anathema",
377
    "bema",
378
    "carcinoma",
379
    "charisma",
380
    "diploma",
381
    "dogma",
382
    "drama",
383
    "edema",
384
    "enema",
385
    "enigma",
386
    "gumma",
387
    "lemma",
388
    "lymphoma",
389
    "magma",
390
    "melisma",
391
    "miasma",
392
    "oedema",
393
    "sarcoma",
394
    "schema",
395
    "soma",
396
    "stigma",
397
    "stoma",
398
    "trauma"
399
  )
400
  
401
  # Table A.13: -en to -ens (anglicized) or -ina (classical)
402
  a13 <- c( "stamen", "foramen", "lumen" )
403
  
404
  # Table A.15: -ex to -exes (anglicized) or -ices (classical)
405
  a15 <- c( 
406
    "apex",
407
    "cortex",
408
    "index",
409
    "latex",
410
    "pontifex",
411
    "simplex",
412
    "vertex",
413
    "vortex"
414
  )
415
  
416
  # Table A.16: -is to -ises (anglicized) or -ides (classical)
417
  a16 <- c( "iris", "clitoris" )
418
  
419
  # Table A.18: -o to -os (anglicized) or -i (classical)
420
  a18 <- c( 
421
    "alto",
422
    "basso",
423
    "canto",
424
    "contralto",
425
    "crescendo",
426
    "solo",
427
    "soprano",
428
    "tempo"
429
  )
430
   
431
  # Table A.21: -um to -ums (anglicized) or -a (classical)
432
  a21 <- c( 
433
    "aquarium",
434
    "compendium",
435
    "consortium",
436
    "cranium",
437
    "curriculum",
438
    "dictum",
439
    "emporium",
440
    "enconium",
441
    "gymnasium",
442
    "honorarium",
443
    "interregnum",
444
    "lustrum",
445
    "maximum",
446
    "medium",
447
    "memorandum",
448
    "millenium",
449
    "minimum",
450
    "momentum",
451
    "optimum",
452
    "phylum",
453
    "quantum",
454
    "rostrum",
455
    "spectrum",
456
    "speculum",
457
    "stadium",
458
    "trapezium",
459
    "ultimatum",
460
    "vacuum",
461
    "velum"
462
  )
463
  
464
  # Table A.22: -us to -uses (anglicized) or -i (classical)
465
  a22 <- c( 
466
    "focus",
467
    "fungus",
468
    "genius",
469
    "incubus",
470
    "nimbus",
471
    "nucleolus",
472
    "radius",
473
    "stylus",
474
    "succubus",
475
    "torus",
476
    "umbilicus",
477
    "uterus"
478
  )
479
  
480
  # Table A.23: -us to -uses (anglicized) or -us (classical)
481
  a23 <- c( 
482
    "apparatus",
483
    "cantus",
484
    "coitus",
485
    "hiatus",
486
    "impetus",
487
    "nexus",
488
    "plexus",
489
    "prospectus",
490
    "sinus",
491
    "status"
492
  )
493
  
494
  output <- replace_suffix( word, "", "im", c( "cherub", "goy", "seraph"  ) )
495
  output <- replace_suffix( output, "", "i", c( "afreet", "afrit", "efreet" ) )
496
  
497
  if( method %in% c( "a", "ac" ) ) {
498
    output <- replace_suffix( output, "us", "uses", a23 )
499
    output <- replace_suffix( output, "us", "uses", a22 )
500
    output <- replace_suffix( output, "um", "ums", a21 )
501
    output <- replace_suffix( output, "o", "os", a18 )
502
    output <- replace_suffix( output, "is", "ises", a16 )
503
    output <- replace_suffix( output, "ex", "exes", a15 )
504
    output <- replace_suffix( output, "en", "ens", a13 )
505
    output <- replace_suffix( output, "a", "as", a12 )
506
    output <- replace_suffix( output, "a", "as", a11 )
507
  } else {
508
    output <- replace_suffix( output, "us", "us", a23 )
509
    output <- replace_suffix( output, "us", "i", a22 )
510
    output <- replace_suffix( output, "um", "a", a21 )
511
    output <- replace_suffix( output, "o", "i", a18 )
512
    output <- replace_suffix( output, "is", "ides", a16 )
513
    output <- replace_suffix( output, "ex", "ices", a15 )
514
    output <- replace_suffix( output, "en", "ina", a13 )
515
    output <- replace_suffix( output, "a", "ata", a12 )
516
    output <- replace_suffix( output, "a", "ae", a11 )
517
  }
518
519
  ifelse( 
520
    output == word & (method %in% c( "a", "ac" ) | !word %in% a23), 
521
    NA_character_, 
522
    output
523
  )
524
}
525
526
# -----------------------------------------------------------------------------
527
# Rule 8
528
#
529
# Suffixes -ch, -sh, -ss, -x, and -z take -es as plural (e.g., churches,
530
# classes).
531
# -----------------------------------------------------------------------------
532
pluralize_ch_sh_ss_suffixes <- function( word ) {
533
  output <- sub( "(([cs]h)|(x|z))$", "\\1es", word )
534
  output <- replace_suffix( output, "ss", "sses" )
535
536
  ifelse( output == word, NA_character_, output )
537
}
538
539
# -----------------------------------------------------------------------------
540
# Rule 9
541
#
542
# Certain words ending in -f or -fe take -ves in the plural.
543
# -----------------------------------------------------------------------------
544
pluralize_f_and_fe_suffix <- function( word ) {
545
  output <- sub( "([aeo]l|[^d]ea|ar)f$", "\\1ves", word )
546
  output <- sub( "([nlw]i)fe$", "\\1ves", output )
547
548
  ifelse( output == word, NA_character_, output )
549
}
550
551
# -----------------------------------------------------------------------------
552
# Rule 10
553
#
554
# Words ending in -y take -ies.
555
# -----------------------------------------------------------------------------
556
pluralize_y_suffix <- function( word ) {
557
  output <- sub( "([aeiou]y)$", "\\1s", word )
558
  output <- sub( "([A-Z].*y)$", "\\1s", output )
559
  output <- replace_suffix( output, "y", "ies" )
560
561
  ifelse( output == word, NA_character_, output )
562
}
563
564
# -----------------------------------------------------------------------------
565
# Rule 11
566
#
567
# Some words ending in -o take -os (lassos, solos). See tables A.17 and A.18.
568
# Others take -oes (potatoes, dominoes).
569
# When -o is preceded by a vowel always take -os (folios, bamboos).
570
# -----------------------------------------------------------------------------
571
pluralize_o_suffix <- function( word, method = c( "ac", "ca", "a", "c" ) ) {
572
  method <- match.arg( method )
573
574
  # Table A.17: -o to -os
575
  a17 <- c( 
576
    "albino",
577
    "archipelago",
578
    "armadillo",
579
    "commando",
580
    "ditto",
581
    "dynamo",
582
    "embryo",
583
    "fiasco",
584
    "generalissimo",
585
    "ghetto",
586
    "guano",
587
    "inferno",
588
    "jumbo",
589
    "lingo",
590
    "lumbago",
591
    "magneto",
592
    "manifesto",
593
    "medico",
594
    "octavo",
595
    "photo",
596
    "pro",
597
    "quarto",
598
    "rhino",
599
    "stylo"
600
  )
601
602
  # Table A.18: -o to -os (anglicized) or -i (classical)
603
  a18 <- c( 
604
    "alto",
605
    "basso",
606
    "canto",
607
    "contralto",
608
    "crescendo",
609
    "solo",
610
    "soprano",
611
    "tempo"
612
  )
613
614
  output <- replace_suffix( word, "o", "os", a17 )
615
  replacement <- if( method %in% c( "c", "ca" ) ) "i" else "os"
616
  output <- replace_suffix( output, "o", replacement, a18 )
617
618
  ifelse( output == word, NA_character_, output )
619
}
620
621
# -----------------------------------------------------------------------------
622
# Rule 12
623
#
624
# Compound word pluralization.
625
# -----------------------------------------------------------------------------
626
pluralize_compound_words <- function(
627
  word, method = c( "ac", "ca", "a", "c" ) ) {
628
  method <- match.arg( method )
629
630
  # "General" is pluralized.
631
  military <- c(
632
    "Adjutant",
633
    "Brigadier",
634
    "Lieutenant",
635
    "Major",
636
    "Quartermaster"
637
  )
638
639
  # X of Y -> plural(X) of Y
640
  # X at Y -> plural(X) of Y
641
  # X Y general -> X plural(Y) general
642
  # X-in-Y -> plural(X)-in-Y
643
644
  # Major Generals
645
  # Adjutant Generals
646
  # Lieutenant Generals
647
  # Brigadier Generals
648
  # Quartermaster Generals
649
650
  pluralize_cw <- Vectorize(
651
    function( cw, seps ) {
652
      if( cw[length( cw )] %in% c( "General", "general" ) && 
653
          (!cw[length( cw )] %in% military) ) {
654
        cw[1] <- pluralize( cw[1], method )
655
      } else {
656
        cw[1] <- pluralize( cw[1], method )
657
      }
658
659
      paste( paste0( seps, cw ), collapse = "" )
660
    }
661
  )
662
663
  parts <- strsplit( word, "[- ]+" )
664
  seps <- strsplit( word, "[^ -]+" )
665
  is_compound <- grepl( "[- ]", word )
666
  output <- word
667
  output[!is_compound] <- NA_character_
668
  output[is_compound] <- pluralize_cw( parts[is_compound], seps[is_compound] )
669
670
  output
671
}
672
673
# -----------------------------------------------------------------------------
674
# Rule 13
675
#
676
# Add -es if ending in -s (e.g., tennis, lychnis); otherwise, add -s.
677
# -----------------------------------------------------------------------------
678
pluralize_regular <- function( word ) {
679
  ending <- 's'
680
681
  if( endsWith( word, ending ) ) {
682
    ending <- "es"
683
  }
684
685
  paste0( word, ending )
686
}
687
688
# -----------------------------------------------------------------------------
689
# Determines whether the word ends with one of the given suffixes.
690
# -----------------------------------------------------------------------------
691
check_suffix <- function( x, suffixes ) {
692
  pattern <- paste0( "(", paste( suffixes, collapse = "|" ), ")$" )
693
  grepl( pattern, x, ignore.case = TRUE )
694
}
695
696
# -----------------------------------------------------------------------------
697
# Replaces the suffix of the word.
698
# -----------------------------------------------------------------------------
699
replace_suffix <- function( x, suffix, replacement, eligible = NULL ) {
700
  ifelse( 
701
    is.null( eligible ) | x %in% eligible, 
702
    sub( paste0( suffix, "$" ), replacement, x ),
703
    x
704
  )
705
}
706
707
# -----------------------------------------------------------------------------
708
# Returns y if x is na, otherwise x.
709
# -----------------------------------------------------------------------------
710
if.na <- function( x, y ) {
711
  ifelse( is.na( x ), y, x )
712
}
713
714
# -----------------------------------------------------------------------------
715
# Reduces the given function list.
716
# -----------------------------------------------------------------------------
717
coalesce <- function( ... ) {
718
  args <- list( ... )
719
  Reduce( if.na, args )
720
}
721
1722
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 README.md
1
![Total Downloads](https://img.shields.io/github/downloads/DaveJarvis/keenwrite/total?color=blue&label=Total%20Downloads&style=flat) ![Release Downloads](https://img.shields.io/github/downloads/DaveJarvis/keenwrite/latest/total?color=purple&label=Release%20Downloads&style=flat) ![Released](https://img.shields.io/github/release-date/DaveJarvis/keenwrite?color=red&style=flat&label=Released) ![Version](https://img.shields.io/github/v/release/DaveJarvis/keenwrite?style=flat&label=Release)
2
3
# ![Logo](docs/images/app-title.png)
4
5
A free, open-source, cross-platform desktop Markdown editor that can produce beautifully typeset PDFs.
6
7
## Download
8
9
Download one of the following editions:
10
11
* [Windows](https://github.com/DaveJarvis/keenwrite/releases/latest/download/keenwrite.exe)
12
* [Linux](https://github.com/DaveJarvis/keenwrite/releases/latest/download/keenwrite.bin)
13
* [Java Archive](https://github.com/DaveJarvis/keenwrite/releases/latest/download/keenwrite.jar)
14
15
## Run
16
17
Note that the first time the application runs, it will unpack itself into a local directory. Subsequent starts will be faster.
18
19
### Windows
20
21
Double-click the application to start; give the application permission to run.
22
23
### Linux
24
25
Execute the following commands in a terminal:
26
27
``` bash
28
chmod +x keenwrite.bin
29
./keenwrite.bin
30
```
31
32
### Other
33
34
On other platforms, such as MacOS, start the application as follows:
35
36
1. Download the *Full version* of the Java Runtime Environment, [JRE 19](https://bell-sw.com/pages/downloads).
37
1. Install the JRE (include JRE's `bin` directory in the `PATH` environment variable).
38
1. Open a new terminal.
39
1. Verify the installation: `java -version`
40
1. Download [keenwrite.jar](https://github.com/DaveJarvis/keenwrite/releases/latest/download/keenwrite.jar).
41
1. Download [keenwrite.sh](https://raw.githubusercontent.com/DaveJarvis/keenwrite/master/keenwrite.sh).
42
1. Place the `.jar` and `.sh` in the same directory.
43
1. Make `keenwrite.sh` executable: `chmod +x keenwrite.sh`
44
1. Run: `./keenwrite.sh`
45
46
The application is started.
47
48
## Features
49
50
The application offers:
51
52
* User-defined interpolated strings
53
* Auto-complete variable names based on variable values
54
* High-quality PDF exports
55
* Real-time spell check
56
* Real-time rendering of math using TeX notation
57
* Real-time document statistics (with CJK word separation)
58
* Diagrams: Mermaid, GraphViz, UML, sequence, timing, and more
59
* Dark, custom, and responsive user interface skins
60
* Integrated file manager
61
* Interactive document outline
62
* Internationalized font support (e.g., Chinese, Japanese, Korean, etc.)
63
* Support for Pandoc's fenced div extended attribute syntax
64
* R integration
65
* Customizable user interface having detachable tabs
66
* Platform-independent (Windows, Linux, MacOS)
67
68
## Typesetting
69
70
Typesetting to PDF files requires the following:
71
72
* [Theme Pack](https://github.com/DaveJarvis/keenwrite-themes/releases/latest/download/theme-pack.zip)
73
* [ConTeXt](https://wiki.contextgarden.net/Installation)
74
75
## Usage
76
77
Read the [detailed documentation](docs/README.md) for using the application.
78
79
### Skins
80
81
Read the [skins documentation](docs/skins.md) to learn about how to change
82
the user interface appearance.
83
84
## Screenshots
85
86
See [screenshots](docs/screenshots.md) for visuals.
87
88
## License
89
90
This software is licensed under the [BSD 2-Clause License](LICENSE.md) and
91
based on [Markdown-Writer-FX](https://github.com/JFormDesigner/markdown-writer-fx/blob/main/LICENSE).
92
193
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 19](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 build.gradle
1
import static org.gradle.api.JavaVersion.*
2
3
buildscript {
4
  repositories {
5
    mavenCentral()
6
  }
7
  dependencies {
8
    classpath 'org.owasp:dependency-check-gradle:7.4.3'
9
  }
10
}
11
12
plugins {
13
  id 'application'
14
  id 'org.openjfx.javafxplugin' version '0.0.13'
15
  id 'com.palantir.git-version' version '0.15.0'
16
}
17
18
apply plugin: 'org.owasp.dependencycheck'
19
20
repositories {
21
  mavenCentral()
22
23
  maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
24
  maven { url 'https://nexus.bedatadriven.com/content/groups/public' }
25
26
  maven {
27
    url "https://css4j.github.io/maven"
28
    mavenContent {
29
      releasesOnly()
30
    }
31
    content {
32
      includeGroup 'com.github.css4j'
33
      includeGroup 'io.sf.carte'
34
      includeGroup 'io.sf.jclf'
35
    }
36
  }
37
}
38
39
// Assume a cross-platform überjar unless targetOs is set.
40
String[] os = ['win', 'mac', 'linux']
41
42
if (project.hasProperty( 'targetOs' )) {
43
  if ('windows' == targetOs) {
44
    os = ["win"]
45
  } else {
46
    os = [targetOs]
47
  }
48
}
49
50
def moduleSecurity = [
51
    '--add-opens=javafx.graphics/javafx.scene=ALL-UNNAMED',
52
    '--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED',
53
    '--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED',
54
    '--add-opens=javafx.graphics/javafx.scene.text=ALL-UNNAMED',
55
    '--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED',
56
    '--add-opens=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED',
57
    '--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED',
58
    '--add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED',
59
    '--add-exports=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED',
60
    '--add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED',
61
    '--add-exports=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED',
62
    '--add-exports=javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED',
63
    '--add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED',
64
]
65
66
javafx {
67
  version = '19'
68
  modules = ['javafx.base', 'javafx.controls', 'javafx.graphics', 'javafx.swing']
69
  configuration = 'compileOnly'
70
}
71
72
dependencies {
73
  def v_junit = '5.9.1'
74
  def v_flexmark = '0.64.0'
75
  def v_jackson = '2.14.0'
76
  def v_echosvg = '0.2.2'
77
  def v_picocli = '4.7.0'
78
79
  // JavaFX
80
  implementation 'org.controlsfx:controlsfx:11.1.2'
81
  implementation 'org.fxmisc.richtext:richtextfx:0.11.0'
82
  implementation 'org.fxmisc.flowless:flowless:0.7.0'
83
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
84
  implementation 'com.miglayout:miglayout-javafx:11.0'
85
  implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.11.0'
86
  implementation 'com.panemu:tiwulfx-dock:0.2'
87
88
  // Markdown
89
  implementation "com.vladsch.flexmark:flexmark:${v_flexmark}"
90
  implementation "com.vladsch.flexmark:flexmark-ext-definition:${v_flexmark}"
91
  implementation "com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:${v_flexmark}"
92
  implementation "com.vladsch.flexmark:flexmark-ext-superscript:${v_flexmark}"
93
  implementation "com.vladsch.flexmark:flexmark-ext-tables:${v_flexmark}"
94
  implementation "com.vladsch.flexmark:flexmark-ext-typographic:${v_flexmark}"
95
96
  // YAML
97
  implementation "com.fasterxml.jackson.core:jackson-core:${v_jackson}"
98
  implementation "com.fasterxml.jackson.core:jackson-databind:${v_jackson}"
99
  implementation "com.fasterxml.jackson.core:jackson-annotations:${v_jackson}"
100
  implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${v_jackson}"
101
102
  // HTML parsing and rendering
103
  implementation 'org.jsoup:jsoup:1.15.3'
104
  // TODO: https://github.com/flyingsaucerproject/flyingsaucer/pull/170
105
  //implementation 'org.xhtmlrenderer:flying-saucer-core:9.1.22'
106
107
  // R
108
  implementation 'org.renjin:renjin-script-engine:3.5-beta76'
109
  implementation 'org.renjin.cran:rjson:0.2.15-renjin-21'
110
111
  // SVG
112
  implementation "io.sf.carte:echosvg-awt-util:${v_echosvg}"
113
  implementation "io.sf.carte:echosvg-bridge:${v_echosvg}"
114
  implementation "io.sf.carte:echosvg-css:${v_echosvg}"
115
  implementation "io.sf.carte:echosvg-dom:${v_echosvg}"
116
  implementation "io.sf.carte:echosvg-ext:${v_echosvg}"
117
  implementation "io.sf.carte:echosvg-gvt:${v_echosvg}"
118
  implementation "io.sf.carte:echosvg-parser:${v_echosvg}"
119
  implementation "io.sf.carte:echosvg-script:${v_echosvg}"
120
  implementation "io.sf.carte:echosvg-svg-dom:${v_echosvg}"
121
  implementation "io.sf.carte:echosvg-svggen:${v_echosvg}"
122
  implementation "io.sf.carte:echosvg-transcoder:${v_echosvg}"
123
  implementation "io.sf.carte:echosvg-util:${v_echosvg}"
124
  implementation "io.sf.carte:echosvg-xml:${v_echosvg}"
125
126
  // Misc.
127
  implementation 'org.ahocorasick:ahocorasick:0.6.3'
128
  implementation 'org.apache.commons:commons-configuration2:2.8.0'
129
  implementation 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
130
  implementation 'javax.validation:validation-api:2.0.1.Final'
131
  implementation 'org.greenrobot:eventbus-java:3.3.1'
132
  implementation 'commons-beanutils:commons-beanutils:1.9.4'
133
134
  // Command-line parsing
135
  implementation "info.picocli:picocli:${v_picocli}"
136
  annotationProcessor "info.picocli:picocli-codegen:${v_picocli}"
137
138
  // KeenQuotes, KeenType, KeenSpell, word split.
139
  implementation fileTree( include: ['**/*.jar'], dir: 'libs' )
140
141
  def fx = ['controls', 'graphics', 'fxml', 'swing']
142
143
  fx.each { fxitem ->
144
    os.each { ositem ->
145
      runtimeOnly "org.openjfx:javafx-${fxitem}:${javafx.version}:${ositem}"
146
    }
147
  }
148
149
  testImplementation 'org.testfx:testfx-junit5:4.0.16-alpha'
150
  testImplementation "org.junit.jupiter:junit-jupiter-api:${v_junit}"
151
  testImplementation "org.junit.jupiter:junit-jupiter-params:${v_junit}"
152
  testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
153
}
154
155
final resourceDir = sourceSets.main.resources.srcDirs[0]
156
final Properties config = new Properties()
157
final File configFile = file( "${resourceDir}/bootstrap.properties" )
158
final FileInputStream configStream = new FileInputStream( configFile )
159
config.load( configStream )
160
configStream.close()
161
162
final String applicationName = config.get( 'application.title' ).toString().toLowerCase()
163
final String applicationPackage = "com.${applicationName}"
164
final String applicationClass = "${applicationPackage}.Launcher"
165
166
java {
167
  sourceCompatibility = VERSION_17
168
  targetCompatibility = VERSION_17
169
}
170
171
compileJava {
172
  options.compilerArgs += [
173
      "-Xlint:unchecked",
174
      "-Xlint:deprecation",
175
      "-Aproject=${applicationPackage}/${applicationName}"
176
  ]
177
}
178
179
application {
180
  mainClass.set( applicationClass )
181
  applicationDefaultJvmArgs = moduleSecurity
182
}
183
184
version = gitVersion()
185
186
final File p = new File( "${resourceDir}/com/${applicationName}/app.properties" )
187
p.write( "application.version=${version}" )
188
189
jar {
190
  duplicatesStrategy = DuplicatesStrategy.EXCLUDE
191
192
  doFirst {
193
    manifest {
194
      attributes 'Main-Class': applicationClass
195
    }
196
  }
197
198
  from {
199
    (configurations.runtimeClasspath.findAll { !it.path.endsWith( ".pom" ) })
200
        .collect { it.isDirectory() ? it : zipTree( it ) }
201
  }
202
203
  archiveFileName = "${applicationName}.jar"
204
205
  exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
206
}
207
208
distributions {
209
  main {
210
    distributionBaseName.set( applicationName )
211
212
    contents {
213
      from { ['LICENSE.md', 'README.md'] }
214
      into( 'images' ) {
215
        from { 'images' }
216
      }
217
    }
218
  }
219
}
220
221
test {
222
  useJUnitPlatform()
223
224
  doFirst { jvmArgs = moduleSecurity }
225
  testLogging { exceptionFormat = 'full' }
226
}
1227
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://fonts.google.com/download?family=Courier%20Prime" "courier-prime.zip"
47
ADD "https://fonts.google.com/download?family=Inconsolata" "inconsolata.zip"
48
ADD "https://fonts.google.com/download?family=Libre%20Baskerville" "libre-baskerville.zip"
49
ADD "https://fonts.google.com/download?family=Nunito" "nunito.zip"
50
ADD "https://fonts.google.com/download?family=Roboto" "roboto.zip"
51
ADD "https://fonts.google.com/download?family=Roboto%20Mono" "roboto-mono.zip"
52
ADD "https://github.com/adobe-fonts/source-serif/releases/download/4.004R/source-serif-4.004.zip" "source-serif.zip"
53
ADD "https://www.omnibus-type.com/wp-content/uploads/Archivo-Narrow.zip" "archivo-narrow.zip"
54
55
# Typesetting software
56
ADD "http://lmtx.pragma-ade.nl/install-lmtx/context-linuxmusl.zip" "context.zip"
57
58
# ########################################################################
59
#
60
# Install components, modules, configure system, remove unnecessary files
61
#
62
# ########################################################################
63
WORKDIR $CONTEXT_HOME
64
65
RUN \
66
  apk --update --no-cache \
67
    add ca-certificates curl fontconfig inkscape rsync && \
68
  mkdir -p \
69
    "$FONTS_DIR" "$INSTALL_DIR" \
70
    "$TARGET_DIR" "$SOURCE_DIR" "$THEMES_DIR" "$IMAGES_DIR" "$CACHES_DIR" && \
71
  echo "export CONTEXT_HOME=\"$CONTEXT_HOME\"" >> $PROFILE && \
72
  echo "export PATH=\"\$PATH:\$CONTEXT_HOME/tex/texmf-linuxmusl/bin\"" >> $PROFILE && \
73
  echo "export OSFONTDIR=\"/usr/share/fonts//\"" >> $PROFILE && \
74
  echo "PS1='\\u@typesetter:\\w\\$ '" >> $PROFILE && \
75
  unzip -d $CONTEXT_HOME $DOWNLOAD_DIR/context.zip && \
76
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/courier-prime.zip "*.ttf" && \
77
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/libre-baskerville.zip "*.ttf" && \
78
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/inconsolata.zip "**/Inconsolata/*.ttf" && \
79
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/nunito.zip "static/*.ttf" && \
80
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/roboto.zip "*.ttf" && \
81
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/roboto-mono.zip "static/*.ttf" && \
82
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/source-serif.zip "source-serif-4.004/OTF/SourceSerif4-*.otf" && \
83
  unzip -j -o -d $FONTS_DIR $DOWNLOAD_DIR/archivo-narrow.zip "Archivo-Narrow/otf/*.otf" && \
84
  mv $DOWNLOAD_DIR/*tf $FONTS_DIR && \
85
  fc-cache -f -v && \
86
  mkdir -p tex && \
87
  rsync \
88
    --recursive --links --times \
89
    --info=progress2,remove,symsafe,flist,del \
90
    --human-readable --del \
91
    rsync://contextgarden.net/minimals/current/modules/ modules && \
92
  rsync \
93
    -rlt --exclude=/VERSION --del modules/*/ tex/texmf-modules && \
94
  sh install.sh && \
95
  rm -f $DOWNLOAD_DIR/*.zip && \
96
  rm -rf \
97
    "modules" \
98
    "/var/cache" \
99
    "/usr/share/icons" \
100
    $CONTEXT_HOME/tex/texmf-modules/doc \
101
    $CONTEXT_HOME/tex/texmf-context/doc && \
102
  mkdir -p $CONTEXT_HOME/tex/texmf-fonts/tex/context/user && \
103
  ln -s $CONTEXT_HOME/tex/texmf-fonts/tex/context/user $HOME/fonts && \
104
  source $PROFILE && \
105
  mtxrun --generate && \
106
  find \
107
    /usr/share/inkscape \
108
    -type f -not -iname "*.xml" -exec rm {} \; && \
109
  find \
110
    $CONTEXT_HOME \
111
    -type f \
112
      \( -iname \*.pdf -o -iname \*.txt -o -iname \*.log \) \
113
    -exec rm {} \;
114
115
# ########################################################################
116
#
117
# Ensure login goes to the target directory. ConTeXt prefers to export to
118
# the current working directory.
119
#
120
# ########################################################################
121
WORKDIR $TARGET_DIR
122
1123
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
readonly BUILD_DIR=build
12
readonly PROPERTIES="${SCRIPT_DIR}/../src/main/resources/bootstrap.properties"
13
14
# Read the properties file to get the container version.
15
while IFS='=' read -r key value
16
do
17
  key=$(echo $key | tr '.' '_')
18
  eval ${key}=\${value}
19
done < "${PROPERTIES}"
20
21
readonly CONTAINER_EXE=podman
22
readonly CONTAINER_SHORTNAME=typesetter
23
readonly CONTAINER_VERSION=${container_version}
24
readonly CONTAINER_NETWORK=host
25
readonly CONTAINER_FILE="${CONTAINER_SHORTNAME}"
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_DIR_SOURCE="/root/source"
31
readonly CONTAINER_DIR_TARGET="/root/target"
32
readonly CONTAINER_DIR_IMAGES="/root/images"
33
readonly CONTAINER_DIR_FONTS="/root/fonts"
34
readonly CONTAINER_REPO=ghcr.io
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
ARG_ACCESS_TOKEN=""
43
44
DEPENDENCIES=(
45
  "podman,https://podman.io"
46
  "tar,https://www.gnu.org/software/tar"
47
  "bzip2,https://gitlab.com/bzip2/bzip2"
48
)
49
50
ARGUMENTS+=(
51
  "b,build,Build container"
52
  "c,connect,Connect to container"
53
  "d,delete,Remove all containers"
54
  "s,source,Set mount point for input document (before typesetting)"
55
  "t,target,Set mount point for output file (after typesetting)"
56
  "i,images,Set mount point for image files (to typeset)"
57
  "f,fonts,Set mount point for font files (during typesetting)"
58
  "k,token,Set personal access token (to publish)"
59
  "l,load,Load container (${CONTAINER_COMPRESSED_PATH})"
60
  "p,publish,Publish the container (after logging in)"
61
  "r,run,Run a command in the container (\"${ARG_CONTAINER_COMMAND}\")"
62
  "v,version,Set container version to publish (${CONTAINER_VERSION})"
63
  "x,export,Save container (${CONTAINER_COMPRESSED_PATH})"
64
)
65
66
# ---------------------------------------------------------------------------
67
# Manages the container.
68
# ---------------------------------------------------------------------------
69
execute() {
70
  $do_delete
71
  $do_build
72
  $do_publish
73
  $do_export
74
  $do_load
75
  $do_execute
76
  $do_connect
77
78
  return 1
79
}
80
81
# ---------------------------------------------------------------------------
82
# Deletes all containers.
83
# ---------------------------------------------------------------------------
84
utile_delete() {
85
  $log "Deleting all containers"
86
87
  ${CONTAINER_EXE} rmi --all --force > /dev/null
88
89
  $log "Containers deleted"
90
}
91
92
# ---------------------------------------------------------------------------
93
# Builds the container file in the current working directory.
94
# ---------------------------------------------------------------------------
95
utile_build() {
96
  $log "Building"
97
98
  # Show what commands are run while building, but not the commands' output.
99
  ${CONTAINER_EXE} build \
100
    --network=${CONTAINER_NETWORK} \
101
    --squash \
102
    -t ${ARG_CONTAINER_NAME} . | \
103
  grep ^STEP
104
}
105
106
# ---------------------------------------------------------------------------
107
# Publishes the container to the repository.
108
# ---------------------------------------------------------------------------
109
utile_publish() {
110
  local -r username=$(git config user.name | tr '[A-Z]' '[a-z]')
111
  local -r repo="${CONTAINER_REPO}/${username}/${ARG_CONTAINER_NAME}"
112
113
  if [ ! -z ${ARG_ACCESS_TOKEN} ]; then
114
    echo ${ARG_ACCESS_TOKEN} | \
115
      ${CONTAINER_EXE} login ghcr.io -u $(git config user.name) --password-stdin
116
117
    $log "Tagging"
118
119
    ${CONTAINER_EXE} tag ${ARG_CONTAINER_NAME} ${repo}
120
121
    $log "Pushing ${ARG_CONTAINER_NAME} to ${CONTAINER_REPO}"
122
123
    ${CONTAINER_EXE} push ${repo}
124
125
    $log "Published ${ARG_CONTAINER_NAME} to ${CONTAINER_REPO}"
126
  else
127
    error "Provide a personal access token (-k TOKEN) to publish."
128
  fi
129
}
130
131
# ---------------------------------------------------------------------------
132
# Creates the command-line option for a read-only mountpoint.
133
#
134
# $1 - The host directory.
135
# $2 - The guest (container) directory.
136
# $3 - The file system permissions (set to 1 for read-write).
137
# ---------------------------------------------------------------------------
138
get_mountpoint() {
139
  $log "Mounting ${1} as ${2}"
140
141
  local result=""
142
  local binding="ro"
143
144
  if [ ! -z "${3+x}" ]; then
145
    binding="Z"
146
  fi
147
148
  if [ ! -z "${1}" ]; then
149
    result="-v ${1}:${2}:${binding}"
150
  fi
151
152
  echo "${result}"
153
}
154
155
get_mountpoint_source() {
156
  echo $(get_mountpoint "${ARG_MOUNTPOINT_SOURCE}" "${CONTAINER_DIR_SOURCE}")
157
}
158
159
get_mountpoint_target() {
160
  echo $(get_mountpoint "${ARG_MOUNTPOINT_TARGET}" "${CONTAINER_DIR_TARGET}" 1)
161
}
162
163
get_mountpoint_images() {
164
  echo $(get_mountpoint "${ARG_MOUNTPOINT_IMAGES}" "${CONTAINER_DIR_IMAGES}")
165
}
166
167
get_mountpoint_fonts() {
168
  echo $(get_mountpoint "${ARG_MOUNTPOINT_FONTS}" "${CONTAINER_DIR_FONTS}")
169
}
170
171
# ---------------------------------------------------------------------------
172
# Connects to the container.
173
# ---------------------------------------------------------------------------
174
utile_connect() {
175
  $log "Connecting to container"
176
177
  declare -r mount_source=$(get_mountpoint_source)
178
  declare -r mount_target=$(get_mountpoint_target)
179
  declare -r mount_images=$(get_mountpoint_images)
180
  declare -r mount_fonts=$(get_mountpoint_fonts)
181
182
  ${CONTAINER_EXE} run \
183
    --network="${CONTAINER_NETWORK}" \
184
    --rm \
185
    -it \
186
    ${mount_source} \
187
    ${mount_target} \
188
    ${mount_images} \
189
    ${mount_fonts} \
190
    "${ARG_CONTAINER_NAME}"
191
}
192
193
# ---------------------------------------------------------------------------
194
# Runs a command in the container.
195
#
196
# Examples:
197
#
198
#   ./manage.sh -r "ls /"
199
#   ./manage.sh -r "context --version"
200
# ---------------------------------------------------------------------------
201
utile_execute() {
202
  $log "Running \"${ARG_CONTAINER_COMMAND}\":"
203
204
  ${CONTAINER_EXE} run \
205
    --network=${CONTAINER_NETWORK} \
206
    --rm \
207
    -i \
208
    -t "${ARG_CONTAINER_NAME}" \
209
    /bin/sh --login -c "${ARG_CONTAINER_COMMAND}"
210
}
211
212
# ---------------------------------------------------------------------------
213
# Saves the container to a file.
214
# ---------------------------------------------------------------------------
215
utile_export() {
216
  if [[ -f "${CONTAINER_COMPRESSED_PATH}" ]]; then
217
    warning "${CONTAINER_COMPRESSED_PATH} exists, delete before saving."
218
  else
219
    $log "Saving ${CONTAINER_SHORTNAME} image"
220
221
    mkdir -p "${BUILD_DIR}"
222
223
    ${CONTAINER_EXE} save \
224
      --quiet \
225
      -o "${BUILD_DIR}/${CONTAINER_ARCHIVE_FILE}" \
226
      "${ARG_CONTAINER_NAME}"
227
228
    $log "Compressing to ${CONTAINER_COMPRESSED_PATH}"
229
    gzip "${CONTAINER_ARCHIVE_PATH}"
230
231
    $log "Saved ${CONTAINER_SHORTNAME} image"
232
  fi
233
}
234
235
# ---------------------------------------------------------------------------
236
# Loads the container from a file.
237
# ---------------------------------------------------------------------------
238
utile_load() {
239
  if [[ -f "${CONTAINER_COMPRESSED_PATH}" ]]; then
240
    $log "Loading ${CONTAINER_SHORTNAME} from ${CONTAINER_COMPRESSED_PATH}"
241
242
    ${CONTAINER_EXE} load \
243
      --quiet \
244
      -i "${CONTAINER_COMPRESSED_PATH}"
245
246
    $log "Loaded ${CONTAINER_SHORTNAME} image"
247
  else
248
    warning "Missing ${CONTAINER_COMPRESSED_PATH}; use build follwed by save"
249
  fi
250
}
251
252
argument() {
253
  local consume=1
254
255
  case "$1" in
256
    -b|--build)
257
    do_build=utile_build
258
    ;;
259
    -c|--connect)
260
    do_connect=utile_connect
261
    ;;
262
    -d|--delete)
263
    do_delete=utile_delete
264
    ;;
265
    -k|--token)
266
    if [ ! -z "${2+x}" ]; then
267
      ARG_ACCESS_TOKEN="$2"
268
      consume=2
269
    fi
270
    ;;
271
    -l|--load)
272
    do_load=utile_load
273
    ;;
274
    -i|--images)
275
    if [ ! -z "${2+x}" ]; then
276
      ARG_MOUNTPOINT_IMAGES="$2"
277
      consume=2
278
    fi
279
    ;;
280
    -t|--target)
281
    if [ ! -z "${2+x}" ]; then
282
      ARG_MOUNTPOINT_TARGET="$2"
283
      consume=2
284
    fi
285
    ;;
286
    -p|--publish)
287
    do_publish=utile_publish
288
    ;;
289
    -r|--run)
290
    do_execute=utile_execute
291
292
    if [ ! -z "${2+x}" ]; then
293
      ARG_CONTAINER_COMMAND="$2"
294
      consume=2
295
    fi
296
    ;;
297
    -s|--source)
298
    if [ ! -z "${2+x}" ]; then
299
      ARG_MOUNTPOINT_SOURCE="$2"
300
      consume=2
301
    fi
302
    ;;
303
    -v|--version)
304
    if [ ! -z "${2+x}" ]; then
305
      ARG_CONTAINER_NAME="${CONTAINER_SHORTNAME}:$2"
306
      consume=2
307
    fi
308
    ;;
309
    -x|--export)
310
    do_export=utile_export
311
    ;;
312
  esac
313
314
  return ${consume}
315
}
316
317
do_build=:
318
do_connect=:
319
do_delete=:
320
do_execute=:
321
do_load=:
322
do_publish=:
323
do_export=:
324
325
main "$@"
326
1327
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
15
## Example usage
16
17
On Linux, simple usages include:
18
19
    keenwrite.bin -i $HOME/document/01.md -o document.xhtml
20
21
    keenwrite.bin -i $HOME/document/01.md -o document.md \
22
      -v $HOME/document/variables.yaml
23
24
That command will convert `01.md` into the respective file formats. In
25
the first case, it will become an HTML page. In the second case, it will
26
become a Markdown document with all variables interpolated and replaced.
27
28
A more complex example follows:
29
30
    keenwrite.bin -i $HOME/document/01.Rmd -o document.pdf \
31
      --image-dir=$HOME/document/images -v $HOME/document/variables.yaml \
32
      --metadata="title={{book.title}}" --metadata="author={{book.author}}" \
33
      --r-dir=$HOME/document/r --r-script=$HOME/document/r/bootstrap.R \
34
      --theme-dir=$HOME/document/themes/boschet
35
36
That command will convert `01.Rmd` to `document.pdf` and replace the metadata
37
using values from the variable definitions file.
38
39
Directory names containing spaces must be quoted. For example, on Windows:
40
41
    keenwrite.bin -i "C:\Users\My Documents\01.Rmd" -o document.pdf
42
143
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 familes 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
4
15
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="19.0.1"
34
ARG_JAVA_UPDATE="11"
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
  "tar,https://www.gnu.org/software/tar"
47
  "wine,https://www.winehq.org"
48
  "unzip,http://infozip.sourceforge.net"
49
)
50
51
ARGUMENTS+=(
52
  "a,arch,Target operating system architecture (amd64)"
53
  "o,os,Target operating system (linux, windows, mac)"
54
  "u,update,Java update version number (${ARG_JAVA_UPDATE})"
55
  "v,version,Full Java version (${ARG_JAVA_VERSION})"
56
)
57
58
ARCHIVE_EXT="tar.gz"
59
ARCHIVE_APP="tar xf"
60
APP_EXTENSION="bin"
61
62
# ---------------------------------------------------------------------------
63
# Generates
64
# ---------------------------------------------------------------------------
65
execute() {
66
  $do_configure_target
67
  $do_build
68
  $do_clean
69
70
  pushd "${ARG_DIR_DIST}" > /dev/null 2>&1
71
72
  $do_extract_java
73
  $do_create_launch_script
74
  $do_copy_archive
75
76
  popd > /dev/null 2>&1
77
78
  $do_create_launcher
79
80
  $do_brand_windows
81
82
  return 1
83
}
84
85
# ---------------------------------------------------------------------------
86
# Configure platform-specific commands and file names.
87
# ---------------------------------------------------------------------------
88
utile_configure_target() {
89
  if [ "${ARG_JAVA_OS}" = "windows" ]; then
90
    ARCHIVE_EXT="zip"
91
    ARCHIVE_APP="unzip -qq"
92
    FILE_DIST_EXEC="run.bat"
93
    APP_EXTENSION="exe"
94
    do_create_launch_script=utile_create_launch_script_windows
95
    do_brand_windows=utile_brand_windows
96
  fi
97
}
98
99
# ---------------------------------------------------------------------------
100
# Build platform-specific überjar.
101
# ---------------------------------------------------------------------------
102
utile_build() {
103
  $log "Delete ${ARG_PATH_DIST_JAR}"
104
  rm -f "${ARG_PATH_DIST_JAR}"
105
106
  $log "Build application for ${ARG_JAVA_OS}"
107
  gradle clean jar -PtargetOs="${ARG_JAVA_OS}"
108
}
109
110
# ---------------------------------------------------------------------------
111
# Purges the existing distribution directory to recreate the launcher.
112
# This refreshes the JRE from the downloaded archive.
113
# ---------------------------------------------------------------------------
114
utile_clean() {
115
  $log "Recreate ${ARG_DIR_DIST}"
116
  rm -rf "${ARG_DIR_DIST}"
117
  mkdir -p "${ARG_DIR_DIST}"
118
}
119
120
# ---------------------------------------------------------------------------
121
# Extract platform-specific Java Runtime Environment. This will download
122
# and cache the required Java Runtime Environment for the target platform.
123
# On subsequent runs, the cached version is used, instead of issuing another
124
# download.
125
# ---------------------------------------------------------------------------
126
utile_extract_java() {
127
  $log "Extract Java"
128
  local -r java_vm="jre"
129
  local -r java_version="${ARG_JAVA_VERSION}+${ARG_JAVA_UPDATE}"
130
  local -r url_java="https://download.bell-sw.com/java/${java_version}/bellsoft-${java_vm}${java_version}-${ARG_JAVA_OS}-${ARG_JAVA_ARCH}-full.${ARCHIVE_EXT}"
131
132
  local -r file_java="${java_vm}-${java_version}-${ARG_JAVA_OS}-${ARG_JAVA_ARCH}.${ARCHIVE_EXT}"
133
  local -r path_java="/tmp/${file_java}"
134
135
  # File must have contents.
136
  if [ ! -s ${path_java} ]; then
137
    $log "Download ${url_java} to ${path_java}"
138
    wget -q "${url_java}" -O "${path_java}"
139
  fi
140
141
  $log "Unpack ${path_java}"
142
  $ARCHIVE_APP "${path_java}"
143
144
  local -r dir_java="${java_vm}-${ARG_JAVA_VERSION}-full"
145
146
  $log "Rename ${dir_java} to ${ARG_JAVA_DIR}"
147
  mv "${dir_java}" "${ARG_JAVA_DIR}"
148
}
149
150
# ---------------------------------------------------------------------------
151
# Create Linux-specific launch script.
152
# ---------------------------------------------------------------------------
153
utile_create_launch_script_linux() {
154
  $log "Create Linux launch script"
155
156
  cat > "${FILE_DIST_EXEC}" << __EOT
157
#!/usr/bin/env bash
158
159
readonly SCRIPT_SRC="\$(dirname "\${BASH_SOURCE[\${#BASH_SOURCE[@]} - 1]}")"
160
161
"\${SCRIPT_SRC}/${ARG_JAVA_DIR}/bin/java" ${OPT_JAVA} -jar "\${SCRIPT_SRC}/${FILE_APP_JAR}" "\$@" 2>/dev/null
162
__EOT
163
164
  chmod +x "${FILE_DIST_EXEC}"
165
}
166
167
# ---------------------------------------------------------------------------
168
# Create Windows-specific launch script.
169
# ---------------------------------------------------------------------------
170
utile_create_launch_script_windows() {
171
  $log "Create Windows launch script"
172
173
  cat > "${FILE_DIST_EXEC}" << __EOT
174
@echo off
175
176
set SCRIPT_DIR=%~dp0
177
"%SCRIPT_DIR%\\${ARG_JAVA_DIR}\\bin\\java" ${OPT_JAVA} -jar "%SCRIPT_DIR%\\${APP_NAME}.jar" %* 2>nul
178
__EOT
179
180
  # Convert Unix end of line characters (\n) to Windows format (\r\n).
181
  # This avoids any potential line conversion issues with the repository.
182
  sed -i 's/$/\r/' "${FILE_DIST_EXEC}"
183
}
184
185
# ---------------------------------------------------------------------------
186
# Modify the binary to include icon and identifying information.
187
# ---------------------------------------------------------------------------
188
utile_brand_windows() {
189
  # Read the properties file to get the application name (case sensitvely).
190
  while IFS='=' read -r key value
191
  do
192
    key=$(echo $key | tr '.' '_')
193
    eval ${key}=\${value}
194
  done < "src/main/resources/bootstrap.properties"
195
196
  readonly BINARY="${APP_NAME}.exe"
197
  readonly VERSION=$(git describe --tags)
198
  readonly COMPANY="White Magic Software, Ltd."
199
  readonly YEAR=$(date +%Y)
200
  readonly DESCRIPTION="Markdown editor with live preview, variables, and math."
201
  readonly SIZE=$(stat --format="%s" ${BINARY})
202
203
  wine ${SCRIPT_DIR}/scripts/rcedit-x64.exe "${BINARY}" \
204
    --set-icon "scripts/logo.ico" \
205
    --set-version-string "OriginalFilename" "${application_title}.exe" \
206
    --set-version-string "CompanyName" "${COMPANY}" \
207
    --set-version-string "ProductName" "${application_title}" \
208
    --set-version-string "LegalCopyright" "Copyright ${YEAR} ${COMPANY}" \
209
    --set-version-string "FileDescription" "${DESCRIPTION}" \
210
    --set-version-string "Size" "${DESCRIPTION}" \
211
    --set-product-version "${VERSION}" \
212
    --set-file-version "${VERSION}"
213
214
  mv -f "${BINARY}" "${application_title}.exe"
215
}
216
217
# ---------------------------------------------------------------------------
218
# Copy application überjar.
219
# ---------------------------------------------------------------------------
220
utile_copy_archive() {
221
  $log "Create copy of ${FILE_APP_JAR}"
222
  cp "${ARG_PATH_DIST_JAR}" "${FILE_APP_JAR}"
223
}
224
225
# ---------------------------------------------------------------------------
226
# Create platform-specific launcher binary.
227
# ---------------------------------------------------------------------------
228
utile_create_launcher() {
229
  local -r FILE_APP_NAME="${APP_NAME}.${APP_EXTENSION}"
230
  $log "Create ${FILE_APP_NAME}"
231
232
  # Warp-packer does not seem to overwrite the file.
233
  rm -f "${FILE_APP_NAME}"
234
235
  # Download uses amd64, but warp-packer differs.
236
  if [ "${ARG_JAVA_ARCH}" = "amd64" ]; then
237
    ARG_JAVA_ARCH="x64"
238
  fi
239
240
  warp-packer \
241
    pack \
242
    --arch "${ARG_JAVA_OS}-${ARG_JAVA_ARCH}" \
243
    --input-dir "${ARG_DIR_DIST}" \
244
    --exec "${FILE_DIST_EXEC}" \
245
    --output "${FILE_APP_NAME}" > /dev/null
246
247
  chmod +x "${FILE_APP_NAME}"
248
}
249
250
argument() {
251
  local consume=2
252
253
  case "$1" in
254
    -a|--arch)
255
    ARG_JAVA_ARCH="$2"
256
    ;;
257
    -o|--os)
258
    ARG_JAVA_OS="$2"
259
    ;;
260
    -u|--update)
261
    ARG_JAVA_UPDATE="$2"
262
    ;;
263
    -v|--version)
264
    ARG_JAVA_VERSION="$2"
265
    ;;
266
  esac
267
268
  return ${consume}
269
}
270
271
do_configure_target=utile_configure_target
272
do_build=utile_build
273
do_clean=utile_clean
274
do_extract_java=utile_extract_java
275
do_create_launch_script=utile_create_launch_script_linux
276
do_copy_archive=utile_copy_archive
277
do_create_launcher=utile_create_launcher
278
do_brand_windows=:
279
280
main "$@"
281
1282
A keenwrite.sh
1
#!/usr/bin/env bash
2
3
java \
4
  --add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED \
5
  --add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED \
6
  --add-opens=javafx.graphics/javafx.scene.text=ALL-UNNAMED \
7
  --add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED \
8
  --add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED \
9
  --add-opens=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED \
10
  --add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED \
11
  --add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED \
12
  --add-exports=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED \
13
  --add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED \
14
  --add-exports=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED \
15
  --add-exports=javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED \
16
  --add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED \
17
  -jar keenwrite.jar $@
18
119
A libs/flying-saucer-core-9.1.23.jar
Binary file
A libs/keenquotes.jar
Binary file
A libs/keenspell.jar
Binary file
A libs/keentype-lib.jar
Binary file
A libs/tokenize.jar
Binary file
A r.zip
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 Windows installer binary"
33
  ${BIN_INSTALLER} -o windows
34
35
  $log "Build Linux installer binary"
36
  ${BIN_INSTALLER} -o linux
37
}
38
39
preprocess() {
40
  while IFS='=' read -r key value; do
41
    if [[ "${key}" = "" || "${key}" = "#"* ]]; then
42
      continue
43
    fi
44
45
    key=$(echo $key | tr '.' '_')
46
    eval ${key}=\${value}
47
  done < "${FILE_PROPERTIES}"
48
49
  application_title="${application_title}"
50
51
  return 1
52
}
53
54
main "$@"
55
156
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
package com.keenwrite;
2
3
import com.keenwrite.cmdline.Arguments;
4
import com.keenwrite.util.AlphanumComparator;
5
6
import java.io.IOException;
7
import java.nio.file.Path;
8
import java.util.ArrayList;
9
import java.util.concurrent.Callable;
10
import java.util.concurrent.CompletableFuture;
11
import java.util.concurrent.ExecutorService;
12
import java.util.concurrent.atomic.AtomicInteger;
13
14
import static com.keenwrite.Launcher.terminate;
15
import static com.keenwrite.events.StatusEvent.clue;
16
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
17
import static com.keenwrite.util.FileWalker.walk;
18
import static java.lang.System.lineSeparator;
19
import static java.nio.file.Files.readString;
20
import static java.nio.file.Files.writeString;
21
import static java.util.concurrent.Executors.newFixedThreadPool;
22
import static org.apache.commons.io.FilenameUtils.getExtension;
23
24
/**
25
 * Responsible for executing common commands. These commands are shared by
26
 * both the graphical and the command-line interfaces.
27
 */
28
public class AppCommands {
29
  private static final ExecutorService sExecutor = newFixedThreadPool( 1 );
30
31
  /**
32
   * Sci-fi genres, which are can be longer than other genres, typically fall
33
   * below 150,000 words at 6 chars per word. This reduces re-allocations of
34
   * memory when concatenating files together when exporting novels.
35
   */
36
  private static final int DOCUMENT_LENGTH = 150_000 * 6;
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
   */
72
  private static void file_export(
73
    final Arguments args, final CompletableFuture<Path> future ) {
74
    assert args != null;
75
    assert future != null;
76
77
    final Callable<Path> callableTask = () -> {
78
      try {
79
        final var context = args.createProcessorContext();
80
        final var concat = context.getConcatenate();
81
        final var inputPath = context.getSourcePath();
82
        final var outputPath = context.getTargetPath();
83
        final var chain = createProcessors( context );
84
        final var inputDoc = read( inputPath, concat );
85
        final var outputDoc = chain.apply( inputDoc );
86
87
        // Processors can export binary files. In such cases, processors will
88
        // return null to prevent further processing.
89
        final var result =
90
          outputDoc == null ? null : writeString( outputPath, outputDoc );
91
92
        future.complete( outputPath );
93
        return result;
94
      } catch( final Throwable ex ) {
95
        future.completeExceptionally( ex );
96
        return null;
97
      }
98
    };
99
100
    // Prevent the application from blocking while the processor executes.
101
    sExecutor.submit( callableTask );
102
  }
103
104
  /**
105
   * Concatenates all the files in the same directory as the given file into
106
   * a string. The extension is determined by the given file name pattern; the
107
   * order files are concatenated is based on their numeric sort order (this
108
   * avoids lexicographic sorting).
109
   * <p>
110
   * If the parent path to the file being edited in the text editor cannot
111
   * be found then this will return the editor's text, without iterating through
112
   * the parent directory. (Should never happen, but who knows?)
113
   * </p>
114
   * <p>
115
   * New lines are automatically appended to separate each file.
116
   * </p>
117
   *
118
   * @param inputPath The path to the source file to read.
119
   * @param concat    {@code true} to concatenate all files with the same
120
   *                  extension as the source path.
121
   * @return All files in the same directory as the file being edited
122
   * concatenated into a single string.
123
   */
124
  private static String read( final Path inputPath, final boolean concat )
125
    throws IOException {
126
    final var parent = inputPath.getParent();
127
    final var filename = inputPath.getFileName().toString();
128
    final var extension = getExtension( filename );
129
130
    // Short-circuit because: only one file was requested; there is no parent
131
    // directory to scan for files; or there's no extension for globbing.
132
    if( !concat || parent == null || extension.isBlank() ) {
133
      return readString( inputPath );
134
    }
135
136
    final var glob = "**/*." + extension;
137
    final var files = new ArrayList<Path>();
138
    walk( parent, glob, files::add );
139
    files.sort( new AlphanumComparator<>() );
140
141
    final var text = new StringBuilder( DOCUMENT_LENGTH );
142
    final var eol = lineSeparator();
143
144
    for( final var file : files ) {
145
      text.append( readString( file ) );
146
      text.append( eol );
147
    }
148
149
    return text.toString();
150
  }
151
}
1152
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
15
/**
16
 * Responsible for loading the bootstrap.properties file, which is
17
 * tactically located outside the standard resource reverse domain name
18
 * namespace to avoid hard-coding the application name in many places.
19
 * Instead, the application name is located in the bootstrap file, which is
20
 * then used to look up the remaining settings.
21
 * <p>
22
 * See {@link Constants#PATH_PROPERTIES_SETTINGS} for details.
23
 * </p>
24
 */
25
public final class Bootstrap {
26
  private static final String PATH_BOOTSTRAP = "/bootstrap.properties";
27
28
  /**
29
   * Must be populated before deriving the app title (order matters).
30
   */
31
  private static final Properties sP = new Properties();
32
33
  public static String APP_TITLE;
34
  public static String APP_VERSION;
35
  public static String CONTAINER_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
    try( final var in = openResource( PATH_BOOTSTRAP ) ) {
47
      sP.load( in );
48
49
      APP_TITLE = sP.getProperty( "application.title" );
50
      CONTAINER_VERSION = sP.getProperty( "container.version" );
51
    } catch( final Exception ex ) {
52
      APP_TITLE = "KeenWrite";
53
54
      // Bootstrap properties cannot be found, use a default value.
55
      final var fmt = "Unable to load %s resource, applying defaults.%n";
56
      clue( ex, fmt, PATH_BOOTSTRAP );
57
58
      // There's no way to know what container version is compatible. This
59
      // value will cause a failure when downloading the container,
60
      CONTAINER_VERSION = "1.0.0";
61
    }
62
63
    APP_TITLE_LOWERCASE = APP_TITLE.toLowerCase();
64
65
    try {
66
      APP_VERSION = Launcher.getVersion();
67
    } catch( final Exception ex ) {
68
      APP_VERSION = "0.0.0";
69
70
      // Application version cannot be found, use a default value.
71
      final var fmt = "Unable to determine application version.";
72
      clue( ex, fmt );
73
    }
74
75
    // The plug-in that requests the version from the repository tag will
76
    // add a "dirty" number and indicator suffix. Removing it allows the
77
    // "clean" version to be used to pull a corresponding typesetter container.
78
    APP_VERSION_CLEAN = APP_VERSION.replaceAll( "-.*", "" );
79
    APP_YEAR = getYear();
80
81
    // This also sets the user agent for the SVG rendering library.
82
    System.setProperty( "http.agent", APP_TITLE + " " + APP_VERSION_CLEAN );
83
84
    USER_DATA_DIR = UserDataDir.getAppPath( APP_TITLE_LOWERCASE );
85
    USER_CACHE_DIR = USER_DATA_DIR.resolve( "cache" ).toFile();
86
87
    if( !USER_CACHE_DIR.exists() ) {
88
      final var ignored = USER_CACHE_DIR.mkdirs();
89
    }
90
  }
91
92
  @SuppressWarnings( "SameParameterValue" )
93
  private static InputStream openResource( final String path ) {
94
    return Constants.class.getResourceAsStream( path );
95
  }
96
97
  private static String getYear() {
98
    return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) );
99
  }
100
}
1101
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 java.lang.String.format;
11
import static org.apache.commons.io.FilenameUtils.removeExtension;
12
13
/**
14
 * Provides controls for processor behaviour when transforming input documents.
15
 */
16
public enum ExportFormat {
17
18
  /**
19
   * For HTML exports, encode TeX as SVG. Treat image links relatively.
20
   */
21
  HTML_TEX_SVG( ".html" ),
22
23
  /**
24
   * For HTML exports, encode TeX using {@code $} delimiters, suitable for
25
   * rendering by an external TeX typesetting engine (or online with KaTeX).
26
   * Treat image links relatively.
27
   */
28
  HTML_TEX_DELIMITED( ".html" ),
29
30
  /**
31
   * For XHTML exports, encode TeX using {@code $} delimiters.
32
   */
33
  XHTML_TEX( ".xml" ),
34
35
  /**
36
   * Exports as PDF file format.
37
   */
38
  APPLICATION_PDF( ".pdf" ),
39
40
  /**
41
   * Indicates no special export format is to be created. No extension is
42
   * applicable. Image links must use absolute directories.
43
   */
44
  NONE( "" );
45
46
  /**
47
   * Preferred file name extension for the given file type.
48
   */
49
  private final String mExtension;
50
51
  /**
52
   * Looks up the {@link ExportFormat} based on the given path and subtype.
53
   *
54
   * @param path     The type to find.
55
   * @param modifier The subtype to find (for HTML).
56
   * @return An object to control the output file format.
57
   * @throws IllegalArgumentException The type/subtype could not be found.
58
   */
59
  public static ExportFormat valueFrom( final Path path, final String modifier )
60
    throws IllegalArgumentException {
61
    assert path != null;
62
63
    return valueFrom( MediaType.valueFrom( path ), modifier );
64
  }
65
66
  /**
67
   * Looks up the {@link ExportFormat} based on the given path and subtype.
68
   *
69
   * @param extension The type to find.
70
   * @param modifier  The subtype to find (for HTML).
71
   * @return An object to control the output file format.
72
   * @throws IllegalArgumentException The type/subtype could not be found.
73
   */
74
  public static ExportFormat valueFrom(
75
    final String extension, final String modifier )
76
    throws IllegalArgumentException {
77
    assert extension != null;
78
79
    return valueFrom( MediaTypeExtension.fromExtension( extension ), modifier );
80
  }
81
82
  /**
83
   * Looks up the {@link ExportFormat} based on the given path and subtype.
84
   *
85
   * @param type     The media type to find.
86
   * @param modifier The subtype to find (for HTML).
87
   * @return An object to control the output file format.
88
   * @throws IllegalArgumentException The type/subtype could not be found.
89
   */
90
  public static ExportFormat valueFrom(
91
    final MediaType type, final String modifier ) {
92
    return switch( type ) {
93
      case TEXT_HTML, TEXT_XHTML -> "svg".equalsIgnoreCase( modifier.trim() )
94
        ? HTML_TEX_SVG
95
        : HTML_TEX_DELIMITED;
96
      case APP_PDF -> APPLICATION_PDF;
97
      default -> throw new IllegalArgumentException( format(
98
        "Unrecognized format type and subtype: '%s' and '%s'", type, modifier
99
      ) );
100
    };
101
  }
102
103
  ExportFormat( final String extension ) {
104
    mExtension = extension;
105
  }
106
107
  /**
108
   * Returns the given {@link File} with its extension replaced by one that
109
   * matches this {@link ExportFormat} extension.
110
   *
111
   * @param file The file to perform an extension swap.
112
   * @return The given file with its extension replaced.
113
   */
114
  public File toExportFilename( final File file ) {
115
    return new File( removeExtension( file.getName() ) + mExtension );
116
  }
117
118
  /**
119
   * Delegates to {@link #toExportFilename(File)} after converting the given
120
   * {@link Path} to an instance of {@link File}.
121
   *
122
   * @param path The {@link Path} to convert to a {@link File}.
123
   * @return The given path with its extension replaced.
124
   */
125
  public File toExportFilename( final Path path ) {
126
    return toExportFilename( path.toFile() );
127
  }
128
}
1129
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.application.Platform;
38
import javafx.beans.property.*;
39
import javafx.collections.ListChangeListener;
40
import javafx.concurrent.Task;
41
import javafx.event.ActionEvent;
42
import javafx.event.Event;
43
import javafx.event.EventHandler;
44
import javafx.scene.Node;
45
import javafx.scene.Scene;
46
import javafx.scene.control.SplitPane;
47
import javafx.scene.control.Tab;
48
import javafx.scene.control.TabPane;
49
import javafx.scene.control.Tooltip;
50
import javafx.scene.control.TreeItem.TreeModificationEvent;
51
import javafx.scene.input.KeyEvent;
52
import javafx.stage.Stage;
53
import javafx.stage.Window;
54
import org.greenrobot.eventbus.Subscribe;
55
56
import java.io.File;
57
import java.io.FileNotFoundException;
58
import java.nio.file.Path;
59
import java.util.*;
60
import java.util.concurrent.ExecutorService;
61
import java.util.concurrent.ScheduledExecutorService;
62
import java.util.concurrent.ScheduledFuture;
63
import java.util.concurrent.atomic.AtomicBoolean;
64
import java.util.concurrent.atomic.AtomicReference;
65
import java.util.function.Consumer;
66
import java.util.function.Function;
67
import java.util.stream.Collectors;
68
69
import static com.keenwrite.ExportFormat.NONE;
70
import static com.keenwrite.Launcher.terminate;
71
import static com.keenwrite.Messages.get;
72
import static com.keenwrite.constants.Constants.*;
73
import static com.keenwrite.events.Bus.register;
74
import static com.keenwrite.events.StatusEvent.clue;
75
import static com.keenwrite.io.MediaType.*;
76
import static com.keenwrite.io.MediaType.TypeName.TEXT;
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.runLater;
88
import static javafx.scene.control.ButtonType.NO;
89
import static javafx.scene.control.ButtonType.YES;
90
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
91
import static javafx.scene.input.KeyCode.ENTER;
92
import static javafx.scene.input.KeyCode.SPACE;
93
import static javafx.scene.input.KeyCombination.ALT_DOWN;
94
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
95
import static javafx.util.Duration.millis;
96
import static javax.swing.SwingUtilities.invokeLater;
97
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
98
99
/**
100
 * Responsible for wiring together the main application components for a
101
 * particular {@link Workspace} (project). These include the definition views,
102
 * text editors, and preview pane along with any corresponding controllers.
103
 */
104
public final class MainPane extends SplitPane {
105
106
  private static final ExecutorService sExecutor = newFixedThreadPool( 1 );
107
  private static final Notifier sNotifier = Services.load( Notifier.class );
108
109
  /**
110
   * Used when opening files to determine how each file should be binned and
111
   * therefore what tab pane to be opened within.
112
   */
113
  private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of(
114
    TEXT_MARKDOWN, TEXT_R_MARKDOWN, UNDEFINED
115
  );
116
117
  private final ScheduledExecutorService mSaver = newScheduledThreadPool( 1 );
118
  private final AtomicReference<ScheduledFuture<?>> mSaveTask =
119
    new AtomicReference<>();
120
121
  /**
122
   * Prevents re-instantiation of processing classes.
123
   */
124
  private final Map<TextResource, Processor<String>> mProcessors =
125
    new HashMap<>();
126
127
  private final Workspace mWorkspace;
128
129
  /**
130
   * Groups similar file type tabs together.
131
   */
132
  private final List<TabPane> mTabPanes = new ArrayList<>();
133
134
  /**
135
   * Renders the actively selected plain text editor tab.
136
   */
137
  private final HtmlPreview mPreview;
138
139
  /**
140
   * Provides an interactive document outline.
141
   */
142
  private final DocumentOutline mOutline = new DocumentOutline();
143
144
  /**
145
   * Changing the active editor fires the value changed event. This allows
146
   * refreshes to happen when external definitions are modified and need to
147
   * trigger the processing chain.
148
   */
149
  private final ObjectProperty<TextEditor> mTextEditor =
150
    createActiveTextEditor();
151
152
  /**
153
   * Changing the active definition editor fires the value changed event. This
154
   * allows refreshes to happen when external definitions are modified and need
155
   * to trigger the processing chain.
156
   */
157
  private final ObjectProperty<TextDefinition> mDefinitionEditor;
158
159
  private final ObjectProperty<SpellChecker> mSpellChecker;
160
161
  private final TextEditorSpellChecker mEditorSpeller;
162
163
  /**
164
   * Called when the definition data is changed.
165
   */
166
  private final EventHandler<TreeModificationEvent<Event>> mTreeHandler =
167
    event -> {
168
      process( getTextEditor() );
169
      save( getTextDefinition() );
170
    };
171
172
  /**
173
   * Tracks the number of detached tab panels opened into their own windows,
174
   * which allows unique identification of subordinate windows by their title.
175
   * It is doubtful more than 128 windows, much less 256, will be created.
176
   */
177
  private byte mWindowCount;
178
179
  private final VariableNameInjector mVariableNameInjector;
180
181
  private final RBootstrapController mRBootstrapController;
182
183
  private final DocumentStatistics mStatistics;
184
185
  @SuppressWarnings( {"FieldCanBeLocal", "unused"} )
186
  private final TypesetterInstaller mInstallWizard;
187
188
  /**
189
   * Adds all content panels to the main user interface. This will load the
190
   * configuration settings from the workspace to reproduce the settings from
191
   * a previous session.
192
   */
193
  public MainPane( final Workspace workspace ) {
194
    mWorkspace = workspace;
195
    mSpellChecker = createSpellChecker();
196
    mEditorSpeller = createTextEditorSpellChecker( mSpellChecker );
197
    mPreview = new HtmlPreview( workspace );
198
    mStatistics = new DocumentStatistics( workspace );
199
    mTextEditor.set( createMarkdownEditor( DOCUMENT_DEFAULT ) );
200
    mDefinitionEditor = createActiveDefinitionEditor( mTextEditor );
201
    mVariableNameInjector = new VariableNameInjector( mWorkspace );
202
    mRBootstrapController = new RBootstrapController(
203
      mWorkspace, this::getDefinitions );
204
205
    open( collect( getRecentFiles() ) );
206
    viewPreview();
207
    setDividerPositions( calculateDividerPositions() );
208
209
    // Once the main scene's window regains focus, update the active definition
210
    // editor to the currently selected tab.
211
    runLater( () -> getWindow().setOnCloseRequest( event -> {
212
      // Order matters: Open file names must be persisted before closing all.
213
      mWorkspace.save();
214
215
      if( closeAll() ) {
216
        Platform.exit();
217
        terminate( 0 );
218
      }
219
220
      event.consume();
221
    } ) );
222
223
    register( this );
224
    initAutosave( workspace );
225
226
    restoreSession();
227
    runLater( this::restoreFocus );
228
229
    mInstallWizard = new TypesetterInstaller( workspace );
230
  }
231
232
  /**
233
   * Called when spellchecking can be run. This will reload the dictionary
234
   * into memory once, and then re-use it for all the existing text editors.
235
   *
236
   * @param event The event to process, having a populated word-frequency map.
237
   */
238
  @Subscribe
239
  public void handle( final LexiconLoadedEvent event ) {
240
    final var lexicon = event.getLexicon();
241
242
    try {
243
      final var checker = SymSpellSpeller.forLexicon( lexicon );
244
      mSpellChecker.set( checker );
245
    } catch( final Exception ex ) {
246
      clue( ex );
247
    }
248
  }
249
250
  @Subscribe
251
  public void handle( final TextEditorFocusEvent event ) {
252
    mTextEditor.set( event.get() );
253
  }
254
255
  @Subscribe
256
  public void handle( final TextDefinitionFocusEvent event ) {
257
    mDefinitionEditor.set( event.get() );
258
  }
259
260
  /**
261
   * Typically called when a file name is clicked in the preview panel.
262
   *
263
   * @param event The event to process, must contain a valid file reference.
264
   */
265
  @Subscribe
266
  public void handle( final FileOpenEvent event ) {
267
    final File eventFile;
268
    final var eventUri = event.getUri();
269
270
    if( eventUri.isAbsolute() ) {
271
      eventFile = new File( eventUri.getPath() );
272
    }
273
    else {
274
      final var activeFile = getTextEditor().getFile();
275
      final var parent = activeFile.getParentFile();
276
277
      if( parent == null ) {
278
        clue( new FileNotFoundException( eventUri.getPath() ) );
279
        return;
280
      }
281
      else {
282
        final var parentPath = parent.getAbsolutePath();
283
        eventFile = Path.of( parentPath, eventUri.getPath() ).toFile();
284
      }
285
    }
286
287
    final var mediaType = MediaTypeExtension.fromFile( eventFile );
288
289
    runLater( () -> {
290
      // Open text files locally.
291
      if( mediaType.isType( TEXT ) ) {
292
        open( eventFile );
293
      }
294
      else {
295
        try {
296
          // Delegate opening all other file types to the operating system.
297
          getDesktop().open( eventFile );
298
        } catch( final Exception ex ) {
299
          clue( ex );
300
        }
301
      }
302
    } );
303
  }
304
305
  @Subscribe
306
  public void handle( final CaretNavigationEvent event ) {
307
    runLater( () -> {
308
      final var textArea = getTextEditor();
309
      textArea.moveTo( event.getOffset() );
310
      textArea.requestFocus();
311
    } );
312
  }
313
314
  @Subscribe
315
  public void handle( final InsertDefinitionEvent<String> event ) {
316
    final var leaf = event.getLeaf();
317
    final var editor = mTextEditor.get();
318
319
    mVariableNameInjector.insert( editor, leaf );
320
  }
321
322
  private void initAutosave( final Workspace workspace ) {
323
    final var rate = workspace.integerProperty( KEY_EDITOR_AUTOSAVE );
324
325
    rate.addListener(
326
      ( c, o, n ) -> {
327
        final var taskRef = mSaveTask.get();
328
329
        // Prevent multiple autosaves from running.
330
        if( taskRef != null ) {
331
          taskRef.cancel( false );
332
        }
333
334
        initAutosave( rate );
335
      }
336
    );
337
338
    // Start the save listener (avoids duplicating some code).
339
    initAutosave( rate );
340
  }
341
342
  private void initAutosave( final IntegerProperty rate ) {
343
    mSaveTask.set(
344
      mSaver.scheduleAtFixedRate(
345
        () -> {
346
          if( getTextEditor().isModified() ) {
347
            // Ensure the modified indicator is cleared by running on EDT.
348
            runLater( this::save );
349
          }
350
        }, 0, rate.intValue(), SECONDS
351
      )
352
    );
353
  }
354
355
  /**
356
   * TODO: Load divider positions from exported settings, see
357
   *   {@link #collect(SetProperty)} comment.
358
   */
359
  private double[] calculateDividerPositions() {
360
    final var ratio = 100f / getItems().size() / 100;
361
    final var positions = getDividerPositions();
362
363
    for( int i = 0; i < positions.length; i++ ) {
364
      positions[ i ] = ratio * i;
365
    }
366
367
    return positions;
368
  }
369
370
  /**
371
   * Opens all the files into the application, provided the paths are unique.
372
   * This may only be called for any type of files that a user can edit
373
   * (i.e., update and persist), such as definitions and text files.
374
   *
375
   * @param files The list of files to open.
376
   */
377
  public void open( final List<File> files ) {
378
    files.forEach( this::open );
379
  }
380
381
  /**
382
   * This opens the given file. Since the preview pane is not a file that
383
   * can be opened, it is safe to add a listener to the detachable pane.
384
   * This will exit early if the given file is not a regular file (i.e., a
385
   * directory).
386
   *
387
   * @param inputFile The file to open.
388
   */
389
  private void open( final File inputFile ) {
390
    // Prevent opening directories (a non-existent "untitled.md" is fine).
391
    if( !inputFile.isFile() && inputFile.exists() ) {
392
      return;
393
    }
394
395
    final var tab = createTab( inputFile );
396
    final var node = tab.getContent();
397
    final var mediaType = MediaType.valueFrom( inputFile );
398
    final var tabPane = obtainTabPane( mediaType );
399
400
    tab.setTooltip( createTooltip( inputFile ) );
401
    tabPane.setFocusTraversable( false );
402
    tabPane.setTabClosingPolicy( ALL_TABS );
403
    tabPane.getTabs().add( tab );
404
405
    // Attach the tab scene factory for new tab panes.
406
    if( !getItems().contains( tabPane ) ) {
407
      addTabPane(
408
        node instanceof TextDefinition ? 0 : getItems().size(), tabPane
409
      );
410
    }
411
412
    if( inputFile.isFile() ) {
413
      getRecentFiles().add( inputFile.getAbsolutePath() );
414
    }
415
  }
416
417
  /**
418
   * Gives focus to the most recently edited document and attempts to move
419
   * the caret to the most recently known offset into said document.
420
   */
421
  private void restoreSession() {
422
    final var workspace = getWorkspace();
423
    final var file = workspace.fileProperty( KEY_UI_RECENT_DOCUMENT );
424
    final var offset = workspace.integerProperty( KEY_UI_RECENT_OFFSET );
425
426
    for( final var pane : mTabPanes ) {
427
      for( final var tab : pane.getTabs() ) {
428
        final var tooltip = tab.getTooltip();
429
430
        if( tooltip != null ) {
431
          final var tabName = tooltip.getText();
432
          final var fileName = file.getValue().toString();
433
434
          if( tabName.equalsIgnoreCase( fileName ) ) {
435
            final var node = tab.getContent();
436
437
            pane.getSelectionModel().select( tab );
438
            node.requestFocus();
439
440
            if( node instanceof TextEditor editor ) {
441
              editor.moveTo( offset.getValue() );
442
            }
443
444
            break;
445
          }
446
        }
447
      }
448
    }
449
  }
450
451
  /**
452
   * Sets the focus to the middle pane, which contains the text editor tabs.
453
   */
454
  private void restoreFocus() {
455
    // Work around a bug where focusing directly on the middle pane results
456
    // in the R engine not loading variables properly.
457
    mTabPanes.get( 0 ).requestFocus();
458
459
    // This is the only line that should be required.
460
    mTabPanes.get( 1 ).requestFocus();
461
  }
462
463
  /**
464
   * Opens a new text editor document using the default document file name.
465
   */
466
  public void newTextEditor() {
467
    open( DOCUMENT_DEFAULT );
468
  }
469
470
  /**
471
   * Opens a new definition editor document using the default definition
472
   * file name.
473
   */
474
  public void newDefinitionEditor() {
475
    open( DEFINITION_DEFAULT );
476
  }
477
478
  /**
479
   * Iterates over all tab panes to find all {@link TextEditor}s and request
480
   * that they save themselves.
481
   */
482
  public void saveAll() {
483
    iterateEditors( this::save );
484
  }
485
486
  /**
487
   * Requests that the active {@link TextEditor} saves itself. Don't bother
488
   * checking if modified first because if the user swaps external media from
489
   * an external source (e.g., USB thumb drive), save should not second-guess
490
   * the user: save always re-saves. Also, it's less code.
491
   */
492
  public void save() {
493
    save( getTextEditor() );
494
  }
495
496
  /**
497
   * Saves the active {@link TextEditor} under a new name.
498
   *
499
   * @param files The new active editor {@link File} reference, must contain
500
   *              at least one element.
501
   */
502
  public void saveAs( final List<File> files ) {
503
    assert files != null;
504
    assert !files.isEmpty();
505
    final var editor = getTextEditor();
506
    final var tab = getTab( editor );
507
    final var file = files.get( 0 );
508
509
    // If the file type has changed, refresh the processors.
510
    final var mediaType = MediaType.valueFrom( file );
511
    final var typeChanged = !editor.isMediaType( mediaType );
512
513
    if( typeChanged ) {
514
      removeProcessor( editor );
515
    }
516
517
    editor.rename( file );
518
    tab.ifPresent( t -> {
519
      t.setText( editor.getFilename() );
520
      t.setTooltip( createTooltip( file ) );
521
    } );
522
523
    if( typeChanged ) {
524
      updateProcessors( editor );
525
      process( editor );
526
    }
527
528
    save();
529
  }
530
531
  /**
532
   * Saves the given {@link TextResource} to a file. This is typically used
533
   * to save either an instance of {@link TextEditor} or {@link TextDefinition}.
534
   *
535
   * @param resource The resource to export.
536
   */
537
  private void save( final TextResource resource ) {
538
    try {
539
      resource.save();
540
    } catch( final Exception ex ) {
541
      clue( ex );
542
      sNotifier.alert(
543
        getWindow(), resource.getPath(), "TextResource.saveFailed", ex
544
      );
545
    }
546
  }
547
548
  /**
549
   * Closes all open {@link TextEditor}s; all {@link TextDefinition}s stay open.
550
   *
551
   * @return {@code true} when all editors, modified or otherwise, were
552
   * permitted to close; {@code false} when one or more editors were modified
553
   * and the user requested no closing.
554
   */
555
  public boolean closeAll() {
556
    var closable = true;
557
558
    for( final var tabPane : mTabPanes ) {
559
      final var tabIterator = tabPane.getTabs().iterator();
560
561
      while( tabIterator.hasNext() ) {
562
        final var tab = tabIterator.next();
563
        final var resource = tab.getContent();
564
565
        // The definition panes auto-save, so being specific here prevents
566
        // closing the definitions in the situation where the user wants to
567
        // continue editing (i.e., possibly save unsaved work).
568
        if( !(resource instanceof TextEditor) ) {
569
          continue;
570
        }
571
572
        if( canClose( (TextEditor) resource ) ) {
573
          tabIterator.remove();
574
          close( tab );
575
        }
576
        else {
577
          closable = false;
578
        }
579
      }
580
    }
581
582
    return closable;
583
  }
584
585
  /**
586
   * Calls the tab's {@link Tab#getOnClosed()} handler to carry out a close
587
   * event.
588
   *
589
   * @param tab The {@link Tab} that was closed.
590
   */
591
  private void close( final Tab tab ) {
592
    assert tab != null;
593
594
    final var handler = tab.getOnClosed();
595
596
    if( handler != null ) {
597
      handler.handle( new ActionEvent() );
598
    }
599
  }
600
601
  /**
602
   * Closes the active tab; delegates to {@link #canClose(TextResource)}.
603
   */
604
  public void close() {
605
    final var editor = getTextEditor();
606
607
    if( canClose( editor ) ) {
608
      close( editor );
609
    }
610
  }
611
612
  /**
613
   * Closes the given {@link TextResource}. This must not be called from within
614
   * a loop that iterates over the tab panes using {@code forEach}, lest a
615
   * concurrent modification exception be thrown.
616
   *
617
   * @param resource The {@link TextResource} to close, without confirming with
618
   *                 the user.
619
   */
620
  private void close( final TextResource resource ) {
621
    getTab( resource ).ifPresent(
622
      tab -> {
623
        close( tab );
624
        tab.getTabPane().getTabs().remove( tab );
625
      }
626
    );
627
  }
628
629
  /**
630
   * Answers whether the given {@link TextResource} may be closed.
631
   *
632
   * @param editor The {@link TextResource} to try closing.
633
   * @return {@code true} when the editor may be closed; {@code false} when
634
   * the user has requested to keep the editor open.
635
   */
636
  private boolean canClose( final TextResource editor ) {
637
    final var editorTab = getTab( editor );
638
    final var canClose = new AtomicBoolean( true );
639
640
    if( editor.isModified() ) {
641
      final var filename = new StringBuilder();
642
      editorTab.ifPresent( tab -> filename.append( tab.getText() ) );
643
644
      final var message = sNotifier.createNotification(
645
        Messages.get( "Alert.file.close.title" ),
646
        Messages.get( "Alert.file.close.text" ),
647
        filename.toString()
648
      );
649
650
      final var dialog = sNotifier.createConfirmation( getWindow(), message );
651
652
      dialog.showAndWait().ifPresent(
653
        save -> canClose.set( save == YES ? editor.save() : save == NO )
654
      );
655
    }
656
657
    return canClose.get();
658
  }
659
660
  private void iterateEditors( final Consumer<TextEditor> consumer ) {
661
    mTabPanes.forEach(
662
      tp -> tp.getTabs().forEach( tab -> {
663
        final var node = tab.getContent();
664
665
        if( node instanceof final TextEditor editor ) {
666
          consumer.accept( editor );
667
        }
668
      } )
669
    );
670
  }
671
672
  private ObjectProperty<TextEditor> createActiveTextEditor() {
673
    final var editor = new SimpleObjectProperty<TextEditor>();
674
675
    editor.addListener( ( c, o, n ) -> {
676
      if( n != null ) {
677
        mPreview.setBaseUri( n.getPath() );
678
        process( n );
679
      }
680
    } );
681
682
    return editor;
683
  }
684
685
  /**
686
   * Adds the HTML preview tab to its own, singular tab pane.
687
   */
688
  public void viewPreview() {
689
    viewTab( mPreview, TEXT_HTML, "Pane.preview.title" );
690
  }
691
692
  /**
693
   * Adds the document outline tab to its own, singular tab pane.
694
   */
695
  public void viewOutline() {
696
    viewTab( mOutline, APP_DOCUMENT_OUTLINE, "Pane.outline.title" );
697
  }
698
699
  public void viewStatistics() {
700
    viewTab( mStatistics, APP_DOCUMENT_STATISTICS, "Pane.statistics.title" );
701
  }
702
703
  public void viewFiles() {
704
    try {
705
      final var factory = new FilePickerFactory( getWorkspace() );
706
      final var fileManager = factory.createModeless();
707
      viewTab( fileManager, APP_FILE_MANAGER, "Pane.files.title" );
708
    } catch( final Exception ex ) {
709
      clue( ex );
710
    }
711
  }
712
713
  private void viewTab(
714
    final Node node, final MediaType mediaType, final String key ) {
715
    final var tabPane = obtainTabPane( mediaType );
716
717
    for( final var tab : tabPane.getTabs() ) {
718
      if( tab.getContent() == node ) {
719
        return;
720
      }
721
    }
722
723
    tabPane.getTabs().add( createTab( get( key ), node ) );
724
    addTabPane( tabPane );
725
  }
726
727
  public void viewRefresh() {
728
    mPreview.refresh();
729
    Engine.clear();
730
    mRBootstrapController.update();
731
  }
732
733
  /**
734
   * Returns the tab that contains the given {@link TextEditor}.
735
   *
736
   * @param editor The {@link TextEditor} instance to find amongst the tabs.
737
   * @return The first tab having content that matches the given tab.
738
   */
739
  private Optional<Tab> getTab( final TextResource editor ) {
740
    return mTabPanes.stream()
741
                    .flatMap( pane -> pane.getTabs().stream() )
742
                    .filter( tab -> editor.equals( tab.getContent() ) )
743
                    .findFirst();
744
  }
745
746
  /**
747
   * Creates a new {@link DefinitionEditor} wrapped in a listener that
748
   * is used to detect when the active {@link DefinitionEditor} has changed.
749
   * Upon changing, the variables are interpolated and the active text editor
750
   * is refreshed.
751
   *
752
   * @param textEditor Text editor to update with the revised resolved map.
753
   * @return A newly configured property that represents the active
754
   * {@link DefinitionEditor}, never null.
755
   */
756
  private ObjectProperty<TextDefinition> createActiveDefinitionEditor(
757
    final ObjectProperty<TextEditor> textEditor ) {
758
    final var defEditor = new SimpleObjectProperty<>(
759
      createDefinitionEditor()
760
    );
761
762
    defEditor.addListener( ( c, o, n ) -> {
763
      final var editor = textEditor.get();
764
765
      if( editor.isMediaType( TEXT_R_MARKDOWN ) ) {
766
        // Initialize R before the editor is added.
767
        mRBootstrapController.update();
768
      }
769
770
      process( editor );
771
    } );
772
773
    return defEditor;
774
  }
775
776
  private Tab createTab( final String filename, final Node node ) {
777
    return new DetachableTab( filename, node );
778
  }
779
780
  private Tab createTab( final File file ) {
781
    final var r = createTextResource( file );
782
    final var tab = createTab( r.getFilename(), r.getNode() );
783
784
    r.modifiedProperty().addListener(
785
      ( c, o, n ) -> tab.setText( r.getFilename() + (n ? "*" : "") )
786
    );
787
788
    // This is called when either the tab is closed by the user clicking on
789
    // the tab's close icon or when closing (all) from the file menu.
790
    tab.setOnClosed(
791
      __ -> getRecentFiles().remove( file.getAbsolutePath() )
792
    );
793
794
    // When closing a tab, give focus to the newly revealed tab.
795
    tab.selectedProperty().addListener( ( c, o, n ) -> {
796
      if( n != null && n ) {
797
        final var pane = tab.getTabPane();
798
799
        if( pane != null ) {
800
          pane.requestFocus();
801
        }
802
      }
803
    } );
804
805
    tab.tabPaneProperty().addListener( ( cPane, oPane, nPane ) -> {
806
      if( nPane != null ) {
807
        nPane.focusedProperty().addListener( ( c, o, n ) -> {
808
          if( n != null && n ) {
809
            final var selected = nPane.getSelectionModel().getSelectedItem();
810
            final var node = selected.getContent();
811
            node.requestFocus();
812
          }
813
        } );
814
      }
815
    } );
816
817
    return tab;
818
  }
819
820
  /**
821
   * Creates bins for the different {@link MediaType}s, which eventually are
822
   * added to the UI as separate tab panes. If ever a general-purpose scene
823
   * exporter is developed to serialize a scene to an FXML file, this could
824
   * be replaced by such a class.
825
   * <p>
826
   * When binning the files, this makes sure that at least one file exists
827
   * for every type. If the user has opted to close a particular type (such
828
   * as the definition pane), the view will suppressed elsewhere.
829
   * </p>
830
   * <p>
831
   * The order that the binned files are returned will be reflected in the
832
   * order that the corresponding panes are rendered in the UI.
833
   * </p>
834
   *
835
   * @param paths The file paths to bin according to their type.
836
   * @return An in-order list of files, first by structured definition files,
837
   * then by plain text documents.
838
   */
839
  private List<File> collect( final SetProperty<String> paths ) {
840
    // Treat all files destined for the text editor as plain text documents
841
    // so that they are added to the same pane. Grouping by TEXT_PLAIN is a
842
    // bit arbitrary, but means explicitly capturing TEXT_PLAIN isn't needed.
843
    final Function<MediaType, MediaType> bin =
844
      m -> PLAIN_TEXT_FORMAT.contains( m ) ? TEXT_PLAIN : m;
845
846
    // Create two groups: YAML files and plain text files. The order that
847
    // the elements are listed in the enumeration for media types determines
848
    // what files are loaded first. Variable definitions come before all other
849
    // plain text documents.
850
    final var bins = paths
851
      .stream()
852
      .collect(
853
        groupingBy(
854
          path -> bin.apply( MediaType.fromFilename( path ) ),
855
          () -> new TreeMap<>( Enum::compareTo ),
856
          Collectors.toList()
857
        )
858
      );
859
860
    bins.putIfAbsent( TEXT_YAML, List.of( DEFINITION_DEFAULT.toString() ) );
861
    bins.putIfAbsent( TEXT_PLAIN, List.of( DOCUMENT_DEFAULT.toString() ) );
862
863
    final var result = new LinkedList<File>();
864
865
    // Ensure that the same types are listed together (keep insertion order).
866
    bins.forEach( ( mediaType, files ) -> result.addAll(
867
      files.stream().map( File::new ).toList() )
868
    );
869
870
    return result;
871
  }
872
873
  /**
874
   * Force the active editor to update, which will cause the processor
875
   * to re-evaluate the interpolated definition map thereby updating the
876
   * preview pane.
877
   *
878
   * @param editor Contains the source document to update in the preview pane.
879
   */
880
  private void process( final TextEditor editor ) {
881
    // Ensure processing does not run on the JavaFX thread, which frees the
882
    // text editor immediately for caret movement. The preview will have a
883
    // slight delay when catching up to the caret position.
884
    final var task = new Task<Void>() {
885
      @Override
886
      public Void call() {
887
        try {
888
          final var p = mProcessors.getOrDefault( editor, IDENTITY );
889
          p.apply( editor == null ? "" : editor.getText() );
890
        } catch( final Exception ex ) {
891
          clue( ex );
892
        }
893
894
        return null;
895
      }
896
    };
897
898
    // TODO: Each time the editor successfully runs the processor the task is
899
    //   considered successful. Due to the rapid-fire nature of processing
900
    //   (e.g., keyboard navigation, fast typing), it isn't necessary to
901
    //   scroll each time.
902
    //   The algorithm:
903
    //   1. Peek at the oldest time.
904
    //   2. If the difference between the oldest time and current time exceeds
905
    //      250 milliseconds, then invoke the scrolling.
906
    //   3. Insert the current time into the circular queue.
907
    task.setOnSucceeded(
908
      e -> invokeLater( () -> mPreview.scrollTo( CARET_ID ) )
909
    );
910
911
    // Prevents multiple process requests from executing simultaneously (due
912
    // to having a restricted queue size).
913
    sExecutor.execute( task );
914
  }
915
916
  /**
917
   * Lazily creates a {@link TabPane} configured to listen for tab select
918
   * events. The tab pane is associated with a given media type so that
919
   * similar files can be grouped together.
920
   *
921
   * @param mediaType The media type to associate with the tab pane.
922
   * @return An instance of {@link TabPane} that will handle tab docking.
923
   */
924
  private TabPane obtainTabPane( final MediaType mediaType ) {
925
    for( final var pane : mTabPanes ) {
926
      for( final var tab : pane.getTabs() ) {
927
        final var node = tab.getContent();
928
929
        if( node instanceof TextResource r && r.supports( mediaType ) ) {
930
          return pane;
931
        }
932
      }
933
    }
934
935
    final var pane = createTabPane();
936
    mTabPanes.add( pane );
937
    return pane;
938
  }
939
940
  /**
941
   * Creates an initialized {@link TabPane} instance.
942
   *
943
   * @return A new {@link TabPane} with all listeners configured.
944
   */
945
  private TabPane createTabPane() {
946
    final var tabPane = new DetachableTabPane();
947
948
    initStageOwnerFactory( tabPane );
949
    initTabListener( tabPane );
950
951
    return tabPane;
952
  }
953
954
  /**
955
   * When any {@link DetachableTabPane} is detached from the main window,
956
   * the stage owner factory must be given its parent window, which will
957
   * own the child window. The parent window is the {@link MainPane}'s
958
   * {@link Scene}'s {@link Window} instance.
959
   *
960
   * <p>
961
   * This will derives the new title from the main window title, incrementing
962
   * the window count to help uniquely identify the child windows.
963
   * </p>
964
   *
965
   * @param tabPane A new {@link DetachableTabPane} to configure.
966
   */
967
  private void initStageOwnerFactory( final DetachableTabPane tabPane ) {
968
    tabPane.setStageOwnerFactory( stage -> {
969
      final var title = get(
970
        "Detach.tab.title",
971
        ((Stage) getWindow()).getTitle(), ++mWindowCount
972
      );
973
      stage.setTitle( title );
974
975
      return getScene().getWindow();
976
    } );
977
  }
978
979
  /**
980
   * Responsible for configuring the content of each {@link DetachableTab} when
981
   * it is added to the given {@link DetachableTabPane} instance.
982
   * <p>
983
   * For {@link TextEditor} contents, an instance of {@link ScrollEventHandler}
984
   * is initialized to perform synchronized scrolling between the editor and
985
   * its preview window. Additionally, the last tab in the tab pane's list of
986
   * tabs is given focus.
987
   * </p>
988
   * <p>
989
   * Note that multiple tabs can be added simultaneously.
990
   * </p>
991
   *
992
   * @param tabPane A new {@link TabPane} to configure.
993
   */
994
  private void initTabListener( final TabPane tabPane ) {
995
    tabPane.getTabs().addListener(
996
      ( final ListChangeListener.Change<? extends Tab> listener ) -> {
997
        while( listener.next() ) {
998
          if( listener.wasAdded() ) {
999
            final var tabs = listener.getAddedSubList();
1000
1001
            tabs.forEach( tab -> {
1002
              final var node = tab.getContent();
1003
1004
              if( node instanceof TextEditor ) {
1005
                initScrollEventListener( tab );
1006
              }
1007
            } );
1008
1009
            // Select and give focus to the last tab opened.
1010
            final var index = tabs.size() - 1;
1011
            if( index >= 0 ) {
1012
              final var tab = tabs.get( index );
1013
              tabPane.getSelectionModel().select( tab );
1014
              tab.getContent().requestFocus();
1015
            }
1016
          }
1017
        }
1018
      }
1019
    );
1020
  }
1021
1022
  /**
1023
   * Synchronizes scrollbar positions between the given {@link Tab} that
1024
   * contains an instance of {@link TextEditor} and {@link HtmlPreview} pane.
1025
   *
1026
   * @param tab The container for an instance of {@link TextEditor}.
1027
   */
1028
  private void initScrollEventListener( final Tab tab ) {
1029
    final var editor = (TextEditor) tab.getContent();
1030
    final var scrollPane = editor.getScrollPane();
1031
    final var scrollBar = mPreview.getVerticalScrollBar();
1032
    final var handler = new ScrollEventHandler( scrollPane, scrollBar );
1033
1034
    handler.enabledProperty().bind( tab.selectedProperty() );
1035
  }
1036
1037
  private void addTabPane( final int index, final TabPane tabPane ) {
1038
    final var items = getItems();
1039
1040
    if( !items.contains( tabPane ) ) {
1041
      items.add( index, tabPane );
1042
    }
1043
  }
1044
1045
  private void addTabPane( final TabPane tabPane ) {
1046
    addTabPane( getItems().size(), tabPane );
1047
  }
1048
1049
  private GenericBuilder<Mutator, ProcessorContext> processorContextBuilder() {
1050
    final var w = getWorkspace();
1051
1052
    return builder()
1053
      .with( Mutator::setDefinitions, this::getDefinitions )
1054
      .with( Mutator::setLocale, w::getLocale )
1055
      .with( Mutator::setMetadata, w::getMetadata )
1056
      .with( Mutator::setThemesPath, w::getThemesPath )
1057
      .with( Mutator::setCachesPath,
1058
             () -> w.getFile( KEY_CACHES_DIR ) )
1059
      .with( Mutator::setImagesPath,
1060
             () -> w.getFile( KEY_IMAGES_DIR ) )
1061
      .with( Mutator::setImageOrder,
1062
             () -> w.getString( KEY_IMAGES_ORDER ) )
1063
      .with( Mutator::setImageServer,
1064
             () -> w.getString( KEY_IMAGES_SERVER ) )
1065
      .with( Mutator::setFontsPath,
1066
             () -> w.getFile( KEY_TYPESET_CONTEXT_FONTS_DIR ) )
1067
      .with( Mutator::setCaret,
1068
             () -> getTextEditor().getCaret() )
1069
      .with( Mutator::setSigilBegan,
1070
             () -> w.getString( KEY_DEF_DELIM_BEGAN ) )
1071
      .with( Mutator::setSigilEnded,
1072
             () -> w.getString( KEY_DEF_DELIM_ENDED ) )
1073
      .with( Mutator::setRScript,
1074
             () -> w.getString( KEY_R_SCRIPT ) )
1075
      .with( Mutator::setRWorkingDir,
1076
             () -> w.getFile( KEY_R_DIR ).toPath() )
1077
      .with( Mutator::setCurlQuotes,
1078
             () -> w.getBoolean( KEY_TYPESET_TYPOGRAPHY_QUOTES ) )
1079
      .with( Mutator::setAutoRemove,
1080
             () -> w.getBoolean( KEY_TYPESET_CONTEXT_CLEAN ) );
1081
  }
1082
1083
  public ProcessorContext createProcessorContext() {
1084
    return createProcessorContext( null, NONE );
1085
  }
1086
1087
  /**
1088
   * @param targetPath Used when exporting to a PDF file (binary).
1089
   * @param format     Used when processors export to a new text format.
1090
   * @return A new {@link ProcessorContext} to use when creating an instance of
1091
   * {@link Processor}.
1092
   */
1093
  public ProcessorContext createProcessorContext(
1094
    final Path targetPath, final ExportFormat format ) {
1095
    final var textEditor = getTextEditor();
1096
    final var sourcePath = textEditor.getPath();
1097
1098
    return processorContextBuilder()
1099
      .with( Mutator::setSourcePath, sourcePath )
1100
      .with( Mutator::setTargetPath, targetPath )
1101
      .with( Mutator::setExportFormat, format )
1102
      .build();
1103
  }
1104
1105
  /**
1106
   * @param sourcePath Used by {@link ProcessorFactory} to determine
1107
   *                   {@link Processor} type to create based on file type.
1108
   * @return A new {@link ProcessorContext} to use when creating an instance of
1109
   * {@link Processor}.
1110
   */
1111
  private ProcessorContext createProcessorContext( final Path sourcePath ) {
1112
    return processorContextBuilder()
1113
      .with( Mutator::setSourcePath, sourcePath )
1114
      .with( Mutator::setExportFormat, NONE )
1115
      .build();
1116
  }
1117
1118
  private TextResource createTextResource( final File file ) {
1119
    // TODO: Create PlainTextEditor that's returned by default.
1120
    return MediaType.valueFrom( file ) == TEXT_YAML
1121
      ? createDefinitionEditor( file )
1122
      : createMarkdownEditor( file );
1123
  }
1124
1125
  /**
1126
   * Creates an instance of {@link MarkdownEditor} that listens for both
1127
   * caret change events and text change events. Text change events must
1128
   * take priority over caret change events because it's possible to change
1129
   * the text without moving the caret (e.g., delete selected text).
1130
   *
1131
   * @param inputFile The file containing contents for the text editor.
1132
   * @return A non-null text editor.
1133
   */
1134
  private MarkdownEditor createMarkdownEditor( final File inputFile ) {
1135
    final var editor = new MarkdownEditor( inputFile, getWorkspace() );
1136
1137
    updateProcessors( editor );
1138
1139
    // Listener for editor modifications or caret position changes.
1140
    editor.addDirtyListener( ( c, o, n ) -> {
1141
      if( n ) {
1142
        // Reset the status bar after changing the text.
1143
        clue();
1144
1145
        // Processing the text may update the status bar.
1146
        process( getTextEditor() );
1147
1148
        // Update the caret position in the status bar.
1149
        CaretMovedEvent.fire( editor.getCaret() );
1150
      }
1151
    } );
1152
1153
    editor.addEventListener(
1154
      keyPressed( SPACE, CONTROL_DOWN ), this::autoinsert
1155
    );
1156
1157
    editor.addEventListener(
1158
      keyPressed( ENTER, ALT_DOWN ), event -> mEditorSpeller.autofix( editor )
1159
    );
1160
1161
    final var textArea = editor.getTextArea();
1162
1163
    // Spell check when the paragraph changes.
1164
    textArea
1165
      .plainTextChanges()
1166
      .filter( p -> !p.isIdentity() )
1167
      .subscribe( change -> mEditorSpeller.checkParagraph( textArea, change ) );
1168
1169
    // Store the caret position to restore it after restarting the application.
1170
    textArea.caretPositionProperty().addListener(
1171
      ( c, o, n ) ->
1172
        getWorkspace().integerProperty( KEY_UI_RECENT_OFFSET ).setValue( n )
1173
    );
1174
1175
    // Set the active editor, which refreshes the preview panel.
1176
    mTextEditor.set( editor );
1177
1178
    // Check the entire document after the spellchecker is initialized (with
1179
    // a valid lexicon) so that only the current paragraph need be scanned
1180
    // while editing. (Technically, only the most recently modified word must
1181
    // be scanned.)
1182
    mSpellChecker.addListener(
1183
      ( c, o, n ) -> runLater(
1184
        () -> iterateEditors( mEditorSpeller::checkDocument )
1185
      )
1186
    );
1187
1188
    // Check the entire document after it has been loaded.
1189
    mEditorSpeller.checkDocument( mTextEditor.get() );
1190
1191
    return editor;
1192
  }
1193
1194
  /**
1195
   * Creates a processor for an editor, provided one doesn't already exist.
1196
   *
1197
   * @param editor The editor that potentially requires an associated processor.
1198
   */
1199
  private void updateProcessors( final TextEditor editor ) {
1200
    final var path = editor.getFile().toPath();
1201
1202
    mProcessors.computeIfAbsent(
1203
      editor, p -> createProcessors(
1204
        createProcessorContext( path ),
1205
        createHtmlPreviewProcessor()
1206
      )
1207
    );
1208
  }
1209
1210
  /**
1211
   * Removes a processor for an editor. This is required because a file may
1212
   * change type while editing (e.g., from plain Markdown to R Markdown).
1213
   * In the case that an editor's type changes, its associated processor must
1214
   * be changed accordingly.
1215
   *
1216
   * @param editor The editor that potentially requires an associated processor.
1217
   */
1218
  private void removeProcessor( final TextEditor editor ) {
1219
    mProcessors.remove( editor );
1220
  }
1221
1222
  /**
1223
   * Creates a {@link Processor} capable of rendering an HTML document onto
1224
   * a GUI widget.
1225
   *
1226
   * @return The {@link Processor} for rendering an HTML document.
1227
   */
1228
  private Processor<String> createHtmlPreviewProcessor() {
1229
    return new HtmlPreviewProcessor( getPreview() );
1230
  }
1231
1232
  /**
1233
   * Creates a spellchecker that accepts all words as correct. This allows
1234
   * the spellchecker property to be initialized to a known valid value.
1235
   *
1236
   * @return A wrapped {@link PermissiveSpeller}.
1237
   */
1238
  private ObjectProperty<SpellChecker> createSpellChecker() {
1239
    return new SimpleObjectProperty<>( new PermissiveSpeller() );
1240
  }
1241
1242
  private TextEditorSpellChecker createTextEditorSpellChecker(
1243
    final ObjectProperty<SpellChecker> spellChecker ) {
1244
    return new TextEditorSpellChecker( spellChecker );
1245
  }
1246
1247
  /**
1248
   * Delegates to {@link #autoinsert()}.
1249
   *
1250
   * @param keyEvent Ignored.
1251
   */
1252
  private void autoinsert( final KeyEvent keyEvent ) {
1253
    autoinsert();
1254
  }
1255
1256
  /**
1257
   * Finds a node that matches the word at the caret, then inserts the
1258
   * corresponding definition. The definition token delimiters depend on
1259
   * the type of file being edited.
1260
   */
1261
  public void autoinsert() {
1262
    mVariableNameInjector.autoinsert( getTextEditor(), getTextDefinition() );
1263
  }
1264
1265
  private TextDefinition createDefinitionEditor() {
1266
    return createDefinitionEditor( DEFINITION_DEFAULT );
1267
  }
1268
1269
  private TextDefinition createDefinitionEditor( final File file ) {
1270
    final var editor = new DefinitionEditor( file, createTreeTransformer() );
1271
1272
    editor.addTreeChangeHandler( mTreeHandler );
1273
1274
    return editor;
1275
  }
1276
1277
  private TreeTransformer createTreeTransformer() {
1278
    return new YamlTreeTransformer();
1279
  }
1280
1281
  private Tooltip createTooltip( final File file ) {
1282
    final var path = file.toPath();
1283
    final var tooltip = new Tooltip( path.toString() );
1284
1285
    tooltip.setShowDelay( millis( 200 ) );
1286
1287
    return tooltip;
1288
  }
1289
1290
  public HtmlPreview getPreview() {
1291
    return mPreview;
1292
  }
1293
1294
  /**
1295
   * Returns the active text editor.
1296
   *
1297
   * @return The text editor that currently has focus.
1298
   */
1299
  public TextEditor getTextEditor() {
1300
    return mTextEditor.get();
1301
  }
1302
1303
  /**
1304
   * Returns the active text editor property.
1305
   *
1306
   * @return The property container for the active text editor.
1307
   */
1308
  public ReadOnlyObjectProperty<TextEditor> textEditorProperty() {
1309
    return mTextEditor;
1310
  }
1311
1312
  /**
1313
   * Returns the active text definition editor.
1314
   *
1315
   * @return The property container for the active definition editor.
1316
   */
1317
  public TextDefinition getTextDefinition() {
1318
    return mDefinitionEditor.get();
1319
  }
1320
1321
  /**
1322
   * Returns the active variable definitions, without any interpolation.
1323
   * Interpolation is a responsibility of {@link Processor} instances.
1324
   *
1325
   * @return The key-value pairs, not interpolated.
1326
   */
1327
  private Map<String, String> getDefinitions() {
1328
    return getTextDefinition().getDefinitions();
1329
  }
1330
1331
  public Window getWindow() {
1332
    return getScene().getWindow();
1333
  }
1334
1335
  public Workspace getWorkspace() {
1336
    return mWorkspace;
1337
  }
1338
1339
  /**
1340
   * Returns the set of file names opened in the application. The names must
1341
   * be converted to {@link File} objects.
1342
   *
1343
   * @return A {@link Set} of file names.
1344
   */
1345
  private <E> SetProperty<E> getRecentFiles() {
1346
    return getWorkspace().setsProperty( KEY_UI_RECENT_OPEN_PATH );
1347
  }
1348
}
11349
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.*;
29
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
30
import static javafx.scene.input.KeyEvent.KEY_RELEASED;
31
32
/**
33
 * Responsible for creating the bar scene: menu bar, toolbar, and status bar.
34
 */
35
public final class MainScene {
36
37
  private final Scene mScene;
38
  private final MenuBar mMenuBar;
39
  private final Node mToolBar;
40
  private final StatusBar mStatusBar;
41
  private final FileWatchService mFileWatchService = new FileWatchService();
42
  private FileModifiedListener mStylesheetFileListener = event -> {};
43
44
  public MainScene( final Workspace workspace ) {
45
    final var mainPane = createMainPane( workspace );
46
    final var actions = createApplicationActions( mainPane );
47
    final var caretStatus = createCaretStatus();
48
49
    mMenuBar = setManagedLayout( createMenuBar( actions ) );
50
    mToolBar = setManagedLayout( createToolBar() );
51
    mStatusBar = setManagedLayout( createStatusBar() );
52
53
    mStatusBar.getRightItems().add( caretStatus );
54
55
    final var appPane = new BorderPane();
56
    appPane.setTop( new VBox( mMenuBar, mToolBar ) );
57
    appPane.setCenter( mainPane );
58
    appPane.setBottom( mStatusBar );
59
60
    final var fileWatcher = new Thread( mFileWatchService );
61
    fileWatcher.setDaemon( true );
62
    fileWatcher.start();
63
64
    mScene = createScene( appPane );
65
    initStylesheets( mScene, workspace );
66
  }
67
68
  /**
69
   * Called by the {@link MainApp} to get a handle on the {@link Scene}
70
   * created by an instance of {@link MainScene}.
71
   *
72
   * @return The {@link Scene} created at construction time.
73
   */
74
  public Scene getScene() {
75
    return mScene;
76
  }
77
78
  public void toggleMenuBar() {
79
    final var node = mMenuBar;
80
    node.setVisible( !node.isVisible() );
81
  }
82
83
  public void toggleToolBar() {
84
    final var node = mToolBar;
85
    node.setVisible( !node.isVisible() );
86
  }
87
88
  public void toggleStatusBar() {
89
    final var node = mStatusBar;
90
    node.setVisible( !node.isVisible() );
91
  }
92
93
  MenuBar getMenuBar() {
94
    return mMenuBar;
95
  }
96
97
  public StatusBar getStatusBar() {return mStatusBar;}
98
99
  private void initStylesheets( final Scene scene, final Workspace workspace ) {
100
    final var internal = workspace.skinProperty( KEY_UI_SKIN_SELECTION );
101
    final var external = workspace.fileProperty( KEY_UI_SKIN_CUSTOM );
102
    final var inSkin = internal.get();
103
    final var exSkin = external.get();
104
    applyStylesheets( scene, inSkin, exSkin );
105
106
    internal.addListener(
107
      ( c, o, n ) -> {
108
        if( n != null ) {
109
          applyStylesheets( scene, n, exSkin );
110
        }
111
      }
112
    );
113
114
    external.addListener(
115
      ( c, o, n ) -> {
116
        if( o != null ) {
117
          mFileWatchService.unregister( o );
118
        }
119
120
        if( n != null ) {
121
          try {
122
            applyStylesheets( scene, inSkin, n );
123
          } catch( final Exception ex ) {
124
            // Changes to the CSS file won't autoload, which is okay.
125
            clue( ex );
126
          }
127
        }
128
      }
129
    );
130
131
    mFileWatchService.removeListener( mStylesheetFileListener );
132
    mStylesheetFileListener = event ->
133
      runLater( () -> applyStylesheets( scene, inSkin, event.getFile() ) );
134
    mFileWatchService.addListener( mStylesheetFileListener );
135
  }
136
137
  private String getStylesheet( final String filename ) {
138
    return MessageFormat.format( STYLESHEET_APPLICATION_SKIN, filename );
139
  }
140
141
  /**
142
   * Clears then re-applies all the internal stylesheets.
143
   *
144
   * @param scene    The scene to stylize.
145
   * @param internal The CSS file name bundled with the application.
146
   * @param external The (optional) customized CSS file specified by the user.
147
   */
148
  private void applyStylesheets(
149
    final Scene scene, final String internal, final File external ) {
150
    final var stylesheets = scene.getStylesheets();
151
    stylesheets.clear();
152
    stylesheets.add( STYLESHEET_APPLICATION_BASE );
153
    stylesheets.add( STYLESHEET_MARKDOWN );
154
    stylesheets.add( getStylesheet( toFilename( internal ) ) );
155
156
    try {
157
      if( external != null && external.canRead() && !external.isDirectory() ) {
158
        stylesheets.add( external.toURI().toURL().toString() );
159
        mFileWatchService.register( external );
160
      }
161
    } catch( final Exception ex ) {
162
      clue( ex );
163
    }
164
  }
165
166
  private MainPane createMainPane( final Workspace workspace ) {
167
    return new MainPane( workspace );
168
  }
169
170
  private GuiCommands createApplicationActions( final MainPane mainPane ) {
171
    return new GuiCommands( this, mainPane );
172
  }
173
174
  /**
175
   * Creates the class responsible for updating the UI with the caret position
176
   * based on the active text editor.
177
   *
178
   * @return The {@link CaretStatus} responsible for updating the
179
   * {@link StatusBar} whenever the caret changes position.
180
   */
181
  private CaretStatus createCaretStatus() {
182
    return new CaretStatus();
183
  }
184
185
  /**
186
   * Creates a new scene that is attached to the given {@link Parent}.
187
   *
188
   * @param parent The container for the scene.
189
   * @return A scene to capture user interactions, UI styles, etc.
190
   */
191
  private Scene createScene( final Parent parent ) {
192
    final var scene = new Scene( parent );
193
194
    // After the app loses focus, when the user switches back using Alt+Tab,
195
    // the menu is sometimes engaged. See MainApp::initStage().
196
    //
197
    // JavaFX Bug: https://bugs.openjdk.java.net/browse/JDK-8090647
198
    scene.addEventHandler( KEY_PRESSED, event -> {
199
      // Only consume lone ALT key press events. If the modifier is used in
200
      // combination with another key, don't consume the event. First check
201
      // if ALT is down before getting the key code as a micro-optimization.
202
      if( event.isAltDown() ) {
203
        if( event.getCode() == ALT || event.getCode() == ALT_GRAPH ) {
204
          event.consume();
205
        }
206
      }
207
    } );
208
209
    // Update the synchronized scrolling status when user presses scroll lock.
210
    scene.addEventHandler( KEY_RELEASED, event -> {
211
      if( event.getCode() == SCROLL_LOCK ) {
212
        fireScrollLockEvent();
213
      }
214
    } );
215
216
    return scene;
217
  }
218
219
  /**
220
   * Binds the visible property of the node to the managed property so that
221
   * hiding the node also removes the screen real estate that it occupies.
222
   * This allows the user to hide the menu bar, toolbar, etc.
223
   *
224
   * @param node The node to have its real estate bound to visibility.
225
   * @return The given node for fluent-like convenience.
226
   */
227
  private <T extends Node> T setManagedLayout( final T node ) {
228
    node.managedProperty().bind( node.visibleProperty() );
229
    return node;
230
  }
231
}
1232
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
package com.keenwrite.cmdline;
2
3
import com.fasterxml.jackson.databind.JsonNode;
4
import com.fasterxml.jackson.databind.ObjectMapper;
5
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
6
import com.keenwrite.ExportFormat;
7
import com.keenwrite.processors.ProcessorContext;
8
import com.keenwrite.processors.ProcessorContext.Mutator;
9
import picocli.CommandLine;
10
11
import java.io.File;
12
import java.io.IOException;
13
import java.nio.file.Files;
14
import java.nio.file.Path;
15
import java.util.HashMap;
16
import java.util.Locale;
17
import java.util.Map;
18
import java.util.Map.Entry;
19
import java.util.concurrent.Callable;
20
import java.util.function.Consumer;
21
22
import static com.keenwrite.constants.Constants.DIAGRAM_SERVER_NAME;
23
24
/**
25
 * Responsible for mapping command-line arguments to keys that are used by
26
 * the application.
27
 */
28
@CommandLine.Command(
29
  name = "KeenWrite",
30
  mixinStandardHelpOptions = true,
31
  description = "Plain text editor for editing with variables"
32
)
33
@SuppressWarnings( "unused" )
34
public final class Arguments implements Callable<Integer> {
35
  @CommandLine.Option(
36
    names = {"--all"},
37
    description =
38
      "Concatenate files before processing (${DEFAULT-VALUE})",
39
    defaultValue = "false"
40
  )
41
  private boolean mConcatenate;
42
43
  @CommandLine.Option(
44
    names = {"--keep-files"},
45
    description =
46
      "Retain temporary build files (${DEFAULT-VALUE})",
47
    defaultValue = "false"
48
  )
49
  private boolean mKeepFiles;
50
51
  @CommandLine.Option(
52
    names = {"--curl-quotes"},
53
    description =
54
      "Replace straight quotes with curly quotes (${DEFAULT-VALUE})",
55
    defaultValue = "true"
56
  )
57
  private Boolean mCurlQuotes;
58
59
  @CommandLine.Option(
60
    names = {"-d", "--debug"},
61
    description =
62
      "Enable logging to the console (${DEFAULT-VALUE})",
63
    paramLabel = "Boolean",
64
    defaultValue = "false"
65
  )
66
  private boolean mDebug;
67
68
  @CommandLine.Option(
69
    names = {"-i", "--input"},
70
    description =
71
      "Source document file path",
72
    paramLabel = "PATH",
73
    defaultValue = "stdin",
74
    required = true
75
  )
76
  private Path mSourcePath;
77
78
  @CommandLine.Option(
79
    names = {"--font-dir"},
80
    description =
81
      "Directory to specify additional fonts",
82
    paramLabel = "String"
83
  )
84
  private File mFontDir;
85
86
  @CommandLine.Option(
87
    names = {"--format-subtype"},
88
    description =
89
      "Export TeX subtype for HTML formats: svg, delimited",
90
    paramLabel = "String",
91
    defaultValue = "svg"
92
  )
93
  private String mFormatSubtype;
94
95
  @CommandLine.Option(
96
    names = {"--cache-dir"},
97
    description =
98
      "Directory to store remote resources",
99
    paramLabel = "DIR"
100
  )
101
  private File mCachesDir;
102
103
  @CommandLine.Option(
104
    names = {"--image-dir"},
105
    description =
106
      "Directory containing images",
107
    paramLabel = "DIR"
108
  )
109
  private File mImagesDir;
110
111
  @CommandLine.Option(
112
    names = {"--image-order"},
113
    description =
114
      "Comma-separated image order (${DEFAULT-VALUE})",
115
    paramLabel = "String",
116
    defaultValue = "svg,pdf,png,jpg,tiff"
117
  )
118
  private String mImageOrder;
119
120
  @CommandLine.Option(
121
    names = {"--image-server"},
122
    description =
123
      "SVG diagram rendering service (${DEFAULT-VALUE})",
124
    paramLabel = "String",
125
    defaultValue = DIAGRAM_SERVER_NAME
126
  )
127
  private String mImageServer;
128
129
  @CommandLine.Option(
130
    names = {"--locale"},
131
    description =
132
      "Set localization (${DEFAULT-VALUE})",
133
    paramLabel = "String",
134
    defaultValue = "en"
135
  )
136
  private String mLocale;
137
138
  @CommandLine.Option(
139
    names = {"-m", "--metadata"},
140
    description =
141
      "Map metadata keys to values, variable names allowed",
142
    paramLabel = "key=value"
143
  )
144
  private Map<String, String> mMetadata;
145
146
  @CommandLine.Option(
147
    names = {"-o", "--output"},
148
    description =
149
      "Destination document file path",
150
    paramLabel = "PATH",
151
    defaultValue = "stdout",
152
    required = true
153
  )
154
  private Path mTargetPath;
155
156
  @CommandLine.Option(
157
    names = {"-q", "--quiet"},
158
    description =
159
      "Suppress all status messages (${DEFAULT-VALUE})",
160
    defaultValue = "false"
161
  )
162
  private boolean mQuiet;
163
164
  @CommandLine.Option(
165
    names = {"--r-dir"},
166
    description =
167
      "R working directory",
168
    paramLabel = "DIR"
169
  )
170
  private Path mRWorkingDir;
171
172
  @CommandLine.Option(
173
    names = {"--r-script"},
174
    description =
175
      "R bootstrap script file path",
176
    paramLabel = "PATH"
177
  )
178
  private Path mRScriptPath;
179
180
  @CommandLine.Option(
181
    names = {"--sigil-opening"},
182
    description =
183
      "Starting sigil for variable names (${DEFAULT-VALUE})",
184
    paramLabel = "String",
185
    defaultValue = "{{"
186
  )
187
  private String mSigilBegan;
188
189
  @CommandLine.Option(
190
    names = {"--sigil-closing"},
191
    description =
192
      "Ending sigil for variable names (${DEFAULT-VALUE})",
193
    paramLabel = "String",
194
    defaultValue = "}}"
195
  )
196
  private String mSigilEnded;
197
198
  @CommandLine.Option(
199
    names = {"--theme-dir"},
200
    description =
201
      "Theme directory",
202
    paramLabel = "DIR"
203
  )
204
  private Path mThemesDir;
205
206
  @CommandLine.Option(
207
    names = {"-v", "--variables"},
208
    description =
209
      "Variables file path",
210
    paramLabel = "PATH"
211
  )
212
  private Path mPathVariables;
213
214
  private final Consumer<Arguments> mLauncher;
215
216
  public Arguments( final Consumer<Arguments> launcher ) {
217
    mLauncher = launcher;
218
  }
219
220
  public ProcessorContext createProcessorContext()
221
    throws IOException {
222
    final var definitions = parse( mPathVariables );
223
    final var format = ExportFormat.valueFrom( mTargetPath, mFormatSubtype );
224
    final var locale = lookupLocale( mLocale );
225
    final var rScript = read( mRScriptPath );
226
227
    return ProcessorContext
228
      .builder()
229
      .with( Mutator::setSourcePath, mSourcePath )
230
      .with( Mutator::setTargetPath, mTargetPath )
231
      .with( Mutator::setThemesPath, () -> mThemesDir )
232
      .with( Mutator::setCachesPath, () -> mCachesDir )
233
      .with( Mutator::setImagesPath, () -> mImagesDir )
234
      .with( Mutator::setImageServer, () -> mImageServer )
235
      .with( Mutator::setImageOrder, () -> mImageOrder )
236
      .with( Mutator::setFontsPath, () -> mFontDir )
237
      .with( Mutator::setExportFormat, format )
238
      .with( Mutator::setDefinitions, () -> definitions )
239
      .with( Mutator::setMetadata, () -> mMetadata )
240
      .with( Mutator::setLocale, () -> locale )
241
      .with( Mutator::setConcatenate, mConcatenate )
242
      .with( Mutator::setSigilBegan, () -> mSigilBegan )
243
      .with( Mutator::setSigilEnded, () -> mSigilEnded )
244
      .with( Mutator::setRWorkingDir, () -> mRWorkingDir )
245
      .with( Mutator::setRScript, () -> rScript )
246
      .with( Mutator::setCurlQuotes, () -> mCurlQuotes )
247
      .with( Mutator::setAutoRemove, () -> !mKeepFiles )
248
      .build();
249
  }
250
251
  public boolean quiet() {
252
    return mQuiet;
253
  }
254
255
  public boolean debug() {
256
    return mDebug;
257
  }
258
259
  /**
260
   * Launches the main application window. This is called when not running
261
   * in headless mode.
262
   *
263
   * @return {@code 0}
264
   * @throws Exception The application encountered an unrecoverable error.
265
   */
266
  @Override
267
  public Integer call() throws Exception {
268
    mLauncher.accept( this );
269
    return 0;
270
  }
271
272
  private static String read( final Path path ) throws IOException {
273
    return path == null ? "" : Files.readString( path );
274
  }
275
276
  /**
277
   * Parses the given YAML document into a map of key-value pairs.
278
   *
279
   * @param vars Variable definition file to read, may be {@code null} if no
280
   *             variables are specified.
281
   * @return A non-interpolated variable map, or an empty map.
282
   * @throws IOException Could not read the variable definition file
283
   */
284
  private static Map<String, String> parse( final Path vars )
285
    throws IOException {
286
    final var map = new HashMap<String, String>();
287
288
    if( vars != null ) {
289
      final var yaml = read( vars );
290
      final var factory = new YAMLFactory();
291
      final var json = new ObjectMapper( factory ).readTree( yaml );
292
293
      parse( json, "", map );
294
    }
295
296
    return map;
297
  }
298
299
  private static void parse(
300
    final JsonNode json, final String parent, final Map<String, String> map ) {
301
    assert json != null;
302
    assert parent != null;
303
    assert map != null;
304
305
    json.fields().forEachRemaining( node -> parse( node, parent, map ) );
306
  }
307
308
  private static void parse(
309
    final Entry<String, JsonNode> node,
310
    final String parent,
311
    final Map<String, String> map ) {
312
    assert node != null;
313
    assert parent != null;
314
    assert map != null;
315
316
    final var jsonNode = node.getValue();
317
    final var keyName = parent + "." + node.getKey();
318
319
    if( jsonNode.isValueNode() ) {
320
      // Trim the leading period, which is always present.
321
      map.put( keyName.substring( 1 ), node.getValue().asText() );
322
    }
323
    else if( jsonNode.isObject() ) {
324
      parse( jsonNode, keyName, map );
325
    }
326
  }
327
328
  private static Locale lookupLocale( final String locale ) {
329
    try {
330
      return Locale.forLanguageTag( locale );
331
    } catch( final Exception ex ) {
332
      return Locale.ENGLISH;
333
    }
334
  }
335
}
1336
A src/main/java/com/keenwrite/cmdline/ColourScheme.java
1
package com.keenwrite.cmdline;
2
3
import static picocli.CommandLine.Help.Ansi.Style.*;
4
import static picocli.CommandLine.Help.ColorScheme;
5
import static picocli.CommandLine.Help.ColorScheme.Builder;
6
7
/**
8
 * Responsible for creating the command-line parser's colour scheme.
9
 */
10
public class ColourScheme {
11
12
  /**
13
   * Creates a new color scheme for use with command-line parsing.
14
   *
15
   * @return The new color scheme to apply to the parsesr.
16
   */
17
  public static ColorScheme create() {
18
    return new Builder()
19
      .commands( bold )
20
      .options( fg_blue, bold )
21
      .parameters( fg_blue )
22
      .optionParams( italic )
23
      .errors( fg_red, bold )
24
      .stackTraces( italic )
25
      .build();
26
  }
27
}
128
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
9
/**
10
 * Responsible for running the application in headless mode.
11
 */
12
public class HeadlessApp {
13
14
  /**
15
   * Contains directives that control text file processing.
16
   */
17
  private final Arguments mArgs;
18
19
  /**
20
   * Creates a new command-line version of the application.
21
   *
22
   * @param args The post-processed command-line arguments.
23
   */
24
  public HeadlessApp( final Arguments args ) {
25
    assert args != null;
26
27
    mArgs = args;
28
29
    register( this );
30
    AppCommands.run( mArgs );
31
  }
32
33
  /**
34
   * When a status message is shown, write it to the console, if not in
35
   * quiet mode.
36
   *
37
   * @param event The event published when the status changes.
38
   */
39
  @Subscribe
40
  public void handle( final StatusEvent event ) {
41
    if( !mArgs.quiet() ) {
42
      System.out.println( event );
43
    }
44
  }
45
46
  /**
47
   * Entry point for running the application in headless mode.
48
   *
49
   * @param args The parsed command-line arguments.
50
   */
51
  public static void main( final Arguments args ) {
52
    new HeadlessApp( args );
53
  }
54
}
155
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 java.util.*;
5
6
import static java.lang.Math.min;
7
8
/**
9
 * Responsible for maintaining a circular queue where newly added items will
10
 * overwrite existing items.
11
 * <p>
12
 * <strong>Warning:</strong> This class is not thread-safe.
13
 * </p>
14
 *
15
 * @param <E> The type of elements to store in this collection.
16
 */
17
@SuppressWarnings( "unchecked" )
18
public class CircularQueue<E>
19
  extends AbstractCollection<E> implements Queue<E> {
20
21
  /**
22
   * Simplifies the code by reusing an existing list implementation.
23
   * Initialized with {@code null} values at construction time.
24
   */
25
  private final Object[] mElements;
26
27
  /**
28
   * Maximum number of elements allowed in the collection before old elements
29
   * are overwritten. Set at construction time.
30
   */
31
  private final int mCapacity;
32
33
  /**
34
   * Insertion position when a new element is added. Starts at zero.
35
   */
36
  private int mProducer;
37
38
  /**
39
   * Retrieval position when the oldest element is removed. Starts at zero.
40
   */
41
  private int mConsumer;
42
43
  /**
44
   * The number of elements in the collection. This cannot delegate to the
45
   * {@link #mElements} list. Starts at zero.
46
   */
47
  private int mSize;
48
49
  /**
50
   * Creates a new circular queue that has a limited number of elements that
51
   * may be added before newly added elements will overwrite the oldest
52
   * elements that were added previously.
53
   * <p>
54
   * <strong>Warning:</strong> Client classes must take care not to exceed
55
   * memory limits imposed by the Java Virtual Machine.
56
   *
57
   * @param capacity Maximum number elements allowed in the list, must be
58
   *                 greater than one.
59
   */
60
  public CircularQueue( final int capacity ) {
61
    assert capacity > 1;
62
63
    mCapacity = capacity;
64
    mElements = new Object[ capacity ];
65
  }
66
67
  /**
68
   * Adds an element to the end of the collection. This overwrites the oldest
69
   * element in the collection when the queue is full. The number of elements,
70
   * reflected by the return value of {@link #size()} will not exceed the
71
   * capacity.
72
   *
73
   * @param element The item to insert into the collection, must not be
74
   *                {@code null}.
75
   * @return {@code true} Non-{@code null} items are always added.
76
   * @throws NullPointerException if the given element is {@code null}.
77
   *                              The iterator requires a consecutive
78
   *                              non-{@code null} range (no gaps).
79
   */
80
  @Override
81
  public boolean add( final E element ) {
82
    if( element == null ) {
83
      throw new NullPointerException();
84
    }
85
86
    mElements[ mProducer++ ] = element;
87
    mProducer %= mCapacity;
88
    mSize = min( mSize + 1, mCapacity );
89
90
    return true;
91
  }
92
93
  /**
94
   * Delegates to {@link #add(E)}.
95
   */
96
  @Override
97
  public boolean offer( final E element ) {
98
    return add( element );
99
  }
100
101
  /**
102
   * Removes the oldest element that was added to the collection.  The number
103
   * of elements reflected by the return value of {@link #size()} will not
104
   * drop below zero.
105
   *
106
   * @return The oldest element.
107
   * @throws NoSuchElementException The collection is empty.
108
   */
109
  @Override
110
  public E remove() {
111
    if( isEmpty() ) {
112
      throw new NoSuchElementException();
113
    }
114
115
    final E element = (E) mElements[ mConsumer ];
116
117
    mElements[ mConsumer++ ] = null;
118
    mConsumer %= mCapacity;
119
    mSize--;
120
121
    return element;
122
  }
123
124
  /**
125
   * Delegates to {@link #remove()}, but does not throw an exception.
126
   *
127
   * @return The oldest element.
128
   */
129
  @Override
130
  public E poll() {
131
    return isEmpty() ? null : remove();
132
  }
133
134
  /**
135
   * Returns the oldest element that was added to the collection.
136
   *
137
   * @return The oldest element.
138
   * @throws NoSuchElementException The collection is empty.
139
   */
140
  @Override
141
  public E element() {
142
    if( isEmpty() ) {
143
      throw new NoSuchElementException();
144
    }
145
146
    return (E) mElements[ mConsumer ];
147
  }
148
149
  /**
150
   * Delegates to {@link #element()}, but does not throw an exception.
151
   *
152
   * @return The oldest element.
153
   */
154
  @Override
155
  public E peek() {
156
    return isEmpty() ? null : element();
157
  }
158
159
  /**
160
   * Answers how many elements are currently in the collection.
161
   *
162
   * @return The number of elements that have been added to but not removed
163
   * from the collection.
164
   */
165
  @Override
166
  public int size() {
167
    return mSize;
168
  }
169
170
  /**
171
   * Returns a facility to visit each of the elements in the
172
   * {@link CircularQueue}. This will start iterating at the oldest element
173
   * and stop when there are no more elements.
174
   * <p>
175
   * The iterator is not thread-safe; concurrent modifications to the number
176
   * of elements in the {@link CircularQueue} will result in undefined
177
   * behaviour.
178
   *
179
   * @return A new {@link Iterator} instance capable of visiting each element.
180
   */
181
  @Override
182
  public Iterator<E> iterator() {
183
    return new Iterator<>() {
184
      private int mIndex = mConsumer;
185
      private boolean mFirst = true;
186
187
      @Override
188
      public boolean hasNext() {
189
        return (mFirst || mIndex != mConsumer) && mElements[ mIndex ] != null;
190
      }
191
192
      @Override
193
      public E next() {
194
        final var element = mElements[ mIndex++ ];
195
        mIndex %= mCapacity;
196
        mFirst = false;
197
198
        return (E) element;
199
      }
200
    };
201
  }
202
203
  @Override
204
  public String toString() {
205
    return Arrays.toString( mElements );
206
  }
207
}
1208
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.util.HashMap;
7
import java.util.Map;
8
import java.util.concurrent.ConcurrentHashMap;
9
10
/**
11
 * Responsible for interpolating key-value pairs in a map. That is, this will
12
 * iterate over all key-value pairs and replace keys wrapped in sigils
13
 * with corresponding definition value from the same map.
14
 */
15
public class InterpolatingMap extends ConcurrentHashMap<String, String> {
16
  private static final int GROUP_DELIMITED = 1;
17
18
  /**
19
   * Used to override the default initial capacity in {@link HashMap}.
20
   */
21
  private static final int INITIAL_CAPACITY = 1 << 8;
22
23
  private final SigilKeyOperator mOperator;
24
25
  /**
26
   * @param operator Contains the opening and closing sigils that mark
27
   *                 where variable names begin and end.
28
   */
29
  public InterpolatingMap( final SigilKeyOperator operator ) {
30
    super( INITIAL_CAPACITY );
31
32
    assert operator != null;
33
    mOperator = operator;
34
  }
35
36
  /**
37
   * @param operator Contains the opening and closing sigils that mark
38
   *                 where variable names begin and end.
39
   * @param m        The initial {@link Map} to copy into this instance.
40
   */
41
  public InterpolatingMap(
42
    final SigilKeyOperator operator, final Map<String, String> m ) {
43
    this( operator );
44
    putAll( m );
45
  }
46
47
  /**
48
   * Interpolates all values in the map that reference other values by way
49
   * of key names. Performs a non-greedy match of key names delimited by
50
   * definition tokens. This operation modifies the map directly.
51
   *
52
   * @return {@code this}
53
   */
54
  public InterpolatingMap interpolate() {
55
    for( final var k : keySet() ) {
56
      replace( k, interpolate( get( k ) ) );
57
    }
58
59
    return this;
60
  }
61
62
  /**
63
   * Given a value with zero or more key references, this will resolve all
64
   * the values, recursively. If a key cannot be de-referenced, the value will
65
   * contain the key name, including the original sigils.
66
   *
67
   * @param value    Value containing zero or more key references.
68
   * @return The given value with all embedded key references interpolated.
69
   */
70
  public String interpolate( String value ) {
71
    assert value != null;
72
73
    final var matcher = mOperator.match( value );
74
75
    while( matcher.find() ) {
76
      final var keyName = matcher.group( GROUP_DELIMITED );
77
      final var mapValue = get( keyName );
78
79
      if( mapValue != null ) {
80
        final var keyValue = interpolate( mapValue );
81
        value = value.replace( mOperator.apply( keyName ), keyValue );
82
      }
83
    }
84
85
    return value;
86
  }
87
}
188
A src/main/java/com/keenwrite/constants/Constants.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.constants;
3
4
import com.keenwrite.Bootstrap;
5
import com.keenwrite.Services;
6
import com.keenwrite.service.Settings;
7
8
import java.io.File;
9
import java.nio.charset.Charset;
10
import java.nio.file.Path;
11
import java.util.Locale;
12
13
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
14
import static com.keenwrite.Bootstrap.USER_DATA_DIR;
15
import static com.keenwrite.preferences.LocaleScripts.withScript;
16
import static java.io.File.separator;
17
import static java.lang.String.format;
18
import static java.lang.System.getProperty;
19
import static org.apache.commons.lang3.SystemUtils.*;
20
21
/**
22
 * Defines application-wide default values.
23
 */
24
public final class Constants {
25
26
  /**
27
   * Used by the default settings to load the {@link Settings} service. This
28
   * must come before any attempt is made to create a {@link Settings} object.
29
   * The reference to {@link Bootstrap#APP_TITLE_LOWERCASE} should cause the
30
   * JVM to load {@link Bootstrap} prior to proceeding. Loading that class
31
   * beforehand will read the bootstrap properties file to determine the
32
   * application name, which is then used to locate the settings properties.
33
   */
34
  public static final String PATH_PROPERTIES_SETTINGS =
35
    format( "/com/%s/settings.properties", APP_TITLE_LOWERCASE );
36
37
  /**
38
   * The {@link Settings} uses {@link #PATH_PROPERTIES_SETTINGS}.
39
   */
40
  public static final Settings sSettings = Services.load( Settings.class );
41
42
  public static final double WINDOW_X_DEFAULT = 0;
43
  public static final double WINDOW_Y_DEFAULT = 0;
44
  public static final double WINDOW_W_DEFAULT = 1200;
45
  public static final double WINDOW_H_DEFAULT = 800;
46
47
  public static final File DOCUMENT_DEFAULT = getFile( "document" );
48
  public static final int DOCUMENT_OFFSET = 0;
49
  public static final File DEFINITION_DEFAULT = getFile( "definition" );
50
  public static final File PDF_DEFAULT = getFile( "pdf" );
51
52
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
53
54
  public static final String STYLESHEET_APPLICATION_BASE =
55
    get( "file.stylesheet.application.base" );
56
  public static final String STYLESHEET_APPLICATION_SKIN =
57
    get( "file.stylesheet.application.skin" );
58
  public static final String STYLESHEET_MARKDOWN =
59
    get( "file.stylesheet.markdown" );
60
  public static final String STYLESHEET_MARKDOWN_LOCALE =
61
    "file.stylesheet.markdown.locale";
62
  public static final String STYLESHEET_PREVIEW =
63
    get( "file.stylesheet.preview" );
64
  public static final String STYLESHEET_PREVIEW_LOCALE =
65
    "file.stylesheet.preview.locale";
66
67
  public static final File FILE_PREFERENCES = getPreferencesFile();
68
69
  /**
70
   * Refer to file name extension settings in the configuration file. Do not
71
   * terminate with a period.
72
   */
73
  public static final String GLOB_PREFIX_FILE = "file.ext";
74
75
  /**
76
   * Three parameters: line number, column number, and offset.
77
   */
78
  public static final String STATUS_BAR_LINE = "Main.status.line";
79
80
  public static final String STATUS_BAR_OK = "Main.status.state.default";
81
82
  /**
83
   * Used to show an error while parsing, usually syntactical.
84
   */
85
  public static final String STATUS_PARSE_ERROR = "Main.status.error.parse";
86
  public static final String STATUS_DEFINITION_BLANK =
87
    "Main.status.error.def.blank";
88
  public static final String STATUS_DEFINITION_EMPTY =
89
    "Main.status.error.def.empty";
90
91
  /**
92
   * One parameter: the word under the cursor that could not be found.
93
   */
94
  public static final String STATUS_DEFINITION_MISSING =
95
    "Main.status.error.def.missing";
96
97
  /**
98
   * Default image extension order to use when scanning.
99
   */
100
  public static final String PERSIST_IMAGES_DEFAULT =
101
    get( "file.ext.image.order" );
102
103
  /**
104
   * Default working directory to use for R startup script.
105
   */
106
  public static final File USER_DIRECTORY =
107
    new File( System.getProperty( "user.dir" ) );
108
109
  public static final String NEWLINE = System.lineSeparator();
110
111
  /**
112
   * Default path to use for an untitled (pathless) file.
113
   */
114
  public static final Path DEFAULT_DIRECTORY = USER_DIRECTORY.toPath();
115
116
  /**
117
   * Default character set to use when reading/writing files.
118
   */
119
  public static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
120
121
  /**
122
   * Default starting delimiter for definition variables. This value must
123
   * not overlap math delimiters, so do not use $ tokens as the first
124
   * delimiter.
125
   */
126
  public static final String DEF_DELIM_BEGAN_DEFAULT = "{{";
127
128
  /**
129
   * Default ending delimiter for definition variables.
130
   */
131
  public static final String DEF_DELIM_ENDED_DEFAULT = "}}";
132
133
  /**
134
   * Default starting delimiter when inserting R variables.
135
   */
136
  public static final String R_DELIM_BEGAN_DEFAULT = "x( ";
137
138
  /**
139
   * Default ending delimiter when inserting R variables.
140
   */
141
  public static final String R_DELIM_ENDED_DEFAULT = " )";
142
143
  /**
144
   * Resource directory where different language lexicons are located.
145
   */
146
  public static final String LEXICONS_DIRECTORY = "lexicons";
147
148
  /**
149
   * Absolute location of true type font files within the Java archive file.
150
   */
151
  public static final String FONT_DIRECTORY = "/fonts";
152
153
  /**
154
   * Default text editor font name.
155
   */
156
  public static final String FONT_NAME_EDITOR_DEFAULT = "Noto Sans Regular";
157
158
  /**
159
   * Default text editor font size, in points.
160
   */
161
  public static final float FONT_SIZE_EDITOR_DEFAULT = 12f;
162
163
  /**
164
   * Default preview font name.
165
   */
166
  public static final String FONT_NAME_PREVIEW_DEFAULT = "Source Serif 4";
167
168
  /**
169
   * Default preview font size, in points.
170
   */
171
  public static final float FONT_SIZE_PREVIEW_DEFAULT = 13f;
172
173
  /**
174
   * Scaling factor for rendering mathematics.
175
   */
176
  public static final double FONT_SIZE_MATH_DEFAULT = 2;
177
178
  /**
179
   * Default monospace preview font name.
180
   */
181
  public static final String FONT_NAME_PREVIEW_MONO_NAME_DEFAULT =
182
    "Source Code Pro";
183
184
  /**
185
   * Default monospace preview font size, in points.
186
   */
187
  public static final float FONT_SIZE_PREVIEW_MONO_SIZE_DEFAULT = 13f;
188
189
  /**
190
   * Default locale for font loading, including ISO 15924 alpha-4 script code.
191
   */
192
  public static final Locale LOCALE_DEFAULT = withScript( Locale.getDefault() );
193
194
  /**
195
   * Default CSS to apply (resolves to a minimal implementation).
196
   */
197
  public static final String SKIN_DEFAULT = "Modena Light";
198
199
  /**
200
   * Custom JavaFX CSS to apply to user interface.
201
   */
202
  public static final File SKIN_CUSTOM_DEFAULT = null;
203
204
  /**
205
   * Custom HTML CSS to apply to HTML preview panel.
206
   */
207
  public static final File PREVIEW_CUSTOM_DEFAULT = null;
208
209
  /**
210
   * Default identifier to use for synchronized scrolling.
211
   */
212
  public static final String CARET_ID = "caret";
213
214
  /**
215
   * Default spacing for UI items (e.g., toolbars).
216
   */
217
  public static final int UI_CONTROL_SPACING = 10;
218
219
  /**
220
   * Default server name for rendering diagrams.
221
   */
222
  public static final String DIAGRAM_SERVER_NAME = "kroki.io";
223
224
  /**
225
   * Application action messages properties prefix.
226
   */
227
  public static final String ACTION_PREFIX = "Action.";
228
229
  /**
230
   * Restrict theme names when displaying.
231
   */
232
  public static final byte THEME_NAME_LENGTH = 30;
233
234
  /**
235
   * Prevent instantiation.
236
   */
237
  private Constants() {
238
  }
239
240
  /**
241
   * Converts from points to pixels because FlyingSaucer cannot handle points
242
   * properly. This is used to convert font sizes.
243
   *
244
   * @param points The points to convert to pixels.
245
   * @return The given number of points in equivalent pixels.
246
   */
247
  public static int toPixels( final double points ) {
248
    return (int) (points * (1 + 1 / 3f));
249
  }
250
251
  static String get( final String key ) {
252
    return sSettings.getSetting( key, "" );
253
  }
254
255
  /**
256
   * Returns a default {@link File} instance based on the given key suffix.
257
   *
258
   * @param suffix Appended to {@code "file.default."}.
259
   * @return A new {@link File} instance that references the settings file name.
260
   */
261
  private static File getFile( final String suffix ) {
262
    return new File( get( "file.default." + suffix ) );
263
  }
264
265
  /**
266
   * Returns the equivalent of {@code $HOME/.filename.xml}.
267
   */
268
  private static File getPreferencesFile() {
269
    return new File( format(
270
      "%s%s.%s.xml",
271
      getProperty( "user.home" ),
272
      separator,
273
      APP_TITLE_LOWERCASE
274
    ) );
275
  }
276
277
  /**
278
   * Tries to get a system-independent path to the user's fonts directory.
279
   */
280
  public static File getFontDirectory() {
281
    final var FONT_PATH = Path.of( "fonts" );
282
    final var USER_HOME = System.getProperty( "user.home" );
283
284
    final String fontBase;
285
    final Path fontUser;
286
287
    if( IS_OS_WINDOWS ) {
288
      fontBase = System.getenv( "WINDIR" );
289
      fontUser = FONT_PATH;
290
    }
291
    else if( IS_OS_MAC ) {
292
      fontBase = USER_HOME;
293
      fontUser = Path.of( "Library", "Fonts" );
294
    }
295
    else if( IS_OS_UNIX ) {
296
      fontBase = USER_HOME;
297
      fontUser = Path.of( ".fonts" );
298
    }
299
    else {
300
      fontBase = USER_DATA_DIR.toString();
301
      fontUser = FONT_PATH;
302
    }
303
304
    return (fontBase == null
305
      ? USER_DATA_DIR.relativize( fontUser )
306
      : Path.of( fontBase ).resolve( fontUser )).toFile();
307
  }
308
}
1309
A src/main/java/com/keenwrite/constants/GraphicsConstants.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.constants;
3
4
import javafx.scene.image.Image;
5
import javafx.scene.image.ImageView;
6
7
import java.util.ArrayList;
8
import java.util.List;
9
10
import static com.keenwrite.constants.Constants.get;
11
12
/**
13
 * Defines application-wide default values for GUI-related items. This helps
14
 * ensure that unit tests that have no graphical dependencies will pass.
15
 */
16
public class GraphicsConstants {
17
  public static final List<Image> LOGOS = createImages(
18
    "file.logo.16",
19
    "file.logo.32",
20
    "file.logo.128",
21
    "file.logo.256",
22
    "file.logo.512"
23
  );
24
25
  public static final Image ICON_DIALOG = LOGOS.get( 1 );
26
27
  public static final ImageView ICON_DIALOG_NODE = new ImageView( ICON_DIALOG );
28
29
  /**
30
   * Converts the given file names to images, such as application icons.
31
   *
32
   * @param keys The file names to convert to images.
33
   * @return The images loaded from the file name references.
34
   */
35
  private static List<Image> createImages( final String... keys ) {
36
    final List<Image> images = new ArrayList<>( keys.length );
37
38
    for( final var key : keys ) {
39
      images.add( new Image( get( key ) ) );
40
    }
41
42
    return images;
43
  }
44
}
145
A src/main/java/com/keenwrite/dom/DocumentConverter.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.dom;
3
4
import org.jetbrains.annotations.NotNull;
5
import org.jsoup.Jsoup;
6
import org.jsoup.helper.W3CDom;
7
import org.jsoup.nodes.Document.OutputSettings.Syntax;
8
import org.jsoup.nodes.Node;
9
import org.jsoup.nodes.TextNode;
10
import org.jsoup.select.NodeVisitor;
11
import org.w3c.dom.Document;
12
13
import java.util.LinkedHashMap;
14
import java.util.Map;
15
16
import static com.keenwrite.dom.DocumentParser.sDomImplementation;
17
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
18
import static java.util.Map.*;
19
20
/**
21
 * Responsible for converting JSoup document object model (DOM) to a W3C DOM.
22
 * Provides a lighter implementation than the superclass by overriding the
23
 * {@link #fromJsoup(org.jsoup.nodes.Document)} method to reuse factories,
24
 * builders, and implementations.
25
 */
26
public final class DocumentConverter extends W3CDom {
27
  /**
28
   * Retain insertion order using an instance of {@link LinkedHashMap} so
29
   * that ligature substitution uses longer ligatures ahead of shorter
30
   * ligatures. The word "ruffian" should use the "ffi" ligature, not the "ff"
31
   * ligature.
32
   */
33
  private static final Map<String, String> LIGATURES = ofEntries(
34
    entry( "ffi", "ffi" ),
35
    entry( "ffl", "ffl" ),
36
    entry( "ff", "ff" ),
37
    entry( "fi", "fi" ),
38
    entry( "fl", "fl" )
39
  );
40
41
  private static final NodeVisitor LIGATURE_VISITOR = new NodeVisitor() {
42
    @Override
43
    public void head( final @NotNull Node node, final int depth ) {
44
      if( node instanceof final TextNode textNode ) {
45
        final var parent = node.parentNode();
46
        final var name = parent == null ? "root" : parent.nodeName();
47
48
        if( !("pre".equalsIgnoreCase( name ) ||
49
          "code".equalsIgnoreCase( name ) ||
50
          "kbd".equalsIgnoreCase( name ) ||
51
          "var".equalsIgnoreCase( name ) ||
52
          "tt".equalsIgnoreCase( name )) ) {
53
          // Calling getWholeText() will return newlines, which must be kept
54
          // to ensure that preformatted text maintains its formatting.
55
          textNode.text( replace( textNode.getWholeText(), LIGATURES ) );
56
        }
57
      }
58
    }
59
60
    @Override
61
    public void tail( final @NotNull Node node, final int depth ) { }
62
  };
63
64
  @Override
65
  public @NotNull Document fromJsoup( final org.jsoup.nodes.Document in ) {
66
    assert in != null;
67
68
    final var out = DocumentParser.newDocument();
69
    final var doctype = in.documentType();
70
71
    if( doctype != null ) {
72
      out.appendChild(
73
        sDomImplementation.createDocumentType(
74
          doctype.name(),
75
          doctype.publicId(),
76
          doctype.systemId()
77
        )
78
      );
79
    }
80
81
    out.setXmlStandalone( true );
82
    in.traverse( LIGATURE_VISITOR );
83
    convert( in, out );
84
85
    return out;
86
  }
87
88
  /**
89
   * Converts the given non-well-formed HTML document into an XML document
90
   * while preserving whitespace.
91
   *
92
   * @param html The document to convert.
93
   * @return The converted document as an object model.
94
   */
95
  public static org.jsoup.nodes.Document parse( final String html ) {
96
    final var document = Jsoup.parse( html );
97
98
    document
99
      .outputSettings()
100
      .syntax( Syntax.xml )
101
      .prettyPrint( false );
102
103
    return document;
104
  }
105
}
1106
A src/main/java/com/keenwrite/dom/DocumentParser.java
1
package com.keenwrite.dom;
2
3
import org.w3c.dom.*;
4
import org.xml.sax.InputSource;
5
import org.xml.sax.SAXException;
6
7
import javax.xml.parsers.DocumentBuilder;
8
import javax.xml.parsers.DocumentBuilderFactory;
9
import javax.xml.transform.Transformer;
10
import javax.xml.transform.TransformerException;
11
import javax.xml.transform.TransformerFactory;
12
import javax.xml.transform.dom.DOMSource;
13
import javax.xml.transform.stream.StreamResult;
14
import javax.xml.xpath.XPath;
15
import javax.xml.xpath.XPathExpression;
16
import javax.xml.xpath.XPathExpressionException;
17
import javax.xml.xpath.XPathFactory;
18
import java.io.*;
19
import java.nio.file.Path;
20
import java.util.HashMap;
21
import java.util.Map;
22
import java.util.function.Consumer;
23
24
import static com.keenwrite.events.StatusEvent.clue;
25
import static java.nio.charset.StandardCharsets.UTF_16;
26
import static java.nio.charset.StandardCharsets.UTF_8;
27
import static java.nio.file.Files.write;
28
import static javax.xml.transform.OutputKeys.*;
29
import static javax.xml.xpath.XPathConstants.NODESET;
30
31
/**
32
 * Responsible for initializing an XML parser.
33
 */
34
public class DocumentParser {
35
  private static final String LOAD_EXTERNAL_DTD =
36
    "http://apache.org/xml/features/nonvalidating/load-external-dtd";
37
  private static final String INDENT_AMOUNT =
38
    "{http://xml.apache.org/xslt}indent-amount";
39
40
  private static final ByteArrayOutputStream sWriter =
41
    new ByteArrayOutputStream( 65536 );
42
  private static final OutputStreamWriter sOutput =
43
    new OutputStreamWriter( sWriter );
44
45
  /**
46
   * Caches {@link XPathExpression}s to avoid re-compiling.
47
   */
48
  private static final Map<String, XPathExpression> sXpaths = new HashMap<>();
49
50
  private static final DocumentBuilderFactory sDocumentFactory;
51
  private static DocumentBuilder sDocumentBuilder;
52
  private static Transformer sTransformer;
53
  private static final XPath sXpath = XPathFactory.newInstance().newXPath();
54
55
  public static DOMImplementation sDomImplementation;
56
57
  static {
58
    sDocumentFactory = DocumentBuilderFactory.newInstance();
59
60
    sDocumentFactory.setValidating( false );
61
    sDocumentFactory.setAttribute( LOAD_EXTERNAL_DTD, false );
62
    sDocumentFactory.setNamespaceAware( true );
63
    sDocumentFactory.setIgnoringComments( true );
64
    sDocumentFactory.setIgnoringElementContentWhitespace( true );
65
66
    try {
67
      sDocumentBuilder = sDocumentFactory.newDocumentBuilder();
68
      sDomImplementation = sDocumentBuilder.getDOMImplementation();
69
      sTransformer = TransformerFactory.newInstance().newTransformer();
70
71
      // Ensure Unicode characters (emojis) are encoded correctly.
72
      sTransformer.setOutputProperty( ENCODING, UTF_16.toString() );
73
      sTransformer.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
74
      sTransformer.setOutputProperty( METHOD, "xml" );
75
      sTransformer.setOutputProperty( INDENT, "no" );
76
      sTransformer.setOutputProperty( INDENT_AMOUNT, "2" );
77
    } catch( final Exception ex ) {
78
      clue( ex );
79
    }
80
  }
81
82
  public static Document newDocument() {
83
    return sDocumentBuilder.newDocument();
84
  }
85
86
  /**
87
   * Creates a new document object model based on the given XML document
88
   * string. This will return an empty document if the document could not
89
   * be parsed.
90
   *
91
   * @param xml The document text to convert into a DOM.
92
   * @return The DOM that represents the given XML data.
93
   */
94
  public static Document parse( final String xml ) {
95
    assert xml != null;
96
97
    final var input = new InputSource();
98
99
    try( final var reader = new StringReader( xml ) ) {
100
      input.setEncoding( UTF_8.toString() );
101
      input.setCharacterStream( reader );
102
103
      return sDocumentBuilder.parse( input );
104
    } catch( final Exception ex ) {
105
      clue( ex );
106
107
      return sDocumentBuilder.newDocument();
108
    }
109
  }
110
111
  /**
112
   * Parses the given file contents into a document object model.
113
   *
114
   * @param doc The source XML document to parse.
115
   * @return The file as a document object model.
116
   * @throws IOException  Could not open the document.
117
   * @throws SAXException Could not read the XML file content.
118
   */
119
  public static Document parse( final File doc )
120
    throws IOException, SAXException {
121
    assert doc != null;
122
123
    try( final var in = new FileInputStream( doc ) ) {
124
      return parse( in );
125
    }
126
  }
127
128
  /**
129
   * Parses the given file contents into a document object model. Callers
130
   * must close the stream.
131
   *
132
   * @param doc The source XML document to parse.
133
   * @return The {@link InputStream} converted to a document object model.
134
   * @throws IOException  Could not open the document.
135
   * @throws SAXException Could not read the XML file content.
136
   */
137
  public static Document parse( final InputStream doc )
138
    throws IOException, SAXException {
139
    assert doc != null;
140
141
    return sDocumentBuilder.parse( doc );
142
  }
143
144
  /**
145
   * Allows an operation to be applied for every node in the document that
146
   * matches a given tag name pattern.
147
   *
148
   * @param document Document to traverse.
149
   * @param xpath    Document elements to find via {@link XPath} expression.
150
   * @param consumer The consumer to call for each matching document node.
151
   */
152
  public static void visit(
153
    final Document document,
154
    final CharSequence xpath,
155
    final Consumer<Node> consumer ) {
156
    assert document != null;
157
    assert consumer != null;
158
159
    try {
160
      final var expr = compile( xpath );
161
      final var nodeSet = expr.evaluate( document, NODESET );
162
163
      if( nodeSet instanceof NodeList nodes ) {
164
        for( int i = 0, len = nodes.getLength(); i < len; i++ ) {
165
          consumer.accept( nodes.item( i ) );
166
        }
167
      }
168
    } catch( final Exception ex ) {
169
      clue( ex );
170
    }
171
  }
172
173
  public static Node createMeta(
174
    final Document document, final Map.Entry<String, String> entry ) {
175
    assert document != null;
176
    assert entry != null;
177
178
    final var node = document.createElement( "meta" );
179
180
    node.setAttribute( "name", entry.getKey() );
181
    node.setAttribute( "content", entry.getValue() );
182
183
    return node;
184
  }
185
186
  public static Node createElement(
187
    final Document doc, final String nodeName, final String nodeValue ) {
188
    assert doc != null;
189
    assert nodeName != null;
190
    assert !nodeName.isBlank();
191
192
    final var node = doc.createElement( nodeName );
193
194
    if( nodeValue != null ) {
195
      node.setTextContent( nodeValue );
196
    }
197
198
    return node;
199
  }
200
201
  public static String toString( final Document xhtml ) {
202
    assert xhtml != null;
203
204
    try( final var writer = new StringWriter() ) {
205
      final var result = new StreamResult( writer );
206
207
      transform( xhtml, result );
208
209
      return writer.toString();
210
    } catch( final Exception ex ) {
211
      clue( ex );
212
      return "";
213
    }
214
  }
215
216
  public static String transform( final Element root )
217
    throws IOException, TransformerException {
218
    assert root != null;
219
220
    try( final var writer = new StringWriter() ) {
221
      transform( root.getOwnerDocument(), new StreamResult( writer ) );
222
223
      return writer.toString();
224
    }
225
  }
226
227
  /**
228
   * Remove whitespace, comments, and XML/DOCTYPE declarations to make
229
   * processing work with ConTeXt.
230
   *
231
   * @param path The SVG file to process.
232
   * @throws Exception The file could not be processed.
233
   */
234
  public static void sanitize( final Path path ) throws Exception {
235
    assert path != null;
236
237
    // Preprocessing the SVG image is a single-threaded operation, no matter
238
    // how many SVG images are in the document to typeset.
239
    sWriter.reset();
240
241
    final var target = new StreamResult( sOutput );
242
    final var source = sDocumentBuilder.parse( path.toFile() );
243
244
    transform( source, target );
245
    write( path, sWriter.toByteArray() );
246
  }
247
248
  /**
249
   * Converts a string into an {@link XPathExpression}, which may be used to
250
   * extract elements from a {@link Document} object model.
251
   *
252
   * @param cs The string to convert to an {@link XPathExpression}.
253
   * @return {@code null} if there was an error compiling the xpath.
254
   */
255
  public static XPathExpression compile( final CharSequence cs ) {
256
    assert cs != null;
257
258
    final var xpath = cs.toString();
259
260
    return sXpaths.computeIfAbsent( xpath, k -> {
261
      try {
262
        return sXpath.compile( xpath );
263
      } catch( final XPathExpressionException ex ) {
264
        clue( ex );
265
        return null;
266
      }
267
    } );
268
  }
269
270
  /**
271
   * Streams an instance of {@link Document} as a plain text XML document.
272
   *
273
   * @param src The source document to transform.
274
   * @param dst The destination location to write the transformed version.
275
   * @throws TransformerException Could not transform the document.
276
   */
277
  private static void transform( final Document src, final StreamResult dst )
278
    throws TransformerException {
279
    sTransformer.transform( new DOMSource( src ), dst );
280
  }
281
282
  /**
283
   * Use the {@code static} constants and methods, not an instance, at least
284
   * until an iterable sub-interface is written.
285
   */
286
  private DocumentParser() { }
287
}
1288
A src/main/java/com/keenwrite/editors/TextDefinition.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors;
3
4
import com.keenwrite.editors.definition.DefinitionEditor;
5
import com.keenwrite.editors.definition.DefinitionTreeItem;
6
import com.keenwrite.editors.markdown.MarkdownEditor;
7
import javafx.scene.control.TreeItem;
8
9
import java.util.Map;
10
11
/**
12
 * Differentiates an instance of {@link TextResource} from an instance of
13
 * {@link DefinitionEditor} or {@link MarkdownEditor}.
14
 */
15
public interface TextDefinition extends TextResource {
16
17
  /**
18
   * Requests all variable definitions.
19
   *
20
   * @return The definition map without interpolation.
21
   */
22
  Map<String, String> getDefinitions();
23
24
  /**
25
   * Requests that the visual representation be expanded to the given node.
26
   *
27
   * @param node Request expansion to this node.
28
   */
29
  <T> void expand( TreeItem<T> node );
30
31
  /**
32
   * Adds a new item to the definition hierarchy.
33
   */
34
  void createDefinition();
35
36
  /**
37
   * Edits the currently selected definition in the hierarchy.
38
   */
39
  void renameDefinition();
40
41
  /**
42
   * Removes the currently selected definition in the hierarchy.
43
   */
44
  void deleteDefinitions();
45
46
  /**
47
   * Finds the definition that exact matches the given text.
48
   *
49
   * @param text The value to find, never {@code null}.
50
   * @return The leaf that contains the given value.
51
   */
52
  DefinitionTreeItem<String> findLeafExact( String text );
53
54
  /**
55
   * Finds the definition that starts with the given text.
56
   *
57
   * @param text The value to find, never {@code null}.
58
   * @return The leaf that starts with the given value.
59
   */
60
  DefinitionTreeItem<String> findLeafStartsWith( String text );
61
62
  /**
63
   * Finds the definition that contains the given text, matching case.
64
   *
65
   * @param text The value to find, never {@code null}.
66
   * @return The leaf that contains the exact given value.
67
   */
68
  DefinitionTreeItem<String> findLeafContains( String text );
69
70
  /**
71
   * Finds the definition that contains the given text, ignoring case.
72
   *
73
   * @param text The value to find, never {@code null}.
74
   * @return The leaf that contains the given value, regardless of case.
75
   */
76
  DefinitionTreeItem<String> findLeafContainsNoCase( String text );
77
78
  /**
79
   * Answers whether there are any definitions written.
80
   *
81
   * @return {@code true} when there are no definitions.
82
   */
83
  boolean isEmpty();
84
}
185
A src/main/java/com/keenwrite/editors/TextEditor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors;
3
4
import com.keenwrite.editors.common.Caret;
5
import javafx.scene.control.IndexRange;
6
import org.fxmisc.flowless.VirtualizedScrollPane;
7
import org.fxmisc.richtext.StyleClassedTextArea;
8
9
/**
10
 * Responsible for differentiating an instance of {@link TextResource} from
11
 * other {@link TextResource} subtypes, such as a {@link TextDefinition}.
12
 * This is primarily used as a marker interface, but also defines a minimal
13
 * set of functionality required by all {@link TextEditor} instances, which
14
 * includes scrolling facilities.
15
 */
16
public interface TextEditor extends TextResource {
17
18
  /**
19
   * Returns the scrollbars associated with the editor's view so that they
20
   * can be moved for synchronized scrolling.
21
   *
22
   * @return The initialized horizontal and vertical scrollbars.
23
   */
24
  VirtualizedScrollPane<StyleClassedTextArea> getScrollPane();
25
26
  StyleClassedTextArea getTextArea();
27
28
  /**
29
   * Delegates requesting focus to the internal {@link StyleClassedTextArea}.
30
   */
31
  default void requestFocus() {
32
    getTextArea().requestFocus();
33
  }
34
35
  /**
36
   * Returns the complete text for the specified paragraph index.
37
   *
38
   * @param paragraph The zero-based paragraph index.
39
   * @throws IndexOutOfBoundsException The paragraph index is less than zero
40
   *                                   or greater than the number of
41
   *                                   paragraphs in the document.
42
   */
43
  String getText( int paragraph ) throws IndexOutOfBoundsException;
44
45
  /**
46
   * Returns the text between the indexes specified by the given
47
   * {@link IndexRange}.
48
   *
49
   * @param indexes The start and end document indexes to reference.
50
   * @return The text between the specified indexes.
51
   * @throws IndexOutOfBoundsException The indexes are invalid.
52
   */
53
  String getText( IndexRange indexes ) throws IndexOutOfBoundsException;
54
55
  /**
56
   * Moves the caret to the given document offset.
57
   *
58
   * @param offset The absolute offset into the document, zero-based.
59
   */
60
  void moveTo( final int offset );
61
62
  /**
63
   * Returns an object that can be used to track the current caret position
64
   * within the document.
65
   *
66
   * @return The caret's position, which is updated continuously.
67
   */
68
  Caret getCaret();
69
70
  /**
71
   * Replaces the text within the given range with the given string.
72
   *
73
   * @param indexes The starting and ending document indexes that represent
74
   *                the range of text to replace.
75
   * @param s       The text to replace, which can be shorter or longer than the
76
   *                specified range.
77
   */
78
  void replaceText( IndexRange indexes, String s );
79
80
  /**
81
   * Returns the starting and ending indexes into the document for the
82
   * word at the current caret position.
83
   * <p>
84
   * Finds the start and end indexes for the word in the current document,
85
   * where the caret is located. There are a few different scenarios, where
86
   * the caret can be at: the start, end, or middle of a word; also, the
87
   * caret can be at the end or beginning of a punctuated word; as well, the
88
   * caret could be at the beginning or end of the line or document.
89
   * </p>
90
   *
91
   * @return The start and ending index into the current document that
92
   * represent the word boundaries of the word under the caret.
93
   */
94
  IndexRange getCaretWord();
95
96
  /**
97
   * Convenience method to get the word at the current caret position.
98
   *
99
   * @return This will return the empty string if the caret is out of bounds.
100
   */
101
  default String getCaretWordText() {
102
    return getText( getCaretWord() );
103
  }
104
105
  /**
106
   * Requests undoing the last text-changing action.
107
   */
108
  void undo();
109
110
  /**
111
   * Requests redoing the last text-changing action that was undone.
112
   */
113
  void redo();
114
115
  /**
116
   * Requests cutting the selected text, or the current line if none selected.
117
   */
118
  void cut();
119
120
  /**
121
   * Requests copying the selected text, or no operation if none selected.
122
   */
123
  void copy();
124
125
  /**
126
   * Requests pasting from the clipboard into the editor. This will replace
127
   * text if selected, otherwise the clipboard contents are inserted at the
128
   * cursor.
129
   */
130
  void paste();
131
132
  /**
133
   * Requests selecting the entire document. This will replace the existing
134
   * selection, if any.
135
   */
136
  void selectAll();
137
138
  /**
139
   * Requests making the selected text, or word at caret, bold.
140
   */
141
  default void bold() {}
142
143
  /**
144
   * Requests making the selected text, or word at caret, italic.
145
   */
146
  default void italic() {}
147
148
  /**
149
   * Requests making the selected text, or word at caret, monospace.
150
   */
151
  default void monospace() {}
152
153
  /**
154
   * Requests making the selected text, or word at caret, a superscript.
155
   */
156
  default void superscript() {}
157
158
  /**
159
   * Requests making the selected text, or word at caret, a subscript.
160
   */
161
  default void subscript() {}
162
163
  /**
164
   * Requests making the selected text, or word at caret, struck.
165
   */
166
  default void strikethrough() {}
167
168
  /**
169
   * Requests making the selected text, or word at caret, a blockquote block.
170
   */
171
  default void blockquote() {}
172
173
  /**
174
   * Requests making the selected text, or word at caret, inline code.
175
   */
176
  default void code() {}
177
178
  /**
179
   * Requests making the selected text, or word at caret, a fenced code block.
180
   */
181
  default void fencedCodeBlock() {}
182
183
  /**
184
   * Requests making the selected text, or word at caret, a heading.
185
   *
186
   * @param level The heading level to apply (typically 1 through 3).
187
   */
188
  default void heading( final int level ) {}
189
190
  /**
191
   * Requests making the selected text, or word at caret, an unordered list
192
   * block.
193
   */
194
  default void unorderedList() {}
195
196
  /**
197
   * Requests making the selected text, or word at caret, an ordered list block.
198
   */
199
  default void orderedList() {}
200
201
  /**
202
   * Requests making the selected text, or inserting at the caret, a
203
   * horizontal rule.
204
   */
205
  default void horizontalRule() {}
206
207
  /**
208
   * Requests that styling be added to the document between the given
209
   * integer values.
210
   *
211
   * @param indexes Document offset where style is to start and end.
212
   * @param style   The style class to apply between the given offset indexes.
213
   */
214
  default void stylize( final IndexRange indexes, final String style ) {}
215
216
  /**
217
   * Requests that the most recent styling for the given style class be
218
   * removed from the document between the given integer values.
219
   */
220
  default void unstylize( final String style ) {}
221
}
1222
A src/main/java/com/keenwrite/editors/TextResource.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors;
3
4
import com.keenwrite.io.MediaType;
5
import javafx.beans.property.ReadOnlyBooleanProperty;
6
import javafx.scene.Node;
7
import org.mozilla.universalchardet.UniversalDetector;
8
9
import java.io.File;
10
import java.nio.charset.Charset;
11
import java.nio.file.Path;
12
13
import static com.keenwrite.constants.Constants.DEFAULT_CHARSET;
14
import static com.keenwrite.events.StatusEvent.clue;
15
import static java.nio.charset.Charset.forName;
16
import static java.nio.file.Files.readAllBytes;
17
import static java.nio.file.Files.write;
18
import static java.util.Arrays.asList;
19
import static java.util.Locale.ENGLISH;
20
21
/**
22
 * A text resource can be persisted and retrieved from its persisted location.
23
 */
24
public interface TextResource {
25
  /**
26
   * Sets the text string that to be changed through some graphical user
27
   * interface. For example, a YAML document must be parsed from the given
28
   * text string into a tree view with which the user may interact.
29
   *
30
   * @param text The new content for the resource.
31
   */
32
  void setText( String text );
33
34
  /**
35
   * Returns the text string that may have been modified by the user through
36
   * some graphical user interface.
37
   *
38
   * @return The text value, based on the value set from
39
   * {@link #setText(String)}, but possibly mutated.
40
   */
41
  String getText();
42
43
  /**
44
   * Return the character encoding for this file.
45
   *
46
   * @return A non-null character set, primarily detected from file contents.
47
   */
48
  Charset getEncoding();
49
50
  /**
51
   * Renames the current file to the given fully qualified file name.
52
   *
53
   * @param file The new file name.
54
   */
55
  void rename( final File file );
56
57
  /**
58
   * Returns the file name, without any directory components, for this instance.
59
   * Useful for showing as a tab title.
60
   *
61
   * @return The file name value returned from {@link #getFile()}.
62
   */
63
  default String getFilename() {
64
    final var filename = getFile().toPath().getFileName();
65
    return filename == null ? "" : filename.toString();
66
  }
67
68
  /**
69
   * Returns the fully qualified {@link File} to the editable text resource.
70
   * Useful for showing as a tab tooltip, saving the file, or reading it.
71
   *
72
   * @return A non-null {@link File} instance.
73
   */
74
  File getFile();
75
76
  /**
77
   * Returns the {@link MediaType} associated with the file being edited.
78
   *
79
   * @return The {@link MediaType} for the editor's file.
80
   */
81
  default MediaType getMediaType() {
82
    return MediaType.valueFrom( getFile() );
83
  }
84
85
  /**
86
   * Answers whether this instance is an editor for at least one of the given
87
   * {@link MediaType} references.
88
   *
89
   * @param mediaTypes The {@link MediaType} references to compare against.
90
   * @return {@code true} if the given list of media types contains the
91
   * {@link MediaType} for this editor.
92
   */
93
  default boolean isMediaType( final MediaType... mediaTypes ) {
94
    return asList( mediaTypes ).contains( getMediaType() );
95
  }
96
97
  /**
98
   * Returns the fully qualified {@link Path} to the editable text resource.
99
   * This delegates to {@link #getFile()}.
100
   *
101
   * @return A non-null {@link Path} instance.
102
   */
103
  default Path getPath() {
104
    return getFile().toPath();
105
  }
106
107
  /**
108
   * Read the file contents and update the text accordingly. If the file
109
   * cannot be read then no changes will happen to the text. Fails silently.
110
   *
111
   * @param path The fully qualified {@link Path}, including a file name, to
112
   *             fully read into the editor.
113
   * @return The character encoding for the file at the given {@link Path}.
114
   */
115
  default Charset open( final Path path ) {
116
    final var file = path.toFile();
117
    Charset encoding = DEFAULT_CHARSET;
118
119
    try {
120
      if( file.exists() ) {
121
        if( file.canWrite() && file.canRead() ) {
122
          final var bytes = readAllBytes( path );
123
          encoding = detectEncoding( bytes );
124
125
          setText( asString( bytes, encoding ) );
126
        }
127
        else {
128
          clue( "TextResource.load.error.permissions", file.toString() );
129
        }
130
      }
131
      else {
132
        clue( "TextResource.load.error.unsaved", file.toString() );
133
      }
134
    } catch( final Exception ex ) {
135
      clue( ex );
136
    }
137
138
    return encoding;
139
  }
140
141
  /**
142
   * Read the file contents and update the text accordingly. If the file
143
   * cannot be read then no changes will happen to the text. This delegates
144
   * to {@link #open(Path)}.
145
   *
146
   * @param file The {@link File} to fully read into the editor.
147
   * @return The file's character encoding.
148
   */
149
  default Charset open( final File file ) {
150
    return open( file.toPath() );
151
  }
152
153
  /**
154
   * Save the file contents and clear the modified flag. If the file cannot
155
   * be saved, the exception is swallowed and this method returns {@code false}.
156
   *
157
   * @return {@code true} the file was saved; {@code false} if upon exception.
158
   */
159
  default boolean save() {
160
    try {
161
      write( getPath(), asBytes( getText() ) );
162
      clearModifiedProperty();
163
      return true;
164
    } catch( final Exception ex ) {
165
      clue( ex );
166
    }
167
168
    return false;
169
  }
170
171
  /**
172
   * Returns the node associated with this {@link TextResource}.
173
   *
174
   * @return The view component for the {@link TextResource}.
175
   */
176
  Node getNode();
177
178
  /**
179
   * Answers whether the resource has been modified.
180
   *
181
   * @return {@code true} the resource has changed; {@code false} means that
182
   * no changes to the resource have been made.
183
   */
184
  default boolean isModified() {
185
    return modifiedProperty().get();
186
  }
187
188
  /**
189
   * Returns a property that answers whether this text resource has been
190
   * changed from the original text that was opened.
191
   *
192
   * @return A property representing the modified state of this
193
   * {@link TextResource}.
194
   */
195
  ReadOnlyBooleanProperty modifiedProperty();
196
197
  /**
198
   * Lowers the modified flag such that listeners to the modified property
199
   * will be informed that the text that's being edited no longer differs
200
   * from what's persisted.
201
   */
202
  void clearModifiedProperty();
203
204
  private String asString( final byte[] text, final Charset encoding ) {
205
    return new String( text, encoding );
206
  }
207
208
  /**
209
   * Converts the given string to an array of bytes using the encoding that was
210
   * originally detected (if any) and associated with this file.
211
   *
212
   * @param text The text to convert into the original file encoding.
213
   * @return A series of bytes ready for writing to a file.
214
   */
215
  private byte[] asBytes( final String text ) {
216
    return text.getBytes( getEncoding() );
217
  }
218
219
  private Charset detectEncoding( final byte[] bytes ) {
220
    final var detector = new UniversalDetector( null );
221
    detector.handleData( bytes, 0, bytes.length );
222
    detector.dataEnd();
223
224
    final var charset = detector.getDetectedCharset();
225
226
    return charset == null
227
      ? DEFAULT_CHARSET
228
      : forName( charset.toUpperCase( ENGLISH ) );
229
  }
230
231
  /**
232
   * Answers whether the given resource are of the same conceptual type. This
233
   * method is intended to be overridden by subclasses.
234
   *
235
   * @param mediaType The type to compare.
236
   * @return {@code true} if the {@link TextResource} is compatible with the
237
   * given {@link MediaType}.
238
   */
239
  default boolean supports( final MediaType mediaType ) {
240
    return isMediaType( mediaType );
241
  }
242
}
1243
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 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.common;
3
4
import com.keenwrite.events.ScrollLockEvent;
5
import javafx.beans.property.BooleanProperty;
6
import javafx.beans.property.SimpleBooleanProperty;
7
import javafx.event.Event;
8
import javafx.event.EventHandler;
9
import javafx.scene.control.ScrollBar;
10
import javafx.scene.control.skin.ScrollBarSkin;
11
import javafx.scene.input.MouseEvent;
12
import javafx.scene.input.ScrollEvent;
13
import javafx.scene.layout.StackPane;
14
import org.fxmisc.flowless.VirtualizedScrollPane;
15
import org.fxmisc.richtext.StyleClassedTextArea;
16
import org.greenrobot.eventbus.Subscribe;
17
18
import javax.swing.*;
19
import java.util.function.Consumer;
20
21
import static com.keenwrite.events.Bus.register;
22
import static java.lang.Math.max;
23
import static java.lang.Math.min;
24
import static javafx.geometry.Orientation.VERTICAL;
25
import static javax.swing.SwingUtilities.invokeLater;
26
27
/**
28
 * Converts scroll events from {@link VirtualizedScrollPane} scroll bars to
29
 * an instance of {@link JScrollBar}.
30
 * <p>
31
 * Called to synchronize the scrolling areas for either scrolling with the
32
 * mouse or scrolling using the scrollbar's thumb. Both are required to avoid
33
 * scrolling on the estimatedScrollYProperty that occurs when text events
34
 * fire. Scrolling performed for text events are handled separately to ensure
35
 * the preview panel scrolls to the same position in the Markdown editor,
36
 * taking into account things like images, tables, and other potentially
37
 * long vertical presentation items.
38
 * </p>
39
 */
40
public final class ScrollEventHandler implements EventHandler<Event> {
41
42
  private final class MouseHandler implements EventHandler<MouseEvent> {
43
    private final EventHandler<? super MouseEvent> mOldHandler;
44
45
    /**
46
     * Constructs a new handler for mouse scrolling events.
47
     *
48
     * @param oldHandler Receives the event after scrolling takes place.
49
     */
50
    private MouseHandler( final EventHandler<? super MouseEvent> oldHandler ) {
51
      mOldHandler = oldHandler;
52
    }
53
54
    @Override
55
    public void handle( final MouseEvent event ) {
56
      ScrollEventHandler.this.handle( event );
57
      mOldHandler.handle( event );
58
    }
59
  }
60
61
  private final class ScrollHandler implements EventHandler<ScrollEvent> {
62
    @Override
63
    public void handle( final ScrollEvent event ) {
64
      ScrollEventHandler.this.handle( event );
65
    }
66
  }
67
68
  private final VirtualizedScrollPane<StyleClassedTextArea> mEditorScrollPane;
69
  private final JScrollBar mPreviewScrollBar;
70
  private final BooleanProperty mEnabled = new SimpleBooleanProperty();
71
72
  private boolean mLocked;
73
74
  /**
75
   * @param editorScrollPane Scroll event source (human movement).
76
   * @param previewScrollBar Scroll event destination (corresponding movement).
77
   */
78
  public ScrollEventHandler(
79
    final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane,
80
    final JScrollBar previewScrollBar ) {
81
    mEditorScrollPane = editorScrollPane;
82
    mPreviewScrollBar = previewScrollBar;
83
84
    mEditorScrollPane.addEventFilter( ScrollEvent.ANY, new ScrollHandler() );
85
86
    initVerticalScrollBarThumb(
87
      mEditorScrollPane,
88
      thumb -> {
89
        final var handler = new MouseHandler( thumb.getOnMouseDragged() );
90
        thumb.setOnMouseDragged( handler );
91
      }
92
    );
93
94
    register( this );
95
  }
96
97
  /**
98
   * Gets a property intended to be bound to selected property of the tab being
99
   * scrolled. This is required because there's only one preview pane but
100
   * multiple editor panes. Each editor pane maintains its own scroll position.
101
   *
102
   * @return A {@link BooleanProperty} representing whether the scroll
103
   * events for this tab are to be executed.
104
   */
105
  public BooleanProperty enabledProperty() {
106
    return mEnabled;
107
  }
108
109
  /**
110
   * Scrolls the preview scrollbar relative to the edit scrollbar. Algorithm
111
   * is based on Karl Tauber's ratio calculation.
112
   *
113
   * @param event Unused; either {@link MouseEvent} or {@link ScrollEvent}
114
   */
115
  @Override
116
  public void handle( final Event event ) {
117
    invokeLater( () -> {
118
      if( isEnabled() ) {
119
        // e prefix is for editor pane.
120
        final var eScrollPane = getEditorScrollPane();
121
        final var eScrollY =
122
          eScrollPane.estimatedScrollYProperty().getValue().intValue();
123
        final var eHeight = (int)
124
          (eScrollPane.totalHeightEstimateProperty().getValue().intValue()
125
            - eScrollPane.getHeight());
126
        final var eRatio = eHeight > 0
127
          ? min( max( eScrollY / (float) eHeight, 0 ), 1 ) : 0;
128
129
        // p prefix is for preview pane.
130
        final var pScrollBar = getPreviewScrollBar();
131
        final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight();
132
        final var pScrollY = (int) (pHeight * eRatio);
133
134
        pScrollBar.setValue( pScrollY );
135
        pScrollBar.getParent().repaint();
136
      }
137
    } );
138
  }
139
140
  @Subscribe
141
  public void handle( final ScrollLockEvent event ) {
142
    mLocked = event.isLocked();
143
  }
144
145
  private void initVerticalScrollBarThumb(
146
    final VirtualizedScrollPane<StyleClassedTextArea> pane,
147
    final Consumer<StackPane> consumer ) {
148
    // When the skin property is set, the stack pane is available (not null).
149
    getVerticalScrollBar( pane ).skinProperty().addListener( ( c, o, n ) -> {
150
      for( final var node : ((ScrollBarSkin) n).getChildren() ) {
151
        // Brittle, but what can you do?
152
        if( node.getStyleClass().contains( "thumb" ) ) {
153
          consumer.accept( (StackPane) node );
154
        }
155
      }
156
    } );
157
  }
158
159
  /**
160
   * Returns the vertical {@link ScrollBar} instance associated with the
161
   * given scroll pane. This is {@code null}-safe because the scroll pane
162
   * initializes its vertical {@link ScrollBar} upon construction.
163
   *
164
   * @param pane The scroll pane that contains a vertical {@link ScrollBar}.
165
   * @return The vertical {@link ScrollBar} associated with the scroll pane.
166
   * @throws IllegalStateException Could not obtain the vertical scroll bar.
167
   */
168
  private ScrollBar getVerticalScrollBar(
169
    final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
170
171
    for( final var node : pane.getChildrenUnmodifiable() ) {
172
      if( node instanceof final ScrollBar scrollBar &&
173
        scrollBar.getOrientation() == VERTICAL ) {
174
        return scrollBar;
175
      }
176
    }
177
178
    throw new IllegalStateException( "No vertical scroll bar found." );
179
  }
180
181
  private boolean isEnabled() {
182
    // TODO: As a minor optimization, when this is set to false, it could remove
183
    // the MouseHandler and ScrollHandler so that events only dispatch to one
184
    // object (instead of one per editor tab).
185
    return mEnabled.get() && !mLocked;
186
  }
187
188
  private VirtualizedScrollPane<StyleClassedTextArea> getEditorScrollPane() {
189
    return mEditorScrollPane;
190
  }
191
192
  private JScrollBar getPreviewScrollBar() {
193
    return mPreviewScrollBar;
194
  }
195
}
1196
A src/main/java/com/keenwrite/editors/common/VariableNameInjector.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.common;
3
4
import com.keenwrite.editors.TextDefinition;
5
import com.keenwrite.editors.TextEditor;
6
import com.keenwrite.editors.definition.DefinitionTreeItem;
7
import com.keenwrite.io.MediaType;
8
import com.keenwrite.preferences.Key;
9
import com.keenwrite.preferences.Workspace;
10
import com.keenwrite.processors.r.RInlineEvaluator;
11
import com.keenwrite.sigils.PropertyKeyOperator;
12
import com.keenwrite.sigils.RKeyOperator;
13
14
import java.util.function.UnaryOperator;
15
16
import static com.keenwrite.constants.Constants.*;
17
import static com.keenwrite.events.StatusEvent.clue;
18
import static com.keenwrite.preferences.AppKeys.*;
19
20
/**
21
 * Provides the logic for injecting variable names within the editor.
22
 */
23
public final class VariableNameInjector {
24
  private final Workspace mWorkspace;
25
26
  public VariableNameInjector( final Workspace workspace ) {
27
    assert workspace != null;
28
29
    mWorkspace = workspace;
30
  }
31
32
  /**
33
   * Find a node that matches the current word and substitute the definition
34
   * reference. After calling this method the document being edited will have
35
   * the word under the caret replaced with a corresponding variable name
36
   * bracketed by sigils according to the document's media type.
37
   *
38
   * @param editor      The editor having a caret and a word under that caret.
39
   * @param definitions The list of variable definitions to search for a value
40
   *                    that matches the word under the caret.
41
   */
42
  public void autoinsert(
43
    final TextEditor editor,
44
    final TextDefinition definitions ) {
45
    assert editor != null;
46
    assert definitions != null;
47
48
    try {
49
      if( definitions.isEmpty() ) {
50
        clue( STATUS_DEFINITION_EMPTY );
51
      }
52
      else {
53
        final var indexes = editor.getCaretWord();
54
        final var word = editor.getText( indexes );
55
56
        if( word.isBlank() ) {
57
          clue( STATUS_DEFINITION_BLANK );
58
        }
59
        else {
60
          final var leaf = findLeaf( definitions, word );
61
62
          if( leaf == null ) {
63
            clue( STATUS_DEFINITION_MISSING, word );
64
          }
65
          else {
66
            insert( editor, leaf );
67
            definitions.expand( leaf );
68
          }
69
        }
70
      }
71
    } catch( final Exception ex ) {
72
      clue( STATUS_DEFINITION_BLANK, ex );
73
    }
74
  }
75
76
  public void insert(
77
    final TextEditor editor,
78
    final DefinitionTreeItem<String> leaf ) {
79
    assert editor != null;
80
    assert leaf != null;
81
82
    final var mediaType = editor.getMediaType();
83
    final var operator = createOperator( mediaType );
84
    final var indexes = editor.getCaretWord();
85
86
    editor.replaceText( indexes, operator.apply( leaf.toPath() ) );
87
  }
88
89
  /**
90
   * Creates an instance of {@link UnaryOperator} that can wrap a value with
91
   * sigils.
92
   *
93
   * @param mediaType The type of document with variables to sigilize.
94
   * @return An operator that produces sigilized variable names.
95
   */
96
  private UnaryOperator<String> createOperator( final MediaType mediaType ) {
97
    final String began;
98
    final String ended;
99
    final UnaryOperator<String> operator;
100
101
    switch( mediaType ) {
102
      case TEXT_MARKDOWN -> {
103
        began = getString( KEY_DEF_DELIM_BEGAN );
104
        ended = getString( KEY_DEF_DELIM_ENDED );
105
        operator = s -> s;
106
      }
107
      case TEXT_R_MARKDOWN -> {
108
        began = RInlineEvaluator.PREFIX + getString( KEY_R_DELIM_BEGAN );
109
        ended = getString( KEY_R_DELIM_ENDED ) + RInlineEvaluator.SUFFIX;
110
        operator = new RKeyOperator();
111
      }
112
      case TEXT_PROPERTIES -> {
113
        began = PropertyKeyOperator.BEGAN;
114
        ended = PropertyKeyOperator.ENDED;
115
        operator = s -> s;
116
      }
117
      default -> {
118
        began = "";
119
        ended = "";
120
        operator = s -> s;
121
      }
122
    }
123
124
    return s -> began + operator.apply( s ) + ended;
125
  }
126
127
  private String getString( final Key key ) {
128
    assert key != null;
129
130
    return mWorkspace.getString( key );
131
  }
132
133
  /**
134
   * Looks for the given word, matching first by exact, next by a starts-with
135
   * condition with diacritics replaced, then by containment.
136
   *
137
   * @param word Match the word by: exact, beginning, containment, or other.
138
   */
139
  @SuppressWarnings( "ConstantConditions" )
140
  private static DefinitionTreeItem<String> findLeaf(
141
    final TextDefinition definition, final String word ) {
142
    assert definition != null;
143
    assert word != null;
144
145
    DefinitionTreeItem<String> leaf = null;
146
147
    leaf = leaf == null ? definition.findLeafExact( word ) : leaf;
148
    leaf = leaf == null ? definition.findLeafStartsWith( word ) : leaf;
149
    leaf = leaf == null ? definition.findLeafContains( word ) : leaf;
150
    leaf = leaf == null ? definition.findLeafContainsNoCase( word ) : leaf;
151
152
    return leaf;
153
  }
154
}
1155
A src/main/java/com/keenwrite/editors/definition/DefinitionEditor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import com.keenwrite.constants.Constants;
5
import com.keenwrite.editors.TextDefinition;
6
import com.keenwrite.events.InsertDefinitionEvent;
7
import com.keenwrite.events.TextDefinitionFocusEvent;
8
import com.keenwrite.processors.r.Engine;
9
import com.keenwrite.ui.tree.AltTreeView;
10
import com.keenwrite.ui.tree.TreeItemConverter;
11
import javafx.beans.property.BooleanProperty;
12
import javafx.beans.property.ReadOnlyBooleanProperty;
13
import javafx.beans.property.SimpleBooleanProperty;
14
import javafx.beans.value.ObservableValue;
15
import javafx.collections.ObservableList;
16
import javafx.event.ActionEvent;
17
import javafx.event.Event;
18
import javafx.event.EventHandler;
19
import javafx.scene.Node;
20
import javafx.scene.control.*;
21
import javafx.scene.input.KeyEvent;
22
import javafx.scene.layout.BorderPane;
23
import javafx.scene.layout.HBox;
24
25
import java.io.File;
26
import java.nio.charset.Charset;
27
import java.util.*;
28
29
import static com.keenwrite.Messages.get;
30
import static com.keenwrite.constants.Constants.*;
31
import static com.keenwrite.events.StatusEvent.clue;
32
import static com.keenwrite.ui.fonts.IconFactory.createGraphic;
33
import static javafx.geometry.Pos.CENTER;
34
import static javafx.geometry.Pos.TOP_CENTER;
35
import static javafx.scene.control.SelectionMode.MULTIPLE;
36
import static javafx.scene.control.TreeItem.childrenModificationEvent;
37
import static javafx.scene.control.TreeItem.valueChangedEvent;
38
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
39
40
/**
41
 * Provides the user interface that holds a {@link TreeView}, which
42
 * allows users to interact with key/value pairs loaded from the
43
 * document parser and adapted using a {@link TreeTransformer}.
44
 */
45
public final class DefinitionEditor extends BorderPane
46
  implements TextDefinition {
47
48
  /**
49
   * Contains the root that is added to the view.
50
   */
51
  private final DefinitionTreeItem<String> mTreeRoot = createRootTreeItem();
52
53
  /**
54
   * Contains a view of the definitions.
55
   */
56
  private final TreeView<String> mTreeView =
57
    new AltTreeView<>( mTreeRoot, new TreeItemConverter() );
58
59
  /**
60
   * Used to adapt the structured document into a {@link TreeView}.
61
   */
62
  private final TreeTransformer mTreeTransformer;
63
64
  /**
65
   * Handlers for key press events.
66
   */
67
  private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
68
    = new HashSet<>();
69
70
  private final Map<String, String> mDefinitions = new HashMap<>();
71
72
  /**
73
   * Opened file's character encoding, or {@link Constants#DEFAULT_CHARSET} if
74
   * either no encoding could be determined or this is a new (empty) file.
75
   */
76
  private final Charset mEncoding;
77
78
  /**
79
   * Tracks whether the in-memory definitions have changed with respect to the
80
   * persisted definitions.
81
   */
82
  private final BooleanProperty mModified = new SimpleBooleanProperty();
83
84
  /**
85
   * File being edited by this editor instance, which may be renamed.
86
   */
87
  private File mFile;
88
89
  /**
90
   * This is provided for unit tests that are not backed by files.
91
   *
92
   * @param treeTransformer Responsible for transforming the definitions into
93
   *                        {@link TreeItem} instances.
94
   */
95
  public DefinitionEditor(
96
    final TreeTransformer treeTransformer ) {
97
    this( DEFINITION_DEFAULT, treeTransformer );
98
  }
99
100
  /**
101
   * Constructs a definition pane with a given tree view root.
102
   *
103
   * @param file The file of definitions to maintain through the UI.
104
   */
105
  public DefinitionEditor(
106
    final File file,
107
    final TreeTransformer treeTransformer ) {
108
    assert file != null;
109
    assert treeTransformer != null;
110
111
    mFile = file;
112
    mTreeTransformer = treeTransformer;
113
114
    mTreeView.setContextMenu( createContextMenu() );
115
    mTreeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
116
    mTreeView.focusedProperty().addListener( this::focused );
117
    getSelectionModel().setSelectionMode( MULTIPLE );
118
119
    final var buttonBar = new HBox();
120
    buttonBar.getChildren().addAll(
121
      createButton( "create", e -> createDefinition() ),
122
      createButton( "rename", e -> renameDefinition() ),
123
      createButton( "delete", e -> deleteDefinitions() )
124
    );
125
    buttonBar.setAlignment( CENTER );
126
    buttonBar.setSpacing( UI_CONTROL_SPACING );
127
    setTop( buttonBar );
128
    setCenter( mTreeView );
129
    setAlignment( buttonBar, TOP_CENTER );
130
131
    mEncoding = open( mFile );
132
    updateDefinitions( getDefinitions(), getTreeView().getRoot() );
133
134
    // After the file is opened, watch for changes, not before. Otherwise,
135
    // upon saving, users will be prompted to save a file that hasn't had
136
    // any modifications (from their perspective).
137
    addTreeChangeHandler( event -> {
138
      mModified.set( true );
139
      updateDefinitions( getDefinitions(), getTreeView().getRoot() );
140
    } );
141
  }
142
143
  /**
144
   * Replaces the given list of variable definitions with a flat hierarchy
145
   * of the converted {@link TreeView} root.
146
   *
147
   * @param definitions The definition map to update.
148
   * @param root        The values to flatten then insert into the map.
149
   */
150
  private void updateDefinitions(
151
    final Map<String, String> definitions,
152
    final TreeItem<String> root ) {
153
    definitions.clear();
154
    definitions.putAll( TreeItemMapper.convert( root ) );
155
    Engine.clear();
156
  }
157
158
  /**
159
   * Returns the variable definitions.
160
   *
161
   * @return The definition map.
162
   */
163
  @Override
164
  public Map<String, String> getDefinitions() {
165
    return mDefinitions;
166
  }
167
168
  @Override
169
  public void setText( final String document ) {
170
    final var foster = mTreeTransformer.transform( document );
171
    final var biological = getTreeRoot();
172
173
    for( final var child : foster.getChildren() ) {
174
      biological.getChildren().add( child );
175
    }
176
177
    getTreeView().refresh();
178
  }
179
180
  @Override
181
  public String getText() {
182
    final var result = new StringBuilder( 32768 );
183
184
    try {
185
      result.append( mTreeTransformer.transform( getTreeView().getRoot() ) );
186
187
      final var problem = isTreeWellFormed();
188
      problem.ifPresent( node -> clue( "yaml.error.tree.form", node ) );
189
    } catch( final Exception ex ) {
190
      // Catch errors while checking for a well-formed tree (e.g., stack smash).
191
      // Also catch any transformation exceptions (e.g., Json processing).
192
      clue( ex );
193
    }
194
195
    return result.toString();
196
  }
197
198
  @Override
199
  public File getFile() {
200
    return mFile;
201
  }
202
203
  @Override
204
  public void rename( final File file ) {
205
    mFile = file;
206
  }
207
208
  @Override
209
  public Charset getEncoding() {
210
    return mEncoding;
211
  }
212
213
  @Override
214
  public Node getNode() {
215
    return this;
216
  }
217
218
  @Override
219
  public ReadOnlyBooleanProperty modifiedProperty() {
220
    return mModified;
221
  }
222
223
  @Override
224
  public void clearModifiedProperty() {
225
    mModified.setValue( false );
226
  }
227
228
  private Button createButton(
229
    final String msgKey, final EventHandler<ActionEvent> eventHandler ) {
230
    final var keyPrefix = Constants.ACTION_PREFIX + "definition." + msgKey;
231
    final var button = new Button( get( keyPrefix + ".text" ) );
232
    final var graphic = createGraphic( get( keyPrefix + ".icon" ) );
233
234
    button.setOnAction( eventHandler );
235
    button.setGraphic( graphic );
236
    button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) );
237
238
    return button;
239
  }
240
241
  /**
242
   * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
243
   * is modified. The modifications include: item value changes, item additions,
244
   * and item removals.
245
   * <p>
246
   * Safe to call multiple times; if a handler is already registered, the
247
   * old handler is used.
248
   * </p>
249
   *
250
   * @param handler The handler to call whenever any {@link TreeItem} changes.
251
   */
252
  public void addTreeChangeHandler(
253
    final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
254
    final var root = getTreeView().getRoot();
255
    root.addEventHandler( valueChangedEvent(), handler );
256
    root.addEventHandler( childrenModificationEvent(), handler );
257
  }
258
259
  /**
260
   * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
261
   * well-formed for export. A tree is considered well-formed if the following
262
   * conditions are met:
263
   *
264
   * <ul>
265
   *   <li>The root node contains at least one child node having a leaf.</li>
266
   *   <li>There are no leaf nodes with sibling leaf nodes.</li>
267
   * </ul>
268
   *
269
   * @return {@code null} if the document is well-formed, otherwise the
270
   * problematic child {@link TreeItem}.
271
   */
272
  public Optional<TreeItem<String>> isTreeWellFormed() {
273
    final var root = getTreeView().getRoot();
274
275
    for( final var child : root.getChildren() ) {
276
      final var problemChild = isWellFormed( child );
277
278
      if( child.isLeaf() || problemChild != null ) {
279
        return Optional.ofNullable( problemChild );
280
      }
281
    }
282
283
    return Optional.empty();
284
  }
285
286
  /**
287
   * Determines whether the document is well-formed by ensuring that
288
   * child branches do not contain multiple leaves.
289
   *
290
   * @param item The subtree to check for well-formedness.
291
   * @return {@code null} when the tree is well-formed, otherwise the
292
   * problematic {@link TreeItem}.
293
   */
294
  private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
295
    int childLeafs = 0;
296
    int childBranches = 0;
297
298
    for( final var child : item.getChildren() ) {
299
      if( child.isLeaf() ) {
300
        childLeafs++;
301
      }
302
      else {
303
        childBranches++;
304
      }
305
306
      final var problemChild = isWellFormed( child );
307
308
      if( problemChild != null ) {
309
        return problemChild;
310
      }
311
    }
312
313
    return ((childBranches > 0 && childLeafs == 0) ||
314
      (childBranches == 0 && childLeafs <= 1)) ? null : item;
315
  }
316
317
  @Override
318
  public DefinitionTreeItem<String> findLeafExact( final String text ) {
319
    return getTreeRoot().findLeafExact( text );
320
  }
321
322
  @Override
323
  public DefinitionTreeItem<String> findLeafContains( final String text ) {
324
    return getTreeRoot().findLeafContains( text );
325
  }
326
327
  @Override
328
  public DefinitionTreeItem<String> findLeafContainsNoCase(
329
    final String text ) {
330
    return getTreeRoot().findLeafContainsNoCase( text );
331
  }
332
333
  @Override
334
  public DefinitionTreeItem<String> findLeafStartsWith( final String text ) {
335
    return getTreeRoot().findLeafStartsWith( text );
336
  }
337
338
  public void select( final TreeItem<String> item ) {
339
    getSelectionModel().clearSelection();
340
    getSelectionModel().select( getTreeView().getRow( item ) );
341
  }
342
343
  /**
344
   * Collapses the tree, recursively.
345
   */
346
  public void collapse() {
347
    collapse( getTreeRoot().getChildren() );
348
  }
349
350
  /**
351
   * Collapses the tree, recursively.
352
   *
353
   * @param <T>   The type of tree item to expand (usually String).
354
   * @param nodes The nodes to collapse.
355
   */
356
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
357
    for( final var node : nodes ) {
358
      node.setExpanded( false );
359
      collapse( node.getChildren() );
360
    }
361
  }
362
363
  /**
364
   * @return {@code true} when the user is editing a {@link TreeItem}.
365
   */
366
  private boolean isEditingTreeItem() {
367
    return getTreeView().editingItemProperty().getValue() != null;
368
  }
369
370
  /**
371
   * Changes to edit mode for the selected item.
372
   */
373
  @Override
374
  public void renameDefinition() {
375
    getTreeView().edit( getSelectedItem() );
376
  }
377
378
  /**
379
   * Removes all selected items from the {@link TreeView}.
380
   */
381
  @Override
382
  public void deleteDefinitions() {
383
    for( final var item : getSelectedItems() ) {
384
      final var parent = item.getParent();
385
386
      if( parent != null ) {
387
        parent.getChildren().remove( item );
388
      }
389
    }
390
  }
391
392
  /**
393
   * Deletes the selected item.
394
   */
395
  private void deleteSelectedItem() {
396
    final var c = getSelectedItem();
397
    getSiblings( c ).remove( c );
398
  }
399
400
  private void insertSelectedItem() {
401
    if( getSelectedItem() instanceof DefinitionTreeItem<String> node ) {
402
      if( node.isLeaf() ) {
403
        InsertDefinitionEvent.fire( node );
404
      }
405
    }
406
  }
407
408
  /**
409
   * Adds a new item under the selected item (or root if nothing is selected).
410
   * There are a few conditions to consider: when adding to the root,
411
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
412
   * root must contain two items: a key and a value.
413
   */
414
  @Override
415
  public void createDefinition() {
416
    final var value = createDefinitionTreeItem();
417
    getSelectedItem().getChildren().add( value );
418
    expand( value );
419
    select( value );
420
  }
421
422
  private ContextMenu createContextMenu() {
423
    final var menu = new ContextMenu();
424
    final var items = menu.getItems();
425
426
    addMenuItem( items, ACTION_PREFIX + "definition.create.text" )
427
      .setOnAction( e -> createDefinition() );
428
    addMenuItem( items, ACTION_PREFIX + "definition.rename.text" )
429
      .setOnAction( e -> renameDefinition() );
430
    addMenuItem( items, ACTION_PREFIX + "definition.delete.text" )
431
      .setOnAction( e -> deleteSelectedItem() );
432
    addMenuItem( items, ACTION_PREFIX + "definition.insert.text" )
433
      .setOnAction( e -> insertSelectedItem() );
434
435
    return menu;
436
  }
437
438
  /**
439
   * Executes hot-keys for edits to the definition tree.
440
   *
441
   * @param event Contains the key code of the key that was pressed.
442
   */
443
  private void keyEventFilter( final KeyEvent event ) {
444
    if( !isEditingTreeItem() ) {
445
      switch( event.getCode() ) {
446
        case ENTER -> {
447
          expand( getSelectedItem() );
448
          event.consume();
449
        }
450
451
        case DELETE -> deleteDefinitions();
452
        case INSERT -> createDefinition();
453
454
        case R -> {
455
          if( event.isControlDown() ) {
456
            renameDefinition();
457
          }
458
        }
459
      }
460
461
      for( final var handler : getKeyEventHandlers() ) {
462
        handler.handle( event );
463
      }
464
    }
465
  }
466
467
  /**
468
   * Called when the editor's input focus changes. This will fire an event
469
   * for subscribers.
470
   *
471
   * @param ignored Not used.
472
   * @param o       The old input focus property value.
473
   * @param n       The new input focus property value.
474
   */
475
  private void focused(
476
    final ObservableValue<? extends Boolean> ignored,
477
    final Boolean o,
478
    final Boolean n ) {
479
    if( n != null && n ) {
480
      TextDefinitionFocusEvent.fire( this );
481
    }
482
  }
483
484
  /**
485
   * Adds a menu item to a list of menu items.
486
   *
487
   * @param items    The list of menu items to append to.
488
   * @param labelKey The resource bundle key name for the menu item's label.
489
   * @return The menu item added to the list of menu items.
490
   */
491
  private MenuItem addMenuItem(
492
    final List<MenuItem> items, final String labelKey ) {
493
    final MenuItem menuItem = createMenuItem( labelKey );
494
    items.add( menuItem );
495
    return menuItem;
496
  }
497
498
  private MenuItem createMenuItem( final String labelKey ) {
499
    return new MenuItem( get( labelKey ) );
500
  }
501
502
  /**
503
   * Creates a new {@link TreeItem} that is intended to be the root-level item
504
   * added to the {@link TreeView}. This allows the root item to be
505
   * distinguished from the other items so that reference keys do not include
506
   * "Definition" as part of their name.
507
   *
508
   * @return A new {@link TreeItem}, never {@code null}.
509
   */
510
  private RootTreeItem<String> createRootTreeItem() {
511
    return new RootTreeItem<>( get( "Pane.definition.node.root.title" ) );
512
  }
513
514
  private DefinitionTreeItem<String> createDefinitionTreeItem() {
515
    return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) );
516
  }
517
518
  @Override
519
  public void requestFocus() {
520
    getTreeView().requestFocus();
521
  }
522
523
  /**
524
   * Expands the node to the root, recursively.
525
   *
526
   * @param <T>  The type of tree item to expand (usually String).
527
   * @param node The node to expand.
528
   */
529
  @Override
530
  public <T> void expand( final TreeItem<T> node ) {
531
    if( node != null ) {
532
      expand( node.getParent() );
533
      node.setExpanded( !node.isLeaf() );
534
    }
535
  }
536
537
  /**
538
   * Answers whether there are any definitions in the tree.
539
   *
540
   * @return {@code true} when there are no definitions; {@code false} when
541
   * there's at least one definition.
542
   */
543
  @Override
544
  public boolean isEmpty() {
545
    return getTreeRoot().isEmpty();
546
  }
547
548
  /**
549
   * Returns the actively selected item in the tree.
550
   *
551
   * @return The selected item, or the tree root item if no item is selected.
552
   */
553
  public TreeItem<String> getSelectedItem() {
554
    final var item = getSelectionModel().getSelectedItem();
555
    return item == null ? getTreeRoot() : item;
556
  }
557
558
  /**
559
   * Returns the {@link TreeView} that contains the definition hierarchy.
560
   *
561
   * @return A non-null instance.
562
   */
563
  private TreeView<String> getTreeView() {
564
    return mTreeView;
565
  }
566
567
  /**
568
   * Returns the root of the tree.
569
   *
570
   * @return The first node added to the definition tree.
571
   */
572
  private DefinitionTreeItem<String> getTreeRoot() {
573
    return mTreeRoot;
574
  }
575
576
  private ObservableList<TreeItem<String>> getSiblings(
577
    final TreeItem<String> item ) {
578
    final var root = getTreeView().getRoot();
579
    final var parent = (item == null || item == root) ? root : item.getParent();
580
581
    return parent.getChildren();
582
  }
583
584
  private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
585
    return getTreeView().getSelectionModel();
586
  }
587
588
  /**
589
   * Returns a copy of all the selected items.
590
   *
591
   * @return A list, possibly empty, containing all selected items in the
592
   * {@link TreeView}.
593
   */
594
  private List<TreeItem<String>> getSelectedItems() {
595
    return new ArrayList<>( getSelectionModel().getSelectedItems() );
596
  }
597
598
  private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() {
599
    return mKeyEventHandlers;
600
  }
601
}
1602
A src/main/java/com/keenwrite/editors/definition/DefinitionTreeItem.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import com.keenwrite.util.Diacritics;
5
import javafx.scene.control.TreeItem;
6
7
import java.util.Stack;
8
import java.util.function.BiFunction;
9
10
/**
11
 * Provides behaviour afforded to definition keys and corresponding value.
12
 *
13
 * @param <T> The type of {@link TreeItem} (usually string).
14
 */
15
public class DefinitionTreeItem<T> extends TreeItem<T> {
16
17
  /**
18
   * Constructs a new item with a default value.
19
   *
20
   * @param value Passed up to superclass.
21
   */
22
  public DefinitionTreeItem( final T value ) {
23
    super( value );
24
  }
25
26
  /**
27
   * Finds a leaf starting at the current node with text that matches the given
28
   * value. Search is performed case-sensitively.
29
   *
30
   * @param text The text to match against each leaf in the tree.
31
   * @return The leaf that has a value exactly matching the given text.
32
   */
33
  public DefinitionTreeItem<T> findLeafExact( final String text ) {
34
    return findLeaf( text, DefinitionTreeItem::valueEquals );
35
  }
36
37
  /**
38
   * Finds a leaf starting at the current node with text that matches the given
39
   * value. Search is performed case-sensitively.
40
   *
41
   * @param text The text to match against each leaf in the tree.
42
   * @return The leaf that has a value that contains the given text.
43
   */
44
  public DefinitionTreeItem<T> findLeafContains( final String text ) {
45
    return findLeaf( text, DefinitionTreeItem::valueContains );
46
  }
47
48
  /**
49
   * Finds a leaf starting at the current node with text that matches the given
50
   * value. Search is performed case-insensitively.
51
   *
52
   * @param text The text to match against each leaf in the tree.
53
   * @return The leaf that has a value that contains the given text.
54
   */
55
  public DefinitionTreeItem<T> findLeafContainsNoCase( final String text ) {
56
    return findLeaf( text, DefinitionTreeItem::valueContainsNoCase );
57
  }
58
59
  /**
60
   * Finds a leaf starting at the current node with text that matches the given
61
   * value. Search is performed case-sensitively.
62
   *
63
   * @param text The text to match against each leaf in the tree.
64
   * @return The leaf that has a value that starts with the given text.
65
   */
66
  public DefinitionTreeItem<T> findLeafStartsWith( final String text ) {
67
    return findLeaf( text, DefinitionTreeItem::valueStartsWith );
68
  }
69
70
  /**
71
   * Finds a leaf starting at the current node with text that matches the given
72
   * value.
73
   *
74
   * @param text     The text to match against each leaf in the tree.
75
   * @param findMode What algorithm is used to match the given text.
76
   * @return The leaf that has a value starting with the given text, or {@code
77
   * null} if there was no match found.
78
   */
79
  @SuppressWarnings( "AssignmentUsedAsCondition" )
80
  public DefinitionTreeItem<T> findLeaf(
81
    final String text,
82
    final BiFunction<DefinitionTreeItem<T>, String, Boolean> findMode ) {
83
    final var stack = new Stack<DefinitionTreeItem<T>>();
84
    stack.push( this );
85
86
    // Don't hunt for blank (empty) keys.
87
    boolean found = text.isBlank();
88
89
    while( !found && !stack.isEmpty() ) {
90
      final var node = stack.pop();
91
92
      for( final var child : node.getChildren() ) {
93
        final var result = (DefinitionTreeItem<T>) child;
94
95
        if( result.isLeaf() ) {
96
          if( found = findMode.apply( result, text ) ) {
97
            return result;
98
          }
99
        }
100
        else {
101
          stack.push( result );
102
        }
103
      }
104
    }
105
106
    return null;
107
  }
108
109
  /**
110
   * Returns true if this node is a leaf and its value equals the given text.
111
   *
112
   * @param s The text to compare against the node value.
113
   * @return true Node is a leaf and its value equals the given value.
114
   */
115
  private boolean valueEquals( final String s ) {
116
    return isLeaf() && getValue().equals( s );
117
  }
118
119
  /**
120
   * Removes diacritic characters from the given definition item.
121
   *
122
   * @param item The {@link DefinitionTreeItem} to strip of diacritics.
123
   * @param <T>  The type of item contained by {@link DefinitionTreeItem}s.
124
   * @return The given item, without any accented characters.
125
   */
126
  private static <T> String removeAccents( final DefinitionTreeItem<T> item ) {
127
    return Diacritics.remove( item.getValue().toString() );
128
  }
129
130
  /**
131
   * Returns true if this node is a leaf and its value contains the given text.
132
   *
133
   * @param s The text to compare against the node value.
134
   * @return true Node is a leaf and its value contains the given value.
135
   */
136
  private boolean valueContains( final String s ) {
137
    return isLeaf() && removeAccents( this ).contains( s );
138
  }
139
140
  /**
141
   * Returns true if this node is a leaf and its value contains the given text.
142
   *
143
   * @param s The text to compare against the node value.
144
   * @return true Node is a leaf and its value contains the given value.
145
   */
146
  private boolean valueContainsNoCase( final String s ) {
147
    return isLeaf() && removeAccents( this )
148
      .toLowerCase()
149
      .contains( s.toLowerCase() );
150
  }
151
152
  /**
153
   * Returns true if this node is a leaf and its value starts with the given
154
   * text.
155
   *
156
   * @param s The text to compare against the node value.
157
   * @return true Node is a leaf and its value starts with the given value.
158
   */
159
  private boolean valueStartsWith( final String s ) {
160
    return isLeaf() && removeAccents( this ).startsWith( s );
161
  }
162
163
  /**
164
   * Returns the path for this node, with nodes made distinct using the
165
   * separator character. This uses two loops: one for pushing nodes onto a
166
   * stack and one for popping them off to create the path in desired order.
167
   *
168
   * @return A non-null string, possibly empty.
169
   */
170
  public String toPath() {
171
    return TreeItemMapper.toPath( getParent() );
172
  }
173
174
  /**
175
   * Answers whether there are any definitions in this tree.
176
   *
177
   * @return {@code true} when there are no definitions in the tree; {@code
178
   * false} when there is at least one definition present.
179
   */
180
  public boolean isEmpty() {
181
    return getChildren().isEmpty();
182
  }
183
}
1184
A src/main/java/com/keenwrite/editors/definition/RootTreeItem.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import javafx.scene.control.TreeItem;
5
import javafx.scene.control.TreeView;
6
7
/**
8
 * Marker interface for top-most {@link TreeItem}. This class allows the
9
 * {@link TreeItemMapper} to ignore the topmost definition. Such contortions
10
 * are necessary because {@link TreeView} requires a root item that isn't part
11
 * of the user's definition file.
12
 * <p>
13
 * Another approach would be to associate object pairs per {@link TreeItem},
14
 * but that would be a waste of memory since the only "exception" case is
15
 * the root {@link TreeItem}.
16
 * </p>
17
 *
18
 * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}.
19
 */
20
public final class RootTreeItem<T> extends DefinitionTreeItem<T> {
21
  /**
22
   * Default constructor, calls the superclass, no other behaviour.
23
   *
24
   * @param value The {@link TreeItem} node name to construct the superclass.
25
   * @see TreeItemMapper#convert(TreeItem) for details on how this
26
   * class is used.
27
   */
28
  public RootTreeItem( final T value ) {
29
    super( value );
30
  }
31
}
132
A src/main/java/com/keenwrite/editors/definition/TreeItemMapper.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import com.fasterxml.jackson.databind.JsonNode;
5
import javafx.scene.control.TreeItem;
6
7
import java.util.HashMap;
8
import java.util.Iterator;
9
import java.util.Map;
10
import java.util.Stack;
11
12
/**
13
 * Given a {@link TreeItem}, this will generate a flat map with all the
14
 * keys using a dot-separated notation to represent the tree's hierarchy.
15
 *
16
 * <ol>
17
 *   <li>Load YAML file into {@link JsonNode} hierarchy.</li>
18
 *   <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li>
19
 *   <li>Convert the {@link TreeItem} hierarchy into a flat map.</li>
20
 * </ol>
21
 */
22
public final class TreeItemMapper {
23
  /**
24
   * Key name hierarchy separator (i.e., the dots in {@code root.node.var}).
25
   */
26
  public static final String SEPARATOR = ".";
27
28
  /**
29
   * Default buffer length for key names that should be large enough to
30
   * avoid reallocating memory to increase the {@link StringBuilder}'s
31
   * buffer.
32
   */
33
  public static final int DEFAULT_KEY_LENGTH = 64;
34
35
  /**
36
   * In-order traversal of a {@link TreeItem} hierarchy, exposing each item
37
   * as a consecutive list.
38
   */
39
  private static final class TreeIterator
40
    implements Iterator<TreeItem<String>> {
41
    private final Stack<TreeItem<String>> mStack = new Stack<>();
42
43
    public TreeIterator( final TreeItem<String> root ) {
44
      if( root != null ) {
45
        mStack.push( root );
46
      }
47
    }
48
49
    @Override
50
    public boolean hasNext() {
51
      return !mStack.isEmpty();
52
    }
53
54
    @Override
55
    public TreeItem<String> next() {
56
      final var next = mStack.pop();
57
      next.getChildren().forEach( mStack::push );
58
59
      return next;
60
    }
61
  }
62
63
  /**
64
   * Iterate over a given root node (at any level of the tree) and process each
65
   * leaf node into a flat map.
66
   *
67
   * @param root The topmost item in the tree.
68
   */
69
  public static Map<String, String> convert( final TreeItem<String> root ) {
70
    final var map = new HashMap<String, String>();
71
72
    new TreeIterator( root ).forEachRemaining( item -> {
73
      if( item.isLeaf() && item.getParent() != null ) {
74
        map.put( toPath( item.getParent() ), item.getValue() );
75
      }
76
    } );
77
78
    return map;
79
  }
80
81
  /**
82
   * For a given node, this will ascend the tree to generate a key name
83
   * that is associated with the leaf node's value.
84
   *
85
   * @param node Ascendants represent the key to this node's value.
86
   * @param <T>  Data type that the {@link TreeItem} contains.
87
   * @return The string representation of the node's unique key.
88
   */
89
  public static <T> String toPath( TreeItem<T> node ) {
90
    final var key = new StringBuilder( DEFAULT_KEY_LENGTH );
91
    final var stack = new Stack<TreeItem<T>>();
92
93
    while( node != null && !(node instanceof RootTreeItem) ) {
94
      stack.push( node );
95
      node = node.getParent();
96
    }
97
98
    // Gets set at end of first iteration (to avoid an if condition).
99
    var separator = "";
100
101
    while( !stack.empty() ) {
102
      final T subkey = stack.pop().getValue();
103
      key.append( separator );
104
      key.append( subkey );
105
      separator = SEPARATOR;
106
    }
107
108
    return key.toString();
109
  }
110
}
1111
A src/main/java/com/keenwrite/editors/definition/TreeTransformer.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.definition;
3
4
import javafx.scene.control.TreeItem;
5
6
/**
7
 * Responsible for converting an object hierarchy into a {@link TreeItem}
8
 * hierarchy.
9
 */
10
public interface TreeTransformer {
11
  /**
12
   * Adapts the document produced by the given parser into a {@link TreeItem}
13
   * object that can be presented to the user within a GUI. The root of the
14
   * tree must be merged by the view layer.
15
   *
16
   * @param document The document to transform into a viewable hierarchy.
17
   */
18
  TreeItem<String> transform( String document );
19
20
  /**
21
   * Exports the given root node to the given path.
22
   *
23
   * @param root The root node to export.
24
   */
25
  String transform( TreeItem<String> root );
26
}
127
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 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.markdown;
3
4
import com.vladsch.flexmark.ast.Link;
5
6
/**
7
 * Represents the model for a hyperlink: text, url, and title.
8
 */
9
public final class HyperlinkModel {
10
11
  private String text;
12
  private String url;
13
  private String title;
14
15
  /**
16
   * Constructs a new hyperlink model in Markdown format by default with no
17
   * title (i.e., tooltip).
18
   *
19
   * @param text The hyperlink text displayed (e.g., displayed to the user).
20
   * @param url  The destination URL (e.g., when clicked).
21
   */
22
  public HyperlinkModel( final String text, final String url ) {
23
    this( text, url, null );
24
  }
25
26
  /**
27
   * Constructs a new hyperlink model for the given AST link.
28
   *
29
   * @param link A Markdown link.
30
   */
31
  public HyperlinkModel( final Link link ) {
32
    this(
33
      link.getText().toString(),
34
      link.getUrl().toString(),
35
      link.getTitle().toString()
36
    );
37
  }
38
39
  /**
40
   * Constructs a new hyperlink model in Markdown format by default.
41
   *
42
   * @param text  The hyperlink text displayed (e.g., displayed to the user).
43
   * @param url   The destination URL (e.g., when clicked).
44
   * @param title The hyperlink title (e.g., shown as a tooltip).
45
   */
46
  public HyperlinkModel(
47
    final String text, final String url, final String title ) {
48
    setText( text );
49
    setUrl( url );
50
    setTitle( title );
51
  }
52
53
  /**
54
   * Returns the string in Markdown format by default.
55
   *
56
   * @return A Markdown version of the hyperlink.
57
   */
58
  @Override
59
  public String toString() {
60
    String format = "%s%s%s";
61
62
    if( hasText() ) {
63
      format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)");
64
    }
65
66
    // Becomes ""+URL+"" if no text is set.
67
    // Becomes [TITLE]+(URL)+"" if no title is set.
68
    // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
69
    return String.format( format, getText(), getUrl(), getTitle() );
70
  }
71
72
  public void setText( final String text ) {
73
    this.text = sanitize( text );
74
  }
75
76
  public void setUrl( final String url ) {
77
    this.url = sanitize( url );
78
  }
79
80
  public void setTitle( final String title ) {
81
    this.title = sanitize( title );
82
  }
83
84
  /**
85
   * Answers whether text has been set for the hyperlink.
86
   *
87
   * @return true This is a text link.
88
   */
89
  public boolean hasText() {
90
    return !getText().isEmpty();
91
  }
92
93
  /**
94
   * Answers whether a title (tooltip) has been set for the hyperlink.
95
   *
96
   * @return true There is a title.
97
   */
98
  public boolean hasTitle() {
99
    return !getTitle().isEmpty();
100
  }
101
102
  public String getText() {
103
    return this.text;
104
  }
105
106
  public String getUrl() {
107
    return this.url;
108
  }
109
110
  public String getTitle() {
111
    return this.title;
112
  }
113
114
  private String sanitize( final String s ) {
115
    return s == null ? "" : s;
116
  }
117
}
1118
A src/main/java/com/keenwrite/editors/markdown/LinkVisitor.java
1
/*
2
 * Copyright 2020-2021 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.editors.markdown;
29
30
import com.vladsch.flexmark.ast.Link;
31
import com.vladsch.flexmark.util.ast.Node;
32
import com.vladsch.flexmark.util.ast.NodeVisitor;
33
import com.vladsch.flexmark.util.ast.VisitHandler;
34
35
/**
36
 * Responsible for extracting a hyperlink from the document so that the user
37
 * can edit the link within a dialog.
38
 */
39
public final class LinkVisitor {
40
41
  private NodeVisitor mVisitor;
42
  private Link mLink;
43
  private final int mOffset;
44
45
  /**
46
   * Creates a hyperlink given an offset into a paragraph and the Markdown AST
47
   * link node.
48
   *
49
   * @param index Index into the paragraph that indicates the hyperlink to
50
   *              change.
51
   */
52
  public LinkVisitor( final int index ) {
53
    mOffset = index;
54
  }
55
56
  public Link process( final Node root ) {
57
    getVisitor().visit( root );
58
    return getLink();
59
  }
60
61
  /**
62
   * @param link Not null.
63
   */
64
  private void visit( final Link link ) {
65
    final int began = link.getStartOffset();
66
    final int ended = link.getEndOffset();
67
    final int index = getOffset();
68
69
    if( index >= began && index <= ended ) {
70
      setLink( link );
71
    }
72
  }
73
74
  private synchronized NodeVisitor getVisitor() {
75
    if( mVisitor == null ) {
76
      mVisitor = createVisitor();
77
    }
78
79
    return mVisitor;
80
  }
81
82
  protected NodeVisitor createVisitor() {
83
    return new NodeVisitor(
84
      new VisitHandler<>( Link.class, LinkVisitor.this::visit ) );
85
  }
86
87
  private Link getLink() {
88
    return mLink;
89
  }
90
91
  private void setLink( final Link link ) {
92
    mLink = link;
93
  }
94
95
  public int getOffset() {
96
    return mOffset;
97
  }
98
}
199
A src/main/java/com/keenwrite/editors/markdown/MarkdownEditor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors.markdown;
3
4
import com.keenwrite.constants.Constants;
5
import com.keenwrite.editors.TextEditor;
6
import com.keenwrite.editors.common.Caret;
7
import com.keenwrite.events.TextEditorFocusEvent;
8
import com.keenwrite.io.MediaType;
9
import com.keenwrite.preferences.LocaleProperty;
10
import com.keenwrite.preferences.Workspace;
11
import com.keenwrite.processors.markdown.extensions.CaretExtension;
12
import javafx.beans.binding.Bindings;
13
import javafx.beans.property.*;
14
import javafx.beans.value.ChangeListener;
15
import javafx.event.Event;
16
import javafx.scene.Node;
17
import javafx.scene.control.IndexRange;
18
import javafx.scene.input.KeyEvent;
19
import javafx.scene.layout.BorderPane;
20
import org.fxmisc.flowless.VirtualizedScrollPane;
21
import org.fxmisc.richtext.StyleClassedTextArea;
22
import org.fxmisc.richtext.model.StyleSpans;
23
import org.fxmisc.undo.UndoManager;
24
import org.fxmisc.wellbehaved.event.EventPattern;
25
import org.fxmisc.wellbehaved.event.Nodes;
26
27
import java.io.File;
28
import java.nio.charset.Charset;
29
import java.text.BreakIterator;
30
import java.text.MessageFormat;
31
import java.util.*;
32
import java.util.function.Consumer;
33
import java.util.function.Supplier;
34
import java.util.regex.Pattern;
35
36
import static com.keenwrite.MainApp.keyDown;
37
import static com.keenwrite.constants.Constants.*;
38
import static com.keenwrite.events.StatusEvent.clue;
39
import static com.keenwrite.io.MediaType.TEXT_MARKDOWN;
40
import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN;
41
import static com.keenwrite.preferences.AppKeys.*;
42
import static java.lang.Character.isWhitespace;
43
import static java.lang.String.format;
44
import static java.util.Collections.singletonList;
45
import static javafx.application.Platform.runLater;
46
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.ALWAYS;
47
import static javafx.scene.input.KeyCode.*;
48
import static javafx.scene.input.KeyCombination.*;
49
import static org.apache.commons.lang3.StringUtils.stripEnd;
50
import static org.apache.commons.lang3.StringUtils.stripStart;
51
import static org.fxmisc.richtext.Caret.CaretVisibility.ON;
52
import static org.fxmisc.richtext.model.StyleSpans.singleton;
53
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
54
import static org.fxmisc.wellbehaved.event.InputMap.consume;
55
56
/**
57
 * Responsible for editing Markdown documents.
58
 */
59
public final class MarkdownEditor extends BorderPane implements TextEditor {
60
  /**
61
   * Regular expression that matches the type of markup block. This is used
62
   * when Enter is pressed to continue the block environment.
63
   */
64
  private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile(
65
    "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
66
67
  private final Workspace mWorkspace;
68
69
  /**
70
   * The text editor.
71
   */
72
  private final StyleClassedTextArea mTextArea =
73
    new StyleClassedTextArea( false );
74
75
  /**
76
   * Wraps the text editor in scrollbars.
77
   */
78
  private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
79
    new VirtualizedScrollPane<>( mTextArea );
80
81
  /**
82
   * Tracks where the caret is located in this document. This offers observable
83
   * properties for caret position changes.
84
   */
85
  private final Caret mCaret = createCaret( mTextArea );
86
87
  /**
88
   * File being edited by this editor instance.
89
   */
90
  private File mFile;
91
92
  /**
93
   * Set to {@code true} upon text or caret position changes. Value is {@code
94
   * false} by default.
95
   */
96
  private final BooleanProperty mDirty = new SimpleBooleanProperty();
97
98
  /**
99
   * Opened file's character encoding, or {@link Constants#DEFAULT_CHARSET} if
100
   * either no encoding could be determined or this is a new (empty) file.
101
   */
102
  private final Charset mEncoding;
103
104
  /**
105
   * Tracks whether the in-memory definitions have changed with respect to the
106
   * persisted definitions.
107
   */
108
  private final BooleanProperty mModified = new SimpleBooleanProperty();
109
110
  public MarkdownEditor( final File file, final Workspace workspace ) {
111
    mEncoding = open( mFile = file );
112
    mWorkspace = workspace;
113
114
    initTextArea( mTextArea );
115
    initStyle( mTextArea );
116
    initScrollPane( mScrollPane );
117
    initHotKeys();
118
    initUndoManager();
119
  }
120
121
  private void initTextArea( final StyleClassedTextArea textArea ) {
122
    textArea.setShowCaret( ON );
123
    textArea.setWrapText( true );
124
    textArea.requestFollowCaret();
125
    textArea.moveTo( 0 );
126
127
    textArea.textProperty().addListener( ( c, o, n ) -> {
128
      // Fire, regardless of whether the caret position has changed.
129
      mDirty.set( false );
130
131
      // Prevent the subsequent caret position change from raising dirty bits.
132
      mDirty.set( true );
133
    } );
134
135
    textArea.caretPositionProperty().addListener( ( c, o, n ) -> {
136
      // Fire when the caret position has changed and the text has not.
137
      mDirty.set( true );
138
      mDirty.set( false );
139
    } );
140
141
    textArea.focusedProperty().addListener( ( c, o, n ) -> {
142
      if( n != null && n ) {
143
        TextEditorFocusEvent.fire( this );
144
      }
145
    } );
146
  }
147
148
  private void initStyle( final StyleClassedTextArea textArea ) {
149
    textArea.getStyleClass().add( "markdown" );
150
151
    final var stylesheets = textArea.getStylesheets();
152
    stylesheets.add( getStylesheetPath( getLocale() ) );
153
154
    localeProperty().addListener( ( c, o, n ) -> {
155
      if( n != null ) {
156
        stylesheets.clear();
157
        stylesheets.add( getStylesheetPath( getLocale() ) );
158
      }
159
    } );
160
161
    fontNameProperty().addListener(
162
      ( c, o, n ) ->
163
        setFont( mTextArea, getFontName(), getFontSize() )
164
    );
165
166
    fontSizeProperty().addListener(
167
      ( c, o, n ) ->
168
        setFont( mTextArea, getFontName(), getFontSize() )
169
    );
170
171
    setFont( mTextArea, getFontName(), getFontSize() );
172
  }
173
174
  private void initScrollPane(
175
    final VirtualizedScrollPane<StyleClassedTextArea> scrollpane ) {
176
    scrollpane.setVbarPolicy( ALWAYS );
177
    setCenter( scrollpane );
178
  }
179
180
  private void initHotKeys() {
181
    addEventListener( keyPressed( ENTER ), this::onEnterPressed );
182
    addEventListener( keyPressed( X, CONTROL_DOWN ), this::cut );
183
    addEventListener( keyPressed( TAB ), this::tab );
184
    addEventListener( keyPressed( TAB, SHIFT_DOWN ), this::untab );
185
  }
186
187
  private void initUndoManager() {
188
    final var undoManager = getUndoManager();
189
    final var markedPosition = undoManager.atMarkedPositionProperty();
190
191
    undoManager.forgetHistory();
192
    undoManager.mark();
193
    mModified.bind( Bindings.not( markedPosition ) );
194
  }
195
196
  @Override
197
  public void moveTo( final int offset ) {
198
    assert 0 <= offset && offset <= mTextArea.getLength();
199
200
    mTextArea.moveTo( offset );
201
    mTextArea.requestFollowCaret();
202
  }
203
204
  /**
205
   * Delegate the focus request to the text area itself.
206
   */
207
  @Override
208
  public void requestFocus() {
209
    mTextArea.requestFocus();
210
  }
211
212
  @Override
213
  public void setText( final String text ) {
214
    mTextArea.clear();
215
    mTextArea.appendText( text );
216
    mTextArea.getUndoManager().mark();
217
  }
218
219
  @Override
220
  public String getText() {
221
    return mTextArea.getText();
222
  }
223
224
  @Override
225
  public Charset getEncoding() {
226
    return mEncoding;
227
  }
228
229
  @Override
230
  public File getFile() {
231
    return mFile;
232
  }
233
234
  @Override
235
  public void rename( final File file ) {
236
    mFile = file;
237
  }
238
239
  @Override
240
  public void undo() {
241
    final var manager = getUndoManager();
242
    xxdo( manager::isUndoAvailable, manager::undo, "Main.status.error.undo" );
243
  }
244
245
  @Override
246
  public void redo() {
247
    final var manager = getUndoManager();
248
    xxdo( manager::isRedoAvailable, manager::redo, "Main.status.error.redo" );
249
  }
250
251
  /**
252
   * Performs an undo or redo action, if possible, otherwise displays an error
253
   * message to the user.
254
   *
255
   * @param ready  Answers whether the action can be executed.
256
   * @param action The action to execute.
257
   * @param key    The informational message key having a value to display if
258
   *               the {@link Supplier} is not ready.
259
   */
260
  private void xxdo(
261
    final Supplier<Boolean> ready, final Runnable action, final String key ) {
262
    if( ready.get() ) {
263
      action.run();
264
    }
265
    else {
266
      clue( key );
267
    }
268
  }
269
270
  @Override
271
  public void cut() {
272
    final var selected = mTextArea.getSelectedText();
273
274
    // Emulate selecting the current line by firing Home then Shift+Down Arrow.
275
    if( selected == null || selected.isEmpty() ) {
276
      // Note: mTextArea.selectLine() does not select empty lines.
277
      mTextArea.fireEvent( keyDown( HOME, false ) );
278
      mTextArea.fireEvent( keyDown( DOWN, true ) );
279
    }
280
281
    mTextArea.cut();
282
  }
283
284
  @Override
285
  public void copy() {
286
    mTextArea.copy();
287
  }
288
289
  @Override
290
  public void paste() {
291
    mTextArea.paste();
292
  }
293
294
  @Override
295
  public void selectAll() {
296
    mTextArea.selectAll();
297
  }
298
299
  @Override
300
  public void bold() {
301
    enwrap( "**" );
302
  }
303
304
  @Override
305
  public void italic() {
306
    enwrap( "*" );
307
  }
308
309
  @Override
310
  public void monospace() {
311
    enwrap( "`" );
312
  }
313
314
  @Override
315
  public void superscript() {
316
    enwrap( "^" );
317
  }
318
319
  @Override
320
  public void subscript() {
321
    enwrap( "~" );
322
  }
323
324
  @Override
325
  public void strikethrough() {
326
    enwrap( "~~" );
327
  }
328
329
  @Override
330
  public void blockquote() {
331
    block( "> " );
332
  }
333
334
  @Override
335
  public void code() {
336
    enwrap( "`" );
337
  }
338
339
  @Override
340
  public void fencedCodeBlock() {
341
    enwrap( "\n\n```\n", "\n```\n\n" );
342
  }
343
344
  @Override
345
  public void heading( final int level ) {
346
    final var hashes = new String( new char[ level ] ).replace( "\0", "#" );
347
    block( format( "%s ", hashes ) );
348
  }
349
350
  @Override
351
  public void unorderedList() {
352
    block( "* " );
353
  }
354
355
  @Override
356
  public void orderedList() {
357
    block( "1. " );
358
  }
359
360
  @Override
361
  public void horizontalRule() {
362
    block( format( "---%n%n" ) );
363
  }
364
365
  @Override
366
  public Node getNode() {
367
    return this;
368
  }
369
370
  @Override
371
  public ReadOnlyBooleanProperty modifiedProperty() {
372
    return mModified;
373
  }
374
375
  @Override
376
  public void clearModifiedProperty() {
377
    getUndoManager().mark();
378
  }
379
380
  @Override
381
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
382
    return mScrollPane;
383
  }
384
385
  @Override
386
  public StyleClassedTextArea getTextArea() {
387
    return mTextArea;
388
  }
389
390
  private final Map<String, IndexRange> mStyles = new HashMap<>();
391
392
  @Override
393
  public void stylize( final IndexRange range, final String style ) {
394
    final var began = range.getStart();
395
    final var ended = range.getEnd() + 1;
396
397
    assert 0 <= began && began <= ended;
398
    assert style != null;
399
400
    // TODO: Ensure spell check and find highlights can coexist.
401
//    final var spans = mTextArea.getStyleSpans( range );
402
//    System.out.println( "SPANS: " + spans );
403
404
//    final var spans = mTextArea.getStyleSpans( range );
405
//    mTextArea.setStyleSpans( began, merge( spans, range.getLength(), style
406
//    ) );
407
408
//    final var builder = new StyleSpansBuilder<Collection<String>>();
409
//    builder.add( singleton( style ), range.getLength() + 1 );
410
//    mTextArea.setStyleSpans( began, builder.create() );
411
412
//    final var s = mTextArea.getStyleSpans( began, ended );
413
//    System.out.println( "STYLES: " +s );
414
415
    mStyles.put( style, range );
416
    mTextArea.setStyleClass( began, ended, style );
417
418
    // Ensure that whenever the user interacts with the text that the found
419
    // word will have its highlighting removed. The handler removes itself.
420
    // This won't remove the highlighting if the caret position moves by mouse.
421
    final var handler = mTextArea.getOnKeyPressed();
422
    mTextArea.setOnKeyPressed( event -> {
423
      mTextArea.setOnKeyPressed( handler );
424
      unstylize( style );
425
    } );
426
427
    //mTextArea.setStyleSpans(began, ended, s);
428
  }
429
430
  private static StyleSpans<Collection<String>> merge(
431
    StyleSpans<Collection<String>> spans, int len, String style ) {
432
    spans = spans.overlay(
433
      singleton( singletonList( style ), len ),
434
      ( bottomSpan, list ) -> {
435
        final List<String> l =
436
          new ArrayList<>( bottomSpan.size() + list.size() );
437
        l.addAll( bottomSpan );
438
        l.addAll( list );
439
        return l;
440
      } );
441
442
    return spans;
443
  }
444
445
  @Override
446
  public void unstylize( final String style ) {
447
    final var indexes = mStyles.remove( style );
448
    if( indexes != null ) {
449
      mTextArea.clearStyle( indexes.getStart(), indexes.getEnd() + 1 );
450
    }
451
  }
452
453
  @Override
454
  public Caret getCaret() {
455
    return mCaret;
456
  }
457
458
  /**
459
   * A {@link Caret} instance is not directly coupled ot the GUI because
460
   * document processing does not always require interactive status bar
461
   * updates. This can happen when processing from the command-line. However,
462
   * the processors need the {@link Caret} instance to inject the caret
463
   * position into the document. Making the {@link CaretExtension} optional
464
   * would require more effort than using a {@link Caret} model that is
465
   * decoupled from GUI widgets.
466
   *
467
   * @param editor The text editor containing caret position information.
468
   * @return An instance of {@link Caret} that tracks the GUI caret position.
469
   */
470
  private Caret createCaret( final StyleClassedTextArea editor ) {
471
    return Caret
472
      .builder()
473
      .with( Caret.Mutator::setParagraph,
474
             () -> editor.currentParagraphProperty().getValue() )
475
      .with( Caret.Mutator::setParagraphs,
476
             () -> editor.getParagraphs().size() )
477
      .with( Caret.Mutator::setParaOffset,
478
             () -> editor.caretColumnProperty().getValue() )
479
      .with( Caret.Mutator::setTextOffset,
480
             () -> editor.caretPositionProperty().getValue() )
481
      .with( Caret.Mutator::setTextLength,
482
             () -> editor.lengthProperty().getValue() )
483
      .build();
484
  }
485
486
  /**
487
   * This method adds listeners to editor events.
488
   *
489
   * @param <T>      The event type.
490
   * @param <U>      The consumer type for the given event type.
491
   * @param event    The event of interest.
492
   * @param consumer The method to call when the event happens.
493
   */
494
  public <T extends Event, U extends T> void addEventListener(
495
    final EventPattern<? super T, ? extends U> event,
496
    final Consumer<? super U> consumer ) {
497
    Nodes.addInputMap( mTextArea, consume( event, consumer ) );
498
  }
499
500
  private void onEnterPressed( final KeyEvent ignored ) {
501
    final var currentLine = getCaretParagraph();
502
    final var matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
503
504
    // By default, insert a new line by itself.
505
    String newText = NEWLINE;
506
507
    // If the pattern was matched then determine what block type to continue.
508
    if( matcher.matches() ) {
509
      if( matcher.group( 2 ).isEmpty() ) {
510
        final var pos = mTextArea.getCaretPosition();
511
        mTextArea.selectRange( pos - currentLine.length(), pos );
512
      }
513
      else {
514
        // Indent the new line with the same whitespace characters and
515
        // list markers as current line. This ensures that the indentation
516
        // is propagated.
517
        newText = newText.concat( matcher.group( 1 ) );
518
      }
519
    }
520
521
    mTextArea.replaceSelection( newText );
522
    mTextArea.requestFollowCaret();
523
  }
524
525
  private void cut( final KeyEvent event ) {
526
    cut();
527
  }
528
529
  private void tab( final KeyEvent event ) {
530
    final var range = mTextArea.selectionProperty().getValue();
531
    final var sb = new StringBuilder( 1024 );
532
533
    if( range.getLength() > 0 ) {
534
      final var selection = mTextArea.getSelectedText();
535
536
      selection.lines().forEach(
537
        l -> sb.append( "\t" ).append( l ).append( NEWLINE )
538
      );
539
    }
540
    else {
541
      sb.append( "\t" );
542
    }
543
544
    mTextArea.replaceSelection( sb.toString() );
545
  }
546
547
  private void untab( final KeyEvent event ) {
548
    final var range = mTextArea.selectionProperty().getValue();
549
550
    if( range.getLength() > 0 ) {
551
      final var selection = mTextArea.getSelectedText();
552
      final var sb = new StringBuilder( selection.length() );
553
554
      selection.lines().forEach(
555
        l -> sb.append( l.startsWith( "\t" ) ? l.substring( 1 ) : l )
556
               .append( NEWLINE )
557
      );
558
559
      mTextArea.replaceSelection( sb.toString() );
560
    }
561
    else {
562
      final var p = getCaretParagraph();
563
564
      if( p.startsWith( "\t" ) ) {
565
        mTextArea.selectParagraph();
566
        mTextArea.replaceSelection( p.substring( 1 ) );
567
      }
568
    }
569
  }
570
571
  /**
572
   * Observers may listen for changes to the property returned from this method
573
   * to receive notifications when either the text or caret have changed. This
574
   * should not be used to track whether the text has been modified.
575
   */
576
  public void addDirtyListener( ChangeListener<Boolean> listener ) {
577
    mDirty.addListener( listener );
578
  }
579
580
  /**
581
   * Surrounds the selected text or word under the caret in Markdown markup.
582
   *
583
   * @param token The beginning and ending token for enclosing the text.
584
   */
585
  private void enwrap( final String token ) {
586
    enwrap( token, token );
587
  }
588
589
  /**
590
   * Surrounds the selected text or word under the caret in Markdown markup.
591
   *
592
   * @param began The beginning token for enclosing the text.
593
   * @param ended The ending token for enclosing the text.
594
   */
595
  private void enwrap( final String began, String ended ) {
596
    // Ensure selected text takes precedence over the word at caret position.
597
    final var selected = mTextArea.selectionProperty().getValue();
598
    final var range = selected.getLength() == 0
599
      ? getCaretWord()
600
      : selected;
601
    String text = mTextArea.getText( range );
602
603
    int length = range.getLength();
604
    text = stripStart( text, null );
605
    final int beganIndex = range.getStart() + length - text.length();
606
607
    length = text.length();
608
    text = stripEnd( text, null );
609
    final int endedIndex = range.getEnd() - (length - text.length());
610
611
    mTextArea.replaceText( beganIndex, endedIndex, began + text + ended );
612
  }
613
614
  /**
615
   * Inserts the given block-level markup at the current caret position
616
   * within the document. This will prepend two blank lines to ensure that
617
   * the block element begins at the start of a new line.
618
   *
619
   * @param markup The text to insert at the caret.
620
   */
621
  private void block( final String markup ) {
622
    final int pos = mTextArea.getCaretPosition();
623
    mTextArea.insertText( pos, format( "%n%n%s", markup ) );
624
  }
625
626
  /**
627
   * Returns the caret position within the current paragraph.
628
   *
629
   * @return A value from 0 to the length of the current paragraph.
630
   */
631
  private int getCaretColumn() {
632
    return mTextArea.getCaretColumn();
633
  }
634
635
  @Override
636
  public IndexRange getCaretWord() {
637
    final var paragraph = getCaretParagraph()
638
      .replaceAll( "---", "   " )
639
      .replaceAll( "--", "  " )
640
      .replaceAll( "[\\[\\]{}()]", " " );
641
    final var length = paragraph.length();
642
    final var column = getCaretColumn();
643
644
    var began = column;
645
    var ended = column;
646
647
    while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
648
      began--;
649
    }
650
651
    while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
652
      ended++;
653
    }
654
655
    final var iterator = BreakIterator.getWordInstance();
656
    iterator.setText( paragraph );
657
658
    while( began < length && iterator.isBoundary( began + 1 ) ) {
659
      began++;
660
    }
661
662
    while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
663
      ended--;
664
    }
665
666
    final var offset = getCaretDocumentOffset( column );
667
668
    return IndexRange.normalize( began + offset, ended + offset );
669
  }
670
671
  private int getCaretDocumentOffset( final int column ) {
672
    return mTextArea.getCaretPosition() - column;
673
  }
674
675
  /**
676
   * Returns the index of the paragraph where the caret resides.
677
   *
678
   * @return A number greater than or equal to 0.
679
   */
680
  private int getCurrentParagraph() {
681
    return mTextArea.getCurrentParagraph();
682
  }
683
684
  /**
685
   * Returns the text for the paragraph that contains the caret.
686
   *
687
   * @return A non-null string, possibly empty.
688
   */
689
  private String getCaretParagraph() {
690
    return getText( getCurrentParagraph() );
691
  }
692
693
  @Override
694
  public String getText( final int paragraph ) {
695
    return mTextArea.getText( paragraph );
696
  }
697
698
  @Override
699
  public String getText( final IndexRange indexes )
700
    throws IndexOutOfBoundsException {
701
    return mTextArea.getText( indexes.getStart(), indexes.getEnd() );
702
  }
703
704
  @Override
705
  public void replaceText( final IndexRange indexes, final String s ) {
706
    mTextArea.replaceText( indexes, s );
707
  }
708
709
  private UndoManager<?> getUndoManager() {
710
    return mTextArea.getUndoManager();
711
  }
712
713
  /**
714
   * Returns the path to a {@link Locale}-specific stylesheet.
715
   *
716
   * @return A non-null string to inject into the HTML document head.
717
   */
718
  private static String getStylesheetPath( final Locale locale ) {
719
    return MessageFormat.format(
720
      sSettings.getSetting( STYLESHEET_MARKDOWN_LOCALE, "" ),
721
      locale.getLanguage(),
722
      locale.getScript(),
723
      locale.getCountry()
724
    );
725
  }
726
727
  private Locale getLocale() {
728
    return localeProperty().toLocale();
729
  }
730
731
  private LocaleProperty localeProperty() {
732
    return mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
733
  }
734
735
  /**
736
   * Sets the font family name and font size at the same time. When the
737
   * workspace is loaded, the default font values are changed, which results
738
   * in this method being called.
739
   *
740
   * @param area   Change the font settings for this text area.
741
   * @param name   New font family name to apply.
742
   * @param points New font size to apply (in points, not pixels).
743
   */
744
  private void setFont(
745
    final StyleClassedTextArea area, final String name, final double points ) {
746
    runLater( () -> area.setStyle(
747
      format(
748
        "-fx-font-family:'%s';-fx-font-size:%spx;", name, toPixels( points )
749
      )
750
    ) );
751
  }
752
753
  private String getFontName() {
754
    return fontNameProperty().get();
755
  }
756
757
  private StringProperty fontNameProperty() {
758
    return mWorkspace.stringProperty( KEY_UI_FONT_EDITOR_NAME );
759
  }
760
761
  private double getFontSize() {
762
    return fontSizeProperty().get();
763
  }
764
765
  private DoubleProperty fontSizeProperty() {
766
    return mWorkspace.doubleProperty( KEY_UI_FONT_EDITOR_SIZE );
767
  }
768
769
  /**
770
   * Answers whether the given resource is of compatible {@link MediaType}s.
771
   *
772
   * @param mediaType The {@link MediaType} to compare.
773
   * @return {@code true} if the given {@link MediaType} is suitable for
774
   * editing with this type of editor.
775
   */
776
  @Override
777
  public boolean supports( final MediaType mediaType ) {
778
    return isMediaType( mediaType ) ||
779
      mediaType == TEXT_MARKDOWN ||
780
      mediaType == TEXT_R_MARKDOWN;
781
  }
782
}
1783
A src/main/java/com/keenwrite/events/AppEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
import static com.keenwrite.events.Bus.post;
5
6
/**
7
 * Marker interface for all application events.
8
 */
9
public interface AppEvent {
10
11
  /**
12
   * Submits this event to the {@link Bus}.
13
   */
14
  default void publish() {
15
    post( this );
16
  }
17
}
118
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 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
import com.keenwrite.editors.common.Caret;
5
6
/**
7
 * Responsible for notifying when the caret has moved, which includes giving
8
 * focus to a different editor.
9
 */
10
public class CaretMovedEvent implements AppEvent {
11
  private final Caret mCaret;
12
13
  private CaretMovedEvent( final Caret caret ) {
14
    assert caret != null;
15
    mCaret = caret;
16
  }
17
18
  public static void fire( final Caret caret ) {
19
    new CaretMovedEvent( caret ).publish();
20
  }
21
22
  public Caret getCaret() {
23
    return mCaret;
24
  }
25
}
126
A src/main/java/com/keenwrite/events/CaretNavigationEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
import com.keenwrite.ui.outline.DocumentOutline;
5
6
/**
7
 * Collates information about a caret event, which is typically triggered when
8
 * the user double-clicks in the {@link DocumentOutline}. This is an imperative
9
 * event, meaning that the position of the caret will be changed after this
10
 * event is handled. As opposed to a {@link CaretMovedEvent}, which provides
11
 * information about the caret after it has been moved.
12
 */
13
public class CaretNavigationEvent implements AppEvent {
14
  /**
15
   * Absolute document offset.
16
   */
17
  private final int mOffset;
18
19
  private CaretNavigationEvent( final int offset ) {
20
    mOffset = offset;
21
  }
22
23
  /**
24
   * Publishes an event that requests moving the caret to the given offset.
25
   *
26
   * @param offset Move the caret to this document offset.
27
   */
28
  public static void fire( final int offset ) {
29
    new CaretNavigationEvent( offset ).publish();
30
  }
31
32
  public int getOffset() {
33
    return mOffset;
34
  }
35
}
136
A src/main/java/com/keenwrite/events/DocumentChangedEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
/**
5
 * Collates information about an HTML document that has changed.
6
 */
7
public class DocumentChangedEvent implements AppEvent {
8
  private final String mText;
9
10
  /**
11
   * Hash document (as plain text) so subscribers are notified upon changes.
12
   */
13
  private static int sHash;
14
15
  /**
16
   * Creates an event with the new plain text document, having all variables
17
   * substituted and all markup removed.
18
   *
19
   * @param text The document text that has changed since the last time this
20
   *             type of event was fired.
21
   */
22
  private DocumentChangedEvent( final String text ) {
23
    mText = text;
24
  }
25
26
  /**
27
   * When the given document may have changed. This will only fire a change
28
   * event if the given document has changed from the last time this
29
   * event was fired. The document is first converted to plain text before
30
   * the comparison is made.
31
   *
32
   * @param html The document that may have changed.
33
   */
34
  public static void fire( final String html ) {
35
    // Hashing the document text ignores caret position changes.
36
    final var hash = html.hashCode();
37
38
    if( hash != sHash ) {
39
      sHash = hash;
40
      new DocumentChangedEvent( html ).publish();
41
    }
42
  }
43
44
  /**
45
   * Returns the text that has changed.
46
   *
47
   * @return The new document text.
48
   */
49
  public String getDocument() {
50
    return mText;
51
  }
52
53
  /**
54
   * Returns the document.
55
   *
56
   * @return The value from {@link #getDocument()}.
57
   */
58
  @Override
59
  public String toString() {
60
    return getDocument();
61
  }
62
}
163
A src/main/java/com/keenwrite/events/ExportFailedEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
/**
5
 * Responsible for kicking off an alert message when exporting (e.g., to PDF)
6
 * fails. This can happen when the executable to typeset the document cannot
7
 * be found.
8
 */
9
public class ExportFailedEvent implements AppEvent {
10
  public static void fire() {
11
    new ExportFailedEvent().publish();
12
  }
13
}
114
A src/main/java/com/keenwrite/events/FileOpenEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
import java.net.URI;
5
6
/**
7
 * Collates information about a file requested to be opened. This can be called
8
 * when the user clicks a hyperlink in HTML preview panel.
9
 */
10
public class FileOpenEvent implements AppEvent {
11
  private final URI mUri;
12
13
  private FileOpenEvent( final URI uri ) {
14
    assert uri != null;
15
    mUri = uri;
16
  }
17
18
  /**
19
   * Fires a new file open event using the given {@link URI} instance.
20
   *
21
   * @param uri The instance of {@link URI} to open as a file in a text editor.
22
   */
23
  public static void fire( final URI uri ) {
24
    new FileOpenEvent( uri ).publish();
25
  }
26
27
  /**
28
   * Returns the requested file name to be opened.
29
   *
30
   * @return A file reference that can be opened in a text editor.
31
   */
32
  public URI getUri() {
33
    return mUri;
34
  }
35
}
136
A src/main/java/com/keenwrite/events/FocusEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
/**
5
 * Collates information about an object that has gained focus. This is typically
6
 * used by text resource editors (such as text editors and definition editors).
7
 */
8
public class FocusEvent<T> implements AppEvent {
9
  private final T mNode;
10
11
  protected FocusEvent( final T node ) {
12
    mNode = node;
13
  }
14
15
  /**
16
   * This method is used to help update the UI whenever a component has gained
17
   * input focus.
18
   *
19
   * @return The object that has gained focus.
20
   */
21
  public T get() {
22
    return mNode;
23
  }
24
}
125
A src/main/java/com/keenwrite/events/HyperlinkOpenEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
import java.io.IOException;
5
import java.net.URI;
6
7
import static com.keenwrite.events.StatusEvent.clue;
8
9
/**
10
 * Collates information about a URL requested to be opened.
11
 */
12
public class HyperlinkOpenEvent implements AppEvent {
13
  private final URI mUri;
14
15
  private HyperlinkOpenEvent( final URI uri ) {
16
    mUri = uri;
17
  }
18
19
  /**
20
   * Requests to open the default browser at the given location.
21
   *
22
   * @param uri The location to open.
23
   */
24
  public static void fire( final URI uri )
25
    throws IOException {
26
    new HyperlinkOpenEvent( uri ).publish();
27
  }
28
29
  /**
30
   * Requests to open the default browser at the given location.
31
   *
32
   * @param uri The location to open.
33
   */
34
  public static void fire( final String uri ) {
35
    try {
36
      fire( new URI( uri ) );
37
    } catch( final Exception ex ) {
38
      clue( ex );
39
    }
40
  }
41
42
  /**
43
   * Returns the requested resource to be opened.
44
   *
45
   * @return A reference that can be opened in a web browser.
46
   */
47
  public URI getUri() {
48
    return mUri;
49
  }
50
}
151
A src/main/java/com/keenwrite/events/InsertDefinitionEvent.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
import com.keenwrite.editors.definition.DefinitionTreeItem;
5
6
/**
7
 * Collates information about a request to insert a reference to a
8
 * definition value into the active document.
9
 */
10
public class InsertDefinitionEvent<T> implements AppEvent {
11
12
  private final DefinitionTreeItem<T> mLeaf;
13
14
  private InsertDefinitionEvent( final DefinitionTreeItem<T> leaf ) {
15
    mLeaf = leaf;
16
  }
17
18
  public static <T> void fire( final DefinitionTreeItem<T> leaf ) {
19
    assert leaf != null;
20
    assert leaf.isLeaf();
21
22
    new InsertDefinitionEvent<>( leaf ).publish();
23
  }
24
25
  /**
26
   * Returns the {@link DefinitionTreeItem} that is to be inserted into the
27
   * active document.
28
   *
29
   * @return The item to insert (as a variable).
30
   */
31
  public DefinitionTreeItem<T> getLeaf() {
32
    return mLeaf;
33
  }
34
}
135
A src/main/java/com/keenwrite/events/ParseHeadingEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
import com.keenwrite.processors.Processor;
5
6
/**
7
 * Collates information about a document heading that has been parsed, after
8
 * all pertinent {@link Processor}s applied.
9
 */
10
public class ParseHeadingEvent implements AppEvent {
11
  private static final int NEW_OUTLINE_LEVEL = 0;
12
13
  /**
14
   * The heading text, which may be {@code null} upon creating a new outline.
15
   */
16
  private final String mText;
17
18
  /**
19
   * The heading level, which will be set to {@link #NEW_OUTLINE_LEVEL} if this
20
   * event indicates that the existing outline should be cleared anew.
21
   */
22
  private final int mLevel;
23
24
  /**
25
   * Offset into the text where the heading is found.
26
   */
27
  private final int mOffset;
28
29
  private ParseHeadingEvent(
30
    final int level, final String text, final int offset ) {
31
    mText = text;
32
    mLevel = level;
33
    mOffset = offset;
34
  }
35
36
  /**
37
   * Call to indicate a new outline is to be created.
38
   */
39
  public static void fireNewOutlineEvent() {
40
    new ParseHeadingEvent( NEW_OUTLINE_LEVEL, "Document", 0 ).publish();
41
  }
42
43
  /**
44
   * Call to indicate that a new heading must be added to the document outline.
45
   *
46
   * @param text   The heading text (parsed and processed).
47
   * @param level  A value between 1 and 6.
48
   * @param offset Absolute offset into document where heading is found.
49
   */
50
  public static void fire(
51
    final int level, final String text, final int offset ) {
52
    assert text != null;
53
    assert 1 <= level && level <= 6;
54
    assert 0 <= offset;
55
    new ParseHeadingEvent( level, text, offset ).publish();
56
  }
57
58
  public boolean isNewOutline() {
59
    return getLevel() == NEW_OUTLINE_LEVEL;
60
  }
61
62
  public int getLevel() {
63
    return mLevel;
64
  }
65
66
  /**
67
   * Returns the text description for the heading.
68
   *
69
   * @return The post-parsed and processed heading text from the document.
70
   */
71
  public String getText() {
72
    return mText;
73
  }
74
75
  /**
76
   * Returns an offset into the document where the heading is found.
77
   *
78
   * @return A zero-based document offset.
79
   */
80
  public int getOffset() {
81
    return mOffset;
82
  }
83
84
  @Override
85
  public String toString() {
86
    return getText();
87
  }
88
}
189
A src/main/java/com/keenwrite/events/ScrollLockEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
import static java.awt.Toolkit.getDefaultToolkit;
5
import static java.awt.event.KeyEvent.VK_SCROLL_LOCK;
6
7
/**
8
 * Collates information about the scroll lock status.
9
 */
10
public class ScrollLockEvent implements AppEvent {
11
  private final boolean mLocked;
12
13
  private ScrollLockEvent( final boolean locked ) {
14
    mLocked = locked;
15
  }
16
17
  /**
18
   * Fires a scroll lock event provided that the scroll lock key is in the
19
   * off state.
20
   *
21
   * @param locked The new locked status.
22
   */
23
  public static void fireScrollLockEvent( final boolean locked ) {
24
    // If the scroll lock key is off, allow the status to change.
25
    if( !getScrollLockKeyStatus() ) {
26
      fire( locked );
27
    }
28
  }
29
30
  /**
31
   * Fires a scroll lock event based on the current status of the scroll
32
   * lock key.
33
   */
34
  public static void fireScrollLockEvent() {
35
    fire( getScrollLockKeyStatus() );
36
  }
37
38
  /**
39
   * Answers whether the synchronized scrolling should be locked in place
40
   * (i.e., prevent sync scrolling).
41
   *
42
   * @return {@code true} when the user has locked the scrollbar position.
43
   */
44
  public boolean isLocked() {
45
    return mLocked;
46
  }
47
48
  private static void fire( final boolean locked ) {
49
    new ScrollLockEvent( locked ).publish();
50
  }
51
52
  /**
53
   * Returns the state of the scroll lock key.
54
   *
55
   * @return {@code true} when the scroll lock key is in the on state.
56
   */
57
  private static boolean getScrollLockKeyStatus() {
58
    return getDefaultToolkit().getLockingKeyState( VK_SCROLL_LOCK );
59
  }
60
}
161
A src/main/java/com/keenwrite/events/StatusEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
import com.keenwrite.AppCommands;
5
6
import java.util.List;
7
8
import static com.keenwrite.Messages.get;
9
import static com.keenwrite.constants.Constants.NEWLINE;
10
import static com.keenwrite.constants.Constants.STATUS_BAR_OK;
11
import static java.lang.String.format;
12
import static java.lang.String.join;
13
import static java.util.Arrays.stream;
14
15
/**
16
 * Collates information about an application issue. The issues can be
17
 * exceptions, state problems, parsing errors, and so forth.
18
 */
19
public final class StatusEvent implements AppEvent {
20
  /**
21
   * Reference a class in the top-level package that doesn't depend on any
22
   * JavaFX APIs.
23
   */
24
  private static final String PACKAGE_NAME = AppCommands.class.getPackageName();
25
26
  private static final String ENGLISHIFY =
27
    "(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])";
28
29
  /**
30
   * Detailed information about a problem.
31
   */
32
  private final String mMessage;
33
34
  /**
35
   * Provides stack trace information that isolates the cause.
36
   */
37
  private final Throwable mProblem;
38
39
  /**
40
   * Constructs a new event that contains a problem description to help the
41
   * user resolve an issue encountered while using the application.
42
   *
43
   * @param message The human-readable message, typically displayed on-screen.
44
   */
45
  public StatusEvent( final String message ) {
46
    this( message, null );
47
  }
48
49
  public StatusEvent( final Throwable problem ) {
50
    this( "", 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
    assert message != null;
59
    mMessage = message;
60
    mProblem = problem;
61
  }
62
63
  /**
64
   * Returns the stack trace information for the issue encountered. This is
65
   * optional because usually a status message isn't an application error.
66
   *
67
   * @return Optional stack trace to pinpoint the problem area in the code.
68
   */
69
  public String getProblem() {
70
    // 256 is arbitrary; stack traces shouldn't be much larger.
71
    final var sb = new StringBuilder( 256 );
72
    final var trace = mProblem;
73
74
    if( trace != null ) {
75
      stream( trace.getStackTrace() )
76
        .takeWhile( StatusEvent::filter )
77
        .limit( 10 )
78
        .toList()
79
        .forEach( e -> sb.append( e.toString() ).append( NEWLINE ) );
80
    }
81
82
    return sb.toString();
83
  }
84
85
  @Override
86
  public String toString() {
87
    // Not exactly sure how the message can be null, but it happened once!
88
    final var message = mMessage == null ? "UNKNOWN" : mMessage;
89
90
    return format( "%s%s%s",
91
                   message,
92
                   message.isBlank() ? "" : " ",
93
                   mProblem == null ? "" : toEnglish( mProblem ) );
94
  }
95
96
  /**
97
   * Returns {@code true} to allow the {@link StackTraceElement} to pass
98
   * through the filter.
99
   *
100
   * @param e The element to check against the filter.
101
   */
102
  private static boolean filter( final StackTraceElement e ) {
103
    final var clazz = e.getClassName();
104
    return clazz.contains( PACKAGE_NAME ) ||
105
      clazz.contains( "org.renjin." ) ||
106
      clazz.contains( "sun." ) ||
107
      clazz.contains( "flexmark." ) ||
108
      clazz.contains( "java." );
109
  }
110
111
  /**
112
   * Separates the exception class name from TitleCase into lowercase,
113
   * space-separated words. This makes the exception look a little more like
114
   * English. Any {@link RuntimeException} instances passed into this method
115
   * will have the cause extracted, if possible.
116
   *
117
   * @param problem The exception that triggered the status event change.
118
   * @return A human-readable message with the exception name and the
119
   * exception's message.
120
   */
121
  private static String toEnglish( Throwable problem ) {
122
    assert problem != null;
123
124
    // Subclasses of RuntimeException must be subject to Englishification.
125
    if( problem.getClass().equals( RuntimeException.class ) ) {
126
      final var cause = problem.getCause();
127
      return cause == null ? problem.getMessage() : cause.getMessage();
128
    }
129
130
    final var className = problem.getClass().getSimpleName();
131
    final var words = join( " ", className.split( ENGLISHIFY ) );
132
    return format( "(%s: %s)", words.toLowerCase(), problem.getMessage() );
133
  }
134
135
  /**
136
   * Returns the message used to construct the event.
137
   *
138
   * @return The message for this event.
139
   */
140
  public String getMessage() {
141
    return mMessage;
142
  }
143
144
  /**
145
   * Resets the status bar to a default message. Indicates that there are no
146
   * issues to bring to the user's attention.
147
   */
148
  public static void clue() {
149
    fire( get( STATUS_BAR_OK, "OK" ) );
150
  }
151
152
  /**
153
   * Notifies listeners of a series of messages. This is useful when providing
154
   * users feedback of how third-party executables have failed.
155
   *
156
   * @param messages The lines of text to display.
157
   */
158
  public static void clue( final List<String> messages ) {
159
    messages.forEach( StatusEvent::fire );
160
  }
161
162
  /**
163
   * Notifies listeners of an error.
164
   *
165
   * @param key The message bundle key to look up.
166
   * @param t   The exception that caused the error.
167
   */
168
  public static void clue( final String key, final Throwable t ) {
169
    fire( get( key ), t );
170
  }
171
172
  /**
173
   * Notifies listeners of a custom message.
174
   *
175
   * @param key  The property key having a value to populate with arguments.
176
   * @param args The placeholder values to substitute into the key's value.
177
   */
178
  public static void clue( final String key, final Object... args ) {
179
    fire( get( key, args ) );
180
  }
181
182
  /**
183
   * Notifies listeners of a custom message.
184
   *
185
   * @param ex   The exception that warranted calling this method.
186
   * @param fmt  The string format specifier.
187
   * @param args The arguments to weave into the format specifier.
188
   */
189
  public static void clue(
190
    final Exception ex,
191
    final String fmt,
192
    final Object... args ) {
193
    final var msg = format( fmt, args );
194
    clue( msg, ex );
195
  }
196
197
  /**
198
   * Notifies listeners of an exception occurs that warrants the user's
199
   * attention.
200
   *
201
   * @param problem The exception with a message to display to the user.
202
   */
203
  public static void clue( final Throwable problem ) {
204
    fire( problem );
205
  }
206
207
  private static void fire( final String message ) {
208
    new StatusEvent( message ).publish();
209
  }
210
211
  private static void fire( final Throwable problem ) {
212
    new StatusEvent( problem ).publish();
213
  }
214
215
  private static void fire(
216
    final String message, final Throwable problem ) {
217
    new StatusEvent( message, problem ).publish();
218
  }
219
}
1220
A src/main/java/com/keenwrite/events/TextDefinitionFocusEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
import com.keenwrite.editors.TextDefinition;
5
6
public class TextDefinitionFocusEvent extends FocusEvent<TextDefinition> {
7
  protected TextDefinitionFocusEvent( final TextDefinition editor ) {
8
    super( editor );
9
  }
10
11
  /**
12
   * When the {@link TextDefinition} editor has focus, fire an event so that
13
   * subscribers may perform an action.
14
   *
15
   * @param editor The instance of editor that has gained input focus.
16
   */
17
  public static void fire( final TextDefinition editor ) {
18
    new TextDefinitionFocusEvent( editor ).publish();
19
  }
20
}
121
A src/main/java/com/keenwrite/events/TextEditorFocusEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
import com.keenwrite.editors.TextEditor;
5
6
/**
7
 * Collates information about the text editor that has gained focus.
8
 */
9
public class TextEditorFocusEvent extends FocusEvent<TextEditor> {
10
  protected TextEditorFocusEvent( final TextEditor editor ) {
11
    super( editor );
12
  }
13
14
  /**
15
   * When the {@link TextEditor} has focus, fire an event so that subscribers
16
   * may perform an action---such as parsing and rendering the contents.
17
   *
18
   * @param editor The instance of editor that has gained input focus.
19
   */
20
  public static void fire( final TextEditor editor ) {
21
    new TextEditorFocusEvent( editor ).publish();
22
  }
23
}
124
A src/main/java/com/keenwrite/events/WordCountEvent.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events;
3
4
/**
5
 * Collates information about the word count changing.
6
 */
7
public class WordCountEvent implements AppEvent {
8
  /**
9
   * Number of words in the document.
10
   */
11
  private final int mCount;
12
13
  private WordCountEvent( final int count ) {
14
    mCount = count;
15
  }
16
17
  /**
18
   * Publishes an event that indicates the number of words in the document.
19
   *
20
   * @param count The approximate number of words in the document.
21
   */
22
  public static void fire( final int count ) {
23
    new WordCountEvent( count ).publish();
24
  }
25
26
  public int getCount() {
27
    return mCount;
28
  }
29
}
130
A src/main/java/com/keenwrite/events/spelling/LexiconEvent.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events.spelling;
3
4
import com.keenwrite.events.AppEvent;
5
6
public abstract class LexiconEvent implements AppEvent {
7
}
18
A src/main/java/com/keenwrite/events/spelling/LexiconLoadedEvent.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events.spelling;
3
4
import java.util.Map;
5
6
/**
7
 * Collates information about the lexicon. Fired when the lexicon has been
8
 * fully loaded into memory.
9
 */
10
public class LexiconLoadedEvent extends LexiconEvent {
11
12
  private final Map<String, Long> mLexicon;
13
14
  private LexiconLoadedEvent( final Map<String, Long> lexicon ) {
15
    mLexicon = lexicon;
16
  }
17
18
  public static void fire( final Map<String, Long> lexicon ) {
19
    new LexiconLoadedEvent( lexicon ).publish();
20
  }
21
22
  /**
23
   * Returns a word-frequency map used by the spell checking library.
24
   *
25
   * @return The lexicon that was loaded.
26
   */
27
  public Map<String, Long> getLexicon() {
28
    return mLexicon;
29
  }
30
}
131
A src/main/java/com/keenwrite/events/workspace/WorkspaceEvent.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events.workspace;
3
4
import com.keenwrite.events.AppEvent;
5
6
/**
7
 * Superclass of all events related to the workspace.
8
 */
9
public abstract class WorkspaceEvent implements AppEvent {
10
}
111
A src/main/java/com/keenwrite/events/workspace/WorkspaceLoadedEvent.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.events.workspace;
3
4
import com.keenwrite.preferences.Workspace;
5
6
/**
7
 * Indicates that the {@link Workspace} has been loaded.
8
 */
9
public class WorkspaceLoadedEvent extends WorkspaceEvent {
10
  private final Workspace mWorkspace;
11
12
  private WorkspaceLoadedEvent( final Workspace workspace ) {
13
    assert workspace != null;
14
15
    mWorkspace = workspace;
16
  }
17
18
  /**
19
   * Publishes an event that indicates a new {@link Workspace} has been loaded.
20
   */
21
  public static void fire( final Workspace workspace ) {
22
    new WorkspaceLoadedEvent( workspace ).publish();
23
  }
24
25
  /**
26
   * Returns a reference to the {@link Workspace} that was loaded.
27
   *
28
   * @return The {@link Workspace} that has loaded user preferences.
29
   */
30
  public Workspace getWorkspace() {
31
    return mWorkspace;
32
  }
33
}
134
A src/main/java/com/keenwrite/exceptions/MissingFileException.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.exceptions;
3
4
import java.io.FileNotFoundException;
5
6
import static com.keenwrite.Messages.get;
7
8
/**
9
 * Responsible for informing the user when a file cannot be found.
10
 * This avoids duplicating the error message prefix.
11
 */
12
public final class MissingFileException extends FileNotFoundException {
13
  /**
14
   * Constructs a new {@link MissingFileException} using the given path.
15
   *
16
   * @param uri The path to the file resource that could not be found.
17
   */
18
  public MissingFileException( final String uri ) {
19
    super( get( "Main.status.error.file.missing", uri ) );
20
  }
21
}
122
A src/main/java/com/keenwrite/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.File;
5
import java.io.FileNotFoundException;
6
7
/**
8
 * Indicates a command could not be found to run.
9
 */
10
public class CommandNotFoundException extends FileNotFoundException {
11
  /**
12
   * Creates a new exception indicating that the given command could not be
13
   * found (or executed).
14
   *
15
   * @param command The binary file's command name that could not be run.
16
   */
17
  public CommandNotFoundException( final String command ) {
18
    super( command );
19
  }
20
21
  /**
22
   * Creates a new exception indicating that the given command could not be
23
   * found (or executed).
24
   *
25
   * @param file The binary file's command name that could not be run.
26
   */
27
  public CommandNotFoundException( final File file ) {
28
    this( file.getAbsolutePath() );
29
  }
30
}
131
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 java.nio.file.FileSystems.getDefault;
16
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
17
import static java.util.Collections.newSetFromMap;
18
19
/**
20
 * Responsible for watching when a file has been changed.
21
 */
22
public class FileWatchService implements Runnable {
23
  /**
24
   * Set to {@code false} when {@link #stop()} is called.
25
   */
26
  private volatile boolean mRunning;
27
28
  /**
29
   * Contains the listeners to notify when a given file has changed.
30
   */
31
  private final Set<FileModifiedListener> mListeners =
32
    newSetFromMap( new ConcurrentHashMap<>() );
33
  private final WatchService mWatchService;
34
  private final BiMap<File, WatchKey> mWatched = HashBiMap.create();
35
36
  /**
37
   * Creates a new file system watch service with the given files to watch.
38
   *
39
   * @param files The files to watch for file system events.
40
   */
41
  public FileWatchService( final File... files ) {
42
    mWatchService = createWatchService();
43
44
    try {
45
      for( final var file : files ) {
46
        register( file );
47
      }
48
    } catch( final Exception ex ) {
49
      throw new RuntimeException( ex );
50
    }
51
  }
52
53
  /**
54
   * Runs the event handler until {@link #stop()} is called.
55
   *
56
   * @throws RuntimeException There was an error watching for file events.
57
   */
58
  @Override
59
  public void run() {
60
    mRunning = true;
61
62
    while( mRunning ) {
63
      handleEvents();
64
    }
65
  }
66
67
  private void handleEvents() {
68
    try {
69
      final var watchKey = mWatchService.take();
70
71
      for( final var pollEvent : watchKey.pollEvents() ) {
72
        final var watchable = (Path) watchKey.watchable();
73
        final var context = (Path) pollEvent.context();
74
        final var file = watchable.resolve( context ).toFile();
75
76
        if( mWatched.containsKey( file ) ) {
77
          final var fileEvent = new FileEvent( file );
78
79
          for( final var listener : mListeners ) {
80
            listener.accept( fileEvent );
81
          }
82
        }
83
      }
84
85
      if( !watchKey.reset() ) {
86
        unregister( watchKey );
87
      }
88
    } catch( final Exception ex ) {
89
      throw new RuntimeException( ex );
90
    }
91
  }
92
93
  /**
94
   * Adds the given {@link File}'s containing directory to the watch list. When
95
   * the given {@link File} is modified, this service will receive a
96
   * notification that the containing directory has been modified, which will
97
   * then be filtered by file name.
98
   * <p>
99
   * This method is idempotent.
100
   * </p>
101
   *
102
   * @param file The {@link File} to watch for modification events.
103
   * @return The {@link File}'s directory watch state.
104
   * @throws IOException              Could not register the directory.
105
   * @throws IllegalArgumentException The {@link File} has no parent directory.
106
   */
107
  public WatchKey register( final File file ) throws IOException {
108
    if( mWatched.containsKey( file ) ) {
109
      return mWatched.get( file );
110
    }
111
112
    final var path = getParentDirectory( file );
113
    final var watchKey = path.register( mWatchService, ENTRY_MODIFY );
114
115
    return mWatched.put( file, watchKey );
116
  }
117
118
  /**
119
   * Removes the given {@link File}'s containing directory from the watch list.
120
   * <p>
121
   * This method is idempotent.
122
   * </p>
123
   *
124
   * @param file The {@link File} to no longer watch.
125
   * @throws IllegalArgumentException The {@link File} has no parent directory.
126
   */
127
  public void unregister( final File file ) {
128
    mWatched.remove( cancel( file ) );
129
  }
130
131
  /**
132
   * Cancels watching the given file for file system changes.
133
   *
134
   * @param file The {@link File} to watch for file events.
135
   * @return The given file, always.
136
   */
137
  private File cancel( final File file ) {
138
    final var watchKey = mWatched.get( file );
139
140
    if( watchKey != null ) {
141
      watchKey.cancel();
142
    }
143
144
    return file;
145
  }
146
147
  /**
148
   * Removes the given {@link WatchKey} from the registration map.
149
   *
150
   * @param watchKey The {@link WatchKey} to remove from the map.
151
   */
152
  private void unregister( final WatchKey watchKey ) {
153
    unregister( mWatched.inverse().get( watchKey ) );
154
  }
155
156
  /**
157
   * Adds a listener to be notified when a file under watch has been modified.
158
   * Listeners are backed by a set.
159
   *
160
   * @param listener The {@link FileModifiedListener} to add to the list.
161
   * @return {@code true} if this set did not already contain listener.
162
   */
163
  public boolean addListener( final FileModifiedListener listener ) {
164
    return mListeners.add( listener );
165
  }
166
167
  /**
168
   * Removes a listener from the notify list.
169
   *
170
   * @param listener The {@link FileModifiedListener} to remove.
171
   */
172
  public void removeListener( final FileModifiedListener listener ) {
173
    mListeners.remove( listener );
174
  }
175
176
  /**
177
   * Shuts down the file watch service and clears both watchers and listeners.
178
   *
179
   * @throws IOException Could not close the watch service.
180
   */
181
  public void stop() throws IOException {
182
    mRunning = false;
183
184
    for( final var file : mWatched.keySet() ) {
185
      cancel( file );
186
    }
187
188
    mWatched.clear();
189
    mListeners.clear();
190
    mWatchService.close();
191
  }
192
193
  /**
194
   * Returns the directory containing the given {@link File} instance.
195
   *
196
   * @param file The {@link File}'s containing directory to watch.
197
   * @return The {@link Path} to the {@link File}'s directory.
198
   * @throws IllegalArgumentException The {@link File} has no parent directory.
199
   */
200
  private Path getParentDirectory( final File file ) {
201
    assert file != null;
202
    assert !file.isDirectory();
203
204
    final var directory = file.getParentFile();
205
206
    if( directory == null ) {
207
      throw new IllegalArgumentException( file.getAbsolutePath() );
208
    }
209
210
    return directory.toPath();
211
  }
212
213
  private WatchService createWatchService() {
214
    try {
215
      return getDefault().newWatchService();
216
    } catch( final Exception ex ) {
217
      // Create a fallback that allows the class to be instantiated and used
218
      // without without preventing the application from launching.
219
      return new PollingWatchService();
220
    }
221
  }
222
}
1223
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 org.apache.commons.io.FilenameUtils.getExtension;
12
13
/**
14
 * Defines various file formats and format contents.
15
 *
16
 * @see
17
 * <a href="https://www.iana.org/assignments/media-types/media-types.xhtml">IANA
18
 * Media Types</a>
19
 */
20
@SuppressWarnings( "SpellCheckingInspection" )
21
public enum MediaType {
22
  APP_DOCUMENT_OUTLINE( APPLICATION, "x-document-outline" ),
23
  APP_DOCUMENT_STATISTICS( APPLICATION, "x-document-statistics" ),
24
  APP_FILE_MANAGER( APPLICATION, "x-file-manager" ),
25
26
  APP_ACAD( APPLICATION, "acad" ),
27
  APP_JAVA_OBJECT( APPLICATION, "x-java-serialized-object" ),
28
  APP_JAVA( APPLICATION, "java" ),
29
  APP_PS( APPLICATION, "postscript" ),
30
  APP_EPS( APPLICATION, "eps" ),
31
  APP_PDF( APPLICATION, "pdf" ),
32
  APP_ZIP( APPLICATION, "zip" ),
33
34
  /*
35
   * Standard font types.
36
   */
37
  FONT_OTF( "otf" ),
38
  FONT_TTF( "ttf" ),
39
40
  /*
41
   * Standard image types.
42
   */
43
  IMAGE_APNG( "apng" ),
44
  IMAGE_ACES( "aces" ),
45
  IMAGE_AVCI( "avci" ),
46
  IMAGE_AVCS( "avcs" ),
47
  IMAGE_BMP( "bmp" ),
48
  IMAGE_CGM( "cgm" ),
49
  IMAGE_DICOM_RLE( "dicom_rle" ),
50
  IMAGE_EMF( "emf" ),
51
  IMAGE_EXAMPLE( "example" ),
52
  IMAGE_FITS( "fits" ),
53
  IMAGE_G3FAX( "g3fax" ),
54
  IMAGE_GIF( "gif" ),
55
  IMAGE_HEIC( "heic" ),
56
  IMAGE_HEIF( "heif" ),
57
  IMAGE_HEJ2K( "hej2k" ),
58
  IMAGE_HSJ2( "hsj2" ),
59
  IMAGE_X_ICON( "x-icon" ),
60
  IMAGE_JLS( "jls" ),
61
  IMAGE_JP2( "jp2" ),
62
  IMAGE_JPEG( "jpeg" ),
63
  IMAGE_JPH( "jph" ),
64
  IMAGE_JPHC( "jphc" ),
65
  IMAGE_JPM( "jpm" ),
66
  IMAGE_JPX( "jpx" ),
67
  IMAGE_JXR( "jxr" ),
68
  IMAGE_JXRA( "jxrA" ),
69
  IMAGE_JXRS( "jxrS" ),
70
  IMAGE_JXS( "jxs" ),
71
  IMAGE_JXSC( "jxsc" ),
72
  IMAGE_JXSI( "jxsi" ),
73
  IMAGE_JXSS( "jxss" ),
74
  IMAGE_KTX( "ktx" ),
75
  IMAGE_KTX2( "ktx2" ),
76
  IMAGE_NAPLPS( "naplps" ),
77
  IMAGE_PNG( "png" ),
78
  IMAGE_PHOTOSHOP( "photoshop" ),
79
  IMAGE_SVG_XML( "svg+xml" ),
80
  IMAGE_T38( "t38" ),
81
  IMAGE_TIFF( "tiff" ),
82
  IMAGE_WEBP( "webp" ),
83
  IMAGE_WMF( "wmf" ),
84
  IMAGE_X_BITMAP( "x-xbitmap" ),
85
  IMAGE_X_PIXMAP( "x-xpixmap" ),
86
87
  /*
88
   * Standard audio types.
89
   */
90
  AUDIO_SIMPLE( AUDIO, "basic" ),
91
  AUDIO_MP3( AUDIO, "mp3" ),
92
  AUDIO_WAV( AUDIO, "x-wav" ),
93
94
  /*
95
   * Standard video types.
96
   */
97
  VIDEO_MNG( VIDEO, "x-mng" ),
98
99
  /*
100
   * Document types for editing or displaying documents, mix of standard and
101
   * application-specific. The order that these are declared reflect in the
102
   * ordinal value used during comparisons.
103
   */
104
  TEXT_YAML( TEXT, "yaml" ),
105
  TEXT_PLAIN( TEXT, "plain" ),
106
  TEXT_MARKDOWN( TEXT, "markdown" ),
107
  TEXT_R_MARKDOWN( TEXT, "R+markdown" ),
108
  TEXT_PROPERTIES( TEXT, "x-java-properties" ),
109
  TEXT_HTML( TEXT, "html" ),
110
  TEXT_XHTML( TEXT, "xhtml+xml" ),
111
  TEXT_XML( TEXT, "xml" ),
112
113
  /*
114
   * When all other lights go out.
115
   */
116
  UNDEFINED( TypeName.UNDEFINED, "undefined" );
117
118
  /**
119
   * The IANA-defined types.
120
   */
121
  public enum TypeName {
122
    APPLICATION,
123
    AUDIO,
124
    IMAGE,
125
    TEXT,
126
    UNDEFINED,
127
    VIDEO
128
  }
129
130
  /**
131
   * The fully qualified IANA-defined media type.
132
   */
133
  private final String mMediaType;
134
135
  /**
136
   * The IANA-defined type name.
137
   */
138
  private final TypeName mTypeName;
139
140
  /**
141
   * The IANA-defined subtype name.
142
   */
143
  private final String mSubtype;
144
145
  /**
146
   * Constructs an instance using the default type name of "image".
147
   *
148
   * @param subtype The image subtype name.
149
   */
150
  MediaType( final String subtype ) {
151
    this( IMAGE, subtype );
152
  }
153
154
  /**
155
   * Constructs an instance using an IANA-defined type and subtype pair.
156
   *
157
   * @param typeName The media type's type name.
158
   * @param subtype  The media type's subtype name.
159
   */
160
  MediaType( final TypeName typeName, final String subtype ) {
161
    mTypeName = typeName;
162
    mSubtype = subtype;
163
    mMediaType = typeName.toString().toLowerCase() + '/' + subtype;
164
  }
165
166
  /**
167
   * Returns the {@link MediaType} associated with the given file.
168
   *
169
   * @param file Has a file name that may contain an extension associated with
170
   *             a known {@link MediaType}.
171
   * @return {@link MediaType#UNDEFINED} if the extension has not been
172
   * assigned, otherwise the {@link MediaType} associated with this
173
   * {@link File}'s file name extension.
174
   */
175
  public static MediaType valueFrom( final File file ) {
176
    assert file != null;
177
    return fromFilename( file.getName() );
178
  }
179
180
  /**
181
   * Returns the {@link MediaType} associated with the given file name.
182
   *
183
   * @param filename The file name that may contain an extension associated
184
   *                 with a known {@link MediaType}.
185
   * @return {@link MediaType#UNDEFINED} if the extension has not been
186
   * assigned, otherwise the {@link MediaType} associated with this
187
   * {@link URL}'s file name extension.
188
   */
189
  public static MediaType fromFilename( final String filename ) {
190
    assert filename != null;
191
    return fromExtension( getExtension( filename ) );
192
  }
193
194
  /**
195
   * Returns the {@link MediaType} associated with the path to a file.
196
   *
197
   * @param path Has a file name that may contain an extension associated with
198
   *             a known {@link MediaType}.
199
   * @return {@link MediaType#UNDEFINED} if the extension has not been
200
   * assigned, otherwise the {@link MediaType} associated with this
201
   * {@link File}'s file name extension.
202
   */
203
  public static MediaType valueFrom( final Path path ) {
204
    assert path != null;
205
    return valueFrom( path.toFile() );
206
  }
207
208
  /**
209
   * Determines the media type an IANA-defined, semi-colon-separated string.
210
   * This is often used after making an HTTP request to extract the type
211
   * and subtype from the content-type.
212
   *
213
   * @param header The content-type header value, may be {@code null}.
214
   * @return The data type for the resource or {@link MediaType#UNDEFINED} if
215
   * unmapped.
216
   */
217
  public static MediaType valueFrom( String header ) {
218
    if( header == null || header.isBlank() ) {
219
      return UNDEFINED;
220
    }
221
222
    // Trim off the character encoding.
223
    var i = header.indexOf( ';' );
224
    header = header.substring( 0, i == -1 ? header.length() : i );
225
226
    // Split the type and subtype.
227
    i = header.indexOf( '/' );
228
    i = i == -1 ? header.length() : i;
229
    final var type = header.substring( 0, i );
230
    final var subtype = header.substring( i + 1 );
231
232
    return valueFrom( type, subtype );
233
  }
234
235
  /**
236
   * Returns the {@link MediaType} for the given type and subtype names.
237
   *
238
   * @param type    The IANA-defined type name.
239
   * @param subtype The IANA-defined subtype name.
240
   * @return {@link MediaType#UNDEFINED} if there is no {@link MediaType} that
241
   * matches the given type and subtype names.
242
   */
243
  public static MediaType valueFrom(
244
    final String type, final String subtype ) {
245
    assert type != null;
246
    assert subtype != null;
247
248
    for( final var mediaType : values() ) {
249
      if( mediaType.equals( type, subtype ) ) {
250
        return mediaType;
251
      }
252
    }
253
254
    return UNDEFINED;
255
  }
256
257
  /**
258
   * Answers whether the given type and subtype names equal this enumerated
259
   * value. This performs a case-insensitive comparison.
260
   *
261
   * @param type    The type name to compare against this {@link MediaType}.
262
   * @param subtype The subtype name to compare against this {@link MediaType}.
263
   * @return {@code true} when the type and subtype name match.
264
   */
265
  public boolean equals( final String type, final String subtype ) {
266
    assert type != null;
267
    assert subtype != null;
268
269
    return mTypeName.name().equalsIgnoreCase( type ) &&
270
      mSubtype.equalsIgnoreCase( subtype );
271
  }
272
273
  /**
274
   * Answers whether the given {@link TypeName} matches this type name.
275
   *
276
   * @param typeName The {@link TypeName} to compare against the internal value.
277
   * @return {@code true} if the given value is the same IANA-defined type name.
278
   */
279
  @SuppressWarnings( "unused" )
280
  public boolean isType( final TypeName typeName ) {
281
    return mTypeName == typeName;
282
  }
283
284
  /**
285
   * Answers whether this instance is a scalable vector graphic.
286
   *
287
   * @return {@code true} if this instance represents an SVG object.
288
   */
289
  public boolean isSvg() {
290
    return equals( IMAGE_SVG_XML );
291
  }
292
293
  public boolean isUndefined() {
294
    return equals( UNDEFINED );
295
  }
296
297
  /**
298
   * Returns the IANA-defined subtype classification. Primarily used by
299
   * {@link MediaTypeExtension} to initialize associations where the subtype
300
   * name and the file name extension have a 1:1 mapping.
301
   *
302
   * @return The IANA subtype value.
303
   */
304
  public String getSubtype() {
305
    return mSubtype;
306
  }
307
308
  /**
309
   * Creates a temporary {@link File} that starts with the given prefix.
310
   *
311
   * @param prefix    The file name begins with this string (empty is allowed).
312
   * @param directory The directory wherein the file is created.
313
   * @return The fully qualified path to the temporary file.
314
   * @throws IOException Could not create the temporary file.
315
   */
316
  public Path createTempFile(
317
    final String prefix,
318
    final Path directory ) throws IOException {
319
    return createTempFile( prefix, directory, false );
320
  }
321
322
  /**
323
   * Creates a temporary {@link File} that starts with the given prefix.
324
   *
325
   * @param prefix    The file name begins with this string (empty is allowed).
326
   * @param directory The directory wherein the file is created.
327
   * @param purge     Set to {@code true} to delete the file on exit.
328
   * @return The fully qualified path to the temporary file.
329
   * @throws IOException Could not create the temporary file.
330
   */
331
  public Path createTempFile(
332
    final String prefix,
333
    final Path directory,
334
    final boolean purge )
335
    throws IOException {
336
    assert prefix != null;
337
338
    final var suffix = '.' + MediaTypeExtension
339
      .valueFrom( this )
340
      .getExtension();
341
342
    final var file = File.createTempFile( prefix, suffix, directory.toFile() );
343
344
    if( purge ) {
345
      file.deleteOnExit();
346
    }
347
348
    return file.toPath();
349
  }
350
351
  /**
352
   * Returns the IANA-defined type and subtype.
353
   *
354
   * @return The unique media type identifier.
355
   */
356
  @Override
357
  public String toString() {
358
    return mMediaType;
359
  }
360
}
1361
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.util.List;
8
9
import static com.keenwrite.io.MediaType.*;
10
import static java.util.List.of;
11
12
/**
13
 * Responsible for associating file extensions with {@link MediaType} instances.
14
 * Insertion order must be maintained because the first element in the list
15
 * represents the file name extension that corresponds to its icon.
16
 */
17
public enum MediaTypeExtension {
18
  MEDIA_APP_ACAD( APP_ACAD, of( "dwg" ) ),
19
  MEDIA_APP_PDF( APP_PDF ),
20
  MEDIA_APP_PS( APP_PS, of( "ps" ) ),
21
  MEDIA_APP_EPS( APP_EPS ),
22
  MEDIA_APP_ZIP( APP_ZIP ),
23
24
  MEDIA_AUDIO_MP3( AUDIO_MP3 ),
25
  MEDIA_AUDIO_SIMPLE( AUDIO_SIMPLE, of( "au" ) ),
26
  MEDIA_AUDIO_WAV( AUDIO_WAV, of( "wav" ) ),
27
28
  MEDIA_FONT_OTF( FONT_OTF ),
29
  MEDIA_FONT_TTF( FONT_TTF ),
30
31
  MEDIA_IMAGE_APNG( IMAGE_APNG ),
32
  MEDIA_IMAGE_BMP( IMAGE_BMP ),
33
  MEDIA_IMAGE_GIF( IMAGE_GIF ),
34
  MEDIA_IMAGE_JPEG( IMAGE_JPEG,
35
                    of( "jpg", "jpe", "jpeg", "jfif", "pjpeg", "pjp" ) ),
36
  MEDIA_IMAGE_PNG( IMAGE_PNG ),
37
  MEDIA_IMAGE_PSD( IMAGE_PHOTOSHOP, of( "psd" ) ),
38
  MEDIA_IMAGE_SVG( IMAGE_SVG_XML, of( "svg" ) ),
39
  MEDIA_IMAGE_TIFF( IMAGE_TIFF, of( "tiff", "tif" ) ),
40
  MEDIA_IMAGE_WEBP( IMAGE_WEBP ),
41
  MEDIA_IMAGE_X_BITMAP( IMAGE_X_BITMAP, of( "xbm" ) ),
42
  MEDIA_IMAGE_X_PIXMAP( IMAGE_X_PIXMAP, of( "xpm" ) ),
43
44
  MEDIA_VIDEO_MNG( VIDEO_MNG, of( "mng" ) ),
45
46
  MEDIA_TEXT_MARKDOWN( TEXT_MARKDOWN, of(
47
    "md", "markdown", "mdown", "mdtxt", "mdtext", "mdwn", "mkd", "mkdown",
48
    "mkdn" ) ),
49
  MEDIA_TEXT_PLAIN( TEXT_PLAIN, of( "txt", "asc", "ascii", "text", "utxt" ) ),
50
  MEDIA_TEXT_R_MARKDOWN( TEXT_R_MARKDOWN, of( "Rmd" ) ),
51
  MEDIA_TEXT_PROPERTIES( TEXT_PROPERTIES, of( "properties" ) ),
52
  MEDIA_TEXT_XHTML( TEXT_XHTML, of( "htm", "html", "xhtml" ) ),
53
  MEDIA_TEXT_XML( TEXT_XML ),
54
  MEDIA_TEXT_YAML( TEXT_YAML, of( "yaml", "yml" ) ),
55
56
  MEDIA_UNDEFINED( UNDEFINED, of( "undefined" ) );
57
58
  /**
59
   * Returns the {@link MediaTypeExtension} that matches the given media type.
60
   *
61
   * @param mediaType The media type to find.
62
   * @return The correlated value or {@link #MEDIA_UNDEFINED} if not found.
63
   */
64
  public static MediaTypeExtension valueFrom( final MediaType mediaType ) {
65
    for( final var type : values() ) {
66
      if( type.isMediaType( mediaType ) ) {
67
        return type;
68
      }
69
    }
70
71
    return MEDIA_UNDEFINED;
72
  }
73
74
  /**
75
   * Returns the {@link MediaType} associated with the given file name
76
   * extension. The extension must not contain a period.
77
   *
78
   * @param extension File name extension, case-insensitive, {@code null}-safe.
79
   * @return The associated {@link MediaType} as defined by IANA.
80
   */
81
  public static MediaType fromExtension( final String extension ) {
82
    final var sanitized = sanitize( extension );
83
84
    for( final var mediaType : MediaTypeExtension.values() ) {
85
      if( mediaType.isType( sanitized ) ) {
86
        return mediaType.getMediaType();
87
      }
88
    }
89
90
    return UNDEFINED;
91
  }
92
93
  /**
94
   * Returns the {@link MediaType} associated with the given file.
95
   *
96
   * @param file The file having an extension to map to a {@link MediaType}.
97
   * @return The associated {@link MediaType} as defined by IANA.
98
   */
99
  public static MediaType fromFile( final File file ) {
100
    return fromExtension( FilenameUtils.getExtension( file.getName() ) );
101
  }
102
103
  private static String sanitize( final String extension ) {
104
    return extension == null ? "" : extension.toLowerCase();
105
  }
106
107
  private final MediaType mMediaType;
108
  private final List<String> mExtensions;
109
110
  /**
111
   * Several media types have only one corresponding standard file name
112
   * extension; this constructor calls {@link MediaType#getSubtype()} to obtain
113
   * said extension. Some {@link MediaType}s have a single extension but their
114
   * assigned IANA name differs (e.g., {@code svg} maps to {@code svg+xml})
115
   * and thus must not use this constructor.
116
   *
117
   * @param mediaType The {@link MediaType} containing only one extension.
118
   */
119
  MediaTypeExtension( final MediaType mediaType ) {
120
    this( mediaType, of( mediaType.getSubtype() ) );
121
  }
122
123
  /**
124
   * Constructs an association of file name extensions to a single {@link
125
   * MediaType}.
126
   *
127
   * @param mediaType  The {@link MediaType} to associate with the given
128
   *                   file name extensions.
129
   * @param extensions The file name extensions used to lookup a corresponding
130
   *                   {@link MediaType}.
131
   */
132
  MediaTypeExtension(
133
    final MediaType mediaType, final List<String> extensions ) {
134
    assert mediaType != null;
135
    assert extensions != null;
136
    assert !extensions.isEmpty();
137
138
    mMediaType = mediaType;
139
    mExtensions = extensions;
140
  }
141
142
  /**
143
   * Returns the first file name extension in the list of file names given
144
   * at construction time.
145
   *
146
   * @return The one file name to rule them all.
147
   */
148
  public String getExtension() {
149
    return mExtensions.get( 0 );
150
  }
151
152
  boolean isMediaType( final MediaType mediaType ) {
153
    return mMediaType == mediaType;
154
  }
155
156
  private boolean isType( final String sanitized ) {
157
    for( final var extension : mExtensions ) {
158
      if( extension.equalsIgnoreCase( sanitized ) ) {
159
        return true;
160
      }
161
    }
162
163
    return false;
164
  }
165
166
  private MediaType getMediaType() {
167
    return mMediaType;
168
  }
169
}
1170
A src/main/java/com/keenwrite/io/MediaTypeSniffer.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import java.io.*;
5
import java.util.LinkedHashMap;
6
import java.util.Map;
7
8
import static com.keenwrite.io.MediaType.*;
9
import static java.lang.System.arraycopy;
10
11
/**
12
 * Associates file signatures with IANA-defined {@link MediaType}s. See:
13
 * <a href="https://www.garykessler.net/library/file_sigs.html">
14
 * Gary Kessler's List
15
 * </a>,
16
 * <a href="https://en.wikipedia.org/wiki/List_of_file_signatures">
17
 * Wikipedia's List
18
 * </a>, and
19
 * <a href="https://github.com/veniware/Space-Maker/blob/master/FileSignatures.cs">
20
 * Space Maker's List
21
 * </a>
22
 */
23
public class MediaTypeSniffer {
24
  private static final int FORMAT_LENGTH = 11;
25
  private static final int END_OF_DATA = -2;
26
27
  private static final Map<int[], MediaType> FORMAT = new LinkedHashMap<>();
28
29
  private static void put( final int[] data, final MediaType mediaType ) {
30
    FORMAT.put( data, mediaType );
31
  }
32
33
  static {
34
    //@formatter:off
35
    put( ints( 0x3C, 0x73, 0x76, 0x67, 0x20 ), IMAGE_SVG_XML );
36
    put( ints( 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ), IMAGE_PNG );
37
    put( ints( 0xFF, 0xD8, 0xFF, 0xE0 ), IMAGE_JPEG );
38
    put( ints( 0xFF, 0xD8, 0xFF, 0xEE ), IMAGE_JPEG );
39
    put( ints( 0xFF, 0xD8, 0xFF, 0xE1, -1, -1, 0x45, 0x78, 0x69, 0x66, 0x00 ), IMAGE_JPEG );
40
    put( ints( 0x49, 0x49, 0x2A, 0x00 ), IMAGE_TIFF );
41
    put( ints( 0x4D, 0x4D, 0x00, 0x2A ), IMAGE_TIFF );
42
    put( ints( 0x47, 0x49, 0x46, 0x38 ), IMAGE_GIF );
43
    put( ints( 0x52, 0x49, 0x46, 0x46, -1, -1, -1, -1, 0x57, 0x45, 0x42, 0x50 ), IMAGE_WEBP );
44
    put( ints( 0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E ), APP_PDF );
45
    put( ints( 0x25, 0x21, 0x50, 0x53, 0x2D, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x2D ), APP_EPS );
46
    put( ints( 0x25, 0x21, 0x50, 0x53 ), APP_PS );
47
    put( ints( 0x38, 0x42, 0x50, 0x53, 0x00, 0x01 ), IMAGE_PHOTOSHOP );
48
    put( ints( 0x8A, 0x4D, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ), VIDEO_MNG );
49
    put( ints( 0x42, 0x4D ), IMAGE_BMP );
50
    put( ints( 0xFF, 0xFB, 0x30 ), AUDIO_MP3 );
51
    put( ints( 0x49, 0x44, 0x33 ), AUDIO_MP3 );
52
    put( ints( 0x3C, 0x21 ), TEXT_HTML );
53
    put( ints( 0x3C, 0x68, 0x74, 0x6D, 0x6C ), TEXT_HTML );
54
    put( ints( 0x3C, 0x68, 0x65, 0x61, 0x64 ), TEXT_HTML );
55
    put( ints( 0x3C, 0x62, 0x6F, 0x64, 0x79 ), TEXT_HTML );
56
    put( ints( 0x3C, 0x48, 0x54, 0x4D, 0x4C ), TEXT_HTML );
57
    put( ints( 0x3C, 0x48, 0x45, 0x41, 0x44 ), TEXT_HTML );
58
    put( ints( 0x3C, 0x42, 0x4F, 0x44, 0x59 ), TEXT_HTML );
59
    put( ints( 0x3C, 0x3F, 0x78, 0x6D, 0x6C, 0x20 ), TEXT_XML );
60
    put( ints( 0xFE, 0xFF, 0x00, 0x3C, 0x00, 0x3f, 0x00, 0x78 ), TEXT_XML );
61
    put( ints( 0xFF, 0xFE, 0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00 ), TEXT_XML );
62
    put( ints( 0x23, 0x64, 0x65, 0x66 ), IMAGE_X_BITMAP );
63
    put( ints( 0x21, 0x20, 0x58, 0x50, 0x4D, 0x32 ), IMAGE_X_PIXMAP );
64
    put( ints( 0x2E, 0x73, 0x6E, 0x64 ), AUDIO_SIMPLE );
65
    put( ints( 0x64, 0x6E, 0x73, 0x2E ), AUDIO_SIMPLE );
66
    put( ints( 0x52, 0x49, 0x46, 0x46 ), AUDIO_WAV );
67
    put( ints( 0x50, 0x4B ), APP_ZIP );
68
    put( ints( 0x41, 0x43, -1, -1, -1, -1, 0x00, 0x00, 0x00, 0x00, 0x00 ), APP_ACAD );
69
    put( ints( 0xCA, 0xFE, 0xBA, 0xBE ), APP_JAVA );
70
    put( ints( 0xAC, 0xED ), APP_JAVA_OBJECT );
71
    //@formatter:on
72
  }
73
74
  /**
75
   * Returns the {@link MediaType} for a given set of bytes.
76
   *
77
   * @param data Binary data to compare against the list of known formats.
78
   * @return The IANA-defined {@link MediaType}, or
79
   * {@link MediaType#UNDEFINED} if indeterminate.
80
   */
81
  public static MediaType getMediaType( final byte[] data ) {
82
    assert data != null;
83
84
    final var source = new int[]{
85
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
86
    };
87
88
    for( int i = 0; i < data.length; i++ ) {
89
      source[ i ] = data[ i ] & 0xFF;
90
    }
91
92
    for( final var key : FORMAT.keySet() ) {
93
      int i = -1;
94
      boolean matches = true;
95
96
      while( ++i < FORMAT_LENGTH && key[ i ] != END_OF_DATA && matches ) {
97
        matches = key[ i ] == source[ i ] || key[ i ] == -1;
98
      }
99
100
      if( matches ) {
101
        return FORMAT.get( key );
102
      }
103
    }
104
105
    return UNDEFINED;
106
  }
107
108
  /**
109
   * Convenience method to return the probed media type for the given
110
   * {@link SysFile} instance by delegating to
111
   * {@link #getMediaType(InputStream)}.
112
   *
113
   * @param file File to ascertain the {@link MediaType}.
114
   * @return The IANA-defined {@link MediaType}, or
115
   * {@link MediaType#UNDEFINED} if indeterminate.
116
   * @throws IOException Could not read from the {@link File}.
117
   */
118
  public static MediaType getMediaType( final File file )
119
    throws IOException {
120
    try( final var fis = new FileInputStream( file ) ) {
121
      return getMediaType( fis );
122
    }
123
  }
124
125
  /**
126
   * Convenience method to return the probed media type for the given
127
   * {@link BufferedInputStream} instance. <strong>This resets the stream
128
   * pointer</strong> making the call idempotent. Users of this class should
129
   * prefer to call this method when operating on streams to avoid advancing
130
   * the stream.
131
   *
132
   * @param bis Data source to ascertain the {@link MediaType}.
133
   * @return The IANA-defined {@link MediaType}, or
134
   * {@link MediaType#UNDEFINED} if indeterminate.
135
   * @throws IOException Could not read from the stream.
136
   */
137
  public static MediaType getMediaType( final BufferedInputStream bis )
138
    throws IOException {
139
    bis.mark( FORMAT_LENGTH );
140
    final var result = getMediaType( (InputStream) bis );
141
    bis.reset();
142
143
    return result;
144
  }
145
146
  /**
147
   * Returns the probed media type for the given {@link InputStream} instance.
148
   * The caller is responsible for closing the stream. <strong>This advances
149
   * the stream.</strong> Use {@link #getMediaType(BufferedInputStream)} to
150
   * perform a non-destructive read.
151
   *
152
   * @param is Data source to ascertain the {@link MediaType}.
153
   * @return The IANA-defined {@link MediaType}, or
154
   * {@link MediaType#UNDEFINED} if indeterminate.
155
   * @throws IOException Could not read from the {@link InputStream}.
156
   */
157
  private static MediaType getMediaType( final InputStream is )
158
    throws IOException {
159
    final var input = new byte[ FORMAT_LENGTH ];
160
    final var count = is.read( input, 0, FORMAT_LENGTH );
161
162
    if( count > 1 ) {
163
      final var available = new byte[ count ];
164
      arraycopy( input, 0, available, 0, count );
165
      return getMediaType( available );
166
    }
167
168
    return UNDEFINED;
169
  }
170
171
  /**
172
   * Creates integer array from the given data, padded with
173
   * {@link #END_OF_DATA} values up to {@link #FORMAT_LENGTH}.
174
   *
175
   * @param data The input byte values to pad.
176
   * @return The data with padding.
177
   */
178
  private static int[] ints( final int... data ) {
179
    final var magic = new int[ FORMAT_LENGTH + 1 ];
180
    int i = -1;
181
182
    while( ++i < data.length ) {
183
      magic[ i ] = data[ i ];
184
    }
185
186
    while( i < FORMAT_LENGTH ) {
187
      magic[ i++ ] = END_OF_DATA;
188
    }
189
190
    return magic;
191
  }
192
}
1193
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.util.concurrent.Executors.newFixedThreadPool;
11
12
/**
13
 * Consumes the standard output of a {@link Process} created from a
14
 * {@link ProcessBuilder}. Directs the output to a {@link Consumer} of
15
 * strings. This will run on its own thread and close the stream when
16
 * no more data can be processed.
17
 * <p>
18
 * <strong>Warning:</strong> Do not use this with binary data, it is only
19
 * meant for text streams, such as standard out from running command-line
20
 * applications.
21
 * </p>
22
 */
23
public class StreamGobbler implements Callable<Boolean> {
24
  private final InputStream mInput;
25
  private final Consumer<String> mConsumer;
26
27
  /**
28
   * Constructs a new instance of {@link StreamGobbler} that is capable of
29
   * reading an {@link InputStream} and passing each line of textual data from
30
   * that stream over to a string {@link Consumer}.
31
   *
32
   * @param input    The stream having input to pass to the consumer.
33
   * @param consumer The {@link Consumer} that receives each line.
34
   */
35
  private StreamGobbler(
36
    final InputStream input,
37
    final Consumer<String> consumer ) {
38
    assert input != null;
39
    assert consumer != null;
40
41
    mInput = input;
42
    mConsumer = consumer;
43
  }
44
45
  /**
46
   * Consumes the input until no more data is available. Closes the stream.
47
   *
48
   * @return {@link Boolean#TRUE} always.
49
   * @throws IOException Could not read from the stream.
50
   */
51
  @Override
52
  public Boolean call() throws IOException {
53
    try( final var input = new InputStreamReader( mInput );
54
         final var buffer = new BufferedReader( input ) ) {
55
      buffer.lines().forEach( mConsumer );
56
    }
57
58
    return Boolean.TRUE;
59
  }
60
61
  /**
62
   * Reads the given {@link InputStream} on a separate thread and passes
63
   * each line of text input to the given {@link Consumer}.
64
   *
65
   * @param inputStream The stream having input to pass to the consumer.
66
   * @param consumer    The {@link Consumer} that receives each line.
67
   */
68
  public static void gobble(
69
    final InputStream inputStream, final Consumer<String> consumer ) {
70
    try( final var executor = newFixedThreadPool( 1 ) ) {
71
      executor.submit( new StreamGobbler( inputStream, consumer ) );
72
    }
73
  }
74
}
175
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 java.io.File;
5
import java.io.FileInputStream;
6
import java.io.IOException;
7
import java.nio.file.Path;
8
import java.security.MessageDigest;
9
import java.security.NoSuchAlgorithmException;
10
import java.util.Optional;
11
import java.util.function.Function;
12
import java.util.regex.Pattern;
13
14
import static com.keenwrite.util.DataTypeConverter.toHex;
15
import static java.lang.System.getenv;
16
import static java.nio.file.Files.isExecutable;
17
import static java.util.regex.Pattern.compile;
18
import static java.util.regex.Pattern.quote;
19
import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
20
21
/**
22
 * Responsible for file-related functionality.
23
 */
24
public final class SysFile extends java.io.File {
25
  /**
26
   * For finding executable programs. These are used in an O( n^2 ) search,
27
   * so don't add more entries than necessary.
28
   */
29
  private static final String[] EXTENSIONS = new String[]
30
    {"", ".exe", ".bat", ".cmd", ".msi", ".com"};
31
32
  /**
33
   * Number of bytes to read at a time when computing this file's checksum.
34
   */
35
  private static final int BUFFER_SIZE = 16384;
36
37
  //@formatter:off
38
  private static final String SYS_KEY =
39
    "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
40
  private static final String USR_KEY =
41
    "HKEY_CURRENT_USER\\Environment";
42
  //@formatter:on
43
44
  /**
45
   * Regular expression pattern for matching %VARIABLE% names.
46
   */
47
  private static final String VAR_REGEX = "%.*?%";
48
  private static final Pattern VAR_PATTERN = compile( VAR_REGEX );
49
50
  private static final String REG_REGEX = "\\s*path\\s+REG_EXPAND_SZ\\s+(.*)";
51
  private static final Pattern REG_PATTERN = compile( REG_REGEX );
52
53
  /**
54
   * Creates a new instance for a given file name.
55
   *
56
   * @param filename Filename to query existence as executable.
57
   */
58
  public SysFile( final String filename ) {
59
    super( filename );
60
  }
61
62
  /**
63
   * Creates a new instance for a given {@link File}. This is useful for
64
   * validating checksums against an existing {@link File} instance that
65
   * may optionally exist in a directory listed in the PATH environment
66
   * variable.
67
   *
68
   * @param file The file to change into a "system file".
69
   */
70
  public SysFile( final File file ) {
71
    super( file.getAbsolutePath() );
72
  }
73
74
  /**
75
   * Answers whether the path returned from {@link #locate()} is an executable
76
   * that can be run using a {@link ProcessBuilder}.
77
   */
78
  public boolean canRun() {
79
    return locate().isPresent();
80
  }
81
82
  /**
83
   * For a file name that represents an executable (without an extension)
84
   * file, this determines the first matching executable found in the PATH
85
   * environment variable. This will search the PATH each time the method
86
   * is invoked, triggering a full directory scan for all paths listed in
87
   * the environment variable. The result is not cached, so avoid calling
88
   * this in a critical loop.
89
   * <p>
90
   * After installing software, the software might be located in the PATH,
91
   * but not available to run by its name alone. In such cases, we need the
92
   * absolute path to the executable to run it. This will always return
93
   * the fully qualified path, otherwise an empty result.
94
   *
95
   * @param map The mapping function of registry variable names to values.
96
   * @return The fully qualified {@link Path} to the executable filename
97
   * provided at construction time.
98
   */
99
  public Optional<Path> locate( final Function<String, String> map ) {
100
    final var exe = getName();
101
    final var paths = paths( map ).split( quote( pathSeparator ) );
102
103
    for( final var path : paths ) {
104
      final var p = Path.of( path ).resolve( exe );
105
106
      for( final var extension : EXTENSIONS ) {
107
        final var filename = Path.of( p + extension );
108
109
        if( isExecutable( filename ) ) {
110
          return Optional.of( filename );
111
        }
112
      }
113
    }
114
115
    return Optional.empty();
116
  }
117
118
  /**
119
   * Convenience method that locates a binary executable file in the path
120
   * by using {@link System#getenv(String)} to retrieve environment variables
121
   * that are expanded when parsing the PATH.
122
   *
123
   * @see #locate(Function)
124
   */
125
  public Optional<Path> locate() {
126
    return locate( System::getenv );
127
  }
128
129
  /**
130
   * Changes to the PATH environment variable aren't reflected for the
131
   * currently running task. The registry, however, contains the updated
132
   * value. Reading the registry is a hack.
133
   *
134
   * @param map The mapping function of registry variable names to values.
135
   * @return The revised PATH variables as stored in the registry.
136
   */
137
  private String paths( final Function<String, String> map ) {
138
    return IS_OS_WINDOWS ? pathsWindows( map ) : pathsSane();
139
  }
140
141
  private String pathsSane() {
142
    return getenv( "PATH" );
143
  }
144
145
  private String pathsWindows( final Function<String, String> map ) {
146
    try {
147
      final var hklm = query( SYS_KEY );
148
      final var hkcu = query( USR_KEY );
149
150
      return expand( hklm, map ) + pathSeparator + expand( hkcu, map );
151
    } catch( final IOException ex ) {
152
      // Return the PATH environment variable if the registry query fails.
153
      return pathsSane();
154
    }
155
  }
156
157
  /**
158
   * Queries a registry key PATH value.
159
   *
160
   * @param key The registry key name to look up.
161
   * @return The value for the registry key.
162
   */
163
  private String query( final String key ) throws IOException {
164
    final var regVarName = "path";
165
    final var args = new String[]{"reg", "query", key, "/v", regVarName};
166
    final var process = Runtime.getRuntime().exec( args );
167
    final var stream = process.getInputStream();
168
    final var regValue = new StringBuffer( 1024 );
169
170
    StreamGobbler.gobble( stream, text -> {
171
      if( text.contains( regVarName ) ) {
172
        regValue.append( parseRegEntry( text ) );
173
      }
174
    } );
175
176
    try {
177
      process.waitFor();
178
    } catch( final InterruptedException ex ) {
179
      throw new IOException( ex );
180
    } finally {
181
      process.destroy();
182
    }
183
184
185
    return regValue.toString();
186
  }
187
188
  String parseRegEntry( final String text ) {
189
    assert text != null;
190
191
    final var matcher = REG_PATTERN.matcher( text );
192
    return matcher.find() ? matcher.group( 1 ) : text.trim();
193
  }
194
195
  /**
196
   * PATH environment variables returned from the registry have unexpanded
197
   * variables of the form %VARIABLE%. This method will expand those values,
198
   * if possible, from the environment. This will only perform a single
199
   * expansion, which should be adequate for most needs.
200
   *
201
   * @param s The %VARIABLE%-encoded value to expand.
202
   * @return The given value with all encoded values expanded.
203
   */
204
  String expand( final String s, final Function<String, String> map ) {
205
    // Assigned to the unexpanded string, initially.
206
    String expanded = s;
207
208
    final var matcher = VAR_PATTERN.matcher( expanded );
209
210
    while( matcher.find() ) {
211
      final var match = matcher.group( 0 );
212
      String value = map.apply( match );
213
214
      if( value == null ) {
215
        value = "";
216
      }
217
      else {
218
        value = value.replace( "\\", "\\\\" );
219
      }
220
221
      final var subexpr = compile( quote( match ) );
222
      expanded = subexpr.matcher( expanded ).replaceAll( value );
223
    }
224
225
    return expanded;
226
  }
227
228
  /**
229
   * Answers whether this file's SHA-256 checksum equals the given
230
   * hexadecimal-encoded checksum string.
231
   *
232
   * @param hex The string to compare against the checksum for this file.
233
   * @return {@code true} if the checksums match; {@code false} on any
234
   * error or checksums don't match.
235
   */
236
  public boolean isChecksum( final String hex ) {
237
    assert hex != null;
238
239
    try {
240
      return checksum( "SHA-256" ).equalsIgnoreCase( hex );
241
    } catch( final Exception ex ) {
242
      return false;
243
    }
244
  }
245
246
  /**
247
   * Returns the hash code for this file.
248
   *
249
   * @return The hex-encoded hash code for the file contents.
250
   */
251
  @SuppressWarnings( "SameParameterValue" )
252
  private String checksum( final String algorithm )
253
    throws NoSuchAlgorithmException, IOException {
254
    final var digest = MessageDigest.getInstance( algorithm );
255
256
    try( final var in = new FileInputStream( this ) ) {
257
      final var bytes = new byte[ BUFFER_SIZE ];
258
      int count;
259
260
      while( (count = in.read( bytes )) != -1 ) {
261
        digest.update( bytes, 0, count );
262
      }
263
264
      return toHex( digest.digest() );
265
    }
266
  }
267
}
1268
A src/main/java/com/keenwrite/io/UserDataDir.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import java.nio.file.Path;
5
6
import static java.lang.System.getProperty;
7
import static java.lang.System.getenv;
8
import static org.apache.commons.lang3.SystemUtils.*;
9
10
/**
11
 * Responsible for determining the directory to write application data, across
12
 * multiple platforms. See also:
13
 *
14
 * <ul>
15
 * <li>
16
 *   <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">
17
 *     Linux: XDG Base Directory Specification
18
 *   </a>
19
 * </li>
20
 * <li>
21
 *   <a href="https://learn.microsoft.com/en-us/windows/deployment/usmt/usmt-recognized-environment-variables">
22
 *     Windows: Recognized environment variables
23
 *   </a>
24
 * </li>
25
 * <li>
26
 *   <a href="https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html">
27
 *     macOS: File System Programming Guide
28
 *   </a>
29
 * </li>
30
 * </ul>
31
 * </p>
32
 */
33
public final class UserDataDir {
34
35
  private static final Path UNDEFINED = Path.of( "/" );
36
37
  private static final String PROP_USER_HOME = getProperty( "user.home" );
38
  private static final String PROP_USER_DIR = getProperty( "user.dir" );
39
  private static final String PROP_OS_VERSION = getProperty( "os.version" );
40
  private static final String ENV_APPDATA = getenv( "AppData" );
41
  private static final String ENV_XDG_DATA_HOME = getenv( "XDG_DATA_HOME" );
42
43
  private UserDataDir() { }
44
45
  /**
46
   * Makes a valiant attempt at determining where to create application-specific
47
   * files, regardless of operating system.
48
   *
49
   * @param appName The application name that seeks to create files.
50
   * @return A fully qualified {@link Path} to a directory wherein files may
51
   * be created that are user- and application-specific.
52
   */
53
  public static Path getAppPath( final String appName ) {
54
    final var osPath = isWindows()
55
      ? getWinAppPath()
56
      : isMacOs()
57
      ? getMacAppPath()
58
      : isUnix()
59
      ? getUnixAppPath()
60
      : UNDEFINED;
61
62
    final var path = osPath.equals( UNDEFINED )
63
      ? getDefaultAppPath( appName )
64
      : osPath.resolve( appName );
65
66
    final var alternate = Path.of( PROP_USER_DIR, appName );
67
68
    return ensureExists( path )
69
      ? path
70
      : ensureExists( alternate )
71
      ? alternate
72
      : Path.of( PROP_USER_DIR );
73
  }
74
75
  private static Path getWinAppPath() {
76
    return isValid( ENV_APPDATA )
77
      ? Path.of( ENV_APPDATA )
78
      : home( getWinVerAppPath() );
79
  }
80
81
  /**
82
   * Gets the application path with respect to the Windows version.
83
   *
84
   * @return The directory name paths relative to the user's home directory.
85
   */
86
  private static String[] getWinVerAppPath() {
87
    return PROP_OS_VERSION.startsWith( "5." )
88
      ? new String[]{"Application Data"}
89
      : new String[]{"AppData", "Roaming"};
90
  }
91
92
  private static Path getMacAppPath() {
93
    final var path = home( "Library", "Application Support" );
94
95
    return ensureExists( path ) ? path : UNDEFINED;
96
  }
97
98
  private static Path getUnixAppPath() {
99
    // Fallback in case the XDG data directory is undefined.
100
    var path = home( ".local", "share" );
101
102
    if( isValid( ENV_XDG_DATA_HOME ) ) {
103
      final var xdgPath = Path.of( ENV_XDG_DATA_HOME );
104
105
      path = ensureExists( xdgPath ) ? xdgPath : path;
106
    }
107
108
    return path;
109
  }
110
111
  /**
112
   * Returns a hidden directory relative to the user's home directory.
113
   *
114
   * @param appName The application name.
115
   * @return A suitable directory for storing application files.
116
   */
117
  private static Path getDefaultAppPath( final String appName ) {
118
    return home( '.' + appName );
119
  }
120
121
  private static Path home( final String... paths ) {
122
    return Path.of( PROP_USER_HOME, paths );
123
  }
124
125
  /**
126
   * Verifies whether the path exists or was created.
127
   *
128
   * @param path The directory to verify.
129
   * @return {@code true} if the path already exists or was created,
130
   * {@code false} if the directory doesn't exist and couldn't be created.
131
   */
132
  private static boolean ensureExists( final Path path ) {
133
    final var file = path.toFile();
134
    return file.exists() || file.mkdirs();
135
  }
136
137
  /**
138
   * Answers whether the given string contains content.
139
   *
140
   * @param s The string to check, may be {@code null}.
141
   * @return {@code true} if the string is neither {@code null} nor blank.
142
   */
143
  private static boolean isValid( final String s ) {
144
    return !(s == null || s.isBlank());
145
  }
146
147
  private static boolean isWindows() {
148
    return IS_OS_WINDOWS;
149
  }
150
151
  private static boolean isMacOs() {
152
    return IS_OS_MAC;
153
  }
154
155
  private static boolean isUnix() {
156
    return IS_OS_UNIX;
157
  }
158
}
1159
A src/main/java/com/keenwrite/io/Zip.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io;
3
4
import java.io.IOException;
5
import java.io.UncheckedIOException;
6
import java.nio.charset.StandardCharsets;
7
import java.nio.file.Files;
8
import java.nio.file.Path;
9
import java.util.concurrent.atomic.AtomicReference;
10
import java.util.function.BiConsumer;
11
import java.util.zip.ZipEntry;
12
import java.util.zip.ZipFile;
13
14
import static java.nio.file.Files.createDirectories;
15
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
16
17
/**
18
 * Responsible for managing zipped archive files. Does not handle archives
19
 * within archives.
20
 */
21
public final class Zip {
22
  /**
23
   * Extracts the contents of the zip archive into its current directory. The
24
   * contents of the archive must be {@link StandardCharsets#UTF_8}. For
25
   * example, if the {@link Path} is <code>/tmp/filename.zip</code>, then
26
   * the contents of the file will be extracted into <code>/tmp</code>.
27
   *
28
   * @param zipPath The {@link Path} to the zip file to extract.
29
   * @throws IOException Could not extract the zip file, zip entries, or find
30
   *                     the parent directory that contains the path to the
31
   *                     zip archive.
32
   */
33
  public static void extract( final Path zipPath ) throws IOException {
34
    final var path = zipPath.getParent().normalize();
35
36
    iterate( zipPath, ( zipFile, zipEntry ) -> {
37
      // Determine the directory name where the zip archive resides. Files will
38
      // be extracted relative to that directory.
39
      final var zipEntryPath = path.resolve( zipEntry.getName() );
40
41
      // Guard against zip slip.
42
      if( zipEntryPath.normalize().startsWith( path ) ) {
43
        try {
44
          extract( zipFile, zipEntry, zipEntryPath );
45
        } catch( final IOException ex ) {
46
          throw new UncheckedIOException( ex );
47
        }
48
      }
49
    } );
50
  }
51
52
  /**
53
   * Returns the first root-level directory found in the zip archive. Only call
54
   * this function if you know there is exactly one top-level directory in the
55
   * zip archive. If there are multiple top-level directories, one of the
56
   * directories will be returned, albeit indeterminately. No files are
57
   * extracted when calling this function.
58
   *
59
   * @param zipPath The path to the zip archive to process.
60
   * @return The fully qualified root-level directory resolved relatively to
61
   * the zip archive itself.
62
   * @throws IOException Could not process the zip archive.
63
   */
64
  public static Path root( final Path zipPath ) throws IOException {
65
    // Directory that contains the zip archive file.
66
    final var zipParent = zipPath.getParent();
67
68
    if( zipParent == null ) {
69
      throw new IOException( zipPath + " has no parent" );
70
    }
71
72
    final var result = new AtomicReference<>( zipParent );
73
74
    iterate( zipPath, ( zipFile, zipEntry ) -> {
75
      final var zipEntryPath = Path.of( zipEntry.getName() );
76
77
      // The first entry without a parent is considered the root-level entry.
78
      // Return the relative directory path to that entry.
79
      if( zipEntryPath.getParent() == null ) {
80
        result.set( zipParent.resolve( zipEntryPath ) );
81
      }
82
    } );
83
84
    // The zip file doesn't have a sane folder structure, so return the
85
    // directory where the zip file was found.
86
    return result.get();
87
  }
88
89
  /**
90
   * Processes each entry in the zip archive.
91
   *
92
   * @param zipPath  The path to the zip file being processed.
93
   * @param consumer The {@link BiConsumer} that receives each entry in the
94
   *                 zip archive.
95
   * @throws IOException Could not extract zip file entries.
96
   */
97
  private static void iterate(
98
    final Path zipPath,
99
    final BiConsumer<ZipFile, ZipEntry> consumer )
100
    throws IOException {
101
    assert zipPath.toFile().isFile();
102
103
    try( final var zipFile = new ZipFile( zipPath.toFile() ) ) {
104
      final var entries = zipFile.entries();
105
106
      while( entries.hasMoreElements() ) {
107
        consumer.accept( zipFile, entries.nextElement() );
108
      }
109
    }
110
  }
111
112
  /**
113
   * Extracts a single entry of a zip file to a given directory. This will
114
   * create the necessary directory path if it doesn't exist. Empty
115
   * directories are not re-created.
116
   *
117
   * @param zipFile      The zip archive to extract.
118
   * @param zipEntry     An entry in the zip archive.
119
   * @param zipEntryPath The file location to write the zip entry.
120
   * @throws IOException Could not extract the zip file entry.
121
   */
122
  private static void extract(
123
    final ZipFile zipFile,
124
    final ZipEntry zipEntry,
125
    final Path zipEntryPath ) throws IOException {
126
    // Only extract files, skip empty directories.
127
    if( !zipEntry.isDirectory() ) {
128
      createDirectories( zipEntryPath.getParent() );
129
130
      try( final var in = zipFile.getInputStream( zipEntry ) ) {
131
        Files.copy( in, zipEntryPath, REPLACE_EXISTING );
132
      }
133
    }
134
  }
135
}
1136
A src/main/java/com/keenwrite/io/downloads/DownloadManager.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io.downloads;
3
4
import com.keenwrite.io.MediaType;
5
import com.keenwrite.io.MediaTypeSniffer;
6
7
import java.io.*;
8
import java.net.HttpURLConnection;
9
import java.net.URI;
10
import java.net.URL;
11
import java.time.Duration;
12
import java.util.zip.GZIPInputStream;
13
14
import static java.lang.Math.toIntExact;
15
import static java.lang.String.format;
16
import static java.lang.System.*;
17
import static java.net.HttpURLConnection.HTTP_OK;
18
import static java.net.HttpURLConnection.setFollowRedirects;
19
20
/**
21
 * Responsible for downloading files and publishing status updates. This will
22
 * download a resource provided by an instance of {@link URL} into a given
23
 * {@link OutputStream}.
24
 */
25
public final class DownloadManager {
26
  static {
27
    setProperty( "http.keepAlive", "false" );
28
    setFollowRedirects( true );
29
  }
30
31
  /**
32
   * Number of bytes to read at a time.
33
   */
34
  private static final int BUFFER_SIZE = 16384;
35
36
  /**
37
   * HTTP request timeout.
38
   */
39
  private static final Duration TIMEOUT = Duration.ofSeconds( 30 );
40
41
  @FunctionalInterface
42
  public interface ProgressListener {
43
    /**
44
     * Called when a chunk of data has been read. This is called synchronously
45
     * when downloading the data; do not execute long-running tasks in this
46
     * method (a few milliseconds is fine).
47
     *
48
     * @param percentage A value between 0 and 100, inclusive, represents the
49
     *                   percentage of bytes downloaded relative to the total.
50
     *                   A value of -1 means the total number of bytes to
51
     *                   download is unknown.
52
     * @param bytes      When {@code percentage} is greater than or equal to
53
     *                   zero, this is the total number of bytes. When {@code
54
     *                   percentage} equals -1, this is the number of bytes
55
     *                   read so far.
56
     */
57
    void update( int percentage, long bytes );
58
  }
59
60
  /**
61
   * Callers may check the value of isSuccessful
62
   */
63
  public static final class DownloadToken implements Closeable {
64
    private final HttpURLConnection mConn;
65
    private final BufferedInputStream mInput;
66
    private final MediaType mMediaType;
67
    private final long mBytesTotal;
68
69
    private DownloadToken(
70
      final HttpURLConnection conn,
71
      final BufferedInputStream input,
72
      final MediaType mediaType
73
    ) {
74
      assert conn != null;
75
      assert input != null;
76
      assert mediaType != null;
77
78
      mConn = conn;
79
      mInput = input;
80
      mMediaType = mediaType;
81
      mBytesTotal = conn.getContentLength();
82
    }
83
84
    /**
85
     * Provides the ability to download remote files asynchronously while
86
     * being updated regarding the download progress. The given
87
     * {@link OutputStream} will be closed after downloading is complete.
88
     *
89
     * @param output   Where to write the file contents.
90
     * @param listener Receives download progress status updates.
91
     * @return A {@link Runnable} task that can be executed in the background
92
     * to download the resource for this {@link DownloadToken}.
93
     */
94
    public Runnable download(
95
      final OutputStream output,
96
      final ProgressListener listener ) {
97
      return () -> {
98
        final var buffer = new byte[ BUFFER_SIZE ];
99
        final var stream = getInputStream();
100
        final var bytesTotal = mBytesTotal;
101
102
        long bytesTally = 0;
103
        int bytesRead;
104
105
        try( output ) {
106
          while( (bytesRead = stream.read( buffer )) != -1 ) {
107
            if( Thread.currentThread().isInterrupted() ) {
108
              throw new InterruptedException();
109
            }
110
111
            bytesTally += bytesRead;
112
113
            if( bytesTotal > 0 ) {
114
              listener.update(
115
                toIntExact( bytesTally * 100 / bytesTotal ),
116
                bytesTotal
117
              );
118
            }
119
            else {
120
              listener.update( -1, bytesRead );
121
            }
122
123
            output.write( buffer, 0, bytesRead );
124
          }
125
        } catch( final Exception ex ) {
126
          throw new RuntimeException( ex );
127
        } finally {
128
          close();
129
        }
130
      };
131
    }
132
133
    public void close() {
134
      try {
135
        getInputStream().close();
136
      } catch( final Exception ignored ) {
137
      } finally {
138
        mConn.disconnect();
139
      }
140
    }
141
142
    /**
143
     * Returns the input stream to the resource to download.
144
     *
145
     * @return The stream to read.
146
     */
147
    public BufferedInputStream getInputStream() {
148
      return mInput;
149
    }
150
151
    public MediaType getMediaType() {
152
      return mMediaType;
153
    }
154
155
    /**
156
     * Answers whether the type of content associated with the download stream
157
     * is a scalable vector graphic.
158
     *
159
     * @return {@code true} if the given {@link MediaType} has SVG contents.
160
     */
161
    public boolean isSvg() {
162
      return getMediaType().isSvg();
163
    }
164
  }
165
166
  /**
167
   * Opens the input stream for the resource to download.
168
   *
169
   * @param url The {@link URL} resource to download.
170
   * @return A token that can be used for downloading the content with
171
   * periodic updates or retrieving the stream for downloading the content.
172
   * @throws IOException The stream could not be opened.
173
   */
174
  public static DownloadToken open( final String url ) throws IOException {
175
    // Pass an undefined media type so that any type of file can be retrieved.
176
    return open( new URL( url ) );
177
  }
178
179
  public static DownloadToken open( final URI uri )
180
    throws IOException {
181
    return open( uri.toURL() );
182
  }
183
184
  /**
185
   * Opens the input stream for the resource to download and verifies that
186
   * the given {@link MediaType} matches the requested type. Callers are
187
   * responsible for closing the {@link DownloadManager} to close the
188
   * underlying stream and the HTTP connection. Connections must be closed by
189
   * callers if {@link DownloadToken#download(OutputStream, ProgressListener)}
190
   * isn't called (i.e., {@link DownloadToken#getMediaType()} is called
191
   * after the transport layer's Content-Type is requested but not contents
192
   * are downloaded).
193
   *
194
   * @param url The {@link URL} resource to download.
195
   * @return A token that can be used for downloading the content with
196
   * periodic updates or retrieving the stream for downloading the content.
197
   * @throws IOException The resource could not be downloaded.
198
   */
199
  public static DownloadToken open( final URL url ) throws IOException {
200
    final var conn = connect( url );
201
202
    MediaType contentType;
203
204
    try {
205
      contentType = MediaType.valueFrom( conn.getContentType() );
206
    } catch( final Exception ex ) {
207
      // If the media type couldn't be detected, try using the stream.
208
      contentType = MediaType.UNDEFINED;
209
    }
210
211
    final var input = open( conn );
212
213
    // Peek at the magic header bytes to determine the media type.
214
    final var magicType = MediaTypeSniffer.getMediaType( input );
215
216
    // If the transport protocol's Content-Type doesn't align with the
217
    // media type for the magic header, defer to the transport protocol.
218
    final MediaType mediaType =
219
      !contentType.equals( magicType ) && !magicType.isUndefined()
220
        ? contentType
221
        : magicType;
222
223
    return new DownloadToken( conn, input, mediaType );
224
  }
225
226
  /**
227
   * Establishes a connection to the remote {@link URL} resource.
228
   *
229
   * @param url The {@link URL} representing a resource to download.
230
   * @return The connection manager for the {@link URL}.
231
   * @throws IOException         Could not establish a connection.
232
   * @throws ArithmeticException Could not compute a timeout value (this
233
   *                             should never happen because the timeout is
234
   *                             less than a minute).
235
   * @see #TIMEOUT
236
   */
237
  private static HttpURLConnection connect( final URL url )
238
    throws IOException, ArithmeticException {
239
    // Both HTTP and HTTPS are covered by this condition.
240
    if( url.openConnection() instanceof HttpURLConnection conn ) {
241
      conn.setUseCaches( false );
242
      conn.setInstanceFollowRedirects( true );
243
      conn.setRequestProperty( "Accept-Encoding", "gzip" );
244
      conn.setRequestProperty( "User-Agent", getProperty( "http.agent" ) );
245
      conn.setRequestMethod( "GET" );
246
      conn.setConnectTimeout( toIntExact( TIMEOUT.toMillis() ) );
247
      conn.setRequestProperty( "connection", "close" );
248
      conn.connect();
249
250
      final var code = conn.getResponseCode();
251
252
      if( code != HTTP_OK ) {
253
        final var message = format(
254
          "%s [HTTP %d: %s]",
255
          url.getFile(),
256
          code,
257
          conn.getResponseMessage()
258
        );
259
260
        throw new IOException( message );
261
      }
262
263
      return conn;
264
    }
265
266
    throw new UnsupportedOperationException( url.toString() );
267
  }
268
269
  /**
270
   * Returns a stream in an open state. Callers are responsible for closing.
271
   *
272
   * @param conn The connection to open, which could be compressed.
273
   * @return The open stream.
274
   * @throws IOException Could not open the stream.
275
   */
276
  private static BufferedInputStream open( final HttpURLConnection conn )
277
    throws IOException {
278
    return open( conn.getContentEncoding(), conn.getInputStream() );
279
  }
280
281
  /**
282
   * Returns a stream in an open state. Callers are responsible for closing.
283
   * The input stream may be compressed.
284
   *
285
   * @param encoding The content encoding for the stream.
286
   * @param is       The stream to wrap with a suitable decoder.
287
   * @return The open stream, with any gzip content-encoding decoded.
288
   * @throws IOException Could not open the stream.
289
   */
290
  private static BufferedInputStream open(
291
    final String encoding, final InputStream is ) throws IOException {
292
    return new BufferedInputStream(
293
      "gzip".equalsIgnoreCase( encoding )
294
        ? new GZIPInputStream( is )
295
        : is
296
    );
297
  }
298
}
1299
A src/main/java/com/keenwrite/io/downloads/events/DownloadConnectionFailedEvent.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io.downloads.events;
3
4
import java.net.URL;
5
6
/**
7
 * Collates information about an HTTP connection that could not be established.
8
 */
9
public class DownloadConnectionFailedEvent extends DownloadEvent {
10
11
  private final Exception mEx;
12
13
  /**
14
   * Constructs a new event that tracks the status of downloading a file.
15
   *
16
   * @param url The {@link URL} that has triggered a download event.
17
   * @param ex  The reason the connection failed.
18
   */
19
  public DownloadConnectionFailedEvent(
20
    final URL url, final Exception ex ) {
21
    super( url );
22
    mEx = ex;
23
  }
24
25
  public static void fire( final URL url, final Exception ex ) {
26
    new DownloadConnectionFailedEvent( url, ex ).publish();
27
  }
28
29
  /**
30
   * Returns the {@link Exception} that caused this event to be published.
31
   *
32
   * @return The {@link Exception} encountered when establishing a connection.
33
   */
34
  public Exception getException() {
35
    return mEx;
36
  }
37
}
138
A src/main/java/com/keenwrite/io/downloads/events/DownloadEvent.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io.downloads.events;
3
4
import com.keenwrite.events.AppEvent;
5
6
import java.net.URL;
7
import java.time.Instant;
8
9
/**
10
 * The parent class to all download-related status events.
11
 */
12
public class DownloadEvent implements AppEvent {
13
14
  private final Instant mInstant = Instant.now();
15
  private final URL mUrl;
16
17
  /**
18
   * Constructs a new event that tracks the status of downloading a file.
19
   *
20
   * @param url The {@link URL} that has triggered a download event.
21
   */
22
  public DownloadEvent( final URL url ) {
23
    mUrl = url;
24
  }
25
26
  /**
27
   * Returns the download link as an instance of {@link URL}.
28
   *
29
   * @return The {@link URL} being downloaded.
30
   */
31
  public URL getUrl() {
32
    return mUrl;
33
  }
34
35
  /**
36
   * Returns the moment in time that this event was published.
37
   *
38
   * @return The published date and time.
39
   */
40
  public Instant when() {
41
    return mInstant;
42
  }
43
}
144
A src/main/java/com/keenwrite/io/downloads/events/DownloadFailedEvent.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io.downloads.events;
3
4
import java.net.URL;
5
6
public class DownloadFailedEvent extends DownloadEvent {
7
8
  private final int mResponseCode;
9
10
  /**
11
   * Constructs a new event that indicates downloading a file was not
12
   * successful.
13
   *
14
   * @param url          The {@link URL} that has triggered a download event.
15
   * @param responseCode The HTTP response code associated with the failure.
16
   */
17
  public DownloadFailedEvent( final URL url, final int responseCode ) {
18
    super( url );
19
20
    mResponseCode = responseCode;
21
  }
22
23
  public static void fire( final URL url, final int responseCode ) {
24
    new DownloadFailedEvent( url, responseCode ).publish();
25
  }
26
27
  /**
28
   * Returns the HTTP response code for a failed download.
29
   *
30
   * @return An HTTP response code.
31
   */
32
  public int getResponseCode() {
33
    return mResponseCode;
34
  }
35
}
136
A src/main/java/com/keenwrite/io/downloads/events/DownloadStartedEvent.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io.downloads.events;
3
4
import java.net.URL;
5
6
/**
7
 * Collates information about a document that has started downloading.
8
 */
9
public class DownloadStartedEvent extends DownloadEvent {
10
11
  public DownloadStartedEvent( final URL url ) {
12
    super( url );
13
  }
14
15
  public static void fire( final URL url ) {
16
    new DownloadStartedEvent( url ).publish();
17
  }
18
}
119
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_IMAGES = key( KEY_ROOT, "images" );
33
  public static final Key KEY_CACHES_DIR = key( KEY_IMAGES, "cache" );
34
  public static final Key KEY_IMAGES_DIR = key( KEY_IMAGES, "dir" );
35
  public static final Key KEY_IMAGES_ORDER = key( KEY_IMAGES, "order" );
36
  public static final Key KEY_IMAGES_RESIZE = key( KEY_IMAGES, "resize" );
37
  public static final Key KEY_IMAGES_SERVER = key( KEY_IMAGES, "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 based using
89
   * {@link SkeletonStorageHandler} and
90
   * numerous {@link Category} objects.
91
   *
92
   * @return A component for editing preferences.
93
   * @throws RuntimeException Could not construct the {@link PreferencesFx}
94
   *                          object (e.g., illegal access permissions,
95
   *                          unmapped XML resource).
96
   */
97
  private PreferencesFx createPreferencesFx() {
98
    return PreferencesFx.of( createStorageHandler(), createCategories() )
99
                        .instantPersistent( false )
100
                        .dialogIcon( ICON_DIALOG );
101
  }
102
103
  /**
104
   * Override the {@link PreferencesFx} storage handler to perform no actions.
105
   * Persistence is accomplished using the {@link XmlStore}.
106
   *
107
   * @return A no-op {@link StorageHandler} implementation.
108
   */
109
  private StorageHandler createStorageHandler() {
110
    return new SkeletonStorageHandler();
111
  }
112
113
  private Category[] createCategories() {
114
    return new Category[]{
115
      Category.of(
116
        get( KEY_DOC ),
117
        Group.of(
118
          get( KEY_DOC_META ),
119
          Setting.of( label( KEY_DOC_META ) ),
120
          Setting.of( title( KEY_DOC_META ),
121
                      createTableField( listEntryProperty( KEY_DOC_META ) ),
122
                      listEntryProperty( KEY_DOC_META ) )
123
        )
124
      ),
125
      Category.of(
126
        get( KEY_TYPESET ),
127
        Group.of(
128
          get( KEY_TYPESET_CONTEXT ),
129
          Setting.of( label( KEY_TYPESET_CONTEXT_THEMES_PATH ) ),
130
          Setting.of( title( KEY_TYPESET_CONTEXT_THEMES_PATH ),
131
                      directoryProperty( KEY_TYPESET_CONTEXT_THEMES_PATH ),
132
                      true ),
133
          Setting.of( label( KEY_TYPESET_CONTEXT_CLEAN ) ),
134
          Setting.of( title( KEY_TYPESET_CONTEXT_CLEAN ),
135
                      booleanProperty( KEY_TYPESET_CONTEXT_CLEAN ) )
136
        ),
137
        Group.of(
138
          get( KEY_TYPESET_CONTEXT_FONTS ),
139
          Setting.of( label( KEY_TYPESET_CONTEXT_FONTS_DIR ) ),
140
          Setting.of( title( KEY_TYPESET_CONTEXT_FONTS_DIR ),
141
                      directoryProperty( KEY_TYPESET_CONTEXT_FONTS_DIR ),
142
                      true )
143
        ),
144
        Group.of(
145
          get( KEY_TYPESET_TYPOGRAPHY ),
146
          Setting.of( label( KEY_TYPESET_TYPOGRAPHY_QUOTES ) ),
147
          Setting.of( title( KEY_TYPESET_TYPOGRAPHY_QUOTES ),
148
                      booleanProperty( KEY_TYPESET_TYPOGRAPHY_QUOTES ) )
149
        )
150
      ),
151
      Category.of(
152
        get( KEY_EDITOR ),
153
        Group.of(
154
          get( KEY_EDITOR_AUTOSAVE ),
155
          Setting.of( label( KEY_EDITOR_AUTOSAVE ) ),
156
          Setting.of( title( KEY_EDITOR_AUTOSAVE ),
157
                      integerProperty( KEY_EDITOR_AUTOSAVE ) )
158
        )
159
      ),
160
      Category.of(
161
        get( KEY_R ),
162
        Group.of(
163
          get( KEY_R_DIR ),
164
          Setting.of( label( KEY_R_DIR ) ),
165
          Setting.of( title( KEY_R_DIR ),
166
                      directoryProperty( KEY_R_DIR ),
167
                      true )
168
        ),
169
        Group.of(
170
          get( KEY_R_SCRIPT ),
171
          Setting.of( label( KEY_R_SCRIPT ) ),
172
          createMultilineSetting( "Script", KEY_R_SCRIPT )
173
        ),
174
        Group.of(
175
          get( KEY_R_DELIM_BEGAN ),
176
          Setting.of( label( KEY_R_DELIM_BEGAN ) ),
177
          Setting.of( title( KEY_R_DELIM_BEGAN ),
178
                      stringProperty( KEY_R_DELIM_BEGAN ) )
179
        ),
180
        Group.of(
181
          get( KEY_R_DELIM_ENDED ),
182
          Setting.of( label( KEY_R_DELIM_ENDED ) ),
183
          Setting.of( title( KEY_R_DELIM_ENDED ),
184
                      stringProperty( KEY_R_DELIM_ENDED ) )
185
        )
186
      ),
187
      Category.of(
188
        get( KEY_IMAGES ),
189
        Group.of(
190
          get( KEY_IMAGES_DIR ),
191
          Setting.of( label( KEY_IMAGES_DIR ) ),
192
          Setting.of( title( KEY_IMAGES_DIR ),
193
                      directoryProperty( KEY_IMAGES_DIR ),
194
                      true ),
195
          Setting.of( label( KEY_CACHES_DIR ) ),
196
          Setting.of( title( KEY_CACHES_DIR ),
197
                      directoryProperty( KEY_CACHES_DIR ),
198
                      true )
199
        ),
200
        Group.of(
201
          get( KEY_IMAGES_ORDER ),
202
          Setting.of( label( KEY_IMAGES_ORDER ) ),
203
          Setting.of( title( KEY_IMAGES_ORDER ),
204
                      stringProperty( KEY_IMAGES_ORDER ) )
205
        ),
206
        Group.of(
207
          get( KEY_IMAGES_RESIZE ),
208
          Setting.of( label( KEY_IMAGES_RESIZE ) ),
209
          Setting.of( title( KEY_IMAGES_RESIZE ),
210
                      booleanProperty( KEY_IMAGES_RESIZE ) )
211
        ),
212
        Group.of(
213
          get( KEY_IMAGES_SERVER ),
214
          Setting.of( label( KEY_IMAGES_SERVER ) ),
215
          Setting.of( title( KEY_IMAGES_SERVER ),
216
                      stringProperty( KEY_IMAGES_SERVER ) )
217
        )
218
      ),
219
      Category.of(
220
        get( KEY_DEF ),
221
        Group.of(
222
          get( KEY_DEF_PATH ),
223
          Setting.of( label( KEY_DEF_PATH ) ),
224
          Setting.of( title( KEY_DEF_PATH ),
225
                      fileProperty( KEY_DEF_PATH ), false )
226
        ),
227
        Group.of(
228
          get( KEY_DEF_DELIM_BEGAN ),
229
          Setting.of( label( KEY_DEF_DELIM_BEGAN ) ),
230
          Setting.of( title( KEY_DEF_DELIM_BEGAN ),
231
                      stringProperty( KEY_DEF_DELIM_BEGAN ) )
232
        ),
233
        Group.of(
234
          get( KEY_DEF_DELIM_ENDED ),
235
          Setting.of( label( KEY_DEF_DELIM_ENDED ) ),
236
          Setting.of( title( KEY_DEF_DELIM_ENDED ),
237
                      stringProperty( KEY_DEF_DELIM_ENDED ) )
238
        )
239
      ),
240
      Category.of(
241
        get( KEY_UI_FONT ),
242
        Group.of(
243
          get( KEY_UI_FONT_EDITOR ),
244
          Setting.of( label( KEY_UI_FONT_EDITOR_NAME ) ),
245
          Setting.of( title( KEY_UI_FONT_EDITOR_NAME ),
246
                      createFontNameField(
247
                        stringProperty( KEY_UI_FONT_EDITOR_NAME ),
248
                        doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) ),
249
                      stringProperty( KEY_UI_FONT_EDITOR_NAME ) ),
250
          Setting.of( label( KEY_UI_FONT_EDITOR_SIZE ) ),
251
          Setting.of( title( KEY_UI_FONT_EDITOR_SIZE ),
252
                      doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) )
253
        ),
254
        Group.of(
255
          get( KEY_UI_FONT_PREVIEW ),
256
          Setting.of( label( KEY_UI_FONT_PREVIEW_NAME ) ),
257
          Setting.of( title( KEY_UI_FONT_PREVIEW_NAME ),
258
                      createFontNameField(
259
                        stringProperty( KEY_UI_FONT_PREVIEW_NAME ),
260
                        doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
261
                      stringProperty( KEY_UI_FONT_PREVIEW_NAME ) ),
262
          Setting.of( label( KEY_UI_FONT_PREVIEW_SIZE ) ),
263
          Setting.of( title( KEY_UI_FONT_PREVIEW_SIZE ),
264
                      doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
265
          Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
266
          Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_NAME ),
267
                      createFontNameField(
268
                        stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ),
269
                        doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
270
                      stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
271
          Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
272
          Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_SIZE ),
273
                      doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) )
274
        ),
275
        Group.of(
276
          get( KEY_UI_FONT_MATH ),
277
          Setting.of( title( KEY_UI_FONT_MATH_SIZE ),
278
                      doubleProperty( KEY_UI_FONT_MATH_SIZE ) )
279
        )
280
      ),
281
      Category.of(
282
        get( KEY_UI_SKIN ),
283
        Group.of(
284
          get( KEY_UI_SKIN_SELECTION ),
285
          Setting.of( label( KEY_UI_SKIN_SELECTION ) ),
286
          Setting.of( title( KEY_UI_SKIN_SELECTION ),
287
                      skinListProperty(),
288
                      skinProperty( KEY_UI_SKIN_SELECTION ) )
289
        ),
290
        Group.of(
291
          get( KEY_UI_SKIN_CUSTOM ),
292
          Setting.of( label( KEY_UI_SKIN_CUSTOM ) ),
293
          Setting.of( title( KEY_UI_SKIN_CUSTOM ),
294
                      fileProperty( KEY_UI_SKIN_CUSTOM ), false )
295
        )
296
      ),
297
      Category.of(
298
        get( KEY_UI_PREVIEW ),
299
        Group.of(
300
          get( KEY_UI_PREVIEW_STYLESHEET ),
301
          Setting.of( label( KEY_UI_PREVIEW_STYLESHEET ) ),
302
          Setting.of( title( KEY_UI_PREVIEW_STYLESHEET ),
303
                      fileProperty( KEY_UI_PREVIEW_STYLESHEET ), false )
304
        )
305
      ),
306
      Category.of(
307
        get( KEY_LANGUAGE ),
308
        Group.of(
309
          get( KEY_LANGUAGE_LOCALE ),
310
          Setting.of( label( KEY_LANGUAGE_LOCALE ) ),
311
          Setting.of( title( KEY_LANGUAGE_LOCALE ),
312
                      localeListProperty(),
313
                      localeProperty( KEY_LANGUAGE_LOCALE ) )
314
        )
315
      )
316
    };
317
  }
318
319
  @SuppressWarnings( "unchecked" )
320
  private Setting<StringField, StringProperty> createMultilineSetting(
321
    final String description, final Key property ) {
322
    final Setting<StringField, StringProperty> setting =
323
      Setting.of( description, stringProperty( property ) );
324
    final var field = setting.getElement();
325
    field.multiline( true );
326
327
    return setting;
328
  }
329
330
  /**
331
   * Map ENTER and ESCAPE keys to OK and CANCEL buttons, respectively.
332
   */
333
  private void initKeyEventHandler( final PreferencesFx preferences ) {
334
    final var view = preferences.getView();
335
    final var nodes = view.getChildrenUnmodifiable();
336
    final var master = (MasterDetailPane) nodes.get( 0 );
337
    final var detail = (NavigationView) master.getDetailNode();
338
    final var pane = (DialogPane) view.getParent();
339
340
    detail.setOnKeyReleased( key -> {
341
      switch( key.getCode() ) {
342
        case ENTER -> ((Button) pane.lookupButton( OK )).fire();
343
        case ESCAPE -> ((Button) pane.lookupButton( CANCEL )).fire();
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
      }
121
    } );
122
123
    final var stage = (Stage) pane.getScene().getWindow();
124
    stage.getIcons().add( ICON_DIALOG );
125
126
    final var frontPanel = (Region) pane.getContent();
127
    for( final var node : frontPanel.getChildrenUnmodifiable() ) {
128
      if( node instanceof final ListView<?> listView ) {
129
        final var handler = new ListViewHandler<>( listView );
130
        listView.setOnKeyPressed( handler::handle );
131
      }
132
    }
133
134
    return dialog;
135
  }
136
137
  /**
138
   * Responsible for handling key presses when selecting a font. Based on
139
   * <a href="https://stackoverflow.com/a/43604223/59087">Martin Široký</a>'s
140
   * answer.
141
   *
142
   * @param <T> The type of {@link ListView} to search.
143
   */
144
  private static final class ListViewHandler<T> {
145
    /**
146
     * Amount of time to wait between key presses that typing a subsequent
147
     * key is considered part of the same search, in milliseconds.
148
     */
149
    private static final int RESET_DELAY_MS = 1250;
150
151
    private String mNeedle = "";
152
    private int mSearchSkip = 0;
153
    private long mLastTyped = currentTimeMillis();
154
    private final ListView<T> mHaystack;
155
156
    private ListViewHandler( final ListView<T> listView ) {
157
      mHaystack = listView;
158
    }
159
160
    private void handle( final KeyEvent key ) {
161
      var ch = key.getText();
162
      final var code = key.getCode();
163
164
      if( ch == null || ch.isEmpty() || code == ESCAPE || code == ENTER ) {
165
        return;
166
      }
167
168
      ch = ch.toUpperCase();
169
170
      if( mNeedle.equals( ch ) ) {
171
        mSearchSkip++;
172
      }
173
      else {
174
        mNeedle = currentTimeMillis() - mLastTyped > RESET_DELAY_MS
175
          ? ch : mNeedle + ch;
176
      }
177
178
      mLastTyped = currentTimeMillis();
179
180
      boolean found = false;
181
      int skipped = 0;
182
183
      for( final T item : mHaystack.getItems() ) {
184
        final var straw = item.toString().toUpperCase();
185
186
        if( straw.startsWith( mNeedle ) ) {
187
          if( mSearchSkip > skipped ) {
188
            skipped++;
189
            continue;
190
          }
191
192
          mHaystack.getSelectionModel().select( item );
193
          final int index = mHaystack.getSelectionModel().getSelectedIndex();
194
          mHaystack.getFocusModel().focus( index );
195
          mHaystack.scrollTo( index );
196
          found = true;
197
          break;
198
        }
199
      }
200
201
      if( !found ) {
202
        clue( "Main.status.font.search.missing", mNeedle );
203
        mSearchSkip = 0;
204
      }
205
    }
206
  }
207
}
1208
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;
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 model = field.viewProperty();
41
    final var table = new TableView<>( model );
42
43
    table.setColumnResizePolicy( CONSTRAINED_RESIZE_POLICY );
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
          model.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
  @Override
103
  public Preferences getPreferences() {
104
    return null;
105
  }
106
}
1107
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
10
import java.util.ArrayList;
11
12
import static com.dlsc.formsfx.model.util.BindingMode.CONTINUOUS;
13
import static javafx.collections.FXCollections.observableList;
14
15
/**
16
 * Responsible for binding a form field to a map of values that, ultimately,
17
 * users may edit.
18
 *
19
 * @param <P> The type of {@link Property} to store in the list.
20
 */
21
public class TableField<P> extends Field<TableField<P>> {
22
23
  /**
24
   * Create a writeable list as the data model.
25
   */
26
  private final ListProperty<P> mViewProperty = new SimpleListProperty<>(
27
    observableList( new ArrayList<>() )
28
  );
29
30
  /**
31
   * Contains the data model entries to persist.
32
   */
33
  private final ListProperty<P> mSaveProperty;
34
35
  /**
36
   * Creates a new {@link TableField} with a reference to the list that is to
37
   * be persisted.
38
   *
39
   * @param persist A list of items that will be persisted.
40
   * @param <P>     The type of elements in the list to persist.
41
   * @return A new {@link TableField} used to help render a UI widget.
42
   */
43
  public static <P> TableField<P> ofListType( final ListProperty<P> persist ) {
44
    return new TableField<>( persist );
45
  }
46
47
  private TableField( final ListProperty<P> property ) {
48
    mSaveProperty = property;
49
  }
50
51
  /**
52
   * Returns the data model that seeds the user interface. At any point the
53
   * user may cancel editing, which will revert to the previously persisted
54
   * set.
55
   *
56
   * @return The source for values displayed in the UI.
57
   */
58
  public ListProperty<P> viewProperty() {
59
    return mViewProperty;
60
  }
61
62
  /**
63
   * Called when a new UI instance is opened.
64
   *
65
   * @param bindingMode Indicates how the view data model is bound to the
66
   *                    persistence data model.
67
   */
68
  @Override
69
  public void setBindingMode( final BindingMode bindingMode ) {
70
    if( CONTINUOUS.equals( bindingMode ) ) {
71
      mViewProperty.addAll( mSaveProperty );
72
    }
73
  }
74
75
  /**
76
   * Answers whether the user input is valid.
77
   *
78
   * @return {@code true} Users may provide any key or value strings.
79
   */
80
  @Override
81
  protected boolean validate() {
82
    return true;
83
  }
84
85
  /**
86
   * Update the properties to save by copying the properties updated in the
87
   * user interface (i.e., the view). To be clear, the properties are not
88
   * persisted after calling this method, merely moved out of the UI data
89
   * model and into the to-be-saved data model.
90
   */
91
  @Override
92
  public void persist() {
93
    mSaveProperty.clear();
94
    mSaveProperty.addAll( mViewProperty );
95
  }
96
97
  /**
98
   * The {@link TableField} doesn't bind values, as such the reset can be
99
   * a no-op because only {@link #persist()} will update the properties to
100
   * save.
101
   */
102
  @Override
103
  public void reset() {}
104
}
1105
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_CACHES_DIR, asFileProperty( USER_CACHE_DIR ) ),
74
    entry( KEY_IMAGES_DIR, asFileProperty( USER_DIRECTORY ) ),
75
    entry( KEY_IMAGES_ORDER, asStringProperty( PERSIST_IMAGES_DEFAULT ) ),
76
    entry( KEY_IMAGES_RESIZE, asBooleanProperty( true ) ),
77
    entry( KEY_IMAGES_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 ignored ) {
316
        // When no configuration (item), use the default value.
317
      }
318
    } );
319
320
    mSets.keySet().forEach( key -> {
321
      final var set = store.getSet( key );
322
      final SetProperty<String> property = setsProperty( key );
323
324
      property.setValue( observableSet( set ) );
325
    } );
326
327
    mLists.keySet().forEach( key -> {
328
      final var map = store.getMap( key );
329
      final ListProperty<Entry<String, String>> property = listsProperty( key );
330
      final var list = map
331
        .entrySet()
332
        .stream()
333
        .toList();
334
335
      property.setValue( observableArrayList( list ) );
336
    } );
337
338
    WorkspaceLoadedEvent.fire( this );
339
  }
340
341
  /**
342
   * Saves the current workspace.
343
   */
344
  public void save() {
345
    final var store = createXmlStore();
346
347
    try {
348
      // Update the string values to include the application version.
349
      valuesProperty( KEY_META_VERSION ).setValue( getVersion() );
350
351
      mValues.forEach( ( k, v ) -> store.setValue( k, marshall( v ) ) );
352
      mSets.forEach( store::setSet );
353
      mLists.forEach( store::setMap );
354
355
      store.save( FILE_PREFERENCES );
356
    } catch( final Exception ex ) {
357
      clue( ex );
358
    }
359
  }
360
361
  /**
362
   * Returns a value that represents a setting in the application that the user
363
   * may configure, either directly or indirectly.
364
   *
365
   * @param key The reference to the users' preference stored in deference
366
   *            of app reëntrance.
367
   * @return An observable property to be persisted.
368
   */
369
  @SuppressWarnings( "unchecked" )
370
  public <T, U extends Property<T>> U valuesProperty( final Key key ) {
371
    assert key != null;
372
    return (U) mValues.get( key );
373
  }
374
375
  /**
376
   * Returns a set of values that represent a setting in the application that
377
   * the user may configure, either directly or indirectly. The property
378
   * returned is backed by a {@link Set}.
379
   *
380
   * @param key The {@link Key} associated with a preference value.
381
   * @return An observable property to be persisted.
382
   */
383
  @SuppressWarnings( "unchecked" )
384
  public <T> SetProperty<T> setsProperty( final Key key ) {
385
    assert key != null;
386
    return (SetProperty<T>) mSets.get( key );
387
  }
388
389
  /**
390
   * Returns a list of values that represent a setting in the application that
391
   * the user may configure, either directly or indirectly. The property
392
   * returned is backed by a mutable {@link List}.
393
   *
394
   * @param key The {@link Key} associated with a preference value.
395
   * @return An observable property to be persisted.
396
   */
397
  @SuppressWarnings( "unchecked" )
398
  public <K, V> ListProperty<Entry<K, V>> listsProperty( final Key key ) {
399
    assert key != null;
400
    return (ListProperty<Entry<K, V>>) mLists.get( key );
401
  }
402
403
  /**
404
   * Returns the {@link String} {@link Property} associated with the given
405
   * {@link Key} from the internal list of preference values. The caller
406
   * must be sure that the given {@link Key} is associated with a {@link File}
407
   * {@link Property}.
408
   *
409
   * @param key The {@link Key} associated with a preference value.
410
   * @return The value associated with the given {@link Key}.
411
   */
412
  public StringProperty stringProperty( final Key key ) {
413
    assert key != null;
414
    return valuesProperty( key );
415
  }
416
417
  /**
418
   * Returns the {@link Boolean} {@link Property} associated with the given
419
   * {@link Key} from the internal list of preference values. The caller
420
   * must be sure that the given {@link Key} is associated with a {@link File}
421
   * {@link Property}.
422
   *
423
   * @param key The {@link Key} associated with a preference value.
424
   * @return The value associated with the given {@link Key}.
425
   */
426
  public BooleanProperty booleanProperty( final Key key ) {
427
    assert key != null;
428
    return valuesProperty( key );
429
  }
430
431
  /**
432
   * Returns the {@link Integer} {@link Property} associated with the given
433
   * {@link Key} from the internal list of preference values. The caller
434
   * must be sure that the given {@link Key} is associated with a {@link File}
435
   * {@link Property}.
436
   *
437
   * @param key The {@link Key} associated with a preference value.
438
   * @return The value associated with the given {@link Key}.
439
   */
440
  public IntegerProperty integerProperty( final Key key ) {
441
    assert key != null;
442
    return valuesProperty( key );
443
  }
444
445
  /**
446
   * Returns the {@link Double} {@link Property} associated with the given
447
   * {@link Key} from the internal list of preference values. The caller
448
   * must be sure that the given {@link Key} is associated with a {@link File}
449
   * {@link Property}.
450
   *
451
   * @param key The {@link Key} associated with a preference value.
452
   * @return The value associated with the given {@link Key}.
453
   */
454
  public DoubleProperty doubleProperty( final Key key ) {
455
    assert key != null;
456
    return valuesProperty( key );
457
  }
458
459
  /**
460
   * Returns the {@link File} {@link Property} associated with the given
461
   * {@link Key} from the internal list of preference values. The caller
462
   * must be sure that the given {@link Key} is associated with a {@link File}
463
   * {@link Property}.
464
   *
465
   * @param key The {@link Key} associated with a preference value.
466
   * @return The value associated with the given {@link Key}.
467
   */
468
  public ObjectProperty<File> fileProperty( final Key key ) {
469
    assert key != null;
470
    return valuesProperty( key );
471
  }
472
473
  /**
474
   * Returns the {@link Locale} {@link Property} associated with the given
475
   * {@link Key} from the internal list of preference values. The caller
476
   * must be sure that the given {@link Key} is associated with a {@link File}
477
   * {@link Property}.
478
   *
479
   * @param key The {@link Key} associated with a preference value.
480
   * @return The value associated with the given {@link Key}.
481
   */
482
  public LocaleProperty localeProperty( final Key key ) {
483
    assert key != null;
484
    return valuesProperty( key );
485
  }
486
487
  public ObjectProperty<String> skinProperty( final Key key ) {
488
    assert key != null;
489
    return valuesProperty( key );
490
  }
491
492
  public String getString( final Key key ) {
493
    assert key != null;
494
    return stringProperty( key ).get();
495
  }
496
497
  /**
498
   * Returns the {@link Boolean} preference value associated with the given
499
   * {@link Key}. The caller must be sure that the given {@link Key} is
500
   * associated with a value that matches the return type.
501
   *
502
   * @param key The {@link Key} associated with a preference value.
503
   * @return The value associated with the given {@link Key}.
504
   */
505
  public boolean getBoolean( final Key key ) {
506
    assert key != null;
507
    return booleanProperty( key ).get();
508
  }
509
510
  /**
511
   * Returns the {@link Integer} preference value associated with the given
512
   * {@link Key}. The caller must be sure that the given {@link Key} is
513
   * associated with a value that matches the return type.
514
   *
515
   * @param key The {@link Key} associated with a preference value.
516
   * @return The value associated with the given {@link Key}.
517
   */
518
  public int getInteger( final Key key ) {
519
    assert key != null;
520
    return integerProperty( key ).get();
521
  }
522
523
  /**
524
   * Returns the {@link Double} preference value associated with the given
525
   * {@link Key}. The caller must be sure that the given {@link Key} is
526
   * associated with a value that matches the return type.
527
   *
528
   * @param key The {@link Key} associated with a preference value.
529
   * @return The value associated with the given {@link Key}.
530
   */
531
  public double getDouble( final Key key ) {
532
    assert key != null;
533
    return doubleProperty( key ).get();
534
  }
535
536
  /**
537
   * Returns the {@link File} preference value associated with the given
538
   * {@link Key}. The caller must be sure that the given {@link Key} is
539
   * associated with a value that matches the return type.
540
   *
541
   * @param key The {@link Key} associated with a preference value.
542
   * @return The value associated with the given {@link Key}.
543
   */
544
  public File getFile( final Key key ) {
545
    assert key != null;
546
    return fileProperty( key ).get();
547
  }
548
549
  /**
550
   * Returns the language locale setting for the
551
   * {@link AppKeys#KEY_LANGUAGE_LOCALE} key.
552
   *
553
   * @return The user's current locale setting.
554
   */
555
  public Locale getLocale() {
556
    return localeProperty( KEY_LANGUAGE_LOCALE ).toLocale();
557
  }
558
559
  @SuppressWarnings( "unchecked" )
560
  public <K, V> Map<K, V> getMetadata() {
561
    final var metadata = listsProperty( KEY_DOC_META );
562
    final var map = new HashMap<K, V>( metadata.size() );
563
564
    metadata.forEach(
565
      entry -> map.put( (K) entry.getKey(), (V) entry.getValue() )
566
    );
567
568
    return map;
569
  }
570
571
  public Path getThemesPath() {
572
    final var dir = getFile( KEY_TYPESET_CONTEXT_THEMES_PATH );
573
    final var name = getString( KEY_TYPESET_CONTEXT_THEME_SELECTION );
574
575
    return Path.of( dir.toString(), name );
576
  }
577
578
  /**
579
   * Delegates to {@link #listen(Key, ReadOnlyProperty, BooleanSupplier)},
580
   * providing a value of {@code true} for the {@link BooleanSupplier} to
581
   * indicate the property changes always take effect.
582
   *
583
   * @param key      The value to bind to the internal key property.
584
   * @param property The external property value that sets the internal value.
585
   */
586
  public <T> void listen( final Key key, final ReadOnlyProperty<T> property ) {
587
    assert key != null;
588
    assert property != null;
589
590
    listen( key, property, () -> true );
591
  }
592
593
  /**
594
   * Binds a read-only property to a value in the preferences. This allows
595
   * user interface properties to change and the preferences will be
596
   * synchronized automatically.
597
   * <p>
598
   * This calls {@link Platform#runLater(Runnable)} to ensure that all pending
599
   * application window states are finished before assessing whether property
600
   * changes should be applied. Without this, exiting the application while the
601
   * window is maximized would persist the window's maximum dimensions,
602
   * preventing restoration to its prior, non-maximum size.
603
   *
604
   * @param key      The value to bind to the internal key property.
605
   * @param property The external property value that sets the internal value.
606
   * @param enabled  Indicates whether property changes should be applied.
607
   */
608
  public <T> void listen(
609
    final Key key,
610
    final ReadOnlyProperty<T> property,
611
    final BooleanSupplier enabled ) {
612
    assert key != null;
613
    assert property != null;
614
    assert enabled != null;
615
616
    property.addListener(
617
      ( c, o, n ) -> runLater( () -> {
618
        if( enabled.getAsBoolean() ) {
619
          valuesProperty( key ).setValue( n );
620
        }
621
      } )
622
    );
623
  }
624
625
  /**
626
   * Creates a lightweight persistence mechanism for user preferences.
627
   *
628
   * @return The {@link XmlStore} that helps with persisting application state.
629
   */
630
  private XmlStore createXmlStore() {
631
    // Root-level configuration item is the application name.
632
    return new XmlStore( APP_TITLE_LOWERCASE );
633
  }
634
}
1635
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 javax.xml.xpath.XPathConstants.NODE;
21
22
/**
23
 * Responsible for managing XML documents, which includes reading, writing,
24
 * retrieving, and setting elements. This is an alternative to Apache
25
 * Commons Configuration, JAXB, and Jackson. All of them are heavyweight and
26
 * the latter are difficult to use with dynamic data (because they require
27
 * annotations).
28
 * <p>
29
 * <strong>Note:</strong> It is preferable to use a different instance when
30
 * loading and saving the documents. Otherwise, old and duplicate data will
31
 * persist. Using a new instance ensures that elements removed from the
32
 * user preferences will not persist across XML configuration file versions.
33
 */
34
public class XmlStore {
35
  private static final String SEPARATOR = "/";
36
37
  private final String mRoot;
38
  private Document mDocument = DocumentParser.newDocument();
39
40
  /**
41
   * Constructs a new instance with a blank {@link Document}. Call the
42
   * {@link #load(File)} method to populate the document from a {@link File},
43
   * or {@link #save(File)} to persist the current document state.
44
   *
45
   * @param root The root-level document element.
46
   */
47
  public XmlStore( final String root ) {
48
    assert root != null;
49
50
    mRoot = root;
51
  }
52
53
  /**
54
   * Loads the given configuration file into a document object model.
55
   * Clients of this class can set and retrieve elements via the requisite
56
   * access methods.
57
   *
58
   * @param config File containing persistent user preferences.
59
   */
60
  public void load( final File config ) {
61
    assert config != null;
62
    assert config.isFile();
63
64
    try {
65
      mDocument = DocumentParser.parse( config );
66
    } catch( final Exception ignored ) {
67
      mDocument = DocumentParser.newDocument();
68
    }
69
  }
70
71
  /**
72
   * Returns the document value associated with the given key name.
73
   *
74
   * @param key {@link Key} name to retrieve.
75
   * @return The value associated with the key.
76
   * @throws NoSuchElementException No value could be found for the key.
77
   */
78
  public String getValue( final Key key ) throws NoSuchElementException {
79
    assert key != null;
80
81
    try {
82
      final var node = toNode( key, mDocument );
83
84
      if( node != null ) {
85
        return node.getTextContent();
86
      }
87
    } catch( final XPathExpressionException ignored ) {}
88
89
    throw new NoSuchElementException( key.toString() );
90
  }
91
92
  /**
93
   * Returns a set of document values associated with the given key name. This
94
   * is suitable for basic sets, such as:
95
   * <pre>
96
   *   {@code
97
   *   <recent>
98
   *     <file>/tmp/filename.txt</file>
99
   *     <file>/home/username/document.md</file>
100
   *     <file>/usr/local/share/app/conf/help.Rmd</file>
101
   *   </recent>}
102
   * </pre>
103
   * <p>
104
   * The {@code file} element name can be ignored.
105
   *
106
   * @param key {@link Key} name to retrieve.
107
   * @return The values associated with the key, or an empty set if none found.
108
   */
109
  public Set<String> getSet( final Key key ) {
110
    assert key != null;
111
112
    final var set = new LinkedHashSet<String>();
113
114
    visit( key, node -> set.add( node.getTextContent() ) );
115
116
    return set;
117
  }
118
119
  /**
120
   * Returns a map of name/value pairs associated with the given key name.
121
   * This is suitable for mapped values, such as:
122
   * <pre>
123
   *   {@code
124
   *   <meta>
125
   *     <title>{{book.title}}</title>
126
   *     <author>{{book.author}}</author>
127
   *     <date>{{book.publish.date}}</date>
128
   *   </meta>}
129
   * </pre>
130
   * <p>
131
   * The element names under the {@code meta} node must be preserved along
132
   * with their values. Resolving the values based on the variable definitions
133
   * (in moustache syntax) is not a responsibility of this class.
134
   *
135
   * @param key {@link Key} name to retrieve (e.g., {@code meta}).
136
   * @return A map of element names to element values, or an empty map if
137
   * none found.
138
   */
139
  public Map<String, String> getMap( final Key key ) {
140
    assert key != null;
141
142
    // Create a new key that will match all child nodes under the given key,
143
    // extracting each element as a name/value pair for the resulting map.
144
    final var all = Key.key( key, "*" );
145
    final var map = new LinkedHashMap<String, String>();
146
147
    visit( all, node -> map.put( node.getNodeName(), node.getTextContent() ) );
148
149
    return map;
150
  }
151
152
  /**
153
   * Call to write the user preferences to a file.
154
   *
155
   * @param config The file wherein the preferences are saved.
156
   * @throws IOException Could not write to the file.
157
   */
158
  public void save( final File config ) throws IOException {
159
    assert config != null;
160
161
    try( final var writer = new FileWriter( config ) ) {
162
      writer.write( DocumentParser.toString( mDocument ) );
163
    }
164
  }
165
166
  public void setValue( final Key key, final String value ) {
167
    assert key != null;
168
    assert value != null;
169
170
    try {
171
      final var node = upsert( key, mDocument );
172
173
      node.setTextContent( value );
174
    } catch( final XPathExpressionException ignored ) {}
175
  }
176
177
  public void setSet( final Key key, final SetProperty<?> set ) {
178
    assert key != null;
179
    assert set != null;
180
181
    Node node = null;
182
183
    try {
184
      for( final var item : set ) {
185
        if( node == null ) {
186
          node = upsert( key, mDocument );
187
        }
188
        else {
189
          final var doc = node.getOwnerDocument();
190
          final var sibling = doc.createElement( key.name() );
191
          var parent = node.getParentNode();
192
193
          if( parent == null ) {
194
            parent = doc.getDocumentElement();
195
          }
196
197
          parent.appendChild( sibling );
198
          node = sibling;
199
        }
200
201
        node.setTextContent( item.toString() );
202
      }
203
    } catch( final XPathExpressionException ignored ) {}
204
  }
205
206
  /**
207
   * @param key  The application key representing a user preference.
208
   * @param list List of {@link Entry} items.
209
   */
210
  public void setMap( final Key key, final ListProperty<?> list ) {
211
    assert key != null;
212
    assert list != null;
213
214
    for( final var item : list ) {
215
      if( item instanceof Entry<?, ?> entry ) {
216
        try {
217
          final var child = Key.key( key, entry.getKey().toString() );
218
          final var node = upsert( child, mDocument );
219
220
          node.setTextContent( entry.getValue().toString() );
221
        } catch( final XPathExpressionException ignored ) {}
222
      }
223
    }
224
  }
225
226
  private Node toNode( final Key key, final Document doc )
227
    throws XPathExpressionException {
228
    final var xpath = toXPath( key );
229
    final var expr = DocumentParser.compile( xpath );
230
    final var element = expr.evaluate( doc, NODE );
231
232
    return element instanceof Node node ? node : null;
233
  }
234
235
  /**
236
   * Provides the equivalent of update-or-insert behaviour provided by some
237
   * SQL databases. Finds the element in the document represented by the
238
   * given {@link Key}. If no element is found then the full path to the
239
   * element is created. In essence, this method converts a hierarchy of
240
   * {@link Key} names into a hierarchy of {@link Document} {@link Element}s
241
   * (i.e., {@link Node}s).
242
   * <p>
243
   * For example, given a key named {@code workspace.meta.version}, this will
244
   * produce a document structure that, when exported as XML, resembles:
245
   * <pre>{@code
246
   *   <root>
247
   *     <workspace>
248
   *       <meta>
249
   *         <version/>
250
   *       </meta>
251
   *     </workspace>
252
   *   </root>
253
   * }</pre>
254
   * <p>
255
   * The calling code is responsible for populating the {@link Node} returned
256
   * with its particular value. In the example above, the text content of the
257
   * {@link Node} would be filled with the application version number.
258
   *
259
   * @param key The application key representing a user preference.
260
   * @param doc The document that may contain an xpath for the {@link Key}.
261
   * @return The existing or new element.
262
   */
263
  private Node upsert( final Key key, final Document doc )
264
    throws XPathExpressionException {
265
    assert key != null;
266
    assert doc != null;
267
268
    final var missing = new Stack<Key>();
269
    Key visitor = key;
270
    Node parent = null;
271
272
    do {
273
      final var node = toNode( visitor, doc );
274
275
      // If an element exists on the first iteration, return it because there
276
      // is no missing hierarchy to create.
277
      if( node != null ) {
278
        if( missing.isEmpty() ) {
279
          return node;
280
        }
281
282
        parent = node;
283
      }
284
      else {
285
        // Track the number of elements in the hierarchy that don't exist.
286
        missing.push( visitor );
287
288
        // Attempt to find the parent xpath in the document.
289
        visitor = visitor.parent();
290
      }
291
    }
292
    while( visitor != null && parent == null );
293
294
    // If the document is empty, update the top-level document element.
295
    if( parent == null ) {
296
      parent = doc.getDocumentElement();
297
298
      // If there is still no top-level element, then create it.
299
      if( parent == null ) {
300
        parent = doc.createElement( mRoot );
301
        doc.appendChild( parent );
302
      }
303
    }
304
305
    assert parent != null;
306
307
    // Create the hierarchy.
308
    while( !missing.isEmpty() ) {
309
      visitor = missing.pop();
310
311
      final var child = doc.createElement( visitor.name() );
312
      parent.appendChild( child );
313
      parent = child;
314
    }
315
316
    return parent;
317
  }
318
319
  /**
320
   * Abstraction for functionality that requires iterating over multiple
321
   * nodes under a particular xpath.
322
   *
323
   * @param key      {@link #toXPath(Key) Compiled} into an {@link XPath}.
324
   * @param consumer Accepts each node that matches the {@link XPath}.
325
   */
326
  private void visit( final Key key, final Consumer<Node> consumer ) {
327
    assert key != null;
328
    assert consumer != null;
329
330
    try {
331
      final var xpath = toXPath( key );
332
333
      DocumentParser.visit( mDocument, xpath, consumer );
334
    } catch( final XPathExpressionException ignored ) {
335
      // Programming error. Triggered by loading a previous config version?
336
    }
337
  }
338
339
  /**
340
   * Creates an {@link XPathExpression} value based on the given {@link Key}.
341
   *
342
   * @param key The {@link Key} to convert to an xpath string.
343
   * @return The given {@link Key} compiled into an {@link XPathExpression}.
344
   * @throws XPathExpressionException Could not compile the {@link Key}.
345
   */
346
  private StringBuilder toXPath( final Key key )
347
    throws XPathExpressionException {
348
    assert key != null;
349
350
    final var sb = new StringBuilder( 128 );
351
352
    key.walk( sb::append, SEPARATOR );
353
    sb.insert( 0, SEPARATOR );
354
355
    if( !mRoot.isBlank() ) {
356
      sb.insert( 0, SEPARATOR + mRoot );
357
    }
358
359
    return sb;
360
  }
361
362
  /**
363
   * Pretty-prints the XML document into a string. Meant to be used for
364
   * debugging. To save the configuration, see {@link #save(File)}.
365
   *
366
   * @return The document in a well-formed, indented, string format.
367
   */
368
  @Override
369
  public String toString() {
370
    return DocumentParser.toString( mDocument );
371
  }
372
}
1373
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.ui.adapters.ReplacedElementAdapter;
5
import com.keenwrite.collections.BoundedCache;
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.SvgReplacedElementFactory.HTML_IMAGE;
19
import static com.keenwrite.preview.SvgReplacedElementFactory.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(
80
        source, k -> {
81
          final var r = f.createReplacedElement( c, box, uac, width, height );
82
          return r instanceof final ImageReplacedElement ire
83
            ? createImageElement( box, ire )
84
            : r;
85
        }
86
      );
87
88
      if( replaced != null ) {
89
        return replaced;
90
      }
91
    }
92
93
    return null;
94
  }
95
96
  @Override
97
  public void reset() {
98
    for( final var factory : mFactories ) {
99
      factory.reset();
100
    }
101
  }
102
103
  @Override
104
  public void remove( final Element element ) {
105
    for( final var factory : mFactories ) {
106
      factory.remove( element );
107
    }
108
  }
109
110
  public void addFactory( final ReplacedElementFactory factory ) {
111
    mFactories.add( factory );
112
  }
113
114
  public void clearCache() {
115
    mCache.clear();
116
  }
117
118
  /**
119
   * Creates a new image that maintains its aspect ratio while fitting into
120
   * the given {@link BlockBox}. If the image is too big, it is scaled down.
121
   *
122
   * @param box The bounding region the image must fit into.
123
   * @param ire The image to resize.
124
   * @return An image that is scaled down to fit, but only if necessary.
125
   */
126
  private SmoothImageReplacedElement createImageElement(
127
    final BlockBox box, final ImageReplacedElement ire ) {
128
    return new SmoothImageReplacedElement(
129
      ire.getImage(), min( ire.getIntrinsicWidth(), box.getWidth() ), -1 );
130
  }
131
}
1132
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.util.zip.Deflater;
5
6
import static java.lang.String.format;
7
import static java.util.Base64.getUrlEncoder;
8
9
/**
10
 * Responsible for transforming text-based diagram descriptions into URLs
11
 * that the HTML renderer can embed as SVG images.
12
 */
13
public class DiagramUrlGenerator {
14
  private DiagramUrlGenerator() {
15
  }
16
17
  /**
18
   * Returns a URL that can be embedded as the {@code src} attribute to an HTML
19
   * {@code img} tag.
20
   *
21
   * @param server  Name of server to use for diagram conversion.
22
   * @param diagram Diagram type (e.g., Graphviz, Block, PlantUML).
23
   * @param text    Diagram text that conforms to the diagram type.
24
   * @return A secure URL string to use as an image {@code src} attribute.
25
   */
26
  public static String toUrl(
27
    final String server, final String diagram, final String text ) {
28
    return format(
29
      "https://%s/%s/svg/%s", server, diagram, encode( text )
30
    );
31
  }
32
33
  /**
34
   * Convert the plain-text version of the diagram into a URL-encoded value
35
   * suitable for passing to a web server using an HTTP GET request.
36
   *
37
   * @param text The diagram text to encode.
38
   * @return The URL-encoded (and compressed) version of the text.
39
   */
40
  private static String encode( final String text ) {
41
    return getUrlEncoder().encodeToString( compress( text.getBytes() ) );
42
  }
43
44
  /**
45
   * Compresses a sequence of bytes using ZLIB format.
46
   *
47
   * @param source The data to compress.
48
   * @return A lossless, compressed sequence of bytes.
49
   */
50
  private static byte[] compress( byte[] source ) {
51
    final var deflater = new Deflater();
52
    deflater.setInput( source );
53
    deflater.finish();
54
55
    final var compressed = new byte[ Short.MAX_VALUE ];
56
    final var size = deflater.deflate( compressed );
57
    final var result = new byte[ size ];
58
59
    System.arraycopy( compressed, 0, result, 0, size );
60
61
    return result;
62
  }
63
}
164
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
        }
80
      } catch( final Exception ex ) {
81
        clue( ex );
82
      }
83
    }
84
  }
85
86
  private static final XhtmlNamespaceHandler XNH = new XhtmlNamespaceHandler();
87
  private final ChainedReplacedElementFactory mFactory;
88
89
  FlyingSaucerPanel() {
90
    // The order is important: SwingReplacedElementFactory replaces SVG images
91
    // with a blank image, which will cause the chained factory to cache the
92
    // image and exit. Instead, the SVG must execute first to rasterize the
93
    // content. Consequently, the chained factory must maintain insertion order.
94
    mFactory = new ChainedReplacedElementFactory(
95
      new SvgReplacedElementFactory(),
96
      new SwingReplacedElementFactory()
97
    );
98
99
    final var context = getSharedContext();
100
    final var textRenderer = context.getTextRenderer();
101
    context.setReplacedElementFactory( mFactory );
102
    textRenderer.setSmoothingThreshold( 0 );
103
104
    addDocumentListener( new DocumentEventHandler() );
105
    removeMouseTrackingListeners();
106
    addMouseTrackingListener( new HyperlinkListener() );
107
    addComponentListener( new ComponentEventHandler() );
108
  }
109
110
  /**
111
   * Updates the document model displayed by the renderer. Effectively, this
112
   * updates the HTML document to provide new content.
113
   *
114
   * @param doc     A complete HTML5 document, including doctype.
115
   * @param baseUri URI to use for finding relative files, such as images.
116
   */
117
  @Override
118
  public void render( final Document doc, final String baseUri ) {
119
    setDocument( doc, baseUri, XNH );
120
  }
121
122
  @Override
123
  public void clearCache() {
124
    mFactory.clearCache();
125
  }
126
127
  @Override
128
  public void scrollTo( final String id, final JScrollPane scrollPane ) {
129
    int iter = 0;
130
    Box box = null;
131
132
    while( iter++ < 3 && ((box = getBoxById( id )) == null) ) {
133
      try {
134
        sleep( 10 );
135
      } catch( final Exception ex ) {
136
        clue( ex );
137
      }
138
    }
139
140
    scrollTo( box, scrollPane );
141
  }
142
143
  /**
144
   * Scrolls to the location specified by the {@link Box} that corresponds
145
   * to a point somewhere in the preview pane. If there is no caret, then
146
   * this will not change the scroll position. Changing the scroll position
147
   * to the top if the {@link Box} instance is {@code null} will result in
148
   * jumping around a lot and inconsistent synchronization issues.
149
   *
150
   * @param box The rectangular region containing the caret, or {@code null}
151
   *            if the HTML does not have a caret.
152
   */
153
  private void scrollTo( final Box box, final JScrollPane scrollPane ) {
154
    if( box != null ) {
155
      invokeLater( () -> {
156
        scrollTo( createPoint( box, scrollPane ) );
157
        scrollPane.repaint();
158
      } );
159
    }
160
  }
161
162
  /**
163
   * Creates a {@link Point} to use as a reference for scrolling to the area
164
   * described by the given {@link Box}. The {@link Box} coordinates are used
165
   * to populate the {@link Point}'s location, with minor adjustments for
166
   * vertical centering.
167
   *
168
   * @param box The {@link Box} that represents a scrolling anchor reference.
169
   * @return A coordinate suitable for scrolling to.
170
   */
171
  private Point createPoint( final Box box, final JScrollPane scrollPane ) {
172
    assert box != null;
173
174
    // Scroll back up by half the height of the scroll bar to keep the typing
175
    // area within the view port; otherwise, the view port will have jumped too
176
    // high up and the most recently typed letters won't be visible.
177
    int y = max( box.getAbsY() - scrollPane.getVerticalScrollBar()
178
                                           .getHeight() / 2, 0 );
179
    int x = box.getAbsX();
180
181
    if( !box.getStyle().isInline() ) {
182
      final var margin = box.getMargin( getLayoutContext() );
183
      y += margin.top();
184
      x += margin.left();
185
    }
186
187
    return new Point( x, y );
188
  }
189
190
  /**
191
   * Delegates to the {@link SharedContext}.
192
   *
193
   * @param id The HTML element identifier to retrieve in {@link Box} form.
194
   * @return The {@link Box} that corresponds to the given element ID, or
195
   * {@code null} if none found.
196
   */
197
  Box getBoxById( final String id ) {
198
    return getSharedContext().getBoxById( id );
199
  }
200
201
  /**
202
   * Suppress scrolling to the top on updates.
203
   */
204
  @Override
205
  public void resetScrollPosition() {
206
  }
207
208
  /**
209
   * The default mouse click listener attempts navigation within the preview
210
   * panel. We want to usurp that behaviour to open the link in a
211
   * platform-specific browser.
212
   */
213
  private void removeMouseTrackingListeners() {
214
    for( final var listener : getMouseTrackingListeners() ) {
215
      if( !(listener instanceof HoverListener) ) {
216
        removeMouseTrackingListener( (FSMouseListener) listener );
217
      }
218
    }
219
  }
220
}
1221
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
    final var toolkit = getDefaultToolkit();
41
    final var hints = toolkit.getDesktopProperty( "awt.font.desktophints" );
42
43
    if( hints instanceof final Map map ) {
44
      for( final var key : map.keySet() ) {
45
        final var hint = map.get( key );
46
        RENDERING_HINTS.put( key, hint );
47
      }
48
    }
49
  }
50
51
  /**
52
   * Defines a reusable constant, nothing more.
53
   */
54
  private HighQualityRenderingHints() {
55
  }
56
}
157
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_IMAGES_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/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
    return new Dimension( newW, newH );
60
  }
61
}
162
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
  public static BufferedImage rasterizeImage(
312
    final String svg, final double scale )
313
    throws ParseException, TranscoderException {
314
    final var document = toDocument( svg );
315
    final var root = document.getDocumentElement();
316
    final var width = root.getAttribute( SVG_WIDTH_ATTRIBUTE );
317
    final var height = root.getAttribute( SVG_HEIGHT_ATTRIBUTE );
318
    final var w = INT_FORMAT.parse( width ).intValue() * scale;
319
    final var h = INT_FORMAT.parse( height ).intValue() * scale;
320
321
    return rasterize( svg, w, h );
322
  }
323
324
  /**
325
   * Converts an SVG string into a rasterized image that can be drawn on
326
   * a graphics context.
327
   *
328
   * @param svg The SVG xml document.
329
   * @param w   Scale the image width to this size (aspect ratio is
330
   *            maintained).
331
   * @return The vector graphic transcoded into a raster image format.
332
   */
333
  public static BufferedImage rasterizeImage( final String svg, final int w )
334
    throws TranscoderException {
335
    return rasterize( toDocument( svg ), w );
336
  }
337
338
  /**
339
   * Given a document object model (DOM) {@link Element}, this will convert that
340
   * element to a string.
341
   *
342
   * @param root The DOM node to convert to a string.
343
   * @return The DOM node as an escaped, plain text string.
344
   */
345
  public static String toSvg( final Element root ) {
346
    try {
347
      return transform( root ).replaceAll( "xmlns=\"\" ", "" );
348
    } catch( final Exception ex ) {
349
      clue( ex );
350
    }
351
352
    return BROKEN_IMAGE_SVG;
353
  }
354
355
  /**
356
   * Converts an SVG XML string into a new {@link Document} instance.
357
   *
358
   * @param xml The XML containing SVG elements.
359
   * @return The SVG contents parsed into a {@link Document} object model.
360
   */
361
  private static Document toDocument( final String xml ) {
362
    try( final var reader = new StringReader( xml ) ) {
363
      return FACTORY_DOM.createSVGDocument(
364
        "http://www.w3.org/2000/svg", reader );
365
    } catch( final Exception ex ) {
366
      throw new IllegalArgumentException( ex );
367
    }
368
  }
369
370
  /**
371
   * Creates a rasterized image of the given source document.
372
   *
373
   * @param input     The source document to transcode.
374
   * @param hintKey   Transcoding hint key.
375
   * @param hintValue Transcoding hint value.
376
   * @return A new {@link BufferedImageTranscoder} instance with the given
377
   * transcoding hint applied.
378
   */
379
  private static BufferedImage rasterize(
380
    final TranscoderInput input, final Key hintKey, final float hintValue )
381
    throws TranscoderException {
382
    final var hints = new HashMap<Key, Object>();
383
    hints.put( hintKey, hintValue );
384
385
    return rasterize( input, hints );
386
  }
387
388
  private static BufferedImage rasterize(
389
    final String svg, final double w, final double h )
390
    throws TranscoderException {
391
    final var hints = new HashMap<Key, Object>();
392
    hints.put( KEY_WIDTH, (float) w );
393
    hints.put( KEY_HEIGHT, (float) h );
394
395
    return rasterize( new TranscoderInput( toDocument( svg ) ), hints );
396
  }
397
398
  public static BufferedImage rasterize(
399
    final TranscoderInput input,
400
    final Map<TranscodingHints.Key, Object> hints ) throws TranscoderException {
401
    final var transcoder = new BufferedImageTranscoder();
402
403
    for( final var hint : hints.entrySet() ) {
404
      transcoder.addTranscodingHint( hint.getKey(), hint.getValue() );
405
    }
406
407
    transcoder.setErrorHandler( sErrorHandler );
408
    transcoder.transcode( input, null );
409
410
    return transcoder.getImage();
411
  }
412
413
  /**
414
   * Returns either the given element's SVG document width, or the display
415
   * width, whichever is smaller.
416
   *
417
   * @param root  The SVG document's root node.
418
   * @param width The display width (e.g., rendering canvas width).
419
   * @return The lower value of the document's width or the display width.
420
   */
421
  @SuppressWarnings( "ConstantValue" )
422
  private static float fit( final Element root, final int width ) {
423
    final var w = root.getAttribute( SVG_WIDTH_ATTRIBUTE );
424
425
    return w == null || w.isBlank()
426
      ? width
427
      : fit( root, w, width );
428
  }
429
430
  /**
431
   * Returns the width in user space units (pixels?).
432
   *
433
   * @param root  The element containing the width attribute.
434
   * @param w     The element's width attribute value.
435
   * @param width The rendering canvas width.
436
   * @return Either the rendering canvas width or SVG document width,
437
   * whichever is smaller.
438
   */
439
  private static float fit(
440
    final Element root, final String w, final int width ) {
441
    final var usWidth = svgHorizontalLengthToUserSpace(
442
      w, SVG_WIDTH_ATTRIBUTE, createContext( BRIDGE_CONTEXT, root )
443
    );
444
445
    // If the image is too small, scale it to 1/4 the canvas width.
446
    return Math.min( usWidth < 5 ? width / 4.0f : usWidth, (float) width );
447
  }
448
}
1449
A src/main/java/com/keenwrite/preview/SvgReplacedElementFactory.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import com.keenwrite.io.MediaType;
5
import com.keenwrite.ui.adapters.ReplacedElementAdapter;
6
import org.xhtmlrenderer.extend.ReplacedElement;
7
import org.xhtmlrenderer.extend.UserAgentCallback;
8
import org.xhtmlrenderer.layout.LayoutContext;
9
import org.xhtmlrenderer.render.BlockBox;
10
import org.xhtmlrenderer.swing.ImageReplacedElement;
11
12
import java.awt.image.BufferedImage;
13
import java.io.File;
14
import java.net.URI;
15
import java.nio.file.Path;
16
17
import static com.keenwrite.events.StatusEvent.clue;
18
import static com.keenwrite.io.downloads.DownloadManager.open;
19
import static com.keenwrite.preview.SvgRasterizer.*;
20
import static com.keenwrite.processors.markdown.extensions.tex.TexNode.HTML_TEX;
21
import static com.keenwrite.util.ProtocolScheme.getProtocol;
22
23
/**
24
 * Responsible for running {@link SvgRasterizer} on SVG images detected within
25
 * a document to transform them into rasterized versions.
26
 */
27
public final class SvgReplacedElementFactory extends ReplacedElementAdapter {
28
29
  public static final String HTML_IMAGE = "img";
30
  public static final String HTML_IMAGE_SRC = "src";
31
32
  private static final ImageReplacedElement BROKEN_IMAGE =
33
    createImageReplacedElement( BROKEN_IMAGE_PLACEHOLDER );
34
35
  @Override
36
  public ReplacedElement createReplacedElement(
37
    final LayoutContext c,
38
    final BlockBox box,
39
    final UserAgentCallback uac,
40
    final int cssWidth,
41
    final int cssHeight ) {
42
    final var e = box.getElement();
43
44
    ImageReplacedElement image = null;
45
46
    try {
47
      BufferedImage raster = null;
48
49
      switch( e.getNodeName() ) {
50
        case HTML_IMAGE -> {
51
          final var source = e.getAttribute( HTML_IMAGE_SRC );
52
53
          URI uri = null;
54
55
          if( getProtocol( source ).isHttp() ) {
56
            try( final var response = open( source ) ) {
57
              if( response.isSvg() ) {
58
                // Rasterize SVG from URL resource.
59
                raster = rasterize(
60
                  response.getInputStream(),
61
                  box.getContentWidth()
62
                );
63
              }
64
65
              clue( "Main.status.image.request.fetch", source );
66
            }
67
          }
68
          else if( MediaType.fromFilename( source ).isSvg() ) {
69
            // Attempt to rasterize based on file name.
70
            final var path = new File( source ).toPath();
71
72
            if( path.isAbsolute() ) {
73
              uri = path.toUri();
74
            }
75
            else {
76
              final var base = new URI( e.getBaseURI() ).getPath();
77
              uri = Path.of( base, source ).toUri();
78
            }
79
          }
80
81
          if( uri != null ) {
82
            raster = rasterize( uri, box.getContentWidth() );
83
          }
84
        }
85
        case HTML_TEX ->
86
          // Convert the TeX element to a raster graphic.
87
          raster = rasterize( MathRenderer.toString( e.getTextContent() ) );
88
      }
89
90
      if( raster != null ) {
91
        image = createImageReplacedElement( raster );
92
      }
93
    } catch( final Exception ex ) {
94
      image = BROKEN_IMAGE;
95
      clue( ex );
96
    }
97
98
    return image == null ? BROKEN_IMAGE : image;
99
  }
100
101
  private static ImageReplacedElement createImageReplacedElement(
102
    final BufferedImage bi ) {
103
    return new ImageReplacedElement( bi, bi.getWidth(), bi.getHeight() );
104
  }
105
}
1106
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
 * 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
11
/**
12
 * This class let you create dimension constrains based on a actual image.
13
 */
14
public class ConstrainedDimension {
15
  private ConstrainedDimension() {
16
  }
17
18
  /**
19
   * Will always return a dimension with positive width and height;
20
   *
21
   * @param dimension of the unscaled image
22
   * @return the dimension of the scaled image
23
   */
24
  public Dimension getDimension( Dimension dimension ) {
25
    return dimension;
26
  }
27
28
  /**
29
   * Used when the destination size is fixed. This may not keep the image
30
   * aspect radio.
31
   *
32
   * @param width  destination dimension width
33
   * @param height destination dimension height
34
   * @return destination dimension (width x height)
35
   */
36
  public static ConstrainedDimension createAbsolutionDimension(
37
    final int width, final int height ) {
38
    assert width > 0 && height > 0 : "Dimensions must be positive integers";
39
    return new ConstrainedDimension() {
40
      public Dimension getDimension( Dimension dimension ) {
41
        return new Dimension( width, height );
42
      }
43
    };
44
  }
45
}
146
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, final int destWidth, final int destHeight ) {
69
    this( filter,
70
          createAbsolutionDimension( destWidth, destHeight ) );
71
  }
72
73
  public ResampleOp(
74
    final ResampleFilter filter, ConstrainedDimension dimensionConstrain ) {
75
    super( dimensionConstrain );
76
    mFilter = filter;
77
  }
78
79
  public BufferedImage doFilter(
80
    BufferedImage srcImg, BufferedImage dest, int dstWidth, int dstHeight ) {
81
    this.dstWidth = dstWidth;
82
    this.dstHeight = dstHeight;
83
84
    if( dstWidth < 3 || dstHeight < 3 ) {
85
      throw new IllegalArgumentException( "Target must be at least 3x3." );
86
    }
87
88
    assert multipleInvocationLock.incrementAndGet() == 1 :
89
      "Multiple concurrent invocations detected";
90
91
    final var srcType = srcImg.getType();
92
93
    if( srcType == TYPE_BYTE_BINARY ||
94
      srcType == TYPE_BYTE_INDEXED ||
95
      srcType == TYPE_CUSTOM ) {
96
      srcImg = ImageUtils.convert(
97
        srcImg,
98
        srcImg.getColorModel().hasAlpha() ? TYPE_4BYTE_ABGR : TYPE_3BYTE_BGR );
99
    }
100
101
    this.nrChannels = ImageUtils.nrChannels( srcImg );
102
    assert nrChannels > 0;
103
    this.srcWidth = srcImg.getWidth();
104
    this.srcHeight = srcImg.getHeight();
105
106
    byte[][] workPixels = new byte[ srcHeight ][ dstWidth * nrChannels ];
107
108
    // Pre-calculate  sub-sampling
109
    horizontalSubsamplingData = createSubSampling(
110
      mFilter, srcWidth, dstWidth );
111
    verticalSubsamplingData = createSubSampling(
112
      mFilter, srcHeight, dstHeight );
113
114
    final BufferedImage scrImgCopy = srcImg;
115
    final byte[][] workPixelsCopy = workPixels;
116
    final Thread[] threads = new Thread[ threadCount - 1 ];
117
118
    for( int i = 1; i < threadCount; i++ ) {
119
      final int finalI = i;
120
      threads[ i - 1 ] = new Thread( () -> horizontallyFromSrcToWork(
121
        scrImgCopy, workPixelsCopy, finalI, threadCount ) );
122
      threads[ i - 1 ].start();
123
    }
124
125
    horizontallyFromSrcToWork( scrImgCopy, workPixelsCopy, 0, threadCount );
126
    waitForAllThreads( threads );
127
128
    byte[] outPixels = new byte[ dstWidth * dstHeight * nrChannels ];
129
130
    // --------------------------------------------------
131
    // Apply filter to sample vertically from Work to Dst
132
    // --------------------------------------------------
133
    final byte[] outPixelsCopy = outPixels;
134
    for( int i = 1; i < threadCount; i++ ) {
135
      final int finalI = i;
136
      threads[ i - 1 ] = new Thread( () -> verticalFromWorkToDst(
137
        workPixelsCopy, outPixelsCopy, finalI, threadCount ) );
138
      threads[ i - 1 ].start();
139
    }
140
    verticalFromWorkToDst( workPixelsCopy, outPixelsCopy, 0, threadCount );
141
    waitForAllThreads( threads );
142
143
    // free memory
144
    // noinspection UnusedAssignment
145
    workPixels = null;
146
147
    final BufferedImage out;
148
149
    if( dest != null &&
150
      dstWidth == dest.getWidth() &&
151
      dstHeight == dest.getHeight() ) {
152
      out = dest;
153
      int nrDestChannels = ImageUtils.nrChannels( dest );
154
      if( nrDestChannels != nrChannels ) {
155
        final var errorMgs = format(
156
          "Destination image must be compatible width source image. Source " +
157
            "image had %d channels destination image had %d channels",
158
          nrChannels, nrDestChannels );
159
        throw new RuntimeException( errorMgs );
160
      }
161
    }
162
    else {
163
      out = new BufferedImage(
164
        dstWidth, dstHeight, getResultBufferedImageType( srcImg ) );
165
    }
166
167
    ImageUtils.setBGRPixels( outPixels, out, 0, 0, dstWidth, dstHeight );
168
169
    assert multipleInvocationLock.decrementAndGet() == 0 : "Multiple " +
170
      "concurrent invocations detected";
171
172
    return out;
173
  }
174
175
  private void waitForAllThreads( final Thread[] threads ) {
176
    try {
177
      for( final Thread thread : threads ) {
178
        thread.join( Long.MAX_VALUE );
179
      }
180
    } catch( final InterruptedException e ) {
181
      currentThread().interrupt();
182
      throw new RuntimeException( e );
183
    }
184
  }
185
186
  static SubSamplingData createSubSampling(
187
    ResampleFilter filter, int srcSize, int dstSize ) {
188
    final float scale = (float) dstSize / (float) srcSize;
189
    final int[] arrN = new int[ dstSize ];
190
    final int numContributors;
191
    final float[] arrWeight;
192
    final int[] arrPixel;
193
194
    final float fwidth = filter.getSamplingRadius();
195
196
    float centerOffset = 0.5f / scale;
197
198
    if( scale < 1.0f ) {
199
      final float width = fwidth / scale;
200
      // Add 2 to be safe with the ceiling
201
      numContributors = (int) (width * 2.0f + 2);
202
      arrWeight = new float[ dstSize * numContributors ];
203
      arrPixel = new int[ dstSize * numContributors ];
204
205
      final float fNormFac = (float) (1f / (Math.ceil( width ) / fwidth));
206
207
      for( int i = 0; i < dstSize; i++ ) {
208
        final int subindex = i * numContributors;
209
        float center = i / scale + centerOffset;
210
        int left = (int) Math.floor( center - width );
211
        int right = (int) Math.ceil( center + width );
212
        for( int j = left; j <= right; j++ ) {
213
          float weight;
214
          weight = filter.apply( (center - j) * fNormFac );
215
216
          if( weight == 0.0f ) {
217
            continue;
218
          }
219
          int n;
220
          if( j < 0 ) {
221
            n = -j;
222
          }
223
          else if( j >= srcSize ) {
224
            n = srcSize - j + srcSize - 1;
225
          }
226
          else {
227
            n = j;
228
          }
229
          int k = arrN[ i ];
230
          //assert k == j-left:String.format("%s = %s %s", k,j,left);
231
          arrN[ i ]++;
232
          if( n < 0 || n >= srcSize ) {
233
            weight = 0.0f;// Flag that cell should not be used
234
          }
235
          arrPixel[ subindex + k ] = n;
236
          arrWeight[ subindex + k ] = weight;
237
        }
238
        // normalize the filter's weight's so the sum equals to 1.0, very
239
        // important for avoiding box type of artifacts
240
        final int max = arrN[ i ];
241
        float tot = 0;
242
        for( int k = 0; k < max; k++ ) { tot += arrWeight[ subindex + k ]; }
243
        if( tot != 0f ) { // 0 should never happen except bug in filter
244
          for( int k = 0; k < max; k++ ) { arrWeight[ subindex + k ] /= tot; }
245
        }
246
      }
247
    }
248
    else {
249
      // super-sampling
250
      // Scales from smaller to bigger height
251
      numContributors = (int) (fwidth * 2.0f + 1);
252
      arrWeight = new float[ dstSize * numContributors ];
253
      arrPixel = new int[ dstSize * numContributors ];
254
      //
255
      for( int i = 0; i < dstSize; i++ ) {
256
        final int subindex = i * numContributors;
257
        float center = i / scale + centerOffset;
258
        int left = (int) Math.floor( center - fwidth );
259
        int right = (int) Math.ceil( center + fwidth );
260
        for( int j = left; j <= right; j++ ) {
261
          float weight = filter.apply( center - j );
262
          if( weight == 0.0f ) {
263
            continue;
264
          }
265
          int n;
266
          if( j < 0 ) {
267
            n = -j;
268
          }
269
          else if( j >= srcSize ) {
270
            n = srcSize - j + srcSize - 1;
271
          }
272
          else {
273
            n = j;
274
          }
275
          int k = arrN[ i ];
276
          arrN[ i ]++;
277
          if( n < 0 || n >= srcSize ) {
278
            weight = 0.0f;// Flag that cell should not be used
279
          }
280
          arrPixel[ subindex + k ] = n;
281
          arrWeight[ subindex + k ] = weight;
282
        }
283
        // normalize the filter's weight's so the sum equals to 1.0, very
284
        // important for avoiding box type of artifacts
285
        final int max = arrN[ i ];
286
        float tot = 0;
287
        for( int k = 0; k < max; k++ ) { tot += arrWeight[ subindex + k ]; }
288
        assert tot != 0 : "should never happen except bug in filter";
289
        if( tot != 0f ) {
290
          for( int k = 0; k < max; k++ ) { arrWeight[ subindex + k ] /= tot; }
291
        }
292
      }
293
    }
294
    return new SubSamplingData( arrN, arrPixel, arrWeight, numContributors );
295
  }
296
297
  private void verticalFromWorkToDst( byte[][] workPixels, byte[] outPixels,
298
                                      int start, int delta ) {
299
    if( nrChannels == 1 ) {
300
      verticalFromWorkToDstGray(
301
        workPixels, outPixels, start, threadCount );
302
      return;
303
    }
304
    boolean useChannel3 = nrChannels > 3;
305
    for( int x = start; x < dstWidth; x += delta ) {
306
      final int xLoc = x * nrChannels;
307
      for( int y = dstHeight - 1; y >= 0; y-- ) {
308
        final int yTimesNumContributors =
309
          y * verticalSubsamplingData.numContributors;
310
        final int max = verticalSubsamplingData.arrN[ y ];
311
        final int sampleLocation = (y * dstWidth + x) * nrChannels;
312
313
        float sample0 = 0.0f;
314
        float sample1 = 0.0f;
315
        float sample2 = 0.0f;
316
        float sample3 = 0.0f;
317
        int index = yTimesNumContributors;
318
        for( int j = max - 1; j >= 0; j-- ) {
319
          int valueLoc = verticalSubsamplingData.arrPixel[ index ];
320
          float arrWeight = verticalSubsamplingData.arrWeight[ index ];
321
          sample0 += (workPixels[ valueLoc ][ xLoc ] & 0xff) * arrWeight;
322
          sample1 += (workPixels[ valueLoc ][ xLoc + 1 ] & 0xff) * arrWeight;
323
          sample2 += (workPixels[ valueLoc ][ xLoc + 2 ] & 0xff) * arrWeight;
324
          if( useChannel3 ) {
325
            sample3 += (workPixels[ valueLoc ][ xLoc + 3 ] & 0xff) * arrWeight;
326
          }
327
328
          index++;
329
        }
330
331
        outPixels[ sampleLocation ] = toByte( sample0 );
332
        outPixels[ sampleLocation + 1 ] = toByte( sample1 );
333
        outPixels[ sampleLocation + 2 ] = toByte( sample2 );
334
335
        if( useChannel3 ) {
336
          outPixels[ sampleLocation + 3 ] = toByte( sample3 );
337
        }
338
      }
339
    }
340
  }
341
342
  private void verticalFromWorkToDstGray(
343
    byte[][] workPixels, byte[] outPixels, int start, int delta ) {
344
    for( int x = start; x < dstWidth; x += delta ) {
345
      for( int y = dstHeight - 1; y >= 0; y-- ) {
346
        final int yTimesNumContributors =
347
          y * verticalSubsamplingData.numContributors;
348
        final int max = verticalSubsamplingData.arrN[ y ];
349
        final int sampleLocation = y * dstWidth + x;
350
        float sample0 = 0.0f;
351
        int index = yTimesNumContributors;
352
353
        for( int j = max - 1; j >= 0; j-- ) {
354
          int valueLocation = verticalSubsamplingData.arrPixel[ index ];
355
          float arrWeight = verticalSubsamplingData.arrWeight[ index ];
356
          sample0 += (workPixels[ valueLocation ][ x ] & 0xff) * arrWeight;
357
358
          index++;
359
        }
360
361
        outPixels[ sampleLocation ] = toByte( sample0 );
362
      }
363
    }
364
  }
365
366
  /**
367
   * Apply filter to sample horizontally from Src to Work
368
   */
369
  private void horizontallyFromSrcToWork(
370
    BufferedImage srcImg, byte[][] workPixels, int start, int delta ) {
371
    if( nrChannels == 1 ) {
372
      horizontallyFromSrcToWorkGray( srcImg, workPixels, start, delta );
373
      return;
374
    }
375
376
    // Used if we work on int based bitmaps, later used to keep channel values
377
    final int[] tempPixels = new int[ srcWidth ];
378
    // create reusable row to minimize memory overhead
379
    final byte[] srcPixels = new byte[ srcWidth * nrChannels ];
380
    final boolean useChannel3 = nrChannels > 3;
381
382
    for( int k = start; k < srcHeight; k = k + delta ) {
383
      ImageUtils.getPixelsBGR( srcImg, k, srcWidth, srcPixels, tempPixels );
384
385
      for( int i = dstWidth - 1; i >= 0; i-- ) {
386
        int sampleLocation = i * nrChannels;
387
        final int max = horizontalSubsamplingData.arrN[ i ];
388
389
        float sample0 = 0.0f;
390
        float sample1 = 0.0f;
391
        float sample2 = 0.0f;
392
        float sample3 = 0.0f;
393
        int index = i * horizontalSubsamplingData.numContributors;
394
        for( int j = max - 1; j >= 0; j-- ) {
395
          float arrWeight = horizontalSubsamplingData.arrWeight[ index ];
396
          int pixelIndex =
397
            horizontalSubsamplingData.arrPixel[ index ] * nrChannels;
398
399
          sample0 += (srcPixels[ pixelIndex ] & 0xff) * arrWeight;
400
          sample1 += (srcPixels[ pixelIndex + 1 ] & 0xff) * arrWeight;
401
          sample2 += (srcPixels[ pixelIndex + 2 ] & 0xff) * arrWeight;
402
          if( useChannel3 ) {
403
            sample3 += (srcPixels[ pixelIndex + 3 ] & 0xff) * arrWeight;
404
          }
405
          index++;
406
        }
407
408
        workPixels[ k ][ sampleLocation ] = toByte( sample0 );
409
        workPixels[ k ][ sampleLocation + 1 ] = toByte( sample1 );
410
        workPixels[ k ][ sampleLocation + 2 ] = toByte( sample2 );
411
        if( useChannel3 ) {
412
          workPixels[ k ][ sampleLocation + 3 ] = toByte( sample3 );
413
        }
414
      }
415
    }
416
  }
417
418
  /**
419
   * Apply filter to sample horizontally from Src to Work
420
   */
421
  private void horizontallyFromSrcToWorkGray(
422
    BufferedImage srcImg, byte[][] workPixels, int start, int delta ) {
423
    // Used if we work on int based bitmaps, later used to keep channel values
424
    final int[] tempPixels = new int[ srcWidth ];
425
    // create reusable row to minimize memory overhead
426
    final byte[] srcPixels = new byte[ srcWidth ];
427
428
    for( int k = start; k < srcHeight; k = k + delta ) {
429
      ImageUtils.getPixelsBGR( srcImg, k, srcWidth, srcPixels, tempPixels );
430
431
      for( int i = dstWidth - 1; i >= 0; i-- ) {
432
        final int max = horizontalSubsamplingData.arrN[ i ];
433
434
        float sample0 = 0.0f;
435
        int index = i * horizontalSubsamplingData.numContributors;
436
        for( int j = max - 1; j >= 0; j-- ) {
437
          float arrWeight = horizontalSubsamplingData.arrWeight[ index ];
438
          int pixelIndex = horizontalSubsamplingData.arrPixel[ index ];
439
440
          sample0 += (srcPixels[ pixelIndex ] & 0xff) * arrWeight;
441
          index++;
442
        }
443
444
        workPixels[ k ][ i ] = toByte( sample0 );
445
      }
446
    }
447
  }
448
449
  private static byte toByte( final float f ) {
450
    if( f < 0 ) {
451
      return 0;
452
    }
453
454
    return (byte) (f > MAX_CHANNEL_VALUE ? MAX_CHANNEL_VALUE : f + 0.5f);
455
  }
456
457
  protected int getResultBufferedImageType( BufferedImage srcImg ) {
458
    return nrChannels == 3
459
      ? TYPE_3BYTE_BGR
460
      : nrChannels == 4
461
      ? TYPE_4BYTE_ABGR
462
      : srcImg.getSampleModel().getDataType() == TYPE_USHORT
463
      ? TYPE_USHORT_GRAY
464
      : TYPE_BYTE_GRAY;
465
  }
466
}
1467
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.typesetting.Typesetter.Mutator;
10
import static java.nio.file.Files.deleteIfExists;
11
import static java.nio.file.Files.writeString;
12
13
/**
14
 * Responsible for using a typesetting engine to convert an XHTML document
15
 * into a PDF file. This must not be run from the JavaFX thread.
16
 */
17
public final class PdfProcessor extends ExecutorProcessor<String> {
18
  private final ProcessorContext mProcessorContext;
19
20
  public PdfProcessor( final ProcessorContext context ) {
21
    assert context != null;
22
    mProcessorContext = context;
23
  }
24
25
  /**
26
   * Converts a document by calling a third-party application to typeset the
27
   * given XHTML document.
28
   *
29
   * @param xhtml The document to convert to a PDF file.
30
   * @return {@code null} because there is no valid return value from generating
31
   * a PDF file.
32
   */
33
  public String apply( final String xhtml ) {
34
    try {
35
      clue( "Main.status.typeset.create" );
36
      final var context = mProcessorContext;
37
      final var parent = context.getTargetPath().getParent();
38
      final var document =
39
        TEXT_XML.createTempFile( APP_TITLE_ABBR, parent );
40
      final var typesetter = Typesetter
41
        .builder()
42
        .with( Mutator::setAutoRemove, context.getAutoRemove() )
43
        .with( Mutator::setSourcePath, writeString( document, xhtml ) )
44
        .with( Mutator::setTargetPath, context.getTargetPath() )
45
        .with( Mutator::setThemesPath, context.getThemesPath() )
46
        .with( Mutator::setImagesPath, context.getImagesPath() )
47
        .with( Mutator::setCachesPath, context.getCachesPath() )
48
        .with( Mutator::setFontsPath, context.getFontsPath() )
49
        .build();
50
51
      typesetter.typeset();
52
53
      // Smote the temporary file after typesetting the document.
54
      if( typesetter.autoRemove() ) {
55
        deleteIfExists( document );
56
      }
57
    } catch( final Exception ex ) {
58
      // Typesetter runtime exceptions will pass up the call stack.
59
      clue( "Main.status.typeset.failed", ex );
60
    }
61
62
    // Do not continue processing (the document was typeset into a binary).
63
    return null;
64
  }
65
}
166
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 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.collections.InterpolatingMap;
6
import com.keenwrite.constants.Constants;
7
import com.keenwrite.editors.common.Caret;
8
import com.keenwrite.io.FileType;
9
import com.keenwrite.sigils.PropertyKeyOperator;
10
import com.keenwrite.sigils.SigilKeyOperator;
11
import com.keenwrite.util.GenericBuilder;
12
import org.renjin.repackaged.guava.base.Splitter;
13
14
import java.io.File;
15
import java.nio.file.Path;
16
import java.util.HashMap;
17
import java.util.Locale;
18
import java.util.Map;
19
import java.util.concurrent.Callable;
20
import java.util.function.Supplier;
21
22
import static com.keenwrite.Bootstrap.USER_CACHE_DIR;
23
import static com.keenwrite.Bootstrap.USER_DATA_DIR;
24
import static com.keenwrite.constants.Constants.*;
25
import static com.keenwrite.io.FileType.UNKNOWN;
26
import static com.keenwrite.io.MediaType.TEXT_PROPERTIES;
27
import static com.keenwrite.io.MediaType.valueFrom;
28
import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate;
29
30
/**
31
 * Provides a context for configuring a chain of {@link Processor} instances.
32
 */
33
public final class ProcessorContext {
34
35
  private final Mutator mMutator;
36
37
  /**
38
   * Determines the file type from the path extension. This should only be
39
   * called when it is known that the file type won't be a definition file
40
   * (e.g., YAML or other definition source), but rather an editable file
41
   * (e.g., Markdown, R Markdown, etc.).
42
   *
43
   * @param path The path with a file name extension.
44
   * @return The FileType for the given path.
45
   */
46
  private static FileType lookup( final Path path ) {
47
    assert path != null;
48
49
    final var prefix = GLOB_PREFIX_FILE;
50
    final var keys = sSettings.getKeys( prefix );
51
52
    var found = false;
53
    var fileType = UNKNOWN;
54
55
    while( keys.hasNext() && !found ) {
56
      final var key = keys.next();
57
      final var patterns = sSettings.getStringSettingList( key );
58
      final var predicate = createFileTypePredicate( patterns );
59
60
      if( predicate.test( path.toFile() ) ) {
61
        // Remove the EXTENSIONS_PREFIX to get the file name extension mapped
62
        // to a standard name (as defined in the settings.properties file).
63
        final String suffix = key.replace( prefix + '.', "" );
64
        fileType = FileType.from( suffix );
65
        found = true;
66
      }
67
    }
68
69
    return fileType;
70
  }
71
72
  public boolean isExportFormat( final ExportFormat exportFormat ) {
73
    return mMutator.mExportFormat == exportFormat;
74
  }
75
76
  /**
77
   * Responsible for populating the instance variables required by the
78
   * context.
79
   */
80
  public static class Mutator {
81
    private Path mSourcePath;
82
    private Path mTargetPath;
83
    private ExportFormat mExportFormat;
84
    private boolean mConcatenate;
85
86
    private Supplier<Path> mThemesPath = USER_DIRECTORY::toPath;
87
    private Supplier<Locale> mLocale = () -> Locale.ENGLISH;
88
89
    private Supplier<Map<String, String>> mDefinitions = HashMap::new;
90
    private Supplier<Map<String, String>> mMetadata = HashMap::new;
91
    private Supplier<Caret> mCaret = () -> Caret.builder().build();
92
93
    private Supplier<Path> mFontsPath = () -> getFontDirectory().toPath();
94
95
    private Supplier<Path> mImagesPath = USER_DIRECTORY::toPath;
96
    private Supplier<String> mImageServer = () -> DIAGRAM_SERVER_NAME;
97
    private Supplier<String> mImageOrder = () -> PERSIST_IMAGES_DEFAULT;
98
99
    private Supplier<Path> mCachesPath = USER_CACHE_DIR::toPath;
100
101
    private Supplier<String> mSigilBegan = () -> DEF_DELIM_BEGAN_DEFAULT;
102
    private Supplier<String> mSigilEnded = () -> DEF_DELIM_ENDED_DEFAULT;
103
104
    private Supplier<Path> mRWorkingDir = USER_DIRECTORY::toPath;
105
    private Supplier<String> mRScript = () -> "";
106
107
    private Supplier<Boolean> mCurlQuotes = () -> true;
108
    private Supplier<Boolean> mAutoRemove = () -> true;
109
110
    public void setSourcePath( final Path sourcePath ) {
111
      assert sourcePath != null;
112
      mSourcePath = sourcePath;
113
    }
114
115
    public void setTargetPath( final Path outputPath ) {
116
      assert outputPath != null;
117
      mTargetPath = outputPath;
118
    }
119
120
    public void setTargetPath( final File targetPath ) {
121
      assert targetPath != null;
122
      setTargetPath( targetPath.toPath() );
123
    }
124
125
    public void setThemesPath( final Supplier<Path> themesPath ) {
126
      assert themesPath != null;
127
      mThemesPath = themesPath;
128
    }
129
130
    public void setCachesPath( final Supplier<File> cachesDir ) {
131
      assert cachesDir != null;
132
133
      mCachesPath = () -> {
134
        final var dir = cachesDir.get();
135
136
        return (dir == null ? USER_DATA_DIR.toFile() : dir).toPath();
137
      };
138
    }
139
140
    public void setImagesPath( final Supplier<File> imagesDir ) {
141
      assert imagesDir != null;
142
143
      mImagesPath = () -> {
144
        final var dir = imagesDir.get();
145
146
        return (dir == null ? USER_DIRECTORY : dir).toPath();
147
      };
148
    }
149
150
    public void setImageOrder( final Supplier<String> imageOrder ) {
151
      assert imageOrder != null;
152
      mImageOrder = imageOrder;
153
    }
154
155
    public void setImageServer( final Supplier<String> imageServer ) {
156
      assert imageServer != null;
157
      mImageServer = imageServer;
158
    }
159
160
    public void setFontsPath( final Supplier<File> fontsPath ) {
161
      assert fontsPath != null;
162
      mFontsPath = () -> {
163
        final var dir = fontsPath.get();
164
165
        return (dir == null ? USER_DIRECTORY : dir).toPath();
166
      };
167
    }
168
169
    public void setExportFormat( final ExportFormat exportFormat ) {
170
      assert exportFormat != null;
171
      mExportFormat = exportFormat;
172
    }
173
174
    public void setConcatenate( final boolean concatenate ) {
175
      mConcatenate = concatenate;
176
    }
177
178
    public void setLocale( final Supplier<Locale> locale ) {
179
      assert locale != null;
180
      mLocale = locale;
181
    }
182
183
    /**
184
     * Sets the list of fully interpolated key-value pairs to use when
185
     * substituting variable names back into the document as variable values.
186
     * This uses a {@link Callable} reference so that GUI and command-line
187
     * usage can insert their respective behaviours. That is, this method
188
     * prevents coupling the GUI to the CLI.
189
     *
190
     * @param supplier Defines how to retrieve the definitions.
191
     */
192
    public void setDefinitions( final Supplier<Map<String, String>> supplier ) {
193
      assert supplier != null;
194
      mDefinitions = supplier;
195
    }
196
197
    public void setMetadata( final Supplier<Map<String, String>> metadata ) {
198
      assert metadata != null;
199
      mMetadata = metadata.get() == null ? HashMap::new : metadata;
200
    }
201
202
    /**
203
     * Sets the source for deriving the {@link Caret}. Typically, this is
204
     * the text editor that has focus.
205
     *
206
     * @param caret The source for the currently active caret.
207
     */
208
    public void setCaret( final Supplier<Caret> caret ) {
209
      assert caret != null;
210
      mCaret = caret;
211
    }
212
213
    public void setSigilBegan( final Supplier<String> sigilBegan ) {
214
      assert sigilBegan != null;
215
      mSigilBegan = sigilBegan;
216
    }
217
218
    public void setSigilEnded( final Supplier<String> sigilEnded ) {
219
      assert sigilEnded != null;
220
      mSigilEnded = sigilEnded;
221
    }
222
223
    public void setRWorkingDir( final Supplier<Path> rWorkingDir ) {
224
      assert rWorkingDir != null;
225
226
      mRWorkingDir = rWorkingDir;
227
    }
228
229
    public void setRScript( final Supplier<String> rScript ) {
230
      assert rScript != null;
231
      mRScript = rScript;
232
    }
233
234
    public void setCurlQuotes( final Supplier<Boolean> curlQuotes ) {
235
      assert curlQuotes != null;
236
      mCurlQuotes = curlQuotes;
237
    }
238
239
    public void setAutoRemove( final Supplier<Boolean> autoRemove ) {
240
      assert autoRemove != null;
241
      mAutoRemove = autoRemove;
242
    }
243
244
    private boolean isExportFormat( final ExportFormat format ) {
245
      return mExportFormat == format;
246
    }
247
  }
248
249
  public static GenericBuilder<Mutator, ProcessorContext> builder() {
250
    return GenericBuilder.of( Mutator::new, ProcessorContext::new );
251
  }
252
253
  /**
254
   * Creates a new context for use by the {@link ProcessorFactory} when
255
   * instantiating new {@link Processor} instances. Although all the
256
   * parameters are required, not all {@link Processor} instances will use
257
   * all parameters.
258
   */
259
  private ProcessorContext( final Mutator mutator ) {
260
    assert mutator != null;
261
262
    mMutator = mutator;
263
  }
264
265
  public Path getSourcePath() {
266
    return mMutator.mSourcePath;
267
  }
268
269
  /**
270
   * Fully qualified file name to use when exporting (e.g., document.pdf).
271
   *
272
   * @return Full path to a file name.
273
   */
274
  public Path getTargetPath() {
275
    return mMutator.mTargetPath;
276
  }
277
278
  public ExportFormat getExportFormat() {
279
    return mMutator.mExportFormat;
280
  }
281
282
  public Locale getLocale() {
283
    return mMutator.mLocale.get();
284
  }
285
286
  /**
287
   * Returns the variable map of definitions, without interpolation.
288
   *
289
   * @return A map to help dereference variables.
290
   */
291
  public Map<String, String> getDefinitions() {
292
    return mMutator.mDefinitions.get();
293
  }
294
295
  /**
296
   * Returns the variable map of definitions, with interpolation.
297
   *
298
   * @return A map to help dereference variables.
299
   */
300
  public InterpolatingMap getInterpolatedDefinitions() {
301
    return new InterpolatingMap(
302
      createDefinitionKeyOperator(), getDefinitions()
303
    ).interpolate();
304
  }
305
306
  public Map<String, String> getMetadata() {
307
    return mMutator.mMetadata.get();
308
  }
309
310
  /**
311
   * Returns the current caret position in the document being edited and is
312
   * always up-to-date.
313
   *
314
   * @return Caret position in the document.
315
   */
316
  public Supplier<Caret> getCaret() {
317
    return mMutator.mCaret;
318
  }
319
320
  /**
321
   * Returns the directory that contains the file being edited. When
322
   * {@link Constants#DOCUMENT_DEFAULT} is created, the parent path is
323
   * {@code null}. This will get absolute path to the file before trying to
324
   * get te parent path, which should always be a valid path. In the unlikely
325
   * event that the base path cannot be determined by the path alone, the
326
   * default user directory is returned. This is necessary for the creation
327
   * of new files.
328
   *
329
   * @return Path to the directory containing a file being edited, or the
330
   * default user directory if the base path cannot be determined.
331
   */
332
  public Path getBaseDir() {
333
    final var path = getSourcePath().toAbsolutePath().getParent();
334
    return path == null ? DEFAULT_DIRECTORY : path;
335
  }
336
337
  FileType getSourceFileType() {
338
    return lookup( getSourcePath() );
339
  }
340
341
  public Path getThemesPath() {
342
    return mMutator.mThemesPath.get();
343
  }
344
345
  public Path getImagesPath() {
346
    return mMutator.mImagesPath.get();
347
  }
348
349
  public Path getCachesPath() {
350
    return mMutator.mCachesPath.get();
351
  }
352
353
  public Iterable<String> getImageOrder() {
354
    assert mMutator.mImageOrder != null;
355
356
    final var order = mMutator.mImageOrder.get();
357
    final var token = order.contains( "," ) ? ',' : ' ';
358
359
    return Splitter.on( token ).split( token + order );
360
  }
361
362
  public String getImageServer() {
363
    return mMutator.mImageServer.get();
364
  }
365
366
  public Path getFontsPath() {
367
    return mMutator.mFontsPath.get();
368
  }
369
370
  public boolean getAutoRemove() {
371
    return mMutator.mAutoRemove.get();
372
  }
373
374
  public Path getRWorkingDir() {
375
    return mMutator.mRWorkingDir.get();
376
  }
377
378
  public String getRScript() {
379
    return mMutator.mRScript.get();
380
  }
381
382
  public boolean getCurlQuotes() {
383
    return mMutator.mCurlQuotes.get();
384
  }
385
386
  /**
387
   * Answers whether to process a single text file or all text files in
388
   * the same directory as a single text file. See {@link #getSourcePath()}
389
   * for the file to process (or all files in its directory).
390
   *
391
   * @return {@code true} means to process all text files, {@code false}
392
   * means to process a single file.
393
   */
394
  public boolean getConcatenate() {
395
    return mMutator.mConcatenate;
396
  }
397
398
  public SigilKeyOperator createKeyOperator() {
399
    return createKeyOperator( getSourcePath() );
400
  }
401
402
  /**
403
   * Returns the sigil operator for the given {@link Path}.
404
   *
405
   * @param path The type of file being edited, from its extension.
406
   */
407
  private SigilKeyOperator createKeyOperator( final Path path ) {
408
    assert path != null;
409
410
    return valueFrom( path ) == TEXT_PROPERTIES
411
      ? createPropertyKeyOperator()
412
      : createDefinitionKeyOperator();
413
  }
414
415
  private SigilKeyOperator createPropertyKeyOperator() {
416
    return new PropertyKeyOperator();
417
  }
418
419
  private SigilKeyOperator createDefinitionKeyOperator() {
420
    final var began = mMutator.mSigilBegan.get();
421
    final var ended = mMutator.mSigilEnded.get();
422
423
    return new SigilKeyOperator( began, ended );
424
  }
425
}
1426
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/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 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import com.keenwrite.dom.DocumentParser;
5
import com.keenwrite.io.MediaTypeExtension;
6
import com.keenwrite.ui.heuristics.WordCounter;
7
import com.keenwrite.util.DataTypeConverter;
8
import com.whitemagicsoftware.keenquotes.parser.Contractions;
9
import com.whitemagicsoftware.keenquotes.parser.Curler;
10
import org.w3c.dom.Document;
11
12
import java.io.FileNotFoundException;
13
import java.nio.file.Path;
14
import java.util.LinkedHashMap;
15
import java.util.List;
16
import java.util.Locale;
17
import java.util.Map;
18
19
import static com.keenwrite.Bootstrap.APP_TITLE_ABBR;
20
import static com.keenwrite.dom.DocumentParser.*;
21
import static com.keenwrite.events.StatusEvent.clue;
22
import static com.keenwrite.io.downloads.DownloadManager.open;
23
import static com.keenwrite.util.ProtocolScheme.getProtocol;
24
import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML;
25
import static java.lang.String.format;
26
import static java.lang.String.valueOf;
27
import static java.nio.file.Files.copy;
28
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
29
30
/**
31
 * Responsible for making an XHTML document complete by wrapping it with html
32
 * and body elements. This doesn't have to be super-efficient because it's
33
 * not run in real-time.
34
 */
35
public final class XhtmlProcessor extends ExecutorProcessor<String> {
36
  private static final Curler sTypographer =
37
    new Curler( createContractions(), FILTER_XML, true );
38
39
  private final ProcessorContext mContext;
40
41
  public XhtmlProcessor(
42
    final Processor<String> successor, final ProcessorContext context ) {
43
    super( successor );
44
45
    assert context != null;
46
    mContext = context;
47
  }
48
49
  /**
50
   * Responsible for producing a well-formed XML document complete with
51
   * metadata (title, author, keywords, copyright, and date).
52
   *
53
   * @param html The HTML document to transform into an XHTML document.
54
   * @return The transformed HTML document.
55
   */
56
  @Override
57
  public String apply( final String html ) {
58
    clue( "Main.status.typeset.xhtml" );
59
60
    try {
61
      final var doc = parse( html );
62
      setMetaData( doc );
63
64
      visit( doc, "//img", node -> {
65
        try {
66
          final var attrs = node.getAttributes();
67
          final var attr = attrs.getNamedItem( "src" );
68
69
          if( attr != null ) {
70
            final var src = attr.getTextContent();
71
            final Path location;
72
            final Path imagesDir;
73
74
            // Download into a cache directory, which can be written to without
75
            // any possibility of overwriting local image files. Further, the
76
            // filenames are hashed as a second layer of protection.
77
            if( getProtocol( src ).isRemote() ) {
78
              location = downloadImage( src );
79
              imagesDir = getCachesPath();
80
            }
81
            else {
82
              location = resolveImage( src );
83
              imagesDir = getImagesPath();
84
            }
85
86
            final var relative = imagesDir.relativize( location );
87
88
            attr.setTextContent( relative.toString() );
89
          }
90
        } catch( final Exception ex ) {
91
          clue( ex );
92
        }
93
      } );
94
95
      final var document = DocumentParser.toString( doc );
96
      final var curl = mContext.getCurlQuotes();
97
98
      return curl ? sTypographer.apply( document ) : document;
99
    } catch( final Exception ex ) {
100
      clue( ex );
101
    }
102
103
    return html;
104
  }
105
106
  /**
107
   * Applies the metadata fields to the document.
108
   *
109
   * @param doc The document to adorn with metadata.
110
   */
111
  private void setMetaData( final Document doc ) {
112
    final var metadata = createMetaDataMap( doc );
113
    final var title = metadata.get( "title" );
114
115
    visit( doc, "/html/head", node -> {
116
      // Insert <title>text</title> inside <head>.
117
      node.appendChild( createElement( doc, "title", title ) );
118
119
      // Insert each <meta name=x content=y /> inside <head>.
120
      metadata.entrySet().forEach(
121
        entry -> node.appendChild( createMeta( doc, entry ) )
122
      );
123
    } );
124
  }
125
126
  /**
127
   * Generates document metadata, including word count.
128
   *
129
   * @param doc The document containing the text to tally.
130
   * @return A map of metadata key/value pairs.
131
   */
132
  private Map<String, String> createMetaDataMap( final Document doc ) {
133
    final var result = new LinkedHashMap<String, String>();
134
    final var metadata = getMetadata();
135
    final var map = mContext.getInterpolatedDefinitions();
136
137
    metadata.forEach(
138
      ( key, value ) -> {
139
        final var interpolated = map.interpolate( value );
140
141
        if( !interpolated.isEmpty() ) {
142
          result.put( key, interpolated );
143
        }
144
      }
145
    );
146
    result.put( "count", wordCount( doc ) );
147
148
    return result;
149
  }
150
151
  /**
152
   * The metadata is in list form because the user interface for entering the
153
   * key-value pairs is a table, which requires a generic {@link List} rather
154
   * than a generic {@link Map}.
155
   *
156
   * @return The document metadata.
157
   */
158
  private Map<String, String> getMetadata() {
159
    return mContext.getMetadata();
160
  }
161
162
  /**
163
   * Hashes the URL so that the number of files doesn't eat up disk space
164
   * over time. For static resources, a feature could be added to prevent
165
   * downloading the URL if the hashed filename already exists.
166
   *
167
   * @param src The source file's URL to download.
168
   * @return A {@link Path} to the local file containing the URL's contents.
169
   * @throws Exception Could not download or save the file.
170
   */
171
  private Path downloadImage( final String src ) throws Exception {
172
    final Path imageFile;
173
    final var cachesPath = getCachesPath();
174
175
    clue( "Main.status.image.xhtml.image.download", src );
176
177
    try( final var response = open( src ) ) {
178
      final var mediaType = response.getMediaType();
179
180
      final var ext = MediaTypeExtension.valueFrom( mediaType ).getExtension();
181
      final var hash = DataTypeConverter.toHex( DataTypeConverter.hash( src ) );
182
      final var id = hash.toLowerCase();
183
184
      imageFile = cachesPath.resolve( APP_TITLE_ABBR + id + '.' + ext );
185
186
      // Preserve image files if auto-remove is turned off.
187
      if( autoRemove() ) {
188
        imageFile.toFile().deleteOnExit();
189
      }
190
191
      try( final var image = response.getInputStream() ) {
192
        copy( image, imageFile, REPLACE_EXISTING );
193
      }
194
195
      if( mediaType.isSvg() ) {
196
        sanitize( imageFile );
197
      }
198
    }
199
200
    return imageFile;
201
  }
202
203
  private Path resolveImage( final String src ) throws Exception {
204
    var imagePath = getImagesPath();
205
    var found = false;
206
207
    Path imageFile = null;
208
209
    clue( "Main.status.image.xhtml.image.resolve", src );
210
211
    for( final var extension : getImageOrder() ) {
212
      final var filename = format(
213
        "%s%s%s", src, extension.isBlank() ? "" : ".", extension );
214
      imageFile = imagePath.resolve( filename );
215
216
      if( imageFile.toFile().exists() ) {
217
        found = true;
218
        break;
219
      }
220
    }
221
222
    if( !found ) {
223
      imagePath = getDocumentDir();
224
      imageFile = imagePath.resolve( src );
225
226
      if( !imageFile.toFile().exists() ) {
227
        final var filename = imageFile.toString();
228
        clue( "Main.status.image.xhtml.image.missing", filename );
229
230
        throw new FileNotFoundException( filename );
231
      }
232
    }
233
234
    clue( "Main.status.image.xhtml.image.found", imageFile.toString() );
235
236
    return imageFile;
237
  }
238
239
  private Path getImagesPath() {
240
    return mContext.getImagesPath();
241
  }
242
243
  private Path getCachesPath() {
244
    return mContext.getCachesPath();
245
  }
246
247
  /**
248
   * By including an "empty" extension, the first element returned
249
   * will be the empty string. Thus, the first extension to try is the
250
   * file's default extension. Subsequent iterations will try to find
251
   * a file that has a name matching one of the preferred extensions.
252
   *
253
   * @return A list of extensions, including an empty string at the start.
254
   */
255
  private Iterable<String> getImageOrder() {
256
    return mContext.getImageOrder();
257
  }
258
259
  /**
260
   * Returns the absolute path to the document being edited, which can be used
261
   * to find files included using relative paths.
262
   *
263
   * @return The directory containing the edited file.
264
   */
265
  private Path getDocumentDir() {
266
    return mContext.getBaseDir();
267
  }
268
269
  private Locale getLocale() {
270
    return mContext.getLocale();
271
  }
272
273
  private boolean autoRemove() {
274
    return mContext.getAutoRemove();
275
  }
276
277
  private String wordCount( final Document doc ) {
278
    final var sb = new StringBuilder( 65536 * 10 );
279
280
    visit(
281
      doc,
282
      "//*[normalize-space( text() ) != '']",
283
      node -> sb.append( node.getTextContent() )
284
    );
285
286
    return valueOf( WordCounter.create( getLocale() ).count( sb.toString() ) );
287
  }
288
289
  /**
290
   * Creates contracts with a custom set of unambiguous strings.
291
   *
292
   * @return List of contractions to use for curling straight quotes.
293
   */
294
  private static Contractions createContractions() {
295
    return new Contractions.Builder().build();
296
  }
297
}
1298
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( parse( 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
  public 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 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.io.MediaType;
6
import com.keenwrite.processors.Processor;
7
import com.keenwrite.processors.ProcessorContext;
8
import com.keenwrite.processors.VariableProcessor;
9
import com.keenwrite.processors.markdown.extensions.CaretExtension;
10
import com.keenwrite.processors.markdown.extensions.DocumentOutlineExtension;
11
import com.keenwrite.processors.markdown.extensions.ImageLinkExtension;
12
import com.keenwrite.processors.markdown.extensions.fences.FencedBlockExtension;
13
import com.keenwrite.processors.markdown.extensions.r.RInlineExtension;
14
import com.keenwrite.processors.markdown.extensions.tex.TeXExtension;
15
import com.keenwrite.processors.r.RInlineEvaluator;
16
import com.keenwrite.processors.r.RVariableProcessor;
17
import com.vladsch.flexmark.util.misc.Extension;
18
19
import java.util.ArrayList;
20
import java.util.List;
21
import java.util.function.Function;
22
23
import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN;
24
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
25
26
/**
27
 * Responsible for parsing a Markdown document and rendering it as HTML.
28
 */
29
public final class MarkdownProcessor extends BaseMarkdownProcessor {
30
31
  private MarkdownProcessor(
32
    final Processor<String> successor, final ProcessorContext context ) {
33
    super( successor, context );
34
  }
35
36
  public static MarkdownProcessor create( final ProcessorContext context ) {
37
    return create( IDENTITY, context );
38
  }
39
40
  public static MarkdownProcessor create(
41
    final Processor<String> successor, final ProcessorContext context ) {
42
    return new MarkdownProcessor( successor, context );
43
  }
44
45
  /**
46
   * Creating extensions based using an instance of {@link ProcessorContext}
47
   * indicates that the {@link CaretExtension} should be used to inject the
48
   * caret position into the final HTML document. This enables the HTML
49
   * preview pane to scroll to the same position, relatively speaking, within
50
   * the main document. Scrolling is developed this way to decouple the
51
   * document being edited from the preview pane so that multiple document
52
   * formats can be edited.
53
   *
54
   * @param context Contains necessary information needed to create
55
   *                extensions used by the Markdown parser.
56
   * @return {@link List} of extensions invoked when parsing Markdown.
57
   */
58
  @Override
59
  List<Extension> createExtensions( final ProcessorContext context ) {
60
    final var inputPath = context.getSourcePath();
61
    final var mediaType = MediaType.valueFrom( inputPath );
62
    final Processor<String> processor;
63
    final Function<String, String> evaluator;
64
    final List<Extension> result = new ArrayList<>();
65
66
    if( mediaType == TEXT_R_MARKDOWN ) {
67
      final var rVarProcessor = new RVariableProcessor( IDENTITY, context );
68
      final var rInlineEvaluator = new RInlineEvaluator( rVarProcessor );
69
      result.add( RInlineExtension.create( rInlineEvaluator, context ) );
70
      processor = rVarProcessor;
71
      evaluator = rInlineEvaluator;
72
    }
73
    else {
74
      processor = new VariableProcessor( IDENTITY, context );
75
      evaluator = processor;
76
    }
77
78
    // Add typographic, table, strikethrough, and similar extensions.
79
    result.addAll( super.createExtensions( context ) );
80
81
    result.add( ImageLinkExtension.create( context ) );
82
    result.add( TeXExtension.create( evaluator, context ) );
83
    result.add( FencedBlockExtension.create( processor, evaluator, context ) );
84
85
    if( context.isExportFormat( ExportFormat.NONE ) ) {
86
      result.add( CaretExtension.create( context ) );
87
    }
88
89
    result.add( DocumentOutlineExtension.create( processor ) );
90
    return result;
91
  }
92
}
193
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
17
import static com.keenwrite.ExportFormat.NONE;
18
import static com.keenwrite.events.StatusEvent.clue;
19
import static com.keenwrite.util.ProtocolScheme.getProtocol;
20
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
21
import static com.vladsch.flexmark.html.renderer.LinkStatus.VALID;
22
23
/**
24
 * Responsible for ensuring that images can be rendered relative to a path.
25
 * This allows images to be located virtually anywhere.
26
 */
27
public class ImageLinkExtension extends HtmlRendererAdapter {
28
29
  private final ProcessorContext mContext;
30
31
  private ImageLinkExtension( @NotNull final ProcessorContext context ) {
32
    mContext = context;
33
  }
34
35
  /**
36
   * Creates an extension capable of using a relative path to embed images.
37
   *
38
   * @param context Contains the base directory to search in for images.
39
   * @return The new {@link ImageLinkExtension}, not {@code null}.
40
   */
41
  public static ImageLinkExtension create(
42
    @NotNull final ProcessorContext context ) {
43
    return new ImageLinkExtension( context );
44
  }
45
46
  @Override
47
  public void extend( @NotNull final Builder builder,
48
                      @NotNull final String rendererType ) {
49
    builder.linkResolverFactory( new ResolverFactory() );
50
  }
51
52
  private final class ResolverFactory extends IndependentLinkResolverFactory {
53
    @Override
54
    public @NotNull LinkResolver apply(
55
      @NotNull final LinkResolverBasicContext context ) {
56
      return new ImageLinkResolver();
57
    }
58
  }
59
60
  private class ImageLinkResolver implements LinkResolver {
61
    public ImageLinkResolver() {
62
    }
63
64
    @NotNull
65
    @Override
66
    public ResolvedLink resolveLink(
67
      @NotNull final Node node,
68
      @NotNull final LinkResolverBasicContext context,
69
      @NotNull final ResolvedLink link ) {
70
      return node instanceof Image ? forImage( link, node ) : link;
71
    }
72
73
    /**
74
     * Algorithm:
75
     * <ol>
76
     *   <li>Accept remote URLs as valid links.</li>
77
     *   <li>Accept existing readable files as valid links.</li>
78
     *   <li>Accept non-{@link ExportFormat#NONE} exports as valid links.</li>
79
     *   <li>Append the images dir to the edited file's dir (baseDir).</li>
80
     *   <li>Search for images by extension.</li>
81
     * </ol>
82
     *
83
     * @param link The link URL to resolve.
84
     * @param node The document node containing the URL.
85
     * @return The {@link ResolvedLink} instance used to render the link.
86
     */
87
    private ResolvedLink forImage( final ResolvedLink link, final Node node ) {
88
      var uri = link.getUrl();
89
      final var protocol = getProtocol( uri );
90
91
      if( protocol.isRemote() ) {
92
        return valid( link, uri );
93
      }
94
95
      final var baseDir = getBaseDir();
96
97
      // Determine the fully-qualified file name (fqfn).
98
      final var fqfn = Path.of( baseDir.toString(), uri ).toFile();
99
100
      if( fqfn.isFile() && fqfn.canRead() ||
101
        mContext.getExportFormat() != NONE ) {
102
        return valid( link, uri );
103
      }
104
105
      try {
106
        // Compute the path to the image file. The base directory should
107
        // be an absolute path to the file being edited, without an extension.
108
        final var imagesDir = getImageDir();
109
        final var relativeDir = imagesDir.toString().isEmpty()
110
          ? imagesDir : baseDir.relativize( imagesDir );
111
        final var imageFile = Path.of(
112
          baseDir.toString(), relativeDir.toString(), uri );
113
114
        for( final var ext : getImageOrder() ) {
115
          var file = new File( imageFile.toString() + '.' + ext );
116
117
          if( file.exists() && file.canRead() ) {
118
            uri = file.toURI().toString();
119
            return valid( link, uri );
120
          }
121
        }
122
123
        clue( "Main.status.error.file.missing.near",
124
              imageFile + ".*", node.getLineNumber()
125
        );
126
      } catch( final Exception ex ) {
127
        clue( ex );
128
      }
129
130
      return link;
131
    }
132
133
    private ResolvedLink valid( final ResolvedLink link, final String url ) {
134
      return link.withStatus( VALID ).withUrl( url );
135
    }
136
137
    private Path getImageDir() {
138
      return mContext.getImagesPath();
139
    }
140
141
    private Iterable<String> getImageOrder() {
142
      return mContext.getImageOrder();
143
    }
144
145
    private Path getBaseDir() {
146
      return mContext.getBaseDir();
147
    }
148
  }
149
}
1150
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
      final var result = mRChunkEvaluator.apply( r );
210
211
      return new Tuple<>( svg, link );
212
    }
213
214
    /**
215
     * Splits attributes of the form <code>{r key1=value2 key2=value2}</code>
216
     * into a comma-separated string containing only the key/value pairs,
217
     * such as <code>key1=value1,key2=value2</code>.
218
     *
219
     * @param bs The complete line after the fenced block demarcation.
220
     * @return A comma-separated string of name/value pairs.
221
     */
222
    private String getAttributes( final BasedSequence bs ) {
223
      final var result = new StringBuilder();
224
      final var split = bs.splitList( " " );
225
      final var splits = split.size();
226
227
      for( var i = 1; i < splits; i++ ) {
228
        final var based = split.get( i ).toString();
229
        final var attribute = based.replace( '}', ' ' );
230
231
        // The order of attribute evaluations is in order of performance.
232
        if( !attribute.isBlank() &&
233
          attribute.indexOf( '=' ) > 1 &&
234
          attribute.matches( ".*\\d.*" ) ) {
235
236
          // The comma will do double-duty for separating individual attributes
237
          // as well as being the comma that separates all attributes from the
238
          // SVG image file name.
239
          result.append( ',' ).append( attribute );
240
        }
241
      }
242
243
      return result.toString();
244
    }
245
246
    /**
247
     * This method is a stop-gap because blank lines that contain only
248
     * whitespace are collapsed into lines without any spaces. Consequently,
249
     * the typesetting software does not honour the blank lines, which
250
     * then would otherwise discard blank lines entirely.
251
     * <p>
252
     * Given the following:
253
     *
254
     * <pre>
255
     *   if( bool ) {
256
     *
257
     *
258
     *   }
259
     * </pre>
260
     * <p>
261
     * The typesetter would otherwise render this incorrectly as:
262
     *
263
     * <pre>
264
     *   if( bool ) {
265
     *   }
266
     * </pre>
267
     * <p>
268
     */
269
    private void render(
270
      final FencedCodeBlock node,
271
      final NodeRendererContext context,
272
      final HtmlWriter html ) {
273
      assert node != null;
274
      assert context != null;
275
      assert html != null;
276
277
      html.line();
278
      html.srcPosWithTrailingEOL( node.getChars() )
279
          .withAttr()
280
          .tag( "pre" )
281
          .openPre();
282
283
      final var options = context.getHtmlOptions();
284
      final var languageClass = lookupLanguageClass( node, options );
285
286
      if( !languageClass.isBlank() ) {
287
        html.attr( "class", languageClass );
288
      }
289
290
      html.srcPosWithEOL( node.getContentChars() )
291
          .withAttr( CODE_CONTENT )
292
          .tag( "code" );
293
294
      final var lines = node.getContentLines();
295
296
      for( final var line : lines ) {
297
        if( line.isBlank() ) {
298
          html.text( "    " );
299
        }
300
301
        html.text( line );
302
      }
303
304
      html.tag( "/code" );
305
      html.tag( "/pre" )
306
          .closePre();
307
      html.lineIf( options.htmlBlockCloseTagEol );
308
    }
309
310
    private String lookupLanguageClass(
311
      final FencedCodeBlock node,
312
      final HtmlRendererOptions options ) {
313
      assert node != null;
314
      assert options != null;
315
316
      final var info = node.getInfo();
317
318
      if( info.isNotNull() && !info.isBlank() ) {
319
        final var lang = node
320
          .getInfoDelimitedByAny( options.languageDelimiterSet )
321
          .unescape();
322
        return options
323
          .languageClassMap
324
          .getOrDefault( lang, options.languageClassPrefix + lang );
325
      }
326
327
      return options.noLanguageClass;
328
    }
329
  }
330
331
  private class Factory implements DelegatingNodeRendererFactory {
332
    public Factory() { }
333
334
    @NotNull
335
    @Override
336
    public NodeRenderer apply( @NotNull final DataHolder options ) {
337
      return new CustomRenderer();
338
    }
339
340
    /**
341
     * Return {@code null} to indicate this may delegate to the core renderer.
342
     */
343
    @Override
344
    public Set<Class<?>> getDelegates() {
345
      return null;
346
    }
347
  }
348
}
1349
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/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<>( 512 );
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-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.r;
3
4
import com.keenwrite.preferences.Workspace;
5
import com.keenwrite.sigils.RKeyOperator;
6
7
import java.util.HashMap;
8
import java.util.Map;
9
import java.util.function.Supplier;
10
11
import static com.keenwrite.events.StatusEvent.clue;
12
import static com.keenwrite.preferences.AppKeys.KEY_R_DIR;
13
import static com.keenwrite.preferences.AppKeys.KEY_R_SCRIPT;
14
import static com.keenwrite.processors.r.RVariableProcessor.escape;
15
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
16
17
/**
18
 * Transforms a document containing R statements into Markdown.
19
 */
20
public final class RBootstrapController {
21
22
  private static final RKeyOperator KEY_OPERATOR = new RKeyOperator();
23
24
  private final Workspace mWorkspace;
25
  private final Supplier<Map<String, String>> mDefinitions;
26
27
  public RBootstrapController(
28
    final Workspace workspace,
29
    final Supplier<Map<String, String>> supplier ) {
30
    mWorkspace = workspace;
31
    mDefinitions = supplier;
32
33
    mWorkspace.stringProperty( KEY_R_SCRIPT )
34
              .addListener( ( c, o, n ) -> update() );
35
    mWorkspace.fileProperty( KEY_R_DIR )
36
              .addListener( ( c, o, n ) -> update() );
37
  }
38
39
  /**
40
   * Updates the R code so that R can find imported libraries. Note that
41
   * any existing R functionality will not be overwritten if this method is
42
   * called multiple times.
43
   */
44
  public void update() {
45
    final var bootstrap = getRScript();
46
47
    if( !bootstrap.isBlank() ) {
48
      final var dir = getRWorkingDirectory();
49
      final var definitions = mDefinitions.get();
50
      final var map = new HashMap<String, String>( definitions.size() + 1 );
51
52
      definitions.forEach(
53
        ( k, v ) -> map.put( KEY_OPERATOR.apply( k ), escape( v ) )
54
      );
55
      map.put(
56
        KEY_OPERATOR.apply( "application.r.working.directory" ),
57
        escape( dir )
58
      );
59
60
      try {
61
        Engine.eval( replace( bootstrap, map ) );
62
      } catch( final Exception ex ) {
63
        clue( ex );
64
        // A problem with the bootstrap script is likely caused by variables
65
        // not being loaded. This implies that the R processor is being invoked
66
        // too soon.
67
      }
68
    }
69
  }
70
71
  private String getRScript() {
72
    return mWorkspace.getString( KEY_R_SCRIPT );
73
  }
74
75
  private String getRWorkingDirectory() {
76
    final var wd = mWorkspace.getFile( KEY_R_DIR );
77
    return wd.toString().replace( '\\', '/' );
78
  }
79
}
180
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;
48
49
      while( (began = text.indexOf( PREFIX, index )) >= 0 ) {
50
        buffer.append( text, index, began );
51
52
        ended = text.indexOf( SUFFIX, began + 1 );
53
54
        if( ended > began ) {
55
          final var r = mProcessor.apply(
56
            text.substring( began + PREFIX_LENGTH, ended )
57
          );
58
59
          // Return the evaluated R expression for insertion back into the text.
60
          buffer.append( Engine.eval( r ) );
61
62
          index = ended + 1;
63
        }
64
      }
65
66
      buffer.append( text.substring( index ) );
67
68
      return buffer.toString();
69
    } catch( final Exception ex ) {
70
      clue( STATUS_PARSE_ERROR, ex.getMessage() );
71
72
      // If the string couldn't be parsed using R, append the statement
73
      // that failed to parse, instead of its evaluated value.
74
      return text;
75
    }
76
  }
77
78
  /**
79
   * Answers whether the engine associated with this evaluator may attempt to
80
   * evaluate the given source code statement.
81
   *
82
   * @param code The source code to verify.
83
   * @return {@code true} if the code may be evaluated.
84
   */
85
  @Override
86
  public boolean test( final String code ) {
87
    return code.startsWith( PREFIX );
88
  }
89
}
190
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.ArrayList;
13
import java.util.List;
14
15
import static org.ahocorasick.trie.Trie.builder;
16
17
/**
18
 * Responsible for finding words in a text document. This implementation uses
19
 * a {@link Trie} for efficiency.
20
 */
21
public final class SearchModel {
22
  private final ObjectProperty<IndexRange> mMatchOffset =
23
      new SimpleObjectProperty<>();
24
  private final ObjectProperty<Integer> mMatchCount =
25
      new SimpleObjectProperty<>();
26
  private final ObjectProperty<Integer> mMatchIndex =
27
      new SimpleObjectProperty<>();
28
29
  private CyclicIterator<Emit> mMatches = new CyclicIterator<>( List.of() );
30
31
  private String mNeedle = "";
32
33
  /**
34
   * Creates a new {@link SearchModel} that finds all text string in a
35
   * document simultaneously.
36
   */
37
  public SearchModel() {
38
  }
39
40
  public ObjectProperty<Integer> matchCountProperty() {
41
    return mMatchCount;
42
  }
43
44
  public ObjectProperty<Integer> matchIndexProperty() {
45
    return mMatchIndex;
46
  }
47
48
  /**
49
   * Observers watch this property to be notified when a needle has been
50
   * found in the haystack. Use {@link IndexRange#getStart()} to get the
51
   * absolute offset into the text (zero-based).
52
   *
53
   * @return The {@link IndexRange} property to observe, representing the
54
   * most recently matched text offset into the document.
55
   */
56
  public ObservableValue<IndexRange> matchOffsetProperty() {
57
    return mMatchOffset;
58
  }
59
60
  /**
61
   * Searches the document for text matching the given parameter value. This
62
   * is the main entry point for kicking off text searches.
63
   *
64
   * @param needle   The text string to find in the document, no regex allowed.
65
   * @param haystack The document to search within for a text string.
66
   */
67
  public void search( final String needle, final String haystack ) {
68
    assert needle != null;
69
    assert haystack != null;
70
71
    final var trie = builder()
72
        .ignoreCase()
73
        .ignoreOverlaps()
74
        .addKeyword( needle )
75
        .build();
76
    final var emits = trie.parseText( haystack );
77
78
    mMatches = new CyclicIterator<>( new ArrayList<>( emits ) );
79
    mMatchCount.set( emits.size() );
80
    mNeedle = needle;
81
    advance();
82
  }
83
84
  /**
85
   * Searches the document for the last known needle.
86
   *
87
   * @param haystack The new text to search.
88
   */
89
  public void search( final String haystack ) {
90
    search( mNeedle, haystack );
91
  }
92
93
  /**
94
   * Moves the search iterator to the next match, wrapping as needed.
95
   */
96
  public void advance() {
97
    if( mMatches.hasNext() ) {
98
      setCurrent( mMatches.next() );
99
    }
100
  }
101
102
  /**
103
   * Moves the search iterator to the previous match, wrapping as needed.
104
   */
105
  public void retreat() {
106
    if( mMatches.hasPrevious() ) {
107
      setCurrent( mMatches.previous() );
108
    }
109
  }
110
111
  private void setCurrent( final Emit emit ) {
112
    mMatchOffset.set( new IndexRange( emit.getStart(), emit.getEnd() ) );
113
    mMatchIndex.set( mMatches.getIndex() + 1 );
114
  }
115
}
1116
A src/main/java/com/keenwrite/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.SecureRandom;
6
import java.security.cert.X509Certificate;
7
8
import static javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier;
9
import static javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory;
10
11
/**
12
 * Responsible for trusting all certificate chains. The purpose of this class
13
 * is to work-around certificate issues caused by software that blocks
14
 * HTTP requests. For example, Zscaler may block HTTP requests to kroki.io
15
 * when generating diagrams.
16
 */
17
public final class PermissiveCertificate {
18
  /**
19
   * Create a trust manager that does not validate certificate chains.
20
   */
21
  private static final TrustManager[] TRUST_ALL_CERTS = new TrustManager[]{
22
    new X509TrustManager() {
23
      @Override
24
      public X509Certificate[] getAcceptedIssuers() {
25
        return new X509Certificate[ 0 ];
26
      }
27
28
      @Override
29
      public void checkClientTrusted(
30
        X509Certificate[] certs, String authType ) {
31
      }
32
33
      @Override
34
      public void checkServerTrusted(
35
        X509Certificate[] certs, String authType ) {
36
      }
37
    }
38
  };
39
40
  /**
41
   * Responsible for permitting all hostnames for making HTTP requests.
42
   */
43
  private static class PermissiveHostNameVerifier implements HostnameVerifier {
44
    @Override
45
    public boolean verify( final String hostname, final SSLSession session ) {
46
      return true;
47
    }
48
  }
49
50
  /**
51
   * Install the all-trusting trust manager. If this fails it means that in
52
   * certain situations the HTML preview may fail to render diagrams. A way
53
   * to work around the issue is to install a local server for generating
54
   * diagrams.
55
   */
56
  public static boolean installTrustManager() {
57
    try {
58
      final var context = SSLContext.getInstance( "SSL" );
59
      context.init( null, TRUST_ALL_CERTS, new SecureRandom() );
60
      setDefaultSSLSocketFactory( context.getSocketFactory() );
61
      setDefaultHostnameVerifier( new PermissiveHostNameVerifier() );
62
      return true;
63
    } catch( final Exception ex ) {
64
      return false;
65
    }
66
  }
67
68
  /**
69
   * Use {@link #installTrustManager()}.
70
   */
71
  private PermissiveCertificate() {
72
  }
73
}
174
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 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.spelling.impl;
3
4
import com.keenwrite.events.spelling.LexiconLoadedEvent;
5
import com.keenwrite.exceptions.MissingFileException;
6
7
import java.io.BufferedReader;
8
import java.io.InputStream;
9
import java.io.InputStreamReader;
10
import java.util.HashMap;
11
import java.util.Locale;
12
13
import static com.keenwrite.constants.Constants.LEXICONS_DIRECTORY;
14
import static com.keenwrite.events.StatusEvent.clue;
15
import static java.lang.String.format;
16
import static java.nio.charset.StandardCharsets.UTF_8;
17
18
/**
19
 * Responsible for loading a set of single words, asynchronously.
20
 */
21
public final class Lexicon {
22
  /**
23
   * Most lexicons have 100,000 words.
24
   */
25
  private static final int LEXICON_CAPACITY = 100_000;
26
27
  /**
28
   * The word-frequency entries are tab-delimited.
29
   */
30
  private static final char DELIMITER = '\t';
31
32
  /**
33
   * Load the lexicon into memory then fire an event indicating that the
34
   * word-frequency pairs are available to use for spellchecking. This
35
   * happens asynchronously so that the UI can load faster.
36
   *
37
   * @param locale The locale having a corresponding lexicon to load.
38
   */
39
  public static void read( final Locale locale ) {
40
    assert locale != null;
41
42
    new Thread( read( toResourcePath( locale ) ) ).start();
43
  }
44
45
  private static Runnable read( final String path ) {
46
    return () -> {
47
      try( final var resource = openResource( path ) ) {
48
        read( resource );
49
      } catch( final Exception ex ) {
50
        clue( ex );
51
      }
52
    };
53
  }
54
55
  private static void read( final InputStream resource ) {
56
    try( final var input = new InputStreamReader( resource, UTF_8 );
57
         final var reader = new BufferedReader( input ) ) {
58
      read( reader );
59
    } catch( final Exception ex ) {
60
      clue( ex );
61
    }
62
  }
63
64
  private static void read( final BufferedReader reader ) {
65
    try {
66
      long count = 0;
67
      final var lexicon = new HashMap<String, Long>( LEXICON_CAPACITY );
68
      String line;
69
70
      while( (line = reader.readLine()) != null ) {
71
        final var index = line.indexOf( DELIMITER );
72
        final var word = line.substring( 0, index == -1 ? 0 : index );
73
        final var frequency = parse( line.substring( index + 1 ) );
74
75
        lexicon.put( word, frequency );
76
77
        // Slower machines may benefit users by showing a loading message.
78
        if( ++count % 25_000 == 0 ) {
79
          status( "loading", count );
80
        }
81
      }
82
83
      // Indicate that loading the lexicon is finished.
84
      status( "loaded", count );
85
      LexiconLoadedEvent.fire( lexicon );
86
    } catch( final Exception ex ) {
87
      clue( ex );
88
    }
89
  }
90
91
  /**
92
   * Prevents autoboxing and uses cached values when possible. A return value
93
   * of 0L means that the word will receive the lowest priority. If there's
94
   * an error (i.e., data corruption) parsing the number, the spell checker
95
   * will still work, but be suboptimal for all erroneous entries.
96
   *
97
   * @param number The numeric value to parse into a long object.
98
   * @return The parsed value, or 0L if the number couldn't be parsed.
99
   */
100
  private static Long parse( final String number ) {
101
    try {
102
      return Long.valueOf( number );
103
    } catch( final NumberFormatException ex ) {
104
      clue( ex );
105
      return 0L;
106
    }
107
  }
108
109
  private static InputStream openResource( final String path )
110
    throws MissingFileException {
111
    final var resource = Lexicon.class.getResourceAsStream( path );
112
113
    if( resource == null ) {
114
      throw new MissingFileException( path );
115
    }
116
117
    return resource;
118
  }
119
120
  /**
121
   * Convert a {@link Locale} into a path that can be loaded as a resource.
122
   *
123
   * @param locale The {@link Locale} to convert to a resource.
124
   * @return The slash-separated path to a lexicon resource file.
125
   */
126
  private static String toResourcePath( final Locale locale ) {
127
    final var language = locale.getLanguage();
128
    return format( "/%s/%s.txt", LEXICONS_DIRECTORY, language );
129
  }
130
131
  private static void status( final String s, final long count ) {
132
    clue( "Main.status.lexicon." + s, count );
133
  }
134
}
1135
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.typesetting.containerization.Podman;
7
import org.apache.commons.io.FilenameUtils;
8
9
import java.nio.file.Path;
10
import java.util.LinkedList;
11
import java.util.concurrent.Callable;
12
13
import static com.keenwrite.constants.Constants.USER_DIRECTORY;
14
import static com.keenwrite.io.StreamGobbler.gobble;
15
import static com.keenwrite.typesetting.containerization.Podman.MANAGER;
16
import static java.lang.String.format;
17
18
/**
19
 * Responsible for invoking a typesetter installed inside a container.
20
 */
21
public final class GuestTypesetter extends Typesetter
22
  implements Callable<Boolean> {
23
  private static final String SOURCE = "/root/source";
24
  private static final String TARGET = "/root/target";
25
  private static final String THEMES = "/root/themes";
26
  private static final String IMAGES = "/root/images";
27
  private static final String CACHES = "/root/caches";
28
  private static final String FONTS = "/root/fonts";
29
30
  private static final boolean READONLY = true;
31
  private static final boolean READWRITE = false;
32
33
  private static final String TYPESETTER_VERSION =
34
    TYPESETTER_EXE + " --version > /dev/null";
35
36
  public GuestTypesetter( final Mutator mutator ) {
37
    super( mutator );
38
  }
39
40
  @Override
41
  public Boolean call() throws Exception {
42
    final var sourcePath = getSourcePath();
43
    final var targetPath = getTargetPath();
44
    final var themesPath = getThemesPath();
45
46
    final var sourceDir = normalize( sourcePath.getParent() );
47
    final var targetDir = normalize( targetPath.getParent() );
48
    final var themesDir = normalize( themesPath.getParent() );
49
    final var imagesDir = normalize( getImagesPath() );
50
    final var cachesDir = normalize( getCachesPath() );
51
    final var fontsDir = normalize( getFontsPath() );
52
53
    final var sourceFile = sourcePath.getFileName();
54
    final var targetFile = targetPath.getFileName();
55
    final var themesFile = themesPath.getFileName();
56
57
    final var manager = new Podman();
58
    manager.mount( sourceDir, SOURCE, READONLY );
59
    manager.mount( targetDir, TARGET, READWRITE );
60
    manager.mount( themesDir, THEMES, READONLY );
61
    manager.mount( imagesDir, IMAGES, READONLY );
62
    manager.mount( cachesDir, CACHES, READWRITE );
63
    manager.mount( fontsDir, FONTS, READONLY );
64
65
    final var args = new LinkedList<String>();
66
    args.add( TYPESETTER_EXE );
67
    args.addAll( commonOptions() );
68
    args.add( format(
69
      "--arguments=themesdir=%s/%s,imagesdir=%s,cachesdir=%s",
70
      THEMES, themesFile, IMAGES, CACHES
71
    ) );
72
    args.add( format( "--path='%s/%s'", THEMES, themesFile ) );
73
    args.add( format( "--result='%s'", removeExtension( targetFile ) ) );
74
    args.add( format( "%s/%s", SOURCE, sourceFile ) );
75
76
    final var listener = new PaginationListener();
77
    final var command = String.join( " ", args );
78
79
    manager.run( in -> StreamGobbler.gobble( in, listener ), command );
80
81
    return true;
82
  }
83
84
  /**
85
   * If the path doesn't exist right before typesetting, switch the path
86
   * to the user's home directory to increase the odds of the typesetter
87
   * succeeding. This could help, for example, if the images directory was
88
   * deleted or moved.
89
   *
90
   * @param path The path to verify existence.
91
   * @return The given path, if it exists, otherwise the user's home directory.
92
   */
93
  private static Path normalize( final Path path ) {
94
    assert path != null;
95
96
    return path.toFile().exists()
97
      ? path
98
      : USER_DIRECTORY.toPath();
99
  }
100
101
  static String removeExtension( final Path path ) {
102
    return FilenameUtils.removeExtension( path.toString() );
103
  }
104
105
  /**
106
   * @return {@code true} indicates that the containerized typesetter is
107
   * installed, properly configured, and ready to typeset documents.
108
   */
109
  static boolean isReady() {
110
    if( MANAGER.canRun() ) {
111
      final var exitCode = new StringBuilder();
112
      final var manager = new Podman();
113
114
      try {
115
        // Running blocks until the command completes.
116
        manager.run(
117
          input -> gobble( input, s -> exitCode.append( s.trim() ) ),
118
          TYPESETTER_VERSION + "; echo $?"
119
        );
120
121
        // If the typesetter ran with an exit code of 0, it is available.
122
        return exitCode.indexOf( "0" ) == 0;
123
      } catch( final CommandNotFoundException ignored ) { }
124
    }
125
126
    return false;
127
  }
128
}
1129
A src/main/java/com/keenwrite/typesetting/HostTypesetter.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting;
3
4
import com.keenwrite.collections.CircularQueue;
5
import com.keenwrite.io.StreamGobbler;
6
import com.keenwrite.io.SysFile;
7
8
import java.io.FileNotFoundException;
9
import java.io.IOException;
10
import java.nio.file.NoSuchFileException;
11
import java.nio.file.Path;
12
import java.util.ArrayList;
13
import java.util.List;
14
import java.util.concurrent.Callable;
15
16
import static com.keenwrite.constants.Constants.DEFAULT_DIRECTORY;
17
import static com.keenwrite.events.StatusEvent.clue;
18
import static java.lang.ProcessBuilder.Redirect.DISCARD;
19
import static java.lang.System.getProperty;
20
import static java.nio.file.Files.*;
21
import static java.util.Arrays.asList;
22
import static java.util.concurrent.TimeUnit.SECONDS;
23
import static org.apache.commons.io.FilenameUtils.removeExtension;
24
25
/**
26
 * Responsible for invoking an executable to typeset text. This will
27
 * construct suitable command-line arguments to invoke the typesetting engine.
28
 * This uses a version of the typesetter installed on the host system.
29
 */
30
public final class HostTypesetter extends Typesetter
31
  implements Callable<Boolean> {
32
  private static final SysFile TYPESETTER = new SysFile( TYPESETTER_EXE );
33
34
  HostTypesetter( final Mutator mutator ) {
35
    super( mutator );
36
  }
37
38
  /**
39
   * Answers whether the typesetting software is installed locally.
40
   *
41
   * @return {@code true} if the typesetting software is installed on the host.
42
   */
43
  public static boolean isReady() {
44
    return TYPESETTER.canRun();
45
  }
46
47
  /**
48
   * Launches a task to typeset a document.
49
   */
50
  private class TypesetTask implements Callable<Boolean> {
51
    private final List<String> mArgs = new ArrayList<>();
52
53
    /**
54
     * Working directory must be set because ConTeXt cannot write the
55
     * result to an arbitrary location.
56
     */
57
    private final Path mDirectory;
58
59
    private TypesetTask() {
60
      final var parentDir = getTargetPath().getParent();
61
      mDirectory = parentDir == null ? DEFAULT_DIRECTORY : parentDir;
62
    }
63
64
    /**
65
     * Initializes ConTeXt, which means creating the cache directory if it
66
     * doesn't already exist. The theme entry point must be named 'main.tex'.
67
     *
68
     * @return {@code true} if the cache directory exists.
69
     */
70
    private boolean reinitialize() {
71
      final var cacheExists = !isEmpty( getCacheDir().toPath() );
72
73
      // Ensure invoking multiple times will load the correct arguments.
74
      mArgs.clear();
75
      mArgs.add( TYPESETTER_EXE );
76
77
      if( cacheExists ) {
78
        mArgs.addAll( options() );
79
80
        final var sb = new StringBuilder( 128 );
81
        mArgs.forEach( arg -> sb.append( arg ).append( " " ) );
82
        clue( sb.toString() );
83
      }
84
      else {
85
        mArgs.add( "--generate" );
86
      }
87
88
      return cacheExists;
89
    }
90
91
    /**
92
     * Setting {@code TEXMFCACHE} when run on a fresh system fails on the first
93
     * try. If the cache directory doesn't exist, attempt to create it, then
94
     * call ConTeXt to generate the PDF. This is brittle because if the
95
     * directory is empty, or not populated with cached data, a false positive
96
     * will be returned, resulting in no PDF being created.
97
     *
98
     * @return {@code true} if the document was typeset successfully.
99
     * @throws IOException          If the process could not be started.
100
     * @throws InterruptedException If the process was killed.
101
     */
102
    private boolean typeset() throws IOException, InterruptedException {
103
      return reinitialize() ? call() : call() && reinitialize() && call();
104
    }
105
106
    @Override
107
    public Boolean call() throws IOException, InterruptedException {
108
      final var stdout = new CircularQueue<String>( 150 );
109
      final var builder = new ProcessBuilder( mArgs );
110
      builder.directory( mDirectory.toFile() );
111
      builder.environment().put( "TEXMFCACHE", getCacheDir().toString() );
112
113
      // Without redirecting (or draining) stderr, the command may not
114
      // terminate successfully.
115
      builder.redirectError( DISCARD );
116
117
      final var process = builder.start();
118
      final var listener = new PaginationListener();
119
120
      // Slurp page numbers in a separate thread while typesetting.
121
      StreamGobbler.gobble( process.getInputStream(), line -> {
122
        listener.accept( line );
123
        stdout.add( line );
124
      } );
125
126
      // Even though the process has completed, there may be incomplete I/O.
127
      process.waitFor();
128
129
      // Allow time for any incomplete I/O to take place.
130
      process.waitFor( 1, SECONDS );
131
132
      final var exit = process.exitValue();
133
      process.destroy();
134
135
      // If there was an error, the typesetter will leave behind log, pdf, and
136
      // error files.
137
      if( exit > 0 ) {
138
        final var xmlName = getSourcePath().getFileName().toString();
139
        final var srcName = getTargetPath().getFileName().toString();
140
        final var logName = newExtension( xmlName, ".log" );
141
        final var errName = newExtension( xmlName, "-error.log" );
142
        final var pdfName = newExtension( xmlName, ".pdf" );
143
        final var tuaName = newExtension( xmlName, ".tua" );
144
        final var badName = newExtension( srcName, ".log" );
145
146
        log( badName );
147
        log( logName );
148
        log( errName );
149
        log( stdout.stream().toList() );
150
151
        // Users may opt to keep these files around for debugging purposes.
152
        if( autoRemove() ) {
153
          deleteIfExists( logName );
154
          deleteIfExists( errName );
155
          deleteIfExists( pdfName );
156
          deleteIfExists( badName );
157
          deleteIfExists( tuaName );
158
        }
159
      }
160
161
      // Exit value for a successful invocation of the typesetter. This value
162
      // is returned when creating the cache on the first run as well as
163
      // creating PDFs on subsequent runs (after the cache has been created).
164
      // Users don't care about exit codes, only whether the PDF was generated.
165
      return exit == 0;
166
    }
167
168
    private Path newExtension( final String baseName, final String ext ) {
169
      final var path = getTargetPath();
170
      return path.resolveSibling( removeExtension( baseName ) + ext );
171
    }
172
173
    /**
174
     * Fires a status message for each line in the given file. The file format
175
     * is somewhat machine-readable, but no effort beyond line splitting is
176
     * made to parse the text.
177
     *
178
     * @param path Path to the file containing error messages.
179
     */
180
    private void log( final Path path ) throws IOException {
181
      if( exists( path ) ) {
182
        log( readAllLines( path ) );
183
      }
184
    }
185
186
    private void log( final List<String> lines ) {
187
      final var splits = new ArrayList<String>( lines.size() * 2 );
188
189
      for( final var line : lines ) {
190
        splits.addAll( asList( line.split( "\\\\n" ) ) );
191
      }
192
193
      clue( splits );
194
    }
195
196
    /**
197
     * Returns the location of the cache directory.
198
     *
199
     * @return A fully qualified path to the location to store temporary
200
     * files between typesetting runs.
201
     */
202
    @SuppressWarnings( "SpellCheckingInspection" )
203
    private java.io.File getCacheDir() {
204
      final var temp = getProperty( "java.io.tmpdir" );
205
      final var cache = Path.of( temp, "luatex-cache" );
206
      return cache.toFile();
207
    }
208
209
    /**
210
     * Answers whether the given directory is empty. The typesetting software
211
     * creates a non-empty directory by default. The return value from this
212
     * method is a proxy to answering whether the typesetter has been run for
213
     * the first time or not.
214
     *
215
     * @param path The directory to check for emptiness.
216
     * @return {@code true} if the directory is empty.
217
     */
218
    private boolean isEmpty( final Path path ) {
219
      try( final var stream = newDirectoryStream( path ) ) {
220
        return !stream.iterator().hasNext();
221
      } catch( final NoSuchFileException | FileNotFoundException ex ) {
222
        // A missing directory means it doesn't exist, ergo is empty.
223
        return true;
224
      } catch( final IOException ex ) {
225
        throw new RuntimeException( ex );
226
      }
227
    }
228
  }
229
230
  /**
231
   * This will typeset the document using a new process. The return value only
232
   * indicates whether the typesetter exists, not whether the typesetting was
233
   * successful. The typesetter must be known to exist prior to calling this
234
   * method.
235
   *
236
   * @throws IOException                 If the process could not be started.
237
   * @throws InterruptedException        If the process was killed.
238
   * @throws TypesetterNotFoundException When no typesetter is along the PATH.
239
   */
240
  @Override
241
  public Boolean call()
242
    throws IOException, InterruptedException, TypesetterNotFoundException {
243
    final var task = new HostTypesetter.TypesetTask();
244
    return task.typeset();
245
  }
246
}
1247
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 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting;
3
4
import com.keenwrite.util.GenericBuilder;
5
import com.keenwrite.util.Time;
6
7
import java.nio.file.Path;
8
import java.time.Duration;
9
import java.util.LinkedList;
10
import java.util.List;
11
import java.util.concurrent.Callable;
12
13
import static com.keenwrite.events.StatusEvent.clue;
14
import static com.keenwrite.util.Time.toElapsedTime;
15
import static java.lang.String.format;
16
import static java.lang.System.currentTimeMillis;
17
import static java.time.Duration.ofMillis;
18
19
/**
20
 * Responsible for typesetting a document using either a typesetter installed
21
 * on the computer ({@link HostTypesetter} or installed within a container
22
 * ({@link GuestTypesetter}).
23
 */
24
public class Typesetter {
25
  /**
26
   * Name of the executable program that can typeset documents.
27
   */
28
  static final String TYPESETTER_EXE = "mtxrun";
29
30
  public static GenericBuilder<Mutator, Typesetter> builder() {
31
    return GenericBuilder.of( Mutator::new, Typesetter::new );
32
  }
33
34
  public static final class Mutator {
35
    private Path mSourcePath;
36
    private Path mTargetPath;
37
    private Path mThemesPath;
38
    private Path mImagesPath;
39
    private Path mCachesPath;
40
    private Path mFontsPath;
41
    private boolean mAutoRemove;
42
43
    /**
44
     * @param inputPath The input document to typeset.
45
     */
46
    public void setSourcePath( final Path inputPath ) {
47
      mSourcePath = inputPath;
48
    }
49
50
    /**
51
     * @param outputPath Path to the finished typeset document to create.
52
     */
53
    public void setTargetPath( final Path outputPath ) {
54
      mTargetPath = outputPath;
55
    }
56
57
    /**
58
     * @param themePath Fully qualified path to the theme directory, which
59
     *                  ends with the selected theme name.
60
     */
61
    public void setThemesPath( final Path themePath ) {
62
      mThemesPath = themePath;
63
    }
64
65
    /**
66
     * @param imagePath Fully qualified path to the "images" directory.
67
     */
68
    public void setImagesPath( final Path imagePath ) {
69
      mImagesPath = imagePath;
70
    }
71
72
    /**
73
     * @param cachePath Fully qualified path to the "caches" directory.
74
     */
75
    public void setCachesPath( final Path cachePath ) {
76
      mCachesPath = cachePath;
77
    }
78
79
    /**
80
     * @param fontsPath Fully qualified path to the "fonts" directory.
81
     */
82
    public void setFontsPath( final Path fontsPath ) {
83
      mFontsPath = fontsPath;
84
    }
85
86
    /**
87
     * @param remove {@code true} to remove all temporary files after the
88
     *               typesetter produces a PDF file.
89
     */
90
    public void setAutoRemove( final boolean remove ) {
91
      mAutoRemove = remove;
92
    }
93
94
    public Path getSourcePath() {
95
      return mSourcePath;
96
    }
97
98
    public Path getTargetPath() {
99
      return mTargetPath;
100
    }
101
102
    public Path getThemesPath() {
103
      return mThemesPath;
104
    }
105
106
    public Path getImagesPath() {
107
      return mImagesPath;
108
    }
109
110
    public Path getCachesPath() {
111
      return mCachesPath;
112
    }
113
114
    public Path getFontsPath() {
115
      return mFontsPath;
116
    }
117
118
    public boolean isAutoRemove() {
119
      return mAutoRemove;
120
    }
121
  }
122
123
  private final Mutator mMutator;
124
125
  /**
126
   * Creates a new {@link Typesetter} instance capable of configuring the
127
   * typesetter used to generate a typeset document.
128
   */
129
  Typesetter( final Mutator mutator ) {
130
    assert mutator != null;
131
132
    mMutator = mutator;
133
  }
134
135
  public void typeset() throws Exception {
136
    final Callable<Boolean> typesetter;
137
138
    if( HostTypesetter.isReady() ) {
139
      typesetter = new HostTypesetter( mMutator );
140
    }
141
    else if( GuestTypesetter.isReady() ) {
142
      typesetter = new GuestTypesetter( mMutator );
143
    }
144
    else {
145
      throw new TypesetterNotFoundException( TYPESETTER_EXE );
146
    }
147
148
    final var outputPath = getTargetPath();
149
    final var prefix = "Main.status.typeset";
150
151
    clue( prefix + ".began", outputPath );
152
153
    final var time = currentTimeMillis();
154
    final var success = typesetter.call();
155
    final var suffix = success ? ".success" : ".failure";
156
157
    clue( prefix + ".ended" + suffix, outputPath, since( time ) );
158
  }
159
160
  /**
161
   * Generates the command-line arguments used to invoke the typesetter.
162
   */
163
  @SuppressWarnings( "SpellCheckingInspection" )
164
  List<String> options() {
165
    final var args = commonOptions();
166
167
    final var sourcePath = getSourcePath().toString();
168
    final var targetPath = getTargetPath().getFileName();
169
    final var themesPath = getThemesPath();
170
    final var imagesPath = getImagesPath();
171
    final var cachesPath = getCachesPath();
172
173
    args.add(
174
      format( "--arguments=themesdir=%s,imagesdir=%s,cachesdir=%s",
175
              themesPath, imagesPath, cachesPath  )
176
    );
177
    args.add( format( "--path='%s'", themesPath ) );
178
    args.add( format( "--result='%s'", targetPath ) );
179
    args.add( sourcePath );
180
181
    return args;
182
  }
183
184
  @SuppressWarnings( "SpellCheckingInspection" )
185
  List<String> commonOptions() {
186
    final var args = new LinkedList<String>();
187
188
    args.add( "--autogenerate" );
189
    args.add( "--script" );
190
    args.add( "mtx-context" );
191
    args.add( "--batchmode" );
192
    args.add( "--nonstopmode" );
193
    args.add( "--purgeall" );
194
    args.add( "--environment='main'" );
195
196
    return args;
197
  }
198
199
  protected Path getSourcePath() {
200
    return mMutator.getSourcePath();
201
  }
202
203
  protected Path getTargetPath() {
204
    return mMutator.getTargetPath();
205
  }
206
207
  protected Path getThemesPath() {
208
    return mMutator.getThemesPath();
209
  }
210
211
  protected Path getImagesPath() {
212
    return mMutator.getImagesPath();
213
  }
214
215
  protected Path getCachesPath() {
216
    return mMutator.getCachesPath();
217
  }
218
219
  protected Path getFontsPath() {
220
    return mMutator.getFontsPath();
221
  }
222
223
  /**
224
   * Answers whether logs and other files should be deleted upon error. The
225
   * log files are useful for debugging.
226
   *
227
   * @return {@code true} to delete generated files.
228
   */
229
  public boolean autoRemove() {
230
    return mMutator.isAutoRemove();
231
  }
232
233
  public static boolean canRun() {
234
    return hostCanRun() || guestCanRun();
235
  }
236
237
  private static boolean hostCanRun() {
238
    return HostTypesetter.isReady();
239
  }
240
241
  private static boolean guestCanRun() {
242
    return GuestTypesetter.isReady();
243
  }
244
245
  /**
246
   * Calculates the time that has elapsed from the current time to the
247
   * given moment in time.
248
   *
249
   * @param start The starting time, which should be before the current time.
250
   * @return A human-readable formatted time.
251
   * @see Time#toElapsedTime(Duration)
252
   */
253
  private static String since( final long start ) {
254
    return toElapsedTime( ofMillis( currentTimeMillis() - start ) );
255
  }
256
}
1257
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 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.containerization;
3
4
import com.keenwrite.io.CommandNotFoundException;
5
6
import java.io.File;
7
import java.io.IOException;
8
import java.util.List;
9
10
public interface ContainerManager {
11
  /**
12
   * Installs the container software, in quiet and headless mode if possible.
13
   *
14
   * @param exe The installer binary to run.
15
   * @return The exit code from the installer program, or -1 on failure.
16
   * @throws IOException The container installer could not be run.
17
   */
18
  int install( final File exe )
19
    throws IOException;
20
21
  /**
22
   * Runs preliminary commands against the container before starting.
23
   *
24
   * @param processor Processes the command output (in a separate thread).
25
   * @throws CommandNotFoundException The container executable was not found.
26
   */
27
  void start( StreamProcessor processor ) throws CommandNotFoundException;
28
29
  /**
30
   * Requests that the container manager load an image into the container.
31
   *
32
   * @param name The full container name of the image to pull.
33
   * @param processor Processes the command output (in a separate thread).
34
   * @throws CommandNotFoundException The container executable was not found.
35
   */
36
  void pull( StreamProcessor processor, String name )
37
    throws CommandNotFoundException;
38
39
  /**
40
   * Runs a command using the container manager.
41
   *
42
   * @param processor Processes the command output (in a separate thread).
43
   * @param args      The command and arguments to run.
44
   * @return The exit code returned by the installer program.
45
   * @throws CommandNotFoundException The container executable was not found.
46
   */
47
  int run( StreamProcessor processor, String... args )
48
    throws CommandNotFoundException;
49
50
  /**
51
   * Convenience method to run a command using the container manager.
52
   *
53
   * @see #run(StreamProcessor, String...)
54
   */
55
  default int run( final StreamProcessor listener, final List<String> args )
56
    throws CommandNotFoundException {
57
    return run( listener, toArray( args ) );
58
  }
59
60
  /**
61
   * Convenience method to convert a {@link List} into an array.
62
   *
63
   * @param list The elements to convert to an array.
64
   * @return The converted {@link List}.
65
   */
66
  default String[] toArray( final List<String> list ) {
67
    return list.toArray( new String[ 0 ] );
68
  }
69
}
170
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.io.CommandNotFoundException;
5
import com.keenwrite.io.SysFile;
6
7
import java.io.File;
8
import java.nio.file.Path;
9
import java.util.LinkedList;
10
import java.util.List;
11
12
import static com.keenwrite.Bootstrap.CONTAINER_VERSION;
13
import static java.lang.String.format;
14
import static java.lang.System.arraycopy;
15
import static java.util.Arrays.copyOf;
16
17
/**
18
 * Provides facilities for interacting with a container environment.
19
 */
20
public final class Podman implements ContainerManager {
21
  public static final SysFile MANAGER = new SysFile( "podman" );
22
  public static final String CONTAINER_SHORTNAME = "typesetter";
23
  public static final String CONTAINER_NAME =
24
    format( "%s:%s", CONTAINER_SHORTNAME, CONTAINER_VERSION );
25
26
  private final List<String> mMountPoints = new LinkedList<>();
27
28
  public Podman() { }
29
30
  @Override
31
  public int install( final File exe ) {
32
    // This monstrosity runs the installer in the background without displaying
33
    // a secondary command window, while blocking until the installer completes
34
    // and an exit code can be determined. I hate Windows.
35
    final var builder = processBuilder(
36
      "cmd", "/c",
37
      format(
38
        "start /b /high /wait cmd /c %s /quiet /install & exit ^!errorlevel^!",
39
        exe.getAbsolutePath()
40
      )
41
    );
42
43
    try {
44
      // Wait for installation to finish (successfully or not).
45
      return wait( builder.start() );
46
    } catch( final Exception ignored ) {
47
      return -1;
48
    }
49
  }
50
51
  @Override
52
  public void start( final StreamProcessor processor )
53
    throws CommandNotFoundException {
54
    machine( processor, "stop" );
55
    podman( processor, "system", "prune", "--force" );
56
    machine( processor, "rm", "--force" );
57
    machine( processor, "init" );
58
    machine( processor, "start" );
59
  }
60
61
  @Override
62
  public void pull( final StreamProcessor processor, final String name )
63
    throws CommandNotFoundException {
64
    podman( processor, "pull", "ghcr.io/davejarvis/" + name );
65
  }
66
67
  /**
68
   * Runs:
69
   * <p>
70
   * <code>podman run --network=host --rm -t IMAGE /bin/sh -lc</code>
71
   * </p>
72
   * followed by the given arguments.
73
   *
74
   * @param args The command and arguments to run against the container.
75
   * @return The exit code from running the container manager (not the
76
   * exit code from running the command).
77
   * @throws CommandNotFoundException Container manager couldn't be found.
78
   */
79
  @Override
80
  public int run(
81
    final StreamProcessor processor,
82
    final String... args ) throws CommandNotFoundException {
83
    final var options = new LinkedList<String>();
84
    options.add( "run" );
85
    options.add( "--rm" );
86
    options.add( "--network=host" );
87
    options.addAll( mMountPoints );
88
    options.add( "-t" );
89
    options.add( CONTAINER_NAME );
90
    options.add( "/bin/sh" );
91
    options.add( "-lc" );
92
93
    final var command = toArray( toArray( options ), args );
94
    return podman( processor, command );
95
  }
96
97
  /**
98
   * Generates a command-line argument representing a mount point between
99
   * the host and guest systems.
100
   *
101
   * @param hostDir  The host directory to mount in the container.
102
   * @param guestDir The guest directory to map from the container to host.
103
   * @param readonly Set {@code true} to make the mount point read-only.
104
   */
105
  public void mount(
106
    final Path hostDir, final String guestDir, final boolean readonly ) {
107
    assert hostDir != null;
108
    assert guestDir != null;
109
    assert !guestDir.isBlank();
110
    assert hostDir.toFile().isDirectory();
111
112
    mMountPoints.add(
113
      format( "-v%s:%s:%s", hostDir, guestDir, readonly ? "ro" : "Z" )
114
    );
115
  }
116
117
  private static void machine(
118
    final StreamProcessor processor,
119
    final String... args )
120
    throws CommandNotFoundException {
121
    podman( processor, toArray( "machine", args ) );
122
  }
123
124
  private static int podman(
125
    final StreamProcessor processor, final String... args )
126
    throws CommandNotFoundException {
127
    try {
128
      final var exe = MANAGER.locate();
129
      final var path = exe.orElseThrow();
130
      final var builder = processBuilder( path, args );
131
      final var process = builder.start();
132
133
      processor.start( process.getInputStream() );
134
135
      return wait( process );
136
    } catch( final Exception ex ) {
137
      throw new CommandNotFoundException( MANAGER.toString() );
138
    }
139
  }
140
141
  /**
142
   * Performs a blocking wait until the {@link Process} completes.
143
   *
144
   * @param process The {@link Process} to await completion.
145
   * @return The exit code from running a command.
146
   * @throws InterruptedException The {@link Process} was interrupted.
147
   */
148
  private static int wait( final Process process ) throws InterruptedException {
149
    final var exitCode = process.waitFor();
150
    process.destroy();
151
152
    return exitCode;
153
  }
154
155
  private static ProcessBuilder processBuilder( final String... args ) {
156
    final var builder = new ProcessBuilder( args );
157
    builder.redirectErrorStream( true );
158
159
    return builder;
160
  }
161
162
  private static ProcessBuilder processBuilder(
163
    final File file, final String... s ) {
164
    return processBuilder( toArray( file.getAbsolutePath(), s ) );
165
  }
166
167
  private static ProcessBuilder processBuilder(
168
    final Path path, final String... s ) {
169
    return processBuilder( path.toFile(), s );
170
  }
171
172
  /**
173
   * Merges two arrays into a single array.
174
   *
175
   * @param first  The first array to merge before the second array.
176
   * @param second The second array to merge after the first array.
177
   * @param <T>    The type of arrays to merge.
178
   * @return The merged arrays, with the first array elements preceding the
179
   * second array's elements.
180
   */
181
  private static <T> T[] toArray( final T[] first, final T[] second ) {
182
    assert first != null;
183
    assert second != null;
184
    assert first.length > 0;
185
    assert second.length > 0;
186
187
    final var merged = copyOf( first, first.length + second.length );
188
    arraycopy( second, 0, merged, first.length, second.length );
189
    return merged;
190
  }
191
192
  /**
193
   * Convenience method to merge a single string with an array of strings.
194
   *
195
   * @param first  The first item to prepend to the secondary items.
196
   * @param second The second item to combine with the first item.
197
   * @return A new array with the first element at index 0 and the second
198
   * elements starting at index 1.
199
   */
200
  private static String[] toArray( final String first, String... second ) {
201
    assert first != null;
202
    assert second != null;
203
    assert second.length > 0;
204
205
    return toArray( new String[]{first}, second );
206
  }
207
}
1208
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 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.installer;
3
4
import com.keenwrite.events.ExportFailedEvent;
5
import com.keenwrite.preferences.AppKeys;
6
import com.keenwrite.preferences.Workspace;
7
import com.keenwrite.typesetting.installer.panes.*;
8
import org.controlsfx.dialog.Wizard;
9
import org.greenrobot.eventbus.Subscribe;
10
11
import java.util.LinkedList;
12
13
import static com.keenwrite.Messages.get;
14
import static com.keenwrite.events.Bus.register;
15
import static org.apache.commons.lang3.SystemUtils.*;
16
17
/**
18
 * Responsible for installing the typesetting system and all its requirements.
19
 */
20
public final class TypesetterInstaller {
21
  private final Workspace mWorkspace;
22
23
  /**
24
   * Registers for the {@link ExportFailedEvent}, which, when received,
25
   * indicates that the typesetting software must be installed.
26
   *
27
   * @param workspace To set {@link AppKeys#KEY_TYPESET_CONTEXT_THEMES_PATH} via
28
   *                  {@link TypesetterThemesDownloadPane}.
29
   */
30
  public TypesetterInstaller( final Workspace workspace ) {
31
    assert workspace != null;
32
33
    mWorkspace = workspace;
34
35
    register( this );
36
  }
37
38
  @Subscribe
39
  @SuppressWarnings( "unused" )
40
  public void handle( final ExportFailedEvent failedEvent ) {
41
    final var wizard = wizard();
42
43
    wizard.showAndWait();
44
  }
45
46
  private Wizard wizard() {
47
    final var title = get( "Wizard.typesetter.all.1.install.title" );
48
    final var wizard = new Wizard( this, title );
49
    final var wizardFlow = wizardFlow();
50
51
    wizard.setFlow( wizardFlow );
52
53
    return wizard;
54
  }
55
56
  private Wizard.Flow wizardFlow() {
57
    final var panels = wizardPanes();
58
    return new Wizard.LinearFlow( panels );
59
  }
60
61
  private InstallerPane[] wizardPanes() {
62
    final var panes = new LinkedList<InstallerPane>();
63
64
    // STEP 1: Introduction panel (all)
65
    panes.add( new IntroductionPane() );
66
67
    if( IS_OS_WINDOWS ) {
68
      // STEP 2 a: Download container (Windows)
69
      panes.add( new WindowsManagerDownloadPane() );
70
      // STEP 2 b: Install container (Windows)
71
      panes.add( new WindowsManagerInstallPane() );
72
    }
73
    else if( IS_OS_UNIX ) {
74
      // STEP 2: Install container (Unix)
75
      panes.add( new UnixManagerInstallPane() );
76
    }
77
    else {
78
      // STEP 2: Install container (other)
79
      panes.add( new UniversalManagerInstallPane() );
80
    }
81
82
    if( !IS_OS_LINUX ) {
83
      // STEP 3: Initialize container (all except Linux)
84
      panes.add( new ManagerInitializationPane() );
85
    }
86
87
    // STEP 4: Install typesetter container image (all)
88
    panes.add( new TypesetterImageDownloadPane() );
89
90
    // STEP 5: Download and install typesetter themes (all)
91
    panes.add( new TypesetterThemesDownloadPane( mWorkspace ) );
92
93
    return panes.toArray( InstallerPane[]::new );
94
  }
95
}
196
A src/main/java/com/keenwrite/typesetting/installer/WizardConstants.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.installer;
3
4
/**
5
 * Provides common constants across all panes.
6
 */
7
public class WizardConstants {
8
9
10
  private WizardConstants() { }
11
}
112
A src/main/java/com/keenwrite/typesetting/installer/panes/AbstractDownloadPane.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.installer.panes;
3
4
import com.keenwrite.io.SysFile;
5
import javafx.collections.ObservableMap;
6
import javafx.concurrent.Task;
7
import javafx.scene.control.Label;
8
import javafx.scene.layout.BorderPane;
9
import org.controlsfx.dialog.Wizard;
10
11
import java.io.File;
12
import java.net.URI;
13
14
import static com.keenwrite.Bootstrap.USER_DATA_DIR;
15
import static com.keenwrite.Messages.get;
16
import static com.keenwrite.Messages.getUri;
17
18
/**
19
 * Responsible for asynchronous downloads.
20
 */
21
public abstract class AbstractDownloadPane extends InstallerPane {
22
  private static final String STATUS = ".status";
23
24
  private final Label mStatus;
25
  private final File mTarget;
26
  private final String mFilename;
27
  private final URI mUri;
28
29
  public AbstractDownloadPane() {
30
    mUri = getUri( getPrefix() + ".download.link.url" );
31
    mFilename = toFilename( mUri );
32
    final var directory = USER_DATA_DIR;
33
    mTarget = directory.resolve( mFilename ).toFile();
34
    final var source = labelf( getPrefix() + ".paths", mFilename, directory );
35
    mStatus = labelf( getPrefix() + STATUS + ".progress", 0, 0 );
36
37
    final var border = new BorderPane();
38
    border.setTop( source );
39
    border.setCenter( spacer() );
40
    border.setBottom( mStatus );
41
42
    setContent( border );
43
  }
44
45
  @Override
46
  public void onEnteringPage( final Wizard wizard ) {
47
    disableNext( true );
48
49
    final var threadName = getClass().getCanonicalName();
50
    final var properties = wizard.getProperties();
51
    final var thread = properties.get( threadName );
52
53
    if( thread instanceof Task<?> downloader && downloader.isRunning() ) {
54
      return;
55
    }
56
57
    updateProperties( properties );
58
59
    final var target = getTarget();
60
    final var sysFile = new SysFile( target );
61
    final var checksum = getChecksum();
62
63
    if( sysFile.exists() ) {
64
      final var checksumOk = sysFile.isChecksum( checksum );
65
      final var suffix = checksumOk ? ".ok" : ".no";
66
67
      updateStatus( STATUS + ".checksum" + suffix, mFilename );
68
      disableNext( !checksumOk );
69
    }
70
    else {
71
      final var task = downloadAsync( mUri, target, ( progress, bytes ) -> {
72
        final var suffix = progress < 0 ? ".bytes" : ".progress";
73
74
        updateStatus( STATUS + suffix, progress, bytes );
75
      } );
76
77
      properties.put( threadName, task );
78
79
      task.setOnSucceeded( e -> onDownloadSucceeded( threadName, properties ) );
80
      task.setOnFailed( e -> onDownloadFailed( threadName, properties ) );
81
      task.setOnCancelled( e -> onDownloadFailed( threadName, properties ) );
82
    }
83
  }
84
85
  protected void updateProperties(
86
    final ObservableMap<Object, Object> properties ) {
87
  }
88
89
  @Override
90
  protected String getHeaderKey() {
91
    return getPrefix() + ".header";
92
  }
93
94
  protected File getTarget() {
95
    return mTarget;
96
  }
97
98
  protected abstract String getChecksum();
99
100
  protected abstract String getPrefix();
101
102
  protected void onDownloadSucceeded(
103
    final String threadName, final ObservableMap<Object, Object> properties ) {
104
    updateStatus( STATUS + ".success" );
105
    properties.remove( threadName );
106
    disableNext( false );
107
  }
108
109
  protected void onDownloadFailed(
110
    final String threadName, final ObservableMap<Object, Object> properties ) {
111
    updateStatus( STATUS + ".failure" );
112
    properties.remove( threadName );
113
  }
114
115
  protected void updateStatus( final String suffix, final Object... args ) {
116
    update( mStatus, get( getPrefix() + suffix, args ) );
117
  }
118
119
  protected void deleteTarget() {
120
    final var ignored = getTarget().delete();
121
  }
122
}
1123
A src/main/java/com/keenwrite/typesetting/installer/panes/InstallerPane.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.installer.panes;
3
4
import com.keenwrite.events.HyperlinkOpenEvent;
5
import com.keenwrite.io.downloads.DownloadManager;
6
import com.keenwrite.io.downloads.DownloadManager.ProgressListener;
7
import com.keenwrite.typesetting.containerization.ContainerManager;
8
import com.keenwrite.typesetting.containerization.Podman;
9
import javafx.animation.Animation;
10
import javafx.animation.RotateTransition;
11
import javafx.concurrent.Task;
12
import javafx.geometry.Insets;
13
import javafx.scene.Node;
14
import javafx.scene.control.*;
15
import javafx.scene.image.ImageView;
16
import javafx.scene.layout.BorderPane;
17
import javafx.scene.layout.FlowPane;
18
import javafx.scene.layout.Pane;
19
import org.controlsfx.dialog.Wizard;
20
import org.controlsfx.dialog.WizardPane;
21
22
import java.io.File;
23
import java.io.FileOutputStream;
24
import java.net.URI;
25
import java.nio.file.Paths;
26
import java.util.concurrent.Callable;
27
28
import static com.keenwrite.Messages.get;
29
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
30
import static java.lang.System.lineSeparator;
31
import static javafx.animation.Interpolator.LINEAR;
32
import static javafx.application.Platform.runLater;
33
import static javafx.scene.control.ButtonBar.ButtonData.NEXT_FORWARD;
34
import static javafx.scene.control.ContentDisplay.RIGHT;
35
import static javafx.util.Duration.seconds;
36
37
/**
38
 * Responsible for creating a {@link WizardPane} with a common header for all
39
 * subclasses.
40
 */
41
public abstract class InstallerPane extends WizardPane {
42
  /**
43
   * Unique key name to store the animation object so that it can be stopped.
44
   */
45
  private static final String PROP_ROTATION = "Wizard.typesetter.next.animate";
46
47
  /**
48
   * Defines amount of spacing between the installer's UI widgets, in pixels.
49
   */
50
  static final int PAD = 10;
51
52
  private static final double HEADER_FONT_SCALE = 1.25;
53
54
  public InstallerPane() {
55
    setHeader( createHeader() );
56
  }
57
58
  /**
59
   * When leaving the page, stop the animation. This is idempotent.
60
   *
61
   * @param wizard The wizard controlling the installer steps.
62
   */
63
  @Override
64
  public void onExitingPage( final Wizard wizard ) {
65
    super.onExitingPage( wizard );
66
    runLater( () -> stopAnimation( getNextButton() ) );
67
  }
68
69
  /**
70
   * Returns the property bundle key representing the dialog box title.
71
   */
72
  protected abstract String getHeaderKey();
73
74
  private BorderPane createHeader() {
75
    final var headerLabel = label( getHeaderKey() );
76
    headerLabel.setScaleX( HEADER_FONT_SCALE );
77
    headerLabel.setScaleY( HEADER_FONT_SCALE );
78
79
    final var separator = new Separator();
80
    separator.setPadding( new Insets( PAD, 0, 0, 0 ) );
81
82
    final var header = new BorderPane();
83
    header.setCenter( headerLabel );
84
    header.setRight( new ImageView( ICON_DIALOG ) );
85
    header.setBottom( separator );
86
    header.setPadding( new Insets( PAD, PAD, 0, PAD ) );
87
88
    return header;
89
  }
90
91
  /**
92
   * Disables the "Next" button during the installer. Normally disabling UI
93
   * elements is an anti-pattern (along with modal dialogs); however, in this
94
   * case, installation cannot proceed until each step is successfully
95
   * completed. Further, there may be "misleading" success messages shown
96
   * in the output panel, which the user may take as the step being complete.
97
   *
98
   * @param disable Set to {@code true} to disable the button.
99
   */
100
  void disableNext( final boolean disable ) {
101
    runLater( () -> {
102
      final var button = getNextButton();
103
104
      button.setDisable( disable );
105
106
      if( disable ) {
107
        startAnimation( button );
108
      }
109
      else {
110
        stopAnimation( button );
111
      }
112
    } );
113
  }
114
115
  /**
116
   * Returns the {@link Button} for advancing the wizard to the next pane.
117
   *
118
   * @return The Next button, if present, otherwise a new {@link Button}
119
   * instance so that API calls will succeed, despite not affecting the UI.
120
   */
121
  private Button getNextButton() {
122
    for( final var buttonType : getButtonTypes() ) {
123
      final var buttonData = buttonType.getButtonData();
124
125
      if( buttonData.equals( NEXT_FORWARD ) &&
126
        lookupButton( buttonType ) instanceof Button button ) {
127
        return button;
128
      }
129
    }
130
131
    // If there's no Next button, return a fake button.
132
    return new Button();
133
  }
134
135
  private void startAnimation( final Button button ) {
136
    // Create an image that is slightly taller than the button's font.
137
    final var graphic = new ImageView( ICON_DIALOG );
138
    graphic.setFitHeight( button.getFont().getSize() + 2 );
139
    graphic.setPreserveRatio( true );
140
    graphic.setSmooth( true );
141
142
    button.setGraphic( graphic );
143
    button.setGraphicTextGap( PAD );
144
    button.setContentDisplay( RIGHT );
145
146
    final var rotation = new RotateTransition( seconds( 1 ), graphic );
147
    getProperties().put( PROP_ROTATION, rotation );
148
149
    rotation.setCycleCount( Animation.INDEFINITE );
150
    rotation.setByAngle( 360 );
151
    rotation.setInterpolator( LINEAR );
152
    rotation.play();
153
  }
154
155
  private void stopAnimation( final Button button ) {
156
    final var animation = getProperties().get( PROP_ROTATION );
157
158
    if( animation instanceof RotateTransition rotation ) {
159
      rotation.stop();
160
      button.setGraphic( null );
161
      getProperties().remove( PROP_ROTATION );
162
    }
163
  }
164
165
  static TitledPane titledPane( final String title, final Node child ) {
166
    final var pane = new TitledPane( title, child );
167
    pane.setAnimated( false );
168
    pane.setCollapsible( false );
169
    pane.setExpanded( true );
170
171
    return pane;
172
  }
173
174
  static TextArea textArea( final int rows, final int cols ) {
175
    final var textarea = new TextArea();
176
    textarea.setEditable( false );
177
    textarea.setWrapText( true );
178
    textarea.setPrefRowCount( rows );
179
    textarea.setPrefColumnCount( cols );
180
181
    return textarea;
182
  }
183
184
  static Label label( final String key ) {
185
    return new Label( get( key ) );
186
  }
187
188
  /**
189
   * Like printf for labels.
190
   *
191
   * @param key    The property key to look up.
192
   * @param values The values to insert at the placeholders.
193
   * @return The formatted text with values replaced.
194
   */
195
  @SuppressWarnings( "SpellCheckingInspection" )
196
  static Label labelf( final String key, final Object... values ) {
197
    return new Label( get( key, values ) );
198
  }
199
200
  @SuppressWarnings( "SameParameterValue" )
201
  static Button button( final String key ) {
202
    return new Button( get( key ) );
203
  }
204
205
  static Node flowPane( final Node... nodes ) {
206
    return new FlowPane( nodes );
207
  }
208
209
  /**
210
   * Provides vertical spacing between {@link Node}s.
211
   *
212
   * @return A new empty vertical gap widget.
213
   */
214
  static Node spacer() {
215
    final var spacer = new Pane();
216
    spacer.setPadding( new Insets( PAD, 0, 0, 0 ) );
217
218
    return spacer;
219
  }
220
221
  static Hyperlink hyperlink( final String prefix ) {
222
    final var label = get( prefix + ".lbl" );
223
    final var url = get( prefix + ".url" );
224
    final var link = new Hyperlink( label );
225
226
    link.setOnAction( e -> browse( url ) );
227
    link.setTooltip( new Tooltip( url ) );
228
229
    return link;
230
  }
231
232
  /**
233
   * Opens a browser window off of the JavaFX main execution thread. This
234
   * is necessary so that the links open immediately, instead of being blocked
235
   * by any modal dialog (i.e., the {@link Wizard} instance).
236
   *
237
   * @param property The property key name associated with a hyperlink URL.
238
   */
239
  static void browse( final String property ) {
240
    final var url = get( property );
241
    final var task = createTask( () -> {
242
      HyperlinkOpenEvent.fire( url );
243
      return null;
244
    } );
245
    final var thread = createThread( task );
246
247
    thread.start();
248
  }
249
250
  static <T> Task<T> createTask( final Callable<T> callable ) {
251
    return new Task<>() {
252
      @Override
253
      protected T call() throws Exception {
254
        return callable.call();
255
      }
256
    };
257
  }
258
259
  static <T> Thread createThread( final Task<T> task ) {
260
    final var thread = new Thread( task );
261
    thread.setDaemon( true );
262
    return thread;
263
  }
264
265
  /**
266
   * Creates a container that can have its standard output read as an input
267
   * stream that's piped directly to a {@link TextArea}.
268
   *
269
   * @return An object that can perform tasks against a container.
270
   */
271
  static ContainerManager createContainer() {
272
    return new Podman();
273
  }
274
275
  static void update( final Label node, final String text ) {
276
    runLater( () -> node.setText( text ) );
277
  }
278
279
  static void append( final TextArea node, final String text ) {
280
    runLater( () -> {
281
      node.appendText( text );
282
      node.appendText( lineSeparator() );
283
    } );
284
  }
285
286
  /**
287
   * Downloads a resource to a local file in a separate {@link Thread}.
288
   *
289
   * @param uri      The resource to download.
290
   * @param file     The destination mTarget for the resource.
291
   * @param listener Receives updates as the download proceeds.
292
   */
293
  static Task<Void> downloadAsync(
294
    final URI uri,
295
    final File file,
296
    final ProgressListener listener ) {
297
    final Task<Void> task = createTask( () -> {
298
      try( final var token = DownloadManager.open( uri ) ) {
299
        final var output = new FileOutputStream( file );
300
        final var downloader = token.download( output, listener );
301
302
        downloader.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 Paths.get( uri.getPath() ).toFile().getName();
314
  }
315
}
1316
A src/main/java/com/keenwrite/typesetting/installer/panes/IntroductionPane.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.installer.panes;
3
4
/**
5
 * Responsible for informing the user as to what will happen next.
6
 */
7
public final class IntroductionPane extends InstallerPane {
8
  private static final String PREFIX = "Wizard.typesetter.all.1.install";
9
10
  public IntroductionPane() {
11
    setContent( flowPane(
12
      hyperlink( PREFIX + ".about.container.link" ),
13
      label( PREFIX + ".about.text.1" ),
14
      hyperlink( PREFIX + ".about.typesetter.link" ),
15
      label( PREFIX + ".about.text.2" )
16
    ) );
17
  }
18
19
  @Override
20
  protected String getHeaderKey() {
21
    return PREFIX + ".header";
22
  }
23
}
124
A src/main/java/com/keenwrite/typesetting/installer/panes/ManagerInitializationPane.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.installer.panes;
3
4
import com.keenwrite.typesetting.containerization.ContainerManager;
5
6
/**
7
 * Responsible for initializing the container manager on all platforms except
8
 * for Linux.
9
 */
10
public final class ManagerInitializationPane extends ManagerOutputPane {
11
12
  private static final String PREFIX =
13
    "Wizard.typesetter.all.3.install.container";
14
15
  public ManagerInitializationPane() {
16
    super(
17
      PREFIX + ".correct",
18
      PREFIX + ".missing",
19
      ContainerManager::start,
20
      35
21
    );
22
  }
23
24
  @Override
25
  public String getHeaderKey() {
26
    return PREFIX + ".header";
27
  }
28
}
129
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 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.installer.panes;
3
4
import static com.keenwrite.typesetting.containerization.Podman.CONTAINER_NAME;
5
6
/**
7
 * Responsible for installing the typesetter's image via the container manager.
8
 */
9
public final class TypesetterImageDownloadPane extends ManagerOutputPane {
10
  private static final String PREFIX =
11
    "Wizard.typesetter.all.4.download.image";
12
13
  public TypesetterImageDownloadPane() {
14
    super(
15
      PREFIX + ".correct",
16
      PREFIX + ".missing",
17
      (container, processor) -> container.pull( processor, CONTAINER_NAME ),
18
      45
19
    );
20
  }
21
22
  @Override
23
  public String getHeaderKey() {
24
    return PREFIX + ".header";
25
  }
26
}
127
A src/main/java/com/keenwrite/typesetting/installer/panes/TypesetterThemesDownloadPane.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.installer.panes;
3
4
import com.keenwrite.io.UserDataDir;
5
import com.keenwrite.io.Zip;
6
import com.keenwrite.preferences.Workspace;
7
import javafx.collections.ObservableMap;
8
import org.controlsfx.dialog.Wizard;
9
10
import java.io.File;
11
import java.io.IOException;
12
13
import static com.keenwrite.Messages.get;
14
import static com.keenwrite.events.StatusEvent.clue;
15
import static com.keenwrite.preferences.AppKeys.KEY_TYPESET_CONTEXT_THEMES_PATH;
16
17
/**
18
 * Responsible for downloading themes into the application's data directory.
19
 * The data directory differs between platforms, which is handled
20
 * transparently by the {@link UserDataDir} class.
21
 */
22
public class TypesetterThemesDownloadPane extends AbstractDownloadPane {
23
  private static final String PREFIX =
24
    "Wizard.typesetter.all.5.download.themes";
25
26
  private final Workspace mWorkspace;
27
28
  public TypesetterThemesDownloadPane( final Workspace workspace ) {
29
    assert workspace != null;
30
    mWorkspace = workspace;
31
  }
32
33
  @Override
34
  public void onEnteringPage( final Wizard wizard ) {
35
    // Delete the target themes file to force re-download so that unzipping
36
    // the file takes place. This side-steps checksum validation, which would
37
    // be best implemented after downloading.
38
    deleteTarget();
39
    super.onEnteringPage( wizard );
40
  }
41
42
  @Override
43
  protected void onDownloadSucceeded(
44
    final String threadName, final ObservableMap<Object, Object> properties ) {
45
    super.onDownloadSucceeded( threadName, properties );
46
47
    try {
48
      process( getTarget() );
49
    } catch( final Exception ex ) {
50
      clue( ex );
51
    }
52
  }
53
54
  private void process( final File target ) throws IOException {
55
    Zip.extract( target.toPath() );
56
57
    // Replace the default themes directory with the downloaded version.
58
    final var root = Zip.root( target.toPath() ).toFile();
59
60
    // Make sure the typesetter will know where to find the themes.
61
    mWorkspace.fileProperty( KEY_TYPESET_CONTEXT_THEMES_PATH ).set( root );
62
    mWorkspace.save();
63
64
    // The themes pack is no longer needed.
65
    deleteTarget();
66
  }
67
68
  @Override
69
  protected String getPrefix() {
70
    return PREFIX;
71
  }
72
73
  @Override
74
  protected String getChecksum() {
75
    return get( "Wizard.typesetter.themes.checksum" );
76
  }
77
}
178
A src/main/java/com/keenwrite/typesetting/installer/panes/UniversalManagerInstallPane.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.installer.panes;
3
4
/**
5
 * Responsible for installing the container manager for any operating system
6
 * that was not explicitly detected.
7
 */
8
public final class UniversalManagerInstallPane extends InstallerPane {
9
  private static final String PREFIX =
10
    "Wizard.typesetter.all.2.install.container";
11
12
  public UniversalManagerInstallPane() { }
13
14
  @Override
15
  protected String getHeaderKey() {
16
    return PREFIX + ".header";
17
  }
18
}
119
A src/main/java/com/keenwrite/typesetting/installer/panes/UnixManagerInstallPane.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.installer.panes;
3
4
import com.keenwrite.ui.clipboard.Clipboard;
5
import javafx.geometry.Insets;
6
import javafx.scene.Node;
7
import javafx.scene.control.ButtonBar;
8
import javafx.scene.control.ComboBox;
9
import javafx.scene.control.TextArea;
10
import javafx.scene.layout.BorderPane;
11
import javafx.scene.layout.HBox;
12
import javafx.scene.layout.VBox;
13
import org.jetbrains.annotations.NotNull;
14
15
import static com.keenwrite.Messages.get;
16
import static com.keenwrite.Messages.getInt;
17
import static java.lang.String.format;
18
import static org.apache.commons.lang3.SystemUtils.IS_OS_MAC;
19
20
public final class UnixManagerInstallPane extends InstallerPane {
21
  private static final String PREFIX =
22
    "Wizard.typesetter.unix.2.install.container";
23
24
  private final TextArea mCommands = textArea( 2, 40 );
25
26
  public UnixManagerInstallPane() {
27
    final var titledPane = titledPane( "Run", mCommands );
28
    final var comboBox = createUnixOsCommandMap();
29
    final var selection = comboBox.getSelectionModel();
30
    selection
31
      .selectedItemProperty()
32
      .addListener( ( c, o, n ) -> mCommands.setText( n.command() ) );
33
34
    // Auto-select if running on macOS.
35
    if( IS_OS_MAC ) {
36
      final var items = comboBox.getItems();
37
38
      for( final var item : items ) {
39
        if( "macOS".equalsIgnoreCase( item.name ) ) {
40
          selection.select( item );
41
          break;
42
        }
43
      }
44
    }
45
    else {
46
      selection.select( 0 );
47
    }
48
49
    final var distro = label( PREFIX + ".os" );
50
    distro.setText( distro.getText() + ":" );
51
    distro.setPadding( new Insets( PAD / 2.0, PAD, 0, 0 ) );
52
53
    final var hbox = new HBox();
54
    hbox.getChildren().add( distro );
55
    hbox.getChildren().add( comboBox );
56
    hbox.setPadding( new Insets( 0, 0, PAD, 0 ) );
57
58
    final var stepsPane = new VBox();
59
    final var steps = stepsPane.getChildren();
60
    steps.add( label( PREFIX + ".step.0" ) );
61
    steps.add( spacer() );
62
    steps.add( label( PREFIX + ".step.1" ) );
63
    steps.add( label( PREFIX + ".step.2" ) );
64
    steps.add( label( PREFIX + ".step.3" ) );
65
    steps.add( label( PREFIX + ".step.4" ) );
66
    steps.add( spacer() );
67
68
    steps.add( flowPane(
69
      label( PREFIX + ".details.prefix" ),
70
      hyperlink( PREFIX + ".details.link" ),
71
      label( PREFIX + ".details.suffix" )
72
    ) );
73
    steps.add( spacer() );
74
75
    final var border = new BorderPane();
76
    border.setTop( stepsPane );
77
    border.setCenter( hbox );
78
    border.setBottom( titledPane );
79
80
    setContent( border );
81
  }
82
83
  @Override
84
  public Node createButtonBar() {
85
    final var node = super.createButtonBar();
86
    final var layout = new BorderPane();
87
    final var copyButton = button( PREFIX + ".copy.began" );
88
89
    // Change the label to indicate clipboard is updated.
90
    copyButton.setOnAction( event -> {
91
      Clipboard.write( mCommands.getText() );
92
      copyButton.setText( get( PREFIX + ".copy.ended" ) );
93
    } );
94
95
    if( node instanceof ButtonBar buttonBar ) {
96
      copyButton.setMinWidth( buttonBar.getButtonMinWidth() );
97
    }
98
99
    layout.setPadding( new Insets( PAD, PAD, PAD, PAD ) );
100
    layout.setLeft( copyButton );
101
    layout.setRight( node );
102
103
    return layout;
104
  }
105
106
  @Override
107
  protected String getHeaderKey() {
108
    return PREFIX + ".header";
109
  }
110
111
  private record UnixOsCommand( String name, String command )
112
    implements Comparable<UnixOsCommand> {
113
    @Override
114
    public int compareTo(
115
      final @NotNull UnixOsCommand other ) {
116
      return toString().compareToIgnoreCase( other.toString() );
117
    }
118
119
    @Override
120
    public String toString() {
121
      return name;
122
    }
123
  }
124
125
  /**
126
   * Creates a collection of *nix distributions mapped to instructions for users
127
   * to run in a terminal.
128
   *
129
   * @return A map of *nix to instructions.
130
   */
131
  private static ComboBox<UnixOsCommand> createUnixOsCommandMap() {
132
    new ComboBox<UnixOsCommand>();
133
    final var comboBox = new ComboBox<UnixOsCommand>();
134
    final var items = comboBox.getItems();
135
    final var prefix = PREFIX + ".command";
136
    final var distros = getInt( prefix + ".distros", 14 );
137
138
    for( int i = 1; i <= distros; i++ ) {
139
      final var suffix = format( ".%02d", i );
140
      final var name = get( prefix + ".os.name" + suffix );
141
      final var command = get( prefix + ".os.text" + suffix );
142
143
      items.add( new UnixOsCommand( name, command ) );
144
    }
145
146
    items.sort( UnixOsCommand::compareTo );
147
148
    return comboBox;
149
  }
150
}
1151
A src/main/java/com/keenwrite/typesetting/installer/panes/WindowsManagerDownloadPane.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.installer.panes;
3
4
import javafx.collections.ObservableMap;
5
6
import static com.keenwrite.Messages.get;
7
import static com.keenwrite.typesetting.installer.panes.WindowsManagerInstallPane.WIN_BIN;
8
9
/**
10
 * Responsible for downloading the container manager software on Windows.
11
 */
12
public final class WindowsManagerDownloadPane extends AbstractDownloadPane {
13
  private static final String PREFIX =
14
    "Wizard.typesetter.win.2.download.container";
15
16
  @Override
17
  protected void updateProperties(
18
    final ObservableMap<Object, Object> properties ) {
19
    properties.put( WIN_BIN, getTarget() );
20
  }
21
22
  @Override
23
  protected String getPrefix() {
24
    return PREFIX;
25
  }
26
27
  @Override
28
  protected String getChecksum() {
29
    return get( "Wizard.typesetter.container.checksum" );
30
  }
31
}
132
A src/main/java/com/keenwrite/typesetting/installer/panes/WindowsManagerInstallPane.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting.installer.panes;
3
4
import com.keenwrite.typesetting.containerization.ContainerManager;
5
import javafx.scene.control.TextArea;
6
import javafx.scene.layout.BorderPane;
7
import javafx.scene.layout.VBox;
8
import org.controlsfx.dialog.Wizard;
9
10
import java.io.File;
11
12
import static com.keenwrite.Messages.get;
13
14
/**
15
 * Responsible for installing the container manager on Windows.
16
 */
17
public final class WindowsManagerInstallPane extends InstallerPane {
18
  /**
19
   * Property for the installation thread to help with reentrancy.
20
   */
21
  private static final String WIN_INSTALLER = "windows.container.installer";
22
23
  /**
24
   * Shared property to track name of container manager binary file.
25
   */
26
  static final String WIN_BIN = "windows.container.binary";
27
28
  private static final String PREFIX =
29
    "Wizard.typesetter.win.2.install.container";
30
31
  private final ContainerManager mContainer;
32
  private final TextArea mCommands;
33
34
  public WindowsManagerInstallPane() {
35
    mCommands = textArea( 2, 55 );
36
37
    final var titledPane = titledPane( "Output", mCommands );
38
    append( mCommands, get( PREFIX + ".status.running" ) );
39
40
    final var stepsPane = new VBox();
41
    final var steps = stepsPane.getChildren();
42
    steps.add( label( PREFIX + ".step.0" ) );
43
    steps.add( spacer() );
44
    steps.add( label( PREFIX + ".step.1" ) );
45
    steps.add( label( PREFIX + ".step.2" ) );
46
    steps.add( label( PREFIX + ".step.3" ) );
47
    steps.add( spacer() );
48
    steps.add( titledPane );
49
50
    final var border = new BorderPane();
51
    border.setTop( stepsPane );
52
53
    mContainer = createContainer();
54
  }
55
56
  @Override
57
  public void onEnteringPage( final Wizard wizard ) {
58
    disableNext( true );
59
60
    // Pull the fully qualified installer path from the properties.
61
    final var properties = wizard.getProperties();
62
    final var thread = properties.get( WIN_INSTALLER );
63
64
    if( thread instanceof Thread installer && installer.isAlive() ) {
65
      return;
66
    }
67
68
    final var binary = properties.get( WIN_BIN );
69
    final var key = PREFIX + ".status";
70
71
    if( binary instanceof File exe ) {
72
      final var task = createTask( () -> {
73
        final var exit = mContainer.install( exe );
74
75
        // Remove the installer after installation is finished.
76
        properties.remove( thread );
77
78
        final var msg = exit == 0
79
          ? get( key + ".success" )
80
          : get( key + ".failure", exit );
81
82
        append( mCommands, msg );
83
        disableNext( exit != 0 );
84
85
        return null;
86
      } );
87
88
      final var installer = createThread( task );
89
      properties.put( WIN_INSTALLER, installer );
90
      installer.start();
91
    }
92
    else {
93
      append( mCommands, get( PREFIX + ".unknown", binary ) );
94
    }
95
  }
96
97
  @Override
98
  public String getHeaderKey() {
99
    return PREFIX + ".header";
100
  }
101
}
1102
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_svg", e -> actions.file_export_html_svg() ),
77
          addAction( "file.export.html_tex", e -> actions.file_export_html_tex() ),
78
          addAction( "file.export.xhtml_tex", e -> actions.file_export_xhtml_tex() )
79
        ),
80
      SEPARATOR,
81
      addAction( "file.exit", e -> actions.file_exit() )
82
    );
83
    // @formatter:on
84
  }
85
86
  @NotNull
87
  private static Menu createMenuEdit(
88
    final GuiCommands actions, final SeparatorAction SEPARATOR ) {
89
    return createMenu(
90
      get( "Main.menu.edit" ),
91
      SEPARATOR,
92
      addAction( "edit.undo", e -> actions.edit_undo() ),
93
      addAction( "edit.redo", e -> actions.edit_redo() ),
94
      SEPARATOR,
95
      addAction( "edit.cut", e -> actions.edit_cut() ),
96
      addAction( "edit.copy", e -> actions.edit_copy() ),
97
      addAction( "edit.paste", e -> actions.edit_paste() ),
98
      addAction( "edit.select_all", e -> actions.edit_select_all() ),
99
      SEPARATOR,
100
      addAction( "edit.find", e -> actions.edit_find() ),
101
      addAction( "edit.find_next", e -> actions.edit_find_next() ),
102
      addAction( "edit.find_prev", e -> actions.edit_find_prev() ),
103
      SEPARATOR,
104
      addAction( "edit.preferences", e -> actions.edit_preferences() )
105
    );
106
  }
107
108
  @NotNull
109
  private static Menu createMenuFormat( final GuiCommands actions ) {
110
    return createMenu(
111
      get( "Main.menu.format" ),
112
      addAction( "format.bold", e -> actions.format_bold() ),
113
      addAction( "format.italic", e -> actions.format_italic() ),
114
      addAction( "format.monospace", e -> actions.format_monospace() ),
115
      addAction( "format.superscript", e -> actions.format_superscript() ),
116
      addAction( "format.subscript", e -> actions.format_subscript() ),
117
      addAction( "format.strikethrough", e -> actions.format_strikethrough() )
118
    );
119
  }
120
121
  @NotNull
122
  private static Menu createMenuInsert(
123
    final GuiCommands actions,
124
    final SeparatorAction SEPARATOR ) {
125
    // @formatter:off
126
    return createMenu(
127
      get( "Main.menu.insert" ),
128
      addAction( "insert.blockquote", e -> actions.insert_blockquote() ),
129
      addAction( "insert.code", e -> actions.insert_code() ),
130
      addAction( "insert.fenced_code_block", e -> actions.insert_fenced_code_block() ),
131
      SEPARATOR,
132
      addAction( "insert.link", e -> actions.insert_link() ),
133
      addAction( "insert.image", e -> actions.insert_image() ),
134
      SEPARATOR,
135
      addAction( "insert.heading_1", e -> actions.insert_heading_1() ),
136
      addAction( "insert.heading_2", e -> actions.insert_heading_2() ),
137
      addAction( "insert.heading_3", e -> actions.insert_heading_3() ),
138
      SEPARATOR,
139
      addAction( "insert.unordered_list", e -> actions.insert_unordered_list() ),
140
      addAction( "insert.ordered_list", e -> actions.insert_ordered_list() ),
141
      addAction( "insert.horizontal_rule", e -> actions.insert_horizontal_rule() )
142
    );
143
    // @formatter:on
144
  }
145
146
  @NotNull
147
  private static Menu createMenuVariable(
148
    final GuiCommands actions, final SeparatorAction SEPARATOR ) {
149
    return createMenu(
150
      get( "Main.menu.definition" ),
151
      addAction( "definition.insert", e -> actions.definition_autoinsert() ),
152
      SEPARATOR,
153
      addAction( "definition.create", e -> actions.definition_create() ),
154
      addAction( "definition.rename", e -> actions.definition_rename() ),
155
      addAction( "definition.delete", e -> actions.definition_delete() )
156
    );
157
  }
158
159
  @NotNull
160
  private static Menu createMenuView(
161
    final GuiCommands actions, final SeparatorAction SEPARATOR ) {
162
    return createMenu(
163
      get( "Main.menu.view" ),
164
      addAction( "view.refresh", e -> actions.view_refresh() ),
165
      SEPARATOR,
166
      addAction( "view.preview", e -> actions.view_preview() ),
167
      addAction( "view.outline", e -> actions.view_outline() ),
168
      addAction( "view.statistics", e -> actions.view_statistics() ),
169
      addAction( "view.files", e -> actions.view_files() ),
170
      SEPARATOR,
171
      addAction( "view.menubar", e -> actions.view_menubar() ),
172
      addAction( "view.toolbar", e -> actions.view_toolbar() ),
173
      addAction( "view.statusbar", e -> actions.view_statusbar() ),
174
      SEPARATOR,
175
      addAction( "view.log", e -> actions.view_log() )
176
    );
177
  }
178
179
  @NotNull
180
  private static Menu createMenuHelp( final GuiCommands actions ) {
181
    return createMenu(
182
      get( "Main.menu.help" ),
183
      addAction( "help.about", e -> actions.help_about() )
184
    );
185
  }
186
187
  public static Node createToolBar() {
188
    final var SEPARATOR = new SeparatorAction();
189
190
    return createToolBar(
191
      getAction( "file.new" ),
192
      getAction( "file.open" ),
193
      getAction( "file.save" ),
194
      SEPARATOR,
195
      getAction( "file.export.pdf" ),
196
      SEPARATOR,
197
      getAction( "edit.undo" ),
198
      getAction( "edit.redo" ),
199
      getAction( "edit.cut" ),
200
      getAction( "edit.copy" ),
201
      getAction( "edit.paste" ),
202
      SEPARATOR,
203
      getAction( "format.bold" ),
204
      getAction( "format.italic" ),
205
      getAction( "format.superscript" ),
206
      getAction( "format.subscript" ),
207
      getAction( "insert.blockquote" ),
208
      getAction( "insert.code" ),
209
      getAction( "insert.fenced_code_block" ),
210
      SEPARATOR,
211
      getAction( "insert.link" ),
212
      getAction( "insert.image" ),
213
      SEPARATOR,
214
      getAction( "insert.heading_1" ),
215
      SEPARATOR,
216
      getAction( "insert.unordered_list" ),
217
      getAction( "insert.ordered_list" )
218
    );
219
  }
220
221
  public static StatusBar createStatusBar() {
222
    return new EventedStatusBar();
223
  }
224
225
  /**
226
   * Adds a new action to the list of actions.
227
   *
228
   * @param key     The name of the action to register in {@link #sMap}.
229
   * @param handler Performs the action upon request.
230
   * @return The newly registered action.
231
   */
232
  private static Action addAction(
233
    final String key, final EventHandler<ActionEvent> handler ) {
234
    assert key != null;
235
    assert handler != null;
236
237
    final var action = Action
238
      .builder()
239
      .setId( key )
240
      .setHandler( handler )
241
      .build();
242
243
    sMap.put( key, action );
244
245
    return action;
246
  }
247
248
  private static Action getAction( final String key ) {
249
    return sMap.get( key );
250
  }
251
252
  public static Menu createMenu(
253
    final String text, final MenuAction... actions ) {
254
    return new Menu( text, null, createMenuItems( actions ) );
255
  }
256
257
  public static MenuItem[] createMenuItems( final MenuAction... actions ) {
258
    final var menuItems = new MenuItem[ actions.length ];
259
260
    for( var i = 0; i < actions.length; i++ ) {
261
      menuItems[ i ] = actions[ i ].createMenuItem();
262
    }
263
264
    return menuItems;
265
  }
266
267
  private static ToolBar createToolBar( final MenuAction... actions ) {
268
    return new ToolBar( createToolBarButtons( actions ) );
269
  }
270
271
  private static Node[] createToolBarButtons( final MenuAction... actions ) {
272
    final var len = actions.length;
273
    final var nodes = new Node[ len ];
274
275
    for( var i = 0; i < len; i++ ) {
276
      nodes[ i ] = actions[ i ].createToolBarNode();
277
    }
278
279
    return nodes;
280
  }
281
}
1282
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.editors.TextDefinition;
8
import com.keenwrite.editors.TextEditor;
9
import com.keenwrite.editors.markdown.HyperlinkModel;
10
import com.keenwrite.editors.markdown.LinkVisitor;
11
import com.keenwrite.events.CaretMovedEvent;
12
import com.keenwrite.events.ExportFailedEvent;
13
import com.keenwrite.preferences.Key;
14
import com.keenwrite.preferences.PreferencesController;
15
import com.keenwrite.preferences.Workspace;
16
import com.keenwrite.processors.markdown.MarkdownProcessor;
17
import com.keenwrite.search.SearchModel;
18
import com.keenwrite.typesetting.Typesetter;
19
import com.keenwrite.ui.controls.SearchBar;
20
import com.keenwrite.ui.dialogs.ExportDialog;
21
import com.keenwrite.ui.dialogs.ExportSettings;
22
import com.keenwrite.ui.dialogs.ImageDialog;
23
import com.keenwrite.ui.dialogs.LinkDialog;
24
import com.keenwrite.ui.explorer.FilePicker;
25
import com.keenwrite.ui.explorer.FilePickerFactory;
26
import com.keenwrite.ui.logging.LogView;
27
import com.keenwrite.util.AlphanumComparator;
28
import com.keenwrite.util.RangeValidator;
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.io.IOException;
39
import java.nio.file.Path;
40
import java.util.ArrayList;
41
import java.util.List;
42
import java.util.Optional;
43
import java.util.concurrent.atomic.AtomicInteger;
44
45
import static com.keenwrite.Bootstrap.*;
46
import static com.keenwrite.ExportFormat.*;
47
import static com.keenwrite.Messages.get;
48
import static com.keenwrite.constants.Constants.PDF_DEFAULT;
49
import static com.keenwrite.constants.Constants.USER_DIRECTORY;
50
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE;
51
import static com.keenwrite.events.StatusEvent.clue;
52
import static com.keenwrite.preferences.AppKeys.*;
53
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
54
import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType;
55
import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType.*;
56
import static com.keenwrite.util.FileWalker.walk;
57
import static java.lang.System.lineSeparator;
58
import static java.nio.file.Files.readString;
59
import static java.nio.file.Files.writeString;
60
import static javafx.application.Platform.runLater;
61
import static javafx.event.Event.fireEvent;
62
import static javafx.scene.control.Alert.AlertType.INFORMATION;
63
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
64
import static org.apache.commons.io.FilenameUtils.getExtension;
65
66
/**
67
 * Responsible for abstracting how functionality is mapped to the application.
68
 * This allows users to customize accelerator keys and will provide pluggable
69
 * functionality so that different text markup languages can change documents
70
 * using their respective syntax.
71
 */
72
public final class GuiCommands {
73
  private static final String STYLE_SEARCH = "search";
74
75
  /**
76
   * Sci-fi genres, which are can be longer than other genres, typically fall
77
   * below 150,000 words at 6 chars per word. This reduces re-allocations of
78
   * memory when concatenating files together when exporting novels.
79
   */
80
  private static final int DOCUMENT_LENGTH = 150_000 * 6;
81
82
  /**
83
   * When an action is executed, this is one of the recipients.
84
   */
85
  private final MainPane mMainPane;
86
87
  private final MainScene mMainScene;
88
89
  private final LogView mLogView;
90
91
  /**
92
   * Tracks finding text in the active document.
93
   */
94
  private final SearchModel mSearchModel;
95
96
  private boolean mCanTypeset;
97
98
  /**
99
   * A {@link Task} can only be run once, so wrap it in a {@link Service} to
100
   * allow re-launching the typesetting task repeatedly.
101
   */
102
  private Service<Path> mTypesetService;
103
104
  /**
105
   * Prevent a race-condition between checking to see if the typesetting task
106
   * is running and restarting the task itself.
107
   */
108
  private final Object mMutex = new Object();
109
110
  public GuiCommands( final MainScene scene, final MainPane pane ) {
111
    mMainScene = scene;
112
    mMainPane = pane;
113
    mLogView = new LogView();
114
    mSearchModel = new SearchModel();
115
    mSearchModel.matchOffsetProperty().addListener( ( c, o, n ) -> {
116
      final var editor = getActiveTextEditor();
117
118
      // Clear highlighted areas before highlighting a new region.
119
      if( o != null ) {
120
        editor.unstylize( STYLE_SEARCH );
121
      }
122
123
      if( n != null ) {
124
        editor.moveTo( n.getStart() );
125
        editor.stylize( n, STYLE_SEARCH );
126
      }
127
    } );
128
129
    // When the active text editor changes ...
130
    mMainPane.textEditorProperty().addListener(
131
      ( c, o, n ) -> {
132
        // ... update the haystack.
133
        mSearchModel.search( getActiveTextEditor().getText() );
134
135
        // ... update the status bar with the current caret position.
136
        if( n != null ) {
137
          final var w = getWorkspace();
138
          final var recentDoc = w.fileProperty( KEY_UI_RECENT_DOCUMENT );
139
140
          // ... preserve the most recent document.
141
          recentDoc.setValue( n.getFile() );
142
          CaretMovedEvent.fire( n.getCaret() );
143
        }
144
      }
145
    );
146
  }
147
148
  public void file_new() {
149
    getMainPane().newTextEditor();
150
  }
151
152
  public void file_open() {
153
    pickFiles( FILE_OPEN_MULTIPLE ).ifPresent( l -> getMainPane().open( l ) );
154
  }
155
156
  public void file_close() {
157
    getMainPane().close();
158
  }
159
160
  public void file_close_all() {
161
    getMainPane().closeAll();
162
  }
163
164
  public void file_save() {
165
    getMainPane().save();
166
  }
167
168
  public void file_save_as() {
169
    pickFiles( FILE_SAVE_AS ).ifPresent( l -> getMainPane().saveAs( l ) );
170
  }
171
172
  public void file_save_all() {
173
    getMainPane().saveAll();
174
  }
175
176
  /**
177
   * Converts the actively edited file in the given file format.
178
   *
179
   * @param format The destination file format.
180
   */
181
  private void file_export( final ExportFormat format ) {
182
    file_export( format, false );
183
  }
184
185
  /**
186
   * Converts one or more files into the given file format. If {@code dir}
187
   * is set to true, this will first append all files in the same directory
188
   * as the actively edited file.
189
   *
190
   * @param format The destination file format.
191
   * @param dir    Export all files in the actively edited file's directory.
192
   */
193
  private void file_export( final ExportFormat format, final boolean dir ) {
194
    final var editor = getMainPane().getTextEditor();
195
    final var exported = getWorkspace().fileProperty( KEY_UI_RECENT_EXPORT );
196
    final var exportParent = exported.get().toPath().getParent();
197
    final var editorParent = editor.getPath().getParent();
198
    final var userHomeParent = USER_DIRECTORY.toPath();
199
    final var exportPath = exportParent != null
200
      ? exportParent
201
      : editorParent != null
202
      ? editorParent
203
      : userHomeParent;
204
205
    final var filename = format.toExportFilename( editor.getPath() );
206
    final var selected = PDF_DEFAULT
207
      .getName()
208
      .equals( exported.get().getName() );
209
    final var selection = pickFile(
210
      selected
211
        ? filename
212
        : exported.get(),
213
      exportPath,
214
      FILE_EXPORT
215
    );
216
217
    selection.ifPresent( files -> file_export( editor, format, files, dir ) );
218
  }
219
220
  private void file_export(
221
    final TextEditor editor,
222
    final ExportFormat format,
223
    final List<File> files,
224
    final boolean dir ) {
225
    editor.save();
226
    final var main = getMainPane();
227
    final var exported = getWorkspace().fileProperty( KEY_UI_RECENT_EXPORT );
228
229
    final var sourceFile = files.get( 0 );
230
    final var sourcePath = sourceFile.toPath();
231
    final var document = dir ? append( editor ) : editor.getText();
232
    final var context = main.createProcessorContext( sourcePath, format );
233
234
    final var service = new Service<Path>() {
235
      @Override
236
      protected Task<Path> createTask() {
237
        final var task = new Task<Path>() {
238
          @Override
239
          protected Path call() throws Exception {
240
            final var chain = createProcessors( context );
241
            final var export = chain.apply( document );
242
243
            // Processors can export binary files. In such cases, processors
244
            // return null to prevent further processing.
245
            return export == null ? null : writeString( sourcePath, export );
246
          }
247
        };
248
249
        task.setOnSucceeded(
250
          e -> {
251
            // Remember the exported file name for next time.
252
            exported.setValue( sourceFile );
253
254
            final var result = task.getValue();
255
256
            // Binary formats must notify users of success independently.
257
            if( result != null ) {
258
              clue( "Main.status.export.success", result );
259
            }
260
          }
261
        );
262
263
        task.setOnFailed( e -> {
264
          final var ex = task.getException();
265
          clue( ex );
266
267
          if( ex instanceof TypeNotPresentException ) {
268
            fireExportFailedEvent();
269
          }
270
        } );
271
272
        return task;
273
      }
274
    };
275
276
    mTypesetService = service;
277
    typeset( service );
278
  }
279
280
  /**
281
   * @param dir {@code true} means to export all files in the active file
282
   *            editor's directory; {@code false} means to export only the
283
   *            actively edited file.
284
   */
285
  private void file_export_pdf( final boolean dir ) {
286
    final var workspace = getWorkspace();
287
    final var themes = workspace.getFile(
288
      KEY_TYPESET_CONTEXT_THEMES_PATH
289
    );
290
    final var theme = workspace.stringProperty(
291
      KEY_TYPESET_CONTEXT_THEME_SELECTION
292
    );
293
    final var chapters = workspace.stringProperty(
294
      KEY_TYPESET_CONTEXT_CHAPTERS
295
    );
296
    final var settings = ExportSettings
297
      .builder()
298
      .with( ExportSettings.Mutator::setTheme, theme )
299
      .with( ExportSettings.Mutator::setChapters, chapters )
300
      .build();
301
302
    // Don't re-validate the typesetter installation each time. If the
303
    // user mucks up the typesetter installation, it'll get caught the
304
    // next time the application is started. Don't use |= because it
305
    // won't short-circuit.
306
    mCanTypeset = mCanTypeset || Typesetter.canRun();
307
308
    if( mCanTypeset ) {
309
      // If the typesetter is installed, allow the user to select a theme. If
310
      // the themes aren't installed, a status message will appear.
311
      if( ExportDialog.choose( getWindow(), themes, settings, dir ) ) {
312
        file_export( APPLICATION_PDF, dir );
313
      }
314
    }
315
    else {
316
      fireExportFailedEvent();
317
    }
318
  }
319
320
  public void file_export_pdf() {
321
    file_export_pdf( false );
322
  }
323
324
  public void file_export_pdf_dir() {
325
    file_export_pdf( true );
326
  }
327
328
  public void file_export_repeat() {
329
    typeset( mTypesetService );
330
  }
331
332
  public void file_export_html_svg() {
333
    file_export( HTML_TEX_SVG );
334
  }
335
336
  public void file_export_html_tex() {
337
    file_export( HTML_TEX_DELIMITED );
338
  }
339
340
  public void file_export_xhtml_tex() {
341
    file_export( XHTML_TEX );
342
  }
343
344
  private void fireExportFailedEvent() {
345
    runLater( ExportFailedEvent::fire );
346
  }
347
348
  public void file_exit() {
349
    final var window = getWindow();
350
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
351
  }
352
353
  public void edit_undo() {
354
    getActiveTextEditor().undo();
355
  }
356
357
  public void edit_redo() {
358
    getActiveTextEditor().redo();
359
  }
360
361
  public void edit_cut() {
362
    getActiveTextEditor().cut();
363
  }
364
365
  public void edit_copy() {
366
    getActiveTextEditor().copy();
367
  }
368
369
  public void edit_paste() {
370
    getActiveTextEditor().paste();
371
  }
372
373
  public void edit_select_all() {
374
    getActiveTextEditor().selectAll();
375
  }
376
377
  public void edit_find() {
378
    final var nodes = getMainScene().getStatusBar().getLeftItems();
379
380
    if( nodes.isEmpty() ) {
381
      final var searchBar = new SearchBar();
382
383
      searchBar.matchIndexProperty().bind( mSearchModel.matchIndexProperty() );
384
      searchBar.matchCountProperty().bind( mSearchModel.matchCountProperty() );
385
386
      searchBar.setOnCancelAction( event -> {
387
        final var editor = getActiveTextEditor();
388
        nodes.remove( searchBar );
389
        editor.unstylize( STYLE_SEARCH );
390
        editor.getNode().requestFocus();
391
      } );
392
393
      searchBar.addInputListener( ( c, o, n ) -> {
394
        if( n != null && !n.isEmpty() ) {
395
          mSearchModel.search( n, getActiveTextEditor().getText() );
396
        }
397
      } );
398
399
      searchBar.setOnNextAction( event -> edit_find_next() );
400
      searchBar.setOnPrevAction( event -> edit_find_prev() );
401
402
      nodes.add( searchBar );
403
      searchBar.requestFocus();
404
    }
405
    else {
406
      nodes.clear();
407
    }
408
  }
409
410
  public void edit_find_next() {
411
    mSearchModel.advance();
412
  }
413
414
  public void edit_find_prev() {
415
    mSearchModel.retreat();
416
  }
417
418
  public void edit_preferences() {
419
    try {
420
      new PreferencesController( getWorkspace() ).show();
421
    } catch( final Exception ex ) {
422
      clue( ex );
423
    }
424
  }
425
426
  public void format_bold() {
427
    getActiveTextEditor().bold();
428
  }
429
430
  public void format_italic() {
431
    getActiveTextEditor().italic();
432
  }
433
434
  public void format_monospace() {
435
    getActiveTextEditor().monospace();
436
  }
437
438
  public void format_superscript() {
439
    getActiveTextEditor().superscript();
440
  }
441
442
  public void format_subscript() {
443
    getActiveTextEditor().subscript();
444
  }
445
446
  public void format_strikethrough() {
447
    getActiveTextEditor().strikethrough();
448
  }
449
450
  public void insert_blockquote() {
451
    getActiveTextEditor().blockquote();
452
  }
453
454
  public void insert_code() {
455
    getActiveTextEditor().code();
456
  }
457
458
  public void insert_fenced_code_block() {
459
    getActiveTextEditor().fencedCodeBlock();
460
  }
461
462
  public void insert_link() {
463
    insertObject( createLinkDialog() );
464
  }
465
466
  public void insert_image() {
467
    insertObject( createImageDialog() );
468
  }
469
470
  private void insertObject( final Dialog<String> dialog ) {
471
    final var textArea = getActiveTextEditor().getTextArea();
472
    dialog.showAndWait().ifPresent( textArea::replaceSelection );
473
  }
474
475
  private Dialog<String> createLinkDialog() {
476
    return new LinkDialog( getWindow(), createHyperlinkModel() );
477
  }
478
479
  private Dialog<String> createImageDialog() {
480
    final var path = getActiveTextEditor().getPath();
481
    final var parentDir = path.getParent();
482
    return new ImageDialog( getWindow(), parentDir );
483
  }
484
485
  /**
486
   * Returns one of: selected text, word under cursor, or parsed hyperlink from
487
   * the Markdown AST.
488
   *
489
   * @return An instance containing the link URL and display text.
490
   */
491
  private HyperlinkModel createHyperlinkModel() {
492
    final var context = getMainPane().createProcessorContext();
493
    final var editor = getActiveTextEditor();
494
    final var textArea = editor.getTextArea();
495
    final var selectedText = textArea.getSelectedText();
496
497
    // Convert current paragraph to Markdown nodes.
498
    final var mp = MarkdownProcessor.create( context );
499
    final var p = textArea.getCurrentParagraph();
500
    final var paragraph = textArea.getText( p );
501
    final var node = mp.toNode( paragraph );
502
    final var visitor = new LinkVisitor( textArea.getCaretColumn() );
503
    final var link = visitor.process( node );
504
505
    if( link != null ) {
506
      textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
507
    }
508
509
    return createHyperlinkModel( link, selectedText );
510
  }
511
512
  private HyperlinkModel createHyperlinkModel(
513
    final Link link, final String selection ) {
514
515
    return link == null
516
      ? new HyperlinkModel( selection, "https://localhost" )
517
      : new HyperlinkModel( link );
518
  }
519
520
  public void insert_heading_1() {
521
    insert_heading( 1 );
522
  }
523
524
  public void insert_heading_2() {
525
    insert_heading( 2 );
526
  }
527
528
  public void insert_heading_3() {
529
    insert_heading( 3 );
530
  }
531
532
  private void insert_heading( final int level ) {
533
    getActiveTextEditor().heading( level );
534
  }
535
536
  public void insert_unordered_list() {
537
    getActiveTextEditor().unorderedList();
538
  }
539
540
  public void insert_ordered_list() {
541
    getActiveTextEditor().orderedList();
542
  }
543
544
  public void insert_horizontal_rule() {
545
    getActiveTextEditor().horizontalRule();
546
  }
547
548
  public void definition_create() {
549
    getActiveTextDefinition().createDefinition();
550
  }
551
552
  public void definition_rename() {
553
    getActiveTextDefinition().renameDefinition();
554
  }
555
556
  public void definition_delete() {
557
    getActiveTextDefinition().deleteDefinitions();
558
  }
559
560
  public void definition_autoinsert() {
561
    getMainPane().autoinsert();
562
  }
563
564
  public void view_refresh() {
565
    getMainPane().viewRefresh();
566
  }
567
568
  public void view_preview() {
569
    getMainPane().viewPreview();
570
  }
571
572
  public void view_outline() {
573
    getMainPane().viewOutline();
574
  }
575
576
  public void view_files() { getMainPane().viewFiles(); }
577
578
  public void view_statistics() {
579
    getMainPane().viewStatistics();
580
  }
581
582
  public void view_menubar() {
583
    getMainScene().toggleMenuBar();
584
  }
585
586
  public void view_toolbar() {
587
    getMainScene().toggleToolBar();
588
  }
589
590
  public void view_statusbar() {
591
    getMainScene().toggleStatusBar();
592
  }
593
594
  public void view_log() {
595
    mLogView.view();
596
  }
597
598
  public void help_about() {
599
    final var alert = new Alert( INFORMATION );
600
    final var prefix = "Dialog.about.";
601
    alert.setTitle( get( prefix + "title", APP_TITLE ) );
602
    alert.setHeaderText( get( prefix + "header", APP_TITLE ) );
603
    alert.setContentText( get( prefix + "content", APP_YEAR, APP_VERSION ) );
604
    alert.setGraphic( ICON_DIALOG_NODE );
605
    alert.initOwner( getWindow() );
606
    alert.showAndWait();
607
  }
608
609
  private <T> void typeset( final Service<T> service ) {
610
    synchronized( mMutex ) {
611
      if( service != null && !service.isRunning() ) {
612
        service.reset();
613
        service.start();
614
      }
615
    }
616
  }
617
618
  /**
619
   * Concatenates all the files in the same directory as the given file into
620
   * a string. The extension is determined by the given file name pattern; the
621
   * order files are concatenated is based on their numeric sort order (this
622
   * avoids lexicographic sorting).
623
   * <p>
624
   * If the parent path to the file being edited in the text editor cannot
625
   * be found then this will return the editor's text, without iterating through
626
   * the parent directory. (Should never happen, but who knows?)
627
   * </p>
628
   * <p>
629
   * New lines are automatically appended to separate each file.
630
   * </p>
631
   *
632
   * @param editor The text editor containing
633
   * @return All files in the same directory as the file being edited
634
   * concatenated into a single string.
635
   */
636
  private String append( final TextEditor editor ) {
637
    final var pattern = editor.getPath();
638
    final var parent = pattern.getParent();
639
640
    // Short-circuit because nothing else can be done.
641
    if( parent == null ) {
642
      clue( "Main.status.export.concat.parent", pattern );
643
      return editor.getText();
644
    }
645
646
    final var filename = pattern.getFileName().toString();
647
    final var extension = getExtension( filename );
648
649
    if( extension.isBlank() ) {
650
      clue( "Main.status.export.concat.extension", filename );
651
      return editor.getText();
652
    }
653
654
    try {
655
      final var glob = "**/*." + extension;
656
      final var files = new ArrayList<Path>();
657
      final var text = new StringBuilder( DOCUMENT_LENGTH );
658
      final var range = getString( KEY_TYPESET_CONTEXT_CHAPTERS );
659
      final var validator = new RangeValidator( range );
660
      final var chapter = new AtomicInteger();
661
662
      walk( parent, glob, files::add );
663
      files.sort( new AlphanumComparator<>() );
664
      files.forEach( file -> {
665
        try {
666
          clue( "Main.status.export.concat", file );
667
668
          if( validator.test( chapter.incrementAndGet() ) ) {
669
            // Ensure multiple files are separated by an EOL.
670
            text.append( readString( file ) ).append( lineSeparator() );
671
          }
672
        } catch( final IOException ex ) {
673
          clue( "Main.status.export.concat.io", file );
674
        }
675
      } );
676
677
      return text.toString();
678
    } catch( final Throwable t ) {
679
      clue( t );
680
      return editor.getText();
681
    }
682
  }
683
684
  private Optional<List<File>> pickFiles( final SelectionType type ) {
685
    return createPicker( type ).choose();
686
  }
687
688
  @SuppressWarnings( "SameParameterValue" )
689
  private Optional<List<File>> pickFile(
690
    final File file,
691
    final Path directory,
692
    final SelectionType type ) {
693
    final var picker = createPicker( type );
694
    picker.setInitialFilename( file );
695
    picker.setInitialDirectory( directory );
696
    return picker.choose();
697
  }
698
699
  private FilePicker createPicker( final SelectionType type ) {
700
    final var factory = new FilePickerFactory( getWorkspace() );
701
    return factory.createModal( getWindow(), type );
702
  }
703
704
  private TextEditor getActiveTextEditor() {
705
    return getMainPane().getTextEditor();
706
  }
707
708
  private TextDefinition getActiveTextDefinition() {
709
    return getMainPane().getTextDefinition();
710
  }
711
712
  private MainScene getMainScene() {
713
    return mMainScene;
714
  }
715
716
  private MainPane getMainPane() {
717
    return mMainPane;
718
  }
719
720
  private Workspace getWorkspace() {
721
    return mMainPane.getWorkspace();
722
  }
723
724
  @SuppressWarnings( "SameParameterValue" )
725
  private String getString( final Key key ) {
726
    return getWorkspace().getString( key );
727
  }
728
729
  private Window getWindow() {
730
    return getMainPane().getWindow();
731
  }
732
}
1733
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 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.clipboard;
3
4
import javafx.scene.input.ClipboardContent;
5
6
import static javafx.scene.input.Clipboard.getSystemClipboard;
7
8
/**
9
 * Responsible for pasting into the computer's clipboard.
10
 */
11
public class Clipboard {
12
  /**
13
   * Copies the given text into the clipboard, overwriting all data.
14
   *
15
   * @param text The text to insert into the clipboard.
16
   */
17
  public static void write( final String text ) {
18
    final var contents = new ClipboardContent();
19
    contents.putString( text );
20
    getSystemClipboard().setContent( contents );
21
  }
22
23
  /**
24
   * Delegates to {@link #write(String)}.
25
   *
26
   * @see #write(String)
27
   */
28
  public static void write( final StringBuilder text ) {
29
    write( text.toString() );
30
  }
31
}
132
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.ui.fonts.IconFactory.createGraphic;
47
import static org.controlsfx.glyphfont.FontAwesome.Glyph.FILE_ALT;
48
49
/**
50
 * Button that opens a file chooser to select a local file for a URL.
51
 */
52
public class BrowseFileButton extends Button {
53
54
  private final List<ExtensionFilter> mExtensionFilters = new ArrayList<>();
55
  private final ObjectProperty<Path> mBasePath = new SimpleObjectProperty<>();
56
  private final ObjectProperty<String> mUrl = new SimpleObjectProperty<>();
57
58
  public BrowseFileButton() {
59
    setGraphic( createGraphic( FILE_ALT ) );
60
    setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) );
61
    setOnAction( this::browse );
62
63
    disableProperty().bind( mBasePath.isNull() );
64
65
    // workaround for a JavaFX bug:
66
    //   avoid closing the dialog that contains this control when the user
67
    //   closes the FileChooser or DirectoryChooser using the ESC key
68
    addEventHandler( KeyEvent.KEY_RELEASED, e -> {
69
      if( e.getCode() == KeyCode.ESCAPE ) {
70
        e.consume();
71
      }
72
    } );
73
  }
74
75
  public void addExtensionFilter( ExtensionFilter extensionFilter ) {
76
    mExtensionFilters.add( extensionFilter );
77
  }
78
79
  public ObjectProperty<String> urlProperty() {
80
    return mUrl;
81
  }
82
83
  private void browse( ActionEvent e ) {
84
    var fileChooser = new FileChooser();
85
    fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) );
86
    fileChooser.getExtensionFilters().addAll( mExtensionFilters );
87
    fileChooser.getExtensionFilters()
88
               .add( new ExtensionFilter( Messages.get(
89
                   "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) );
90
    fileChooser.setInitialDirectory( getInitialDirectory() );
91
    var result = fileChooser.showOpenDialog( getScene().getWindow() );
92
    if( result != null ) {
93
      updateUrl( result );
94
    }
95
  }
96
97
  private File getInitialDirectory() {
98
    //TODO build initial directory based on current value of 'url' property
99
    return getBasePath().toFile();
100
  }
101
102
  private void updateUrl( File file ) {
103
    String newUrl;
104
    try {
105
      newUrl = getBasePath().relativize( file.toPath() ).toString();
106
    } catch( final Exception ex ) {
107
      newUrl = file.toString();
108
    }
109
    mUrl.set( newUrl.replace( '\\', '/' ) );
110
  }
111
112
  public void setBasePath( Path basePath ) {
113
    this.mBasePath.set( basePath );
114
  }
115
116
  private Path getBasePath() {
117
    return mBasePath.get();
118
  }
119
}
1120
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
  /**
169
   * Creates a vertical bar, used to divide the search results from the
170
   * application status message.
171
   *
172
   * @return A vertical separator.
173
   */
174
  private Node createSeparatorVertical() {
175
    return new Separator( VERTICAL );
176
  }
177
178
  /**
179
   * Breathing room between the search box and the application status message.
180
   * This could also be accomplished by using CSS.
181
   *
182
   * @param width The spacer's width.
183
   * @return A new {@link Node} having about 10px of space.
184
   */
185
  private Node createSpacer( final int width ) {
186
    final var spacer = new Region();
187
    spacer.setPrefWidth( width );
188
    VBox.setVgrow( spacer, ALWAYS );
189
    return spacer;
190
  }
191
192
  private Node getIcon( final String id ) {
193
    return createGraphic( getMessageValue( id, "icon" ) );
194
  }
195
196
  private String getMessageValue( final String id, final String suffix ) {
197
    return get( format( MESSAGE_KEY, id, suffix ) );
198
  }
199
200
  private void addAll( final Node... nodes ) {
201
    getChildren().addAll( nodes );
202
  }
203
}
1204
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
    initIcon( (Stage) owner );
38
  }
39
40
  /**
41
   * Initialize the component layout.
42
   */
43
  protected abstract void initComponents();
44
45
  /**
46
   * Set the dialog to use a button order pane with an OK and a CANCEL button.
47
   */
48
  protected void initDialogPane() {
49
    setDialogPane( new ButtonOrderPane() );
50
  }
51
52
  /**
53
   * Set an OK and CANCEL button on the dialog.
54
   */
55
  protected void initDialogButtons() {
56
    getDialogPane().getButtonTypes().addAll( OK, CANCEL );
57
  }
58
59
  /**
60
   * Attaches a close request to the dialog's [X] button so that the user
61
   * can always close the window, even if there's an error.
62
   */
63
  protected final void initCloseAction() {
64
    final var window = getDialogPane().getScene().getWindow();
65
    window.setOnCloseRequest( event -> window.hide() );
66
  }
67
68
  private void initIcon( final Stage owner ) {
69
    owner.getIcons().add( ICON_DIALOG );
70
  }
71
}
172
A src/main/java/com/keenwrite/ui/dialogs/ExportDialog.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.dialogs;
3
4
import com.keenwrite.events.ExportFailedEvent;
5
import com.keenwrite.util.FileWalker;
6
import com.keenwrite.util.RangeValidator;
7
import com.keenwrite.util.ResourceWalker;
8
import javafx.geometry.Insets;
9
import javafx.scene.Node;
10
import javafx.scene.control.ComboBox;
11
import javafx.scene.control.Label;
12
import javafx.scene.control.TextField;
13
import javafx.scene.image.Image;
14
import javafx.scene.input.KeyCode;
15
import javafx.scene.layout.GridPane;
16
import javafx.scene.text.Font;
17
import javafx.stage.Stage;
18
import javafx.stage.Window;
19
20
import java.io.File;
21
import java.io.FileInputStream;
22
import java.io.IOException;
23
import java.io.InputStreamReader;
24
import java.nio.charset.StandardCharsets;
25
import java.nio.file.Path;
26
import java.util.Collections;
27
import java.util.LinkedList;
28
import java.util.List;
29
import java.util.Properties;
30
import java.util.concurrent.atomic.AtomicReference;
31
32
import static com.keenwrite.Messages.get;
33
import static com.keenwrite.constants.Constants.THEME_NAME_LENGTH;
34
import static com.keenwrite.constants.Constants.UI_CONTROL_SPACING;
35
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
36
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE;
37
import static com.keenwrite.events.StatusEvent.clue;
38
import static com.keenwrite.util.FileWalker.walk;
39
import static java.lang.Math.max;
40
import static java.nio.charset.StandardCharsets.UTF_8;
41
import static java.text.Normalizer.Form.NFKD;
42
import static java.text.Normalizer.normalize;
43
import static javafx.application.Platform.runLater;
44
import static javafx.geometry.Pos.CENTER;
45
import static javafx.scene.control.ButtonType.OK;
46
import static org.apache.commons.lang3.StringUtils.abbreviate;
47
48
/**
49
 * Provides controls for exporting to PDF, such as selecting a theme and
50
 * creating a subset of chapter numbers.
51
 */
52
public final class ExportDialog extends AbstractDialog<ExportSettings> {
53
  private static final String UNCRITIC = "\\p{InCombiningDiacriticalMarks}+";
54
55
  private record Theme( Path path, String name ) implements Comparable<Theme> {
56
    /**
57
     * Answers whether the given theme directory name matches the theme name
58
     * that the user selected.
59
     *
60
     * @param themeDir The user-selected directory to compare with the
61
     *                 corresponding path of this {@link Theme}.
62
     * @return {@code true} if the given directory matches the ending portion
63
     * of the {@link Path} associated with this {@link Theme} instance.
64
     */
65
    public boolean matches( final String themeDir ) {
66
      final var normalized = normalize( themeDir, NFKD );
67
      final var name = normalized.replaceAll( UNCRITIC, "" );
68
      final var path = path().getFileName().toString();
69
70
      return path.equalsIgnoreCase( name );
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
      return name().compareTo( o.name() );
86
    }
87
  }
88
89
  private final File mThemes;
90
  private final ExportSettings mSettings;
91
  private GridPane mPane;
92
  private ComboBox<Theme> mComboBox;
93
  private TextField mChapters;
94
  private final boolean mMissingThemes;
95
96
  /**
97
   * Construction must use static method to allow caching themes in the
98
   * future, if needed.
99
   */
100
  private ExportDialog(
101
    final Window owner,
102
    final File themesDir,
103
    final ExportSettings settings,
104
    final boolean multiple
105
  ) {
106
    super( owner, get( "Dialog.typesetting.settings.title" ) );
107
108
    assert themesDir != null;
109
    assert settings != null;
110
111
    mThemes = themesDir;
112
    mSettings = settings;
113
114
    setResultConverter( button -> button == OK ? settings : null );
115
116
    final var themes = readThemes( themesDir );
117
118
    mMissingThemes = themes.isEmpty();
119
120
    // Typesetting installation has been corrupted. This is probably due
121
    // to the user's settings file gone missing. Rather than force users
122
    // to find the "themes" directory location, re-install the typesetter,
123
    if( mMissingThemes ) {
124
      clue( "Dialog.typesetting.settings.themes.missing",
125
            themesDir.getAbsolutePath() );
126
      ExportFailedEvent.fire();
127
      return;
128
    }
129
130
    final var previousTheme = mSettings.themeProperty().get();
131
132
    initComboBox( mComboBox, previousTheme, themes );
133
134
    mPane.add( createLabel( "Dialog.typesetting.settings.theme" ), 0, 1 );
135
    mPane.add( mComboBox, 1, 1 );
136
137
    var title = "Dialog.typesetting.settings.header.";
138
    final var focusNode = new AtomicReference<Node>( mComboBox );
139
140
    if( multiple ) {
141
      mPane.add( createLabel( "Dialog.typesetting.settings.chapters" ), 0, 2 );
142
      mPane.add( mChapters, 1, 2 );
143
144
      focusNode.set( mChapters );
145
      title += "multiple";
146
    }
147
    else {
148
      title += "single";
149
    }
150
151
    // Remember the chapter range regardless of text field visibility.
152
    mChapters.textProperty().bindBidirectional( mSettings.chaptersProperty() );
153
154
    setHeaderText( get( title ) );
155
156
    final var dialogPane = getDialogPane();
157
    dialogPane.setContent( mPane );
158
159
    runLater( () -> focusNode.get().requestFocus() );
160
  }
161
162
  /**
163
   * Prompts a user to select a theme, answering {@code false} if no theme
164
   * was selected. The themes must be on the native file system; using the
165
   * {@link FileWalker} is a little more optimal than {@link ResourceWalker}.
166
   *
167
   * @param owner    The parent {@link Window} responsible for the dialog.
168
   * @param themes   Theme directory root.
169
   * @param settings Configuration preferences to use when exporting.
170
   * @param multiple Pass {@code true} to input a chapter number subset.
171
   * @return {@code true} if the user accepted or selected a theme.
172
   */
173
  public static boolean choose(
174
    final Window owner,
175
    final File themes,
176
    final ExportSettings settings,
177
    final boolean multiple
178
  ) {
179
    assert themes != null;
180
    assert settings != null;
181
182
    return new ExportDialog( owner, themes, settings, multiple ).pick();
183
  }
184
185
  /**
186
   * @return {@code true} if the user accepted or selected a theme.
187
   * @see #choose(Window, File, ExportSettings, boolean)
188
   */
189
  private boolean pick() {
190
    try {
191
      if( !mMissingThemes ) {
192
        final var result = showAndWait();
193
194
        // The result will only be set if the OK button is pressed.
195
        if( result.isPresent() ) {
196
          final var theme = mComboBox.getSelectionModel().getSelectedItem();
197
          final var path = theme.path().getFileName().toString();
198
          mSettings.themeProperty().setValue( path );
199
200
          return true;
201
        }
202
      }
203
    } catch( final Exception ex ) {
204
      clue( get( "Main.status.error.theme.missing", mThemes ), ex );
205
    }
206
207
    return false;
208
  }
209
210
  @Override
211
  protected void initComponents() {
212
    initIcon();
213
    setResizable( true );
214
215
    mPane = createContentPane();
216
    mComboBox = createComboBox();
217
    mComboBox.setOnKeyPressed( event -> {
218
      // When the user presses the down arrow, open the drop-down. This
219
      // prevents navigating to the cancel button.
220
      if( event.getCode() == KeyCode.DOWN && !mComboBox.isShowing() ) {
221
        mComboBox.show();
222
        event.consume();
223
      }
224
    } );
225
226
    mChapters = createNumericTextField();
227
  }
228
229
  private void initIcon() {
230
    setGraphic( ICON_DIALOG_NODE );
231
    setStageGraphic( ICON_DIALOG );
232
  }
233
234
  @SuppressWarnings( "SameParameterValue" )
235
  private void setStageGraphic( final Image icon ) {
236
    if( getDialogPane().getScene().getWindow() instanceof final Stage stage ) {
237
      stage.getIcons().add( icon );
238
    }
239
  }
240
241
  private void initComboBox(
242
    final ComboBox<Theme> comboBox,
243
    final String previousTheme,
244
    final List<Theme> choices
245
  ) {
246
    assert comboBox != null;
247
    assert previousTheme != null;
248
    assert choices != null;
249
250
    final var items = comboBox.getItems();
251
    items.clear();
252
    items.addAll( choices );
253
254
    // Set the selected item to user's settings value.
255
    for( final var choice : choices ) {
256
      if( choice.matches( previousTheme ) ) {
257
        comboBox.getSelectionModel().select(
258
          items.get( max( items.indexOf( choice ), 0 ) )
259
        );
260
261
        break;
262
      }
263
    }
264
  }
265
266
  private List<Theme> readThemes( final File themesDir ) {
267
    try {
268
      // List themes in alphabetical order (human-readable by directory name).
269
      final var choices = new LinkedList<Theme>();
270
271
      // Populate the choices with themes detected on the system.
272
      walk( themesDir.toPath(), "**/theme.properties", path -> {
273
        try {
274
          final var themeName = readThemeName( path );
275
          final var themePath = path.getParent();
276
          choices.add( new Theme( themePath, themeName ) );
277
        } catch( final Exception ex ) {
278
          clue( "Main.status.error.theme.name", path );
279
        }
280
      } );
281
282
      Collections.sort( choices );
283
284
      return choices;
285
    } catch( final Exception ex ) {
286
      clue( ex );
287
    }
288
289
    return Collections.emptyList();
290
  }
291
292
  private ComboBox<Theme> createComboBox() {
293
    return new ComboBox<>();
294
  }
295
296
  private GridPane createContentPane() {
297
    final var grid = new GridPane();
298
299
    grid.setAlignment( CENTER );
300
    grid.setHgap( UI_CONTROL_SPACING );
301
    grid.setVgap( UI_CONTROL_SPACING );
302
    grid.setPadding( new Insets( 25, 25, 25, 25 ) );
303
304
    return grid;
305
  }
306
307
  /**
308
   * Creates an input field that only accepts whole numbers. This allows users
309
   * to enter in chapter ranges such as: <code>1-5, 7, 9-10</code>.
310
   *
311
   * @return A {@link TextField} that censors non-conforming characters.
312
   */
313
  private TextField createNumericTextField() {
314
    final var textField = new TextField();
315
316
    textField.textProperty().addListener(
317
      ( c, o, n ) -> textField.setText( RangeValidator.normalize( n ) )
318
    );
319
320
    return textField;
321
  }
322
323
  private Label createLabel( final String key ) {
324
    final var label = new Label( get( key ) + ":" );
325
    final var font = label.getFont();
326
    final var upscale = new Font( font.getName(), 14 );
327
328
    label.setFont( upscale );
329
330
    return label;
331
  }
332
333
  /**
334
   * Returns the theme's human-friendly name from a file conforming to
335
   * {@link Properties}.
336
   *
337
   * @param file A fully qualified file name readable using {@link Properties}.
338
   * @return The human-friendly theme name.
339
   * @throws IOException          The {@link Properties} file cannot be read.
340
   * @throws NullPointerException The name field is not defined.
341
   */
342
  private String readThemeName( final Path file ) throws Exception {
343
    return read( file ).get( "name" ).toString();
344
  }
345
346
  /**
347
   * Reads an instance of {@link Properties} from the given {@link Path} using
348
   * {@link StandardCharsets#UTF_8} encoding.
349
   *
350
   * @param path The fully qualified path to the file.
351
   * @return The path to the file to read.
352
   * @throws IOException Could not open the file for reading.
353
   */
354
  private Properties read( final Path path ) throws IOException {
355
    final var properties = new Properties();
356
357
    try(
358
      final var f = new FileInputStream( path.toFile() );
359
      final var in = new InputStreamReader( f, UTF_8 )
360
    ) {
361
      properties.load( in );
362
    }
363
364
    return properties;
365
  }
366
}
1367
A src/main/java/com/keenwrite/ui/dialogs/ExportSettings.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.dialogs;
3
4
import com.keenwrite.util.GenericBuilder;
5
import javafx.beans.property.StringProperty;
6
7
/**
8
 * Provides export settings such as the selected theme and chapter numbers
9
 * to include.
10
 */
11
public class ExportSettings {
12
  private final Mutator mMutator;
13
14
  public static class Mutator {
15
    private StringProperty mThemeProperty;
16
    private StringProperty mChaptersProperty;
17
18
    public void setTheme( final StringProperty theme ) {
19
      assert theme != null;
20
      mThemeProperty = theme;
21
    }
22
23
    public void setChapters( final StringProperty chapters ) {
24
      assert chapters != null;
25
      mChaptersProperty = chapters;
26
    }
27
  }
28
29
  /**
30
   * Force using the builder pattern.
31
   */
32
  private ExportSettings( final Mutator mutator ) {
33
    assert mutator != null;
34
35
    mMutator = mutator;
36
  }
37
38
  public static GenericBuilder<Mutator, ExportSettings> builder() {
39
    return GenericBuilder.of(
40
      ExportSettings.Mutator::new, ExportSettings::new
41
    );
42
  }
43
44
  public StringProperty themeProperty() {
45
    return mMutator.mThemeProperty;
46
  }
47
48
  public StringProperty chaptersProperty() {
49
    return mMutator.mChaptersProperty;
50
  }
51
}
52
153
A src/main/java/com/keenwrite/ui/dialogs/ImageDialog.java
1
/*
2
 * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.ui.dialogs;
28
29
import static com.keenwrite.Messages.get;
30
import com.keenwrite.ui.controls.BrowseFileButton;
31
import com.keenwrite.ui.controls.EscapeTextField;
32
import java.nio.file.Path;
33
import javafx.application.Platform;
34
import javafx.beans.binding.Bindings;
35
import javafx.beans.property.SimpleStringProperty;
36
import javafx.beans.property.StringProperty;
37
import javafx.scene.control.ButtonBar.ButtonData;
38
import static javafx.scene.control.ButtonType.OK;
39
import javafx.scene.control.DialogPane;
40
import javafx.scene.control.Label;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
import javafx.stage.Window;
43
import org.tbee.javafx.scene.layout.fxml.MigPane;
44
45
/**
46
 * Dialog to enter a Markdown image.
47
 */
48
public class ImageDialog extends AbstractDialog<String> {
49
50
  private final StringProperty image = new SimpleStringProperty();
51
52
  public ImageDialog( final Window owner, final Path basePath ) {
53
    super(owner, "Dialog.image.title" );
54
    
55
    final DialogPane dialogPane = getDialogPane();
56
    dialogPane.setContent( pane );
57
58
    linkBrowseFileButton.setBasePath( basePath );
59
    linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
60
    linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() );
61
62
    dialogPane.lookupButton( OK ).disableProperty().bind(
63
      urlField.escapedTextProperty().isEmpty()
64
      .or( textField.escapedTextProperty().isEmpty() ) );
65
66
    image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "![%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.format( "![%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) );
69
    previewField.textProperty().bind( image );
70
71
    setResultConverter( dialogButton -> {
72
      ButtonData data = dialogButton != null ? dialogButton.getButtonData() : null;
73
      return data == ButtonData.OK_DONE ? image.get() : null;
74
    } );
75
76
    Platform.runLater( () -> {
77
      urlField.requestFocus();
78
79
      if( urlField.getText().startsWith( "http://" ) ) {
80
        urlField.selectRange( "http://".length(), urlField.getLength() );
81
      }
82
    } );
83
  }
84
85
  @Override
86
  protected void initComponents() {
87
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
88
    pane = new MigPane();
89
    Label urlLabel = new Label();
90
    urlField = new EscapeTextField();
91
    linkBrowseFileButton = new BrowseFileButton();
92
    Label textLabel = new Label();
93
    textField = new EscapeTextField();
94
    Label titleLabel = new Label();
95
    titleField = new EscapeTextField();
96
    Label previewLabel = new Label();
97
    previewField = new Label();
98
99
    //======== pane ========
100
    {
101
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" );
102
      pane.setRows( "[][][][]" );
103
104
      //---- urlLabel ----
105
      urlLabel.setText( get( "Dialog.image.urlLabel.text" ) );
106
      pane.add( urlLabel, "cell 0 0" );
107
108
      //---- urlField ----
109
      urlField.setEscapeCharacters( "()" );
110
      urlField.setText( "http://yourlink.com" );
111
      urlField.setPromptText( "http://yourlink.com" );
112
      pane.add( urlField, "cell 1 0" );
113
      pane.add( linkBrowseFileButton, "cell 2 0" );
114
115
      //---- textLabel ----
116
      textLabel.setText( get( "Dialog.image.textLabel.text" ) );
117
      pane.add( textLabel, "cell 0 1" );
118
119
      //---- textField ----
120
      textField.setEscapeCharacters( "[]" );
121
      pane.add( textField, "cell 1 1 2 1" );
122
123
      //---- titleLabel ----
124
      titleLabel.setText( get( "Dialog.image.titleLabel.text" ) );
125
      pane.add( titleLabel, "cell 0 2" );
126
      pane.add( titleField, "cell 1 2 2 1" );
127
128
      //---- previewLabel ----
129
      previewLabel.setText( get( "Dialog.image.previewLabel.text" ) );
130
      pane.add( previewLabel, "cell 0 3" );
131
      pane.add( previewField, "cell 1 3 2 1" );
132
    }
133
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
134
  }
135
136
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
137
  private MigPane pane;
138
  private EscapeTextField urlField;
139
  private BrowseFileButton linkBrowseFileButton;
140
  private EscapeTextField textField;
141
  private EscapeTextField titleField;
142
  private Label previewField;
143
	// JFormDesigner - End of variables declaration  //GEN-END:variables
144
}
1145
A src/main/java/com/keenwrite/ui/dialogs/ImageDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "ImageDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "ImageDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
34
				name: "linkBrowseFileButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "javafx.scene.control.Label" ) {
39
				name: "textLabel"
40
				"text": new FormMessage( null, "ImageDialog.textLabel.text" )
41
				auxiliary() {
42
					"JavaCodeGenerator.variableLocal": true
43
				}
44
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
45
				"value": "cell 0 1"
46
			} )
47
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
48
				name: "textField"
49
				"escapeCharacters": "[]"
50
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
51
				"value": "cell 1 1 2 1"
52
			} )
53
			add( new FormComponent( "javafx.scene.control.Label" ) {
54
				name: "titleLabel"
55
				"text": new FormMessage( null, "ImageDialog.titleLabel.text" )
56
				auxiliary() {
57
					"JavaCodeGenerator.variableLocal": true
58
				}
59
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
60
				"value": "cell 0 2"
61
			} )
62
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
63
				name: "titleField"
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 1 2 2 1"
66
			} )
67
			add( new FormComponent( "javafx.scene.control.Label" ) {
68
				name: "previewLabel"
69
				"text": new FormMessage( null, "ImageDialog.previewLabel.text" )
70
				auxiliary() {
71
					"JavaCodeGenerator.variableLocal": true
72
				}
73
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
74
				"value": "cell 0 3"
75
			} )
76
			add( new FormComponent( "javafx.scene.control.Label" ) {
77
				name: "previewField"
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 1 3 2 1"
80
			} )
81
		}, new FormLayoutConstraints( null ) {
82
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
83
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
84
		} )
85
	}
86
}
187
A src/main/java/com/keenwrite/ui/dialogs/LinkDialog.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.ui.dialogs;
29
30
import com.keenwrite.ui.controls.EscapeTextField;
31
import com.keenwrite.editors.markdown.HyperlinkModel;
32
import javafx.application.Platform;
33
import javafx.beans.binding.Bindings;
34
import javafx.beans.property.SimpleStringProperty;
35
import javafx.beans.property.StringProperty;
36
import javafx.scene.control.ButtonBar.ButtonData;
37
import javafx.scene.control.DialogPane;
38
import javafx.scene.control.Label;
39
import javafx.stage.Window;
40
import org.tbee.javafx.scene.layout.fxml.MigPane;
41
42
import static com.keenwrite.Messages.get;
43
import static javafx.scene.control.ButtonType.OK;
44
45
/**
46
 * Dialog to enter a Markdown link.
47
 */
48
public class LinkDialog extends AbstractDialog<String> {
49
50
  private final StringProperty link = new SimpleStringProperty();
51
52
  public LinkDialog(
53
    final Window owner, final HyperlinkModel hyperlink ) {
54
    super( owner, "Dialog.link.title" );
55
56
    final DialogPane dialogPane = getDialogPane();
57
    dialogPane.setContent( pane );
58
59
    dialogPane.lookupButton( OK ).disableProperty().bind(
60
      urlField.escapedTextProperty().isEmpty() );
61
62
    textField.setText( hyperlink.getText() );
63
    urlField.setText( hyperlink.getUrl() );
64
    titleField.setText( hyperlink.getTitle() );
65
66
    link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() )
69
        .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) )
70
        .otherwise( urlField.escapedTextProperty() ) ) );
71
72
    setResultConverter( dialogButton -> {
73
      ButtonData data = dialogButton != null ? dialogButton.getButtonData() : null;
74
      return data == ButtonData.OK_DONE ? link.get() : null;
75
    } );
76
77
    Platform.runLater( () -> {
78
      urlField.requestFocus();
79
      urlField.selectRange( 0, urlField.getLength() );
80
    } );
81
  }
82
83
  @Override
84
  protected void initComponents() {
85
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
86
    pane = new MigPane();
87
    Label urlLabel = new Label();
88
    urlField = new EscapeTextField();
89
    Label textLabel = new Label();
90
    textField = new EscapeTextField();
91
    Label titleLabel = new Label();
92
    titleField = new EscapeTextField();
93
94
    //======== pane ========
95
    {
96
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" );
97
      pane.setRows( "[][][][]" );
98
99
      //---- urlLabel ----
100
      urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
101
      pane.add( urlLabel, "cell 0 0" );
102
103
      //---- urlField ----
104
      urlField.setEscapeCharacters( "()" );
105
      pane.add( urlField, "cell 1 0" );
106
107
      //---- textLabel ----
108
      textLabel.setText( get( "Dialog.link.textLabel.text" ) );
109
      pane.add( textLabel, "cell 0 1" );
110
111
      //---- textField ----
112
      textField.setEscapeCharacters( "[]" );
113
      pane.add( textField, "cell 1 1 3 1" );
114
115
      //---- titleLabel ----
116
      titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
117
      pane.add( titleLabel, "cell 0 2" );
118
      pane.add( titleField, "cell 1 2 3 1" );
119
    }
120
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
121
  }
122
123
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
124
  private MigPane pane;
125
  private EscapeTextField urlField;
126
  private EscapeTextField textField;
127
  private EscapeTextField titleField;
128
  // JFormDesigner - End of variables declaration  //GEN-END:variables
129
}
1130
A src/main/java/com/keenwrite/ui/dialogs/LinkDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "LinkDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "LinkDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) {
34
				name: "linkBrowseDirectoyButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
39
				name: "linkBrowseFileButton"
40
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
41
				"value": "cell 3 0"
42
			} )
43
			add( new FormComponent( "javafx.scene.control.Label" ) {
44
				name: "textLabel"
45
				"text": new FormMessage( null, "LinkDialog.textLabel.text" )
46
				auxiliary() {
47
					"JavaCodeGenerator.variableLocal": true
48
				}
49
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
50
				"value": "cell 0 1"
51
			} )
52
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
53
				name: "textField"
54
				"escapeCharacters": "[]"
55
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
56
				"value": "cell 1 1 3 1"
57
			} )
58
			add( new FormComponent( "javafx.scene.control.Label" ) {
59
				name: "titleLabel"
60
				"text": new FormMessage( null, "LinkDialog.titleLabel.text" )
61
				auxiliary() {
62
					"JavaCodeGenerator.variableLocal": true
63
				}
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 0 2"
66
			} )
67
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
68
				name: "titleField"
69
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
70
				"value": "cell 1 2 3 1"
71
			} )
72
			add( new FormComponent( "javafx.scene.control.Label" ) {
73
				name: "previewLabel"
74
				"text": new FormMessage( null, "LinkDialog.previewLabel.text" )
75
				auxiliary() {
76
					"JavaCodeGenerator.variableLocal": true
77
				}
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 0 3"
80
			} )
81
			add( new FormComponent( "javafx.scene.control.Label" ) {
82
				name: "previewField"
83
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
84
				"value": "cell 1 3 3 1"
85
			} )
86
		}, new FormLayoutConstraints( null ) {
87
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
88
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
89
		} )
90
	}
91
}
192
A src/main/java/com/keenwrite/ui/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.preferences.AppKeys.KEY_UI_RECENT_DIR;
18
import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType.*;
19
import static java.lang.String.format;
20
21
/**
22
 * Shim for a {@link FilePicker} instance that is implemented in pure Java.
23
 * This particular picker is added to avoid using the bug-ridden JavaFX
24
 * {@link FileChooser} that invokes the native file chooser.
25
 */
26
public class FilePickerFactory {
27
  public enum SelectionType {
28
    DIRECTORY_OPEN( "open" ),
29
    FILE_IMPORT( "import" ),
30
    FILE_EXPORT( "export" ),
31
    FILE_OPEN_SINGLE( "open" ),
32
    FILE_OPEN_MULTIPLE( "open" ),
33
    FILE_OPEN_NEW( "open" ),
34
    FILE_SAVE_AS( "save" );
35
36
    private final String mTitle;
37
38
    SelectionType( final String title ) {
39
      assert title != null;
40
      mTitle = Messages.get( format( "Dialog.file.choose.%s.title", title ) );
41
    }
42
43
    public String getTitle() {
44
      return mTitle;
45
    }
46
  }
47
48
  private final ObjectProperty<File> mDirectory;
49
  private final Locale mLocale;
50
51
  public FilePickerFactory( final Workspace workspace ) {
52
    mDirectory = workspace.fileProperty( KEY_UI_RECENT_DIR );
53
    mLocale = workspace.getLocale();
54
  }
55
56
  public FilePicker createModal(
57
    final Window owner, final SelectionType options ) {
58
    final var picker = new NativeFilePicker( owner, options );
59
60
    picker.setInitialDirectory( mDirectory.get().toPath() );
61
62
    return picker;
63
  }
64
65
  public Node createModeless() {
66
    return new FilesView( mDirectory, mLocale );
67
  }
68
69
  /**
70
   * Operating system's file selection dialog.
71
   */
72
  private static final class NativeFilePicker implements FilePicker {
73
    private final FileChooser mChooser = new FileChooser();
74
    private final Window mOwner;
75
    private final SelectionType mType;
76
77
    public NativeFilePicker( final Window owner, final SelectionType type ) {
78
      assert owner != null;
79
      assert type != null;
80
81
      mOwner = owner;
82
      mType = type;
83
    }
84
85
    @Override
86
    public void setInitialFilename( final File file ) {
87
      assert file != null;
88
89
      mChooser.setInitialFileName( file.getName() );
90
    }
91
92
    @Override
93
    public void setInitialDirectory( final Path path ) {
94
      assert path != null;
95
96
      final var file = path.toFile();
97
98
      mChooser.setInitialDirectory(
99
        file.exists() ? file : new File( System.getProperty( "user.home" ) )
100
      );
101
    }
102
103
    @Override
104
    public Optional<List<File>> choose() {
105
      if( mType == FILE_OPEN_MULTIPLE ) {
106
        return Optional.ofNullable( mChooser.showOpenMultipleDialog( mOwner ) );
107
      }
108
109
      final File file = mType == FILE_EXPORT || mType == FILE_SAVE_AS
110
        ? mChooser.showSaveDialog( mOwner )
111
        : mChooser.showOpenDialog( mOwner );
112
113
      return file == null ? Optional.empty() : Optional.of( List.of( file ) );
114
    }
115
  }
116
}
1117
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.ui.controls.BrowseButton;
6
import javafx.beans.property.*;
7
import javafx.collections.ObservableList;
8
import javafx.collections.transformation.SortedList;
9
import javafx.scene.control.*;
10
import javafx.scene.layout.BorderPane;
11
import javafx.scene.layout.HBox;
12
import javafx.stage.FileChooser;
13
import javafx.util.Callback;
14
15
import java.io.File;
16
import java.io.IOException;
17
import java.nio.file.Path;
18
import java.nio.file.Paths;
19
import java.time.Instant;
20
import java.time.format.DateTimeFormatter;
21
import java.util.List;
22
import java.util.Locale;
23
import java.util.Objects;
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.ui.fonts.IconFactory.createFileIcon;
29
import static java.nio.file.Files.size;
30
import static java.time.Instant.ofEpochMilli;
31
import static java.time.ZoneId.systemDefault;
32
import static java.time.format.DateTimeFormatter.ofPattern;
33
import static java.util.Comparator.comparing;
34
import static javafx.collections.FXCollections.observableArrayList;
35
import static javafx.scene.control.TableView.CONSTRAINED_RESIZE_POLICY;
36
import static javafx.scene.input.KeyCode.ENTER;
37
import static javafx.scene.layout.Priority.ALWAYS;
38
import static org.apache.commons.io.FilenameUtils.getExtension;
39
40
/**
41
 * Responsible for browsing files.
42
 */
43
public class FilesView extends BorderPane implements FilePicker {
44
  /**
45
   * When this directory changes, the input field will update accordingly.
46
   */
47
  private final ObjectProperty<File> mDirectory;
48
49
  /**
50
   * Data model for the file list shown in tabular format.
51
   */
52
  private final ObservableList<PathEntry> mItems = observableArrayList();
53
54
  /**
55
   * Used to format a file's date string from a {@code long} value.
56
   */
57
  private final DateTimeFormatter mDateFormatter;
58
59
  /**
60
   * Used to format a file's time string from a {@code long} value.
61
   */
62
  private final DateTimeFormatter mTimeFormatter;
63
64
  /**
65
   * Constructs a new view of a directory, listing all the files contained
66
   * therein. This will update the recent directory so that it will be
67
   * restored upon restart.
68
   *
69
   * @param recent Contains the initial (recent) directory.
70
   * @param locale Contains the language settings.
71
   */
72
  public FilesView(
73
    final ObjectProperty<File> recent, final Locale locale ) {
74
    mDirectory = recent;
75
    mDateFormatter = createFormatter( "yyyy-MMM-dd", locale );
76
    mTimeFormatter = createFormatter( "HH:mm:ss", locale );
77
78
    final var browse = createDirectoryChooser();
79
    final var table = createFileTable();
80
81
    final var sortedItems = new SortedList<>( mItems );
82
    sortedItems.comparatorProperty().bind( table.comparatorProperty() );
83
    table.setItems( sortedItems );
84
85
    setTop( browse );
86
    setCenter( table );
87
88
    mDirectory.addListener( ( c, o, n ) -> updateListing( n ) );
89
    updateListing( mDirectory.get() );
90
  }
91
92
  @Override
93
  public void setInitialFilename( final File file ) {
94
  }
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
        for( final var f : Objects.requireNonNull( directory.list() ) ) {
112
          if( !f.startsWith( "." ) ) {
113
            mItems.add( pathEntry( Paths.get( directory.toString(), f ) ) );
114
          }
115
        }
116
      } catch( final Exception ex ) {
117
        clue( ex );
118
      }
119
    }
120
  }
121
122
  /**
123
   * Allows the user to use an instance of {@link FileChooser} to change the
124
   * directory.
125
   *
126
   * @return The browse button and input field.
127
   */
128
  private HBox createDirectoryChooser() {
129
    final var dirProperty = directoryProperty();
130
    final var directory = dirProperty.get();
131
    final var hbox = new HBox();
132
    final var field = new TextField();
133
134
    mDirectory.addListener( ( c, o, n ) -> {
135
      if( n != null ) {field.setText( n.getAbsolutePath() );}
136
    } );
137
138
    field.setOnKeyPressed( event -> {
139
      if( event.getCode() == ENTER ) {
140
        mDirectory.set( new File( field.getText() ) );
141
      }
142
    } );
143
144
    final var button = new BrowseButton( directory, mDirectory::set );
145
146
    hbox.getChildren().add( button );
147
    hbox.getChildren().add( field );
148
    hbox.setSpacing( UI_CONTROL_SPACING );
149
    HBox.setHgrow( field, ALWAYS );
150
151
    return hbox;
152
  }
153
154
  @SuppressWarnings( "unchecked" )
155
  private TableView<FilesView.PathEntry> createFileTable() {
156
    final var style = "-fx-alignment: BASELINE_LEFT;";
157
    final var table = new TableView<FilesView.PathEntry>();
158
    table.setColumnResizePolicy( CONSTRAINED_RESIZE_POLICY );
159
160
    table.setRowFactory( tv -> {
161
      final var row = new TableRow<PathEntry>();
162
163
      row.setOnMouseClicked( event -> {
164
        if( event.getClickCount() == 2 && !row.isEmpty() ) {
165
          final var entry = row.getItem();
166
          final var dir = mDirectory.get();
167
          final var filename = entry.nameProperty().get();
168
          final var path = Path.of( dir.toString(), filename );
169
          final var file = path.toFile();
170
171
          if( file.isFile() ) {
172
            FileOpenEvent.fire( path.toUri() );
173
          }
174
          else if( file.isDirectory() ) {
175
            mDirectory.set( path.normalize().toFile() );
176
          }
177
        }
178
      } );
179
180
      return row;
181
    } );
182
183
    final TableColumn<PathEntry, Path> colType = createColumn( "Type" );
184
    final TableColumn<PathEntry, String> colName = createColumn( "Name" );
185
    final TableColumn<PathEntry, Number> colSize = createColumn( "Size" );
186
    final TableColumn<PathEntry, String> colDate = createColumn( "Date" );
187
    final TableColumn<PathEntry, String> colTime = createColumn( "Modified" );
188
189
    colType.setCellFactory( new FileCell<>() );
190
191
    colType.setCellValueFactory( stat -> stat.getValue().typeProperty() );
192
    colName.setCellValueFactory( stat -> stat.getValue().nameProperty() );
193
    colSize.setCellValueFactory( stat -> stat.getValue().sizeProperty() );
194
    colDate.setCellValueFactory( stat -> stat.getValue().dateProperty() );
195
    colTime.setCellValueFactory( stat -> stat.getValue().timeProperty() );
196
197
    colType.setStyle( style );
198
    colName.setStyle( style );
199
    colSize.setStyle( style );
200
    colDate.setStyle( style );
201
    colTime.setStyle( style );
202
203
    final var columns = table.getColumns();
204
    columns.add( colType );
205
    columns.add( colName );
206
    columns.add( colSize );
207
    columns.add( colDate );
208
    columns.add( colTime );
209
210
    table.getSortOrder().setAll( colName, colDate, colTime );
211
212
    colType.setComparator(
213
      comparing( p -> getExtension( p.getFileName().toString() ) )
214
    );
215
216
    return table;
217
  }
218
219
  public ObjectProperty<File> directoryProperty() {
220
    return mDirectory;
221
  }
222
223
  private static DateTimeFormatter createFormatter(
224
    final String format, final Locale locale ) {
225
    return ofPattern( format, locale ).withZone( systemDefault() );
226
  }
227
228
  public PathEntry pathEntry( final Path path ) throws IOException {
229
    return new PathEntry( path );
230
  }
231
232
  /**
233
   * Responsible for rendering file system objects as image icons.
234
   *
235
   * @param <T> The data model type associated with a fully qualified path.
236
   * @param <P> Simplifies swapping {@link Path} for {@link File}.
237
   */
238
  private static class FileCell<T, P extends Path> extends TableCell<T, P>
239
    implements Callback<TableColumn<T, P>, TableCell<T, P>> {
240
    @Override
241
    public TableCell<T, P> call( final TableColumn<T, P> param ) {
242
      return new TableCell<>() {
243
        @Override
244
        protected void updateItem( final P path, final boolean empty ) {
245
          super.updateItem( path, empty );
246
          setText( null );
247
248
          try {
249
            setGraphic( empty || path == null ? null : createFileIcon( path ) );
250
          } catch( final Exception ex ) {
251
            clue( ex );
252
          }
253
        }
254
      };
255
    }
256
  }
257
258
  protected final class PathEntry {
259
    private final ObjectProperty<Path> mType;
260
    private final StringProperty mName;
261
    private final LongProperty mSize;
262
    private final StringProperty mDate;
263
    private final StringProperty mTime;
264
265
    private PathEntry( final Path path ) throws IOException {
266
      this(
267
        path,
268
        path.getFileName().toString(),
269
        size( path ),
270
        ofEpochMilli( path.toFile().lastModified() )
271
      );
272
    }
273
274
    public PathEntry(
275
      final Path type,
276
      final String name,
277
      final long size,
278
      final Instant modified ) {
279
      this(
280
        new SimpleObjectProperty<>( type ),
281
        new SimpleStringProperty( name ),
282
        new SimpleLongProperty( size ),
283
        new SimpleStringProperty( mDateFormatter.format( modified ) ),
284
        new SimpleStringProperty( mTimeFormatter.format( modified ) )
285
      );
286
    }
287
288
    private PathEntry(
289
      final ObjectProperty<Path> type,
290
      final StringProperty name,
291
      final LongProperty size,
292
      final StringProperty date,
293
      final StringProperty time ) {
294
      mType = type;
295
      mName = name;
296
      mSize = size;
297
      mDate = date;
298
      mTime = time;
299
    }
300
301
    private ObjectProperty<Path> typeProperty() {
302
      return mType;
303
    }
304
305
    private StringProperty nameProperty() {
306
      return mName;
307
    }
308
309
    private LongProperty sizeProperty() {
310
      return mSize;
311
    }
312
313
    private StringProperty dateProperty() {
314
      return mDate;
315
    }
316
317
    private StringProperty timeProperty() {
318
      return mTime;
319
    }
320
  }
321
322
  private <E, T> TableColumn<E, T> createColumn( final String key ) {
323
    return new TableColumn<>( key );
324
  }
325
}
1326
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 javafx.scene.Node;
7
import javafx.scene.image.Image;
8
import javafx.scene.image.ImageView;
9
import org.controlsfx.glyphfont.FontAwesome;
10
import org.controlsfx.glyphfont.Glyph;
11
12
import java.awt.*;
13
import java.awt.image.BufferedImage;
14
import java.io.IOException;
15
import java.io.InputStream;
16
import java.nio.file.Path;
17
import java.nio.file.attribute.BasicFileAttributes;
18
import java.util.HashMap;
19
import java.util.Map;
20
21
import static com.keenwrite.events.StatusEvent.clue;
22
import static com.keenwrite.io.MediaTypeExtension.MEDIA_UNDEFINED;
23
import static com.keenwrite.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER;
24
import static com.keenwrite.preview.SvgRasterizer.rasterize;
25
import static java.awt.Font.*;
26
import static java.nio.file.Files.readAttributes;
27
import static javafx.embed.swing.SwingFXUtils.toFXImage;
28
import static org.apache.commons.io.FilenameUtils.getExtension;
29
import static org.controlsfx.glyphfont.FontAwesome.Glyph.valueOf;
30
31
/**
32
 * Responsible for creating FontAwesome glyphs and graphics.
33
 */
34
public class IconFactory {
35
  /**
36
   * File icon height, in pixels.
37
   */
38
  private static final int ICON_HEIGHT = 16;
39
40
  /**
41
   * Singleton to prevent re-loading the TTF file.
42
   */
43
  private static final FontAwesome FONT_AWESOME = new FontAwesome();
44
45
  /**
46
   * Caches file type icons encountered.
47
   */
48
  private static final Map<String, Image> ICONS = new HashMap<>();
49
50
  /**
51
   * Create a {@link Node} representation for the given icon name.
52
   *
53
   * @param icon Name of icon to convert to a UI object (case-insensitive).
54
   * @return A UI object suitable for display.
55
   */
56
  public static Node createGraphic( final String icon ) {
57
    assert icon != null;
58
59
    // Return a label glyph.
60
    return icon.isEmpty()
61
      ? new Glyph()
62
      : createGlyph( icon );
63
  }
64
65
  /**
66
   * Create a {@link Node} representation for the given FontAwesome glyph.
67
   *
68
   * @param glyph The glyph to convert to a {@link Node}.
69
   * @return The given glyph as a text label.
70
   */
71
  public static Node createGraphic( final FontAwesome.Glyph glyph ) {
72
    return FONT_AWESOME.create( glyph );
73
  }
74
75
  /**
76
   * Creates a suitable {@link Node} icon representation for the given file.
77
   * This will first look up the {@link MediaType} before matching based on
78
   * the file name extension.
79
   *
80
   * @param path The file to represent graphically.
81
   * @return An icon representation for the given file.
82
   */
83
  public static ImageView createFileIcon( final Path path ) throws IOException {
84
    final var attrs = readAttributes( path, BasicFileAttributes.class );
85
    final var filename = path.getFileName().toString();
86
    String extension;
87
88
    if( "..".equals( filename ) ) {
89
      extension = "folder-up";
90
    }
91
    else if( attrs.isDirectory() ) {
92
      extension = "folder";
93
    }
94
    else if( attrs.isSymbolicLink() ) {
95
      extension = "folder-link";
96
    }
97
    else {
98
      final var mediaType = MediaType.valueFrom( path );
99
      final var mte = MediaTypeExtension.valueFrom( mediaType );
100
101
      // if the file extension is not known to the app, try loading an icon
102
      // that corresponds to the extension directly.
103
      extension = mte == MEDIA_UNDEFINED
104
        ? getExtension( filename )
105
        : mte.getExtension();
106
    }
107
108
    if( extension == null ) {
109
      extension = "";
110
    }
111
    else {
112
      extension = extension.toLowerCase();
113
    }
114
115
    // Each cell in the table must have a distinct parent, so the image views
116
    // cannot be reused. The underlying buffered image can be cached, though.
117
    final var image =
118
      ICONS.computeIfAbsent( extension, IconFactory::createFxImage );
119
    final var imageView = new ImageView();
120
    imageView.setPreserveRatio( true );
121
    imageView.setFitHeight( ICON_HEIGHT );
122
    imageView.setImage( image );
123
124
    return imageView;
125
  }
126
127
  private static Image createFxImage( final String extension ) {
128
    return toFXImage( createImage( extension ), null );
129
  }
130
131
  private static BufferedImage createImage( final String extension ) {
132
    try( final var icon = open( "icons/" + extension + ".svg" ) ) {
133
      if( icon == null ) {
134
        throw new IllegalArgumentException( extension );
135
      }
136
137
      return rasterize( icon );
138
    } catch( final Exception ex ) {
139
      clue( ex );
140
141
      // If the extension was unknown, fall back to a blank icon, falling
142
      // back again to a broken image if blank cannot be found (to avoid
143
      // infinite recursion).
144
      return "blank".equals( extension )
145
        ? BROKEN_IMAGE_PLACEHOLDER
146
        : createImage( "blank" );
147
    }
148
  }
149
150
  private static InputStream open( final String resource ) {
151
    return IconFactory.class.getResourceAsStream( resource );
152
  }
153
154
  /**
155
   * Returns the font to use when adding icons to the UI.
156
   *
157
   * @param size The font size to use when drawing the icon.
158
   * @return A font containing numerous icons.
159
   */
160
  public static Font getIconFont( final int size ) {
161
    try( final var fontStream = openFont() ) {
162
      final var font = createFont( TRUETYPE_FONT, fontStream );
163
      return font.deriveFont( PLAIN, size );
164
    } catch( final Exception e ) {
165
      // This doesn't actually work, seemingly after an upgrade to ControlsFX.
166
      // As such, creating the font and deriving it will work.
167
      return new Font( FONT_AWESOME.getName(), PLAIN, size );
168
    }
169
  }
170
171
  /**
172
   * This re-reads the {@link FontAwesome} font TTF resource. For a reason
173
   * not yet investigated, the font doesn't appear to be accessible to the
174
   * application. This may have happened during an upgrade to ControlsFX.
175
   * Callers are responsible for closing the stream.
176
   *
177
   * @return A stream containing font TrueType glyph information.
178
   */
179
  private static InputStream openFont() {
180
    return FontAwesome.class.getResourceAsStream( "fontawesome-webfont.ttf" );
181
  }
182
183
  private static Node createGlyph( final String icon ) {
184
    return createGraphic( valueOf( icon.toUpperCase() ) );
185
  }
186
187
  /**
188
   * Prevent instantiation. Use the {@link #createGraphic(String)} method to
189
   * create an icon for display.
190
   */
191
  private IconFactory() {}
192
}
1193
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.whitemagicsoftware.wordcount.TokenizerException;
8
import javafx.beans.property.IntegerProperty;
9
import javafx.beans.property.SimpleIntegerProperty;
10
import javafx.beans.property.SimpleStringProperty;
11
import javafx.beans.property.StringProperty;
12
import javafx.collections.ObservableList;
13
import javafx.collections.transformation.SortedList;
14
import javafx.scene.control.TableColumn;
15
import javafx.scene.control.TableView;
16
import org.greenrobot.eventbus.Subscribe;
17
18
import static com.keenwrite.events.Bus.register;
19
import static com.keenwrite.events.StatusEvent.clue;
20
import static com.keenwrite.preferences.AppKeys.KEY_LANGUAGE_LOCALE;
21
import static com.keenwrite.preferences.AppKeys.KEY_UI_FONT_EDITOR_NAME;
22
import static com.keenwrite.ui.heuristics.DocumentStatistics.StatEntry;
23
import static java.lang.String.format;
24
import static javafx.application.Platform.runLater;
25
import static javafx.collections.FXCollections.observableArrayList;
26
27
/**
28
 * Responsible for displaying document statistics, such as word count and
29
 * word frequency.
30
 */
31
public final class DocumentStatistics extends TableView<StatEntry> {
32
33
  private WordCounter mWordCounter;
34
  private final ObservableList<StatEntry> mItems = observableArrayList();
35
36
  /**
37
   * Creates a new observer of document change events that will gather and
38
   * display document statistics (e.g., word counts).
39
   *
40
   * @param workspace Settings used to configure the statistics engine.
41
   */
42
  public DocumentStatistics( final Workspace workspace ) {
43
    mWordCounter = WordCounter.create( workspace.getLocale() );
44
45
    final var sortedItems = new SortedList<>( mItems );
46
    sortedItems.comparatorProperty().bind( comparatorProperty() );
47
    setItems( sortedItems );
48
49
    initView();
50
    initListeners( workspace );
51
    register( this );
52
53
    final var fontName = workspace.stringProperty( KEY_UI_FONT_EDITOR_NAME );
54
55
    fontName.addListener(
56
      ( c, o, n ) -> {
57
        if( n != null ) {
58
          setFontFamily( n );
59
        }
60
      }
61
    );
62
63
    setFontFamily( fontName.getValue() );
64
  }
65
66
  /**
67
   * Called when the hash code for the current document changes. This happens
68
   * when non-collapsable-whitespace is added to the document. When the
69
   * document is sent for rendering, the parsed document is converted to text.
70
   * If that text differs in its hash code, then this method is called. The
71
   * implication is that all variables and executable statements have been
72
   * replaced. An event bus subscriber is used so that text processing occurs
73
   * outside the UI processing threads.
74
   *
75
   * @param event Container for the document text that has changed.
76
   */
77
  @Subscribe
78
  public void handle( final DocumentChangedEvent event ) {
79
    try {
80
      runLater( () -> {
81
        mItems.clear();
82
        final var document = event.getDocument();
83
        final var wordCount = mWordCounter.count(
84
          document, ( k, count ) -> {
85
            // Generate statistics for words that occur thrice or more.
86
            if( count > 2 ) {
87
              mItems.add( new StatEntry( k, count ) );
88
            }
89
          }
90
        );
91
92
        WordCountEvent.fire( wordCount );
93
      } );
94
    } catch( final TokenizerException ex ) {
95
      clue( ex );
96
    }
97
  }
98
99
  @SuppressWarnings( "unchecked" )
100
  private void initView() {
101
    final TableColumn<StatEntry, String> colWord = createColumn( "Word" );
102
    final TableColumn<StatEntry, Number> colCount = createColumn( "Count" );
103
104
    colWord.setCellValueFactory( stat -> stat.getValue().wordProperty() );
105
    colCount.setCellValueFactory( stat -> stat.getValue().tallyProperty() );
106
    colCount.setComparator( colCount.getComparator().reversed() );
107
108
    final var columns = getColumns();
109
    columns.add( colWord );
110
    columns.add( colCount );
111
112
    setMaxWidth( Double.MAX_VALUE );
113
    setPrefWidth( 128 );
114
    setColumnResizePolicy( CONSTRAINED_RESIZE_POLICY );
115
    getSortOrder().setAll( colCount, colWord );
116
117
    getStyleClass().add( "" );
118
  }
119
120
  private void initListeners( final Workspace workspace ) {
121
    final var property = workspace.localeProperty( KEY_LANGUAGE_LOCALE );
122
    property.addListener(
123
      ( c, o, n ) -> mWordCounter = WordCounter.create( property.toLocale() )
124
    );
125
  }
126
127
  private <E, T> TableColumn<E, T> createColumn( final String key ) {
128
    return new TableColumn<>( key );
129
  }
130
131
  private void setFontFamily( final String value ) {
132
    runLater( () -> setStyle( format( "-fx-font-family:'%s';", value ) ) );
133
  }
134
135
  /**
136
   * Represents the number of times a word appears in a document.
137
   */
138
  protected static final class StatEntry {
139
    private final StringProperty mWord;
140
    private final IntegerProperty mTally;
141
142
    public StatEntry( final String word, final int tally ) {
143
      mWord = new SimpleStringProperty( word );
144
      mTally = new SimpleIntegerProperty( tally );
145
    }
146
147
    private StringProperty wordProperty() {
148
      return mWord;
149
    }
150
151
    private IntegerProperty tallyProperty() {
152
      return mTally;
153
    }
154
  }
155
}
1156
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.wordcount.Tokenizer;
5
import com.whitemagicsoftware.wordcount.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.clipboard.Clipboard;
6
import javafx.beans.property.SimpleStringProperty;
7
import javafx.beans.property.StringProperty;
8
import javafx.collections.ObservableList;
9
import javafx.scene.control.*;
10
import javafx.scene.input.KeyCodeCombination;
11
import javafx.stage.Stage;
12
import org.greenrobot.eventbus.Subscribe;
13
14
import java.time.LocalDateTime;
15
import java.util.Objects;
16
import java.util.TreeSet;
17
18
import static com.keenwrite.Messages.get;
19
import static com.keenwrite.constants.Constants.ACTION_PREFIX;
20
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
21
import static com.keenwrite.events.Bus.register;
22
import static com.keenwrite.events.StatusEvent.clue;
23
import static java.time.LocalDateTime.now;
24
import static java.time.format.DateTimeFormatter.ofPattern;
25
import static javafx.application.Platform.runLater;
26
import static javafx.collections.FXCollections.observableArrayList;
27
import static javafx.event.ActionEvent.ACTION;
28
import static javafx.scene.control.Alert.AlertType.INFORMATION;
29
import static javafx.scene.control.ButtonType.OK;
30
import static javafx.scene.control.SelectionMode.MULTIPLE;
31
import static javafx.scene.input.KeyCode.C;
32
import static javafx.scene.input.KeyCode.INSERT;
33
import static javafx.scene.input.KeyCombination.CONTROL_ANY;
34
import static javafx.stage.Modality.NONE;
35
36
/**
37
 * Responsible for logging application issues to {@link TableView} entries.
38
 */
39
public final class LogView extends Alert {
40
  /**
41
   * Number of error messages to retain in the {@link TableView}; must be
42
   * greater than zero. Typesetting the document can cause many page number
43
   * messages to be logged.
44
   */
45
  private static final int CACHE_SIZE = 500;
46
47
  private final ObservableList<LogEntry> mItems = observableArrayList();
48
  private final TableView<LogEntry> mTable = new TableView<>( mItems );
49
50
  public LogView() {
51
    super( INFORMATION );
52
    setTitle( get( ACTION_PREFIX + "view.log.text" ) );
53
    initModality( NONE );
54
    initTableView();
55
    setResizable( true );
56
    initButtons();
57
    initIcon();
58
    initActions();
59
    register( this );
60
  }
61
62
  @Subscribe
63
  public void log( final StatusEvent event ) {
64
    runLater( () -> {
65
      final var logEntry = new LogEntry( event );
66
67
      if( !mItems.contains( logEntry ) ) {
68
        mItems.add( logEntry );
69
70
        while( mItems.size() > CACHE_SIZE ) {
71
          mItems.remove( 0 );
72
        }
73
74
        mTable.scrollTo( logEntry );
75
      }
76
    } );
77
  }
78
79
  /**
80
   * Brings the dialog to the foreground, showing it if needed.
81
   */
82
  public void view() {
83
    super.show();
84
    getStage().toFront();
85
  }
86
87
  /**
88
   * Removes all the entries from the list.
89
   */
90
  public void clear() {
91
    mItems.clear();
92
    clue();
93
  }
94
95
  private void initTableView() {
96
    final var ctrlC = new KeyCodeCombination( C, CONTROL_ANY );
97
    final var ctrlInsert = new KeyCodeCombination( INSERT, CONTROL_ANY );
98
99
    final var colDate = new TableColumn<LogEntry, String>( "Timestamp" );
100
    final var colMessage = new TableColumn<LogEntry, String>( "Message" );
101
    final var colTrace = new TableColumn<LogEntry, String>( "Trace" );
102
103
    colDate.setCellValueFactory( log -> log.getValue().dateProperty() );
104
    colMessage.setCellValueFactory( log -> log.getValue().messageProperty() );
105
    colTrace.setCellValueFactory( log -> log.getValue().traceProperty() );
106
107
    final var columns = mTable.getColumns();
108
    columns.add( colDate );
109
    columns.add( colMessage );
110
    columns.add( colTrace );
111
112
    mTable.setMaxWidth( Double.MAX_VALUE );
113
    mTable.setPrefWidth( 1024 );
114
    mTable.getSelectionModel().setSelectionMode( MULTIPLE );
115
    mTable.setOnKeyPressed( event -> {
116
      if( ctrlC.match( event ) || ctrlInsert.match( event ) ) {
117
        copyToClipboard( mTable );
118
      }
119
    } );
120
121
    final var pane = getDialogPane();
122
    pane.setContent( mTable );
123
  }
124
125
  private void initButtons() {
126
    final var pane = getDialogPane();
127
    final var CLEAR = new ButtonType( "CLEAR" );
128
    pane.getButtonTypes().add( CLEAR );
129
130
    final var buttonOk = (Button) pane.lookupButton( OK );
131
    final var buttonClear = (Button) pane.lookupButton( CLEAR );
132
133
    buttonOk.setDefaultButton( true );
134
    buttonClear.addEventFilter( ACTION, event -> {
135
      clear();
136
      event.consume();
137
    } );
138
139
    pane.setOnKeyReleased( t -> {
140
      switch( t.getCode() ) {
141
        case ENTER, ESCAPE -> buttonOk.fire();
142
      }
143
    } );
144
  }
145
146
  private void initIcon() {
147
    getStage().getIcons().add( ICON_DIALOG );
148
  }
149
150
  private void initActions() {
151
    final var stage = getStage();
152
    stage.setOnCloseRequest( event -> stage.hide() );
153
  }
154
155
  private Stage getStage() {
156
    return (Stage) getDialogPane().getScene().getWindow();
157
  }
158
159
  private static final class LogEntry {
160
    private final StringProperty mDate;
161
    private final StringProperty mMessage;
162
    private final StringProperty mTrace;
163
164
    /**
165
     * Constructs a new {@link LogEntry} for the current time.
166
     */
167
    public LogEntry( final StatusEvent event ) {
168
      mDate = new SimpleStringProperty( toString( now() ) );
169
      mMessage = new SimpleStringProperty( event.getMessage() );
170
      mTrace = new SimpleStringProperty( event.getProblem() );
171
    }
172
173
    private StringProperty messageProperty() {
174
      return mMessage;
175
    }
176
177
    private StringProperty dateProperty() {
178
      return mDate;
179
    }
180
181
    private StringProperty traceProperty() {
182
      return mTrace;
183
    }
184
185
    @Override
186
    public boolean equals( final Object o ) {
187
      if( this == o ) { return true; }
188
      if( o == null || getClass() != o.getClass() ) { return false; }
189
190
      return Objects.equals( mMessage.get(), ((LogEntry) o).mMessage.get() );
191
    }
192
193
    @Override
194
    public int hashCode() {
195
      return mMessage != null ? mMessage.hashCode() : 0;
196
    }
197
198
    @Override
199
    public String toString() {
200
      final var date = mDate == null ? "" : mDate.get();
201
      final var message = mMessage == null ? "" : mMessage.get();
202
      final var trace = mTrace == null ? "" : mTrace.get();
203
204
      return "LogEntry{" +
205
        "mDate=" + (date == null ? "''" : date) +
206
        ", mMessage=" + (message == null ? "''" : message) +
207
        ", mTrace=" + (trace == null ? "''" : trace) +
208
        '}';
209
    }
210
211
    private String toString( final LocalDateTime date ) {
212
      return date.format( ofPattern( "d MMM u HH:mm:ss" ) );
213
    }
214
  }
215
216
  /**
217
   * Copies the contents of the selected rows into the clipboard; code is from
218
   * <a href="https://stackoverflow.com/a/48126059/59087">StackOverflow</a>.
219
   *
220
   * @param table The {@link TableView} having selected rows to copy.
221
   */
222
  public void copyToClipboard( final TableView<?> table ) {
223
    final var sb = new StringBuilder();
224
    final var rows = new TreeSet<Integer>();
225
    boolean firstRow = true;
226
227
    for( final var position : table.getSelectionModel().getSelectedCells() ) {
228
      rows.add( position.getRow() );
229
    }
230
231
    for( final var row : rows ) {
232
      if( !firstRow ) {
233
        sb.append( '\n' );
234
      }
235
236
      firstRow = false;
237
      boolean firstCol = true;
238
239
      for( final var column : table.getColumns() ) {
240
        if( !firstCol ) {
241
          sb.append( '\t' );
242
        }
243
244
        firstCol = false;
245
        final var data = column.getCellData( row );
246
        sb.append( data == null ? "" : data.toString() );
247
      }
248
    }
249
250
    Clipboard.write( sb );
251
  }
252
}
1253
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().size();
81
82
      paraId = Math.min( paraId + 1, paragraphs - 1 );
83
      paragraph = editor.getParagraph( paraId );
84
      text = paragraph.getText();
85
    }
86
87
    // Prevent doubling-up styles.
88
    editor.clearStyle( paraId );
89
90
    spellcheck( editor, text, paraId );
91
  }
92
93
  /**
94
   * Spellchecks a subset of the entire document.
95
   *
96
   * @param editor The document (or portions thereof) to spellcheck.
97
   * @param text   Look up words for this text in the lexicon.
98
   * @param paraId Set to -1 to apply resulting style spans to the entire text.
99
   */
100
  private void spellcheck(
101
    final StyleClassedTextArea editor, final String text, final int paraId ) {
102
    final var builder = new StyleSpansBuilder<Collection<String>>();
103
    final var runningIndex = new AtomicInteger( 0 );
104
105
    // The text nodes must be relayed through a contextual "visitor" that
106
    // can return text in chunks with correlative offsets into the string.
107
    // This allows Markdown and R Markdown documents to return sets of
108
    // words to check.
109
    final var node = mParser.parse( text );
110
    final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> {
111
      // Treat hyphenated compound words as individual words.
112
      final var check = visited.replace( '-', ' ' );
113
      final var checker = getSpellChecker();
114
115
      checker.proofread( check, ( misspelled, prevIndex, currIndex ) -> {
116
        prevIndex += bIndex;
117
        currIndex += bIndex;
118
119
        // Clear styling between lexiconically absent words.
120
        builder.add( emptyList(), prevIndex - runningIndex.get() );
121
        builder.add( singleton( "spelling" ), currIndex - prevIndex );
122
        runningIndex.set( currIndex );
123
      } );
124
    } );
125
126
    visitor.visit( node );
127
128
    // If the running index was set, at least one word triggered the listener.
129
    if( runningIndex.get() > 0 ) {
130
      // Clear styling after the last lexiconically absent word.
131
      builder.add( emptyList(), text.length() - runningIndex.get() );
132
133
      final var spans = builder.create();
134
135
      if( paraId >= 0 ) {
136
        editor.setStyleSpans( paraId, 0, spans );
137
      }
138
      else {
139
        editor.setStyleSpans( 0, spans );
140
      }
141
    }
142
  }
143
144
  /**
145
   * Called to display a pop-up with a list of spelling corrections. When the
146
   * user selects an item from the list, the word at the caret position is
147
   * replaced (with the selected item).
148
   */
149
  public void autofix( final TextEditor editor ) {
150
    final var caretWord = editor.getCaretWord();
151
    final var textArea = editor.getTextArea();
152
    final var word = textArea.getText( caretWord );
153
    final var suggestions = checkWord( word, 10 );
154
155
    if( suggestions.isEmpty() ) {
156
      clue( "Editor.spelling.check.matches.none", word );
157
    }
158
    else if( !suggestions.contains( word ) ) {
159
      final var menu = createSuggestionsPopup( textArea );
160
      final var items = menu.getItems();
161
      textArea.setContextMenu( menu );
162
163
      for( final var correction : suggestions ) {
164
        items.add( createSuggestedItem( textArea, caretWord, correction ) );
165
      }
166
167
      textArea.getCaretBounds().ifPresent(
168
        bounds -> {
169
          menu.setOnShown( event -> menu.requestFocus() );
170
          menu.show( textArea, bounds.getCenterX(), bounds.getCenterY() );
171
        }
172
      );
173
    }
174
    else {
175
      clue( "Editor.spelling.check.matches.okay", word );
176
    }
177
  }
178
179
  private ContextMenu createSuggestionsPopup(
180
    final StyleClassedTextArea textArea ) {
181
    final var menu = new ContextMenu();
182
183
    menu.setAutoHide( true );
184
    menu.setHideOnEscape( true );
185
    menu.setOnHidden( event -> textArea.setContextMenu( null ) );
186
187
    return menu;
188
  }
189
190
  /**
191
   * Creates a menu item capable of replacing a word under the cursor.
192
   *
193
   * @param textArea The text upon which this action will replace.
194
   * @param i        The beginning and ending text offset to replace.
195
   * @param s        The text to replace at the given offset.
196
   * @return The menu item that, if actioned, will replace the text.
197
   */
198
  private MenuItem createSuggestedItem(
199
    final StyleClassedTextArea textArea,
200
    final IndexRange i,
201
    final String s ) {
202
    final var menuItem = new MenuItem( s );
203
204
    menuItem.setOnAction( event -> textArea.replaceText( i, s ) );
205
206
    return menuItem;
207
  }
208
209
  /**
210
   * Returns a list of suggests for the given word. This is typically used to
211
   * check for suitable replacements of the word at the caret position.
212
   *
213
   * @param word  The word to spellcheck.
214
   * @param count The maximum number of suggested alternatives to return.
215
   * @return A list of recommended spellings for the given word.
216
   */
217
  public List<String> checkWord( final String word, final int count ) {
218
    return getSpellChecker().suggestions( word, count );
219
  }
220
221
  private SpellChecker getSpellChecker() {
222
    return mSpellChecker.get();
223
  }
224
225
  private static final class TextVisitor {
226
    private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>(
227
      com.vladsch.flexmark.ast.Text.class, this::visit )
228
    );
229
230
    private final SpellCheckListener mConsumer;
231
232
    public TextVisitor( final SpellCheckListener consumer ) {
233
      mConsumer = consumer;
234
    }
235
236
    private void visit( final com.vladsch.flexmark.util.ast.Node node ) {
237
      if( node instanceof com.vladsch.flexmark.ast.Text ) {
238
        mConsumer.accept( node.getChars().toString(),
239
                          node.getStartOffset(),
240
                          node.getEndOffset() );
241
      }
242
243
      mVisitor.visitChildren( node );
244
    }
245
  }
246
}
1247
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.util.Comparator;
33
34
import static java.lang.Character.isDigit;
35
36
/**
37
 * Responsible for sorting lists that may contain numeric values. Usage:
38
 * <pre>
39
 *   Collections.sort(list, new AlphanumComparator());
40
 * </pre>
41
 * <p>
42
 * Where "list" is the list to sort alphanumerically, not lexicographically.
43
 * </p>
44
 */
45
public final class AlphanumComparator<T> implements Comparator<T> {
46
  /**
47
   * Returns a chunk of text that is continuous with respect to digits or
48
   * non-digits.
49
   *
50
   * @param s      The string to compare.
51
   * @param length The string length, for improved efficiency.
52
   * @param marker The current index into a subset of the given string.
53
   * @return The substring {@code s} that is a continuous text chunk of the
54
   * same character type.
55
   */
56
  private StringBuilder chunk( final String s, final int length, int marker ) {
57
    assert s != null;
58
    assert length >= 0;
59
    assert marker < length;
60
61
    // Prevent any possible memory re-allocations by using the length.
62
    final var chunk = new StringBuilder( length );
63
    var c = s.charAt( marker );
64
    final var chunkType = isDigit( c );
65
66
    // While the character at the current position is the same type (numeric or
67
    // alphabetic), append the character to the current chunk.
68
    while( marker < length &&
69
      isDigit( c = s.charAt( marker++ ) ) == chunkType ) {
70
      chunk.append( c );
71
    }
72
73
    return chunk;
74
  }
75
76
  /**
77
   * Performs an alphanumeric comparison of two strings, sorting numerically
78
   * first when numbers are found within the string. If either argument is
79
   * {@code null}, this will return zero.
80
   *
81
   * @param o1 The object to compare against {@code s2}, converted to string.
82
   * @param o2 The object to compare against {@code s1}, converted to string.
83
   * @return a negative integer, zero, or a positive integer if the first
84
   * argument is less than, equal to, or greater than the second, respectively.
85
   */
86
  @Override
87
  public int compare( final T o1, final T o2 ) {
88
    if( o1 == null || o2 == null ) {
89
      return 0;
90
    }
91
92
    final var s1 = o1.toString();
93
    final var s2 = o2.toString();
94
    final var s1Length = s1.length();
95
    final var s2Length = s2.length();
96
97
    var thisMarker = 0;
98
    var thatMarker = 0;
99
100
    while( thisMarker < s1Length && thatMarker < s2Length ) {
101
      final var thisChunk = chunk( s1, s1Length, thisMarker );
102
      final var thisChunkLength = thisChunk.length();
103
      thisMarker += thisChunkLength;
104
      final var thatChunk = chunk( s2, s2Length, thatMarker );
105
      final var thatChunkLength = thatChunk.length();
106
      thatMarker += thatChunkLength;
107
108
      // If both chunks contain numeric characters, sort them numerically
109
      int result;
110
111
      if( isDigit( thisChunk.charAt( 0 ) ) &&
112
        isDigit( thatChunk.charAt( 0 ) ) ) {
113
        // If equal, the first different number counts
114
        if( (result = thisChunkLength - thatChunkLength) == 0 ) {
115
          for( var i = 0; i < thisChunkLength; i++ ) {
116
            result = thisChunk.charAt( i ) - thatChunk.charAt( i );
117
118
            if( result != 0 ) {
119
              return result;
120
            }
121
          }
122
        }
123
      }
124
      else {
125
        result = thisChunk.compareTo( thatChunk );
126
      }
127
128
      if( result != 0 ) {
129
        return result;
130
      }
131
    }
132
133
    return s1Length - s2Length;
134
  }
135
}
1136
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.List;
5
import java.util.ListIterator;
6
import java.util.NoSuchElementException;
7
8
/**
9
 * Responsible for iterating over a list either forwards or backwards. When
10
 * the iterator reaches the last element in the list, the next element will
11
 * be the first. When the iterator reaches the first element in the list,
12
 * the previous element will be the last.
13
 * <p>
14
 * Due to the ability to move forwards and backwards through the list, rather
15
 * than force client classes to track the list index independently, this
16
 * iterator provides an accessor to the index. The index is zero-based.
17
 * </p>
18
 *
19
 * @param <T> The type of list to be cycled.
20
 */
21
public final class CyclicIterator<T> implements ListIterator<T> {
22
  private final List<T> mList;
23
24
  /**
25
   * Initialize to an invalid index so that the first calls to either
26
   * {@link #previous()} or {@link #next()} will return the starting or ending
27
   * element.
28
   */
29
  private int mIndex = -1;
30
31
  /**
32
   * Creates an iterator that cycles indefinitely through the given list.
33
   *
34
   * @param list The list to cycle through indefinitely.
35
   */
36
  public CyclicIterator( final List<T> list ) {
37
    mList = list;
38
  }
39
40
  /**
41
   * @return {@code true} if there is at least one element.
42
   */
43
  @Override
44
  public boolean hasNext() {
45
    return !mList.isEmpty();
46
  }
47
48
  /**
49
   * @return {@code true} if there is at least one element.
50
   */
51
  @Override
52
  public boolean hasPrevious() {
53
    return !mList.isEmpty();
54
  }
55
56
  @Override
57
  public int nextIndex() {
58
    return computeIndex( +1 );
59
  }
60
61
  @Override
62
  public int previousIndex() {
63
    return computeIndex( -1 );
64
  }
65
66
  @Override
67
  public void remove() {
68
    mList.remove( mIndex );
69
  }
70
71
  @Override
72
  public void set( final T t ) {
73
    mList.set( mIndex, t );
74
  }
75
76
  @Override
77
  public void add( final T t ) {
78
    mList.add( mIndex, t );
79
  }
80
81
  /**
82
   * Returns the next item in the list, which will cycle to the first
83
   * item as necessary.
84
   *
85
   * @return The next item in the list, cycling to the start if needed.
86
   */
87
  @Override
88
  public T next() {
89
    return cycle( +1 );
90
  }
91
92
  /**
93
   * Returns the previous item in the list, which will cycle to the last
94
   * item as necessary.
95
   *
96
   * @return The previous item in the list, cycling to the end if needed.
97
   */
98
  @Override
99
  public T previous() {
100
    return cycle( -1 );
101
  }
102
103
  /**
104
   * Cycles to the next or previous element, depending on the direction value.
105
   *
106
   * @param direction Use -1 for previous, +1 for next.
107
   * @return The next or previous item in the list.
108
   */
109
  private T cycle( final int direction ) {
110
    try {
111
      return mList.get( mIndex = computeIndex( direction ) );
112
    } catch( final Exception ex ) {
113
      throw new NoSuchElementException( ex );
114
    }
115
  }
116
117
  /**
118
   * Returns the index of the value retrieved from the most recent call to
119
   * either {@link #previous()} or {@link #next()}.
120
   *
121
   * @return The list item index or -1 if no calls have been made to retrieve
122
   * an item from the list.
123
   */
124
  public int getIndex() {
125
    return mIndex;
126
  }
127
128
  private int computeIndex( final int direction ) {
129
    final var i = mIndex + direction;
130
    final var size = mList.size();
131
    final var result = i < 0
132
        ? size - 1
133
        : size == 0 ? 0 : i % size;
134
135
    // Ensure the invariant holds.
136
    assert 0 <= result && result < size || size == 0;
137
138
    return result;
139
  }
140
}
1141
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() );
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,
23
  /**
24
   * Denotes either HTTP or HTTPS.
25
   */
26
  HTTP,
27
  /**
28
   * Denotes the File Transfer Protocol.
29
   */
30
  FTP,
31
  /**
32
   * Denotes Java archive file.
33
   */
34
  JAR,
35
  /**
36
   * Could not determine schema (or is not supported by the application).
37
   */
38
  UNKNOWN;
39
40
  /**
41
   * Returns the protocol for a given URI or file name.
42
   *
43
   * @param uri Determine the protocol for this URI or file name.
44
   * @return The protocol for the given resource.
45
   */
46
  public static ProtocolScheme getProtocol( final String uri ) {
47
    try {
48
      return getProtocol( new URI( uri ) );
49
    } catch( final Exception ex ) {
50
      // Using double-slashes is a shorthand to instruct the browser to
51
      // reference a resource using the parent URL's security model. This
52
      // is known as a protocol-relative URL.
53
      return uri.startsWith( "//" ) ? HTTP : valueFrom( new File( uri ) );
54
    }
55
  }
56
57
  /**
58
   * Returns the protocol for a given URI or file name.
59
   *
60
   * @param uri Determine the protocol for this URI or file name.
61
   * @return The protocol for the given resource.
62
   */
63
  public static ProtocolScheme getProtocol( final URI uri )
64
    throws MalformedURLException {
65
    return uri.isAbsolute()
66
      ? valueFrom( uri )
67
      : valueFrom( uri.toURL() );
68
  }
69
70
  /**
71
   * Determines the protocol scheme for a given string.
72
   *
73
   * @param protocol A string representing data encoding protocol scheme.
74
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
75
   * valid value from this enumeration.
76
   */
77
  public static ProtocolScheme valueFrom( final String protocol ) {
78
    final var sanitized = protocol == null ? "" : protocol.toUpperCase();
79
80
    for( final var scheme : values() ) {
81
      // This will match HTTP/HTTPS as well as FILE*, which may be inaccurate.
82
      if( sanitized.startsWith( scheme.name() ) ) {
83
        return scheme;
84
      }
85
    }
86
87
    return UNKNOWN;
88
  }
89
90
  /**
91
   * Determines the protocol scheme for a given {@link File}.
92
   *
93
   * @param file A file having a URI that contains a protocol scheme.
94
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
95
   * valid value from this enumeration.
96
   */
97
  public static ProtocolScheme valueFrom( final File file ) {
98
    return valueFrom( file.toURI() );
99
  }
100
101
  /**
102
   * Determines the protocol scheme for a given {@link URI}.
103
   *
104
   * @param uri A URI that contains a protocol scheme.
105
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
106
   * valid value from this enumeration.
107
   */
108
  public static ProtocolScheme valueFrom( final URI uri ) {
109
    try {
110
      return valueFrom( uri.toURL() );
111
    } catch( final Exception ex ) {
112
      clue( ex );
113
      return UNKNOWN;
114
    }
115
  }
116
117
  /**
118
   * Determines the protocol scheme for a given {@link URL}.
119
   *
120
   * @param url The {@link URL} containing a protocol scheme.
121
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
122
   * valid value from this enumeration.
123
   */
124
  public static ProtocolScheme valueFrom( final URL url ) {
125
    return valueFrom( url.getProtocol() );
126
  }
127
128
  /**
129
   * Answers whether the given {@link URL} points to a remote resource.
130
   *
131
   * @param url The {@link URL} containing a protocol scheme.
132
   * @return {@link true} if the protocol must be fetched via HTTP or FTP.
133
   */
134
  public static boolean isRemote( final URL url ) {
135
    return valueFrom( url ).isRemote();
136
  }
137
138
  /**
139
   * Answers {@code true} if the given protocol is for a local file, which
140
   * includes a JAR file.
141
   *
142
   * @return {@code false} the protocol is not a local file reference.
143
   */
144
  public boolean isFile() {
145
    return this == FILE || this == JAR;
146
  }
147
148
  /**
149
   * Answers whether the given protocol is HTTP or HTTPS.
150
   *
151
   * @return {@code true} the protocol is either HTTP or HTTPS.
152
   */
153
  public boolean isHttp() {
154
    return this == HTTP;
155
  }
156
157
  /**
158
   * Answers whether the given protocol is FTP.
159
   *
160
   * @return {@code true} the protocol is FTP.
161
   */
162
  public boolean isFtp() {
163
    return this == HTTP;
164
  }
165
166
  /**
167
   * Answers whether the given protocol represents a remote resource.
168
   *
169
   * @return {@code true} the protocol is HTTP or FTP.
170
   */
171
  public boolean isRemote() {
172
    return isHttp() || isFtp();
173
  }
174
175
  /**
176
   * Answers {@code true} if the given protocol is for a Java archive file.
177
   *
178
   * @return {@code false} the protocol is not a Java archive file.
179
   */
180
  public boolean isJar() {
181
    return this == JAR;
182
  }
183
}
1184
A src/main/java/com/keenwrite/util/RangeValidator.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.util.ArrayList;
5
import java.util.List;
6
import java.util.function.Predicate;
7
8
/**
9
 * Responsible for answering whether a given integer value falls within a
10
 * set of range specifiers. For example, if the range is "1-3, 5, 7-9, 11-",
11
 * then values of 0, 4, and 10 return {@code false} while values of 2, 5,
12
 * and 37 would return {@code true}.
13
 */
14
public final class RangeValidator implements Predicate<Integer> {
15
16
  /**
17
   * Container for a pair of integer values that can answer whether a given
18
   * value is included within the bounds provided by the pair.
19
   */
20
  private static class Range {
21
    private final int mLo;
22
    private final int mHi;
23
24
    private Range( final int lo, final int hi ) {
25
      assert lo <= hi;
26
27
      mLo = lo;
28
      mHi = hi;
29
    }
30
31
    private boolean includes( final int i ) {
32
      return mLo <= i && i <= mHi || mLo == -1 && mHi == -1;
33
    }
34
  }
35
36
  private final List<Range> mRanges = new ArrayList<>();
37
38
  /**
39
   * Creates an instance of {@link RangeValidator} that can verify whether
40
   * an integer value will fall within one of the numeric ranges in the
41
   * given listing.
42
   *
43
   * @param range The listing of ranges to validate against.
44
   */
45
  public RangeValidator( final String range ) {
46
    assert normalize( range ).equals( range );
47
48
    parse( range );
49
  }
50
51
  @Override
52
  public boolean test( final Integer integer ) {
53
    for( final var range : mRanges ) {
54
      if( range.includes( integer ) ) {
55
        return true;
56
      }
57
    }
58
59
    return false;
60
  }
61
62
  /**
63
   * Given a string meant to represent a comma-separated range of numbers,
64
   * this will ensure that the range meets the formatting requirements.
65
   *
66
   * @param range The sequences to validate (can be {@code null}).
67
   * @return The given range with all non-conforming characters removed, or
68
   * the empty string if {@code null} was provided.
69
   */
70
  public static String normalize( final String range ) {
71
    return range == null
72
      ? ""
73
      : range.matches( "^\\d+(-\\d+)?(?:,\\d+(?:-\\d+)?)*+$" )
74
      ? range
75
      : range.replaceAll( "[^-,\\d\\s]", "" );
76
  }
77
78
  /**
79
   * Populates the internal list of {@link Range} instances.
80
   *
81
   * @param s The string containing zero or more comma-separated integer
82
   *          ranges, themselves separated by hyphens.
83
   */
84
  private void parse( final String s ) {
85
    for( final var commaRange : normalize( s ).split( "," ) ) {
86
      final var hyphenRanges = commaRange.split( "-" );
87
      final Range range;
88
89
      if( hyphenRanges.length == 2 ) {
90
        final var hrlo = hyphenRanges[ 0 ].trim();
91
        final var hrhi = hyphenRanges[ 1 ].trim();
92
93
        if( hrlo.isEmpty() ) {
94
          range = new Range( 1, Integer.parseInt( hrhi ) );
95
        }
96
        else {
97
          final var lo = Integer.parseInt( hrlo );
98
          final var hi = Integer.parseInt( hrhi );
99
100
          range = new Range( lo, hi );
101
        }
102
      }
103
      else if( hyphenRanges.length == 1 ) {
104
        final var hri = hyphenRanges[ 0 ].trim();
105
106
        if( hri.isEmpty() ) {
107
          // Special case for all numbers being valid.
108
          range = new Range( -1, -1 );
109
        }
110
        else {
111
          final var i = Integer.parseInt( hyphenRanges[ 0 ].trim() );
112
          final var index = commaRange.trim().indexOf( '-' );
113
114
          // If the hyphen is to the left of the number, the range is bounded
115
          // from 0 to the number. Otherwise, the range is "unbounded" starting
116
          // at the number.
117
          if( index == -1 ) {
118
            range = new Range( i, i );
119
          }
120
          else if( index == 0 ) {
121
            range = new Range( 1, i );
122
          }
123
          else {
124
            range = new Range( i, Integer.MAX_VALUE );
125
          }
126
        }
127
      }
128
      else {
129
        // Ignore the range.
130
        range = new Range( 0, 0 );
131
      }
132
133
      mRanges.add( range );
134
    }
135
  }
136
}
1137
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
2
container.version=2.11.5
13
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
158
Main.status.error.parse=Evaluation error: {0}
159
Main.status.error.def.blank=Move the caret to a word before inserting a variable
160
Main.status.error.def.empty=Create a variable before inserting one
161
Main.status.error.def.missing=No variable value found for ''{0}''
162
Main.status.error.r=Error with [{0}...]: {1}
163
Main.status.error.file.missing=Not found: ''{0}''
164
Main.status.error.file.missing.near=Not found: ''{0}'' near line {1}
165
166
Main.status.error.messages.recursion=Lookup depth exceeded, check for loops in ''{0}''
167
Main.status.error.messages.syntax=Missing ''}'' in ''{0}''
168
169
Main.status.error.undo=Cannot undo; beginning of undo history reached
170
Main.status.error.redo=Cannot redo; end of redo history reached
171
172
Main.status.error.theme.missing=Install themes before exporting (no themes found at ''{0}'')
173
Main.status.error.theme.name=Cannot find theme name for ''{0}''
174
175
Main.status.image.request.init=Initializing HTTP request
176
Main.status.image.request.fetch=Downloaded image ''{0}''
177
Main.status.image.request.success=Determined content type ''{0}''
178
Main.status.image.request.error.media=No media type for ''{0}''
179
Main.status.image.request.error.cert=Could not accept certificate for ''{0}''
180
181
Main.status.image.xhtml.image.download=Downloading ''{0}''
182
Main.status.image.xhtml.image.resolve=Qualify path for ''{0}''
183
Main.status.image.xhtml.image.found=Found image ''{0}''
184
Main.status.image.xhtml.image.missing=Missing image ''{0}''
185
186
Main.status.font.search.missing=No font name starting with ''{0}'' was found
187
188
Main.status.export.concat=Concatenating ''{0}''
189
Main.status.export.concat.parent=No parent directory found for ''{0}''
190
Main.status.export.concat.extension=File name must have an extension ''{0}''
191
Main.status.export.concat.io=Could not read from ''{0}''
192
193
Main.status.typeset.create=Creating typesetter
194
Main.status.typeset.xhtml=Export document as XHTML
195
Main.status.typeset.began=Started typesetting ''{0}''
196
Main.status.typeset.failed=Could not generate PDF file
197
Main.status.typeset.page=Typesetting page {0} of {1} (pass {2})
198
Main.status.typeset.ended.success=Finished typesetting ''{0}'' ({1} elapsed)
199
Main.status.typeset.ended.failure=Failed to typeset ''{0}'' ({1} elapsed)
200
201
Main.status.lexicon.loading=Loading lexicon: {0} words
202
Main.status.lexicon.loaded=Loaded lexicon: {0} words
203
204
# ########################################################################
205
# Search Bar
206
# ########################################################################
207
208
Main.search.stop.tooltip=Close search bar
209
Main.search.stop.icon=CLOSE
210
Main.search.next.tooltip=Find next match
211
Main.search.next.icon=CHEVRON_DOWN
212
Main.search.prev.tooltip=Find previous match
213
Main.search.prev.icon=CHEVRON_UP
214
Main.search.find.tooltip=Search document for text
215
Main.search.find.icon=SEARCH
216
Main.search.match.none=No matches
217
Main.search.match.some={0} of {1} matches
218
219
# ########################################################################
220
# Definition Pane and its Tree View
221
# ########################################################################
222
223
Definition.menu.add.default=Undefined
224
225
# ########################################################################
226
# Variable Definitions Pane
227
# ########################################################################
228
229
Pane.definition.node.root.title=Variables
230
231
# ########################################################################
232
# HTML Preview Pane
233
# ########################################################################
234
235
Pane.preview.title=Preview
236
237
# ########################################################################
238
# Document Outline Pane
239
# ########################################################################
240
241
Pane.outline.title=Outline
242
243
# ########################################################################
244
# File Manager Pane
245
# ########################################################################
246
247
Pane.files.title=Files
248
249
# ########################################################################
250
# Document Outline Pane
251
# ########################################################################
252
253
Pane.statistics.title=Statistics
254
255
# ########################################################################
256
# Failure messages with respect to YAML files.
257
# ########################################################################
258
259
yaml.error.open=Could not open YAML file (ensure non-empty file).
260
yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''.
261
yaml.error.missing=Empty variable value for key ''{0}''.
262
yaml.error.tree.form=Unassigned variable near ''{0}''.
263
264
# ########################################################################
265
# Text Resource
266
# ########################################################################
267
268
TextResource.load.error.unsaved=The file ''{0}'' is unsaved or does not exist.
269
TextResource.load.error.permissions=The file ''{0}'' must be readable and writable.
270
271
TextResource.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
272
TextResource.saveFailed.title=Save
273
274
# ########################################################################
275
# File Open
276
# ########################################################################
277
278
Dialog.file.choose.open.title=Open File
279
Dialog.file.choose.save.title=Save File
280
Dialog.file.choose.export.title=Export File
281
Dialog.file.choose.import.title=Import File
282
283
Dialog.file.choose.filter.title.source=Source Files
284
Dialog.file.choose.filter.title.definition=Variable Files
285
Dialog.file.choose.filter.title.xml=XML Files
286
Dialog.file.choose.filter.title.all=All Files
287
288
# ########################################################################
289
# Browse File
290
# ########################################################################
291
292
BrowseFileButton.chooser.title=Open local file
293
BrowseFileButton.chooser.allFilesFilter=All Files
294
BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
295
296
# ########################################################################
297
# Browse Directory
298
# ########################################################################
299
300
BrowseDirectoryButton.chooser.title=Open local directory
301
BrowseDirectoryButton.tooltip=${BrowseDirectoryButton.chooser.title}
302
303
# ########################################################################
304
# Alert Dialog
305
# ########################################################################
306
307
Alert.file.close.title=Close
308
Alert.file.close.text=Save changes to {0}?
309
310
# ########################################################################
311
# Typesetter Installation Wizard
312
# ########################################################################
313
314
Wizard.typesetter.name=ConTeXt
315
Wizard.typesetter.container.name=Podman
316
Wizard.typesetter.container.version=4.3.1
317
Wizard.typesetter.container.checksum=b741702663234ca36e1555149721580dc31ae76985d50c022a8641c6db2f5b93
318
Wizard.typesetter.themes.version=1.8.2
319
Wizard.typesetter.themes.checksum=00e0f46ea2cb4a812a4780b61a838ea94d78167c1abc9e16767401914a5e989d
320
321
# STEP 1: Introduction panel (all)
322
Wizard.typesetter.all.1.install.title=Install typesetting system
323
Wizard.typesetter.all.1.install.header=Install typesetting system
324
Wizard.typesetter.all.1.install.about.container.link.lbl=${Wizard.typesetter.container.name}
325
Wizard.typesetter.all.1.install.about.container.link.url=https://podman.io
326
Wizard.typesetter.all.1.install.about.text.1=manages the container for the extensive
327
Wizard.typesetter.all.1.install.about.typesetter.link.lbl=${Wizard.typesetter.name}
328
Wizard.typesetter.all.1.install.about.typesetter.link.url=https://contextgarden.net
329
Wizard.typesetter.all.1.install.about.text.2=\
330
  typesetting software, which generates PDF files. This wizard\n\
331
  will guide you through the installation process. After each\n\
332
  step, you'll be prompted to click a button. Click Next to begin.
333
334
# STEP 2: Install container manager (Unix)
335
# Append steps to keep numbers stable; sorted programmatically.
336
Wizard.typesetter.unix.2.install.container.header=Install ${Wizard.typesetter.container.name} for Linux / macOS / Unix
337
# Copy button states
338
Wizard.typesetter.unix.2.install.container.copy.began=Copy
339
Wizard.typesetter.unix.2.install.container.copy.ended=Copied
340
Wizard.typesetter.unix.2.install.container.os=Operating System
341
Wizard.typesetter.unix.2.install.container.step.0=Complete the following steps to install ${Wizard.typesetter.container.name}:
342
Wizard.typesetter.unix.2.install.container.step.1=\t1. Select this computer's ${Wizard.typesetter.unix.2.install.container.os}.
343
Wizard.typesetter.unix.2.install.container.step.2=\t2. Open a new terminal.
344
Wizard.typesetter.unix.2.install.container.step.3=\t3. Run the commands provided below in the terminal.
345
Wizard.typesetter.unix.2.install.container.step.4=\t4. Click Next to continue.
346
Wizard.typesetter.unix.2.install.container.details.prefix=See
347
Wizard.typesetter.unix.2.install.container.details.link.lbl=${Wizard.typesetter.container.name}'s instructions
348
Wizard.typesetter.unix.2.install.container.details.link.url=https://podman.io/getting-started/installation
349
Wizard.typesetter.unix.2.install.container.details.suffix=for more details.
350
Wizard.typesetter.unix.2.install.container.command.distros=14
351
Wizard.typesetter.unix.2.install.container.command.os.name.01=Arch Linux & Manjaro Linux
352
Wizard.typesetter.unix.2.install.container.command.os.text.01=sudo pacman -S podman
353
Wizard.typesetter.unix.2.install.container.command.os.name.02=Alpine Linux
354
Wizard.typesetter.unix.2.install.container.command.os.text.02=sudo apk add podman
355
Wizard.typesetter.unix.2.install.container.command.os.name.03=CentOS
356
Wizard.typesetter.unix.2.install.container.command.os.text.03=sudo yum -y install podman
357
Wizard.typesetter.unix.2.install.container.command.os.name.04=Debian
358
Wizard.typesetter.unix.2.install.container.command.os.text.04=sudo apt-get -y install podman
359
Wizard.typesetter.unix.2.install.container.command.os.name.05=Fedora
360
Wizard.typesetter.unix.2.install.container.command.os.text.05=sudo dnf -y install podman
361
Wizard.typesetter.unix.2.install.container.command.os.name.06=Gentoo
362
Wizard.typesetter.unix.2.install.container.command.os.text.06=sudo emerge app-containers/podman
363
Wizard.typesetter.unix.2.install.container.command.os.name.07=OpenEmbedded
364
Wizard.typesetter.unix.2.install.container.command.os.text.07=bitbake podman
365
Wizard.typesetter.unix.2.install.container.command.os.name.08=openSUSE
366
Wizard.typesetter.unix.2.install.container.command.os.text.08=sudo zypper install podman
367
Wizard.typesetter.unix.2.install.container.command.os.name.09=RHEL7
368
Wizard.typesetter.unix.2.install.container.command.os.text.09=\
369
  sudo subscription-manager repos \
370
    --enable=rhel-7-server-extras-rpms\n\
371
  sudo yum -y install podman
372
Wizard.typesetter.unix.2.install.container.command.os.name.10=RHEL8
373
Wizard.typesetter.unix.2.install.container.command.os.text.10=\
374
  sudo yum module enable -y container-tools:rhel8\n\
375
  sudo yum module install -y container-tools:rhel8
376
Wizard.typesetter.unix.2.install.container.command.os.name.11=Ubuntu 20.10+
377
Wizard.typesetter.unix.2.install.container.command.os.text.11=\
378
  sudo apt-get -y update\n\
379
  sudo apt-get -y install podman
380
Wizard.typesetter.unix.2.install.container.command.os.name.12=Linuxmint
381
Wizard.typesetter.unix.2.install.container.command.os.text.12=${Wizard.typesetter.unix.2.install.container.command.os.text.11}
382
Wizard.typesetter.unix.2.install.container.command.os.name.13=Linuxmint LMDE
383
Wizard.typesetter.unix.2.install.container.command.os.text.13=${Wizard.typesetter.unix.2.install.container.command.os.text.04}
384
Wizard.typesetter.unix.2.install.container.command.os.name.14=macOS
385
Wizard.typesetter.unix.2.install.container.command.os.text.14=\
386
  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \n\
387
  brew install podman
388
389
# STEP 2 a: Download container manager (Windows)
390
Wizard.typesetter.win.2.download.container.header=Download ${Wizard.typesetter.container.name} for Windows
391
Wizard.typesetter.win.2.download.container.homepage.link.lbl=${Wizard.typesetter.container.name}
392
Wizard.typesetter.win.2.download.container.homepage.link.url=https://podman.io
393
Wizard.typesetter.win.2.download.container.download.link.lbl=repository
394
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
395
Wizard.typesetter.win.2.download.container.paths=Downloading {0} into {1}.
396
# suppress inspection "UnusedMessageFormatParameter"
397
Wizard.typesetter.win.2.download.container.status.bytes=Downloaded {1} bytes (size unknown).
398
Wizard.typesetter.win.2.download.container.status.progress=Downloaded {0} % of {1} bytes.
399
Wizard.typesetter.win.2.download.container.status.checksum.ok=File {0} exists. Click Next to continue.
400
Wizard.typesetter.win.2.download.container.status.checksum.no=Integrity check failed, {0} may be corrupt.
401
Wizard.typesetter.win.2.download.container.status.success=Download successful. Click Next to continue.
402
Wizard.typesetter.win.2.download.container.status.failure=Download failed. Check network then click Previous to try again.
403
404
# STEP 2 b: Install container manager (Windows)
405
Wizard.typesetter.win.2.install.container.header=Install ${Wizard.typesetter.container.name} for Windows
406
Wizard.typesetter.win.2.install.container.step.0=Complete the following steps to install ${Wizard.typesetter.container.name}:
407
Wizard.typesetter.win.2.install.container.step.1=\t1. Open the task bar.
408
Wizard.typesetter.win.2.install.container.step.2=\t2. Click the shield icon to grant permissions.
409
Wizard.typesetter.win.2.install.container.step.3=\t3. Click Yes in the User Account Control dialog to install.
410
Wizard.typesetter.win.2.install.container.status.running=Installing ...
411
Wizard.typesetter.win.2.install.container.status.success=Installation successful.\nClick Next to continue.
412
Wizard.typesetter.win.2.install.container.status.failure=Installation failed with exit code {0}.
413
Wizard.typesetter.win.2.install.container.status.unknown=Could not determine installer file type: {0}
414
415
# STEP 2: Install container manager (Universal, undetected operating system)
416
Wizard.typesetter.all.2.install.container.header=Install ${Wizard.typesetter.container.name}
417
Wizard.typesetter.all.2.install.container.homepage.lbl=${Wizard.typesetter.container.name}
418
Wizard.typesetter.all.2.install.container.homepage.url=https://podman.io
419
420
# STEP 3: Initialize container manager (all except Linux)
421
Wizard.typesetter.all.3.install.container.header=Initialize ${Wizard.typesetter.container.name}
422
Wizard.typesetter.all.3.install.container.correct=${Wizard.typesetter.container.name} initialized.\nClick Next to continue.
423
Wizard.typesetter.all.3.install.container.missing=Install ${Wizard.typesetter.container.name} before continuing.
424
425
# STEP 4: Install typesetter container image (all)
426
Wizard.typesetter.all.4.download.image.header=Download ${Wizard.typesetter.name} image
427
Wizard.typesetter.all.4.download.image.correct=Download successful.\nClick Next to continue.
428
Wizard.typesetter.all.4.download.image.missing=Install ${Wizard.typesetter.container.name} before continuing.
429
430
# STEP 5: Download typesetter themes (all)
431
Wizard.typesetter.all.5.download.themes.header=Download ${Wizard.typesetter.name} themes
432
Wizard.typesetter.all.5.download.themes.download.link.lbl=repository
433
Wizard.typesetter.all.5.download.themes.download.link.url=https://github.com/DaveJarvis/keenwrite-themes/releases/download/${Wizard.typesetter.themes.version}/theme-pack.zip
434
Wizard.typesetter.all.5.download.themes.paths=Downloading {0} into {1}.
435
Wizard.typesetter.all.5.download.themes.status.bytes=Downloaded {0} bytes (size unknown).
436
Wizard.typesetter.all.5.download.themes.status.progress=Downloaded {0} % of {1} bytes.
437
Wizard.typesetter.all.5.download.themes.status.checksum.ok=File {0} exists. Click Finish to continue.
438
Wizard.typesetter.all.5.download.themes.status.checksum.no=Integrity check failed, {0} may be corrupt.
439
Wizard.typesetter.all.5.download.themes.status.success=Download successful. Click Finish to continue.
440
Wizard.typesetter.all.5.download.themes.status.failure=Download failed. Check network then click Previous to try again.
441
442
# ########################################################################
443
# Image Dialog
444
# ########################################################################
445
446
Dialog.image.title=Image
447
Dialog.image.chooser.imagesFilter=Images
448
Dialog.image.previewLabel.text=Markdown Preview\:
449
Dialog.image.textLabel.text=Alternate Text\:
450
Dialog.image.titleLabel.text=Title (tooltip)\:
451
Dialog.image.urlLabel.text=Image URL\:
452
453
# ########################################################################
454
# Hyperlink Dialog
455
# ########################################################################
456
457
Dialog.link.title=Link
458
Dialog.link.previewLabel.text=Markdown Preview\:
459
Dialog.link.textLabel.text=Link Text\:
460
Dialog.link.titleLabel.text=Title (tooltip)\:
461
Dialog.link.urlLabel.text=Link URL\:
462
463
# ########################################################################
464
# Typesetting Settings Dialog
465
# ########################################################################
466
467
Dialog.typesetting.settings.title=Typesetting export settings
468
Dialog.typesetting.settings.header.single=Export current document
469
Dialog.typesetting.settings.theme=Theme
470
Dialog.typesetting.settings.themes.missing=Install themes into {0}.
471
472
Dialog.typesetting.settings.header.multiple=Export multiple documents
473
Dialog.typesetting.settings.chapters=Chapters (e.g., 1-3, 5, 7-)
474
475
# ########################################################################
476
# About Dialog
477
# ########################################################################
478
479
Dialog.about.title=About {0}
480
Dialog.about.header={0}
481
Dialog.about.content=Copyright 2016-{0} White Magic Software, Ltd.\n\nVersion: {1}
482
483
# ########################################################################
484
# Application Actions
485
# ########################################################################
486
487
Action.file.new.description=Create a new file
488
Action.file.new.accelerator=Shortcut+N
489
Action.file.new.icon=FILE_ALT
490
Action.file.new.text=_New
491
492
Action.file.open.description=Open a new file
493
Action.file.open.accelerator=Shortcut+O
494
Action.file.open.text=_Open...
495
Action.file.open.icon=FOLDER_OPEN_ALT
496
497
Action.file.close.description=Close the current document
498
Action.file.close.accelerator=Shortcut+W
499
Action.file.close.text=_Close
500
501
Action.file.close_all.description=Close all open documents
502
Action.file.close_all.accelerator=Ctrl+F4
503
Action.file.close_all.text=Close All
504
505
Action.file.save.description=Save the document
506
Action.file.save.accelerator=Shortcut+S
507
Action.file.save.text=_Save
508
Action.file.save.icon=FLOPPY_ALT
509
510
Action.file.save_as.description=Rename the current document
511
Action.file.save_as.text=Save _As
512
513
Action.file.save_all.description=Save all open documents
514
Action.file.save_all.accelerator=Shortcut+Shift+S
515
Action.file.save_all.text=Save A_ll
516
517
Action.file.export.pdf.description=Typeset the document
518
Action.file.export.pdf.accelerator=Shortcut+P
519
Action.file.export.pdf.text=_PDF
520
Action.file.export.pdf.icon=FILE_PDF_ALT
521
522
Action.file.export.pdf.dir.description=Typeset files in document directory
523
Action.file.export.pdf.dir.accelerator=Shortcut+Shift+P
524
Action.file.export.pdf.dir.text=_Joined PDF
525
Action.file.export.pdf.dir.icon=FILE_PDF_ALT
526
527
Action.file.export.pdf.repeat.description=Repeat previous typesetting command
528
Action.file.export.pdf.repeat.accelerator=Shortcut+Shift+E
529
Action.file.export.pdf.repeat.text=_Repeat Export
530
Action.file.export.pdf.repeat.icon=FILE_PDF_ALT
531
532
Action.file.export.html_svg.description=Export the current document as HTML + SVG
533
Action.file.export.text=_Export As
534
Action.file.export.html_svg.text=HTML and S_VG
535
536
Action.file.export.html_tex.description=Export the current document as HTML + TeX
537
Action.file.export.html_tex.text=HTML and _TeX
538
539
Action.file.export.xhtml_tex.description=Export as XHTML + TeX
540
Action.file.export.xhtml_tex.text=_XHTML and TeX
541
542
Action.file.export.markdown.description=Export the current document as Markdown
543
Action.file.export.markdown.text=Markdown
544
545
Action.file.exit.description=Quit the application
546
Action.file.exit.text=E_xit
547
548
549
Action.edit.undo.description=Undo the previous edit
550
Action.edit.undo.accelerator=Shortcut+Z
551
Action.edit.undo.text=_Undo
552
Action.edit.undo.icon=UNDO
553
554
Action.edit.redo.description=Redo the previous edit
555
Action.edit.redo.accelerator=Shortcut+Y
556
Action.edit.redo.text=_Redo
557
Action.edit.redo.icon=REPEAT
558
559
Action.edit.cut.description=Delete the selected text or line
560
Action.edit.cut.accelerator=Shortcut+X
561
Action.edit.cut.text=Cu_t
562
Action.edit.cut.icon=CUT
563
564
Action.edit.copy.description=Copy the selected text
565
Action.edit.copy.accelerator=Shortcut+C
566
Action.edit.copy.text=_Copy
567
Action.edit.copy.icon=COPY
568
569
Action.edit.paste.description=Paste from the clipboard
570
Action.edit.paste.accelerator=Shortcut+V
571
Action.edit.paste.text=_Paste
572
Action.edit.paste.icon=PASTE
573
574
Action.edit.select_all.description=Highlight the current document text
575
Action.edit.select_all.accelerator=Shortcut+A
576
Action.edit.select_all.text=Select _All
577
578
Action.edit.find.description=Search for text in the document
579
Action.edit.find.accelerator=Shortcut+F
580
Action.edit.find.text=_Find
581
Action.edit.find.icon=SEARCH
582
583
Action.edit.find_next.description=Find next occurrence
584
Action.edit.find_next.accelerator=F3
585
Action.edit.find_next.text=Find _Next
586
587
Action.edit.find_prev.description=Find previous occurrence
588
Action.edit.find_prev.accelerator=Shift+F3
589
Action.edit.find_prev.text=Find _Prev
590
591
Action.edit.preferences.description=Edit user preferences
592
Action.edit.preferences.accelerator=Ctrl+Alt+S
593
Action.edit.preferences.text=_Preferences
594
595
596
Action.format.bold.description=Insert strong text
597
Action.format.bold.accelerator=Shortcut+B
598
Action.format.bold.text=_Bold
599
Action.format.bold.icon=BOLD
600
601
Action.format.italic.description=Insert text emphasis
602
Action.format.italic.accelerator=Shortcut+I
603
Action.format.italic.text=_Italic
604
Action.format.italic.icon=ITALIC
605
606
Action.format.monospace.description=Insert monospace text
607
Action.format.monospace.accelerator=Shortcut+`
608
Action.format.monospace.text=_Monospace
609
610
Action.format.superscript.description=Insert superscript text
611
Action.format.superscript.accelerator=Shortcut+[
612
Action.format.superscript.text=Su_perscript
613
Action.format.superscript.icon=SUPERSCRIPT
614
615
Action.format.subscript.description=Insert subscript text
616
Action.format.subscript.accelerator=Shortcut+]
617
Action.format.subscript.text=Su_bscript
618
Action.format.subscript.icon=SUBSCRIPT
619
620
Action.format.strikethrough.description=Insert struck text
621
Action.format.strikethrough.accelerator=Shortcut+T
622
Action.format.strikethrough.text=Stri_kethrough
623
Action.format.strikethrough.icon=STRIKETHROUGH
624
625
626
Action.insert.blockquote.description=Insert blockquote
627
Action.insert.blockquote.accelerator=Ctrl+Q
628
Action.insert.blockquote.text=_Blockquote
629
Action.insert.blockquote.icon=QUOTE_LEFT
630
631
Action.insert.code.description=Insert inline code
632
Action.insert.code.accelerator=Shortcut+K
633
Action.insert.code.text=Inline _Code
634
Action.insert.code.icon=CODE
635
636
Action.insert.fenced_code_block.description=Insert code block
637
Action.insert.fenced_code_block.accelerator=Shortcut+Shift+K
638
Action.insert.fenced_code_block.text=_Fenced Code Block
639
Action.insert.fenced_code_block.prompt.text=Enter code here
640
Action.insert.fenced_code_block.icon=FILE_CODE_ALT
641
642
Action.insert.link.description=Insert hyperlink
643
Action.insert.link.accelerator=Shortcut+L
644
Action.insert.link.text=_Link...
645
Action.insert.link.icon=LINK
646
647
Action.insert.image.description=Insert image
648
Action.insert.image.accelerator=Shortcut+G
649
Action.insert.image.text=_Image...
650
Action.insert.image.icon=PICTURE_ALT
651
652
Action.insert.heading.description=Insert heading level
653
Action.insert.heading.accelerator=Shortcut+
654
Action.insert.heading.icon=HEADER
655
656
Action.insert.heading_1.description=${Action.insert.heading.description} 1
657
Action.insert.heading_1.accelerator=${Action.insert.heading.accelerator}1
658
Action.insert.heading_1.text=Heading _1
659
Action.insert.heading_1.icon=${Action.insert.heading.icon}
660
661
Action.insert.heading_2.description=${Action.insert.heading.description} 2
662
Action.insert.heading_2.accelerator=${Action.insert.heading.accelerator}2
663
Action.insert.heading_2.text=Heading _2
664
Action.insert.heading_2.icon=${Action.insert.heading.icon}
665
666
Action.insert.heading_3.description=${Action.insert.heading.description} 3
667
Action.insert.heading_3.accelerator=${Action.insert.heading.accelerator}3
668
Action.insert.heading_3.text=Heading _3
669
Action.insert.heading_3.icon=${Action.insert.heading.icon}
670
671
Action.insert.unordered_list.description=Insert bulleted list
672
Action.insert.unordered_list.accelerator=Shortcut+U
673
Action.insert.unordered_list.text=_Unordered List
674
Action.insert.unordered_list.icon=LIST_UL
675
676
Action.insert.ordered_list.description=Insert enumerated list
677
Action.insert.ordered_list.accelerator=Shortcut+Shift+O
678
Action.insert.ordered_list.text=_Ordered List
679
Action.insert.ordered_list.icon=LIST_OL
680
681
Action.insert.horizontal_rule.description=Insert horizontal rule
682
Action.insert.horizontal_rule.accelerator=Shortcut+H
683
Action.insert.horizontal_rule.text=_Horizontal Rule
684
Action.insert.horizontal_rule.icon=LIST_OL
685
686
687
Action.definition.create.description=Create a new variable
688
Action.definition.create.text=_Create
689
Action.definition.create.icon=TREE
690
Action.definition.create.tooltip=Add new item (Insert)
691
692
Action.definition.rename.description=Rename the selected variable
693
Action.definition.rename.text=_Rename
694
Action.definition.rename.icon=EDIT
695
Action.definition.rename.tooltip=Rename selected item (F2)
696
697
Action.definition.delete.description=Delete the selected variables
698
Action.definition.delete.text=De_lete
699
Action.definition.delete.icon=TRASH
700
Action.definition.delete.tooltip=Delete selected items (Delete)
701
702
Action.definition.insert.description=Insert a variable
703
Action.definition.insert.accelerator=Ctrl+Space
704
Action.definition.insert.text=_Insert
705
Action.definition.insert.icon=STAR
706
707
708
Action.view.refresh.description=Clear all caches
709
Action.view.refresh.accelerator=F5
710
Action.view.refresh.text=Refresh
711
712
Action.view.preview.description=Open document preview
713
Action.view.preview.accelerator=F6
714
Action.view.preview.text=Preview
715
716
Action.view.outline.description=Open document outline
717
Action.view.outline.accelerator=F7
718
Action.view.outline.text=Outline
719
720
Action.view.statistics.description=Open document word counts
721
Action.view.statistics.accelerator=F8
722
Action.view.statistics.text=Statistics
723
724
Action.view.files.description=Open file manager
725
Action.view.files.accelerator=Ctrl+F8
726
Action.view.files.text=Files
727
728
Action.view.menubar.description=Toggle menu bar
729
Action.view.menubar.accelerator=Ctrl+F9
730
Action.view.menubar.text=Menu bar
731
732
Action.view.toolbar.description=Toggle toolbar
733
Action.view.toolbar.accelerator=Ctrl+Shift+F9
734
Action.view.toolbar.text=Toolbar
735
736
Action.view.statusbar.description=Toggle status bar
737
Action.view.statusbar.accelerator=Ctrl+Shift+Alt+F9
738
Action.view.statusbar.text=Status bar
739
740
Action.view.log.description=Open document issues
741
Action.view.log.accelerator=F12
742
Action.view.log.text=Log
743
744
745
Action.help.about.description=Show help dialog
746
Action.help.about.accelerator=F1
747
Action.help.about.text=About
748
Action.help.about.icon=INFO
1749
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
}
1327
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.util.concurrent.Semaphore;
10
import java.util.function.Consumer;
11
12
import static java.io.File.createTempFile;
13
import static java.nio.file.Files.write;
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
    write( file.toPath(), text.getBytes(), 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
        fail();
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 static org.junit.jupiter.api.Assertions.assertEquals;
7
import static org.junit.jupiter.api.Assertions.assertTrue;
8
9
class SysFileTest {
10
  private static final String REG_PATH_PREFIX =
11
    "%USERPROFILE%";
12
  private static final String REG_PATH_SUFFIX =
13
    "\\AppData\\Local\\Microsoft\\WindowsApps;";
14
  private static final String REG_PATH = REG_PATH_PREFIX + REG_PATH_SUFFIX;
15
16
  @Test
17
  void test_Locate_ExistingExecutable_PathFound() {
18
    final var command = "ls";
19
    final var file = new SysFile( command );
20
    assertTrue( file.canRun() );
21
22
    final var located = file.locate();
23
    assertTrue( located.isPresent() );
24
25
    final var path = located.get();
26
    final var actual = path.toAbsolutePath().toString();
27
    final var expected = "/usr/bin/" + command;
28
29
    assertEquals( expected, actual );
30
  }
31
32
  @Test
33
  void test_Parse_RegistryEntry_ValueObtained() {
34
    final var file = new SysFile( "unused" );
35
    final var expected = REG_PATH;
36
    final var actual =
37
      file.parseRegEntry( "    path    REG_EXPAND_SZ    " + expected );
38
39
    assertEquals( expected, actual );
40
  }
41
42
  @Test
43
  void test_Expand_RegistryEntry_VariablesExpanded() {
44
    final var value = "UserProfile";
45
    final var file = new SysFile( "unused" );
46
    final var expected = value + REG_PATH_SUFFIX;
47
    final var actual = file.expand( REG_PATH, s -> value );
48
49
    assertEquals( expected, actual );
50
  }
51
}
152
A src/test/java/com/keenwrite/io/UserDataDirTest.java
1
/* Copyright 2022 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.FileNotFoundException;
7
8
import static org.junit.jupiter.api.Assertions.*;
9
10
class UserDataDirTest {
11
  @Test
12
  void test_Unix_GetAppDirectory_DirectoryExists()
13
    throws FileNotFoundException {
14
    final var path = UserDataDir.getAppPath( "test" );
15
    final var file = path.toFile();
16
17
    assertTrue( file.exists() );
18
    assertTrue( file.delete() );
19
    assertFalse( file.exists() );
20
  }
21
}
122
A src/test/java/com/keenwrite/io/downloads/DownloadManagerTest.java
1
/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.io.downloads;
3
4
import org.junit.jupiter.api.Test;
5
6
import java.io.IOException;
7
import java.io.OutputStream;
8
import java.util.concurrent.ExecutionException;
9
import java.util.concurrent.Executors;
10
import java.util.concurrent.atomic.AtomicInteger;
11
import java.util.concurrent.atomic.AtomicLong;
12
13
import static com.keenwrite.io.downloads.DownloadManager.ProgressListener;
14
import static com.keenwrite.io.downloads.DownloadManager.open;
15
import static java.io.OutputStream.nullOutputStream;
16
import static java.lang.System.setProperty;
17
import static org.junit.jupiter.api.Assertions.*;
18
19
class DownloadManagerTest {
20
21
  static {
22
    // By default, this returns null, which is not a valid user agent.
23
    setProperty( "http.agent", DownloadManager.class.getCanonicalName() );
24
  }
25
26
  private static final String SITE = "https://github.com/";
27
  private static final String URL
28
    = SITE + "DaveJarvis/keenwrite/releases/latest/download/keenwrite.exe";
29
30
  @Test
31
  void test_Async_DownloadRequested_DownloadCompletes()
32
    throws IOException, InterruptedException, ExecutionException {
33
    final var complete = new AtomicInteger();
34
    final var transferred = new AtomicLong();
35
36
    final OutputStream output = nullOutputStream();
37
    final ProgressListener listener = ( percentage, bytes ) -> {
38
      complete.set( percentage );
39
      transferred.set( bytes );
40
    };
41
42
    final var token = open( URL );
43
    final var executor = Executors.newFixedThreadPool( 1 );
44
    final var result = token.download( output, listener );
45
    final var future = executor.submit( result );
46
47
    assertFalse( future.isDone() );
48
    assertTrue( complete.get() < 100 );
49
    assertTrue( transferred.get() > 100_000 );
50
51
    future.get();
52
53
    assertEquals( 100, complete.get() );
54
55
    token.close();
56
  }
57
}
158
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::setThemesPath, () -> Path.of( "b" ) )
50
      .with( ProcessorContext.Mutator::setCaret, () -> caret )
51
      .with( ProcessorContext.Mutator::setImagesPath, () -> 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
        "<p id=\"caret\">the \uD83D\uDC4D emoji</p>\n"
68
      ),
69
      Arguments.of(
70
        XHTML_TEX,
71
        """
72
          <html>
73
            <head>
74
              <title> </title>
75
              <meta charset="utf8"/>
76
              <meta content="2" name="count"/>
77
            </head>
78
            <body>
79
              <p id="caret">the 👍 emoji</p>
80
          </body>
81
          </html>
82
          """
83
      )
84
    );
85
  }
86
}
187
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.AwaitFxExtension;
5
import com.keenwrite.editors.common.Caret;
6
import com.keenwrite.processors.Processor;
7
import com.keenwrite.processors.ProcessorContext;
8
import com.keenwrite.processors.markdown.extensions.ImageLinkExtension;
9
import com.vladsch.flexmark.html.HtmlRenderer;
10
import com.vladsch.flexmark.parser.Parser;
11
import javafx.stage.Stage;
12
import org.junit.jupiter.api.Test;
13
import org.junit.jupiter.api.extension.ExtendWith;
14
import org.testfx.framework.junit5.ApplicationExtension;
15
import org.testfx.framework.junit5.Start;
16
17
import java.io.File;
18
import java.net.URI;
19
import java.net.URL;
20
import java.nio.file.Path;
21
import java.nio.file.Paths;
22
import java.util.HashMap;
23
import java.util.List;
24
import java.util.Map;
25
26
import static com.keenwrite.ExportFormat.XHTML_TEX;
27
import static com.keenwrite.constants.Constants.DOCUMENT_DEFAULT;
28
import static java.lang.String.format;
29
import static javafx.application.Platform.runLater;
30
import static org.junit.jupiter.api.Assertions.assertEquals;
31
import static org.junit.jupiter.api.Assertions.assertNotNull;
32
import static org.testfx.util.WaitForAsyncUtils.waitForFxEvents;
33
34
/**
35
 * Responsible for testing that linked images render into HTML according to
36
 * the {@link ImageLinkExtension} rules.
37
 */
38
@ExtendWith( {ApplicationExtension.class, AwaitFxExtension.class} )
39
@SuppressWarnings( "SameParameterValue" )
40
public class ImageLinkExtensionTest {
41
  private static final Map<String, String> IMAGES = new HashMap<>();
42
43
  private static final String URI_WEB = "placekitten.com/200/200";
44
  private static final String URI_DIRNAME = "images";
45
  private static final String URI_FILENAME = "kitten";
46
47
  /**
48
   * Path to use for testing image file name resolution. Note that resources use
49
   * forward slashes, regardless of OS.
50
   */
51
  private static final String URI_PATH = URI_DIRNAME + '/' + URI_FILENAME;
52
53
  /**
54
   * Extension for the first existing image that matches the preferred image
55
   * extension order.
56
   */
57
  private static final String URI_IMAGE_EXT = ".png";
58
59
  /**
60
   * Relative path to an image that exists.
61
   */
62
  private static final String URI_IMAGE = URI_PATH + URI_IMAGE_EXT;
63
64
  static {
65
    addUri( URI_PATH + ".png" );
66
    addUri( URI_PATH + ".jpg" );
67
    addUri( URI_PATH, getResource( URI_PATH + URI_IMAGE_EXT ) );
68
    addUri( "//" + URI_WEB );
69
    addUri( "http://" + URI_WEB );
70
    addUri( "https://" + URI_WEB );
71
  }
72
73
  @Start
74
  @SuppressWarnings( "unused" )
75
  private void start( final Stage stage ) {
76
  }
77
78
  private static void addUri( final String actualExpected ) {
79
    addUri( actualExpected, actualExpected );
80
  }
81
82
  private static void addUri( final String actual, final String expected ) {
83
    IMAGES.put( toMd( actual ), toHtml( expected ) );
84
  }
85
86
  private static String toMd( final String resource ) {
87
    return format( "![Tooltip](%s 'Title')", resource );
88
  }
89
90
  private static String toHtml( final String url ) {
91
    return format(
92
      "<p><img src=\"%s\" alt=\"Tooltip\" title=\"Title\" /></p>\n", url );
93
  }
94
95
  /**
96
   * Test that the key URIs present in the {@link #IMAGES} map are rendered
97
   * as the value URIs present in the same map.
98
   */
99
  @Test
100
  void test_ImageLookup_RelativePathWithExtension_ResolvedSuccessfully() {
101
    final var resource = getResourcePath( URI_IMAGE );
102
    final var imagePath = new File( URI_IMAGE ).toPath();
103
    final var subpaths = resource.getNameCount() - imagePath.getNameCount();
104
    final var subpath = resource.subpath( 0, subpaths );
105
106
    // The root component isn't considered part of the path, so add it back.
107
    final var documentPath = Path.of(
108
      resource.getRoot().resolve( subpath ).toString(),
109
      DOCUMENT_DEFAULT.getName() );
110
    final var context = createProcessorContext( documentPath );
111
    final var extension = ImageLinkExtension.create( context );
112
    final var extensions = List.of( extension );
113
    final var pBuilder = Parser.builder();
114
    final var hBuilder = HtmlRenderer.builder();
115
    final var parser = pBuilder.extensions( extensions ).build();
116
    final var renderer = hBuilder.extensions( extensions ).build();
117
118
    assertNotNull( parser );
119
    assertNotNull( renderer );
120
121
    for( final var entry : IMAGES.entrySet() ) {
122
      final var key = entry.getKey();
123
      final var node = parser.parse( key );
124
      final var expectedHtml = entry.getValue();
125
      final var actualHtml = new StringBuilder( 128 );
126
127
      runLater( () -> actualHtml.append( renderer.render( node ) ) );
128
129
      waitForFxEvents();
130
      assertEquals( expectedHtml, actualHtml.toString() );
131
    }
132
  }
133
134
  /**
135
   * Creates a new {@link ProcessorContext} for the given file name path.
136
   *
137
   * @param inputPath Fully qualified path to the file name.
138
   * @return A context used for creating new {@link Processor} instances.
139
   */
140
  private ProcessorContext createProcessorContext( final Path inputPath ) {
141
    return ProcessorContext
142
      .builder()
143
      .with( ProcessorContext.Mutator::setSourcePath, inputPath )
144
      .with( ProcessorContext.Mutator::setExportFormat, XHTML_TEX )
145
      .with( ProcessorContext.Mutator::setCaret, () -> Caret.builder().build() )
146
      .build();
147
  }
148
149
  private static URL toUrl( final String path ) {
150
    final var clazz = ImageLinkExtensionTest.class;
151
    final var packagePath = clazz.getPackageName().replace( '.', '/' );
152
    final var resourcePath = '/' + packagePath + '/' + path;
153
    return clazz.getResource( resourcePath );
154
  }
155
156
  private static URI toUri( final String path ) {
157
    try {
158
      return toUrl( path ).toURI();
159
    } catch( final Exception ex ) {
160
      throw new RuntimeException( ex );
161
    }
162
  }
163
164
  private static Path getResourcePath( final String path ) {
165
    return Paths.get( toUri( path ) );
166
  }
167
168
  private static String getResource( final String path ) {
169
    return toUri( path ).toString();
170
  }
171
}
1172
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", "octopuses" ),
35
      entry( "ox", "oxen" ),
36
      entry( "soliloquy", "soliloquies" ),
37
      entry( "trilby", "trilbys" ),
38
      entry( "wolf", "wolves" )
39
  );
40
41
  @BeforeAll
42
  static void setup() throws ScriptException {
43
    r( "setwd( 'R' );" );
44
    r( "source( 'pluralize.R' );" );
45
  }
46
47
  @Test
48
  @SuppressWarnings("UnnecessaryLocalVariable")
49
  public void test_Pluralize_SingularForms_PluralForms()
50
      throws ScriptException {
51
    for( final var key : PLURAL_MAP.keySet() ) {
52
      final var expectedSingular = key;
53
      final var expectedPlural = PLURAL_MAP.get( key );
54
      final var actualSingular = pluralize( key, 1 );
55
      final var actualPlural = pluralize( key, 2 );
56
57
      assertEquals( expectedSingular, actualSingular );
58
      assertEquals( expectedPlural, actualPlural );
59
    }
60
  }
61
62
  private String pluralize( final String word, final int count )
63
      throws ScriptException {
64
    return r( format( "pluralize( '%s', %d );", word, count ) ).toString();
65
  }
66
67
  private static Object r( final String code ) throws ScriptException {
68
    return ENGINE.eval( code );
69
  }
70
}
171
A src/test/java/com/keenwrite/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