AsciiDocPreviewBlockMacro.java
package pro.verron.asciidoc.preview;
import org.asciidoctor.ast.StructuralNode;
import org.asciidoctor.extension.BlockMacroProcessor;
import org.asciidoctor.extension.Name;
import pro.verron.asciidoc.compiler.AsciiDocCompiler;
import pro.verron.asciidoc.core.AsciiDocModel;
import java.awt.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import static java.util.Collections.emptyList;
/// Block macro that embeds a visual preview of an external AsciiDoc template
/// as an image within the current document.
///
/// Usage: `preview::template.adoc[theme=word,format=png,dpi=192]`
///
/// Attributes:
/// - `theme` — the rendering theme (default: `word`)
/// - `format` — output format, `png` or `svg` (default: `png`)
/// - `dpi` — resolution in dots per inch (default: `96`)
///
/// Output images are cached and only regenerated when the source file changes.
///
/// @see AsciiDocPreviewExtensionRegistry
@Name("preview")
public class AsciiDocPreviewBlockMacro
extends BlockMacroProcessor {
/// Constructs a new macro processor with the default macro name.
public AsciiDocPreviewBlockMacro() {
}
/// Constructs a new [AsciiDocPreviewBlockMacro] with the specified macro name.
///
/// @param macroName the name of the macro
public AsciiDocPreviewBlockMacro(String macroName) {
super(macroName);
}
@Override
public StructuralNode process(StructuralNode parent, String target, Map<String, Object> attributes) {
String docDirAttr = (String) parent.getDocument()
.getAttribute("docdir");
if (docDirAttr == null) docDirAttr = ".";
Path docDir = Paths.get(docDirAttr);
Path adocPath = docDir.resolve(target);
if (!Files.exists(adocPath)) {
return createBlock(parent, "paragraph", "Preview file not found: " + adocPath.toAbsolutePath());
}
String theme = (String) attributes.getOrDefault("theme", "word");
String format = (String) attributes.getOrDefault("format", "png");
int dpi = Integer.parseInt((String) attributes.getOrDefault("dpi", "96"));
try {
String content = Files.readString(adocPath);
AsciiDocModel model = AsciiDocCompiler.toModel(content);
// Re-create model with overridden theme
Map<String, String> newAttributes = new HashMap<>(model.getAttributes());
newAttributes.put("theme", theme);
model = AsciiDocModel.of(newAttributes, model.getBlocks());
String baseName = target.contains(".") ? target.substring(0, target.lastIndexOf('.')) : target;
String fileName = baseName + "-" + theme + "-" + dpi + "." + format;
String imagesOutDirAttr = (String) parent.getDocument()
.getAttribute("imagesoutdir");
Path imagesOutDir;
if (imagesOutDirAttr != null) {
imagesOutDir = Paths.get(imagesOutDirAttr);
}
else {
String outDirAttr = (String) parent.getDocument()
.getAttribute("outdir");
if (outDirAttr != null) {
imagesOutDir = Paths.get(outDirAttr);
}
else {
imagesOutDir = docDir;
}
}
Path outputPath = imagesOutDir.resolve(fileName);
Files.createDirectories(imagesOutDir);
// Caching: check if output exists and is newer than source
if (!Files.exists(outputPath) || Files.getLastModifiedTime(outputPath)
.toMillis() < Files.getLastModifiedTime(adocPath)
.toMillis()) {
if ("svg".equalsIgnoreCase(format)) {
String svg = AsciiDocCompiler.toSvg(model);
Files.writeString(outputPath, svg);
}
else {
String svg = AsciiDocCompiler.toSvg(model);
AsciiDocCompiler.saveSvgAsImage(svg, outputPath, dpi, Color.WHITE);
}
}
Map<String, Object> imageAttributes = new HashMap<>();
imageAttributes.put("target", fileName);
imageAttributes.put("alt", "Preview of " + target);
return createBlock(parent, "image", emptyList(), imageAttributes, new HashMap<>());
} catch (IOException e) {
return createBlock(parent, "paragraph", "Error generating preview: " + e.getMessage());
}
}
}