diff --git a/packages/eslint/compat-headers.js b/packages/eslint/compat-headers.js index 08eb3ce19..2a674d4e7 100644 --- a/packages/eslint/compat-headers.js +++ b/packages/eslint/compat-headers.js @@ -17,6 +17,7 @@ const compatMap = { storageName: [], "early-start": [], "require-css": [], + allFrames: [], }, }; diff --git a/packages/eslint/linter-config.ts b/packages/eslint/linter-config.ts index d6b57a1c4..01a3c528e 100644 --- a/packages/eslint/linter-config.ts +++ b/packages/eslint/linter-config.ts @@ -1,5 +1,4 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ -const { configs } = require("eslint-plugin-userscripts"); +import { configs } from "eslint-plugin-userscripts"; // 默认规则 const config = { @@ -21,12 +20,12 @@ const config = { rules: { "constructor-super": ["error"], "for-direction": ["error"], - "getter-return": ["error"], + "getter-return": ["warn"], // implicitly means return undefined "no-async-promise-executor": ["error"], "no-case-declarations": ["error"], "no-class-assign": ["error"], "no-compare-neg-zero": ["error"], - "no-cond-assign": ["error"], + "no-cond-assign": ["warn"], // this is common writing style in JavaScript "no-const-assign": ["error"], "no-constant-condition": ["error"], "no-control-regex": ["error"], @@ -37,7 +36,7 @@ const config = { "no-dupe-else-if": ["error"], "no-dupe-keys": ["error"], "no-duplicate-case": ["error"], - "no-empty": ["error"], + "no-empty": ["error", { allowEmptyCatch: true }], "no-empty-character-class": ["error"], "no-empty-pattern": ["error"], "no-ex-assign": ["error"], @@ -45,7 +44,7 @@ const config = { "no-extra-semi": ["error"], "no-fallthrough": ["error"], "no-func-assign": ["error"], - "no-global-assign": ["error"], + "no-global-assign": ["warn"], // we always modify global variable in UserScript "no-import-assign": ["error"], "no-inner-declarations": ["error"], "no-invalid-regexp": ["error"], @@ -58,10 +57,10 @@ const config = { "no-obj-calls": ["error"], "no-octal": ["error"], "no-prototype-builtins": ["error"], - "no-redeclare": ["error"], + "no-redeclare": ["error", { builtinGlobals: false }], "no-regex-spaces": ["error"], "no-self-assign": ["error"], - "no-setter-return": ["error"], + "no-setter-return": ["warn"], // sometimes developers like to return true in setter "no-shadow-restricted-names": ["error"], "no-sparse-arrays": ["error"], "no-this-before-super": ["error"], @@ -75,13 +74,13 @@ const config = { "no-unused-vars": ["warn"], "no-useless-backreference": ["error"], "no-useless-catch": ["error"], - "no-useless-escape": ["error"], + "no-useless-escape": ["error", { allowRegexCharacters: ["-", "&", "/"] }], "no-with": ["error"], "require-yield": ["error"], "use-isnan": ["error"], "valid-typeof": ["error"], ...configs.recommended.rules, - }, + } as Record, env: { es6: true, browser: true, @@ -90,8 +89,18 @@ const config = { }; // 调整规则 -config.rules["userscripts/align-attributes"] = ["warn", 2]; +// ScriptCat 在 Monaco 侧用自定义检查处理 metadata 对齐: +// 只要求 value 起始列一致,不要求固定空格数。 +config.rules["userscripts/align-attributes"] = []; config.rules["userscripts/require-download-url"] = ["warn"]; +// ScriptCat 不适用 - 有必要存在的用法 +// 不是所有 @include 都要改为 @match。改用自定义处理 +config.rules["userscripts/better-use-match"] = []; +// 不是 @name @name:en @name:zh-CN @name:zh-TW @name:ja 都要放在最前。这个连 warning 也很无谓 +config.rules["userscripts/require-name"] = []; +// ScriptCat 不用指定 ==UserScript== 放最前。在 ==UserScript== 前面可以写其他注释, 例如是 License。 不视为 invalid +config.rules["userscripts/no-invalid-metadata"] = []; + // 以文本形式导出默认规则 export const defaultConfig = JSON.stringify(config, null, 2); diff --git a/src/linter.worker.ts b/src/linter.worker.ts index 69495b0f9..3ef01daf2 100644 --- a/src/linter.worker.ts +++ b/src/linter.worker.ts @@ -1,14 +1,15 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ -const { Linter } = require("eslint-linter-browserify"); -const { rules } = require("eslint-plugin-userscripts"); +import { Linter } from "eslint-linter-browserify"; +import { rules } from "eslint-plugin-userscripts"; // eslint语法检查,使用webworker const linter = new Linter({ configType: "eslintrc" }); // 额外定义 userscripts 规则 -const formatRules = Object.fromEntries(Object.entries(rules).map(([key, metas]) => ["userscripts/" + key, metas])); -linter.defineRules(formatRules as any); +const formatRules: typeof rules = Object.fromEntries( + Object.entries(rules).map(([key, metas]) => ["userscripts/" + key, metas]) +); +linter.defineRules(formatRules); const getRules = linter.getRules(); diff --git a/src/pages/components/CodeEditor/index.tsx b/src/pages/components/CodeEditor/index.tsx index ed23bcffe..16fe9d9de 100644 --- a/src/pages/components/CodeEditor/index.tsx +++ b/src/pages/components/CodeEditor/index.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useImperativeHandle, useRef, useState } from "react"; import { systemConfig } from "@App/pages/store/global"; import { LinterWorkerController, registerEditor } from "@App/pkg/utils/monaco-editor"; import { fnPlaceHolder } from "@App/pages/store/AppContext"; +import { clearModelEslintFixes, getModelEslintFixKey } from "@App/pkg/utils/monaco-editor/eslintFixCache"; fnPlaceHolder.setEditorTheme = (theme: string) => editor.setTheme(theme); @@ -259,13 +260,13 @@ const CodeEditor = React.forwardRef<{ editor: editor.IStandaloneCodeEditor | und editor.setModelMarkers(model, "ESLint", message.markers); - // 更新 eslint-fix 快取(每次替换整个 map,避免已修复问题的过期条目残留) + // 更新当前 model 的 eslint-fix 快取,避免多个脚本编辑器互相覆盖 quick-fix。 const eslintFixMap = (window.MonacoEnvironment as any)?.eslintFixMap; if (eslintFixMap) { - eslintFixMap.clear(); + clearModelEslintFixes(eslintFixMap, model); message.markers.forEach((m: TMarker) => { if (m.fix) { - const key = `${m.code.value}|${m.startLineNumber}|${m.endLineNumber}|${m.startColumn}|${m.endColumn}`; + const key = getModelEslintFixKey(model, m.code.value, m); eslintFixMap.set(key, m.fix); } }); @@ -288,6 +289,10 @@ const CodeEditor = React.forwardRef<{ editor: editor.IStandaloneCodeEditor | und timer = null; } changeListener.dispose(); + const eslintFixMap = (window.MonacoEnvironment as any)?.eslintFixMap; + if (eslintFixMap) { + clearModelEslintFixes(eslintFixMap, model); + } LinterWorkerController.hookRemoveListener("message", messageHandler); }; }, [monacoEditor, enableEslint, eslintConfig, id]); diff --git a/src/pkg/utils/monaco-editor/eslintFixCache.test.ts b/src/pkg/utils/monaco-editor/eslintFixCache.test.ts new file mode 100644 index 000000000..06d71fb39 --- /dev/null +++ b/src/pkg/utils/monaco-editor/eslintFixCache.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import type { editor } from "monaco-editor"; +import { clearModelEslintFixes, getModelEslintFixKey, type EslintFix } from "./eslintFixCache"; + +const createMockModel = (uri: string): editor.ITextModel => + ({ + uri: { + toString: () => uri, + }, + }) as editor.ITextModel; + +const marker = { + startLineNumber: 1, + endLineNumber: 5, + startColumn: 1, + endColumn: 19, +}; + +const fix: EslintFix = { + range: { + startLineNumber: 2, + endLineNumber: 2, + startColumn: 9, + endColumn: 10, + }, + text: " ", +}; + +describe("eslint fix cache", () => { + it("uses the model uri in fix keys so identical markers from different editors do not collide", () => { + const modelA = createMockModel("inmemory://model/a"); + const modelB = createMockModel("inmemory://model/b"); + + expect(getModelEslintFixKey(modelA, "userscripts/align-attributes", marker)).not.toBe( + getModelEslintFixKey(modelB, "userscripts/align-attributes", marker) + ); + }); + + it("clears only fixes for the current model", () => { + const modelA = createMockModel("inmemory://model/a"); + const modelB = createMockModel("inmemory://model/b"); + const map = new Map(); + const keyA = getModelEslintFixKey(modelA, "userscripts/align-attributes", marker); + const keyB = getModelEslintFixKey(modelB, "userscripts/align-attributes", marker); + + map.set(keyA, fix); + map.set(keyB, fix); + + clearModelEslintFixes(map, modelA); + + expect(map.has(keyA)).toBe(false); + expect(map.has(keyB)).toBe(true); + }); +}); diff --git a/src/pkg/utils/monaco-editor/eslintFixCache.ts b/src/pkg/utils/monaco-editor/eslintFixCache.ts new file mode 100644 index 000000000..0f3479b3f --- /dev/null +++ b/src/pkg/utils/monaco-editor/eslintFixCache.ts @@ -0,0 +1,27 @@ +import type { editor, IRange } from "monaco-editor"; + +export type EslintFix = { + range: IRange; + text: string; +}; + +type EslintFixMarkerPosition = Pick< + editor.IMarkerData, + "startLineNumber" | "endLineNumber" | "startColumn" | "endColumn" +>; + +export const getEslintFixKey = (modelUri: string, eslintRuleId: string, marker: EslintFixMarkerPosition) => { + return `${modelUri}|${eslintRuleId}|${marker.startLineNumber}|${marker.endLineNumber}|${marker.startColumn}|${marker.endColumn}`; +}; + +export const getModelEslintFixKey = (model: editor.ITextModel, eslintRuleId: string, marker: EslintFixMarkerPosition) => + getEslintFixKey(model.uri.toString(), eslintRuleId, marker); + +export const clearModelEslintFixes = (eslintFixMap: Map, model: editor.ITextModel) => { + const prefix = `${model.uri.toString()}|`; + for (const key of eslintFixMap.keys()) { + if (key.startsWith(prefix)) { + eslintFixMap.delete(key); + } + } +}; diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 1d0886471..151134d8b 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -1,31 +1,89 @@ import { systemConfig } from "@App/pages/store/global"; import EventEmitter from "eventemitter3"; -import { languages } from "monaco-editor"; +import { editor, languages, MarkerSeverity } from "monaco-editor"; import { findGlobalInsertionInfo, updateGlobalCommentLine } from "./utils"; -import type { EditorLangCode, EditorPrompt } from "./langs"; +import type { EditorLangCode, EditorLangEntry } from "./langs"; import { asEditorLangEntry, editorLangs } from "./langs"; import { deferred } from "../utils"; +import { type EslintFix, getModelEslintFixKey } from "./eslintFixCache"; interface ILinterWorker extends Worker { myLinterHook: EventEmitter; } +type MetadataLineParts = { + prefix: string; + tag: string; + normalizedTag: MetadataTag; + spacing: string; + value: string; + suffix: string; +}; + +type MetadataTag = "connect" | "match" | "include"; + +type MetadataLineFix = { + code: string; + title: string; + text: string; +}; + +type TextEdit = languages.IWorkspaceTextEdit["textEdit"]; + +type MetadataAlignmentLine = { + lineNumber: number; + lineText: string; + prefix: string; + tag: string; + spacing: string; + value: string; + valueColumn: number; +}; + +type MetadataAlignmentBlock = { + startLineNumber: number; + endLineNumber: number; + lines: MetadataAlignmentLine[]; +}; + +type MetadataAlignmentFix = { + range: TextEdit["range"]; + text: string; +}; + +type ScriptcatMonacoEnvironment = typeof window.MonacoEnvironment & { + myLinterWorker?: ILinterWorker; + eslintFixMap?: Map; +}; + // 注册 eslint worker(全局单例) const linterWorkerDeferred = deferred(); -const langPromise = systemConfig.getLanguage(); +const configuredLanguagePromise = systemConfig.getLanguage(); + +let currentEditorLang: EditorLangEntry; +type EditorLangEntryPrompt = typeof currentEditorLang.prompt; +let promptByMetadataTag: EditorLangEntryPrompt; -let multiLang = asEditorLangEntry("en-US"); +const loadEditorLangEntry = (languageCode: EditorLangCode) => { + currentEditorLang = asEditorLangEntry(languageCode); + promptByMetadataTag = Object.fromEntries( + Object.entries(currentEditorLang.prompt).map(([metadataTag, prompt]) => [metadataTag.toLowerCase(), prompt]) + ) as typeof currentEditorLang.prompt; +}; + +loadEditorLangEntry("en-US"); -const updateLang = (lang: string) => { - lang = `${lang || ""}` as EditorLangCode | ""; - const key = ((Object.hasOwn(editorLangs, lang) && lang) || "en-US") as EditorLangCode; - multiLang = asEditorLangEntry(key); +const updateEditorLang = (language: string) => { + const requestedLanguageCode = `${language || ""}` as EditorLangCode | ""; + const supportedLanguageCode = ((Object.hasOwn(editorLangs, requestedLanguageCode) && requestedLanguageCode) || + "en-US") as EditorLangCode; + loadEditorLangEntry(supportedLanguageCode); }; -langPromise.then((res) => updateLang(res)); +configuredLanguagePromise.then((language) => updateEditorLang(language)); -systemConfig.addListener("language", (lang) => { - updateLang(lang); +systemConfig.addListener("language", (language) => { + updateEditorLang(language); }); export class LinterWorkerController { @@ -51,7 +109,590 @@ export class LinterWorkerController { } } -let isRegisterEditorDone = false; +let isEditorRegistered = false; + +const scriptcatMarkerOwner = "ScriptCat"; +const eslintMarkerOwner = "ESLint"; +const scriptcatMetadataAlignmentRuleId = "scriptcat/align-metadata-attributes"; +const scriptcatRemoveConnectWildcardRuleId = "scriptcat/remove-connect-wildcard"; +const scriptcatReplaceMatchTldWildcardRuleId = "scriptcat/replace-match-tld-wildcard-with-include"; +const scriptcatReplaceIncludeWithMatchRuleId = "scriptcat/replace-include-with-match"; +const quickfixKind = "quickfix"; +const noop = () => {}; +const metaLinePattern = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/; +const metadataFixPattern = /^(\s*\/\/[ \t]*@)(connect|match|include)([ \t]+)(\S+)(.*)$/i; +const metadataAlignmentPattern = /^(\s*\/\/[ \t]*@)(\S+)([ \t]+)(.*)$/; +const userscriptHeaderPattern = /^\s*\/\/[ \t]*==UserScript==[ \t]*$/; +const userscriptEndPattern = /^\s*\/\/[ \t]*==\/UserScript==[ \t]*$/; +const matchMetadataPattern = /^(\*|[-a-z]+|http\*):\/\/([^/]+)(\/.*)?$/i; +const noUndefMessagePattern = /^[^']*'([^']+)'[^']*$/; + +const getMonacoEnvironment = () => window.MonacoEnvironment as ScriptcatMonacoEnvironment | undefined; + +const ensureEslintFixMap = (environment: ScriptcatMonacoEnvironment) => { + environment.eslintFixMap ??= new Map(); + return environment.eslintFixMap; +}; + +const getMarkerCode = (marker: editor.IMarkerData) => { + if (!marker.code) return ""; + return typeof marker.code === "string" ? marker.code : marker.code.value; +}; + +const createTextEditAction = ( + model: editor.ITextModel, + title: string, + diagnostics: editor.IMarkerData[], + textEdit: TextEdit, + isPreferred: boolean +) => { + return { + title, + diagnostics, + kind: quickfixKind, + edit: { + edits: [{ resource: model.uri, textEdit, versionId: undefined }], + }, + isPreferred, + } satisfies languages.CodeAction; +}; + +const createTextEditsAction = ( + model: editor.ITextModel, + title: string, + diagnostics: editor.IMarkerData[], + textEdits: TextEdit[], + isPreferred: boolean +) => { + return { + title, + diagnostics, + kind: quickfixKind, + edit: { + edits: textEdits.map((textEdit) => ({ + resource: model.uri, + textEdit, + versionId: undefined, + })), + }, + isPreferred, + } satisfies languages.CodeAction; +}; + +const createLineReplacementAction = ( + model: editor.ITextModel, + title: string, + diagnostics: editor.IMarkerData[], + lineNumber: number, + lineText: string, + text: string, + isPreferred: boolean +) => { + return createTextEditAction( + model, + title, + diagnostics, + { + range: { + startLineNumber: lineNumber, + startColumn: 1, + endLineNumber: lineNumber, + endColumn: lineText.length + 1, + }, + text, + }, + isPreferred + ); +}; + +const isSimpleValidHost = (hostName: string) => { + if (!hostName) return false; + try { + hostName = hostName.toLowerCase(); + return new URL(`https://${hostName}.com/path`).origin === `https://${hostName}.com`; + } catch { + return false; + } +}; + +const parseMetadataLine = (lineText: string): MetadataLineParts | null => { + if (lineText.length < 6 || !lineText.includes("@")) return null; + const metadataMatch = metadataFixPattern.exec(lineText); + if (!metadataMatch) return null; + + const [, prefix, tag, spacing, value, suffix] = metadataMatch; + return { + prefix, + tag, + normalizedTag: tag.toLowerCase() as MetadataTag, + spacing, + value, + suffix, + }; +}; + +const createMetadataFix = (code: string, titleTemplate: string, titleValue: string, text: string): MetadataLineFix => { + return { + code, + title: titleTemplate.replace("{0}", titleValue), + text, + }; +}; + +const getIncludeSpacing = (spacing: string, tag: string) => { + const lenDiff = "include".length - tag.length; + return lenDiff > 0 && spacing.length > lenDiff ? spacing.slice(0, -lenDiff) : spacing; +}; + +const normalizeHost = (hostPattern: string) => { + const wildcardNormalizedHost = hostPattern + .split(".") + .map((hostSegment) => (hostSegment.includes("*") ? "*" : hostSegment)) + .join("."); + return wildcardNormalizedHost; +}; + +const getConnectMetadataFixes = ({ prefix, tag, spacing, value, suffix }: MetadataLineParts): MetadataLineFix[] => { + if (!value.startsWith("*.") || value.includes("**")) return []; + + const hostName = value.slice(2); + if (!/\.\w{2,}$/.test(hostName) || !isSimpleValidHost(hostName)) return []; + + const titleTemplate = currentEditorLang.removeConnectWildcard; + return [ + createMetadataFix( + scriptcatRemoveConnectWildcardRuleId, + titleTemplate, + hostName, + `${prefix}${tag}${spacing}${hostName}${suffix}` + ), + ]; +}; + +const getMatchMetadataFixes = ({ + prefix, + normalizedTag, + spacing, + value, + suffix, +}: MetadataLineParts): MetadataLineFix[] => { + if (!value || value.startsWith("/")) return []; + const metadataValueMatch = matchMetadataPattern.exec(value); + if (!metadataValueMatch || !metadataValueMatch[2]) return []; + const hostPattern = metadataValueMatch[2]; + const wildcardNormalizedHost = normalizeHost(hostPattern); + if ( + !wildcardNormalizedHost.endsWith(".*") || + !hostPattern.includes(".") || + hostPattern.includes("**") || + hostPattern.includes("\\") + ) + return []; + + const hostName = hostPattern.slice(0, hostPattern.lastIndexOf(".")); + if (!isSimpleValidHost(hostName.replace(/\*/g, "x"))) return []; + + const includeSpacing = getIncludeSpacing(spacing, normalizedTag); + const tldValue = `${metadataValueMatch[1]}://${hostName}.tld${metadataValueMatch[3] || ""}`; + + const titleTemplate = currentEditorLang.replaceMatchTldWildcardWithInclude; + const actions = []; + if (hostPattern.endsWith(".*")) { + actions.push( + createMetadataFix( + scriptcatReplaceMatchTldWildcardRuleId, + titleTemplate, + tldValue, + `${prefix}include${includeSpacing}${tldValue}${suffix}` + ) + ); + } + actions.push( + createMetadataFix( + scriptcatReplaceMatchTldWildcardRuleId, + titleTemplate, + value, + `${prefix}include${includeSpacing}${value}${suffix}` + ) + ); + return actions; +}; + +const getIncludeMetadataFixes = ({ + prefix, + normalizedTag, + spacing, + value, + suffix, +}: MetadataLineParts): MetadataLineFix[] => { + const metadataValueMatch = matchMetadataPattern.exec(value); + const hostPattern = metadataValueMatch?.[2]; + const wildcardNormalizedHost = hostPattern ? normalizeHost(hostPattern) : ""; + if ( + !metadataValueMatch || + !hostPattern || + wildcardNormalizedHost.endsWith(".*") || + hostPattern.includes("**") || + hostPattern.endsWith(".tld") + ) + return []; + if (wildcardNormalizedHost.split(".").every((hostSegment) => hostSegment === "*" || /^[\w-]+$/.test(hostSegment))) { + const includeSpacing = getIncludeSpacing(spacing, normalizedTag); + const titleTemplate = currentEditorLang.replaceIncludeWithMatch; + return [ + createMetadataFix( + scriptcatReplaceIncludeWithMatchRuleId, + titleTemplate, + value, + `${prefix}match ${includeSpacing}${value}${suffix}` + ), + ]; + } + return []; +}; + +const getMetadataLineFixes = (lineText: string): MetadataLineFix[] => { + const parts = parseMetadataLine(lineText); + if (!parts) return []; + + switch (parts.normalizedTag) { + case "connect": + return getConnectMetadataFixes(parts); + case "match": + return getMatchMetadataFixes(parts); + case "include": + return getIncludeMetadataFixes(parts); + default: + return []; + } +}; + +const getMetadataLineActions = ( + model: editor.ITextModel, + lineNumber: number, + lineText: string, + markers: editor.IMarkerData[] +): languages.CodeAction[] => { + const metadataFixes = getMetadataLineFixes(lineText); + if (metadataFixes.length === 0) return []; + + return metadataFixes.map((metadataFix, index) => + createLineReplacementAction( + model, + metadataFix.title, + markers.filter( + (marker) => + marker.source === scriptcatMarkerOwner && + marker.startLineNumber === lineNumber && + getMarkerCode(marker) === metadataFix.code + ), + lineNumber, + lineText, + metadataFix.text, + index === 0 + ) + ); +}; + +const getMetadataAlignmentLine = (lineNumber: number, lineText: string): MetadataAlignmentLine | null => { + const match = metadataAlignmentPattern.exec(lineText); + if (!match) return null; + + const [, prefix, tag, spacing, value] = match; + return { + lineNumber, + lineText, + prefix, + tag, + spacing, + value, + valueColumn: prefix.length + tag.length + spacing.length, + }; +}; + +const getMetadataAlignmentBlocks = (model: editor.ITextModel): MetadataAlignmentBlock[] => { + const blocks: MetadataAlignmentBlock[] = []; + const lineCount = model.getLineCount(); + let currentBlock: MetadataAlignmentBlock | null = null; + + const finishBlock = (endLineNumber: number) => { + if (!currentBlock) return; + currentBlock.endLineNumber = endLineNumber; + blocks.push(currentBlock); + currentBlock = null; + }; + + for (let lineNumber = 1; lineNumber <= lineCount; lineNumber += 1) { + const lineText = model.getLineContent(lineNumber); + + if (userscriptHeaderPattern.test(lineText)) { + finishBlock(lineNumber - 1); + currentBlock = { + startLineNumber: lineNumber, + endLineNumber: lineNumber, + lines: [], + }; + continue; + } + + if (!currentBlock) continue; + + const alignmentLine = getMetadataAlignmentLine(lineNumber, lineText); + if (alignmentLine) { + currentBlock.lines.push(alignmentLine); + } + + if (userscriptEndPattern.test(lineText)) { + finishBlock(lineNumber); + } + } + + finishBlock(lineCount); + return blocks; +}; + +const getMetadataAlignmentTargetColumn = (lines: MetadataAlignmentLine[]) => + Math.max(...lines.map((line) => line.prefix.length + line.tag.length + 1)); + +const isMetadataAlignmentBlockAligned = (block: MetadataAlignmentBlock) => { + if (block.lines.length < 2) return true; + const firstValueColumn = block.lines[0].valueColumn; + return block.lines.every((line) => line.valueColumn === firstValueColumn); +}; + +const getMetadataAlignmentFix = (model: editor.ITextModel, block: MetadataAlignmentBlock): MetadataAlignmentFix => { + const targetColumn = getMetadataAlignmentTargetColumn(block.lines); + const lineFixes = new Map( + block.lines.map((line) => { + const spacing = " ".repeat(Math.max(1, targetColumn - line.prefix.length - line.tag.length)); + return [line.lineNumber, `${line.prefix}${line.tag}${spacing}${line.value}`]; + }) + ); + const blockLines: string[] = []; + + for (let lineNumber = block.startLineNumber; lineNumber <= block.endLineNumber; lineNumber += 1) { + blockLines.push(lineFixes.get(lineNumber) ?? model.getLineContent(lineNumber)); + } + + return { + range: { + startLineNumber: block.startLineNumber, + startColumn: 1, + endLineNumber: block.endLineNumber, + endColumn: model.getLineContent(block.endLineNumber).length + 1, + }, + text: blockLines.join("\n"), + }; +}; + +const getMetadataAlignmentBlockAtLine = (model: editor.ITextModel, lineNumber: number) => + getMetadataAlignmentBlocks(model).find( + (block) => + block.startLineNumber <= lineNumber && + lineNumber <= block.endLineNumber && + !isMetadataAlignmentBlockAligned(block) + ); + +const getMetadataAlignmentActions = ( + model: editor.ITextModel, + lineNumber: number, + markers: editor.IMarkerData[] +): languages.CodeAction[] => { + const alignmentMarkers = markers.filter( + (marker) => + marker.source === scriptcatMarkerOwner && + getMarkerCode(marker) === scriptcatMetadataAlignmentRuleId && + marker.startLineNumber <= lineNumber && + lineNumber <= marker.endLineNumber + ); + if (alignmentMarkers.length === 0) return []; + + const block = getMetadataAlignmentBlockAtLine(model, lineNumber); + if (!block) return []; + + return [ + createTextEditsAction( + model, + currentEditorLang.quickfix.replace("{0}", scriptcatMetadataAlignmentRuleId), + alignmentMarkers, + [getMetadataAlignmentFix(model, block)], + true + ), + ]; +}; + +const getNoUndefGlobalName = (marker: editor.IMarkerData) => { + return noUndefMessagePattern.exec(marker.message)?.[1] || null; +}; + +const getGlobalDeclarationTextEdit = (model: editor.ITextModel, globalName: string): TextEdit => { + const { insertLine, globalLine } = findGlobalInsertionInfo(model); + + if (globalLine == null) { + return { + range: { + startLineNumber: insertLine, + startColumn: 1, + endLineNumber: insertLine, + endColumn: 1, + }, + text: `/* global ${globalName} */\n`, + }; + } + + const existingGlobalLineText = model.getLineContent(globalLine); + return { + range: { + startLineNumber: globalLine, + startColumn: 1, + endLineNumber: globalLine, + endColumn: existingGlobalLineText.length + 1, + }, + text: updateGlobalCommentLine(existingGlobalLineText, globalName), + }; +}; + +const getMarkerCodeActions = ( + model: editor.ITextModel, + marker: editor.IMarkerData, + eslintFixMap?: Map +): languages.CodeAction[] => { + if (marker.source !== eslintMarkerOwner) return []; + const eslintRuleId = getMarkerCode(marker); + if (!eslintRuleId) return []; + + const actions: languages.CodeAction[] = []; + + const eslintFix = eslintFixMap?.get(getModelEslintFixKey(model, eslintRuleId, marker)); + if (eslintFix) { + actions.push( + createTextEditAction( + model, + currentEditorLang.quickfix.replace("{0}", eslintRuleId), + [marker], + { + range: eslintFix.range, + text: eslintFix.text, + }, + true + ) + ); + } + + let canAddEslintDisableNextLine = true; + + switch (eslintRuleId) { + case "no-undef": { + const globalName = getNoUndefGlobalName(marker); + if (globalName) { + actions.push( + createTextEditAction( + model, + currentEditorLang.declareGlobal.replace("{0}", globalName), + [marker], + getGlobalDeclarationTextEdit(model, globalName), + false + ) + ); + } + break; + } + case "userscripts/align-attributes": + case "userscripts/better-use-match": + case "userscripts/no-invalid-headers": + canAddEslintDisableNextLine = false; + } + + if (canAddEslintDisableNextLine) { + actions.push( + createTextEditAction( + model, + currentEditorLang.addEslintDisableNextLine, + [marker], + { + range: { + startLineNumber: marker.startLineNumber, + endLineNumber: marker.startLineNumber, + startColumn: 1, + endColumn: 1, + }, + text: `// eslint-disable-next-line ${eslintRuleId}\n`, + }, + true + ) + ); + } + actions.push( + createTextEditAction( + model, + currentEditorLang.addEslintDisable, + [marker], + { + range: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 1, + }, + text: `/* eslint-disable ${eslintRuleId} */\n`, + }, + true + ) + ); + + return actions; +}; + +const updateScriptcatMetadataMarkers = (model: editor.ITextModel) => { + if (model.getLanguageId() !== "javascript") return; + + const markers: editor.IMarkerData[] = []; + for (const block of getMetadataAlignmentBlocks(model)) { + if (isMetadataAlignmentBlockAligned(block)) continue; + markers.push({ + severity: MarkerSeverity.Warning, + message: currentEditorLang.quickfix.replace("{0}", scriptcatMetadataAlignmentRuleId), + source: scriptcatMarkerOwner, + code: scriptcatMetadataAlignmentRuleId, + startLineNumber: block.startLineNumber, + startColumn: 1, + endLineNumber: block.endLineNumber, + endColumn: model.getLineContent(block.endLineNumber).length + 1, + }); + } + + const lineCount = model.getLineCount(); + for (let lineNumber = 1; lineNumber <= lineCount; lineNumber += 1) { + const lineText = model.getLineContent(lineNumber); + const metadataLineFixes = getMetadataLineFixes(lineText); + if (metadataLineFixes.length === 0) continue; + + markers.push({ + severity: MarkerSeverity.Warning, + message: metadataLineFixes[0].title, + source: scriptcatMarkerOwner, + code: metadataLineFixes[0].code, + startLineNumber: lineNumber, + startColumn: 1, + endLineNumber: lineNumber, + endColumn: lineText.length + 1, + }); + } + + editor.setModelMarkers(model, scriptcatMarkerOwner, markers); +}; + +const registerScriptcatMetadataMarkerProvider = () => { + const registerMetadataModel = (model: editor.ITextModel) => { + updateScriptcatMetadataMarkers(model); + model.onDidChangeContent(() => { + updateScriptcatMetadataMarkers(model); + }); + }; + + editor.getModels().forEach(registerMetadataModel); + editor.onDidCreateModel(registerMetadataModel); +}; /** * 注册 monaco-editor 的全局环境与语言支援 @@ -59,12 +700,14 @@ let isRegisterEditorDone = false; */ export function registerEditor() { // 避免重复注册 - if (isRegisterEditorDone) return; - isRegisterEditorDone = true; + if (isEditorRegistered) return; + isEditorRegistered = true; // worker 初始化:复用已有 worker 或创建新的 - if ((window.MonacoEnvironment as any)?.myLinterWorker) { - linterWorkerDeferred.resolve((window.MonacoEnvironment as any)?.myLinterWorker); + const existingEnvironment = getMonacoEnvironment(); + if (existingEnvironment?.myLinterWorker) { + ensureEslintFixMap(existingEnvironment); + linterWorkerDeferred.resolve(existingEnvironment.myLinterWorker); } else { const linterWorker = new Worker("/src/linter.worker.js") as ILinterWorker; linterWorker.myLinterHook = new EventEmitter(); @@ -74,201 +717,66 @@ export function registerEditor() { }; window.MonacoEnvironment = { - getWorkerUrl(moduleId: any, label: any) { + ...existingEnvironment, + getWorkerUrl(_moduleId: unknown, label: string) { if (label === "typescript" || label === "javascript") { return "/src/ts.worker.js"; } return "/src/editor.worker.js"; }, - }; - - Object.assign(window.MonacoEnvironment, { myLinterWorker: linterWorker, - eslintFixMap: new Map(), - }); + eslintFixMap: new Map(), + } as ScriptcatMonacoEnvironment; linterWorkerDeferred.resolve(linterWorker); } // provider 注册始终执行,不受 worker 复用影响 - const META_LINE = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/; + registerScriptcatMetadataMarkerProvider(); languages.registerHoverProvider("javascript", { provideHover: (model, position) => { - return new Promise((resolve) => { - const line = model.getLineContent(position.lineNumber); - const m = META_LINE.exec(line); - if (m) { - const key = m[1] as keyof EditorPrompt; - const prompt = multiLang.prompt; - resolve({ - contents: [ - { - value: prompt[key] || multiLang.undefinedPrompt, - supportHtml: true, - }, - ], - }); - } else if (/==UserScript==/.test(line)) { - resolve({ - contents: [{ value: multiLang.thisIsAUserScript }], - }); - } else { - resolve(null); - } - }); - }, - }); + const lineText = model.getLineContent(position.lineNumber); + const metadataCommentMatch = metaLinePattern.exec(lineText); - languages.registerCodeActionProvider("javascript", { - provideCodeActions: (model /** ITextModel */, range /** Range */, context /** CodeActionContext */) => { - const actions: languages.CodeAction[] = []; - const eslintFixMap = >(window.MonacoEnvironment as any)?.eslintFixMap; - - for (let i = 0; i < context.markers.length; i++) { - // 判断有没有修复方案 - const val = context.markers[i]; - const code = typeof val.code === "string" ? val.code : val.code!.value; - - // 1. eslint-fix - const baseKey = `${code}|${val.startLineNumber}|${val.endLineNumber}|${val.startColumn}|${val.endColumn}`; - const fix = eslintFixMap?.get(baseKey); - if (fix) { - actions.push({ - title: multiLang.quickfix.replace("{0}", code), - diagnostics: [val], - kind: "quickfix", - edit: { - edits: [ - { - resource: model.uri, - textEdit: { - range: fix.range, - text: fix.text, - }, - versionId: undefined, - }, - ], + if (metadataCommentMatch) { + const metadataTag = metadataCommentMatch[1].toLowerCase() as keyof EditorLangEntryPrompt; + return { + contents: [ + { + value: promptByMetadataTag[metadataTag] || currentEditorLang.undefinedPrompt, + supportHtml: true, }, - isPreferred: true, - } satisfies languages.CodeAction); - } - - // 2. no-undef → /* global */ - if (code === "no-undef") { - const message = val.message || ""; - const match = message.match(/^[^']*'([^']+)'[^']*$/); - const globalName = match?.[1]; - - if (globalName) { - const { insertLine, globalLine } = findGlobalInsertionInfo(model); - let textEdit: languages.IWorkspaceTextEdit["textEdit"]; - - if (globalLine != null) { - // there is already a /* global ... */ line → update it - const oldLine = model.getLineContent(globalLine); - const newLine = updateGlobalCommentLine(oldLine, globalName); - textEdit = { - range: { - startLineNumber: globalLine, - startColumn: 1, - endLineNumber: globalLine, - endColumn: oldLine.length + 1, - }, - text: newLine, - }; - } else { - // no global line yet → insert a new one - textEdit = { - range: { - startLineNumber: insertLine, - startColumn: 1, - endLineNumber: insertLine, - endColumn: 1, - }, - text: `/* global ${globalName} */\n`, - }; - } - - actions.push({ - title: multiLang.declareGlobal.replace("{0}", globalName), - diagnostics: [val], - kind: "quickfix", - edit: { edits: [{ resource: model.uri, textEdit, versionId: undefined }] }, - isPreferred: false, - } satisfies languages.CodeAction); - } - } + ], + }; + } - // 3. disable-next-line / disable - actions.push({ - title: multiLang.addEslintDisableNextLine, - diagnostics: [val], - kind: "quickfix", - edit: { - edits: [ - { - resource: model.uri, - textEdit: { - range: { - startLineNumber: val.startLineNumber, - endLineNumber: val.startLineNumber, - startColumn: 1, - endColumn: 1, - }, - text: `// eslint-disable-next-line ${code}\n`, - }, - versionId: undefined, - }, - ], - }, - isPreferred: true, - } satisfies languages.CodeAction); - - actions.push({ - title: multiLang.addEslintDisable, - diagnostics: [val], - kind: "quickfix", - edit: { - edits: [ - { - resource: model.uri, - textEdit: { - range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - text: `/* eslint-disable ${code} */\n`, - }, - versionId: undefined, - }, - ], - }, - isPreferred: true, - } satisfies languages.CodeAction); + if (/==UserScript==/.test(lineText)) { + return { contents: [{ value: currentEditorLang.thisIsAUserScript }] }; } - // const actions = context.markers.map((error) => { - // const edit: languages.IWorkspaceTextEdit = { - // resource: model.uri, - // textEdit: { - // range, - // text: "console.log(1)", - // }, - // versionId: undefined, - // }; - // return { - // title: ``, - // diagnostics: [error], - // kind: "quickfix", - // edit: { - // edits: [edit], - // }, - // isPreferred: true, - // }; - // }); - - return { actions, dispose: () => {} }; + return null; }, }); + languages.registerCodeActionProvider( + "javascript", + { + provideCodeActions: (model /** ITextModel */, range /** Range */, context /** CodeActionContext */) => { + const eslintFixMap = getMonacoEnvironment()?.eslintFixMap; + const lineText = model.getLineContent(range.startLineNumber); + const actions = [ + ...getMetadataLineActions(model, range.startLineNumber, lineText, context.markers), + ...getMetadataAlignmentActions(model, range.startLineNumber, context.markers), + ...context.markers.flatMap((marker) => getMarkerCodeActions(model, marker, eslintFixMap)), + ]; + + return { actions, dispose: noop }; + }, + }, + { providedCodeActionKinds: ["quickfix"] } + ); + // 设定编译器选项与额外类型定义 Promise.all([systemConfig.getEditorConfig(), systemConfig.getEditorTypeDefinition()]).then( ([editorConfig, typeDefinition]) => { diff --git a/src/pkg/utils/monaco-editor/langs.ts b/src/pkg/utils/monaco-editor/langs.ts index 42507baee..ed2147e7e 100644 --- a/src/pkg/utils/monaco-editor/langs.ts +++ b/src/pkg/utils/monaco-editor/langs.ts @@ -7,6 +7,9 @@ export const editorLangs = { addEslintDisableNextLine: "添加 eslint-disable-next-line 注释", addEslintDisable: "添加 eslint-disable 注释", declareGlobal: "将 '{0}' 声明为全局变量 (/* global */)", + removeConnectWildcard: "移除 @connect 通配符,改为 {0}", + replaceMatchTldWildcardWithInclude: "将 @match 顶级域名通配符改为 @include {0}", + replaceIncludeWithMatch: "将 @include 改为 @match {0}", prompt: { name: "脚本名称", namespace: "脚本命名空间", @@ -56,6 +59,13 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), downloadURL: "脚本更新的下载地址", supportURL: "支持站点、bug 反馈页面", source: "脚本源码页", + scriptUrl: "订阅脚本中引用的用户脚本地址", + storageName: "脚本值存储空间名称,用于让多个脚本共享同一个存储空间", + tag: "脚本标签,多个标签可用逗号或空格分隔", + cloudCat: "标记脚本支持导出为 CloudCat 云端脚本包", + cloudServer: "脚本使用的 CloudCat 云端服务", + exportValue: "导出为云端脚本时需要导出的脚本存储值", + exportCookie: "导出为云端脚本时需要导出的 Cookie", crontab: `定时脚本 crontab 参考(不适用于云端脚本) * * * * * * 每秒运行一次 * * * * * 每分钟运行一次 @@ -83,6 +93,9 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisableNextLine: "Add eslint-disable-next-line Comment", addEslintDisable: "Add eslint-disable Comment", declareGlobal: "Declare '{0}' as a global variable (/* global */)", + removeConnectWildcard: "Remove @connect wildcard: {0}", + replaceMatchTldWildcardWithInclude: "Replace @match TLD wildcard with @include {0}", + replaceIncludeWithMatch: "Replace @include with @match {0}", prompt: { name: "Script name", namespace: "Script namespace", @@ -120,11 +133,24 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), unwrap: "Makes the user script bypass sandbox wrapping and be injected and executed directly in the page’s native global scope.
The script can directly access and modify the page’s real global variables, but will not be able to use user script privileged APIs such as GM.*.
Commonly used in scenarios that require deep interaction with native page scripts or when migrating from regular page scripts.", definition: "ScriptCat-only: URL of a `.d.ts` file used for editor auto-completion", - antifeature: "For script markets: describe any unwanted or controversial features", + antifeature: `Related to script markets: unwanted features should include this description value +referral-link: This script modifies or redirects to the author's referral link +ads: This script inserts ads on the pages you visit +payment: This script requires payment to be used properly +miner: This script engages in mining activities +membership: This script requires registration as a member to be used properly +tracking: This script tracks your user information`.replace(/\n/g, "
"), updateURL: "URL used to check for script updates", downloadURL: "URL used to download script updates", supportURL: "Support site / bug report page", source: "Script source code page", + scriptUrl: "User script URL referenced by a subscription script", + storageName: "Script value storage name, used to share one storage area across multiple scripts", + tag: "Script tags, separated by commas or spaces", + cloudCat: "Marks the script as exportable to a CloudCat cloud script package", + cloudServer: "CloudCat cloud service used by the script", + exportValue: "Script storage values to export when exporting as a cloud script", + exportCookie: "Cookies to export when exporting as a cloud script", crontab: `Scheduled script crontab examples (not for cloud scripts) * * * * * * Run every second * * * * * Run every minute @@ -152,6 +178,9 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisableNextLine: "新增 eslint-disable-next-line 註解", addEslintDisable: "新增 eslint-disable 註解", declareGlobal: "將 '{0}' 宣告為全域變數 (/* global */)", + removeConnectWildcard: "移除 @connect 萬用字元,改為 {0}", + replaceMatchTldWildcardWithInclude: "將 @match 頂級網域萬用字元改為 @include {0}", + replaceIncludeWithMatch: "將 @include 改為 @match {0}", prompt: { name: "腳本名稱", namespace: "腳本命名空間", @@ -189,11 +218,24 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), unwrap: "讓使用者腳本不經過沙箱封裝,直接注入並執行於頁面的原生全域作用域中。
腳本可直接存取並修改頁面真實的全域變數,但將無法使用 GM.* 等使用者腳本的特權 API。
常用於需要與頁面原生腳本深度互動,或從一般頁面腳本遷移的場景。", definition: "ScriptCat 特有功能:一個 `.d.ts` 檔案的引用網址,可啟用編輯器自動提示", - antifeature: "與腳本市場相關,不受歡迎的功能需要在此描述", + antifeature: `與腳本市場相關,不受歡迎的功能需要加上此描述值 +referral-link:此腳本會修改或重新導向至作者的返傭連結 +ads:此腳本會在您存取的頁面上插入廣告 +payment:此腳本需要您付費才能正常使用 +miner:此腳本存在挖礦行為 +membership:此腳本需要註冊會員才能正常使用 +tracking:此腳本會追蹤您的使用者資訊`.replace(/\n/g, "
"), updateURL: "腳本檢查更新的 url", downloadURL: "腳本更新的下載網址", supportURL: "支援站點、錯誤回報頁面", source: "腳本原始碼頁面", + scriptUrl: "訂閱腳本中引用的使用者腳本網址", + storageName: "腳本值儲存空間名稱,用於讓多個腳本共享同一個儲存空間", + tag: "腳本標籤,多個標籤可用逗號或空格分隔", + cloudCat: "標記腳本支援匯出為 CloudCat 雲端腳本套件", + cloudServer: "腳本使用的 CloudCat 雲端服務", + exportValue: "匯出為雲端腳本時需要匯出的腳本儲存值", + exportCookie: "匯出為雲端腳本時需要匯出的 Cookie", crontab: `排程腳本 crontab 參考(不適用於雲端腳本) * * * * * * 每秒執行一次 * * * * * 每分鐘執行一次 @@ -221,6 +263,9 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisableNextLine: "eslint-disable-next-line コメントを追加", addEslintDisable: "eslint-disable コメントを追加", declareGlobal: "'{0}' をグローバル変数として宣言 (/* global */)", + removeConnectWildcard: "@connect のワイルドカードを削除: {0}", + replaceMatchTldWildcardWithInclude: "@match の TLD ワイルドカードを @include {0} に置換", + replaceIncludeWithMatch: "@include を @match {0} に置換", prompt: { name: "スクリプト名", namespace: "スクリプトの名前空間", @@ -258,11 +303,24 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), unwrap: "ユーザースクリプトをサンドボックスでラップせず、ページのネイティブなグローバルスコープに直接注入して実行します。
スクリプトはページの実際のグローバル変数に直接アクセスおよび変更できますが、GM.* などのユーザースクリプトの特権 API は使用できなくなります。
ページのネイティブスクリプトとの深い連携が必要な場合や、通常のページスクリプトから移行する際によく使用されます。", definition: "ScriptCat 専用機能:`.d.ts` ファイルの URL。エディタの補完を有効にします。", - antifeature: "スクリプトマーケット向け:好まれない機能がある場合、ここに説明を記載します。", + antifeature: `スクリプトマーケットに関連します。好まれない機能にはこの説明値を追加する必要があります +referral-link:このスクリプトは作者のアフィリエイトリンクに変更またはリダイレクトします +ads:このスクリプトはアクセスしたページに広告を挿入します +payment:このスクリプトは正常に使用するために支払いが必要です +miner:このスクリプトにはマイニング動作があります +membership:このスクリプトは正常に使用するためにメンバー登録が必要です +tracking:このスクリプトはユーザー情報を追跡します`.replace(/\n/g, "
"), updateURL: "スクリプト更新を確認する URL", downloadURL: "スクリプト更新をダウンロードする URL", supportURL: "サポートサイト・バグ報告ページ", source: "スクリプトのソースコードページ", + scriptUrl: "サブスクリプションスクリプトで参照するユーザースクリプト URL", + storageName: "複数のスクリプトで同じ保存領域を共有するためのスクリプト値ストレージ名", + tag: "スクリプトタグ。複数のタグはカンマまたはスペースで区切ります", + cloudCat: "スクリプトを CloudCat クラウドスクリプトパッケージとしてエクスポート可能にする印", + cloudServer: "スクリプトが使用する CloudCat クラウドサービス", + exportValue: "クラウドスクリプトとしてエクスポートする際に出力するスクリプト保存値", + exportCookie: "クラウドスクリプトとしてエクスポートする際に出力する Cookie", crontab: `スケジュールスクリプトの crontab 例(クラウドスクリプトには非対応) * * * * * * 毎秒実行 * * * * * 毎分実行 @@ -290,6 +348,9 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisableNextLine: "eslint-disable-next-line Kommentar hinzufügen", addEslintDisable: "eslint-disable Kommentar hinzufügen", declareGlobal: "'{0}' als globale Variable deklarieren (/* global */)", + removeConnectWildcard: "@connect-Wildcard entfernen: {0}", + replaceMatchTldWildcardWithInclude: "@match-TLD-Wildcard durch @include {0} ersetzen", + replaceIncludeWithMatch: "@include durch @match {0} ersetzen", prompt: { name: "Skriptname", namespace: "Skript-Namensraum", @@ -327,11 +388,25 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), unwrap: "Ermöglicht es, das Benutzerskript ohne Sandbox-Kapselung direkt in den nativen globalen Gültigkeitsbereich der Seite zu injizieren und auszuführen.
Das Skript kann direkt auf die tatsächlichen globalen Variablen der Seite zugreifen und diese verändern, kann jedoch keine privilegierten Benutzerskript-APIs wie GM.* verwenden.
Wird häufig in Szenarien eingesetzt, die eine tiefe Interaktion mit nativen Seitenskripten erfordern oder bei der Migration von normalen Seitenskripten.", definition: "Nur für ScriptCat: URL zu einer `.d.ts`-Datei für Editor-Autovervollständigung", - antifeature: "Für Script-Marktplätze: hier unerwünschte oder kontroverse Funktionen beschreiben", + antifeature: + `Bezieht sich auf Script-Marktplätze: unerwünschte Funktionen sollten diesen Beschreibungswert enthalten +referral-link: Dieses Skript modifiziert oder leitet zu den Affiliate-Links des Autors um +ads: Dieses Skript fügt Werbung auf den von Ihnen besuchten Seiten ein +payment: Dieses Skript erfordert eine Zahlung für die normale Nutzung +miner: Dieses Skript hat Mining-Verhalten +membership: Dieses Skript erfordert eine Mitgliedschaftsregistrierung für die normale Nutzung +tracking: Dieses Skript verfolgt Ihre Benutzerinformationen`.replace(/\n/g, "
"), updateURL: "URL zur Aktualisierungsprüfung des Skripts", downloadURL: "URL zum Herunterladen von Skriptaktualisierungen", supportURL: "Support-Seite / Bugtracker", source: "Quellcode-Seite des Skripts", + scriptUrl: "Benutzerskript-URL, die von einem Abonnement-Skript referenziert wird", + storageName: "Speichername für Skriptwerte, um einen Speicherbereich mit mehreren Skripten zu teilen", + tag: "Skript-Tags, getrennt durch Kommas oder Leerzeichen", + cloudCat: "Markiert das Skript als exportierbar in ein CloudCat-Cloud-Skriptpaket", + cloudServer: "Vom Skript verwendeter CloudCat-Clouddienst", + exportValue: "Skript-Speicherwerte, die beim Export als Cloud-Skript exportiert werden", + exportCookie: "Cookies, die beim Export als Cloud-Skript exportiert werden", crontab: `Beispiele für geplante Skripte (crontab, nicht für Cloud-Skripte) * * * * * * Jede Sekunde ausführen * * * * * Jede Minute ausführen @@ -359,6 +434,9 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisableNextLine: "Thêm chú thích eslint-disable-next-line", addEslintDisable: "Thêm chú thích eslint-disable", declareGlobal: "Khai báo '{0}' là biến toàn cục (/* global */)", + removeConnectWildcard: "Bỏ ký tự đại diện @connect: {0}", + replaceMatchTldWildcardWithInclude: "Thay wildcard TLD @match bằng @include {0}", + replaceIncludeWithMatch: "Thay @include bằng @match {0}", prompt: { name: "Tên script", namespace: "Namespace của script", @@ -396,11 +474,24 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), unwrap: "Cho phép script người dùng bỏ qua sandbox và được chèn, thực thi trực tiếp trong phạm vi toàn cục gốc của trang.
Script có thể trực tiếp truy cập và chỉnh sửa các biến toàn cục thực sự của trang, nhưng sẽ không thể sử dụng các API đặc quyền của user script như GM.*.
Thường được dùng trong các trường hợp cần tương tác sâu với script gốc của trang hoặc khi chuyển đổi từ script trang thông thường.", definition: "Tính năng riêng của ScriptCat: URL tới tệp `.d.ts` giúp bật gợi ý tự động trong trình soạn thảo", - antifeature: "Dùng cho chợ script: mô tả các tính năng không được người dùng ưa thích", + antifeature: `Liên quan đến chợ script: các tính năng không được ưa thích cần thêm giá trị mô tả này +referral-link: Script này sửa đổi hoặc chuyển hướng đến liên kết giới thiệu của tác giả +ads: Script này chèn quảng cáo vào các trang bạn truy cập +payment: Script này yêu cầu thanh toán để sử dụng đúng cách +miner: Script này tham gia vào các hoạt động đào coin +membership: Script này yêu cầu đăng ký làm thành viên để sử dụng đúng cách +tracking: Script này theo dõi thông tin người dùng của bạn`.replace(/\n/g, "
"), updateURL: "URL dùng để kiểm tra cập nhật script", downloadURL: "URL tải về bản cập nhật script", supportURL: "Trang hỗ trợ / báo lỗi", source: "Trang mã nguồn script", + scriptUrl: "URL user script được tham chiếu bởi script đăng ký", + storageName: "Tên vùng lưu trữ giá trị script, dùng để chia sẻ cùng một vùng lưu trữ giữa nhiều script", + tag: "Thẻ script, phân tách bằng dấu phẩy hoặc khoảng trắng", + cloudCat: "Đánh dấu script có thể xuất thành gói cloud script CloudCat", + cloudServer: "Dịch vụ CloudCat cloud mà script sử dụng", + exportValue: "Giá trị lưu trữ script cần xuất khi xuất thành cloud script", + exportCookie: "Cookie cần xuất khi xuất thành cloud script", crontab: `Ví dụ crontab cho script chạy định kỳ (không áp dụng cho script trên cloud) * * * * * * Chạy mỗi giây * * * * * Chạy mỗi phút @@ -428,6 +519,9 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), addEslintDisableNextLine: "Добавить комментарий eslint-disable-next-line", addEslintDisable: "Добавить комментарий eslint-disable", declareGlobal: "Объявить '{0}' как глобальную переменную (/* global */)", + removeConnectWildcard: "Удалить wildcard @connect: {0}", + replaceMatchTldWildcardWithInclude: "Заменить TLD wildcard @match на @include {0}", + replaceIncludeWithMatch: "Заменить @include на @match {0}", prompt: { name: "Имя скрипта", namespace: "Пространство имён скрипта", @@ -465,11 +559,25 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), unwrap: "Позволяет пользовательскому скрипту обходить песочницу и напрямую внедряться и выполняться в нативной глобальной области видимости страницы.
Скрипт может напрямую получать доступ к реальным глобальным переменным страницы и изменять их, однако не сможет использовать привилегированные API пользовательских скриптов, такие как GM.*.
Обычно используется в сценариях, требующих глубокой интеграции с нативными скриптами страницы или при миграции с обычных скриптов страницы.", definition: "Особенность ScriptCat: URL файла `.d.ts`, используемого для автодополнения в редакторе", - antifeature: "Для маркетплейсов скриптов: опишите здесь нежелательные / спорные функции", + antifeature: `Связано с маркетплейсами скриптов: для нежелательных функций следует добавить это значение описания +referral-link: Этот скрипт изменяет или перенаправляет на реферальную ссылку автора +ads: Этот скрипт вставляет рекламу на посещаемые вами страницы +payment: Этот скрипт требует оплаты для нормального использования +miner: Этот скрипт содержит функции майнинга +membership: Этот скрипт требует регистрации членства для нормального использования +tracking: Этот скрипт отслеживает информацию о пользователе`.replace(/\n/g, "
"), updateURL: "URL для проверки обновлений скрипта", downloadURL: "URL для загрузки обновлений скрипта", supportURL: "Страница поддержки / отчёта об ошибках", source: "Страница с исходным кодом скрипта", + scriptUrl: "URL пользовательского скрипта, на который ссылается скрипт подписки", + storageName: + "Имя хранилища значений скрипта для совместного использования одного хранилища несколькими скриптами", + tag: "Теги скрипта, разделённые запятыми или пробелами", + cloudCat: "Отмечает, что скрипт можно экспортировать в пакет облачного скрипта CloudCat", + cloudServer: "Облачный сервис CloudCat, используемый скриптом", + exportValue: "Значения хранилища скрипта для экспорта при экспорте как облачного скрипта", + exportCookie: "Cookie для экспорта при экспорте как облачного скрипта", crontab: `Примеры crontab для планового запуска скриптов (не для облачных скриптов) * * * * * * Запуск каждую секунду * * * * * Запуск каждую минуту @@ -491,7 +599,6 @@ tracking:该脚本会追踪你的用户信息`.replace(/\n/g, "
"), } as const; export type EditorLangCode = keyof typeof editorLangs; -export type EditorPrompt = (typeof editorLangs)["zh-CN"]["prompt"]; export type EditorLangEntry = (typeof editorLangs)["zh-CN"]; export function asEditorLangEntry(key: T) { diff --git a/src/types/eslint-linter-browserify.d.ts b/src/types/eslint-linter-browserify.d.ts new file mode 100644 index 000000000..1af63e438 --- /dev/null +++ b/src/types/eslint-linter-browserify.d.ts @@ -0,0 +1,3 @@ +declare module "eslint-linter-browserify" { + export { Linter } from "eslint"; +}