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…
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.
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.