mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-17 07:59:48 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e31294415d | |||
| 926f74e9c9 | |||
| 339990e87e | |||
| 3c5c3e006d | |||
| 2aa9790789 | |||
| 38fc733b99 |
@@ -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.67.0"
|
"version": "1.68.0"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 1.68.0 - 2026-03-14
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-article-illustrator`: add configurable output directory (`default_output_dir`) with 4 options — `imgs-subdir`, `same-dir`, `illustrations-subdir`, `independent`
|
||||||
|
- `baoyu-cover-image`: add character preservation from reference images — use `usage: direct` to pass people references to model for stylized likeness
|
||||||
|
|
||||||
## 1.67.0 - 2026-03-13
|
## 1.67.0 - 2026-03-13
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 1.68.0 - 2026-03-14
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-article-illustrator`:新增可配置输出目录(`default_output_dir`),支持 4 种选项——`imgs-subdir`、`same-dir`、`illustrations-subdir`、`independent`
|
||||||
|
- `baoyu-cover-image`:新增参考图片人物保留功能——当参考图包含人物时使用 `usage: direct` 传递给模型,风格化保留人物特征
|
||||||
|
|
||||||
## 1.67.0 - 2026-03-13
|
## 1.67.0 - 2026-03-13
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.67.0**.
|
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.68.0**.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { initRenderer, renderMarkdown } from "./renderer.ts";
|
||||||
|
|
||||||
|
const render = (md: string) => {
|
||||||
|
const r = initRenderer();
|
||||||
|
return renderMarkdown(md, r).html;
|
||||||
|
};
|
||||||
|
|
||||||
|
test("bold with inline code (no underscore)", () => {
|
||||||
|
const html = render("**算出 `logits`,算出 `loss`。**");
|
||||||
|
assert.match(html, /<code[^>]*>logits<\/code>/);
|
||||||
|
assert.match(html, /<code[^>]*>loss<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code (contains underscore)", () => {
|
||||||
|
const html = render("**变成 `input_ids`。**");
|
||||||
|
assert.match(html, /<code[^>]*>input_ids<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emphasis with inline code", () => {
|
||||||
|
const html = render("*查看 `hidden_states`*");
|
||||||
|
assert.match(html, /<code[^>]*>hidden_states<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plain inline code (regression)", () => {
|
||||||
|
const html = render("`lm_head`");
|
||||||
|
assert.match(html, /<code[^>]*>lm_head<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold without code (regression)", () => {
|
||||||
|
const html = render("**纯粗体文本**");
|
||||||
|
assert.match(html, /<strong[^>]*>纯粗体文本<\/strong>/);
|
||||||
|
assert.doesNotMatch(html, /<code/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing backticks", () => {
|
||||||
|
const html = render("**``a`b``**");
|
||||||
|
assert.match(html, /<code[^>]*>a`b<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emphasis with inline code containing backticks", () => {
|
||||||
|
const html = render("*``a`b``*");
|
||||||
|
assert.match(html, /<em[^>]*><code[^>]*>a`b<\/code><\/em>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing consecutive backticks", () => {
|
||||||
|
const html = render("**```a``b```**");
|
||||||
|
assert.match(html, /<code[^>]*>a``b<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing only backticks", () => {
|
||||||
|
const html = render("**```` `` ````**");
|
||||||
|
assert.match(html, /<code[^>]*>``<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing only spaces", () => {
|
||||||
|
const oneSpace = render("**`` ``**");
|
||||||
|
assert.match(oneSpace, /<code[^>]*> <\/code>/);
|
||||||
|
|
||||||
|
const twoSpaces = render("**`` ``**");
|
||||||
|
assert.match(twoSpaces, /<code[^>]*> <\/code>/);
|
||||||
|
});
|
||||||
@@ -109,6 +109,13 @@ function parseFrontMatterAndContent(markdownText: string): ParseResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wrapInlineCode(value: string): string {
|
||||||
|
const runs = value.match(/`+/g);
|
||||||
|
const fence = "`".repeat(Math.max(...(runs?.map((run) => run.length) ?? [0])) + 1);
|
||||||
|
const padding = /^ *$/.test(value) ? "" : " ";
|
||||||
|
return `${fence}${padding}${value}${padding}${fence}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function initRenderer(opts: IOpts = {}): RendererAPI {
|
export function initRenderer(opts: IOpts = {}): RendererAPI {
|
||||||
const footnotes: [number, string, string][] = [];
|
const footnotes: [number, string, string][] = [];
|
||||||
let footnoteIndex = 0;
|
let footnoteIndex = 0;
|
||||||
@@ -369,6 +376,7 @@ function preprocessCjkEmphasis(markdown: string): string {
|
|||||||
const tree = processor.parse(markdown);
|
const tree = processor.parse(markdown);
|
||||||
const extractText = (node: any): string => {
|
const extractText = (node: any): string => {
|
||||||
if (node.type === "text") return node.value;
|
if (node.type === "text") return node.value;
|
||||||
|
if (node.type === "inlineCode") return wrapInlineCode(node.value);
|
||||||
if (node.children) return node.children.map(extractText).join("");
|
if (node.children) return node.children.map(extractText).join("");
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ Full procedures: [references/workflow.md](references/workflow.md#step-5-generate
|
|||||||
|
|
||||||
### Step 6: Finalize
|
### Step 6: Finalize
|
||||||
|
|
||||||
Insert `` after paragraphs.
|
Insert `` after paragraphs. Path computed relative to article file based on output directory setting.
|
||||||
|
|
||||||
```
|
```
|
||||||
Article Illustration Complete!
|
Article Illustration Complete!
|
||||||
@@ -143,15 +143,27 @@ Images: X/N generated
|
|||||||
|
|
||||||
## Output Directory
|
## Output Directory
|
||||||
|
|
||||||
|
Output directory is determined by `default_output_dir` in EXTEND.md (set during first-time setup):
|
||||||
|
|
||||||
|
| `default_output_dir` | Output Path | Markdown Insert Path |
|
||||||
|
|----------------------|-------------|----------------------|
|
||||||
|
| `imgs-subdir` (default) | `{article-dir}/imgs/` | `imgs/NN-{type}-{slug}.png` |
|
||||||
|
| `same-dir` | `{article-dir}/` | `NN-{type}-{slug}.png` |
|
||||||
|
| `illustrations-subdir` | `{article-dir}/illustrations/` | `illustrations/NN-{type}-{slug}.png` |
|
||||||
|
| `independent` | `illustrations/{topic-slug}/` | `illustrations/{topic-slug}/NN-{type}-{slug}.png` (relative to cwd) |
|
||||||
|
|
||||||
|
All auxiliary files (outline, prompts) are saved inside the output directory:
|
||||||
|
|
||||||
```
|
```
|
||||||
illustrations/{topic-slug}/
|
{output-dir}/
|
||||||
├── source-{slug}.{ext}
|
|
||||||
├── references/ # if provided
|
|
||||||
├── outline.md
|
├── outline.md
|
||||||
├── prompts/
|
├── prompts/
|
||||||
|
│ └── NN-{type}-{slug}.md
|
||||||
└── NN-{type}-{slug}.png
|
└── NN-{type}-{slug}.png
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When input is **pasted content** (no file path), always uses `illustrations/{topic-slug}/` with `source-{slug}.{ext}` saved alongside.
|
||||||
|
|
||||||
**Slug**: 2-4 words, kebab-case. **Conflict**: append `-YYYYMMDD-HHMMSS`.
|
**Slug**: 2-4 words, kebab-case. **Conflict**: append `-YYYYMMDD-HHMMSS`.
|
||||||
|
|
||||||
## Modification
|
## Modification
|
||||||
|
|||||||
@@ -69,7 +69,23 @@ options:
|
|||||||
description: "Friendly, approachable, personal"
|
description: "Friendly, approachable, personal"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Question 3: Save Location
|
### Question 3: Output Directory
|
||||||
|
|
||||||
|
```
|
||||||
|
header: "Output Directory"
|
||||||
|
question: "Where to save generated illustrations when illustrating a file?"
|
||||||
|
options:
|
||||||
|
- label: "imgs-subdir (Recommended)"
|
||||||
|
description: "{article-dir}/imgs/ — images in a subdirectory next to the article"
|
||||||
|
- label: "same-dir"
|
||||||
|
description: "{article-dir}/ — images alongside the article file"
|
||||||
|
- label: "illustrations-subdir"
|
||||||
|
description: "{article-dir}/illustrations/ — separate illustrations subdirectory"
|
||||||
|
- label: "independent"
|
||||||
|
description: "illustrations/{topic-slug}/ — standalone directory in cwd"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Question 4: Save Location
|
||||||
|
|
||||||
```
|
```
|
||||||
header: "Save"
|
header: "Save"
|
||||||
@@ -108,6 +124,7 @@ watermark:
|
|||||||
preferred_style:
|
preferred_style:
|
||||||
name: [selected style or null]
|
name: [selected style or null]
|
||||||
description: ""
|
description: ""
|
||||||
|
default_output_dir: imgs-subdir # same-dir | imgs-subdir | illustrations-subdir | independent
|
||||||
language: null
|
language: null
|
||||||
custom_styles: []
|
custom_styles: []
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ Reference Style Extracted (no file):
|
|||||||
|
|
||||||
| Input | Output Directory | Next |
|
| Input | Output Directory | Next |
|
||||||
|-------|------------------|------|
|
|-------|------------------|------|
|
||||||
| File path | Ask user (1.2) | → 1.2 |
|
| File path | EXTEND.md `default_output_dir` (default: `imgs-subdir`). If not configured, confirm in 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.
|
**Backup rule for pasted content**: If `source.md` exists in target directory, rename to `source-backup-YYYYMMDD-HHMMSS.md` before saving.
|
||||||
@@ -68,7 +68,7 @@ Check preferences and existing state, then ask ALL needed questions in ONE AskUs
|
|||||||
|
|
||||||
| Question | When to Ask | Options |
|
| Question | When to Ask | Options |
|
||||||
|----------|-------------|---------|
|
|----------|-------------|---------|
|
||||||
| Output directory | No `default_output_dir` in EXTEND.md | `{article-dir}/`, `{article-dir}/imgs/` (Recommended), `{article-dir}/illustrations/`, `illustrations/{topic-slug}/` |
|
| Output directory | No `default_output_dir` in EXTEND.md | `{article-dir}/imgs/` (Recommended), `{article-dir}/`, `{article-dir}/illustrations/`, `illustrations/{topic-slug}/` |
|
||||||
| Existing images | Target dir has `.png/.jpg/.webp` files | `supplement`, `overwrite`, `regenerate` |
|
| Existing images | Target dir has `.png/.jpg/.webp` files | `supplement`, `overwrite`, `regenerate` |
|
||||||
| Article update | Always (file path input) | `update`, `copy` |
|
| Article update | Always (file path input) | `update`, `copy` |
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ Reference Images:
|
|||||||
|
|
||||||
## Step 4: Generate Outline
|
## Step 4: Generate Outline
|
||||||
|
|
||||||
Save as `outline.md`:
|
Save as `{output-dir}/outline.md` (all paths below are relative to the output directory determined in Step 1.1/1.2):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
@@ -285,7 +285,7 @@ references: # Only if references provided
|
|||||||
|
|
||||||
For each illustration in the outline:
|
For each illustration in the outline:
|
||||||
|
|
||||||
1. **Create prompt file**: `prompts/NN-{type}-{slug}.md`
|
1. **Create prompt file**: `{output-dir}/prompts/NN-{type}-{slug}.md`
|
||||||
2. **Include YAML frontmatter**:
|
2. **Include YAML frontmatter**:
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
@@ -381,10 +381,14 @@ Add: `Include a subtle watermark "[content]" at [position].`
|
|||||||
|
|
||||||
### 6.1 Update Article
|
### 6.1 Update Article
|
||||||
|
|
||||||
Insert after corresponding paragraph:
|
Insert after corresponding paragraph, using path relative to article file:
|
||||||
```markdown
|
|
||||||

|
| `default_output_dir` | Insert Path |
|
||||||
```
|
|----------------------|-------------|
|
||||||
|
| `imgs-subdir` | `` |
|
||||||
|
| `same-dir` | `` |
|
||||||
|
| `illustrations-subdir` | `` |
|
||||||
|
| `independent` | `` (relative to cwd) |
|
||||||
|
|
||||||
Alt text: concise description in article's language.
|
Alt text: concise description in article's language.
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ if (Test-Path "$HOME/.baoyu-skills/baoyu-cover-image/EXTEND.md") { "user" }
|
|||||||
2. **Save source content** (if pasted, save to `source.md`)
|
2. **Save source content** (if pasted, save to `source.md`)
|
||||||
3. **Analyze content**: topic, tone, keywords, visual metaphors
|
3. **Analyze content**: topic, tone, keywords, visual metaphors
|
||||||
4. **Deep analyze references** ⚠️: Extract specific, concrete elements (see reference-images.md)
|
4. **Deep analyze references** ⚠️: Extract specific, concrete elements (see reference-images.md)
|
||||||
|
- If references contain **people** → set `usage: direct` so model sees reference image, describe character features for stylized preservation (see reference-images.md § Character Analysis)
|
||||||
5. **Detect language**: Compare source, user input, EXTEND.md preference
|
5. **Detect language**: Compare source, user input, EXTEND.md preference
|
||||||
6. **Determine output directory**: Per File Structure rules
|
6. **Determine output directory**: Per File Structure rules
|
||||||
|
|
||||||
|
|||||||
@@ -83,12 +83,17 @@ Full library: [references/visual-elements.md](visual-elements.md)
|
|||||||
|
|
||||||
### Character Handling
|
### Character Handling
|
||||||
|
|
||||||
When people are needed:
|
**Default (no reference with people)**:
|
||||||
- Use simplified silhouettes or abstract stick figures
|
- Use simplified silhouettes or abstract stick figures
|
||||||
- Symbolic representations (head + shoulders outline)
|
- Symbolic representations (head + shoulders outline)
|
||||||
- NO realistic faces, detailed anatomy, or photographic representations
|
- NO realistic faces, detailed anatomy, or photographic representations
|
||||||
- Cartoon/icon style consistent with rendering choice
|
- Cartoon/icon style consistent with rendering choice
|
||||||
|
|
||||||
|
**When reference images contain people**:
|
||||||
|
- Reference image is passed to model (`usage: direct`) — model must visually reference it to preserve character likeness
|
||||||
|
- Stylize to match chosen rendering (cartoon/vector), preserving distinctive features (hair, clothing, pose)
|
||||||
|
- NEVER photorealistic
|
||||||
|
|
||||||
## Mood Application
|
## Mood Application
|
||||||
|
|
||||||
Apply mood adjustments to the base palette:
|
Apply mood adjustments to the base palette:
|
||||||
|
|||||||
@@ -201,6 +201,12 @@ CRITICAL: The generated cover MUST visually reference the provided images. The c
|
|||||||
- [Typography]: [Specific treatment, e.g., "Uppercase text with wide letter-spacing"]
|
- [Typography]: [Specific treatment, e.g., "Uppercase text with wide letter-spacing"]
|
||||||
- [Layout element]: [Specific spatial element, e.g., "Bottom banner strip in dark color"]
|
- [Layout element]: [Specific spatial element, e.g., "Bottom banner strip in dark color"]
|
||||||
|
|
||||||
|
## From Ref 1 ([filename]) — Characters (if people present):
|
||||||
|
- **Character 1**: [Appearance, e.g., "Woman, long wavy blonde hair"] → MUST stylize: [e.g., "flat-vector, simplified face, keep blonde hair, label: 'Nicole Forsgren'"]
|
||||||
|
- **Character 2**: [Appearance, e.g., "Man, short dark hair, stubble"] → MUST stylize: [e.g., "flat-vector, simplified face, keep dark hair, label: 'Gergely Orosz'"]
|
||||||
|
- **Placement**: [e.g., "Right third, side by side, facing left toward main visual"]
|
||||||
|
- **Style**: Match rendering style, NOT photorealistic
|
||||||
|
|
||||||
## From Ref 2 ([filename]) — REQUIRED elements:
|
## From Ref 2 ([filename]) — REQUIRED elements:
|
||||||
[Same detailed breakdown]
|
[Same detailed breakdown]
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ usage: direct | style | palette
|
|||||||
|
|
||||||
| Usage | When to Use |
|
| Usage | When to Use |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| `direct` | Reference matches desired output closely |
|
| `direct` | Model sees reference image directly; required if people must appear in output |
|
||||||
| `style` | Extract visual style characteristics only |
|
| `style` | Extract visual style only (not for people who must appear) |
|
||||||
| `palette` | Extract color scheme only |
|
| `palette` | Extract color scheme only |
|
||||||
|
|
||||||
## Verbal Extraction (No File)
|
## Verbal Extraction (No File)
|
||||||
@@ -59,6 +59,19 @@ References are high-priority inputs. Extract **specific, concrete, reproducible*
|
|||||||
|
|
||||||
**Output format**: List each element as bullet that can be copy-pasted into prompt as mandatory instruction.
|
**Output format**: List each element as bullet that can be copy-pasted into prompt as mandatory instruction.
|
||||||
|
|
||||||
|
### Character Analysis ⚠️ If Reference Contains People
|
||||||
|
|
||||||
|
Use `usage: direct` so model sees the reference image. Additionally describe per character: **appearance**, **pose**, **clothing** → with **transformation rules** (stylize to match rendering).
|
||||||
|
|
||||||
|
| Extract | Good | Bad |
|
||||||
|
|---------|------|-----|
|
||||||
|
| Appearance | "Woman: long wavy blonde hair, friendly smile" | "A woman" |
|
||||||
|
| Pose | "Standing, facing camera, confident posture" | "Standing" |
|
||||||
|
| Clothing | "Dark T-shirt, business casual" | "Formal" |
|
||||||
|
| Transform | "Flat-vector cartoon, keep hair color & clothing" | "Make cartoon" |
|
||||||
|
|
||||||
|
Use `usage: direct`. Output each character as MUST/REQUIRED prompt instruction.
|
||||||
|
|
||||||
## Verification Output
|
## Verification Output
|
||||||
|
|
||||||
**For saved files**:
|
**For saved files**:
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { initRenderer, renderMarkdown } from "./renderer.ts";
|
||||||
|
|
||||||
|
const render = (md: string) => {
|
||||||
|
const r = initRenderer();
|
||||||
|
return renderMarkdown(md, r).html;
|
||||||
|
};
|
||||||
|
|
||||||
|
test("bold with inline code (no underscore)", () => {
|
||||||
|
const html = render("**算出 `logits`,算出 `loss`。**");
|
||||||
|
assert.match(html, /<code[^>]*>logits<\/code>/);
|
||||||
|
assert.match(html, /<code[^>]*>loss<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code (contains underscore)", () => {
|
||||||
|
const html = render("**变成 `input_ids`。**");
|
||||||
|
assert.match(html, /<code[^>]*>input_ids<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emphasis with inline code", () => {
|
||||||
|
const html = render("*查看 `hidden_states`*");
|
||||||
|
assert.match(html, /<code[^>]*>hidden_states<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plain inline code (regression)", () => {
|
||||||
|
const html = render("`lm_head`");
|
||||||
|
assert.match(html, /<code[^>]*>lm_head<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold without code (regression)", () => {
|
||||||
|
const html = render("**纯粗体文本**");
|
||||||
|
assert.match(html, /<strong[^>]*>纯粗体文本<\/strong>/);
|
||||||
|
assert.doesNotMatch(html, /<code/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing backticks", () => {
|
||||||
|
const html = render("**``a`b``**");
|
||||||
|
assert.match(html, /<code[^>]*>a`b<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emphasis with inline code containing backticks", () => {
|
||||||
|
const html = render("*``a`b``*");
|
||||||
|
assert.match(html, /<em[^>]*><code[^>]*>a`b<\/code><\/em>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing consecutive backticks", () => {
|
||||||
|
const html = render("**```a``b```**");
|
||||||
|
assert.match(html, /<code[^>]*>a``b<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing only backticks", () => {
|
||||||
|
const html = render("**```` `` ````**");
|
||||||
|
assert.match(html, /<code[^>]*>``<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing only spaces", () => {
|
||||||
|
const oneSpace = render("**`` ``**");
|
||||||
|
assert.match(oneSpace, /<code[^>]*> <\/code>/);
|
||||||
|
|
||||||
|
const twoSpaces = render("**`` ``**");
|
||||||
|
assert.match(twoSpaces, /<code[^>]*> <\/code>/);
|
||||||
|
});
|
||||||
@@ -109,6 +109,13 @@ function parseFrontMatterAndContent(markdownText: string): ParseResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wrapInlineCode(value: string): string {
|
||||||
|
const runs = value.match(/`+/g);
|
||||||
|
const fence = "`".repeat(Math.max(...(runs?.map((run) => run.length) ?? [0])) + 1);
|
||||||
|
const padding = /^ *$/.test(value) ? "" : " ";
|
||||||
|
return `${fence}${padding}${value}${padding}${fence}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function initRenderer(opts: IOpts = {}): RendererAPI {
|
export function initRenderer(opts: IOpts = {}): RendererAPI {
|
||||||
const footnotes: [number, string, string][] = [];
|
const footnotes: [number, string, string][] = [];
|
||||||
let footnoteIndex = 0;
|
let footnoteIndex = 0;
|
||||||
@@ -369,6 +376,7 @@ function preprocessCjkEmphasis(markdown: string): string {
|
|||||||
const tree = processor.parse(markdown);
|
const tree = processor.parse(markdown);
|
||||||
const extractText = (node: any): string => {
|
const extractText = (node: any): string => {
|
||||||
if (node.type === "text") return node.value;
|
if (node.type === "text") return node.value;
|
||||||
|
if (node.type === "inlineCode") return wrapInlineCode(node.value);
|
||||||
if (node.children) return node.children.map(extractText).join("");
|
if (node.children) return node.children.map(extractText).join("");
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { initRenderer, renderMarkdown } from "./renderer.ts";
|
||||||
|
|
||||||
|
const render = (md: string) => {
|
||||||
|
const r = initRenderer();
|
||||||
|
return renderMarkdown(md, r).html;
|
||||||
|
};
|
||||||
|
|
||||||
|
test("bold with inline code (no underscore)", () => {
|
||||||
|
const html = render("**算出 `logits`,算出 `loss`。**");
|
||||||
|
assert.match(html, /<code[^>]*>logits<\/code>/);
|
||||||
|
assert.match(html, /<code[^>]*>loss<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code (contains underscore)", () => {
|
||||||
|
const html = render("**变成 `input_ids`。**");
|
||||||
|
assert.match(html, /<code[^>]*>input_ids<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emphasis with inline code", () => {
|
||||||
|
const html = render("*查看 `hidden_states`*");
|
||||||
|
assert.match(html, /<code[^>]*>hidden_states<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plain inline code (regression)", () => {
|
||||||
|
const html = render("`lm_head`");
|
||||||
|
assert.match(html, /<code[^>]*>lm_head<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold without code (regression)", () => {
|
||||||
|
const html = render("**纯粗体文本**");
|
||||||
|
assert.match(html, /<strong[^>]*>纯粗体文本<\/strong>/);
|
||||||
|
assert.doesNotMatch(html, /<code/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing backticks", () => {
|
||||||
|
const html = render("**``a`b``**");
|
||||||
|
assert.match(html, /<code[^>]*>a`b<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emphasis with inline code containing backticks", () => {
|
||||||
|
const html = render("*``a`b``*");
|
||||||
|
assert.match(html, /<em[^>]*><code[^>]*>a`b<\/code><\/em>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing consecutive backticks", () => {
|
||||||
|
const html = render("**```a``b```**");
|
||||||
|
assert.match(html, /<code[^>]*>a``b<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing only backticks", () => {
|
||||||
|
const html = render("**```` `` ````**");
|
||||||
|
assert.match(html, /<code[^>]*>``<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing only spaces", () => {
|
||||||
|
const oneSpace = render("**`` ``**");
|
||||||
|
assert.match(oneSpace, /<code[^>]*> <\/code>/);
|
||||||
|
|
||||||
|
const twoSpaces = render("**`` ``**");
|
||||||
|
assert.match(twoSpaces, /<code[^>]*> <\/code>/);
|
||||||
|
});
|
||||||
@@ -109,6 +109,13 @@ function parseFrontMatterAndContent(markdownText: string): ParseResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wrapInlineCode(value: string): string {
|
||||||
|
const runs = value.match(/`+/g);
|
||||||
|
const fence = "`".repeat(Math.max(...(runs?.map((run) => run.length) ?? [0])) + 1);
|
||||||
|
const padding = /^ *$/.test(value) ? "" : " ";
|
||||||
|
return `${fence}${padding}${value}${padding}${fence}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function initRenderer(opts: IOpts = {}): RendererAPI {
|
export function initRenderer(opts: IOpts = {}): RendererAPI {
|
||||||
const footnotes: [number, string, string][] = [];
|
const footnotes: [number, string, string][] = [];
|
||||||
let footnoteIndex = 0;
|
let footnoteIndex = 0;
|
||||||
@@ -369,6 +376,7 @@ function preprocessCjkEmphasis(markdown: string): string {
|
|||||||
const tree = processor.parse(markdown);
|
const tree = processor.parse(markdown);
|
||||||
const extractText = (node: any): string => {
|
const extractText = (node: any): string => {
|
||||||
if (node.type === "text") return node.value;
|
if (node.type === "text") return node.value;
|
||||||
|
if (node.type === "inlineCode") return wrapInlineCode(node.value);
|
||||||
if (node.children) return node.children.map(extractText).join("");
|
if (node.children) return node.children.map(extractText).join("");
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { initRenderer, renderMarkdown } from "./renderer.ts";
|
||||||
|
|
||||||
|
const render = (md: string) => {
|
||||||
|
const r = initRenderer();
|
||||||
|
return renderMarkdown(md, r).html;
|
||||||
|
};
|
||||||
|
|
||||||
|
test("bold with inline code (no underscore)", () => {
|
||||||
|
const html = render("**算出 `logits`,算出 `loss`。**");
|
||||||
|
assert.match(html, /<code[^>]*>logits<\/code>/);
|
||||||
|
assert.match(html, /<code[^>]*>loss<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code (contains underscore)", () => {
|
||||||
|
const html = render("**变成 `input_ids`。**");
|
||||||
|
assert.match(html, /<code[^>]*>input_ids<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emphasis with inline code", () => {
|
||||||
|
const html = render("*查看 `hidden_states`*");
|
||||||
|
assert.match(html, /<code[^>]*>hidden_states<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plain inline code (regression)", () => {
|
||||||
|
const html = render("`lm_head`");
|
||||||
|
assert.match(html, /<code[^>]*>lm_head<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold without code (regression)", () => {
|
||||||
|
const html = render("**纯粗体文本**");
|
||||||
|
assert.match(html, /<strong[^>]*>纯粗体文本<\/strong>/);
|
||||||
|
assert.doesNotMatch(html, /<code/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing backticks", () => {
|
||||||
|
const html = render("**``a`b``**");
|
||||||
|
assert.match(html, /<code[^>]*>a`b<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emphasis with inline code containing backticks", () => {
|
||||||
|
const html = render("*``a`b``*");
|
||||||
|
assert.match(html, /<em[^>]*><code[^>]*>a`b<\/code><\/em>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing consecutive backticks", () => {
|
||||||
|
const html = render("**```a``b```**");
|
||||||
|
assert.match(html, /<code[^>]*>a``b<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing only backticks", () => {
|
||||||
|
const html = render("**```` `` ````**");
|
||||||
|
assert.match(html, /<code[^>]*>``<\/code>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bold with inline code containing only spaces", () => {
|
||||||
|
const oneSpace = render("**`` ``**");
|
||||||
|
assert.match(oneSpace, /<code[^>]*> <\/code>/);
|
||||||
|
|
||||||
|
const twoSpaces = render("**`` ``**");
|
||||||
|
assert.match(twoSpaces, /<code[^>]*> <\/code>/);
|
||||||
|
});
|
||||||
@@ -109,6 +109,13 @@ function parseFrontMatterAndContent(markdownText: string): ParseResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wrapInlineCode(value: string): string {
|
||||||
|
const runs = value.match(/`+/g);
|
||||||
|
const fence = "`".repeat(Math.max(...(runs?.map((run) => run.length) ?? [0])) + 1);
|
||||||
|
const padding = /^ *$/.test(value) ? "" : " ";
|
||||||
|
return `${fence}${padding}${value}${padding}${fence}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function initRenderer(opts: IOpts = {}): RendererAPI {
|
export function initRenderer(opts: IOpts = {}): RendererAPI {
|
||||||
const footnotes: [number, string, string][] = [];
|
const footnotes: [number, string, string][] = [];
|
||||||
let footnoteIndex = 0;
|
let footnoteIndex = 0;
|
||||||
@@ -369,6 +376,7 @@ function preprocessCjkEmphasis(markdown: string): string {
|
|||||||
const tree = processor.parse(markdown);
|
const tree = processor.parse(markdown);
|
||||||
const extractText = (node: any): string => {
|
const extractText = (node: any): string => {
|
||||||
if (node.type === "text") return node.value;
|
if (node.type === "text") return node.value;
|
||||||
|
if (node.type === "inlineCode") return wrapInlineCode(node.value);
|
||||||
if (node.children) return node.children.map(extractText).join("");
|
if (node.children) return node.children.map(extractText).join("");
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user