swift-html icon indicating copy to clipboard operation
swift-html copied to clipboard

Make Tag easier to subclass

Open bgisme opened this issue 2 years ago • 0 comments

This pull request attempts to make Tag more straightforward to subclass.

Right now, if you take a basic html tag like Div and customize it, the rendered tag name will be the same as your custom subclass.

Example... class MyClass: Div { }

Renders as... <myclass></myclass>

So if you want to subclass an html tag like Div and keep that name, you have to add extra code.

class MyClass: Div {
     override open class func createNode() -> Node {
         Node(type: .standard, name: "div")
    }
}

This seems like overkill. And prone to mistakes. Like changing the superclass from 'Div' to 'P' and then forgetting to change the node name to match.

The proposed code changes try to fix this. And in the process, make customizing a tag name simple—either take the name of your subclass or override one class variable.

The HTML tags still use their own class name as the node name, to prevent typos. And all subclasses get it for free.

The code changes also try to make Tag more accommodating to complicated setups.

Here are the different ways to use Tag from simple to complex.

  1. Subclass Tag and it will render with your class name lowercased.
class MyTag: Tag { }

// <mytag></mytag>
  1. Subclass Tag and override the name property.
class MyTag: Tag { 

    override open class var name: String { "myTag" }
}

// <myTag></myTag>
  1. Subclass Tag and override the node property.
class MyTag: Tag {
    
    override open class var node: Node { .init(type: .empty, name: "myTag") }
}

// <myTag>
  1. Subclass Tag, create your own custom initializer and then call the Tag designated initializer.
class MyTag: Tag {

    init(myAttributeValue: String, @TagBuilder _ builder: () -> Tag) {
        let attribute = .init(key: "myKey", value: myAttributeValue)
        let node = Node(type: .empty, name: "myTag", attributes: [attribute])
        super.init(node: node, [builder()])
    }
}

A few other subtle but important changes...

Node names are no longer optional. There were some forced unwraps in DocumentRenderer that seemed like a bad place to crash. And since Tag automatically generates a name, leaving it optional seemed unnecessary.

• An extension was added to String for init() with a class name. This is used in all the html tags to convert their class names into node names. And since the class names are type checked, it helps prevent typos if the class name changes.

• A new intermediate class TypedTag was created for subclasses with a specific node type: .standard, .empty, .comment, .group. It seemed appropriate to keep all their initializers in one file instead of many. So now there's a subclass for each node type: GroupTag, EmptyTag, StandardTag, CommentTag. And these make reading the html tags a little more clear. You can immediately tell how the tag will render...

class Div: StandardTag { }

class A: EmptyTag { }

bgisme avatar Jul 11 '23 18:07 bgisme