Web component support (#211)

* Foundational components for reading/processing Wattsi files into classes.

* Introducing tests for compliance with wattsi definitions.

- Several elements have been found to be missing from the code generator.
- A long-term strategy for dealing with obsolete elements will be needed.  Should we remove and change the API?  Or should we deprecate and leave in-place?
- Found one element that was accidentally added to the code generator but never used.  Hopefully.
- Fixes will be applied in following commits.

* Added newer elements/tags and cleaned up invalid Tag class.

- Added DataTag.
- Added SlotTag and TemplateTag to support web components.
- Added global attributes to support web components.
- Removed GenerateTag.  Accidentally introduced by string replacement in code generator.

* Upgrading revapi and maven plugin. Configured revapi to ignore removal of GenerateTag.

- The GenerateTag class was created accidentally.  It does not have any representation in the HTML standard is should not be provided in this library.
This commit is contained in:
Scott Embler
2022-06-29 20:13:46 -04:00
committed by GitHub
parent c2177d0584
commit d1c404d5db
19 changed files with 129126 additions and 82 deletions

View File

@@ -63,7 +63,7 @@ public final class TagCreatorCodeGenerator {
}
// This is a method that contains all ContainerTags, there is nothing below it
static List<String> emptyTags() {
public static List<String> emptyTags() {
return Arrays.asList(
"area",
"base",
@@ -84,7 +84,7 @@ public final class TagCreatorCodeGenerator {
);
}
static List<String> containerTags() {
public static List<String> containerTags() {
return Arrays.asList(
"a",
"abbr",
@@ -103,6 +103,7 @@ public final class TagCreatorCodeGenerator {
"cite",
"code",
"colgroup",
"data",
"datalist",
"dd",
"del",
@@ -134,7 +135,6 @@ public final class TagCreatorCodeGenerator {
"label",
"legend",
"li",
"generate",
"main",
"map",
"mark",
@@ -161,6 +161,7 @@ public final class TagCreatorCodeGenerator {
"script",
"section",
"select",
"slot",
"small",
"span",
"strong",
@@ -171,6 +172,7 @@ public final class TagCreatorCodeGenerator {
"table",
"tbody",
"td",
"template",
"textarea",
"tfoot",
"th",

View File

@@ -0,0 +1,94 @@
package j2html_codegen.generators;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import j2html_codegen.wattsi.AttributeDefinition;
import j2html_codegen.wattsi.ElementDefinition;
import j2html_codegen.wattsi.WattsiSource;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import javax.lang.model.element.Modifier;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class WattsiGenerator {
public static void main(String... args) throws IOException {
Path source = Paths.get(args[0]);
Document doc = Jsoup.parse(source.toFile(), "UTF-8", "https://html.spec.whatwg.org/");
WattsiSource wattsi = new WattsiSource(doc);
List<ElementDefinition> elements = wattsi.elementDefinitions();
List<AttributeDefinition> attributes = wattsi.attributeDefinitions();
// for (ElementDefinition element : elements) {
// System.out.println((element.isObsolete() ? "!" : "") + element.name());
// for (AttributeDefinition attribute : attributes) {
// if (attribute.appliesTo(element)) {
// System.out.println(" " + (attribute.isObsolete() ? "!" : "") + attribute.name());
// }
// }
// System.out.println();
// }
for (ElementDefinition element : elements) {
ClassName className = ClassName.get(
"com.j2html",
capitalize(element.name()) + "Tag"
);
TypeSpec.Builder type = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC);
if (element.isObsolete()) {
type.addAnnotation(Deprecated.class);
}
for (AttributeDefinition attribute : attributes) {
if (attribute.appliesTo(element)) {
String name = methodName("with", attribute.name().split("-"));
MethodSpec.Builder setter = MethodSpec.methodBuilder(name)
.addModifiers(Modifier.PUBLIC)
.returns(className)
.addStatement("return this");
if(attribute.isObsolete()){
setter.addAnnotation(Deprecated.class);
}
type.addMethod(setter.build());
}
}
// System.out.println(type.build().toString());
}
System.out.println(doc.select("dfn"));
}
private static String methodName(String prefix, String... words){
String[] tmp = new String[words.length + 1];
tmp[0] = prefix;
for(int i = 0; i < words.length; i++){
tmp[i+1] = words[i];
}
return methodName(tmp);
}
private static String methodName(String... words){
String[] camelCase = new String[words.length];
camelCase[0] = words[0];
for(int i = 1; i < words.length; i++){
camelCase[i] = capitalize(words[i]);
}
return String.join("", camelCase);
}
private static String capitalize(String word){
return word.substring(0,1).toUpperCase() + word.substring(1).toLowerCase();
}
}

View File

@@ -80,7 +80,7 @@ public final class AttributesList {
new AttrD("min", true, "input","meter"),
new AttrD("multiple", false, "input","select"),
new AttrD("muted", false, "video","audio"),
new AttrD("name", true, "button","fieldset","form","iframe","input","map","meta","object","output","param","select","textarea"),
new AttrD("name", true, "button","fieldset","form","iframe","input","map","meta","object","output","param","select","slot","textarea"),
new AttrD("novalidate", false, "form"),
new AttrD("onabort", true, "audio","embed","img","object","video"),
new AttrD("onafterprint", true, "body"),
@@ -174,7 +174,7 @@ public final class AttributesList {
//new AttrD("translate"),// global attribute
new AttrD("type", true, "a","button","embed","input","link","menu","object","script","source","style"),
new AttrD("usemap", true, "img","object"),
new AttrD("value", true, "button","input","li","option","meter","progress","param"),
new AttrD("value", true, "button","data","input","li","option","meter","progress","param"),
new AttrD("width", true, "canvas","embed","iframe","img","input","object","video"),
new AttrD("wrap", true, "textarea")
);

View File

@@ -0,0 +1,10 @@
package j2html_codegen.wattsi;
public interface AttributeDefinition {
String name();
boolean appliesTo(ElementDefinition element);
boolean isObsolete();
}

View File

@@ -0,0 +1,9 @@
package j2html_codegen.wattsi;
public interface ElementDefinition {
String name();
boolean isObsolete();
}

View File

@@ -0,0 +1,204 @@
package j2html_codegen.wattsi;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.Elements;
import java.util.*;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList;
public class WattsiSource {
private final Document doc;
private final Set<Reference> obsolete = new HashSet<>();
public WattsiSource(Document doc) {
this.doc = doc;
// Find where obsolete elements are defined or referenced.
Elements obsoleteElements = doc.select("p:contains(Elements in the following list are entirely obsolete) + dl");
// Convert definitions into references to record obsolete elements.
obsoleteElements.select("dt > dfn[element]")
.stream()
.map(WattsiElement::new)
.map(WattsiElement::reference)
.forEach(obsolete::add);
// Extract references to record obsolete elements.
obsoleteElements.select("dt > code")
.stream()
.map(Element::childNodes)
.map(Reference::from)
.forEach(obsolete::add);
// Find where obsolete attributes are defined or referenced.
Elements obsoleteAttributes = doc.select("p:contains(The following attributes are obsolete) + dl");
// Convert definitions into references to record obsolete attributes.
obsoleteAttributes.select("dt > dfn[element-attr]").stream()
.map(WattsiAttribute::new)
.map(WattsiAttribute::reference)
.forEach(obsolete::add);
// System.out.println(obsoleteAttributes.select("dt"));
// obsoleteAttributes.select("dt > code").stream()
// .map(Element::childNodes)
// .map(Reference::from)
// .forEach(System.err::println);
// System.out.println(
// doc.select("dfn[obsolete]")
// );
}
public List<ElementDefinition> elementDefinitions() {
return doc.select("dfn[element]").stream()
.map(WattsiElement::new)
.collect(toList());
}
public List<AttributeDefinition> attributeDefinitions() {
return doc.select("dfn[element-attr]").stream()
.map(WattsiAttribute::new)
.collect(toList());
}
public class WattsiElement implements ElementDefinition {
private final Element dfn;
WattsiElement(Element dfn) {
if (!"dfn".equals(dfn.tagName())) {
throw new IllegalArgumentException("Element cannot be defined from: " + dfn);
}
if (!dfn.hasAttr("element")) {
throw new IllegalArgumentException("Does not define an element: " + dfn);
}
if (dfn.childrenSize() != 1) {
throw new IllegalArgumentException("Element cannot have multiple definitions: " + dfn);
}
this.dfn = dfn;
}
private Reference reference() {
return Reference.from(dfn.childNodes());
}
@Override
public String name() {
if (dfn.hasAttr("data-x")) {
return dfn.attr("data-x");
}
return Reference.from(dfn.childNodes()).key;
}
@Override
public boolean isObsolete() {
return obsolete.contains(reference());
}
}
public class WattsiAttribute implements AttributeDefinition {
private final Element dfn;
WattsiAttribute(Element dfn) {
if (!"dfn".equals(dfn.tagName())) {
throw new IllegalArgumentException("Attribute cannot be defined from: " + dfn);
}
if (!dfn.hasAttr("element-attr")) {
throw new IllegalArgumentException("Does not define an attribute: " + dfn);
}
if (dfn.childrenSize() != 1) {
throw new IllegalArgumentException("Attribute cannot have multiple definitions: " + dfn);
}
this.dfn = dfn;
}
private Reference reference() {
return Reference.from(dfn.childNodes());
}
@Override
public String name() {
return reference().text;
}
private List<String> targets() {
if (dfn.hasAttr("for")) {
return Arrays.asList(dfn.attr("for").trim().split(","));
}
return new ArrayList<>();
}
@Override
public boolean appliesTo(ElementDefinition element) {
return targets().contains(element.name());
}
@Override
public boolean isObsolete() {
return obsolete.contains(reference());
}
}
private static class Reference {
private final String key;
private final String text;
Reference(String key, String text) {
this.key = key;
this.text = text;
}
@Override
public String toString() {
return key + "[" + text + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Reference reference = (Reference) o;
return key.equals(reference.key);
}
@Override
public int hashCode() {
return Objects.hash(key);
}
public static Reference from(List<Node> nodes) {
if (nodes.stream().allMatch(n -> n instanceof TextNode)) {
String txt = nodes.stream()
.map(n -> (TextNode) n)
.map(TextNode::text)
.collect(Collectors.joining(" "));
return new Reference(txt, txt);
}
for (Node node : nodes) {
if (node instanceof Element) {
Element element = (Element) node;
if (element.is("code") || element.is("span")) {
if (element.hasAttr("data-x")) {
return new Reference(element.attr("data-x").toLowerCase(), element.text());
} else {
return new Reference(element.text().toLowerCase(), element.text());
}
}
}
}
return null;
}
}
}