Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `AsciidocFormatterStep` for formatting AsciiDoc (`.adoc`) files. Supports normalizing setext headings, block delimiters, heading whitespace, list markers, trailing whitespace, blank lines around headings, one-sentence-per-line, title case, and auto-adding `----` delimiters to bare source blocks. ([#2955](https://github.com/diffplug/spotless/pull/2955))

## [4.6.2] - 2026-05-27
### Fixed
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ lib('generic.NativeCmdStep') +'{{yes}} | {{yes}}
lib('generic.ReplaceRegexStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('generic.ReplaceStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('generic.TrimTrailingWhitespaceStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('asciidoc.AsciidocFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('antlr4.Antlr4FormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('biome.BiomeStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('cpp.ClangFormatStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
Expand Down Expand Up @@ -133,6 +134,7 @@ lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}}
| [`generic.ReplaceRegexStep`](lib/src/main/java/com/diffplug/spotless/generic/ReplaceRegexStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`generic.ReplaceStep`](lib/src/main/java/com/diffplug/spotless/generic/ReplaceStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`generic.TrimTrailingWhitespaceStep`](lib/src/main/java/com/diffplug/spotless/generic/TrimTrailingWhitespaceStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`asciidoc.AsciidocFormatterStep`](lib/src/main/java/com/diffplug/spotless/asciidoc/AsciidocFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`antlr4.Antlr4FormatterStep`](lib/src/main/java/com/diffplug/spotless/antlr4/Antlr4FormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`biome.BiomeStep`](lib/src/main/java/com/diffplug/spotless/biome/BiomeStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`cpp.ClangFormatStep`](lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* 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.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;

/** Handles transformations for Asciidoc blocks (delimiters, source blocks). */
final class AsciidocBlockHandler {
private final List<String> lines;

AsciidocBlockHandler(List<String> lines) {
this.lines = lines;
}

private static final String BLOCK_DELIMITER_CHARS = "-=.*_+/";

// Source / listing block attribute lines: [source], [source,java], [listing], [source%linenums,java], [source#id,java], etc.
private static final Pattern SOURCE_BLOCK_ATTR = Pattern.compile("^\\[(source|listing)[,\\]%#].*");

static boolean isBlockDelimiter(CharSequence line) {
int len = line.length();
if (len < 4) {
return false;
}
char c = line.charAt(0);
if (BLOCK_DELIMITER_CHARS.indexOf(c) < 0) {
return false;
}
for (int i = 1; i < len; i++) {
if (line.charAt(i) != c) {
return false;
}
}
return true;
}

void normalizeBlockDelimiters() {
BlockTracker bt = new BlockTracker();

for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
if (bt.isOpen()) {
String closed = bt.tryClose(line);
if (closed != null) {
lines.set(i, closed.repeat(4));
}
} else if (isBlockDelimiter(line)) {
if (line.length() > 4) {
String prev = i == 0 ? null : lines.get(i - 1);
boolean notSetextUnderline = prev == null || prev.isBlank()
|| AsciidocHeadingHandler.detectSetextUnderline(prev, line) == null;
if (notSetextUnderline) {
lines.set(i, String.valueOf(line.charAt(0)).repeat(4));
bt.open(lines.get(i));
}
} else {
bt.open(line);
}
}
}
}

void ensureSourceDelimiters() {
Collection<String> result = new ArrayList<>(lines.size() + 8);
BlockTracker bt = new BlockTracker();
int i = 0;
while (i < lines.size()) {
String line = lines.get(i);

if (bt.isOpen()) {
result.add(line);
bt.tryClose(line);
i++;
continue;
}

if (isBlockDelimiter(line)) {
result.add(line);
bt.open(line);
i++;
continue;
}

if (SOURCE_BLOCK_ATTR.matcher(line).matches()) {
result.add(line);
i++;
if (i < lines.size()) {
String next = lines.get(i);
if (isBlockDelimiter(next)) {
result.add(next);
bt.open(next);
i++;
} else if (!next.isBlank() && !next.startsWith("[")) {
result.add("----");
while (i < lines.size() && !lines.get(i).isBlank()) {
result.add(lines.get(i));
i++;
}
result.add("----");
}
}
continue;
}

result.add(line);
i++;
}
lines.clear();
lines.addAll(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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;

public class AsciidocFormatterConfig implements Serializable {
@Serial
private static final long serialVersionUID = 1L;

private boolean normalizeSetextHeadings = true;
private boolean collapseConsecutiveBlankLines = true;
private boolean oneSentencePerLine = true;
private boolean normalizeBlockDelimiters = true;
private boolean removeTrailingHeaderEqualsSign = true;
private boolean titleCase = false;
private boolean removeTrailingWhitespace = true;
private boolean normalizeListBullets = false;
private boolean normalizeOrderedListMarkers = false;
private boolean ensureHeadingBlankLines = true;
private boolean ensureSourceDelimiters = false;

public boolean isNormalizeSetextHeadings() {
return normalizeSetextHeadings;
}

public void setNormalizeSetextHeadings(boolean normalizeSetextHeadings) {
this.normalizeSetextHeadings = normalizeSetextHeadings;
}

public boolean isCollapseConsecutiveBlankLines() {
return collapseConsecutiveBlankLines;
}

public void setCollapseConsecutiveBlankLines(boolean collapseConsecutiveBlankLines) {
this.collapseConsecutiveBlankLines = collapseConsecutiveBlankLines;
}

public boolean isOneSentencePerLine() {
return oneSentencePerLine;
}

public void setOneSentencePerLine(boolean oneSentencePerLine) {
this.oneSentencePerLine = oneSentencePerLine;
}

public boolean isNormalizeBlockDelimiters() {
return normalizeBlockDelimiters;
}

public void setNormalizeBlockDelimiters(boolean normalizeBlockDelimiters) {
this.normalizeBlockDelimiters = normalizeBlockDelimiters;
}

public boolean isRemoveTrailingHeaderEqualsSign() {
return removeTrailingHeaderEqualsSign;
}

public void setRemoveTrailingHeaderEqualsSign(boolean removeTrailingHeaderEqualsSign) {
this.removeTrailingHeaderEqualsSign = removeTrailingHeaderEqualsSign;
}

public boolean isTitleCase() {
return titleCase;
}

public void setTitleCase(boolean titleCase) {
this.titleCase = titleCase;
}

public boolean isRemoveTrailingWhitespace() {
return removeTrailingWhitespace;
}

public void setRemoveTrailingWhitespace(boolean removeTrailingWhitespace) {
this.removeTrailingWhitespace = removeTrailingWhitespace;
}

public boolean isNormalizeListBullets() {
return normalizeListBullets;
}

public void setNormalizeListBullets(boolean normalizeListBullets) {
this.normalizeListBullets = normalizeListBullets;
}

public boolean isNormalizeOrderedListMarkers() {
return normalizeOrderedListMarkers;
}

public void setNormalizeOrderedListMarkers(boolean normalizeOrderedListMarkers) {
this.normalizeOrderedListMarkers = normalizeOrderedListMarkers;
}

public boolean isEnsureHeadingBlankLines() {
return ensureHeadingBlankLines;
}

public void setEnsureHeadingBlankLines(boolean ensureHeadingBlankLines) {
this.ensureHeadingBlankLines = ensureHeadingBlankLines;
}

public boolean isEnsureSourceDelimiters() {
return ensureSourceDelimiters;
}

public void setEnsureSourceDelimiters(boolean ensureSourceDelimiters) {
this.ensureSourceDelimiters = ensureSourceDelimiters;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

import com.diffplug.spotless.FormatterFunc;

import edu.umd.cs.findbugs.annotations.NonNull;

/**
* A formatter function for Asciidoc that applies various formatting rules
* based on the provided configuration.
*/
public class AsciidocFormatterFunc implements FormatterFunc {

private static final Pattern LINE_SPLITTER = Pattern.compile("\\R");

private final AsciidocFormatterConfig config;

public AsciidocFormatterFunc(AsciidocFormatterConfig config) {
this.config = config;
}

@NonNull @Override
public String apply(@NonNull String input) throws Exception {
List<String> lines = new ArrayList<>(Arrays.asList(LINE_SPLITTER.split(input, -1)));
// Ordering constraints:
// removeTrailingWhitespace before collapseConsecutiveBlankLines
// - whitespace-only lines must be emptied before they can be collapsed.
// removeTrailingHeaderEqualsSign before normalizeSetextHeadings
// - symmetric ATX headings must be cleaned before setext conversion so that
// a setext title ending with '=' is not later mistaken for symmetric decoration.
// normalizeSetextHeadings before ensureHeadingBlankLines
// - setext headings are converted to ATX first so they receive blank-line padding.

AsciidocLineHandler lineHandler = new AsciidocLineHandler(lines);
if (config.isRemoveTrailingWhitespace()) {
lineHandler.removeTrailingWhitespace();
}
AsciidocHeadingHandler headingHandler = new AsciidocHeadingHandler(lines);
if (config.isRemoveTrailingHeaderEqualsSign()) {
headingHandler.removeTrailingHeaderEqualsSign();
}
if (config.isNormalizeSetextHeadings()) {
headingHandler.normalizeSetextHeadings();
}
AsciidocBlockHandler blockHandler = new AsciidocBlockHandler(lines);
if (config.isEnsureSourceDelimiters()) {
blockHandler.ensureSourceDelimiters();
}
if (config.isNormalizeBlockDelimiters()) {
blockHandler.normalizeBlockDelimiters();
}

// Combine simple line-by-line transforms into a single in-place pass
if (config.isTitleCase() || config.isNormalizeListBullets() || config.isNormalizeOrderedListMarkers()) {
lineHandler.applyLineTransformations(config);
}

if (config.isEnsureHeadingBlankLines()) {
headingHandler.ensureHeadingBlankLines();
}
AsciidocSentenceHandler sentenceHandler = new AsciidocSentenceHandler(lines);
if (config.isOneSentencePerLine()) {
sentenceHandler.applySentencePerLine();
}
if (config.isCollapseConsecutiveBlankLines()) {
lineHandler.collapseBlankLines();
}

return String.join("\n", lines);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 com.diffplug.spotless.FormatterStep;

public final class AsciidocFormatterStep {
public static final String NAME = "asciidoc";

private AsciidocFormatterStep() {}

public static FormatterStep create(AsciidocFormatterConfig config) {
return FormatterStep.create(NAME, config, AsciidocFormatterFunc::new);
}
}
Loading