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.
- package/.clangd +5 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +362 -0
- package/CMakeLists.txt +774 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/build-wasm/generated/roms.cpp +2447 -0
- package/docker-compose.staging.yml +9 -0
- package/docs/basic-rom-disassembly.md +6663 -0
- package/docs/softswitch-comparison.md +273 -0
- package/docs/thunderclock-debug.md +89 -0
- package/examples/cube.bas +72 -0
- package/examples/hello.s +55 -0
- package/examples/scroll.s +140 -0
- package/package.json +18 -0
- package/public/assets/apple-logo-old.png +0 -0
- package/public/assets/apple-logo.png +0 -0
- package/public/assets/drive-closed-light-on.png +0 -0
- package/public/assets/drive-closed.png +0 -0
- package/public/assets/drive-open-light-on.png +0 -0
- package/public/assets/drive-open.png +0 -0
- package/public/audio-worklet.js +82 -0
- package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
- package/public/disks/ProDOS 2.4.3.po +0 -0
- package/public/disks/h32mb.2mg +0 -0
- package/public/disks/library.json +26 -0
- package/public/docs/llms/llm-assembler.txt +90 -0
- package/public/docs/llms/llm-basic-program.txt +256 -0
- package/public/docs/llms/llm-disk-drives.txt +72 -0
- package/public/docs/llms/llm-file-explorer.txt +50 -0
- package/public/docs/llms/llm-hard-drives.txt +80 -0
- package/public/docs/llms/llm-main.txt +51 -0
- package/public/docs/llms/llm-slot-configuration.txt +66 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/index.html +661 -0
- package/public/llms.txt +49 -0
- package/public/manifest.json +29 -0
- package/public/shaders/burnin.glsl +22 -0
- package/public/shaders/crt.glsl +706 -0
- package/public/shaders/edge.glsl +109 -0
- package/public/shaders/vertex.glsl +8 -0
- package/public/sw.js +186 -0
- package/roms/341-0027.bin +0 -0
- package/roms/341-0160-A-US-UK.bin +0 -0
- package/roms/341-0160-A.bin +0 -0
- package/roms/342-0273-A-US-UK.bin +0 -0
- package/roms/342-0349-B-C0-FF.bin +0 -0
- package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
- package/roms/Thunderclock Plus ROM.bin +0 -0
- package/scripts/generate_roms.sh +69 -0
- package/src/bindings/wasm_interface.cpp +1940 -0
- package/src/core/assembler/assembler.cpp +1239 -0
- package/src/core/assembler/assembler.hpp +115 -0
- package/src/core/audio/audio.cpp +160 -0
- package/src/core/audio/audio.hpp +81 -0
- package/src/core/basic/basic_detokenizer.cpp +436 -0
- package/src/core/basic/basic_detokenizer.hpp +41 -0
- package/src/core/basic/basic_tokenizer.cpp +286 -0
- package/src/core/basic/basic_tokenizer.hpp +26 -0
- package/src/core/basic/basic_tokens.hpp +295 -0
- package/src/core/cards/disk2_card.cpp +568 -0
- package/src/core/cards/disk2_card.hpp +316 -0
- package/src/core/cards/expansion_card.hpp +185 -0
- package/src/core/cards/mockingboard/ay8910.cpp +616 -0
- package/src/core/cards/mockingboard/ay8910.hpp +159 -0
- package/src/core/cards/mockingboard/via6522.cpp +530 -0
- package/src/core/cards/mockingboard/via6522.hpp +163 -0
- package/src/core/cards/mockingboard_card.cpp +312 -0
- package/src/core/cards/mockingboard_card.hpp +159 -0
- package/src/core/cards/mouse_card.cpp +654 -0
- package/src/core/cards/mouse_card.hpp +190 -0
- package/src/core/cards/smartport/block_device.cpp +202 -0
- package/src/core/cards/smartport/block_device.hpp +60 -0
- package/src/core/cards/smartport/smartport_card.cpp +603 -0
- package/src/core/cards/smartport/smartport_card.hpp +120 -0
- package/src/core/cards/thunderclock_card.cpp +237 -0
- package/src/core/cards/thunderclock_card.hpp +122 -0
- package/src/core/cpu/cpu6502.cpp +1609 -0
- package/src/core/cpu/cpu6502.hpp +203 -0
- package/src/core/debug/condition_evaluator.cpp +470 -0
- package/src/core/debug/condition_evaluator.hpp +87 -0
- package/src/core/disassembler/disassembler.cpp +552 -0
- package/src/core/disassembler/disassembler.hpp +171 -0
- package/src/core/disk-image/disk_image.hpp +267 -0
- package/src/core/disk-image/dsk_disk_image.cpp +827 -0
- package/src/core/disk-image/dsk_disk_image.hpp +204 -0
- package/src/core/disk-image/gcr_encoding.cpp +147 -0
- package/src/core/disk-image/gcr_encoding.hpp +78 -0
- package/src/core/disk-image/woz_disk_image.cpp +1049 -0
- package/src/core/disk-image/woz_disk_image.hpp +343 -0
- package/src/core/emulator.cpp +2126 -0
- package/src/core/emulator.hpp +434 -0
- package/src/core/filesystem/dos33.cpp +178 -0
- package/src/core/filesystem/dos33.hpp +66 -0
- package/src/core/filesystem/pascal.cpp +262 -0
- package/src/core/filesystem/pascal.hpp +87 -0
- package/src/core/filesystem/prodos.cpp +369 -0
- package/src/core/filesystem/prodos.hpp +119 -0
- package/src/core/input/keyboard.cpp +227 -0
- package/src/core/input/keyboard.hpp +111 -0
- package/src/core/mmu/mmu.cpp +1387 -0
- package/src/core/mmu/mmu.hpp +236 -0
- package/src/core/types.hpp +196 -0
- package/src/core/video/video.cpp +680 -0
- package/src/core/video/video.hpp +156 -0
- package/src/css/assembler-editor.css +1617 -0
- package/src/css/base.css +470 -0
- package/src/css/basic-debugger.css +791 -0
- package/src/css/basic-editor.css +792 -0
- package/src/css/controls.css +783 -0
- package/src/css/cpu-debugger.css +1413 -0
- package/src/css/debug-base.css +160 -0
- package/src/css/debug-windows.css +6455 -0
- package/src/css/disk-drives.css +406 -0
- package/src/css/documentation.css +392 -0
- package/src/css/file-explorer.css +867 -0
- package/src/css/hard-drive.css +180 -0
- package/src/css/layout.css +217 -0
- package/src/css/memory-windows.css +798 -0
- package/src/css/modals.css +510 -0
- package/src/css/monitor.css +425 -0
- package/src/css/release-notes.css +101 -0
- package/src/css/responsive.css +400 -0
- package/src/css/rule-builder.css +340 -0
- package/src/css/save-states.css +201 -0
- package/src/css/settings-windows.css +1231 -0
- package/src/css/window-switcher.css +150 -0
- package/src/js/agent/agent-manager.js +643 -0
- package/src/js/agent/agent-tools.js +293 -0
- package/src/js/agent/agent-version-tools.js +131 -0
- package/src/js/agent/assembler-tools.js +357 -0
- package/src/js/agent/basic-program-tools.js +894 -0
- package/src/js/agent/disk-tools.js +417 -0
- package/src/js/agent/file-explorer-tools.js +269 -0
- package/src/js/agent/index.js +13 -0
- package/src/js/agent/main-tools.js +222 -0
- package/src/js/agent/slot-tools.js +303 -0
- package/src/js/agent/smartport-tools.js +257 -0
- package/src/js/agent/window-tools.js +80 -0
- package/src/js/audio/audio-driver.js +417 -0
- package/src/js/audio/audio-worklet.js +85 -0
- package/src/js/audio/index.js +8 -0
- package/src/js/config/default-layout.js +34 -0
- package/src/js/config/version.js +8 -0
- package/src/js/data/apple2-rom-routines.js +577 -0
- package/src/js/debug/assembler-editor-window.js +2993 -0
- package/src/js/debug/basic-breakpoint-manager.js +529 -0
- package/src/js/debug/basic-program-parser.js +436 -0
- package/src/js/debug/basic-program-window.js +2594 -0
- package/src/js/debug/basic-variable-inspector.js +447 -0
- package/src/js/debug/breakpoint-manager.js +472 -0
- package/src/js/debug/cpu-debugger-window.js +2396 -0
- package/src/js/debug/index.js +22 -0
- package/src/js/debug/label-manager.js +238 -0
- package/src/js/debug/memory-browser-window.js +416 -0
- package/src/js/debug/memory-heat-map-window.js +481 -0
- package/src/js/debug/memory-map-window.js +206 -0
- package/src/js/debug/mockingboard-window.js +882 -0
- package/src/js/debug/mouse-card-window.js +355 -0
- package/src/js/debug/rule-builder-window.js +648 -0
- package/src/js/debug/soft-switch-window.js +458 -0
- package/src/js/debug/stack-viewer-window.js +221 -0
- package/src/js/debug/symbols.js +416 -0
- package/src/js/debug/trace-panel.js +291 -0
- package/src/js/debug/zero-page-watch-window.js +297 -0
- package/src/js/disk-manager/disk-drives-window.js +212 -0
- package/src/js/disk-manager/disk-operations.js +284 -0
- package/src/js/disk-manager/disk-persistence.js +301 -0
- package/src/js/disk-manager/disk-surface-renderer.js +388 -0
- package/src/js/disk-manager/drive-sounds.js +139 -0
- package/src/js/disk-manager/hard-drive-manager.js +481 -0
- package/src/js/disk-manager/hard-drive-persistence.js +187 -0
- package/src/js/disk-manager/hard-drive-window.js +57 -0
- package/src/js/disk-manager/index.js +890 -0
- package/src/js/display/display-settings-window.js +383 -0
- package/src/js/display/index.js +10 -0
- package/src/js/display/screen-window.js +342 -0
- package/src/js/display/webgl-renderer.js +705 -0
- package/src/js/file-explorer/disassembler.js +574 -0
- package/src/js/file-explorer/dos33.js +266 -0
- package/src/js/file-explorer/file-viewer.js +359 -0
- package/src/js/file-explorer/index.js +1261 -0
- package/src/js/file-explorer/prodos.js +549 -0
- package/src/js/file-explorer/utils.js +67 -0
- package/src/js/help/documentation-window.js +1096 -0
- package/src/js/help/index.js +10 -0
- package/src/js/help/release-notes-window.js +85 -0
- package/src/js/help/release-notes.js +612 -0
- package/src/js/input/gamepad-handler.js +176 -0
- package/src/js/input/index.js +12 -0
- package/src/js/input/input-handler.js +396 -0
- package/src/js/input/joystick-window.js +404 -0
- package/src/js/input/mouse-handler.js +99 -0
- package/src/js/input/text-selection.js +462 -0
- package/src/js/main.js +653 -0
- package/src/js/state/index.js +15 -0
- package/src/js/state/save-states-window.js +393 -0
- package/src/js/state/state-manager.js +409 -0
- package/src/js/state/state-persistence.js +218 -0
- package/src/js/ui/confirm.js +43 -0
- package/src/js/ui/disk-drive-positioner.js +347 -0
- package/src/js/ui/reminder-controller.js +129 -0
- package/src/js/ui/slot-configuration-window.js +560 -0
- package/src/js/ui/theme-manager.js +61 -0
- package/src/js/ui/toast.js +44 -0
- package/src/js/ui/ui-controller.js +897 -0
- package/src/js/ui/window-switcher.js +275 -0
- package/src/js/utils/basic-autocomplete.js +832 -0
- package/src/js/utils/basic-highlighting.js +473 -0
- package/src/js/utils/basic-tokenizer.js +153 -0
- package/src/js/utils/basic-tokens.js +117 -0
- package/src/js/utils/constants.js +28 -0
- package/src/js/utils/indexeddb-helper.js +225 -0
- package/src/js/utils/merlin-editor-support.js +905 -0
- package/src/js/utils/merlin-highlighting.js +551 -0
- package/src/js/utils/storage.js +125 -0
- package/src/js/utils/string-utils.js +19 -0
- package/src/js/utils/wasm-memory.js +54 -0
- package/src/js/windows/base-window.js +690 -0
- package/src/js/windows/index.js +9 -0
- package/src/js/windows/window-manager.js +375 -0
- package/tests/catch2/catch.hpp +17976 -0
- package/tests/common/basic_program_builder.cpp +119 -0
- package/tests/common/basic_program_builder.hpp +209 -0
- package/tests/common/disk_image_builder.cpp +444 -0
- package/tests/common/disk_image_builder.hpp +141 -0
- package/tests/common/test_helpers.hpp +118 -0
- package/tests/gcr/gcr-test.cpp +142 -0
- package/tests/integration/check-rom.js +70 -0
- package/tests/integration/compare-boot.js +239 -0
- package/tests/integration/crash-trace.js +102 -0
- package/tests/integration/disk-boot-test.js +264 -0
- package/tests/integration/memory-crash.js +108 -0
- package/tests/integration/nibble-read-test.js +249 -0
- package/tests/integration/phase-test.js +159 -0
- package/tests/integration/test_emulator.cpp +291 -0
- package/tests/integration/test_emulator_basic.cpp +91 -0
- package/tests/integration/test_emulator_debug.cpp +344 -0
- package/tests/integration/test_emulator_disk.cpp +153 -0
- package/tests/integration/test_emulator_state.cpp +163 -0
- package/tests/klaus/6502_functional_test.bin +0 -0
- package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
- package/tests/klaus/klaus_6502_test.cpp +184 -0
- package/tests/klaus/klaus_65c02_test.cpp +197 -0
- package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
- package/tests/thunderclock/thunderclock_test.cpp +550 -0
- package/tests/unit/test_assembler.cpp +521 -0
- package/tests/unit/test_audio.cpp +196 -0
- package/tests/unit/test_ay8910.cpp +311 -0
- package/tests/unit/test_basic_detokenizer.cpp +265 -0
- package/tests/unit/test_basic_tokenizer.cpp +382 -0
- package/tests/unit/test_block_device.cpp +259 -0
- package/tests/unit/test_condition_evaluator.cpp +219 -0
- package/tests/unit/test_cpu6502.cpp +1301 -0
- package/tests/unit/test_cpu_addressing.cpp +361 -0
- package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
- package/tests/unit/test_cpu_decimal.cpp +166 -0
- package/tests/unit/test_cpu_interrupts.cpp +285 -0
- package/tests/unit/test_disassembler.cpp +323 -0
- package/tests/unit/test_disk2_card.cpp +330 -0
- package/tests/unit/test_dos33.cpp +273 -0
- package/tests/unit/test_dsk_disk_image.cpp +315 -0
- package/tests/unit/test_expansion_card.cpp +178 -0
- package/tests/unit/test_gcr_encoding.cpp +232 -0
- package/tests/unit/test_keyboard.cpp +262 -0
- package/tests/unit/test_mmu.cpp +555 -0
- package/tests/unit/test_mmu_slots.cpp +323 -0
- package/tests/unit/test_mockingboard.cpp +352 -0
- package/tests/unit/test_mouse_card.cpp +386 -0
- package/tests/unit/test_pascal.cpp +248 -0
- package/tests/unit/test_prodos.cpp +259 -0
- package/tests/unit/test_smartport_card.cpp +321 -0
- package/tests/unit/test_thunderclock.cpp +354 -0
- package/tests/unit/test_via6522.cpp +323 -0
- package/tests/unit/test_video.cpp +319 -0
- package/tests/unit/test_woz_disk_image.cpp +257 -0
- package/vite.config.js +96 -0
- package/wiki/AI-Agent.md +372 -0
- package/wiki/Architecture-Overview.md +303 -0
- package/wiki/Audio-System.md +449 -0
- package/wiki/CPU-Emulation.md +477 -0
- package/wiki/Debugger.md +516 -0
- package/wiki/Disk-Drives.md +161 -0
- package/wiki/Disk-System-Internals.md +547 -0
- package/wiki/Display-Settings.md +88 -0
- package/wiki/Expansion-Slots.md +187 -0
- package/wiki/File-Explorer.md +259 -0
- package/wiki/Getting-Started.md +156 -0
- package/wiki/Home.md +69 -0
- package/wiki/Input-Devices.md +183 -0
- package/wiki/Keyboard-Shortcuts.md +158 -0
- package/wiki/Memory-System.md +364 -0
- package/wiki/Save-States.md +172 -0
- 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 ? "&" : ch === 0x3c ? "<" : ch === 0x3e ? ">" : 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
|
+
}
|