AsciiDocToText.java

1
package pro.verron.officestamper.asciidoc;
2
3
import java.util.List;
4
import java.util.Map;
5
import java.util.function.Function;
6
import java.util.stream.Collectors;
7
8
import static pro.verron.officestamper.asciidoc.AsciiDocModel.*;
9
10
/// The AsciiDocToText class is a utility for converting an [AsciiDocModel]
11
/// to a plain text representation. This class implements the [Function]
12
/// interface, where it transforms the given AsciiDoc model into a formatted string
13
/// based on specific rules for rendering various AsciiDoc elements.
14
///
15
/// The conversion logic handles multiple AsciiDoc constructs including, but not
16
/// limited to, headings, paragraphs, lists (ordered and unordered), tables,
17
/// blockquotes, images, inline elements with styling, code blocks, and macros.
18
/// Each AsciiDoc element is translated into its corresponding plain text
19
/// representation using custom rendering methods.
20
///
21
/// This class is immutable and operates as a stateless utility to ensure thread safety.
22
public final class AsciiDocToText
23
        implements Function<AsciiDocModel, String> {
24
25
    private static String renderInlines(List<Inline> inlines) {
26
        var sb = new StringBuilder();
27
        for (Inline inline : inlines) {
28
            sb.append(switch (inline) {
29
                case Text(String text) -> text;
30
                case Bold(List<Inline> children) -> "*%s*".formatted(renderInlines(children));
31
                case Italic(List<Inline> children) -> "_%s_".formatted(renderInlines(children));
32
                case Sup(List<Inline> children) -> "^%s^".formatted(renderInlines(children));
33
                case Sub(List<Inline> children) -> "~%s~".formatted(renderInlines(children));
34
                case Tab _ -> sb.append("\t");
35
                case Link(String url, String text) -> "%s[%s]".formatted(url, text);
36
                case InlineImage(String path, Map<String, String> map) -> "image:%s[%s]".formatted(path,
37
                        map.entrySet()
38
                           .stream()
39 1 1. lambda$renderInlines$0 : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::lambda$renderInlines$0 → NO_COVERAGE
                           .map(e -> e.getKey() + "=" + e.getValue())
40
                           .collect(Collectors.joining(", ")));
41
                case Styled(String role, List<Inline> children) -> "[%s]#%s#".formatted(role, renderInlines(children));
42
                case InlineMacro(String name, String id, List<String> list) ->
43
                        "%s:%s[%s]".formatted(name, id, String.join(", ", list));
44
            });
45
        }
46 1 1. renderInlines : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderInlines → KILLED
        return sb.toString();
47
    }
48
49
    private static String renderCellContent(Cell cell, boolean isAsciidoc, int level) {
50
        var blockList = cell.blocks();
51 1 1. renderCellContent : negated conditional → KILLED
        if (!isAsciidoc) {
52 1 1. renderCellContent : negated conditional → KILLED
            if (blockList.isEmpty()) return "";
53
            Paragraph p = (Paragraph) blockList.getFirst();
54 1 1. renderCellContent : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderCellContent → KILLED
            return renderInlines(p.inlines());
55
        }
56
        else {
57 1 1. renderCellContent : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderCellContent → KILLED
            return blockList.stream()
58 1 1. lambda$renderCellContent$0 : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::lambda$renderCellContent$0 → KILLED
                            .map(block -> renderBlock(block, level))
59
                            .collect(Collectors.joining())
60
                            .trim();
61
        }
62
    }
63
64
    private static String renderBlock(Block block, int tableLevel) {
65 1 1. renderBlock : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderBlock → KILLED
        return switch (block) {
66 1 1. renderBlock : negated conditional → NO_COVERAGE
            case Heading(_, int level, List<Inline> inlines) -> renderHeading(level, inlines);
67
            case Paragraph(List<String> header, List<Inline> inlines) -> renderHeader(header) + renderInlines(inlines);
68
            case UnorderedList(List<ListItem> items1) -> renderList(items1, "* ");
69
            case OrderedList(List<ListItem> items) -> renderList(items, ". ");
70
            case Table(List<Row> rows) -> renderTable(rows, tableLevel);
71
            case Blockquote(List<Inline> inlines) -> renderBlockquote(inlines);
72
            case CodeBlock(String language, String content) -> renderCodeBlock(language, content);
73
            case ImageBlock(String url, String altText) -> renderImageBlock(url, altText);
74
            case OpenBlock openBlock -> render(openBlock);
75
            case MacroBlock(String name, String id, List<String> list) ->
76
                    "%s::%s[%s]".formatted(name, id, String.join(", ", list));
77
            case Break _ -> "<<<";
78
            case CommentLine(String comment) -> ("// %s").formatted(comment);
79
        } + "\n\n";
80
    }
81
82
    private static String render(OpenBlock openBlock) {
83
        var sb = new StringBuilder();
84
        sb.append("[%s]\n".formatted(String.join(", ", openBlock.header())));
85
        sb.append("--\n");
86
        openBlock.content()
87
                 .stream()
88 1 1. lambda$render$0 : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::lambda$render$0 → NO_COVERAGE
                 .map(p -> renderBlock(p, 0))
89 1 1. render : removed call to java/util/stream/Stream::forEach → NO_COVERAGE
                 .forEach(sb::append);
90
        sb.append("--\n");
91 1 1. render : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::render → NO_COVERAGE
        return sb.toString();
92
    }
93
94
    private static String renderTable(List<Row> rows, int level) {
95
        var cellDelimiter = switch (level) {
96
            case 0 -> "|";
97
            case 1 -> "!";
98
            default -> throw new IllegalArgumentException("Table nesting level must be between 0 and 1");
99
        };
100
        var tableDelimiter = cellDelimiter + "===";
101
        var sb = new StringBuilder();
102
        sb.append(tableDelimiter);
103
        sb.append("\n");
104
        for (Row row : rows) {
105
            var style = row.style();
106 1 1. renderTable : removed call to java/util/Optional::ifPresent → SURVIVED
            style.ifPresent(s -> sb.append("[%s]\n".formatted(s)));
107
            for (Cell cell : row.cells()) {
108
                var blockList = cell.blocks();
109
                var size = blockList.size();
110 4 1. renderTable : negated conditional → SURVIVED
2. renderTable : changed conditional boundary → KILLED
3. renderTable : negated conditional → KILLED
4. renderTable : negated conditional → KILLED
                boolean isAsciidoc = size > 1 || (size == 1 && !(blockList.getFirst() instanceof Paragraph));
111
                cell.style()
112 1 1. renderTable : removed call to java/util/Optional::ifPresent → SURVIVED
                    .ifPresent(s -> sb.append("[%s]\n".formatted(s)));
113 2 1. renderTable : negated conditional → KILLED
2. renderTable : Replaced integer addition with subtraction → KILLED
                sb.append(isAsciidoc ? "a" + cellDelimiter : cellDelimiter)
114
                  .append(renderCellContent(cell, isAsciidoc, level + 1))
115
                  .append("\n");
116
            }
117
        }
118
        sb.append(tableDelimiter);
119 1 1. renderTable : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderTable → KILLED
        return sb.toString();
120
    }
121
122
    private static String renderImageBlock(String url, String altText) {
123 1 1. renderImageBlock : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderImageBlock → NO_COVERAGE
        return "image::" + url + "[" + altText + "]";
124
    }
125
126
    private static String renderCodeBlock(String language, String content) {
127 2 1. renderCodeBlock : negated conditional → NO_COVERAGE
2. renderCodeBlock : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderCodeBlock → NO_COVERAGE
        return (language.isEmpty() ? "" : "[source," + language + "]\n") + "----\n" + content + "\n----";
128
    }
129
130
    private static String renderBlockquote(List<Inline> inlines) {
131 1 1. renderBlockquote : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderBlockquote → NO_COVERAGE
        return "____\n" + renderInlines(inlines) + "\n____";
132
    }
133
134
    private static String renderList(List<ListItem> items1, String x) {
135 1 1. renderList : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderList → NO_COVERAGE
        return items1.stream()
136 1 1. lambda$renderList$0 : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::lambda$renderList$0 → NO_COVERAGE
                     .map(item -> x + renderInlines(item.inlines()) + "\n")
137
                     .collect(Collectors.joining("\n"));
138
    }
139
140
    private static String renderHeading(int level, List<Inline> inlines) {
141 1 1. renderHeading : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderHeading → NO_COVERAGE
        return "=".repeat(level) + " " + renderInlines(inlines);
142
    }
143
144
    private static String renderHeader(List<String> header) {
145 1 1. renderHeader : negated conditional → KILLED
        if (header.isEmpty()) return "";
146 1 1. renderHeader : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderHeader → NO_COVERAGE
        return "[%s]\n".formatted(String.join(", ", header));
147
    }
148
149
    /// Applies the conversion logic on the given AsciiDoc model and renders its blocks
150
    /// into a concatenated string representation.
151
    ///
152
    /// @param model the AsciiDoc model containing blocks to be processed and rendered
153
    /// @return a string representation of the rendered blocks from the provided model
154
    public String apply(AsciiDocModel model) {
155 1 1. apply : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::apply → KILLED
        return model.getBlocks()
156
                    .stream()
157 1 1. lambda$apply$0 : replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::lambda$apply$0 → KILLED
                    .map((Block block) -> renderBlock(block, 0))
158
                    .collect(Collectors.joining());
159
    }
160
}

