AsciiDocModel.java

package pro.verron.officestamper.asciidoc;

import java.util.*;

import static java.util.Collections.emptyList;

/// Represents a minimal in-memory model of an AsciiDoc document.
///
/// This model intentionally supports a compact subset sufficient for rendering to WordprocessingML and JavaFX Scene: -
/// Headings (levels 1..6) using leading '=' markers - Paragraphs separated by blank lines - Inline emphasis for bold
/// and italic using AsciiDoc-like markers: *bold*, _italic_
public final class AsciiDocModel {
    private final List<Block> blocks;

    private AsciiDocModel(List<Block> blocks) {
        this.blocks = List.copyOf(blocks);
    }

    /// Creates a new [AsciiDocModel] from the provided blocks.
    ///
    /// @param blocks ordered content blocks
    ///
    /// @return immutable AsciiDocModel
    public static AsciiDocModel of(List<Block> blocks) {
        Objects.requireNonNull(blocks, "blocks");
        return new AsciiDocModel(new ArrayList<>(blocks));
    }

    /// Returns the ordered list of blocks comprising the document.
    ///
    /// @return immutable list of blocks
    public List<Block> getBlocks() {
        return blocks;
    }

    /// Marker interface for document blocks.
    public sealed interface Block
            permits Blockquote, Break, CodeBlock, CommentLine, Heading, ImageBlock, MacroBlock, OpenBlock,
            OrderedList, Paragraph, Table, UnorderedList {
        int size();
    }

    /// Inline fragment inside a paragraph/heading.
    public sealed interface Inline
            permits Bold, InlineImage, InlineMacro, Italic, Link, Styled, Sub, Sup, Tab, Text {
        /// Returns the text of the inline fragment.
        ///
        /// @return text
        String text();
    }

    /// Heading block (levels 1..6).
    ///
    /// @param level heading level
    /// @param inlines inline fragments
    public record Heading(List<String> header, int level, List<Inline> inlines)
            implements Block {
        public Heading(int level, List<Inline> inlines) {
            this(emptyList(), level, inlines);
        }

        /// Constructor.
        ///
        /// @param level heading level
        /// @param inlines inline fragments
        public Heading(List<String> header, int level, List<Inline> inlines) {
            if (level < 1 || level > 6) {
                throw new IllegalArgumentException("Heading level must be between 1 and 6");
            }
            this.header = List.copyOf(header);
            this.level = level;
            this.inlines = List.copyOf(inlines);
        }

        @Override
        public int size() {
            return 1;
        }
    }

    /// Paragraph block.
    ///
    /// @param inlines inline fragments
    public record Paragraph(List<String> header, List<Inline> inlines)
            implements Block {
        public Paragraph(List<Inline> inlines) {
            this(emptyList(), inlines);
        }

        /// Constructor.
        ///
        /// @param inlines inline fragments
        public Paragraph(List<String> header, List<Inline> inlines) {
            this.header = List.copyOf(header);
            this.inlines = List.copyOf(inlines);
        }

        @Override
        public int size() {
            return 1;
        }
    }

    /// Text fragment.
    ///
    /// @param text text
    public record Text(String text)
            implements Inline {}

    /// Bold inline that can contain nested inlines.
    ///
    /// @param children nested inline fragments
    public record Bold(List<Inline> children)
            implements Inline {
        /// Constructor.
        ///
        /// @param children nested inline fragments
        public Bold(List<Inline> children) {
            this.children = List.copyOf(children);
        }

        @Override
        public String text() {
            StringBuilder sb = new StringBuilder();
            for (Inline in : children) sb.append(in.text());
            return sb.toString();
        }
    }

    public record Sup(List<Inline> children)
            implements Inline {
        public Sup(List<Inline> children) {
            this.children = List.copyOf(children);
        }

        @Override
        public String text() {
            StringBuilder sb = new StringBuilder();
            for (Inline in : children) sb.append(in.text());
            return sb.toString();
        }
    }

    public record Sub(List<Inline> children)
            implements Inline {
        public Sub(List<Inline> children) {
            this.children = List.copyOf(children);
        }

        @Override
        public String text() {
            StringBuilder sb = new StringBuilder();
            for (Inline in : children) sb.append(in.text());
            return sb.toString();
        }
    }

    /// Italic inline that can contain nested inlines.
    ///
    /// @param children nested inline fragments
    public record Italic(List<Inline> children)
            implements Inline {
        /// Constructor.
        ///
        /// @param children nested inline fragments
        public Italic(List<Inline> children) {
            this.children = List.copyOf(children);
        }

        @Override
        public String text() {
            StringBuilder sb = new StringBuilder();
            for (Inline in : children) sb.append(in.text());
            return sb.toString();
        }
    }

    /// Inline tab marker to be rendered as a DOCX tab stop.
    public record Tab()
            implements Inline {
        @Override
        public String text() {
            return "\t";
        }
    }

