Posts

TeXnical Writing Part 1: Foundations

Nov 2, 2020
Dave Jarvis
23.9

Introduction

Plain text documents are timeless. The earliest ASCII documents ever written can still be opened, read, and modified today, on virtually any hardware, using any operating system, without any proprietary software, online service, or third-party conversion program. In stark contrast are document formats such as those produced by Microsoft Word, ClarisWorks, Lotus Manuscript, WordPerfect, WordStar, and most other word processors, whether discontinued or not.

A double-edged sword to some proprietary word processors is their ability to show the document’s final form while editing. This ability makes tying a document’s appearance to its content easy, which can lead to troubles later on when attempting to change either independently.

Before 1978, typesetting mathematics was expensive, laborious, or aesthetically displeasing. Eventually, a small subset of word processing software offered features to typeset formulas and equations (e.g., Microsoft Word 6.0 with Equation Editor 1.0 released in the early 1990s). While this addressed some issues, proprietary file formats remained problematic.

In 1978, Donald Knuth released an open specification for typesetting mathematics using a plain text syntax called TeX (pronounced “tech”). Back then computers were not fast enough to render TeX in real-time, making it impossible to see the final form while actively editing the document. Modern laptops are powerful enough to typeset TeX in real-time, decreasing the amount of time spent waiting for documents to build.

Software developers, scientists, engineers, and mathematicians all use mathematics to communicate algorithms, formulas, and equations succinctly and accurately. As technology-based societies expand, we need tools to help people express complex ideas without ties to proprietary technology.

In other words, we’d like a workflow that gives us the ability to write plain text documents that may be published to any medium, such as web or print, using cross-platform software.

In this series, we’ll develop a Java-based desktop application using JavaFX that converts mathematical formulas written in Markdown documents into HTML documents. We’ll conquer technical challenges using techniques that allow us to achieve the real-time conversion from Markdown to HTML. Each article in the series builds upon the previous one. Broadly, the articles include:

  • Foundation — Create a new project, develop a text editor, and build an executable binary.
  • Markdown — Update the text editor to support Markdown syntax, and provide a preview panel to show the result.
  • Syntax — Add syntax highlighting to the text editor.
  • Math — Include a TeX engine that can draw mathematical formulas on a JavaFX canvas or as scalable vector graphics, such as the following equation:
  • Performance — Profile the application to attain real-time rendering of hundreds of formulas.

To develop the editor, we’ll use Liberica JDK for a few reasons. First, it comes bundled with JavaFX, which is a feature-rich library for writing cross-platform desktop applications. Second, the way the download URLs are designed makes it super-easy to write installation scripts that target specific Java versions.

Audience

Readers are expected to have a working knowledge of software design patterns, the Java programming language, and be familiar with web-related technologies such as HTML and CSS. Basic knowledge of TeX is useful but not necessary. Readers should understand basic development operations, such as configuring environment variables. Although the instructions are written for a Unix-like platform (e.g., Linux), the principles behind them are applicable to any operating system.

For Windows users, consider one of the following solutions:

Software requirements

Download the latest versions of the following software packages, preferably in .zip or .tar.gz format where possible:

For the purposes of these instructions, we’ll refer to the download location as $(logname)/downloads, which references the non-administrative account name. On Windows, this would be equivalent to %USERNAME%\Downloads.

Development environment setup

This section describes the installation steps to configure the system for development. There are many ways to set up a software development environment. Installing third-party applications into /opt then leveraging symbolic links makes upgrading and downgrading a simple matter of replacing a symbolic link. This approach can work on Windows but requires special software to create directory links.

Liberica JDK

Broadly, we want to install Liberica JDK into a common directory, set up the JAVA_HOME environment variable, update the system PATH variable, then verify that Java is installed. Accomplish this by completing the following steps to install Java system-wide:

  1. Open a terminal.
  2. Change to the root user (e.g., sudo su -).
  3. Extract Liberica JDK using the following commands (change the archive filename as appropriate):
    cd /opt
    tar xf $(logname)/downloads/bellsoft-jdk15+36-linux-amd64-full.tar.gz
    
  4. Create a symbolic link having a name that is independent of the Java version installed:
    ln -s jdk-15-full jdk
    
  5. Log out of the root account.

