DEV Community

Murahashi [Matt] Kenichi
Murahashi [Matt] Kenichi

Posted on

2 1

Toy browser engine current WIP status

Goal: Understand how to transform html and css to image.

If I write simple html and css:

<html>
  <body>
    <div class="outer">
      <div class="inner" />
    <div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode
* { display: block; padding: 12px; }
.outer { background: #800000; }
.inner { background: #0000ff; }
Enter fullscreen mode Exit fullscreen mode

I execute this:

node_modules/.bin/ts-node example/toy-engine-cli.ts --css=index.css --html=index.html | display
Enter fullscreen mode Exit fullscreen mode

I will get this image!

Current Status

node_modules/.bin/ts-node example/demo.ts | display
Enter fullscreen mode Exit fullscreen mode

It works!

Look back to pipeline.

https://github.com/sanemat/ts-toy-engine
https://dev.to/sanemat/let-s-build-browser-engine-in-typescript-vol0-toy-browser-engine-egm

HTML -(HTML parser)-> DOM

Not Yet!

shortcut

// <html>
//   <body>
//     <div class="outer">
//       <div class="inner" />
//     <div>
//   </body>
// </html>

const domNode = elem("html", new Map([]), []);
const body = elem("body", new Map([]), []);
const outer = elem("div", new Map([["class", "outer"]]), []);
const inner = elem("div", new Map([["class", "inner"]]), []);
domNode.children.push(body);
body.children.push(outer);
outer.children.push(inner);

// console.dir(domNode, { depth: null });
//
// DomNode {
//   children: [
//     DomNode {
//       children: [
//         DomNode {
//           children: [
//             DomNode {
//               children: [],
//               nodeType: Element {
//                 format: 0,
//                 element: ElementData {
//                   tagName: 'div',
//                   attributes: Map { 'class' => 'inner' }
//                 }
//               }
//             }
//           ],
//           nodeType: Element {
//             format: 0,
//             element: ElementData {
//               tagName: 'div',
//               attributes: Map { 'class' => 'outer' }
//             }
//           }
//         }
//       ],
//       nodeType: Element {
//         format: 0,
//         element: ElementData { tagName: 'body', attributes: Map {} }
//       }
//     }
//   ],
//   nodeType: Element {
//     format: 0,
//     element: ElementData { tagName: 'html', attributes: Map {} }
//   }
// }
Enter fullscreen mode Exit fullscreen mode

CSS -(CSS Parser)-> Rules

Not Yet!

shortcut

// * { display: block; padding: 12px; }
// .outer { background: maroon; /* 800000 */ }
// .inner { background: blue; /* 0000ff */ }

const stylesheet = new Stylesheet([]);
stylesheet.rules.push(
  new Rule(
    [new Selector.Simple(new SimpleSelector(null, null, []))],
    [
      new Declaration("display", new CssValue.Keyword("block")),
      new Declaration("padding", new CssValue.Length(12, Unit.Px))
    ]
  )
);
stylesheet.rules.push(
  new Rule(
    [new Selector.Simple(new SimpleSelector(null, null, ["outer"]))],
    [new Declaration("background", new CssValue.ColorValue(new Color(128, 0, 0, 255)))]
  )
);
stylesheet.rules.push(
  new Rule(
    [new Selector.Simple(new SimpleSelector(null, null, ["inner"]))],
    [new Declaration("background", new CssValue.ColorValue(new Color(0, 0, 255, 255)))]
  )
);

// console.dir(stylesheet, { depth: null });
Enter fullscreen mode Exit fullscreen mode

DOM+Rules -> Style Tree

const styleRoot = styleTree(domNode, stylesheet);
// console.dir(styleRoot, { depth: null });
// Stylesheet {
//   rules: [
//     Rule {
//       selectors: [
//         Simple {
//           format: 0,
//           selector: SimpleSelector { tagName: null, id: null, classValue: [] }
//         }
//       ],
//       declarations: [
//         Declaration {
//           name: 'display',
//           value: Keyword { format: 0, keyword: 'block' }
//         },
//         Declaration {
//           name: 'padding',
//           value: Length { format: 1, length: 12, unit: 0 }
//         }
//       ]
//     },
//     Rule {
//       selectors: [
//         Simple {
//           format: 0,
//           selector: SimpleSelector {
//             tagName: null,
//             id: null,
//             classValue: [ 'outer' ]
//           }
//         }
//       ],
//       declarations: [
//         Declaration {
//           name: 'background',
//           value: ColorValue {
//             format: 2,
//             colorValue: Color { r: 128, g: 0, b: 0, a: 255 }
//           }
//         }
//       ]
//     },
//     Rule {
//       selectors: [
//         Simple {
//           format: 0,
//           selector: SimpleSelector {
//             tagName: null,
//             id: null,
//             classValue: [ 'inner' ]
//           }
//         }
//       ],
//       declarations: [
//         Declaration {
//           name: 'background',
//           value: ColorValue {
//             format: 2,
//             colorValue: Color { r: 0, g: 0, b: 255, a: 255 }
//           }
//         }
//       ]
//     }
//   ]
// }
Enter fullscreen mode Exit fullscreen mode

Style Tree -> Layout Tree

const viewport = new Dimensions(
  new Rect(0, 0, 200, 100),
  new EdgeSizes(0, 0, 0, 0),
  new EdgeSizes(0, 0, 0, 0),
  new EdgeSizes(0, 0, 0, 0)
);

const layoutRoot = layoutTree(styleRoot, viewport);
// console.dir(layoutRoot, { depth: null });
// LayoutBox {
//   dimensions: Dimensions {
//     content: Rect { x: 12, y: 12, width: 176, height: 72 },
//     padding: EdgeSizes { left: 12, right: 12, top: 12, bottom: 12 },
//     border: EdgeSizes { left: 0, right: 0, top: 0, bottom: 0 },
//     margin: EdgeSizes { left: 0, right: 0, top: 0, bottom: 0 }
//   },
//   boxType: BlockNode {
//     format: 0,
//     styledNode: StyledNode {
//       node: DomNode {
//         children: [
//           DomNode {
//             children: [
//               DomNode {
//                 children: [
//                   DomNode {
//                     children: [],
//                     nodeType: Element {
//                       format: 0,
//                       element: ElementData {
//                         tagName: 'div',
//                         attributes: Map { 'class' => 'inner' }
//                       }
//                     }
//                   }
//                 ],
//                 nodeType: Element {
//                   format: 0,
//                   element: ElementData {
//                     tagName: 'div',
//                     attributes: Map { 'class' => 'outer' }
//                   }
//                 }
//               }
//             ],
//             nodeType: Element {
//               format: 0,
//               element: ElementData { tagName: 'body', attributes: Map {} }
//             }
//           }
//         ],
//         nodeType: Element {
//           format: 0,
//           element: ElementData { tagName: 'html', attributes: Map {} }
//         }
//       },
//       specifiedValues: Map {
//         'display' => Keyword { format: 0, keyword: 'block' },
//         'padding' => Length { format: 1, length: 12, unit: 0 }
//       },
//       children: [
//         StyledNode {
//           node: DomNode {
//             children: [
//               DomNode {
//                 children: [
//                   DomNode {
//                     children: [],
//                     nodeType: Element {
//                       format: 0,
//                       element: ElementData {
//                         tagName: 'div',
//                         attributes: Map { 'class' => 'inner' }
//                       }
//                     }
//                   }
//                 ],
//                 nodeType: Element {
//                   format: 0,
//                   element: ElementData {
//                     tagName: 'div',
//                     attributes: Map { 'class' => 'outer' }
//                   }
//                 }
//               }
//             ],
//             nodeType: Element {
//               format: 0,
//               element: ElementData { tagName: 'body', attributes: Map {} }
//             }
//           },
//           specifiedValues: Map {
//             'display' => Keyword { format: 0, keyword: 'block' },
//             'padding' => Length { format: 1, length: 12, unit: 0 }
//           },
//           children: [
//             StyledNode {
//               node: DomNode {
//                 children: [
//                   DomNode {
//                     children: [],
//                     nodeType: Element {
//                       format: 0,
//                       element: ElementData {
//                         tagName: 'div',
//                         attributes: Map { 'class' => 'inner' }
//                       }
//                     }
//                   }
//                 ],
//                 nodeType: Element {
//                   format: 0,
//                   element: ElementData {
//                     tagName: 'div',
//                     attributes: Map { 'class' => 'outer' }
//                   }
//                 }
//               },
//               specifiedValues: Map {
//                 'display' => Keyword { format: 0, keyword: 'block' },
//                 'padding' => Length { format: 1, length: 12, unit: 0 },
//                 'background' => ColorValue {
//                   format: 2,
//                   colorValue: Color { r: 128, g: 0, b: 0, a: 255 }
//                 }
//               },
//               children: [
//                 StyledNode {
//                   node: DomNode {
//                     children: [],
//                     nodeType: Element {
//                       format: 0,
//                       element: ElementData {
//                         tagName: 'div',
//                         attributes: Map { 'class' => 'inner' }
//                       }
//                     }
//                   },
//                   specifiedValues: Map {
//                     'display' => Keyword { format: 0, keyword: 'block' },
//                     'padding' => Length { format: 1, length: 12, unit: 0 },
//                     'background' => ColorValue {
//                       format: 2,
//                       colorValue: Color { r: 0, g: 0, b: 255, a: 255 }
//                     }
//                   },
//                   children: []
//                 }
//               ]
//             }
//           ]
//         }
//       ]
//     }
//   },
//   children: [
//     LayoutBox {
//       dimensions: Dimensions {
//         content: Rect { x: 24, y: 24, width: 152, height: 48 },
//         padding: EdgeSizes { left: 12, right: 12, top: 12, bottom: 12 },
//         border: EdgeSizes { left: 0, right: 0, top: 0, bottom: 0 },
//         margin: EdgeSizes { left: 0, right: 0, top: 0, bottom: 0 }
//       },
//       boxType: BlockNode {
//         format: 0,
//         styledNode: StyledNode {
//           node: DomNode {
//             children: [
//               DomNode {
//                 children: [
//                   DomNode {
//                     children: [],
//                     nodeType: Element {
//                       format: 0,
//                       element: ElementData {
//                         tagName: 'div',
//                         attributes: Map { 'class' => 'inner' }
//                       }
//                     }
//                   }
//                 ],
//                 nodeType: Element {
//                   format: 0,
//                   element: ElementData {
//                     tagName: 'div',
//                     attributes: Map { 'class' => 'outer' }
//                   }
//                 }
//               }
//             ],
//             nodeType: Element {
//               format: 0,
//               element: ElementData { tagName: 'body', attributes: Map {} }
//             }
//           },
//           specifiedValues: Map {
//             'display' => Keyword { format: 0, keyword: 'block' },
//             'padding' => Length { format: 1, length: 12, unit: 0 }
//           },
//           children: [
//             StyledNode {
//               node: DomNode {
//                 children: [
//                   DomNode {
//                     children: [],
//                     nodeType: Element {
//                       format: 0,
//                       element: ElementData {
//                         tagName: 'div',
//                         attributes: Map { 'class' => 'inner' }
//                       }
//                     }
//                   }
//                 ],
//                 nodeType: Element {
//                   format: 0,
//                   element: ElementData {
//                     tagName: 'div',
//                     attributes: Map { 'class' => 'outer' }
//                   }
//                 }
//               },
//               specifiedValues: Map {
//                 'display' => Keyword { format: 0, keyword: 'block' },
//                 'padding' => Length { format: 1, length: 12, unit: 0 },
//                 'background' => ColorValue {
//                   format: 2,
//                   colorValue: Color { r: 128, g: 0, b: 0, a: 255 }
//                 }
//               },
//               children: [
//                 StyledNode {
//                   node: DomNode {
//                     children: [],
//                     nodeType: Element {
//                       format: 0,
//                       element: ElementData {
//                         tagName: 'div',
//                         attributes: Map { 'class' => 'inner' }
//                       }
//                     }
//                   },
//                   specifiedValues: Map {
//                     'display' => Keyword { format: 0, keyword: 'block' },
//                     'padding' => Length { format: 1, length: 12, unit: 0 },
//                     'background' => ColorValue {
//                       format: 2,
//                       colorValue: Color { r: 0, g: 0, b: 255, a: 255 }
//                     }
//                   },
//                   children: []
//                 }
//               ]
//             }
//           ]
//         }
//       },
//       children: [
//         LayoutBox {
//           dimensions: Dimensions {
//             content: Rect { x: 36, y: 36, width: 128, height: 24 },
//             padding: EdgeSizes { left: 12, right: 12, top: 12, bottom: 12 },
//             border: EdgeSizes { left: 0, right: 0, top: 0, bottom: 0 },
//             margin: EdgeSizes { left: 0, right: 0, top: 0, bottom: 0 }
//           },
//           boxType: BlockNode {
//             format: 0,
//             styledNode: StyledNode {
//               node: DomNode {
//                 children: [
//                   DomNode {
//                     children: [],
//                     nodeType: Element {
//                       format: 0,
//                       element: ElementData {
//                         tagName: 'div',
//                         attributes: Map { 'class' => 'inner' }
//                       }
//                     }
//                   }
//                 ],
//                 nodeType: Element {
//                   format: 0,
//                   element: ElementData {
//                     tagName: 'div',
//                     attributes: Map { 'class' => 'outer' }
//                   }
//                 }
//               },
//               specifiedValues: Map {
//                 'display' => Keyword { format: 0, keyword: 'block' },
//                 'padding' => Length { format: 1, length: 12, unit: 0 },
//                 'background' => ColorValue {
//                   format: 2,
//                   colorValue: Color { r: 128, g: 0, b: 0, a: 255 }
//                 }
//               },
//               children: [
//                 StyledNode {
//                   node: DomNode {
//                     children: [],
//                     nodeType: Element {
//                       format: 0,
//                       element: ElementData {
//                         tagName: 'div',
//                         attributes: Map { 'class' => 'inner' }
//                       }
//                     }
//                   },
//                   specifiedValues: Map {
//                     'display' => Keyword { format: 0, keyword: 'block' },
//                     'padding' => Length { format: 1, length: 12, unit: 0 },
//                     'background' => ColorValue {
//                       format: 2,
//                       colorValue: Color { r: 0, g: 0, b: 255, a: 255 }
//                     }
//                   },
//                   children: []
//                 }
//               ]
//             }
//           },
//           children: [
//             LayoutBox {
//               dimensions: Dimensions {
//                 content: Rect { x: 48, y: 48, width: 104, height: 0 },
//                 padding: EdgeSizes { left: 12, right: 12, top: 12, bottom: 12 },
//                 border: EdgeSizes { left: 0, right: 0, top: 0, bottom: 0 },
//                 margin: EdgeSizes { left: 0, right: 0, top: 0, bottom: 0 }
//               },
//               boxType: BlockNode {
//                 format: 0,
//                 styledNode: StyledNode {
//                   node: DomNode {
//                     children: [],
//                     nodeType: Element {
//                       format: 0,
//                       element: ElementData {
//                         tagName: 'div',
//                         attributes: Map { 'class' => 'inner' }
//                       }
//                     }
//                   },
//                   specifiedValues: Map {
//                     'display' => Keyword { format: 0, keyword: 'block' },
//                     'padding' => Length { format: 1, length: 12, unit: 0 },
//                     'background' => ColorValue {
//                       format: 2,
//                       colorValue: Color { r: 0, g: 0, b: 255, a: 255 }
//                     }
//                   },
//                   children: []
//                 }
//               },
//               children: []
//             }
//           ]
//         }
//       ]
//     }
//   ]
// }
Enter fullscreen mode Exit fullscreen mode

Layout Tree -> Image

DONE

code

// node_modules/.bin/ts-node example/demo.ts | display
import { elem } from "../src/dom";
import {
  Color,
  CssValue,
  Declaration,
  Rule,
  Selector,
  SimpleSelector,
  Stylesheet,
  Unit
} from "../src/css";
import { styleTree } from "../src/style";
import { Dimensions, EdgeSizes, layoutTree, Rect } from "../src/layout";
import { paint } from "../src/painting";
import * as Jimp from "jimp";

// <html>
//   <body>
//     <div class="outer">
//       <div class="inner" />
//     <div>
//   </body>
// </html>

// * { display: block; padding: 12px; }
// .outer { background: maroon; /* 800000 */ }
// .inner { background: blue; /* 0000ff */ }

const domNode = elem("html", new Map([]), []);
const body = elem("body", new Map([]), []);
const outer = elem("div", new Map([["class", "outer"]]), []);
const inner = elem("div", new Map([["class", "inner"]]), []);
domNode.children.push(body);
body.children.push(outer);
outer.children.push(inner);

const stylesheet = new Stylesheet([]);
stylesheet.rules.push(
  new Rule(
    [new Selector.Simple(new SimpleSelector(null, null, []))],
    [
      new Declaration("display", new CssValue.Keyword("block")),
      new Declaration("padding", new CssValue.Length(12, Unit.Px))
    ]
  )
);
stylesheet.rules.push(
  new Rule(
    [new Selector.Simple(new SimpleSelector(null, null, ["outer"]))],
    [new Declaration("background", new CssValue.ColorValue(new Color(128, 0, 0, 255)))]
  )
);
stylesheet.rules.push(
  new Rule(
    [new Selector.Simple(new SimpleSelector(null, null, ["inner"]))],
    [new Declaration("background", new CssValue.ColorValue(new Color(0, 0, 255, 255)))]
  )
);

const styleRoot = styleTree(domNode, stylesheet);
const viewport = new Dimensions(
  new Rect(0, 0, 200, 100),
  new EdgeSizes(0, 0, 0, 0),
  new EdgeSizes(0, 0, 0, 0),
  new EdgeSizes(0, 0, 0, 0)
);

const layoutRoot = layoutTree(styleRoot, viewport);
const canvas = paint(layoutRoot, viewport.content);
Jimp.create(canvas.width, canvas.height)
  .then(value => {
    let buffer = value.bitmap.data;
    for (let i = 0; i < canvas.pixels.length; i++) {
      buffer[i * 4] = canvas.pixels[i].r;
      buffer[i * 4 + 1] = canvas.pixels[i].g;
      buffer[i * 4 + 2] = canvas.pixels[i].b;
      buffer[i * 4 + 3] = canvas.pixels[i].a;
    }
    return value.getBufferAsync(Jimp.MIME_PNG);
  })
  .then(value => {
    process.stdout.write(value);
  })
  .catch(error => {
    console.error(error);
  });
Enter fullscreen mode Exit fullscreen mode

Next Action

  • HTML -(HTML parser)-> DOM
  • CSS -(CSS Parser)-> Rules

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay