mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 13:59:47 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10e1c426a0 | |||
| dbab83ff82 | |||
| 478e66a93a | |||
| 3a5866eb4b | |||
| c0162bb3af | |||
| e9342a16f8 | |||
| 5cb86d8e51 | |||
| 66ed08f010 | |||
| b179166d28 | |||
| 4805526649 | |||
| 89159bf86e |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "1.26.0"
|
||||
"version": "1.27.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
[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
|
||||
|
||||
### 新功能
|
||||
|
||||
@@ -78,6 +78,8 @@ Progress:
|
||||
| File path | Ask user (1.2) | → 1.2 |
|
||||
| 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)
|
||||
|
||||
Check `default_output_dir` in preferences:
|
||||
@@ -236,6 +238,7 @@ image_count: 4
|
||||
**5.1 Create Prompts**
|
||||
|
||||
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**
|
||||
|
||||
@@ -247,7 +250,9 @@ Add: `Include a subtle watermark "[content]" at [position].`
|
||||
|
||||
**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"
|
||||
3. On failure: retry once, then log and continue
|
||||
|
||||
|
||||
@@ -194,6 +194,7 @@ Input → Preferences → Analyze → [Check Existing?] → [Confirm: Style + Re
|
||||
**Character reference is MANDATORY for visual consistency.**
|
||||
|
||||
**7.1 Generate character sheet first**:
|
||||
- **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
|
||||
```bash
|
||||
# Use Reference Sheet Prompt from characters/characters.md
|
||||
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 |
|
||||
| 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
|
||||
# Example: ALWAYS include --ref for consistency
|
||||
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
||||
|
||||
@@ -84,6 +84,7 @@ Read source content, save it if needed, and perform deep analysis.
|
||||
1. **Save source content** (if not already a file):
|
||||
- If user provides a file path: use as-is
|
||||
- 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
|
||||
3. **Deep analysis** following `analysis-framework.md`:
|
||||
- Target audience identification
|
||||
@@ -329,6 +330,7 @@ Create image generation prompts for all pages.
|
||||
1. Create prompt following art style + tone guidelines
|
||||
2. Include character visual descriptions for consistency
|
||||
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**:
|
||||
```markdown
|
||||
@@ -405,8 +407,9 @@ With confirmed prompts from Step 5/6:
|
||||
### 7.1 Generate Character Reference Sheet (first)
|
||||
|
||||
1. Use Reference Sheet Prompt from `characters/characters.md`
|
||||
2. Generate → `characters/characters.png`
|
||||
3. This ensures visual consistency for all subsequent pages
|
||||
2. **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
|
||||
3. Generate → `characters/characters.png`
|
||||
4. This ensures visual consistency for all subsequent 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)**:
|
||||
1. Read prompt from `prompts/NN-{cover|page}-[slug].md`
|
||||
2. Generate image using Strategy A or B (based on skill capability)
|
||||
3. Save to `NN-{cover|page}-[slug].png`
|
||||
4. Report progress after each generation: "Generated X/N: [page title]"
|
||||
2. **Backup rule**: If image file exists, rename to `NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.png`
|
||||
3. Generate image using Strategy A or B (based on skill capability)
|
||||
4. Save to `NN-{cover|page}-[slug].png`
|
||||
5. Report progress after each generation: "Generated X/N: [page title]"
|
||||
|
||||
**Session Management**:
|
||||
If image generation skill supports `--sessionId`:
|
||||
|
||||
@@ -209,6 +209,7 @@ Schema: [references/config/preferences-schema.md](references/config/preferences-
|
||||
### Step 1: Analyze Content
|
||||
|
||||
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
|
||||
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
|
||||
@@ -226,6 +227,8 @@ Validate all 5 dimensions + aspect ratio. Full confirmation flow: [references/wo
|
||||
|
||||
### 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)
|
||||
|
||||
### Step 4: Generate Image
|
||||
|
||||
@@ -13,8 +13,7 @@ Scripts in `scripts/` subdirectory. Replace `${SKILL_DIR}` with this SKILL.md's
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `scripts/main.ts` | Main entry point with CLI options |
|
||||
| `scripts/cjk-emphasis.ts` | Fix CJK emphasis/bold punctuation issues |
|
||||
| `scripts/main.ts` | Main entry point with CLI options (uses remark-cjk-friendly for CJK emphasis) |
|
||||
| `scripts/quotes.ts` | Replace ASCII quotes with fullwidth quotes |
|
||||
| `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("");
|
||||
}
|
||||
@@ -1,17 +1,12 @@
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { unified } from "unified";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkCjkFriendly from "remark-cjk-friendly";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkFrontmatter from "remark-frontmatter";
|
||||
import remarkStringify from "remark-stringify";
|
||||
import { visit } from "unist-util-visit";
|
||||
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 { applyAutocorrect } from "./autocorrect";
|
||||
|
||||
@@ -36,6 +31,12 @@ const DEFAULT_OPTIONS: Required<FormatOptions> = {
|
||||
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 {
|
||||
try {
|
||||
const doc = YAML.parseDocument(value);
|
||||
@@ -49,12 +50,9 @@ function formatMarkdownContent(
|
||||
content: string,
|
||||
options: Required<FormatOptions>
|
||||
): string {
|
||||
if (options.emphasis) {
|
||||
content = fixCjkEmphasisSpacing(content);
|
||||
}
|
||||
|
||||
const processor = unified()
|
||||
.use(remarkParse)
|
||||
.use(options.emphasis ? remarkCjkFriendly : [])
|
||||
.use(remarkGfm)
|
||||
.use(remarkFrontmatter, ["yaml"])
|
||||
.use(remarkStringify, {
|
||||
@@ -63,7 +61,7 @@ function formatMarkdownContent(
|
||||
|
||||
const tree = processor.parse(content);
|
||||
|
||||
visit(tree, (node, _index, parent) => {
|
||||
visit(tree, (node) => {
|
||||
if (node.type === "text" && options.quotes) {
|
||||
const textNode = node as { value: string };
|
||||
textNode.value = replaceQuotes(textNode.value);
|
||||
@@ -77,54 +75,11 @@ function formatMarkdownContent(
|
||||
}
|
||||
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);
|
||||
if (options.emphasis) {
|
||||
result = fixCjkEmphasisSpacing(result);
|
||||
result = decodeHtmlEntities(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+1125
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"remark-cjk-friendly": "^1.1.0",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
|
||||
@@ -153,10 +153,12 @@ Schema: `references/config/preferences-schema.md`
|
||||
**1.2 Analyze Content → `analysis.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
|
||||
3. Detect source language and user language
|
||||
4. Extract design instructions from user input
|
||||
5. Save analysis
|
||||
- **Backup rule**: If `analysis.md` exists, rename to `analysis-backup-YYYYMMDD-HHMMSS.md`
|
||||
|
||||
See `references/analysis-framework.md` for detailed format.
|
||||
|
||||
@@ -182,13 +184,20 @@ Recommend 3-5 layout×style combinations based on:
|
||||
|
||||
### Step 4: Confirm Options
|
||||
|
||||
Present all options in single confirmation:
|
||||
1. **Combination** (always): 3+ options with rationale
|
||||
2. **Aspect** (always): landscape/portrait/square
|
||||
3. **Language** (only if source ≠ user language): which language for text
|
||||
Use **single AskUserQuestion call** with multiple questions to confirm all options together:
|
||||
|
||||
| Question | When | Options |
|
||||
|----------|------|---------|
|
||||
| **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`
|
||||
|
||||
**Backup rule**: If `prompts/infographic.md` exists, rename to `prompts/infographic-backup-YYYYMMDD-HHMMSS.md`
|
||||
|
||||
Combine:
|
||||
1. Layout definition from `references/layouts/<layout>.md`
|
||||
2. Style definition from `references/styles/<style>.md`
|
||||
@@ -199,8 +208,10 @@ Combine:
|
||||
### Step 6: Generate Image
|
||||
|
||||
1. Select available image generation skill (ask user if multiple)
|
||||
2. Call with prompt file and output path
|
||||
3. On failure, auto-retry once
|
||||
2. **Check for existing file**: Before generating, check if `infographic.png` exists
|
||||
- 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
|
||||
|
||||
|
||||
+1395
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 { marked, type RendererObject, type Tokens } from "marked";
|
||||
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 {
|
||||
markedAlert,
|
||||
@@ -552,12 +556,47 @@ interface CliOptions {
|
||||
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): {
|
||||
html: string;
|
||||
readingTime: ReadTimeResults;
|
||||
} {
|
||||
const preprocessed = preprocessCjkEmphasis(raw);
|
||||
const { markdownContent, readingTime: readingTimeResult } =
|
||||
renderer.parseFrontMatterAndContent(raw);
|
||||
renderer.parseFrontMatterAndContent(preprocessed);
|
||||
|
||||
const html = marked.parse(markdownContent) as string;
|
||||
|
||||
|
||||
@@ -226,6 +226,7 @@ Schema: `references/config/preferences-schema.md`
|
||||
**1.2 Analyze Content**
|
||||
|
||||
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
|
||||
3. Analyze content signals for style recommendations
|
||||
4. Detect source language
|
||||
@@ -483,6 +484,7 @@ options:
|
||||
- Add slide-specific content
|
||||
- If `Layout:` specified, include layout guidance from `references/layouts.md`
|
||||
3. Save to `prompts/` directory
|
||||
- **Backup rule**: If prompt file exists, rename to `prompts/NN-slide-{slug}-backup-YYYYMMDD-HHMMSS.md`
|
||||
|
||||
**After generation**:
|
||||
- If `--prompts-only`, stop here and output prompt summary
|
||||
@@ -539,7 +541,9 @@ options:
|
||||
**Standard flow**:
|
||||
1. Select available image generation skill
|
||||
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)
|
||||
5. Auto-retry once on failure before reporting error
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: baoyu-xhs-images
|
||||
description: Generates Xiaohongshu (Little Red Book) infographic series with 11 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.
|
||||
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
|
||||
@@ -42,7 +42,7 @@ Break down complex content into eye-catching infographic series for Xiaohongshu
|
||||
|
||||
| Dimension | Controls | Options |
|
||||
|-----------|----------|---------|
|
||||
| **Style** | Visual aesthetics: colors, lines, decorations | cute, fresh, warm, bold, minimal, retro, pop, notion, chalkboard, notebook, study-notes |
|
||||
| **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, mindmap, quadrant |
|
||||
|
||||
Style × Layout can be freely combined. Example: `--style notion --layout dense` creates an intellectual-looking knowledge card with high information density.
|
||||
@@ -60,7 +60,6 @@ Style × Layout can be freely combined. Example: `--style notion --layout dense`
|
||||
| `pop` | Vibrant, energetic, eye-catching |
|
||||
| `notion` | Minimalist hand-drawn line art, intellectual |
|
||||
| `chalkboard` | Colorful chalk on black board, educational |
|
||||
| `notebook` | Hand-drawn infographic style, watercolor rendering + Morandi palette |
|
||||
| `study-notes` | Realistic handwritten photo style, blue pen + red annotations + yellow highlighter |
|
||||
|
||||
Detailed style definitions: `references/presets/<style>.md`
|
||||
@@ -93,7 +92,6 @@ Detailed layout definitions: `references/elements/canvas.md`
|
||||
| Fun, exciting, wow, amazing | `pop` | sparse/list |
|
||||
| Knowledge, concept, productivity, SaaS | `notion` | dense/list |
|
||||
| Education, tutorial, learning, teaching, classroom | `chalkboard` | balanced/dense |
|
||||
| Notes, hand-drawn, infographic, study, mindmap, Morandi | `notebook` | mindmap/balanced/dense |
|
||||
| Notes, handwritten, study guide, knowledge, realistic, photo | `study-notes` | dense/list/mindmap |
|
||||
|
||||
## Outline Strategies
|
||||
@@ -233,6 +231,7 @@ Read source content, save it if needed, and perform deep analysis.
|
||||
1. **Save source content** (if not already a file):
|
||||
- If user provides a file path: use as-is
|
||||
- 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
|
||||
3. **Deep analysis** following `references/workflows/analysis-framework.md`:
|
||||
- Content type classification (种草/干货/测评/教程/避坑...)
|
||||
@@ -359,7 +358,9 @@ With confirmed outline + style + layout:
|
||||
|
||||
**For each image (cover + content + ending)**:
|
||||
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
|
||||
- **Backup rule**: If image file exists, rename to `NN-{type}-[slug]-backup-YYYYMMDD-HHMMSS.png`
|
||||
3. Report progress after each generation
|
||||
|
||||
**Watermark Application** (if enabled in preferences):
|
||||
@@ -434,7 +435,6 @@ Files:
|
||||
| pop | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ |
|
||||
| notion | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ |
|
||||
| chalkboard | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
||||
| notebook | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ |
|
||||
| study-notes | ✗ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ |
|
||||
|
||||
## References
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
name: notebook
|
||||
category: educational
|
||||
---
|
||||
|
||||
# Notebook Style
|
||||
|
||||
Hand-drawn infographic / study notes aesthetic with watercolor rendering and Morandi color palette.
|
||||
|
||||
## Element Combination
|
||||
|
||||
```yaml
|
||||
canvas:
|
||||
ratio: portrait-3-4
|
||||
grid: single | dual | triptych
|
||||
|
||||
image_effects:
|
||||
cutout: soft
|
||||
stroke: none | watercolor-edge
|
||||
filter: muted-tones | morandi
|
||||
|
||||
typography:
|
||||
decorated: watercolor-highlight | handwritten-bold
|
||||
tags: pill | rounded-rect
|
||||
direction: horizontal
|
||||
|
||||
decorations:
|
||||
emphasis: circle-mark | underline | checkmark
|
||||
background: paper-texture | watercolor-wash
|
||||
doodles: hand-drawn-lines | mindmap-nodes | speech-bubbles | hearts | peace-dove
|
||||
frames: rounded-rect | none
|
||||
```
|
||||
|
||||
## Color Palette (Morandi Tones)
|
||||
|
||||
| Role | Colors | Hex |
|
||||
|------|--------|-----|
|
||||
| Primary | Sage green, Teal | #A8B5A0, #4A7C7C |
|
||||
| Background | Off-white, Warm paper | #FAF8F5, #F5F0E8 |
|
||||
| Accents | Dust blue, Warm grey, Terracotta | #8FA3B0, #9A9590, #C4A484 |
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Bold handwritten titles with watercolor wash blocks (sage green/teal backgrounds)
|
||||
- Black line illustrations, slightly imperfect, simulating ink effect
|
||||
- Soft paper texture background, mimicking watercolor paper
|
||||
- Mindmap nodes, list boxes, explanatory doodles
|
||||
- Symbolic icons: hearts, speech bubbles, peace dove, handshake
|
||||
- Flat lighting, vector illustration feel with analog texture
|
||||
|
||||
## Typography
|
||||
|
||||
- Bold handwritten fonts
|
||||
- Watercolor wash effect backgrounds
|
||||
- Clear and readable, CJK optimized
|
||||
|
||||
## Best Layout Pairings
|
||||
|
||||
| Layout | Compatibility | Use Case |
|
||||
|--------|---------------|----------|
|
||||
| sparse | ✓✓ | Covers, quotes |
|
||||
| balanced | ✓✓ | Standard content cards |
|
||||
| dense | ✓✓ | Knowledge cards, checklists |
|
||||
| list | ✓✓ | Step guides, rankings |
|
||||
| comparison | ✓✓ | Before/after, contrasts |
|
||||
| flow | ✓✓ | Processes, timelines |
|
||||
| mindmap | ✓✓ | Mind maps |
|
||||
| quadrant | ✓✓ | Quadrant analysis, circular sections |
|
||||
|
||||
## Best For
|
||||
|
||||
- Study notes, knowledge summaries
|
||||
- Mind maps, infographics
|
||||
- Tutorials, knowledge sharing
|
||||
- Bullet journal style content
|
||||
- Concept explanations, framework overviews
|
||||
Reference in New Issue
Block a user