Java is now installed into /opt/jdk. Next, configure the environment variables for a non-administrative account as follows:

  1. Edit the system environment variables (e.g., $HOME/.bashrc).
  2. Append the following lines:
    export JAVA_HOME=/opt/jdk
    export IDEA_HOME=/opt/idea
    export GRADLE_HOME=/opt/gradle
    export PATH="$PATH:$HOME/bin:$JAVA_HOME/bin:$IDEA_HOME/bin:$GRADLE_HOME/bin"
    
  3. Save the file.
  4. Open a new terminal (you may close the previous one).
  5. Verify that Java is installed:
    java -version
    

If successful, the console will display a result similar to the following output:

openjdk version "15" 2020-09-15
OpenJDK Runtime Environment (build 15+36)
OpenJDK 64-Bit Server VM (build 15+36, mixed mode, sharing)

Java is installed. Advantages to this installation approach include:

  • full control over the exact version of Java being installed without regards to availability by package managers;
  • the environment variables never have to change value;
  • switching between versions entails changing a single symbolic link; and
  • we can ensure that the only place Java binaries and supporting libraries are located is under /opt.

The main disadvantage is that some packages cannot detect that Java is installed, despite both JAVA_HOME being set and the java command being available via the PATH.

There are other ways to install Java on your system, such as using a package manager. Note that you must configure the package manager to download the full version of Liberica JDK.

Gradle

Gradle is a build system we’ll use to simplify compiling Java source files into bytecode then bundling our bytecode and third-party libraries into a single JAR file. Install Gradle using similar steps to installing Java, as follows:

  1. Open a terminal, if not already opened.
  2. Change to the root user (e.g., sudo su -).
  3. Extract Gradle using the following commands (be sure to substitute the version number as appropriate):
    cd /opt
    unzip $(logname)/downloads/gradle-6.6.1-bin.zip
    
  4. Create a symbolic link as follows:
    ln -s gradle-6.6.1 gradle
    
  5. Log out of the root account.
  6. Verify that Gradle is installed:
    gradle -version
    

If all went well, the console will display a result that begins with the following output, showing the version number:

------------------------------------------------------------
Gradle 6.6.1
------------------------------------------------------------

Gradle is installed.

IntelliJ IDEA

Repeat similar steps to installing Java and Gradle for installing the integrated development environment (IDE):

  1. Open a terminal, if not already opened.
  2. Change to the root user (e.g., sudo su -).
  3. Extract IntelliJ IDEA using the following commands (substitute the version number as appropriate):
    cd /opt
    tar xf $(logname)/downloads/ideaIC-2020.2.2.tar.gz
    
  4. Create a symbolic link as follows:
    ln -s idea-IC-202.7319.50 idea
    
  5. Log out of the root account.

IntelliJ IDEA is installed.

Project environment setup

For this project, we’re going to create a non-modular application built using Gradle. After Gradle is configured, we’ll import the project into the IDE and then begin development.

Initialization

Initialize the project as follows:

mkdir -p $HOME/dev/java/mdtexfx
cd $HOME/dev/java/mdtexfx
gradle init

Next, Gradle will ask a few questions; when prompted, provide the following answers:

  1. Choose 2 to build an application.
  2. Choose 3 to set the language to Java.
  3. Choose 1 to set the build script to use Groovy.
  4. Choose 4 to test using JUnit Jupiter (a.k.a. JUnit 5).
  5. Press Enter to accept a Project name of mdtexfx.
  6. Set Source package to com.mdtexfx

Gradle will create the following directory structure:

.

├── gradle
│   └── wrapper
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── mdtexfx
    │   └── resources
    └── test
        ├── java
        │   └── com
        │       └── mdtexfx
        └── resources

The project is initialized, along with the following source files:

  • App.java — contains the main application entry point; and
  • AppTest.java — helps verify that the code in App.java is correct.

Build and then run the project by typing the following commands into the terminal:

./gradlew clean build
./gradlew run

The output from Gradle will contain:

> Task :run
Hello world.

This demonstrates that we’ve successfully initialized a rudimentary Gradle application.

Open project

Confirm that the application can run within the IDE as follows:

  1. Start IntelliJ IDEA.
  2. If no project is currently open:
    1. Click Open or Import.
    2. Open $HOME/dev/java/mdtexfx.
  3. Otherwise, if a project is already open:
    1. Choose File → New → Project from Existing Sources.
    2. Browse to $HOME/dev/java/mdtexfx.
    3. Click OK.
    4. Select Gradle.
    5. Click Finish.
  4. Click Run → Run.
  5. Select App.

