├── .gitignore ├── README.md ├── engine ├── css │ └── parser.go ├── html │ └── parser.go └── render │ ├── displayList.go │ ├── layoutTree.go │ ├── paint.go │ └── renderTree.go ├── example ├── example.css └── example.html ├── go.mod ├── go.sum ├── image.png ├── main.go ├── main.wasm └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.png 3 | !image.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pandora 🏺✨ 2 | 3 | Pandora is toy browser engine written in Golang and compiled in WASI. 4 | Because why not? 5 | 6 | (I just wanted to know how browser render stuff, so I tried to build a browser engine that receives `html` and `css` and outputs a `png`). 7 | 8 | Okay. There's no fancy stuff like positioning, z-indexes, flexbox etc... it's very basic. 9 | It was really complex (for my level) and forgive me for errors in advance. Any contribution is welcomed!! 10 | 11 |
12 | 13 | 14 | ```html 15 | 16 | 17 | Pandora 18 | 19 | 20 |
21 |

Hello

22 |

World

23 |
24 | 25 | 26 | ``` 27 | ```css 28 | html { 29 | background-color: rgb(255, 224, 193); 30 | width: 500px; 31 | height: 500px; 32 | } 33 | 34 | body { 35 | display: block; 36 | background-color: rgb(99, 35, 0); 37 | width: 480px; 38 | height: 480px; 39 | top: 10px; 40 | left: 10px; 41 | } 42 | 43 | #container { 44 | background-color: rgb(0, 126, 164); 45 | width: 280px; 46 | height: 280px; 47 | top: 100px; 48 | left: 100px; 49 | } 50 | 51 | .paragraph { 52 | background-color: rgb(228, 139, 255); 53 | width: 200px; 54 | height: 100px; 55 | top: 10px; 56 | left: 10px; 57 | } 58 | 59 | .another-text { 60 | background-color: rgb(0, 58, 103); 61 | width: 140px; 62 | height: 80px; 63 | top: 120px; 64 | left: 10px; 65 | } 66 | ``` 67 | 68 | Pandora 69 | 70 | Anyway you can play around with the `html` structure and the `css` rules (top, left, background-color) 71 | 72 | # Why ❓ 73 | 74 | Why not? 75 | 76 | Joke... I've built Pandora because I wanted to learn how browser renders web pages (and it is really complicated). 77 | So Pandora was built for LEARNERS. 78 | 79 | > I tried to document each section of the code as much as I could (I'm still doing it) with link to resources I've studied while building this, but if you want to improve it, feel free to open issues and pull requests. 80 | 81 | # How it works ❓ 82 | 83 | Reading this amazing articles about building a Browser engine I decided to try to build one in Go (as I do not know anything about Rust). 84 | 85 | - Pandora takes a `.html` and `.css`files 86 | - Builds the `DOM tree` and a very basic `CSSOM` 87 | - Builds a `Render Tree` from the two 88 | - Builds a `Layout Tree` from the Render Tree 89 | - Creates a `Display List` 90 | - Renders, creating a `.png` image 91 | 92 | # How to use ❓ 93 | 94 | To render the html and css in `example/*` 95 | 96 | Pandora can be compiled and used as a normal Go program 97 | 98 | ```bash 99 | pandora 100 | ``` 101 | 102 | ```bash 103 | pandora --html example/example.html --css example/example 104 | ``` 105 | 106 | Pandora can also be used as a WASI 107 | 108 | With `wasmtime` (specify the directory for the `--dir` flag in which start looking for the files) 109 | 110 | ```bash 111 | wasmtime --dir . main.wasm -- --html example/example.html --css example/example.css 112 | ``` 113 | 114 | Pandora supports `background-color` `top` `left` `margin` `margin-top` etc... 115 | Follow the Css sample. 116 | Obviously you can contribute whenever you want to make Pandora support more stuff !! 117 | 118 | # Requirements ✋ 119 | 120 | - Go 121 | - TinyGo 122 | - Wasmtime / wasmer or any WASI runtime 123 | 124 | Build 125 | 126 | Go 127 | ```bash 128 | go build 129 | ``` 130 | 131 | WASI 132 | 133 | ```bash 134 | tinygo build -wasm-abi=generic -target=wasi -o main.wasm main.go 135 | ``` 136 | 137 | # Roadmap 138 | 139 | 1.