Mutations

39

1.1
Location : lambda$renderInlines$0
Killed by : none
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::lambda$renderInlines$0 → NO_COVERAGE

46

1.1
Location : renderInlines
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderInlines → KILLED

51

1.1
Location : renderCellContent
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
negated conditional → KILLED

52

1.1
Location : renderCellContent
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
negated conditional → KILLED

54

1.1
Location : renderCellContent
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderCellContent → KILLED

57

1.1
Location : renderCellContent
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderCellContent → KILLED

58

1.1
Location : lambda$renderCellContent$0
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::lambda$renderCellContent$0 → KILLED

65

1.1
Location : renderBlock
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderBlock → KILLED

66

1.1
Location : renderBlock
Killed by : none
negated conditional → NO_COVERAGE

88

1.1
Location : lambda$render$0
Killed by : none
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::lambda$render$0 → NO_COVERAGE

89

1.1
Location : render
Killed by : none
removed call to java/util/stream/Stream::forEach → NO_COVERAGE

91

1.1
Location : render
Killed by : none
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::render → NO_COVERAGE

106

1.1
Location : renderTable
Killed by : none
removed call to java/util/Optional::ifPresent → SURVIVED
Covering tests

110

1.1
Location : renderTable
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
changed conditional boundary → KILLED

2.2
Location : renderTable
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
negated conditional → KILLED

