comark 0.0.1 → 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/README.md +104 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/internal/frontmatter.d.ts +16 -0
- package/dist/internal/frontmatter.js +43 -0
- package/dist/internal/parse/auto-close/index.d.ts +12 -0
- package/dist/internal/parse/auto-close/index.js +457 -0
- package/dist/internal/parse/auto-close/table.d.ts +4 -0
- package/dist/internal/parse/auto-close/table.js +161 -0
- package/dist/internal/parse/auto-unwrap.d.ts +20 -0
- package/dist/internal/parse/auto-unwrap.js +42 -0
- package/dist/internal/parse/html/html_block_rule.d.ts +2 -0
- package/dist/internal/parse/html/html_block_rule.js +60 -0
- package/dist/internal/parse/html/html_blocks.d.ts +2 -0
- package/dist/internal/parse/html/html_blocks.js +66 -0
- package/dist/internal/parse/html/html_inline_rule.d.ts +2 -0
- package/dist/internal/parse/html/html_inline_rule.js +43 -0
- package/dist/internal/parse/html/html_re.d.ts +3 -0
- package/dist/internal/parse/html/html_re.js +18 -0
- package/dist/internal/parse/html/index.d.ts +18 -0
- package/dist/internal/parse/html/index.js +122 -0
- package/dist/internal/parse/incremental.d.ts +12 -0
- package/dist/internal/parse/incremental.js +39 -0
- package/dist/internal/parse/token-processor.d.ts +9 -0
- package/dist/internal/parse/token-processor.js +803 -0
- package/dist/internal/props-validation.d.ts +12 -0
- package/dist/internal/props-validation.js +112 -0
- package/dist/internal/stringify/attributes.d.ts +21 -0
- package/dist/internal/stringify/attributes.js +67 -0
- package/dist/internal/stringify/handlers/a.d.ts +3 -0
- package/dist/internal/stringify/handlers/a.js +11 -0
- package/dist/internal/stringify/handlers/blockquote.d.ts +3 -0
- package/dist/internal/stringify/handlers/blockquote.js +18 -0
- package/dist/internal/stringify/handlers/br.d.ts +3 -0
- package/dist/internal/stringify/handlers/br.js +3 -0
- package/dist/internal/stringify/handlers/code.d.ts +3 -0
- package/dist/internal/stringify/handlers/code.js +11 -0
- package/dist/internal/stringify/handlers/comment.d.ts +3 -0
- package/dist/internal/stringify/handlers/comment.js +6 -0
- package/dist/internal/stringify/handlers/del.d.ts +3 -0
- package/dist/internal/stringify/handlers/del.js +4 -0
- package/dist/internal/stringify/handlers/emphesis.d.ts +3 -0
- package/dist/internal/stringify/handlers/emphesis.js +13 -0
- package/dist/internal/stringify/handlers/heading.d.ts +3 -0
- package/dist/internal/stringify/handlers/heading.js +7 -0
- package/dist/internal/stringify/handlers/hr.d.ts +3 -0
- package/dist/internal/stringify/handlers/hr.js +3 -0
- package/dist/internal/stringify/handlers/html.d.ts +3 -0
- package/dist/internal/stringify/handlers/html.js +73 -0
- package/dist/internal/stringify/handlers/img.d.ts +3 -0
- package/dist/internal/stringify/handlers/img.js +9 -0
- package/dist/internal/stringify/handlers/index.d.ts +2 -0
- package/dist/internal/stringify/handlers/index.js +56 -0
- package/dist/internal/stringify/handlers/li.d.ts +3 -0
- package/dist/internal/stringify/handlers/li.js +43 -0
- package/dist/internal/stringify/handlers/math.d.ts +3 -0
- package/dist/internal/stringify/handlers/math.js +8 -0
- package/dist/internal/stringify/handlers/mdc.d.ts +3 -0
- package/dist/internal/stringify/handlers/mdc.js +47 -0
- package/dist/internal/stringify/handlers/mermaid.d.ts +3 -0
- package/dist/internal/stringify/handlers/mermaid.js +8 -0
- package/dist/internal/stringify/handlers/ol.d.ts +3 -0
- package/dist/internal/stringify/handlers/ol.js +18 -0
- package/dist/internal/stringify/handlers/p.d.ts +3 -0
- package/dist/internal/stringify/handlers/p.js +8 -0
- package/dist/internal/stringify/handlers/pre.d.ts +3 -0
- package/dist/internal/stringify/handlers/pre.js +60 -0
- package/dist/internal/stringify/handlers/strong.d.ts +3 -0
- package/dist/internal/stringify/handlers/strong.js +13 -0
- package/dist/internal/stringify/handlers/table.d.ts +8 -0
- package/dist/internal/stringify/handlers/table.js +180 -0
- package/dist/internal/stringify/handlers/template.d.ts +3 -0
- package/dist/internal/stringify/handlers/template.js +14 -0
- package/dist/internal/stringify/handlers/ul.d.ts +3 -0
- package/dist/internal/stringify/handlers/ul.js +18 -0
- package/dist/internal/stringify/indent.d.ts +4 -0
- package/dist/internal/stringify/indent.js +8 -0
- package/dist/internal/stringify/state.d.ts +13 -0
- package/dist/internal/stringify/state.js +121 -0
- package/dist/internal/yaml.d.ts +12 -0
- package/dist/internal/yaml.js +51 -0
- package/dist/parse.d.ts +66 -0
- package/dist/parse.js +163 -0
- package/dist/plugins/alert.d.ts +2 -0
- package/dist/plugins/alert.js +66 -0
- package/dist/plugins/emoji.d.ts +3 -0
- package/dist/plugins/emoji.js +438 -0
- package/dist/plugins/headings.d.ts +48 -0
- package/dist/plugins/headings.js +85 -0
- package/dist/plugins/highlight.d.ts +63 -0
- package/dist/plugins/highlight.js +235 -0
- package/dist/plugins/math.d.ts +59 -0
- package/dist/plugins/math.js +263 -0
- package/dist/plugins/mermaid.d.ts +38 -0
- package/dist/plugins/mermaid.js +185 -0
- package/dist/plugins/security.d.ts +11 -0
- package/dist/plugins/security.js +32 -0
- package/dist/plugins/summary.d.ts +2 -0
- package/dist/plugins/summary.js +22 -0
- package/dist/plugins/task-list.d.ts +8 -0
- package/dist/plugins/task-list.js +117 -0
- package/dist/plugins/toc.d.ts +15 -0
- package/dist/plugins/toc.js +118 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.js +29 -0
- package/dist/types.d.ts +258 -0
- package/dist/types.js +1 -0
- package/dist/utils/caret.d.ts +7 -0
- package/dist/utils/caret.js +36 -0
- package/dist/utils/index.d.ts +38 -0
- package/dist/utils/index.js +149 -0
- package/package.json +73 -9
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<img src="https://github.com/comarkdown/comark/blob/main/assets/banner.jpg" width="100%" alt="Comark banner" />
|
|
2
|
+
|
|
3
|
+
# comark
|
|
4
|
+
|
|
5
|
+
[](https://npmx.dev/comark)
|
|
6
|
+
[](https://npm.chart.dev/comark)
|
|
7
|
+
[](https://github.com/comarkdown/comark/actions/workflows/ci.yml)
|
|
8
|
+
[](https://comark.dev)
|
|
9
|
+
[](https://github.com/comarkdown/comark/blob/main/LICENSE)
|
|
10
|
+
|
|
11
|
+
A high-performance markdown parser and renderer with Vue, React & Svelte components support.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- 🚀 Fast markdown-exit based parser
|
|
16
|
+
- 📦 Stream API for buffered parsing
|
|
17
|
+
- 🔧 Comark component syntax support
|
|
18
|
+
- 🔒 Auto-close unclosed markdown syntax (perfect for streaming)
|
|
19
|
+
- 📝 Frontmatter parsing (YAML)
|
|
20
|
+
- 📑 Automatic table of contents generation
|
|
21
|
+
- 🎯 Full TypeScript support
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Vue
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @comark/vue katex
|
|
29
|
+
# or
|
|
30
|
+
pnpm add @comark/vue katex
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```vue
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
import { Comark } from '@comark/vue'
|
|
36
|
+
import math, { Math } from '@comark/vue/plugins/math'
|
|
37
|
+
|
|
38
|
+
const chatMessage = ...
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<Comark :components="{ Math }" :plugins="[math()]">{{ chatMessage }}</Comark>
|
|
43
|
+
</template>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### React
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install @comark/react katex
|
|
50
|
+
# or
|
|
51
|
+
pnpm add @comark/react katex
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { Comark } from '@comark/react'
|
|
56
|
+
import math, { Math } from '@comark/react/plugins/math'
|
|
57
|
+
|
|
58
|
+
function App() {
|
|
59
|
+
const chatMessage = ...
|
|
60
|
+
return <Comark components={{ Math }} plugins={[math()]}>{chatMessage}</Comark>
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Svelte
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install @comark/svelte katex
|
|
68
|
+
# or
|
|
69
|
+
pnpm add @comark/svelte katex
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```svelte
|
|
73
|
+
<script lang="ts">
|
|
74
|
+
import { Comark } from '@comark/svelte'
|
|
75
|
+
import math, { Math } from '@comark/svelte/plugins/math'
|
|
76
|
+
|
|
77
|
+
const chatMessage = ...
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<Comark markdown={chatMessage} components={{ math: Math }} plugins={[math()]} />
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### HTML (No Framework)
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm install @comark/html
|
|
87
|
+
# or
|
|
88
|
+
pnpm add @comark/html
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import { render } from '@comark/html'
|
|
93
|
+
|
|
94
|
+
const chatMessage = ...
|
|
95
|
+
|
|
96
|
+
const html = await render(chatMessage)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
Made with ❤️
|
|
103
|
+
|
|
104
|
+
Published under [MIT License](./LICENSE).
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Re-export auto-close utilities
|
|
2
|
+
export { autoCloseMarkdown } from "./internal/parse/auto-close/index.js";
|
|
3
|
+
// Re-export parse utilities
|
|
4
|
+
export { applyAutoUnwrap } from "./internal/parse/auto-unwrap.js";
|
|
5
|
+
// Re-export parse utilities
|
|
6
|
+
export * from "./parse.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse frontmatter from content
|
|
3
|
+
* @param content - The content to parse
|
|
4
|
+
* @returns The content and data
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseFrontmatter(content: string): {
|
|
7
|
+
content: string;
|
|
8
|
+
data: Record<string, any>;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Render frontmatter to content
|
|
12
|
+
* @param data - The data to render
|
|
13
|
+
* @param content - The content to render
|
|
14
|
+
* @returns The rendered content
|
|
15
|
+
*/
|
|
16
|
+
export declare function renderFrontmatter(data: Record<string, any> | undefined | null, content?: string): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parseYaml, stringifyYaml } from "./yaml.js";
|
|
2
|
+
const FRONTMATTER_DELIMITER_DEFAULT = '---';
|
|
3
|
+
const LF = '\n';
|
|
4
|
+
const CR = '\r';
|
|
5
|
+
/**
|
|
6
|
+
* Parse frontmatter from content
|
|
7
|
+
* @param content - The content to parse
|
|
8
|
+
* @returns The content and data
|
|
9
|
+
*/
|
|
10
|
+
export function parseFrontmatter(content) {
|
|
11
|
+
let data = {};
|
|
12
|
+
if (content.startsWith(FRONTMATTER_DELIMITER_DEFAULT)) {
|
|
13
|
+
const idx = content.indexOf(LF + FRONTMATTER_DELIMITER_DEFAULT);
|
|
14
|
+
if (idx !== -1) {
|
|
15
|
+
const hasCarriageReturn = content[idx - 1] === CR;
|
|
16
|
+
const frontmatter = content.slice(4, idx - (hasCarriageReturn ? 1 : 0));
|
|
17
|
+
if (frontmatter) {
|
|
18
|
+
data = parseYaml(frontmatter);
|
|
19
|
+
content = content.slice(idx + 4 + (hasCarriageReturn ? 1 : 0));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
content,
|
|
25
|
+
data,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Render frontmatter to content
|
|
30
|
+
* @param data - The data to render
|
|
31
|
+
* @param content - The content to render
|
|
32
|
+
* @returns The rendered content
|
|
33
|
+
*/
|
|
34
|
+
export function renderFrontmatter(data, content) {
|
|
35
|
+
if (!data || Object.keys(data).length === 0) {
|
|
36
|
+
return (content?.trim() || '');
|
|
37
|
+
}
|
|
38
|
+
const fm = stringifyYaml(data).trim();
|
|
39
|
+
if (content) {
|
|
40
|
+
return FRONTMATTER_DELIMITER_DEFAULT + LF + fm + LF + FRONTMATTER_DELIMITER_DEFAULT + LF + LF + content.trim();
|
|
41
|
+
}
|
|
42
|
+
return fm;
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-closes unclosed markdown and Comark component syntax
|
|
3
|
+
* Useful for streaming/incremental parsing where content may be partial
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Linear-time auto-close implementation without regex
|
|
7
|
+
* Processes markdown in O(n) time by scanning character-by-character
|
|
8
|
+
*
|
|
9
|
+
* @param markdown - The markdown content to auto-close
|
|
10
|
+
* @returns The markdown with unclosed syntax closed
|
|
11
|
+
*/
|
|
12
|
+
export declare function autoCloseMarkdown(markdown: string): string;
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-closes unclosed markdown and Comark component syntax
|
|
3
|
+
* Useful for streaming/incremental parsing where content may be partial
|
|
4
|
+
*/
|
|
5
|
+
import { closeTables } from "./table.js";
|
|
6
|
+
/**
|
|
7
|
+
* Linear-time auto-close implementation without regex
|
|
8
|
+
* Processes markdown in O(n) time by scanning character-by-character
|
|
9
|
+
*
|
|
10
|
+
* @param markdown - The markdown content to auto-close
|
|
11
|
+
* @returns The markdown with unclosed syntax closed
|
|
12
|
+
*/
|
|
13
|
+
export function autoCloseMarkdown(markdown) {
|
|
14
|
+
if (!markdown || markdown === '')
|
|
15
|
+
return markdown;
|
|
16
|
+
const lines = markdown.split('\n');
|
|
17
|
+
const n = lines.length;
|
|
18
|
+
// Single linear pass to collect document state
|
|
19
|
+
let inFrontmatter = false;
|
|
20
|
+
let inBlockMath = false;
|
|
21
|
+
let tableStart = -1;
|
|
22
|
+
const componentStack = [];
|
|
23
|
+
for (let idx = 0; idx < n; idx++) {
|
|
24
|
+
const line = lines[idx];
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
// Frontmatter: only starts at document line 0
|
|
27
|
+
if (idx === 0 && trimmed === '---') {
|
|
28
|
+
inFrontmatter = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (inFrontmatter) {
|
|
32
|
+
if (trimmed === '---')
|
|
33
|
+
inFrontmatter = false;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// Block math delimiter on its own line
|
|
37
|
+
if (trimmed === '$$') {
|
|
38
|
+
inBlockMath = !inBlockMath;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
// YAML props fence inside a component
|
|
42
|
+
if (trimmed === '---' && componentStack.length > 0) {
|
|
43
|
+
componentStack[componentStack.length - 1].hasYamlProps = !componentStack[componentStack.length - 1].hasYamlProps;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Table block tracking (consecutive pipe-starting lines)
|
|
47
|
+
if (trimmed.startsWith('|')) {
|
|
48
|
+
tableStart = tableStart === -1 ? idx : tableStart;
|
|
49
|
+
}
|
|
50
|
+
else if (tableStart !== -1) {
|
|
51
|
+
tableStart = -1;
|
|
52
|
+
}
|
|
53
|
+
// Clear the line if there is no open component and the last line is a component fence without name
|
|
54
|
+
if (idx === n - 1) {
|
|
55
|
+
if (trimmed[0] === ':' && componentStack.length === 0) {
|
|
56
|
+
let colonCount = 0;
|
|
57
|
+
while (colonCount < trimmed.length && trimmed[colonCount] === ':')
|
|
58
|
+
colonCount++;
|
|
59
|
+
if (trimmed.slice(colonCount).trim() === '') {
|
|
60
|
+
lines[idx] = '';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Component open/close (lines starting with :: or more colons)
|
|
65
|
+
if (trimmed[0] === ':') {
|
|
66
|
+
let colonCount = 0;
|
|
67
|
+
while (colonCount < trimmed.length && trimmed[colonCount] === ':')
|
|
68
|
+
colonCount++;
|
|
69
|
+
if (colonCount >= 2) {
|
|
70
|
+
let indentEnd = 0;
|
|
71
|
+
while (indentEnd < line.length && (line[indentEnd] === ' ' || line[indentEnd] === '\t'))
|
|
72
|
+
indentEnd++;
|
|
73
|
+
const indent = line.slice(0, indentEnd);
|
|
74
|
+
const ch = trimmed[colonCount] ?? '';
|
|
75
|
+
const isName = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '$';
|
|
76
|
+
if (isName) {
|
|
77
|
+
let nameEnd = colonCount;
|
|
78
|
+
while (nameEnd < trimmed.length) {
|
|
79
|
+
const c = trimmed[nameEnd];
|
|
80
|
+
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '$' || c === '.' || c === '-' || c === '_'))
|
|
81
|
+
break;
|
|
82
|
+
nameEnd++;
|
|
83
|
+
}
|
|
84
|
+
componentStack.push({ depth: colonCount, name: trimmed.slice(colonCount, nameEnd), indent, hasYamlProps: false });
|
|
85
|
+
}
|
|
86
|
+
else if (colonCount === trimmed.length && componentStack.length > 0) {
|
|
87
|
+
const top = componentStack[componentStack.length - 1];
|
|
88
|
+
if (top.depth === colonCount)
|
|
89
|
+
componentStack.pop();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Fix inline markers on last line (skip inside block-level structures)
|
|
95
|
+
const lastIdx = n - 1;
|
|
96
|
+
if (!inFrontmatter && !inBlockMath && lines[lastIdx].trim() !== '$$') {
|
|
97
|
+
lines[lastIdx] = closeInlineMarkersLinear(lines[lastIdx]);
|
|
98
|
+
}
|
|
99
|
+
let result = lines.join('\n');
|
|
100
|
+
// Fix tables
|
|
101
|
+
if (tableStart !== -1) {
|
|
102
|
+
result = closeTables(result);
|
|
103
|
+
}
|
|
104
|
+
// Close unclosed frontmatter
|
|
105
|
+
if (inFrontmatter) {
|
|
106
|
+
const lastTrimmed = lines[lastIdx].trim();
|
|
107
|
+
if (lastTrimmed === '-' || lastTrimmed === '--') {
|
|
108
|
+
result += '-'.repeat(3 - lastTrimmed.length);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
result += result.endsWith('\n') ? '---' : '\n---';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Close unclosed block math
|
|
115
|
+
if (inBlockMath) {
|
|
116
|
+
result += result.endsWith('\n') ? '$$' : '\n$$';
|
|
117
|
+
}
|
|
118
|
+
// Close Comark components
|
|
119
|
+
if (markdown.includes('::')) {
|
|
120
|
+
// Close unclosed brace in last line props
|
|
121
|
+
const lastLineStart = result.lastIndexOf('\n') + 1;
|
|
122
|
+
const finalLine = result.slice(lastLineStart);
|
|
123
|
+
let lastOpenBrace = -1;
|
|
124
|
+
for (let i = finalLine.length - 1; i >= 0; i--) {
|
|
125
|
+
if (finalLine[i] === '}')
|
|
126
|
+
break;
|
|
127
|
+
if (finalLine[i] === '{') {
|
|
128
|
+
lastOpenBrace = i;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (lastOpenBrace >= 0) {
|
|
133
|
+
const propsContent = finalLine.slice(lastOpenBrace + 1);
|
|
134
|
+
let dq = 0;
|
|
135
|
+
let sq = 0;
|
|
136
|
+
for (let i = 0; i < propsContent.length; i++) {
|
|
137
|
+
if (propsContent[i] === '"')
|
|
138
|
+
dq++;
|
|
139
|
+
if (propsContent[i] === '\'')
|
|
140
|
+
sq++;
|
|
141
|
+
}
|
|
142
|
+
let braceClose = '';
|
|
143
|
+
if (dq % 2 === 1)
|
|
144
|
+
braceClose += '"';
|
|
145
|
+
if (sq % 2 === 1)
|
|
146
|
+
braceClose += '\'';
|
|
147
|
+
result += braceClose + '}';
|
|
148
|
+
}
|
|
149
|
+
if (componentStack.length > 0) {
|
|
150
|
+
// Complete partial YAML fence (- or --) in top component's props
|
|
151
|
+
const topComp = componentStack[componentStack.length - 1];
|
|
152
|
+
const newLastStart = result.lastIndexOf('\n') + 1;
|
|
153
|
+
const newFinalTrimmed = result.slice(newLastStart).trim();
|
|
154
|
+
if (topComp.hasYamlProps && (newFinalTrimmed === '-' || newFinalTrimmed === '--')) {
|
|
155
|
+
result += '-'.repeat(3 - newFinalTrimmed.length);
|
|
156
|
+
topComp.hasYamlProps = false;
|
|
157
|
+
}
|
|
158
|
+
// Append component closers
|
|
159
|
+
const compClosers = [];
|
|
160
|
+
while (componentStack.length > 0) {
|
|
161
|
+
const comp = componentStack.pop();
|
|
162
|
+
if (comp.hasYamlProps)
|
|
163
|
+
compClosers.push(comp.indent + '---');
|
|
164
|
+
compClosers.push(comp.indent + ':'.repeat(comp.depth));
|
|
165
|
+
}
|
|
166
|
+
result += '\n' + compClosers.join('\n');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Closes inline markers (*, **, ***, ~~, `, $, $$, [, () on the last line
|
|
173
|
+
* without using regex - pure character scanning in O(n) time
|
|
174
|
+
*/
|
|
175
|
+
function closeInlineMarkersLinear(line) {
|
|
176
|
+
const len = line.length;
|
|
177
|
+
if (len === 0)
|
|
178
|
+
return line;
|
|
179
|
+
// Count markers by scanning
|
|
180
|
+
let asteriskCount = 0;
|
|
181
|
+
let underscoreCount = 0;
|
|
182
|
+
let tildeCount = 0; // Count individual tildes
|
|
183
|
+
let backtickCount = 0;
|
|
184
|
+
let dollarCount = 0; // Count $ for math
|
|
185
|
+
let dollarPairCount = 0; // Count $$ pairs for block math
|
|
186
|
+
let bracketBalance = 0; // [ minus ]
|
|
187
|
+
let parenBalance = 0; // ( minus ) after last ]
|
|
188
|
+
let lastBracketPos = -1;
|
|
189
|
+
// Track trailing whitespace
|
|
190
|
+
let contentEnd = len;
|
|
191
|
+
while (contentEnd > 0 && (line[contentEnd - 1] === ' ' || line[contentEnd - 1] === '\t')) {
|
|
192
|
+
contentEnd--;
|
|
193
|
+
}
|
|
194
|
+
const hasTrailingSpace = contentEnd < len;
|
|
195
|
+
// Track ** positions for O(n) complete pair detection (avoids O(n^3) nested loops)
|
|
196
|
+
const doubleAsteriskPositions = [];
|
|
197
|
+
const doubleUnderscorePositions = [];
|
|
198
|
+
// Single-pass scan through the line - O(n)
|
|
199
|
+
for (let i = 0; i < len; i++) {
|
|
200
|
+
const ch = line[i];
|
|
201
|
+
if (ch === '*') {
|
|
202
|
+
asteriskCount++;
|
|
203
|
+
// Track ** positions (not part of ***)
|
|
204
|
+
if (i + 1 < len && line[i + 1] === '*') {
|
|
205
|
+
const isPartOfTriple = (i > 0 && line[i - 1] === '*') || (i + 2 < len && line[i + 2] === '*');
|
|
206
|
+
if (!isPartOfTriple) {
|
|
207
|
+
doubleAsteriskPositions.push(i);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (ch === '_') {
|
|
212
|
+
underscoreCount++;
|
|
213
|
+
// Track __ positions (for bold)
|
|
214
|
+
if (i + 1 < len && line[i + 1] === '_') {
|
|
215
|
+
doubleUnderscorePositions.push(i);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (ch === '~') {
|
|
219
|
+
tildeCount++;
|
|
220
|
+
}
|
|
221
|
+
else if (ch === '`') {
|
|
222
|
+
backtickCount++;
|
|
223
|
+
}
|
|
224
|
+
else if (ch === '$') {
|
|
225
|
+
// Count $$ pairs for block/display math
|
|
226
|
+
if (i + 1 < len && line[i + 1] === '$') {
|
|
227
|
+
dollarPairCount++;
|
|
228
|
+
dollarCount += 2; // Count both dollars in the pair
|
|
229
|
+
i++; // Skip next $ since we counted the pair
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
dollarCount++; // Single $ for inline math
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (ch === '[') {
|
|
236
|
+
bracketBalance++;
|
|
237
|
+
lastBracketPos = i;
|
|
238
|
+
}
|
|
239
|
+
else if (ch === ']') {
|
|
240
|
+
bracketBalance--;
|
|
241
|
+
lastBracketPos = i;
|
|
242
|
+
}
|
|
243
|
+
else if (ch === '(') {
|
|
244
|
+
if (lastBracketPos >= 0 && i > lastBracketPos) {
|
|
245
|
+
parenBalance++;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if (ch === ')') {
|
|
249
|
+
if (lastBracketPos >= 0 && i > lastBracketPos) {
|
|
250
|
+
parenBalance--;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Check for complete ** pairs in O(1) - pairs are matched left to right
|
|
255
|
+
const hasCompleteBoldPair = doubleAsteriskPositions.length >= 2;
|
|
256
|
+
let closingSuffix = '';
|
|
257
|
+
let shouldTrim = false;
|
|
258
|
+
// Check for unclosed markers in priority order
|
|
259
|
+
// Check *** (bold+italic)
|
|
260
|
+
// Only treat as *** if line actually starts with *** (not just has 3 asterisks total)
|
|
261
|
+
if (asteriskCount >= 3 && line[0] === '*' && line[1] === '*' && line[2] === '*') {
|
|
262
|
+
const remainder = asteriskCount % 6;
|
|
263
|
+
if (remainder === 3) {
|
|
264
|
+
// Check if line starts with more than 3 asterisks (e.g., ****)
|
|
265
|
+
if (!(line[3] === '*')) {
|
|
266
|
+
// Check if marker at end with no content
|
|
267
|
+
if (!(contentEnd >= 3 && line[contentEnd - 1] === '*' && line[contentEnd - 2] === '*'
|
|
268
|
+
&& line[contentEnd - 3] === '*' && (contentEnd === 3 || line[contentEnd - 4] === ' '))) {
|
|
269
|
+
closingSuffix = '***';
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else if (remainder > 3 && remainder < 6) {
|
|
274
|
+
const needed = 6 - remainder;
|
|
275
|
+
closingSuffix = '*'.repeat(needed);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Check ** (bold) if not already closing
|
|
279
|
+
if (!closingSuffix && asteriskCount >= 2) {
|
|
280
|
+
const remainder = asteriskCount % 4;
|
|
281
|
+
if (remainder === 2) {
|
|
282
|
+
// Only check for ** if there are actually ** markers in the line
|
|
283
|
+
// This prevents "*italic*" (2 asterisks) from being treated as unclosed **
|
|
284
|
+
if (doubleAsteriskPositions.length > 0) {
|
|
285
|
+
// Check if line starts with more asterisks than ** (e.g., *** or more)
|
|
286
|
+
// This prevents "***text***" or "***text** *more" from being seen as unclosed **
|
|
287
|
+
const startsWithMoreAsterisks = line[0] === '*' && line[1] === '*' && line[2] === '*';
|
|
288
|
+
if (!startsWithMoreAsterisks) {
|
|
289
|
+
// Check if marker at end with no content
|
|
290
|
+
const endsWithMarker = contentEnd >= 2 && line[contentEnd - 1] === '*' && line[contentEnd - 2] === '*';
|
|
291
|
+
const markerAtEnd = endsWithMarker && (contentEnd === 2 || line[contentEnd - 3] === ' ');
|
|
292
|
+
if (!markerAtEnd) {
|
|
293
|
+
// Check if all asterisks are paired (bold + italic complete)
|
|
294
|
+
// If we have complete ** pairs, check remaining asterisks for italic
|
|
295
|
+
const boldAsterisksUsed = Math.floor(doubleAsteriskPositions.length / 2) * 4;
|
|
296
|
+
const remainingSingle = asteriskCount - boldAsterisksUsed;
|
|
297
|
+
const allPaired = hasCompleteBoldPair && remainingSingle % 2 === 0;
|
|
298
|
+
if (!allPaired) {
|
|
299
|
+
// Check if line ends with word (not just a closing marker)
|
|
300
|
+
const lastChar = line[contentEnd - 1];
|
|
301
|
+
const endsWithWord = (lastChar >= 'a' && lastChar <= 'z')
|
|
302
|
+
|| (lastChar >= 'A' && lastChar <= 'Z')
|
|
303
|
+
|| (lastChar >= '0' && lastChar <= '9');
|
|
304
|
+
if (!hasCompleteBoldPair || endsWithWord) {
|
|
305
|
+
closingSuffix = '**';
|
|
306
|
+
if (hasTrailingSpace && !endsWithMarker) {
|
|
307
|
+
shouldTrim = true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else if (remainder > 2 && remainder < 4) {
|
|
316
|
+
const needed = 4 - remainder;
|
|
317
|
+
closingSuffix = '*'.repeat(needed);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Check * (italic) if not already closing
|
|
321
|
+
if (!closingSuffix && asteriskCount % 2 === 1) {
|
|
322
|
+
// Check if line starts with more asterisks (e.g., ** or ***)
|
|
323
|
+
// But allow italic closing if bold pairs are complete
|
|
324
|
+
const startsWithMoreAsterisks = line[0] === '*' && line[1] === '*';
|
|
325
|
+
if (!startsWithMoreAsterisks || hasCompleteBoldPair) {
|
|
326
|
+
// Check if * followed by space (invalid italic)
|
|
327
|
+
let validItalic = false;
|
|
328
|
+
for (let i = 0; i < len; i++) {
|
|
329
|
+
if (line[i] === '*') {
|
|
330
|
+
const nextCh = i + 1 < len ? line[i + 1] : '';
|
|
331
|
+
const prevCh = i > 0 ? line[i - 1] : '';
|
|
332
|
+
// Valid if not followed by space, or if it's preceded by space (closing)
|
|
333
|
+
if (nextCh !== ' ' || prevCh === ' ') {
|
|
334
|
+
validItalic = true;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (validItalic) {
|
|
340
|
+
// Check marker at end with no content
|
|
341
|
+
// Only skip if it's truly isolated (e.g., "input *")
|
|
342
|
+
// Don't skip if there are complete pairs before it (e.g., "input **bold** *")
|
|
343
|
+
const markerAtEnd = contentEnd >= 1 && line[contentEnd - 1] === '*'
|
|
344
|
+
&& (contentEnd === 1 || line[contentEnd - 2] === ' ');
|
|
345
|
+
if (!markerAtEnd || asteriskCount > 1) {
|
|
346
|
+
closingSuffix = '*';
|
|
347
|
+
const endsWithMarker = line[contentEnd - 1] === '*';
|
|
348
|
+
if (hasTrailingSpace && !endsWithMarker) {
|
|
349
|
+
shouldTrim = true;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Check __ (double underscore bold)
|
|
356
|
+
if (!closingSuffix && underscoreCount >= 2) {
|
|
357
|
+
const remainder = underscoreCount % 4;
|
|
358
|
+
if (remainder === 2) {
|
|
359
|
+
// Only check for __ if there are actually __ markers in the line
|
|
360
|
+
if (doubleUnderscorePositions.length > 0) {
|
|
361
|
+
const hasCompleteUnderscorePair = doubleUnderscorePositions.length >= 2;
|
|
362
|
+
// Check if marker at end with no content
|
|
363
|
+
const endsWithMarker = contentEnd >= 2 && line[contentEnd - 1] === '_' && line[contentEnd - 2] === '_';
|
|
364
|
+
const markerAtEnd = endsWithMarker && (contentEnd === 2 || line[contentEnd - 3] === ' ');
|
|
365
|
+
if (!markerAtEnd && !hasCompleteUnderscorePair) {
|
|
366
|
+
closingSuffix = '__';
|
|
367
|
+
if (hasTrailingSpace && !endsWithMarker) {
|
|
368
|
+
shouldTrim = true;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else if (remainder > 2 && remainder < 4) {
|
|
374
|
+
const needed = 4 - remainder;
|
|
375
|
+
closingSuffix = '_'.repeat(needed);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Check _ (underscore italic)
|
|
379
|
+
if (!closingSuffix && underscoreCount % 2 === 1) {
|
|
380
|
+
// Check if _ followed by space (invalid italic)
|
|
381
|
+
let validItalic = false;
|
|
382
|
+
for (let i = 0; i < len; i++) {
|
|
383
|
+
if (line[i] === '_') {
|
|
384
|
+
const nextCh = i + 1 < len ? line[i + 1] : '';
|
|
385
|
+
const prevCh = i > 0 ? line[i - 1] : '';
|
|
386
|
+
// Valid if not followed by space, or if it's preceded by space (closing)
|
|
387
|
+
if (nextCh !== ' ' || prevCh === ' ') {
|
|
388
|
+
validItalic = true;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (validItalic) {
|
|
394
|
+
// Check marker at end with no content
|
|
395
|
+
const markerAtEnd = contentEnd >= 1 && line[contentEnd - 1] === '_'
|
|
396
|
+
&& (contentEnd === 1 || line[contentEnd - 2] === ' ');
|
|
397
|
+
if (!markerAtEnd) {
|
|
398
|
+
closingSuffix = '_';
|
|
399
|
+
const endsWithMarker = line[contentEnd - 1] === '_';
|
|
400
|
+
if (hasTrailingSpace && !endsWithMarker) {
|
|
401
|
+
shouldTrim = true;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Check ~~ (strikethrough)
|
|
407
|
+
if (!closingSuffix && tildeCount >= 2) {
|
|
408
|
+
const remainder = tildeCount % 4;
|
|
409
|
+
if (remainder === 2) {
|
|
410
|
+
// Two tildes unclosed, close with ~~
|
|
411
|
+
closingSuffix = '~~';
|
|
412
|
+
if (hasTrailingSpace)
|
|
413
|
+
shouldTrim = true;
|
|
414
|
+
}
|
|
415
|
+
else if (remainder > 2 && remainder < 4) {
|
|
416
|
+
// Partial marker like ~~text~ (3 tildes), need 1 more
|
|
417
|
+
const needed = 4 - remainder;
|
|
418
|
+
closingSuffix = '~'.repeat(needed);
|
|
419
|
+
if (hasTrailingSpace)
|
|
420
|
+
shouldTrim = true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// Check ` (code)
|
|
424
|
+
if (!closingSuffix && backtickCount % 2 === 1) {
|
|
425
|
+
closingSuffix = '`';
|
|
426
|
+
}
|
|
427
|
+
// Check $$ (block math) - takes priority over single $
|
|
428
|
+
// Don't close if the line is just $$ (block math delimiter on its own line)
|
|
429
|
+
if (!closingSuffix && dollarPairCount % 2 === 1) {
|
|
430
|
+
const trimmedLine = line.trim();
|
|
431
|
+
// Only close if this isn't a standalone $$ (which would be a block math delimiter)
|
|
432
|
+
if (trimmedLine !== '$$') {
|
|
433
|
+
closingSuffix = '$$';
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Check $ (inline math)
|
|
437
|
+
if (!closingSuffix && dollarCount % 2 === 1) {
|
|
438
|
+
closingSuffix = '$';
|
|
439
|
+
}
|
|
440
|
+
// Check [ ] (brackets)
|
|
441
|
+
if (!closingSuffix && bracketBalance > 0) {
|
|
442
|
+
closingSuffix = ']';
|
|
443
|
+
}
|
|
444
|
+
// Check ( ) (parens)
|
|
445
|
+
if (!closingSuffix && parenBalance > 0) {
|
|
446
|
+
closingSuffix = ')';
|
|
447
|
+
}
|
|
448
|
+
// Apply closing
|
|
449
|
+
if (shouldTrim && closingSuffix) {
|
|
450
|
+
let trimmedLen = len;
|
|
451
|
+
while (trimmedLen > 0 && (line[trimmedLen - 1] === ' ' || line[trimmedLen - 1] === '\t')) {
|
|
452
|
+
trimmedLen--;
|
|
453
|
+
}
|
|
454
|
+
return line.slice(0, trimmedLen) + closingSuffix;
|
|
455
|
+
}
|
|
456
|
+
return line + closingSuffix;
|
|
457
|
+
}
|