Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.
The main server is currently down. We are running on a backup server, so editing and search functionality are temporarily disabled. Please check back in a few hours.

PocketCompiler 3DS: Difference between revisions

From GameBrew
Created page with "{{Infobox 3DS Homebrews |title=PocketCompiler |image=PocketCompiler3DS.png |description=An almost fully featured HTML/JS/CSS compiler application made for 3DS/n3DS consoles. |author=PlanetDogeCodes |lastupdated=2026/06/17 |type=Other Apps |version=0.36 |license=Apache-2.0 |download=https://dlhb.gamebrew.org/3dshomebrews/PocketCompiler3DS.7z |website=https://github.com/PlanetDogeCodes/Pocket-Compiler |source=https://github.com/PlanetDogeCodes/Pocket-Compiler }} PocketComp..."
 
0.48
 
(2 intermediate revisions by the same user not shown)
Line 4: Line 4:
|description=An almost fully featured HTML/JS/CSS compiler application made for 3DS/n3DS consoles.
|description=An almost fully featured HTML/JS/CSS compiler application made for 3DS/n3DS consoles.
|author=PlanetDogeCodes
|author=PlanetDogeCodes
|lastupdated=2026/06/17
|lastupdated=2026/07/14
|type=Other Apps
|type=Other Apps
|version=0.36
|version=0.48
|license=Apache-2.0
|license=Apache-2.0
|download=https://dlhb.gamebrew.org/3dshomebrews/PocketCompiler3DS.7z
|download=https://dlhb.gamebrew.org/3dshomebrews/PocketCompiler3DS.7z
Line 12: Line 12:
|source=https://github.com/PlanetDogeCodes/Pocket-Compiler
|source=https://github.com/PlanetDogeCodes/Pocket-Compiler
}}
}}
PocketCompiler is an experimental Nintendo 3DS HTML/JS/CSS compiler and code editor
PocketCompiler is an experimental Nintendo 3DS HTML/JS/CSS compiler and code editor.


It's designed to let you write HTML/JS/CSS on the 3DS, allowing for easy development. You can:
PocketCompiler is designed to let you:
* Edit code on the bottom screen.
* Write HTML/JS/CSS on the 3DS, allowing for easy development
* Preview output/status on the top screen.
* Compile and run web pages directly on the top screen with a real DOM-based rendering engine
* Save/load projects from the SD card.
* Save/load/rename/delete projects from the SD card
* Debug with a live JS console


The default project path is <code>sdmc:/3ds/PocketCompiler/projects</code>/
The default project path is <code>sdmc:/3ds/PocketCompiler/projects</code>/


== Features ==
== Features ==
* Basic HTML parsing.
* HTML parsing with DOM tree construction
* Simple CSS.
* CSS engine with cascade, inheritance, specificity, custom properties (<code>var()</code>), <code>calc()</code>, <code>@media</code> skip, <code>:not()</code>, <code>:hover</code>/<code>:focus</code>/<code>:active</code>/<code>:checked</code>/<code>:disabled</code>, <code>:first-child</code>/<code>:last-child</code>/<code>:nth-child()</code>, comma selectors, <code>!important</code>, box-sizing, flex/grid layout, transitions, box-shadow, linear-gradient, word-wrap, and 40+ CSS properties
* Basic JavaScript runtime capabilities.
* JavaScript runtime via Duktape (ES5 + subset of ES6)
* DOM-like elements.
* DOM manipulation: <code>getElementById</code>, <code>querySelector</code>/<code>querySelectorAll</code>, <code>createElement</code>, <code>appendChild</code>, <code>innerHTML</code>, <code>textContent</code>, <code>classList</code>, live <code>style</code> object (Proxy-backed), <code>addEventListener</code>, <code>onclick</code>/<code>onload</code>/etc. property handlers
* Keyboard/Mouse/Touch events.
* Keyboard, mouse, and touch events with bubbling
* Nearly complete iframe support.
* <code>document.write()</code> with incremental parsing
* document.write()
* Software keyboard integration for <code>&lt;input&gt;</code>/<code>&lt;textarea&gt;</code> text entry and <code>window.prompt()</code>
* Limited API and link fetching.
* Form submission via click on submit buttons
* IndexedDB-adjacent SD-card storage.
* <code>fetch()</code> / XMLHttpRequest (SD-card backed)
* Custom Canvas rendering.
* localStorage / sessionStorage (SD-card persisted)
* Custom WebGL rendering.
* IndexedDB (SD-card persisted, simplified sync API)
* Image loading.
* Canvas 2D rendering
* Web audio support.
* WebGL scaffolding (Citro3D backend)
* Image loading (PNG via LodePNG)
* Web Audio (oscillators, gain, WAV playback via NDSP)
* Near-complete iframe support
* Auto-save draft (writes to SD on compile + 10s of idle)
* Crash-recovery: &quot;Recover autosave&quot; action restores unsaved work
* Remembers last-opened file across launches
* File explorer: save, load, rename, delete, sort by name/size/date


== Canvas / WebGL ==
== Canvas / WebGL ==
PocketCompiler has rendering systems for:
PocketCompiler has rendering systems for:
* canvas command buffers
* Canvas 2D: rectangles, paths, text, images, gradients, transforms, <code>fillText</code>/<code>strokeText</code>/<code>measureText</code>
* rectangles, lines, text, placeholders
* WebGL: <code>getContext(&quot;webgl&quot;)</code> detection, buffers, shaders/programs, uniforms/attributes, textures, draw calls, viewport/depth/blend/scissor state, Citro3D backend scaffolding
* <code>getContext("webgl")</code> detection
* buffers
* shaders/programs
* uniforms/attributes
* textures
* draw calls
* viewport/depth/blend/scissor state
* Citro3D backend scaffolding
 
This is not yet full WebGL.
This is not yet full WebGL.


== Storage ==
== Storage ==
PocketCompiler includes SD-card-backed storage systems:
PocketCompiler includes SD-card-backed storage systems:
<pre>sdmc:/3ds/PocketCompiler/idb/
sdmc:/3ds/PocketCompiler/cache_api/
sdmc:/3ds/PocketCompiler/resource_cache/</pre>


Supported or scaffolded:
<pre>sdmc:/3ds/PocketCompiler/
* IndexedDB-style databases/object stores
    config.txt        <- last-opened file (auto-managed)
* <code>put</code>, <code>get</code>, <code>delete</code>, <code>clear</code>, <code>count</code>
    autosave.html    <- crash-recovery draft (auto-managed)
* localStorage-style storage
    console_log.txt  <- exported JS console log (written on demand)
* sessionStorage-style storage
    projects/        <- all .html project files
* Cache API-style storage
    storage/
* binary-safe values
        local_*.json  <- localStorage data
* 1 MB per-value/resource cap
        idb/          <- IndexedDB data</pre>
 
Supported:
* IndexedDB-style databases with <code>put</code>, <code>get</code>, <code>delete</code>, <code>clear</code>, <code>close</code>
* localStorage with SD-card persistence
* sessionStorage (in-memory, per session)
* Cookie-style storage


== Audio ==
== Audio ==
PocketCompiler includes a small Web Audio-style system:
PocketCompiler includes a Web Audio-style system:
* AudioContext-like runtime
* AudioContext-like runtime
* OscillatorNode-like node
* OscillatorNode (sine/square/triangle/sawtooth)
* GainNode-like node
* GainNode
* sine/square/triangle/sawtooth tones
* AudioBufferSourceNode with WAV file loading
* NDSP-backed output when available
* NDSP-backed output when available
* safe fallback if audio fails
* Safe fallback if audio fails


