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

\n

X = ∑(√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("![''](data:image/svg+xml;base64,")) - 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