@jianwen-lang/parser 0.1.1
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/LICENSE +21 -0
- package/README.md +95 -0
- package/dist/cli/render.d.ts +1 -0
- package/dist/cli/render.js +300 -0
- package/dist/core/ast.d.ts +286 -0
- package/dist/core/ast.js +2 -0
- package/dist/core/block/rules/heading.d.ts +9 -0
- package/dist/core/block/rules/heading.js +75 -0
- package/dist/core/block/rules/horizontal-rule.d.ts +9 -0
- package/dist/core/block/rules/horizontal-rule.js +87 -0
- package/dist/core/block/rules/include.d.ts +9 -0
- package/dist/core/block/rules/include.js +64 -0
- package/dist/core/block/rules/index.d.ts +2 -0
- package/dist/core/block/rules/index.js +16 -0
- package/dist/core/block/rules/types.d.ts +21 -0
- package/dist/core/block/rules/types.js +2 -0
- package/dist/core/block/types.d.ts +10 -0
- package/dist/core/block/types.js +2 -0
- package/dist/core/block-parser.d.ts +3 -0
- package/dist/core/block-parser.js +1385 -0
- package/dist/core/clone.d.ts +9 -0
- package/dist/core/clone.js +32 -0
- package/dist/core/diagnostics.d.ts +12 -0
- package/dist/core/diagnostics.js +24 -0
- package/dist/core/errors.d.ts +12 -0
- package/dist/core/errors.js +2 -0
- package/dist/core/inline/rules/backtick.d.ts +4 -0
- package/dist/core/inline/rules/backtick.js +90 -0
- package/dist/core/inline/rules/bracket.d.ts +4 -0
- package/dist/core/inline/rules/bracket.js +721 -0
- package/dist/core/inline/rules/disabled.d.ts +2 -0
- package/dist/core/inline/rules/disabled.js +34 -0
- package/dist/core/inline/rules/escape.d.ts +2 -0
- package/dist/core/inline/rules/escape.js +11 -0
- package/dist/core/inline/rules/index.d.ts +2 -0
- package/dist/core/inline/rules/index.js +25 -0
- package/dist/core/inline/rules/marker-highlight.d.ts +4 -0
- package/dist/core/inline/rules/marker-highlight.js +56 -0
- package/dist/core/inline/rules/style.d.ts +4 -0
- package/dist/core/inline/rules/style.js +120 -0
- package/dist/core/inline/rules/types.d.ts +22 -0
- package/dist/core/inline/rules/types.js +2 -0
- package/dist/core/inline/types.d.ts +1 -0
- package/dist/core/inline/types.js +2 -0
- package/dist/core/inline-parser.d.ts +3 -0
- package/dist/core/inline-parser.js +52 -0
- package/dist/core/location.d.ts +9 -0
- package/dist/core/location.js +30 -0
- package/dist/core/parser.d.ts +9 -0
- package/dist/core/parser.js +423 -0
- package/dist/core/traverse.d.ts +7 -0
- package/dist/core/traverse.js +71 -0
- package/dist/html/convert.d.ts +29 -0
- package/dist/html/convert.js +61 -0
- package/dist/html/format.d.ts +1 -0
- package/dist/html/format.js +17 -0
- package/dist/html/render/blocks.d.ts +4 -0
- package/dist/html/render/blocks.js +433 -0
- package/dist/html/render/html.d.ts +8 -0
- package/dist/html/render/html.js +89 -0
- package/dist/html/render/inlines.d.ts +4 -0
- package/dist/html/render/inlines.js +110 -0
- package/dist/html/render/meta.d.ts +3 -0
- package/dist/html/render/meta.js +51 -0
- package/dist/html/render/utils.d.ts +31 -0
- package/dist/html/render/utils.js +213 -0
- package/dist/html/theme/base/css.d.ts +2 -0
- package/dist/html/theme/base/css.js +383 -0
- package/dist/html/theme/dark/css.d.ts +2 -0
- package/dist/html/theme/dark/css.js +108 -0
- package/dist/html/theme/default/colors.d.ts +13 -0
- package/dist/html/theme/default/colors.js +2 -0
- package/dist/html/theme/default/css.d.ts +2 -0
- package/dist/html/theme/default/css.js +6 -0
- package/dist/html/theme/default/runtime.d.ts +0 -0
- package/dist/html/theme/default/runtime.js +31 -0
- package/dist/html/theme/light/css.d.ts +2 -0
- package/dist/html/theme/light/css.js +55 -0
- package/dist/html/theme/preset/colors.d.ts +10 -0
- package/dist/html/theme/preset/colors.js +14 -0
- package/dist/html/theme/runtime.d.ts +0 -0
- package/dist/html/theme/runtime.js +31 -0
- package/dist/html/theme/theme.d.ts +9 -0
- package/dist/html/theme/theme.js +21 -0
- package/dist/lexer/lexer.d.ts +17 -0
- package/dist/lexer/lexer.js +47 -0
- package/dist/parser.d.ts +6 -0
- package/dist/parser.js +22 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jianwen-lang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @jianwen-lang/parser `0.1.0`
|
|
2
|
+
|
|
3
|
+
Jianwen(简文)是一种类似 Markdown 的轻量级标记语言,针对网页博客、公众号等内容发布场景的排版能力进行优化。本包提供 Jianwen 的 **TypeScript 核心解析器**(输出结构化 AST)以及 **HTML 渲染器**,用于在编辑器、渲染服务、静态站点生成等场景复用同一套解析语义。
|
|
4
|
+
|
|
5
|
+
- **环境无关**:核心解析不依赖 DOM,可在 Node.js 与浏览器(经打包)运行
|
|
6
|
+
- **错误容忍**:解析会尽可能继续,并返回诊断信息
|
|
7
|
+
- **可扩展**:通过 AST 节点与规则扩展语法
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i @jianwen-lang/parser
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 快速开始:解析为 AST
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { parseJianwenWithErrors } from '@jianwen-lang/parser';
|
|
19
|
+
|
|
20
|
+
const source = `
|
|
21
|
+
# 标题
|
|
22
|
+
|
|
23
|
+
这里是 *简文* 内容
|
|
24
|
+
`.trim();
|
|
25
|
+
|
|
26
|
+
const { ast, errors } = parseJianwenWithErrors(source);
|
|
27
|
+
if (errors.length > 0) {
|
|
28
|
+
console.warn(errors);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(ast.type); // "document"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
常用类型:
|
|
35
|
+
|
|
36
|
+
- `JianwenDocument`:文档 AST 根节点
|
|
37
|
+
- `ParseError`:诊断信息结构
|
|
38
|
+
|
|
39
|
+
## 渲染:AST -> HTML
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { parseJianwenWithErrors, renderDocumentToHtml } from '@jianwen-lang/parser';
|
|
43
|
+
|
|
44
|
+
const { ast, errors } = parseJianwenWithErrors(source);
|
|
45
|
+
const html = renderDocumentToHtml(ast, { includeMeta: true, format: true });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
如果你希望直接得到完整 HTML 文档(含 `<html>`/`<head>`/`<body>` 与默认 CSS):
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { renderJianwenToHtmlDocument } from '@jianwen-lang/parser';
|
|
52
|
+
|
|
53
|
+
const { html, ast, errors } = renderJianwenToHtmlDocument(source, {
|
|
54
|
+
document: { format: true },
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Include:文件/标签展开(可选)
|
|
59
|
+
|
|
60
|
+
解析阶段支持将 `[@](path)`(文件 include)或 `[@=tag]`(标签 include)按需展开。开启方式:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { parseJianwenWithErrors } from '@jianwen-lang/parser';
|
|
64
|
+
import * as fs from 'node:fs';
|
|
65
|
+
|
|
66
|
+
const { ast, errors } = parseJianwenWithErrors(source, {
|
|
67
|
+
expandInclude: true,
|
|
68
|
+
includeMaxDepth: 8,
|
|
69
|
+
loadFile: (path) => fs.readFileSync(path, 'utf8'),
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
说明:
|
|
74
|
+
|
|
75
|
+
- 若启用 `expandInclude` 但未提供 `loadFile`,文件 include 会保留为 AST 节点并产生 warning。
|
|
76
|
+
- include 解析内置缓存,并带有最大深度与循环检测。
|
|
77
|
+
|
|
78
|
+
## API 概览
|
|
79
|
+
|
|
80
|
+
- 解析
|
|
81
|
+
- `parseJianwen(source, options?) => JianwenDocument`
|
|
82
|
+
- `parseJianwenWithErrors(source, options?) => { ast: JianwenDocument; errors: ParseError[] }`
|
|
83
|
+
- `ParseOptions`:`expandInclude` / `includeMaxDepth` / `loadFile`
|
|
84
|
+
- 渲染
|
|
85
|
+
- `renderDocumentToHtml(doc, options?) => string`
|
|
86
|
+
- `renderJianwenToHtmlDocument(source, options?) => { html; ast; errors }`
|
|
87
|
+
- `buildHtmlDocument(bodyHtml, options?) => string`
|
|
88
|
+
|
|
89
|
+
## 规范与扩展
|
|
90
|
+
|
|
91
|
+
语法与 AST 设计约定请参考项目仓库内的规范文档(例如 `docs/standards.md` 与语言语法文档)。
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT(见 `LICENSE`)。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runRenderCli(argv: string[]): void;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runRenderCli = runRenderCli;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const convert_1 = require("../html/convert");
|
|
40
|
+
const TAB_WIDTH = 4;
|
|
41
|
+
function isCombiningCodePoint(code) {
|
|
42
|
+
return ((code >= 0x0300 && code <= 0x036f) ||
|
|
43
|
+
(code >= 0x1ab0 && code <= 0x1aff) ||
|
|
44
|
+
(code >= 0x1dc0 && code <= 0x1dff) ||
|
|
45
|
+
(code >= 0x20d0 && code <= 0x20ff) ||
|
|
46
|
+
(code >= 0xfe20 && code <= 0xfe2f));
|
|
47
|
+
}
|
|
48
|
+
function isFullWidthCodePoint(code) {
|
|
49
|
+
return (code >= 0x1100 &&
|
|
50
|
+
(code <= 0x115f ||
|
|
51
|
+
code === 0x2329 ||
|
|
52
|
+
code === 0x232a ||
|
|
53
|
+
(code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
|
|
54
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
55
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
56
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
57
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
58
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
59
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
60
|
+
(code >= 0x20000 && code <= 0x3fffd)));
|
|
61
|
+
}
|
|
62
|
+
function getDisplayWidth(value) {
|
|
63
|
+
let width = 0;
|
|
64
|
+
for (const ch of value) {
|
|
65
|
+
if (ch === '\t') {
|
|
66
|
+
const nextStop = TAB_WIDTH - (width % TAB_WIDTH);
|
|
67
|
+
width += nextStop;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const code = ch.codePointAt(0);
|
|
71
|
+
if (code === undefined) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (isCombiningCodePoint(code)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
width += isFullWidthCodePoint(code) ? 2 : 1;
|
|
78
|
+
}
|
|
79
|
+
return width;
|
|
80
|
+
}
|
|
81
|
+
function parseCliArgs(argv) {
|
|
82
|
+
const args = [...argv];
|
|
83
|
+
const inputFilePath = args.shift();
|
|
84
|
+
if (!inputFilePath) {
|
|
85
|
+
throw new Error('Missing input file path');
|
|
86
|
+
}
|
|
87
|
+
let outputFilePath = 'out.html';
|
|
88
|
+
let theme;
|
|
89
|
+
let format = false;
|
|
90
|
+
let includeCss = true;
|
|
91
|
+
let cssHref;
|
|
92
|
+
let includeRuntime = false;
|
|
93
|
+
let runtimeSrc;
|
|
94
|
+
let includeComments = false;
|
|
95
|
+
let includeMeta = true;
|
|
96
|
+
while (args.length > 0) {
|
|
97
|
+
const arg = args.shift();
|
|
98
|
+
if (!arg)
|
|
99
|
+
break;
|
|
100
|
+
if (arg === '-o' || arg === '--out') {
|
|
101
|
+
const value = args.shift();
|
|
102
|
+
if (!value)
|
|
103
|
+
throw new Error('Missing value for --out');
|
|
104
|
+
outputFilePath = value;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (arg === '--theme') {
|
|
108
|
+
const value = args.shift();
|
|
109
|
+
if (value !== 'light' && value !== 'dark' && value !== 'auto') {
|
|
110
|
+
throw new Error('Invalid --theme value (expected light|dark|auto)');
|
|
111
|
+
}
|
|
112
|
+
theme = value;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (arg === '--format') {
|
|
116
|
+
format = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (arg === '--no-css') {
|
|
120
|
+
includeCss = false;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (arg === '--css-href') {
|
|
124
|
+
const value = args.shift();
|
|
125
|
+
if (!value)
|
|
126
|
+
throw new Error('Missing value for --css-href');
|
|
127
|
+
cssHref = value;
|
|
128
|
+
includeCss = true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (arg === '--runtime') {
|
|
132
|
+
includeRuntime = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (arg === '--runtime-src') {
|
|
136
|
+
const value = args.shift();
|
|
137
|
+
if (!value)
|
|
138
|
+
throw new Error('Missing value for --runtime-src');
|
|
139
|
+
runtimeSrc = value;
|
|
140
|
+
includeRuntime = true;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (arg === '--comments') {
|
|
144
|
+
includeComments = true;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (arg === '--no-meta') {
|
|
148
|
+
includeMeta = false;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (arg === '-h' || arg === '--help') {
|
|
152
|
+
console.log(`Usage: jw-render <input.jw> [options]
|
|
153
|
+
|
|
154
|
+
Options:
|
|
155
|
+
-o, --out <file> Output HTML path (default: out.html)
|
|
156
|
+
--theme <light|dark|auto> Set document theme via data-jw-theme
|
|
157
|
+
--format Beautify output HTML
|
|
158
|
+
--no-css Do not inline CSS
|
|
159
|
+
--css-href <href> Link CSS instead of inlining
|
|
160
|
+
--runtime Append runtime <script> tag (default src: ${convert_1.DEFAULT_RUNTIME_SRC})
|
|
161
|
+
--runtime-src <src> Override runtime <script src=...>
|
|
162
|
+
--comments Include comment nodes
|
|
163
|
+
--no-meta Do not render meta header
|
|
164
|
+
`);
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
inputFilePath,
|
|
171
|
+
outputFilePath,
|
|
172
|
+
theme,
|
|
173
|
+
format,
|
|
174
|
+
includeCss,
|
|
175
|
+
cssHref,
|
|
176
|
+
includeRuntime,
|
|
177
|
+
runtimeSrc,
|
|
178
|
+
includeComments,
|
|
179
|
+
includeMeta,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function runRenderCli(argv) {
|
|
183
|
+
const options = parseCliArgs(argv);
|
|
184
|
+
const absoluteInput = path.resolve(process.cwd(), options.inputFilePath);
|
|
185
|
+
const absoluteOutput = path.resolve(process.cwd(), options.outputFilePath);
|
|
186
|
+
if (!fs.existsSync(absoluteInput)) {
|
|
187
|
+
throw new Error(`Input file not found: ${absoluteInput}`);
|
|
188
|
+
}
|
|
189
|
+
const baseDir = path.dirname(absoluteInput);
|
|
190
|
+
const source = fs.readFileSync(absoluteInput, 'utf-8');
|
|
191
|
+
const sourceLines = source.split(/\r?\n/);
|
|
192
|
+
const includeLineCache = new Map();
|
|
193
|
+
const getIncludeLines = (target) => {
|
|
194
|
+
if (includeLineCache.has(target)) {
|
|
195
|
+
return includeLineCache.get(target);
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const includePath = path.resolve(baseDir, target);
|
|
199
|
+
if (!fs.existsSync(includePath)) {
|
|
200
|
+
includeLineCache.set(target, []);
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
const includeSource = fs.readFileSync(includePath, 'utf-8');
|
|
204
|
+
const lines = includeSource.split(/\r?\n/);
|
|
205
|
+
includeLineCache.set(target, lines);
|
|
206
|
+
return lines;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
includeLineCache.set(target, []);
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const loadFile = (target) => {
|
|
214
|
+
try {
|
|
215
|
+
const includePath = path.resolve(baseDir, target);
|
|
216
|
+
if (!fs.existsSync(includePath)) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
return fs.readFileSync(includePath, 'utf-8');
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const resolveInclude = (mode, target) => {
|
|
226
|
+
if (mode !== 'file' || !target.endsWith('.html')) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const includePath = path.resolve(baseDir, target);
|
|
231
|
+
if (!fs.existsSync(includePath)) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
return fs.readFileSync(includePath, 'utf-8');
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const renderOptions = {
|
|
241
|
+
includeMeta: options.includeMeta,
|
|
242
|
+
includeComments: options.includeComments,
|
|
243
|
+
format: options.format,
|
|
244
|
+
documentTheme: options.theme,
|
|
245
|
+
sourceFilePath: absoluteInput,
|
|
246
|
+
outputFilePath: absoluteOutput,
|
|
247
|
+
resolveInclude,
|
|
248
|
+
};
|
|
249
|
+
const documentOptions = {
|
|
250
|
+
includeCss: options.includeCss,
|
|
251
|
+
cssHref: options.cssHref,
|
|
252
|
+
includeRuntime: options.includeRuntime,
|
|
253
|
+
runtimeSrc: options.runtimeSrc,
|
|
254
|
+
format: options.format,
|
|
255
|
+
};
|
|
256
|
+
const result = (0, convert_1.renderJianwenToHtmlDocument)(source, {
|
|
257
|
+
parse: {
|
|
258
|
+
expandInclude: true,
|
|
259
|
+
includeMaxDepth: 10,
|
|
260
|
+
loadFile,
|
|
261
|
+
},
|
|
262
|
+
render: renderOptions,
|
|
263
|
+
document: documentOptions,
|
|
264
|
+
});
|
|
265
|
+
if (result.errors.length > 0) {
|
|
266
|
+
console.error(`Parser reported ${result.errors.length} warning(s)/error(s).`);
|
|
267
|
+
for (const error of result.errors) {
|
|
268
|
+
const includeMatch = /^\[include:([^\]]+)\]\s*/.exec(error.message);
|
|
269
|
+
const includeTarget = includeMatch?.[1];
|
|
270
|
+
const lines = includeTarget ? (getIncludeLines(includeTarget) ?? []) : sourceLines;
|
|
271
|
+
const lineText = lines[error.line - 1];
|
|
272
|
+
const rawColumn = Math.max(1, error.column ?? 1);
|
|
273
|
+
const safeColumn = lineText ? Math.min(rawColumn, lineText.length + 1) : rawColumn;
|
|
274
|
+
const location = `${error.line}:${rawColumn}`;
|
|
275
|
+
console.error(`${error.severity.toUpperCase()} ${location} ${error.message}`);
|
|
276
|
+
if (includeTarget) {
|
|
277
|
+
console.error(` (from include: ${includeTarget})`);
|
|
278
|
+
}
|
|
279
|
+
if (lineText !== undefined) {
|
|
280
|
+
const prefixText = lineText.slice(0, safeColumn - 1);
|
|
281
|
+
const caretPos = getDisplayWidth(prefixText);
|
|
282
|
+
console.error(` ${lineText}`);
|
|
283
|
+
console.error(` ${' '.repeat(caretPos)}^`);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
console.error(' (source line unavailable)');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const hasError = result.errors.some((error) => error.severity === 'error');
|
|
290
|
+
if (hasError) {
|
|
291
|
+
console.error('Render aborted due to parser errors.');
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
fs.writeFileSync(absoluteOutput, result.html, { encoding: 'utf8' });
|
|
296
|
+
console.log(`Rendered ${options.inputFilePath} -> ${options.outputFilePath}`);
|
|
297
|
+
}
|
|
298
|
+
if (require.main === module) {
|
|
299
|
+
runRenderCli(process.argv.slice(2));
|
|
300
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
export interface JianwenDocument {
|
|
2
|
+
type: 'document';
|
|
3
|
+
/** Original input text, optional, for debugging */
|
|
4
|
+
source?: string;
|
|
5
|
+
/** Document metadata parsed from initialization template (optional) */
|
|
6
|
+
meta?: JianwenMeta;
|
|
7
|
+
/** Top-level block list in document order */
|
|
8
|
+
children: BlockNode[];
|
|
9
|
+
}
|
|
10
|
+
export interface JianwenMeta {
|
|
11
|
+
/** [title]= */
|
|
12
|
+
title?: string;
|
|
13
|
+
/** [author]= */
|
|
14
|
+
author?: string;
|
|
15
|
+
/** [author_url]= */
|
|
16
|
+
authorUrl?: string;
|
|
17
|
+
/** [time]= */
|
|
18
|
+
time?: string;
|
|
19
|
+
/** [add_info]= */
|
|
20
|
+
addInfo?: string;
|
|
21
|
+
/** [tag(s)]=, parsed as comma-separated tag array */
|
|
22
|
+
tags?: string[];
|
|
23
|
+
/** [global_font]= font style applied to the entire document */
|
|
24
|
+
globalFont?: ('italic' | 'bold' | 'heavy' | 'slim' | 'serif' | 'mono')[];
|
|
25
|
+
}
|
|
26
|
+
export type ListKind = 'bullet' | 'ordered' | 'task' | 'foldable';
|
|
27
|
+
export type TaskStatus = 'unknown' | 'todo' | 'in_progress' | 'not_done' | 'done';
|
|
28
|
+
export interface ColorAttribute {
|
|
29
|
+
kind: 'preset' | 'hex';
|
|
30
|
+
/** Preset name like 'red' or Hex '#A14A00' */
|
|
31
|
+
value: string;
|
|
32
|
+
}
|
|
33
|
+
export interface InlineAttributes {
|
|
34
|
+
color?: ColorAttribute;
|
|
35
|
+
/** [!color] - used for highlight fill color or link underline color */
|
|
36
|
+
secondaryColor?: ColorAttribute;
|
|
37
|
+
/** [square] / [rounded] / [rounded=num] */
|
|
38
|
+
shape?: 'square' | 'rounded';
|
|
39
|
+
/** [rounded=num] rounded corner radius, default 1.0 */
|
|
40
|
+
roundedRadius?: number;
|
|
41
|
+
/** Font size 1~5, [1]=1em, [2]=1.375em, [3]=1.75em, [4]=2.125em, [5]=2.5em (H1 level) */
|
|
42
|
+
fontSize?: number;
|
|
43
|
+
fontStyle?: ('italic' | 'bold' | 'heavy' | 'slim' | 'serif' | 'mono')[];
|
|
44
|
+
}
|
|
45
|
+
export interface BlockAttributes extends InlineAttributes {
|
|
46
|
+
/** Block content alignment within its own boundary */
|
|
47
|
+
align?: 'left' | 'right' | 'center';
|
|
48
|
+
/** Block starting column position L/C/R */
|
|
49
|
+
position?: 'L' | 'C' | 'R';
|
|
50
|
+
/** Whether to truncate on the left side of current column */
|
|
51
|
+
truncateLeft?: boolean;
|
|
52
|
+
/** Whether to truncate on the right side of current column */
|
|
53
|
+
truncateRight?: boolean;
|
|
54
|
+
/** Whether the current block is collapsed by [fold] */
|
|
55
|
+
fold?: boolean;
|
|
56
|
+
/** Whether the current block displays on the same line as the block above (triggered by [->]) */
|
|
57
|
+
sameLine?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export type BlockNode = ParagraphBlock | HeadingBlock | ContentTitleBlock | QuoteBlock | ListBlock | ListItemBlock | CodeBlock | TableBlock | HorizontalRuleBlock | ImageBlock | HtmlBlock | FootnoteDefBlock | FootnotesBlock | CommentBlock | DisabledBlock | IncludeBlock | TaggedBlock | RawBlock;
|
|
60
|
+
export interface ParagraphBlock {
|
|
61
|
+
type: 'paragraph';
|
|
62
|
+
children: InlineNode[];
|
|
63
|
+
blockAttrs?: BlockAttributes;
|
|
64
|
+
}
|
|
65
|
+
export interface HeadingBlock {
|
|
66
|
+
type: 'heading';
|
|
67
|
+
level: 1 | 2 | 3 | 4 | 5;
|
|
68
|
+
/** Whether it's a foldable heading in `#+` form */
|
|
69
|
+
foldable?: boolean;
|
|
70
|
+
children: InlineNode[];
|
|
71
|
+
blockAttrs?: BlockAttributes;
|
|
72
|
+
}
|
|
73
|
+
export interface ContentTitleBlock {
|
|
74
|
+
type: 'contentTitle';
|
|
75
|
+
children: InlineNode[];
|
|
76
|
+
}
|
|
77
|
+
export interface QuoteBlock {
|
|
78
|
+
type: 'quote';
|
|
79
|
+
/** @ / @@ / @@@ ... nesting level */
|
|
80
|
+
level: number;
|
|
81
|
+
/** Quote content can contain paragraphs, lists, etc. */
|
|
82
|
+
children: BlockNode[];
|
|
83
|
+
blockAttrs?: BlockAttributes;
|
|
84
|
+
}
|
|
85
|
+
export interface ListBlock {
|
|
86
|
+
type: 'list';
|
|
87
|
+
kind: ListKind;
|
|
88
|
+
/** Ordered list number style, reserved for extension */
|
|
89
|
+
orderedStyle?: 'decimal';
|
|
90
|
+
children: ListItemBlock[];
|
|
91
|
+
blockAttrs?: BlockAttributes;
|
|
92
|
+
}
|
|
93
|
+
export interface ListItemBlock {
|
|
94
|
+
type: 'listItem';
|
|
95
|
+
kind: ListKind;
|
|
96
|
+
/** Only valid for ordered/foldable, e.g., "1", "1.1", "1.1.1" */
|
|
97
|
+
ordinal?: string;
|
|
98
|
+
/** Task status: -[] / -[o] / -[x] / -[v] */
|
|
99
|
+
taskStatus?: TaskStatus;
|
|
100
|
+
/** Indent level, based on syntax level (- / -- / + / ++ etc.) */
|
|
101
|
+
indent: number;
|
|
102
|
+
/** List item content can be paragraphs, sublists, etc. */
|
|
103
|
+
children: BlockNode[];
|
|
104
|
+
blockAttrs?: BlockAttributes;
|
|
105
|
+
}
|
|
106
|
+
export interface CodeBlock {
|
|
107
|
+
type: 'code';
|
|
108
|
+
/** Language identifier in ```C */
|
|
109
|
+
language?: string;
|
|
110
|
+
/** Raw code content */
|
|
111
|
+
value: string;
|
|
112
|
+
/** True when preceded by [html] */
|
|
113
|
+
htmlLike?: boolean;
|
|
114
|
+
blockAttrs?: BlockAttributes;
|
|
115
|
+
}
|
|
116
|
+
export interface TableBlock {
|
|
117
|
+
type: 'table';
|
|
118
|
+
rows: TableRow[];
|
|
119
|
+
/** Column alignment */
|
|
120
|
+
align?: ('left' | 'right' | 'center')[];
|
|
121
|
+
/** Attributes like alignment in [sheet,r] */
|
|
122
|
+
blockAttrs?: BlockAttributes;
|
|
123
|
+
}
|
|
124
|
+
export interface TableRow {
|
|
125
|
+
type: 'tableRow';
|
|
126
|
+
cells: TableCell[];
|
|
127
|
+
}
|
|
128
|
+
export interface TableCell {
|
|
129
|
+
type: 'tableCell';
|
|
130
|
+
children: InlineNode[];
|
|
131
|
+
align?: 'left' | 'right' | 'center';
|
|
132
|
+
}
|
|
133
|
+
export interface HorizontalRuleBlock {
|
|
134
|
+
type: 'hr';
|
|
135
|
+
/** Corresponds to ----- / ***** / ===== / ~~~~~ */
|
|
136
|
+
style: 'solid' | 'dashed' | 'bold' | 'wavy';
|
|
137
|
+
/** Supports [color] for advanced line styling */
|
|
138
|
+
colorAttr?: ColorAttribute;
|
|
139
|
+
blockAttrs?: BlockAttributes;
|
|
140
|
+
}
|
|
141
|
+
export interface ImageBlock {
|
|
142
|
+
type: 'image';
|
|
143
|
+
url: string;
|
|
144
|
+
/** Can be represented by content title block */
|
|
145
|
+
title?: string;
|
|
146
|
+
/** [shape] or [rounded,img] */
|
|
147
|
+
shape?: 'square' | 'rounded';
|
|
148
|
+
/** [rounded=num] rounded corner radius, default 1.0 */
|
|
149
|
+
roundedRadius?: number;
|
|
150
|
+
blockAttrs?: BlockAttributes;
|
|
151
|
+
}
|
|
152
|
+
export interface HtmlBlock {
|
|
153
|
+
type: 'html';
|
|
154
|
+
/** If from [html](url), this is the reference path */
|
|
155
|
+
source?: string;
|
|
156
|
+
/** If from [html] + code block, this is the inline HTML text */
|
|
157
|
+
value?: string;
|
|
158
|
+
blockAttrs?: BlockAttributes;
|
|
159
|
+
}
|
|
160
|
+
export interface FootnotesBlock {
|
|
161
|
+
type: 'footnotes';
|
|
162
|
+
children: FootnoteDefBlock[];
|
|
163
|
+
blockAttrs?: BlockAttributes;
|
|
164
|
+
}
|
|
165
|
+
export interface FootnoteDefBlock {
|
|
166
|
+
type: 'footnoteDef';
|
|
167
|
+
/** Corresponds to [fn:id] / [fn=id] */
|
|
168
|
+
id: string;
|
|
169
|
+
/** Footnote content can contain simple blocks */
|
|
170
|
+
children: BlockNode[];
|
|
171
|
+
blockAttrs?: BlockAttributes;
|
|
172
|
+
}
|
|
173
|
+
export interface CommentBlock {
|
|
174
|
+
type: 'commentBlock';
|
|
175
|
+
/** Comment content */
|
|
176
|
+
children: BlockNode[];
|
|
177
|
+
blockAttrs?: BlockAttributes;
|
|
178
|
+
}
|
|
179
|
+
export interface DisabledBlock {
|
|
180
|
+
type: 'disabledBlock';
|
|
181
|
+
/** Raw text, no inline parsing */
|
|
182
|
+
raw: string;
|
|
183
|
+
blockAttrs?: BlockAttributes;
|
|
184
|
+
}
|
|
185
|
+
export interface IncludeBlock {
|
|
186
|
+
type: 'include';
|
|
187
|
+
/**
|
|
188
|
+
* mode: 'file' 对应语法 `[@](path)`,target 为相对路径;
|
|
189
|
+
* mode: 'tag' 对应语法 `[@=name]`,target 为区块标签名称。
|
|
190
|
+
*/
|
|
191
|
+
mode: 'file' | 'tag';
|
|
192
|
+
target: string;
|
|
193
|
+
blockAttrs?: BlockAttributes;
|
|
194
|
+
}
|
|
195
|
+
export interface TaggedBlock {
|
|
196
|
+
type: 'taggedBlock';
|
|
197
|
+
/** Block tag name, e.g., name in [tag=name] / [t=name] / [f=name] */
|
|
198
|
+
name: string;
|
|
199
|
+
/** The tagged block */
|
|
200
|
+
child: BlockNode;
|
|
201
|
+
blockAttrs?: BlockAttributes;
|
|
202
|
+
}
|
|
203
|
+
export interface RawBlock {
|
|
204
|
+
type: 'raw';
|
|
205
|
+
/** Raw text content, fallback for unsupported or reserved syntax */
|
|
206
|
+
value: string;
|
|
207
|
+
blockAttrs?: BlockAttributes;
|
|
208
|
+
}
|
|
209
|
+
export type InlineNode = TextNode | EmphasisNode | StrongNode | UnderlineNode | StrikeNode | WaveNode | SuperscriptNode | SubscriptNode | HighlightNode | LinkNode | FootnoteRefNode | InlineCommentNode | InlineAttrsNode | CodeSpanNode;
|
|
210
|
+
export interface TextNode {
|
|
211
|
+
type: 'text';
|
|
212
|
+
value: string;
|
|
213
|
+
}
|
|
214
|
+
export interface CodeSpanNode {
|
|
215
|
+
type: 'codeSpan';
|
|
216
|
+
value: string;
|
|
217
|
+
}
|
|
218
|
+
export interface EmphasisNode {
|
|
219
|
+
type: 'em';
|
|
220
|
+
children: InlineNode[];
|
|
221
|
+
}
|
|
222
|
+
export interface StrongNode {
|
|
223
|
+
type: 'strong';
|
|
224
|
+
children: InlineNode[];
|
|
225
|
+
}
|
|
226
|
+
export interface UnderlineNode {
|
|
227
|
+
type: 'underline';
|
|
228
|
+
children: InlineNode[];
|
|
229
|
+
}
|
|
230
|
+
export interface StrikeNode {
|
|
231
|
+
type: 'strike';
|
|
232
|
+
children: InlineNode[];
|
|
233
|
+
}
|
|
234
|
+
export interface WaveNode {
|
|
235
|
+
type: 'wave';
|
|
236
|
+
children: InlineNode[];
|
|
237
|
+
}
|
|
238
|
+
export interface SuperscriptNode {
|
|
239
|
+
type: 'sup';
|
|
240
|
+
children: InlineNode[];
|
|
241
|
+
}
|
|
242
|
+
export interface SubscriptNode {
|
|
243
|
+
type: 'sub';
|
|
244
|
+
children: InlineNode[];
|
|
245
|
+
}
|
|
246
|
+
export interface HighlightNode {
|
|
247
|
+
type: 'highlight';
|
|
248
|
+
/**
|
|
249
|
+
* mode: 'frame' 对应语法文档中的成对反引号 ``...`` 框选效果;
|
|
250
|
+
* mode: 'marker' 对应语法文档中的 =...= 荧光笔高亮。
|
|
251
|
+
*/
|
|
252
|
+
mode: 'frame' | 'marker';
|
|
253
|
+
children: InlineNode[];
|
|
254
|
+
/** [color] */
|
|
255
|
+
colorAttr?: ColorAttribute;
|
|
256
|
+
/** [!color] */
|
|
257
|
+
fillColorAttr?: ColorAttribute;
|
|
258
|
+
/** [square] / [rounded] */
|
|
259
|
+
shape?: 'square' | 'rounded';
|
|
260
|
+
/** [rounded=num] */
|
|
261
|
+
roundedRadius?: number;
|
|
262
|
+
}
|
|
263
|
+
export interface LinkNode {
|
|
264
|
+
type: 'link';
|
|
265
|
+
/** url / heading / block tag */
|
|
266
|
+
href: string;
|
|
267
|
+
/** Text content */
|
|
268
|
+
children: InlineNode[];
|
|
269
|
+
colorAttr?: ColorAttribute;
|
|
270
|
+
/** [!color] */
|
|
271
|
+
underlineColorAttr?: ColorAttribute;
|
|
272
|
+
}
|
|
273
|
+
export interface FootnoteRefNode {
|
|
274
|
+
type: 'footnoteRef';
|
|
275
|
+
/** [fn:id] */
|
|
276
|
+
id: string;
|
|
277
|
+
}
|
|
278
|
+
export interface InlineCommentNode {
|
|
279
|
+
type: 'commentInline';
|
|
280
|
+
children: InlineNode[];
|
|
281
|
+
}
|
|
282
|
+
export interface InlineAttrsNode {
|
|
283
|
+
type: 'inlineAttrs';
|
|
284
|
+
attrs: InlineAttributes;
|
|
285
|
+
children: InlineNode[];
|
|
286
|
+
}
|