From 01c7432c718a5f2e95b37ca0cfc69732be252867 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Mon, 1 Dec 2025 12:56:20 -0500 Subject: [PATCH] Initial commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- LICENSE | 202 +++++++++++++++++ README.md | 29 +++ cmd/generate/main.go | 422 +++++++++++++++++++++++++++++++++++ example/main.go | 52 +++++ generated_a.go | 227 +++++++++++++++++++ generated_area.go | 197 ++++++++++++++++ generated_base.go | 43 ++++ generated_body.go | 179 +++++++++++++++ generated_br.go | 35 +++ generated_button.go | 153 +++++++++++++ generated_canvas.go | 27 +++ generated_caption.go | 35 +++ generated_col.go | 67 ++++++ generated_data.go | 35 +++ generated_datalist.go | 35 +++ generated_details.go | 45 ++++ generated_dialog.go | 53 +++++ generated_dir.go | 37 +++ generated_div.go | 35 +++ generated_dl.go | 37 +++ generated_embed.go | 75 +++++++ generated_fieldset.go | 87 ++++++++ generated_font.go | 43 ++++ generated_form.go | 125 +++++++++++ generated_frame.go | 77 +++++++ generated_frameset.go | 187 ++++++++++++++++ generated_head.go | 27 +++ generated_hr.go | 69 ++++++ generated_html.go | 35 +++ generated_iframe.go | 133 +++++++++++ generated_img.go | 159 +++++++++++++ generated_input.go | 329 +++++++++++++++++++++++++++ generated_label.go | 35 +++ generated_legend.go | 35 +++ generated_li.go | 45 ++++ generated_link.go | 173 ++++++++++++++ generated_map.go | 43 ++++ generated_marquee.go | 87 ++++++++ generated_media.go | 223 ++++++++++++++++++ generated_menu.go | 37 +++ generated_meta.go | 67 ++++++ generated_meter.go | 85 +++++++ generated_object.go | 159 +++++++++++++ generated_ol.go | 65 ++++++ generated_optgroup.go | 45 ++++ generated_option.go | 91 ++++++++ generated_output.go | 101 +++++++++ generated_p.go | 35 +++ generated_param.go | 59 +++++ generated_picture.go | 27 +++ generated_pre.go | 37 +++ generated_progress.go | 61 +++++ generated_q.go | 35 +++ generated_script.go | 137 ++++++++++++ generated_select.go | 149 +++++++++++++ generated_selectedcontent.go | 27 +++ generated_slot.go | 35 +++ generated_source.go | 67 ++++++ generated_span.go | 27 +++ generated_style.go | 61 +++++ generated_table.go | 91 ++++++++ generated_td.go | 127 +++++++++++ generated_template.go | 81 +++++++ generated_textarea.go | 173 ++++++++++++++ generated_time.go | 35 +++ generated_title.go | 35 +++ generated_tr.go | 85 +++++++ generated_track.go | 77 +++++++ generated_ul.go | 45 ++++ generated_unknown.go | 27 +++ go.mod | 3 + types.go | 74 ++++++ 72 files changed, 6192 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmd/generate/main.go create mode 100644 example/main.go create mode 100644 generated_a.go create mode 100644 generated_area.go create mode 100644 generated_base.go create mode 100644 generated_body.go create mode 100644 generated_br.go create mode 100644 generated_button.go create mode 100644 generated_canvas.go create mode 100644 generated_caption.go create mode 100644 generated_col.go create mode 100644 generated_data.go create mode 100644 generated_datalist.go create mode 100644 generated_details.go create mode 100644 generated_dialog.go create mode 100644 generated_dir.go create mode 100644 generated_div.go create mode 100644 generated_dl.go create mode 100644 generated_embed.go create mode 100644 generated_fieldset.go create mode 100644 generated_font.go create mode 100644 generated_form.go create mode 100644 generated_frame.go create mode 100644 generated_frameset.go create mode 100644 generated_head.go create mode 100644 generated_hr.go create mode 100644 generated_html.go create mode 100644 generated_iframe.go create mode 100644 generated_img.go create mode 100644 generated_input.go create mode 100644 generated_label.go create mode 100644 generated_legend.go create mode 100644 generated_li.go create mode 100644 generated_link.go create mode 100644 generated_map.go create mode 100644 generated_marquee.go create mode 100644 generated_media.go create mode 100644 generated_menu.go create mode 100644 generated_meta.go create mode 100644 generated_meter.go create mode 100644 generated_object.go create mode 100644 generated_ol.go create mode 100644 generated_optgroup.go create mode 100644 generated_option.go create mode 100644 generated_output.go create mode 100644 generated_p.go create mode 100644 generated_param.go create mode 100644 generated_picture.go create mode 100644 generated_pre.go create mode 100644 generated_progress.go create mode 100644 generated_q.go create mode 100644 generated_script.go create mode 100644 generated_select.go create mode 100644 generated_selectedcontent.go create mode 100644 generated_slot.go create mode 100644 generated_source.go create mode 100644 generated_span.go create mode 100644 generated_style.go create mode 100644 generated_table.go create mode 100644 generated_td.go create mode 100644 generated_template.go create mode 100644 generated_textarea.go create mode 100644 generated_time.go create mode 100644 generated_title.go create mode 100644 generated_tr.go create mode 100644 generated_track.go create mode 100644 generated_ul.go create mode 100644 generated_unknown.go create mode 100644 go.mod create mode 100644 types.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1647f19 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# html + +Type-safe HTML generation for Go, generated from WebIDL. + +## Usage + +```go +import . "github.com/imjasonh/html" + +page := Div( + P(Raw("Hello, "), T(username), Raw("!")), + A(Raw("Click here")).Href("/home"), + Img().Src("/logo.png").Alt("Logo"), +).Render() +``` + +## Features + +- Generated from [WHATWG HTML Standard](https://html.spec.whatwg.org/) WebIDL +- Type-safe attributes (bool, int, string) +- XSS protection: `T()` escapes user input, `Raw()` for trusted HTML +- `Attr(key, value)` escape hatch for arbitrary attributes +- Automatic HTML escaping of attribute values + +## Generating + +```bash +go run cmd/generate/main.go # Fetches latest WebIDL and regenerates elements +``` diff --git a/cmd/generate/main.go b/cmd/generate/main.go new file mode 100644 index 0000000..d597e29 --- /dev/null +++ b/cmd/generate/main.go @@ -0,0 +1,422 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "net/http" + "os" + "regexp" + "sort" + "strings" + "text/template" +) + +const webidlURL = "https://raw.githubusercontent.com/w3c/webref/main/ed/idl/html.idl" + +// VoidElements lists HTML elements that are self-closing +var voidElements = map[string]bool{ + "area": true, "base": true, "br": true, "col": true, "embed": true, + "hr": true, "img": true, "input": true, "link": true, "meta": true, + "param": true, "source": true, "track": true, "wbr": true, +} + +// AttributeType represents the Go type for an attribute +type AttributeType string + +const ( + TypeString AttributeType = "string" + TypeBool AttributeType = "bool" + TypeInt AttributeType = "int" + TypeFloat AttributeType = "float64" + TypeURL AttributeType = "string" // URLs are strings + TypeEnum AttributeType = "string" // Enums are strings +) + +// Attribute represents an HTML attribute with its type +type Attribute struct { + Name string + GoName string // Capitalized method name + Type AttributeType + EnumValues []string // For enum types +} + +// ElementDef defines an HTML element +type ElementDef struct { + Name string + GoTypeName string // e.g., "DivElement" + GoFuncName string // e.g., "Div" + IsVoid bool + Attributes []Attribute +} + +func main() { + fmt.Println("Fetching WebIDL from", webidlURL) + resp, err := http.Get(webidlURL) + if err != nil { + fmt.Fprintf(os.Stderr, "Error fetching WebIDL: %v\n", err) + os.Exit(1) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + fmt.Fprintf(os.Stderr, "Error: HTTP %d\n", resp.StatusCode) + os.Exit(1) + } + + idlContent, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading WebIDL: %v\n", err) + os.Exit(1) + } + + fmt.Println("Parsing WebIDL...") + elements := parseWebIDL(string(idlContent)) + fmt.Printf("Found %d HTML elements\n", len(elements)) + + fmt.Println("Generating Go files...") + if err := generateElements(elements); err != nil { + fmt.Fprintf(os.Stderr, "Error generating files: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Successfully generated %d element files\n", len(elements)) +} + +func parseWebIDL(idl string) []ElementDef { + elements := make(map[string]*ElementDef) + mixins := make(map[string][]Attribute) // Store mixin attributes + includes := make(map[string][]string) // Track which interfaces include which mixins + + // Regex patterns for parsing + interfacePattern := regexp.MustCompile(`(?m)^interface\s+(HTML\w+Element)\s*:\s*HTMLElement\s*\{`) + partialPattern := regexp.MustCompile(`(?m)^partial\s+interface\s+(HTML\w+Element)\s*\{`) + mixinPattern := regexp.MustCompile(`(?m)^interface\s+mixin\s+(\w+)\s*\{`) + includesPattern := regexp.MustCompile(`(?m)^(HTML\w+Element)\s+includes\s+(\w+);`) + attributePattern := regexp.MustCompile(`(?m)^\s*(?:\[[^\]]+\]\s+)*(?:stringifier\s+)?(?:readonly\s+)?attribute\s+(\w+)\s+(\w+);`) + + scanner := bufio.NewScanner(strings.NewReader(idl)) + var currentInterface string + var currentMixin string + var inInterface bool + var inMixin bool + braceDepth := 0 + + for scanner.Scan() { + line := scanner.Text() + + // Track brace depth to know when we exit an interface + braceDepth += strings.Count(line, "{") - strings.Count(line, "}") + + // Check for includes statements + if matches := includesPattern.FindStringSubmatch(line); matches != nil { + interfaceName := matches[1] + mixinName := matches[2] + includes[interfaceName] = append(includes[interfaceName], mixinName) + continue + } + + // Check for mixin definitions + if matches := mixinPattern.FindStringSubmatch(line); matches != nil { + currentMixin = matches[1] + inMixin = true + if mixins[currentMixin] == nil { + mixins[currentMixin] = []Attribute{} + } + continue + } + + // Check for interface or partial interface declarations + if matches := interfacePattern.FindStringSubmatch(line); matches != nil { + currentInterface = matches[1] + inInterface = true + + // Extract element name (e.g., "HTMLDivElement" -> "div") + elemName := extractElementName(currentInterface) + if elemName != "" { + if _, exists := elements[elemName]; !exists { + elements[elemName] = &ElementDef{ + Name: elemName, + GoTypeName: strings.TrimPrefix(currentInterface, "HTML"), + GoFuncName: capitalize(elemName), + IsVoid: voidElements[elemName], + Attributes: []Attribute{}, + } + } + } + continue + } + + if matches := partialPattern.FindStringSubmatch(line); matches != nil { + currentInterface = matches[1] + inInterface = true + continue + } + + // Exit interface/mixin when braces close + if braceDepth == 0 { + inInterface = false + inMixin = false + currentInterface = "" + currentMixin = "" + } + + // Parse attributes within interface + if inInterface && currentInterface != "" { + if matches := attributePattern.FindStringSubmatch(line); matches != nil { + attrType := matches[1] + attrName := matches[2] + + elemName := extractElementName(currentInterface) + if elemName != "" && elements[elemName] != nil { + goType := mapIDLTypeToGo(attrType) + attr := Attribute{ + Name: attrName, + GoName: capitalize(attrName), + Type: goType, + } + + // Check if attribute already exists + exists := false + for _, existing := range elements[elemName].Attributes { + if existing.Name == attrName { + exists = true + break + } + } + if !exists { + elements[elemName].Attributes = append(elements[elemName].Attributes, attr) + } + } + } + } + + // Parse attributes within mixin + if inMixin && currentMixin != "" { + if matches := attributePattern.FindStringSubmatch(line); matches != nil { + attrType := matches[1] + attrName := matches[2] + + goType := mapIDLTypeToGo(attrType) + attr := Attribute{ + Name: attrName, + GoName: capitalize(attrName), + Type: goType, + } + + // Check if attribute already exists + exists := false + for _, existing := range mixins[currentMixin] { + if existing.Name == attrName { + exists = true + break + } + } + if !exists { + mixins[currentMixin] = append(mixins[currentMixin], attr) + } + } + } + } + + // Apply mixins to interfaces that include them + for interfaceName, mixinNames := range includes { + elemName := extractElementName(interfaceName) + if elemName != "" && elements[elemName] != nil { + for _, mixinName := range mixinNames { + if mixinAttrs, ok := mixins[mixinName]; ok { + for _, attr := range mixinAttrs { + // Check if attribute already exists + exists := false + for _, existing := range elements[elemName].Attributes { + if existing.Name == attr.Name { + exists = true + break + } + } + if !exists { + elements[elemName].Attributes = append(elements[elemName].Attributes, attr) + } + } + } + } + } + } + + // Convert map to sorted slice + var result []ElementDef + for _, elem := range elements { + // Sort attributes + sort.Slice(elem.Attributes, func(i, j int) bool { + return elem.Attributes[i].Name < elem.Attributes[j].Name + }) + result = append(result, *elem) + } + + // Sort elements by name + sort.Slice(result, func(i, j int) bool { + return result[i].Name < result[j].Name + }) + + return result +} + +func extractElementName(interfaceName string) string { + // Extract element name from interface name + // e.g., "HTMLDivElement" -> "div" + // e.g., "HTMLAnchorElement" -> "a" + + if !strings.HasPrefix(interfaceName, "HTML") || !strings.HasSuffix(interfaceName, "Element") { + return "" + } + + name := strings.TrimPrefix(interfaceName, "HTML") + name = strings.TrimSuffix(name, "Element") + + // Handle special cases + specialCases := map[string]string{ + "Anchor": "a", + "DList": "dl", + "Directory": "dir", + "Heading": "", // Skip generic heading + "Image": "img", + "Mod": "", // Skip generic mod (ins/del) + "OList": "ol", + "Paragraph": "p", + "Quote": "q", + "TableCaption": "caption", + "TableCell": "td", + "TableCol": "col", + "TableRow": "tr", + "TableSection": "", // Skip generic section + "UList": "ul", + } + + if special, ok := specialCases[name]; ok { + return special + } + + return strings.ToLower(name) +} + +func mapIDLTypeToGo(idlType string) AttributeType { + // Map WebIDL types to Go types + switch idlType { + case "boolean": + return TypeBool + case "long", "unsigned long", "short", "unsigned short": + return TypeInt + case "double", "float": + return TypeFloat + case "DOMString", "USVString": + return TypeString + default: + return TypeString // Default to string for unknown types + } +} + +func capitalize(s string) string { + if s == "" { + return s + } + // Convert hyphenated names to camelCase (e.g., "http-equiv" -> "HttpEquiv") + parts := strings.Split(s, "-") + for i := range parts { + if len(parts[i]) > 0 { + parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:] + } + } + return strings.Join(parts, "") +} + +const elementTemplate = `package html +{{if needsFmt .Attributes}} +import "fmt" +{{end}} +// {{.GoTypeName}} represents the <{{.Name}}> HTML element. +{{if .IsVoid}} +type {{.GoTypeName}} basicElement[HTMLContent] +{{else}} +type {{.GoTypeName}}[C HTMLContent] basicElement[C] +{{end}} + +// {{.GoFuncName}} creates a new <{{.Name}}> element. +func {{.GoFuncName}}({{if not .IsVoid}}children ...HTMLContent{{end}}) *{{.GoTypeName}}{{if not .IsVoid}}[HTMLContent]{{end}} { + return &{{.GoTypeName}}{{if not .IsVoid}}[HTMLContent]{{end}}{ + tagName: "{{.Name}}", + {{if .IsVoid}}isVoid: true,{{end}} + {{if not .IsVoid}}content: children,{{end}} + } +} + +// Render returns the HTML string representation. +func (e *{{.GoTypeName}}{{if not .IsVoid}}[C]{{end}}) Render() string { + {{if .IsVoid}}return (*basicElement[HTMLContent])(e).Render(){{else}}return (*basicElement[C])(e).Render(){{end}} +} + +// Attr sets an arbitrary attribute as an escape hatch. +func (e *{{.GoTypeName}}{{if not .IsVoid}}[C]{{end}}) Attr(key, value string) *{{.GoTypeName}}{{if not .IsVoid}}[C]{{end}} { + e.attributes = append(e.attributes, Attribute{Key: key, Value: value}) + return e +} +{{range .Attributes}} +{{if eq .Type "bool"}} +// {{.GoName}} sets the {{.Name}} attribute. +func (e *{{$.GoTypeName}}{{if not $.IsVoid}}[C]{{end}}) {{.GoName}}(v bool) *{{$.GoTypeName}}{{if not $.IsVoid}}[C]{{end}} { + if v { + e.attributes = append(e.attributes, Attribute{Key: "{{.Name}}", Value: "{{.Name}}"}) + } + return e +} +{{else if eq .Type "int"}} +// {{.GoName}} sets the {{.Name}} attribute. +func (e *{{$.GoTypeName}}{{if not $.IsVoid}}[C]{{end}}) {{.GoName}}(v int) *{{$.GoTypeName}}{{if not $.IsVoid}}[C]{{end}} { + e.attributes = append(e.attributes, Attribute{Key: "{{.Name}}", Value: fmt.Sprintf("%d", v)}) + return e +} +{{else if eq .Type "float64"}} +// {{.GoName}} sets the {{.Name}} attribute. +func (e *{{$.GoTypeName}}{{if not $.IsVoid}}[C]{{end}}) {{.GoName}}(v float64) *{{$.GoTypeName}}{{if not $.IsVoid}}[C]{{end}} { + e.attributes = append(e.attributes, Attribute{Key: "{{.Name}}", Value: fmt.Sprintf("%g", v)}) + return e +} +{{else}} +// {{.GoName}} sets the {{.Name}} attribute. +func (e *{{$.GoTypeName}}{{if not $.IsVoid}}[C]{{end}}) {{.GoName}}(v string) *{{$.GoTypeName}}{{if not $.IsVoid}}[C]{{end}} { + e.attributes = append(e.attributes, Attribute{Key: "{{.Name}}", Value: v}) + return e +} +{{end}} +{{end}} +` + +func needsFmt(attrs []Attribute) bool { + for _, attr := range attrs { + if attr.Type == TypeInt || attr.Type == TypeFloat { + return true + } + } + return false +} + +func generateElements(elements []ElementDef) error { + tmpl := template.Must(template.New("element").Funcs(template.FuncMap{ + "needsFmt": needsFmt, + }).Parse(elementTemplate)) + + for _, elem := range elements { + filename := fmt.Sprintf("generated_%s.go", elem.Name) + f, err := os.Create(filename) + if err != nil { + return fmt.Errorf("creating file %s: %w", filename, err) + } + + if err := tmpl.Execute(f, elem); err != nil { + f.Close() + return fmt.Errorf("executing template for %s: %w", elem.Name, err) + } + f.Close() + } + + return nil +} diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..5697777 --- /dev/null +++ b/example/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + + . "github.com/imjasonh/html" +) + +func main() { + // Render to HTML string + fmt.Println(Div( + P( + Raw("Welcome to the app. Here's a link: "), + A(Raw("Click Here")).Href("/home").Target("_blank"), + Raw("!"), + ).Attr("data-section", "intro"), + + // A self-closing element (void element) + Img(). + Src("/asset/img.jpg"). + Alt("A Placeholder Image"). + Attr("loading", "lazy"). + Attr("data-category", "hero"), + + // XSS check: User input must be wrapped with T() for escaping + P(T("User Input: ")), + + // More examples + Ul( + Li(Raw("Automatic XSS escaping")), + Li(Raw("Type-safe attributes from WebIDL")), + Li(Raw("Builder pattern with method chaining")), + ), + + Form( + Label(Raw("Name:")), + Input(). + Type("text"). + Name("username"). + Placeholder("Enter your name"). + Required(true), + Button(Raw("Submit")). + Type("submit"), + ). + Action("/submit"). + Method("post"), + + Raw(""), // Raw HTML insertion + + Raw(``), // Raw HTML insertion + ).Render()) +} diff --git a/generated_a.go b/generated_a.go new file mode 100644 index 0000000..b504cfc --- /dev/null +++ b/generated_a.go @@ -0,0 +1,227 @@ +package html + +// AnchorElement represents the HTML element. + +type AnchorElement[C HTMLContent] basicElement[C] + + +// A creates a new element. +func A(children ...HTMLContent) *AnchorElement[HTMLContent] { + return &AnchorElement[HTMLContent]{ + tagName: "a", + + content: children, + } +} + +// Render returns the HTML string representation. +func (e *AnchorElement[C]) Render() string { + return (*basicElement[C])(e).Render() +} + +// Attr sets an arbitrary attribute as an escape hatch. +func (e *AnchorElement[C]) Attr(key, value string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: key, Value: value}) + return e +} + + +// Charset sets the charset attribute. +func (e *AnchorElement[C]) Charset(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "charset", Value: v}) + return e +} + + + +// Coords sets the coords attribute. +func (e *AnchorElement[C]) Coords(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "coords", Value: v}) + return e +} + + + +// Download sets the download attribute. +func (e *AnchorElement[C]) Download(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "download", Value: v}) + return e +} + + + +// Hash sets the hash attribute. +func (e *AnchorElement[C]) Hash(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "hash", Value: v}) + return e +} + + + +// Host sets the host attribute. +func (e *AnchorElement[C]) Host(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "host", Value: v}) + return e +} + + + +// Hostname sets the hostname attribute. +func (e *AnchorElement[C]) Hostname(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "hostname", Value: v}) + return e +} + + + +// Href sets the href attribute. +func (e *AnchorElement[C]) Href(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "href", Value: v}) + return e +} + + + +// Hreflang sets the hreflang attribute. +func (e *AnchorElement[C]) Hreflang(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "hreflang", Value: v}) + return e +} + + + +// Name sets the name attribute. +func (e *AnchorElement[C]) Name(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "name", Value: v}) + return e +} + + + +// Origin sets the origin attribute. +func (e *AnchorElement[C]) Origin(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "origin", Value: v}) + return e +} + + + +// Password sets the password attribute. +func (e *AnchorElement[C]) Password(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "password", Value: v}) + return e +} + + + +// Pathname sets the pathname attribute. +func (e *AnchorElement[C]) Pathname(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "pathname", Value: v}) + return e +} + + + +// Ping sets the ping attribute. +func (e *AnchorElement[C]) Ping(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "ping", Value: v}) + return e +} + + + +// Port sets the port attribute. +func (e *AnchorElement[C]) Port(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "port", Value: v}) + return e +} + + + +// Protocol sets the protocol attribute. +func (e *AnchorElement[C]) Protocol(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "protocol", Value: v}) + return e +} + + + +// ReferrerPolicy sets the referrerPolicy attribute. +func (e *AnchorElement[C]) ReferrerPolicy(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "referrerPolicy", Value: v}) + return e +} + + + +// Rel sets the rel attribute. +func (e *AnchorElement[C]) Rel(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "rel", Value: v}) + return e +} + + + +// RelList sets the relList attribute. +func (e *AnchorElement[C]) RelList(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "relList", Value: v}) + return e +} + + + +// Rev sets the rev attribute. +func (e *AnchorElement[C]) Rev(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "rev", Value: v}) + return e +} + + + +// Search sets the search attribute. +func (e *AnchorElement[C]) Search(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "search", Value: v}) + return e +} + + + +// Shape sets the shape attribute. +func (e *AnchorElement[C]) Shape(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "shape", Value: v}) + return e +} + + + +// Target sets the target attribute. +func (e *AnchorElement[C]) Target(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "target", Value: v}) + return e +} + + + +// Text sets the text attribute. +func (e *AnchorElement[C]) Text(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "text", Value: v}) + return e +} + + + +// Type sets the type attribute. +func (e *AnchorElement[C]) Type(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "type", Value: v}) + return e +} + + + +// Username sets the username attribute. +func (e *AnchorElement[C]) Username(v string) *AnchorElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "username", Value: v}) + return e +} + + diff --git a/generated_area.go b/generated_area.go new file mode 100644 index 0000000..a8a20db --- /dev/null +++ b/generated_area.go @@ -0,0 +1,197 @@ +package html + +// AreaElement represents the HTML element. + +type AreaElement basicElement[HTMLContent] + + +// Area creates a new element. +func Area() *AreaElement { + return &AreaElement{ + tagName: "area", + isVoid: true, + + } +} + +// Render returns the HTML string representation. +func (e *AreaElement) Render() string { + return (*basicElement[HTMLContent])(e).Render() +} + +// Attr sets an arbitrary attribute as an escape hatch. +func (e *AreaElement) Attr(key, value string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: key, Value: value}) + return e +} + + +// Alt sets the alt attribute. +func (e *AreaElement) Alt(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "alt", Value: v}) + return e +} + + + +// Coords sets the coords attribute. +func (e *AreaElement) Coords(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "coords", Value: v}) + return e +} + + + +// Download sets the download attribute. +func (e *AreaElement) Download(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "download", Value: v}) + return e +} + + + +// Hash sets the hash attribute. +func (e *AreaElement) Hash(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "hash", Value: v}) + return e +} + + + +// Host sets the host attribute. +func (e *AreaElement) Host(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "host", Value: v}) + return e +} + + + +// Hostname sets the hostname attribute. +func (e *AreaElement) Hostname(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "hostname", Value: v}) + return e +} + + + +// Href sets the href attribute. +func (e *AreaElement) Href(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "href", Value: v}) + return e +} + + + +// NoHref sets the noHref attribute. +func (e *AreaElement) NoHref(v bool) *AreaElement { + if v { + e.attributes = append(e.attributes, Attribute{Key: "noHref", Value: "noHref"}) + } + return e +} + + + +// Origin sets the origin attribute. +func (e *AreaElement) Origin(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "origin", Value: v}) + return e +} + + + +// Password sets the password attribute. +func (e *AreaElement) Password(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "password", Value: v}) + return e +} + + + +// Pathname sets the pathname attribute. +func (e *AreaElement) Pathname(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "pathname", Value: v}) + return e +} + + + +// Ping sets the ping attribute. +func (e *AreaElement) Ping(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "ping", Value: v}) + return e +} + + + +// Port sets the port attribute. +func (e *AreaElement) Port(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "port", Value: v}) + return e +} + + + +// Protocol sets the protocol attribute. +func (e *AreaElement) Protocol(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "protocol", Value: v}) + return e +} + + + +// ReferrerPolicy sets the referrerPolicy attribute. +func (e *AreaElement) ReferrerPolicy(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "referrerPolicy", Value: v}) + return e +} + + + +// Rel sets the rel attribute. +func (e *AreaElement) Rel(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "rel", Value: v}) + return e +} + + + +// RelList sets the relList attribute. +func (e *AreaElement) RelList(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "relList", Value: v}) + return e +} + + + +// Search sets the search attribute. +func (e *AreaElement) Search(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "search", Value: v}) + return e +} + + + +// Shape sets the shape attribute. +func (e *AreaElement) Shape(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "shape", Value: v}) + return e +} + + + +// Target sets the target attribute. +func (e *AreaElement) Target(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "target", Value: v}) + return e +} + + + +// Username sets the username attribute. +func (e *AreaElement) Username(v string) *AreaElement { + e.attributes = append(e.attributes, Attribute{Key: "username", Value: v}) + return e +} + + diff --git a/generated_base.go b/generated_base.go new file mode 100644 index 0000000..de5ef97 --- /dev/null +++ b/generated_base.go @@ -0,0 +1,43 @@ +package html + +// BaseElement represents the HTML element. + +type BaseElement basicElement[HTMLContent] + + +// Base creates a new element. +func Base() *BaseElement { + return &BaseElement{ + tagName: "base", + isVoid: true, + + } +} + +// Render returns the HTML string representation. +func (e *BaseElement) Render() string { + return (*basicElement[HTMLContent])(e).Render() +} + +// Attr sets an arbitrary attribute as an escape hatch. +func (e *BaseElement) Attr(key, value string) *BaseElement { + e.attributes = append(e.attributes, Attribute{Key: key, Value: value}) + return e +} + + +// Href sets the href attribute. +func (e *BaseElement) Href(v string) *BaseElement { + e.attributes = append(e.attributes, Attribute{Key: "href", Value: v}) + return e +} + + + +// Target sets the target attribute. +func (e *BaseElement) Target(v string) *BaseElement { + e.attributes = append(e.attributes, Attribute{Key: "target", Value: v}) + return e +} + + diff --git a/generated_body.go b/generated_body.go new file mode 100644 index 0000000..7425c87 --- /dev/null +++ b/generated_body.go @@ -0,0 +1,179 @@ +package html + +// BodyElement represents the HTML element. + +type BodyElement[C HTMLContent] basicElement[C] + + +// Body creates a new element. +func Body(children ...HTMLContent) *BodyElement[HTMLContent] { + return &BodyElement[HTMLContent]{ + tagName: "body", + + content: children, + } +} + +// Render returns the HTML string representation. +func (e *BodyElement[C]) Render() string { + return (*basicElement[C])(e).Render() +} + +// Attr sets an arbitrary attribute as an escape hatch. +func (e *BodyElement[C]) Attr(key, value string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: key, Value: value}) + return e +} + + +// Background sets the background attribute. +func (e *BodyElement[C]) Background(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "background", Value: v}) + return e +} + + + +// Onafterprint sets the onafterprint attribute. +func (e *BodyElement[C]) Onafterprint(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onafterprint", Value: v}) + return e +} + + + +// Onbeforeprint sets the onbeforeprint attribute. +func (e *BodyElement[C]) Onbeforeprint(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onbeforeprint", Value: v}) + return e +} + + + +// Onbeforeunload sets the onbeforeunload attribute. +func (e *BodyElement[C]) Onbeforeunload(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onbeforeunload", Value: v}) + return e +} + + + +// Onhashchange sets the onhashchange attribute. +func (e *BodyElement[C]) Onhashchange(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onhashchange", Value: v}) + return e +} + + + +// Onlanguagechange sets the onlanguagechange attribute. +func (e *BodyElement[C]) Onlanguagechange(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onlanguagechange", Value: v}) + return e +} + + + +// Onmessage sets the onmessage attribute. +func (e *BodyElement[C]) Onmessage(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onmessage", Value: v}) + return e +} + + + +// Onmessageerror sets the onmessageerror attribute. +func (e *BodyElement[C]) Onmessageerror(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onmessageerror", Value: v}) + return e +} + + + +// Onoffline sets the onoffline attribute. +func (e *BodyElement[C]) Onoffline(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onoffline", Value: v}) + return e +} + + + +// Ononline sets the ononline attribute. +func (e *BodyElement[C]) Ononline(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "ononline", Value: v}) + return e +} + + + +// Onpagehide sets the onpagehide attribute. +func (e *BodyElement[C]) Onpagehide(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onpagehide", Value: v}) + return e +} + + + +// Onpagereveal sets the onpagereveal attribute. +func (e *BodyElement[C]) Onpagereveal(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onpagereveal", Value: v}) + return e +} + + + +// Onpageshow sets the onpageshow attribute. +func (e *BodyElement[C]) Onpageshow(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onpageshow", Value: v}) + return e +} + + + +// Onpageswap sets the onpageswap attribute. +func (e *BodyElement[C]) Onpageswap(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onpageswap", Value: v}) + return e +} + + + +// Onpopstate sets the onpopstate attribute. +func (e *BodyElement[C]) Onpopstate(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onpopstate", Value: v}) + return e +} + + + +// Onrejectionhandled sets the onrejectionhandled attribute. +func (e *BodyElement[C]) Onrejectionhandled(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onrejectionhandled", Value: v}) + return e +} + + + +// Onstorage sets the onstorage attribute. +func (e *BodyElement[C]) Onstorage(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onstorage", Value: v}) + return e +} + + + +// Onunhandledrejection sets the onunhandledrejection attribute. +func (e *BodyElement[C]) Onunhandledrejection(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onunhandledrejection", Value: v}) + return e +} + + + +// Onunload sets the onunload attribute. +func (e *BodyElement[C]) Onunload(v string) *BodyElement[C] { + e.attributes = append(e.attributes, Attribute{Key: "onunload", Value: v}) + return e +} + + diff --git a/generated_br.go b/generated_br.go new file mode 100644 index 0000000..47934ea --- /dev/null +++ b/generated_br.go @@ -0,0 +1,35 @@ +package html + +// BRElement represents the
HTML element. + +type BRElement basicElement[HTMLContent] + + +// Br creates a new
element. +func Br() *BRElement { + return &BRElement{ + tagName: "br", + isVoid: true, + + } +} + +// Render returns the HTML string representation. +func (e *BRElement) Render() string { + return (*basicElement[HTMLContent])(e).Render() +} + +// Attr sets an arbitrary attribute as an escape hatch. +func (e *BRElement) Attr(key, value string) *BRElement { + e.attributes = append(e.attributes, Attribute{Key: key, Value: value}) + return e +} + + +// Clear sets the clear attribute. +func (e *BRElement) Clear(v string) *BRElement { + e.attributes = append(e.attributes, Attribute{Key: "clear", Value: v}) + return e +} + + diff --git a/generated_button.go b/generated_button.go new file mode 100644 index 0000000..1d8636f --- /dev/null +++ b/generated_button.go @@ -0,0 +1,153 @@ +package html + +// ButtonElement represents the