== Limitations ==
== Limitations ==
Line 79: Line 81:


It does not yet fully support:
It does not yet fully support:
* Complete HTML parsing
* Full modern CSS (no CSS animations/@keyframes, no rotate()/scale() transforms, limited position: fixed)
* Full CSS layout/cascade
* Full modern JavaScript (no ES modules, no async/await, no Proxy traps beyond get/set)
* Full modern JavaScript/browser behavior
* Real networking (all fetch/XHR URLs resolve to SD-card files)
* Complete Promise/event-loop behavior
* Full WebGL 1.0 (no real GLSL shader translation)
* Full WebGL 1.0
* Full Canvas 2D API (no drawImage from video, no complex path fills)
* Real GLSL shader translation
* Full Web Audio API (no real-time effects, no offline rendering)
* Full Canvas 2D API
* Full Web Audio API
* Full IndexedDB compatibility
* Complete iframe isolation
* Complete iframe isolation
* Pointer lock
* Pointer lock


The 3DS also has very limited RAM, CPU, GPU power, and screen space, so PocketCompiler uses strict safety limits (1 MB per resource/value/cache entry, bounded event queues, bounded command buffers, bounded editor/runtime structures)
The 3DS also has very limited RAM, CPU, GPU power, and screen space, so PocketCompiler uses strict safety limits (bounded DOM pool, bounded event queues, bounded command buffers, 4 MB max audio buffer, recursion depth guards).
 
==Controls==
'''Edit Mode — Editor Tab:'''
 
D-Pad Up/Down - Scroll code vertically
 
D-Pad Left/Right - Scroll code horizontally
 
Circle Pad - Move cursor dot
 
Touch - Snap cursor to tap; tap tabs / code lines / file rows
 
A - Edit current line (3DS keyboard; auto-indent if empty)
 
B - Undo (5-step ring)
 
X - Save project (file explorer)
 
Y - Load project (file explorer)
 
L - Toggle JS Console tab
 
R - Open Actions menu (Find, Go-to, Duplicate, Comment, Rename, Delete, Sort, Export log, Recover autosave)
 
Start - Compile + run
 
Select - Show controls menu (scrollable)
 
'''Edit Mode — Console Tab:'''
 
D-Pad Up/Down - Scroll console log
 
A - Type & eval JS expression
 
B - Clear console log
 
L - Back to editor tab


== Controls ==
'''Run Mode:'''
D-Pad - Arrow key movement


