@stylewx/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core.test.d.ts +2 -0
- package/dist/core.test.d.ts.map +1 -0
- package/dist/core.test.js +107 -0
- package/dist/core.test.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +14 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +71 -0
- package/dist/markdown.js.map +1 -0
- package/dist/reading-time.d.ts +9 -0
- package/dist/reading-time.d.ts.map +1 -0
- package/dist/reading-time.js +38 -0
- package/dist/reading-time.js.map +1 -0
- package/dist/render.d.ts +14 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +46 -0
- package/dist/render.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.test.d.ts","sourceRoot":"","sources":["../src/core.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { markdownToHtml, renderMarkdownToHtml, countWords, estimateReadingMinutes, } from './index.js';
|
|
3
|
+
import { getPresetTheme, compileThemeToCss } from '@stylewx/theme';
|
|
4
|
+
const theme = getPresetTheme('tech-minimal');
|
|
5
|
+
const sampleMarkdown = `# 科技极简示例
|
|
6
|
+
|
|
7
|
+
## 标题二
|
|
8
|
+
|
|
9
|
+
这是一段普通正文,包含 **加粗**、*斜体* 以及 [一个链接](https://example.com)。
|
|
10
|
+
|
|
11
|
+
> 这是一段引用文字。
|
|
12
|
+
|
|
13
|
+
- 列表项一
|
|
14
|
+
- 列表项二
|
|
15
|
+
- 嵌套项
|
|
16
|
+
|
|
17
|
+
1. 有序一
|
|
18
|
+
2. 有序二
|
|
19
|
+
|
|
20
|
+
\`\`\`js
|
|
21
|
+
const x = 1
|
|
22
|
+
console.log(x)
|
|
23
|
+
\`\`\`
|
|
24
|
+
|
|
25
|
+
| 列A | 列B |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| 1 | 2 |
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
结尾段落。
|
|
34
|
+
`;
|
|
35
|
+
describe('markdownToHtml', () => {
|
|
36
|
+
it('把 Markdown 转成 HTML,保留 GFM 语法', () => {
|
|
37
|
+
const html = markdownToHtml('# Hello\n\n- a\n- b\n\n| x | y |\n| - | - |\n| 1 | 2 |');
|
|
38
|
+
expect(html).toContain('<h1>');
|
|
39
|
+
expect(html).toContain('<li>');
|
|
40
|
+
expect(html).toContain('<table>');
|
|
41
|
+
expect(html).toContain('<td');
|
|
42
|
+
});
|
|
43
|
+
it('允许内嵌 HTML(交由 validator 负责安全校验)', () => {
|
|
44
|
+
const html = markdownToHtml('text <script>alert(1)</script>');
|
|
45
|
+
expect(html).toContain('<script>alert(1)</script>');
|
|
46
|
+
});
|
|
47
|
+
it('容错:`##无空格` 也识别为标题(CommonMark 需空格,这里 lenient)', () => {
|
|
48
|
+
expect(markdownToHtml('##照相')).toContain('<h2>');
|
|
49
|
+
expect(markdownToHtml('##照相')).toContain('照相');
|
|
50
|
+
// 已有空格 / 纯 # 不受影响
|
|
51
|
+
expect(markdownToHtml('## 正常')).toContain('<h2>');
|
|
52
|
+
const h6 = markdownToHtml('######深标题');
|
|
53
|
+
expect(h6).toContain('<h6>');
|
|
54
|
+
});
|
|
55
|
+
it('代码围栏内的 `##` 行不被误判为标题', () => {
|
|
56
|
+
const md = '```\n##not-a-heading\n```';
|
|
57
|
+
const html = markdownToHtml(md);
|
|
58
|
+
expect(html).not.toContain('<h2>');
|
|
59
|
+
expect(html).toContain('##not-a-heading');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('renderMarkdownToHtml', () => {
|
|
63
|
+
it('输出不含 <style> / <link> / class 依赖(属性内联率 100%)', () => {
|
|
64
|
+
const { html } = renderMarkdownToHtml(sampleMarkdown, theme);
|
|
65
|
+
expect(html).not.toContain('<style');
|
|
66
|
+
expect(html).not.toContain('<link');
|
|
67
|
+
expect(html).not.toContain('class=');
|
|
68
|
+
});
|
|
69
|
+
it('主题覆盖的 block 元素均携带内联 style', () => {
|
|
70
|
+
const { html } = renderMarkdownToHtml(sampleMarkdown, theme);
|
|
71
|
+
// 这些 block 一定出现在示例中(除 img 外都应有 style)
|
|
72
|
+
for (const tag of ['h1', 'h2', 'p', 'strong', 'a', 'blockquote', 'li', 'pre', 'code', 'hr', 'img']) {
|
|
73
|
+
expect(html.includes(`<${tag}`) && new RegExp(`<${tag}[^>]*style=`).test(html), `元素 <${tag}> 应带内联 style`).toBe(true);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
it('token 引用在最终输出中已被解析(无残留 {{ }})', () => {
|
|
77
|
+
const { html } = renderMarkdownToHtml(sampleMarkdown, theme);
|
|
78
|
+
expect(html).not.toContain('{{');
|
|
79
|
+
});
|
|
80
|
+
it('主题 CSS 编译结果可被 validator 白名单复验', () => {
|
|
81
|
+
const css = compileThemeToCss(theme);
|
|
82
|
+
expect(css).toContain('#0b6bff');
|
|
83
|
+
expect(css).not.toContain('position');
|
|
84
|
+
});
|
|
85
|
+
it('非法主题抛错', () => {
|
|
86
|
+
const bad = structuredClone(theme);
|
|
87
|
+
bad.blocks.p['position'] = 'absolute';
|
|
88
|
+
expect(() => renderMarkdownToHtml(sampleMarkdown, bad)).toThrow(/position|过滤/);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('快照测试', () => {
|
|
92
|
+
it('固定 Markdown + 固定主题 → 固定 HTML 结构', () => {
|
|
93
|
+
const { html } = renderMarkdownToHtml('# 快照标题\n\n正文一段,**加粗**。\n\n- 项一\n\n> 引文\n\n```\ncode\n```', theme);
|
|
94
|
+
expect(html).toMatchSnapshot();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('reading time', () => {
|
|
98
|
+
it('统计字数', () => {
|
|
99
|
+
expect(countWords('# 你好世界 hello world')).toBe(6);
|
|
100
|
+
expect(countWords('短文本')).toBe(3);
|
|
101
|
+
});
|
|
102
|
+
it('估算阅读时长至少 1 分钟', () => {
|
|
103
|
+
expect(estimateReadingMinutes('短文本')).toBe(1);
|
|
104
|
+
expect(estimateReadingMinutes('word '.repeat(4000))).toBe(12);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
//# sourceMappingURL=core.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.test.js","sourceRoot":"","sources":["../src/core.test.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 容错:把「#后直接跟非空格」的行规范成合法 ATX 标题。
|
|
3
|
+
* CommonMark 要求 `## 标题`(# 后要有空格)才算标题;但中文公众号写作常写 `##标题`(无空格),
|
|
4
|
+
* 未规范时会被当成普通段落。这里在解析前把 `#{1,6}` 后紧跟非空格的字符前补一个空格,
|
|
5
|
+
* 使其成为合法标题。跳过 ``` / ~~~ 代码围栏与缩进代码,避免误伤代码。
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeNoSpaceAtxHeadings(markdown: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* 把 Markdown 字符串转换为嵌套的 HTML 片段(不含根容器)。
|
|
10
|
+
* @param markdown 文章 Markdown
|
|
11
|
+
* @returns HTML 字符串,例如 `<h1>…</h1><p>…</p>`
|
|
12
|
+
*/
|
|
13
|
+
export declare function markdownToHtml(markdown: string): string;
|
|
14
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAoCA;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiBpE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWvD"}
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → HTML 转换管线(unified / remark / rehype)。
|
|
3
|
+
* 纯函数、无 DOM / Node 依赖,可同构。
|
|
4
|
+
* 保留 GFM 表格/删除线/任务列表;允许内嵌 HTML(由下游 validator 负责安全校验)。
|
|
5
|
+
* 渲染时会移除所有 `class` 属性 —— 输出完全依赖内联样式,不依赖任何 class 选择器。
|
|
6
|
+
*/
|
|
7
|
+
import { unified } from 'unified';
|
|
8
|
+
import remarkParse from 'remark-parse';
|
|
9
|
+
import remarkGfm from 'remark-gfm';
|
|
10
|
+
import remarkRehype from 'remark-rehype';
|
|
11
|
+
import rehypeRaw from 'rehype-raw';
|
|
12
|
+
import rehypeStringify from 'rehype-stringify';
|
|
13
|
+
/** 递归移除所有节点上的 className(去除 language-* 等,避免任何 class 依赖)。 */
|
|
14
|
+
function stripClassNames(node) {
|
|
15
|
+
if (node.properties && 'className' in node.properties) {
|
|
16
|
+
delete node.properties.className;
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(node.children)) {
|
|
19
|
+
for (const child of node.children)
|
|
20
|
+
stripClassNames(child);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function rehypeRemoveClassPlugin() {
|
|
24
|
+
return (tree) => {
|
|
25
|
+
stripClassNames(tree);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 容错:把「#后直接跟非空格」的行规范成合法 ATX 标题。
|
|
30
|
+
* CommonMark 要求 `## 标题`(# 后要有空格)才算标题;但中文公众号写作常写 `##标题`(无空格),
|
|
31
|
+
* 未规范时会被当成普通段落。这里在解析前把 `#{1,6}` 后紧跟非空格的字符前补一个空格,
|
|
32
|
+
* 使其成为合法标题。跳过 ``` / ~~~ 代码围栏与缩进代码,避免误伤代码。
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeNoSpaceAtxHeadings(markdown) {
|
|
35
|
+
const lines = markdown.split(/\r?\n/);
|
|
36
|
+
let inFence = false;
|
|
37
|
+
return lines
|
|
38
|
+
.map((line) => {
|
|
39
|
+
// 代码围栏(``` 或 ~~~,最多 3 空格缩进):围栏行切换状态,围栏内原样保留
|
|
40
|
+
if (/^\s{0,3}(```|~~~)/.test(line)) {
|
|
41
|
+
inFence = !inFence;
|
|
42
|
+
return line;
|
|
43
|
+
}
|
|
44
|
+
if (inFence)
|
|
45
|
+
return line;
|
|
46
|
+
// 有前置空格的缩进代码 / 其它块不处理(ATX 标题不能缩进)
|
|
47
|
+
if (/^(\s|\t)/.test(line))
|
|
48
|
+
return line;
|
|
49
|
+
// 在 `#` 与首个非空格、非#字符之间补空格 -> 合法 ATX 标题;已有空格 / 纯# / 超长#串不受影响
|
|
50
|
+
return line.replace(/^(#{1,6})([^\s#])/, '$1 $2');
|
|
51
|
+
})
|
|
52
|
+
.join('\n');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 把 Markdown 字符串转换为嵌套的 HTML 片段(不含根容器)。
|
|
56
|
+
* @param markdown 文章 Markdown
|
|
57
|
+
* @returns HTML 字符串,例如 `<h1>…</h1><p>…</p>`
|
|
58
|
+
*/
|
|
59
|
+
export function markdownToHtml(markdown) {
|
|
60
|
+
const normalized = normalizeNoSpaceAtxHeadings(markdown);
|
|
61
|
+
const file = unified()
|
|
62
|
+
.use(remarkParse)
|
|
63
|
+
.use(remarkGfm)
|
|
64
|
+
.use(remarkRehype, { allowDangerousHtml: true })
|
|
65
|
+
.use(rehypeRaw)
|
|
66
|
+
.use(rehypeRemoveClassPlugin)
|
|
67
|
+
.use(rehypeStringify)
|
|
68
|
+
.processSync(normalized);
|
|
69
|
+
return String(file);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=markdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,WAAW,MAAM,cAAc,CAAA;AACtC,OAAO,SAAS,MAAM,YAAY,CAAA;AAClC,OAAO,YAAY,MAAM,eAAe,CAAA;AACxC,OAAO,SAAS,MAAM,YAAY,CAAA;AAClC,OAAO,eAAe,MAAM,kBAAkB,CAAA;AAS9C,2DAA2D;AAC3D,SAAS,eAAe,CAAC,IAAc;IACrC,IAAI,IAAI,CAAC,UAAU,IAAI,WAAW,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACtD,OAAO,IAAI,CAAC,UAAU,CAAC,SAAS,CAAA;IAClC,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ;YAAE,eAAe,CAAC,KAAK,CAAC,CAAA;IAC3D,CAAC;AACH,CAAC;AAED,SAAS,uBAAuB;IAC9B,OAAO,CAAC,IAAc,EAAQ,EAAE;QAC9B,eAAe,CAAC,IAAI,CAAC,CAAA;IACvB,CAAC,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,2BAA2B,CAAC,QAAgB;IAC1D,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IACrC,IAAI,OAAO,GAAG,KAAK,CAAA;IACnB,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACZ,4CAA4C;QAC5C,IAAI,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,OAAO,GAAG,CAAC,OAAO,CAAA;YAClB,OAAO,IAAI,CAAA;QACb,CAAC;QACD,IAAI,OAAO;YAAE,OAAO,IAAI,CAAA;QACxB,kCAAkC;QAClC,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAA;QACtC,2DAA2D;QAC3D,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,OAAO,CAAC,CAAA;IACnD,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,MAAM,UAAU,GAAG,2BAA2B,CAAC,QAAQ,CAAC,CAAA;IACxD,MAAM,IAAI,GAAG,OAAO,EAAE;SACnB,GAAG,CAAC,WAAW,CAAC;SAChB,GAAG,CAAC,SAAS,CAAC;SACd,GAAG,CAAC,YAAY,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC;SAC/C,GAAG,CAAC,SAAS,CAAC;SACd,GAAG,CAAC,uBAAuB,CAAC;SAC5B,GAAG,CAAC,eAAe,CAAC;SACpB,WAAW,CAAC,UAAU,CAAC,CAAA;IAC1B,OAAO,MAAM,CAAC,IAAI,CAAC,CAAA;AACrB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把「标记的正文时间」估算出来 —— 中英文混合的阅读时长估算。
|
|
3
|
+
* 纯函数,无 DOM / Node 依赖。
|
|
4
|
+
*/
|
|
5
|
+
/** 去掉所有空白与 Markdown 语法符号,返回纯文本字数。 */
|
|
6
|
+
export declare function countWords(markdown: string): number;
|
|
7
|
+
/** 估算阅读时长(分钟)。手机端中文约 350 字/分钟。 */
|
|
8
|
+
export declare function estimateReadingMinutes(markdown: string): number;
|
|
9
|
+
//# sourceMappingURL=reading-time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reading-time.d.ts","sourceRoot":"","sources":["../src/reading-time.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qCAAqC;AACrC,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQnD;AAED,kCAAkC;AAClC,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI/D"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把「标记的正文时间」估算出来 —— 中英文混合的阅读时长估算。
|
|
3
|
+
* 纯函数,无 DOM / Node 依赖。
|
|
4
|
+
*/
|
|
5
|
+
/** 去掉所有空白与 Markdown 语法符号,返回纯文本字数。 */
|
|
6
|
+
export function countWords(markdown) {
|
|
7
|
+
const stripped = removeMarkdownSyntax(markdown);
|
|
8
|
+
// 中文按字符计(大约 1 字 = 1 词),英文按单词计。
|
|
9
|
+
const cjk = stripped.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g)?.length ?? 0;
|
|
10
|
+
const latin = stripped
|
|
11
|
+
.replace(/[\u4e00-\u9fff\u3400-\u4dbf]/g, ' ')
|
|
12
|
+
.match(/[a-zA-Z0-9]+/g)?.length ?? 0;
|
|
13
|
+
return cjk + latin;
|
|
14
|
+
}
|
|
15
|
+
/** 估算阅读时长(分钟)。手机端中文约 350 字/分钟。 */
|
|
16
|
+
export function estimateReadingMinutes(markdown) {
|
|
17
|
+
const words = countWords(markdown);
|
|
18
|
+
if (words <= 0)
|
|
19
|
+
return 1;
|
|
20
|
+
return Math.max(1, Math.ceil(words / 350));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 简单剥离 Markdown 语法(用于字数统计)。尽可能逼近纯文本。
|
|
24
|
+
*/
|
|
25
|
+
function removeMarkdownSyntax(markdown) {
|
|
26
|
+
return markdown
|
|
27
|
+
.replace(/```[\s\S]*?```/g, ' ') // 代码块
|
|
28
|
+
.replace(/`[^`]*`/g, ' ') // 内联代码
|
|
29
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1') // 图片
|
|
30
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // 链接
|
|
31
|
+
.replace(/^\s{1,4}#{1,6}\s+/gm, ' ') // 标题
|
|
32
|
+
.replace(/^\s*[-*+]\s+/gm, ' ') // 列表
|
|
33
|
+
.replace(/^\s*>\s?/gm, ' ') // 引用
|
|
34
|
+
.replace(/[*_~]{1,3}([^*_~]+)[*_~]{1,3}/g, '$1') // 强调 / 删除线
|
|
35
|
+
.replace(/\|/g, ' ') // 表格分隔
|
|
36
|
+
.replace(/\s+/g, ' ');
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=reading-time.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reading-time.js","sourceRoot":"","sources":["../src/reading-time.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qCAAqC;AACrC,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,MAAM,QAAQ,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAA;IAC/C,+BAA+B;IAC/B,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,+BAA+B,CAAC,EAAE,MAAM,IAAI,CAAC,CAAA;IACxE,MAAM,KAAK,GAAG,QAAQ;SACnB,OAAO,CAAC,+BAA+B,EAAE,GAAG,CAAC;SAC7C,KAAK,CAAC,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC,CAAA;IACtC,OAAO,GAAG,GAAG,KAAK,CAAA;AACpB,CAAC;AAED,kCAAkC;AAClC,MAAM,UAAU,sBAAsB,CAAC,QAAgB;IACrD,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAA;IAClC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,CAAC,CAAA;IACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,CAAA;AAC5C,CAAC;AAED;;GAEG;AACH,SAAS,oBAAoB,CAAC,QAAgB;IAC5C,OAAO,QAAQ;SACZ,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC,MAAM;SACtC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,OAAO;SAChC,OAAO,CAAC,yBAAyB,EAAE,IAAI,CAAC,CAAC,KAAK;SAC9C,OAAO,CAAC,wBAAwB,EAAE,IAAI,CAAC,CAAC,KAAK;SAC7C,OAAO,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC,KAAK;SACzC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC,KAAK;SACpC,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC,KAAK;SAChC,OAAO,CAAC,gCAAgC,EAAE,IAAI,CAAC,CAAC,WAAW;SAC3D,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO;SAC3B,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AACzB,CAAC"}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Theme } from '@stylewx/theme';
|
|
2
|
+
export interface RenderResult {
|
|
3
|
+
/** 最终可直接粘贴进微信公众号的 HTML:全部样式已内联,无 <style>/<link>/class 依赖。 */
|
|
4
|
+
html: string;
|
|
5
|
+
/** 实际使用的主题(已通过 schema + 微信白名单校验)。 */
|
|
6
|
+
theme: Theme;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 渲染一篇 Markdown 为微信兼容的内联样式 HTML。
|
|
10
|
+
* @param markdown 文章 Markdown
|
|
11
|
+
* @param theme 已通过 themeSchema 校验的主题
|
|
12
|
+
*/
|
|
13
|
+
export declare function renderMarkdownToHtml(markdown: string, theme: Theme): RenderResult;
|
|
14
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAG3C,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAA;IACZ,qCAAqC;IACrC,KAAK,EAAE,KAAK,CAAA;CACb;AAuBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,YAAY,CAcjF"}
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 无头排版内核:Markdown → 微信兼容的内联样式 HTML。
|
|
3
|
+
* 组合 unified/remark/rehype 渲染与 juice 内联,纯函数、无 DOM / Node 独有 API。
|
|
4
|
+
*/
|
|
5
|
+
import juice from 'juice';
|
|
6
|
+
import { compileRootBaseStyle, compileThemeToCss, validateTheme, } from '@stylewx/theme';
|
|
7
|
+
import { markdownToHtml } from './markdown.js';
|
|
8
|
+
/** 禁止 juice 去抓取任何外部资源(保持 headless、无网络、可复现)。 */
|
|
9
|
+
function juiceOptions(theme) {
|
|
10
|
+
return {
|
|
11
|
+
extraCss: compileThemeToCss(theme),
|
|
12
|
+
applyStyleTags: false,
|
|
13
|
+
removeStyleTags: true,
|
|
14
|
+
insertPreservedExtraCss: false,
|
|
15
|
+
preserveMediaQueries: false,
|
|
16
|
+
preserveFontFaces: false,
|
|
17
|
+
preserveKeyFrames: false,
|
|
18
|
+
preservePseudos: false,
|
|
19
|
+
preserveImportant: false,
|
|
20
|
+
webResources: {
|
|
21
|
+
links: false,
|
|
22
|
+
scripts: false,
|
|
23
|
+
images: false,
|
|
24
|
+
svgs: false,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 渲染一篇 Markdown 为微信兼容的内联样式 HTML。
|
|
30
|
+
* @param markdown 文章 Markdown
|
|
31
|
+
* @param theme 已通过 themeSchema 校验的主题
|
|
32
|
+
*/
|
|
33
|
+
export function renderMarkdownToHtml(markdown, theme) {
|
|
34
|
+
const validation = validateTheme(theme);
|
|
35
|
+
if (!validation.ok || !validation.theme) {
|
|
36
|
+
const detail = validation.issues.map((i) => `${i.path}: ${i.message}`).join(';');
|
|
37
|
+
throw new Error(`主题不合法,无法渲染:${detail}`);
|
|
38
|
+
}
|
|
39
|
+
const safeTheme = validation.theme;
|
|
40
|
+
const bodyHtml = markdownToHtml(markdown);
|
|
41
|
+
const baseStyle = compileRootBaseStyle(safeTheme);
|
|
42
|
+
const wrapped = `<section style="${baseStyle}">${bodyHtml}</section>`;
|
|
43
|
+
const html = juice(wrapped, juiceOptions(safeTheme));
|
|
44
|
+
return { html, theme: safeTheme };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,GACd,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAS9C,+CAA+C;AAC/C,SAAS,YAAY,CAAC,KAAY;IAChC,OAAO;QACL,QAAQ,EAAE,iBAAiB,CAAC,KAAK,CAAC;QAClC,cAAc,EAAE,KAAK;QACrB,eAAe,EAAE,IAAI;QACrB,uBAAuB,EAAE,KAAK;QAC9B,oBAAoB,EAAE,KAAK;QAC3B,iBAAiB,EAAE,KAAK;QACxB,iBAAiB,EAAE,KAAK;QACxB,eAAe,EAAE,KAAK;QACtB,iBAAiB,EAAE,KAAK;QACxB,YAAY,EAAE;YACZ,KAAK,EAAE,KAAK;YACZ,OAAO,EAAE,KAAK;YACd,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,KAAK;SACZ;KACF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAE,KAAY;IACjE,MAAM,UAAU,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA;IACvC,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACxC,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAChF,MAAM,IAAI,KAAK,CAAC,cAAc,MAAM,EAAE,CAAC,CAAA;IACzC,CAAC;IACD,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,CAAA;IAElC,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAA;IACzC,MAAM,SAAS,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAA;IACjD,MAAM,OAAO,GAAG,mBAAmB,SAAS,KAAK,QAAQ,YAAY,CAAA;IACrE,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,CAAA;IAEpD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;AACnC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stylewx/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "stylewx headless render core: Markdown → WeChat-compatible inlined-style HTML. Pure function, no DOM / no Node-only APIs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
23
|
+
"prepublishOnly": "pnpm build"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@stylewx/theme": "workspace:*",
|
|
27
|
+
"juice": "^11.0.1",
|
|
28
|
+
"rehype-raw": "^7.0.0",
|
|
29
|
+
"rehype-stringify": "^10.0.1",
|
|
30
|
+
"remark-gfm": "^4.0.1",
|
|
31
|
+
"remark-parse": "^11.0.0",
|
|
32
|
+
"remark-rehype": "^11.1.1",
|
|
33
|
+
"unified": "^11.0.5"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
}
|
|
38
|
+
}
|