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,473 @@
1
+ /*
2
+ * basic-highlighting.js - BASIC syntax highlighting with smart formatting
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ import { escapeHtml } from "./string-utils.js";
9
+ import { APPLESOFT_TOKENS } from "./basic-tokens.js";
10
+
11
+ // BASIC keyword categories for syntax highlighting
12
+ // Colors follow the Apple II rainbow theme (Green, Yellow, Orange, Red, Purple, Blue)
13
+ export const BASIC_CATEGORIES = {
14
+ // RED - Flow control that changes execution path
15
+ flow: [
16
+ "GOTO", "GOSUB", "RETURN", "IF", "THEN", "ON", "ONERR", "RESUME",
17
+ "END", "STOP", "RUN",
18
+ ],
19
+ // PURPLE - Loop constructs
20
+ loop: ["FOR", "TO", "STEP", "NEXT"],
21
+ // BLUE - Input/Output operations
22
+ io: ["PRINT", "INPUT", "GET", "DATA", "READ", "RESTORE"],
23
+ // GREEN - Graphics and display
24
+ graphics: [
25
+ "GR", "HGR", "HGR2", "TEXT", "PLOT", "HPLOT", "HLIN", "VLIN",
26
+ "COLOR=", "HCOLOR=", "DRAW", "XDRAW", "ROT=", "SCALE=", "SCRN(",
27
+ "HOME", "HTAB", "VTAB", "NORMAL", "INVERSE", "FLASH",
28
+ ],
29
+ // ORANGE - Memory and system
30
+ memory: ["PEEK", "POKE", "CALL", "HIMEM:", "LOMEM:", "USR", "DEF", "FN"],
31
+ // CYAN - Built-in functions
32
+ functions: [
33
+ "SGN", "INT", "ABS", "SQR", "RND", "LOG", "EXP", "COS", "SIN",
34
+ "TAN", "ATN", "LEN", "ASC", "VAL", "STR$", "CHR$", "LEFT$",
35
+ "RIGHT$", "MID$", "FRE", "PDL", "POS", "TAB(", "SPC(",
36
+ ],
37
+ // YELLOW - Variable declarations
38
+ variable: ["DIM", "LET", "DEL", "NEW", "CLR", "CLEAR"],
39
+ // MUTED - Miscellaneous
40
+ misc: [
41
+ "REM", "LOAD", "SAVE", "SHLOAD", "STORE", "RECALL", "PR#", "IN#",
42
+ "WAIT", "CONT", "LIST", "TRACE", "NOTRACE", "SPEED=", "POP",
43
+ "NOT", "AND", "OR", "&",
44
+ ],
45
+ };
46
+
47
+ // Keywords that increase indentation level
48
+ const INDENT_INCREASE = new Set(["FOR", "GOSUB"]);
49
+ // Keywords that decrease indentation level (before the line)
50
+ const INDENT_DECREASE = new Set(["NEXT", "RETURN"]);
51
+
52
+ // Build a set of all keywords for quick lookup
53
+ const ALL_KEYWORDS = new Set();
54
+ for (const category of Object.values(BASIC_CATEGORIES)) {
55
+ for (const keyword of category) {
56
+ ALL_KEYWORDS.add(keyword);
57
+ }
58
+ }
59
+
60
+ // Also add any tokens not in categories
61
+ for (const token of APPLESOFT_TOKENS) {
62
+ if (token && token.length > 0) {
63
+ ALL_KEYWORDS.add(token);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Get CSS class for a BASIC keyword
69
+ * @param {string} keyword - The keyword (uppercase)
70
+ * @returns {string} CSS class name
71
+ */
72
+ export function getBasicKeywordClass(keyword) {
73
+ const kw = keyword.trim().toUpperCase();
74
+ if (BASIC_CATEGORIES.flow.includes(kw)) return "bas-flow";
75
+ if (BASIC_CATEGORIES.loop.includes(kw)) return "bas-loop";
76
+ if (BASIC_CATEGORIES.io.includes(kw)) return "bas-io";
77
+ if (BASIC_CATEGORIES.graphics.includes(kw)) return "bas-graphics";
78
+ if (BASIC_CATEGORIES.memory.includes(kw)) return "bas-memory";
79
+ if (BASIC_CATEGORIES.functions.includes(kw)) return "bas-func";
80
+ if (BASIC_CATEGORIES.variable.includes(kw)) return "bas-var";
81
+ if (BASIC_CATEGORIES.misc.includes(kw)) return "bas-misc";
82
+ return "bas-keyword";
83
+ }
84
+
85
+ // Sort keywords by length (longest first) for matching
86
+ const SORTED_KEYWORDS = Array.from(ALL_KEYWORDS)
87
+ .filter(k => k.length > 1 || /[&]/.test(k)) // Include & but not single operators
88
+ .sort((a, b) => b.length - a.length);
89
+
90
+ /**
91
+ * Calculate the indentation level for each line based on control structures
92
+ * Handles multiple FOR/NEXT on the same line (e.g., FOR A=1 TO 2:FOR B=1 TO 2)
93
+ * @param {string[]} lines - Array of BASIC lines
94
+ * @returns {number[]} Array of indentation levels (0-8)
95
+ */
96
+ function calculateIndentLevels(lines) {
97
+ const levels = [];
98
+ let currentLevel = 0;
99
+
100
+ for (const line of lines) {
101
+ const upper = line.toUpperCase();
102
+
103
+ // Count all FOR and NEXT occurrences on this line
104
+ const forMatches = upper.match(/\bFOR\b/g);
105
+ const nextMatches = upper.match(/\bNEXT\b/g);
106
+ const forCount = forMatches ? forMatches.length : 0;
107
+ const nextCount = nextMatches ? nextMatches.length : 0;
108
+
109
+ // Decrease level for each NEXT (before rendering the line)
110
+ // Only decrease by NEXTs not paired with FORs on this same line
111
+ const unpaired = Math.max(0, nextCount - forCount);
112
+ if (unpaired > 0) {
113
+ currentLevel = Math.max(0, currentLevel - unpaired);
114
+ }
115
+
116
+ levels.push(Math.min(currentLevel, 8)); // Cap at 8 levels
117
+
118
+ // Increase level for each FOR not closed by a NEXT on the same line
119
+ const netOpen = Math.max(0, forCount - nextCount);
120
+ if (netOpen > 0) {
121
+ currentLevel += netOpen;
122
+ }
123
+ }
124
+
125
+ return levels;
126
+ }
127
+
128
+ /**
129
+ * Highlight BASIC source code (text input, not tokenized)
130
+ * @param {string} source - BASIC source code
131
+ * @param {Object} options - Options
132
+ * @param {boolean} options.preserveCase - Keep original case (default: false, converts to uppercase)
133
+ * @returns {string} HTML with syntax highlighting spans
134
+ */
135
+ export function highlightBasicSource(source, options = {}) {
136
+ const { preserveCase = false } = options;
137
+ const lines = source.split(/\r?\n/);
138
+ const highlightedLines = [];
139
+
140
+ // Calculate indent levels for all lines
141
+ const indentLevels = calculateIndentLevels(lines);
142
+
143
+ for (let i = 0; i < lines.length; i++) {
144
+ const highlighted = highlightBasicLine(lines[i], preserveCase);
145
+ const indentClass = indentLevels[i] > 0 ? ` indent-${indentLevels[i]}` : "";
146
+ // We don't wrap here - the wrapper will add indentation class if needed
147
+ highlightedLines.push({ html: highlighted, indent: indentLevels[i] });
148
+ }
149
+
150
+ // Return just the HTML - caller will handle indent classes
151
+ return highlightedLines.map(l => l.html).join("\n");
152
+ }
153
+
154
+ /**
155
+ * Highlight BASIC source with indent information
156
+ * @param {string} source - BASIC source code
157
+ * @param {Object} options - Options
158
+ * @param {boolean} options.preserveCase - Keep original case (default: false)
159
+ * @returns {{html: string, indent: number}[]} Array of highlighted lines with indent levels
160
+ */
161
+ export function highlightBasicSourceWithIndent(source, options = {}) {
162
+ const { preserveCase = false } = options;
163
+ const lines = source.split(/\r?\n/);
164
+ const indentLevels = calculateIndentLevels(lines);
165
+ const result = [];
166
+
167
+ for (let i = 0; i < lines.length; i++) {
168
+ result.push({
169
+ html: highlightBasicLine(lines[i], preserveCase),
170
+ indent: indentLevels[i],
171
+ });
172
+ }
173
+
174
+ return result;
175
+ }
176
+
177
+ /**
178
+ * Auto-format BASIC source code
179
+ * - Sorts lines by line number (so changing a line number moves it to correct position)
180
+ * - Right-aligns line numbers to consistent width
181
+ * - Adds indentation for control structures (FOR/NEXT loops)
182
+ * @param {string} source - BASIC source code
183
+ * @returns {string} Formatted source code
184
+ */
185
+ export function formatBasicSource(source) {
186
+ const rawLines = source.split(/\r?\n/);
187
+
188
+ // Parse lines and extract line numbers for sorting
189
+ const parsedLines = [];
190
+ for (const line of rawLines) {
191
+ const trimmed = line.trim();
192
+ if (!trimmed) continue; // Skip empty lines
193
+
194
+ const match = trimmed.match(/^(\d+)\s*(.*)/);
195
+ if (match) {
196
+ parsedLines.push({
197
+ lineNumber: parseInt(match[1], 10),
198
+ lineNumStr: match[1],
199
+ code: match[2] || "",
200
+ });
201
+ } else {
202
+ // Lines without line numbers are kept but won't be sorted
203
+ // (unusual in BASIC but handle gracefully)
204
+ parsedLines.push({
205
+ lineNumber: -1, // Sort to top
206
+ lineNumStr: null,
207
+ code: trimmed,
208
+ });
209
+ }
210
+ }
211
+
212
+ // Sort by line number (lines without numbers stay at top)
213
+ parsedLines.sort((a, b) => a.lineNumber - b.lineNumber);
214
+
215
+ // Rebuild lines array for indent calculation
216
+ const sortedLines = parsedLines.map((p) =>
217
+ p.lineNumStr ? `${p.lineNumStr} ${p.code}` : p.code
218
+ );
219
+
220
+ // Calculate indentation on sorted lines
221
+ const indentLevels = calculateIndentLevels(sortedLines);
222
+
223
+ // Find max line number width for alignment
224
+ let maxLineNumWidth = 0;
225
+ for (const p of parsedLines) {
226
+ if (p.lineNumStr) {
227
+ maxLineNumWidth = Math.max(maxLineNumWidth, p.lineNumStr.length);
228
+ }
229
+ }
230
+
231
+ // Format each line
232
+ const formattedLines = [];
233
+ for (let i = 0; i < sortedLines.length; i++) {
234
+ formattedLines.push(formatBasicLine(sortedLines[i], maxLineNumWidth, indentLevels[i]));
235
+ }
236
+
237
+ return formattedLines.join("\n");
238
+ }
239
+
240
+ // Indent string constant (2 spaces per level)
241
+ const INDENT_CHARS = " ";
242
+
243
+ /**
244
+ * Convert BASIC code to uppercase while preserving string contents
245
+ * @param {string} code - BASIC code
246
+ * @returns {string} Code with keywords/variables uppercase, strings preserved
247
+ */
248
+ function toUppercasePreservingStrings(code) {
249
+ let result = "";
250
+ let inString = false;
251
+
252
+ for (let i = 0; i < code.length; i++) {
253
+ const char = code[i];
254
+
255
+ if (char === '"') {
256
+ inString = !inString;
257
+ result += char;
258
+ } else if (inString) {
259
+ // Inside string - preserve case
260
+ result += char;
261
+ } else {
262
+ // Outside string - convert to uppercase
263
+ result += char.toUpperCase();
264
+ }
265
+ }
266
+
267
+ return result;
268
+ }
269
+
270
+ /**
271
+ * Format a single line of BASIC source
272
+ * @param {string} line - Single line of BASIC
273
+ * @param {number} maxLineNumWidth - Width to pad line numbers to
274
+ * @param {number} indentLevel - Indentation level (0-8)
275
+ * @returns {string} Formatted line
276
+ */
277
+ function formatBasicLine(line, maxLineNumWidth, indentLevel) {
278
+ const trimmed = line.trim();
279
+ if (!trimmed) {
280
+ return "";
281
+ }
282
+
283
+ // Parse line number and code
284
+ const match = trimmed.match(/^(\d+)\s*(.*)/);
285
+
286
+ if (match) {
287
+ const lineNum = match[1];
288
+ const code = toUppercasePreservingStrings(match[2]);
289
+
290
+ // Right-align line number by left-padding with spaces
291
+ const padding = " ".repeat(Math.max(0, maxLineNumWidth - lineNum.length));
292
+
293
+ // Add indentation
294
+ const indent = indentLevel > 0 ? INDENT_CHARS.repeat(indentLevel) : "";
295
+
296
+ return `${padding}${lineNum} ${indent}${code}`;
297
+ }
298
+
299
+ // No line number - just return trimmed with indent (also uppercase)
300
+ const indent = indentLevel > 0 ? INDENT_CHARS.repeat(indentLevel) : "";
301
+ return `${indent}${toUppercasePreservingStrings(trimmed)}`;
302
+ }
303
+
304
+ /**
305
+ * Highlight a single line of BASIC source
306
+ * @param {string} line - Single line of BASIC
307
+ * @param {boolean} preserveCase - Keep original case
308
+ * @returns {string} HTML with syntax highlighting
309
+ */
310
+ function highlightBasicLine(line, preserveCase) {
311
+ if (!line.trim()) {
312
+ return escapeHtml(line);
313
+ }
314
+
315
+ const upper = line.toUpperCase();
316
+ let result = "";
317
+ let i = 0;
318
+
319
+ // Check for line number at start
320
+ const lineNumMatch = upper.match(/^(\s*)(\d+)(\s*)/);
321
+ if (lineNumMatch) {
322
+ result += escapeHtml(lineNumMatch[1]); // Leading whitespace
323
+ result += `<span class="bas-linenum">${lineNumMatch[2]}</span>`;
324
+ result += escapeHtml(lineNumMatch[3]); // Trailing whitespace
325
+ i = lineNumMatch[0].length;
326
+ }
327
+
328
+ let inString = false;
329
+ let inRem = false;
330
+ let stringStart = -1;
331
+ let remStart = -1;
332
+
333
+ while (i < line.length) {
334
+ const char = line[i];
335
+ const upperChar = upper[i];
336
+
337
+ // Handle REM - rest of line is comment
338
+ if (inRem) {
339
+ if (remStart === -1) remStart = i;
340
+ i++;
341
+ continue;
342
+ }
343
+
344
+ // Handle strings
345
+ if (char === '"') {
346
+ if (inString) {
347
+ // End of string
348
+ const str = line.substring(stringStart, i + 1);
349
+ result += `<span class="bas-string">${escapeHtml(str)}</span>`;
350
+ inString = false;
351
+ stringStart = -1;
352
+ i++;
353
+ continue;
354
+ } else {
355
+ // Start of string
356
+ inString = true;
357
+ stringStart = i;
358
+ i++;
359
+ continue;
360
+ }
361
+ }
362
+
363
+ if (inString) {
364
+ i++;
365
+ continue;
366
+ }
367
+
368
+ // Try to match a keyword
369
+ const remaining = upper.substring(i);
370
+ let matched = false;
371
+
372
+ for (const keyword of SORTED_KEYWORDS) {
373
+ if (remaining.startsWith(keyword)) {
374
+ // Check it's not part of a longer identifier
375
+ const nextChar = remaining[keyword.length];
376
+ const isKeywordEnd = !nextChar || !/[A-Z0-9]/.test(nextChar);
377
+ const isSpecial = /[=(:&]$/.test(keyword) || keyword === "&";
378
+
379
+ if (isKeywordEnd || isSpecial) {
380
+ const originalCase = preserveCase ? line.substring(i, i + keyword.length) : keyword;
381
+ const kwClass = getBasicKeywordClass(keyword);
382
+ result += `<span class="${kwClass}">${escapeHtml(originalCase)}</span>`;
383
+ i += keyword.length;
384
+ matched = true;
385
+
386
+ // Check for REM
387
+ if (keyword === "REM") {
388
+ inRem = true;
389
+ remStart = i;
390
+ }
391
+ break;
392
+ }
393
+ }
394
+ }
395
+
396
+ if (matched) continue;
397
+
398
+ // Check for numbers
399
+ if (/[0-9]/.test(upperChar)) {
400
+ let num = char;
401
+ let j = i + 1;
402
+ while (j < line.length && /[0-9.]/.test(line[j])) {
403
+ num += line[j];
404
+ j++;
405
+ }
406
+ // Check for E notation
407
+ if (j < line.length && /[Ee]/.test(line[j])) {
408
+ num += line[j];
409
+ j++;
410
+ if (j < line.length && /[+-]/.test(line[j])) {
411
+ num += line[j];
412
+ j++;
413
+ }
414
+ while (j < line.length && /[0-9]/.test(line[j])) {
415
+ num += line[j];
416
+ j++;
417
+ }
418
+ }
419
+ result += `<span class="bas-number">${escapeHtml(num)}</span>`;
420
+ i = j;
421
+ continue;
422
+ }
423
+
424
+ // Check for variable names
425
+ if (/[A-Za-z]/.test(char)) {
426
+ let varName = char;
427
+ let j = i + 1;
428
+ while (j < line.length && /[A-Za-z0-9]/.test(line[j])) {
429
+ varName += line[j];
430
+ j++;
431
+ }
432
+ // Check for type suffix
433
+ if (j < line.length && /[$%]/.test(line[j])) {
434
+ varName += line[j];
435
+ j++;
436
+ }
437
+ const displayName = preserveCase ? varName : varName.toUpperCase();
438
+ result += `<span class="bas-variable">${escapeHtml(displayName)}</span>`;
439
+ i = j;
440
+ continue;
441
+ }
442
+
443
+ // Check for operators
444
+ if ("+-*/^=<>".includes(char)) {
445
+ result += `<span class="bas-operator">${escapeHtml(char)}</span>`;
446
+ i++;
447
+ continue;
448
+ }
449
+
450
+ // Check for punctuation
451
+ if ("(),;:".includes(char)) {
452
+ result += `<span class="bas-punct">${escapeHtml(char)}</span>`;
453
+ i++;
454
+ continue;
455
+ }
456
+
457
+ // Default: just output the character
458
+ result += escapeHtml(char);
459
+ i++;
460
+ }
461
+
462
+ // Flush remaining content
463
+ if (inString && stringStart !== -1) {
464
+ const str = line.substring(stringStart);
465
+ result += `<span class="bas-string">${escapeHtml(str)}</span>`;
466
+ }
467
+ if (inRem && remStart !== -1) {
468
+ const rem = line.substring(remStart);
469
+ result += `<span class="bas-comment">${escapeHtml(rem)}</span>`;
470
+ }
471
+
472
+ return result;
473
+ }
@@ -0,0 +1,153 @@
1
+ /*
2
+ * basic-tokenizer.js - Applesoft BASIC tokenizer for direct memory insertion
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ import { APPLESOFT_TOKENS } from './basic-tokens.js';
9
+
10
+ // Build a lookup from keyword string to token byte value.
11
+ // Sort by length descending for greedy longest-match.
12
+ const KEYWORD_LIST = APPLESOFT_TOKENS
13
+ .map((kw, i) => ({ keyword: kw, token: 0x80 + i }))
14
+ .sort((a, b) => b.keyword.length - a.keyword.length);
15
+
16
+ /**
17
+ * Tokenize a single BASIC line's content (without the line number).
18
+ * @param {string} text - The line content (e.g. "PRINT \"HELLO\":GOTO 10")
19
+ * @returns {Uint8Array} Tokenized bytes
20
+ */
21
+ export function tokenizeLine(text) {
22
+ const bytes = [];
23
+ const upper = text.toUpperCase();
24
+ let i = 0;
25
+ let inRem = false;
26
+ let inData = false;
27
+ let inQuote = false;
28
+
29
+ while (i < upper.length) {
30
+ const ch = upper[i];
31
+
32
+ // Inside a quoted string - emit as-is until closing quote
33
+ if (inQuote) {
34
+ bytes.push(text.charCodeAt(i));
35
+ if (ch === '"') {
36
+ inQuote = false;
37
+ }
38
+ i++;
39
+ continue;
40
+ }
41
+
42
+ // After REM token - emit rest of line as raw ASCII
43
+ if (inRem) {
44
+ bytes.push(text.charCodeAt(i));
45
+ i++;
46
+ continue;
47
+ }
48
+
49
+ // After DATA token - emit as-is until colon
50
+ if (inData) {
51
+ if (ch === ':') {
52
+ inData = false;
53
+ // Fall through to normal processing for the colon
54
+ } else {
55
+ bytes.push(text.charCodeAt(i));
56
+ i++;
57
+ continue;
58
+ }
59
+ }
60
+
61
+ // Opening quote
62
+ if (ch === '"') {
63
+ inQuote = true;
64
+ bytes.push(text.charCodeAt(i));
65
+ i++;
66
+ continue;
67
+ }
68
+
69
+ // ? is shorthand for PRINT
70
+ if (ch === '?') {
71
+ bytes.push(0xBA); // PRINT token
72
+ i++;
73
+ continue;
74
+ }
75
+
76
+ // Try greedy longest-match against keywords
77
+ let matched = false;
78
+ const remaining = upper.substring(i);
79
+
80
+ for (const { keyword, token } of KEYWORD_LIST) {
81
+ if (remaining.startsWith(keyword)) {
82
+ bytes.push(token);
83
+ i += keyword.length;
84
+ matched = true;
85
+
86
+ if (token === 0xB2) { // REM
87
+ inRem = true;
88
+ } else if (token === 0x83) { // DATA
89
+ inData = true;
90
+ }
91
+ break;
92
+ }
93
+ }
94
+
95
+ if (!matched) {
96
+ // Emit character as ASCII byte
97
+ bytes.push(text.charCodeAt(i));
98
+ i++;
99
+ }
100
+ }
101
+
102
+ return new Uint8Array(bytes);
103
+ }
104
+
105
+ /**
106
+ * Build a full tokenized Applesoft BASIC program in memory format.
107
+ * @param {Array<{lineNumber: number, content: string}>} lines - Parsed program lines
108
+ * @param {number} [txttab=0x0801] - Starting address of the program
109
+ * @returns {{ bytes: Uint8Array, endAddr: number }}
110
+ */
111
+ export function tokenizeProgram(lines, txttab = 0x0801) {
112
+ // First pass: tokenize all lines and compute sizes
113
+ const tokenizedLines = lines.map(line => {
114
+ const tokenBytes = tokenizeLine(line.content);
115
+ // Each line: [next-ptr:2] [line-num:2] [token-bytes...] [0x00]
116
+ const lineSize = 2 + 2 + tokenBytes.length + 1;
117
+ return { lineNumber: line.lineNumber, tokenBytes, lineSize };
118
+ });
119
+
120
+ // Calculate total size: all lines + end marker (0x00, 0x00)
121
+ const totalSize = tokenizedLines.reduce((sum, l) => sum + l.lineSize, 0) + 2;
122
+ const bytes = new Uint8Array(totalSize);
123
+ let offset = 0;
124
+ let addr = txttab;
125
+
126
+ // Second pass: write bytes with correct next-pointers
127
+ for (const line of tokenizedLines) {
128
+ const nextAddr = addr + line.lineSize;
129
+
130
+ // Next-pointer (little-endian)
131
+ bytes[offset] = nextAddr & 0xFF;
132
+ bytes[offset + 1] = (nextAddr >> 8) & 0xFF;
133
+
134
+ // Line number (little-endian)
135
+ bytes[offset + 2] = line.lineNumber & 0xFF;
136
+ bytes[offset + 3] = (line.lineNumber >> 8) & 0xFF;
137
+
138
+ // Token bytes
139
+ bytes.set(line.tokenBytes, offset + 4);
140
+
141
+ // Line terminator
142
+ bytes[offset + 4 + line.tokenBytes.length] = 0x00;
143
+
144
+ offset += line.lineSize;
145
+ addr = nextAddr;
146
+ }
147
+
148
+ // End-of-program marker
149
+ bytes[offset] = 0x00;
150
+ bytes[offset + 1] = 0x00;
151
+
152
+ return { bytes, endAddr: addr + 2 };
153
+ }