mirror of
https://github.com/cnwenzhihong/Obsidian-auto-paste-link.git
synced 2026-07-22 07:45:50 +00:00
Prepare 1.0.0 release
This commit is contained in:
parent
29ab7dacc4
commit
382eed7d22
12 changed files with 521 additions and 40 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,6 +3,7 @@ node_modules/
|
|||
|
||||
main.js
|
||||
*.map
|
||||
release/
|
||||
|
||||
.env
|
||||
.env.*
|
||||
|
|
|
|||
21
LICENSE
Normal file
21
LICENSE
Normal 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
167
README.md
|
|
@ -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:插入 `<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 ``.
|
||||
- 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
|
||||

|
||||
```
|
||||
|
||||
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`.
|
||||
|
|
|
|||
BIN
docs/assets/auto-paste-link-trail.mp4
Normal file
BIN
docs/assets/auto-paste-link-trail.mp4
Normal file
Binary file not shown.
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
14
package.json
14
package.json
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"0.1.0": "1.5.0"
|
||||
"1.0.0": "1.5.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue