├── .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 |
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