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
*.jar binary
21
*.jpg binary
22
*.mng binary
23
*.png binary
24
*.zip binary
25
*.otf binary
26
*.ttf binary
27
128
A .github/ISSUE_TEMPLATE/bug_report.md
1
---
2
name: Bug report
3
about: Create a report to help us improve
4
title: ''
5
labels: bug
6
assignees: ''
7
8
---
9
10
**Description**
11
A concise problem description.
12
13
**Replicate**
14
Exact and complete steps to reproduce the problem 100% of the time:
15
16
1. Open '...'
17
1. Click '....'
18
1. Click '....'
19
20
**Expected**
21
Describe the expected behaviour.
22
23
**Actual**
24
Describe the actual behaviour.
25
26
**Screenshots**
27
Add screenshots to show the problem, if applicable.
28
29
**Environment**
30
 - Operating System: (Windows, Linux, Mac)
31
 - Application: e.g., 1.7.16
32
33
**Details**
34
Add additional information, if applicable.
135
A .gitignore
1
dist
2
*.bin
3
*.exe
4
/*.jar
5
build
6
.gradle
7
contacted.csv
8
video
9
.settings
10
.classpath
11
.idea
12
themes
13
quotes
114
A .project
1
<?xml version="1.0" encoding="UTF-8"?>
2
<projectDescription>
3
	<name>Markdown Writer FX</name>
4
	<comment></comment>
5
	<projects>
6
	</projects>
7
	<buildSpec>
8
		<buildCommand>
9
			<name>org.eclipse.jdt.core.javabuilder</name>
10
			<arguments>
11
			</arguments>
12
		</buildCommand>
13
		<buildCommand>
14
			<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
15
			<arguments>
16
			</arguments>
17
		</buildCommand>
18
	</buildSpec>
19
	<natures>
20
		<nature>org.eclipse.jdt.core.javanature</nature>
21
		<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
22
	</natures>
23
	<filteredResources>
24
		<filter>
25
			<id>1438449113801</id>
26
			<name></name>
27
			<type>26</type>
28
			<matcher>
29
				<id>org.eclipse.ui.ide.multiFilter</id>
30
				<arguments>1.0-projectRelativePath-matches-false-false-build</arguments>
31
			</matcher>
32
		</filter>
33
		<filter>
34
			<id>1438449113801</id>
35
			<name></name>
36
			<type>26</type>
37
			<matcher>
38
				<id>org.eclipse.ui.ide.multiFilter</id>
39
				<arguments>1.0-projectRelativePath-matches-false-false-.gradle</arguments>
40
			</matcher>
41
		</filter>
42
	</filteredResources>
43
</projectDescription>
144
A BUILD.md
1
# Introduction
2
3
This document describes how to build the application and platform binaries.
4
5
# Requirements
6
7
Download and install the following software packages:
8
9
* [JDK 17](https://bell-sw.com/pages/downloads/?version=java-17) (Full JDK + JavaFX)
10
* [Gradle 7.2](https://gradle.org/releases)
11
* [Git 2.33](https://git-scm.com/downloads)
12
13
## Repository
14
15
Clone the repository as follows:
16
17
    git clone https://github.com/DaveJarvis/keenwrite.git
18
19
The repository is cloned.
20
21
# Build
22
23
Build the application überjar as follows:
24
25
    cd keenwrite
26
    gradle clean jar
27
28
The application is built.
29
30
# Run
31
32
After the application is compiled, run it using `keenwrite.sh`.
33
34
# Integrated development environments
35
36
This section describes setup instructions to import and run the application using an integrated development environment (IDE). Running the application should trigger a build.
37
38
## IntelliJ IDEA
39
40
This section describes how to build and run the application using IntellIJ's IDEA.
41
42
### Import
43
44
Complete the following steps to import the application:
45
46
1. Start the IDE.
47
1. Click **File → New → Project from Existing Sources**.
48
1. Browse to the directory containing `keenwrite`.
49
1. Click **OK**.
50
1. Select **Gradle** as the external model.
51
1. Click **Finish**.
52
53
The project is imported into the IDE.
54
55
### Run
56
57
Run the application within the IDE as follows:
58
59
1. Open **Launcher.java**.
60
1. Click **Run → Launcher**.
61
62
The application is started.
63
64
# Installers
65
66
This section describes how to set up the development environment and build native executables for supported operating systems.
67
68
## Setup
69
70
Follow these one-time setup instructions to begin:
71
72
1. Ensure `$HOME/bin` is set in the `PATH` environment variable.
73
1. Copy `build-template` into `$HOME/bin`.
74
75
Setup is complete.
76
77
## Binaries
78
79
Run the `installer` script to build platform-specific binaries, such as:
80
81
    ./installer -V -o linux
82
83
The `installer` script:
84
85
* downloads a JDK;
86
* generates a run script;
87
* bundles the JDK, run script, and JAR file; and
88
* creates a standalone binary, so no installation required.
89
90
Run `./installer -h` to see all command-line options.
91
92
# Releases
93
94
After installing `scripts/build-template`, build release binaries as follows:
95
96
    git tag -a 2.0.0 -m "Release name"
97
    git push origin --tags
98
    ./release.sh
99
100
When finished, browse to the project releases page to draft a new release.
101
102
# Versioning
103
104
Version numbers are read directly from Git using a plugin. The version number is written to `app.properties` in the `resources` directory. The application reads that file to display version information upon start.
105
1106
A CODE_OF_CONDUCT.md
1
# Contributor Covenant Code of Conduct
2
3
## Our Pledge
4
5
In the interest of fostering an open and welcoming environment, we as
6
contributors and maintainers pledge to making participation in our project and
7
our community a harassment-free experience for everyone, regardless of age, body
8
size, disability, ethnicity, sex characteristics, gender identity and expression,
9
level of experience, education, socio-economic status, nationality, personal
10
appearance, race, religion, or sexual identity and orientation.
11
12
## Our Standards
13
14
Examples of behavior that contributes to creating a positive environment
15
include:
16
17
* Using welcoming and inclusive language
18
* Being respectful of differing viewpoints and experiences
19
* Gracefully accepting constructive criticism
20
* Focusing on what is best for the community
21
* Showing empathy towards other community members
22
23
Examples of unacceptable behavior by participants include:
24
25
* The use of sexualized language or imagery and unwelcome sexual attention or
26
 advances
27
* Trolling, insulting/derogatory comments, and personal or political attacks
28
* Public or private harassment
29
* Publishing others' private information, such as a physical or electronic
30
 address, without explicit permission
31
* Other conduct which could reasonably be considered inappropriate in a
32
 professional setting
33
34
## Our Responsibilities
35
36
Project maintainers are responsible for clarifying the standards of acceptable
37
behavior and are expected to take appropriate and fair corrective action in
38
response to any instances of unacceptable behavior.
39
40
Project maintainers have the right and responsibility to remove, edit, or
41
reject comments, commits, code, wiki edits, issues, and other contributions
42
that are not aligned to this Code of Conduct, or to ban temporarily or
43
permanently any contributor for other behaviors that they deem inappropriate,
44
threatening, offensive, or harmful.
45
46
## Scope
47
48
This Code of Conduct applies both within project spaces and in public spaces
49
when an individual is representing the project or its community. Examples of
50
representing a project or community include using an official project e-mail
51
address, posting via an official social media account, or acting as an appointed
52
representative at an online or offline event. Representation of a project may be
53
further defined and clarified by project maintainers.
54
55
## Enforcement
56
57
Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
reported by contacting the project team at Dave.Jarvis@gmail.com. All
59
complaints will be reviewed and investigated and will result in a response that
60
is deemed necessary and appropriate to the circumstances. The project team is
61
obligated to maintain confidentiality with regard to the reporter of an incident.
62
Further details of specific enforcement policies may be posted separately.
63
64
Project maintainers who do not follow or enforce the Code of Conduct in good
65
faith may face temporary or permanent repercussions as determined by other
66
members of the project's leadership.
67
68
## Attribution
69
70
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72
73
[homepage]: https://www.contributor-covenant.org
74
75
For answers to common questions about this code of conduct, see
76
https://www.contributor-covenant.org/faq
177
A LICENSE.md
1
# License
2
3
Copyright 2020 White Magic Software, Ltd.
4
5
Copyright 2015 Karl Tauber
6
7
All rights reserved.
8
9
Redistribution and use in source and binary forms, with or without
10
modification, are permitted provided that the following conditions are met:
11
12
* Redistributions of source code must retain the above copyright
13
  notice, this list of conditions and the following disclaimer.
14
15
* Redistributions in binary form must reproduce the above copyright
16
  notice, this list of conditions and the following disclaimer in the
17
  documentation and/or other materials provided with the distribution.
18
19
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
130
A R/README.md
1
# R Functions
2
3
Import the files in this directory into the application, which include:
4
5
* pluralize.R
6
* possessive.R
7
* conversion.R
8
* csv.R
9
10
# pluralize.R
11
12
This file defines a function that implements most of Damian Conway's [An Algorithmic Approach to English Pluralization](http://blob.perl.org/tpc/1998/User_Applications/Algorithmic%20Approach%20Plurals/Algorithmic_Plurals.html).
13
14
## Usage
15
16
Example usages of the pluralize function include:
17
18
    `r#pluralize( 'mouse' )` - mice
19
    `r#pluralize( 'buzz' )` - buzzes
20
    `r#pluralize( 'bus' )` - busses
21
22
# possessive.R
23
24
This file defines a function that applies possessives to English words.
25
26
## Usage
27
28
Example usages of the possessive function include:
29
30
    `r#pos( 'Ross' )` - Ross'
31
    `r#pos( 'Ruby' )` - Ruby's
32
    `r#pos( 'Lois' )` - Lois'
33
    `r#pos( 'my' )` - mine
34
    `r#pos( 'Your' )` - Yours
35
136
A R/bootstrap.R
1
setwd( '{{application.r.working.directory}}' )
2
assign( "anchor", '{{date.anchor}}', envir = .GlobalEnv )
3
4
source( 'pluralize.R' )
5
source( 'possessive.R' )
6
source( 'conversion.R' )
7
source( 'csv.R' )
8
19
A R/conversion.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# Substitute R expressions in a document with their evaluated value. The
26
# anchor variable must be set for functions that use relative dates.
27
# -----------------------------------------------------------------------------
28
29
# -----------------------------------------------------------------------------
30
# Evaluates an expression; writes s if there is no expression.
31
# -----------------------------------------------------------------------------
32
x <- function( s ) {
33
  tryCatch( {
34
    r = eval( parse( text = s ) )
35
36
    # If the result isn't primitive, then it was probably parsed into
37
    # an unprintable object (e.g., "gray" becomes a colour). In those
38
    # cases, return the original text string. Otherwise, an atomic
39
    # value means a primitive type (string, integer, etc.) that can be
40
    # written directly into the document.
41
    ifelse( is.atomic( r ), r, s );
42
  },
43
  warning = function( w ) { s },
44
  error = function( e ) { s } )
45
}
46
47
# -----------------------------------------------------------------------------
48
# Returns a date offset by a given number of days, relative to the given
49
# date (d). This does not use the anchor, but is used to get the anchor's
50
# value as a date.
51
# -----------------------------------------------------------------------------
52
when <- function( d, n = 0, format = "%Y-%m-%d" ) {
53
  as.Date( d, format = format ) + x( n )
54
}
55
56
# -----------------------------------------------------------------------------
57
# Full date (s) offset by an optional number of days before or after.
58
# This will remove leading zeros (applying leading spaces instead, which
59
# are ignored by any worthwhile typesetting engine).
60
# -----------------------------------------------------------------------------
61
annal <- function( days = 0, format = "%Y-%m-%d", oformat = "%B %d, %Y" ) {
62
  format( when( anchor, days ), format = oformat )
63
}
64
65
# -----------------------------------------------------------------------------
66
# Extracts the year from a date string.
67
# -----------------------------------------------------------------------------
68
year <- function( days = 0, format = "%Y-%m-%d" ) {
69
  annal( days, format, "%Y" )
70
}
71
72
# -----------------------------------------------------------------------------
73
# Day of the week (in days since the anchor date).
74
# -----------------------------------------------------------------------------
75
weekday <- function( n ) {
76
  weekdays( when( anchor, n ) )
77
}
78
79
# -----------------------------------------------------------------------------
80
# String concatenate function alias because paste0 is a terrible name.
81
# -----------------------------------------------------------------------------
82
concat <- paste0
83
84
# -----------------------------------------------------------------------------
85
# Translates a number from digits to words using Chicago Manual of Style.
86
# This does not translate numbers greater than one hundred. If ordinal
87
# is TRUE, this will return the ordinal name. This will not produce ordinals
88
# for numbers greater than 100.
89
# -----------------------------------------------------------------------------
90
cms <- function( n, ordinal = FALSE ) {
91
  n <- x( n )
92
93
  if( n == 0 ) {
94
    if( ordinal ) {
95
      return( "zeroth" )
96
    }
97
98
    return( "zero" )
99
  }
100
101
  # Concatenate this a little later.
102
  if( n < 0 ) {
103
    result = "negative "
104
    n = abs( n )
105
  }
106
107
  # Do not spell out numbers greater than one hundred.
108
  if( n > 100 ) {
109
    # Comma-separated numbers.
110
    return( commas( n ) )
111
  }
112
113
  # Don't go beyond 100.
114
  if( n == 100 ) {
115
    if( ordinal ) {
116
      return( "one hundredth" )
117
    }
118
119
    return( "one hundred" )
120
  }
121
122
  # Samuel Langhorne Clemens noted English has too many exceptions.
123
  small = c(
124
    "one", "two", "three", "four", "five",
125
    "six", "seven", "eight", "nine", "ten",
126
    "eleven", "twelve", "thirteen", "fourteen", "fifteen",
127
    "sixteen", "seventeen", "eighteen", "nineteen"
128
  )
129
130
  ord_small = c(
131
    "first", "second", "third", "fourth", "fifth",
132
    "sixth", "seventh", "eighth", "ninth", "tenth",
133
    "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth",
134
    "sixteenth", "seventeenth", "eighteenth", "nineteenth", "twentieth"
135
  )
136
137
  # After this, the number (n) is between 20 and 99.
138
  if( n < 20 ) {
139
    if( ordinal ) {
140
      return( .subset( ord_small, n %% 100 ) )
141
    }
142
143
    return( .subset( small, n %% 100 ) )
144
  }
145
146
  tens = c( "",
147
    "twenty", "thirty", "forty", "fifty",
148
    "sixty", "seventy", "eighty", "ninety"
149
  )
150
151
  ord_tens = c( "",
152
    "twentieth", "thirtieth", "fortieth", "fiftieth",
153
    "sixtieth", "seventieth", "eightieth", "ninetieth"
154
  )
155
156
  ones_index = n %% 10
157
  n = n %/% 10
158
159
  # No number in the ones column, so the number must be a multiple of ten.
160
  if( ones_index == 0 ) {
161
    if( ordinal ) {
162
      return( .subset( ord_tens, n ) )
163
    }
164
165
    return( .subset( tens, n ) )
166
  }
167
168
  # Find the value from the ones column.
169
  if( ordinal ) {
170
    unit_1 = .subset( ord_small, ones_index )
171
  }
172
  else {
173
    unit_1 = .subset( small, ones_index )
174
  }
175
176
  # Find the tens column.
177
  unit_10 = .subset( tens, n )
178
179
  # Hyphenate the tens and the ones together.
180
  concat( unit_10, concat( "-", unit_1 ) )
181
}
182
183
# -----------------------------------------------------------------------------
184
# Returns a number as a comma-delimited string. This is a work-around
185
# until Renjin fixes https://github.com/bedatadriven/renjin/issues/338
186
# -----------------------------------------------------------------------------
187
commas <- function( n ) {
188
  n <- x( n )
189
190
  s <- sprintf( "%03.0f", n %% 1000 )
191
  n <- n %/% 1000
192
193
  while( n > 0 ) {
194
    s <- concat( sprintf( "%03.0f", n %% 1000 ), ',', s )
195
    n <- n %/% 1000
196
  }
197
198
  gsub( '^0*', '', s )
199
}
200
201
# -----------------------------------------------------------------------------
202
# Returns a human-readable string that provides the elapsed time between
203
# two numbers in terms of years, months, and days. If any unit value is zero,
204
# the unit is not included. The words (year, month, day) are pluralized
205
# according to English grammar. The numbers are written out according to
206
# Chicago Manual of Style. This applies the serial comma.
207
#
208
# Both numbers are offsets relative to the anchor date.
209
#
210
# If all unit values are zero, this returns s ("same day" by default).
211
#
212
# If the start date (began) is greater than end date (ended), the dates are
213
# swapped before calculations are performed. This allows any two dates
214
# to be compared and positive unit values are always returned.
215
# -----------------------------------------------------------------------------
216
elapsed <- function( began, ended, s = "same day" ) {
217
  began = when( anchor, began )
218
  ended = when( anchor, ended )
219
220
  # Swap the dates if the end date comes before the start date.
221
  if( as.integer( ended - began ) < 0 ) {
222
    tempd = began
223
    began = ended
224
    ended = tempd
225
  }
226
227
  # Calculate number of elapsed years.
228
  years = length( seq( from = began, to = ended, by = 'year' ) ) - 1
229
230
  # Move the start date up by the number of elapsed years.
231
  if( years > 0 ) {
232
    began = seq( began, length = 2, by = concat( years, " years" ) )[2]
233
    years = pl.numeric( "year", years )
234
  }
235
  else {
236
    # Zero years.
237
    years = ""
238
  }
239
240
  # Calculate number of elapsed months, excluding years.
241
  months = length( seq( from = began, to = ended, by = 'month' ) ) - 1
242
243
  # Move the start date up by the number of elapsed months
244
  if( months > 0 ) {
245
    began = seq( began, length = 2, by = concat( months, " months" ) )[2]
246
    months = pl.numeric( "month", months )
247
  }
248
  else {
249
    # Zero months
250
    months = ""
251
  }
252
253
  # Calculate number of elapsed days, excluding months and years.
254
  days = length( seq( from = began, to = ended, by = 'day' ) ) - 1
255
256
  if( days > 0 ) {
257
    days = pl.numeric( "day", days )
258
  }
259
  else {
260
    # Zero days
261
    days = ""
262
  }
263
264
  if( years <= 0 && months <= 0 && days <= 0 ) {
265
    return( s )
266
  }
267
268
  # Put them all in a vector, then remove the empty values.
269
  s <- c( years, months, days )
270
  s <- s[ s != "" ]
271
272
  r <- paste( s, collapse = ", " )
273
274
  # If all three items are present, replace the last comma with ", and".
275
  if( length( s ) > 2 ) {
276
    return( gsub( "(.*),", "\\1, and", r ) )
277
  }
278
279
  # Does nothing if no commas are present.
280
  gsub( "(.*),", "\\1 and", r )
281
}
282
283
# -----------------------------------------------------------------------------
284
# Returns the number (n) in English followed by the plural or singular
285
# form of the given string (s; resumably a noun), if applicable, according
286
# to English grammar. That is, pl.numeric( "wolf", 5 ) will return
287
# "five wolves".
288
# -----------------------------------------------------------------------------
289
pl.numeric <- function( s, n ) {
290
  concat( cms( n ), concat( " ", pluralize( s, n ) ) )
291
}
292
293
# -----------------------------------------------------------------------------
294
# Pluralize s if n is not equal to 1.
295
# -----------------------------------------------------------------------------
296
pl <- function( s, n=2 ) {
297
  pluralize( s, x( n ) )
298
}
299
300
# -----------------------------------------------------------------------------
301
# Name of the season, starting with an capital letter.
302
# -----------------------------------------------------------------------------
303
season <- function( n, format = "%Y-%m-%d" ) {
304
  WS <- as.Date("2016-12-15", "%Y-%m-%d") # Winter Solstice
305
  SE <- as.Date("2016-03-15", "%Y-%m-%d") # Spring Equinox
306
  SS <- as.Date("2016-06-15", "%Y-%m-%d") # Summer Solstice
307
  AE <- as.Date("2016-09-15", "%Y-%m-%d") # Autumn Equinox
308
309
  d <- when( anchor, n )
310
  d <- as.Date( strftime( d, format="2016-%m-%d" ) )
311
312
  ifelse( d >= WS | d < SE, "Winter",
313
    ifelse( d >= SE & d < SS, "Spring",
314
      ifelse( d >= SS & d < AE, "Summer", "Autumn" )
315
    )
316
  )
317
}
318
319
# -----------------------------------------------------------------------------
320
# Converts the first letter in a string to lowercase
321
# -----------------------------------------------------------------------------
322
lc <- function( s ) {
323
  concat( tolower( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
324
}
325
326
# -----------------------------------------------------------------------------
327
# Converts the entire string to lowercase
328
# -----------------------------------------------------------------------------
329
lower <- tolower
330
331
# -----------------------------------------------------------------------------
332
# Converts the first letter in a string to uppercase
333
# -----------------------------------------------------------------------------
334
uc <- function( s ) {
335
  concat( toupper( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
336
}
337
338
# -----------------------------------------------------------------------------
339
# Returns the number of days between the given dates.
340
# -----------------------------------------------------------------------------
341
days <- function( d1, d2, format = "%Y-%m-%d" ) {
342
  dates = c( d1, d2 )
343
  dt = strptime( dates, format = format )
344
  as.integer( difftime( dates[2], dates[1], units = "days" ) )
345
}
346
347
# -----------------------------------------------------------------------------
348
# Returns the number of years elapsed.
349
# -----------------------------------------------------------------------------
350
years <- function( began, ended ) {
351
  began = when( anchor, began )
352
  ended = when( anchor, ended )
353
354
  # Swap the dates if the end date comes before the start date.
355
  if( as.integer( ended - began ) < 0 ) {
356
    tempd = began
357
    began = ended
358
    ended = tempd
359
  }
360
361
  # Calculate number of elapsed years.
362
  length( seq( from = began, to = ended, by = 'year' ) ) - 1
363
}
364
365
# -----------------------------------------------------------------------------
366
# Full name of the month, starting with a capital letter.
367
# -----------------------------------------------------------------------------
368
month <- function( n ) {
369
  # Faster than month.name[ x( n ) ]
370
  .subset( month.name, x( n ) )
371
}
372
373
# -----------------------------------------------------------------------------
374
# -----------------------------------------------------------------------------
375
money <- function( n ) {
376
  commas( x( n ) )
377
}
378
379
# -----------------------------------------------------------------------------
380
# -----------------------------------------------------------------------------
381
timeline <- function( n ) {
382
  concat( weekday( n ), ", ", annal( n ), " (", season( n ), ")" )
383
}
384
385
# -----------------------------------------------------------------------------
386
# Rounds to the nearest base value (e.g., round to nearest 10).
387
#
388
# @param base The nearest value to round to.
389
# -----------------------------------------------------------------------------
390
round.up <- function( n, base = 5 ) {
391
  base * round( x( n ) / base )
392
}
393
394
# -----------------------------------------------------------------------------
395
# Computes linear distance between two points using Haversine formula.
396
# Although Earth is an oblate spheroid, this will produce results close
397
# enough for most purposes.
398
#
399
# @param lat1/lon1 The source latitude and longitude.
400
# @param lat2/lon2 The destination latitude and longitude.
401
# @param radius The radius of the sphere.
402
#
403
# @return The distance between the two coordinates in meters.
404
# -----------------------------------------------------------------------------
405
haversine <- function( lat1, lon1, lat2, lon2, radius = 6371 ) {
406
  # Convert decimal degrees to radians
407
  lon1 = lon1 * pi / 180
408
  lon2 = lon2 * pi / 180
409
  lat1 = lat1 * pi / 180
410
  lat2 = lat2 * pi / 180
411
412
  # Haversine formula
413
  dlon = lon2 - lon1
414
  dlat = lat2 - lat1
415
  a = sin( dlat / 2 ) ** 2 + cos( lat1 ) * cos( lat2 ) * sin( dlon / 2 ) ** 2
416
  c = 2 * atan2( sqrt( a ), sqrt( 1-a ) )
417
418
  return( radius * c * 1000 )
419
}
420
1421
A R/csv.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# Converts CSV to Markdown.
26
#
27
# Reads a CSV file and converts the contents to a Markdown table. The
28
# file must be in the working directory as specified by setwd.
29
#
30
# @param f The filename to convert.
31
# @param decimals Rounded decimal places (default 1).
32
# @param totals Include total sums (default TRUE).
33
# @param align Right-align numbers (default TRUE).
34
# -----------------------------------------------------------------------------
35
csv2md <- function( f, decimals = 2, totals = T, align = T ) {
36
  # Read the CVS data from the file; ensure strings become characters.
37
  df <- read.table( f, sep=',', header=T, stringsAsFactors=F )
38
39
  if( totals ) {
40
    # Determine what columns can be summed.
41
    number <- which( unlist( lapply( df, is.numeric ) ) )
42
43
    # Use colSums when more than one summable column exists.
44
    if( length( number ) > 1 ) {
45
      f.sum <- colSums
46
    }
47
    else {
48
      f.sum <- sum
49
    }
50
51
    # Calculate the sum of all the summable columns and insert the
52
    # results back into the data frame.
53
    df[ (nrow( df ) + 1), number ] <- f.sum( df[, number], na.rm=TRUE )
54
55
    # pluralise would be heavyweight here.
56
    if( length( number ) > 1 ) {
57
      t <- "**Totals**"
58
    }
59
    else {
60
      t <- "**Total**"
61
    }
62
63
    # Change the first column of the last line to "Total(s)".
64
    df[ nrow( df ), 1 ] <- t
65
66
    # Don't clutter the output with "NA" text.
67
    df[ is.na( df ) ] <- ""
68
  }
69
70
  if( align ) {
71
    is.char <- vapply( df, is.character, logical( 1 ) )
72
    dashes <- paste( ifelse( is.char, ':---', '---:' ), collapse='|' )
73
  }
74
  else {
75
    dashes <- paste( rep( '---', length( df ) ), collapse = '|' )
76
  }
77
78
  # Create a Markdown version of the data frame.
79
  paste(
80
    paste( names( df ), collapse = '|'), '\n',
81
    dashes, '\n', 
82
    paste(
83
      Reduce( function( x, y ) {
84
          paste( x, format( y, digits = decimals ), sep = '|' )
85
        }, df
86
      ),
87
      collapse = '|\n', sep=''
88
    )
89
  )
90
}
91
192
A R/pluralize.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# See Damian Conway's "An Algorithmic Approach to English Pluralization":
26
#   http://goo.gl/oRL4MP
27
# See Oliver Glerke's Evo Inflector: https://github.com/atteo/evo-inflector/
28
# See Shevek's Pluralizer: https://github.com/shevek/linguistics/
29
# See also: http://www.freevectors.net/assets/files/plural.txt
30
# -----------------------------------------------------------------------------
31
pluralize <- function( s, n ) {
32
  result <- s
33
34
  # Partial implementation of Conway's algorithm for nouns.
35
  if( n != 1 ) {
36
    if( pl.noninflective( s ) ||
37
        pl.suffix( "es", s ) ||
38
        pl.suffix( "fish", s ) ||
39
        pl.suffix( "ois", s ) ||
40
        pl.suffix( "sheep", s ) ||
41
        pl.suffix( "deer", s ) ||
42
        pl.suffix( "pox", s ) ||
43
        pl.suffix( "[A-Z].*ese", s ) ||
44
        pl.suffix( "itis", s ) ) {
45
      # 1. Retain non-inflective user-mapped noun as is.
46
      # 2. Retain non-inflective plural as is.
47
      result <- s
48
    }
49
    else if( pl.is.irregular.pl( s ) ) {
50
      # 4. Change irregular plurals based on mapping.
51
      result <- pl.irregular.pl( s )
52
    }
53
    else if( pl.is.irregular.es( s ) ) {
54
      # x. From Shevek's
55
      result <- pl.inflect( s, "", "es" )
56
    }
57
    else if( pl.suffix( "man", s ) ) {
58
      # 5. For -man, change -an to -en
59
      result <- pl.inflect( s, "an", "en" )
60
    }
61
    else if( pl.suffix( "[lm]ouse", s ) ) {
62
      # 5. For [lm]ouse, change -ouse to -ice
63
      result <- pl.inflect( s, "ouse", "ice" )
64
    }
65
    else if( pl.suffix( "tooth", s ) ) {
66
      # 5. For -tooth, change -ooth to -eeth
67
      result <- pl.inflect( s, "ooth", "eeth" )
68
    }
69
    else if( pl.suffix( "goose", s ) ) {
70
      # 5. For -goose, change -oose to -eese
71
      result <- pl.inflect( s, "oose", "eese" )
72
    }
73
    else if( pl.suffix( "foot", s ) ) {
74
      # 5. For -foot, change -oot to -eet
75
      result <- pl.inflect( s, "oot", "eet" )
76
    }
77
    else if( pl.suffix( "zoon", s ) ) {
78
      # 5. For -zoon, change -on to -a
79
      result <- pl.inflect( s, "on", "a" )
80
    }
81
    else if( pl.suffix( "[csx]is", s ) ) {
82
      # 5. Change -cis, -sis, -xis to -es
83
      result <- pl.inflect( s, "is", "es" )
84
    }
85
    else if( pl.suffix( "([cs]h|ss|zz|x|s)", s ) ) {
86
      # 8. Change -ch, -sh, -ss, -zz, -x, -s to -es
87
      result <- pl.inflect( s, "", "es" )
88
    }
89
    else if( pl.suffix( "([aeo]lf|[^d]eaf|arf)", s ) ) {
90
      # 9. Change -f to -ves
91
      result <- pl.inflect( s, "f", "ves" )
92
    }
93
    else if( pl.suffix( "[nlw]ife", s ) ) {
94
      # 10. Change -fe to -ves
95
      result <- pl.inflect( s, "fe", "ves" )
96
    }
97
    else if( pl.suffix( "[aeiou]y", s ) ) {
98
      # 11. Change -[aeiou]y to -ys
99
      result <- pl.inflect( s, "", "s" )
100
    }
101
    else if( pl.suffix( "y", s ) ) {
102
      # 12. Change -y to -ies
103
      result <- pl.inflect( s, "y", "ies" )
104
    }
105
    else if( pl.suffix( "z", s ) ) {
106
      # x. Change -z to -zzes
107
      result <- pl.inflect( s, "", "zes" )
108
    }
109
    else {
110
      # 13. Default plural: add -s
111
      result <- pl.inflect( s, "", "s" )
112
    }
113
  }
114
115
  result
116
}
117
118
# -----------------------------------------------------------------------------
119
# Returns the given string (s) with its suffix replaced by r.
120
# -----------------------------------------------------------------------------
121
pl.inflect <- function( s, suffix, r ) {
122
  gsub( paste( suffix, "$", sep="" ), r, s )
123
}
124
125
# -----------------------------------------------------------------------------
126
# Answers whether the given string (s) has the given ending.
127
# -----------------------------------------------------------------------------
128
pl.suffix <- function( ending, s ) {
129
  grepl( paste( ending, "$", sep="" ), s )
130
}
131
132
# -----------------------------------------------------------------------------
133
# Answers whether the given string (s) is a noninflective noun.
134
# -----------------------------------------------------------------------------
135
pl.noninflective <- function( s ) {
136
  v <- c(
137
    "aircraft",
138
    "Bhutanese",
139
    "bison",
140
    "bream",
141
    "Burmese",
142
    "carp",
143
    "chassis",
144
    "Chinese",
145
    "clippers",
146
    "cod",
147
    "contretemps",
148
    "corps",
149
    "debris",
150
    "djinn",
151
    "eland",
152
    "elk",
153
    "flounder",
154
    "fracas",
155
    "gallows",
156
    "graffiti",
157
    "headquarters",
158
    "high-jinks",
159
    "homework",
160
    "hovercraft",
161
    "innings",
162
    "Japanese",
163
    "Lebanese",
164
    "mackerel",
165
    "means",
166
    "mews",
167
    "mice",
168
    "mumps",
169
    "news",
170
    "pincers",
171
    "pliers",
172
    "Portuguese",
173
    "proceedings",
174
    "salmon",
175
    "scissors",
176
    "sea-bass",
177
    "Senegalese",
178
    "shears",
179
    "Siamese",
180
    "Sinhalese",
181
    "spacecraft",
182
    "swine",
183
    "trout",
184
    "tuna",
185
    "Vietnamese",
186
    "watercraft",
187
    "whiting",
188
    "wildebeest"
189
  )
190
191
  is.element( s, v )
192
}
193
194
# -----------------------------------------------------------------------------
195
# Answers whether the given string (s) is an irregular plural.
196
# -----------------------------------------------------------------------------
197
pl.is.irregular.pl <- function( s ) {
198
  # Could be refactored with pl.irregular.pl...
199
  v <- c(
200
    "beef", "brother", "child", "cow", "ephemeris", "genie", "money",
201
    "mongoose", "mythos", "octopus", "ox", "soliloquy", "trilby"
202
  )
203
204
  is.element( s, v )
205
}
206
207
# -----------------------------------------------------------------------------
208
# Call to pluralize an irregular noun. Only call after confirming
209
# the noun is irregular via pl.is.irregular.pl.
210
# -----------------------------------------------------------------------------
211
pl.irregular.pl <- function( s ) {
212
  v <- list(
213
    "beef" = "beefs",
214
    "brother" = "brothers",
215
    "child" = "children",
216
    "cow" = "cows",
217
    "ephemeris" = "ephemerides",
218
    "genie" = "genies",
219
    "money" = "moneys",
220
    "mongoose" = "mongooses",
221
    "mythos" = "mythoi",
222
    "octopus" = "octopuses",
223
    "ox" = "oxen",
224
    "passerby" = "passersby",
225
    "soliloquy" = "soliloquies",
226
    "trilby" = "trilbys"
227
  )
228
229
  # Faster version of v[[ s ]]
230
  .subset2( v, s )
231
}
232
233
# -----------------------------------------------------------------------------
234
# Answers whether the given string (s) pluralizes with -es.
235
# -----------------------------------------------------------------------------
236
pl.is.irregular.es <- function( s ) {
237
  v <- c(
238
    "acropolis", "aegis", "alias", "asbestos", "bathos", "bias", "bronchitis",
239
    "bursitis", "caddis", "cannabis", "canvas", "chaos", "cosmos", "dais",
240
    "digitalis", "epidermis", "ethos", "eyas", "gas", "glottis", "hubris",
241
    "ibis", "lens", "mantis", "marquis", "metropolis", "pathos", "pelvis",
242
    "polis", "rhinoceros", "sassafrass", "trellis"
243
  )
244
245
  is.element( s, v )
246
}
247
1248
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 <- 'their'
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.pos <- function( s ) {
105
  if( s == 'm' ) {
106
    s = 'his'
107
  }
108
  else if( s == 'f' ) {
109
    s <- 'her'
110
  }
111
  else {
112
    s <- 'theirs'
113
  }
114
115
  s
116
}
117
118
pro.noun <- function( s ) {
119
  if( s == 'm' ) {
120
    s = 'man'
121
  }
122
  else if( s == 'f' ) {
123
    s <- 'woman'
124
  }
125
126
  s
127
}
128
1129
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) ![Release Date](https://img.shields.io/github/release-date/DaveJarvis/keenwrite?color=red&style=flat&label=Release%20Date) ![Release 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 text editor that uses [interpolated strings](https://en.wikipedia.org/wiki/String_interpolation) to reference values defined externally.
6
7
## Download
8
9
Download one of the following editions:
10
11
* [Windows](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.exe)
12
* [Linux](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.bin)
13
* [Java Archive](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/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
When upgrading to a new version, delete the following directory:
22
23
    C:\Users\%USERNAME%\AppData\Local\warp\packages\keenwrite.exe
24
25
Double-click the application to start; give the application permission to run.
26
27
### Linux
28
29
Execute the following commands in a terminal:
30
31
``` bash
32
chmod +x keenwrite.bin
33
./keenwrite.bin
34
```
35
36
### Other
37
38
On other platforms, start the application as follows:
39
40
1. Download the *full version* of the Java Runtime Environment, [JRE 17](https://bell-sw.com/pages/downloads/?version=java-17).
41
1. Install the JRE.
42
1. Open a terminal window.
43
1. Verify the installation: `java -version`
44
1. Make `keenwrite.sh` executable.
45
1. Run: `./keenwrite.sh`
46
47
The application is started.
48
49
## Features
50
51
The application offers:
52
53
* User-defined interpolated strings
54
* Auto-complete variable names based on variable values
55
* High-quality PDF exports
56
* Real-time spell check
57
* Real-time rendering of math using TeX notation
58
* Real-time document statistics (with CJK word separation)
59
* Diagrams: Mermaid, GraphViz, UML, sequence, timing, and more
60
* Dark, custom, and responsive user interface skins
61
* Integrated file manager
62
* Interactive document outline
63
* Internationalized font support (e.g., Chinese, Japanese, Korean, etc.)
64
* Support for Pandoc's fenced div extended attribute syntax
65
* R integration
66
* Customizable user interface having detachable tabs
67
* Platform-independent (Windows, Linux, MacOS)
68
69
## Usage
70
71
Read the [detailed documentation](docs/README.md) for using the application.
72
73
### Skins
74
75
Read the [skins documentation](docs/skins.md) to learn about how to change
76
the user interface appearance.
77
78
## Screenshots
79
80
See [screenshots](docs/screenshots.md) for visuals.
81
82
## License
83
84
This software is licensed under the [BSD 2-Clause License](LICENSE.md) and
85
based on [Markdown-Writer-FX](licenses/MARKDOWN-WRITER-FX.md).
86
187
A README.zh-CN.md
1
# ![Logo](docs/images/app-title.zh-CN.png)
2
3
智能写入是一个文本编辑器,它使用插值字符串引用外部定义的值。
4
5
## 下载
6
7
下载以下版本之一:
8
9
* [Windows](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.exe)
10
* [Linux](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.bin)
11
* [Java Archive](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.jar)
12
13
## 跑
14
15
在第一次运行期间,应用程序将自身解压到本地目录中。随后的启动会更快。
16
17
### Windows
18
19
双击应用程序以启动。您必须授予应用程序运行权限。 
20
21
升级时,删除以下目录:
22
23
    C:\Users\%USERNAME%\AppData\Local\warp\packages\keenwrite.exe
24
25
### Linux
26
27
执行以下命令:
28
29
``` bash
30
chmod +x keenwrite.bin
31
./keenwrite.bin
32
```
33
34
### Other
35
36
Download and install a full version of [OpenJDK 15](https://bell-sw.com/pages/downloads/?version=java-15#mn) that includes JavaFX module support, then run:
37
38
``` bash
39
java -jar keenwrite.jar
40
```
41
42
## 特征
43
44
* 用户定义的插值字符串
45
* 带变量替换的实时预览
46
* 基于变量值自动完成变量名
47
* 独立于操作系统
48
* 打字时拼写检查
49
* 使用TeX的子集编写数学公式
50
* 嵌入R语句
51
52
## 软件使用
53
54
See the [detailed documentation](docs/README.md) for information about
55
using the application.
56
57
## 截图
58
59
![GraphViz Diagram Screenshot](docs/images/screenshots/01.png)
60
61
![Korean Poem Screenshot](docs/images/screenshots/02.png)
62
63
![TeX Equations Screenshot](docs/images/screenshots/03.png)
64
65
66
## 软件许可证
67
68
This software is licensed under the [BSD 2-Clause License](LICENSE.md) and
69
based on [Markdown-Writer-FX](licenses/MARKDOWN-WRITER-FX.md).
70
171
A build.gradle
1
plugins {
2
  id 'application'
3
  id 'org.openjfx.javafxplugin' version '0.0.10'
4
  id 'com.palantir.git-version' version '0.12.3'
5
}
6
7
repositories {
8
  mavenCentral()
9
10
  maven {
11
    url 'https://oss.sonatype.org/content/repositories/snapshots/'
12
  }
13
14
  maven {
15
    url "https://nexus.bedatadriven.com/content/groups/public"
16
  }
17
}
18
19
// Assume a cross-platform überjar unless targetOs is set.
20
String[] os = ["win", "mac", "linux"]
21
22
if (project.hasProperty('targetOs')) {
23
  if ("windows" == targetOs) {
24
    os = ["win"]
25
  } else {
26
    os = [targetOs]
27
  }
28
}
29
30
def moduleSecurity = [
31
    "--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED",
32
    "--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED",
33
    "--add-opens=javafx.graphics/javafx.scene.text=ALL-UNNAMED",
34
    "--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED",
35
    "--add-opens=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED",
36
    "--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED",
37
    "--add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED",
38
    "--add-exports=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED",
39
    "--add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED",
40
    "--add-exports=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED",
41
    "--add-exports=javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED",
42
    "--add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED",
43
]
44
45
javafx {
46
  version = "17"
47
  modules = ['javafx.controls', 'javafx.swing']
48
  configuration = 'compileOnly'
49
}
50
51
dependencies {
52
  def v_junit = '5.8.1'
53
  def v_flexmark = '0.62.2'
54
  def v_jackson = '2.13.0'
55
  def v_batik = '1.14'
56
  def v_wheatsheaf = '2.0.1'
57
58
  // JavaFX
59
  // TODO: Reinstate when JDK 17-compatible release is published
60
  //implementation 'org.controlsfx:controlsfx:11.1.0'
61
  implementation 'org.fxmisc.richtext:richtextfx:0.10.7'
62
  implementation 'org.fxmisc.flowless:flowless:0.6.7'
63
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
64
  implementation 'com.miglayout:miglayout-javafx:11.0'
65
  implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.8.0'
66
67
  // Pure JavaFX File Chooser
68
  implementation "com.io7m.jwheatsheaf:com.io7m.jwheatsheaf:${v_wheatsheaf}"
69
  implementation "com.io7m.jwheatsheaf:com.io7m.jwheatsheaf.api:${v_wheatsheaf}"
70
  implementation "com.io7m.jwheatsheaf:com.io7m.jwheatsheaf.ui:${v_wheatsheaf}"
71
72
  // Markdown
73
  implementation "com.vladsch.flexmark:flexmark:${v_flexmark}"
74
  implementation "com.vladsch.flexmark:flexmark-ext-definition:${v_flexmark}"
75
  implementation "com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:${v_flexmark}"
76
  implementation "com.vladsch.flexmark:flexmark-ext-superscript:${v_flexmark}"
77
  implementation "com.vladsch.flexmark:flexmark-ext-tables:${v_flexmark}"
78
  implementation "com.vladsch.flexmark:flexmark-ext-typographic:${v_flexmark}"
79
80
  // YAML
81
  implementation "com.fasterxml.jackson.core:jackson-core:${v_jackson}"
82
  implementation "com.fasterxml.jackson.core:jackson-databind:${v_jackson}"
83
  implementation "com.fasterxml.jackson.core:jackson-annotations:${v_jackson}"
84
  implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${v_jackson}"
85
  implementation 'org.yaml:snakeyaml:1.29'
86
87
  // XML
88
  implementation 'com.ximpleware:vtd-xml:2.13.4'
89
90
  // HTML parsing and rendering
91
  implementation 'org.jsoup:jsoup:1.14.3'
92
  // TODO: Wait for https://github.com/flyingsaucerproject/flyingsaucer/pull/170
93
  //implementation 'org.xhtmlrenderer:flying-saucer-core:9.1.22'
94
95
  // R
96
  implementation 'org.renjin:renjin-script-engine:3.5-beta76'
97
98
  // SVG
99
  implementation "org.apache.xmlgraphics:batik-anim:${v_batik}"
100
  implementation "org.apache.xmlgraphics:batik-awt-util:${v_batik}"
101
  implementation "org.apache.xmlgraphics:batik-bridge:${v_batik}"
102
  implementation "org.apache.xmlgraphics:batik-css:${v_batik}"
103
  implementation "org.apache.xmlgraphics:batik-dom:${v_batik}"
104
  implementation "org.apache.xmlgraphics:batik-ext:${v_batik}"
105
  implementation "org.apache.xmlgraphics:batik-gvt:${v_batik}"
106
  implementation "org.apache.xmlgraphics:batik-parser:${v_batik}"
107
  implementation "org.apache.xmlgraphics:batik-script:${v_batik}"
108
  implementation "org.apache.xmlgraphics:batik-svg-dom:${v_batik}"
109
  implementation "org.apache.xmlgraphics:batik-svggen:${v_batik}"
110
  implementation "org.apache.xmlgraphics:batik-transcoder:${v_batik}"
111
  implementation "org.apache.xmlgraphics:batik-rasterizer:${v_batik}"
112
  implementation "org.apache.xmlgraphics:batik-util:${v_batik}"
113
  implementation "org.apache.xmlgraphics:batik-xml:${v_batik}"
114
115
  // Misc.
116
  implementation 'org.ahocorasick:ahocorasick:0.6.3'
117
  implementation 'org.apache.commons:commons-configuration2:2.7'
118
  implementation 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
119
  implementation 'javax.validation:validation-api:2.0.1.Final'
120
  implementation 'org.greenrobot:eventbus:3.2.0'
121
122
  implementation 'org.apache.commons:commons-configuration2:2.7'
123
  //noinspection GradlePackageUpdate
124
  implementation 'commons-beanutils:commons-beanutils:1.9.4'
125
126
  // Spelling, TeX, Docking, KeenQuotes
127
  implementation fileTree(include: ['**/*.jar'], dir: 'libs')
128
129
  def fx = ['controls', 'graphics', 'fxml', 'swing']
130
131
  fx.each { fxitem ->
132
    os.each { ositem ->
133
      runtimeOnly "org.openjfx:javafx-${fxitem}:${javafx.version}:${ositem}"
134
    }
135
  }
136
137
  testImplementation "org.testfx:testfx-junit5:4.0.16-alpha"
138
  testImplementation "org.junit.jupiter:junit-jupiter-api:${v_junit}"
139
  testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
140
}
141
142
compileJava {
143
  options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
144
}
145
146
def resourceDir = sourceSets.main.resources.srcDirs[0]
147
148
final Properties config = new Properties()
149
final File configFile = file("${resourceDir}/bootstrap.properties")
150
final FileInputStream configStream = new FileInputStream(configFile)
151
config.load(configStream)
152
configStream.close()
153
154
final String applicationName = config.get("application.title").toString().toLowerCase()
155
final String applicationClass = "com.${applicationName}.Launcher"
156
157
application {
158
  mainClass.set(applicationClass)
159
  applicationDefaultJvmArgs = moduleSecurity
160
}
161
162
version = gitVersion()
163
164
final File propertiesFile = new File("${resourceDir}/com/${applicationName}/app.properties")
165
propertiesFile.write("application.version=${version}")
166
167
jar {
168
  duplicatesStrategy = DuplicatesStrategy.EXCLUDE
169
170
  doFirst {
171
    manifest {
172
      attributes 'Main-Class': applicationClass
173
    }
174
  }
175
176
  from {
177
    (configurations.runtimeClasspath.findAll { !it.path.endsWith(".pom") }).collect {
178
      it.isDirectory() ? it : zipTree(it)
179
    }
180
  }
181
182
  archiveFileName = "${applicationName}.jar"
183
184
  exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
185
}
186
187
distributions {
188
  main {
189
    distributionBaseName = applicationName
190
    contents {
191
      from { ['LICENSE.md', 'README.md'] }
192
      into('images') {
193
        from { 'images' }
194
      }
195
    }
196
  }
197
}
198
199
test {
200
  useJUnitPlatform()
201
202
  doFirst {
203
    jvmArgs = moduleSecurity
204
  }
205
206
  testLogging {
207
    exceptionFormat = 'full'
208
  }
209
}
1210
A docs/README.md
1
# Documentation
2
3
The following documents have additional details about using the editor:
4
5
* [div.md](div.md) -- Syntax for annotated text (fenced divs)
6
* [i18n.md](i18n.md) -- Internationalization features
7
* [r.md](r.md) -- R functions within R Markdown documents
8
* [samples](samples) -- Example documents
9
* [skins.md](skins.md) -- User interface customization
10
* [svg.md](svg.md) -- Resolve issues with some SVG files
11
* [typesetting.md](typesetting.md) -- Document typesetting
12
* [variables.md](variables.md) -- Variable definitions and interpolation
13
14
# Contributions
15
16
* [credits.md](credits.md) -- Thanks to authors of contributing projects
17
* [licenses](licenses) -- Third-party licenses
18
119
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/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/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/r.md
1
# Introduction
2
3
This document describes how to use the [R](https://www.r-project.org/)
4
programming language from within the application. The application uses an
5
interpreter known as [Renjin](https://www.renjin.org/) to integrate with R.
6
7
# Hello world
8
9
Complete the following steps to see R in action:
10
11
1. Start the application.
12
1. Click **File → New** to create a new file.
13
1. Click **File → Save As**.
14
1. Set **Name** to: `addition.Rmd`
15
1. Click **Save**.
16
17
Setting the file name extension tells the application what processor to
18
use when transforming the contents for display in the preview pane. Continue
19
by typing in the following text, including the backticks:
20
21
```r
22
`r#1 + 1`
23
```
24
25
The preview pane shows the result of `1` plus `1`:
26
27
```
28
2.0
29
```
30
31
# Bootstrap script
32
33
Being able to run R code while editing an R Markdown document is convenient.
34
Having the ability to call functions is where the power of R can be
35
leveraged.
36
37
Complete the following steps to call an R function from your own library:
38
39
1. Click **File → New** to create a new file.
40
1. Click **File → Save As**.
41
1. Browse to your home directory.
42
1. Set **Name** to: `library.R`.
43
1. Click **Save**.
44
1. Set the contents to:
45
    ``` r
46
    sum <- function( a, b ) {
47
      a + b
48
    }
49
    ```
50
1. Click the **Save** icon.
51
1. Click **R → Script**.
52
1. Set the **R Startup Script** contents to:
53
    ``` r
54
    source( 'library.R' );
55
    ```
56
1. Click **OK**.
57
1. Create a new file.
58
1. Set the contents to:
59
    ``` r
60
    `r#sum( 5, 5 )`
61
    ```
62
1. Save the file as `sum.R`.
63
64
The preview panel shows the result of calling the `sum` function:
65
66
```
67
10.0
68
```
69
70
This shows how the bootstrap script can load `library.R`, which defines
71
a `sum` function that is called by name in the Markdown document.
72
73
# Working directory
74
75
R files may be sourced from any directory, not just the user's home
76
directory. Accomplish this as follows:
77
78
1. Click **R → Directory**.
79
1. Set **Directory** to a different directory.
80
1. Click **OK**.
81
1. Create the directory if it does not exist.
82
1. Move `library.R` into the directory.
83
1. Append a new function to `library.R` as follows:
84
    ``` r
85
    mul <- function( a, b ) {
86
      a * b
87
    }
88
    ```
89
1. Click **R → Script**.
90
1. Set the **R Startup Script** contents to:
91
    ``` r
92
    setwd( '{{application.r.working.directory}}' );
93
    source( 'library.R' );
94
    ```
95
1. Change `sum.Rmd` to:
96
    ``` r
97
    `r#mul( 5, 5 )`
98
    ```
99
1. Close the file `sum.Rmd`.
100
1. Confirm saving the file when prompted.
101
1. Re-open `sum.Rmd`.
102
103
The preview panel shows:
104
105
```
106
25.0
107
```
108
109
Calling `setwd` using `'{{application.r.working.directory}}'` changes the
110
working directory where the R engine searches for source files.
111
112
# YAML 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.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/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 docs/video/.gitignore
1
*.avi
2
*.wav
3
*.png
4
*.mp4
5
*.mp3
6
17
A docs/video/title.blend
Binary file
A docs/video/traced-text.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
   xmlns:cc="http://creativecommons.org/ns#"
5
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
   xmlns:svg="http://www.w3.org/2000/svg"
7
   xmlns="http://www.w3.org/2000/svg"
8
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
   width="211.87125mm"
11
   height="56.576mm"
12
   viewBox="0 0 211.87125 56.576"
13
   version="1.1"
14
   id="svg8"
15
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
16
   sodipodi:docname="traced-text.svg">
17
  <defs
18
     id="defs2" />
19
  <sodipodi:namedview
20
     id="base"
21
     pagecolor="#ffffff"
22
     bordercolor="#666666"
23
     borderopacity="1.0"
24
     inkscape:pageopacity="0.0"
25
     inkscape:pageshadow="2"
26
     inkscape:zoom="1.4142136"
27
     inkscape:cx="367.6429"
28
     inkscape:cy="129.23348"
29
     inkscape:document-units="mm"
30
     inkscape:current-layer="layer1"
31
     inkscape:document-rotation="0"
32
     showgrid="false"
33
     fit-margin-top="10"
34
     fit-margin-left="10"
35
     fit-margin-right="10"
36
     fit-margin-bottom="10" />
37
  <metadata
38
     id="metadata5">
39
    <rdf:RDF>
40
      <cc:Work
41
         rdf:about="">
42
        <dc:format>image/svg+xml</dc:format>
43
        <dc:type
44
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
45
        <dc:title></dc:title>
46
      </cc:Work>
47
    </rdf:RDF>
48
  </metadata>
49
  <g
50
     inkscape:label="Layer 1"
51
     inkscape:groupmode="layer"
52
     id="layer1"
53
     transform="translate(-1.4263456,-106.05539)">
54
    <text
55
       xml:space="preserve"
56
       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;line-height:1.25;font-family:'Alex Brush';-inkscape-font-specification:'Alex Brush, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
57
       x="12.289946"
58
       y="147.80539"
59
       id="text835"><tspan
60
         sodipodi:role="line"
61
         id="tspan833"
62
         x="12.289946"
63
         y="147.80539"
64
         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Alex Brush';-inkscape-font-specification:'Alex Brush, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583">Scrivenvar</tspan></text>
65
    <path
66
       sodipodi:nodetypes="cssssc"
67
       id="path859"
68
       d="m 47.37594,126.25759 c 5.878995,0.58684 8.108819,-2.8906 6.991897,-5.39049 -4.163299,-9.31827 -26.104298,-1.57165 -26.47428,4.67958 -0.290066,4.90098 4.329286,5.69691 9.138161,6.81221 4.75698,1.10326 9.980125,1.72503 10.138085,4.5281 0.511551,9.07772 -11.28247,13.50974 -21.577969,13.14767"
69
       style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#4eb059;stroke-width:0.132292;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
70
    <path
71
       sodipodi:nodetypes="cssc"
72
       id="path861"
73
       d="m 61.538159,137.91416 c 8.229745,-12.05206 -9.227635,-1.22793 -10.272792,5.40306 -0.929347,5.89623 4.566953,5.63307 9.024721,2.11036 5.095939,-4.02702 8.706628,-8.11599 12.031905,-13.9409"
74
       style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#4eb059;stroke-width:0.132292;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
75
    <path
76
       sodipodi:nodetypes="ccssc"
77
       id="path863"
78
       d="m 72.321991,131.48668 c 3.834665,-5.91801 -1.131419,0.83402 0.75311,2.48796 2.189872,1.94816 6.580549,-2.11016 5.400159,-0.72958 -0.854851,0.99983 -9.857527,10.41157 -5.126492,13.80621 2.461609,1.76627 8.936925,-2.58857 11.751532,-5.5313"
79
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
80
    <path
81
       sodipodi:nodetypes="csssc"
82
       id="path963"
83
       d="m 85.1003,141.51997 c 0,0 6.754775,-9.24626 6.743495,-8.01563 -0.01328,1.44899 -5.040946,6.68411 -6.63123,10.08427 -0.90584,1.93677 -0.626402,4.68995 2.447111,4.25184 1.468017,-0.20926 5.212094,-2.44913 10.029682,-7.66684"
84
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
85
    <path
86
       sodipodi:nodetypes="csccc"
87
       id="path965"
88
       d="m 97.689357,140.17361 c 0,0 3.797813,-8.42805 4.594353,-7.95573 0.58723,0.34822 -6.526154,13.32545 -5.477472,14.50806 2.435753,1.7862 19.064212,-11.51107 15.563042,-16.73913 -0.73409,-1.34256 -3.18033,-1.99148 -3.18033,-1.99148"
89
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
90
    <path
91
       sodipodi:nodetypes="csssc"
92
       d="m 113.37707,141.34636 c 4.23091,0.29831 11.94363,-4.90618 10.94354,-7.7799 -1.29105,-3.70978 -8.05529,1.78774 -9.69006,3.68511 -4.97668,5.77609 -4.11733,10.31478 -0.92228,10.61275 3.436,0.32045 8.83724,-3.13085 13.69698,-9.62574"
93
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
94
       id="path967" />
95
    <path
96
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
97
       d="m 146.49943,140.17361 c 0,0 3.79781,-8.42805 4.59435,-7.95573 0.58723,0.34822 -6.52616,13.32545 -5.47747,14.50806 2.43575,1.7862 19.06421,-11.51107 15.56304,-16.73913 -0.73409,-1.34256 -3.10123,-1.96263 -3.10123,-1.96263"
98
       id="path970"
99
       sodipodi:nodetypes="csccc" />
100
    <path
101
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
102
       d="m 188.80833,131.36316 c 3.83466,-5.91801 -1.13142,0.83402 0.75311,2.48796 2.18987,1.94816 6.58055,-2.11016 5.40016,-0.72958 -0.85485,0.99983 -9.98962,10.60367 -5.12649,13.80621 2.8329,1.86556 9.63808,-2.25455 13.61435,-8.05051"
103
       id="path987"
104
       sodipodi:nodetypes="ccssc" />
105
    <path
106
       sodipodi:nodetypes="ccsssccc"
107
       d="m 127.40525,138.23858 c 1.53961,-1.23511 5.06979,-6.4876 5.94375,-5.82833 -1.7832,2.5949 -8.95273,13.68991 -7.1105,13.94503 1.19011,0.16482 7.25976,-8.00422 10.87675,-10.901 1.83151,-1.46682 4.35069,-3.49971 5.94917,-3.73267 1.66376,-0.24247 -1.93803,2.90472 -3.80099,5.77097 -1.36327,2.14988 -4.92421,8.02816 -2.69839,9.35481 3.0826,1.21137 7.35116,-4.27566 9.93439,-6.67382"
108
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
109
       id="path989" />
110
    <path
111
       sodipodi:nodetypes="csscsc"
112
       id="path992"
113
       d="m 176.85645,132.78853 c -3.26879,-6.24001 -16.43513,7.99373 -16.14879,12.14556 0.1378,1.99804 2.16776,3.14653 3.8818,2.44798 4.44909,-1.8132 11.93103,-13.58278 13.4413,-14.18515 -6.97685,9.84354 -7.04537,13.29844 -4.02229,13.83262 2.49715,0.44125 8.94275,-6.11484 14.79986,-15.66638"
114
       style="fill:none;fill-opacity:0.8;stroke:#4eb059;stroke-width:0.132292;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
115
  </g>
116
</svg>
1117
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 images/logo64.png
Binary file
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
readonly OPT_JAVA=$(cat << END_OF_ARGS
15
--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED \
16
--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED \
17
--add-opens=javafx.graphics/javafx.scene.text=ALL-UNNAMED \
18
--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED \
19
--add-opens=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED \
20
--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED \
21
--add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED \
22
--add-exports=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED \
23
--add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED \
24
--add-exports=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED \
25
--add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED
26
END_OF_ARGS
27
)
28
29
ARG_JAVA_OS="linux"
30
ARG_JAVA_ARCH="amd64"
31
ARG_JAVA_VERSION="17"
32
ARG_JAVA_UPDATE="35"
33
ARG_JAVA_DIR="java"
34
35
ARG_DIR_DIST="dist"
36
37
FILE_DIST_EXEC="run.sh"
38
39
ARG_PATH_DIST_JAR="${SCRIPT_DIR}/build/libs/${FILE_APP_JAR}"
40
41
DEPENDENCIES=(
42
  "gradle,https://gradle.org"
43
  "warp-packer,https://github.com/dgiagio/warp"
44
  "tar,https://www.gnu.org/software/tar"
45
  "unzip,http://infozip.sourceforge.net"
46
)
47
48
ARGUMENTS+=(
49
  "a,arch,Target operating system architecture (amd64)"
50
  "o,os,Target operating system (linux, windows, mac)"
51
  "u,update,Java update version number (${ARG_JAVA_UPDATE})"
52
  "v,version,Full Java version (${ARG_JAVA_VERSION})"
53
)
54
55
ARCHIVE_EXT="tar.gz"
56
ARCHIVE_APP="tar xf"
57
APP_EXTENSION="bin"
58
59
# ---------------------------------------------------------------------------
60
# Generates
61
# ---------------------------------------------------------------------------
62
execute() {
63
  $do_configure_target
64
  $do_build
65
  $do_clean
66
67
  pushd "${ARG_DIR_DIST}" > /dev/null 2>&1
68
69
  $do_extract_java
70
  $do_create_launch_script
71
  $do_copy_archive
72
73
  popd > /dev/null 2>&1
74
75
  $do_create_launcher
76
77
  return 1
78
}
79
80
# ---------------------------------------------------------------------------
81
# Configure platform-specific commands and file names.
82
# ---------------------------------------------------------------------------
83
utile_configure_target() {
84
  if [ "${ARG_JAVA_OS}" = "windows" ]; then
85
    ARCHIVE_EXT="zip"
86
    ARCHIVE_APP="unzip -qq"
87
    FILE_DIST_EXEC="run.bat"
88
    APP_EXTENSION="exe"
89
    do_create_launch_script=utile_create_launch_script_windows
90
  fi
91
}
92
93
# ---------------------------------------------------------------------------
94
# Build platform-specific überjar.
95
# ---------------------------------------------------------------------------
96
utile_build() {
97
  $log "Delete ${ARG_PATH_DIST_JAR}"
98
  rm -f "${ARG_PATH_DIST_JAR}"
99
100
  $log "Build application for ${ARG_JAVA_OS}"
101
  gradle clean jar -PtargetOs="${ARG_JAVA_OS}"
102
}
103
104
# ---------------------------------------------------------------------------
105
# Purges the existing distribution directory to recreate the launcher.
106
# This refreshes the JRE from the downloaded archive.
107
# ---------------------------------------------------------------------------
108
utile_clean() {
109
  $log "Recreate ${ARG_DIR_DIST}"
110
  rm -rf "${ARG_DIR_DIST}"
111
  mkdir -p "${ARG_DIR_DIST}"
112
}
113
114
# ---------------------------------------------------------------------------
115
# Extract platform-specific Java Runtime Environment. This will download
116
# and cache the required Java Runtime Environment for the target platform.
117
# On subsequent runs, the cached version is used, instead of issuing another
118
# download.
119
# ---------------------------------------------------------------------------
120
utile_extract_java() {
121
  $log "Extract Java"
122
  local -r java_vm="jre"
123
  local -r java_version="${ARG_JAVA_VERSION}+${ARG_JAVA_UPDATE}"
124
  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}"
125
126
  local -r file_java="${java_vm}-${java_version}-${ARG_JAVA_OS}-${ARG_JAVA_ARCH}.${ARCHIVE_EXT}"
127
  local -r path_java="/tmp/${file_java}"
128
129
  # File must have contents.
130
  if [ ! -s ${path_java} ]; then
131
    $log "Download ${url_java} to ${path_java}"
132
    wget -q "${url_java}" -O "${path_java}"
133
  fi
134
135
  $log "Unpack ${path_java}"
136
  $ARCHIVE_APP "${path_java}"
137
138
  local -r dir_java="${java_vm}-${ARG_JAVA_VERSION}-full"
139
140
  $log "Rename ${dir_java} to ${ARG_JAVA_DIR}"
141
  mv "${dir_java}" "${ARG_JAVA_DIR}"
142
}
143
144
# ---------------------------------------------------------------------------
145
# Create Linux-specific launch script.
146
# ---------------------------------------------------------------------------
147
utile_create_launch_script_linux() {
148
  $log "Create Linux launch script"
149
150
  cat > "${FILE_DIST_EXEC}" << __EOT
151
#!/usr/bin/env bash
152
153
readonly SCRIPT_SRC="\$(dirname "\${BASH_SOURCE[\${#BASH_SOURCE[@]} - 1]}")"
154
155
"\${SCRIPT_SRC}/${ARG_JAVA_DIR}/bin/java" ${OPT_JAVA} -jar "\${SCRIPT_SRC}/${FILE_APP_JAR}" "\$@" 2>/dev/null &
156
__EOT
157
158
  chmod +x "${FILE_DIST_EXEC}"
159
}
160
161
# ---------------------------------------------------------------------------
162
# Create Windows-specific launch script.
163
# ---------------------------------------------------------------------------
164
utile_create_launch_script_windows() {
165
  $log "Create Windows launch script"
166
167
  cat > "${FILE_DIST_EXEC}" << __EOT
168
@echo off
169
170
set SCRIPT_DIR=%~dp0
171
"%SCRIPT_DIR%\\${ARG_JAVA_DIR}\\bin\\java" ${OPT_JAVA} -jar "%SCRIPT_DIR%\\${APP_NAME}.jar" %* 2>nul
172
__EOT
173
174
  # Convert Unix end of line characters (\n) to Windows format (\r\n).
175
  # This avoids any potential line conversion issues with the repository.
176
  sed -i 's/$/\r/' "${FILE_DIST_EXEC}"
177
}
178
179
# ---------------------------------------------------------------------------
180
# Copy application überjar.
181
# ---------------------------------------------------------------------------
182
utile_copy_archive() {
183
  $log "Create copy of ${FILE_APP_JAR}"
184
  cp "${ARG_PATH_DIST_JAR}" "${FILE_APP_JAR}"
185
}
186
187
# ---------------------------------------------------------------------------
188
# Create platform-specific launcher binary.
189
# ---------------------------------------------------------------------------
190
utile_create_launcher() {
191
  local -r FILE_APP_NAME="${APP_NAME}.${APP_EXTENSION}"
192
  $log "Create ${FILE_APP_NAME}"
193
194
  # Warp-packer does not seem to overwrite the file.
195
  rm -f "${FILE_APP_NAME}"
196
197
  # Download uses amd64, but warp-packer differs.
198
  if [ "${ARG_JAVA_ARCH}" = "amd64" ]; then
199
    ARG_JAVA_ARCH="x64"
200
  fi
201
202
  warp-packer \
203
    --arch "${ARG_JAVA_OS}-${ARG_JAVA_ARCH}" \
204
    --input_dir "${ARG_DIR_DIST}" \
205
    --exec "${FILE_DIST_EXEC}" \
206
    --output "${FILE_APP_NAME}" > /dev/null
207
208
  chmod +x "${FILE_APP_NAME}"
209
}
210
211
argument() {
212
  local consume=2
213
214
  case "$1" in
215
    -a|--arch)
216
    ARG_JAVA_ARCH="$2"
217
    ;;
218
    -o|--os)
219
    ARG_JAVA_OS="$2"
220
    ;;
221
    -u|--update)
222
    ARG_JAVA_UPDATE="$2"
223
    ;;
224
    -v|--version)
225
    ARG_JAVA_VERSION="$2"
226
    ;;
227
  esac
228
229
  return ${consume}
230
}
231
232
do_configure_target=utile_configure_target
233
do_build=utile_build
234
do_clean=utile_clean
235
do_extract_java=utile_extract_java
236
do_create_launch_script=utile_create_launch_script_linux
237
do_copy_archive=utile_copy_archive
238
do_create_launcher=utile_create_launcher
239
240
main "$@"
241
1242
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.text=ALL-UNNAMED \
9
  --add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED \
10
  --add-exports=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED \
11
  --add-exports=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED \
12
  --add-exports=javafx.graphics/com.sun.javafx.text=ALL-UNNAMED \
13
  --add-exports=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED \
14
  --add-exports=javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED \
15
  --add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED \
16
  -jar build/libs/keenwrite.jar
17
118
A libs/controlsfx-11.1.1.jar
Binary file
A libs/flying-saucer-core-9.1.22.jar
Binary file
A libs/jmathtex.jar
Binary file
A libs/jsymspell/jsymspell-core-1.0.jar
Binary file
A libs/keenquotes.jar
Binary file
A libs/tiwulfx-dock-0.1.jar
Binary file
A libs/tokenize.jar
Binary file
A release.sh
1
#!/usr/bin/env bash
2
3
# ---------------------------------------------------------------------------
4
# This script builds Windows, Linux, and Java archive binaries for a
5
# release.
6
# ---------------------------------------------------------------------------
7
8
source $HOME/bin/build-template
9
10
readonly FILE_PROPERTIES="${SCRIPT_DIR}/src/main/resources/bootstrap.properties"
11
readonly BIN_INSTALLER="${SCRIPT_DIR}/installer.sh"
12
13
DEPENDENCIES=(
14
  "gradle,https://gradle.org"
15
  "zip,http://infozip.sourceforge.net"
16
  "${FILE_PROPERTIES},File containing application name"
17
)
18
19
execute() {
20
  $log "Build Windows installer binary"
21
  ${BIN_INSTALLER} -o windows
22
23
  $log "Build Linux installer binary"
24
  ${BIN_INSTALLER} -o linux
25
26
  $log "Build Java archive"
27
  gradle clean jar
28
  mv "build/libs/${application_title}.jar" .
29
}
30
31
preprocess() {
32
  while IFS='=' read -r key value; do
33
    if [[ "${key}" = "" || "${key}" = "#"* ]]; then
34
      continue
35
    fi
36
37
    key=$(echo $key | tr '.' '_')
38
    eval ${key}=\${value}
39
  done < "${FILE_PROPERTIES}"
40
41
  application_title="${application_title,,}"
42
43
  return 1
44
}
45
46
main "$@"
47
148
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/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 settings.gradle
11
A src/main/java/com/keenwrite/AbstractFileFactory.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.io.FileType;
5
6
import java.nio.file.Path;
7
8
import static com.keenwrite.constants.Constants.GLOB_PREFIX_FILE;
9
import static com.keenwrite.constants.Constants.sSettings;
10
import static com.keenwrite.io.FileType.UNKNOWN;
11
import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate;
12
13
/**
14
 * Provides common behaviours for factories that instantiate classes based on
15
 * file type.
16
 */
17
public abstract class AbstractFileFactory {
18
19
  /**
20
   * Determines the file type from the path extension. This should only be
21
   * called when it is known that the file type won't be a definition file
22
   * (e.g., YAML or other definition source), but rather an editable file
23
   * (e.g., Markdown, R Markdown, etc.).
24
   *
25
   * @param path The path with a file name extension.
26
   * @return The FileType for the given path.
27
   */
28
  public static FileType lookup( final Path path ) {
29
    assert path != null;
30
31
    return lookup( path, GLOB_PREFIX_FILE );
32
  }
33
34
  /**
35
   * Creates a file type that corresponds to the given path.
36
   *
37
   * @param path   Reference to a variable definition file.
38
   * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE.
39
   * @return The file type that corresponds to the given path.
40
   */
41
  protected static FileType lookup( final Path path, final String prefix ) {
42
    assert path != null;
43
    assert prefix != null;
44
45
    final var keys = sSettings.getKeys( prefix );
46
47
    var found = false;
48
    var fileType = UNKNOWN;
49
50
    while( keys.hasNext() && !found ) {
51
      final var key = keys.next();
52
      final var patterns = sSettings.getStringSettingList( key );
53
      final var predicate = createFileTypePredicate( patterns );
54
55
      if( found = predicate.test( path.toFile() ) ) {
56
        // Remove the EXTENSIONS_PREFIX to get the file name extension mapped
57
        // to a standard name (as defined in the settings.properties file).
58
        final String suffix = key.replace( prefix + '.', "" );
59
        fileType = FileType.from( suffix );
60
      }
61
    }
62
63
    return fileType;
64
  }
65
}
166
A src/main/java/com/keenwrite/Bootstrap.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.constants.Constants;
5
6
import java.io.InputStream;
7
import java.util.Calendar;
8
import java.util.Properties;
9
10
import static org.apache.batik.util.ParsedURL.setGlobalUserAgent;
11
12
/**
13
 * Responsible for loading the bootstrap.properties file, which is
14
 * tactically located outside of the standard resource reverse domain name
15
 * namespace to avoid hard-coding the application name in many places.
16
 * Instead, the application name is located in the bootstrap file, which is
17
 * then used to look-up the remaining settings.
18
 * <p>
19
 * See {@link Constants#PATH_PROPERTIES_SETTINGS} for details.
20
 * </p>
21
 */
22
public final class Bootstrap {
23
  /**
24
   * Order matters, this must be populated before deriving the app title.
25
   */
26
  private static final Properties P = new Properties();
27
28
  static {
29
    try( final var in = openResource( "/bootstrap.properties" ) ) {
30
      P.load( in );
31
    } catch( final Exception ignored ) {
32
      // Bootstrap properties cannot be found, throw in the towel.
33
    }
34
  }
35
36
  public static final String APP_TITLE = P.getProperty( "application.title" );
37
  public static final String APP_TITLE_LOWERCASE = APP_TITLE.toLowerCase();
38
  public static final String APP_VERSION = Launcher.getVersion();
39
  public static final String APP_YEAR = getYear();
40
41
  static {
42
    System.setProperty( "http.agent", APP_TITLE + " " + APP_VERSION );
43
    setGlobalUserAgent( System.getProperty( "http.agent" ) );
44
  }
45
46
  @SuppressWarnings( "SameParameterValue" )
47
  private static InputStream openResource( final String path ) {
48
    return Constants.class.getResourceAsStream( path );
49
  }
50
51
  private static String getYear() {
52
    return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) );
53
  }
54
}
155
A src/main/java/com/keenwrite/Caret.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
3
4
import com.keenwrite.util.GenericBuilder;
5
import javafx.beans.value.ObservableValue;
6
import org.fxmisc.richtext.StyleClassedTextArea;
7
import org.fxmisc.richtext.model.Paragraph;
8
import org.reactfx.collection.LiveList;
9
10
import java.util.Collection;
11
12
import static com.keenwrite.constants.Constants.STATUS_BAR_LINE;
13
import static com.keenwrite.Messages.get;
14
15
/**
16
 * Represents the absolute, relative, and maximum position of the caret. The
17
 * caret position is a character offset into the text.
18
 */
19
public class Caret {
20
21
  public static GenericBuilder<Caret.Mutator, Caret> builder() {
22
    return GenericBuilder.of( Caret.Mutator::new, Caret::new );
23
  }
24
25
  /**
26
   * Used for building a new {@link Caret} instance.
27
   */
28
  public static class Mutator {
29
    /**
30
     * Caret's current paragraph index (i.e., current caret line number).
31
     */
32
    private ObservableValue<Integer> mParagraph;
33
34
    /**
35
     * Used to count the number of lines in the text editor document.
36
     */
37
    private LiveList<Paragraph<Collection<String>, String,
38
      Collection<String>>> mParagraphs;
39
40
    /**
41
     * Caret offset into the full text, represented as a string index.
42
     */
43
    private ObservableValue<Integer> mTextOffset;
44
45
    /**
46
     * Caret offset into the current paragraph, represented as a string index.
47
     */
48
    private ObservableValue<Integer> mParaOffset;
49
50
    /**
51
     * Total number of characters in the document.
52
     */
53
    private ObservableValue<Integer> mTextLength;
54
55
    /**
56
     * Configures this caret position using properties from the given editor.
57
     *
58
     * @param editor The text editor that has a caret with position properties.
59
     */
60
    public void setEditor( final StyleClassedTextArea editor ) {
61
      mParagraph = editor.currentParagraphProperty();
62
      mParagraphs = editor.getParagraphs();
63
      mParaOffset = editor.caretColumnProperty();
64
      mTextOffset = editor.caretPositionProperty();
65
      mTextLength = editor.lengthProperty();
66
    }
67
  }
68
69
  private final Mutator mMutator;
70
71
  /**
72
   * Force using the builder pattern.
73
   */
74
  private Caret( final Mutator mutator ) {
75
    assert mutator != null;
76
77
    mMutator = mutator;
78
  }
79
80
  /**
81
   * Allows observers to be notified when the value of the caret changes.
82
   *
83
   * @return An observer for the caret's document offset.
84
   */
85
  public ObservableValue<Integer> textOffsetProperty() {
86
    return mMutator.mTextOffset;
87
  }
88
89
  /**
90
   * Answers whether the caret's offset into the text is between the given
91
   * offsets.
92
   *
93
   * @param began Starting value compared against the caret's text offset.
94
   * @param ended Ending value compared against the caret's text offset.
95
   * @return {@code true} when the caret's text offset is between the given
96
   * values, inclusively (for either value).
97
   */
98
  public boolean isBetweenText( final int began, final int ended ) {
99
    final var offset = getTextOffset();
100
    return began <= offset && offset <= ended;
101
  }
102
103
  /**
104
   * Answers whether the caret's offset into the paragraph is before the given
105
   * offset.
106
   *
107
   * @param offset Compared against the caret's paragraph offset.
108
   * @return {@code true} the caret's offset is before the given offset.
109
   */
110
  public boolean isBeforeColumn( final int offset ) {
111
    return getParaOffset() < offset;
112
  }
113
114
  /**
115
   * Answers whether the caret's offset into the text is before the given
116
   * text offset.
117
   *
118
   * @param offset Compared against the caret's text offset.
119
   * @return {@code true} the caret's offset is after the given offset.
120
   */
121
  public boolean isAfterColumn( final int offset ) {
122
    return getParaOffset() > offset;
123
  }
124
125
  /**
126
   * Answers whether the caret's offset into the text exceeds the length of
127
   * the text.
128
   *
129
   * @return {@code true} when the caret is at the end of the text boundary.
130
   */
131
  public boolean isAfterText() {
132
    return getTextOffset() >= getTextLength();
133
  }
134
135
  public boolean isAfter( final int offset ) {
136
    return offset >= getTextOffset();
137
  }
138
139
  private int getParagraph() {
140
    return mMutator.mParagraph.getValue();
141
  }
142
143
  /**
144
   * Returns the number of lines in the text editor.
145
   *
146
   * @return The size of the text editor's paragraph list plus one.
147
   */
148
  private int getParagraphCount() {
149
    return mMutator.mParagraphs.size() + 1;
150
  }
151
152
  /**
153
   * Returns the absolute position of the caret within the entire document.
154
   *
155
   * @return A zero-based index of the caret position.
156
   */
157
  private int getTextOffset() {
158
    return mMutator.mTextOffset.getValue();
159
  }
160
161
  /**
162
   * Returns the position of the caret within the current paragraph being
163
   * edited.
164
   *
165
   * @return A zero-based index of the caret position relative to the
166
   * current paragraph.
167
   */
168
  private int getParaOffset() {
169
    return mMutator.mParaOffset.getValue();
170
  }
171
172
  /**
173
   * Returns the total number of characters in the document being edited.
174
   *
175
   * @return A zero-based count of the total characters in the document.
176
   */
177
  private int getTextLength() {
178
    return mMutator.mTextLength.getValue();
179
  }
180
181
  /**
182
   * Returns a human-readable string that shows the current caret position
183
   * within the text. Typically this will include the current line number,
184
   * the number of lines, and the character offset into the text.
185
   * <p>
186
   * If the {@link Caret} has not been properly built, this will return a
187
   * string for the status bar having all values set to zero. This can happen
188
   * during unit testing, but should not happen any other time.
189
   * </p>
190
   *
191
   * @return A string to present to an end user.
192
   */
193
  @Override
194
  public String toString() {
195
    try {
196
      return get( STATUS_BAR_LINE,
197
                  getParagraph() + 1,
198
                  getParagraphCount(),
199
                  getTextOffset() + 1 );
200
    } catch( final Exception ex ) {
201
      return get( STATUS_BAR_LINE, 0, 0, 0 );
202
    }
203
  }
204
}
1205
A src/main/java/com/keenwrite/DefinitionNameInjector.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.definition.DefinitionTreeItem;
7
import com.keenwrite.sigils.SigilOperator;
8
9
import static com.keenwrite.constants.Constants.*;
10
import static com.keenwrite.events.StatusEvent.clue;
11
12
/**
13
 * Provides the logic for injecting variable names within the editor.
14
 */
15
public final class DefinitionNameInjector {
16
17
  /**
18
   * Prevent instantiation.
19
   */
20
  private DefinitionNameInjector() {
21
  }
22
23
  /**
24
   * Find a node that matches the current word and substitute the definition
25
   * reference.
26
   */
27
  public static void autoinsert(
28
    final TextEditor editor,
29
    final TextDefinition definitions,
30
    final SigilOperator operator ) {
31
    try {
32
      if( definitions.isEmpty() ) {
33
        clue( STATUS_DEFINITION_EMPTY );
34
      }
35
      else {
36
        final var indexes = editor.getCaretWord();
37
        final var word = editor.getText( indexes );
38
39
        if( word.isBlank() ) {
40
          clue( STATUS_DEFINITION_BLANK );
41
        }
42
        else {
43
          final var leaf = findLeaf( definitions, word );
44
45
          if( leaf == null ) {
46
            clue( STATUS_DEFINITION_MISSING, word );
47
          }
48
          else {
49
            final var entokened = operator.entoken( leaf.toPath() );
50
            editor.replaceText( indexes, operator.apply( entokened ) );
51
            definitions.expand( leaf );
52
          }
53
        }
54
      }
55
    } catch( final Exception ex ) {
56
      clue( STATUS_DEFINITION_BLANK, ex );
57
    }
58
  }
59
60
  /**
61
   * Looks for the given word, matching first by exact, next by a starts-with
62
   * condition with diacritics replaced, then by containment.
63
   *
64
   * @param word Match the word by: exact, beginning, containment, or other.
65
   */
66
  @SuppressWarnings( "ConstantConditions" )
67
  private static DefinitionTreeItem<String> findLeaf(
68
    final TextDefinition definition, final String word ) {
69
    assert word != null;
70
71
    DefinitionTreeItem<String> leaf = null;
72
73
    leaf = leaf == null ? definition.findLeafExact( word ) : leaf;
74
    leaf = leaf == null ? definition.findLeafStartsWith( word ) : leaf;
75
    leaf = leaf == null ? definition.findLeafContains( word ) : leaf;
76
    leaf = leaf == null ? definition.findLeafContainsNoCase( word ) : leaf;
77
78
    return leaf;
79
  }
80
}
181
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 java.io.File;
5
import java.nio.file.Path;
6
7
import static org.apache.commons.io.FilenameUtils.removeExtension;
8
9
/**
10
 * Provides controls for processor behaviour when transforming input documents.
11
 */
12
public enum ExportFormat {
13
14
  /**
15
   * For HTML exports, encode TeX as SVG. Treat image links relatively.
16
   */
17
  HTML_TEX_SVG( ".html" ),
18
19
  /**
20
   * For HTML exports, encode TeX using {@code $} delimiters, suitable for
21
   * rendering by an external TeX typesetting engine (or online with KaTeX).
22
   * Treat image links relatively.
23
   */
24
  HTML_TEX_DELIMITED( ".html" ),
25
26
  /**
27
   * For XHTML exports, encode TeX using {@code $} delimiters.
28
   */
29
  XHTML_TEX( ".xml" ),
30
31
  /**
32
   * Indicates that the processors should export to a Markdown format.
33
   * Treat image links relatively.
34
   */
35
  MARKDOWN_PLAIN( ".out.md" ),
36
37
  /**
38
   * Exports as PDF file format.
39
   */
40
  APPLICATION_PDF( ".pdf" ),
41
42
  /**
43
   * Indicates no special export format is to be created. No extension is
44
   * applicable. Image links must use absolute directories.
45
   */
46
  NONE( "" );
47
48
  /**
49
   * Preferred file name extension for the given file type.
50
   */
51
  private final String mExtension;
52
53
  ExportFormat( final String extension ) {
54
    mExtension = extension;
55
  }
56
57
  /**
58
   * Returns the given {@link File} with its extension replaced by one that
59
   * matches this {@link ExportFormat} extension.
60
   *
61
   * @param file The file to perform an extension swap.
62
   * @return The given file with its extension replaced.
63
   */
64
  public File toExportFilename( final File file ) {
65
    return new File( removeExtension( file.getName() ) + mExtension );
66
  }
67
68
  /**
69
   * Delegates to {@link #toExportFilename(File)} after converting the given
70
   * {@link Path} to an instance of {@link File}.
71
   *
72
   * @param path The {@link Path} to convert to a {@link File}.
73
   * @return The given path with its extension replaced.
74
   */
75
  public File toExportFilename( final Path path ) {
76
    return toExportFilename( path.toFile() );
77
  }
78
}
179
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 java.io.IOException;
5
import java.io.InputStream;
6
import java.util.Properties;
7
8
import static com.keenwrite.Bootstrap.*;
9
import static com.keenwrite.PermissiveCertificate.installTrustManager;
10
import static java.lang.String.format;
11
12
/**
13
 * Launches the application using the {@link MainApp} class.
14
 *
15
 * <p>
16
 * This is required until modules are implemented, which may never happen
17
 * because the application should be ported away from Java and JavaFX.
18
 * </p>
19
 */
20
public final class Launcher {
21
  /**
22
   * Delegates to the application entry point.
23
   *
24
   * @param args Command-line arguments.
25
   */
26
  public static void main( final String[] args ) {
27
    try {
28
      installTrustManager();
29
      showAppInfo();
30
      MainApp.main( args );
31
    } catch( final Throwable t ) {
32
      log( t );
33
    }
34
  }
35
36
  @SuppressWarnings( "RedundantStringFormatCall" )
37
  private static void showAppInfo() {
38
    out( format( "%s version %s", APP_TITLE, APP_VERSION ) );
39
    out( format( "Copyright 2016-%s White Magic Software, Ltd.", APP_YEAR ) );
40
    out( format( "Portions copyright 2015-2020 Karl Tauber." ) );
41
  }
42
43
  private static void out( final String s ) {
44
    System.out.println( s );
45
  }
46
47
  /**
48
   * Returns the application version number retrieved from the application
49
   * properties file. The properties file is generated at build time, which
50
   * keys off the repository.
51
   *
52
   * @return The application version number.
53
   * @throws RuntimeException An {@link IOException} occurred.
54
   */
55
  public static String getVersion() {
56
    try {
57
      final var properties = loadProperties( "app.properties" );
58
      return properties.getProperty( "application.version" );
59
    } catch( final Exception ex ) {
60
      throw new RuntimeException( ex );
61
    }
62
  }
63
64
  @SuppressWarnings( "SameParameterValue" )
65
  private static Properties loadProperties( final String resource )
66
    throws IOException {
67
    final var properties = new Properties();
68
    properties.load( getResourceAsStream( getResourceName( resource ) ) );
69
    return properties;
70
  }
71
72
  private static String getResourceName( final String resource ) {
73
    return format( "%s/%s", getPackagePath(), resource );
74
  }
75
76
  private static String getPackagePath() {
77
    return Launcher.class.getPackageName().replace( '.', '/' );
78
  }
79
80
  private static InputStream getResourceAsStream( final String resource ) {
81
    return Launcher.class.getClassLoader().getResourceAsStream( resource );
82
  }
83
84
  /**
85
   * Logs the message of an error to the console.
86
   *
87
   * @param error The fatal error that could not be handled.
88
   */
89
  private static void log( final Throwable error ) {
90
    var message = error.getMessage();
91
92
    if( message != null && message.toLowerCase().contains( "javafx" ) ) {
93
      message = "Re-run using a Java Runtime Environment that includes JavaFX.";
94
    }
95
96
    out( format( "ERROR: %s", message ) );
97
  }
98
}
199
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.events.HyperlinkOpenEvent;
5
import com.keenwrite.preferences.Workspace;
6
import com.keenwrite.util.ArrayScanner;
7
import javafx.application.Application;
8
import javafx.event.Event;
9
import javafx.event.EventType;
10
import javafx.scene.input.KeyCode;
11
import javafx.scene.input.KeyEvent;
12
import javafx.stage.Stage;
13
import org.greenrobot.eventbus.Subscribe;
14
15
import java.util.function.BooleanSupplier;
16
import java.util.logging.LogManager;
17
18
import static com.keenwrite.Bootstrap.APP_TITLE;
19
import static com.keenwrite.constants.GraphicsConstants.LOGOS;
20
import static com.keenwrite.events.Bus.register;
21
import static com.keenwrite.preferences.WorkspaceKeys.*;
22
import static com.keenwrite.util.FontLoader.initFonts;
23
import static javafx.scene.input.KeyCode.ALT;
24
import static javafx.scene.input.KeyCode.F11;
25
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
26
import static javafx.scene.input.KeyEvent.KEY_RELEASED;
27
28
/**
29
 * Application entry point. The application allows users to edit plain text
30
 * files in a markup notation and see a real-time preview of the formatted
31
 * output.
32
 */
33
public final class MainApp extends Application {
34
35
  private Workspace mWorkspace;
36
  private MainScene mMainScene;
37
38
  /**
39
   * Application entry point.
40
   *
41
   * @param args Command-line arguments.
42
   */
43
  public static void main( final String[] args ) {
44
    if( !ArrayScanner.contains( args, "--debug" ) ) {
45
      disableLogging();
46
    }
47
48
    launch( args );
49
  }
50
51
  /**
52
   * Suppress logging to standard output and standard error.
53
   */
54
  private static void disableLogging() {
55
    LogManager.getLogManager().reset();
56
    System.err.close();
57
  }
58
59
  /**
60
   * JavaFX entry point.
61
   *
62
   * @param stage The primary application stage.
63
   */
64
  @Override
65
  public void start( final Stage stage ) {
66
    // Must be instantiated after the UI is initialized (i.e., not in main)
67
    // because it interacts with GUI properties.
68
    mWorkspace = new Workspace();
69
70
    initFonts();
71
    initState( stage );
72
    initStage( stage );
73
    initIcons( stage );
74
    initScene( stage );
75
76
    stage.show();
77
    register( this );
78
  }
79
80
  private void initState( final Stage stage ) {
81
    final var enable = createBoundsEnabledSupplier( stage );
82
83
    stage.setX( mWorkspace.toDouble( KEY_UI_WINDOW_X ) );
84
    stage.setY( mWorkspace.toDouble( KEY_UI_WINDOW_Y ) );
85
    stage.setWidth( mWorkspace.toDouble( KEY_UI_WINDOW_W ) );
86
    stage.setHeight( mWorkspace.toDouble( KEY_UI_WINDOW_H ) );
87
    stage.setMaximized( mWorkspace.toBoolean( KEY_UI_WINDOW_MAX ) );
88
    stage.setFullScreen( mWorkspace.toBoolean( KEY_UI_WINDOW_FULL ) );
89
90
    mWorkspace.listen( KEY_UI_WINDOW_X, stage.xProperty(), enable );
91
    mWorkspace.listen( KEY_UI_WINDOW_Y, stage.yProperty(), enable );
92
    mWorkspace.listen( KEY_UI_WINDOW_W, stage.widthProperty(), enable );
93
    mWorkspace.listen( KEY_UI_WINDOW_H, stage.heightProperty(), enable );
94
    mWorkspace.listen( KEY_UI_WINDOW_MAX, stage.maximizedProperty() );
95
    mWorkspace.listen( KEY_UI_WINDOW_FULL, stage.fullScreenProperty() );
96
  }
97
98
  private void initStage( final Stage stage ) {
99
    stage.setTitle( APP_TITLE );
100
    stage.addEventHandler( KEY_PRESSED, event -> {
101
      if( F11.equals( event.getCode() ) ) {
102
        stage.setFullScreen( !stage.isFullScreen() );
103
      }
104
    } );
105
106
    // After the app loses focus, when the user switches back using Alt+Tab,
107
    // the menu mnemonic is sometimes engaged, swallowing the first letter that
108
    // the user types---if it is a menu mnemonic. See MainScene::createScene().
109
    //
110
    // JavaFX Bug: https://bugs.openjdk.java.net/browse/JDK-8090647
111
    stage.focusedProperty().addListener( ( c, lost, show ) -> {
112
      for( final var menu : mMainScene.getMenuBar().getMenus() ) {
113
        menu.hide();
114
      }
115
116
      for( final var mnemonics : stage.getScene().getMnemonics().values() ) {
117
        for( final var mnemonic : mnemonics ) {
118
          mnemonic.getNode().fireEvent( keyUp( ALT ) );
119
        }
120
      }
121
    } );
122
  }
123
124
  private void initIcons( final Stage stage ) {
125
    stage.getIcons().addAll( LOGOS );
126
  }
127
128
  private void initScene( final Stage stage ) {
129
    mMainScene = new MainScene( mWorkspace );
130
    stage.setScene( mMainScene.getScene() );
131
  }
132
133
  /**
134
   * When a hyperlink website URL is clicked, this method is called to launch
135
   * the default browser to the event's location.
136
   *
137
   * @param event The event called when a hyperlink was clicked.
138
   */
139
  @Subscribe
140
  public void handle( final HyperlinkOpenEvent event ) {
141
    getHostServices().showDocument( event.getUri().toString() );
142
  }
143
144
  /**
145
   * When the window is maximized, full screen, or iconified, prevent updating
146
   * the window bounds. This is used so that if the user exits the application
147
   * when full screen (or maximized), restarting the application will recall
148
   * the previous bounds, allowing for continuity of expected behaviour.
149
   *
150
   * @param stage The window to check for "normal" status.
151
   * @return {@code false} when the bounds must not be changed, ergo persisted.
152
   */
153
  private BooleanSupplier createBoundsEnabledSupplier( final Stage stage ) {
154
    return () ->
155
      !(stage.isMaximized() || stage.isFullScreen() || stage.isIconified());
156
  }
157
158
  /**
159
   * Creates an instance of {@link KeyEvent} that represents pressing a key.
160
   *
161
   * @param code  The key to simulate being pressed down.
162
   * @param shift Whether shift key modifier shall modify the key code.
163
   * @return An instance of {@link KeyEvent} that may be used to simulate
164
   * a key being pressed.
165
   */
166
  public static Event keyDown( final KeyCode code, final boolean shift ) {
167
    return keyEvent( KEY_PRESSED, code, shift );
168
  }
169
170
  /**
171
   * Creates an instance of {@link KeyEvent} that represents releasing a key.
172
   *
173
   * @param code  The key to simulate being released up.
174
   * @param shift Whether shift key modifier shall modify the key code.
175
   * @return An instance of {@link KeyEvent} that may be used to simulate
176
   * a key being released.
177
   */
178
  public static Event keyUp( final KeyCode code, final boolean shift ) {
179
    return keyEvent( KEY_RELEASED, code, shift );
180
  }
181
182
  /**
183
   * Creates an instance of {@link KeyEvent} that represents a key released
184
   * event without any modifier keys held.
185
   *
186
   * @param code The key code representing a key to simulate releasing.
187
   * @return An instance of {@link KeyEvent}.
188
   */
189
  public static Event keyUp( final KeyCode code ) {
190
    return keyUp( code, false );
191
  }
192
193
  private static Event keyEvent(
194
    final EventType<KeyEvent> type, final KeyCode code, final boolean shift ) {
195
    return new KeyEvent(
196
      type, "", "", code, shift, false, false, false
197
    );
198
  }
199
}
1200
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.definition.DefinitionEditor;
8
import com.keenwrite.editors.definition.TreeTransformer;
9
import com.keenwrite.editors.definition.yaml.YamlTreeTransformer;
10
import com.keenwrite.editors.markdown.MarkdownEditor;
11
import com.keenwrite.events.*;
12
import com.keenwrite.io.MediaType;
13
import com.keenwrite.preferences.Key;
14
import com.keenwrite.preferences.Workspace;
15
import com.keenwrite.preview.HtmlPreview;
16
import com.keenwrite.processors.Processor;
17
import com.keenwrite.processors.ProcessorContext;
18
import com.keenwrite.processors.ProcessorFactory;
19
import com.keenwrite.processors.markdown.extensions.CaretExtension;
20
import com.keenwrite.service.events.Notifier;
21
import com.keenwrite.sigils.RSigilOperator;
22
import com.keenwrite.sigils.SigilOperator;
23
import com.keenwrite.sigils.Tokens;
24
import com.keenwrite.sigils.YamlSigilOperator;
25
import com.keenwrite.ui.explorer.FilePickerFactory;
26
import com.keenwrite.ui.heuristics.DocumentStatistics;
27
import com.keenwrite.ui.outline.DocumentOutline;
28
import com.panemu.tiwulfx.control.dock.DetachableTab;
29
import com.panemu.tiwulfx.control.dock.DetachableTabPane;
30
import javafx.application.Platform;
31
import javafx.beans.property.*;
32
import javafx.collections.ListChangeListener;
33
import javafx.concurrent.Task;
34
import javafx.event.ActionEvent;
35
import javafx.event.Event;
36
import javafx.event.EventHandler;
37
import javafx.scene.Node;
38
import javafx.scene.Scene;
39
import javafx.scene.control.*;
40
import javafx.scene.control.TreeItem.TreeModificationEvent;
41
import javafx.scene.input.KeyEvent;
42
import javafx.scene.layout.FlowPane;
43
import javafx.stage.Stage;
44
import javafx.stage.Window;
45
import org.greenrobot.eventbus.Subscribe;
46
47
import java.io.File;
48
import java.io.FileNotFoundException;
49
import java.nio.file.Path;
50
import java.util.*;
51
import java.util.concurrent.ExecutorService;
52
import java.util.concurrent.ScheduledExecutorService;
53
import java.util.concurrent.ScheduledFuture;
54
import java.util.concurrent.atomic.AtomicBoolean;
55
import java.util.concurrent.atomic.AtomicReference;
56
import java.util.function.Function;
57
import java.util.stream.Collectors;
58
59
import static com.keenwrite.ExportFormat.NONE;
60
import static com.keenwrite.Messages.get;
61
import static com.keenwrite.constants.Constants.*;
62
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE;
63
import static com.keenwrite.events.Bus.register;
64
import static com.keenwrite.events.HyperlinkOpenEvent.fireHyperlinkOpenEvent;
65
import static com.keenwrite.events.StatusEvent.clue;
66
import static com.keenwrite.io.MediaType.*;
67
import static com.keenwrite.preferences.WorkspaceKeys.*;
68
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
69
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
70
import static java.lang.String.format;
71
import static java.lang.System.getProperty;
72
import static java.util.concurrent.Executors.newFixedThreadPool;
73
import static java.util.concurrent.Executors.newScheduledThreadPool;
74
import static java.util.concurrent.TimeUnit.SECONDS;
75
import static java.util.stream.Collectors.groupingBy;
76
import static javafx.application.Platform.runLater;
77
import static javafx.scene.control.Alert.AlertType.ERROR;
78
import static javafx.scene.control.ButtonType.*;
79
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
80
import static javafx.scene.input.KeyCode.SPACE;
81
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
82
import static javafx.util.Duration.millis;
83
import static javax.swing.SwingUtilities.invokeLater;
84
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
85
86
/**
87
 * Responsible for wiring together the main application components for a
88
 * particular workspace (project). These include the definition views,
89
 * text editors, and preview pane along with any corresponding controllers.
90
 */
91
public final class MainPane extends SplitPane {
92
  private static final ExecutorService sExecutor = newFixedThreadPool( 1 );
93
94
  private final ScheduledExecutorService mSaver = newScheduledThreadPool( 1 );
95
  private final AtomicReference<ScheduledFuture<?>> mSaveTask =
96
    new AtomicReference<>();
97
98
  private static final Notifier sNotifier = Services.load( Notifier.class );
99
100
  /**
101
   * Used when opening files to determine how each file should be binned and
102
   * therefore what tab pane to be opened within.
103
   */
104
  private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of(
105
    TEXT_MARKDOWN, TEXT_R_MARKDOWN, UNDEFINED
106
  );
107
108
  /**
109
   * Prevents re-instantiation of processing classes.
110
   */
111
  private final Map<TextResource, Processor<String>> mProcessors =
112
    new HashMap<>();
113
114
  private final Workspace mWorkspace;
115
116
  /**
117
   * Groups similar file type tabs together.
118
   */
119
  private final List<TabPane> mTabPanes = new ArrayList<>();
120
121
  /**
122
   * Stores definition names and values.
123
   */
124
  private final Map<String, String> mResolvedMap =
125
    new HashMap<>( MAP_SIZE_DEFAULT );
126
127
  /**
128
   * Renders the actively selected plain text editor tab.
129
   */
130
  private final HtmlPreview mPreview;
131
132
  /**
133
   * Provides an interactive document outline.
134
   */
135
  private final DocumentOutline mOutline = new DocumentOutline();
136
137
  /**
138
   * Changing the active editor fires the value changed event. This allows
139
   * refreshes to happen when external definitions are modified and need to
140
   * trigger the processing chain.
141
   */
142
  private final ObjectProperty<TextEditor> mActiveTextEditor =
143
    createActiveTextEditor();
144
145
  /**
146
   * Changing the active definition editor fires the value changed event. This
147
   * allows refreshes to happen when external definitions are modified and need
148
   * to trigger the processing chain.
149
   */
150
  private final ObjectProperty<TextDefinition> mActiveDefinitionEditor =
151
    createActiveDefinitionEditor( mActiveTextEditor );
152
153
  /**
154
   * Tracks the number of detached tab panels opened into their own windows,
155
   * which allows unique identification of subordinate windows by their title.
156
   * It is doubtful more than 128 windows, much less 256, will be created.
157
   */
158
  private byte mWindowCount;
159
160
  /**
161
   * Called when the definition data is changed.
162
   */
163
  private final EventHandler<TreeModificationEvent<Event>> mTreeHandler =
164
    event -> {
165
      final var editor = mActiveDefinitionEditor.get();
166
167
      resolve( editor );
168
      process( getActiveTextEditor() );
169
      save( editor );
170
    };
171
172
  private final DocumentStatistics mStatistics;
173
174
  /**
175
   * Adds all content panels to the main user interface. This will load the
176
   * configuration settings from the workspace to reproduce the settings from
177
   * a previous session.
178
   */
179
  public MainPane( final Workspace workspace ) {
180
    mWorkspace = workspace;
181
    mPreview = new HtmlPreview( workspace );
182
    mStatistics = new DocumentStatistics( workspace );
183
    mActiveTextEditor.set( new MarkdownEditor( workspace ) );
184
185
    open( bin( getRecentFiles() ) );
186
    viewPreview();
187
    setDividerPositions( calculateDividerPositions() );
188
189
    // Once the main scene's window regains focus, update the active definition
190
    // editor to the currently selected tab.
191
    runLater( () -> getWindow().setOnCloseRequest( ( event ) -> {
192
      // Order matters here. We want to close all the tabs to ensure each
193
      // is saved, but after they are closed, the workspace should still
194
      // retain the list of files that were open. If this line came after
195
      // closing, then restarting the application would list no files.
196
      mWorkspace.save();
197
198
      if( closeAll() ) {
199
        Platform.exit();
200
        System.exit( 0 );
201
      }
202
      else {
203
        event.consume();
204
      }
205
    } ) );
206
207
    register( this );
208
    initAutosave( workspace );
209
  }
210
211
  @Subscribe
212
  public void handle( final TextEditorFocusEvent event ) {
213
    mActiveTextEditor.set( event.get() );
214
  }
215
216
  @Subscribe
217
  public void handle( final TextDefinitionFocusEvent event ) {
218
    mActiveDefinitionEditor.set( event.get() );
219
  }
220
221
  /**
222
   * Typically called when a file name is clicked in the preview panel.
223
   *
224
   * @param event The event to process, must contain a valid file reference.
225
   */
226
  @Subscribe
227
  public void handle( final FileOpenEvent event ) {
228
    final File eventFile;
229
    final var eventUri = event.getUri();
230
231
    if( eventUri.isAbsolute() ) {
232
      eventFile = new File( eventUri.getPath() );
233
    }
234
    else {
235
      final var activeFile = getActiveTextEditor().getFile();
236
      final var parent = activeFile.getParentFile();
237
238
      if( parent == null ) {
239
        clue( new FileNotFoundException( eventUri.getPath() ) );
240
        return;
241
      }
242
      else {
243
        final var parentPath = parent.getAbsolutePath();
244
        eventFile = Path.of( parentPath, eventUri.getPath() ).toFile();
245
      }
246
    }
247
248
    runLater( () -> open( eventFile ) );
249
  }
250
251
  @Subscribe
252
  public void handle( final CaretNavigationEvent event ) {
253
    runLater( () -> {
254
      final var textArea = getActiveTextEditor().getTextArea();
255
      textArea.moveTo( event.getOffset() );
256
      textArea.requestFollowCaret();
257
      textArea.requestFocus();
258
    } );
259
  }
260
261
  @Subscribe
262
  @SuppressWarnings( "unused" )
263
  public void handle( final ExportFailedEvent event ) {
264
    final var os = getProperty( "os.name" );
265
    final var arch = getProperty( "os.arch" ).toLowerCase();
266
    final var bits = getProperty( "sun.arch.data.model" );
267
268
    final var title = Messages.get( "Alert.typesetter.missing.title" );
269
    final var header = Messages.get( "Alert.typesetter.missing.header" );
270
    final var version = Messages.get(
271
      "Alert.typesetter.missing.version",
272
      os,
273
      arch
274
        .replaceAll( "amd.*|i.*|x86.*", "X86" )
275
        .replaceAll( "mips.*", "MIPS" )
276
        .replaceAll( "armv.*", "ARM" ),
277
      bits );
278
    final var text = Messages.get( "Alert.typesetter.missing.installer.text" );
279
280
    // Download and install ConTeXt for {0} {1} {2}-bit
281
    final var content = format( "%s %s", text, version );
282
    final var flowPane = new FlowPane();
283
    final var link = new Hyperlink( text );
284
    final var label = new Label( version );
285
    flowPane.getChildren().addAll( link, label );
286
287
    final var alert = new Alert( ERROR, content, OK );
288
    alert.setTitle( title );
289
    alert.setHeaderText( header );
290
    alert.getDialogPane().contentProperty().set( flowPane );
291
    alert.setGraphic( ICON_DIALOG_NODE );
292
293
    link.setOnAction( ( e ) -> {
294
      alert.close();
295
      final var url = Messages.get( "Alert.typesetter.missing.installer.url" );
296
      runLater( () -> fireHyperlinkOpenEvent( url ) );
297
    } );
298
299
    alert.showAndWait();
300
  }
301
302
  private void initAutosave( final Workspace workspace ) {
303
    final var rate = workspace.integerProperty( KEY_EDITOR_AUTOSAVE );
304
305
    rate.addListener(
306
      ( c, o, n ) -> {
307
        final var taskRef = mSaveTask.get();
308
309
        // Prevent multiple autosaves from running.
310
        if( taskRef != null ) {
311
          taskRef.cancel( false );
312
        }
313
314
        initAutosave( rate );
315
      }
316
    );
317
318
    // Start the save listener (avoids duplicating some code).
319
    initAutosave( rate );
320
  }
321
322
  private void initAutosave( final IntegerProperty rate ) {
323
    mSaveTask.set(
324
      mSaver.scheduleAtFixedRate(
325
        () -> {
326
          if( getActiveTextEditor().isModified() ) {
327
            // Ensure the modified indicator is cleared by running on EDT.
328
            runLater( this::save );
329
          }
330
        }, 0, rate.intValue(), SECONDS
331
      )
332
    );
333
  }
334
335
  /**
336
   * TODO: Load divider positions from exported settings, see
337
   *   {@link #bin(SetProperty)} comment.
338
   */
339
  private double[] calculateDividerPositions() {
340
    final var ratio = 100f / getItems().size() / 100;
341
    final var positions = getDividerPositions();
342
343
    for( int i = 0; i < positions.length; i++ ) {
344
      positions[ i ] = ratio * i;
345
    }
346
347
    return positions;
348
  }
349
350
  /**
351
   * Opens all the files into the application, provided the paths are unique.
352
   * This may only be called for any type of files that a user can edit
353
   * (i.e., update and persist), such as definitions and text files.
354
   *
355
   * @param files The list of files to open.
356
   */
357
  public void open( final List<File> files ) {
358
    files.forEach( this::open );
359
  }
360
361
  /**
362
   * This opens the given file. Since the preview pane is not a file that
363
   * can be opened, it is safe to add a listener to the detachable pane.
364
   *
365
   * @param file The file to open.
366
   */
367
  private void open( final File file ) {
368
    final var tab = createTab( file );
369
    final var node = tab.getContent();
370
    final var mediaType = MediaType.valueFrom( file );
371
    final var tabPane = obtainTabPane( mediaType );
372
373
    tab.setTooltip( createTooltip( file ) );
374
    tabPane.setFocusTraversable( false );
375
    tabPane.setTabClosingPolicy( ALL_TABS );
376
    tabPane.getTabs().add( tab );
377
378
    // Attach the tab scene factory for new tab panes.
379
    if( !getItems().contains( tabPane ) ) {
380
      addTabPane(
381
        node instanceof TextDefinition ? 0 : getItems().size(), tabPane
382
      );
383
    }
384
385
    getRecentFiles().add( file.getAbsolutePath() );
386
  }
387
388
  /**
389
   * Opens a new text editor document using the default document file name.
390
   */
391
  public void newTextEditor() {
392
    open( DOCUMENT_DEFAULT );
393
  }
394
395
  /**
396
   * Opens a new definition editor document using the default definition
397
   * file name.
398
   */
399
  public void newDefinitionEditor() {
400
    open( DEFINITION_DEFAULT );
401
  }
402
403
  /**
404
   * Iterates over all tab panes to find all {@link TextEditor}s and request
405
   * that they save themselves.
406
   */
407
  public void saveAll() {
408
    mTabPanes.forEach(
409
      ( tp ) -> tp.getTabs().forEach( ( tab ) -> {
410
        final var node = tab.getContent();
411
        if( node instanceof final TextEditor editor ) {
412
          save( editor );
413
        }
414
      } )
415
    );
416
  }
417
418
  /**
419
   * Requests that the active {@link TextEditor} saves itself. Don't bother
420
   * checking if modified first because if the user swaps external media from
421
   * an external source (e.g., USB thumb drive), save should not second-guess
422
   * the user: save always re-saves. Also, it's less code.
423
   */
424
  public void save() {
425
    save( getActiveTextEditor() );
426
  }
427
428
  /**
429
   * Saves the active {@link TextEditor} under a new name.
430
   *
431
   * @param files The new active editor {@link File} reference, must contain
432
   *              at least one element.
433
   */
434
  public void saveAs( final List<File> files ) {
435
    assert files != null;
436
    assert !files.isEmpty();
437
    final var editor = getActiveTextEditor();
438
    final var tab = getTab( editor );
439
    final var file = files.get( 0 );
440
441
    editor.rename( file );
442
    tab.ifPresent( t -> {
443
      t.setText( editor.getFilename() );
444
      t.setTooltip( createTooltip( file ) );
445
    } );
446
447
    save();
448
  }
449
450
  /**
451
   * Saves the given {@link TextResource} to a file. This is typically used
452
   * to save either an instance of {@link TextEditor} or {@link TextDefinition}.
453
   *
454
   * @param resource The resource to export.
455
   */
456
  private void save( final TextResource resource ) {
457
    try {
458
      resource.save();
459
    } catch( final Exception ex ) {
460
      clue( ex );
461
      sNotifier.alert(
462
        getWindow(), resource.getPath(), "TextResource.saveFailed", ex
463
      );
464
    }
465
  }
466
467
  /**
468
   * Closes all open {@link TextEditor}s; all {@link TextDefinition}s stay open.
469
   *
470
   * @return {@code true} when all editors, modified or otherwise, were
471
   * permitted to close; {@code false} when one or more editors were modified
472
   * and the user requested no closing.
473
   */
474
  public boolean closeAll() {
475
    var closable = true;
476
477
    for( final var tabPane : mTabPanes ) {
478
      final var tabIterator = tabPane.getTabs().iterator();
479
480
      while( tabIterator.hasNext() ) {
481
        final var tab = tabIterator.next();
482
        final var resource = tab.getContent();
483
484
        // The definition panes auto-save, so being specific here prevents
485
        // closing the definitions in the situation where the user wants to
486
        // continue editing (i.e., possibly save unsaved work).
487
        if( !(resource instanceof TextEditor) ) {
488
          continue;
489
        }
490
491
        if( canClose( (TextEditor) resource ) ) {
492
          tabIterator.remove();
493
          close( tab );
494
        }
495
        else {
496
          closable = false;
497
        }
498
      }
499
    }
500
501
    return closable;
502
  }
503
504
  /**
505
   * Calls the tab's {@link Tab#getOnClosed()} handler to carry out a close
506
   * event.
507
   *
508
   * @param tab The {@link Tab} that was closed.
509
   */
510
  private void close( final Tab tab ) {
511
    assert tab != null;
512
513
    final var handler = tab.getOnClosed();
514
515
    if( handler != null ) {
516
      handler.handle( new ActionEvent() );
517
    }
518
  }
519
520
  /**
521
   * Closes the active tab; delegates to {@link #canClose(TextResource)}.
522
   */
523
  public void close() {
524
    final var editor = getActiveTextEditor();
525
526
    if( canClose( editor ) ) {
527
      close( editor );
528
    }
529
  }
530
531
  /**
532
   * Closes the given {@link TextResource}. This must not be called from within
533
   * a loop that iterates over the tab panes using {@code forEach}, lest a
534
   * concurrent modification exception be thrown.
535
   *
536
   * @param resource The {@link TextResource} to close, without confirming with
537
   *                 the user.
538
   */
539
  private void close( final TextResource resource ) {
540
    getTab( resource ).ifPresent(
541
      ( tab ) -> {
542
        close( tab );
543
        tab.getTabPane().getTabs().remove( tab );
544
      }
545
    );
546
  }
547
548
  /**
549
   * Answers whether the given {@link TextResource} may be closed.
550
   *
551
   * @param editor The {@link TextResource} to try closing.
552
   * @return {@code true} when the editor may be closed; {@code false} when
553
   * the user has requested to keep the editor open.
554
   */
555
  private boolean canClose( final TextResource editor ) {
556
    final var editorTab = getTab( editor );
557
    final var canClose = new AtomicBoolean( true );
558
559
    if( editor.isModified() ) {
560
      final var filename = new StringBuilder();
561
      editorTab.ifPresent( ( tab ) -> filename.append( tab.getText() ) );
562
563
      final var message = sNotifier.createNotification(
564
        Messages.get( "Alert.file.close.title" ),
565
        Messages.get( "Alert.file.close.text" ),
566
        filename.toString()
567
      );
568
569
      final var dialog = sNotifier.createConfirmation( getWindow(), message );
570
571
      dialog.showAndWait().ifPresent(
572
        save -> canClose.set( save == YES ? editor.save() : save == NO )
573
      );
574
    }
575
576
    return canClose.get();
577
  }
578
579
  private ObjectProperty<TextEditor> createActiveTextEditor() {
580
    final var editor = new SimpleObjectProperty<TextEditor>();
581
582
    editor.addListener( ( c, o, n ) -> {
583
      if( n != null ) {
584
        mPreview.setBaseUri( n.getPath() );
585
        process( n );
586
      }
587
    } );
588
589
    return editor;
590
  }
591
592
  /**
593
   * Adds the HTML preview tab to its own, singular tab pane.
594
   */
595
  public void viewPreview() {
596
    viewTab( mPreview, TEXT_HTML, "Pane.preview.title" );
597
  }
598
599
  /**
600
   * Adds the document outline tab to its own, singular tab pane.
601
   */
602
  public void viewOutline() {
603
    viewTab( mOutline, APP_DOCUMENT_OUTLINE, "Pane.outline.title" );
604
  }
605
606
  public void viewStatistics() {
607
    viewTab( mStatistics, APP_DOCUMENT_STATISTICS, "Pane.statistics.title" );
608
  }
609
610
  public void viewFiles() {
611
    try {
612
      final var factory = new FilePickerFactory( mWorkspace );
613
      final var fileManager = factory.createModeless();
614
      viewTab( fileManager, APP_FILE_MANAGER, "Pane.files.title" );
615
    } catch( final Exception ex ) {
616
      clue( ex );
617
    }
618
  }
619
620
  private void viewTab(
621
    final Node node, final MediaType mediaType, final String key ) {
622
    final var tabPane = obtainTabPane( mediaType );
623
624
    for( final var tab : tabPane.getTabs() ) {
625
      if( tab.getContent() == node ) {
626
        return;
627
      }
628
    }
629
630
    tabPane.getTabs().add( createTab( get( key ), node ) );
631
    addTabPane( tabPane );
632
  }
633
634
  public void viewRefresh() {
635
    mPreview.refresh();
636
  }
637
638
  /**
639
   * Returns the tab that contains the given {@link TextEditor}.
640
   *
641
   * @param editor The {@link TextEditor} instance to find amongst the tabs.
642
   * @return The first tab having content that matches the given tab.
643
   */
644
  private Optional<Tab> getTab( final TextResource editor ) {
645
    return mTabPanes.stream()
646
                    .flatMap( pane -> pane.getTabs().stream() )
647
                    .filter( tab -> editor.equals( tab.getContent() ) )
648
                    .findFirst();
649
  }
650
651
  /**
652
   * Creates a new {@link DefinitionEditor} wrapped in a listener that
653
   * is used to detect when the active {@link DefinitionEditor} has changed.
654
   * Upon changing, the {@link #mResolvedMap} is updated and the active
655
   * text editor is refreshed.
656
   *
657
   * @param editor Text editor to update with the revised resolved map.
658
   * @return A newly configured property that represents the active
659
   * {@link DefinitionEditor}, never null.
660
   */
661
  private ObjectProperty<TextDefinition> createActiveDefinitionEditor(
662
    final ObjectProperty<TextEditor> editor ) {
663
    final var definitions = new SimpleObjectProperty<TextDefinition>();
664
    definitions.addListener( ( c, o, n ) -> {
665
      resolve( n == null ? createDefinitionEditor() : n );
666
      process( editor.get() );
667
    } );
668
669
    return definitions;
670
  }
671
672
  private Tab createTab( final String filename, final Node node ) {
673
    return new DetachableTab( filename, node );
674
  }
675
676
  private Tab createTab( final File file ) {
677
    final var r = createTextResource( file );
678
    final var tab = createTab( r.getFilename(), r.getNode() );
679
680
    r.modifiedProperty().addListener(
681
      ( c, o, n ) -> tab.setText( r.getFilename() + (n ? "*" : "") )
682
    );
683
684
    // This is called when either the tab is closed by the user clicking on
685
    // the tab's close icon or when closing (all) from the file menu.
686
    tab.setOnClosed(
687
      ( __ ) -> getRecentFiles().remove( file.getAbsolutePath() )
688
    );
689
690
    // When closing a tab, give focus to the newly revealed tab.
691
    tab.selectedProperty().addListener( ( c, o, n ) -> {
692
      if( n != null && n ) {
693
        final var pane = tab.getTabPane();
694
695
        if( pane != null ) {
696
          pane.requestFocus();
697
        }
698
      }
699
    } );
700
701
    tab.tabPaneProperty().addListener( ( cPane, oPane, nPane ) -> {
702
      if( nPane != null ) {
703
        nPane.focusedProperty().addListener( ( c, o, n ) -> {
704
          if( n != null && n ) {
705
            final var selected = nPane.getSelectionModel().getSelectedItem();
706
            final var node = selected.getContent();
707
            node.requestFocus();
708
          }
709
        } );
710
      }
711
    } );
712
713
    return tab;
714
  }
715
716
  /**
717
   * Creates bins for the different {@link MediaType}s, which eventually are
718
   * added to the UI as separate tab panes. If ever a general-purpose scene
719
   * exporter is developed to serialize a scene to an FXML file, this could
720
   * be replaced by such a class.
721
   * <p>
722
   * When binning the files, this makes sure that at least one file exists
723
   * for every type. If the user has opted to close a particular type (such
724
   * as the definition pane), the view will suppressed elsewhere.
725
   * </p>
726
   * <p>
727
   * The order that the binned files are returned will be reflected in the
728
   * order that the corresponding panes are rendered in the UI.
729
   * </p>
730
   *
731
   * @param paths The file paths to bin according to their type.
732
   * @return An in-order list of files, first by structured definition files,
733
   * then by plain text documents.
734
   */
735
  private List<File> bin( final SetProperty<String> paths ) {
736
    // Treat all files destined for the text editor as plain text documents
737
    // so that they are added to the same pane. Grouping by TEXT_PLAIN is a
738
    // bit arbitrary, but means explicitly capturing TEXT_PLAIN isn't needed.
739
    final Function<MediaType, MediaType> bin =
740
      m -> PLAIN_TEXT_FORMAT.contains( m ) ? TEXT_PLAIN : m;
741
742
    // Create two groups: YAML files and plain text files.
743
    final var bins = paths
744
      .stream()
745
      .collect(
746
        groupingBy( path -> bin.apply( MediaType.fromFilename( path ) ) )
747
      );
748
749
    bins.putIfAbsent( TEXT_YAML, List.of( DEFINITION_DEFAULT.toString() ) );
750
    bins.putIfAbsent( TEXT_PLAIN, List.of( DOCUMENT_DEFAULT.toString() ) );
751
752
    final var result = new ArrayList<File>( paths.size() );
753
754
    // Ensure that the same types are listed together (keep insertion order).
755
    bins.forEach( ( mediaType, files ) -> result.addAll(
756
      files.stream().map( File::new ).collect( Collectors.toList() ) )
757
    );
758
759
    return result;
760
  }
761
762
  /**
763
   * Uses the given {@link TextDefinition} instance to update the
764
   * {@link #mResolvedMap}.
765
   *
766
   * @param editor A non-null, possibly empty definition editor.
767
   */
768
  private void resolve( final TextDefinition editor ) {
769
    assert editor != null;
770
771
    final var tokens = createDefinitionTokens();
772
    final var operator = new YamlSigilOperator( tokens );
773
    final var map = new HashMap<String, String>();
774
775
    editor.toMap().forEach( ( k, v ) -> map.put( operator.entoken( k ), v ) );
776
777
    mResolvedMap.clear();
778
    mResolvedMap.putAll( editor.interpolate( map, tokens ) );
779
  }
780
781
  /**
782
   * Force the active editor to update, which will cause the processor
783
   * to re-evaluate the interpolated definition map thereby updating the
784
   * preview pane.
785
   *
786
   * @param editor Contains the source document to update in the preview pane.
787
   */
788
  private void process( final TextEditor editor ) {
789
    // Ensure processing does not run on the JavaFX thread, which frees the
790
    // text editor immediately for caret movement. The preview will have a
791
    // slight delay when catching up to the caret position.
792
    final var task = new Task<Void>() {
793
      @Override
794
      public Void call() {
795
        try {
796
          final var p = mProcessors.getOrDefault( editor, IDENTITY );
797
          p.apply( editor == null ? "" : editor.getText() );
798
        } catch( final Exception ex ) {
799
          clue( ex );
800
        }
801
802
        return null;
803
      }
804
    };
805
806
    task.setOnSucceeded(
807
      e -> invokeLater( () -> mPreview.scrollTo( CARET_ID ) )
808
    );
809
810
    // Prevents multiple process requests from executing simultaneously (due
811
    // to having a restricted queue size).
812
    sExecutor.execute( task );
813
  }
814
815
  /**
816
   * Lazily creates a {@link TabPane} configured to listen for tab select
817
   * events. The tab pane is associated with a given media type so that
818
   * similar files can be grouped together.
819
   *
820
   * @param mediaType The media type to associate with the tab pane.
821
   * @return An instance of {@link TabPane} that will handle tab docking.
822
   */
823
  private TabPane obtainTabPane( final MediaType mediaType ) {
824
    for( final var pane : mTabPanes ) {
825
      for( final var tab : pane.getTabs() ) {
826
        final var node = tab.getContent();
827
828
        if( node instanceof TextResource r && r.supports( mediaType ) ) {
829
          return pane;
830
        }
831
      }
832
    }
833
834
    final var pane = createTabPane();
835
    mTabPanes.add( pane );
836
    return pane;
837
  }
838
839
  /**
840
   * Creates an initialized {@link TabPane} instance.
841
   *
842
   * @return A new {@link TabPane} with all listeners configured.
843
   */
844
  private TabPane createTabPane() {
845
    final var tabPane = new DetachableTabPane();
846
847
    initStageOwnerFactory( tabPane );
848
    initTabListener( tabPane );
849
850
    return tabPane;
851
  }
852
853
  /**
854
   * When any {@link DetachableTabPane} is detached from the main window,
855
   * the stage owner factory must be given its parent window, which will
856
   * own the child window. The parent window is the {@link MainPane}'s
857
   * {@link Scene}'s {@link Window} instance.
858
   *
859
   * <p>
860
   * This will derives the new title from the main window title, incrementing
861
   * the window count to help uniquely identify the child windows.
862
   * </p>
863
   *
864
   * @param tabPane A new {@link DetachableTabPane} to configure.
865
   */
866
  private void initStageOwnerFactory( final DetachableTabPane tabPane ) {
867
    tabPane.setStageOwnerFactory( ( stage ) -> {
868
      final var title = get(
869
        "Detach.tab.title",
870
        ((Stage) getWindow()).getTitle(), ++mWindowCount
871
      );
872
      stage.setTitle( title );
873
874
      return getScene().getWindow();
875
    } );
876
  }
877
878
  /**
879
   * Responsible for configuring the content of each {@link DetachableTab} when
880
   * it is added to the given {@link DetachableTabPane} instance.
881
   * <p>
882
   * For {@link TextEditor} contents, an instance of {@link ScrollEventHandler}
883
   * is initialized to perform synchronized scrolling between the editor and
884
   * its preview window. Additionally, the last tab in the tab pane's list of
885
   * tabs is given focus.
886
   * </p>
887
   * <p>
888
   * Note that multiple tabs can be added simultaneously.
889
   * </p>
890
   *
891
   * @param tabPane A new {@link TabPane} to configure.
892
   */
893
  private void initTabListener( final TabPane tabPane ) {
894
    tabPane.getTabs().addListener(
895
      ( final ListChangeListener.Change<? extends Tab> listener ) -> {
896
        while( listener.next() ) {
897
          if( listener.wasAdded() ) {
898
            final var tabs = listener.getAddedSubList();
899
900
            tabs.forEach( ( tab ) -> {
901
              final var node = tab.getContent();
902
903
              if( node instanceof TextEditor ) {
904
                initScrollEventListener( tab );
905
              }
906
            } );
907
908
            // Select and give focus to the last tab opened.
909
            final var index = tabs.size() - 1;
910
            if( index >= 0 ) {
911
              final var tab = tabs.get( index );
912
              tabPane.getSelectionModel().select( tab );
913
              tab.getContent().requestFocus();
914
            }
915
          }
916
        }
917
      }
918
    );
919
  }
920
921
  /**
922
   * Synchronizes scrollbar positions between the given {@link Tab} that
923
   * contains an instance of {@link TextEditor} and {@link HtmlPreview} pane.
924
   *
925
   * @param tab The container for an instance of {@link TextEditor}.
926
   */
927
  private void initScrollEventListener( final Tab tab ) {
928
    final var editor = (TextEditor) tab.getContent();
929
    final var scrollPane = editor.getScrollPane();
930
    final var scrollBar = mPreview.getVerticalScrollBar();
931
    final var handler = new ScrollEventHandler( scrollPane, scrollBar );
932
    handler.enabledProperty().bind( tab.selectedProperty() );
933
  }
934
935
  private void addTabPane( final int index, final TabPane tabPane ) {
936
    final var items = getItems();
937
    if( !items.contains( tabPane ) ) {
938
      items.add( index, tabPane );
939
    }
940
  }
941
942
  private void addTabPane( final TabPane tabPane ) {
943
    addTabPane( getItems().size(), tabPane );
944
  }
945
946
  public ProcessorContext createProcessorContext() {
947
    return createProcessorContext( null, NONE );
948
  }
949
950
  public ProcessorContext createProcessorContext(
951
    final Path exportPath, final ExportFormat format ) {
952
    final var editor = getActiveTextEditor();
953
    return createProcessorContext(
954
      editor.getPath(), exportPath, format, editor.getCaret() );
955
  }
956
957
  private ProcessorContext createProcessorContext(
958
    final Path path, final Caret caret ) {
959
    return createProcessorContext( path, null, ExportFormat.NONE, caret );
960
  }
961
962
  /**
963
   * @param path       Used by {@link ProcessorFactory} to determine
964
   *                   {@link Processor} type to create based on file type.
965
   * @param exportPath Used when exporting to a PDF file (binary).
966
   * @param format     Used when processors export to a new text format.
967
   * @param caret      Used by {@link CaretExtension} to add ID attribute into
968
   *                   preview document for scrollbar synchronization.
969
   * @return A new {@link ProcessorContext} to use when creating an instance of
970
   * {@link Processor}.
971
   */
972
  private ProcessorContext createProcessorContext(
973
    final Path path, final Path exportPath, final ExportFormat format,
974
    final Caret caret ) {
975
    return new ProcessorContext(
976
      mPreview, mResolvedMap, path, exportPath, format, mWorkspace, caret
977
    );
978
  }
979
980
  private TextResource createTextResource( final File file ) {
981
    // TODO: Create PlainTextEditor that's returned by default.
982
    return MediaType.valueFrom( file ) == TEXT_YAML
983
      ? createDefinitionEditor( file )
984
      : createMarkdownEditor( file );
985
  }
986
987
  /**
988
   * Creates an instance of {@link MarkdownEditor} that listens for both
989
   * caret change events and text change events. Text change events must
990
   * take priority over caret change events because it's possible to change
991
   * the text without moving the caret (e.g., delete selected text).
992
   *
993
   * @param file The file containing contents for the text editor.
994
   * @return A non-null text editor.
995
   */
996
  private TextResource createMarkdownEditor( final File file ) {
997
    final var path = file.toPath();
998
    final var editor = new MarkdownEditor( file, getWorkspace() );
999
    final var caret = editor.getCaret();
1000
    final var context = createProcessorContext( path, caret );
1001
1002
    mProcessors.computeIfAbsent( editor, p -> createProcessors( context ) );
1003
1004
    editor.addDirtyListener( ( c, o, n ) -> {
1005
      if( n ) {
1006
        // Reset the status to OK after changing the text.
1007
        clue();
1008
1009
        // Processing the text may update the status bar.
1010
        process( getActiveTextEditor() );
1011
      }
1012
    } );
1013
1014
    editor.addEventListener(
1015
      keyPressed( SPACE, CONTROL_DOWN ), this::autoinsert
1016
    );
1017
1018
    // Set the active editor, which refreshes the preview panel.
1019
    mActiveTextEditor.set( editor );
1020
1021
    return editor;
1022
  }
1023
1024
  /**
1025
   * Delegates to {@link #autoinsert()}.
1026
   *
1027
   * @param event Ignored.
1028
   */
1029
  @SuppressWarnings( "unused" )
1030
  private void autoinsert( final KeyEvent event ) {
1031
    autoinsert();
1032
  }
1033
1034
  /**
1035
   * Finds a node that matches the word at the caret, then inserts the
1036
   * corresponding definition. The definition token delimiters depend on
1037
   * the type of file being edited.
1038
   */
1039
  public void autoinsert() {
1040
    final var definitions = getActiveTextDefinition();
1041
    final var editor = getActiveTextEditor();
1042
    final var mediaType = editor.getMediaType();
1043
    final var operator = getSigilOperator( mediaType );
1044
1045
    DefinitionNameInjector.autoinsert( editor, definitions, operator );
1046
  }
1047
1048
  private TextDefinition createDefinitionEditor() {
1049
    return createDefinitionEditor( DEFINITION_DEFAULT );
1050
  }
1051
1052
  private TextDefinition createDefinitionEditor( final File file ) {
1053
    final var editor = new DefinitionEditor( file, createTreeTransformer() );
1054
    editor.addTreeChangeHandler( mTreeHandler );
1055
    return editor;
1056
  }
1057
1058
  private TreeTransformer createTreeTransformer() {
1059
    return new YamlTreeTransformer();
1060
  }
1061
1062
  private Tooltip createTooltip( final File file ) {
1063
    final var path = file.toPath();
1064
    final var tooltip = new Tooltip( path.toString() );
1065
1066
    tooltip.setShowDelay( millis( 200 ) );
1067
    return tooltip;
1068
  }
1069
1070
  public TextEditor getActiveTextEditor() {
1071
    return mActiveTextEditor.get();
1072
  }
1073
1074
  public ReadOnlyObjectProperty<TextEditor> activeTextEditorProperty() {
1075
    return mActiveTextEditor;
1076
  }
1077
1078
  public TextDefinition getActiveTextDefinition() {
1079
    return mActiveDefinitionEditor.get();
1080
  }
1081
1082
  public Window getWindow() {
1083
    return getScene().getWindow();
1084
  }
1085
1086
  public Workspace getWorkspace() {
1087
    return mWorkspace;
1088
  }
1089
1090
  /**
1091
   * Returns the sigil operator for the given {@link MediaType}.
1092
   *
1093
   * @param mediaType The type of file being edited.
1094
   */
1095
  private SigilOperator getSigilOperator( final MediaType mediaType ) {
1096
    final var operator = new YamlSigilOperator( createDefinitionTokens() );
1097
1098
    return mediaType == TEXT_R_MARKDOWN
1099
      ? new RSigilOperator( createRTokens(), operator )
1100
      : operator;
1101
  }
1102
1103
  /**
1104
   * Returns the set of file names opened in the application. The names must
1105
   * be converted to {@link File} objects.
1106
   *
1107
   * @return A {@link Set} of file names.
1108
   */
1109
  private SetProperty<String> getRecentFiles() {
1110
    return getWorkspace().setsProperty( KEY_UI_FILES_PATH );
1111
  }
1112
1113
  private StringProperty stringProperty( final Key key ) {
1114
    return getWorkspace().stringProperty( key );
1115
  }
1116
1117
  private Tokens createRTokens() {
1118
    return createTokens( KEY_R_DELIM_BEGAN, KEY_R_DELIM_ENDED );
1119
  }
1120
1121
  private Tokens createDefinitionTokens() {
1122
    return createTokens( KEY_DEF_DELIM_BEGAN, KEY_DEF_DELIM_ENDED );
1123
  }
1124
1125
  private Tokens createTokens( final Key began, final Key ended ) {
1126
    return new Tokens( stringProperty( began ), stringProperty( ended ) );
1127
  }
1128
}
11129
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.ApplicationActions;
8
import com.keenwrite.ui.listeners.CaretListener;
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
19
import static com.keenwrite.Messages.get;
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.SkinProperty.toFilename;
24
import static com.keenwrite.preferences.WorkspaceKeys.KEY_UI_SKIN_CUSTOM;
25
import static com.keenwrite.preferences.WorkspaceKeys.KEY_UI_SKIN_SELECTION;
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, tool bar, and status bar.
34
 */
35
public final class MainScene {
36
  private final Scene mScene;
37
  private final MenuBar mMenuBar;
38
  private final Node mToolBar;
39
  private final StatusBar mStatusBar;
40
  private final FileWatchService mFileWatchService = new FileWatchService();
41
  private FileModifiedListener mStylesheetFileListener = event -> {};
42
43
  public MainScene( final Workspace workspace ) {
44
    final var mainPane = createMainPane( workspace );
45
    final var actions = createApplicationActions( mainPane );
46
    final var caretListener = createCaretListener( mainPane );
47
    mMenuBar = setManagedLayout( createMenuBar( actions ) );
48
    mToolBar = setManagedLayout( createToolBar() );
49
    mStatusBar = setManagedLayout( createStatusBar() );
50
51
    mStatusBar.getRightItems().add( caretListener );
52
53
    final var appPane = new BorderPane();
54
    appPane.setTop( new VBox( mMenuBar, mToolBar ) );
55
    appPane.setCenter( mainPane );
56
    appPane.setBottom( mStatusBar );
57
58
    final var fileWatcher = new Thread( mFileWatchService );
59
    fileWatcher.setDaemon( true );
60
    fileWatcher.start();
61
62
    mScene = createScene( appPane );
63
    initStylesheets( mScene, workspace );
64
  }
65
66
  /**
67
   * Called by the {@link MainApp} to get a handle on the {@link Scene}
68
   * created by an instance of {@link MainScene}.
69
   *
70
   * @return The {@link Scene} created at construction time.
71
   */
72
  public Scene getScene() {
73
    return mScene;
74
  }
75
76
  public void toggleMenuBar() {
77
    final var node = mMenuBar;
78
    node.setVisible( !node.isVisible() );
79
  }
80
81
  public void toggleToolBar() {
82
    final var node = mToolBar;
83
    node.setVisible( !node.isVisible() );
84
  }
85
86
  public void toggleStatusBar() {
87
    final var node = mStatusBar;
88
    node.setVisible( !node.isVisible() );
89
  }
90
91
  MenuBar getMenuBar() {
92
    return mMenuBar;
93
  }
94
95
  public StatusBar getStatusBar() { return mStatusBar; }
96
97
  private void initStylesheets( final Scene scene, final Workspace workspace ) {
98
    final var internal = workspace.skinProperty( KEY_UI_SKIN_SELECTION );
99
    final var external = workspace.fileProperty( KEY_UI_SKIN_CUSTOM );
100
    final var inSkin = internal.get();
101
    final var exSkin = external.get();
102
    applyStylesheets( scene, inSkin, exSkin );
103
104
    internal.addListener(
105
      ( c, o, n ) -> {
106
        if( n != null ) {
107
          applyStylesheets( scene, n, exSkin );
108
        }
109
      }
110
    );
111
112
    external.addListener(
113
      ( c, o, n ) -> {
114
        if( o != null ) {
115
          mFileWatchService.unregister( o );
116
        }
117
118
        if( n != null ) {
119
          try {
120
            applyStylesheets( scene, inSkin, n );
121
          } catch( final Exception ex ) {
122
            // Changes to the CSS file won't autoload, which is okay.
123
            clue( ex );
124
          }
125
        }
126
      }
127
    );
128
129
    mFileWatchService.removeListener( mStylesheetFileListener );
130
    mStylesheetFileListener = event ->
131
      runLater( () -> applyStylesheets( scene, inSkin, event.getFile() ) );
132
    mFileWatchService.addListener( mStylesheetFileListener );
133
  }
134
135
  private String getStylesheet( final String filename ) {
136
    return get( STYLESHEET_APPLICATION_SKIN, filename );
137
  }
138
139
  /**
140
   * Clears then re-applies all the internal stylesheets.
141
   *
142
   * @param scene    The scene to stylize.
143
   * @param internal The CSS file name bundled with the application.
144
   * @param external The (optional) customized CSS file specified by the user.
145
   */
146
  private void applyStylesheets(
147
    final Scene scene, final String internal, final File external ) {
148
    final var stylesheets = scene.getStylesheets();
149
    stylesheets.clear();
150
    stylesheets.add( STYLESHEET_APPLICATION_BASE );
151
    stylesheets.add( STYLESHEET_MARKDOWN );
152
    stylesheets.add( getStylesheet( toFilename( internal ) ) );
153
154
    try {
155
      if( external != null && external.canRead() && !external.isDirectory() ) {
156
        stylesheets.add( external.toURI().toURL().toString() );
157
        mFileWatchService.register( external );
158
      }
159
    } catch( final Exception ex ) {
160
      clue( ex );
161
    }
162
  }
163
164
  private MainPane createMainPane( final Workspace workspace ) {
165
    return new MainPane( workspace );
166
  }
167
168
  private ApplicationActions createApplicationActions(
169
    final MainPane mainPane ) {
170
    return new ApplicationActions( this, mainPane );
171
  }
172
173
  /**
174
   * Creates the class responsible for updating the UI with the caret position
175
   * based on the active text editor.
176
   *
177
   * @return The {@link CaretListener} responsible for updating the
178
   * {@link StatusBar} whenever the caret changes position.
179
   */
180
  private CaretListener createCaretListener( final MainPane mainPane ) {
181
    return new CaretListener( mainPane.activeTextEditorProperty() );
182
  }
183
184
  /**
185
   * Creates a new scene that is attached to the given {@link Parent}.
186
   *
187
   * @param parent The container for the scene.
188
   * @return A scene to capture user interactions, UI styles, etc.
189
   */
190
  private Scene createScene( final Parent parent ) {
191
    final var scene = new Scene( parent );
192
193
    // After the app loses focus, when the user switches back using Alt+Tab,
194
    // the menu is sometimes engaged. See MainApp::initStage().
195
    //
196
    // JavaFX Bug: https://bugs.openjdk.java.net/browse/JDK-8090647
197
    scene.addEventHandler( KEY_PRESSED, event -> {
198
      // Only consume lone ALT key press events. If the modifier is used in
199
      // combination with another key, don't consume the event. First check
200
      // if ALT is down before getting the key code as a micro-optimization.
201
      if( event.isAltDown() ) {
202
        if( event.getCode() == ALT || event.getCode() == ALT_GRAPH ) {
203
          event.consume();
204
        }
205
      }
206
    } );
207
208
    // Update the synchronized scrolling status when user presses scroll lock.
209
    scene.addEventHandler( KEY_RELEASED, event -> {
210
      if( event.getCode() == SCROLL_LOCK ) {
211
        fireScrollLockEvent();
212
      }
213
    } );
214
215
    return scene;
216
  }
217
218
  /**
219
   * Binds the visible property of the node to the managed property so that
220
   * hiding the node also removes the screen real estate that it occupies.
221
   * This allows the user to hide the menu bar, tool bar, etc.
222
   *
223
   * @param node The node to have its real estate bound to visibility.
224
   * @return The given node for fluent-like convenience.
225
   */
226
  private <T extends Node> T setManagedLayout( final T node ) {
227
    node.managedProperty().bind( node.visibleProperty() );
228
    return node;
229
  }
230
}
1231
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.preferences.Key;
5
6
import java.text.MessageFormat;
7
import java.util.Enumeration;
8
import java.util.ResourceBundle;
9
import java.util.Stack;
10
11
import static com.keenwrite.constants.Constants.APP_BUNDLE_NAME;
12
import static java.util.ResourceBundle.getBundle;
13
14
/**
15
 * Recursively resolves message properties. Property values can refer to other
16
 * properties using a <code>${var}</code> syntax.
17
 */
18
public final class Messages {
19
20
  private static final ResourceBundle RESOURCE_BUNDLE =
21
    getBundle( APP_BUNDLE_NAME );
22
23
  private Messages() {
24
  }
25
26
  /**
27
   * Return the value of a resource bundle value after having resolved any
28
   * references to other bundle variables.
29
   *
30
   * @param props The bundle containing resolvable properties.
31
   * @param s     The value for a key to resolve.
32
   * @return The value of the key with all references recursively dereferenced.
33
   */
34
  @SuppressWarnings( "SameParameterValue" )
35
  private static String resolve( final ResourceBundle props, final String s ) {
36
    final var len = s.length();
37
    final var stack = new Stack<StringBuilder>();
38
    var sb = new StringBuilder( 256 );
39
    var open = false;
40
41
    for( var i = 0; i < len; i++ ) {
42
      final var c = s.charAt( i );
43
44
      switch( c ) {
45
        case '$': {
46
          if( i + 1 < len && s.charAt( i + 1 ) == '{' ) {
47
            stack.push( sb );
48
49
            if( stack.size() > 20 ) {
50
              final var m = get( "Main.status.error.messages.recursion", s );
51
              throw new IllegalArgumentException( m );
52
            }
53
54
            sb = new StringBuilder( 256 );
55
            i++;
56
            open = true;
57
          }
58
59
          break;
60
        }
61
62
        case '}': {
63
          if( open ) {
64
            open = false;
65
            final var name = sb.toString();
66
67
            sb = stack.pop();
68
            sb.append( props.getString( name ) );
69
            break;
70
          }
71
        }
72
73
        default: {
74
          sb.append( c );
75
          break;
76
        }
77
      }
78
    }
79
80
    if( open ) {
81
      final var m = get( "Main.status.error.messages.syntax", s );
82
      throw new IllegalArgumentException( m );
83
    }
84
85
    return sb.toString();
86
  }
87
88
  /**
89
   * Returns the value for a key from the message bundle.
90
   *
91
   * @param key Retrieve the value for this key.
92
   * @return The value for the key.
93
   */
94
  public static String get( final String key ) {
95
    try {
96
      return resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) );
97
    } catch( final Exception ignored ) {
98
      return key;
99
    }
100
  }
101
102
  /**
103
   * Returns the value for a key from the message bundle.
104
   *
105
   * @param key Retrieve the value for this key.
106
   * @return The value for the key.
107
   */
108
  public static String get( final Key key ) {
109
    return get( key.toString() );
110
  }
111
112
  public static String getLiteral( final String key ) {
113
    return RESOURCE_BUNDLE.getString( key );
114
  }
115
116
  public static String get( final String key, final boolean interpolate ) {
117
    return interpolate ? get( key ) : getLiteral( key );
118
  }
119
120
  /**
121
   * Returns the value for a key from the message bundle with the arguments
122
   * replacing <code>{#}</code> place holders.
123
   *
124
   * @param key  Retrieve the value for this key.
125
   * @param args The values to substitute for place holders.
126
   * @return The value for the key.
127
   */
128
  public static String get( final String key, final Object... args ) {
129
    return MessageFormat.format( get( key ), args );
130
  }
131
132
  /**
133
   * Answers whether the given key is contained in the application's messages
134
   * properties file.
135
   *
136
   * @param key The key to look for in the {@link ResourceBundle}.
137
   * @return {@code true} when the key exists as an exact match.
138
   */
139
  public static boolean containsKey( final String key ) {
140
    return RESOURCE_BUNDLE.containsKey( key );
141
  }
142
143
  /**
144
   * Returns all key names in the application's messages properties file.
145
   *
146
   * @return All key names in the {@link ResourceBundle} encapsulated by
147
   * this class.
148
   */
149
  public static Enumeration<String> getKeys() {
150
    return RESOURCE_BUNDLE.getKeys();
151
  }
152
}
1153
A src/main/java/com/keenwrite/PermissiveCertificate.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
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 final static 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
   * Use {@link #installTrustManager()}.
52
   */
53
  private PermissiveCertificate() {
54
  }
55
56
  /**
57
   * Install the all-trusting trust manager. If this fails it means that in
58
   * certain situations the HTML preview may fail to render diagrams. A way
59
   * to work-around the issue is to install a local server for generating
60
   * diagrams.
61
   */
62
  public static boolean installTrustManager() {
63
    try {
64
      final var context = SSLContext.getInstance( "SSL" );
65
      context.init( null, TRUST_ALL_CERTS, new SecureRandom() );
66
      setDefaultSSLSocketFactory( context.getSocketFactory() );
67
      setDefaultHostnameVerifier( new PermissiveHostNameVerifier() );
68
      return true;
69
    } catch( final Exception ex ) {
70
      return false;
71
    }
72
  }
73
}
174
A src/main/java/com/keenwrite/ScrollEventHandler.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite;
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 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 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/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/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.preferences.LocaleScripts.withScript;
15
import static java.io.File.separator;
16
import static java.lang.String.format;
17
import static java.lang.System.getProperty;
18
19
/**
20
 * Defines application-wide default values.
21
 */
22
public final class Constants {
23
24
  /**
25
   * Used by the default settings to load the {@link Settings} service. This
26
   * must come before any attempt is made to create a {@link Settings} object.
27
   * The reference to {@link Bootstrap#APP_TITLE_LOWERCASE} should cause the
28
   * JVM to load {@link Bootstrap} prior to proceeding. Loading that class
29
   * beforehand will read the bootstrap properties file to determine the
30
   * application name, which is then used to locate the settings properties.
31
   */
32
  public static final String PATH_PROPERTIES_SETTINGS =
33
    format( "/com/%s/settings.properties", APP_TITLE_LOWERCASE );
34
35
  /**
36
   * The {@link Settings} uses {@link #PATH_PROPERTIES_SETTINGS}.
37
   */
38
  public static final Settings sSettings = Services.load( Settings.class );
39
40
  public static final double WINDOW_X_DEFAULT = 0;
41
  public static final double WINDOW_Y_DEFAULT = 0;
42
  public static final double WINDOW_W_DEFAULT = 1200;
43
  public static final double WINDOW_H_DEFAULT = 800;
44
45
  public static final File DOCUMENT_DEFAULT = getFile( "document" );
46
  public static final File DEFINITION_DEFAULT = getFile( "definition" );
47
48
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
49
50
  public static final String STYLESHEET_APPLICATION_BASE =
51
    get( "file.stylesheet.application.base" );
52
  public static final String STYLESHEET_APPLICATION_SKIN =
53
    get( "file.stylesheet.application.skin" );
54
  public static final String STYLESHEET_MARKDOWN =
55
    get( "file.stylesheet.markdown" );
56
  public static final String STYLESHEET_MARKDOWN_LOCALE =
57
    "file.stylesheet.markdown.locale";
58
  public static final String STYLESHEET_PREVIEW =
59
    get( "file.stylesheet.preview" );
60
  public static final String STYLESHEET_PREVIEW_LOCALE =
61
    "file.stylesheet.preview.locale";
62
63
  public static final String FILE_PREFERENCES = getPreferencesFilename();
64
65
  /**
66
   * Refer to file name extension settings in the configuration file. Do not
67
   * terminate with a period.
68
   */
69
  public static final String GLOB_PREFIX_FILE = "file.ext";
70
71
  /**
72
   * Three parameters: line number, column number, and offset.
73
   */
74
  public static final String STATUS_BAR_LINE = "Main.status.line";
75
76
  public static final String STATUS_BAR_OK = "Main.status.state.default";
77
78
  /**
79
   * Used to show an error while parsing, usually syntactical.
80
   */
81
  public static final String STATUS_PARSE_ERROR = "Main.status.error.parse";
82
  public static final String STATUS_DEFINITION_BLANK =
83
    "Main.status.error.def.blank";
84
  public static final String STATUS_DEFINITION_EMPTY =
85
    "Main.status.error.def.empty";
86
87
  /**
88
   * One parameter: the word under the cursor that could not be found.
89
   */
90
  public static final String STATUS_DEFINITION_MISSING =
91
    "Main.status.error.def.missing";
92
93
  /**
94
   * Used when creating flat maps relating to resolved variables.
95
   */
96
  public static final int MAP_SIZE_DEFAULT = 128;
97
98
  /**
99
   * Default image extension order to use when scanning.
100
   */
101
  public static final String PERSIST_IMAGES_DEFAULT =
102
    get( "file.ext.image.order" );
103
104
  /**
105
   * Default working directory to use for R startup script.
106
   */
107
  public static final File USER_DIRECTORY =
108
    new File( System.getProperty( "user.dir" ) );
109
110
  public static final String NEWLINE = System.lineSeparator();
111
112
  /**
113
   * Default path to use for an untitled (pathless) file.
114
   */
115
  public static final Path DEFAULT_DIRECTORY = USER_DIRECTORY.toPath();
116
117
  /**
118
   * Default character set to use when reading/writing files.
119
   */
120
  public static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
121
122
  /**
123
   * Default starting delimiter for definition variables. This value must
124
   * not overlap math delimiters, so do not use $ tokens as the first
125
   * delimiter.
126
   */
127
  public static final String DEF_DELIM_BEGAN_DEFAULT = "{{";
128
129
  /**
130
   * Default ending delimiter for definition variables.
131
   */
132
  public static final String DEF_DELIM_ENDED_DEFAULT = "}}";
133
134
  /**
135
   * Default starting delimiter when inserting R variables.
136
   */
137
  public static final String R_DELIM_BEGAN_DEFAULT = "x( ";
138
139
  /**
140
   * Default ending delimiter when inserting R variables.
141
   */
142
  public static final String R_DELIM_ENDED_DEFAULT = " )";
143
144
  /**
145
   * Resource directory where different language lexicons are located.
146
   */
147
  public static final String LEXICONS_DIRECTORY = "lexicons";
148
149
  /**
150
   * Absolute location of true type font files within the Java archive file.
151
   */
152
  public static final String FONT_DIRECTORY = "/fonts";
153
154
  /**
155
   * Default text editor font name.
156
   */
157
  public static final String FONT_NAME_EDITOR_DEFAULT = "Noto Sans Regular";
158
159
  /**
160
   * Default text editor font size, in points.
161
   */
162
  public static final float FONT_SIZE_EDITOR_DEFAULT = 12f;
163
164
  /**
165
   * Default preview font name.
166
   */
167
  public static final String FONT_NAME_PREVIEW_DEFAULT = "Source Serif 4";
168
169
  /**
170
   * Default preview font size, in points.
171
   */
172
  public static final float FONT_SIZE_PREVIEW_DEFAULT = 13f;
173
174
  /**
175
   * Default monospace preview font name.
176
   */
177
  public static final String FONT_NAME_PREVIEW_MONO_NAME_DEFAULT =
178
    "Source Code Pro";
179
180
  /**
181
   * Default monospace preview font size, in points.
182
   */
183
  public static final float FONT_SIZE_PREVIEW_MONO_SIZE_DEFAULT = 13f;
184
185
  /**
186
   * Default locale for font loading, including ISO 15924 alpha-4 script code.
187
   */
188
  public static final Locale LOCALE_DEFAULT = withScript( Locale.getDefault() );
189
190
  /**
191
   * Default CSS to apply (resolves to a minimal implementation).
192
   */
193
  public static final String SKIN_DEFAULT = "Modena Light";
194
195
  /**
196
   * Custom JavaFX CSS to apply to user interface.
197
   */
198
  public static final File SKIN_CUSTOM_DEFAULT = null;
199
200
  /**
201
   * Custom HTML CSS to apply to HTML preview panel.
202
   */
203
  public static final File PREVIEW_CUSTOM_DEFAULT = null;
204
205
  /**
206
   * Default identifier to use for synchronized scrolling.
207
   */
208
  public static final String CARET_ID = "caret";
209
210
  /**
211
   * Default spacing for UI items (e.g., toolbars).
212
   */
213
  public static final int UI_CONTROL_SPACING = 10;
214
215
  /**
216
   * Default server name for rendering diagrams.
217
   */
218
  public static final String DIAGRAM_SERVER_NAME = "kroki.io";
219
220
  /**
221
   * Application action messages properties prefix.
222
   */
223
  public static final String ACTION_PREFIX = "Action.";
224
225
  /**
226
   * Restrict theme names when displaying.
227
   */
228
  public static final byte THEME_NAME_LENGTH = 30;
229
230
  /**
231
   * Prevent instantiation.
232
   */
233
  private Constants() {
234
  }
235
236
  /**
237
   * Converts from points to pixels because FlyingSaucer cannot handle points
238
   * properly. This is used to convert font sizes.
239
   *
240
   * @param points The points to convert to pixels.
241
   * @return The given number of points in equivalent pixels.
242
   */
243
  public static int toPixels( final double points ) {
244
    return (int) (points * (1 + 1 / 3f));
245
  }
246
247
  static String get( final String key ) {
248
    return sSettings.getSetting( key, "" );
249
  }
250
251
  /**
252
   * Returns a default {@link File} instance based on the given key suffix.
253
   *
254
   * @param suffix Appended to {@code "file.default."}.
255
   * @return A new {@link File} instance that references the settings file name.
256
   */
257
  private static File getFile( final String suffix ) {
258
    return new File( get( "file.default." + suffix ) );
259
  }
260
261
  /**
262
   * Returns the equivalent of {@code $HOME/.filename.xml}.
263
   */
264
  private static String getPreferencesFilename() {
265
    return format(
266
      "%s%s.%s.xml",
267
      getProperty( "user.home" ),
268
      separator,
269
      APP_TITLE_LOWERCASE
270
    );
271
  }
272
}
1273
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.jsoup.helper.W3CDom;
5
import org.jsoup.nodes.Node;
6
import org.jsoup.nodes.TextNode;
7
import org.jsoup.select.NodeVisitor;
8
import org.w3c.dom.Document;
9
10
import java.util.LinkedHashMap;
11
import java.util.Map;
12
13
import static com.keenwrite.dom.DocumentParser.sDomImplementation;
14
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
15
16
/**
17
 * Responsible for converting JSoup document object model (DOM) to a W3C DOM.
18
 * Provides a lighter implementation than the superclass by overriding the
19
 * {@link #fromJsoup(org.jsoup.nodes.Document)} method to reuse factories,
20
 * builders, and implementations.
21
 */
22
public final class DocumentConverter extends W3CDom {
23
  /**
24
   * Retain insertion order using an instance of {@link LinkedHashMap} so
25
   * that ligature substitution uses longer ligatures ahead of shorter
26
   * ligatures. The word "ruffian" should use the "ffi" ligature, not the "ff"
27
   * ligature.
28
   */
29
  private static final Map<String, String> LIGATURES = new LinkedHashMap<>();
30
31
  static {
32
    LIGATURES.put( "ffi", "\uFB03" );
33
    LIGATURES.put( "ffl", "\uFB04" );
34
    LIGATURES.put( "ff", "\uFB00" );
35
    LIGATURES.put( "fi", "\uFB01" );
36
    LIGATURES.put( "fl", "\uFB02" );
37
  }
38
39
  private static final NodeVisitor LIGATURE_VISITOR = new NodeVisitor() {
40
    @Override
41
    public void head( final Node node, final int depth ) {
42
      if( node instanceof final TextNode textNode ) {
43
        final var parent = node.parentNode();
44
        final var name = parent == null ? "root" : parent.nodeName();
45
46
        if( !("pre".equalsIgnoreCase( name ) ||
47
          "code".equalsIgnoreCase( name ) ||
48
          "kbd".equalsIgnoreCase( name ) ||
49
          "var".equalsIgnoreCase( name ) ||
50
          "tt".equalsIgnoreCase( name )) ) {
51
          // Calling getWholeText() will return newlines, which must be kept
52
          // to ensure that preformatted text maintains its formatting.
53
          textNode.text( replace( textNode.getWholeText(), LIGATURES ) );
54
        }
55
      }
56
    }
57
58
    @Override
59
    public void tail( final Node node, final int depth ) {
60
    }
61
  };
62
63
  @Override
64
  public Document fromJsoup( final org.jsoup.nodes.Document in ) {
65
    assert in != null;
66
67
    final var out = DocumentParser.newDocument();
68
    final org.jsoup.nodes.DocumentType doctype = in.documentType();
69
70
    if( doctype != null ) {
71
      out.appendChild(
72
        sDomImplementation.createDocumentType(
73
          doctype.name(),
74
          doctype.publicId(),
75
          doctype.systemId()
76
        )
77
      );
78
    }
79
80
    out.setXmlStandalone( true );
81
    in.traverse( LIGATURE_VISITOR );
82
    convert( in, out );
83
84
    return out;
85
  }
86
}
187
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.IOException;
19
import java.io.InputStream;
20
import java.io.StringReader;
21
import java.io.StringWriter;
22
import java.nio.file.Path;
23
import java.util.HashMap;
24
import java.util.Map;
25
import java.util.function.Consumer;
26
27
import static com.keenwrite.events.StatusEvent.clue;
28
import static java.nio.charset.StandardCharsets.UTF_8;
29
import static javax.xml.transform.OutputKeys.*;
30
import static javax.xml.xpath.XPathConstants.NODESET;
31
32
/**
33
 * Responsible for initializing an XML parser.
34
 */
35
public class DocumentParser {
36
  private static final String LOAD_EXTERNAL_DTD =
37
    "http://apache.org/xml/features/nonvalidating/load-external-dtd";
38
39
  /**
40
   * Caches {@link XPathExpression}s to avoid re-compiling.
41
   */
42
  private static final Map<String, XPathExpression> sXpaths = new HashMap<>();
43
44
  private static final DocumentBuilderFactory sDocumentFactory;
45
  private static DocumentBuilder sDocumentBuilder;
46
  public static DOMImplementation sDomImplementation;
47
  public static Transformer sTransformer;
48
  private static final XPath sXpath = XPathFactory.newInstance().newXPath();
49
50
  static {
51
    sDocumentFactory = DocumentBuilderFactory.newInstance();
52
53
    sDocumentFactory.setValidating( false );
54
    sDocumentFactory.setAttribute( LOAD_EXTERNAL_DTD, false );
55
    sDocumentFactory.setNamespaceAware( true );
56
    sDocumentFactory.setIgnoringComments( true );
57
    sDocumentFactory.setIgnoringElementContentWhitespace( true );
58
59
    try {
60
      sDocumentBuilder = sDocumentFactory.newDocumentBuilder();
61
      sDomImplementation = sDocumentBuilder.getDOMImplementation();
62
      sTransformer = TransformerFactory.newInstance().newTransformer();
63
64
      sTransformer.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
65
      sTransformer.setOutputProperty( METHOD, "xml" );
66
      sTransformer.setOutputProperty( INDENT, "no" );
67
      sTransformer.setOutputProperty( ENCODING, UTF_8.toString() );
68
    } catch( final Exception ex ) {
69
      clue( ex );
70
    }
71
  }
72
73
  /**
74
   * Use the {@code static} constants and methods, not an instance, at least
75
   * until an iterable sub-interface is written.
76
   */
77
  private DocumentParser() {}
78
79
  public static Document newDocument() {
80
    return sDocumentBuilder.newDocument();
81
  }
82
83
  /**
84
   * Creates a new document object model based on the given XML document
85
   * string. This will return an empty document if the document could not
86
   * be parsed.
87
   *
88
   * @param xml The document text to convert into a DOM.
89
   * @return The DOM that represents the given XML data.
90
   */
91
  public static Document parse( final String xml ) {
92
    final var input = new InputSource();
93
94
    try( final var reader = new StringReader( xml ) ) {
95
      input.setEncoding( UTF_8.toString() );
96
      input.setCharacterStream( reader );
97
98
      return sDocumentBuilder.parse( input );
99
    } catch( final Exception ex ) {
100
      clue( ex );
101
102
      return sDocumentBuilder.newDocument();
103
    }
104
  }
105
106
  public static Document parse( final InputStream doc )
107
    throws IOException, SAXException {
108
    return sDocumentBuilder.parse( doc );
109
  }
110
111
  /**
112
   * Allows an operation to be applied for every node in the document that
113
   * matches a given tag name pattern.
114
   *
115
   * @param document Document to traverse.
116
   * @param xpath    Document elements to find via {@link XPath} expression.
117
   * @param consumer The consumer to call for each matching document node.
118
   */
119
  public static void walk(
120
    final Document document,
121
    final String xpath,
122
    final Consumer<Node> consumer ) {
123
    assert document != null;
124
    assert consumer != null;
125
126
    try {
127
      final var expr = lookupXPathExpression( xpath );
128
      final var nodes = (NodeList) expr.evaluate( document, NODESET );
129
130
      if( nodes != null ) {
131
        for( int i = 0, len = nodes.getLength(); i < len; i++ ) {
132
          consumer.accept( nodes.item( i ) );
133
        }
134
      }
135
    } catch( final Exception ex ) {
136
      clue( ex );
137
    }
138
  }
139
140
  public static Node createMeta(
141
    final Document document, final Map.Entry<String, String> entry ) {
142
    final var node = document.createElement( "meta" );
143
144
    node.setAttribute( "name", entry.getKey() );
145
    node.setAttribute( "content", entry.getValue() );
146
147
    return node;
148
  }
149
150
  public static String toString( final Document xhtml ) {
151
    try( final var writer = new StringWriter() ) {
152
      final var domSource = new DOMSource( xhtml );
153
      final var result = new StreamResult( writer );
154
155
      sTransformer.transform( domSource, result );
156
157
      return writer.toString();
158
    } catch( final Exception ex ) {
159
      clue( ex );
160
      return "";
161
    }
162
  }
163
164
  public static String transform( final Element root )
165
    throws IOException, TransformerException {
166
    try( final var writer = new StringWriter() ) {
167
      sTransformer.transform(
168
        new DOMSource( root ), new StreamResult( writer )
169
      );
170
171
      return writer.toString();
172
    }
173
  }
174
175
  /**
176
   * Remove whitespace, comments, and XML/DOCTYPE declarations to make
177
   * processing work with ConTeXt.
178
   *
179
   * @param path The SVG file to process.
180
   * @throws Exception The file could not be processed.
181
   */
182
  public static void sanitize( final Path path )
183
    throws Exception {
184
    final var file = path.toFile();
185
186
    sTransformer.transform(
187
      new DOMSource( sDocumentBuilder.parse( file ) ), new StreamResult( file )
188
    );
189
  }
190
191
  /**
192
   * Adorns the given document with {@code html}, {@code head}, and
193
   * {@code body} elements.
194
   *
195
   * @param html The document to decorate.
196
   * @return A document with a typical HTML structure.
197
   */
198
  public static String decorate( final String html ) {
199
    return
200
      "<html><head><title> </title><meta charset='utf8'/></head><body>"
201
        + html
202
        + "</body></html>";
203
  }
204
205
  private static XPathExpression lookupXPathExpression( final String xpath ) {
206
    return sXpaths.computeIfAbsent( xpath, k -> {
207
      try {
208
        return sXpath.compile( xpath );
209
      } catch( final XPathExpressionException ex ) {
210
        clue( ex );
211
        return null;
212
      }
213
    } );
214
  }
215
}
1216
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 com.keenwrite.sigils.Tokens;
8
import javafx.scene.control.TreeItem;
9
10
import java.util.Map;
11
12
/**
13
 * Differentiates an instance of {@link TextResource} from an instance of
14
 * {@link DefinitionEditor} or {@link MarkdownEditor}.
15
 */
16
public interface TextDefinition extends TextResource {
17
  /**
18
   * Converts the definitions into a map, ready for interpolation.
19
   *
20
   * @return The list of key value pairs delimited with tokens.
21
   */
22
  Map<String, String> toMap();
23
24
  /**
25
   * Performs string interpolation on the values in the given map. This will
26
   * change any value in the map that contains a variable that matches
27
   * the definition regex pattern against the given {@link Tokens}.
28
   *
29
   * @param map Contains values that represent references to keys.
30
   * @param tokens The beginning and ending tokens that delimit variables.
31
   */
32
  Map<String, String> interpolate( Map<String, String> map, Tokens tokens );
33
34
  /**
35
   * Requests that the visual representation be expanded to the given
36
   * node.
37
   *
38
   * @param node Request expansion to this node.
39
   */
40
  <T> void expand( TreeItem<T> node );
41
42
  /**
43
   * Adds a new item to the definition hierarchy.
44
   */
45
  void createDefinition();
46
47
  /**
48
   * Edits the currently selected definition in the hierarchy.
49
   */
50
  void renameDefinition();
51
52
  /**
53
   * Removes the currently selected definition in the hierarchy.
54
   */
55
  void deleteDefinitions();
56
57
  /**
58
   * Finds the definition that exact matches the given text.
59
   *
60
   * @param text The value to find, never {@code null}.
61
   * @return The leaf that contains the given value.
62
   */
63
  DefinitionTreeItem<String> findLeafExact( String text );
64
65
  /**
66
   * Finds the definition that starts with the given text.
67
   *
68
   * @param text The value to find, never {@code null}.
69
   * @return The leaf that starts with the given value.
70
   */
71
  DefinitionTreeItem<String> findLeafStartsWith( String text );
72
73
  /**
74
   * Finds the definition that contains the given text, matching case.
75
   *
76
   * @param text The value to find, never {@code null}.
77
   * @return The leaf that contains the exact given value.
78
   */
79
  DefinitionTreeItem<String> findLeafContains( String text );
80
81
  /**
82
   * Finds the definition that contains the given text, ignoring case.
83
   *
84
   * @param text The value to find, never {@code null}.
85
   * @return The leaf that contains the given value, regardless of case.
86
   */
87
  DefinitionTreeItem<String> findLeafContainsNoCase( String text );
88
89
  /**
90
   * Answers whether there are any definitions written.
91
   *
92
   * @return {@code true} when there are no definitions.
93
   */
94
  boolean isEmpty();
95
}
196
A src/main/java/com/keenwrite/editors/TextEditor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.editors;
3
4
import com.keenwrite.Caret;
5
import javafx.scene.control.IndexRange;
6
import org.fxmisc.flowless.VirtualizedScrollPane;
7
import org.fxmisc.richtext.StyleClassedTextArea;
8
9
/**
10
 * Responsible for differentiating an instance of {@link TextResource} from
11
 * other {@link TextResource} subtypes, such as a {@link TextDefinition}.
12
 * This is primarily used as a marker interface, but also defines a minimal
13
 * set of functionality required by all {@link TextEditor} instances, which
14
 * includes scrolling facilities.
15
 */
16
public interface TextEditor extends TextResource {
17
18
  /**
19
   * Returns the scrollbars associated with the editor's view so that they
20
   * can be moved for synchronized scrolling.
21
   *
22
   * @return The initialized horizontal and vertical scrollbars.
23
   */
24
  VirtualizedScrollPane<StyleClassedTextArea> getScrollPane();
25
26
  StyleClassedTextArea getTextArea();
27
28
  /**
29
   * Requests that styling be added to the document between the given
30
   * integer values.
31
   *
32
   * @param indexes Document offset where style is to start and end.
33
   * @param style   The style class to apply between the given offset indexes.
34
   */
35
  default void stylize( final IndexRange indexes, final String style ) {
36
  }
37
38
  /**
39
   * Requests that the most recent styling for the given style class be
40
   * removed from the document between the given integer values.
41
   */
42
  default void unstylize( final String style ) {
43
  }
44
45
  /**
46
   * Returns the complete text for the specified paragraph index.
47
   *
48
   * @param paragraph The zero-based paragraph index.
49
   * @throws IndexOutOfBoundsException The paragraph index is less than zero
50
   *                                   or greater than the number of
51
   *                                   paragraphs in the document.
52
   */
53
  String getText( int paragraph ) throws IndexOutOfBoundsException;
54
55
  /**
56
   * Returns the text between the indexes specified by the given
57
   * {@link IndexRange}.
58
   *
59
   * @param indexes The start and end document indexes to reference.
60
   * @return The text between the specified indexes.
61
   * @throws IndexOutOfBoundsException The indexes are invalid.
62
   */
63
  String getText( IndexRange indexes ) throws IndexOutOfBoundsException;
64
65
  /**
66
   * Moves the caret to the given document offset.
67
   *
68
   * @param offset The absolute offset into the document, zero-based.
69
   */
70
  void moveTo( final int offset );
71
72
  /**
73
   * Returns an object that can be used to track the current caret position
74
   * within the document.
75
   *
76
   * @return The caret's position, which is updated continuously.
77
   */
78
  Caret getCaret();
79
80
  /**
81
   * Replaces the text within the given range with the given string.
82
   *
83
   * @param indexes The starting and ending document indexes that represent
84
   *                the range of text to replace.
85
   * @param s       The text to replace, which can be shorter or longer than the
86
   *                specified range.
87
   */
88
  void replaceText( IndexRange indexes, String s );
89
90
  /**
91
   * Returns the starting and ending indexes into the document for the
92
   * word at the current caret position.
93
   * <p>
94
   * Finds the start and end indexes for the word in the current document,
95
   * where the caret is located. There are a few different scenarios, where
96
   * the caret can be at: the start, end, or middle of a word; also, the
97
   * caret can be at the end or beginning of a punctuated word; as well, the
98
   * caret could be at the beginning or end of the line or document.
99
   * </p>
100
   *
101
   * @return The start and ending index into the current document that
102
   * represent the word boundaries of the word under the caret.
103
   */
104
  IndexRange getCaretWord();
105
106
  /**
107
   * Convenience method to get the word at the current caret position.
108
   *
109
   * @return This will return the empty string if the caret is out of bounds.
110
   */
111
  default String getCaretWordText() {
112
    return getText( getCaretWord() );
113
  }
114
115
  /**
116
   * Requests undoing the last text-changing action.
117
   */
118
  void undo();
119
120
  /**
121
   * Requests redoing the last text-changing action that was undone.
122
   */
123
  void redo();
124
125
  /**
126
   * Requests cutting the selected text, or the current line if none selected.
127
   */
128
  void cut();
129
130
  /**
131
   * Requests copying the selected text, or no operation if none selected.
132
   */
133
  void copy();
134
135
  /**
136
   * Requests pasting from the clipboard into the editor. This will replace
137
   * text if selected, otherwise the clipboard contents are inserted at the
138
   * cursor.
139
   */
140
  void paste();
141
142
  /**
143
   * Requests selecting the entire document. This will replace the existing
144
   * selection, if any.
145
   */
146
  void selectAll();
147
148
  /**
149
   * Requests making the selected text, or word at caret, bold.
150
   */
151
  default void bold() { }
152
153
  /**
154
   * Requests making the selected text, or word at caret, italic.
155
   */
156
  default void italic() { }
157
158
  /**
159
   * Requests making the selected text, or word at caret, monospace.
160
   */
161
  default void monospace() { }
162
163
  /**
164
   * Requests making the selected text, or word at caret, a superscript.
165
   */
166
  default void superscript() { }
167
168
  /**
169
   * Requests making the selected text, or word at caret, a subscript.
170
   */
171
  default void subscript() { }
172
173
  /**
174
   * Requests making the selected text, or word at caret, struck.
175
   */
176
  default void strikethrough() { }
177
178
  /**
179
   * Requests making the selected text, or word at caret, a blockquote block.
180
   */
181
  default void blockquote() { }
182
183
  /**
184
   * Requests making the selected text, or word at caret, inline code.
185
   */
186
  default void code() { }
187
188
  /**
189
   * Requests making the selected text, or word at caret, a fenced code block.
190
   */
191
  default void fencedCodeBlock() { }
192
193
  /**
194
   * Requests making the selected text, or word at caret, a heading.
195
   *
196
   * @param level The heading level to apply (typically 1 through 3).
197
   */
198
  default void heading( final int level ) { }
199
200
  /**
201
   * Requests making the selected text, or word at caret, an unordered list
202
   * block.
203
   */
204
  default void unorderedList() { }
205
206
  /**
207
   * Requests making the selected text, or word at caret, an ordered list block.
208
   */
209
  default void orderedList() { }
210
211
  /**
212
   * Requests making the selected text, or inserting at the caret, a
213
   * horizontal rule.
214
   */
215
  default void horizontalRule() { }
216
}
1217
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/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.sigils.Tokens;
7
import com.keenwrite.ui.tree.AltTreeView;
8
import com.keenwrite.ui.tree.TreeItemConverter;
9
import javafx.beans.property.BooleanProperty;
10
import javafx.beans.property.ReadOnlyBooleanProperty;
11
import javafx.beans.property.SimpleBooleanProperty;
12
import javafx.beans.value.ObservableValue;
13
import javafx.collections.ObservableList;
14
import javafx.event.ActionEvent;
15
import javafx.event.Event;
16
import javafx.event.EventHandler;
17
import javafx.scene.Node;
18
import javafx.scene.control.*;
19
import javafx.scene.input.KeyEvent;
20
import javafx.scene.layout.BorderPane;
21
import javafx.scene.layout.HBox;
22
23
import java.io.File;
24
import java.nio.charset.Charset;
25
import java.util.*;
26
import java.util.regex.Pattern;
27
28
import static com.keenwrite.constants.Constants.*;
29
import static com.keenwrite.Messages.get;
30
import static com.keenwrite.events.StatusEvent.clue;
31
import static com.keenwrite.events.TextDefinitionFocusEvent.fireTextDefinitionFocus;
32
import static com.keenwrite.ui.fonts.IconFactory.createGraphic;
33
import static java.lang.String.format;
34
import static java.util.regex.Pattern.compile;
35
import static java.util.regex.Pattern.quote;
36
import static javafx.geometry.Pos.CENTER;
37
import static javafx.geometry.Pos.TOP_CENTER;
38
import static javafx.scene.control.SelectionMode.MULTIPLE;
39
import static javafx.scene.control.TreeItem.childrenModificationEvent;
40
import static javafx.scene.control.TreeItem.valueChangedEvent;
41
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
42
43
/**
44
 * Provides the user interface that holds a {@link TreeView}, which
45
 * allows users to interact with key/value pairs loaded from the
46
 * document parser and adapted using a {@link TreeTransformer}.
47
 */
48
public final class DefinitionEditor extends BorderPane
49
  implements TextDefinition {
50
  private static final int GROUP_DELIMITED = 1;
51
52
  /**
53
   * Contains the root that is added to the view.
54
   */
55
  private final DefinitionTreeItem<String> mTreeRoot = createRootTreeItem();
56
57
  /**
58
   * Contains a view of the definitions.
59
   */
60
  private final TreeView<String> mTreeView =
61
    new AltTreeView<>( mTreeRoot, new TreeItemConverter() );
62
63
  /**
64
   * Used to adapt the structured document into a {@link TreeView}.
65
   */
66
  private final TreeTransformer mTreeTransformer;
67
68
  /**
69
   * Handlers for key press events.
70
   */
71
  private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
72
    = new HashSet<>();
73
74
  /**
75
   * File being edited by this editor instance.
76
   */
77
  private File mFile;
78
79
  /**
80
   * Opened file's character encoding, or {@link Constants#DEFAULT_CHARSET} if
81
   * either no encoding could be determined or this is a new (empty) file.
82
   */
83
  private final Charset mEncoding;
84
85
  /**
86
   * Tracks whether the in-memory definitions have changed with respect to the
87
   * persisted definitions.
88
   */
89
  private final BooleanProperty mModified = new SimpleBooleanProperty();
90
91
  /**
92
   * This is provided for unit tests that are not backed by files.
93
   *
94
   * @param treeTransformer Responsible for transforming the definitions into
95
   *                        {@link TreeItem} instances.
96
   */
97
  public DefinitionEditor(
98
    final TreeTransformer treeTransformer ) {
99
    this( DEFINITION_DEFAULT, treeTransformer );
100
  }
101
102
  /**
103
   * Constructs a definition pane with a given tree view root.
104
   *
105
   * @param file The file of definitions to maintain through the UI.
106
   */
107
  public DefinitionEditor(
108
    final File file,
109
    final TreeTransformer treeTransformer ) {
110
    assert file != null;
111
    assert treeTransformer != null;
112
113
    mFile = file;
114
    mTreeTransformer = treeTransformer;
115
116
    //mTreeView.setCellFactory( new TreeCellFactory() );
117
    mTreeView.setContextMenu( createContextMenu() );
118
    mTreeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
119
    mTreeView.focusedProperty().addListener( this::focused );
120
    getSelectionModel().setSelectionMode( MULTIPLE );
121
122
    final var buttonBar = new HBox();
123
    buttonBar.getChildren().addAll(
124
      createButton( "create", e -> createDefinition() ),
125
      createButton( "rename", e -> renameDefinition() ),
126
      createButton( "delete", e -> deleteDefinitions() )
127
    );
128
    buttonBar.setAlignment( CENTER );
129
    buttonBar.setSpacing( UI_CONTROL_SPACING );
130
131
    setTop( buttonBar );
132
    setCenter( mTreeView );
133
    setAlignment( buttonBar, TOP_CENTER );
134
    mEncoding = open( mFile );
135
136
    // After the file is opened, watch for changes, not before. Otherwise,
137
    // upon saving, users will be prompted to save a file that hasn't had
138
    // any modifications (from their perspective).
139
    addTreeChangeHandler( event -> mModified.set( true ) );
140
  }
141
142
  @Override
143
  public void setText( final String document ) {
144
    final var foster = mTreeTransformer.transform( document );
145
    final var biological = getTreeRoot();
146
147
    for( final var child : foster.getChildren() ) {
148
      biological.getChildren().add( child );
149
    }
150
151
    getTreeView().refresh();
152
  }
153
154
  @Override
155
  public String getText() {
156
    final var result = new StringBuilder( 32768 );
157
158
    try {
159
      final var root = getTreeView().getRoot();
160
      final var problem = isTreeWellFormed();
161
162
      problem.ifPresentOrElse(
163
        ( node ) -> clue( "yaml.error.tree.form", node ),
164
        () -> result.append( mTreeTransformer.transform( root ) )
165
      );
166
    } catch( final Exception ex ) {
167
      // Catch errors while checking for a well-formed tree (e.g., stack smash).
168
      // Also catch any transformation exceptions (e.g., Json processing).
169
      clue( ex );
170
    }
171
172
    return result.toString();
173
  }
174
175
  @Override
176
  public File getFile() {
177
    return mFile;
178
  }
179
180
  @Override
181
  public void rename( final File file ) {
182
    mFile = file;
183
  }
184
185
  @Override
186
  public Charset getEncoding() {
187
    return mEncoding;
188
  }
189
190
  @Override
191
  public Node getNode() {
192
    return this;
193
  }
194
195
  @Override
196
  public ReadOnlyBooleanProperty modifiedProperty() {
197
    return mModified;
198
  }
199
200
  @Override
201
  public void clearModifiedProperty() {
202
    mModified.setValue( false );
203
  }
204
205
  private Button createButton(
206
    final String msgKey, final EventHandler<ActionEvent> eventHandler ) {
207
    final var keyPrefix = Constants.ACTION_PREFIX + "definition." + msgKey;
208
    final var button = new Button( get( keyPrefix + ".text" ) );
209
    final var graphic = createGraphic( get( keyPrefix + ".icon" ) );
210
211
    button.setOnAction( eventHandler );
212
    button.setGraphic( graphic );
213
    button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) );
214
215
    return button;
216
  }
217
218
  @Override
219
  public Map<String, String> toMap() {
220
    return new TreeItemMapper().toMap( getTreeView().getRoot() );
221
  }
222
223
  @Override
224
  public Map<String, String> interpolate(
225
    final Map<String, String> map, final Tokens tokens ) {
226
227
    // Non-greedy match of key names delimited by definition tokens.
228
    final var pattern = compile(
229
      format( "(%s.*?%s)",
230
              quote( tokens.getBegan() ),
231
              quote( tokens.getEnded() )
232
      )
233
    );
234
235
    map.replaceAll( ( k, v ) -> resolve( map, v, pattern ) );
236
    return map;
237
  }
238
239
  /**
240
   * Given a value with zero or more key references, this will resolve all
241
   * the values, recursively. If a key cannot be de-referenced, the value will
242
   * contain the key name.
243
   *
244
   * @param map     Map to search for keys when resolving key references.
245
   * @param value   Value containing zero or more key references.
246
   * @param pattern The regular expression pattern to match variable key names.
247
   * @return The given value with all embedded key references interpolated.
248
   */
249
  private String resolve(
250
    final Map<String, String> map, String value, final Pattern pattern ) {
251
    final var matcher = pattern.matcher( value );
252
253
    while( matcher.find() ) {
254
      final var keyName = matcher.group( GROUP_DELIMITED );
255
      final var mapValue = map.get( keyName );
256
      final var keyValue = mapValue == null
257
        ? keyName
258
        : resolve( map, mapValue, pattern );
259
260
      value = value.replace( keyName, keyValue );
261
    }
262
263
    return value;
264
  }
265
266
267
  /**
268
   * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
269
   * is modified. The modifications include: item value changes, item additions,
270
   * and item removals.
271
   * <p>
272
   * Safe to call multiple times; if a handler is already registered, the
273
   * old handler is used.
274
   * </p>
275
   *
276
   * @param handler The handler to call whenever any {@link TreeItem} changes.
277
   */
278
  public void addTreeChangeHandler(
279
    final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
280
    final var root = getTreeView().getRoot();
281
    root.addEventHandler( valueChangedEvent(), handler );
282
    root.addEventHandler( childrenModificationEvent(), handler );
283
  }
284
285
  /**
286
   * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
287
   * well-formed for export. A tree is considered well-formed if the following
288
   * conditions are met:
289
   *
290
   * <ul>
291
   *   <li>The root node contains at least one child node having a leaf.</li>
292
   *   <li>There are no leaf nodes with sibling leaf nodes.</li>
293
   * </ul>
294
   *
295
   * @return {@code null} if the document is well-formed, otherwise the
296
   * problematic child {@link TreeItem}.
297
   */
298
  public Optional<TreeItem<String>> isTreeWellFormed() {
299
    final var root = getTreeView().getRoot();
300
301
    for( final var child : root.getChildren() ) {
302
      final var problemChild = isWellFormed( child );
303
304
      if( child.isLeaf() || problemChild != null ) {
305
        return Optional.ofNullable( problemChild );
306
      }
307
    }
308
309
    return Optional.empty();
310
  }
311
312
  /**
313
   * Determines whether the document is well-formed by ensuring that
314
   * child branches do not contain multiple leaves.
315
   *
316
   * @param item The sub-tree to check for well-formedness.
317
   * @return {@code null} when the tree is well-formed, otherwise the
318
   * problematic {@link TreeItem}.
319
   */
320
  private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
321
    int childLeafs = 0;
322
    int childBranches = 0;
323
324
    for( final var child : item.getChildren() ) {
325
      if( child.isLeaf() ) {
326
        childLeafs++;
327
      }
328
      else {
329
        childBranches++;
330
      }
331
332
      final var problemChild = isWellFormed( child );
333
334
      if( problemChild != null ) {
335
        return problemChild;
336
      }
337
    }
338
339
    return ((childBranches > 0 && childLeafs == 0) ||
340
      (childBranches == 0 && childLeafs <= 1)) ? null : item;
341
  }
342
343
  @Override
344
  public DefinitionTreeItem<String> findLeafExact( final String text ) {
345
    return getTreeRoot().findLeafExact( text );
346
  }
347
348
  @Override
349
  public DefinitionTreeItem<String> findLeafContains( final String text ) {
350
    return getTreeRoot().findLeafContains( text );
351
  }
352
353
  @Override
354
  public DefinitionTreeItem<String> findLeafContainsNoCase(
355
    final String text ) {
356
    return getTreeRoot().findLeafContainsNoCase( text );
357
  }
358
359
  @Override
360
  public DefinitionTreeItem<String> findLeafStartsWith( final String text ) {
361
    return getTreeRoot().findLeafStartsWith( text );
362
  }
363
364
  public void select( final TreeItem<String> item ) {
365
    getSelectionModel().clearSelection();
366
    getSelectionModel().select( getTreeView().getRow( item ) );
367
  }
368
369
  /**
370
   * Collapses the tree, recursively.
371
   */
372
  public void collapse() {
373
    collapse( getTreeRoot().getChildren() );
374
  }
375
376
  /**
377
   * Collapses the tree, recursively.
378
   *
379
   * @param <T>   The type of tree item to expand (usually String).
380
   * @param nodes The nodes to collapse.
381
   */
382
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
383
    for( final var node : nodes ) {
384
      node.setExpanded( false );
385
      collapse( node.getChildren() );
386
    }
387
  }
388
389
  /**
390
   * @return {@code true} when the user is editing a {@link TreeItem}.
391
   */
392
  private boolean isEditingTreeItem() {
393
    return getTreeView().editingItemProperty().getValue() != null;
394
  }
395
396
  /**
397
   * Changes to edit mode for the selected item.
398
   */
399
  @Override
400
  public void renameDefinition() {
401
    getTreeView().edit( getSelectedItem() );
402
  }
403
404
  /**
405
   * Removes all selected items from the {@link TreeView}.
406
   */
407
  @Override
408
  public void deleteDefinitions() {
409
    for( final var item : getSelectedItems() ) {
410
      final var parent = item.getParent();
411
412
      if( parent != null ) {
413
        parent.getChildren().remove( item );
414
      }
415
    }
416
  }
417
418
  /**
419
   * Deletes the selected item.
420
   */
421
  private void deleteSelectedItem() {
422
    final var c = getSelectedItem();
423
    getSiblings( c ).remove( c );
424
  }
425
426
  /**
427
   * Adds a new item under the selected item (or root if nothing is selected).
428
   * There are a few conditions to consider: when adding to the root,
429
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
430
   * root must contain two items: a key and a value.
431
   */
432
  @Override
433
  public void createDefinition() {
434
    final var value = createDefinitionTreeItem();
435
    getSelectedItem().getChildren().add( value );
436
    expand( value );
437
    select( value );
438
  }
439
440
  private ContextMenu createContextMenu() {
441
    final var menu = new ContextMenu();
442
    final var items = menu.getItems();
443
444
    addMenuItem( items, ACTION_PREFIX + "definition.create.text" )
445
      .setOnAction( e -> createDefinition() );
446
    addMenuItem( items, ACTION_PREFIX + "definition.rename.text" )
447
      .setOnAction( e -> renameDefinition() );
448
    addMenuItem( items, ACTION_PREFIX + "definition.delete.text" )
449
      .setOnAction( e -> deleteSelectedItem() );
450
451
    return menu;
452
  }
453
454
  /**
455
   * Executes hot-keys for edits to the definition tree.
456
   *
457
   * @param event Contains the key code of the key that was pressed.
458
   */
459
  private void keyEventFilter( final KeyEvent event ) {
460
    if( !isEditingTreeItem() ) {
461
      switch( event.getCode() ) {
462
        case ENTER -> {
463
          expand( getSelectedItem() );
464
          event.consume();
465
        }
466
467
        case DELETE -> deleteDefinitions();
468
        case INSERT -> createDefinition();
469
470
        case R -> {
471
          if( event.isControlDown() ) {
472
            renameDefinition();
473
          }
474
        }
475
      }
476
477
      for( final var handler : getKeyEventHandlers() ) {
478
        handler.handle( event );
479
      }
480
    }
481
  }
482
483
  /**
484
   * Called when the editor's input focus changes. This will fire an event
485
   * for subscribers.
486
   *
487
   * @param ignored Not used.
488
   * @param o       The old input focus property value.
489
   * @param n       The new input focus property value.
490
   */
491
  private void focused(
492
    final ObservableValue<? extends Boolean> ignored,
493
    final Boolean o,
494
    final Boolean n ) {
495
    if( n != null && n ) {
496
      fireTextDefinitionFocus( this );
497
    }
498
  }
499
500
  /**
501
   * Adds a menu item to a list of menu items.
502
   *
503
   * @param items    The list of menu items to append to.
504
   * @param labelKey The resource bundle key name for the menu item's label.
505
   * @return The menu item added to the list of menu items.
506
   */
507
  private MenuItem addMenuItem(
508
    final List<MenuItem> items, final String labelKey ) {
509
    final MenuItem menuItem = createMenuItem( labelKey );
510
    items.add( menuItem );
511
    return menuItem;
512
  }
513
514
  private MenuItem createMenuItem( final String labelKey ) {
515
    return new MenuItem( get( labelKey ) );
516
  }
517
518
  /**
519
   * Creates a new {@link TreeItem} that is intended to be the root-level item
520
   * added to the {@link TreeView}. This allows the root item to be
521
   * distinguished from the other items so that reference keys do not include
522
   * "Definition" as part of their name.
523
   *
524
   * @return A new {@link TreeItem}, never {@code null}.
525
   */
526
  private RootTreeItem<String> createRootTreeItem() {
527
    return new RootTreeItem<>( get( "Pane.definition.node.root.title" ) );
528
  }
529
530
  private DefinitionTreeItem<String> createDefinitionTreeItem() {
531
    return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) );
532
  }
533
534
  @Override
535
  public void requestFocus() {
536
    //super.requestFocus();
537
    getTreeView().requestFocus();
538
  }
539
540
  /**
541
   * Expands the node to the root, recursively.
542
   *
543
   * @param <T>  The type of tree item to expand (usually String).
544
   * @param node The node to expand.
545
   */
546
  @Override
547
  public <T> void expand( final TreeItem<T> node ) {
548
    if( node != null ) {
549
      expand( node.getParent() );
550
      node.setExpanded( !node.isLeaf() );
551
    }
552
  }
553
554
  /**
555
   * Answers whether there are any definitions in the tree.
556
   *
557
   * @return {@code true} when there are no definitions; {@code false} when
558
   * there's at least one definition.
559
   */
560
  @Override
561
  public boolean isEmpty() {
562
    return getTreeRoot().isEmpty();
563
  }
564
565
  /**
566
   * Returns the actively selected item in the tree.
567
   *
568
   * @return The selected item, or the tree root item if no item is selected.
569
   */
570
  public TreeItem<String> getSelectedItem() {
571
    final var item = getSelectionModel().getSelectedItem();
572
    return item == null ? getTreeRoot() : item;
573
  }
574
575
  /**
576
   * Returns the {@link TreeView} that contains the definition hierarchy.
577
   *
578
   * @return A non-null instance.
579
   */
580
  private TreeView<String> getTreeView() {
581
    return mTreeView;
582
  }
583
584
  /**
585
   * Returns the root of the tree.
586
   *
587
   * @return The first node added to the definition tree.
588
   */
589
  private DefinitionTreeItem<String> getTreeRoot() {
590
    return mTreeRoot;
591
  }
592
593
  private ObservableList<TreeItem<String>> getSiblings(
594
    final TreeItem<String> item ) {
595
    final var root = getTreeView().getRoot();
596
    final var parent = (item == null || item == root) ? root : item.getParent();
597
598
    return parent.getChildren();
599
  }
600
601
  private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
602
    return getTreeView().getSelectionModel();
603
  }
604
605
  /**
606
   * Returns a copy of all the selected items.
607
   *
608
   * @return A list, possibly empty, containing all selected items in the
609
   * {@link TreeView}.
610
   */
611
  private List<TreeItem<String>> getSelectedItems() {
612
    return new ArrayList<>( getSelectionModel().getSelectedItems() );
613
  }
614
615
  private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() {
616
    return mKeyEventHandlers;
617
  }
618
}
1619
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 javafx.scene.control.TreeItem;
5
6
import java.util.Stack;
7
import java.util.function.BiFunction;
8
9
import static java.text.Normalizer.Form.NFD;
10
import static java.text.Normalizer.normalize;
11
12
/**
13
 * Provides behaviour afforded to definition keys and corresponding value.
14
 *
15
 * @param <T> The type of {@link TreeItem} (usually string).
16
 */
17
public class DefinitionTreeItem<T> extends TreeItem<T> {
18
19
  /**
20
   * Constructs a new item with a default value.
21
   *
22
   * @param value Passed up to superclass.
23
   */
24
  public DefinitionTreeItem( final T value ) {
25
    super( value );
26
  }
27
28
  /**
29
   * Finds a leaf starting at the current node with text that matches the given
30
   * value. Search is performed case-sensitively.
31
   *
32
   * @param text The text to match against each leaf in the tree.
33
   * @return The leaf that has a value exactly matching the given text.
34
   */
35
  public DefinitionTreeItem<T> findLeafExact( final String text ) {
36
    return findLeaf( text, DefinitionTreeItem::valueEquals );
37
  }
38
39
  /**
40
   * Finds a leaf starting at the current node with text that matches the given
41
   * value. Search is performed case-sensitively.
42
   *
43
   * @param text The text to match against each leaf in the tree.
44
   * @return The leaf that has a value that contains the given text.
45
   */
46
  public DefinitionTreeItem<T> findLeafContains( final String text ) {
47
    return findLeaf( text, DefinitionTreeItem::valueContains );
48
  }
49
50
  /**
51
   * Finds a leaf starting at the current node with text that matches the given
52
   * value. Search is performed case-insensitively.
53
   *
54
   * @param text The text to match against each leaf in the tree.
55
   * @return The leaf that has a value that contains the given text.
56
   */
57
  public DefinitionTreeItem<T> findLeafContainsNoCase( final String text ) {
58
    return findLeaf( text, DefinitionTreeItem::valueContainsNoCase );
59
  }
60
61
  /**
62
   * Finds a leaf starting at the current node with text that matches the given
63
   * value. Search is performed case-sensitively.
64
   *
65
   * @param text The text to match against each leaf in the tree.
66
   * @return The leaf that has a value that starts with the given text.
67
   */
68
  public DefinitionTreeItem<T> findLeafStartsWith( final String text ) {
69
    return findLeaf( text, DefinitionTreeItem::valueStartsWith );
70
  }
71
72
  /**
73
   * Finds a leaf starting at the current node with text that matches the given
74
   * value.
75
   *
76
   * @param text     The text to match against each leaf in the tree.
77
   * @param findMode What algorithm is used to match the given text.
78
   * @return The leaf that has a value starting with the given text, or {@code
79
   * null} if there was no match found.
80
   */
81
  public DefinitionTreeItem<T> findLeaf(
82
    final String text,
83
    final BiFunction<DefinitionTreeItem<T>, String, Boolean> findMode ) {
84
    final var stack = new Stack<DefinitionTreeItem<T>>();
85
    stack.push( this );
86
87
    // Don't hunt for blank (empty) keys.
88
    boolean found = text.isBlank();
89
90
    while( !found && !stack.isEmpty() ) {
91
      final var node = stack.pop();
92
93
      for( final var child : node.getChildren() ) {
94
        final var result = (DefinitionTreeItem<T>) child;
95
96
        if( result.isLeaf() ) {
97
          if( found = findMode.apply( result, text ) ) {
98
            return result;
99
          }
100
        }
101
        else {
102
          stack.push( result );
103
        }
104
      }
105
    }
106
107
    return null;
108
  }
109
110
  /**
111
   * Returns the value of the string without diacritic marks.
112
   *
113
   * @return A non-null, possibly empty string.
114
   */
115
  private String getDiacriticlessValue() {
116
    return normalize( getValue().toString(), NFD )
117
      .replaceAll( "\\p{M}", "" );
118
  }
119
120
  /**
121
   * Returns true if this node is a leaf and its value equals the given text.
122
   *
123
   * @param s The text to compare against the node value.
124
   * @return true Node is a leaf and its value equals the given value.
125
   */
126
  private boolean valueEquals( final String s ) {
127
    return isLeaf() && getValue().equals( s );
128
  }
129
130
  /**
131
   * Returns true if this node is a leaf and its value contains the given text.
132
   *
133
   * @param s The text to compare against the node value.
134
   * @return true Node is a leaf and its value contains the given value.
135
   */
136
  private boolean valueContains( final String s ) {
137
    return isLeaf() && getDiacriticlessValue().contains( s );
138
  }
139
140
  /**
141
   * Returns true if this node is a leaf and its value contains the given text.
142
   *
143
   * @param s The text to compare against the node value.
144
   * @return true Node is a leaf and its value contains the given value.
145
   */
146
  private boolean valueContainsNoCase( final String s ) {
147
    return isLeaf() &&
148
      getDiacriticlessValue().toLowerCase().contains( s.toLowerCase() );
149
  }
150
151
  /**
152
   * Returns true if this node is a leaf and its value starts with the given
153
   * text.
154
   *
155
   * @param s The text to compare against the node value.
156
   * @return true Node is a leaf and its value starts with the given value.
157
   */
158
  private boolean valueStartsWith( final String s ) {
159
    return isLeaf() && getDiacriticlessValue().startsWith( s );
160
  }
161
162
  /**
163
   * Returns the path for this node, with nodes made distinct using the
164
   * separator character. This uses two loops: one for pushing nodes onto a
165
   * stack and one for popping them off to create the path in desired order.
166
   *
167
   * @return A non-null string, possibly empty.
168
   */
169
  public String toPath() {
170
    return new TreeItemMapper().toPath( getParent() );
171
  }
172
173
  /**
174
   * Answers whether there are any definitions in this tree.
175
   *
176
   * @return {@code true} when there are no definitions in the tree; {@code
177
   * false} when there is at least one definition present.
178
   */
179
  public boolean isEmpty() {
180
    return getChildren().isEmpty();
181
  }
182
}
1183
A src/main/java/com/keenwrite/editors/definition/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#toMap(TreeItem) for details on how this
26
   * class is used.
27
   */
28
  public RootTreeItem( final T value ) {
29
    super( value );
30
  }
31
}
132
A src/main/java/com/keenwrite/editors/definition/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
import javafx.scene.control.TreeView;
7
8
import java.util.HashMap;
9
import java.util.Iterator;
10
import java.util.Map;
11
import java.util.Stack;
12
13
import static com.keenwrite.constants.Constants.MAP_SIZE_DEFAULT;
14
15
/**
16
 * Given a {@link TreeItem}, this will generate a flat map with all the
17
 * values in the tree recursively interpolated. The application integrates
18
 * definition files as follows:
19
 * <ol>
20
 *   <li>Load YAML file into {@link JsonNode} hierarchy.</li>
21
 *   <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li>
22
 *   <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li>
23
 *   <li>Substitute flat map variables into document as required.</li>
24
 * </ol>
25
 *
26
 * <p>
27
 * This class is responsible for producing the interpolated flat map. This
28
 * allows dynamic edits of the {@link TreeView} to be displayed without
29
 * having to reload the definition file. Reloading the definition file would
30
 * work, but has a number of drawbacks.
31
 * </p>
32
 */
33
public final class TreeItemMapper {
34
  /**
35
   * Separates definition keys (e.g., the dots in {@code $root.node.var$}).
36
   */
37
  public static final String SEPARATOR = ".";
38
39
  /**
40
   * Default buffer length for keys ({@link StringBuilder} has 16 character
41
   * buffer) that should be large enough for most keys to avoid reallocating
42
   * memory to increase the {@link StringBuilder}'s buffer.
43
   */
44
  public static final int DEFAULT_KEY_LENGTH = 64;
45
46
  /**
47
   * In-order traversal of a {@link TreeItem} hierarchy, exposing each item
48
   * as a consecutive list.
49
   */
50
  private static final class TreeIterator
51
    implements Iterator<TreeItem<String>> {
52
    private final Stack<TreeItem<String>> mStack = new Stack<>();
53
54
    public TreeIterator( final TreeItem<String> root ) {
55
      if( root != null ) {
56
        mStack.push( root );
57
      }
58
    }
59
60
    @Override
61
    public boolean hasNext() {
62
      return !mStack.isEmpty();
63
    }
64
65
    @Override
66
    public TreeItem<String> next() {
67
      final TreeItem<String> next = mStack.pop();
68
      next.getChildren().forEach( mStack::push );
69
70
      return next;
71
    }
72
  }
73
74
  public TreeItemMapper() {
75
  }
76
77
  /**
78
   * Iterate over a given root node (at any level of the tree) and process each
79
   * leaf node into a flat map. Values must be interpolated separately.
80
   */
81
  public Map<String, String> toMap( final TreeItem<String> root ) {
82
    final var map = new HashMap<String, String>( MAP_SIZE_DEFAULT );
83
    final var iterator = new TreeIterator( root );
84
85
    iterator.forEachRemaining( item -> {
86
      if( item.isLeaf() ) {
87
        map.put( toPath( item.getParent() ), item.getValue() );
88
      }
89
    } );
90
91
    return map;
92
  }
93
94
  /**
95
   * For a given node, this will ascend the tree to generate a key name
96
   * that is associated with the leaf node's value.
97
   *
98
   * @param node Ascendants represent the key to this node's value.
99
   * @param <T>  Data type that the {@link TreeItem} contains.
100
   * @return The string representation of the node's unique key.
101
   */
102
  public <T> String toPath( TreeItem<T> node ) {
103
    assert node != null;
104
105
    final var key = new StringBuilder( DEFAULT_KEY_LENGTH );
106
    final var stack = new Stack<TreeItem<T>>();
107
108
    while( node != null && !(node instanceof RootTreeItem) ) {
109
      stack.push( node );
110
      node = node.getParent();
111
    }
112
113
    // Gets set at end of first iteration (to avoid an if condition).
114
    var separator = "";
115
116
    while( !stack.empty() ) {
117
      final T subkey = stack.pop().getValue();
118
      key.append( separator );
119
      key.append( subkey );
120
      separator = SEPARATOR;
121
    }
122
123
    return key.toString();
124
  }
125
}
1126
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.MINIMIZE_QUOTES;
17
import static com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.SPLIT_LINES;
18
import static com.keenwrite.events.StatusEvent.clue;
19
20
/**
21
 * Transforms a JsonNode hierarchy into a tree that can be displayed in a user
22
 * interface and vice-versa.
23
 */
24
public final class YamlTreeTransformer implements TreeTransformer {
25
  private static final YAMLFactory sFactory;
26
  private static final YAMLMapper sMapper;
27
28
  static {
29
    sFactory = new YAMLFactory();
30
    sFactory.configure( MINIMIZE_QUOTES, true );
31
    sFactory.configure( SPLIT_LINES, false );
32
    sMapper = new YAMLMapper( sFactory );
33
  }
34
35
  /**
36
   * Constructs a new instance that will use the given path to read the object
37
   * hierarchy from a data source.
38
   */
39
  public YamlTreeTransformer() {
40
  }
41
42
  @Override
43
  public String transform( final TreeItem<String> treeItem ) {
44
    try {
45
      final var root = sMapper.createObjectNode();
46
47
      // Iterate over the root item's children. The root item is used by the
48
      // application to ensure definitions can always be added to a tree, as
49
      // such it is not meant to be exported, only its children.
50
      for( final var child : treeItem.getChildren() ) {
51
        transform( child, root );
52
      }
53
54
      return sMapper.writeValueAsString( root );
55
    } catch( final Exception ex ) {
56
      clue( ex );
57
      throw new RuntimeException( ex );
58
    }
59
  }
60
61
  /**
62
   * Converts a YAML document to a {@link TreeItem} based on the document
63
   * keys.
64
   *
65
   * @param document The YAML document to convert to a hierarchy of
66
   *                 {@link TreeItem} instances.
67
   * @throws StackOverflowError If infinite recursion is encountered.
68
   */
69
  @Override
70
  public TreeItem<String> transform( final String document ) {
71
    final var jsonNode = toJson( document );
72
    final var rootItem = createTreeItem( "root" );
73
74
    transform( jsonNode, rootItem );
75
76
    return rootItem;
77
  }
78
79
  private JsonNode toJson( final String yaml ) {
80
    try {
81
      return new ObjectMapper( sFactory ).readTree( yaml );
82
    } catch( final Exception ex ) {
83
      // Ensure that a document root node exists.
84
      return new ObjectMapper().createObjectNode();
85
    }
86
  }
87
88
  /**
89
   * Recursive method to generate an object hierarchy that represents the
90
   * given {@link TreeItem} hierarchy.
91
   *
92
   * @param item The {@link TreeItem} to reproduce as an object hierarchy.
93
   * @param node The {@link ObjectNode} to update to reflect the
94
   *             {@link TreeItem} hierarchy.
95
   */
96
  private void transform( final TreeItem<String> item, ObjectNode node ) {
97
    final var children = item.getChildren();
98
99
    // If the current item has more than one non-leaf child, it's an
100
    // object node and must become a new nested object.
101
    if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) {
102
      node = node.putObject( item.getValue() );
103
    }
104
105
    for( final var child : children ) {
106
      if( child.isLeaf() ) {
107
        node.put( item.getValue(), child.getValue() );
108
      }
109
      else {
110
        transform( child, node );
111
      }
112
    }
113
  }
114
115
  /**
116
   * Iterate over a given root node (at any level of the tree) and adapt each
117
   * leaf node.
118
   *
119
   * @param node A JSON node (YAML node) to adapt.
120
   * @param item The tree item to use as the root when processing the node.
121
   * @throws StackOverflowError If infinite recursion is encountered.
122
   */
123
  private void transform( final JsonNode node, final TreeItem<String> item ) {
124
    node.fields().forEachRemaining( leaf -> transform( leaf, item ) );
125
  }
126
127
  /**
128
   * Recursively adapt each rootNode to a corresponding rootItem.
129
   *
130
   * @param node The node to adapt.
131
   * @param item The item to adapt using the node's key.
132
   * @throws StackOverflowError If infinite recursion is encountered.
133
   */
134
  private void transform(
135
    final Entry<String, JsonNode> node, final TreeItem<String> item ) {
136
    final var leafNode = node.getValue();
137
    final var key = node.getKey();
138
    final var leaf = createTreeItem( key );
139
140
    if( leafNode.isValueNode() ) {
141
      leaf.getChildren().add( createTreeItem( node.getValue().asText() ) );
142
    }
143
144
    item.getChildren().add( leaf );
145
146
    if( leafNode.isObject() ) {
147
      transform( leafNode, leaf );
148
    }
149
  }
150
151
  /**
152
   * Creates a new {@link TreeItem} that can be added to the {@link TreeView}.
153
   *
154
   * @param value The node's value.
155
   * @return A new {@link TreeItem}, never {@code null}.
156
   */
157
  private TreeItem<String> createTreeItem( final String value ) {
158
    return new DefinitionTreeItem<>( value );
159
  }
160
}
1161
A src/main/java/com/keenwrite/editors/definition/yaml/package-info.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
3
/**
4
 * This package contains classes that can parse YAML documents into a GUI
5
 * representation.
6
 */
7
package com.keenwrite.editors.definition.yaml;
18
A src/main/java/com/keenwrite/editors/markdown/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 final void setText( final String text ) {
73
    this.text = sanitize( text );
74
  }
75
76
  public final void setUrl( final String url ) {
77
    this.url = sanitize( url );
78
  }
79
80
  public final 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.Caret;
5
import com.keenwrite.constants.Constants;
6
import com.keenwrite.editors.TextEditor;
7
import com.keenwrite.io.MediaType;
8
import com.keenwrite.preferences.LocaleProperty;
9
import com.keenwrite.preferences.Workspace;
10
import com.keenwrite.spelling.impl.TextEditorSpeller;
11
import javafx.beans.binding.Bindings;
12
import javafx.beans.property.*;
13
import javafx.beans.value.ChangeListener;
14
import javafx.event.Event;
15
import javafx.scene.Node;
16
import javafx.scene.control.IndexRange;
17
import javafx.scene.input.KeyEvent;
18
import javafx.scene.layout.BorderPane;
19
import org.fxmisc.flowless.VirtualizedScrollPane;
20
import org.fxmisc.richtext.StyleClassedTextArea;
21
import org.fxmisc.richtext.model.StyleSpans;
22
import org.fxmisc.undo.UndoManager;
23
import org.fxmisc.wellbehaved.event.EventPattern;
24
import org.fxmisc.wellbehaved.event.Nodes;
25
26
import java.io.File;
27
import java.nio.charset.Charset;
28
import java.text.BreakIterator;
29
import java.util.*;
30
import java.util.function.Consumer;
31
import java.util.function.Supplier;
32
import java.util.regex.Pattern;
33
34
import static com.keenwrite.MainApp.keyDown;
35
import static com.keenwrite.Messages.get;
36
import static com.keenwrite.constants.Constants.*;
37
import static com.keenwrite.events.StatusEvent.clue;
38
import static com.keenwrite.events.TextEditorFocusEvent.fireTextEditorFocus;
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.WorkspaceKeys.*;
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.CONTROL_DOWN;
49
import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
50
import static org.apache.commons.lang3.StringUtils.stripEnd;
51
import static org.apache.commons.lang3.StringUtils.stripStart;
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
  /**
68
   * The text editor.
69
   */
70
  private final StyleClassedTextArea mTextArea =
71
    new StyleClassedTextArea( false );
72
73
  /**
74
   * Wraps the text editor in scrollbars.
75
   */
76
  private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
77
    new VirtualizedScrollPane<>( mTextArea );
78
79
  private final Workspace mWorkspace;
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 Workspace workspace ) {
111
    this( DOCUMENT_DEFAULT, workspace );
112
  }
113
114
  public MarkdownEditor( final File file, final Workspace workspace ) {
115
    mEncoding = open( mFile = file );
116
    mWorkspace = workspace;
117
118
    initTextArea( mTextArea );
119
    initStyle( mTextArea );
120
    initScrollPane( mScrollPane );
121
    initSpellchecker( mTextArea );
122
    initHotKeys();
123
    initUndoManager();
124
  }
125
126
  private void initTextArea( final StyleClassedTextArea textArea ) {
127
    textArea.setWrapText( true );
128
    textArea.requestFollowCaret();
129
    textArea.moveTo( 0 );
130
131
    textArea.textProperty().addListener( ( c, o, n ) -> {
132
      // Fire, regardless of whether the caret position has changed.
133
      mDirty.set( false );
134
135
      // Prevent a caret position change from raising the dirty bits.
136
      mDirty.set( true );
137
    } );
138
139
    textArea.caretPositionProperty().addListener( ( c, o, n ) -> {
140
      // Fire when the caret position has changed and the text has not.
141
      mDirty.set( true );
142
      mDirty.set( false );
143
    } );
144
145
    textArea.focusedProperty().addListener( ( c, o, n ) -> {
146
      if( n != null && n ) {
147
        fireTextEditorFocus( this );
148
      }
149
    } );
150
  }
151
152
  private void initStyle( final StyleClassedTextArea textArea ) {
153
    textArea.getStyleClass().add( "markdown" );
154
155
    final var stylesheets = textArea.getStylesheets();
156
    stylesheets.add( getStylesheetPath( getLocale() ) );
157
158
    localeProperty().addListener( ( c, o, n ) -> {
159
      if( n != null ) {
160
        stylesheets.clear();
161
        stylesheets.add( getStylesheetPath( getLocale() ) );
162
      }
163
    } );
164
165
    fontNameProperty().addListener(
166
      ( c, o, n ) ->
167
        setFont( mTextArea, getFontName(), getFontSize() )
168
    );
169
170
    fontSizeProperty().addListener(
171
      ( c, o, n ) ->
172
        setFont( mTextArea, getFontName(), getFontSize() )
173
    );
174
175
    setFont( mTextArea, getFontName(), getFontSize() );
176
  }
177
178
  private void initScrollPane(
179
    final VirtualizedScrollPane<StyleClassedTextArea> scrollpane ) {
180
    scrollpane.setVbarPolicy( ALWAYS );
181
    setCenter( scrollpane );
182
  }
183
184
  private void initSpellchecker( final StyleClassedTextArea textarea ) {
185
    final var speller = new TextEditorSpeller();
186
    speller.checkDocument( textarea );
187
    speller.checkParagraphs( textarea );
188
  }
189
190
  private void initHotKeys() {
191
    addEventListener( keyPressed( ENTER ), this::onEnterPressed );
192
    addEventListener( keyPressed( X, CONTROL_DOWN ), this::cut );
193
    addEventListener( keyPressed( TAB ), this::tab );
194
    addEventListener( keyPressed( TAB, SHIFT_DOWN ), this::untab );
195
    addEventListener( keyPressed( INSERT ), this::onInsertPressed );
196
  }
197
198
  private void initUndoManager() {
199
    final var undoManager = getUndoManager();
200
    final var markedPosition = undoManager.atMarkedPositionProperty();
201
202
    undoManager.forgetHistory();
203
    undoManager.mark();
204
    mModified.bind( Bindings.not( markedPosition ) );
205
  }
206
207
  @Override
208
  public void moveTo( final int offset ) {
209
    assert 0 <= offset && offset <= mTextArea.getLength();
210
    mTextArea.moveTo( offset );
211
    mTextArea.requestFollowCaret();
212
  }
213
214
  /**
215
   * Delegate the focus request to the text area itself.
216
   */
217
  @Override
218
  public void requestFocus() {
219
    mTextArea.requestFocus();
220
  }
221
222
  @Override
223
  public void setText( final String text ) {
224
    mTextArea.clear();
225
    mTextArea.appendText( text );
226
    mTextArea.getUndoManager().mark();
227
  }
228
229
  @Override
230
  public String getText() {
231
    return mTextArea.getText();
232
  }
233
234
  @Override
235
  public Charset getEncoding() {
236
    return mEncoding;
237
  }
238
239
  @Override
240
  public File getFile() {
241
    return mFile;
242
  }
243
244
  @Override
245
  public void rename( final File file ) {
246
    mFile = file;
247
  }
248
249
  @Override
250
  public void undo() {
251
    final var manager = getUndoManager();
252
    xxdo( manager::isUndoAvailable, manager::undo, "Main.status.error.undo" );
253
  }
254
255
  @Override
256
  public void redo() {
257
    final var manager = getUndoManager();
258
    xxdo( manager::isRedoAvailable, manager::redo, "Main.status.error.redo" );
259
  }
260
261
  /**
262
   * Performs an undo or redo action, if possible, otherwise displays an error
263
   * message to the user.
264
   *
265
   * @param ready  Answers whether the action can be executed.
266
   * @param action The action to execute.
267
   * @param key    The informational message key having a value to display if
268
   *               the {@link Supplier} is not ready.
269
   */
270
  private void xxdo(
271
    final Supplier<Boolean> ready, final Runnable action, final String key ) {
272
    if( ready.get() ) {
273
      action.run();
274
    }
275
    else {
276
      clue( key );
277
    }
278
  }
279
280
  @Override
281
  public void cut() {
282
    final var selected = mTextArea.getSelectedText();
283
284
    // Emulate selecting the current line by firing Home then Shift+Down Arrow.
285
    if( selected == null || selected.isEmpty() ) {
286
      // Note: mTextArea.selectLine() does not select empty lines.
287
      mTextArea.fireEvent( keyDown( HOME, false ) );
288
      mTextArea.fireEvent( keyDown( DOWN, true ) );
289
    }
290
291
    mTextArea.cut();
292
  }
293
294
  @Override
295
  public void copy() {
296
    mTextArea.copy();
297
  }
298
299
  @Override
300
  public void paste() {
301
    mTextArea.paste();
302
  }
303
304
  @Override
305
  public void selectAll() {
306
    mTextArea.selectAll();
307
  }
308
309
  @Override
310
  public void bold() {
311
    enwrap( "**" );
312
  }
313
314
  @Override
315
  public void italic() {
316
    enwrap( "*" );
317
  }
318
319
  @Override
320
  public void monospace() {
321
    enwrap( "`" );
322
  }
323
324
  @Override
325
  public void superscript() {
326
    enwrap( "^" );
327
  }
328
329
  @Override
330
  public void subscript() {
331
    enwrap( "~" );
332
  }
333
334
  @Override
335
  public void strikethrough() {
336
    enwrap( "~~" );
337
  }
338
339
  @Override
340
  public void blockquote() {
341
    block( "> " );
342
  }
343
344
  @Override
345
  public void code() {
346
    enwrap( "`" );
347
  }
348
349
  @Override
350
  public void fencedCodeBlock() {
351
    enwrap( "\n\n```\n", "\n```\n\n" );
352
  }
353
354
  @Override
355
  public void heading( final int level ) {
356
    final var hashes = new String( new char[ level ] ).replace( "\0", "#" );
357
    block( format( "%s ", hashes ) );
358
  }
359
360
  @Override
361
  public void unorderedList() {
362
    block( "* " );
363
  }
364
365
  @Override
366
  public void orderedList() {
367
    block( "1. " );
368
  }
369
370
  @Override
371
  public void horizontalRule() {
372
    block( format( "---%n%n" ) );
373
  }
374
375
  @Override
376
  public Node getNode() {
377
    return this;
378
  }
379
380
  @Override
381
  public ReadOnlyBooleanProperty modifiedProperty() {
382
    return mModified;
383
  }
384
385
  @Override
386
  public void clearModifiedProperty() {
387
    getUndoManager().mark();
388
  }
389
390
  @Override
391
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
392
    return mScrollPane;
393
  }
394
395
  @Override
396
  public StyleClassedTextArea getTextArea() {
397
    return mTextArea;
398
  }
399
400
  private final Map<String, IndexRange> mStyles = new HashMap<>();
401
402
  @Override
403
  public void stylize( final IndexRange range, final String style ) {
404
    final var began = range.getStart();
405
    final var ended = range.getEnd() + 1;
406
407
    assert 0 <= began && began <= ended;
408
    assert style != null;
409
410
    // TODO: Ensure spell check and find highlights can coexist.
411
//    final var spans = mTextArea.getStyleSpans( range );
412
//    System.out.println( "SPANS: " + spans );
413
414
//    final var spans = mTextArea.getStyleSpans( range );
415
//    mTextArea.setStyleSpans( began, merge( spans, range.getLength(), style
416
//    ) );
417
418
//    final var builder = new StyleSpansBuilder<Collection<String>>();
419
//    builder.add( singleton( style ), range.getLength() + 1 );
420
//    mTextArea.setStyleSpans( began, builder.create() );
421
422
//    final var s = mTextArea.getStyleSpans( began, ended );
423
//    System.out.println( "STYLES: " +s );
424
425
    mStyles.put( style, range );
426
    mTextArea.setStyleClass( began, ended, style );
427
428
    // Ensure that whenever the user interacts with the text that the found
429
    // word will have its highlighting removed. The handler removes itself.
430
    // This won't remove the highlighting if the caret position moves by mouse.
431
    final var handler = mTextArea.getOnKeyPressed();
432
    mTextArea.setOnKeyPressed( ( event ) -> {
433
      mTextArea.setOnKeyPressed( handler );
434
      unstylize( style );
435
    } );
436
437
    //mTextArea.setStyleSpans(began, ended, s);
438
  }
439
440
  private static StyleSpans<Collection<String>> merge(
441
    StyleSpans<Collection<String>> spans, int len, String style ) {
442
    spans = spans.overlay(
443
      singleton( singletonList( style ), len ),
444
      ( bottomSpan, list ) -> {
445
        final List<String> l =
446
          new ArrayList<>( bottomSpan.size() + list.size() );
447
        l.addAll( bottomSpan );
448
        l.addAll( list );
449
        return l;
450
      } );
451
452
    return spans;
453
  }
454
455
  @Override
456
  public void unstylize( final String style ) {
457
    final var indexes = mStyles.remove( style );
458
    if( indexes != null ) {
459
      mTextArea.clearStyle( indexes.getStart(), indexes.getEnd() + 1 );
460
    }
461
  }
462
463
  @Override
464
  public Caret getCaret() {
465
    return mCaret;
466
  }
467
468
  private Caret createCaret( final StyleClassedTextArea editor ) {
469
    return Caret
470
      .builder()
471
      .with( Caret.Mutator::setEditor, editor )
472
      .build();
473
  }
474
475
  /**
476
   * This method adds listeners to editor events.
477
   *
478
   * @param <T>      The event type.
479
   * @param <U>      The consumer type for the given event type.
480
   * @param event    The event of interest.
481
   * @param consumer The method to call when the event happens.
482
   */
483
  public <T extends Event, U extends T> void addEventListener(
484
    final EventPattern<? super T, ? extends U> event,
485
    final Consumer<? super U> consumer ) {
486
    Nodes.addInputMap( mTextArea, consume( event, consumer ) );
487
  }
488
489
  private void onEnterPressed( final KeyEvent ignored ) {
490
    final var currentLine = getCaretParagraph();
491
    final var matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
492
493
    // By default, insert a new line by itself.
494
    String newText = NEWLINE;
495
496
    // If the pattern was matched then determine what block type to continue.
497
    if( matcher.matches() ) {
498
      if( matcher.group( 2 ).isEmpty() ) {
499
        final var pos = mTextArea.getCaretPosition();
500
        mTextArea.selectRange( pos - currentLine.length(), pos );
501
      }
502
      else {
503
        // Indent the new line with the same whitespace characters and
504
        // list markers as current line. This ensures that the indentation
505
        // is propagated.
506
        newText = newText.concat( matcher.group( 1 ) );
507
      }
508
    }
509
510
    mTextArea.replaceSelection( newText );
511
  }
512
513
  /**
514
   * TODO: 105 - Insert key toggle overwrite (typeover) mode
515
   *
516
   * @param ignored Unused.
517
   */
518
  private void onInsertPressed( final KeyEvent ignored ) {
519
  }
520
521
  private void cut( final KeyEvent event ) {
522
    cut();
523
  }
524
525
  private void tab( final KeyEvent event ) {
526
    final var range = mTextArea.selectionProperty().getValue();
527
    final var sb = new StringBuilder( 1024 );
528
529
    if( range.getLength() > 0 ) {
530
      final var selection = mTextArea.getSelectedText();
531
532
      selection.lines().forEach(
533
        ( l ) -> sb.append( "\t" ).append( l ).append( NEWLINE )
534
      );
535
    }
536
    else {
537
      sb.append( "\t" );
538
    }
539
540
    mTextArea.replaceSelection( sb.toString() );
541
  }
542
543
  private void untab( final KeyEvent event ) {
544
    final var range = mTextArea.selectionProperty().getValue();
545
546
    if( range.getLength() > 0 ) {
547
      final var selection = mTextArea.getSelectedText();
548
      final var sb = new StringBuilder( selection.length() );
549
550
      selection.lines().forEach(
551
        ( l ) -> sb.append( l.startsWith( "\t" ) ? l.substring( 1 ) : l )
552
                   .append( NEWLINE )
553
      );
554
555
      mTextArea.replaceSelection( sb.toString() );
556
    }
557
    else {
558
      final var p = getCaretParagraph();
559
560
      if( p.startsWith( "\t" ) ) {
561
        mTextArea.selectParagraph();
562
        mTextArea.replaceSelection( p.substring( 1 ) );
563
      }
564
    }
565
  }
566
567
  /**
568
   * Observers may listen for changes to the property returned from this method
569
   * to receive notifications when either the text or caret have changed. This
570
   * should not be used to track whether the text has been modified.
571
   */
572
  public void addDirtyListener( ChangeListener<Boolean> listener ) {
573
    mDirty.addListener( listener );
574
  }
575
576
  /**
577
   * Surrounds the selected text or word under the caret in Markdown markup.
578
   *
579
   * @param token The beginning and ending token for enclosing the text.
580
   */
581
  private void enwrap( final String token ) {
582
    enwrap( token, token );
583
  }
584
585
  /**
586
   * Surrounds the selected text or word under the caret in Markdown markup.
587
   *
588
   * @param began The beginning token for enclosing the text.
589
   * @param ended The ending token for enclosing the text.
590
   */
591
  private void enwrap( final String began, String ended ) {
592
    // Ensure selected text takes precedence over the word at caret position.
593
    final var selected = mTextArea.selectionProperty().getValue();
594
    final var range = selected.getLength() == 0
595
      ? getCaretWord()
596
      : selected;
597
    String text = mTextArea.getText( range );
598
599
    int length = range.getLength();
600
    text = stripStart( text, null );
601
    final int beganIndex = range.getStart() + (length - text.length());
602
603
    length = text.length();
604
    text = stripEnd( text, null );
605
    final int endedIndex = range.getEnd() - (length - text.length());
606
607
    mTextArea.replaceText( beganIndex, endedIndex, began + text + ended );
608
  }
609
610
  /**
611
   * Inserts the given block-level markup at the current caret position
612
   * within the document. This will prepend two blank lines to ensure that
613
   * the block element begins at the start of a new line.
614
   *
615
   * @param markup The text to insert at the caret.
616
   */
617
  private void block( final String markup ) {
618
    final int pos = mTextArea.getCaretPosition();
619
    mTextArea.insertText( pos, format( "%n%n%s", markup ) );
620
  }
621
622
  /**
623
   * Returns the caret position within the current paragraph.
624
   *
625
   * @return A value from 0 to the length of the current paragraph.
626
   */
627
  private int getCaretColumn() {
628
    return mTextArea.getCaretColumn();
629
  }
630
631
  @Override
632
  public IndexRange getCaretWord() {
633
    final var paragraph = getCaretParagraph();
634
    final var length = paragraph.length();
635
    final var column = getCaretColumn();
636
637
    var began = column;
638
    var ended = column;
639
640
    while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
641
      began--;
642
    }
643
644
    while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
645
      ended++;
646
    }
647
648
    final var iterator = BreakIterator.getWordInstance();
649
    iterator.setText( paragraph );
650
651
    while( began < length && iterator.isBoundary( began + 1 ) ) {
652
      began++;
653
    }
654
655
    while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
656
      ended--;
657
    }
658
659
    final var offset = getCaretDocumentOffset( column );
660
661
    return IndexRange.normalize( began + offset, ended + offset );
662
  }
663
664
  private int getCaretDocumentOffset( final int column ) {
665
    return mTextArea.getCaretPosition() - column;
666
  }
667
668
  /**
669
   * Returns the index of the paragraph where the caret resides.
670
   *
671
   * @return A number greater than or equal to 0.
672
   */
673
  private int getCurrentParagraph() {
674
    return mTextArea.getCurrentParagraph();
675
  }
676
677
  /**
678
   * Returns the text for the paragraph that contains the caret.
679
   *
680
   * @return A non-null string, possibly empty.
681
   */
682
  private String getCaretParagraph() {
683
    return getText( getCurrentParagraph() );
684
  }
685
686
  @Override
687
  public String getText( final int paragraph ) {
688
    return mTextArea.getText( paragraph );
689
  }
690
691
  @Override
692
  public String getText( final IndexRange indexes )
693
    throws IndexOutOfBoundsException {
694
    return mTextArea.getText( indexes.getStart(), indexes.getEnd() );
695
  }
696
697
  @Override
698
  public void replaceText( final IndexRange indexes, final String s ) {
699
    mTextArea.replaceText( indexes, s );
700
  }
701
702
  private UndoManager<?> getUndoManager() {
703
    return mTextArea.getUndoManager();
704
  }
705
706
  /**
707
   * Returns the path to a {@link Locale}-specific stylesheet.
708
   *
709
   * @return A non-null string to inject into the HTML document head.
710
   */
711
  private static String getStylesheetPath( final Locale locale ) {
712
    return get(
713
      sSettings.getSetting( STYLESHEET_MARKDOWN_LOCALE, "" ),
714
      locale.getLanguage(),
715
      locale.getScript(),
716
      locale.getCountry()
717
    );
718
  }
719
720
  private Locale getLocale() {
721
    return localeProperty().toLocale();
722
  }
723
724
  private LocaleProperty localeProperty() {
725
    return mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
726
  }
727
728
  /**
729
   * Sets the font family name and font size at the same time. When the
730
   * workspace is loaded, the default font values are changed, which results
731
   * in this method being called.
732
   *
733
   * @param area   Change the font settings for this text area.
734
   * @param name   New font family name to apply.
735
   * @param points New font size to apply (in points, not pixels).
736
   */
737
  private void setFont(
738
    final StyleClassedTextArea area, final String name, final double points ) {
739
    runLater( () -> area.setStyle(
740
      format(
741
        "-fx-font-family:'%s';-fx-font-size:%spx;", name, toPixels( points )
742
      )
743
    ) );
744
  }
745
746
  private String getFontName() {
747
    return fontNameProperty().get();
748
  }
749
750
  private StringProperty fontNameProperty() {
751
    return mWorkspace.stringProperty( KEY_UI_FONT_EDITOR_NAME );
752
  }
753
754
  private double getFontSize() {
755
    return fontSizeProperty().get();
756
  }
757
758
  private DoubleProperty fontSizeProperty() {
759
    return mWorkspace.doubleProperty( KEY_UI_FONT_EDITOR_SIZE );
760
  }
761
762
  /**
763
   * Answers whether the given resource is of compatible {@link MediaType}s.
764
   *
765
   * @param mediaType The {@link MediaType} to compare.
766
   * @return {@code true} if the given {@link MediaType} is suitable for
767
   * editing with this type of editor.
768
   */
769
  @Override
770
  public boolean supports( final MediaType mediaType ) {
771
    return isMediaType( mediaType ) ||
772
      mediaType == TEXT_MARKDOWN ||
773
      mediaType == TEXT_R_MARKDOWN;
774
  }
775
}
1776
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 fire() {
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/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}.
9
 */
10
public class CaretNavigationEvent implements AppEvent {
11
  /**
12
   * Absolute document offset.
13
   */
14
  private final int mOffset;
15
16
  private CaretNavigationEvent( final int offset ) {
17
    mOffset = offset;
18
  }
19
20
  /**
21
   * Publishes an event that requests moving the caret to the given offset.
22
   *
23
   * @param offset Move the caret to this document offset.
24
   */
25
  public static void fireCaretNavigationEvent( final int offset ) {
26
    new CaretNavigationEvent( offset ).fire();
27
  }
28
29
  public int getOffset() {
30
    return mOffset;
31
  }
32
}
133
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 fireDocumentChangedEvent( 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 ).fire();
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 fireExportFailedEvent() {
11
    new ExportFailedEvent().fire();
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 fireFileOpenEvent( final URI uri ) {
24
    new FileOpenEvent( uri ).fire();
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 fireHyperlinkOpenEvent( final URI uri )
25
    throws IOException {
26
    new HyperlinkOpenEvent( uri ).fire();
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 fireHyperlinkOpenEvent( final String uri ) {
35
    try {
36
      fireHyperlinkOpenEvent( 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/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 ).fire();
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 fireNewHeadingEvent(
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 ).fire();
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 ).fire();
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.MainApp;
5
6
import java.util.List;
7
import java.util.stream.Collectors;
8
9
import static com.keenwrite.Messages.get;
10
import static com.keenwrite.constants.Constants.NEWLINE;
11
import static com.keenwrite.constants.Constants.STATUS_BAR_OK;
12
import static java.lang.String.format;
13
import static java.lang.String.join;
14
import static java.util.Arrays.stream;
15
16
/**
17
 * Collates information about an application issue. The issues can be
18
 * exceptions, state problems, parsing errors, and so forth.
19
 */
20
public final class StatusEvent implements AppEvent {
21
  private static final String PACKAGE_NAME = MainApp.class.getPackageName();
22
23
  private static final String ENGLISHIFY =
24
    "(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])";
25
26
  /**
27
   * Detailed information about a problem.
28
   */
29
  private final String mMessage;
30
31
  /**
32
   * Provides stack trace information that isolates the cause.
33
   */
34
  private final Throwable mProblem;
35
36
  /**
37
   * Constructs a new event that contains a problem description to help the
38
   * user resolve an issue encountered while using the application.
39
   *
40
   * @param message The human-readable message, typically displayed on-screen.
41
   */
42
  public StatusEvent( final String message ) {
43
    assert message != null;
44
    mMessage = message;
45
    mProblem = null;
46
  }
47
48
  public StatusEvent( final Throwable problem ) {
49
    this( "", problem );
50
  }
51
52
  public StatusEvent( final String message, final Throwable problem ) {
53
    assert message != null;
54
    assert problem != null;
55
    mMessage = message;
56
    mProblem = problem;
57
  }
58
59
  /**
60
   * Returns the stack trace information for the issue encountered. This is
61
   * optional because usually a status message isn't an application error.
62
   *
63
   * @return Optional stack trace to pin-point the problem area in the code.
64
   */
65
  public String getProblem() {
66
    // 256 is arbitrary; stack traces shouldn't be much larger.
67
    final var sb = new StringBuilder( 256 );
68
    final var trace = mProblem;
69
70
    if( trace != null ) {
71
      stream( trace.getStackTrace() )
72
        .takeWhile( StatusEvent::filter )
73
        .limit( 10 )
74
        .collect( Collectors.toList() )
75
        .forEach( e -> sb.append( e.toString() ).append( NEWLINE ) );
76
    }
77
78
    return sb.toString();
79
  }
80
81
  @Override
82
  public String toString() {
83
    return format( "%s%s%s",
84
                   mMessage,
85
                   mMessage.isBlank() ? "" : " ",
86
                   mProblem == null ? "" : toEnglish( mProblem ) );
87
  }
88
89
  private static boolean filter( final StackTraceElement e ) {
90
    final var clazz = e.getClassName();
91
    return clazz.contains( PACKAGE_NAME ) ||
92
      clazz.contains( "org.renjin." ) ||
93
      clazz.contains( "sun." ) ||
94
      clazz.contains( "flexmark." ) ||
95
      clazz.contains( "java." );
96
  }
97
98
  /**
99
   * Separates the exception class name from TitleCase into lowercase,
100
   * space-separated words. This makes the exception look a little more like
101
   * English. Any {@link RuntimeException} instances passed into this method
102
   * will have the cause extracted, if possible.
103
   *
104
   * @param problem The exception that triggered the status event change.
105
   * @return A human-readable message with the exception name and the
106
   * exception's message.
107
   */
108
  private static String toEnglish( Throwable problem ) {
109
    assert problem != null;
110
111
    // Subclasses of RuntimeException must be subject to Englishification.
112
    if( problem.getClass().equals( RuntimeException.class ) ) {
113
      final var cause = problem.getCause();
114
      return cause == null ? problem.getMessage() : cause.getMessage();
115
    }
116
117
    final var className = problem.getClass().getSimpleName();
118
    final var words = join( " ", className.split( ENGLISHIFY ) );
119
    return format( "(%s: %s)", words.toLowerCase(), problem.getMessage() );
120
  }
121
122
  /**
123
   * Returns the message used to construct the event.
124
   *
125
   * @return The message for this event.
126
   */
127
  public String getMessage() {
128
    return mMessage;
129
  }
130
131
  /**
132
   * Resets the status bar to a default message. Indicates that there are no
133
   * issues to bring to the user's attention.
134
   */
135
  public static void clue() {
136
    fireStatusEvent( get( STATUS_BAR_OK, "OK" ) );
137
  }
138
139
  /**
140
   * Notifies listeners of a series of messages. This is useful when providing
141
   * users feedback of how third-party executables have failed.
142
   *
143
   * @param messages The lines of text to display.
144
   */
145
  public static void clue( final List<String> messages ) {
146
    messages.forEach( StatusEvent::fireStatusEvent );
147
  }
148
149
  /**
150
   * Notifies listeners of an error.
151
   *
152
   * @param key The message bundle key to look up.
153
   * @param t   The exception that caused the error.
154
   */
155
  public static void clue( final String key, final Throwable t ) {
156
    fireStatusEvent( get( key ), t );
157
  }
158
159
  /**
160
   * Notifies listeners of a custom message.
161
   *
162
   * @param key  The property key having a value to populate with arguments.
163
   * @param args The placeholder values to substitute into the key's value.
164
   */
165
  public static void clue( final String key, final Object... args ) {
166
    fireStatusEvent( get( key, args ) );
167
  }
168
169
  /**
170
   * Notifies listeners of an exception occurs that warrants the user's
171
   * attention.
172
   *
173
   * @param problem The exception with a message to display to the user.
174
   */
175
  public static void clue( final Throwable problem ) {
176
    fireStatusEvent( problem );
177
  }
178
179
  private static void fireStatusEvent( final String message ) {
180
    new StatusEvent( message ).fire();
181
  }
182
183
  private static void fireStatusEvent( final Throwable problem ) {
184
    new StatusEvent( problem ).fire();
185
  }
186
187
  private static void fireStatusEvent(
188
    final String message, final Throwable problem ) {
189
    new StatusEvent( message, problem ).fire();
190
  }
191
}
1192
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 fireTextDefinitionFocus( final TextDefinition editor ) {
18
    new TextDefinitionFocusEvent( editor ).fire();
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
public class TextEditorFocusEvent extends FocusEvent<TextEditor> {
7
  protected TextEditorFocusEvent( final TextEditor editor ) {
8
    super( editor );
9
  }
10
11
  /**
12
   * When the {@link TextEditor} has focus, fire an event so that subscribers
13
   * may perform an action---such as parsing and rendering the contents.
14
   *
15
   * @param editor The instance of editor that has gained input focus.
16
   */
17
  public static void fireTextEditorFocus( final TextEditor editor ) {
18
    new TextEditorFocusEvent( editor ).fire();
19
  }
20
}
121
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 fireWordCountEvent( final int count ) {
23
    new WordCountEvent( count ).fire();
24
  }
25
26
  public int getCount() {
27
    return mCount;
28
  }
29
}
130
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/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 succumb 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
   * @return {@code true} if this contained the given listener.
172
   */
173
  public boolean removeListener( final FileModifiedListener listener ) {
174
    return mListeners.remove( listener );
175
  }
176
177
  /**
178
   * Shuts down the file watch service and clears both watchers and listeners.
179
   *
180
   * @throws IOException Could not close the watch service.
181
   */
182
  public void stop() throws IOException {
183
    mRunning = false;
184
185
    for( final var file : mWatched.keySet() ) {
186
      cancel( file );
187
    }
188
189
    mWatched.clear();
190
    mListeners.clear();
191
    mWatchService.close();
192
  }
193
194
  /**
195
   * Returns the directory containing the given {@link File} instance.
196
   *
197
   * @param file The {@link File}'s containing directory to watch.
198
   * @return The {@link Path} to the {@link File}'s directory.
199
   * @throws IllegalArgumentException The {@link File} has no parent directory.
200
   */
201
  private Path getParentDirectory( final File file ) {
202
    assert file != null;
203
    assert !file.isDirectory();
204
205
    final var directory = file.getParentFile();
206
207
    if( directory == null ) {
208
      throw new IllegalArgumentException( file.getAbsolutePath() );
209
    }
210
211
    return directory.toPath();
212
  }
213
214
  private WatchService createWatchService() {
215
    try {
216
      return getDefault().newWatchService();
217
    } catch( final Exception ex ) {
218
      // Create a fallback that allows the class to be instantiated and used
219
      // without without preventing the application from launching.
220
      return new PollingWatchService();
221
    }
222
  }
223
}
1224
A src/main/java/com/keenwrite/io/HttpFacade.java
1
package com.keenwrite.io;
2
3
import java.io.BufferedInputStream;
4
import java.io.Closeable;
5
import java.io.IOException;
6
import java.io.InputStream;
7
import java.net.HttpURLConnection;
8
import java.net.URI;
9
import java.net.URL;
10
import java.net.URLConnection;
11
import java.util.zip.GZIPInputStream;
12
13
import static com.keenwrite.events.StatusEvent.clue;
14
import static java.lang.System.getProperty;
15
import static java.lang.System.setProperty;
16
import static java.net.HttpURLConnection.HTTP_OK;
17
import static java.net.HttpURLConnection.setFollowRedirects;
18
19
/**
20
 * Responsible for making HTTP requests, a thin wrapper around the
21
 * {@link URLConnection} class. This will attempt to use compression.
22
 * <p>
23
 * This class must be used within a try-with-resources block to ensure all
24
 * resources are released, even if only calling {@link Response#getMediaType()}.
25
 * </p>
26
 */
27
public class HttpFacade {
28
  static {
29
    setProperty( "http.keepAlive", "false" );
30
    setFollowRedirects( true );
31
  }
32
33
  /**
34
   * Sends an HTTP GET request to a server.
35
   *
36
   * @param url The remote resource to fetch.
37
   * @return The server response.
38
   */
39
  public static Response httpGet( final URL url ) throws Exception {
40
    return new Response( url );
41
  }
42
43
  /**
44
   * Convenience method to send an HTTP GET request to a server.
45
   *
46
   * @param uri The remote resource to fetch.
47
   * @return The server response.
48
   * @see #httpGet(URL)
49
   */
50
  public static Response httpGet( final URI uri ) throws Exception {
51
    return httpGet( uri.toURL() );
52
  }
53
54
  /**
55
   * Convenience method to send an HTTP GET request to a server.
56
   *
57
   * @param url The remote resource to fetch.
58
   * @return The server response.
59
   * @see #httpGet(URL)
60
   */
61
  public static Response httpGet( final String url ) throws Exception {
62
    return httpGet( new URL( url ) );
63
  }
64
65
  /**
66
   * Callers are responsible for closing the response.
67
   */
68
  public static final class Response implements Closeable {
69
    private final HttpURLConnection mConn;
70
    private final BufferedInputStream mStream;
71
72
    private Response( final URL url ) throws IOException {
73
      assert url != null;
74
75
      clue( "Main.status.image.request.init" );
76
77
      if( url.openConnection() instanceof HttpURLConnection conn ) {
78
        conn.setUseCaches( false );
79
        conn.setInstanceFollowRedirects( true );
80
        conn.setRequestProperty( "Accept-Encoding", "gzip" );
81
        conn.setRequestProperty( "User-Agent", getProperty( "http.agent" ) );
82
        conn.setRequestMethod( "GET" );
83
        conn.setConnectTimeout( 15000 );
84
        conn.setRequestProperty( "connection", "close" );
85
        conn.connect();
86
87
        clue( "Main.status.image.request.fetch", url.getHost() );
88
89
        final var code = conn.getResponseCode();
90
91
        // Even though there are other "okay" error codes, tell the user when
92
        // a resource has changed in any unexpected way.
93
        if( code != HTTP_OK ) {
94
          throw new IOException( url + " [HTTP " + code + "]" );
95
        }
96
97
        mConn = conn;
98
        mStream = openBufferedInputStream();
99
      }
100
      else {
101
        throw new UnsupportedOperationException( url.toString() );
102
      }
103
    }
104
105
    /**
106
     * Returns the {@link MediaType} based on the resulting HTTP content type
107
     * provided by the server. If the content type from the server is not
108
     * found, this will probe the first several bytes to determine the type.
109
     *
110
     * @return The stream's IANA-defined {@link MediaType}.
111
     */
112
    public MediaType getMediaType() throws IOException {
113
      final var contentType = mConn.getContentType();
114
      var mediaType = MediaType.valueFrom( contentType );
115
116
      if( mediaType.isUndefined() ) {
117
        mediaType = MediaTypeSniffer.getMediaType( mStream );
118
      }
119
120
      clue( "Main.status.image.request.success", mediaType );
121
      return mediaType;
122
    }
123
124
    /**
125
     * Returns the stream opened using an HTTP connection, decompressing if
126
     * the server supports gzip compression. The caller must close the stream
127
     * by calling {@link #close()} on this object.
128
     *
129
     * @return The stream representing the content at the URL used to
130
     * construct the {@link HttpFacade}.
131
     */
132
    public InputStream getInputStream() throws IOException {
133
      return mStream;
134
    }
135
136
    /**
137
     * This will disconnect the HTTP request and close the associated stream.
138
     */
139
    @Override
140
    public void close() {
141
      mConn.disconnect();
142
    }
143
144
    /**
145
     * Opens the connection for reading. It is an error to call this more than
146
     * once. This may use gzip compression. A {@link BufferedInputStream} is
147
     * returned to allow peeking at the stream when checking the content
148
     * type.
149
     *
150
     * @return The {@link InputStream} containing content from an HTTP request.
151
     * @throws IOException Could not open the stream.
152
     */
153
    private BufferedInputStream openBufferedInputStream() throws IOException {
154
      final var encoding = mConn.getContentEncoding();
155
      final var is = mConn.getInputStream();
156
157
      return new BufferedInputStream(
158
        "gzip".equalsIgnoreCase( encoding ) ? new GZIPInputStream( is ) : is );
159
    }
160
  }
161
}
1162
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.nio.file.Path;
7
8
import static com.keenwrite.io.MediaType.TypeName.*;
9
import static com.keenwrite.io.MediaTypeExtension.getMediaType;
10
import static java.io.File.createTempFile;
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
public enum MediaType {
21
  APP_DOCUMENT_OUTLINE( APPLICATION, "x-document-outline" ),
22
  APP_DOCUMENT_STATISTICS( APPLICATION, "x-document-statistics" ),
23
  APP_FILE_MANAGER( APPLICATION, "x-file-manager" ),
24
25
  APP_ACAD( APPLICATION, "acad" ),
26
  APP_JAVA_OBJECT( APPLICATION, "x-java-serialized-object" ),
27
  APP_JAVA( APPLICATION, "java" ),
28
  APP_PS( APPLICATION, "postscript" ),
29
  APP_EPS( APPLICATION, "eps" ),
30
  APP_PDF( APPLICATION, "pdf" ),
31
  APP_ZIP( APPLICATION, "zip" ),
32
33
  /*
34
   * Standard font types.
35
   */
36
  FONT_OTF( "otf" ),
37
  FONT_TTF( "ttf" ),
38
39
  /*
40
   * Standard image types.
41
   */
42
  IMAGE_APNG( "apng" ),
43
  IMAGE_ACES( "aces" ),
44
  IMAGE_AVCI( "avci" ),
45
  IMAGE_AVCS( "avcs" ),
46
  IMAGE_BMP( "bmp" ),
47
  IMAGE_CGM( "cgm" ),
48
  IMAGE_DICOM_RLE( "dicom_rle" ),
49
  IMAGE_EMF( "emf" ),
50
  IMAGE_EXAMPLE( "example" ),
51
  IMAGE_FITS( "fits" ),
52
  IMAGE_G3FAX( "g3fax" ),
53
  IMAGE_GIF( "gif" ),
54
  IMAGE_HEIC( "heic" ),
55
  IMAGE_HEIF( "heif" ),
56
  IMAGE_HEJ2K( "hej2k" ),
57
  IMAGE_HSJ2( "hsj2" ),
58
  IMAGE_X_ICON( "x-icon" ),
59
  IMAGE_JLS( "jls" ),
60
  IMAGE_JP2( "jp2" ),
61
  IMAGE_JPEG( "jpeg" ),
62
  IMAGE_JPH( "jph" ),
63
  IMAGE_JPHC( "jphc" ),
64
  IMAGE_JPM( "jpm" ),
65
  IMAGE_JPX( "jpx" ),
66
  IMAGE_JXR( "jxr" ),
67
  IMAGE_JXRA( "jxrA" ),
68
  IMAGE_JXRS( "jxrS" ),
69
  IMAGE_JXS( "jxs" ),
70
  IMAGE_JXSC( "jxsc" ),
71
  IMAGE_JXSI( "jxsi" ),
72
  IMAGE_JXSS( "jxss" ),
73
  IMAGE_KTX( "ktx" ),
74
  IMAGE_KTX2( "ktx2" ),
75
  IMAGE_NAPLPS( "naplps" ),
76
  IMAGE_PNG( "png" ),
77
  IMAGE_PHOTOSHOP( "photoshop" ),
78
  IMAGE_SVG_XML( "svg+xml" ),
79
  IMAGE_T38( "t38" ),
80
  IMAGE_TIFF( "tiff" ),
81
  IMAGE_WEBP( "webp" ),
82
  IMAGE_WMF( "wmf" ),
83
  IMAGE_X_BITMAP( "x-xbitmap" ),
84
  IMAGE_X_PIXMAP( "x-xpixmap" ),
85
86
  /*
87
   * Standard audio types.
88
   */
89
  AUDIO_BASIC( AUDIO, "basic" ),
90
  AUDIO_MP3( AUDIO, "mp3" ),
91
  AUDIO_WAV( AUDIO, "x-wav" ),
92
93
  /*
94
   * Standard video types.
95
   */
96
  VIDEO_MNG( VIDEO, "x-mng" ),
97
98
  /*
99
   * Document types for editing or displaying documents, mix of standard and
100
   * application-specific.
101
   */
102
  TEXT_HTML( TEXT, "html" ),
103
  TEXT_MARKDOWN( TEXT, "markdown" ),
104
  TEXT_PLAIN( TEXT, "plain" ),
105
  TEXT_R_MARKDOWN( TEXT, "R+markdown" ),
106
  TEXT_XHTML( TEXT, "xhtml+xml" ),
107
  TEXT_XML( TEXT, "xml" ),
108
  TEXT_YAML( TEXT, "yaml" ),
109
110
  /*
111
   * When all other lights go out.
112
   */
113
  UNDEFINED( TypeName.UNDEFINED, "undefined" );
114
115
  /**
116
   * The IANA-defined types.
117
   */
118
  public enum TypeName {
119
    APPLICATION,
120
    AUDIO,
121
    IMAGE,
122
    TEXT,
123
    UNDEFINED,
124
    VIDEO
125
  }
126
127
  /**
128
   * The fully qualified IANA-defined media type.
129
   */
130
  private final String mMediaType;
131
132
  /**
133
   * The IANA-defined type name.
134
   */
135
  private final TypeName mTypeName;
136
137
  /**
138
   * The IANA-defined subtype name.
139
   */
140
  private final String mSubtype;
141
142
  /**
143
   * Constructs an instance using the default type name of "image".
144
   *
145
   * @param subtype The image subtype name.
146
   */
147
  MediaType( final String subtype ) {
148
    this( IMAGE, subtype );
149
  }
150
151
  /**
152
   * Constructs an instance using an IANA-defined type and subtype pair.
153
   *
154
   * @param typeName The media type's type name.
155
   * @param subtype  The media type's subtype name.
156
   */
157
  MediaType( final TypeName typeName, final String subtype ) {
158
    mTypeName = typeName;
159
    mSubtype = subtype;
160
    mMediaType = typeName.toString().toLowerCase() + '/' + subtype;
161
  }
162
163
  /**
164
   * Returns the {@link MediaType} associated with the given file.
165
   *
166
   * @param file Has a file name that may contain an extension associated with
167
   *             a known {@link MediaType}.
168
   * @return {@link MediaType#UNDEFINED} if the extension has not been
169
   * assigned, otherwise the {@link MediaType} associated with this
170
   * {@link File}'s file name extension.
171
   */
172
  public static MediaType valueFrom( final File file ) {
173
    assert file != null;
174
    return fromFilename( file.getName() );
175
  }
176
177
  /**
178
   * Returns the {@link MediaType} associated with the given file name.
179
   *
180
   * @param filename The file name that may contain an extension associated
181
   *                 with a known {@link MediaType}.
182
   * @return {@link MediaType#UNDEFINED} if the extension has not been
183
   * assigned, otherwise the {@link MediaType} associated with this
184
   * URL's file name extension.
185
   */
186
  public static MediaType fromFilename( final String filename ) {
187
    assert filename != null;
188
    return getMediaType( getExtension( filename ) );
189
  }
190
191
  /**
192
   * Returns the {@link MediaType} associated with the path to a file.
193
   *
194
   * @param path Has a file name that may contain an extension associated with
195
   *             a known {@link MediaType}.
196
   * @return {@link MediaType#UNDEFINED} if the extension has not been
197
   * assigned, otherwise the {@link MediaType} associated with this
198
   * {@link File}'s file name extension.
199
   */
200
  public static MediaType valueFrom( final Path path ) {
201
    assert path != null;
202
    return valueFrom( path.toFile() );
203
  }
204
205
  /**
206
   * Determines the media type an IANA-defined, semi-colon-separated string.
207
   * This is often used after making an HTTP request to extract the type
208
   * and subtype from the content-type.
209
   *
210
   * @param header The content-type header value, may be {@code null}.
211
   * @return The data type for the resource or {@link MediaType#UNDEFINED} if
212
   * unmapped.
213
   */
214
  public static MediaType valueFrom( String header ) {
215
    if( header == null || header.isBlank() ) {
216
      return UNDEFINED;
217
    }
218
219
    // Trim off the character encoding.
220
    var i = header.indexOf( ';' );
221
    header = header.substring( 0, i == -1 ? header.length() : i );
222
223
    // Split the type and subtype.
224
    i = header.indexOf( '/' );
225
    i = i == -1 ? header.length() : i;
226
    final var type = header.substring( 0, i );
227
    final var subtype = header.substring( i + 1 );
228
229
    return valueFrom( type, subtype );
230
  }
231
232
  /**
233
   * Returns the {@link MediaType} for the given type and subtype names.
234
   *
235
   * @param type    The IANA-defined type name.
236
   * @param subtype The IANA-defined subtype name.
237
   * @return {@link MediaType#UNDEFINED} if there is no {@link MediaType} that
238
   * matches the given type and subtype names.
239
   */
240
  public static MediaType valueFrom(
241
    final String type, final String subtype ) {
242
    assert type != null;
243
    assert subtype != null;
244
245
    for( final var mediaType : values() ) {
246
      if( mediaType.equals( type, subtype ) ) {
247
        return mediaType;
248
      }
249
    }
250
251
    return UNDEFINED;
252
  }
253
254
  /**
255
   * Answers whether the given type and subtype names equal this enumerated
256
   * value. This performs a case-insensitive comparison.
257
   *
258
   * @param type    The type name to compare against this {@link MediaType}.
259
   * @param subtype The subtype name to compare against this {@link MediaType}.
260
   * @return {@code true} when the type and subtype name match.
261
   */
262
  public boolean equals( final String type, final String subtype ) {
263
    assert type != null;
264
    assert subtype != null;
265
266
    return mTypeName.name().equalsIgnoreCase( type ) &&
267
      mSubtype.equalsIgnoreCase( subtype );
268
  }
269
270
  /**
271
   * Answers whether the given {@link TypeName} matches this type name.
272
   *
273
   * @param typeName The {@link TypeName} to compare against the internal value.
274
   * @return {@code true} if the given value is the same IANA-defined type name.
275
   */
276
  public boolean isType( final TypeName typeName ) {
277
    return mTypeName == typeName;
278
  }
279
280
  /**
281
   * Answers whether this instance is a scalable vector graphic.
282
   *
283
   * @return {@code true} if this instance represents an SVG object.
284
   */
285
  public boolean isSvg() {
286
    return this == IMAGE_SVG_XML;
287
  }
288
289
  public boolean isUndefined() {
290
    return this == UNDEFINED;
291
  }
292
293
  /**
294
   * Returns the IANA-defined subtype classification. Primarily used by
295
   * {@link MediaTypeExtension} to initialize associations where the subtype
296
   * name and the file name extension have a 1:1 mapping.
297
   *
298
   * @return The IANA subtype value.
299
   */
300
  public String getSubtype() {
301
    return mSubtype;
302
  }
303
304
  /**
305
   * Creates a temporary {@link File} that starts with the given prefix. The
306
   * file will be deleted when the application exits.
307
   *
308
   * @param prefix The file name begins with this string (may be empty).
309
   * @return The fully qualified path to the temporary file.
310
   * @throws IOException Could not create the temporary file.
311
   */
312
  public Path createTemporaryFile( final String prefix ) throws IOException {
313
    assert prefix != null;
314
315
    final var file = createTempFile(
316
      prefix, '.' + MediaTypeExtension.valueFrom( this ).getExtension() );
317
    file.deleteOnExit();
318
    return file.toPath();
319
  }
320
321
  /**
322
   * Returns the IANA-defined type and sub-type.
323
   *
324
   * @return The unique media type identifier.
325
   */
326
  @Override
327
  public String toString() {
328
    return mMediaType;
329
  }
330
}
1331
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 java.util.List;
5
6
import static com.keenwrite.io.MediaType.*;
7
import static java.util.List.of;
8
9
/**
10
 * Responsible for associating file extensions with {@link MediaType} instances.
11
 * Insertion order must be maintained because the first element in the list
12
 * represents the file name extension that corresponds to its icon.
13
 */
14
public enum MediaTypeExtension {
15
  MEDIA_APP_ACAD( APP_ACAD, of( "dwg" ) ),
16
  MEDIA_APP_PDF( APP_PDF ),
17
  MEDIA_APP_PS( APP_PS, of( "ps" ) ),
18
  MEDIA_APP_EPS( APP_EPS ),
19
  MEDIA_APP_ZIP( APP_ZIP ),
20
21
  MEDIA_AUDIO_MP3( AUDIO_MP3 ),
22
  MEDIA_AUDIO_BASIC( AUDIO_BASIC, of( "au" ) ),
23
  MEDIA_AUDIO_WAV( AUDIO_WAV, of( "wav" ) ),
24
25
  MEDIA_FONT_OTF( FONT_OTF ),
26
  MEDIA_FONT_TTF( FONT_TTF ),
27
28
  MEDIA_IMAGE_APNG( IMAGE_APNG ),
29
  MEDIA_IMAGE_BMP( IMAGE_BMP ),
30
  MEDIA_IMAGE_GIF( IMAGE_GIF ),
31
  MEDIA_IMAGE_JPEG( IMAGE_JPEG,
32
                    of( "jpg", "jpe", "jpeg", "jfif", "pjpeg", "pjp" ) ),
33
  MEDIA_IMAGE_PNG( IMAGE_PNG ),
34
  MEDIA_IMAGE_PSD( IMAGE_PHOTOSHOP, of( "psd" ) ),
35
  MEDIA_IMAGE_SVG( IMAGE_SVG_XML, of( "svg" ) ),
36
  MEDIA_IMAGE_TIFF( IMAGE_TIFF, of( "tiff", "tif" ) ),
37
  MEDIA_IMAGE_WEBP( IMAGE_WEBP ),
38
  MEDIA_IMAGE_X_BITMAP( IMAGE_X_BITMAP, of( "xbm" ) ),
39
  MEDIA_IMAGE_X_PIXMAP( IMAGE_X_PIXMAP, of( "xpm" ) ),
40
41
  MEDIA_VIDEO_MNG( VIDEO_MNG, of( "mng" ) ),
42
43
  MEDIA_TEXT_MARKDOWN( TEXT_MARKDOWN, of(
44
    "md", "markdown", "mdown", "mdtxt", "mdtext", "mdwn", "mkd", "mkdown",
45
    "mkdn" ) ),
46
  MEDIA_TEXT_PLAIN( TEXT_PLAIN, of( "txt", "asc", "ascii", "text", "utxt" ) ),
47
  MEDIA_TEXT_R_MARKDOWN( TEXT_R_MARKDOWN, of( "Rmd" ) ),
48
  MEDIA_TEXT_XHTML( TEXT_XHTML, of( "xhtml" ) ),
49
  MEDIA_TEXT_XML( TEXT_XML ),
50
  MEDIA_TEXT_YAML( TEXT_YAML, of( "yaml", "yml" ) ),
51
52
  MEDIA_UNDEFINED( UNDEFINED, of( "undefined" ) );
53
54
  private final MediaType mMediaType;
55
  private final List<String> mExtensions;
56
57
  /**
58
   * Several media types have only one corresponding standard file name
59
   * extension; this constructor calls {@link MediaType#getSubtype()} to obtain
60
   * said extension. Some {@link MediaType}s have a single extension but their
61
   * assigned IANA name differs (e.g., {@code svg} maps to {@code svg+xml})
62
   * and thus must not use this constructor.
63
   *
64
   * @param mediaType The {@link MediaType} containing only one extension.
65
   */
66
  MediaTypeExtension( final MediaType mediaType ) {
67
    this( mediaType, of( mediaType.getSubtype() ) );
68
  }
69
70
  /**
71
   * Constructs an association of file name extensions to a single {@link
72
   * MediaType}.
73
   *
74
   * @param mediaType  The {@link MediaType} to associate with the given
75
   *                   file name extensions.
76
   * @param extensions The file name extensions used to lookup a corresponding
77
   *                   {@link MediaType}.
78
   */
79
  MediaTypeExtension(
80
    final MediaType mediaType, final List<String> extensions ) {
81
    assert mediaType != null;
82
    assert extensions != null;
83
    assert !extensions.isEmpty();
84
85
    mMediaType = mediaType;
86
    mExtensions = extensions;
87
  }
88
89
  /**
90
   * Returns the first file name extension in the list of file names given
91
   * at construction time.
92
   *
93
   * @return The one file name to rule them all.
94
   */
95
  public String getExtension() {
96
    return mExtensions.get( 0 );
97
  }
98
99
  /**
100
   * Returns the {@link MediaTypeExtension} that matches the given media type.
101
   *
102
   * @param mediaType The media type to find.
103
   * @return The correlated value or {@link #MEDIA_UNDEFINED} if not found.
104
   */
105
  public static MediaTypeExtension valueFrom( final MediaType mediaType ) {
106
    for( final var type : values() ) {
107
      if( type.isMediaType( mediaType ) ) {
108
        return type;
109
      }
110
    }
111
112
    return MEDIA_UNDEFINED;
113
  }
114
115
  boolean isMediaType( final MediaType mediaType ) {
116
    return mMediaType == mediaType;
117
  }
118
119
  /**
120
   * Returns the {@link MediaType} associated with the given file name
121
   * extension. The extension must not contain a period.
122
   *
123
   * @param extension File name extension, case insensitive, {@code null}-safe.
124
   * @return The associated {@link MediaType} as defined by IANA.
125
   */
126
  static MediaType getMediaType( final String extension ) {
127
    final var sanitized = sanitize( extension );
128
129
    for( final var mediaType : MediaTypeExtension.values() ) {
130
      if( mediaType.isType( sanitized ) ) {
131
        return mediaType.getMediaType();
132
      }
133
    }
134
135
    return UNDEFINED;
136
  }
137
138
  private boolean isType( final String sanitized ) {
139
    for( final var extension : mExtensions ) {
140
      if( extension.equalsIgnoreCase( sanitized ) ) {
141
        return true;
142
      }
143
    }
144
145
    return false;
146
  }
147
148
  private static String sanitize( final String extension ) {
149
    return extension == null ? "" : extension.toLowerCase();
150
  }
151
152
  private MediaType getMediaType() {
153
    return mMediaType;
154
  }
155
}
1156
A src/main/java/com/keenwrite/io/MediaTypeSniffer.java
1
package com.keenwrite.io;
2
3
import java.io.*;
4
import java.nio.file.Path;
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
 * Responsible for associating file signatures with IANA-defined
13
 * {@link MediaType} instances. For details see:
14
 * <ul>
15
 *   <li>
16
 *     <a href="https://www.garykessler.net/library/file_sigs.html">Kessler's List</a>
17
 *   </li>
18
 *   <li>
19
 *     <a href="https://en.wikipedia.org/wiki/List_of_file_signatures">Wikipedia's List</a>
20
 *   </li>
21
 *   <li>
22
 *     <a href="https://github.com/veniware/Space-Maker/blob/master/FileSignatures.cs">Space Maker's List</a>
23
 *   </li>
24
 * </ul>
25
 */
26
public class MediaTypeSniffer {
27
  private static final int FORMAT_LENGTH = 11;
28
  private static final int END_OF_DATA = -2;
29
30
  private static final Map<int[], MediaType> FORMAT = new LinkedHashMap<>();
31
32
  static {
33
    //@formatter:off
34
    FORMAT.put( ints( 0x3C, 0x73, 0x76, 0x67, 0x20 ), IMAGE_SVG_XML );
35
    FORMAT.put( ints( 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ), IMAGE_PNG );
36
    FORMAT.put( ints( 0xFF, 0xD8, 0xFF, 0xE0 ), IMAGE_JPEG );
37
    FORMAT.put( ints( 0xFF, 0xD8, 0xFF, 0xEE ), IMAGE_JPEG );
38
    FORMAT.put( ints( 0xFF, 0xD8, 0xFF, 0xE1, -1, -1, 0x45, 0x78, 0x69, 0x66, 0x00 ), IMAGE_JPEG );
39
    FORMAT.put( ints( 0x49, 0x49, 0x2A, 0x00 ), IMAGE_TIFF );
40
    FORMAT.put( ints( 0x4D, 0x4D, 0x00, 0x2A ), IMAGE_TIFF );
41
    FORMAT.put( ints( 0x47, 0x49, 0x46, 0x38 ), IMAGE_GIF );
42
    FORMAT.put( ints( 0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E ), APP_PDF );
43
    FORMAT.put( ints( 0x25, 0x21, 0x50, 0x53, 0x2D, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x2D ), APP_EPS );
44
    FORMAT.put( ints( 0x25, 0x21, 0x50, 0x53 ), APP_PS );
45
    FORMAT.put( ints( 0x38, 0x42, 0x50, 0x53, 0x00, 0x01 ), IMAGE_PHOTOSHOP );
46
    FORMAT.put( ints( 0x8A, 0x4D, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ), VIDEO_MNG );
47
    FORMAT.put( ints( 0x42, 0x4D ), IMAGE_BMP );
48
    FORMAT.put( ints( 0xFF, 0xFB, 0x30 ), AUDIO_MP3 );
49
    FORMAT.put( ints( 0x49, 0x44, 0x33 ), AUDIO_MP3 );
50
    FORMAT.put( ints( 0x3C, 0x21 ), TEXT_HTML );
51
    FORMAT.put( ints( 0x3C, 0x68, 0x74, 0x6D, 0x6C ), TEXT_HTML );
52
    FORMAT.put( ints( 0x3C, 0x68, 0x65, 0x61, 0x64 ), TEXT_HTML );
53
    FORMAT.put( ints( 0x3C, 0x62, 0x6F, 0x64, 0x79 ), TEXT_HTML );
54
    FORMAT.put( ints( 0x3C, 0x48, 0x54, 0x4D, 0x4C ), TEXT_HTML );
55
    FORMAT.put( ints( 0x3C, 0x48, 0x45, 0x41, 0x44 ), TEXT_HTML );
56
    FORMAT.put( ints( 0x3C, 0x42, 0x4F, 0x44, 0x59 ), TEXT_HTML );
57
    FORMAT.put( ints( 0x3C, 0x3F, 0x78, 0x6D, 0x6C, 0x20 ), TEXT_XML );
58
    FORMAT.put( ints( 0xFE, 0xFF, 0x00, 0x3C, 0x00, 0x3f, 0x00, 0x78 ), TEXT_XML );
59
    FORMAT.put( ints( 0xFF, 0xFE, 0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00 ), TEXT_XML );
60
    FORMAT.put( ints( 0x23, 0x64, 0x65, 0x66 ), IMAGE_X_BITMAP );
61
    FORMAT.put( ints( 0x21, 0x20, 0x58, 0x50, 0x4D, 0x32 ), IMAGE_X_PIXMAP );
62
    FORMAT.put( ints( 0x2E, 0x73, 0x6E, 0x64 ), AUDIO_BASIC );
63
    FORMAT.put( ints( 0x64, 0x6E, 0x73, 0x2E ), AUDIO_BASIC );
64
    FORMAT.put( ints( 0x52, 0x49, 0x46, 0x46 ), AUDIO_WAV );
65
    FORMAT.put( ints( 0x50, 0x4B ), APP_ZIP );
66
    FORMAT.put( ints( 0x41, 0x43, -1, -1, -1, -1, 0x00, 0x00, 0x00, 0x00, 0x00 ), APP_ACAD );
67
    FORMAT.put( ints( 0xCA, 0xFE, 0xBA, 0xBE ), APP_JAVA );
68
    FORMAT.put( ints( 0xAC, 0xED ), APP_JAVA_OBJECT );
69
    //@formatter:on
70
  }
71
72
  private MediaTypeSniffer() {
73
  }
74
75
  /**
76
   * Returns the {@link MediaType} for a given set of bytes.
77
   *
78
   * @param data Binary data to compare against the list of known formats.
79
   * @return The IANA-defined {@link MediaType}, or
80
   * {@link MediaType#UNDEFINED} if indeterminate.
81
   */
82
  public static MediaType getMediaType( final byte[] data ) {
83
    assert data != null;
84
85
    final var source = new int[]{
86
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
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 Path} instance by delegating to {@link #getMediaType(InputStream)}.
111
   *
112
   * @param path Path to ascertain the {@link MediaType}.
113
   * @return The IANA-defined {@link MediaType}, or
114
   * {@link MediaType#UNDEFINED} if indeterminate.
115
   * @throws IOException Could not read from the {@link SysFile}.
116
   */
117
  public static MediaType getMediaType( final Path path ) throws IOException {
118
    return getMediaType( path.toFile() );
119
  }
120
121
  /**
122
   * Convenience method to return the probed media type for the given
123
   * {@link SysFile} instance by delegating to
124
   * {@link #getMediaType(InputStream)}.
125
   *
126
   * @param file File to ascertain the {@link MediaType}.
127
   * @return The IANA-defined {@link MediaType}, or
128
   * {@link MediaType#UNDEFINED} if indeterminate.
129
   * @throws IOException Could not read from the {@link SysFile}.
130
   */
131
  public static MediaType getMediaType( final File file )
132
    throws IOException {
133
    try( final var fis = new FileInputStream( file ) ) {
134
      return getMediaType( fis );
135
    }
136
  }
137
138
  /**
139
   * Convenience method to return the probed media type for the given
140
   * {@link BufferedInputStream} instance. <strong>This resets the stream
141
   * pointer</strong> making the call idempotent. Users of this class should
142
   * prefer to call this method when operating on streams to avoid advancing
143
   * the stream.
144
   *
145
   * @param bis Data source to ascertain the {@link MediaType}.
146
   * @return The IANA-defined {@link MediaType}, or
147
   * {@link MediaType#UNDEFINED} if indeterminate.
148
   * @throws IOException Could not read from the {@link SysFile}.
149
   */
150
  public static MediaType getMediaType( final BufferedInputStream bis )
151
    throws IOException {
152
    bis.mark( FORMAT_LENGTH );
153
    final var result = getMediaType( (InputStream) bis );
154
    bis.reset();
155
156
    return result;
157
  }
158
159
  /**
160
   * Helper method to return the probed media type for the given
161
   * {@link InputStream} instance. The caller is responsible for closing
162
   * the stream. <strong>This advances the stream pointer.</strong>
163
   *
164
   * @param is Data source to ascertain the {@link MediaType}.
165
   * @return The IANA-defined {@link MediaType}, or
166
   * {@link MediaType#UNDEFINED} if indeterminate.
167
   * @throws IOException Could not read from the {@link InputStream}.
168
   * @see #getMediaType(BufferedInputStream) to perform a non-destructive
169
   * read.
170
   */
171
  private static MediaType getMediaType( final InputStream is )
172
    throws IOException {
173
    final var input = new byte[ FORMAT_LENGTH ];
174
    final var count = is.read( input, 0, FORMAT_LENGTH );
175
176
    if( count > 1 ) {
177
      final var available = new byte[ count ];
178
      arraycopy( input, 0, available, 0, count );
179
      return getMediaType( available );
180
    }
181
182
    return UNDEFINED;
183
  }
184
185
  /**
186
   * Creates an array of integers from the given data, padded with {@link
187
   * #END_OF_DATA} values up to {@link #FORMAT_LENGTH}.
188
   *
189
   * @param data The input byte values to pad.
190
   * @return The data with padding.
191
   */
192
  private static int[] ints( final int... data ) {
193
    final var magic = new int[ FORMAT_LENGTH ];
194
    int i = -1;
195
    while( ++i < data.length ) {
196
      magic[ i ] = data[ i ];
197
    }
198
199
    while( i < FORMAT_LENGTH ) {
200
      magic[ i++ ] = END_OF_DATA;
201
    }
202
203
    return magic;
204
  }
205
}
1206
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/SysFile.java
1
package com.keenwrite.io;
2
3
import java.nio.file.Path;
4
import java.nio.file.Paths;
5
import java.util.stream.Stream;
6
7
import static java.lang.System.getenv;
8
import static java.nio.file.Files.isExecutable;
9
import static java.util.regex.Pattern.quote;
10
11
/**
12
 * Responsible for file-related functionality.
13
 */
14
public class SysFile extends java.io.File {
15
  /**
16
   * For finding executable programs.
17
   */
18
  private static final String[] EXTENSIONS = new String[]
19
    {"", ".com", ".exe", ".bat", ".cmd"};
20
21
  /**
22
   * Creates a new instance for a given file name.
23
   *
24
   * @param pathname File name to represent for subsequent operations.
25
   */
26
  public SysFile( final String pathname ) {
27
    super( pathname );
28
  }
29
30
  /**
31
   * For a file name that represents an executable (without an extension)
32
   * file, this determines whether the executable is found in the PATH
33
   * environment variable. This will search the PATH each time the method
34
   * is invoked, triggering a full directory scan for all paths listed in
35
   * the environment variable. The result is not cached, so avoid calling
36
   * this in a critical loop.
37
   *
38
   * @return {@code true} when the given file name references an executable
39
   * file located in the PATH environment variable.
40
   */
41
  public boolean canRun() {
42
    final var exe = getName();
43
    final var paths = getenv( "PATH" ).split( quote( pathSeparator ) );
44
    return Stream.of( paths ).map( Paths::get ).anyMatch(
45
      path -> {
46
        final var p = path.resolve( exe );
47
48
        for( final var extension : EXTENSIONS ) {
49
          if( isExecutable( Path.of( p.toString() + extension ) ) ) {
50
            return true;
51
          }
52
        }
53
54
        return false;
55
      }
56
    );
57
  }
58
}
159
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/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
/**
5
 * Responsible for creating a type hierarchy of preference storage keys.
6
 */
7
public class Key {
8
  private final Key mParent;
9
  private final String mName;
10
11
  private Key( final Key parent, final String name ) {
12
    mParent = parent;
13
    mName = name;
14
  }
15
16
  /**
17
   * Returns a new key with no parent.
18
   *
19
   * @param name The key name, never {@code null}.
20
   * @return The new {@link Key} instance with a name but no parent.
21
   */
22
  public static Key key( final String name ) {
23
    assert name != null && !name.isEmpty();
24
    return key( null, name );
25
  }
26
27
  /**
28
   * Returns a new key with a given parent.
29
   *
30
   * @param parent The parent of this {@link Key}, or {@code null} if this is
31
   *               the topmost key in the chain.
32
   * @param name   The key name, never {@code null}.
33
   * @return The new {@link Key} instance with a name and parent.
34
   */
35
  public static Key key( final Key parent, final String name ) {
36
    assert name != null && !name.isEmpty();
37
    return new Key( parent, name );
38
  }
39
40
  private Key parent() {
41
    return mParent;
42
  }
43
44
  private String name() {
45
    return mName;
46
  }
47
48
  /**
49
   * Returns a dot-separated path representing the key's name.
50
   *
51
   * @return The recursively derived dot-separated key name.
52
   */
53
  @Override
54
  public String toString() {
55
    return parent() == null ? name() : parent().toString() + '.' + name();
56
  }
57
}
158
A src/main/java/com/keenwrite/preferences/LocaleProperty.java
1
/* Copyright 2020-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
   * Lists the locales having fonts that are supported by the application.
23
   * When the Markdown and preview CSS files are loaded, a general file is
24
   * first loaded, then a specific file is loaded according to the locale.
25
   * The specific file overrides font families so that different languages
26
   * may be presented.
27
   * <p>
28
   * Using an instance of {@link LinkedHashMap} preserves display order.
29
   * </p>
30
   * <p>
31
   * See
32
   * <a href="https://www.oracle.com/java/technologies/javase/jdk12locales.html">JDK 12 Locales</a>
33
   * for details.
34
   * </p>
35
   */
36
  private static final Map<String, Locale> sLocales = new LinkedHashMap<>();
37
38
  static {
39
    final String[] tags = {
40
      "en-Latn-AU",
41
      "en-Latn-CA",
42
      "en-Latn-GB",
43
      "en-Latn-NZ",
44
      "en-Latn-US",
45
      "en-Latn-ZA",
46
      "ja-Jpan-JP",
47
      "ko-Kore-KR",
48
      "zh-Hans-CN",
49
      "zh-Hans-SG",
50
      "zh-Hant-HK",
51
      "zh-Hant-TW",
52
    };
53
54
    for( final var tag : tags ) {
55
      final var locale = forLanguageTag( tag );
56
      sLocales.put( locale.getDisplayName(), locale );
57
    }
58
  }
59
60
  public LocaleProperty( final Locale locale ) {
61
    super( sanitize( locale ).getDisplayName() );
62
  }
63
64
  public static String parseLocale( final String languageTag ) {
65
    final var locale = forLanguageTag( languageTag );
66
    final var key = getKey( sLocales, locale );
67
    return key == null ? LOCALE_DEFAULT.getDisplayName() : key;
68
  }
69
70
  public static String toLanguageTag( final String displayName ) {
71
    return sLocales.getOrDefault( displayName, LOCALE_DEFAULT ).toLanguageTag();
72
  }
73
74
  public Locale toLocale() {
75
    return sLocales.getOrDefault( getValue(), LOCALE_DEFAULT );
76
  }
77
78
  private static Locale sanitize( final Locale locale ) {
79
    // If the language is "und"efined then use the default locale.
80
    return locale == null || "und".equalsIgnoreCase( locale.toLanguageTag() )
81
      ? LOCALE_DEFAULT
82
      : locale;
83
  }
84
85
  public static ObservableList<String> localeListProperty() {
86
    return listProperty( sLocales.keySet() );
87
  }
88
89
  /**
90
   * Performs an O(n) search through the given map to find the key that is
91
   * mapped to the given value. A bi-directional map would be faster, but
92
   * also introduces additional dependencies. This doesn't need to be fast
93
   * because it happens once, at start up, and there aren't a lot of values.
94
   *
95
   * @param map   The map containing a key to find based on a value.
96
   * @param value The value to find within the map.
97
   * @param <K>   The type of key associated with a value.
98
   * @param <V>   The type of value associated with a key.
99
   * @return The key that corresponds to the given value, or {@code null} if
100
   * the key is not found.
101
   */
102
  @SuppressWarnings( "SameParameterValue" )
103
  private static <K, V> K getKey( final Map<K, V> map, final V value ) {
104
    for( final var entry : map.entrySet() ) {
105
      if( Objects.equals( value, entry.getValue() ) ) {
106
        return entry.getKey();
107
      }
108
    }
109
110
    return null;
111
  }
112
}
1113
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.PreferencesFxEvent;
7
import com.dlsc.preferencesfx.model.Category;
8
import com.dlsc.preferencesfx.model.Group;
9
import com.dlsc.preferencesfx.model.Setting;
10
import com.dlsc.preferencesfx.util.StorageHandler;
11
import com.dlsc.preferencesfx.view.NavigationView;
12
import javafx.beans.property.*;
13
import javafx.event.EventHandler;
14
import javafx.scene.Node;
15
import javafx.scene.control.Button;
16
import javafx.scene.control.DialogPane;
17
import javafx.scene.control.Label;
18
import org.controlsfx.control.MasterDetailPane;
19
20
import java.io.File;
21
22
import static com.dlsc.formsfx.model.structure.Field.ofStringType;
23
import static com.dlsc.preferencesfx.PreferencesFxEvent.EVENT_PREFERENCES_SAVED;
24
import static com.keenwrite.Messages.get;
25
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
26
import static com.keenwrite.preferences.LocaleProperty.localeListProperty;
27
import static com.keenwrite.preferences.SkinProperty.skinListProperty;
28
import static com.keenwrite.preferences.WorkspaceKeys.*;
29
import static javafx.scene.control.ButtonType.CANCEL;
30
import static javafx.scene.control.ButtonType.OK;
31
32
/**
33
 * Provides the ability for users to configure their preferences. This links
34
 * the {@link Workspace} model with the {@link PreferencesFx} view, in MVC.
35
 */
36
@SuppressWarnings( "SameParameterValue" )
37
public final class PreferencesController {
38
39
  private final Workspace mWorkspace;
40
  private final PreferencesFx mPreferencesFx;
41
42
  public PreferencesController( final Workspace workspace ) {
43
    mWorkspace = workspace;
44
45
    // All properties must be initialized before creating the dialog.
46
    mPreferencesFx = createPreferencesFx();
47
48
    initKeyEventHandler( mPreferencesFx );
49
  }
50
51
  /**
52
   * Display the user preferences settings dialog (non-modal).
53
   */
54
  public void show() {
55
    getPreferencesFx().show( false );
56
  }
57
58
  /**
59
   * Call to persist the settings. Strictly speaking, this could watch on
60
   * all values for external changes then save automatically.
61
   */
62
  public void save() {
63
    getPreferencesFx().saveSettings();
64
  }
65
66
  /**
67
   * Delegates to the {@link PreferencesFx} event handler for monitoring
68
   * save events.
69
   *
70
   * @param eventHandler The handler to call when the preferences are saved.
71
   */
72
  public void addSaveEventHandler(
73
    final EventHandler<? super PreferencesFxEvent> eventHandler ) {
74
    getPreferencesFx().addEventHandler( EVENT_PREFERENCES_SAVED, eventHandler );
75
  }
76
77
  private StringField createFontNameField(
78
    final StringProperty fontName, final DoubleProperty fontSize ) {
79
    final var control = new SimpleFontControl( "Change" );
80
    control.fontSizeProperty().addListener( ( c, o, n ) -> {
81
      if( n != null ) {
82
        fontSize.set( n.doubleValue() );
83
      }
84
    } );
85
    return ofStringType( fontName ).render( control );
86
  }
87
88
  /**
89
   * Creates the preferences dialog based using {@link XmlStorageHandler} 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
  private StorageHandler createStorageHandler() {
104
    return new XmlStorageHandler();
105
  }
106
107
  private Category[] createCategories() {
108
    return new Category[]{
109
      Category.of(
110
        get( KEY_DOC ),
111
        Group.of(
112
          get( KEY_DOC_TITLE ),
113
          Setting.of( label( KEY_DOC_TITLE ) ),
114
          Setting.of( title( KEY_DOC_TITLE ),
115
                      stringProperty( KEY_DOC_TITLE ) )
116
        ),
117
        Group.of(
118
          get( KEY_DOC_AUTHOR ),
119
          Setting.of( label( KEY_DOC_AUTHOR ) ),
120
          Setting.of( title( KEY_DOC_AUTHOR ),
121
                      stringProperty( KEY_DOC_AUTHOR ) )
122
        ),
123
        Group.of(
124
          get( KEY_DOC_BYLINE ),
125
          Setting.of( label( KEY_DOC_BYLINE ) ),
126
          Setting.of( title( KEY_DOC_BYLINE ),
127
                      stringProperty( KEY_DOC_BYLINE ) )
128
        ),
129
        Group.of(
130
          get( KEY_DOC_ADDRESS ),
131
          Setting.of( label( KEY_DOC_ADDRESS ) ),
132
          createMultilineSetting( "Address", KEY_DOC_ADDRESS )
133
        ),
134
        Group.of(
135
          get( KEY_DOC_PHONE ),
136
          Setting.of( label( KEY_DOC_PHONE ) ),
137
          Setting.of( title( KEY_DOC_PHONE ),
138
                      stringProperty( KEY_DOC_PHONE ) )
139
        ),
140
        Group.of(
141
          get( KEY_DOC_EMAIL ),
142
          Setting.of( label( KEY_DOC_EMAIL ) ),
143
          Setting.of( title( KEY_DOC_EMAIL ),
144
                      stringProperty( KEY_DOC_EMAIL ) )
145
        ),
146
        Group.of(
147
          get( KEY_DOC_KEYWORDS ),
148
          Setting.of( label( KEY_DOC_KEYWORDS ) ),
149
          Setting.of( title( KEY_DOC_KEYWORDS ),
150
                      stringProperty( KEY_DOC_KEYWORDS ) )
151
        ),
152
        Group.of(
153
          get( KEY_DOC_COPYRIGHT ),
154
          Setting.of( label( KEY_DOC_COPYRIGHT ) ),
155
          Setting.of( title( KEY_DOC_COPYRIGHT ),
156
                      stringProperty( KEY_DOC_COPYRIGHT ) )
157
        ),
158
        Group.of(
159
          get( KEY_DOC_DATE ),
160
          Setting.of( label( KEY_DOC_DATE ) ),
161
          Setting.of( title( KEY_DOC_DATE ),
162
                      stringProperty( KEY_DOC_DATE ) )
163
        )
164
      ),
165
      Category.of(
166
        get( KEY_TYPESET ),
167
        Group.of(
168
          get( KEY_TYPESET_CONTEXT ),
169
          Setting.of( label( KEY_TYPESET_CONTEXT_THEMES_PATH ) ),
170
          Setting.of( title( KEY_TYPESET_CONTEXT_THEMES_PATH ),
171
                      fileProperty( KEY_TYPESET_CONTEXT_THEMES_PATH ), true ),
172
          Setting.of( label( KEY_TYPESET_CONTEXT_CLEAN ) ),
173
          Setting.of( title( KEY_TYPESET_CONTEXT_CLEAN ),
174
                      booleanProperty( KEY_TYPESET_CONTEXT_CLEAN ) )
175
        ),
176
        Group.of(
177
          get( KEY_TYPESET_TYPOGRAPHY ),
178
          Setting.of( label( KEY_TYPESET_TYPOGRAPHY_QUOTES ) ),
179
          Setting.of( title( KEY_TYPESET_TYPOGRAPHY_QUOTES ),
180
                      booleanProperty( KEY_TYPESET_TYPOGRAPHY_QUOTES ) )
181
        )
182
      ),
183
      Category.of(
184
        get( KEY_EDITOR ),
185
        Group.of(
186
          get( KEY_EDITOR_AUTOSAVE ),
187
          Setting.of( label( KEY_EDITOR_AUTOSAVE ) ),
188
          Setting.of( title( KEY_EDITOR_AUTOSAVE ),
189
                      integerProperty( KEY_EDITOR_AUTOSAVE ) )
190
        )
191
      ),
192
      Category.of(
193
        get( KEY_R ),
194
        Group.of(
195
          get( KEY_R_DIR ),
196
          Setting.of( label( KEY_R_DIR,
197
                             stringProperty( KEY_DEF_DELIM_BEGAN ).get(),
198
                             stringProperty( KEY_DEF_DELIM_ENDED ).get() ) ),
199
          Setting.of( title( KEY_R_DIR ),
200
                      fileProperty( KEY_R_DIR ), true )
201
        ),
202
        Group.of(
203
          get( KEY_R_SCRIPT ),
204
          Setting.of( label( KEY_R_SCRIPT ) ),
205
          createMultilineSetting( "Script", KEY_R_SCRIPT )
206
        ),
207
        Group.of(
208
          get( KEY_R_DELIM_BEGAN ),
209
          Setting.of( label( KEY_R_DELIM_BEGAN ) ),
210
          Setting.of( title( KEY_R_DELIM_BEGAN ),
211
                      stringProperty( KEY_R_DELIM_BEGAN ) )
212
        ),
213
        Group.of(
214
          get( KEY_R_DELIM_ENDED ),
215
          Setting.of( label( KEY_R_DELIM_ENDED ) ),
216
          Setting.of( title( KEY_R_DELIM_ENDED ),
217
                      stringProperty( KEY_R_DELIM_ENDED ) )
218
        )
219
      ),
220
      Category.of(
221
        get( KEY_IMAGES ),
222
        Group.of(
223
          get( KEY_IMAGES_DIR ),
224
          Setting.of( label( KEY_IMAGES_DIR ) ),
225
          Setting.of( title( KEY_IMAGES_DIR ),
226
                      fileProperty( KEY_IMAGES_DIR ), true )
227
        ),
228
        Group.of(
229
          get( KEY_IMAGES_ORDER ),
230
          Setting.of( label( KEY_IMAGES_ORDER ) ),
231
          Setting.of( title( KEY_IMAGES_ORDER ),
232
                      stringProperty( KEY_IMAGES_ORDER ) )
233
        ),
234
        Group.of(
235
          get( KEY_IMAGES_RESIZE ),
236
          Setting.of( label( KEY_IMAGES_RESIZE ) ),
237
          Setting.of( title( KEY_IMAGES_RESIZE ),
238
                      booleanProperty( KEY_IMAGES_RESIZE ) )
239
        ),
240
        Group.of(
241
          get( KEY_IMAGES_SERVER ),
242
          Setting.of( label( KEY_IMAGES_SERVER ) ),
243
          Setting.of( title( KEY_IMAGES_SERVER ),
244
                      stringProperty( KEY_IMAGES_SERVER ) )
245
        )
246
      ),
247
      Category.of(
248
        get( KEY_DEF ),
249
        Group.of(
250
          get( KEY_DEF_PATH ),
251
          Setting.of( label( KEY_DEF_PATH ) ),
252
          Setting.of( title( KEY_DEF_PATH ),
253
                      fileProperty( KEY_DEF_PATH ), false )
254
        ),
255
        Group.of(
256
          get( KEY_DEF_DELIM_BEGAN ),
257
          Setting.of( label( KEY_DEF_DELIM_BEGAN ) ),
258
          Setting.of( title( KEY_DEF_DELIM_BEGAN ),
259
                      stringProperty( KEY_DEF_DELIM_BEGAN ) )
260
        ),
261
        Group.of(
262
          get( KEY_DEF_DELIM_ENDED ),
263
          Setting.of( label( KEY_DEF_DELIM_ENDED ) ),
264
          Setting.of( title( KEY_DEF_DELIM_ENDED ),
265
                      stringProperty( KEY_DEF_DELIM_ENDED ) )
266
        )
267
      ),
268
      Category.of(
269
        get( KEY_UI_FONT ),
270
        Group.of(
271
          get( KEY_UI_FONT_EDITOR ),
272
          Setting.of( label( KEY_UI_FONT_EDITOR_NAME ) ),
273
          Setting.of( title( KEY_UI_FONT_EDITOR_NAME ),
274
                      createFontNameField(
275
                        stringProperty( KEY_UI_FONT_EDITOR_NAME ),
276
                        doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) ),
277
                      stringProperty( KEY_UI_FONT_EDITOR_NAME ) ),
278
          Setting.of( label( KEY_UI_FONT_EDITOR_SIZE ) ),
279
          Setting.of( title( KEY_UI_FONT_EDITOR_SIZE ),
280
                      doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) )
281
        ),
282
        Group.of(
283
          get( KEY_UI_FONT_PREVIEW ),
284
          Setting.of( label( KEY_UI_FONT_PREVIEW_NAME ) ),
285
          Setting.of( title( KEY_UI_FONT_PREVIEW_NAME ),
286
                      createFontNameField(
287
                        stringProperty( KEY_UI_FONT_PREVIEW_NAME ),
288
                        doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
289
                      stringProperty( KEY_UI_FONT_PREVIEW_NAME ) ),
290
          Setting.of( label( KEY_UI_FONT_PREVIEW_SIZE ) ),
291
          Setting.of( title( KEY_UI_FONT_PREVIEW_SIZE ),
292
                      doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
293
          Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
294
          Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_NAME ),
295
                      createFontNameField(
296
                        stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ),
297
                        doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
298
                      stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
299
          Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
300
          Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_SIZE ),
301
                      doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) )
302
        )
303
      ),
304
      Category.of(
305
        get( KEY_UI_SKIN ),
306
        Group.of(
307
          get( KEY_UI_SKIN_SELECTION ),
308
          Setting.of( label( KEY_UI_SKIN_SELECTION ) ),
309
          Setting.of( title( KEY_UI_SKIN_SELECTION ),
310
                      skinListProperty(),
311
                      skinProperty( KEY_UI_SKIN_SELECTION ) )
312
        ),
313
        Group.of(
314
          get( KEY_UI_SKIN_CUSTOM ),
315
          Setting.of( label( KEY_UI_SKIN_CUSTOM ) ),
316
          Setting.of( title( KEY_UI_SKIN_CUSTOM ),
317
                      fileProperty( KEY_UI_SKIN_CUSTOM ), false )
318
        )
319
      ),
320
      Category.of(
321
        get( KEY_UI_PREVIEW ),
322
        Group.of(
323
          get( KEY_UI_PREVIEW_STYLESHEET ),
324
          Setting.of( label( KEY_UI_PREVIEW_STYLESHEET ) ),
325
          Setting.of( title( KEY_UI_PREVIEW_STYLESHEET ),
326
                      fileProperty( KEY_UI_PREVIEW_STYLESHEET ), false )
327
        )
328
      ),
329
      Category.of(
330
        get( KEY_LANGUAGE ),
331
        Group.of(
332
          get( KEY_LANGUAGE_LOCALE ),
333
          Setting.of( label( KEY_LANGUAGE_LOCALE ) ),
334
          Setting.of( title( KEY_LANGUAGE_LOCALE ),
335
                      localeListProperty(),
336
                      localeProperty( KEY_LANGUAGE_LOCALE ) )
337
        )
338
      )};
339
  }
340
341
  @SuppressWarnings( "unchecked" )
342
  private Setting<StringField, StringProperty> createMultilineSetting(
343
    final String description, final Key property ) {
344
    final Setting<StringField, StringProperty> setting =
345
      Setting.of( description, stringProperty( property ) );
346
    final var field = setting.getElement();
347
    field.multiline( true );
348
349
    return setting;
350
  }
351
352
  private void initKeyEventHandler( final PreferencesFx preferences ) {
353
    final var view = preferences.getView();
354
    final var nodes = view.getChildrenUnmodifiable();
355
    final var master = (MasterDetailPane) nodes.get( 0 );
356
    final var detail = (NavigationView) master.getDetailNode();
357
    final var pane = (DialogPane) view.getParent();
358
359
    detail.setOnKeyReleased( ( key ) -> {
360
      switch( key.getCode() ) {
361
        case ENTER -> ((Button) pane.lookupButton( OK )).fire();
362
        case ESCAPE -> ((Button) pane.lookupButton( CANCEL )).fire();
363
      }
364
    } );
365
  }
366
367
  /**
368
   * Creates a label for the given key after interpolating its value.
369
   *
370
   * @param key The key to find in the resource bundle.
371
   * @return The value of the key as a label.
372
   */
373
  private Node label( final Key key ) {
374
    return label( key, (String[]) null );
375
  }
376
377
  private Node label( final Key key, final String... values ) {
378
    return new Label( get( key.toString() + ".desc", (Object[]) values ) );
379
  }
380
381
  private String title( final Key key ) {
382
    return get( key.toString() + ".title" );
383
  }
384
385
  private ObjectProperty<File> fileProperty( final Key key ) {
386
    return mWorkspace.fileProperty( key );
387
  }
388
389
  private StringProperty stringProperty( final Key key ) {
390
    return mWorkspace.stringProperty( key );
391
  }
392
393
  private BooleanProperty booleanProperty( final Key key ) {
394
    return mWorkspace.booleanProperty( key );
395
  }
396
397
  @SuppressWarnings( "SameParameterValue" )
398
  private IntegerProperty integerProperty( final Key key ) {
399
    return mWorkspace.integerProperty( key );
400
  }
401
402
  @SuppressWarnings( "SameParameterValue" )
403
  private DoubleProperty doubleProperty( final Key key ) {
404
    return mWorkspace.doubleProperty( key );
405
  }
406
407
  private ObjectProperty<String> skinProperty( final Key key ) {
408
    return mWorkspace.skinProperty( key );
409
  }
410
411
  private ObjectProperty<String> localeProperty( final Key key ) {
412
    return mWorkspace.localeProperty( key );
413
  }
414
415
  private PreferencesFx getPreferencesFx() {
416
    return mPreferencesFx;
417
  }
418
}
1419
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/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( SKIN_DEFAULT );
28
    sSkins.add( "Silver Cavern" );
29
    sSkins.add( "Solarized Dark" );
30
    sSkins.add( "Vampire Byte" );
31
  }
32
33
  public SkinProperty( final String skin ) {
34
    super( skin );
35
  }
36
37
  public static ObservableList<String> skinListProperty() {
38
    return listProperty( sSkins );
39
  }
40
41
  /**
42
   * Returns the given skin name as a sanitized file name, which must map
43
   * to a stylesheet file bundled with the application. This does not include
44
   * the path to the stylesheet. If the given name is not known, the file
45
   * name for {@link Constants#SKIN_DEFAULT} is returned. The extension must
46
   * be added separately.
47
   *
48
   * @param skin The name to convert to a file name.
49
   * @return The given name converted lower case, spaces replaced with
50
   * underscores, without the ".css" extension appended.
51
   */
52
  public static String toFilename( final String skin ) {
53
    return sanitize( skin ).toLowerCase().replace( ' ', '_' );
54
  }
55
56
  /**
57
   * Ensures that the given name is in the list of known skins.
58
   *
59
   * @param skin Validate this name's existence.
60
   * @return The given name, if valid, otherwise the default skin.
61
   */
62
  private static String sanitize( final String skin ) {
63
    return sSkins.contains( skin ) ? skin : SKIN_DEFAULT;
64
  }
65
}
166
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.constants.Constants;
5
import com.keenwrite.sigils.Tokens;
6
import javafx.application.Platform;
7
import javafx.beans.property.*;
8
import javafx.collections.ObservableList;
9
import org.apache.commons.configuration2.XMLConfiguration;
10
import org.apache.commons.configuration2.builder.fluent.Configurations;
11
import org.apache.commons.configuration2.io.FileHandler;
12
13
import java.io.File;
14
import java.time.Year;
15
import java.time.ZonedDateTime;
16
import java.util.*;
17
import java.util.function.BiConsumer;
18
import java.util.function.BooleanSupplier;
19
import java.util.function.Consumer;
20
import java.util.function.Function;
21
22
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
23
import static com.keenwrite.Launcher.getVersion;
24
import static com.keenwrite.constants.Constants.*;
25
import static com.keenwrite.events.StatusEvent.clue;
26
import static com.keenwrite.preferences.WorkspaceKeys.*;
27
import static java.lang.String.valueOf;
28
import static java.lang.System.getProperty;
29
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
30
import static java.util.Map.entry;
31
import static javafx.application.Platform.runLater;
32
import static javafx.collections.FXCollections.observableArrayList;
33
import static javafx.collections.FXCollections.observableSet;
34
35
/**
36
 * Responsible for defining behaviours for separate projects. A workspace has
37
 * the ability to save and restore a session, including the window dimensions,
38
 * tab setup, files, and user preferences.
39
 * <p>
40
 * The configuration must support hierarchical (nested) configuration nodes
41
 * to persist the user interface state. Although possible with a flat
42
 * configuration file, it's not nearly as simple or elegant.
43
 * </p>
44
 * <p>
45
 * Neither JSON nor HOCON support schema validation and versioning, which makes
46
 * XML the more suitable configuration file format. Schema validation and
47
 * versioning provide future-proofing and ease of reading and upgrading previous
48
 * versions of the configuration file.
49
 * </p>
50
 * <p>
51
 * Persistent preferences may be set directly by the user or indirectly by
52
 * the act of using the application.
53
 * </p>
54
 * <p>
55
 * Note the following definitions:
56
 * </p>
57
 * <dl>
58
 *   <dt>File</dt>
59
 *   <dd>References a file name (no path), path, or directory.</dd>
60
 *   <dt>Path</dt>
61
 *   <dd>Fully qualified file name, which includes all parent directories.</dd>
62
 *   <dt>Dir</dt>
63
 *   <dd>Directory without file name ({@link File#isDirectory()} is true).</dd>
64
 * </dl>
65
 */
66
public final class Workspace {
67
  private final Map<Key, Property<?>> VALUES = Map.ofEntries(
68
    entry( KEY_META_VERSION, asStringProperty( getVersion() ) ),
69
    entry( KEY_META_NAME, asStringProperty( "default" ) ),
70
71
    entry( KEY_DOC_TITLE, asStringProperty( "title" ) ),
72
    entry( KEY_DOC_AUTHOR, asStringProperty( getProperty( "user.name" ) ) ),
73
    entry( KEY_DOC_BYLINE, asStringProperty( getProperty( "user.name" ) ) ),
74
    entry( KEY_DOC_ADDRESS, asStringProperty( "" ) ),
75
    entry( KEY_DOC_PHONE, asStringProperty( "" ) ),
76
    entry( KEY_DOC_EMAIL, asStringProperty( "" ) ),
77
    entry( KEY_DOC_KEYWORDS, asStringProperty( "science, nature" ) ),
78
    entry( KEY_DOC_COPYRIGHT, asStringProperty( getYear() ) ),
79
    entry( KEY_DOC_DATE, asStringProperty( getDate() ) ),
80
81
    entry( KEY_EDITOR_AUTOSAVE, asIntegerProperty( 30 ) ),
82
83
    entry( KEY_R_SCRIPT, asStringProperty( "" ) ),
84
    entry( KEY_R_DIR, asFileProperty( USER_DIRECTORY ) ),
85
    entry( KEY_R_DELIM_BEGAN, asStringProperty( R_DELIM_BEGAN_DEFAULT ) ),
86
    entry( KEY_R_DELIM_ENDED, asStringProperty( R_DELIM_ENDED_DEFAULT ) ),
87
88
    entry( KEY_IMAGES_DIR, asFileProperty( USER_DIRECTORY ) ),
89
    entry( KEY_IMAGES_ORDER, asStringProperty( PERSIST_IMAGES_DEFAULT ) ),
90
    entry( KEY_IMAGES_RESIZE, asBooleanProperty( true ) ),
91
    entry( KEY_IMAGES_SERVER, asStringProperty( DIAGRAM_SERVER_NAME ) ),
92
93
    entry( KEY_DEF_PATH, asFileProperty( DEFINITION_DEFAULT ) ),
94
    entry( KEY_DEF_DELIM_BEGAN, asStringProperty( DEF_DELIM_BEGAN_DEFAULT ) ),
95
    entry( KEY_DEF_DELIM_ENDED, asStringProperty( DEF_DELIM_ENDED_DEFAULT ) ),
96
97
    entry( KEY_UI_RECENT_DIR, asFileProperty( USER_DIRECTORY ) ),
98
    entry( KEY_UI_RECENT_DOCUMENT, asFileProperty( DOCUMENT_DEFAULT ) ),
99
    entry( KEY_UI_RECENT_DEFINITION, asFileProperty( DEFINITION_DEFAULT ) ),
100
101
    //@formatter:off
102
    entry( KEY_UI_FONT_EDITOR_NAME, asStringProperty( FONT_NAME_EDITOR_DEFAULT ) ),
103
    entry( KEY_UI_FONT_EDITOR_SIZE, asDoubleProperty( FONT_SIZE_EDITOR_DEFAULT ) ),
104
    entry( KEY_UI_FONT_PREVIEW_NAME, asStringProperty( FONT_NAME_PREVIEW_DEFAULT ) ),
105
    entry( KEY_UI_FONT_PREVIEW_SIZE, asDoubleProperty( FONT_SIZE_PREVIEW_DEFAULT ) ),
106
    entry( KEY_UI_FONT_PREVIEW_MONO_NAME, asStringProperty( FONT_NAME_PREVIEW_MONO_NAME_DEFAULT ) ),
107
    entry( KEY_UI_FONT_PREVIEW_MONO_SIZE, asDoubleProperty( FONT_SIZE_PREVIEW_MONO_SIZE_DEFAULT ) ),
108
109
    entry( KEY_UI_WINDOW_X, asDoubleProperty( WINDOW_X_DEFAULT ) ),
110
    entry( KEY_UI_WINDOW_Y, asDoubleProperty( WINDOW_Y_DEFAULT ) ),
111
    entry( KEY_UI_WINDOW_W, asDoubleProperty( WINDOW_W_DEFAULT ) ),
112
    entry( KEY_UI_WINDOW_H, asDoubleProperty( WINDOW_H_DEFAULT ) ),
113
    entry( KEY_UI_WINDOW_MAX, asBooleanProperty() ),
114
    entry( KEY_UI_WINDOW_FULL, asBooleanProperty() ),
115
116
    entry( KEY_UI_SKIN_SELECTION, asSkinProperty( SKIN_DEFAULT ) ),
117
    entry( KEY_UI_SKIN_CUSTOM, asFileProperty( SKIN_CUSTOM_DEFAULT ) ),
118
119
    entry( KEY_UI_PREVIEW_STYLESHEET, asFileProperty( PREVIEW_CUSTOM_DEFAULT ) ),
120
121
    entry( KEY_LANGUAGE_LOCALE, asLocaleProperty( LOCALE_DEFAULT ) ),
122
123
    entry( KEY_TYPESET_CONTEXT_CLEAN, asBooleanProperty( true ) ),
124
    entry( KEY_TYPESET_CONTEXT_THEMES_PATH, asFileProperty( USER_DIRECTORY ) ),
125
    entry( KEY_TYPESET_CONTEXT_THEME_SELECTION, asStringProperty( "boschet" ) ),
126
    entry( KEY_TYPESET_TYPOGRAPHY_QUOTES, asBooleanProperty( true ) )
127
    //@formatter:on
128
  );
129
130
  private StringProperty asStringProperty( final String defaultValue ) {
131
    return new SimpleStringProperty( defaultValue );
132
  }
133
134
  @SuppressWarnings( "SameParameterValue" )
135
  private IntegerProperty asIntegerProperty( final int defaultValue ) {
136
    return new SimpleIntegerProperty( defaultValue );
137
  }
138
139
  private DoubleProperty asDoubleProperty( final double defaultValue ) {
140
    return new SimpleDoubleProperty( defaultValue );
141
  }
142
143
  private BooleanProperty asBooleanProperty() {
144
    return new SimpleBooleanProperty();
145
  }
146
147
  @SuppressWarnings( "SameParameterValue" )
148
  private BooleanProperty asBooleanProperty( final boolean defaultValue ) {
149
    return new SimpleBooleanProperty( defaultValue );
150
  }
151
152
  private FileProperty asFileProperty( final File defaultValue ) {
153
    return new FileProperty( defaultValue );
154
  }
155
156
  @SuppressWarnings( "SameParameterValue" )
157
  private SkinProperty asSkinProperty( final String defaultValue ) {
158
    return new SkinProperty( defaultValue );
159
  }
160
161
  @SuppressWarnings( "SameParameterValue" )
162
  private LocaleProperty asLocaleProperty( final Locale defaultValue ) {
163
    return new LocaleProperty( defaultValue );
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
      FileProperty.class, File::new
177
    );
178
179
  private static final Map<Class<?>, Function<String, Object>> MARSHALL =
180
    Map.of(
181
      LocaleProperty.class, LocaleProperty::toLanguageTag
182
    );
183
184
  private final Map<Key, SetProperty<?>> SETS = Map.ofEntries(
185
    entry(
186
      KEY_UI_FILES_PATH,
187
      new SimpleSetProperty<>( observableSet( new HashSet<>() ) )
188
    )
189
  );
190
191
  /**
192
   * Creates a new {@link Workspace} that will attempt to load a configuration
193
   * file. If the configuration file cannot be loaded, the workspace settings
194
   * will return default values. This allows unit tests to provide an instance
195
   * of {@link Workspace} when necessary without encountering failures.
196
   */
197
  public Workspace() {
198
    load( FILE_PREFERENCES );
199
  }
200
201
  /**
202
   * Creates a new {@link Workspace} that will attempt to load the given
203
   * configuration file.
204
   *
205
   * @param filename The file to load.
206
   */
207
  public Workspace( final String filename ) {
208
    load( filename );
209
  }
210
211
  /**
212
   * Creates an instance of {@link ObservableList} that is based on a
213
   * modifiable observable array list for the given items.
214
   *
215
   * @param items The items to wrap in an observable list.
216
   * @param <E>   The type of items to add to the list.
217
   * @return An observable property that can have its contents modified.
218
   */
219
  public static <E> ObservableList<E> listProperty( final Set<E> items ) {
220
    return new SimpleListProperty<>( observableArrayList( items ) );
221
  }
222
223
  /**
224
   * Returns a value that represents a setting in the application that the user
225
   * may configure, either directly or indirectly.
226
   *
227
   * @param key The reference to the users' preference stored in deference
228
   *            of app reëntrance.
229
   * @return An observable property to be persisted.
230
   */
231
  @SuppressWarnings( "unchecked" )
232
  public <T, U extends Property<T>> U valuesProperty( final Key key ) {
233
    assert key != null;
234
    // The type that goes into the map must come out.
235
    return (U) VALUES.get( key );
236
  }
237
238
  /**
239
   * Returns a list of values that represent a setting in the application that
240
   * the user may configure, either directly or indirectly. The property
241
   * returned is backed by a mutable {@link Set}.
242
   *
243
   * @param key The {@link Key} associated with a preference value.
244
   * @return An observable property to be persisted.
245
   */
246
  @SuppressWarnings( "unchecked" )
247
  public <T> SetProperty<T> setsProperty( final Key key ) {
248
    assert key != null;
249
    // The type that goes into the map must come out.
250
    return (SetProperty<T>) SETS.get( key );
251
  }
252
253
  /**
254
   * Returns the {@link Boolean} preference value associated with the given
255
   * {@link Key}. The caller must be sure that the given {@link Key} is
256
   * associated with a value that matches the return type.
257
   *
258
   * @param key The {@link Key} associated with a preference value.
259
   * @return The value associated with the given {@link Key}.
260
   */
261
  public boolean toBoolean( final Key key ) {
262
    assert key != null;
263
    return (Boolean) valuesProperty( key ).getValue();
264
  }
265
266
  /**
267
   * Returns the {@link Integer} preference value associated with the given
268
   * {@link Key}. The caller must be sure that the given {@link Key} is
269
   * associated with a value that matches the return type.
270
   *
271
   * @param key The {@link Key} associated with a preference value.
272
   * @return The value associated with the given {@link Key}.
273
   */
274
  public int toInteger( final Key key ) {
275
    assert key != null;
276
    return (Integer) valuesProperty( key ).getValue();
277
  }
278
279
  /**
280
   * Returns the {@link Double} preference value associated with the given
281
   * {@link Key}. The caller must be sure that the given {@link Key} is
282
   * associated with a value that matches the return type.
283
   *
284
   * @param key The {@link Key} associated with a preference value.
285
   * @return The value associated with the given {@link Key}.
286
   */
287
  public double toDouble( final Key key ) {
288
    assert key != null;
289
    return (Double) valuesProperty( key ).getValue();
290
  }
291
292
  public File toFile( final Key key ) {
293
    assert key != null;
294
    return fileProperty( key ).get();
295
  }
296
297
  public String toString( final Key key ) {
298
    assert key != null;
299
    return stringProperty( key ).get();
300
  }
301
302
  public Tokens toTokens( final Key began, final Key ended ) {
303
    assert began != null;
304
    assert ended != null;
305
    return new Tokens( stringProperty( began ), stringProperty( ended ) );
306
  }
307
308
  @SuppressWarnings( "SameParameterValue" )
309
  public IntegerProperty integerProperty( final Key key ) {
310
    assert key != null;
311
    return valuesProperty( key );
312
  }
313
314
  @SuppressWarnings( "SameParameterValue" )
315
  public DoubleProperty doubleProperty( final Key key ) {
316
    assert key != null;
317
    return valuesProperty( key );
318
  }
319
320
  /**
321
   * Returns the {@link File} {@link Property} associated with the given
322
   * {@link Key} from the internal list of preference values. The caller
323
   * must be sure that the given {@link Key} is associated with a {@link File}
324
   * {@link Property}.
325
   *
326
   * @param key The {@link Key} associated with a preference value.
327
   * @return The value associated with the given {@link Key}.
328
   */
329
  public ObjectProperty<File> fileProperty( final Key key ) {
330
    assert key != null;
331
    return valuesProperty( key );
332
  }
333
334
  public ObjectProperty<String> skinProperty( final Key key ) {
335
    assert key != null;
336
    return valuesProperty( key );
337
  }
338
339
  public LocaleProperty localeProperty( final Key key ) {
340
    assert key != null;
341
    return valuesProperty( key );
342
  }
343
344
  /**
345
   * Returns the language locale setting for the
346
   * {@link WorkspaceKeys#KEY_LANGUAGE_LOCALE} key.
347
   *
348
   * @return The user's current locale setting.
349
   */
350
  public Locale getLocale() {
351
    return localeProperty( KEY_LANGUAGE_LOCALE ).toLocale();
352
  }
353
354
  public StringProperty stringProperty( final Key key ) {
355
    assert key != null;
356
    return valuesProperty( key );
357
  }
358
359
  public BooleanProperty booleanProperty( final Key key ) {
360
    assert key != null;
361
    return valuesProperty( key );
362
  }
363
364
  public void loadValueKeys( final Consumer<Key> consumer ) {
365
    VALUES.keySet().forEach( consumer );
366
  }
367
368
  public void loadSetKeys( final Consumer<Key> consumer ) {
369
    SETS.keySet().forEach( consumer );
370
  }
371
372
  /**
373
   * Calls the given consumer for all single-value keys. For lists, see
374
   * {@link #saveSets(BiConsumer)}.
375
   *
376
   * @param consumer Called to accept each preference key value.
377
   */
378
  public void saveValues( final BiConsumer<Key, Property<?>> consumer ) {
379
    VALUES.forEach( consumer );
380
  }
381
382
  /**
383
   * Calls the given consumer for all multi-value keys. For single items, see
384
   * {@link #saveValues(BiConsumer)}. Callers are responsible for iterating
385
   * over the list of items retrieved through this method.
386
   *
387
   * @param consumer Called to accept each preference key list.
388
   */
389
  public void saveSets( final BiConsumer<Key, SetProperty<?>> consumer ) {
390
    SETS.forEach( consumer );
391
  }
392
393
  /**
394
   * Delegates to {@link #listen(Key, ReadOnlyProperty, BooleanSupplier)},
395
   * providing a value of {@code true} for the {@link BooleanSupplier} to
396
   * indicate the property changes always take effect.
397
   *
398
   * @param key      The value to bind to the internal key property.
399
   * @param property The external property value that sets the internal value.
400
   */
401
  public <T> void listen( final Key key, final ReadOnlyProperty<T> property ) {
402
    listen( key, property, () -> true );
403
  }
404
405
  /**
406
   * Binds a read-only property to a value in the preferences. This allows
407
   * user interface properties to change and the preferences will be
408
   * synchronized automatically.
409
   * <p>
410
   * This calls {@link Platform#runLater(Runnable)} to ensure that all pending
411
   * application window states are finished before assessing whether property
412
   * changes should be applied. Without this, exiting the application while the
413
   * window is maximized would persist the window's maximum dimensions,
414
   * preventing restoration to its prior, non-maximum size.
415
   * </p>
416
   *
417
   * @param key      The value to bind to the internal key property.
418
   * @param property The external property value that sets the internal value.
419
   * @param enabled  Indicates whether property changes should be applied.
420
   */
421
  public <T> void listen(
422
    final Key key,
423
    final ReadOnlyProperty<T> property,
424
    final BooleanSupplier enabled ) {
425
    property.addListener(
426
      ( c, o, n ) -> runLater( () -> {
427
        if( enabled.getAsBoolean() ) {
428
          valuesProperty( key ).setValue( n );
429
        }
430
      } )
431
    );
432
  }
433
434
  /**
435
   * Saves the current workspace.
436
   */
437
  public void save() {
438
    try {
439
      final var config = new XMLConfiguration();
440
441
      // The root config key can only be set for an empty configuration file.
442
      config.setRootElementName( APP_TITLE_LOWERCASE );
443
      valuesProperty( KEY_META_VERSION ).setValue( getVersion() );
444
445
      saveValues( ( key, property ) ->
446
                    config.setProperty( key.toString(), marshall( property ) )
447
      );
448
449
      saveSets( ( key, set ) -> {
450
        final var keyName = key.toString();
451
        set.forEach( ( value ) -> config.addProperty( keyName, value ) );
452
      } );
453
      new FileHandler( config ).save( FILE_PREFERENCES );
454
    } catch( final Exception ex ) {
455
      clue( ex );
456
    }
457
  }
458
459
  /**
460
   * Attempts to load the {@link Constants#FILE_PREFERENCES} configuration file.
461
   * If not found, this will fall back to an empty configuration file, leaving
462
   * the application to fill in default values.
463
   *
464
   * @param filename The file containing user preferences to load.
465
   */
466
  private void load( final String filename ) {
467
    try {
468
      final var config = new Configurations().xml( filename );
469
470
      loadValueKeys( ( key ) -> {
471
        final var configValue = config.getProperty( key.toString() );
472
473
        // Allow other properties to load, even if any are missing.
474
        if( configValue != null ) {
475
          final var propertyValue = valuesProperty( key );
476
          propertyValue.setValue( unmarshall( propertyValue, configValue ) );
477
        }
478
      } );
479
480
      loadSetKeys( ( key ) -> {
481
        final var configSet =
482
          new LinkedHashSet<>( config.getList( key.toString() ) );
483
        final var propertySet = setsProperty( key );
484
        propertySet.setValue( observableSet( configSet ) );
485
      } );
486
    } catch( final Exception ex ) {
487
      clue( ex );
488
    }
489
  }
490
491
  private Object unmarshall(
492
    final Property<?> property, final Object configValue ) {
493
    final var setting = configValue.toString();
494
495
    return UNMARSHALL
496
      .getOrDefault( property.getClass(), ( value ) -> value )
497
      .apply( setting );
498
  }
499
500
  private Object marshall( final Property<?> property ) {
501
    return property.getValue() == null
502
      ? null
503
      : MARSHALL
504
      .getOrDefault( property.getClass(), ( __ ) -> property.getValue() )
505
      .apply( property.getValue().toString() );
506
  }
507
508
  private String getYear() {
509
    return valueOf( Year.now().getValue() );
510
  }
511
512
  private String getDate() {
513
    return ZonedDateTime.now().format( RFC_1123_DATE_TIME );
514
  }
515
}
1516
A src/main/java/com/keenwrite/preferences/WorkspaceKeys.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 WorkspaceKeys {
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_TITLE = key( KEY_DOC, "title" );
20
  public static final Key KEY_DOC_AUTHOR = key( KEY_DOC, "author" );
21
  public static final Key KEY_DOC_BYLINE = key( KEY_DOC, "byline" );
22
  public static final Key KEY_DOC_ADDRESS = key( KEY_DOC, "address" );
23
  public static final Key KEY_DOC_PHONE = key( KEY_DOC, "phone" );
24
  public static final Key KEY_DOC_EMAIL = key( KEY_DOC, "email" );
25
  public static final Key KEY_DOC_KEYWORDS = key( KEY_DOC, "keywords" );
26
  public static final Key KEY_DOC_DATE = key( KEY_DOC, "date" );
27
  public static final Key KEY_DOC_COPYRIGHT = key( KEY_DOC, "copyright" );
28
29
  public static final Key KEY_EDITOR = key( KEY_ROOT, "editor" );
30
  public static final Key KEY_EDITOR_AUTOSAVE = key( KEY_EDITOR, "autosave" );
31
32
  public static final Key KEY_R = key( KEY_ROOT, "r" );
33
  public static final Key KEY_R_SCRIPT = key( KEY_R, "script" );
34
  public static final Key KEY_R_DIR = key( KEY_R, "dir" );
35
  public static final Key KEY_R_DELIM = key( KEY_R, "delimiter" );
36
  public static final Key KEY_R_DELIM_BEGAN = key( KEY_R_DELIM, "began" );
37
  public static final Key KEY_R_DELIM_ENDED = key( KEY_R_DELIM, "ended" );
38
39
  public static final Key KEY_IMAGES = key( KEY_ROOT, "images" );
40
  public static final Key KEY_IMAGES_DIR = key( KEY_IMAGES, "dir" );
41
  public static final Key KEY_IMAGES_ORDER = key( KEY_IMAGES, "order" );
42
  public static final Key KEY_IMAGES_RESIZE = key( KEY_IMAGES, "resize" );
43
  public static final Key KEY_IMAGES_SERVER = key( KEY_IMAGES, "server" );
44
45
  public static final Key KEY_DEF = key( KEY_ROOT, "definition" );
46
  public static final Key KEY_DEF_PATH = key( KEY_DEF, "path" );
47
  public static final Key KEY_DEF_DELIM = key( KEY_DEF, "delimiter" );
48
  public static final Key KEY_DEF_DELIM_BEGAN = key( KEY_DEF_DELIM, "began" );
49
  public static final Key KEY_DEF_DELIM_ENDED = key( KEY_DEF_DELIM, "ended" );
50
51
  public static final Key KEY_UI = key( KEY_ROOT, "ui" );
52
53
  public static final Key KEY_UI_RECENT = key( KEY_UI, "recent" );
54
  public static final Key KEY_UI_RECENT_DIR = key( KEY_UI_RECENT, "dir" );
55
  public static final Key KEY_UI_RECENT_DOCUMENT = key( KEY_UI_RECENT, "document" );
56
  public static final Key KEY_UI_RECENT_DEFINITION = key( KEY_UI_RECENT, "definition" );
57
58
  public static final Key KEY_UI_FILES = key( KEY_UI, "files" );
59
  public static final Key KEY_UI_FILES_PATH = key( KEY_UI_FILES, "path" );
60
61
  public static final Key KEY_UI_FONT = key( KEY_UI, "font" );
62
  public static final Key KEY_UI_FONT_EDITOR = key( KEY_UI_FONT, "editor" );
63
  public static final Key KEY_UI_FONT_EDITOR_NAME = key( KEY_UI_FONT_EDITOR, "name" );
64
  public static final Key KEY_UI_FONT_EDITOR_SIZE = key( KEY_UI_FONT_EDITOR, "size" );
65
  public static final Key KEY_UI_FONT_PREVIEW = key( KEY_UI_FONT, "preview" );
66
  public static final Key KEY_UI_FONT_PREVIEW_NAME = key( KEY_UI_FONT_PREVIEW, "name" );
67
  public static final Key KEY_UI_FONT_PREVIEW_SIZE = key( KEY_UI_FONT_PREVIEW, "size" );
68
  public static final Key KEY_UI_FONT_PREVIEW_MONO = key( KEY_UI_FONT_PREVIEW, "mono" );
69
  public static final Key KEY_UI_FONT_PREVIEW_MONO_NAME = key( KEY_UI_FONT_PREVIEW_MONO, "name" );
70
  public static final Key KEY_UI_FONT_PREVIEW_MONO_SIZE = key( KEY_UI_FONT_PREVIEW_MONO, "size" );
71
72
  public static final Key KEY_UI_WINDOW = key( KEY_UI, "window" );
73
  public static final Key KEY_UI_WINDOW_X = key( KEY_UI_WINDOW, "x" );
74
  public static final Key KEY_UI_WINDOW_Y = key( KEY_UI_WINDOW, "y" );
75
  public static final Key KEY_UI_WINDOW_W = key( KEY_UI_WINDOW, "width" );
76
  public static final Key KEY_UI_WINDOW_H = key( KEY_UI_WINDOW, "height" );
77
  public static final Key KEY_UI_WINDOW_MAX = key( KEY_UI_WINDOW, "maximized" );
78
  public static final Key KEY_UI_WINDOW_FULL = key( KEY_UI_WINDOW, "full" );
79
80
  public static final Key KEY_UI_SKIN = key( KEY_UI, "skin" );
81
  public static final Key KEY_UI_SKIN_SELECTION = key( KEY_UI_SKIN, "selection" );
82
  public static final Key KEY_UI_SKIN_CUSTOM = key( KEY_UI_SKIN, "custom" );
83
84
  public static final Key KEY_UI_PREVIEW = key( KEY_UI, "preview" );
85
  public static final Key KEY_UI_PREVIEW_STYLESHEET = key( KEY_UI_PREVIEW, "stylesheet" );
86
87
  public static final Key KEY_LANGUAGE = key( KEY_ROOT, "language" );
88
  public static final Key KEY_LANGUAGE_LOCALE = key( KEY_LANGUAGE, "locale" );
89
90
  public static final Key KEY_TYPESET = key( KEY_ROOT, "typeset" );
91
  public static final Key KEY_TYPESET_CONTEXT = key( KEY_TYPESET, "context" );
92
  public static final Key KEY_TYPESET_CONTEXT_THEMES = key( KEY_TYPESET_CONTEXT, "themes" );
93
  public static final Key KEY_TYPESET_CONTEXT_THEMES_PATH = key( KEY_TYPESET_CONTEXT_THEMES, "path" );
94
  public static final Key KEY_TYPESET_CONTEXT_THEME_SELECTION = key( KEY_TYPESET_CONTEXT_THEMES, "selection" );
95
  public static final Key KEY_TYPESET_CONTEXT_CLEAN = key( KEY_TYPESET_CONTEXT, "clean" );
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 WorkspaceKeys() { }
104
}
1105
A src/main/java/com/keenwrite/preferences/XmlStorageHandler.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}.
13
 * <p>
14
 * This implies that undo/redo functionality must be disabled because the
15
 * {@link Workspace} does not preserve previous states.
16
 * </p>
17
 */
18
public final class XmlStorageHandler implements StorageHandler {
19
  @Override
20
  public void saveSelectedCategory( final String breadcrumb ) { }
21
22
  @Override
23
  public String loadSelectedCategory() {
24
    return "";
25
  }
26
27
  @Override
28
  public void saveDividerPosition( final double dividerPosition ) {
29
  }
30
31
  @Override
32
  public double loadDividerPosition() {
33
    return 0;
34
  }
35
36
  @Override
37
  public void saveWindowWidth( final double windowWidth ) { }
38
39
  @Override
40
  public double loadWindowWidth() {
41
    return 0;
42
  }
43
44
  @Override
45
  public void saveWindowHeight( final double windowHeight ) { }
46
47
  @Override
48
  public double loadWindowHeight() {
49
    return 0;
50
  }
51
52
  @Override
53
  public void saveWindowPosX( final double windowPosX ) { }
54
55
  @Override
56
  public double loadWindowPosX() {
57
    return 0;
58
  }
59
60
  @Override
61
  public void saveWindowPosY( final double windowPosY ) { }
62
63
  @Override
64
  public double loadWindowPosY() {
65
    return 0;
66
  }
67
68
  @Override
69
  public void saveObject( final String breadcrumb, final Object object ) { }
70
71
  @Override
72
  public Object loadObject(
73
    final String breadcrumb, final Object defaultObject ) {
74
    return defaultObject;
75
  }
76
77
  @Override
78
  public <T> T loadObject(
79
    final String breadcrumb, final Class<T> type, final T defaultObject ) {
80
    return defaultObject;
81
  }
82
83
  @Override
84
  @SuppressWarnings("rawtypes")
85
  public ObservableList loadObservableList(
86
    final String breadcrumb, final ObservableList defaultObservableList ) {
87
    return defaultObservableList;
88
  }
89
90
  @Override
91
  public <T> ObservableList<T> loadObservableList(
92
    final String breadcrumb,
93
    final Class<T> type,
94
    final ObservableList<T> defaultObservableList ) {
95
    return defaultObservableList;
96
  }
97
98
  @Override
99
  public boolean clearPreferences() {
100
    return false;
101
  }
102
103
  @Override
104
  public Preferences getPreferences() {
105
    return null;
106
  }
107
}
1108
A src/main/java/com/keenwrite/preview/ChainedReplacedElementFactory.java
1
/* Copyright 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.util.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.ui.adapters.DocumentAdapter;
5
import javafx.beans.property.BooleanProperty;
6
import javafx.beans.property.SimpleBooleanProperty;
7
import org.w3c.dom.Document;
8
import org.xhtmlrenderer.layout.SharedContext;
9
import org.xhtmlrenderer.render.Box;
10
import org.xhtmlrenderer.simple.XHTMLPanel;
11
import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
12
import org.xhtmlrenderer.swing.*;
13
14
import javax.swing.*;
15
import java.awt.*;
16
import java.awt.event.ComponentAdapter;
17
import java.awt.event.ComponentEvent;
18
import java.net.URI;
19
20
import static com.keenwrite.events.FileOpenEvent.fireFileOpenEvent;
21
import static com.keenwrite.events.HyperlinkOpenEvent.fireHyperlinkOpenEvent;
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 implements
34
  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 -> fireHyperlinkOpenEvent( uri );
78
          case FILE -> fireFileOpenEvent( 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().getHeight() / 2, 0 );
178
    int x = box.getAbsX();
179
180
    if( !box.getStyle().isInline() ) {
181
      final var margin = box.getMargin( getLayoutContext() );
182
      y += margin.top();
183
      x += margin.left();
184
    }
185
186
    return new Point( x, y );
187
  }
188
189
  /**
190
   * Delegates to the {@link SharedContext}.
191
   *
192
   * @param id The HTML element identifier to retrieve in {@link Box} form.
193
   * @return The {@link Box} that corresponds to the given element ID, or
194
   * {@code null} if none found.
195
   */
196
  Box getBoxById( final String id ) {
197
    return getSharedContext().getBoxById( id );
198
  }
199
200
  /**
201
   * Suppress scrolling to the top on updates.
202
   */
203
  @Override
204
  public void resetScrollPosition() {
205
  }
206
207
  /**
208
   * The default mouse click listener attempts navigation within the preview
209
   * panel. We want to usurp that behaviour to open the link in a
210
   * platform-specific browser.
211
   */
212
  private void removeMouseTrackingListeners() {
213
    for( final var listener : getMouseTrackingListeners() ) {
214
      if( !(listener instanceof HoverListener) ) {
215
        removeMouseTrackingListener( (FSMouseListener) listener );
216
      }
217
    }
218
  }
219
}
1220
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.ScrollLockEvent;
6
import com.keenwrite.preferences.LocaleProperty;
7
import com.keenwrite.preferences.Workspace;
8
import javafx.beans.property.DoubleProperty;
9
import javafx.beans.property.StringProperty;
10
import javafx.embed.swing.SwingNode;
11
import org.greenrobot.eventbus.Subscribe;
12
13
import javax.swing.*;
14
import java.awt.*;
15
import java.awt.event.ComponentEvent;
16
import java.awt.event.ComponentListener;
17
import java.net.URL;
18
import java.nio.file.Path;
19
import java.util.Locale;
20
21
import static com.keenwrite.Messages.get;
22
import static com.keenwrite.constants.Constants.*;
23
import static com.keenwrite.events.Bus.register;
24
import static com.keenwrite.events.DocumentChangedEvent.fireDocumentChangedEvent;
25
import static com.keenwrite.events.ScrollLockEvent.fireScrollLockEvent;
26
import static com.keenwrite.events.StatusEvent.clue;
27
import static com.keenwrite.preferences.WorkspaceKeys.*;
28
import static com.keenwrite.ui.fonts.IconFactory.getIconFont;
29
import static java.awt.BorderLayout.*;
30
import static java.awt.event.KeyEvent.*;
31
import static java.lang.String.format;
32
import static javafx.scene.CacheHint.SPEED;
33
import static javax.swing.JComponent.WHEN_IN_FOCUSED_WINDOW;
34
import static javax.swing.KeyStroke.getKeyStroke;
35
import static javax.swing.SwingUtilities.invokeLater;
36
import static org.controlsfx.glyphfont.FontAwesome.Glyph.LOCK;
37
import static org.controlsfx.glyphfont.FontAwesome.Glyph.UNLOCK_ALT;
38
import static org.jsoup.Jsoup.parse;
39
40
/**
41
 * Responsible for parsing an HTML document.
42
 */
43
public final class HtmlPreview extends SwingNode implements ComponentListener {
44
  /**
45
   * Converts a text string to a structured HTML document.
46
   */
47
  private static final DocumentConverter CONVERTER = new DocumentConverter();
48
49
  /**
50
   * Used to populate the {@link #HTML_HEAD} with stylesheet file references.
51
   */
52
  private static final String HTML_STYLESHEET =
53
    "<link rel='stylesheet' href='%s'/>";
54
55
  private static final String HTML_BASE =
56
    "<base href='%s'/>";
57
58
  /**
59
   * Render CSS using points (pt) not pixels (px) to reduce the chance of
60
   * poor rendering. The {@link #generateHead()} method fills placeholders.
61
   * When the user has not set a locale, only one stylesheet is added to
62
   * the document. In order, the placeholders are as follows:
63
   * <ol>
64
   * <li>%s --- language</li>
65
   * <li>%s --- default stylesheet</li>
66
   * <li>%s --- language-specific stylesheet</li>
67
   * <li>%s --- user-customized stylesheet</li>
68
   * <li>%s --- font family</li>
69
   * <li>%d --- font size (must be pixels, not points due to bug)</li>
70
   * <li>%s --- base href</li>
71
   * </p>
72
   */
73
  private static final String HTML_HEAD =
74
    """
75
      <!doctype html>
76
      <html lang='%s'><head><title> </title><meta charset='utf-8'/>
77
      %s%s%s<style>body{font-family:'%s';font-size: %dpx;}</style>%s</head><body>
78
      """;
79
80
  private static final String HTML_TAIL = "</body></html>";
81
82
  private static final URL HTML_STYLE_PREVIEW = toUrl( STYLESHEET_PREVIEW );
83
84
  /**
85
   * Reusing this buffer prevents repetitious memory re-allocations.
86
   */
87
  private final StringBuilder mDocument = new StringBuilder( 65536 );
88
89
  private HtmlRenderer mPreview;
90
  private JScrollPane mScrollPane;
91
  private String mBaseUriPath = "";
92
  private String mHead;
93
94
  private volatile boolean mLocked;
95
  private final JButton mScrollLockButton = new JButton();
96
  private final Workspace mWorkspace;
97
98
  /**
99
   * Creates a new preview pane that can scroll to the caret position within the
100
   * document.
101
   *
102
   * @param workspace Contains locale and font size information.
103
   */
104
  public HtmlPreview( final Workspace workspace ) {
105
    mWorkspace = workspace;
106
    mHead = generateHead();
107
108
    // Attempts to prevent a flash of black un-styled content upon load.
109
    setStyle( "-fx-background-color: white;" );
110
111
    invokeLater( () -> {
112
      mPreview = new FlyingSaucerPanel();
113
      mScrollPane = new JScrollPane( (Component) mPreview );
114
      final var verticalBar = mScrollPane.getVerticalScrollBar();
115
      final var verticalPanel = new JPanel( new BorderLayout() );
116
117
      final var map = verticalBar.getInputMap( WHEN_IN_FOCUSED_WINDOW );
118
      addKeyboardEvents( map );
119
120
      mScrollLockButton.setFont( getIconFont( 14 ) );
121
      mScrollLockButton.setText( getLockText( mLocked ) );
122
      mScrollLockButton.setMargin( new Insets( 1, 0, 0, 0 ) );
123
      mScrollLockButton.addActionListener( e -> fireScrollLockEvent( !mLocked ) );
124
125
      verticalPanel.add( verticalBar, CENTER );
126
      verticalPanel.add( mScrollLockButton, PAGE_END );
127
128
      final var wrapper = new JPanel( new BorderLayout() );
129
      wrapper.add( mScrollPane, CENTER );
130
      wrapper.add( verticalPanel, LINE_END );
131
132
      // Enabling the cache attempts to prevent black flashes when resizing.
133
      setCache( true );
134
      setCacheHint( SPEED );
135
      setContent( wrapper );
136
      wrapper.addComponentListener( this );
137
    } );
138
139
    localeProperty().addListener( ( c, o, n ) -> rerender() );
140
    fontFamilyProperty().addListener( ( c, o, n ) -> rerender() );
141
    fontSizeProperty().addListener( ( c, o, n ) -> rerender() );
142
143
    register( this );
144
  }
145
146
  @Subscribe
147
  public void handle( final ScrollLockEvent event ) {
148
    mLocked = event.isLocked();
149
    invokeLater( () -> mScrollLockButton.setText( getLockText( mLocked ) ) );
150
  }
151
152
  /**
153
   * Updates the internal HTML source shown in the preview pane.
154
   *
155
   * @param html The new HTML document to display.
156
   */
157
  public void render( final String html ) {
158
    final var doc = CONVERTER.fromJsoup( parse( decorate( html ) ) );
159
    final var uri = getBaseUri();
160
    doc.setDocumentURI( uri );
161
162
    invokeLater( () -> mPreview.render( doc, uri ) );
163
164
    fireDocumentChangedEvent( html );
165
  }
166
167
  /**
168
   * Clears the caches then re-renders the content.
169
   */
170
  public void refresh() {
171
    mPreview.clearCache();
172
    rerender();
173
  }
174
175
  /**
176
   * Recomputes the HTML head then renders the document.
177
   */
178
  private void rerender() {
179
    mHead = generateHead();
180
    render( mDocument.toString() );
181
  }
182
183
  /**
184
   * Attaches the HTML head prefix and HTML tail suffix to the given HTML
185
   * string.
186
   *
187
   * @param html The HTML to adorn with opening and closing tags.
188
   * @return A complete HTML document, ready for rendering.
189
   */
190
  private String decorate( final String html ) {
191
    mDocument.setLength( 0 );
192
    mDocument.append( html );
193
194
    // Head and tail must be separate from document due to re-rendering.
195
    return mHead + mDocument + HTML_TAIL;
196
  }
197
198
  /**
199
   * Called when settings are changed that affect the HTML document preamble.
200
   * This is a minor performance optimization to avoid generating the head
201
   * each time that the document itself changes.
202
   *
203
   * @return A new doctype and HTML {@code head} element.
204
   */
205
  private String generateHead() {
206
    final var locale = getLocale();
207
    final var base = getBaseUri();
208
    final var custom = getCustomStylesheetUrl();
209
210
    // Point sizes are converted to pixels because of a rendering bug.
211
    return format(
212
      HTML_HEAD,
213
      locale.getLanguage(),
214
      toStylesheetString( HTML_STYLE_PREVIEW ),
215
      toStylesheetString( toUrl( locale ) ),
216
      toStylesheetString( custom ),
217
      getFontFamily(),
218
      toPixels( getFontSize() ),
219
      base.isBlank() ? "" : format( HTML_BASE, base )
220
    );
221
  }
222
223
  /**
224
   * Clears the preview pane by rendering an empty string.
225
   */
226
  public void clear() {
227
    render( "" );
228
  }
229
230
  /**
231
   * Sets the base URI to the containing directory the file being edited.
232
   *
233
   * @param path The path to the file being edited.
234
   */
235
  public void setBaseUri( final Path path ) {
236
    final var parent = path.getParent();
237
    mBaseUriPath = parent == null ? "" : parent.toUri().toString();
238
  }
239
240
  /**
241
   * Scrolls to the closest element matching the given identifier without
242
   * waiting for the document to be ready.
243
   *
244
   * @param id Scroll the preview pane to this unique paragraph identifier.
245
   */
246
  public void scrollTo( final String id ) {
247
    if( !mLocked ) {
248
      invokeLater( () -> {
249
        mPreview.scrollTo( id, mScrollPane );
250
        mScrollPane.repaint();
251
      } );
252
    }
253
  }
254
255
  private String getBaseUri() {
256
    return mBaseUriPath;
257
  }
258
259
  private JScrollPane getScrollPane() {
260
    return mScrollPane;
261
  }
262
263
  public JScrollBar getVerticalScrollBar() {
264
    return getScrollPane().getVerticalScrollBar();
265
  }
266
267
  /**
268
   * Returns the ISO 639 alpha-2 or alpha-3 language code followed by a hyphen
269
   * followed by the ISO 15924 alpha-4 script code, followed by an ISO 3166
270
   * alpha-2 country code or UN M.49 numeric-3 area code. For example, this
271
   * could return "en-Latn-CA" for Canadian English written in the Latin
272
   * character set.
273
   *
274
   * @return Unique identifier for language and country.
275
   */
276
  private static URL toUrl( final Locale locale ) {
277
    return toUrl(
278
      get(
279
        sSettings.getSetting( STYLESHEET_PREVIEW_LOCALE, "" ),
280
        locale.getLanguage(),
281
        locale.getScript(),
282
        locale.getCountry()
283
      )
284
    );
285
  }
286
287
  private static URL toUrl( final String path ) {
288
    return HtmlPreview.class.getResource( path );
289
  }
290
291
  private Locale getLocale() {
292
    return localeProperty().toLocale();
293
  }
294
295
  private LocaleProperty localeProperty() {
296
    return mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
297
  }
298
299
  private String getFontFamily() {
300
    return fontFamilyProperty().get();
301
  }
302
303
  private StringProperty fontFamilyProperty() {
304
    return mWorkspace.stringProperty( KEY_UI_FONT_PREVIEW_NAME );
305
  }
306
307
  private double getFontSize() {
308
    return fontSizeProperty().get();
309
  }
310
311
  /**
312
   * Returns the font size in points.
313
   *
314
   * @return The user-defined font size (in pt).
315
   */
316
  private DoubleProperty fontSizeProperty() {
317
    return mWorkspace.doubleProperty( KEY_UI_FONT_PREVIEW_SIZE );
318
  }
319
320
  private String getLockText( final boolean locked ) {
321
    return Character.toString( (locked ? LOCK : UNLOCK_ALT).getChar() );
322
  }
323
324
  private URL getCustomStylesheetUrl() {
325
    try {
326
      return mWorkspace.toFile( KEY_UI_PREVIEW_STYLESHEET ).toURI().toURL();
327
    } catch( final Exception ex ) {
328
      clue( ex );
329
      return null;
330
    }
331
  }
332
333
  /**
334
   * Maps keyboard events to scrollbar commands so that users may control
335
   * the {@link HtmlPreview} panel using the keyboard.
336
   *
337
   * @param map The map to update with keyboard events.
338
   */
339
  private void addKeyboardEvents( final InputMap map ) {
340
    map.put( getKeyStroke( VK_DOWN, 0 ), "positiveUnitIncrement" );
341
    map.put( getKeyStroke( VK_UP, 0 ), "negativeUnitIncrement" );
342
    map.put( getKeyStroke( VK_PAGE_DOWN, 0 ), "positiveBlockIncrement" );
343
    map.put( getKeyStroke( VK_PAGE_UP, 0 ), "negativeBlockIncrement" );
344
    map.put( getKeyStroke( VK_HOME, 0 ), "minScroll" );
345
    map.put( getKeyStroke( VK_END, 0 ), "maxScroll" );
346
  }
347
348
  @Override
349
  public void componentResized( final ComponentEvent e ) {
350
    if( mWorkspace.toBoolean( KEY_IMAGES_RESIZE ) ) {
351
      mPreview.clearCache();
352
    }
353
354
    // Force update on the Swing EDT, otherwise the scrollbar and content
355
    // will not be updated correctly on some platforms.
356
    invokeLater( () -> getContent().repaint() );
357
  }
358
359
  @Override
360
  public void componentMoved( final ComponentEvent e ) {}
361
362
  @Override
363
  public void componentShown( final ComponentEvent e ) {}
364
365
  @Override
366
  public void componentHidden( final ComponentEvent e ) {}
367
368
  private static String toStylesheetString( final URL url ) {
369
    return url == null ? "" : format( HTML_STYLESHEET, url );
370
  }
371
}
1372
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.whitemagicsoftware.tex.*;
5
import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
6
import org.w3c.dom.Document;
7
8
import java.util.function.Supplier;
9
10
import static com.keenwrite.events.StatusEvent.clue;
11
12
/**
13
 * Responsible for rendering formulas as scalable vector graphics (SVG).
14
 */
15
public final class MathRenderer {
16
17
  /**
18
   * Singleton instance for rendering math symbols.
19
   */
20
  public static final MathRenderer MATH_RENDERER = new MathRenderer();
21
22
  /**
23
   * Default font size in points.
24
   */
25
  private static final float FONT_SIZE = 20f;
26
27
  private final TeXFont mTeXFont = createDefaultTeXFont( FONT_SIZE );
28
  private final TeXEnvironment mEnvironment = createTeXEnvironment( mTeXFont );
29
  private final SvgDomGraphics2D mGraphics = createSvgDomGraphics2D();
30
31
  private MathRenderer() {
32
    mGraphics.scale( FONT_SIZE, FONT_SIZE );
33
  }
34
35
  /**
36
   * This method only takes a few seconds to generate
37
   *
38
   * @param equation A mathematical expression to render.
39
   * @return The given string with all formulas transformed into SVG format.
40
   */
41
  public Document render( final String equation ) {
42
    final var formula = new TeXFormula( equation );
43
    final var box = formula.createBox( mEnvironment );
44
    final var l = new TeXLayout( box, FONT_SIZE );
45
46
    mGraphics.initialize( l.getWidth(), l.getHeight() );
47
    box.draw( mGraphics, l.getX(), l.getY() );
48
    return mGraphics.toDom();
49
  }
50
51
  @SuppressWarnings("SameParameterValue")
52
  private TeXFont createDefaultTeXFont( final float fontSize ) {
53
    return create( () -> new DefaultTeXFont( fontSize ) );
54
  }
55
56
  private TeXEnvironment createTeXEnvironment( final TeXFont texFont ) {
57
    return create( () -> new TeXEnvironment( texFont ) );
58
  }
59
60
  private SvgDomGraphics2D createSvgDomGraphics2D() {
61
    return create( SvgDomGraphics2D::new );
62
  }
63
64
  /**
65
   * Tries to instantiate a given object, returning {@code null} on failure.
66
   * The failure message is bubbled up to to the user interface.
67
   *
68
   * @param supplier Creates an instance.
69
   * @param <T>      The type of instance being created.
70
   * @return An instance of the parameterized type or {@code null} upon error.
71
   */
72
  private <T> T create( final Supplier<T> supplier ) {
73
    try {
74
      return supplier.get();
75
    } catch( final Exception ex ) {
76
      clue( ex );
77
      return null;
78
    }
79
  }
80
}
181
A src/main/java/com/keenwrite/preview/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 final static 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 org.apache.batik.anim.dom.SAXSVGDocumentFactory;
5
import org.apache.batik.bridge.BridgeContext;
6
import org.apache.batik.bridge.DocumentLoader;
7
import org.apache.batik.bridge.UserAgent;
8
import org.apache.batik.bridge.UserAgentAdapter;
9
import org.apache.batik.css.parser.Parser;
10
import org.apache.batik.gvt.renderer.ImageRenderer;
11
import org.apache.batik.transcoder.TranscoderException;
12
import org.apache.batik.transcoder.TranscoderInput;
13
import org.apache.batik.transcoder.TranscoderOutput;
14
import org.apache.batik.transcoder.image.ImageTranscoder;
15
import org.apache.batik.util.XMLResourceDescriptor;
16
import org.w3c.css.sac.CSSException;
17
import org.w3c.dom.Document;
18
import org.w3c.dom.Element;
19
20
import java.awt.*;
21
import java.awt.image.BufferedImage;
22
import java.io.File;
23
import java.io.IOException;
24
import java.io.InputStream;
25
import java.io.StringReader;
26
import java.net.URI;
27
import java.nio.file.Path;
28
import java.text.NumberFormat;
29
import java.text.ParseException;
30
31
import static com.keenwrite.dom.DocumentParser.transform;
32
import static com.keenwrite.events.StatusEvent.clue;
33
import static com.keenwrite.preview.HighQualityRenderingHints.RENDERING_HINTS;
34
import static java.awt.image.BufferedImage.TYPE_INT_RGB;
35
import static java.text.NumberFormat.getIntegerInstance;
36
import static org.apache.batik.bridge.UnitProcessor.createContext;
37
import static org.apache.batik.bridge.UnitProcessor.svgHorizontalLengthToUserSpace;
38
import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH;
39
import static org.apache.batik.transcoder.TranscodingHints.Key;
40
import static org.apache.batik.transcoder.image.ImageTranscoder.KEY_PIXEL_UNIT_TO_MILLIMETER;
41
import static org.apache.batik.util.SVGConstants.SVG_WIDTH_ATTRIBUTE;
42
import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName;
43
44
/**
45
 * Responsible for converting SVG images into rasterized PNG images.
46
 */
47
public final class SvgRasterizer {
48
  /**
49
   * <a href="https://issues.apache.org/jira/browse/BATIK-1112">Bug fix</a>
50
   */
51
  public static final class InkscapeCssParser extends Parser {
52
    public void parseStyleDeclaration( final String source )
53
      throws CSSException, IOException {
54
      super.parseStyleDeclaration(
55
        source.replaceAll( "-inkscape-font-specification:[^;\"]*;", "" )
56
      );
57
    }
58
  }
59
60
  static {
61
    XMLResourceDescriptor.setCSSParserClassName(
62
      InkscapeCssParser.class.getName()
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
71
  private static final SAXSVGDocumentFactory FACTORY_DOM =
72
    new SAXSVGDocumentFactory( getXMLParserClassName() );
73
74
  private static final NumberFormat INT_FORMAT = getIntegerInstance();
75
76
  public static final BufferedImage BROKEN_IMAGE_PLACEHOLDER;
77
78
  /**
79
   * A FontAwesome camera icon, cleft asunder.
80
   */
81
  public static final String BROKEN_IMAGE_SVG =
82
    "<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www" +
83
      ".w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c" +
84
      ".332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path " +
85
      "d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531" +
86
      ".03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2" +
87
      ".511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-" +
88
      ".367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1" +
89
      ".699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 " +
90
      "2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094" +
91
      ".0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5" +
92
      ".0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-" +
93
      ".195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4" +
94
      ".191406-3.652344.207031-.015625.414063-.015625.617187-.007812l" +
95
      ".933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2" +
96
      ".820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3" +
97
      ".429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3" +
98
      ".429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 " +
99
      ".140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281" +
100
      ".808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2" +
101
      ".472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4" +
102
      ".285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1" +
103
      ".9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-" +
104
      ".667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1" +
105
      ".253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 " +
106
      "0'/></g></svg>";
107
108
  static {
109
    // The width and height cannot be embedded in the SVG above because the
110
    // path element values are relative to the viewBox dimensions.
111
    final int w = 75;
112
    final int h = 75;
113
    BufferedImage image;
114
115
    try {
116
      image = rasterizeString( BROKEN_IMAGE_SVG, w );
117
    } catch( final Exception ex ) {
118
      image = new BufferedImage( w, h, TYPE_INT_RGB );
119
      final var graphics = (Graphics2D) image.getGraphics();
120
      graphics.setRenderingHints( RENDERING_HINTS );
121
122
      // Fall back to a (\) symbol.
123
      graphics.setColor( new Color( 204, 204, 204 ) );
124
      graphics.fillRect( 0, 0, w, h );
125
      graphics.setColor( new Color( 255, 204, 204 ) );
126
      graphics.setStroke( new BasicStroke( 4 ) );
127
      graphics.drawOval( w / 4, h / 4, w / 2, h / 2 );
128
      graphics.drawLine( w / 4 + (int) (w / 4 / Math.PI),
129
                         h / 4 + (int) (w / 4 / Math.PI),
130
                         w / 2 + w / 4 - (int) (w / 4 / Math.PI),
131
                         h / 2 + h / 4 - (int) (w / 4 / Math.PI) );
132
    }
133
134
    BROKEN_IMAGE_PLACEHOLDER = image;
135
  }
136
137
  /**
138
   * Responsible for creating a new {@link ImageRenderer} implementation that
139
   * can render a DOM as an SVG image.
140
   */
141
  private static class BufferedImageTranscoder extends ImageTranscoder {
142
    private BufferedImage mImage;
143
144
    @Override
145
    public BufferedImage createImage( final int w, final int h ) {
146
      return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB );
147
    }
148
149
    @Override
150
    public void writeImage(
151
      final BufferedImage image, final TranscoderOutput output ) {
152
      mImage = image;
153
    }
154
155
    public BufferedImage getImage() {
156
      return mImage;
157
    }
158
159
    @Override
160
    protected ImageRenderer createRenderer() {
161
      final ImageRenderer renderer = super.createRenderer();
162
      final RenderingHints hints = renderer.getRenderingHints();
163
      hints.putAll( RENDERING_HINTS );
164
      renderer.setRenderingHints( hints );
165
166
      return renderer;
167
    }
168
  }
169
170
  /**
171
   * Rasterizes the given SVG input stream into an image at 96 DPI.
172
   *
173
   * @param svg The SVG data to rasterize, must be closed by caller.
174
   * @return The given input stream converted to a rasterized image.
175
   */
176
  public static BufferedImage rasterize( final InputStream svg )
177
    throws TranscoderException {
178
    return rasterize( svg, 96 );
179
  }
180
181
  /**
182
   * Rasterizes the given SVG input stream into an image.
183
   *
184
   * @param svg The SVG data to rasterize, must be closed by caller.
185
   * @param dpi Resolution to use when rasterizing (default is 96 DPI).
186
   * @return The given input stream converted to a rasterized image at the
187
   * given resolution.
188
   */
189
  public static BufferedImage rasterize(
190
    final InputStream svg, final float dpi ) throws TranscoderException {
191
    return rasterize(
192
      new TranscoderInput( svg ),
193
      KEY_PIXEL_UNIT_TO_MILLIMETER,
194
      1f / dpi * 25.4f
195
    );
196
  }
197
198
  /**
199
   * Rasterizes the given document into an image.
200
   *
201
   * @param svg   The SVG {@link Document} to rasterize.
202
   * @param width The rasterized image's width (in pixels).
203
   * @return The rasterized image.
204
   */
205
  public static BufferedImage rasterize(
206
    final Document svg, final int width ) throws TranscoderException {
207
    return rasterize(
208
      new TranscoderInput( svg ),
209
      KEY_WIDTH,
210
      fit( svg.getDocumentElement(), width )
211
    );
212
  }
213
214
  /**
215
   * Rasterizes the given vector graphic file using the width dimension
216
   * specified by the document's width attribute.
217
   *
218
   * @param document The {@link Document} containing a vector graphic.
219
   * @return A rasterized image as an instance of {@link BufferedImage}, or
220
   * {@link #BROKEN_IMAGE_PLACEHOLDER} if the graphic could not be rasterized.
221
   */
222
  public static BufferedImage rasterize( final Document document )
223
    throws ParseException, TranscoderException {
224
    final var root = document.getDocumentElement();
225
    final var width = root.getAttribute( SVG_WIDTH_ATTRIBUTE );
226
    return rasterize( document, INT_FORMAT.parse( width ).intValue() );
227
  }
228
229
  /**
230
   * Rasterizes the vector graphic file at the given URI. If any exception
231
   * happens, a broken image icon is returned instead.
232
   *
233
   * @param path  The {@link Path} to a vector graphic file.
234
   * @param width Scale the image to the given width (px); aspect ratio is
235
   *              maintained.
236
   * @return A rasterized image as an instance of {@link BufferedImage}.
237
   */
238
  public static BufferedImage rasterize( final Path path, final int width ) {
239
    return rasterize( path.toUri(), width );
240
  }
241
242
  /**
243
   * Rasterizes the vector graphic file at the given URI. If any exception
244
   * happens, a broken image icon is returned instead.
245
   *
246
   * @param uri   The URI to a vector graphic file, which must include the
247
   *              protocol scheme (such as file:// or https://).
248
   * @param width Scale the image to the given width (px); aspect ratio is
249
   *              maintained.
250
   * @return A rasterized image as an instance of {@link BufferedImage}.
251
   */
252
  public static BufferedImage rasterize( final String uri, final int width ) {
253
    return rasterize( new File( uri ).toURI(), width );
254
  }
255
256
  /**
257
   * Converts an SVG drawing into a rasterized image that can be drawn on
258
   * a graphics context.
259
   *
260
   * @param uri   The path to the image (can be web address).
261
   * @param width Scale the image to the given width (px); aspect ratio is
262
   *              maintained.
263
   * @return The vector graphic transcoded into a raster image format.
264
   */
265
  public static BufferedImage rasterize( final URI uri, final int width ) {
266
    try {
267
      return rasterize( FACTORY_DOM.createDocument( uri.toString() ), width );
268
    } catch( final Exception ex ) {
269
      clue( ex );
270
    }
271
272
    return BROKEN_IMAGE_PLACEHOLDER;
273
  }
274
275
  /**
276
   * Converts an SVG string into a rasterized image that can be drawn on
277
   * a graphics context. The dimensions are determined from the document.
278
   *
279
   * @param xml The SVG xml document.
280
   * @return The vector graphic transcoded into a raster image format.
281
   */
282
  public static BufferedImage rasterizeString( final String xml )
283
    throws ParseException, TranscoderException {
284
    final var document = toDocument( xml );
285
    final var root = document.getDocumentElement();
286
    final var width = root.getAttribute( SVG_WIDTH_ATTRIBUTE );
287
    return rasterizeString( xml, INT_FORMAT.parse( width ).intValue() );
288
  }
289
290
  /**
291
   * Converts an SVG string into a rasterized image that can be drawn on
292
   * a graphics context.
293
   *
294
   * @param svg The SVG xml document.
295
   * @param w   Scale the image width to this size (aspect ratio is
296
   *            maintained).
297
   * @return The vector graphic transcoded into a raster image format.
298
   */
299
  public static BufferedImage rasterizeString( final String svg, final int w )
300
    throws TranscoderException {
301
    return rasterize( toDocument( svg ), w );
302
  }
303
304
  /**
305
   * Given a document object model (DOM) {@link Element}, this will convert that
306
   * element to a string.
307
   *
308
   * @param root The DOM node to convert to a string.
309
   * @return The DOM node as an escaped, plain text string.
310
   */
311
  public static String toSvg( final Element root ) {
312
    try {
313
      return transform( root ).replaceAll( "xmlns=\"\" ", "" );
314
    } catch( final Exception ex ) {
315
      clue( ex );
316
    }
317
318
    return BROKEN_IMAGE_SVG;
319
  }
320
321
  /**
322
   * Converts an SVG XML string into a new {@link Document} instance.
323
   *
324
   * @param xml The XML containing SVG elements.
325
   * @return The SVG contents parsed into a {@link Document} object model.
326
   */
327
  private static Document toDocument( final String xml ) {
328
    try( final var reader = new StringReader( xml ) ) {
329
      return FACTORY_DOM.createSVGDocument(
330
        "http://www.w3.org/2000/svg", reader );
331
    } catch( final Exception ex ) {
332
      throw new IllegalArgumentException( ex );
333
    }
334
  }
335
336
  /**
337
   * Creates a rasterized image of the given source document.
338
   *
339
   * @param input The source document to transcode.
340
   * @param key   Transcoding hint key.
341
   * @param width Transcoding hint value.
342
   * @return A new {@link BufferedImageTranscoder} instance with the given
343
   * transcoding hint applied.
344
   */
345
  private static BufferedImage rasterize(
346
    final TranscoderInput input, final Key key, final float width )
347
    throws TranscoderException {
348
    final var transcoder = new BufferedImageTranscoder();
349
350
    transcoder.addTranscodingHint( key, width );
351
    transcoder.transcode( input, null );
352
353
    return transcoder.getImage();
354
  }
355
356
  /**
357
   * Returns either the given element's SVG document width, or the display
358
   * width, whichever is smaller.
359
   *
360
   * @param root  The SVG document's root node.
361
   * @param width The display width (e.g., rendering canvas width).
362
   * @return The lower value of the document's width or the display width.
363
   */
364
  private static float fit( final Element root, final int width ) {
365
    final var w = root.getAttribute( SVG_WIDTH_ATTRIBUTE );
366
367
    return w == null || w.isBlank() ? width : fit( root, w, width );
368
  }
369
370
  /**
371
   * Returns the width in user space units (pixels?).
372
   *
373
   * @param root  The element containing the width attribute.
374
   * @param w     The element's width attribute value.
375
   * @param width The rendering canvas width.
376
   * @return Either the rendering canvas width or SVG document width,
377
   * whichever is smaller.
378
   */
379
  private static float fit(
380
    final Element root, final String w, final int width ) {
381
    final var usWidth = svgHorizontalLengthToUserSpace(
382
      w, SVG_WIDTH_ATTRIBUTE, createContext( BRIDGE_CONTEXT, root )
383
    );
384
385
    // If the image is too small, scale it to 1/4 the canvas width.
386
    return Math.min( usWidth < 5 ? width / 4.0f : usWidth, (float) width );
387
  }
388
}
1389
A src/main/java/com/keenwrite/preview/SvgReplacedElementFactory.java
1
/* Copyright 2020-2021 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.net.URI;
14
import java.nio.file.Path;
15
16
import static com.keenwrite.events.StatusEvent.clue;
17
import static com.keenwrite.io.HttpFacade.httpGet;
18
import static com.keenwrite.preview.MathRenderer.MATH_RENDERER;
19
import static com.keenwrite.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER;
20
import static com.keenwrite.preview.SvgRasterizer.rasterize;
21
import static com.keenwrite.processors.markdown.extensions.tex.TexNode.HTML_TEX;
22
import static com.keenwrite.util.ProtocolScheme.getProtocol;
23
24
/**
25
 * Responsible for running {@link SvgRasterizer} on SVG images detected within
26
 * a document to transform them into rasterized versions.
27
 */
28
public final class SvgReplacedElementFactory extends ReplacedElementAdapter {
29
30
  public static final String HTML_IMAGE = "img";
31
  public static final String HTML_IMAGE_SRC = "src";
32
33
  private static final ImageReplacedElement BROKEN_IMAGE =
34
    createImageReplacedElement( BROKEN_IMAGE_PLACEHOLDER );
35
36
  @Override
37
  public ReplacedElement createReplacedElement(
38
    final LayoutContext c,
39
    final BlockBox box,
40
    final UserAgentCallback uac,
41
    final int cssWidth,
42
    final int cssHeight ) {
43
    final var e = box.getElement();
44
45
    ImageReplacedElement image = null;
46
47
    try {
48
      BufferedImage raster = null;
49
50
      switch( e.getNodeName() ) {
51
        case HTML_IMAGE -> {
52
          final var source = e.getAttribute( HTML_IMAGE_SRC );
53
          var mediaType = MediaType.fromFilename( source );
54
          URI uri = null;
55
56
          if( getProtocol( source ).isHttp() ) {
57
            if( mediaType.isSvg() || mediaType.isUndefined() ) {
58
              uri = new URI( source );
59
60
              try( final var response = httpGet( uri ) ) {
61
                mediaType = response.getMediaType();
62
              }
63
64
              // Attempt to rasterize SVG depending on URL resource content.
65
              if( !mediaType.isSvg() ) {
66
                uri = null;
67
              }
68
            }
69
          }
70
          else if( mediaType.isSvg() ) {
71
            // Attempt to rasterize based on file name.
72
            final var path = Path.of( new URI( source ).getPath() );
73
74
            if( path.isAbsolute() ) {
75
              uri = path.toUri();
76
            }
77
            else {
78
              final var base = new URI( e.getBaseURI() ).getPath();
79
              uri = Path.of( base, source ).toUri();
80
            }
81
          }
82
83
          if( uri != null ) {
84
            raster = rasterize( uri, box.getContentWidth() );
85
          }
86
        }
87
        case HTML_TEX ->
88
          // Convert the TeX element to a raster graphic.
89
          raster = rasterize( MATH_RENDERER.render( e.getTextContent() ) );
90
      }
91
92
      if( raster != null ) {
93
        image = createImageReplacedElement( raster );
94
      }
95
    } catch( final Exception ex ) {
96
      image = BROKEN_IMAGE;
97
      clue( ex );
98
    }
99
100
    return image;
101
  }
102
103
  private static ImageReplacedElement createImageReplacedElement(
104
    final BufferedImage bi ) {
105
    return new ImageReplacedElement( bi, bi.getWidth(), bi.getHeight() );
106
  }
107
}
1108
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 a 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 final static float PI_FLOAT = (float) Math.PI;
11
12
  private float sincModified( float value ) {
13
    return (float) Math.sin( value ) / value;
14
  }
15
16
  public final 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.createAbsolutionDimension;
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 getNumContributors() {
56
      return numContributors;
57
    }
58
59
    public int[] getArrN() {
60
      return arrN;
61
    }
62
63
    public float[] getArrWeight() {
64
      return arrWeight;
65
    }
66
  }
67
68
  private SubSamplingData horizontalSubsamplingData;
69
  private SubSamplingData verticalSubsamplingData;
70
71
  private final int threadCount = getRuntime().availableProcessors();
72
  private final AtomicInteger multipleInvocationLock = new AtomicInteger();
73
  private final ResampleFilter mFilter;
74
75
  public ResampleOp(
76
    final ResampleFilter filter, final int destWidth, final int destHeight ) {
77
    this( filter,
78
          createAbsolutionDimension( destWidth, destHeight ) );
79
  }
80
81
  public ResampleOp(
82
    final ResampleFilter filter, ConstrainedDimension dimensionConstrain ) {
83
    super( dimensionConstrain );
84
    mFilter = filter;
85
  }
86
87
  public BufferedImage doFilter(
88
    BufferedImage srcImg, BufferedImage dest, int dstWidth, int dstHeight ) {
89
    this.dstWidth = dstWidth;
90
    this.dstHeight = dstHeight;
91
92
    if( dstWidth < 3 || dstHeight < 3 ) {
93
      throw new IllegalArgumentException( "Target must be at least 3x3." );
94
    }
95
96
    assert multipleInvocationLock.incrementAndGet() == 1 :
97
      "Multiple concurrent invocations detected";
98
99
    final var srcType = srcImg.getType();
100
101
    if( srcType == TYPE_BYTE_BINARY ||
102
      srcType == TYPE_BYTE_INDEXED ||
103
      srcType == TYPE_CUSTOM ) {
104
      srcImg = ImageUtils.convert(
105
        srcImg,
106
        srcImg.getColorModel().hasAlpha() ? TYPE_4BYTE_ABGR : TYPE_3BYTE_BGR );
107
    }
108
109
    this.nrChannels = ImageUtils.nrChannels( srcImg );
110
    assert nrChannels > 0;
111
    this.srcWidth = srcImg.getWidth();
112
    this.srcHeight = srcImg.getHeight();
113
114
    byte[][] workPixels = new byte[ srcHeight ][ dstWidth * nrChannels ];
115
116
    // Pre-calculate  sub-sampling
117
    horizontalSubsamplingData = createSubSampling(
118
      mFilter, srcWidth, dstWidth );
119
    verticalSubsamplingData = createSubSampling(
120
      mFilter, srcHeight, dstHeight );
121
122
    final BufferedImage scrImgCopy = srcImg;
123
    final byte[][] workPixelsCopy = workPixels;
124
    final Thread[] threads = new Thread[ threadCount - 1 ];
125
126
    for( int i = 1; i < threadCount; i++ ) {
127
      final int finalI = i;
128
      threads[ i - 1 ] = new Thread( () -> horizontallyFromSrcToWork(
129
        scrImgCopy, workPixelsCopy, finalI, threadCount ) );
130
      threads[ i - 1 ].start();
131
    }
132
133
    horizontallyFromSrcToWork( scrImgCopy, workPixelsCopy, 0, threadCount );
134
    waitForAllThreads( threads );
135
136
    byte[] outPixels = new byte[ dstWidth * dstHeight * nrChannels ];
137
138
    // --------------------------------------------------
139
    // Apply filter to sample vertically from Work to Dst
140
    // --------------------------------------------------
141
    final byte[] outPixelsCopy = outPixels;
142
    for( int i = 1; i < threadCount; i++ ) {
143
      final int finalI = i;
144
      threads[ i - 1 ] = new Thread( () -> verticalFromWorkToDst(
145
        workPixelsCopy, outPixelsCopy, finalI, threadCount ) );
146
      threads[ i - 1 ].start();
147
    }
148
    verticalFromWorkToDst( workPixelsCopy, outPixelsCopy, 0, threadCount );
149
    waitForAllThreads( threads );
150
151
    //noinspection UnusedAssignment
152
    workPixels = null; // free memory
153
    final BufferedImage out;
154
    if( dest != null && dstWidth == dest.getWidth() && dstHeight == dest.getHeight() ) {
155
      out = dest;
156
      int nrDestChannels = ImageUtils.nrChannels( dest );
157
      if( nrDestChannels != nrChannels ) {
158
        final var errorMgs = format(
159
          "Destination image must be compatible width source image. Source " +
160
            "image had %d channels destination image had %d channels",
161
          nrChannels, nrDestChannels );
162
        throw new RuntimeException( errorMgs );
163
      }
164
    }
165
    else {
166
      out = new BufferedImage(
167
        dstWidth, dstHeight, getResultBufferedImageType( srcImg ) );
168
    }
169
170
    ImageUtils.setBGRPixels( outPixels, out, 0, 0, dstWidth, dstHeight );
171
172
    assert multipleInvocationLock.decrementAndGet() == 0 : "Multiple " +
173
      "concurrent invocations detected";
174
175
    return out;
176
  }
177
178
  private void waitForAllThreads( final Thread[] threads ) {
179
    try {
180
      for( final Thread thread : threads ) {
181
        thread.join( Long.MAX_VALUE );
182
      }
183
    } catch( final InterruptedException e ) {
184
      currentThread().interrupt();
185
      throw new RuntimeException( e );
186
    }
187
  }
188
189
  static SubSamplingData createSubSampling(
190
    ResampleFilter filter, int srcSize, int dstSize ) {
191
    final float scale = (float) dstSize / (float) srcSize;
192
    final int[] arrN = new int[ dstSize ];
193
    final int numContributors;
194
    final float[] arrWeight;
195
    final int[] arrPixel;
196
197
    final float fwidth = filter.getSamplingRadius();
198
199
    float centerOffset = 0.5f / scale;
200
201
    if( scale < 1.0f ) {
202
      final float width = fwidth / scale;
203
      // Add 2 to be safe with the ceiling
204
      numContributors = (int) (width * 2.0f + 2);
205
      arrWeight = new float[ dstSize * numContributors ];
206
      arrPixel = new int[ dstSize * numContributors ];
207
208
      final float fNormFac = (float) (1f / (Math.ceil( width ) / fwidth));
209
210
      for( int i = 0; i < dstSize; i++ ) {
211
        final int subindex = i * numContributors;
212
        float center = i / scale + centerOffset;
213
        int left = (int) Math.floor( center - width );
214
        int right = (int) Math.ceil( center + width );
215
        for( int j = left; j <= right; j++ ) {
216
          float weight;
217
          weight = filter.apply( (center - j) * fNormFac );
218
219
          if( weight == 0.0f ) {
220
            continue;
221
          }
222
          int n;
223
          if( j < 0 ) {
224
            n = -j;
225
          }
226
          else if( j >= srcSize ) {
227
            n = srcSize - j + srcSize - 1;
228
          }
229
          else {
230
            n = j;
231
          }
232
          int k = arrN[ i ];
233
          //assert k == j-left:String.format("%s = %s %s", k,j,left);
234
          arrN[ i ]++;
235
          if( n < 0 || n >= srcSize ) {
236
            weight = 0.0f;// Flag that cell should not be used
237
          }
238
          arrPixel[ subindex + k ] = n;
239
          arrWeight[ subindex + k ] = weight;
240
        }
241
        // normalize the filter's weight's so the sum equals to 1.0, very
242
        // important for avoiding box type of artifacts
243
        final int max = arrN[ i ];
244
        float tot = 0;
245
        for( int k = 0; k < max; k++ ) { tot += arrWeight[ subindex + k ]; }
246
        if( tot != 0f ) { // 0 should never happen except bug in filter
247
          for( int k = 0; k < max; k++ ) { arrWeight[ subindex + k ] /= tot; }
248
        }
249
      }
250
    }
251
    else {
252
      // super-sampling
253
      // Scales from smaller to bigger height
254
      numContributors = (int) (fwidth * 2.0f + 1);
255
      arrWeight = new float[ dstSize * numContributors ];
256
      arrPixel = new int[ dstSize * numContributors ];
257
      //
258
      for( int i = 0; i < dstSize; i++ ) {
259
        final int subindex = i * numContributors;
260
        float center = i / scale + centerOffset;
261
        int left = (int) Math.floor( center - fwidth );
262
        int right = (int) Math.ceil( center + fwidth );
263
        for( int j = left; j <= right; j++ ) {
264
          float weight = filter.apply( center - j );
265
          if( weight == 0.0f ) {
266
            continue;
267
          }
268
          int n;
269
          if( j < 0 ) {
270
            n = -j;
271
          }
272
          else if( j >= srcSize ) {
273
            n = srcSize - j + srcSize - 1;
274
          }
275
          else {
276
            n = j;
277
          }
278
          int k = arrN[ i ];
279
          arrN[ i ]++;
280
          if( n < 0 || n >= srcSize ) {
281
            weight = 0.0f;// Flag that cell should not be used
282
          }
283
          arrPixel[ subindex + k ] = n;
284
          arrWeight[ subindex + k ] = weight;
285
        }
286
        // normalize the filter's weight's so the sum equals to 1.0, very
287
        // important for avoiding box type of artifacts
288
        final int max = arrN[ i ];
289
        float tot = 0;
290
        for( int k = 0; k < max; k++ ) { tot += arrWeight[ subindex + k ]; }
291
        assert tot != 0 : "should never happen except bug in filter";
292
        if( tot != 0f ) {
293
          for( int k = 0; k < max; k++ ) { arrWeight[ subindex + k ] /= tot; }
294
        }
295
      }
296
    }
297
    return new SubSamplingData( arrN, arrPixel, arrWeight, numContributors );
298
  }
299
300
  private void verticalFromWorkToDst( byte[][] workPixels, byte[] outPixels,
301
                                      int start, int delta ) {
302
    if( nrChannels == 1 ) {
303
      verticalFromWorkToDstGray(
304
        workPixels, outPixels, start, threadCount );
305
      return;
306
    }
307
    boolean useChannel3 = nrChannels > 3;
308
    for( int x = start; x < dstWidth; x += delta ) {
309
      final int xLocation = x * nrChannels;
310
      for( int y = dstHeight - 1; y >= 0; y-- ) {
311
        final int yTimesNumContributors =
312
          y * verticalSubsamplingData.numContributors;
313
        final int max = verticalSubsamplingData.arrN[ y ];
314
        final int sampleLocation = (y * dstWidth + x) * nrChannels;
315
316
        float sample0 = 0.0f;
317
        float sample1 = 0.0f;
318
        float sample2 = 0.0f;
319
        float sample3 = 0.0f;
320
        int index = yTimesNumContributors;
321
        for( int j = max - 1; j >= 0; j-- ) {
322
          int valueLocation = verticalSubsamplingData.arrPixel[ index ];
323
          float arrWeight = verticalSubsamplingData.arrWeight[ index ];
324
          sample0 += (workPixels[ valueLocation ][ xLocation ] & 0xff) * arrWeight;
325
          sample1 += (workPixels[ valueLocation ][ xLocation + 1 ] & 0xff) * arrWeight;
326
          sample2 += (workPixels[ valueLocation ][ xLocation + 2 ] & 0xff) * arrWeight;
327
          if( useChannel3 ) {
328
            sample3 += (workPixels[ valueLocation ][ xLocation + 3 ] & 0xff) * arrWeight;
329
          }
330
331
          index++;
332
        }
333
334
        outPixels[ sampleLocation ] = toByte( sample0 );
335
        outPixels[ sampleLocation + 1 ] = toByte( sample1 );
336
        outPixels[ sampleLocation + 2 ] = toByte( sample2 );
337
338
        if( useChannel3 ) {
339
          outPixels[ sampleLocation + 3 ] = toByte( sample3 );
340
        }
341
      }
342
    }
343
  }
344
345
  private void verticalFromWorkToDstGray(
346
    byte[][] workPixels, byte[] outPixels, int start, int delta ) {
347
    for( int x = start; x < dstWidth; x += delta ) {
348
      for( int y = dstHeight - 1; y >= 0; y-- ) {
349
        final int yTimesNumContributors =
350
          y * verticalSubsamplingData.numContributors;
351
        final int max = verticalSubsamplingData.arrN[ y ];
352
        final int sampleLocation = y * dstWidth + x;
353
        float sample0 = 0.0f;
354
        int index = yTimesNumContributors;
355
356
        for( int j = max - 1; j >= 0; j-- ) {
357
          int valueLocation = verticalSubsamplingData.arrPixel[ index ];
358
          float arrWeight = verticalSubsamplingData.arrWeight[ index ];
359
          sample0 += (workPixels[ valueLocation ][ x ] & 0xff) * arrWeight;
360
361
          index++;
362
        }
363
364
        outPixels[ sampleLocation ] = toByte( sample0 );
365
      }
366
    }
367
  }
368
369
  /**
370
   * Apply filter to sample horizontally from Src to Work
371
   */
372
  private void horizontallyFromSrcToWork(
373
    BufferedImage srcImg, byte[][] workPixels, int start, int delta ) {
374
    if( nrChannels == 1 ) {
375
      horizontallyFromSrcToWorkGray( srcImg, workPixels, start, delta );
376
      return;
377
    }
378
379
    // Used if we work on int based bitmaps, later used to keep channel values
380
    final int[] tempPixels = new int[ srcWidth ];
381
    // create reusable row to minimize memory overhead
382
    final byte[] srcPixels = new byte[ srcWidth * nrChannels ];
383
    final boolean useChannel3 = nrChannels > 3;
384
385
    for( int k = start; k < srcHeight; k = k + delta ) {
386
      ImageUtils.getPixelsBGR( srcImg, k, srcWidth, srcPixels, tempPixels );
387
388
      for( int i = dstWidth - 1; i >= 0; i-- ) {
389
        int sampleLocation = i * nrChannels;
390
        final int max = horizontalSubsamplingData.arrN[ i ];
391
392
        float sample0 = 0.0f;
393
        float sample1 = 0.0f;
394
        float sample2 = 0.0f;
395
        float sample3 = 0.0f;
396
        int index = i * horizontalSubsamplingData.numContributors;
397
        for( int j = max - 1; j >= 0; j-- ) {
398
          float arrWeight = horizontalSubsamplingData.arrWeight[ index ];
399
          int pixelIndex =
400
            horizontalSubsamplingData.arrPixel[ index ] * nrChannels;
401
402
          sample0 += (srcPixels[ pixelIndex ] & 0xff) * arrWeight;
403
          sample1 += (srcPixels[ pixelIndex + 1 ] & 0xff) * arrWeight;
404
          sample2 += (srcPixels[ pixelIndex + 2 ] & 0xff) * arrWeight;
405
          if( useChannel3 ) {
406
            sample3 += (srcPixels[ pixelIndex + 3 ] & 0xff) * arrWeight;
407
          }
408
          index++;
409
        }
410
411
        workPixels[ k ][ sampleLocation ] = toByte( sample0 );
412
        workPixels[ k ][ sampleLocation + 1 ] = toByte( sample1 );
413
        workPixels[ k ][ sampleLocation + 2 ] = toByte( sample2 );
414
        if( useChannel3 ) {
415
          workPixels[ k ][ sampleLocation + 3 ] = toByte( sample3 );
416
        }
417
      }
418
    }
419
  }
420
421
  /**
422
   * Apply filter to sample horizontally from Src to Work
423
   */
424
  private void horizontallyFromSrcToWorkGray(
425
    BufferedImage srcImg, byte[][] workPixels, int start, int delta ) {
426
    // Used if we work on int based bitmaps, later used to keep channel values
427
    final int[] tempPixels = new int[ srcWidth ];
428
    // create reusable row to minimize memory overhead
429
    final byte[] srcPixels = new byte[ srcWidth ];
430
431
    for( int k = start; k < srcHeight; k = k + delta ) {
432
      ImageUtils.getPixelsBGR( srcImg, k, srcWidth, srcPixels, tempPixels );
433
434
      for( int i = dstWidth - 1; i >= 0; i-- ) {
435
        final int max = horizontalSubsamplingData.arrN[ i ];
436
437
        float sample0 = 0.0f;
438
        int index = i * horizontalSubsamplingData.numContributors;
439
        for( int j = max - 1; j >= 0; j-- ) {
440
          float arrWeight = horizontalSubsamplingData.arrWeight[ index ];
441
          int pixelIndex = horizontalSubsamplingData.arrPixel[ index ];
442
443
          sample0 += (srcPixels[ pixelIndex ] & 0xff) * arrWeight;
444
          index++;
445
        }
446
447
        workPixels[ k ][ i ] = toByte( sample0 );
448
      }
449
    }
450
  }
451
452
  private static byte toByte( final float f ) {
453
    if( f < 0 ) {
454
      return 0;
455
    }
456
457
    return (byte) (f > MAX_CHANNEL_VALUE ? MAX_CHANNEL_VALUE : f + 0.5f);
458
  }
459
460
  protected int getResultBufferedImageType( BufferedImage srcImg ) {
461
    return nrChannels == 3
462
      ? TYPE_3BYTE_BGR
463
      : nrChannels == 4
464
      ? TYPE_4BYTE_ABGR
465
      : srcImg.getSampleModel().getDataType() == TYPE_USHORT
466
      ? TYPE_USHORT_GRAY
467
      : TYPE_BYTE_GRAY;
468
  }
469
}
1470
A src/main/java/com/keenwrite/processors/DefinitionProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import java.util.Map;
5
import java.util.function.Function;
6
7
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
8
9
/**
10
 * Processes interpolated string definitions in the document and inserts
11
 * their values into the post-processed text. The default variable syntax is
12
 * {@code $variable$}.
13
 */
14
public class DefinitionProcessor
15
  extends ExecutorProcessor<String> implements Function<String, String> {
16
17
  private final Map<String, String> mDefinitions;
18
19
  /**
20
   * Constructs a processor capable of interpolating string definitions.
21
   *
22
   * @param successor Subsequent link in the processing chain.
23
   * @param context   Contains resolved definitions map.
24
   */
25
  public DefinitionProcessor(
26
      final Processor<String> successor,
27
      final ProcessorContext context ) {
28
    super( successor );
29
    mDefinitions = context.getResolvedMap();
30
  }
31
32
  /**
33
   * Processes the given text document by replacing variables with their values.
34
   *
35
   * @param text The document text that includes variables that should be
36
   *             replaced with values when rendered as HTML.
37
   * @return The text with all variables replaced.
38
   */
39
  @Override
40
  public String apply( final String text ) {
41
    return replace( text, getDefinitions() );
42
  }
43
44
  /**
45
   * Returns the map to use for variable substitution.
46
   *
47
   * @return A map of variable names to values.
48
   */
49
  protected Map<String, String> getDefinitions() {
50
    return mDefinitions;
51
  }
52
}
153
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 sHtmlPreviewPane;
18
19
  /**
20
   * Constructs the end of a processing chain.
21
   *
22
   * @param htmlPreviewPane The pane to update with the post-processed document.
23
   */
24
  public HtmlPreviewProcessor( final HtmlPreview htmlPreviewPane ) {
25
    sHtmlPreviewPane = htmlPreviewPane;
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
    getHtmlPreviewPane().render( html );
40
    return html;
41
  }
42
43
  private HtmlPreview getHtmlPreviewPane() {
44
    return sHtmlPreviewPane;
45
  }
46
}
147
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 java.io.IOException;
7
8
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
9
import static com.keenwrite.events.StatusEvent.clue;
10
import static com.keenwrite.io.MediaType.TEXT_XML;
11
import static java.nio.file.Files.deleteIfExists;
12
import static java.nio.file.Files.writeString;
13
14
/**
15
 * Responsible for using a typesetting engine to convert an XHTML document
16
 * into a PDF file. This must not be run from the JavaFX thread.
17
 */
18
public final class PdfProcessor extends ExecutorProcessor<String> {
19
  private final ProcessorContext mContext;
20
21
  public PdfProcessor( final ProcessorContext context ) {
22
    assert context != null;
23
    mContext = context;
24
  }
25
26
  /**
27
   * Converts a document by calling a third-party library to typeset the given
28
   * XHTML document.
29
   *
30
   * @param xhtml The document to convert to a PDF file.
31
   * @return {@code null} because there is no valid return value from generating
32
   * a PDF file.
33
   */
34
  public String apply( final String xhtml ) {
35
    try {
36
      clue( "Main.status.typeset.create" );
37
      final var document = TEXT_XML.createTemporaryFile( APP_TITLE_LOWERCASE );
38
      final var pathInput = writeString( document, xhtml );
39
      final var pathOutput = mContext.getExportPath();
40
      final var typesetter = new Typesetter( mContext.getWorkspace() );
41
42
      typesetter.typeset( pathInput, pathOutput );
43
44
      // Smote the temporary file after typesetting the document.
45
      if( typesetter.autoclean() ) {
46
        deleteIfExists( document );
47
      }
48
      else {
49
        document.toFile().deleteOnExit();
50
      }
51
    } catch( final IOException | InterruptedException ex ) {
52
      // Typesetter runtime exceptions will pass up the call stack.
53
      clue( "Main.status.typeset.failed", ex );
54
    }
55
56
    // Do not continue processing (the document was typeset into a binary).
57
    return null;
58
  }
59
}
160
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.Caret;
5
import com.keenwrite.ExportFormat;
6
import com.keenwrite.constants.Constants;
7
import com.keenwrite.io.FileType;
8
import com.keenwrite.preferences.Workspace;
9
import com.keenwrite.preview.HtmlPreview;
10
11
import java.nio.file.Path;
12
import java.util.Map;
13
14
import static com.keenwrite.AbstractFileFactory.lookup;
15
import static com.keenwrite.constants.Constants.DEFAULT_DIRECTORY;
16
17
/**
18
 * Provides a context for configuring a chain of {@link Processor} instances.
19
 */
20
public final class ProcessorContext {
21
  private final HtmlPreview mHtmlPreview;
22
  private final Map<String, String> mResolvedMap;
23
  private final Path mDocumentPath;
24
  private final Path mExportPath;
25
  private final Caret mCaret;
26
  private final ExportFormat mExportFormat;
27
  private final Workspace mWorkspace;
28
29
  /**
30
   * Creates a new context for use by the {@link ProcessorFactory} when
31
   * instantiating new {@link Processor} instances. Although all the
32
   * parameters are required, not all {@link Processor} instances will use
33
   * all parameters.
34
   *
35
   * @param htmlPreview  Where to display the final (HTML) output.
36
   * @param resolvedMap  Fully expanded interpolated strings.
37
   * @param documentPath Path to the document to process.
38
   * @param exportPath   Fully qualified filename to use when exporting.
39
   * @param exportFormat Indicate configuration options for export format.
40
   * @param workspace    Persistent user preferences settings.
41
   * @param caret        Location of the caret in the edited document, which is
42
   *                     used to synchronize the scrollbars.
43
   */
44
  public ProcessorContext(
45
    final HtmlPreview htmlPreview,
46
    final Map<String, String> resolvedMap,
47
    final Path documentPath,
48
    final Path exportPath,
49
    final ExportFormat exportFormat,
50
    final Workspace workspace,
51
    final Caret caret ) {
52
    assert htmlPreview != null;
53
    assert resolvedMap != null;
54
    assert documentPath != null;
55
    assert exportFormat != null;
56
    assert workspace != null;
57
    assert caret != null;
58
59
    mHtmlPreview = htmlPreview;
60
    mResolvedMap = resolvedMap;
61
    mDocumentPath = documentPath;
62
    mCaret = caret;
63
    mExportPath = exportPath;
64
    mExportFormat = exportFormat;
65
    mWorkspace = workspace;
66
  }
67
68
  public boolean isExportFormat( final ExportFormat format ) {
69
    return mExportFormat == format;
70
  }
71
72
  HtmlPreview getPreview() {
73
    return mHtmlPreview;
74
  }
75
76
  /**
77
   * Returns the variable map of interpolated definitions.
78
   *
79
   * @return A map to help dereference variables.
80
   */
81
  Map<String, String> getResolvedMap() {
82
    return mResolvedMap;
83
  }
84
85
  /**
86
   * Fully qualified file name to use when exporting (e.g., document.pdf).
87
   *
88
   * @return Full path to a file name.
89
   */
90
  public Path getExportPath() {
91
    return mExportPath;
92
  }
93
94
  public ExportFormat getExportFormat() {
95
    return mExportFormat;
96
  }
97
98
  /**
99
   * Returns the current caret position in the document being edited and is
100
   * always up-to-date.
101
   *
102
   * @return Caret position in the document.
103
   */
104
  public Caret getCaret() {
105
    return mCaret;
106
  }
107
108
  /**
109
   * Returns the directory that contains the file being edited.
110
   * When {@link Constants#DOCUMENT_DEFAULT} is created, the parent path is
111
   * {@code null}. This will get absolute path to the file before trying to
112
   * get te parent path, which should always be a valid path. In the unlikely
113
   * event that the base path cannot be determined by the path alone, the
114
   * default user directory is returned. This is necessary for the creation
115
   * of new files.
116
   *
117
   * @return Path to the directory containing a file being edited, or the
118
   * default user directory if the base path cannot be determined.
119
   */
120
  public Path getBaseDir() {
121
    final var path = getDocumentPath().toAbsolutePath().getParent();
122
    return path == null ? DEFAULT_DIRECTORY : path;
123
  }
124
125
  public Path getDocumentPath() {
126
    return mDocumentPath;
127
  }
128
129
  FileType getFileType() {
130
    return lookup( getDocumentPath() );
131
  }
132
133
  public Workspace getWorkspace() {
134
    return mWorkspace;
135
  }
136
}
1137
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.AbstractFileFactory;
5
import com.keenwrite.preview.HtmlPreview;
6
import com.keenwrite.processors.markdown.MarkdownProcessor;
7
8
import static com.keenwrite.ExportFormat.*;
9
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
10
11
/**
12
 * Responsible for creating processors capable of parsing, transforming,
13
 * interpolating, and rendering known file types.
14
 */
15
public final class ProcessorFactory extends AbstractFileFactory {
16
17
  private final ProcessorContext mContext;
18
19
  /**
20
   * Constructs a factory with the ability to create processors that can perform
21
   * text and caret processing to generate a final preview.
22
   *
23
   * @param context Parameters needed to construct various processors.
24
   */
25
  private ProcessorFactory( final ProcessorContext context ) {
26
    mContext = context;
27
  }
28
29
  private Processor<String> createProcessor() {
30
    final var context = getProcessorContext();
31
32
    // If the content is not to be exported, then the successor processor
33
    // is one that parses Markdown into HTML and passes the string to the
34
    // HTML preview pane.
35
    //
36
    // Otherwise, bolt on a processor that---after the interpolation and
37
    // substitution phase, which includes text strings or R code---will
38
    // generate HTML or plain Markdown. HTML has a few output formats:
39
    // with embedded SVG representing formulas, or without any conversion
40
    // to SVG. Without conversion would require client-side rendering of
41
    // math (such as using the JavaScript-based KaTeX engine).
42
    final var successor = context.isExportFormat( NONE )
43
      ? createHtmlPreviewProcessor( context )
44
      : context.isExportFormat( XHTML_TEX )
45
      ? createXhtmlProcessor( context )
46
      : context.isExportFormat( APPLICATION_PDF )
47
      ? createPdfProcessor( context )
48
      : createIdentityProcessor( context );
49
50
    final var processor = switch( context.getFileType() ) {
51
      case SOURCE, RMARKDOWN -> createMarkdownProcessor( successor );
52
      default -> createPreformattedProcessor( successor );
53
    };
54
55
    return new ExecutorProcessor<>( processor );
56
  }
57
58
  /**
59
   * Creates a new {@link Processor} chain suitable for parsing and rendering
60
   * the file opened at the given tab.
61
   *
62
   * @param context The tab containing a text editor, path, and caret position.
63
   * @return A processor that can render the given tab's text.
64
   */
65
  public static Processor<String> createProcessors(
66
    final ProcessorContext context ) {
67
    return new ProcessorFactory( context ).createProcessor();
68
  }
69
70
  /**
71
   * Instantiates a new {@link Processor} that has no successor and returns
72
   * the string it was given without modification.
73
   *
74
   * @return An instance of {@link Processor} that performs no processing.
75
   */
76
  @SuppressWarnings( "unused" )
77
  private Processor<String> createIdentityProcessor(
78
    final ProcessorContext ignored ) {
79
    return IDENTITY;
80
  }
81
82
  /**
83
   * Instantiates a new {@link Processor} that passes an incoming HTML
84
   * string to a user interface widget that can render HTML as a web page.
85
   *
86
   * @return An instance of {@link Processor} that forwards HTML for display.
87
   */
88
  @SuppressWarnings( "unused" )
89
  private Processor<String> createHtmlPreviewProcessor(
90
    final ProcessorContext ignored ) {
91
    return new HtmlPreviewProcessor( getPreviewPane() );
92
  }
93
94
  /**
95
   * Instantiates a {@link Processor} responsible for parsing Markdown and
96
   * definitions.
97
   *
98
   * @return A chain of {@link Processor}s for processing Markdown and
99
   * definitions.
100
   */
101
  private Processor<String> createMarkdownProcessor(
102
    final Processor<String> successor ) {
103
    final var dp = createDefinitionProcessor( successor );
104
    return MarkdownProcessor.create( dp, getProcessorContext() );
105
  }
106
107
  private Processor<String> createDefinitionProcessor(
108
    final Processor<String> successor ) {
109
    return new DefinitionProcessor( successor, getProcessorContext() );
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 Processor<String> createXhtmlProcessor(
121
    final ProcessorContext context ) {
122
    return createXhtmlProcessor( IDENTITY, context );
123
  }
124
125
  private Processor<String> createXhtmlProcessor(
126
    final Processor<String> successor, final ProcessorContext context ) {
127
    return new XhtmlProcessor( successor, context );
128
  }
129
130
  private Processor<String> createPdfProcessor(
131
    final ProcessorContext context ) {
132
    final var pdfp = new PdfProcessor( context );
133
    return createXhtmlProcessor( pdfp, context );
134
  }
135
136
  private Processor<String> createPreformattedProcessor(
137
    final Processor<String> successor ) {
138
    return new PreformattedProcessor( successor );
139
  }
140
141
  private ProcessorContext getProcessorContext() {
142
    return mContext;
143
  }
144
145
  private HtmlPreview getPreviewPane() {
146
    return getProcessorContext().getPreview();
147
  }
148
}
1149
A src/main/java/com/keenwrite/processors/XhtmlProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors;
3
4
import com.keenwrite.dom.DocumentParser;
5
import com.keenwrite.preferences.Key;
6
import com.keenwrite.preferences.Workspace;
7
import com.keenwrite.ui.heuristics.WordCounter;
8
import com.whitemagicsoftware.keenquotes.Contractions;
9
import com.whitemagicsoftware.keenquotes.Converter;
10
import javafx.beans.property.StringProperty;
11
import org.w3c.dom.Document;
12
13
import java.io.FileNotFoundException;
14
import java.nio.file.Path;
15
import java.util.List;
16
import java.util.Locale;
17
import java.util.Map;
18
import java.util.regex.Pattern;
19
20
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
21
import static com.keenwrite.dom.DocumentParser.*;
22
import static com.keenwrite.events.StatusEvent.clue;
23
import static com.keenwrite.io.HttpFacade.httpGet;
24
import static com.keenwrite.preferences.WorkspaceKeys.*;
25
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
26
import static com.keenwrite.util.ProtocolScheme.getProtocol;
27
import static com.whitemagicsoftware.keenquotes.Converter.CHARS;
28
import static com.whitemagicsoftware.keenquotes.ParserFactory.ParserType.PARSER_XML;
29
import static java.lang.String.format;
30
import static java.lang.String.valueOf;
31
import static java.nio.file.Files.copy;
32
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
33
import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS;
34
import static java.util.regex.Pattern.compile;
35
36
/**
37
 * Responsible for making an XHTML document complete by wrapping it with html
38
 * and body elements. This doesn't have to be super-efficient because it's
39
 * not run in real-time.
40
 */
41
public final class XhtmlProcessor extends ExecutorProcessor<String> {
42
  private final static Pattern BLANK =
43
    compile( "\\p{Blank}", UNICODE_CHARACTER_CLASS );
44
45
  private final static Converter sTypographer = new Converter(
46
    lex -> clue( lex.toString() ), contractions(), CHARS, PARSER_XML );
47
48
  private final ProcessorContext mContext;
49
50
  public XhtmlProcessor(
51
    final Processor<String> successor, final ProcessorContext context ) {
52
    super( successor );
53
54
    assert context != null;
55
    mContext = context;
56
  }
57
58
  /**
59
   * Responsible for producing a well-formed XML document complete with
60
   * metadata (title, author, keywords, copyright, and date).
61
   *
62
   * @param html The HTML document to transform into an XHTML document.
63
   * @return The transformed HTML document.
64
   */
65
  @Override
66
  public String apply( final String html ) {
67
    clue( "Main.status.typeset.xhtml" );
68
69
    try {
70
      final var doc = DocumentParser.parse( decorate( html ) );
71
      setMetaData( doc );
72
73
      walk( doc, "//img", node -> {
74
        try {
75
          final var attrs = node.getAttributes();
76
77
          if( attrs != null ) {
78
            final var attr = attrs.getNamedItem( "src" );
79
80
            if( attr != null ) {
81
              final var imageFile = exportImage( attr.getTextContent() );
82
83
              attr.setTextContent( imageFile.toString() );
84
            }
85
          }
86
        } catch( final Exception ex ) {
87
          clue( ex );
88
        }
89
      } );
90
91
      final var document = DocumentParser.toString( doc );
92
93
      return curl() ? sTypographer.apply( document ) : document;
94
    } catch( final Exception ex ) {
95
      clue( ex );
96
    }
97
98
    return html;
99
  }
100
101
  /**
102
   * Applies the metadata fields to the document.
103
   *
104
   * @param doc The document to adorn with metadata.
105
   */
106
  private void setMetaData( final Document doc ) {
107
    final var metadata = createMetaData( doc );
108
109
    walk( doc, "/html/head", node ->
110
      metadata.entrySet()
111
              .forEach( entry -> node.appendChild( createMeta( doc, entry ) ) )
112
    );
113
    walk( doc, "/html/head/title", node -> node.setTextContent( title() ) );
114
  }
115
116
  /**
117
   * Generates document metadata, including word count.
118
   *
119
   * @param doc The document containing the text to tally.
120
   * @return A map of metadata key/value pairs.
121
   */
122
  private Map<String, String> createMetaData( final Document doc ) {
123
    return Map.of(
124
      "author", author(),
125
      "byline", byLine(),
126
      "address", address(),
127
      "phone", phone(),
128
      "email", email(),
129
      "count", wordCount( doc ),
130
      "keywords", keywords(),
131
      "copyright", copyright(),
132
      "date", date()
133
    );
134
  }
135
136
  /**
137
   * For a given src URI, this method will attempt to normalize it such that a
138
   * third-party application can find the file. Normalization could entail
139
   * downloading from the Internet or finding a suitable file name extension.
140
   *
141
   * @param src A path, local or remote, to a partial or complete file name.
142
   * @return A local file system path to the source path.
143
   * @throws Exception Could not read from, write to, or find a file.
144
   */
145
  private Path exportImage( final String src ) throws Exception {
146
    Path imageFile = null;
147
148
    final var protocol = getProtocol( src );
149
150
    // Download remote resources into temporary files.
151
    if( protocol.isRemote() ) {
152
      final var response = httpGet( src );
153
      final var mediaType = response.getMediaType();
154
155
      imageFile = mediaType.createTemporaryFile( APP_TITLE_LOWERCASE );
156
157
      try( final var image = response.getInputStream() ) {
158
        copy( image, imageFile, REPLACE_EXISTING );
159
      }
160
161
      // Strip comments, superfluous whitespace, DOCTYPE, and XML declarations.
162
      if( mediaType.isSvg() ) {
163
        DocumentParser.sanitize( imageFile );
164
      }
165
    }
166
    else {
167
      final var extensions = " " + getImageOrder().trim();
168
      var imagePath = getImagePath();
169
      var found = false;
170
171
      // By including " " in the extensions, the first element returned
172
      // will be the empty string. Thus the first extension to try is the
173
      // file's default extension. Subsequent iterations will try to find
174
      // a file that has a name matching one of the preferred extensions.
175
      for( final var extension : BLANK.split( extensions ) ) {
176
        final var filename = format(
177
          "%s%s%s", src, extension.isBlank() ? "" : ".", extension );
178
        imageFile = Path.of( imagePath, filename );
179
180
        if( imageFile.toFile().exists() ) {
181
          found = true;
182
          break;
183
        }
184
      }
185
186
      if( !found ) {
187
        imagePath = getDocumentDir().toString();
188
        imageFile = Path.of( imagePath, src );
189
190
        if( !imageFile.toFile().exists() ) {
191
          throw new FileNotFoundException( imageFile.toString() );
192
        }
193
      }
194
    }
195
196
    return imageFile;
197
  }
198
199
  private String getImagePath() {
200
    return getWorkspace().toFile( KEY_IMAGES_DIR ).toString();
201
  }
202
203
  private String getImageOrder() {
204
    return getWorkspace().toString( KEY_IMAGES_ORDER );
205
  }
206
207
  /**
208
   * Returns the absolute path to the document being edited, which can be used
209
   * to find files included using relative paths.
210
   *
211
   * @return The directory containing the edited file.
212
   */
213
  private Path getDocumentDir() {
214
    return mContext.getBaseDir();
215
  }
216
217
  private Workspace getWorkspace() {
218
    return mContext.getWorkspace();
219
  }
220
221
  private Locale locale() {return getWorkspace().getLocale();}
222
223
  private String title() {
224
    return resolve( KEY_DOC_TITLE );
225
  }
226
227
  private String author() {
228
    return resolve( KEY_DOC_AUTHOR );
229
  }
230
231
  private String byLine() {
232
    return resolve( KEY_DOC_BYLINE );
233
  }
234
235
  private String address() {
236
    return resolve( KEY_DOC_ADDRESS ).replaceAll( "\n", "\\\\\\break{}" );
237
  }
238
239
  private String phone() {
240
    return resolve( KEY_DOC_PHONE );
241
  }
242
243
  private String email() {
244
    return resolve( KEY_DOC_EMAIL );
245
  }
246
247
  private String wordCount( final Document doc ) {
248
    final var sb = new StringBuilder( 65536 * 10 );
249
250
    walk(
251
      doc,
252
      "//*[normalize-space( text() ) != '']",
253
      node -> sb.append( node.getTextContent() )
254
    );
255
256
    return valueOf( WordCounter.create( locale() ).count( sb.toString() ) );
257
  }
258
259
  private String keywords() {
260
    return resolve( KEY_DOC_KEYWORDS );
261
  }
262
263
  private String copyright() {
264
    return resolve( KEY_DOC_COPYRIGHT );
265
  }
266
267
  private String date() {
268
    return resolve( KEY_DOC_DATE );
269
  }
270
271
  /**
272
   * Answers whether straight quotation marks should be curled.
273
   *
274
   * @return {@code false} to prevent curling straight quotes.
275
   */
276
  private boolean curl() {
277
    return getWorkspace().toBoolean( KEY_TYPESET_TYPOGRAPHY_QUOTES );
278
  }
279
280
  private String resolve( final Key key ) {
281
    return replace( asString( key ), mContext.getResolvedMap() );
282
  }
283
284
  private String asString( final Key key ) {
285
    return stringProperty( key ).get();
286
  }
287
288
  private StringProperty stringProperty( final Key key ) {
289
    return getWorkspace().stringProperty( key );
290
  }
291
292
  /**
293
   * Creates contracts with a custom set of unambiguous strings.
294
   *
295
   * @return List of contractions to use for curling straight quotes.
296
   */
297
  private static Contractions contractions() {
298
    final var builder = new Contractions.Builder();
299
    return builder.withBeganUnambiguous( List.of( "bout" ) ).build();
300
  }
301
}
1302
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.processors.ExecutorProcessor;
5
import com.keenwrite.processors.Processor;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension;
8
import com.keenwrite.processors.markdown.extensions.r.RExtension;
9
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
10
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
11
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
12
import com.vladsch.flexmark.ext.tables.TablesExtension;
13
import com.vladsch.flexmark.html.HtmlRenderer;
14
import com.vladsch.flexmark.parser.Parser;
15
import com.vladsch.flexmark.util.ast.IParse;
16
import com.vladsch.flexmark.util.ast.IRender;
17
import com.vladsch.flexmark.util.ast.Node;
18
import com.vladsch.flexmark.util.misc.Extension;
19
20
import java.util.ArrayList;
21
import java.util.List;
22
23
/**
24
 * Responsible for parsing and rendering Markdown into HTML. This is required
25
 * to break a circular dependency between the {@link MarkdownProcessor} and
26
 * {@link RExtension}.
27
 */
28
public class BaseMarkdownProcessor extends ExecutorProcessor<String> {
29
30
  private final IParse mParser;
31
  private final IRender mRenderer;
32
33
  public BaseMarkdownProcessor(
34
    final Processor<String> successor, final ProcessorContext context ) {
35
    super( successor );
36
37
    final var builder = Parser.builder();
38
    final var extensions = createExtensions( context );
39
    mParser = builder.extensions( extensions ).build();
40
    mRenderer = HtmlRenderer.builder().extensions( extensions ).build();
41
  }
42
43
  /**
44
   * Instantiates a number of extensions to be applied when parsing.
45
   *
46
   * @param context The context that subclasses use to configure custom
47
   *                extension behaviour.
48
   * @return A {@link List} of {@link Extension} instances that change the
49
   * {@link Parser}'s behaviour.
50
   */
51
  List<Extension> createExtensions( final ProcessorContext context ) {
52
    final var extensions = new ArrayList<Extension>();
53
54
    extensions.add( DefinitionExtension.create() );
55
    extensions.add( StrikethroughSubscriptExtension.create() );
56
    extensions.add( SuperscriptExtension.create() );
57
    extensions.add( TablesExtension.create() );
58
    extensions.add( FencedDivExtension.create() );
59
60
    return extensions;
61
  }
62
63
  /**
64
   * Converts the given Markdown string into HTML, without the doctype, html,
65
   * head, and body tags.
66
   *
67
   * @param markdown The string to convert from Markdown to HTML.
68
   * @return The HTML representation of the Markdown document.
69
   */
70
  @Override
71
  public String apply( final String markdown ) {
72
    return toHtml( parse( markdown ) );
73
  }
74
75
  /**
76
   * Returns the AST in the form of a node for the given Markdown document. This
77
   * can be used, for example, to determine if a hyperlink exists inside of a
78
   * paragraph.
79
   *
80
   * @param markdown The Markdown to convert into an AST.
81
   * @return The Markdown AST for the given text (usually a paragraph).
82
   */
83
  public Node toNode( final String markdown ) {
84
    return parse( markdown );
85
  }
86
87
  /**
88
   * Returns the result of converting the given AST into an HTML string.
89
   *
90
   * @param node The AST {@link Node} to convert to an HTML string.
91
   * @return The given {@link Node} as an HTML string.
92
   */
93
  public String toHtml( final Node node ) {
94
    return getRenderer().render( node );
95
  }
96
97
  /**
98
   * Helper method to create an AST given some Markdown.
99
   *
100
   * @param markdown The Markdown to parse.
101
   * @return The root node of the Markdown tree.
102
   */
103
  private Node parse( final String markdown ) {
104
    return getParser().parse( markdown );
105
  }
106
107
  /**
108
   * Creates the Markdown document processor.
109
   *
110
   * @return An instance of {@link IParse} for building abstract syntax trees.
111
   */
112
  private IParse getParser() {
113
    return mParser;
114
  }
115
116
  private IRender getRenderer() {
117
    return mRenderer;
118
  }
119
}
1120
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.io.MediaType;
5
import com.keenwrite.processors.DefinitionProcessor;
6
import com.keenwrite.processors.Processor;
7
import com.keenwrite.processors.ProcessorContext;
8
import com.keenwrite.processors.markdown.extensions.*;
9
import com.keenwrite.processors.markdown.extensions.fences.FencedBlockExtension;
10
import com.keenwrite.processors.markdown.extensions.r.RExtension;
11
import com.keenwrite.processors.markdown.extensions.tex.TeXExtension;
12
import com.keenwrite.processors.r.RProcessor;
13
import com.vladsch.flexmark.util.misc.Extension;
14
15
import java.util.ArrayList;
16
import java.util.List;
17
18
import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN;
19
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
20
21
/**
22
 * Responsible for parsing a Markdown document and rendering it as HTML.
23
 */
24
public final class MarkdownProcessor extends BaseMarkdownProcessor {
25
26
  private MarkdownProcessor(
27
    final Processor<String> successor, final ProcessorContext context ) {
28
    super( successor, context );
29
  }
30
31
  public static MarkdownProcessor create( final ProcessorContext context ) {
32
    return create( IDENTITY, context );
33
  }
34
35
  public static MarkdownProcessor create(
36
    final Processor<String> successor, final ProcessorContext context ) {
37
    return new MarkdownProcessor( successor, context );
38
  }
39
40
  /**
41
   * Creating extensions based using an instance of {@link ProcessorContext}
42
   * indicates that the {@link CaretExtension} should be used to inject the
43
   * caret position into the final HTML document. This enables the HTML
44
   * preview pane to scroll to the same position, relatively speaking, within
45
   * the main document. Scrolling is developed this way to decouple the
46
   * document being edited from the preview pane so that multiple document
47
   * formats can be edited.
48
   *
49
   * @param context Contains necessary information needed to create
50
   *                extensions used by the Markdown parser.
51
   * @return {@link List} of extensions invoked when parsing Markdown.
52
   */
53
  @Override
54
  List<Extension> createExtensions( final ProcessorContext context ) {
55
    final var editorFile = context.getDocumentPath();
56
    final var mediaType = MediaType.valueFrom( editorFile );
57
    final Processor<String> processor;
58
    final List<Extension> extensions = new ArrayList<>();
59
60
    if( mediaType == TEXT_R_MARKDOWN ) {
61
      final var rProcessor = new RProcessor( context );
62
      extensions.add( RExtension.create( rProcessor, context ) );
63
      processor = rProcessor;
64
    }
65
    else {
66
      processor = new DefinitionProcessor( IDENTITY, context );
67
    }
68
69
    // Add typographic, table, strikethrough, and similar extensions.
70
    extensions.addAll( super.createExtensions( context ) );
71
72
    extensions.add( ImageLinkExtension.create( context ) );
73
    extensions.add( TeXExtension.create( processor, context ) );
74
    extensions.add( FencedBlockExtension.create( processor, context ) );
75
    extensions.add( CaretExtension.create( context ) );
76
    extensions.add( DocumentOutlineExtension.create( processor ) );
77
    return extensions;
78
  }
79
}
180
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.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 static com.keenwrite.constants.Constants.CARET_ID;
19
import static com.keenwrite.processors.markdown.extensions.EmptyNode.EMPTY_NODE;
20
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
21
22
/**
23
 * Responsible for giving most block-level elements a unique identifier
24
 * attribute. The identifier is used to coordinate scrolling.
25
 */
26
public class CaretExtension extends HtmlRendererAdapter {
27
28
  private final Caret mCaret;
29
30
  private CaretExtension( final ProcessorContext context ) {
31
    mCaret = context.getCaret();
32
  }
33
34
  public static CaretExtension create( final ProcessorContext context ) {
35
    return new CaretExtension( context );
36
  }
37
38
  @Override
39
  public void extend( @NotNull final Builder builder,
40
                      @NotNull final String rendererType ) {
41
    builder.attributeProviderFactory(
42
      IdAttributeProvider.createFactory( mCaret ) );
43
  }
44
45
  /**
46
   * Responsible for creating the id attribute. This class is instantiated
47
   * once: for the HTML element containing the {@link Constants#CARET_ID}.
48
   */
49
  public static class IdAttributeProvider implements AttributeProvider {
50
    private final Caret mCaret;
51
    private boolean mAdded;
52
53
    public IdAttributeProvider( final Caret caret ) {
54
      mCaret = caret;
55
    }
56
57
    private static AttributeProviderFactory createFactory( final Caret caret ) {
58
      return new IndependentAttributeProviderFactory() {
59
        @Override
60
        public @NotNull AttributeProvider apply(
61
          @NotNull final LinkResolverContext context ) {
62
          return new IdAttributeProvider( caret );
63
        }
64
      };
65
    }
66
67
    @Override
68
    public void setAttributes( @NotNull Node curr,
69
                               @NotNull AttributablePart part,
70
                               @NotNull MutableAttributes attributes ) {
71
      // Optimization: if a caret is inserted, don't try to find another.
72
      if( mAdded ) {
73
        return;
74
      }
75
76
      // If a table block has been earmarked with an empty node, it means
77
      // another extension has generated code from an external source. The
78
      // Markdown processor won't be able to determine the caret position
79
      // with any semblance of accuracy, so skip the element. This usually
80
      // happens with tables, but in theory any Markdown generated from an
81
      // external source (e.g., an R script) could produce text that has no
82
      // caret position that can be calculated.
83
      var table = curr;
84
85
      if( !(curr instanceof TableBlock) ) {
86
        table = curr.getAncestorOfType( TableBlock.class );
87
      }
88
89
      // The table was generated outside the document
90
      if( table != null && table.getLastChild() == EMPTY_NODE ) {
91
        return;
92
      }
93
94
      final var outside = mCaret.isAfterText() ? 1 : 0;
95
      final var began = curr.getStartOffset();
96
      final var ended = curr.getEndOffset() + outside;
97
      final var prev = curr.getPrevious();
98
99
      // If the caret is within the bounds of the current node or the
100
      // caret is within the bounds of the end of the previous node and
101
      // the start of the current node, then mark the current node with
102
      // a caret indicator.
103
      if( mCaret.isBetweenText( began, ended ) ||
104
        prev != null && mCaret.isBetweenText( prev.getEndOffset(), began ) ) {
105
        // This line empowers synchronizing the text editor with the preview.
106
        attributes.addValue( AttributeImpl.of( "id", CARET_ID ) );
107
108
        // We're done until the user moves the caret (micro-optimization)
109
        mAdded = true;
110
      }
111
    }
112
  }
113
}
1114
A src/main/java/com/keenwrite/processors/markdown/extensions/DocumentOutlineExtension.java
1
package com.keenwrite.processors.markdown.extensions;
2
3
import com.keenwrite.processors.Processor;
4
import com.vladsch.flexmark.ast.Heading;
5
import com.vladsch.flexmark.parser.Parser.Builder;
6
import com.vladsch.flexmark.parser.Parser.ParserExtension;
7
import com.vladsch.flexmark.parser.block.NodePostProcessor;
8
import com.vladsch.flexmark.parser.block.NodePostProcessorFactory;
9
import com.vladsch.flexmark.util.ast.Document;
10
import com.vladsch.flexmark.util.ast.Node;
11
import com.vladsch.flexmark.util.ast.NodeTracker;
12
import com.vladsch.flexmark.util.data.MutableDataHolder;
13
import org.jetbrains.annotations.NotNull;
14
15
import java.util.regex.Pattern;
16
17
import static com.keenwrite.events.ParseHeadingEvent.fireNewHeadingEvent;
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
        fireNewHeadingEvent( 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
  }
22
}
123
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.preferences.Workspace;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.vladsch.flexmark.ast.Image;
8
import com.vladsch.flexmark.html.IndependentLinkResolverFactory;
9
import com.vladsch.flexmark.html.LinkResolver;
10
import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
11
import com.vladsch.flexmark.html.renderer.ResolvedLink;
12
import com.vladsch.flexmark.util.ast.Node;
13
import org.jetbrains.annotations.NotNull;
14
15
import java.io.File;
16
import java.nio.file.Path;
17
18
import static com.keenwrite.ExportFormat.NONE;
19
import static com.keenwrite.events.StatusEvent.clue;
20
import static com.keenwrite.preferences.WorkspaceKeys.KEY_IMAGES_DIR;
21
import static com.keenwrite.preferences.WorkspaceKeys.KEY_IMAGES_ORDER;
22
import static com.keenwrite.util.ProtocolScheme.getProtocol;
23
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
24
import static com.vladsch.flexmark.html.renderer.LinkStatus.VALID;
25
import static org.renjin.repackaged.guava.base.Splitter.on;
26
27
/**
28
 * Responsible for ensuring that images can be rendered relative to a path.
29
 * This allows images to be located virtually anywhere.
30
 */
31
public class ImageLinkExtension extends HtmlRendererAdapter {
32
33
  private final Path mBaseDir;
34
  private final Workspace mWorkspace;
35
  private final ExportFormat mExportFormat;
36
37
  private ImageLinkExtension(
38
    @NotNull final ProcessorContext context ) {
39
    mBaseDir = context.getBaseDir();
40
    mWorkspace = context.getWorkspace();
41
    mExportFormat = context.getExportFormat();
42
  }
43
44
  /**
45
   * Creates an extension capable of using a relative path to embed images.
46
   *
47
   * @param context Contains the base directory to search in for images.
48
   * @return The new {@link ImageLinkExtension}, not {@code null}.
49
   */
50
  public static ImageLinkExtension create(
51
    @NotNull final ProcessorContext context ) {
52
    return new ImageLinkExtension( context );
53
  }
54
55
  @Override
56
  public void extend( @NotNull final Builder builder,
57
                      @NotNull final String rendererType ) {
58
    builder.linkResolverFactory( new ResolverFactory() );
59
  }
60
61
  private final class ResolverFactory extends IndependentLinkResolverFactory {
62
    @Override
63
    public @NotNull LinkResolver apply(
64
      @NotNull final LinkResolverBasicContext context ) {
65
      return new ImageLinkResolver();
66
    }
67
  }
68
69
  private class ImageLinkResolver implements LinkResolver {
70
    public ImageLinkResolver() {
71
    }
72
73
    @NotNull
74
    @Override
75
    public ResolvedLink resolveLink(
76
      @NotNull final Node node,
77
      @NotNull final LinkResolverBasicContext context,
78
      @NotNull final ResolvedLink link ) {
79
      return node instanceof Image ? forImage( link, node ) : link;
80
    }
81
82
    /**
83
     * Algorithm:
84
     * <ol>
85
     *   <li>Accept remote URLs as valid links.</li>
86
     *   <li>Accept existing readable files as valid links.</li>
87
     *   <li>Accept non-{@link ExportFormat#NONE} exports as valid links.</li>
88
     *   <li>Append the images dir to the edited file's dir (baseDir).</li>
89
     *   <li>Search for images by extension.</li>
90
     * </ol>
91
     *
92
     * @param link The link URL to resolve.
93
     * @param node The document node containing the URL.
94
     * @return The {@link ResolvedLink} instance used to render the link.
95
     */
96
    private ResolvedLink forImage( final ResolvedLink link, final Node node ) {
97
      var uri = link.getUrl();
98
      final var protocol = getProtocol( uri );
99
100
      if( protocol.isRemote() ) {
101
        return valid( link, uri );
102
      }
103
104
      final var baseDir = getBaseDir();
105
106
      // Determine the fully-qualified file name (fqfn).
107
      final var fqfn = Path.of( baseDir.toString(), uri ).toFile();
108
109
      if( fqfn.isFile() && fqfn.canRead() ) {
110
        return valid( link, uri );
111
      }
112
113
      if( mExportFormat != NONE ) {
114
        return valid( link, uri );
115
      }
116
117
      try {
118
        // Compute the path to the image file. The base directory should
119
        // be an absolute path to the file being edited, without an extension.
120
        final var imagesDir = getUserImagesDir();
121
        final var relativeDir = imagesDir.toString().isEmpty()
122
          ? imagesDir : baseDir.relativize( imagesDir );
123
        final var imageFile = Path.of(
124
          baseDir.toString(), relativeDir.toString(), uri );
125
126
        for( final var ext : getImageExtensions() ) {
127
          var file = new File( imageFile.toString() + '.' + ext );
128
129
          if( file.exists() && file.canRead() ) {
130
            uri = file.toURI().toString();
131
            return valid( link, uri );
132
          }
133
        }
134
135
        clue( "Main.status.error.file.missing.near",
136
              imageFile + ".*", node.getLineNumber()
137
        );
138
      } catch( final Exception ex ) {
139
        clue( ex );
140
      }
141
142
      return link;
143
    }
144
145
    private ResolvedLink valid( final ResolvedLink link, final String url ) {
146
      return link.withStatus( VALID ).withUrl( url );
147
    }
148
149
    private Path getUserImagesDir() {
150
      return mWorkspace.toFile( KEY_IMAGES_DIR ).toPath();
151
    }
152
153
    private Iterable<String> getImageExtensions() {
154
      return on( ' ' ).split( mWorkspace.toString( KEY_IMAGES_ORDER ) );
155
    }
156
157
    private Path getBaseDir() {
158
      return mBaseDir;
159
    }
160
  }
161
}
1162
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.preferences.Workspace;
5
import com.keenwrite.preview.DiagramUrlGenerator;
6
import com.keenwrite.processors.DefinitionProcessor;
7
import com.keenwrite.processors.Processor;
8
import com.keenwrite.processors.ProcessorContext;
9
import com.keenwrite.processors.markdown.MarkdownProcessor;
10
import com.keenwrite.processors.markdown.extensions.HtmlRendererAdapter;
11
import com.vladsch.flexmark.ast.FencedCodeBlock;
12
import com.vladsch.flexmark.html.renderer.DelegatingNodeRendererFactory;
13
import com.vladsch.flexmark.html.renderer.NodeRenderer;
14
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
15
import com.vladsch.flexmark.util.data.DataHolder;
16
import com.vladsch.flexmark.util.sequence.BasedSequence;
17
import org.jetbrains.annotations.NotNull;
18
19
import java.util.HashSet;
20
import java.util.Set;
21
22
import static com.keenwrite.preferences.WorkspaceKeys.KEY_IMAGES_SERVER;
23
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
24
import static com.vladsch.flexmark.html.renderer.LinkType.LINK;
25
26
/**
27
 * Responsible for converting textual diagram descriptions into HTML image
28
 * elements.
29
 */
30
public class FencedBlockExtension extends HtmlRendererAdapter {
31
  private final static String DIAGRAM_STYLE = "diagram-";
32
  private final static int DIAGRAM_STYLE_LEN = DIAGRAM_STYLE.length();
33
34
  private final Processor<String> mProcessor;
35
  private final Workspace mWorkspace;
36
37
  public FencedBlockExtension(
38
    final Processor<String> processor, final Workspace workspace ) {
39
    assert processor != null;
40
    assert workspace != null;
41
    mProcessor = processor;
42
    mWorkspace = workspace;
43
  }
44
45
  /**
46
   * Creates a new parser for fenced blocks. This calls out to a web service
47
   * to generate SVG files of text diagrams.
48
   * <p>
49
   * Internally, this creates a {@link DefinitionProcessor} to substitute
50
   * variable definitions. This is necessary because the order of processors
51
   * matters. If the {@link DefinitionProcessor} comes before an instance of
52
   * {@link MarkdownProcessor}, for example, then the caret position in the
53
   * preview pane will not align with the caret position in the editor
54
   * pane. The {@link MarkdownProcessor} must come before all else. However,
55
   * when parsing fenced blocks, the variables within the block must be
56
   * interpolated before being sent to the diagram web service.
57
   * </p>
58
   *
59
   * @param processor Used to pre-process the text.
60
   * @return A new {@link FencedBlockExtension} capable of shunting ASCII
61
   * diagrams to a service for conversion to SVG.
62
   */
63
  public static FencedBlockExtension create(
64
    final Processor<String> processor, final ProcessorContext context ) {
65
    assert processor != null;
66
    assert context != null;
67
    return new FencedBlockExtension( processor, context.getWorkspace() );
68
  }
69
70
  @Override
71
  public void extend(
72
    @NotNull final Builder builder, @NotNull final String rendererType ) {
73
    builder.nodeRendererFactory( new Factory() );
74
  }
75
76
  /**
77
   * Converts the given {@link BasedSequence} to a lowercase value.
78
   *
79
   * @param text The character string to convert to lowercase.
80
   * @return The lowercase text value, or the empty string for no text.
81
   */
82
  private static String sanitize( final BasedSequence text ) {
83
    assert text != null;
84
    return text.toString().toLowerCase();
85
  }
86
87
  /**
88
   * Responsible for generating images from a fenced block that contains a
89
   * diagram reference.
90
   */
91
  private class CustomRenderer implements NodeRenderer {
92
93
    @Override
94
    public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
95
      final var set = new HashSet<NodeRenderingHandler<?>>();
96
97
      set.add( new NodeRenderingHandler<>(
98
        FencedCodeBlock.class, ( node, context, html ) -> {
99
        final var style = sanitize( node.getInfo() );
100
101
        if( style.startsWith( DIAGRAM_STYLE ) ) {
102
          final var type = style.substring( DIAGRAM_STYLE_LEN );
103
          final var content = node.getContentChars().normalizeEOL();
104
          final var text = mProcessor.apply( content );
105
          final var server = mWorkspace.toString( KEY_IMAGES_SERVER );
106
          final var source = DiagramUrlGenerator.toUrl( server, type, text );
107
          final var link = context.resolveLink( LINK, source, false );
108
109
          html.attr( "src", source );
110
          html.withAttr( link );
111
          html.tagVoid( "img" );
112
        }
113
        else {
114
          context.delegateRender();
115
        }
116
      } ) );
117
118
      return set;
119
    }
120
  }
121
122
  private class Factory implements DelegatingNodeRendererFactory {
123
    public Factory() {}
124
125
    @NotNull
126
    @Override
127
    public NodeRenderer apply( @NotNull final DataHolder options ) {
128
      return new CustomRenderer();
129
    }
130
131
    /**
132
     * Return {@code null} to indicate this may delegate to the core renderer.
133
     */
134
    @Override
135
    public Set<Class<?>> getDelegates() {
136
      return null;
137
    }
138
  }
139
}
1140
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/RExtension.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.InlineRProcessor;
8
import com.keenwrite.processors.r.RProcessor;
9
import com.keenwrite.sigils.RSigilOperator;
10
import com.vladsch.flexmark.ast.Paragraph;
11
import com.vladsch.flexmark.ast.Text;
12
import com.vladsch.flexmark.parser.InlineParserExtensionFactory;
13
import com.vladsch.flexmark.parser.InlineParserFactory;
14
import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
15
import com.vladsch.flexmark.parser.internal.InlineParserImpl;
16
import com.vladsch.flexmark.parser.internal.LinkRefProcessorData;
17
import com.vladsch.flexmark.util.data.DataHolder;
18
import com.vladsch.flexmark.util.data.MutableDataHolder;
19
20
import java.util.BitSet;
21
import java.util.List;
22
import java.util.Map;
23
24
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
25
import static com.keenwrite.processors.markdown.extensions.EmptyNode.EMPTY_NODE;
26
import static com.vladsch.flexmark.parser.Parser.Builder;
27
import static com.vladsch.flexmark.parser.Parser.ParserExtension;
28
29
/**
30
 * Responsible for processing inline R statements (denoted using the
31
 * {@link RSigilOperator#PREFIX}) to prevent them from being converted to
32
 * HTML {@code <code>} elements and stop them from interfering with TeX
33
 * statements. Note that TeX statements are processed using a Markdown
34
 * extension, rather than an implementation of {@link Processor}. For this
35
 * reason, some pre-conversion is necessary.
36
 */
37
public final class RExtension implements ParserExtension {
38
  private final InlineParserFactory INLINE_FACTORY = InlineParser::new;
39
  private final RProcessor mProcessor;
40
  private final BaseMarkdownProcessor mMarkdownProcessor;
41
42
  private RExtension(
43
    final RProcessor processor, final ProcessorContext context ) {
44
    mProcessor = processor;
45
    mMarkdownProcessor = new BaseMarkdownProcessor( IDENTITY, context );
46
  }
47
48
  /**
49
   * Creates an extension capable of intercepting R code blocks and preventing
50
   * them from being converted into HTML {@code <code>} elements.
51
   */
52
  public static RExtension create(
53
    final RProcessor processor, final ProcessorContext context ) {
54
    return new RExtension( processor, context );
55
  }
56
57
  @Override
58
  public void extend( final Builder builder ) {
59
    builder.customInlineParserFactory( INLINE_FACTORY );
60
  }
61
62
  @Override
63
  public void parserOptions( final MutableDataHolder options ) {
64
  }
65
66
  /**
67
   * Prevents rendering {@code `r} statements as inline HTML {@code <code>}
68
   * blocks, which allows the {@link InlineRProcessor} to post-process the
69
   * text prior to display in the preview pane. This intervention assists
70
   * with decoupling the caret from the Markdown content so that the two
71
   * can vary independently in the architecture while permitting synchronization
72
   * of the editor and preview pane.
73
   * <p>
74
   * The text is therefore processed twice: once by flexmark-java and once by
75
   * {@link InlineRProcessor}.
76
   * </p>
77
   */
78
  private class InlineParser extends InlineParserImpl {
79
    private InlineParser(
80
      final DataHolder options,
81
      final BitSet specialCharacters,
82
      final BitSet delimiterCharacters,
83
      final Map<Character, DelimiterProcessor> delimiterProcessors,
84
      final LinkRefProcessorData referenceLinkProcessors,
85
      final List<InlineParserExtensionFactory> inlineParserExtensions ) {
86
      super( options,
87
             specialCharacters,
88
             delimiterCharacters,
89
             delimiterProcessors,
90
             referenceLinkProcessors,
91
             inlineParserExtensions );
92
      mProcessor.init();
93
    }
94
95
    /**
96
     * The superclass handles a number backtick parsing edge cases; this method
97
     * changes the behaviour to retain R code snippets, identified by
98
     * {@link RSigilOperator#PREFIX}, so that subsequent processing can
99
     * invoke R. If other languages are added, the {@link InlineParser} will
100
     * have to be rewritten to identify more than merely R.
101
     *
102
     * @return The return value from {@link super#parseBackticks()}.
103
     * @inheritDoc
104
     */
105
    @Override
106
    protected final boolean parseBackticks() {
107
      final var foundTicks = super.parseBackticks();
108
109
      if( foundTicks && mProcessor.isReady() ) {
110
        final var blockNode = getBlock();
111
        final var codeNode = blockNode.getLastChild();
112
113
        if( codeNode != null ) {
114
          final var code = codeNode.getChars().toString();
115
116
          if( code.startsWith( RSigilOperator.PREFIX ) ) {
117
            codeNode.unlink();
118
            final var rText = mProcessor.apply( code );
119
            var node = mMarkdownProcessor.toNode( rText );
120
121
            if( node.getFirstChild() instanceof Paragraph ) {
122
              node = new Text( rText );
123
            }
124
            else {
125
              node = node.getFirstChild();
126
127
              if( node != null ) {
128
                // Mark the node as being generated code, such as text returned
129
                // from an R function.
130
                node.appendChild( EMPTY_NODE );
131
              }
132
            }
133
134
            if( node != null ) {
135
              blockNode.appendChild( node );
136
            }
137
          }
138
        }
139
      }
140
141
      return foundTicks;
142
    }
143
  }
144
}
1145
A src/main/java/com/keenwrite/processors/markdown/extensions/r/ROutputProcessor.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.ExecutorProcessor;
5
import com.keenwrite.processors.r.InlineRProcessor;
6
import com.keenwrite.processors.markdown.MarkdownProcessor;
7
import com.keenwrite.processors.markdown.extensions.tex.TeXExtension;
8
import com.vladsch.flexmark.ast.Paragraph;
9
import com.vladsch.flexmark.ast.Text;
10
import com.vladsch.flexmark.html.HtmlRenderer;
11
import com.vladsch.flexmark.parser.Parser;
12
import com.vladsch.flexmark.util.ast.IParse;
13
import com.vladsch.flexmark.util.ast.IRender;
14
15
/**
16
 * Responsible for parsing the output from an R eval statement. This class
17
 * is used to avoid an circular dependency whereby the {@link InlineRProcessor}
18
 * must treat the output from an R function call as Markdown, which would
19
 * otherwise require a {@link MarkdownProcessor} instance; however, the
20
 * {@link MarkdownProcessor} class gives precedence to its extensions, which
21
 * means the {@link TeXExtension} will be executed <em>before</em> the
22
 * {@link InlineRProcessor}, thereby being exposed to backticks in a TeX
23
 * macro---a syntax error. To break the cycle, the {@link InlineRProcessor}
24
 * uses this class instead of {@link MarkdownProcessor}.
25
 */
26
public class ROutputProcessor extends ExecutorProcessor<String> {
27
  private final IParse mParser = Parser.builder().build();
28
  private final IRender mRenderer = HtmlRenderer.builder().build();
29
30
  @Override
31
  public String apply( final String markdown ) {
32
    var node = mParser.parse( markdown ).getFirstChild();
33
34
    if( node == null ) {
35
      node = new Text();
36
    }
37
    else if( node.isOrDescendantOfType( Paragraph.class ) ) {
38
      node = new Text( node.getChars() );
39
    }
40
41
    // Trimming prevents displaced commas and unwanted newlines.
42
    return mRenderer.render( node ).trim();
43
  }
44
}
145
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.Processor;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.keenwrite.processors.markdown.extensions.HtmlRendererAdapter;
8
import com.keenwrite.processors.markdown.extensions.tex.TexNodeRenderer.Factory;
9
import com.vladsch.flexmark.html.HtmlRenderer;
10
import com.vladsch.flexmark.parser.Parser;
11
import com.vladsch.flexmark.util.data.MutableDataHolder;
12
import org.jetbrains.annotations.NotNull;
13
14
import static com.vladsch.flexmark.parser.Parser.ParserExtension;
15
16
/**
17
 * Responsible for wrapping delimited TeX code in Markdown into an XML element
18
 * that the HTML renderer can handle. For example, {@code $E=mc^2$} becomes
19
 * {@code <tex>E=mc^2</tex>} when passed to HTML renderer. The HTML renderer
20
 * is responsible for converting the TeX code for display. This avoids inserting
21
 * SVG code into the Markdown document, which the parser would then have to
22
 * iterate---a <em>very</em> wasteful operation that impacts front-end
23
 * performance.
24
 */
25
public class TeXExtension extends HtmlRendererAdapter
26
  implements ParserExtension {
27
28
  /**
29
   * Responsible for pre-parsing the input.
30
   */
31
  private final Processor<String> mProcessor;
32
33
  /**
34
   * Controls how the node renderer produces TeX code within HTML output.
35
   */
36
  private final ExportFormat mExportFormat;
37
38
  private TeXExtension(
39
    final Processor<String> processor, final ProcessorContext context  ) {
40
    mProcessor = processor;
41
    mExportFormat = context.getExportFormat();
42
  }
43
44
  /**
45
   * Creates an extension capable of handling delimited TeX code in Markdown.
46
   *
47
   * @return The new {@link TeXExtension}, never {@code null}.
48
   */
49
  public static TeXExtension create(
50
    final Processor<String> processor, final ProcessorContext context  ) {
51
    return new TeXExtension( processor, context );
52
  }
53
54
  /**
55
   * Adds the TeX extension for HTML document export types.
56
   *
57
   * @param builder      The document builder.
58
   * @param rendererType Indicates the document type to be built.
59
   */
60
  @Override
61
  public void extend( @NotNull final HtmlRenderer.Builder builder,
62
                      @NotNull final String rendererType ) {
63
    if( "HTML".equalsIgnoreCase( rendererType ) ) {
64
      builder.nodeRendererFactory( new Factory( mExportFormat, mProcessor ) );
65
    }
66
  }
67
68
  @Override
69
  public void extend( final Parser.Builder builder ) {
70
    builder.customDelimiterProcessor( new TeXInlineDelimiterProcessor() );
71
  }
72
73
  @Override
74
  public void parserOptions( final MutableDataHolder options ) {
75
  }
76
}
177
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.SvgRasterizer;
6
import com.keenwrite.processors.Processor;
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
20
import static com.keenwrite.ExportFormat.*;
21
import static com.keenwrite.preview.MathRenderer.MATH_RENDERER;
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
      MARKDOWN_PLAIN, new TexDelimNodeRenderer(),
35
      NONE, RENDERER
36
    );
37
38
  public static class Factory implements NodeRendererFactory {
39
    private final RendererFacade mNodeRenderer;
40
41
    public Factory(
42
      final ExportFormat exportFormat, final Processor<String> processor ) {
43
      mNodeRenderer = EXPORT_RENDERERS.getOrDefault( exportFormat, RENDERER );
44
      mNodeRenderer.setProcessor( processor );
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 Processor<String> mProcessor;
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 setProcessor( final Processor<String> processor ) {
78
      mProcessor = processor;
79
    }
80
81
    Processor<String> getProcessor() {
82
      return mProcessor;
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 = getProcessor().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 = MATH_RENDERER.render(
121
        tex == null ? "" : getProcessor().apply( tex ) );
122
      final var svg = SvgRasterizer.toSvg( doc.getDocumentElement() );
123
      html.raw( svg );
124
    }
125
  }
126
127
  /**
128
   * Responsible for rendering a TeX node as text bracketed by $ tokens.
129
   */
130
  private static class TexDelimNodeRenderer extends RendererFacade {
131
    void render( final TexNode node,
132
                 final NodeRendererContext context,
133
                 final HtmlWriter html ) {
134
      html.raw( TOKEN_OPEN );
135
      html.raw( getProcessor().apply( node.getText().toString() ) );
136
      html.raw( TOKEN_CLOSE );
137
    }
138
  }
139
}
1140
A src/main/java/com/keenwrite/processors/r/InlineRProcessor.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.processors.DefinitionProcessor;
6
import com.keenwrite.processors.Processor;
7
import com.keenwrite.processors.ProcessorContext;
8
import com.keenwrite.processors.markdown.extensions.r.ROutputProcessor;
9
import com.keenwrite.util.BoundedCache;
10
import javafx.beans.property.Property;
11
12
import javax.script.ScriptEngine;
13
import javax.script.ScriptEngineManager;
14
import java.io.File;
15
import java.nio.file.Path;
16
import java.util.Map;
17
import java.util.concurrent.atomic.AtomicBoolean;
18
19
import static com.keenwrite.constants.Constants.STATUS_PARSE_ERROR;
20
import static com.keenwrite.Messages.get;
21
import static com.keenwrite.events.StatusEvent.clue;
22
import static com.keenwrite.preferences.WorkspaceKeys.*;
23
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
24
import static com.keenwrite.sigils.RSigilOperator.PREFIX;
25
import static com.keenwrite.sigils.RSigilOperator.SUFFIX;
26
import static java.lang.Math.max;
27
import static java.lang.Math.min;
28
import static java.lang.String.format;
29
30
/**
31
 * Transforms a document containing R statements into Markdown.
32
 */
33
public final class InlineRProcessor extends DefinitionProcessor {
34
  private final Processor<String> mPostProcessor = new ROutputProcessor();
35
36
  /**
37
   * Where to put document inline evaluated R expressions, constrained to
38
   * avoid running out of memory.
39
   */
40
  private final Map<String, String> mEvalCache =
41
    new BoundedCache<>( 512 );
42
43
  private static final ScriptEngine ENGINE =
44
    (new ScriptEngineManager()).getEngineByName( "Renjin" );
45
46
  private static final int PREFIX_LENGTH = PREFIX.length();
47
48
  private final AtomicBoolean mDirty = new AtomicBoolean( false );
49
50
  private final Workspace mWorkspace;
51
52
  /**
53
   * Constructs a processor capable of evaluating R statements.
54
   *
55
   * @param successor Subsequent link in the processing chain.
56
   * @param context   Contains resolved definitions map.
57
   */
58
  public InlineRProcessor(
59
    final Processor<String> successor,
60
    final ProcessorContext context ) {
61
    super( successor, context );
62
63
    mWorkspace = context.getWorkspace();
64
65
    bootstrapScriptProperty().addListener(
66
      ( __, oldScript, newScript ) -> setDirty( true ) );
67
    workingDirectoryProperty().addListener(
68
      ( __, oldScript, newScript ) -> setDirty( true ) );
69
70
    // TODO: Watch the "R" property keys in the workspace, directly.
71
72
    // If the user saves the preferences, make sure that any R-related settings
73
    // changes are applied.
74
//    getWorkspace().addSaveEventHandler( ( handler ) -> {
75
//      if( isDirty() ) {
76
//        init();
77
//        setDirty( false );
78
//      }
79
//    } );
80
81
    init();
82
  }
83
84
  /**
85
   * Initialises the R code so that R can find imported libraries. Note that
86
   * any existing R functionality will not be overwritten if this method is
87
   * called multiple times.
88
   *
89
   * @return {@code true} if initialization completed and all variables were
90
   * replaced; {@code false} if any variables remain.
91
   */
92
  public boolean init() {
93
    final var bootstrap = getBootstrapScript();
94
95
    if( !bootstrap.isBlank() ) {
96
      final var wd = getWorkingDirectory();
97
      final var dir = wd.toString().replace( '\\', '/' );
98
      final var map = getDefinitions();
99
      final var defBegan = mWorkspace.toString( KEY_DEF_DELIM_BEGAN );
100
      final var defEnded = mWorkspace.toString( KEY_DEF_DELIM_ENDED );
101
102
      map.put( defBegan + "application.r.working.directory" + defEnded, dir );
103
104
      final var replaced = replace( bootstrap, map );
105
      final var bIndex = replaced.indexOf( defBegan );
106
107
      // If there's a delimiter in the replaced text it means not all variables
108
      // are bound, which is an error.
109
      if( bIndex >= 0 ) {
110
        var eIndex = replaced.indexOf( defEnded );
111
        eIndex = (eIndex == -1) ? replaced.length() - 1 : max( bIndex, eIndex );
112
113
        final var def = replaced.substring(
114
          bIndex + defBegan.length(), eIndex );
115
        clue( "Main.status.error.bootstrap.eval",
116
              format( "%s%s%s", defBegan, def, defEnded ) );
117
118
        return false;
119
      }
120
      else {
121
        eval( replaced );
122
      }
123
    }
124
125
    return true;
126
  }
127
128
  /**
129
   * Empties the cache.
130
   */
131
  public void clear() {
132
    mEvalCache.clear();
133
  }
134
135
  /**
136
   * Sets the dirty flag to indicate that the bootstrap script or working
137
   * directory has been modified. Upon saving the preferences, if this flag
138
   * is true, then {@link #init()} will be called to reload the R environment.
139
   *
140
   * @param dirty Set to true to reload changes upon closing preferences.
141
   */
142
  private void setDirty( final boolean dirty ) {
143
    mDirty.set( dirty );
144
  }
145
146
  /**
147
   * Answers whether R-related settings have been modified.
148
   *
149
   * @return {@code true} when the settings have changed.
150
   */
151
  private boolean isDirty() {
152
    return mDirty.get();
153
  }
154
155
  /**
156
   * Evaluates all R statements in the source document and inserts the
157
   * calculated value into the generated document.
158
   *
159
   * @param text The document text that includes variables that should be
160
   *             replaced with values when rendered as HTML.
161
   * @return The generated document with output from all R statements
162
   * substituted with value returned from their execution.
163
   */
164
  @Override
165
  public String apply( final String text ) {
166
    final int length = text.length();
167
168
    // The * 2 is a wild guess at the ratio of R statements to the length
169
    // of text produced by those statements.
170
    final StringBuilder sb = new StringBuilder( length * 2 );
171
172
    int prevIndex = 0;
173
    int currIndex = text.indexOf( PREFIX );
174
175
    while( currIndex >= 0 ) {
176
      // Copy everything up to, but not including, the opening token.
177
      sb.append( text, prevIndex, currIndex );
178
179
      // Jump to the start of the R statement.
180
      prevIndex = currIndex + PREFIX_LENGTH;
181
182
      // Find the closing token, without indexing past the text boundary.
183
      currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) );
184
185
      // Only evaluate inline R statements that have end delimiters.
186
      if( currIndex > 1 ) {
187
        // Extract the inline R statement to be evaluated.
188
        final var r = text.substring( prevIndex, currIndex );
189
190
        // Pass the R statement into the R engine for evaluation.
191
        try {
192
          // Append the string representation of the result into the text.
193
          sb.append( evalCached( r ) );
194
        } catch( final Exception ex ) {
195
          // Inform the user that there was a problem.
196
          clue( STATUS_PARSE_ERROR, ex.getMessage(), currIndex );
197
198
          // If the string couldn't be parsed using R, append the statement
199
          // that failed to parse, instead of its evaluated value.
200
          sb.append( PREFIX ).append( r ).append( SUFFIX );
201
        }
202
203
        // Retain the R statement's ending position in the text.
204
        prevIndex = currIndex + 1;
205
      }
206
207
      // Find the start of the next inline R statement.
208
      currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) );
209
    }
210
211
    // Copy from the previous index to the end of the string.
212
    return sb.append( text.substring( min( prevIndex, length ) ) ).toString();
213
  }
214
215
  /**
216
   * Look up an R expression from the cache then return the resulting object.
217
   * If the R expression hasn't been cached, it'll first be evaluated.
218
   *
219
   * @param r The expression to evaluate.
220
   * @return The object resulting from the evaluation.
221
   */
222
  private String evalCached( final String r ) {
223
    return mEvalCache.computeIfAbsent( r, __ -> evalHtml( r ) );
224
  }
225
226
  /**
227
   * Converts the given string to HTML, trimming new lines, and inlining
228
   * the text if it is a paragraph. Otherwise, the resulting HTML is most likely
229
   * complex (e.g., a Markdown table) and should be rendered as its HTML
230
   * equivalent.
231
   *
232
   * @param r The R expression to evaluate then convert to HTML.
233
   * @return The result from the R expression as an HTML element.
234
   */
235
  private String evalHtml( final String r ) {
236
    return mPostProcessor.apply( eval( r ) );
237
  }
238
239
  /**
240
   * Evaluate an R expression and return the resulting object.
241
   *
242
   * @param r The expression to evaluate.
243
   * @return The object resulting from the evaluation.
244
   */
245
  private String eval( final String r ) {
246
    try {
247
      return ENGINE.eval( r ).toString();
248
    } catch( final Exception ex ) {
249
      final var expr = r.substring( 0, min( r.length(), 50 ) );
250
      clue( get( "Main.status.error.r", expr, ex.getMessage() ), ex );
251
      return "";
252
    }
253
  }
254
255
  /**
256
   * Return the given path if not {@code null}, otherwise return the path to
257
   * the user's directory.
258
   *
259
   * @return A non-null path.
260
   */
261
  private Path getWorkingDirectory() {
262
    return workingDirectoryProperty().getValue().toPath();
263
  }
264
265
  private Property<File> workingDirectoryProperty() {
266
    return getWorkspace().fileProperty( KEY_R_DIR );
267
  }
268
269
  /**
270
   * Loads the R init script from the application's persisted preferences.
271
   *
272
   * @return A non-null string, possibly empty.
273
   */
274
  private String getBootstrapScript() {
275
    return bootstrapScriptProperty().getValue();
276
  }
277
278
  private Property<String> bootstrapScriptProperty() {
279
    return getWorkspace().valuesProperty( KEY_R_SCRIPT );
280
  }
281
282
  private Workspace getWorkspace() {
283
    return mWorkspace;
284
  }
285
}
1286
A src/main/java/com/keenwrite/processors/r/RProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.r;
3
4
import com.keenwrite.processors.ExecutorProcessor;
5
import com.keenwrite.processors.Processor;
6
import com.keenwrite.processors.ProcessorContext;
7
8
import java.util.function.Function;
9
10
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
11
12
/**
13
 * Responsible for processing R statements within a text block.
14
 */
15
public final class RProcessor
16
  extends ExecutorProcessor<String> implements Function<String, String> {
17
  private final Processor<String> mProcessor;
18
  private final InlineRProcessor mInlineRProcessor;
19
20
  private boolean mReady;
21
22
  public RProcessor( final ProcessorContext context ) {
23
    final var irp = new InlineRProcessor( IDENTITY, context );
24
    final var rvp = new RVariableProcessor( irp, context );
25
    mProcessor = new ExecutorProcessor<>( rvp );
26
    mInlineRProcessor = irp;
27
  }
28
29
  public void init() {
30
    mReady = mInlineRProcessor.init();
31
  }
32
33
  public String apply( final String text ) {
34
    return mProcessor.apply( text );
35
  }
36
37
  public boolean isReady() {
38
    return mReady;
39
  }
40
}
141
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.preferences.Workspace;
5
import com.keenwrite.processors.DefinitionProcessor;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.keenwrite.sigils.RSigilOperator;
8
import com.keenwrite.sigils.SigilOperator;
9
import com.keenwrite.sigils.YamlSigilOperator;
10
11
import java.util.HashMap;
12
import java.util.Map;
13
14
import static com.keenwrite.preferences.WorkspaceKeys.*;
15
16
/**
17
 * Converts the keys of the resolved map from default form to R form, then
18
 * performs a substitution on the text. The default R variable syntax is
19
 * {@code v$tree$leaf}.
20
 */
21
public final class RVariableProcessor extends DefinitionProcessor {
22
23
  private final SigilOperator mSigilOperator;
24
25
  public RVariableProcessor(
26
    final InlineRProcessor irp, final ProcessorContext context ) {
27
    super( irp, context );
28
    mSigilOperator = createSigilOperator( context.getWorkspace() );
29
  }
30
31
  /**
32
   * Returns the R-based version of the interpolated variable definitions.
33
   *
34
   * @return Variable names transmogrified from the default syntax to R syntax.
35
   */
36
  @Override
37
  protected Map<String, String> getDefinitions() {
38
    return entoken( super.getDefinitions() );
39
  }
40
41
  /**
42
   * Converts the given map from regular variables to R variables.
43
   *
44
   * @param map Map of variable names to values.
45
   * @return Map of R variables.
46
   */
47
  private Map<String, String> entoken( final Map<String, String> map ) {
48
    final var rMap = new HashMap<String, String>( map.size() );
49
50
    for( final var entry : map.entrySet() ) {
51
      final var key = entry.getKey();
52
      rMap.put( mSigilOperator.entoken( key ), escape( map.get( key ) ) );
53
    }
54
55
    return rMap;
56
  }
57
58
  private String escape( final String value ) {
59
    return '\'' + escape( value, '\'', "\\'" ) + '\'';
60
  }
61
62
  /**
63
   * TODO: Make generic method for replacing text.
64
   *
65
   * @param haystack Search this string for the needle, must not be null.
66
   * @param needle   The character to find in the haystack.
67
   * @param thread   Replace the needle with this text, if the needle is found.
68
   * @return The haystack with the all instances of needle replaced with thread.
69
   */
70
  @SuppressWarnings( "SameParameterValue" )
71
  private String escape(
72
    final String haystack, final char needle, final String thread ) {
73
    int end = haystack.indexOf( needle );
74
75
    if( end < 0 ) {
76
      return haystack;
77
    }
78
79
    final int length = haystack.length();
80
    int start = 0;
81
82
    // Replace up to 32 occurrences before the string reallocates its buffer.
83
    final var sb = new StringBuilder( length + 32 );
84
85
    while( end >= 0 ) {
86
      sb.append( haystack, start, end ).append( thread );
87
      start = end + 1;
88
      end = haystack.indexOf( needle, start );
89
    }
90
91
    return sb.append( haystack.substring( start ) ).toString();
92
  }
93
94
  private SigilOperator createSigilOperator( final Workspace workspace ) {
95
    final var tokens = workspace.toTokens(
96
      KEY_R_DELIM_BEGAN, KEY_R_DELIM_ENDED );
97
    final var antecedent = createDefinitionOperator( workspace );
98
    return new RSigilOperator( tokens, antecedent );
99
  }
100
101
  private SigilOperator createDefinitionOperator( final Workspace workspace ) {
102
    final var tokens = workspace.toTokens(
103
      KEY_DEF_DELIM_BEGAN, KEY_DEF_DELIM_ENDED );
104
    return new YamlSigilOperator( tokens );
105
  }
106
}
1107
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
  /**
12
   * Default (empty) constructor.
13
   */
14
  protected AbstractTextReplacer() {
15
  }
16
17
  protected String[] keys( final Map<String, String> map ) {
18
    return map.keySet().toArray( new String[ 0 ] );
19
  }
20
21
  protected String[] values( final Map<String, String> map ) {
22
    return map.values().toArray( new String[ 0 ] );
23
  }
24
}
125
A src/main/java/com/keenwrite/processors/text/AhoCorasickReplacer.java
1
/* Copyright 2020-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
19
  @Override
20
  public String replace( final String text, final Map<String, String> map ) {
21
    // Create a buffer sufficiently large that re-allocations are minimized.
22
    final var sb = new StringBuilder( (int)(text.length() * 1.25) );
23
24
    // Definition names cannot overlap.
25
    final var builder = builder().ignoreOverlaps();
26
    builder.addKeywords( keys( map ) );
27
28
    int index = 0;
29
30
    // Replace all instances with dereferenced variables.
31
    for( final var emit : builder.build().parseText( text ) ) {
32
      sb.append( text, index, emit.getStart() );
33
      sb.append( map.get( emit.getKeyword() ) );
34
      index = emit.getEnd() + 1;
35
    }
36
37
    // Add the remainder of the string (contains no more matches).
38
    sb.append( text.substring( index ) );
39
40
    return sb.toString();
41
  }
42
}
143
A src/main/java/com/keenwrite/processors/text/StringUtilsReplacer.java
1
/* Copyright 2020-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.apache.commons.lang3.StringUtils.replaceEach;
7
8
/**
9
 * Replaces text using Apache's StringUtils.replaceEach method.
10
 */
11
public class StringUtilsReplacer extends AbstractTextReplacer {
12
13
  /**
14
   * Default (empty) constructor.
15
   */
16
  protected StringUtilsReplacer() {
17
  }
18
19
  @Override
20
  public String replace( final String text, final Map<String, String> map ) {
21
    return replaceEach( text, keys( map ), values( map ) );
22
  }
23
}
124
A src/main/java/com/keenwrite/processors/text/TextReplacementFactory.java
1
/* Copyright 2020-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 StringUtils implementation is slower
24
    // than the Aho-Corsick algorithm implementation.
25
    return length < 1500 ? APACHE : AHO_CORASICK;
26
  }
27
28
  /**
29
   * Convenience method to instantiate a suitable text replacer algorithm and
30
   * perform a replacement using the given map. At this point, the values should
31
   * be already dereferenced and ready to be substituted verbatim; any
32
   * recursively defined values must have been interpolated previously.
33
   *
34
   * @param text The text containing zero or more variables to replace.
35
   * @param map  The map of variables to their dereferenced values.
36
   * @return The text with all variables replaced.
37
   */
38
  public static String replace(
39
      final String text, final Map<String, String> map ) {
40
    return getTextReplacer( text.length() ).replace( text, map );
41
  }
42
}
143
A src/main/java/com/keenwrite/processors/text/TextReplacer.java
1
/* Copyright 2020-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/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 = createProperties();
25
26
  public DefaultSettings() {
27
  }
28
29
  /**
30
   * Returns the value of a string property.
31
   *
32
   * @param property     The property key.
33
   * @param defaultValue The value to return if no property key has been set.
34
   * @return The property key value, or defaultValue when no key found.
35
   */
36
  @Override
37
  public String getSetting( final String property, final String defaultValue ) {
38
    return getSettings().getString( property, defaultValue );
39
  }
40
41
  /**
42
   * Returns the value of a string property.
43
   *
44
   * @param property     The property key.
45
   * @param defaultValue The value to return if no property key has been set.
46
   * @return The property key value, or defaultValue when no key found.
47
   */
48
  @Override
49
  public int getSetting( final String property, final int defaultValue ) {
50
    return getSettings().getInt( property, defaultValue );
51
  }
52
53
  /**
54
   * Convert the generic list of property objects into strings.
55
   *
56
   * @param property The property value to coerce.
57
   * @param defaults The defaults values to use should the property be unset.
58
   * @return The list of properties coerced from objects to strings.
59
   */
60
  @Override
61
  public List<String> getStringSettingList(
62
      final String property, final List<String> defaults ) {
63
    return getSettings().getList( String.class, property, defaults );
64
  }
65
66
  /**
67
   * Convert a list of property objects into strings, with no default value.
68
   *
69
   * @param property The property value to coerce.
70
   * @return The list of properties coerced from objects to strings.
71
   */
72
  @Override
73
  public List<String> getStringSettingList( final String property ) {
74
    return getStringSettingList( property, null );
75
  }
76
77
  /**
78
   * Returns a list of property names that begin with the given prefix.
79
   *
80
   * @param prefix The prefix to compare against each property name.
81
   * @return The list of property names that have the given prefix.
82
   */
83
  @Override
84
  public Iterator<String> getKeys( final String prefix ) {
85
    return getSettings().getKeys( prefix );
86
  }
87
88
  private PropertiesConfiguration createProperties() {
89
    final var url = getPropertySource();
90
    final var configuration = new PropertiesConfiguration();
91
92
    if( url != null ) {
93
      try( final var reader = new InputStreamReader(
94
          url.openStream(), getDefaultEncoding() ) ) {
95
        configuration.setListDelimiterHandler( createListDelimiterHandler() );
96
        configuration.read( reader );
97
      } catch( final Exception ex ) {
98
        throw new RuntimeException( ex );
99
      }
100
    }
101
102
    return configuration;
103
  }
104
105
  protected Charset getDefaultEncoding() {
106
    return Charset.defaultCharset();
107
  }
108
109
  protected ListDelimiterHandler createListDelimiterHandler() {
110
    return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
111
  }
112
113
  private URL getPropertySource() {
114
    return DefaultSettings.class.getResource( PATH_PROPERTIES_SETTINGS );
115
  }
116
117
  private PropertiesConfiguration getSettings() {
118
    return mProperties;
119
  }
120
}
1121
A src/main/java/com/keenwrite/sigils/RSigilOperator.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.sigils;
3
4
import static com.keenwrite.sigils.YamlSigilOperator.KEY_SEPARATOR_DEF;
5
6
/**
7
 * Brackets variable names between {@link #PREFIX} and {@link #SUFFIX} sigils.
8
 */
9
public final class RSigilOperator extends SigilOperator {
10
  private static final char KEY_SEPARATOR_R = '$';
11
12
  public static final String PREFIX = "`r#";
13
  public static final char SUFFIX = '`';
14
15
  /**
16
   * Definition variables are inserted into the document before R variables,
17
   * so this is required to reformat the definition variable suitable for R.
18
   */
19
  private final SigilOperator mAntecedent;
20
21
  /**
22
   * Constructs a new {@link RSigilOperator} capable of wrapping tokens around
23
   * variable names (keys).
24
   *
25
   * @param tokens     The starting and ending tokens.
26
   * @param antecedent The operator to use to undo any previous entokenizing.
27
   */
28
  public RSigilOperator( final Tokens tokens, final SigilOperator antecedent ) {
29
    super( tokens );
30
31
    mAntecedent = antecedent;
32
  }
33
34
  /**
35
   * Returns the given string with backticks prepended and appended. The
36
   *
37
   * @param key The string to adorn with R token delimiters.
38
   * @return PREFIX + delimiterBegan + variableName + delimiterEnded + SUFFIX.
39
   */
40
  @Override
41
  public String apply( final String key ) {
42
    assert key != null;
43
    return PREFIX + getBegan() + key + getEnded() + SUFFIX;
44
  }
45
46
  /**
47
   * Transforms a definition key (bracketed by token delimiters) into the
48
   * expected format for an R variable key name.
49
   * <p>
50
   * The algorithm to entoken a definition name is faster than
51
   * {@link String#replace(char, char)}. Faster still would be to cache the
52
   * values, but that would mean managing the cache when the user changes
53
   * the beginning and ending of the R delimiters. This code gives about a
54
   * 2% performance boost when scrolling using cursor keys. After the JIT
55
   * warms up, this super-minor bottleneck vanishes.
56
   * </p>
57
   *
58
   * @param key The variable name to transform, can be empty but not null.
59
   * @return The transformed variable name.
60
   */
61
  public String entoken( final String key ) {
62
    final var detokened = new StringBuilder( key.length() );
63
    detokened.append( "v$" );
64
    detokened.append( mAntecedent.detoken( key ) );
65
66
    // The 3 is for "v$X" where X cannot be a period.
67
    for( int i = detokened.length() - 1; i >= 3; i-- ) {
68
      if( detokened.charAt( i ) == KEY_SEPARATOR_DEF ) {
69
        detokened.setCharAt( i, KEY_SEPARATOR_R );
70
      }
71
    }
72
73
    return detokened.toString();
74
  }
75
}
176
A src/main/java/com/keenwrite/sigils/SigilOperator.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.sigils;
3
4
import java.util.function.UnaryOperator;
5
6
/**
7
 * Responsible for updating definition keys to use a machine-readable format
8
 * corresponding to the type of file being edited. This changes a definition
9
 * key name based on some criteria determined by the factory that creates
10
 * implementations of this interface.
11
 */
12
public abstract class SigilOperator implements UnaryOperator<String> {
13
  private final Tokens mTokens;
14
15
  SigilOperator( final Tokens tokens ) {
16
    mTokens = tokens;
17
  }
18
19
  /**
20
   * Removes start and stop definition key delimiters from the given key. This
21
   * method does not check for delimiters, only that there are sufficient
22
   * characters to remove from either end of the given key.
23
   *
24
   * @param key The key adorned with start and stop tokens.
25
   * @return The given key with the delimiters removed.
26
   */
27
  String detoken( final String key ) {
28
    return key;
29
  }
30
31
  String getBegan() {
32
    return mTokens.getBegan();
33
  }
34
35
  String getEnded() {
36
    return mTokens.getEnded();
37
  }
38
39
  /**
40
   * Wraps the given key in the began and ended tokens. This may perform any
41
   * preprocessing necessary to ensure the transformation happens.
42
   *
43
   * @param key The variable name to transform.
44
   * @return The given key with tokens to delimit it (from the edited text).
45
   */
46
  public abstract String entoken( final String key );
47
}
148
A src/main/java/com/keenwrite/sigils/Tokens.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.sigils;
3
4
import javafx.beans.property.StringProperty;
5
6
import java.util.AbstractMap.SimpleImmutableEntry;
7
8
/**
9
 * Convenience class for pairing a start and an end sigil together.
10
 */
11
public final class Tokens
12
  extends SimpleImmutableEntry<StringProperty, StringProperty> {
13
14
  /**
15
   * Associates a new key-value pair.
16
   *
17
   * @param began The starting sigil.
18
   * @param ended The ending sigil.
19
   */
20
  public Tokens( final StringProperty began, final StringProperty ended ) {
21
    super( began, ended );
22
  }
23
24
  /**
25
   * @return The opening sigil token.
26
   */
27
  public String getBegan() {
28
    return getKey().get();
29
  }
30
31
  /**
32
   * @return The closing sigil token, or the empty string if none set.
33
   */
34
  public String getEnded() {
35
    return getValue().get();
36
  }
37
}
138
A src/main/java/com/keenwrite/sigils/YamlSigilOperator.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.sigils;
3
4
/**
5
 * Brackets definition keys with token delimiters.
6
 */
7
public final class YamlSigilOperator extends SigilOperator {
8
  public static final char KEY_SEPARATOR_DEF = '.';
9
10
  public YamlSigilOperator( final Tokens tokens ) {
11
    super( tokens );
12
  }
13
14
  /**
15
   * Returns the given {@link String} verbatim because variables in YAML
16
   * documents and plain Markdown documents already have the appropriate
17
   * tokenizable syntax wrapped around the text.
18
   *
19
   * @param key Returned verbatim.
20
   */
21
  @Override
22
  public String apply( final String key ) {
23
    return key;
24
  }
25
26
  /**
27
   * Adds delimiters to the given key.
28
   *
29
   * @param key The key to adorn with start and stop definition tokens.
30
   * @return The given key bracketed by definition token symbols.
31
   */
32
  public String entoken( final String key ) {
33
    assert key != null;
34
    return getBegan() + key + getEnded();
35
  }
36
37
  /**
38
   * Removes start and stop definition key delimiters from the given key.
39
   *
40
   * @param key The key that may have start and stop tokens.
41
   * @return The given key with the delimiters removed.
42
   */
43
  public String detoken( final String key ) {
44
    final var began = getBegan();
45
    final var ended = getEnded();
46
    final int bLength = began.length();
47
    final int eLength = ended.length();
48
    final var bIndex = key.indexOf( began );
49
    final var eIndex = key.indexOf( ended, bIndex );
50
    final var kLength = key.length();
51
52
    return key.substring(
53
      bIndex == -1 ? 0 : bLength, eIndex == -1 ? kLength : kLength - eLength );
54
  }
55
}
156
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/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.exceptions.MissingFileException;
5
import com.keenwrite.spelling.api.SpellCheckListener;
6
import com.keenwrite.spelling.api.SpellChecker;
7
import io.gitlab.rxp90.jsymspell.SuggestItem;
8
import io.gitlab.rxp90.jsymspell.SymSpell;
9
import io.gitlab.rxp90.jsymspell.SymSpellBuilder;
10
11
import java.io.BufferedReader;
12
import java.io.InputStreamReader;
13
import java.text.BreakIterator;
14
import java.util.ArrayList;
15
import java.util.Collection;
16
import java.util.List;
17
import java.util.stream.Collectors;
18
19
import static com.keenwrite.constants.Constants.LEXICONS_DIRECTORY;
20
import static com.keenwrite.events.StatusEvent.clue;
21
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity;
22
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.ALL;
23
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.CLOSEST;
24
import static java.lang.Character.isLetter;
25
import static java.nio.charset.StandardCharsets.UTF_8;
26
27
/**
28
 * Responsible for spell checking using {@link SymSpell}.
29
 */
30
public class SymSpellSpeller implements SpellChecker {
31
  private final BreakIterator mBreakIterator = BreakIterator.getWordInstance();
32
  private final SymSpell mSymSpell;
33
34
  /**
35
   * Creates a new spellchecker for a lexicon of words found in the specified
36
   * file.
37
   *
38
   * @param filename Lexicon language file (e.g., "en.txt").
39
   * @return An instance of {@link SpellChecker} that can check if a word
40
   * is correct and suggest alternatives, or {@link PermissiveSpeller} if the
41
   * lexicon cannot be loaded.
42
   */
43
  public static SpellChecker forLexicon( final String filename ) {
44
    try {
45
      final var lexicon = readLexicon( filename );
46
      return SymSpellSpeller.forLexicon( lexicon );
47
    } catch( final Exception ex ) {
48
      clue( ex );
49
      return new PermissiveSpeller();
50
    }
51
  }
52
53
  private static SpellChecker forLexicon(
54
    final Collection<String> lexiconWords ) {
55
    assert lexiconWords != null && !lexiconWords.isEmpty();
56
57
    final var builder = new SymSpellBuilder()
58
      .setLexiconWords( lexiconWords );
59
60
    return new SymSpellSpeller( builder.build() );
61
  }
62
63
  /**
64
   * Prevent direct instantiation so that only the {@link SpellChecker}
65
   * interface
66
   * is available.
67
   *
68
   * @param symSpell The implementation-specific spell checker.
69
   */
70
  private SymSpellSpeller( final SymSpell symSpell ) {
71
    mSymSpell = symSpell;
72
  }
73
74
  @Override
75
  public boolean inLexicon( final String lexeme ) {
76
    return lookup( lexeme, CLOSEST ).size() == 1;
77
  }
78
79
  @Override
80
  public List<String> suggestions( final String lexeme, int count ) {
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, final SpellCheckListener consumer ) {
98
    assert text != null;
99
    assert consumer != null;
100
101
    mBreakIterator.setText( text );
102
103
    int boundaryIndex = mBreakIterator.first();
104
    int previousIndex = 0;
105
106
    while( boundaryIndex != BreakIterator.DONE ) {
107
      final var lex =
108
        text.substring( previousIndex, boundaryIndex ).toLowerCase();
109
110
      // Get the lexeme for the possessive.
111
      final var pos = lex.endsWith( "'s" ) || lex.endsWith( "’s" );
112
      final var lexeme = pos ? lex.substring( 0, lex.length() - 2 ) : lex;
113
114
      if( isWord( lexeme ) && !inLexicon( lexeme ) ) {
115
        consumer.accept( lex, previousIndex, boundaryIndex );
116
      }
117
118
      previousIndex = boundaryIndex;
119
      boundaryIndex = mBreakIterator.next();
120
    }
121
  }
122
123
  @SuppressWarnings( "SameParameterValue" )
124
  private static Collection<String> readLexicon( final String filename )
125
    throws Exception {
126
    final var path = '/' + LEXICONS_DIRECTORY + '/' + filename;
127
128
    try( final var resource =
129
           SymSpellSpeller.class.getResourceAsStream( path ) ) {
130
      if( resource == null ) {
131
        throw new MissingFileException( path );
132
      }
133
134
      try( final var isr = new InputStreamReader( resource, UTF_8 );
135
           final var reader = new BufferedReader( isr ) ) {
136
        return reader.lines().collect( Collectors.toList() );
137
      }
138
    }
139
  }
140
141
  /**
142
   * Answers whether the given string is likely a word by checking the first
143
   * character.
144
   *
145
   * @param word The word to check.
146
   * @return {@code true} if the word begins with a letter.
147
   */
148
  private boolean isWord( final String word ) {
149
    return !word.isEmpty() && isLetter( word.charAt( 0 ) );
150
  }
151
152
  /**
153
   * Returns a list of {@link SuggestItem} instances that provide alternative
154
   * spellings for the given lexeme.
155
   *
156
   * @param lexeme A word to look up in the lexicon.
157
   * @param v      Influences the number of results returned.
158
   * @return Alternative lexemes.
159
   */
160
  private List<SuggestItem> lookup( final String lexeme, final Verbosity v ) {
161
    return mSymSpell.lookup( lexeme, v );
162
  }
163
}
1164
A src/main/java/com/keenwrite/spelling/impl/TextEditorSpeller.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 com.vladsch.flexmark.parser.Parser;
7
import com.vladsch.flexmark.util.ast.NodeVisitor;
8
import com.vladsch.flexmark.util.ast.VisitHandler;
9
import org.fxmisc.richtext.StyleClassedTextArea;
10
import org.fxmisc.richtext.model.StyleSpansBuilder;
11
12
import java.util.Collection;
13
import java.util.concurrent.atomic.AtomicInteger;
14
15
import static com.keenwrite.spelling.impl.SymSpellSpeller.forLexicon;
16
import static java.util.Collections.emptyList;
17
import static java.util.Collections.singleton;
18
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
19
20
/**
21
 * Responsible for checking the spelling of a document being edited.
22
 */
23
public final class TextEditorSpeller {
24
  /**
25
   * Only load the dictionary into memory once, because it's huge.
26
   */
27
  private static final SpellChecker sSpellChecker = forLexicon( "en.txt" );
28
29
  private final Parser mParser;
30
31
  public TextEditorSpeller() {
32
     mParser = Parser.builder().build();
33
  }
34
35
  /**
36
   * Delegates to {@link #spellcheck(StyleClassedTextArea, String, int)}.
37
   * call to spell check the entire document.
38
   */
39
  public void checkDocument( final StyleClassedTextArea editor ) {
40
    spellcheck( editor, editor.getText(), -1 );
41
  }
42
43
  /**
44
   * Listen for changes to the any particular paragraph and perform a quick
45
   * spell check upon it. The style classes in the editor will be changed to
46
   * mark any spelling mistakes in the paragraph. The user may then interact
47
   * with any misspelled word (i.e., any piece of text that is marked) to
48
   * revise the spelling.
49
   *
50
   * @param editor The text area containing paragraphs to spellcheck.
51
   */
52
  public void checkParagraphs( final StyleClassedTextArea editor ) {
53
    // Use the plain text changes so that notifications of style changes
54
    // are suppressed. Checking against the identity ensures that only
55
    // new text additions or deletions trigger proofreading.
56
    editor.plainTextChanges()
57
          .filter( p -> !p.isIdentity() ).subscribe( change -> {
58
59
      // Check current paragraph; the whole document was checked upon opening.
60
      final var offset = change.getPosition();
61
      final var position = editor.offsetToPosition( offset, Forward );
62
      final var paraId = position.getMajor();
63
      final var paragraph = editor.getParagraph( paraId );
64
      final var text = paragraph.getText();
65
66
      // Prevent doubling-up styles.
67
      editor.clearStyle( paraId );
68
69
      spellcheck( editor, text, paraId );
70
    } );
71
  }
72
73
  /**
74
   * Spellchecks a subset of the entire document.
75
   *
76
   * @param text   Look up words for this text in the lexicon.
77
   * @param paraId Set to -1 to apply resulting style spans to the entire
78
   *               text.
79
   */
80
  private void spellcheck(
81
    final StyleClassedTextArea editor, final String text, final int paraId ) {
82
    final var builder = new StyleSpansBuilder<Collection<String>>();
83
    final var runningIndex = new AtomicInteger( 0 );
84
85
    // The text nodes must be relayed through a contextual "visitor" that
86
    // can return text in chunks with correlative offsets into the string.
87
    // This allows Markdown, R Markdown, XML, and R XML documents to return
88
    // sets of words to check.
89
90
    final var node = mParser.parse( text );
91
    final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> {
92
      // Treat hyphenated compound words as individual words.
93
      final var check = visited.replace( '-', ' ' );
94
95
      sSpellChecker.proofread( check, ( misspelled, prevIndex, currIndex ) -> {
96
        prevIndex += bIndex;
97
        currIndex += bIndex;
98
99
        // Clear styling between lexiconically absent words.
100
        builder.add( emptyList(), prevIndex - runningIndex.get() );
101
        builder.add( singleton( "spelling" ), currIndex - prevIndex );
102
        runningIndex.set( currIndex );
103
      } );
104
    } );
105
106
    visitor.visit( node );
107
108
    // If the running index was set, at least one word triggered the listener.
109
    if( runningIndex.get() > 0 ) {
110
      // Clear styling after the last lexiconically absent word.
111
      builder.add( emptyList(), text.length() - runningIndex.get() );
112
113
      final var spans = builder.create();
114
115
      if( paraId >= 0 ) {
116
        editor.setStyleSpans( paraId, 0, spans );
117
      }
118
      else {
119
        editor.setStyleSpans( 0, spans );
120
      }
121
    }
122
  }
123
124
  /**
125
   * TODO: #59 -- Replace with generic interface; provide Markdown/XML
126
   * implementations.
127
   */
128
  private static final class TextVisitor {
129
    private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>(
130
      com.vladsch.flexmark.ast.Text.class, this::visit )
131
    );
132
133
    private final SpellCheckListener mConsumer;
134
135
    public TextVisitor( final SpellCheckListener consumer ) {
136
      mConsumer = consumer;
137
    }
138
139
    private void visit( final com.vladsch.flexmark.util.ast.Node node ) {
140
      if( node instanceof com.vladsch.flexmark.ast.Text ) {
141
        mConsumer.accept( node.getChars().toString(),
142
                          node.getStartOffset(),
143
                          node.getEndOffset() );
144
      }
145
146
      mVisitor.visitChildren( node );
147
    }
148
  }
149
}
1150
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/Typesetter.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.typesetting;
3
4
import com.keenwrite.io.SysFile;
5
import com.keenwrite.preferences.Workspace;
6
import com.keenwrite.util.BoundedCache;
7
8
import java.io.*;
9
import java.nio.file.NoSuchFileException;
10
import java.nio.file.Path;
11
import java.util.ArrayList;
12
import java.util.List;
13
import java.util.Map;
14
import java.util.Scanner;
15
import java.util.concurrent.Callable;
16
import java.util.regex.Pattern;
17
18
import static com.keenwrite.constants.Constants.DEFAULT_DIRECTORY;
19
import static com.keenwrite.events.StatusEvent.clue;
20
import static com.keenwrite.preferences.WorkspaceKeys.*;
21
import static java.lang.ProcessBuilder.Redirect.DISCARD;
22
import static java.lang.String.format;
23
import static java.lang.System.currentTimeMillis;
24
import static java.lang.System.getProperty;
25
import static java.nio.file.Files.*;
26
import static java.util.Arrays.asList;
27
import static java.util.concurrent.TimeUnit.*;
28
import static org.apache.commons.io.FilenameUtils.removeExtension;
29
30
/**
31
 * Responsible for invoking an executable to typeset text. This will
32
 * construct suitable command-line arguments to invoke the typesetting engine.
33
 */
34
public class Typesetter {
35
  private static final SysFile TYPESETTER = new SysFile( "mtxrun" );
36
37
  private final Workspace mWorkspace;
38
39
  /**
40
   * Creates a new {@link Typesetter} instance capable of configuring the
41
   * typesetter used to generate a typeset document.
42
   */
43
  public Typesetter( final Workspace workspace ) {
44
    mWorkspace = workspace;
45
  }
46
47
  public static boolean canRun() {
48
    return TYPESETTER.canRun();
49
  }
50
51
  /**
52
   * This will typeset the document using a new process. The return value only
53
   * indicates whether the typesetter exists, not whether the typesetting was
54
   * successful.
55
   *
56
   * @param in  The input document to typeset.
57
   * @param out Path to the finished typeset document.
58
   * @throws IOException                 If the process could not be started.
59
   * @throws InterruptedException        If the process was killed.
60
   * @throws TypesetterNotFoundException When no typesetter is along the PATH.
61
   */
62
  public void typeset( final Path in, final Path out )
63
    throws IOException, InterruptedException, TypesetterNotFoundException {
64
    if( TYPESETTER.canRun() ) {
65
      clue( "Main.status.typeset.began", out );
66
      final var task = new TypesetTask( in, out );
67
      final var time = currentTimeMillis();
68
      final var success = task.typeset();
69
70
      clue( "Main.status.typeset.ended." + (success ? "success" : "failure"),
71
            out, since( time )
72
      );
73
    }
74
    else {
75
      throw new TypesetterNotFoundException( TYPESETTER.toString() );
76
    }
77
  }
78
79
  /**
80
   * Calculates the time that has elapsed from the current time to the
81
   * given moment in time.
82
   *
83
   * @param start The starting time, which should be before the current time.
84
   * @return A human-readable formatted time.
85
   * @see #asElapsed(long)
86
   */
87
  private static String since( final long start ) {
88
    return asElapsed( currentTimeMillis() - start );
89
  }
90
91
  /**
92
   * Converts an elapsed time to a human-readable format (hours, minutes,
93
   * seconds, and milliseconds).
94
   *
95
   * @param elapsed An elapsed time, in milliseconds.
96
   * @return Human-readable elapsed time.
97
   */
98
  private static String asElapsed( final long elapsed ) {
99
    final var hours = MILLISECONDS.toHours( elapsed );
100
    final var eHours = elapsed - HOURS.toMillis( hours );
101
    final var minutes = MILLISECONDS.toMinutes( eHours );
102
    final var eMinutes = eHours - MINUTES.toMillis( minutes );
103
    final var seconds = MILLISECONDS.toSeconds( eMinutes );
104
    final var eSeconds = eMinutes - SECONDS.toMillis( seconds );
105
    final var milliseconds = MILLISECONDS.toMillis( eSeconds );
106
107
    return format( "%02d:%02d:%02d.%03d",
108
                   hours, minutes, seconds, milliseconds );
109
  }
110
111
  /**
112
   * Launches a task to typeset a document.
113
   */
114
  private class TypesetTask implements Callable<Boolean> {
115
    private final List<String> mArgs = new ArrayList<>();
116
    private final Path mInput;
117
    private final Path mOutput;
118
119
    /**
120
     * Working directory must be set because ConTeXt cannot write the
121
     * result to an arbitrary location.
122
     */
123
    private final Path mDirectory;
124
125
    private TypesetTask( final Path input, final Path output ) {
126
      assert input != null;
127
      assert output != null;
128
129
      final var parentDir = output.getParent();
130
      mInput = input;
131
      mOutput = output;
132
      mDirectory = parentDir == null ? DEFAULT_DIRECTORY : parentDir;
133
    }
134
135
    /**
136
     * Initializes ConTeXt, which means creating the cache directory if it
137
     * doesn't already exist. The theme entry point must be named 'main.tex'.
138
     *
139
     * @return {@code true} if the cache directory exists.
140
     */
141
    private boolean reinitialize() {
142
      final var filename = mOutput.getFileName();
143
      final var themes = getThemesPath();
144
      final var theme = getThemesSelection();
145
      final var cacheExists = !isEmpty( getCacheDir().toPath() );
146
147
      // Ensure invoking multiple times will load the correct arguments.
148
      mArgs.clear();
149
      mArgs.add( TYPESETTER.getName() );
150
151
      if( cacheExists ) {
152
        mArgs.add( "--autogenerate" );
153
        mArgs.add( "--script" );
154
        mArgs.add( "mtx-context" );
155
        mArgs.add( "--batchmode" );
156
        mArgs.add( "--nonstopmode" );
157
        mArgs.add( "--purgeall" );
158
        mArgs.add( "--path='" + Path.of( themes.toString(), theme ) + "'" );
159
        mArgs.add( "--environment='main'" );
160
        mArgs.add( "--result='" + filename + "'" );
161
        mArgs.add( mInput.toString() );
162
163
        final var sb = new StringBuilder( 128 );
164
        mArgs.forEach( arg -> sb.append( arg ).append( " " ) );
165
        clue( sb.toString() );
166
      }
167
      else {
168
        mArgs.add( "--generate" );
169
      }
170
171
      return cacheExists;
172
    }
173
174
    /**
175
     * Setting {@code TEXMFCACHE} when run on a fresh system fails on the first
176
     * try. If the cache directory doesn't exist, attempt to create it, then
177
     * call ConTeXt to generate the PDF. This is brittle because if the
178
     * directory is empty, or not populated with cached data, a false positive
179
     * will be returned, resulting in no PDF being created.
180
     *
181
     * @return {@code true} if the document was typeset successfully.
182
     * @throws IOException          If the process could not be started.
183
     * @throws InterruptedException If the process was killed.
184
     */
185
    private boolean typeset() throws IOException, InterruptedException {
186
      return reinitialize() ? call() : call() && reinitialize() && call();
187
    }
188
189
    @Override
190
    public Boolean call() throws IOException, InterruptedException {
191
      final var stdout = new BoundedCache<String, String>( 150 );
192
      final var builder = new ProcessBuilder( mArgs );
193
      builder.directory( mDirectory.toFile() );
194
      builder.environment().put( "TEXMFCACHE", getCacheDir().toString() );
195
196
      // Without redirecting (or draining) stderr, the command may not
197
      // terminate successfully.
198
      builder.redirectError( DISCARD );
199
200
      final var process = builder.start();
201
      final var stream = process.getInputStream();
202
203
      // Reading from stdout allows slurping page numbers while generating.
204
      final var listener = new PaginationListener( stream, stdout );
205
      listener.start();
206
207
      // Even though the process has completed, there may be incomplete I/O.
208
      process.waitFor();
209
210
      // Allow time for any incomplete I/O to take place.
211
      process.waitFor( 1, SECONDS );
212
213
      final var exit = process.exitValue();
214
      process.destroy();
215
216
      // If there was an error, the typesetter will leave behind log, pdf, and
217
      // error files.
218
      if( exit > 0 ) {
219
        final var xmlName = mInput.getFileName().toString();
220
        final var srcName = mOutput.getFileName().toString();
221
        final var logName = newExtension( xmlName, ".log" );
222
        final var errName = newExtension( xmlName, "-error.log" );
223
        final var pdfName = newExtension( xmlName, ".pdf" );
224
        final var tuaName = newExtension( xmlName, ".tua" );
225
        final var badName = newExtension( srcName, ".log" );
226
227
        log( badName );
228
        log( logName );
229
        log( errName );
230
        log( stdout.keySet().stream().toList() );
231
232
        // Users may opt to keep these files around for debugging purposes.
233
        if( autoclean() ) {
234
          deleteIfExists( logName );
235
          deleteIfExists( errName );
236
          deleteIfExists( pdfName );
237
          deleteIfExists( badName );
238
          deleteIfExists( tuaName );
239
        }
240
      }
241
242
      // Exit value for a successful invocation of the typesetter. This value
243
      // value is returned when creating the cache on the first run as well as
244
      // creating PDFs on subsequent runs (after the cache has been created).
245
      // Users don't care about exit codes, only whether the PDF was generated.
246
      return exit == 0;
247
    }
248
249
    private Path newExtension( final String baseName, final String ext ) {
250
      return mOutput.resolveSibling( removeExtension( baseName ) + ext );
251
    }
252
253
    /**
254
     * Fires a status message for each line in the given file. The file format
255
     * is somewhat machine-readable, but no effort beyond line splitting is
256
     * made to parse the text.
257
     *
258
     * @param path Path to the file containing error messages.
259
     */
260
    private void log( final Path path ) throws IOException {
261
      if( exists( path ) ) {
262
        log( readAllLines( path ) );
263
      }
264
    }
265
266
    private void log( final List<String> lines ) {
267
      final var splits = new ArrayList<String>( lines.size() * 2 );
268
269
      for( final var line : lines ) {
270
        splits.addAll( asList( line.split( "\\\\n" ) ) );
271
      }
272
273
      clue( splits );
274
    }
275
276
    /**
277
     * Returns the location of the cache directory.
278
     *
279
     * @return A fully qualified path to the location to store temporary
280
     * files between typesetting runs.
281
     */
282
    private java.io.File getCacheDir() {
283
      final var temp = getProperty( "java.io.tmpdir" );
284
      final var cache = Path.of( temp, "luatex-cache" );
285
      return cache.toFile();
286
    }
287
288
    /**
289
     * Answers whether the given directory is empty. The typesetting software
290
     * creates a non-empty directory by default. The return value from this
291
     * method is a proxy to answering whether the typesetter has been run for
292
     * the first time or not.
293
     *
294
     * @param path The directory to check for emptiness.
295
     * @return {@code true} if the directory is empty.
296
     */
297
    private boolean isEmpty( final Path path ) {
298
      try( final var stream = newDirectoryStream( path ) ) {
299
        return !stream.iterator().hasNext();
300
      } catch( final NoSuchFileException | FileNotFoundException ex ) {
301
        // A missing directory means it doesn't exist, ergo is empty.
302
        return true;
303
      } catch( final IOException ex ) {
304
        throw new RuntimeException( ex );
305
      }
306
    }
307
  }
308
309
  /**
310
   * Responsible for parsing the output from the typesetting engine and
311
   * updating the status bar to provide assurance that typesetting is
312
   * executing.
313
   *
314
   * <p>
315
   * Example lines written to standard output:
316
   * </p>
317
   * <pre>{@code
318
   * pages           > flushing realpage 15, userpage 15, subpage 15
319
   * pages           > flushing realpage 16, userpage 16, subpage 16
320
   * pages           > flushing realpage 1, userpage 1, subpage 1
321
   * pages           > flushing realpage 2, userpage 2, subpage 2
322
   * }</pre>
323
   * <p>
324
   * The lines are parsed; the first number is displayed in a status bar
325
   * message.
326
   * </p>
327
   */
328
  private static class PaginationListener extends Thread {
329
    private static final Pattern DIGITS = Pattern.compile( "[^\\d]+" );
330
331
    private final InputStream mInputStream;
332
333
    private final Map<String, String> mCache;
334
335
    public PaginationListener(
336
      final InputStream in, final Map<String, String> cache ) {
337
      mInputStream = in;
338
      mCache = cache;
339
    }
340
341
    @Override
342
    public void run() {
343
      try( final var reader = createReader( mInputStream ) ) {
344
        int pageCount = 1;
345
        int passCount = 1;
346
        int pageTotal = 0;
347
        String line;
348
349
        while( (line = reader.readLine()) != null ) {
350
          mCache.put( line, "" );
351
352
          if( line.startsWith( "pages" ) ) {
353
            // The bottleneck will be the typesetting engine writing to stdout,
354
            // not the parsing of stdout.
355
            final var scanner = new Scanner( line ).useDelimiter( DIGITS );
356
            final var digits = scanner.next();
357
            final var page = Integer.parseInt( digits );
358
359
            // If the page number is less than the previous page count, it
360
            // means that the typesetting engine has started another pass.
361
            if( page < pageCount ) {
362
              passCount++;
363
              pageTotal = pageCount;
364
            }
365
366
            pageCount = page;
367
368
            // Inform the user of pages being typeset.
369
            clue( "Main.status.typeset.page",
370
                  pageCount, pageTotal < 1 ? "?" : pageTotal, passCount
371
            );
372
          }
373
        }
374
      } catch( final IOException ex ) {
375
        clue( ex );
376
        throw new RuntimeException( ex );
377
      }
378
    }
379
380
    private BufferedReader createReader( final InputStream inputStream ) {
381
      return new BufferedReader( new InputStreamReader( inputStream ) );
382
    }
383
  }
384
385
  private File getThemesPath() {
386
    return mWorkspace.toFile( KEY_TYPESET_CONTEXT_THEMES_PATH );
387
  }
388
389
  private String getThemesSelection() {
390
    return mWorkspace.toString( KEY_TYPESET_CONTEXT_THEME_SELECTION );
391
  }
392
393
  /**
394
   * Answers whether logs and other files should be deleted upon error. The
395
   * log files are useful for debugging.
396
   *
397
   * @return {@code true} to delete generated files.
398
   */
399
  public boolean autoclean() {
400
    return mWorkspace.toBoolean( KEY_TYPESET_CONTEXT_CLEAN );
401
  }
402
}
1403
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/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
  public Action(
32
    final String text,
33
    final String accelerator,
34
    final String icon,
35
    final EventHandler<ActionEvent> handler ) {
36
    assert text != null;
37
    assert handler != null;
38
39
    mText = text;
40
    mAccelerator = accelerator == null ? null : valueOf( accelerator );
41
    mIcon = icon;
42
    mHandler = handler;
43
  }
44
45
  @Override
46
  public MenuItem createMenuItem() {
47
    // This will either become a menu or a menu item, depending on whether
48
    // sub-actions are defined.
49
    final MenuItem menuItem;
50
51
    if( mSubActions.isEmpty() ) {
52
      // Regular menu item has no sub-menus.
53
      menuItem = new MenuItem( mText );
54
    }
55
    else {
56
      // Sub-actions are translated into sub-menu items beneath this action.
57
      final var submenu = new Menu( mText );
58
59
      for( final var action : mSubActions ) {
60
        // Recursive call that creates a sub-menu hierarchy.
61
        submenu.getItems().add( action.createMenuItem() );
62
      }
63
64
      menuItem = submenu;
65
    }
66
67
    if( mAccelerator != null ) {
68
      menuItem.setAccelerator( mAccelerator );
69
    }
70
71
    if( mIcon != null ) {
72
      menuItem.setGraphic( createGraphic( mIcon ) );
73
    }
74
75
    menuItem.setOnAction( mHandler );
76
77
    return menuItem;
78
  }
79
80
  @Override
81
  public Button createToolBarNode() {
82
    final var button = createIconButton();
83
    var tooltip = mText;
84
85
    if( tooltip.endsWith( "..." ) ) {
86
      tooltip = tooltip.substring( 0, tooltip.length() - 3 );
87
    }
88
89
    // Do not display mnemonic accelerator character in tooltip text.
90
    // The accelerator key will still be available, this is display-only.
91
    tooltip = tooltip.replace( "_", "" );
92
93
    if( mAccelerator != null ) {
94
      tooltip += " (" + mAccelerator.getDisplayText() + ')';
95
    }
96
97
    button.setTooltip( new Tooltip( tooltip ) );
98
    button.setFocusTraversable( false );
99
    button.setOnAction( mHandler );
100
101
    return button;
102
  }
103
104
  private Button createIconButton() {
105
    return new Button( null, createGraphic( mIcon ) );
106
  }
107
108
  /**
109
   * Adds subordinate actions to the menu. This is used to establish sub-menu
110
   * relationships. The default behaviour does not wire up any registration;
111
   * subclasses are responsible for handling how actions relate to one another.
112
   *
113
   * @param action Actions that only exist with respect to this action.
114
   */
115
  public MenuAction addSubActions( final MenuAction... action ) {
116
    mSubActions.addAll( List.of( action ) );
117
    return this;
118
  }
119
120
  /**
121
   * TODO: Reuse the {@link GenericBuilder}.
122
   *
123
   * @return The {@link Builder} for an instance of {@link Action}.
124
   */
125
  public static Builder builder() {
126
    return new Builder();
127
  }
128
129
  /**
130
   * Provides a fluent interface around constructing actions so that duplication
131
   * can be avoided.
132
   */
133
  public static class Builder {
134
    private String mText;
135
    private String mAccelerator;
136
    private String mIcon;
137
    private EventHandler<ActionEvent> mHandler;
138
139
    /**
140
     * Sets the text, icon, and accelerator for a given action identifier.
141
     * See the messages properties file for details.
142
     *
143
     * @param id The identifier to look up in the properties file.
144
     * @return An instance of {@link Builder} that can be built into an
145
     * instance of {@link Action}.
146
     */
147
    public Builder setId( final String id ) {
148
      final var prefix = ACTION_PREFIX + id + ".";
149
      final var text = prefix + "text";
150
      final var icon = prefix + "icon";
151
      final var accelerator = prefix + "accelerator";
152
      final var builder = setText( text ).setIcon( icon );
153
154
      return Messages.containsKey( accelerator )
155
        ? builder.setAccelerator( Messages.get( accelerator ) )
156
        : builder;
157
    }
158
159
    /**
160
     * Sets the action text based on a resource bundle key.
161
     *
162
     * @param key The key to look up in the {@link Messages}.
163
     * @return The corresponding value, or the key name if none found.
164
     */
165
    private Builder setText( final String key ) {
166
      mText = Messages.get( key, key );
167
      return this;
168
    }
169
170
    private Builder setAccelerator( final String accelerator ) {
171
      mAccelerator = accelerator;
172
      return this;
173
    }
174
175
    private Builder setIcon( final String iconKey ) {
176
      assert iconKey != null;
177
178
      // If there's no icon associated with the icon key name, don't attempt
179
      // to create a graphic for the icon, because it won't exist.
180
      final var iconName = Messages.get( iconKey );
181
      mIcon = iconKey.equals( iconName ) ? "" : iconName;
182
183
      return this;
184
    }
185
186
    public Builder setHandler( final EventHandler<ActionEvent> handler ) {
187
      mHandler = handler;
188
      return this;
189
    }
190
191
    public Action build() {
192
      return new Action( mText, mAccelerator, mIcon, mHandler );
193
    }
194
  }
195
}
1196
A src/main/java/com/keenwrite/ui/actions/ApplicationActions.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.ExportFailedEvent;
12
import com.keenwrite.preferences.PreferencesController;
13
import com.keenwrite.preferences.Workspace;
14
import com.keenwrite.processors.markdown.MarkdownProcessor;
15
import com.keenwrite.search.SearchModel;
16
import com.keenwrite.typesetting.Typesetter;
17
import com.keenwrite.ui.controls.SearchBar;
18
import com.keenwrite.ui.dialogs.ImageDialog;
19
import com.keenwrite.ui.dialogs.LinkDialog;
20
import com.keenwrite.ui.dialogs.ThemePicker;
21
import com.keenwrite.ui.explorer.FilePicker;
22
import com.keenwrite.ui.explorer.FilePickerFactory;
23
import com.keenwrite.ui.logging.LogView;
24
import com.keenwrite.util.AlphanumComparator;
25
import com.vladsch.flexmark.ast.Link;
26
import javafx.concurrent.Task;
27
import javafx.scene.control.Alert;
28
import javafx.scene.control.Dialog;
29
import javafx.stage.Window;
30
import javafx.stage.WindowEvent;
31
32
import java.io.File;
33
import java.io.IOException;
34
import java.nio.file.Path;
35
import java.util.ArrayList;
36
import java.util.List;
37
import java.util.Optional;
38
import java.util.concurrent.ExecutorService;
39
40
import static com.keenwrite.Bootstrap.*;
41
import static com.keenwrite.ExportFormat.*;
42
import static com.keenwrite.Messages.get;
43
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE;
44
import static com.keenwrite.events.StatusEvent.clue;
45
import static com.keenwrite.preferences.WorkspaceKeys.KEY_TYPESET_CONTEXT_THEMES_PATH;
46
import static com.keenwrite.preferences.WorkspaceKeys.KEY_TYPESET_CONTEXT_THEME_SELECTION;
47
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
48
import static com.keenwrite.ui.explorer.FilePickerFactory.Options;
49
import static com.keenwrite.ui.explorer.FilePickerFactory.Options.*;
50
import static com.keenwrite.util.FileWalker.walk;
51
import static java.nio.file.Files.readString;
52
import static java.nio.file.Files.writeString;
53
import static java.util.concurrent.Executors.newFixedThreadPool;
54
import static javafx.application.Platform.runLater;
55
import static javafx.event.Event.fireEvent;
56
import static javafx.scene.control.Alert.AlertType.INFORMATION;
57
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
58
import static org.apache.commons.io.FilenameUtils.getExtension;
59
60
/**
61
 * Responsible for abstracting how functionality is mapped to the application.
62
 * This allows users to customize accelerator keys and will provide pluggable
63
 * functionality so that different text markup languages can change documents
64
 * using their respective syntax.
65
 */
66
public final class ApplicationActions {
67
  private static final ExecutorService sExecutor = newFixedThreadPool( 1 );
68
69
  private static final String STYLE_SEARCH = "search";
70
71
  /**
72
   * Sci-fi genres, which are can be longer than other genres, typically fall
73
   * below 150,000 words at 6 chars per word. This reduces re-allocations of
74
   * memory when concatenating files together when exporting novels.
75
   */
76
  private static final int DOCUMENT_LENGTH = 150_000 * 6;
77
78
  /**
79
   * When an action is executed, this is one of the recipients.
80
   */
81
  private final MainPane mMainPane;
82
83
  private final MainScene mMainScene;
84
85
  private final LogView mLogView;
86
87
  /**
88
   * Tracks finding text in the active document.
89
   */
90
  private final SearchModel mSearchModel;
91
92
  public ApplicationActions( final MainScene scene, final MainPane pane ) {
93
    mMainScene = scene;
94
    mMainPane = pane;
95
    mLogView = new LogView();
96
    mSearchModel = new SearchModel();
97
    mSearchModel.matchOffsetProperty().addListener( ( c, o, n ) -> {
98
      final var editor = getActiveTextEditor();
99
100
      // Clear highlighted areas before highlighting a new region.
101
      if( o != null ) {
102
        editor.unstylize( STYLE_SEARCH );
103
      }
104
105
      if( n != null ) {
106
        editor.moveTo( n.getStart() );
107
        editor.stylize( n, STYLE_SEARCH );
108
      }
109
    } );
110
111
    // When the active text editor changes, update the haystack.
112
    mMainPane.activeTextEditorProperty().addListener(
113
      ( c, o, n ) -> mSearchModel.search( getActiveTextEditor().getText() )
114
    );
115
  }
116
117
  public void file_new() {
118
    getMainPane().newTextEditor();
119
  }
120
121
  public void file_open() {
122
    pickFiles( FILE_OPEN_MULTIPLE ).ifPresent( l -> getMainPane().open( l ) );
123
  }
124
125
  public void file_close() {
126
    getMainPane().close();
127
  }
128
129
  public void file_close_all() {
130
    getMainPane().closeAll();
131
  }
132
133
  public void file_save() {
134
    getMainPane().save();
135
  }
136
137
  public void file_save_as() {
138
    pickFiles( FILE_SAVE_AS ).ifPresent( l -> getMainPane().saveAs( l ) );
139
  }
140
141
  public void file_save_all() {
142
    getMainPane().saveAll();
143
  }
144
145
  /**
146
   * Converts the actively edited file in the given file format.
147
   *
148
   * @param format The destination file format.
149
   */
150
  private void file_export( final ExportFormat format ) {
151
    file_export( format, false );
152
  }
153
154
  /**
155
   * Converts one or more files into the given file format. If {@code dir}
156
   * is set to true, this will first append all files in the same directory
157
   * as the actively edited file.
158
   *
159
   * @param format The destination file format.
160
   * @param dir    Export all files in the actively edited file's directory.
161
   */
162
  private void file_export( final ExportFormat format, final boolean dir ) {
163
    final var main = getMainPane();
164
    final var editor = main.getActiveTextEditor();
165
    final var filename = format.toExportFilename( editor.getPath() );
166
    final var selection = pickFiles( filename, FILE_EXPORT );
167
168
    selection.ifPresent( ( files ) -> {
169
      final var file = files.get( 0 );
170
      final var path = file.toPath();
171
      final var document = dir ? append( editor ) : editor.getText();
172
      final var context = main.createProcessorContext( path, format );
173
174
      final var task = new Task<Path>() {
175
        @Override
176
        protected Path call() throws Exception {
177
          final var chain = createProcessors( context );
178
          final var export = chain.apply( document );
179
180
          // Processors can export binary files. In such cases, processors
181
          // return null to prevent further processing.
182
          return export == null ? null : writeString( path, export );
183
        }
184
      };
185
186
      task.setOnSucceeded(
187
        e -> {
188
          final var result = task.getValue();
189
190
          // Binary formats must notify users of success independently.
191
          if( result != null ) {
192
            clue( "Main.status.export.success", result );
193
          }
194
        }
195
      );
196
197
      task.setOnFailed( e -> {
198
        final var ex = task.getException();
199
        clue( ex );
200
201
        if( ex instanceof TypeNotPresentException ) {
202
          fireExportFailedEvent();
203
        }
204
      } );
205
206
      sExecutor.execute( task );
207
    } );
208
  }
209
210
  /**
211
   * @param dir {@code true} means to export all files in the active file
212
   *            editor's directory; {@code false} means to export only the
213
   *            actively edited file.
214
   */
215
  private void file_export_pdf( final boolean dir ) {
216
    final var workspace = getWorkspace();
217
    final var themes = workspace.toFile( KEY_TYPESET_CONTEXT_THEMES_PATH );
218
    final var theme = workspace.stringProperty(
219
      KEY_TYPESET_CONTEXT_THEME_SELECTION );
220
221
    if( Typesetter.canRun() ) {
222
      // If the typesetter is installed, allow the user to select a theme. If
223
      // the themes aren't installed, a status message will appear.
224
      if( ThemePicker.choose( themes, theme ) ) {
225
        file_export( APPLICATION_PDF, dir );
226
      }
227
    }
228
    else {
229
      fireExportFailedEvent();
230
    }
231
  }
232
233
  public void file_export_pdf() {
234
    file_export_pdf( false );
235
  }
236
237
  public void file_export_pdf_dir() {
238
    file_export_pdf( true );
239
  }
240
241
  public void file_export_html_svg() {
242
    file_export( HTML_TEX_SVG );
243
  }
244
245
  public void file_export_html_tex() {
246
    file_export( HTML_TEX_DELIMITED );
247
  }
248
249
  public void file_export_xhtml_tex() {
250
    file_export( XHTML_TEX );
251
  }
252
253
  public void file_export_markdown() {
254
    file_export( MARKDOWN_PLAIN );
255
  }
256
257
  private void fireExportFailedEvent() {
258
    runLater( ExportFailedEvent::fireExportFailedEvent );
259
  }
260
261
  public void file_exit() {
262
    final var window = getWindow();
263
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
264
  }
265
266
  public void edit_undo() {
267
    getActiveTextEditor().undo();
268
  }
269
270
  public void edit_redo() {
271
    getActiveTextEditor().redo();
272
  }
273
274
  public void edit_cut() {
275
    getActiveTextEditor().cut();
276
  }
277
278
  public void edit_copy() {
279
    getActiveTextEditor().copy();
280
  }
281
282
  public void edit_paste() {
283
    getActiveTextEditor().paste();
284
  }
285
286
  public void edit_select_all() {
287
    getActiveTextEditor().selectAll();
288
  }
289
290
  public void edit_find() {
291
    final var nodes = getMainScene().getStatusBar().getLeftItems();
292
293
    if( nodes.isEmpty() ) {
294
      final var searchBar = new SearchBar();
295
296
      searchBar.matchIndexProperty().bind( mSearchModel.matchIndexProperty() );
297
      searchBar.matchCountProperty().bind( mSearchModel.matchCountProperty() );
298
299
      searchBar.setOnCancelAction( ( event ) -> {
300
        final var editor = getActiveTextEditor();
301
        nodes.remove( searchBar );
302
        editor.unstylize( STYLE_SEARCH );
303
        editor.getNode().requestFocus();
304
      } );
305
306
      searchBar.addInputListener( ( c, o, n ) -> {
307
        if( n != null && !n.isEmpty() ) {
308
          mSearchModel.search( n, getActiveTextEditor().getText() );
309
        }
310
      } );
311
312
      searchBar.setOnNextAction( ( event ) -> edit_find_next() );
313
      searchBar.setOnPrevAction( ( event ) -> edit_find_prev() );
314
315
      nodes.add( searchBar );
316
      searchBar.requestFocus();
317
    }
318
    else {
319
      nodes.clear();
320
    }
321
  }
322
323
  public void edit_find_next() {
324
    mSearchModel.advance();
325
  }
326
327
  public void edit_find_prev() {
328
    mSearchModel.retreat();
329
  }
330
331
  public void edit_preferences() {
332
    try {
333
      new PreferencesController( getWorkspace() ).show();
334
    } catch( final Exception ex ) {
335
      clue( ex );
336
    }
337
  }
338
339
  public void format_bold() {
340
    getActiveTextEditor().bold();
341
  }
342
343
  public void format_italic() {
344
    getActiveTextEditor().italic();
345
  }
346
347
  public void format_monospace() {
348
    getActiveTextEditor().monospace();
349
  }
350
351
  public void format_superscript() {
352
    getActiveTextEditor().superscript();
353
  }
354
355
  public void format_subscript() {
356
    getActiveTextEditor().subscript();
357
  }
358
359
  public void format_strikethrough() {
360
    getActiveTextEditor().strikethrough();
361
  }
362
363
  public void insert_blockquote() {
364
    getActiveTextEditor().blockquote();
365
  }
366
367
  public void insert_code() {
368
    getActiveTextEditor().code();
369
  }
370
371
  public void insert_fenced_code_block() {
372
    getActiveTextEditor().fencedCodeBlock();
373
  }
374
375
  public void insert_link() {
376
    insertObject( createLinkDialog() );
377
  }
378
379
  public void insert_image() {
380
    insertObject( createImageDialog() );
381
  }
382
383
  private void insertObject( final Dialog<String> dialog ) {
384
    final var textArea = getActiveTextEditor().getTextArea();
385
    dialog.showAndWait().ifPresent( textArea::replaceSelection );
386
  }
387
388
  private Dialog<String> createLinkDialog() {
389
    return new LinkDialog( getWindow(), createHyperlinkModel() );
390
  }
391
392
  private Dialog<String> createImageDialog() {
393
    final var path = getActiveTextEditor().getPath();
394
    final var parentDir = path.getParent();
395
    return new ImageDialog( getWindow(), parentDir );
396
  }
397
398
  /**
399
   * Returns one of: selected text, word under cursor, or parsed hyperlink from
400
   * the Markdown AST.
401
   *
402
   * @return An instance containing the link URL and display text.
403
   */
404
  private HyperlinkModel createHyperlinkModel() {
405
    final var context = getMainPane().createProcessorContext();
406
    final var editor = getActiveTextEditor();
407
    final var textArea = editor.getTextArea();
408
    final var selectedText = textArea.getSelectedText();
409
410
    // Convert current paragraph to Markdown nodes.
411
    final var mp = MarkdownProcessor.create( context );
412
    final var p = textArea.getCurrentParagraph();
413
    final var paragraph = textArea.getText( p );
414
    final var node = mp.toNode( paragraph );
415
    final var visitor = new LinkVisitor( textArea.getCaretColumn() );
416
    final var link = visitor.process( node );
417
418
    if( link != null ) {
419
      textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
420
    }
421
422
    return createHyperlinkModel( link, selectedText );
423
  }
424
425
  private HyperlinkModel createHyperlinkModel(
426
    final Link link, final String selection ) {
427
428
    return link == null
429
      ? new HyperlinkModel( selection, "https://localhost" )
430
      : new HyperlinkModel( link );
431
  }
432
433
  public void insert_heading_1() {
434
    insert_heading( 1 );
435
  }
436
437
  public void insert_heading_2() {
438
    insert_heading( 2 );
439
  }
440
441
  public void insert_heading_3() {
442
    insert_heading( 3 );
443
  }
444
445
  private void insert_heading( final int level ) {
446
    getActiveTextEditor().heading( level );
447
  }
448
449
  public void insert_unordered_list() {
450
    getActiveTextEditor().unorderedList();
451
  }
452
453
  public void insert_ordered_list() {
454
    getActiveTextEditor().orderedList();
455
  }
456
457
  public void insert_horizontal_rule() {
458
    getActiveTextEditor().horizontalRule();
459
  }
460
461
  public void definition_create() {
462
    getActiveTextDefinition().createDefinition();
463
  }
464
465
  public void definition_rename() {
466
    getActiveTextDefinition().renameDefinition();
467
  }
468
469
  public void definition_delete() {
470
    getActiveTextDefinition().deleteDefinitions();
471
  }
472
473
  public void definition_autoinsert() {
474
    getMainPane().autoinsert();
475
  }
476
477
  public void view_refresh() {
478
    getMainPane().viewRefresh();
479
  }
480
481
  public void view_preview() {
482
    getMainPane().viewPreview();
483
  }
484
485
  public void view_outline() {
486
    getMainPane().viewOutline();
487
  }
488
489
  public void view_files() { getMainPane().viewFiles(); }
490
491
  public void view_statistics() {
492
    getMainPane().viewStatistics();
493
  }
494
495
  public void view_menubar() {
496
    getMainScene().toggleMenuBar();
497
  }
498
499
  public void view_toolbar() {
500
    getMainScene().toggleToolBar();
501
  }
502
503
  public void view_statusbar() {
504
    getMainScene().toggleStatusBar();
505
  }
506
507
  public void view_log() {
508
    mLogView.view();
509
  }
510
511
  public void help_about() {
512
    final var alert = new Alert( INFORMATION );
513
    final var prefix = "Dialog.about.";
514
    alert.setTitle( get( prefix + "title", APP_TITLE ) );
515
    alert.setHeaderText( get( prefix + "header", APP_TITLE ) );
516
    alert.setContentText( get( prefix + "content", APP_YEAR, APP_VERSION ) );
517
    alert.setGraphic( ICON_DIALOG_NODE );
518
    alert.initOwner( getWindow() );
519
    alert.showAndWait();
520
  }
521
522
  /**
523
   * Concatenates all the files in the same directory as the given file into
524
   * a string. The extension is determined by the given file name pattern; the
525
   * order files are concatenated is based on their numeric sort order (this
526
   * avoids lexicographic sorting).
527
   * <p>
528
   * If the parent path to the file being edited in the text editor cannot
529
   * be found then this will return the editor's text, without iterating through
530
   * the parent directory. (Should never happen, but who knows?)
531
   * </p>
532
   * <p>
533
   * New lines are automatically appended to separate each file.
534
   * </p>
535
   *
536
   * @param editor The text editor containing
537
   * @return All files in the same directory as the file being edited
538
   * concatenated into a single string.
539
   */
540
  private String append( final TextEditor editor ) {
541
    final var pattern = editor.getPath();
542
    final var parent = pattern.getParent();
543
544
    // Short-circuit because nothing else can be done.
545
    if( parent == null ) {
546
      clue( "Main.status.export.concat.parent", pattern );
547
      return editor.getText();
548
    }
549
550
    final var filename = pattern.getFileName().toString();
551
    final var extension = getExtension( filename );
552
553
    if( extension == null || extension.isBlank() ) {
554
      clue( "Main.status.export.concat.extension", filename );
555
      return editor.getText();
556
    }
557
558
    try {
559
      final var glob = "**/*." + extension;
560
      final ArrayList<Path> files = new ArrayList<>();
561
      walk( parent, glob, files::add );
562
      files.sort( new AlphanumComparator<>() );
563
564
      final var text = new StringBuilder( DOCUMENT_LENGTH );
565
566
      files.forEach( ( file ) -> {
567
        try {
568
          clue( "Main.status.export.concat", file );
569
          text.append( readString( file ) );
570
        } catch( final IOException ex ) {
571
          clue( "Main.status.export.concat.io", file );
572
        }
573
      } );
574
575
      return text.toString();
576
    } catch( final Throwable t ) {
577
      clue( t );
578
      return editor.getText();
579
    }
580
  }
581
582
  private Optional<List<File>> pickFiles( final Options... options ) {
583
    return createPicker( options ).choose();
584
  }
585
586
  private Optional<List<File>> pickFiles(
587
    final File filename, final Options... options ) {
588
    final var picker = createPicker( options );
589
    picker.setInitialFilename( filename );
590
    return picker.choose();
591
  }
592
593
  private FilePicker createPicker( final Options... options ) {
594
    final var factory = new FilePickerFactory( getWorkspace() );
595
    return factory.createModal( getWindow(), options );
596
  }
597
598
  private TextEditor getActiveTextEditor() {
599
    return getMainPane().getActiveTextEditor();
600
  }
601
602
  private TextDefinition getActiveTextDefinition() {
603
    return getMainPane().getActiveTextDefinition();
604
  }
605
606
  private MainScene getMainScene() {
607
    return mMainScene;
608
  }
609
610
  private MainPane getMainPane() {
611
    return mMainPane;
612
  }
613
614
  private Workspace getWorkspace() {
615
    return mMainPane.getWorkspace();
616
  }
617
618
  private Window getWindow() {
619
    return getMainPane().getWindow();
620
  }
621
}
1622
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
14
import java.util.HashMap;
15
import java.util.Map;
16
17
import static com.keenwrite.Messages.get;
18
19
/**
20
 * Responsible for wiring all application actions to menus, toolbar buttons,
21
 * and keyboard shortcuts.
22
 */
23
public final class ApplicationBars {
24
25
  private static final Map<String, Action> sMap = new HashMap<>( 64 );
26
27
  /**
28
   * Empty constructor.
29
   */
30
  public ApplicationBars() {
31
  }
32
33
  /**
34
   * Creates the main application affordances.
35
   *
36
   * @param actions The {@link ApplicationActions} that map user interface
37
   *                selections to executable code.
38
   * @return An instance of {@link MenuBar} that contains the menu.
39
   */
40
  public static MenuBar createMenuBar( final ApplicationActions actions ) {
41
    final var SEPARATOR_ACTION = new SeparatorAction();
42
43
    //@formatter:off
44
    return new MenuBar(
45
    createMenu(
46
      get( "Main.menu.file" ),
47
      addAction( "file.new", e -> actions.file_new() ),
48
      addAction( "file.open", e -> actions.file_open() ),
49
      SEPARATOR_ACTION,
50
      addAction( "file.close", e -> actions.file_close() ),
51
      addAction( "file.close_all", e -> actions.file_close_all() ),
52
      SEPARATOR_ACTION,
53
      addAction( "file.save", e -> actions.file_save() ),
54
      addAction( "file.save_as", e -> actions.file_save_as() ),
55
      addAction( "file.save_all", e -> actions.file_save_all() ),
56
      SEPARATOR_ACTION,
57
      addAction( "file.export", e -> {} )
58
        .addSubActions(
59
          addAction( "file.export.pdf", e -> actions.file_export_pdf() ),
60
          addAction( "file.export.pdf.dir", e -> actions.file_export_pdf_dir() ),
61
          addAction( "file.export.html_svg", e -> actions.file_export_html_svg() ),
62
          addAction( "file.export.html_tex", e -> actions.file_export_html_tex() ),
63
          addAction( "file.export.xhtml_tex", e -> actions.file_export_xhtml_tex() ),
64
          addAction( "file.export.markdown", e -> actions.file_export_markdown() )
65
        ),
66
      SEPARATOR_ACTION,
67
      addAction( "file.exit", e -> actions.file_exit() )
68
    ),
69
    createMenu(
70
      get( "Main.menu.edit" ),
71
      SEPARATOR_ACTION,
72
      addAction( "edit.undo", e -> actions.edit_undo() ),
73
      addAction( "edit.redo", e -> actions.edit_redo() ),
74
      SEPARATOR_ACTION,
75
      addAction( "edit.cut", e -> actions.edit_cut() ),
76
      addAction( "edit.copy", e -> actions.edit_copy() ),
77
      addAction( "edit.paste", e -> actions.edit_paste() ),
78
      addAction( "edit.select_all", e -> actions.edit_select_all() ),
79
      SEPARATOR_ACTION,
80
      addAction( "edit.find", e -> actions.edit_find() ),
81
      addAction( "edit.find_next", e -> actions.edit_find_next() ),
82
      addAction( "edit.find_prev", e -> actions.edit_find_prev() ),
83
      SEPARATOR_ACTION,
84
      addAction( "edit.preferences", e -> actions.edit_preferences() )
85
    ),
86
    createMenu(
87
      get( "Main.menu.format" ),
88
      addAction( "format.bold", e -> actions.format_bold() ),
89
      addAction( "format.italic", e -> actions.format_italic() ),
90
      addAction( "format.monospace", e -> actions.format_monospace() ),
91
      addAction( "format.superscript", e -> actions.format_superscript() ),
92
      addAction( "format.subscript", e -> actions.format_subscript() ),
93
      addAction( "format.strikethrough", e -> actions.format_strikethrough() )
94
    ),
95
    createMenu(
96
      get( "Main.menu.insert" ),
97
      addAction( "insert.blockquote", e -> actions.insert_blockquote() ),
98
      addAction( "insert.code", e -> actions.insert_code() ),
99
      addAction( "insert.fenced_code_block", e -> actions.insert_fenced_code_block() ),
100
      SEPARATOR_ACTION,
101
      addAction( "insert.link", e -> actions.insert_link() ),
102
      addAction( "insert.image", e -> actions.insert_image() ),
103
      SEPARATOR_ACTION,
104
      addAction( "insert.heading_1", e -> actions.insert_heading_1() ),
105
      addAction( "insert.heading_2", e -> actions.insert_heading_2() ),
106
      addAction( "insert.heading_3", e -> actions.insert_heading_3() ),
107
      SEPARATOR_ACTION,
108
      addAction( "insert.unordered_list", e -> actions.insert_unordered_list() ),
109
      addAction( "insert.ordered_list", e -> actions.insert_ordered_list() ),
110
      addAction( "insert.horizontal_rule", e -> actions.insert_horizontal_rule() )
111
    ),
112
    createMenu(
113
      get( "Main.menu.definition" ),
114
      addAction( "definition.insert", e -> actions.definition_autoinsert() ),
115
      SEPARATOR_ACTION,
116
      addAction( "definition.create", e -> actions.definition_create() ),
117
      addAction( "definition.rename", e -> actions.definition_rename() ),
118
      addAction( "definition.delete", e -> actions.definition_delete() )
119
    ),
120
    createMenu(
121
      get( "Main.menu.view" ),
122
      addAction( "view.refresh", e -> actions.view_refresh() ),
123
      SEPARATOR_ACTION,
124
      addAction( "view.preview", e -> actions.view_preview() ),
125
      addAction( "view.outline", e -> actions.view_outline() ),
126
      addAction( "view.statistics", e-> actions.view_statistics() ),
127
      addAction( "view.files", e-> actions.view_files() ),
128
      SEPARATOR_ACTION,
129
      addAction( "view.menubar", e -> actions.view_menubar() ),
130
      addAction( "view.toolbar", e -> actions.view_toolbar() ),
131
      addAction( "view.statusbar", e -> actions.view_statusbar() ),
132
      SEPARATOR_ACTION,
133
      addAction( "view.log", e -> actions.view_log() )
134
    ),
135
    createMenu(
136
      get( "Main.menu.help" ),
137
      addAction( "help.about", e -> actions.help_about() )
138
    ) );
139
    //@formatter:on
140
  }
141
142
  public static Node createToolBar() {
143
    final var SEPARATOR_ACTION = new SeparatorAction();
144
145
    return createToolBar(
146
      getAction( "file.new" ),
147
      getAction( "file.open" ),
148
      getAction( "file.save" ),
149
      SEPARATOR_ACTION,
150
      getAction( "file.export.pdf" ),
151
      SEPARATOR_ACTION,
152
      getAction( "edit.undo" ),
153
      getAction( "edit.redo" ),
154
      getAction( "edit.cut" ),
155
      getAction( "edit.copy" ),
156
      getAction( "edit.paste" ),
157
      SEPARATOR_ACTION,
158
      getAction( "format.bold" ),
159
      getAction( "format.italic" ),
160
      getAction( "format.superscript" ),
161
      getAction( "format.subscript" ),
162
      getAction( "insert.blockquote" ),
163
      getAction( "insert.code" ),
164
      getAction( "insert.fenced_code_block" ),
165
      SEPARATOR_ACTION,
166
      getAction( "insert.link" ),
167
      getAction( "insert.image" ),
168
      SEPARATOR_ACTION,
169
      getAction( "insert.heading_1" ),
170
      SEPARATOR_ACTION,
171
      getAction( "insert.unordered_list" ),
172
      getAction( "insert.ordered_list" )
173
    );
174
  }
175
176
  public static StatusBar createStatusBar() {
177
    return new EventedStatusBar();
178
  }
179
180
  /**
181
   * Adds a new action to the list of actions.
182
   *
183
   * @param key     The name of the action to register in {@link #sMap}.
184
   * @param handler Performs the action upon request.
185
   * @return The newly registered action.
186
   */
187
  private static Action addAction(
188
    final String key, final EventHandler<ActionEvent> handler ) {
189
    assert key != null;
190
    assert handler != null;
191
192
    final var action = Action
193
      .builder()
194
      .setId( key )
195
      .setHandler( handler )
196
      .build();
197
198
    sMap.put( key, action );
199
200
    return action;
201
  }
202
203
  private static Action getAction( final String key ) {
204
    return sMap.get( key );
205
  }
206
207
  public static Menu createMenu(
208
    final String text, final MenuAction... actions ) {
209
    return new Menu( text, null, createMenuItems( actions ) );
210
  }
211
212
  public static MenuItem[] createMenuItems( final MenuAction... actions ) {
213
    final var menuItems = new MenuItem[ actions.length ];
214
215
    for( var i = 0; i < actions.length; i++ ) {
216
      menuItems[ i ] = actions[ i ].createMenuItem();
217
    }
218
219
    return menuItems;
220
  }
221
222
  private static ToolBar createToolBar( final MenuAction... actions ) {
223
    return new ToolBar( createToolBarButtons( actions ) );
224
  }
225
226
  private static Node[] createToolBarButtons( final MenuAction... actions ) {
227
    final var len = actions.length;
228
    final var nodes = new Node[ len ];
229
230
    for( var i = 0; i < len; i++ ) {
231
      nodes[ i ] = actions[ i ].createToolBarNode();
232
    }
233
234
    return nodes;
235
  }
236
}
1237
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/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/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/dialogs/ThemePicker.java
1
/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.dialogs;
3
4
import com.keenwrite.util.FileWalker;
5
import com.keenwrite.util.ResourceWalker;
6
import javafx.beans.property.StringProperty;
7
import javafx.scene.control.ChoiceDialog;
8
import javafx.scene.control.ComboBox;
9
import javafx.scene.image.Image;
10
import javafx.scene.input.KeyCode;
11
import javafx.stage.Stage;
12
13
import java.io.File;
14
import java.io.FileInputStream;
15
import java.io.IOException;
16
import java.io.InputStreamReader;
17
import java.nio.charset.StandardCharsets;
18
import java.nio.file.Path;
19
import java.util.Properties;
20
import java.util.TreeMap;
21
22
import static com.keenwrite.Messages.get;
23
import static com.keenwrite.constants.Constants.THEME_NAME_LENGTH;
24
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
25
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE;
26
import static com.keenwrite.events.StatusEvent.clue;
27
import static com.keenwrite.util.FileWalker.walk;
28
import static java.lang.Math.max;
29
import static java.nio.charset.StandardCharsets.UTF_8;
30
import static org.codehaus.plexus.util.StringUtils.abbreviate;
31
32
/**
33
 * Responsible for allowing the user to pick from the available themes found
34
 * in the system.
35
 */
36
public class ThemePicker extends ChoiceDialog<String> {
37
  private final File mThemes;
38
  private final StringProperty mTheme;
39
40
  /**
41
   * Construction must use static method to allow caching themes in the
42
   * future, if needed.
43
   *
44
   * @see #choose(File, StringProperty)
45
   */
46
  @SuppressWarnings( "rawtypes" )
47
  private ThemePicker( final File themes, final StringProperty theme ) {
48
    assert themes != null;
49
    assert theme != null;
50
51
    mThemes = themes;
52
    mTheme = theme;
53
    initIcon();
54
    setTitle( get( "Dialog.theme.title" ) );
55
    setHeaderText( get( "Dialog.theme.header" ) );
56
57
    final var options = (ComboBox) getDialogPane().lookup( ".combo-box" );
58
    options.setOnKeyPressed( ( event ) -> {
59
      // When the user presses the down arrow, open the drop-down. This prevents
60
      // navigating to the cancel button.
61
      if( event.getCode() == KeyCode.DOWN && !options.isShowing() ) {
62
        options.show();
63
        event.consume();
64
      }
65
    } );
66
  }
67
68
  private void initIcon() {
69
    setGraphic( ICON_DIALOG_NODE );
70
    setStageGraphic( ICON_DIALOG );
71
  }
72
73
  @SuppressWarnings( "SameParameterValue" )
74
  private void setStageGraphic( final Image icon ) {
75
    if( getDialogPane().getScene().getWindow() instanceof final Stage stage ) {
76
      stage.getIcons().add( icon );
77
    }
78
  }
79
80
  /**
81
   * Prompts a user to select a theme, answering {@code false} if no theme
82
   * was selected. The themes must be on the native file system; using the
83
   * {@link FileWalker} is a little more optimal than {@link ResourceWalker}.
84
   *
85
   * @param themes Theme directory root.
86
   * @param theme  Selected theme property name.
87
   * @return {@code true} if the user accepted or selected a theme.
88
   */
89
  public static boolean choose(
90
    final File themes, final StringProperty theme ) {
91
    assert themes != null;
92
    assert theme != null;
93
94
    return new ThemePicker( themes, theme ).pick();
95
  }
96
97
  /**
98
   * @return {@code true} if the user accepted or selected a theme.
99
   * @see #choose(File, StringProperty)
100
   */
101
  private boolean pick() {
102
    try {
103
      // List themes in alphabetical order (human readable by directory name).
104
      final var choices = new TreeMap<String, String>();
105
      final String[] selection = new String[]{""};
106
107
      // Populate the choices with themes detected on the system.
108
      walk( mThemes.toPath(), "**/theme.properties", ( path ) -> {
109
        try {
110
          final var displayed = readThemeName( path );
111
          final var themeName = path.getParent().toFile().getName();
112
          choices.put( abbreviate( displayed, THEME_NAME_LENGTH ), themeName );
113
114
          // Set the selected item to user's settings value.
115
          if( themeName.equals( mTheme.get() ) ) {
116
            selection[ 0 ] = displayed;
117
          }
118
        } catch( final Exception ex ) {
119
          clue( "Main.status.error.theme.name", path );
120
        }
121
      } );
122
123
      final var items = getItems();
124
      items.addAll( choices.keySet() );
125
      setSelectedItem( items.get( max( items.indexOf( selection[ 0 ] ), 0 ) ) );
126
127
      final var result = showAndWait();
128
129
      if( result.isPresent() ) {
130
        mTheme.set( choices.get( result.get() ) );
131
        return true;
132
      }
133
    } catch( final Exception ex ) {
134
      clue( get( "Main.status.error.theme.missing", mThemes ), ex );
135
    }
136
137
    return false;
138
  }
139
140
  /**
141
   * Returns the theme's human-friendly name from a file conforming to
142
   * {@link Properties}.
143
   *
144
   * @param file A fully qualified file name readable using {@link Properties}.
145
   * @return The human-friendly theme name.
146
   * @throws IOException          The {@link Properties} file cannot be read.
147
   * @throws NullPointerException The name field is not defined.
148
   */
149
  private String readThemeName( final Path file ) throws Exception {
150
    return read( file ).get( "name" ).toString();
151
  }
152
153
  /**
154
   * Reads an instance of {@link Properties} from the given {@link Path} using
155
   * {@link StandardCharsets#UTF_8} encoding.
156
   *
157
   * @param path The fully qualified path to the file.
158
   * @return The path to the file to read.
159
   * @throws IOException Could not open the file for reading.
160
   */
161
  private Properties read( final Path path ) throws IOException {
162
    final var properties = new Properties();
163
164
    try( final var in = new InputStreamReader(
165
      new FileInputStream( path.toFile() ), UTF_8 ) ) {
166
      properties.load( in );
167
    }
168
169
    return properties;
170
  }
171
}
1172
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
  default 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.io7m.jwheatsheaf.ui.JWFileChoosers;
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.ArrayList;
14
import java.util.List;
15
import java.util.Locale;
16
import java.util.Optional;
17
18
import static com.io7m.jwheatsheaf.api.JWFileChooserAction.*;
19
import static com.io7m.jwheatsheaf.api.JWFileChooserConfiguration.Builder;
20
import static com.io7m.jwheatsheaf.api.JWFileChooserConfiguration.builder;
21
import static com.keenwrite.constants.Constants.USER_DIRECTORY;
22
import static com.keenwrite.events.StatusEvent.clue;
23
import static com.keenwrite.preferences.WorkspaceKeys.KEY_UI_RECENT_DIR;
24
import static java.nio.file.FileSystems.getDefault;
25
import static java.util.Optional.ofNullable;
26
27
/**
28
 * Shim for a {@link FilePicker} instance that is implemented in pure Java.
29
 * This particular picker is added to avoid using the bug-ridden JavaFX
30
 * {@link FileChooser} that invokes the native file chooser.
31
 */
32
public class FilePickerFactory {
33
  public enum Options {
34
    DIRECTORY_OPEN,
35
    FILE_IMPORT,
36
    FILE_EXPORT,
37
    FILE_OPEN_SINGLE,
38
    FILE_OPEN_MULTIPLE,
39
    FILE_OPEN_NEW,
40
    FILE_SAVE_AS,
41
    PERMIT_CREATE_DIRS,
42
  }
43
44
  private final ObjectProperty<File> mDirectory;
45
  private final Locale mLocale;
46
47
  public FilePickerFactory( final Workspace workspace ) {
48
    mDirectory = workspace.fileProperty( KEY_UI_RECENT_DIR );
49
    mLocale = workspace.getLocale();
50
  }
51
52
  public FilePicker createModal(
53
    final Window owner, final Options... options ) {
54
    final var picker = new PureFilePicker( owner, options );
55
    picker.setInitialDirectory( mDirectory.get().toPath() );
56
57
    return picker;
58
  }
59
60
  public Node createModeless() {
61
    return new FilesView( mDirectory, mLocale );
62
  }
63
64
  /**
65
   * Pure Java implementation of a file selection widget.
66
   */
67
  private class PureFilePicker implements FilePicker {
68
    private final Window mParent;
69
    private final Builder mBuilder;
70
71
    private PureFilePicker( final Window window, final Options... options ) {
72
      mParent = window;
73
      mBuilder = builder().setFileSystem( getDefault() );
74
75
      final var args = ofNullable( options ).orElse( options );
76
77
      var title = "Dialog.file.choose.open.title";
78
      var action = OPEN_EXISTING_SINGLE;
79
80
      // It is a programming error to provide options that save or export to
81
      // multiple files.
82
      for( final var arg : args ) {
83
        switch( arg ) {
84
          case FILE_EXPORT -> {
85
            title = "Dialog.file.choose.export.title";
86
            action = CREATE;
87
          }
88
          case FILE_SAVE_AS -> {
89
            title = "Dialog.file.choose.save.title";
90
            action = CREATE;
91
          }
92
          case FILE_OPEN_SINGLE -> action = OPEN_EXISTING_SINGLE;
93
          case FILE_OPEN_MULTIPLE -> action = OPEN_EXISTING_MULTIPLE;
94
          case PERMIT_CREATE_DIRS -> mBuilder.setAllowDirectoryCreation( true );
95
        }
96
      }
97
98
      //mBuilder.setTitle( get( title ) );
99
      mBuilder.setAction( action );
100
    }
101
102
    @Override
103
    public void setInitialDirectory( final Path path ) {
104
      mBuilder.setInitialDirectory( path );
105
    }
106
107
//    private JWFileChooserFilterType createFileFilters() {
108
//      final var filters = new JWFilterGlobFactory();
109
//
110
//      return filters.create( "PDF Files" )
111
//                    .addRule( INCLUDE, "**/*.pdf" )
112
//                    .addRule( EXCLUDE_AND_HALT, "**/.*" )
113
//                    .build();
114
//    }
115
116
    @Override
117
    public Optional<List<File>> choose() {
118
      final var config = mBuilder.build();
119
      try( final var chooserType = JWFileChoosers.create() ) {
120
        final var chooser = chooserType.create( mParent, config );
121
        final var paths = chooser.showAndWait();
122
        final var files = new ArrayList<File>( paths.size() );
123
        paths.forEach( path -> {
124
          final var file = path.toFile();
125
          files.add( file );
126
127
          // Set to the directory of the last file opened successfully.
128
          setRecentDirectory( file );
129
        } );
130
131
        return files.isEmpty() ? Optional.empty() : Optional.of( files );
132
      } catch( final Exception ex ) {
133
        clue( ex );
134
      }
135
136
      return Optional.empty();
137
    }
138
  }
139
140
  /**
141
   * Sets the value for the most recent directly selected. This will get the
142
   * parent location from the given file. If the parent is a readable directory
143
   * then this will update the most recent directory property.
144
   *
145
   * @param file A file contained in a directory.
146
   */
147
  private void setRecentDirectory( final File file ) {
148
    assert file != null;
149
150
    final var parent = file.getParentFile();
151
    final var dir = parent == null ? USER_DIRECTORY : parent;
152
153
    if( dir.isDirectory() && dir.canRead() ) {
154
      mDirectory.setValue( dir );
155
    }
156
  }
157
}
1158
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.ui.controls.BrowseButton;
5
import javafx.beans.property.*;
6
import javafx.collections.ObservableList;
7
import javafx.collections.transformation.SortedList;
8
import javafx.scene.control.*;
9
import javafx.scene.layout.BorderPane;
10
import javafx.scene.layout.HBox;
11
import javafx.stage.FileChooser;
12
import javafx.util.Callback;
13
14
import java.io.File;
15
import java.io.IOException;
16
import java.nio.file.Path;
17
import java.nio.file.Paths;
18
import java.time.Instant;
19
import java.time.format.DateTimeFormatter;
20
import java.util.List;
21
import java.util.Locale;
22
import java.util.Optional;
23
24
import static com.keenwrite.constants.Constants.UI_CONTROL_SPACING;
25
import static com.keenwrite.events.FileOpenEvent.fireFileOpenEvent;
26
import static com.keenwrite.events.StatusEvent.clue;
27
import static com.keenwrite.ui.fonts.IconFactory.createFileIcon;
28
import static java.nio.file.Files.size;
29
import static java.time.Instant.ofEpochMilli;
30
import static java.time.ZoneId.systemDefault;
31
import static java.time.format.DateTimeFormatter.ofPattern;
32
import static java.util.Comparator.comparing;
33
import static javafx.collections.FXCollections.observableArrayList;
34
import static javafx.scene.control.TableView.CONSTRAINED_RESIZE_POLICY;
35
import static javafx.scene.input.KeyCode.ENTER;
36
import static javafx.scene.layout.Priority.ALWAYS;
37
import static org.apache.commons.io.FilenameUtils.getExtension;
38
39
/**
40
 * Responsible for browsing files.
41
 */
42
public class FilesView extends BorderPane implements FilePicker {
43
  /**
44
   * When this directory changes, the input field will update accordingly.
45
   */
46
  private final ObjectProperty<File> mDirectory;
47
48
  /**
49
   * Data model for the file list shown in tabular format.
50
   */
51
  private final ObservableList<PathEntry> mItems = observableArrayList();
52
53
  /**
54
   * Used to format a file's date string from a {@code long} value.
55
   */
56
  private final DateTimeFormatter mDateFormatter;
57
58
  /**
59
   * Used to format a file's time string from a {@code long} value.
60
   */
61
  private final DateTimeFormatter mTimeFormatter;
62
63
  /**
64
   * Constructs a new view of a directory, listing all the files contained
65
   * therein. This will update the recent directory so that it will be
66
   * restored upon restart.
67
   *
68
   * @param recent Contains the initial (recent) directory.
69
   * @param locale Contains the language settings.
70
   */
71
  public FilesView(
72
    final ObjectProperty<File> recent, final Locale locale ) {
73
    mDirectory = recent;
74
    mDateFormatter = createFormatter( "yyyy-MMM-dd", locale );
75
    mTimeFormatter = createFormatter( "HH:mm:ss", locale );
76
77
    final var browse = createDirectoryChooser();
78
    final var table = createFileTable();
79
80
    final var sortedItems = new SortedList<>( mItems );
81
    sortedItems.comparatorProperty().bind( table.comparatorProperty() );
82
    table.setItems( sortedItems );
83
84
    setTop( browse );
85
    setCenter( table );
86
87
    mDirectory.addListener( ( c, o, n ) -> updateListing( n ) );
88
    updateListing( mDirectory.get() );
89
  }
90
91
  @Override
92
  public Optional<List<File>> choose() {
93
    return Optional.empty();
94
  }
95
96
  private void updateListing( final File directory ) {
97
    if( directory != null ) {
98
      mItems.clear();
99
100
      try {
101
        if( directory.getParent() != null ) {
102
          // Allow traversal to parent-directory.
103
          mItems.add( pathEntry( Paths.get( ".." ) ) );
104
        }
105
106
        for( final var f : directory.list() ) {
107
          if( !f.startsWith( "." ) ) {
108
            mItems.add( pathEntry( Paths.get( directory.toString(), f ) ) );
109
          }
110
        }
111
      } catch( final Exception ex ) {
112
        clue( ex );
113
      }
114
    }
115
  }
116
117
  /**
118
   * Allows the user to use an instance of {@link FileChooser} to change the
119
   * directory.
120
   *
121
   * @return The browse button and input field.
122
   */
123
  private HBox createDirectoryChooser() {
124
    final var dirProperty = directoryProperty();
125
    final var directory = dirProperty.get();
126
    final var hbox = new HBox();
127
    final var field = new TextField();
128
129
    mDirectory.addListener( ( c, o, n ) -> {
130
      if( n != null ) { field.setText( n.getAbsolutePath() ); }
131
    } );
132
133
    field.setOnKeyPressed( event -> {
134
      if( event.getCode() == ENTER ) {
135
        mDirectory.set( new File( field.getText() ) );
136
      }
137
    } );
138
139
    final var button = new BrowseButton( directory, mDirectory::set );
140
141
    hbox.getChildren().add( button );
142
    hbox.getChildren().add( field );
143
    hbox.setSpacing( UI_CONTROL_SPACING );
144
    HBox.setHgrow( field, ALWAYS );
145
146
    return hbox;
147
  }
148
149
  @SuppressWarnings( "unchecked" )
150
  private TableView<FilesView.PathEntry> createFileTable() {
151
    final var style = "-fx-alignment: BASELINE_LEFT;";
152
    final var table = new TableView<FilesView.PathEntry>();
153
    table.setColumnResizePolicy( CONSTRAINED_RESIZE_POLICY );
154
155
    table.setRowFactory( tv -> {
156
      final var row = new TableRow<PathEntry>();
157
158
      row.setOnMouseClicked( event -> {
159
        if( event.getClickCount() == 2 && !row.isEmpty() ) {
160
          final var entry = row.getItem();
161
          final var dir = mDirectory.get();
162
          final var filename = entry.nameProperty().get();
163
          final var path = Path.of( dir.toString(), filename );
164
          final var file = path.toFile();
165
166
          if( file.isFile() ) {
167
            fireFileOpenEvent( path.toUri() );
168
          }
169
          else if( file.isDirectory() ) {
170
            mDirectory.set( path.normalize().toFile() );
171
          }
172
        }
173
      } );
174
175
      return row;
176
    } );
177
178
    final TableColumn<PathEntry, Path> colType = createColumn( "Type" );
179
    final TableColumn<PathEntry, String> colName = createColumn( "Name" );
180
    final TableColumn<PathEntry, Number> colSize = createColumn( "Size" );
181
    final TableColumn<PathEntry, String> colDate = createColumn( "Date" );
182
    final TableColumn<PathEntry, String> colTime = createColumn( "Modified" );
183
184
    colType.setCellFactory( new FileCell<>() );
185
186
    colType.setCellValueFactory( stat -> stat.getValue().typeProperty() );
187
    colName.setCellValueFactory( stat -> stat.getValue().nameProperty() );
188
    colSize.setCellValueFactory( stat -> stat.getValue().sizeProperty() );
189
    colDate.setCellValueFactory( stat -> stat.getValue().dateProperty() );
190
    colTime.setCellValueFactory( stat -> stat.getValue().timeProperty() );
191
192
    colType.setStyle( style );
193
    colName.setStyle( style );
194
    colSize.setStyle( style );
195
    colDate.setStyle( style );
196
    colTime.setStyle( style );
197
198
    final var columns = table.getColumns();
199
    columns.add( colType );
200
    columns.add( colName );
201
    columns.add( colSize );
202
    columns.add( colDate );
203
    columns.add( colTime );
204
205
    table.getSortOrder().setAll( colName, colDate, colTime );
206
207
    colType.setComparator(
208
      comparing( p -> getExtension( p.getFileName().toString() ) )
209
    );
210
211
    return table;
212
  }
213
214
  public ObjectProperty<File> directoryProperty() {
215
    return mDirectory;
216
  }
217
218
  private static DateTimeFormatter createFormatter(
219
    final String format, final Locale locale ) {
220
    return ofPattern( format, locale ).withZone( systemDefault() );
221
  }
222
223
  public PathEntry pathEntry( final Path path ) throws IOException {
224
    return new PathEntry( path );
225
  }
226
227
  /**
228
   * Responsible for rendering file system objects as image icons.
229
   *
230
   * @param <T> The data model type associated with a fully qualified path.
231
   * @param <P> Simplifies swapping {@link Path} for {@link File}.
232
   */
233
  private static class FileCell<T, P extends Path> extends TableCell<T, P>
234
    implements Callback<TableColumn<T, P>, TableCell<T, P>> {
235
    @Override
236
    public TableCell<T, P> call( final TableColumn<T, P> param ) {
237
      return new TableCell<>() {
238
        @Override
239
        protected void updateItem( final P path, final boolean empty ) {
240
          super.updateItem( path, empty );
241
          setText( null );
242
243
          try {
244
            setGraphic( empty || path == null ? null : createFileIcon( path ) );
245
          } catch( final Exception ex ) {
246
            clue( ex );
247
          }
248
        }
249
      };
250
    }
251
  }
252
253
  protected final class PathEntry {
254
    private final ObjectProperty<Path> mType;
255
    private final StringProperty mName;
256
    private final LongProperty mSize;
257
    private final StringProperty mDate;
258
    private final StringProperty mTime;
259
260
    protected PathEntry( final Path path ) throws IOException {
261
      this(
262
        path,
263
        path.getFileName().toString(),
264
        size( path ),
265
        ofEpochMilli( path.toFile().lastModified() )
266
      );
267
    }
268
269
    public PathEntry(
270
      final Path type,
271
      final String name,
272
      final long size,
273
      final Instant modified ) {
274
      this(
275
        new SimpleObjectProperty<>( type ),
276
        new SimpleStringProperty( name ),
277
        new SimpleLongProperty( size ),
278
        new SimpleStringProperty( mDateFormatter.format( modified ) ),
279
        new SimpleStringProperty( mTimeFormatter.format( modified ) )
280
      );
281
    }
282
283
    private PathEntry(
284
      final ObjectProperty<Path> type,
285
      final StringProperty name,
286
      final LongProperty size,
287
      final StringProperty date,
288
      final StringProperty time ) {
289
      mType = type;
290
      mName = name;
291
      mSize = size;
292
      mDate = date;
293
      mTime = time;
294
    }
295
296
    private ObjectProperty<Path> typeProperty() {
297
      return mType;
298
    }
299
300
    private StringProperty nameProperty() {
301
      return mName;
302
    }
303
304
    private LongProperty sizeProperty() {
305
      return mSize;
306
    }
307
308
    private StringProperty dateProperty() {
309
      return mDate;
310
    }
311
312
    private StringProperty timeProperty() {
313
      return mTime;
314
    }
315
  }
316
317
  private <E, T> TableColumn<E, T> createColumn( final String key ) {
318
    return new TableColumn<>( key );
319
  }
320
}
1321
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
   * Prevent instantiation. Use the {@link #createGraphic(String)} method to
52
   * create an icon for display.
53
   */
54
  private IconFactory() {}
55
56
  /**
57
   * Create a {@link Node} representation for the given icon name.
58
   *
59
   * @param icon Name of icon to convert to a UI object (case-insensitive).
60
   * @return A UI object suitable for display.
61
   */
62
  public static Node createGraphic( final String icon ) {
63
    assert icon != null;
64
65
    // Return a label glyph.
66
    return icon.isEmpty()
67
      ? new Glyph()
68
      : createGlyph( icon );
69
  }
70
71
  /**
72
   * Create a {@link Node} representation for the given FontAwesome glyph.
73
   *
74
   * @param glyph The glyph to convert to a {@link Node}.
75
   * @return The given glyph as a text label.
76
   */
77
  public static Node createGraphic( final FontAwesome.Glyph glyph ) {
78
    return FONT_AWESOME.create( glyph );
79
  }
80
81
  /**
82
   * Creates a suitable {@link Node} icon representation for the given file.
83
   * This will first look up the {@link MediaType} before matching based on
84
   * the file name extension.
85
   *
86
   * @param path The file to represent graphically.
87
   * @return An icon representation for the given file.
88
   */
89
  public static ImageView createFileIcon( final Path path ) throws IOException {
90
    final var attrs = readAttributes( path, BasicFileAttributes.class );
91
    final var filename = path.getFileName().toString();
92
    String extension;
93
94
    if( "..".equals( filename ) ) {
95
      extension = "folder-up";
96
    }
97
    else if( attrs.isDirectory() ) {
98
      extension = "folder";
99
    }
100
    else if( attrs.isSymbolicLink() ) {
101
      extension = "folder-link";
102
    }
103
    else {
104
      final var mediaType = MediaType.valueFrom( path );
105
      final var mte = MediaTypeExtension.valueFrom( mediaType );
106
107
      // if the file extension is not known to the app, try loading an icon
108
      // that corresponds to the extension directly.
109
      extension = mte == MEDIA_UNDEFINED
110
        ? getExtension( filename )
111
        : mte.getExtension();
112
    }
113
114
    if( extension == null ) {
115
      extension = "";
116
    }
117
    else {
118
      extension = extension.toLowerCase();
119
    }
120
121
    // Each cell in the table must have a distinct parent, so the image views
122
    // cannot be reused. The underlying buffered image can be cached, though.
123
    final var image =
124
      ICONS.computeIfAbsent( extension, IconFactory::createFxImage );
125
    final var imageView = new ImageView();
126
    imageView.setPreserveRatio( true );
127
    imageView.setFitHeight( ICON_HEIGHT );
128
    imageView.setImage( image );
129
130
    return imageView;
131
  }
132
133
  private static Image createFxImage( final String extension ) {
134
    return toFXImage( createImage( extension ), null );
135
  }
136
137
  private static BufferedImage createImage( final String extension ) {
138
    try( final var icon = open( "icons/" + extension + ".svg" ) ) {
139
      if( icon == null ) {
140
        throw new IllegalArgumentException( extension );
141
      }
142
143
      return rasterize( icon );
144
    } catch( final Exception ex ) {
145
      clue( ex );
146
147
      // If the extension was unknown, fall back to a blank icon, falling
148
      // back again to a broken image if blank cannot be found (to avoid
149
      // infinite recursion).
150
      return "blank".equals( extension )
151
        ? BROKEN_IMAGE_PLACEHOLDER
152
        : createImage( "blank" );
153
    }
154
  }
155
156
  private static InputStream open( final String resource ) {
157
    return IconFactory.class.getResourceAsStream( resource );
158
  }
159
160
  /**
161
   * Returns the font to use when adding icons to the UI.
162
   *
163
   * @param size The font size to use when drawing the icon.
164
   * @return A font containing numerous icons.
165
   */
166
  public static Font getIconFont( final int size ) {
167
    try( final var fontStream = openFont() ) {
168
      final var font = createFont( TRUETYPE_FONT, fontStream );
169
      return font.deriveFont( PLAIN, size );
170
    } catch( final Exception e ) {
171
      // This doesn't actually work, seemingly after an upgrade to ControlsFX.
172
      // As such, creating the font and deriving it will work.
173
      return new Font( FONT_AWESOME.getName(), PLAIN, size );
174
    }
175
  }
176
177
  /**
178
   * This re-reads the {@link FontAwesome} font TTF resource. For a reason
179
   * not yet investigated, the font doesn't appear to be accessible to the
180
   * application. This may have happened during an upgrade to ControlsFX.
181
   * Callers are responsible for closing the stream.
182
   *
183
   * @return A stream containing font TrueType glyph information.
184
   */
185
  private static InputStream openFont() {
186
    return FontAwesome.class.getResourceAsStream( "fontawesome-webfont.ttf" );
187
  }
188
189
  private static Node createGlyph( final String icon ) {
190
    return createGraphic( valueOf( icon.toUpperCase() ) );
191
  }
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.preferences.Workspace;
6
import com.whitemagicsoftware.wordcount.TokenizerException;
7
import javafx.beans.property.IntegerProperty;
8
import javafx.beans.property.SimpleIntegerProperty;
9
import javafx.beans.property.SimpleStringProperty;
10
import javafx.beans.property.StringProperty;
11
import javafx.collections.ObservableList;
12
import javafx.collections.transformation.SortedList;
13
import javafx.scene.control.TableColumn;
14
import javafx.scene.control.TableView;
15
import org.greenrobot.eventbus.Subscribe;
16
17
import static com.keenwrite.events.Bus.register;
18
import static com.keenwrite.events.StatusEvent.clue;
19
import static com.keenwrite.events.WordCountEvent.fireWordCountEvent;
20
import static com.keenwrite.preferences.WorkspaceKeys.KEY_LANGUAGE_LOCALE;
21
import static com.keenwrite.preferences.WorkspaceKeys.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
        fireWordCountEvent( 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/CaretListener.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.listeners;
3
4
import com.keenwrite.Caret;
5
import com.keenwrite.editors.TextEditor;
6
import com.keenwrite.events.WordCountEvent;
7
import javafx.beans.property.ReadOnlyObjectProperty;
8
import javafx.beans.value.ChangeListener;
9
import javafx.beans.value.ObservableValue;
10
import javafx.scene.control.Label;
11
import javafx.scene.layout.VBox;
12
import org.greenrobot.eventbus.Subscribe;
13
14
import static com.keenwrite.events.Bus.register;
15
import static javafx.application.Platform.runLater;
16
import static javafx.geometry.Pos.BASELINE_CENTER;
17
18
/**
19
 * Responsible for updating the UI whenever the caret changes position.
20
 * Only one instance of {@link CaretListener} is allowed, which prevents
21
 * duplicate adds to the observable property.
22
 */
23
public class CaretListener extends VBox implements ChangeListener<Integer> {
24
25
  /**
26
   * Use an instance of {@link Label} for its built-in CSS style class.
27
   */
28
  private final Label mLineNumberText = new Label();
29
  private volatile Caret mCaret;
30
31
  /**
32
   * Approximate number of words in the document.
33
   */
34
  private volatile int mCount;
35
36
  public CaretListener( final ReadOnlyObjectProperty<TextEditor> editor ) {
37
    assert editor != null;
38
39
    setAlignment( BASELINE_CENTER );
40
    getChildren().add( mLineNumberText );
41
42
    editor.addListener( ( c, o, n ) -> {
43
      if( n != null ) {
44
        updateListener( n.getCaret() );
45
      }
46
    } );
47
48
    updateListener( editor.get().getCaret() );
49
    register( this );
50
  }
51
52
  /**
53
   * Called whenever the caret position changes.
54
   *
55
   * @param c The caret position property.
56
   * @param o The old caret position offset.
57
   * @param n The new caret position offset.
58
   */
59
  @Override
60
  public void changed(
61
    final ObservableValue<? extends Integer> c,
62
    final Integer o, final Integer n ) {
63
    updateLineNumber();
64
  }
65
66
  @Subscribe
67
  public void handle( final WordCountEvent event ) {
68
    mCount = event.getCount();
69
    updateLineNumber();
70
  }
71
72
  private void updateListener( final Caret caret ) {
73
    assert caret != null;
74
75
    final var property = caret.textOffsetProperty();
76
77
    property.removeListener( this );
78
    mCaret = caret;
79
    property.addListener( this );
80
    updateLineNumber();
81
  }
82
83
  private void updateLineNumber() {
84
    runLater(
85
      () -> mLineNumberText.setText( mCaret.toString() + " | " + mCount )
86
    );
87
  }
88
}
189
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 javafx.beans.property.SimpleStringProperty;
6
import javafx.beans.property.StringProperty;
7
import javafx.collections.ObservableList;
8
import javafx.scene.control.*;
9
import javafx.scene.input.ClipboardContent;
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.Clipboard.getSystemClipboard;
32
import static javafx.scene.input.KeyCode.C;
33
import static javafx.scene.input.KeyCode.INSERT;
34
import static javafx.scene.input.KeyCombination.CONTROL_ANY;
35
import static javafx.stage.Modality.NONE;
36
37
/**
38
 * Responsible for logging application issues to {@link TableView} entries.
39
 */
40
public final class LogView extends Alert {
41
  /**
42
   * Number of error messages to retain in the {@link TableView}; must be
43
   * greater than zero. Typesetting the document can cause many page number
44
   * messages to be logged.
45
   */
46
  private static final int CACHE_SIZE = 500;
47
48
  private final ObservableList<LogEntry> mItems = observableArrayList();
49
  private final TableView<LogEntry> mTable = new TableView<>( mItems );
50
51
  public LogView() {
52
    super( INFORMATION );
53
    setTitle( get( ACTION_PREFIX + "view.log.text" ) );
54
    initModality( NONE );
55
    initTableView();
56
    setResizable( true );
57
    initButtons();
58
    initIcon();
59
    initActions();
60
    register( this );
61
  }
62
63
  @Subscribe
64
  public void log( final StatusEvent event ) {
65
    runLater( () -> {
66
      final var logEntry = new LogEntry( event );
67
68
      if( !mItems.contains( logEntry ) ) {
69
        mItems.add( logEntry );
70
71
        while( mItems.size() > CACHE_SIZE ) {
72
          mItems.remove( 0 );
73
        }
74
75
        mTable.scrollTo( logEntry );
76
      }
77
    } );
78
  }
79
80
  /**
81
   * Brings the dialog to the foreground, showing it if needed.
82
   */
83
  public void view() {
84
    super.show();
85
    getStage().toFront();
86
  }
87
88
  /**
89
   * Removes all the entries from the list.
90
   */
91
  public void clear() {
92
    mItems.clear();
93
    clue();
94
  }
95
96
  private void initTableView() {
97
    final var ctrlC = new KeyCodeCombination( C, CONTROL_ANY );
98
    final var ctrlInsert = new KeyCodeCombination( INSERT, CONTROL_ANY );
99
100
    final var colDate = new TableColumn<LogEntry, String>( "Timestamp" );
101
    final var colMessage = new TableColumn<LogEntry, String>( "Message" );
102
    final var colTrace = new TableColumn<LogEntry, String>( "Trace" );
103
104
    colDate.setCellValueFactory( log -> log.getValue().dateProperty() );
105
    colMessage.setCellValueFactory( log -> log.getValue().messageProperty() );
106
    colTrace.setCellValueFactory( log -> log.getValue().traceProperty() );
107
108
    final var columns = mTable.getColumns();
109
    columns.add( colDate );
110
    columns.add( colMessage );
111
    columns.add( colTrace );
112
113
    mTable.setMaxWidth( Double.MAX_VALUE );
114
    mTable.setPrefWidth( 1024 );
115
    mTable.getSelectionModel().setSelectionMode( MULTIPLE );
116
    mTable.setOnKeyPressed( event -> {
117
      if( ctrlC.match( event ) || ctrlInsert.match( event ) ) {
118
        copyToClipboard( mTable );
119
      }
120
    } );
121
122
    final var pane = getDialogPane();
123
    pane.setContent( mTable );
124
  }
125
126
  private void initButtons() {
127
    final var pane = getDialogPane();
128
    final var CLEAR = new ButtonType( "CLEAR" );
129
    pane.getButtonTypes().add( CLEAR );
130
131
    final var buttonOk = (Button) pane.lookupButton( OK );
132
    final var buttonClear = (Button) pane.lookupButton( CLEAR );
133
134
    buttonOk.setDefaultButton( true );
135
    buttonClear.addEventFilter( ACTION, event -> {
136
      clear();
137
      event.consume();
138
    } );
139
140
    pane.setOnKeyReleased( t -> {
141
      switch( t.getCode() ) {
142
        case ENTER, ESCAPE -> buttonOk.fire();
143
      }
144
    } );
145
  }
146
147
  private void initIcon() {
148
    getStage().getIcons().add( ICON_DIALOG );
149
  }
150
151
  private void initActions() {
152
    final var stage = getStage();
153
    stage.setOnCloseRequest( event -> stage.hide() );
154
  }
155
156
  private Stage getStage() {
157
    return (Stage) getDialogPane().getScene().getWindow();
158
  }
159
160
  private static final class LogEntry {
161
    private final StringProperty mDate;
162
    private final StringProperty mMessage;
163
    private final StringProperty mTrace;
164
165
    /**
166
     * Constructs a new {@link LogEntry} for the current time.
167
     */
168
    public LogEntry( final StatusEvent event ) {
169
      mDate = new SimpleStringProperty( toString( now() ) );
170
      mMessage = new SimpleStringProperty( event.getMessage() );
171
      mTrace = new SimpleStringProperty( event.getProblem() );
172
    }
173
174
    private StringProperty messageProperty() {
175
      return mMessage;
176
    }
177
178
    private StringProperty dateProperty() {
179
      return mDate;
180
    }
181
182
    private StringProperty traceProperty() {
183
      return mTrace;
184
    }
185
186
    @Override
187
    public boolean equals( final Object o ) {
188
      if( this == o ) { return true; }
189
      if( o == null || getClass() != o.getClass() ) { return false; }
190
191
      return Objects.equals( mMessage.get(), ((LogEntry) o).mMessage.get() );
192
    }
193
194
    @Override
195
    public int hashCode() {
196
      return mMessage != null ? mMessage.hashCode() : 0;
197
    }
198
199
    @Override
200
    public String toString() {
201
      final var date = mDate == null ? "" : mDate.get();
202
      final var message = mMessage == null ? "" : mMessage.get();
203
      final var trace = mTrace == null ? "" : mTrace.get();
204
205
      return "LogEntry{" +
206
        "mDate=" + (date == null ? "''" : date) +
207
        ", mMessage=" + (message == null ? "''" : message) +
208
        ", mTrace=" + (trace == null ? "''" : trace) +
209
        '}';
210
    }
211
212
    private String toString( final LocalDateTime date ) {
213
      return date.format( ofPattern( "d MMM u HH:mm:ss" ) );
214
    }
215
  }
216
217
  /**
218
   * Copies the contents of the selected rows into the clipboard; code is from
219
   * <a href="https://stackoverflow.com/a/48126059/59087">StackOverflow</a>.
220
   *
221
   * @param table The {@link TableView} having selected rows to copy.
222
   */
223
  public void copyToClipboard( final TableView<?> table ) {
224
    final var sb = new StringBuilder();
225
    final var rows = new TreeSet<Integer>();
226
    boolean firstRow = true;
227
228
    for( final var position : table.getSelectionModel().getSelectedCells() ) {
229
      rows.add( position.getRow() );
230
    }
231
232
    for( final var row : rows ) {
233
      if( !firstRow ) {
234
        sb.append( '\n' );
235
      }
236
237
      firstRow = false;
238
      boolean firstCol = true;
239
240
      for( final var column : table.getColumns() ) {
241
        if( !firstCol ) {
242
          sb.append( '\t' );
243
        }
244
245
        firstCol = false;
246
        final var data = column.getCellData( row );
247
        sb.append( data == null ? "" : data.toString() );
248
      }
249
    }
250
251
    final var contents = new ClipboardContent();
252
    contents.putString( sb.toString() );
253
    getSystemClipboard().setContent( contents );
254
  }
255
}
1256
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.ParseHeadingEvent;
5
import javafx.scene.Node;
6
import javafx.scene.control.TreeCell;
7
import javafx.scene.control.TreeItem;
8
import javafx.scene.control.TreeView;
9
import javafx.util.Callback;
10
import org.greenrobot.eventbus.Subscribe;
11
12
import static com.keenwrite.events.Bus.register;
13
import static com.keenwrite.events.CaretNavigationEvent.fireCaretNavigationEvent;
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
            fireCaretNavigationEvent( 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/tree/AltTreeCell.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.tree;
3
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.control.TextField;
10
import javafx.scene.control.cell.TextFieldTreeCell;
11
import javafx.scene.input.KeyEvent;
12
import javafx.util.StringConverter;
13
14
import static javafx.application.Platform.runLater;
15
import static javafx.scene.input.KeyCode.ENTER;
16
import static javafx.scene.input.KeyCode.TAB;
17
import static javafx.scene.input.KeyEvent.KEY_RELEASED;
18
19
/**
20
 * Responsible for enhancing the existing cell behaviour with fairly common
21
 * functionality, including commit on focus loss and Enter to commit.
22
 *
23
 * @param <T> The type of data stored by the tree.
24
 */
25
public class AltTreeCell<T> extends TextFieldTreeCell<T> {
26
  private final KeyHandler mKeyHandler = new KeyHandler();
27
  private final Property<String> mInputText = new SimpleStringProperty();
28
  private FocusListener mFocusListener;
29
30
  public AltTreeCell( final StringConverter<T> converter ) {
31
    super( converter );
32
    assert converter != null;
33
34
    // When the text field is added as the graphics context, we hook into
35
    // the changed value to get a handle on the text field. From there it is
36
    // possible to add change the keyboard and focus behaviours.
37
    graphicProperty().addListener( ( c, o, n ) -> {
38
      if( o instanceof TextField ) {
39
        o.removeEventHandler( KEY_RELEASED, mKeyHandler );
40
        o.focusedProperty().removeListener( mFocusListener );
41
      }
42
43
      if( n instanceof final TextField input ) {
44
        n.addEventFilter( KEY_RELEASED, mKeyHandler );
45
        mInputText.bind( input.textProperty() );
46
        mFocusListener = new FocusListener( input );
47
        n.focusedProperty().addListener( mFocusListener );
48
      }
49
    } );
50
  }
51
52
  private void commitEdit() {
53
    commitEdit( getConverter().fromString( mInputText.getValue() ) );
54
  }
55
56
  /**
57
   * Responsible for accepting the text when users press the Enter or Tab key.
58
   */
59
  private class KeyHandler implements EventHandler<KeyEvent> {
60
    @Override
61
    public void handle( final KeyEvent event ) {
62
      if( event.getCode() == ENTER || event.getCode() == TAB ) {
63
        commitEdit();
64
        event.consume();
65
      }
66
    }
67
  }
68
69
  /**
70
   * Responsible for committing edits when focus is lost. This will also
71
   * deselect the input field when focus is gained so that typing text won't
72
   * overwrite the entire existing text.
73
   */
74
  private class FocusListener implements ChangeListener<Boolean> {
75
    private final TextField mInput;
76
77
    private FocusListener( final TextField input ) {
78
      mInput = input;
79
    }
80
81
    @Override
82
    public void changed(
83
      final ObservableValue<? extends Boolean> c,
84
      final Boolean endedFocus, final Boolean beganFocus ) {
85
86
      if( beganFocus ) {
87
        runLater( mInput::deselect );
88
      }
89
      else if( endedFocus ) {
90
        commitEdit();
91
      }
92
    }
93
  }
94
}
195
A src/main/java/com/keenwrite/ui/tree/AltTreeCellFactory.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.ui.tree;
3
4
import javafx.scene.control.TreeCell;
5
import javafx.scene.control.TreeView;
6
import javafx.util.Callback;
7
import javafx.util.StringConverter;
8
9
/**
10
 * Responsible for creating new {@link TreeCell} instances.
11
 * <p>
12
 * TODO: #22 -- Upon refactoring variable functionality, re-instate drag & drop.
13
 * </p>
14
 *
15
 * @param <T> The data type stored in the tree.
16
 */
17
public class AltTreeCellFactory<T>
18
  implements Callback<TreeView<T>, TreeCell<T>> {
19
  private final StringConverter<T> mConverter;
20
21
  public AltTreeCellFactory( final StringConverter<T> converter ) {
22
    mConverter = converter;
23
  }
24
25
  @Override
26
  public TreeCell<T> call( final TreeView<T> treeView ) {
27
    return new AltTreeCell<>( mConverter );
28
  }
29
}
130
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 javafx.scene.control.TreeItem;
5
import javafx.scene.control.TreeView;
6
import javafx.util.StringConverter;
7
8
/**
9
 * Responsible for allowing users to edit items in the tree as well as
10
 * drag and drop. The goal is to be a drop-in replacement for the regular
11
 * JavaFX {@link TreeView}, which does not offer editing and moving {@link
12
 * TreeItem} instances.
13
 *
14
 * @param <T> The type of data to edit.
15
 */
16
public class AltTreeView<T> extends TreeView<T> {
17
  public AltTreeView(
18
    final TreeItem<T> root, final StringConverter<T> converter ) {
19
    super( root );
20
21
    setEditable( true );
22
    setCellFactory( new AltTreeCellFactory<>( converter ) );
23
    setShowRoot( false );
24
25
    // When focus is lost, clear the selected item only when not editing.
26
    focusedProperty().addListener( ( c, o, n ) -> {
27
      if( o && getEditingItem() == null ) {
28
        getSelectionModel().clearSelection();
29
      }
30
    } );
31
  }
32
}
133
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
            if( (result = thisChunk.charAt( i ) - thatChunk.charAt( i )) != 0 ) {
117
              return result;
118
            }
119
          }
120
        }
121
      }
122
      else {
123
        result = thisChunk.compareTo( thatChunk );
124
      }
125
126
      if( result != 0 ) {
127
        return result;
128
      }
129
    }
130
131
    return s1Length - s2Length;
132
  }
133
}
1134
A src/main/java/com/keenwrite/util/ArrayScanner.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import static java.lang.Math.max;
5
6
/**
7
 * Scans an array (haystack) for a particular value (needle).
8
 *
9
 * <p>
10
 * This class is {@code null}-hostile.
11
 */
12
public class ArrayScanner {
13
14
  /**
15
   * The index value returned when an element is not found in an array.
16
   */
17
  public static final int MISSING = -1;
18
19
  /**
20
   * Finds the index of the given needle in the haystack.
21
   *
22
   * @param haystack The haystack to search through for the needle.
23
   * @param needle   The needle to find in the haystack.
24
   * @return Index of the needle within the haystack, or {@link #MISSING}
25
   * if not found.
26
   */
27
  public static int indexOf( final Object[] haystack, final Object needle ) {
28
    assert haystack != null;
29
    assert needle != null;
30
31
    return indexOf( haystack, needle, 0 );
32
  }
33
34
  /**
35
   * Finds the index of the given needle in the haystack.
36
   *
37
   * @param haystack The haystack to search through for the needle.
38
   * @param needle   The needle to find in the haystack.
39
   * @param offset   The starting offset into the haystack to begin looking
40
   *                 (the value may be greater than or less than the number
41
   *                 of array elements).
42
   * @return Index of the needle within the haystack, or {@link #MISSING}
43
   * if not found.
44
   */
45
  public static int indexOf(
46
    final Object[] haystack, final Object needle, int offset ) {
47
    assert haystack != null;
48
    assert needle != null;
49
50
    for( int i = max( 0, offset ); i < haystack.length; i++ ) {
51
      if( needle.equals( haystack[ i ] ) ) {
52
        return i;
53
      }
54
    }
55
56
    return MISSING;
57
  }
58
59
  /**
60
   * Checks if the object is in the given array.
61
   *
62
   * @param haystack The haystack to search through for the needle.
63
   * @param needle   The needle to find in the haystack.
64
   * @return {@code true} if the array contains the object.
65
   */
66
  public static boolean contains(
67
    final Object[] haystack, final Object needle ) {
68
    assert haystack != null;
69
    assert needle != null;
70
71
    return indexOf( haystack, needle ) != MISSING;
72
  }
73
}
174
A src/main/java/com/keenwrite/util/BoundedCache.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.util;
3
4
import java.util.LinkedHashMap;
5
import java.util.Map;
6
7
/**
8
 * A map that removes the oldest entry once its capacity (cache size) has
9
 * been reached.
10
 *
11
 * @param <K> The type of key mapped to a value.
12
 * @param <V> The type of value mapped to a key.
13
 */
14
public final class BoundedCache<K, V> extends LinkedHashMap<K, V> {
15
  private final int mCacheSize;
16
17
  /**
18
   * Constructs a new instance having a finite size.
19
   *
20
   * @param cacheSize The maximum number of entries.
21
   */
22
  public BoundedCache( final int cacheSize ) {
23
    mCacheSize = cacheSize;
24
  }
25
26
  @Override
27
  protected boolean removeEldestEntry( final Map.Entry<K, V> eldest ) {
28
    return size() > mCacheSize;
29
  }
30
}
131
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 && result <= 0;
137
138
    return result;
139
  }
140
}
1141
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.ArrayList;
5
import java.util.List;
6
import java.util.function.BiConsumer;
7
import java.util.function.Consumer;
8
import java.util.function.Function;
9
import java.util.function.Supplier;
10
11
/**
12
 * Responsible for constructing objects that would otherwise require
13
 * a long list of constructor parameters.
14
 * <p>
15
 * See <a href="https://stackoverflow.com/a/31754787/59087">source</a> for
16
 * details.
17
 * </p>
18
 *
19
 * @param <MT> The mutable definition for the type of object to build.
20
 * @param <IT> The immutable definition for the type of object to build.
21
 */
22
public class GenericBuilder<MT, IT> {
23
  /**
24
   * Provides the methods to use for setting object properties.
25
   */
26
  private final Supplier<MT> mMutable;
27
28
  /**
29
   * Calling {@link #build()} will instantiate the immutable instance using
30
   * the mutator.
31
   */
32
  private final Function<MT, IT> mImmutable;
33
34
  /**
35
   * Adds a modifier to call when building an instance.
36
   */
37
  private final List<Consumer<MT>> mModifiers = new ArrayList<>();
38
39
  /**
40
   * Constructs a new builder instance that is capable of populating values for
41
   * any type of object.
42
   *
43
   * @param mutator Provides methods to use for setting object properties.
44
   */
45
  protected GenericBuilder(
46
      final Supplier<MT> mutator, final Function<MT, IT> immutable ) {
47
    assert mutator != null;
48
    assert immutable != null;
49
50
    mMutable = mutator;
51
    mImmutable = immutable;
52
  }
53
54
  /**
55
   * Starting point for building an instance of a particular class.
56
   *
57
   * @param supplier Returns the instance to build.
58
   * @param <MT>     The type of class to build.
59
   * @return A new {@link GenericBuilder} capable of populating data for an
60
   * instance of the class provided by the {@link Supplier}.
61
   */
62
  public static <MT, IT> GenericBuilder<MT, IT> of(
63
      final Supplier<MT> supplier, final Function<MT, IT> immutable ) {
64
    return new GenericBuilder<>( supplier, immutable );
65
  }
66
67
  /**
68
   * Registers a new value with the builder.
69
   *
70
   * @param consumer Accepts a value to be set upon the built object.
71
   * @param value    The value to use when building.
72
   * @param <V>      The type of value used when building.
73
   * @return This {@link GenericBuilder} instance.
74
   */
75
  public <V> GenericBuilder<MT, IT> with(
76
      final BiConsumer<MT, V> consumer, final V value ) {
77
    mModifiers.add( instance -> consumer.accept( instance, value ) );
78
    return this;
79
  }
80
81
  /**
82
   * Instantiates then populates the immutable object to build.
83
   *
84
   * @return The newly built object.
85
   */
86
  public IT build() {
87
    final var value = mMutable.get();
88
    mModifiers.forEach( modifier -> modifier.accept( value ) );
89
    mModifiers.clear();
90
    return mImmutable.apply( value );
91
  }
92
}
193
A src/main/java/com/keenwrite/util/ProtocolScheme.java
1
/* Copyright 2020-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 short-hand 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/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/r/README.md
1
# R Scripts
2
3
These R scripts illustrate how R can be used within an application to perform calculations using variables. Authors are free to write their own scripts, of course. These scripts serve as an example of how to automate certain tasks while writing.
4
5
## Configuration
6
7
Configure the editor to use the R scripts as follows:
8
9
1. Copy the R scripts into same directory as your Markdown files.
10
1. Start the editor.
11
1. Click **Tools → R Script**.
12
1. Copy and paste the following:
13
14
        assign( 'anchor', as.Date( '$date.anchor$', format='%Y-%m-%d' ), envir = .GlobalEnv );
15
        setwd( '$application.r.working.directory$' );
16
        source( 'pluralize.R' );
17
        source( 'csv.R' );
18
        source( 'conversion.R' );
19
20
1. Click **File → New** to create a new file.
21
1. Click **File → Save As** to set a filename.
22
1. Set **Name** to: `variables.yaml`
23
1. Click **OK**.
24
1. Paste the following definitions:
25
26
        date:
27
          anchor: 2017-01-01
28
        editor:
29
          examples:
30
            season: 2017-09-02
31
            math:
32
              x: 1
33
              y: $editor.examples.math.x$ + 1
34
              z: $editor.examples.math.y$ + 1
35
            name:
36
              given: Josephene
37
38
1. Save and close the file.
39
1. Click **File → Open**
40
1. Change **Markdown Files** to **Definition Files**.
41
1. Select `variables.yaml`.
42
1. Click **Open**.
43
44
R functionality is configured.
45
46
## Definitions
47
48
The variables definitions within `variables.yaml` are available to R using the R syntax. An additional variable, `application.r.working.directory` is added to the list of variables. The value is set to the working directory of the file being edited. Hover the mouse cursor over the file tab in the editor to see the full path to the file.
49
50
## Examples
51
52
This section demonstrates how to use the R functions when editing. Complete the following steps to begin:
53
54
1. Click **File → New** to create a new file.
55
1. Click **File → Save As** to set a filename.
56
1. Set **Name** to: `example.Rmd`
57
1. Click **OK**.
58
59
The examples are ready for use within the editor.
60
61
### Arithmetic
62
63
Type the following to perform a simple calculation:
64
65
    `r# 1+1`
66
67
The preview pane shows `2.0`.
68
69
### Functions
70
71
Call the [format](https://stat.ethz.ch/R-manual/R-devel/library/base/html/format.html) function to truncate unwanted decimal places as follows:
72
73
    `r# format(1+1,digits=1)`
74
75
The preview pane shows `2`.
76
77
### Pluralize
78
79
Many English words can be pluralized as follows:
80
81
    `r# pl('wolf',2)`
82
83
The preview pane shows `wolves`. The `pluralize.R` file contains a partial implementation of Damian Conway's algorithmic approach to English pluralization.
84
85
### Chicago Manual of Style
86
87
Apply the Chicago Manual of Style for words less than one-hundred as follows:
88
89
       `r# cms(1)` `r# cms(99)` `r# cms(101)`
90
91
The preview pane shows numbers written out as `one` and `ninety-nine`, followed by the digits 101.
92
93
### Data Import
94
95
Import and display information from a CSV file as follows:
96
97
1. Click **File → New** to create a new file.
98
1. Click **File → Save As** to rename the file.
99
1. Set the filename to: `data.csv`
100
1. Paste the following into `data.csv`:
101
102
        Animal,Quantity,Country
103
        Aardwolf,1,Africa
104
        Keel-billed toucan,1,Belize
105
        Beaver,2,Canada
106
        Mute swan,3,Denmark
107
        Lion,5,Ethiopia
108
        Brown bear,8,Finland
109
        Dolphin,13,Greece
110
        Turul,21,Hungary
111
        Gyrfalcon,34,Iceland
112
        Red-billed streamertail,55,Jamaica
113
114
1. Click the `example.Rmd` tab.
115
1. Type the following:
116
117
       `r# csv2md('data.csv',total=F)`
118
119
1. Type the following to calculate a total for all numeric columns:
120
121
       `r# csv2md('data.csv')`
122
123
This imports the data from an external file and formats the information into a table, automatically. Update the data as follows:
124
125
1. Click the `data.csv` tab to edit the data.
126
1. Change the data by adding a new row.
127
1. Save the file.
128
1. Click the `example.Rmd` tab.
129
130
The preview pane shows the revised contents.
131
132
### Elapsed Time
133
134
The duration of a timeline, given in numbers of days, can be computed into English as follows:
135
136
    `r# elapsed(1,1)`
137
138
The preview pane shows `same day`. Change the expression to:
139
140
    `r# elapsed(1,2)`
141
142
The preview pane shows `one day`. Change the expression to:
143
144
    `r# elapsed(1,112358)`
145
146
The preview pane shows `307 years, seven months, and sixteen days`, combined using the Chicago Manual of Style, the pluralization function, and a [serial comma](https://www.behance.net/gallery/19417363/The-Oxford-Comma).
147
148
### Variable Syntax
149
150
The syntax for a variable changes when using an R Markdown file (denoted by the `.Rmd` filename extension), as opposed to a regular Markdown file (`.md`). Return to the example file and type the following:
151
152
    `r# v$date$anchor`
153
154
The preview pane shows the date.
155
156
### Autocomplete
157
158
Automatically insert a variable reference into the text as follows:
159
160
1. Type: `Jos`
161
    * Note the capital letter, matches are case sensitive.
162
1. Hold down the `Control` key.
163
1. Tap the `Spacebar`
164
165
The editor shows:
166
167
    `r#x( v$editor$examples$name$given )`
168
169
The preview pane shows:
170
171
    Josephine
172
173
Here, the `x` function evaluates its parameter as an expression. This allows variables to include expressions in their definition.
174
175
### Variable Definition Expressions
176
177
Definition file variables are have the ability to reference other definitions. Try the following:
178
179
    x = `r#x( v$editor$examples$math$x )`;
180
    y = `r#x( v$editor$examples$math$y )`;
181
    z = `r#x( v$editor$examples$math$z )`
182
183
The preview pane shows:
184
185
    x = 1.0; y = 2.0; z = 3.0
186
187
### Case
188
189
Ensure words begin with a lowercase letter as follows:
190
191
    `r#lc( v$editor$examples$name$given )`
192
193
The preview pane shows:
194
195
    josephine
196
197
Similarly, ensure an uppercase letter as follows:
198
199
    `r#uc( 'hello, world!' )`
200
201
The preview pane shows:
202
203
    Hello, world!
204
205
### Month
206
207
Display the month name given a month number as follows:
208
209
    `r# month( 1 )`
210
211
The preview pane shows:
212
213
    January
214
215
## Summary
216
217
Authors can inline R statements into documents, directly, so long as those statements generate text. Plots, graphs, and images must be referenced as external image files or URLs.
1218
A src/main/r/conversion.R
1
# ########################################################################
2
#
3
# Substitute R expressions in a document with their evaluated value. The
4
# anchor variable must be set for functions that use relative dates.
5
#
6
# ########################################################################
7
8
# Evaluates an expression; writes s if there is no expression.
9
x <- function( s ) {
10
  return(
11
    tryCatch({
12
      r = eval( parse( text=s ) )
13
14
      # If the result isn't primitive, then it was probably parsed into
15
      # an unprintable object (e.g., "gray" becomes a colour). In those
16
      # cases, return the original text string. Otherwise, an atomic
17
      # value means a primitive type (string, integer, etc.) that can be
18
      # written directly into the document.
19
      #
20
      # See: http://stackoverflow.com/a/19501276/59087
21
      if( is.atomic( r ) ) {
22
        r
23
      }
24
      else {
25
        s
26
      }
27
    },
28
    warning = function( w ) {
29
      s
30
    },
31
    error = function( e ) {
32
      s
33
    })
34
  )
35
}
36
37
# Returns a date offset by a given number of days, relative to the given
38
# date (d). This does not use the anchor, but is used to get the anchor's
39
# value as a date.
40
when <- function( d, n = 0, format = "%Y-%m-%d" ) {
41
  as.Date( d, format = format ) + x( n )
42
}
43
44
# Full date (s) offset by an optional number of days before or after.
45
# This will remove leading zeros (applying leading spaces instead, which
46
# are ignored by any worthwhile typesetting engine).
47
annal <- function( days = 0, format = "%Y-%m-%d", oformat = "%B %d, %Y" ) {
48
  format( when( anchor, days ), format = oformat )
49
}
50
51
# Extracts the year from a date string.
52
year <- function( days = 0, format = "%Y-%m-%d" ) {
53
  annal( days, format, "%Y" )
54
}
55
56
# Day of the week (in days since the anchor date).
57
weekday <- function( n ) {
58
  weekdays( when( anchor, n ) )
59
}
60
61
# String concatenate function alias because paste0 is a terrible name.
62
concat <- paste0
63
64
# Translates a number from digits to words using Chicago Manual of Style.
65
# This does not translate numbers greater than one hundred. If ordinal
66
# is TRUE, this will return the ordinal name. This will not produce ordinals
67
# for numbers greater than 100
68
cms <- function( n, ordinal = FALSE ) {
69
  n <- x( n )
70
71
  # We're done here.
72
  if( n == 0 ) {
73
    if( ordinal ) {
74
      return( "zeroth" )
75
    }
76
77
    return( "zero" )
78
  }
79
80
  # Concatenate this a little later.
81
  if( n < 0 ) {
82
    result = "negative "
83
    n = abs( n )
84
  }
85
86
  # Do not spell out numbers greater than one hundred.
87
  if( n > 100 ) {
88
    # Comma-separated numbers.
89
    return( format( n, big.mark=",", trim=TRUE, scientific=FALSE ) )
90
  }
91
92
  # Don't go beyond 100.
93
  if( n == 100 ) {
94
    if( ordinal ) {
95
      return( "one hundredth" )
96
    }
97
98
    return( "one hundred" )
99
  }
100
101
  # Samuel Langhorne Clemens noted English has too many exceptions.
102
  small = c(
103
    "one", "two", "three", "four", "five",
104
    "six", "seven", "eight", "nine", "ten",
105
    "eleven", "twelve", "thirteen", "fourteen", "fifteen",
106
    "sixteen", "seventeen", "eighteen", "nineteen"
107
  )
108
109
  ord_small = c(
110
    "first", "second", "third", "fourth", "fifth",
111
    "sixth", "seventh", "eighth", "ninth", "tenth",
112
    "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth",
113
    "sixteenth", "seventeenth", "eighteenth", "nineteenth", "twentieth"
114
  )
115
116
  # After this, the number (n) is between 20 and 99.
117
  if( n < 20 ) {
118
    if( ordinal ) {
119
      return( .subset( ord_small, n %% 100 ) )
120
    }
121
122
    return( .subset( small, n %% 100 ) )
123
  }
124
125
  tens = c( "",
126
    "twenty", "thirty", "forty", "fifty",
127
    "sixty", "seventy", "eighty", "ninety"
128
  )
129
130
  ord_tens = c( "",
131
    "twentieth", "thirtieth", "fortieth", "fiftieth",
132
    "sixtieth", "seventieth", "eightieth", "ninetieth"
133
  )
134
135
  ones_index = n %% 10
136
  n = n %/% 10
137
138
  # No number in the ones column, so the number must be a multiple of ten.
139
  if( ones_index == 0 ) {
140
    if( ordinal ) {
141
      return( .subset( ord_tens, n ) )
142
    }
143
144
    return( .subset( tens, n ) )
145
  }
146
147
  # Find the value from the ones column.
148
  if( ordinal ) {
149
    unit_1 = .subset( ord_small, ones_index )
150
  }
151
  else {
152
    unit_1 = .subset( small, ones_index )
153
  }
154
155
  # Find the tens column.
156
  unit_10 = .subset( tens, n )
157
158
  # Hyphenate the tens and the ones together.
159
  concat( unit_10, concat( "-", unit_1 ) )
160
}
161
162
# Returns a human-readable string that provides the elapsed time between
163
# two numbers in terms of years, months, and days. If any unit value is zero,
164
# the unit is not included. The words (year, month, day) are pluralized
165
# according to English grammar. The numbers are written out according to
166
# Chicago Manual of Style. This applies the serial comma.
167
#
168
# Both numbers are offsets relative to the anchor date.
169
#
170
# If all unit values are zero, this returns s ("same day" by default).
171
#
172
# If the start date (began) is greater than end date (ended), the dates are
173
# swapped before calculations are performed. This allows any two dates
174
# to be compared and positive unit values are always returned.
175
#
176
elapsed <- function( began, ended, s = "same day" ) {
177
  began = when( anchor, began )
178
  ended = when( anchor, ended )
179
180
  # Swap the dates if the end date comes before the start date.
181
  if( as.integer( ended - began ) < 0 ) {
182
    tempd = began
183
    began = ended
184
    ended = tempd
185
  }
186
187
  # Calculate number of elapsed years.
188
  years = length( seq( from = began, to = ended, by = 'year' ) ) - 1
189
190
  # Move the start date up by the number of elapsed years.
191
  if( years > 0 ) {
192
    began = seq( began, length = 2, by = concat( years, " years" ) )[2]
193
    years = pl.numeric( "year", years )
194
  }
195
  else {
196
    # Zero years.
197
    years = ""
198
  }
199
200
  # Calculate number of elapsed months, excluding years.
201
  months = length( seq( from = began, to = ended, by = 'month' ) ) - 1
202
203
  # Move the start date up by the number of elapsed months
204
  if( months > 0 ) {
205
    began = seq( began, length = 2, by = concat( months, " months" ) )[2]
206
    months = pl.numeric( "month", months )
207
  }
208
  else {
209
    # Zero months
210
    months = ""
211
  }
212
213
  # Calculate number of elapsed days, excluding months and years.
214
  days = length( seq( from = began, to = ended, by = 'day' ) ) - 1
215
216
  if( days > 0 ) {
217
    days = pl.numeric( "day", days )
218
  }
219
  else {
220
    # Zero days
221
    days = ""
222
  }
223
224
  if( years <= 0 && months <= 0 && days <= 0 ) {
225
    return( s )
226
  }
227
228
  # Put them all in a vector, then remove the empty values.
229
  s <- c( years, months, days )
230
  s <- s[ s != "" ]
231
232
  r <- paste( s, collapse = ", " )
233
234
  # If all three items are present, replace the last comma with ", and".
235
  if( length( s ) > 2 ) {
236
    return( gsub( "(.*),", "\\1, and", r ) )
237
  }
238
239
  # Does nothing if no commas are present.
240
  gsub( "(.*),", "\\1 and", r )
241
}
242
243
# Returns the number (n) in English followed by the plural or singular
244
# form of the given string (s; resumably a noun), if applicable, according
245
# to English grammar. That is, pl.numeric( "wolf", 5 ) will return
246
# "five wolves".
247
pl.numeric <- function( s, n ) {
248
  concat( cms( n ), concat( " ", pluralize( s, n ) ) )
249
}
250
251
# Name of the season, starting with an capital letter.
252
season <- function( n, format = "%Y-%m-%d" ) {
253
  WS <- as.Date("2016-12-15", "%Y-%m-%d") # Winter Solstice
254
  SE <- as.Date("2016-03-15", "%Y-%m-%d") # Spring Equinox
255
  SS <- as.Date("2016-06-15", "%Y-%m-%d") # Summer Solstice
256
  AE <- as.Date("2016-09-15", "%Y-%m-%d") # Autumn Equinox
257
258
  d <- when( anchor, n )
259
  d <- as.Date( strftime( d, format="2016-%m-%d" ) )
260
261
  ifelse( d >= WS | d < SE, "Winter",
262
    ifelse( d >= SE & d < SS, "Spring",
263
      ifelse( d >= SS & d < AE, "Summer", "Autumn" )
264
    )
265
  )
266
}
267
268
# Converts the first letter in a string to lowercase
269
lc <- function( s ) {
270
  concat( tolower( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
271
}
272
273
# Converts the first letter in a string to uppercase
274
uc <- function( s ) {
275
  concat( toupper( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
276
}
277
278
# Returns the number of days between the given dates.
279
days <- function( d1, d2, format = "%Y-%m-%d" ) {
280
  dates = c( d1, d2 )
281
  dt = strptime( dates, format = format )
282
  as.integer( difftime( dates[2], dates[1], units = "days" ) )
283
}
284
285
# Returns the number of years elapsed.
286
years <- function( began, ended ) {
287
  began = when( anchor, began )
288
  ended = when( anchor, ended )
289
290
  # Swap the dates if the end date comes before the start date.
291
  if( as.integer( ended - began ) < 0 ) {
292
    tempd = began
293
    began = ended
294
    ended = tempd
295
  }
296
297
  # Calculate number of elapsed years.
298
  length( seq( from = began, to = ended, by = 'year' ) ) - 1
299
}
300
301
# Full name of the month, starting with a capital letter.
302
month <- function( n ) {
303
  # Faster than month.name[ x( n ) ]
304
  .subset( month.name, x( n ) )
305
}
306
307
money <- function( n ) {
308
  formatC( x( n ), format="d" )
309
}
1310
A src/main/r/csv.R
1
# ######################################################################
2
#
3
# Copyright 2016, White Magic Software, Ltd.
4
# 
5
# Permission is hereby granted, free of charge, to any person obtaining
6
# a copy of this software and associated documentation files (the
7
# "Software"), to deal in the Software without restriction, including
8
# without limitation the rights to use, copy, modify, merge, publish,
9
# distribute, sublicense, and/or sell copies of the Software, and to
10
# permit persons to whom the Software is furnished to do so, subject to
11
# the following conditions:
12
# 
13
# The above copyright notice and this permission notice shall be
14
# included in all copies or substantial portions of the Software.
15
# 
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
#
24
# ######################################################################
25
26
# ######################################################################
27
#
28
# Converts CSV to Markdown.
29
#
30
# ######################################################################
31
32
# Reads a CSV file and converts the contents to a Markdown table. The
33
# file must be in the working directory as specified by setwd.
34
#
35
# @param f The filename to convert.
36
# @param decimals Rounded decimal places (default 1).
37
# @param totals Include total sums (default TRUE).
38
# @param align Right-align numbers (default TRUE).
39
csv2md <- function( f, decimals = 1, totals = T, align = T ) {
40
  # Read the CVS data from the file; ensure strings become characters.
41
  df <- read.table( f, sep=',', header=T, stringsAsFactors=F )
42
43
  if( totals ) {
44
    # Determine what columns can be summed.
45
    number <- which( unlist( lapply( df, is.numeric ) ) )
46
47
    # Use colSums when more than one summable column exists.
48
    if( length( number ) > 1 ) {
49
      f.sum <- colSums
50
    }
51
    else {
52
      f.sum <- sum
53
    }
54
55
    # Calculate the sum of all the summable columns and insert the
56
    # results back into the data frame.
57
    df[ (nrow( df ) + 1), number ] <- f.sum( df[, number], na.rm=TRUE )
58
59
    # pluralize would be heavyweight here.
60
    if( length( number ) > 1 ) {
61
      t <- "**Totals**"
62
    }
63
    else {
64
      t <- "**Total**"
65
    }
66
67
    # Change the first column of the last line to "Total(s)".
68
    df[ nrow( df ), 1 ] <- t
69
70
    # Don't clutter the output with "NA" text.
71
    df[ is.na( df ) ] <- ""
72
  }
73
74
  if( align ) {
75
    is.char <- vapply( df, is.character, logical( 1 ) )
76
    dashes <- paste( ifelse( is.char, ':---', '---:' ), collapse='|' )
77
  }
78
  else {
79
    dashes <- paste( rep( '---', length( df ) ), collapse = '|')
80
  }
81
82
  # Create a Markdown version of the data frame.
83
  paste(
84
    paste( names( df ), collapse = '|'), '\n',
85
    dashes, '\n', 
86
    paste(
87
      Reduce( function( x, y ) {
88
          paste( x, format( y, digits = decimals ), sep = '|' )
89
        }, df
90
      ),
91
      collapse = '|\n', sep=''
92
    )
93
  )
94
}
95
196
A src/main/r/pluralize.R
1
# ######################################################################
2
#
3
# Copyright 2016, White Magic Software, Ltd.
4
# 
5
# Permission is hereby granted, free of charge, to any person obtaining
6
# a copy of this software and associated documentation files (the
7
# "Software"), to deal in the Software without restriction, including
8
# without limitation the rights to use, copy, modify, merge, publish,
9
# distribute, sublicense, and/or sell copies of the Software, and to
10
# permit persons to whom the Software is furnished to do so, subject to
11
# the following conditions:
12
# 
13
# The above copyright notice and this permission notice shall be
14
# included in all copies or substantial portions of the Software.
15
# 
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
#
24
# ######################################################################
25
26
# ######################################################################
27
#
28
# See Damian Conway's "An Algorithmic Approach to English Pluralization":
29
#   http://goo.gl/oRL4MP
30
# See Oliver Glerke's Evo Inflector: https://github.com/atteo/evo-inflector/
31
# See Shevek's Pluralizer: https://github.com/shevek/linguistics/
32
# See also: http://www.freevectors.net/assets/files/plural.txt
33
#
34
# ######################################################################
35
36
pluralize <- function( s, n ) {
37
  result <- s
38
39
  # Partial implementation of Conway's algorithm for nouns.
40
  if( n != 1 ) {
41
    if( pl.noninflective( s ) ||
42
        pl.suffix( "fish", s ) ||
43
        pl.suffix( "ois", s ) ||
44
        pl.suffix( "sheep", s ) ||
45
        pl.suffix( "deer", s ) ||
46
        pl.suffix( "pox", s ) ||
47
        pl.suffix( "[A-Z].*ese", s ) ||
48
        pl.suffix( "itis", s ) ) {
49
      # 1. Retain non-inflective user-mapped noun as is.
50
      # 2. Retain non-inflective plural as is.
51
      result <- s
52
    }
53
    else if( pl.is.irregular.pl( s ) ) {
54
      # 4. Change irregular plurals based on mapping.
55
      result <- pl.irregular.pl( s )
56
    }
57
    else if( pl.is.irregular.es( s ) ) {
58
      # x. From Shevek's Pluralizer
59
      result <- pl.inflect( s, "", "es" )
60
    }
61
    else if( pl.suffix( "man", s ) ) {
62
      # 5. For -man, change -an to -en
63
      result <- pl.inflect( s, "an", "en" )
64
    }
65
    else if( pl.suffix( "[lm]ouse", s ) ) {
66
      # 5. For [lm]ouse, change -ouse to -ice
67
      result <- pl.inflect( s, "ouse", "ice" )
68
    }
69
    else if( pl.suffix( "tooth", s ) ) {
70
      # 5. For -tooth, change -ooth to -eeth
71
      result <- pl.inflect( s, "ooth", "eeth" )
72
    }
73
    else if( pl.suffix( "goose", s ) ) {
74
      # 5. For -goose, change -oose to -eese
75
      result <- pl.inflect( s, "oose", "eese" )
76
    }
77
    else if( pl.suffix( "foot", s ) ) {
78
      # 5. For -foot, change -oot to -eet
79
      result <- pl.inflect( s, "oot", "eet" )
80
    }
81
    else if( pl.suffix( "zoon", s ) ) {
82
      # 5. For -zoon, change -on to -a
83
      result <- pl.inflect( s, "on", "a" )
84
    }
85
    else if( pl.suffix( "[csx]is", s ) ) {
86
      # 5. Change -cis, -sis, -xis to -es
87
      result <- pl.inflect( s, "is", "es" )
88
    }
89
    else if( pl.suffix( "([cs]h|ss)", s ) ) {
90
      # 8. Change -ch, -sh, -ss to -es
91
      result <- pl.inflect( s, "", "es" )
92
    }
93
    else if( pl.suffix( "([aeo]lf|[^d]eaf|arf)", s ) ) {
94
      # 9. Change -f to -ves
95
      result <- pl.inflect( s, "f", "ves" )
96
    }
97
    else if( pl.suffix( "[nlw]ife", s ) ) {
98
      # 9. Change -fe to -ves
99
      result <- pl.inflect( s, "fe", "ves" )
100
    }
101
    else if( pl.suffix( "([aeiou]y|[A-Z].*y)", s ) ) {
102
      # 10. Change -y to -ys.
103
      result <- pl.inflect( s, "", "s" )
104
    }
105
    else if( pl.suffix( "y", s ) ) {
106
      # 10. Change -y to -ies.
107
      result <- pl.inflect( s, "y", "ies" )
108
    }
109
    else {
110
      # 13. Default plural: add -s.
111
      result <- pl.inflect( s, "", "s" )
112
    }
113
  }
114
115
  result
116
}
117
118
# Pluralize s if n is not equal to 1.
119
pl <- function( s, n ) {
120
  pluralize( s, x( n ) )
121
}
122
123
# Returns the given string (s) with its suffix replaced by r.
124
pl.inflect <- function( s, suffix, r ) {
125
  gsub( paste( suffix, "$", sep="" ), r, s )
126
}
127
128
# Answers whether the given string (s) has the given ending.
129
pl.suffix <- function( ending, s ) {
130
  grepl( paste( ending, "$", sep="" ), s )
131
}
132
133
# Answers whether the given string (s) is a noninflective noun.
134
pl.noninflective <- function( s ) {
135
  v <- c(
136
    "aircraft", "Bhutanese", "bison", "bream", "breeches", "britches",
137
    "Burmese", "carp", "chassis", "Chinese", "clippers", "cod", "contretemps",
138
    "corps", "debris", "diabetes", "djinn", "eland", "elk", "flounder",
139
    "fracas", "gallows", "graffiti", "headquarters", "herpes", "high-jinks",
140
    "homework", "hovercraft", "innings", "jackanapes", "Japanese",
141
    "Lebanese", "mackerel", "means", "measles", "mews", "mumps", "news",
142
    "pincers", "pliers", "Portuguese", "proceedings", "rabies", "salmon",
143
    "scissors", "sea-bass", "Senegalese", "series", "shears", "Siamese",
144
    "Sinhalese", "spacecraft", "species", "swine", "trout", "tuna",
145
    "Vietnamese", "watercraft", "whiting", "wildebeest"
146
  )
147
148
  is.element( s, v )
149
}
150
151
# Answers whether the given string (s) is an irregular plural.
152
pl.is.irregular.pl <- function( s ) {
153
  # Could be refactored with pl.irregular.pl...
154
  v <- c(
155
    "beef", "brother", "child", "cow", "ephemeris", "genie", "money",
156
    "mongoose", "mythos", "octopus", "ox", "soliloquy", "trilby"
157
  )
158
159
  is.element( s, v )
160
}
161
162
# Call to pluralize an irregular noun. Only call after confirming
163
# the noun is irregular via pl.is.irregular.pl.
164
pl.irregular.pl <- function( s ) {
165
  v <- list(
166
    "beef" = "beefs",
167
    "brother" = "brothers",
168
    "child" = "children",
169
    "cow" = "cows",
170
    "ephemeris" = "ephemerides",
171
    "genie" = "genies",
172
    "money" = "moneys",
173
    "mongoose" = "mongooses",
174
    "mythos" = "mythoi",
175
    "octopus" = "octopuses",
176
    "ox" = "oxen",
177
    "soliloquy" = "soliloquies",
178
    "trilby" = "trilbys"
179
  )
180
181
  # Faster version of v[[ s ]]
182
  .subset2( v, s )
183
}
184
185
# Answers whether the given string (s) pluralizes with -es.
186
pl.is.irregular.es <- function( s ) {
187
  v <- c(
188
    "acropolis", "aegis", "alias", "asbestos", "bathos", "bias", "bronchitis",
189
    "bursitis", "caddis", "cannabis", "canvas", "chaos", "cosmos", "dais",
190
    "digitalis", "epidermis", "ethos", "eyas", "gas", "glottis", "hubris",
191
    "ibis", "lens", "mantis", "marquis", "metropolis", "pathos", "pelvis",
192
    "polis", "rhinoceros", "sassafrass", "trellis"
193
  )
194
195
  is.element( s, v )
196
}
197
1198
A src/main/resources/META-INF/services/com.keenwrite.service.Settings
1
1
com.keenwrite.service.impl.DefaultSettings
A src/main/resources/META-INF/services/com.keenwrite.service.events.Notifier
1
1
com.keenwrite.service.events.impl.DefaultNotifier
A src/main/resources/bootstrap.properties
1
# Used by the Gradle build script and the application.
2
application.title=KeenWrite
3
14
A src/main/resources/com/keenwrite/.gitignore
1
app.properties
12
A src/main/resources/com/keenwrite/app-title.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
   xmlns:cc="http://creativecommons.org/ns#"
5
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
   xmlns:svg="http://www.w3.org/2000/svg"
7
   xmlns="http://www.w3.org/2000/svg"
8
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
   version="1.1"
11
   width="971.53119"
12
   height="498.39355"
13
   viewBox="0 0 971.53119 498.39354"
14
   xml:space="preserve"
15
   id="svg52"
16
   sodipodi:docname="app-title.svg"
17
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
18
   inkscape:export-filename="/home/jarvisd/dev/java/scrivenvar/docs/images/app-title.png"
19
   inkscape:export-xdpi="24.66"
20
   inkscape:export-ydpi="24.66"><metadata
21
   id="metadata56"><rdf:RDF><cc:Work
22
       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
23
         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
24
   inkscape:document-rotation="0"
25
   pagecolor="#ffffff"
26
   bordercolor="#666666"
27
   borderopacity="1"
28
   objecttolerance="10"
29
   gridtolerance="10"
30
   guidetolerance="10"
31
   inkscape:pageopacity="0"
32
   inkscape:pageshadow="2"
33
   inkscape:window-width="640"
34
   inkscape:window-height="480"
35
   id="namedview54"
36
   showgrid="false"
37
   inkscape:zoom="0.78417969"
38
   inkscape:cx="455.5775"
39
   inkscape:cy="347.59625"
40
   inkscape:current-layer="svg52"
41
   fit-margin-top="0"
42
   fit-margin-left="0"
43
   fit-margin-right="0"
44
   fit-margin-bottom="0" />
45
<desc
46
   id="desc2">Created with Fabric.js 3.6.3</desc>
47
<defs
48
   id="defs4"><rect
49
   x="114.92139"
50
   y="132.06313"
51
   width="470.12033"
52
   height="175.55823"
53
   id="rect933" />
54
55
56
57
		
58
		
59
		
60
		
61
		
62
		
63
		
64
		
65
66
<linearGradient
67
   y2="-0.049471263"
68
   x2="0.96880889"
69
   y1="-0.044911571"
70
   x1="0.15235768"
71
   gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)"
72
   gradientUnits="userSpaceOnUse"
73
   id="SVGID_1_302284">
74
<stop
75
   id="stop9"
76
   style="stop-color:#ec706a;stop-opacity:1"
77
   offset="0%" />
78
<stop
79
   id="stop11"
80
   style="stop-color:#ecd980;stop-opacity:1"
81
   offset="100%" />
82
</linearGradient>
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
</defs>
99
100
<g
101
   id="g853"
102
   transform="translate(-394.35834,-171.20491)"><path
103
     style="fill:url(#SVGID_1_302284);fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
104
     paint-order="stroke"
105
     d="m 425.11895,550.88213 c -46.93797,72.14807 -26.19433,103.38343 -26.19433,103.38343 v 0 c 0,0 31.07048,-45.59403 48.81648,-27.97293 v 0 c 15.24298,15.10308 -12.06548,43.30583 -12.06548,43.30583 v 0 c 0,0 166.06898,-68.436 89.90407,-144.24619 v 0 c 0,0 -16.00237,-18.40049 -39.62873,-18.40548 v 0 c -17.28637,0 -38.64951,9.84223 -60.83201,43.93534"
106
     stroke-linecap="round"
107
     id="path14" /><path
108
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
109
     paint-order="stroke"
110
     d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z"
111
     stroke-linecap="round"
112
     id="path22" /><path
113
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
114
     paint-order="stroke"
115
     d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z"
116
     stroke-linecap="round"
117
     id="path26" /><path
118
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
119
     paint-order="stroke"
120
     d="m 791.45508,258.2912 c -6.12517,-3.44728 -14.03892,-2.61294 -19.29478,2.61793 -6.36997,6.33501 -6.39495,16.63688 -0.0649,23.00186 L 613.81523,441.29182 583.28931,410.60103 c 96.04423,-96.4489 126.74501,-177.76974 126.74501,-177.76974 79.22249,-11.81068 139.14522,-43.08601 168.97169,-61.62638 z"
121
     stroke-linecap="round"
122
     id="path30" /><path
123
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
124
     paint-order="stroke"
125
     d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z"
126
     stroke-linecap="round"
127
     id="path34" /><path
128
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
129
     paint-order="stroke"
130
     d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z"
131
     stroke-linecap="round"
132
     id="path38" /><path
133
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
134
     paint-order="stroke"
135
     d="m 798.0649,265.0575 87.61088,-87.14624 c -18.72523,29.76151 -50.29032,89.4844 -62.52567,168.64194 0,0 -77.5688,34.88248 -178.68403,125.55095 L 613.81527,441.28846 772.09539,283.91262 c 6.35998,6.39496 16.63687,6.38996 23.00185,0.06 5.14095,-5.10597 6.11018,-12.8049 2.96766,-18.91508"
136
     stroke-linecap="round"
137
     id="path42" /></g>
138
139
<text
140
   xml:space="preserve"
141
   id="text931"
142
   style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect933);fill:#000000;fill-opacity:1;stroke:none;"
143
   transform="translate(-394.35834,-171.20491)" /><text
144
   xml:space="preserve"
145
   style="font-style:italic;font-variant:normal;font-weight:800;font-stretch:normal;font-size:133.333px;line-height:1.25;font-family:'Merriweather Sans';-inkscape-font-specification:'Merriweather Sans, Ultra-Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#51a9cf;fill-opacity:1;stroke:none"
146
   x="311.66693"
147
   y="402.20627"
148
   id="text939"><tspan
149
     sodipodi:role="line"
150
     id="tspan937"
151
     x="311.66693"
152
     y="402.20627">KeenWrite</tspan></text></svg>
1153
A src/main/resources/com/keenwrite/build.sh
1
#!/bin/bash
2
3
INKSCAPE="/usr/bin/inkscape"
4
PNG_COMPRESS="optipng"
5
PNG_COMPRESS_OPTS="-o9 *png"
6
ICO_TOOL="icotool"
7
ICO_TOOL_OPTS="-c -o ../../../../../icons/logo.ico logo64.png"
8
9
declare -a SIZES=("16" "32" "64" "128" "256" "512")
10
11
for i in "${SIZES[@]}"; do
12
  # -y: export background opacity 0
13
  $INKSCAPE -y 0 -w "${i}" --export-overwrite --export-type=png -o "logo${i}.png" "logo.svg" 
14
done
15
16
# Compess the PNG images.
17
which $PNG_COMPRESS && $PNG_COMPRESS $PNG_COMPRESS_OPTS
18
19
# Generate an ICO file.
20
which $ICO_TOOL && $ICO_TOOL $ICO_TOOL_OPTS
21
122
A src/main/resources/com/keenwrite/editor/markdown.css
1
/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
2
3
.markdown {
4
  -fx-font-family: 'Noto Sans';
5
  -fx-font-size: 11pt;
6
  -fx-padding: .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_en-Latn-AU.css
11
A src/main/resources/com/keenwrite/editor/markdown_en-Latn-CA.css
11
A src/main/resources/com/keenwrite/editor/markdown_en-Latn-GB.css
11
A src/main/resources/com/keenwrite/editor/markdown_en-Latn-NZ.css
11
A src/main/resources/com/keenwrite/editor/markdown_en-Latn-US.css
11
A src/main/resources/com/keenwrite/editor/markdown_en-Latn-ZA.css
11
A src/main/resources/com/keenwrite/editor/markdown_ja-Jpan-JP.css
1
.markdown {
2
  -fx-font-family: 'Noto Sans CJK JP';
3
}
14
A src/main/resources/com/keenwrite/editor/markdown_ko-Kore-KR.css
1
.markdown {
2
  -fx-font-family: 'Noto Sans CJK KR';
3
}
14
A src/main/resources/com/keenwrite/editor/markdown_zh-Hans-CN.css
1
.markdown {
2
  -fx-font-family: 'Noto Sans CJK SC';
3
}
14
A src/main/resources/com/keenwrite/editor/markdown_zh-Hans-SG.css
1
.markdown {
2
  -fx-font-family: 'Noto Sans CJK SC';
3
}
14
A src/main/resources/com/keenwrite/editor/markdown_zh-Hant-HK.css
1
.markdown {
2
  -fx-font-family: 'Noto Sans CJK HK';
3
}
14
A src/main/resources/com/keenwrite/editor/markdown_zh-Hant-TW.css
1
.markdown {
2
  -fx-font-family: 'Noto Sans CJK TC';
3
}
14
A src/main/resources/com/keenwrite/logo.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
   xmlns:cc="http://creativecommons.org/ns#"
5
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
   xmlns:svg="http://www.w3.org/2000/svg"
7
   xmlns="http://www.w3.org/2000/svg"
8
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
11
   sodipodi:docname="icon.svg"
12
   id="svg52"
13
   xml:space="preserve"
14
   viewBox="0 0 512 512"
15
   height="512"
16
   width="512"
17
   version="1.1"><metadata
18
   id="metadata56"><rdf:RDF><cc:Work
19
       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
20
         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview
21
   inkscape:current-layer="svg52"
22
   inkscape:cy="369.17559"
23
   inkscape:cx="343.24925"
24
   inkscape:zoom="0.78417969"
25
   showgrid="false"
26
   id="namedview54"
27
   inkscape:window-height="480"
28
   inkscape:window-width="640"
29
   inkscape:pageshadow="2"
30
   inkscape:pageopacity="0"
31
   guidetolerance="10"
32
   gridtolerance="10"
33
   objecttolerance="10"
34
   borderopacity="1"
35
   bordercolor="#666666"
36
   pagecolor="#ffffff"
37
   inkscape:document-rotation="0" />
38
<desc
39
   id="desc2">Created with Fabric.js 3.6.3</desc>
40
<defs
41
   id="defs4"><rect
42
   id="rect933"
43
   height="175.55823"
44
   width="470.12033"
45
   y="132.06313"
46
   x="114.92139" />
47
48
49
50
		
51
		
52
		
53
		
54
		
55
		
56
		
57
		
58
59
<linearGradient
60
   id="SVGID_1_302284"
61
   gradientUnits="userSpaceOnUse"
62
   gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)"
63
   x1="0.15235768"
64
   y1="-0.044911571"
65
   x2="0.96880889"
66
   y2="-0.049471263">
67
<stop
68
   offset="0%"
69
   style="stop-color:#ec706a;stop-opacity:1"
70
   id="stop9" />
71
<stop
72
   offset="100%"
73
   style="stop-color:#ecd980;stop-opacity:1"
74
   id="stop11" />
75
</linearGradient>
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
</defs>
92
93
<g
94
   transform="translate(-384.01706,-164.40168)"
95
   id="g853"><path
96
     id="path14"
97
     stroke-linecap="round"
98
     d="m 425.11895,550.88213 c -46.93797,72.14807 -26.19433,103.38343 -26.19433,103.38343 v 0 c 0,0 31.07048,-45.59403 48.81648,-27.97293 v 0 c 15.24298,15.10308 -12.06548,43.30583 -12.06548,43.30583 v 0 c 0,0 166.06898,-68.436 89.90407,-144.24619 v 0 c 0,0 -16.00237,-18.40049 -39.62873,-18.40548 v 0 c -17.28637,0 -38.64951,9.84223 -60.83201,43.93534"
99
     paint-order="stroke"
100
     style="fill:url(#SVGID_1_302284);fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
101
     id="path22"
102
     stroke-linecap="round"
103
     d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z"
104
     paint-order="stroke"
105
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
106
     id="path26"
107
     stroke-linecap="round"
108
     d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z"
109
     paint-order="stroke"
110
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
111
     id="path30"
112
     stroke-linecap="round"
113
     d="m 791.45508,258.2912 c -6.12517,-3.44728 -14.03892,-2.61294 -19.29478,2.61793 -6.36997,6.33501 -6.39495,16.63688 -0.0649,23.00186 L 613.81523,441.29182 583.28931,410.60103 c 96.04423,-96.4489 126.74501,-177.76974 126.74501,-177.76974 79.22249,-11.81068 139.14522,-43.08601 168.97169,-61.62638 z"
114
     paint-order="stroke"
115
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
116
     id="path34"
117
     stroke-linecap="round"
118
     d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z"
119
     paint-order="stroke"
120
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
121
     id="path38"
122
     stroke-linecap="round"
123
     d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z"
124
     paint-order="stroke"
125
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
126
     id="path42"
127
     stroke-linecap="round"
128
     d="m 798.0649,265.0575 87.61088,-87.14624 c -18.72523,29.76151 -50.29032,89.4844 -62.52567,168.64194 0,0 -77.5688,34.88248 -178.68403,125.55095 L 613.81527,441.28846 772.09539,283.91262 c 6.35998,6.39496 16.63687,6.38996 23.00185,0.06 5.14095,-5.10597 6.11018,-12.8049 2.96766,-18.91508"
129
     paint-order="stroke"
130
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /></g>
131
132
<text
133
   style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect933);fill:#000000;fill-opacity:1;stroke:none;"
134
   id="text931"
135
   xml:space="preserve" /></svg>
1136
A src/main/resources/com/keenwrite/logo128.png
Binary file
A src/main/resources/com/keenwrite/logo16.png
Binary file
A src/main/resources/com/keenwrite/logo256.png
Binary file
A src/main/resources/com/keenwrite/logo32.png
Binary file
A src/main/resources/com/keenwrite/logo512.png
Binary file
A src/main/resources/com/keenwrite/logo64.png
Binary file
A src/main/resources/com/keenwrite/messages.properties
1
# ########################################################################
2
# Main Application Window
3
# ########################################################################
4
5
# suppress inspection "UnusedProperty" for whole file
6
7
# ########################################################################
8
# Workspace preferences
9
# ########################################################################
10
11
workspace.document=Document
12
workspace.document.title=Title Name
13
workspace.document.title.desc=Full document title, or variable reference (e.g., '{{'book.title'}}').
14
workspace.document.title.title=Title
15
workspace.document.author=Author Name
16
workspace.document.author.desc=Full name of primary author, or variable reference (e.g., '{{'book.author'}}').
17
workspace.document.author.title=Name
18
workspace.document.byline=Byline
19
workspace.document.byline.desc=Author name, pen name, byline, pseudonym, or variable reference.
20
workspace.document.byline.title=Name
21
workspace.document.address=Address
22
workspace.document.address.desc=Author mailing address, or variable reference.
23
workspace.document.address.title=Address
24
workspace.document.phone=Phone
25
workspace.document.phone.desc=Author phone number, or variable reference.
26
workspace.document.phone.title=Number
27
workspace.document.email=Email
28
workspace.document.email.desc=Author email address, or variable reference.
29
workspace.document.email.title=Email
30
workspace.document.keywords=Keywords
31
workspace.document.keywords.desc=Comma-separated words relating to subject matter, or variable reference.
32
workspace.document.keywords.title=Words
33
workspace.document.copyright=Copyright
34
workspace.document.copyright.desc=Continuous years of publication, or variable reference.
35
workspace.document.copyright.title=Year(s)
36
workspace.document.date=Publish Date
37
workspace.document.date.desc=Date and time document was published, or variable reference.
38
workspace.document.date.title=Timestamp
39
40
workspace.editor=Editor
41
workspace.editor.autosave=Autosave
42
workspace.editor.autosave.desc=Amount of time to wait between saves, in seconds (0 means disabled).
43
workspace.editor.autosave.title=Timeout
44
45
workspace.typeset=Typesetting
46
workspace.typeset.context=ConTeXt
47
workspace.typeset.context.themes.path=Paths
48
workspace.typeset.context.themes.path.desc=Directory containing theme subdirectories.
49
workspace.typeset.context.themes.path.title=Themes
50
workspace.typeset.context.clean=Clean
51
workspace.typeset.context.clean.desc=Delete ancillary files after an unsuccessful export.
52
workspace.typeset.context.clean.title=Purge
53
workspace.typeset.typography=Typography
54
workspace.typeset.typography.quotes=Quotation Marks
55
workspace.typeset.typography.quotes.desc=Convert straight quotes into curly quotes and primes.
56
workspace.typeset.typography.quotes.title=Curl
57
58
workspace.r=R
59
workspace.r.script=Startup Script
60
workspace.r.script.desc=Script runs prior to executing R statements within the document.
61
workspace.r.dir=Working Directory
62
workspace.r.dir.desc=Value assigned to {0}application.r.working.directory{1} and usable in the startup script.
63
workspace.r.dir.title=Directory
64
workspace.r.delimiter.began=Delimiter Prefix
65
workspace.r.delimiter.began.desc=Prefix of expression that wraps inserted variables.
66
workspace.r.delimiter.began.title=Opening
67
workspace.r.delimiter.ended=Delimiter Suffix
68
workspace.r.delimiter.ended.desc=Suffix of expression that wraps inserted variables.
69
workspace.r.delimiter.ended.title=Closing
70
71
workspace.images=Images
72
workspace.images.dir=Absolute Directory
73
workspace.images.dir.desc=Path to search for local file system images.
74
workspace.images.dir.title=Directory
75
workspace.images.order=Extensions
76
workspace.images.order.desc=Preferred order of image file types to embed, separated by spaces.
77
workspace.images.order.title=Extensions
78
workspace.images.resize=Resize
79
workspace.images.resize.desc=Scale images to fit the preview panel when resizing, automatically.
80
workspace.images.resize.title=Resize
81
workspace.images.server=Diagram Server
82
workspace.images.server.desc=Server used to generate diagrams (e.g., kroki.io).
83
workspace.images.server.title=Name
84
85
workspace.definition=Variable
86
workspace.definition.path=File name
87
workspace.definition.path.desc=Absolute path to interpolated string variables.
88
workspace.definition.path.title=Path
89
workspace.definition.delimiter.began=Delimiter Prefix
90
workspace.definition.delimiter.began.desc=Indicates when a variable name is starting.
91
workspace.definition.delimiter.began.title=Opening
92
workspace.definition.delimiter.ended=Delimiter Suffix
93
workspace.definition.delimiter.ended.desc=Indicates when a variable name is ending.
94
workspace.definition.delimiter.ended.title=Closing
95
96
workspace.ui.skin=Skins
97
workspace.ui.skin.selection=Bundled
98
workspace.ui.skin.selection.desc=Pre-packaged application style (default: Modena Light).
99
workspace.ui.skin.selection.title=Name
100
workspace.ui.skin.custom=Custom
101
workspace.ui.skin.custom.desc=User-defined JavaFX cascading stylesheet file.
102
workspace.ui.skin.custom.title=Path
103
104
workspace.ui.preview=Preview
105
workspace.ui.preview.stylesheet=Stylesheet
106
workspace.ui.preview.stylesheet.desc=User-defined HTML cascading stylesheet file.
107
workspace.ui.preview.stylesheet.title=Path
108
109
workspace.ui.font=Fonts
110
workspace.ui.font.editor=Editor Font
111
workspace.ui.font.editor.name=Name
112
workspace.ui.font.editor.name.desc=Text editor font name (sans-serif font recommended).
113
workspace.ui.font.editor.name.title=Family
114
workspace.ui.font.editor.size=Size
115
workspace.ui.font.editor.size.desc=Font size.
116
workspace.ui.font.editor.size.title=Points
117
workspace.ui.font.preview=Preview Font
118
workspace.ui.font.preview.name=Name
119
workspace.ui.font.preview.name.desc=Preview pane font name (must support ligatures, serif font recommended).
120
workspace.ui.font.preview.name.title=Family
121
workspace.ui.font.preview.size=Size
122
workspace.ui.font.preview.size.desc=Font size.
123
workspace.ui.font.preview.size.title=Points
124
workspace.ui.font.preview.mono.name=Name
125
workspace.ui.font.preview.mono.name.desc=Monospace font name.
126
workspace.ui.font.preview.mono.name.title=Family
127
workspace.ui.font.preview.mono.size=Size
128
workspace.ui.font.preview.mono.size.desc=Monospace font size.
129
workspace.ui.font.preview.mono.size.title=Points
130
131
workspace.language=Language
132
workspace.language.locale=Internationalization
133
workspace.language.locale.desc=Language for application and HTML export.
134
workspace.language.locale.title=Locale
135
136
# ########################################################################
137
# Menu Bar
138
# ########################################################################
139
140
Main.menu.file=_File
141
Main.menu.edit=_Edit
142
Main.menu.insert=_Insert
143
Main.menu.format=Forma_t
144
Main.menu.definition=_Variable
145
Main.menu.view=Vie_w
146
Main.menu.help=_Help
147
148
# ########################################################################
149
# Detachable Tabs
150
# ########################################################################
151
152
# {0} is the application title; {1} is a unique window ID.
153
Detach.tab.title={0} - {1}
154
155
# ########################################################################
156
# Status Bar
157
# ########################################################################
158
159
Main.status.text.offset=offset
160
Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2}
161
Main.status.state.default=OK
162
Main.status.export.success=Saved as ''{0}''
163
164
Main.status.error.bootstrap.eval=Note: Bootstrap variable of ''{0}'' not found
165
166
Main.status.error.parse={0} (near ${Main.status.text.offset} {1})
167
Main.status.error.def.blank=Move the caret to a word before inserting a variable
168
Main.status.error.def.empty=Create a variable before inserting one
169
Main.status.error.def.missing=No variable value found for ''{0}''
170
Main.status.error.r=Error with [{0}...]: {1}
171
Main.status.error.file.missing=Not found: ''{0}''
172
Main.status.error.file.missing.near=Not found: ''{0}'' near line {1}
173
174
Main.status.error.messages.recursion=Lookup depth exceeded, check for loops in ''{0}''
175
Main.status.error.messages.syntax=Missing ''}'' in ''{0}''
176
177
Main.status.error.undo=Cannot undo; beginning of undo history reached
178
Main.status.error.redo=Cannot redo; end of redo history reached
179
180
Main.status.error.theme.missing=Install themes before exporting (no themes found at ''{0}'')
181
Main.status.error.theme.name=Cannot find theme name for ''{0}''
182
183
Main.status.image.request.init=Initializing HTTP request
184
Main.status.image.request.fetch=Requesting content type from ''{0}''
185
Main.status.image.request.success=Determined content type ''{0}''
186
Main.status.image.request.error.media=No media type for ''{0}''
187
Main.status.image.request.error.cert=Could not accept certificate for ''{0}''
188
189
Main.status.font.search.missing=No font name starting with ''{0}'' was found
190
191
Main.status.export.concat=Concatenating ''{0}''
192
Main.status.export.concat.parent=No parent directory found for ''{0}''
193
Main.status.export.concat.extension=File name must have an extension ''{0}''
194
Main.status.export.concat.io=Could not read from ''{0}''
195
196
Main.status.typeset.create=Creating typesetter
197
Main.status.typeset.xhtml=Export document as XHTML
198
Main.status.typeset.began=Started typesetting ''{0}''
199
Main.status.typeset.failed=Could not generate PDF file
200
Main.status.typeset.page=Typesetting page {0} of {1} (pass {2})
201
Main.status.typeset.ended.success=Finished typesetting ''{0}'' ({1} elapsed)
202
Main.status.typeset.ended.failure=Failed to typeset ''{0}'' ({1} elapsed)
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
# ########################################################################
272
# Text Resources
273
# ########################################################################
274
275
TextResource.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
276
TextResource.saveFailed.title=Save
277
278
# ########################################################################
279
# File Open
280
# ########################################################################
281
282
Dialog.file.choose.open.title=Open File
283
Dialog.file.choose.save.title=Save File
284
Dialog.file.choose.export.title=Export File
285
286
Dialog.file.choose.filter.title.source=Source Files
287
Dialog.file.choose.filter.title.definition=Variable Files
288
Dialog.file.choose.filter.title.xml=XML Files
289
Dialog.file.choose.filter.title.all=All Files
290
291
# ########################################################################
292
# Browse File
293
# ########################################################################
294
295
BrowseFileButton.chooser.title=Open local file
296
BrowseFileButton.chooser.allFilesFilter=All Files
297
BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
298
299
# ########################################################################
300
# Browse Directory
301
# ########################################################################
302
303
BrowseDirectoryButton.chooser.title=Open local directory
304
BrowseDirectoryButton.tooltip=${BrowseDirectoryButton.chooser.title}
305
306
# ########################################################################
307
# Alert Dialog
308
# ########################################################################
309
310
Alert.file.close.title=Close
311
Alert.file.close.text=Save changes to {0}?
312
313
# ########################################################################
314
# Typesetting Alert Dialog
315
# ########################################################################
316
317
Alert.typesetter.missing.title=Missing Typesetter
318
Alert.typesetter.missing.header=Install typesetter
319
Alert.typesetter.missing.version=for {0} {1} {2}-bit
320
Alert.typesetter.missing.installer.text=Download and install ConTeXt
321
Alert.typesetter.missing.installer.url=https://wiki.contextgarden.net/Installation
322
323
# ########################################################################
324
# Image Dialog
325
# ########################################################################
326
327
Dialog.image.title=Image
328
Dialog.image.chooser.imagesFilter=Images
329
Dialog.image.previewLabel.text=Markdown Preview\:
330
Dialog.image.textLabel.text=Alternate Text\:
331
Dialog.image.titleLabel.text=Title (tooltip)\:
332
Dialog.image.urlLabel.text=Image URL\:
333
334
# ########################################################################
335
# Hyperlink Dialog
336
# ########################################################################
337
338
Dialog.link.title=Link
339
Dialog.link.previewLabel.text=Markdown Preview\:
340
Dialog.link.textLabel.text=Link Text\:
341
Dialog.link.titleLabel.text=Title (tooltip)\:
342
Dialog.link.urlLabel.text=Link URL\:
343
344
# ########################################################################
345
# Themes Dialog
346
# ########################################################################
347
348
Dialog.theme.title=Typesetting theme
349
Dialog.theme.header=Choose a typesetting theme
350
351
# ########################################################################
352
# About Dialog
353
# ########################################################################
354
355
Dialog.about.title=About {0}
356
Dialog.about.header={0}
357
Dialog.about.content=Copyright 2016-{0} White Magic Software, Ltd.\n\nVersion: {1}
358
359
# ########################################################################
360
# Application Actions
361
# ########################################################################
362
363
Action.file.new.description=Create a new file
364
Action.file.new.accelerator=Shortcut+N
365
Action.file.new.icon=FILE_ALT
366
Action.file.new.text=_New
367
368
Action.file.open.description=Open a new file
369
Action.file.open.accelerator=Shortcut+O
370
Action.file.open.text=_Open...
371
Action.file.open.icon=FOLDER_OPEN_ALT
372
373
Action.file.close.description=Close the current document
374
Action.file.close.accelerator=Shortcut+W
375
Action.file.close.text=_Close
376
377
Action.file.close_all.description=Close all open documents
378
Action.file.close_all.accelerator=Ctrl+F4
379
Action.file.close_all.text=Close All
380
381
Action.file.save.description=Save the document
382
Action.file.save.accelerator=Shortcut+S
383
Action.file.save.text=_Save
384
Action.file.save.icon=FLOPPY_ALT
385
386
Action.file.save_as.description=Rename the current document
387
Action.file.save_as.text=Save _As
388
389
Action.file.save_all.description=Save all open documents
390
Action.file.save_all.accelerator=Shortcut+Shift+S
391
Action.file.save_all.text=Save A_ll
392
393
Action.file.export.pdf.description=Typeset the document
394
Action.file.export.pdf.accelerator=Shortcut+P
395
Action.file.export.pdf.text=_PDF
396
Action.file.export.pdf.icon=FILE_PDF_ALT
397
398
Action.file.export.pdf.dir.description=Typeset files in document directory
399
Action.file.export.pdf.dir.accelerator=Shortcut+Shift+P
400
Action.file.export.pdf.dir.text=_Joined PDF
401
Action.file.export.pdf.dir.icon=FILE_PDF_ALT
402
403
Action.file.export.html_svg.description=Export the current document as HTML + SVG
404
Action.file.export.text=_Export As
405
Action.file.export.html_svg.text=HTML and S_VG
406
407
Action.file.export.html_tex.description=Export the current document as HTML + TeX
408
Action.file.export.html_tex.text=HTML and _TeX
409
410
Action.file.export.xhtml_tex.description=Export as XHTML + TeX
411
Action.file.export.xhtml_tex.text=_XHTML and TeX
412
413
Action.file.export.markdown.description=Export the current document as Markdown
414
Action.file.export.markdown.text=Markdown
415
416
Action.file.exit.description=Quit the application
417
Action.file.exit.text=E_xit
418
419
420
Action.edit.undo.description=Undo the previous edit
421
Action.edit.undo.accelerator=Shortcut+Z
422
Action.edit.undo.text=_Undo
423
Action.edit.undo.icon=UNDO
424
425
Action.edit.redo.description=Redo the previous edit
426
Action.edit.redo.accelerator=Shortcut+Y
427
Action.edit.redo.text=_Redo
428
Action.edit.redo.icon=REPEAT
429
430
Action.edit.cut.description=Delete the selected text or line
431
Action.edit.cut.accelerator=Shortcut+X
432
Action.edit.cut.text=Cu_t
433
Action.edit.cut.icon=CUT
434
435
Action.edit.copy.description=Copy the selected text
436
Action.edit.copy.accelerator=Shortcut+C
437
Action.edit.copy.text=_Copy
438
Action.edit.copy.icon=COPY
439
440
Action.edit.paste.description=Paste from the clipboard
441
Action.edit.paste.accelerator=Shortcut+V
442
Action.edit.paste.text=_Paste
443
Action.edit.paste.icon=PASTE
444
445
Action.edit.select_all.description=Highlight the current document text
446
Action.edit.select_all.accelerator=Shortcut+A
447
Action.edit.select_all.text=Select _All
448
449
Action.edit.find.description=Search for text in the document
450
Action.edit.find.accelerator=Shortcut+F
451
Action.edit.find.text=_Find
452
Action.edit.find.icon=SEARCH
453
454
Action.edit.find_next.description=Find next occurrence
455
Action.edit.find_next.accelerator=F3
456
Action.edit.find_next.text=Find _Next
457
458
Action.edit.find_prev.description=Find previous occurrence
459
Action.edit.find_prev.accelerator=Shift+F3
460
Action.edit.find_prev.text=Find _Prev
461
462
Action.edit.preferences.description=Edit user preferences
463
Action.edit.preferences.accelerator=Ctrl+Alt+S
464
Action.edit.preferences.text=_Preferences
465
466
467
Action.format.bold.description=Insert strong text
468
Action.format.bold.accelerator=Shortcut+B
469
Action.format.bold.text=_Bold
470
Action.format.bold.icon=BOLD
471
472
Action.format.italic.description=Insert text emphasis
473
Action.format.italic.accelerator=Shortcut+I
474
Action.format.italic.text=_Italic
475
Action.format.italic.icon=ITALIC
476
477
Action.format.monospace.description=Insert monospace text
478
Action.format.monospace.accelerator=Shortcut+`
479
Action.format.monospace.text=_Monospace
480
481
Action.format.superscript.description=Insert superscript text
482
Action.format.superscript.accelerator=Shortcut+[
483
Action.format.superscript.text=Su_perscript
484
Action.format.superscript.icon=SUPERSCRIPT
485
486
Action.format.subscript.description=Insert subscript text
487
Action.format.subscript.accelerator=Shortcut+]
488
Action.format.subscript.text=Su_bscript
489
Action.format.subscript.icon=SUBSCRIPT
490
491
Action.format.strikethrough.description=Insert struck text
492
Action.format.strikethrough.accelerator=Shortcut+T
493
Action.format.strikethrough.text=Stri_kethrough
494
Action.format.strikethrough.icon=STRIKETHROUGH
495
496
497
Action.insert.blockquote.description=Insert blockquote
498
Action.insert.blockquote.accelerator=Ctrl+Q
499
Action.insert.blockquote.text=_Blockquote
500
Action.insert.blockquote.icon=QUOTE_LEFT
501
502
Action.insert.code.description=Insert inline code
503
Action.insert.code.accelerator=Shortcut+K
504
Action.insert.code.text=Inline _Code
505
Action.insert.code.icon=CODE
506
507
Action.insert.fenced_code_block.description=Insert code block
508
Action.insert.fenced_code_block.accelerator=Shortcut+Shift+K
509
Action.insert.fenced_code_block.text=_Fenced Code Block
510
Action.insert.fenced_code_block.prompt.text=Enter code here
511
Action.insert.fenced_code_block.icon=FILE_CODE_ALT
512
513
Action.insert.link.description=Insert hyperlink
514
Action.insert.link.accelerator=Shortcut+L
515
Action.insert.link.text=_Link...
516
Action.insert.link.icon=LINK
517
518
Action.insert.image.description=Insert image
519
Action.insert.image.accelerator=Shortcut+G
520
Action.insert.image.text=_Image...
521
Action.insert.image.icon=PICTURE_ALT
522
523
Action.insert.heading.description=Insert heading level
524
Action.insert.heading.accelerator=Shortcut+
525
Action.insert.heading.icon=HEADER
526
527
Action.insert.heading_1.description=${Action.insert.heading.description} 1
528
Action.insert.heading_1.accelerator=${Action.insert.heading.accelerator}1
529
Action.insert.heading_1.text=Heading _1
530
Action.insert.heading_1.icon=${Action.insert.heading.icon}
531
532
Action.insert.heading_2.description=${Action.insert.heading.description} 2
533
Action.insert.heading_2.accelerator=${Action.insert.heading.accelerator}2
534
Action.insert.heading_2.text=Heading _2
535
Action.insert.heading_2.icon=${Action.insert.heading.icon}
536
537
Action.insert.heading_3.description=${Action.insert.heading.description} 3
538
Action.insert.heading_3.accelerator=${Action.insert.heading.accelerator}3
539
Action.insert.heading_3.text=Heading _3
540
Action.insert.heading_3.icon=${Action.insert.heading.icon}
541
542
Action.insert.unordered_list.description=Insert bulleted list
543
Action.insert.unordered_list.accelerator=Shortcut+U
544
Action.insert.unordered_list.text=_Unordered List
545
Action.insert.unordered_list.icon=LIST_UL
546
547
Action.insert.ordered_list.description=Insert enumerated list
548
Action.insert.ordered_list.accelerator=Shortcut+Shift+O
549
Action.insert.ordered_list.text=_Ordered List
550
Action.insert.ordered_list.icon=LIST_OL
551
552
Action.insert.horizontal_rule.description=Insert horizontal rule
553
Action.insert.horizontal_rule.accelerator=Shortcut+H
554
Action.insert.horizontal_rule.text=_Horizontal Rule
555
Action.insert.horizontal_rule.icon=LIST_OL
556
557
558
Action.definition.create.description=Create a new variable
559
Action.definition.create.text=_Create
560
Action.definition.create.icon=TREE
561
Action.definition.create.tooltip=Add new item (Insert)
562
563
Action.definition.rename.description=Rename the selected variable
564
Action.definition.rename.text=_Rename
565
Action.definition.rename.icon=EDIT
566
Action.definition.rename.tooltip=Rename selected item (F2)
567
568
Action.definition.delete.description=Delete the selected variables
569
Action.definition.delete.text=De_lete
570
Action.definition.delete.icon=TRASH
571
Action.definition.delete.tooltip=Delete selected items (Delete)
572
573
Action.definition.insert.description=Insert a variable
574
Action.definition.insert.accelerator=Ctrl+Space
575
Action.definition.insert.text=_Insert
576
Action.definition.insert.icon=STAR
577
578
579
Action.view.refresh.description=Clear all caches
580
Action.view.refresh.accelerator=F5
581
Action.view.refresh.text=Refresh
582
583
Action.view.preview.description=Open document preview
584
Action.view.preview.accelerator=F6
585
Action.view.preview.text=Preview
586
587
Action.view.outline.description=Open document outline
588
Action.view.outline.accelerator=F7
589
Action.view.outline.text=Outline
590
591
Action.view.statistics.description=Open document word counts
592
Action.view.statistics.accelerator=F8
593
Action.view.statistics.text=Statistics
594
595
Action.view.files.description=Open file manager
596
Action.view.files.accelerator=Ctrl+F8
597
Action.view.files.text=Files
598
599
Action.view.menubar.description=Toggle menu bar
600
Action.view.menubar.accelerator=Ctrl+F9
601
Action.view.menubar.text=Menu bar
602
603
Action.view.toolbar.description=Toggle tool bar
604
Action.view.toolbar.accelerator=Ctrl+Shift+F9
605
Action.view.toolbar.text=Tool bar
606
607
Action.view.statusbar.description=Toggle status bar
608
Action.view.statusbar.accelerator=Ctrl+Shift+Alt+F9
609
Action.view.statusbar.text=Status bar
610
611
Action.view.log.description=Open document issues
612
Action.view.log.accelerator=F12
613
Action.view.log.text=Log
614
615
616
Action.help.about.description=Show help dialog
617
Action.help.about.accelerator=F1
618
Action.help.about.text=About
619
Action.help.about.icon=INFO
1620
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
  background-color: #fff;
6
  margin: 0 auto;
7
  line-height: 1.6;
8
  color: #454545;
9
  padding: .5em;
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
}
1240
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
# ########################################################################
2
# Application
3
# ########################################################################
4
5
application.title=keenwrite
6
application.package=com/${application.title}
7
application.messages= com.${application.title}.messages
8
9
# Suppress multiple file modified notifications for one logical modification.
10
# Given in milliseconds.
11
application.watchdog.timeout=50
12
13
# ########################################################################
14
# Preferences
15
# ########################################################################
16
17
preferences.root=com.${application.title}
18
preferences.root.state=state
19
preferences.root.options=options
20
preferences.root.definition.source=definition.source
21
22
# ########################################################################
23
# File and Path References
24
# ########################################################################
25
26
file.stylesheet.application.dir=${application.package}/skins
27
file.stylesheet.application.base=${file.stylesheet.application.dir}/scene.css
28
file.stylesheet.application.skin=${file.stylesheet.application.dir}/{0}.css
29
file.stylesheet.markdown=${application.package}/editor/markdown.css
30
# {0} language code, {1} script code, {2} country code
31
file.stylesheet.markdown.locale=${application.package}/editor/markdown_{0}-{1}-{2}.css
32
file.stylesheet.xml=${application.package}/xml.css
33
34
# Preview styles are loaded statically through a class's classloader.
35
file.stylesheet.preview=webview.css
36
# {0} language code, {1} script code, {2} country code
37
file.stylesheet.preview.locale=webview_{0}-{1}-{2}.css
38
39
file.logo.16=${application.package}/logo16.png
40
file.logo.32=${application.package}/logo32.png
41
file.logo.128=${application.package}/logo128.png
42
file.logo.256=${application.package}/logo256.png
43
file.logo.512=${application.package}/logo512.png
44
45
# Default file name when a new file is created.
46
# This ensures that the file type can always be
47
# discerned so that the correct type of variable
48
# reference can be inserted.
49
file.default.document=untitled.md
50
file.default.definition=variables.yaml
51
52
# ########################################################################
53
# File name Extensions
54
# ########################################################################
55
56
# Comma-separated list of definition file name extensions.
57
definition.file.ext.json=*.json
58
definition.file.ext.toml=*.toml
59
definition.file.ext.yaml=*.yml,*.yaml
60
definition.file.ext.properties=*.properties,*.props
61
62
# Comma-separated list of file name extensions.
63
file.ext.rmarkdown=*.Rmd
64
file.ext.rxml=*.Rxml
65
file.ext.source=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown},${file.ext.rxml}
66
file.ext.definition=${definition.file.ext.yaml}
67
file.ext.xml=*.xml,${file.ext.rxml}
68
file.ext.all=*.*
69
70
# File name extension search order for images.
71
file.ext.image.order=svg pdf png jpg tiff
72
73
# ########################################################################
74
# Variable Name Editor
75
# ########################################################################
76
77
# Maximum number of characters for a variable name. A variable is defined
78
# as one or more non-whitespace characters up to this maximum length.
79
editor.variable.maxLength=256
80
81
# ########################################################################
82
# Dialog Preferences
83
# ########################################################################
84
85
dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R
86
dialog.alert.button.order.linux=L_HE+UNYACBXIO_R
87
dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R
88
89
# Ensures a consistent button order for alert dialogs across platforms (because
90
# the default button order on Linux defies all logic).
91
dialog.alert.button.order=${dialog.alert.button.order.windows}
192
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/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/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
# Building
2
3
The lexicon files are retrieved from SymSpell in the parent directory:
4
5
svn export \
6
  https://github.com/wolfgarbe/SymSpell/trunk/SymSpell.FrequencyDictionary/ lexicons
7
8
The lexicons and bigrams are both space-separated, but parsing a
9
tab-delimited file is easier, so change them to tab-separated files.
110
A src/main/resources/lexicons/de.txt
Binary file
A src/main/resources/lexicons/en.txt
Binary file
A src/main/resources/lexicons/es.txt
Binary file
A src/main/resources/lexicons/ext/README.md
1
# Overview
2
3
Lexicons in this directory are meant to relate to a particular subject
4
(medicine, chemistry, math, sports, and such), extend the main lexicon,
5
or not be in common use.
6
17
A src/main/resources/lexicons/ext/contractions.txt
11
2
'aight
3
ain't
4
amn't
5
aren't
6
can't
7
'cause
8
couldn't
9
couldn't've
10
could've
11
daren't
12
daresn't
13
dasn't
14
didn't
15
doesn't
16
don't
17
dunno
18
d'ye
19
e'er
20
everybody's
21
everyone's
22
g'day
23
gimme
24
giv'n
25
gonna
26
gon't
27
gotta
28
hadn't
29
had've
30
hasn't
31
haven't
32
he'd
33
he'll
34
he's
35
he've
36
how'd
37
howdy
38
how'll
39
how're
40
how's
41
how've
42
i'd
43
i'dn't've
44
i'd've
45
i'll
46
i'm
47
i'm'a
48
imma
49
innit
50
isn't
51
it'd
52
it'll
53
it's
54
i've
55
let's
56
ma'am
57
mayn't
58
may've
59
methinks
60
mightn't
61
might've
62
mustn't
63
mustn't've
64
must've
65
needn't
66
ne'er
67
o'clock
68
o'er
69
ol'
70
oughtn't
71
shalln't
72
shan't
73
she'd
74
she'll
75
she's
76
shouldn't
77
shouldn't've
78
should've
79
somebody's
80
someone's
81
something's
82
so're
83
that'd
84
that'll
85
that're
86
that's
87
there'd
88
there'll
89
there're
90
there's
91
these'd
92
these'll
93
these're
94
these've
95
they'd
96
they'll
97
they're
98
they've
99
this's
100
those're
101
those've
102
'tis
103
to've
104
'twas
105
'twouldn't
106
wanna
107
wasn't
108
we'd
109
we'd've
110
we'll
111
we're
112
weren't
113
we've
114
what'd
115
what'll
116
what're
117
what's
118
what've
119
when'd
120
when'll
121
when's
122
where'd
123
where'll
124
where're
125
where's
126
where've
127
which'd
128
which'll
129
which're
130
which's
131
which've
132
who'd
133
who'd've
134
who'll
135
who're
136
who's
137
who've
138
why'd
139
why'll
140
why're
141
why's
142
willn't
143
won't
144
wouldn't
145
wouldn't've
146
would've
147
y'all
148
y'all'd've
149
y'all're
150
you'd
151
you'dn't've
152
you'll
153
you're
154
you've
155
A src/main/resources/lexicons/ext/tech.txt
1
analytics	130337
2
hotspot	130022
3
instantiation	130000
4
onboarding	129953
5
biometric	129795
6
anamorphic	129777
7
benchmarking	129772
8
cybersecurity	129769
9
barcode	129757
10
splitter	129755
11
keychain	129719
12
crowdfunding	129696
13
polymorphism	129688
14
automata	129666
15
shockwave	129658
16
profiler	129648
17
kerning	129646
18
nanometer	129630
19
meridiem	129624
20
influencer	129618
21
passcode	129617
22
sexting	129607
23
cryptology	129606
24
biometrics	129606
25
bitcoin	129599
26
specular	129598
27
accelerometer	129588
28
googolplex	129583
29
grayscale	129576
30
ascender	129571
31
pixelated	129569
32
rockstar	129565
33
ragdoll	129564
34
cyberattack	129564
35
cryptanalysis	129562
36
ransomware	129553
37
crowdsourcing	129552
38
hackathon	129551
39
audiobook	129544
40
degauss	129543
41
attenuator	129540
42
jetpack	129538
43
packrat	129536
44
backlight	129535
45
bootable	129530
46
octothorpe	129529
47
newsfeed	129525
48
extranet	129523
49
failover	129516
50
cyberbullying	129516
51
neumann	129515
52
capacitive	129514
53
backlit	129511
54
millimicron	129507
55
inductor	129505
56
workgroup	129502
57
journaling	129500
58
middleware	129499
59
spooler	129497
60
clamshell	129495
61
wireframe	129494
62
modularity	129493
63
strikethrough	129489
64
petabyte	129487
65
jughead	129482
66
acyclic	129482
67
gearhead	129478
68
stateful	129473
69
submenu	129467
70
pseudorandom	129463
71
earbuds	129461
72
narrowband	129460
73
recordable	129457
74
unallocated	129455
75
mappable	129455
76
chipset	129454
77
multicast	129447
78
loopback	129444
79
pixelate	129441
80
cryptographic	129441
81
pixelation	129438
82
autocorrect	129438
83
teraflop	129437
84
digitizer	129436
85
tunnelling	129434
86
deduplication	129434
87
subwoofer	129433
88
touchpad	129429
89
namespace	129428
90
microcontroller	129428
91
geolocation	129428
92
telepresence	129427
93
driverless	129426
94
photolithography	129425
95
multiphase	129425
96
verifier	129424
97
robocall	129424
98
autofocus	129424
99
kilobit	129422
100
hacktivist	129419
101
geocache	129415
102
rasterize	129412
103
plaintext	129411
104
pipelining	129411
105
technobabble	129409
106
defragment	129409
107
connectionless	129409
108
homomorphic	129407
109
demodulator	129406
110
datagram	129406
111
activex	129406
112
normalisation	129404
113
blackhole	129402
114
cyberstalker	129401
115
multifunction	129400
116
undirected	129397
117
ciphertext	129397
118
superspeed	129396
119
spacebar	129395
120
cyberwar	129395
121
borderless	129395
122
transcode	129393
123
cyberbully	129393
124
multimeter	129392
125
dropship	129391
126
yottabyte	129390
127
infector	129390
128
superclass	129389
129
tooltip	129388
130
dereference	129387
131
combinator	129386
132
milliwatt	129385
133
cyberstalking	129384
134
subfolder	129383
135
wideband	129382
136
noncontiguous	129382
137
ferroelectric	129382
138
cybersquatting	129378
139
autofill	129378
140
trackpad	129376
141
associatively	129376
142
luggable	129374
143
seamonkey	129373
144
defragmentation	129373
145
starcraft	129371
146
obliquing	129371
147
leadless	129371
148
greeking	129371
149
upgradeable	129370
150
radiosity	129370
151
transcoding	129369
152
quintillionth	129369
153
bitmapped	129369
154
subdirectory	129368
155
degausser	129368
156
curtiss	129368
157
scunthorpe	129367
158
undelete	129365
159
gigaflops	129365
160
darknet	129365
161
zettabyte	129364
162
topologies	129363
163
spidering	129363
164
photorealism	129363
165
multithreading	129363
166
deallocate	129363
167
mersenne	129362
168
machinima	129361
169
satisfiable	129360
170
laserjet	129360
171
multicore	129359
172
microblog	129359
173
megaflops	129359
174
homeomorphic	129359
175
microblogging	129358
176
kilobaud	129358
177
cyberwarfare	129358
178
microarchitecture	129357
179
autosave	129357
180
wirelessly	129356
181
sneakernet	129355
182
textbox	129354
183
obfuscator	129354
184
microkernel	129353
185
substring	129352
186
macroinstruction	129352
187
endianness	129352
188
indexable	129351
189
backtick	129351
190
unshielded	129350
191
cleartext	129350
192
autocomplete	129349
193
abandonware	129349
194
hacktivism	129348
195
antikythera	129348
196
stereolithography	129347
197
photorealistic	129347
198
macrovision	129347
199
greasemonkey	129347
200
geotagging	129347
201
disassembler	129346
202
spacewar	129345
203
pluggable	129345
204
kilobits	129345
205
webcomic	129344
206
unfollow	129344
207
photosensor	129344
208
petaflop	129344
209
garageband	129344
210
truetype	129343
211
subnetwork	129342
212
backpropagation	129342
213
supercomputing	129340
214
smartwatch	129340
215
unbundled	129339
216
smilies	129339
217
milliamp	129339
218
bytecode	129339
219
trackpoint	129337
220
slipstreaming	129337
221
monospace	129337
222
memoization	129337
223
scaleable	129336
224
respawn	129335
225
multicasting	129335
226
geocacher	129335
227
workgroups	129334
228
ferrofluid	129334
229
smartdrive	129333
230
subsampling	129332
231
rasterization	129332
232
guiltware	129332
233
defragger	129332
234
satisfiability	129331
235
activision	129331
236
subdirectories	129330
237
segfault	129330
238
flamebait	129330
239
framebuffer	129329
240
defragging	129329
241
decompiler	129329
242
unshift	129328
243
memristor	129328
244
zebibyte	129327
245
semiprime	129327
246
rotoscoping	129327
247
hypertransport	129327
248
smartmedia	129326
249
grayware	129326
250
defragmenting	129326
251
defragmenter	129326
252
repagination	129325
253
subnetting	129324
254
skeuomorphism	129324
255
screencast	129324
256
stylesheet	129323
257
superintelligence	129322
258
multitenancy	129322
259
datastore	129322
260
autoplay	129322
261
repaginate	129321
262
macbook	129321
263
geotagged	129321
264
baudrate	129321
265
transmeta	129320
266
screwless	129320
267
nameserver	129320
268
interexchange	129320
269
geocoding	129319
270
downloader	129319
271
autodiscovery	129319
272
extortion	65752
273
emoji	65684
274
googol	65618
275
backside	65388
276
fibre	65387
277
metre	65333
278
royale	65173
279
radix	65093
280
hotdog	65091
281
lecher	65062
282
uptime	65009
283
unbound	64979
284
eniac	64975
285
synaptic	64966
286
voxel	64926
287
selfie	64917
288
uplink	64887
289
fanboy	64857
290
defrag	64849
291
nondisclosure	64839
292
qubit	64828
293
yippie	64821
294
gearhead	64819
295
subnet	64818
296
endian	64798
297
bezier	64797
298
reallocation	64796
299
telephonic	64789
300
mosfet	64777
301
mutex	64775
302
inkjet	64772
303
gobbing	64768
304
shader	64766
305
ultralight	64755
306
hackers	64746
307
pacman	64742
308
unlink	64741
309
undock	64740
310
understroke	64738
311
beginners	64736
312
photoscope	64731
313
gantt	64725
314
programmers	64722
315
todays	64720
316
moores	64716
317
fullscreen	64715
318
moveless	64708
319
reformatted	64704
320
deallocate	64704
321
laserdisc	64702
322
macos	64700
323
nonactive	64697
324
nonadjacent	64696
325
hotfix	64695
326
keylogger	64694
327
geotag	64691
328
oreilly	64681
329
exabit	64678
330
jailbroken	64677
331
fuzzer	64676
332
noninteractive	64673
333
multifactor	64672
334
letterspacing	64671
335
preinstall	64669
336
multiboot	64666
337
runescape	64665
338
micropayment	64664
339
numpad	64663
340
preinstalled	64661
341
jailbreaking	64660
342
attend	2158
343
withstand	1809
344
transpire	1116
345
reading	1110
346
texture	1065
347
capitalize	832
348
calling	779
349
unfold	767
350
starboard	679
351
commode	625
352
doing	594
353
textbook	499
354
unease	378
355
unpack	358
356
keycard	231
357
mainspring	207
358
grr	180
359
geocaching	167
360
microbus	160
361
mp3	147
362
svg	139
363
shifted	128
364
texted	127
365
towheaded	118
366
mineshaft	115
367
nonparty	95
368
crossbite	80
369
resignedness	69
370
msrp	61
371
inbreak	53
372
nanocomposite	44
373
md5	44
374
neomorphic	41
375
superstrain	28
376
lifers	27
377
multination	26
378
smartwatch	22
379
antilibration	22
380
zapf	20
381
mp4	20
1382
A src/main/resources/lexicons/fr.txt
Binary file
A src/main/resources/lexicons/he.txt
Binary file
A src/main/resources/lexicons/it.txt
Binary file
A src/main/resources/lexicons/ru.txt
Binary file
A src/main/resources/lexicons/zh.txt
Binary file
A src/test/java/com/keenwrite/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.testfx.framework.junit5.Start;
22
23
import static com.keenwrite.util.FontLoader.initFonts;
24
25
public class TreeViewTest extends Application {
26
  private final SimpleObjectProperty<Node> mTextEditor =
27
    new SimpleObjectProperty<>();
28
29
  private final EventHandler<TreeItem.TreeModificationEvent<Event>> mTreeHandler =
30
    event -> refresh( mTextEditor.get() );
31
32
  private void refresh( final Node node ) {
33
    throw new RuntimeException( "Derp: " + node );
34
  }
35
36
  public static void main( final String[] args ) {
37
    initFonts();
38
    launch( args );
39
  }
40
41
  public void start( final Stage stage ) {
42
    onStart( stage );
43
  }
44
45
  @Start
46
  private void onStart( final Stage stage ) {
47
    final var workspace = new Workspace();
48
    final var mainPane = new SplitPane();
49
50
    final var transformer = new YamlTreeTransformer();
51
    final var editor = new DefinitionEditor( transformer );
52
53
    final var tabPane1 = new DetachableTabPane();
54
    tabPane1.addTab( "Editor", editor );
55
56
    final var tabPane2 = new DetachableTabPane();
57
    final var tab21 = tabPane2.addTab( "Picker", new ColorPicker() );
58
    final var tab22 = tabPane2.addTab( "Editor",
59
                                       new MarkdownEditor( workspace ) );
60
    tab21.setTooltip( new Tooltip( "Colour Picker" ) );
61
    tab22.setTooltip( new Tooltip( "Text Editor" ) );
62
63
    final var tabPane3 = new DetachableTabPane();
64
    tabPane3.addTab( "Preview", new HtmlPreview( workspace ) );
65
66
    editor.addTreeChangeHandler( mTreeHandler );
67
68
    mainPane.getItems().addAll( tabPane1, tabPane2, tabPane3 );
69
70
    final var scene = new Scene( mainPane );
71
    stage.setScene( scene );
72
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.junit.jupiter.api.Test;
6
import org.junit.jupiter.api.extension.ExtendWith;
7
import org.testfx.framework.junit5.ApplicationExtension;
8
9
import java.util.regex.Pattern;
10
11
import static java.util.regex.Pattern.compile;
12
import static javafx.application.Platform.runLater;
13
import static org.junit.jupiter.api.Assertions.assertEquals;
14
import static org.junit.jupiter.api.Assertions.assertTrue;
15
16
@ExtendWith( {ApplicationExtension.class, AwaitFxExtension.class} )
17
public class MarkdownEditorTest {
18
  private static final String[] WORDS = new String[]{
19
    "Italicize",
20
    "English's",
21
    "foreign",
22
    "words",
23
    "based",
24
    "on",
25
    "popularity,",
26
    "like",
27
    "_bête_",
28
    "_noire_",
29
    "and",
30
    "_Weltanschauung_",
31
    "but",
32
    "not",
33
    "résumé.",
34
    "Don't",
35
    "omit",
36
    "accented",
37
    "characters!",
38
    "Cœlacanthe",
39
    "L'Haÿ-les-Roses",
40
    "Mühlfeldstraße",
41
    "Da̱nx̱a̱laga̱litła̱n",
42
  };
43
44
  private static final String TEXT = String.join( " ", WORDS );
45
46
  private static final Pattern REGEX = compile(
47
    "[^\\p{Mn}\\p{Me}\\p{L}\\p{N}'-]+" );
48
49
  /**
50
   * Test that the {@link MarkdownEditor} can retrieve a word at the caret
51
   * position, regardless of whether the caret is at the beginning, middle, or
52
   * end of the word.
53
   */
54
  @Test
55
  public void test_CaretWord_GetISO88591Word_WordSelected() {
56
    runLater( () -> {
57
      final var editor = createMarkdownEditor();
58
59
      for( int i = 0; i < WORDS.length; i++ ) {
60
        final var word = WORDS[ i ];
61
        final var len = word.length();
62
        final var expected = REGEX.matcher( word ).replaceAll( "" );
63
64
        for( int j = 0; j < len; j++ ) {
65
          editor.moveTo( offset( i ) + j );
66
          final var actual = editor.getCaretWordText();
67
          assertEquals( expected, actual );
68
        }
69
      }
70
    } );
71
  }
72
73
  /**
74
   * Test that the {@link MarkdownEditor} can make a word bold.
75
   */
76
  @Test
77
  public void test_CaretWord_SetWordBold_WordIsBold() {
78
    final var index = 20;
79
    final var editor = createMarkdownEditor();
80
81
    editor.moveTo( offset( index ) );
82
    editor.bold();
83
    assertTrue( editor.getText().contains( "**" + WORDS[ index ] + "**" ) );
84
  }
85
86
  /**
87
   * Returns the document offset for a string at the given index.
88
   */
89
  private static int offset( final int index ) {
90
    assert 0 <= index && index < WORDS.length;
91
    int offset = 0;
92
93
    for( int i = 0; i < index; i++ ) {
94
      offset += WORDS[ i ].length();
95
    }
96
97
    // Add the index to compensate for one space between words.
98
    return offset + index;
99
  }
100
101
  /**
102
   * Returns an instance of {@link MarkdownEditor} pre-populated with
103
   * {@link #TEXT}.
104
   *
105
   * @return A new {@link MarkdownEditor} instance, ready for unit tests.
106
   */
107
  private MarkdownEditor createMarkdownEditor() {
108
    final var workspace = new Workspace();
109
    final var editor = new MarkdownEditor( workspace );
110
    editor.setText( TEXT );
111
    return editor;
112
  }
113
}
1114
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.api.Test;
14
15
import java.util.ArrayList;
16
import java.util.List;
17
18
import static org.junit.jupiter.api.Assertions.assertEquals;
19
20
/**
21
 * Test that basic styles for conversion exports as expected.
22
 */
23
public class ParserTest {
24
25
  @Test
26
  void test_Conversion_InlineStyles_ExportedAsHtml() {
27
    final var md = "*emphasis* _emphasis_ **strong**";
28
29
    final var extensions = createExtensions();
30
    final var options = new MutableDataSet();
31
    final var parser = Parser
32
      .builder( options )
33
      .extensions( extensions )
34
      .build();
35
    final var renderer = HtmlRenderer
36
      .builder( options )
37
      .extensions( extensions )
38
      .build();
39
40
    final var document = parser.parse( md );
41
    final var actual = renderer.render( document );
42
    final var expected =
43
      "<p><em>emphasis</em> <em>emphasis</em> <strong>strong</strong></p>\n";
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
}
160
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.net.URI;
7
import java.util.Map;
8
9
import static com.keenwrite.io.HttpFacade.httpGet;
10
import static com.keenwrite.io.MediaType.*;
11
import static org.junit.jupiter.api.Assertions.*;
12
13
/**
14
 * Test that {@link MediaType} instances can be queried and return reliable
15
 * results.
16
 */
17
public class MediaTypeTest {
18
  /**
19
   * Test that {@link MediaType#equals(String, String)} is case insensitive.
20
   */
21
  @Test
22
  public void test_Equality_IgnoreCase_Success() {
23
    final var mediaType = TEXT_PLAIN;
24
    assertTrue( mediaType.equals( "TeXt", "Plain" ) );
25
    assertEquals( "text/plain", mediaType.toString() );
26
  }
27
28
  /**
29
   * Test that {@link MediaType#fromFilename(String)} can lookup by file name.
30
   */
31
  @Test
32
  public void test_FilenameExtensions_Supported_Success() {
33
    final var map = Map.of(
34
      "jpeg", IMAGE_JPEG,
35
      "png", IMAGE_PNG,
36
      "svg", IMAGE_SVG_XML,
37
      "md", TEXT_MARKDOWN,
38
      "Rmd", TEXT_R_MARKDOWN,
39
      "txt", TEXT_PLAIN,
40
      "yml", TEXT_YAML
41
    );
42
43
    map.forEach( ( k, v ) -> assertEquals( v, fromFilename( "f." + k ) ) );
44
  }
45
46
  /**
47
   * Test that remote fetches will pull and identify the type of resource
48
   * based on the HTTP Content-Type header (or shallow decoding).
49
   */
50
  @Test
51
  public void test_HttpRequest_Supported_Success() {
52
    //@formatter:off
53
    final var map = Map.of(
54
       "https://stackoverflow.com/robots.txt", TEXT_PLAIN,
55
       "https://place-hold.it/300x500", IMAGE_GIF,
56
       "https://placekitten.com/g/200/300", IMAGE_JPEG,
57
       "https://upload.wikimedia.org/wikipedia/commons/9/9f/Vimlogo.svg", IMAGE_SVG_XML,
58
       "https://kroki.io//graphviz/svg/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", IMAGE_SVG_XML
59
    );
60
    //@formatter:on
61
62
    map.forEach( ( k, v ) -> {
63
      try( var response = httpGet( new URI( k ) ) ) {
64
        assertEquals( v, response.getMediaType() );
65
      } catch( Exception e ) {
66
        fail();
67
      }
68
    } );
69
  }
70
}
171
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.preview.DiagramUrlGenerator.toUrl;
6
import static org.junit.jupiter.api.Assertions.assertEquals;
7
8
/**
9
 * Responsible for testing that images sent to the diagram server will render.
10
 */
11
class DiagramUrlGeneratorTest {
12
  private final static String SERVER_NAME = "kroki.io";
13
14
  // @formatter:off
15
  private final static String[] DIAGRAMS = new String[]{
16
    "graphviz",
17
    "digraph G {Hello->World; World->Hello;}",
18
    "https://kroki.io/graphviz/svg/eJxLyUwvSizIUHBXqPZIzcnJ17ULzy_KSbFWAFO6dmBB61oAE9kNww==",
19
20
    "blockdiag",
21
    """
22
      blockdiag {
23
        Kroki -> generates -> "Block diagrams";
24
        Kroki -> is -> "very easy!";
25
26
        Kroki [color = "greenyellow"];
27
        "Block diagrams" [color = "pink"];
28
        "very easy!" [color = "orange"];
29
      }
30
      """,
31
    "https://kroki.io/blockdiag/svg/eJxdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="
32
  };
33
  // @formatter:on
34
35
  /**
36
   * Test that URL encoding works with Kroki's server.
37
   */
38
  @Test
39
  public void test_Generation_TextDiagram_UrlEncoded() {
40
    // Use a map of pairs if this test needs more complexity.
41
    for( int i = 0; i < DIAGRAMS.length / 3; i += 3 ) {
42
      final var name = DIAGRAMS[ i ];
43
      final var text = DIAGRAMS[ i + 1 ];
44
      final var expected = DIAGRAMS[ i + 2 ];
45
      final var actual = toUrl( SERVER_NAME, name, text );
46
47
      assertEquals( expected, actual );
48
    }
49
  }
50
}
151
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.Caret;
6
import com.keenwrite.preferences.Workspace;
7
import com.keenwrite.preview.HtmlPreview;
8
import com.keenwrite.processors.Processor;
9
import com.keenwrite.processors.ProcessorContext;
10
import com.keenwrite.processors.markdown.extensions.ImageLinkExtension;
11
import com.vladsch.flexmark.html.HtmlRenderer;
12
import com.vladsch.flexmark.parser.Parser;
13
import javafx.stage.Stage;
14
import org.junit.jupiter.api.Test;
15
import org.junit.jupiter.api.extension.ExtendWith;
16
import org.testfx.framework.junit5.ApplicationExtension;
17
import org.testfx.framework.junit5.Start;
18
19
import java.io.File;
20
import java.net.URI;
21
import java.net.URL;
22
import java.nio.file.Path;
23
import java.nio.file.Paths;
24
import java.util.HashMap;
25
import java.util.List;
26
import java.util.Map;
27
28
import static com.keenwrite.constants.Constants.DOCUMENT_DEFAULT;
29
import static com.keenwrite.ExportFormat.NONE;
30
import static java.lang.String.format;
31
import static javafx.application.Platform.runLater;
32
import static org.junit.jupiter.api.Assertions.assertEquals;
33
import static org.junit.jupiter.api.Assertions.assertNotNull;
34
import static org.testfx.util.WaitForAsyncUtils.waitForFxEvents;
35
36
/**
37
 * Responsible for testing that linked images render into HTML according to
38
 * the {@link ImageLinkExtension} rules.
39
 */
40
@ExtendWith( {ApplicationExtension.class, AwaitFxExtension.class} )
41
@SuppressWarnings( "SameParameterValue" )
42
public class ImageLinkExtensionTest {
43
  private static final Workspace sWorkspace = new Workspace(
44
    getResource( "workspace.xml" ) );
45
46
  private static final Map<String, String> IMAGES = new HashMap<>();
47
48
  private static final String URI_WEB = "placekitten.com/200/200";
49
  private static final String URI_DIRNAME = "images";
50
  private static final String URI_FILENAME = "kitten";
51
52
  /**
53
   * Path to use for testing image file name resolution. Note that resources use
54
   * forward slashes, regardless of OS.
55
   */
56
  private static final String URI_PATH = URI_DIRNAME + '/' + URI_FILENAME;
57
58
  /**
59
   * Extension for the first existing image that matches the preferred image
60
   * extension order.
61
   */
62
  private static final String URI_IMAGE_EXT = ".png";
63
64
  /**
65
   * Relative path to an image that exists.
66
   */
67
  private static final String URI_IMAGE = URI_PATH + URI_IMAGE_EXT;
68
69
  static {
70
    addUri( URI_PATH + ".png" );
71
    addUri( URI_PATH + ".jpg" );
72
    addUri( URI_PATH, getResource( URI_PATH + URI_IMAGE_EXT ) );
73
    addUri( "//" + URI_WEB );
74
    addUri( "http://" + URI_WEB );
75
    addUri( "https://" + URI_WEB );
76
  }
77
78
  private HtmlPreview mPreview;
79
80
  @Start
81
  @SuppressWarnings( "unused" )
82
  private void start( final Stage stage ) {
83
    mPreview = new HtmlPreview( sWorkspace );
84
  }
85
86
  private static void addUri( final String actualExpected ) {
87
    addUri( actualExpected, actualExpected );
88
  }
89
90
  private static void addUri( final String actual, final String expected ) {
91
    IMAGES.put( toMd( actual ), toHtml( expected ) );
92
  }
93
94
  private static String toMd( final String resource ) {
95
    return format( "![Tooltip](%s 'Title')", resource );
96
  }
97
98
  private static String toHtml( final String url ) {
99
    return format(
100
      "<p><img src=\"%s\" alt=\"Tooltip\" title=\"Title\" /></p>\n", url );
101
  }
102
103
  /**
104
   * Test that the key URIs present in the {@link #IMAGES} map are rendered
105
   * as the value URIs present in the same map.
106
   */
107
  @Test
108
  void test_ImageLookup_RelativePathWithExtension_ResolvedSuccessfully() {
109
    final var resource = getResourcePath( URI_IMAGE );
110
    final var imagePath = new File( URI_IMAGE ).toPath();
111
    final var subpaths = resource.getNameCount() - imagePath.getNameCount();
112
    final var subpath = resource.subpath( 0, subpaths );
113
114
    // The root component isn't considered part of the path, so add it back.
115
    final var documentPath = Path.of(
116
      resource.getRoot().resolve( subpath ).toString(),
117
      DOCUMENT_DEFAULT.getName() );
118
    final var context = createProcessorContext( documentPath );
119
    final var extension = ImageLinkExtension.create( context );
120
    final var extensions = List.of( extension );
121
    final var pBuilder = Parser.builder();
122
    final var hBuilder = HtmlRenderer.builder();
123
    final var parser = pBuilder.extensions( extensions ).build();
124
    final var renderer = hBuilder.extensions( extensions ).build();
125
126
    assertNotNull( parser );
127
    assertNotNull( renderer );
128
129
    for( final var entry : IMAGES.entrySet() ) {
130
      final var key = entry.getKey();
131
      final var node = parser.parse( key );
132
      final var expectedHtml = entry.getValue();
133
      final var actualHtml = new StringBuilder( 128 );
134
135
      runLater( () -> actualHtml.append( renderer.render( node ) ) );
136
137
      waitForFxEvents();
138
      assertEquals( expectedHtml, actualHtml.toString() );
139
    }
140
  }
141
142
  /**
143
   * Creates a new {@link ProcessorContext} for the given file name path.
144
   *
145
   * @param documentPath Fully qualified path to the file name.
146
   * @return A context used for creating new {@link Processor} instances.
147
   */
148
  private ProcessorContext createProcessorContext( final Path documentPath ) {
149
    return new ProcessorContext(
150
      mPreview,
151
      new HashMap<>(),
152
      documentPath,
153
      null,
154
      NONE,
155
      sWorkspace,
156
      Caret.builder().build()
157
    );
158
  }
159
160
  private static URL toUrl( final String path ) {
161
    final var clazz = ImageLinkExtensionTest.class;
162
    final var packagePath = clazz.getPackageName().replace( '.', '/' );
163
    final var resourcePath = '/' + packagePath + '/' + path;
164
    return clazz.getResource( resourcePath );
165
  }
166
167
  private static URI toUri( final String path ) {
168
    try {
169
      return toUrl( path ).toURI();
170
    } catch( final Exception ex ) {
171
      throw new RuntimeException( ex );
172
    }
173
  }
174
175
  private static Path getResourcePath( final String path ) {
176
    return Paths.get( toUri( path ) );
177
  }
178
179
  private static String getResource( final String path ) {
180
    return toUri( path ).toString();
181
  }
182
}
1183
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/sigils/RSigilOperatorTest.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.sigils;
3
4
import javafx.beans.property.SimpleStringProperty;
5
import javafx.beans.property.StringProperty;
6
import org.junit.jupiter.api.Test;
7
8
import static org.junit.jupiter.api.Assertions.assertEquals;
9
10
/**
11
 * Responsible for simulating R variable injection.
12
 */
13
class RSigilOperatorTest {
14
15
  private final SigilOperator mOperator = createRSigilOperator();
16
17
  /**
18
   * Test that a key name becomes an R variable.
19
   */
20
  @Test
21
  void test_Entoken_KeyName_Tokenized() {
22
    final var expected = "v$a$b$c$d";
23
    final var actual = mOperator.entoken( "{{a.b.c.d}}" );
24
    assertEquals( expected, actual );
25
  }
26
27
  /**
28
   * Test that a key name becomes a viable R expression.
29
   */
30
  @Test
31
  void test_Apply_KeyName_Expression() {
32
    final var expected = "`r#x(v$a$b$c$d)`";
33
    final var actual = mOperator.apply( "v$a$b$c$d" );
34
    assertEquals( expected, actual );
35
  }
36
37
  private StringProperty createToken( final String token ) {
38
    return new SimpleStringProperty( token );
39
  }
40
41
  private Tokens createRTokens() {
42
    return createTokens( "x(", ")" );
43
  }
44
45
  private Tokens createYamlTokens() {
46
    return createTokens( "{{", "}}" );
47
  }
48
49
  private Tokens createTokens( final String began, final String ended ) {
50
    return new Tokens( createToken( began ), createToken( ended ) );
51
  }
52
53
  private YamlSigilOperator createYamlSigilOperator() {
54
    return new YamlSigilOperator( createYamlTokens() );
55
  }
56
57
  private RSigilOperator createRSigilOperator() {
58
    return new RSigilOperator( createRTokens(), createYamlSigilOperator() );
59
  }
60
}
161
A src/test/java/com/keenwrite/tex/TeXRasterizationTest.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.tex;
29
30
import com.whitemagicsoftware.tex.DefaultTeXFont;
31
import com.whitemagicsoftware.tex.TeXEnvironment;
32
import com.whitemagicsoftware.tex.TeXFormula;
33
import com.whitemagicsoftware.tex.TeXLayout;
34
import com.whitemagicsoftware.tex.graphics.AbstractGraphics2D;
35
import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
36
import com.whitemagicsoftware.tex.graphics.SvgGraphics2D;
37
import org.apache.batik.transcoder.TranscoderException;
38
import org.junit.jupiter.api.Test;
39
import org.xml.sax.SAXException;
40
41
import javax.imageio.ImageIO;
42
import java.awt.image.BufferedImage;
43
import java.io.ByteArrayInputStream;
44
import java.io.File;
45
import java.io.IOException;
46
import java.nio.file.Path;
47
import java.text.ParseException;
48
49
import static com.keenwrite.dom.DocumentParser.parse;
50
import static com.keenwrite.preview.SvgRasterizer.*;
51
import static java.lang.System.getProperty;
52
import static org.junit.jupiter.api.Assertions.assertEquals;
53
54
/**
55
 * Test that TeX rasterization produces a readable image.
56
 */
57
public class TeXRasterizationTest {
58
  private static final String EQUATION =
59
    "G_{\\mu \\nu} = \\frac{8 \\pi G}{c^4} T_{{\\mu \\nu}}";
60
61
  private static final String DIR_TEMP = getProperty( "java.io.tmpdir" );
62
63
  private static final long FILESIZE = 12364;
64
65
  /**
66
   * Test that an equation can be converted to a raster image and the
67
   * final raster image size corresponds to the input equation. This is
68
   * a simple way to verify that the rasterization process is correct,
69
   * albeit if any aspect of the SVG algorithm changes (such as padding
70
   * around the equation), it will cause this test to fail, which is a bit
71
   * misleading.
72
   */
73
  @Test
74
  public void test_Rasterize_SimpleFormula_CorrectImageSize()
75
    throws IOException, ParseException, TranscoderException {
76
    final var g = new SvgGraphics2D();
77
78
    drawGraphics( g );
79
    verifyImage( rasterizeString( g.toString() ) );
80
  }
81
82
  /**
83
   * Test that an SVG document object model can be parsed and rasterized into
84
   * an image.
85
   */
86
  @Test
87
  public void getTest_SvgDomGraphics2D_InputElement_OutputRasterizedImage()
88
    throws IOException, SAXException, ParseException, TranscoderException {
89
    final var g = new SvgGraphics2D();
90
    drawGraphics( g );
91
92
    final var expectedSvg = g.toString();
93
    final var bytes = expectedSvg.getBytes();
94
    final var doc = parse( new ByteArrayInputStream( bytes ) );
95
    final var actualSvg = toSvg( doc.getDocumentElement() );
96
97
    verifyImage( rasterizeString( actualSvg ) );
98
  }
99
100
  /**
101
   * Test that an SVG image from a DOM element can be rasterized.
102
   *
103
   * @throws IOException Could not write the image.
104
   */
105
  @Test
106
  public void test_SvgDomGraphics2D_InputDom_OutputRasterizedImage()
107
    throws IOException, ParseException, TranscoderException {
108
    final var g = new SvgDomGraphics2D();
109
110
    drawGraphics( g );
111
    verifyImage( rasterize( g.toDom() ) );
112
  }
113
114
  /**
115
   * Asserts that the given image matches an expected file size.
116
   *
117
   * @param image The image to check against the file size.
118
   * @throws IOException Could not write the image.
119
   */
120
  private void verifyImage( final BufferedImage image ) throws IOException {
121
    final var file = export( image, "dom.png" );
122
    assertEquals( FILESIZE, file.length() );
123
  }
124
125
  /**
126
   * Creates an SVG string for the default equation and font size.
127
   */
128
  private void drawGraphics( final AbstractGraphics2D g ) {
129
    final var size = 100f;
130
    final var texFont = new DefaultTeXFont( size );
131
    final var env = new TeXEnvironment( texFont );
132
    g.scale( size, size );
133
134
    final var formula = new TeXFormula( EQUATION );
135
    final var box = formula.createBox( env );
136
    final var layout = new TeXLayout( box, size );
137
138
    g.initialize( layout.getWidth(), layout.getHeight() );
139
    box.draw( g, layout.getX(), layout.getY() );
140
  }
141
142
  @SuppressWarnings( "SameParameterValue" )
143
  private File export( final BufferedImage image, final String filename )
144
    throws IOException {
145
    final var path = Path.of( DIR_TEMP, filename );
146
    final var file = path.toFile();
147
    ImageIO.write( image, "png", file );
148
    file.deleteOnExit();
149
    return file;
150
  }
151
}
1152
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/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/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