diff --git a/CHANGES.md b/CHANGES.md index 3bb39603f6..0da50103fd 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. ([#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/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..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. @@ -27,6 +27,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 +74,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 +146,74 @@ 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#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 * @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.getFormatterOptions()); return formatterOptions; } + /** + * Applies arbitrary formatter options from the config map to the given data holder. + * 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 formatterOptions) { + if (formatterOptions.isEmpty()) { + return; + } + MutableDataSet defaults = new MutableDataSet(); + for (Map.Entry entry : formatterOptions.entrySet()) { + String fieldName = entry.getKey(); + String rawValue = entry.getValue(); + Field field; + try { + field = Formatter.class.getField(fieldName); + } catch (NoSuchFieldException e) { + throw new IllegalArgumentException( + "Unknown flexmark formatter option: 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, fieldName); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void setOption(MutableDataHolder options, DataKey dataKey, Object defaultValue, String rawValue, + String fieldName) { + 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 '" + fieldName + "': " + + (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..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. @@ -19,6 +19,8 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.TreeMap; public class FlexmarkConfig implements Serializable { @Serial @@ -26,11 +28,13 @@ public class FlexmarkConfig implements Serializable { /** * The emulation profile is used by both the parser and the formatter and generally determines the markdown flavor. - * COMMONMARK is the default defined by flexmark-java. + * COMMONMARK is the default defined by flexmark-java. For convenience, this can also be set via + * {@code formatterOptions} with the key {@code FORMATTER_EMULATION_PROFILE}. */ private String emulationProfile = "COMMONMARK"; private List pegdownExtensions = List.of("ALL"); private List extensions = new ArrayList<>(); + private Map formatterOptions = new TreeMap<>(); public String getEmulationProfile() { return emulationProfile; @@ -56,4 +60,12 @@ public void setExtensions(List extensions) { this.extensions = extensions; } + public Map getFormatterOptions() { + return formatterOptions; + } + + public void setFormatterOptions(Map formatterOptions) { + this.formatterOptions = new TreeMap<>(formatterOptions); + } + } diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 6153cb7a63..48aa9843cb 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. ([#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-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..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. @@ -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..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. @@ -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/CHANGES.md b/plugin-maven/CHANGES.md index dfdc2eedfd..0694244049 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 ``. ([#2968](https://github.com/diffplug/spotless/pull/2968)) ## [3.6.0] - 2026-05-27 ### Added 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 a187019c22..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. @@ -15,7 +15,10 @@ */ 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; @@ -36,6 +39,8 @@ public class Flexmark implements FormatterStepFactory { private String pegdownExtensions; @Parameter private String extensions; + @Parameter + private Map formatterOptions; @Override public FormatterStep newFormatterStep(FormatterStepConfig config) { @@ -50,6 +55,17 @@ public FormatterStep newFormatterStep(FormatterStepConfig config) { if (this.extensions != null) { flexmarkConfig.setExtensions(List.of(this.extensions.split(","))); } + 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 df55059c45..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. @@ -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..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. @@ -15,7 +15,10 @@ */ 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; @@ -35,6 +38,29 @@ void behaviorOldest() { "markdown/flexmark/FlexmarkFormatted.md"); } + @Test + void flexmarkOptionsRightMargin() { + FlexmarkConfig config = new FlexmarkConfig(); + config.setExtensions(List.of("YamlFrontMatter")); + config.setFormatterOptions(Map.of("RIGHT_MARGIN", "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.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: no field Formatter.NON_EXISTENT_OPTION." + + " See https://github.com/vsch/flexmark-java/wiki/Markdown-Formatter#options"); + } + @Test void behaviorLatest() { FlexmarkConfig config = new FlexmarkConfig();