Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M .gitignore
1010
.classpath
1111
.idea
12
count
13
themes
14
quotes
15
tex
16
spell
12
count/
13
themes/
14
quotes/
15
tex/
16
spell/
1717
keenwrite.build_artifacts.txt
1818
todo
19
tokens
19
tokens/
2020
M BUILD.md
88
99
* [JDK 21](https://bell-sw.com/pages/downloads) (Full JDK + JavaFX)
10
* [Gradle 8.3](https://gradle.org/releases)
11
* [Git 2.40.1](https://git-scm.com/downloads)
10
* [Gradle 8.5](https://gradle.org/releases)
11
* [Git 2.43](https://git-scm.com/downloads)
1212
* [warp v0.4.0-alpha](https://github.com/Reisz/warp/releases/tag/v0.4.0)
1313
...
8484
## Binaries
8585
86
Run the `installer` script to build platform-specific binaries, such as:
86
Run the `installer.sh` script to build platform-specific binaries, such as:
8787
88
    ./installer -V -o linux
88
    ./installer.sh -V -o linux
8989
90
The `installer` script:
90
The `installer.sh` script:
9191
9292
* downloads a JDK;
9393
* generates a run script;
9494
* bundles the JDK, run script, and JAR file; and
9595
* creates a standalone binary, so no installation required.
9696
97
Run `./installer -h` to see all command-line options.
97
Run `./installer.sh -h` to see all command-line options.
9898
9999
# Releases
M bug-filter.xml
33
  <Match>
44
    <Or>
5
      <Bug code="EI, EI2" />
5
      <Bug code="EI, EI2, CT, RV, PI" />
66
    </Or>
77
  </Match>
M build.gradle
99
  }
1010
  dependencies {
11
    classpath 'org.owasp:dependency-check-gradle:8.4.2'
12
    classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:5.2.1"
11
    classpath 'org.owasp:dependency-check-gradle:9.0.1'
12
    classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:5.2.4"
1313
  }
1414
}
1515
1616
plugins {
1717
  id 'application'
1818
  id 'org.openjfx.javafxplugin' version '0.1.0'
1919
  id 'com.palantir.git-version' version '3.0.0'
20
  id "com.github.spotbugs" version "5.2.1"
20
  id "com.github.spotbugs" version "5.2.4"
2121
}
2222
...
8989
9090
dependencies {
91
  def v_junit = '5.10.0'
91
  def v_junit = '5.10.1'
9292
  def v_flexmark = '0.64.8'
9393
  def v_jackson = '2.15.3'
94
  def v_echosvg = '1.0'
94
  def v_echosvg = '1.0.1'
9595
  def v_picocli = '4.7.5'
9696
9797
  // JavaFX
98
  implementation 'org.controlsfx:controlsfx:11.1.2'
98
  implementation 'org.controlsfx:controlsfx:11.2.0'
9999
  implementation 'org.fxmisc.richtext:richtextfx:0.11.2'
100100
  implementation 'org.fxmisc.flowless:flowless:0.7.2'
...
120120
121121
  // HTML parsing and rendering
122
  implementation 'org.jsoup:jsoup:1.16.1'
122
  implementation 'org.jsoup:jsoup:1.17.1'
123123
  implementation 'org.xhtmlrenderer:flying-saucer-core:9.3.1'
124124
125125
  // R
126
  implementation 'org.apache.commons:commons-compress:1.24.0'
126
  implementation 'org.apache.commons:commons-compress:1.25.0'
127127
  implementation 'org.codehaus.plexus:plexus-utils:4.0.0'
128128
  implementation 'org.renjin:renjin-script-engine:3.5-beta76'
...
137137
  implementation "io.sf.carte:echosvg-gvt:${v_echosvg}"
138138
  implementation "io.sf.carte:echosvg-parser:${v_echosvg}"
139
  implementation "io.sf.carte:echosvg-script:${v_echosvg}"
140139
  implementation "io.sf.carte:echosvg-svg-dom:${v_echosvg}"
141140
  implementation "io.sf.carte:echosvg-svggen:${v_echosvg}"
142141
  implementation "io.sf.carte:echosvg-transcoder:${v_echosvg}"
143142
  implementation "io.sf.carte:echosvg-util:${v_echosvg}"
144143
  implementation "io.sf.carte:echosvg-xml:${v_echosvg}"
145144
146145
  // Misc.
147146
  implementation 'org.ahocorasick:ahocorasick:0.6.3'
148
  implementation 'org.apache.commons:commons-configuration2:2.9.0'
147
  implementation 'org.apache.commons:commons-lang3:3.14.0'
149148
  implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
150149
  implementation 'jakarta.validation:jakarta.validation-api:3.0.2'
...
171170
  testImplementation 'org.assertj:assertj-core:3.24.2'
172171
  testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
172
}
173
174
/**
175
 * Mozilla's JavaScript engine, Rhino, is used by the SVG rasterizer.
176
 * The application does not support animated SVG files (the HTML
177
 * renderer won't honour them), so we can eliminate the dependency from
178
 * the binaries.
179
 */
180
configurations {
181
  all*.exclude group: 'org.mozilla', module: 'rhino'
173182
}
174183
...
251260
  useJUnitPlatform()
252261
253
  doFirst { jvmArgs = moduleSecurity }
262
  doFirst { jvmArgs += moduleSecurity }
254263
  testLogging { exceptionFormat = 'full' }
264
}
265
266
tasks.withType( JavaCompile ).configureEach {
267
  options.compilerArgs += "--enable-preview"
268
}
269
270
tasks.withType( JavaExec ).configureEach {
271
  jvmArgs += "--enable-preview"
272
}
273
274
tasks.withType( Test ).configureEach {
275
  jvmArgs += "--enable-preview"
255276
}
256277
M docs/README.md
1111
* [svg.md](svg.md) -- Resolve issues with some SVG files
1212
* [metadata.md](metadata.md) -- Document metadata
13
* [references.md](references.md) -- Captions and cross-references
1314
* [typesetting.md](typesetting.md) -- Document typesetting
1415
* [variables.md](variables.md) -- Variable definitions and interpolation
A docs/references.md
1
# Captions and cross-references
2
3
Users may define captions and cross-references to tables, figures,
4
and equations. Unfortunately, at time of writing, the CommonMark
5
specification is frozen. This means cross-references must be implemented
6
as an extension to the Markdown specification, leaving the door open for
7
differing Markdown rendering libraries and applications to diverge on
8
with respect to syntax.
9
10
# Syntax
11
12
This section describes how to use captions and cross-references within
13
Markdown documents. The CommonMark standard details different ways to
14
add captions to tables and figures. While those are supported, a more
15
consistent syntax is available.
16
17
## Captions
18
19
Tables, figures, and equations are captioned using the same syntax. In
20
general, a line that starts with a double colon after a blank line will
21
result in a caption added to the item immediately proceding it. The
22
remainder of this section provides examples.
23
24
### Images
25
26
An image caption:
27
28
```
29
![kitten](https://placekitten.com/600/350)
30
31
:: Figure caption text
32
```
33
34
### Table
35
36
A table caption:
37
38
```
39
| a | b | c |
40
|---|---|---|
41
| 1 | 2 | 3 |
42
| 4 | 5 | 6 |
43
| 7 | 8 | 9 |
44
45
:: Table caption text
46
```
47
48
### Equation
49
50
An equation caption:
51
52
```
53
$$E | mc^2$$
54
55
:: Equation caption
56
```
57
58
# Cross-references
59
60
There are two parts to a cross-reference: the anchor name and its references.
61
An anchor name must be uniquely defined. Any number of references may refer
62
to an anchor name. Anchor names can be associated with any item in the
63
document, and are primarily added to captions.
64
65
The general syntax for anchor names and references is:
66
67
* `{#type-name:label}` (anchor name)
68
* `[@type-name:label]` (reference)
69
70
The `type-name` can be any alphanumeric value, starting with a letter.
71
Type names user-defined categories for the item type. Labels are user-defined
72
identifiers that must be unique per item.
73
74
Consider the following example:
75
76
```
77
In [@fig:kitten], a cute kitten is shown.
78
79
![kitten](https://placekitten.com/600/350)
80
81
:: World's cutest kitten {#fig:kitten}
82
83
There is no cuter kitten than the one in [@fig:kitten].
84
```
85
86
The anchor name uniquely defines where an item in a document is located. Any
87
number of references, anywhere in the document, may reference an anchor name.
88
There are few restrictions placed on the possible type names and labels. Here
89
are a few more anchor name examples:
90
91
```
92
{#fig:cats}
93
{#図版:猫}
94
{#eq:mass-energy}
95
{#eqn:laplace}
96
```
97
98
## Type names
99
100
To avoid duplicating writing the label each time (e.g., Figure, Table,
101
Equation), there are a number of predefined labels associated with
102
type names. The following table lists the type names and the label
103
generated by the typesetting system:
104
105
| Type name | English name
106
|---|---|
107
| algorithm | Algorithm |
108
| alg | Algorithm |
109
| equation | Equation |
110
| eqn | Equation |
111
| eq | Equation |
112
| figure | Figure |
113
| fig | Figure |
114
| formula | Formula |
115
| listing | Listing |
116
| list | Listing |
117
| lst | Listing |
118
| lyric | Lyrics |
119
| music | Score |
120
| score | Score |
121
| source | Listing |
122
| src | Listing |
123
| tab | Table |
124
| table | Table |
125
| tbl | Table |
126
127
These values are defined in the theme's `xhtml/xml-references.tex` file.
128
1129
M docs/skins.md
6262
can tweak them to your taste. Accomplish this as follows:
6363
64
1. Visit the [skin](https://github.com/DaveJarvis/keenwrite/tree/master/src/main/resources/com/keenwrite/skins) repository directory
64
1. Visit the [skin](https://gitlab.com/DaveJarvis/KeenWrite/-/tree/main/src/main/resources/com/keenwrite/skins) repository directory
6565
1. Click one of the files (e.g., `haunted_grey.css`).
6666
1. Click **Raw**.
6767
1. Copy the entire text.
6868
1. Return to `custom.css`.
6969
1. Delete the contents.
7070
1. Paste the copied text.
7171
1. Save the file.
7272
7373
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.
74
[markdown.css](https://gitlab.com/DaveJarvis/KeenWrite/-/blob/main/src/main/resources/com/keenwrite/editor/markdown.css), which is also in the repository.
7575
7676
# Modena
...
8383
# JavaFX CSS
8484
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
85
The [Java CSS Reference Guide](https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/doc-files/cssref.html) is exhaustive. In addition to showing many
8686
differences between JavaFX CSS and W3C CSS, the guide introduces numerous
8787
helpful functions for manipulating colours and gradients using existing
...
9797
9898
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.
99
it along. Either open a new issue in the [issue tracker](https://gitlab.com/DaveJarvis/KeenWrite/-/issues) that contains the CSS file or submit a pull request.
100100
101101
M docs/typesetting-custom.md
3535
1. Run **install.bat** to download and install the software.
3636
    * 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>.
37
1. Right-click [localpath.bat](https://gitlab.com/DaveJarvis/KeenWrite/-/raw/main/scripts/localpath.bat).
3838
1. Select **Save Link As** (or similar).
3939
1. Save the file to the typesetting software's "root" directory.
...
8080
Install and configure the default theme pack as follows:
8181
82
1. Download the <a href="https://gitreleases.dev/gh/DaveJarvis/keenwrite-themes/latest/theme-pack.zip">theme-pack.zip</a> archive.
82
1. Download the <a href="https://gitlab.com/DaveJarvis/keenwrite-themes/-/releases/permalink/latest/downloads/theme-pack.zip">theme-pack.zip</a> archive.
8383
1. Extract archive into a known location.
8484
1. Start the text editor, if not already running.
...
160160
Here are a few documents that introduce the typesetting system:
161161
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))
162
* [What is ConTeXt?](https://www.pragma-ade.com/general/manuals/what-is-context.pdf)
163
* [A not so short introduction to ConTeXt](https://github.com/contextgarden/not-so-short-introduction-to-context)
164
* [Dealing with XML in ConTeXt MKIV](https://pragma-ade.com/general/manuals/xml-mkiv.pdf)
165
* [Typographic Programming](https://www.pragma-ade.com/general/manuals/style.pdf)
166166
167167
The [documentation library](https://wiki.contextgarden.net/Documentation) includes the following gems:
M docs/typesetting.md
33
The application uses the [ConTeXt](https://contextgarden.net) typesetting
44
system, the [podman](https://podman.io/) container manager, various
5
[themes](https://github.com/DaveJarvis/keenwrite-themes/), and numerous
5
[themes](https://gitlab.com/DaveJarvis/keenwrite-themes), and numerous
66
fonts to produce high-quality PDF files. The container manager significantly
77
reduces the number of manual steps in the installation process.
...
1515
1. Start the application.
1616
1. Click **File → Export As → PDF**.
17
18
A wizard appears.
19
20
## Windows
21
22
23
## Linux
24
2517
26
## macOS
18
Follow the steps in the wizard to install the requisite software and
19
typesetting themes.
2720
2821
M installer.sh
2020
2121
readonly OPT_JAVA=$(cat << END_OF_ARGS
22
-Dprism.order=sw \
23
--enable-preview \
2224
--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED \
2325
--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED \
M keenwrite.sh
22
33
java \
4
  -Dprism.order=sw \
5
  --enable-preview \
46
  --add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED \
57
  --add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED \
M publish.sh
88
readonly RELEASE=$(git describe --abbrev=0 --tags)
99
readonly APP_NAME=$(cut -d= -f2 ./src/main/resources/bootstrap.properties)
10
readonly PATH_TOKEN="tokens/${APP_NAME,,}.pat"
11
readonly URL=$(cat tokens/publish.url)
10
readonly APP_NAME_LC=${APP_NAME,,}
11
readonly PATH_TOKEN="tokens/${APP_NAME_LC}.pat"
12
readonly URL=$(cat "tokens/publish.url")
13
readonly FILE_VERSION="version.txt"
1214
1315
# ---------------------------------------------------------------------------
14
# Publishes a self-extracting installer to the repository.
16
# Adds download URLs to a release.
17
#
18
# $1 - The system (Linux, WIndows, MacOS, Java)
19
# ---------------------------------------------------------------------------
20
release() {
21
  local -r OS="${1}"
22
  local ARCH=" (64-bit, x86)"
23
  local FILE_PREFIX="${APP_NAME_LC}"
24
  local FILE_SUFFIX="bin"
25
26
  case ${OS} in
27
    MacOS)
28
      FILE_SUFFIX="app"
29
    ;;
30
    Windows)
31
      FILE_PREFIX="${APP_NAME}"
32
      FILE_SUFFIX="exe"
33
    ;;
34
    Java)
35
      ARCH=""
36
      FILE_SUFFIX="jar"
37
    ;;
38
    *)
39
      # Linux, others
40
    ;;
41
  esac
42
43
  local -r BINARY="${FILE_PREFIX}.${FILE_SUFFIX}"
44
45
  upload "${BINARY}"
46
47
  glab release upload ${RELEASE} \
48
    --assets-links="[{
49
      \"name\":\"${APP_NAME} for ${OS}${ARCH}\",
50
      \"url\":\"https://${APP_NAME_LC}.com/downloads/${BINARY}\",
51
      \"link_type\":\"other\"
52
    }]"
53
}
54
55
# ---------------------------------------------------------------------------
56
# Uploads a file to the remote host.
1557
#
1658
# $1 - The relative path to the file to upload.
1759
# ---------------------------------------------------------------------------
18
publish() {
19
  local -r PATH_ARCHIVE="${1}"
60
upload() {
61
  local -r FILENAME="${1}"
2062
21
  if [ -f "${PATH_ARCHIVE}" ]; then
22
    scp "${PATH_ARCHIVE}" "${URL}"
63
  if [ -f "${FILENAME}" ]; then
64
    scp "${FILENAME}" "${URL}"
2365
  else
24
    echo "Missing ${PATH_ARCHIVE}, continuing."
66
    echo "Missing ${FILE_BINARY}, continuing."
2567
  fi
2668
}
2769
2870
if [ -f "${PATH_TOKEN}" ]; then
2971
  cat "${PATH_TOKEN}" | glab auth login --hostname gitlab.com --stdin
3072
31
  publish "${APP_NAME,,}.jar"
32
  publish "${APP_NAME,,}.bin"
33
  publish "${APP_NAME,,}.app"
34
  publish "${APP_NAME}.exe"
73
  release "Windows"
74
  release "MacOS"
75
  release "Linux"
76
  release "Java"
77
78
  echo "${RELEASE}" > "${FILE_VERSION}"
79
  upload "${FILE_VERSION}"
80
  mv "${FILE_VERSION}" "www/downloads"
3581
else
3682
  echo "Create ${PATH_TOKEN} before publishing the release."
M src/main/java/com/keenwrite/AppCommands.java
2424
import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN;
2525
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
26
import static java.nio.charset.StandardCharsets.UTF_8;
2627
import static java.nio.file.Files.readString;
2728
import static java.nio.file.Files.writeString;
...
8889
        // Processors can export binary files. In such cases, processors will
8990
        // return null to prevent further processing.
90
        final var result =
91
          outputDoc == null ? null : writeString( outputPath, outputDoc );
91
        final var result = outputDoc == null
92
          ? null
93
          : writeString( outputPath, outputDoc, UTF_8 );
9294
9395
        future.complete( outputPath );
...
141143
    // directory to scan for files; or there's no extension for globbing.
142144
    if( !concat || parent == null || extension.isBlank() ) {
143
      return readString( inputPath );
145
      return readString( inputPath, UTF_8 );
144146
    }
145147
M src/main/java/com/keenwrite/cmdline/Arguments.java
2525
2626
import static com.keenwrite.constants.Constants.DIAGRAM_SERVER_NAME;
27
import static java.nio.charset.StandardCharsets.UTF_8;
2728
2829
/**
...
293294
294295
  private static String read( final Path path ) throws IOException {
295
    return path == null ? "" : Files.readString( path );
296
    return path == null ? "" : Files.readString( path, UTF_8 );
296297
  }
297298
M src/main/java/com/keenwrite/collections/InterpolatingMap.java
22
package com.keenwrite.collections;
33
4
import com.keenwrite.sigils.PropertyKeyOperator;
45
import com.keenwrite.sigils.SigilKeyOperator;
56
67
import java.io.Serial;
78
import java.util.HashMap;
89
import java.util.Map;
910
import java.util.Objects;
1011
import java.util.concurrent.ConcurrentHashMap;
12
13
import static java.util.regex.Matcher.quoteReplacement;
1114
1215
/**
...
2730
2831
  private transient final SigilKeyOperator mOperator;
32
33
  /**
34
   * Creates a new interpolating map using the {@link PropertyKeyOperator}.
35
   */
36
  public InterpolatingMap() {
37
    this( new PropertyKeyOperator() );
38
  }
2939
3040
  /**
...
7383
   * @return The given value with all embedded key references interpolated.
7484
   */
75
  public String interpolate( String value ) {
85
  public String interpolate( final String value ) {
7686
    assert value != null;
7787
7888
    final var matcher = mOperator.match( value );
89
    final var sb = new StringBuilder( value.length() << 1 );
7990
8091
    while( matcher.find() ) {
8192
      final var keyName = matcher.group( GROUP_DELIMITED );
8293
      final var mapValue = get( keyName );
8394
8495
      if( mapValue != null ) {
8596
        final var keyValue = interpolate( mapValue );
86
        value = value.replace( mOperator.apply( keyName ), keyValue );
97
        matcher.appendReplacement( sb, quoteReplacement( keyValue ) );
8798
      }
8899
    }
89100
90
    return value;
101
    matcher.appendTail( sb );
102
    return sb.toString();
91103
  }
92104
93105
  @Override
94106
  public boolean equals( final Object o ) {
95
    if( this == o ) { return true; }
96
    if( o == null || getClass() != o.getClass() ) { return false; }
97
    if( !super.equals( o ) ) { return false; }
107
    if( this == o ) {
108
      return true;
109
    }
110
111
    if( o == null || getClass() != o.getClass() ) {
112
      return false;
113
    }
114
115
    if( !super.equals( o ) ) {
116
      return false;
117
    }
118
98119
    final InterpolatingMap that = (InterpolatingMap) o;
99120
    return Objects.equals( mOperator, that.mOperator );
M src/main/java/com/keenwrite/commands/ConcatenateCommand.java
1717
import static com.keenwrite.util.FileWalker.walk;
1818
import static java.lang.System.lineSeparator;
19
import static java.nio.charset.StandardCharsets.UTF_8;
1920
import static java.nio.file.Files.readString;
2021
...
6364
          clue( "Main.status.export.concat", file );
6465
65
          text.append( readString( file ) )
66
          text.append( readString( file, UTF_8 ) )
6667
              .append( eol );
6768
        }
A src/main/java/com/keenwrite/config/PropertiesConfiguration.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.config;
6
7
import com.keenwrite.collections.InterpolatingMap;
8
9
import java.io.IOException;
10
import java.io.Reader;
11
import java.util.*;
12
13
import static java.util.Arrays.*;
14
15
/**
16
 * Responsible for reading and interpolating properties files.
17
 */
18
public class PropertiesConfiguration {
19
  private static final String VALUE_SEPARATOR = ",";
20
21
  private final InterpolatingMap mMap = new InterpolatingMap();
22
23
  public PropertiesConfiguration() {}
24
25
  public void read( final Reader reader ) throws IOException {
26
    final var properties = new Properties();
27
    properties.load( reader );
28
29
    for( final var name : properties.stringPropertyNames() ) {
30
      mMap.put( name, properties.getProperty( name ) );
31
    }
32
33
    mMap.interpolate();
34
  }
35
36
  /**
37
   * Returns the value of a string property.
38
   *
39
   * @param property     The property key.
40
   * @param defaultValue The value to return if no property key has been set.
41
   * @return The property key value, or defaultValue when no key found.
42
   */
43
  public String getString( final String property, final String defaultValue ) {
44
    assert property != null;
45
46
    return mMap.getOrDefault( property, defaultValue );
47
  }
48
49
  /**
50
   * Returns the value of a string property.
51
   *
52
   * @param property     The property key.
53
   * @param defaultValue The value to return if no property key has been set.
54
   * @return The property key value, or defaultValue when no key found.
55
   */
56
  public int getInt( final String property, final int defaultValue ) {
57
    assert property != null;
58
59
    return parse( mMap.get( property ), defaultValue );
60
  }
61
62
  /**
63
   * Convert the generic list of property objects into strings.
64
   *
65
   * @param property The property value to coerce.
66
   * @param defaults The values to use should the property be unset.
67
   * @return The list of properties coerced from objects to strings.
68
   */
69
  public List<String> getList(
70
    final String property, final List<String> defaults ) {
71
    assert property != null;
72
73
    final var value = mMap.get( property );
74
75
    return value == null
76
      ? defaults
77
      : asList( value.split( VALUE_SEPARATOR ) );
78
  }
79
80
  /**
81
   * Returns a list of property names that begin with the given prefix.
82
   * Note that the prefix must be separated from other values with a
83
   * period.
84
   *
85
   * @param prefix The prefix to compare against each property name. When
86
   *               comparing, the prefix value will have a period appended.
87
   * @return The list of property names that have the given prefix.
88
   */
89
  public Iterator<String> getKeys( final String prefix ) {
90
    assert prefix != null;
91
92
    final var result = new HashMap<String, String>();
93
    final var prefixDotted = prefix + '.';
94
95
    for( final var entry : mMap.entrySet() ) {
96
      final var key = entry.getKey();
97
98
      if( key.startsWith( prefixDotted ) ) {
99
        final var value = entry.getValue();
100
        result.put( key, value );
101
      }
102
    }
103
104
    return result.keySet().iterator();
105
  }
106
107
  private static int parse( final String s, final int defaultValue ) {
108
    try {
109
      return s == null || s.isBlank() ? defaultValue : Integer.parseInt( s );
110
    } catch( final NumberFormatException e ) {
111
      return defaultValue;
112
    }
113
  }
114
}
1115
M src/main/java/com/keenwrite/editors/markdown/MarkdownEditor.java
1212
import com.keenwrite.preferences.LocaleProperty;
1313
import com.keenwrite.preferences.Workspace;
14
import com.keenwrite.processors.markdown.extensions.CaretExtension;
14
import com.keenwrite.processors.markdown.extensions.caret.CaretExtension;
1515
import javafx.beans.binding.Bindings;
1616
import javafx.beans.property.*;
...
122122
  }
123123
124
  @SuppressWarnings( "unused" )
124125
  private void initTextArea( final StyleClassedTextArea textArea ) {
125126
    textArea.setShowCaret( ON );
...
149150
  }
150151
152
  @SuppressWarnings( "unused" )
151153
  private void initStyle( final StyleClassedTextArea textArea ) {
152154
    textArea.getStyleClass().add( "markdown" );
M src/main/java/com/keenwrite/preview/DiagramUrlGenerator.java
5151
   * @return A lossless, compressed sequence of bytes.
5252
   */
53
  private static byte[] compress( byte[] source ) {
53
  private static byte[] compress( final byte[] source ) {
5454
    final var deflater = new Deflater();
5555
    deflater.setInput( source );
M src/main/java/com/keenwrite/preview/FlyingSaucerPanel.java
182182
    if( !box.getStyle().isInline() ) {
183183
      final var margin = box.getMargin( getLayoutContext() );
184
      y += margin.top();
185
      x += margin.left();
184
      y += (int) margin.top();
185
      x += (int) margin.left();
186186
    }
187187
M src/main/java/com/keenwrite/preview/HtmlPreview.java
119119
      mScrollLockButton.setMargin( new Insets( 1, 0, 0, 0 ) );
120120
      mScrollLockButton.addActionListener(
121
        e -> fireScrollLockEvent( !mScrollLocked )
121
        _ -> fireScrollLockEvent( !mScrollLocked )
122122
      );
123123
M src/main/java/com/keenwrite/processors/PdfProcessor.java
99
import static com.keenwrite.io.SysFile.normalize;
1010
import static com.keenwrite.typesetting.Typesetter.Mutator;
11
import static java.nio.charset.StandardCharsets.UTF_8;
1112
import static java.nio.file.Files.deleteIfExists;
1213
import static java.nio.file.Files.writeString;
...
4344
4445
      final var document = TEXT_XML.createTempFile( APP_TITLE_ABBR, parent );
45
      final var sourcePath = writeString( document, xhtml );
46
      final var sourcePath = writeString( document, xhtml, UTF_8 );
4647
      clue( "Main.status.typeset.setting", "source", sourcePath );
4748
M src/main/java/com/keenwrite/processors/ProcessorFactory.java
130130
  private static Processor<String> createPdfProcessor(
131131
    final ProcessorContext context ) {
132
    final var pdfp = new PdfProcessor( context );
133
    return createXhtmlProcessor( pdfp, context );
132
    final var pdfProcessor = new PdfProcessor( context );
133
    return createXhtmlProcessor( pdfProcessor, context );
134134
  }
135135
M src/main/java/com/keenwrite/processors/markdown/BaseMarkdownProcessor.java
66
import com.keenwrite.processors.Processor;
77
import com.keenwrite.processors.ProcessorContext;
8
import com.keenwrite.processors.markdown.extensions.captions.CaptionExtension;
89
import com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension;
910
import com.keenwrite.processors.markdown.extensions.r.RInlineExtension;
11
import com.keenwrite.processors.markdown.extensions.references.CrossReferenceExtension;
1012
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
1113
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
...
6769
    extensions.add( TablesExtension.create() );
6870
    extensions.add( FencedDivExtension.create() );
71
    extensions.add( CrossReferenceExtension.create() );
72
    extensions.add( CaptionExtension.create() );
6973
7074
    return extensions;
M src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
1010
import com.keenwrite.processors.ProcessorContext;
1111
import com.keenwrite.processors.VariableProcessor;
12
import com.keenwrite.processors.markdown.extensions.CaretExtension;
13
import com.keenwrite.processors.markdown.extensions.DocumentOutlineExtension;
14
import com.keenwrite.processors.markdown.extensions.ImageLinkExtension;
12
import com.keenwrite.processors.markdown.extensions.caret.CaretExtension;
1513
import com.keenwrite.processors.markdown.extensions.fences.FencedBlockExtension;
14
import com.keenwrite.processors.markdown.extensions.images.ImageLinkExtension;
15
import com.keenwrite.processors.markdown.extensions.outline.DocumentOutlineExtension;
1616
import com.keenwrite.processors.markdown.extensions.r.RInlineExtension;
17
import com.keenwrite.processors.markdown.extensions.tex.TeXExtension;
17
import com.keenwrite.processors.markdown.extensions.tex.TexExtension;
1818
import com.keenwrite.processors.r.RInlineEvaluator;
1919
import com.keenwrite.processors.r.RVariableProcessor;
...
8383
8484
    result.add( ImageLinkExtension.create( context ) );
85
    result.add( TeXExtension.create( evaluator, context ) );
85
    result.add( TexExtension.create( evaluator, context ) );
8686
    result.add( FencedBlockExtension.create( processor, evaluator, context ) );
8787
D src/main/java/com/keenwrite/processors/markdown/extensions/CaretExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions;
3
4
import com.keenwrite.editors.common.Caret;
5
import com.keenwrite.constants.Constants;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.vladsch.flexmark.ext.tables.TableBlock;
8
import com.vladsch.flexmark.html.AttributeProvider;
9
import com.vladsch.flexmark.html.AttributeProviderFactory;
10
import com.vladsch.flexmark.html.IndependentAttributeProviderFactory;
11
import com.vladsch.flexmark.html.renderer.AttributablePart;
12
import com.vladsch.flexmark.html.renderer.LinkResolverContext;
13
import com.vladsch.flexmark.util.ast.Node;
14
import com.vladsch.flexmark.util.html.AttributeImpl;
15
import com.vladsch.flexmark.util.html.MutableAttributes;
16
import org.jetbrains.annotations.NotNull;
17
18
import java.util.function.Supplier;
19
20
import static com.keenwrite.constants.Constants.CARET_ID;
21
import static com.keenwrite.processors.markdown.extensions.EmptyNode.EMPTY_NODE;
22
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
23
24
/**
25
 * Responsible for giving most block-level elements a unique identifier
26
 * attribute. The identifier is used to coordinate scrolling.
27
 */
28
public class CaretExtension extends HtmlRendererAdapter {
29
30
  private final Supplier<Caret> mCaret;
31
32
  private CaretExtension( final ProcessorContext context ) {
33
    mCaret = context.getCaret();
34
  }
35
36
  public static CaretExtension create( final ProcessorContext context ) {
37
    return new CaretExtension( context );
38
  }
39
40
  @Override
41
  public void extend( @NotNull final Builder builder,
42
                      @NotNull final String rendererType ) {
43
    builder.attributeProviderFactory(
44
      IdAttributeProvider.createFactory( mCaret ) );
45
  }
46
47
  /**
48
   * Responsible for creating the id attribute. This class is instantiated
49
   * once: for the HTML element containing the {@link Constants#CARET_ID}.
50
   */
51
  public static class IdAttributeProvider implements AttributeProvider {
52
    private final Supplier<Caret> mCaret;
53
    private boolean mAdded;
54
55
    public IdAttributeProvider( final Supplier<Caret> caret ) {
56
      mCaret = caret;
57
    }
58
59
    private static AttributeProviderFactory createFactory(
60
      final Supplier<Caret> caret ) {
61
      return new IndependentAttributeProviderFactory() {
62
        @Override
63
        public @NotNull AttributeProvider apply(
64
          @NotNull final LinkResolverContext context ) {
65
          return new IdAttributeProvider( caret );
66
        }
67
      };
68
    }
69
70
    @Override
71
    public void setAttributes( @NotNull Node curr,
72
                               @NotNull AttributablePart part,
73
                               @NotNull MutableAttributes attributes ) {
74
      // Optimization: if a caret is inserted, don't try to find another.
75
      if( mAdded ) {
76
        return;
77
      }
78
79
      final var caret = mCaret.get();
80
81
      // If a table block has been earmarked with an empty node, it means
82
      // another extension has generated code from an external source. The
83
      // Markdown processor won't be able to determine the caret position
84
      // with any semblance of accuracy, so skip the element. This usually
85
      // happens with tables, but in theory any Markdown generated from an
86
      // external source (e.g., an R script) could produce text that has no
87
      // caret position that can be calculated.
88
      var table = curr;
89
90
      if( !(curr instanceof TableBlock) ) {
91
        table = curr.getAncestorOfType( TableBlock.class );
92
      }
93
94
      // The table was generated outside the document
95
      if( table != null && table.getLastChild() == EMPTY_NODE ) {
96
        return;
97
      }
98
99
      final var outside = caret.isAfterText() ? 1 : 0;
100
      final var began = curr.getStartOffset();
101
      final var ended = curr.getEndOffset() + outside;
102
      final var prev = curr.getPrevious();
103
104
      // If the caret is within the bounds of the current node or the
105
      // caret is within the bounds of the end of the previous node and
106
      // the start of the current node, then mark the current node with
107
      // a caret indicator.
108
      if( caret.isBetweenText( began, ended ) ||
109
        prev != null && caret.isBetweenText( prev.getEndOffset(), began ) ) {
110
        // This line empowers synchronizing the text editor with the preview.
111
        attributes.addValue( AttributeImpl.of( "id", CARET_ID ) );
112
113
        // We're done until the user moves the caret (micro-optimization)
114
        mAdded = true;
115
      }
116
    }
117
  }
118
}
1191
D src/main/java/com/keenwrite/processors/markdown/extensions/DocumentOutlineExtension.java
1
package com.keenwrite.processors.markdown.extensions;
2
3
import com.keenwrite.events.ParseHeadingEvent;
4
import com.keenwrite.processors.Processor;
5
import com.vladsch.flexmark.ast.Heading;
6
import com.vladsch.flexmark.parser.Parser.Builder;
7
import com.vladsch.flexmark.parser.Parser.ParserExtension;
8
import com.vladsch.flexmark.parser.block.NodePostProcessor;
9
import com.vladsch.flexmark.parser.block.NodePostProcessorFactory;
10
import com.vladsch.flexmark.util.ast.Document;
11
import com.vladsch.flexmark.util.ast.Node;
12
import com.vladsch.flexmark.util.ast.NodeTracker;
13
import com.vladsch.flexmark.util.data.MutableDataHolder;
14
import org.jetbrains.annotations.NotNull;
15
16
import java.util.regex.Pattern;
17
18
import static com.keenwrite.events.ParseHeadingEvent.fireNewOutlineEvent;
19
20
public final class DocumentOutlineExtension implements ParserExtension {
21
  private static final Pattern sRegex = Pattern.compile( "^(#+)" );
22
23
  private final Processor<String> mProcessor;
24
25
  private DocumentOutlineExtension( final Processor<String> processor ) {
26
    mProcessor = processor;
27
  }
28
29
  @Override
30
  public void parserOptions( final MutableDataHolder options ) {}
31
32
  @Override
33
  public void extend( final Builder builder ) {
34
    builder.postProcessorFactory( new Factory() );
35
  }
36
37
  public static DocumentOutlineExtension create(
38
    final Processor<String> processor ) {
39
    return new DocumentOutlineExtension( processor );
40
  }
41
42
  private class HeadingNodePostProcessor extends NodePostProcessor {
43
    @Override
44
    public void process(
45
      @NotNull final NodeTracker state, @NotNull final Node node ) {
46
      final var heading = mProcessor.apply( node.getChars().toString() );
47
      final var matcher = sRegex.matcher( heading );
48
49
      if( matcher.find() ) {
50
        final var level = matcher.group().length();
51
        final var text = heading.substring( level );
52
        final var offset = node.getStartOffset();
53
        ParseHeadingEvent.fire( level, text, offset );
54
      }
55
    }
56
  }
57
58
  public class Factory extends NodePostProcessorFactory {
59
    public Factory() {
60
      super( false );
61
      addNodes( Heading.class );
62
    }
63
64
    @NotNull
65
    @Override
66
    public NodePostProcessor apply( @NotNull final Document document ) {
67
      fireNewOutlineEvent();
68
      return new HeadingNodePostProcessor();
69
    }
70
  }
71
}
721
D 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
}
271
D 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
}
221
D src/main/java/com/keenwrite/processors/markdown/extensions/ImageLinkExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.processors.ProcessorContext;
6
import com.vladsch.flexmark.ast.Image;
7
import com.vladsch.flexmark.html.IndependentLinkResolverFactory;
8
import com.vladsch.flexmark.html.LinkResolver;
9
import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
10
import com.vladsch.flexmark.html.renderer.ResolvedLink;
11
import com.vladsch.flexmark.util.ast.Node;
12
import org.jetbrains.annotations.NotNull;
13
14
import java.io.File;
15
import java.nio.file.Path;
16
import java.util.Optional;
17
18
import static com.keenwrite.events.StatusEvent.clue;
19
import static com.keenwrite.io.SysFile.toFile;
20
import static com.keenwrite.util.ProtocolScheme.getProtocol;
21
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
22
import static com.vladsch.flexmark.html.renderer.LinkStatus.VALID;
23
24
/**
25
 * Responsible for ensuring that images can be rendered relative to a path.
26
 * This allows images to be located virtually anywhere.
27
 */
28
public class ImageLinkExtension extends HtmlRendererAdapter {
29
30
  private final ProcessorContext mContext;
31
32
  private ImageLinkExtension( @NotNull final ProcessorContext context ) {
33
    mContext = context;
34
  }
35
36
  /**
37
   * Creates an extension capable of using a relative path to embed images.
38
   *
39
   * @param context Contains the base directory to search in for images.
40
   * @return The new {@link ImageLinkExtension}, not {@code null}.
41
   */
42
  public static ImageLinkExtension create(
43
    @NotNull final ProcessorContext context ) {
44
    return new ImageLinkExtension( context );
45
  }
46
47
  @Override
48
  public void extend( @NotNull final Builder builder,
49
                      @NotNull final String rendererType ) {
50
    builder.linkResolverFactory( new ResolverFactory() );
51
  }
52
53
  private final class ResolverFactory extends IndependentLinkResolverFactory {
54
    @Override
55
    public @NotNull LinkResolver apply(
56
      @NotNull final LinkResolverBasicContext context ) {
57
      return new ImageLinkResolver();
58
    }
59
  }
60
61
  private class ImageLinkResolver implements LinkResolver {
62
    public ImageLinkResolver() {
63
    }
64
65
    @NotNull
66
    @Override
67
    public ResolvedLink resolveLink(
68
      @NotNull final Node node,
69
      @NotNull final LinkResolverBasicContext context,
70
      @NotNull final ResolvedLink link ) {
71
      return node instanceof Image ? forImage( link, node ) : link;
72
    }
73
74
    /**
75
     * Algorithm:
76
     * <ol>
77
     *   <li>Accept remote URLs as valid links.</li>
78
     *   <li>Accept existing readable files as valid links.</li>
79
     *   <li>Accept non-{@link ExportFormat#NONE} exports as valid links.</li>
80
     *   <li>Append the images dir to the edited file's dir (baseDir).</li>
81
     *   <li>Search for images by extension.</li>
82
     * </ol>
83
     *
84
     * @param link The link URL to resolve.
85
     * @param node The document node containing the URL.
86
     * @return The {@link ResolvedLink} instance used to render the link.
87
     */
88
    private ResolvedLink forImage( final ResolvedLink link, final Node node ) {
89
      final var url = link.getUrl();
90
      final var protocolScheme = getProtocol( url );
91
92
      return protocolScheme.isRemote()
93
        ? valid( link, url )
94
        : resolveImageFile( link, node, url );
95
    }
96
97
    private ResolvedLink resolveImageFile(
98
      final ResolvedLink link,
99
      final Node node,
100
      final String url ) {
101
      final var userPath = new File( url );
102
103
      // If the user specified a fully qualified path name, use it verbatim.
104
      return readable( userPath )
105
        ? valid( link, url )
106
        : resolveUnqualifiedImageFile( link, node, url );
107
    }
108
109
    private ResolvedLink resolveUnqualifiedImageFile(
110
      final ResolvedLink link,
111
      final Node node,
112
      final String url ) {
113
      final var baseDir = getBaseDir();
114
      final var fqfn = baseDir.resolve( Path.of( url ) );
115
116
      // If the image can be found relative to the base directory, then
117
      // use the link as is when resolving the path.
118
      return readable( toFile( fqfn ) )
119
        ? valid( link, url )
120
        : resolveExtensionlessImageFile( link, node, url );
121
    }
122
123
    private ResolvedLink resolveExtensionlessImageFile(
124
      final ResolvedLink link,
125
      final Node node,
126
      final String url
127
    ) {
128
      final var imagePath = new File( url );
129
      final var file = resolveImageExtension( imagePath );
130
131
      return file.isPresent() && readable( file.get() )
132
        ? valid( link, file.get().toString() )
133
        : resolveRelativeImageFile( link, node, url );
134
    }
135
136
    private ResolvedLink resolveRelativeImageFile(
137
      final ResolvedLink link,
138
      final Node node,
139
      final String url ) {
140
      final var baseDir = getBaseDir();
141
142
      try {
143
        // Compute the path to the image file. The base directory should
144
        // be an absolute path to the file being edited, without an extension.
145
        final var imagesDir = getImageDir();
146
        final var baseImagesDir = baseDir.resolve( imagesDir );
147
        final var imagePath = baseImagesDir.resolve( url );
148
        final var file = resolveImageExtension( toFile( imagePath ) );
149
150
        if( file.isPresent() ) {
151
          final var resolved = imagesDir.resolve( file.get().toPath() );
152
          final var relative = baseDir.relativize( resolved );
153
          return valid( link, relative.toString() );
154
        }
155
156
        clue( "Main.status.error.file.missing.near",
157
              imagePath + ".*", node.getLineNumber()
158
        );
159
      } catch( final Exception ex ) {
160
        clue( ex );
161
      }
162
163
      return link;
164
    }
165
166
    private Optional<File> resolveImageExtension( final File imagePath ) {
167
      for( final var ext : getImageOrder() ) {
168
        final var file = new File( imagePath.toString() + '.' + ext );
169
170
        if( readable( file ) ) {
171
          return Optional.of( file );
172
        }
173
      }
174
175
      return Optional.empty();
176
    }
177
178
    private ResolvedLink valid( final ResolvedLink link, final String url ) {
179
      return link.withStatus( VALID ).withUrl( url );
180
    }
181
182
    private Path getImageDir() {
183
      return mContext.getImageDir();
184
    }
185
186
    private Iterable<String> getImageOrder() {
187
      return mContext.getImageOrder();
188
    }
189
190
    private Path getBaseDir() {
191
      return mContext.getBaseDir();
192
    }
193
  }
194
195
  private static boolean readable( final File file ) {
196
    return file.isFile() && file.canRead();
197
  }
198
}
1991
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionBlock.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.vladsch.flexmark.html.HtmlWriter;
8
import com.vladsch.flexmark.parser.InlineParser;
9
import com.vladsch.flexmark.util.ast.Block;
10
import com.vladsch.flexmark.util.sequence.BasedSequence;
11
import org.jetbrains.annotations.NotNull;
12
13
/**
14
 * Responsible for retaining the text node and all child nodes with respect
15
 * to a caption. The caption can be associated with most items, such as
16
 * block quotes, tables, math expressions, and images.
17
 */
18
class CaptionBlock extends Block {
19
  private final BasedSequence mCaption;
20
21
  CaptionBlock( final BasedSequence caption ) {
22
    assert caption != null;
23
24
    mCaption = caption;
25
  }
26
27
  /**
28
   * Opens the caption.
29
   *
30
   * @param writer Where to write the opening tags.
31
   */
32
  void opening( final HtmlWriter writer ) {
33
    writer.raw( "<span class=\"caption\">" );
34
  }
35
36
  /**
37
   * Closes the caption.
38
   *
39
   * @param writer Where to write the closing tags.
40
   */
41
  void closing( final HtmlWriter writer ) {
42
    writer.raw( "</span>" );
43
  }
44
45
  void parse( final InlineParser inlineParser ) {
46
    assert inlineParser != null;
47
48
    inlineParser.parse( mCaption, this );
49
  }
50
51
  @NotNull
52
  @Override
53
  public BasedSequence[] getSegments() {
54
    return BasedSequence.EMPTY_SEGMENTS;
55
  }
56
}
157
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionBlockParserFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.vladsch.flexmark.parser.block.AbstractBlockParserFactory;
8
import com.vladsch.flexmark.parser.block.BlockStart;
9
import com.vladsch.flexmark.parser.block.MatchedBlockParser;
10
import com.vladsch.flexmark.parser.block.ParserState;
11
import com.vladsch.flexmark.util.data.DataHolder;
12
13
class CaptionBlockParserFactory extends AbstractBlockParserFactory {
14
  CaptionBlockParserFactory( final DataHolder options ) {
15
    super( options );
16
  }
17
18
  @Override
19
  public BlockStart tryStart(
20
    final ParserState state,
21
    final MatchedBlockParser matchedBlockParser ) {
22
23
    final var flush = state.getIndent() == 0;
24
    final var index = state.getNextNonSpaceIndex();
25
    final var line = state.getLine();
26
    final var length = line.length();
27
    final var text = line.subSequence( index, length );
28
29
    return flush && CaptionParser.canParse( text )
30
      ? BlockStart.of( new CaptionParser( text ) ).atIndex( length )
31
      : BlockStart.none();
32
  }
33
}
134
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionCustomBlockParserFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownCustomBlockParserFactory;
8
import com.vladsch.flexmark.parser.block.BlockParserFactory;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
11
class CaptionCustomBlockParserFactory extends MarkdownCustomBlockParserFactory {
12
  CaptionCustomBlockParserFactory() {}
13
14
  @Override
15
  public BlockParserFactory createBlockParserFactory(
16
    final DataHolder options ) {
17
    return new CaptionBlockParserFactory( options );
18
  }
19
}
120
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionExtension.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownRendererExtension;
8
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
9
import com.vladsch.flexmark.parser.Parser.Builder;
10
11
/**
12
 * Responsible for parsing and rendering {@link CaptionBlock} instances.
13
 */
14
public final class CaptionExtension extends MarkdownRendererExtension {
15
  /**
16
   * Use {@link #create()}.
17
   */
18
  private CaptionExtension() {}
19
20
  /**
21
   * Returns a new {@link CaptionExtension}.
22
   *
23
   * @return An extension capable of parsing caption syntax.
24
   */
25
  public static CaptionExtension create() {
26
    return new CaptionExtension();
27
  }
28
29
  @Override
30
  public void extend( final Builder builder ) {
31
    builder.customBlockParserFactory( new CaptionCustomBlockParserFactory() );
32
    builder.postProcessorFactory( new CaptionPostProcessorFactory() );
33
  }
34
35
  @Override
36
  protected NodeRendererFactory createNodeRendererFactory() {
37
    return new CaptionNodeRendererFactory();
38
  }
39
}
140
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionNodeRenderer.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.keenwrite.processors.markdown.extensions.references.CrossReferenceNode;
8
import com.vladsch.flexmark.html.HtmlWriter;
9
import com.vladsch.flexmark.html.renderer.CoreNodeRenderer;
10
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
11
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
12
import com.vladsch.flexmark.util.ast.Node;
13
import com.vladsch.flexmark.util.data.DataHolder;
14
15
import java.util.HashSet;
16
import java.util.LinkedList;
17
import java.util.List;
18
import java.util.Set;
19
20
/**
21
 * Responsible for rendering {@link CaptionBlock} instances as HTML (via
22
 * delegation).
23
 */
24
class CaptionNodeRenderer extends CoreNodeRenderer {
25
  CaptionNodeRenderer( final DataHolder options ) {
26
    super( options );
27
  }
28
29
  @Override
30
  public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
31
    return new HashSet<>( List.of(
32
      new NodeRenderingHandler<>( CaptionBlock.class, this::render )
33
    ) );
34
  }
35
36
  private void render(
37
    final CaptionBlock node,
38
    final NodeRendererContext context,
39
    final HtmlWriter html ) {
40
    final var anchors = new LinkedList<Node>();
41
42
    html.raw( "<p>" );
43
    node.opening( html );
44
45
    if( node.hasChildren() ) {
46
      for( final var child : node.getChildren() ) {
47
        if( !child.isOrDescendantOfType( CrossReferenceNode.class ) ) {
48
          context.render( child );
49
        }
50
        else {
51
          anchors.add( child );
52
        }
53
      }
54
    }
55
56
    node.closing( html );
57
58
    for( final var anchor : anchors ) {
59
      context.render( anchor );
60
    }
61
62
    html.raw( "</p>" );
63
  }
64
}
165
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionNodeRendererFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownNodeRendererFactory;
8
import com.vladsch.flexmark.html.renderer.NodeRenderer;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
11
class CaptionNodeRendererFactory extends MarkdownNodeRendererFactory {
12
  @Override
13
  protected NodeRenderer createNodeRenderer( final DataHolder options ) {
14
    return new CaptionNodeRenderer( options );
15
  }
16
}
117
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionParser.java
1
package com.keenwrite.processors.markdown.extensions.captions;
2
3
import com.vladsch.flexmark.parser.InlineParser;
4
import com.vladsch.flexmark.parser.block.AbstractBlockParser;
5
import com.vladsch.flexmark.parser.block.BlockContinue;
6
import com.vladsch.flexmark.parser.block.ParserState;
7
import com.vladsch.flexmark.util.ast.Block;
8
import com.vladsch.flexmark.util.sequence.BasedSequence;
9
10
class CaptionParser extends AbstractBlockParser {
11
  private final CaptionBlock mBlock;
12
13
  CaptionParser( final BasedSequence text ) {
14
    assert text != null;
15
    assert text.isNotEmpty();
16
    assert text.length() > 2;
17
18
    final var caption = text.subSequence( 2 );
19
20
    mBlock = new CaptionBlock( caption.trim() );
21
  }
22
23
  static boolean canParse( final BasedSequence text ) {
24
    return text.length() > 3 &&
25
           text.charAt( 0 ) == ':' &&
26
           text.charAt( 1 ) == ':' &&
27
           text.charAt( 2 ) != ':';
28
  }
29
30
  @Override
31
  public Block getBlock() {
32
    return mBlock;
33
  }
34
35
  @Override
36
  public BlockContinue tryContinue( final ParserState state ) {
37
    return BlockContinue.none();
38
  }
39
40
  @Override
41
  public void parseInlines( final InlineParser inlineParser ) {
42
    assert inlineParser != null;
43
44
    mBlock.parse( inlineParser );
45
  }
46
47
  @Override
48
  public void closeBlock( final ParserState state ) {}
49
}
150
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionPostProcessor.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.keenwrite.processors.markdown.extensions.fences.ClosingDivBlock;
8
import com.keenwrite.processors.markdown.extensions.fences.OpeningDivBlock;
9
import com.vladsch.flexmark.parser.block.NodePostProcessor;
10
import com.vladsch.flexmark.util.ast.Node;
11
import com.vladsch.flexmark.util.ast.NodeTracker;
12
import org.jetbrains.annotations.NotNull;
13
14
/**
15
 * Captions are written most naturally <em>after</em>> the element that they
16
 * apply to, regardless of whether they are figures, tables, code listings,
17
 * algorithms, or equations. The typesetting software uses event-based parsing
18
 * of XML elements, meaning the DOM isn't fully loaded into memory. This means
19
 * that captions must come <em>before</em> the item being captioned.
20
 * <p>
21
 * To reconcile this UX conundrum, we swap captions with the previous node.
22
 */
23
class CaptionPostProcessor extends NodePostProcessor {
24
  @Override
25
  public void process(
26
    @NotNull final NodeTracker state,
27
    @NotNull final Node caption ) {
28
29
    var previous = caption.getPrevious();
30
31
    if( previous != null ) {
32
      swap( previous, caption );
33
    }
34
  }
35
36
  private void swap( final Node previous, final Node caption ) {
37
    assert previous != null;
38
    assert caption != null;
39
40
    var swap = previous;
41
    boolean found = true;
42
43
    if( swap.isOrDescendantOfType( ClosingDivBlock.class ) ) {
44
      found = false;
45
46
      while( !found && swap != null ) {
47
        if( swap.isOrDescendantOfType( OpeningDivBlock.class ) ) {
48
          found = true;
49
        }
50
        else {
51
          swap = swap.getPrevious();
52
        }
53
      }
54
    }
55
56
    if( found ) {
57
      swap.insertBefore( caption );
58
    }
59
  }
60
}
161
A src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionPostProcessorFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.captions;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownPostProcessorFactory;
8
import com.vladsch.flexmark.parser.block.NodePostProcessor;
9
import com.vladsch.flexmark.util.ast.Document;
10
11
class CaptionPostProcessorFactory extends MarkdownPostProcessorFactory {
12
  CaptionPostProcessorFactory() {
13
    // The argument isn't used by the Markdown parsing library.
14
    super( false );
15
16
    addNodes( CaptionBlock.class );
17
  }
18
19
  @Override
20
  protected NodePostProcessor createPostProcessor( final Document document ) {
21
    return new CaptionPostProcessor();
22
  }
23
}
124
A src/main/java/com/keenwrite/processors/markdown/extensions/caret/CaretExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.caret;
3
4
import com.keenwrite.editors.common.Caret;
5
import com.keenwrite.processors.ProcessorContext;
6
import com.keenwrite.processors.markdown.extensions.common.HtmlRendererAdapter;
7
import org.jetbrains.annotations.NotNull;
8
9
import java.util.function.Supplier;
10
11
import static com.keenwrite.processors.markdown.extensions.caret.IdAttributeProvider.createFactory;
12
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
13
14
/**
15
 * Responsible for giving most block-level elements a unique identifier
16
 * attribute. The identifier is used to coordinate scrolling.
17
 */
18
public class CaretExtension extends HtmlRendererAdapter {
19
20
  private final Supplier<Caret> mCaret;
21
22
  private CaretExtension( final ProcessorContext context ) {
23
    mCaret = context.getCaret();
24
  }
25
26
  public static CaretExtension create( final ProcessorContext context ) {
27
    return new CaretExtension( context );
28
  }
29
30
  @Override
31
  public void extend(
32
    @NotNull final Builder builder,
33
    @NotNull final String rendererType ) {
34
    builder.attributeProviderFactory( createFactory( mCaret ) );
35
  }
36
}
137
A src/main/java/com/keenwrite/processors/markdown/extensions/caret/IdAttributeProvider.java
1
package com.keenwrite.processors.markdown.extensions.caret;
2
3
import com.keenwrite.constants.Constants;
4
import com.keenwrite.editors.common.Caret;
5
import com.vladsch.flexmark.ext.tables.TableBlock;
6
import com.vladsch.flexmark.html.AttributeProvider;
7
import com.vladsch.flexmark.html.AttributeProviderFactory;
8
import com.vladsch.flexmark.html.IndependentAttributeProviderFactory;
9
import com.vladsch.flexmark.html.renderer.AttributablePart;
10
import com.vladsch.flexmark.html.renderer.LinkResolverContext;
11
import com.vladsch.flexmark.util.ast.Node;
12
import com.vladsch.flexmark.util.html.AttributeImpl;
13
import com.vladsch.flexmark.util.html.MutableAttributes;
14
import org.jetbrains.annotations.NotNull;
15
16
import java.util.function.Supplier;
17
18
import static com.keenwrite.constants.Constants.CARET_ID;
19
import static com.keenwrite.processors.markdown.extensions.common.EmptyNode.EMPTY_NODE;
20
21
/**
22
 * Responsible for creating the id attribute. This class is instantiated
23
 * once: for the HTML element containing the {@link Constants#CARET_ID}.
24
 */
25
final class IdAttributeProvider implements AttributeProvider {
26
  private final Supplier<Caret> mCaret;
27
  private boolean mAdded;
28
29
  public IdAttributeProvider( final Supplier<Caret> caret ) {
30
    mCaret = caret;
31
  }
32
33
  static AttributeProviderFactory createFactory(
34
    final Supplier<Caret> caret ) {
35
    return new IndependentAttributeProviderFactory() {
36
      @Override
37
      public @NotNull AttributeProvider apply(
38
        @NotNull final LinkResolverContext context ) {
39
        return new IdAttributeProvider( caret );
40
      }
41
    };
42
  }
43
44
  @Override
45
  public void setAttributes(
46
    @NotNull final Node curr,
47
    @NotNull final AttributablePart part,
48
    @NotNull final MutableAttributes attributes ) {
49
    // Optimization: if a caret is inserted, don't try to find another.
50
    if( mAdded ) {
51
      return;
52
    }
53
54
    final var caret = mCaret.get();
55
56
    // If a table block has been earmarked with an empty node, it means
57
    // another extension has generated code from an external source. The
58
    // Markdown processor won't be able to determine the caret position
59
    // with any semblance of accuracy, so skip the element. This usually
60
    // happens with tables, but in theory any Markdown generated from an
61
    // external source (e.g., an R script) could produce text that has no
62
    // caret position that can be calculated.
63
    var table = curr;
64
65
    if( !(curr instanceof TableBlock) ) {
66
      table = curr.getAncestorOfType( TableBlock.class );
67
    }
68
69
    // The table was generated outside the document
70
    if( table != null && table.getLastChild() == EMPTY_NODE ) {
71
      return;
72
    }
73
74
    final var outside = caret.isAfterText() ? 1 : 0;
75
    final var began = curr.getStartOffset();
76
    final var ended = curr.getEndOffset() + outside;
77
    final var prev = curr.getPrevious();
78
79
    // If the caret is within the bounds of the current node or the
80
    // caret is within the bounds of the end of the previous node and
81
    // the start of the current node, then mark the current node with
82
    // a caret indicator.
83
    if( caret.isBetweenText( began, ended ) ||
84
        prev != null && caret.isBetweenText( prev.getEndOffset(), began ) ) {
85
      // This line empowers synchronizing the text editor with the preview.
86
      attributes.addValue( AttributeImpl.of( "id", CARET_ID ) );
87
88
      // We're done until the user moves the caret (micro-optimization)
89
      mAdded = true;
90
    }
91
  }
92
}
193
A src/main/java/com/keenwrite/processors/markdown/extensions/common/EmptyNode.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.common;
3
4
import com.vladsch.flexmark.util.ast.Node;
5
import com.vladsch.flexmark.util.sequence.BasedSequence;
6
import org.jetbrains.annotations.NotNull;
7
8
/**
9
 * The singleton is injected into the abstract syntax tree to mark an instance
10
 * of {@link Node} such that it must not be processed normally. Using a wrapper
11
 * for a given {@link Node} cannot work because the class type is used by
12
 * the parsing library for processing.
13
 */
14
public final class EmptyNode extends Node {
15
  public static final Node EMPTY_NODE = new EmptyNode();
16
17
  /**
18
   * Use {@link #EMPTY_NODE}.
19
   */
20
  private EmptyNode() {}
21
22
  @NotNull
23
  @Override
24
  public BasedSequence[] getSegments() {
25
    return BasedSequence.EMPTY_SEGMENTS;
26
  }
27
}
128
A src/main/java/com/keenwrite/processors/markdown/extensions/common/HtmlRendererAdapter.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.common;
3
4
import com.vladsch.flexmark.util.data.MutableDataHolder;
5
import org.jetbrains.annotations.NotNull;
6
7
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
8
9
/**
10
 * Hides the {@link #rendererOptions(MutableDataHolder)} from subclasses
11
 * that would otherwise implement the {@link HtmlRendererExtension} interface.
12
 */
13
public abstract class HtmlRendererAdapter implements HtmlRendererExtension {
14
  /**
15
   * Empty, unused.
16
   *
17
   * @param options Ignored.
18
   */
19
  @Override
20
  public void rendererOptions( @NotNull final MutableDataHolder options ) { }
21
}
122
A src/main/java/com/keenwrite/processors/markdown/extensions/common/MarkdownCustomBlockParserFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.common;
6
7
import com.vladsch.flexmark.parser.block.BlockParserFactory;
8
import com.vladsch.flexmark.parser.block.CustomBlockParserFactory;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
import org.jetbrains.annotations.NotNull;
11
import org.jetbrains.annotations.Nullable;
12
13
import java.util.Set;
14
15
public abstract class MarkdownCustomBlockParserFactory
16
  implements CustomBlockParserFactory {
17
  /**
18
   * Subclasses must return a new {@link BlockParserFactory} instance.
19
   *
20
   * @param options Passed into the new instance constructor.
21
   * @return The new {@link BlockParserFactory} instance.
22
   */
23
  protected abstract BlockParserFactory createBlockParserFactory(
24
    DataHolder options );
25
26
  @NotNull
27
  @Override
28
  public BlockParserFactory apply( @NotNull final DataHolder options ) {
29
    return createBlockParserFactory( options );
30
  }
31
32
  @Override
33
  public @Nullable Set<Class<?>> getAfterDependents() {
34
    return null;
35
  }
36
37
  @Override
38
  public @Nullable Set<Class<?>> getBeforeDependents() {
39
    return null;
40
  }
41
42
  @Override
43
  public boolean affectsGlobalScope() {
44
    return false;
45
  }
46
}
147
A src/main/java/com/keenwrite/processors/markdown/extensions/common/MarkdownNodeRendererFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.common;
6
7
import com.vladsch.flexmark.html.renderer.NodeRenderer;
8
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
import org.jetbrains.annotations.NotNull;
11
12
public abstract class MarkdownNodeRendererFactory
13
  implements NodeRendererFactory {
14
  @NotNull
15
  @Override
16
  public NodeRenderer apply( @NotNull final DataHolder options ) {
17
    return createNodeRenderer( options );
18
  }
19
20
  protected abstract NodeRenderer createNodeRenderer( DataHolder options );
21
}
122
A src/main/java/com/keenwrite/processors/markdown/extensions/common/MarkdownParserExtension.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.common;
6
7
import com.vladsch.flexmark.parser.Parser.ParserExtension;
8
import com.vladsch.flexmark.util.data.MutableDataHolder;
9
10
/**
11
 * Provides a default {@link #parserOptions(MutableDataHolder)} implementation.
12
 */
13
public interface MarkdownParserExtension extends ParserExtension {
14
  @Override
15
  default void parserOptions( final MutableDataHolder options ) {}
16
}
117
A src/main/java/com/keenwrite/processors/markdown/extensions/common/MarkdownPostProcessorFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.common;
6
7
import com.vladsch.flexmark.parser.block.NodePostProcessor;
8
import com.vladsch.flexmark.parser.block.NodePostProcessorFactory;
9
import com.vladsch.flexmark.util.ast.Document;
10
import org.jetbrains.annotations.NotNull;
11
12
public abstract class MarkdownPostProcessorFactory
13
  extends NodePostProcessorFactory {
14
  public MarkdownPostProcessorFactory( final boolean ignored ) {
15
    super( ignored );
16
  }
17
18
  @NotNull
19
  @Override
20
  public NodePostProcessor apply( @NotNull Document document ) {
21
    return createPostProcessor( document );
22
  }
23
24
  protected abstract NodePostProcessor createPostProcessor( Document document );
25
}
126
A src/main/java/com/keenwrite/processors/markdown/extensions/common/MarkdownRendererExtension.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.common;
6
7
import com.vladsch.flexmark.html.HtmlRenderer.Builder;
8
import com.vladsch.flexmark.html.renderer.NodeRenderer;
9
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
10
import org.jetbrains.annotations.NotNull;
11
12
public abstract class MarkdownRendererExtension extends HtmlRendererAdapter
13
  implements MarkdownParserExtension {
14
15
  /**
16
   * Implemented by subclasses to create the {@link NodeRendererFactory} capable
17
   * of converting nodes created by an extension into HTML elements.
18
   *
19
   * @return The {@link NodeRendererFactory} for producing {@link NodeRenderer}
20
   * instances.
21
   */
22
  protected abstract NodeRendererFactory createNodeRendererFactory();
23
24
  /**
25
   * Adds an extension for HTML document export types.
26
   *
27
   * @param builder      The document builder.
28
   * @param rendererType Indicates the document type to be built.
29
   */
30
  @Override
31
  public void extend(
32
    @NotNull final Builder builder,
33
    @NotNull final String rendererType ) {
34
    if( "HTML".equalsIgnoreCase( rendererType ) ) {
35
      builder.nodeRendererFactory( createNodeRendererFactory() );
36
    }
37
  }
38
}
139
M src/main/java/com/keenwrite/processors/markdown/extensions/fences/ClosingDivBlock.java
55
66
/**
7
 * Responsible for helping to generate a closing div element.
7
 * Responsible for helping to generate a closing {@code div} element.
88
 */
9
class ClosingDivBlock extends DivBlock {
9
public final class ClosingDivBlock extends DivBlock {
1010
  @Override
11
  void export( final HtmlWriter html ) {
11
  void write( final HtmlWriter html ) {
1212
    html.closeTag( HTML_DIV );
1313
  }
M src/main/java/com/keenwrite/processors/markdown/extensions/fences/DivBlock.java
2121
   * @param html Builds the HTML document to be written.
2222
   */
23
  abstract void export( HtmlWriter html );
23
  abstract void write( HtmlWriter html );
2424
}
2525
M src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedBlockExtension.java
77
import com.keenwrite.processors.VariableProcessor;
88
import com.keenwrite.processors.markdown.MarkdownProcessor;
9
import com.keenwrite.processors.markdown.extensions.HtmlRendererAdapter;
9
import com.keenwrite.processors.markdown.extensions.common.HtmlRendererAdapter;
1010
import com.keenwrite.processors.r.RChunkEvaluator;
1111
import com.keenwrite.processors.r.RVariableProcessor;
...
2525
2626
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
27
import static com.keenwrite.constants.Constants.TEMPORARY_DIRECTORY;
2728
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
2829
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
...
3637
 */
3738
public final class FencedBlockExtension extends HtmlRendererAdapter {
38
  private static final String TEMP_DIR = System.getProperty( "java.io.tmpdir" );
39
4039
  /**
4140
   * Ensure that the device is always closed to prevent an out-of-resources
...
202201
203202
      // The URI helps convert backslashes to forward slashes.
204
      final var uri = Path.of( TEMP_DIR, filename ).toUri();
203
      final var uri = Path.of( TEMPORARY_DIRECTORY, filename ).toUri();
205204
      final var svg = uri.getPath();
206205
      final var link = context.resolveLink( LINK, svg, false );
M src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivExtension.java
22
package com.keenwrite.processors.markdown.extensions.fences;
33
4
import com.keenwrite.processors.markdown.extensions.HtmlRendererAdapter;
5
import com.vladsch.flexmark.html.HtmlRenderer;
6
import com.vladsch.flexmark.parser.Parser;
4
import com.keenwrite.processors.markdown.extensions.common.MarkdownCustomBlockParserFactory;
5
import com.keenwrite.processors.markdown.extensions.common.MarkdownRendererExtension;
6
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
7
import com.vladsch.flexmark.parser.Parser.Builder;
78
import com.vladsch.flexmark.parser.block.*;
89
import com.vladsch.flexmark.util.ast.Block;
910
import com.vladsch.flexmark.util.data.DataHolder;
10
import com.vladsch.flexmark.util.data.MutableDataHolder;
1111
import com.vladsch.flexmark.util.html.Attribute;
1212
import com.vladsch.flexmark.util.html.AttributeImpl;
13
import org.jetbrains.annotations.NotNull;
14
import org.jetbrains.annotations.Nullable;
1513
1614
import java.util.ArrayList;
17
import java.util.Set;
1815
import java.util.regex.Pattern;
1916
20
import static com.vladsch.flexmark.parser.Parser.ParserExtension;
17
import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS;
18
import static java.util.regex.Pattern.compile;
2119
2220
/**
...
5351
 * </p>
5452
 */
55
public class FencedDivExtension extends HtmlRendererAdapter
56
  implements ParserExtension {
53
public class FencedDivExtension extends MarkdownRendererExtension {
5754
  /**
5855
   * Matches any number of colons at start of line. This will match both the
5956
   * opening and closing fences, with any number of colons.
6057
   */
61
  private static final Pattern FENCE = Pattern.compile( "^:::.*" );
58
  private static final Pattern FENCE = compile( "^:::.*" );
6259
6360
  /**
6461
   * After a fenced div is detected, this will match the opening fence.
6562
   */
66
  private static final Pattern FENCE_OPENING = Pattern.compile(
67
    "^:::+\\s+([\\p{IsAlphabetic}\\p{IsDigit}-_]+|\\{.+})\\s*$" );
63
  private static final Pattern FENCE_OPENING = compile(
64
    "^:::+\\s+([\\p{Alnum}-_]+|\\{.+})\\s*$",
65
    UNICODE_CHARACTER_CLASS );
6866
6967
  /**
7068
   * Matches whether extended syntax is being used.
7169
   */
72
  private static final Pattern ATTR_CSS = Pattern.compile( "\\{(.+)}" );
70
  private static final Pattern ATTR_CSS = compile( "\\{(.+)}" );
7371
7472
  /**
7573
   * Matches either individual CSS definitions (id/class, {@code <d>}) or
7674
   * key/value pairs ({@code <k>} and {@link <v>}). The key/value pair
7775
   * will match optional quotes.
7876
   */
79
  private static final Pattern ATTR_PAIRS = Pattern.compile(
77
  private static final Pattern ATTR_PAIRS = compile(
8078
    "\\s*" +
81
      "(?<d>[#.][\\p{IsAlphabetic}\\p{IsDigit}-_]+[^\\s=])|" +
82
      "((?<k>[\\p{IsAlphabetic}\\p{IsDigit}-_]+)=" +
83
      "\"*(?<v>(?<=\")[^\"]+(?=\")|([^\\s]+))\"*)" );
79
    "(?<d>[#.][\\p{Alnum}-_]+[^\\s=])|" +
80
    "((?<k>[\\p{Alnum}-_]+)=" +
81
    "\"*(?<v>(?<=\")[^\"]+(?=\")|(\\S+))\"*)",
82
    UNICODE_CHARACTER_CLASS );
8483
8584
  public static FencedDivExtension create() {
8685
    return new FencedDivExtension();
87
  }
88
89
  @Override
90
  public void parserOptions( final MutableDataHolder options ) {
9186
  }
9287
9388
  @Override
94
  public void extend( final Parser.Builder builder ) {
95
    builder.customBlockParserFactory( new Factory() );
89
  public void extend( final Builder builder ) {
90
    builder.customBlockParserFactory( new DivBlockParserFactory() );
9691
  }
9792
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
   */
10493
  @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
    }
94
  protected NodeRendererFactory createNodeRendererFactory() {
95
    return new FencedDivNodeRendererFactory();
11096
  }
11197
11298
  /**
11399
   * Responsible for creating an instance of {@link ParserFactory}.
114100
   */
115
  private static class Factory implements CustomBlockParserFactory {
101
  private static class DivBlockParserFactory
102
    extends MarkdownCustomBlockParserFactory {
116103
    @Override
117
    public @NotNull BlockParserFactory apply(
118
      @NotNull final DataHolder options ) {
104
    public BlockParserFactory createBlockParserFactory( final DataHolder options ) {
119105
      return new ParserFactory( options );
120106
    }
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; }
130107
  }
131108
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivNodeRendererFactory.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.common.MarkdownNodeRendererFactory;
5
import com.vladsch.flexmark.html.renderer.NodeRenderer;
6
import com.vladsch.flexmark.util.data.DataHolder;
7
8
class FencedDivNodeRendererFactory extends MarkdownNodeRendererFactory {
9
  @Override
10
  protected NodeRenderer createNodeRenderer( final DataHolder options ) {
11
    return new FencedDivRenderer();
12
  }
13
}
114
M src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivRenderer.java
55
import com.vladsch.flexmark.html.renderer.NodeRenderer;
66
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
7
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
87
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
import org.jetbrains.annotations.NotNull;
118
import org.jetbrains.annotations.Nullable;
129
1310
import java.util.Set;
1411
1512
/**
16
 * Responsible for rendering opening and closing fenced div blocks as HTMl
17
 * div elements.
13
 * Responsible for rendering opening and closing fenced div blocks as HTML
14
 * {@code div} elements.
1815
 */
1916
class FencedDivRenderer implements NodeRenderer {
17
  @Nullable
2018
  @Override
21
  public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
19
  public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
2220
    return Set.of(
2321
      new NodeRenderingHandler<>( OpeningDivBlock.class, this::render ),
2422
      new NodeRenderingHandler<>( ClosingDivBlock.class, this::render )
2523
    );
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 );
3524
  }
3625
3726
  /**
38
   * Renders the closing fenced div block as an HTML {@code </div>} element.
27
   * Renders the fenced div block as an HTML {@code <div></div>} element.
3928
   */
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
    }
29
  void render(
30
    final DivBlock node,
31
    final NodeRendererContext context,
32
    final HtmlWriter html ) {
33
    node.write( html );
5134
  }
5235
}
M src/main/java/com/keenwrite/processors/markdown/extensions/fences/OpeningDivBlock.java
99
1010
/**
11
 * Responsible for helping to generate an opening div element.
11
 * Responsible for helping to generate an opening {@code div} element.
1212
 */
13
class OpeningDivBlock extends DivBlock {
13
public final class OpeningDivBlock extends DivBlock {
1414
  private final List<Attribute> mAttributes = new ArrayList<>();
1515
1616
  OpeningDivBlock( final List<Attribute> attributes ) {
1717
    assert attributes != null;
1818
    mAttributes.addAll( attributes );
1919
  }
2020
21
  void export( final HtmlWriter html ) {
21
  @Override
22
  void write( final HtmlWriter html ) {
2223
    mAttributes.forEach( html::attr );
2324
    html.withAttr().tag( HTML_DIV );
A src/main/java/com/keenwrite/processors/markdown/extensions/images/ImageLinkExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.images;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.processors.ProcessorContext;
6
import com.keenwrite.processors.markdown.extensions.common.HtmlRendererAdapter;
7
import com.vladsch.flexmark.ast.Image;
8
import com.vladsch.flexmark.html.IndependentLinkResolverFactory;
9
import com.vladsch.flexmark.html.LinkResolver;
10
import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
11
import com.vladsch.flexmark.html.renderer.ResolvedLink;
12
import com.vladsch.flexmark.util.ast.Node;
13
import org.jetbrains.annotations.NotNull;
14
15
import java.io.File;
16
import java.nio.file.Path;
17
import java.util.Optional;
18
19
import static com.keenwrite.events.StatusEvent.clue;
20
import static com.keenwrite.io.SysFile.toFile;
21
import static com.keenwrite.util.ProtocolScheme.getProtocol;
22
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
23
import static com.vladsch.flexmark.html.renderer.LinkStatus.VALID;
24
25
/**
26
 * Responsible for ensuring that images can be rendered relative to a path.
27
 * This allows images to be located virtually anywhere.
28
 */
29
public class ImageLinkExtension extends HtmlRendererAdapter {
30
31
  private final ProcessorContext mContext;
32
33
  private ImageLinkExtension( @NotNull final ProcessorContext context ) {
34
    mContext = context;
35
  }
36
37
  /**
38
   * Creates an extension capable of using a relative path to embed images.
39
   *
40
   * @param context Contains the base directory to search in for images.
41
   * @return The new {@link ImageLinkExtension}, not {@code null}.
42
   */
43
  public static ImageLinkExtension create(
44
    @NotNull final ProcessorContext context ) {
45
    return new ImageLinkExtension( context );
46
  }
47
48
  @Override
49
  public void extend(
50
    @NotNull final Builder builder,
51
    @NotNull final String rendererType ) {
52
    builder.linkResolverFactory( new ResolverFactory() );
53
  }
54
55
  private final class ResolverFactory extends IndependentLinkResolverFactory {
56
    @NotNull
57
    @Override
58
    public LinkResolver apply(
59
      @NotNull final LinkResolverBasicContext context ) {
60
      return new ImageLinkResolver();
61
    }
62
  }
63
64
  private final class ImageLinkResolver implements LinkResolver {
65
    private ImageLinkResolver() {}
66
67
    @NotNull
68
    @Override
69
    public ResolvedLink resolveLink(
70
      @NotNull final Node node,
71
      @NotNull final LinkResolverBasicContext context,
72
      @NotNull final ResolvedLink link ) {
73
      return node instanceof Image ? forImage( link, node ) : link;
74
    }
75
76
    /**
77
     * Algorithm:
78
     * <ol>
79
     *   <li>Accept remote URLs as valid links.</li>
80
     *   <li>Accept existing readable files as valid links.</li>
81
     *   <li>Accept non-{@link ExportFormat#NONE} exports as valid links.</li>
82
     *   <li>Append the images dir to the edited file's dir (baseDir).</li>
83
     *   <li>Search for images by extension.</li>
84
     * </ol>
85
     *
86
     * @param link The link URL to resolve.
87
     * @param node The document node containing the URL.
88
     * @return The {@link ResolvedLink} instance used to render the link.
89
     */
90
    private ResolvedLink forImage( final ResolvedLink link, final Node node ) {
91
      final var url = link.getUrl();
92
      final var protocolScheme = getProtocol( url );
93
94
      return protocolScheme.isRemote()
95
        ? valid( link, url )
96
        : resolveImageFile( link, node, url );
97
    }
98
99
    private ResolvedLink resolveImageFile(
100
      final ResolvedLink link,
101
      final Node node,
102
      final String url ) {
103
      final var userPath = new File( url );
104
105
      // If the user specified a fully qualified path name, use it verbatim.
106
      return readable( userPath )
107
        ? valid( link, url )
108
        : resolveUnqualifiedImageFile( link, node, url );
109
    }
110
111
    private ResolvedLink resolveUnqualifiedImageFile(
112
      final ResolvedLink link,
113
      final Node node,
114
      final String url ) {
115
      final var baseDir = getBaseDir();
116
      final var fqfn = baseDir.resolve( Path.of( url ) );
117
118
      // If the image can be found relative to the base directory, then
119
      // use the link as is when resolving the path.
120
      return readable( toFile( fqfn ) )
121
        ? valid( link, url )
122
        : resolveExtensionlessImageFile( link, node, url );
123
    }
124
125
    private ResolvedLink resolveExtensionlessImageFile(
126
      final ResolvedLink link,
127
      final Node node,
128
      final String url
129
    ) {
130
      final var imagePath = new File( url );
131
      final var file = resolveImageExtension( imagePath );
132
133
      return file.isPresent() && readable( file.get() )
134
        ? valid( link, file.get().toString() )
135
        : resolveRelativeImageFile( link, node, url );
136
    }
137
138
    private ResolvedLink resolveRelativeImageFile(
139
      final ResolvedLink link,
140
      final Node node,
141
      final String url ) {
142
      final var baseDir = getBaseDir();
143
144
      try {
145
        // Compute the path to the image file. The base directory should
146
        // be an absolute path to the file being edited, without an extension.
147
        final var imagesDir = getImageDir();
148
        final var baseImagesDir = baseDir.resolve( imagesDir );
149
        final var imagePath = baseImagesDir.resolve( url );
150
        final var file = resolveImageExtension( toFile( imagePath ) );
151
152
        if( file.isPresent() ) {
153
          final var resolved = imagesDir.resolve( file.get().toPath() );
154
          final var relative = baseDir.relativize( resolved );
155
          return valid( link, relative.toString() );
156
        }
157
158
        clue( "Main.status.error.file.missing.near",
159
              imagePath + ".*", node.getLineNumber()
160
        );
161
      } catch( final Exception ex ) {
162
        clue( ex );
163
      }
164
165
      return link;
166
    }
167
168
    private Optional<File> resolveImageExtension( final File imagePath ) {
169
      for( final var ext : getImageOrder() ) {
170
        final var file = new File( imagePath.toString() + '.' + ext );
171
172
        if( readable( file ) ) {
173
          return Optional.of( file );
174
        }
175
      }
176
177
      return Optional.empty();
178
    }
179
180
    private ResolvedLink valid( final ResolvedLink link, final String url ) {
181
      return link.withStatus( VALID ).withUrl( url );
182
    }
183
184
    private Path getImageDir() {
185
      return mContext.getImageDir();
186
    }
187
188
    private Iterable<String> getImageOrder() {
189
      return mContext.getImageOrder();
190
    }
191
192
    private Path getBaseDir() {
193
      return mContext.getBaseDir();
194
    }
195
  }
196
197
  private static boolean readable( final File file ) {
198
    return file.isFile() && file.canRead();
199
  }
200
}
1201
A src/main/java/com/keenwrite/processors/markdown/extensions/outline/DocumentOutlineExtension.java
1
package com.keenwrite.processors.markdown.extensions.outline;
2
3
import com.keenwrite.events.ParseHeadingEvent;
4
import com.keenwrite.processors.Processor;
5
import com.keenwrite.processors.markdown.extensions.common.MarkdownParserExtension;
6
import com.vladsch.flexmark.ast.Heading;
7
import com.vladsch.flexmark.parser.Parser.Builder;
8
import com.vladsch.flexmark.parser.block.NodePostProcessor;
9
import com.vladsch.flexmark.parser.block.NodePostProcessorFactory;
10
import com.vladsch.flexmark.util.ast.Document;
11
import com.vladsch.flexmark.util.ast.Node;
12
import com.vladsch.flexmark.util.ast.NodeTracker;
13
import org.jetbrains.annotations.NotNull;
14
15
import java.util.regex.Pattern;
16
17
import static com.keenwrite.events.ParseHeadingEvent.fireNewOutlineEvent;
18
19
public final class DocumentOutlineExtension implements MarkdownParserExtension {
20
  private static final Pattern REGEX = Pattern.compile( "^(#+)" );
21
22
  private final Processor<String> mProcessor;
23
24
  private DocumentOutlineExtension( final Processor<String> processor ) {
25
    mProcessor = processor;
26
  }
27
28
  @Override
29
  public void extend( final Builder builder ) {
30
    builder.postProcessorFactory( new Factory() );
31
  }
32
33
  public static DocumentOutlineExtension create(
34
    final Processor<String> processor ) {
35
    return new DocumentOutlineExtension( processor );
36
  }
37
38
  private class HeadingNodePostProcessor extends NodePostProcessor {
39
    @Override
40
    public void process(
41
      @NotNull final NodeTracker state, @NotNull final Node node ) {
42
      final var heading = mProcessor.apply( node.getChars().toString() );
43
      final var matcher = REGEX.matcher( heading );
44
45
      if( matcher.find() ) {
46
        final var level = matcher.group().length();
47
        final var text = heading.substring( level );
48
        final var offset = node.getStartOffset();
49
        ParseHeadingEvent.fire( level, text, offset );
50
      }
51
    }
52
  }
53
54
  public class Factory extends NodePostProcessorFactory {
55
    public Factory() {
56
      super( false );
57
      addNodes( Heading.class );
58
    }
59
60
    @NotNull
61
    @Override
62
    public NodePostProcessor apply( @NotNull final Document document ) {
63
      fireNewOutlineEvent();
64
      return new HeadingNodePostProcessor();
65
    }
66
  }
67
}
168
M src/main/java/com/keenwrite/processors/markdown/extensions/r/RInlineExtension.java
55
import com.keenwrite.processors.ProcessorContext;
66
import com.keenwrite.processors.markdown.BaseMarkdownProcessor;
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownParserExtension;
78
import com.keenwrite.processors.r.RInlineEvaluator;
89
import com.vladsch.flexmark.ast.Paragraph;
910
import com.vladsch.flexmark.parser.InlineParserExtensionFactory;
1011
import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
1112
import com.vladsch.flexmark.parser.internal.InlineParserImpl;
1213
import com.vladsch.flexmark.parser.internal.LinkRefProcessorData;
1314
import com.vladsch.flexmark.util.data.DataHolder;
14
import com.vladsch.flexmark.util.data.MutableDataHolder;
1515
1616
import java.util.BitSet;
1717
import java.util.List;
1818
import java.util.Map;
1919
2020
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
2121
import static com.vladsch.flexmark.parser.Parser.Builder;
22
import static com.vladsch.flexmark.parser.Parser.ParserExtension;
2322
2423
/**
2524
 * Responsible for processing inline R statements (denoted using the
2625
 * {@link RInlineEvaluator#PREFIX}) to prevent them from being converted to
2726
 * HTML {@code <code>} elements and stop them from interfering with TeX
2827
 * statements. Note that TeX statements are processed using a Markdown
2928
 * extension, rather than an implementation of {@link Processor}. For this
3029
 * reason, some pre-conversion is necessary.
3130
 */
32
public final class RInlineExtension implements ParserExtension {
31
public final class RInlineExtension implements MarkdownParserExtension {
3332
  private final RInlineEvaluator mEvaluator;
3433
  private final BaseMarkdownProcessor mMarkdownProcessor;
...
5554
    builder.customInlineParserFactory( InlineParser::new );
5655
  }
57
58
  @Override
59
  public void parserOptions( final MutableDataHolder options ) {}
6056
6157
  /**
A src/main/java/com/keenwrite/processors/markdown/extensions/references/AnchorNameDelimiterProcessor.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.parser.InlineParser;
8
import com.vladsch.flexmark.parser.core.delimiter.Delimiter;
9
import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
10
import com.vladsch.flexmark.parser.delimiter.DelimiterRun;
11
import com.vladsch.flexmark.util.ast.Node;
12
13
import static com.keenwrite.constants.Constants.DEF_DELIM_BEGAN_DEFAULT;
14
15
/**
16
 * Responsible for processing {@code {@type:id}} anchors.
17
 */
18
class AnchorNameDelimiterProcessor implements DelimiterProcessor {
19
20
  @Override
21
  public void process(
22
    final Delimiter opener,
23
    final Delimiter closer,
24
    final int delimitersUsed ) {
25
    final var node = new AnchorNameNode();
26
    opener.moveNodesBetweenDelimitersTo( node, closer );
27
  }
28
29
  @Override
30
  public char getOpeningCharacter() {
31
    return '{';
32
  }
33
34
  @Override
35
  public char getClosingCharacter() {
36
    return '}';
37
  }
38
39
  @Override
40
  public int getMinLength() {
41
    return 1;
42
  }
43
44
  @Override
45
  public int getDelimiterUse(
46
    final DelimiterRun opener,
47
    final DelimiterRun closer ) {
48
    final var text = opener.getNode();
49
50
    // Ensure that the default delimiters are respected (not clobbered by
51
    // transforming them into anchor links).
52
    return text.getChars().toString().equals( DEF_DELIM_BEGAN_DEFAULT ) ? 0 : 1;
53
  }
54
55
  @Override
56
  public Node unmatchedDelimiterNode(
57
    final InlineParser inlineParser,
58
    final DelimiterRun delimiter ) {
59
    return null;
60
  }
61
62
  @Override
63
  public boolean canBeOpener(
64
    final String before,
65
    final String after,
66
    final boolean leftFlanking,
67
    final boolean rightFlanking,
68
    final boolean beforeIsPunctuation,
69
    final boolean afterIsPunctuation,
70
    final boolean beforeIsWhitespace,
71
    final boolean afterIsWhiteSpace ) {
72
    return leftFlanking;
73
  }
74
75
  @Override
76
  public boolean canBeCloser(
77
    final String before,
78
    final String after,
79
    final boolean leftFlanking,
80
    final boolean rightFlanking,
81
    final boolean beforeIsPunctuation,
82
    final boolean afterIsPunctuation,
83
    final boolean beforeIsWhitespace,
84
    final boolean afterIsWhiteSpace ) {
85
    return rightFlanking;
86
  }
87
88
  @Override
89
  public boolean skipNonOpenerCloser() {
90
    return false;
91
  }
92
}
193
A src/main/java/com/keenwrite/processors/markdown/extensions/references/AnchorNameNode.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.ast.DelimitedNodeImpl;
8
import com.vladsch.flexmark.util.sequence.BasedSequence;
9
import org.jetbrains.annotations.NotNull;
10
11
/**
12
 * Responsible for writing HTML anchor names in the form
13
 * {@code <a data-type="..." name="name" />}, where {@code name} can be
14
 * referred to by a cross-reference.
15
 *
16
 * @see AnchorXrefNode
17
 */
18
public class AnchorNameNode extends DelimitedNodeImpl implements CrossReferenceNode {
19
20
  private BasedSequence mOpeningMarker = BasedSequence.EMPTY;
21
  private BasedSequence mClosingMarker = BasedSequence.EMPTY;
22
23
  private BasedSequenceNameParser mParser;
24
25
  public AnchorNameNode() {}
26
27
  @Override
28
  public String getTypeName() {
29
    return mParser.getTypeName();
30
  }
31
32
  @Override
33
  public String getIdName() {
34
    return mParser.getIdName();
35
  }
36
37
  @Override
38
  public String getRefAttrName() {
39
    return "name";
40
  }
41
42
  @Override
43
  public BasedSequence getOpeningMarker() {
44
    return mOpeningMarker;
45
  }
46
47
  @NotNull
48
  @Override
49
  public BasedSequence getChars() {
50
    return BasedSequence.EMPTY;
51
  }
52
53
  @Override
54
  public void setOpeningMarker( final BasedSequence openingMarker ) {
55
    mOpeningMarker = openingMarker;
56
  }
57
58
  @Override
59
  public BasedSequence getText() {
60
    return BasedSequence.EMPTY;
61
  }
62
63
  @Override
64
  public void setText( final BasedSequence text ) {
65
    mParser = BasedSequenceNameParser.parse( text );
66
  }
67
68
  @Override
69
  public BasedSequence getClosingMarker() {
70
    return mClosingMarker;
71
  }
72
73
  @Override
74
  public void setClosingMarker( final BasedSequence closingMarker ) {
75
    mClosingMarker = closingMarker;
76
  }
77
}
178
A src/main/java/com/keenwrite/processors/markdown/extensions/references/AnchorXrefNode.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.util.ast.Node;
8
import com.vladsch.flexmark.util.sequence.BasedSequence;
9
import org.jetbrains.annotations.NotNull;
10
11
/**
12
 * Responsible for writing HTML anchor cross-references in the form
13
 * {@code <a data-type="..." href="#name" />} where {@code name} refers
14
 * to an anchor name.
15
 *
16
 * @see AnchorNameNode
17
 */
18
public class AnchorXrefNode extends Node implements CrossReferenceNode {
19
  private final String mTypeName;
20
  private final String mIdName;
21
22
  AnchorXrefNode( final String type, final String id ) {
23
    mTypeName = type;
24
    mIdName = STR. "#\{ id }" ;
25
  }
26
27
  @Override
28
  public String getTypeName() {
29
    return mTypeName;
30
  }
31
32
  @Override
33
  public String getIdName() {
34
    return mIdName;
35
  }
36
37
  @Override
38
  public String getRefAttrName() {
39
    return "href";
40
  }
41
42
  @NotNull
43
  @Override
44
  public BasedSequence[] getSegments() {
45
    return BasedSequence.EMPTY_SEGMENTS;
46
  }
47
}
148
A src/main/java/com/keenwrite/processors/markdown/extensions/references/AnchorXrefProcessorFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.parser.LinkRefProcessor;
8
import com.vladsch.flexmark.parser.LinkRefProcessorFactory;
9
import com.vladsch.flexmark.util.ast.Document;
10
import com.vladsch.flexmark.util.ast.Node;
11
import com.vladsch.flexmark.util.data.DataHolder;
12
import com.vladsch.flexmark.util.sequence.BasedSequence;
13
import org.jetbrains.annotations.NotNull;
14
15
/**
16
 * Responsible for processing {@code [@type:id]} anchors.
17
 */
18
class AnchorXrefProcessorFactory implements LinkRefProcessorFactory {
19
  private final LinkRefProcessor mProcessor = new AnchorLinkRefProcessor();
20
21
  @Override
22
  public boolean getWantExclamationPrefix( @NotNull final DataHolder options ) {
23
    return false;
24
  }
25
26
  @Override
27
  public int getBracketNestingLevel( @NotNull final DataHolder options ) {
28
    return 0;
29
  }
30
31
  @NotNull
32
  @Override
33
  public LinkRefProcessor apply( @NotNull final Document document ) {
34
    return mProcessor;
35
  }
36
37
  private static class AnchorLinkRefProcessor implements LinkRefProcessor {
38
39
    @Override
40
    public boolean getWantExclamationPrefix() {
41
      return false;
42
    }
43
44
    @Override
45
    public int getBracketNestingLevel() {
46
      return 0;
47
    }
48
49
    @Override
50
    public boolean isMatch( @NotNull final BasedSequence nodeChars ) {
51
      return nodeChars.indexOf( '@' ) == 1;
52
    }
53
54
    @NotNull
55
    @Override
56
    public Node createNode( @NotNull final BasedSequence nodeChars ) {
57
      return BasedSequenceXrefParser.parse( nodeChars ).toNode();
58
    }
59
60
    @NotNull
61
    @Override
62
    public BasedSequence adjustInlineText(
63
      @NotNull final Document document,
64
      @NotNull final Node node ) {
65
      return BasedSequence.EMPTY;
66
    }
67
68
    @Override
69
    public boolean allowDelimiters(
70
      @NotNull final BasedSequence chars,
71
      @NotNull final Document document,
72
      @NotNull final Node node ) {
73
      return false;
74
    }
75
76
    @Override
77
    public void updateNodeElements(
78
      @NotNull final Document document,
79
      @NotNull final Node node ) {}
80
  }
81
}
182
A src/main/java/com/keenwrite/processors/markdown/extensions/references/BasedSequenceNameParser.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.util.sequence.BasedSequence;
8
9
import java.util.regex.Pattern;
10
11
class BasedSequenceNameParser extends BasedSequenceParser {
12
  private static final String REGEX = STR. "#\{ REGEX_INNER }" ;
13
  private static final Pattern PATTERN = asPattern( REGEX );
14
15
  private BasedSequenceNameParser( final String text ) {
16
    super( text );
17
  }
18
19
  static BasedSequenceNameParser parse( final BasedSequence chars ) {
20
    return new BasedSequenceNameParser( chars.toString() );
21
  }
22
23
  @Override
24
  Pattern getPattern() {
25
    return PATTERN;
26
  }
27
}
128
A src/main/java/com/keenwrite/processors/markdown/extensions/references/BasedSequenceParser.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import java.util.regex.Matcher;
8
import java.util.regex.Pattern;
9
10
import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS;
11
12
abstract class BasedSequenceParser {
13
  /**
14
   * Shared syntax between subclasses: a letter followed by zero or more
15
   * alphanumeric characters.
16
   */
17
  static final String REGEX_INNER =
18
    "(\\p{Alpha}[\\p{Alnum}-_]+):(\\p{Alpha}[\\p{Alnum}-_]+)";
19
20
  private final String mTypeName;
21
  private final String mIdName;
22
23
  BasedSequenceParser( final String text ) {
24
    final var matcher = createMatcher( text );
25
26
    if( matcher.find() ) {
27
      mTypeName = matcher.group( 1 );
28
      mIdName = matcher.group( 2 );
29
    }
30
    else {
31
      mTypeName = null;
32
      mIdName = null;
33
    }
34
  }
35
36
  static Pattern asPattern( final String regex ) {
37
    return Pattern.compile( regex, UNICODE_CHARACTER_CLASS );
38
  }
39
40
  abstract Pattern getPattern();
41
42
  /**
43
   * Creates a regular expression pattern matcher that can extract the
44
   * reference elements from text.
45
   *
46
   * @param text The text containing an anchor or cross-reference to an anchor.
47
   * @return The {@link Matcher} to use when extracting the text elements.
48
   */
49
  Matcher createMatcher( final String text ) {
50
    return getPattern().matcher( text );
51
  }
52
53
  String getTypeName() {
54
    return mTypeName;
55
  }
56
57
  String getIdName() {
58
    return mIdName;
59
  }
60
}
161
A src/main/java/com/keenwrite/processors/markdown/extensions/references/BasedSequenceXrefParser.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.util.ast.Node;
8
import com.vladsch.flexmark.util.sequence.BasedSequence;
9
10
import java.util.regex.Pattern;
11
12
import static com.keenwrite.processors.markdown.extensions.common.EmptyNode.EMPTY_NODE;
13
14
class BasedSequenceXrefParser extends BasedSequenceParser {
15
  private static final String REGEX = STR. "\\[@\{ REGEX_INNER }]" ;
16
  private static final Pattern PATTERN = asPattern( REGEX );
17
18
  private BasedSequenceXrefParser( final String text ) {
19
    super( text );
20
  }
21
22
  static BasedSequenceXrefParser parse( final BasedSequence chars ) {
23
    return new BasedSequenceXrefParser( chars.toString() );
24
  }
25
26
  @Override
27
  Pattern getPattern() {
28
    return PATTERN;
29
  }
30
31
  Node toNode() {
32
    final var typeName = getTypeName();
33
    final var idName = getIdName();
34
35
    return typeName == null || idName == null
36
      ? EMPTY_NODE
37
      : new AnchorXrefNode( typeName, idName );
38
  }
39
}
140
A src/main/java/com/keenwrite/processors/markdown/extensions/references/CrossReferenceExtension.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownRendererExtension;
8
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
9
import com.vladsch.flexmark.parser.Parser.Builder;
10
11
/**
12
 * Responsible for processing {@code {@type:id}} anchors and their corresponding
13
 * {@code [@type:id]} cross-references.
14
 */
15
public final class CrossReferenceExtension extends MarkdownRendererExtension {
16
  /**
17
   * Use {@link #create()}.
18
   */
19
  private CrossReferenceExtension() {}
20
21
  /**
22
   * Returns a new {@link CrossReferenceExtension}.
23
   *
24
   * @return An extension capable of parsing cross-reference syntax.
25
   */
26
  public static CrossReferenceExtension create() {
27
    return new CrossReferenceExtension();
28
  }
29
30
  @Override
31
  public void extend( final Builder builder ) {
32
    builder.linkRefProcessorFactory( new AnchorXrefProcessorFactory() );
33
    builder.customDelimiterProcessor( new AnchorNameDelimiterProcessor() );
34
  }
35
36
  @Override
37
  protected NodeRendererFactory createNodeRendererFactory() {
38
    return new CrossReferencesNodeRendererFactory();
39
  }
40
}
141
A src/main/java/com/keenwrite/processors/markdown/extensions/references/CrossReferenceNode.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.html.HtmlWriter;
8
9
/**
10
 * Responsible for generating anchor links, either named or cross-referenced.
11
 */
12
public interface CrossReferenceNode {
13
  String getTypeName();
14
15
  String getIdName();
16
17
  String getRefAttrName();
18
19
  /**
20
   * Writes the HTML representation for this cross-reference node.
21
   *
22
   * @param html The HTML tag is written to the {@link HtmlWriter}.
23
   */
24
  default void write( final HtmlWriter html ) {
25
    final var type = getTypeName();
26
    final var id = getIdName();
27
    final var attr = getRefAttrName();
28
29
    final var clazz = STR. "class=\"\{ attr }\"" ;
30
    final var dataType = STR. "data-type=\"\{ type }\"" ;
31
    final var refId = STR. "\{ attr }=\"\{ id }\"" ;
32
33
    html.raw( STR. "<a \{ clazz } \{ dataType } \{ refId } />" );
34
  }
35
}
136
A src/main/java/com/keenwrite/processors/markdown/extensions/references/CrossReferencesNodeRenderer.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.vladsch.flexmark.html.HtmlWriter;
8
import com.vladsch.flexmark.html.renderer.NodeRenderer;
9
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
10
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
11
12
import java.util.Arrays;
13
import java.util.HashSet;
14
import java.util.Set;
15
16
/**
17
 * Responsible for rendering HTML elements that correspond to cross-references.
18
 */
19
class CrossReferencesNodeRenderer implements NodeRenderer {
20
  @Override
21
  public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
22
    return new HashSet<>( Arrays.asList(
23
      new NodeRenderingHandler<>( AnchorNameNode.class, this::render ),
24
      new NodeRenderingHandler<>( AnchorXrefNode.class, this::render )
25
    ) );
26
  }
27
28
  private void render(
29
    final CrossReferenceNode node,
30
    final NodeRendererContext context,
31
    final HtmlWriter html ) {
32
    node.write( html );
33
  }
34
}
135
A src/main/java/com/keenwrite/processors/markdown/extensions/references/CrossReferencesNodeRendererFactory.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownNodeRendererFactory;
8
import com.vladsch.flexmark.html.renderer.NodeRenderer;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
11
class CrossReferencesNodeRendererFactory extends MarkdownNodeRendererFactory {
12
  @Override
13
  protected NodeRenderer createNodeRenderer( final DataHolder options ) {
14
    return new CrossReferencesNodeRenderer();
15
  }
16
}
117
D src/main/java/com/keenwrite/processors/markdown/extensions/tex/TeXExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.tex;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.processors.ProcessorContext;
6
import com.keenwrite.processors.markdown.extensions.HtmlRendererAdapter;
7
import com.keenwrite.processors.markdown.extensions.tex.TexNodeRenderer.Factory;
8
import com.vladsch.flexmark.html.HtmlRenderer;
9
import com.vladsch.flexmark.parser.Parser;
10
import com.vladsch.flexmark.util.data.MutableDataHolder;
11
import org.jetbrains.annotations.NotNull;
12
13
import java.util.function.Function;
14
15
import static com.vladsch.flexmark.parser.Parser.ParserExtension;
16
17
/**
18
 * Responsible for wrapping delimited TeX code in Markdown into an XML element
19
 * that the HTML renderer can handle. For example, {@code $E=mc^2$} becomes
20
 * {@code <tex>E=mc^2</tex>} when passed to HTML renderer. The HTML renderer
21
 * is responsible for converting the TeX code for display. This avoids inserting
22
 * SVG code into the Markdown document, which the parser would then have to
23
 * iterate---a <em>very</em> wasteful operation that impacts front-end
24
 * performance.
25
 */
26
public class TeXExtension extends HtmlRendererAdapter
27
  implements ParserExtension {
28
29
  /**
30
   * Responsible for pre-parsing the input.
31
   */
32
  private final Function<String, String> mEvaluator;
33
34
  /**
35
   * Controls how the node renderer produces TeX code within HTML output.
36
   */
37
  private final ExportFormat mExportFormat;
38
39
  private TeXExtension(
40
    final Function<String, String> evaluator, final ProcessorContext context  ) {
41
    mEvaluator = evaluator;
42
    mExportFormat = context.getExportFormat();
43
  }
44
45
  /**
46
   * Creates an extension capable of handling delimited TeX code in Markdown.
47
   *
48
   * @return The new {@link TeXExtension}, never {@code null}.
49
   */
50
  public static TeXExtension create(
51
    final Function<String, String> evaluator, final ProcessorContext context  ) {
52
    return new TeXExtension( evaluator, context );
53
  }
54
55
  /**
56
   * Adds the TeX extension for HTML document export types.
57
   *
58
   * @param builder      The document builder.
59
   * @param rendererType Indicates the document type to be built.
60
   */
61
  @Override
62
  public void extend( @NotNull final HtmlRenderer.Builder builder,
63
                      @NotNull final String rendererType ) {
64
    if( "HTML".equalsIgnoreCase( rendererType ) ) {
65
      builder.nodeRendererFactory( new Factory( mExportFormat, mEvaluator ) );
66
    }
67
  }
68
69
  @Override
70
  public void extend( final Parser.Builder builder ) {
71
    builder.customDelimiterProcessor( new TeXInlineDelimiterProcessor() );
72
  }
73
74
  @Override
75
  public void parserOptions( final MutableDataHolder options ) {
76
  }
77
}
781
D 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
}
841
A src/main/java/com/keenwrite/processors/markdown/extensions/tex/TexExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.tex;
3
4
import com.keenwrite.ExportFormat;
5
import com.keenwrite.processors.ProcessorContext;
6
import com.keenwrite.processors.markdown.extensions.common.MarkdownRendererExtension;
7
import com.keenwrite.processors.markdown.extensions.tex.TexNodeRenderer.TexNodeRendererFactory;
8
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
9
import com.vladsch.flexmark.parser.Parser;
10
11
import java.util.function.Function;
12
13
/**
14
 * Responsible for wrapping delimited TeX code in Markdown into an XML element
15
 * that the HTML renderer can handle. For example, {@code $E=mc^2$} becomes
16
 * {@code <tex>E=mc^2</tex>} when passed to HTML renderer. The HTML renderer
17
 * is responsible for converting the TeX code for display. This avoids inserting
18
 * SVG code into the Markdown document, which the parser would then have to
19
 * iterate---a <em>very</em> wasteful operation that impacts front-end
20
 * performance.
21
 */
22
public class TexExtension extends MarkdownRendererExtension {
23
  /**
24
   * Responsible for pre-parsing the input.
25
   */
26
  private final Function<String, String> mEvaluator;
27
28
  /**
29
   * Controls how the node renderer produces TeX code within HTML output.
30
   */
31
  private final ExportFormat mExportFormat;
32
33
  private TexExtension(
34
    final Function<String, String> evaluator,
35
    final ProcessorContext context ) {
36
    mEvaluator = evaluator;
37
    mExportFormat = context.getExportFormat();
38
  }
39
40
  /**
41
   * Creates an extension capable of handling delimited TeX code in Markdown.
42
   *
43
   * @return The new {@link TexExtension}, never {@code null}.
44
   */
45
  public static TexExtension create(
46
    final Function<String, String> evaluator, final ProcessorContext context ) {
47
    return new TexExtension( evaluator, context );
48
  }
49
50
  /**
51
   * Creates the TeX {@link NodeRendererFactory} for HTML document export types.
52
   */
53
  @Override
54
  public NodeRendererFactory createNodeRendererFactory() {
55
    return new TexNodeRendererFactory( mExportFormat, mEvaluator );
56
  }
57
58
  @Override
59
  public void extend( final Parser.Builder builder ) {
60
    builder.customDelimiterProcessor( new TexInlineDelimiterProcessor() );
61
  }
62
}
163
A src/main/java/com/keenwrite/processors/markdown/extensions/tex/TexInlineDelimiterProcessor.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.tex;
3
4
import com.vladsch.flexmark.parser.InlineParser;
5
import com.vladsch.flexmark.parser.core.delimiter.Delimiter;
6
import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
7
import com.vladsch.flexmark.parser.delimiter.DelimiterRun;
8
import com.vladsch.flexmark.util.ast.Node;
9
10
class TexInlineDelimiterProcessor implements DelimiterProcessor {
11
12
  @Override
13
  public void process(
14
    final Delimiter opener,
15
    final Delimiter closer,
16
    final int delimitersUsed ) {
17
    final var node = new TexNode( opener, closer );
18
    opener.moveNodesBetweenDelimitersTo( node, closer );
19
  }
20
21
  @Override
22
  public char getOpeningCharacter() {
23
    return '$';
24
  }
25
26
  @Override
27
  public char getClosingCharacter() {
28
    return '$';
29
  }
30
31
  @Override
32
  public int getMinLength() {
33
    return 1;
34
  }
35
36
  /**
37
   * Allow for $ or $$.
38
   *
39
   * @param opener One or more opening delimiter characters.
40
   * @param closer One or more closing delimiter characters.
41
   * @return The number of delimiters to use to determine whether a valid
42
   * opening delimiter expression is found.
43
   */
44
  @Override
45
  public int getDelimiterUse(
46
    final DelimiterRun opener,
47
    final DelimiterRun closer ) {
48
    return 1;
49
  }
50
51
  @Override
52
  public boolean canBeOpener(
53
    final String before,
54
    final String after,
55
    final boolean leftFlanking,
56
    final boolean rightFlanking,
57
    final boolean beforeIsPunctuation,
58
    final boolean afterIsPunctuation,
59
    final boolean beforeIsWhitespace,
60
    final boolean afterIsWhiteSpace ) {
61
    return leftFlanking;
62
  }
63
64
  @Override
65
  public boolean canBeCloser(
66
    final String before,
67
    final String after,
68
    final boolean leftFlanking,
69
    final boolean rightFlanking,
70
    final boolean beforeIsPunctuation,
71
    final boolean afterIsPunctuation,
72
    final boolean beforeIsWhitespace,
73
    final boolean afterIsWhiteSpace ) {
74
    return rightFlanking;
75
  }
76
77
  @Override
78
  public Node unmatchedDelimiterNode(
79
    final InlineParser inlineParser,
80
    final DelimiterRun delimiter ) {
81
    return null;
82
  }
83
84
  @Override
85
  public boolean skipNonOpenerCloser() {
86
    return false;
87
  }
88
}
189
M src/main/java/com/keenwrite/processors/markdown/extensions/tex/TexNode.java
77
public class TexNode extends DelimitedNodeImpl {
88
  /**
9
   * TeX expression wrapped in a {@code <tex>} element.
9
   * A TeX expression wrapped in a {@code <tex>} element.
1010
   */
1111
  public static final String HTML_TEX = "tex";
1212
13
  public static final String TOKEN_OPEN = "$";
14
  public static final String TOKEN_CLOSE = "$";
13
  static final String TOKEN_OPEN = "$";
14
  static final String TOKEN_CLOSE = "$";
1515
1616
  private final String mOpener;
...
3333
   * @return Either '$' or '$$'.
3434
   */
35
  public String getOpeningDelimiter() { return mOpener; }
35
  public String getOpeningDelimiter() {
36
    return mOpener;
37
  }
3638
3739
  /**
3840
   * @return Either '$' or '$$'.
3941
   */
40
  public String getClosingDelimiter() { return mCloser; }
42
  public String getClosingDelimiter() {
43
    return mCloser;
44
  }
4145
4246
  private String getDelimiter( final Delimiter delimiter ) {
M src/main/java/com/keenwrite/processors/markdown/extensions/tex/TexNodeRenderer.java
3030
      APPLICATION_PDF, new TexElementNodeRenderer( true ),
3131
      HTML_TEX_SVG, new TexSvgNodeRenderer(),
32
      HTML_TEX_DELIMITED, new TexDelimNodeRenderer(),
32
      HTML_TEX_DELIMITED, new TexDelimitedNodeRenderer(),
3333
      XHTML_TEX, new TexElementNodeRenderer( true ),
3434
      NONE, RENDERER
3535
    );
3636
37
  public static class Factory implements NodeRendererFactory {
37
  public static class TexNodeRendererFactory implements NodeRendererFactory {
3838
    private final RendererFacade mNodeRenderer;
3939
40
    public Factory(
40
    public TexNodeRendererFactory(
4141
      final ExportFormat exportFormat,
4242
      final Function<String, String> evaluator ) {
43
      mNodeRenderer = EXPORT_RENDERERS.getOrDefault( exportFormat, RENDERER );
43
      final var format = exportFormat == null ? NONE : exportFormat;
44
45
      mNodeRenderer = EXPORT_RENDERERS.getOrDefault( format, RENDERER );
4446
      mNodeRenderer.setEvaluator( evaluator );
4547
    }
...
129131
   * Responsible for rendering a TeX node as text bracketed by $ tokens.
130132
   */
131
  private static class TexDelimNodeRenderer extends RendererFacade {
133
  private static class TexDelimitedNodeRenderer extends RendererFacade {
132134
    void render( final TexNode node,
133135
                 final NodeRendererContext context,
M src/main/java/com/keenwrite/service/events/impl/DefaultNotification.java
1111
public class DefaultNotification implements Notification {
1212
13
  private final String title;
14
  private final String content;
13
  private final String mTitle;
14
  private final String mContent;
1515
1616
  /**
...
2525
      final String message,
2626
      final Object... args ) {
27
    this.title = title;
28
    this.content = MessageFormat.format( message, args );
27
    mTitle = title;
28
    mContent = MessageFormat.format( message, args );
2929
  }
3030
3131
  @Override
3232
  public String getTitle() {
33
    return this.title;
33
    return mTitle;
3434
  }
3535
3636
  @Override
3737
  public String getContent() {
38
    return this.content;
38
    return mContent;
3939
  }
4040
M src/main/java/com/keenwrite/service/impl/DefaultSettings.java
22
package com.keenwrite.service.impl;
33
4
import com.keenwrite.config.PropertiesConfiguration;
45
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;
86
97
import java.io.InputStreamReader;
...
1917
 */
2018
public final class DefaultSettings implements Settings {
21
22
  private static final char VALUE_SEPARATOR = ',';
2319
2420
  private final PropertiesConfiguration mProperties = loadProperties();
2521
26
  public DefaultSettings() {
27
  }
22
  public DefaultSettings() {}
2823
2924
  /**
...
5550
   *
5651
   * @param property The property value to coerce.
57
   * @param defaults The defaults values to use should the property be unset.
52
   * @param defaults The values to use should the property be unset.
5853
   * @return The list of properties coerced from objects to strings.
5954
   */
6055
  @Override
6156
  public List<String> getStringSettingList(
62
      final String property, final List<String> defaults ) {
63
    return getSettings().getList( String.class, property, defaults );
57
    final String property, final List<String> defaults ) {
58
    return getSettings().getList( property, defaults );
6459
  }
6560
...
8984
    final var url = getPropertySource();
9085
    final var configuration = new PropertiesConfiguration();
86
    final var encoding = getDefaultEncoding();
9187
9288
    if( url != null ) {
9389
      try( final var reader = new InputStreamReader(
94
          url.openStream(), getDefaultEncoding() ) ) {
95
        configuration.setListDelimiterHandler( createListDelimiterHandler() );
90
        url.openStream(), encoding ) ) {
9691
        configuration.read( reader );
9792
      } catch( final Exception ex ) {
...
105100
  private Charset getDefaultEncoding() {
106101
    return Charset.defaultCharset();
107
  }
108
109
  private ListDelimiterHandler createListDelimiterHandler() {
110
    return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
111102
  }
112103
M src/main/java/com/keenwrite/typesetting/HostTypesetter.java
2222
import static com.keenwrite.io.SysFile.toFile;
2323
import static java.lang.ProcessBuilder.Redirect.DISCARD;
24
import static java.nio.charset.StandardCharsets.UTF_8;
2425
import static java.nio.file.Files.*;
2526
import static java.util.Arrays.asList;
...
184185
    private void log( final Path path ) throws IOException {
185186
      if( exists( path ) ) {
186
        log( readAllLines( path ) );
187
        log( readAllLines( path, UTF_8 ) );
187188
      }
188189
    }
M src/main/java/com/keenwrite/typesetting/installer/panes/UnixManagerInstallPane.java
55
package com.keenwrite.typesetting.installer.panes;
66
7
import com.keenwrite.ui.clipboard.Clipboard;
7
import com.keenwrite.ui.clipboard.SystemClipboard;
88
import javafx.geometry.Insets;
99
import javafx.scene.Node;
...
9292
    // Change the label to indicate clipboard is updated.
9393
    copyButton.setOnAction( event -> {
94
      Clipboard.write( mCommands.getText() );
94
      SystemClipboard.write( mCommands.getText() );
9595
      copyButton.setText( get( PREFIX + ".copy.ended" ) );
9696
    } );
M src/main/java/com/keenwrite/ui/actions/GuiCommands.java
5151
import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType;
5252
import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType.*;
53
import static java.nio.charset.StandardCharsets.UTF_8;
5354
import static java.nio.file.Files.writeString;
5455
import static javafx.application.Platform.runLater;
...
230231
            // Processors can export binary files. In such cases, processors
231232
            // return null to prevent further processing.
232
            return export == null ? null : writeString( sourcePath, export );
233
            return export == null
234
              ? null
235
              : writeString( sourcePath, export, UTF_8 );
233236
          }
234237
        };
D src/main/java/com/keenwrite/ui/clipboard/Clipboard.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.clipboard;
6
7
import javafx.scene.control.TableView;
8
import javafx.scene.input.ClipboardContent;
9
10
import java.util.TreeSet;
11
12
import static javafx.scene.input.Clipboard.getSystemClipboard;
13
14
/**
15
 * Responsible for pasting into the computer's clipboard.
16
 */
17
public class Clipboard {
18
  /**
19
   * Copies the given text into the clipboard, overwriting all data.
20
   *
21
   * @param text The text to insert into the clipboard.
22
   */
23
  public static void write( final String text ) {
24
    final var contents = new ClipboardContent();
25
    contents.putString( text );
26
    getSystemClipboard().setContent( contents );
27
  }
28
29
  /**
30
   * Delegates to {@link #write(String)}.
31
   *
32
   * @see #write(String)
33
   */
34
  public static void write( final StringBuilder text ) {
35
    write( text.toString() );
36
  }
37
38
  /**
39
   * Copies the contents of the selected rows into the clipboard; code is from
40
   * <a href="https://stackoverflow.com/a/48126059/59087">StackOverflow</a>.
41
   *
42
   * @param table The {@link TableView} having selected rows to copy.
43
   */
44
  public static <T> void write( final TableView<T> table ) {
45
    final var sb = new StringBuilder( 2048 );
46
    final var rows = new TreeSet<Integer>();
47
    final var cols = table.getColumns();
48
49
    for( final var position : table.getSelectionModel().getSelectedCells() ) {
50
      rows.add( position.getRow() );
51
    }
52
53
    String rSep = "";
54
55
    for( final var row : rows ) {
56
      sb.append( rSep );
57
58
      String cSep = "";
59
60
      for( final var column : cols ) {
61
        sb.append( cSep );
62
63
        final var data = column.getCellData( row );
64
        sb.append( data == null ? "" : data.toString() );
65
66
        cSep = "\t";
67
      }
68
69
      rSep = "\n";
70
    }
71
72
    write( sb );
73
  }
74
}
751
A src/main/java/com/keenwrite/ui/clipboard/SystemClipboard.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.ui.clipboard;
6
7
import javafx.scene.control.TableView;
8
import javafx.scene.input.ClipboardContent;
9
10
import java.util.TreeSet;
11
12
import static javafx.scene.input.Clipboard.getSystemClipboard;
13
14
/**
15
 * Responsible for pasting into the computer's clipboard.
16
 */
17
public class SystemClipboard {
18
  /**
19
   * Copies the given text into the clipboard, overwriting all data.
20
   *
21
   * @param text The text to insert into the clipboard.
22
   */
23
  public static void write( final String text ) {
24
    final var contents = new ClipboardContent();
25
    contents.putString( text );
26
    getSystemClipboard().setContent( contents );
27
  }
28
29
  /**
30
   * Delegates to {@link #write(String)}.
31
   *
32
   * @see #write(String)
33
   */
34
  public static void write( final StringBuilder text ) {
35
    write( text.toString() );
36
  }
37
38
  /**
39
   * Copies the contents of the selected rows into the clipboard; code is from
40
   * <a href="https://stackoverflow.com/a/48126059/59087">StackOverflow</a>.
41
   *
42
   * @param table The {@link TableView} having selected rows to copy.
43
   */
44
  public static <T> void write( final TableView<T> table ) {
45
    final var sb = new StringBuilder( 2048 );
46
    final var rows = new TreeSet<Integer>();
47
    final var cols = table.getColumns();
48
49
    for( final var position : table.getSelectionModel().getSelectedCells() ) {
50
      rows.add( position.getRow() );
51
    }
52
53
    String rSep = "";
54
55
    for( final var row : rows ) {
56
      sb.append( rSep );
57
58
      String cSep = "";
59
60
      for( final var column : cols ) {
61
        sb.append( cSep );
62
63
        final var data = column.getCellData( row );
64
        sb.append( data == null ? "" : data.toString() );
65
66
        cSep = "\t";
67
      }
68
69
      rSep = "\n";
70
    }
71
72
    write( sb );
73
  }
74
}
175
M src/main/java/com/keenwrite/ui/heuristics/DocumentStatistics.java
66
import com.keenwrite.preferences.Workspace;
77
import com.keenwrite.ui.actions.Keyboard;
8
import com.keenwrite.ui.clipboard.Clipboard;
8
import com.keenwrite.ui.clipboard.SystemClipboard;
99
import com.whitemagicsoftware.keencount.TokenizerException;
1010
import javafx.beans.property.IntegerProperty;
...
136136
    setOnKeyPressed( event -> {
137137
      if( Keyboard.isCopy( event ) ) {
138
        Clipboard.write( this );
138
        SystemClipboard.write( this );
139139
      }
140140
    } );
M src/main/java/com/keenwrite/ui/logging/LogView.java
44
import com.keenwrite.events.StatusEvent;
55
import com.keenwrite.ui.actions.Keyboard;
6
import com.keenwrite.ui.clipboard.Clipboard;
6
import com.keenwrite.ui.clipboard.SystemClipboard;
77
import javafx.beans.property.SimpleStringProperty;
88
import javafx.beans.property.StringProperty;
...
117117
    mTable.setOnKeyPressed( event -> {
118118
      if( Keyboard.isCopy( event ) ) {
119
        Clipboard.write( mTable );
119
        SystemClipboard.write( mTable );
120120
      }
121121
    } );
M src/main/resources/com/keenwrite/messages.properties
327327
Wizard.typesetter.container.image.tag=${Wizard.typesetter.container.image.name}:${Wizard.typesetter.container.image.version}
328328
Wizard.typesetter.container.image.url=https://repository.keenwrite.com/containers/${Wizard.typesetter.container.image.tag}
329
Wizard.typesetter.themes.version=1.8.4
330
Wizard.typesetter.themes.checksum=b30dd800df89c1d06b2446469e62c12252434946f950a874e1ca225719ba4efc
329
Wizard.typesetter.themes.version=1.9.0
330
Wizard.typesetter.themes.checksum=aeea45b8c6008fae4f1e84b6668f43a2b7200dbcac716941e4543d0602aec59d
331331
332332
Wizard.container.install.command=Installing container using: ''{0}''
M src/main/resources/com/keenwrite/preview/webview.css
7878
}
7979
80
/* CROSS-REFERENCES ***/
81
a.href::after {
82
  content: " (" attr(data-type) " reference)";
83
}
84
8085
/* ITEMIZED LISTS ***/
8186
ol, ul {
...
300305
}
301306
302
div.bubblerx:after, div.bubbletx:after {
307
div.bubblerx::after, div.bubbletx::after {
303308
  content: "";
304309
  position: absolute;
...
317322
}
318323
319
div.bubbletx:after {
324
div.bubbletx::after {
320325
  right: -1em;
321326
  border-left: 1em solid #ccc;
...
334339
}
335340
341
/* TYPEWRITER ***/
336342
div.typewritten {
337343
  font-family: monospace;
338344
  font-size: 16px;
339345
  font-weight: bold;
340346
}
341
342347
M src/test/java/com/keenwrite/flexmark/ParserTest.java
33
44
import com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension;
5
import com.keenwrite.processors.markdown.extensions.references.CrossReferenceExtension;
56
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
67
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
...
5455
    extensions.add( TablesExtension.create() );
5556
    extensions.add( FencedDivExtension.create() );
57
    extensions.add( CrossReferenceExtension.create() );
5658
5759
    return extensions;
M src/test/java/com/keenwrite/io/FileObjectTest.java
88
import java.io.OutputStreamWriter;
99
10
import static com.keenwrite.constants.Constants.TEMPORARY_DIRECTORY;
1011
import static java.io.File.separator;
1112
import static java.lang.String.format;
1213
import static java.nio.charset.StandardCharsets.UTF_8;
1314
1415
/**
1516
 * Tests file resource allocation.
1617
 */
1718
public class FileObjectTest {
18
  private static final String TEMP_DIR = System.getProperty( "java.io.tmpdir" );
19
2019
  /**
2120
   * Test that resources are not exhausted.
...
3029
3130
    for( int i = 0; i < 10000; i++ ) {
32
      final var filename = format( "%s%s%d.txt", TEMP_DIR, separator, i );
31
      final var filename = format(
32
        "%s%s%d.txt", TEMPORARY_DIRECTORY, separator, i
33
      );
3334
      final var fileObject = session
3435
        .getFileSystemManager()
M src/test/java/com/keenwrite/io/FileWatchServiceTest.java
1212
1313
import static java.io.File.createTempFile;
14
import static java.nio.charset.StandardCharsets.UTF_8;
1415
import static java.nio.file.StandardOpenOption.APPEND;
1516
import static java.nio.file.StandardOpenOption.CREATE;
...
4445
    thread.start();
4546
    service.addListener( listener );
46
    Files.writeString( file.toPath(), text, CREATE, APPEND );
47
    Files.writeString( file.toPath(), text, UTF_8, CREATE, APPEND );
4748
    semaphor.acquire();
4849
    service.stop();
M src/test/java/com/keenwrite/io/downloads/DownloadManagerTest.java
2727
  }
2828
29
  private static final String URL =
29
  private static final String URL_BINARY =
3030
    "https://keenwrite.com/downloads/KeenWrite.exe";
3131
...
4545
    file.deleteOnExit();
4646
47
    final var token = open( URL );
47
    final var token = open( URL_BINARY );
4848
    final var executor = Executors.newFixedThreadPool( 1 );
4949
    final var result = token.download( file, listener );
D src/test/java/com/keenwrite/processors/markdown/ImageLinkExtensionTest.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown;
3
4
import com.keenwrite.editors.common.Caret;
5
import com.keenwrite.processors.Processor;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.keenwrite.processors.markdown.extensions.ImageLinkExtension;
8
import com.vladsch.flexmark.html.HtmlRenderer;
9
import com.vladsch.flexmark.parser.Parser;
10
import org.junit.jupiter.api.Test;
11
12
import java.io.File;
13
import java.net.URI;
14
import java.nio.file.Path;
15
import java.nio.file.Paths;
16
import java.util.LinkedHashMap;
17
import java.util.List;
18
import java.util.Map;
19
20
import static com.keenwrite.ExportFormat.XHTML_TEX;
21
import static com.keenwrite.constants.Constants.DOCUMENT_DEFAULT;
22
import static java.lang.String.format;
23
import static org.junit.jupiter.api.Assertions.assertEquals;
24
import static org.junit.jupiter.api.Assertions.assertNotNull;
25
26
/**
27
 * Responsible for testing that linked images render into HTML according to
28
 * the {@link ImageLinkExtension} rules.
29
 */
30
@SuppressWarnings( "SameParameterValue" )
31
public class ImageLinkExtensionTest {
32
  private static final String UIR_DIR = "images";
33
  private static final String URI_FILE = "kitten";
34
  private static final String URI_PATH = UIR_DIR + '/' + URI_FILE;
35
  private static final String PATH_KITTEN_JPG = URI_PATH + ".jpg";
36
  private static final String PATH_KITTEN_PNG = URI_PATH + ".png";
37
38
  private static final Map<String, String> IMAGES = new LinkedHashMap<>();
39
40
  static {
41
    add( PATH_KITTEN_PNG, URI_FILE );
42
    add( PATH_KITTEN_PNG, URI_PATH );
43
    add( PATH_KITTEN_PNG, PATH_KITTEN_PNG );
44
    add( PATH_KITTEN_JPG, PATH_KITTEN_JPG );
45
    add( "//placekitten.com/200/200", "//placekitten.com/200/200" );
46
    add( "ftp://placekitten.com/200/200", "ftp://placekitten.com/200/200" );
47
    add( "http://placekitten.com/200/200", "http://placekitten.com/200/200" );
48
    add( "https://placekitten.com/200/200", "https://placekitten.com/200/200" );
49
  }
50
51
  private static void add( final String expected, final String actual ) {
52
    IMAGES.put( toMd( actual ), toHtml( expected ) );
53
  }
54
55
  private static String toMd( final String resource ) {
56
    return format( "![Tooltip](%s 'Title')", resource );
57
  }
58
59
  private static String toHtml( final String url ) {
60
    return format(
61
      "<p><img src=\"%s\" alt=\"Tooltip\" title=\"Title\" /></p>%n", url );
62
  }
63
64
  /**
65
   * Test that the key URIs present in the {@link #IMAGES} map are rendered
66
   * as the value URIs present in the same map.
67
   */
68
  @Test
69
  void test_ImageLookup_RelativePathWithExtension_ResolvedSuccessfully() {
70
    final var resource = getResourcePath( PATH_KITTEN_PNG );
71
    final var imagePath = new File( PATH_KITTEN_PNG ).toPath();
72
    final var subpaths = resource.getNameCount() - imagePath.getNameCount();
73
    final var subpath = resource.subpath( 0, subpaths );
74
75
    final var root = resource.getRoot();
76
    assertNotNull( root );
77
78
    final var resolved = root.resolve( subpath );
79
    final var doc = resolved.toString();
80
81
    // The root component isn't considered part of the path, so add it back.
82
    final var documentPath = Path.of( doc, DOCUMENT_DEFAULT.getName() );
83
    final var imagesDir = Path.of( "images" );
84
    final var context = createProcessorContext( documentPath, imagesDir );
85
    final var extension = ImageLinkExtension.create( context );
86
    final var extensions = List.of( extension );
87
    final var pBuilder = Parser.builder();
88
    final var hBuilder = HtmlRenderer.builder();
89
    final var parser = pBuilder.extensions( extensions ).build();
90
    final var renderer = hBuilder.extensions( extensions ).build();
91
92
    assertNotNull( parser );
93
    assertNotNull( renderer );
94
95
    for( final var entry : IMAGES.entrySet() ) {
96
      final var key = entry.getKey();
97
      final var node = parser.parse( key );
98
      final var expectedHtml = entry.getValue();
99
      final var actualHtml = renderer.render( node );
100
101
      assertEquals( expectedHtml, actualHtml );
102
    }
103
  }
104
105
  /**
106
   * Creates a new {@link ProcessorContext} for the given file name path.
107
   *
108
   * @param inputPath Fully qualified path to the file name.
109
   * @return A context used for creating new {@link Processor} instances.
110
   */
111
  private ProcessorContext createProcessorContext(
112
    final Path inputPath, final Path imagesDir ) {
113
    return ProcessorContext
114
      .builder()
115
      .with( ProcessorContext.Mutator::setSourcePath, inputPath )
116
      .with( ProcessorContext.Mutator::setExportFormat, XHTML_TEX )
117
      .with( ProcessorContext.Mutator::setCaret, () -> Caret.builder().build() )
118
      .with( ProcessorContext.Mutator::setImageDir, imagesDir::toFile )
119
      .build();
120
  }
121
122
  private static URI toUri( final String path ) {
123
    try {
124
      return Path.of( path ).toUri();
125
    } catch( final Exception ex ) {
126
      throw new RuntimeException( ex );
127
    }
128
  }
129
130
  private static Path getResourcePath( final String path ) {
131
    return Paths.get( toUri( path ) );
132
  }
133
}
1341
A src/test/java/com/keenwrite/processors/markdown/extensions/images/ImageLinkExtensionTest.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.images;
3
4
import com.keenwrite.editors.common.Caret;
5
import com.keenwrite.processors.Processor;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.vladsch.flexmark.html.HtmlRenderer;
8
import com.vladsch.flexmark.parser.Parser;
9
import org.junit.jupiter.api.Test;
10
11
import java.io.File;
12
import java.net.URI;
13
import java.nio.file.Path;
14
import java.nio.file.Paths;
15
import java.util.LinkedHashMap;
16
import java.util.List;
17
import java.util.Map;
18
19
import static com.keenwrite.ExportFormat.XHTML_TEX;
20
import static com.keenwrite.constants.Constants.DOCUMENT_DEFAULT;
21
import static java.lang.String.format;
22
import static org.junit.jupiter.api.Assertions.assertEquals;
23
import static org.junit.jupiter.api.Assertions.assertNotNull;
24
25
/**
26
 * Responsible for testing that linked images render into HTML according to
27
 * the {@link ImageLinkExtension} rules.
28
 */
29
@SuppressWarnings( "SameParameterValue" )
30
public class ImageLinkExtensionTest {
31
  private static final String UIR_DIR = "images";
32
  private static final String URI_FILE = "kitten";
33
  private static final String URI_PATH = UIR_DIR + '/' + URI_FILE;
34
  private static final String PATH_KITTEN_JPG = URI_PATH + ".jpg";
35
  private static final String PATH_KITTEN_PNG = URI_PATH + ".png";
36
37
  private static final Map<String, String> IMAGES = new LinkedHashMap<>();
38
39
  static {
40
    add( PATH_KITTEN_PNG, URI_FILE );
41
    add( PATH_KITTEN_PNG, URI_PATH );
42
    add( PATH_KITTEN_PNG, PATH_KITTEN_PNG );
43
    add( PATH_KITTEN_JPG, PATH_KITTEN_JPG );
44
    add( "//placekitten.com/200/200", "//placekitten.com/200/200" );
45
    add( "ftp://placekitten.com/200/200", "ftp://placekitten.com/200/200" );
46
    add( "http://placekitten.com/200/200", "http://placekitten.com/200/200" );
47
    add( "https://placekitten.com/200/200", "https://placekitten.com/200/200" );
48
  }
49
50
  private static void add( final String expected, final String actual ) {
51
    IMAGES.put( toMd( actual ), toHtml( expected ) );
52
  }
53
54
  private static String toMd( final String resource ) {
55
    return format( "![Tooltip](%s 'Title')", resource );
56
  }
57
58
  private static String toHtml( final String url ) {
59
    return format(
60
      "<p><img src=\"%s\" alt=\"Tooltip\" title=\"Title\" /></p>%n", url );
61
  }
62
63
  /**
64
   * Test that the key URIs present in the {@link #IMAGES} map are rendered
65
   * as the value URIs present in the same map.
66
   */
67
  @Test
68
  void test_ImageLookup_RelativePathWithExtension_ResolvedSuccessfully() {
69
    final var resource = getResourcePath( PATH_KITTEN_PNG );
70
    final var imagePath = new File( PATH_KITTEN_PNG ).toPath();
71
    final var subpaths = resource.getNameCount() - imagePath.getNameCount();
72
    final var subpath = resource.subpath( 0, subpaths );
73
74
    final var root = resource.getRoot();
75
    assertNotNull( root );
76
77
    final var resolved = root.resolve( subpath );
78
    final var doc = resolved.toString();
79
80
    // The root component isn't considered part of the path, so add it back.
81
    final var documentPath = Path.of( doc, DOCUMENT_DEFAULT.getName() );
82
    final var imagesDir = Path.of( "images" );
83
    final var context = createProcessorContext( documentPath, imagesDir );
84
    final var extension = ImageLinkExtension.create( context );
85
    final var extensions = List.of( extension );
86
    final var pBuilder = Parser.builder();
87
    final var hBuilder = HtmlRenderer.builder();
88
    final var parser = pBuilder.extensions( extensions ).build();
89
    final var renderer = hBuilder.extensions( extensions ).build();
90
91
    assertNotNull( parser );
92
    assertNotNull( renderer );
93
94
    for( final var entry : IMAGES.entrySet() ) {
95
      final var key = entry.getKey();
96
      final var node = parser.parse( key );
97
      final var expectedHtml = entry.getValue();
98
      final var actualHtml = renderer.render( node );
99
100
      assertEquals( expectedHtml, actualHtml );
101
    }
102
  }
103
104
  /**
105
   * Creates a new {@link ProcessorContext} for the given file name path.
106
   *
107
   * @param inputPath Fully qualified path to the file name.
108
   * @return A context used for creating new {@link Processor} instances.
109
   */
110
  private ProcessorContext createProcessorContext(
111
    final Path inputPath, final Path imagesDir ) {
112
    return ProcessorContext
113
      .builder()
114
      .with( ProcessorContext.Mutator::setSourcePath, inputPath )
115
      .with( ProcessorContext.Mutator::setExportFormat, XHTML_TEX )
116
      .with( ProcessorContext.Mutator::setCaret, () -> Caret.builder().build() )
117
      .with( ProcessorContext.Mutator::setImageDir, imagesDir::toFile )
118
      .build();
119
  }
120
121
  private static URI toUri( final String path ) {
122
    try {
123
      return Path.of( path ).toUri();
124
    } catch( final Exception ex ) {
125
      throw new RuntimeException( ex );
126
    }
127
  }
128
129
  private static Path getResourcePath( final String path ) {
130
    return Paths.get( toUri( path ) );
131
  }
132
}
1133
A src/test/java/com/keenwrite/processors/markdown/extensions/references/CaptionsAndCrossReferencesExtensionTest.java
1
/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.references;
6
7
import com.keenwrite.processors.markdown.extensions.captions.CaptionExtension;
8
import com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension;
9
import com.keenwrite.processors.markdown.extensions.tex.TexExtension;
10
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
11
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
12
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
13
import com.vladsch.flexmark.ext.tables.TablesExtension;
14
import com.vladsch.flexmark.html.HtmlRenderer;
15
import com.vladsch.flexmark.parser.Parser;
16
import com.vladsch.flexmark.parser.Parser.ParserExtension;
17
import org.junit.jupiter.params.ParameterizedTest;
18
import org.junit.jupiter.params.provider.Arguments;
19
import org.junit.jupiter.params.provider.MethodSource;
20
21
import java.util.LinkedList;
22
import java.util.List;
23
import java.util.stream.Stream;
24
25
import static com.keenwrite.ExportFormat.XHTML_TEX;
26
import static com.keenwrite.processors.ProcessorContext.Mutator;
27
import static com.keenwrite.processors.ProcessorContext.builder;
28
import static org.junit.jupiter.api.Assertions.assertEquals;
29
30
@SuppressWarnings( "SpellCheckingInspection" )
31
public class CaptionsAndCrossReferencesExtensionTest {
32
  @ParameterizedTest
33
  @MethodSource( "testDocuments" )
34
  public void test_References_Documents_Html(
35
    final String input, final String expected
36
  ) {
37
    final var pBuilder = Parser.builder();
38
    final var hBuilder = HtmlRenderer.builder();
39
    final var extensions = createExtensions();
40
    final var parser = pBuilder.extensions( extensions ).build();
41
    final var renderer = hBuilder.extensions( extensions ).build();
42
43
    final var document = parser.parse( input );
44
    final var actual = renderer.render( document );
45
46
    assertEquals( expected, actual );
47
  }
48
49
  private static Stream<Arguments> testDocuments() {
50
    return Stream.of(
51
      args(
52
        """
53
          {#fig:cats} [@fig:cats]
54
          {#table:dogs} [@table:dogs]
55
          {#ocean:whale-01} [@ocean:whale-02]
56
          """,
57
        """
58
          <p><a class="name" data-type="fig" name="cats" /> <a class="href" data-type="fig" href="#cats" />
59
          <a class="name" data-type="table" name="dogs" /> <a class="href" data-type="table" href="#dogs" />
60
          <a class="name" data-type="ocean" name="whale-01" /> <a class="href" data-type="ocean" href="#whale-02" /></p>
61
          """
62
      ),
63
      args(
64
        """
65
          {#日本:w0mbatß}
66
          [@日本:w0mbatß]
67
          """,
68
        """
69
          <p><a class="name" data-type="日本" name="w0mbatß" />
70
          <a class="href" data-type="日本" href="#w0mbatß" /></p>
71
          """
72
      ),
73
      args(
74
        """
75
          Lorem ipsum dolor sit amet, consectetur adipiscing elit.
76
          {#fig:cats} Sed do eiusmod tempor incididunt ut
77
          labore et dolore magna aliqua. Ut enim ad minim veniam,
78
          quis nostrud exercitation ullamco laboris nisi ut aliquip
79
          ex ea commodo consequat. [@fig:cats]
80
          """,
81
        """
82
          <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
83
          <a class="name" data-type="fig" name="cats" /> Sed do eiusmod tempor incididunt ut
84
          labore et dolore magna aliqua. Ut enim ad minim veniam,
85
          quis nostrud exercitation ullamco laboris nisi ut aliquip
86
          ex ea commodo consequat. <a class="href" data-type="fig" href="#cats" /></p>
87
          """
88
      ),
89
      args(
90
        """
91
          {#note:advancement} Advancement isn't
92
          measured by the ingenuity of inventions, but by humanity's ability
93
          to anticipate and forfend dire aftermaths *before* using them.
94
95
          [@note:advancement]
96
97
          To what end?
98
          """,
99
        """
100
          <p><a class="name" data-type="note" name="advancement" /> Advancement isn't
101
          measured by the ingenuity of inventions, but by humanity's ability
102
          to anticipate and forfend dire aftermaths <em>before</em> using them.</p>
103
          <p><a class="href" data-type="note" href="#advancement" /></p>
104
          <p>To what end?</p>
105
          """
106
      ),
107
      args(
108
        """
109
          $E=mc^2$ {#eq:label}
110
          """,
111
        """
112
          <p><tex>$E=mc^2$</tex> <a class="name" data-type="eq" name="label" /></p>
113
          """
114
      ),
115
      args(
116
        """
117
          $$E=mc^2$$ {#eq:label}
118
          """,
119
        """
120
          <p><tex>$$E=mc^2$$</tex> <a class="name" data-type="eq" name="label" /></p>
121
          """
122
      ),
123
      args(
124
        """
125
          $$E=mc^2$$
126
127
          :: Caption {#eqn:energy}
128
          """,
129
        """
130
          <p><span class="caption">Caption </span><a class="name" data-type="eqn" name="energy" /></p>
131
          <p><tex>$$E=mc^2$$</tex></p>
132
          """
133
      ),
134
      args(
135
        """
136
          ``` haskell
137
          main :: IO ()
138
          ```
139
140
          :: Source code caption {#listing:haskell1}
141
          """,
142
        """
143
          <p><span class="caption">Source code caption </span><a class="name" data-type="listing" name="haskell1" /></p>
144
          <pre><code class="language-haskell">main :: IO ()
145
          </code></pre>
146
          """
147
      ),
148
      args(
149
        """
150
          ::: warning
151
          Do not eat processed **sugar**.
152
153
          Seriously.
154
          :::
155
156
          :: Caption {#warning:sugar}
157
          """,
158
        """
159
          <p><span class="caption">Caption </span><a class="name" data-type="warning" name="sugar" /></p><div class="warning">
160
          <p>Do not eat processed <strong>sugar</strong>.</p>
161
          <p>Seriously.</p>
162
          </div>
163
          """
164
      ),
165
      args(
166
        """
167
          ![alt text](tunnel)
168
169
          :: Caption {#fig:label}
170
          """,
171
        """
172
          <p><span class="caption">Caption </span><a class="name" data-type="fig" name="label" /></p>
173
          <p><img src="tunnel" alt="alt text" /></p>
174
          """
175
      ),
176
      args(
177
        """
178
          ![kitteh](placekitten)
179
180
          :: Caption **bold** {#fig:label} *italics*
181
          """,
182
        """
183
          <p><span class="caption">Caption <strong>bold</strong>  <em>italics</em></span><a class="name" data-type="fig" name="label" /></p>
184
          <p><img src="placekitten" alt="kitteh" /></p>
185
          """
186
      ),
187
      args(
188
        """
189
          > I'd like to be the lucky devil who gets to burn with you.
190
          >
191
          > Well, I'm no angel, my wings have been clipped;
192
          >
193
          > I've traded my halo for horns and a whip.
194
195
          :: Meschiya Lake - Lucky Devil {#lyrics:blues}
196
          """,
197
        """
198
          <p><span class="caption">Meschiya Lake - Lucky Devil </span><a class="name" data-type="lyrics" name="blues" /></p>
199
          <blockquote>
200
          <p>I'd like to be the lucky devil who gets to burn with you.</p>
201
          <p>Well, I'm no angel, my wings have been clipped;</p>
202
          <p>I've traded my halo for horns and a whip.</p>
203
          </blockquote>
204
          """
205
      ),
206
      args(
207
        """
208
          | a | b | c |
209
          |---|---|---|
210
          | 1 | 2 | 3 |
211
          | 4 | 5 | 6 |
212
213
          :: Caption {#tbl:label}
214
          """,
215
        """
216
          <p><span class="caption">Caption </span><a class="name" data-type="tbl" name="label" /></p>
217
          <table>
218
          <thead>
219
          <tr><th>a</th><th>b</th><th>c</th></tr>
220
          </thead>
221
          <tbody>
222
          <tr><td>1</td><td>2</td><td>3</td></tr>
223
          <tr><td>4</td><td>5</td><td>6</td></tr>
224
          </tbody>
225
          </table>
226
          """
227
      ),
228
      args(
229
        """
230
          ``` diagram-plantuml
231
          @startuml
232
          Alice -> Bob: Request
233
          Bob --> Alice: Response
234
          @enduml
235
          ```
236
237
          :: Diagram {#dia:seq1}
238
          """,
239
        """
240
          <p><span class="caption">Diagram </span><a class="name" data-type="dia" name="seq1" /></p>
241
          <pre><code class="language-diagram-plantuml">@startuml
242
          Alice -&gt; Bob: Request
243
          Bob --&gt; Alice: Response
244
          @enduml
245
          </code></pre>
246
          """
247
      ),
248
      args(
249
        """
250
          ::: lyrics
251
          Weather hit, meltin' road.
252
          Our mama's gone, six feet cold.
253
          Gas on down to future town,
254
          Make prophecy take hold.
255
256
          Warnin' sign, cent'ry old:
257
          When buyin' coal, air is sold.
258
          Aim our toil, ten figure oil;
259
          Trade life on Earth for gold.
260
          :::
261
          """,
262
        """
263
          <div class="lyrics">
264
          <p>Weather hit, meltin' road.
265
          Our mama's gone, six feet cold.
266
          Gas on down to future town,
267
          Make prophecy take hold.</p>
268
          <p>Warnin' sign, cent'ry old:
269
          When buyin' coal, air is sold.
270
          Aim our toil, ten figure oil;
271
          Trade life on Earth for gold.</p>
272
          </div>
273
          """
274
      )
275
    );
276
  }
277
278
  private static Arguments args( final String in, final String out ) {
279
    return Arguments.of( in, out );
280
  }
281
282
  private List<ParserExtension> createExtensions() {
283
    final var extensions = new LinkedList<ParserExtension>();
284
    final var context = builder()
285
      .with( Mutator::setExportFormat, XHTML_TEX )
286
      .build();
287
288
    extensions.add( TexExtension.create( s -> s, context ) );
289
    extensions.add( DefinitionExtension.create() );
290
    extensions.add( StrikethroughSubscriptExtension.create() );
291
    extensions.add( SuperscriptExtension.create() );
292
    extensions.add( TablesExtension.create() );
293
    extensions.add( FencedDivExtension.create() );
294
    extensions.add( CrossReferenceExtension.create() );
295
    extensions.add( CaptionExtension.create() );
296
297
    return extensions;
298
  }
299
}
1300
A www/count.sh
1
#!/usr/bin/env bash
2
3
awk '{s+=$1} END {print s}' downloads/*-count.txt 2> /dev/null || echo 0
4
15
A www/downloads/.gitignore
1
version.txt
12
A www/downloads/.htaccess
1
SetEnv no-gzip dont-vary
2
3
<IfModule mod_rewrite.c>
4
RewriteEngine On
5
6
# Ensure the file exists before attemping to download it.
7
RewriteCond %{REQUEST_FILENAME} -f
8
9
# Rewrite requests for file extensions to track.
10
RewriteRule ^([^/]+\.(zip|app|bin|exe|jar))$ counter.php?filename=$1 [L]
11
</IfModule>
112
A www/downloads/counter.php
1
<?php
2
  /* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
3
   *
4
   * SPDX-License-Identifier: MIT
5
   */
6
7
  // Log all errors to a temporary file.
8
  ini_set( 'log_errors', 1 );
9
  ini_set( 'error_log', '/tmp/php-errors.log' );
10
11
  // Do not impose a time limit for downloads.
12
  set_time_limit( 0 );
13
14
  // Flush any previous output buffers.
15
  while( ob_get_level() > 0 ) {
16
    ob_end_flush();
17
  }
18
19
  if( session_id() === "" ) {
20
    session_start();
21
  }
22
23
  // Keep running upon client disconnect (helps catch file transfer failures).
24
  // This setting requires checking whether the connection has been aborted at
25
  // a regular interval to prevent bogging the server with abandoned requests.
26
  ignore_user_abort( true );
27
28
  $filename = get_sanitized_filename();
29
  $valid_filename = !empty( $filename );
30
  $expiry = 24 * 60 * 60;
31
32
  if( $valid_filename && download( $filename ) && token_expired( $expiry ) ) {
33
    increment_count( "$filename-count.txt" );
34
  }
35
36
  /**
37
   * Retrieve the file name being downloaded from the HTTP GET request.
38
   *
39
   * @return string The sanitized file name (without path information).
40
   */
41
  function get_sanitized_filename() {
42
    $filepath = isset( $_GET[ 'filename' ] ) ? $_GET[ 'filename' ] : '';
43
    $fileinfo = pathinfo( $filepath );
44
45
    // Remove path information (no /etc/passwd or ../../etc/passwd for you).
46
    $basename = $fileinfo[ 'basename' ];
47
48
    if( isset( $_SERVER[ 'HTTP_USER_AGENT' ] ) ) {
49
      $periods = substr_count( $basename, '.' );
50
51
      // Address IE bug regarding multiple periods in filename.
52
      $basename = strstr( $_SERVER[ 'HTTP_USER_AGENT' ], 'MSIE' )
53
        ? mb_ereg_replace( '/\./', '%2e', $basename, $periods - 1 )
54
        : $basename;
55
    }
56
57
    // Trim all external and internal spaces.
58
    $basename = mb_ereg_replace( '/\s+/', '', $basename );
59
60
    // Sanitize.
61
    $basename = mb_ereg_replace( '([^\w\d\-_~,;\[\]\(\).])', '', $basename );
62
63
    return $basename;
64
  }
65
66
  /**
67
   * Answers whether the user's download token has expired.
68
   *
69
   * @param int $lifetime Number of seconds before expiring the token.
70
   *
71
   * @return bool True indicates the token has expired (or was not set).
72
   */
73
  function token_expired( $lifetime ) {
74
    $TOKEN_NAME = 'LAST_DOWNLOAD';
75
    $now = time();
76
    $expired = !isset( $_SESSION[ $TOKEN_NAME ] );
77
78
    if( !$expired && ($now - $_SESSION[ $TOKEN_NAME ] > $lifetime) ) {
79
      $expired = true;
80
      $_SESSION = array();
81
82
      session_destroy();
83
    }
84
85
    $_SESSION[ $TOKEN_NAME ] = $now;
86
87
    $TOKEN_CREATE = 'CREATED';
88
89
    if( !isset( $_SESSION[ $TOKEN_CREATE ] ) ) {
90
      $_SESSION[ $TOKEN_CREATE ] = $now;
91
    }
92
    else if( $now - $_SESSION[ $TOKEN_CREATE ] > $lifetime ) {
93
      // Avoid session fixation attacks by regenerating tokens.
94
      session_regenerate_id( true );
95
      $_SESSION[ $TOKEN_CREATE ] = $now;
96
    }
97
98
    return $expired;
99
  }
100
101
  /**
102
   * Downloads a file, allowing for resuming partial downloads.
103
   *
104
   * @param string $filename File to download, must be in script directory.
105
   *
106
   * @return bool True if the file was transferred.
107
   */
108
  function download( $filename ) {
109
    // Don't cache the file stats result (e.g., file size).
110
    clearstatcache();
111
112
    $size = @filesize( $filename );
113
    $size = $size === false || empty( $size ) ? 0 : $size;
114
    $content_type = mime_content_type( $filename );
115
    list( $seek_start, $content_length ) = parse_range( $size );
116
117
    // Added by PHP, removed by us.
118
    header_remove( 'x-powered-by' );
119
120
    // HTTP/1.1 clients must treat invalid date formats, especially 0, as past.
121
    header( 'Expires: 0' );
122
123
    // Prevent local caching.
124
    header( 'Cache-Control: public, must-revalidate, post-check=0, pre-check=0' );
125
126
    // No response message portion may be cached (e.g., by a proxy server).
127
    header( 'Cache-Control: private', false );
128
129
    // Force the browser to download, rather than display the file inline.
130
    header( "Content-Disposition: attachment; filename=\"$filename\"" );
131
    header( 'Accept-Ranges: bytes' );
132
    header( "Content-Length: $content_length" );
133
    header( "Content-Type: $content_type" );
134
135
    $method = isset( $_SERVER[ 'REQUEST_METHOD' ] )
136
      ? $_SERVER[ 'REQUEST_METHOD' ]
137
      : 'GET';
138
139
    // Honour HTTP HEAD requests.
140
    return $method === 'HEAD'
141
      ? false
142
      : transmit( $filename, $seek_start, $size );
143
  }
144
145
  /**
146
   * Parses the HTTP range request header, provided one was sent by the
147
   * client. This provides download resume functionality.
148
   *
149
   * @param int $size The total file size (as stored on disk). 
150
   *
151
   * @return array The starting offset for resuming the download, or 0 to
152
   * download the entire file (i.e., no offset could be parsed); also the
153
   * number of bytes to be transferred.
154
   */
155
  function parse_range( $size ) {
156
    // By default, start transmitting at the beginning of the file.
157
    $seek_start = 0;
158
    $content_length = $size;
159
160
    // Check if a range is sent by browser or download manager.
161
    if( isset( $_SERVER[ 'HTTP_RANGE' ] ) ) {
162
      $range_format = '/^bytes=\d*-\d*(,\d*-\d*)*$/';
163
      $request_range = $_SERVER[ 'HTTP_RANGE' ];
164
165
      // Ensure the content request range is in a valid format.
166
      if( !preg_match( $range_format, $request_range, $matches ) ) {
167
        header( 'HTTP/1.1 416 Requested Range Not Satisfiable' );
168
        header( "Content-Range: bytes */$size" );
169
170
        // Terminate because the range is invalid.
171
        exit;
172
      }
173
174
      // Multiple ranges could be specified, but only serve the first range.
175
      $seek_start = isset( $matches[ 1 ] ) ? $matches[ 1 ] + 0 : 0;
176
      $seek_end = isset( $matches[ 2 ] ) ? $matches[ 2 ] + 0 : $size - 1;
177
      $range_bytes = $seek_start . '-' . $seek_end . '/' . $size;
178
      $content_length = $seek_end - $seek_start + 1;
179
180
      header( 'HTTP/1.1 206 Partial Content' );
181
      header( "Content-Range: bytes $range_bytes" );
182
    }
183
184
    return array( $seek_start, $content_length );
185
  }
186
187
  /**
188
   * Transmits a file from the server to the client.
189
   *
190
   * @param string $filename File to download, must be this script directory.
191
   * @param int $seek_start Offset into file to start downloading.
192
   * @param int $size Total size of the file.
193
   *
194
   * @return bool True if the file was transferred.
195
   */
196
  function transmit( $filename, $seek_start, $size ) {
197
    // Buffer after sending HTTP headers to allow client download estimates.
198
    if( ob_get_level() == 0 ) {
199
      ob_start();
200
    }
201
202
    // Don't count missing files as download hits.
203
    $bytes_sent = -1;
204
205
    // Open the file to be downloaded.
206
    $fp = @fopen( $filename, 'rb' );
207
208
    if( $fp !== false ) {
209
      @fseek( $fp, $seek_start );
210
211
      $aborted = false;
212
      $bytes_sent = $seek_start;
213
      $chunk_size = 1024 * 16;
214
215
      while( !feof( $fp ) && !$aborted ) {
216
        // Stream the file.
217
        print( @fread( $fp, $chunk_size ) );
218
219
        // Track running total of bytes sent.
220
        $bytes_sent += $chunk_size;
221
222
        // Send the file to download in small chunks.
223
        if( ob_get_level() > 0 ) {
224
          ob_flush();
225
        }
226
227
        flush();
228
229
        // Chunking the file allows detecting when the connection has closed.
230
        $aborted = connection_aborted() || connection_status() != 0;
231
      }
232
233
      // Indicate that transmission is complete.
234
      if( ob_get_level() > 0 ) {
235
        ob_end_flush();
236
      }
237
238
      fclose( $fp );
239
    }
240
241
    // Download succeeded if the total bytes matches or exceeds the file size.
242
    return $bytes_sent >= $size;
243
  }
244
245
  /**
246
   * Increments the number in a file using an exclusive lock. The file
247
   * is set to an initial value set to 0 if it doesn't exist.
248
   *
249
   * @param string $filename The file containing a number to increment.
250
   */
251
  function increment_count( $filename ) {
252
    try {
253
      lock_open( $filename );
254
255
      // Coerce value to largest natural numeric data type.
256
      $count = @file_get_contents( $filename ) + 0;
257
258
      // Write the new counter value.
259
      file_put_contents( $filename, $count + 1 );
260
    }
261
    finally {
262
      lock_close( $filename );
263
    }
264
  }
265
266
  /**
267
   * Acquires a lock for a particular file. Callers would be prudent to
268
   * call this function from within a try/finally block and close the lock
269
   * in the finally section. The amount of time between opening and closing
270
   * the lock must be minimal because parallel processes will be waiting on
271
   * the lock's release.
272
   *
273
   * @param string $filename The name of file to lock.
274
   *
275
   * @return bool True if the lock was obtained, false upon excessive attempts.
276
   */
277
  function lock_open( $filename ) {
278
    $lockdir = create_lock_filename( $filename );
279
280
    // Track the number of times a lock attempt is made.
281
    $iterations = 0;
282
283
    do {
284
      // Creates and tests lock file existence atomically.
285
      if( @mkdir( $lockdir, 0777 ) ) {
286
        // Exit the loop.
287
        $iterations = 0;
288
      }
289
      else {
290
        $iterations++;
291
        $lifetime = time() - filemtime( $lockdir );
292
293
        if( $lifetime > 10 ) {
294
          // If the lock has gone stale, delete it.
295
          @rmdir( $lockdir );
296
        }
297
        else {
298
          // Wait a random duration to avoid concurrency conflicts.
299
          usleep( rand( 1000, 10000 ) );
300
        }
301
      }
302
    }
303
    while( $iterations > 0 && $iterations < 10 );
304
305
    // Indicate whether the maximum number of lock attempts were exceeded.
306
    return $iterations == 0;
307
  }
308
309
  /**
310
   * Releases the lock on a particular file.
311
   *
312
   * @param string $filename The name of file that was locked.
313
   */
314
  function lock_close( $filename ) {
315
    @rmdir( create_lock_filename( $filename ) );
316
  }
317
318
  /**
319
   * Creates a uniquely named lock directory name.
320
   *
321
   * @param string $filename The name of the file under contention.
322
   *
323
   * @return string A unique lock file reference for the given file name.
324
   */
325
  function create_lock_filename( $filename ) {
326
    return $filename .'.lock';
327
  }
328
?>
1329
M www/images/icons/apple.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   viewBox="0 0 157.33104 75"
4
   version="1.1"
5
   id="svg1"
6
   width="157.33104"
7
   height="75"
8
   xmlns="http://www.w3.org/2000/svg"
9
   xmlns:svg="http://www.w3.org/2000/svg">
10
  <defs
11
     id="defs1" />
12
  <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
13
  <path
14
     d="m 56.86155,15.62503 c 0,-1.72852 -1.39648,-3.125 -3.125,-3.125 -1.72852,0 -3.125,1.39648 -3.125,3.125 V 39.3262 l -7.16797,-7.16797 c -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 -1.2207,1.22071 -1.2207,3.20313 0,4.42383 l 12.5,12.5 c 1.22071,1.2207 3.20313,1.2207 4.42383,0 l 12.5,-12.5 c 1.2207,-1.2207 1.2207,-3.20312 0,-4.42383 -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 l -7.1582,7.16797 z m -21.875,31.25 c -3.44727,0 -6.25,2.80273 -6.25,6.25 v 3.125 c 0,3.44727 2.80273,6.25 6.25,6.25 h 37.5 c 3.44727,0 6.25,-2.80273 6.25,-6.25 v -3.125 c 0,-3.44727 -2.80273,-6.25 -6.25,-6.25 h -9.91211 l -4.42383,4.42383 c -2.4414,2.4414 -6.39648,2.4414 -8.83789,0 l -4.41406,-4.42383 z m 35.9375,10.15625 c -1.29883,0 -2.34375,-1.04492 -2.34375,-2.34375 0,-1.29883 1.04492,-2.34375 2.34375,-2.34375 1.29883,0 2.34375,1.04492 2.34375,2.34375 0,1.29883 -1.04492,2.34375 -2.34375,2.34375 z"
15
     id="path16323"
16
     style="fill:#000000;fill-opacity:1;stroke-width:0.0976562" />
17
  <path
18
     d="m 121.70706,38.92224 c -0.0223,-4.09673 1.83069,-7.18881 5.58138,-9.46601 -2.0986,-3.00277 -5.26882,-4.65486 -9.45485,-4.97858 -3.96277,-0.31256 -8.29392,2.31069 -9.87903,2.31069 -1.67441,0 -5.51439,-2.19906 -8.52833,-2.19906 -6.22882,0.10046 -12.84832,4.96742 -12.84832,14.86877 q 0,4.38696 1.60743,9.06415 c 1.42883,4.09673 6.58602,14.1432 11.96646,13.97576 2.81303,-0.067 4.79998,-1.99813 8.46136,-1.99813 3.54976,0 5.39161,1.99813 8.52834,1.99813 5.4251,-0.0781 10.09112,-9.20927 11.45298,-13.31716 -7.27811,-3.42696 -6.88742,-10.04647 -6.88742,-10.25856 z m -6.31811,-18.32922 c 3.04743,-3.61673 2.76836,-6.90974 2.67906,-8.09299 -2.69022,0.15627 -5.80463,1.83069 -7.5795,3.89579 -1.95348,2.21023 -3.10325,4.9451 -2.85767,8.02602 2.91348,0.22325 5.57021,-1.27256 7.75811,-3.82882 z"
19
     id="path5811"
20
     style="fill:#000000;fill-opacity:1;stroke-width:0.111627" />
21
</svg>
22
1
<svg width="157.331" height="75" xmlns="http://www.w3.org/2000/svg"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc.--><path d="M56.862 15.625a3.122 3.122 0 0 0-3.125-3.125 3.122 3.122 0 0 0-3.125 3.125v23.701l-7.168-7.168a3.13 3.13 0 0 0-4.424 0 3.13 3.13 0 0 0 0 4.424l12.5 12.5a3.13 3.13 0 0 0 4.424 0l12.5-12.5a3.13 3.13 0 0 0 0-4.424 3.13 3.13 0 0 0-4.424 0l-7.158 7.168zm-21.875 31.25a6.256 6.256 0 0 0-6.25 6.25v3.125a6.256 6.256 0 0 0 6.25 6.25h37.5a6.256 6.256 0 0 0 6.25-6.25v-3.125a6.256 6.256 0 0 0-6.25-6.25h-9.913l-4.423 4.424a6.248 6.248 0 0 1-8.838 0l-4.414-4.424zm35.937 10.156a2.338 2.338 0 0 1-2.344-2.343 2.338 2.338 0 0 1 2.344-2.344 2.338 2.338 0 0 1 2.344 2.344 2.338 2.338 0 0 1-2.344 2.343z" style="fill:#000;fill-opacity:1;stroke-width:.0976562"/><path d="M121.707 38.922c-.022-4.096 1.83-7.189 5.581-9.466-2.098-3.003-5.268-4.655-9.454-4.978-3.963-.313-8.294 2.31-9.88 2.31-1.674 0-5.514-2.199-8.528-2.199-6.229.1-12.848 4.968-12.848 14.87q0 4.386 1.607 9.063c1.43 4.097 6.586 14.143 11.967 13.976 2.813-.067 4.8-1.998 8.461-1.998 3.55 0 5.392 1.998 8.528 1.998 5.426-.078 10.092-9.21 11.453-13.317-7.278-3.427-6.887-10.047-6.887-10.259zm-6.318-18.329c3.047-3.617 2.768-6.91 2.679-8.093-2.69.156-5.805 1.83-7.58 3.896-1.953 2.21-3.103 4.945-2.857 8.026 2.913.223 5.57-1.273 7.758-3.829z" style="fill:#000;fill-opacity:1;stroke-width:.111627"/></svg>
M www/images/icons/java.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   viewBox="0 0 157.33104 75"
4
   version="1.1"
5
   id="svg1"
6
   width="157.33104"
7
   height="75"
8
   xmlns="http://www.w3.org/2000/svg"
9
   xmlns:svg="http://www.w3.org/2000/svg">
10
  <defs
11
     id="defs1" />
12
  <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
13
  <path
14
     d="m 53.68516,15.625 c 0,-1.72852 -1.39648,-3.125 -3.125,-3.125 -1.72852,0 -3.125,1.39648 -3.125,3.125 V 39.32617 L 40.26719,32.1582 c -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 -1.2207,1.22071 -1.2207,3.20313 0,4.42383 l 12.5,12.5 c 1.22071,1.2207 3.20313,1.2207 4.42383,0 l 12.5,-12.5 c 1.2207,-1.2207 1.2207,-3.20312 0,-4.42383 -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 l -7.1582,7.16797 z m -21.875,31.25 c -3.44727,0 -6.25,2.80273 -6.25,6.25 v 3.125 c 0,3.44727 2.80273,6.25 6.25,6.25 h 37.5 c 3.44727,0 6.25,-2.80273 6.25,-6.25 v -3.125 c 0,-3.44727 -2.80273,-6.25 -6.25,-6.25 h -9.91211 l -4.42383,4.42383 c -2.4414,2.4414 -6.39648,2.4414 -8.83789,0 L 41.72227,46.875 Z m 35.9375,10.15625 c -1.29883,0 -2.34375,-1.04492 -2.34375,-2.34375 0,-1.29883 1.04492,-2.34375 2.34375,-2.34375 1.29883,0 2.34375,1.04492 2.34375,2.34375 0,1.29883 -1.04492,2.34375 -2.34375,2.34375 z"
15
     id="path16474"
16
     style="fill:#000000;fill-opacity:1;stroke-width:0.0976562" />
17
  <path
18
     d="m 121.74171,43.05624 c 0.95702,-0.65429 2.28513,-1.22069 2.28513,-1.22069 0,0 -3.77925,0.68359 -7.53897,0.99608 -4.59954,0.38086 -9.54088,0.45898 -12.02132,0.12695 -5.86906,-0.78124 3.22261,-2.93941 3.22261,-2.93941 0,0 -3.52534,-0.23437 -7.87099,1.85544 -5.12688,2.48044 12.69515,3.61324 21.92354,1.18163 z m -8.33973,-3.13473 c -1.85545,-4.16986 -8.11513,-7.83192 0,-14.23809 C 123.52489,17.69525 118.33355,12.5 118.33355,12.5 c 2.09959,8.25185 -7.38271,10.75181 -10.8104,15.8787 -2.33395,3.50581 1.14256,7.26553 5.87883,11.54281 z m 11.19126,-17.2068 c 0.01,0 -17.10915,4.27729 -8.93543,13.69123 2.41208,2.7734 -0.63476,5.27336 -0.63476,5.27336 0,0 6.12297,-3.16402 3.3105,-7.11904 -2.62691,-3.69136 -4.63861,-5.52727 6.25969,-11.84555 z m -0.5957,26.41567 a 1.190414,1.190414 0 0 1 -0.19531,0.2539 c 12.52914,-3.29097 7.91982,-11.61117 1.93357,-9.50183 a 1.6923604,1.6923604 0 0 0 -0.80077,0.61523 6.879792,6.879792 0 0 1 1.0742,-0.29297 c 3.02731,-0.63475 7.37296,4.05268 -2.01169,8.92567 z m 4.60541,6.0839 c 0,0 1.416,1.1621 -1.55272,2.07029 -5.65422,1.70896 -23.51531,2.22653 -28.47618,0.0684 -1.78709,-0.77147 1.56248,-1.85544 2.61715,-2.08005 1.09374,-0.23437 1.72849,-0.19531 1.72849,-0.19531 -1.98239,-1.39646 -12.82209,2.74411 -5.50773,3.92573 19.94504,3.24215 36.3667,-1.45506 31.19099,-3.78901 z m -21.83174,-4.04291 c -7.68545,2.1484 4.67767,6.58194 14.4627,2.39254 a 18.15308,18.15308 0 0 1 -2.75387,-1.34764 c -4.36518,0.83007 -6.38664,0.88866 -10.35143,0.43945 -3.27144,-0.37109 -1.3574,-1.48435 -1.3574,-1.48435 z m 17.55836,9.49206 c -7.68545,1.44529 -17.16774,1.27928 -22.7829,0.35156 0,-0.01 1.15232,0.94725 7.07022,1.3281 9.00378,0.57617 22.83173,-0.32226 23.15399,-4.58001 0,0 -0.62499,1.6113 -7.44131,2.90035 z M 120.07181,46.9722 c -5.78117,1.11327 -9.13074,1.08397 -13.3592,0.64453 -3.27144,-0.3418 -1.13279,-1.92381 -1.13279,-1.92381 -8.47645,2.81247 4.70696,5.99602 16.55251,2.52927 a 5.89543,5.89543 0 0 1 -2.06052,-1.24999 z"
19
     id="path5936"
20
     style="fill:#000000;fill-opacity:1;stroke-width:0.097655" />
21
</svg>
22
1
<svg width="157.331" height="75" xmlns="http://www.w3.org/2000/svg"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc.--><path d="M53.685 15.625A3.122 3.122 0 0 0 50.56 12.5a3.122 3.122 0 0 0-3.125 3.125v23.701l-7.168-7.168a3.13 3.13 0 0 0-4.424 0 3.13 3.13 0 0 0 0 4.424l12.5 12.5a3.13 3.13 0 0 0 4.424 0l12.5-12.5a3.13 3.13 0 0 0 0-4.424 3.13 3.13 0 0 0-4.424 0l-7.158 7.168zM31.81 46.875a6.256 6.256 0 0 0-6.25 6.25v3.125a6.256 6.256 0 0 0 6.25 6.25h37.5a6.256 6.256 0 0 0 6.25-6.25v-3.125a6.256 6.256 0 0 0-6.25-6.25h-9.912l-4.424 4.424a6.248 6.248 0 0 1-8.838 0l-4.414-4.424Zm35.938 10.156a2.338 2.338 0 0 1-2.344-2.343 2.338 2.338 0 0 1 2.344-2.344 2.338 2.338 0 0 1 2.343 2.343 2.338 2.338 0 0 1-2.343 2.344z" style="fill:#000;fill-opacity:1;stroke-width:.0976562"/><path d="M121.742 43.056c.957-.654 2.285-1.22 2.285-1.22s-3.78.683-7.54.996c-4.599.38-9.54.459-12.02.127-5.87-.782 3.222-2.94 3.222-2.94s-3.525-.234-7.87 1.856c-5.128 2.48 12.694 3.613 21.923 1.181zm-8.34-3.134c-1.855-4.17-8.115-7.832 0-14.239 10.123-7.988 4.932-13.183 4.932-13.183 2.1 8.252-7.383 10.752-10.81 15.879-2.335 3.506 1.142 7.265 5.878 11.543zm11.191-17.207c.01 0-17.109 4.277-8.935 13.69 2.412 2.774-.635 5.274-.635 5.274s6.123-3.164 3.31-7.119c-2.626-3.691-4.638-5.527 6.26-11.845zm-.595 26.415a1.19 1.19 0 0 1-.196.254c12.53-3.29 7.92-11.61 1.934-9.502a1.692 1.692 0 0 0-.801.616 6.88 6.88 0 0 1 1.074-.293c3.028-.635 7.373 4.052-2.011 8.925zm4.605 6.084s1.416 1.162-1.553 2.07c-5.654 1.71-23.515 2.227-28.476.069-1.787-.772 1.563-1.855 2.617-2.08 1.094-.234 1.729-.195 1.729-.195-1.983-1.397-12.822 2.744-5.508 3.925 19.945 3.242 36.367-1.455 31.19-3.789zm-21.832-4.043c-7.685 2.149 4.678 6.582 14.463 2.393a18.153 18.153 0 0 1-2.754-1.348c-4.365.83-6.387.889-10.351.44-3.272-.371-1.358-1.485-1.358-1.485zm17.559 9.492c-7.686 1.446-17.168 1.28-22.783.352 0-.01 1.152.947 7.07 1.328 9.004.576 22.832-.322 23.154-4.58 0 0-.625 1.611-7.441 2.9zm-4.258-13.69c-5.781 1.112-9.13 1.083-13.36.644-3.27-.342-1.132-1.924-1.132-1.924-8.477 2.812 4.707 5.996 16.552 2.53a5.895 5.895 0 0 1-2.06-1.25z" style="fill:#000;fill-opacity:1;stroke-width:.097655"/></svg>
M www/images/icons/linux.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   viewBox="0 0 157.33104 75"
4
   version="1.1"
5
   id="svg1"
6
   width="157.33104"
7
   height="75"
8
   xmlns="http://www.w3.org/2000/svg"
9
   xmlns:svg="http://www.w3.org/2000/svg">
10
  <defs
11
     id="defs1" />
12
  <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
13
  <path
14
     d="m 111.9104,24.539926 c 0.0976,0.0488 0.17577,0.166 0.29295,0.166 0.10741,0 0.27341,-0.039 0.28318,-0.14647 0.0196,-0.13671 -0.18553,-0.22459 -0.31248,-0.28318 -0.166,-0.0684 -0.38082,-0.0976 -0.53706,-0.01 -0.039,0.0195 -0.0781,0.0684 -0.0586,0.10741 0.0293,0.12695 0.22459,0.10742 0.332,0.166 z m -2.13849,0.166 c 0.11718,0 0.1953,-0.11718 0.29294,-0.166 0.10742,-0.0586 0.30271,-0.039 0.34177,-0.15624 0.0195,-0.0391 -0.0195,-0.0879 -0.0586,-0.10741 -0.15623,-0.0879 -0.37106,-0.0586 -0.53706,0.01 -0.12694,0.0586 -0.33201,0.14647 -0.31248,0.28318 0.01,0.0976 0.17577,0.14647 0.27342,0.13671 z m 21.59,27.22431 c -0.35153,-0.3906 -0.51753,-1.13273 -0.70306,-1.92368 -0.17577,-0.79095 -0.38083,-1.64048 -1.02531,-2.18731 -0.12694,-0.10742 -0.25388,-0.20506 -0.39059,-0.28318 -0.12694,-0.0781 -0.26365,-0.14648 -0.40036,-0.1953 0.89836,-2.66579 0.54683,-5.32182 -0.3613,-7.72397 -1.11318,-2.9392 -3.05638,-5.50735 -4.54063,-7.26502 -1.66979,-2.09944 -3.29075,-4.09146 -3.26146,-7.03067 0.0488,-4.48205 0.49801,-12.81143 -7.40172,-12.8212 -9.99918,-0.0196 -7.49938,10.09682 -7.6068,13.20203 -0.166,2.28497 -0.62495,4.08169 -2.19708,6.31784 -1.84555,2.19708 -4.442986,5.74171 -5.673356,9.44257 -0.58588,1.7479 -0.8593,3.5251 -0.60541,5.20465 -0.63472,0.56636 -1.11319,1.43543 -1.62096,1.97249 -0.41012,0.41989 -1.00578,0.57612 -1.66002,0.81048 -0.65424,0.23436 -1.36708,0.58589 -1.8065,1.4159 -0.20506,0.38083 -0.27341,0.79095 -0.27341,1.21084 0,0.38083 0.0586,0.77142 0.11718,1.15225 0.11717,0.79095 0.24412,1.53307 0.0781,2.03108 -0.50777,1.40613 -0.57613,2.38261 -0.21483,3.09544 0.37106,0.71283 1.11319,1.02531 1.96273,1.20108 1.68931,0.35153 3.98405,0.26365 5.79054,1.2206 1.933426,1.01554 3.896156,1.37684 5.458526,1.01554 1.13271,-0.25389 2.06037,-0.93742 2.52908,-1.97249 1.22061,-0.01 2.56815,-0.5273 4.71641,-0.64448 1.45496,-0.11718 3.28098,0.51753 5.38041,0.40035 0.0586,0.2246 0.13671,0.44919 0.24412,0.65425 v 0.01 c 0.81048,1.63072 2.32403,2.37285 3.93522,2.2459 1.62097,-0.12694 3.32981,-1.07413 4.71641,-2.72438 1.32802,-1.60143 3.51533,-2.26544 4.97029,-3.14427 0.7226,-0.43941 1.30849,-0.98624 1.35731,-1.78696 0.039,-0.80071 -0.42965,-1.68931 -1.51355,-2.90015 z m -19.16833,-30.90565 c 0.95695,-2.16779 3.33957,-2.12873 4.29652,-0.0391 0.63471,1.38661 0.35153,3.01733 -0.41989,3.94499 -0.15623,-0.0781 -0.57612,-0.25388 -1.23036,-0.47847 0.10741,-0.11718 0.30271,-0.26365 0.38082,-0.44919 0.46872,-1.15224 -0.0195,-2.6365 -0.88859,-2.66579 -0.71284,-0.0488 -1.35731,1.0546 -1.15225,2.24591 -0.40036,-0.1953 -0.91789,-0.34177 -1.26943,-0.42965 -0.0976,-0.67378 -0.0293,-1.42567 0.28318,-2.12874 z m -3.97428,-1.12295 c 0.98625,0 2.03108,1.3866 1.86508,3.27122 -0.34177,0.0976 -0.6933,0.24412 -0.99601,0.44918 0.11718,-0.86907 -0.32224,-1.96274 -0.93742,-1.91391 -0.82025,0.0684 -0.95696,2.07014 -0.17577,2.74392 0.0977,0.0781 0.18553,-0.0195 -0.57613,0.53706 -1.52331,-1.42566 -1.0253,-5.08747 0.82025,-5.08747 z m -1.32802,5.92724 c 0.60542,-0.44918 1.32802,-0.97648 1.37685,-1.0253 0.45894,-0.42965 1.31825,-1.3866 2.72438,-1.3866 0.6933,0 1.52331,0.22459 2.52909,0.86906 0.61518,0.40036 1.10342,0.42966 2.20684,0.90813 0.82025,0.34177 1.33778,0.94719 1.02531,1.7772 -0.25389,0.6933 -1.07413,1.40613 -2.21661,1.76743 -1.0839,0.35153 -1.93344,1.56237 -3.73016,1.45496 -0.38083,-0.0196 -0.68354,-0.0977 -0.93742,-0.20507 -0.78119,-0.34176 -1.19131,-1.01554 -1.95297,-1.46472 -0.83977,-0.46871 -1.28896,-1.01554 -1.43543,-1.49401 -0.13671,-0.47848 0,-0.87884 0.41012,-1.20108 z m 0.32224,32.61449 c -0.26365,3.42745 -4.28675,3.35909 -7.352896,1.75766 -2.91968,-1.54284 -6.69866,-0.63471 -7.47009,-2.13849 -0.23435,-0.45895 -0.23435,-1.24013 0.25389,-2.57791 v -0.0195 c 0.23435,-0.74213 0.0586,-1.56237 -0.0586,-2.33379 -0.11718,-0.76166 -0.17577,-1.46472 0.0879,-1.95297 0.34177,-0.65424 0.83002,-0.8886 1.4452,-1.10342 1.00578,-0.3613 1.15225,-0.33201 1.9139,-0.96672 0.53707,-0.55659 0.92766,-1.25966 1.39637,-1.75766 0.49801,-0.53707 0.97648,-0.79095 1.72837,-0.67378 0.79095,0.11718 1.474486,0.66401 2.138496,1.56237 l 1.9139,3.47628 c 0.92766,1.9432 4.20863,4.72617 4.00357,6.72796 z m -0.1367,-2.52909 c -0.40036,-0.64448 -0.93743,-1.32801 -1.40614,-1.9139 0.6933,0 1.38661,-0.21483 1.63073,-0.86907 0.22459,-0.60542 0,-1.45496 -0.7226,-2.43144 -1.31825,-1.7772 -3.73992,-3.17357 -3.73992,-3.17357 -1.31825,-0.82024 -2.06038,-1.82602 -2.40215,-2.91968 -0.34176,-1.09366 -0.29294,-2.2752 -0.0293,-3.43721 0.50777,-2.23614 1.81626,-4.4137 2.65603,-5.78077 0.22459,-0.16601 0.0781,0.31247 -0.84954,2.03108 -0.83,1.57214 -2.382606,5.20465 -0.25388,8.04621 0.0586,-2.02132 0.53706,-4.08169 1.34754,-6.00536 1.17178,-2.67556 3.64228,-7.31385 3.83757,-11.00495 0.10742,0.0781 0.44919,0.31247 0.60543,0.40036 0.44918,0.26365 0.79095,0.65424 1.23036,1.00577 1.21084,0.97648 2.78298,0.89836 4.14028,0.11718 0.60542,-0.34177 1.09366,-0.73236 1.55261,-0.87883 0.96672,-0.30271 1.73814,-0.83978 2.17755,-1.46473 0.75189,2.96851 2.50956,7.25526 3.63251,9.34493 0.59566,1.11319 1.78697,3.46651 2.3045,6.30807 0.32224,-0.01 0.68354,0.039 1.06437,0.13671 1.34754,-3.48604 -1.14249,-7.24549 -2.27521,-8.29033 -0.45894,-0.44918 -0.47847,-0.64448 -0.25388,-0.63471 1.23037,1.09366 2.85133,3.29074 3.43722,5.76124 0.27341,1.13272 0.32223,2.31426 0.039,3.48604 1.60143,0.66401 3.50557,1.7479 2.9978,3.39815 -0.21483,-0.01 -0.31248,0 -0.41013,0 0.31248,-0.98624 -0.38082,-1.7186 -2.22637,-2.54861 -1.91391,-0.83978 -3.51534,-0.83978 -3.73993,1.2206 -1.18154,0.41012 -1.78696,1.43543 -2.08967,2.66579 -0.27341,1.09366 -0.35153,2.41191 -0.42965,3.89617 -0.0488,0.75189 -0.35154,1.75766 -0.66401,2.83179 -3.1345,2.23615 -7.48961,3.21263 -11.16118,0.70307 z m 25.13463,-1.12295 c -0.0879,1.64049 -4.0231,1.94319 -6.17136,4.54063 -1.28895,1.53308 -2.87085,2.38262 -4.25746,2.49003 -1.38661,0.10741 -2.58768,-0.46871 -3.29074,-1.88461 -0.45895,-1.08389 -0.23436,-2.25567 0.10741,-3.54462 0.3613,-1.38661 0.89836,-2.81227 0.96672,-3.96452 0.0781,-1.48425 0.166,-2.78297 0.41012,-3.77898 0.25388,-1.00578 0.64448,-1.67955 1.33778,-2.06038 0.0293,-0.0195 0.0684,-0.0293 0.0976,-0.0488 0.0781,1.28895 0.71284,2.59744 1.83579,2.88062 1.23037,0.32223 2.9978,-0.73237 3.74969,-1.59167 0.87884,-0.0293 1.53308,-0.0879 2.20685,0.49801 0.96672,0.83001 0.6933,2.95874 1.66978,4.06216 1.03507,1.13272 1.36708,1.90414 1.33778,2.40215 z m -24.93934,-27.77114 c 0.1953,0.18553 0.45895,0.43942 0.78119,0.6933 0.64448,0.50777 1.54284,1.03507 2.6658,1.03507 1.13271,0 2.19708,-0.57612 3.10521,-1.0546 0.47847,-0.25388 1.06436,-0.68353 1.44519,-1.01554 0.38083,-0.332 0.57612,-0.61518 0.30271,-0.64447 -0.27342,-0.0293 -0.25389,0.25388 -0.58589,0.498 -0.42965,0.31248 -0.94719,0.7226 -1.35731,0.95695 -0.7226,0.41012 -1.90414,0.99601 -2.91968,0.99601 -1.01554,0 -1.82602,-0.46871 -2.43144,-0.94718 -0.30271,-0.24412 -0.55659,-0.48824 -0.75189,-0.67378 -0.14647,-0.1367 -0.18553,-0.44918 -0.41989,-0.47847 -0.13671,-0.01 -0.17577,0.3613 0.166,0.63471 z"
15
     id="path6061"
16
     style="fill:#000000;fill-opacity:1;stroke-width:0.097648" />
17
  <path
18
     d="m 52.578354,15.625006 c 0,-1.72852 -1.39648,-3.125 -3.125,-3.125 -1.72852,0 -3.125,1.39648 -3.125,3.125 v 23.70117 l -7.16797,-7.16797 c -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 -1.2207,1.22071 -1.2207,3.20313 0,4.42383 l 12.5,12.5 c 1.22071,1.2207 3.20313,1.2207 4.42383,0 l 12.5,-12.5 c 1.2207,-1.2207 1.2207,-3.20312 0,-4.42383 -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 l -7.1582,7.16797 z m -21.875,31.25 c -3.44727,0 -6.25,2.80273 -6.25,6.25 v 3.125 c 0,3.44727 2.80273,6.25 6.25,6.25 h 37.5 c 3.44727,0 6.25,-2.80273 6.25,-6.25 v -3.125 c 0,-3.44727 -2.80273,-6.25 -6.25,-6.25 h -9.91211 l -4.42383,4.42383 c -2.4414,2.4414 -6.39648,2.4414 -8.83789,0 l -4.41406,-4.42383 z m 35.9375,10.15625 c -1.29883,0 -2.34375,-1.04492 -2.34375,-2.34375 0,-1.29883 1.04492,-2.34375 2.34375,-2.34375 1.29883,0 2.34375,1.04492 2.34375,2.34375 0,1.29883 -1.04492,2.34375 -2.34375,2.34375 z"
19
     id="path15448"
20
     style="fill:#000000;fill-opacity:1;stroke-width:0.0976562" />
21
</svg>
22
1
<svg width="157.331" height="75" xmlns="http://www.w3.org/2000/svg"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc.--><path d="M111.91 24.54c.098.049.176.166.293.166.108 0 .274-.04.284-.147.02-.136-.186-.224-.313-.283-.166-.068-.38-.097-.537-.01-.039.02-.078.069-.059.108.03.127.225.107.332.166zm-2.138.166c.117 0 .195-.117.293-.166.107-.059.303-.04.342-.156.02-.04-.02-.088-.059-.108-.156-.088-.371-.058-.537.01-.127.059-.332.147-.313.283.01.098.176.147.274.137zm21.59 27.224c-.352-.39-.518-1.132-.703-1.923-.176-.791-.381-1.64-1.025-2.188a2.639 2.639 0 0 0-.391-.283 2.167 2.167 0 0 0-.4-.195c.898-2.666.546-5.322-.362-7.724-1.113-2.94-3.056-5.508-4.54-7.265-1.67-2.1-3.291-4.092-3.262-7.03.049-4.483.498-12.812-7.402-12.822-9.999-.02-7.499 10.097-7.606 13.202-.166 2.285-.625 4.082-2.197 6.318-1.846 2.197-4.443 5.741-5.674 9.442-.586 1.748-.86 3.525-.605 5.205-.635.566-1.113 1.435-1.621 1.972-.41.42-1.006.577-1.66.811-.654.234-1.367.586-1.807 1.416-.205.38-.273.79-.273 1.21 0 .382.059.772.117 1.153.117.79.244 1.533.078 2.031-.508 1.406-.576 2.383-.215 3.095.371.713 1.114 1.026 1.963 1.202 1.69.351 3.984.263 5.79 1.22 1.934 1.016 3.897 1.377 5.46 1.016 1.132-.254 2.06-.938 2.528-1.973 1.22-.01 2.568-.527 4.717-.644 1.455-.117 3.28.517 5.38.4.059.225.137.45.244.654v.01c.81 1.631 2.324 2.373 3.935 2.246 1.621-.127 3.33-1.074 4.717-2.724 1.328-1.602 3.515-2.266 4.97-3.144.723-.44 1.309-.987 1.357-1.787.04-.801-.43-1.69-1.513-2.9zm-19.168-30.905c.957-2.168 3.34-2.13 4.296-.04.635 1.387.352 3.018-.42 3.945-.156-.078-.576-.253-1.23-.478.107-.117.303-.264.38-.45.47-1.151-.019-2.636-.888-2.665-.713-.049-1.357 1.055-1.152 2.246-.4-.195-.918-.342-1.27-.43a3.89 3.89 0 0 1 .284-2.128zm-3.975-1.123c.987 0 2.031 1.386 1.865 3.27a3.511 3.511 0 0 0-.996.45c.118-.869-.322-1.963-.937-1.914-.82.069-.957 2.07-.176 2.744.098.078.186-.02-.576.537-1.523-1.426-1.025-5.087.82-5.087zm-1.328 5.927c.606-.45 1.328-.977 1.377-1.025.46-.43 1.318-1.387 2.725-1.387.693 0 1.523.225 2.529.869.615.4 1.103.43 2.206.908.82.342 1.338.947 1.026 1.777-.254.694-1.074 1.406-2.217 1.768-1.084.351-1.933 1.562-3.73 1.455a2.722 2.722 0 0 1-.937-.205c-.782-.342-1.192-1.016-1.953-1.465-.84-.469-1.29-1.016-1.436-1.494-.137-.479 0-.879.41-1.201zm.323 32.614c-.264 3.428-4.287 3.36-7.353 1.758-2.92-1.543-6.699-.635-7.47-2.138-.235-.46-.235-1.24.253-2.578v-.02c.235-.742.059-1.562-.058-2.334-.117-.761-.176-1.464.088-1.953.341-.654.83-.888 1.445-1.103 1.006-.361 1.152-.332 1.914-.967.537-.556.927-1.26 1.396-1.757.498-.538.977-.791 1.729-.674.79.117 1.474.664 2.138 1.562l1.914 3.476c.928 1.944 4.209 4.727 4.004 6.728zm-.137-2.529c-.4-.644-.938-1.328-1.406-1.914.693 0 1.386-.214 1.63-.869.225-.605 0-1.455-.722-2.431-1.318-1.777-3.74-3.174-3.74-3.174-1.318-.82-2.06-1.826-2.402-2.92-.342-1.093-.293-2.275-.03-3.437.508-2.236 1.817-4.413 2.656-5.78.225-.166.079.312-.85 2.03-.83 1.573-2.382 5.205-.253 8.047.059-2.021.537-4.082 1.348-6.005 1.171-2.676 3.642-7.314 3.837-11.005.108.078.45.312.606.4.449.264.79.654 1.23 1.006 1.21.976 2.783.898 4.14.117.606-.342 1.094-.732 1.553-.879.967-.303 1.738-.84 2.177-1.465.752 2.969 2.51 7.256 3.633 9.345.596 1.113 1.787 3.467 2.304 6.308.323-.01.684.04 1.065.137 1.347-3.486-1.143-7.245-2.275-8.29-.46-.45-.479-.645-.254-.635 1.23 1.094 2.851 3.29 3.437 5.761.273 1.133.322 2.315.039 3.486 1.601.664 3.505 1.748 2.998 3.398-.215-.01-.313 0-.41 0 .312-.986-.381-1.718-2.227-2.548-1.914-.84-3.515-.84-3.74 1.22-1.181.41-1.787 1.436-2.09 2.666-.273 1.094-.35 2.412-.43 3.896-.048.752-.35 1.758-.663 2.832-3.135 2.236-7.49 3.213-11.161.703zm25.134-1.123c-.087 1.64-4.023 1.944-6.17 4.541-1.29 1.533-2.872 2.383-4.258 2.49-1.387.107-2.588-.469-3.291-1.885-.46-1.084-.234-2.255.107-3.544.362-1.387.899-2.813.967-3.965.078-1.484.166-2.783.41-3.779.254-1.006.645-1.68 1.338-2.06.03-.02.068-.03.098-.049.078 1.289.712 2.598 1.835 2.88 1.23.323 2.998-.732 3.75-1.591.879-.03 1.533-.088 2.207.498.967.83.693 2.959 1.67 4.062 1.035 1.133 1.367 1.904 1.337 2.402zm-24.939-27.77c.195.185.46.439.781.692.645.508 1.543 1.036 2.666 1.036 1.133 0 2.197-.577 3.105-1.055.479-.254 1.065-.684 1.446-1.016.38-.332.576-.615.302-.644-.273-.03-.254.254-.586.498-.43.312-.947.723-1.357.957-.723.41-1.904.996-2.92.996-1.015 0-1.826-.469-2.431-.947-.303-.244-.557-.489-.752-.674-.146-.137-.186-.45-.42-.479-.137-.01-.176.362.166.635z" style="fill:#000;fill-opacity:1;stroke-width:.097648"/><path d="M52.578 15.625a3.122 3.122 0 0 0-3.125-3.125 3.122 3.122 0 0 0-3.125 3.125v23.701l-7.168-7.168a3.13 3.13 0 0 0-4.423 0 3.13 3.13 0 0 0 0 4.424l12.5 12.5a3.13 3.13 0 0 0 4.423 0l12.5-12.5a3.13 3.13 0 0 0 0-4.424 3.13 3.13 0 0 0-4.423 0l-7.159 7.168zm-21.875 31.25a6.256 6.256 0 0 0-6.25 6.25v3.125a6.256 6.256 0 0 0 6.25 6.25h37.5a6.256 6.256 0 0 0 6.25-6.25v-3.125a6.256 6.256 0 0 0-6.25-6.25h-9.912l-4.424 4.424a6.248 6.248 0 0 1-8.837 0l-4.415-4.424zm35.938 10.156a2.338 2.338 0 0 1-2.344-2.343 2.338 2.338 0 0 1 2.344-2.344 2.338 2.338 0 0 1 2.344 2.344 2.338 2.338 0 0 1-2.344 2.343z" style="fill:#000;fill-opacity:1;stroke-width:.0976562"/></svg>
M www/images/icons/windows.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   viewBox="0 0 157.33104 75"
4
   version="1.1"
5
   id="svg1"
6
   width="157.33104"
7
   height="75"
8
   xmlns="http://www.w3.org/2000/svg"
9
   xmlns:svg="http://www.w3.org/2000/svg">
10
  <defs
11
     id="defs1" />
12
  <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
13
  <path
14
     d="M 83.66552,19.38616 104.15659,16.5625 V 36.36161 H 83.66552 Z m 0,36.22768 20.49107,2.82366 V 38.88393 H 83.66552 Z m 22.74554,3.125 L 133.66552,62.5 V 38.88393 h -27.25446 z m 0,-42.47768 v 20.10045 h 27.25446 V 12.5 Z"
15
     id="path6186"
16
     style="fill:#000000;fill-opacity:1;stroke-width:0.111607" />
17
  <path
18
     d="m 51.79052,15.625 c 0,-1.72852 -1.39648,-3.125 -3.125,-3.125 -1.72852,0 -3.125,1.39648 -3.125,3.125 V 39.32617 L 38.37255,32.1582 c -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 -1.2207,1.22071 -1.2207,3.20313 0,4.42383 l 12.5,12.5 c 1.22071,1.2207 3.20313,1.2207 4.42383,0 l 12.5,-12.5 c 1.2207,-1.2207 1.2207,-3.20312 0,-4.42383 -1.2207,-1.2207 -3.20312,-1.2207 -4.42383,0 l -7.1582,7.16797 z m -21.875,31.25 c -3.44727,0 -6.25,2.80273 -6.25,6.25 v 3.125 c 0,3.44727 2.80273,6.25 6.25,6.25 h 37.5 c 3.44727,0 6.25,-2.80273 6.25,-6.25 v -3.125 c 0,-3.44727 -2.80273,-6.25 -6.25,-6.25 h -9.91211 l -4.42383,4.42383 c -2.4414,2.4414 -6.39648,2.4414 -8.83789,0 L 39.82763,46.875 Z m 35.9375,10.15625 c -1.29883,0 -2.34375,-1.04492 -2.34375,-2.34375 0,-1.29883 1.04492,-2.34375 2.34375,-2.34375 1.29883,0 2.34375,1.04492 2.34375,2.34375 0,1.29883 -1.04492,2.34375 -2.34375,2.34375 z"
19
     id="path16036"
20
     style="fill:#000000;fill-opacity:1;stroke-width:0.0976562" />
21
</svg>
22
1
<svg width="157.331" height="75" xmlns="http://www.w3.org/2000/svg"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc.--><path d="m83.666 19.386 20.49-2.823v19.799h-20.49Zm0 36.228 20.49 2.823V38.884h-20.49Zm22.745 3.125 27.255 3.761V38.884H106.41zm0-42.478v20.1h27.255V12.5Z" style="fill:#000;fill-opacity:1;stroke-width:.111607"/><path d="M51.79 15.625a3.122 3.122 0 0 0-3.124-3.125 3.122 3.122 0 0 0-3.125 3.125v23.701l-7.168-7.168a3.13 3.13 0 0 0-4.424 0 3.13 3.13 0 0 0 0 4.424l12.5 12.5a3.13 3.13 0 0 0 4.424 0l12.5-12.5a3.13 3.13 0 0 0 0-4.424 3.13 3.13 0 0 0-4.424 0l-7.158 7.168zm-21.874 31.25a6.256 6.256 0 0 0-6.25 6.25v3.125a6.256 6.256 0 0 0 6.25 6.25h37.5a6.256 6.256 0 0 0 6.25-6.25v-3.125a6.256 6.256 0 0 0-6.25-6.25h-9.913l-4.423 4.424a6.248 6.248 0 0 1-8.838 0l-4.414-4.424Zm35.937 10.156a2.338 2.338 0 0 1-2.344-2.343 2.338 2.338 0 0 1 2.344-2.344 2.338 2.338 0 0 1 2.344 2.343 2.338 2.338 0 0 1-2.344 2.344z" style="fill:#000;fill-opacity:1;stroke-width:.0976562"/></svg>
M www/images/logo/title.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   width="300"
4
   height="71.784"
5
   viewBox="0 0 79.375 18.993"
6
   version="1.1"
7
   id="svg7"
8
   xmlns:xlink="http://www.w3.org/1999/xlink"
9
   xmlns="http://www.w3.org/2000/svg"
10
   xmlns:svg="http://www.w3.org/2000/svg">
11
  <defs
12
     id="defs2">
13
    <linearGradient
14
       xlink:href="#a"
15
       id="b"
16
       x1=".152"
17
       x2=".969"
18
       y1="-.045"
19
       y2="-.049"
20
       gradientTransform="matrix(-5.21456 25.531 -25.531 -5.21456 762.3 221.95)"
21
       gradientUnits="userSpaceOnUse" />
22
    <linearGradient
23
       id="a"
24
       x1=".152"
25
       x2=".969"
26
       y1="-.045"
27
       y2="-.049"
28
       gradientTransform="matrix(-5.21456 25.531 -25.531 -5.21456 762.3 221.95)"
29
       gradientUnits="userSpaceOnUse">
30
      <stop
31
         offset="0"
32
         stop-color="#ec706a"
33
         id="stop1" />
34
      <stop
35
         offset="1"
36
         stop-color="#ecd980"
37
         id="stop2" />
38
    </linearGradient>
39
  </defs>
40
  <path
41
     id="path32"
42
     style="fill:#51a9cf;fill-opacity:1"
43
     d="M 76.696905 4.2860773 C 75.760905 4.2860773 74.906182 4.5252025 74.132183 5.0038687 C 73.363517 5.4772016 72.75569 6.1470652 72.309024 7.0130643 C 71.857024 7.8677302 71.631024 8.8301007 71.631024 9.9007663 C 71.631024 10.551432 71.749377 11.151121 71.986044 11.69912 C 72.22271 12.242453 72.599183 12.680446 73.115183 13.013779 C 73.609849 13.347112 74.249645 13.514011 75.034977 13.514011 L 75.036011 13.515045 C 75.401344 13.515045 75.774515 13.471486 76.155848 13.384819 C 76.543181 13.293486 76.914668 13.167728 77.270001 13.007061 C 77.63 12.840395 77.957928 12.643578 78.253928 12.416912 C 78.554594 12.191579 78.804426 11.944165 79.003759 11.674832 L 78.278216 10.319866 C 78.132883 10.513199 77.91502 10.709648 77.625021 10.908982 C 77.334354 11.107648 77.019551 11.274547 76.680885 11.409213 C 76.342218 11.54388 76.024997 11.610753 75.728997 11.610753 C 75.341664 11.610753 75.061765 11.52782 74.889765 11.361154 C 74.717766 11.194487 74.615472 10.938419 74.582805 10.593753 L 74.582805 10.367925 C 75.255471 10.195925 75.882142 9.9917041 76.462808 9.7550376 C 77.043475 9.5130379 77.551793 9.2307205 77.987792 8.9080542 C 78.423125 8.5860545 78.761759 8.2149346 79.003759 7.794935 C 79.251092 7.3702687 79.374799 6.8890835 79.374799 6.351084 C 79.374799 5.9477511 79.27544 5.5926906 79.076106 5.2860243 C 78.882773 4.9746912 78.586815 4.7302129 78.188815 4.5522131 C 77.796149 4.3748799 77.298904 4.2860773 76.696905 4.2860773 z M 76.066964 5.8188125 C 76.28763 5.8188125 76.440888 5.8834174 76.526888 6.0120839 C 76.618888 6.1360838 76.664865 6.3406717 76.664865 6.6260048 C 76.664865 6.9160045 76.602828 7.1983219 76.478828 7.4729882 C 76.355495 7.7416546 76.189113 7.986133 75.979113 8.2067994 C 75.774447 8.4267992 75.548446 8.6044044 75.301113 8.7390709 C 75.054447 8.8737374 74.804098 8.9485318 74.550765 8.9638652 C 74.561478 8.6893636 74.596925 8.3453745 74.656186 7.9318786 C 74.714853 7.517879 74.857404 7.1312164 75.08407 6.7712167 C 75.25607 6.5018837 75.414314 6.275883 75.55898 6.0932165 C 75.704314 5.9105501 75.873631 5.8188125 76.066964 5.8188125 z " />
44
  <path
45
     id="path25"
46
     style="fill:#51a9cf;fill-opacity:1"
47
     d="M 15.616943 4.2860773 C 14.680944 4.2860773 13.826221 4.5252025 13.052222 5.0038687 C 12.283556 5.4772016 11.675729 6.1470652 11.229063 7.0130643 C 10.777063 7.8677302 10.551062 8.8301007 10.551062 9.9007663 C 10.551062 10.551432 10.669416 11.151121 10.906082 11.69912 C 11.142749 12.24312 11.519222 12.68148 12.035221 13.014813 C 12.529888 13.348146 13.169684 13.515045 13.955016 13.515045 L 13.95605 13.515045 C 14.321383 13.515045 14.694554 13.471486 15.075887 13.384819 C 15.46322 13.293486 15.834707 13.167728 16.19004 13.007061 C 16.550039 12.840395 16.877967 12.643578 17.173967 12.416912 C 17.474633 12.191579 17.724464 11.944165 17.923798 11.674832 L 17.198255 10.319866 C 17.052922 10.513199 16.834692 10.709648 16.544026 10.908982 C 16.254026 11.107648 15.93959 11.274547 15.600924 11.409213 C 15.262257 11.54388 14.945036 11.610753 14.649036 11.610753 C 14.261703 11.610753 13.981804 11.52782 13.809804 11.361154 C 13.637804 11.194487 13.53551 10.938419 13.502844 10.593753 L 13.502844 10.367925 C 14.17551 10.195925 14.802181 9.9917041 15.382847 9.7550376 C 15.963513 9.5130379 16.471464 9.2307205 16.906797 8.9080542 C 17.342797 8.5860545 17.681798 8.2149346 17.923798 7.794935 C 18.171131 7.3702687 18.294837 6.8890835 18.294837 6.351084 C 18.294837 5.9477511 18.195478 5.5926906 17.996145 5.2860243 C 17.802812 4.9746912 17.506853 4.7302129 17.108854 4.5522131 C 16.716188 4.3748799 16.218943 4.2860773 15.616943 4.2860773 z M 14.987003 5.8188125 C 15.207669 5.8188125 15.360927 5.8834174 15.446927 6.0120839 C 15.538926 6.1360838 15.584904 6.3406717 15.584904 6.6260048 C 15.584904 6.9160045 15.522867 7.1983219 15.398867 7.4729882 C 15.275534 7.7416546 15.109152 7.986133 14.899152 8.2067994 C 14.694486 8.4267992 14.468485 8.6044044 14.221152 8.7390709 C 13.974485 8.8737374 13.724137 8.9488987 13.470804 8.9648987 C 13.481471 8.6902323 13.516892 8.3458782 13.576225 7.9318786 C 13.634891 7.517879 13.777443 7.1312164 14.004109 6.7712167 C 14.176109 6.5018837 14.334353 6.275883 14.479019 6.0932165 C 14.624352 5.9105501 14.79367 5.8188125 14.987003 5.8188125 z " />
48
  <path
49
     id="path27"
50
     style="fill:#51a9cf;fill-opacity:1"
51
     d="M 24.377202 4.2860773 C 23.441869 4.2860773 22.586629 4.5252025 21.811963 5.0038687 C 21.043297 5.4772016 20.43547 6.1470652 19.988804 7.0130643 C 19.536805 7.8677302 19.310804 8.8301007 19.310804 9.9007663 C 19.310804 10.551432 19.429158 11.151121 19.665824 11.69912 C 19.90249 12.24312 20.278964 12.68148 20.794963 13.014813 C 21.290296 13.348146 21.930092 13.515045 22.714758 13.515045 C 23.080757 13.515045 23.455179 13.471486 23.837179 13.384819 C 24.223845 13.293486 24.594448 13.167728 24.949781 13.007061 C 25.309781 12.840395 25.638225 12.643578 25.934225 12.416912 C 26.235558 12.191579 26.48539 11.944165 26.684056 11.674832 L 25.957996 10.319866 C 25.812663 10.513199 25.594801 10.709648 25.304801 10.908982 C 25.014135 11.107648 24.699848 11.274547 24.361182 11.409213 C 24.022516 11.54388 23.705144 11.610753 23.409811 11.610753 C 23.021812 11.610753 22.742062 11.52782 22.570063 11.361154 C 22.398729 11.194487 22.296435 10.938419 22.263102 10.593753 L 22.263102 10.367925 C 22.935768 10.195925 23.56244 9.9917041 24.143106 9.7550376 C 24.723772 9.5130379 25.23209 9.2307205 25.668089 8.9080542 C 26.103422 8.5860545 26.442423 8.2149346 26.68509 7.794935 C 26.931756 7.3702687 27.055096 6.8890835 27.055096 6.351084 C 27.055096 5.9477511 26.955587 5.5926906 26.75692 5.2860243 C 26.562921 4.9746912 26.267112 4.7302129 25.869112 4.5522131 C 25.477113 4.3748799 24.979868 4.2860773 24.377202 4.2860773 z M 23.747778 5.8188125 C 23.967778 5.8188125 24.121552 5.8834174 24.208218 6.0120839 C 24.299552 6.1360838 24.345162 6.3406717 24.345162 6.6260048 C 24.345162 6.9160045 24.283125 7.1983219 24.159125 7.4729882 C 24.035792 7.7416546 23.868893 7.986133 23.658894 8.2067994 C 23.454894 8.4267992 23.228893 8.6044044 22.980893 8.7390709 C 22.734227 8.8737374 22.484396 8.9488987 22.231062 8.9648987 C 22.242396 8.6902323 22.2773 8.3458782 22.335966 7.9318786 C 22.3953 7.517879 22.537851 7.1312164 22.763851 6.7712167 C 22.935851 6.5018837 23.094461 6.275883 23.239794 6.0932165 C 23.385128 5.9105501 23.554445 5.8188125 23.747778 5.8188125 z " />
52
  <path
53
     d="M751.566 230.706c-2.527 12.283 1.949 15.415 1.949 15.415s1.787-7.837 5.132-6.391c2.871 1.237.668 6.515.668 6.515s18.882-18.12 4.517-24.348c0 0-3.15-1.644-6.354-.387-2.345.92-4.718 3.391-5.912 9.196"
54
     paint-order="stroke"
55
     style="fill:url(#b);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.72817;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
56
     transform="matrix(.20372 0 0 .20372 -95.423 -31.173)"
57
     id="path2" />
58
  <path
59
     fill="#126d95"
60
     d="m771.784 223.478-6.529-10.073 1.046-3.86 5.948 1.61zM773.124 207.876l-.545 2.01-5.935-1.607.545-2.01z"
61
     style="stroke-width:.59394"
62
     transform="matrix(.20372 0 0 .20372 -95.423 -31.173)"
63
     id="path3" />
64
  <g
65
     fill="#51a9cf"
66
     id="g4">
67
    <path
68
       d="M62.74 3.45a.467.467 0 0 0-.446.344.47.47 0 0 0 .332.578l-1.69 6.235-1.21-.328c1.012-3.807.605-6.29.605-6.29A13.105 13.105 0 0 0 63.674 0zM60.87 10.849l-.11.41-1.215-.33.11-.41zM60.69 11.516l-.213.787-2.182 1.1 1.183-2.216z"
69
       id="path4" />
70
  </g>
71
  <path
72
     fill="#126d95"
73
     d="m777.66 170.312 4.589-16.945c-.19 4.992.169 14.581 4.286 25.19 0 0-7.065 9.8-13.065 28.138l-5.96-1.614 8.295-30.605a2.308 2.308 0 0 0 1.855-4.164"
74
     style="stroke-width:.59394"
75
     transform="matrix(.20372 0 0 .20372 -95.423 -31.173)"
76
     id="path5" />
77
  <path
78
     style="fill:#51a9cf;fill-opacity:1"
79
     d="M 52.691,13.385 53.917,4.4 h 2.71 l -0.129,1.872 q 0.178,-0.525 0.516,-0.976 0.34,-0.452 0.799,-0.726 0.46,-0.283 0.992,-0.283 0.428,0 0.549,0.097 l -0.5,2.856 Q 58.797,7.143 58.555,7.086 58.313,7.03 58.079,7.03 q -0.347,0 -0.645,0.072 -0.29,0.065 -0.565,0.218 -0.266,0.154 -0.548,0.404 l -0.823,5.662 z"
80
     id="path31" />
81
  <path
82
     style="fill:#51a9cf;fill-opacity:1"
83
     d="M 40.107,13.385 38.639,1.334 h 2.823 l 0.452,7.711 0.161,1.759 0.532,-1.759 2.033,-5.05 -0.29,-2.661 h 2.726 l 0.468,7.711 0.178,1.759 0.42,-1.759 2.629,-7.711 h 2.872 l -4.68,12.051 H 45.705 L 45.253,9.078 45.108,7.755 44.624,9.078 43.059,13.385 Z"
84
     id="path30" />
85
  <path
86
     style="fill:#51a9cf;fill-opacity:1"
87
     d="M 28.088,13.385 29.314,4.4 h 2.694 l -0.113,1.63 q 0.549,-0.823 1.29,-1.283 0.75,-0.46 1.63,-0.46 1.065,0 1.622,0.549 0.556,0.548 0.556,1.823 0,0.177 -0.056,0.645 -0.049,0.46 -0.13,1.025 -0.072,0.556 -0.137,1.024 L 36.582,9.95 q -0.057,0.436 -0.137,1 -0.073,0.557 -0.154,1.097 -0.072,0.54 -0.129,0.912 l -0.056,0.427 h -2.807 q 0.12,-0.806 0.217,-1.516 0.105,-0.718 0.186,-1.34 0.08,-0.628 0.145,-1.16 Q 33.944,8.554 34,8.014 34.065,7.465 34.073,7.175 34.089,6.748 33.928,6.578 33.767,6.401 33.428,6.401 33.21,6.401 32.968,6.506 32.734,6.61 32.5,6.812 32.266,7.006 32.048,7.28 31.838,7.554 31.669,7.885 l -0.774,5.501 z"
88
     id="path29" />
89
  <path
90
     style="fill:#51a9cf;fill-opacity:1"
91
     d="M 0,13.385 1.726,1.334 h 2.71 L 3.662,6.609 7.905,1.334 H 11.1 L 6.324,6.642 q 0.113,0.21 0.274,0.532 0.17,0.315 0.404,0.79 0.242,0.468 0.572,1.138 0.34,0.67 0.8,1.59 0.209,0.427 0.41,0.814 0.202,0.379 0.388,0.718 0.185,0.33 0.355,0.62 l 0.33,0.541 H 6.582 Q 6.55,13.329 6.462,13.151 6.372,12.974 6.235,12.675 6.098,12.369 5.905,11.949 l -0.42,-0.968 -1,-2.307 -1.29,1.356 -0.485,3.355 z"
92
     id="path6" />
93
  <path
94
     style="fill:#51a9cf;fill-opacity:1"
95
     d="m 67.21,13.547 q -0.854,0 -1.355,-0.444 -0.5,-0.444 -0.5,-1.25 0,-0.073 0.024,-0.347 l 0.057,-0.613 q 0.04,-0.347 0.08,-0.638 L 66.081,6.109 H 65.42 l 0.29,-1.71 h 0.823 l 0.774,-2.162 h 2.114 L 69.114,4.4 h 1.759 l -0.178,1.71 h -1.79 l -0.307,2.227 -0.194,1.387 q -0.072,0.508 -0.104,0.774 l -0.049,0.388 q -0.008,0.12 -0.008,0.226 0,0.242 0.113,0.379 0.113,0.137 0.436,0.137 0.169,0 0.427,-0.089 0.258,-0.097 0.516,-0.242 0.259,-0.153 0.428,-0.315 l 0.436,1.34 q -0.42,0.322 -0.944,0.605 -0.517,0.274 -1.13,0.451 -0.613,0.17 -1.314,0.17 z"
96
     id="path7" />
97
</svg>
98
1
<svg width="300" height="71.784" viewBox="0 0 79.375 18.993" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient xlink:href="#a" id="b" x1=".152" x2=".969" y1="-.045" y2="-.049" gradientTransform="rotate(101.544 290.55 422.146) scale(26.05808)" gradientUnits="userSpaceOnUse"/><linearGradient id="a" x1=".152" x2=".969" y1="-.045" y2="-.049" gradientTransform="rotate(101.544 290.55 422.146) scale(26.05808)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ec706a"/><stop offset="1" stop-color="#ecd980"/></linearGradient></defs><path style="fill:#51a9cf;fill-opacity:1" d="M76.697 4.286c-.936 0-1.79.24-2.565.718-.768.473-1.376 1.143-1.823 2.01-.452.854-.678 1.816-.678 2.887 0 .65.118 1.25.355 1.798.237.543.613.981 1.13 1.315.494.333 1.134.5 1.919.5l.001.001c.365 0 .739-.044 1.12-.13a6.09 6.09 0 0 0 1.114-.378c.36-.167.688-.363.984-.59.3-.225.55-.473.75-.742l-.726-1.355c-.145.193-.363.39-.653.589a4.83 4.83 0 0 1-.944.5 2.583 2.583 0 0 1-.952.202c-.387 0-.667-.083-.84-.25-.171-.167-.274-.423-.306-.767v-.226a14.49 14.49 0 0 0 1.88-.613 7.222 7.222 0 0 0 1.525-.847 3.661 3.661 0 0 0 1.016-1.113c.247-.425.37-.906.37-1.444 0-.403-.099-.758-.298-1.065-.193-.311-.49-.556-.887-.734-.393-.177-.89-.266-1.492-.266zm-.63 1.533c.22 0 .374.064.46.193.092.124.138.329.138.614 0 .29-.062.572-.186.847-.124.269-.29.513-.5.734-.205.22-.43.397-.678.532a1.78 1.78 0 0 1-.75.225c.01-.275.046-.619.105-1.032.059-.414.201-.8.428-1.16.172-.27.33-.496.475-.679.145-.182.315-.274.508-.274zM15.617 4.286c-.936 0-1.79.24-2.565.718-.768.473-1.376 1.143-1.823 2.01-.452.854-.678 1.816-.678 2.887 0 .65.118 1.25.355 1.798.237.544.613.982 1.13 1.316.494.333 1.134.5 1.919.5h.001c.365 0 .739-.044 1.12-.13a6.09 6.09 0 0 0 1.114-.378c.36-.167.688-.363.984-.59.3-.225.55-.473.75-.742l-.726-1.355c-.145.193-.363.39-.654.589-.29.199-.604.366-.943.5a2.583 2.583 0 0 1-.952.202c-.387 0-.667-.083-.84-.25-.171-.167-.273-.423-.306-.767v-.226a14.49 14.49 0 0 0 1.88-.613 7.197 7.197 0 0 0 1.524-.847 3.656 3.656 0 0 0 1.017-1.113c.247-.425.37-.906.37-1.444 0-.403-.099-.758-.298-1.065-.193-.311-.49-.556-.887-.734-.393-.177-.89-.266-1.492-.266zm-.63 1.533c.22 0 .374.064.46.193.092.124.138.329.138.614 0 .29-.062.572-.186.847-.123.269-.29.513-.5.734-.205.22-.43.397-.678.532-.247.135-.497.21-.75.226.01-.275.046-.62.105-1.033.059-.414.201-.8.428-1.16.172-.27.33-.496.475-.679.145-.182.315-.274.508-.274zM24.377 4.286c-.935 0-1.79.24-2.565.718-.769.473-1.377 1.143-1.823 2.01-.452.854-.678 1.816-.678 2.887 0 .65.118 1.25.355 1.798.236.544.613.982 1.129 1.316.495.333 1.135.5 1.92.5.366 0 .74-.044 1.122-.13a6.091 6.091 0 0 0 1.113-.378 5.22 5.22 0 0 0 .984-.59 3.6 3.6 0 0 0 .75-.742l-.726-1.355c-.145.193-.363.39-.653.589-.29.199-.605.366-.944.5a2.583 2.583 0 0 1-.951.202c-.388 0-.668-.083-.84-.25-.171-.167-.274-.423-.307-.767v-.226a14.49 14.49 0 0 0 1.88-.613 7.222 7.222 0 0 0 1.525-.847 3.673 3.673 0 0 0 1.017-1.113c.247-.425.37-.906.37-1.444 0-.403-.1-.758-.298-1.065-.194-.311-.49-.556-.888-.734-.392-.177-.89-.266-1.492-.266zm-.63 1.533c.22 0 .375.064.461.193.092.124.137.329.137.614 0 .29-.062.572-.186.847-.123.269-.29.513-.5.734-.204.22-.43.397-.678.532a1.79 1.79 0 0 1-.75.226c.011-.275.046-.62.105-1.033.06-.414.202-.8.428-1.16.172-.27.33-.496.476-.679.145-.182.314-.274.508-.274z"/><path d="M751.566 230.706c-2.527 12.283 1.949 15.415 1.949 15.415s1.787-7.837 5.132-6.391c2.871 1.237.668 6.515.668 6.515s18.882-18.12 4.517-24.348c0 0-3.15-1.644-6.354-.387-2.345.92-4.718 3.391-5.912 9.196" paint-order="stroke" style="fill:url(#b);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.72817;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" transform="translate(-95.423 -31.173) scale(.20372)"/><path fill="#126d95" d="m771.784 223.478-6.529-10.073 1.046-3.86 5.948 1.61zm1.34-15.602-.545 2.01-5.935-1.607.545-2.01z" style="stroke-width:.59394" transform="translate(-95.423 -31.173) scale(.20372)"/><path d="M62.74 3.45a.467.467 0 0 0-.446.344.47.47 0 0 0 .332.578l-1.69 6.235-1.21-.328c1.012-3.807.605-6.29.605-6.29A13.105 13.105 0 0 0 63.674 0zm-1.87 7.399-.11.41-1.215-.33.11-.41zm-.18.667-.213.787-2.182 1.1 1.183-2.216z" fill="#51a9cf"/><path fill="#126d95" d="m777.66 170.312 4.589-16.945c-.19 4.992.169 14.581 4.286 25.19 0 0-7.065 9.8-13.065 28.138l-5.96-1.614 8.295-30.605a2.308 2.308 0 0 0 1.855-4.164" style="stroke-width:.59394" transform="translate(-95.423 -31.173) scale(.20372)"/><path style="fill:#51a9cf;fill-opacity:1" d="M52.691 13.385 53.917 4.4h2.71l-.129 1.872q.178-.525.516-.976.34-.452.799-.726.46-.283.992-.283.428 0 .549.097l-.5 2.856q-.057-.097-.299-.154-.242-.056-.476-.056-.347 0-.645.072-.29.065-.565.218-.266.154-.548.404l-.823 5.662zM40.107 13.385 38.639 1.334h2.823l.452 7.711.161 1.759.532-1.759 2.033-5.05-.29-2.661h2.726l.468 7.711.178 1.759.42-1.759 2.629-7.711h2.872l-4.68 12.051h-3.258l-.452-4.307-.145-1.323-.484 1.323-1.565 4.307ZM28.088 13.385 29.314 4.4h2.694l-.113 1.63q.549-.823 1.29-1.283.75-.46 1.63-.46 1.065 0 1.622.549.556.548.556 1.823 0 .177-.056.645-.049.46-.13 1.025-.072.556-.137 1.024l-.088.597q-.057.436-.137 1-.073.557-.154 1.097-.072.54-.129.912l-.056.427h-2.807q.12-.806.217-1.516.105-.718.186-1.34.08-.628.145-1.16.097-.816.153-1.356.065-.549.073-.839.016-.427-.145-.597-.161-.177-.5-.177-.218 0-.46.105-.234.104-.468.306-.234.194-.452.468-.21.274-.379.605l-.774 5.501zM0 13.385 1.726 1.334h2.71l-.774 5.275 4.243-5.275H11.1L6.324 6.642q.113.21.274.532.17.315.404.79.242.468.572 1.138.34.67.8 1.59.209.427.41.814.202.379.388.718.185.33.355.62l.33.541H6.582q-.032-.056-.12-.234-.09-.177-.227-.476-.137-.306-.33-.726l-.42-.968-1-2.307-1.29 1.356-.485 3.355zM67.21 13.547q-.854 0-1.355-.444-.5-.444-.5-1.25 0-.073.024-.347l.057-.613q.04-.347.08-.638l.565-4.146h-.661l.29-1.71h.823l.774-2.162h2.114L69.114 4.4h1.759l-.178 1.71h-1.79l-.307 2.227-.194 1.387q-.072.508-.104.774l-.049.388q-.008.12-.008.226 0 .242.113.379t.436.137q.169 0 .427-.089.258-.097.516-.242.259-.153.428-.315l.436 1.34q-.42.322-.944.605-.517.274-1.13.451-.613.17-1.314.17z"/></svg>
D www/index.html
1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
  <title>KeenWrite</title>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <meta name="description" content="cross-platform, open-source desktop editor">
8
  <meta name="keywords" content="markdown, text, editor, software">
9
  <meta name="robots" content="index, follow">
10
  <link rel="stylesheet" href="styles/base.css"
11
    media="print" onload="this.media='all'">
12
</head>
13
<body>
14
<header>
15
  <img src="images/logo/title.svg" alt="KeenWrite" class="title">
16
  <p>
17
    A free, cross-platform desktop text editor for producing beautifully
18
    typeset PDF files.
19
  </p>
20
</header>
21
<main>
22
  <div class="downloads">
23
    <a href="downloads/keenwrite.bin"
24
       class="download"
25
       title="Download for 64-bit Linux (x86)"
26
       aria-label="Download for Linux"><img
27
         src="images/icons/linux.svg"
28
         alt="Download for Linux"
29
         class="download"></a>
30
    <a href="downloads/keenwrite.jar"
31
       class="download"
32
       title="Download for Java virtual machine"
33
       aria-label="Download for Java"><img
34
         src="images/icons/java.svg"
35
         alt="Download for Java"
36
         class="download"></a>
37
    <a href="downloads/KeenWrite.exe"
38
       class="download"
39
       title="Download for 64-bit Windows (x86)"
40
       aria-label="Download for Windows"><img
41
         src="images/icons/windows.svg"
42
         alt="Download for Windows"
43
         class="download"></a>
44
    <a href="downloads/keenwrite.app"
45
       class="download"
46
       title="Download for 64-bit MacOS (x86)"
47
       aria-label="Download for MacOS"><img
48
         src="images/icons/apple.svg"
49
         alt="Download for MacOS"
50
         class="download"></a>
51
  </div>
52
</main>
53
<nav>
54
  <ul>
55
    <li><a href="screenshots.html">screenshots</a></li>
56
    <li><a href="https://www.youtube.com/playlist?list=PLB-WIt1cZYLm1MMx2FBG9KWzPIoWZMKu_">tutorials</a></li>
57
    <li><a href="https://gitlab.com/DaveJarvis/KeenWrite">sources</a></li>
58
    <li><a href="https://gitlab.com/DaveJarvis/KeenWrite/issues">issues</a></li>
59
    <li><a href="https://gitlab.com/DaveJarvis/KeenWrite/-/blob/main/docs/README.md">documentation</a></li>
60
  </ul>
61
</nav>
62
<footer>
63
  &copy; 2023, White Magic Software, Ltd.
64
</footer>
65
</body>
66
</html>
67
681
A www/index.shtml
1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
  <title>KeenWrite</title>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <meta name="description" content="cross-platform, open-source desktop editor">
8
  <meta name="keywords" content="markdown, text, editor, software">
9
  <meta name="robots" content="index, follow">
10
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self' 'unsafe-inline';">
11
  <style><!--#include file="styles/base.css" --></style>
12
</head>
13
<body>
14
<header>
15
  <img src="images/logo/title.svg" alt="KeenWrite" class="title">
16
  <p>
17
    A free, cross-platform desktop text editor for producing beautifully
18
    typeset PDF files.
19
  </p>
20
</header>
21
<main>
22
  <div class="downloads">
23
    <a href="downloads/keenwrite.bin"
24
       class="download"
25
       title="Download for 64-bit Linux (x86)"
26
       aria-label="Download for Linux"><img
27
         src="images/icons/linux.svg"
28
         alt="Download for Linux"
29
         class="download"></a>
30
    <a href="downloads/keenwrite.jar"
31
       class="download"
32
       title="Download for Java virtual machine"
33
       aria-label="Download for Java"><img
34
         src="images/icons/java.svg"
35
         alt="Download for Java"
36
         class="download"></a>
37
    <a href="downloads/KeenWrite.exe"
38
       class="download"
39
       title="Download for 64-bit Windows (x86)"
40
       aria-label="Download for Windows"><img
41
         src="images/icons/windows.svg"
42
         alt="Download for Windows"
43
         class="download"></a>
44
    <a href="downloads/keenwrite.app"
45
       class="download"
46
       title="Download for 64-bit MacOS (x86)"
47
       aria-label="Download for MacOS"><img
48
         src="images/icons/apple.svg"
49
         alt="Download for MacOS"
50
         class="download"></a>
51
  </div>
52
  <!--#config timefmt="%d-%b-%Y" -->
53
  <p class="version">
54
  <strong>Version <!--#include file="downloads/version.txt" --></strong>
55
  <br><!--#flastmod virtual="downloads/version.txt" -->
56
  <br><!--#exec cmd="./count.sh" --> downloads
57
  </p>
58
</main>
59
<nav>
60
  <ul>
61
    <li><a href="screenshots.html">screenshots</a></li>
62
    <li><a href="https://www.youtube.com/playlist?list=PLB-WIt1cZYLm1MMx2FBG9KWzPIoWZMKu_">tutorials</a></li>
63
    <li><a href="https://gitlab.com/DaveJarvis/KeenWrite">sources</a></li>
64
    <li><a href="https://gitlab.com/DaveJarvis/KeenWrite/issues">issues</a></li>
65
    <li><a href="https://gitlab.com/DaveJarvis/KeenWrite/-/blob/main/docs/README.md">documentation</a></li>
66
  </ul>
67
</nav>
68
<footer>
69
  &copy; 2023, White Magic Software, Ltd.
70
</footer>
71
</body>
72
</html>
73
174
M www/robots.txt
33
crawl-delay: 60
44
5
user-agent: Googlebot
6
disallow: /repository/*
7
disallow: /downloads/*
8
user-agent: Bingbot
9
disallow: /repository/*
10
disallow: /downloads/*
11
user-agent: YandexBot
12
disallow: /repository/*
13
disallow: /downloads/*
14
user-agent: MicrosoftBot
15
disallow: /repository/*
16
disallow: /downloads/*
17
18
M www/styles/base.css
1
/* ************************************************************************ 
1
/*
22
 * Page
3
 * ************************************************************************ */
3
 */
44
:root {
55
  --accent-colour: #ec706a;
...
2121
}
2222
23
/* ************************************************************************ 
23
/*
2424
 * Header
25
 * ************************************************************************ */
26
25
 */
2726
header {
2827
  /* Avoid being flush with top of page, put space between the title and
...
4342
}
4443
45
/* ************************************************************************ 
44
/*
4645
 * Screenshots
47
 * ************************************************************************ */
48
46
 */
4947
main.screenshots {
5048
  text-align: center;
...
7068
}
7169
72
/* ************************************************************************ 
73
 * Download buttons
74
 * ************************************************************************ */
70
/*
71
 * Version information
72
 */
73
main > p.version {
74
  text-align: center;
75
}
7576
77
/*
78
 * Download buttons
79
 */
7680
main > div.downloads {
7781
  /* Arrange the buttons in a responsive, 2 x 2 grid. */
...
104108
  filter: invert(6%)
105109
    sepia(58%) saturate(857%) hue-rotate(158deg) brightness(91%) contrast(91%);
110
111
  width: 157px;
112
  height: 75px;
106113
}
107114
108
/* ************************************************************************ 
115
/*
109116
 * Navigation
110
 * ************************************************************************ */
111
117
 */
112118
nav {
113119
  /* Don't crowd navigation links against the download buttons. */
...
140146
}
141147
142
/* ************************************************************************ 
148
/*
143149
 * Footer
144
 * ************************************************************************ */
150
 */
145151
footer {
146152
  margin-top: 2em;