新增远程图片URL替换功能,允许用户在发布后将本地图片链接替换为Discourse上的远程URL。

新增支持markdown格式引用本地图片的上传.
This commit is contained in:
wood chen 2025-07-12 01:54:44 +08:00
parent 4900f26222
commit e56a479a45
6 changed files with 132 additions and 17 deletions

View File

@ -15,7 +15,7 @@ export class DiscourseAPI {
) {}
// 上传图片到Discourse
async uploadImage(file: TFile): Promise<string | null> {
async uploadImage(file: TFile): Promise<{shortUrl: string, fullUrl?: string} | null> {
try {
const imgfile = await this.app.vault.readBinary(file);
const boundary = genBoundary();
@ -53,7 +53,25 @@ export class DiscourseAPI {
if (response.status == 200) {
const jsonResponse = response.json;
return jsonResponse.short_url;
let fullUrl: string | undefined;
// 处理完整URL的拼接
if (jsonResponse.url) {
// 如果返回的url已经是完整URL包含http/https直接使用
if (jsonResponse.url.startsWith('http://') || jsonResponse.url.startsWith('https://')) {
fullUrl = jsonResponse.url;
} else {
// 如果是相对路径需要与baseUrl拼接
const baseUrl = this.settings.baseUrl.replace(/\/$/, ''); // 移除尾部斜杠
const urlPath = jsonResponse.url.startsWith('/') ? jsonResponse.url : `/${jsonResponse.url}`;
fullUrl = `${baseUrl}${urlPath}`;
}
}
return {
shortUrl: jsonResponse.short_url,
fullUrl: fullUrl
};
} else {
new NotifyUser(this.app, `Error uploading image: ${response.status}`).open();
return null;

View File

@ -6,6 +6,7 @@ export interface DiscourseSyncSettings {
baseUrl: string;
category: number;
skipH1: boolean;
useRemoteImageUrl: boolean;
userApiKey: string;
lastNotifiedVersion?: string; // 记录上次显示更新通知的版本
}
@ -14,6 +15,7 @@ export const DEFAULT_SETTINGS: DiscourseSyncSettings = {
baseUrl: "https://yourforum.example.com",
category: 1,
skipH1: false,
useRemoteImageUrl: false,
userApiKey: ""
};
@ -230,5 +232,17 @@ export class DiscourseSyncSettingsTab extends PluginSettingTab {
await this.plugin.saveSettings();
})
);
new Setting(publishSection)
.setName(t('USE_REMOTE_IMAGE_URL'))
.setDesc(t('USE_REMOTE_IMAGE_URL_DESC'))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.useRemoteImageUrl)
.onChange(async (value) => {
this.plugin.settings.useRemoteImageUrl = value;
await this.plugin.saveSettings();
})
);
}
}

View File

