buninu-browser 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +90 -1
  3. package/buninu-browser.js +1 -1
  4. package/frontends/wayland/README.md +35 -2
  5. package/frontends/wayland/client.js +8 -6
  6. package/frontends/wayland/clipboard.js +1 -1
  7. package/frontends/wayland/index.js +1 -1
  8. package/frontends/wayland/input.js +1 -1
  9. package/frontends/wayland/resource-io.js +6 -0
  10. package/frontends/wayland/transport-posix.js +120 -0
  11. package/frontends/wayland/transport-win32.js +9 -0
  12. package/frontends/wayland/transport.js +3 -120
  13. package/frontends/wayland/win32-resources.js +80 -0
  14. package/frontends/wayland/win32-wire.js +62 -0
  15. package/lib/audio/biquad.js +25 -0
  16. package/lib/audio/piano.js +6 -3
  17. package/lib/audio/playback-buffer.js +5 -5
  18. package/lib/audio/pulse-client.js +24 -5
  19. package/lib/audio/web-audio.js +570 -0
  20. package/lib/browser-dialog.js +116 -0
  21. package/lib/cdp-server.js +43 -11
  22. package/lib/file-picker.js +48 -0
  23. package/lib/headless-shell.js +34 -12
  24. package/lib/layout/text-layout.js +112 -19
  25. package/lib/media/mp4-frame-times.js +93 -0
  26. package/lib/paint/direct-canvas.ts +39 -0
  27. package/lib/paint/display-list.js +37 -12
  28. package/lib/paint/raster-readback.js +16 -7
  29. package/lib/renderer/animation-frames.js +10 -5
  30. package/lib/renderer/bindings.js +69 -16
  31. package/lib/renderer/canvas-2d.js +265 -15
  32. package/lib/renderer/canvas-round-rect.js +36 -0
  33. package/lib/renderer/css-animations.js +173 -0
  34. package/lib/renderer/dialog-client.js +30 -0
  35. package/lib/renderer/dialog-worker.js +35 -0
  36. package/lib/renderer/eval-environment.js +28 -0
  37. package/lib/renderer/file-input.js +49 -0
  38. package/lib/renderer/host.js +33 -4
  39. package/lib/renderer/html-drag.js +77 -0
  40. package/lib/renderer/image-loading.js +45 -2
  41. package/lib/renderer/main.js +4 -0
  42. package/lib/renderer/module-transform.js +5 -3
  43. package/lib/renderer/page-builtins.js +59 -0
  44. package/lib/renderer/page-realm.js +177 -40
  45. package/lib/renderer/page-renderer.js +397 -49
  46. package/lib/renderer/piano-input.js +3 -1
  47. package/lib/renderer/raster-surface.js +18 -0
  48. package/lib/renderer/script-rewrite.js +165 -9
  49. package/lib/renderer/video-presentation.js +5 -4
  50. package/lib/renderer/webgl/clip.js +24 -0
  51. package/lib/renderer/webgl/context.js +158 -0
  52. package/lib/renderer/webgl/cpu-fragment.js +211 -0
  53. package/lib/renderer/webgl/draw-cache.js +22 -0
  54. package/lib/renderer/webgl/pipeline.js +70 -0
  55. package/lib/renderer/webgl/shaders.js +58 -0
  56. package/lib/renderer/webgl/textures.js +43 -0
  57. package/lib/renderer/window-named-properties.js +46 -0
  58. package/lib/screenshot-cli.js +4 -7
  59. package/lib/style/animation-values.js +29 -0
  60. package/lib/style/computed-style.js +109 -13
  61. package/lib/style/transform.js +44 -0
  62. package/lib/window-size.js +10 -0
  63. package/package.json +1 -1
  64. package/patches/happy-dom/0007-scope-content-models-to-html-namespace.patch +29 -0
  65. package/patches/happy-dom/README.md +16 -0
  66. package/test/animation-frames.test.js +15 -0
  67. package/test/canvas-2d.test.js +252 -4
  68. package/test/canvas-frame-pacing.test.js +7 -0
  69. package/test/cdp-viewers.test.js +36 -0
  70. package/test/classic-games-patches/0001-fix-game-initialization.patch +177 -0
  71. package/test/classic-games-patches/README.md +156 -0
  72. package/test/classic-games-scenarios.js +133 -0
  73. package/test/clipboard.test.js +16 -0
  74. package/test/computed-style.test.js +18 -0
  75. package/test/css-animation-lifecycle.test.js +28 -0
  76. package/test/css-animations.test.js +83 -0
  77. package/test/direct-canvas.test.js +39 -0
  78. package/test/dynamic-inline-handlers.test.js +25 -0
  79. package/test/eval-declarations.test.js +22 -0
  80. package/test/file-picker.test.js +26 -0
  81. package/test/fixtures/canvas-damage.js +23 -0
  82. package/test/fixtures/canvas-frame-pacing.js +35 -0
  83. package/test/fixtures/direct-canvas.js +96 -0
  84. package/test/fixtures/javascript-dialogs.html +8 -0
  85. package/test/fixtures/media-audio-lifetime.js +65 -0
  86. package/test/fixtures/renderer-memory.js +7 -5
  87. package/test/fixtures/video-black-replay.js +8 -6
  88. package/test/fixtures/video-slow-end.js +39 -0
  89. package/test/fixtures/web-audio-renderer.js +45 -0
  90. package/test/flex.test.js +26 -0
  91. package/test/grid.test.js +13 -0
  92. package/test/headless-shell.test.js +9 -0
  93. package/test/headless-window-size.test.js +28 -0
  94. package/test/html-rcdata.test.js +35 -0
  95. package/test/image-decode.test.js +18 -0
  96. package/test/javascript-dialog.test.js +83 -0
  97. package/test/manual-breakout-paddle.js +42 -0
  98. package/test/manual-breakout-static.js +35 -0
  99. package/test/manual-classic-games.js +41 -0
  100. package/test/manual-clumsy-bird-three-pipes.js +81 -0
  101. package/test/manual-clumsy-bird.js +52 -0
  102. package/test/manual-hextris.js +30 -0
  103. package/test/manual-numbered-music.js +24 -0
  104. package/test/manual-t-rex-runner.js +49 -0
  105. package/test/manual-threejs-scene.js +64 -0
  106. package/test/manual-webgl-benchmark.js +27 -0
  107. package/test/manual-webgl-cpu.html +26 -0
  108. package/test/media-audio-lifetime.test.js +8 -0
  109. package/test/mp4-frame-times.test.js +33 -0
  110. package/test/page-scripts.test.js +96 -1
  111. package/test/piano-input.test.js +15 -2
  112. package/test/piano.test.js +11 -2
  113. package/test/playback-buffer.test.js +11 -0
  114. package/test/pointer-drag.test.js +33 -0
  115. package/test/positioning.test.js +82 -0
  116. package/test/pulse-client.test.js +51 -2
  117. package/test/raster-readback.test.js +14 -0
  118. package/test/script-rewrite.test.js +61 -3
  119. package/test/video-slow-end.test.js +6 -0
  120. package/test/viewport-keyboard-overflow.test.js +25 -0
  121. package/test/wayland-win32-resources.test.js +47 -0
  122. package/test/wayland-win32-wire.test.js +59 -0
  123. package/test/web-audio-renderer.test.js +8 -0
  124. package/test/web-audio.test.js +264 -0
  125. package/test/webgl-clip.test.js +9 -0
  126. package/test/webgl-cpu-fragment.test.js +43 -0
  127. package/test/window-named-properties.test.js +41 -0
  128. package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfig.ts +2 -2
  129. package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfigContentModelEnum.ts +1 -0
  130. package/vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts +23 -3
  131. package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +1 -1
  132. package/vendor/land-renderer/LICENSE +21 -0
  133. package/vendor/land-renderer/README.md +14 -0
  134. package/vendor/land-renderer/direct-compositor.ts +443 -0
  135. package/vendor/land-renderer/frame.js +88 -0
  136. package/vendor/land-renderer/png-fast.js +85 -0
  137. package/vendor/land-renderer/raw-frames.js +66 -0
  138. package/vendor/land-renderer/render-types.ts +7 -0
  139. package/vendor/land-renderer/renderer.ts +134 -0
  140. package/vendor/land-renderer/snapshot.js +37 -0
  141. package/vendor/land-renderer/viewport.js +18 -0
  142. package/vendor/ses/dist/ses.mjs +1 -1
