AsciiDocToText.java
package pro.verron.officestamper.asciidoc;
import org.jspecify.annotations.NonNull;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import static pro.verron.officestamper.asciidoc.AsciiDocModel.*;
public final class AsciiDocToText
implements Function<AsciiDocModel, String> {
private static String renderInlines(List<@NonNull Inline> inlines) {
var sb = new StringBuilder();
for (Inline inline : inlines) {
sb.append(switch (inline) {
case Text(String text) -> text;
case Bold(List<Inline> children) -> "*%s*".formatted(renderInlines(children));
case Italic(List<Inline> children) -> "_%s_".formatted(renderInlines(children));
case Sup(List<Inline> children) -> "^%s^".formatted(renderInlines(children));
case Sub(List<Inline> children) -> "~%s~".formatted(renderInlines(children));
case Tab _ -> sb.append("\t");
case Link(String url, String text) -> "%s[%s]".formatted(url, text);
case InlineImage(String path, Map<String, String> map) -> "image:%s[%s]".formatted(path,
map.entrySet()
.stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining(", ")));
case Styled(String role, List<Inline> children) -> "[%s]#%s#".formatted(role, renderInlines(children));
case InlineMacro(String name, String id, List<String> list) ->
"%s:%s[%s]".formatted(name, id, String.join(", ", list));
});
}
return sb.toString();
}
private static String renderCellContent(Cell cell, boolean isAsciidoc, int level) {
var blockList = cell.blocks();
if (!isAsciidoc) {
if (blockList.isEmpty()) return "";
Paragraph p = (Paragraph) blockList.getFirst();
return renderInlines(p.inlines());
}
else {
return blockList.stream()
.map(block -> renderBlock(block, level))
.collect(Collectors.joining())
.trim();
}
}
private static String renderBlock(Block block, int tableLevel) {
return switch (block) {
case Heading(_, int level, List<Inline> inlines) -> renderHeading(level, inlines);
case Paragraph(List<String> header, List<Inline> inlines) -> renderHeader(header) + renderInlines(inlines);
case UnorderedList(List<ListItem> items1) -> renderList(items1, "* ");
case OrderedList(List<ListItem> items) -> renderList(items, ". ");
case Table(List<Row> rows) -> renderTable(rows, tableLevel);
case Blockquote(List<Inline> inlines) -> renderBlockquote(inlines);
case CodeBlock(String language, String content) -> renderCodeBlock(language, content);
case ImageBlock(String url, String altText) -> renderImageBlock(url, altText);
case OpenBlock openBlock -> render(openBlock);
case MacroBlock(String name, String id, List<String> list) ->
"%s::%s[%s]".formatted(name, id, String.join(", ", list));
case Break _ -> "<<<";
case CommentLine(String comment) -> ("// %s").formatted(comment);
} + "\n\n";
}
private static String render(OpenBlock openBlock) {
var sb = new StringBuilder();
sb.append("[%s]\n".formatted(String.join(", ", openBlock.header())));
sb.append("--\n");
openBlock.content()
.stream()
.map(p -> renderBlock(p, 0))
.forEach(sb::append);
sb.append("--\n");
return sb.toString();
}
private static String renderTable(List<Row> rows, int level) {
var cellDelimiter = switch (level) {
case 0 -> "|";
case 1 -> "!";
default -> throw new IllegalArgumentException("Table nesting level must be between 0 and 1");
};
var tableDelimiter = cellDelimiter + "===";
var sb = new StringBuilder();
sb.append(tableDelimiter);
sb.append("\n");
for (Row row : rows) {
var style = row.style();
style.ifPresent(s -> sb.append("[%s]\n".formatted(s)));
for (Cell cell : row.cells()) {
var blockList = cell.blocks();
var size = blockList.size();
boolean isAsciidoc = size > 1 || (size == 1 && !(blockList.getFirst() instanceof Paragraph));
cell.style()
.ifPresent(s -> sb.append("[%s]\n".formatted(s)));
sb.append(isAsciidoc ? "a" + cellDelimiter : cellDelimiter)
.append(renderCellContent(cell, isAsciidoc, level + 1))
.append("\n");
}
}
sb.append(tableDelimiter);
return sb.toString();
}
private static String renderImageBlock(String url, String altText) {
return "image::" + url + "[" + altText + "]";
}
private static String renderCodeBlock(String language, String content) {
return (language.isEmpty() ? "" : "[source," + language + "]\n") + "----\n" + content + "\n----";
}
private static String renderBlockquote(List<Inline> inlines) {
return "____\n" + renderInlines(inlines) + "\n____";
}
private static String renderList(List<ListItem> items1, String x) {
return items1.stream()
.map(item -> x + renderInlines(item.inlines()) + "\n")
.collect(Collectors.joining("\n"));
}
private static String renderHeading(int level, List<Inline> inlines) {
return "=".repeat(level) + " " + renderInlines(inlines);
}
private static String renderHeader(List<String> header) {
if (header.isEmpty()) return "";
return "[%s]\n".formatted(String.join(", ", header));
}
public String apply(AsciiDocModel model) {
return model.getBlocks()
.stream()
.map((Block block) -> renderBlock(block, 0))
.collect(Collectors.joining());
}
}