Circle Pad - Move mouse cursor (it's just a dot)
D-Pad - Scroll web page (all directions)


A - Click, Edit code line
Circle Pad - Move cursor (never scrolls)


B - Undo
A / L - Left-click


X - Save project
B / R - Right-click


Y - Load project
X - Release cursor lock


Start - Run/Compile
Y - Space key (sent to web content)


Select - Show controls menu
Start - Stop, Return to editor


== Screenshots ==
== Screenshots ==
Line 115: Line 150:


== Changelog ==
== Changelog ==
'''v0.48'''
* The 3DS crash issue in the previous version was caused by the '''Duktape fatal error handler''' entering an infinite spin loop (<code>for(;;) svcSleepThread(...)</code>) after JS context creation completed. When any post-creation Duktape operation triggered a fatal error (OOM, etc.), the handler froze the entire 3DS, requiring a reboot.
* Fixes Applied
** '''Fatal handler no longer spins infinitely (web_js.c)'''
*** Changed the post-creation fatal handler from <code>for(;;) svcSleepThread(1000000000LL)</code> to simply <code>return</code>. While returning from a Duktape fatal handler is technically undefined, in practice Duktape propagates the error as a JS exception or returns a safe value. The app continues running instead of freezing the 3DS.
** '''<code>&lt;style&gt;</code> block buffer increased (web_html_parser.c)'''
*** <code>stbuf</code> increased from 8192 to 16384 bytes. A 31KB HTML file can easily have &gt;8KB of CSS. Truncation at 8KB produced malformed CSS that could confuse the parser.
** '''<code>&lt;script&gt;</code> block buffer increased (web_html_parser.c)'''
*** <code>scbuf</code> increased from 16384 to 32768 bytes. Large inline scripts were being truncated at 16KB, causing syntax errors (caught by <code>duk_peval_string</code> but wasteful).
** '''DOM pool sizes increased (web_dom.h)'''
*** <code>WE_MAX_NODES</code>: 1024 → 2048 (a complex 31KB page can have &gt;1024 elements + text nodes)
*** <code>WE_MAX_ATTRS</code>: 512 → 1024 (matching the larger node pool)
** '''C2D_TextBuf NULL checks (web_render.c)'''
*** Added OOM guards after <code>C2D_TextBufNew(4096)</code> in both the wrapping and non-wrapping text render paths. If allocation fails, the function skips drawing that text instead of crashing.
** '''All unprotected <code>duk_eval_string</code> calls replaced with protected versions:'''
: {| class="wikitable"
|-
! File
! Call
! Fix
|-
| <code>web_js.c:1346</code>
| <code>duk_eval_string_noresult</code> (polyfills)
| → <code>duk_peval_string_noresult</code>
|-
| <code>web_iframe.c:184</code>
| <code>duk_eval_string_noresult</code> (postMessage stub)
| → <code>duk_peval_string_noresult</code>
|-
| <code>web_canvas.c:369</code>
| <code>duk_eval_string</code> (canvas property setup)
| → <code>duk_peval_string</code> + fallback to plain object on failure
|}
'''v0.47'''
* '''Multi-line text rendering'''
** <code>wrender_text</code> previously drew the entire text string as a single <code>C2D_DrawText</code> call, so paragraphs that the layout engine had correctly wrapped to multiple lines rendered as one long line overflowing off-screen. The function now detects when text exceeds the available width, splits at word boundaries using the same <code>char_width</code> metric as the layout engine, and draws each line separately with correct alignment, bold, italic, and decoration.
* '''<code>position: fixed</code> / <code>sticky</code>'''
** <code>position: fixed</code> elements no longer scroll with the page — the renderer skips applying the scroll offset for <code>position==3</code> nodes, keeping them pinned to the viewport. <code>position: sticky</code> is now parsed (value 4) and treated like <code>relative</code> for layout. True scroll-tracking stickiness is deferred.
* '''Dirty-only CSS cascade'''
** <code>css_cascade</code> previously ran <code>css_compute_node</code> on every element every frame when any node was dirty. On a 100-element page with 50 CSS rules, that's ~5000 <code>selector_matches</code> calls per frame. Now only processes nodes where <code>dirty==1</code>. On page load all nodes are dirty (unchanged behavior); during per-frame hover/focus updates, typically 1-2 nodes are dirty, reducing cascade cost by ~99%.
* '''<code>window.getComputedStyle()</code>'''
** Returns a read-only object with 20+ computed style properties as CSS-formatted strings: <code>width</code>, <code>height</code>, <code>display</code> (as string), <code>color</code>/<code>backgroundColor</code> (as <code>rgba(...)</code>), <code>fontSize</code>, <code>position</code>, <code>opacity</code> (as 0-1 float), <code>visibility</code>, margins, padding, border, <code>left</code>/<code>top</code>, and <code>offsetLeft/Top/Width/Height</code>. Fixed: <code>display</code> was being set twice (boolean then string); <code>opacity</code> was returning 0-255 instead of 0.0-1.0.
* '''<code>Element.matches()</code> rewritten'''
** Previously only handled <code>#id</code>, <code>.class</code>, and bare tag names. Now delegates to <code>css_selector_matches</code> — the full selector matching engine — so it handles descendant selectors, attribute selectors, pseudo-classes (<code>:hover</code>, <code>:checked</code>), <code>:not()</code>, adjacent siblings (<code>+</code>), and pseudo-elements.
* '''<code>Element.closest()</code>'''
** New method. Walks up the ancestor chain (including self) until one matches the given selector, returning the element or <code>null</code>. Cycle-guarded with <code>g_dom_node_n</code> bound. Uses the full <code>css_selector_matches</code> engine for each ancestor check.
* '''<code>Array.from()</code> polyfill + <code>NodeList</code> alias'''
** Polyfills <code>Array.from</code> for array-like objects and iterables with optional <code>mapFn</code>/<code>thisArg</code>. Aliases <code>NodeList</code> to <code>Array</code> so <code>instanceof NodeList</code> checks pass and <code>NodeList.prototype.forEach</code> exists (Duktape arrays already have <code>forEach</code>). <code>JSON.parse</code>/<code>JSON.stringify</code> verified working via Duktape's built-in <code>DUK_USE_JSON_BUILTIN</code>.
* '''Files changed'''
{| class="wikitable"
|-
! File
! Changes
|-
| <code>web_render.c</code>
| Multi-line text wrapping in <code>wrender_text</code>; <code>position:fixed</code> scroll-offset skip
|-
| <code>web_layout.c</code>
| <code>position:fixed</code> uses viewport (0,0); <code>position:sticky</code> (value 4) treated like relative
|-
| <code>web_css.c</code>
| Dirty-only cascade in <code>css_cascade</code>; <code>css_selector_matches</code> public wrapper; <code>position:sticky</code> parser
|-
| <code>web_css.h</code>
| <code>css_selector_matches</code> declaration; <code>CSS_SEL_LEN</code> 64→128
|-
| <code>web_dom.h</code>
| <code>position</code> comment updated for sticky (value 4)
|-
| <code>web_js.c</code>
| <code>getComputedStyle</code>, <code>closest</code>, rewritten <code>matches</code>, <code>Array.from</code> polyfill, <code>NodeList</code> alias, opacity format fix
|}
'''v0.45'''
* '''All New Stuff'''
** CSS selectors were completely failing on complex selectors
*** The <code>selector_matches</code> function only handled '''simple selectors''' (tag, <code>.class</code>, <code>#id</code>, <code>tag.class</code>). It could not match any selector containing:
**** '''Descendant combinators''' (spaces): <code>.tog input</code>, <code>.field input[type=text]</code>, <code>details:not([open]) summary</code>
**** '''Adjacent sibling combinators''' (<code>+</code>): <code>.tog input:checked + .tog-sw</code>
**** '''Attribute selectors''' (<code>[type=checkbox]</code>): <code>input[type=checkbox]</code>
**** '''Pseudo-elements''' (<code>::after</code>, <code>::before</code>): <code>summary::after</code>, <code>.tog-sw::after</code>
*** Since nearly every selector in the EaglerLite CSS uses at least one of these, '''zero CSS rules were matching''' — the page rendered with default styles only.
** DPad scroll was broken
*** The v0.43 DPad scroll fix (which used <code>we_page_scroll_by</code> when the web engine was loaded) was accidentally reverted during the v0.44 bugfix pass, going back to the old text-mode-only scroll. DPad Up/Down did nothing when viewing a web page.
* '''What I fixed'''
** New <code>simple_selector_matches</code> function
*** Handles a single simple selector component, now with:
**** '''Attribute selectors''': <code>[attr]</code>, <code>[attr=val]</code>, <code>[attr=&quot;val&quot;]</code> — parses the bracket, splits on <code>=</code>, strips quotes, checks the node's attribute
**** '''Pseudo-elements''': <code>::after</code>, <code>::before</code>, <code>::marker</code>, <code>::placeholder</code>, <code>::selection</code> — always accepted (we don't render them, but the selector matches the base element so its other properties still apply)
**** '''<code>*</code> with attribute selectors''': <code>*[attr=val]</code> works
**** Uses <code>css_classlist_has</code> (local, no strtok) instead of <code>classlist_has</code> (which lives in web_js.c)
** Rewritten <code>selector_matches</code> function
*** Now handles '''compound selectors''' with combinators:
**** '''Descendant''' (space): <code>.tog input</code> — walks up ancestors checking each part right-to-left
**** '''Adjacent sibling''' (<code>+</code>): <code>input + .tog-sw</code> — checks the immediately preceding sibling
**** Splits the selector at spaces and <code>+</code> (respecting <code>[bracket]</code> depth so <code>[type=checkbox]</code> doesn't get split)
**** Each part is matched via <code>simple_selector_matches</code>
**** Guard counter on all ancestor walks prevents infinite loops
** DPad scroll restored
*** DPad Up/Down/Left/Right now scrolls the web page via <code>we_page_scroll_by(0, ±30)</code> when <code>g_web_engine.loaded</code> is true. Falls back to text-mode scroll when the engine isn't loaded. Circle Pad is always cursor-only.
'''v0.44'''
* '''CSS Custom Properties (Variables) — <code>var()</code> + <code>:root</code>'''
** The biggest addition. <code>:root { --bg: #000; }</code> stores custom properties in a global variable table. <code>var(--bg)</code> is resolved at parse time before any property handler runs — works in colors, lengths, and any value. Supports <code>var(--name, fallback)</code> syntax and recursive resolution (variables referencing other variables). Up to 64 variables stored. Reset on page recompile.
* '''<code>@media</code> Queries — Graceful Skip'''
** <code>@media(max-width: 680px) { ... }</code> is now parsed and skipped gracefully (with proper brace-nesting tracking). Previously, <code>@media</code> would confuse the stylesheet parser and corrupt all subsequent rules. Also skips <code>@keyframes</code>, <code>@import</code>, <code>@font-face</code>, and <code>@charset</code>.
* '''<code>:root</code> Selector'''
** <code>:root { ... }</code> declarations are applied as a universal <code>*</code> rule with the lowest specificity, so <code>:root</code> properties cascade to all elements via inheritance. Custom properties (<code>--name: value</code>) are stored in the variable table.
* '''<code>:not()</code> Selector'''
** <code>:not(.hidden)</code> now works. The inner selector is recursively matched and negated. Forward-declared to handle the mutual recursion between <code>pseudo_matches</code> → <code>pseudo_not_matches</code> → <code>selector_matches</code>.
* '''Comma-Separated Selectors'''
** <code>h1, h2, h3 { ... }</code> now creates a separate rule for each sub-selector instead of creating one rule with a comma in the selector (which never matched anything).
* '''<code>!important</code> Support'''
** <code>color: red !important</code> is now accepted — the <code>!important</code> suffix is stripped before parsing. All declarations are treated as having the same specificity (later wins), so <code>!important</code> doesn't change cascade behavior but no longer causes parse failures.
* '''New CSS Properties (15 additions)'''
{| class="wikitable"
|-
! Property
! Description
|-
| <code>box-sizing</code>
| <code>border-box</code> / <code>content-box</code> (parsed, stored)
|-
| <code>flex</code> shorthand
| <code>flex: 1</code> → grow=1; <code>flex: 1 0 auto</code> → grow/shrink/basis
|-
| <code>flex-shrink</code>
| Default 1
|-
| <code>flex-basis</code>
| <code>auto</code> or length
|-
| <code>pointer-events</code>
| <code>none</code> / <code>auto</code>
|-
| <code>user-select</code>
| <code>none</code> / <code>auto</code>
|-
| <code>outline</code>
| Width + color (simplified, no layout impact)
|-
| <code>outline-width</code> / <code>outline-color</code> / <code>outline-style</code>
| Individual properties
|-
| <code>text-transform</code>
| <code>uppercase</code> / <code>lowercase</code> / <code>capitalize</code> / <code>none</code>
|-
| <code>letter-spacing</code>
| px or <code>normal</code>
|-
| <code>line-height</code>
| px, unitless multiplier, or <code>normal</code>
|-
| <code>list-style</code> / <code>list-style-type</code>
| <code>none</code> / <code>disc</code> / <code>circle</code> / <code>square</code> / <code>decimal</code>
|-
| <code>object-fit</code>
| <code>fill</code> / <code>contain</code> / <code>cover</code>
|-
| <code>min/max-width/height</code>
| Now handles <code>none</code> and <code>auto</code> correctly
|}
'''v0.43'''
* '''UI: DPad = Scroll, Circle Pad = Cursor (fixed)'''
** The biggest UX fix: DPad and Circle Pad no longer conflict. Previously, DPad both scrolled the fallback text renderer AND sent arrow key events to web content — pressing Up would scroll the page ''and'' fire a <code>keydown(&quot;ArrowUp&quot;)</code> at the same time. Circle Pad could also trigger scroll in some paths.
** '''Now:'''
*** '''DPad''' scrolls the web page (30px per press, all four directions). When the web engine isn't loaded, it scrolls the fallback text output instead.
*** '''Circle Pad''' always moves the cursor — it never scrolls, in any mode.
*** DPad was removed from the web content key map entirely, so no more phantom arrow key events.
*** Only '''Y''' (Space) generates a keyboard event for web content.
*** Controls help screen updated to reflect the new assignments.
* '''Cookie Clicker Compatibility: 3 Major JS APIs Added'''
** '''<code>element.classList</code>''' — Cookie Clicker (and most modern JS) uses <code>classList.add()</code>, <code>.remove()</code>, <code>.toggle()</code>, <code>.contains()</code> extensively. Previously only <code>className</code> existed. Now <code>classList</code> returns a live object with all four methods that manipulate the node's <code>class</code> attribute and trigger <code>css_compute_node</code> so styles re-cascade.
** '''Live <code>element.style</code> object''' — Cookie Clicker does <code>element.style.display = 'none'</code>, <code>element.style.left = '100px'</code>, etc. Previously <code>style</code> was a dead object — setting properties did nothing. Now the style object is wrapped in a '''Duktape Proxy''' that intercepts every property get/set:
*** '''Get''' (<code>element.style.display</code>): parses the inline <code>style</code> attribute and returns the property value.
*** '''Set''' (<code>element.style.display = 'none'</code>): updates the inline style string via a C-backed <code>cssText</code> setter, triggers <code>css_compute_node</code> + <code>dom_mark_dirty</code> so the layout and renderer pick up the change immediately.
** '''<code>onclick</code>/<code>onload</code>/<code>onerror</code> property handlers''' — Cookie Clicker assigns <code>element.onclick = function(){...}</code> instead of using <code>addEventListener</code> or inline <code>onclick=&quot;...&quot;</code> attributes. Previously only inline attribute handlers and <code>addEventListener</code> worked. Now <code>we_dispatch</code> checks for JS function properties on the node object (<code>onclick</code>, <code>onmousedown</code>, <code>onload</code>, <code>onsubmit</code>, etc.) and calls them with a proper event object. Supports all 13 event types.
'''v0.42'''
* '''CSS Transitions'''
** <code>transition: 0.3s ease-in-out</code> now actually animates. When a style property changes on an element with a transition duration, the old and new values are interpolated over the duration using smoothstep easing. Supports 25 numeric properties (opacity, width, height, margins, padding, position, font-size, border, transforms) plus color properties (color, background) which interpolate per-channel. The transition engine runs in the frame loop and marks transitioning nodes dirty so layout re-runs each frame to reflect the changing values.
* '''Box-Shadow'''
** <code>box-shadow: 2px 2px 4px rgba(0,0,0,0.5)</code> is parsed into offset/blur/color and drawn as a semi-transparent rect behind the element before the background fill. No real gaussian blur (3DS hardware can't do that efficiently), but the spread approximation looks reasonable for most UI shadows.
* '''Linear-Gradient Backgrounds'''
** <code>background: linear-gradient(to right, red, blue)</code> is parsed into start/end colors and a direction, then rendered as 16 interpolated color bands. Supports <code>to right/bottom/left/top</code> directions and <code>Ndeg</code> angles. Falls back to solid <code>background</code> color when no gradient is active. Transparent colors work correctly (uses explicit &quot;set&quot; flags instead of checking for zero).
* '''CSS Grid Layout'''
** <code>display: grid</code> with <code>grid-template-columns</code> now lays out children in a grid. Supports fixed pixel widths, <code>fr</code>/<code>auto</code> (equal distribution), and <code>repeat(N, ...)</code> syntax up to 8 columns. Rows auto-size to the tallest cell, gap is applied between cells, and children are vertically centered within their row.
* '''CSS <code>calc()</code>'''
** <code>calc(100% - 20px)</code> is now evaluated at layout time. Supports <code>+</code>, <code>-</code>, <code>*</code>, <code>/</code> with correct operator precedence, and all length units (px, em, rem, vw, vh, %) work inside calc terms. Self-contained in the length parser — no new systems.
* '''Form Submission'''
** Clicking a <code>&lt;button type=&quot;submit&quot;&gt;</code> or <code>&lt;input type=&quot;submit&quot;&gt;</code> inside a <code>&lt;form&gt;</code> now fires the form's <code>onsubmit</code> handler and dispatches a <code>submit</code> event. This replaces the old START-key approach which conflicted with START=exit-run-mode.
* '''Keybind Conflict Fixes'''
** Resolved button assignment conflicts between the app shell and web content event system. In run mode, only DPad (arrow keys) and Y (Space) generate keyboard events for web content. A/B/X/L/R/START are exclusively mouse clicks, cursor lock, and exit-run-mode — no more phantom keydown events alongside mouse clicks.
* '''Bug Fixes'''
** IndexedDB path overflow: <code>store</code>/<code>key</code> strings from JS are now length-validated to prevent path truncation
** IndexedDB <code>get()</code> return logic: was returning true for empty files
** IndexedDB <code>clear()</code> prefix buffer: enlarged from 80 to 128 bytes to handle longer store names
** Transition color precision: RGBA8 colors now stored as uint32 (not float) to avoid precision loss past 2^24
** Gradient transparent-color: <code>0x00000000</code> (transparent) no longer treated as &quot;unset&quot;
'''v0.41'''
* '''<code>&lt;input&gt;</code>/<code>&lt;textarea&gt;</code> text entry'''
** New <code>we_input_prompt()</code> in <code>web_events.c</code> opens the 3DS software keyboard when a click lands on an <code>&lt;input&gt;</code>/<code>&lt;textarea&gt;</code> (or an ancestor of one). Reads the current <code>value</code> attribute, shows it as initial text, writes the result back, fires <code>input</code> + <code>change</code> events, and marks the node dirty so the renderer shows the new text.
** New <code>we_text_input()</code> helper wraps <code>swkbdInit</code>/<code>swkbdInputText</code> for reuse.
* '''CSS <code>transform: translate()</code>'''
** <code>wrender_node</code> now applies <code>translate_x</code>/<code>translate_y</code> to the draw position, and folds the offset into the children's <code>ox</code>/<code>oy</code> so children move with the translated parent. ~6 lines of real logic; the parser already stored the values since v0.39.
* '''Word-wrap in the layout engine'''
** Replaced the naive <code>tw/avail_w + 1</code> character-count wrapping with proper word-boundary wrapping. Walks the text word by word, tracks line width, breaks when the next word would overflow. Hard-breaks words longer than a full line. Respects <code>white-space: pre</code> and <code>nowrap</code>.
* '''CSS <code>:hover</code> / <code>:focus</code> / <code>:nth-child</code> pseudo-classes'''
** New <code>pseudo_matches()</code> function checks <code>:hover</code>, <code>:focus</code>, <code>:active</code>, <code>:checked</code>, <code>:disabled</code>, <code>:first-child</code>, <code>:last-child</code>, <code>:nth-child(n/odd/even)</code>.
** <code>selector_matches()</code> rewritten to split selectors at <code>:</code> and check the pseudo-class after the base selector matches. Supports chained pseudos (<code>a:focus:hover</code>).
** Dynamic re-cascade: <code>we_events_update</code> marks nodes dirty on hover/focus changes; <code>we_update</code> now re-runs <code>css_cascade</code> (not just <code>layout_document</code>) when nodes are dirty, so <code>:hover</code>/<code>:focus</code> styles apply dynamically.
* '''Keyboard input to web content'''
** <code>window.prompt(message, default)</code> now opens the 3DS software keyboard and returns the typed string (was a no-op stub returning <code>undefined</code>).
** Clicking an <code>&lt;input&gt;</code>/<code>&lt;textarea&gt;</code> opens the keyboard (Feature 1 above).
* '''localStorage persistence + IndexedDB completion'''
** '''IndexedDB C API hardened''': <code>ws_idb_open</code> now checks for existing databases with the same name (no duplicates); <code>ws_idb_get</code> validates <code>fread</code> return (was a potential underflow); <code>ws_idb_clear</code> is no longer a no-op — it scans the directory with <code>opendir</code>/<code>readdir</code> and removes all files matching the store prefix; all functions now have proper NULL/bounds checks.
** '''IndexedDB JS bindings''': <code>ws_install_idb</code> now installs a real <code>indexedDB</code> object to JS with <code>open()</code>, <code>put()</code>, <code>get()</code>, <code>delete()</code>, <code>clear()</code>, <code>close()</code> methods. <code>put()</code> JSON-encodes values; <code>get()</code> safely JSON-parses them back (falls back to string if not valid JSON). The &quot;IndexedDB&quot; feature pill is now actually true.
'''v0.38'''
* '''Crash / correctness bugs:'''
** Canvas 2D silently broken — wc_install_bindings() built its property-setup script with snprintf into a 512-byte buffer, but the actual script is 854 bytes. It was truncating mid-statement, so ctx.fillStyle = 'red' and every other canvas property setter did nothing, with zero error shown. Now passed as a literal directly to duk_eval_string — no buffer, no truncation possible.
** Event listeners could misfire — DOMEvent.js_ref was a uint8_t, silently wrapping after the 255th addEventListener() call in a session and firing the wrong callback. Widened to uint16_t.
** Listener ref counter never reset — it was a function-local static that persisted across every recompile instead of resetting with the fresh Duktape context each page load gets. Moved to file scope with an explicit reset tied to context creation.
** Unchecked array access in we_document_open() — switched to the existing bounds-checked accessor so a corrupted sibling chain can't read out-of-bounds memory.
* '''Stack-safety hardening (3DS has only 256KB of stack):'''
** ~3KB of stacked buffers in the CSS parser — css_parse_inline's 2048-byte buffer is called while css_parse_stylesheet's 1024-byte buffer is still live. Both made static (verified safe: parsing is never re-entrant).
** Unbounded layout recursion — nothing stopped a deeply-nested DOM tree (up to the 1024-node cap) from recursing that many stack frames deep. Added a depth guard capped well above any realistic page.
* Version v0.37 was skipped due to issues with a beta build.
* CIA files will be added in version 1.0
'''v0.36'''
'''v0.36'''
* '''Crash / correctness fixes:'''
* '''Crash / correctness fixes:'''
Line 193: Line 461:


== Credits ==
== Credits ==
PocketCompiler project and idea by PlanetDogeCodes (me). Some browser runtime features, custom Canvas/WebGL Integration, and 3DS Optimizations were all made with help from GPT-5.5 xhigh. Some UI elements and DOM-style event handling were made with help from Claude Sonnet 4.6
PocketCompiler project and idea by PlanetDogeCodes. Some browser runtime features, custom Canvas/WebGL Integration, and 3DS Optimizations were all made with help from GPT-5.5 (xhigh) and GLM 5.2 (max). Some UI elements and DOM-style event handling were made with help from Claude Sonnet 5 (high)


Built with:
Built with:

Latest revision as of 02:08, 17 July 2026

PocketCompiler
General
AuthorPlanetDogeCodes
TypeOther Apps
Version0.48
LicenseApache-2.0
Last Updated2026/07/14
Links
Download
Website
Source

PocketCompiler is an experimental Nintendo 3DS HTML/JS/CSS compiler and code editor.

PocketCompiler is designed to let you:

  • Write HTML/JS/CSS on the 3DS, allowing for easy development
  • Compile and run web pages directly on the top screen with a real DOM-based rendering engine
  • Save/load/rename/delete projects from the SD card
  • Debug with a live JS console

The default project path is sdmc:/3ds/PocketCompiler/projects/

Features

  • HTML parsing with DOM tree construction
  • CSS engine with cascade, inheritance, specificity, custom properties (var()), calc()@media skip, :not():hover/:focus/:active/:checked/:disabled:first-child/:last-child/:nth-child(), comma selectors, !important, box-sizing, flex/grid layout, transitions, box-shadow, linear-gradient, word-wrap, and 40+ CSS properties
  • JavaScript runtime via Duktape (ES5 + subset of ES6)
  • DOM manipulation: getElementByIdquerySelector/querySelectorAllcreateElementappendChildinnerHTMLtextContentclassList, live style object (Proxy-backed), addEventListeneronclick/onload/etc. property handlers
  • Keyboard, mouse, and touch events with bubbling
  • document.write() with incremental parsing
  • Software keyboard integration for <input>/<textarea> text entry and window.prompt()
  • Form submission via click on submit buttons
  • fetch() / XMLHttpRequest (SD-card backed)
  • localStorage / sessionStorage (SD-card persisted)
  • IndexedDB (SD-card persisted, simplified sync API)
  • Canvas 2D rendering
  • WebGL scaffolding (Citro3D backend)
  • Image loading (PNG via LodePNG)
  • Web Audio (oscillators, gain, WAV playback via NDSP)
  • Near-complete iframe support
  • Auto-save draft (writes to SD on compile + 10s of idle)
  • Crash-recovery: "Recover autosave" action restores unsaved work
  • Remembers last-opened file across launches
  • File explorer: save, load, rename, delete, sort by name/size/date

Canvas / WebGL

PocketCompiler has rendering systems for:

  • Canvas 2D: rectangles, paths, text, images, gradients, transforms, fillText/strokeText/measureText
  • WebGL: getContext("webgl") detection, buffers, shaders/programs, uniforms/attributes, textures, draw calls, viewport/depth/blend/scissor state, Citro3D backend scaffolding

This is not yet full WebGL.

Storage

PocketCompiler includes SD-card-backed storage systems:

sdmc:/3ds/PocketCompiler/
    config.txt        <- last-opened file (auto-managed)
    autosave.html     <- crash-recovery draft (auto-managed)
    console_log.txt   <- exported JS console log (written on demand)
    projects/         <- all .html project files
    storage/
        local_*.json  <- localStorage data
        idb/          <- IndexedDB data

Supported:

  • IndexedDB-style databases with putgetdeleteclearclose
  • localStorage with SD-card persistence
  • sessionStorage (in-memory, per session)
  • Cookie-style storage

Audio

PocketCompiler includes a Web Audio-style system:

  • AudioContext-like runtime
  • OscillatorNode (sine/square/triangle/sawtooth)
  • GainNode
  • AudioBufferSourceNode with WAV file loading
  • NDSP-backed output when available
  • Safe fallback if audio fails

Limitations

PocketCompiler is still in early development.

It does not yet fully support:

  • Full modern CSS (no CSS animations/@keyframes, no rotate()/scale() transforms, limited position: fixed)
  • Full modern JavaScript (no ES modules, no async/await, no Proxy traps beyond get/set)
  • Real networking (all fetch/XHR URLs resolve to SD-card files)
  • Full WebGL 1.0 (no real GLSL shader translation)
  • Full Canvas 2D API (no drawImage from video, no complex path fills)
  • Full Web Audio API (no real-time effects, no offline rendering)
  • Complete iframe isolation
  • Pointer lock

The 3DS also has very limited RAM, CPU, GPU power, and screen space, so PocketCompiler uses strict safety limits (bounded DOM pool, bounded event queues, bounded command buffers, 4 MB max audio buffer, recursion depth guards).

Controls

Edit Mode — Editor Tab:

D-Pad Up/Down - Scroll code vertically

D-Pad Left/Right - Scroll code horizontally

Circle Pad - Move cursor dot

Touch - Snap cursor to tap; tap tabs / code lines / file rows

A - Edit current line (3DS keyboard; auto-indent if empty)

B - Undo (5-step ring)

X - Save project (file explorer)

Y - Load project (file explorer)

L - Toggle JS Console tab

R - Open Actions menu (Find, Go-to, Duplicate, Comment, Rename, Delete, Sort, Export log, Recover autosave)

Start - Compile + run

Select - Show controls menu (scrollable)

Edit Mode — Console Tab:

D-Pad Up/Down - Scroll console log

A - Type & eval JS expression

B - Clear console log

L - Back to editor tab

Run Mode:

D-Pad - Scroll web page (all directions)

Circle Pad - Move cursor (never scrolls)

A / L - Left-click

B / R - Right-click

X - Release cursor lock

Y - Space key (sent to web content)

Start - Stop, Return to editor

Screenshots

PocketCompiler3DS2.png PocketCompiler3DS3.png

Changelog

v0.48

  • The 3DS crash issue in the previous version was caused by the Duktape fatal error handler entering an infinite spin loop (for(;;) svcSleepThread(...)) after JS context creation completed. When any post-creation Duktape operation triggered a fatal error (OOM, etc.), the handler froze the entire 3DS, requiring a reboot.
  • Fixes Applied
    • Fatal handler no longer spins infinitely (web_js.c)
      • Changed the post-creation fatal handler from for(;;) svcSleepThread(1000000000LL) to simply return. While returning from a Duktape fatal handler is technically undefined, in practice Duktape propagates the error as a JS exception or returns a safe value. The app continues running instead of freezing the 3DS.
    • <style> block buffer increased (web_html_parser.c)
      • stbuf increased from 8192 to 16384 bytes. A 31KB HTML file can easily have >8KB of CSS. Truncation at 8KB produced malformed CSS that could confuse the parser.
    • <script> block buffer increased (web_html_parser.c)
      • scbuf increased from 16384 to 32768 bytes. Large inline scripts were being truncated at 16KB, causing syntax errors (caught by duk_peval_string but wasteful).
    • DOM pool sizes increased (web_dom.h)
      • WE_MAX_NODES: 1024 → 2048 (a complex 31KB page can have >1024 elements + text nodes)
      • WE_MAX_ATTRS: 512 → 1024 (matching the larger node pool)
    • C2D_TextBuf NULL checks (web_render.c)
      • Added OOM guards after C2D_TextBufNew(4096) in both the wrapping and non-wrapping text render paths. If allocation fails, the function skips drawing that text instead of crashing.
    • All unprotected duk_eval_string calls replaced with protected versions:
File Call Fix
web_js.c:1346 duk_eval_string_noresult (polyfills) → duk_peval_string_noresult
web_iframe.c:184 duk_eval_string_noresult (postMessage stub) → duk_peval_string_noresult
web_canvas.c:369 duk_eval_string (canvas property setup) → duk_peval_string + fallback to plain object on failure

v0.47

  • Multi-line text rendering
    • wrender_text previously drew the entire text string as a single C2D_DrawText call, so paragraphs that the layout engine had correctly wrapped to multiple lines rendered as one long line overflowing off-screen. The function now detects when text exceeds the available width, splits at word boundaries using the same char_width metric as the layout engine, and draws each line separately with correct alignment, bold, italic, and decoration.
  • position: fixed / sticky
    • position: fixed elements no longer scroll with the page — the renderer skips applying the scroll offset for position==3 nodes, keeping them pinned to the viewport. position: sticky is now parsed (value 4) and treated like relative for layout. True scroll-tracking stickiness is deferred.
  • Dirty-only CSS cascade
    • css_cascade previously ran css_compute_node on every element every frame when any node was dirty. On a 100-element page with 50 CSS rules, that's ~5000 selector_matches calls per frame. Now only processes nodes where dirty==1. On page load all nodes are dirty (unchanged behavior); during per-frame hover/focus updates, typically 1-2 nodes are dirty, reducing cascade cost by ~99%.
  • window.getComputedStyle()
    • Returns a read-only object with 20+ computed style properties as CSS-formatted strings: widthheightdisplay (as string), color/backgroundColor (as rgba(...)), fontSizepositionopacity (as 0-1 float), visibility, margins, padding, border, left/top, and offsetLeft/Top/Width/Height. Fixed: display was being set twice (boolean then string); opacity was returning 0-255 instead of 0.0-1.0.
  • Element.matches() rewritten
    • Previously only handled #id.class, and bare tag names. Now delegates to css_selector_matches — the full selector matching engine — so it handles descendant selectors, attribute selectors, pseudo-classes (:hover:checked), :not(), adjacent siblings (+), and pseudo-elements.
  • Element.closest()
    • New method. Walks up the ancestor chain (including self) until one matches the given selector, returning the element or null. Cycle-guarded with g_dom_node_n bound. Uses the full css_selector_matches engine for each ancestor check.
  • Array.from() polyfill + NodeList alias
    • Polyfills Array.from for array-like objects and iterables with optional mapFn/thisArg. Aliases NodeList to Array so instanceof NodeList checks pass and NodeList.prototype.forEach exists (Duktape arrays already have forEach). JSON.parse/JSON.stringify verified working via Duktape's built-in DUK_USE_JSON_BUILTIN.
  • Files changed
File Changes
web_render.c Multi-line text wrapping in wrender_textposition:fixed scroll-offset skip
web_layout.c position:fixed uses viewport (0,0); position:sticky (value 4) treated like relative
web_css.c Dirty-only cascade in css_cascadecss_selector_matches public wrapper; position:sticky parser
web_css.h css_selector_matches declaration; CSS_SEL_LEN 64→128
web_dom.h position comment updated for sticky (value 4)
web_js.c getComputedStyleclosest, rewritten matchesArray.from polyfill, NodeList alias, opacity format fix

v0.45

  • All New Stuff
    • CSS selectors were completely failing on complex selectors
      • The selector_matches function only handled simple selectors (tag, .class#idtag.class). It could not match any selector containing:
        • Descendant combinators (spaces): .tog input.field input[type=text]details:not([open]) summary
        • Adjacent sibling combinators (+): .tog input:checked + .tog-sw
        • Attribute selectors ([type=checkbox]): input[type=checkbox]
        • Pseudo-elements (::after::before): summary::after.tog-sw::after
      • Since nearly every selector in the EaglerLite CSS uses at least one of these, zero CSS rules were matching — the page rendered with default styles only.
    • DPad scroll was broken
      • The v0.43 DPad scroll fix (which used we_page_scroll_by when the web engine was loaded) was accidentally reverted during the v0.44 bugfix pass, going back to the old text-mode-only scroll. DPad Up/Down did nothing when viewing a web page.
  • What I fixed
    • New simple_selector_matches function
      • Handles a single simple selector component, now with:
        • Attribute selectors[attr][attr=val][attr="val"] — parses the bracket, splits on =, strips quotes, checks the node's attribute
        • Pseudo-elements::after::before::marker::placeholder::selection — always accepted (we don't render them, but the selector matches the base element so its other properties still apply)
        • * with attribute selectors*[attr=val] works
        • Uses css_classlist_has (local, no strtok) instead of classlist_has (which lives in web_js.c)
    • Rewritten selector_matches function
      • Now handles compound selectors with combinators:
        • Descendant (space): .tog input — walks up ancestors checking each part right-to-left
        • Adjacent sibling (+): input + .tog-sw — checks the immediately preceding sibling
        • Splits the selector at spaces and + (respecting [bracket] depth so [type=checkbox] doesn't get split)
        • Each part is matched via simple_selector_matches
        • Guard counter on all ancestor walks prevents infinite loops
    • DPad scroll restored
      • DPad Up/Down/Left/Right now scrolls the web page via we_page_scroll_by(0, ±30) when g_web_engine.loaded is true. Falls back to text-mode scroll when the engine isn't loaded. Circle Pad is always cursor-only.

v0.44

  • CSS Custom Properties (Variables) — var() + :root
    • The biggest addition. :root { --bg: #000; } stores custom properties in a global variable table. var(--bg) is resolved at parse time before any property handler runs — works in colors, lengths, and any value. Supports var(--name, fallback) syntax and recursive resolution (variables referencing other variables). Up to 64 variables stored. Reset on page recompile.
  • @media Queries — Graceful Skip
    • @media(max-width: 680px) { ... } is now parsed and skipped gracefully (with proper brace-nesting tracking). Previously, @media would confuse the stylesheet parser and corrupt all subsequent rules. Also skips @keyframes@import@font-face, and @charset.
  • :root Selector
    • :root { ... } declarations are applied as a universal * rule with the lowest specificity, so :root properties cascade to all elements via inheritance. Custom properties (--name: value) are stored in the variable table.
  • :not() Selector
    • :not(.hidden) now works. The inner selector is recursively matched and negated. Forward-declared to handle the mutual recursion between pseudo_matches → pseudo_not_matches → selector_matches.
  • Comma-Separated Selectors
    • h1, h2, h3 { ... } now creates a separate rule for each sub-selector instead of creating one rule with a comma in the selector (which never matched anything).
  • !important Support
    • color: red !important is now accepted — the !important suffix is stripped before parsing. All declarations are treated as having the same specificity (later wins), so !important doesn't change cascade behavior but no longer causes parse failures.
  • New CSS Properties (15 additions)
Property Description
box-sizing border-box / content-box (parsed, stored)
flex shorthand flex: 1 → grow=1; flex: 1 0 auto → grow/shrink/basis
flex-shrink Default 1
flex-basis auto or length
pointer-events none / auto
user-select none / auto
outline Width + color (simplified, no layout impact)
outline-width / outline-color / outline-style Individual properties
text-transform uppercase / lowercase / capitalize / none
letter-spacing px or normal
line-height px, unitless multiplier, or normal
list-style / list-style-type none / disc / circle / square / decimal
object-fit fill / contain / cover
min/max-width/height Now handles none and auto correctly

v0.43

  • UI: DPad = Scroll, Circle Pad = Cursor (fixed)
    • The biggest UX fix: DPad and Circle Pad no longer conflict. Previously, DPad both scrolled the fallback text renderer AND sent arrow key events to web content — pressing Up would scroll the page and fire a keydown("ArrowUp") at the same time. Circle Pad could also trigger scroll in some paths.
    • Now:
      • DPad scrolls the web page (30px per press, all four directions). When the web engine isn't loaded, it scrolls the fallback text output instead.
      • Circle Pad always moves the cursor — it never scrolls, in any mode.
      • DPad was removed from the web content key map entirely, so no more phantom arrow key events.
      • Only Y (Space) generates a keyboard event for web content.
      • Controls help screen updated to reflect the new assignments.
  • Cookie Clicker Compatibility: 3 Major JS APIs Added
    • element.classList — Cookie Clicker (and most modern JS) uses classList.add().remove().toggle().contains() extensively. Previously only className existed. Now classList returns a live object with all four methods that manipulate the node's class attribute and trigger css_compute_node so styles re-cascade.
    • Live element.style object — Cookie Clicker does element.style.display = 'none'element.style.left = '100px', etc. Previously style was a dead object — setting properties did nothing. Now the style object is wrapped in a Duktape Proxy that intercepts every property get/set:
      • Get (element.style.display): parses the inline style attribute and returns the property value.
      • Set (element.style.display = 'none'): updates the inline style string via a C-backed cssText setter, triggers css_compute_node + dom_mark_dirty so the layout and renderer pick up the change immediately.
    • onclick/onload/onerror property handlers — Cookie Clicker assigns element.onclick = function(){...} instead of using addEventListener or inline onclick="..." attributes. Previously only inline attribute handlers and addEventListener worked. Now we_dispatch checks for JS function properties on the node object (onclickonmousedownonloadonsubmit, etc.) and calls them with a proper event object. Supports all 13 event types.

v0.42

  • CSS Transitions
    • transition: 0.3s ease-in-out now actually animates. When a style property changes on an element with a transition duration, the old and new values are interpolated over the duration using smoothstep easing. Supports 25 numeric properties (opacity, width, height, margins, padding, position, font-size, border, transforms) plus color properties (color, background) which interpolate per-channel. The transition engine runs in the frame loop and marks transitioning nodes dirty so layout re-runs each frame to reflect the changing values.
  • Box-Shadow
    • box-shadow: 2px 2px 4px rgba(0,0,0,0.5) is parsed into offset/blur/color and drawn as a semi-transparent rect behind the element before the background fill. No real gaussian blur (3DS hardware can't do that efficiently), but the spread approximation looks reasonable for most UI shadows.
  • Linear-Gradient Backgrounds
    • background: linear-gradient(to right, red, blue) is parsed into start/end colors and a direction, then rendered as 16 interpolated color bands. Supports to right/bottom/left/top directions and Ndeg angles. Falls back to solid background color when no gradient is active. Transparent colors work correctly (uses explicit "set" flags instead of checking for zero).
  • CSS Grid Layout
    • display: grid with grid-template-columns now lays out children in a grid. Supports fixed pixel widths, fr/auto (equal distribution), and repeat(N, ...) syntax up to 8 columns. Rows auto-size to the tallest cell, gap is applied between cells, and children are vertically centered within their row.
  • CSS calc()
    • calc(100% - 20px) is now evaluated at layout time. Supports +-*/ with correct operator precedence, and all length units (px, em, rem, vw, vh, %) work inside calc terms. Self-contained in the length parser — no new systems.
  • Form Submission
    • Clicking a <button type="submit"> or <input type="submit"> inside a <form> now fires the form's onsubmit handler and dispatches a submit event. This replaces the old START-key approach which conflicted with START=exit-run-mode.
  • Keybind Conflict Fixes
    • Resolved button assignment conflicts between the app shell and web content event system. In run mode, only DPad (arrow keys) and Y (Space) generate keyboard events for web content. A/B/X/L/R/START are exclusively mouse clicks, cursor lock, and exit-run-mode — no more phantom keydown events alongside mouse clicks.
  • Bug Fixes
    • IndexedDB path overflow: store/key strings from JS are now length-validated to prevent path truncation
    • IndexedDB get() return logic: was returning true for empty files
    • IndexedDB clear() prefix buffer: enlarged from 80 to 128 bytes to handle longer store names
    • Transition color precision: RGBA8 colors now stored as uint32 (not float) to avoid precision loss past 2^24
    • Gradient transparent-color: 0x00000000 (transparent) no longer treated as "unset"

v0.41

  • <input>/<textarea> text entry
    • New we_input_prompt() in web_events.c opens the 3DS software keyboard when a click lands on an <input>/<textarea> (or an ancestor of one). Reads the current value attribute, shows it as initial text, writes the result back, fires input + change events, and marks the node dirty so the renderer shows the new text.
    • New we_text_input() helper wraps swkbdInit/swkbdInputText for reuse.
  • CSS transform: translate()
    • wrender_node now applies translate_x/translate_y to the draw position, and folds the offset into the children's ox/oy so children move with the translated parent. ~6 lines of real logic; the parser already stored the values since v0.39.
  • Word-wrap in the layout engine
    • Replaced the naive tw/avail_w + 1 character-count wrapping with proper word-boundary wrapping. Walks the text word by word, tracks line width, breaks when the next word would overflow. Hard-breaks words longer than a full line. Respects white-space: pre and nowrap.
  • CSS :hover / :focus / :nth-child pseudo-classes
    • New pseudo_matches() function checks :hover:focus:active:checked:disabled:first-child:last-child:nth-child(n/odd/even).
    • selector_matches() rewritten to split selectors at : and check the pseudo-class after the base selector matches. Supports chained pseudos (a:focus:hover).
    • Dynamic re-cascade: we_events_update marks nodes dirty on hover/focus changes; we_update now re-runs css_cascade (not just layout_document) when nodes are dirty, so :hover/:focus styles apply dynamically.
  • Keyboard input to web content
    • window.prompt(message, default) now opens the 3DS software keyboard and returns the typed string (was a no-op stub returning undefined).
    • Clicking an <input>/<textarea> opens the keyboard (Feature 1 above).
  • localStorage persistence + IndexedDB completion
    • IndexedDB C API hardenedws_idb_open now checks for existing databases with the same name (no duplicates); ws_idb_get validates fread return (was a potential underflow); ws_idb_clear is no longer a no-op — it scans the directory with opendir/readdir and removes all files matching the store prefix; all functions now have proper NULL/bounds checks.
    • IndexedDB JS bindingsws_install_idb now installs a real indexedDB object to JS with open()put()get()delete()clear()close() methods. put() JSON-encodes values; get() safely JSON-parses them back (falls back to string if not valid JSON). The "IndexedDB" feature pill is now actually true.

v0.38

  • Crash / correctness bugs:
    • Canvas 2D silently broken — wc_install_bindings() built its property-setup script with snprintf into a 512-byte buffer, but the actual script is 854 bytes. It was truncating mid-statement, so ctx.fillStyle = 'red' and every other canvas property setter did nothing, with zero error shown. Now passed as a literal directly to duk_eval_string — no buffer, no truncation possible.
    • Event listeners could misfire — DOMEvent.js_ref was a uint8_t, silently wrapping after the 255th addEventListener() call in a session and firing the wrong callback. Widened to uint16_t.
    • Listener ref counter never reset — it was a function-local static that persisted across every recompile instead of resetting with the fresh Duktape context each page load gets. Moved to file scope with an explicit reset tied to context creation.
    • Unchecked array access in we_document_open() — switched to the existing bounds-checked accessor so a corrupted sibling chain can't read out-of-bounds memory.
  • Stack-safety hardening (3DS has only 256KB of stack):
    • ~3KB of stacked buffers in the CSS parser — css_parse_inline's 2048-byte buffer is called while css_parse_stylesheet's 1024-byte buffer is still live. Both made static (verified safe: parsing is never re-entrant).
    • Unbounded layout recursion — nothing stopped a deeply-nested DOM tree (up to the 1024-node cap) from recursing that many stack frames deep. Added a depth guard capped well above any realistic page.
  • Version v0.37 was skipped due to issues with a beta build.
  • CIA files will be added in version 1.0

v0.36

  • Crash / correctness fixes:
    • editor_find and editor_find_prevstart_line was not clamped before use — with a corrupted or uninitialized value it could make editor_get_line walk off the end of the buffer. Now clamped to [0, total-1].
    • editor_replace_at: added col > llen guard so an exact-end-of-line replacement can't produce a negative memcpy count.
    • we_eval_js: added !g_web_engine.loaded check — without it, a stale non-NULL js_ctx pointer left from a previous we_unload() call could be dereferenced.
    • draw_consolei < WJS_LOG_MAX_LINES guard added — a corrupted g_wjs_log_n could have caused a read past the log array bounds.
  • Compiler warning fixes:
    • (down & KEY_X) && ... — explicit parentheses added (GCC -Wall warning eliminated).
    • Two strcat() calls replaced with strncat().
    • LAYOUT_ED_H corrected to 178.0f (was 180.0f) — the 2px mismatch caused the cursor-to-line mapping to select the wrong line near the bottom of the editor panel.

v0.34

  • Slight UI changes
  • Bordered rendering for text & iframe
  • Memory usage fixes
  • More accurate CSS parsing
File Fixes/Changes
web_layout.c Body avail_w bug fixed: was 2×margin_left, now margin_left + margin_right.
web_layout.c Text node lay_w uses full available container width for alignment.
web_render.c Scale formula size/11.0f → size/30.0f (correct for system font at scale 1.0 ≈ 30px).
web_render.c TEXT nodes use parent container width for proper text alignment.
web_render.c Bottom border drawn on document viewport to frame the output area.
web_engine.c css_apply_ua_defaults() called after css_reset(), before parse — so user styles always win.
web_engine.c RUNNING bar cleaned up; conflicting title bar overlay removed.

v0.33.5

  • Hopefully fixed crashing issues

v0.33.4

  • MAYBE fixed crashing issues (untested, will update when tested)

v0.33.3

  • Fixed surface-level crashing issues

v0.33.2

  • Maybe fixed crashing issues
  • Stability fixes
  • Fixed errors while compiling

v0.33

  • Fixed scrolling
  • Fixed line truncation
  • Added more complete rendering capabilities
  • Added pointer lock integration
  • Added page interaction

v0.32

  • Fixed the HTML rendering engine
  • Slight UI changes

v0.31.1

  • Completely overhauled the UI (AI was used to help with this)
  • New frame buffer that minimizes the visual bug to almost unnoticeable proportions.

Pre-Release v0.30

  • Fixed app crashing on file explorer open (MAYBE FR THIS TIME)
  • Fixed the visual bug (kinda)

Credits

PocketCompiler project and idea by PlanetDogeCodes. Some browser runtime features, custom Canvas/WebGL Integration, and 3DS Optimizations were all made with help from GPT-5.5 (xhigh) and GLM 5.2 (max). Some UI elements and DOM-style event handling were made with help from Claude Sonnet 5 (high)

Built with:

  • Visual Studio Code
  • devkitPro
  • devkitARM
  • libctru
  • Citro2D
  • Citro3D

External links

Advertising: