web-a2e 1.0.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 (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,551 @@
1
+ /*
2
+ * merlin-highlighting.js - Merlin assembler syntax highlighting
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ import { escapeHtml } from "./string-utils.js";
9
+
10
+ // 6502/65C02 mnemonics grouped by category (matching disassembler colour scheme)
11
+ export const BRANCH_MNEMONICS = new Set([
12
+ 'JMP', 'JSR', 'BCC', 'BCS', 'BEQ', 'BMI', 'BNE', 'BPL', 'BRA', 'BVC',
13
+ 'BVS', 'RTS', 'RTI', 'BRK',
14
+ 'BBR0', 'BBR1', 'BBR2', 'BBR3', 'BBR4', 'BBR5', 'BBR6', 'BBR7',
15
+ 'BBS0', 'BBS1', 'BBS2', 'BBS3', 'BBS4', 'BBS5', 'BBS6', 'BBS7',
16
+ ]);
17
+
18
+ export const LOAD_MNEMONICS = new Set([
19
+ 'LDA', 'LDX', 'LDY', 'STA', 'STX', 'STY', 'STZ',
20
+ ]);
21
+
22
+ export const MATH_MNEMONICS = new Set([
23
+ 'ADC', 'SBC', 'AND', 'ORA', 'EOR', 'ASL', 'LSR', 'ROL', 'ROR',
24
+ 'INC', 'DEC', 'INA', 'DEA', 'INX', 'DEX', 'INY', 'DEY',
25
+ 'CMP', 'CPX', 'CPY', 'BIT', 'TRB', 'TSB',
26
+ 'RMB0', 'RMB1', 'RMB2', 'RMB3', 'RMB4', 'RMB5', 'RMB6', 'RMB7',
27
+ 'SMB0', 'SMB1', 'SMB2', 'SMB3', 'SMB4', 'SMB5', 'SMB6', 'SMB7',
28
+ ]);
29
+
30
+ export const STACK_MNEMONICS = new Set([
31
+ 'PHA', 'PHP', 'PHX', 'PHY', 'PLA', 'PLP', 'PLX', 'PLY',
32
+ 'TAX', 'TAY', 'TSX', 'TXA', 'TXS', 'TYA',
33
+ ]);
34
+
35
+ export const FLAG_MNEMONICS = new Set([
36
+ 'CLC', 'CLD', 'CLI', 'CLV', 'SEC', 'SED', 'SEI', 'NOP', 'WAI', 'STP',
37
+ ]);
38
+
39
+ // All mnemonics combined for detection
40
+ export const ALL_MNEMONICS = new Set([
41
+ ...BRANCH_MNEMONICS, ...LOAD_MNEMONICS, ...MATH_MNEMONICS,
42
+ ...STACK_MNEMONICS, ...FLAG_MNEMONICS,
43
+ ]);
44
+
45
+ // Merlin directives (pseudo-ops)
46
+ export const DIRECTIVES = new Set([
47
+ 'ORG', 'EQU', 'DS', 'DFB', 'DW', 'DA', 'DDB', 'ASC', 'DCI', 'HEX',
48
+ 'PUT', 'USE', 'OBJ', 'LST', 'DO', 'ELSE', 'FIN', 'LUP', '--^', 'REL',
49
+ 'TYP', 'SAV', 'DSK', 'CHN', 'ENT', 'EXT', 'DUM', 'DEND', 'ERR', 'CYC',
50
+ 'DAT', 'EXP', 'PAU', 'SW', 'USR', 'XC', 'MX', 'TR', 'KBD', 'PMC',
51
+ 'PAG', 'TTL', 'SKP', 'CHK', 'IF', 'ELUP', 'END', 'MAC', 'EOM', '<<<',
52
+ 'ADR', 'ADRL', 'DB', 'LNK', 'STR', 'STRL', 'REV',
53
+ ]);
54
+
55
+ // Combined set for detection heuristic
56
+ const OPCODE_SET = new Set([...ALL_MNEMONICS, ...DIRECTIVES]);
57
+
58
+ /**
59
+ * Get CSS class for a mnemonic based on its category
60
+ */
61
+ export function getMnemonicClass(mnemonic) {
62
+ const upper = mnemonic.toUpperCase();
63
+ if (BRANCH_MNEMONICS.has(upper)) return 'mer-branch';
64
+ if (LOAD_MNEMONICS.has(upper)) return 'mer-load';
65
+ if (MATH_MNEMONICS.has(upper)) return 'mer-math';
66
+ if (STACK_MNEMONICS.has(upper)) return 'mer-stack';
67
+ if (FLAG_MNEMONICS.has(upper)) return 'mer-flag';
68
+ return 'mer-macro';
69
+ }
70
+
71
+ /**
72
+ * Detect if text content appears to be Merlin assembler source.
73
+ * Scans first ~20 non-empty lines looking for recognised mnemonics/directives
74
+ * in the opcode column position.
75
+ * @param {string} text - Plain text (high bits already stripped, CRs converted)
76
+ * @returns {boolean}
77
+ */
78
+ export function isMerlinSource(text) {
79
+ const lines = text.split('\n');
80
+ let hits = 0;
81
+ let checked = 0;
82
+
83
+ for (const line of lines) {
84
+ if (checked >= 20) break;
85
+
86
+ const trimmed = line.trim();
87
+ if (!trimmed) continue;
88
+
89
+ checked++;
90
+
91
+ // Full-line comments are consistent with Merlin but don't count as hits
92
+ if (trimmed[0] === '*' || trimmed[0] === ';') continue;
93
+
94
+ // Parse opcode column: either first token (if line starts with whitespace)
95
+ // or second token (if line starts with a label)
96
+ const tokens = trimmed.split(/\s+/);
97
+ let opcode = null;
98
+
99
+ if (/^\s/.test(line)) {
100
+ // Line starts with whitespace: first token is the opcode
101
+ opcode = tokens[0];
102
+ } else if (tokens.length >= 2) {
103
+ // Line starts with a label: second token is the opcode
104
+ opcode = tokens[1];
105
+ }
106
+
107
+ if (opcode && OPCODE_SET.has(opcode.toUpperCase())) {
108
+ hits++;
109
+ }
110
+ }
111
+
112
+ return hits >= 3;
113
+ }
114
+
115
+ /**
116
+ * Highlight Merlin assembler source code with column-aligned output.
117
+ * @param {string} text - Plain text source (high bits stripped, CRs converted to newlines)
118
+ * @returns {string} HTML with syntax highlighting spans
119
+ */
120
+ export function highlightMerlinSource(text) {
121
+ const lines = text.split('\n');
122
+ const highlighted = [];
123
+
124
+ for (const line of lines) {
125
+ highlighted.push(highlightMerlinLine(line));
126
+ }
127
+
128
+ return highlighted.join('');
129
+ }
130
+
131
+ /**
132
+ * Highlight a single Merlin source line, rendered as a flex row with
133
+ * fixed-width columns for label, opcode, operand, and comment.
134
+ * @param {string} line
135
+ * @returns {string} HTML
136
+ */
137
+ function highlightMerlinLine(line) {
138
+ if (!line) return '<span class="mer-line"></span>';
139
+
140
+ const trimmed = line.trim();
141
+ if (!trimmed) return '<span class="mer-line"></span>';
142
+
143
+ // Full-line comment: starts with * or ;
144
+ if (trimmed[0] === '*' || trimmed[0] === ';') {
145
+ return `<span class="mer-line mer-comment-line"><span class="mer-comment">${escapeHtml(trimmed)}</span></span>`;
146
+ }
147
+
148
+ // Parse into columns: LABEL OPCODE OPERAND ;COMMENT
149
+ const hasLabel = !/^\s/.test(line);
150
+ const parts = splitMerlinColumns(line, hasLabel);
151
+
152
+ // Build the four column spans
153
+ const labelHtml = renderLabel(parts.label);
154
+ const opcodeHtml = renderOpcode(parts.opcode);
155
+ const operandHtml = renderOperandCol(parts.operand);
156
+ const commentHtml = parts.comment !== null
157
+ ? `<span class="mer-col mer-col-comment"><span class="mer-comment">${escapeHtml(parts.comment)}</span></span>`
158
+ : '';
159
+
160
+ return `<span class="mer-line">${labelHtml}${opcodeHtml}${operandHtml}${commentHtml}</span>`;
161
+ }
162
+
163
+ /**
164
+ * Render the label column
165
+ */
166
+ function renderLabel(label) {
167
+ if (label === null) {
168
+ return '<span class="mer-col mer-col-label"></span>';
169
+ }
170
+ const cls = isLocalLabel(label) ? 'mer-local' : 'mer-label';
171
+ return `<span class="mer-col mer-col-label"><span class="${cls}">${escapeHtml(label)}</span></span>`;
172
+ }
173
+
174
+ /**
175
+ * Render the opcode column with category-based colouring
176
+ */
177
+ function renderOpcode(opcode) {
178
+ if (opcode === null) {
179
+ return '<span class="mer-col mer-col-opcode"></span>';
180
+ }
181
+ const upper = opcode.toUpperCase();
182
+ let cls;
183
+ if (ALL_MNEMONICS.has(upper)) {
184
+ cls = getMnemonicClass(opcode);
185
+ } else if (DIRECTIVES.has(upper)) {
186
+ cls = 'mer-directive';
187
+ } else {
188
+ cls = 'mer-macro';
189
+ }
190
+ return `<span class="mer-col mer-col-opcode"><span class="${cls}">${escapeHtml(opcode)}</span></span>`;
191
+ }
192
+
193
+ /**
194
+ * Render the operand column with sub-token highlighting
195
+ */
196
+ function renderOperandCol(operand) {
197
+ if (operand === null) {
198
+ return '<span class="mer-col mer-col-operand"></span>';
199
+ }
200
+ return `<span class="mer-col mer-col-operand">${highlightOperand(operand)}</span>`;
201
+ }
202
+
203
+ /**
204
+ * Split a Merlin source line into its column components.
205
+ * @param {string} line - The source line
206
+ * @param {boolean} hasLabel - Whether the line starts with a label
207
+ * @returns {Object} Column parts
208
+ */
209
+ function splitMerlinColumns(line, hasLabel) {
210
+ const result = {
211
+ label: null,
212
+ opcode: null,
213
+ operand: null,
214
+ comment: null,
215
+ };
216
+
217
+ let pos = 0;
218
+
219
+ if (hasLabel) {
220
+ // Extract label (runs until whitespace)
221
+ const labelMatch = line.match(/^(\S+)/);
222
+ if (labelMatch) {
223
+ result.label = labelMatch[1];
224
+ pos = labelMatch[1].length;
225
+ }
226
+ }
227
+
228
+ // Skip whitespace to reach opcode
229
+ const spaceMatch = line.substring(pos).match(/^(\s+)/);
230
+ if (spaceMatch) {
231
+ pos += spaceMatch[1].length;
232
+ }
233
+
234
+ if (pos >= line.length) return result;
235
+
236
+ // Extract opcode
237
+ const opcodeMatch = line.substring(pos).match(/^(\S+)/);
238
+ if (opcodeMatch) {
239
+ result.opcode = opcodeMatch[1];
240
+ pos += opcodeMatch[1].length;
241
+ }
242
+
243
+ // Skip whitespace to reach operand
244
+ const afterOpcodeSpace = line.substring(pos).match(/^(\s+)/);
245
+ if (afterOpcodeSpace) {
246
+ pos += afterOpcodeSpace[1].length;
247
+ }
248
+
249
+ if (pos >= line.length) return result;
250
+
251
+ // The rest is operand + possibly inline comment
252
+ const rest = line.substring(pos);
253
+ const { operand, comment } = splitOperandAndComment(rest);
254
+
255
+ result.operand = operand;
256
+ result.comment = comment;
257
+
258
+ return result;
259
+ }
260
+
261
+ /**
262
+ * Split the remainder of a line into operand and inline comment.
263
+ * Respects string delimiters so semicolons inside strings aren't treated as comments.
264
+ * @param {string} rest
265
+ * @returns {Object} { operand, comment }
266
+ */
267
+ function splitOperandAndComment(rest) {
268
+ let inSingleQuote = false;
269
+ let inDoubleQuote = false;
270
+
271
+ for (let i = 0; i < rest.length; i++) {
272
+ const ch = rest[i];
273
+
274
+ if (ch === "'" && !inDoubleQuote) {
275
+ inSingleQuote = !inSingleQuote;
276
+ } else if (ch === '"' && !inSingleQuote) {
277
+ inDoubleQuote = !inDoubleQuote;
278
+ } else if (ch === ';' && !inSingleQuote && !inDoubleQuote) {
279
+ const before = rest.substring(0, i).trimEnd();
280
+ return {
281
+ operand: before || null,
282
+ comment: rest.substring(i),
283
+ };
284
+ }
285
+ }
286
+
287
+ return {
288
+ operand: rest.trimEnd() || null,
289
+ comment: null,
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Check if a label is a Merlin local label (starts with : or ])
295
+ * @param {string} label
296
+ * @returns {boolean}
297
+ */
298
+ function isLocalLabel(label) {
299
+ return label[0] === ':' || label[0] === ']';
300
+ }
301
+
302
+ /**
303
+ * Highlight an operand field, identifying strings, numbers, label refs, and symbols.
304
+ * @param {string} operand
305
+ * @returns {string} HTML
306
+ */
307
+ function highlightOperand(operand) {
308
+ let html = '';
309
+ let i = 0;
310
+
311
+ while (i < operand.length) {
312
+ const ch = operand[i];
313
+
314
+ // String literal (single or double quoted)
315
+ if (ch === "'" || ch === '"') {
316
+ const quote = ch;
317
+ let j = i + 1;
318
+ while (j < operand.length && operand[j] !== quote) {
319
+ j++;
320
+ }
321
+ if (j < operand.length) j++; // include closing quote
322
+ html += `<span class="mer-string">${escapeHtml(operand.substring(i, j))}</span>`;
323
+ i = j;
324
+ continue;
325
+ }
326
+
327
+ // Hex number: $xx...
328
+ if (ch === '$') {
329
+ let j = i + 1;
330
+ while (j < operand.length && /[0-9A-Fa-f]/.test(operand[j])) {
331
+ j++;
332
+ }
333
+ if (j > i + 1) {
334
+ html += `<span class="mer-number">${escapeHtml(operand.substring(i, j))}</span>`;
335
+ i = j;
336
+ continue;
337
+ }
338
+ }
339
+
340
+ // Binary number: %01...
341
+ if (ch === '%') {
342
+ let j = i + 1;
343
+ while (j < operand.length && /[01]/.test(operand[j])) {
344
+ j++;
345
+ }
346
+ if (j > i + 1) {
347
+ html += `<span class="mer-number">${escapeHtml(operand.substring(i, j))}</span>`;
348
+ i = j;
349
+ continue;
350
+ }
351
+ }
352
+
353
+ // Decimal number
354
+ if (/[0-9]/.test(ch)) {
355
+ let j = i;
356
+ while (j < operand.length && /[0-9]/.test(operand[j])) {
357
+ j++;
358
+ }
359
+ html += `<span class="mer-number">${escapeHtml(operand.substring(i, j))}</span>`;
360
+ i = j;
361
+ continue;
362
+ }
363
+
364
+ // Local label reference (: or ] prefix)
365
+ if (ch === ':' || ch === ']') {
366
+ let j = i + 1;
367
+ while (j < operand.length && /[A-Za-z0-9_]/.test(operand[j])) {
368
+ j++;
369
+ }
370
+ if (j > i + 1) {
371
+ html += `<span class="mer-local">${escapeHtml(operand.substring(i, j))}</span>`;
372
+ i = j;
373
+ continue;
374
+ }
375
+ }
376
+
377
+ // Immediate prefix (#)
378
+ if (ch === '#') {
379
+ html += `<span class="mer-immediate">${escapeHtml(ch)}</span>`;
380
+ i++;
381
+ continue;
382
+ }
383
+
384
+ // Label / symbol reference (identifier)
385
+ if (/[A-Za-z_]/.test(ch)) {
386
+ let j = i + 1;
387
+ while (j < operand.length && /[A-Za-z0-9_.]/.test(operand[j])) {
388
+ j++;
389
+ }
390
+ html += `<span class="mer-symbol">${escapeHtml(operand.substring(i, j))}</span>`;
391
+ i = j;
392
+ continue;
393
+ }
394
+
395
+ // Operators and punctuation (, + - * / < > ^ | & ( ))
396
+ if (',+-*/<>^|&()'.includes(ch)) {
397
+ html += `<span class="mer-punct">${escapeHtml(ch)}</span>`;
398
+ i++;
399
+ continue;
400
+ }
401
+
402
+ // Anything else
403
+ html += escapeHtml(ch);
404
+ i++;
405
+ }
406
+
407
+ return html;
408
+ }
409
+
410
+ /**
411
+ * Highlight Merlin assembler source code for inline editor overlay.
412
+ * Unlike highlightMerlinSource() which uses flex columns, this version
413
+ * produces inline spans that preserve original whitespace exactly,
414
+ * ensuring pixel-perfect alignment with a textarea overlay.
415
+ * @param {string} text - Source text
416
+ * @returns {string} HTML with syntax highlighting spans
417
+ */
418
+ export function highlightMerlinSourceInline(text) {
419
+ const lines = text.split('\n');
420
+ const highlighted = [];
421
+
422
+ for (const line of lines) {
423
+ highlighted.push(highlightMerlinLineInline(line));
424
+ }
425
+
426
+ return highlighted.join('\n');
427
+ }
428
+
429
+ /**
430
+ * Highlight a single line inline (preserving exact whitespace).
431
+ * @param {string} line
432
+ * @returns {string} HTML
433
+ */
434
+ function highlightMerlinLineInline(line) {
435
+ if (!line) return '';
436
+
437
+ const trimmed = line.trim();
438
+ if (!trimmed) return escapeHtml(line);
439
+
440
+ // Full-line comment
441
+ if (trimmed[0] === '*' || trimmed[0] === ';') {
442
+ return `<span class="mer-comment">${escapeHtml(line)}</span>`;
443
+ }
444
+
445
+ // Parse into columns, preserving whitespace positions
446
+ const hasLabel = !/^\s/.test(line);
447
+ let pos = 0;
448
+ let result = '';
449
+
450
+ // Leading whitespace
451
+ while (pos < line.length && (line[pos] === ' ' || line[pos] === '\t')) {
452
+ result += line[pos];
453
+ pos++;
454
+ }
455
+
456
+ // Label (if present)
457
+ if (hasLabel && pos < line.length) {
458
+ const labelStart = pos;
459
+ while (pos < line.length && line[pos] !== ' ' && line[pos] !== '\t') pos++;
460
+ const label = line.substring(labelStart, pos);
461
+ const cls = isLocalLabel(label) ? 'mer-local' : 'mer-label';
462
+ result += `<span class="${cls}">${escapeHtml(label)}</span>`;
463
+ }
464
+
465
+ // Whitespace between label and opcode
466
+ while (pos < line.length && (line[pos] === ' ' || line[pos] === '\t')) {
467
+ result += line[pos];
468
+ pos++;
469
+ }
470
+
471
+ if (pos >= line.length) return result;
472
+
473
+ // Check for inline comment at opcode position
474
+ if (line[pos] === ';' || line[pos] === '*') {
475
+ result += `<span class="mer-comment">${escapeHtml(line.substring(pos))}</span>`;
476
+ return result;
477
+ }
478
+
479
+ // Opcode
480
+ const opcodeStart = pos;
481
+ while (pos < line.length && line[pos] !== ' ' && line[pos] !== '\t') pos++;
482
+ const opcode = line.substring(opcodeStart, pos);
483
+ const upper = opcode.toUpperCase();
484
+
485
+ let opcCls;
486
+ if (ALL_MNEMONICS.has(upper)) {
487
+ opcCls = getMnemonicClass(opcode);
488
+ } else if (DIRECTIVES.has(upper)) {
489
+ opcCls = 'mer-directive';
490
+ } else {
491
+ opcCls = 'mer-macro';
492
+ }
493
+ result += `<span class="${opcCls}">${escapeHtml(opcode)}</span>`;
494
+
495
+ // Whitespace between opcode and operand
496
+ while (pos < line.length && (line[pos] === ' ' || line[pos] === '\t')) {
497
+ result += line[pos];
498
+ pos++;
499
+ }
500
+
501
+ if (pos >= line.length) return result;
502
+
503
+ // Rest of line: operand + possible comment
504
+ const rest = line.substring(pos);
505
+ const { operand, comment, commentOffset } = splitOperandAndCommentWithOffset(rest);
506
+
507
+ if (operand) {
508
+ result += highlightOperand(operand);
509
+ }
510
+
511
+ if (comment !== null) {
512
+ // Add any whitespace between operand end and comment start
513
+ const operandLen = operand ? operand.length : 0;
514
+ const gap = rest.substring(operandLen, commentOffset);
515
+ result += escapeHtml(gap);
516
+ result += `<span class="mer-comment">${escapeHtml(comment)}</span>`;
517
+ }
518
+
519
+ return result;
520
+ }
521
+
522
+ /**
523
+ * Split operand and comment, also returning the comment's offset in the rest string.
524
+ */
525
+ function splitOperandAndCommentWithOffset(rest) {
526
+ let inSingleQuote = false;
527
+ let inDoubleQuote = false;
528
+
529
+ for (let i = 0; i < rest.length; i++) {
530
+ const ch = rest[i];
531
+
532
+ if (ch === "'" && !inDoubleQuote) {
533
+ inSingleQuote = !inSingleQuote;
534
+ } else if (ch === '"' && !inSingleQuote) {
535
+ inDoubleQuote = !inDoubleQuote;
536
+ } else if (ch === ';' && !inSingleQuote && !inDoubleQuote) {
537
+ const before = rest.substring(0, i).trimEnd();
538
+ return {
539
+ operand: before || null,
540
+ comment: rest.substring(i),
541
+ commentOffset: i,
542
+ };
543
+ }
544
+ }
545
+
546
+ return {
547
+ operand: rest.trimEnd() || null,
548
+ comment: null,
549
+ commentOffset: rest.length,
550
+ };
551
+ }
@@ -0,0 +1,125 @@
1
+ /*
2
+ * storage.js - Local storage utilities
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * Safely get an item from localStorage
10
+ * @param {string} key - The storage key
11
+ * @param {*} defaultValue - Default value if key doesn't exist or on error
12
+ * @returns {string|null} The stored value or defaultValue
13
+ */
14
+ export function getItem(key, defaultValue = null) {
15
+ try {
16
+ const value = localStorage.getItem(key);
17
+ return value !== null ? value : defaultValue;
18
+ } catch (e) {
19
+ // localStorage may be unavailable (private browsing, disabled, etc.)
20
+ return defaultValue;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Safely set an item in localStorage
26
+ * @param {string} key - The storage key
27
+ * @param {string} value - The value to store
28
+ * @returns {boolean} True if successful, false otherwise
29
+ */
30
+ export function setItem(key, value) {
31
+ try {
32
+ localStorage.setItem(key, value);
33
+ return true;
34
+ } catch (e) {
35
+ // localStorage may be unavailable or quota exceeded
36
+ return false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Safely remove an item from localStorage
42
+ * @param {string} key - The storage key
43
+ * @returns {boolean} True if successful, false otherwise
44
+ */
45
+ export function removeItem(key) {
46
+ try {
47
+ localStorage.removeItem(key);
48
+ return true;
49
+ } catch (e) {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Safely get and parse a JSON item from localStorage
56
+ * @param {string} key - The storage key
57
+ * @param {*} defaultValue - Default value if key doesn't exist, on error, or invalid JSON
58
+ * @returns {*} The parsed value or defaultValue
59
+ */
60
+ export function getJSON(key, defaultValue = null) {
61
+ try {
62
+ const value = localStorage.getItem(key);
63
+ if (value === null) return defaultValue;
64
+ return JSON.parse(value);
65
+ } catch (e) {
66
+ // localStorage unavailable or invalid JSON
67
+ return defaultValue;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Safely stringify and set a JSON item in localStorage
73
+ * @param {string} key - The storage key
74
+ * @param {*} value - The value to store (will be JSON.stringify'd)
75
+ * @returns {boolean} True if successful, false otherwise
76
+ */
77
+ export function setJSON(key, value) {
78
+ try {
79
+ localStorage.setItem(key, JSON.stringify(value));
80
+ return true;
81
+ } catch (e) {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Safely get a numeric value from localStorage
88
+ * @param {string} key - The storage key
89
+ * @param {number} defaultValue - Default value if key doesn't exist, on error, or not a valid number
90
+ * @param {object} options - Optional constraints: { min, max }
91
+ * @returns {number} The numeric value or defaultValue
92
+ */
93
+ export function getNumber(key, defaultValue, options = {}) {
94
+ try {
95
+ const value = localStorage.getItem(key);
96
+ if (value === null) return defaultValue;
97
+
98
+ const num = parseFloat(value);
99
+ if (isNaN(num)) return defaultValue;
100
+
101
+ // Apply constraints if provided
102
+ if (options.min !== undefined && num < options.min) return defaultValue;
103
+ if (options.max !== undefined && num > options.max) return defaultValue;
104
+
105
+ return num;
106
+ } catch (e) {
107
+ return defaultValue;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Safely get a boolean value from localStorage
113
+ * @param {string} key - The storage key
114
+ * @param {boolean} defaultValue - Default value if key doesn't exist or on error
115
+ * @returns {boolean} The boolean value or defaultValue
116
+ */
117
+ export function getBoolean(key, defaultValue) {
118
+ try {
119
+ const value = localStorage.getItem(key);
120
+ if (value === null) return defaultValue;
121
+ return value === 'true';
122
+ } catch (e) {
123
+ return defaultValue;
124
+ }
125
+ }
@@ -0,0 +1,19 @@
1
+ /*
2
+ * string-utils.js - String utility functions
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * Escape HTML special characters to prevent XSS
10
+ * @param {string} text - The text to escape
11
+ * @returns {string} - The escaped text safe for HTML insertion
12
+ */
13
+ export function escapeHtml(text) {
14
+ return text
15
+ .replace(/&/g, "&amp;")
16
+ .replace(/</g, "&lt;")
17
+ .replace(/>/g, "&gt;")
18
+ .replace(/"/g, "&quot;");
19
+ }