diff --git a/cli/htmgo/htmltogo/entry.go b/cli/htmgo/htmltogo/entry.go deleted file mode 100644 index 9406939..0000000 --- a/cli/htmgo/htmltogo/entry.go +++ /dev/null @@ -1,119 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "log" - "strings" - - "github.com/dave/jennifer/jen" - "golang.org/x/net/html" -) - -func main() { - // Example HTML input - htmlData := ` -

Manage Patients

Name: Sydne

Reason for visit: arm hurts

- ` - - // Parse the HTML - doc, err := html.Parse(bytes.NewReader([]byte(htmlData))) - if err != nil { - log.Fatal(err) - } - - // Create a new Jennifer file - f := jen.NewFile("main") - - // Generate Jennifer code for the parsed HTML tree - generatedCode := processNode(doc.FirstChild) - - // Add the generated code to the file - f.Func().Id("Render").Params().Block(generatedCode...) - - // Render the generated code - var buf bytes.Buffer - err = f.Render(&buf) - if err != nil { - log.Fatal(err) - } - - //// Format the generated code - //formattedCode, err := format.Source(buf.Bytes()) - //if err != nil { - // log.Fatal(err) - //} - - // Output the formatted code - fmt.Println(string(buf.Bytes())) -} - -// Recursively process the HTML nodes and generate Jennifer code -func processNode(n *html.Node) []jen.Code { - var code []jen.Code - - // Only process element nodes - if n.Type == html.ElementNode { - // Create a dynamic method call based on the tag name - tagMethod := strings.Title(n.Data) // Capitalize the first letter of the tag - - // Add dynamic method call for the tag (e.g., h.Div(), h.Button(), etc.) - code = append(code, jen.Id("h").Dot(tagMethod).Call(mergeArgs(n)...)) - } - - return code -} - -// Merge attributes and children into a single slice for Call() -func mergeArgs(n *html.Node) []jen.Code { - // Process attributes - attrs := processAttributes(n.Attr) - - // Process children - children := processChildren(n) - - // Combine attributes and children into one slice - return append(attrs, children...) -} - -// Process child nodes of a given HTML node -func processChildren(n *html.Node) []jen.Code { - var children []jen.Code - - for c := n.FirstChild; c != nil; c = c.NextSibling { - children = append(children, processNode(c)...) - } - - return children -} - -func FormatFieldName(name string) string { - split := strings.Split(name, "_") - if strings.Contains(name, "-") { - split = strings.Split(name, "-") - } - parts := make([]string, 0) - for _, s := range split { - parts = append(parts, PascalCase(s)) - } - return strings.Join(parts, "") -} - -func PascalCase(s string) string { - if s == "" { - return s - } - // Convert the first rune (character) to uppercase and concatenate with the rest of the string - return strings.ToUpper(string(s[0])) + s[1:] -} - -// Process the attributes of an HTML node and return Jennifer code -func processAttributes(attrs []html.Attribute) []jen.Code { - var args []jen.Code - for _, attr := range attrs { - // Dynamically handle all attributes - attrMethod := FormatFieldName(attr.Key) // E.g., convert "data-role" to "DataRole" - args = append(args, jen.Id("h").Dot(attrMethod).Call(jen.Lit(attr.Val))) - } - return args -} diff --git a/tools/html-to-htmgo/go.mod b/tools/html-to-htmgo/go.mod new file mode 100644 index 0000000..5025aea --- /dev/null +++ b/tools/html-to-htmgo/go.mod @@ -0,0 +1,8 @@ +module html-to-htmgo + +go 1.23.0 + +require ( + golang.org/x/net v0.30.0 + golang.org/x/text v0.19.0 +) diff --git a/tools/html-to-htmgo/go.sum b/tools/html-to-htmgo/go.sum new file mode 100644 index 0000000..613920c --- /dev/null +++ b/tools/html-to-htmgo/go.sum @@ -0,0 +1,8 @@ +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= diff --git a/tools/html-to-htmgo/internal/adapters/services/formatter/formatter.go b/tools/html-to-htmgo/internal/adapters/services/formatter/formatter.go new file mode 100644 index 0000000..f6cc02a --- /dev/null +++ b/tools/html-to-htmgo/internal/adapters/services/formatter/formatter.go @@ -0,0 +1,29 @@ +package formatter + +import ( + "go/format" + "html-to-htmgo/internal/domain" +) + +type Formatter struct { +} + +func (f Formatter) Format(node *domain.CustomNode) string { + b := []byte(`package main +import ( + "github.com/maddalax/htmgo/framework/h" +) +func MyComponent() *h.Element { + return ` + node.String() + ` +}`) + dist, err := format.Source(b) + if err != nil { + return string(b) + } + + return string(dist) +} + +func New() Formatter { + return Formatter{} +} diff --git a/tools/html-to-htmgo/internal/adapters/services/parser/parser.go b/tools/html-to-htmgo/internal/adapters/services/parser/parser.go new file mode 100644 index 0000000..3583d46 --- /dev/null +++ b/tools/html-to-htmgo/internal/adapters/services/parser/parser.go @@ -0,0 +1,69 @@ +package parser + +import ( + "bytes" + "errors" + "fmt" + "golang.org/x/net/html" + "html-to-htmgo/internal/domain" + "strings" +) + +type Parser struct { +} + +var ParseErr = errors.New("parse error") + +func (p Parser) FromBytes(in []byte) (*domain.CustomNode, error) { + hNode, err := html.Parse(bytes.NewReader(in)) + if err != nil { + return nil, fmt.Errorf("%w: %v", ParseErr, err) + } + var findBody func(n *html.Node) *html.Node + findBody = func(n *html.Node) *html.Node { + if n.Data == "body" { + return n + } + var e *html.Node + for c := n.FirstChild; c != nil; c = c.NextSibling { + e = findBody(c) + } + return e + } + + body := findBody(hNode) + if body == nil { + return nil, fmt.Errorf("%w", ParseErr) + } + + var f func(*html.Node, *domain.CustomNode) *domain.CustomNode + f = func(n *html.Node, cNode *domain.CustomNode) *domain.CustomNode { + if n.Type == html.ElementNode { + cNode.SetType(n.Data) + + for _, attr := range n.Attr { + cNode.AddAttr(attr.Key, attr.Val) + } + } + + if n.Type == html.TextNode && len(strings.TrimSpace(n.Data)) > 0 { + cNode.ParentNode.AddAttr("h.Text", strings.TrimSpace(n.Data)) + } + + var i uint + for c := n.FirstChild; c != nil; c = c.NextSibling { + cNode.Nodes = append(cNode.Nodes, &domain.CustomNode{ParentNode: cNode, Level: cNode.Level + 1}) + cNode.Nodes[i] = f(c, cNode.Nodes[i]) + i++ + } + return cNode + } + output := &domain.CustomNode{} + out := f(body, output) + + return out, nil +} + +func New() Parser { + return Parser{} +} diff --git a/tools/html-to-htmgo/internal/adapters/services/parser/parser_test.go b/tools/html-to-htmgo/internal/adapters/services/parser/parser_test.go new file mode 100644 index 0000000..5d2de8d --- /dev/null +++ b/tools/html-to-htmgo/internal/adapters/services/parser/parser_test.go @@ -0,0 +1,33 @@ +package parser + +import ( + "errors" + "testing" +) + +func FuzzFromBytes(f *testing.F) { + serviceParser := New() + f.Add([]byte("Hello World")) + f.Add([]byte("TestSample")) + f.Add([]byte("
Some random text
")) + f.Add([]byte("Invalid HTML")) + f.Add([]byte("

")) + f.Add([]byte(" 10000 { // (10KB) + t.Skip() + } + _, err := serviceParser.FromBytes(data) + if err != nil { + return + } + if err != nil && !isExpectedError(err) { + t.Errorf("Unexpected error: %v", err) + } + }) +} + +func isExpectedError(err error) bool { + return err != nil && errors.Is(err, ParseErr) +} diff --git a/tools/html-to-htmgo/internal/domain/formatter.go b/tools/html-to-htmgo/internal/domain/formatter.go new file mode 100644 index 0000000..35c4709 --- /dev/null +++ b/tools/html-to-htmgo/internal/domain/formatter.go @@ -0,0 +1,5 @@ +package domain + +type Formatter interface { + Format(node *CustomNode) (string, error) +} diff --git a/tools/html-to-htmgo/internal/domain/node.go b/tools/html-to-htmgo/internal/domain/node.go new file mode 100644 index 0000000..d0fdfa4 --- /dev/null +++ b/tools/html-to-htmgo/internal/domain/node.go @@ -0,0 +1,127 @@ +package domain + +import ( + "fmt" + "slices" + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type CustomNode struct { + ParentNode *CustomNode + Level uint + customType bool + Type string + Attrs []Attr + Nodes []*CustomNode +} + +func (n *CustomNode) SetType(in string) { + switch in { + case "head": + n.Type = "h.Head" + case "thead": + n.Type = "h.THead" + case "tbody": + n.Type = "h.TBody" + case "id": + n.Type = "h.Id" + case "path": + n.Type = "path" + n.customType = true + case "circle": + n.Type = "circle" + n.customType = true + case "rect": + n.Type = "rect" + n.customType = true + case "line": + n.Type = "line" + n.customType = true + case "polyline": + n.Type = "line" + n.customType = true + case "svg": + n.Type = "h.Svg" + default: + n.Type = fmt.Sprintf("h.%s", cases.Title(language.English).String(in)) + } +} + +func (n *CustomNode) AddAttr(key, value string) { + if slices.Contains([]string{"xmlns", "fill", "viewBox", "stroke", "stroke-width", "fill-rule", "d", "stroke-linecap", "stroke-linejoin", "cx", "cy", "r", "x", "y", "rx", "ry", "x1", "x2", "y1", "y2", "points"}, key) { + n.Attrs = append(n.Attrs, Attr{ + custom: true, + key: key, + value: value, + }) + return + } + + switch { + case key == "id": + n.Attrs = append(n.Attrs, Attr{key: "h.Id", value: value}) + case key == "tabindex": + n.Attrs = append(n.Attrs, Attr{key: "h.TabIndex", value: value}) + case key == "h.Text": + n.Attrs = append(n.Attrs, Attr{key: key, value: value}) + case strings.ContainsRune(key, '-'): + n.Attrs = append(n.Attrs, Attr{ + custom: true, + key: key, + value: value, + }) + fmt.Printf("key: %s, value: %s\n", key, value) + default: + n.Attrs = append(n.Attrs, Attr{key: "h." + cases.Title(language.English).String(key), value: value}) + } +} + +func (n *CustomNode) String() string { + str := "" + + if n.customType { + str += "h.Tag(\"" + n.Type + "\"," + } else { + str += n.Type + "(" + } + + if len(n.Attrs) > 0 { + for _, v := range n.Attrs { + switch { + case v.custom: + str = fmt.Sprintf("%sh.Attribute(\"%s\",\"%s\"),", str, v.key, v.value) + case v.hyphenated: + str = fmt.Sprintf("%s%s(\"%s\", \"%s\"),", str, v.key, v.arg, v.value) + case len(v.value) > 0: + if strings.Contains(v.value, "\n") { + str = fmt.Sprintf("%s%s(`%s`),", str, v.key, v.value) + } else { + str = fmt.Sprintf("%s%s(\"%s\"),", str, v.key, v.value) + } + case v.value == "": + str = fmt.Sprintf("%s%s(\"\"),", str, v.key) + default: + str = fmt.Sprintf("%s%s(),", str, v.key) + } + } + } + + if len(n.Nodes) > 0 { + for _, v := range n.Nodes { + if v.Type != "" { + str = fmt.Sprintf("%s\n%s%s,", str, strings.Repeat(" ", int(n.Level)), v) + } + } + } + + str = fmt.Sprintf("%s\n%s)", str, strings.Repeat(" ", int(n.Level))) + return str +} + +type Attr struct { + custom, hyphenated bool + key, value, arg string +} diff --git a/tools/html-to-htmgo/internal/domain/parser.go b/tools/html-to-htmgo/internal/domain/parser.go new file mode 100644 index 0000000..e7c1597 --- /dev/null +++ b/tools/html-to-htmgo/internal/domain/parser.go @@ -0,0 +1,5 @@ +package domain + +type HTMLParser interface { + FromBytes(bytes []byte) (*CustomNode, error) +} diff --git a/tools/html-to-htmgo/main.go b/tools/html-to-htmgo/main.go new file mode 100644 index 0000000..cd0fd25 --- /dev/null +++ b/tools/html-to-htmgo/main.go @@ -0,0 +1,22 @@ +// Forked from https://github.com/PiotrKowalski/html-to-gomponents + +package main + +import ( + serviceformatter "html-to-htmgo/internal/adapters/services/formatter" + serviceparser "html-to-htmgo/internal/adapters/services/parser" +) + +func Parse(input []byte) []byte { + parser := serviceparser.New() + formatter := serviceformatter.New() + parsed, err := parser.FromBytes( + input, + ) + + if err != nil { + return nil + } + + return []byte(formatter.Format(parsed)) +}