Skip to content
Open
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 @@ -14,6 +14,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
* Bump default `ktfmt` version to latest `0.58` -> `0.59`. ([#2681](https://github.com/diffplug/spotless/pull/2681)
### Fixed
- palantirJavaFormat is no longer arbitrarily set to outdated versions on Java 17, latest available version is always used ([#2686](https://github.com/diffplug/spotless/pull/2686) fixes [#2685](https://github.com/diffplug/spotless/issues/2685))
### Added
- new options to customize Flexmark, e.g. to allow YAML front matter ([#2616](https://github.com/diffplug/spotless/issues/2616))
### Removed
* **BREAKING** Drop support for older Ktlint versions. ([#2711](https://github.com/diffplug/spotless/pull/2711))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 DiffPlug
* Copyright 2021-2025 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,6 +15,12 @@
*/
package com.diffplug.spotless.glue.markdown;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.vladsch.flexmark.formatter.Formatter;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.parser.ParserEmulationProfile;
Expand All @@ -23,32 +29,50 @@
import com.vladsch.flexmark.util.ast.Document;
import com.vladsch.flexmark.util.data.MutableDataHolder;
import com.vladsch.flexmark.util.data.MutableDataSet;
import com.vladsch.flexmark.util.misc.Extension;

import com.diffplug.spotless.FormatterFunc;
import com.diffplug.spotless.markdown.FlexmarkConfig;

/**
* The formatter function for <a href="https://github.com/vsch/flexmark-java">flexmark-java</a>.
*/
public class FlexmarkFormatterFunc implements FormatterFunc {

/**
* 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.
*/
private static final String DEFAULT_EMULATION_PROFILE = "COMMONMARK";
private static final Map<String, String> KNOWN_EXTENSIONS = new HashMap<>();
static {
// using strings to maximize compatibility with older flexmark versions
KNOWN_EXTENSIONS.put("Abbreviation", "com.vladsch.flexmark.ext.abbreviation.AbbreviationExtension");
KNOWN_EXTENSIONS.put("Admonition", "com.vladsch.flexmark.ext.admonition.AdmonitionExtension");
KNOWN_EXTENSIONS.put("Aside", "com.vladsch.flexmark.ext.aside.AsideExtension");
KNOWN_EXTENSIONS.put("Attributes", "com.vladsch.flexmark.ext.attributes.AttributesExtension");
KNOWN_EXTENSIONS.put("Definition", "com.vladsch.flexmark.ext.definition.DefinitionExtension");
KNOWN_EXTENSIONS.put("Emoji", "com.vladsch.flexmark.ext.emoji.EmojiExtension");
KNOWN_EXTENSIONS.put("EnumeratedReference", "com.vladsch.flexmark.ext.enumerated.reference.EnumeratedReferenceExtension");
KNOWN_EXTENSIONS.put("Footnote", "com.vladsch.flexmark.ext.footnotes.FootnoteExtension");
KNOWN_EXTENSIONS.put("GitLab", "com.vladsch.flexmark.ext.gitlab.GitLabExtension");
KNOWN_EXTENSIONS.put("JekyllFrontMatter", "com.vladsch.flexmark.ext.jekyll.front.matter.JekyllFrontMatterExtension");
KNOWN_EXTENSIONS.put("JekyllTag", "com.vladsch.flexmark.ext.jekyll.tag.JekyllTagExtension");
KNOWN_EXTENSIONS.put("Macros", "com.vladsch.flexmark.ext.macros.MacrosExtension");
KNOWN_EXTENSIONS.put("SimToc", "com.vladsch.flexmark.ext.toc.SimTocExtension");
KNOWN_EXTENSIONS.put("Tables", "com.vladsch.flexmark.ext.tables.TablesExtension");
KNOWN_EXTENSIONS.put("TaskList", "com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension");
KNOWN_EXTENSIONS.put("WikiLink", "com.vladsch.flexmark.ext.wikilink.WikiLinkExtension");
KNOWN_EXTENSIONS.put("YamlFrontMatter", "com.vladsch.flexmark.ext.yaml.front.matter.YamlFrontMatterExtension");
}

private final Parser parser;
private final Formatter formatter;

public FlexmarkFormatterFunc() {
public FlexmarkFormatterFunc(FlexmarkConfig config) {
// flexmark-java has a separate parser and renderer (formatter)
// this is build from the example in https://github.com/vsch/flexmark-java/wiki/Markdown-Formatter

// The emulation profile generally determines the markdown flavor. We use the same one for both the parser and
// the formatter, to make sure this formatter func is idempotent.
final ParserEmulationProfile emulationProfile = ParserEmulationProfile.valueOf(DEFAULT_EMULATION_PROFILE);
final ParserEmulationProfile emulationProfile = ParserEmulationProfile.valueOf(config.getEmulationProfile());

final MutableDataHolder parserOptions = createParserOptions(emulationProfile);
final MutableDataHolder parserOptions = createParserOptions(emulationProfile, config);
final MutableDataHolder formatterOptions = createFormatterOptions(parserOptions, emulationProfile);

parser = Parser.builder(parserOptions).build();
Expand All @@ -62,12 +86,63 @@ public FlexmarkFormatterFunc() {
* @param emulationProfile the emulation profile (or flavor of markdown) the parser should use
* @return the created parser options
*/
private static MutableDataHolder createParserOptions(ParserEmulationProfile emulationProfile) {
final MutableDataHolder parserOptions = PegdownOptionsAdapter.flexmarkOptions(PegdownExtensions.ALL).toMutable();
private static MutableDataHolder createParserOptions(ParserEmulationProfile emulationProfile, FlexmarkConfig config) {
int pegdownExtensions = buildPegdownExtensions(config.getPegdownExtensions());
Extension[] extensions = buildExtensions(config.getExtensions());
final MutableDataHolder parserOptions = PegdownOptionsAdapter.flexmarkOptions(
pegdownExtensions, extensions).toMutable();
parserOptions.set(Parser.PARSER_EMULATION_PROFILE, emulationProfile);
return parserOptions;
}

/**
* Loads all listed pegdown extensions by using the constants defined in {@link PegdownExtensions}.
* Additionally, pure digit strings are directly converted to allow highly customized configurations.
*
* @param config the string-array configuration for pegdown
* @return bit-wise or'd extensions
*/
private static int buildPegdownExtensions(List<String> config) {
int extensions = PegdownExtensions.NONE;
for (String str : config) {
if (str.matches("\\d+")) {
extensions |= Integer.parseInt(str);
} else if (str.matches("(0x|0X)?[a-fA-F0-9]+")) {
extensions |= Integer.decode(str);
} else {
try {
Field field = PegdownExtensions.class.getField(str);
extensions |= field.getInt(null);
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException("Unknown PegdownExtension '" + str + "'");
}
}
}
return extensions;
}

/**
* Loads all listed extensions by looking up the implementation class (optionally resolving shortcuts
* for known extensions) and calling the conventional create-method.
*
* @param config the string-list of the configured extensions
* @return the array of Extension instances
*/
private static Extension[] buildExtensions(List<String> config) {
Extension[] extensions = new Extension[config.size()];
for (int i = 0; i < extensions.length; i++) {
String className = KNOWN_EXTENSIONS.getOrDefault(config.get(i), config.get(i));
try {
Class<?> c = Extension.class.getClassLoader().loadClass(className);
Method create = c.getMethod("create");
extensions[i] = Extension.class.cast(create.invoke(null));
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException("Unknown flexmark extension '" + config.get(i) + "'");
}
}
return extensions;
}

/**
* Creates the formatter options, copies the parser extensions and changes defaults that make sense for a formatter.
* See: https://github.com/vsch/flexmark-java/wiki/Markdown-Formatter#options
Expand All @@ -76,7 +151,8 @@ private static MutableDataHolder createParserOptions(ParserEmulationProfile emul
* @param emulationProfile the emulation profile (or flavor of markdown) the formatter should use
* @return the created formatter options
*/
private static MutableDataHolder createFormatterOptions(MutableDataHolder parserOptions, ParserEmulationProfile emulationProfile) {
private static MutableDataHolder createFormatterOptions(MutableDataHolder parserOptions,
ParserEmulationProfile emulationProfile) {
final MutableDataHolder formatterOptions = new MutableDataSet();
formatterOptions.set(Parser.EXTENSIONS, Parser.EXTENSIONS.get(parserOptions));
formatterOptions.set(Formatter.FORMATTER_EMULATION_PROFILE, emulationProfile);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2025 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.markdown;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

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.
*/
private String emulationProfile = "COMMONMARK";
private List<String> pegdownExtensions = List.of("ALL");
private List<String> extensions = new ArrayList<>();

public String getEmulationProfile() {
return emulationProfile;
}

public void setEmulationProfile(String emulationProfile) {
this.emulationProfile = emulationProfile;
}

public List<String> getPegdownExtensions() {
return pegdownExtensions;
}

public void setPegdownExtensions(List<String> pegdownExtensions) {
this.pegdownExtensions = pegdownExtensions;
}

public List<String> getExtensions() {
return extensions;
}

public void setExtensions(List<String> extensions) {
this.extensions = extensions;
}

}
22 changes: 13 additions & 9 deletions lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkStep.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,24 @@ public final class FlexmarkStep implements Serializable {
public static final String NAME = "flexmark-java";

private final JarState.Promised jarState;
private final FlexmarkConfig config;

private FlexmarkStep(JarState.Promised jarState) {
private FlexmarkStep(JarState.Promised jarState, FlexmarkConfig config) {
this.jarState = jarState;
this.config = config;
}

/** Creates a formatter step for the default version. */
public static FormatterStep create(Provisioner provisioner) {
return create(defaultVersion(), provisioner);
public static FormatterStep create(Provisioner provisioner, FlexmarkConfig config) {
return create(defaultVersion(), provisioner, config);
}

/** Creates a formatter step for the given version. */
public static FormatterStep create(String version, Provisioner provisioner) {
public static FormatterStep create(String version, Provisioner provisioner, FlexmarkConfig config) {
Objects.requireNonNull(version, "version");
Objects.requireNonNull(provisioner, "provisioner");
return FormatterStep.create(NAME,
new FlexmarkStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + version, provisioner))),
new FlexmarkStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + version, provisioner)), config),
FlexmarkStep::equalityState,
State::createFormat);
}
Expand All @@ -59,24 +61,26 @@ public static String defaultVersion() {
}

private State equalityState() {
return new State(jarState.get());
return new State(jarState.get(), config);
}

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

private final JarState jarState;
private final FlexmarkConfig config;

State(JarState jarState) {
State(JarState jarState, FlexmarkConfig config) {
this.jarState = jarState;
this.config = config;
}

FormatterFunc createFormat() throws Exception {
final ClassLoader classLoader = jarState.getClassLoader();
final Class<?> formatterFunc = classLoader.loadClass("com.diffplug.spotless.glue.markdown.FlexmarkFormatterFunc");
final Constructor<?> constructor = formatterFunc.getConstructor();
return (FormatterFunc) constructor.newInstance();
final Constructor<?> constructor = formatterFunc.getConstructor(FlexmarkConfig.class);
return (FormatterFunc) constructor.newInstance(config);
}
}
}
2 changes: 2 additions & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
* Use absolute path in the git pre push hook
### Fixed
- palantirJavaFormat is no longer arbitrarily set to outdated versions on Java 17, latest available version is always used ([#2686](https://github.com/diffplug/spotless/pull/2686) fixes [#2685](https://github.com/diffplug/spotless/issues/2685))
### Added
- new options to customize Flexmark, e.g. to allow YAML front matter ([#2616](https://github.com/diffplug/spotless/issues/2616))
### Removed
* **BREAKING** Drop support for older Ktlint versions. ([#2711](https://github.com/diffplug/spotless/pull/2711))

Expand Down
8 changes: 7 additions & 1 deletion plugin-gradle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,10 @@ spotless {

[homepage](https://github.com/vsch/flexmark-java). Flexmark is a flexible Commonmark/Markdown parser that can be used to format Markdown files. It supports different [flavors of Markdown](https://github.com/vsch/flexmark-java#markdown-processor-emulation) and [many formatting options](https://github.com/vsch/flexmark-java/wiki/Markdown-Formatter#options).

Currently, none of the available options can be configured yet. It uses only the default options together with `COMMONMARK` as `FORMATTER_EMULATION_PROFILE`.
The default configuration uses a pegdown compatible parser with `COMMONMARK` as `FORMATTER_EMULATION_PROFILE`.
You can change the `emulationProfile` to one of the other [supported profiles](https://github.com/vsch/flexmark-java/blob/master/flexmark/src/main/java/com/vladsch/flexmark/parser/ParserEmulationProfile.java).
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.

To apply flexmark to all of the `.md` files in your project, use this snippet:

Expand All @@ -745,6 +748,9 @@ spotless {
flexmark {
target '**/*.md' // you have to set the target manually
flexmark() // or flexmark('0.64.8') // version is optional
.emulationProfile('COMMONMARK') // optional
.pegdownExtensions('ALL') // optional
.extensions('YamlFrontMatter') // optional
}
}
```
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 DiffPlug
* Copyright 2023-2025 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,11 +15,13 @@
*/
package com.diffplug.gradle.spotless;

import java.util.List;
import java.util.Objects;

import javax.inject.Inject;

import com.diffplug.spotless.FormatterStep;
import com.diffplug.spotless.markdown.FlexmarkConfig;
import com.diffplug.spotless.markdown.FlexmarkStep;

public class FlexmarkExtension extends FormatExtension {
Expand Down Expand Up @@ -50,14 +52,35 @@ protected void setupTask(SpotlessTask task) {
public class FlexmarkFormatterConfig {

private final String version;
private final FlexmarkConfig config = new FlexmarkConfig();

FlexmarkFormatterConfig(String version) {
this.version = Objects.requireNonNull(version);
addStep(createStep());
}

public FlexmarkFormatterConfig emulationProfile(String emulationProfile) {
this.config.setEmulationProfile(emulationProfile);
return this;
}

public FlexmarkFormatterConfig pegdownExtensions(int pegdownExtensions) {
this.config.setPegdownExtensions(List.of(Integer.toString(pegdownExtensions)));
return this;
}

public FlexmarkFormatterConfig pegdownExtensions(String... pegdownExtensions) {
this.config.setPegdownExtensions(List.of(pegdownExtensions));
return this;
}

public FlexmarkFormatterConfig extensions(String... extensions) {
this.config.setExtensions(List.of(extensions));
return this;
}

private FormatterStep createStep() {
return FlexmarkStep.create(this.version, provisioner());
return FlexmarkStep.create(this.version, provisioner(), config);
}
}

Expand Down
Loading
Loading