AsciiDocToDocx.java
package pro.verron.asciidoc.docx;
import org.docx4j.jaxb.Context;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.PartName;
import org.docx4j.openpackaging.parts.WordprocessingML.DocumentSettingsPart;
import org.docx4j.openpackaging.parts.WordprocessingML.FooterPart;
import org.docx4j.openpackaging.parts.WordprocessingML.HeaderPart;
import org.docx4j.relationships.Relationship;
import org.docx4j.wml.*;
import org.jspecify.annotations.Nullable;
import pro.verron.asciidoc.core.*;
import pro.verron.asciidoc.core.Text;
import java.math.BigInteger;
import java.util.List;
import java.util.function.Function;
/// Renders an [AsciiDocModel] into a [WordprocessingMLPackage] using Docx4J.
///
/// Implements [Function]<[AsciiDocModel], [WordprocessingMLPackage]> and
/// converts headings, paragraphs, lists, tables, blockquotes, code blocks,
/// images, and inline elements (bold, italic, links) into their DOCX
/// counterparts.
///
/// @see DocxToAsciiDoc
public final class AsciiDocToDocx
implements Function<AsciiDocModel, WordprocessingMLPackage> {
private int headerCount = 1;
private int footerCount = 1;
/// Constructs a new [AsciiDocToDocx] converter.
public AsciiDocToDocx() {}
private static P createHeading(ObjectFactory factory, Heading heading) {
P p = factory.createP();
PPr ppr = factory.createPPr();
PPrBase.PStyle pStyle = factory.createPPrBasePStyle();
pStyle.setVal("Heading" + heading.level());
ppr.setPStyle(pStyle);
p.setPPr(ppr);
RPr headingRunPr = factory.createRPr();
// Increase size a bit relative to level if heading styles are missing
HpsMeasure sz = factory.createHpsMeasure();
int base = switch (heading.level()) {
case 1 -> 32; // 16pt
case 2 -> 28;
case 3 -> 26;
case 4 -> 24;
case 5 -> 22;
default -> 20;
};
sz.setVal(BigInteger.valueOf(base));
headingRunPr.setSz(sz);
headingRunPr.setSzCs(sz);
addInlines(factory, p, heading.inlines(), headingRunPr);
return p;
}
private static P createParagraph(
ObjectFactory factory,
Paragraph paragraph
) {
P p = factory.createP();
addInlines(factory, p, paragraph.inlines(), null);
return p;
}
private static void addInlines(
ObjectFactory factory,
P p,
List<Inline> inlines,
@Nullable RPr base
) {
for (Inline inline : inlines) {
emitInline(factory, p, inline, base);
}
}
private static Tbl createTable(ObjectFactory factory, Table table) {
Tbl tbl = factory.createTbl();
// Minimal table without explicit grid; Word will auto-fit columns
for (Row row : table.rows()) {
Tr tr = factory.createTr();
for (Cell cell : row.cells()) {
Tc tc = factory.createTc();
for (Block block : cell.blocks()) {
addBlock(factory, tc.getContent(), block);
}
tr.getContent()
.add(tc);
}
tbl.getContent()
.add(tr);
}
return tbl;
}
private static void addBlock(
ObjectFactory factory,
List<Object> content,
Block block
)
throws UnsupportedOperationException {
switch (block) {
case Heading h -> content.add(createHeading(factory, h));
case Paragraph p -> content.add(createParagraph(factory, p));
case Table t -> content.add(createTable(factory, t));
case UnorderedList(List<ListItem> items1) -> {
for (ListItem item : items1) {
content.add(createListItem(factory, item, "* "));
}
}
case OrderedList(List<ListItem> items) -> {
int i = 1;
for (ListItem item : items) {
content.add(createListItem(factory, item, (i++) + ". "));
}
}
case QuoteBlock b -> content.add(createBlockquote(factory, b));
case CodeBlock cb -> content.add(createCodeBlock(factory, cb));
case ImageBlock ib -> content.add(createImageBlock(factory, ib));
case Break _ -> throw new java.lang.UnsupportedOperationException(
"Breaks are not supported");
case CommentBlock _ -> throw new UnsupportedOperationException(
"Comments are not supported");
case OpenBlock ob -> {
for (Block subBlock : ob.content()) {
addBlock(factory, content, subBlock);
}
}
case MacroBlock macroBlock ->
throw new UnsupportedOperationException(
"Macro blocks are not supported");
}
}
private static P createListItem(
ObjectFactory factory,
ListItem item,
String prefix
) {
P p = factory.createP();
R r = factory.createR();
org.docx4j.wml.Text t = factory.createText();
t.setValue(prefix);
r.getContent()
.add(t);
p.getContent()
.add(r);
addInlines(factory, p, item.inlines(), null);
return p;
}
private static P createBlockquote(
ObjectFactory factory,
QuoteBlock quoteBlock
) {
P p = factory.createP();
PPr ppr = factory.createPPr();
PPrBase.Ind ind = factory.createPPrBaseInd();
ind.setLeft(BigInteger.valueOf(720)); // 0.5 inch
ppr.setInd(ind);
p.setPPr(ppr);
addInlines(factory, p, quoteBlock.inlines(), null);
return p;
}
private static P createCodeBlock(
ObjectFactory factory,
CodeBlock codeBlock
) {
P p = factory.createP();
RPr rpr = factory.createRPr();
RFonts fonts = factory.createRFonts();
fonts.setAscii("Courier New");
fonts.setHAnsi("Courier New");
rpr.setRFonts(fonts);
String[] lines = codeBlock.content()
.split("\n");
for (int i = 0; i < lines.length; i++) {
R r = factory.createR();
r.setRPr(rpr);
org.docx4j.wml.Text t = factory.createText();
t.setValue(lines[i]);
t.setSpace("preserve");
r.getContent()
.add(t);
if (i < lines.length - 1) {
r.getContent()
.add(factory.createBr());
}
p.getContent()
.add(r);
}
return p;
}
private static P createImageBlock(
ObjectFactory factory,
ImageBlock imageBlock
) {
P p = factory.createP();
R r = factory.createR();
org.docx4j.wml.Text t = factory.createText();
t.setValue("[Image: " + imageBlock.url() + " - " + imageBlock.altText()
+ "]");
r.getContent()
.add(t);
p.getContent()
.add(r);
return p;
}
private static void emitInline(
ObjectFactory factory,
P p,
Inline inline,
@Nullable RPr base
) {
switch (inline) {
case Text(String text) -> {
RPr rpr = base != null
? deepCopy(factory, base)
: factory.createRPr();
String[] lines = text.split("\n", -1);
for (int i = 0; i < lines.length; i++) {
if (!lines[i].isEmpty()) {
R r = factory.createR();
r.setRPr(rpr);
org.docx4j.wml.Text tx = factory.createText();
tx.setValue(lines[i]);
tx.setSpace("preserve");
r.getContent()
.add(tx);
p.getContent()
.add(r);
}
if (i < lines.length - 1) {
R r = factory.createR();
r.setRPr(rpr);
r.getContent()
.add(factory.createBr());
p.getContent()
.add(r);
}
}
return;
}
case Bold(List<Inline> children) -> {
RPr next = base != null
? deepCopy(factory, base)
: factory.createRPr();
next.setB(new BooleanDefaultTrue());
for (Inline child : children) {
emitInline(factory, p, child, next);
}
return;
}
case Italic(List<Inline> children) -> {
RPr next = base != null
? deepCopy(factory, base)
: factory.createRPr();
next.setI(new BooleanDefaultTrue());
for (Inline child : children) {
emitInline(factory, p, child, next);
}
return;
}
case Tab _ -> {
R r = factory.createR();
R.Tab tab = factory.createRTab();
r.getContent()
.add(tab);
p.getContent()
.add(r);
}
default -> { /* DO NOTHING */ }
}
if (inline instanceof Link link) {
R r = factory.createR();
RPr rpr = base != null
? deepCopy(factory, base)
: factory.createRPr();
Color color = factory.createColor();
color.setVal("0000FF");
rpr.setColor(color);
U u = factory.createU();
u.setVal(UnderlineEnumeration.SINGLE);
rpr.setU(u);
r.setRPr(rpr);
org.docx4j.wml.Text t = factory.createText();
t.setValue(link.text());
r.getContent()
.add(t);
p.getContent()
.add(r);
}
if (inline instanceof ImageInline ii) {
R r = factory.createR();
org.docx4j.wml.Text t = factory.createText();
t.setValue("[Image: " + ii.path() + "]");
r.getContent()
.add(t);
p.getContent()
.add(r);
}
}
private static RPr deepCopy(ObjectFactory factory, RPr src) {
// Minimal copy of relevant props; docx4j doesn't offer a trivial
// clone here.
RPr c = factory.createRPr();
if (src.getB() != null) {
BooleanDefaultTrue b = new BooleanDefaultTrue();
c.setB(b);
}
if (src.getI() != null) {
BooleanDefaultTrue i = new BooleanDefaultTrue();
c.setI(i);
}
if (src.getSz() != null) {
HpsMeasure sz = factory.createHpsMeasure();
sz.setVal(src.getSz()
.getVal());
c.setSz(sz);
HpsMeasure szCs = factory.createHpsMeasure();
szCs.setVal(src.getSz()
.getVal());
c.setSzCs(szCs);
}
return c;
}
private static boolean isHeaderOrFooter(OpenBlock ob) {
return ob.header()
.stream()
.anyMatch(h -> h.startsWith("header")
|| h.startsWith("footer"));
}
private static void addReference(
WordprocessingMLPackage pkg,
ObjectFactory factory,
String relId,
String role,
boolean isHeader
) {
SectPr sectPr = pkg.getMainDocumentPart()
.getJaxbElement()
.getBody()
.getSectPr();
if (sectPr == null) {
sectPr = factory.createSectPr();
pkg.getMainDocumentPart()
.getJaxbElement()
.getBody()
.setSectPr(sectPr);
}
HdrFtrRef type = switch (role) {
case "header-even", "footer-even" -> {
enableEvenOddHeaders(pkg, factory);
yield HdrFtrRef.EVEN;
}
case "header-first", "footer-first" -> {
sectPr.setTitlePg(new BooleanDefaultTrue());
yield HdrFtrRef.FIRST;
}
default -> HdrFtrRef.DEFAULT;
};
if (isHeader) {
HeaderReference ref = factory.createHeaderReference();
ref.setId(relId);
ref.setType(type);
sectPr.getEGHdrFtrReferences()
.add(ref);
}
else {
FooterReference ref = factory.createFooterReference();
ref.setId(relId);
ref.setType(type);
sectPr.getEGHdrFtrReferences()
.add(ref);
}
}
private static void enableEvenOddHeaders(
WordprocessingMLPackage pkg,
ObjectFactory factory
) {
try {
DocumentSettingsPart dsp = pkg.getMainDocumentPart()
.getDocumentSettingsPart();
if (dsp == null) {
dsp = new DocumentSettingsPart();
pkg.getMainDocumentPart()
.addTargetPart(dsp);
dsp.setJaxbElement(factory.createCTSettings());
}
dsp.getContents()
.setEvenAndOddHeaders(new BooleanDefaultTrue());
} catch (Docx4JException e) {
throw new RuntimeException(e);
}
}
/// Creates a new WordprocessingMLPackage and fills it with content from the
/// model.
///
/// @param model parsed AsciiDoc model
///
/// @return package containing the rendered document
@Override
public WordprocessingMLPackage apply(AsciiDocModel model) {
headerCount = 1;
footerCount = 1;
try {
var pkg = WordprocessingMLPackage.createPackage();
var factory = Context.getWmlObjectFactory();
var mainContent = pkg.getMainDocumentPart()
.getContent();
mainContent.clear();
for (Block block : model.getBlocks()) {
if (block instanceof OpenBlock ob && isHeaderOrFooter(ob)) {
processHeaderOrFooter(pkg, factory, ob);
}
else {
addBlock(factory, mainContent, block);
}
}
return pkg;
} catch (Docx4JException e) {
throw new IllegalStateException(
"Unable to create WordprocessingMLPackage",
e);
}
}
private void processHeaderOrFooter(
WordprocessingMLPackage pkg,
ObjectFactory factory,
OpenBlock ob
) {
String role = ob.header()
.get(0);
try {
if (role.startsWith("header")) {
HeaderPart hp = new HeaderPart(new PartName(
"/word/header" + (headerCount++) + ".xml"));
hp.setJaxbElement(factory.createHdr());
for (Block subBlock : ob.content()) {
addBlock(factory, hp.getContent(), subBlock);
}
Relationship rel = pkg.getMainDocumentPart()
.addTargetPart(hp);
addReference(pkg, factory, rel.getId(), role, true);
}
else if (role.startsWith("footer")) {
FooterPart fp = new FooterPart(new PartName(
"/word/footer" + (footerCount++) + ".xml"));
fp.setJaxbElement(factory.createFtr());
for (Block subBlock : ob.content()) {
addBlock(factory, fp.getContent(), subBlock);
}
Relationship rel = pkg.getMainDocumentPart()
.addTargetPart(fp);
addReference(pkg, factory, rel.getId(), role, false);
}
} catch (Docx4JException e) {
throw new RuntimeException(e);
}
}
}