Skip to content
Merged
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
18 changes: 10 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -63,12 +63,14 @@ java -jar gmd-core/target/gmd-3.1.0.jar toPdfRaw input.gmd output.pdf
```xml
<!-- In /pom.xml -->
<properties>
<revision>3.1.0</revision>
<revision>3.2.0-SNAPSHOT</revision>
</properties>
```

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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -199,7 +201,7 @@ Requires:
### Gradle Plugin Portal (gmd-gradle-plugin)
```bash
cd gmd-gradle-plugin
./gradlew publishPlugins
./gradlew publishPlugins -Ppublish.version=<release-version>
```

### GitHub Releases
Expand Down
6 changes: 3 additions & 3 deletions gmd-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>21</maven.compiler.release>

<openhtmltopdf.version>1.1.65</openhtmltopdf.version>
<commonmark.version>0.29.0</commonmark.version>
<junit.version>6.1.2</junit.version>
<openhtmltopdf.version>1.1.70</openhtmltopdf.version>
<commonmark.version>0.30.0</commonmark.version>
<junit.version>6.1.3</junit.version>
<!-- paths for groovydoc -> javadoc jar -->
<groovydoc.output>${project.build.directory}/site/groovydoc</groovydoc.output>
<main.class>se.alipsa.gmd.core.Gmd</main.class>
Expand Down
7 changes: 7 additions & 0 deletions gmd-core/release.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<!DOCTYPE html>\n"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')

Expand All @@ -31,12 +35,29 @@ 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')

String md = new File(target, 'plain.md').text
assertTrue(md.contains('# Hi'))
assertTrue(!md.contains('<!DOCTYPE'), 'Markdown output must not be decorated')
}

@Test
void commandLineEntryPointAcceptsSourceTargetAndOutputTypeArguments() {
File src = sourceDirWith('command-line', "# Hi\n")
File target = new File(testOutputDir, 'out-command-line')
target.deleteDir()
assertFalse(target.exists(), "Could not clear test target ${target.absolutePath}")

assertEquals('se.alipsa.gmd.core.GmdProcessor', GmdProcessor.name)
GmdProcessor.main([src.absolutePath, target.absolutePath, 'md'] as String[])

File output = new File(target, 'command-line.md')
assertTrue(output.isFile(), "Expected command-line output at ${output.absolutePath}")
assertTrue(output.text.contains('# Hi'))
}
}
46 changes: 25 additions & 21 deletions gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ class GmdTest extends AbstractGmdTest {
}

def exception = assertThrows(GmdException) {
gmd.gmdToPdf('# Test', new File(AbstractGmdTest.testOutputDir, 'failed.pdf'))
gmd.gmdToPdf('# Test', new File(testOutputDir, 'failed.pdf'))
}
def processException = assertThrows(GmdException) {
gmd.processHtmlAndSaveAsPdf('<p>Test</p>', new File(AbstractGmdTest.testOutputDir, 'failed-process.pdf'))
gmd.processHtmlAndSaveAsPdf('<p>Test</p>', new File(testOutputDir, 'failed-process.pdf'))
}

assertInstanceOf(IllegalStateException, exception.cause)
Expand All @@ -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)
Expand All @@ -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()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -313,7 +317,7 @@ class GmdTest extends AbstractGmdTest {

assertTrue(html.contains("<h1>Some equations</h1>\n<p>X = ∑(√2π + ∛3)</p>\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())
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand Down
Loading
Loading