Prepare 1.0.0 release

This commit is contained in:
wzh 2026-06-14 02:09:46 +08:00
parent 29ab7dacc4
commit 382eed7d22
12 changed files with 521 additions and 40 deletions

1
.gitignore vendored
View file

@ -3,6 +3,7 @@ node_modules/
main.js
*.map
release/
.env
.env.*

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 cnwenzhihong
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

167
README.md
View file

@ -1,32 +1,153 @@
# Auto Paste Link
Obsidian 插件:从外部粘贴单个 URL 时,自动生成 Markdown 普通链接、图片链接或视频嵌入。对受支持的重要站点,可自动补全链接标题。
Auto Paste Link is an Obsidian plugin that formats a single pasted URL into a Markdown link, image embed, or video embed. For selected supported websites, it can also fill the link title automatically.
## 行为
中文:这是一个 Obsidian 插件,用于在粘贴单个 URL 时自动生成 Markdown 普通链接、图片嵌入或视频嵌入。对受支持的重要站点,可以自动补全链接标题。
- 粘贴普通 URL立即插入 `[](URL)`,光标停在标题位置。
- 粘贴受支持站点 URL先插入 `[](URL)`,标题请求成功后自动补全为 `[标题](URL)`
- 选中文本后粘贴普通 URL插入 `[选中文本](URL)`,不联网获取标题。
- 粘贴图片 URL插入 `![](URL)` 并把光标移动到下一行开头。
- 粘贴视频文件 URL插入 `<video src="URL" controls muted autoplay loop></video>` 并换行。
- 粘贴一段混合文本时不自动转换,保持原样。
- YAML frontmatter 默认不处理,避免破坏元数据。
## Demo
## 设置
[Watch the demo video](docs/assets/auto-paste-link-trail.mp4)
- 自动获取支持站点标题:默认开启。
- 标题请求超时:默认 `3000ms`。Fab 访问速度通常较慢,低于该值可能经常取不到标题。
- 当前支持站点bilibili、YouTube、Fab。
- 选中文本作为链接标题:默认开启。
- 自动嵌入图片链接:默认开启。
- 图片链接后自动换行:默认开启。
- 处理 YAML frontmatter默认关闭。
- 自动嵌入视频链接:默认开启。
- 视频扩展名:默认 `mp4`,用于识别直接视频文件 URL。
- 图片扩展名:用于识别 `.jpg`、`.png`、`.webp` 等 URL。
- 图片链接匹配规则:每行一个 JavaScript 正则,用于识别无扩展名图片链接。
中文:[查看演示视频](docs/assets/auto-paste-link-trail.mp4)
## 开发
## Core Behavior
- Normal URL: inserts `[](URL)` and keeps the cursor inside `[]`.
- Selected text + normal URL: inserts `[selected text](URL)`.
- Supported website URL: inserts `[](URL)` first, then fills the title when the title request succeeds.
- Image URL: inserts `![](URL)`.
- Direct video file URL: inserts `<video src="URL" controls muted autoplay loop></video>`.
- Mixed text is not converted.
- YAML frontmatter is ignored by default.
- `Ctrl+Shift+V` keeps Obsidian's plain paste behavior and skips plugin processing.
## Examples
Normal link:
```md
[](https://www.baidu.com)
```
Normal link with selected text:
```md
[百度](https://www.baidu.com)
```
Image link:
```md
![](https://images.steamusercontent.com/ugc/example/hash/?imw=5000)
```
Video link:
```html
<video src="https://example.com/video.mp4" controls muted autoplay loop></video>
```
## Supported Title Sites
Automatic title fetching is intentionally limited to important supported sites:
- bilibili
- YouTube
- Fab
Other websites are not fetched for titles.
## Image Detection
Image detection is deliberately conservative. The plugin does not treat every CDN as an image source.
Detection order:
1. Image file extension, such as `.jpg`, `.png`, `.webp`, `.gif`, `.svg`, `.avif`.
2. Trusted image sources.
3. Advanced regular expressions.
Built-in trusted image sources include:
- `images.unsplash.com/`
- `i.imgur.com/`
- `images.steamusercontent.com/`
- `pbs.twimg.com/media/`
- `i.ytimg.com/vi/`
- `img.youtube.com/vi/`
- `i0.hdslb.com/bfs/`
- `i1.hdslb.com/bfs/`
- `i2.hdslb.com/bfs/`
Generic CDN domains such as `cloudfront.net`, `akamaihd.net`, `fastly.net`, and `googleusercontent.com` are not built in, because they can serve images, webpages, scripts, downloads, and many other resource types.
## Custom Trusted Image Sources
Use custom trusted image sources for hosts or paths that clearly serve images.
Each source has:
- Host: required, for example `images.example.com`.
- Path prefix: optional, for example `/media/`.
- Include subdomains: optional. Keep it off unless subdomains are also known image sources.
Examples:
```text
host: images.example.com
path prefix: /media/
include subdomains: off
```
This matches:
```text
https://images.example.com/media/abc
```
It does not match:
```text
https://images.example.com/files/abc
https://sub.images.example.com/media/abc
```
If `include subdomains` is enabled, then subdomains such as `cdn.images.example.com` can also match.
## Advanced Regular Expressions
Advanced image URL regular expressions are still available for edge cases, but they should be treated as an escape hatch.
Use trusted image sources first. Use regular expressions only when a source cannot be expressed by host and path prefix.
The default advanced rule recognizes explicit image format query parameters, for example:
```text
https://example.com/resource?id=1&format=jpg
```
## Settings
- Auto fetch supported site titles: enabled by default.
- Title request timeout: `3000ms` by default. Fab is usually slower than other supported sites.
- Use selected text as the link title: enabled by default.
- Process YAML frontmatter: disabled by default.
- Auto embed image links: enabled by default.
- Add newline after image links: enabled by default.
- Image extensions are configurable.
- Custom trusted image sources are configurable.
- Advanced image URL regular expressions are configurable.
- Auto embed video links: enabled by default.
- Video extensions are configurable. The default is `mp4`.
## Manual Installation
1. Download `main.js`, `manifest.json`, and `styles.css` from the latest GitHub release.
2. Create this folder in your vault: `.obsidian/plugins/auto-paste-link/`.
3. Put the three downloaded files into that folder.
4. Enable the plugin in Obsidian settings.
## Development
```bash
corepack pnpm install
@ -34,4 +155,4 @@ corepack pnpm test
corepack pnpm build
```
构建完成后,把 `main.js`、`manifest.json`、`styles.css` 放入 Obsidian 库的 `.obsidian/plugins/auto-paste-link/` 目录并启用插件。
The GitHub release tag must exactly match the version in `manifest.json`, for example `1.0.0` without a `v` prefix. Release assets must include `main.js`, `manifest.json`, and `styles.css`.

