JSON Editor with RichTextFX (part 2)

This post is part of the 100 Days of JavaFX Series.

In the part 1 we discovered how to use the capabilities of the CodeArea component from the RichTextFX library in order to have a textarea suited for editing code and which can render rich text.

In this post (part 2) we will make a JSON syntax highlighter for the JSON Editor of our JSON Schema application.

Leveraging Jackson Parser

Jackson is a good and popular option when it comes to working with JSON in the Java ecosystem. We want to leverage the JsonParser class to break down our JSON String into JSON tokens and apply styling to those individual tokens.

Add the Jackson dependency to your pom.xml file. Also, don’t forget to add requires com.fasterxml.jackson.core; to you module-info.java file.

<!-- pom.xml -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-core</artifactId>
  <version>2.12.0</version>
</dependency>

Below is a simple example of how the JsonParser can be used to parse and extract tokens from a JSON string.

String code = "{\"a\": 5}";
JsonFactory jsonFactory = new JsonFactory();
JsonParser parser = jsonFactory.createParser(code);
while (!parser.isClosed()) {
  JsonToken jsonToken = parser.nextToken();
  // TODO: Work with the json token
}

Because we are parsing JSON for the purpose of syntax highlighting we can ingore JSON Parsing exceptions and not (or partially) highlight invalid JSON.

CSS classes

As seen in the previous post, we need to create CSS classes in order to apply styling to the CodeArea.

/* src/main/resources/style.css */

.json-property { -fx-fill: blue; }
.json-number   { -fx-fill: purple; }
.json-string   { -fx-fill: red; }

The following method will also help us turn a JsonToken to a CSS class:

public static String jsonTokenToClassName(JsonToken jsonToken) {
  if (jsonToken == null) {
    return "";
  }
  switch (jsonToken) {
    case FIELD_NAME:
      return "json-property";
    case VALUE_STRING:
      return "json-string";
    case VALUE_NUMBER_FLOAT:
    case VALUE_NUMBER_INT:
      return "json-number";
    default:
      return "";
  }
}

JSON Highilighting Class

In order to keep the logic in EditingPane as simple as possible, the highlighting logic will take place in seaparated class.

package eu.leward.jschema.highlighting;

class Json {
  private final JsonFactory jsonFactory = new JsonFactory();

  public StyleSpans<Collection<String>> highlight(String code) {
    StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
    // TODO: Apply styling
    return spansBuilder.create();
  }
}

This won’t do much, but gives us some foundation to build on.

JSON Syntax Highlighting using Jackson

For each JsonToken matched, we will assign a CSS class if we want to color it and then add a new StyleSpan for that token.

public StyleSpans<Collection<String>> highlight(String code) {
  StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();

  try {
    JsonParser parser = jsonFactory.createParser(code);
    while (!parser.isClosed()) {
      JsonToken jsonToken = parser.nextToken();
      
      int length = parser.getTextLength();
      // Because getTextLength() does contain the surrounding ""
      if(jsonToken == JsonToken.VALUE_STRING || jsonToken == JsonToken.FIELD_NAME) {
        length += 2;
      }

      String className = jsonTokenToClassName(jsonToken);
      if (!className.isEmpty()) {
        spansBuilder.add(Collections.singleton(className), length);
      }
    }
  } catch (IOException e) {
    // Ignoring JSON parsing exception in the context of
    // syntax highlighting
  }

  return spansBuilder.create();
}

And if we run it…

Highlighting of JSON Properties is not correct

Uh oh. Not quite what we expected… So what happened here?

Did you notice when we add a StyleSpan to the builder we only specify the length of the span, not where it starts?

The thing is: Style Spans need to be contiguous from start to finish.

To overcome this issue we can track the last highlighted character, and fill in the blanks if needed.

Replace in the previous snippet to add spans of non styled text:

String className = jsonTokenToClassName(jsonToken);
if (!className.isEmpty()) {
    int start = (int) parser.getTokenLocation().getCharOffset();
    // Fill the gaps, since Style Spans need to be contiguous.
    if(start > lastPos)
    {
        int noStyleLength = start - lastPos;
        spansBuilder.add(Collections.emptyList(), noStyleLength);
    }
    lastPos = start + length;

    spansBuilder.add(Collections.singleton(className), length);
}

Let’s run it again.

Highlighting of JSON is now correct

It looks like we got it right this time 👍

Free tech tip: You can also add the following to avoid an error in case of an empty JSON document:

if(lastPos == 0) {
  spansBuilder.add(Collections.emptyList(), code.length());
}

A slightly different implementation is available in my 100-days-of-javafx GitHub repository.