{"id":15147292,"url":"https://github.com/tomashubelbauer/leveret","last_synced_at":"2026-02-02T00:03:17.272Z","repository":{"id":257269477,"uuid":"857301610","full_name":"TomasHubelbauer/leveret","owner":"TomasHubelbauer","description":"A TypeScript+Bun+`canvas`-based \"web browser\"! My submission to the first-ever Browser Jam organized by @awesomekling","archived":false,"fork":false,"pushed_at":"2024-10-13T12:12:06.000Z","size":168,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-17T09:05:26.851Z","etag":null,"topics":["bun","bun-js","canvas","napi-rs","web-browser"],"latest_commit_sha":null,"homepage":"https://github.com/BrowserJam/jam001/pull/14","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TomasHubelbauer.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-09-14T09:37:41.000Z","updated_at":"2024-10-13T12:12:11.000Z","dependencies_parsed_at":"2024-09-16T01:16:18.665Z","dependency_job_id":null,"html_url":"https://github.com/TomasHubelbauer/leveret","commit_stats":null,"previous_names":["tomashubelbauer/leveret"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/TomasHubelbauer/leveret","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fleveret","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fleveret/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fleveret/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fleveret/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TomasHubelbauer","download_url":"https://codeload.github.com/TomasHubelbauer/leveret/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fleveret/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260581288,"owners_count":23031569,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bun","bun-js","canvas","napi-rs","web-browser"],"created_at":"2024-09-26T12:40:27.426Z","updated_at":"2026-02-02T00:03:17.244Z","avatar_url":"https://github.com/TomasHubelbauer.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Leveret\n\nLeveret is an experimental web browser written in TypeScript for the Bun runtime\nand it uses the Skia graphics system via `@napi-rs/canvas`.\n\nThis is how Leveret looks displaying the first-ever web page:\n\n![](index.png)\n\nLeveret started off as a submission to the seminal [Browser Jam hackathon](https://github.com/BrowserJam/browserjam)\norganized by [Andreas Kling](https://x.com/awesomekling) of the [Ladybird browser](https://ladybird.org/)\nfame.\n\nYou can find [the submission here](https://github.com/BrowserJam/jam001/pull/14).\n\nAs of current, it has not progressed beyond the hackathon submission quality\nstate; perhaps during the next Browser Jam, more improvements will be delivered.\n\n## Tasks\n\n### Use my `HTMLRewriter`-based `DOMParser` and drop the custom HTML parser here\n\nhttps://github.com/TomasHubelbauer/bun-domparser\n\n### Look into using Bun's experimental CSS parser\n\nhttps://bun.sh/blog/bun-v1.1.30#experimental-css-parsing-bundling\n\n`HTMLRewriter` will also probably be usable to implement a basic query selector\nengine by parsing the whole HTML and then parsing it again with the CSS selector\nand comparing the two trees to see which nodes of the original tree match the\nnodes of the selector-driven tree.\n\nThis should be combined with the usage of the `HTMLRewriter`-based `DOMParser` I\nmention above.\n\n### Improve the HTML parser to not set a node as `cursor` until fully closed\n\nNote that this will become obsolete once I switch to my `DOMParser` based on the\n`HTMLRewriter` API bundled with Bun.\n\nRight now, I'm materializing nodes the moment their opening tag finishes parsing\nwhich allows me to simplify the attribute parsing states, but results in an\nunfaithful representation of the incomplete DOM tree (stuff that's not fully\ncommited should be shuffled via state, not the tree) and complicates recognizing\nsome scenarios like `\u003cIMG/\u003e` versus `\u003cIMG /\u003e` and when to materialize it which\nwould be unambiguous if tags were persisted to the node tree only once closed.\n\n### Use Bun's C compilation feature to show a native window to stream RGBA onto\n\nBun is set to release built-in support for C code compilation.\nThis could allow me to use native OS APIs to call up a native window and flush a\nbuffer onto its surface.\n\n[Bun Twitter post about native code compilation](https://x.com/jarredsumner/status/1834880518757781919)\n\nUse Cocoa to display a native macOS window, make an `NSImageView` and export a\nmethod that takes `unsigned char *rgbaBuffer` and builds a `NSBitmapImageRep`\nfrom it, then `NSImage` from the bitmap and sets the image to the image view.\n\nNext up, react to the window size changes by either polling on every frame on\nthe Bun side or figuring out how to export a method that takes Bun callbacks and\ncalls them in response to the macOS side event loop resize event entries.\n\nThis will allow me to dynamically resize the `canvas` and its context to react\nto the window resizing events and re-layout.\n\nNext up, listen to keyboard and mouse events to be able to add interactivity to\nthe renderer and bring the whole project closer to a full browser.\n\nThen there is a question of the browser chrome and whether to implement it in\nnative code (I am leaning towards that right now) or as a dedicated separate\narea on the render surface (need to make sure the page can't bleed into it).\n\n### Set up a GitHub Actions workflow to publish native executables on each push\n\nUse the Bun GitHub Action to run the `compile` command and publish the resulting\nbuild artifacts to the Releases tab on GitHub.\n\n### Monitor Bun's API surface for a built-in `DOMParser` implementation support\n\nhttps://github.com/oven-sh/bun/discussions/1522\n\nThis would allow me to drop the bespoke HTML to DOM parsing logic.\nArguably, this takes away from the home-grown-ness of the web browser, but the\nlayout part would remain bespoke and I am happy to shift any part of the process\nto a Bun built-in, but not a non-built-in dependency.\n\n## Notes\n\n### Architecture overview\n\nThe script downloads the HTML text of the URL and parses it to a simple DOM tree\nwhich gets visited during the layout stage and adorned with layout metadata like\ncoordinates and dimensions of each element.\n\nText runs that are too long to fit the viewport get wrapped by breaking up their\npatern inline element into several pieces as they fit the viewport constraints.\n\nIn the final phase, the tree is walked and each node recursively rendered onto\nthe Skia-based `canvas`-like rendering surface.\n\nThe rendered frame gets saved to a file named `index.png`.\n\nThe current architecture doesn't handle user interactivity (scrolling, clicking,\nselecting etc.) nor does it render into a native OS window yet.\n\n### Technology choices\n\nTypeScript was selected because I like it as a programming language.\nIf I didn't choose TypeScript, I would have chosen JavaScript instead.\n\nI really like Bun for its versatility and battery-included nature.\nThe programming language decision was also driven by the fact that I knew I was\ngoing to use Bun as the runtime.\n\nI didn't contemplate using Node or Deno, because while they are getting better,\nthey are not nearly as pain-free to set up and get productive with as Bun is.\n\nOne example of this is the ability to use `@napi-rs/canvas` as a zero-dependency\ndependency straight-away without having to deal with Gyp or Node native modules.\nIt is possible that this would also work right away in Node or Deno, but it is\nonly one of many interactions with the toolchain that Bun makes smooth.\n\nBun has supported easy depending on and bundling of Node native modules since\nversion 1.0.23, in whose release notes I found out about `@napi-rs/canvas`:\n\n[Bun 1.0.23 release notes](https://bun.sh/blog/bun-v1.0.23#embed-node-files-with-bun-build-compile)\n\nI didn't consider using a native runtime or a runtime-free programming language\nlike C, C++, Rust, Zig or C#/.NET due to tool-chain setup unpleasantness.\nTo me, none of these are as problem-free to set up and get going with as Bun is,\nit only takes three commands to install it, install the dependencies and run the\ncode or compile it to a native executable.\n\nThe closest non-JavaScript/non-TypeScript experience to this would probably be\nRuby, but I don't like the programming language and while I prefer to minimize\nmy use of dependencies, when I have to use some, I prefer to reach for ones in\nthe Node ecosystem over the ones in the Ruby ecosystem.\n\n### Dependency choices\n\nI am using `latest` versions of all dependencies (`@napi-rs/canvas` and Bun's\ntypes as a development dependenct) as I want the code to break the moment a new\nversion comes out so I can immediately switch to it and adjust my code for it.\n\nI would use Bun for HTML parsing if it supported `DOMParser` out-of-the-box and\nwill do it if it ever does.\n\nIn general, my rule is to use the built-in thing first, write it myself second\nand use a dependency a distant third.\n\n### Development workflow (testing, debugging)\n\nI use VS Code.\n\nThere are several ways in which Leveret can be developed and I switch between\nthem.\n\nMost often, I'll write code and periodically check it using tests.\nTests can be run using `bun test` or `bun test *.test.ts`.\nI use `test.only`, `test.skip`, `test.todo` etc. to control what tests run atop\nthe test file name specified in the CLI argument.\n\nSometimes, I'll want to write code and observe the graphical impact of the edits\nI made.\nIn this scenario, I'll open `index.ts` and `index.png` side-by-side in VS Code\nand run `bun --watch .` to make sure all code changes get picked up and executed\nresulting in `image.png` refreshing in its editor pane in VS Code.\nThis gives quick visual feedback that can sometimes be faster than normal tests.\n\nWhen I want to use the debugger, I make use of the [Bun VS Code extension](https://bun.sh/guides/runtime/vscode-debugger)\nthat adds Run File and Debug File buttons to the top-right of the code editor\narea.\n\nThere is no need to set up a VS Code debugger configuration to make this work.\n\nI also use snapshot testing which is unfortunately more home-made that I'd like\ndue to Bun's lack of control over snapshot testing behavior.\n\nBun support snapshot testing out-of-the-box, but AFAIK cannot be configured to\ntell the test runner what file names to use for the snapshots and where to put\nthem, so they cannot be easily previewed in VS Code for a quick check.\n\nFor this reason, I implement a custom helper to check the snapshots and store\nthem under my desired path related to the test file.\n\nBut also doesn't yet allow a test to know its name programatically, so for the\nsnapshot testing, I need to manually repeat the test name for the snapshot name.\n\n## Logs\n\n### 2024-09-15 Sunday\n\n#### Cleared up the `@napi-rs/canvas` API confusion I asked about in a ticket\n\nhttps://github.com/Brooooooklyn/canvas/issues/894\n\n- `toBuffer` - synchronous\n- `encode` - asynchronous\n- `data` - same as `CanvasRenderingContext2D.getImageData` (both RGBA)\n\n#### Submitted the hackathon submission to the Browser Jam #1 repository\n\nhttps://github.com/BrowserJam/jam001/pull/14\n\nMy submission was quickly merged.\n\n[I also shared it on Twitter](https://x.com/tomashubelbauer/status/1835343613196108097).\n\n#### Finalized the hackathon submission (layout and render stages completed)\n\nI've progressed on the layout engine most of the day and then topped the work\noff with a method for rendering the laid out page.\n\nI've added tests for both of these stages as well as tests for the HTML parser.\n\n### 2024-09-14 Saturday\n\n#### Implemented HTML parsing of the Browser Jam #1 first WWW page assignment\n\nI've crafted a very basic HTML parser capable of understanding the assignment\nweb page, but not much more.\n\n#### Started the repository and put together the basic `@napi-rs/canvas` PoC\n\nThis project was inspired by the first ever BrowserJam announced by Andreas\nKling, the founder of the Ladybird browser:\n\n- [Twitter announcement post](https://x.com/awesomekling/status/1834625388510585276)\n- [BrowserJam repository](https://github.com/BrowserJam/browserjam)\n\nThe project repository is at [`tomashubelbauer/leveret` on GitHub](https://github.com/TomasHubelbauer/leveret).\n\nI installed the Canvas package using `bun add @napi-rs/canvas`.\nIt installs the main package `@napi-rs/canvas` and a platform-specific package,\nin my case `@napi-rs/canvas-darwin-arm64`.\n\nThe package has no other dependencies and ships as Node native module (`.node`),\nnot as plain TypeScript/JavaScript.\n\nSee [`Brooooooklyn/canvas` on GitHub](https://github.com/Brooooooklyn/canvas)\nfor the repository and [`@napi-rs/canvas` on NPM](https://www.npmjs.com/package/@napi-rs/canvas)\nfor the Node package.\n\nWe can see the native module working with this simple script:\n\n(Dependencies installed are `@napi-rs/canvas` and `@types/bun` (development).)\n\n`index.tx`:\n\n```typescript\nimport { createCanvas } from '@napi-rs/canvas';\nimport { write } from 'bun';\n\nconst canvas = createCanvas(640, 480);\nconst context = canvas.getContext('2d');\n\ncontext.fillStyle = 'white';\ncontext.fillRect(0, 0, canvas.width, canvas.height);\n\ncontext.fillStyle = 'black';\ncontext.fillText('Hello, world!!', 10, 50);\n\nconst buffer = await canvas.encode('png');\nawait write('index.png', buffer);\n```\n\nRun using `bun .` and open `index.png` to see the resulting image.\n\nWe can also run the example into a native binary using `bun build --compile .`.\nThe resulting `index` executable requires no dependencies (no `node_modules`)\nand can be shipped and ran standalone.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftomashubelbauer%2Fleveret","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftomashubelbauer%2Fleveret","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftomashubelbauer%2Fleveret/lists"}