@miliastry/quasar 1.0.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 (172) hide show
  1. package/LICENSE +119 -0
  2. package/README.md +45 -0
  3. package/dist/Visuals/lyne.css +1027 -0
  4. package/dist/Visuals/osu.css +257 -0
  5. package/dist/index.d.mts +4563 -0
  6. package/dist/index.d.ts +4563 -0
  7. package/dist/index.js +11291 -0
  8. package/dist/index.mjs +11197 -0
  9. package/package.json +51 -0
  10. package/src/Analysis/Contracts/AnalysisReport.ts +26 -0
  11. package/src/Analysis/Contracts/Contribution.ts +76 -0
  12. package/src/Analysis/Contracts/Pass.ts +76 -0
  13. package/src/Analysis/Contracts/PipelineContext.ts +48 -0
  14. package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +292 -0
  15. package/src/Analysis/Passes/Analysis/MergeableColorAnalyzer.ts +112 -0
  16. package/src/Analysis/Passes/Analysis/RainbowAnalyzer.ts +233 -0
  17. package/src/Analysis/Passes/Analysis/WaveAnalyzer.ts +211 -0
  18. package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +135 -0
  19. package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +84 -0
  20. package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +99 -0
  21. package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +119 -0
  22. package/src/Analysis/Passes/Decision/DefaultDecision.ts +139 -0
  23. package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +179 -0
  24. package/src/Analysis/Passes/Transform/CollapseGradientTransform.ts +176 -0
  25. package/src/Analysis/Passes/Transform/MergeColorsTransform.ts +126 -0
  26. package/src/Analysis/Passes/Transform/RainbowCollapseTransform.ts +83 -0
  27. package/src/Analysis/Passes/Transform/WaveCollapseTransform.ts +88 -0
  28. package/src/Analysis/Passes/Utility/CharacterCountAnalyzer.ts +45 -0
  29. package/src/Analysis/Pipeline/Pipeline.ts +133 -0
  30. package/src/Analysis/Pipeline/PipelineBuilder.ts +55 -0
  31. package/src/Analysis/Pipeline/PipelineStage.ts +19 -0
  32. package/src/Analysis/Utils/color-utils.ts +132 -0
  33. package/src/Analysis/__tests__/Integration.test.ts +162 -0
  34. package/src/Analysis/__tests__/Pipeline.test.ts +133 -0
  35. package/src/Analysis/index.ts +52 -0
  36. package/src/BBCode/BBCodeDocumentModel.ts +175 -0
  37. package/src/BBCode/BBCodeToGreenNode.ts +755 -0
  38. package/src/BBCode/Parser.ts +384 -0
  39. package/src/BBCode/index.ts +12 -0
  40. package/src/Collab/positions.ts +91 -0
  41. package/src/Commands/Command.ts +44 -0
  42. package/src/Commands/CommandRegistry.ts +78 -0
  43. package/src/Commands/DeleteNode.ts +20 -0
  44. package/src/Commands/InsertText.ts +21 -0
  45. package/src/Commands/SplitMerge.ts +28 -0
  46. package/src/Commands/WrapInTag.ts +21 -0
  47. package/src/Commands/index.ts +6 -0
  48. package/src/Diff/TreeDiffer.ts +264 -0
  49. package/src/Diff/__tests__/TreeDiffer.test.ts +65 -0
  50. package/src/Diff/index.ts +2 -0
  51. package/src/Events/EventBus.ts +160 -0
  52. package/src/Events/index.ts +2 -0
  53. package/src/Formatter/Formatter.ts +54 -0
  54. package/src/Formatter/index.ts +2 -0
  55. package/src/HTML/HTMLDocumentModel.ts +35 -0
  56. package/src/HTML/HTMLToGreenNode.ts +290 -0
  57. package/src/Incremental/ChangeTracker.ts +105 -0
  58. package/src/Incremental/IncrementalParser.ts +591 -0
  59. package/src/Incremental/__tests__/IncrementalParser.test.ts +164 -0
  60. package/src/Incremental/index.ts +4 -0
  61. package/src/Lexer/BBCodeLexer.ts +382 -0
  62. package/src/Lexer/Lexer.ts +181 -0
  63. package/src/Lexer/index.ts +10 -0
  64. package/src/Linter/Linter.ts +193 -0
  65. package/src/Linter/index.ts +2 -0
  66. package/src/Markdown/MarkdownAST.ts +112 -0
  67. package/src/Markdown/MarkdownDocumentModel.ts +55 -0
  68. package/src/Markdown/MarkdownLexer.ts +203 -0
  69. package/src/Markdown/MarkdownParser.ts +455 -0
  70. package/src/Markdown/MarkdownToGreenNode.ts +153 -0
  71. package/src/Model/DocumentModel.ts +694 -0
  72. package/src/Model/NodeFactory.ts +117 -0
  73. package/src/Model/TagRegistry.ts +495 -0
  74. package/src/Model/index.ts +5 -0
  75. package/src/Plugins/PluginAPI.ts +119 -0
  76. package/src/Plugins/PluginRegistry.ts +132 -0
  77. package/src/Plugins/index.ts +3 -0
  78. package/src/Queries/QueryEngine.ts +152 -0
  79. package/src/Queries/index.ts +1 -0
  80. package/src/RenderPipeline/RenderPipeline.ts +125 -0
  81. package/src/RenderPipeline/RenderTree.ts +134 -0
  82. package/src/RenderPipeline/index.ts +4 -0
  83. package/src/Semantic/SemanticAnalyzer.ts +506 -0
  84. package/src/Semantic/index.ts +2 -0
  85. package/src/Symbols/SymbolTable.ts +124 -0
  86. package/src/Symbols/index.ts +1 -0
  87. package/src/Syntax/GreenNode.ts +324 -0
  88. package/src/Syntax/GreenNodePool.ts +269 -0
  89. package/src/Syntax/NodeMatcher.ts +370 -0
  90. package/src/Syntax/RedNode.ts +569 -0
  91. package/src/Syntax/RedNodeStore.ts +184 -0
  92. package/src/Syntax/TreeBuilder.ts +214 -0
  93. package/src/Syntax/__tests__/GreenNode.test.ts +33 -0
  94. package/src/Syntax/__tests__/RedNode.test.ts +81 -0
  95. package/src/Syntax/__tests__/RedNodeStore.test.ts +104 -0
  96. package/src/Syntax/greenEdit.ts +110 -0
  97. package/src/Syntax/hash.ts +30 -0
  98. package/src/Syntax/index.ts +12 -0
  99. package/src/Syntax/partition.ts +161 -0
  100. package/src/Syntax/preserveNodeIds.ts +201 -0
  101. package/src/Tests/ASTOptimizerIdempotence.test.ts +77 -0
  102. package/src/Tests/BlockPatcher.test.ts +437 -0
  103. package/src/Tests/BlockPatcherWindowed.test.ts +364 -0
  104. package/src/Tests/BoxDrawer.test.ts +217 -0
  105. package/src/Tests/BoxRichTitle.test.ts +105 -0
  106. package/src/Tests/Chars500kBenchmark.test.ts +151 -0
  107. package/src/Tests/Chars500kEdits.test.ts +321 -0
  108. package/src/Tests/CollabPositions.test.ts +146 -0
  109. package/src/Tests/CompilerPathProfiling.test.ts +186 -0
  110. package/src/Tests/DOMMorpher.test.ts +142 -0
  111. package/src/Tests/DomPatchPerf.test.ts +60 -0
  112. package/src/Tests/EffectSegments.snapshot.json +616 -0
  113. package/src/Tests/EffectSegments.test.ts +68 -0
  114. package/src/Tests/FindNodeAtOffset.test.ts +65 -0
  115. package/src/Tests/Fuzzer.test.ts +166 -0
  116. package/src/Tests/GreenNodePool.test.ts +153 -0
  117. package/src/Tests/Lexer.test.ts +238 -0
  118. package/src/Tests/LyneMode.test.ts +187 -0
  119. package/src/Tests/ModelCoherence.test.ts +180 -0
  120. package/src/Tests/Partition.test.ts +238 -0
  121. package/src/Tests/PluginTags.test.ts +150 -0
  122. package/src/Tests/ProblematicSection.test.ts +46 -0
  123. package/src/Tests/ProblematicSectionHTML.test.ts +58 -0
  124. package/src/Tests/RedReuse.test.ts +134 -0
  125. package/src/Tests/ReproDelete20k.test.ts +62 -0
  126. package/src/Tests/SemanticValidators.test.ts +136 -0
  127. package/src/Tests/StableNodeIds.test.ts +210 -0
  128. package/src/Tests/StudioColorBloat.test.ts +25 -0
  129. package/src/Tests/StudioDebugText.test.ts +27 -0
  130. package/src/Tests/StudioTrailingChar.test.ts +25 -0
  131. package/src/Tests/StudioValidText.test.ts +25 -0
  132. package/src/Tests/UrlImgBug.test.ts +23 -0
  133. package/src/Tests/VisualBuilderFidelity.test.ts +105 -0
  134. package/src/Tests/referenceDocument.ts +119 -0
  135. package/src/Transactions/Transaction.ts +176 -0
  136. package/src/Transactions/UndoManager.ts +111 -0
  137. package/src/Transactions/index.ts +3 -0
  138. package/src/Transformers/ASTOptimizer.ts +315 -0
  139. package/src/Transformers/GradientTransformer.ts +143 -0
  140. package/src/Transformers/GrowTransformer.ts +115 -0
  141. package/src/Transformers/RainbowTransformer.ts +121 -0
  142. package/src/Transformers/SineWaveTransformer.ts +130 -0
  143. package/src/Transformers/Transformer.ts +22 -0
  144. package/src/Types/core.ts +270 -0
  145. package/src/Types/diagnostics.ts +156 -0
  146. package/src/Types/index.ts +23 -0
  147. package/src/Types/operations.ts +180 -0
  148. package/src/Types/queries.ts +121 -0
  149. package/src/Types/symbols.ts +69 -0
  150. package/src/Types/tokens.ts +186 -0
  151. package/src/Utils/BBCodeGenerator.ts +126 -0
  152. package/src/Utils/ColorMath.ts +276 -0
  153. package/src/Utils/color.ts +112 -0
  154. package/src/Utils/dom-to-svg.test.ts +86 -0
  155. package/src/Utils/dom-to-svg.ts +615 -0
  156. package/src/Utils/treeTransformers.ts +717 -0
  157. package/src/Visitors/BBBlocksExporter.ts +69 -0
  158. package/src/Visitors/BBCodeExporter.ts +318 -0
  159. package/src/Visitors/BlockPatcher.ts +963 -0
  160. package/src/Visitors/DOMMorpher.ts +134 -0
  161. package/src/Visitors/HTMLRenderer.ts +1077 -0
  162. package/src/Visitors/JSONExporter.ts +66 -0
  163. package/src/Visitors/MarkdownExporter.ts +99 -0
  164. package/src/Visitors/SVGRenderer.ts +35 -0
  165. package/src/Visitors/TiptapExporter.ts +145 -0
  166. package/src/Visitors/Visitor.ts +48 -0
  167. package/src/Visitors/index.ts +9 -0
  168. package/src/Visuals/BoxDrawer.ts +175 -0
  169. package/src/Visuals/index.ts +42 -0
  170. package/src/Visuals/lyne.css +1027 -0
  171. package/src/Visuals/osu.css +257 -0
  172. package/src/index.ts +197 -0
