diff --git a/CLAUDE.md b/CLAUDE.md
index 3fbc338..e18997f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -51,9 +51,9 @@ mvn versions:display-dependency-updates # Maven dependency updates only
mvn -pl gmd-core clean package
# Use the fat JAR
-java -jar gmd-core/target/gmd-3.1.0.jar toHtml input.gmd output.html
-java -jar gmd-core/target/gmd-3.1.0.jar toPdf input.gmd output.pdf
-java -jar gmd-core/target/gmd-3.1.0.jar toPdfRaw input.gmd output.pdf
+java -jar gmd-core/target/gmd-3.2.0-SNAPSHOT.jar toHtml input.gmd output.html
+java -jar gmd-core/target/gmd-3.2.0-SNAPSHOT.jar toPdf input.gmd output.pdf
+java -jar gmd-core/target/gmd-3.2.0-SNAPSHOT.jar toPdfRaw input.gmd output.pdf
```
## Version Management
@@ -63,12 +63,14 @@ java -jar gmd-core/target/gmd-3.1.0.jar toPdfRaw input.gmd output.pdf
```xml
- 3.1.0
+ 3.2.0-SNAPSHOT
```
All child modules automatically inherit this version via `${revision}`. The `flatten-maven-plugin` resolves this during build/deployment.
+When bumping `revision`, verify that the Maven `${plugin.version}` default and Gradle resource-based default remain synchronized with the core version, and update their user documentation if the default behavior changes.
+
## Project Structure
### Module Organization
@@ -149,8 +151,8 @@ PDF Output
## Key Dependencies
- **Groovy**: 5.0.8 (groovy, groovy-templates, groovy-jsr223)
-- **Markdown**: commonmark 0.29.0 + GFM tables extension
-- **PDF**: openhtmltopdf 1.1.65 (core, pdfbox, mathml, svg)
+- **Markdown**: commonmark 0.30.0 + GFM tables extension
+- **PDF**: openhtmltopdf 1.1.70 (core, pdfbox, mathml, svg)
- **Syntax highlighting**: Highlight.js 11.7.0, syntax-transpiled and evaluated by Rhino 1.9.1
- **HTML fragments**: Jsoup 1.23.1
- **Matrix**: se.alipsa.matrix BOM 2.5.1 (charts, core, xchart)
@@ -177,7 +179,7 @@ Rhino keeps mutable Highlight.js state in a shared scope, so calls are synchroni
## Testing
-Tests use JUnit 5 (Jupiter 6.1.2):
+Tests use JUnit Jupiter 6.1.3:
- `gmd-core/src/test/groovy/test/alipsa/groovy/gmd/`
- GmdTest.groovy
- GmdTemplateEngineTest.groovy
@@ -199,7 +201,7 @@ Requires:
### Gradle Plugin Portal (gmd-gradle-plugin)
```bash
cd gmd-gradle-plugin
-./gradlew publishPlugins
+./gradlew publishPlugins -Ppublish.version=
```
### GitHub Releases
diff --git a/gmd-core/pom.xml b/gmd-core/pom.xml
index 7066aea..6c75aba 100644
--- a/gmd-core/pom.xml
+++ b/gmd-core/pom.xml
@@ -39,9 +39,9 @@
UTF-8
21
- 1.1.65
- 0.29.0
- 6.1.2
+ 1.1.70
+ 0.30.0
+ 6.1.3
${project.build.directory}/site/groovydoc
se.alipsa.gmd.core.Gmd
diff --git a/gmd-core/release.md b/gmd-core/release.md
index 8fcc4f3..2bb05cf 100644
--- a/gmd-core/release.md
+++ b/gmd-core/release.md
@@ -1,5 +1,12 @@
# Gmd Release History
+## v3.2.0, unreleased
+- restore the `HtmlDecorator.BOOTSTRAP_CSS` and `HIGHLIGHT_JS_CSS` compatibility constants
+- upgrade the JUnit BOM from 6.1.2 to 6.1.3
+- upgrade OpenHTMLToPDF from 1.1.65 to 1.1.70
+- upgrade CommonMark from 0.29.0 to 0.30.0
+- remove obsolete JavaFX filtering from the fat jar assembly
+
## v3.1.0, 2026-08-02
- add the independent `highlightjs-jvm` module using Rhino and a syntax-transpiled Highlight.js bundle
- highlight code blocks synchronously before PDF rendering
diff --git a/gmd-core/src/main/groovy/se/alipsa/gmd/core/HtmlDecorator.groovy b/gmd-core/src/main/groovy/se/alipsa/gmd/core/HtmlDecorator.groovy
index 3d70cdc..30f28e8 100644
--- a/gmd-core/src/main/groovy/se/alipsa/gmd/core/HtmlDecorator.groovy
+++ b/gmd-core/src/main/groovy/se/alipsa/gmd/core/HtmlDecorator.groovy
@@ -8,14 +8,15 @@ class HtmlDecorator {
private static final Logger log = LogManager.getLogger(HtmlDecorator.class)
public static final String HIGHLIGHT_JS_CSS_PATH = "/highlightJs/styles/default.min.css"
+ public static final String HIGHLIGHT_JS_CSS = styleLink(HIGHLIGHT_JS_CSS_PATH, "\n", "\n")
public static final String BOOTSTRAP_CSS_PATH = "/META-INF/resources/webjars/bootstrap/5.3.8/css/bootstrap.css"
/**
- * The external URL of the Bootstrap stylesheet.
+ * The external-form URL of the Bootstrap stylesheet, suitable for use in an HTML href.
*
- * @deprecated Use {@link #BOOTSTRAP_CSS_PATH} when referring to the classpath resource.
+ * {@link #BOOTSTRAP_CSS_PATH} is the classpath resource path and is not a replacement for
+ * this external URL.
*/
- @Deprecated
public static final String BOOTSTRAP_CSS = resourceUrlExternalForm(BOOTSTRAP_CSS_PATH)
public static final String HTML5_DECLARATION = "\n"
diff --git a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/AbstractGmdTest.groovy b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/AbstractGmdTest.groovy
index a0450a5..83307f9 100644
--- a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/AbstractGmdTest.groovy
+++ b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/AbstractGmdTest.groovy
@@ -1,15 +1,9 @@
package test.alipsa.groovy.gmd
-import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.io.TempDir
class AbstractGmdTest {
- static File testOutputDir = new File("build/test-results/")
-
- @BeforeAll
- static void init() {
- if (!testOutputDir.exists()) {
- testOutputDir.mkdirs()
- }
- }
+ @TempDir
+ File testOutputDir
}
diff --git a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdHighlightTest.groovy b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdHighlightTest.groovy
index 36309cb..a0ce8e2 100644
--- a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdHighlightTest.groovy
+++ b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdHighlightTest.groovy
@@ -32,7 +32,7 @@ class GmdHighlightTest extends AbstractGmdTest {
def html = gmd.gmdToHtmlDoc(text)
// create a pdf file from the html
- def pdfFile = new File(AbstractGmdTest.testOutputDir, "testHighlight.pdf")
+ def pdfFile = new File(testOutputDir, "testHighlight.pdf")
if (pdfFile.exists()) pdfFile.delete()
gmd.processHtmlAndSaveAsPdf(html, pdfFile)
//gmd.gmdToPdf(text, pdfFile)
diff --git a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdProcessorTest.groovy b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdProcessorTest.groovy
index 0265d67..96a26fe 100644
--- a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdProcessorTest.groovy
+++ b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdProcessorTest.groovy
@@ -3,12 +3,14 @@ package test.alipsa.groovy.gmd
import org.junit.jupiter.api.Test
import se.alipsa.gmd.core.GmdProcessor
+import static org.junit.jupiter.api.Assertions.assertEquals
+import static org.junit.jupiter.api.Assertions.assertFalse
import static org.junit.jupiter.api.Assertions.assertTrue
class GmdProcessorTest extends AbstractGmdTest {
- private static File sourceDirWith(String name, String content) {
- File dir = new File(AbstractGmdTest.testOutputDir, "src-${name}")
+ private File sourceDirWith(String name, String content) {
+ File dir = new File(testOutputDir, "src-${name}")
dir.mkdirs()
new File(dir, "${name}.gmd").text = content
return dir
@@ -17,7 +19,9 @@ class GmdProcessorTest extends AbstractGmdTest {
@Test
void htmlOutputIsACompleteDocument() {
File src = sourceDirWith('doc', "# Hi\n\n```{groovy}\nout.println('x')\n```\n")
- File target = new File(AbstractGmdTest.testOutputDir, 'out-doc')
+ File target = new File(testOutputDir, 'out-doc')
+ target.deleteDir()
+ assertFalse(target.exists(), "Could not clear test target ${target.absolutePath}")
new GmdProcessor().process(src.absolutePath, target.absolutePath, 'html')
@@ -31,7 +35,9 @@ class GmdProcessorTest extends AbstractGmdTest {
@Test
void mdOutputIsStillPlainMarkdown() {
File src = sourceDirWith('plain', "# Hi\n\n```{groovy echo=false}\nout.println('x')\n```\n")
- File target = new File(AbstractGmdTest.testOutputDir, 'out-plain')
+ File target = new File(testOutputDir, 'out-plain')
+ target.deleteDir()
+ assertFalse(target.exists(), "Could not clear test target ${target.absolutePath}")
new GmdProcessor().process(src.absolutePath, target.absolutePath, 'md')
@@ -39,4 +45,19 @@ class GmdProcessorTest extends AbstractGmdTest {
assertTrue(md.contains('# Hi'))
assertTrue(!md.contains('Test
', new File(AbstractGmdTest.testOutputDir, 'failed-process.pdf'))
+ gmd.processHtmlAndSaveAsPdf('Test
', new File(testOutputDir, 'failed-process.pdf'))
}
assertInstanceOf(IllegalStateException, exception.cause)
@@ -66,7 +66,7 @@ class GmdTest extends AbstractGmdTest {
@Test
void gmdToHtmlFile() {
def gmd = new Gmd()
- def htmlfFile = new File(AbstractGmdTest.testOutputDir, "gmdToHtmlFile.pdf")
+ def htmlfFile = new File(testOutputDir, "gmdToHtmlFile.pdf")
if (htmlfFile.exists()) htmlfFile.delete()
gmd.gmdToHtml(text, htmlfFile)
assertEquals(gmd.gmdToHtmlDoc(text), htmlfFile.text)
@@ -79,30 +79,30 @@ class GmdTest extends AbstractGmdTest {
@Test
void gmdToPdf() {
def gmd = new Gmd()
- def pdfFile = new File(AbstractGmdTest.testOutputDir, "gmdToPdf.pdf")
+ def pdfFile = new File(testOutputDir, "gmdToPdf.pdf")
if (pdfFile.exists()) pdfFile.delete()
def html = gmd.gmdToHtmlDoc(text)
gmd.htmlToPdf(html, pdfFile)
assertTrue(pdfFile.exists())
- def pdfFile2 = new File(AbstractGmdTest.testOutputDir, "gmdToPdf2.pdf")
+ def pdfFile2 = new File(testOutputDir, "gmdToPdf2.pdf")
if (pdfFile2.exists()) pdfFile2.delete()
gmd.gmdToPdf(text, pdfFile2)
assertTrue(pdfFile.exists())
// Files might differ with a few bytes
assertEquals((pdfFile.length()/15).intValue(), (pdfFile2.length()/15).intValue())
- def pdfFile3 = new File(AbstractGmdTest.testOutputDir, "gmdToPdf3.pdf")
+ def pdfFile3 = new File(testOutputDir, "gmdToPdf3.pdf")
gmd.gmdToPdf(text, pdfFile3)
assertEquals((pdfFile2.length()/15).intValue(), (pdfFile3.length()/15).intValue())
}
@Test
void toPdfRawIsUndecorated() {
- File source = new File(AbstractGmdTest.testOutputDir, 'rawVsStyled.gmd')
+ File source = new File(testOutputDir, 'rawVsStyled.gmd')
source.text = "# Test\n\n```groovy\ndef a = 1\n```\n"
- File styled = new File(AbstractGmdTest.testOutputDir, 'styled.pdf')
- File raw = new File(AbstractGmdTest.testOutputDir, 'raw.pdf')
+ File styled = new File(testOutputDir, 'styled.pdf')
+ File raw = new File(testOutputDir, 'raw.pdf')
if (styled.exists()) styled.delete()
if (raw.exists()) raw.delete()
@@ -226,12 +226,16 @@ class GmdTest extends AbstractGmdTest {
}
@Test
- void retainsDeprecatedBootstrapCssCompatibilityConstant() {
- def resourceUrl = se.alipsa.gmd.core.HtmlDecorator.class
- .getResource(se.alipsa.gmd.core.HtmlDecorator.BOOTSTRAP_CSS_PATH)
- .toExternalForm()
-
- assertEquals(resourceUrl, se.alipsa.gmd.core.HtmlDecorator.BOOTSTRAP_CSS)
+ void retainsBootstrapCssCompatibilityConstants() {
+ assertTrue(!se.alipsa.gmd.core.HtmlDecorator.BOOTSTRAP_CSS.isEmpty())
+ assertNotEquals(se.alipsa.gmd.core.HtmlDecorator.BOOTSTRAP_CSS_PATH,
+ se.alipsa.gmd.core.HtmlDecorator.BOOTSTRAP_CSS)
+ assertTrue(se.alipsa.gmd.core.HtmlDecorator.BOOTSTRAP_CSS.endsWith('bootstrap.css'))
+ assertTrue(se.alipsa.gmd.core.HtmlDecorator.BOOTSTRAP_CSS.startsWith('file:')
+ || se.alipsa.gmd.core.HtmlDecorator.BOOTSTRAP_CSS.startsWith('jar:'))
+ assertTrue(se.alipsa.gmd.core.HtmlDecorator.HIGHLIGHT_JS_CSS.contains('default.min.css'))
+ assertTrue(se.alipsa.gmd.core.HtmlDecorator.HIGHLIGHT_JS_CSS.contains("href='file:")
+ || se.alipsa.gmd.core.HtmlDecorator.HIGHLIGHT_JS_CSS.contains("href='jar:"))
}
@Test
@@ -281,7 +285,7 @@ class GmdTest extends AbstractGmdTest {
def text = '## Hello `=name`!'
def gmd = new Gmd()
def html = gmd.gmdToHtmlDoc(text, [name: "Per"])
- def pdfFile = new File(AbstractGmdTest.testOutputDir, "gmdToPdfWithParameter.pdf")
+ def pdfFile = new File(testOutputDir, "gmdToPdfWithParameter.pdf")
if (pdfFile.exists()) pdfFile.delete()
gmd.htmlToPdf(html, pdfFile)
assertTrue(pdfFile.exists())
@@ -313,7 +317,7 @@ class GmdTest extends AbstractGmdTest {
assertTrue(html.contains("Some equations
\nX = ∑(√2π + ∛3)
\n"))
- def pdfFile = new File(AbstractGmdTest.testOutputDir, "testPdfWithSpecialCharacters.pdf")
+ def pdfFile = new File(testOutputDir, "testPdfWithSpecialCharacters.pdf")
if (pdfFile.exists()) pdfFile.delete()
gmd.htmlToPdf(html, pdfFile)
assertTrue(pdfFile.exists())
@@ -565,7 +569,7 @@ out.println(chart)
assertTrue(md.contains('# Employees'))
assertTrue(md.contains(")
- def htmlFile = new File(AbstractGmdTest.testOutputDir, "testXChart.html")
+ def htmlFile = new File(testOutputDir, "testXChart.html")
gmd.gmdToHtml(text, htmlFile)
assertTrue(htmlFile.exists())
assertTrue(htmlFile.length() > 100, "No html content")
@@ -577,7 +581,7 @@ out.println(chart)
void testMathmlToPDF() {
def html = IOUtils.toString(this.class.getResource('/mathml.html'), StandardCharsets.UTF_8)
Gmd gmd = new Gmd()
- def pdfFile = new File(AbstractGmdTest.testOutputDir, "testMathmlToPDF.pdf")
+ def pdfFile = new File(testOutputDir, "testMathmlToPDF.pdf")
gmd.htmlToPdf(html, pdfFile)
assertTrue(pdfFile.exists())
println("Wrote $pdfFile.absolutePath")
@@ -587,12 +591,12 @@ out.println(chart)
void gmdCommandLineTest() {
String file = getClass().getResource("/test.gmd").getFile()
- File htmlFile = new File(AbstractGmdTest.testOutputDir,"gmdCommandLineTest.html")
+ File htmlFile = new File(testOutputDir,"gmdCommandLineTest.html")
Gmd.main("toHtml", file, htmlFile.absolutePath)
assertTrue(htmlFile.exists())
assertTrue(htmlFile.length() > 100, "No html content")
- File pdfFile = new File(AbstractGmdTest.testOutputDir, "gmdCommandLineTest.pdf")
+ File pdfFile = new File(testOutputDir, "gmdCommandLineTest.pdf")
Gmd.main("toPdf", file, pdfFile.absolutePath)
assertTrue(pdfFile.exists())
assertTrue(pdfFile.length() > 100, "No pdf content")
diff --git a/gmd-gradle-plugin/build.gradle b/gmd-gradle-plugin/build.gradle
index 56ebb86..7d571ba 100644
--- a/gmd-gradle-plugin/build.gradle
+++ b/gmd-gradle-plugin/build.gradle
@@ -7,13 +7,58 @@ plugins {
id 'signing'
id 'maven-publish'
id 'com.gradle.plugin-publish' version '2.1.1'
- id "io.github.ben-manes.versions" version "0.58.0"
+ id "io.github.ben-manes.versions" version "0.61.0"
}
group = 'se.alipsa.gmd'
-version = project.findProperty("publish.version") ?: "unspecified"
+def publishVersion = project.findProperty("publish.version")
+def revision = null
+if (!publishVersion) {
+ revision = providers.fileContents(layout.projectDirectory.file('../pom.xml')).asText.orElse('').map { pomText ->
+ if (!pomText) {
+ throw new GradleException('Could not read the project revision from ../pom.xml')
+ }
+ try {
+ def documentBuilderFactory = javax.xml.parsers.DocumentBuilderFactory.newInstance()
+ documentBuilderFactory.setNamespaceAware(true)
+ documentBuilderFactory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true)
+ documentBuilderFactory.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true)
+ documentBuilderFactory.setFeature('http://apache.org/xml/features/nonvalidating/load-external-dtd', false)
+ documentBuilderFactory.setFeature('http://xml.org/sax/features/external-general-entities', false)
+ documentBuilderFactory.setFeature('http://xml.org/sax/features/external-parameter-entities', false)
+ documentBuilderFactory.setAttribute(javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD, '')
+ documentBuilderFactory.setAttribute(javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA, '')
+ documentBuilderFactory.setXIncludeAware(false)
+ documentBuilderFactory.setExpandEntityReferences(false)
+ def document = documentBuilderFactory.newDocumentBuilder().parse(
+ new org.xml.sax.InputSource(new java.io.StringReader(pomText))
+ )
+ String value = javax.xml.xpath.XPathFactory.newInstance().newXPath().evaluate(
+ "/*[local-name()='project']/*[local-name()='properties']/*[local-name()='revision']",
+ document
+ )?.trim()
+ if (!value) {
+ throw new GradleException('Could not read the project revision from ../pom.xml')
+ }
+ value
+ } catch (GradleException e) {
+ throw e
+ } catch (Exception e) {
+ throw new GradleException('Could not read the project revision from ../pom.xml', e)
+ }
+ }.get()
+}
+version = publishVersion ?: revision
+def pluginVersion = version.toString()
description = 'Compiles gmd files into markdown, html or pdf'
+processResources {
+ inputs.property('gmdVersion', pluginVersion)
+ filesMatching('gmd-version.properties') {
+ expand(gmdVersion: pluginVersion)
+ }
+}
+
gradlePlugin {
website.set("https://github.com/Alipsa/GroovyMarkdown")
vcsUrl.set("https://github.com/Alipsa/GroovyMarkdown.git")
@@ -38,12 +83,15 @@ repositories {
}
dependencies {
- testImplementation platform('org.junit:junit-bom:6.1.2')
+ testImplementation platform('org.junit:junit-bom:6.1.3')
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.junit.platform:junit-platform-launcher'
}
test {
+ systemProperty('gmd.plugin.version', pluginVersion)
+ systemProperty('gmd.publish.version', publishVersion ?: '')
+ systemProperty('gmd.root.pom', layout.projectDirectory.file('../pom.xml').asFile.absolutePath)
useJUnitPlatform()
testLogging {
// set options for log level LIFECYCLE
@@ -62,6 +110,13 @@ test {
}
project.afterEvaluate {
+ tasks.matching { it.name == 'publishPlugins' }.configureEach {
+ doFirst {
+ if (!publishVersion) {
+ throw new GradleException('publishPlugins requires -Ppublish.version=')
+ }
+ }
+ }
tasks.named('signPluginMavenPublication') {
enabled = project.findProperty('signing.keyId') != null
}
diff --git a/gmd-gradle-plugin/readme.md b/gmd-gradle-plugin/readme.md
index 9ea46ae..9decabd 100644
--- a/gmd-gradle-plugin/readme.md
+++ b/gmd-gradle-plugin/readme.md
@@ -24,7 +24,7 @@ Possible parameters are:
`outputType=html` writes a complete decorated HTML document, including the doctype,
embedded styles, and syntax-highlighted code blocks.
- `groovyVersion` - the version of Groovy to use. Default is `5.0.8`
-- `gmdVersion` - the version of GMD to use. Default is `3.1.0`
+- `gmdVersion` - the version of GMD to use. Defaults to the Gradle plugin version.
- `log4jVersion` - the version of log4j to use. Default is `2.26.1`
- `ivyVersion` - the version of ivy to use. Default is `2.6.0`
- `runTaskBefore` - the task that the gmd plugin should run before. Default is 'test'
diff --git a/gmd-gradle-plugin/release.md b/gmd-gradle-plugin/release.md
index 052c929..ed4c0c3 100644
--- a/gmd-gradle-plugin/release.md
+++ b/gmd-gradle-plugin/release.md
@@ -1,5 +1,12 @@
# GMD Gradle Plugin release history
+## v3.2.0, unreleased
+- derive the default GMD core version from a generated plugin-version resource populated from the root Maven revision, failing clearly if metadata is unavailable
+- derive the development artifact version from the root Maven revision and require an explicit version when publishing
+- upgrade the Gradle Versions Plugin from 0.58.0 to 0.61.0
+- upgrade the JUnit BOM from 6.1.2 to 6.1.3
+- validate the Gradle Plugin Portal publication before uploading the plugin
+
## v3.1.1, in progress
- replace the deprecated `Project.getProperties()` calls used by signing configuration with `findProperty`, keeping the plugin compatible with Gradle 10
- make `processGmd` compatible with the Gradle configuration cache and parallel execution
diff --git a/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/GmdGradlePlugin.groovy b/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/GmdGradlePlugin.groovy
index 8f50b67..e849861 100644
--- a/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/GmdGradlePlugin.groovy
+++ b/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/GmdGradlePlugin.groovy
@@ -10,6 +10,10 @@ import org.gradle.api.artifacts.repositories.ArtifactRepository
import org.gradle.api.artifacts.repositories.MavenArtifactRepository
import org.gradle.api.tasks.TaskProvider
+import java.io.IOException
+import java.io.InputStream
+import java.util.Properties
+
@CompileStatic
class GmdGradlePlugin implements Plugin {
@@ -21,7 +25,7 @@ class GmdGradlePlugin implements Plugin {
extension.outputType.convention('md')
extension.groovyVersion.convention('5.0.8')
extension.log4jVersion.convention('2.26.1')
- extension.gmdVersion.convention('3.1.0')
+ extension.gmdVersion.convention(project.providers.provider { defaultGmdVersion() })
extension.ivyVersion.convention('2.6.0')
extension.runTaskBefore.convention('test')
@@ -59,6 +63,36 @@ class GmdGradlePlugin implements Plugin {
}
}
+ private static String defaultGmdVersion() {
+ InputStream stream = GmdGradlePlugin.class.getResourceAsStream('/gmd-version.properties')
+ if (stream == null) {
+ throw new IllegalStateException(
+ 'GMD core version metadata is missing from the Gradle plugin; set gmdPlugin.gmdVersion explicitly'
+ )
+ }
+ try {
+ Properties properties = new Properties()
+ properties.load(stream)
+ String version = properties.getProperty('gmd.version')
+ String normalizedVersion = version == null ? null : version.trim()
+ if (normalizedVersion == null || normalizedVersion.isEmpty()
+ || normalizedVersion.contains('$') || normalizedVersion.contains('{')) {
+ throw new IllegalStateException(
+ 'GMD core version metadata is invalid; set gmdPlugin.gmdVersion explicitly'
+ )
+ }
+ return normalizedVersion
+ } catch (IOException e) {
+ throw new IllegalStateException('Could not read GMD core version metadata', e)
+ } finally {
+ try {
+ stream.close()
+ } catch (IOException ignored) {
+ // Ignore cleanup failures while resolving the version resource.
+ }
+ }
+ }
+
static Configuration addDependencies(Project project,
String groovyVersion, String log4jVersion, String gmdVersion,
String ivyVersion) {
diff --git a/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/GmdGradlePluginParams.groovy b/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/GmdGradlePluginParams.groovy
index 1c98700..fcc2797 100644
--- a/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/GmdGradlePluginParams.groovy
+++ b/gmd-gradle-plugin/src/main/groovy/se/alipsa/gmd/gradle/GmdGradlePluginParams.groovy
@@ -36,7 +36,7 @@ interface GmdGradlePluginParams {
Property getLog4jVersion()
/**
- * The version of GMD to use. Default is 3.1.0
+ * The version of GMD to use. Defaults to the Gradle plugin version.
*/
Property getGmdVersion()
diff --git a/gmd-gradle-plugin/src/main/resources/gmd-version.properties b/gmd-gradle-plugin/src/main/resources/gmd-version.properties
new file mode 100644
index 0000000..fe31744
--- /dev/null
+++ b/gmd-gradle-plugin/src/main/resources/gmd-version.properties
@@ -0,0 +1 @@
+gmd.version=${gmdVersion}
diff --git a/gmd-gradle-plugin/src/test/groovy/test/alipsa/gmd/gradle/GmdGradlePluginTest.groovy b/gmd-gradle-plugin/src/test/groovy/test/alipsa/gmd/gradle/GmdGradlePluginTest.groovy
index 10df153..9263f62 100644
--- a/gmd-gradle-plugin/src/test/groovy/test/alipsa/gmd/gradle/GmdGradlePluginTest.groovy
+++ b/gmd-gradle-plugin/src/test/groovy/test/alipsa/gmd/gradle/GmdGradlePluginTest.groovy
@@ -4,10 +4,69 @@ import groovy.ant.AntBuilder
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.gradle.testkit.runner.GradleRunner
+import se.alipsa.gmd.gradle.GmdGradlePlugin
+
+import java.util.Properties
+
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS
class GmdGradlePluginTest {
+ private static String rootPomRevision() {
+ def factory = javax.xml.parsers.DocumentBuilderFactory.newInstance()
+ factory.setNamespaceAware(true)
+ factory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true)
+ factory.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true)
+ factory.setFeature('http://xml.org/sax/features/external-general-entities', false)
+ factory.setFeature('http://xml.org/sax/features/external-parameter-entities', false)
+ factory.setAttribute(javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD, '')
+ factory.setAttribute(javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA, '')
+ factory.setXIncludeAware(false)
+ factory.setExpandEntityReferences(false)
+ String rootPomPath = System.getProperty('gmd.root.pom')
+ Assertions.assertNotNull(rootPomPath, 'The root POM path must be provided by the Gradle test task')
+ def document = factory.newDocumentBuilder().parse(new File(rootPomPath))
+ def project = document.documentElement
+ def properties = directElementChild(project, 'properties')
+ def revision = directElementChild(properties, 'revision')
+ Assertions.assertNotNull(revision, 'The root POM must define project/properties/revision')
+ revision.getTextContent().trim()
+ }
+
+ private static org.w3c.dom.Node directElementChild(org.w3c.dom.Node parent, String localName) {
+ Assertions.assertNotNull(parent, "Expected a parent element containing direct $localName")
+ for (int i = 0; i < parent.getChildNodes().getLength(); i++) {
+ def child = parent.getChildNodes().item(i)
+ if (child.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE && child.getLocalName() == localName) {
+ return child
+ }
+ }
+ return null
+ }
+
+ @Test
+ void defaultGmdVersionComesFromGeneratedResource() {
+ URL resource = GmdGradlePlugin.class.getResource('/gmd-version.properties')
+ Assertions.assertNotNull(resource, 'The plugin version resource must be generated during processResources')
+
+ Properties properties = new Properties()
+ resource.withInputStream { properties.load(it) }
+ String resourceVersion = properties.getProperty('gmd.version')
+ String expectedVersion = System.getProperty('gmd.plugin.version')
+ String publishVersion = System.getProperty('gmd.publish.version')
+ Assertions.assertNotNull(expectedVersion)
+ Assertions.assertNotNull(resourceVersion)
+ if (!publishVersion) {
+ Assertions.assertEquals(rootPomRevision(), expectedVersion)
+ }
+ Assertions.assertEquals(expectedVersion, resourceVersion)
+ Assertions.assertFalse(resourceVersion.contains('$'), "The generated resource must be expanded: $resourceVersion")
+
+ def method = GmdGradlePlugin.class.getDeclaredMethod('defaultGmdVersion')
+ method.setAccessible(true)
+ Assertions.assertEquals(resourceVersion, method.invoke(null))
+ }
+
@Test
void testPlugin() {
File targetDir = null
@@ -55,13 +114,13 @@ class GmdGradlePluginTest {
group = 'test.alipsa.gmd'
version = '1.0.0-SNAPSHOT'
repositories {
- // Enable us to to use local snapshots
- mavenLocal()
+ mavenCentral()
}
gmdPlugin {
sourceDir = 'src/test/gmd'
targetDir = 'build/target'
outputType = 'html'
+ gmdVersion = '3.1.0' // Keep the standalone TestKit test independent of unpublished snapshots.
runTaskBefore = 'build' // we dont have tests so specify the task to not get a warning
}
""".stripIndent()
@@ -70,7 +129,7 @@ class GmdGradlePluginTest {
settingsFile.text = """
pluginManagement {
repositories {
- mavenLocal()
+ mavenCentral()
}
plugins {
id 'se.alipsa.gmd.gmd-gradle-plugin' version "1.0.0"
diff --git a/gmd-gradle-plugin/src/test/manual-test/build.gradle b/gmd-gradle-plugin/src/test/manual-test/build.gradle
index 1553ad4..92313bd 100644
--- a/gmd-gradle-plugin/src/test/manual-test/build.gradle
+++ b/gmd-gradle-plugin/src/test/manual-test/build.gradle
@@ -12,7 +12,7 @@ repositories {
dependencies {
implementation 'org.apache.groovy:groovy:5.0.8'
- testImplementation(platform('org.junit:junit-bom:6.1.2'))
+ testImplementation(platform('org.junit:junit-bom:6.1.3'))
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
@@ -28,4 +28,4 @@ test {
testLogging {
showStandardStreams = true
}
-}
\ No newline at end of file
+}
diff --git a/gmd-maven-plugin/pom.xml b/gmd-maven-plugin/pom.xml
index 9402322..ece36a3 100644
--- a/gmd-maven-plugin/pom.xml
+++ b/gmd-maven-plugin/pom.xml
@@ -157,6 +157,15 @@
org.apache.maven.plugins
maven-compiler-plugin
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ ${project.version}
+
+
+
org.apache.maven.plugins
maven-plugin-plugin
diff --git a/gmd-maven-plugin/readme.md b/gmd-maven-plugin/readme.md
index f697f1f..dc83dfc 100644
--- a/gmd-maven-plugin/readme.md
+++ b/gmd-maven-plugin/readme.md
@@ -34,7 +34,7 @@ Possible configuration parameters are:
embedded styles, and syntax-highlighted code blocks.
- `groovyVersion` - the version of Groovy to use. Default is `5.0.8`
- `log4jVersion` - the version of Log4j to use. Default is `2.26.1`
-- `gmdVersion` - the version of GMD core to use. Default is `3.1.0`
+- `gmdVersion` - the version of GMD core to use. Defaults to the plugin version.
- `ivyVersion` - the version of Ivy to use. Default is `2.6.0`
If you don't want to run the plugin explicitly, you can add it to an existing lifecycle as follows:
diff --git a/gmd-maven-plugin/release.md b/gmd-maven-plugin/release.md
index 909e17f..410abe6 100644
--- a/gmd-maven-plugin/release.md
+++ b/gmd-maven-plugin/release.md
@@ -1,5 +1,11 @@
# GMD Maven Plugin release history
+## v3.2.0, unreleased
+- make the default GMD core version follow the Maven plugin version to prevent release drift
+- validate Maven Central release artifacts before publishing
+- publish source and Javadoc artifacts as part of the release workflow
+- replace deprecated Maven `@Component` injection with JSR-330 `@Inject`
+
## v3.1.0, 2026-08-02
- resolve all output types without JavaFX dependencies
- resolve relative source and target paths from the Maven project base directory
diff --git a/gmd-maven-plugin/src/main/java/se/alipsa/gmd/maven/GmdMavenPlugin.java b/gmd-maven-plugin/src/main/java/se/alipsa/gmd/maven/GmdMavenPlugin.java
index 0ef5853..bdf66d9 100644
--- a/gmd-maven-plugin/src/main/java/se/alipsa/gmd/maven/GmdMavenPlugin.java
+++ b/gmd-maven-plugin/src/main/java/se/alipsa/gmd/maven/GmdMavenPlugin.java
@@ -4,11 +4,15 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
+import javax.inject.Inject;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
-import org.apache.maven.plugins.annotations.*;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
@@ -40,7 +44,7 @@ public class GmdMavenPlugin extends AbstractMojo {
@Parameter(name = "log4jVersion", property = "processGmd.log4jVersion", defaultValue = "2.26.1")
private String log4jVersion;
- @Parameter(name = "gmdVersion", property = "processGmd.gmdVersion", defaultValue = "3.1.0")
+ @Parameter(name = "gmdVersion", property = "processGmd.gmdVersion", defaultValue = "${plugin.version}")
private String gmdVersion;
@Parameter(name = "ivyVersion", property = "processGmd.ivyVersion", defaultValue = "2.6.0")
@@ -52,7 +56,7 @@ public class GmdMavenPlugin extends AbstractMojo {
@Parameter(defaultValue = "${session}", readonly = true, required = true)
private MavenSession session;
- @Component
+ @Inject
private RepositorySystem repositorySystem;
/**
@@ -111,7 +115,7 @@ public String getLog4jVersion() {
}
/**
- * The version of GMD core to use. Default is 3.1.0
+ * The version of GMD core to use. Defaults to the plugin version.
*
* @return The version of GMD core to use.
*/
@@ -174,7 +178,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
command.add(getJavaExecutable());
command.add("-cp");
command.add(classpath.toString());
- command.add("se.alipsa.gmd.core.GmdProcessor");
+ command.add(getGmdProcessorClassName());
command.add(srcDir.getCanonicalPath());
command.add(outputDirectory.getCanonicalPath());
command.add(normalizedOutputType);
@@ -258,4 +262,13 @@ private String getJavaExecutable() {
String javaHome = System.getProperty("java.home");
return javaHome + File.separator + "bin" + File.separator + "java";
}
+
+ /**
+ * Returns the entry point used by the forked GMD process.
+ *
+ * @return the fully qualified GmdProcessor class name
+ */
+ protected String getGmdProcessorClassName() {
+ return se.alipsa.gmd.core.GmdProcessor.class.getName();
+ }
}
diff --git a/gmd-maven-plugin/src/test/java/test/alipsa/gmd/maven/GmdMavenPluginTest.java b/gmd-maven-plugin/src/test/java/test/alipsa/gmd/maven/GmdMavenPluginTest.java
index 3030e5b..799d5ad 100644
--- a/gmd-maven-plugin/src/test/java/test/alipsa/gmd/maven/GmdMavenPluginTest.java
+++ b/gmd-maven-plugin/src/test/java/test/alipsa/gmd/maven/GmdMavenPluginTest.java
@@ -2,13 +2,38 @@
import org.apache.maven.api.plugin.testing.InjectMojo;
import org.apache.maven.api.plugin.testing.MojoTest;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.project.MavenProject;
import org.junit.jupiter.api.Test;
import se.alipsa.gmd.maven.GmdMavenPlugin;
import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.lang.reflect.Field;
import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.resolution.DependencyRequest;
+import org.eclipse.aether.resolution.DependencyResult;
+import org.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
@MojoTest
public class GmdMavenPluginTest {
@@ -18,6 +43,10 @@ public class GmdMavenPluginTest {
public void testGmdMavenPlugin(GmdMavenPlugin plugin) throws Exception {
File pomFile = new File("src/test/projects/");
assertTrue(pomFile.exists());
+ String expectedVersion = System.getProperty("gmd.plugin.version");
+ assertNotNull(expectedVersion);
+ assertNotNull(plugin.getGmdVersion());
+ assertEquals(expectedVersion, plugin.getGmdVersion());
// Execute the plugin
plugin.execute();
@@ -41,4 +70,111 @@ public void testGmdMavenPlugin(GmdMavenPlugin plugin) throws Exception {
assertTrue(testInlineHtml.contains(" and the time is "));
}
+ @Test
+ @InjectMojo(goal = "processGmd", pom = "src/test/projects/pom.xml")
+ public void testGmdMavenPluginWithResolvedDependencies(GmdMavenPlugin plugin) throws Exception {
+ RepositorySystem repositorySystem = Mockito.mock(RepositorySystem.class);
+ RepositorySystemSession repositorySession = Mockito.mock(RepositorySystemSession.class);
+ LocalRepositoryManager localRepositoryManager = Mockito.mock(LocalRepositoryManager.class);
+ MavenSession session = Mockito.mock(MavenSession.class);
+ DependencyResult dependencyResult = new DependencyResult(new DependencyRequest());
+
+ when(session.getRepositorySession()).thenReturn(repositorySession);
+ when(repositorySession.getLocalRepositoryManager()).thenReturn(localRepositoryManager);
+ when(repositorySystem.resolveDependencies(any(RepositorySystemSession.class), any(DependencyRequest.class)))
+ .thenReturn(dependencyResult);
+
+ List artifactResults = Arrays.stream(System.getProperty("java.class.path")
+ .split(java.util.regex.Pattern.quote(File.pathSeparator)))
+ .map(path -> {
+ Artifact artifact = Mockito.mock(Artifact.class);
+ when(artifact.getFile()).thenReturn(new File(path));
+ return new ArtifactResult(new ArtifactRequest()).setArtifact(artifact);
+ })
+ .toList();
+ dependencyResult.setArtifactResults(artifactResults);
+
+ setField(plugin, "repositorySystem", repositorySystem);
+ setField(plugin, "session", session);
+ setField(plugin, "targetDir", "target/gmd-resolved");
+ File outputDirectory = new File(plugin.getTargetDir());
+ deleteDirectory(outputDirectory);
+ assertFalse(outputDirectory.exists(), "Could not clear resolved-dependency test output");
+
+ plugin.execute();
+
+ File testHtml = new File(plugin.getTargetDir(), "test.html");
+ assertTrue(testHtml.isFile(), "The resolved-dependency fork did not write test.html");
+ assertTrue(Files.readString(testHtml.toPath()).contains("Greetings
"));
+ Mockito.verify(repositorySystem).resolveDependencies(any(RepositorySystemSession.class), any(DependencyRequest.class));
+ }
+
+ @Test
+ public void testGmdMavenPluginReportsForkFailure() throws Exception {
+ FailingGmdMavenPlugin plugin = new FailingGmdMavenPlugin();
+ RepositorySystem repositorySystem = Mockito.mock(RepositorySystem.class);
+ RepositorySystemSession repositorySession = Mockito.mock(RepositorySystemSession.class);
+ LocalRepositoryManager localRepositoryManager = Mockito.mock(LocalRepositoryManager.class);
+ MavenSession session = Mockito.mock(MavenSession.class);
+ MavenProject project = Mockito.mock(MavenProject.class);
+ DependencyResult dependencyResult = new DependencyResult(new DependencyRequest());
+
+ when(session.getRepositorySession()).thenReturn(repositorySession);
+ when(repositorySession.getLocalRepositoryManager()).thenReturn(localRepositoryManager);
+ when(repositorySystem.resolveDependencies(any(RepositorySystemSession.class), any(DependencyRequest.class)))
+ .thenReturn(dependencyResult);
+ when(project.getBasedir()).thenReturn(new File("src/test/projects/manual").getAbsoluteFile());
+
+ setField(plugin, "repositorySystem", repositorySystem);
+ setField(plugin, "session", session);
+ setField(plugin, "project", project);
+ setField(plugin, "sourceDir", "src/test/gmd");
+ setField(plugin, "targetDir", "target/gmd-bad-main");
+ setField(plugin, "outputType", "html");
+
+ MojoFailureException exception = assertThrows(MojoFailureException.class, plugin::execute);
+ assertTrue(exception.getMessage().contains("GmdProcessor exited with code"));
+ }
+
+ private static void deleteDirectory(File directory) throws IOException {
+ if (!directory.exists()) {
+ return;
+ }
+ try (var paths = Files.walk(directory.toPath())) {
+ try {
+ paths.sorted(Comparator.reverseOrder()).forEach(path -> {
+ try {
+ Files.deleteIfExists(path);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
+ } catch (UncheckedIOException e) {
+ throw e.getCause();
+ }
+ }
+ }
+
+ private static void setField(Object target, String name, Object value) throws Exception {
+ Class> type = target.getClass();
+ while (type != null) {
+ try {
+ Field field = type.getDeclaredField(name);
+ field.setAccessible(true);
+ field.set(target, value);
+ return;
+ } catch (NoSuchFieldException e) {
+ type = type.getSuperclass();
+ }
+ }
+ throw new NoSuchFieldException(name);
+ }
+
+ private static final class FailingGmdMavenPlugin extends GmdMavenPlugin {
+ @Override
+ protected String getGmdProcessorClassName() {
+ return "se.alipsa.gmd.core.MissingGmdProcessor";
+ }
+ }
+
}
diff --git a/gmd-maven-plugin/src/test/projects/manual/pom.xml b/gmd-maven-plugin/src/test/projects/manual/pom.xml
index 12047d4..86c885d 100644
--- a/gmd-maven-plugin/src/test/projects/manual/pom.xml
+++ b/gmd-maven-plugin/src/test/projects/manual/pom.xml
@@ -17,7 +17,7 @@
org.junit
junit-bom
- 6.1.2
+ 6.1.3
pom
import
diff --git a/highlightjs-jvm/pom.xml b/highlightjs-jvm/pom.xml
index 4006ba5..7cd6333 100644
--- a/highlightjs-jvm/pom.xml
+++ b/highlightjs-jvm/pom.xml
@@ -98,6 +98,13 @@
+
+ org.codehaus.mojo
+ versions-maven-plugin
+
+ file://${project.parent.basedir}/version-plugin-rules.xml
+
+
diff --git a/highlightjs-jvm/release.md b/highlightjs-jvm/release.md
index 4a27deb..4933ce2 100644
--- a/highlightjs-jvm/release.md
+++ b/highlightjs-jvm/release.md
@@ -1,5 +1,13 @@
# Highlight.js JVM release history
+## v3.2.0, unreleased
+- upgrade the JUnit BOM used by module tests to 6.1.3
+- require Node 22 for bundle generation and verify the generated bundle in CI
+- add Maven publication metadata and attach source and Javadoc artifacts
+- configure the versions Maven plugin to use the shared repository version rules
+- add Javadocs to the public highlighter APIs
+- upgrade the Babel build dependencies
+
## v3.1.0, 2026-08-02
- add the independent `highlightjs-jvm` module
- expose Highlight.js string highlighting through the `SyntaxHighlighter` API
diff --git a/pom.xml b/pom.xml
index 5c7aebe..e552f62 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
- 3.1.1-SNAPSHOT
+ 3.2.0-SNAPSHOT
UTF-8
21
@@ -31,7 +31,7 @@
5.0.8
1.23.1
- 6.1.2
+ 6.1.3
2.26.1