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,551 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* merlin-highlighting.js - Merlin assembler syntax highlighting
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { escapeHtml } from "./string-utils.js";
|
|
9
|
+
|
|
10
|
+
// 6502/65C02 mnemonics grouped by category (matching disassembler colour scheme)
|
|
11
|
+
export const BRANCH_MNEMONICS = new Set([
|
|
12
|
+
'JMP', 'JSR', 'BCC', 'BCS', 'BEQ', 'BMI', 'BNE', 'BPL', 'BRA', 'BVC',
|
|
13
|
+
'BVS', 'RTS', 'RTI', 'BRK',
|
|
14
|
+
'BBR0', 'BBR1', 'BBR2', 'BBR3', 'BBR4', 'BBR5', 'BBR6', 'BBR7',
|
|
15
|
+
'BBS0', 'BBS1', 'BBS2', 'BBS3', 'BBS4', 'BBS5', 'BBS6', 'BBS7',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export const LOAD_MNEMONICS = new Set([
|
|
19
|
+
'LDA', 'LDX', 'LDY', 'STA', 'STX', 'STY', 'STZ',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export const MATH_MNEMONICS = new Set([
|
|
23
|
+
'ADC', 'SBC', 'AND', 'ORA', 'EOR', 'ASL', 'LSR', 'ROL', 'ROR',
|
|
24
|
+
'INC', 'DEC', 'INA', 'DEA', 'INX', 'DEX', 'INY', 'DEY',
|
|
25
|
+
'CMP', 'CPX', 'CPY', 'BIT', 'TRB', 'TSB',
|
|
26
|
+
'RMB0', 'RMB1', 'RMB2', 'RMB3', 'RMB4', 'RMB5', 'RMB6', 'RMB7',
|
|
27
|
+
'SMB0', 'SMB1', 'SMB2', 'SMB3', 'SMB4', 'SMB5', 'SMB6', 'SMB7',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export const STACK_MNEMONICS = new Set([
|
|
31
|
+
'PHA', 'PHP', 'PHX', 'PHY', 'PLA', 'PLP', 'PLX', 'PLY',
|
|
32
|
+
'TAX', 'TAY', 'TSX', 'TXA', 'TXS', 'TYA',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export const FLAG_MNEMONICS = new Set([
|
|
36
|
+
'CLC', 'CLD', 'CLI', 'CLV', 'SEC', 'SED', 'SEI', 'NOP', 'WAI', 'STP',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// All mnemonics combined for detection
|
|
40
|
+
export const ALL_MNEMONICS = new Set([
|
|
41
|
+
...BRANCH_MNEMONICS, ...LOAD_MNEMONICS, ...MATH_MNEMONICS,
|
|
42
|
+
...STACK_MNEMONICS, ...FLAG_MNEMONICS,
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// Merlin directives (pseudo-ops)
|
|
46
|
+
export const DIRECTIVES = new Set([
|
|
47
|
+
'ORG', 'EQU', 'DS', 'DFB', 'DW', 'DA', 'DDB', 'ASC', 'DCI', 'HEX',
|
|
48
|
+
'PUT', 'USE', 'OBJ', 'LST', 'DO', 'ELSE', 'FIN', 'LUP', '--^', 'REL',
|
|
49
|
+
'TYP', 'SAV', 'DSK', 'CHN', 'ENT', 'EXT', 'DUM', 'DEND', 'ERR', 'CYC',
|
|
50
|
+
'DAT', 'EXP', 'PAU', 'SW', 'USR', 'XC', 'MX', 'TR', 'KBD', 'PMC',
|
|
51
|
+
'PAG', 'TTL', 'SKP', 'CHK', 'IF', 'ELUP', 'END', 'MAC', 'EOM', '<<<',
|
|
52
|
+
'ADR', 'ADRL', 'DB', 'LNK', 'STR', 'STRL', 'REV',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
// Combined set for detection heuristic
|
|
56
|
+
const OPCODE_SET = new Set([...ALL_MNEMONICS, ...DIRECTIVES]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get CSS class for a mnemonic based on its category
|
|
60
|
+
*/
|
|
61
|
+
export function getMnemonicClass(mnemonic) {
|
|
62
|
+
const upper = mnemonic.toUpperCase();
|
|
63
|
+
if (BRANCH_MNEMONICS.has(upper)) return 'mer-branch';
|
|
64
|
+
if (LOAD_MNEMONICS.has(upper)) return 'mer-load';
|
|
65
|
+
if (MATH_MNEMONICS.has(upper)) return 'mer-math';
|
|
66
|
+
if (STACK_MNEMONICS.has(upper)) return 'mer-stack';
|
|
67
|
+
if (FLAG_MNEMONICS.has(upper)) return 'mer-flag';
|
|
68
|
+
return 'mer-macro';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Detect if text content appears to be Merlin assembler source.
|
|
73
|
+
* Scans first ~20 non-empty lines looking for recognised mnemonics/directives
|
|
74
|
+
* in the opcode column position.
|
|
75
|
+
* @param {string} text - Plain text (high bits already stripped, CRs converted)
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
export function isMerlinSource(text) {
|
|
79
|
+
const lines = text.split('\n');
|
|
80
|
+
let hits = 0;
|
|
81
|
+
let checked = 0;
|
|
82
|
+
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
if (checked >= 20) break;
|
|
85
|
+
|
|
86
|
+
const trimmed = line.trim();
|
|
87
|
+
if (!trimmed) continue;
|
|
88
|
+
|
|
89
|
+
checked++;
|
|
90
|
+
|
|
91
|
+
// Full-line comments are consistent with Merlin but don't count as hits
|
|
92
|
+
if (trimmed[0] === '*' || trimmed[0] === ';') continue;
|
|
93
|
+
|
|
94
|
+
// Parse opcode column: either first token (if line starts with whitespace)
|
|
95
|
+
// or second token (if line starts with a label)
|
|
96
|
+
const tokens = trimmed.split(/\s+/);
|
|
97
|
+
let opcode = null;
|
|
98
|
+
|
|
99
|
+
if (/^\s/.test(line)) {
|
|
100
|
+
// Line starts with whitespace: first token is the opcode
|
|
101
|
+
opcode = tokens[0];
|
|
102
|
+
} else if (tokens.length >= 2) {
|
|
103
|
+
// Line starts with a label: second token is the opcode
|
|
104
|
+
opcode = tokens[1];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (opcode && OPCODE_SET.has(opcode.toUpperCase())) {
|
|
108
|
+
hits++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return hits >= 3;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Highlight Merlin assembler source code with column-aligned output.
|
|
117
|
+
* @param {string} text - Plain text source (high bits stripped, CRs converted to newlines)
|
|
118
|
+
* @returns {string} HTML with syntax highlighting spans
|
|
119
|
+
*/
|
|
120
|
+
export function highlightMerlinSource(text) {
|
|
121
|
+
const lines = text.split('\n');
|
|
122
|
+
const highlighted = [];
|
|
123
|
+
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
highlighted.push(highlightMerlinLine(line));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return highlighted.join('');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Highlight a single Merlin source line, rendered as a flex row with
|
|
133
|
+
* fixed-width columns for label, opcode, operand, and comment.
|
|
134
|
+
* @param {string} line
|
|
135
|
+
* @returns {string} HTML
|
|
136
|
+
*/
|
|
137
|
+
function highlightMerlinLine(line) {
|
|
138
|
+
if (!line) return '<span class="mer-line"></span>';
|
|
139
|
+
|
|
140
|
+
const trimmed = line.trim();
|
|
141
|
+
if (!trimmed) return '<span class="mer-line"></span>';
|
|
142
|
+
|
|
143
|
+
// Full-line comment: starts with * or ;
|
|
144
|
+
if (trimmed[0] === '*' || trimmed[0] === ';') {
|
|
145
|
+
return `<span class="mer-line mer-comment-line"><span class="mer-comment">${escapeHtml(trimmed)}</span></span>`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Parse into columns: LABEL OPCODE OPERAND ;COMMENT
|
|
149
|
+
const hasLabel = !/^\s/.test(line);
|
|
150
|
+
const parts = splitMerlinColumns(line, hasLabel);
|
|
151
|
+
|
|
152
|
+
// Build the four column spans
|
|
153
|
+
const labelHtml = renderLabel(parts.label);
|
|
154
|
+
const opcodeHtml = renderOpcode(parts.opcode);
|
|
155
|
+
const operandHtml = renderOperandCol(parts.operand);
|
|
156
|
+
const commentHtml = parts.comment !== null
|
|
157
|
+
? `<span class="mer-col mer-col-comment"><span class="mer-comment">${escapeHtml(parts.comment)}</span></span>`
|
|
158
|
+
: '';
|
|
159
|
+
|
|
160
|
+
return `<span class="mer-line">${labelHtml}${opcodeHtml}${operandHtml}${commentHtml}</span>`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Render the label column
|
|
165
|
+
*/
|
|
166
|
+
function renderLabel(label) {
|
|
167
|
+
if (label === null) {
|
|
168
|
+
return '<span class="mer-col mer-col-label"></span>';
|
|
169
|
+
}
|
|
170
|
+
const cls = isLocalLabel(label) ? 'mer-local' : 'mer-label';
|
|
171
|
+
return `<span class="mer-col mer-col-label"><span class="${cls}">${escapeHtml(label)}</span></span>`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Render the opcode column with category-based colouring
|
|
176
|
+
*/
|
|
177
|
+
function renderOpcode(opcode) {
|
|
178
|
+
if (opcode === null) {
|
|
179
|
+
return '<span class="mer-col mer-col-opcode"></span>';
|
|
180
|
+
}
|
|
181
|
+
const upper = opcode.toUpperCase();
|
|
182
|
+
let cls;
|
|
183
|
+
if (ALL_MNEMONICS.has(upper)) {
|
|
184
|
+
cls = getMnemonicClass(opcode);
|
|
185
|
+
} else if (DIRECTIVES.has(upper)) {
|
|
186
|
+
cls = 'mer-directive';
|
|
187
|
+
} else {
|
|
188
|
+
cls = 'mer-macro';
|
|
189
|
+
}
|
|
190
|
+
return `<span class="mer-col mer-col-opcode"><span class="${cls}">${escapeHtml(opcode)}</span></span>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Render the operand column with sub-token highlighting
|
|
195
|
+
*/
|
|
196
|
+
function renderOperandCol(operand) {
|
|
197
|
+
if (operand === null) {
|
|
198
|
+
return '<span class="mer-col mer-col-operand"></span>';
|
|
199
|
+
}
|
|
200
|
+
return `<span class="mer-col mer-col-operand">${highlightOperand(operand)}</span>`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Split a Merlin source line into its column components.
|
|
205
|
+
* @param {string} line - The source line
|
|
206
|
+
* @param {boolean} hasLabel - Whether the line starts with a label
|
|
207
|
+
* @returns {Object} Column parts
|
|
208
|
+
*/
|
|
209
|
+
function splitMerlinColumns(line, hasLabel) {
|
|
210
|
+
const result = {
|
|
211
|
+
label: null,
|
|
212
|
+
opcode: null,
|
|
213
|
+
operand: null,
|
|
214
|
+
comment: null,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
let pos = 0;
|
|
218
|
+
|
|
219
|
+
if (hasLabel) {
|
|
220
|
+
// Extract label (runs until whitespace)
|
|
221
|
+
const labelMatch = line.match(/^(\S+)/);
|
|
222
|
+
if (labelMatch) {
|
|
223
|
+
result.label = labelMatch[1];
|
|
224
|
+
pos = labelMatch[1].length;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Skip whitespace to reach opcode
|
|
229
|
+
const spaceMatch = line.substring(pos).match(/^(\s+)/);
|
|
230
|
+
if (spaceMatch) {
|
|
231
|
+
pos += spaceMatch[1].length;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (pos >= line.length) return result;
|
|
235
|
+
|
|
236
|
+
// Extract opcode
|
|
237
|
+
const opcodeMatch = line.substring(pos).match(/^(\S+)/);
|
|
238
|
+
if (opcodeMatch) {
|
|
239
|
+
result.opcode = opcodeMatch[1];
|
|
240
|
+
pos += opcodeMatch[1].length;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Skip whitespace to reach operand
|
|
244
|
+
const afterOpcodeSpace = line.substring(pos).match(/^(\s+)/);
|
|
245
|
+
if (afterOpcodeSpace) {
|
|
246
|
+
pos += afterOpcodeSpace[1].length;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (pos >= line.length) return result;
|
|
250
|
+
|
|
251
|
+
// The rest is operand + possibly inline comment
|
|
252
|
+
const rest = line.substring(pos);
|
|
253
|
+
const { operand, comment } = splitOperandAndComment(rest);
|
|
254
|
+
|
|
255
|
+
result.operand = operand;
|
|
256
|
+
result.comment = comment;
|
|
257
|
+
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Split the remainder of a line into operand and inline comment.
|
|
263
|
+
* Respects string delimiters so semicolons inside strings aren't treated as comments.
|
|
264
|
+
* @param {string} rest
|
|
265
|
+
* @returns {Object} { operand, comment }
|
|
266
|
+
*/
|
|
267
|
+
function splitOperandAndComment(rest) {
|
|
268
|
+
let inSingleQuote = false;
|
|
269
|
+
let inDoubleQuote = false;
|
|
270
|
+
|
|
271
|
+
for (let i = 0; i < rest.length; i++) {
|
|
272
|
+
const ch = rest[i];
|
|
273
|
+
|
|
274
|
+
if (ch === "'" && !inDoubleQuote) {
|
|
275
|
+
inSingleQuote = !inSingleQuote;
|
|
276
|
+
} else if (ch === '"' && !inSingleQuote) {
|
|
277
|
+
inDoubleQuote = !inDoubleQuote;
|
|
278
|
+
} else if (ch === ';' && !inSingleQuote && !inDoubleQuote) {
|
|
279
|
+
const before = rest.substring(0, i).trimEnd();
|
|
280
|
+
return {
|
|
281
|
+
operand: before || null,
|
|
282
|
+
comment: rest.substring(i),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
operand: rest.trimEnd() || null,
|
|
289
|
+
comment: null,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check if a label is a Merlin local label (starts with : or ])
|
|
295
|
+
* @param {string} label
|
|
296
|
+
* @returns {boolean}
|
|
297
|
+
*/
|
|
298
|
+
function isLocalLabel(label) {
|
|
299
|
+
return label[0] === ':' || label[0] === ']';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Highlight an operand field, identifying strings, numbers, label refs, and symbols.
|
|
304
|
+
* @param {string} operand
|
|
305
|
+
* @returns {string} HTML
|
|
306
|
+
*/
|
|
307
|
+
function highlightOperand(operand) {
|
|
308
|
+
let html = '';
|
|
309
|
+
let i = 0;
|
|
310
|
+
|
|
311
|
+
while (i < operand.length) {
|
|
312
|
+
const ch = operand[i];
|
|
313
|
+
|
|
314
|
+
// String literal (single or double quoted)
|
|
315
|
+
if (ch === "'" || ch === '"') {
|
|
316
|
+
const quote = ch;
|
|
317
|
+
let j = i + 1;
|
|
318
|
+
while (j < operand.length && operand[j] !== quote) {
|
|
319
|
+
j++;
|
|
320
|
+
}
|
|
321
|
+
if (j < operand.length) j++; // include closing quote
|
|
322
|
+
html += `<span class="mer-string">${escapeHtml(operand.substring(i, j))}</span>`;
|
|
323
|
+
i = j;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Hex number: $xx...
|
|
328
|
+
if (ch === '$') {
|
|
329
|
+
let j = i + 1;
|
|
330
|
+
while (j < operand.length && /[0-9A-Fa-f]/.test(operand[j])) {
|
|
331
|
+
j++;
|
|
332
|
+
}
|
|
333
|
+
if (j > i + 1) {
|
|
334
|
+
html += `<span class="mer-number">${escapeHtml(operand.substring(i, j))}</span>`;
|
|
335
|
+
i = j;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Binary number: %01...
|
|
341
|
+
if (ch === '%') {
|
|
342
|
+
let j = i + 1;
|
|
343
|
+
while (j < operand.length && /[01]/.test(operand[j])) {
|
|
344
|
+
j++;
|
|
345
|
+
}
|
|
346
|
+
if (j > i + 1) {
|
|
347
|
+
html += `<span class="mer-number">${escapeHtml(operand.substring(i, j))}</span>`;
|
|
348
|
+
i = j;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Decimal number
|
|
354
|
+
if (/[0-9]/.test(ch)) {
|
|
355
|
+
let j = i;
|
|
356
|
+
while (j < operand.length && /[0-9]/.test(operand[j])) {
|
|
357
|
+
j++;
|
|
358
|
+
}
|
|
359
|
+
html += `<span class="mer-number">${escapeHtml(operand.substring(i, j))}</span>`;
|
|
360
|
+
i = j;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Local label reference (: or ] prefix)
|
|
365
|
+
if (ch === ':' || ch === ']') {
|
|
366
|
+
let j = i + 1;
|
|
367
|
+
while (j < operand.length && /[A-Za-z0-9_]/.test(operand[j])) {
|
|
368
|
+
j++;
|
|
369
|
+
}
|
|
370
|
+
if (j > i + 1) {
|
|
371
|
+
html += `<span class="mer-local">${escapeHtml(operand.substring(i, j))}</span>`;
|
|
372
|
+
i = j;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Immediate prefix (#)
|
|
378
|
+
if (ch === '#') {
|
|
379
|
+
html += `<span class="mer-immediate">${escapeHtml(ch)}</span>`;
|
|
380
|
+
i++;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Label / symbol reference (identifier)
|
|
385
|
+
if (/[A-Za-z_]/.test(ch)) {
|
|
386
|
+
let j = i + 1;
|
|
387
|
+
while (j < operand.length && /[A-Za-z0-9_.]/.test(operand[j])) {
|
|
388
|
+
j++;
|
|
389
|
+
}
|
|
390
|
+
html += `<span class="mer-symbol">${escapeHtml(operand.substring(i, j))}</span>`;
|
|
391
|
+
i = j;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Operators and punctuation (, + - * / < > ^ | & ( ))
|
|
396
|
+
if (',+-*/<>^|&()'.includes(ch)) {
|
|
397
|
+
html += `<span class="mer-punct">${escapeHtml(ch)}</span>`;
|
|
398
|
+
i++;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Anything else
|
|
403
|
+
html += escapeHtml(ch);
|
|
404
|
+
i++;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return html;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Highlight Merlin assembler source code for inline editor overlay.
|
|
412
|
+
* Unlike highlightMerlinSource() which uses flex columns, this version
|
|
413
|
+
* produces inline spans that preserve original whitespace exactly,
|
|
414
|
+
* ensuring pixel-perfect alignment with a textarea overlay.
|
|
415
|
+
* @param {string} text - Source text
|
|
416
|
+
* @returns {string} HTML with syntax highlighting spans
|
|
417
|
+
*/
|
|
418
|
+
export function highlightMerlinSourceInline(text) {
|
|
419
|
+
const lines = text.split('\n');
|
|
420
|
+
const highlighted = [];
|
|
421
|
+
|
|
422
|
+
for (const line of lines) {
|
|
423
|
+
highlighted.push(highlightMerlinLineInline(line));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return highlighted.join('\n');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Highlight a single line inline (preserving exact whitespace).
|
|
431
|
+
* @param {string} line
|
|
432
|
+
* @returns {string} HTML
|
|
433
|
+
*/
|
|
434
|
+
function highlightMerlinLineInline(line) {
|
|
435
|
+
if (!line) return '';
|
|
436
|
+
|
|
437
|
+
const trimmed = line.trim();
|
|
438
|
+
if (!trimmed) return escapeHtml(line);
|
|
439
|
+
|
|
440
|
+
// Full-line comment
|
|
441
|
+
if (trimmed[0] === '*' || trimmed[0] === ';') {
|
|
442
|
+
return `<span class="mer-comment">${escapeHtml(line)}</span>`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Parse into columns, preserving whitespace positions
|
|
446
|
+
const hasLabel = !/^\s/.test(line);
|
|
447
|
+
let pos = 0;
|
|
448
|
+
let result = '';
|
|
449
|
+
|
|
450
|
+
// Leading whitespace
|
|
451
|
+
while (pos < line.length && (line[pos] === ' ' || line[pos] === '\t')) {
|
|
452
|
+
result += line[pos];
|
|
453
|
+
pos++;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Label (if present)
|
|
457
|
+
if (hasLabel && pos < line.length) {
|
|
458
|
+
const labelStart = pos;
|
|
459
|
+
while (pos < line.length && line[pos] !== ' ' && line[pos] !== '\t') pos++;
|
|
460
|
+
const label = line.substring(labelStart, pos);
|
|
461
|
+
const cls = isLocalLabel(label) ? 'mer-local' : 'mer-label';
|
|
462
|
+
result += `<span class="${cls}">${escapeHtml(label)}</span>`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Whitespace between label and opcode
|
|
466
|
+
while (pos < line.length && (line[pos] === ' ' || line[pos] === '\t')) {
|
|
467
|
+
result += line[pos];
|
|
468
|
+
pos++;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (pos >= line.length) return result;
|
|
472
|
+
|
|
473
|
+
// Check for inline comment at opcode position
|
|
474
|
+
if (line[pos] === ';' || line[pos] === '*') {
|
|
475
|
+
result += `<span class="mer-comment">${escapeHtml(line.substring(pos))}</span>`;
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Opcode
|
|
480
|
+
const opcodeStart = pos;
|
|
481
|
+
while (pos < line.length && line[pos] !== ' ' && line[pos] !== '\t') pos++;
|
|
482
|
+
const opcode = line.substring(opcodeStart, pos);
|
|
483
|
+
const upper = opcode.toUpperCase();
|
|
484
|
+
|
|
485
|
+
let opcCls;
|
|
486
|
+
if (ALL_MNEMONICS.has(upper)) {
|
|
487
|
+
opcCls = getMnemonicClass(opcode);
|
|
488
|
+
} else if (DIRECTIVES.has(upper)) {
|
|
489
|
+
opcCls = 'mer-directive';
|
|
490
|
+
} else {
|
|
491
|
+
opcCls = 'mer-macro';
|
|
492
|
+
}
|
|
493
|
+
result += `<span class="${opcCls}">${escapeHtml(opcode)}</span>`;
|
|
494
|
+
|
|
495
|
+
// Whitespace between opcode and operand
|
|
496
|
+
while (pos < line.length && (line[pos] === ' ' || line[pos] === '\t')) {
|
|
497
|
+
result += line[pos];
|
|
498
|
+
pos++;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (pos >= line.length) return result;
|
|
502
|
+
|
|
503
|
+
// Rest of line: operand + possible comment
|
|
504
|
+
const rest = line.substring(pos);
|
|
505
|
+
const { operand, comment, commentOffset } = splitOperandAndCommentWithOffset(rest);
|
|
506
|
+
|
|
507
|
+
if (operand) {
|
|
508
|
+
result += highlightOperand(operand);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (comment !== null) {
|
|
512
|
+
// Add any whitespace between operand end and comment start
|
|
513
|
+
const operandLen = operand ? operand.length : 0;
|
|
514
|
+
const gap = rest.substring(operandLen, commentOffset);
|
|
515
|
+
result += escapeHtml(gap);
|
|
516
|
+
result += `<span class="mer-comment">${escapeHtml(comment)}</span>`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Split operand and comment, also returning the comment's offset in the rest string.
|
|
524
|
+
*/
|
|
525
|
+
function splitOperandAndCommentWithOffset(rest) {
|
|
526
|
+
let inSingleQuote = false;
|
|
527
|
+
let inDoubleQuote = false;
|
|
528
|
+
|
|
529
|
+
for (let i = 0; i < rest.length; i++) {
|
|
530
|
+
const ch = rest[i];
|
|
531
|
+
|
|
532
|
+
if (ch === "'" && !inDoubleQuote) {
|
|
533
|
+
inSingleQuote = !inSingleQuote;
|
|
534
|
+
} else if (ch === '"' && !inSingleQuote) {
|
|
535
|
+
inDoubleQuote = !inDoubleQuote;
|
|
536
|
+
} else if (ch === ';' && !inSingleQuote && !inDoubleQuote) {
|
|
537
|
+
const before = rest.substring(0, i).trimEnd();
|
|
538
|
+
return {
|
|
539
|
+
operand: before || null,
|
|
540
|
+
comment: rest.substring(i),
|
|
541
|
+
commentOffset: i,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
operand: rest.trimEnd() || null,
|
|
548
|
+
comment: null,
|
|
549
|
+
commentOffset: rest.length,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* storage.js - Local storage utilities
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Safely get an item from localStorage
|
|
10
|
+
* @param {string} key - The storage key
|
|
11
|
+
* @param {*} defaultValue - Default value if key doesn't exist or on error
|
|
12
|
+
* @returns {string|null} The stored value or defaultValue
|
|
13
|
+
*/
|
|
14
|
+
export function getItem(key, defaultValue = null) {
|
|
15
|
+
try {
|
|
16
|
+
const value = localStorage.getItem(key);
|
|
17
|
+
return value !== null ? value : defaultValue;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// localStorage may be unavailable (private browsing, disabled, etc.)
|
|
20
|
+
return defaultValue;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Safely set an item in localStorage
|
|
26
|
+
* @param {string} key - The storage key
|
|
27
|
+
* @param {string} value - The value to store
|
|
28
|
+
* @returns {boolean} True if successful, false otherwise
|
|
29
|
+
*/
|
|
30
|
+
export function setItem(key, value) {
|
|
31
|
+
try {
|
|
32
|
+
localStorage.setItem(key, value);
|
|
33
|
+
return true;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// localStorage may be unavailable or quota exceeded
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Safely remove an item from localStorage
|
|
42
|
+
* @param {string} key - The storage key
|
|
43
|
+
* @returns {boolean} True if successful, false otherwise
|
|
44
|
+
*/
|
|
45
|
+
export function removeItem(key) {
|
|
46
|
+
try {
|
|
47
|
+
localStorage.removeItem(key);
|
|
48
|
+
return true;
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Safely get and parse a JSON item from localStorage
|
|
56
|
+
* @param {string} key - The storage key
|
|
57
|
+
* @param {*} defaultValue - Default value if key doesn't exist, on error, or invalid JSON
|
|
58
|
+
* @returns {*} The parsed value or defaultValue
|
|
59
|
+
*/
|
|
60
|
+
export function getJSON(key, defaultValue = null) {
|
|
61
|
+
try {
|
|
62
|
+
const value = localStorage.getItem(key);
|
|
63
|
+
if (value === null) return defaultValue;
|
|
64
|
+
return JSON.parse(value);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
// localStorage unavailable or invalid JSON
|
|
67
|
+
return defaultValue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Safely stringify and set a JSON item in localStorage
|
|
73
|
+
* @param {string} key - The storage key
|
|
74
|
+
* @param {*} value - The value to store (will be JSON.stringify'd)
|
|
75
|
+
* @returns {boolean} True if successful, false otherwise
|
|
76
|
+
*/
|
|
77
|
+
export function setJSON(key, value) {
|
|
78
|
+
try {
|
|
79
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
80
|
+
return true;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Safely get a numeric value from localStorage
|
|
88
|
+
* @param {string} key - The storage key
|
|
89
|
+
* @param {number} defaultValue - Default value if key doesn't exist, on error, or not a valid number
|
|
90
|
+
* @param {object} options - Optional constraints: { min, max }
|
|
91
|
+
* @returns {number} The numeric value or defaultValue
|
|
92
|
+
*/
|
|
93
|
+
export function getNumber(key, defaultValue, options = {}) {
|
|
94
|
+
try {
|
|
95
|
+
const value = localStorage.getItem(key);
|
|
96
|
+
if (value === null) return defaultValue;
|
|
97
|
+
|
|
98
|
+
const num = parseFloat(value);
|
|
99
|
+
if (isNaN(num)) return defaultValue;
|
|
100
|
+
|
|
101
|
+
// Apply constraints if provided
|
|
102
|
+
if (options.min !== undefined && num < options.min) return defaultValue;
|
|
103
|
+
if (options.max !== undefined && num > options.max) return defaultValue;
|
|
104
|
+
|
|
105
|
+
return num;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return defaultValue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Safely get a boolean value from localStorage
|
|
113
|
+
* @param {string} key - The storage key
|
|
114
|
+
* @param {boolean} defaultValue - Default value if key doesn't exist or on error
|
|
115
|
+
* @returns {boolean} The boolean value or defaultValue
|
|
116
|
+
*/
|
|
117
|
+
export function getBoolean(key, defaultValue) {
|
|
118
|
+
try {
|
|
119
|
+
const value = localStorage.getItem(key);
|
|
120
|
+
if (value === null) return defaultValue;
|
|
121
|
+
return value === 'true';
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return defaultValue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* string-utils.js - String utility functions
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Escape HTML special characters to prevent XSS
|
|
10
|
+
* @param {string} text - The text to escape
|
|
11
|
+
* @returns {string} - The escaped text safe for HTML insertion
|
|
12
|
+
*/
|
|
13
|
+
export function escapeHtml(text) {
|
|
14
|
+
return text
|
|
15
|
+
.replace(/&/g, "&")
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/"/g, """);
|
|
19
|
+
}
|