AsciiDocToFx.java
package pro.verron.officestamper.asciidoc;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.*;
import javafx.scene.text.Text;
import java.util.List;
import static pro.verron.officestamper.asciidoc.AsciiDocModel.*;
/// Renders [AsciiDocModel] into a JavaFX [Scene].
public final class AsciiDocToFx {
private static void emitInline(TextFlow flow, Inline inline, Font base, FontWeight weight, FontPosture posture) {
switch (inline) {
case AsciiDocModel.Text(String text) -> {
flow.getChildren()
.add(styledText(text, base, weight, posture));
return;
}
case Bold(List<Inline> children) -> {
for (Inline child : children) {
emitInline(flow, child, base, FontWeight.BOLD, posture);
}
return;
}
case Italic(List<Inline> children) -> {
for (Inline child : children) {
emitInline(flow, child, base, weight, FontPosture.ITALIC);
}
return;
}
case Link link -> {
Text t = styledText(link.text(), base, weight, posture);
t.setUnderline(true);
t.setFill(javafx.scene.paint.Color.BLUE);
flow.getChildren()
.add(t);
}
default -> { /* DO NOTHING */ }
}
if (inline instanceof InlineImage ii) {
flow.getChildren()
.add(new Text("[Image: " + ii.path() + "]"));
}
}
private static Font fontForHeading(int level) {
double size = switch (level) {
case 1 -> 24;
case 2 -> 20;
case 3 -> 18;
case 4 -> 16;
case 5 -> 14;
default -> 13;
};
return Font.font(Font.getDefault()
.getFamily(), FontWeight.BOLD, size);
}
private static Text styledText(String value, Font base, FontWeight weight, FontPosture posture) {
Text t = new Text(value);
t.setFont(Font.font(base.getFamily(), weight, posture, base.getSize()));
return t;
}
/// Compiles the model into a simple scrollable Scene using a VBox of TextFlow nodes.
///
/// Headings are rendered with larger font sizes; bold/italic are applied per inline fragment.
///
/// @param model parsed AsciiDoc model
///
/// @return JavaFX scene containing the rendered content
public Scene apply(AsciiDocModel model) {
var rootBox = new VBox(8.0);
rootBox.setPadding(new Insets(16));
for (Block block : model.getBlocks()) {
TextFlow flow = new TextFlow();
switch (block) {
case Heading(_, int level, List<Inline> inlines) -> {
for (Inline inline : inlines) {
emitInline(flow, inline, fontForHeading(level), FontWeight.NORMAL, FontPosture.REGULAR);
}
}
case Paragraph(_, List<Inline> inlines) -> {
for (Inline inline : inlines) {
emitInline(flow, inline, Font.getDefault(), FontWeight.NORMAL, FontPosture.REGULAR);
}
}
case UnorderedList(List<ListItem> items1) -> {
for (ListItem item : items1) {
TextFlow itemFlow = new TextFlow();
itemFlow.getChildren()
.add(new Text("• "));
for (Inline inline : item.inlines()) {
emitInline(itemFlow, inline, Font.getDefault(), FontWeight.NORMAL, FontPosture.REGULAR);
}
rootBox.getChildren()
.add(itemFlow);
}
continue;
}
case OrderedList(List<ListItem> items) -> {
int i = 1;
for (ListItem item : items) {
TextFlow itemFlow = new TextFlow();
itemFlow.getChildren()
.add(new Text((i++) + ". "));
for (Inline inline : item.inlines()) {
emitInline(itemFlow, inline, Font.getDefault(), FontWeight.NORMAL, FontPosture.REGULAR);
}
rootBox.getChildren()
.add(itemFlow);
}
continue;
}
case Blockquote(List<Inline> inlines) -> {
flow.setPadding(new Insets(0, 0, 0, 20));
for (Inline inline : inlines) {
emitInline(flow, inline, Font.getDefault(), FontWeight.NORMAL, FontPosture.ITALIC);
}
}
case CodeBlock cb -> {
flow.setStyle("-fx-background-color: #f4f4f4; -fx-font-family: 'monospace';");
flow.getChildren()
.add(new Text(cb.content()));
}
case ImageBlock(String url, String altText) -> flow.getChildren()
.add(new Text(
"[Image: " + url + " - " + altText + "]"));
default -> { /* Do NOTHING */ }
}
rootBox.getChildren()
.add(flow);
}
var scroll = new ScrollPane(rootBox);
scroll.setFitToWidth(true);
VBox.setVgrow(scroll, Priority.ALWAYS);
return new Scene(scroll, 800, 600);
}
}