From 3daaec52c19c9094820b0d19a7b855bd64501a44 Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Wed, 25 Jun 2025 00:36:23 +0200 Subject: [PATCH 01/15] feat: Java bindings Signed-off-by: Dmitry Dygalo --- .github/workflows/build.yml | 31 +++ .github/workflows/java-release.yml | 163 +++++++++++++ README.md | 2 +- bindings/java/.gitignore | 2 + bindings/java/CHANGELOG.md | 5 + bindings/java/Cargo.toml | 18 ++ bindings/java/README.md | 217 ++++++++++++++++++ bindings/java/build.gradle | 56 +++++ bindings/java/rustfmt.toml | 2 + .../jmh/java/org/cssinline/CSSBoxInliner.java | 45 ++++ .../java/org/cssinline/CSSInlineBench.java | 72 ++++++ .../main/java/org/cssinline/CssInline.java | 83 +++++++ .../java/org/cssinline/CssInlineConfig.java | 175 ++++++++++++++ .../org/cssinline/CssInlineException.java | 33 +++ .../org/cssinline/NativeLibraryLoader.java | 91 ++++++++ bindings/java/src/main/rust/lib.rs | 154 +++++++++++++ .../java/org/cssinline/CssInlineTest.java | 136 +++++++++++ bindings/javascript/wasm/index.html | 13 +- 18 files changed, 1290 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/java-release.yml create mode 100644 bindings/java/.gitignore create mode 100644 bindings/java/CHANGELOG.md create mode 100644 bindings/java/Cargo.toml create mode 100644 bindings/java/README.md create mode 100644 bindings/java/build.gradle create mode 100644 bindings/java/rustfmt.toml create mode 100644 bindings/java/src/jmh/java/org/cssinline/CSSBoxInliner.java create mode 100644 bindings/java/src/jmh/java/org/cssinline/CSSInlineBench.java create mode 100644 bindings/java/src/main/java/org/cssinline/CssInline.java create mode 100644 bindings/java/src/main/java/org/cssinline/CssInlineConfig.java create mode 100644 bindings/java/src/main/java/org/cssinline/CssInlineException.java create mode 100644 bindings/java/src/main/java/org/cssinline/NativeLibraryLoader.java create mode 100644 bindings/java/src/main/rust/lib.rs create mode 100644 bindings/java/src/test/java/org/cssinline/CssInlineTest.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 14bbc191..1b5690c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -438,6 +438,37 @@ jobs: set -e yarn test + test-java: + env: + JAVA_VERSION: "21" + strategy: + fail-fast: false + matrix: + os: [ubuntu-22.04, macos-13, windows-2022] + + name: Java ${{ env.JAVA_VERSION }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Build native library + # Build with `--release` as Gradle config expects `target/release` + run: cargo build --release + working-directory: bindings/java + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: ${{ env.JAVA_VERSION }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Run tests + working-directory: bindings/java + run: gradle clean test + test-python: strategy: fail-fast: false diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml new file mode 100644 index 00000000..b14daccd --- /dev/null +++ b/.github/workflows/java-release.yml @@ -0,0 +1,163 @@ +name: "[Java] Release" + +on: + pull_request: {} + push: + tags: + - java-v* + +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + JAVA_VERSION: "21" + +jobs: + build-native-libraries: + strategy: + matrix: + include: + - os: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + lib: libcss_inline.so + platform: linux-x86_64 + + - os: macos-13 + target: x86_64-apple-darwin + lib: libcss_inline.dylib + platform: darwin-x86_64 + - os: macos-14 + target: aarch64-apple-darwin + lib: libcss_inline.dylib + platform: darwin-aarch64 + + - os: windows-2022 + target: x86_64-pc-windows-msvc + lib: css_inline.dll + platform: win32-x86_64 + + name: Build native library for ${{ matrix.target }} + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Build native library + working-directory: bindings/java + run: cargo build --release --target ${{ matrix.target }} + + - name: Upload native library + uses: actions/upload-artifact@v4 + with: + name: native-${{ matrix.platform }} + if-no-files-found: error + path: bindings/java/target/${{ matrix.target }}/release/${{ matrix.lib }} + + build-jar: + name: Build JAR file + runs-on: ubuntu-22.04 + needs: build-native-libraries + steps: + - uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: ${{ env.JAVA_VERSION }} + + - name: Download all native libraries + uses: actions/download-artifact@v4 + with: + path: native-libs + + - name: Assemble JAR with native libraries + working-directory: bindings/java + run: | + mkdir -p src/main/resources/org/cssinline/native/{linux-x86_64,darwin-x86_64,darwin-aarch64,win32-x86_64} + + # Copy native libraries to their expected location + cp ../../native-libs/native-linux-x86_64/libcss_inline.so src/main/resources/org/cssinline/native/linux-x86_64/ + cp ../../native-libs/native-darwin-x86_64/libcss_inline.dylib src/main/resources/org/cssinline/native/darwin-x86_64/ + cp ../../native-libs/native-darwin-aarch64/libcss_inline.dylib src/main/resources/org/cssinline/native/darwin-aarch64/ + cp ../../native-libs/native-win32-x86_64/css_inline.dll src/main/resources/org/cssinline/native/win32-x86_64/ + + # This one will also run tests + gradle build + + - name: Upload JAR + uses: actions/upload-artifact@v4 + with: + name: java-jar + if-no-files-found: error + path: bindings/java/build/libs/*.jar + + test-jar: + needs: build-jar + strategy: + matrix: + include: + - os: ubuntu-22.04 + platform: linux-x86_64 + - os: macos-13 + platform: darwin-x86_64 + - os: macos-14 + platform: darwin-aarch64 + - os: windows-2022 + platform: win32-x86_64 + + name: Test JAR on ${{ matrix.platform }} + runs-on: ${{ matrix.os }} + steps: + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: ${{ env.JAVA_VERSION }} + + - name: Download JAR + uses: actions/download-artifact@v4 + with: + name: java-jar + + - name: Integration test on ${{ matrix.platform }} + run: | + cat > Test.java << 'EOF' + import org.cssinline.CssInline; + public class Test { + public static void main(String[] args) { + try { + String html = "

Test

"; + String result = CssInline.inline(html); + if (!result.contains("style=\"color: red;\"")) { + System.err.println("Expected inlined style not found in: " + result); + System.exit(1); + } + System.out.println("✓ Integration test passed on ${{ matrix.platform }}"); + } catch (Exception e) { + System.err.println("Integration test failed: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + } + EOF + + JAR_FILE=$(ls *.jar) + + if [[ "$RUNNER_OS" == "Windows" ]]; then + CLASSPATH_SEP=";" + else + CLASSPATH_SEP=":" + fi + + javac -cp "$JAR_FILE" Test.java + java -cp "$JAR_FILE${CLASSPATH_SEP}." Test diff --git a/README.md b/README.md index 1aa469ff..5dd99360 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ into: - Optionally caches external stylesheets - Works on Linux, Windows, and macOS - Supports HTML5 & CSS3 -- Bindings for [Python](https://github.com/Stranger6667/css-inline/tree/master/bindings/python), [Ruby](https://github.com/Stranger6667/css-inline/tree/master/bindings/ruby), [JavaScript](https://github.com/Stranger6667/css-inline/tree/master/bindings/javascript), [C](https://github.com/Stranger6667/css-inline/tree/master/bindings/c), and a [WebAssembly](https://github.com/Stranger6667/css-inline/tree/master/bindings/javascript/wasm) module to run in browsers. +- Bindings for [Python](https://github.com/Stranger6667/css-inline/tree/master/bindings/python), [Ruby](https://github.com/Stranger6667/css-inline/tree/master/bindings/ruby), [JavaScript](https://github.com/Stranger6667/css-inline/tree/master/bindings/javascript), [Java](https://github.com/Stranger6667/css-inline/tree/master/bindings/java), [C](https://github.com/Stranger6667/css-inline/tree/master/bindings/c), and a [WebAssembly](https://github.com/Stranger6667/css-inline/tree/master/bindings/javascript/wasm) module to run in browsers. - Command Line Interface ## Playground diff --git a/bindings/java/.gitignore b/bindings/java/.gitignore new file mode 100644 index 00000000..67bcc2f7 --- /dev/null +++ b/bindings/java/.gitignore @@ -0,0 +1,2 @@ +.gradle/ +build/ diff --git a/bindings/java/CHANGELOG.md b/bindings/java/CHANGELOG.md new file mode 100644 index 00000000..a17d7f83 --- /dev/null +++ b/bindings/java/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Initial public release diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml new file mode 100644 index 00000000..ceb1b86b --- /dev/null +++ b/bindings/java/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "css_inline" +version = "0.15.0" +edition = "2024" +authors = ["Dmitry Dygalo "] + +[lib] +crate-type = ["cdylib"] +path = "src/main/rust/lib.rs" + +[dependencies] +jni = "0.21.1" + +[dependencies.css-inline] +path = "../../css-inline" +version = "*" +default-features = false +features = ["http", "file", "stylesheet-cache"] diff --git a/bindings/java/README.md b/bindings/java/README.md new file mode 100644 index 00000000..45ab2e00 --- /dev/null +++ b/bindings/java/README.md @@ -0,0 +1,217 @@ +# css-inline + +[build status](https://github.com/Stranger6667/css-inline/actions/workflows/build.yml) +[maven central](#installation) +[javadoc](#documentation) + +Java bindings for the high-performance `css-inline` library that inlines CSS into HTML 'style' attributes. + +This library is designed for scenarios such as preparing HTML emails or embedding HTML into third-party web pages. + +Transforms HTML like this: + +```html + + + + + +

Big Text

+ + +``` + +into: + +```html + + + +

Big Text

+ + +``` + +## Features + +- Uses reliable components from Mozilla's Servo project +- Inlines CSS from `style` and `link` tags +- Removes `style` and `link` tags +- Resolves external stylesheets (including local files) +- Optionally caches external stylesheets +- Works on Linux, Windows, and macOS +- Supports HTML5 & CSS3 + +## Installation + +**Maven:** +```xml + + org.css-inline + css-inline + 0.15.0 + +``` + +**Gradle:** +```gradle +implementation 'org.css-inline:css-inline:0.15.0' +``` + +### Platform Support + +This JAR includes native libraries for the following platforms: + +- **Linux** x86_64 +- **macOS** x86_64 +- **macOS** aarch64 (Apple Silicon) +- **Windows** x86_64 + +Requires Java 11+ with 64-bit JVM. + +## Usage + +```java +import org.cssinline.CssInline; + +public class Example { + public static void main(String[] args) { + String html = """ + + + + + +

Big Text

+ + """; + + String inlined = CssInline.inline(html); + System.out.println(inlined); + } +} +``` + +You can also configure the inlining process: + +```java +import org.cssinline.CssInline; +import org.cssinline.CssInlineConfig; + +public class ConfigExample { + public static void main(String[] args) { + String html = "..."; + + CssInlineConfig config = new CssInlineConfig.Builder() + .setLoadRemoteStylesheets(false) + .setKeepStyleTags(true) + .setBaseUrl("https://example.com/") + .build(); + + String inlined = CssInline.inline(html, config); + } +} +``` + +- **`setInlineStyleTags(boolean)`** - Inline CSS from ` + + + +

Big Text

+ +``` + +The `data-css-inline="ignore"` attribute also allows you to skip `link` and `style` tags: + +```html + + + + + +

Big Text

+ +``` + +Alternatively, you may keep `style` from being removed by using the `data-css-inline="keep"` attribute. +This is useful if you want to keep `@media` queries for responsive emails in separate `style` tags: + +```html + + + + + +

Big Text

+ +``` + +Such tags will be kept in the resulting HTML even if the `keepStyleTags` option is set to `false`. + +## Performance + +`css-inline` is powered by efficient tooling from Mozilla's Servo project to provide high-performance CSS inlining for Java applications. + +Here is the performance comparison: + +| | Size | `css-inline 0.15.0` | `CSSBox 5.0.0` | +|-------------|---------|---------------------|------------------------| +| Basic | 230 B | 7.67 µs | 209.93 µs (**27.37x**) | +| Realistic-1 | 8.58 KB | 123.18 µs | 1.92 ms (**15.58x**) | +| Realistic-2 | 4.3 KB | 77.74 µs | 608.65 µs (**7.82x**) | +| GitHub page | 1.81 MB | 168.43 ms | 316.21 ms (**1.87x**) | + +The benchmarking code is available in the `src/jmh/java/org/cssinline/CSSInlineBench.java` file. The benchmarks were conducted using the stable `rustc 1.87`, `OpenJDK 24.0.1` on Ryzen 9 9950X. + +## License + +This project is licensed under the terms of the [MIT license](https://opensource.org/licenses/MIT). diff --git a/bindings/java/build.gradle b/bindings/java/build.gradle new file mode 100644 index 00000000..82a52aeb --- /dev/null +++ b/bindings/java/build.gradle @@ -0,0 +1,56 @@ +plugins { + id 'java' + id 'me.champeau.jmh' version '0.7.3' +} + +group = 'org.css-inline' +version = '0.15.0' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' + implementation 'com.google.code.gson:gson:2.10.1' + + jmh 'org.openjdk.jmh:jmh-core:1.37' + jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37' + + jmh 'net.sf.cssbox:cssbox:5.0.0' + jmh 'net.sf.cssbox:jstyleparser:3.5.0' + jmh 'com.fasterxml.jackson.core:jackson-core:2.15.2' + jmh 'com.fasterxml.jackson.core:jackson-databind:2.15.2' +} + +jmh { + jmhVersion = '1.37' + + def baseArgs = [ + '-server', + '--add-opens=java.base/sun.misc=ALL-UNNAMED', + '--add-exports=java.base/sun.misc=ALL-UNNAMED', + '--enable-native-access=ALL-UNNAMED' + ] + + def nativeDir = file('target/release') + if (nativeDir.exists()) { + jvmArgs = baseArgs + ["-Djava.library.path=${nativeDir.absolutePath}"] + println "Using native library path: ${nativeDir.absolutePath}" + } else { + jvmArgs = baseArgs + println "Native dir not found, using bundled libraries" + } +} + +test { + useJUnitPlatform() + + def nativeDir = file('target/release') + if (nativeDir.exists()) { + jvmArgs += "-Djava.library.path=${nativeDir.absolutePath}" + println "Test using native library path: ${nativeDir.absolutePath}" + } else { + println "Test using bundled libraries" + } +} diff --git a/bindings/java/rustfmt.toml b/bindings/java/rustfmt.toml new file mode 100644 index 00000000..bf772232 --- /dev/null +++ b/bindings/java/rustfmt.toml @@ -0,0 +1,2 @@ +imports_granularity = "Crate" +edition = "2021" diff --git a/bindings/java/src/jmh/java/org/cssinline/CSSBoxInliner.java b/bindings/java/src/jmh/java/org/cssinline/CSSBoxInliner.java new file mode 100644 index 00000000..c5cdfd73 --- /dev/null +++ b/bindings/java/src/jmh/java/org/cssinline/CSSBoxInliner.java @@ -0,0 +1,45 @@ +package org.cssinline; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import org.fit.cssbox.css.CSSNorm; +import org.fit.cssbox.css.DOMAnalyzer; +import org.fit.cssbox.css.NormalOutput; +import org.fit.cssbox.css.Output; +import org.fit.cssbox.io.DOMSource; +import org.fit.cssbox.io.DefaultDOMSource; +import org.fit.cssbox.io.DefaultDocumentSource; +import org.fit.cssbox.io.DocumentSource; +import org.w3c.dom.Document; + +public class CSSBoxInliner { + + public String inline(String html) throws Exception { + String dataUrl = "data:text/html;charset=utf-8," + java.net.URLEncoder.encode(html, "UTF-8"); + DocumentSource docSource = new DefaultDocumentSource(dataUrl); + + try { + DOMSource parser = new DefaultDOMSource(docSource); + Document doc = parser.parse(); + + DOMAnalyzer da = new DOMAnalyzer(doc, docSource.getURL()); + da.attributesToStyles(); + da.addStyleSheet(null, CSSNorm.stdStyleSheet(), DOMAnalyzer.Origin.AGENT); + da.addStyleSheet(null, CSSNorm.userStyleSheet(), DOMAnalyzer.Origin.AGENT); + da.getStyleSheets(); + + da.stylesToDomInherited(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos, true, "UTF-8"); + Output out = new NormalOutput(doc); + out.dumpTo(ps); + ps.close(); + + return baos.toString("UTF-8"); + + } finally { + docSource.close(); + } + } +} diff --git a/bindings/java/src/jmh/java/org/cssinline/CSSInlineBench.java b/bindings/java/src/jmh/java/org/cssinline/CSSInlineBench.java new file mode 100644 index 00000000..12efd0b7 --- /dev/null +++ b/bindings/java/src/jmh/java/org/cssinline/CSSInlineBench.java @@ -0,0 +1,72 @@ +package org.cssinline; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Fork(value = 1, jvmArgs = {"-server"}) +@Warmup(iterations = 1, time = 10, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 3, time = 10, timeUnit = TimeUnit.SECONDS) +public class CSSInlineBench { + + @Param({"simple", "merging", "double_quotes", "big_email_1", "big_email_2", "big_page"}) + public String name; + + private static Map cases; + + private CssInlineConfig cfg; + + private CSSBoxInliner cssBoxInliner; + + @Setup(Level.Trial) + public void setup() throws IOException { + cfg = new CssInlineConfig.Builder().build(); + cssBoxInliner = new CSSBoxInliner(); + + if (cases == null) { + Path path = Paths.get("").resolve("../../benchmarks/benchmarks.json"); + + List> list = new ObjectMapper().readValue(path.toFile(), + new TypeReference>>() { + }); + + cases = new LinkedHashMap<>(); + for (Map entry : list) { + cases.put(entry.get("name"), entry.get("html")); + } + } + } + + @Benchmark + public String benchCSSInline() { + return CssInline.inline(cases.get(name), cfg); + } + + @Benchmark + public String benchCSSBox() { + try { + return cssBoxInliner.inline(cases.get(name)); + } catch (Exception e) { + throw new RuntimeException("CSSBox benchmark failed", e); + } + } + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder().include(CSSInlineBench.class.getSimpleName()).build(); + new Runner(opt).run(); + } +} diff --git a/bindings/java/src/main/java/org/cssinline/CssInline.java b/bindings/java/src/main/java/org/cssinline/CssInline.java new file mode 100644 index 00000000..325e7b3d --- /dev/null +++ b/bindings/java/src/main/java/org/cssinline/CssInline.java @@ -0,0 +1,83 @@ +package org.cssinline; + +/** + * Main entry point for CSS inlining. + * + * Inlines CSS styles from <style> and <link> tags directly into + * HTML element style attributes. Useful for preparing HTML emails or embedding + * HTML content where external stylesheets are not supported. + */ +public class CssInline { + static { + NativeLibraryLoader.loadLibrary(); + } + + /** + * Private constructor to prevent instantiation of utility class. + */ + private CssInline() {} + + private static native String nativeInline(String html, CssInlineConfig cfg); + private static native String nativeInlineFragment(String html, String css, CssInlineConfig cfg); + + /** + * Inlines CSS styles into HTML elements using the specified configuration. + * + * @param html + * the HTML document to process + * @param cfg + * the configuration object specifying inlining behavior + * @return the HTML document with CSS styles inlined + * @throws CssInlineException + * if an error occurs during processing + */ + public static String inline(String html, CssInlineConfig cfg) { + return nativeInline(html, cfg); + } + + /** + * Inlines CSS styles into HTML elements using default configuration. + * + * @param html + * the HTML document to process + * @return the HTML document with CSS styles inlined + * @throws CssInlineException + * if an error occurs during processing + */ + public static String inline(String html) { + return inline(html, new CssInlineConfig.Builder().build()); + } + + /** + * Inlines the provided CSS into an HTML fragment using the specified configuration. + * + *

Unlike {@link #inline(String, CssInlineConfig)}, this method works with HTML fragments + * (elements without <html>, <head>, or <body> tags) and applies the + * provided CSS directly without parsing <style> or <link> tags. + * + * @param fragment the HTML fragment to process + * @param css the CSS rules to inline + * @param cfg the configuration object specifying inlining behavior + * @return the HTML fragment with CSS styles inlined + * @throws CssInlineException if an error occurs during processing + */ + public static String inlineFragment(String fragment, String css, CssInlineConfig cfg) { + return nativeInlineFragment(fragment, css, cfg); + } + + /** + * Inlines the provided CSS into an HTML fragment using default configuration. + * + *

Unlike {@link #inline(String)}, this method works with HTML fragments + * (elements without <html>, <head>, or <body> tags) and applies the + * provided CSS directly without parsing <style> or <link> tags. + * + * @param fragment the HTML fragment to process + * @param css the CSS rules to inline + * @return the HTML fragment with CSS styles inlined + * @throws CssInlineException if an error occurs during processing + */ + public static String inlineFragment(String fragment, String css) { + return inlineFragment(fragment, css, new CssInlineConfig.Builder().build()); + } +} diff --git a/bindings/java/src/main/java/org/cssinline/CssInlineConfig.java b/bindings/java/src/main/java/org/cssinline/CssInlineConfig.java new file mode 100644 index 00000000..557e2b15 --- /dev/null +++ b/bindings/java/src/main/java/org/cssinline/CssInlineConfig.java @@ -0,0 +1,175 @@ +package org.cssinline; + +/** Configuration options for CSS inlining. */ +public class CssInlineConfig { + /** Whether to inline CSS from "style" tags. */ + public final boolean inlineStyleTags; + + /** Keep "style" tags after inlining. */ + public final boolean keepStyleTags; + + /** Keep "link" tags after inlining. */ + public final boolean keepLinkTags; + + /** Whether remote stylesheets should be loaded or not. */ + public final boolean loadRemoteStylesheets; + + /** Used for loading external stylesheets via relative URLs. */ + public final String baseUrl; + + /** Additional CSS to inline. */ + public final String extraCss; + + /** External stylesheet cache size. */ + public final int cacheSize; + + /** Pre-allocate capacity for HTML nodes during parsing. */ + public final int preallocateNodeCapacity; + + private CssInlineConfig(boolean inlineStyleTags, boolean keepStyleTags, boolean keepLinkTags, + boolean loadRemoteStylesheets, String baseUrl, String extraCss, int cacheSize, + int preallocateNodeCapacity) { + this.inlineStyleTags = inlineStyleTags; + this.keepStyleTags = keepStyleTags; + this.keepLinkTags = keepLinkTags; + this.loadRemoteStylesheets = loadRemoteStylesheets; + this.baseUrl = baseUrl; + this.extraCss = extraCss; + this.cacheSize = cacheSize; + this.preallocateNodeCapacity = preallocateNodeCapacity; + } + + /** + * Builder for creating {@link CssInlineConfig} instances. + */ + public static class Builder { + private boolean inlineStyleTags = true; + private boolean keepStyleTags = false; + private boolean keepLinkTags = false; + private boolean loadRemoteStylesheets = true; + private String baseUrl = null; + private String extraCss = null; + private int cacheSize = 0; + private int preallocateNodeCapacity = 32; + + /** + * Creates a new builder with default configuration values. + */ + public Builder() { + // Default constructor for builder + } + + /** + * Whether to inline CSS from "style" tags. + * Sometimes HTML may include boilerplate styles that are not applicable + * in every scenario and it is useful to ignore them and use extraCss instead. + * + * @param b true to inline CSS from style tags, false to ignore them + * @return this builder instance for method chaining + */ + public Builder setInlineStyleTags(boolean b) { + this.inlineStyleTags = b; + return this; + } + + /** + * Keep "style" tags after inlining. + * + * @param b true to preserve style tags, false to remove them + * @return this builder instance for method chaining + */ + public Builder setKeepStyleTags(boolean b) { + this.keepStyleTags = b; + return this; + } + + /** + * Keep "link" tags after inlining. + * + * @param b true to preserve link tags, false to remove them + * @return this builder instance for method chaining + */ + public Builder setKeepLinkTags(boolean b) { + this.keepLinkTags = b; + return this; + } + + /** + * Whether remote stylesheets should be loaded or not. + * + * @param b true to load external stylesheets, false to ignore them + * @return this builder instance for method chaining + */ + public Builder setLoadRemoteStylesheets(boolean b) { + this.loadRemoteStylesheets = b; + return this; + } + + /** + * Used for loading external stylesheets via relative URLs. + * + * @param url the base URL as a string, or null to use no base URL + * @return this builder instance for method chaining + */ + public Builder setBaseUrl(String url) { + this.baseUrl = url; + return this; + } + + /** + * Additional CSS to inline. + * + * @param css additional CSS rules as a string, or null for no extra CSS + * @return this builder instance for method chaining + */ + public Builder setExtraCss(String css) { + this.extraCss = css; + return this; + } + + /** + * External stylesheet cache size. + * + * @param size + * cache size, must be non-negative + * @return this builder instance for method chaining + * @throws IllegalArgumentException + * if size is negative + */ + public Builder setCacheSize(int size) { + if (size < 0) { + throw new IllegalArgumentException("Cache size must be non-negative, got: " + size); + } + this.cacheSize = size; + return this; + } + + /** + * Pre-allocate capacity for HTML nodes during parsing. Can improve performance + * when you have an estimate of the number of nodes in your HTML document. + * + * @param cap + * initial node capacity, must be positive + * @return this builder instance for method chaining + * @throws IllegalArgumentException + * if cap is zero or negative + */ + public Builder setPreallocateNodeCapacity(int cap) { + if (cap <= 0) { + throw new IllegalArgumentException("Preallocate node capacity must be positive, got: " + cap); + } + this.preallocateNodeCapacity = cap; + return this; + } + + /** + * Creates a new {@link CssInlineConfig} instance with the current builder settings. + * + * @return a new immutable configuration instance + */ + public CssInlineConfig build() { + return new CssInlineConfig(inlineStyleTags, keepStyleTags, keepLinkTags, loadRemoteStylesheets, baseUrl, + extraCss, cacheSize, preallocateNodeCapacity); + } + } +} diff --git a/bindings/java/src/main/java/org/cssinline/CssInlineException.java b/bindings/java/src/main/java/org/cssinline/CssInlineException.java new file mode 100644 index 00000000..dc3db31a --- /dev/null +++ b/bindings/java/src/main/java/org/cssinline/CssInlineException.java @@ -0,0 +1,33 @@ +package org.cssinline; + +/** + * Exception thrown when CSS inlining operations fail. + * + * This runtime exception is thrown when errors occur during CSS processing, + * HTML parsing, or stylesheet loading. + */ +public class CssInlineException extends RuntimeException { + + /** + * Creates a new exception with the specified error message. + * + * @param message + * the error message describing what went wrong + */ + public CssInlineException(String message) { + super(message); + } + + /** + * Creates a new exception with the specified error message and underlying + * cause. + * + * @param message + * the error message describing what went wrong + * @param cause + * the underlying exception that caused this error + */ + public CssInlineException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bindings/java/src/main/java/org/cssinline/NativeLibraryLoader.java b/bindings/java/src/main/java/org/cssinline/NativeLibraryLoader.java new file mode 100644 index 00000000..6785fad7 --- /dev/null +++ b/bindings/java/src/main/java/org/cssinline/NativeLibraryLoader.java @@ -0,0 +1,91 @@ +package org.cssinline; + +import java.io.*; +import java.nio.file.*; + +class NativeLibraryLoader { + private static final String LIBRARY_NAME = "css_inline"; + private static boolean loaded = false; + + static synchronized void loadLibrary() { + if (loaded) + return; + + try { + // Try system library first (for development) + System.loadLibrary(LIBRARY_NAME); + loaded = true; + return; + } catch (UnsatisfiedLinkError e) { + // Fall back to bundled library + } + + String platform = detectPlatform(); + String libraryPath = "/org/cssinline/native/" + platform + "/" + getLibraryFileName(); + + try (InputStream is = NativeLibraryLoader.class.getResourceAsStream(libraryPath)) { + if (is == null) { + throw new UnsatisfiedLinkError("Native library not found: " + libraryPath + + ". Available platforms: linux-x86_64, darwin-x86_64, darwin-aarch64, win32-x86_64"); + } + + // Extract to temporary file + Path tempDir = Files.createTempDirectory("css-inline-native"); + Path tempFile = tempDir.resolve(getLibraryFileName()); + Files.copy(is, tempFile, StandardCopyOption.REPLACE_EXISTING); + + // Load library + System.load(tempFile.toAbsolutePath().toString()); + loaded = true; + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + Files.deleteIfExists(tempFile); + Files.deleteIfExists(tempDir); + } catch (IOException ignored) { + } + })); + + } catch (IOException e) { + throw new UnsatisfiedLinkError("Failed to load native library: " + e.getMessage()); + } + } + + private static String detectPlatform() { + String os = System.getProperty("os.name").toLowerCase(); + String arch = System.getProperty("os.arch").toLowerCase(); + + String osName; + if (os.contains("windows")) { + osName = "win32"; + } else if (os.contains("mac") || os.contains("darwin")) { + osName = "darwin"; + } else if (os.contains("linux")) { + osName = "linux"; + } else { + throw new UnsatisfiedLinkError("Unsupported OS: " + os); + } + + String archName; + if (arch.contains("amd64") || arch.contains("x86_64")) { + archName = "x86_64"; + } else if (arch.contains("aarch64") || arch.contains("arm64")) { + archName = "aarch64"; + } else { + throw new UnsatisfiedLinkError("Unsupported architecture: " + arch); + } + + return osName + "-" + archName; + } + + private static String getLibraryFileName() { + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("windows")) { + return LIBRARY_NAME + ".dll"; + } else if (os.contains("mac") || os.contains("darwin")) { + return "lib" + LIBRARY_NAME + ".dylib"; + } else { + return "lib" + LIBRARY_NAME + ".so"; + } + } +} diff --git a/bindings/java/src/main/rust/lib.rs b/bindings/java/src/main/rust/lib.rs new file mode 100644 index 00000000..3a1b3615 --- /dev/null +++ b/bindings/java/src/main/rust/lib.rs @@ -0,0 +1,154 @@ +use core::fmt; +use css_inline::{CSSInliner, StylesheetCache}; +use jni::{ + JNIEnv, + errors::Result as JNIResult, + objects::{JClass, JObject, JString}, + sys::jstring, +}; +use std::{borrow::Cow, num::NonZeroUsize}; + +trait JNIExt { + fn get_rust_string(&mut self, obj: &JString) -> String; + fn to_jstring(&mut self, obj: String) -> jstring; + fn get_bool_field(&mut self, obj: &JObject, name: &str) -> JNIResult; + fn get_int_field(&mut self, obj: &JObject, name: &str) -> JNIResult; + fn get_string_field_opt(&mut self, obj: &JObject, name: &str) -> JNIResult>; +} + +impl<'a> JNIExt for JNIEnv<'a> { + fn get_rust_string(&mut self, obj: &JString) -> String { + self.get_string(&obj) + .expect("Failed to get Java String") + .into() + } + + fn to_jstring(&mut self, obj: String) -> jstring { + self.new_string(obj) + .expect("Failed to get Java String") + .into_raw() + } + + fn get_bool_field(&mut self, obj: &JObject, name: &str) -> JNIResult { + self.get_field(obj, name, "Z")?.z() + } + + fn get_int_field(&mut self, obj: &JObject, name: &str) -> JNIResult { + self.get_field(obj, name, "I")?.i() + } + + fn get_string_field_opt(&mut self, cfg: &JObject, name: &str) -> JNIResult> { + let value = self.get_field(cfg, name, "Ljava/lang/String;")?.l()?; + if value.is_null() { + Ok(None) + } else { + Ok(Some(self.get_string(&JString::from(value))?.into())) + } + } +} + +enum Error { + Jni(jni::errors::Error), + Other(E), +} + +impl From for Error { + fn from(value: jni::errors::Error) -> Self { + Error::Jni(value) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Jni(error) => error.fmt(f), + Error::Other(error) => error.fmt(f), + } + } +} + +fn build_inliner( + env: &mut JNIEnv, + cfg: JObject, +) -> Result, Error> { + let inline_style_tags = env.get_bool_field(&cfg, "inlineStyleTags")?; + let keep_style_tags = env.get_bool_field(&cfg, "keepStyleTags")?; + let keep_link_tags = env.get_bool_field(&cfg, "keepLinkTags")?; + let load_remote_stylesheets = env.get_bool_field(&cfg, "loadRemoteStylesheets")?; + let cache_size = env.get_int_field(&cfg, "cacheSize")?; + let preallocate_node_capacity = env.get_int_field(&cfg, "preallocateNodeCapacity")?; + + let extra_css = env.get_string_field_opt(&cfg, "extraCss")?; + let base_url = env.get_string_field_opt(&cfg, "baseUrl")?; + let mut builder = CSSInliner::options() + .inline_style_tags(inline_style_tags) + .keep_style_tags(keep_style_tags) + .keep_link_tags(keep_link_tags) + .load_remote_stylesheets(load_remote_stylesheets) + .extra_css(extra_css.map(Cow::Owned)) + .preallocate_node_capacity(preallocate_node_capacity as usize); + + if let Some(url) = base_url { + match css_inline::Url::parse(&url) { + Ok(url) => { + builder = builder.base_url(Some(url)); + } + Err(error) => return Err(Error::Other(error)), + } + } + + if cache_size > 0 { + builder = builder.cache(StylesheetCache::new( + NonZeroUsize::new(cache_size as usize).expect("Cache size is not null"), + )); + } + + Ok(builder.build()) +} + +fn throw(mut env: JNIEnv, message: String) -> jstring { + let exception = env + .find_class("org/cssinline/CssInlineException") + .expect("CssInlineException class not found"); + env.throw_new(exception, message) + .expect("Failed to throw CssInlineException"); + std::ptr::null_mut() +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_org_cssinline_CssInline_nativeInline( + mut env: JNIEnv, + _class: JClass, + input: JString, + cfg: JObject, +) -> jstring { + let html = env.get_rust_string(&input); + let inliner = match build_inliner(&mut env, cfg) { + Ok(inliner) => inliner, + Err(error) => return throw(env, error.to_string()), + }; + match inliner.inline(&html) { + Ok(out) => env.to_jstring(out), + Err(error) => throw(env, error.to_string()), + } +} + +#[unsafe(no_mangle)] +pub extern "system" fn Java_org_cssinline_CssInline_nativeInlineFragment( + mut env: JNIEnv, + _class: JClass, + input: JString, + css: JString, + cfg: JObject, +) -> jstring { + let html = env.get_rust_string(&input); + let css = env.get_rust_string(&css); + let inliner = match build_inliner(&mut env, cfg) { + Ok(inliner) => inliner, + Err(error) => return throw(env, error.to_string()), + }; + match inliner.inline_fragment(&html, &css) { + Ok(out) => env.to_jstring(out), + Err(error) => throw(env, error.to_string()), + } +} diff --git a/bindings/java/src/test/java/org/cssinline/CssInlineTest.java b/bindings/java/src/test/java/org/cssinline/CssInlineTest.java new file mode 100644 index 00000000..465a3252 --- /dev/null +++ b/bindings/java/src/test/java/org/cssinline/CssInlineTest.java @@ -0,0 +1,136 @@ +package org.cssinline; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class CssInlineTest { + + @Test + void inlinesSimpleStyleTag() { + String html = "

Hello

"; + + String out = CssInline.inline(html); + + assertTrue(out.contains("style=\"color: blue;\""), "Output should inline styles for h1, got: " + out); + } + + @Test + void extraCssAddsBackground() { + String html = "

Hello

"; + + CssInlineConfig cfg = new CssInlineConfig.Builder().setExtraCss("h1 { color: blue; }").build(); + + String out = CssInline.inline(html, cfg); + + assertTrue(out.contains("style=\"color: blue;\""), "Output should inline styles for h1, got: " + out); + } + + @Test + void validBaseUrlParses() { + CssInlineConfig cfg = new CssInlineConfig.Builder().setBaseUrl("https://example.com/styles/").build(); + + String in = "

No styles

"; + String out = CssInline.inline(in, cfg); + assertNotNull(out); + assertTrue(out.contains("

No styles

")); + } + + @Test + void invalidBaseUrlThrows() { + CssInlineConfig cfg = new CssInlineConfig.Builder().setBaseUrl("not a url").build(); + + CssInlineException ex = assertThrows(CssInlineException.class, () -> CssInline.inline("

Hi

", cfg)); + assertEquals(ex.getMessage(), "relative URL without a base", + "Expected URL parse error, got: " + ex.getMessage()); + } + + @Test + void keepStyleTagsPreserved() { + String html = "" + "

Bold

"; + + CssInlineConfig cfg = new CssInlineConfig.Builder().setKeepStyleTags(true).build(); + + String out = CssInline.inline(html, cfg); + assertTrue(out.contains(""), "Expected to keep original style tags"); + assertTrue(out.contains("style=\"font-weight: bold;\"")); + } + + @Test + void negativeCacheSizeThrows() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new CssInlineConfig.Builder().setCacheSize(-1)); + assertEquals("Cache size must be non-negative, got: -1", ex.getMessage()); + } + + @Test + void zeroOrNegativePreallocateCapacityThrows() { + IllegalArgumentException ex1 = assertThrows(IllegalArgumentException.class, + () -> new CssInlineConfig.Builder().setPreallocateNodeCapacity(0)); + assertEquals("Preallocate node capacity must be positive, got: 0", ex1.getMessage()); + + IllegalArgumentException ex2 = assertThrows(IllegalArgumentException.class, + () -> new CssInlineConfig.Builder().setPreallocateNodeCapacity(-5)); + assertEquals("Preallocate node capacity must be positive, got: -5", ex2.getMessage()); + } + + @Test + void validConfigurationBuilds() { + assertDoesNotThrow(() -> { + CssInlineConfig cfg = new CssInlineConfig.Builder().setCacheSize(0).setCacheSize(100) + .setPreallocateNodeCapacity(1).build(); + }); + } + + @Test + void inlineFragmentBasic() { + String fragment = """ +
+

Hello

+
+

who am i

+
+
"""; + + String css = """ + p { + color: red; + } + + h1 { + color: blue; + } + """; + + String result = CssInline.inlineFragment(fragment, css); + + assertTrue(result.contains("h1 style=\"color: blue;\""), "Should inline h1 color"); + assertTrue(result.contains("p style=\"color: red;\""), "Should inline p color"); + assertFalse(result.contains(""), "Should not add html wrapper"); + assertFalse(result.contains(""), "Should not add head wrapper"); + } + + @Test + void inlineFragmentWithConfig() { + String fragment = "

Test

"; + String css = "h1 { color: blue; font-size: 16px; }"; + + CssInlineConfig config = new CssInlineConfig.Builder() + .setExtraCss("div { margin: 10px; }") + .build(); + + String result = CssInline.inlineFragment(fragment, css, config); + + assertTrue(result.contains("h1 style=\"color: blue;font-size: 16px;\""), + "Should inline h1 styles"); + assertTrue(result.contains("div style=\"margin: 10px;\""), + "Should apply extra CSS"); + } + + @Test + void inlineFragmentEmpty() { + String result = CssInline.inlineFragment("", ""); + + assertEquals("", result, "Empty fragment and CSS should return empty string"); + } +} diff --git a/bindings/javascript/wasm/index.html b/bindings/javascript/wasm/index.html index 3b7a9bef..4f26b3ba 100644 --- a/bindings/javascript/wasm/index.html +++ b/bindings/javascript/wasm/index.html @@ -5,11 +5,11 @@ CSS Inline | High-performance CSS inlining High-performance library for inlining CSS into HTML 'style' attributes

- css-inline leverages Mozilla's Servo project components and works with - Rust, Python, Ruby, JavaScript, and C. The playground below is powered - by WebAssembly allows direct browser interaction with the library. Edit - the HTML in the text area and click "Inline" to view the results - instantly. + css-inline uses components from Mozilla's Servo project and provides + bindings for Rust, Python, Ruby, JavaScript, Java, and C. The playground + runs the library compiled to WebAssembly in the browser. Paste HTML with + CSS into the text area and click "Inline" to process the output.

From 3b8f466331cf58298dd85e08c224dba806d5e363 Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 13:58:37 +0200 Subject: [PATCH 02/15] wip Signed-off-by: Dmitry Dygalo --- bindings/java/README.md | 4 ++-- bindings/java/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bindings/java/README.md b/bindings/java/README.md index 45ab2e00..a14f1339 100644 --- a/bindings/java/README.md +++ b/bindings/java/README.md @@ -1,8 +1,8 @@ # css-inline [build status](https://github.com/Stranger6667/css-inline/actions/workflows/build.yml) -[maven central](#installation) -[javadoc](#documentation) +[maven central](#installation) +[javadoc](https://javadoc.io/badge2/org.css-inline/css-inline/javadoc.svg) Java bindings for the high-performance `css-inline` library that inlines CSS into HTML 'style' attributes. diff --git a/bindings/java/build.gradle b/bindings/java/build.gradle index 82a52aeb..88f4d866 100644 --- a/bindings/java/build.gradle +++ b/bindings/java/build.gradle @@ -4,7 +4,7 @@ plugins { } group = 'org.css-inline' -version = '0.15.0' +version = System.getenv('VERSION') ?: '0.15.0-SNAPSHOT' repositories { mavenCentral() From 01de0c23d59a8fe6b1ef2192bc1def2d5b257010 Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 14:23:37 +0200 Subject: [PATCH 03/15] wip Signed-off-by: Dmitry Dygalo --- .github/workflows/build.yml | 5 ++- .github/workflows/java-release.yml | 63 ++++++++++++++++++++++++++++++ bindings/java/README.md | 52 ++++++++++++++++++------ bindings/java/build.gradle | 56 ++++++++++++++++++++++++-- 4 files changed, 159 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b5690c2..2bc5201b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,9 @@ on: - master schedule: [cron: "40 1 * * *"] +env: + JAVA_VERSION: "21" + jobs: commitsar: name: Verify commit messages @@ -439,8 +442,6 @@ jobs: yarn test test-java: - env: - JAVA_VERSION: "21" strategy: fail-fast: false matrix: diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml index b14daccd..e2cea583 100644 --- a/.github/workflows/java-release.yml +++ b/.github/workflows/java-release.yml @@ -161,3 +161,66 @@ jobs: javac -cp "$JAR_FILE" Test.java java -cp "$JAR_FILE${CLASSPATH_SEP}." Test + + publish-github-packages: + name: Publish to GitHub Packages + runs-on: ubuntu-22.04 + needs: [build-jar, test-jar] + if: startsWith(github.ref, 'refs/tags/java-v') + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: ${{ env.JAVA_VERSION }} + + - name: Download JAR artifact + uses: actions/download-artifact@v4 + with: + name: java-jar + path: build/libs + + - name: Extract version + run: echo "version=${GITHUB_REF#refs/tags/java-v}" >> $GITHUB_ENV + + - name: Publish to GitHub Packages + working-directory: bindings/java + run: | + # Copy the pre-built JAR to the expected location + mkdir -p build/libs + cp ../../build/libs/*.jar build/libs/ + + # Publish using the existing JAR + gradle publish -Pversion=${{ env.version }} + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ env.version }} + + release: + name: Create GitHub Release + runs-on: ubuntu-22.04 + needs: [build-jar, test-jar, publish-github-packages] + if: startsWith(github.ref, 'refs/tags/java-v') + steps: + - name: Download JAR + uses: actions/download-artifact@v4 + with: + name: java-jar + path: dist + + - name: Extract version + run: echo "version=${GITHUB_REF#refs/tags/java-v}" >> $GITHUB_ENV + + - name: GitHub Release + uses: softprops/action-gh-release@v2 + with: + make_latest: false + draft: true + name: "[Java] Release ${{ env.version }}" + files: dist/*.jar diff --git a/bindings/java/README.md b/bindings/java/README.md index a14f1339..daaea9b4 100644 --- a/bindings/java/README.md +++ b/bindings/java/README.md @@ -1,8 +1,7 @@ # css-inline [build status](https://github.com/Stranger6667/css-inline/actions/workflows/build.yml) -[maven central](#installation) -[javadoc](https://javadoc.io/badge2/org.css-inline/css-inline/javadoc.svg) +[github packages](https://github.com/Stranger6667/css-inline/packages) Java bindings for the high-performance `css-inline` library that inlines CSS into HTML 'style' attributes. @@ -44,20 +43,49 @@ into: ## Installation -**Maven:** -```xml - - org.css-inline - css-inline - 0.15.0 - -``` +This package is available on [GitHub Packages](https://github.com/Stranger6667/css-inline/packages). + +### Setup + +GitHub Packages requires authentication even for public packages. See the [GitHub documentation](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#authenticating-to-github-packages) for authentication setup. **Gradle:** ```gradle -implementation 'org.css-inline:css-inline:0.15.0' +repositories { + maven { + url = uri("https://maven.pkg.github.com/Stranger6667/css-inline") + credentials { + username = project.findProperty("gpr.user") ?: System.getenv("USERNAME") + password = project.findProperty("gpr.key") ?: System.getenv("TOKEN") + } + } +} + +dependencies { + implementation 'org.css-inline:css-inline:0.15.0' +} ``` +**Maven:** +```xml + + + github + https://maven.pkg.github.com/Stranger6667/css-inline + + + + + + org.css-inline + css-inline + 0.15.0 + + +``` + +See [GitHub's Maven documentation](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry) for Maven authentication setup. + ### Platform Support This JAR includes native libraries for the following platforms: @@ -142,7 +170,7 @@ public class FragmentExample { p { color: red; } - + h1 { color: blue; } diff --git a/bindings/java/build.gradle b/bindings/java/build.gradle index 88f4d866..80132344 100644 --- a/bindings/java/build.gradle +++ b/bindings/java/build.gradle @@ -1,13 +1,63 @@ plugins { - id 'java' + id 'java-library' + id 'maven-publish' id 'me.champeau.jmh' version '0.7.3' } group = 'org.css-inline' version = System.getenv('VERSION') ?: '0.15.0-SNAPSHOT' -repositories { - mavenCentral() +java { + withJavadocJar() + withSourcesJar() + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +publishing { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/Stranger6667/css-inline") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } + + publications { + maven(MavenPublication) { + from components.java + + pom { + name = 'CSS Inline Java' + description = 'Java bindings for css-inline - a high-performance CSS inlining library' + url = 'https://css-inline.org' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/licenses/MIT' + } + } + + developers { + developer { + id = 'stranger6667' + name = 'Dmitry Dygalo' + email = 'dmitry@dygalo.dev' + } + } + + scm { + connection = 'scm:git:git://github.com/Stranger6667/css-inline.git' + developerConnection = 'scm:git:ssh://github.com:Stranger6667/css-inline.git' + url = 'https://github.com/Stranger6667/css-inline/tree/master' + } + } + } + } } dependencies { From 038cb8886f0ac4219dbebb39d11db5da0968713d Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 14:30:47 +0200 Subject: [PATCH 04/15] wip Signed-off-by: Dmitry Dygalo --- bindings/java/README.md | 2 +- bindings/java/build.gradle | 34 +++++++++++++++++++--------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/bindings/java/README.md b/bindings/java/README.md index daaea9b4..0408894a 100644 --- a/bindings/java/README.md +++ b/bindings/java/README.md @@ -95,7 +95,7 @@ This JAR includes native libraries for the following platforms: - **macOS** aarch64 (Apple Silicon) - **Windows** x86_64 -Requires Java 11+ with 64-bit JVM. +Requires Java 17+ with 64-bit JVM. ## Usage diff --git a/bindings/java/build.gradle b/bindings/java/build.gradle index 80132344..299b3efd 100644 --- a/bindings/java/build.gradle +++ b/bindings/java/build.gradle @@ -10,8 +10,25 @@ version = System.getenv('VERSION') ?: '0.15.0-SNAPSHOT' java { withJavadocJar() withSourcesJar() - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' + implementation 'com.google.code.gson:gson:2.10.1' + + jmh 'org.openjdk.jmh:jmh-core:1.37' + jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37' + + jmh 'net.sf.cssbox:cssbox:5.0.0' + jmh 'net.sf.cssbox:jstyleparser:3.5.0' + jmh 'com.fasterxml.jackson.core:jackson-core:2.15.2' + jmh 'com.fasterxml.jackson.core:jackson-databind:2.15.2' } publishing { @@ -60,19 +77,6 @@ publishing { } } -dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' - implementation 'com.google.code.gson:gson:2.10.1' - - jmh 'org.openjdk.jmh:jmh-core:1.37' - jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37' - - jmh 'net.sf.cssbox:cssbox:5.0.0' - jmh 'net.sf.cssbox:jstyleparser:3.5.0' - jmh 'com.fasterxml.jackson.core:jackson-core:2.15.2' - jmh 'com.fasterxml.jackson.core:jackson-databind:2.15.2' -} - jmh { jmhVersion = '1.37' From 6e00caeeac4aba73e02364120abc399209a24f92 Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 14:42:05 +0200 Subject: [PATCH 05/15] wip Signed-off-by: Dmitry Dygalo --- .github/workflows/build.yml | 2 +- .github/workflows/java-release.yml | 2 +- bindings/java/README.md | 2 +- bindings/java/build.gradle | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2bc5201b..23150474 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -447,7 +447,7 @@ jobs: matrix: os: [ubuntu-22.04, macos-13, windows-2022] - name: Java ${{ env.JAVA_VERSION }} on ${{ matrix.os }} + name: Java 21 on ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml index e2cea583..3e3e932a 100644 --- a/.github/workflows/java-release.yml +++ b/.github/workflows/java-release.yml @@ -91,7 +91,7 @@ jobs: cp ../../native-libs/native-win32-x86_64/css_inline.dll src/main/resources/org/cssinline/native/win32-x86_64/ # This one will also run tests - gradle build + gradle build --info - name: Upload JAR uses: actions/upload-artifact@v4 diff --git a/bindings/java/README.md b/bindings/java/README.md index 0408894a..cba46dab 100644 --- a/bindings/java/README.md +++ b/bindings/java/README.md @@ -95,7 +95,7 @@ This JAR includes native libraries for the following platforms: - **macOS** aarch64 (Apple Silicon) - **Windows** x86_64 -Requires Java 17+ with 64-bit JVM. +Requires Java 21+ with 64-bit JVM. ## Usage diff --git a/bindings/java/build.gradle b/bindings/java/build.gradle index 299b3efd..b7af745b 100644 --- a/bindings/java/build.gradle +++ b/bindings/java/build.gradle @@ -10,8 +10,8 @@ version = System.getenv('VERSION') ?: '0.15.0-SNAPSHOT' java { withJavadocJar() withSourcesJar() - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } repositories { From 685dfff6b6a1ec900384d9aee07e3005018449a3 Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 14:50:01 +0200 Subject: [PATCH 06/15] wip Signed-off-by: Dmitry Dygalo --- .github/workflows/build.yml | 4 ++-- .github/workflows/java-release.yml | 27 ++++++++++++++++++++++++++- bindings/java/README.md | 2 +- bindings/java/build.gradle | 4 ++-- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23150474..962e6581 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ on: schedule: [cron: "40 1 * * *"] env: - JAVA_VERSION: "21" + JAVA_VERSION: "17" jobs: commitsar: @@ -447,7 +447,7 @@ jobs: matrix: os: [ubuntu-22.04, macos-13, windows-2022] - name: Java 21 on ${{ matrix.os }} + name: Java 17 on ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml index 3e3e932a..44346654 100644 --- a/.github/workflows/java-release.yml +++ b/.github/workflows/java-release.yml @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true env: - JAVA_VERSION: "21" + JAVA_VERSION: "17" jobs: build-native-libraries: @@ -93,6 +93,31 @@ jobs: # This one will also run tests gradle build --info + echo "=== After gradle build ===" + ls -la build/libs/ || echo "No build/libs directory" + + - name: Verify JAR contents + working-directory: bindings/java + run: | + echo "=== Built JARs ===" + ls -la build/libs/ + + echo "=== JAR contents (first 30 lines) ===" + jar tf build/libs/*.jar | head -30 + + echo "=== Looking for CssInline class ===" + if jar tf build/libs/*.jar | grep -q "org/cssinline/CssInline.class"; then + echo "✅ CssInline.class found" + else + echo "❌ CssInline.class NOT found!" + echo "=== All .class files in JAR ===" + jar tf build/libs/*.jar | grep "\.class$" || echo "No .class files found!" + exit 1 + fi + + echo "=== Looking for native libraries ===" + jar tf build/libs/*.jar | grep -E "\.(so|dylib|dll)$" || echo "No native libraries found" + - name: Upload JAR uses: actions/upload-artifact@v4 with: diff --git a/bindings/java/README.md b/bindings/java/README.md index cba46dab..0408894a 100644 --- a/bindings/java/README.md +++ b/bindings/java/README.md @@ -95,7 +95,7 @@ This JAR includes native libraries for the following platforms: - **macOS** aarch64 (Apple Silicon) - **Windows** x86_64 -Requires Java 21+ with 64-bit JVM. +Requires Java 17+ with 64-bit JVM. ## Usage diff --git a/bindings/java/build.gradle b/bindings/java/build.gradle index b7af745b..299b3efd 100644 --- a/bindings/java/build.gradle +++ b/bindings/java/build.gradle @@ -10,8 +10,8 @@ version = System.getenv('VERSION') ?: '0.15.0-SNAPSHOT' java { withJavadocJar() withSourcesJar() - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } repositories { From 8ea6e707fcbbd7c0f91a9319b954548619d93c2b Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 14:56:52 +0200 Subject: [PATCH 07/15] wip Signed-off-by: Dmitry Dygalo --- .github/workflows/java-release.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml index 44346654..31b3ce93 100644 --- a/.github/workflows/java-release.yml +++ b/.github/workflows/java-release.yml @@ -90,8 +90,20 @@ jobs: cp ../../native-libs/native-darwin-aarch64/libcss_inline.dylib src/main/resources/org/cssinline/native/darwin-aarch64/ cp ../../native-libs/native-win32-x86_64/css_inline.dll src/main/resources/org/cssinline/native/win32-x86_64/ - # This one will also run tests - gradle build --info + echo "=== Gradle version and Java info ===" + gradle --version + + echo "=== Source files ===" + find src/main/java -name "*.java" + + echo "=== Compile Java only first ===" + gradle compileJava --stacktrace --info + + echo "=== Check compiled classes ===" + find build/classes -name "*.class" 2>/dev/null || echo "No compiled classes found!" + + echo "=== Full build ===" + gradle build --stacktrace --info echo "=== After gradle build ===" ls -la build/libs/ || echo "No build/libs directory" From 430c59ded63a5540430bce2f224108288b3f70bc Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 15:08:57 +0200 Subject: [PATCH 08/15] wip Signed-off-by: Dmitry Dygalo --- bindings/java/build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bindings/java/build.gradle b/bindings/java/build.gradle index 299b3efd..5737d712 100644 --- a/bindings/java/build.gradle +++ b/bindings/java/build.gradle @@ -18,6 +18,14 @@ repositories { mavenCentral() } +jar { + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +sourcesJar { + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' implementation 'com.google.code.gson:gson:2.10.1' From 4a26fafd53b1c823dc51362aec4ad5ba536db31a Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 15:21:33 +0200 Subject: [PATCH 09/15] wip Signed-off-by: Dmitry Dygalo --- bindings/java/build.gradle | 18 ++++++++++++++++++ bindings/java/settings.gradle | 1 + 2 files changed, 19 insertions(+) create mode 100644 bindings/java/settings.gradle diff --git a/bindings/java/build.gradle b/bindings/java/build.gradle index 5737d712..d15ad074 100644 --- a/bindings/java/build.gradle +++ b/bindings/java/build.gradle @@ -20,6 +20,24 @@ repositories { jar { duplicatesStrategy = DuplicatesStrategy.INCLUDE + + // Debug what's happening + doFirst { + println "=== JAR Task Debug ===" + println "Classes dirs exist: ${sourceSets.main.output.classesDirs.every { it.exists() }}" + println "Resources dir exists: ${sourceSets.main.output.resourcesDir?.exists()}" + + sourceSets.main.output.classesDirs.each { dir -> + if (dir.exists()) { + println "Found classes in: ${dir}" + dir.eachFileRecurse { file -> + if (file.name.endsWith('.class')) { + println " Class: ${file.name}" + } + } + } + } + } } sourcesJar { diff --git a/bindings/java/settings.gradle b/bindings/java/settings.gradle new file mode 100644 index 00000000..f2a23080 --- /dev/null +++ b/bindings/java/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'css-inline' From 5503782a9eae22f87f33556663ae6c23e03621da Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 15:28:37 +0200 Subject: [PATCH 10/15] wip Signed-off-by: Dmitry Dygalo --- .github/workflows/java-release.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml index 31b3ce93..4acf5d8b 100644 --- a/.github/workflows/java-release.yml +++ b/.github/workflows/java-release.yml @@ -114,21 +114,30 @@ jobs: echo "=== Built JARs ===" ls -la build/libs/ + # Get the main JAR (not sources or javadoc) + MAIN_JAR=$(ls build/libs/css-inline-*-SNAPSHOT.jar | grep -v sources | grep -v javadoc) + echo "=== Checking main JAR: $MAIN_JAR ===" + echo "=== JAR contents (first 30 lines) ===" - jar tf build/libs/*.jar | head -30 + jar tf "$MAIN_JAR" | head -30 echo "=== Looking for CssInline class ===" - if jar tf build/libs/*.jar | grep -q "org/cssinline/CssInline.class"; then + if jar tf "$MAIN_JAR" | grep -q "org/cssinline/CssInline.class"; then echo "✅ CssInline.class found" else echo "❌ CssInline.class NOT found!" echo "=== All .class files in JAR ===" - jar tf build/libs/*.jar | grep "\.class$" || echo "No .class files found!" + jar tf "$MAIN_JAR" | grep "\.class$" || echo "No .class files found!" exit 1 fi echo "=== Looking for native libraries ===" - jar tf build/libs/*.jar | grep -E "\.(so|dylib|dll)$" || echo "No native libraries found" + jar tf "$MAIN_JAR" | grep -E "\.(so|dylib|dll)$" || echo "No native libraries found" + + echo "=== Classes found in main JAR ===" + jar tf "$MAIN_JAR" | grep "\.class$" | wc -l + echo "=== Native libs found in main JAR ===" + jar tf "$MAIN_JAR" | grep -E "\.(so|dylib|dll)$" | wc -l - name: Upload JAR uses: actions/upload-artifact@v4 From b2da5bd177962503b4ee3266b955796183e99f5c Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 15:36:13 +0200 Subject: [PATCH 11/15] wip Signed-off-by: Dmitry Dygalo --- .github/workflows/java-release.yml | 3 ++- bindings/java/build.gradle | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml index 4acf5d8b..3eb67abd 100644 --- a/.github/workflows/java-release.yml +++ b/.github/workflows/java-release.yml @@ -197,7 +197,8 @@ jobs: } EOF - JAR_FILE=$(ls *.jar) + JAR_FILE=$(ls css-inline-*-SNAPSHOT.jar | grep -v sources | grep -v javadoc) + echo "Using JAR: $JAR_FILE" if [[ "$RUNNER_OS" == "Windows" ]]; then CLASSPATH_SEP=";" diff --git a/bindings/java/build.gradle b/bindings/java/build.gradle index d15ad074..3dbe531f 100644 --- a/bindings/java/build.gradle +++ b/bindings/java/build.gradle @@ -8,8 +8,6 @@ group = 'org.css-inline' version = System.getenv('VERSION') ?: '0.15.0-SNAPSHOT' java { - withJavadocJar() - withSourcesJar() sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } From 748bde36285d8871422976d32b6b739a54c8cafe Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 15:40:14 +0200 Subject: [PATCH 12/15] wip Signed-off-by: Dmitry Dygalo --- bindings/java/build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bindings/java/build.gradle b/bindings/java/build.gradle index 3dbe531f..9e92477a 100644 --- a/bindings/java/build.gradle +++ b/bindings/java/build.gradle @@ -38,10 +38,6 @@ jar { } } -sourcesJar { - duplicatesStrategy = DuplicatesStrategy.INCLUDE -} - dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' implementation 'com.google.code.gson:gson:2.10.1' From 2b532108a2490e647386b73e62d7488da607a6ee Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 15:48:09 +0200 Subject: [PATCH 13/15] wip Signed-off-by: Dmitry Dygalo --- .github/workflows/java-release.yml | 50 +----------------------------- bindings/java/build.gradle | 22 ------------- 2 files changed, 1 insertion(+), 71 deletions(-) diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml index 3eb67abd..9495e816 100644 --- a/.github/workflows/java-release.yml +++ b/.github/workflows/java-release.yml @@ -90,54 +90,7 @@ jobs: cp ../../native-libs/native-darwin-aarch64/libcss_inline.dylib src/main/resources/org/cssinline/native/darwin-aarch64/ cp ../../native-libs/native-win32-x86_64/css_inline.dll src/main/resources/org/cssinline/native/win32-x86_64/ - echo "=== Gradle version and Java info ===" - gradle --version - - echo "=== Source files ===" - find src/main/java -name "*.java" - - echo "=== Compile Java only first ===" - gradle compileJava --stacktrace --info - - echo "=== Check compiled classes ===" - find build/classes -name "*.class" 2>/dev/null || echo "No compiled classes found!" - - echo "=== Full build ===" - gradle build --stacktrace --info - - echo "=== After gradle build ===" - ls -la build/libs/ || echo "No build/libs directory" - - - name: Verify JAR contents - working-directory: bindings/java - run: | - echo "=== Built JARs ===" - ls -la build/libs/ - - # Get the main JAR (not sources or javadoc) - MAIN_JAR=$(ls build/libs/css-inline-*-SNAPSHOT.jar | grep -v sources | grep -v javadoc) - echo "=== Checking main JAR: $MAIN_JAR ===" - - echo "=== JAR contents (first 30 lines) ===" - jar tf "$MAIN_JAR" | head -30 - - echo "=== Looking for CssInline class ===" - if jar tf "$MAIN_JAR" | grep -q "org/cssinline/CssInline.class"; then - echo "✅ CssInline.class found" - else - echo "❌ CssInline.class NOT found!" - echo "=== All .class files in JAR ===" - jar tf "$MAIN_JAR" | grep "\.class$" || echo "No .class files found!" - exit 1 - fi - - echo "=== Looking for native libraries ===" - jar tf "$MAIN_JAR" | grep -E "\.(so|dylib|dll)$" || echo "No native libraries found" - - echo "=== Classes found in main JAR ===" - jar tf "$MAIN_JAR" | grep "\.class$" | wc -l - echo "=== Native libs found in main JAR ===" - jar tf "$MAIN_JAR" | grep -E "\.(so|dylib|dll)$" | wc -l + gradle build --info - name: Upload JAR uses: actions/upload-artifact@v4 @@ -198,7 +151,6 @@ jobs: EOF JAR_FILE=$(ls css-inline-*-SNAPSHOT.jar | grep -v sources | grep -v javadoc) - echo "Using JAR: $JAR_FILE" if [[ "$RUNNER_OS" == "Windows" ]]; then CLASSPATH_SEP=";" diff --git a/bindings/java/build.gradle b/bindings/java/build.gradle index 9e92477a..a7882a38 100644 --- a/bindings/java/build.gradle +++ b/bindings/java/build.gradle @@ -16,28 +16,6 @@ repositories { mavenCentral() } -jar { - duplicatesStrategy = DuplicatesStrategy.INCLUDE - - // Debug what's happening - doFirst { - println "=== JAR Task Debug ===" - println "Classes dirs exist: ${sourceSets.main.output.classesDirs.every { it.exists() }}" - println "Resources dir exists: ${sourceSets.main.output.resourcesDir?.exists()}" - - sourceSets.main.output.classesDirs.each { dir -> - if (dir.exists()) { - println "Found classes in: ${dir}" - dir.eachFileRecurse { file -> - if (file.name.endsWith('.class')) { - println " Class: ${file.name}" - } - } - } - } - } -} - dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' implementation 'com.google.code.gson:gson:2.10.1' From a63113878ad2d5fcc2b9aaa5e15e55797f3a3c81 Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 16:07:27 +0200 Subject: [PATCH 14/15] wip Signed-off-by: Dmitry Dygalo --- .github/workflows/java-release.yml | 36 ++++++++++++------------------ 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml index 9495e816..a5badf22 100644 --- a/.github/workflows/java-release.yml +++ b/.github/workflows/java-release.yml @@ -50,6 +50,12 @@ jobs: with: targets: ${{ matrix.target }} + - uses: Swatinem/rust-cache@v2 + with: + workspaces: | + css-inline + bindings/java + - name: Build native library working-directory: bindings/java run: cargo build --release --target ${{ matrix.target }} @@ -133,24 +139,17 @@ jobs: import org.cssinline.CssInline; public class Test { public static void main(String[] args) { - try { - String html = "

Test

"; - String result = CssInline.inline(html); - if (!result.contains("style=\"color: red;\"")) { - System.err.println("Expected inlined style not found in: " + result); - System.exit(1); - } - System.out.println("✓ Integration test passed on ${{ matrix.platform }}"); - } catch (Exception e) { - System.err.println("Integration test failed: " + e.getMessage()); - e.printStackTrace(); - System.exit(1); + String html = "

Test

"; + String result = CssInline.inline(html); + if (!result.contains("style=\"color: red;\"")) { + throw new RuntimeException("Expected inlined style not found in: " + result); } + System.out.println("✓ Integration test passed on ${{ matrix.platform }}"); } } EOF - JAR_FILE=$(ls css-inline-*-SNAPSHOT.jar | grep -v sources | grep -v javadoc) + JAR_FILE=$(ls *.jar) if [[ "$RUNNER_OS" == "Windows" ]]; then CLASSPATH_SEP=";" @@ -182,24 +181,17 @@ jobs: uses: actions/download-artifact@v4 with: name: java-jar - path: build/libs + path: bindings/java/build/libs - name: Extract version run: echo "version=${GITHUB_REF#refs/tags/java-v}" >> $GITHUB_ENV - name: Publish to GitHub Packages working-directory: bindings/java - run: | - # Copy the pre-built JAR to the expected location - mkdir -p build/libs - cp ../../build/libs/*.jar build/libs/ - - # Publish using the existing JAR - gradle publish -Pversion=${{ env.version }} + run: gradle publish -Pversion=${{ env.version }} env: GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ env.version }} release: name: Create GitHub Release From 8d6796ac1a4569ebe2345aca32cb61b7f810682d Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 29 Jun 2025 16:14:50 +0200 Subject: [PATCH 15/15] wip Signed-off-by: Dmitry Dygalo --- .github/workflows/java-release.yml | 1 - bindings/java/README.md | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml index a5badf22..58de762e 100644 --- a/.github/workflows/java-release.yml +++ b/.github/workflows/java-release.yml @@ -1,7 +1,6 @@ name: "[Java] Release" on: - pull_request: {} push: tags: - java-v* diff --git a/bindings/java/README.md b/bindings/java/README.md index 0408894a..8232ec33 100644 --- a/bindings/java/README.md +++ b/bindings/java/README.md @@ -45,6 +45,8 @@ into: This package is available on [GitHub Packages](https://github.com/Stranger6667/css-inline/packages). +> Maven Central publishing is in the works + ### Setup GitHub Packages requires authentication even for public packages. See the [GitHub documentation](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#authenticating-to-github-packages) for authentication setup. @@ -147,6 +149,8 @@ public class ConfigExample { - **`setBaseUrl(String)`** - Base URL for resolving relative URLs (default: `null`) - **`setLoadRemoteStylesheets(boolean)`** - Load external stylesheets (default: `true`) - **`setExtraCss(String)`** - Additional CSS to inline (default: `null`) +- **`setCacheSize(int)`** - External stylesheet cache size, must be ≥ 0 (default: `0`) +- **`setPreallocateNodeCapacity(int)`** - HTML node capacity, must be > 0 (default: `32`) ### HTML Fragments