`。
-- ` ...` → 前缀补全 `/img/`。
-
-### 7.4 Gallery 功能
-- 短代码 Regex:`{{
`)。
-- 规划:`caption` 字段可映射为 wikilink alias 或 `
区域,便于复制。
-4. Notice 简要提示(DryRun 或 完成)。
-
-错误处理:
-- try/catch 包裹,失败写入 resultPre 文本 + Notice。
-- run 按钮在执行期间 disabled,防止重复触发。
-
-后续增强设想:
-| 项目 | 说明 |
-|------|------|
-| 进度条 | 删除大批量时显示当前进度/总数 |
-| 失败重试 | 针对 fails 列表单独重试按钮 |
-| 过滤条件 | 增加标题关键词 / 日期起止输入 |
-| 多账号选择 | 下拉列出已配置的 appid 列表 |
-| 日志导出 | 一键复制 JSON 结果 |
-
diff --git a/archives/v1.3.0/diagrams.md b/archives/v1.3.0/diagrams.md
deleted file mode 100644
index 113d8c3..0000000
--- a/archives/v1.3.0/diagrams.md
+++ /dev/null
@@ -1,178 +0,0 @@
-# Diagrams (Mermaid)
-
-> 动态架构与主要交互可视化。与文字说明对应:`architecture.md` / `image-pipeline.md` / `render-service-blueprint.md`。
-
-## 1. 模块类图 (当前实现概览)
-```mermaid
-classDiagram
- class NotePreview {
- +renderMarkdown()
- +uploadImages()
- +postArticle()
- +postImages()
- +exportHTML()
- }
- class ArticleRender {
- +renderMarkdown(file)
- +getMetadata()
- +uploadImages(appid)
- +postArticle(appid,cover?)
- +postImages(appid)
- +exportHTML()
- +transformGalleryBlock()
- +applyCustomInlineBlocks()
- }
- class LocalImageManager {
- +setImage(path,info)
- +uploadLocalImage(token,vault)
- +uploadRemoteImage(root,token)
- +replaceImages(root)
- }
- class LocalFile {
- +markedExtension()
- }
- class AssetsManager {
- +loadAssets()
- +getTheme(name)
- +getHighlight(name)
- }
- class NMPSettings {
- +wxInfo
- +authKey
- +enableMarkdownImageToWikilink
- +galleryPrePath
- +galleryNumPic
- +defaultCoverPic
- }
- class WeChatAPI {
- +wxGetToken()
- +wxAddDraft()
- +wxUploadImage()
- }
-
- NotePreview --> ArticleRender
- ArticleRender --> LocalImageManager
- ArticleRender --> AssetsManager
- ArticleRender --> NMPSettings
- ArticleRender --> WeChatAPI
- ArticleRender --> LocalFile
- LocalFile --> LocalImageManager
- NotePreview --> NMPSettings
- NotePreview --> AssetsManager
-```
-
-## 2. 发布草稿时序图
-```mermaid
-sequenceDiagram
- participant U as User
- participant NP as NotePreview
- participant AR as ArticleRender
- participant WX as WeChatAPI
- participant LIM as LocalImageManager
-
- U->>NP: 点击 发草稿
- NP->>AR: postArticle(appid)
- AR->>WX: wxGetToken(authKey, appid)
- WX-->>AR: token
- AR->>AR: cachedElementsToImages()
- AR->>LIM: uploadLocalImage(token)
- LIM-->>AR: local media_id(s)
- AR->>LIM: uploadRemoteImage(token)
- LIM-->>AR: remote media_id(s)
- AR->>LIM: replaceImages()
- AR->>AR: resolve cover (frontmatter / first image / default)
- AR->>WX: wxAddDraft(draft JSON)
- WX-->>AR: media_id | err
- AR-->>NP: 结果
- NP-->>U: 成功 / 失败提示
-```
-
-## 3. 图片上传流程图
-```mermaid
-graph TD
- A[Start UploadImages] --> B{AuthKey/AppId?}
- B -- No --> Z[Throw Error]
- B -- Yes --> C[Get Token]
- C --> D[cachedElementsToImages]
- D --> E[uploadLocalImage]
- E --> F[uploadRemoteImage]
- F --> G[replaceImages]
- G --> H[Copy HTML to Clipboard]
- H --> I[End]
-```
-
-## 4. 自动封面推断逻辑
-```mermaid
-graph TD
- A[Need Cover?] -->|No| Z[Skip]
- A -->|Yes| B[Frontmatter cover?]
- B -- Yes --> H[Use frontmatter]
- B -- No --> C[Strip Frontmatter]
- C --> D[Scan Markdown Images]
- C --> E[Scan Wikilink Images]
- D --> F[Collect Candidates]
- E --> F[Collect Candidates]
- F --> G{Any Body Image?}
- G -- Yes --> H[Use first body image]
- G -- No --> I[Gallery Expanded?]
- I -- Yes --> H[Use first gallery image]
- I -- No --> J[defaultCoverPic Config?]
- J -- Yes --> H[Use defaultCoverPic]
- J -- No --> Z[Cover stays empty]
-```
-
-## 4.1 行级轻语法与日志节流 (补充)
-```mermaid
-graph TD
- M[Markdown Raw] --> P[Preprocess Gallery Shortcode]
- P --> GB[Gallery Block Parse]
- GB --> MD[Marked Parse]
- MD --> IB[applyCustomInlineBlocks]
- IB --> R[Render HTML]
- R --> L{Log Throttle}
- L --> R1[Path Log]
- L --> R2[Cover Fallback Log]
-```
-
-## 5. 未来 RenderService Pipeline 图
-```mermaid
-graph TD
- L[Loader] --> FM[Frontmatter]
- FM --> PP[Preprocessors]
- PP --> P[Parser]
- P --> TR[Transformers]
- TR --> RI[ResourceIndex]
- RI --> R[Renderer]
- R --> PO[Postprocessors]
- PO --> EX[Exporters]
-```
-
-## 6. 并发上传示意 (未来优化)
-```mermaid
-graph TD
- A[Images] --> B[Partition]
- B --> C[Pool]
- C --> D[Upload]
- D --> E{Success?}
- E -->|No| R[Retry]
- E -->|Yes| F[Collect ids]
- R --> C
- F --> G[Done]
-```
-
-## 7. 状态机概览 (发布按钮)
-```mermaid
-stateDiagram-v2
- [*] --> Idle
- Idle --> Uploading : 点击 上传/发布
- Uploading --> Publishing : 草稿模式
- Uploading --> Completed : 仅上传
- Publishing --> Completed : 响应成功
- Publishing --> Failed : 接口错误
- Uploading --> Failed : 资源错误
- Failed --> Idle : 用户重试
- Completed --> Idle : 新文件切换
-```
-
----
-需要我将这些图嵌入到 README 的一个“开发者”章节吗?可以继续提出。
diff --git a/archives/v1.3.0/manifest.json b/archives/v1.3.0/manifest.json
deleted file mode 100644
index e05f008..0000000
--- a/archives/v1.3.0/manifest.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "id": "note-to-mp",
- "name": "NoteToMP",
- "version": "1.3.0",
- "minAppVersion": "1.4.5",
- "description": "Send notes to WeChat MP drafts, or copy notes to WeChat MP editor, perfect preservation of note styles, support code highlighting, line numbers in code, and support local image uploads.",
- "author": "Sun Booshi",
- "authorUrl": "https://sunboshi.tech",
- "isDesktopOnly": false
-}
diff --git a/archives/v1.3.0/package.json b/archives/v1.3.0/package.json
deleted file mode 100644
index 0cbe0e0..0000000
--- a/archives/v1.3.0/package.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "name": "note-to-mp",
- "version": "1.3.0",
- "description": "This is a plugin for Obsidian (https://obsidian.md)",
- "main": "main.js",
- "scripts": {
- "dev": "node esbuild.config.mjs",
- "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
- "download": "node tools/download.mjs",
- "version": "node version-bump.mjs && git add manifest.json versions.json"
- },
- "keywords": [],
- "author": "",
- "license": "MIT",
- "devDependencies": {
- "@types/node": "^16.11.6",
- "@typescript-eslint/eslint-plugin": "5.29.0",
- "@typescript-eslint/parser": "5.29.0",
- "builtin-modules": "3.3.0",
- "esbuild": "0.17.3",
- "obsidian": "latest",
- "tslib": "2.4.0",
- "typescript": "4.7.4"
- },
- "dependencies": {
- "@zip.js/zip.js": "^2.7.43",
- "highlight.js": "^11.9.0",
- "html-to-image": "^1.11.11",
- "marked": "^12.0.1",
- "marked-highlight": "^2.1.3"
- }
-}
diff --git a/archives/v1.3.0/source-snapshot-v1.3.0.tar.gz b/archives/v1.3.0/source-snapshot-v1.3.0.tar.gz
deleted file mode 100644
index 199d5ae..0000000
Binary files a/archives/v1.3.0/source-snapshot-v1.3.0.tar.gz and /dev/null differ
diff --git a/archives/v1.3.0/styles.css b/archives/v1.3.0/styles.css
deleted file mode 100644
index dd3aefe..0000000
--- a/archives/v1.3.0/styles.css
+++ /dev/null
@@ -1,142 +0,0 @@
-/* archives/v1.3.0/styles.css — 归档版本的样式文件。 */
-
-/* =========================================================== */
-/* UI 样式 */
-/* =========================================================== */
-.note-preview {
- min-height: 100%;
- width: 100%;
- height: 100%;
- background-color: #fff;
- display: flex;
- flex-direction: column;
-}
-
-.render-div {
- flex: 1;
- overflow-y: auto;
- padding: 10px;
-}
-
-.preview-toolbar {
- position: relative;
- min-height: 100px;
- border-bottom: #e4e4e4 1px solid;
- background-color: var(--background-primary);
-}
-
-.toolbar-line {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- align-items: center;
- margin: 10px 10px;
-}
-
-.copy-button {
- margin-right: 10px;
-}
-
-.refresh-button {
- margin-right: 10px;
-}
-
-.upload-input {
- margin-left: 10px;
- visibility: hidden;
- width: 0px;
-}
-
-.style-label {
- margin-right: 10px;
-}
-
-.style-select {
- margin-right: 10px;
- width: 120px;
-}
-
-.msg-view {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: var(--background-primary);
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- font-size: 18px;
- z-index: 9999;
- display: none;
-}
-
-.msg-title {
- margin-bottom: 20px;
- max-width: 90%;
-}
-
-.note-mpcard-wrapper {
- margin: 20px 20px;
- background-color: rgb(250, 250, 250);
- padding: 10px 20px;
- border-radius: 10px;
-}
-.note-mpcard-content {
- display: flex;
-}
-.note-mpcard-headimg {
- border: none !important;
- border-radius: 27px !important;
- box-shadow: none !important;
- width: 54px !important;
- height: 54px !important;
- margin: 0 !important;
-}
-.note-mpcard-info {
- margin-left: 10px;
-}
-.note-mpcard-nickname {
- font-size: 17px;
- font-weight: 500;
- color: rgba(0, 0, 0, 0.9);
-}
-
-.note-mpcard-signature {
- font-size: 14px;
- color: rgba(0, 0, 0, 0.55);
-}
-.note-mpcard-foot {
- margin-top: 20px;
- padding-top: 10px;
- border-top: 1px solid #ececec;
- font-size: 14px;
- color: rgba(0, 0, 0, 0.3);
-}
-
-.loading-wrapper {
- display: flex;
- width: 100%;
- height: 100%;
- align-items: center;
- justify-content: center;
-}
-
-.loading-spinner {
- width: 50px; /* 可调整大小 */
- height: 50px;
- border: 4px solid #fcd6ff; /* 底色,浅灰 */
- border-top: 4px solid #bb0cdf; /* 主色,蓝色顶部产生旋转感 */
- border-radius: 50%; /* 圆形 */
- animation: spin 1s linear infinite; /* 旋转动画 */
-}
-
-@keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
-}
\ No newline at end of file
diff --git a/build.sh b/build.sh
index 54aef03..cc7eb02 100755
--- a/build.sh
+++ b/build.sh
@@ -14,6 +14,7 @@ for FILE in "${FILES[@]}"; do
BACKUP="$PLUGIN_DIR/backup/$FILE.bk"
if [ -f "$TARGET" ]; then
+ mkdir -p "$(dirname "$BACKUP")"
cp -f "$TARGET" "$BACKUP"
echo "已备份 $TARGET -> $BACKUP"
fi
@@ -25,3 +26,12 @@ for FILE in "${FILES[@]}"; do
echo "⚠️ 源文件 $FILE 不存在,跳过"
fi
done
+
+# 4. 覆盖复制 assets 目录(静默)
+if [ -d "assets" ]; then
+ mkdir -p "$PLUGIN_DIR/assets"
+ rsync -a --delete assets/ "$PLUGIN_DIR/assets/" >/dev/null
+ echo "已同步 assets -> $PLUGIN_DIR/assets/"
+else
+ echo "⚠️ 源目录 assets 不存在,跳过"
+fi
diff --git a/images/xhs/note2mdtest_1.png b/images/xhs/note2mdtest_1.png
index 28b299b..8d130bf 100644
Binary files a/images/xhs/note2mdtest_1.png and b/images/xhs/note2mdtest_1.png differ
diff --git a/images/xhs/note2mdtest_2.png b/images/xhs/note2mdtest_2.png
index 81fd305..fa99fdc 100644
Binary files a/images/xhs/note2mdtest_2.png and b/images/xhs/note2mdtest_2.png differ
diff --git a/images/xhs/note2mdtest_3.png b/images/xhs/note2mdtest_3.png
index dc986ac..37f1a90 100644
Binary files a/images/xhs/note2mdtest_3.png and b/images/xhs/note2mdtest_3.png differ
diff --git a/images/xhs/note2mdtest_4.png b/images/xhs/note2mdtest_4.png
index 8c5b965..b9e45d1 100644
Binary files a/images/xhs/note2mdtest_4.png and b/images/xhs/note2mdtest_4.png differ
diff --git a/images/xhs/note2mdtest_5.png b/images/xhs/note2mdtest_5.png
index 4ba4f3a..21c8eaa 100644
Binary files a/images/xhs/note2mdtest_5.png and b/images/xhs/note2mdtest_5.png differ
diff --git a/images/xhs/note2mdtest_6.png b/images/xhs/note2mdtest_6.png
index cbab7c4..9103225 100644
Binary files a/images/xhs/note2mdtest_6.png and b/images/xhs/note2mdtest_6.png differ
diff --git a/images/xhs/note2mdtest_7.png b/images/xhs/note2mdtest_7.png
index 17d1102..d60152e 100644
Binary files a/images/xhs/note2mdtest_7.png and b/images/xhs/note2mdtest_7.png differ
diff --git a/images/xhs/note2mdtest_8.png b/images/xhs/note2mdtest_8.png
index 1a95302..14126fe 100644
Binary files a/images/xhs/note2mdtest_8.png and b/images/xhs/note2mdtest_8.png differ
diff --git a/images/xhs/note2mdtest_9.png b/images/xhs/note2mdtest_9.png
index f62b16c..32abb19 100644
Binary files a/images/xhs/note2mdtest_9.png and b/images/xhs/note2mdtest_9.png differ
diff --git a/src/xiaohongshu/paginator.ts b/src/xiaohongshu/paginator.ts
index 7f8a131..3fd9e89 100644
--- a/src/xiaohongshu/paginator.ts
+++ b/src/xiaohongshu/paginator.ts
@@ -27,7 +27,9 @@ function parseAspectRatio(ratio: string): { width: number; height: number } {
*/
function getTargetPageHeight(settings: NMPSettings): number {
const ratio = parseAspectRatio(settings.sliceImageAspectRatio);
- return Math.round((settings.sliceImageWidth * ratio.height) / ratio.width);
+ const height = Math.round((settings.sliceImageWidth * ratio.height) / ratio.width);
+ console.log(`[paginator] 计算页面高度: 宽度=${settings.sliceImageWidth}, 比例=${settings.sliceImageAspectRatio} (${ratio.width}:${ratio.height}), 高度=${height}`);
+ return height;
}
/**
@@ -150,20 +152,26 @@ function wrapPageContent(elements: Element[]): string {
/**
* 渲染单个页面到容器
+ * 预览时缩放显示,切图时使用实际尺寸
*/
export function renderPage(
container: HTMLElement,
pageContent: string,
settings: NMPSettings
): void {
- const pageHeight = getTargetPageHeight(settings);
- const pageWidth = settings.sliceImageWidth;
+ // 实际内容尺寸(切图使用)
+ const actualPageWidth = settings.sliceImageWidth;
+ const actualPageHeight = getTargetPageHeight(settings);
+
+ console.log(`[renderPage] 渲染页面: 宽=${actualPageWidth}, 高=${actualPageHeight}`);
container.innerHTML = '';
+
+ // 直接设置为实际尺寸,用于切图
+ // 预览时通过外层 CSS 的 max-width 限制显示宽度,浏览器自动缩放
container.style.cssText = `
- width: ${pageWidth}px;
- min-height: ${pageHeight}px;
- max-height: ${pageHeight}px;
+ width: ${actualPageWidth}px;
+ height: ${actualPageHeight}px;
overflow: hidden;
box-sizing: border-box;
padding: 40px;
diff --git a/src/xiaohongshu/slice.ts b/src/xiaohongshu/slice.ts
index d45f145..39fd840 100644
--- a/src/xiaohongshu/slice.ts
+++ b/src/xiaohongshu/slice.ts
@@ -52,9 +52,11 @@ export async function sliceCurrentPage(
const originalWidth = pageElement.style.width;
const originalMaxWidth = pageElement.style.maxWidth;
const originalMinWidth = pageElement.style.minWidth;
+ const originalTransform = pageElement.style.transform;
try {
- // 临时设置为目标宽度
+ // 临时移除 transform 缩放,恢复实际尺寸用于切图
+ pageElement.style.transform = 'none';
pageElement.style.width = `${sliceImageWidth}px`;
pageElement.style.maxWidth = `${sliceImageWidth}px`;
pageElement.style.minWidth = `${sliceImageWidth}px`;
@@ -78,6 +80,7 @@ export async function sliceCurrentPage(
} finally {
// 恢复样式
+ pageElement.style.transform = originalTransform;
pageElement.style.width = originalWidth;
pageElement.style.maxWidth = originalMaxWidth;
pageElement.style.minWidth = originalMinWidth;
diff --git a/src/xiaohongshu/xhs-preview.ts b/src/xiaohongshu/xhs-preview.ts
index 075580f..9bc0c36 100644
--- a/src/xiaohongshu/xhs-preview.ts
+++ b/src/xiaohongshu/xhs-preview.ts
@@ -28,14 +28,14 @@ export class XiaohongshuPreview {
// UI 元素
topToolbar!: HTMLDivElement;
templateSelect!: HTMLSelectElement;
- themeSelect!: HTMLSelectElement;
- fontSelect!: HTMLSelectElement;
- fontSizeDisplay!: HTMLSpanElement;
+ fontSizeInput!: HTMLInputElement;
pageContainer!: HTMLDivElement;
bottomToolbar!: HTMLDivElement;
pageNavigation!: HTMLDivElement;
pageNumberDisplay!: HTMLSpanElement;
+ styleEl: HTMLStyleElement | null = null; // 主题样式注入节点
+ currentThemeClass: string = '';
// 分页数据
pages: PageInfo[] = [];
@@ -61,6 +61,14 @@ export class XiaohongshuPreview {
build(): void {
this.container.empty();
this.container.addClass('xhs-preview-container');
+ // 准备样式挂载节点
+ if (!this.styleEl) {
+ this.styleEl = document.createElement('style');
+ this.styleEl.setAttr('data-xhs-style', '');
+ }
+ if (!this.container.contains(this.styleEl)) {
+ this.container.appendChild(this.styleEl);
+ }
// 顶部工具栏
this.buildTopToolbar();
@@ -102,31 +110,7 @@ export class XiaohongshuPreview {
option.text = name;
});
- // 主题选择
- const themeLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
- themeLabel.innerText = '主题';
- this.themeSelect = this.topToolbar.createEl('select', { cls: 'xhs-select' });
- const themes = this.assetsManager.themes;
- themes.forEach(theme => {
- const option = this.themeSelect.createEl('option');
- option.value = theme.className;
- option.text = theme.name;
- });
- this.themeSelect.value = this.settings.defaultStyle;
- this.themeSelect.onchange = () => this.onThemeChanged();
-
- // 字体选择
- const fontLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
- fontLabel.innerText = '字体';
- this.fontSelect = this.topToolbar.createEl('select', { cls: 'xhs-select' });
- ['系统默认', '宋体', '黑体', '楷体', '仿宋'].forEach(name => {
- const option = this.fontSelect.createEl('option');
- option.value = name;
- option.text = name;
- });
- this.fontSelect.onchange = () => this.onFontChanged();
-
- // 字号控制
+ // 字号控制(可直接编辑)
const fontSizeLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
fontSizeLabel.innerText = '字号';
const fontSizeGroup = this.topToolbar.createDiv({ cls: 'font-size-group' });
@@ -134,7 +118,13 @@ export class XiaohongshuPreview {
const decreaseBtn = fontSizeGroup.createEl('button', { text: '−', cls: 'font-size-btn' });
decreaseBtn.onclick = () => this.changeFontSize(-1);
- this.fontSizeDisplay = fontSizeGroup.createEl('span', { text: '16', cls: 'font-size-display' });
+ this.fontSizeInput = fontSizeGroup.createEl('input', {
+ cls: 'font-size-input',
+ attr: { type: 'number', min: '12', max: '36', value: '16' }
+ });
+ this.fontSizeInput.style.width = '50px';
+ this.fontSizeInput.style.textAlign = 'center';
+ this.fontSizeInput.onchange = () => this.onFontSizeInputChanged();
const increaseBtn = fontSizeGroup.createEl('button', { text: '+', cls: 'font-size-btn' });
increaseBtn.onclick = () => this.changeFontSize(1);
@@ -188,6 +178,8 @@ export class XiaohongshuPreview {
new Notice(`分页完成:共 ${this.pages.length} 页`);
this.currentPageIndex = 0;
+ // 初次渲染时应用当前主题
+ this.applyThemeCSS();
this.renderCurrentPage();
} finally {
document.body.removeChild(tempContainer);
@@ -203,7 +195,15 @@ export class XiaohongshuPreview {
const page = this.pages[this.currentPageIndex];
this.pageContainer.empty();
- const pageElement = this.pageContainer.createDiv({ cls: 'xhs-page' });
+ // 重置滚动位置到顶部
+ this.pageContainer.scrollTop = 0;
+
+ // 创建包裹器,为缩放后的页面预留正确的布局空间
+ const wrapper = this.pageContainer.createDiv({ cls: 'xhs-page-wrapper' });
+
+ const classes = ['xhs-page'];
+ if (this.currentThemeClass) classes.push('note-to-mp');
+ const pageElement = wrapper.createDiv({ cls: classes.join(' ') });
renderPage(pageElement, page.content, this.settings);
// 应用字体设置
@@ -214,47 +214,33 @@ export class XiaohongshuPreview {
}
/**
- * 应用字体设置
+ * 应用字体设置(仅字号,字体从主题读取)
*/
private applyFontSettings(element: HTMLElement): void {
- const fontFamily = this.fontSelect.value;
- const fontSize = this.currentFontSize;
-
- let fontFamilyCSS = '';
- switch (fontFamily) {
- case '宋体': fontFamilyCSS = 'SimSun, serif'; break;
- case '黑体': fontFamilyCSS = 'SimHei, sans-serif'; break;
- case '楷体': fontFamilyCSS = 'KaiTi, serif'; break;
- case '仿宋': fontFamilyCSS = 'FangSong, serif'; break;
- default: fontFamilyCSS = 'system-ui, -apple-system, sans-serif';
+ element.style.fontSize = `${this.currentFontSize}px`;
+ }
+
+ /**
+ * 切换字号(± 按钮)
+ */
+ private async changeFontSize(delta: number): Promise