The same “Hello world” output message appears, which indicates that the IDE can run the application.

Synchronize with build script

IntelliJ IDEA may issue false warnings or report errors that are not necessarily correct; keeping the IDE in sync with the latest changes to the Gradle configuration file can resolve erroneous warnings. Accomplish this using the following steps, from within the IDE:

  1. Open build.gradle.
  2. Optionally, remove or replace the header comment at the top of the file.
  3. Click the Load Gradle Changes icon.

The IDE reloads the Gradle configuration file.

Configure Gradle

Now that IntelliJ IDEA can run the application, we can proceed with configuring the build script for a simple JavaFX editor. Our Gradle build scripts use Groovy syntax, though other languages are possible—like Kotlin. This entails the following coarse-grained steps:

  • Apply a JavaFX plugin
  • Configure the JavaFX plugin
  • Update dependencies
  • Create an überjar
  • Run the application

An überjar is a Java archive (.jar) file that contains all class files and resources necessary to run the application.

Apply JavaFX plugin

JavaFX requires native binaries for each target platform. The JavaFX plugin simplifies the work of including native binaries for a specific platform.

Update the plugins section to include the JavaFX plugin:

// Apply the JavaFX plugin to add support for JavaFX.
id 'org.openjfx.javafxplugin' version '0.0.9'

Configure JavaFX plugin

After the repositories section, configure the JavaFX plugin as follows:

javafx {
  version = '15'
  modules = ['javafx.controls']
  configuration = 'compileOnly'
}

Setting the configuration option to compileOnly instructs the JavaFX plugin that the native binaries will be provided separately. This is where Liberica JDK shines because it bundles the necessary JavaFX components with its full version of the OpenJDK.

Setting the modules indicates that the application requires basic user interface functionality. This includes the concept of a scene, which is fundamental for creating a user interface in JavaFX.

The version setting instructs the plugin to use modules for the given JavaFX version.

Update dependencies

Inside the dependencies section, insert the following code:

runtimeOnly "org.openjfx:javafx-controls:15:linux"

Adding the given dependency tells Gradle to download the necessary native binaries for running the application on Linux. As the application grows in complexity, we’ll need to include additional JavaFX dependencies.

Create an überjar

After the application section but before the test section, add the following code:

jar {
  manifest {
    attributes 'Main-Class': application.mainClassName
  }
  from {
    (configurations.runtimeClasspath).collect {
      it.isDirectory() ? it : zipTree( it )
    }
  } {
    exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
  }
}

By default, Gradle will create a Java archive filename using the project name (i.e., mdtexfx.jar). To customize the filename, set archiveFileName to the desired value.

Setting manifest allows the application’s main entry point to be executed by passing a -jar argument when running the Java virtual machine, which we’ll see shortly.

The from section instructs Gradle to combine all the Java archive files required to run the application into a single file—the überjar. Without this step, launching the application would otherwise require passing a classpath argument to the Java virtual machine that includes paths to all the JAR files necessary to run the application.

Excluding *.RSA, *.SF, and *.DSA removes all signature files from the final überjar. This precaution ensures that developers can sign the final Java archive file without conflicting with existing signature files. If .pom files are present, we may have to exclude those as well.

Run application

First, build the application’s Java archive (JAR) file through the IDE as follows:

  1. Click Gradle, found on the right-hand side.
  2. Expand mdtexfx → Tasks → build.
  3. Double-click jar.

The application is built.

Next, run the application as follows:

  1. Open a terminal.
  2. Change to the project directory (i.e., $HOME/dev/java/mdtexfx).
  3. Type: java -jar build/libs/mdtexfx.jar

The console shows:

Hello world.

An application JAR file is built.

Text editor

Implement a minimal text editor as follows:

  1. In the IDE, expand mdtexfx → src → main → java → com.mdtexfx.
  2. Double-click App to open it.
  3. Replace the contents with the following program:
    package com.mdtexfx;
    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.scene.control.TextArea;
    import javafx.scene.layout.Pane;
    import javafx.stage.Stage;
     
    public class App extends Application { @Override public void start( final Stage stage ) { final var editor = new TextArea(); final var scene = new Scene( editor ); stage.setScene( scene ); stage.show(); } }

Notice that our main class (App) inherits from the JavaFX class named Application. The Application class provides the scaffolding necessary to launch a standalone JavaFX program.

