Compare commits

..

14 Commits

Author SHA1 Message Date
Jim Liu 宝玉 10e1c426a0 chore: release v1.27.0 2026-02-01 02:15:08 -06:00
Jim Liu 宝玉 dbab83ff82 feat(baoyu-markdown-to-html): add remark-cjk-friendly for CJK emphasis
Preprocess markdown with remark-cjk-friendly to properly handle
CJK emphasis markers before HTML conversion.
2026-02-01 02:14:12 -06:00
Jim Liu 宝玉 478e66a93a docs(baoyu-infographic): clarify AskUserQuestion usage
Emphasize combining multiple questions into single call.
2026-02-01 02:14:06 -06:00
Jim Liu 宝玉 3a5866eb4b refactor(baoyu-format-markdown): use remark-cjk-friendly for CJK emphasis
Replace custom CJK emphasis handling with remark-cjk-friendly library,
significantly simplifying the codebase.
2026-02-01 02:13:58 -06:00
Jim Liu 宝玉 c0162bb3af chore: release v1.26.1 2026-01-29 22:02:42 -06:00
Jim Liu 宝玉 e9342a16f8 fix(baoyu-xhs-images): remove notebook style, add backup rules for prompts and images 2026-01-29 22:01:47 -06:00
Jim Liu 宝玉 5cb86d8e51 feat(baoyu-slide-deck): add backup rules for source, prompt, and slide image files 2026-01-29 22:01:42 -06:00
Jim Liu 宝玉 66ed08f010 feat(baoyu-infographic): add backup rules for source, analysis, prompt, and image files 2026-01-29 22:01:22 -06:00
Jim Liu 宝玉 b179166d28 feat(baoyu-cover-image): add backup rules for source and prompt files 2026-01-29 22:01:17 -06:00
Jim Liu 宝玉 4805526649 feat(baoyu-comic): add backup rules for source, character sheet, prompts, and page images 2026-01-29 22:00:59 -06:00
Jim Liu 宝玉 89159bf86e feat(baoyu-article-illustrator): add backup rules for source, prompt, and image files 2026-01-29 22:00:37 -06:00
Jim Liu 宝玉 b45b5d2c6e chore: release v1.26.0 2026-01-29 20:59:45 -06:00
Jim Liu 宝玉 62866426c5 feat(baoyu-xhs-images): add notebook and study-notes styles with mindmap and quadrant layouts
- notebook: hand-drawn infographic style with watercolor rendering and Morandi palette
- study-notes: realistic handwritten photo style with blue pen, red annotations, yellow highlighter
- mindmap: center radial layout (4-8 branches)
- quadrant: four-quadrant / circular section layout
2026-01-29 20:59:24 -06:00
Jim Liu 宝玉 28a7db6129 chore: release v1.25.4 2026-01-29 17:17:58 -06:00
23 changed files with 2931 additions and 411 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
}, },
"metadata": { "metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency", "description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.25.3" "version": "1.27.0"
}, },
"plugins": [ "plugins": [
{ {
+22
View File
@@ -2,6 +2,28 @@
English | [中文](./CHANGELOG.zh.md) English | [中文](./CHANGELOG.zh.md)
## 1.26.1 - 2026-01-29
### Features
- `baoyu-article-illustrator`, `baoyu-comic`, `baoyu-cover-image`, `baoyu-infographic`, `baoyu-slide-deck`, `baoyu-xhs-images`: add backup rules for existing files—automatically renames source, prompt, and image files with timestamp suffix before overwriting.
### Fixes
- `baoyu-xhs-images`: remove `notebook` style (10 styles remaining).
## 1.26.0 - 2026-01-29
### Features
- `baoyu-xhs-images`: add `notebook` style (hand-drawn infographic with watercolor rendering and Morandi palette) and `study-notes` style (realistic handwritten photo aesthetic).
- `baoyu-xhs-images`: add `mindmap` (center radial) and `quadrant` (four-section grid) layouts.
## 1.25.4 - 2026-01-29
### Fixes
- `baoyu-markdown-to-html`: generate proper `<img>` tags with `data-local-path` attribute instead of text placeholders.
- `baoyu-post-to-wechat`: fix API publishing to read image paths from `data-local-path` attribute; fix title/cover extraction from corresponding `.md` frontmatter when publishing HTML files.
- `baoyu-post-to-wechat`: fix CLI argument parsing to handle unknown parameters gracefully; add `--summary` parameter support.
- `baoyu-post-to-wechat`: fix browser publishing to convert `<img>` tags back to text placeholders before paste.
## 1.25.3 - 2026-01-28 ## 1.25.3 - 2026-01-28
### Features ### Features
+22
View File
@@ -2,6 +2,28 @@
[English](./CHANGELOG.md) | 中文 [English](./CHANGELOG.md) | 中文
## 1.26.1 - 2026-01-29
### 新功能
- `baoyu-article-illustrator``baoyu-comic``baoyu-cover-image``baoyu-infographic``baoyu-slide-deck``baoyu-xhs-images`:新增文件备份规则,覆盖前自动将现有源文件、提示词和图片重命名为带时间戳后缀的备份文件。
### 修复
- `baoyu-xhs-images`:移除 `notebook` 风格(保留 10 种风格)。
## 1.26.0 - 2026-01-29
### 新功能
- `baoyu-xhs-images`:新增 `notebook` 风格(水彩渲染手绘信息图 + 莫兰迪配色)和 `study-notes` 风格(真实手写照片美学)。
- `baoyu-xhs-images`:新增 `mindmap`(中心发散式)和 `quadrant`(四象限)布局。
## 1.25.4 - 2026-01-29
### 修复
- `baoyu-markdown-to-html`:生成带 `data-local-path` 属性的 `<img>` 标签,而非纯文本占位符。
- `baoyu-post-to-wechat`:修复 API 发布时从 `data-local-path` 属性读取图片路径;修复发布 HTML 文件时从对应 `.md` 的 frontmatter 提取标题和封面图。
- `baoyu-post-to-wechat`:修复命令行参数解析,正确跳过未知参数;新增 `--summary` 参数支持。
- `baoyu-post-to-wechat`:修复浏览器发布模式,粘贴前将 `<img>` 标签转换回文本占位符。
## 1.25.3 - 2026-01-28 ## 1.25.3 - 2026-01-28
### 新功能 ### 新功能
+6 -1
View File
@@ -78,6 +78,8 @@ Progress:
| File path | Ask user (1.2) | → 1.2 | | File path | Ask user (1.2) | → 1.2 |
| Pasted content | `illustrations/{topic-slug}/` | → 1.4 | | Pasted content | `illustrations/{topic-slug}/` | → 1.4 |
**Backup rule for pasted content**: If `source.md` exists in target directory, rename to `source-backup-YYYYMMDD-HHMMSS.md` before saving.
**1.2 Determine Output Directory** (file path input only) **1.2 Determine Output Directory** (file path input only)
Check `default_output_dir` in preferences: Check `default_output_dir` in preferences:
@@ -236,6 +238,7 @@ image_count: 4
**5.1 Create Prompts** **5.1 Create Prompts**
Follow [references/prompt-construction.md](references/prompt-construction.md). Save to `prompts/illustration-{slug}.md`. Follow [references/prompt-construction.md](references/prompt-construction.md). Save to `prompts/illustration-{slug}.md`.
- **Backup rule**: If prompt file exists, rename to `prompts/illustration-{slug}-backup-YYYYMMDD-HHMMSS.md`
**5.2 Select Generation Skill** **5.2 Select Generation Skill**
@@ -247,7 +250,9 @@ Add: `Include a subtle watermark "[content]" at [position].`
**5.4 Generate** **5.4 Generate**
1. Generate sequentially 1. For each illustration:
- **Backup rule**: If image file exists, rename to `NN-{type}-{slug}-backup-YYYYMMDD-HHMMSS.png`
- Generate image
2. After each: "Generated X/N" 2. After each: "Generated X/N"
3. On failure: retry once, then log and continue 3. On failure: retry once, then log and continue
+5
View File
@@ -194,6 +194,7 @@ Input → Preferences → Analyze → [Check Existing?] → [Confirm: Style + Re
**Character reference is MANDATORY for visual consistency.** **Character reference is MANDATORY for visual consistency.**
**7.1 Generate character sheet first**: **7.1 Generate character sheet first**:
- **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
```bash ```bash
# Use Reference Sheet Prompt from characters/characters.md # Use Reference Sheet Prompt from characters/characters.md
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \ npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
@@ -214,6 +215,10 @@ Compress to reduce token usage when used as reference image:
| Supports `--ref` | Pass `characters/characters.png` with EVERY page | | Supports `--ref` | Pass `characters/characters.png` with EVERY page |
| No `--ref` support | Prepend character descriptions to EVERY prompt file | | No `--ref` support | Prepend character descriptions to EVERY prompt file |
**Backup rules for page generation**:
- If prompt file exists: rename to `prompts/NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.md`
- If image file exists: rename to `NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.png`
```bash ```bash
# Example: ALWAYS include --ref for consistency # Example: ALWAYS include --ref for consistency
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \ npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
+9 -5
View File
@@ -84,6 +84,7 @@ Read source content, save it if needed, and perform deep analysis.
1. **Save source content** (if not already a file): 1. **Save source content** (if not already a file):
- If user provides a file path: use as-is - If user provides a file path: use as-is
- If user pastes content: save to `source.md` in target directory - If user pastes content: save to `source.md` in target directory
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
2. Read source content 2. Read source content
3. **Deep analysis** following `analysis-framework.md`: 3. **Deep analysis** following `analysis-framework.md`:
- Target audience identification - Target audience identification
@@ -329,6 +330,7 @@ Create image generation prompts for all pages.
1. Create prompt following art style + tone guidelines 1. Create prompt following art style + tone guidelines
2. Include character visual descriptions for consistency 2. Include character visual descriptions for consistency
3. Save to `prompts/NN-{cover|page}-[slug].md` 3. Save to `prompts/NN-{cover|page}-[slug].md`
- **Backup rule**: If prompt file exists, rename to `prompts/NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.md`
**Prompt File Format**: **Prompt File Format**:
```markdown ```markdown
@@ -405,8 +407,9 @@ With confirmed prompts from Step 5/6:
### 7.1 Generate Character Reference Sheet (first) ### 7.1 Generate Character Reference Sheet (first)
1. Use Reference Sheet Prompt from `characters/characters.md` 1. Use Reference Sheet Prompt from `characters/characters.md`
2. Generate → `characters/characters.png` 2. **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
3. This ensures visual consistency for all subsequent pages 3. Generate → `characters/characters.png`
4. This ensures visual consistency for all subsequent pages
### 7.2 Generate Comic Pages ### 7.2 Generate Comic Pages
@@ -453,9 +456,10 @@ When skill does NOT support reference images, create combined prompt files:
**For each page (cover + pages)**: **For each page (cover + pages)**:
1. Read prompt from `prompts/NN-{cover|page}-[slug].md` 1. Read prompt from `prompts/NN-{cover|page}-[slug].md`
2. Generate image using Strategy A or B (based on skill capability) 2. **Backup rule**: If image file exists, rename to `NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.png`
3. Save to `NN-{cover|page}-[slug].png` 3. Generate image using Strategy A or B (based on skill capability)
4. Report progress after each generation: "Generated X/N: [page title]" 4. Save to `NN-{cover|page}-[slug].png`
5. Report progress after each generation: "Generated X/N: [page title]"
**Session Management**: **Session Management**:
If image generation skill supports `--sessionId`: If image generation skill supports `--sessionId`:
+3
View File
@@ -209,6 +209,7 @@ Schema: [references/config/preferences-schema.md](references/config/preferences-
### Step 1: Analyze Content ### Step 1: Analyze Content
1. **Save source content** (if pasted, save to `source.md` in target directory; if file path, use as-is) 1. **Save source content** (if pasted, save to `source.md` in target directory; if file path, use as-is)
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
2. **Content analysis**: Extract topic, core message, tone, keywords; identify visual metaphors; detect content type 2. **Content analysis**: Extract topic, core message, tone, keywords; identify visual metaphors; detect content type
3. **Language detection**: Detect source language, note user's input language, compare with EXTEND.md preference 3. **Language detection**: Detect source language, note user's input language, compare with EXTEND.md preference
4. **Determine output directory** per File Structure rules. If no `default_output_dir` preference + file path input, include in Step 2 Q4 4. **Determine output directory** per File Structure rules. If no `default_output_dir` preference + file path input, include in Step 2 Q4
@@ -226,6 +227,8 @@ Validate all 5 dimensions + aspect ratio. Full confirmation flow: [references/wo
### Step 3: Create Prompt ### Step 3: Create Prompt
**Backup rule**: If `prompts/cover.md` exists, rename to `prompts/cover-backup-YYYYMMDD-HHMMSS.md`
Save to `prompts/cover.md`. Full template: [references/workflow/prompt-template.md](references/workflow/prompt-template.md) Save to `prompts/cover.md`. Full template: [references/workflow/prompt-template.md](references/workflow/prompt-template.md)
### Step 4: Generate Image ### Step 4: Generate Image
+1 -2
View File
@@ -13,8 +13,7 @@ Scripts in `scripts/` subdirectory. Replace `${SKILL_DIR}` with this SKILL.md's
| Script | Purpose | | Script | Purpose |
|--------|---------| |--------|---------|
| `scripts/main.ts` | Main entry point with CLI options | | `scripts/main.ts` | Main entry point with CLI options (uses remark-cjk-friendly for CJK emphasis) |
| `scripts/cjk-emphasis.ts` | Fix CJK emphasis/bold punctuation issues |
| `scripts/quotes.ts` | Replace ASCII quotes with fullwidth quotes | | `scripts/quotes.ts` | Replace ASCII quotes with fullwidth quotes |
| `scripts/autocorrect.ts` | Add CJK/English spacing via autocorrect | | `scripts/autocorrect.ts` | Add CJK/English spacing via autocorrect |
@@ -1,314 +0,0 @@
const CJK_PUNCT_COMMON = "。.,、?!:;";
const CJK_OPENING_PUNCT = "(〔〖〘〚「『〈《【\u201C\u2018";
const CJK_CLOSING_PUNCT = ")〕〗〙〛」』〉》】\u201D\u2019" + CJK_PUNCT_COMMON;
const CJK_SCRIPTS =
"\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}";
export const CJK_CLOSING_PUNCT_RE = new RegExp(`[${CJK_CLOSING_PUNCT}]`);
export const CJK_OPENING_PUNCT_RE = new RegExp(`^[${CJK_OPENING_PUNCT}]`);
export const CJK_CHAR_RE = new RegExp(`[${CJK_SCRIPTS}]`, "u");
const PUNCT_OR_SYMBOL_RE = /[\p{P}\p{S}]/u;
const WORD_CHAR_RE = /[\p{L}\p{N}]/u;
const CJK_PUNCT_PAIRS: Record<string, string> = {
"“": "”",
"": "",
"": "",
"": "",
"〖": "〗",
"〘": "〙",
"〚": "〛",
"「": "」",
"『": "』",
"〈": "〉",
"《": "》",
"【": "】",
};
function findInlineCodeRanges(text: string): Array<[number, number]> {
const ranges: Array<[number, number]> = [];
let i = 0;
while (i < text.length) {
if (text[i] !== "`") {
i += 1;
continue;
}
let run = 1;
while (i + run < text.length && text[i + run] === "`") {
run += 1;
}
const start = i;
let j = i + run;
let found = false;
while (j < text.length) {
if (text[j] !== "`") {
j += 1;
continue;
}
let closeRun = 1;
while (j + closeRun < text.length && text[j + closeRun] === "`") {
closeRun += 1;
}
if (closeRun === run) {
ranges.push([start, j + closeRun - 1]);
i = j + closeRun;
found = true;
break;
}
j += closeRun;
}
if (!found) {
i = start + run;
}
}
return ranges;
}
function isEscaped(text: string, pos: number): boolean {
let count = 0;
for (let i = pos - 1; i >= 0 && text[i] === "\\"; i -= 1) {
count += 1;
}
return count % 2 === 1;
}
function isWhitespaceChar(ch: string | undefined): boolean {
return !ch || /\s/u.test(ch);
}
function isPunctuationOrSymbol(ch: string | undefined): boolean {
return !!ch && PUNCT_OR_SYMBOL_RE.test(ch);
}
function isMatchingCjkPunct(open: string, close: string): boolean {
return CJK_PUNCT_PAIRS[open] === close;
}
function mapNonCodeSegments(
block: string,
mapper: (segment: string) => string
): string {
const codeRanges = findInlineCodeRanges(block);
if (codeRanges.length === 0) {
return mapper(block);
}
let result = "";
let lastIndex = 0;
for (const [start, end] of codeRanges) {
if (start > lastIndex) {
result += mapper(block.slice(lastIndex, start));
}
result += block.slice(start, end + 1);
lastIndex = end + 1;
}
if (lastIndex < block.length) {
result += mapper(block.slice(lastIndex));
}
return result;
}
function moveCjkPunctuationOutsideEmphasis(block: string): string {
return mapNonCodeSegments(block, (segment) => {
const delimiterPositions: number[] = [];
let cursor = 0;
while (cursor < segment.length - 1) {
if (
segment[cursor] === "*" &&
segment[cursor + 1] === "*" &&
segment[cursor - 1] !== "*" &&
segment[cursor + 2] !== "*" &&
!isEscaped(segment, cursor)
) {
delimiterPositions.push(cursor);
cursor += 2;
continue;
}
cursor += 1;
}
if (delimiterPositions.length < 2) return segment;
const stack: number[] = [];
const pairs: Array<{ open: number; close: number }> = [];
for (const pos of delimiterPositions) {
if (stack.length === 0) {
stack.push(pos);
} else {
const open = stack.pop() as number;
pairs.push({ open, close: pos });
}
}
const skip = new Set<number>();
const insertBefore = new Map<number, string>();
for (const pair of pairs) {
const openPunctPos = pair.open + 2;
const closePunctPos = pair.close - 1;
if (openPunctPos >= closePunctPos) continue;
const openPunct = segment[openPunctPos];
const closePunct = segment[closePunctPos];
if (!openPunct || !closePunct) continue;
if (!CJK_OPENING_PUNCT_RE.test(openPunct)) continue;
if (!CJK_CLOSING_PUNCT_RE.test(closePunct)) continue;
if (!isMatchingCjkPunct(openPunct, closePunct)) continue;
if (openPunctPos + 1 >= closePunctPos) continue;
const inner = segment.slice(openPunctPos + 1, closePunctPos);
if (inner.length === 0) continue;
skip.add(openPunctPos);
skip.add(closePunctPos);
insertBefore.set(pair.open, (insertBefore.get(pair.open) ?? "") + openPunct);
const afterClose = pair.close + 2;
insertBefore.set(
afterClose,
(insertBefore.get(afterClose) ?? "") + closePunct
);
}
if (skip.size === 0) return segment;
let result = "";
for (let idx = 0; idx < segment.length; idx += 1) {
const insert = insertBefore.get(idx);
if (insert) {
result += insert;
}
if (skip.has(idx)) {
continue;
}
result += segment[idx];
}
const tailInsert = insertBefore.get(segment.length);
if (tailInsert) {
result += tailInsert;
}
return result;
});
}
function fixCjkEmphasisSpacingInBlock(block: string): string {
const normalized = moveCjkPunctuationOutsideEmphasis(block);
const codeRanges = findInlineCodeRanges(normalized);
let rangeIndex = 0;
const delimiters: Array<{
pos: number;
canOpen: boolean;
canClose: boolean;
}> = [];
let cursor = 0;
while (cursor < normalized.length - 1) {
if (rangeIndex < codeRanges.length && cursor >= codeRanges[rangeIndex][0]) {
if (cursor <= codeRanges[rangeIndex][1]) {
cursor = codeRanges[rangeIndex][1] + 1;
continue;
}
rangeIndex += 1;
continue;
}
if (
normalized[cursor] === "*" &&
normalized[cursor + 1] === "*" &&
normalized[cursor - 1] !== "*" &&
normalized[cursor + 2] !== "*" &&
!isEscaped(normalized, cursor)
) {
const before = normalized[cursor - 1];
const after = normalized[cursor + 2];
const beforeIsSpace = isWhitespaceChar(before);
const afterIsSpace = isWhitespaceChar(after);
const beforeIsPunct = isPunctuationOrSymbol(before);
const afterIsPunct = isPunctuationOrSymbol(after);
const leftFlanking =
!afterIsSpace && (!afterIsPunct || beforeIsSpace || beforeIsPunct);
const rightFlanking =
!beforeIsSpace && (!beforeIsPunct || afterIsSpace || afterIsPunct);
const cjkPunctBefore = !!before && CJK_CLOSING_PUNCT_RE.test(before);
const wordAfter = !!after && WORD_CHAR_RE.test(after);
delimiters.push({
pos: cursor,
canOpen: leftFlanking,
canClose: rightFlanking || (cjkPunctBefore && wordAfter),
});
cursor += 2;
continue;
}
cursor += 1;
}
const stack: Array<{ pos: number }> = [];
const pairs: Array<{ open: number; close: number }> = [];
for (const delimiter of delimiters) {
if (delimiter.canClose) {
let openerIndex = -1;
for (let j = stack.length - 1; j >= 0; j -= 1) {
openerIndex = j;
break;
}
if (openerIndex !== -1) {
const opener = stack.splice(openerIndex, 1)[0];
pairs.push({ open: opener.pos, close: delimiter.pos });
}
}
if (delimiter.canOpen) {
stack.push({ pos: delimiter.pos });
}
}
if (pairs.length === 0) return normalized;
const insertPositions = new Set<number>();
for (const pair of pairs) {
const insideLast = normalized[pair.close - 1];
const afterClose = normalized[pair.close + 2];
if (!afterClose) continue;
if (
CJK_CLOSING_PUNCT_RE.test(insideLast) &&
WORD_CHAR_RE.test(afterClose)
) {
insertPositions.add(pair.close + 2);
}
}
if (insertPositions.size === 0) return normalized;
let result = "";
for (let idx = 0; idx < normalized.length; idx += 1) {
if (insertPositions.has(idx)) {
result += " ";
}
result += normalized[idx];
}
if (insertPositions.has(normalized.length)) {
result += " ";
}
return result;
}
export function fixCjkEmphasisSpacing(content: string): string {
const parts = content.split(/(^```[\s\S]*?^```|^~~~[\s\S]*?^~~~)/m);
return parts
.map((part, i) => {
if (i % 2 === 1) return part;
const blocks = part.split(/(\n\s*\n+)/);
return blocks
.map((block, index) => {
if (index % 2 === 1) return block;
return fixCjkEmphasisSpacingInBlock(block);
})
.join("");
})
.join("");
}
+10 -55
View File
@@ -1,17 +1,12 @@
import { readFileSync, writeFileSync } from "fs"; import { readFileSync, writeFileSync } from "fs";
import { unified } from "unified"; import { unified } from "unified";
import remarkParse from "remark-parse"; import remarkParse from "remark-parse";
import remarkCjkFriendly from "remark-cjk-friendly";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import remarkFrontmatter from "remark-frontmatter"; import remarkFrontmatter from "remark-frontmatter";
import remarkStringify from "remark-stringify"; import remarkStringify from "remark-stringify";
import { visit } from "unist-util-visit"; import { visit } from "unist-util-visit";
import YAML from "yaml"; import YAML from "yaml";
import {
fixCjkEmphasisSpacing,
CJK_CLOSING_PUNCT_RE,
CJK_OPENING_PUNCT_RE,
CJK_CHAR_RE,
} from "./cjk-emphasis";
import { replaceQuotes } from "./quotes"; import { replaceQuotes } from "./quotes";
import { applyAutocorrect } from "./autocorrect"; import { applyAutocorrect } from "./autocorrect";
@@ -36,6 +31,12 @@ const DEFAULT_OPTIONS: Required<FormatOptions> = {
emphasis: true, emphasis: true,
}; };
function decodeHtmlEntities(text: string): string {
return text.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex) =>
String.fromCodePoint(parseInt(hex, 16))
);
}
function formatFrontmatter(value: string): string | null { function formatFrontmatter(value: string): string | null {
try { try {
const doc = YAML.parseDocument(value); const doc = YAML.parseDocument(value);
@@ -49,12 +50,9 @@ function formatMarkdownContent(
content: string, content: string,
options: Required<FormatOptions> options: Required<FormatOptions>
): string { ): string {
if (options.emphasis) {
content = fixCjkEmphasisSpacing(content);
}
const processor = unified() const processor = unified()
.use(remarkParse) .use(remarkParse)
.use(options.emphasis ? remarkCjkFriendly : [])
.use(remarkGfm) .use(remarkGfm)
.use(remarkFrontmatter, ["yaml"]) .use(remarkFrontmatter, ["yaml"])
.use(remarkStringify, { .use(remarkStringify, {
@@ -63,7 +61,7 @@ function formatMarkdownContent(
const tree = processor.parse(content); const tree = processor.parse(content);
visit(tree, (node, _index, parent) => { visit(tree, (node) => {
if (node.type === "text" && options.quotes) { if (node.type === "text" && options.quotes) {
const textNode = node as { value: string }; const textNode = node as { value: string };
textNode.value = replaceQuotes(textNode.value); textNode.value = replaceQuotes(textNode.value);
@@ -77,54 +75,11 @@ function formatMarkdownContent(
} }
return; return;
} }
if (
options.emphasis &&
(node.type === "strong" ||
node.type === "emphasis" ||
node.type === "delete") &&
parent
) {
const siblings = (parent as { children: typeof node[] }).children;
const idx = siblings.indexOf(node);
const children = (node as { children: typeof node[] }).children;
if (!children || children.length === 0) return;
const lastChild = children[children.length - 1];
if (lastChild.type === "text") {
const lastText = (lastChild as { value: string }).value;
if (
CJK_CLOSING_PUNCT_RE.test(lastText.slice(-1)) &&
idx + 1 < siblings.length
) {
const nextSib = siblings[idx + 1];
if (nextSib.type === "text") {
const nextText = (nextSib as { value: string }).value;
if (CJK_CHAR_RE.test(nextText.charAt(0))) {
(nextSib as { value: string }).value = " " + nextText;
}
}
}
}
const firstChild = children[0];
if (firstChild.type === "text") {
const firstText = (firstChild as { value: string }).value;
if (CJK_OPENING_PUNCT_RE.test(firstText) && idx > 0) {
const prevSib = siblings[idx - 1];
if (prevSib.type === "text") {
const prevText = (prevSib as { value: string }).value;
if (CJK_CHAR_RE.test(prevText.charAt(prevText.length - 1))) {
(prevSib as { value: string }).value = prevText + " ";
}
}
}
}
}
}); });
let result = processor.stringify(tree); let result = processor.stringify(tree);
if (options.emphasis) { if (options.emphasis) {
result = fixCjkEmphasisSpacing(result); result = decodeHtmlEntities(result);
} }
return result; return result;
} }
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"remark-cjk-friendly": "^1.1.0",
"remark-frontmatter": "^5.0.0", "remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
+17 -6
View File
@@ -153,10 +153,12 @@ Schema: `references/config/preferences-schema.md`
**1.2 Analyze Content → `analysis.md`** **1.2 Analyze Content → `analysis.md`**
1. Save source content (file path or paste → `source.md`) 1. Save source content (file path or paste → `source.md`)
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
2. Analyze: topic, data type, complexity, tone, audience 2. Analyze: topic, data type, complexity, tone, audience
3. Detect source language and user language 3. Detect source language and user language
4. Extract design instructions from user input 4. Extract design instructions from user input
5. Save analysis 5. Save analysis
- **Backup rule**: If `analysis.md` exists, rename to `analysis-backup-YYYYMMDD-HHMMSS.md`
See `references/analysis-framework.md` for detailed format. See `references/analysis-framework.md` for detailed format.
@@ -182,13 +184,20 @@ Recommend 3-5 layout×style combinations based on:
### Step 4: Confirm Options ### Step 4: Confirm Options
Present all options in single confirmation: Use **single AskUserQuestion call** with multiple questions to confirm all options together:
1. **Combination** (always): 3+ options with rationale
2. **Aspect** (always): landscape/portrait/square | Question | When | Options |
3. **Language** (only if source ≠ user language): which language for text |----------|------|---------|
| **Combination** | Always | 3+ layout×style combos with rationale |
| **Aspect** | Always | landscape (16:9), portrait (9:16), square (1:1) |
| **Language** | Only if source ≠ user language | Language for text content |
**Important**: Do NOT split into separate AskUserQuestion calls. Combine all applicable questions into one call.
### Step 5: Generate Prompt → `prompts/infographic.md` ### Step 5: Generate Prompt → `prompts/infographic.md`
**Backup rule**: If `prompts/infographic.md` exists, rename to `prompts/infographic-backup-YYYYMMDD-HHMMSS.md`
Combine: Combine:
1. Layout definition from `references/layouts/<layout>.md` 1. Layout definition from `references/layouts/<layout>.md`
2. Style definition from `references/styles/<style>.md` 2. Style definition from `references/styles/<style>.md`
@@ -199,8 +208,10 @@ Combine:
### Step 6: Generate Image ### Step 6: Generate Image
1. Select available image generation skill (ask user if multiple) 1. Select available image generation skill (ask user if multiple)
2. Call with prompt file and output path 2. **Check for existing file**: Before generating, check if `infographic.png` exists
3. On failure, auto-retry once - If exists: Rename to `infographic-backup-YYYYMMDD-HHMMSS.png`
3. Call with prompt file and output path
4. On failure, auto-retry once
### Step 7: Output Summary ### Step 7: Output Summary
@@ -214,7 +214,6 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
} }
fs.copyFileSync(tempHtmlPath, finalHtmlPath); fs.copyFileSync(tempHtmlPath, finalHtmlPath);
console.error(`[markdown-to-html] HTML saved to: ${finalHtmlPath}`);
const contentImages: ImageInfo[] = []; const contentImages: ImageInfo[] = [];
for (const img of images) { for (const img of images) {
@@ -226,6 +225,15 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
}); });
} }
let htmlContent = fs.readFileSync(finalHtmlPath, 'utf-8');
for (const img of contentImages) {
const imgTag = `<img src="${img.placeholder}" data-local-path="${img.localPath}" style="display: block; width: 100%; margin: 1.5em auto;">`;
htmlContent = htmlContent.replace(img.placeholder, imgTag);
}
fs.writeFileSync(finalHtmlPath, htmlContent, 'utf-8');
console.error(`[markdown-to-html] HTML saved to: ${finalHtmlPath}`);
return { return {
title, title,
author, author,
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
{
"dependencies": {
"fflate": "^0.8.2",
"front-matter": "^4.0.2",
"highlight.js": "^11.11.1",
"juice": "^11.0.1",
"marked": "^15.0.6",
"reading-time": "^1.5.0",
"remark-cjk-friendly": "^1.1.0",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"unified": "^11.0.5"
}
}
@@ -7,6 +7,10 @@ import frontMatter from "front-matter";
import hljs from "highlight.js/lib/core"; import hljs from "highlight.js/lib/core";
import { marked, type RendererObject, type Tokens } from "marked"; import { marked, type RendererObject, type Tokens } from "marked";
import readingTime, { type ReadTimeResults } from "reading-time"; import readingTime, { type ReadTimeResults } from "reading-time";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkCjkFriendly from "remark-cjk-friendly";
import remarkStringify from "remark-stringify";
import { import {
markedAlert, markedAlert,
@@ -552,12 +556,47 @@ interface CliOptions {
keepTitle: boolean; keepTitle: boolean;
} }
function preprocessCjkEmphasis(markdown: string): string {
const processor = unified()
.use(remarkParse)
.use(remarkCjkFriendly);
const tree = processor.parse(markdown);
const visit = (node: any, parent?: any, index?: number) => {
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
visit(node.children[i], node, i);
}
}
if (node.type === "strong" && parent && typeof index === "number") {
const text = extractText(node);
parent.children[index] = { type: "html", value: `<strong>${text}</strong>` };
}
if (node.type === "emphasis" && parent && typeof index === "number") {
const text = extractText(node);
parent.children[index] = { type: "html", value: `<em>${text}</em>` };
}
};
const extractText = (node: any): string => {
if (node.type === "text") return node.value;
if (node.children) return node.children.map(extractText).join("");
return "";
};
visit(tree);
const stringify = unified().use(remarkStringify);
let result = stringify.stringify(tree);
result = result.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex) =>
String.fromCodePoint(parseInt(hex, 16))
);
return result;
}
function renderMarkdown(raw: string, renderer: RendererAPI): { function renderMarkdown(raw: string, renderer: RendererAPI): {
html: string; html: string;
readingTime: ReadTimeResults; readingTime: ReadTimeResults;
} { } {
const preprocessed = preprocessCjkEmphasis(raw);
const { markdownContent, readingTime: readingTimeResult } = const { markdownContent, readingTime: readingTimeResult } =
renderer.parseFrontMatterAndContent(raw); renderer.parseFrontMatterAndContent(preprocessed);
const html = marked.parse(markdownContent) as string; const html = marked.parse(markdownContent) as string;
@@ -177,7 +177,7 @@ async function uploadImagesInHtml(
accessToken: string, accessToken: string,
baseDir: string baseDir: string
): Promise<{ html: string; firstMediaId: string }> { ): Promise<{ html: string; firstMediaId: string }> {
const imgRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi; const imgRegex = /<img[^>]*\ssrc=["']([^"']+)["'][^>]*>/gi;
const matches = [...html.matchAll(imgRegex)]; const matches = [...html.matchAll(imgRegex)];
if (matches.length === 0) { if (matches.length === 0) {
@@ -188,7 +188,7 @@ async function uploadImagesInHtml(
let updatedHtml = html; let updatedHtml = html;
for (const match of matches) { for (const match of matches) {
const [, src] = match; const [fullTag, src] = match;
if (!src) continue; if (!src) continue;
if (src.startsWith("https://mmbiz.qpic.cn")) { if (src.startsWith("https://mmbiz.qpic.cn")) {
@@ -198,15 +198,21 @@ async function uploadImagesInHtml(
continue; continue;
} }
console.error(`[wechat-api] Uploading image: ${src}`); const localPathMatch = fullTag.match(/data-local-path=["']([^"']+)["']/);
const imagePath = localPathMatch ? localPathMatch[1]! : src;
console.error(`[wechat-api] Uploading image: ${imagePath}`);
try { try {
const resp = await uploadImage(src, accessToken, baseDir); const resp = await uploadImage(imagePath, accessToken, baseDir);
updatedHtml = updatedHtml.replace(src, resp.url); const newTag = fullTag
.replace(/\ssrc=["'][^"']+["']/, ` src="${resp.url}"`)
.replace(/\sdata-local-path=["'][^"']+["']/, "");
updatedHtml = updatedHtml.replace(fullTag, newTag);
if (!firstMediaId) { if (!firstMediaId) {
firstMediaId = resp.media_id; firstMediaId = resp.media_id;
} }
} catch (err) { } catch (err) {
console.error(`[wechat-api] Failed to upload ${src}:`, err); console.error(`[wechat-api] Failed to upload ${imagePath}:`, err);
} }
} }
@@ -310,6 +316,7 @@ Arguments:
Options: Options:
--title <title> Override title --title <title> Override title
--summary <text> Article summary (for future use)
--theme <name> Theme name for markdown (default, grace, simple). Default: default --theme <name> Theme name for markdown (default, grace, simple). Default: default
--cover <path> Cover image path (local or URL) --cover <path> Cover image path (local or URL)
--dry-run Parse and render only, don't publish --dry-run Parse and render only, don't publish
@@ -337,6 +344,7 @@ interface CliArgs {
filePath: string; filePath: string;
isHtml: boolean; isHtml: boolean;
title?: string; title?: string;
summary?: string;
theme: string; theme: string;
cover?: string; cover?: string;
dryRun: boolean; dryRun: boolean;
@@ -358,12 +366,16 @@ function parseArgs(argv: string[]): CliArgs {
const arg = argv[i]!; const arg = argv[i]!;
if (arg === "--title" && argv[i + 1]) { if (arg === "--title" && argv[i + 1]) {
args.title = argv[++i]; args.title = argv[++i];
} else if (arg === "--summary" && argv[i + 1]) {
args.summary = argv[++i];
} else if (arg === "--theme" && argv[i + 1]) { } else if (arg === "--theme" && argv[i + 1]) {
args.theme = argv[++i]!; args.theme = argv[++i]!;
} else if (arg === "--cover" && argv[i + 1]) { } else if (arg === "--cover" && argv[i + 1]) {
args.cover = argv[++i]; args.cover = argv[++i];
} else if (arg === "--dry-run") { } else if (arg === "--dry-run") {
args.dryRun = true; args.dryRun = true;
} else if (arg.startsWith("--") && argv[i + 1] && !argv[i + 1]!.startsWith("-")) {
i++;
} else if (!arg.startsWith("-")) { } else if (!arg.startsWith("-")) {
args.filePath = arg; args.filePath = arg;
} }
@@ -405,6 +417,15 @@ async function main(): Promise<void> {
if (args.isHtml) { if (args.isHtml) {
htmlPath = filePath; htmlPath = filePath;
htmlContent = extractHtmlContent(htmlPath); htmlContent = extractHtmlContent(htmlPath);
const mdPath = filePath.replace(/\.html$/i, ".md");
if (fs.existsSync(mdPath)) {
const mdContent = fs.readFileSync(mdPath, "utf-8");
const parsed = parseFrontmatter(mdContent);
frontmatter = parsed.frontmatter;
if (!title && frontmatter.title) {
title = frontmatter.title;
}
}
if (!title) { if (!title) {
title = extractHtmlTitle(fs.readFileSync(htmlPath, "utf-8")); title = extractHtmlTitle(fs.readFileSync(htmlPath, "utf-8"));
} }
@@ -115,7 +115,7 @@ async function sendPaste(cdp?: CdpConnection, sessionId?: string): Promise<void>
} }
} }
async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string): Promise<void> { async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string, contentImages: ImageInfo[] = []): Promise<void> {
const absolutePath = path.isAbsolute(htmlFilePath) ? htmlFilePath : path.resolve(process.cwd(), htmlFilePath); const absolutePath = path.isAbsolute(htmlFilePath) ? htmlFilePath : path.resolve(process.cwd(), htmlFilePath);
const fileUrl = `file://${absolutePath}`; const fileUrl = `file://${absolutePath}`;
@@ -128,6 +128,28 @@ async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string): Pr
await cdp.send('Runtime.enable', {}, { sessionId }); await cdp.send('Runtime.enable', {}, { sessionId });
await sleep(2000); await sleep(2000);
if (contentImages.length > 0) {
console.log('[wechat] Replacing img tags with placeholders for browser paste...');
const replacements = contentImages.map(img => ({ placeholder: img.placeholder, localPath: img.localPath }));
await cdp.send<{ result: { value: unknown } }>('Runtime.evaluate', {
expression: `
(function() {
const replacements = ${JSON.stringify(replacements)};
for (const r of replacements) {
const imgs = document.querySelectorAll('img[src="' + r.placeholder + '"], img[data-local-path="' + r.localPath + '"]');
for (const img of imgs) {
const text = document.createTextNode(r.placeholder);
img.parentNode.replaceChild(text, img);
}
}
return true;
})()
`,
returnByValue: true,
}, { sessionId });
await sleep(500);
}
console.log('[wechat] Selecting #output content...'); console.log('[wechat] Selecting #output content...');
await cdp.send<{ result: { value: unknown } }>('Runtime.evaluate', { await cdp.send<{ result: { value: unknown } }>('Runtime.evaluate', {
expression: ` expression: `
@@ -176,7 +198,7 @@ async function parseMarkdownWithPlaceholders(markdownPath: string, theme?: strin
return JSON.parse(output); return JSON.parse(output);
} }
function parseHtmlMeta(htmlPath: string): { title: string; author: string; summary: string } { function parseHtmlMeta(htmlPath: string): { title: string; author: string; summary: string; contentImages: ImageInfo[] } {
const content = fs.readFileSync(htmlPath, 'utf-8'); const content = fs.readFileSync(htmlPath, 'utf-8');
let title = ''; let title = '';
@@ -203,7 +225,45 @@ function parseHtmlMeta(htmlPath: string): { title: string; author: string; summa
} }
} }
return { title, author, summary }; const mdPath = htmlPath.replace(/\.html$/i, '.md');
if (fs.existsSync(mdPath)) {
const mdContent = fs.readFileSync(mdPath, 'utf-8');
const fmMatch = mdContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (fmMatch) {
const lines = fmMatch[1]!.split('\n');
for (const line of lines) {
const colonIdx = line.indexOf(':');
if (colonIdx > 0) {
const key = line.slice(0, colonIdx).trim();
let value = line.slice(colonIdx + 1).trim();
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
if (key === 'title' && !title) title = value;
if (key === 'author' && !author) author = value;
if ((key === 'description' || key === 'summary') && !summary) summary = value;
}
}
}
}
const contentImages: ImageInfo[] = [];
const imgRegex = /<img[^>]*\ssrc=["']([^"']+)["'][^>]*>/gi;
const matches = [...content.matchAll(imgRegex)];
for (const match of matches) {
const [fullTag, src] = match;
if (!src || src.startsWith('http')) continue;
const localPathMatch = fullTag.match(/data-local-path=["']([^"']+)["']/);
if (localPathMatch) {
contentImages.push({
placeholder: src,
localPath: localPathMatch[1]!,
originalPath: src,
});
}
}
return { title, author, summary, contentImages };
} }
async function selectAndReplacePlaceholder(session: ChromeSession, placeholder: string): Promise<boolean> { async function selectAndReplacePlaceholder(session: ChromeSession, placeholder: string): Promise<boolean> {
@@ -273,9 +333,13 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
effectiveAuthor = effectiveAuthor || meta.author; effectiveAuthor = effectiveAuthor || meta.author;
effectiveSummary = effectiveSummary || meta.summary; effectiveSummary = effectiveSummary || meta.summary;
effectiveHtmlFile = htmlFile; effectiveHtmlFile = htmlFile;
if (meta.contentImages.length > 0) {
contentImages = meta.contentImages;
}
console.log(`[wechat] Title: ${effectiveTitle || '(empty)'}`); console.log(`[wechat] Title: ${effectiveTitle || '(empty)'}`);
console.log(`[wechat] Author: ${effectiveAuthor || '(empty)'}`); console.log(`[wechat] Author: ${effectiveAuthor || '(empty)'}`);
console.log(`[wechat] Summary: ${effectiveSummary || '(empty)'}`); console.log(`[wechat] Summary: ${effectiveSummary || '(empty)'}`);
console.log(`[wechat] Found ${contentImages.length} images to insert`);
} }
if (effectiveTitle && effectiveTitle.length > 64) throw new Error(`Title too long: ${effectiveTitle.length} chars (max 64)`); if (effectiveTitle && effectiveTitle.length > 64) throw new Error(`Title too long: ${effectiveTitle.length} chars (max 64)`);
@@ -416,7 +480,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
if (effectiveHtmlFile && fs.existsSync(effectiveHtmlFile)) { if (effectiveHtmlFile && fs.existsSync(effectiveHtmlFile)) {
console.log(`[wechat] Copying HTML content from: ${effectiveHtmlFile}`); console.log(`[wechat] Copying HTML content from: ${effectiveHtmlFile}`);
await copyHtmlFromBrowser(cdp, effectiveHtmlFile); await copyHtmlFromBrowser(cdp, effectiveHtmlFile, contentImages);
await sleep(500); await sleep(500);
console.log('[wechat] Pasting into editor...'); console.log('[wechat] Pasting into editor...');
await pasteFromClipboardInEditor(session); await pasteFromClipboardInEditor(session);
+5 -1
View File
@@ -226,6 +226,7 @@ Schema: `references/config/preferences-schema.md`
**1.2 Analyze Content** **1.2 Analyze Content**
1. Save source content (if pasted, save as `source.md`) 1. Save source content (if pasted, save as `source.md`)
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
2. Follow `references/analysis-framework.md` for content analysis 2. Follow `references/analysis-framework.md` for content analysis
3. Analyze content signals for style recommendations 3. Analyze content signals for style recommendations
4. Detect source language 4. Detect source language
@@ -483,6 +484,7 @@ options:
- Add slide-specific content - Add slide-specific content
- If `Layout:` specified, include layout guidance from `references/layouts.md` - If `Layout:` specified, include layout guidance from `references/layouts.md`
3. Save to `prompts/` directory 3. Save to `prompts/` directory
- **Backup rule**: If prompt file exists, rename to `prompts/NN-slide-{slug}-backup-YYYYMMDD-HHMMSS.md`
**After generation**: **After generation**:
- If `--prompts-only`, stop here and output prompt summary - If `--prompts-only`, stop here and output prompt summary
@@ -539,7 +541,9 @@ options:
**Standard flow**: **Standard flow**:
1. Select available image generation skill 1. Select available image generation skill
2. Generate session ID: `slides-{topic-slug}-{timestamp}` 2. Generate session ID: `slides-{topic-slug}-{timestamp}`
3. Generate each slide sequentially with same session ID 3. For each slide:
- **Backup rule**: If image file exists, rename to `NN-slide-{slug}-backup-YYYYMMDD-HHMMSS.png`
- Generate image sequentially with same session ID
4. Report progress: "Generated X/N" (in user's language) 4. Report progress: "Generated X/N" (in user's language)
5. Auto-retry once on failure before reporting error 5. Auto-retry once on failure before reporting error
+22 -14
View File
@@ -1,6 +1,6 @@
--- ---
name: baoyu-xhs-images name: baoyu-xhs-images
description: Generates Xiaohongshu (Little Red Book) infographic series with 9 visual styles and 6 layouts. Breaks content into 1-10 cartoon-style images optimized for XHS engagement. Use when user mentions "小红书图片", "XHS images", "RedNote infographics", "小红书种草", or wants social media infographics for Chinese platforms. description: Generates Xiaohongshu (Little Red Book) infographic series with 10 visual styles and 8 layouts. Breaks content into 1-10 cartoon-style images optimized for XHS engagement. Use when user mentions "小红书图片", "XHS images", "RedNote infographics", "小红书种草", or wants social media infographics for Chinese platforms.
--- ---
# Xiaohongshu Infographic Series Generator # Xiaohongshu Infographic Series Generator
@@ -42,8 +42,8 @@ Break down complex content into eye-catching infographic series for Xiaohongshu
| Dimension | Controls | Options | | Dimension | Controls | Options |
|-----------|----------|---------| |-----------|----------|---------|
| **Style** | Visual aesthetics: colors, lines, decorations | cute, fresh, warm, bold, minimal, retro, pop, notion, chalkboard | | **Style** | Visual aesthetics: colors, lines, decorations | cute, fresh, warm, bold, minimal, retro, pop, notion, chalkboard, study-notes |
| **Layout** | Information structure: density, arrangement | sparse, balanced, dense, list, comparison, flow | | **Layout** | Information structure: density, arrangement | sparse, balanced, dense, list, comparison, flow, mindmap, quadrant |
Style × Layout can be freely combined. Example: `--style notion --layout dense` creates an intellectual-looking knowledge card with high information density. Style × Layout can be freely combined. Example: `--style notion --layout dense` creates an intellectual-looking knowledge card with high information density.
@@ -60,6 +60,7 @@ Style × Layout can be freely combined. Example: `--style notion --layout dense`
| `pop` | Vibrant, energetic, eye-catching | | `pop` | Vibrant, energetic, eye-catching |
| `notion` | Minimalist hand-drawn line art, intellectual | | `notion` | Minimalist hand-drawn line art, intellectual |
| `chalkboard` | Colorful chalk on black board, educational | | `chalkboard` | Colorful chalk on black board, educational |
| `study-notes` | Realistic handwritten photo style, blue pen + red annotations + yellow highlighter |
Detailed style definitions: `references/presets/<style>.md` Detailed style definitions: `references/presets/<style>.md`
@@ -73,6 +74,8 @@ Detailed style definitions: `references/presets/<style>.md`
| `list` | Enumeration and ranking format (4-7 items) | | `list` | Enumeration and ranking format (4-7 items) |
| `comparison` | Side-by-side contrast layout | | `comparison` | Side-by-side contrast layout |
| `flow` | Process and timeline layout (3-6 steps) | | `flow` | Process and timeline layout (3-6 steps) |
| `mindmap` | Center radial mind map layout (4-8 branches) |
| `quadrant` | Four-quadrant / circular section layout |
Detailed layout definitions: `references/elements/canvas.md` Detailed layout definitions: `references/elements/canvas.md`
@@ -89,6 +92,7 @@ Detailed layout definitions: `references/elements/canvas.md`
| Fun, exciting, wow, amazing | `pop` | sparse/list | | Fun, exciting, wow, amazing | `pop` | sparse/list |
| Knowledge, concept, productivity, SaaS | `notion` | dense/list | | Knowledge, concept, productivity, SaaS | `notion` | dense/list |
| Education, tutorial, learning, teaching, classroom | `chalkboard` | balanced/dense | | Education, tutorial, learning, teaching, classroom | `chalkboard` | balanced/dense |
| Notes, handwritten, study guide, knowledge, realistic, photo | `study-notes` | dense/list/mindmap |
## Outline Strategies ## Outline Strategies
@@ -227,6 +231,7 @@ Read source content, save it if needed, and perform deep analysis.
1. **Save source content** (if not already a file): 1. **Save source content** (if not already a file):
- If user provides a file path: use as-is - If user provides a file path: use as-is
- If user pastes content: save to `source.md` in target directory - If user pastes content: save to `source.md` in target directory
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
2. Read source content 2. Read source content
3. **Deep analysis** following `references/workflows/analysis-framework.md`: 3. **Deep analysis** following `references/workflows/analysis-framework.md`:
- Content type classification (种草/干货/测评/教程/避坑...) - Content type classification (种草/干货/测评/教程/避坑...)
@@ -353,7 +358,9 @@ With confirmed outline + style + layout:
**For each image (cover + content + ending)**: **For each image (cover + content + ending)**:
1. Save prompt to `prompts/NN-{type}-[slug].md` (in user's preferred language) 1. Save prompt to `prompts/NN-{type}-[slug].md` (in user's preferred language)
- **Backup rule**: If prompt file exists, rename to `prompts/NN-{type}-[slug]-backup-YYYYMMDD-HHMMSS.md`
2. Generate image using confirmed style and layout 2. Generate image using confirmed style and layout
- **Backup rule**: If image file exists, rename to `NN-{type}-[slug]-backup-YYYYMMDD-HHMMSS.png`
3. Report progress after each generation 3. Report progress after each generation
**Watermark Application** (if enabled in preferences): **Watermark Application** (if enabled in preferences):
@@ -417,17 +424,18 @@ Files:
**Style × Layout Matrix** (✓✓ = highly recommended, ✓ = works well): **Style × Layout Matrix** (✓✓ = highly recommended, ✓ = works well):
| | sparse | balanced | dense | list | comparison | flow | | | sparse | balanced | dense | list | comparison | flow | mindmap | quadrant |
|---|:---:|:---:|:---:|:---:|:---:|:---:| |---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| cute | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | | cute | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ |
| fresh | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | | fresh | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | ✓ | ✓ |
| warm | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ | | warm | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ |
| bold | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ | | bold | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ |
| minimal | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | | minimal | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| retro | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | | retro | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ |
| pop | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | | pop | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ |
| notion | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | | notion | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ |
| chalkboard | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | | chalkboard | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ |
| study-notes | ✗ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ |
## References ## References
@@ -51,6 +51,8 @@ Avoid placing critical content in these areas:
| list | Vertical enumeration | 4-7 | Rankings, checklists, step guides | | list | Vertical enumeration | 4-7 | Rankings, checklists, step guides |
| comparison | Left vs Right | 2 sections | Before/after, pros/cons | | comparison | Left vs Right | 2 sections | Before/after, pros/cons |
| flow | Connected nodes | 3-6 steps | Processes, timelines, workflows | | flow | Connected nodes | 3-6 steps | Processes, timelines, workflows |
| mindmap | Center radial | 4-8 branches | Concept maps, brainstorming, topic overview |
| quadrant | 4-section grid | 4 sections | SWOT analysis, priority matrix, classification |
## Layout by Position ## Layout by Position
@@ -106,3 +108,15 @@ For multi-element compositions:
- Directional flow (top→bottom or left→right) - Directional flow (top→bottom or left→right)
- Connected nodes with arrows - Connected nodes with arrows
- Clear progression indicators - Clear progression indicators
### Mindmap Layout
- Central topic node
- Radial branches outward
- Hierarchical sub-branches
- Organic curved connections
### Quadrant Layout
- 4-section grid (2×2)
- Clear axis labels
- Each quadrant with distinct content
- Optional circular variant for cycles
@@ -0,0 +1,115 @@
---
name: study-notes
category: realistic
---
# Study Notes Style
Realistic handwritten photo aesthetic - student notes style, dense and messy but readable.
## Element Combination
```yaml
canvas:
ratio: portrait-3-4
grid: single
image_effects:
cutout: none
stroke: none
filter: natural-photo
typography:
decorated: none
tags: none
direction: horizontal
decorations:
emphasis: circle-mark | underline | checkmark | cross | star-simple
background: lined-paper-white
doodles: arrows-simple | margin-notes | corrections | explanatory-diagrams
frames: none
```
## Color Palette (Three-Color Annotation System)
| Role | Colors | Hex |
|------|--------|-----|
| Primary | Blue ballpoint, Black ink | #1E3A5F, #1A1A1A |
| Highlights | Yellow highlighter | #FFFF00 (50% opacity) |
| Accents | Red pen (circles, underlines) | #CC0000 |
| Background | White lined paper | #FFFFFF |
## Visual Elements
- Realistic photo perspective: top-down view of study desk
- Hand holding blue ballpoint pen, actively underlining
- Extremely dense handwritten content, filling entire page
- Red pen annotations: circles, underlines, stars, boxes
- Yellow highlighter marking key terms
- Correction marks, cramped notes squeezed into margins
- Simple hand-drawn symbols: → * ✓ ✗ !
- Varying pen pressure creating lighter and darker strokes
## Typography
- Authentic student handwriting
- Messy but readable, clear structure maintained
- Varying font sizes (large titles, small body, tiny margin notes)
- CJK optimized
## Content Structure
Three-section layout:
### Top Section
- Core topic (circled multiple times in red)
- First section title + 3-4 key points
- Arrow connections, red underlines
### Middle Section
- Second section title (red pen box)
- Numbered steps ①②③
- Specific methods and supplementary notes
### Bottom Section
- Third section title (red star)
- Time points / key metrics
- Key quotes / core tips (tiny corner notes)
## Best Layout Pairings
| Layout | Compatibility | Use Case |
|--------|---------------|----------|
| sparse | ✗ | Not suitable - style requires dense content |
| balanced | ✓ | When content is lighter |
| dense | ✓✓ | Best fit - knowledge notes, summaries |
| list | ✓✓ | Step checklists, rankings |
| comparison | ✓ | Comparative analysis |
| flow | ✓ | Process flows |
| mindmap | ✓✓ | Mind map notes |
| quadrant | ✓ | Quadrant analysis |
## Best For
- Study guides, exam notes
- Knowledge organization, framework summaries
- Tutorial summaries, quick notes
- "Top student notes" style content
- Knowledge sharing requiring authentic feel
## Style Rules
### DO ✓
- Keep content extremely dense
- Use simple symbols (→ * ✓ ✗ !)
- Annotate key points with red pen
- Include correction marks
- Squeeze tiny notes into margins
### DON'T ✗
- Use complex emojis
- Leave too much whitespace
- Make neat, tidy layouts
- Add colorful decorations
- Include cartoon elements