From 62c6058e78bac1086923208d2037b86a5b1db9ee Mon Sep 17 00:00:00 2001 From: Ben Tatham Date: Fri, 5 Jun 2026 21:42:28 +0000 Subject: [PATCH 1/7] feat(flexmark): support arbitrary formatter options via flexmarkOptions map Adds a flexmarkOptions map to FlexmarkConfig and the Maven plugin's Flexmark class. Each camelCase key (e.g. rightMargin) is converted to SCREAMING_SNAKE_CASE and resolved to a static DataKey field on the flexmark Formatter class via reflection. Integer, Boolean, and String option types are supported; an unknown key or unsupported type fails the build with a clear error message. Example Maven configuration: 100 Co-Authored-By: Claude Sonnet 4.6 --- .../glue/markdown/FlexmarkFormatterFunc.java | 66 ++++++++++++++++++- .../spotless/markdown/FlexmarkConfig.java | 11 ++++ .../spotless/maven/markdown/Flexmark.java | 6 ++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/lib/src/flexmark/java/com/diffplug/spotless/glue/markdown/FlexmarkFormatterFunc.java b/lib/src/flexmark/java/com/diffplug/spotless/glue/markdown/FlexmarkFormatterFunc.java index 072b958580..3c70b50593 100644 --- a/lib/src/flexmark/java/com/diffplug/spotless/glue/markdown/FlexmarkFormatterFunc.java +++ b/lib/src/flexmark/java/com/diffplug/spotless/glue/markdown/FlexmarkFormatterFunc.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import com.vladsch.flexmark.formatter.Formatter; @@ -27,6 +28,7 @@ import com.vladsch.flexmark.parser.PegdownExtensions; import com.vladsch.flexmark.profile.pegdown.PegdownOptionsAdapter; import com.vladsch.flexmark.util.ast.Document; +import com.vladsch.flexmark.util.data.DataKey; import com.vladsch.flexmark.util.data.MutableDataHolder; import com.vladsch.flexmark.util.data.MutableDataSet; import com.vladsch.flexmark.util.misc.Extension; @@ -73,7 +75,7 @@ public FlexmarkFormatterFunc(FlexmarkConfig config) { final ParserEmulationProfile emulationProfile = ParserEmulationProfile.valueOf(config.getEmulationProfile()); final MutableDataHolder parserOptions = createParserOptions(emulationProfile, config); - final MutableDataHolder formatterOptions = createFormatterOptions(parserOptions, emulationProfile); + final MutableDataHolder formatterOptions = createFormatterOptions(parserOptions, emulationProfile, config); parser = Parser.builder(parserOptions).build(); formatter = Formatter.builder(formatterOptions).build(); @@ -145,20 +147,80 @@ private static Extension[] buildExtensions(List config) { /** * Creates the formatter options, copies the parser extensions and changes defaults that make sense for a formatter. + * Arbitrary flexmark formatter options can be set via {@link FlexmarkConfig#getFlexmarkOptions()}: each key is a + * camelCase name (e.g. {@code rightMargin}) that is converted to SCREAMING_SNAKE_CASE to look up the corresponding + * static {@link DataKey} field on {@link Formatter} via reflection. An unrecognised key fails the build. * See: https://github.com/vsch/flexmark-java/wiki/Markdown-Formatter#options * * @param parserOptions the options used for the parser * @param emulationProfile the emulation profile (or flavor of markdown) the formatter should use + * @param config the flexmark config, including any additional formatter options * @return the created formatter options */ private static MutableDataHolder createFormatterOptions(MutableDataHolder parserOptions, - ParserEmulationProfile emulationProfile) { + ParserEmulationProfile emulationProfile, FlexmarkConfig config) { final MutableDataHolder formatterOptions = new MutableDataSet(); formatterOptions.set(Parser.EXTENSIONS, Parser.EXTENSIONS.get(parserOptions)); formatterOptions.set(Formatter.FORMATTER_EMULATION_PROFILE, emulationProfile); + applyFlexmarkOptions(formatterOptions, config.getFlexmarkOptions()); return formatterOptions; } + /** + * Applies arbitrary formatter options from the config map to the given data holder. + * Each camelCase key (e.g. {@code rightMargin}) is converted to SCREAMING_SNAKE_CASE and looked up as a static + * {@link DataKey} field on {@link Formatter}. Supported value types are {@link Integer}, {@link Boolean}, and + * {@link String}; the type is inferred from the DataKey's default value. An unknown key or unsupported type + * throws {@link IllegalArgumentException}, which fails the build. + */ + private static void applyFlexmarkOptions(MutableDataHolder options, Map flexmarkOptions) { + if (flexmarkOptions.isEmpty()) { + return; + } + MutableDataSet defaults = new MutableDataSet(); + for (Map.Entry entry : flexmarkOptions.entrySet()) { + String camelKey = entry.getKey(); + String rawValue = entry.getValue(); + String fieldName = camelToScreamingSnake(camelKey); + Field field; + try { + field = Formatter.class.getField(fieldName); + } catch (NoSuchFieldException e) { + throw new IllegalArgumentException( + "Unknown flexmark formatter option '" + camelKey + "': no field Formatter." + fieldName + + ". See https://github.com/vsch/flexmark-java/wiki/Markdown-Formatter#options"); + } + DataKey dataKey; + try { + dataKey = (DataKey) field.get(null); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Cannot access field Formatter." + fieldName, e); + } + setOption(options, dataKey, dataKey.getDefaultValue(defaults), rawValue, camelKey); + } + } + + /** Converts a camelCase name (e.g. {@code rightMargin}) to SCREAMING_SNAKE_CASE (e.g. {@code RIGHT_MARGIN}). */ + private static String camelToScreamingSnake(String camel) { + return camel.replaceAll("([A-Z])", "_$1").toUpperCase(Locale.ROOT); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void setOption(MutableDataHolder options, DataKey dataKey, Object defaultValue, String rawValue, + String camelKey) { + if (defaultValue instanceof Integer) { + options.set(dataKey, Integer.parseInt(rawValue)); + } else if (defaultValue instanceof Boolean) { + options.set(dataKey, Boolean.parseBoolean(rawValue)); + } else if (defaultValue instanceof String) { + options.set(dataKey, rawValue); + } else { + throw new IllegalArgumentException("Unsupported type for flexmark option '" + camelKey + "': " + + (defaultValue == null ? "null" : defaultValue.getClass().getName()) + + ". Only Integer, Boolean, and String options are supported."); + } + } + @Override public String apply(String input) throws Exception { final Document parsedMarkdown = parser.parse(input); diff --git a/lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkConfig.java b/lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkConfig.java index b43d6decd2..2011e5f229 100644 --- a/lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkConfig.java +++ b/lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkConfig.java @@ -18,7 +18,9 @@ import java.io.Serial; import java.io.Serializable; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; public class FlexmarkConfig implements Serializable { @Serial @@ -31,6 +33,7 @@ public class FlexmarkConfig implements Serializable { private String emulationProfile = "COMMONMARK"; private List pegdownExtensions = List.of("ALL"); private List extensions = new ArrayList<>(); + private Map flexmarkOptions = new LinkedHashMap<>(); public String getEmulationProfile() { return emulationProfile; @@ -56,4 +59,12 @@ public void setExtensions(List extensions) { this.extensions = extensions; } + public Map getFlexmarkOptions() { + return flexmarkOptions; + } + + public void setFlexmarkOptions(Map flexmarkOptions) { + this.flexmarkOptions = flexmarkOptions; + } + } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Flexmark.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Flexmark.java index a187019c22..289656ee9e 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Flexmark.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Flexmark.java @@ -16,6 +16,7 @@ package com.diffplug.spotless.maven.markdown; import java.util.List; +import java.util.Map; import org.apache.maven.plugins.annotations.Parameter; @@ -36,6 +37,8 @@ public class Flexmark implements FormatterStepFactory { private String pegdownExtensions; @Parameter private String extensions; + @Parameter + private Map flexmarkOptions; @Override public FormatterStep newFormatterStep(FormatterStepConfig config) { @@ -50,6 +53,9 @@ public FormatterStep newFormatterStep(FormatterStepConfig config) { if (this.extensions != null) { flexmarkConfig.setExtensions(List.of(this.extensions.split(","))); } + if (this.flexmarkOptions != null) { + flexmarkConfig.setFlexmarkOptions(this.flexmarkOptions); + } return FlexmarkStep.create(version, config.getProvisioner(), flexmarkConfig); } } From 9c2cd609b10601c93cafef4dfe13dc2b3348f01e Mon Sep 17 00:00:00 2001 From: Ben Tatham Date: Fri, 5 Jun 2026 23:24:44 +0000 Subject: [PATCH 2/7] test(flexmark): add tests for flexmarkOptions map Adds unit tests in FlexmarkStepTest verifying: - rightMargin option wraps long paragraphs at the configured column - an unknown option key fails with a clear IllegalArgumentException Adds a Maven integration test in FlexmarkMavenTest verifying the flexmarkOptions XML map configuration round-trips correctly through the plugin. Adds the corresponding test resource fixtures FlexmarkOptionsUnformatted.md / FlexmarkOptionsFormatted.md. Co-Authored-By: Claude Sonnet 4.6 --- .../maven/markdown/FlexmarkMavenTest.java | 9 ++++++ .../flexmark/FlexmarkOptionsFormatted.md | 8 ++++++ .../flexmark/FlexmarkOptionsUnformatted.md | 7 +++++ .../spotless/markdown/FlexmarkStepTest.java | 28 +++++++++++++++++++ 4 files changed, 52 insertions(+) create mode 100644 testlib/src/main/resources/markdown/flexmark/FlexmarkOptionsFormatted.md create mode 100644 testlib/src/main/resources/markdown/flexmark/FlexmarkOptionsUnformatted.md diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java index df55059c45..053282bbd2 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java @@ -30,6 +30,15 @@ public void testFlexmarkWithSimpleConfig() throws Exception { assertFile("markdown_test.md").sameAsResource("markdown/flexmark/FlexmarkFormatted.md"); } + @Test + public void testFlexmarkWithOptions() throws Exception { + writePomWithMarkdownSteps("YamlFrontMatter100"); + + setFile("markdown_test.md").toResource("markdown/flexmark/FlexmarkOptionsUnformatted.md"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("markdown_test.md").sameAsResource("markdown/flexmark/FlexmarkOptionsFormatted.md"); + } + @Test public void testFlexmarkWithComplexConfig() throws Exception { writePomWithMarkdownSteps("COMMONMARK0x0000FFFFcom.vladsch.flexmark.ext.yaml.front.matter.YamlFrontMatterExtension"); diff --git a/testlib/src/main/resources/markdown/flexmark/FlexmarkOptionsFormatted.md b/testlib/src/main/resources/markdown/flexmark/FlexmarkOptionsFormatted.md new file mode 100644 index 0000000000..c462c92267 --- /dev/null +++ b/testlib/src/main/resources/markdown/flexmark/FlexmarkOptionsFormatted.md @@ -0,0 +1,8 @@ +--- +key: value +--- + +A short paragraph. + +A long paragraph that needs wrapping because it exceeds the configured right margin of one hundred +characters total. diff --git a/testlib/src/main/resources/markdown/flexmark/FlexmarkOptionsUnformatted.md b/testlib/src/main/resources/markdown/flexmark/FlexmarkOptionsUnformatted.md new file mode 100644 index 0000000000..062616bf93 --- /dev/null +++ b/testlib/src/main/resources/markdown/flexmark/FlexmarkOptionsUnformatted.md @@ -0,0 +1,7 @@ +--- +key: value +--- + +A short paragraph. + +A long paragraph that needs wrapping because it exceeds the configured right margin of one hundred characters total. diff --git a/testlib/src/test/java/com/diffplug/spotless/markdown/FlexmarkStepTest.java b/testlib/src/test/java/com/diffplug/spotless/markdown/FlexmarkStepTest.java index 89e387bb85..45a092d48f 100644 --- a/testlib/src/test/java/com/diffplug/spotless/markdown/FlexmarkStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/markdown/FlexmarkStepTest.java @@ -15,10 +15,14 @@ */ package com.diffplug.spotless.markdown; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; +import com.diffplug.spotless.ResourceHarness; import com.diffplug.spotless.StepHarness; import com.diffplug.spotless.TestProvisioner; @@ -35,6 +39,30 @@ void behaviorOldest() { "markdown/flexmark/FlexmarkFormatted.md"); } + @Test + void flexmarkOptionsRightMargin() { + FlexmarkConfig config = new FlexmarkConfig(); + config.setExtensions(List.of("YamlFrontMatter")); + config.setFlexmarkOptions(Map.of("rightMargin", "100")); + StepHarness.forStep(FlexmarkStep.create(TestProvisioner.mavenCentral(), config)) + .testResource( + "markdown/flexmark/FlexmarkOptionsUnformatted.md", + "markdown/flexmark/FlexmarkOptionsFormatted.md"); + } + + @Test + void flexmarkOptionsUnknownKeyFails() { + FlexmarkConfig config = new FlexmarkConfig(); + config.setFlexmarkOptions(Map.of("nonExistentOption", "value")); + assertThatThrownBy(() -> + StepHarness.forStep(FlexmarkStep.create(TestProvisioner.mavenCentral(), config)) + .test("text\n", "text\n")) + .hasRootCauseInstanceOf(IllegalArgumentException.class) + .hasRootCauseMessage( + "Unknown flexmark formatter option 'nonExistentOption': no field Formatter.NON_EXISTENT_OPTION." + + " See https://github.com/vsch/flexmark-java/wiki/Markdown-Formatter#options"); + } + @Test void behaviorLatest() { FlexmarkConfig config = new FlexmarkConfig(); From 5a486ad2f20e4615653b64372cd8c445d6390e5a Mon Sep 17 00:00:00 2001 From: Ben Tatham Date: Mon, 8 Jun 2026 18:23:51 +0000 Subject: [PATCH 3/7] refactor(flexmark): rename flexmarkOptions to formatterOptions, move camelCase conversion to Maven plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FlexmarkConfig stores SCREAMING_SNAKE_CASE keys directly; camelCase→SCREAMING_SNAKE_CASE conversion now lives in the Maven plugin (Flexmark.java) where XML element name convention originates - Renamed flexmarkOptions→formatterOptions throughout (config field, getter/setter, @Parameter, Gradle DSL method, README, tests) - Added formatterOptions(Map) to Gradle FlexmarkExtension with a corresponding integration test - Added emulationProfile javadoc noting it can also be set via formatterOptions as FORMATTER_EMULATION_PROFILE - Updated plugin-maven/README.md with formatterOptions documentation and example Co-Authored-By: Claude Sonnet 4.6 --- .../glue/markdown/FlexmarkFormatterFunc.java | 33 ++++++++----------- .../spotless/markdown/FlexmarkConfig.java | 13 ++++---- plugin-gradle/README.md | 3 ++ .../gradle/spotless/FlexmarkExtension.java | 6 ++++ .../spotless/FlexmarkExtensionTest.java | 21 ++++++++++++ plugin-maven/README.md | 5 +++ .../spotless/maven/markdown/Flexmark.java | 16 +++++++-- .../maven/markdown/FlexmarkMavenTest.java | 2 +- .../spotless/markdown/FlexmarkStepTest.java | 6 ++-- 9 files changed, 72 insertions(+), 33 deletions(-) diff --git a/lib/src/flexmark/java/com/diffplug/spotless/glue/markdown/FlexmarkFormatterFunc.java b/lib/src/flexmark/java/com/diffplug/spotless/glue/markdown/FlexmarkFormatterFunc.java index 3c70b50593..2c9b1f88b4 100644 --- a/lib/src/flexmark/java/com/diffplug/spotless/glue/markdown/FlexmarkFormatterFunc.java +++ b/lib/src/flexmark/java/com/diffplug/spotless/glue/markdown/FlexmarkFormatterFunc.java @@ -19,7 +19,6 @@ import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import com.vladsch.flexmark.formatter.Formatter; @@ -147,9 +146,9 @@ private static Extension[] buildExtensions(List config) { /** * Creates the formatter options, copies the parser extensions and changes defaults that make sense for a formatter. - * Arbitrary flexmark formatter options can be set via {@link FlexmarkConfig#getFlexmarkOptions()}: each key is a - * camelCase name (e.g. {@code rightMargin}) that is converted to SCREAMING_SNAKE_CASE to look up the corresponding - * static {@link DataKey} field on {@link Formatter} via reflection. An unrecognised key fails the build. + * Arbitrary flexmark formatter options can be set via {@link FlexmarkConfig#getFormatterOptions()}: each key is a + * SCREAMING_SNAKE_CASE name (e.g. {@code RIGHT_MARGIN}) matching the corresponding static {@link DataKey} field on + * {@link Formatter}. An unrecognised key fails the build. * See: https://github.com/vsch/flexmark-java/wiki/Markdown-Formatter#options * * @param parserOptions the options used for the parser @@ -162,32 +161,31 @@ private static MutableDataHolder createFormatterOptions(MutableDataHolder parser final MutableDataHolder formatterOptions = new MutableDataSet(); formatterOptions.set(Parser.EXTENSIONS, Parser.EXTENSIONS.get(parserOptions)); formatterOptions.set(Formatter.FORMATTER_EMULATION_PROFILE, emulationProfile); - applyFlexmarkOptions(formatterOptions, config.getFlexmarkOptions()); + applyFlexmarkOptions(formatterOptions, config.getFormatterOptions()); return formatterOptions; } /** * Applies arbitrary formatter options from the config map to the given data holder. - * Each camelCase key (e.g. {@code rightMargin}) is converted to SCREAMING_SNAKE_CASE and looked up as a static + * Each key is a SCREAMING_SNAKE_CASE name (e.g. {@code RIGHT_MARGIN}) matching the corresponding static * {@link DataKey} field on {@link Formatter}. Supported value types are {@link Integer}, {@link Boolean}, and * {@link String}; the type is inferred from the DataKey's default value. An unknown key or unsupported type * throws {@link IllegalArgumentException}, which fails the build. */ - private static void applyFlexmarkOptions(MutableDataHolder options, Map flexmarkOptions) { - if (flexmarkOptions.isEmpty()) { + private static void applyFlexmarkOptions(MutableDataHolder options, Map formatterOptions) { + if (formatterOptions.isEmpty()) { return; } MutableDataSet defaults = new MutableDataSet(); - for (Map.Entry entry : flexmarkOptions.entrySet()) { - String camelKey = entry.getKey(); + for (Map.Entry entry : formatterOptions.entrySet()) { + String fieldName = entry.getKey(); String rawValue = entry.getValue(); - String fieldName = camelToScreamingSnake(camelKey); Field field; try { field = Formatter.class.getField(fieldName); } catch (NoSuchFieldException e) { throw new IllegalArgumentException( - "Unknown flexmark formatter option '" + camelKey + "': no field Formatter." + fieldName + "Unknown flexmark formatter option: no field Formatter." + fieldName + ". See https://github.com/vsch/flexmark-java/wiki/Markdown-Formatter#options"); } DataKey dataKey; @@ -196,18 +194,13 @@ private static void applyFlexmarkOptions(MutableDataHolder options, Map pegdownExtensions = List.of("ALL"); private List extensions = new ArrayList<>(); - private Map flexmarkOptions = new LinkedHashMap<>(); + private Map formatterOptions = new LinkedHashMap<>(); public String getEmulationProfile() { return emulationProfile; @@ -59,12 +60,12 @@ public void setExtensions(List extensions) { this.extensions = extensions; } - public Map getFlexmarkOptions() { - return flexmarkOptions; + public Map getFormatterOptions() { + return formatterOptions; } - public void setFlexmarkOptions(Map flexmarkOptions) { - this.flexmarkOptions = flexmarkOptions; + public void setFormatterOptions(Map formatterOptions) { + this.formatterOptions = formatterOptions; } } diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index be5451276f..6719c5b508 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -795,6 +795,8 @@ You can change the `emulationProfile` to one of the other [supported profiles](h The `pegdownExtensions` can be configured as a list of [constants](https://github.com/vsch/flexmark-java/blob/master/flexmark/src/main/java/com/vladsch/flexmark/parser/PegdownExtensions.java) or as a custom bitset as an integer. Any other `extension` can be configured using either the simple name as shown in the example or using a full-qualified class name. +Arbitrary formatter options from the [flexmark-java Markdown Formatter](https://github.com/vsch/flexmark-java/wiki/Markdown-Formatter#options) can be set via `formatterOptions`. Each key is a `SCREAMING_SNAKE_CASE` constant from [`com.vladsch.flexmark.formatter.Formatter`](https://github.com/vsch/flexmark-java/blob/master/flexmark/src/main/java/com/vladsch/flexmark/formatter/Formatter.java) (e.g. `RIGHT_MARGIN`). Supported value types are `Integer`, `Boolean`, and `String`. + To apply flexmark to all of the `.md` files in your project, use this snippet: ```gradle @@ -805,6 +807,7 @@ spotless { .emulationProfile('COMMONMARK') // optional .pegdownExtensions('ALL') // optional .extensions('YamlFrontMatter') // optional + .formatterOptions(['RIGHT_MARGIN': '120']) // optional } } ``` diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FlexmarkExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FlexmarkExtension.java index 4147645835..f7d255e2c4 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FlexmarkExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FlexmarkExtension.java @@ -16,6 +16,7 @@ package com.diffplug.gradle.spotless; import java.util.List; +import java.util.Map; import java.util.Objects; import javax.inject.Inject; @@ -79,6 +80,11 @@ public FlexmarkFormatterConfig extensions(String... extensions) { return this; } + public FlexmarkFormatterConfig formatterOptions(Map formatterOptions) { + this.config.setFormatterOptions(formatterOptions); + return this; + } + private FormatterStep createStep() { return FlexmarkStep.create(this.version, provisioner(), config); } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FlexmarkExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FlexmarkExtensionTest.java index b449c1416a..15e94ce5b4 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FlexmarkExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FlexmarkExtensionTest.java @@ -43,6 +43,27 @@ void integrationSimpleExtensions() throws IOException { assertFile("markdown_test.md").sameAsResource("markdown/flexmark/FlexmarkFormatted.md"); } + @Test + void integrationFormatterOptions() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " flexmark {", + " target '*.md'", + " flexmark()", + " .extensions('YamlFrontMatter')", + " .formatterOptions(['RIGHT_MARGIN': '100'])", + " }", + "}"); + setFile("markdown_test.md").toResource("markdown/flexmark/FlexmarkOptionsUnformatted.md"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("markdown_test.md").sameAsResource("markdown/flexmark/FlexmarkOptionsFormatted.md"); + } + @Test void integrationComplex() throws IOException { setFile("build.gradle").toLines( diff --git a/plugin-maven/README.md b/plugin-maven/README.md index fa06738721..992eb6bf9a 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -878,11 +878,16 @@ You can change the `emulationProfile` to one of the other [supported profiles](h The `pegdownExtensions` can be configured as a comma-seperated list of [constants](https://github.com/vsch/flexmark-java/blob/master/flexmark/src/main/java/com/vladsch/flexmark/parser/PegdownExtensions.java) or as a custom bitset as an integer. Any other `extension` can be configured using either the simple name as shown in the example or using a full-qualified class name. +Arbitrary formatter options from the [flexmark-java Markdown Formatter](https://github.com/vsch/flexmark-java/wiki/Markdown-Formatter#options) can be set via `formatterOptions`. Each key is a camelCase version of the corresponding `SCREAMING_SNAKE_CASE` constant on [`com.vladsch.flexmark.formatter.Formatter`](https://github.com/vsch/flexmark-java/blob/master/flexmark/src/main/java/com/vladsch/flexmark/formatter/Formatter.java) (e.g. `rightMargin` maps to `Formatter.RIGHT_MARGIN`). Supported value types are `Integer`, `Boolean`, and `String`. + ```xml COMMONMARK ALL,TOC YamlFrontMatter,Emoji + + 120 + ``` diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Flexmark.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Flexmark.java index 289656ee9e..ed65c725ea 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Flexmark.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Flexmark.java @@ -15,7 +15,9 @@ */ package com.diffplug.spotless.maven.markdown; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import org.apache.maven.plugins.annotations.Parameter; @@ -38,7 +40,7 @@ public class Flexmark implements FormatterStepFactory { @Parameter private String extensions; @Parameter - private Map flexmarkOptions; + private Map formatterOptions; @Override public FormatterStep newFormatterStep(FormatterStepConfig config) { @@ -53,9 +55,17 @@ public FormatterStep newFormatterStep(FormatterStepConfig config) { if (this.extensions != null) { flexmarkConfig.setExtensions(List.of(this.extensions.split(","))); } - if (this.flexmarkOptions != null) { - flexmarkConfig.setFlexmarkOptions(this.flexmarkOptions); + if (this.formatterOptions != null) { + Map converted = new LinkedHashMap<>(); + for (Map.Entry entry : this.formatterOptions.entrySet()) { + converted.put(camelToScreamingSnake(entry.getKey()), entry.getValue()); + } + flexmarkConfig.setFormatterOptions(converted); } return FlexmarkStep.create(version, config.getProvisioner(), flexmarkConfig); } + + private static String camelToScreamingSnake(String camel) { + return camel.replaceAll("([A-Z])", "_$1").toUpperCase(Locale.ROOT); + } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java index 053282bbd2..373b2649f9 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java @@ -32,7 +32,7 @@ public void testFlexmarkWithSimpleConfig() throws Exception { @Test public void testFlexmarkWithOptions() throws Exception { - writePomWithMarkdownSteps("YamlFrontMatter100"); + writePomWithMarkdownSteps("YamlFrontMatter100"); setFile("markdown_test.md").toResource("markdown/flexmark/FlexmarkOptionsUnformatted.md"); mavenRunner().withArguments("spotless:apply").runNoError(); diff --git a/testlib/src/test/java/com/diffplug/spotless/markdown/FlexmarkStepTest.java b/testlib/src/test/java/com/diffplug/spotless/markdown/FlexmarkStepTest.java index 45a092d48f..384ba47bfe 100644 --- a/testlib/src/test/java/com/diffplug/spotless/markdown/FlexmarkStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/markdown/FlexmarkStepTest.java @@ -43,7 +43,7 @@ void behaviorOldest() { void flexmarkOptionsRightMargin() { FlexmarkConfig config = new FlexmarkConfig(); config.setExtensions(List.of("YamlFrontMatter")); - config.setFlexmarkOptions(Map.of("rightMargin", "100")); + config.setFormatterOptions(Map.of("RIGHT_MARGIN", "100")); StepHarness.forStep(FlexmarkStep.create(TestProvisioner.mavenCentral(), config)) .testResource( "markdown/flexmark/FlexmarkOptionsUnformatted.md", @@ -53,13 +53,13 @@ void flexmarkOptionsRightMargin() { @Test void flexmarkOptionsUnknownKeyFails() { FlexmarkConfig config = new FlexmarkConfig(); - config.setFlexmarkOptions(Map.of("nonExistentOption", "value")); + config.setFormatterOptions(Map.of("NON_EXISTENT_OPTION", "value")); assertThatThrownBy(() -> StepHarness.forStep(FlexmarkStep.create(TestProvisioner.mavenCentral(), config)) .test("text\n", "text\n")) .hasRootCauseInstanceOf(IllegalArgumentException.class) .hasRootCauseMessage( - "Unknown flexmark formatter option 'nonExistentOption': no field Formatter.NON_EXISTENT_OPTION." + "Unknown flexmark formatter option: no field Formatter.NON_EXISTENT_OPTION." + " See https://github.com/vsch/flexmark-java/wiki/Markdown-Formatter#options"); } From d028735e06ecdfce02260f4a0b60d981cc94ba74 Mon Sep 17 00:00:00 2001 From: Ben Tatham Date: Tue, 9 Jun 2026 17:19:16 +0000 Subject: [PATCH 4/7] chore: add changelog entries for flexmark formatterOptions feature Co-Authored-By: Claude Sonnet 4.6 --- CHANGES.md | 1 + plugin-gradle/CHANGES.md | 1 + plugin-maven/CHANGES.md | 1 + 3 files changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 3bb39603f6..470a7bac26 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ 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)) +- `flexmark` step now supports arbitrary formatter options via a `formatterOptions` map. ([#XXXX](https://github.com/diffplug/spotless/pull/XXXX)) ### Fixed - Support `ktfmt` 0.63 and use its new builder API for formatting options to better avoid future breaking changes. ### Changes diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 6153cb7a63..efb28f3b64 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -6,6 +6,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Added - Add support for AsciiDoc formatting via `adocfmt`. ([#2960](https://github.com/diffplug/spotless/pull/2960)) +- `flexmark()` step now supports arbitrary formatter options via the `formatterOptions` map. ([#XXXX](https://github.com/diffplug/spotless/pull/XXXX)) ### 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-maven/CHANGES.md b/plugin-maven/CHANGES.md index dfdc2eedfd..09a7a750b6 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -6,6 +6,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Added - Add support for AsciiDoc formatting via `adocfmt`. ([#2960](https://github.com/diffplug/spotless/pull/2960)) +- `` step now supports arbitrary formatter options via ``. ([#XXXX](https://github.com/diffplug/spotless/pull/XXXX)) ## [3.6.0] - 2026-05-27 ### Added From c2d0874a2712d00868b968d643417a181bf86c2f Mon Sep 17 00:00:00 2001 From: Ben Tatham Date: Tue, 9 Jun 2026 17:24:32 +0000 Subject: [PATCH 5/7] chore: update changelog PR links to #2968 Co-Authored-By: Claude Sonnet 4.6 --- CHANGES.md | 2 +- plugin-gradle/CHANGES.md | 2 +- plugin-maven/CHANGES.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 470a7bac26..0da50103fd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,7 @@ 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)) -- `flexmark` step now supports arbitrary formatter options via a `formatterOptions` map. ([#XXXX](https://github.com/diffplug/spotless/pull/XXXX)) +- `flexmark` step now supports arbitrary formatter options via a `formatterOptions` map. ([#2968](https://github.com/diffplug/spotless/pull/2968)) ### Fixed - Support `ktfmt` 0.63 and use its new builder API for formatting options to better avoid future breaking changes. ### Changes diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index efb28f3b64..48aa9843cb 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -6,7 +6,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Added - Add support for AsciiDoc formatting via `adocfmt`. ([#2960](https://github.com/diffplug/spotless/pull/2960)) -- `flexmark()` step now supports arbitrary formatter options via the `formatterOptions` map. ([#XXXX](https://github.com/diffplug/spotless/pull/XXXX)) +- `flexmark()` step now supports arbitrary formatter options via the `formatterOptions` map. ([#2968](https://github.com/diffplug/spotless/pull/2968)) ### 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-maven/CHANGES.md b/plugin-maven/CHANGES.md index 09a7a750b6..0694244049 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -6,7 +6,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Added - Add support for AsciiDoc formatting via `adocfmt`. ([#2960](https://github.com/diffplug/spotless/pull/2960)) -- `` step now supports arbitrary formatter options via ``. ([#XXXX](https://github.com/diffplug/spotless/pull/XXXX)) +- `` step now supports arbitrary formatter options via ``. ([#2968](https://github.com/diffplug/spotless/pull/2968)) ## [3.6.0] - 2026-05-27 ### Added From c38bc20b7a465d4ae4d8fc4fe81814b672448ee6 Mon Sep 17 00:00:00 2001 From: Ben Tatham Date: Tue, 9 Jun 2026 18:59:21 +0000 Subject: [PATCH 6/7] Use TreeMap instead of LinkedHashMap for consistent ordering --- .../com/diffplug/spotless/markdown/FlexmarkConfig.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkConfig.java b/lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkConfig.java index c2816681af..db88248c8b 100644 --- a/lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkConfig.java +++ b/lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 DiffPlug + * Copyright 2025-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,9 @@ import java.io.Serial; import java.io.Serializable; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.TreeMap; public class FlexmarkConfig implements Serializable { @Serial @@ -34,7 +34,7 @@ public class FlexmarkConfig implements Serializable { private String emulationProfile = "COMMONMARK"; private List pegdownExtensions = List.of("ALL"); private List extensions = new ArrayList<>(); - private Map formatterOptions = new LinkedHashMap<>(); + private Map formatterOptions = new TreeMap<>(); public String getEmulationProfile() { return emulationProfile; @@ -65,7 +65,7 @@ public Map getFormatterOptions() { } public void setFormatterOptions(Map formatterOptions) { - this.formatterOptions = formatterOptions; + this.formatterOptions = new TreeMap<>(formatterOptions); } } From 1ffc012b65df97c3f834f29000ee09fd1d62822c Mon Sep 17 00:00:00 2001 From: Ben Tatham Date: Tue, 9 Jun 2026 18:59:34 +0000 Subject: [PATCH 7/7] apply spotless jave formatting --- .../spotless/glue/markdown/FlexmarkFormatterFunc.java | 2 +- .../com/diffplug/gradle/spotless/FlexmarkExtension.java | 2 +- .../diffplug/gradle/spotless/FlexmarkExtensionTest.java | 2 +- .../com/diffplug/spotless/maven/markdown/Flexmark.java | 2 +- .../spotless/maven/markdown/FlexmarkMavenTest.java | 2 +- .../com/diffplug/spotless/markdown/FlexmarkStepTest.java | 8 +++----- 6 files changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/src/flexmark/java/com/diffplug/spotless/glue/markdown/FlexmarkFormatterFunc.java b/lib/src/flexmark/java/com/diffplug/spotless/glue/markdown/FlexmarkFormatterFunc.java index 2c9b1f88b4..bee5e50ad6 100644 --- a/lib/src/flexmark/java/com/diffplug/spotless/glue/markdown/FlexmarkFormatterFunc.java +++ b/lib/src/flexmark/java/com/diffplug/spotless/glue/markdown/FlexmarkFormatterFunc.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2025 DiffPlug + * Copyright 2021-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FlexmarkExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FlexmarkExtension.java index f7d255e2c4..9611b7399b 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FlexmarkExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FlexmarkExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 DiffPlug + * Copyright 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FlexmarkExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FlexmarkExtensionTest.java index 15e94ce5b4..23c43e148d 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FlexmarkExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FlexmarkExtensionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 DiffPlug + * Copyright 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Flexmark.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Flexmark.java index ed65c725ea..d005d7c92f 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Flexmark.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Flexmark.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2025 DiffPlug + * Copyright 2021-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java index 373b2649f9..c5d04fdf58 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2025 DiffPlug + * Copyright 2021-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/testlib/src/test/java/com/diffplug/spotless/markdown/FlexmarkStepTest.java b/testlib/src/test/java/com/diffplug/spotless/markdown/FlexmarkStepTest.java index 384ba47bfe..b5672acc16 100644 --- a/testlib/src/test/java/com/diffplug/spotless/markdown/FlexmarkStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/markdown/FlexmarkStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import org.junit.jupiter.api.Test; -import com.diffplug.spotless.ResourceHarness; import com.diffplug.spotless.StepHarness; import com.diffplug.spotless.TestProvisioner; @@ -54,9 +53,8 @@ void flexmarkOptionsRightMargin() { void flexmarkOptionsUnknownKeyFails() { FlexmarkConfig config = new FlexmarkConfig(); config.setFormatterOptions(Map.of("NON_EXISTENT_OPTION", "value")); - assertThatThrownBy(() -> - StepHarness.forStep(FlexmarkStep.create(TestProvisioner.mavenCentral(), config)) - .test("text\n", "text\n")) + assertThatThrownBy(() -> StepHarness.forStep(FlexmarkStep.create(TestProvisioner.mavenCentral(), config)) + .test("text\n", "text\n")) .hasRootCauseInstanceOf(IllegalArgumentException.class) .hasRootCauseMessage( "Unknown flexmark formatter option: no field Formatter.NON_EXISTENT_OPTION."