Next, the start method is called by the Application’s launcher to wire together the components that make up our program, including:

  • TextArea — Graphical user interface element for text editing.
  • Scene — Container for one or more user interface elements.
  • Stage — Top-level container, typically the application window.

JavaFX uses a theatre analogy: there is only one Stage; a Stage can have many Scenes, but only one is “performed” (active) at a time; each Scene contains JavaFX widgets—such as a TextArea—that are called nodes, and they represent the visual elements displayed, much like tangible objects in a theatrical performance.

Calling stage.show() causes the application’s window to open, revealing the scene, which contains a single user interface element: the text area.

From the terminal, rebuild and run the application as follows:

./gradlew clean build
java -jar build/libs/mdtexfx.jar

The following window appears:

Click into the window to give the text area focus, type some text, then close the window. The application starts, accepts input, and can be closed.

Unit test

Changing App.java broke the unit test in AppTest.java. Before continuing, it is good practice to make sure the unit test passes. For our purposes to this point, if the application can be instantiated without failure, we’ll consider the test to have passed. Change AppTest.java as follows:

class AppTest {

@Test void test_Instantiation_NewInstance_Created() { new App(); } }

The unit test now passes. (See Roy Osherove’s unit test naming standards.)

Executable binaries

Building a JavaFX application for multiple platforms normally requires running jlink and jpackage to produce binaries. The problem is that that process must be run on each target platform. This means that to build a binary for OSX, the developer must own OSX. It is unreasonable to expect developers or small companies to run their build processes on every target platform. Instead, let’s create a build process for JavaFX that can target any platform from any platform—much like cross-compiling.

To build standalone binaries for each target platform, we’ll use:

  • Liberica JDK web API; and
  • warp, a cross-platform application packager.

Install warp before continuing.

Build binary

Building a self-contained Linux executable entails a few steps:

  • Prepare a full Java runtime environment
  • Create a launcher script
  • Package the application into a standalone binary

Prepare Java runtime environment

Download then extract a 64-bit Linux Java runtime environment (JRE) as follows:

  1. Open a terminal.
  2. Change to the application directory (e.g., $HOME/dev/java/mdtexfx).
  3. Download and extract Liberica JDK into dist, such as:
    wget -q https://download.bell-sw.com/java/15+36/bellsoft-jre15+36-linux-amd64-full.tar.gz
    mkdir dist
    cd dist
    tar xf ../bellsoft*linux*tar.gz
    mv jre-15-full jre
    cd ..
    

The JRE is extracted into $HOME/dev/java/mdtexfx/dist/jre.

Create launcher script

Create a launcher script responsible for running the application after it is extracted out of the standalone binary. Edit a new file named dist/run.sh, then insert the following contents:

#!/usr/bin/env bash
readonly SCRIPT_SRC="$(dirname "${BASH_SOURCE[${#BASH_SOURCE[@]} - 1]}")"
"${SCRIPT_SRC}/jre/bin/java" -jar "${SCRIPT_SRC}/${paths.app}.jar" "$@" 2>&1 >/dev/null &

Save the file, then finish up by making it executable using chmod +x dist/run.sh.

Package application

The Java Software Development Kit includes jlink and jpackage. These programs help eliminate unused dependencies when creating a JavaFX application, shrinking the size of the production executable. Unfortunately, they cannot be used to cross-build native binaries. That is, Linux versions cannot produce a standalone executable that runs on Windows, or vice-versa. One of Java’s core draws is its ability to create cross-platform software. Since JavaFX is no longer bundled with Java, operating system-specific libraries must be bundled into the Java archive file. This means creating separate native binaries for each target platform. Ideally, we’d be able to create all the native binaries from a single computer, regardless of the operating system. We can, but not with jlink and jpackage.

Instead, package the application into a standalone binary using the warp-packer program as follows:

./gradlew clean build
mv build/libs/mdtexfx.jar dist
warp-packer \
  --arch linux-x64 \
  --input_dir dist \
  --exec run.sh \
  --output mdtexfx.bin

Verify the standalone executable displays the application’s text editor by running it as follows:

./mdtexfx.bin

Summary

We’ve created a standalone binary for a JavaFX application, which can be cross-compiled to multiple platforms. The next article will build upon these foundational concepts to develop a Markdown text editor and a preview pane.

Subcribe to our newsletter

figure

Read the industry news, receive solutions to your problems, and find the ways to save money.

Further reading