diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5579381..8d5b6e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,11 +9,16 @@ jobs: build: runs-on: ubuntu-latest + permissions: + contents: write + attestations: write + id-token: write + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: "20.x" @@ -22,6 +27,13 @@ jobs: npm install npm run build + - name: Generate artifact attestations + uses: actions/attest-build-provenance@v2 + with: + subject-path: | + main.js + styles.css + - name: Extract release notes from CHANGELOG id: changelog run: | @@ -67,3 +79,4 @@ jobs: --draft \ --notes-file release_notes.md \ main.js manifest.json styles.css + diff --git a/CHANGELOG.md b/CHANGELOG.md index 04ce187..1e8121a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [1.6.2] - 2026-05-15 + +### 🔧 Maintenance + +- Replaced `builtin-modules` (dev dependency) with Node.js built-in `module.builtinModules` +- Replaced `dotenv` dependency with native `.env` parsing in the copy-to-vault script +- Added GitHub artifact attestations for `main.js` and `styles.css` release assets +- Fixed all Obsidian plugin linter warnings: + - Used `activeDocument` instead of `document` for popout window compatibility + - Added `defaultPrevented` check and `preventDefault()` call in `editor-paste` handler per Obsidian protocol + - Replaced `MarkdownView | MarkdownFileInfo` union with `MarkdownFileInfo` (parent type) in paste handler + - Used `Vault#configDir` instead of hardcoded `.obsidian` path + - Used `getLanguage()` instead of `localStorage.getItem("language")` + - Used `setIcon()` (Lucide) instead of inline SVG for the info icon in settings + - Added `void` operator on unhandled `navigator.clipboard.writeText()` and async calls + - Replaced deprecated `substr()` with `substring()` + - Removed unnecessary non-null assertion after narrowing guard + +
+改动内容(点击展开中文说明) + +### 🔧 维护 + +- 移除 `builtin-modules` 依赖,改用 Node.js 内置 `module.builtinModules` +- 移除 `dotenv` 依赖,在 copy-to-vault 脚本中改用原生 `.env` 解析 +- 为 release 产物 `main.js` 和 `styles.css` 添加 GitHub Artifact Attestation(加密溯源证明) +- 修复所有 Obsidian 插件 linter 警告: + - 使用 `activeDocument` 替代 `document`,兼容弹出窗口 + - 在 `editor-paste` handler 中补充 `defaultPrevented` 检查与 `preventDefault()` 调用 + - 将粘贴 handler 的参数类型从 `MarkdownView | MarkdownFileInfo` 简化为 `MarkdownFileInfo` + - 使用 `Vault#configDir` 替代硬编码的 `.obsidian` 路径 + - 使用 `getLanguage()` 替代 `localStorage.getItem("language")` + - 设置页 info 图标改用 `setIcon()`(Lucide)替代内联 SVG + - 为未处理的 `navigator.clipboard.writeText()` 和异步调用补加 `void` 运算符 + - 将废弃的 `substr()` 替换为 `substring()` + - 移除守卫语句后不必要的非空断言 + +
+ +--- + ## [1.6.1] - 2026-05-12 ### ✨ Added diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 309f17b..5257877 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,6 +1,6 @@ import esbuild from "esbuild"; import process from "process"; -import builtins from "builtin-modules"; +import { builtinModules } from "module"; const banner = `/* @@ -31,7 +31,7 @@ const context = await esbuild.context({ "@lezer/common", "@lezer/highlight", "@lezer/lr", - ...builtins], + ...builtinModules], format: "cjs", target: "es2018", logLevel: "info", diff --git a/manifest.json b/manifest.json index a0b8a76..6e8ef0b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "easy-copy", "name": "Easy Copy", - "version": "1.6.1", + "version": "1.6.2", "minAppVersion": "0.15.0", "description": "Easily copy the text within inline code, bold text (and many other formats), or quickly generate an elegant link to a heading.", "author": "Moy", diff --git a/package.json b/package.json index a0150f6..766fd1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-sample-plugin", - "version": "1.6.1", + "version": "1.6.2", "description": "This is a sample plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { @@ -24,7 +24,6 @@ "@types/node": "^20.19.0", "@typescript-eslint/eslint-plugin": "^8.58.0", "@typescript-eslint/parser": "^8.58.0", - "builtin-modules": "3.3.0", "esbuild": "0.17.3", "eslint": "^9.39.4", "eslint-plugin-obsidianmd": "^0.1.9", @@ -33,7 +32,5 @@ "typescript": "^5.9.3", "vitest": "^4.1.4" }, - "dependencies": { - "dotenv": "^16.6.1" - } + "dependencies": {} } diff --git a/scripts/copy-to-vault.mjs b/scripts/copy-to-vault.mjs index fb407be..5c4fadc 100644 --- a/scripts/copy-to-vault.mjs +++ b/scripts/copy-to-vault.mjs @@ -1,16 +1,27 @@ // copy-to-vault.mjs -import { copyFile, mkdir } from 'fs/promises'; +import { copyFile, mkdir, readFile } from 'fs/promises'; import { existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; -import dotenv from 'dotenv'; // 获取当前文件的目录 const __dirname = dirname(fileURLToPath(import.meta.url)); const rootDir = join(__dirname, '..'); -// 加载 .env 文件中的环境变量 -dotenv.config(); +// 手动加载 .env 文件中的环境变量(替代 dotenv,避免额外依赖) +const envPath = join(rootDir, '.env'); +if (existsSync(envPath)) { + const envContent = await readFile(envPath, 'utf-8'); + for (const line of envContent.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eqIdx = trimmed.indexOf('='); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + const value = trimmed.slice(eqIdx + 1).trim().replace(/^['"]|['"]$/g, ''); + if (key && !(key in process.env)) process.env[key] = value; + } +} // 获取 VAULT_PATH 环境变量 const VAULT_PATH = process.env.VAULT_PATH; if (!VAULT_PATH) { diff --git a/src/main.ts b/src/main.ts index 92fa328..154d11f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { Editor, EventRef, MarkdownView, Notice, Plugin, Menu, Platform, MarkdownFileInfo, TFile } from 'obsidian'; +import { Editor, MarkdownView, Notice, Plugin, Menu, Platform, MarkdownFileInfo, TFile, getLanguage } from 'obsidian'; import { Language, TranslationKey, I18n } from './i18n'; import { ContextData, ContextType, DEFAULT_SETTINGS, EasyCopySettings, LinkFormat, BlockIdInsertPosition, CodeBlockBehavior } from './type'; import { EasyCopySettingTab } from './settingTab'; @@ -13,7 +13,7 @@ export default class EasyCopy extends Plugin { settings: EasyCopySettings; i18n: I18n; private lastCopyMeta: CopyMetadata | null = null; - private pasteEventRef: EventRef | null = null; + private pasteEventRef: ReturnType | null = null; async onload() { await this.loadSettings(); @@ -38,7 +38,7 @@ export default class EasyCopy extends Plugin { icon: 'copy-plus', editorCallback: (editor: Editor, view: MarkdownView) => { // 实现智能复制功能 - this.contextualCopy(editor, view); + void this.contextualCopy(editor, view); } }); @@ -64,9 +64,9 @@ export default class EasyCopy extends Plugin { new Notice(this.t('no-file')); return; } - const filename = file.basename; - // 自动生成名称 - this.insertBlockIdAndCopyLink(editor, filename, false); + const filename = file.basename; + // 自动生成名称 + void this.insertBlockIdAndCopyLink(editor, filename, false); } }); @@ -81,9 +81,9 @@ export default class EasyCopy extends Plugin { new Notice(this.t('no-file')); return; } - const filename = file.basename; - // 手动输入名称 - this.insertBlockIdAndCopyLink(editor, filename, true); + const filename = file.basename; + // 手动输入名称 + void this.insertBlockIdAndCopyLink(editor, filename, true); } }); @@ -101,10 +101,10 @@ export default class EasyCopy extends Plugin { menu.addItem(item => { item .setTitle(this.t('contextual-copy')) - .setIcon('copy-slash') - .onClick(async () => { - this.contextualCopy(editor, view); - }); + .setIcon('copy-slash') + .onClick(async () => { + void this.contextualCopy(editor, view); + }); }) } }) @@ -117,7 +117,7 @@ export default class EasyCopy extends Plugin { // 注意:其他也使用 navigator.clipboard.writeText() 的插件会 // 绕过这个监听器;冲突需要剪贴板文本完全相同,概率极低。 // 如果需要更稳健的识别机制,可改用自定义 ClipboardItem MIME 类型。 - this.registerDomEvent(document, 'copy', () => { + this.registerDomEvent(activeDocument, 'copy', () => { this.lastCopyMeta = null; }); @@ -139,7 +139,9 @@ export default class EasyCopy extends Plugin { private registerPasteHandler(): void { if (this.pasteEventRef) return; this.pasteEventRef = this.app.workspace.on('editor-paste', (evt, editor, info) => { - this.handlePaste(evt, editor, info); + if (evt.defaultPrevented) return; + const handled = this.handlePaste(evt, editor, info); + if (handled) evt.preventDefault(); }); this.registerEvent(this.pasteEventRef); } @@ -174,13 +176,14 @@ export default class EasyCopy extends Plugin { * 如果其他粘贴类插件(如 Linter)抢先处理了事件,本功能会被跳过。 * 此时需确保 Easy Copy 在冲突插件之前启用(先禁用再启用即可调整顺序)。 */ - private handlePaste(evt: ClipboardEvent, editor: Editor, info: MarkdownView | MarkdownFileInfo): void { + private handlePaste(evt: ClipboardEvent, editor: Editor, info: MarkdownFileInfo): boolean { const clipboardText = evt.clipboardData?.getData('text/plain'); // 如果有活跃的 meta 且剪贴板内容匹配,但被其他插件抢先处理了,输出提示 + // (外层已在 defaultPrevented 时 return,此处作为二次保险) if (evt.defaultPrevented && this.lastCopyMeta && clipboardText === this.lastCopyMeta.clipboardText) { - console.log('[Easy Copy] Paste event was already handled by another plugin. Link path resolution skipped. You can adjust plugin load order in .obsidian/community-plugins.json.'); - return; + console.log('[Easy Copy] Paste event was already handled by another plugin. Link path resolution skipped. You can adjust plugin load order in the community-plugins.json file inside your vault\'s config folder (' + this.app.vault.configDir + ').'); + return false; } const decision = decidePasteResolution({ @@ -194,22 +197,22 @@ export default class EasyCopy extends Plugin { if (decision === 'reset-and-skip') { this.lastCopyMeta = null; - return; + return false; } - if (decision === 'skip') return; + if (decision === 'skip') return false; // decision === 'rewrite':此时 lastCopyMeta 和 clipboardText 必非空。 const meta = this.lastCopyMeta!; const destFile = info.file; - if (!destFile) return; + if (!destFile) return false; // 退化的自引用:同文件粘贴且无锚点会生成 [[]] / [](#) 之类的空链接, // 这种情况下让正常粘贴流程接手即可。 - if (meta.subpath === '' && meta.sourceFilePath === destFile.path) return; + if (meta.subpath === '' && meta.sourceFilePath === destFile.path) return false; const sourceFile = this.app.vault.getAbstractFileByPath(meta.sourceFilePath); - if (!(sourceFile instanceof TFile)) return; + if (!(sourceFile instanceof TFile)) return false; try { const effectiveFormat = this.getEffectiveLinkFormat(); @@ -254,11 +257,13 @@ export default class EasyCopy extends Plugin { } if (link !== clipboardText) { - evt.preventDefault(); editor.replaceSelection(link); + return true; } + return false; } catch { // 链接生成失败——让正常粘贴流程继续 + return false; } } @@ -658,37 +663,37 @@ export default class EasyCopy extends Plugin { this.copyBlockLink(contextType.match!, filename, true, contextType.curLine); return; case ContextType.BOLD: - navigator.clipboard.writeText(contextType.match!); + void navigator.clipboard.writeText(contextType.match!); if (this.settings.showNotice) { new Notice(this.t('bold-copied')); } return; case ContextType.ITALIC: - navigator.clipboard.writeText(contextType.match!); + void navigator.clipboard.writeText(contextType.match!); if (this.settings.showNotice) { new Notice(this.t('italic-copied')); } return; case ContextType.HIGHLIGHT: - navigator.clipboard.writeText(contextType.match!); + void navigator.clipboard.writeText(contextType.match!); if (this.settings.showNotice) { new Notice(this.t('highlight-copied')); } return; case ContextType.STRIKETHROUGH: - navigator.clipboard.writeText(contextType.match!); + void navigator.clipboard.writeText(contextType.match!); if (this.settings.showNotice) { new Notice(this.t('strikethrough-copied')); } return; case ContextType.INLINECODE: - navigator.clipboard.writeText(contextType.match!); + void navigator.clipboard.writeText(contextType.match!); if (this.settings.showNotice) { new Notice(this.t('inline-code-copied')); } return; case ContextType.INLINELATEX: - navigator.clipboard.writeText(contextType.match!); + void navigator.clipboard.writeText(contextType.match!); if (this.settings.showNotice) { new Notice(this.t('inline-latex-copied')); } @@ -696,14 +701,14 @@ export default class EasyCopy extends Plugin { case ContextType.LINKTITLE: // 复制链接标题 - navigator.clipboard.writeText(contextType.match!); + void navigator.clipboard.writeText(contextType.match!); if (this.settings.showNotice) { new Notice(this.t('link-text-copied')); } return; case ContextType.LINEURL: // 复制链接地址 - navigator.clipboard.writeText(contextType.match!); + void navigator.clipboard.writeText(contextType.match!); if (this.settings.showNotice) { new Notice(this.t('link-url-copied')); } @@ -714,14 +719,14 @@ export default class EasyCopy extends Plugin { case ContextType.WIKILINK: { // 复制 [[双链]],可选保留括号 if (!contextType.match) return; - let wikiCopyText = contextType.match!; + let wikiCopyText = contextType.match; if (this.settings.keepWikiBrackets) { wikiCopyText = `[[${wikiCopyText}]]`; } else { // 如果有 |别名,去掉|后面的内容 wikiCopyText = wikiCopyText.split('|')[0]; } - navigator.clipboard.writeText(wikiCopyText); + void navigator.clipboard.writeText(wikiCopyText); if (this.settings.showNotice) { new Notice(this.t('wiki-link-copied')); } @@ -729,7 +734,7 @@ export default class EasyCopy extends Plugin { } case ContextType.CODEBLOCK: { // 复制代码块内容(纯文本或含围栏,由 detectCodeBlock 已决定) - navigator.clipboard.writeText(contextType.match ?? ''); + void navigator.clipboard.writeText(contextType.match ?? ''); if (this.settings.showNotice) { new Notice(this.t('code-block-copied')); } @@ -738,7 +743,7 @@ export default class EasyCopy extends Plugin { case ContextType.CALLOUT: { // 复制 Callout 区块纯文本 const calloutText = contextType.match?.replace(/\n+/g, '\n').replace(/\s+$/g, ''); - navigator.clipboard.writeText(calloutText ?? ''); + void navigator.clipboard.writeText(calloutText ?? ''); if (this.settings.showNotice) { new Notice(this.t('callout-copied')); } @@ -765,7 +770,7 @@ export default class EasyCopy extends Plugin { blockDisplayCharLimit: this.settings.blockDisplayCharLimit, }); - navigator.clipboard.writeText(blockIdLink); + void navigator.clipboard.writeText(blockIdLink); // 存储元数据,供粘贴时解析链接路径使用 const blockFile = this.app.workspace.getActiveFile(); @@ -817,7 +822,7 @@ export default class EasyCopy extends Plugin { simplifiedHeadingToNoteLink: this.settings.simplifiedHeadingToNoteLink, }); - navigator.clipboard.writeText(link); + void navigator.clipboard.writeText(link); // 存储元数据,供粘贴时解析链接路径使用 const headingFile = this.app.workspace.getActiveFile(); @@ -873,7 +878,7 @@ export default class EasyCopy extends Plugin { linkFormat: this.getEffectiveLinkFormat(), }); - navigator.clipboard.writeText(link); + void navigator.clipboard.writeText(link); // 存储元数据,供粘贴时解析链接路径使用 this.lastCopyMeta = buildFileCopyMetadata({ @@ -910,7 +915,7 @@ export default class EasyCopy extends Plugin { blockId = modalBlockId; } else { // 随机生成 - const randomId = Math.random().toString(36).substr(2, 6); + const randomId = Math.random().toString(36).substring(2, 8); blockId = `${randomId}`; } @@ -988,9 +993,8 @@ export default class EasyCopy extends Plugin { * @returns Obsidian的语言代码 */ private getObsidianLanguage(): string { - // 从 localStorage 中获取 Obsidian 的语言设置 - const lang = window.localStorage.getItem("language") || 'en'; - return lang; + // 使用 Obsidian 官方 API 获取语言设置 + return getLanguage(); } /** diff --git a/src/settingTab.ts b/src/settingTab.ts index 56da020..ad91f8a 100644 --- a/src/settingTab.ts +++ b/src/settingTab.ts @@ -3,7 +3,8 @@ import { PluginSettingTab, Setting, requireApiVersion, - } from "obsidian"; + setIcon, +} from "obsidian"; import * as ObsidianModule from "obsidian"; import EasyCopy from "./main"; import { LinkFormat, BlockIdInsertPosition, CodeBlockBehavior } from "./type"; @@ -127,16 +128,16 @@ export class EasyCopySettingTab extends PluginSettingTab { // 「跟随 Obsidian 设置」时遵循 vault 的路径风格(最短/相对/绝对); // 选择明确的 Wiki/Markdown 格式时仅使用最短唯一路径。 formatGroup.addSetting(setting => { - const descFragment = document.createDocumentFragment(); - descFragment.append(this.plugin.t('resolve-link-path-on-paste-desc') + ' '); - const infoIcon = descFragment.createEl('span', { - attr: { - 'aria-label': this.plugin.t('resolve-link-path-on-paste-tooltip'), - 'class': 'clickable-icon setting-editor-extra-setting-button', - 'style': 'display:inline; vertical-align:middle; cursor:help;', - }, - }); - infoIcon.innerHTML = ''; + const descFragment = activeDocument.createDocumentFragment(); + descFragment.append(this.plugin.t('resolve-link-path-on-paste-desc') + ' '); + const infoIcon = descFragment.createEl('span', { + attr: { + 'aria-label': this.plugin.t('resolve-link-path-on-paste-tooltip'), + 'class': 'clickable-icon setting-editor-extra-setting-button', + 'style': 'display:inline; vertical-align:middle; cursor:help;', + }, + }); + setIcon(infoIcon, 'info'); setting .setName(this.plugin.t('resolve-link-path-on-paste')) diff --git a/versions.json b/versions.json index 6b59286..2869e70 100644 --- a/versions.json +++ b/versions.json @@ -16,5 +16,6 @@ "1.5.2": "0.15.0", "1.5.3": "0.15.0", "1.6.0": "0.15.0", - "1.6.1": "0.15.0" + "1.6.1": "0.15.0", + "1.6.2": "0.15.0" }