@ -11,17 +11,30 @@ export class EmbedHandler {
// 提取嵌入引用
extractEmbedReferences(content: string): string[] {
const regex = /!\[\[(.*?)\]\]/g;
const matches = [];
const references: string[] = [];
// 匹配 ![[...]] 格式 (Wiki格式)
const wikiRegex = /!\[\[(.*?)\]\]/g;
let match;
while ((match = regex.exec(content)) !== null) {
matches.push(match[1]);
while ((match = wikiRegex.exec(content)) !== null) {
references.push(match[1]);
}
return matches;
// 匹配 ![](path) 格式 (Markdown格式)
const markdownRegex = /!\[.*?\]\(([^)]+)\)/g;
while ((match = markdownRegex.exec(content)) !== null) {
// 过滤掉网络URL只处理本地文件路径
const path = match[1];
if (!path.startsWith('http://') && !path.startsWith('https://') && !path.startsWith('upload://')) {
references.push(path);
}
}
return references;
}
// 处理嵌入内容
async processEmbeds(embedReferences: string[], activeFileName: string): Promise<string[]> {
async processEmbeds(embedReferences: string[], activeFileName: string, useRemoteUrl = false): Promise<string[]> {
const uploadedUrls: string[] = [];
for (const ref of embedReferences) {
// 处理带有#的文件路径,分离文件名和标题部分
@ -37,8 +50,14 @@ export class EmbedHandler {
if (abstractFile instanceof TFile) {
// 检查是否为图片或PDF文件
if (isImageFile(abstractFile)) {
const imageUrl = await this.api.uploadImage(abstractFile);
uploadedUrls.push(imageUrl || "");
const imageResult = await this.api.uploadImage(abstractFile);
if (imageResult) {
// 根据配置选择使用短URL还是完整URL
const urlToUse = useRemoteUrl && imageResult.fullUrl ? imageResult.fullUrl : imageResult.shortUrl;
uploadedUrls.push(urlToUse);
} else {
uploadedUrls.push("");
}
} else {
// 非图片文件,返回空字符串
uploadedUrls.push("");
@ -58,14 +77,23 @@ export class EmbedHandler {
// 替换内容中的嵌入引用为Markdown格式
replaceEmbedReferences(content: string, embedReferences: string[], uploadedUrls: string[]): string {
let processedContent = content;
embedReferences.forEach((ref, index) => {
const obsRef = `![[${ref}]]`;
// 只有当上传URL不为空时即为图片才替换为Markdown格式的图片链接
if (uploadedUrls[index]) {
const discoRef = `![${ref}](${uploadedUrls[index]})`;
processedContent = processedContent.replace(obsRef, discoRef);
// 处理 ![[...]] 格式 (Wiki格式)
const wikiRef = `![[${ref}]]`;
const wikiReplacement = `![${ref}](${uploadedUrls[index]})`;
processedContent = processedContent.replace(wikiRef, wikiReplacement);
// 处理 ![](path) 格式 (Markdown格式)
// 创建正则表达式来匹配具体的路径
const escapedRef = ref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const markdownRegex = new RegExp(`!\\[([^\\]]*)\\]\\(${escapedRef}\\)`, 'g');
const markdownReplacement = `![$1](${uploadedUrls[index]})`;
processedContent = processedContent.replace(markdownRegex, markdownReplacement);
}
});
return processedContent;
}
}

View File

@ -5,6 +5,8 @@ export default {
'SKIP_H1': 'Skip First Heading',
'SKIP_H1_DESC': 'Skip the first heading (H1) when publishing to Discourse',
'USE_REMOTE_IMAGE_URL': 'Use Remote Image URLs',
'USE_REMOTE_IMAGE_URL_DESC': 'Replace local image links with remote URLs from Discourse after publishing',
'TEST_API_KEY': 'Test Connection',
'TESTING': 'Testing...',
'API_TEST_SUCCESS': 'Connection successful! API key is valid',
@ -17,7 +19,7 @@ export default {
'CATEGORY': 'Category',
'TAGS': 'Tags',
'ENTER_TAG': 'Enter tag name (press Enter to add)',
'ENTER_TAG_WITH_CREATE': 'Enter tag name (can create new tags)',
'ENTER_TAG_WITH_CREATE': 'Enter tag name (press Enter to add) (can create new tags)',
'PUBLISHING': 'Publishing...',
'UPDATING': 'Updating...',
'PUBLISH': 'Publish',

View File

@ -5,6 +5,8 @@ export default {
'SKIP_H1': '跳过一级标题',
'SKIP_H1_DESC': '发布到 Discourse 时跳过笔记中的一级标题',
'USE_REMOTE_IMAGE_URL': '使用远程图片URL',
'USE_REMOTE_IMAGE_URL_DESC': '发布后用Discourse上的远程图片URL替换本地文章中的图片链接',
'TEST_API_KEY': '测试连接',
'TESTING': '测试中...',
'API_TEST_SUCCESS': '连接成功API密钥有效',
@ -17,7 +19,7 @@ export default {
'CATEGORY': '分类',
'TAGS': '标签',
'ENTER_TAG': '输入标签名称(回车添加)',
'ENTER_TAG_WITH_CREATE': '输入标签名称(可创建新标签)',
'ENTER_TAG_WITH_CREATE': '输入标签名称(回车添加)(可创建新标签)',
'PUBLISHING': '发布中...',
'UPDATING': '更新中...',
'PUBLISH': '发布',

View File

@ -239,7 +239,7 @@ export default class PublishToDiscourse extends Plugin implements PluginInterfac
const embedReferences = this.embedHandler.extractEmbedReferences(content);
// 处理嵌入内容
const uploadedUrls = await this.embedHandler.processEmbeds(embedReferences, this.activeFile.name);
const uploadedUrls = await this.embedHandler.processEmbeds(embedReferences, this.activeFile.name, this.settings.useRemoteImageUrl);
// 替换嵌入引用为Markdown格式
content = this.embedHandler.replaceEmbedReferences(content, embedReferences, uploadedUrls);
@ -276,6 +276,11 @@ export default class PublishToDiscourse extends Plugin implements PluginInterfac
// 如果更新成功更新Front Matter
if (result.success) {
await this.updateFrontMatter(postId, topicId, currentTags);
// 如果启用了远程URL替换更新本地文件中的图片链接
if (this.settings.useRemoteImageUrl) {
await this.updateLocalImageLinks(embedReferences, uploadedUrls);
}
}
} else {
// 创建新帖子
@ -289,6 +294,11 @@ export default class PublishToDiscourse extends Plugin implements PluginInterfac
// 如果创建成功更新Front Matter
if (result.success && result.postId && result.topicId) {
await this.updateFrontMatter(result.postId, result.topicId, currentTags);
// 如果启用了远程URL替换更新本地文件中的图片链接
if (this.settings.useRemoteImageUrl) {
await this.updateLocalImageLinks(embedReferences, uploadedUrls);
}
}
}
@ -362,6 +372,47 @@ export default class PublishToDiscourse extends Plugin implements PluginInterfac
}
}
// 更新本地文件中的图片链接为远程URL
private async updateLocalImageLinks(embedReferences: string[], uploadedUrls: string[]) {
try {
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) {
return;
}
let content = await this.app.vault.read(activeFile);
let hasChanges = false;
embedReferences.forEach((ref, index) => {
if (uploadedUrls[index]) {
// 替换 ![[...]] 格式 (Wiki格式)
const wikiRef = `![[${ref}]]`;
const wikiReplacement = `![${ref}](${uploadedUrls[index]})`;
if (content.includes(wikiRef)) {
content = content.replace(wikiRef, wikiReplacement);
hasChanges = true;
}
// 替换 ![](path) 格式 (Markdown格式)
const escapedRef = ref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const markdownRegex = new RegExp(`!\\[([^\\]]*)\\]\\(${escapedRef}\\)`, 'g');
const markdownReplacement = `![$1](${uploadedUrls[index]})`;
if (markdownRegex.test(content)) {
content = content.replace(markdownRegex, markdownReplacement);
hasChanges = true;
}
}
});
// 只有在有变更时才保存文件
if (hasChanges) {
await this.app.vault.modify(activeFile, content);
}
} catch (error) {
console.error('Failed to update local image links:', error);
}
}
onunload() {}
}