+
+```
+
+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..a7882a38
--- /dev/null
+++ b/bindings/java/build.gradle
@@ -0,0 +1,108 @@
+plugins {
+ 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'
+
+java {
+ 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 {
+ 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'
+ }
+ }
+ }
+ }
+}
+
+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/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'
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