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,266 @@
1
+ /*
2
+ * dos33.js - DOS 3.3 filesystem parser
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * DOS 3.3 Filesystem Parser
10
+ * Parses DOS 3.3 disk images to read catalog and file contents
11
+ */
12
+
13
+ import { parseDOS33Filename } from './utils.js';
14
+
15
+ // DOS 3.3 Constants
16
+ const TRACKS = 35;
17
+ const SECTORS_PER_TRACK = 16;
18
+ const BYTES_PER_SECTOR = 256;
19
+ const DISK_SIZE = TRACKS * SECTORS_PER_TRACK * BYTES_PER_SECTOR; // 143,360 bytes
20
+
21
+ // VTOC Location
22
+ const VTOC_TRACK = 17;
23
+ const VTOC_SECTOR = 0;
24
+
25
+ // Catalog structure
26
+ const CATALOG_ENTRY_SIZE = 35;
27
+ const ENTRIES_PER_SECTOR = 7;
28
+
29
+ // File types
30
+ const FILE_TYPES = {
31
+ 0x00: { name: 'T', description: 'Text' },
32
+ 0x01: { name: 'I', description: 'Integer BASIC' },
33
+ 0x02: { name: 'A', description: 'Applesoft BASIC' },
34
+ 0x04: { name: 'B', description: 'Binary' },
35
+ 0x08: { name: 'S', description: 'Type S' },
36
+ 0x10: { name: 'R', description: 'Relocatable' },
37
+ 0x20: { name: 'a', description: 'Type a' },
38
+ 0x40: { name: 'b', description: 'Type b' },
39
+ };
40
+
41
+ /**
42
+ * Calculate sector offset in disk image
43
+ * @param {number} track - Track number (0-34)
44
+ * @param {number} sector - Sector number (0-15)
45
+ * @returns {number} Byte offset in disk image
46
+ */
47
+ function getSectorOffset(track, sector) {
48
+ return (track * SECTORS_PER_TRACK + sector) * BYTES_PER_SECTOR;
49
+ }
50
+
51
+ /**
52
+ * Read a sector from disk data
53
+ * @param {Uint8Array} diskData - Raw disk image data
54
+ * @param {number} track - Track number
55
+ * @param {number} sector - Sector number
56
+ * @returns {Uint8Array} 256-byte sector data
57
+ */
58
+ function readSector(diskData, track, sector) {
59
+ const offset = getSectorOffset(track, sector);
60
+ return diskData.slice(offset, offset + BYTES_PER_SECTOR);
61
+ }
62
+
63
+ /**
64
+ * Parse the VTOC (Volume Table of Contents)
65
+ * @param {Uint8Array} diskData - Raw disk image data
66
+ * @returns {Object|null} VTOC data or null if invalid
67
+ */
68
+ export function parseVTOC(diskData) {
69
+ if (diskData.length < DISK_SIZE) {
70
+ return null;
71
+ }
72
+
73
+ const vtoc = readSector(diskData, VTOC_TRACK, VTOC_SECTOR);
74
+
75
+ // Validate DOS 3.3 signature
76
+ const catalogTrack = vtoc[0x01];
77
+ const catalogSector = vtoc[0x02];
78
+ const dosVersion = vtoc[0x03];
79
+
80
+ // Basic validation
81
+ if (catalogTrack !== 0x11 || dosVersion !== 0x03) {
82
+ return null; // Not a DOS 3.3 disk
83
+ }
84
+
85
+ return {
86
+ catalogTrack,
87
+ catalogSector,
88
+ dosVersion,
89
+ volumeNumber: vtoc[0x06],
90
+ maxTrackSectorPairs: vtoc[0x27],
91
+ lastTrackAllocated: vtoc[0x30],
92
+ tracksPerDisk: vtoc[0x34] || 35,
93
+ sectorsPerTrack: vtoc[0x35] || 16,
94
+ bytesPerSector: (vtoc[0x37] << 8) | vtoc[0x36] || 256,
95
+ };
96
+ }
97
+
98
+
99
+ /**
100
+ * Parse a catalog entry
101
+ * @param {Uint8Array} entry - 35-byte catalog entry
102
+ * @returns {Object|null} Parsed entry or null if empty/deleted
103
+ */
104
+ function parseCatalogEntry(entry) {
105
+ const firstTrack = entry[0x00];
106
+ const firstSector = entry[0x01];
107
+
108
+ // Check for deleted or empty entry
109
+ if (firstTrack === 0xFF || firstTrack === 0x00) {
110
+ return null;
111
+ }
112
+
113
+ const typeAndFlags = entry[0x02];
114
+ const fileType = typeAndFlags & 0x7F;
115
+ const isLocked = (typeAndFlags & 0x80) !== 0;
116
+
117
+ const filenameBytes = entry.slice(0x03, 0x03 + 30);
118
+ const filename = parseDOS33Filename(filenameBytes);
119
+
120
+ const sectorCount = entry[0x21] | (entry[0x22] << 8);
121
+
122
+ return {
123
+ firstTrack,
124
+ firstSector,
125
+ fileType,
126
+ fileTypeName: FILE_TYPES[fileType]?.name || '?',
127
+ fileTypeDescription: FILE_TYPES[fileType]?.description || 'Unknown',
128
+ isLocked,
129
+ filename,
130
+ sectorCount,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Read the disk catalog
136
+ * @param {Uint8Array} diskData - Raw disk image data
137
+ * @returns {Array} Array of catalog entries
138
+ */
139
+ export function readCatalog(diskData) {
140
+ const vtoc = parseVTOC(diskData);
141
+ if (!vtoc) {
142
+ return [];
143
+ }
144
+
145
+ const entries = [];
146
+ let track = vtoc.catalogTrack;
147
+ let sector = vtoc.catalogSector;
148
+ const visited = new Set();
149
+
150
+ // Follow catalog sector chain
151
+ while (track !== 0 && sector !== 0) {
152
+ const key = `${track},${sector}`;
153
+ if (visited.has(key)) break; // Prevent infinite loops
154
+ visited.add(key);
155
+
156
+ const catalogSector = readSector(diskData, track, sector);
157
+
158
+ // Parse entries in this sector (skip first 11 bytes - header)
159
+ for (let i = 0; i < ENTRIES_PER_SECTOR; i++) {
160
+ const entryOffset = 0x0B + (i * CATALOG_ENTRY_SIZE);
161
+ const entryData = catalogSector.slice(entryOffset, entryOffset + CATALOG_ENTRY_SIZE);
162
+ const entry = parseCatalogEntry(entryData);
163
+ if (entry) {
164
+ entries.push(entry);
165
+ }
166
+ }
167
+
168
+ // Get next catalog sector
169
+ track = catalogSector[0x01];
170
+ sector = catalogSector[0x02];
171
+ }
172
+
173
+ return entries;
174
+ }
175
+
176
+ /**
177
+ * Read a file's track/sector list
178
+ * @param {Uint8Array} diskData - Raw disk image data
179
+ * @param {number} track - First T/S list track
180
+ * @param {number} sector - First T/S list sector
181
+ * @returns {Array} Array of {track, sector} pairs
182
+ */
183
+ function readTrackSectorList(diskData, track, sector) {
184
+ const pairs = [];
185
+ const visited = new Set();
186
+
187
+ while (track !== 0) {
188
+ const key = `${track},${sector}`;
189
+ if (visited.has(key)) break;
190
+ visited.add(key);
191
+
192
+ const tsList = readSector(diskData, track, sector);
193
+
194
+ // Read sector pairs from this T/S list (starting at offset 0x0C)
195
+ for (let i = 0x0C; i < 0x100; i += 2) {
196
+ const t = tsList[i];
197
+ const s = tsList[i + 1];
198
+ if (t === 0 && s === 0) break;
199
+ pairs.push({ track: t, sector: s });
200
+ }
201
+
202
+ // Get next T/S list sector
203
+ track = tsList[0x01];
204
+ sector = tsList[0x02];
205
+ }
206
+
207
+ return pairs;
208
+ }
209
+
210
+ /**
211
+ * Read file contents
212
+ * @param {Uint8Array} diskData - Raw disk image data
213
+ * @param {Object} catalogEntry - Catalog entry for the file
214
+ * @returns {Uint8Array} File contents
215
+ */
216
+ export function readFile(diskData, catalogEntry) {
217
+ const sectors = readTrackSectorList(
218
+ diskData,
219
+ catalogEntry.firstTrack,
220
+ catalogEntry.firstSector
221
+ );
222
+
223
+ // Concatenate all data sectors
224
+ const chunks = [];
225
+ for (const { track, sector } of sectors) {
226
+ const sectorData = readSector(diskData, track, sector);
227
+ chunks.push(sectorData);
228
+ }
229
+
230
+ // Combine into single array
231
+ const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
232
+ const result = new Uint8Array(totalSize);
233
+ let offset = 0;
234
+ for (const chunk of chunks) {
235
+ result.set(chunk, offset);
236
+ offset += chunk.length;
237
+ }
238
+
239
+ return result;
240
+ }
241
+
242
+ /**
243
+ * Get file size information for Binary files
244
+ * Binary files have a 4-byte header: 2 bytes address, 2 bytes length
245
+ * @param {Uint8Array} fileData - Raw file data
246
+ * @returns {Object} {address, length} or null
247
+ */
248
+ export function getBinaryFileInfo(fileData) {
249
+ if (fileData.length < 4) return null;
250
+
251
+ const address = fileData[0] | (fileData[1] << 8);
252
+ const length = fileData[2] | (fileData[3] << 8);
253
+
254
+ return { address, length };
255
+ }
256
+
257
+ /**
258
+ * Check if disk is DOS 3.3 format
259
+ * @param {Uint8Array} diskData - Raw disk image data
260
+ * @returns {boolean} True if DOS 3.3 format
261
+ */
262
+ export function isDOS33(diskData) {
263
+ return parseVTOC(diskData) !== null;
264
+ }
265
+
266
+ export { FILE_TYPES, BYTES_PER_SECTOR };
@@ -0,0 +1,359 @@
1
+ /*
2
+ * file-viewer.js - File content viewer for disk images
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * File Viewer - Formats and displays file contents
10
+ * Supports different Apple II file types
11
+ */
12
+
13
+ import { disassemble } from "./disassembler.js";
14
+ import { escapeHtml } from "../utils/string-utils.js";
15
+ import { highlightBasicSource } from "../utils/basic-highlighting.js";
16
+ import { highlightMerlinSource, isMerlinSource } from "../utils/merlin-highlighting.js";
17
+
18
+ let wasmModule = null;
19
+
20
+ /**
21
+ * Set the WASM module for BASIC detokenization
22
+ * @param {Object} wasm - The WASM module instance
23
+ */
24
+ export function setFileViewerWasm(wasm) {
25
+ wasmModule = wasm;
26
+ }
27
+
28
+ /**
29
+ * Detokenize BASIC program using WASM and apply JS syntax highlighting.
30
+ * Returns { html, lineNumToIndex, lineCount }
31
+ */
32
+ function detokenizeBasicViaWasm(data, hasLengthHeader, isApplesoft) {
33
+ if (!wasmModule) {
34
+ return { html: '(WASM module not loaded)', lineNumToIndex: new Map(), lineCount: 0 };
35
+ }
36
+
37
+ const wasm = wasmModule;
38
+
39
+ // Copy data to WASM heap
40
+ const dataPtr = wasm._malloc(data.length);
41
+ wasm.HEAPU8.set(data, dataPtr);
42
+
43
+ // Call WASM detokenizer
44
+ const resultPtr = isApplesoft
45
+ ? wasm._detokenizeApplesoft(dataPtr, data.length, hasLengthHeader)
46
+ : wasm._detokenizeIntegerBasic(dataPtr, data.length, hasLengthHeader);
47
+
48
+ wasm._free(dataPtr);
49
+
50
+ // Read plain text result
51
+ const plainText = resultPtr ? wasm.UTF8ToString(resultPtr) : '';
52
+ if (!plainText) {
53
+ return { html: '', lineNumToIndex: new Map(), lineCount: 0 };
54
+ }
55
+
56
+ // Build lineNumToIndex from the plain text (line numbers are at start of each line)
57
+ const lineNumToIndex = new Map();
58
+ const textLines = plainText.split('\n');
59
+ for (let i = 0; i < textLines.length; i++) {
60
+ const match = textLines[i].match(/^\s*(\d+)/);
61
+ if (match) {
62
+ lineNumToIndex.set(parseInt(match[1], 10), i);
63
+ }
64
+ }
65
+
66
+ // Apply HTML syntax highlighting
67
+ const highlighted = highlightBasicSource(plainText, { preserveCase: false });
68
+
69
+ // Post-process: add bas-lineref data attributes for GOTO/GOSUB/THEN targets
70
+ // The highlighter wraps numbers in <span class="bas-number"> spans.
71
+ // We look for numbers that follow GOTO/GOSUB/THEN keywords and add lineref attributes.
72
+ const html = highlighted.replace(
73
+ /(<span class="bas-(?:flow|misc)">(?:GOTO|GOSUB|THEN)<\/span>\s*)<span class="bas-number">(\d+)<\/span>/g,
74
+ (match, prefix, num) => {
75
+ const lineNum = parseInt(num, 10);
76
+ if (lineNumToIndex.has(lineNum)) {
77
+ return `${prefix}<span class="bas-number bas-lineref" data-target-line="${lineNum}">${num}</span>`;
78
+ }
79
+ return match;
80
+ }
81
+ );
82
+
83
+ return {
84
+ html,
85
+ lineNumToIndex,
86
+ lineCount: textLines.length,
87
+ };
88
+ }
89
+
90
+ function detokenizeApplesoft(data, hasLengthHeader = true) {
91
+ return detokenizeBasicViaWasm(data, hasLengthHeader, true);
92
+ }
93
+
94
+ function detokenizeIntegerBasic(data, hasLengthHeader = true) {
95
+ return detokenizeBasicViaWasm(data, hasLengthHeader, false);
96
+ }
97
+
98
+ /**
99
+ * Format binary data as hex dump with ASCII (returns HTML)
100
+ * @param {Uint8Array} data - Binary data
101
+ * @param {number} baseAddress - Starting address for display
102
+ * @param {number} maxBytes - Maximum bytes to show (0 = all)
103
+ * @returns {string} Formatted hex dump as HTML
104
+ */
105
+ export function formatHexDump(data, baseAddress = 0, maxBytes = 0, bytesPerRow = 16) {
106
+ const lines = [];
107
+ const bytesToShow =
108
+ maxBytes > 0 ? Math.min(data.length, maxBytes) : data.length;
109
+ const groupSize = 8;
110
+
111
+ for (let i = 0; i < bytesToShow; i += bytesPerRow) {
112
+ const addr = (baseAddress + i).toString(16).toUpperCase().padStart(4, "0");
113
+
114
+ let hexBytes = "";
115
+ let ascii = "";
116
+
117
+ for (let j = 0; j < bytesPerRow; j++) {
118
+ // Add group gap every 8 bytes (not at start)
119
+ if (j > 0 && j % groupSize === 0) {
120
+ hexBytes += `<span class="hex-group-gap"></span>`;
121
+ }
122
+
123
+ if (i + j < bytesToShow) {
124
+ const byte = data[i + j];
125
+ const hexStr = byte.toString(16).toUpperCase().padStart(2, "0");
126
+
127
+ // Style based on byte value
128
+ let byteClass = "hex-byte";
129
+ if (byte === 0x00) {
130
+ byteClass += " hex-zero";
131
+ } else if (byte >= 0x20 && byte < 0x7f) {
132
+ byteClass += " hex-printable";
133
+ } else if (byte >= 0x80) {
134
+ byteClass += " hex-highbit";
135
+ }
136
+
137
+ hexBytes += `<span class="${byteClass}">${hexStr}</span> `;
138
+
139
+ // ASCII representation (printable chars only)
140
+ const ch = byte & 0x7f;
141
+ if (ch >= 0x20 && ch < 0x7f) {
142
+ const escaped = ch === 0x26 ? "&amp;" : ch === 0x3c ? "&lt;" : ch === 0x3e ? "&gt;" : String.fromCharCode(ch);
143
+ ascii += `<span class="hex-ascii-printable">${escaped}</span>`;
144
+ } else {
145
+ ascii += `<span class="hex-ascii-dot">.</span>`;
146
+ }
147
+ } else {
148
+ hexBytes += " ";
149
+ ascii += " ";
150
+ }
151
+ }
152
+
153
+ lines.push(
154
+ `<span class="hex-line">` +
155
+ `<span class="hex-addr">${addr}</span>` +
156
+ `<span class="hex-separator">:</span> ` +
157
+ `<span class="hex-bytes">${hexBytes}</span>` +
158
+ `<span class="hex-ascii-separator">│</span>` +
159
+ `<span class="hex-ascii">${ascii}</span>` +
160
+ `<span class="hex-ascii-separator">│</span>` +
161
+ `</span>`
162
+ );
163
+ }
164
+
165
+ if (maxBytes > 0 && data.length > maxBytes) {
166
+ lines.push(`<span class="hex-truncated">... (${data.length - maxBytes} more bytes)</span>`);
167
+ }
168
+
169
+ return lines.join("\n");
170
+ }
171
+
172
+ /**
173
+ * Format text file (strip high bits, convert to readable text)
174
+ * @param {Uint8Array} data - Raw file data
175
+ * @returns {string} Formatted text
176
+ */
177
+ export function formatTextFile(data) {
178
+ let text = "";
179
+
180
+ for (let i = 0; i < data.length; i++) {
181
+ const byte = data[i] & 0x7f; // Strip high bit
182
+
183
+ if (byte === 0x0d) {
184
+ // Carriage return -> newline
185
+ text += "\n";
186
+ } else if (byte === 0x00) {
187
+ // Null - end of text or padding
188
+ continue;
189
+ } else if (byte >= 0x20 && byte < 0x7f) {
190
+ // Printable ASCII
191
+ text += String.fromCharCode(byte);
192
+ } else if (byte === 0x09) {
193
+ // Tab
194
+ text += "\t";
195
+ }
196
+ }
197
+
198
+ return text;
199
+ }
200
+
201
+ /**
202
+ * Format file contents based on type
203
+ * @param {Uint8Array} data - Raw file data
204
+ * @param {number} fileType - DOS 3.3 file type code
205
+ * @param {Object} options - Optional settings
206
+ * @param {boolean} options.hasLengthHeader - Whether BASIC files have 2-byte length header (DOS 3.3 = true, ProDOS = false)
207
+ * @returns {Object} {content, format} where format is 'text' or 'hex'
208
+ */
209
+ export function formatFileContents(data, fileType, options = {}) {
210
+ const { hasLengthHeader = true } = options;
211
+
212
+ switch (fileType) {
213
+ case 0x00: // Text
214
+ return {
215
+ content: formatTextFile(data),
216
+ format: "text",
217
+ description: "Text File",
218
+ };
219
+
220
+ case 0x02: {
221
+ // Applesoft BASIC
222
+ try {
223
+ const result = detokenizeApplesoft(data, hasLengthHeader);
224
+ return {
225
+ content: result.html,
226
+ format: "basic",
227
+ description: "Applesoft BASIC",
228
+ isHtml: true,
229
+ lineNumToIndex: result.lineNumToIndex,
230
+ lineCount: result.lineCount,
231
+ };
232
+ } catch (e) {
233
+ // Fall back to hex if detokenization fails
234
+ return {
235
+ content: formatHexDump(data),
236
+ format: "hex",
237
+ description: "Applesoft BASIC (raw)",
238
+ isHtml: true,
239
+ };
240
+ }
241
+ }
242
+
243
+ case 0x01: {
244
+ // Integer BASIC
245
+ try {
246
+ const result = detokenizeIntegerBasic(data, hasLengthHeader);
247
+ return {
248
+ content: result.html,
249
+ format: "basic",
250
+ description: "Integer BASIC",
251
+ isHtml: true,
252
+ lineNumToIndex: result.lineNumToIndex,
253
+ lineCount: result.lineCount,
254
+ };
255
+ } catch (e) {
256
+ // Fall back to hex if detokenization fails
257
+ return {
258
+ content: formatHexDump(data),
259
+ format: "hex",
260
+ description: "Integer BASIC (raw)",
261
+ isHtml: true,
262
+ };
263
+ }
264
+ }
265
+
266
+ case 0x04: {
267
+ // Binary - DOS 3.3 header: 2 bytes address, 2 bytes length
268
+ let description = "Binary File";
269
+
270
+ if (data.length >= 4) {
271
+ const address = data[0] | (data[1] << 8);
272
+ const length = data[2] | (data[3] << 8);
273
+ description = `Binary File - Load: $${address.toString(16).toUpperCase()}, Length: ${length} bytes`;
274
+ }
275
+
276
+ // Return a promise for async disassembly
277
+ return {
278
+ content: null, // Will be filled by async disassembly
279
+ format: "text",
280
+ description,
281
+ // Async loader for disassembly
282
+ loadAsync: async () => {
283
+ try {
284
+ return await disassemble(data);
285
+ } catch (e) {
286
+ // Fall back to hex dump if disassembly fails
287
+ const displayData = data.length >= 4 ? data.slice(4) : data;
288
+ const baseAddr = data.length >= 4 ? (data[0] | (data[1] << 8)) : 0;
289
+ return formatHexDump(displayData, baseAddr);
290
+ }
291
+ },
292
+ };
293
+ }
294
+
295
+ default:
296
+ return {
297
+ content: formatHexDump(data),
298
+ format: "hex",
299
+ description: "Unknown File Type",
300
+ isHtml: true,
301
+ };
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Format file size for display
307
+ * @param {number} sectors - Number of sectors
308
+ * @returns {string} Formatted size string
309
+ */
310
+ export function formatFileSize(sectors) {
311
+ const bytes = sectors * 256;
312
+ if (bytes < 1024) {
313
+ return `${bytes} bytes`;
314
+ }
315
+ return `${(bytes / 1024).toFixed(1)} KB`;
316
+ }
317
+
318
+ /**
319
+ * Decode raw Apple II bytes to plain text (strip high bits, CR -> newline)
320
+ * @param {Uint8Array} data - Raw file data
321
+ * @returns {string} Decoded text
322
+ */
323
+ function decodeAppleIIText(data) {
324
+ let text = '';
325
+ for (let i = 0; i < data.length; i++) {
326
+ const byte = data[i] & 0x7F;
327
+ if (byte === 0x0D) {
328
+ text += '\n';
329
+ } else if (byte === 0x00) {
330
+ continue;
331
+ } else if (byte >= 0x20 && byte < 0x7F) {
332
+ text += String.fromCharCode(byte);
333
+ } else if (byte === 0x09) {
334
+ text += '\t';
335
+ }
336
+ }
337
+ return text;
338
+ }
339
+
340
+ /**
341
+ * Check if raw file data looks like Merlin assembler source
342
+ * @param {Uint8Array} data - Raw file data (high-bit encoded)
343
+ * @returns {boolean}
344
+ */
345
+ export function checkIsMerlinFile(data) {
346
+ const text = decodeAppleIIText(data);
347
+ return isMerlinSource(text);
348
+ }
349
+
350
+ /**
351
+ * Format raw file data as highlighted Merlin assembler source
352
+ * @param {Uint8Array} data - Raw file data (high-bit encoded)
353
+ * @returns {Object} { content, format, isHtml }
354
+ */
355
+ export function formatMerlinFile(data) {
356
+ const text = decodeAppleIIText(data);
357
+ const html = highlightMerlinSource(text);
358
+ return { content: html, format: 'merlin', isHtml: true };
359
+ }