@@ -0,0 +1,264 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {createWebAudioScope} from '../lib/audio/web-audio.js';
3
+
4
+ function setup(realtime = false) {
5
+ const outputs=[],closed=[],errors=[];
6
+ const scope=createWebAudioScope({realtime,
7
+ audio:{
8
+ open(options,listener){outputs.push({options,listener,pcm:[]});return outputs.length;},
9
+ write(id,bytes){outputs[id-1].pcm.push(new Int16Array(bytes.buffer,bytes.byteOffset,bytes.byteLength/2).slice());},
10
+ close(id){closed.push(id);},
11
+ },reportError:error=>errors.push(String(error)),
12
+ });
13
+ const context=new scope.api.AudioContext({sampleRate:8000});
14
+ const pull=(frames=128)=>{
15
+ const output=outputs.at(-1);
16
+ output.listener({event:'request',bytes:frames*4});
17
+ return output.pcm.at(-1);
18
+ };
19
+ return {...scope,context,outputs,closed,errors,pull};
20
+ }
21
+
22
+ test('oscillator PCM follows frequency, gain, disconnect and reconnect without duplicate connections',()=>{
23
+ const f=setup(),c=f.context,o=c.createOscillator(),g=c.createGain();
24
+ try {
25
+ o.frequency.setValueAtTime(500,c.currentTime);g.gain.value=.25;
26
+ expect(o.connect(g)).toBe(g);g.connect(c.destination);g.connect(c.destination);o.start();
27
+ const first=f.pull();
28
+ expect(first[8]).toBeCloseTo(8192,0); // Stereo frame 4: sin(pi/2) * .25.
29
+ for(let i=0;i<first.length;i+=2)expect(first[i]).toBe(first[i+1]);
30
+ o.frequency.setValueAtTime(1000,c.currentTime);
31
+ const second=f.pull();expect(second[4]).toBeCloseTo(8192,0);
32
+ o.disconnect();expect([...f.pull()].every(x=>x===0)).toBe(true);
33
+ o.connect(g);g.gain.value=.1;expect(f.pull()[4]).toBeCloseTo(3277,0);
34
+ expect(c.currentTime).toBe(512/8000);
35
+ expect(f.outputs).toHaveLength(1);
36
+ }finally{f.dispose();}
37
+ expect(f.closed).toEqual([1]);expect(f.errors).toEqual([]);
38
+ });
39
+
40
+ test('scheduled start, stop, gain ramps and ended events use the audio sample timeline',async()=>{
41
+ const f=setup(),c=f.context,o=c.createOscillator(),g=c.createGain();let ended=0;
42
+ try {
43
+ o.frequency.value=500;o.connect(g).connect(c.destination);
44
+ g.gain.setValueAtTime(0,0).linearRampToValueAtTime(1,.032);
45
+ o.onended=()=>ended++;o.start(.016);o.stop(.04);
46
+ const samples=f.pull(512);
47
+ expect([...samples.slice(0,128*2)].every(x=>x===0)).toBe(true);
48
+ expect(samples[(128+4)*2]).toBeCloseTo(Math.round(32767*132/256),0);
49
+ expect([...samples.slice(320*2)].every(x=>x===0)).toBe(true);
50
+ await Bun.sleep(10);expect(ended).toBe(1);
51
+ f.pull();await Bun.sleep(5);expect(ended).toBe(1);
52
+ expect(()=>o.start()).toThrow();
53
+ }finally{f.dispose();}
54
+ });
55
+
56
+ test('suspend freezes the context and resume reopens only its own stream; close rejects later resume',async()=>{
57
+ const f=setup(),c=f.context,o=c.createOscillator();
58
+ o.connect(c.destination);o.start();f.pull();const time=c.currentTime;
59
+ await c.suspend();expect(c.state).toBe('suspended');expect(f.closed).toEqual([1]);
60
+ f.outputs[0].listener({event:'request',bytes:512});expect(c.currentTime).toBe(time);
61
+ await c.resume();expect(c.state).toBe('running');expect(f.outputs).toHaveLength(2);
62
+ f.pull();expect(c.currentTime).toBeGreaterThan(time);
63
+ await c.close();expect(c.state).toBe('closed');expect(f.closed).toEqual([1,2]);
64
+ await expect(c.resume()).rejects.toMatchObject({name:'InvalidStateError'});
65
+ f.dispose();expect(f.closed).toEqual([1,2]);
66
+ });
67
+
68
+ test('parameter ramps can be held and cancelled; invalid times, ports and cross-context graphs reject',()=>{
69
+ const f=setup(),c=f.context,o=c.createOscillator(),g=c.createGain();
70
+ try {
71
+ const p=g.gain;
72
+ p.setValueAtTime(0,0).linearRampToValueAtTime(1,.1).cancelAndHoldAtTime(.05);
73
+ f.pull(800);expect(p.value).toBeCloseTo(.5);
74
+ p.cancelScheduledValues(0);p.value=.25;expect(p.value).toBe(.25);
75
+ expect(()=>p.setValueAtTime(1,-1)).toThrow(RangeError);
76
+ expect(()=>p.setValueAtTime(NaN,0)).toThrow(TypeError);
77
+ expect(()=>p.exponentialRampToValueAtTime(0,1)).toThrow(RangeError);
78
+ expect(()=>o.stop()).toThrow();
79
+ expect(()=>o.connect(c.destination,1)).toThrow();
80
+ const other=new f.api.AudioContext();expect(()=>o.connect(other.destination)).toThrow();
81
+ }finally{f.dispose();}
82
+ });
83
+
84
+ test('multiple sources mix in one stream and cycles cannot recurse indefinitely',()=>{
85
+ const f=setup(),c=f.context,a=c.createOscillator(),b=new f.api.OscillatorNode(c,{frequency:1000,detune:1200});
86
+ try {
87
+ const g=new f.api.GainNode(c,{gain:.1});
88
+ a.frequency.value=500;a.connect(g);b.connect(g);g.connect(c.destination);
89
+ a.start();b.start();const samples=f.pull();
90
+ expect(samples[4]).toBeCloseTo(Math.round(32767*.1*Math.sin(Math.PI/4)),0);
91
+ expect(f.outputs).toHaveLength(1);
92
+ g.connect(g);expect([...f.pull()].every(Number.isFinite)).toBe(true);
93
+ expect(()=>new f.api.AudioNode(c)).toThrow(TypeError);
94
+ expect(()=>new f.api.AudioParam()).toThrow(TypeError);
95
+ expect(f.api.webkitAudioContext).toBe(f.api.AudioContext);
96
+ }finally{f.dispose();}
97
+ });
98
+
99
+ test('sample buffers play through the existing PCM graph with looping, rate and ended events',async()=>{
100
+ const f=setup(),c=f.context;
101
+ try{
102
+ const b=c.createBuffer(1,4,8000);b.copyToChannel(new Float32Array([0,.25,.5,.75]),0);
103
+ expect([...b.getChannelData(0)]).toEqual([0,.25,.5,.75]);expect(b.duration).toBe(.0005);
104
+ const copy=new Float32Array(3);b.copyFromChannel(copy,0,1);expect([...copy]).toEqual([.25,.5,.75]);
105
+ expect(()=>b.getChannelData(1)).toThrow();
106
+ const node=c.createBufferSource();let ended=0;node.buffer=b;node.onended=()=>ended++;
107
+ node.loop=true;node.loopStart=1/8000;node.loopEnd=3/8000;
108
+ node.connect(c.destination);node.start();
109
+ const pcm=f.pull(8);expect([...pcm.filter((_,i)=>i%2===0)]).toEqual([0,8192,16384,8192,16384,8192,16384,8192]);
110
+ node.stop(c.currentTime);f.pull(128);await Bun.sleep(10);expect(ended).toBe(1);
111
+ const faster=c.createBufferSource();faster.buffer=b;faster.playbackRate.value=2;faster.connect(c.destination);faster.start();
112
+ expect([...f.pull(128).filter((_,i)=>i%2===0)].filter(x=>x!==0)).toEqual([16384]);
113
+ expect(()=>faster.start()).toThrow();
114
+ }finally{f.dispose();}
115
+ });
116
+
117
+ test('decodeAudioData detaches input, resamples samples, completes callbacks and closes the decoder',async()=>{
118
+ let closed=0,success=0,failures=0;
119
+ const scope=createWebAudioScope({realtime:false,decodeAudio:async bytes=>{
120
+ if(bytes[0]===0)throw Error('invalid compressed sound');
121
+ let first=true;return {sampleRate:16000,channels:1,nextSamples(){if(!first)return null;first=false;return new Float32Array([0,.25,.5,.75,1,.75,.5,.25]);},close(){closed++;}};
122
+ }});
123
+ const c=new scope.api.AudioContext({sampleRate:8000});
124
+ try{
125
+ const input=new Uint8Array([1]).buffer;
126
+ const pending=c.decodeAudioData(input,b=>{success++;expect(b.sampleRate).toBe(8000);});
127
+ expect(input.byteLength).toBe(0);
128
+ const b=await pending;expect([...b.getChannelData(0)]).toEqual([0,.5,1,.5]);expect(success).toBe(1);expect(closed).toBe(1);
129
+ await expect(c.decodeAudioData(new Uint8Array([0]).buffer,null,e=>{failures++;expect(e.name).toBe('EncodingError');})).rejects.toMatchObject({name:'EncodingError'});
130
+ expect(failures).toBe(1);
131
+ await expect(c.decodeAudioData(new Uint8Array([1]))).rejects.toThrow('ArrayBuffer');
132
+ }finally{scope.dispose();}
133
+ });
134
+
135
+ test('BiquadFilterNode reports frequency response and filters actual PCM',async()=>{
136
+ const t=setup(),{context}=t;
137
+ try{
138
+ const filter=context.createBiquadFilter();filter.frequency.value=200;filter.Q.value=0;
139
+ expect(filter).toBeInstanceOf(t.api.BiquadFilterNode);
140
+ const frequencies=new Float32Array([0,100,2000,5000]),magnitude=new Float32Array(4),phase=new Float32Array(4);
141
+ filter.getFrequencyResponse(frequencies,magnitude,phase);
142
+ expect(magnitude[0]).toBeCloseTo(1,5);expect(magnitude[1]).toBeGreaterThan(.8);
143
+ expect(magnitude[2]).toBeLessThan(.01);expect(Number.isNaN(magnitude[3])).toBe(true);
144
+ expect(()=>filter.getFrequencyResponse(frequencies,new Float32Array(1),phase)).toThrow();
145
+ const oscillator=context.createOscillator();oscillator.frequency.value=2000;oscillator.connect(filter);filter.connect(context.destination);oscillator.start();
146
+ await context.resume();let filtered;
147
+ for(let i=0;i<12;i++)filtered=t.pull();
148
+ const rms=samples=>Math.sqrt(samples.reduce((sum,value)=>sum+value*value,0)/samples.length);
149
+ expect(rms(filtered)).toBeLessThan(300);
150
+ filter.type='highpass';let passed;
151
+ for(let i=0;i<12;i++)passed=t.pull();
152
+ expect(rms(passed)).toBeGreaterThan(10000);
153
+ }finally{await context.close();t.dispose();}
154
+ });
155
+
156
+
157
+ test('live synthesis yields between bounded packets so page timers can schedule future notes', async()=>{
158
+ const f=setup(true),c=f.context;let timer;
159
+ try {
160
+ let next=0;
161
+ const schedule=()=>{while(next<c.currentTime+.1){const o=c.createOscillator();o.frequency.value=500;o.connect(c.destination);o.start(next);o.stop(next+.055);next+=.04;}};
162
+ schedule();timer=setInterval(schedule,25);
163
+ const started=performance.now();f.outputs[0].listener({event:'request',bytes:8000*.25*4});
164
+ expect(c.currentTime).toBeLessThanOrEqual(.08+128/c.sampleRate);
165
+ await Bun.sleep(170);
166
+ expect(c.currentTime).toBeLessThan((performance.now()-started)/1000+.10);
167
+ const pcm=f.outputs[0].pcm;expect(pcm.length).toBeGreaterThan(3);
168
+ expect(pcm.every(packet=>packet.length<=8000*.096*2)).toBe(true);
169
+ expect(pcm.every(packet=>packet.some(value=>value!==0))).toBe(true);
170
+ } finally {clearInterval(timer);f.dispose();}
171
+ });
172
+
173
+
174
+ test('silent connected gain branches remain reconnectable and preserve elapsed automation',()=>{
175
+ const f=setup(),c=f.context;
176
+ try {
177
+ const master=c.createGain();master.connect(c.destination);
178
+ for(let i=0;i<300;i++){const g=c.createGain();g.gain.setValueAtTime(.1,0).exponentialRampToValueAtTime(.001,.1);g.connect(master);}
179
+ const gain=c.createGain();gain.gain.setValueAtTime(0,0).linearRampToValueAtTime(.5,.032);gain.connect(master);
180
+ expect([...f.pull(256)].every(v=>v===0)).toBe(true);
181
+ const o=c.createOscillator();o.frequency.value=500;o.connect(gain);o.start();
182
+ expect(f.pull()[8]).toBeCloseTo(Math.round(32767*.5),0);
183
+ o.disconnect();expect([...f.pull()].every(v=>v===0)).toBe(true);
184
+ o.connect(gain);expect(f.pull().some(v=>v!==0)).toBe(true);
185
+ }finally{f.dispose();}
186
+ });
187
+
188
+
189
+ test('live audio catches up after repeated long paint tasks instead of slowing its clock',async()=>{
190
+ const f=setup(true),c=f.context,o=c.createOscillator();let painting;
191
+ try {
192
+ o.connect(c.destination);o.start();
193
+ f.outputs[0].listener({event:'request',bytes:8000*2*4});
194
+ const started=performance.now();
195
+ painting=setInterval(()=>{const end=performance.now()+140;while(performance.now()<end){}},16);
196
+ await Bun.sleep(700);
197
+ clearInterval(painting);
198
+ const elapsed=(performance.now()-started)/1000;
199
+ expect(c.currentTime).toBeGreaterThan(elapsed*.9);
200
+ expect(c.currentTime).toBeLessThan(elapsed+.10);
201
+ }finally{clearInterval(painting);f.dispose();}
202
+ });
203
+
204
+ test('unreferenced finished filter graphs are collected after their real tails decay',async()=>{
205
+ const f=setup(),c=f.context;
206
+ let weakFilter,weakGain;
207
+ (()=>{
208
+ const source=c.createBufferSource(),filter=c.createBiquadFilter(),gain=c.createGain();
209
+ weakFilter=new WeakRef(filter);weakGain=new WeakRef(gain);
210
+ filter.type='lowpass';filter.frequency.value=100;filter.Q.value=10;
211
+ const buffer=c.createBuffer(1,1,c.sampleRate);buffer.getChannelData(0)[0]=1;source.buffer=buffer;
212
+ source.connect(filter).connect(gain).connect(c.destination);source.start();
213
+ })();
214
+ try{
215
+ f.pull(128);await Bun.sleep(5);Bun.gc(true);await Bun.sleep(0);
216
+ expect(weakFilter.deref()!==undefined).toBe(true); // tail owns its path after source ends
217
+ expect(f.pull(128).some(v=>v!==0)).toBe(true);
218
+ f.pull(8000); // let the IIR decay below float32's normal range
219
+ for(let i=0;i<4;i++){await Bun.sleep(5);expect(f.pull().every(v=>v===0)).toBe(true);Bun.gc(true);}
220
+ expect(weakFilter.deref()===undefined).toBe(true);
221
+ expect(weakGain.deref()===undefined).toBe(true);
222
+ expect(f.pull().every(v=>v===0)).toBe(true);
223
+ }finally{f.dispose();}
224
+ });
225
+
226
+ test('an author-held silent filter remains reconnectable after collection of other graphs',async()=>{
227
+ const f=setup(),c=f.context,filter=c.createBiquadFilter();
228
+ filter.type='lowpass';filter.frequency.value=500;filter.connect(c.destination);
229
+ try{
230
+ f.pull(256);await Bun.sleep(5);Bun.gc(true);
231
+ const source=c.createOscillator();source.frequency.value=200;source.connect(filter);source.start();
232
+ expect(f.pull(256).some(v=>v!==0)).toBe(true);
233
+ source.stop();f.pull(8000);expect(f.pull().every(v=>v===0)).toBe(true);
234
+ const next=c.createOscillator();next.frequency.value=200;next.connect(filter);next.start();
235
+ expect(f.pull(256).some(v=>v!==0)).toBe(true);
236
+ }finally{f.dispose();}
237
+ });
238
+
239
+ test('future scheduled voices retain intermediate processing nodes until playback',async()=>{
240
+ const f=setup(),c=f.context;let weak;
241
+ (()=>{const source=c.createOscillator(),filter=c.createBiquadFilter(),gain=c.createGain();
242
+ source.frequency.value=500;filter.frequency.value=1000;gain.gain.value=.25;
243
+ weak=new WeakRef(filter);source.connect(filter).connect(gain).connect(c.destination);source.start(.1);source.stop(.2);
244
+ })();
245
+ try{
246
+ for(let i=0;i<3;i++){await Bun.sleep(5);Bun.gc(true)}
247
+ expect(weak.deref()!==undefined).toBe(true);
248
+ const samples=f.pull(2048);
249
+ expect(samples.slice(0,800*2).every(v=>v===0)).toBe(true);
250
+ expect(samples.slice(800*2).some(v=>v!==0)).toBe(true);
251
+ }finally{f.dispose();}
252
+ });
253
+
254
+ test('AudioParam modulation keeps its running input alive through weak input connections',async()=>{
255
+ const f=setup(),c=f.context,signal=c.createOscillator(),gain=c.createGain();
256
+ gain.gain.value=0;signal.frequency.value=500;signal.connect(gain).connect(c.destination);signal.start();
257
+ let mod;
258
+ (()=>{const oscillator=c.createOscillator();oscillator.type='square';oscillator.frequency.value=0;oscillator.connect(gain.gain);oscillator.start();mod=new WeakRef(oscillator)})();
259
+ try{
260
+ await Bun.sleep(5);Bun.gc(true);
261
+ expect(f.pull()[8]).toBeCloseTo(32767,0);
262
+ mod.deref().stop();expect(f.pull().every(v=>v===0)).toBe(true);
263
+ }finally{f.dispose();}
264
+ });
@@ -0,0 +1,9 @@
1
+ import{test,expect}from'bun:test';
2
+ import{clipTriangles}from'../lib/renderer/webgl/clip.js';
3
+ test('homogeneous clipping keeps indexed geometry inside and interpolates crossings',()=>{
4
+ const source=new Float32Array([-.5,-.5,0,1,0,.5,-.5,0,1,1,0,.5,0,1,.5]),indices=[0,1,2];
5
+ const inside=clipTriangles(source,indices,5);expect(inside.vertices).toBe(source);expect(inside.indices).toBe(indices);
6
+ source[5]=2;const clipped=clipTriangles(source,indices,5);expect(clipped.indices.length).toBe(6);
7
+ for(let i=0;i<clipped.vertices.length;i+=5){const v=clipped.vertices.subarray(i,i+5);expect(Math.abs(v[0])).toBeLessThanOrEqual(v[3]);expect(v[4]).toBeGreaterThanOrEqual(0);expect(v[4]).toBeLessThanOrEqual(1);}
8
+ source[0]=NaN;expect(clipTriangles(source,indices,5).indices).toEqual([]);
9
+ });
@@ -0,0 +1,43 @@
1
+ import {beforeAll,expect,test} from 'bun:test';
2
+ import init from '../usr/lib/canvaskit/canvaskit.js';
3
+ import {compileStage,shader,floatImage,imageShader} from '../lib/renderer/webgl/pipeline.js';
4
+ import {runCPUFragment} from '../lib/renderer/webgl/cpu-fragment.js';
5
+ let kit;
6
+ beforeAll(async()=>{kit=await init({wasmBinary:await Bun.file(new URL('../usr/lib/canvaskit/canvaskit.wasm',import.meta.url)).arrayBuffer()});});
7
+ function compare(source,values=new Map()){
8
+ const stage=compileStage(kit,source,'fragment');expect(stage.cpu).not.toBeNull();
9
+ const width=8,height=8,coverage=new Float32Array(width*height*4),varyings=stage.varyings.map(()=>new Float32Array(coverage.length));
10
+ for(let y=0;y<height;y++)for(let x=0;x<width;x++){const i=(y*width+x)*4;coverage.set([1,height-y-.5,.5,1],i);for(const v of varyings)v.set([(x+.5)/width,(y+.5)/height,.25,1],i);}
11
+ const images=[coverage,...varyings].map(v=>floatImage(kit,width,height,v)),children=images.map(i=>imageShader(kit,i)),surface=kit.MakeSurface(width,height),paint=new kit.Paint();let effect;
12
+ try{effect=shader(kit,stage,values,children);paint.setShader(effect);surface.getCanvas().drawPaint(paint);surface.flush();const snapshot=surface.makeImageSnapshot();let expected;try{expected=snapshot.readPixels(0,0,{width,height,colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Premul,colorSpace:kit.ColorSpace.SRGB});}finally{snapshot.delete();}
13
+ const actual=runCPUFragment(stage,values,[],coverage,varyings,width,height,0,0,height);
14
+ let difference=0;for(let i=0;i<actual.length;i++)difference=Math.max(difference,Math.abs(actual[i]-expected[i]));expect(difference).toBeLessThanOrEqual(2);return actual;
15
+ }finally{paint.delete();effect?.delete();surface.delete();for(const c of children)c.delete();for(const i of images)i.delete();stage.effect.delete();}
16
+ }
17
+ test('numeric CPU fragment lighting and derivatives agree with CanvasKit',()=>{
18
+ compare('precision highp float;varying vec3 n;uniform vec3 light;void main(){vec3 N=normalize(n);float v=pow(max(dot(N,normalize(light)),0.),3.);gl_FragColor=vec4(v,abs(dFdx(n.x)),abs(dFdy(n.y)),.75);}',new Map([['light',[1,2,3]]]));
19
+ });
20
+ test('conditional expressions and logical operators do not evaluate skipped side effects',()=>{
21
+ const pixels=compare('precision highp float;float bump(inout float x){x+=.2;return x;}void main(){float x=0.;float a=true?bump(x):bump(x);bool b=false&&(bump(x)>0.);bool c=true||(bump(x)>0.);gl_FragColor=vec4(x,a,0.,1.);}');
22
+ expect([...pixels.slice(0,4)]).toEqual([51,51,0,255]);
23
+ });
24
+ test('mutable shader globals are reset between pixels',()=>{
25
+ const pixels=compare('precision highp float;float v=.25;void main(){v+=.25;gl_FragColor=vec4(v,0.,0.,1.);}');
26
+ expect(pixels[0]).toBe(128);expect(pixels.at(-4)).toBe(128);
27
+ });
28
+ test('control-flow derivatives retain the CanvasKit fallback',()=>{
29
+ for(const body of ['if(gl_FragCoord.x>3.)gl_FragColor=vec4(dFdx(gl_FragCoord.x));else gl_FragColor=vec4(1.);']){
30
+ const stage=compileStage(kit,'precision highp float;void main(){'+body+'}','fragment');expect(stage.cpu).toBeNull();stage.effect.delete();
31
+ }
32
+ });
33
+ test('numeric vertex transforms and varyings agree with CanvasKit float output',async()=>{
34
+ const {runVertices}=await import('../lib/renderer/webgl/pipeline.js');
35
+ const stage=compileStage(kit,'attribute vec3 position;attribute vec3 normal;uniform mat4 model;uniform mat3 normals;varying vec3 v;void main(){v=normalize(normals*normal);gl_Position=model*vec4(position,1.);}','vertex');
36
+ try{
37
+ expect(stage.cpuVertex).not.toBeNull();
38
+ const values=new Map([['model',[2,0,0,.1,0,3,0,.2,0,0,4,.3,5,6,7,1]],['normals',[1,0,0,0,2,0,0,0,3]]]);
39
+ const attrs=new Map([['position',new Float32Array([1,2,3,-1,.5,2])],['normal',new Float32Array([1,2,3,3,2,1])]]);
40
+ const actual=runVertices(kit,stage,values,attrs,2),reference=runVertices(kit,{...stage,cpuVertex:null},values,attrs,2);
41
+ for(let i=0;i<actual.length;i++)expect(actual[i]).toBeCloseTo(reference[i],5);
42
+ }finally{stage.effect.delete();}
43
+ });
@@ -0,0 +1,41 @@
1
+ import {test, expect} from 'bun:test';
2
+ import {RendererHost} from '../lib/renderer/host.js';
3
+ import {parseHTMLDocument} from '../lib/happy-dom/parser.js';
4
+
5
+ test('Window named elements are live, shadowable, and available in classic scripts and inline handlers', async () => {
6
+ const host = new RendererHost();
7
+ try {
8
+ await host.call('loadDocument', {url:'https://example.test/',source:`
9
+ <textarea id="source">hello</textarea><textarea id="result"></textarea>
10
+ <form name="namedForm"></form><input name="notGlobal"><div id="dupe"></div><div id="dupe"></div>
11
+ <button onclick="result.value=source.value.toUpperCase()">Convert</button>
12
+ <script>window.initial = source.value; window.group = dupe;</script>`,contentType:'text/html'});
13
+ expect(await host.call('evaluate', '[initial, namedForm.tagName, typeof notGlobal, dupe.length, dupe.item(0).tagName]')).toEqual(['hello','FORM','undefined',2,'DIV']);
14
+ expect(await host.call('evaluate', 'document.querySelector("button").click(); result.value')).toBe('HELLO');
15
+ expect(await host.call('evaluate', 'source.id="renamed"; [typeof source, renamed.value]')).toEqual(['undefined','hello']);
16
+ expect(await host.call('evaluate', 'window.renamed=42; let x=renamed;delete window.renamed;[x,renamed.value]')).toEqual([42,'hello']);
17
+ expect(await host.call('evaluate', 'document.querySelectorAll("#dupe")[0].remove(); [group.length, dupe.tagName]')).toEqual([1,'DIV']);
18
+ expect(await host.call('evaluate', 'renamed.remove(); typeof renamed')).toBe('undefined');
19
+ expect(await host.call('evaluate', `eval('(async()=>{\\nresult.value="first"\\nresult.value += " second"\\n})()'); result.value`)).toBe('first second');
20
+ } finally {host.close();}
21
+ },20000);
22
+
23
+ test('type selectors accept underscores, including escaped punctuation and selector lists', () => {
24
+ const {document} = parseHTMLDocument('<x_widget></x_widget><g:family_creation></g:family_creation>');
25
+ expect(document.querySelectorAll('x_widget, g\\:family_creation').length).toBe(2);
26
+ });
27
+
28
+ test('deferred absolute backgrounds paint before later relative links, including after relayout', async () => {
29
+ const host = new RendererHost();
30
+ try {
31
+ await host.call('loadDocument',{url:'https://example.test/',contentType:'text/html',source:`
32
+ <style>body{margin:0}.container{position:relative}.background{position:absolute;inset:0;background:red}
33
+ .front{position:relative}a{display:block;height:50px;background:green}</style>
34
+ <div class="container"><div class="background"><div style="height:100%"></div></div><div class="front"><a href="#ok">Click</a></div></div>`});
35
+ for (const width of [800,600,800]) {
36
+ await host.call('resize',width,600,1);
37
+ expect(await host.call('evaluate','document.elementFromPoint(20,20).tagName')).toBe('A');
38
+ expect(await host.call('click',20,20)).toBeTruthy();
39
+ }
40
+ }finally{host.close();}
41
+ },20000);
@@ -78,7 +78,7 @@ export default <
78
78
  },
