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
11 changes: 11 additions & 0 deletions gui/release.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# MarkdownToPdf GUI Release History
(Note, dates are in yyyy-MM-dd format)

## 0.2.0 (2026-08-14)
- Added a pluggable file-access layer for sandboxed distributions. Stored projects are retained
when their files are temporarily inaccessible, and the application can ask to locate an
inaccessible project Markdown file and remember the new location.
- App-store distributions can disable GitHub update checks with
`-Dmd2pdf.update.enabled=false`; this also hides the related **Help** menu items.
- **View external** now lets sandboxed distributions choose where to save the rendered PDF before
opening it in the system viewer. PDF export and file dialogs also handle inaccessible or stale
initial directories more safely.
- CSS style parsing and syntax-highlight injection now behave consistently in every system locale.

## 0.1.1 (2026-08-11)
- Added an update check: **Help > Check for Updates…** asks GitHub Releases whether a newer
version ships an archive for this platform, and offers to open the release page. It also
Expand Down
12 changes: 11 additions & 1 deletion gui/src/main/java/se/alipsa/md2pdf/gui/HtmlUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,23 @@ private HtmlUtils() {}
*/
static String injectSyntaxHighlighting(String html) {
if (html == null) return "";
int idx = html.toLowerCase().lastIndexOf("</head>");
int idx = lastHeadEndTagStart(html);
if (idx >= 0) {
return html.substring(0, idx) + HIGHLIGHT_INJECTION + html.substring(idx);
}
return HIGHLIGHT_INJECTION + html;
}

private static int lastHeadEndTagStart(String html) {
String closingHead = "</head>";
for (int i = html.length() - closingHead.length(); i >= 0; i--) {
if (html.regionMatches(true, i, closingHead, 0, closingHead.length())) {
return i;
}
}
return -1;
}

private static String loadResource(String path) {
try (InputStream is = HtmlUtils.class.getResourceAsStream(path)) {
if (is == null) return "";
Expand Down
5 changes: 3 additions & 2 deletions gui/src/main/java/se/alipsa/md2pdf/model/StyleProfile.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;

Expand Down Expand Up @@ -212,7 +213,7 @@ private static List<String[]> extractBlocks(String css) {
}

private static String normalizeSelector(String sel) {
return sel.replaceAll("\\s+", " ").trim().toLowerCase();
return sel.replaceAll("\\s+", " ").trim().toLowerCase(Locale.ROOT);
}

private static Map<String, String> parseDeclarations(String decls) {
Expand All @@ -222,7 +223,7 @@ private static Map<String, String> parseDeclarations(String decls) {
if (decl.isEmpty()) continue;
int colon = decl.indexOf(':');
if (colon < 0) continue;
String prop = decl.substring(0, colon).trim().toLowerCase();
String prop = decl.substring(0, colon).trim().toLowerCase(Locale.ROOT);
String value = decl.substring(colon + 1).trim();
map.put(prop, value);
}
Expand Down
25 changes: 25 additions & 0 deletions gui/src/test/java/se/alipsa/md2pdf/gui/HtmlUtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package se.alipsa.md2pdf.gui;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Locale;
import org.junit.jupiter.api.Test;

class HtmlUtilsTest {

@Test
void syntaxHighlightingPreservesHeadWithTurkishCapitalI() {
Locale originalLocale = Locale.getDefault();
try {
Locale.setDefault(Locale.US);
String html = "<html><head><style>font-family: 'İstanbul Sans';</style></head><body/></html>";

String highlighted = HtmlUtils.injectSyntaxHighlighting(html);

assertTrue(highlighted.contains("</head>"));
assertTrue(highlighted.indexOf("<script>") < highlighted.indexOf("</head>"));
} finally {
Locale.setDefault(originalLocale);
}
}
}
14 changes: 14 additions & 0 deletions gui/src/test/java/test/alipsa/md2pdf/gui/StyleProfileTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.junit.jupiter.api.Assertions.*;

import java.util.Locale;
import java.util.Properties;
import org.junit.jupiter.api.Test;
import se.alipsa.md2pdf.model.StyleProfile;
Expand Down Expand Up @@ -158,6 +159,19 @@ void partialCssMapsCorrectly() {
assertEquals("#000000", p.getHeadingColor());
}

@Test
void cssPropertyNamesAreLocaleIndependent() {
Locale originalLocale = Locale.getDefault();
try {
Locale.setDefault(Locale.forLanguageTag("tr-TR"));
StyleProfile p = StyleProfile.fromCss("BODY { LINE-HEIGHT: 1.5; }");

assertEquals(1.5, p.getLineHeight(), 0.001);
} finally {
Locale.setDefault(originalLocale);
}
}

@Test
void unknownSelectorsPreservedInExtraCss() {
String css =
Expand Down
19 changes: 19 additions & 0 deletions lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,25 @@ String html = job.toHtml(); // string
job.toHtml(Path.of("out.html")); // file
```

Where the destination directory permits it, file output stages the rendered PDF in a temporary
sibling before replacing the destination, so a rendering or staging failure cannot overwrite an
existing file. The replacement is atomic when the filesystem supports atomic moves. A regular
destination that cannot support sibling staging is rendered in memory before it is written; a
failure while writing that fallback can still leave a partially written destination.

## Logging

By default, constructing the first engine replaces any OpenHTMLtoPDF JDK logger with the bundled
SLF4J bridge. This includes a JDK logger installed by OpenHTMLtoPDF's `XRLog` tuning methods. A
non-JDK logger implementation is preserved.

To explicitly replace OpenHTMLtoPDF's JVM-global logger with the bundled SLF4J bridge during
application startup:

```java
Md2PdfEngine.configureOpenHtmlToPdfLogging();
```

## Styling

Use `css(...)` to **replace** the default stylesheet entirely:
Expand Down
9 changes: 8 additions & 1 deletion lib/release.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# MarkdownToPdf Runtime Release History
(Note, dates are in yyyy-MM-dd format)

## 0.2.0 (2026-08-14)
- PDF file output stages regular filesystem destinations before replacing them, so a rendering or
staging failure preserves an existing file where sibling staging is available.
- Engine construction retains the bundled SLF4J bridge when OpenHTMLtoPDF uses a JDK logger,
including one installed by its `XRLog` tuning methods, while preserving a non-JDK implementation.
- Image data-URL type detection now handles uppercase file extensions consistently across locales.

## 0.1.1 (2026-08-11)
- **Breaking:** renamed `Md2PdfEngine.Job` to `Md2PdfEngine.Renderer`. Code that named the
type explicitly must be updated; the fluent `engine.markdown(...)` chain is unchanged.
Expand All @@ -10,4 +17,4 @@
- jsoup 1.22.2 -> 1.23.1

## 0.1.0 (2026-05-17)
- Initial release
- Initial release
3 changes: 2 additions & 1 deletion lib/src/main/java/se/alipsa/md2pdf/ImageUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Locale;

/** Image utilities */
public class ImageUtil {
Expand Down Expand Up @@ -127,7 +128,7 @@ private static InputStream locateResource(String resource, Class<?> clazz)
}

private static String getMediaType(String resource) {
String res = resource.toLowerCase();
String res = resource.toLowerCase(Locale.ROOT);
if (res.endsWith("png")) {
return "image/png";
}
Expand Down
Loading