Skip to content

Commit 6a83b00

Browse files
committed
feat: Java bindings
Signed-off-by: Dmitry Dygalo <[email protected]>
1 parent a487b68 commit 6a83b00

File tree

17 files changed

+1112
-1
lines changed

17 files changed

+1112
-1
lines changed

.github/workflows/build.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,30 @@ jobs:
438438
set -e
439439
yarn test
440440
441+
test-java:
442+
name: Java JNI tests
443+
runs-on: ubuntu-22.04
444+
steps:
445+
- uses: actions/checkout@v4
446+
447+
- run: cargo build --release
448+
working-directory: bindings/java
449+
450+
- name: Set up Java
451+
uses: actions/setup-java@v4
452+
with:
453+
distribution: temurin
454+
java-version: 21
455+
456+
- name: Setup Gradle
457+
uses: gradle/actions/setup-gradle@v4
458+
459+
- name: Run Java tests
460+
working-directory: bindings/java
461+
run: |
462+
echo "nativeLibraryPath=../target/release" >> gradle.properties
463+
gradle clean test --no-daemon
464+
441465
test-python:
442466
strategy:
443467
fail-fast: false

.github/workflows/java-release.yml

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
name: "[Java] Release"
2+
3+
on:
4+
pull_request: {}
5+
push:
6+
tags:
7+
- java-v*
8+
9+
defaults:
10+
run:
11+
shell: bash
12+
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
env:
18+
JAVA_VERSION: "21"
19+
20+
jobs:
21+
build-native-libs:
22+
strategy:
23+
matrix:
24+
include:
25+
- os: ubuntu-22.04
26+
target: x86_64-unknown-linux-gnu
27+
lib: libcss_inline.so
28+
platform: linux-x86_64
29+
30+
- os: macos-13
31+
target: x86_64-apple-darwin
32+
lib: libcss_inline.dylib
33+
platform: darwin-x86_64
34+
- os: macos-14
35+
target: aarch64-apple-darwin
36+
lib: libcss_inline.dylib
37+
platform: darwin-aarch64
38+
39+
- os: windows-2022
40+
target: x86_64-pc-windows-msvc
41+
lib: css_inline.dll
42+
platform: win32-x86_64
43+
44+
runs-on: ${{ matrix.os }}
45+
steps:
46+
- uses: actions/checkout@v4
47+
48+
- uses: dtolnay/rust-toolchain@master
49+
with:
50+
toolchain: stable
51+
targets: ${{ matrix.target }}
52+
53+
- name: Build native library
54+
working-directory: bindings/java
55+
run: cargo build --release --target ${{ matrix.target }}
56+
57+
- name: Upload native library
58+
uses: actions/upload-artifact@v4
59+
with:
60+
name: native-${{ matrix.platform }}
61+
if-no-files-found: error
62+
path: bindings/java/target/${{ matrix.target }}/release/${{ matrix.lib }}
63+
64+
build-and-test-jar:
65+
runs-on: ubuntu-22.04
66+
needs: build-native-libs
67+
steps:
68+
- uses: actions/checkout@v4
69+
70+
- name: Set up Java
71+
uses: actions/setup-java@v4
72+
with:
73+
distribution: "temurin"
74+
java-version: ${{ env.JAVA_VERSION }}
75+
76+
- name: Download all native libraries
77+
uses: actions/download-artifact@v4
78+
with:
79+
path: native-libs
80+
81+
- name: Assemble JAR with native libraries
82+
working-directory: bindings/java
83+
run: |
84+
# Create resources directory structure
85+
mkdir -p src/main/resources/org/cssinline/native/{linux-x86_64,darwin-x86_64,darwin-aarch64,win32-x86_64}
86+
87+
# Copy native libraries to correct locations
88+
cp ../../native-libs/native-linux-x86_64/libcss_inline.so src/main/resources/org/cssinline/native/linux-x86_64/
89+
cp ../../native-libs/native-darwin-x86_64/libcss_inline.dylib src/main/resources/org/cssinline/native/darwin-x86_64/
90+
cp ../../native-libs/native-darwin-aarch64/libcss_inline.dylib src/main/resources/org/cssinline/native/darwin-aarch64/
91+
cp ../../native-libs/native-win32-x86_64/css_inline.dll src/main/resources/org/cssinline/native/win32-x86_64/
92+
93+
# Build JAR (this runs tests against assembled JAR with native libs)
94+
gradle build
95+
96+
- name: Upload JAR artifact
97+
uses: actions/upload-artifact@v4
98+
with:
99+
name: java-jar
100+
if-no-files-found: error
101+
path: bindings/java/build/libs/*.jar
102+
103+
test-platforms:
104+
needs: build-and-test-jar
105+
strategy:
106+
matrix:
107+
include:
108+
- os: ubuntu-22.04
109+
platform: linux-x86_64
110+
- os: macos-13
111+
platform: darwin-x86_64
112+
- os: macos-14
113+
platform: darwin-aarch64
114+
- os: windows-2022
115+
platform: win32-x86_64
116+
117+
runs-on: ${{ matrix.os }}
118+
steps:
119+
- name: Set up Java
120+
uses: actions/setup-java@v4
121+
with:
122+
distribution: "temurin"
123+
java-version: ${{ env.JAVA_VERSION }}
124+
125+
- name: Download JAR
126+
uses: actions/download-artifact@v4
127+
with:
128+
name: java-jar
129+
130+
- name: Integration test on ${{ matrix.platform }}
131+
run: |
132+
cat > Test.java << 'EOF'
133+
import org.cssinline.CssInline;
134+
public class Test {
135+
public static void main(String[] args) {
136+
try {
137+
String html = "<html><head><style>h1{color:red}</style></head><body><h1>Test</h1></body></html>";
138+
String result = CssInline.inline(html);
139+
if (!result.contains("style=\"color: red;\"")) {
140+
System.err.println("Expected inlined style not found in: " + result);
141+
System.exit(1);
142+
}
143+
System.out.println("✓ Integration test passed on ${{ matrix.platform }}");
144+
} catch (Exception e) {
145+
System.err.println("Integration test failed: " + e.getMessage());
146+
e.printStackTrace();
147+
System.exit(1);
148+
}
149+
}
150+
}
151+
EOF
152+
153+
JAR_FILE=$(ls *.jar)
154+
155+
if [[ "$RUNNER_OS" == "Windows" ]]; then
156+
CLASSPATH_SEP=";"
157+
else
158+
CLASSPATH_SEP=":"
159+
fi
160+
161+
javac -cp "$JAR_FILE" Test.java
162+
java -cp "$JAR_FILE${CLASSPATH_SEP}." Test

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ into:
4141
- Optionally caches external stylesheets
4242
- Works on Linux, Windows, and macOS
4343
- Supports HTML5 & CSS3
44-
- 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.
44+
- 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.
4545
- Command Line Interface
4646

4747
## Playground

bindings/java/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.gradle/
2+
build/

bindings/java/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog
2+
3+
## Unreleased
4+
5+
- Initial public release

bindings/java/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "css_inline"
3+
version = "0.15.0"
4+
edition = "2024"
5+
authors = ["Dmitry Dygalo <[email protected]>"]
6+
7+
[lib]
8+
crate-type = ["cdylib"]
9+
path = "src/main/rust/lib.rs"
10+
11+
[dependencies]
12+
jni = "0.21.1"
13+
14+
[dependencies.css-inline]
15+
path = "../../css-inline"
16+
version = "*"
17+
default-features = false
18+
features = ["http", "file", "stylesheet-cache"]

bindings/java/README.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# css-inline
2+
3+
[<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/Stranger6667/css-inline/build.yml?style=flat-square&labelColor=555555&logo=github" height="20">](https://github.com/Stranger6667/css-inline/actions/workflows/build.yml)
4+
[<img alt="maven central" src="https://img.shields.io/maven-central/v/org.cssinline/css-inline.svg?style=flat-square&color=fc8d62&logo=apachemaven" height="20">](#installation)
5+
[<img alt="javadoc" src="https://img.shields.io/badge/javadoc-css--inline-66c2a5?style=flat-square&labelColor=555555" height="20">](#documentation)
6+
7+
Java bindings for the high-performance `css-inline` library that inlines CSS into HTML 'style' attributes.
8+
9+
This library is designed for scenarios such as preparing HTML emails or embedding HTML into third-party web pages.
10+
11+
Transforms HTML like this:
12+
13+
```html
14+
<html>
15+
<head>
16+
<style>h1 { color:blue; }</style>
17+
</head>
18+
<body>
19+
<h1>Big Text</h1>
20+
</body>
21+
</html>
22+
```
23+
24+
into:
25+
26+
```html
27+
<html>
28+
<head></head>
29+
<body>
30+
<h1 style="color:blue;">Big Text</h1>
31+
</body>
32+
</html>
33+
```
34+
35+
## Features
36+
37+
- Uses reliable components from Mozilla's Servo project
38+
- Inlines CSS from `style` and `link` tags
39+
- Removes `style` and `link` tags
40+
- Resolves external stylesheets (including local files)
41+
- Optionally caches external stylesheets
42+
- Works on Linux, Windows, and macOS
43+
- Supports HTML5 & CSS3
44+
45+
## Installation
46+
47+
**Maven:**
48+
```xml
49+
<dependency>
50+
<groupId>org.css-inline</groupId>
51+
<artifactId>css-inline</artifactId>
52+
<version>0.15.0</version>
53+
</dependency>
54+
```
55+
56+
**Gradle:**
57+
```gradle
58+
implementation 'org.css-inline:css-inline:0.15.0'
59+
```
60+
61+
## Usage
62+
63+
```java
64+
import org.cssinline.CssInline;
65+
66+
public class Example {
67+
public static void main(String[] args) {
68+
String html = """
69+
<html>
70+
<head>
71+
<style>h1 { color:blue; }</style>
72+
</head>
73+
<body>
74+
<h1>Big Text</h1>
75+
</body>
76+
</html>""";
77+
78+
String inlined = CssInline.inline(html);
79+
System.out.println(inlined);
80+
}
81+
}
82+
```
83+
84+
You can also configure the inlining process:
85+
86+
```java
87+
import org.cssinline.CssInline;
88+
import org.cssinline.CssInlineConfig;
89+
90+
public class ConfigExample {
91+
public static void main(String[] args) {
92+
CssInlineConfig config = new CssInlineConfig.Builder()
93+
.loadRemoteStylesheets(false)
94+
.keepStyleTags(true)
95+
.baseUrl("https://example.com/")
96+
.build();
97+
98+
String inlined = CssInline.inline(html, config);
99+
}
100+
}
101+
```
102+
103+
- **`inlineStyleTags(boolean)`** - Inline CSS from `<style>` tags (default: `true`)
104+
- **`keepStyleTags(boolean)`** - Keep `<style>` tags after inlining (default: `false`)
105+
- **`keepLinkTags(boolean)`** - Keep `<link>` tags after inlining (default: `false`)
106+
- **`baseUrl(String)`** - Base URL for resolving relative URLs (default: `null`)
107+
- **`loadRemoteStylesheets(boolean)`** - Load external stylesheets (default: `true`)
108+
- **`extraCss(String)`** - Additional CSS to inline (default: `null`)
109+
110+
You can also skip CSS inlining for an HTML tag by adding the `data-css-inline="ignore"` attribute to it:
111+
112+
```html
113+
<head>
114+
<style>h1 { color:blue; }</style>
115+
</head>
116+
<body>
117+
<!-- The tag below won't receive additional styles -->
118+
<h1 data-css-inline="ignore">Big Text</h1>
119+
</body>
120+
```
121+
122+
The `data-css-inline="ignore"` attribute also allows you to skip `link` and `style` tags:
123+
124+
```html
125+
<head>
126+
<!-- Styles below are ignored -->
127+
<style data-css-inline="ignore">h1 { color:blue; }</style>
128+
</head>
129+
<body>
130+
<h1>Big Text</h1>
131+
</body>
132+
```
133+
134+
Alternatively, you may keep `style` from being removed by using the `data-css-inline="keep"` attribute.
135+
This is useful if you want to keep `@media` queries for responsive emails in separate `style` tags:
136+
137+
```html
138+
<head>
139+
<!-- Styles below are not removed -->
140+
<style data-css-inline="keep">h1 { color:blue; }</style>
141+
</head>
142+
<body>
143+
<h1>Big Text</h1>
144+
</body>
145+
```
146+
147+
Such tags will be kept in the resulting HTML even if the `keepStyleTags` option is set to `false`.
148+
149+
## Performance
150+
151+
`css-inline` is powered by efficient tooling from Mozilla's Servo project and significantly outperforms Java alternatives.
152+
153+
Here is the performance comparison:
154+
155+
| | Size | `css-inline 0.15.0` | `CSSBox 5.0.0` |
156+
|-------------|---------|---------------------|------------------------|
157+
| Basic | 230 B | 7.67 µs | 209.93 µs (**27.37x**) |
158+
| Realistic-1 | 8.58 KB | 123.18 µs | 1.92 ms (**15.58x**) |
159+
| Realistic-2 | 4.3 KB | 77.74 µs | 608.65 µs (**7.82x**) |
160+
| GitHub page | 1.81 MB | 168.43 ms | 316.21 ms (**1.87x**) |
161+
162+
The benchmarking code is available in the `src/jmh/java/orf/cssinline/CSSInlineBench.java` file. The benchmarks were conducted using the stable `rustc 1.87`, `OpenJDK 24.0.1` on Ryzen 9 9950X.
163+
164+
## License
165+
166+
This project is licensed under the terms of the [MIT license](https://opensource.org/licenses/MIT).

0 commit comments

Comments
 (0)