79
79
  textarea: {
80
80
  className: 'HTMLTextAreaElement',
81
- contentModel: HTMLElementConfigContentModelEnum.anyDescendants
81
+ contentModel: HTMLElementConfigContentModelEnum.rcdata
82
82
  },
83
83
  script: {
84
84
  className: 'HTMLScriptElement',
@@ -499,7 +499,7 @@ export default <
499
499
  },
500
500
  title: {
501
501
  className: 'HTMLTitleElement',
502
- contentModel: HTMLElementConfigContentModelEnum.anyDescendants
502
+ contentModel: HTMLElementConfigContentModelEnum.rcdata
503
503
  },
504
504
  tr: {
505
505
  className: 'HTMLTableRowElement',
@@ -1,5 +1,6 @@
1
1
  enum HTMLElementConfigContentModelEnum {
2
2
  rawText = 'rawText',
3
+ rcdata = 'rcdata',
3
4
  noSelfDescendants = 'noSelfDescendants',
4
5
  noFirstLevelSelfDescendants = 'noFirstLevelSelfDescendants',
5
6
  noForbiddenFirstLevelDescendants = 'noForbiddenFirstLevelDescendants',
@@ -326,6 +326,18 @@ export default class HTMLParser {
326
326
  break;
327
327
  }
328
328
 
329
+ if (this.currentNode && this.readState === MarkupReadStateEnum.any &&
330
+ (<Element>this.currentNode)[PropertySymbol.namespaceURI] === NamespaceURI.html &&
331
+ HTMLElementConfig[(<Element>this.currentNode)[PropertySymbol.localName]]?.contentModel === HTMLElementConfigContentModelEnum.rcdata) {
332
+ const tag = (<Element>this.currentNode)[PropertySymbol.localName];
333
+ const end = new RegExp('</' + tag + '(?=[\\t\\n\\f\\r />])[^>]*>', 'gi');
334
+ end.lastIndex = this.markupRegExp.lastIndex;
335
+ const closing = end.exec(html);
336
+ const text = html.slice(this.markupRegExp.lastIndex, closing?.index ?? html.length);
337
+ this.parseRawTextElementContent(tag, text);
338
+ this.markupRegExp.lastIndex = closing ? end.lastIndex : html.length;
339
+ }
340
+
329
341
  lastIndex = this.markupRegExp.lastIndex;
330
342
  }
331
343
 
@@ -467,12 +479,14 @@ export default class HTMLParser {
467
479
 
468
480
  const tagName = this.nextElement![PropertySymbol.tagName]!;
469
481
  const lowerTagName = tagName.toLowerCase();
470
- const config = HTMLElementConfig[lowerTagName];
482
+ const config = this.nextElement![PropertySymbol.namespaceURI] === NamespaceURI.html
483
+ ? HTMLElementConfig[lowerTagName] : undefined;
471
484
  let previousCurrentNode: Node | null = null;
472
485
 
473
486
  while (previousCurrentNode !== this.rootNode) {
474
487
  const parentLowerTagName = (<Element>this.currentNode)[PropertySymbol.tagName]?.toLowerCase();
475
- const parentConfig = parentLowerTagName ? HTMLElementConfig[parentLowerTagName] : null;
488
+ const parentConfig = parentLowerTagName && (<Element>this.currentNode)[PropertySymbol.namespaceURI] === NamespaceURI.html
489
+ ? HTMLElementConfig[parentLowerTagName] : null;
476
490
 
477
491
  if (previousCurrentNode === this.currentNode) {
478
492
  throw new Error(
@@ -601,7 +615,8 @@ export default class HTMLParser {
601
615
  // Raw text elements (e.g. <script>) should be appended after the raw text has been added as content to the element.
602
616
  // <html>, <head> and <body> are special elements with context constraints. They are already available in the document.
603
617
  if (
604
- (!config || config.contentModel !== HTMLElementConfigContentModelEnum.rawText) &&
618
+ (!config || (config.contentModel !== HTMLElementConfigContentModelEnum.rawText &&
619
+ config.contentModel !== HTMLElementConfigContentModelEnum.rcdata)) &&
605
620
  this.nextElement !== documentElement &&
606
621
  this.nextElement !== head &&
607
622
  this.nextElement !== body
@@ -797,6 +812,11 @@ export default class HTMLParser {
797
812
 
798
813
  // Plain text elements such as <script> and <style> should only contain text.
799
814
  // Plain text elements should not decode entities. See #1564.
815
+ if (upperTagName === 'TEXTAREA' || upperTagName === 'TITLE') {
816
+ text = XMLEncodeUtility.decodeHTMLEntities(text.replace(/\r\n?/g, '\n').replace(/\0/g, '\uFFFD'));
817
+ // The tree builder ignores one leading LF in a textarea start tag.
818
+ if (upperTagName === 'TEXTAREA' && text.startsWith('\n')) text = text.slice(1);
819
+ }
800
820
  this.currentNode![PropertySymbol.appendChild](this.rootDocument!.createTextNode(text), true);
801
821
 
802
822
  const rawTextElement = this.currentNode;
@@ -46,7 +46,7 @@ const SELECTOR_GROUP_REGEXP = /(\\[0-9a-fA-F]{1,6}\s?|\\[\s\S])|(\s*[\s,+>~]\s*)
46
46
  * Group 23: Pseudo element (e.g. "::after", "::-webkit-inner-spin-button").
47
47
  */
48
48
  const SELECTOR_REGEXP =
49
- /(\*)|((?:[a-zA-Z0-9\u00A0-\uFFFF-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|#(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|\.(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\]|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\s*([~|^$*]{0,1})\s*=\s*("((?:[^"\\]|\\[\s\S])*)"|'((?:[^'\\]|\\[\s\S])*)')\s*(s|i){0,1}\]|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\s*([~|^$*]{0,1})\s*=\s*(([a-zA-Z0-9\u00A0-\uFFFF_¤£-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\]|:([a-zA-Z-]+)\s*\(.+\)|:([a-zA-Z-]+)|::([a-zA-Z-]+)/g;
49
+ /(\*)|((?:[a-zA-Z0-9\u00A0-\uFFFF_-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|#(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|\.(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\]|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\s*([~|^$*]{0,1})\s*=\s*("((?:[^"\\]|\\[\s\S])*)"|'((?:[^'\\]|\\[\s\S])*)')\s*(s|i){0,1}\]|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\s*([~|^$*]{0,1})\s*=\s*(([a-zA-Z0-9\u00A0-\uFFFF_¤£-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\]|:([a-zA-Z-]+)\s*\(.+\)|:([a-zA-Z-]+)|::([a-zA-Z-]+)/g;
50
50
 
51
51
  /**
52
52
  * Selector pseudo RegExp.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dr. John (醫者小智)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+ # Buninu Land renderer
2
+
3
+ The renderer and its runtime dependencies are copied **unchanged** from
4
+ `buninu-land` commit `3a42a5e315b351aa1e70459133df50541d5368e3` (MIT).
5
+
6
+ Browser integration belongs in `lib/paint/direct-canvas.ts`, not in these
7
+ vendored files. That adapter presents browser RGBA layers as Land surfaces;
8
+ the compositor's channel-symmetric arithmetic preserves RGBA when supplied
9
+ with an opaque RGBA background surface. No red/blue conversion is needed.
10
+
11
+ `direct-compositor.ts` is the shared composition engine, including placement,
12
+ cropping, scaling, opacity detection, geometry/word caches and occlusion.
13
+ `renderer.ts` and its encoding/transport dependencies are included intact;
14
+ browser screenshots retain the browser's existing CDP/encoding owner.