Напиши расширение TurboWarp на JavaScript, названи...
생성일: 2026년 8월 8일
답변에 사용된 모델 GPT-5.6 Thinking by Chat01
생성일: 2026년 8월 8일
답변에 사용된 모델 GPT-5.6 Thinking by Chat01
Напиши расширение TurboWarp на JavaScript, название: "Макросы", цвет расширения: #058aa8 (в системе RGB). В перечислении блоков расширения все заголовки блоков между их перечислением также отображать в палитре блоков там, где они находятся в перечислении блоков. Пустые блоки между блоками в перечислении блоков - небольшое пространство между этими блоками в палитре блоков (если пустых строк несколько, умножать на количество пустых строк между перечислением блоков). Вот максимально подробный план расширения:
"()" - поле ввода текста; "{}" - поле ввода только числового/цифрового значения; "[]" - поле выбора варианта; "([])" - поле выбора варианта, куда можно вставлять блоки-переменные (все овальные блоки, не только переменные); "/" - отделение вариантов друг от друга в поле выбора варианта (не важно, можно ли туда вставлять овальные блоки-переменные или нет).
Блоки:
Заголовок "Симуляция нажатия"
Блок "симулировать нажатие ([])" - варианты выбора: все клавиши, доступные на клавиатуре (включая F1 - F12, Win и все кнопки Нум Пада), все кнопки мыши: "ПКМ" / "СКМ" / "ЛКМ" / "Дополнительный кнопки мыши" - (под "Дополнительные кнопки мыши" подразумевается симуляция нажатие всех дополнительных кнопок мыши одновременно, даже если их нет на мышки пользователя), а также: "Скроллинг вверх" / "Скроллинг вниз" (симулировать скроллинг колёсика мыши вверх или вниз, в зависимости от выбора).
Блок "остановить симуляцию нажатия ([])" - варианты выбора: все клавиши, доступные на клавиатуре (включая F1 - F12, Win и все кнопки Нум Пада), все кнопки мыши: "ПКМ" / "СКМ" / "ЛКМ" / "Дополнительный кнопки мыши" - (под "Дополнительные кнопки мыши" подразумевается симуляция нажатие всех дополнительных кнопок мыши одновременно, даже если их нет на мышки пользователя), а также: "Скроллинг вверх" / "Скроллинг вниз" (симулировать скроллинг колёсика мыши вверх или вниз, в зависимости от выбора).
Блок-сенсор "([]) симулирует нажатие?" - варианты выбора: все клавиши, доступные на клавиатуре (включая F1 - F12, Win и все кнопки Нум Пада), все кнопки мыши: "ПКМ" / "СКМ" / "ЛКМ" / "Дополнительный кнопки мыши" - (под "Дополнительные кнопки мыши" подразумевается симуляция нажатие всех дополнительных кнопок мыши одновременно, даже если их нет на мышки пользователя), а также: "Скроллинг вверх" / "Скроллинг вниз" (симулировать скроллинг колёсика мыши вверх или вниз, в зависимости от выбора).
Блок "симулировать нажатие ()" - можно вписать любую клавишу/кнопку/скроллинг и она будет работать: все клавиши, доступные на клавиатуре (включая F1 - F12, Win и все кнопки Нум Пада), все кнопки мыши: "ПКМ" / "СКМ" / "ЛКМ" / "Дополнительный кнопки мыши" - (под "Дополнительные кнопки мыши" подразумевается симуляция нажатие всех дополнительных кнопок мыши одновременно, даже если их нет на мышки пользователя), а также: "Скроллинг вверх" / "Скроллинг вниз" (симулировать скроллинг колёсика мыши вверх или вниз, в зависимости от выбора).
Блок "остановить симуляцию нажатия ()" - можно вписать любую клавишу/кнопку/скроллинг и она будет работать: все клавиши, доступные на клавиатуре (включая F1 - F12, Win и все кнопки Нум Пада), все кнопки мыши: "ПКМ" / "СКМ" / "ЛКМ" / "Дополнительный кнопки мыши" - (под "Дополнительные кнопки мыши" подразумевается симуляция нажатие всех дополнительных кнопок мыши одновременно, даже если их нет на мышки пользователя), а также: "Скроллинг вверх" / "Скроллинг вниз" (симулировать скроллинг колёсика мыши вверх или вниз, в зависимости от выбора).
Блок-сенсор "() симулирует нажатие?" - можно вписать любую клавишу/кнопку/скроллинг и она будет работать: все клавиши, доступные на клавиатуре (включая F1 - F12, Win и все кнопки Нум Пада), все кнопки мыши: "ПКМ" / "СКМ" / "ЛКМ" / "Дополнительный кнопки мыши" - (под "Дополнительные кнопки мыши" подразумевается симуляция нажатие всех дополнительных кнопок мыши одновременно, даже если их нет на мышки пользователя), а также: "Скроллинг вверх" / "Скроллинг вниз" (симулировать скроллинг колёсика мыши вверх или вниз, в зависимости от выбора).
Блок-переменная "все симулируемые активы" - перечисление всех симулируемых клавиш/кнопок/скроллингов и т.д. в формате: "(название клавиши/кнопки/скриллинга); (название клавиши/кнопки/скриллинга); ....".
Блок-переменная "количество симулируемых активов" - количество всех симулируемых клавиш, кнопок и скроллингов вместе взятых.
Блок "установить тип симуляции нажатия клавиш-букв (["английские и русский" / "только английские" / "только русские"])" - если клавиша - буква, "английский и русские" - симулирует и английскую и русскую версию клавиши одновременно; "только английские" и "только русские" - понятно.
Блок "установить тип симуляции нажатия клавиш-символов (["с Shift и без" / "только с Shift" / "только без Shift"])" - если клавиша-символ, симулировать все возможные варианты этой клавиши в соответствии с выбором в блоке.
Блок "установить раскладку симуляции нажатия клавиш-символов (["английская и русская" / "только английская" / "только русская"])" - если клавиша-символ, симулировать все возможные варианты этой клавиши в соответствии с выбором в блоке.
Блок-переменная "тип симуляции нажатия клавиш-букв"
Блок-переменная "раскладка симуляции нажатия клавиш-символов"
Блок-переменная "тип симуляции нажатия клавиш-символов" - ("с Shift и без" / "только с Shift" / "только без Shift").
Заголовок "Сенсоры нажатий"
15. Блок-сенсор "актив ([]) нажат?" - варианты выбора: все клавиши, доступные на клавиатуре (включая F1 - F12, Win и все кнопки Нум Пада), все кнопки мыши: "ПКМ" / "СКМ" / "ЛКМ" / "Дополнительный кнопки мыши" - (под "Дополнительные кнопки мыши" подразумевается симуляция нажатие всех дополнительных кнопок мыши одновременно, даже если их нет на мышки пользователя), а также: "Скроллинг вверх" / "Скроллинг вниз" (симулировать скроллинг колёсика мыши вверх или вниз, в зависимости от выбора).
16. Блок-сенсор "актив () нажат?" - можно вписать любую клавишу/кнопку/скроллинг и она будет работать: все клавиши, доступные на клавиатуре (включая F1 - F12, Win и все кнопки Нум Пада), все кнопки мыши: "ПКМ" / "СКМ" / "ЛКМ" / "Дополнительный кнопки мыши" - (под "Дополнительные кнопки мыши" подразумевается симуляция нажатие всех дополнительных кнопок мыши одновременно, даже если их нет на мышки пользователя), а также: "Скроллинг вверх" / "Скроллинг вниз" (симулировать скроллинг колёсика мыши вверх или вниз, в зависимости от выбора).
Блок-переменная "все нажатие активы" - перечисление всех симулируемых клавиш/кнопок/скроллингов и т.д. в формате: "(название клавиши/кнопки/скриллинга); (название клавиши/кнопки/скриллинга); ....".
Блок-переменная "количество нажатых активов" - количество всех нажатых клавиш, кнопок и скроллингов вместе взятых.
Блок "установить тип считывания нажатых клавиш-букв (["английский и русский" / "только английский" / "только русский"])" - допустим есть блок-сенсор "клавиша (W) нажата?", с типом "английский и русский" и нажатие "Ц" и нажатие "W" будет засчитываться, с типом "только английский" будет засчитываться только нажатие "W", с "только "только русский" только нажатие "Ц".
Блок "установить тип считывания нажатых клавиш-символов (["с Shift и без" / "только с Shift" / "только без Shift"])" - тоже самое что и прошлый (19) блок, только теперь с Shift и с клавишами-символами.
Блок "установить раскладку считывания нажатых клавиш-символов (["английский и русский" / "только английский" / "только русский"])" - тоже самое, что и девятнадцатый (19) блок, только с клавишами-символами.
Блок-переменная "тип считывания нажатий клавиш-букв" - "английский и русский" / "только английский" / "только русский" в зависимости от девятнадцатого (19) блока.
Блок-переменная "тип считываний нажатий клавиш-символов" - "с Shift и без" / "только с Shift" / "только без Shift" в зависимости от двадцатого (20) блока.
Блок-переменная "раскладка считывания нажатий клавиш-символов" - "английский и русский" / "только английский" / "только русский" в зависимости от двадцать первого (21) блока.
Заголовок "Макросы"
25. Блок "начать запись макроса (особое значение-название макроса, для отделения его от других)" - возможность записывать несколько макросов, засчитывает нажатие всех клавиш/кнопок/скроллингов и т.д., с возможностью воспроизвести их потом.
26. Блок "остановить запись макроса (особое значение-название макроса, для отделения его от других)" - возможность записывать несколько макросов, засчитывает нажатие всех клавиш/кнопок/скроллингов и т.д., с возможностью воспроизвести их потом.
27. Блок "продолжить/приостановить запись макроса (особое значение-название макроса, для отделения его от других)" - возможность записывать несколько макросов, засчитывает нажатие всех клавиш/кнопок/скроллингов и т.д., с возможностью воспроизвести их потом.
28. Блок "очистить макрос (особое значение-название макроса, для отделения его от других)" - возможность записывать несколько макросов, засчитывает нажатие всех клавиш/кнопок/скроллингов и т.д., с возможностью воспроизвести их потом.
Блок "воспроизвести/приостановить макрос (особое значение-название макроса, для отделения его от других)" - возможность записывать несколько макросов, засчитывает нажатие всех клавиш/кнопок/скроллингов и т.д., с возможностью воспроизвести их потом.
Блок-сенсор "макрос (особое значение-название макроса, для отделения его от других) существует?" - если макрос с таким особым значением-названием не записывался или был очищен (сброшен), выдавать значение "false".
Блок-сенсор "макрос (особое значение-название макроса, для отделения его от других) содержит ([])" - варианты выбора: все клавиши, доступные на клавиатуре (включая F1 - F12, Win и все кнопки Нум Пада), все кнопки мыши: "ПКМ" / "СКМ" / "ЛКМ" / "Дополнительный кнопки мыши" - (под "Дополнительные кнопки мыши" подразумевается симуляция нажатие всех дополнительных кнопок мыши одновременно, даже если их нет на мышки пользователя), а также: "Скроллинг вверх" / "Скроллинг вниз" (симулировать скроллинг колёсика мыши вверх или вниз, в зависимости от выбора).
Блок-сенсор "макрос (особое значение-название макроса, для отделения его от других) содержит ()" - можно вписать любую клавишу/кнопку/скроллинг и она будет работать: все клавиши, доступные на клавиатуре (включая F1 - F12, Win и все кнопки Нум Пада), все кнопки мыши: "ПКМ" / "СКМ" / "ЛКМ" / "Дополнительный кнопки мыши" - (под "Дополнительные кнопки мыши" подразумевается симуляция нажатие всех дополнительных кнопок мыши одновременно, даже если их нет на мышки пользователя), а также: "Скроллинг вверх" / "Скроллинг вниз" (симулировать скроллинг колёсика мыши вверх или вниз, в зависимости от выбора).
Блок-переменная "длина макроса (особое значение-название макроса, для отделения его от других)"
Блок-переменная "количество записанных макросов"
Должен быть ОДИН скрипт на JavaScript (.js), без дополнительных скриптов, ТОЛЬКО JAVASCRIPT. Если что-либо (и всё связанное с этим, по желанию) никак реализовать - не реализовывай и пропускай!
Весь JS код напиши текстом в ответ, БЕЗ ВСЯКИХ ССЫЛОК НА СКАЧИВАНИЯ, из-за бага на сайте, ссылки ведут в никуда. ВЕСЬ JS КОД НАПИШИ В ОТВЕТ ЦЕЛИКОМ КАК ТЕКСТ, БЕЗ ССЫЛОК НА СКАЧИВАНИЕ!
Ниже — один цельный JavaScript-скрипт. Его нужно загружать в TurboWarp как unsandboxed: только такой режим даёт расширению прямой доступ к странице и событиям браузера. Меню сделаны с acceptReporters: true, поэтому в них можно вставлять овальные блоки. (TurboWarp Documentation)
Важное ограничение: JavaScript страницы не может создать настоящее доверенное системное нажатие клавиатуры/мыши для Windows или другой программы — события, созданные скриптом, являются недоверенными (isTrusted = false). Поэтому симуляция ниже работает как внутреннее состояние расширения + KeyboardEvent/MouseEvent/WheelEvent внутри страницы TurboWarp; сенсоры и макросы расширения это состояние учитывают. (DOM Standard)
javascript(function (Scratch) { 'use strict'; if (!Scratch.extensions.unsandboxed) { throw new Error('Расширение «Макросы» необходимо запускать без песочницы (unsandboxed).'); } const BlockType = Scratch.BlockType; const ArgumentType = Scratch.ArgumentType; const COLOR = '#058aa8'; const WHEEL_PULSE_MS = 140; const SIMULATED_WHEEL_INTERVAL_MS = 60; const SIMULATED_WHEEL_DELTA = 100; const LETTER_MODE_BOTH = 'английские и русские'; const LETTER_MODE_EN = 'только английские'; const LETTER_MODE_RU = 'только русские'; const READ_LETTER_MODE_BOTH = 'английский и русский'; const READ_LETTER_MODE_EN = 'только английский'; const READ_LETTER_MODE_RU = 'только русский'; const SHIFT_MODE_BOTH = 'с Shift и без'; const SHIFT_MODE_ONLY = 'только с Shift'; const SHIFT_MODE_NONE = 'только без Shift'; const SYMBOL_LAYOUT_BOTH = 'английская и русская'; const SYMBOL_LAYOUT_EN = 'только английская'; const SYMBOL_LAYOUT_RU = 'только русская'; const READ_SYMBOL_LAYOUT_BOTH = 'английский и русский'; const READ_SYMBOL_LAYOUT_EN = 'только английский'; const READ_SYMBOL_LAYOUT_RU = 'только русский'; const EN_LETTERS = { KeyQ: 'q', KeyW: 'w', KeyE: 'e', KeyR: 'r', KeyT: 't', KeyY: 'y', KeyU: 'u', KeyI: 'i', KeyO: 'o', KeyP: 'p', KeyA: 'a', KeyS: 's', KeyD: 'd', KeyF: 'f', KeyG: 'g', KeyH: 'h', KeyJ: 'j', KeyK: 'k', KeyL: 'l', KeyZ: 'z', KeyX: 'x', KeyC: 'c', KeyV: 'v', KeyB: 'b', KeyN: 'n', KeyM: 'm' }; const RU_LETTERS = { KeyQ: 'й', KeyW: 'ц', KeyE: 'у', KeyR: 'к', KeyT: 'е', KeyY: 'н', KeyU: 'г', KeyI: 'ш', KeyO: 'щ', KeyP: 'з', BracketLeft: 'х', BracketRight: 'ъ', KeyA: 'ф', KeyS: 'ы', KeyD: 'в', KeyF: 'а', KeyG: 'п', KeyH: 'р', KeyJ: 'о', KeyK: 'л', KeyL: 'д', Semicolon: 'ж', Quote: 'э', KeyZ: 'я', KeyX: 'ч', KeyC: 'с', KeyV: 'м', KeyB: 'и', KeyN: 'т', KeyM: 'ь', Comma: 'б', Period: 'ю', Backquote: 'ё' }; const EN_SYMBOLS = { Backquote: ['`', '~'], Digit1: ['1', '!'], Digit2: ['2', '@'], Digit3: ['3', '#'], Digit4: ['4', '$'], Digit5: ['5', '%'], Digit6: ['6', '^'], Digit7: ['7', '&'], Digit8: ['8', '*'], Digit9: ['9', '('], Digit0: ['0', ')'], Minus: ['-', '_'], Equal: ['=', '+'], BracketLeft: ['[', '{'], BracketRight: [']', '}'], Backslash: ['\\', '|'], Semicolon: [';', ':'], Quote: ["'", '"'], Comma: [',', '<'], Period: ['.', '>'], Slash: ['/', '?'] }; const RU_SYMBOLS = { Backquote: ['ё', 'Ё'], Digit1: ['1', '!'], Digit2: ['2', '"'], Digit3: ['3', '№'], Digit4: ['4', ';'], Digit5: ['5', '%'], Digit6: ['6', ':'], Digit7: ['7', '?'], Digit8: ['8', '*'], Digit9: ['9', '('], Digit0: ['0', ')'], Minus: ['-', '_'], Equal: ['=', '+'], BracketLeft: ['х', 'Х'], BracketRight: ['ъ', 'Ъ'], Backslash: ['\\', '/'], Semicolon: ['ж', 'Ж'], Quote: ['э', 'Э'], Comma: ['б', 'Б'], Period: ['ю', 'Ю'], Slash: ['.', ','] }; const SPECIAL_KEY_DATA = { Escape: {key: 'Escape', text: 'Esc'}, Tab: {key: 'Tab', text: 'Tab'}, CapsLock: {key: 'CapsLock', text: 'Caps Lock'}, ShiftLeft: {key: 'Shift', text: 'Shift (левый)', location: 1}, ShiftRight: {key: 'Shift', text: 'Shift (правый)', location: 2}, ControlLeft: {key: 'Control', text: 'Ctrl (левый)', location: 1}, ControlRight: {key: 'Control', text: 'Ctrl (правый)', location: 2}, AltLeft: {key: 'Alt', text: 'Alt (левый)', location: 1}, AltRight: {key: 'Alt', text: 'Alt (правый)', location: 2}, MetaLeft: {key: 'Meta', text: 'Win (левый)', location: 1}, MetaRight: {key: 'Meta', text: 'Win (правый)', location: 2}, Space: {key: ' ', text: 'Пробел'}, Enter: {key: 'Enter', text: 'Enter'}, Backspace: {key: 'Backspace', text: 'Backspace'}, Insert: {key: 'Insert', text: 'Insert'}, Delete: {key: 'Delete', text: 'Delete'}, Home: {key: 'Home', text: 'Home'}, End: {key: 'End', text: 'End'}, PageUp: {key: 'PageUp', text: 'Page Up'}, PageDown: {key: 'PageDown', text: 'Page Down'}, ArrowUp: {key: 'ArrowUp', text: 'Стрелка вверх'}, ArrowDown: {key: 'ArrowDown', text: 'Стрелка вниз'}, ArrowLeft: {key: 'ArrowLeft', text: 'Стрелка влево'}, ArrowRight: {key: 'ArrowRight', text: 'Стрелка вправо'}, PrintScreen: {key: 'PrintScreen', text: 'Print Screen'}, ScrollLock: {key: 'ScrollLock', text: 'Scroll Lock'}, Pause: {key: 'Pause', text: 'Pause/Break'}, ContextMenu: {key: 'ContextMenu', text: 'Menu'}, NumLock: {key: 'NumLock', text: 'Num Lock', location: 3}, NumpadDivide: {key: '/', text: 'NumPad /', location: 3}, NumpadMultiply: {key: '*', text: 'NumPad *', location: 3}, NumpadSubtract: {key: '-', text: 'NumPad -', location: 3}, NumpadAdd: {key: '+', text: 'NumPad +', location: 3}, NumpadEnter: {key: 'Enter', text: 'NumPad Enter', location: 3}, NumpadDecimal: {key: '.', text: 'NumPad .', location: 3}, NumpadComma: {key: ',', text: 'NumPad ,', location: 3}, NumpadEqual: {key: '=', text: 'NumPad =', location: 3}, AudioVolumeMute: {key: 'AudioVolumeMute', text: 'Громкость: выкл.'}, AudioVolumeDown: {key: 'AudioVolumeDown', text: 'Громкость -'}, AudioVolumeUp: {key: 'AudioVolumeUp', text: 'Громкость +'}, MediaTrackPrevious: {key: 'MediaTrackPrevious', text: 'Медиа: предыдущий'}, MediaTrackNext: {key: 'MediaTrackNext', text: 'Медиа: следующий'}, MediaPlayPause: {key: 'MediaPlayPause', text: 'Медиа: play/pause'}, MediaStop: {key: 'MediaStop', text: 'Медиа: stop'}, BrowserBack: {key: 'BrowserBack', text: 'Браузер: назад'}, BrowserForward: {key: 'BrowserForward', text: 'Браузер: вперёд'}, BrowserRefresh: {key: 'BrowserRefresh', text: 'Браузер: обновить'}, BrowserStop: {key: 'BrowserStop', text: 'Браузер: stop'}, BrowserSearch: {key: 'BrowserSearch', text: 'Браузер: поиск'}, BrowserFavorites: {key: 'BrowserFavorites', text: 'Браузер: избранное'}, BrowserHome: {key: 'BrowserHome', text: 'Браузер: домой'}, LaunchMail: {key: 'LaunchMail', text: 'Запустить почту'}, LaunchApp1: {key: 'LaunchApplication1', text: 'Запустить приложение 1'}, LaunchApp2: {key: 'LaunchApplication2', text: 'Запустить приложение 2'}, Power: {key: 'Power', text: 'Power'}, Sleep: {key: 'Standby', text: 'Sleep'}, WakeUp: {key: 'WakeUp', text: 'Wake Up'}, Eject: {key: 'Eject', text: 'Eject'}, Help: {key: 'Help', text: 'Help'}, Again: {key: 'Again', text: 'Again'}, Undo: {key: 'Undo', text: 'Undo'}, Cut: {key: 'Cut', text: 'Cut'}, Copy: {key: 'Copy', text: 'Copy'}, Paste: {key: 'Paste', text: 'Paste'}, Find: {key: 'Find', text: 'Find'}, Select: {key: 'Select', text: 'Select'}, Open: {key: 'Open', text: 'Open'}, Props: {key: 'Props', text: 'Props'}, Convert: {key: 'Convert', text: 'Convert'}, NonConvert: {key: 'NonConvert', text: 'NonConvert'}, KanaMode: {key: 'KanaMode', text: 'Kana Mode'}, Lang1: {key: 'HangulMode', text: 'Lang 1'}, Lang2: {key: 'HanjaMode', text: 'Lang 2'}, Lang3: {key: 'Katakana', text: 'Lang 3'}, Lang4: {key: 'Hiragana', text: 'Lang 4'}, Lang5: {key: 'ZenkakuHankaku', text: 'Lang 5'}, IntlBackslash: {key: '\\', text: 'Intl Backslash'}, IntlRo: {key: 'Unidentified', text: 'Intl Ro'}, IntlYen: {key: '¥', text: 'Intl Yen'} }; for (let i = 0; i <= 9; i++) { SPECIAL_KEY_DATA[`Numpad${i}`] = { key: String(i), text: `NumPad ${i}`, location: 3 }; } for (let i = 1; i <= 24; i++) { SPECIAL_KEY_DATA[`F${i}`] = { key: `F${i}`, text: `F${i}` }; } const KEY_DISPLAY = Object.create(null); const KEY_INTENT = Object.create(null); for (const [code, char] of Object.entries(EN_LETTERS)) { if (/^Key[A-Z]$/.test(code)) { KEY_DISPLAY[code] = char.toUpperCase(); KEY_INTENT[code] = 'letter'; } } const SYMBOL_DISPLAY = { Backquote: '` / Ё', Digit1: '1 / !', Digit2: '2 / @', Digit3: '3 / #', Digit4: '4 / $', Digit5: '5 / %', Digit6: '6 / ^', Digit7: '7 / &', Digit8: '8 / *', Digit9: '9 / (', Digit0: '0 / )', Minus: '- / _', Equal: '= / +', BracketLeft: '[ / {', BracketRight: '] / }', Backslash: '\\ / |', Semicolon: '; / :', Quote: "' / \"", Comma: ', / <', Period: '. / >', Slash: '/ / ?' }; for (const [code, text] of Object.entries(SYMBOL_DISPLAY)) { KEY_DISPLAY[code] = text; KEY_INTENT[code] = 'symbol'; } for (const [code, data] of Object.entries(SPECIAL_KEY_DATA)) { KEY_DISPLAY[code] = data.text; if (!KEY_INTENT[code]) { KEY_INTENT[code] = 'key'; } } const MENU_KEY_CODES = [ 'Escape', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20', 'F21', 'F22', 'F23', 'F24', 'Backquote', 'Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5', 'Digit6', 'Digit7', 'Digit8', 'Digit9', 'Digit0', 'Minus', 'Equal', 'Backspace', 'Tab', 'KeyQ', 'KeyW', 'KeyE', 'KeyR', 'KeyT', 'KeyY', 'KeyU', 'KeyI', 'KeyO', 'KeyP', 'BracketLeft', 'BracketRight', 'Backslash', 'CapsLock', 'KeyA', 'KeyS', 'KeyD', 'KeyF', 'KeyG', 'KeyH', 'KeyJ', 'KeyK', 'KeyL', 'Semicolon', 'Quote', 'Enter', 'ShiftLeft', 'KeyZ', 'KeyX', 'KeyC', 'KeyV', 'KeyB', 'KeyN', 'KeyM', 'Comma', 'Period', 'Slash', 'ShiftRight', 'ControlLeft', 'MetaLeft', 'AltLeft', 'Space', 'AltRight', 'MetaRight', 'ContextMenu', 'ControlRight', 'PrintScreen', 'ScrollLock', 'Pause', 'Insert', 'Home', 'PageUp', 'Delete', 'End', 'PageDown', 'ArrowUp', 'ArrowLeft', 'ArrowDown', 'ArrowRight', 'NumLock', 'NumpadDivide', 'NumpadMultiply', 'NumpadSubtract', 'Numpad7', 'Numpad8', 'Numpad9', 'NumpadAdd', 'Numpad4', 'Numpad5', 'Numpad6', 'Numpad1', 'Numpad2', 'Numpad3', 'NumpadEnter', 'Numpad0', 'NumpadDecimal', 'NumpadComma', 'NumpadEqual', 'AudioVolumeMute', 'AudioVolumeDown', 'AudioVolumeUp', 'MediaTrackPrevious', 'MediaTrackNext', 'MediaPlayPause', 'MediaStop', 'BrowserBack', 'BrowserForward', 'BrowserRefresh', 'BrowserStop', 'BrowserSearch', 'BrowserFavorites', 'BrowserHome', 'LaunchMail', 'LaunchApp1', 'LaunchApp2', 'Power', 'Sleep', 'WakeUp', 'Eject', 'Help', 'Again', 'Undo', 'Cut', 'Copy', 'Paste', 'Find', 'Select', 'Open', 'Props', 'Convert', 'NonConvert', 'KanaMode', 'Lang1', 'Lang2', 'Lang3', 'Lang4', 'Lang5', 'IntlBackslash', 'IntlRo', 'IntlYen' ]; const MOUSE_MENU_ITEMS = [ { text: 'ЛКМ', value: 'M|left' }, { text: 'СКМ', value: 'M|middle' }, { text: 'ПКМ', value: 'M|right' }, { text: 'Дополнительные кнопки мыши', value: 'M|extra' }, { text: 'Скроллинг вверх', value: 'W|up' }, { text: 'Скроллинг вниз', value: 'W|down' } ]; const ASSET_MENU_ITEMS = MENU_KEY_CODES.map(code => ({ text: KEY_DISPLAY[code] || code, value: `${ KEY_INTENT[code] === 'letter' ? 'L' : KEY_INTENT[code] === 'symbol' ? 'S' : 'K' }|${code}` })).concat(MOUSE_MENU_ITEMS); const CODE_BY_EN_LETTER = Object.create(null); const CODE_BY_RU_LETTER = Object.create(null); for (const [code, letter] of Object.entries(EN_LETTERS)) { CODE_BY_EN_LETTER[letter] = code; } for (const [code, letter] of Object.entries(RU_LETTERS)) { CODE_BY_RU_LETTER[letter] = code; } const SYMBOL_LOOKUP = Object.create(null); function addSymbolLookup(layout, table) { for (const [code, pair] of Object.entries(table)) { pair.forEach((char, shiftIndex) => { const key = char; if (!SYMBOL_LOOKUP[key]) { SYMBOL_LOOKUP[key] = []; } SYMBOL_LOOKUP[key].push({ code, layout, shift: shiftIndex === 1 }); }); } } addSymbolLookup('en', EN_SYMBOLS); addSymbolLookup('ru', RU_SYMBOLS); const ALIASES = Object.create(null); function normalizeAlias(value) { return String(value ?? '') .trim() .toLowerCase() .replace(/\s+/g, ' '); } function addAlias(alias, value) { ALIASES[normalizeAlias(alias)] = value; } for (const code of MENU_KEY_CODES) { const intent = KEY_INTENT[code] === 'letter' ? 'L' : KEY_INTENT[code] === 'symbol' ? 'S' : 'K'; addAlias(code, `${intent}|${code}`); addAlias(KEY_DISPLAY[code] || code, `${intent}|${code}`); } for (const [code, letter] of Object.entries(EN_LETTERS)) { addAlias(letter, `L|${code}`); addAlias(letter.toUpperCase(), `L|${code}`); } for (const [code, letter] of Object.entries(RU_LETTERS)) { addAlias(letter, `L|${code}`); addAlias(letter.toUpperCase(), `L|${code}`); } const COMMON_ALIASES = { 'esc': 'K|Escape', 'escape': 'K|Escape', 'эскейп': 'K|Escape', 'пробел': 'K|Space', 'space': 'K|Space', 'spacebar': 'K|Space', 'enter': 'K|Enter', 'ввод': 'K|Enter', 'backspace': 'K|Backspace', 'бэкспейс': 'K|Backspace', 'tab': 'K|Tab', 'таб': 'K|Tab', 'caps lock': 'K|CapsLock', 'capslock': 'K|CapsLock', 'капс лок': 'K|CapsLock', 'shift': 'K|ShiftLeft', 'шифт': 'K|ShiftLeft', 'ctrl': 'K|ControlLeft', 'control': 'K|ControlLeft', 'контрол': 'K|ControlLeft', 'alt': 'K|AltLeft', 'альт': 'K|AltLeft', 'win': 'K|MetaLeft', 'windows': 'K|MetaLeft', 'meta': 'K|MetaLeft', 'вин': 'K|MetaLeft', 'стрелка вверх': 'K|ArrowUp', 'arrow up': 'K|ArrowUp', 'up': 'K|ArrowUp', 'стрелка вниз': 'K|ArrowDown', 'arrow down': 'K|ArrowDown', 'down': 'K|ArrowDown', 'стрелка влево': 'K|ArrowLeft', 'arrow left': 'K|ArrowLeft', 'left arrow': 'K|ArrowLeft', 'стрелка вправо': 'K|ArrowRight', 'arrow right': 'K|ArrowRight', 'right arrow': 'K|ArrowRight', 'delete': 'K|Delete', 'del': 'K|Delete', 'удалить': 'K|Delete', 'insert': 'K|Insert', 'ins': 'K|Insert', 'page up': 'K|PageUp', 'pgup': 'K|PageUp', 'page down': 'K|PageDown', 'pgdn': 'K|PageDown', 'print screen': 'K|PrintScreen', 'prtsc': 'K|PrintScreen', 'scroll lock': 'K|ScrollLock', 'pause': 'K|Pause', 'break': 'K|Pause', 'num lock': 'K|NumLock', 'numlock': 'K|NumLock', 'лкм': 'M|left', 'левая кнопка мыши': 'M|left', 'left mouse': 'M|left', 'mouse left': 'M|left', 'lmb': 'M|left', 'скм': 'M|middle', 'средняя кнопка мыши': 'M|middle', 'middle mouse': 'M|middle', 'mouse middle': 'M|middle', 'mmb': 'M|middle', 'пкм': 'M|right', 'правая кнопка мыши': 'M|right', 'right mouse': 'M|right', 'mouse right': 'M|right', 'rmb': 'M|right', 'дополнительные кнопки мыши': 'M|extra', 'дополнительный кнопки мыши': 'M|extra', 'extra mouse buttons': 'M|extra', 'mouse extra': 'M|extra', 'скроллинг вверх': 'W|up', 'скролл вверх': 'W|up', 'scroll up': 'W|up', 'wheel up': 'W|up', 'скроллинг вниз': 'W|down', 'скролл вниз': 'W|down', 'scroll down': 'W|down', 'wheel down': 'W|down' }; for (const [alias, value] of Object.entries(COMMON_ALIASES)) { addAlias(alias, value); } for (let i = 0; i <= 9; i++) { addAlias(`numpad ${i}`, `K|Numpad${i}`); addAlias(`num pad ${i}`, `K|Numpad${i}`); addAlias(`нумпад ${i}`, `K|Numpad${i}`); addAlias(`нум пад ${i}`, `K|Numpad${i}`); } function parseEncodedAsset(encoded, originalText) { const [type, value] = String(encoded).split('|'); if (type === 'L' || type === 'S' || type === 'K') { const intent = type === 'L' ? 'letter' : type === 'S' ? 'symbol' : 'key'; return { kind: 'keyboard', code: value, intent, keyHint: originalText || '', assetKey: `kbd:${value}`, signature: `${type}|${value}` }; } if (type === 'M') { return { kind: 'mouse', buttonName: value, assetKey: `mouse:${value}`, signature: `M|${value}` }; } if (type === 'W') { return { kind: 'wheel', direction: value, assetKey: `wheel:${value}`, signature: `W|${value}` }; } return null; } function resolveAsset(value) { const original = String(value ?? '').trim(); if (!original) { return { kind: 'rawkey', key: '', code: '', intent: 'key', assetKey: 'raw:', signature: 'R|' }; } if (/^[LSKMW]\|/.test(original)) { return parseEncodedAsset(original, original); } const alias = ALIASES[normalizeAlias(original)]; if (alias) { return parseEncodedAsset(alias, original); } const lower = original.toLowerCase(); if (original.length === 1) { if (CODE_BY_EN_LETTER[lower]) { return parseEncodedAsset( `L|${CODE_BY_EN_LETTER[lower]}`, original ); } if (CODE_BY_RU_LETTER[lower]) { return parseEncodedAsset( `L|${CODE_BY_RU_LETTER[lower]}`, original ); } const symbols = SYMBOL_LOOKUP[original] || SYMBOL_LOOKUP[lower]; if (symbols && symbols.length) { return parseEncodedAsset( `S|${symbols[0].code}`, original ); } } const fMatch = /^f(\d{1,2})$/i.exec(original); if (fMatch) { const n = Number(fMatch[1]); if (n >= 1 && n <= 24) { return parseEncodedAsset( `K|F${n}`, original ); } } const npMatch = /^(?:num\s*pad|numpad|нум\s*пад|нумпад)\s*([0-9])$/i.exec(original); if (npMatch) { return parseEncodedAsset( `K|Numpad${npMatch[1]}`, original ); } const codeLike = MENU_KEY_CODES.find( code => code.toLowerCase() === lower ); if (codeLike) { const intent = KEY_INTENT[codeLike] === 'letter' ? 'L' : KEY_INTENT[codeLike] === 'symbol' ? 'S' : 'K'; return parseEncodedAsset( `${intent}|${codeLike}`, original ); } return { kind: 'rawkey', key: original, code: original, intent: 'key', assetKey: `raw:${normalizeAlias(original)}`, signature: `R|${normalizeAlias(original)}` }; } function displayAssetKey(assetKey) { if (assetKey.startsWith('kbd:')) { const code = assetKey.slice(4); return KEY_DISPLAY[code] || code; } if (assetKey === 'mouse:left') { return 'ЛКМ'; } if (assetKey === 'mouse:middle') { return 'СКМ'; } if (assetKey === 'mouse:right') { return 'ПКМ'; } if (assetKey === 'mouse:extra') { return 'Дополнительные кнопки мыши'; } if (assetKey === 'wheel:up') { return 'Скроллинг вверх'; } if (assetKey === 'wheel:down') { return 'Скроллинг вниз'; } if (assetKey.startsWith('raw:')) { return assetKey.slice(4); } return assetKey; } function keyboardKeyForCode(code) { if (SPECIAL_KEY_DATA[code]) { return SPECIAL_KEY_DATA[code].key; } if (EN_LETTERS[code]) { return EN_LETTERS[code]; } if (EN_SYMBOLS[code]) { return EN_SYMBOLS[code][0]; } return code; } function keyboardLocationForCode(code) { if ( SPECIAL_KEY_DATA[code] && SPECIAL_KEY_DATA[code].location ) { return SPECIAL_KEY_DATA[code].location; } if (code.startsWith('Numpad')) { return 3; } return 0; } function mouseButtonName(button) { if (button === 0) { return 'left'; } if (button === 1) { return 'middle'; } if (button === 2) { return 'right'; } return 'extra'; } function mouseButtonMask(button) { if (button === 0) { return 1; } if (button === 1) { return 4; } if (button === 2) { return 2; } if (button === 3) { return 8; } if (button === 4) { return 16; } const shift = Math.max( 0, Math.min(30, button) ); return (1 << shift) >>> 0; } function xmlLabel(text) { return { blockType: BlockType.XML, xml: `<label text="${ text .replace(/&/g, '&') .replace(/"/g, '"') .replace(/</g, '<') .replace(/>/g, '>') }"/>` }; } function xmlGap(lines = 1) { return { blockType: BlockType.XML, xml: `<sep gap="${ Math.max(1, lines) * 12 }"/>` }; } class MacrosExtension { constructor() { this.simulated = new Map(); this.physicalKeys = new Map(); this.physicalMouseButtons = new Set(); this.physicalWheelUntil = { up: 0, down: 0 }; this.macros = new Map(); this.generatedEvents = new WeakSet(); this.letterSimulationMode = LETTER_MODE_BOTH; this.symbolSimulationShiftMode = SHIFT_MODE_BOTH; this.symbolSimulationLayout = SYMBOL_LAYOUT_BOTH; this.letterReadingMode = READ_LETTER_MODE_BOTH; this.symbolReadingShiftMode = SHIFT_MODE_BOTH; this.symbolReadingLayout = READ_SYMBOL_LAYOUT_BOTH; this._bindInputListeners(); } getInfo() { const stringArg = ( defaultValue = '' ) => ({ type: ArgumentType.STRING, defaultValue }); const assetMenuArg = () => ({ type: ArgumentType.STRING, menu: 'ASSET_MENU' }); return { id: 'macros058aa8', name: 'Макросы', color1: COLOR, blocks: [ xmlLabel('Симуляция нажатия'), // 1 { opcode: 'simulateMenu', blockType: BlockType.COMMAND, text: 'симулировать нажатие [ASSET]', arguments: { ASSET: assetMenuArg() } }, // 2 { opcode: 'stopSimulateMenu', blockType: BlockType.COMMAND, text: 'остановить симуляцию нажатия [ASSET]', arguments: { ASSET: assetMenuArg() } }, // 3 { opcode: 'isSimulatingMenu', blockType: BlockType.BOOLEAN, text: '[ASSET] симулирует нажатие?', arguments: { ASSET: assetMenuArg() } }, xmlGap(1), // 4 { opcode: 'simulateText', blockType: BlockType.COMMAND, text: 'симулировать нажатие [ASSET]', arguments: { ASSET: stringArg('A') } }, // 5 { opcode: 'stopSimulateText', blockType: BlockType.COMMAND, text: 'остановить симуляцию нажатия [ASSET]', arguments: { ASSET: stringArg('A') } }, // 6 { opcode: 'isSimulatingText', blockType: BlockType.BOOLEAN, text: '[ASSET] симулирует нажатие?', arguments: { ASSET: stringArg('A') } }, xmlGap(1), // 7 { opcode: 'allSimulated', blockType: BlockType.REPORTER, text: 'все симулируемые активы', disableMonitor: true }, // 8 { opcode: 'simulatedCount', blockType: BlockType.REPORTER, text: 'количество симулируемых активов', disableMonitor: true }, xmlGap(1), // 9 { opcode: 'setLetterSimulationMode', blockType: BlockType.COMMAND, text: 'установить тип симуляции нажатия клавиш-букв [MODE]', arguments: { MODE: { type: ArgumentType.STRING, menu: 'LETTER_SIM_MODE' } } }, // 10 { opcode: 'setSymbolSimulationShiftMode', blockType: BlockType.COMMAND, text: 'установить тип симуляции нажатия клавиш-символов [MODE]', arguments: { MODE: { type: ArgumentType.STRING, menu: 'SHIFT_MODE' } } }, // 11 { opcode: 'setSymbolSimulationLayout', blockType: BlockType.COMMAND, text: 'установить раскладку симуляции нажатия клавиш-символов [MODE]', arguments: { MODE: { type: ArgumentType.STRING, menu: 'SYMBOL_SIM_LAYOUT' } } }, xmlGap(1), // 12 { opcode: 'getLetterSimulationMode', blockType: BlockType.REPORTER, text: 'тип симуляции нажатия клавиш-букв', disableMonitor: true }, // 13 { opcode: 'getSymbolSimulationLayout', blockType: BlockType.REPORTER, text: 'раскладка симуляции нажатия клавиш-символов', disableMonitor: true }, // 14 { opcode: 'getSymbolSimulationShiftMode', blockType: BlockType.REPORTER, text: 'тип симуляции нажатия клавиш-символов', disableMonitor: true }, xmlGap(1), xmlLabel('Сенсоры нажатий'), // 15 { opcode: 'isPressedMenu', blockType: BlockType.BOOLEAN, text: 'актив [ASSET] нажат?', arguments: { ASSET: assetMenuArg() } }, // 16 { opcode: 'isPressedText', blockType: BlockType.BOOLEAN, text: 'актив [ASSET] нажат?', arguments: { ASSET: stringArg('A') } }, xmlGap(1), // 17 { opcode: 'allPressed', blockType: BlockType.REPORTER, text: 'все нажатые активы', disableMonitor: true }, // 18 { opcode: 'pressedCount', blockType: BlockType.REPORTER, text: 'количество нажатых активов', disableMonitor: true }, xmlGap(1), // 19 { opcode: 'setLetterReadingMode', blockType: BlockType.COMMAND, text: 'установить тип считывания нажатых клавиш-букв [MODE]', arguments: { MODE: { type: ArgumentType.STRING, menu: 'LETTER_READ_MODE' } } }, // 20 { opcode: 'setSymbolReadingShiftMode', blockType: BlockType.COMMAND, text: 'установить тип считывания нажатых клавиш-символов [MODE]', arguments: { MODE: { type: ArgumentType.STRING, menu: 'SHIFT_MODE' } } }, // 21 { opcode: 'setSymbolReadingLayout', blockType: BlockType.COMMAND, text: 'установить раскладку считывания нажатых клавиш-символов [MODE]', arguments: { MODE: { type: ArgumentType.STRING, menu: 'SYMBOL_READ_LAYOUT' } } }, xmlGap(1), // 22 { opcode: 'getLetterReadingMode', blockType: BlockType.REPORTER, text: 'тип считывания нажатий клавиш-букв', disableMonitor: true }, // 23 { opcode: 'getSymbolReadingShiftMode', blockType: BlockType.REPORTER, text: 'тип считываний нажатий клавиш-символов', disableMonitor: true }, // 24 { opcode: 'getSymbolReadingLayout', blockType: BlockType.REPORTER, text: 'раскладка считывания нажатий клавиш-символов', disableMonitor: true }, xmlGap(1), xmlLabel('Макросы'), // 25 { opcode: 'startMacroRecording', blockType: BlockType.COMMAND, text: 'начать запись макроса [NAME]', arguments: { NAME: stringArg('макрос 1') } }, // 26 { opcode: 'stopMacroRecording', blockType: BlockType.COMMAND, text: 'остановить запись макроса [NAME]', arguments: { NAME: stringArg('макрос 1') } }, // 27 { opcode: 'toggleMacroRecording', blockType: BlockType.COMMAND, text: 'продолжить/приостановить запись макроса [NAME]', arguments: { NAME: stringArg('макрос 1') } }, // 28 { opcode: 'clearMacro', blockType: BlockType.COMMAND, text: 'очистить макрос [NAME]', arguments: { NAME: stringArg('макрос 1') } }, xmlGap(1), // 29 { opcode: 'toggleMacroPlayback', blockType: BlockType.COMMAND, text: 'воспроизвести/приостановить макрос [NAME]', arguments: { NAME: stringArg('макрос 1') } }, xmlGap(1), // 30 { opcode: 'macroExists', blockType: BlockType.BOOLEAN, text: 'макрос [NAME] существует?', arguments: { NAME: stringArg('макрос 1') } }, // 31 { opcode: 'macroContainsMenu', blockType: BlockType.BOOLEAN, text: 'макрос [NAME] содержит [ASSET]', arguments: { NAME: stringArg('макрос 1'), ASSET: assetMenuArg() } }, // 32 { opcode: 'macroContainsText', blockType: BlockType.BOOLEAN, text: 'макрос [NAME] содержит [ASSET]', arguments: { NAME: stringArg('макрос 1'), ASSET: stringArg('A') } }, xmlGap(1), // 33 { opcode: 'macroLength', blockType: BlockType.REPORTER, text: 'длина макроса [NAME]', arguments: { NAME: stringArg('макрос 1') }, disableMonitor: true }, // 34 { opcode: 'macroCount', blockType: BlockType.REPORTER, text: 'количество записанных макросов', disableMonitor: true } ], menus: { ASSET_MENU: { acceptReporters: true, items: ASSET_MENU_ITEMS }, LETTER_SIM_MODE: { acceptReporters: true, items: [ LETTER_MODE_BOTH, LETTER_MODE_EN, LETTER_MODE_RU ] }, SHIFT_MODE: { acceptReporters: true, items: [ SHIFT_MODE_BOTH, SHIFT_MODE_ONLY, SHIFT_MODE_NONE ] }, SYMBOL_SIM_LAYOUT: { acceptReporters: true, items: [ SYMBOL_LAYOUT_BOTH, SYMBOL_LAYOUT_EN, SYMBOL_LAYOUT_RU ] }, LETTER_READ_MODE: { acceptReporters: true, items: [ READ_LETTER_MODE_BOTH, READ_LETTER_MODE_EN, READ_LETTER_MODE_RU ] }, SYMBOL_READ_LAYOUT: { acceptReporters: true, items: [ READ_SYMBOL_LAYOUT_BOTH, READ_SYMBOL_LAYOUT_EN, READ_SYMBOL_LAYOUT_RU ] } } }; } simulateMenu(args) { this._startSimulation( resolveAsset(args.ASSET) ); } stopSimulateMenu(args) { this._stopSimulation( resolveAsset(args.ASSET) ); } isSimulatingMenu(args) { return this._isSimulating( resolveAsset(args.ASSET) ); } simulateText(args) { this._startSimulation( resolveAsset(args.ASSET) ); } stopSimulateText(args) { this._stopSimulation( resolveAsset(args.ASSET) ); } isSimulatingText(args) { return this._isSimulating( resolveAsset(args.ASSET) ); } allSimulated() { return Array.from( this.simulated.keys() ) .map(displayAssetKey) .join('; '); } simulatedCount() { return this.simulated.size; } setLetterSimulationMode(args) { const value = String(args.MODE); if ( [ LETTER_MODE_BOTH, LETTER_MODE_EN, LETTER_MODE_RU ].includes(value) ) { this.letterSimulationMode = value; } } setSymbolSimulationShiftMode(args) { const value = String(args.MODE); if ( [ SHIFT_MODE_BOTH, SHIFT_MODE_ONLY, SHIFT_MODE_NONE ].includes(value) ) { this.symbolSimulationShiftMode = value; } } setSymbolSimulationLayout(args) { const value = String(args.MODE); if ( [ SYMBOL_LAYOUT_BOTH, SYMBOL_LAYOUT_EN, SYMBOL_LAYOUT_RU ].includes(value) ) { this.symbolSimulationLayout = value; } } getLetterSimulationMode() { return this.letterSimulationMode; } getSymbolSimulationLayout() { return this.symbolSimulationLayout; } getSymbolSimulationShiftMode() { return this.symbolSimulationShiftMode; } isPressedMenu(args) { return this._isPressed( resolveAsset(args.ASSET) ); } isPressedText(args) { return this._isPressed( resolveAsset(args.ASSET) ); } allPressed() { return Array.from( this._collectPressedAssetKeys() ) .map(displayAssetKey) .join('; '); } pressedCount() { return this ._collectPressedAssetKeys() .size; } setLetterReadingMode(args) { const value = String(args.MODE); if ( [ READ_LETTER_MODE_BOTH, READ_LETTER_MODE_EN, READ_LETTER_MODE_RU ].includes(value) ) { this.letterReadingMode = value; } } setSymbolReadingShiftMode(args) { const value = String(args.MODE); if ( [ SHIFT_MODE_BOTH, SHIFT_MODE_ONLY, SHIFT_MODE_NONE ].includes(value) ) { this.symbolReadingShiftMode = value; } } setSymbolReadingLayout(args) { const value = String(args.MODE); if ( [ READ_SYMBOL_LAYOUT_BOTH, READ_SYMBOL_LAYOUT_EN, READ_SYMBOL_LAYOUT_RU ].includes(value) ) { this.symbolReadingLayout = value; } } getLetterReadingMode() { return this.letterReadingMode; } getSymbolReadingShiftMode() { return this.symbolReadingShiftMode; } getSymbolReadingLayout() { return this.symbolReadingLayout; } startMacroRecording(args) { const name = String(args.NAME ?? ''); const old = this.macros.get(name); if (old) { this._stopPlayback(old); } this.macros.set(name, { name, events: [], recording: true, paused: false, elapsed: 0, segmentStart: performance.now(), duration: 0, playback: this._newPlaybackState() }); } stopMacroRecording(args) { const macro = this.macros.get( String(args.NAME ?? '') ); if ( !macro || !macro.recording ) { return; } if (!macro.paused) { macro.elapsed += performance.now() - macro.segmentStart; } macro.recording = false; macro.paused = false; macro.duration = Math.max( macro.duration, macro.elapsed, this._lastMacroEventTime(macro) ); } toggleMacroRecording(args) { const name = String(args.NAME ?? ''); const macro = this.macros.get(name); if (!macro) { return; } const now = performance.now(); if (!macro.recording) { macro.recording = true; macro.paused = false; macro.elapsed = Math.max( macro.duration, this._lastMacroEventTime(macro) ); macro.segmentStart = now; return; } if (macro.paused) { macro.paused = false; macro.segmentStart = now; } else { macro.elapsed += now - macro.segmentStart; macro.duration = Math.max( macro.duration, macro.elapsed, this._lastMacroEventTime(macro) ); macro.paused = true; } } clearMacro(args) { const name = String(args.NAME ?? ''); const macro = this.macros.get(name); if (!macro) { return; } this._stopPlayback(macro); this.macros.delete(name); } toggleMacroPlayback(args) { const macro = this.macros.get( String(args.NAME ?? '') ); if ( !macro || macro.events.length === 0 ) { return; } if (!macro.playback.playing) { this._startPlayback(macro); } else if ( macro.playback.paused ) { this._resumePlayback(macro); } else { this._pausePlayback(macro); } } macroExists(args) { return this.macros.has( String(args.NAME ?? '') ); } macroContainsMenu(args) { return this._macroContains( String(args.NAME ?? ''), resolveAsset(args.ASSET) ); } macroContainsText(args) { return this._macroContains( String(args.NAME ?? ''), resolveAsset(args.ASSET) ); } macroLength(args) { const macro = this.macros.get( String(args.NAME ?? '') ); if (!macro) { return 0; } let duration = Math.max( macro.duration, this._lastMacroEventTime(macro) ); if ( macro.recording && !macro.paused ) { duration = Math.max( duration, macro.elapsed + ( performance.now() - macro.segmentStart ) ); } else if (macro.recording) { duration = Math.max( duration, macro.elapsed ); } // Длина макроса возвращается в секундах. return Math.round( (duration / 1000) * 1000 ) / 1000; } macroCount() { return this.macros.size; } _bindInputListeners() { window.addEventListener( 'keydown', event => { if ( this.generatedEvents.has(event) ) { return; } const code = event.code || this._codeFromEventKey( event.key ); const assetKey = code ? `kbd:${code}` : `raw:${normalizeAlias(event.key)}`; this.physicalKeys.set( assetKey, { code: code || '', key: event.key || '', shiftKey: !!event.shiftKey, ctrlKey: !!event.ctrlKey, altKey: !!event.altKey, metaKey: !!event.metaKey, location: event.location || keyboardLocationForCode( code || '' ), repeat: !!event.repeat } ); this._recordInputEvent({ t: 0, type: 'keydown', assetKey, code: code || '', key: event.key || '', shiftKey: !!event.shiftKey, ctrlKey: !!event.ctrlKey, altKey: !!event.altKey, metaKey: !!event.metaKey, location: event.location || 0, repeat: !!event.repeat }); }, true ); window.addEventListener( 'keyup', event => { if ( this.generatedEvents.has(event) ) { return; } const code = event.code || this._codeFromEventKey( event.key ); const assetKey = code ? `kbd:${code}` : `raw:${normalizeAlias(event.key)}`; this.physicalKeys.delete( assetKey ); this._recordInputEvent({ t: 0, type: 'keyup', assetKey, code: code || '', key: event.key || '', shiftKey: !!event.shiftKey, ctrlKey: !!event.ctrlKey, altKey: !!event.altKey, metaKey: !!event.metaKey, location: event.location || 0, repeat: false }); }, true ); window.addEventListener( 'mousedown', event => { if ( this.generatedEvents.has(event) ) { return; } this.physicalMouseButtons.add( event.button ); const buttonName = mouseButtonName(event.button); this._recordInputEvent({ t: 0, type: 'mousedown', assetKey: `mouse:${buttonName}`, button: event.button, clientX: event.clientX, clientY: event.clientY }); }, true ); window.addEventListener( 'mouseup', event => { if ( this.generatedEvents.has(event) ) { return; } this.physicalMouseButtons.delete( event.button ); const buttonName = mouseButtonName(event.button); this._recordInputEvent({ t: 0, type: 'mouseup', assetKey: `mouse:${buttonName}`, button: event.button, clientX: event.clientX, clientY: event.clientY }); }, true ); window.addEventListener( 'wheel', event => { if ( this.generatedEvents.has(event) ) { return; } const direction = event.deltaY < 0 ? 'up' : 'down'; this.physicalWheelUntil[ direction ] = performance.now() + WHEEL_PULSE_MS; this._recordInputEvent({ t: 0, type: 'wheel', assetKey: `wheel:${direction}`, direction, deltaX: event.deltaX || 0, deltaY: event.deltaY || ( direction === 'up' ? -SIMULATED_WHEEL_DELTA : SIMULATED_WHEEL_DELTA ), deltaZ: event.deltaZ || 0, deltaMode: event.deltaMode || 0 }); }, { capture: true, passive: true } ); window.addEventListener( 'blur', () => { this.physicalKeys.clear(); this.physicalMouseButtons.clear(); this.physicalWheelUntil.up = 0; this.physicalWheelUntil.down = 0; } ); } _startSimulation(spec) { if (!spec) { return; } if ( this.simulated.has( spec.assetKey ) ) { return; } if (spec.kind === 'wheel') { const record = { spec, interval: null, variants: [] }; const pulse = () => { this._emitWheel( spec.direction === 'up' ? -SIMULATED_WHEEL_DELTA : SIMULATED_WHEEL_DELTA ); }; pulse(); record.interval = setInterval( pulse, SIMULATED_WHEEL_INTERVAL_MS ); this.simulated.set( spec.assetKey, record ); return; } if (spec.kind === 'mouse') { const record = { spec, variants: [] }; this.simulated.set( spec.assetKey, record ); if ( spec.buttonName === 'left' ) { this._emitMouse( 'mousedown', 0 ); } else if ( spec.buttonName === 'middle' ) { this._emitMouse( 'mousedown', 1 ); } else if ( spec.buttonName === 'right' ) { this._emitMouse( 'mousedown', 2 ); } else { /* * «Дополнительные кнопки мыши». * Посылаются сразу несколько * типичных дополнительных button-кодов. */ for ( let button = 3; button <= 7; button++ ) { this._emitMouse( 'mousedown', button ); } } return; } const variants = this._expandKeyboardVariants( spec ); const record = { spec, variants }; this.simulated.set( spec.assetKey, record ); for ( const variant of variants ) { this._emitKeyboard( 'keydown', variant ); } } _stopSimulation(spec) { if (!spec) { return; } const record = this.simulated.get( spec.assetKey ); if (!record) { return; } this.simulated.delete( spec.assetKey ); if ( record.spec.kind === 'wheel' ) { if ( record.interval !== null ) { clearInterval( record.interval ); } return; } if ( record.spec.kind === 'mouse' ) { if ( record.spec.buttonName === 'left' ) { this._emitMouse( 'mouseup', 0 ); } else if ( record.spec.buttonName === 'middle' ) { this._emitMouse( 'mouseup', 1 ); } else if ( record.spec.buttonName === 'right' ) { this._emitMouse( 'mouseup', 2 ); } else { for ( let button = 3; button <= 7; button++ ) { this._emitMouse( 'mouseup', button ); } } return; } for ( const variant of record.variants || [] ) { this._emitKeyboard( 'keyup', variant ); } } _isSimulating(spec) { return ( !!spec && this.simulated.has( spec.assetKey ) ); } _expandKeyboardVariants(spec) { if ( spec.kind === 'rawkey' ) { return [ { code: spec.code || '', key: spec.key || '', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, location: 0, repeat: false } ]; } const code = spec.code; if ( spec.intent === 'letter' ) { const layouts = this.letterSimulationMode === LETTER_MODE_EN ? ['en'] : this.letterSimulationMode === LETTER_MODE_RU ? ['ru'] : ['en', 'ru']; const variants = []; for ( const layout of layouts ) { const key = layout === 'en' ? EN_LETTERS[code] : RU_LETTERS[code]; if (!key) { continue; } variants.push({ code, key, shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, location: keyboardLocationForCode( code ), repeat: false }); } if (variants.length) { return this._dedupeVariants( variants ); } } if ( spec.intent === 'symbol' ) { const layouts = this.symbolSimulationLayout === SYMBOL_LAYOUT_EN ? ['en'] : this.symbolSimulationLayout === SYMBOL_LAYOUT_RU ? ['ru'] : ['en', 'ru']; const shifts = this.symbolSimulationShiftMode === SHIFT_MODE_ONLY ? [true] : this.symbolSimulationShiftMode === SHIFT_MODE_NONE ? [false] : [false, true]; const variants = []; for ( const layout of layouts ) { const table = layout === 'en' ? EN_SYMBOLS : RU_SYMBOLS; const pair = table[code]; if (!pair) { continue; } for ( const shiftKey of shifts ) { const key = pair[ shiftKey ? 1 : 0 ]; if ( typeof key !== 'string' ) { continue; } variants.push({ code, key, shiftKey, ctrlKey: false, altKey: false, metaKey: false, location: keyboardLocationForCode( code ), repeat: false }); } } if (variants.length) { return this._dedupeVariants( variants ); } } return [ { code, key: keyboardKeyForCode( code ), shiftKey: code === 'ShiftLeft' || code === 'ShiftRight', ctrlKey: code === 'ControlLeft' || code === 'ControlRight', altKey: code === 'AltLeft' || code === 'AltRight', metaKey: code === 'MetaLeft' || code === 'MetaRight', location: keyboardLocationForCode( code ), repeat: false } ]; } _dedupeVariants(variants) { const seen = new Set(); return variants.filter(v => { const key = `${v.code}|${v.key}|${ v.shiftKey ? 1 : 0 }|${v.location || 0}`; if (seen.has(key)) { return false; } seen.add(key); return true; }); } _isPressed(spec) { if (!spec) { return false; } const now = performance.now(); if ( spec.kind === 'wheel' ) { if ( this.simulated.has( spec.assetKey ) ) { return true; } if ( this.physicalWheelUntil[ spec.direction ] > now ) { return true; } for ( const macro of this.macros.values() ) { if ( macro.playback.playing && macro.playback.wheelUntil[ spec.direction ] > now ) { return true; } } return false; } if ( spec.kind === 'mouse' ) { if ( this.simulated.has( spec.assetKey ) ) { return true; } if ( spec.buttonName === 'left' && this.physicalMouseButtons.has(0) ) { return true; } if ( spec.buttonName === 'middle' && this.physicalMouseButtons.has(1) ) { return true; } if ( spec.buttonName === 'right' && this.physicalMouseButtons.has(2) ) { return true; } if ( spec.buttonName === 'extra' ) { for ( const button of this.physicalMouseButtons ) { if (button >= 3) { return true; } } } return this._isMacroAssetHeld( spec.assetKey ); } if ( spec.kind === 'rawkey' ) { const wanted = normalizeAlias(spec.key); for ( const state of this.physicalKeys.values() ) { if ( normalizeAlias( state.key ) === wanted || normalizeAlias( state.code ) === wanted ) { return true; } } for ( const record of this.simulated.values() ) { if ( record.spec.kind === 'rawkey' && record.spec.assetKey === spec.assetKey ) { return true; } } for ( const macro of this.macros.values() ) { for ( const state of macro.playback.held.values() ) { if ( state.kind === 'keyboard' && ( normalizeAlias( state.key ) === wanted || normalizeAlias( state.code ) === wanted ) ) { return true; } } } return false; } const states = []; const physical = this.physicalKeys.get( spec.assetKey ); if (physical) { states.push(physical); } const simulated = this.simulated.get( spec.assetKey ); if ( simulated && Array.isArray( simulated.variants ) ) { states.push( ...simulated.variants ); } for ( const macro of this.macros.values() ) { const held = macro.playback.held.get( spec.assetKey ); if ( held && held.kind === 'keyboard' ) { states.push(held); } } return states.some( state => this._keyStateAcceptedByReadingMode( spec, state ) ); } _keyStateAcceptedByReadingMode( spec, state ) { if (!state) { return false; } if ( spec.intent === 'key' ) { return true; } if ( spec.intent === 'letter' ) { const key = String( state.key || '' ).toLowerCase(); const en = EN_LETTERS[spec.code]; const ru = RU_LETTERS[spec.code]; const isEn = !!en && key === en.toLowerCase(); const isRu = !!ru && key === ru.toLowerCase(); if ( this.letterReadingMode === READ_LETTER_MODE_EN ) { return isEn; } if ( this.letterReadingMode === READ_LETTER_MODE_RU ) { return isRu; } return isEn || isRu; } if ( spec.intent === 'symbol' ) { const shiftOk = this.symbolReadingShiftMode === SHIFT_MODE_ONLY ? !!state.shiftKey : this.symbolReadingShiftMode === SHIFT_MODE_NONE ? !state.shiftKey : true; if (!shiftOk) { return false; } const key = String(state.key || ''); const enPair = EN_SYMBOLS[spec.code]; const ruPair = RU_SYMBOLS[spec.code]; const enMatch = !!enPair && enPair.includes(key); const ruMatch = !!ruPair && ruPair.includes(key); if ( this.symbolReadingLayout === READ_SYMBOL_LAYOUT_EN ) { return enMatch; } if ( this.symbolReadingLayout === READ_SYMBOL_LAYOUT_RU ) { return ruMatch; } return ( enMatch || ruMatch ); } return true; } _collectPressedAssetKeys() { const result = new Set(); const now = performance.now(); for ( const assetKey of this.physicalKeys.keys() ) { result.add(assetKey); } for ( const button of this.physicalMouseButtons ) { result.add( `mouse:${ mouseButtonName(button) }` ); } if ( this.physicalWheelUntil.up > now ) { result.add('wheel:up'); } if ( this.physicalWheelUntil.down > now ) { result.add('wheel:down'); } for ( const assetKey of this.simulated.keys() ) { result.add(assetKey); } for ( const macro of this.macros.values() ) { if ( !macro.playback.playing ) { continue; } for ( const assetKey of macro.playback.held.keys() ) { result.add(assetKey); } if ( macro.playback.wheelUntil.up > now ) { result.add('wheel:up'); } if ( macro.playback.wheelUntil.down > now ) { result.add('wheel:down'); } } return result; } _recordInputEvent(eventData) { const now = performance.now(); for ( const macro of this.macros.values() ) { if ( !macro.recording || macro.paused ) { continue; } const t = macro.elapsed + ( now - macro.segmentStart ); const event = Object.assign( {}, eventData, {t} ); macro.events.push(event); macro.duration = Math.max( macro.duration, t ); } } _macroContains( name, spec ) { const macro = this.macros.get(name); if ( !macro || !spec ) { return false; } const wantedRaw = spec.kind === 'rawkey' ? normalizeAlias(spec.key) : ''; return macro.events.some( event => { if ( event.assetKey === spec.assetKey ) { return true; } if ( wantedRaw && event.type.startsWith( 'key' ) ) { return ( normalizeAlias( event.key ) === wantedRaw || normalizeAlias( event.code ) === wantedRaw ); } return false; } ); } _newPlaybackState() { return { playing: false, paused: false, events: [], index: 0, position: 0, startedAt: 0, timer: null, held: new Map(), wheelUntil: { up: 0, down: 0 } }; } _startPlayback(macro) { this._stopPlayback(macro); const p = macro.playback; p.playing = true; p.paused = false; p.events = macro.events.map( event => Object.assign( {}, event ) ); p.index = 0; p.position = 0; p.startedAt = performance.now(); p.held.clear(); p.wheelUntil.up = 0; p.wheelUntil.down = 0; this._scheduleNextPlaybackEvent( macro ); } _pausePlayback(macro) { const p = macro.playback; if ( !p.playing || p.paused ) { return; } p.position = this._playbackPosition(p); p.paused = true; if ( p.timer !== null ) { clearTimeout( p.timer ); } p.timer = null; } _resumePlayback(macro) { const p = macro.playback; if ( !p.playing || !p.paused ) { return; } p.paused = false; p.startedAt = performance.now(); this._scheduleNextPlaybackEvent( macro ); } _stopPlayback(macro) { if ( !macro || !macro.playback ) { return; } const p = macro.playback; if ( p.timer !== null ) { clearTimeout( p.timer ); } p.timer = null; this._releasePlaybackHeld(p); p.playing = false; p.paused = false; p.events = []; p.index = 0; p.position = 0; p.startedAt = 0; p.wheelUntil.up = 0; p.wheelUntil.down = 0; } _playbackPosition(p) { if (!p.playing) { return 0; } if (p.paused) { return p.position; } return ( p.position + ( performance.now() - p.startedAt ) ); } _scheduleNextPlaybackEvent( macro ) { const p = macro.playback; if ( !p.playing || p.paused ) { return; } if ( p.index >= p.events.length ) { this._stopPlayback( macro ); return; } const event = p.events[p.index]; const delay = Math.max( 0, event.t - this._playbackPosition(p) ); p.timer = setTimeout( () => { p.timer = null; if ( !p.playing || p.paused ) { return; } this._playMacroEvent( p, event ); p.index++; this._scheduleNextPlaybackEvent( macro ); }, delay ); } _playMacroEvent( playback, event ) { if ( event.type === 'keydown' ) { playback.held.set( event.assetKey, Object.assign( { kind: 'keyboard' }, event ) ); this._emitKeyboard( 'keydown', event ); return; } if ( event.type === 'keyup' ) { playback.held.delete( event.assetKey ); this._emitKeyboard( 'keyup', event ); return; } if ( event.type === 'mousedown' ) { playback.held.set( event.assetKey, Object.assign( { kind: 'mouse' }, event ) ); this._emitMouse( 'mousedown', event.button, event ); return; } if ( event.type === 'mouseup' ) { playback.held.delete( event.assetKey ); this._emitMouse( 'mouseup', event.button, event ); return; } if ( event.type === 'wheel' ) { const direction = event.direction || ( event.deltaY < 0 ? 'up' : 'down' ); playback.wheelUntil[ direction ] = performance.now() + WHEEL_PULSE_MS; this._emitWheel( event.deltaY || ( direction === 'up' ? -SIMULATED_WHEEL_DELTA : SIMULATED_WHEEL_DELTA ), event ); } } _releasePlaybackHeld( playback ) { for ( const state of playback.held.values() ) { if ( state.kind === 'keyboard' ) { this._emitKeyboard( 'keyup', state ); } else if ( state.kind === 'mouse' ) { this._emitMouse( 'mouseup', state.button, state ); } } playback.held.clear(); } _isMacroAssetHeld( assetKey ) { for ( const macro of this.macros.values() ) { if ( macro.playback.playing && macro.playback.held.has( assetKey ) ) { return true; } } return false; } _lastMacroEventTime( macro ) { if ( !macro || macro.events.length === 0 ) { return 0; } return ( macro.events[ macro.events.length - 1 ].t || 0 ); } _emitKeyboard( type, data ) { const target = this._eventTarget(); const code = String( data.code || '' ); const key = String( data.key ?? keyboardKeyForCode(code) ); const init = { key, code, location: Number( data.location || keyboardLocationForCode( code ) || 0 ), ctrlKey: !!data.ctrlKey || this._modifierSimulated( 'ControlLeft', 'ControlRight' ), shiftKey: !!data.shiftKey || this._modifierSimulated( 'ShiftLeft', 'ShiftRight' ), altKey: !!data.altKey || this._modifierSimulated( 'AltLeft', 'AltRight' ), metaKey: !!data.metaKey || this._modifierSimulated( 'MetaLeft', 'MetaRight' ), repeat: !!data.repeat, isComposing: false, bubbles: true, cancelable: true, composed: true }; const event = new KeyboardEvent( type, init ); this._patchLegacyKeyboardCodes( event, code, key ); this.generatedEvents.add( event ); target.dispatchEvent( event ); } _emitMouse( type, button, source = {} ) { const target = this._eventTarget(); const init = { button: Number(button), buttons: type === 'mousedown' ? mouseButtonMask( Number(button) ) : 0, clientX: Number.isFinite( source.clientX ) ? source.clientX : 0, clientY: Number.isFinite( source.clientY ) ? source.clientY : 0, screenX: Number.isFinite( source.screenX ) ? source.screenX : 0, screenY: Number.isFinite( source.screenY ) ? source.screenY : 0, ctrlKey: !!source.ctrlKey, shiftKey: !!source.shiftKey, altKey: !!source.altKey, metaKey: !!source.metaKey, bubbles: true, cancelable: true, composed: true, detail: 1, view: window }; const event = new MouseEvent( type, init ); this.generatedEvents.add( event ); target.dispatchEvent( event ); } _emitWheel( deltaY, source = {} ) { const target = this._eventTarget(); const event = new WheelEvent( 'wheel', { deltaX: Number( source.deltaX || 0 ), deltaY: Number(deltaY), deltaZ: Number( source.deltaZ || 0 ), deltaMode: Number( source.deltaMode || 0 ), ctrlKey: !!source.ctrlKey, shiftKey: !!source.shiftKey, altKey: !!source.altKey, metaKey: !!source.metaKey, bubbles: true, cancelable: true, composed: true, view: window } ); this.generatedEvents.add( event ); target.dispatchEvent( event ); } _eventTarget() { const active = document.activeElement; if ( active && typeof active.dispatchEvent === 'function' ) { return active; } return document; } _modifierSimulated( leftCode, rightCode ) { return ( this.simulated.has( `kbd:${leftCode}` ) || this.simulated.has( `kbd:${rightCode}` ) ); } _patchLegacyKeyboardCodes( event, code, key ) { const value = this._legacyKeyCode( code, key ); try { Object.defineProperty( event, 'keyCode', { get: () => value } ); Object.defineProperty( event, 'which', { get: () => value } ); Object.defineProperty( event, 'charCode', { get: () => 0 } ); } catch (e) { /* * Некоторые браузеры * не разрешают переопределять * устаревшие свойства. */ } } _legacyKeyCode( code, key ) { if ( /^Key[A-Z]$/.test(code) ) { return code.charCodeAt(3); } if ( /^Digit[0-9]$/.test(code) ) { return code.charCodeAt(5); } if ( /^F\d{1,2}$/.test(code) ) { const n = Number( code.slice(1) ); if ( n >= 1 && n <= 24 ) { return 111 + n; } } const map = { Backspace: 8, Tab: 9, Enter: 13, ShiftLeft: 16, ShiftRight: 16, ControlLeft: 17, ControlRight: 17, AltLeft: 18, AltRight: 18, Pause: 19, CapsLock: 20, Escape: 27, Space: 32, PageUp: 33, PageDown: 34, End: 35, Home: 36, ArrowLeft: 37, ArrowUp: 38, ArrowRight: 39, ArrowDown: 40, Insert: 45, Delete: 46, MetaLeft: 91, MetaRight: 92, ContextMenu: 93, Numpad0: 96, Numpad1: 97, Numpad2: 98, Numpad3: 99, Numpad4: 100, Numpad5: 101, Numpad6: 102, Numpad7: 103, Numpad8: 104, Numpad9: 105, NumpadMultiply: 106, NumpadAdd: 107, NumpadSubtract: 109, NumpadDecimal: 110, NumpadDivide: 111, NumLock: 144, ScrollLock: 145, Semicolon: 186, Equal: 187, Comma: 188, Minus: 189, Period: 190, Slash: 191, Backquote: 192, BracketLeft: 219, Backslash: 220, BracketRight: 221, Quote: 222 }; if ( map[code] !== undefined ) { return map[code]; } if ( key && key.length === 1 ) { return key .toUpperCase() .charCodeAt(0); } return 0; } _codeFromEventKey(key) { const text = String(key || ''); const lower = text.toLowerCase(); if ( CODE_BY_EN_LETTER[lower] ) { return ( CODE_BY_EN_LETTER[ lower ] ); } if ( CODE_BY_RU_LETTER[lower] ) { return ( CODE_BY_RU_LETTER[ lower ] ); } const symbols = SYMBOL_LOOKUP[text] || SYMBOL_LOOKUP[lower]; if ( symbols && symbols.length ) { return symbols[0].code; } for ( const [code, data] of Object.entries( SPECIAL_KEY_DATA ) ) { if ( data.key === text ) { return code; } } return ''; } } Scratch.extensions.register( new MacrosExtension() ); })(Scratch);