diff --git a/CHANGES.md b/CHANGES.md
index a686e880dc..3bb39603f6 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -10,6 +10,8 @@ This document is intended for Spotless developers.
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
## [Unreleased]
+### Added
+- Add support for AsciiDoc formatting via `adocfmt`. ([#2960](https://github.com/diffplug/spotless/pull/2960))
### Fixed
- Support `ktfmt` 0.63 and use its new builder API for formatting options to better avoid future breaking changes.
### Changes
diff --git a/lib/src/main/java/com/diffplug/spotless/asciidoc/AdocfmtConfig.java b/lib/src/main/java/com/diffplug/spotless/asciidoc/AdocfmtConfig.java
new file mode 100644
index 0000000000..c5b1d2cb64
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/asciidoc/AdocfmtConfig.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2026 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.asciidoc;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Objects;
+
+public class AdocfmtConfig implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public boolean normalizeSetextHeadings = false;
+ public boolean collapseConsecutiveBlankLines = true;
+ public boolean oneSentencePerLine = false;
+ public boolean normalizeBlockDelimiters = true;
+ public boolean removeTrailingHeaderEqualsSign = true;
+ public boolean titleCase = false;
+ public boolean removeTrailingWhitespace = true;
+ public boolean normalizeListBullets = false;
+ public boolean normalizeOrderedListMarkers = false;
+ public boolean ensureHeadingBlankLines = true;
+ public boolean ensureSourceDelimiters = false;
+
+ public AdocfmtConfig() {}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (!(o instanceof AdocfmtConfig other))
+ return false;
+ return normalizeSetextHeadings == other.normalizeSetextHeadings
+ && collapseConsecutiveBlankLines == other.collapseConsecutiveBlankLines
+ && oneSentencePerLine == other.oneSentencePerLine
+ && normalizeBlockDelimiters == other.normalizeBlockDelimiters
+ && removeTrailingHeaderEqualsSign == other.removeTrailingHeaderEqualsSign
+ && titleCase == other.titleCase
+ && removeTrailingWhitespace == other.removeTrailingWhitespace
+ && normalizeListBullets == other.normalizeListBullets
+ && normalizeOrderedListMarkers == other.normalizeOrderedListMarkers
+ && ensureHeadingBlankLines == other.ensureHeadingBlankLines
+ && ensureSourceDelimiters == other.ensureSourceDelimiters;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(normalizeSetextHeadings, collapseConsecutiveBlankLines, oneSentencePerLine,
+ normalizeBlockDelimiters, removeTrailingHeaderEqualsSign, titleCase, removeTrailingWhitespace,
+ normalizeListBullets, normalizeOrderedListMarkers, ensureHeadingBlankLines, ensureSourceDelimiters);
+ }
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/asciidoc/AdocfmtStep.java b/lib/src/main/java/com/diffplug/spotless/asciidoc/AdocfmtStep.java
new file mode 100644
index 0000000000..0072a46b8e
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/asciidoc/AdocfmtStep.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2026 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.asciidoc;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Objects;
+
+import com.diffplug.spotless.FormatterFunc;
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.JarState;
+import com.diffplug.spotless.Provisioner;
+import com.diffplug.spotless.ThrowingEx;
+
+public final class AdocfmtStep implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ private static final String DEFAULT_VERSION = "0.2.0";
+ private static final String NAME = "adocfmt";
+ private static final String MAVEN_COORDINATE = "org.drjekyll:adocfmt:";
+
+ private final String version;
+ private final AdocfmtConfig config;
+ private final JarState.Promised jarState;
+
+ private AdocfmtStep(String version, AdocfmtConfig config, JarState.Promised jarState) {
+ this.version = version;
+ this.config = config;
+ this.jarState = jarState;
+ }
+
+ public static FormatterStep create(Provisioner provisioner) {
+ return create(defaultVersion(), provisioner);
+ }
+
+ public static FormatterStep create(String version, Provisioner provisioner) {
+ return create(version, provisioner, new AdocfmtConfig());
+ }
+
+ public static FormatterStep create(String version, Provisioner provisioner, AdocfmtConfig config) {
+ Objects.requireNonNull(version, "version");
+ Objects.requireNonNull(provisioner, "provisioner");
+ Objects.requireNonNull(config, "config");
+ return FormatterStep.create(NAME,
+ new AdocfmtStep(version, config, JarState.promise(() -> JarState.from(MAVEN_COORDINATE + version, provisioner))),
+ AdocfmtStep::equalityState,
+ State::createFormat);
+ }
+
+ public static String defaultVersion() {
+ return DEFAULT_VERSION;
+ }
+
+ private State equalityState() {
+ return new State(version, config, jarState.get());
+ }
+
+ private static final class State implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ private final String version;
+ private final AdocfmtConfig config;
+ private final JarState jarState;
+
+ State(String version, AdocfmtConfig config, JarState jarState) {
+ this.version = version;
+ this.config = config;
+ this.jarState = jarState;
+ }
+
+ FormatterFunc createFormat() throws Exception {
+ final ClassLoader classLoader = jarState.getClassLoader();
+ final Class> formatterClass = classLoader.loadClass("org.drjekyll.adocfmt.AsciidocFormatter");
+ final Class> configClass = classLoader.loadClass("org.drjekyll.adocfmt.AsciidocFormatterConfig");
+ final Method builderMethod = configClass.getMethod("builder");
+ Object builder = builderMethod.invoke(null);
+ final Class> builderClass = builder.getClass();
+ builder = builderClass.getMethod("normalizeSetextHeadings", boolean.class).invoke(builder, config.normalizeSetextHeadings);
+ builder = builderClass.getMethod("collapseConsecutiveBlankLines", boolean.class).invoke(builder, config.collapseConsecutiveBlankLines);
+ builder = builderClass.getMethod("oneSentencePerLine", boolean.class).invoke(builder, config.oneSentencePerLine);
+ builder = builderClass.getMethod("normalizeBlockDelimiters", boolean.class).invoke(builder, config.normalizeBlockDelimiters);
+ builder = builderClass.getMethod("removeTrailingHeaderEqualsSign", boolean.class).invoke(builder, config.removeTrailingHeaderEqualsSign);
+ builder = builderClass.getMethod("titleCase", boolean.class).invoke(builder, config.titleCase);
+ builder = builderClass.getMethod("removeTrailingWhitespace", boolean.class).invoke(builder, config.removeTrailingWhitespace);
+ builder = builderClass.getMethod("normalizeListBullets", boolean.class).invoke(builder, config.normalizeListBullets);
+ builder = builderClass.getMethod("normalizeOrderedListMarkers", boolean.class).invoke(builder, config.normalizeOrderedListMarkers);
+ builder = builderClass.getMethod("ensureHeadingBlankLines", boolean.class).invoke(builder, config.ensureHeadingBlankLines);
+ builder = builderClass.getMethod("ensureSourceDelimiters", boolean.class).invoke(builder, config.ensureSourceDelimiters);
+ final Object adocfmtConfig = builderClass.getMethod("build").invoke(builder);
+
+ final Object formatter = formatterClass.getConstructor(configClass).newInstance(adocfmtConfig);
+ final Method formatMethod = formatterClass.getMethod("format", String.class);
+ return input -> {
+ try {
+ return (String) formatMethod.invoke(formatter, input);
+ } catch (InvocationTargetException e) {
+ throw ThrowingEx.unwrapCause(e);
+ }
+ };
+ }
+ }
+}
diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md
index 47ce008f79..6153cb7a63 100644
--- a/plugin-gradle/CHANGES.md
+++ b/plugin-gradle/CHANGES.md
@@ -4,6 +4,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
+### Added
+- Add support for AsciiDoc formatting via `adocfmt`. ([#2960](https://github.com/diffplug/spotless/pull/2960))
### Fixed
- Prevent build caches from interfering when executing under the `-PspotlessIdeHook` mode. ([#2365](https://github.com/diffplug/spotless/issues/2365))
### Changes
diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md
index 339c719668..be5451276f 100644
--- a/plugin-gradle/README.md
+++ b/plugin-gradle/README.md
@@ -60,6 +60,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui
- [Groovy](#groovy) ([eclipse groovy](#eclipse-groovy))
- [Kotlin](#kotlin) ([ktfmt](#ktfmt), [ktlint](#ktlint), [diktat](#diktat), [tabletest-formatter](#tabletest-formatter-1), [prettier](#prettier))
- [Scala](#scala) ([scalafmt](#scalafmt))
+ - [Asciidoc](#asciidoc) ([adocfmt](#adocfmt))
- [C/C++](#cc) ([clang-format](#clang-format), [eclipse cdt](#eclipse-cdt))
- [Protobuf](#protobuf) ([buf](#buf), [clang-format](#clang-format))
- [Python](#python) ([black](#black))
@@ -830,6 +831,41 @@ spotless {
antlr4formatter('1.2.1') // version is optional
```
+## Asciidoc
+
+`com.diffplug.gradle.spotless.AsciidocExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.6.0/com/diffplug/gradle/spotless/AsciidocExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AsciidocExtension.java)
+
+```gradle
+spotless {
+ asciidoc {
+ target 'src/docs/**/*.adoc' // you have to set the target manually
+
+ adocfmt() // has its own section below
+ }
+}
+```
+
+### adocfmt
+
+[homepage](https://github.com/dheid/adocfmt). [available versions](https://search.maven.org/artifact/org.drjekyll/adocfmt).
+
+```gradle
+adocfmt('0.2.0') // version is optional
+ .normalizeSetextHeadings(false) // convert === underlines to ATX == (default: false)
+ .collapseConsecutiveBlankLines(true) // max 1 blank line (default: true)
+ .oneSentencePerLine(false) // each sentence on its own line (default: false)
+ .normalizeBlockDelimiters(true) // exactly 4 characters (e.g. ----) (default: true)
+ .removeTrailingHeaderEqualsSign(true) // == Title == -> == Title (default: true)
+ .titleCase(false) // Title Case headings (default: false)
+ .removeTrailingWhitespace(true) // trim end of line (default: true)
+ .normalizeListBullets(false) // - -> * (default: false)
+ .normalizeOrderedListMarkers(false) // 1. -> . (default: false)
+ .ensureHeadingBlankLines(true) // blank line before/after headings (default: true)
+ .ensureSourceDelimiters(false) // wrap [source] in ---- (default: false)
+```
+
+Options default to `true` or `false` as shown above. This formatter is designed to help you maintain a clean, consistent AsciiDoc structure. Surface-altering options like `oneSentencePerLine` and `normalizeSetextHeadings` default to `false` so that zero-config formatting is conservative; opt in explicitly if you want those transforms.
+
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AsciidocExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AsciidocExtension.java
new file mode 100644
index 0000000000..cf8f3c883f
--- /dev/null
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AsciidocExtension.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2026 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.gradle.spotless;
+
+import javax.inject.Inject;
+
+import com.diffplug.spotless.asciidoc.AdocfmtConfig;
+import com.diffplug.spotless.asciidoc.AdocfmtStep;
+
+public class AsciidocExtension extends FormatExtension {
+ static final String NAME = "asciidoc";
+
+ @Inject
+ public AsciidocExtension(SpotlessExtension spotless) {
+ super(spotless);
+ }
+
+ public AdocfmtFormatterConfig adocfmt() {
+ return adocfmt(AdocfmtStep.defaultVersion());
+ }
+
+ public AdocfmtFormatterConfig adocfmt(String version) {
+ return new AdocfmtFormatterConfig(version);
+ }
+
+ public class AdocfmtFormatterConfig {
+ private final String version;
+ private final AdocfmtConfig config = new AdocfmtConfig();
+
+ AdocfmtFormatterConfig(String version) {
+ this.version = version;
+ addStep(AdocfmtStep.create(version, provisioner(), config));
+ }
+
+ public AdocfmtFormatterConfig normalizeSetextHeadings(boolean normalizeSetextHeadings) {
+ config.normalizeSetextHeadings = normalizeSetextHeadings;
+ replaceStep(AdocfmtStep.create(version, provisioner(), config));
+ return this;
+ }
+
+ public AdocfmtFormatterConfig collapseConsecutiveBlankLines(boolean collapseConsecutiveBlankLines) {
+ config.collapseConsecutiveBlankLines = collapseConsecutiveBlankLines;
+ replaceStep(AdocfmtStep.create(version, provisioner(), config));
+ return this;
+ }
+
+ public AdocfmtFormatterConfig oneSentencePerLine(boolean oneSentencePerLine) {
+ config.oneSentencePerLine = oneSentencePerLine;
+ replaceStep(AdocfmtStep.create(version, provisioner(), config));
+ return this;
+ }
+
+ public AdocfmtFormatterConfig normalizeBlockDelimiters(boolean normalizeBlockDelimiters) {
+ config.normalizeBlockDelimiters = normalizeBlockDelimiters;
+ replaceStep(AdocfmtStep.create(version, provisioner(), config));
+ return this;
+ }
+
+ public AdocfmtFormatterConfig removeTrailingHeaderEqualsSign(boolean removeTrailingHeaderEqualsSign) {
+ config.removeTrailingHeaderEqualsSign = removeTrailingHeaderEqualsSign;
+ replaceStep(AdocfmtStep.create(version, provisioner(), config));
+ return this;
+ }
+
+ public AdocfmtFormatterConfig titleCase(boolean titleCase) {
+ config.titleCase = titleCase;
+ replaceStep(AdocfmtStep.create(version, provisioner(), config));
+ return this;
+ }
+
+ public AdocfmtFormatterConfig removeTrailingWhitespace(boolean removeTrailingWhitespace) {
+ config.removeTrailingWhitespace = removeTrailingWhitespace;
+ replaceStep(AdocfmtStep.create(version, provisioner(), config));
+ return this;
+ }
+
+ public AdocfmtFormatterConfig normalizeListBullets(boolean normalizeListBullets) {
+ config.normalizeListBullets = normalizeListBullets;
+ replaceStep(AdocfmtStep.create(version, provisioner(), config));
+ return this;
+ }
+
+ public AdocfmtFormatterConfig normalizeOrderedListMarkers(boolean normalizeOrderedListMarkers) {
+ config.normalizeOrderedListMarkers = normalizeOrderedListMarkers;
+ replaceStep(AdocfmtStep.create(version, provisioner(), config));
+ return this;
+ }
+
+ public AdocfmtFormatterConfig ensureHeadingBlankLines(boolean ensureHeadingBlankLines) {
+ config.ensureHeadingBlankLines = ensureHeadingBlankLines;
+ replaceStep(AdocfmtStep.create(version, provisioner(), config));
+ return this;
+ }
+
+ public AdocfmtFormatterConfig ensureSourceDelimiters(boolean ensureSourceDelimiters) {
+ config.ensureSourceDelimiters = ensureSourceDelimiters;
+ replaceStep(AdocfmtStep.create(version, provisioner(), config));
+ return this;
+ }
+ }
+
+ @Override
+ protected void setupTask(SpotlessTask task) {
+ if (target == null) {
+ throw noDefaultTargetException();
+ }
+ super.setupTask(task);
+ }
+}
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java
index 0ab99f11ce..83c317f7e7 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java
@@ -234,6 +234,12 @@ public void gherkin(Action closure) {
format(GherkinExtension.NAME, GherkinExtension.class, closure);
}
+ /** Configures the special asciidoc-specific extension. */
+ public void asciidoc(Action closure) {
+ requireNonNull(closure);
+ format(AsciidocExtension.NAME, AsciidocExtension.class, closure);
+ }
+
public void toml(Action closure) {
requireNonNull(closure);
format(TomlExtension.NAME, TomlExtension.class, closure);
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/AsciidocExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/AsciidocExtensionTest.java
new file mode 100644
index 0000000000..2025de9b78
--- /dev/null
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/AsciidocExtensionTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2026 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.gradle.spotless;
+
+import java.io.IOException;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class AsciidocExtensionTest extends GradleIntegrationHarness {
+ @Test
+ void missingTargetFails() throws IOException {
+ setFile("build.gradle").toContent(
+ """
+ plugins {
+ id 'com.diffplug.spotless'
+ }
+ repositories { mavenCentral() }
+ spotless {
+ asciidoc {
+ adocfmt('0.2.0')
+ }
+ }
+ """);
+ String output = gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput();
+ Assertions.assertThat(output).contains("no target set");
+ Assertions.assertThat(output).contains("asciidoc");
+ }
+
+ @Test
+ void integration() throws IOException {
+ setFile("build.gradle")
+ .toContent(
+ """
+ plugins {
+ id 'com.diffplug.spotless'
+ }
+ repositories { mavenCentral() }
+ spotless {
+ asciidoc {
+ target 'test.adoc'
+ adocfmt('0.2.0')
+ .normalizeSetextHeadings(true)
+ .collapseConsecutiveBlankLines(true)
+ .oneSentencePerLine(true)
+ .normalizeBlockDelimiters(true)
+ .removeTrailingHeaderEqualsSign(true)
+ .titleCase(true)
+ .removeTrailingWhitespace(true)
+ .normalizeListBullets(true)
+ .normalizeOrderedListMarkers(true)
+ .ensureHeadingBlankLines(true)
+ .ensureSourceDelimiters(true)
+ }
+ }
+ """);
+ setFile("test.adoc").toResource("asciidoc/adocfmt/dirty.adoc");
+ gradleRunner().withArguments("spotlessApply").build();
+ assertFile("test.adoc").sameAsResource("asciidoc/adocfmt/clean_all_options.adoc");
+ }
+}
diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md
index e06df4137f..dfdc2eedfd 100644
--- a/plugin-maven/CHANGES.md
+++ b/plugin-maven/CHANGES.md
@@ -4,6 +4,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
+### Added
+- Add support for AsciiDoc formatting via `adocfmt`. ([#2960](https://github.com/diffplug/spotless/pull/2960))
+
## [3.6.0] - 2026-05-27
### Added
- Add `` to ``, ``, and `` for the Equo/Solstice P2 cache. ([#2944](https://github.com/diffplug/spotless/pull/2944))
diff --git a/plugin-maven/README.md b/plugin-maven/README.md
index 741f813441..fa06738721 100644
--- a/plugin-maven/README.md
+++ b/plugin-maven/README.md
@@ -44,6 +44,7 @@ user@machine repo % mvn spotless:check
- [Groovy](#groovy) ([eclipse groovy](#eclipse-groovy))
- [Kotlin](#kotlin) ([ktfmt](#ktfmt), [ktlint](#ktlint), [diktat](#diktat), [tabletest-formatter](#tabletest-formatter-1), [prettier](#prettier))
- [Scala](#scala) ([scalafmt](#scalafmt))
+ - [Asciidoc](#asciidoc) ([adocfmt](#adocfmt))
- [C/C++](#cc) ([eclipse cdt](#eclipse-cdt), [clang-format](#clang-format))
- [Python](#python) ([black](#black))
- [Antlr4](#antlr4) ([antlr4formatter](#antlr4formatter))
@@ -704,6 +705,45 @@ Additionally, `editorConfigOverride` options will override what's supplied in `.
```
+## Asciidoc
+
+[code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/asciidoc/Asciidoc.java). [available steps](https://github.com/diffplug/spotless/tree/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/asciidoc).
+
+```xml
+
+
+
+ src/docs/**/*.adoc
+
+
+
+
+
+```
+
+### adocfmt
+
+[homepage](https://github.com/dheid/adocfmt). [available versions](https://search.maven.org/artifact/org.drjekyll/adocfmt). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/asciidoc/Adocfmt.java).
+
+```xml
+
+ 0.2.0
+ true
+ true
+ true
+ true
+ true
+ false
+ true
+ false
+ false
+ true
+ false
+
+```
+
+Options default to `true` or `false` as shown above. This formatter is opinionated and designed to help you maintain a clean, consistent AsciiDoc structure. For example, `` is a highly recommended practice in the AsciiDoc community for better version control diffs.
+
## SQL
[code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/sql/Sql.java). [available steps](https://github.com/diffplug/spotless/tree/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/sql).
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java
index 437a684061..520211334c 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java
@@ -63,6 +63,7 @@
import com.diffplug.spotless.Provisioner;
import com.diffplug.spotless.extra.P2Provisioner;
import com.diffplug.spotless.maven.antlr4.Antlr4;
+import com.diffplug.spotless.maven.asciidoc.Asciidoc;
import com.diffplug.spotless.maven.cpp.Cpp;
import com.diffplug.spotless.maven.css.Css;
import com.diffplug.spotless.maven.generic.Format;
@@ -182,6 +183,9 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo {
@Parameter
private Antlr4 antlr4;
+ @Parameter
+ private Asciidoc asciidoc;
+
@Parameter
private Pom pom;
@@ -453,7 +457,7 @@ private FileLocator getFileLocator() {
}
private List getFormatterFactories() {
- return Stream.concat(formats.stream(), Stream.of(groovy, java, scala, kotlin, cpp, css, typescript, javascript, antlr4, pom, sql, python, markdown, json, shell, yaml, gherkin, go, rdf, protobuf, tableTest, toml))
+ return Stream.concat(formats.stream(), Stream.of(groovy, java, scala, kotlin, asciidoc, cpp, css, typescript, javascript, antlr4, pom, sql, python, markdown, json, shell, yaml, gherkin, go, rdf, protobuf, tableTest, toml))
.filter(Objects::nonNull)
.map(factory -> factory.init(repositorySystemSession))
.collect(toList());
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/asciidoc/Adocfmt.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/asciidoc/Adocfmt.java
new file mode 100644
index 0000000000..51362d3162
--- /dev/null
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/asciidoc/Adocfmt.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2026 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.maven.asciidoc;
+
+import org.apache.maven.plugins.annotations.Parameter;
+
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.asciidoc.AdocfmtConfig;
+import com.diffplug.spotless.asciidoc.AdocfmtStep;
+import com.diffplug.spotless.maven.FormatterStepConfig;
+import com.diffplug.spotless.maven.FormatterStepFactory;
+
+public class Adocfmt implements FormatterStepFactory {
+ @Parameter
+ private String version;
+
+ @Parameter
+ private Boolean normalizeSetextHeadings;
+
+ @Parameter
+ private Boolean collapseConsecutiveBlankLines;
+
+ @Parameter
+ private Boolean oneSentencePerLine;
+
+ @Parameter
+ private Boolean normalizeBlockDelimiters;
+
+ @Parameter
+ private Boolean removeTrailingHeaderEqualsSign;
+
+ @Parameter
+ private Boolean titleCase;
+
+ @Parameter
+ private Boolean removeTrailingWhitespace;
+
+ @Parameter
+ private Boolean normalizeListBullets;
+
+ @Parameter
+ private Boolean normalizeOrderedListMarkers;
+
+ @Parameter
+ private Boolean ensureHeadingBlankLines;
+
+ @Parameter
+ private Boolean ensureSourceDelimiters;
+
+ @Override
+ public FormatterStep newFormatterStep(FormatterStepConfig config) {
+ String version = this.version != null ? this.version : AdocfmtStep.defaultVersion();
+ AdocfmtConfig adocfmtConfig = new AdocfmtConfig();
+ if (normalizeSetextHeadings != null)
+ adocfmtConfig.normalizeSetextHeadings = normalizeSetextHeadings;
+ if (collapseConsecutiveBlankLines != null)
+ adocfmtConfig.collapseConsecutiveBlankLines = collapseConsecutiveBlankLines;
+ if (oneSentencePerLine != null)
+ adocfmtConfig.oneSentencePerLine = oneSentencePerLine;
+ if (normalizeBlockDelimiters != null)
+ adocfmtConfig.normalizeBlockDelimiters = normalizeBlockDelimiters;
+ if (removeTrailingHeaderEqualsSign != null)
+ adocfmtConfig.removeTrailingHeaderEqualsSign = removeTrailingHeaderEqualsSign;
+ if (titleCase != null)
+ adocfmtConfig.titleCase = titleCase;
+ if (removeTrailingWhitespace != null)
+ adocfmtConfig.removeTrailingWhitespace = removeTrailingWhitespace;
+ if (normalizeListBullets != null)
+ adocfmtConfig.normalizeListBullets = normalizeListBullets;
+ if (normalizeOrderedListMarkers != null)
+ adocfmtConfig.normalizeOrderedListMarkers = normalizeOrderedListMarkers;
+ if (ensureHeadingBlankLines != null)
+ adocfmtConfig.ensureHeadingBlankLines = ensureHeadingBlankLines;
+ if (ensureSourceDelimiters != null)
+ adocfmtConfig.ensureSourceDelimiters = ensureSourceDelimiters;
+ return AdocfmtStep.create(version, config.getProvisioner(), adocfmtConfig);
+ }
+}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/asciidoc/Asciidoc.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/asciidoc/Asciidoc.java
new file mode 100644
index 0000000000..67d38a7852
--- /dev/null
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/asciidoc/Asciidoc.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2026 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.maven.asciidoc;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.apache.maven.project.MavenProject;
+
+import com.diffplug.spotless.maven.FormatterFactory;
+
+public class Asciidoc extends FormatterFactory {
+ @Override
+ public Set defaultIncludes(MavenProject project) {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public String licenseHeaderDelimiter() {
+ return null;
+ }
+
+ public void addAdocfmt(Adocfmt adocfmt) {
+ addStepFactory(adocfmt);
+ }
+}
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java
index fd6c4b2047..bcc5791a5b 100644
--- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java
@@ -125,6 +125,10 @@ protected void writePomWithAntlr4Steps(String... steps) throws IOException {
writePom(groupWithSteps("antlr4", steps));
}
+ protected void writePomWithAsciidocSteps(String... steps) throws IOException {
+ writePom(groupWithSteps("asciidoc", including("**/*.adoc"), steps));
+ }
+
protected void writePomWithGroovySteps(String... steps) throws IOException {
writePom(groupWithSteps("groovy", steps));
}
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/asciidoc/AdocfmtMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/asciidoc/AdocfmtMavenTest.java
new file mode 100644
index 0000000000..87926e0be3
--- /dev/null
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/asciidoc/AdocfmtMavenTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2026 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.maven.asciidoc;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+import com.diffplug.spotless.ProcessRunner;
+import com.diffplug.spotless.maven.MavenIntegrationHarness;
+
+public class AdocfmtMavenTest extends MavenIntegrationHarness {
+ @Test
+ public void missingIncludesFails() throws Exception {
+ writePom(groupWithSteps("asciidoc",
+ "",
+ " 0.2.0",
+ ""));
+ ProcessRunner.Result result = mavenRunner().withArguments("spotless:apply").runHasError();
+ assertThat(result.stdOutUtf8()).contains("You must specify some files to include");
+ }
+
+ @Test
+ public void testAdocfmt() throws Exception {
+ writePomWithAsciidocSteps(
+ """
+
+ 0.2.0
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ """);
+
+ setFile("test.adoc").toResource("asciidoc/adocfmt/dirty.adoc");
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile("test.adoc").sameAsResource("asciidoc/adocfmt/clean_all_options.adoc");
+ }
+}
diff --git a/testlib/src/main/resources/asciidoc/adocfmt/clean_all_options.adoc b/testlib/src/main/resources/asciidoc/adocfmt/clean_all_options.adoc
new file mode 100644
index 0000000000..8fab119bf6
--- /dev/null
+++ b/testlib/src/main/resources/asciidoc/adocfmt/clean_all_options.adoc
@@ -0,0 +1,255 @@
+= Asciidoctor Demo
+////
+Big ol' comment
+
+sittin' right 'tween this here title 'n header metadata
+////
+Dan Allen
+:description: A demo of Asciidoctor. This document \
+ exercises numerous features of AsciiDoc \
+ to test Asciidoctor compliance.
+:library: Asciidoctor
+:idprefix:
+:numbered:
+:imagesdir: images
+:experimental:
+//:toc: macro
+:toc: preamble
+:toc-title: pass:[
Contents
]
+:css-signature: demo
+//:max-width: 800px
+//:doctype: book
+//:sectids!:
+ifdef::env-github[]
+:note-caption: :information_source:
+:tip-caption: :bulb:
+endif::[]
+
+This is a demonstration of {library} {asciidoctor-version}.
+And this is the preamble of this document.
+
+[[purpose]]
+.Purpose
+****
+This document exercises many of the features of AsciiDoc to test the {library} implementation.
+****
+
+TIP: If you want the HTML to have the familiar AsciiDoc Python style, load the asciidoc.css stylesheet using the CLI option `-a stylesheet=asciidoc.css`.
+
+== First Steps with AsciiDoc
+
+.Inline Markup
+* underlines around a phrase place _emphasis_
+* astericks around a phrase make the text *bold*
+* double astericks around one or more **l**etters in a word make those letters bold
+* double underscore around a __sub__string in a word emphasizes that substring
+* use carrots around characters to make them ^super^script
+* use tildes around characters to make them ~sub~script
+ifdef::basebackend-html[* to pass through +++HTML+++ directly, surround the text with triple plus]
+ifdef::basebackend-docbook[* to pass through +++XML+++ directly, surround the text with triple plus]
+
+// separate two adjacent lists using a line comment (only the leading // is required)
+
+- characters can be escaped using a {backslash}
+* for instance, you can escape a quote inside emphasized text like _Here\'s Johnny!_
+- you can safely use reserved XML characters like <, > and &, which are escaped when converting
+- force a space{sp}between inline elements using the \{sp} attribute
+- hold text together with an intrinsic non-breaking{nbsp}space attribute, \{nbsp}
+- handle words with unicode characters like in the name Gregory Romé
+- claim your copyright (C), registered trademark (R) or trademark (TM)
+- select menu:View[Zoom > Reset] to reset the zoom
+
+You can write text http://example.com[with inline links], optionally{sp}using an explicit link:http://example.com[link prefix].
+In either case, the link can have a http://example.com?foo=bar&lang=en[query string].
+
+If you want to break a line +
+just end it in a {plus} sign +
+and continue typing on the next line.
+
+=== Lists Upon Lists
+
+.Adjacent Lists
+* this list
+* should join
+
+* to have
+* four items
+
+ifdef::env-github[]
+++++
+
+++++
+endif::env-github[]
+
+[[ordered]]
+.Ordered Lists
+. These items
+. will be auto-numbered
+.. and can be nested
+. A numbered list can nest
+* unordered
+* list
+* items
+
+.Statement
+I swear I left it in _Guy\'s_ car.
+Let\'s go look for it.
+
+[[defs]]
+term::
+definition line two
+[[another_term]]another term::
+
+ another definition, which can be literal (indented) or regular paragraph
+
+This should be a standalone paragraph, not grabbed by the definition list.
+
+[[nested]]
+* first level
+written on two lines
+* first level
++
+....
+with this literal text
+....
+
+* first level
+
+ with more literal text
+
+** second level
+*** third level
+- fourth level
+* back to +
+first level
+
+// this is just a comment
+
+Let's make a horizontal rule...
+
+'''
+
+then take a break.
+
+////
+We'll be right with you...
+
+after this brief interruption.
+////
+
+== ...And We're Back!
+
+Do you want to see a image:tiger.png[Tiger,50]?
+
+Do you feel safer with the tiger in a box?
+
+.Tiger in a Box
+image::tiger.png[]
+
+Fancy some math?
+
+pass:[$$\sum_{i=1}^N (f_i)^2$$]
+
+We'd like to include content, but for now the best we can do is link to it.
+
+include::include.adoc[]
+
+.Asciidoctor Usage Example. The Listing Should Contain 5 Lines.
+[,ruby]
+----
+require 'asciidoctor'
+
+doc = Asciidoctor.load '*This* is http://asciidoc.org[AsciiDoc]!', header_footer: true
+
+puts doc.convert
+----
+
+// TODO: Use ifdef to show output according to current backend
+.Output of Asciidoctor Usage Example
+```html
+
+
+
+
+
+
+Untitled
+
+
+
+
+
+
+
+```
+
+=== Block Quotes and "`Smart`" Ones
+
+____
+AsciiDoc is _so_ *powerful*!
+____
+
+This verse comes to mind.
+
+[verse]
+La la la
+
+Here's another quote:
+
+[quote, Sir Arthur Conan Doyle, The Adventures of Sherlock Holmes]
+____
+When you have eliminated all which is impossible, then whatever remains,
+however improbable, must be the truth.
+____
+
+"`Get moving!`" he shouted.
+
+== Getting Literal [[literally]]
+
+ Want to get literal? Just prefix a line with a space (just 1 space will do).
+
+....
+I'll join that party, too.
+....
+
+We forgot to mention in <> that you can change the numbering style.
+
+.. first item (yeah!)
+.. second item, looking `so mono`
+.. third item, `mono` it is!
+
+// This attribute line will get reattached to the next block
+// despite being followed by a trailing blank line
+[id='wrapup']
+
+== Wrap-up
+
+NOTE: AsciiDoc is quite cool, you should try it!
+
+[TIP]
+.Info
+====
+Go to this URL to learn more about it:
+
+* http://asciidoc.org
+
+Or you could return to the xref:first-steps-with-asciidoc[] or <>.
+====
+
+Here's a reference to the definition of <>, in case you forgot it.
+
+[NOTE]
+One more thing.
+Happy documenting!
+
+[[google]]When all else fails, head over to .
diff --git a/testlib/src/main/resources/asciidoc/adocfmt/clean_complex.adoc b/testlib/src/main/resources/asciidoc/adocfmt/clean_complex.adoc
new file mode 100644
index 0000000000..96e07ae9fc
--- /dev/null
+++ b/testlib/src/main/resources/asciidoc/adocfmt/clean_complex.adoc
@@ -0,0 +1,258 @@
+Asciidoctor Demo
+================
+////
+Big ol' comment
+
+sittin' right 'tween this here title 'n header metadata
+////
+Dan Allen
+:description: A demo of Asciidoctor. This document \
+ exercises numerous features of AsciiDoc \
+ to test Asciidoctor compliance.
+:library: Asciidoctor
+:idprefix:
+:numbered:
+:imagesdir: images
+:experimental:
+//:toc: macro
+:toc: preamble
+:toc-title: pass:[
Contents
]
+:css-signature: demo
+//:max-width: 800px
+//:doctype: book
+//:sectids!:
+ifdef::env-github[]
+:note-caption: :information_source:
+:tip-caption: :bulb:
+endif::[]
+
+This is a demonstration of {library} {asciidoctor-version}.
+And this is the preamble of this document.
+
+[[purpose]]
+.Purpose
+****
+This document exercises many of the features of AsciiDoc to test the {library} implementation.
+****
+
+TIP: If you want the HTML to have the familiar AsciiDoc Python style, load the asciidoc.css stylesheet using the CLI option `-a stylesheet=asciidoc.css`.
+
+== First Steps with AsciiDoc
+
+.Inline markup
+* underlines around a phrase place _emphasis_
+* astericks around a phrase make the text *bold*
+* double astericks around one or more **l**etters in a word make those letters bold
+* double underscore around a __sub__string in a word emphasizes that substring
+* use carrots around characters to make them ^super^script
+* use tildes around characters to make them ~sub~script
+ifdef::basebackend-html[* to pass through +++HTML+++ directly, surround the text with triple plus]
+ifdef::basebackend-docbook[* to pass through +++XML+++ directly, surround the text with triple plus]
+
+// separate two adjacent lists using a line comment (only the leading // is required)
+
+- characters can be escaped using a {backslash}
+* for instance, you can escape a quote inside emphasized text like _Here\'s Johnny!_
+- you can safely use reserved XML characters like <, > and &, which are escaped when converting
+- force a space{sp}between inline elements using the \{sp} attribute
+- hold text together with an intrinsic non-breaking{nbsp}space attribute, \{nbsp}
+- handle words with unicode characters like in the name Gregory Romé
+- claim your copyright (C), registered trademark (R) or trademark (TM)
+- select menu:View[Zoom > Reset] to reset the zoom
+
+You can write text http://example.com[with inline links], optionally{sp}using
+an explicit link:http://example.com[link prefix]. In either case, the link can
+have a http://example.com?foo=bar&lang=en[query string].
+
+If you want to break a line +
+just end it in a {plus} sign +
+and continue typing on the next line.
+
+=== Lists Upon Lists
+
+.Adjacent lists
+* this list
+* should join
+
+* to have
+* four items
+
+ifdef::env-github[]
+++++
+
+++++
+endif::env-github[]
+
+[[ordered]]
+.Ordered lists
+. These items
+. will be auto-numbered
+.. and can be nested
+. A numbered list can nest
+* unordered
+* list
+* items
+
+.Statement
+I swear I left it in _Guy\'s_ car. Let\'s go look for it.
+
+[[defs]]
+term::
+definition
+line two
+[[another_term]]another term::
+
+ another definition, which can be literal (indented) or regular paragraph
+
+This should be a standalone paragraph, not grabbed by the definition list.
+
+[[nested]]
+* first level
+written on two lines
+* first level
++
+....
+with this literal text
+....
+
+* first level
+
+ with more literal text
+
+** second level
+*** third level
+- fourth level
+* back to +
+first level
+
+// this is just a comment
+
+Let's make a horizontal rule...
+
+'''
+
+then take a break.
+
+////
+We'll be right with you...
+
+after this brief interruption.
+////
+
+== ...and we're back!
+
+Do you want to see a image:tiger.png[Tiger,50]?
+
+Do you feel safer with the tiger in a box?
+
+.Tiger in a box
+image::tiger.png[]
+
+Fancy some math?
+
+pass:[$$\sum_{i=1}^N (f_i)^2$$]
+
+We'd like to include content, but for now the best we can do is link to it.
+
+include::include.adoc[]
+
+.Asciidoctor usage example. The listing should contain 5 lines.
+[,ruby]
+----
+require 'asciidoctor'
+
+doc = Asciidoctor.load '*This* is http://asciidoc.org[AsciiDoc]!', header_footer: true
+
+puts doc.convert
+----
+
+// TODO: Use ifdef to show output according to current backend
+.Output of Asciidoctor usage example
+```html
+
+
+
+
+
+
+Untitled
+
+
+
+
+
+
+
+```
+
+=== Block Quotes and "`Smart`" Ones
+
+____
+AsciiDoc is _so_ *powerful*!
+____
+
+This verse comes to mind.
+
+[verse]
+La la la
+
+Here's another quote:
+
+[quote, Sir Arthur Conan Doyle, The Adventures of Sherlock Holmes]
+____
+When you have eliminated all which is impossible, then whatever remains,
+however improbable, must be the truth.
+____
+
+"`Get moving!`" he shouted.
+
+Getting Literal [[literally]]
+-----------------------------
+
+ Want to get literal? Just prefix a line with a space (just 1 space will do).
+
+....
+I'll join that party, too.
+....
+
+We forgot to mention in <> that you can change the numbering style.
+
+.. first item (yeah!)
+.. second item, looking `so mono`
+.. third item, `mono` it is!
+
+// This attribute line will get reattached to the next block
+// despite being followed by a trailing blank line
+[id='wrapup']
+
+Wrap-up
+-------
+
+NOTE: AsciiDoc is quite cool, you should try it!
+
+[TIP]
+.Info
+====
+Go to this URL to learn more about it:
+
+* http://asciidoc.org
+
+Or you could return to the xref:first-steps-with-asciidoc[] or <>.
+====
+
+Here's a reference to the definition of <>, in case you forgot it.
+
+[NOTE]
+One more thing. Happy documenting!
+
+[[google]]When all else fails, head over to .
diff --git a/testlib/src/main/resources/asciidoc/adocfmt/clean_custom.adoc b/testlib/src/main/resources/asciidoc/adocfmt/clean_custom.adoc
new file mode 100644
index 0000000000..1751728b10
--- /dev/null
+++ b/testlib/src/main/resources/asciidoc/adocfmt/clean_custom.adoc
@@ -0,0 +1,255 @@
+Asciidoctor Demo
+================
+////
+Big ol' comment
+
+sittin' right 'tween this here title 'n header metadata
+////
+Dan Allen
+:description: A demo of Asciidoctor. This document \
+ exercises numerous features of AsciiDoc \
+ to test Asciidoctor compliance.
+:library: Asciidoctor
+:idprefix:
+:numbered:
+:imagesdir: images
+:experimental:
+//:toc: macro
+:toc: preamble
+:toc-title: pass:[
Contents
]
+:css-signature: demo
+//:max-width: 800px
+//:doctype: book
+//:sectids!:
+ifdef::env-github[]
+:note-caption: :information_source:
+:tip-caption: :bulb:
+endif::[]
+
+This is a demonstration of {library} {asciidoctor-version}.
+And this is the preamble of this document.
+
+[[purpose]]
+.Purpose
+****
+This document exercises many of the features of AsciiDoc to test the {library} implementation.
+****
+
+TIP: If you want the HTML to have the familiar AsciiDoc Python style, load the asciidoc.css stylesheet using the CLI option `-a stylesheet=asciidoc.css`.
+
+== First Steps with AsciiDoc
+.Inline markup
+* underlines around a phrase place _emphasis_
+* astericks around a phrase make the text *bold*
+* double astericks around one or more **l**etters in a word make those letters bold
+* double underscore around a __sub__string in a word emphasizes that substring
+* use carrots around characters to make them ^super^script
+* use tildes around characters to make them ~sub~script
+ifdef::basebackend-html[* to pass through +++HTML+++ directly, surround the text with triple plus]
+ifdef::basebackend-docbook[* to pass through +++XML+++ directly, surround the text with triple plus]
+
+// separate two adjacent lists using a line comment (only the leading // is required)
+
+- characters can be escaped using a {backslash}
+* for instance, you can escape a quote inside emphasized text like _Here\'s Johnny!_
+- you can safely use reserved XML characters like <, > and &, which are escaped when converting
+- force a space{sp}between inline elements using the \{sp} attribute
+- hold text together with an intrinsic non-breaking{nbsp}space attribute, \{nbsp}
+- handle words with unicode characters like in the name Gregory Romé
+- claim your copyright (C), registered trademark (R) or trademark (TM)
+- select menu:View[Zoom > Reset] to reset the zoom
+
+You can write text http://example.com[with inline links], optionally{sp}using
+an explicit link:http://example.com[link prefix]. In either case, the link can
+have a http://example.com?foo=bar&lang=en[query string].
+
+If you want to break a line +
+just end it in a {plus} sign +
+and continue typing on the next line.
+
+=== Lists Upon Lists
+.Adjacent lists
+* this list
+* should join
+
+* to have
+* four items
+
+ifdef::env-github[]
+++++
+
+++++
+endif::env-github[]
+
+[[ordered]]
+.Ordered lists
+. These items
+. will be auto-numbered
+.. and can be nested
+. A numbered list can nest
+* unordered
+* list
+* items
+
+.Statement
+I swear I left it in _Guy\'s_ car. Let\'s go look for it.
+
+[[defs]]
+term::
+definition
+line two
+[[another_term]]another term::
+
+ another definition, which can be literal (indented) or regular paragraph
+
+This should be a standalone paragraph, not grabbed by the definition list.
+
+[[nested]]
+* first level
+written on two lines
+* first level
++
+....
+with this literal text
+....
+
+* first level
+
+ with more literal text
+
+** second level
+*** third level
+- fourth level
+* back to +
+first level
+
+// this is just a comment
+
+Let's make a horizontal rule...
+
+'''
+
+then take a break.
+
+////
+We'll be right with you...
+
+after this brief interruption.
+////
+
+== ...and we're back!
+
+Do you want to see a image:tiger.png[Tiger,50]?
+
+Do you feel safer with the tiger in a box?
+
+.Tiger in a box
+image::tiger.png[]
+
+Fancy some math?
+
+pass:[$$\sum_{i=1}^N (f_i)^2$$]
+
+We'd like to include content, but for now the best we can do is link to it.
+
+include::include.adoc[]
+
+.Asciidoctor usage example. The listing should contain 5 lines.
+[,ruby]
+----
+require 'asciidoctor'
+
+doc = Asciidoctor.load '*This* is http://asciidoc.org[AsciiDoc]!', header_footer: true
+
+puts doc.convert
+----
+
+// TODO: Use ifdef to show output according to current backend
+.Output of Asciidoctor usage example
+```html
+
+
+
+
+
+
+Untitled
+
+
+
+
+
+
+
+```
+
+=== Block Quotes and "`Smart`" Ones
+
+____
+AsciiDoc is _so_ *powerful*!
+____
+
+This verse comes to mind.
+
+[verse]
+La la la
+
+Here's another quote:
+
+[quote, Sir Arthur Conan Doyle, The Adventures of Sherlock Holmes]
+____
+When you have eliminated all which is impossible, then whatever remains,
+however improbable, must be the truth.
+____
+
+"`Get moving!`" he shouted.
+
+Getting Literal [[literally]]
+-----------------------------
+
+ Want to get literal? Just prefix a line with a space (just 1 space will do).
+
+....
+I'll join that party, too.
+....
+
+We forgot to mention in <> that you can change the numbering style.
+
+.. first item (yeah!)
+.. second item, looking `so mono`
+.. third item, `mono` it is!
+
+// This attribute line will get reattached to the next block
+// despite being followed by a trailing blank line
+[id='wrapup']
+
+Wrap-up
+-------
+NOTE: AsciiDoc is quite cool, you should try it!
+
+[TIP]
+.Info
+====
+Go to this URL to learn more about it:
+
+* http://asciidoc.org
+
+Or you could return to the xref:first-steps-with-asciidoc[] or <>.
+====
+
+Here's a reference to the definition of <>, in case you forgot it.
+
+[NOTE]
+One more thing. Happy documenting!
+
+[[google]]When all else fails, head over to .
diff --git a/testlib/src/main/resources/asciidoc/adocfmt/clean_minimal.adoc b/testlib/src/main/resources/asciidoc/adocfmt/clean_minimal.adoc
new file mode 100644
index 0000000000..96e07ae9fc
--- /dev/null
+++ b/testlib/src/main/resources/asciidoc/adocfmt/clean_minimal.adoc
@@ -0,0 +1,258 @@
+Asciidoctor Demo
+================
+////
+Big ol' comment
+
+sittin' right 'tween this here title 'n header metadata
+////
+Dan Allen
+:description: A demo of Asciidoctor. This document \
+ exercises numerous features of AsciiDoc \
+ to test Asciidoctor compliance.
+:library: Asciidoctor
+:idprefix:
+:numbered:
+:imagesdir: images
+:experimental:
+//:toc: macro
+:toc: preamble
+:toc-title: pass:[
Contents
]
+:css-signature: demo
+//:max-width: 800px
+//:doctype: book
+//:sectids!:
+ifdef::env-github[]
+:note-caption: :information_source:
+:tip-caption: :bulb:
+endif::[]
+
+This is a demonstration of {library} {asciidoctor-version}.
+And this is the preamble of this document.
+
+[[purpose]]
+.Purpose
+****
+This document exercises many of the features of AsciiDoc to test the {library} implementation.
+****
+
+TIP: If you want the HTML to have the familiar AsciiDoc Python style, load the asciidoc.css stylesheet using the CLI option `-a stylesheet=asciidoc.css`.
+
+== First Steps with AsciiDoc
+
+.Inline markup
+* underlines around a phrase place _emphasis_
+* astericks around a phrase make the text *bold*
+* double astericks around one or more **l**etters in a word make those letters bold
+* double underscore around a __sub__string in a word emphasizes that substring
+* use carrots around characters to make them ^super^script
+* use tildes around characters to make them ~sub~script
+ifdef::basebackend-html[* to pass through +++HTML+++ directly, surround the text with triple plus]
+ifdef::basebackend-docbook[* to pass through +++XML+++ directly, surround the text with triple plus]
+
+// separate two adjacent lists using a line comment (only the leading // is required)
+
+- characters can be escaped using a {backslash}
+* for instance, you can escape a quote inside emphasized text like _Here\'s Johnny!_
+- you can safely use reserved XML characters like <, > and &, which are escaped when converting
+- force a space{sp}between inline elements using the \{sp} attribute
+- hold text together with an intrinsic non-breaking{nbsp}space attribute, \{nbsp}
+- handle words with unicode characters like in the name Gregory Romé
+- claim your copyright (C), registered trademark (R) or trademark (TM)
+- select menu:View[Zoom > Reset] to reset the zoom
+
+You can write text http://example.com[with inline links], optionally{sp}using
+an explicit link:http://example.com[link prefix]. In either case, the link can
+have a http://example.com?foo=bar&lang=en[query string].
+
+If you want to break a line +
+just end it in a {plus} sign +
+and continue typing on the next line.
+
+=== Lists Upon Lists
+
+.Adjacent lists
+* this list
+* should join
+
+* to have
+* four items
+
+ifdef::env-github[]
+++++
+
+++++
+endif::env-github[]
+
+[[ordered]]
+.Ordered lists
+. These items
+. will be auto-numbered
+.. and can be nested
+. A numbered list can nest
+* unordered
+* list
+* items
+
+.Statement
+I swear I left it in _Guy\'s_ car. Let\'s go look for it.
+
+[[defs]]
+term::
+definition
+line two
+[[another_term]]another term::
+
+ another definition, which can be literal (indented) or regular paragraph
+
+This should be a standalone paragraph, not grabbed by the definition list.
+
+[[nested]]
+* first level
+written on two lines
+* first level
++
+....
+with this literal text
+....
+
+* first level
+
+ with more literal text
+
+** second level
+*** third level
+- fourth level
+* back to +
+first level
+
+// this is just a comment
+
+Let's make a horizontal rule...
+
+'''
+
+then take a break.
+
+////
+We'll be right with you...
+
+after this brief interruption.
+////
+
+== ...and we're back!
+
+Do you want to see a image:tiger.png[Tiger,50]?
+
+Do you feel safer with the tiger in a box?
+
+.Tiger in a box
+image::tiger.png[]
+
+Fancy some math?
+
+pass:[$$\sum_{i=1}^N (f_i)^2$$]
+
+We'd like to include content, but for now the best we can do is link to it.
+
+include::include.adoc[]
+
+.Asciidoctor usage example. The listing should contain 5 lines.
+[,ruby]
+----
+require 'asciidoctor'
+
+doc = Asciidoctor.load '*This* is http://asciidoc.org[AsciiDoc]!', header_footer: true
+
+puts doc.convert
+----
+
+// TODO: Use ifdef to show output according to current backend
+.Output of Asciidoctor usage example
+```html
+
+
+
+
+
+
+Untitled
+
+
+
+
+
+
+
+```
+
+=== Block Quotes and "`Smart`" Ones
+
+____
+AsciiDoc is _so_ *powerful*!
+____
+
+This verse comes to mind.
+
+[verse]
+La la la
+
+Here's another quote:
+
+[quote, Sir Arthur Conan Doyle, The Adventures of Sherlock Holmes]
+____
+When you have eliminated all which is impossible, then whatever remains,
+however improbable, must be the truth.
+____
+
+"`Get moving!`" he shouted.
+
+Getting Literal [[literally]]
+-----------------------------
+
+ Want to get literal? Just prefix a line with a space (just 1 space will do).
+
+....
+I'll join that party, too.
+....
+
+We forgot to mention in <> that you can change the numbering style.
+
+.. first item (yeah!)
+.. second item, looking `so mono`
+.. third item, `mono` it is!
+
+// This attribute line will get reattached to the next block
+// despite being followed by a trailing blank line
+[id='wrapup']
+
+Wrap-up
+-------
+
+NOTE: AsciiDoc is quite cool, you should try it!
+
+[TIP]
+.Info
+====
+Go to this URL to learn more about it:
+
+* http://asciidoc.org
+
+Or you could return to the xref:first-steps-with-asciidoc[] or <>.
+====
+
+Here's a reference to the definition of <>, in case you forgot it.
+
+[NOTE]
+One more thing. Happy documenting!
+
+[[google]]When all else fails, head over to .
diff --git a/testlib/src/main/resources/asciidoc/adocfmt/dirty.adoc b/testlib/src/main/resources/asciidoc/adocfmt/dirty.adoc
new file mode 100644
index 0000000000..6c39f4f1b9
--- /dev/null
+++ b/testlib/src/main/resources/asciidoc/adocfmt/dirty.adoc
@@ -0,0 +1,256 @@
+Asciidoctor Demo
+================
+////
+Big ol' comment
+
+sittin' right 'tween this here title 'n header metadata
+////
+Dan Allen
+:description: A demo of Asciidoctor. This document \
+ exercises numerous features of AsciiDoc \
+ to test Asciidoctor compliance.
+:library: Asciidoctor
+:idprefix:
+:numbered:
+:imagesdir: images
+:experimental:
+//:toc: macro
+:toc: preamble
+:toc-title: pass:[
Contents
]
+:css-signature: demo
+//:max-width: 800px
+//:doctype: book
+//:sectids!:
+ifdef::env-github[]
+:note-caption: :information_source:
+:tip-caption: :bulb:
+endif::[]
+
+This is a demonstration of {library} {asciidoctor-version}.
+And this is the preamble of this document.
+
+[[purpose]]
+.Purpose
+****
+This document exercises many of the features of AsciiDoc to test the {library} implementation.
+****
+
+TIP: If you want the HTML to have the familiar AsciiDoc Python style, load the asciidoc.css stylesheet using the CLI option `-a stylesheet=asciidoc.css`.
+
+== First Steps with AsciiDoc
+.Inline markup
+* underlines around a phrase place _emphasis_
+* astericks around a phrase make the text *bold*
+* double astericks around one or more **l**etters in a word make those letters bold
+* double underscore around a __sub__string in a word emphasizes that substring
+* use carrots around characters to make them ^super^script
+* use tildes around characters to make them ~sub~script
+ifdef::basebackend-html[* to pass through +++HTML+++ directly, surround the text with triple plus]
+ifdef::basebackend-docbook[* to pass through +++XML+++ directly, surround the text with triple plus]
+
+// separate two adjacent lists using a line comment (only the leading // is required)
+
+- characters can be escaped using a {backslash}
+* for instance, you can escape a quote inside emphasized text like _Here\'s Johnny!_
+- you can safely use reserved XML characters like <, > and &, which are escaped when converting
+- force a space{sp}between inline elements using the \{sp} attribute
+- hold text together with an intrinsic non-breaking{nbsp}space attribute, \{nbsp}
+- handle words with unicode characters like in the name Gregory Romé
+- claim your copyright (C), registered trademark (R) or trademark (TM)
+- select menu:View[Zoom > Reset] to reset the zoom
+
+You can write text http://example.com[with inline links], optionally{sp}using
+an explicit link:http://example.com[link prefix]. In either case, the link can
+have a http://example.com?foo=bar&lang=en[query string].
+
+If you want to break a line +
+just end it in a {plus} sign +
+and continue typing on the next line.
+
+=== Lists Upon Lists
+.Adjacent lists
+* this list
+* should join
+
+* to have
+* four items
+
+ifdef::env-github[]
+++++
+
+++++
+endif::env-github[]
+
+[[ordered]]
+.Ordered lists
+. These items
+. will be auto-numbered
+.. and can be nested
+. A numbered list can nest
+* unordered
+* list
+* items
+
+.Statement
+I swear I left it in _Guy\'s_ car. Let\'s go look for it.
+
+[[defs]]
+term::
+definition
+line two
+[[another_term]]another term::
+
+ another definition, which can be literal (indented) or regular paragraph
+
+This should be a standalone paragraph, not grabbed by the definition list.
+
+[[nested]]
+* first level
+written on two lines
+* first level
++
+....
+with this literal text
+....
+
+* first level
+
+ with more literal text
+
+** second level
+*** third level
+- fourth level
+* back to +
+first level
+
+// this is just a comment
+
+Let's make a horizontal rule...
+
+'''
+
+then take a break.
+
+////
+We'll be right with you...
+
+after this brief interruption.
+////
+
+== ...and we're back!
+
+Do you want to see a image:tiger.png[Tiger,50]?
+
+Do you feel safer with the tiger in a box?
+
+.Tiger in a box
+image::tiger.png[]
+
+Fancy some math?
+
+pass:[$$\sum_{i=1}^N (f_i)^2$$]
+
+We'd like to include content, but for now the best we can do is link to it.
+
+include::include.adoc[]
+
+.Asciidoctor usage example. The listing should contain 5 lines.
+[,ruby]
+----
+require 'asciidoctor'
+
+doc = Asciidoctor.load '*This* is http://asciidoc.org[AsciiDoc]!', header_footer: true
+
+puts doc.convert
+----
+
+// TODO: Use ifdef to show output according to current backend
+.Output of Asciidoctor usage example
+```html
+
+
+
+
+
+
+Untitled
+
+
+
+
+
+
+
+```
+
+=== Block Quotes and "`Smart`" Ones
+
+
+____
+AsciiDoc is _so_ *powerful*!
+____
+
+This verse comes to mind.
+
+[verse]
+La la la
+
+Here's another quote:
+
+[quote, Sir Arthur Conan Doyle, The Adventures of Sherlock Holmes]
+____
+When you have eliminated all which is impossible, then whatever remains,
+however improbable, must be the truth.
+____
+
+"`Get moving!`" he shouted.
+
+Getting Literal [[literally]]
+-----------------------------
+
+ Want to get literal? Just prefix a line with a space (just 1 space will do).
+
+....
+I'll join that party, too.
+....
+
+We forgot to mention in <> that you can change the numbering style.
+
+.. first item (yeah!)
+.. second item, looking `so mono`
+.. third item, `mono` it is!
+
+// This attribute line will get reattached to the next block
+// despite being followed by a trailing blank line
+[id='wrapup']
+
+Wrap-up
+-------
+NOTE: AsciiDoc is quite cool, you should try it!
+
+[TIP]
+.Info
+=====
+Go to this URL to learn more about it:
+
+* http://asciidoc.org
+
+Or you could return to the xref:first-steps-with-asciidoc[] or <>.
+=====
+
+Here's a reference to the definition of <>, in case you forgot it.
+
+[NOTE]
+One more thing. Happy documenting!
+
+[[google]]When all else fails, head over to .
diff --git a/testlib/src/test/java/com/diffplug/spotless/asciidoc/AdocfmtStepTest.java b/testlib/src/test/java/com/diffplug/spotless/asciidoc/AdocfmtStepTest.java
new file mode 100644
index 0000000000..b41a9157f4
--- /dev/null
+++ b/testlib/src/test/java/com/diffplug/spotless/asciidoc/AdocfmtStepTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2026 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.asciidoc;
+
+import org.junit.jupiter.api.Test;
+
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.ResourceHarness;
+import com.diffplug.spotless.SerializableEqualityTester;
+import com.diffplug.spotless.StepHarness;
+import com.diffplug.spotless.TestProvisioner;
+
+class AdocfmtStepTest extends ResourceHarness {
+ @Test
+ void behavior() {
+ FormatterStep step = AdocfmtStep.create(TestProvisioner.mavenCentral());
+ StepHarness.forStep(step).testResource("asciidoc/adocfmt/dirty.adoc", "asciidoc/adocfmt/clean_minimal.adoc");
+ }
+
+ @Test
+ void complexBehavior() {
+ AdocfmtConfig config = new AdocfmtConfig();
+ config.normalizeListBullets = true;
+ config.normalizeOrderedListMarkers = true;
+ config.ensureSourceDelimiters = true;
+ FormatterStep step = AdocfmtStep.create(AdocfmtStep.defaultVersion(), TestProvisioner.mavenCentral(), config);
+ StepHarness.forStep(step).testResource("asciidoc/adocfmt/dirty.adoc", "asciidoc/adocfmt/clean_complex.adoc");
+ }
+
+ @Test
+ void customConfigBehavior() {
+ AdocfmtConfig config = new AdocfmtConfig();
+ config.ensureHeadingBlankLines = false;
+
+ FormatterStep step = AdocfmtStep.create(AdocfmtStep.defaultVersion(), TestProvisioner.mavenCentral(), config);
+
+ StepHarness.forStep(step).testResource("asciidoc/adocfmt/dirty.adoc", "asciidoc/adocfmt/clean_custom.adoc");
+ }
+
+ @Test
+ void equality() {
+ new SerializableEqualityTester() {
+ // fields drive create(); never mutate a config object after passing it to create()
+ boolean titleCase = false;
+ boolean normalizeListBullets = false;
+
+ @Override
+ protected void setupTest(API api) {
+ // default config == same
+ api.areDifferentThan();
+ // change one config option -> different
+ titleCase = true;
+ api.areDifferentThan();
+ // change another config option -> different
+ titleCase = false;
+ normalizeListBullets = true;
+ api.areDifferentThan();
+ }
+
+ @Override
+ protected FormatterStep create() {
+ AdocfmtConfig config = new AdocfmtConfig();
+ config.titleCase = titleCase;
+ config.normalizeListBullets = normalizeListBullets;
+ return AdocfmtStep.create(AdocfmtStep.defaultVersion(), TestProvisioner.mavenCentral(), config);
+ }
+ }.testEquals();
+ }
+}