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.
Files changed (112) hide show
  1. package/README.md +104 -0
  2. package/dist/index.d.ts +4 -0
  3. package/dist/index.js +6 -0
  4. package/dist/internal/frontmatter.d.ts +16 -0
  5. package/dist/internal/frontmatter.js +43 -0
  6. package/dist/internal/parse/auto-close/index.d.ts +12 -0
  7. package/dist/internal/parse/auto-close/index.js +457 -0
  8. package/dist/internal/parse/auto-close/table.d.ts +4 -0
  9. package/dist/internal/parse/auto-close/table.js +161 -0
  10. package/dist/internal/parse/auto-unwrap.d.ts +20 -0
  11. package/dist/internal/parse/auto-unwrap.js +42 -0
  12. package/dist/internal/parse/html/html_block_rule.d.ts +2 -0
  13. package/dist/internal/parse/html/html_block_rule.js +60 -0
  14. package/dist/internal/parse/html/html_blocks.d.ts +2 -0
  15. package/dist/internal/parse/html/html_blocks.js +66 -0
  16. package/dist/internal/parse/html/html_inline_rule.d.ts +2 -0
  17. package/dist/internal/parse/html/html_inline_rule.js +43 -0
  18. package/dist/internal/parse/html/html_re.d.ts +3 -0
  19. package/dist/internal/parse/html/html_re.js +18 -0
  20. package/dist/internal/parse/html/index.d.ts +18 -0
  21. package/dist/internal/parse/html/index.js +122 -0
  22. package/dist/internal/parse/incremental.d.ts +12 -0
  23. package/dist/internal/parse/incremental.js +39 -0
  24. package/dist/internal/parse/token-processor.d.ts +9 -0
  25. package/dist/internal/parse/token-processor.js +803 -0
  26. package/dist/internal/props-validation.d.ts +12 -0
  27. package/dist/internal/props-validation.js +112 -0
  28. package/dist/internal/stringify/attributes.d.ts +21 -0
  29. package/dist/internal/stringify/attributes.js +67 -0
  30. package/dist/internal/stringify/handlers/a.d.ts +3 -0
  31. package/dist/internal/stringify/handlers/a.js +11 -0
  32. package/dist/internal/stringify/handlers/blockquote.d.ts +3 -0
  33. package/dist/internal/stringify/handlers/blockquote.js +18 -0
  34. package/dist/internal/stringify/handlers/br.d.ts +3 -0
  35. package/dist/internal/stringify/handlers/br.js +3 -0
  36. package/dist/internal/stringify/handlers/code.d.ts +3 -0
  37. package/dist/internal/stringify/handlers/code.js +11 -0
  38. package/dist/internal/stringify/handlers/comment.d.ts +3 -0
  39. package/dist/internal/stringify/handlers/comment.js +6 -0
  40. package/dist/internal/stringify/handlers/del.d.ts +3 -0
  41. package/dist/internal/stringify/handlers/del.js +4 -0
  42. package/dist/internal/stringify/handlers/emphesis.d.ts +3 -0
  43. package/dist/internal/stringify/handlers/emphesis.js +13 -0
  44. package/dist/internal/stringify/handlers/heading.d.ts +3 -0
  45. package/dist/internal/stringify/handlers/heading.js +7 -0
  46. package/dist/internal/stringify/handlers/hr.d.ts +3 -0
  47. package/dist/internal/stringify/handlers/hr.js +3 -0
  48. package/dist/internal/stringify/handlers/html.d.ts +3 -0
  49. package/dist/internal/stringify/handlers/html.js +73 -0
  50. package/dist/internal/stringify/handlers/img.d.ts +3 -0
  51. package/dist/internal/stringify/handlers/img.js +9 -0
  52. package/dist/internal/stringify/handlers/index.d.ts +2 -0
  53. package/dist/internal/stringify/handlers/index.js +56 -0
  54. package/dist/internal/stringify/handlers/li.d.ts +3 -0
  55. package/dist/internal/stringify/handlers/li.js +43 -0
  56. package/dist/internal/stringify/handlers/math.d.ts +3 -0
  57. package/dist/internal/stringify/handlers/math.js +8 -0
  58. package/dist/internal/stringify/handlers/mdc.d.ts +3 -0
  59. package/dist/internal/stringify/handlers/mdc.js +47 -0
  60. package/dist/internal/stringify/handlers/mermaid.d.ts +3 -0
  61. package/dist/internal/stringify/handlers/mermaid.js +8 -0
  62. package/dist/internal/stringify/handlers/ol.d.ts +3 -0
  63. package/dist/internal/stringify/handlers/ol.js +18 -0
  64. package/dist/internal/stringify/handlers/p.d.ts +3 -0
  65. package/dist/internal/stringify/handlers/p.js +8 -0
  66. package/dist/internal/stringify/handlers/pre.d.ts +3 -0
  67. package/dist/internal/stringify/handlers/pre.js +60 -0
  68. package/dist/internal/stringify/handlers/strong.d.ts +3 -0
  69. package/dist/internal/stringify/handlers/strong.js +13 -0
  70. package/dist/internal/stringify/handlers/table.d.ts +8 -0
  71. package/dist/internal/stringify/handlers/table.js +180 -0
  72. package/dist/internal/stringify/handlers/template.d.ts +3 -0
  73. package/dist/internal/stringify/handlers/template.js +14 -0
  74. package/dist/internal/stringify/handlers/ul.d.ts +3 -0
  75. package/dist/internal/stringify/handlers/ul.js +18 -0
  76. package/dist/internal/stringify/indent.d.ts +4 -0
  77. package/dist/internal/stringify/indent.js +8 -0
  78. package/dist/internal/stringify/state.d.ts +13 -0
  79. package/dist/internal/stringify/state.js +121 -0
  80. package/dist/internal/yaml.d.ts +12 -0
  81. package/dist/internal/yaml.js +51 -0
  82. package/dist/parse.d.ts +66 -0
  83. package/dist/parse.js +163 -0
  84. package/dist/plugins/alert.d.ts +2 -0
  85. package/dist/plugins/alert.js +66 -0
  86. package/dist/plugins/emoji.d.ts +3 -0
  87. package/dist/plugins/emoji.js +438 -0
  88. package/dist/plugins/headings.d.ts +48 -0
  89. package/dist/plugins/headings.js +85 -0
  90. package/dist/plugins/highlight.d.ts +63 -0
  91. package/dist/plugins/highlight.js +235 -0
  92. package/dist/plugins/math.d.ts +59 -0
  93. package/dist/plugins/math.js +263 -0
  94. package/dist/plugins/mermaid.d.ts +38 -0
  95. package/dist/plugins/mermaid.js +185 -0
  96. package/dist/plugins/security.d.ts +11 -0
  97. package/dist/plugins/security.js +32 -0
  98. package/dist/plugins/summary.d.ts +2 -0
  99. package/dist/plugins/summary.js +22 -0
  100. package/dist/plugins/task-list.d.ts +8 -0
  101. package/dist/plugins/task-list.js +117 -0
  102. package/dist/plugins/toc.d.ts +15 -0
  103. package/dist/plugins/toc.js +118 -0
  104. package/dist/render.d.ts +18 -0
  105. package/dist/render.js +29 -0
  106. package/dist/types.d.ts +258 -0
  107. package/dist/types.js +1 -0
  108. package/dist/utils/caret.d.ts +7 -0
  109. package/dist/utils/caret.js +36 -0
  110. package/dist/utils/index.d.ts +38 -0
  111. package/dist/utils/index.js +149 -0
  112. 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
+ [![npm version](https://img.shields.io/npm/v/comark?color=black)](https://npmx.dev/comark)
6
+ [![npm downloads](https://img.shields.io/npm/dm/comark?color=black)](https://npm.chart.dev/comark)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/comarkdown/comark/ci.yml?branch=main&color=black)](https://github.com/comarkdown/comark/actions/workflows/ci.yml)
8
+ [![Documentation](https://img.shields.io/badge/Documentation-black?logo=readme&logoColor=white)](https://comark.dev)
9
+ [![license](https://img.shields.io/github/license/comarkdown/comark?color=black)](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).
@@ -0,0 +1,4 @@
1
+ export { autoCloseMarkdown } from './internal/parse/auto-close/index.ts';
2
+ export { applyAutoUnwrap } from './internal/parse/auto-unwrap.ts';
3
+ export * from './parse.ts';
4
+ export type * from './types';
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
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Closes unclosed markdown tables
3
+ */
4
+ export declare function closeTables(markdown: string): string;