Fonts

140 | 2.

Display block / inline / inline - block

141 | 142 | # Contributing 143 | 144 | To contribute simply 145 | - create a branch with the feature or the bug fix 146 | - open pull requests 147 | 148 | Pandora is by no means finished there are a lot of things that can be implemented and A LOT of things that can be improved. Any suggestions, pull requests, issues or feedback is greatly welcomed!!! -------------------------------------------------------------------------------- /engine/css/parser.go: -------------------------------------------------------------------------------- 1 | package css 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // We do a similar thing we've done with html 10 | // -split into tokens 11 | // -parse the tokens 12 | 13 | // then we do additional steps 14 | // - create the rules 15 | // - append the rules to the stylesheet 16 | // Each rule has a selector and properties 17 | 18 | type Rule struct { 19 | Selector string 20 | Properties map[string]string 21 | } 22 | 23 | type Stylesheet struct { 24 | Rules []*Rule 25 | } 26 | 27 | // The stylesheet content as string 28 | func (s *Stylesheet) String() string { 29 | var buffer bytes.Buffer 30 | 31 | for _, rule := range s.Rules { 32 | buffer.WriteString(rule.Selector) 33 | buffer.WriteString(" {\n") 34 | for property, value := range rule.Properties { 35 | buffer.WriteString(" ") 36 | buffer.WriteString(property) 37 | buffer.WriteString(": ") 38 | buffer.WriteString(value) 39 | buffer.WriteString("\n") 40 | } 41 | buffer.WriteString("}\n") 42 | } 43 | 44 | return buffer.String() 45 | } 46 | 47 | func ParseCSS(css string) (*Stylesheet, error) { 48 | stylesheet := &Stylesheet{ 49 | Rules: []*Rule{}, 50 | } 51 | 52 | tokens := tokenize(css) 53 | stylesheet.Rules = parseRules(tokens) 54 | 55 | return stylesheet, nil 56 | } 57 | 58 | func tokenize(css string) []string { 59 | // split into tokens 60 | regex := regexp.MustCompile(`(.*[^\s])\s?`) 61 | 62 | tokens := regex.FindAllString(css, -1) 63 | 64 | return tokens 65 | } 66 | 67 | func parseRules(tokens []string) []*Rule { 68 | rules := []*Rule{} 69 | 70 | for _, token := range tokens { 71 | token = strings.TrimSpace(token) 72 | 73 | // Check if the token is a rule 74 | if strings.HasSuffix(token, "{") { 75 | // Get the selector from the token 76 | selector := strings.TrimSuffix(token, "{") 77 | 78 | rules = append(rules, &Rule{ 79 | Selector: selector, 80 | Properties: map[string]string{}, 81 | }) 82 | } else if strings.Contains(token, ":") { 83 | // If token is a property and value 84 | // we use the same method we used in the html parser to get the key value from the attributes 85 | partsOfProperty := strings.SplitN(token, ":", 2) 86 | property := strings.TrimSpace(partsOfProperty[0]) 87 | value := strings.TrimSpace(partsOfProperty[1]) 88 | 89 | lastRule := rules[len(rules)-1] 90 | lastRule.Properties[property] = value 91 | } 92 | } 93 | 94 | return rules 95 | } 96 | -------------------------------------------------------------------------------- /engine/html/parser.go: -------------------------------------------------------------------------------- 1 | package html 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Parse the HTML code. 9 | // This involves breaking the code up into individual tokens, such as tags, attributes, and text content, 10 | // and organizing them into a tree-like structure that represents the hierarchical structure of the page 11 | 12 | // Convert the tokens into a DOM tree. 13 | // The DOM tree is a representation of the page's structure and content that can be understood by the browser engine. 14 | // It includes nodes for each element on the page, such as headings, paragraphs, images, and links, 15 | // as well as their attributes and text content. 16 | 17 | // Traverse the DOM tree and generate output. 18 | // Once the DOM tree has been constructed, you can traverse it and generate output that represents the tree's structure and content. 19 | // This could be done using a simple recursive algorithm that visits each node in the tree 20 | // and outputs its information in a suitable format. 21 | 22 | // Optionally, perform additional processing on the DOM tree. 23 | // Depending on the requirements of your browser engine, you may want to perform additional processing on the DOM tree, 24 | // such as applying CSS styles, calculating layout, or executing JavaScript code. 25 | // These steps are typically performed by the browser engine itself, 26 | // but you could also add them to your HTML parser if desired. 27 | 28 | // The HTML parsing algorithm -> https://html.spec.whatwg.org/multipage/parsing.html#parsing 29 | 30 | type Node struct { 31 | Name string 32 | 33 | Attributes map[string]string 34 | 35 | Children []*Node 36 | 37 | Text string 38 | } 39 | 40 | // parses the html string and returns the root node 41 | func ParseHTML(html string) (*Node, error) { 42 | stack := []*Node{{}} 43 | 44 | pos := 0 45 | 46 | for pos < len(html) { 47 | 48 | // we look for the next '<' character 49 | // if there is no more '<' means we reached the end since 50 | // closes the document 51 | 52 | next := strings.IndexByte(html[pos:], '<') 53 | 54 | if next == -1 { 55 | // we passed the 56 | break 57 | } 58 | 59 | next += pos 60 | 61 | // is it end of tag or start of tag? 62 | if html[next+1] == '/' { 63 | if len(stack) == 0 { 64 | return nil, fmt.Errorf("no matching closing tag at position %d", next) 65 | } 66 | // end of the tag we pop from the stack 67 | stack = stack[:len(stack)-1] 68 | pos = next + 1 69 | 70 | } else { 71 | // start of the tag 72 | node, end := parseTag(html[next+1:]) 73 | 74 | if node == nil { 75 | // if the tag is not well formed skip 76 | pos = end + next + 1 77 | continue 78 | } 79 | 80 | // add the new node as a child of the current node 81 | stack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node) 82 | 83 | stack = append(stack, node) 84 | pos = end + next + 1 85 | } 86 | } 87 | 88 | // return the root node 89 | return stack[0], nil 90 | } 91 | 92 | // parses the tag and returns the node 93 | func parseTag(html string) (*Node, int) { 94 | // find the index of the next opening < character 95 | textEnd := strings.IndexByte(html, '<') 96 | 97 | // find the index of the closing > character 98 | end := strings.IndexByte(html, '>') 99 | 100 | if end == -1 { 101 | return nil, 0 102 | } 103 | 104 | var text string 105 | 106 | if textEnd != -1 { 107 | text = html[:textEnd] 108 | } 109 | 110 | text = strings.TrimSpace(text) 111 | 112 | // remove the opening tag from the text 113 | 114 | // we parse the tag and the attributes 115 | partsOfTag := strings.Split(html[:end], " ") 116 | name := partsOfTag[0] 117 | text = strings.TrimPrefix(text, html[:end]+">") 118 | 119 | attributes := make(map[string]string) 120 | for _, part := range partsOfTag[1:] { 121 | // Attributes are written like class="something" 122 | // so we can use SplitN to get the attr name and the value 123 | attributeAtXRay := strings.SplitN(part, "=", 2) 124 | if len(attributeAtXRay) == 2 { 125 | attributes[attributeAtXRay[0]] = attributeAtXRay[1] 126 | } 127 | } 128 | 129 | node := &Node{ 130 | Name: name, 131 | Attributes: attributes, 132 | Children: []*Node{}, 133 | Text: text, 134 | } 135 | 136 | return node, end 137 | } 138 | 139 | // build the DOM tree 140 | func PrintTree(node *Node, indent int) { 141 | for i := 0; i < indent; i++ { 142 | fmt.Print(" ") 143 | } 144 | fmt.Printf("%s", node.Name) 145 | for k, v := range node.Attributes { 146 | fmt.Printf(" %s=%s", k, v) 147 | } 148 | fmt.Printf(" %s", node.Text) 149 | fmt.Println() 150 | 151 | // Print the node's children. 152 | for _, child := range node.Children { 153 | PrintTree(child, indent+1) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /engine/render/displayList.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import "fmt" 4 | 5 | // A display list, is a flat list of visual elements that are ready to be rendered. 6 | // This makes it faster to render the elements on the screen, 7 | // because the display list can be easily processed and rendered in a single pass and we don't have to traverse 8 | // the layout tree all the time 9 | 10 | type DisplayListItem struct { 11 | Box *Box 12 | Styles map[string]string 13 | Text string 14 | } 15 | 16 | func (dli *DisplayListItem) String() string { 17 | boxString := dli.Box.String() 18 | stylesString := fmt.Sprintf("%+v", dli.Styles) 19 | 20 | // Return a string representation of the DisplayListItem 21 | return fmt.Sprintf("DisplayListItem{Box: %s, Styles: %s}", boxString, stylesString) 22 | } 23 | 24 | type DisplayList []*DisplayListItem 25 | 26 | func (lt *LayoutTree) BuildDisplayList() DisplayList { 27 | displayList := make(DisplayList, 0) 28 | buildDisplayListForNode(lt.Root, &displayList) 29 | return displayList 30 | } 31 | 32 | func buildDisplayListForNode(node *LayoutNode, displayList *DisplayList) { 33 | 34 | displayListItem := &DisplayListItem{ 35 | Box: node.Box, 36 | Styles: node.RenderNode.Styles, 37 | Text: node.RenderNode.Text, 38 | } 39 | *displayList = append(*displayList, displayListItem) 40 | 41 | for _, child := range node.Children { 42 | buildDisplayListForNode(child, displayList) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /engine/render/layoutTree.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | // A layout tree is a data structure used by a web browser to represent the hierarchical layout of the elements on the page. 4 | // It is typically used by the browser to calculate the positions and sizes of the elements on the page, 5 | // as well as their relationships with each other. 6 | 7 | // When rendering a web page, the browser uses the layout tree to determine the positions and sizes of the elements on the page. 8 | // This allows the browser to correctly position and size the elements, and to lay out the elements in a hierarchical manner. 9 | 10 | import ( 11 | "bytes" 12 | "fmt" 13 | "regexp" 14 | "strconv" 15 | ) 16 | 17 | type LayoutTree struct { 18 | Root *LayoutNode 19 | } 20 | 21 | type LayoutNode struct { 22 | Box *Box 23 | Children []*LayoutNode 24 | RenderNode *RenderNode 25 | } 26 | 27 | type Box struct { 28 | Width int 29 | Height int 30 | Padding int 31 | Margin int 32 | Position Point 33 | } 34 | 35 | type Point struct { 36 | X int 37 | Y int 38 | } 39 | 40 | func (b *Box) String() string { 41 | return fmt.Sprintf("Box{Width: %d, Height: %d, Padding: %d, Margin: %d, Position: Point{X: %d, Y: %d}}", b.Width, b.Height, b.Padding, b.Margin, b.Position.X, b.Position.Y) 42 | } 43 | 44 | func (lt *LayoutTree) String() string { 45 | return layoutTreeToString(lt.Root, 0) 46 | } 47 | 48 | func layoutTreeToString(node *LayoutNode, indent int) string { 49 | var buffer bytes.Buffer 50 | for i := 0; i < indent; i++ { 51 | buffer.WriteString(" ") 52 | } 53 | 54 | buffer.WriteString(fmt.Sprintf("(%d,%d) %dx%d", node.Box.Position.X, node.Box.Position.Y, node.Box.Width, node.Box.Height)) 55 | buffer.WriteString("\n") 56 | 57 | for _, child := range node.Children { 58 | buffer.WriteString(layoutTreeToString(child, indent+1)) 59 | } 60 | 61 | return buffer.String() 62 | } 63 | 64 | // create the layout tree 65 | func NewLayoutTree(renderTree *RenderTree) *LayoutTree { 66 | root := buildLayoutTree(renderTree.Root, Point{X: 0, Y: 0}) 67 | return &LayoutTree{ 68 | Root: root, 69 | } 70 | } 71 | 72 | func buildLayoutTree(renderNode *RenderNode, position Point) *LayoutNode { 73 | pos := Point{ 74 | Y: position.Y + parseStyle(renderNode.Styles["top"], 0) + parseStyle(renderNode.Styles["margin"], 0) + parseStyle(renderNode.Styles["margin-top"], 0) - parseStyle(renderNode.Styles["margin-bottom"], 0), 75 | X: position.X + parseStyle(renderNode.Styles["left"], 0) + parseStyle(renderNode.Styles["margin"], 0) - parseStyle(renderNode.Styles["margin-right"], 0), 76 | } 77 | 78 | box := &Box{ 79 | Width: parseStyle(renderNode.Styles["width"], 0), 80 | Height: parseStyle(renderNode.Styles["height"], 0), 81 | Margin: parseStyle(renderNode.Styles["margin"], 0), 82 | Position: pos, 83 | } 84 | 85 | layoutNode := &LayoutNode{ 86 | Box: box, 87 | Children: []*LayoutNode{}, 88 | RenderNode: renderNode, 89 | } 90 | 91 | childX := box.Position.X + box.Margin + box.Padding 92 | childY := box.Position.Y + box.Margin + box.Padding 93 | 94 | for _, child := range renderNode.Children { 95 | childLayout := buildLayoutTree(child, Point{X: childX, Y: childY}) 96 | layoutNode.Children = append(layoutNode.Children, childLayout) 97 | } 98 | 99 | return layoutNode 100 | } 101 | 102 | func parseStyle(value string, defaultValue int) int { 103 | if value == "" { 104 | return defaultValue 105 | } 106 | 107 | regex := regexp.MustCompile(`(\d+)`) 108 | 109 | // Use the FindString function to extract the numeric part of the string. 110 | numericPart := regex.FindString(value) 111 | 112 | val, err := strconv.Atoi(numericPart) 113 | 114 | if err != nil { 115 | return defaultValue 116 | } 117 | return val 118 | } 119 | -------------------------------------------------------------------------------- /engine/render/paint.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "strconv" 7 | "strings" 8 | 9 | "golang.org/x/image/draw" 10 | "golang.org/x/image/font" 11 | "golang.org/x/image/font/basicfont" 12 | "golang.org/x/image/math/fixed" 13 | ) 14 | 15 | func PaintNode(img *image.RGBA, displayListItem *DisplayListItem) { 16 | 17 | posX := displayListItem.Box.Position.X 18 | posY := displayListItem.Box.Position.Y 19 | width := displayListItem.Box.Width 20 | height := displayListItem.Box.Height 21 | 22 | // Define the rectangles to draw 23 | rect := image.Rect(posX, posY, posX+width, posY+height) 24 | 25 | // Define the color to use for the rectangles 26 | r, g, b, a := parseColor(displayListItem.Styles["background-color"]) 27 | 28 | col := color.RGBA{R: r, G: g, B: b, A: a} 29 | draw.Draw(img, rect, &image.Uniform{col}, image.Point{}, draw.Src) 30 | drawText(img, displayListItem) 31 | } 32 | 33 | func drawText(img *image.RGBA, displayListItem *DisplayListItem) { 34 | 35 | f := basicfont.Face7x13 36 | text := displayListItem.Text 37 | 38 | // Draw the text on the image 39 | point := fixed.Point26_6{ 40 | X: fixed.I(displayListItem.Box.Position.X) + ((fixed.I(displayListItem.Box.Width) / 2) - (font.MeasureString(f, text) / 2)), 41 | Y: fixed.I(displayListItem.Box.Position.Y) + (fixed.I(displayListItem.Box.Height) / 2), 42 | } 43 | d := &font.Drawer{ 44 | Dst: img, 45 | Src: image.White, 46 | Face: f, 47 | Dot: point, 48 | } 49 | d.DrawString(text) 50 | } 51 | 52 | // get the color from the CSS property 53 | func parseColor(colorString string) (uint8, uint8, uint8, uint8) { 54 | if strings.HasPrefix(colorString, "rgb(") && strings.HasSuffix(colorString, ");") { 55 | 56 | colorString = strings.TrimPrefix(colorString, "rgb(") 57 | colorString = strings.TrimSuffix(colorString, ");") 58 | 59 | parts := strings.Split(colorString, ",") 60 | 61 | r, err := strconv.Atoi(strings.TrimSpace(parts[0])) 62 | if err != nil { 63 | return 0, 0, 0, 255 64 | } 65 | 66 | g, err := strconv.Atoi(strings.TrimSpace(parts[1])) 67 | if err != nil { 68 | return 0, 0, 0, 255 69 | } 70 | 71 | b, err := strconv.Atoi(strings.TrimSpace(parts[2])) 72 | 73 | if err != nil { 74 | return 0, 0, 0, 255 75 | } 76 | 77 | return uint8(r), uint8(g), uint8(b), 255 78 | } 79 | 80 | return 0, 0, 0, 0 81 | } 82 | -------------------------------------------------------------------------------- /engine/render/renderTree.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "pandora/engine/css" 6 | "pandora/engine/html" 7 | "strings" 8 | ) 9 | 10 | // A render tree is a data structure that represents the visual structure and content of a web page. 11 | // It is used by a web browser to render the page on the user's screen. 12 | // The render tree is constructed by combining the DOM tree (which represents the structural hierarchy of the page) 13 | // with the CSSOM (which represents the styles applied to the page elements). 14 | // The resulting render tree specifies the precise position and appearance of each element on the page, 15 | // as well as any interactions or animations that are defined in the page's CSS or JavaScript code. 16 | 17 | // - Parse the HTML and CSS code to generate a DOM tree and a CSSOM. 18 | // - Traverse the DOM tree and match each element with the appropriate CSS rules from the CSSOM. 19 | // - Create a new tree-like structure that represents the final visual representation of the page, including the layout and styles of each element. 20 | // - Use the render tree to generate the final visual representation of the page on the user's screen. 21 | 22 | type RenderTree struct { 23 | Root *RenderNode 24 | } 25 | 26 | func (rt *RenderTree) String() string { 27 | return renderTreeToString(rt.Root, 0) 28 | } 29 | 30 | func renderTreeToString(node *RenderNode, indent int) string { 31 | var buffer bytes.Buffer 32 | 33 | // Add indentation 34 | for i := 0; i < indent; i++ { 35 | buffer.WriteString(" ") 36 | } 37 | 38 | // Add the node's tag and attributes 39 | buffer.WriteString(node.Tag) 40 | for key, value := range node.Attributes { 41 | buffer.WriteString(" ") 42 | buffer.WriteString(key) 43 | buffer.WriteString("=") 44 | buffer.WriteString(value) 45 | } 46 | 47 | // Add the node's text and styles 48 | if node.Text != "" { 49 | buffer.WriteString(": ") 50 | buffer.WriteString(strings.TrimSpace(node.Text)) 51 | } 52 | if len(node.Styles) > 0 { 53 | buffer.WriteString(" {") 54 | for key, value := range node.Styles { 55 | buffer.WriteString(" ") 56 | buffer.WriteString(key) 57 | buffer.WriteString(": ") 58 | buffer.WriteString(value) 59 | } 60 | buffer.WriteString(" }") 61 | } 62 | buffer.WriteString("\n") 63 | 64 | // Recursively add the node's children 65 | for _, child := range node.Children { 66 | buffer.WriteString(renderTreeToString(child, indent+1)) 67 | } 68 | 69 | return buffer.String() 70 | } 71 | 72 | type RenderNode struct { 73 | Tag string 74 | Attributes map[string]string 75 | Text string 76 | Styles map[string]string 77 | Children []*RenderNode 78 | } 79 | 80 | func NewRenderTree(dom *html.Node, stylesheet *css.Stylesheet) *RenderTree { 81 | root := buildRenderTree(dom, stylesheet) 82 | return &RenderTree{ 83 | Root: root, 84 | } 85 | } 86 | 87 | func buildRenderTree(domNode *html.Node, stylesheet *css.Stylesheet) *RenderNode { 88 | renderNode := &RenderNode{ 89 | Tag: domNode.Name, 90 | Attributes: domNode.Attributes, 91 | Text: domNode.Text, 92 | Children: []*RenderNode{}, 93 | } 94 | 95 | styles := lookupStyles(domNode, stylesheet) 96 | renderNode.Styles = styles 97 | 98 | for _, child := range domNode.Children { 99 | renderChild := buildRenderTree(child, stylesheet) 100 | renderNode.Children = append(renderNode.Children, renderChild) 101 | } 102 | 103 | return renderNode 104 | } 105 | 106 | func lookupStyles(domNode *html.Node, stylesheet *css.Stylesheet) map[string]string { 107 | // Look up the styles for the element in the stylesheet 108 | styles := map[string]string{} 109 | for _, rule := range stylesheet.Rules { 110 | if strings.TrimSpace(rule.Selector) == domNode.Name { 111 | styles = rule.Properties 112 | break 113 | } 114 | 115 | // Check if the selector matches the element's class attribute 116 | if strings.HasPrefix(rule.Selector, ".") { 117 | class := strings.TrimPrefix(rule.Selector, ".") 118 | if strings.ReplaceAll(domNode.Attributes["class"], "\"", "") == strings.TrimSpace(class) { 119 | styles = rule.Properties 120 | break 121 | } 122 | } 123 | 124 | // Check if the selector matches the element's ID attribute 125 | if strings.HasPrefix(rule.Selector, "#") { 126 | id := strings.TrimPrefix(rule.Selector, "#") 127 | if strings.ReplaceAll(domNode.Attributes["id"], "\"", "") == strings.TrimSpace(id) { 128 | styles = rule.Properties 129 | break 130 | } 131 | } 132 | } 133 | return styles 134 | } 135 | -------------------------------------------------------------------------------- /example/example.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: rgb(255, 224, 193); 3 | width: 500px; 4 | height: 500px; 5 | } 6 | 7 | body { 8 | display: block; 9 | background-color: rgb(99, 35, 0); 10 | width: 480px; 11 | height: 480px; 12 | top: 10px; 13 | left: 10px; 14 | } 15 | 16 | #container { 17 | background-color: rgb(0, 126, 164); 18 | width: 280px; 19 | height: 280px; 20 | top: 100px; 21 | left: 100px; 22 | } 23 | 24 | .paragraph { 25 | background-color: rgb(228, 139, 255); 26 | width: 200px; 27 | height: 100px; 28 | top: 10px; 29 | left: 10px; 30 | } 31 | 32 | .another-text { 33 | background-color: rgb(0, 58, 103); 34 | width: 140px; 35 | height: 80px; 36 | top: 120px; 37 | left: 10px; 38 | } 39 | 40 | -------------------------------------------------------------------------------- /example/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pandora 4 | 5 | 6 |
7 |

