Skip to content

Commit 32b4e88

Browse files
committed
Create by Trilium: source/_posts/ziiabvmjyoxp.md
1 parent 9940ef4 commit 32b4e88

File tree

1 file changed

+328
-0
lines changed

1 file changed

+328
-0
lines changed

source/_posts/ziiabvmjyoxp.md

+328
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
---
2+
title: HexoPublisher
3+
category:
4+
- Blog
5+
date: 2024-10-31 12:54:09.159Z
6+
updated: 2024-12-24 04:52:01.113Z
7+
---
8+
9+
class HexoPublisher extends api.NoteContextAwareWidget {
10+
constructor() {
11+
super();
12+
this.bindMethods();
13+
}
14+
15+
get parentWidget() {
16+
return 'center-pane';
17+
}
18+
doRender() {
19+
this.$widget = $();
20+
return this.$widget;
21+
}
22+
bindMethods() {
23+
this.previewContent = this.previewContent.bind(this);
24+
this.publishContent = this.publishContent.bind(this);
25+
}
26+
27+
async refreshWithNote() {
28+
$(document).ready(() => {
29+
this.addButtonsIfNeeded();
30+
this.bindEventHandlers();
31+
});
32+
}
33+
34+
addButtonsIfNeeded() {
35+
const $noteSplit = $("div.component.note-split:not(.hidden-ext)");
36+
if (!$noteSplit.find("div.ribbon-tab-title").hasClass('github-publisher-button')) {
37+
const buttonsHtml = this.getButtonsHtml();
38+
$noteSplit.find(".ribbon-tab-title:not(.backToHis)").last().after(buttonsHtml);
39+
}
40+
}
41+
42+
getButtonsHtml() {
43+
return `
44+
<div class="github-publisher-button ribbon-tab-spacer"></div>
45+
<div class="ribbon-tab-title" data-ribbon-component-name="markdownPreview">
46+
<span class="ribbon-tab-title-icon bx bx-file" data-title="预览文章" data-toggle-command="previewMarkdown"></span>
47+
<span class="ribbon-tab-title-label">预览</span>
48+
</div>
49+
<div class="github-publisher-button ribbon-tab-spacer"></div>
50+
<div class="ribbon-tab-title" data-ribbon-component-name="githubPublish">
51+
<span class="ribbon-tab-title-icon bx bx-upload" data-title="发布到GitHub" data-toggle-command="publishToGithub"></span>
52+
<span class="ribbon-tab-title-label">发布</span>
53+
</div>
54+
`;
55+
}
56+
57+
bindEventHandlers() {
58+
const $buttons = $('div.component.note-split:not(.hidden-ext)');
59+
$buttons.find('[data-toggle-command="previewMarkdown"]').off("click").on("click", this.previewContent);
60+
$buttons.find('[data-toggle-command="publishToGithub"]').off("click").on("click", this.publishContent);
61+
}
62+
63+
async previewContent() {
64+
try {
65+
const { content, frontMatter, note } = await this.collectNoteData();
66+
67+
const processedContent = await this.processContent(content, frontMatter, note);
68+
69+
const previewMessage = `预览内容(图片将会被上传到 source/images 目录):\n\n${processedContent}`;
70+
71+
api.showMessage(previewMessage);
72+
} catch (error) {
73+
api.showError('Failed to preview: ' + error.message);
74+
}
75+
}
76+
77+
78+
async publishContent() {
79+
try {
80+
const { content, frontMatter, note } = await this.collectNoteData();
81+
await this.validatePublishRequirements(note);
82+
83+
const fileName = this.generateFileName(note);
84+
const processedContent = await this.processContent(content, frontMatter, note);
85+
86+
const result = await this.uploadToGithub(note, fileName, processedContent, false);
87+
this.showPublishResult(result);
88+
} catch (error) {
89+
api.showError('Failed to publish: ' + error.message);
90+
}
91+
}
92+
93+
async collectNoteData() {
94+
const editor = await api.getActiveContextTextEditor();
95+
const note = await api.getActiveContextNote();
96+
const content = note.getContent();
97+
api.showMessage(JSON.stringify(content));
98+
const frontMatter = await this.collectFrontMatter(note);
99+
return { content, frontMatter, note };
100+
}
101+
102+
validatePublishRequirements(note) {
103+
const repoPathAttr = note.getAttribute('label', 'githubRepo');
104+
const tokenAttr = note.getAttribute('label', 'githubToken');
105+
106+
if (!repoPathAttr || !tokenAttr) {
107+
throw new Error('Missing required attributes: githubRepo or githubToken');
108+
}
109+
return { repoPath: repoPathAttr.value, token: tokenAttr.value };
110+
}
111+
112+
generateFileName(note) {
113+
return `source/_posts/${note.noteId.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase()}.md`;
114+
}
115+
116+
async processContent(content, frontMatter, note) {
117+
const processedContent = await this.processImages(content, note);
118+
return this.generateHexoContent(processedContent, frontMatter);
119+
}
120+
121+
async processImages(content, note) {
122+
const markdownRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
123+
const htmlImgRegex = /<img[^>]+src="([^"]+)"[^>]*>/g;
124+
let processedContent = content;
125+
// 处理 Markdown 格式的图片
126+
let mdMatch;
127+
while ((mdMatch = markdownRegex.exec(content)) !== null) {
128+
api.showMessage(htmlMatch);
129+
130+
const [fullMatch, altText, imagePath] = mdMatch;
131+
processedContent = await this.handleImageUpload(
132+
processedContent,
133+
imagePath,
134+
fullMatch,
135+
note,
136+
(newPath) => `![${altText}](${newPath})`
137+
);
138+
}
139+
140+
// 处理 HTML 格式的图片
141+
let htmlMatch;
142+
while ((htmlMatch = htmlImgRegex.exec(content)) !== null) {
143+
const [fullImg, imagePath] = htmlMatch;
144+
processedContent = await this.handleImageUpload(
145+
processedContent,
146+
imagePath,
147+
fullImg,
148+
note,
149+
(newPath) => {
150+
// 保持原有的 HTML 标签属性,只更新 src
151+
return fullImg.replace(imagePath, newPath);
152+
}
153+
);
154+
}
155+
156+
return processedContent;
157+
}
158+
159+
async handleImageUpload(content, imagePath, originalText, note, replacer) {
160+
if (imagePath.includes('api/attachments/')) {
161+
try {
162+
const matches = imagePath.match(/api\/attachments\/([^\/]+?)(\/|$)/);
163+
const attachmentId = matches ? matches[1] : null;
164+
if (!attachmentId) {
165+
throw new Error('Could not extract attachment ID from path');
166+
}
167+
const attachmentUrl = `api/attachments/${attachmentId}/download`;
168+
169+
const response = await fetch(attachmentUrl);
170+
const arrayBuffer = await response.arrayBuffer();
171+
const base64Content = Buffer.from(arrayBuffer).toString('base64');
172+
const originalFileName = imagePath.split('/').pop();
173+
174+
const fileName = `source/images/${note.noteId}-${originalFileName}`.toLowerCase();
175+
const requestBody = {
176+
message: `Add image By Trilium: ${fileName}`,
177+
content: base64Content
178+
};
179+
180+
const { repoPath, token } = this.validatePublishRequirements(note);
181+
await this.makeGithubRequest(repoPath, token, fileName, requestBody);
182+
183+
const newImagePath = `/images/${note.noteId}-${originalFileName}`;
184+
return content.replace(originalText, replacer(newImagePath));
185+
} catch (error) {
186+
api.showError(`Failed to process image: ${error.message}`);
187+
}
188+
}
189+
return content;
190+
}
191+
192+
async uploadToGithub(note, fileName, content, isBase64 = true) {
193+
const { repoPath, token } = this.validatePublishRequirements(note);
194+
const { sha, exists } = await this.checkFileExists(repoPath, token, fileName);
195+
196+
const requestBody = {
197+
content: isBase64 ? content : Buffer.from(content).toString('base64'),
198+
message: exists
199+
? `Update by Trilium: ${fileName}`
200+
: `Create by Trilium: ${fileName}`
201+
};
202+
203+
if (sha) {
204+
requestBody.sha = sha;
205+
}
206+
207+
const response = await this.makeGithubRequest(repoPath, token, fileName, requestBody);
208+
return { ...response, isUpdate: exists };
209+
}
210+
211+
212+
async checkFileExists(repoPath, token, fileName) {
213+
try {
214+
const response = await fetch(
215+
`https://api.github.com/repos/${repoPath}/contents/${fileName}`,
216+
this.getGithubRequestOptions('GET', token)
217+
);
218+
219+
if (response.status === 200) {
220+
const fileData = await response.json();
221+
return { sha: fileData.sha, exists: true };
222+
}
223+
return { sha: null, exists: false };
224+
} catch (error) {
225+
console.log('File check failed:', error);
226+
return { sha: null, exists: false };
227+
}
228+
}
229+
230+
getGithubRequestOptions(method, token, body = null) {
231+
const options = {
232+
method,
233+
headers: {
234+
'Authorization': `Bearer ${token}`,
235+
'Accept': 'application/vnd.github.v3+json'
236+
}
237+
};
238+
239+
if (body) {
240+
options.headers['Content-Type'] = 'application/json';
241+
options.body = JSON.stringify(body);
242+
}
243+
244+
return options;
245+
}
246+
247+
async makeGithubRequest(repoPath, token, fileName, body) {
248+
const response = await fetch(
249+
`https://api.github.com/repos/${repoPath}/contents/${fileName}`,
250+
this.getGithubRequestOptions('PUT', token, body)
251+
);
252+
253+
if (!response.ok) {
254+
const errorData = await response.json();
255+
throw new Error(`GitHub API error: ${errorData.message}`);
256+
}
257+
258+
return response.json();
259+
}
260+
261+
showPublishResult(result) {
262+
api.showMessage(result.isUpdate ? 'Note Updated to GitHub!' : 'Note Created to GitHub!');
263+
}
264+
265+
processExcerpt(content, frontMatter, note) {
266+
const excerptAttr = note.getAttribute('label', 'hexoExcerpt');
267+
if (excerptAttr?.value) {
268+
frontMatter.excerpt = excerptAttr.value;
269+
return content;
270+
}
271+
272+
const moreSplit = content.split('<!-- more -->');
273+
if (moreSplit.length > 1) {
274+
frontMatter.excerpt = moreSplit[0].trim();
275+
}
276+
277+
return content;
278+
}
279+
280+
async collectFrontMatter(note) {
281+
const frontMatter = {};
282+
const attributes = note.getAttributes();
283+
284+
for (const attr of attributes) {
285+
if (attr.type === 'label' && attr.name.startsWith('hexo')) {
286+
let key = attr.name.substring(4);
287+
key = key.charAt(0).toLowerCase() + key.slice(1);
288+
289+
if (key === 'tag') {
290+
const tagAttrs = note.getAttributes('label', attr.name);
291+
frontMatter['tags'] = tagAttrs.map(tagAttr => tagAttr.value);
292+
} else {
293+
frontMatter[key] = attr.value;
294+
}
295+
}
296+
}
297+
298+
if (!frontMatter.title) {
299+
frontMatter.title = note.title;
300+
}
301+
302+
if (!frontMatter.category) {
303+
const parentNote = note.getParentNotes()[0];
304+
frontMatter.category = [parentNote.title];
305+
}
306+
307+
const metadata = await note.getMetadata();
308+
frontMatter.date = metadata.utcDateCreated;
309+
frontMatter.updated = metadata.utcDateModified;
310+
311+
return frontMatter;
312+
}
313+
314+
generateHexoContent(content, frontMatter) {
315+
const yaml = Object.entries(frontMatter)
316+
.map(([key, value]) => {
317+
if (Array.isArray(value)) {
318+
return `${key}:\n${value.map(item => ` - ${item}`).join('\n')}`;
319+
}
320+
return `${key}: ${value}`;
321+
})
322+
.join('\n');
323+
324+
return `---\n${yaml}\n---\n\n${content}`;
325+
}
326+
}
327+
328+
module.exports = new HexoPublisher();

0 commit comments

Comments
 (0)