    /// Simple table block: list of rows; each row is a list of cells; each cell contains inline content.
    ///
    /// @param rows table rows
    public record Table(List<Row> rows)
            implements Block {
        /// Constructor.
        ///
        /// @param rows table rows
        public Table(List<Row> rows) {
            this.rows = List.copyOf(rows);
        }

        @Override
        public int size() {
            return rows.stream()
                       .map(Row::cells)
                       .flatMap(Collection::stream)
                       .map(Cell::blocks)
                       .flatMap(Collection::stream)
                       .mapToInt(Block::size)
                       .sum();
        }
    }

    /// Table row.
    ///
    /// @param cells table cells
    public record Row(List<Cell> cells, Optional<String> style) {
        /// Constructor.
        ///
        /// @param cells table cells
        public Row(List<Cell> cells) {
            this(cells, Optional.empty());
        }

        public Row(List<Cell> cells, Optional<String> style) {
            this.cells = List.copyOf(cells);
            this.style = style;
        }

        public static List<Row> listOf() {
            return List.of(of(Cell.listOf()));
        }

        private static Row of(List<Cell> cells) {
            return new Row(cells);
        }
    }

    /// Table cell.
    ///
    /// @param blocks cell content blocks
    public record Cell(List<Block> blocks, Optional<String> style) {
        public Cell(List<Block> blocks) {
            this(blocks, Optional.empty());
        }

        /// Constructor.
        ///
        /// @param blocks cell content blocks
        public Cell(List<Block> blocks, Optional<String> style) {
            this.blocks = List.copyOf(blocks);
            this.style = style;
        }

        private static List<Cell> listOf() {
            return List.of(ofInlines(List.of(new Text("A"))), ofInlines(List.of(new Text("B"))));
        }

        public static Cell ofInlines(List<Inline> inlines) {
            return new Cell(List.of(new Paragraph(inlines)));
        }
    }

    /// Unordered list.
    ///
    /// @param items list items
    public record UnorderedList(List<ListItem> items)
            implements Block {
        @Override
        public int size() {
            return items.size();
        }
    }

    /// Ordered list.
    ///
    /// @param items list items
    public record OrderedList(List<ListItem> items)
            implements Block {
        @Override
        public int size() {
            return items.size();
        }
    }

    /// List item.
    ///
    /// @param inlines inline fragments
    public record ListItem(List<Inline> inlines) {
        /// Constructor.
        ///
        /// @param inlines inline fragments
        public ListItem(List<Inline> inlines) {
            this.inlines = List.copyOf(inlines);
        }
    }

    /// Blockquote.
    ///
    /// @param inlines inline fragments
    public record Blockquote(List<Inline> inlines)
            implements Block {
        /// Constructor.
        ///
        /// @param inlines inline fragments
        public Blockquote(List<Inline> inlines) {
            this.inlines = List.copyOf(inlines);
        }

        @Override
        public int size() {
            return 1;
        }
    }

    /// Code block.
    ///
    /// @param language language
    /// @param content code content
    public record CodeBlock(String language, String content)
            implements Block {
        @Override
        public int size() {
            return 1;
        }
    }

    /// Image block.
    ///
    /// @param url image URL
    /// @param altText alternative text
    public record ImageBlock(String url, String altText)
            implements Block {
        @Override
        public int size() {
            return 1;
        }
    }

    /// Link inline.
    ///
    /// @param url link URL
    /// @param text link text
    public record Link(String url, String text)
            implements Inline {}

    /// Inline image.
    ///
    /// @param path image path
    /// @param map alternative text
    public record InlineImage(String path, Map<String, String> map)
            implements Inline {

        public InlineImage(String path, Map<String, String> map) {
            this.path = path;
            this.map = Collections.unmodifiableMap(new TreeMap<>(map));
        }

        @Override
        public String text() {
            return path;
        }
    }


    public record OpenBlock(List<String> header, List<Block> content)
            implements Block {
        @Override
        public int size() {
            return content.stream()
                          .mapToInt(Block::size)
                          .sum();
        }
    }

    public record Break()
            implements Block {
        @Override
        public int size() {
            return 0;
        }
    }

    public record CommentLine(String comment)
            implements Block {
        @Override
        public int size() {
            return 0;
        }
    }

    public record Styled(String role, List<Inline> children)
            implements Inline {
        @Override
        public String text() {
            StringBuilder sb = new StringBuilder();
            for (Inline in : children) sb.append(in.text());
            return sb.toString();
        }
    }

    public record MacroBlock(String name, String id, List<String> list)
            implements Block {
        @Override
        public int size() {
            return 1;
        }
    }

    public record InlineMacro(String name, String id, List<String> list)
            implements Inline {

        @Override
        public String text() {
            return String.join("", list);
        }
    }
}