Hello

8 |

World

9 |
10 | 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module pandora 2 | 3 | go 1.19 4 | 5 | require golang.org/x/image v0.0.0-20220617043117-41969df76e82 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw= 2 | golang.org/x/image v0.0.0-20220617043117-41969df76e82/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= 3 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 4 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 5 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomscoder/pandora/f2355a4d9bebe7e3cbc12f90bc28b9d4e643a6fe/image.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "image" 6 | "image/color" 7 | "image/draw" 8 | "image/png" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "pandora/engine/css" 13 | "pandora/engine/html" 14 | "pandora/engine/render" 15 | ) 16 | 17 | // A browser engine, also known as a layout engine or rendering engine, 18 | // is the part of a web browser that is responsible for interpreting and rendering HTML, CSS, and other web content. 19 | 20 | func check(err error) { 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | } 25 | 26 | func main() { 27 | htmlFile := flag.String("html", "example/example.html", "the name of the html to search") 28 | cssFile := flag.String("css", "example/example.css", "the name of the html to search") 29 | 30 | // Parse the command-line flags 31 | flag.Parse() 32 | 33 | // Print the value of the flag 34 | _html, err := ioutil.ReadFile(*htmlFile) 35 | check(err) 36 | 37 | _css, err := ioutil.ReadFile(*cssFile) 38 | check(err) 39 | 40 | rootNode, err := html.ParseHTML(string(_html)) 41 | check(err) 42 | 43 | stylesheet, err := css.ParseCSS(string(_css)) 44 | check(err) 45 | 46 | renderTree := render.NewRenderTree(rootNode, stylesheet) 47 | 48 | layoutTree := render.NewLayoutTree(renderTree) 49 | displayList := layoutTree.BuildDisplayList() 50 | 51 | file, err := os.Create("image.png") 52 | check(err) 53 | defer file.Close() 54 | 55 | img := image.NewRGBA(image.Rect(0, 0, 500, 500)) 56 | draw.Draw(img, img.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src) 57 | 58 | for _, item := range displayList { 59 | render.PaintNode(img, item) 60 | } 61 | 62 | if err := png.Encode(file, img); err != nil { 63 | panic(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomscoder/pandora/f2355a4d9bebe7e3cbc12f90bc28b9d4e643a6fe/main.wasm -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pandora", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC" 13 | } 14 | --------------------------------------------------------------------------------