DocxRenderer.java
package pro.verron.officestamper.utils.wml;
import jakarta.xml.bind.JAXBElement;
import org.docx4j.TextUtils;
import org.docx4j.dml.CTBlip;
import org.docx4j.dml.CTBlipFillProperties;
import org.docx4j.dml.Graphic;
import org.docx4j.dml.GraphicData;
import org.docx4j.dml.picture.Pic;
import org.docx4j.dml.wordprocessingDrawing.Inline;
import org.docx4j.mce.AlternateContent;
import org.docx4j.model.structure.HeaderFooterPolicy;
import org.docx4j.model.structure.SectionWrapper;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.Part;
import org.docx4j.openpackaging.parts.WordprocessingML.*;
import org.docx4j.vml.CTShadow;
import org.docx4j.vml.CTShapetype;
import org.docx4j.vml.CTTextbox;
import org.docx4j.vml.VmlShapeElements;
import org.docx4j.wml.*;
import org.jspecify.annotations.NonNull;
import pro.verron.officestamper.utils.UtilsException;
import java.math.BigInteger;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Stream;
import static java.util.Optional.*;
import static java.util.stream.Collectors.joining;
import static pro.verron.officestamper.utils.bit.ByteUtils.readableSize;
import static pro.verron.officestamper.utils.bit.ByteUtils.sha1b64;
/// A utility class for rendering DOCX documents into string representations. This class provides comprehensive
/// functionality to convert various DOCX elements including paragraphs, tables, images, headers, footers, and other
/// document components into human-readable string format for debugging, testing, or display purposes.
///
/// The renderer handles complex document structures and maintains relationships between different document elements
/// while providing meaningful string representations of the content.
///
/// @author Joseph Verron
/// @version ${version}
/// @since 1.6.5
public class DocxRenderer {
private final StyleDefinitionsPart styleDefinitionsPart;
private final WordprocessingMLPackage wmlPackage;
private DocxRenderer(WordprocessingMLPackage wmlPackage) {
this.wmlPackage = wmlPackage;
var mainDocumentPart = wmlPackage.getMainDocumentPart();
this.styleDefinitionsPart = mainDocumentPart.getStyleDefinitionsPart(true);
}
/// Converts a DOCX document represented by a [WordprocessingMLPackage] into its string representation. The method
/// processes the main document content, headers, footers, footnotes, and endnotes (if present), and generates a
/// unified string representation of the document.
///
/// @param wordprocessingMLPackage the [WordprocessingMLPackage] instance representing the DOCX document to
/// be converted into a string; must not be null
///
/// @return a string containing the text and structural elements of the provided DOCX document
public static String docxToString(WordprocessingMLPackage wordprocessingMLPackage) {
return new DocxRenderer(wordprocessingMLPackage).stringify(wordprocessingMLPackage);
}
private static String stringify(Map<String, String> map) {
return map.entrySet()
.stream()
.map(e -> "%s=%s".formatted(e.getKey(), e.getValue()))
.collect(joining(",", "{", "}"));
}
private static <T> Optional<String> stringify(List<T> list, Function<T, Optional<String>> stringify) {
if (list == null) return empty();
if (list.isEmpty()) return empty();
return of(list.stream()
.map(stringify)
.flatMap(Optional::stream)
.collect(joining(",", "[", "]")));
}
private String stringify(WordprocessingMLPackage mlPackage) {
var header = stringifyHeaders(getHeaderPart(mlPackage));
var mainDocumentPart = mlPackage.getMainDocumentPart();
var body = stringify(mainDocumentPart);
var footer = stringifyFooters(getFooterPart(mlPackage));
var hStr = header.map("%s\n\n"::formatted)
.orElse("");
var fStr = footer.map("\n%s\n"::formatted)
.orElse("");
var footnotesPart = mainDocumentPart.getFootnotesPart();
var endnotesPart = mainDocumentPart.getEndNotesPart();
return hStr + body + stringify(footnotesPart).orElse("") + stringify(endnotesPart).orElse("") + fStr;
}
private Optional<String> stringifyHeaders(Stream<HeaderPart> headerPart) {
return headerPart.map(this::stringify)
.flatMap(Optional::stream)
.reduce((a, b) -> a + "\n\n" + b);
}
private Stream<HeaderPart> getHeaderPart(WordprocessingMLPackage document) {
var sections = document.getDocumentModel()
.getSections();
var set = new LinkedHashSet<HeaderPart>();
set.addAll(sections.stream()
.map(SectionWrapper::getHeaderFooterPolicy)
.map(HeaderFooterPolicy::getFirstHeader)
.filter(Objects::nonNull)
.toList());
set.addAll(sections.stream()
.map(SectionWrapper::getHeaderFooterPolicy)
.map(HeaderFooterPolicy::getDefaultHeader)
.filter(Objects::nonNull)
.toList());
set.addAll(sections.stream()
.map(SectionWrapper::getHeaderFooterPolicy)
.map(HeaderFooterPolicy::getEvenHeader)
.filter(Objects::nonNull)
.toList());
return set.stream();
}
private Object stringify(MainDocumentPart mainDocumentPart) {
return stringify(mainDocumentPart.getContent(), mainDocumentPart);
}
private Optional<String> stringifyFooters(Stream<FooterPart> footerPart) {
return footerPart.map(this::stringify)
.flatMap(Optional::stream)
.reduce((a, b) -> a + "\n\n" + b);
}
private Stream<FooterPart> getFooterPart(WordprocessingMLPackage document) {
var sections = document.getDocumentModel()
.getSections();
var set = new LinkedHashSet<FooterPart>();
set.addAll(sections.stream()
.map(SectionWrapper::getHeaderFooterPolicy)
.map(HeaderFooterPolicy::getFirstFooter)
.filter(Objects::nonNull)
.toList());
set.addAll(sections.stream()
.map(SectionWrapper::getHeaderFooterPolicy)
.map(HeaderFooterPolicy::getDefaultFooter)
.filter(Objects::nonNull)
.toList());
set.addAll(sections.stream()
.map(SectionWrapper::getHeaderFooterPolicy)
.map(HeaderFooterPolicy::getEvenFooter)
.filter(Objects::nonNull)
.toList());
return set.stream();
}
private Optional<String> stringify(FootnotesPart footnotesPart) {
if (footnotesPart == null) return Optional.empty();
try {
var list = footnotesPart.getContents()
.getFootnote()
.stream()
.map(c -> stringify(c, footnotesPart))
.flatMap(Optional::stream)
.toList();
if (list.isEmpty()) return Optional.empty();
return Optional.of(list.stream()
.collect(joining("\n", "[footnotes]\n---\n", "\n---\n")));
} catch (Docx4JException e) {
throw new UtilsException("Error processing footnotes", e);
}
}
private Optional<String> stringify(EndnotesPart endnotesPart) {
if (endnotesPart == null) return Optional.empty();
try {
var list = endnotesPart.getContents()
.getEndnote()
.stream()
.map(c -> stringify(c, endnotesPart))
.flatMap(Optional::stream)
.toList();
if (list.isEmpty()) return Optional.empty();
return Optional.of(list.stream()
.collect(joining("\n", "[endnotes]\n---\n", "\n---\n")));
} catch (Docx4JException e) {
throw new UtilsException("Error processing footnotes", e);
}
}
private Optional<String> stringify(HeaderPart part) {
var content = stringify(part.getContent(), part);
if (content.isEmpty()) return empty();
return of("""
[header, name="%s"]
----
%s
----""".formatted(part.getPartName(), content));
}
private String stringify(List<?> list, Part part) {
return list.stream()
.map(o -> stringify(o, part))
.collect(joining());
}
private Optional<String> stringify(FooterPart part) {
var content = stringify(part.getContent(), part);
if (content.isEmpty()) return empty();
return of("""
[footer, name="%s"]
----
%s
----""".formatted(part.getPartName(), content));
}
private Optional<String> stringify(CTFtnEdn c, Part part) {
var type = ofNullable(c.getType()).orElse(STFtnEdn.NORMAL);
if (STFtnEdn.NORMAL != type) return Optional.empty();
return Optional.of("[%s]%s".formatted(c.getId(), stringify(c.getContent(), part)));
}
/// Converts a [PPrBase.Spacing] object into an optional string representation. The method extracts and includes
/// non-null spacing properties such as "after", "before", "beforeLines", "afterLines", "line", and "lineRule" in
/// the resulting string. If all properties are null, the method returns an empty optional.
///
/// @param spacing the [PPrBase.Spacing] object containing paragraph spacing properties; can be null
///
/// @return an [Optional] containing the string representation of the spacing properties, or an empty optional if
/// the spacing object is null or contains no non-null properties
private Optional<String> stringify(PPrBase.Spacing spacing) {
if (spacing == null) return empty();
var map = new TreeMap<String, String>();
ofNullable(spacing.getAfter()).ifPresent(value -> map.put("after", String.valueOf(value)));
ofNullable(spacing.getBefore()).ifPresent(value -> map.put("before", String.valueOf(value)));
ofNullable(spacing.getBeforeLines()).ifPresent(value -> map.put("beforeLines", String.valueOf(value)));
ofNullable(spacing.getAfterLines()).ifPresent(value -> map.put("afterLines", String.valueOf(value)));
ofNullable(spacing.getLine()).ifPresent(value -> map.put("line", String.valueOf(value)));
ofNullable(spacing.getLineRule()).ifPresent(value -> map.put("lineRule", value.value()));
return map.isEmpty() ? empty() : of(stringify(map));
}
private String stringify(Text text) {
return TextUtils.getText(text);
}
private String styleID(String name) {
return styleDefinitionsPart.getNameForStyleID(name);
}
private String stringify(Br br) {
var type = br.getType();
if (type == STBrType.PAGE) return "\n[page-break]\n<<<\n";
else if (type == STBrType.COLUMN) return "\n[col-break]\n<<<\n";
else if (type == STBrType.TEXT_WRAPPING) return "<br/>\n";
else if (type == null) return "<br/>\n";
else throw new UtilsException("Unexpected type: " + type);
}
/// Converts the given [CTBlip] object into a formatted string representation. This method extracts image data
/// associated with the blip, calculates its size, and generates a detailed string with metadata including the part
/// name, embed identifier, content type, readable size, SHA-1 hash, and a custom value.
///
/// @param blip the [CTBlip] object representing an embedded image for which the string representation is to
/// be generated.
///
/// @return a formatted string containing metadata about the image extracted from the [CTBlip], including its size
/// and hash
private String stringify(CTBlip blip, Part part) {
var image = (BinaryPartAbstractImage) part.getRelationshipsPart()
.getPart(blip.getEmbed());
byte[] imageBytes = image.getBytes();
return "%s:%s:%s:%s:sha1=%s:cy=$d".formatted(image.getPartName(),
blip.getEmbed(),
image.getContentType(),
readableSize(imageBytes.length),
sha1b64(imageBytes));
}
private String stringify(PPrBase.Ind ind) {
if (ind == null) return "";
var map = new TreeMap<String, String>();
ofNullable(ind.getLeft()).ifPresent(value -> map.put("l", String.valueOf(value)));
ofNullable(ind.getRight()).ifPresent(value -> map.put("r", String.valueOf(value)));
ofNullable(ind.getFirstLine()).ifPresent(value -> map.put("fl", String.valueOf(value)));
ofNullable(ind.getHanging()).ifPresent(value -> map.put("h", String.valueOf(value)));
ofNullable(ind.getFirstLineChars()).ifPresent(value -> map.put("flc", String.valueOf(value)));
// TODO: no getEnd() before version 11.5.6 of docx4j, add it to the Treemap stop supporting early versions
// TODO: no getEndChars() before version 11.5.6 of docx4j, add it to the Treemap stop supporting early versions
return stringify(map);
}
/// Converts the given object into its string representation based on its type. The method supports various object
/// types and applies the appropriate logic to stringify the content. For unsupported object types, an exception is
/// thrown.
///
/// @param o the object to be converted into a string representation. It supports specific types including
/// JAXBElement, WordprocessingMLPackage, Tbl, Tr, Tc, MainDocumentPart, Body, List, Text, P, R, Drawing,
/// Inline, Graphic, and other types defined in the method. Passing null or unsupported types will result in
/// an exception.
///
/// @return the string representation of the provided object. For some specific types like `R.LastRenderedPageBreak`
/// or `CTMarkupRange`, an empty string may be returned. For objects like `R.Tab` or `R.Cr`, specific
/// strings such as tab or carriage return characters may be returned. In case of unsupported types or null,
/// an exception is thrown.
private String stringify(Object o, Part part) {
return switch (o) {
case JAXBElement<?> jaxb -> stringify(jaxb, part);
case WordprocessingMLPackage mlPackage -> stringify(mlPackage);
case Tbl tbl -> stringify(tbl, part);
case Tr tr -> stringify(tr, part);
case Tc tc -> stringify(tc, part);
case MainDocumentPart mainDocumentPart -> stringify(mainDocumentPart.getContent(), mainDocumentPart);
case Body body -> stringify(body.getContent(), part);
case List<?> list -> stringify(list, part);
case Text text -> stringify(text);
case P p -> stringify(p, part);
case R r -> stringify(r, part);
case Drawing drawing -> stringify(drawing, part);
case Inline inline -> stringify(inline, part);
case Graphic graphic -> stringify(graphic, part);
case GraphicData graphicData -> stringify(graphicData, part);
case Pic pic -> stringify(pic, part);
case CTBlipFillProperties bfp -> stringify(bfp, part);
case CTBlip blip -> stringify(blip, part);
case Br br -> stringify(br);
case R.Tab _ -> "\t";
case R.Cr _ -> "<carriage return>\n";
case R.CommentReference cr -> stringify(cr);
case CommentRangeStart crs -> stringify(crs);
case CommentRangeEnd cre -> stringify(cre);
case SdtBlock block -> stringify(block, part);
case Pict pict -> stringify(pict.getAnyAndAny(), part);
case CTShapetype _ -> "jsldkflksdhlkfhszdlkfnsdl";
case VmlShapeElements vmlShapeElements -> stringify(vmlShapeElements, part);
case CTTextbox ctTextbox -> stringify(ctTextbox.getTxbxContent(), part);
case CTTxbxContent content -> stringify(content.getContent(), part);
case SdtRun run -> stringify(run.getSdtContent(), part);
case SdtContent content -> stringify(content, part);
case CTFtnEdnRef ref -> "[%s]".formatted(ref.getId());
case FootnotesPart footnotesPart -> stringify(footnotesPart).orElse("");
case EndnotesPart endnotesPart -> stringify(endnotesPart).orElse("");
case FldChar fldChar -> stringify(fldChar).orElse("");
case P.Hyperlink hyperlink -> stringify(hyperlink, part).orElse("");
case CTSmartTagRun smartTagRun -> stringify(smartTagRun, part);
case CTAttr ctAttr -> stringify(ctAttr);
case R.Separator _, R.ContinuationSeparator _ -> "\n";
case AlternateContent _, R.LastRenderedPageBreak _, CTShadow _, CTMarkupRange _, ProofErr _,
R.AnnotationRef _, R.FootnoteRef _, R.EndnoteRef _ -> "";
case null -> throw new RuntimeException("Unsupported content: NULL");
default -> throw new RuntimeException("Unsupported content: " + o.getClass());
};
}
private @NonNull String stringify(CTAttr ctAttr) {
return "%s:%s".formatted(ctAttr.getName(), ctAttr.getVal());
}
private @NonNull String stringify(CTSmartTagRun smartTagRun, Part part) {
var smartTagPr = smartTagRun.getSmartTagPr();
return "<tag element=\"%s\" attr=\"%s\">%s<\\tag>".formatted(smartTagRun.getElement(),
stringify(smartTagPr.getAttr(), part),
stringify(smartTagRun.getContent(), part));
}
private String stringify(JAXBElement<?> element, Part part) {
if (element == null) return "";
var elementName = element.getName();
var localPart = elementName.getLocalPart();
if (localPart.equals("instrText")) return "[instrText=" + stringify(element.getValue(), part) + "]";
return stringify(element.getValue(), part);
}
private Optional<String> stringify(FldChar fldChar) {
var fldData = fldChar.getFldData();
return ofNullable(fldData).map(Text::getValue)
.map("[fldchar %s]"::formatted);
}
private Optional<String> stringify(P.Hyperlink hyperlink, Part part) {
return of("[link data=%s]".formatted(stringify(hyperlink.getContent(), part)));
}
private String stringify(SdtBlock block, Part part) {
return stringify(block.getSdtContent(), part) + "\n";
}
private String stringify(SdtContent content, Part part) {
return "[" + stringify(content.getContent(), part).trim() + "]";
}
private String stringify(VmlShapeElements vmlShapeElements, Part part) {
return "[" + stringify(vmlShapeElements.getEGShapeElements(), part).trim() + "]\n";
}
private String stringify(CommentRangeStart crs) {
return "<" + crs.getId() + "|";
}
private String stringify(CommentRangeEnd cre) {
return "|" + cre.getId() + ">";
}
private String stringify(Tc tc, Part part) {
var content = stringify(tc.getContent(), part);
return "|%s\n".formatted(content.trim());
}
private String stringify(Tr tr, Part part) {
var content = stringify(tr.getContent(), part);
return "%s\n".formatted(content);
}
private String stringify(Tbl tbl, Part part) {
var content = stringify(tbl.getContent(), part);
return ("|===\n%s\n|===\n").formatted(content);
}
private String stringify(Pic pic, Part part) {
return stringify(pic.getBlipFill(), part);
}
private String stringify(CTBlipFillProperties blipFillProperties, Part part) {
return stringify(blipFillProperties.getBlip(), part);
}
private String stringify(R.CommentReference commentReference) {
var id = commentReference.getId();
var stringifiedComment = stringifyComment(id);
return "<%s|%s>".formatted(id, stringifiedComment);
}
private String stringifyComment(BigInteger id) {
return WmlUtils.findComment(wmlPackage, id)
.map(Comments.Comment::getContent)
.map(l -> stringify(l, wmlPackage.getMainDocumentPart()))
.orElseThrow()
.strip();
}
private String stringify(GraphicData graphicData, Part part) {
return stringify(graphicData.getPic(), part);
}
private String stringify(Graphic graphic, Part part) {
return stringify(graphic.getGraphicData(), part);
}
private String stringify(Inline inline, Part part) {
var graphic = inline.getGraphic();
var extent = inline.getExtent();
return "%s:%d".formatted(stringify(graphic, part), extent.getCx());
}
private String stringify(Drawing drawing, Part part) {
return stringify(drawing.getAnchorOrInline(), part);
}
private Function<String, String> stringify(PPr pPr) {
if (pPr == null) return Function.identity();
var set = new TreeMap<String, String>();
ofNullable(pPr.getPStyle()).ifPresent(element -> set.put("pStyle", styleID(element.getVal())));
ofNullable(pPr.getJc()).ifPresent(element -> set.put("jc",
element.getVal()
.toString()));
ofNullable(pPr.getInd()).ifPresent(element -> set.put("ind", stringify(element)));
ofNullable(pPr.getKeepLines()).ifPresent(element -> set.put("keepLines", String.valueOf(element.isVal())));
ofNullable(pPr.getKeepNext()).ifPresent(element -> set.put("keepNext", String.valueOf(element.isVal())));
ofNullable(pPr.getOutlineLvl()).ifPresent(element -> set.put("outlineLvl",
element.getVal()
.toString()));
ofNullable(pPr.getPageBreakBefore()).ifPresent(element -> set.put("pageBreakBefore",
String.valueOf(element.isVal())));
ofNullable(pPr.getPBdr()).ifPresent(_ -> set.put("pBdr", "xxx"));
ofNullable(pPr.getPPrChange()).ifPresent(_ -> set.put("pPrChange", "xxx"));
stringify(pPr.getRPr()).ifPresent(key -> set.put("rPr", key));
stringify(pPr.getSectPr()).ifPresent(key -> set.put("sectPr", key));
ofNullable(pPr.getShd()).ifPresent(_ -> set.put("shd", "xxx"));
stringify(pPr.getSpacing()).ifPresent(spacing -> set.put("spacing", spacing));
ofNullable(pPr.getSuppressAutoHyphens()).ifPresent(_ -> set.put("suppressAutoHyphens", "xxx"));
ofNullable(pPr.getSuppressLineNumbers()).ifPresent(_ -> set.put("suppressLineNumbers", "xxx"));
ofNullable(pPr.getSuppressOverlap()).ifPresent(_ -> set.put("suppressOverlap", "xxx"));
ofNullable(pPr.getTabs()).ifPresent(_ -> set.put("tabs", "xxx"));
ofNullable(pPr.getTextAlignment()).ifPresent(_ -> set.put("textAlignment", "xxx"));
ofNullable(pPr.getTextDirection()).ifPresent(_ -> set.put("textDirection", "xxx"));
ofNullable(pPr.getTopLinePunct()).ifPresent(_ -> set.put("topLinePunct", "xxx"));
ofNullable(pPr.getWidowControl()).ifPresent(_ -> set.put("widowControl", "xxx"));
ofNullable(pPr.getFramePr()).ifPresent(_ -> set.put("framePr", "xxx"));
ofNullable(pPr.getWordWrap()).ifPresent(_ -> set.put("wordWrap", "xxx"));
ofNullable(pPr.getDivId()).ifPresent(_ -> set.put("divId", "xxx"));
ofNullable(pPr.getCnfStyle()).ifPresent(style -> set.put("cnfStyle", style.getVal()));
return set.entrySet()
.stream()
.reduce(Function.identity(), (f, entry) -> switch (entry.getKey()) {
case "pStyle" -> f.compose(decorateWithStyle(entry.getValue()));
case "sectPr" -> f.compose(str -> str + "\n[section-break, " + entry.getValue() + "]\n<<<");
default -> f.andThen(s -> s + "<%s=%s>".formatted(entry.getKey(), entry.getValue()));
}, Function::andThen);
}
private String stringify(P p, Part part) {
var runs = stringify(p.getContent(), part);
var ppr = stringify(p.getPPr());
return ppr.apply(runs) + "\n\n";
}
/// Converts the properties of the provided [RPrAbstract] object into a string representation. The method extracts
/// various attributes of the [RPrAbstract] object, such as formatting, styling, and other properties, and maps them
/// to a key-value representation. If no attributes are present or the provided object is null, an empty `Optional`
/// is returned.
///
/// @param rPr the [RPrAbstract] object containing formatting and style attributes; can be null
///
/// @return an [Optional<String>] containing the string representation of the given attributes, or an empty
/// [Optional] if the input is null or no attributes are present
private Optional<String> stringify(RPrAbstract rPr) {
if (rPr == null) return empty();
var map = new TreeMap<String, String>();
ofNullable(rPr.getB()).ifPresent(value -> map.put("b", String.valueOf(value.isVal())));
ofNullable(rPr.getBdr()).ifPresent(_ -> map.put("bdr", "xxx"));
ofNullable(rPr.getCaps()).ifPresent(value -> map.put("caps", String.valueOf(value.isVal())));
ofNullable(rPr.getColor()).ifPresent(value -> map.put("color", value.getVal()));
ofNullable(rPr.getDstrike()).ifPresent(value -> map.put("dstrike", String.valueOf(value.isVal())));
ofNullable(rPr.getI()).ifPresent(value -> map.put("i", String.valueOf(value.isVal())));
ofNullable(rPr.getKern()).ifPresent(value -> map.put("kern", String.valueOf(value.getVal())));
ofNullable(rPr.getLang()).ifPresent(value -> map.put("lang", value.getVal()));
stringify(rPr.getRFonts()).ifPresent(e -> map.put("rFont", e));
ofNullable(rPr.getRStyle()).ifPresent(value -> map.put("rStyle", value.getVal()));
ofNullable(rPr.getRtl()).ifPresent(value -> map.put("rtl", String.valueOf(value.isVal())));
ofNullable(rPr.getShadow()).ifPresent(value -> map.put("shadow", String.valueOf(value.isVal())));
ofNullable(rPr.getShd()).ifPresent(value -> map.put("shd", value.getColor()));
ofNullable(rPr.getSmallCaps()).ifPresent(value -> map.put("smallCaps", String.valueOf(value.isVal())));
ofNullable(rPr.getVertAlign()).ifPresent(value -> map.put("vertAlign",
value.getVal()
.value()));
ofNullable(rPr.getSpacing()).ifPresent(value -> map.put("spacing", String.valueOf(value.getVal())));
ofNullable(rPr.getStrike()).ifPresent(value -> map.put("strike", String.valueOf(value.isVal())));
ofNullable(rPr.getOutline()).ifPresent(value -> map.put("outline", String.valueOf(value.isVal())));
ofNullable(rPr.getEmboss()).ifPresent(value -> map.put("emboss", String.valueOf(value.isVal())));
ofNullable(rPr.getImprint()).ifPresent(value -> map.put("imprint", String.valueOf(value.isVal())));
ofNullable(rPr.getNoProof()).ifPresent(value -> map.put("noProof", String.valueOf(value.isVal())));
ofNullable(rPr.getSpecVanish()).ifPresent(value -> map.put("specVanish", String.valueOf(value.isVal())));
ofNullable(rPr.getU()).ifPresent(value -> map.put("u",
value.getVal()
.value()));
ofNullable(rPr.getVanish()).ifPresent(value -> map.put("vanish", String.valueOf(value.isVal())));
ofNullable(rPr.getW()).ifPresent(value -> map.put("w", String.valueOf(value.getVal())));
ofNullable(rPr.getWebHidden()).ifPresent(value -> map.put("webHidden", String.valueOf(value.isVal())));
ofNullable(rPr.getHighlight()).ifPresent(value -> map.put("highlight", value.getVal()));
ofNullable(rPr.getEffect()).ifPresent(value -> map.put("effect",
value.getVal()
.value()));
return map.isEmpty() ? empty() : of(stringify(map));
}
private Function<? super String, String> decorateWithStyle(String value) {
return switch (value) {
case "Title" -> "= %s\n"::formatted;
case "heading 1" -> "== %s\n"::formatted;
case "heading 2" -> "=== %s\n"::formatted;
case "heading 3" -> "==== %s\n"::formatted;
case "heading 4" -> "===== %s\n"::formatted;
case "heading 5" -> "====== %s\n"::formatted;
case "heading 6" -> "======= %s\n"::formatted;
case "caption" -> ".%s"::formatted;
case "annotation text", "footnote text", "endnote text" -> string -> string;
default -> "[%s] %%s".formatted(value)::formatted;
};
}
private String stringify(R run, Part part) {
String serialized = stringify(run.getContent(), part);
if (serialized.isEmpty()) return "";
return ofNullable(run.getRPr()).flatMap(this::stringify)
.map(rPr -> "❬%s❘%s❭".formatted(serialized, rPr))
.orElse(serialized);
}
private Optional<String> stringify(RFonts rFonts) {
if (rFonts == null) return empty();
var map = new TreeMap<String, String>();
ofNullable(rFonts.getAscii()).ifPresent(value -> map.put("ascii", value));
ofNullable(rFonts.getHAnsi()).ifPresent(value -> map.put("hAnsi", value));
ofNullable(rFonts.getCs()).ifPresent(value -> map.put("cs", value));
ofNullable(rFonts.getEastAsia()).ifPresent(value -> map.put("eastAsia", value));
ofNullable(rFonts.getAsciiTheme()).ifPresent(value -> map.put("asciiTheme", value.value()));
ofNullable(rFonts.getHAnsiTheme()).ifPresent(value -> map.put("hAnsiTheme", value.value()));
ofNullable(rFonts.getCstheme()).ifPresent(value -> map.put("cstheme", value.value()));
ofNullable(rFonts.getEastAsiaTheme()).ifPresent(value -> map.put("eastAsiaTheme", value.value()));
return map.isEmpty() ? empty() : of(stringify(map));
}
private Optional<String> stringify(SectPr sectPr) {
if (sectPr == null) return empty();
var map = new TreeMap<String, String>();
stringify(sectPr.getEGHdrFtrReferences(), this::stringify).ifPresent(value -> map.put("eGHdrFtrReferences",
value));
stringify(sectPr.getPgSz()).ifPresent(value -> map.put("pgSz", value));
stringify(sectPr.getPgMar()).ifPresent(value -> map.put("pgMar", value));
ofNullable(sectPr.getPaperSrc()).ifPresent(_ -> map.put("paperSrc", "xxx"));
ofNullable(sectPr.getBidi()).ifPresent(_ -> map.put("bidi", "xxx"));
ofNullable(sectPr.getRtlGutter()).ifPresent(_ -> map.put("rtlGutter", "xxx"));
stringify(sectPr.getDocGrid()).ifPresent(value -> map.put("docGrid", value));
ofNullable(sectPr.getFormProt()).ifPresent(_ -> map.put("formProt", "xxx"));
ofNullable(sectPr.getVAlign()).ifPresent(_ -> map.put("vAlign", "xxx"));
ofNullable(sectPr.getNoEndnote()).ifPresent(_ -> map.put("noEndnote", "xxx"));
ofNullable(sectPr.getTitlePg()).ifPresent(_ -> map.put("titlePg", "xxx"));
ofNullable(sectPr.getTextDirection()).ifPresent(_ -> map.put("textDirection", "xxx"));
ofNullable(sectPr.getRtlGutter()).ifPresent(_ -> map.put("rtlGutter", "xxx"));
return map.isEmpty() ? empty() : of(stringify(map));
}
private Optional<String> stringify(CTRel ctRel) {
if (ctRel == null) return empty();
var map = new TreeMap<String, String>();
ofNullable(ctRel.getId()).ifPresent(value -> map.put("id", value));
return map.isEmpty() ? empty() : of(stringify(map));
}
private Optional<String> stringify(SectPr.PgSz pgSz) {
if (pgSz == null) return empty();
var map = new TreeMap<String, String>();
ofNullable(pgSz.getOrient()).ifPresent(value -> map.put("orient", String.valueOf(value)));
ofNullable(pgSz.getW()).ifPresent(value -> map.put("w", String.valueOf(value)));
ofNullable(pgSz.getH()).ifPresent(value -> map.put("h", String.valueOf(value)));
ofNullable(pgSz.getCode()).ifPresent(value -> map.put("code", String.valueOf(value)));
return map.isEmpty() ? empty() : of(stringify(map));
}
private Optional<String> stringify(SectPr.PgMar pgMar) {
if (pgMar == null) return empty();
var map = new TreeMap<String, String>();
ofNullable(pgMar.getHeader()).ifPresent(value -> map.put("header", String.valueOf(value)));
ofNullable(pgMar.getFooter()).ifPresent(value -> map.put("footer", String.valueOf(value)));
ofNullable(pgMar.getGutter()).ifPresent(value -> map.put("gutter", String.valueOf(value)));
ofNullable(pgMar.getTop()).ifPresent(value -> map.put("top", String.valueOf(value)));
ofNullable(pgMar.getLeft()).ifPresent(value -> map.put("left", String.valueOf(value)));
ofNullable(pgMar.getBottom()).ifPresent(value -> map.put("bottom", String.valueOf(value)));
ofNullable(pgMar.getRight()).ifPresent(value -> map.put("right", String.valueOf(value)));
return map.isEmpty() ? empty() : of(stringify(map));
}
private Optional<String> stringify(CTDocGrid ctDocGrid) {
if (ctDocGrid == null) return empty();
var map = new TreeMap<String, String>();
ofNullable(ctDocGrid.getCharSpace()).ifPresent(value -> map.put("charSpace", String.valueOf(value)));
ofNullable(ctDocGrid.getLinePitch()).ifPresent(value -> map.put("linePitch", String.valueOf(value)));
ofNullable(ctDocGrid.getType()).ifPresent(value -> map.put("type", String.valueOf(value)));
return map.isEmpty() ? empty() : of(stringify(map));
}
}