@@ -0,0 +1,181 @@
1
+ /**
2
+ * DocumentEngine — Lexer
3
+ *
4
+ * Tokenizes source text into a stream of Tokens with Trivia.
5
+ * Trivia includes whitespace, newlines, and formatting that is
6
+ * preserved in the token stream but not semantically significant.
7
+ *
8
+ * This is a LANGUAGE-INDEPENDENT lexer framework.
9
+ * Language-specific tokenization is provided via the LexerOptions.
10
+ *
11
+ * Inspired by Roslyn's lexer architecture.
12
+ */
13
+
14
+ import type { Token, TokenStream, Trivia } from '../Types/tokens'
15
+ import { createToken, createTrivia, range, ArrayTokenStream } from '../Types/tokens'
16
+ import type { TokenKind } from '../Types/tokens'
17
+
18
+ // ─── Lexer Interface ───────────────────────────────────────────
19
+
20
+ export interface LexerRule {
21
+ /** Pattern to match at the current position */
22
+ pattern: RegExp
23
+ /** Token kind to produce */
24
+ kind: TokenKind
25
+ /** Optional: extract a name from the match (for tags) */
26
+ name?: (match: RegExpExecArray) => string
27
+ /** Optional: extract attributes from the match */
28
+ attrs?: (match: RegExpExecArray) => string
29
+ }
30
+
31
+ export interface LexerOptions {
32
+ /** Language-specific tokenization rules */
33
+ rules: LexerRule[]
34
+ /** Characters that count as whitespace trivia */
35
+ whitespaceChars?: RegExp
36
+ /** Characters that start a newline trivia */
37
+ newlineChars?: RegExp
38
+ /** Whether to collect trivia */
39
+ collectTrivia?: boolean
40
+ /** Lexer hooks: called before each token */
41
+ onToken?: (token: Token) => void
42
+ }
43
+
44
+ // ─── Default Lexer ─────────────────────────────────────────────
45
+
46
+ export class Lexer {
47
+ private options: LexerOptions
48
+ private source: string
49
+ private pos: number
50
+ private tokens: Token[]
51
+
52
+ constructor(options: LexerOptions) {
53
+ this.options = {
54
+ collectTrivia: true,
55
+ whitespaceChars: /[ \t]+/,
56
+ newlineChars: /\r?\n/,
57
+ ...options,
58
+ }
59
+ this.source = ''
60
+ this.pos = 0
61
+ this.tokens = []
62
+ }
63
+
64
+ /**
65
+ * Tokenize source text into a TokenStream.
66
+ */
67
+ tokenize(source: string): TokenStream {
68
+ this.source = source
69
+ this.pos = 0
70
+ this.tokens = []
71
+
72
+ while (this.pos < source.length) {
73
+ const leadingTrivia = this.options.collectTrivia ? this.scanTrivia() : []
74
+
75
+ // Try each rule in order
76
+ const token = this.tryRules(leadingTrivia)
77
+
78
+ if (token) {
79
+ this.tokens.push(token)
80
+ } else {
81
+ // No rule matched — produce a text token for the character
82
+ const start = this.pos
83
+ this.pos++
84
+ const text = source[start]
85
+ const tok = createToken('text', text, start, this.pos, {
86
+ leadingTrivia,
87
+ })
88
+ this.tokens.push(tok)
89
+ }
90
+ }
91
+
92
+ return new ArrayTokenStream(this.tokens) as TokenStream
93
+ }
94
+
95
+ /**
96
+ * Re-tokenize a portion of the source (for incremental parsing).
97
+ * Returns just the new tokens for the affected range.
98
+ */
99
+ retokenize(source: string, start: number, end: number): Token[] {
100
+ const oldSource = this.source
101
+ const oldPos = this.pos
102
+
103
+ this.source = source
104
+ this.pos = start
105
+ this.tokens = []
106
+
107
+ while (this.pos < end && this.pos < source.length) {
108
+ const leadingTrivia = this.scanTrivia()
109
+ const token = this.tryRules(leadingTrivia)
110
+ if (token) {
111
+ this.tokens.push(token)
112
+ } else {
113
+ const s = this.pos
114
+ this.pos++
115
+ this.tokens.push(createToken('text', source[s], s, this.pos, { leadingTrivia }))
116
+ }
117
+ }
118
+
119
+ const result = [...this.tokens]
120
+ this.source = oldSource
121
+ this.pos = oldPos
122
+ this.tokens = []
123
+
124
+ return result
125
+ }
126
+
127
+ // ── Private ─────────────────────────────────────────────
128
+
129
+ private tryRules(leadingTrivia: Trivia[]): Token | null {
130
+ for (const rule of this.options.rules) {
131
+ const match = rule.pattern.exec(this.source.slice(this.pos))
132
+ if (match && match.index === 0) {
133
+ const start = this.pos
134
+ const text = match[0]
135
+ this.pos += text.length
136
+
137
+ const trailingTrivia = this.options.collectTrivia ? this.scanTrivia() : []
138
+
139
+ const token = createToken(rule.kind, text, start, this.pos, {
140
+ name: rule.name?.(match),
141
+ attrs: rule.attrs?.(match),
142
+ tagText: text,
143
+ leadingTrivia,
144
+ trailingTrivia,
145
+ })
146
+
147
+ this.options.onToken?.(token)
148
+ return token
149
+ }
150
+ }
151
+ return null
152
+ }
153
+
154
+ private scanTrivia(): Trivia[] {
155
+ const trivia: Trivia[] = []
156
+
157
+ while (this.pos < this.source.length) {
158
+ const rest = this.source.slice(this.pos)
159
+
160
+ const newlineMatch = this.options.newlineChars!.exec(rest)
161
+ if (newlineMatch && newlineMatch.index === 0) {
162
+ const start = this.pos
163
+ this.pos += newlineMatch[0].length
164
+ trivia.push(createTrivia('newline', newlineMatch[0], start, this.pos))
165
+ continue
166
+ }
167
+
168
+ const wsMatch = this.options.whitespaceChars!.exec(rest)
169
+ if (wsMatch && wsMatch.index === 0) {
170
+ const start = this.pos
171
+ this.pos += wsMatch[0].length
172
+ trivia.push(createTrivia('whitespace', wsMatch[0], start, this.pos))
173
+ continue
174
+ }
175
+
176
+ break
177
+ }
178
+
179
+ return trivia
180
+ }
181
+ }
@@ -0,0 +1,10 @@
1
+ export { Lexer } from './Lexer'
2
+ export type { LexerOptions, LexerRule } from './Lexer'
3
+ export { scanBBCode, formatTokens } from './BBCodeLexer'
4
+ export type {
5
+ BBCodeToken,
6
+ BBCodeOpenToken,
7
+ BBCodeCloseToken,
8
+ BBCodeTextToken,
9
+ BBCodeNewlineToken,
10
+ } from './BBCodeLexer'
@@ -0,0 +1,193 @@
1
+ /**
2
+ * DocumentEngine — Linter
3
+ *
4
+ * Lints the document tree for issues.
5
+ * Similar to ESLint but for BBCode documents.
6
+ *
7
+ * Rules can be added via the Plugin API.
8
+ */
9
+
10
+ import { RedNode } from '../Syntax/RedNode'
11
+ import type { SemanticAnalyzer, Validator } from '../Semantic/SemanticAnalyzer'
12
+
13
+ export type LintSeverity = 'error' | 'warning' | 'info' | 'hint'
14
+
15
+ export interface LintRule {
16
+ code: string
17
+ severity: LintSeverity
18
+ description: string
19
+ validate(node: RedNode, context: LintContext): LintIssue | LintIssue[] | null
20
+ }
21
+
22
+ export interface LintContext {
23
+ source: string
24
+ allNodes: Map<string, RedNode>
25
+ }
26
+
27
+ export interface LintIssue {
28
+ code: string
29
+ message: string
30
+ severity: LintSeverity
31
+ nodeId: string
32
+ range: { start: number; end: number } | null
33
+ fix?: {
34
+ description: string
35
+ apply: () => void
36
+ }
37
+ }
38
+
39
+ export interface LintResult {
40
+ issues: LintIssue[]
41
+ errorCount: number
42
+ warningCount: number
43
+ infoCount: number
44
+ hintCount: number
45
+ }
46
+
47
+ export class Linter {
48
+ private rules: Map<string, LintRule> = new Map()
49
+
50
+ constructor() {
51
+ this.registerBuiltinRules()
52
+ }
53
+
54
+ /**
55
+ * Register a lint rule.
56
+ */
57
+ register(rule: LintRule): void {
58
+ this.rules.set(rule.code, rule)
59
+ }
60
+
61
+ /**
62
+ * Unregister a lint rule.
63
+ */
64
+ unregister(code: string): void {
65
+ this.rules.delete(code)
66
+ }
67
+
68
+ /**
69
+ * Lint a RedNode tree.
70
+ */
71
+ lint(root: RedNode, source: string): LintResult {
72
+ const issues: LintIssue[] = []
73
+ const result = { issues, errorCount: 0, warningCount: 0, infoCount: 0, hintCount: 0 }
74
+
75
+ // Build node index
76
+ const allNodes = new Map<string, RedNode>()
77
+ root.walk(node => { allNodes.set(node.id, node) })
78
+
79
+ const context: LintContext = { source, allNodes }
80
+
81
+ // Walk tree and apply rules
82
+ root.walk(node => {
83
+ for (const [, rule] of this.rules) {
84
+ try {
85
+ const r = rule.validate(node, context)
86
+ if (r) {
87
+ const items = Array.isArray(r) ? r : [r]
88
+ for (const issue of items) {
89
+ issues.push(issue)
90
+ switch (issue.severity) {
91
+ case 'error': result.errorCount++; break
92
+ case 'warning': result.warningCount++; break
93
+ case 'info': result.infoCount++; break
94
+ case 'hint': result.hintCount++; break
95
+ }
96
+ }
97
+ }
98
+ } catch {
99
+ // Rule error shouldn't break linting
100
+ }
101
+ }
102
+ })
103
+
104
+ return result
105
+ }
106
+
107
+ private registerBuiltinRules(): void {
108
+ this.register({
109
+ code: 'no-nested-bold',
110
+ severity: 'warning',
111
+ description: 'Nested bold tags are redundant',
112
+ validate: (node) => {
113
+ if (node.kind === 'bold' && node.parent?.kind === 'bold') {
114
+ return {
115
+ code: 'no-nested-bold',
116
+ message: 'Nested [b] tags are redundant',
117
+ severity: 'warning' as LintSeverity,
118
+ nodeId: node.id,
119
+ range: null,
120
+ }
121
+ }
122
+ return null
123
+ },
124
+ })
125
+
126
+ this.register({
127
+ code: 'no-empty-tags',
128
+ severity: 'hint',
129
+ description: 'Empty tags have no effect',
130
+ validate: (node) => {
131
+ if (['bold', 'italic', 'underline', 'strikethrough', 'color', 'font_size', 'spoiler'].includes(node.kind)) {
132
+ if (node.children.length === 0 && (node.text === '' || !node.text)) {
133
+ return {
134
+ code: 'no-empty-tags',
135
+ message: `Empty [${node.kind}] tag has no effect`,
136
+ severity: 'hint' as LintSeverity,
137
+ nodeId: node.id,
138
+ range: null,
139
+ }
140
+ }
141
+ }
142
+ return null
143
+ },
144
+ })
145
+
146
+ this.register({
147
+ code: 'max-quote-depth',
148
+ severity: 'warning',
149
+ description: 'Quote blocks should not be nested more than 3 levels deep',
150
+ validate: (node) => {
151
+ if (node.kind === 'quote') {
152
+ let depth = 0
153
+ let current = node.parent
154
+ while (current) {
155
+ if (current.kind === 'quote') depth++
156
+ current = current.parent
157
+ }
158
+ if (depth >= 3) {
159
+ return {
160
+ code: 'max-quote-depth',
161
+ message: 'Quote blocks should not be nested more than 3 levels deep',
162
+ severity: 'warning' as LintSeverity,
163
+ nodeId: node.id,
164
+ range: null,
165
+ }
166
+ }
167
+ }
168
+ return null
169
+ },
170
+ })
171
+
172
+ this.register({
173
+ code: 'invalid-url-protocol',
174
+ severity: 'error',
175
+ description: 'URLs must start with http:// or https:// (javascript:, file:, etc. are not allowed)',
176
+ validate: (node) => {
177
+ if (node.kind === 'url') {
178
+ const destination = String(node.metadata?.href ?? '') || node.text
179
+ if (destination && !destination.startsWith('http://') && !destination.startsWith('https://') && !destination.startsWith('mailto:')) {
180
+ return {
181
+ code: 'invalid-url-protocol',
182
+ message: 'URLs must start with http:// or https:// (javascript:, file:, etc. are not allowed)',
183
+ severity: 'error' as LintSeverity,
184
+ nodeId: node.id,
185
+ range: null,
186
+ }
187
+ }
188
+ }
189
+ return null
190
+ },
191
+ })
192
+ }
193
+ }
@@ -0,0 +1,2 @@
1
+ export { Linter } from './Linter'
2
+ export type { LintRule, LintIssue, LintResult, LintContext, LintSeverity } from './Linter'
@@ -0,0 +1,112 @@
1
+ /**
2
+ * DocumentEngine — Markdown AST Nodes
3
+ */
4
+
5
+ export type MarkdownNode =
6
+ | MarkdownDocument
7
+ | MarkdownParagraph
8
+ | MarkdownHeading
9
+ | MarkdownText
10
+ | MarkdownCodeBlock
11
+ | MarkdownCodeInline
12
+ | MarkdownStrong
13
+ | MarkdownEmphasis
14
+ | MarkdownLink
15
+ | MarkdownImage
16
+ | MarkdownList
17
+ | MarkdownListItem
18
+ | MarkdownBlockquote
19
+ | MarkdownSpoiler
20
+ | MarkdownNotice
21
+ | MarkdownSpacing
22
+ | MarkdownEmptyLine;
23
+
24
+ export interface MarkdownDocument {
25
+ type: 'document';
26
+ children: MarkdownNode[];
27
+ }
28
+
29
+ export interface MarkdownParagraph {
30
+ type: 'paragraph';
31
+ children: MarkdownNode[];
32
+ }
33
+
34
+ export interface MarkdownHeading {
35
+ type: 'heading';
36
+ level: number;
37
+ children: MarkdownNode[];
38
+ }
39
+
40
+ export interface MarkdownText {
41
+ type: 'text';
42
+ value: string;
43
+ }
44
+
45
+ export interface MarkdownCodeBlock {
46
+ type: 'code_block';
47
+ lang: string;
48
+ value: string;
49
+ }
50
+
51
+ export interface MarkdownCodeInline {
52
+ type: 'code_inline';
53
+ value: string;
54
+ }
55
+
56
+ export interface MarkdownStrong {
57
+ type: 'strong';
58
+ children: MarkdownNode[];
59
+ }
60
+
61
+ export interface MarkdownEmphasis {
62
+ type: 'emphasis';
63
+ children: MarkdownNode[];
64
+ }
65
+
66
+ export interface MarkdownLink {
67
+ type: 'link';
68
+ url: string;
69
+ children: MarkdownNode[];
70
+ }
71
+
72
+ export interface MarkdownImage {
73
+ type: 'image';
74
+ url: string;
75
+ alt: string;
76
+ }
77
+
78
+ export interface MarkdownList {
79
+ type: 'list';
80
+ ordered: boolean;
81
+ children: MarkdownListItem[];
82
+ }
83
+
84
+ export interface MarkdownListItem {
85
+ type: 'list_item';
86
+ children: MarkdownNode[];
87
+ }
88
+
89
+ export interface MarkdownBlockquote {
90
+ type: 'blockquote';
91
+ children: MarkdownNode[];
92
+ }
93
+
94
+ /** `>!` spoiler block — produced by MarkdownParser, maps to BBCode [spoiler]. */
95
+ export interface MarkdownSpoiler {
96
+ type: 'spoiler';
97
+ children: MarkdownNode[];
98
+ }
99
+
100
+ /** `> [!NOTE]` callout — produced by MarkdownParser, maps to BBCode [notice]. */
101
+ export interface MarkdownNotice {
102
+ type: 'notice';
103
+ children: MarkdownNode[];
104
+ }
105
+
106
+ export interface MarkdownSpacing {
107
+ type: 'spacing';
108
+ }
109
+
110
+ export interface MarkdownEmptyLine {
111
+ type: 'empty_line';
112
+ }
@@ -0,0 +1,55 @@
1
+ import { DocumentModel, type DocumentModelOptions } from '../Model/DocumentModel';
2
+ import { GreenNode, greenLeaf } from '../Syntax/GreenNode';
3
+ import { RedNode } from '../Syntax/RedNode';
4
+ import { scanMarkdown } from './MarkdownLexer';
5
+ import { parseMarkdown } from './MarkdownParser';
6
+ import { markdownAstToGreenTree, greenToRedNode } from './MarkdownToGreenNode';
7
+
8
+ export interface MarkdownDocumentModelOptions extends DocumentModelOptions {
9
+ source?: string;
10
+ language?: string;
11
+ }
12
+
13
+ export class MarkdownDocumentModel extends DocumentModel {
14
+ constructor(options: MarkdownDocumentModelOptions = {}) {
15
+ super({ source: '', language: options.language ?? 'markdown' });
16
+ if (options.source) {
17
+ this.rebuild(options.source);
18
+ }
19
+ }
20
+
21
+ static fromMarkdown(source: string): MarkdownDocumentModel {
22
+ return new MarkdownDocumentModel({ source });
23
+ }
24
+
25
+ protected parseToGreen(source: string): GreenNode {
26
+ try {
27
+ const tokens = scanMarkdown(source);
28
+ const ast = parseMarkdown(tokens);
29
+ return markdownAstToGreenTree(ast);
30
+ } catch (error) {
31
+ console.warn('[MarkdownDocumentModel] Parse error:', error);
32
+ return greenLeaf('text', source);
33
+ }
34
+ }
35
+
36
+ protected buildRedFromGreen(green: GreenNode): RedNode {
37
+ return greenToRedNode(green);
38
+ }
39
+
40
+ // Override text updates to bypass incremental parsing completely.
41
+ // The Markdown AST currently strips markup tokens (like **, [, ]),
42
+ // so GreenTree lengths do not match the raw text lengths.
43
+ // Incremental parsing relies on exact byte offsets, so it fails destructively here.
44
+ applyTextUpdate(newSource: string): void {
45
+ if (this.source === newSource) return;
46
+ this.rebuild(newSource);
47
+ }
48
+
49
+ applyChange(change: any): void {
50
+ // Fallback to full rebuild using the text change
51
+ const before = this.source.slice(0, change.start);
52
+ const after = this.source.slice(change.end);
53
+ this.rebuild(before + change.text + after);
54
+ }
55
+ }