Parse wild HTML with JSoup, process with LuvML's type-safe DSL - the best of both worlds
A lightweight bridge (~3 classes, ~200 lines) that connects JSoup's robust HTML parsing with LuvML's type-safe DOM and powerful DSL. Define custom semantic elements, register them with one line, and get full compile-time type safety with pattern matching.
- JSoup: Great for parsing messy HTML, but no type safety for custom elements
- LuvML: Powerful type-safe DOM with sealed types, but needs HTML parsing
- JAXB: Heavy reflection, verbose API, 15+ lines for what should be 4
// One-line registration
def(BlogPost.class, BlogPost::new)
// Parse and process with type safety
var fragments = converter.convertMixedFragment(html);
// Exhaustive pattern matching, zero casts
switch (element) {
case BlogPost post -> post.slug() // Type-safe!
case CodeBlock code -> code.language() // No casting!
}<dependency>
<groupId>io.github.xyz-jphil</groupId>
<artifactId>xyz-jphil-luvml-jsoup</artifactId>
<version>2.0</version>
</dependency>import static luvml.jsoup2luvml.SemanticElementConverter.*;
import static luvml.E.*; // Standard HTML DSL
import luvml.element.SemanticBlockContainerElement;
import luvml.element.SemanticElementTagNameClassNameMapping.CamelCase_E;
// 1. Define custom element with attribute constants and tag name mapping
public class ProductCard_E extends SemanticBlockContainerElement<ProductCard_E> implements CamelCase_E {
public ProductCard_E() { super(ProductCard_E.class); }
public static final String $sku = "sku";
public static final String $category = "category";
public String sku() { return attr($sku); }
public String category() { return attr($category); }
}
// 2. Create DSL functions (element + attributes)
import static ProductCard_E.*;
import static luvml.E.*;
import static luvml.T.text;
public class ProductDsl {
public static ProductCard_E productCard(Frag_I<?>... content) {
return new ProductCard_E().____(content);
}
public static HtmlAttribute sku(String value) {
return new HtmlAttribute($sku, value);
}
public static HtmlAttribute category(String value) {
return new HtmlAttribute($category, value);
}
}
// 3. Register (one line!)
var converter = semanticElementConverter(
def(ProductCard_E.class, ProductCard_E::new)
);
// 4. Parse HTML
String html = """
<productCard sku="ABC-123" category="electronics">
<h3>Wireless Headphones</h3>
<p class="price">$99.99</p>
</productCard>
""";
var fragments = converter.convertMixedFragment(html);
// 5. Type-safe processing with pattern matching
import static ProductDsl.*;
for (var node : fragments) {
switch (node.nodeType()) {
case Element_T e -> {
if (e.element() instanceof ProductCard_E card) {
System.out.println("SKU: " + card.sku());
System.out.println("Category: " + card.category());
}
}
}
}
// 6. Or create programmatically with DSL - bidirectional!
var newProduct = productCard(
sku("XYZ-789"),
category("audio"),
h3(text("Bluetooth Speaker")),
p(className("price"), text("$149.99"))
);
String renderedHtml = HtmlRenderer.render(newProduct);
// Output: <productCard sku="XYZ-789" category="audio">...</productCard>Key Points:
- Class name
ProductCard_Eβ XML tag<productCard>(camelCase, _E suffix removed) - Implement
CamelCase_Einterface for automatic tag name mapping - Attribute constants (
$sku,$category) for type-safe bidirectional processing - DSL functions for both elements and attributes - import statically for clean syntax
- All text must use
text()wrapper - no mixing String and Frag_I varargs
Tag Name Convention - Composable Mapping Interfaces:
LuvML 2.0 introduces SemanticElementTagNameClassNameMapping with multiple built-in styles:
CamelCase_E:ProductCard_Eβ<productCard>(JSX/React style)LowerCase_E:ProductCard_Eβ<productcard>(standard HTML)LowerKebabFromCamelCase_E:ProductCard_Eβ<product-card>(web components)XmlNamespaceColonAtUnderscores_E:Blog_Post_Eβ<blog:post>(XML namespaces)PreserveCase_E:ProductCard_Eβ<ProductCard>(custom XML)
Simply implement the appropriate interface to control tag name generation without overriding methods!
import luvml.element.SemanticElementTagNameClassNameMapping.CamelCase_E;
// Custom elements - class name BlogPost_E β XML tag <blogPost>
public class BlogPost_E extends SemanticBlockContainerElement<BlogPost_E> implements CamelCase_E {
public BlogPost_E() { super(BlogPost_E.class); }
public static final String $slug = "slug";
public static final String $author = "author";
public static final String $publishDate = "publishDate";
public String slug() { return attr($slug); }
public Optional<String> author() { return attribute($author).optString(); }
public Optional<String> publishDate() { return attribute($publishDate).optString(); }
}
public class CodeSnippet_E extends SemanticBlockContainerElement<CodeSnippet_E> implements CamelCase_E {
public CodeSnippet_E() { super(CodeSnippet_E.class); }
public static final String $language = "language";
public static final String $showLineNumbers = "showLineNumbers";
public String language() { return attr($language); }
public boolean showLineNumbers() {
return attribute($showLineNumbers).optBoolean().orElse(false);
}
}
public class InfoBox_E extends SemanticBlockContainerElement<InfoBox_E> implements CamelCase_E {
public InfoBox_E() { super(InfoBox_E.class); }
public static final String $boxType = "boxType";
public static final String $icon = "icon";
public String boxType() { return attr($boxType); }
public Optional<String> icon() { return attribute($icon).optString(); }
}import static luvml.T.text;
import static luvml.E.*;
import static BlogPost_E.*;
import static CodeSnippet_E.*;
import static InfoBox_E.*;
// DSL factory functions for elements
public class BlogDsl {
public static BlogPost_E blogPost(Frag_I<?>... content) {
return new BlogPost_E().____(content);
}
public static CodeSnippet_E codeSnippet(Frag_I<?>... content) {
return new CodeSnippet_E().____(content);
}
public static InfoBox_E infoBox(Frag_I<?>... content) {
return new InfoBox_E().____(content);
}
// DSL factory functions for attributes
public static HtmlAttribute slug(String value) {
return new HtmlAttribute($slug, value);
}
public static HtmlAttribute author(String value) {
return new HtmlAttribute($author, value);
}
public static HtmlAttribute publishDate(String value) {
return new HtmlAttribute($publishDate, value);
}
public static HtmlAttribute language(String value) {
return new HtmlAttribute($language, value);
}
public static HtmlAttribute showLineNumbers(boolean value) {
return new HtmlAttribute($showLineNumbers, String.valueOf(value));
}
public static HtmlAttribute boxType(String value) {
return new HtmlAttribute($boxType, value);
}
public static HtmlAttribute icon(String value) {
return new HtmlAttribute($icon, value);
}
}var converter = semanticElementConverter(
def(BlogPost_E.class, BlogPost_E::new),
def(CodeSnippet_E.class, CodeSnippet_E::new),
def(InfoBox_E.class, InfoBox_E::new)
);String html = """
<blogPost slug="java-21-features" author="tech-blogger" publishDate="2024-09-30">
<h1>Awesome Java 21 Features</h1>
<infoBox boxType="tip" icon="π‘">
This article covers the latest features in Java 21
</infoBox>
<p>Pattern matching has revolutionized how we write Java:</p>
<codeSnippet language="java" showLineNumbers="true">
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
};
}
</codeSnippet>
<p>Notice how the compiler enforces <strong>exhaustive checking</strong>!</p>
</blogPost>
""";
var fragments = converter.convertMixedFragment(html);void processBlogPost(Frags fragments) {
for (var node : fragments) {
switch (node.nodeType()) {
case Element_T e -> {
switch (e.element()) {
case BlogPost_E post -> {
System.out.println("π Post: " + post.slug());
System.out.println("βοΈ Author: " + post.author().orElse("Anonymous"));
System.out.println("π
Date: " + post.publishDate().orElse("N/A"));
processChildren(post);
}
case CodeSnippet_E code -> {
System.out.println("π» Code (" + code.language() + "):");
if (code.showLineNumbers()) {
renderWithLineNumbers(code);
} else {
renderPlain(code);
}
}
case InfoBox_E box -> {
String icon = box.icon().orElse("βΉοΈ");
System.out.println(icon + " " + box.boxType().toUpperCase());
renderBoxContent(box);
}
case luvml.H1 h1 ->
System.out.println("# " + extractText(h1));
case luvml.P para ->
System.out.println(extractText(para));
case luvml.Strong strong ->
System.out.print("**" + extractText(strong) + "**");
default -> {} // Handle other standard HTML elements
}
}
case AttributelessNode_T a -> {
switch (a.attributelessNodeType()) {
case StringNode_T s -> {
switch (s.stringNodeType()) {
case Text_T t -> System.out.print(t.text().textContent());
default -> {}
}
}
default -> {}
}
}
}
}
}// β Stringly-typed, no safety, error-prone
String html = "<blogPost slug=\"" + slug + "\" author=\"" + author + "\">" +
"<h1>" + escapeHtml(title) + "</h1>" +
"<codeSnippet language=\"java\">" + escapeHtml(code) + "</codeSnippet>" +
"</blogPost>";import static luvml.E.*; // Standard HTML elements
import static luvml.A.*; // Standard HTML attributes
import static luvml.T.text; // Text nodes
import static BlogDsl.*; // Custom blog DSL
// β
Type-safe, composable, compiler-validated
var post = blogPost(
slug("java-21-features"),
author("tech-blogger"),
h1(text("Awesome Java 21 Features")),
infoBox(
boxType("tip"),
icon("π‘"),
text("This article covers Java 21")
),
p(text("Pattern matching has revolutionized Java")),
codeSnippet(
language("java"),
showLineNumbers(true),
text("""
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
""")
)
);
// Render to HTML string
String html = HtmlRenderer.render(post);import static BlogDsl.*;
import static luvml.E.*;
import static luvml.T.text;
// Create with DSL - Type-safe construction
var post = blogPost(
slug("java-21-features"),
author("tech-blogger"),
h1(text("Awesome Java 21 Features")),
infoBox(
boxType("tip"),
icon("π‘"),
text("This article covers Java 21")
),
p(text("Pattern matching has revolutionized Java")),
codeSnippet(
language("java"),
showLineNumbers(true),
text("""
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
""")
)
);
// Render to HTML
String html = HtmlRenderer.render(post);
// Parse back with JSoup
var reparsed = converter.convertMixedFragment(html);
// Process with exhaustive pattern matching
for (var node : reparsed) {
switch (node.nodeType()) {
case Element_T e -> {
switch (e.element()) {
case BlogPost_E blogPost -> {
// Type-safe attribute access
String slug = blogPost.slug();
Optional<String> author = blogPost.author();
// Process children with type safety
for (var child : blogPost.childNodes()) {
switch (child.nodeType()) {
case Element_T ce -> {
switch (ce.element()) {
case CodeSnippet_E code ->
highlightCode(code.language(), extractText(code));
case InfoBox_E box ->
renderAlert(box.boxType(), extractText(box));
case luvml.H1 h1 ->
renderHeading(extractText(h1));
case luvml.P p ->
renderParagraph(extractText(p));
default -> {}
}
}
default -> {}
}
}
}
default -> {}
}
}
default -> {}
}
}Bidirectional guarantees:
- DSL construction β Type-safe element/attribute creation
- HTML parsing β Type-safe element/attribute extraction
- Pattern matching β Exhaustive, compiler-enforced
- Zero casts, zero reflection!
This is the power: Parse messy external HTML with JSoup, generate clean semantic HTML with LuvML DSL - all type-safe, all compile-time checked!
External HTML String
β (JSoup parse)
JSoup Document
β (LuvML-JSoup convert)
LuvML Type-Safe DOM ββ LuvML DSL Construction
β (HtmlRenderer)
Clean HTML Output
Bidirectional transformations, all type-safe:
- HTML β LUVML DOM (via JSoup + this bridge)
- LUVML DSL β LUVML DOM (via fluent builders)
- LUVML DOM β HTML (via renderer)
| Feature | LuvML-JSoup | JAXB |
|---|---|---|
| Registration | def(Class, Constructor) |
@XmlRootElement + complex annotations |
| Runtime Cost | Zero reflection | Heavy reflection |
| Type Safety | Sealed types + exhaustive switch | Manual instanceof chains |
| Syntax | blogPost(slug("abc"), h1("Title")) |
post.setSlug("abc"); post.getContent().add(...) |
| Lines of Code | ~4-5 lines | ~15+ lines (same logic) |
| Custom Elements | First-class, one-line registration | Complex XSD binding |
| DSL Support | Native | None (setters only) |
| Pattern Matching | Exhaustive, compiler-enforced | Manual, error-prone |
JAXB would have been a mistake. This design is faster, safer, and more maintainable.
All container elements now support textContent() via the TextContentProvider interface, mirroring HTML's standard textContent property:
var article = blogPost(
slug("my-post"),
h1(text("Title")),
p(text("First "), strong(text("bold")), text(" word")),
comment("Ignored in text extraction")
);
String plainText = article.textContent();
// Result: "TitleFirst bold word"New findChild() and findChildren() methods provide type-safe access to nested elements:
public class BlogPost_E extends SemanticBlockContainerElement<BlogPost_E> implements CamelCase_E {
// ... attribute methods ...
// Find first CodeSnippet child
public Optional<CodeSnippet_E> firstCodeSnippet() {
return findChild(CodeSnippet_E.class);
}
// Find all InfoBox children
public List<InfoBox_E> allInfoBoxes() {
return findChildren(InfoBox_E.class);
}
}
// Usage
var post = blogPost(...);
post.firstCodeSnippet().ifPresent(code -> {
System.out.println("Language: " + code.language());
});The SemanticElementTagNameClassNameMapping interface system provides multiple strategies:
// Mix different naming conventions in same project
public class ReactButton_E extends SemanticInlineContainerElement<ReactButton_E>
implements CamelCase_E {
// β <reactButton>
}
public class WebButton_E extends SemanticInlineContainerElement<WebButton_E>
implements LowerKebabFromCamelCase_E {
// β <web-button>
}
public class Svg_Circle_E extends SemanticBlockContainerElement<Svg_Circle_E>
implements XmlNamespaceColonAtUnderscores_E {
// β <svg:circle>
}// No casting needed - types preserved through sealed hierarchy
switch (node.nodeType()) {
case Element_T e -> {
var element = e.element(); // Type: Element_I - NO CAST!
switch (element) {
case BlogPost_E post ->
post.slug() // Direct access, no cast
case CodeSnippet_E code ->
code.language() // Direct access, no cast
}
}
}// Compiler ensures ALL types are handled
switch (element) {
case BlogPost_E post -> // ...
case CodeSnippet_E code -> // ...
case InfoBox_E box -> // ...
// Forget a case? Compilation error!
}// Constructor reference evaluated at registration - O(1) HashMap lookup at runtime
def(BlogPost_E.class, BlogPost_E::new)
// β Caches tagName ("blogPost") - _E suffix removed automatically
// β Runtime: HashMap.get("blogPost") β constructor.get()
// β Zero reflection!// Create converter with custom elements
var converter = semanticElementConverter(
def(CustomElement.class, CustomElement::new),
def(AnotherElement.class, AnotherElement::new)
);
// Convert HTML fragment
Frags fragments = converter.convertMixedFragment(htmlString);
// Convert single JSoup element
Node_I<?> node = converter.createSemanticElement(jsoupElement);// Definition holds class + constructor reference
SemanticElementDef<BlogPost> def = def(BlogPost.class, BlogPost::new);
String tagName = def.tagName(); // "blogPost"
boolean isVoid = def.isVoidType(); // false
Supplier<BlogPost> ctor = def.constructor();- Java 21+ (sealed types, pattern matching, switch expressions)
- LuvML 2.0+ (type-safe HTML DOM)
- JSoup 1.17+ (robust HTML parsing)
- LuvX Base: github.com/xyz-jphil/xyz-jphil-luvx-base
- LuvML Core: github.com/xyz-jphil/xyz-jphil-luvml
- LuvML-JSoup: github.com/xyz-jphil/xyz-jphil-luvml-jsoup
No specific license specified. Assume MIT or Apache 2.0 for maximum permissiveness.
LuvML-JSoup is a tiny bridge (~3 classes) that unlocks massive power:
β¨ One-line registration: def(Class, Constructor)
π Zero-cast type safety via sealed types
β‘ Zero reflection - pure static dispatch
π― Exhaustive pattern matching
π Bidirectional: Parse with JSoup, build with DSL
π¦ Tiny library, massive leverage from LuvML foundation
This is world-class Java API design - type safety without verbosity, performance without reflection.
If you believe Java deserves elegant DSLs, star β this project!