@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.
- package/LICENSE +119 -0
- package/README.md +45 -0
- package/dist/Visuals/lyne.css +1027 -0
- package/dist/Visuals/osu.css +257 -0
- package/dist/index.d.mts +4563 -0
- package/dist/index.d.ts +4563 -0
- package/dist/index.js +11291 -0
- package/dist/index.mjs +11197 -0
- package/package.json +51 -0
- package/src/Analysis/Contracts/AnalysisReport.ts +26 -0
- package/src/Analysis/Contracts/Contribution.ts +76 -0
- package/src/Analysis/Contracts/Pass.ts +76 -0
- package/src/Analysis/Contracts/PipelineContext.ts +48 -0
- package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +292 -0
- package/src/Analysis/Passes/Analysis/MergeableColorAnalyzer.ts +112 -0
- package/src/Analysis/Passes/Analysis/RainbowAnalyzer.ts +233 -0
- package/src/Analysis/Passes/Analysis/WaveAnalyzer.ts +211 -0
- package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +135 -0
- package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +84 -0
- package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +99 -0
- package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +119 -0
- package/src/Analysis/Passes/Decision/DefaultDecision.ts +139 -0
- package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +179 -0
- package/src/Analysis/Passes/Transform/CollapseGradientTransform.ts +176 -0
- package/src/Analysis/Passes/Transform/MergeColorsTransform.ts +126 -0
- package/src/Analysis/Passes/Transform/RainbowCollapseTransform.ts +83 -0
- package/src/Analysis/Passes/Transform/WaveCollapseTransform.ts +88 -0
- package/src/Analysis/Passes/Utility/CharacterCountAnalyzer.ts +45 -0
- package/src/Analysis/Pipeline/Pipeline.ts +133 -0
- package/src/Analysis/Pipeline/PipelineBuilder.ts +55 -0
- package/src/Analysis/Pipeline/PipelineStage.ts +19 -0
- package/src/Analysis/Utils/color-utils.ts +132 -0
- package/src/Analysis/__tests__/Integration.test.ts +162 -0
- package/src/Analysis/__tests__/Pipeline.test.ts +133 -0
- package/src/Analysis/index.ts +52 -0
- package/src/BBCode/BBCodeDocumentModel.ts +175 -0
- package/src/BBCode/BBCodeToGreenNode.ts +755 -0
- package/src/BBCode/Parser.ts +384 -0
- package/src/BBCode/index.ts +12 -0
- package/src/Collab/positions.ts +91 -0
- package/src/Commands/Command.ts +44 -0
- package/src/Commands/CommandRegistry.ts +78 -0
- package/src/Commands/DeleteNode.ts +20 -0
- package/src/Commands/InsertText.ts +21 -0
- package/src/Commands/SplitMerge.ts +28 -0
- package/src/Commands/WrapInTag.ts +21 -0
- package/src/Commands/index.ts +6 -0
- package/src/Diff/TreeDiffer.ts +264 -0
- package/src/Diff/__tests__/TreeDiffer.test.ts +65 -0
- package/src/Diff/index.ts +2 -0
- package/src/Events/EventBus.ts +160 -0
- package/src/Events/index.ts +2 -0
- package/src/Formatter/Formatter.ts +54 -0
- package/src/Formatter/index.ts +2 -0
- package/src/HTML/HTMLDocumentModel.ts +35 -0
- package/src/HTML/HTMLToGreenNode.ts +290 -0
- package/src/Incremental/ChangeTracker.ts +105 -0
- package/src/Incremental/IncrementalParser.ts +591 -0
- package/src/Incremental/__tests__/IncrementalParser.test.ts +164 -0
- package/src/Incremental/index.ts +4 -0
- package/src/Lexer/BBCodeLexer.ts +382 -0
- package/src/Lexer/Lexer.ts +181 -0
- package/src/Lexer/index.ts +10 -0
- package/src/Linter/Linter.ts +193 -0
- package/src/Linter/index.ts +2 -0
- package/src/Markdown/MarkdownAST.ts +112 -0
- package/src/Markdown/MarkdownDocumentModel.ts +55 -0
- package/src/Markdown/MarkdownLexer.ts +203 -0
- package/src/Markdown/MarkdownParser.ts +455 -0
- package/src/Markdown/MarkdownToGreenNode.ts +153 -0
- package/src/Model/DocumentModel.ts +694 -0
- package/src/Model/NodeFactory.ts +117 -0
- package/src/Model/TagRegistry.ts +495 -0
- package/src/Model/index.ts +5 -0
- package/src/Plugins/PluginAPI.ts +119 -0
- package/src/Plugins/PluginRegistry.ts +132 -0
- package/src/Plugins/index.ts +3 -0
- package/src/Queries/QueryEngine.ts +152 -0
- package/src/Queries/index.ts +1 -0
- package/src/RenderPipeline/RenderPipeline.ts +125 -0
- package/src/RenderPipeline/RenderTree.ts +134 -0
- package/src/RenderPipeline/index.ts +4 -0
- package/src/Semantic/SemanticAnalyzer.ts +506 -0
- package/src/Semantic/index.ts +2 -0
- package/src/Symbols/SymbolTable.ts +124 -0
- package/src/Symbols/index.ts +1 -0
- package/src/Syntax/GreenNode.ts +324 -0
- package/src/Syntax/GreenNodePool.ts +269 -0
- package/src/Syntax/NodeMatcher.ts +370 -0
- package/src/Syntax/RedNode.ts +569 -0
- package/src/Syntax/RedNodeStore.ts +184 -0
- package/src/Syntax/TreeBuilder.ts +214 -0
- package/src/Syntax/__tests__/GreenNode.test.ts +33 -0
- package/src/Syntax/__tests__/RedNode.test.ts +81 -0
- package/src/Syntax/__tests__/RedNodeStore.test.ts +104 -0
- package/src/Syntax/greenEdit.ts +110 -0
- package/src/Syntax/hash.ts +30 -0
- package/src/Syntax/index.ts +12 -0
- package/src/Syntax/partition.ts +161 -0
- package/src/Syntax/preserveNodeIds.ts +201 -0
- package/src/Tests/ASTOptimizerIdempotence.test.ts +77 -0
- package/src/Tests/BlockPatcher.test.ts +437 -0
- package/src/Tests/BlockPatcherWindowed.test.ts +364 -0
- package/src/Tests/BoxDrawer.test.ts +217 -0
- package/src/Tests/BoxRichTitle.test.ts +105 -0
- package/src/Tests/Chars500kBenchmark.test.ts +151 -0
- package/src/Tests/Chars500kEdits.test.ts +321 -0
- package/src/Tests/CollabPositions.test.ts +146 -0
- package/src/Tests/CompilerPathProfiling.test.ts +186 -0
- package/src/Tests/DOMMorpher.test.ts +142 -0
- package/src/Tests/DomPatchPerf.test.ts +60 -0
- package/src/Tests/EffectSegments.snapshot.json +616 -0
- package/src/Tests/EffectSegments.test.ts +68 -0
- package/src/Tests/FindNodeAtOffset.test.ts +65 -0
- package/src/Tests/Fuzzer.test.ts +166 -0
- package/src/Tests/GreenNodePool.test.ts +153 -0
- package/src/Tests/Lexer.test.ts +238 -0
- package/src/Tests/LyneMode.test.ts +187 -0
- package/src/Tests/ModelCoherence.test.ts +180 -0
- package/src/Tests/Partition.test.ts +238 -0
- package/src/Tests/PluginTags.test.ts +150 -0
- package/src/Tests/ProblematicSection.test.ts +46 -0
- package/src/Tests/ProblematicSectionHTML.test.ts +58 -0
- package/src/Tests/RedReuse.test.ts +134 -0
- package/src/Tests/ReproDelete20k.test.ts +62 -0
- package/src/Tests/SemanticValidators.test.ts +136 -0
- package/src/Tests/StableNodeIds.test.ts +210 -0
- package/src/Tests/StudioColorBloat.test.ts +25 -0
- package/src/Tests/StudioDebugText.test.ts +27 -0
- package/src/Tests/StudioTrailingChar.test.ts +25 -0
- package/src/Tests/StudioValidText.test.ts +25 -0
- package/src/Tests/UrlImgBug.test.ts +23 -0
- package/src/Tests/VisualBuilderFidelity.test.ts +105 -0
- package/src/Tests/referenceDocument.ts +119 -0
- package/src/Transactions/Transaction.ts +176 -0
- package/src/Transactions/UndoManager.ts +111 -0
- package/src/Transactions/index.ts +3 -0
- package/src/Transformers/ASTOptimizer.ts +315 -0
- package/src/Transformers/GradientTransformer.ts +143 -0
- package/src/Transformers/GrowTransformer.ts +115 -0
- package/src/Transformers/RainbowTransformer.ts +121 -0
- package/src/Transformers/SineWaveTransformer.ts +130 -0
- package/src/Transformers/Transformer.ts +22 -0
- package/src/Types/core.ts +270 -0
- package/src/Types/diagnostics.ts +156 -0
- package/src/Types/index.ts +23 -0
- package/src/Types/operations.ts +180 -0
- package/src/Types/queries.ts +121 -0
- package/src/Types/symbols.ts +69 -0
- package/src/Types/tokens.ts +186 -0
- package/src/Utils/BBCodeGenerator.ts +126 -0
- package/src/Utils/ColorMath.ts +276 -0
- package/src/Utils/color.ts +112 -0
- package/src/Utils/dom-to-svg.test.ts +86 -0
- package/src/Utils/dom-to-svg.ts +615 -0
- package/src/Utils/treeTransformers.ts +717 -0
- package/src/Visitors/BBBlocksExporter.ts +69 -0
- package/src/Visitors/BBCodeExporter.ts +318 -0
- package/src/Visitors/BlockPatcher.ts +963 -0
- package/src/Visitors/DOMMorpher.ts +134 -0
- package/src/Visitors/HTMLRenderer.ts +1077 -0
- package/src/Visitors/JSONExporter.ts +66 -0
- package/src/Visitors/MarkdownExporter.ts +99 -0
- package/src/Visitors/SVGRenderer.ts +35 -0
- package/src/Visitors/TiptapExporter.ts +145 -0
- package/src/Visitors/Visitor.ts +48 -0
- package/src/Visitors/index.ts +9 -0
- package/src/Visuals/BoxDrawer.ts +175 -0
- package/src/Visuals/index.ts +42 -0
- package/src/Visuals/lyne.css +1027 -0
- package/src/Visuals/osu.css +257 -0
- package/src/index.ts +197 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocumentEngine — PluginAPI
|
|
3
|
+
*
|
|
4
|
+
* The public API exposed to plugins.
|
|
5
|
+
* Plugin authors use this to register tags, commands, validators, etc.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by VSCode's `vscode` module.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { DocumentModel } from '../Model/DocumentModel'
|
|
11
|
+
import { TagRegistry, type TagDefinition } from '../Model/TagRegistry'
|
|
12
|
+
import { CommandRegistry, type Command } from '../Commands'
|
|
13
|
+
import { SemanticAnalyzer, type Validator } from '../Semantic/SemanticAnalyzer'
|
|
14
|
+
import { Linter, type LintRule } from '../Linter/Linter'
|
|
15
|
+
import { RenderPipeline, type RenderHook } from '../RenderPipeline/RenderPipeline'
|
|
16
|
+
import { PluginRegistry, type PluginManifest, type PluginContribution } from './PluginRegistry'
|
|
17
|
+
|
|
18
|
+
export class PluginAPI {
|
|
19
|
+
readonly model: DocumentModel
|
|
20
|
+
readonly tags: TagRegistry
|
|
21
|
+
readonly commands: CommandRegistry
|
|
22
|
+
readonly semantic: SemanticAnalyzer
|
|
23
|
+
readonly linter: Linter
|
|
24
|
+
readonly renderer: RenderPipeline
|
|
25
|
+
readonly plugins: PluginRegistry
|
|
26
|
+
|
|
27
|
+
constructor(model: DocumentModel) {
|
|
28
|
+
this.model = model
|
|
29
|
+
this.tags = model.tagRegistry
|
|
30
|
+
this.commands = new CommandRegistry()
|
|
31
|
+
this.semantic = model.semanticAnalyzer
|
|
32
|
+
this.linter = new Linter()
|
|
33
|
+
this.renderer = new RenderPipeline()
|
|
34
|
+
this.plugins = new PluginRegistry()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Register a plugin with all its contributions.
|
|
39
|
+
*
|
|
40
|
+
* Example:
|
|
41
|
+
* ```ts
|
|
42
|
+
* api.registerPlugin({
|
|
43
|
+
* name: 'my-plugin',
|
|
44
|
+
* version: '1.0.0'
|
|
45
|
+
* }, {
|
|
46
|
+
* tags: [{ name: 'blur', kind: 'custom', ... }],
|
|
47
|
+
* commands: [{ id: 'my-command', ... }]
|
|
48
|
+
* })
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
registerPlugin(manifest: PluginManifest, contributions: PluginContribution): void {
|
|
52
|
+
const plugin = this.plugins.register(manifest, contributions)
|
|
53
|
+
|
|
54
|
+
// Apply contributions
|
|
55
|
+
if (contributions.tags) {
|
|
56
|
+
for (const tag of contributions.tags) {
|
|
57
|
+
this.tags.register(tag)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (contributions.commands) {
|
|
62
|
+
for (const cmd of contributions.commands) {
|
|
63
|
+
this.commands.register(cmd)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (contributions.validators) {
|
|
68
|
+
for (const v of contributions.validators) {
|
|
69
|
+
this.semantic.register(v)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (contributions.lintRules) {
|
|
74
|
+
for (const rule of contributions.lintRules) {
|
|
75
|
+
this.linter.register(rule)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (contributions.renderHooks) {
|
|
80
|
+
for (const hook of contributions.renderHooks) {
|
|
81
|
+
this.renderer.register(hook)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Activate
|
|
86
|
+
this.plugins.activate(plugin)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Unregister a plugin and remove its contributions.
|
|
91
|
+
*/
|
|
92
|
+
unregisterPlugin(name: string): void {
|
|
93
|
+
const plugin = this.plugins.get(name)
|
|
94
|
+
if (!plugin) return
|
|
95
|
+
|
|
96
|
+
if (plugin.contributions.tags) {
|
|
97
|
+
for (const tag of plugin.contributions.tags) {
|
|
98
|
+
this.tags.unregister(tag.name)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (plugin.contributions.commands) {
|
|
102
|
+
for (const cmd of plugin.contributions.commands) {
|
|
103
|
+
this.commands.unregister(cmd.id)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (plugin.contributions.validators) {
|
|
107
|
+
for (const v of plugin.contributions.validators) {
|
|
108
|
+
this.semantic.unregister(v.code)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (plugin.contributions.lintRules) {
|
|
112
|
+
for (const rule of plugin.contributions.lintRules) {
|
|
113
|
+
this.linter.unregister(rule.code)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.plugins.unregister(name)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocumentEngine — PluginRegistry
|
|
3
|
+
*
|
|
4
|
+
* Manages the lifecycle of plugins/extensions.
|
|
5
|
+
* Each plugin registers contributions (tags, commands, validators, etc.)
|
|
6
|
+
* and can be loaded/unloaded at runtime.
|
|
7
|
+
*
|
|
8
|
+
* Inspired by VSCode's extension system.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { TagDefinition } from '../Model/TagRegistry'
|
|
12
|
+
import type { Command } from '../Commands/Command'
|
|
13
|
+
import type { Validator } from '../Semantic/SemanticAnalyzer'
|
|
14
|
+
import type { LintRule } from '../Linter/Linter'
|
|
15
|
+
import type { RenderHook } from '../RenderPipeline/RenderPipeline'
|
|
16
|
+
|
|
17
|
+
export interface PluginManifest {
|
|
18
|
+
name: string
|
|
19
|
+
version: string
|
|
20
|
+
description?: string
|
|
21
|
+
author?: string
|
|
22
|
+
/** Dependencies on other plugins */
|
|
23
|
+
dependencies?: string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PluginContribution {
|
|
27
|
+
/** Tags to register */
|
|
28
|
+
tags?: TagDefinition[]
|
|
29
|
+
/** Commands to register */
|
|
30
|
+
commands?: Command[]
|
|
31
|
+
/** Semantic validators */
|
|
32
|
+
validators?: Validator[]
|
|
33
|
+
/** Lint rules */
|
|
34
|
+
lintRules?: LintRule[]
|
|
35
|
+
/** Render hooks */
|
|
36
|
+
renderHooks?: RenderHook[]
|
|
37
|
+
/** CSS to inject */
|
|
38
|
+
styles?: string[]
|
|
39
|
+
/** Initialization function */
|
|
40
|
+
activate?: () => void | Promise<void>
|
|
41
|
+
/** Cleanup function */
|
|
42
|
+
deactivate?: () => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Plugin {
|
|
46
|
+
manifest: PluginManifest
|
|
47
|
+
contributions: PluginContribution
|
|
48
|
+
/** Whether the plugin is currently active */
|
|
49
|
+
active: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class PluginRegistry {
|
|
53
|
+
private plugins: Map<string, Plugin> = new Map()
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Register a plugin.
|
|
57
|
+
*/
|
|
58
|
+
register(manifest: PluginManifest, contributions: PluginContribution): Plugin {
|
|
59
|
+
const plugin: Plugin = { manifest, contributions, active: false }
|
|
60
|
+
this.plugins.set(manifest.name, plugin)
|
|
61
|
+
return plugin
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Unregister and deactivate a plugin.
|
|
66
|
+
*/
|
|
67
|
+
unregister(name: string): boolean {
|
|
68
|
+
const plugin = this.plugins.get(name)
|
|
69
|
+
if (!plugin) return false
|
|
70
|
+
this.deactivate(plugin)
|
|
71
|
+
this.plugins.delete(name)
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get a plugin by name.
|
|
77
|
+
*/
|
|
78
|
+
get(name: string): Plugin | undefined {
|
|
79
|
+
return this.plugins.get(name)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get all registered plugins.
|
|
84
|
+
*/
|
|
85
|
+
getAll(): Plugin[] {
|
|
86
|
+
return Array.from(this.plugins.values())
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Activate a plugin (calls activate function).
|
|
91
|
+
*/
|
|
92
|
+
activate(plugin: Plugin): void {
|
|
93
|
+
if (plugin.active) return
|
|
94
|
+
try {
|
|
95
|
+
plugin.contributions.activate?.()
|
|
96
|
+
plugin.active = true
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.warn(`[PluginRegistry] Failed to activate plugin "${plugin.manifest.name}":`, error)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Deactivate a plugin (calls deactivate function).
|
|
104
|
+
*/
|
|
105
|
+
deactivate(plugin: Plugin): void {
|
|
106
|
+
if (!plugin.active) return
|
|
107
|
+
try {
|
|
108
|
+
plugin.contributions.deactivate?.()
|
|
109
|
+
plugin.active = false
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn(`[PluginRegistry] Failed to deactivate plugin "${plugin.manifest.name}":`, error)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Activate all registered plugins.
|
|
117
|
+
*/
|
|
118
|
+
activateAll(): void {
|
|
119
|
+
for (const [, plugin] of this.plugins) {
|
|
120
|
+
this.activate(plugin)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Deactivate all plugins.
|
|
126
|
+
*/
|
|
127
|
+
deactivateAll(): void {
|
|
128
|
+
for (const [, plugin] of this.plugins) {
|
|
129
|
+
this.deactivate(plugin)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocumentEngine — QueryEngine
|
|
3
|
+
*
|
|
4
|
+
* CSS-inspired query engine for the document tree.
|
|
5
|
+
* Enables queries like:
|
|
6
|
+
* query('paragraph > bold')
|
|
7
|
+
* query('quote text')
|
|
8
|
+
* query('[color]')
|
|
9
|
+
* query('list > *')
|
|
10
|
+
*
|
|
11
|
+
* Useful for:
|
|
12
|
+
* - Finding nodes by structure
|
|
13
|
+
* - AI context gathering
|
|
14
|
+
* - Plugin discovery
|
|
15
|
+
* - Lint rule targeting
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { RedNode } from '../Syntax/RedNode'
|
|
19
|
+
import type { Query, QueryStep, QuerySelector, QueryMatch, QueryResult } from '../Types/queries'
|
|
20
|
+
|
|
21
|
+
export class QueryEngine {
|
|
22
|
+
/**
|
|
23
|
+
* Execute a query against a RedNode tree.
|
|
24
|
+
*/
|
|
25
|
+
execute(query: Query, root: RedNode): QueryResult {
|
|
26
|
+
const startTime = performance.now()
|
|
27
|
+
const matches: QueryMatch[] = []
|
|
28
|
+
|
|
29
|
+
// Start from root, apply each step
|
|
30
|
+
let candidates: RedNode[] = [root]
|
|
31
|
+
|
|
32
|
+
for (const step of query.steps) {
|
|
33
|
+
if (candidates.length === 0) break
|
|
34
|
+
|
|
35
|
+
if (step.combinator === 'root') {
|
|
36
|
+
// First step: match against root
|
|
37
|
+
candidates = this.matchStep(step, candidates)
|
|
38
|
+
} else if (step.combinator === 'descendant') {
|
|
39
|
+
// Space: any descendant
|
|
40
|
+
const allDescendants: RedNode[] = []
|
|
41
|
+
for (const c of candidates) {
|
|
42
|
+
c.walk(node => {
|
|
43
|
+
if (node !== c) allDescendants.push(node)
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
candidates = this.matchStep(step, allDescendants)
|
|
47
|
+
} else if (step.combinator === 'child') {
|
|
48
|
+
// > direct child
|
|
49
|
+
const children: RedNode[] = []
|
|
50
|
+
for (const c of candidates) {
|
|
51
|
+
children.push(...c.children)
|
|
52
|
+
}
|
|
53
|
+
candidates = this.matchStep(step, children)
|
|
54
|
+
} else if (step.combinator === 'adjacent') {
|
|
55
|
+
// + next sibling
|
|
56
|
+
const nextSiblings: RedNode[] = []
|
|
57
|
+
for (const c of candidates) {
|
|
58
|
+
const next = c.nextSibling
|
|
59
|
+
if (next) nextSiblings.push(next)
|
|
60
|
+
}
|
|
61
|
+
candidates = this.matchStep(step, nextSiblings)
|
|
62
|
+
} else if (step.combinator === 'sibling') {
|
|
63
|
+
// ~ any sibling
|
|
64
|
+
const siblings: RedNode[] = []
|
|
65
|
+
for (const c of candidates) {
|
|
66
|
+
if (c.parent) {
|
|
67
|
+
siblings.push(...c.parent.children.filter(s => s.id !== c.id))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
candidates = this.matchStep(step, siblings)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const node of candidates) {
|
|
75
|
+
matches.push({
|
|
76
|
+
node,
|
|
77
|
+
score: 1,
|
|
78
|
+
matchedSelectors: [],
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
matches,
|
|
84
|
+
total: matches.length,
|
|
85
|
+
time: performance.now() - startTime,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Filter candidates by applying a query step's selectors.
|
|
91
|
+
*/
|
|
92
|
+
private matchStep(step: QueryStep, candidates: RedNode[]): RedNode[] {
|
|
93
|
+
return candidates.filter(node =>
|
|
94
|
+
step.selector.every(sel => this.matchesSelector(node, sel))
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if a single node matches a single selector.
|
|
100
|
+
*/
|
|
101
|
+
private matchesSelector(node: RedNode, selector: QuerySelector): boolean {
|
|
102
|
+
switch (selector.type) {
|
|
103
|
+
case 'kind':
|
|
104
|
+
return node.kind === selector.kind
|
|
105
|
+
case 'tag': {
|
|
106
|
+
const tagName = node.metadata?.tagName as string | undefined
|
|
107
|
+
return tagName === selector.tag || node.kind === selector.tag
|
|
108
|
+
}
|
|
109
|
+
case 'attribute': {
|
|
110
|
+
const val = node.metadata?.[selector.name]
|
|
111
|
+
if (selector.value !== undefined) {
|
|
112
|
+
return String(val) === selector.value
|
|
113
|
+
}
|
|
114
|
+
return val !== undefined && val !== null
|
|
115
|
+
}
|
|
116
|
+
case 'has_attribute':
|
|
117
|
+
return node.metadata?.[selector.name] !== undefined
|
|
118
|
+
case 'text': {
|
|
119
|
+
const text = node.text || ''
|
|
120
|
+
return text.includes(selector.pattern)
|
|
121
|
+
}
|
|
122
|
+
case 'has_child':
|
|
123
|
+
return node.children.some(c => this.matchesSelector(c, selector.selector))
|
|
124
|
+
case 'has_descendant': {
|
|
125
|
+
let found = false
|
|
126
|
+
node.walk(c => {
|
|
127
|
+
if (c !== node && this.matchesSelector(c, selector.selector)) {
|
|
128
|
+
found = true
|
|
129
|
+
return 'skip'
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
return found
|
|
133
|
+
}
|
|
134
|
+
case 'first_child':
|
|
135
|
+
return node.index === 0
|
|
136
|
+
case 'last_child':
|
|
137
|
+
return node.parent ? node.index === node.parent.children.length - 1 : false
|
|
138
|
+
case 'nth_child':
|
|
139
|
+
return node.index === selector.n - 1
|
|
140
|
+
case 'position':
|
|
141
|
+
if (selector.min !== undefined && node.index < selector.min) return false
|
|
142
|
+
if (selector.max !== undefined && node.index > selector.max) return false
|
|
143
|
+
return true
|
|
144
|
+
case 'depth':
|
|
145
|
+
if (selector.min !== undefined && node.depth < selector.min) return false
|
|
146
|
+
if (selector.max !== undefined && node.depth > selector.max) return false
|
|
147
|
+
return true
|
|
148
|
+
default:
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { QueryEngine } from './QueryEngine'
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocumentEngine — RenderPipeline
|
|
3
|
+
*
|
|
4
|
+
* The Render Pipeline transforms a DocumentModel into renderable output.
|
|
5
|
+
* It sits between the model and the view layer.
|
|
6
|
+
*
|
|
7
|
+
* The pipeline is:
|
|
8
|
+
* DocumentModel → RenderTree → HTML/SVG/Canvas/React
|
|
9
|
+
*
|
|
10
|
+
* Plugins can intercept at any stage:
|
|
11
|
+
* - Add/transform render nodes
|
|
12
|
+
* - Provide custom renderers for custom tags
|
|
13
|
+
* - Post-process the output
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { RedNode } from '../Syntax/RedNode'
|
|
17
|
+
import { RenderTree, type RenderNode, type RenderVariant } from './RenderTree'
|
|
18
|
+
import type { TagRegistry, TagHandlerContext } from '../Model/TagRegistry'
|
|
19
|
+
|
|
20
|
+
export type RenderPhase =
|
|
21
|
+
| 'build' // DocumentModel → RenderNode tree
|
|
22
|
+
| 'transform' // Plugin transforms
|
|
23
|
+
| 'serialize' // RenderNode → Output string
|
|
24
|
+
|
|
25
|
+
export interface RenderHook {
|
|
26
|
+
phase: RenderPhase
|
|
27
|
+
hook: (node: RenderNode, context: RenderContext) => RenderNode | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RenderContext {
|
|
31
|
+
variant: RenderVariant
|
|
32
|
+
registry: TagRegistry
|
|
33
|
+
source: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class RenderPipeline {
|
|
37
|
+
private hooks: RenderHook[] = []
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Register a render hook.
|
|
41
|
+
*/
|
|
42
|
+
register(hook: RenderHook): void {
|
|
43
|
+
this.hooks.push(hook)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Unregister a render hook.
|
|
48
|
+
*/
|
|
49
|
+
unregister(hook: RenderHook): void {
|
|
50
|
+
const idx = this.hooks.indexOf(hook)
|
|
51
|
+
if (idx >= 0) this.hooks.splice(idx, 1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Render a RedNode tree to a string.
|
|
56
|
+
*/
|
|
57
|
+
render(
|
|
58
|
+
root: RedNode,
|
|
59
|
+
variant: RenderVariant = 'html',
|
|
60
|
+
registry: TagRegistry,
|
|
61
|
+
source: string = '',
|
|
62
|
+
): string {
|
|
63
|
+
const context: RenderContext = { variant, registry, source }
|
|
64
|
+
|
|
65
|
+
// Phase 1: Build render tree
|
|
66
|
+
let renderTree = this.buildRenderTree(root, context)
|
|
67
|
+
|
|
68
|
+
// Phase 2: Apply transformations
|
|
69
|
+
for (const hook of this.hooks) {
|
|
70
|
+
if (hook.phase === 'transform') {
|
|
71
|
+
renderTree = hook.hook(renderTree, context) ?? renderTree
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Phase 3: Serialize
|
|
76
|
+
for (const hook of this.hooks) {
|
|
77
|
+
if (hook.phase === 'serialize') {
|
|
78
|
+
const result = hook.hook(renderTree, context)
|
|
79
|
+
if (result) return result.text
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Default serialization
|
|
84
|
+
return RenderTree.toHTML(renderTree)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build a RenderNode tree from a RedNode tree.
|
|
89
|
+
*/
|
|
90
|
+
private buildRenderTree(node: RedNode, context: RenderContext): RenderNode {
|
|
91
|
+
// Text nodes
|
|
92
|
+
if (node.kind === 'text') {
|
|
93
|
+
return RenderTree.text(node.text)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Document root
|
|
97
|
+
if (node.kind === 'document') {
|
|
98
|
+
const children = node.children.map(c => this.buildRenderTree(c, context))
|
|
99
|
+
return RenderTree.container('document', children)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check tag registry for custom render handlers
|
|
103
|
+
const tagDef = context.registry.getByKind(node.kind)
|
|
104
|
+
if (tagDef?.toRenderNode) {
|
|
105
|
+
return tagDef.toRenderNode({
|
|
106
|
+
node,
|
|
107
|
+
source: context.source,
|
|
108
|
+
visitChildren: (n) => {
|
|
109
|
+
const r = this.buildRenderTree(n, context)
|
|
110
|
+
return RenderTree.toHTML(r)
|
|
111
|
+
},
|
|
112
|
+
renderChild: (n) => this.buildRenderTree(n, context),
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Default rendering
|
|
117
|
+
const children = node.children.map(c => this.buildRenderTree(c, context))
|
|
118
|
+
const kind = node.kind
|
|
119
|
+
|
|
120
|
+
return RenderTree.container(kind, children, {
|
|
121
|
+
tag: node.metadata?.tagName,
|
|
122
|
+
...node.metadata,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocumentEngine — RenderTree
|
|
3
|
+
*
|
|
4
|
+
* An intermediate representation between DocumentModel and final output.
|
|
5
|
+
* The RenderTree is what the RenderPipeline processes.
|
|
6
|
+
*
|
|
7
|
+
* This abstraction allows:
|
|
8
|
+
* - Same DocumentModel → multiple render outputs (HTML, Canvas, SVG, PDF)
|
|
9
|
+
* - Plugin renderers that intercept/modify render nodes
|
|
10
|
+
* - Smooth visual transitions between document states
|
|
11
|
+
* - Partial rendering for performance
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { NodeKind, NodeAttributes } from '../Types/core'
|
|
15
|
+
|
|
16
|
+
export type RenderVariant = 'html' | 'canvas' | 'svg' | 'pdf' | 'react' | 'text'
|
|
17
|
+
|
|
18
|
+
export interface RenderNode {
|
|
19
|
+
/** Semantic kind */
|
|
20
|
+
kind: NodeKind | string
|
|
21
|
+
/** Text content */
|
|
22
|
+
text: string
|
|
23
|
+
/** Child render nodes */
|
|
24
|
+
children: RenderNode[]
|
|
25
|
+
/** Props/attributes for the renderer */
|
|
26
|
+
props: Record<string, unknown>
|
|
27
|
+
/** Which render variant this node targets */
|
|
28
|
+
variant?: RenderVariant
|
|
29
|
+
/** CSS class names */
|
|
30
|
+
className?: string
|
|
31
|
+
/** Inline styles */
|
|
32
|
+
style?: Record<string, string | number>
|
|
33
|
+
/** Events/handlers (for interactive renders) */
|
|
34
|
+
events?: Record<string, string>
|
|
35
|
+
/** Metadata for renderers */
|
|
36
|
+
metadata?: Record<string, unknown>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class RenderTree {
|
|
40
|
+
/**
|
|
41
|
+
* Create a render tree from a document node.
|
|
42
|
+
*/
|
|
43
|
+
static fromNode(
|
|
44
|
+
kind: NodeKind | string,
|
|
45
|
+
text: string = '',
|
|
46
|
+
children: RenderNode[] = [],
|
|
47
|
+
props: Record<string, unknown> = {},
|
|
48
|
+
): RenderNode {
|
|
49
|
+
return { kind, text, children, props }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a text render node.
|
|
54
|
+
*/
|
|
55
|
+
static text(content: string): RenderNode {
|
|
56
|
+
return { kind: 'text', text: content, children: [], props: {} }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a container render node.
|
|
61
|
+
*/
|
|
62
|
+
static container(
|
|
63
|
+
kind: NodeKind | string,
|
|
64
|
+
children: RenderNode[],
|
|
65
|
+
props: Record<string, unknown> = {},
|
|
66
|
+
): RenderNode {
|
|
67
|
+
return { kind, text: '', children, props }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Serialize a render tree to HTML.
|
|
72
|
+
*/
|
|
73
|
+
static toHTML(node: RenderNode): string {
|
|
74
|
+
if (node.kind === 'text') return this.escapeHtml(node.text)
|
|
75
|
+
|
|
76
|
+
const tag = this.kindToTag(node.kind)
|
|
77
|
+
const propsStr = this.propsToHtml(node)
|
|
78
|
+
const childrenStr = node.children.map(c => this.toHTML(c)).join('')
|
|
79
|
+
|
|
80
|
+
if (tag) {
|
|
81
|
+
return `<${tag}${propsStr}>${childrenStr}</${tag}>`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return childrenStr
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private static kindToTag(kind: string): string | null {
|
|
88
|
+
const map: Record<string, string> = {
|
|
89
|
+
bold: 'strong',
|
|
90
|
+
italic: 'em',
|
|
91
|
+
underline: 'u',
|
|
92
|
+
strikethrough: 's',
|
|
93
|
+
inline_code: 'code',
|
|
94
|
+
code: 'pre',
|
|
95
|
+
heading: 'h2',
|
|
96
|
+
paragraph: 'p',
|
|
97
|
+
list: 'ul',
|
|
98
|
+
list_item: 'li',
|
|
99
|
+
quote: 'blockquote',
|
|
100
|
+
center: 'div',
|
|
101
|
+
notice: 'div',
|
|
102
|
+
image: 'img',
|
|
103
|
+
video: 'iframe',
|
|
104
|
+
audio: 'audio',
|
|
105
|
+
spoiler: 'span',
|
|
106
|
+
url: 'a',
|
|
107
|
+
email: 'a',
|
|
108
|
+
profile: 'a',
|
|
109
|
+
document: 'div',
|
|
110
|
+
group: 'div',
|
|
111
|
+
text: 'span',
|
|
112
|
+
}
|
|
113
|
+
return map[kind] ?? 'span'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private static propsToHtml(node: RenderNode): string {
|
|
117
|
+
const parts: string[] = []
|
|
118
|
+
if (node.className) parts.push(`class="${node.className}"`)
|
|
119
|
+
if (node.style && Object.keys(node.style).length > 0) {
|
|
120
|
+
const styles = Object.entries(node.style)
|
|
121
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
122
|
+
.join(';')
|
|
123
|
+
parts.push(`style="${styles}"`)
|
|
124
|
+
}
|
|
125
|
+
return parts.length > 0 ? ' ' + parts.join(' ') : ''
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private static escapeHtml(text: string): string {
|
|
129
|
+
return text
|
|
130
|
+
.replace(/&/g, '&')
|
|
131
|
+
.replace(/</g, '<')
|
|
132
|
+
.replace(/>/g, '>')
|
|
133
|
+
}
|
|
134
|
+
}
|