Warning
The library is not in contributions ready state yet , test not exist
A lightweight and flexible HTML parser written in Go that builds an Abstract Syntax Tree (AST) from HTML documents.
- ✅ Parse HTML documents into AST
- ✅ Handle self-closing tags
- ✅ Support for
<style>and<script>raw content tags - ✅ HTML comments parsing
- ✅ DOCTYPE declarations
- ✅ Custom tag handlers
- ✅ Tree manipulation (clone, remove)
- ✅ HTML serialization back to string
- ✅ Attribute type conversion utilities
- ✅ Position tracking for debugging
go get github.com/DilemaFixer/HtmlParserpackage main
import (
"fmt"
parser "github.com/DilemaFixer/HtmlParser"
)
funcmain() {
html:=`<div class="container"> <h1>Hello World</h1> <p>This is a paragraph with <strong>bold</strong> text.</p> </div>`htmlParser:=parser.NewHtmlParser()
ast, err:=htmlParser.ParseHtml(html)
iferr!=nil {
fmt.Println("Error:", err)
return
}
// Print the AST treefor_, tag:=rangeast {
parser.PrintHtmlTree(tag)
}
}typeHtmlTagstruct {
Namestring// Tag name (e.g., "div", "p")InnerHtmlstring// Raw HTML content between tagsInnerContentstring// Text content between tagsIsSelfClosingbool// Whether the tag is self-closingAttributesmap[string]HtmlAttribute// Tag attributesParent*HtmlTag// Parent tag referenceChildren []*HtmlTag// Child tagsPosPosition// Position in source document
}typeHtmlAttributestruct {
Namestring// Attribute nameValuestring// Attribute valueIsValueExistbool// Whether the attribute has a value
}typePositionstruct {
Lineint// Line number in sourceColumnint// Column number in source
}htmlParser:=parser.NewHtmlParser()
ast, err:=htmlParser.ParseHtml(htmlString)
iferr!=nil {
// Handle parsing error
}
// ast is []*HtmlTag - slice of root elements// Check if attribute existsiftag.HasAttribute("class") {
// Get attributeattr:=tag.GetAttribute("class")
ifattr!=nil {
className:=attr.AsString()
}
}
// Set attributetag.SetAttribute("id", "my-id")
// Remove attributetag.RemoveAttribute("class")
// Type conversionswidthAttr:=tag.GetAttribute("width")
ifwidthAttr!=nil {
width, err:=widthAttr.AsInt()
iferr==nil {
// use width as integer
}
}AsString()- stringAsBool()- bool with errorAsInt(),AsInt8(),AsInt16(),AsInt32(),AsInt64()- integers with errorAsUint(),AsUint8(),AsUint16(),AsUint32(),AsUint64()- unsigned integers with errorAsFloat32(),AsFloat64()- floats with error
// Clone tag with all children up to specified depthclonedTag, err:=tag.CloneDown(2) // Clone 2 levels deep// Clone tag and its parents up to specified depthrootClone, err:=tag.CloneUp(3, false) // Clone up 3 parent levels// Remove child from parentparent.RemoveChild(childTag)// Serialize AST back to HTML stringserializer:=parser.NewHtmlSerializer()
htmlString:=serializer.RenderHtml(ast)
fmt.Println(htmlString)htmlParser:=parser.NewHtmlParser()
// Add custom handler for specific tagshtmlParser.AddCustomAttributeHandler("my-tag", func(scanner*Scanner) (*HtmlTag, error) {
// Custom parsing logic for <my-tag>// Return custom HtmlTag
})package main
import (
"fmt"
parser "github.com/DilemaFixer/HtmlParser"
)
constHTML=`<!DOCTYPE html><html lang="en"><head> <meta charset="utf-8" /> <title>Sample Page</title> <style> .highlight { background: yellow; } </style></head><body> <!-- This is a comment --> <div class="container" id="main"> <h1 class="highlight">Welcome</h1> <p>Paragraph with <strong>bold</strong> and <em>italic</em> text.</p> <img src="image.jpg" alt="Description" width="100" height="50" /> <ul> <li data-id="1">Item 1</li> <li data-id="2">Item 2</li> </ul> </div> <script> console.log("Hello from script"); </script></body></html>`funcmain() {
htmlParser:=parser.NewHtmlParser()
ast, err:=htmlParser.ParseHtml(HTML)
iferr!=nil {
fmt.Println("Parsing error:", err)
return
}
// Find specific elementsfindDivs(ast)
// Modify and serializemodifyAndSerialize(ast)
}
funcfindDivs(tags []*HtmlTag) {
for_, tag:=rangetags {
iftag.Name=="div" {
fmt.Printf("Found div with class: %s\n", tag.GetAttribute("class").AsString())
}
// Recursively search childrenfindDivs(tag.Children)
}
}
funcmodifyAndSerialize(ast []*HtmlTag) {
// Find and modify the first h1for_, tag:=rangeast {
h1:=findFirstH1(tag)
ifh1!=nil {
h1.SetAttribute("style", "color: red;")
h1.InnerContent="Modified Title"break
}
}
// Serialize back to HTMLserializer:=parser.NewHtmlSerializer()
html:=serializer.RenderHtml(ast)
fmt.Println("Modified HTML:")
fmt.Println(html)
}
funcfindFirstH1(tag*HtmlTag) *HtmlTag {
iftag.Name=="h1" {
returntag
}
for_, child:=rangetag.Children {
ifresult:=findFirstH1(child); result!=nil {
returnresult
}
}
returnnil
}// Find all form inputsfuncextractFormData(tag*HtmlTag) map[string]string {
formData:=make(map[string]string)
iftag.Name=="input" {
nameAttr:=tag.GetAttribute("name")
valueAttr:=tag.GetAttribute("value")
ifnameAttr!=nil&&valueAttr!=nil {
formData[nameAttr.AsString()] =valueAttr.AsString()
}
}
// Recursively process childrenfor_, child:=rangetag.Children {
childData:=extractFormData(child)
fork, v:=rangechildData {
formData[k] =v
}
}
returnformData
}The parser provides detailed error messages with line and column information:
ast, err:=htmlParser.ParseHtml(invalidHtml)
iferr!=nil {
fmt.Printf("Parsing failed: %s\n", err.Error())
// Output: Html parsing error: unclosed tag 'div' at 5:12
}- ✅ Standard HTML tags
- ✅ Self-closing tags (
<img />,<br />) - ✅ Raw content tags (
<style>,<script>) - ✅ HTML comments (
<!-- comment -->) - ✅ DOCTYPE declarations
- ✅ Attributes with and without values
- ✅ Quoted and unquoted attribute values
- ✅ Nested tag structures
- ✅ Text content preservation
This project is part of the HtmlPuzzles project.
Feel free to submit issues and pull requests to improve the parser functionality.