From 9a838647fec46d0ef9cc2243fd379815fa3b2365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=8B=E6=BE=AAAkimio?= Date: Fri, 17 Jul 2026 23:34:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BA=E8=AF=8D=E8=A1=A8=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E5=A2=9E=E5=8A=A0=E5=8F=AF=E9=80=89=E8=A1=8C?= =?UTF-8?q?=E5=8F=B7=E4=B8=8E=E8=AF=AD=E6=B3=95=E9=AB=98=E4=BA=AE=20(#546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/ace-config.spec.ts | 62 +++++++ src/ace-config.ts | 84 +++++++++ src/locales/en-US.ts | 2 + src/locales/zh-CN.ts | 2 + src/locales/zh-TW.ts | 2 + src/views/system/WordsView.vue | 133 +++++++++++++- src/views/system/__tests__/WordsView.spec.ts | 179 +++++++++++++++++++ 7 files changed, 458 insertions(+), 6 deletions(-) create mode 100644 src/__tests__/ace-config.spec.ts create mode 100644 src/views/system/__tests__/WordsView.spec.ts diff --git a/src/__tests__/ace-config.spec.ts b/src/__tests__/ace-config.spec.ts new file mode 100644 index 00000000..480a9422 --- /dev/null +++ b/src/__tests__/ace-config.spec.ts @@ -0,0 +1,62 @@ +import ace from 'ace-builds' +import { describe, expect, it } from 'vitest' +import '@/ace-config' + +interface AceToken { + type: string + value: string +} + +interface WordListSyntaxMode { + getTokenizer: () => { + getLineTokens: (line: string, state: string) => { tokens: AceToken[] } + } +} + +const WordListSyntaxMode = ace.require('ace/mode/word_list_syntax').Mode as new () => WordListSyntaxMode +const tokenizer = new WordListSyntaxMode().getTokenizer() + +function tokenize(line: string) { + return tokenizer.getLineTokens(line, 'start').tokens +} + +describe('word list syntax mode', () => { + it('highlights a block word as one field', () => { + expect(tokenize('屏蔽词')).toEqual([{ type: 'word_list_block', value: '屏蔽词' }]) + }) + + it('separates replaced and replacement fields without parsing their content', () => { + expect(tokenize('旧名.* => 新名 {[tmdbid=123;type=tv]}')).toEqual([ + { type: 'word_list_replaced', value: '旧名.*' }, + { type: 'keyword.operator.word-list', value: ' => ' }, + { type: 'word_list_replacement', value: '新名 {[tmdbid=123;type=tv]}' }, + ]) + }) + + it('separates front, back, and episode offset fields', () => { + expect(tokenize('第 <> 集 >> EP+1')).toEqual([ + { type: 'word_list_front', value: '第' }, + { type: 'keyword.operator.word-list', value: ' <> ' }, + { type: 'word_list_back', value: '集' }, + { type: 'keyword.operator.word-list', value: ' >> ' }, + { type: 'word_list_offset', value: 'EP+1' }, + ]) + }) + + it('uses all six field types in combined and standalone rules', () => { + const combinedTokens = tokenize('旧名 => 新名 && 第 <> 集 >> 2*EP-1') + + expect(combinedTokens).toEqual([ + { type: 'word_list_replaced', value: '旧名' }, + { type: 'keyword.operator.word-list', value: ' => ' }, + { type: 'word_list_replacement', value: '新名' }, + { type: 'keyword.operator.word-list', value: ' && ' }, + { type: 'word_list_front', value: '第' }, + { type: 'keyword.operator.word-list', value: ' <> ' }, + { type: 'word_list_back', value: '集' }, + { type: 'keyword.operator.word-list', value: ' >> ' }, + { type: 'word_list_offset', value: '2*EP-1' }, + ]) + expect(tokenize('屏蔽词')[0].type).toBe('word_list_block') + }) +}) diff --git a/src/ace-config.ts b/src/ace-config.ts index 507d176a..7621d39e 100644 --- a/src/ace-config.ts +++ b/src/ace-config.ts @@ -578,6 +578,90 @@ function registerWordListMode() { exports.Mode = Mode }, ) + + aceModule.define?.( + 'ace/mode/word_list_syntax_highlight_rules', + ['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text_highlight_rules'], + (require: any, exports: any) => { + const oop = require('../lib/oop') + const TextHighlightRules = require('./text_highlight_rules').TextHighlightRules + + const WordListSyntaxHighlightRules = function (this: any) { + this.$rules = { + start: [ + { + token: 'comment.word-list', + regex: /^#.*/, + }, + { + token: [ + 'text', + 'word_list_replaced', + 'keyword.operator.word-list', + 'word_list_replacement', + 'keyword.operator.word-list', + 'word_list_front', + 'keyword.operator.word-list', + 'word_list_back', + 'keyword.operator.word-list', + 'word_list_offset', + 'text', + ], + regex: /^(\s*)(.*?)( +=> +)(.*?)( +&& +)(.*?)( +<> +)(.*?)( +>> +)(.*?)(\s*)$/, + }, + { + token: ['text', 'word_list_replaced', 'keyword.operator.word-list', 'word_list_replacement', 'text'], + regex: /^(\s*)(.*?)( +=> +)(.*?)(\s*)$/, + }, + { + token: [ + 'text', + 'word_list_front', + 'keyword.operator.word-list', + 'word_list_back', + 'keyword.operator.word-list', + 'word_list_offset', + 'text', + ], + regex: /^(\s*)(.*?)( +<> +)(.*?)( +>> +)(.*?)(\s*)$/, + }, + { + token: ['text', 'word_list_block', 'text'], + regex: /^(\s*)(\S(?:.*?\S)?)(\s*)$/, + }, + ], + } + + this.normalizeRules() + } + + oop.inherits(WordListSyntaxHighlightRules, TextHighlightRules) + exports.WordListSyntaxHighlightRules = WordListSyntaxHighlightRules + }, + ) + + aceModule.define?.( + 'ace/mode/word_list_syntax', + ['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text', 'ace/mode/word_list_syntax_highlight_rules'], + (require: any, exports: any) => { + const oop = require('../lib/oop') + const TextMode = require('./text').Mode + const WordListSyntaxHighlightRules = require('./word_list_syntax_highlight_rules').WordListSyntaxHighlightRules + + const Mode = function (this: any) { + TextMode.call(this) + this.HighlightRules = WordListSyntaxHighlightRules + } + + oop.inherits(Mode, TextMode) + + ;(function (this: any) { + this.$id = 'ace/mode/word_list_syntax' + }).call(Mode.prototype) + + exports.Mode = Mode + }, + ) } ace.config.setModuleUrl('ace/mode/json', modeJsonUrl) diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 6eb99246..d394ee41 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -2215,6 +2215,8 @@ export default { entryCount: '{count} entries', ruleCount: '{count} rules', listLabel: 'Word list content (one rule per line)', + lineNumbers: 'Line numbers', + syntaxHighlighting: 'Syntax highlighting', saved: 'Saved', unsaved: 'Unsaved changes', saveChanges: 'Save changes', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index fc13ad97..9ae682e4 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -2177,6 +2177,8 @@ export default { entryCount: '{count} 条', ruleCount: '共 {count} 条规则', listLabel: '词表内容(每行一个规则)', + lineNumbers: '行号', + syntaxHighlighting: '语法高亮', saved: '已保存', unsaved: '有未保存修改', saveChanges: '保存更改', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 3aba979d..6855ba14 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -2176,6 +2176,8 @@ export default { entryCount: '{count} 條', ruleCount: '共 {count} 條規則', listLabel: '詞表內容(每行一個規則)', + lineNumbers: '行號', + syntaxHighlighting: '語法高亮', saved: '已保存', unsaved: '有未保存修改', saveChanges: '保存更改', diff --git a/src/views/system/WordsView.vue b/src/views/system/WordsView.vue index fd40b252..85d17c48 100644 --- a/src/views/system/WordsView.vue +++ b/src/views/system/WordsView.vue @@ -10,6 +10,9 @@ const { t } = useI18n() const $toast = useToast() const { global: globalTheme } = useTheme() +const WORDS_LINE_NUMBERS_STORAGE_KEY = 'MP_WORDS_SHOW_LINE_NUMBERS' +const WORDS_SYNTAX_HIGHLIGHTING_STORAGE_KEY = 'MP_WORDS_SYNTAX_HIGHLIGHTING' + type TextSectionKey = 'identifiers' | 'releaseGroups' | 'customization' | 'excludeWords' type WordSectionKey = TextSectionKey | 'episodeRules' @@ -45,16 +48,29 @@ const episodeFormatRules = ref([]) const activeSection = ref('identifiers') const expandedHelp = ref(null) const saving = ref(false) +const showLineNumbers = ref(localStorage.getItem(WORDS_LINE_NUMBERS_STORAGE_KEY) === 'true') +const showSyntaxHighlighting = ref(localStorage.getItem(WORDS_SYNTAX_HIGHLIGHTING_STORAGE_KEY) === 'true') +const textEditorLanguage = computed(() => (showSyntaxHighlighting.value ? 'word_list_syntax' : 'word_list')) const textEditorTheme = computed(() => (globalTheme.current.value.dark ? 'github_dark' : 'github_light_default')) -const textEditorOptions = { +const textEditorOptions = computed(() => ({ fontSize: 13.6, highlightActiveLine: false, scrollPastEnd: 0, - showGutter: false, + showFoldWidgets: false, + showGutter: showLineNumbers.value, + showLineNumbers: showLineNumbers.value, showPrintMargin: false, tabSize: 2, -} +})) + +watch(showLineNumbers, value => { + localStorage.setItem(WORDS_LINE_NUMBERS_STORAGE_KEY, String(value)) +}) + +watch(showSyntaxHighlighting, value => { + localStorage.setItem(WORDS_SYNTAX_HIGHLIGHTING_STORAGE_KEY, String(value)) +}) const savedTextValues = reactive>({ identifiers: '', @@ -483,13 +499,33 @@ onMounted(() => {