mirror of
https://github.com/woodchen-ink/obsidian-publish-to-discourse.git
synced 2025-07-18 05:42:05 +08:00
新增远程图片URL替换功能,允许用户在发布后将本地图片链接替换为Discourse上的远程URL。
新增支持markdown格式引用本地图片的上传.
This commit is contained in:
parent
4900f26222
commit
e56a479a45
22
src/api.ts
22
src/api.ts
@ -15,7 +15,7 @@ export class DiscourseAPI {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
// 上传图片到Discourse
|
// 上传图片到Discourse
|
||||||
async uploadImage(file: TFile): Promise<string | null> {
|
async uploadImage(file: TFile): Promise<{shortUrl: string, fullUrl?: string} | null> {
|
||||||
try {
|
try {
|
||||||
const imgfile = await this.app.vault.readBinary(file);
|
const imgfile = await this.app.vault.readBinary(file);
|
||||||
const boundary = genBoundary();
|
const boundary = genBoundary();
|
||||||
@ -53,7 +53,25 @@ export class DiscourseAPI {
|
|||||||
|
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
const jsonResponse = response.json;
|
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 {
|
} else {
|
||||||
new NotifyUser(this.app, `Error uploading image: ${response.status}`).open();
|
new NotifyUser(this.app, `Error uploading image: ${response.status}`).open();
|
||||||
return null;
|
return null;
|
||||||
|
@ -6,6 +6,7 @@ export interface DiscourseSyncSettings {
|
|||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
category: number;
|
category: number;
|
||||||
skipH1: boolean;
|
skipH1: boolean;
|
||||||
|
useRemoteImageUrl: boolean;
|
||||||
userApiKey: string;
|
userApiKey: string;
|
||||||
lastNotifiedVersion?: string; // 记录上次显示更新通知的版本
|
lastNotifiedVersion?: string; // 记录上次显示更新通知的版本
|
||||||
}
|
}
|
||||||
@ -14,6 +15,7 @@ export const DEFAULT_SETTINGS: DiscourseSyncSettings = {
|
|||||||
baseUrl: "https://yourforum.example.com",
|
baseUrl: "https://yourforum.example.com",
|
||||||
category: 1,
|
category: 1,
|
||||||
skipH1: false,
|
skipH1: false,
|
||||||
|
useRemoteImageUrl: false,
|
||||||
userApiKey: ""
|
userApiKey: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -230,5 +232,17 @@ export class DiscourseSyncSettingsTab extends PluginSettingTab {
|
|||||||
await this.plugin.saveSettings();
|
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();
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,17 +11,30 @@ export class EmbedHandler {
|
|||||||
|
|
||||||
// 提取嵌入引用
|
// 提取嵌入引用
|
||||||
extractEmbedReferences(content: string): string[] {
|
extractEmbedReferences(content: string): string[] {
|
||||||
const regex = /!\[\[(.*?)\]\]/g;
|
const references: string[] = [];
|
||||||
const matches = [];
|
|
||||||
|
// 匹配 ![[...]] 格式 (Wiki格式)
|
||||||
|
const wikiRegex = /!\[\[(.*?)\]\]/g;
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(content)) !== null) {
|
while ((match = wikiRegex.exec(content)) !== null) {
|
||||||
matches.push(match[1]);
|
references.push(match[1]);
|
||||||
}
|
}
|
||||||
return matches;
|
|
||||||
|
// 匹配  格式 (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[] = [];
|
const uploadedUrls: string[] = [];
|
||||||
for (const ref of embedReferences) {
|
for (const ref of embedReferences) {
|
||||||
// 处理带有#的文件路径,分离文件名和标题部分
|
// 处理带有#的文件路径,分离文件名和标题部分
|
||||||
@ -37,8 +50,14 @@ export class EmbedHandler {
|
|||||||
if (abstractFile instanceof TFile) {
|
if (abstractFile instanceof TFile) {
|
||||||
// 检查是否为图片或PDF文件
|
// 检查是否为图片或PDF文件
|
||||||
if (isImageFile(abstractFile)) {
|
if (isImageFile(abstractFile)) {
|
||||||
const imageUrl = await this.api.uploadImage(abstractFile);
|
const imageResult = await this.api.uploadImage(abstractFile);
|
||||||
uploadedUrls.push(imageUrl || "");
|
if (imageResult) {
|
||||||
|
// 根据配置选择使用短URL还是完整URL
|
||||||
|
const urlToUse = useRemoteUrl && imageResult.fullUrl ? imageResult.fullUrl : imageResult.shortUrl;
|
||||||
|
uploadedUrls.push(urlToUse);
|
||||||
|
} else {
|
||||||
|
uploadedUrls.push("");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非图片文件,返回空字符串
|
// 非图片文件,返回空字符串
|
||||||
uploadedUrls.push("");
|
uploadedUrls.push("");
|
||||||
@ -58,14 +77,23 @@ export class EmbedHandler {
|
|||||||
// 替换内容中的嵌入引用为Markdown格式
|
// 替换内容中的嵌入引用为Markdown格式
|
||||||
replaceEmbedReferences(content: string, embedReferences: string[], uploadedUrls: string[]): string {
|
replaceEmbedReferences(content: string, embedReferences: string[], uploadedUrls: string[]): string {
|
||||||
let processedContent = content;
|
let processedContent = content;
|
||||||
|
|
||||||
embedReferences.forEach((ref, index) => {
|
embedReferences.forEach((ref, index) => {
|
||||||
const obsRef = `![[${ref}]]`;
|
|
||||||
// 只有当上传URL不为空时(即为图片)才替换为Markdown格式的图片链接
|
|
||||||
if (uploadedUrls[index]) {
|
if (uploadedUrls[index]) {
|
||||||
const discoRef = ``;
|
// 处理 ![[...]] 格式 (Wiki格式)
|
||||||
processedContent = processedContent.replace(obsRef, discoRef);
|
const wikiRef = `![[${ref}]]`;
|
||||||
|
const wikiReplacement = ``;
|
||||||
|
processedContent = processedContent.replace(wikiRef, wikiReplacement);
|
||||||
|
|
||||||
|
// 处理  格式 (Markdown格式)
|
||||||
|
// 创建正则表达式来匹配具体的路径
|
||||||
|
const escapedRef = ref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const markdownRegex = new RegExp(`!\\[([^\\]]*)\\]\\(${escapedRef}\\)`, 'g');
|
||||||
|
const markdownReplacement = ``;
|
||||||
|
processedContent = processedContent.replace(markdownRegex, markdownReplacement);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return processedContent;
|
return processedContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,6 +5,8 @@ export default {
|
|||||||
|
|
||||||
'SKIP_H1': 'Skip First Heading',
|
'SKIP_H1': 'Skip First Heading',
|
||||||
'SKIP_H1_DESC': 'Skip the first heading (H1) when publishing to Discourse',
|
'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',
|
'TEST_API_KEY': 'Test Connection',
|
||||||
'TESTING': 'Testing...',
|
'TESTING': 'Testing...',
|
||||||
'API_TEST_SUCCESS': 'Connection successful! API key is valid',
|
'API_TEST_SUCCESS': 'Connection successful! API key is valid',
|
||||||
@ -17,7 +19,7 @@ export default {
|
|||||||
'CATEGORY': 'Category',
|
'CATEGORY': 'Category',
|
||||||
'TAGS': 'Tags',
|
'TAGS': 'Tags',
|
||||||
'ENTER_TAG': 'Enter tag name (press Enter to add)',
|
'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...',
|
'PUBLISHING': 'Publishing...',
|
||||||
'UPDATING': 'Updating...',
|
'UPDATING': 'Updating...',
|
||||||
'PUBLISH': 'Publish',
|
'PUBLISH': 'Publish',
|
||||||
|
@ -5,6 +5,8 @@ export default {
|
|||||||
|
|
||||||
'SKIP_H1': '跳过一级标题',
|
'SKIP_H1': '跳过一级标题',
|
||||||
'SKIP_H1_DESC': '发布到 Discourse 时跳过笔记中的一级标题',
|
'SKIP_H1_DESC': '发布到 Discourse 时跳过笔记中的一级标题',
|
||||||
|
'USE_REMOTE_IMAGE_URL': '使用远程图片URL',
|
||||||
|
'USE_REMOTE_IMAGE_URL_DESC': '发布后用Discourse上的远程图片URL替换本地文章中的图片链接',
|
||||||
'TEST_API_KEY': '测试连接',
|
'TEST_API_KEY': '测试连接',
|
||||||
'TESTING': '测试中...',
|
'TESTING': '测试中...',
|
||||||
'API_TEST_SUCCESS': '连接成功!API密钥有效',
|
'API_TEST_SUCCESS': '连接成功!API密钥有效',
|
||||||
@ -17,7 +19,7 @@ export default {
|
|||||||
'CATEGORY': '分类',
|
'CATEGORY': '分类',
|
||||||
'TAGS': '标签',
|
'TAGS': '标签',
|
||||||
'ENTER_TAG': '输入标签名称(回车添加)',
|
'ENTER_TAG': '输入标签名称(回车添加)',
|
||||||
'ENTER_TAG_WITH_CREATE': '输入标签名称(可创建新标签)',
|
'ENTER_TAG_WITH_CREATE': '输入标签名称(回车添加)(可创建新标签)',
|
||||||
'PUBLISHING': '发布中...',
|
'PUBLISHING': '发布中...',
|
||||||
'UPDATING': '更新中...',
|
'UPDATING': '更新中...',
|
||||||
'PUBLISH': '发布',
|
'PUBLISH': '发布',
|
||||||
|
53
src/main.ts
53
src/main.ts
@ -239,7 +239,7 @@ export default class PublishToDiscourse extends Plugin implements PluginInterfac
|
|||||||
const embedReferences = this.embedHandler.extractEmbedReferences(content);
|
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格式
|
// 替换嵌入引用为Markdown格式
|
||||||
content = this.embedHandler.replaceEmbedReferences(content, embedReferences, uploadedUrls);
|
content = this.embedHandler.replaceEmbedReferences(content, embedReferences, uploadedUrls);
|
||||||
@ -276,6 +276,11 @@ export default class PublishToDiscourse extends Plugin implements PluginInterfac
|
|||||||
// 如果更新成功,更新Front Matter
|
// 如果更新成功,更新Front Matter
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await this.updateFrontMatter(postId, topicId, currentTags);
|
await this.updateFrontMatter(postId, topicId, currentTags);
|
||||||
|
|
||||||
|
// 如果启用了远程URL替换,更新本地文件中的图片链接
|
||||||
|
if (this.settings.useRemoteImageUrl) {
|
||||||
|
await this.updateLocalImageLinks(embedReferences, uploadedUrls);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 创建新帖子
|
// 创建新帖子
|
||||||
@ -289,6 +294,11 @@ export default class PublishToDiscourse extends Plugin implements PluginInterfac
|
|||||||
// 如果创建成功,更新Front Matter
|
// 如果创建成功,更新Front Matter
|
||||||
if (result.success && result.postId && result.topicId) {
|
if (result.success && result.postId && result.topicId) {
|
||||||
await this.updateFrontMatter(result.postId, result.topicId, currentTags);
|
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 = ``;
|
||||||
|
if (content.includes(wikiRef)) {
|
||||||
|
content = content.replace(wikiRef, wikiReplacement);
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换  格式 (Markdown格式)
|
||||||
|
const escapedRef = ref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const markdownRegex = new RegExp(`!\\[([^\\]]*)\\]\\(${escapedRef}\\)`, 'g');
|
||||||
|
const markdownReplacement = ``;
|
||||||
|
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() {}
|
onunload() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user