diff --git a/src/locales/de-DE/script.json b/src/locales/de-DE/script.json index 0db1194e0..178b691c0 100644 --- a/src/locales/de-DE/script.json +++ b/src/locales/de-DE/script.json @@ -41,6 +41,7 @@ "cron_invalid_expr": "Ungültiger Cron-Ausdruck", "background_script": "Hintergrundskript", "scheduled_script": "Geplantes Skript", + "normal_script": "Benutzerskript", "next_run": "Nächste Ausführung", "background_script_tooltip": "Hintergrundskript. Läuft nach der Aktivierung im Hintergrund.", "scheduled_script_tooltip": "Geplantes Skript. Läuft automatisch zur konfigurierten Zeit.", diff --git a/src/locales/de-DE/settings.json b/src/locales/de-DE/settings.json index 87cd2095a..c4c650dbb 100644 --- a/src/locales/de-DE/settings.json +++ b/src/locales/de-DE/settings.json @@ -79,6 +79,21 @@ "control_script_update_behavior": "Skript-Update-Verhalten kontrollieren", "blacklist_pages_desc": "Verbietet Skripts auf bestimmten Seiten zu laufen, unterstützt Wildcards", "development_tools": "Entwicklertools", + "script_templates": "Skriptvorlagen", + "script_templates_description": "Passe den Anfangscode an, der beim Erstellen eines neuen Skripts erzeugt wird. Betrifft nur danach erstellte Skripte.", + "script_template_saved": "Skriptvorlage gespeichert", + "script_template_reset": "Standard-Skriptvorlage wiederhergestellt", + "script_template_modified": "Geändert", + "script_template_variables": "Verfügbare Variablen", + "script_template_variables_hint": "Eine Variable ohne Wert entfernt die gesamte Zeile; unbekannte Platzhalter bleiben unverändert.", + "script_template_var_name": "Automatisch erzeugter Skriptname", + "script_template_var_match": "Adresse des aktuellen Tabs", + "script_template_var_icon": "Symbol der aktuellen Website", + "script_template_var_domain": "Domain des aktuellen Tabs", + "script_template_var_title": "Titel des aktuellen Tabs", + "script_template_var_date": "Aktuelles Datum; nach dem Doppelpunkt kann ein Format angegeben werden", + "script_template_error_normal_directive": "Eine Vorlage für Benutzerskripte darf @background oder @crontab nicht enthalten", + "script_template_error_missing_directive": "Diese Vorlage muss {{directive}} enthalten", "check_script_code_quality": "Skriptcode-Qualität und Fehler prüfen", "script_run_env": { "title": "Laufzeitumgebung", diff --git a/src/locales/en-US/script.json b/src/locales/en-US/script.json index 21187ae5e..3f75794de 100644 --- a/src/locales/en-US/script.json +++ b/src/locales/en-US/script.json @@ -41,6 +41,7 @@ "cron_invalid_expr": "Invalid cron expression", "background_script": "Background Script", "scheduled_script": "Scheduled Script", + "normal_script": "User Script", "next_run": "Next run", "background_script_tooltip": "Background script. Runs in the background once enabled.", "scheduled_script_tooltip": "Scheduled script. Runs automatically at the configured time.", diff --git a/src/locales/en-US/settings.json b/src/locales/en-US/settings.json index 13bec0644..f04c9f360 100644 --- a/src/locales/en-US/settings.json +++ b/src/locales/en-US/settings.json @@ -79,6 +79,21 @@ "control_script_update_behavior": "Control script update behavior", "blacklist_pages_desc": "Prevent scripts from running on specified pages, supports wildcards", "development_tools": "Development Tools", + "script_templates": "Script Templates", + "script_templates_description": "Customize the initial code generated when creating a new script. Only scripts created afterwards are affected.", + "script_template_saved": "Script template saved", + "script_template_reset": "Default script template restored", + "script_template_modified": "Modified", + "script_template_variables": "Available variables", + "script_template_variables_hint": "A variable that resolves to an empty value removes its entire line; unrecognized placeholders are kept as is.", + "script_template_var_name": "Auto-generated script name", + "script_template_var_match": "URL of the current tab", + "script_template_var_icon": "Icon of the current site", + "script_template_var_domain": "Domain of the current tab", + "script_template_var_title": "Title of the current tab", + "script_template_var_date": "Current date; a format can be given after the colon", + "script_template_error_normal_directive": "A user script template cannot contain @background or @crontab", + "script_template_error_missing_directive": "This template must contain {{directive}}", "check_script_code_quality": "Check script code quality and errors", "script_run_env": { "title": "Operating environment", diff --git a/src/locales/ja-JP/script.json b/src/locales/ja-JP/script.json index b264ff8cd..7bf1f50b9 100644 --- a/src/locales/ja-JP/script.json +++ b/src/locales/ja-JP/script.json @@ -41,6 +41,7 @@ "cron_invalid_expr": "不正な cron 式です", "background_script": "バックグラウンドスクリプト", "scheduled_script": "スケジュールスクリプト", + "normal_script": "ユーザースクリプト", "next_run": "次回実行", "background_script_tooltip": "バックグラウンドスクリプト。有効にするとバックグラウンドで実行されます。", "scheduled_script_tooltip": "スケジュールスクリプト。設定した時刻に自動的に実行されます。", diff --git a/src/locales/ja-JP/settings.json b/src/locales/ja-JP/settings.json index 5f2e5b3e5..08fe7682d 100644 --- a/src/locales/ja-JP/settings.json +++ b/src/locales/ja-JP/settings.json @@ -79,6 +79,21 @@ "control_script_update_behavior": "スクリプト更新の動作を制御", "blacklist_pages_desc": "指定されたページでスクリプトの実行を禁止、ワイルドカードをサポート", "development_tools": "開発ツール", + "script_templates": "スクリプトテンプレート", + "script_templates_description": "新規スクリプト作成時に生成される初期コードをカスタマイズします。以降に作成するスクリプトにのみ適用されます。", + "script_template_saved": "スクリプトテンプレートを保存しました", + "script_template_reset": "デフォルトのスクリプトテンプレートに戻しました", + "script_template_modified": "変更済み", + "script_template_variables": "利用可能な変数", + "script_template_variables_hint": "値が空になる変数は行ごと削除されます。認識できないプレースホルダーはそのまま残ります。", + "script_template_var_name": "自動生成されるスクリプト名", + "script_template_var_match": "現在のタブの URL", + "script_template_var_icon": "現在のサイトのアイコン", + "script_template_var_domain": "現在のタブのドメイン", + "script_template_var_title": "現在のタブのタイトル", + "script_template_var_date": "現在の日付。コロンの後に書式を指定できます", + "script_template_error_normal_directive": "ユーザースクリプトのテンプレートには @background や @crontab を含められません", + "script_template_error_missing_directive": "このテンプレートには {{directive}} が必要です", "check_script_code_quality": "スクリプトコードの品質とエラーをチェック", "script_run_env": { "title": "実行環境", diff --git a/src/locales/ko-KR/script.json b/src/locales/ko-KR/script.json index 6474df5a7..6596b9b58 100644 --- a/src/locales/ko-KR/script.json +++ b/src/locales/ko-KR/script.json @@ -41,6 +41,7 @@ "cron_invalid_expr": "잘못된 cron 표현식입니다", "background_script": "백그라운드 스크립트", "scheduled_script": "예약 스크립트", + "normal_script": "일반 스크립트", "next_run": "다음 실행", "background_script_tooltip": "백그라운드 스크립트입니다. 활성화하면 백그라운드에서 실행됩니다.", "scheduled_script_tooltip": "예약 스크립트입니다. 설정한 시간에 자동으로 실행됩니다.", diff --git a/src/locales/ko-KR/settings.json b/src/locales/ko-KR/settings.json index f1bd00e49..c830cce03 100644 --- a/src/locales/ko-KR/settings.json +++ b/src/locales/ko-KR/settings.json @@ -79,6 +79,21 @@ "control_script_update_behavior": "스크립트 업데이트 동작 제어", "blacklist_pages_desc": "지정한 페이지에서 스크립트 실행을 막습니다. 와일드카드를 지원합니다", "development_tools": "개발 도구", + "script_templates": "스크립트 템플릿", + "script_templates_description": "새 스크립트를 만들 때 생성되는 초기 코드를 사용자 지정합니다. 이후에 만드는 스크립트에만 적용됩니다.", + "script_template_saved": "스크립트 템플릿을 저장했습니다", + "script_template_reset": "기본 스크립트 템플릿으로 되돌렸습니다", + "script_template_modified": "수정됨", + "script_template_variables": "사용 가능한 변수", + "script_template_variables_hint": "값이 비어 있는 변수는 해당 줄 전체가 삭제되며, 인식되지 않는 자리 표시자는 그대로 유지됩니다.", + "script_template_var_name": "자동 생성된 스크립트 이름", + "script_template_var_match": "현재 탭의 주소", + "script_template_var_icon": "현재 사이트의 아이콘", + "script_template_var_domain": "현재 탭의 도메인", + "script_template_var_title": "현재 탭의 제목", + "script_template_var_date": "현재 날짜, 콜론 뒤에 형식을 지정할 수 있습니다", + "script_template_error_normal_directive": "일반 스크립트 템플릿에는 @background 또는 @crontab을 포함할 수 없습니다", + "script_template_error_missing_directive": "이 템플릿에는 {{directive}}이(가) 있어야 합니다", "check_script_code_quality": "스크립트 코드 품질과 오류를 검사", "script_run_env": { "title": "실행 환경", diff --git a/src/locales/pt-BR/script.json b/src/locales/pt-BR/script.json index ac3f1753a..7042dfe08 100644 --- a/src/locales/pt-BR/script.json +++ b/src/locales/pt-BR/script.json @@ -41,6 +41,7 @@ "cron_invalid_expr": "Expressão cron inválida", "background_script": "Script em segundo plano", "scheduled_script": "Script agendado", + "normal_script": "Script de usuário", "next_run": "Próxima execução", "background_script_tooltip": "Script em segundo plano. É executado em segundo plano quando ativado.", "scheduled_script_tooltip": "Script agendado. Executa automaticamente no horário configurado.", diff --git a/src/locales/pt-BR/settings.json b/src/locales/pt-BR/settings.json index 827141920..b4a96c303 100644 --- a/src/locales/pt-BR/settings.json +++ b/src/locales/pt-BR/settings.json @@ -79,6 +79,21 @@ "control_script_update_behavior": "Controlar o comportamento de atualização de scripts", "blacklist_pages_desc": "Impedir que scripts sejam executados nas páginas especificadas, com suporte a curingas", "development_tools": "Ferramentas de desenvolvimento", + "script_templates": "Modelos de script", + "script_templates_description": "Personalize o código inicial gerado ao criar um novo script. Afeta apenas os scripts criados depois.", + "script_template_saved": "Modelo de script salvo", + "script_template_reset": "Modelo de script padrão restaurado", + "script_template_modified": "Modificado", + "script_template_variables": "Variáveis disponíveis", + "script_template_variables_hint": "Uma variável com valor vazio remove a linha inteira; marcadores não reconhecidos são mantidos como estão.", + "script_template_var_name": "Nome do script gerado automaticamente", + "script_template_var_match": "Endereço da aba atual", + "script_template_var_icon": "Ícone do site atual", + "script_template_var_domain": "Domínio da aba atual", + "script_template_var_title": "Título da aba atual", + "script_template_var_date": "Data atual; um formato pode ser indicado após os dois-pontos", + "script_template_error_normal_directive": "Um modelo de script de usuário não pode conter @background ou @crontab", + "script_template_error_missing_directive": "Este modelo deve conter {{directive}}", "check_script_code_quality": "Verificar a qualidade e os erros do código do script", "script_run_env": { "title": "Ambiente de execução", diff --git a/src/locales/ru-RU/script.json b/src/locales/ru-RU/script.json index 912a6502f..97cd6a9d9 100644 --- a/src/locales/ru-RU/script.json +++ b/src/locales/ru-RU/script.json @@ -41,6 +41,7 @@ "cron_invalid_expr": "Неверное выражение cron", "background_script": "Фоновый скрипт", "scheduled_script": "Запланированный скрипт", + "normal_script": "Пользовательский скрипт", "next_run": "Следующий запуск", "background_script_tooltip": "Фоновый скрипт. После включения выполняется в фоне.", "scheduled_script_tooltip": "Скрипт по расписанию. Запускается автоматически в заданное время.", diff --git a/src/locales/ru-RU/settings.json b/src/locales/ru-RU/settings.json index e9bfc5fc3..380fe168a 100644 --- a/src/locales/ru-RU/settings.json +++ b/src/locales/ru-RU/settings.json @@ -79,6 +79,21 @@ "control_script_update_behavior": "Управление поведением обновления скриптов", "blacklist_pages_desc": "Запретить запуск скриптов на указанных страницах, поддерживает подстановочные знаки", "development_tools": "Инструменты разработки", + "script_templates": "Шаблоны скриптов", + "script_templates_description": "Настройте исходный код, который создаётся при создании нового скрипта. Влияет только на скрипты, созданные после изменения.", + "script_template_saved": "Шаблон скрипта сохранён", + "script_template_reset": "Стандартный шаблон скрипта восстановлен", + "script_template_modified": "Изменён", + "script_template_variables": "Доступные переменные", + "script_template_variables_hint": "Переменная с пустым значением удаляется вместе со всей строкой; нераспознанные подстановки остаются без изменений.", + "script_template_var_name": "Автоматически созданное имя скрипта", + "script_template_var_match": "Адрес текущей вкладки", + "script_template_var_icon": "Значок текущего сайта", + "script_template_var_domain": "Домен текущей вкладки", + "script_template_var_title": "Заголовок текущей вкладки", + "script_template_var_date": "Текущая дата; формат можно указать после двоеточия", + "script_template_error_normal_directive": "Шаблон пользовательского скрипта не может содержать @background или @crontab", + "script_template_error_missing_directive": "Этот шаблон должен содержать {{directive}}", "check_script_code_quality": "Проверить качество кода скрипта и ошибки", "script_run_env": { "title": "Среда выполнения", diff --git a/src/locales/tr-TR/script.json b/src/locales/tr-TR/script.json index 1dcad714c..97a21d86c 100644 --- a/src/locales/tr-TR/script.json +++ b/src/locales/tr-TR/script.json @@ -41,6 +41,7 @@ "cron_invalid_expr": "Geçersiz cron ifadesi", "background_script": "Arka Plan Betiği", "scheduled_script": "Zamanlanmış Betik", + "normal_script": "Kullanıcı Betiği", "next_run": "Sonraki çalışma", "background_script_tooltip": "Arka plan betiği. Etkinleştirildiğinde arka planda çalışır.", "scheduled_script_tooltip": "Zamanlanmış betik. Yapılandırılan zamanda otomatik olarak çalışır.", diff --git a/src/locales/tr-TR/settings.json b/src/locales/tr-TR/settings.json index 2abd2c47d..f0f7648cb 100644 --- a/src/locales/tr-TR/settings.json +++ b/src/locales/tr-TR/settings.json @@ -79,6 +79,21 @@ "control_script_update_behavior": "Betik güncelleme davranışını kontrol et", "blacklist_pages_desc": "Betiklerin belirtilen sayfalarda çalışmasını engeller, joker karakterleri destekler", "development_tools": "Geliştirici Araçları", + "script_templates": "Betik Şablonları", + "script_templates_description": "Yeni betik oluşturulurken üretilen başlangıç kodunu özelleştirin. Yalnızca bundan sonra oluşturulan betikleri etkiler.", + "script_template_saved": "Betik şablonu kaydedildi", + "script_template_reset": "Varsayılan betik şablonu geri yüklendi", + "script_template_modified": "Değiştirildi", + "script_template_variables": "Kullanılabilir değişkenler", + "script_template_variables_hint": "Değeri boş olan bir değişken bulunduğu satırın tamamını siler; tanınmayan yer tutucular olduğu gibi kalır.", + "script_template_var_name": "Otomatik oluşturulan betik adı", + "script_template_var_match": "Geçerli sekmenin adresi", + "script_template_var_icon": "Geçerli sitenin simgesi", + "script_template_var_domain": "Geçerli sekmenin etki alanı", + "script_template_var_title": "Geçerli sekmenin başlığı", + "script_template_var_date": "Geçerli tarih; iki nokta üst üste işaretinden sonra biçim belirtilebilir", + "script_template_error_normal_directive": "Kullanıcı betiği şablonu @background veya @crontab içeremez", + "script_template_error_missing_directive": "Bu şablon {{directive}} içermelidir", "check_script_code_quality": "Betik kod kalitesini ve hatalarını denetle", "script_run_env": { "title": "Çalışma Ortamı", diff --git a/src/locales/vi-VN/script.json b/src/locales/vi-VN/script.json index 72b51872d..0552f4e9a 100644 --- a/src/locales/vi-VN/script.json +++ b/src/locales/vi-VN/script.json @@ -41,6 +41,7 @@ "cron_invalid_expr": "Biểu thức cron không hợp lệ", "background_script": "Script nền", "scheduled_script": "Script hẹn giờ", + "normal_script": "Script người dùng", "next_run": "Lần chạy tiếp theo", "background_script_tooltip": "Script nền. Chạy trong nền sau khi được bật.", "scheduled_script_tooltip": "Script hẹn giờ. Tự động chạy vào thời điểm đã cấu hình.", diff --git a/src/locales/vi-VN/settings.json b/src/locales/vi-VN/settings.json index ed380f717..b6a427d2e 100644 --- a/src/locales/vi-VN/settings.json +++ b/src/locales/vi-VN/settings.json @@ -79,6 +79,21 @@ "control_script_update_behavior": "Kiểm soát hành vi cập nhật script", "blacklist_pages_desc": "Ngăn script chạy trên các trang được chỉ định, hỗ trợ ký tự đại diện", "development_tools": "Công cụ phát triển", + "script_templates": "Mẫu script", + "script_templates_description": "Tùy chỉnh mã khởi tạo được sinh ra khi tạo script mới. Chỉ ảnh hưởng đến các script được tạo sau đó.", + "script_template_saved": "Đã lưu mẫu script", + "script_template_reset": "Đã khôi phục mẫu script mặc định", + "script_template_modified": "Đã sửa đổi", + "script_template_variables": "Biến khả dụng", + "script_template_variables_hint": "Biến có giá trị rỗng sẽ bị xóa cả dòng; các chỗ dành sẵn không nhận diện được vẫn giữ nguyên.", + "script_template_var_name": "Tên script được tạo tự động", + "script_template_var_match": "Địa chỉ của tab hiện tại", + "script_template_var_icon": "Biểu tượng của trang hiện tại", + "script_template_var_domain": "Miền của tab hiện tại", + "script_template_var_title": "Tiêu đề của tab hiện tại", + "script_template_var_date": "Ngày hiện tại; có thể chỉ định định dạng sau dấu hai chấm", + "script_template_error_normal_directive": "Mẫu script người dùng không được chứa @background hoặc @crontab", + "script_template_error_missing_directive": "Mẫu này phải chứa {{directive}}", "check_script_code_quality": "Kiểm tra chất lượng mã và lỗi script", "script_run_env": { "title": "Môi trường chạy", diff --git a/src/locales/zh-CN/script.json b/src/locales/zh-CN/script.json index 66ac559d3..cd8b2662d 100644 --- a/src/locales/zh-CN/script.json +++ b/src/locales/zh-CN/script.json @@ -41,6 +41,7 @@ "cron_invalid_expr": "错误的定时表达式", "background_script": "后台脚本", "scheduled_script": "定时脚本", + "normal_script": "普通脚本", "next_run": "下次运行", "background_script_tooltip": "后台脚本,启用后将在后台运行", "scheduled_script_tooltip": "定时脚本,将在设定的时间自动运行", diff --git a/src/locales/zh-CN/settings.json b/src/locales/zh-CN/settings.json index 625d943c7..142d8c6aa 100644 --- a/src/locales/zh-CN/settings.json +++ b/src/locales/zh-CN/settings.json @@ -79,6 +79,21 @@ "control_script_update_behavior": "控制脚本更新的行为", "blacklist_pages_desc": "禁止脚本在指定页面运行,支持通配符", "development_tools": "开发工具", + "script_templates": "脚本模板", + "script_templates_description": "自定义新建脚本时生成的初始代码,只影响之后新建的脚本。", + "script_template_saved": "脚本模板已保存", + "script_template_reset": "已恢复默认脚本模板", + "script_template_modified": "已修改", + "script_template_variables": "可用变量", + "script_template_variables_hint": "取值为空的变量会连同所在行一起删除;未识别的占位符原样保留。", + "script_template_var_name": "自动生成的脚本名", + "script_template_var_match": "当前标签页的地址", + "script_template_var_icon": "当前站点的图标", + "script_template_var_domain": "当前标签页的域名", + "script_template_var_title": "当前标签页的标题", + "script_template_var_date": "当前日期,冒号后可指定格式", + "script_template_error_normal_directive": "普通脚本模板不能包含 @background 或 @crontab", + "script_template_error_missing_directive": "此模板必须包含 {{directive}}", "check_script_code_quality": "检查脚本代码质量和错误", "script_run_env": { "title": "运行环境", diff --git a/src/locales/zh-TW/script.json b/src/locales/zh-TW/script.json index a2b691ed0..d0dbcd608 100644 --- a/src/locales/zh-TW/script.json +++ b/src/locales/zh-TW/script.json @@ -41,6 +41,7 @@ "cron_invalid_expr": "錯誤的排程表達式", "background_script": "背景腳本", "scheduled_script": "排程腳本", + "normal_script": "普通腳本", "next_run": "下次執行", "background_script_tooltip": "背景腳本,啟用後將在背景執行", "scheduled_script_tooltip": "排程腳本,將在設定的時間自動執行", diff --git a/src/locales/zh-TW/settings.json b/src/locales/zh-TW/settings.json index 19a26817e..279f7b017 100644 --- a/src/locales/zh-TW/settings.json +++ b/src/locales/zh-TW/settings.json @@ -79,6 +79,21 @@ "control_script_update_behavior": "控制腳本更新的行為", "blacklist_pages_desc": "禁止腳本在指定頁面執行,支援萬用字元", "development_tools": "開發工具", + "script_templates": "腳本範本", + "script_templates_description": "自訂新增腳本時產生的初始程式碼,只影響之後新增的腳本。", + "script_template_saved": "已儲存腳本範本", + "script_template_reset": "已還原預設腳本範本", + "script_template_modified": "已修改", + "script_template_variables": "可用變數", + "script_template_variables_hint": "取值為空的變數會連同所在行一起刪除;無法辨識的預留位置則原樣保留。", + "script_template_var_name": "自動產生的腳本名稱", + "script_template_var_match": "目前分頁的網址", + "script_template_var_icon": "目前網站的圖示", + "script_template_var_domain": "目前分頁的網域", + "script_template_var_title": "目前分頁的標題", + "script_template_var_date": "目前日期,冒號後可指定格式", + "script_template_error_normal_directive": "普通腳本範本不能包含 @background 或 @crontab", + "script_template_error_missing_directive": "此範本必須包含 {{directive}}", "check_script_code_quality": "檢查腳本程式碼品質和錯誤", "script_run_env": { "title": "運作環境", diff --git a/src/pages/options/routes/ScriptEditor/editorScriptLoaders.test.ts b/src/pages/options/routes/ScriptEditor/editorScriptLoaders.test.ts new file mode 100644 index 000000000..9dc9c0aea --- /dev/null +++ b/src/pages/options/routes/ScriptEditor/editorScriptLoaders.test.ts @@ -0,0 +1,79 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { DEFAULT_SCRIPT_TEMPLATES, type ScriptTemplateOverrides } from "@App/pkg/utils/script_template"; +import type { Script } from "@App/app/repo/scripts"; + +const { getScriptTemplates, prepareScriptByCode } = vi.hoisted(() => ({ + getScriptTemplates: vi.fn(), + prepareScriptByCode: vi.fn(), +})); + +vi.mock("@App/pages/store/global", () => ({ systemConfig: { getScriptTemplates } })); +vi.mock("@App/pkg/utils/script", () => ({ prepareScriptByCode })); + +import { emptyScript } from "./editorScriptLoaders"; + +const setTemplates = (v: ScriptTemplateOverrides) => getScriptTemplates.mockResolvedValue(v); + +// 新建脚本时把渲染后的代码交给 prepareScriptByCode,这里取回它实际收到的代码 +const renderedCode = () => prepareScriptByCode.mock.calls.at(-1)?.[0] as string; + +beforeEach(async () => { + initTestLanguage("en-US"); + vi.clearAllMocks(); + setTemplates({}); + prepareScriptByCode.mockImplementation(async (_code: string, _origin: string, uuid: string) => ({ + script: { uuid } as Script, + })); + await chrome.storage.local.remove(["activeTabUrl"]); +}); + +describe("emptyScript", () => { + it("使用用户自定义模板,并按活动标签页替换变量", async () => { + setTemplates({ + normal: [ + "// ==UserScript==", + "// @name {{name}}", + "// @namespace https://{{domain}}/", + "// @description {{title}}", + "// @match {{match}}", + "// @grant none", + "// ==/UserScript==", + ].join("\n"), + }); + await chrome.storage.local.set({ activeTabUrl: { url: "https://example.com/docs?a=1", title: "示例页面" } }); + + await emptyScript("", "initial"); + + const code = renderedCode(); + expect(code).toContain("// @namespace https://example.com/"); + expect(code).toContain("// @description 示例页面"); + expect(code).toContain("// @match https://example.com/docs?a=1"); + expect(code).toMatch(/\/\/ @name\s+New Userscript [0-9A-Z]{4}-\d+/); + }); + + it("没有活动标签页时回落到默认模板与通配 @match,并删掉取不到值的 @icon 行", async () => { + await emptyScript(""); + + const code = renderedCode(); + expect(code).toContain("// @match https://*/*"); + expect(code).not.toContain("@icon"); + expect(code).not.toContain("{{"); + }); + + it("按模板类型取用对应的自定义模板", async () => { + setTemplates({ crontab: DEFAULT_SCRIPT_TEMPLATES.crontab.replace("* * once * *", "0 8 * * *") }); + + await emptyScript("crontab"); + + expect(renderedCode()).toContain("// @crontab 0 8 * * *"); + }); + + it("读取后清除 activeTabUrl,避免下次新建复用上次的标签页", async () => { + await chrome.storage.local.set({ activeTabUrl: { url: "https://example.com/", title: "示例页面" } }); + + await emptyScript("", "initial"); + + expect(await chrome.storage.local.get(["activeTabUrl"])).toEqual({}); + }); +}); diff --git a/src/pages/options/routes/ScriptEditor/editorScriptLoaders.ts b/src/pages/options/routes/ScriptEditor/editorScriptLoaders.ts index 29d5d4976..fcc219efc 100644 --- a/src/pages/options/routes/ScriptEditor/editorScriptLoaders.ts +++ b/src/pages/options/routes/ScriptEditor/editorScriptLoaders.ts @@ -1,10 +1,14 @@ -import normalTpl from "@App/template/normal.tpl"; -import crontabTpl from "@App/template/crontab.tpl"; -import backgroundTpl from "@App/template/background.tpl"; import { uuidv4 } from "@App/pkg/utils/uuid"; -import { lazyScriptName } from "@App/pkg/config/config"; +import { lazyScriptName, nextScriptName } from "@App/pkg/config/config"; import { prepareScriptByCode } from "@App/pkg/utils/script"; +import { + DEFAULT_TEMPLATE_MATCH, + renderScriptTemplate, + resolveScriptTemplate, + type ScriptTemplateType, +} from "@App/pkg/utils/script_template"; import { ScriptCodeDAO } from "@App/app/repo/scripts"; +import { systemConfig } from "@App/pages/store/global"; import type { EditorTab } from "./useEditorTabs"; const scriptCodeDAO = new ScriptCodeDAO(); @@ -15,59 +19,61 @@ export async function loadScriptCode(uuid: string): Promise { return code?.code || ""; } -// 从激活标签 URL 推断 normal 模板的 @match / @icon -async function resolveInitialMatch(): Promise<[string, string]> { - return new Promise<[string, string]>((resolve) => { +// 模板中与当前页面相关的变量;没有活动标签页时除 match 外都为空,对应行会被删除 +type PageVars = { match: string; icon: string; domain: string; title: string }; + +const EMPTY_PAGE_VARS: PageVars = { match: DEFAULT_TEMPLATE_MATCH, icon: "", domain: "", title: "" }; + +// 从激活标签(由 popup 写入)推断模板变量 +async function resolveActiveTabVars(): Promise { + return new Promise((resolve) => { chrome.storage.local.get(["activeTabUrl"], (result) => { const lastError = chrome.runtime.lastError; - let retUrl = "https://*/*"; - let retIcon = ""; + void chrome.storage.local.remove(["activeTabUrl"]); if (lastError) { console.error("chrome.runtime.lastError in chrome.storage.local.get:", lastError); - void chrome.storage.local.remove(["activeTabUrl"]); - } else { - void chrome.storage.local.remove(["activeTabUrl"]); - const pageUrl = (result?.activeTabUrl as { url?: string } | undefined)?.url; - if (pageUrl) { - try { - const { protocol, pathname, hostname, search } = new URL(pageUrl); - if (protocol && pathname && hostname) { - retUrl = `${protocol}//${hostname}${pathname}${search.length > 1 ? search : ""}`; - if (protocol === "http:" || protocol === "https:") { - retIcon = `https://www.google.com/s2/favicons?sz=64&domain=${hostname}`; - } + resolve(EMPTY_PAGE_VARS); + return; + } + const activeTab = result?.activeTabUrl as { url?: string; title?: string } | undefined; + const pageUrl = activeTab?.url; + const vars: PageVars = { ...EMPTY_PAGE_VARS, title: activeTab?.title || "" }; + if (pageUrl) { + try { + const { protocol, pathname, hostname, search } = new URL(pageUrl); + if (protocol && pathname && hostname) { + vars.match = `${protocol}//${hostname}${pathname}${search.length > 1 ? search : ""}`; + vars.domain = hostname; + if (protocol === "http:" || protocol === "https:") { + vars.icon = `https://www.google.com/s2/favicons?sz=64&domain=${hostname}`; } - } catch { - // ignore malformed url } + } catch { + // ignore malformed url } } - resolve([retUrl, retIcon]); + resolve(vars); }); }); } +function toTemplateType(template: string): ScriptTemplateType { + return template === "background" || template === "crontab" ? template : "normal"; +} + // 新建空脚本(移植 v1.4 emptyScript) export async function emptyScript(template: string, target?: string): Promise { - let code = ""; - switch (template) { - case "background": - code = lazyScriptName(backgroundTpl); - break; - case "crontab": - code = lazyScriptName(crontabTpl); - break; - default: { - code = lazyScriptName(normalTpl); - const [url, icon] = target === "initial" ? await resolveInitialMatch() : ["https://*/*", ""]; - if (icon) { - code = code.replace("{{match}}", url).replace("{{icon}}", icon); - } else { - code = code.replace("{{match}}", url).replace(/[\r\n]*[^\r\n]*\{\{icon\}\}[^\r\n]*/, ""); - } - break; - } - } + const type = toTemplateType(template); + const [overrides, pageVars] = await Promise.all([ + systemConfig.getScriptTemplates(), + target === "initial" ? resolveActiveTabVars() : Promise.resolve(EMPTY_PAGE_VARS), + ]); + const name = nextScriptName(); + const code = renderScriptTemplate(lazyScriptName(resolveScriptTemplate(overrides, type), name), { + name, + ...pageVars, + date: new Date(), + }); const { script } = await prepareScriptByCode(code, "", uuidv4()); script.createtime = 0; // 标记为未保存的新脚本 return { uuid: script.uuid, script, code, subView: "code", isChanged: false }; diff --git a/src/pages/options/routes/Setting/sections/DeveloperMonacoEditor.tsx b/src/pages/options/routes/Setting/sections/DeveloperMonacoEditor.tsx index 379753f04..cae26a166 100644 --- a/src/pages/options/routes/Setting/sections/DeveloperMonacoEditor.tsx +++ b/src/pages/options/routes/Setting/sections/DeveloperMonacoEditor.tsx @@ -5,7 +5,7 @@ import { resolveMonacoTheme } from "@App/pages/components/CodeEditor/theme"; import { registerEditor } from "@App/pkg/utils/monaco-editor"; import { cn } from "@App/pkg/utils/cn"; -type DeveloperMonacoLanguage = "json" | "typescript"; +type DeveloperMonacoLanguage = "json" | "typescript" | "javascript"; type DeveloperMonacoEditorProps = { id: string; diff --git a/src/pages/options/routes/Setting/sections/DeveloperSection.tsx b/src/pages/options/routes/Setting/sections/DeveloperSection.tsx index ad59ba370..1dbcdb93d 100644 --- a/src/pages/options/routes/Setting/sections/DeveloperSection.tsx +++ b/src/pages/options/routes/Setting/sections/DeveloperSection.tsx @@ -11,6 +11,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@App/pages/components/ import { useSystemConfig } from "../../../hooks/useSystemConfig"; import { notify } from "@App/pages/components/ui/toast"; import { DeveloperMonacoEditor } from "./DeveloperMonacoEditor"; +import { ScriptTemplateSettings } from "./ScriptTemplateSettings"; import { DEFAULT_EDITOR_PREFERENCES, type EditorPreferences } from "@App/pkg/config/config"; const JSCONFIG_DOC_URL = "https://code.visualstudio.com/docs/languages/jsconfig"; @@ -60,6 +61,7 @@ export function DeveloperSection({ register }: { register: (id: string) => (el: {t("settings:eslint_rules")} {t("editor:editor_config")} {t("editor:editor_type_definition")} + {t("settings:script_templates")} @@ -189,6 +191,10 @@ export function DeveloperSection({ register }: { register: (id: string) => (el: }} /> + + + + ); diff --git a/src/pages/options/routes/Setting/sections/ScriptTemplateSettings.test.tsx b/src/pages/options/routes/Setting/sections/ScriptTemplateSettings.test.tsx new file mode 100644 index 000000000..27e90043d --- /dev/null +++ b/src/pages/options/routes/Setting/sections/ScriptTemplateSettings.test.tsx @@ -0,0 +1,120 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { DEFAULT_SCRIPT_TEMPLATES } from "@App/pkg/utils/script_template"; + +const { get, set, defaultGet } = vi.hoisted(() => { + const defaultGet = (key: string) => Promise.resolve(key === "script_templates" ? {} : undefined); + return { get: vi.fn(defaultGet), set: vi.fn(), defaultGet }; +}); + +vi.mock("@App/pages/store/global", async () => { + const { createGlobalStoreMock } = await import("@Tests/mocks/pageStores.ts"); + return createGlobalStoreMock({ systemConfig: { get, set } }); +}); + +vi.mock("./DeveloperMonacoEditor", () => ({ + DeveloperMonacoEditor: ({ + value, + onChange, + onBlur, + "data-testid": testId, + ariaLabel, + }: { + value: string; + onChange: (value: string) => void; + onBlur: () => void; + ariaLabel: string; + "data-testid"?: string; + }) => ( +