Binary file not shown.

View file

@ -1,10 +1,10 @@
{
"id": "auto-paste-link",
"name": "Auto Paste Link",
"version": "0.1.0",
"version": "1.0.0",
"minAppVersion": "1.5.0",
"description": "Auto formats single pasted URLs as Markdown links or image embeds.",
"author": "cnwen",
"authorUrl": "",
"description": "Auto formats pasted URLs as Markdown links, media embeds, and supported site titles.",
"author": "cnwenzhihong",
"authorUrl": "https://github.com/cnwenzhihong",
"isDesktopOnly": false
}

View file

@ -1,7 +1,7 @@
{
"name": "obsidian-auto-paste-link",
"version": "0.1.0",
"description": "Auto format single pasted URLs as Markdown links or images in Obsidian.",
"version": "1.0.0",
"description": "Auto format pasted URLs as Markdown links, media embeds, and supported site titles in Obsidian.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
@ -14,8 +14,16 @@
"paste",
"link"
],
"author": "cnwen",
"author": "cnwenzhihong",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/cnwenzhihong/Obsidian-auto-paste-link.git"
},
"bugs": {
"url": "https://github.com/cnwenzhihong/Obsidian-auto-paste-link/issues"
},
"homepage": "https://github.com/cnwenzhihong/Obsidian-auto-paste-link#readme",
"devDependencies": {
"@codemirror/view": "6.38.6",
"@types/node": "latest",

View file

@ -1,4 +1,8 @@
import type { AutoPasteLinkSettings } from "../settings/pluginSettings";
import {
BUILTIN_TRUSTED_IMAGE_SOURCES,
type AutoPasteLinkSettings,
type TrustedImageSource,
} from "../settings/pluginSettings.ts";
export type UrlKind = "normal-link" | "image-link" | "video-link" | "unsupported";
@ -49,6 +53,14 @@ function isImageUrl(rawUrl: string, parsedUrl: URL, settings: AutoPasteLinkSetti
return true;
}
if (isTrustedImageSource(parsedUrl, BUILTIN_TRUSTED_IMAGE_SOURCES)) {
return true;
}
if (isTrustedImageSource(parsedUrl, settings.trustedImageSources)) {
return true;
}
return settings.imageUrlPatterns.some((pattern) => {
try {
return new RegExp(pattern, "i").test(rawUrl);
@ -58,6 +70,21 @@ function isImageUrl(rawUrl: string, parsedUrl: URL, settings: AutoPasteLinkSetti
});
}
function isTrustedImageSource(parsedUrl: URL, sources: TrustedImageSource[]): boolean {
const hostname = parsedUrl.hostname.toLowerCase();
return sources.some((source) => {
const hostMatches = source.includeSubdomains
? hostname === source.host || hostname.endsWith(`.${source.host}`)
: hostname === source.host;
if (!hostMatches) {
return false;
}
return !source.pathPrefix || parsedUrl.pathname.startsWith(source.pathPrefix);
});
}
function isVideoUrl(parsedUrl: URL, settings: AutoPasteLinkSettings): boolean {
if (!settings.embedVideoLinks) {
return false;

View file

@ -22,6 +22,17 @@ interface SettingText {
addNewlineAfterImageDesc: string;
imageExtensionsName: string;
imageExtensionsDesc: string;
builtinTrustedImageSourcesName: string;
trustedImageSourcesName: string;
trustedImageSourcesDesc: string;
addTrustedImageSourceButtonText: string;
trustedImageSourceRowName: string;
trustedImageSourceRowDesc: string;
trustedImageSourceHostPlaceholder: string;
trustedImageSourcePathPrefixPlaceholder: string;
trustedImageSourceIncludeSubdomainsText: string;
deleteTrustedImageSourceButtonText: string;
advancedImageRulesSubsectionName: string;
imageUrlPatternsName: string;
imageUrlPatternsDesc: string;
embedVideoLinksName: string;
@ -53,8 +64,19 @@ const ZH: SettingText = {
addNewlineAfterImageDesc: "开启后,图片 Markdown 插入完成后光标移动到下一行开头。",
imageExtensionsName: "图片扩展名",
imageExtensionsDesc: "每行或逗号分隔一个扩展名,不需要写点号。",
imageUrlPatternsName: "图片链接匹配规则",
imageUrlPatternsDesc: "每行一个 JavaScript 正则,用于识别无扩展名图片 URL。无效正则会被忽略。",
builtinTrustedImageSourcesName: "内置可信图片来源",
trustedImageSourcesName: "自定义可信图片来源",
trustedImageSourcesDesc: "适合添加明确只承载图片的站点或路径。普通 CDN 不建议添加。",
addTrustedImageSourceButtonText: "添加来源",
trustedImageSourceRowName: "来源",
trustedImageSourceRowDesc: "第一个输入 Host第二个输入路径前缀开关表示包含子域名。",
trustedImageSourceHostPlaceholder: "host例如 images.example.com",
trustedImageSourcePathPrefixPlaceholder: "路径前缀,可留空,例如 /media/",
trustedImageSourceIncludeSubdomainsText: "包含子域名",
deleteTrustedImageSourceButtonText: "删除",
advancedImageRulesSubsectionName: "高级规则",
imageUrlPatternsName: "高级正则规则",
imageUrlPatternsDesc: "每行一个 JavaScript 正则。仅建议在可信来源无法表达时使用,无效正则会被忽略。",
embedVideoLinksName: "自动嵌入视频链接",
embedVideoLinksDesc: "开启后,直接视频文件 URL 会插入为 HTML video 标签。",
videoSubsectionName: "视频",
@ -84,8 +106,19 @@ const EN: SettingText = {
addNewlineAfterImageDesc: "When enabled, the cursor moves to the start of the next line after inserting an image link.",
imageExtensionsName: "Image extensions",
imageExtensionsDesc: "Enter one extension per line or separate them with commas. Do not include the dot.",
imageUrlPatternsName: "Image URL patterns",
imageUrlPatternsDesc: "Enter one JavaScript regular expression per line for image URLs without extensions. Invalid patterns are ignored.",
builtinTrustedImageSourcesName: "Built-in trusted image sources",
trustedImageSourcesName: "Custom trusted image sources",
trustedImageSourcesDesc: "Use this for hosts or paths that clearly serve images. Avoid generic CDN domains.",
addTrustedImageSourceButtonText: "Add source",
trustedImageSourceRowName: "Source",
trustedImageSourceRowDesc: "First input is the host, second input is the path prefix. The toggle includes subdomains.",
trustedImageSourceHostPlaceholder: "host, e.g. images.example.com",
trustedImageSourcePathPrefixPlaceholder: "optional path prefix, e.g. /media/",
trustedImageSourceIncludeSubdomainsText: "Include subdomains",
deleteTrustedImageSourceButtonText: "Delete",
advancedImageRulesSubsectionName: "Advanced rules",
imageUrlPatternsName: "Advanced regular expressions",
imageUrlPatternsDesc: "Enter one JavaScript regular expression per line. Use only when trusted sources cannot express the rule. Invalid patterns are ignored.",
embedVideoLinksName: "Embed video links",
embedVideoLinksDesc: "When enabled, direct video file URLs are inserted as HTML video tags.",
videoSubsectionName: "Videos",

View file

@ -7,10 +7,65 @@ export interface AutoPasteLinkSettings {
embedImageLinks: boolean;
embedVideoLinks: boolean;
imageExtensions: string[];
trustedImageSources: TrustedImageSource[];
imageUrlPatterns: string[];
videoExtensions: string[];
}
export interface TrustedImageSource {
host: string;
pathPrefix: string;
includeSubdomains: boolean;
}
export const BUILTIN_TRUSTED_IMAGE_SOURCES: TrustedImageSource[] = [
{
host: "images.unsplash.com",
pathPrefix: "",
includeSubdomains: false,
},
{
host: "i.imgur.com",
pathPrefix: "",
includeSubdomains: false,
},
{
host: "images.steamusercontent.com",
pathPrefix: "",
includeSubdomains: false,
},
{
host: "pbs.twimg.com",
pathPrefix: "/media/",
includeSubdomains: false,
},
{
host: "i.ytimg.com",
pathPrefix: "/vi/",
includeSubdomains: false,
},
{
host: "img.youtube.com",
pathPrefix: "/vi/",
includeSubdomains: false,
},
{
host: "i0.hdslb.com",
pathPrefix: "/bfs/",
includeSubdomains: false,
},
{
host: "i1.hdslb.com",
pathPrefix: "/bfs/",
includeSubdomains: false,
},
{
host: "i2.hdslb.com",
pathPrefix: "/bfs/",
includeSubdomains: false,
},
];
export const DEFAULT_SETTINGS: AutoPasteLinkSettings = {
processYamlFrontmatter: false,
useSelectionAsLinkText: true,
@ -29,8 +84,8 @@ export const DEFAULT_SETTINGS: AutoPasteLinkSettings = {
"svg",
"avif"
],
trustedImageSources: [],
imageUrlPatterns: [
"^https?:\\/\\/(?:images\\.unsplash\\.com|i\\.imgur\\.com)\\/",
"[?&](?:format|fm|type|mime)=([^&#]*)(?:jpg|jpeg|png|gif|webp|avif|svg)"
],
videoExtensions: [
@ -50,6 +105,9 @@ export function normalizeSettings(value: Partial<AutoPasteLinkSettings>): AutoPa
embedImageLinks: value.embedImageLinks ?? DEFAULT_SETTINGS.embedImageLinks,
embedVideoLinks: value.embedVideoLinks ?? DEFAULT_SETTINGS.embedVideoLinks,
imageExtensions: normalizeImageExtensions(value.imageExtensions ?? DEFAULT_SETTINGS.imageExtensions),
trustedImageSources: normalizeTrustedImageSources(
value.trustedImageSources ?? DEFAULT_SETTINGS.trustedImageSources
),
imageUrlPatterns: normalizePatternList(value.imageUrlPatterns ?? DEFAULT_SETTINGS.imageUrlPatterns),
videoExtensions: normalizeVideoExtensions(value.videoExtensions ?? DEFAULT_SETTINGS.videoExtensions),
};
@ -78,6 +136,34 @@ export function normalizePatternList(value: string[] | string): string[] {
return normalizeLineList(value);
}
export function normalizeTrustedImageSources(value: TrustedImageSource[]): TrustedImageSource[] {
const seen = new Set<string>();
const result: TrustedImageSource[] = [];
for (const source of value) {
const host = normalizeTrustedImageSourceHost(source.host);
if (!host) {
continue;
}
const pathPrefix = normalizeTrustedImageSourcePathPrefix(source.pathPrefix);
const includeSubdomains = Boolean(source.includeSubdomains);
const key = `${host}\n${pathPrefix}\n${includeSubdomains}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
result.push({
host,
pathPrefix,
includeSubdomains,
});
}
return result;
}
function normalizeLineList(value: string[] | string): string[] {
const rawItems = Array.isArray(value) ? value : value.split(/[\n,]/);
const seen = new Set<string>();
@ -95,3 +181,28 @@ function normalizeLineList(value: string[] | string): string[] {
return result;
}
function normalizeTrustedImageSourceHost(value: string): string | null {
const rawHost = value.trim().toLowerCase();
if (!rawHost) {
return null;
}
let host = rawHost;
try {
host = new URL(rawHost.includes("://") ? rawHost : `https://${rawHost}`).hostname.toLowerCase();
} catch {
return null;
}
return /^[a-z0-9.-]+$/.test(host) ? host : null;
}
function normalizeTrustedImageSourcePathPrefix(value: string): string {
const pathPrefix = value.trim();
if (!pathPrefix || pathPrefix === "/") {
return "";
}
return pathPrefix.startsWith("/") ? pathPrefix : `/${pathPrefix}`;
}

View file

@ -2,10 +2,13 @@ import { App, PluginSettingTab, Setting } from "obsidian";
import type AutoPasteLinkPlugin from "../main";
import { getSettingText } from "./i18n";
import {
BUILTIN_TRUSTED_IMAGE_SOURCES,
normalizeImageExtensions,
normalizePatternList,
normalizeTitleFetchTimeoutMs,
normalizeTrustedImageSources,
normalizeVideoExtensions,
type TrustedImageSource,
} from "./pluginSettings";
export class AutoPasteLinkSettingTab extends PluginSettingTab {
@ -105,7 +108,74 @@ export class AutoPasteLinkSettingTab extends PluginSettingTab {
})
);
const patternSetting = new Setting(imageSection)
new Setting(imageSection)
.setName(text.builtinTrustedImageSourcesName)
.setDesc(createTrustedImageSourcesDescription(BUILTIN_TRUSTED_IMAGE_SOURCES));
new Setting(imageSection)
.setName(text.trustedImageSourcesName)
.setDesc(text.trustedImageSourcesDesc)
.addButton((button) =>
button
.setButtonText(text.addTrustedImageSourceButtonText)
.onClick(() => {
this.plugin.settings.trustedImageSources = [
...this.plugin.settings.trustedImageSources,
createEmptyTrustedImageSource(),
];
this.display();
})
);
this.plugin.settings.trustedImageSources.forEach((source, index) => {
new Setting(imageSection)
.setName(`${text.trustedImageSourceRowName} ${index + 1}`)
.setDesc(text.trustedImageSourceRowDesc)
.addText((input) =>
input
.setPlaceholder(text.trustedImageSourceHostPlaceholder)
.setValue(source.host)
.onChange(async (value) => {
await this.updateTrustedImageSource(index, {
host: value,
});
})
)
.addText((input) =>
input
.setPlaceholder(text.trustedImageSourcePathPrefixPlaceholder)
.setValue(source.pathPrefix)
.onChange(async (value) => {
await this.updateTrustedImageSource(index, {
pathPrefix: value,
});
})
)
.addToggle((toggle) =>
toggle
.setTooltip(text.trustedImageSourceIncludeSubdomainsText)
.setValue(source.includeSubdomains)
.onChange(async (value) => {
await this.updateTrustedImageSource(index, {
includeSubdomains: value,
});
})
)
.addButton((button) =>
button
.setButtonText(text.deleteTrustedImageSourceButtonText)
.onClick(async () => {
this.plugin.settings.trustedImageSources = this.plugin.settings.trustedImageSources.filter(
(_, sourceIndex) => sourceIndex !== index
);
await this.plugin.saveSettings();
this.display();
})
);
});
const advancedImageSection = addSubsection(imageSection, text.advancedImageRulesSubsectionName);
const patternSetting = new Setting(advancedImageSection)
.setName(text.imageUrlPatternsName)
.setDesc(text.imageUrlPatternsDesc);
patternSetting.settingEl.addClass("auto-paste-link-setting");
@ -143,6 +213,16 @@ export class AutoPasteLinkSettingTab extends PluginSettingTab {
})
);
}
private async updateTrustedImageSource(index: number, patch: Partial<TrustedImageSource>): Promise<void> {
const sources = [...this.plugin.settings.trustedImageSources];
sources[index] = {
...(sources[index] ?? createEmptyTrustedImageSource()),
...patch,
};
this.plugin.settings.trustedImageSources = normalizeTrustedImageSources(sources);
await this.plugin.saveSettings();
}
}
function addSection(containerEl: HTMLElement, name: string): void {
@ -180,3 +260,26 @@ function createDescription(description: string, hint: string): DocumentFragment
return fragment;
}
function createTrustedImageSourcesDescription(sources: TrustedImageSource[]): DocumentFragment {
const fragment = document.createDocumentFragment();
const list = document.createElement("ul");
for (const source of sources) {
const item = document.createElement("li");
const pathPrefix = source.pathPrefix || "/";
item.textContent = `${source.host}${pathPrefix}`;
list.append(item);
}
fragment.append(list);
return fragment;
}
function createEmptyTrustedImageSource(): TrustedImageSource {
return {
host: "",
pathPrefix: "",
includeSubdomains: false,
};
}

View file

@ -4,7 +4,7 @@ import { buildMarkdownInsertion } from "../src/core/markdownInserter.ts";
import { classifyUrlText } from "../src/core/urlClassifier.ts";
import { isInYamlFrontmatter } from "../src/core/yamlRangeDetector.ts";
import { PasteShortcutTracker } from "../src/core/pasteShortcutTracker.ts";
import { DEFAULT_SETTINGS } from "../src/settings/pluginSettings.ts";
import { DEFAULT_SETTINGS, normalizeSettings } from "../src/settings/pluginSettings.ts";
import {
cleanBilibiliTitle,
cleanFabTitle,
@ -82,13 +82,69 @@ test("自定义视频扩展名可识别新增视频类型", () => {
);
});
test("无扩展名图片 URL 可通过规则识别", () => {
test("无扩展名图片 URL 可通过内置可信来源识别", () => {
assert.deepEqual(classifyUrlText("https://images.unsplash.com/photo-1", DEFAULT_SETTINGS), {
kind: "image-link",
url: "https://images.unsplash.com/photo-1",
});
});
test("Steam 无扩展名图片 URL 可通过默认规则识别", () => {
const url = "https://images.steamusercontent.com/ugc/1751306654054219740/8964AA9866F67EB209B64B7F151D13DF053038A5/?imw=5000&imh=5000&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=false";
assert.deepEqual(classifyUrlText(url, DEFAULT_SETTINGS), {
kind: "image-link",
url,
});
});
test("常见高置信图片来源可被识别", () => {
assert.equal(classifyUrlText("https://pbs.twimg.com/media/example?format=jpg", DEFAULT_SETTINGS).kind, "image-link");
assert.equal(classifyUrlText("https://i.ytimg.com/vi/video-id/maxresdefault", DEFAULT_SETTINGS).kind, "image-link");
assert.equal(classifyUrlText("https://i0.hdslb.com/bfs/archive/example", DEFAULT_SETTINGS).kind, "image-link");
});
test("泛 CDN 不会被误识别为图片", () => {
assert.equal(classifyUrlText("https://example.cloudfront.net/assets/resource", DEFAULT_SETTINGS).kind, "normal-link");
assert.equal(classifyUrlText("https://lh3.googleusercontent.com/a/resource", DEFAULT_SETTINGS).kind, "normal-link");
});
test("用户可信图片来源按 host 和路径前缀识别", () => {
const settings = normalizeSettings({
...DEFAULT_SETTINGS,
trustedImageSources: [
{
host: "cdn.example.com",
pathPrefix: "/images/",
includeSubdomains: false,
},
],
});
assert.equal(classifyUrlText("https://cdn.example.com/images/resource", settings).kind, "image-link");
assert.equal(classifyUrlText("https://cdn.example.com/files/resource", settings).kind, "normal-link");
assert.equal(classifyUrlText("https://sub.cdn.example.com/images/resource", settings).kind, "normal-link");
});
test("用户可信图片来源可选择包含子域名", () => {
const settings = normalizeSettings({
...DEFAULT_SETTINGS,
trustedImageSources: [
{
host: "cdn.example.com",
pathPrefix: "",
includeSubdomains: true,
},
],
});
assert.equal(classifyUrlText("https://sub.cdn.example.com/resource", settings).kind, "image-link");
});
test("高级正则规则仍可识别查询参数图片 URL", () => {
assert.equal(classifyUrlText("https://example.com/resource?id=1&format=jpg", DEFAULT_SETTINGS).kind, "image-link");
});
test("混合文本不触发自动替换", () => {
assert.equal(classifyUrlText("看这里 https://www.baidu.com", DEFAULT_SETTINGS).kind, "unsupported");
});

View file

@ -1,3 +1,3 @@
{
"0.1.0": "1.5.0"
"1.0.0": "1.5.0"
}