Compare commits

...

3 Commits

Author SHA1 Message Date
Jim Liu 宝玉 cc95e6fe05 chore: release v1.28.4 2026-02-03 11:49:41 -06:00
Jim Liu 宝玉 e7d9ed7917 fix(baoyu-post-to-wechat): remove extra empty lines after image paste and fix summary timing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 11:49:13 -06:00
Jim Liu 宝玉 876c6332f6 feat(baoyu-markdown-to-html): add HTML meta tags and quote stripping for frontmatter
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 11:49:07 -06:00
6 changed files with 160 additions and 25 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.28.3"
"version": "1.28.4"
},
"plugins": [
{
+8
View File
@@ -2,6 +2,14 @@
English | [中文](./CHANGELOG.zh.md)
## 1.28.4 - 2026-02-03
### Features
- `baoyu-markdown-to-html`: add author and description meta tags to generated HTML from YAML frontmatter; strip quotes from frontmatter values (supports both English and Chinese quotation marks).
### Fixes
- `baoyu-post-to-wechat`: remove extra empty lines after image paste; fix summary field timing to fill after content paste (prevents being overwritten).
## 1.28.3 - 2026-02-03
### Fixes
+8
View File
@@ -2,6 +2,14 @@
[English](./CHANGELOG.md) | 中文
## 1.28.4 - 2026-02-03
### 新功能
- `baoyu-markdown-to-html`:从 YAML frontmatter 生成 author 和 description meta 标签;自动去除 frontmatter 值两端的引号(支持中英文引号)。
### 修复
- `baoyu-post-to-wechat`:移除图片粘贴后产生的多余空行;修复摘要填充时机,改为内容粘贴后填写(避免被覆盖)。
## 1.28.3 - 2026-02-03
### 修复
+14 -3
View File
@@ -126,7 +126,18 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
const { frontmatter, body } = parseFrontmatter(content);
let title = options?.title ?? frontmatter.title ?? '';
const stripQuotes = (s?: string): string => {
if (!s) return '';
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
return s.slice(1, -1);
}
if ((s.startsWith('\u201c') && s.endsWith('\u201d')) || (s.startsWith('\u2018') && s.endsWith('\u2019'))) {
return s.slice(1, -1);
}
return s;
};
let title = options?.title ?? stripQuotes(frontmatter.title) ?? '';
if (!title) {
const lines = body.split('\n');
for (const line of lines) {
@@ -138,8 +149,8 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
}
}
if (!title) title = path.basename(markdownPath, path.extname(markdownPath));
const author = frontmatter.author || '';
let summary = frontmatter.description || frontmatter.summary || '';
const author = stripQuotes(frontmatter.author);
let summary = stripQuotes(frontmatter.description) || stripQuotes(frontmatter.summary);
if (!summary) {
const lines = body.split('\n');
@@ -708,14 +708,28 @@ function normalizeThemeCss(css: string): string {
return stripOutputScope(css);
}
function buildHtmlDocument(title: string, css: string, html: string): string {
return [
interface HtmlDocumentMeta {
title: string;
author?: string;
description?: string;
}
function buildHtmlDocument(meta: HtmlDocumentMeta, css: string, html: string): string {
const lines = [
"<!doctype html>",
"<html>",
"<head>",
' <meta charset="utf-8" />',
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
` <title>${title}</title>`,
` <title>${meta.title}</title>`,
];
if (meta.author) {
lines.push(` <meta name="author" content="${meta.author}" />`);
}
if (meta.description) {
lines.push(` <meta name="description" content="${meta.description}" />`);
}
lines.push(
` <style>${css}</style>`,
"</head>",
"<body>",
@@ -723,8 +737,9 @@ function buildHtmlDocument(title: string, css: string, html: string): string {
html,
" </div>",
"</body>",
"</html>",
].join("\n");
"</html>"
);
return lines.join("\n");
}
async function inlineCss(html: string): Promise<string> {
@@ -814,6 +829,7 @@ async function main(): Promise<void> {
const markdown = fs.readFileSync(inputPath, "utf-8");
const renderer = initRenderer({});
const { yamlData } = renderer.parseFrontMatterAndContent(markdown);
const { html: baseHtml, readingTime: readingTimeResult } = renderMarkdown(
markdown,
renderer
@@ -823,8 +839,23 @@ async function main(): Promise<void> {
content = removeFirstHeading(content);
}
const title = path.basename(outputPath, ".html");
const html = buildHtmlDocument(title, css, content);
const stripQuotes = (s?: string): string | undefined => {
if (!s) return s;
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
return s.slice(1, -1);
}
if ((s.startsWith('\u201c') && s.endsWith('\u201d')) || (s.startsWith('\u2018') && s.endsWith('\u2019'))) {
return s.slice(1, -1);
}
return s;
};
const meta: HtmlDocumentMeta = {
title: stripQuotes(yamlData.title) || path.basename(outputPath, ".html"),
author: stripQuotes(yamlData.author),
description: stripQuotes(yamlData.description) || stripQuotes(yamlData.summary),
};
const html = buildHtmlDocument(meta, css, content);
const inlinedHtml = normalizeInlineCss(await inlineCss(html));
const finalHtml = modifyHtmlStructure(inlinedHtml);
@@ -315,6 +315,71 @@ async function pressDeleteKey(session: ChromeSession): Promise<void> {
await session.cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId: session.sessionId });
}
async function removeExtraEmptyLineAfterImage(session: ChromeSession): Promise<boolean> {
const removed = await evaluate<boolean>(session, `
(function() {
const editor = document.querySelector('.ProseMirror');
if (!editor) return false;
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0) return false;
let node = sel.anchorNode;
if (!node) return false;
let element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
if (!element || !editor.contains(element)) return false;
const isEmptyParagraph = (el) => {
if (!el || el.tagName !== 'P') return false;
const text = (el.textContent || '').trim();
if (text.length > 0) return false;
return el.querySelectorAll('img, figure, video, iframe').length === 0;
};
const hasImage = (el) => {
if (!el) return false;
return !!el.querySelector('img, figure img, picture img');
};
const placeCursorAfter = (el) => {
if (!el) return;
const range = document.createRange();
range.setStartAfter(el);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
};
// Case 1: caret is inside an empty paragraph right after an image block.
const emptyPara = element.closest('p');
if (emptyPara && editor.contains(emptyPara) && isEmptyParagraph(emptyPara)) {
const prev = emptyPara.previousElementSibling;
if (prev && hasImage(prev)) {
emptyPara.remove();
placeCursorAfter(prev);
return true;
}
}
// Case 2: caret is on the image block itself; remove the next empty paragraph.
const imageBlock = element.closest('figure, p');
if (imageBlock && editor.contains(imageBlock) && hasImage(imageBlock)) {
const next = imageBlock.nextElementSibling;
if (next && isEmptyParagraph(next)) {
next.remove();
placeCursorAfter(imageBlock);
return true;
}
}
return false;
})()
`);
if (removed) console.log('[wechat] Removed extra empty line after image.');
return removed;
}
export async function postArticle(options: ArticleOptions): Promise<void> {
const { title, content, htmlFile, markdownFile, theme, author, summary, images = [], submit = false, profileDir, cdpPort } = options;
let { contentImages = [] } = options;
@@ -454,11 +519,6 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
await evaluate(session, `document.querySelector('#author').value = ${JSON.stringify(effectiveAuthor)}; document.querySelector('#author').dispatchEvent(new Event('input', { bubbles: true }));`);
}
if (effectiveSummary) {
console.log(`[wechat] Filling summary: ${effectiveSummary}`);
await evaluate(session, `document.querySelector('#js_description').value = ${JSON.stringify(effectiveSummary)}; document.querySelector('#js_description').dispatchEvent(new Event('input', { bubbles: true }));`);
}
await sleep(500);
if (effectiveTitle) {
@@ -470,15 +530,6 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
}
}
if (effectiveSummary) {
const actualSummary = await evaluate<string>(session, `document.querySelector('#js_description')?.value || ''`);
if (actualSummary === effectiveSummary) {
console.log('[wechat] Summary verified OK.');
} else {
console.warn(`[wechat] Summary verification failed. Expected: "${effectiveSummary}", got: "${actualSummary}"`);
}
}
console.log('[wechat] Clicking on editor...');
await clickElement(session, '.ProseMirror');
await sleep(1000);
@@ -534,6 +585,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
console.log('[wechat] Pasting image...');
await pasteFromClipboardInEditor(session);
await sleep(3000);
await removeExtraEmptyLineAfterImage(session);
}
console.log('[wechat] All images inserted.');
}
@@ -545,6 +597,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
await sleep(500);
await pasteInEditor(session);
await sleep(2000);
await removeExtraEmptyLineAfterImage(session);
}
}
@@ -567,6 +620,30 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
}
}
if (effectiveSummary) {
console.log(`[wechat] Filling summary (after content paste): ${effectiveSummary}`);
await evaluate(session, `
(function() {
const el = document.querySelector('#js_description');
if (!el) return;
el.focus();
el.select();
el.value = ${JSON.stringify(effectiveSummary)};
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('blur', { bubbles: true }));
})()
`);
await sleep(500);
const actualSummary = await evaluate<string>(session, `document.querySelector('#js_description')?.value || ''`);
if (actualSummary === effectiveSummary) {
console.log('[wechat] Summary verified OK.');
} else {
console.warn(`[wechat] Summary verification failed. Expected: "${effectiveSummary}", got: "${actualSummary}"`);
}
}
console.log('[wechat] Saving as draft...');
await evaluate(session, `document.querySelector('#js_submit button').click()`);
await sleep(3000);