@jianwen-lang/parser 0.1.1 → 0.1.3
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/README.md +44 -1
- package/dist/cli/render.js +75 -0
- package/dist/core/block/rules/attribute-line.d.ts +13 -0
- package/dist/core/block/rules/attribute-line.js +227 -0
- package/dist/core/block/rules/code-block.d.ts +2 -0
- package/dist/core/block/rules/code-block.js +73 -0
- package/dist/core/block/rules/code-fence.d.ts +15 -0
- package/dist/core/block/rules/code-fence.js +37 -0
- package/dist/core/block/rules/content-title.d.ts +12 -0
- package/dist/core/block/rules/content-title.js +70 -0
- package/dist/core/block/rules/footnotes.d.ts +9 -0
- package/dist/core/block/rules/footnotes.js +105 -0
- package/dist/core/block/rules/html.d.ts +7 -0
- package/dist/core/block/rules/html.js +48 -0
- package/dist/core/block/rules/image.d.ts +9 -0
- package/dist/core/block/rules/image.js +78 -0
- package/dist/core/block/rules/list.d.ts +3 -0
- package/dist/core/block/rules/list.js +275 -0
- package/dist/core/block/rules/paragraph.d.ts +6 -0
- package/dist/core/block/rules/paragraph.js +55 -0
- package/dist/core/block/rules/quote.d.ts +13 -0
- package/dist/core/block/rules/quote.js +104 -0
- package/dist/core/block/rules/table.d.ts +2 -0
- package/dist/core/block/rules/table.js +199 -0
- package/dist/core/block/runtime.d.ts +25 -0
- package/dist/core/block/runtime.js +125 -0
- package/dist/core/block/types.d.ts +2 -0
- package/dist/core/block-parser.js +186 -1321
- package/dist/core/parser.js +1 -1
- package/dist/html/convert.d.ts +10 -0
- package/dist/html/convert.js +23 -1
- package/dist/html/render/blocks.d.ts +15 -0
- package/dist/html/render/blocks.js +67 -13
- package/dist/html/render/html.d.ts +16 -0
- package/dist/html/render/html.js +53 -14
- package/dist/html/render/utils.d.ts +1 -0
- package/dist/html/theme/base/css.d.ts +1 -1
- package/dist/html/theme/base/css.js +50 -22
- package/dist/html/theme/dark/css.js +48 -0
- package/dist/html/theme/light/css.js +24 -0
- package/dist/html/theme/theme.d.ts +23 -1
- package/dist/html/theme/theme.js +242 -2
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @jianwen-lang/parser `0.1.
|
|
1
|
+
# @jianwen-lang/parser `0.1.2`
|
|
2
2
|
|
|
3
3
|
Jianwen(简文)是一种类似 Markdown 的轻量级标记语言,针对网页博客、公众号等内容发布场景的排版能力进行优化。本包提供 Jianwen 的 **TypeScript 核心解析器**(输出结构化 AST)以及 **HTML 渲染器**,用于在编辑器、渲染服务、静态站点生成等场景复用同一套解析语义。
|
|
4
4
|
|
|
@@ -55,6 +55,19 @@ const { html, ast, errors } = renderJianwenToHtmlDocument(source, {
|
|
|
55
55
|
});
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
如果你在做编辑器并需要“渲染区块 ↔ 源码行范围”映射(例如拖拽区块后回写源码),可以使用:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { renderJianwenToHtmlDocumentWithBlockMap } from '@jianwen-lang/parser';
|
|
62
|
+
|
|
63
|
+
const result = renderJianwenToHtmlDocumentWithBlockMap(source, {
|
|
64
|
+
document: { format: true },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
console.log(result.groups);
|
|
68
|
+
// [{ id, kind, startLine, endLine, readOnly, origin? }, ...]
|
|
69
|
+
```
|
|
70
|
+
|
|
58
71
|
## Include:文件/标签展开(可选)
|
|
59
72
|
|
|
60
73
|
解析阶段支持将 `[@](path)`(文件 include)或 `[@=tag]`(标签 include)按需展开。开启方式:
|
|
@@ -83,8 +96,38 @@ const { ast, errors } = parseJianwenWithErrors(source, {
|
|
|
83
96
|
- `ParseOptions`:`expandInclude` / `includeMaxDepth` / `loadFile`
|
|
84
97
|
- 渲染
|
|
85
98
|
- `renderDocumentToHtml(doc, options?) => string`
|
|
99
|
+
- `renderDocumentToHtmlWithBlockMap(doc, options?) => { html; groups }`
|
|
86
100
|
- `renderJianwenToHtmlDocument(source, options?) => { html; ast; errors }`
|
|
101
|
+
- `renderJianwenToHtmlDocumentWithBlockMap(source, options?) => { html; ast; errors; groups }`
|
|
87
102
|
- `buildHtmlDocument(bodyHtml, options?) => string`
|
|
103
|
+
- `composeThemeCss(options?) => string`(`preset: 'default' | 'none'`,支持 `theme` token 对象与 `extraCss` 追加覆盖)
|
|
104
|
+
- `renderThemeTokenCss(theme) => string`
|
|
105
|
+
- `JIANWEN_THEME_TOKEN_KEYS` / `JianwenThemeConfig` / `JianwenThemeTokenValues`
|
|
106
|
+
|
|
107
|
+
## 主题对象(Token)注入
|
|
108
|
+
|
|
109
|
+
`JianwenThemeConfig` 支持局部覆盖:`light` / `dark` 可只写需要改动的 token,未提供项自动回退到默认主题。
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import {
|
|
113
|
+
composeThemeCss,
|
|
114
|
+
} from '@jianwen-lang/parser';
|
|
115
|
+
|
|
116
|
+
const css = composeThemeCss({
|
|
117
|
+
preset: 'default',
|
|
118
|
+
theme: {
|
|
119
|
+
light: { '--jw-strong-color': '#0b766e' },
|
|
120
|
+
dark: { '--jw-strong-color': '#79e9dc' },
|
|
121
|
+
includeAutoDark: true,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
CLI 也支持通过 `--theme-file` 注入 JSON 文件(同样支持局部覆盖):
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
jw-render input.jw --out out.html --theme-file ./theme.json
|
|
130
|
+
```
|
|
88
131
|
|
|
89
132
|
## 规范与扩展
|
|
90
133
|
|
package/dist/cli/render.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.runRenderCli = runRenderCli;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
+
const theme_1 = require("../html/theme/theme");
|
|
39
40
|
const convert_1 = require("../html/convert");
|
|
40
41
|
const TAB_WIDTH = 4;
|
|
41
42
|
function isCombiningCodePoint(code) {
|
|
@@ -89,6 +90,7 @@ function parseCliArgs(argv) {
|
|
|
89
90
|
let format = false;
|
|
90
91
|
let includeCss = true;
|
|
91
92
|
let cssHref;
|
|
93
|
+
let themeFilePath;
|
|
92
94
|
let includeRuntime = false;
|
|
93
95
|
let runtimeSrc;
|
|
94
96
|
let includeComments = false;
|
|
@@ -128,6 +130,14 @@ function parseCliArgs(argv) {
|
|
|
128
130
|
includeCss = true;
|
|
129
131
|
continue;
|
|
130
132
|
}
|
|
133
|
+
if (arg === '--theme-file') {
|
|
134
|
+
const value = args.shift();
|
|
135
|
+
if (!value)
|
|
136
|
+
throw new Error('Missing value for --theme-file');
|
|
137
|
+
themeFilePath = value;
|
|
138
|
+
includeCss = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
131
141
|
if (arg === '--runtime') {
|
|
132
142
|
includeRuntime = true;
|
|
133
143
|
continue;
|
|
@@ -157,6 +167,7 @@ Options:
|
|
|
157
167
|
--format Beautify output HTML
|
|
158
168
|
--no-css Do not inline CSS
|
|
159
169
|
--css-href <href> Link CSS instead of inlining
|
|
170
|
+
--theme-file <path> Load Jianwen theme token JSON for CSS generation
|
|
160
171
|
--runtime Append runtime <script> tag (default src: ${convert_1.DEFAULT_RUNTIME_SRC})
|
|
161
172
|
--runtime-src <src> Override runtime <script src=...>
|
|
162
173
|
--comments Include comment nodes
|
|
@@ -173,12 +184,72 @@ Options:
|
|
|
173
184
|
format,
|
|
174
185
|
includeCss,
|
|
175
186
|
cssHref,
|
|
187
|
+
themeFilePath,
|
|
176
188
|
includeRuntime,
|
|
177
189
|
runtimeSrc,
|
|
178
190
|
includeComments,
|
|
179
191
|
includeMeta,
|
|
180
192
|
};
|
|
181
193
|
}
|
|
194
|
+
function validateThemeTokenMap(filePath, section, candidate) {
|
|
195
|
+
if (candidate === undefined) {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
199
|
+
throw new Error(`Theme file ${filePath} has invalid "${section}" section (expected object).`);
|
|
200
|
+
}
|
|
201
|
+
const expectedKeys = new Set(theme_1.JIANWEN_THEME_TOKEN_KEYS);
|
|
202
|
+
const tokenMap = candidate;
|
|
203
|
+
const actualKeys = Object.keys(tokenMap);
|
|
204
|
+
const unknownKeys = actualKeys.filter((key) => !expectedKeys.has(key));
|
|
205
|
+
if (unknownKeys.length > 0) {
|
|
206
|
+
throw new Error(`Theme file ${filePath} has unknown ${section} token(s): ${unknownKeys.join(', ')}`);
|
|
207
|
+
}
|
|
208
|
+
for (const key of actualKeys) {
|
|
209
|
+
if (typeof tokenMap[key] !== 'string') {
|
|
210
|
+
throw new Error(`Theme file ${filePath} token ${section}.${key} must be a string.`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return tokenMap;
|
|
214
|
+
}
|
|
215
|
+
function loadThemeFromFile(themeFilePath) {
|
|
216
|
+
const absoluteThemePath = path.resolve(process.cwd(), themeFilePath);
|
|
217
|
+
if (!fs.existsSync(absoluteThemePath)) {
|
|
218
|
+
throw new Error(`Theme file not found: ${absoluteThemePath}`);
|
|
219
|
+
}
|
|
220
|
+
let raw;
|
|
221
|
+
try {
|
|
222
|
+
raw = JSON.parse(fs.readFileSync(absoluteThemePath, 'utf-8'));
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
226
|
+
throw new Error(`Invalid theme JSON file ${absoluteThemePath}: ${message}`);
|
|
227
|
+
}
|
|
228
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
229
|
+
throw new Error(`Theme file ${absoluteThemePath} must contain an object.`);
|
|
230
|
+
}
|
|
231
|
+
const parsed = raw;
|
|
232
|
+
const knownRootKeys = new Set(['light', 'dark', 'includeAutoDark']);
|
|
233
|
+
const unknownRootKeys = Object.keys(parsed).filter((key) => !knownRootKeys.has(key));
|
|
234
|
+
if (unknownRootKeys.length > 0) {
|
|
235
|
+
throw new Error(`Theme file ${absoluteThemePath} has unknown root field(s): ${unknownRootKeys.join(', ')}`);
|
|
236
|
+
}
|
|
237
|
+
const lightCandidate = parsed.light;
|
|
238
|
+
const darkCandidate = parsed.dark;
|
|
239
|
+
const lightOverrides = validateThemeTokenMap(absoluteThemePath, 'light', lightCandidate);
|
|
240
|
+
const darkOverrides = validateThemeTokenMap(absoluteThemePath, 'dark', darkCandidate);
|
|
241
|
+
if (parsed.includeAutoDark !== undefined && typeof parsed.includeAutoDark !== 'boolean') {
|
|
242
|
+
throw new Error(`Theme file ${absoluteThemePath} field includeAutoDark must be a boolean.`);
|
|
243
|
+
}
|
|
244
|
+
if (!lightOverrides && !darkOverrides) {
|
|
245
|
+
throw new Error(`Theme file ${absoluteThemePath} must provide at least one of "light" or "dark" token overrides.`);
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
light: lightOverrides,
|
|
249
|
+
dark: darkOverrides,
|
|
250
|
+
includeAutoDark: parsed.includeAutoDark,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
182
253
|
function runRenderCli(argv) {
|
|
183
254
|
const options = parseCliArgs(argv);
|
|
184
255
|
const absoluteInput = path.resolve(process.cwd(), options.inputFilePath);
|
|
@@ -188,6 +259,9 @@ function runRenderCli(argv) {
|
|
|
188
259
|
}
|
|
189
260
|
const baseDir = path.dirname(absoluteInput);
|
|
190
261
|
const source = fs.readFileSync(absoluteInput, 'utf-8');
|
|
262
|
+
const themeConfig = options.includeCss && options.themeFilePath
|
|
263
|
+
? loadThemeFromFile(options.themeFilePath)
|
|
264
|
+
: undefined;
|
|
191
265
|
const sourceLines = source.split(/\r?\n/);
|
|
192
266
|
const includeLineCache = new Map();
|
|
193
267
|
const getIncludeLines = (target) => {
|
|
@@ -249,6 +323,7 @@ function runRenderCli(argv) {
|
|
|
249
323
|
const documentOptions = {
|
|
250
324
|
includeCss: options.includeCss,
|
|
251
325
|
cssHref: options.cssHref,
|
|
326
|
+
theme: themeConfig,
|
|
252
327
|
includeRuntime: options.includeRuntime,
|
|
253
328
|
runtimeSrc: options.runtimeSrc,
|
|
254
329
|
format: options.format,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BlockAttributes } from '../../ast';
|
|
2
|
+
import { ParseError } from '../../errors';
|
|
3
|
+
import { PendingBlockContext } from '../types';
|
|
4
|
+
export interface ConsumeAttributeLineOptions {
|
|
5
|
+
text: string;
|
|
6
|
+
lineNumber: number;
|
|
7
|
+
errors: ParseError[];
|
|
8
|
+
pending: PendingBlockContext;
|
|
9
|
+
fallbackPosition?: BlockAttributes['position'];
|
|
10
|
+
tabCount?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function isAttributeOnlyLine(text: string): boolean;
|
|
13
|
+
export declare function tryConsumeAttributeOnlyLine(options: ConsumeAttributeLineOptions): boolean;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isAttributeOnlyLine = isAttributeOnlyLine;
|
|
4
|
+
exports.tryConsumeAttributeOnlyLine = tryConsumeAttributeOnlyLine;
|
|
5
|
+
const diagnostics_1 = require("../../diagnostics");
|
|
6
|
+
const include_1 = require("./include");
|
|
7
|
+
const MULTI_ARROW_WARNING_CODE = 'layout-multi-arrow';
|
|
8
|
+
const UNKNOWN_ATTRIBUTE_WARNING_CODE = 'unknown-attribute-token';
|
|
9
|
+
function isAttributeOnlyLine(text) {
|
|
10
|
+
if (text.length === 0) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const trimmed = text.trim();
|
|
14
|
+
if ((0, include_1.matchInclude)(trimmed)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
let i = 0;
|
|
18
|
+
while (i < text.length) {
|
|
19
|
+
const ch = text[i];
|
|
20
|
+
if (ch === '[') {
|
|
21
|
+
const end = text.indexOf(']', i + 1);
|
|
22
|
+
if (end === -1) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
i = end + 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (ch === ' ' || ch === '\t') {
|
|
29
|
+
i += 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
function tryConsumeAttributeOnlyLine(options) {
|
|
37
|
+
const result = parseAttributeLine(options.text, options.lineNumber, options.errors, options.pending.attrs, options.fallbackPosition, options.tabCount);
|
|
38
|
+
if (!result.recognizedAny) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (result.attrs) {
|
|
42
|
+
options.pending.attrs = mergeBlockAttributes(options.pending.attrs, result.attrs);
|
|
43
|
+
}
|
|
44
|
+
if (result.foldNext) {
|
|
45
|
+
options.pending.foldNext = true;
|
|
46
|
+
}
|
|
47
|
+
if (result.tagName) {
|
|
48
|
+
options.pending.tagName = result.tagName;
|
|
49
|
+
}
|
|
50
|
+
if (result.isComment) {
|
|
51
|
+
options.pending.isComment = true;
|
|
52
|
+
}
|
|
53
|
+
if (result.isDisabled) {
|
|
54
|
+
options.pending.isDisabled = true;
|
|
55
|
+
}
|
|
56
|
+
if (result.isSheet) {
|
|
57
|
+
options.pending.isSheet = true;
|
|
58
|
+
}
|
|
59
|
+
if (result.isHtml) {
|
|
60
|
+
options.pending.isHtml = true;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
function parseAttributeLine(text, lineNumber, errors, baseAttrs, fallbackPosition, tabCount) {
|
|
65
|
+
const result = {
|
|
66
|
+
attrs: undefined,
|
|
67
|
+
foldNext: false,
|
|
68
|
+
tagName: undefined,
|
|
69
|
+
isComment: false,
|
|
70
|
+
isDisabled: false,
|
|
71
|
+
isSheet: false,
|
|
72
|
+
isHtml: false,
|
|
73
|
+
recognizedAny: false,
|
|
74
|
+
};
|
|
75
|
+
let attrs = baseAttrs ? { ...baseAttrs } : undefined;
|
|
76
|
+
const unknownTokens = [];
|
|
77
|
+
let arrowCount = 0;
|
|
78
|
+
let i = 0;
|
|
79
|
+
while (i < text.length) {
|
|
80
|
+
const start = text.indexOf('[', i);
|
|
81
|
+
if (start === -1) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
const end = text.indexOf(']', start + 1);
|
|
85
|
+
if (end === -1) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
const inside = text.slice(start + 1, end).trim();
|
|
89
|
+
if (inside.length === 0) {
|
|
90
|
+
i = end + 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const parts = inside.split(',');
|
|
94
|
+
for (const rawPart of parts) {
|
|
95
|
+
const part = rawPart.trim();
|
|
96
|
+
if (part.length === 0) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (part === 'c') {
|
|
100
|
+
attrs = ensureBlockAttributes(attrs);
|
|
101
|
+
attrs.align = 'center';
|
|
102
|
+
result.recognizedAny = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (part === 'r') {
|
|
106
|
+
attrs = ensureBlockAttributes(attrs);
|
|
107
|
+
attrs.align = 'right';
|
|
108
|
+
result.recognizedAny = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (part === '->' || part === '<-' || part === '<->') {
|
|
112
|
+
arrowCount += 1;
|
|
113
|
+
result.recognizedAny = true;
|
|
114
|
+
if (part === '->' && arrowCount > 2) {
|
|
115
|
+
(0, diagnostics_1.reportParseWarning)(errors, {
|
|
116
|
+
message: 'More than two [->] attributes in a row; extra [->] will be treated as plain text.',
|
|
117
|
+
line: lineNumber,
|
|
118
|
+
code: MULTI_ARROW_WARNING_CODE,
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
attrs = ensureBlockAttributes(attrs);
|
|
123
|
+
if (part === '->' || part === '<->') {
|
|
124
|
+
const basePosition = attrs.position ??
|
|
125
|
+
fallbackPosition ??
|
|
126
|
+
(tabCount !== undefined ? mapTabsToPosition(tabCount) : undefined);
|
|
127
|
+
attrs.position = shiftPositionRight(basePosition);
|
|
128
|
+
attrs.sameLine = true;
|
|
129
|
+
}
|
|
130
|
+
if (part === '<-' || part === '<->') {
|
|
131
|
+
attrs.truncateRight = true;
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (part === 'fold') {
|
|
136
|
+
result.foldNext = true;
|
|
137
|
+
result.recognizedAny = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (part === 'sheet') {
|
|
141
|
+
result.isSheet = true;
|
|
142
|
+
result.recognizedAny = true;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (part === 'html') {
|
|
146
|
+
result.isHtml = true;
|
|
147
|
+
result.recognizedAny = true;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (part === 'comment') {
|
|
151
|
+
result.isComment = true;
|
|
152
|
+
result.recognizedAny = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (part === 'disable' || part === 'd') {
|
|
156
|
+
result.isDisabled = true;
|
|
157
|
+
result.recognizedAny = true;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (part.startsWith('tag=')) {
|
|
161
|
+
result.tagName = part.slice('tag='.length);
|
|
162
|
+
result.recognizedAny = true;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (part.startsWith('t=')) {
|
|
166
|
+
result.tagName = part.slice(2);
|
|
167
|
+
result.recognizedAny = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (part.startsWith('f=')) {
|
|
171
|
+
result.tagName = part.slice(2);
|
|
172
|
+
result.recognizedAny = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
unknownTokens.push(part);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
i = end + 1;
|
|
179
|
+
}
|
|
180
|
+
if (!result.recognizedAny && unknownTokens.length > 0) {
|
|
181
|
+
for (const token of unknownTokens) {
|
|
182
|
+
(0, diagnostics_1.reportParseWarning)(errors, {
|
|
183
|
+
message: `Unknown block attribute token "${token}"`,
|
|
184
|
+
line: lineNumber,
|
|
185
|
+
code: UNKNOWN_ATTRIBUTE_WARNING_CODE,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
result.attrs = attrs;
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
function ensureBlockAttributes(attrs) {
|
|
193
|
+
if (attrs) {
|
|
194
|
+
return attrs;
|
|
195
|
+
}
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
function mergeBlockAttributes(base, extra) {
|
|
199
|
+
if (!base) {
|
|
200
|
+
return { ...extra };
|
|
201
|
+
}
|
|
202
|
+
return { ...base, ...extra };
|
|
203
|
+
}
|
|
204
|
+
function mapTabsToPosition(tabCount) {
|
|
205
|
+
if (tabCount === 0) {
|
|
206
|
+
return 'L';
|
|
207
|
+
}
|
|
208
|
+
if (tabCount === 1) {
|
|
209
|
+
return 'C';
|
|
210
|
+
}
|
|
211
|
+
if (tabCount >= 2) {
|
|
212
|
+
return 'R';
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
function shiftPositionRight(position) {
|
|
217
|
+
if (position === 'L') {
|
|
218
|
+
return 'C';
|
|
219
|
+
}
|
|
220
|
+
if (position === 'C') {
|
|
221
|
+
return 'R';
|
|
222
|
+
}
|
|
223
|
+
if (position === 'R') {
|
|
224
|
+
return 'R';
|
|
225
|
+
}
|
|
226
|
+
return 'C';
|
|
227
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tryParseCodeBlock = tryParseCodeBlock;
|
|
4
|
+
const diagnostics_1 = require("../../diagnostics");
|
|
5
|
+
const location_1 = require("../../location");
|
|
6
|
+
const lexer_1 = require("../../../lexer/lexer");
|
|
7
|
+
const code_fence_1 = require("./code-fence");
|
|
8
|
+
function tryParseCodeBlock(ctx) {
|
|
9
|
+
const codeFenceStart = (0, code_fence_1.matchCodeFenceStart)(ctx.trimmedContent);
|
|
10
|
+
const attrCodeFenceStart = codeFenceStart ? null : (0, code_fence_1.matchAttributedCodeFence)(ctx.trimmedContent);
|
|
11
|
+
const effectiveCodeFence = codeFenceStart || attrCodeFenceStart;
|
|
12
|
+
if (!effectiveCodeFence) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const rawLines = [ctx.lineInfo.raw];
|
|
16
|
+
const codeLines = [];
|
|
17
|
+
const fenceIndent = ctx.lineInfo.tabCount;
|
|
18
|
+
let jCode = ctx.index + 1;
|
|
19
|
+
let closed = false;
|
|
20
|
+
while (jCode < ctx.lines.length) {
|
|
21
|
+
const nextRaw = ctx.lines[jCode];
|
|
22
|
+
if (nextRaw === undefined) {
|
|
23
|
+
jCode += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const nextInfo = (0, lexer_1.getLineInfo)(nextRaw);
|
|
27
|
+
const nextTrimmed = nextInfo.content.trim();
|
|
28
|
+
if ((0, code_fence_1.isCodeFenceEnd)(nextTrimmed)) {
|
|
29
|
+
rawLines.push(nextInfo.raw);
|
|
30
|
+
closed = true;
|
|
31
|
+
jCode += 1;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
rawLines.push(nextInfo.raw);
|
|
35
|
+
let codeLine = nextRaw;
|
|
36
|
+
for (let t = 0; t < fenceIndent && codeLine.startsWith('\t'); t++) {
|
|
37
|
+
codeLine = codeLine.slice(1);
|
|
38
|
+
}
|
|
39
|
+
codeLines.push(codeLine);
|
|
40
|
+
jCode += 1;
|
|
41
|
+
}
|
|
42
|
+
if (!closed) {
|
|
43
|
+
(0, diagnostics_1.reportParseError)(ctx.errors, {
|
|
44
|
+
message: 'Code block is not closed with ```',
|
|
45
|
+
line: ctx.index + 1,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const blockAttrs = ctx.buildBlockAttrs(ctx.lineInfo.tabCount);
|
|
49
|
+
const isHtmlBlock = ctx.pending.isHtml || (attrCodeFenceStart?.isHtml ?? false);
|
|
50
|
+
let block;
|
|
51
|
+
if (ctx.pending.isDisabled) {
|
|
52
|
+
const disabled = {
|
|
53
|
+
type: 'disabledBlock',
|
|
54
|
+
raw: rawLines.join('\n'),
|
|
55
|
+
blockAttrs,
|
|
56
|
+
};
|
|
57
|
+
(0, location_1.setNodeLocation)(disabled, ctx.lineLocation);
|
|
58
|
+
block = disabled;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const codeBlock = {
|
|
62
|
+
type: 'code',
|
|
63
|
+
language: effectiveCodeFence.language,
|
|
64
|
+
value: codeLines.join('\n'),
|
|
65
|
+
htmlLike: isHtmlBlock ? true : undefined,
|
|
66
|
+
blockAttrs,
|
|
67
|
+
};
|
|
68
|
+
(0, location_1.setNodeLocation)(codeBlock, ctx.lineLocation);
|
|
69
|
+
block = codeBlock;
|
|
70
|
+
}
|
|
71
|
+
ctx.commitBlock(block, blockAttrs, { allowTag: !ctx.pending.isDisabled });
|
|
72
|
+
return jCode;
|
|
73
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface CodeFenceStartMatchResult {
|
|
2
|
+
language?: string;
|
|
3
|
+
}
|
|
4
|
+
export interface AttributedCodeFenceResult {
|
|
5
|
+
language?: string;
|
|
6
|
+
isHtml: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function matchCodeFenceStart(trimmed: string): CodeFenceStartMatchResult | undefined;
|
|
9
|
+
export declare function isCodeFenceEnd(trimmed: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Match code fence with optional leading attributes like [html]```
|
|
12
|
+
* Returns the language and whether it's an HTML block
|
|
13
|
+
*/
|
|
14
|
+
export declare function matchAttributedCodeFence(trimmed: string): AttributedCodeFenceResult | undefined;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.matchCodeFenceStart = matchCodeFenceStart;
|
|
4
|
+
exports.isCodeFenceEnd = isCodeFenceEnd;
|
|
5
|
+
exports.matchAttributedCodeFence = matchAttributedCodeFence;
|
|
6
|
+
function matchCodeFenceStart(trimmed) {
|
|
7
|
+
if (!trimmed.startsWith('```')) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const m = trimmed.match(/^```([^\s`]*)\s*$/);
|
|
11
|
+
if (!m) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const group = m[1];
|
|
15
|
+
if (group === undefined || group.length === 0) {
|
|
16
|
+
return { language: undefined };
|
|
17
|
+
}
|
|
18
|
+
return { language: group };
|
|
19
|
+
}
|
|
20
|
+
function isCodeFenceEnd(trimmed) {
|
|
21
|
+
return /^```\s*$/.test(trimmed);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Match code fence with optional leading attributes like [html]```
|
|
25
|
+
* Returns the language and whether it's an HTML block
|
|
26
|
+
*/
|
|
27
|
+
function matchAttributedCodeFence(trimmed) {
|
|
28
|
+
// Match pattern: optional [attr]... followed by ```language?
|
|
29
|
+
const match = trimmed.match(/^(\[.+\])*\s*```([^\s`]*)\s*$/);
|
|
30
|
+
if (!match) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const attrPart = match[1] || '';
|
|
34
|
+
const language = match[2] || undefined;
|
|
35
|
+
const isHtml = /\[html\]/i.test(attrPart);
|
|
36
|
+
return { language, isHtml };
|
|
37
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BlockNode } from '../../ast';
|
|
2
|
+
import { BlockRuleContext } from './types';
|
|
3
|
+
interface ContentTitleRuleContext extends BlockRuleContext {
|
|
4
|
+
blocks: BlockNode[];
|
|
5
|
+
resetPending: () => void;
|
|
6
|
+
}
|
|
7
|
+
interface ContentTitleMatchResult {
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function tryParseContentTitleBlock(ctx: ContentTitleRuleContext): number | null;
|
|
11
|
+
export declare function matchContentTitle(content: string): ContentTitleMatchResult | undefined;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tryParseContentTitleBlock = tryParseContentTitleBlock;
|
|
4
|
+
exports.matchContentTitle = matchContentTitle;
|
|
5
|
+
const location_1 = require("../../location");
|
|
6
|
+
function tryParseContentTitleBlock(ctx) {
|
|
7
|
+
const contentTitleMatch = matchContentTitle(ctx.lineInfo.content);
|
|
8
|
+
if (!contentTitleMatch) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const text = contentTitleMatch.text;
|
|
12
|
+
let consumedByImageTitle = false;
|
|
13
|
+
if (canAttachToPreviousImage(ctx.pending)) {
|
|
14
|
+
const lastBlock = ctx.blocks[ctx.blocks.length - 1];
|
|
15
|
+
if (lastBlock && lastBlock.type === 'image') {
|
|
16
|
+
lastBlock.title = text;
|
|
17
|
+
consumedByImageTitle = true;
|
|
18
|
+
}
|
|
19
|
+
else if (lastBlock && lastBlock.type === 'taggedBlock' && lastBlock.child.type === 'image') {
|
|
20
|
+
lastBlock.child.title = text;
|
|
21
|
+
consumedByImageTitle = true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (consumedByImageTitle) {
|
|
25
|
+
ctx.resetPending();
|
|
26
|
+
return ctx.index + 1;
|
|
27
|
+
}
|
|
28
|
+
const blockAttrs = ctx.buildBlockAttrs(ctx.lineInfo.tabCount);
|
|
29
|
+
let block;
|
|
30
|
+
if (ctx.pending.isDisabled) {
|
|
31
|
+
const disabled = {
|
|
32
|
+
type: 'disabledBlock',
|
|
33
|
+
raw: ctx.lineInfo.raw,
|
|
34
|
+
blockAttrs,
|
|
35
|
+
};
|
|
36
|
+
(0, location_1.setNodeLocation)(disabled, ctx.lineLocation);
|
|
37
|
+
block = disabled;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const contentTitle = {
|
|
41
|
+
type: 'contentTitle',
|
|
42
|
+
children: [{ type: 'text', value: text }],
|
|
43
|
+
};
|
|
44
|
+
(0, location_1.setNodeLocation)(contentTitle, ctx.lineLocation);
|
|
45
|
+
block = contentTitle;
|
|
46
|
+
}
|
|
47
|
+
ctx.commitBlock(block, blockAttrs);
|
|
48
|
+
return ctx.index + 1;
|
|
49
|
+
}
|
|
50
|
+
function matchContentTitle(content) {
|
|
51
|
+
const m = content.match(/^>\s+(.+)$/);
|
|
52
|
+
if (!m) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const group = m[1];
|
|
56
|
+
if (group === undefined) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const text = group.trimEnd();
|
|
60
|
+
return { text };
|
|
61
|
+
}
|
|
62
|
+
function canAttachToPreviousImage(pending) {
|
|
63
|
+
return (!pending.attrs &&
|
|
64
|
+
!pending.foldNext &&
|
|
65
|
+
!pending.tagName &&
|
|
66
|
+
!pending.isComment &&
|
|
67
|
+
!pending.isDisabled &&
|
|
68
|
+
!pending.isSheet &&
|
|
69
|
+
!pending.isHtml);
|
|
70
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BlockNode } from '../../ast';
|
|
2
|
+
import { ParseError } from '../../errors';
|
|
3
|
+
import { BlockRuleContext } from './types';
|
|
4
|
+
interface FootnotesRuleContext extends BlockRuleContext {
|
|
5
|
+
parseBlocks: (source: string, errors: ParseError[]) => BlockNode[];
|
|
6
|
+
}
|
|
7
|
+
export declare function tryParseFootnotesBlock(ctx: FootnotesRuleContext): number | null;
|
|
8
|
+
export declare function isFootnotesLine(trimmed: string): boolean;
|
|
9
|
+
export {};
|