3.3
Location : renderTable
Killed by : none
negated conditional → SURVIVED
Covering tests

4.4
Location : renderTable
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
negated conditional → KILLED

112

1.1
Location : renderTable
Killed by : none
removed call to java/util/Optional::ifPresent → SURVIVED
Covering tests

113

1.1
Location : renderTable
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
negated conditional → KILLED

2.2
Location : renderTable
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
Replaced integer addition with subtraction → KILLED

119

1.1
Location : renderTable
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderTable → KILLED

123

1.1
Location : renderImageBlock
Killed by : none
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderImageBlock → NO_COVERAGE

127

1.1
Location : renderCodeBlock
Killed by : none
negated conditional → NO_COVERAGE

2.2
Location : renderCodeBlock
Killed by : none
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderCodeBlock → NO_COVERAGE

131

1.1
Location : renderBlockquote
Killed by : none
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderBlockquote → NO_COVERAGE

135

1.1
Location : renderList
Killed by : none
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderList → NO_COVERAGE

136

1.1
Location : lambda$renderList$0
Killed by : none
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::lambda$renderList$0 → NO_COVERAGE

141

1.1
Location : renderHeading
Killed by : none
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderHeading → NO_COVERAGE

145

1.1
Location : renderHeader
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
negated conditional → KILLED

146

1.1
Location : renderHeader
Killed by : none
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::renderHeader → NO_COVERAGE

155

1.1
Location : apply
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::apply → KILLED

157

1.1
Location : lambda$apply$0
Killed by : pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest.[engine:junit-jupiter]/[class:pro.verron.officestamper.asciidoc.test.DocxToAsciiDocTest]/[method:shouldRenderNestedTable()]
replaced return value with "" for pro/verron/officestamper/asciidoc/AsciiDocToText::lambda$apply$0 → KILLED

Active mutators

Tests examined


Report generated by PIT 1.23.1 support