@miliastry/quasar 1.0.5 → 1.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/dist/Visuals/lyne.css +191 -21
- package/dist/Visuals/osu.css +36 -5
- package/dist/index.js +9619 -3135
- package/dist/index.mjs +9490 -3136
- package/dist/types/Analysis/Contracts/AnalysisReport.d.ts +22 -0
- package/dist/types/Analysis/Contracts/Contribution.d.ts +68 -0
- package/dist/types/Analysis/Contracts/Pass.d.ts +59 -0
- package/dist/types/Analysis/Contracts/PipelineContext.d.ts +34 -0
- package/dist/types/Analysis/Passes/Analysis/ColorUsageAnalyzer.d.ts +56 -0
- package/dist/types/Analysis/Passes/Analysis/GradientAnalyzer.d.ts +111 -0
- package/dist/types/Analysis/Passes/Analysis/MergeableColorAnalyzer.d.ts +26 -0
- package/dist/types/Analysis/Passes/Analysis/RainbowAnalyzer.d.ts +47 -0
- package/dist/types/Analysis/Passes/Analysis/SymbolAnalyzer.d.ts +89 -0
- package/dist/types/Analysis/Passes/Analysis/WaveAnalyzer.d.ts +44 -0
- package/dist/types/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Decision/DefaultDecision.d.ts +30 -0
- package/dist/types/Analysis/Passes/Decision/PaletteRemapDecision.d.ts +169 -0
- package/dist/types/Analysis/Passes/Decision/__tests__/DefaultDecision.test.d.ts +4 -0
- package/dist/types/Analysis/Passes/Transform/CollapseGradientTransform.d.ts +37 -0
- package/dist/types/Analysis/Passes/Transform/MergeColorsTransform.d.ts +28 -0
- package/dist/types/Analysis/Passes/Transform/RainbowCollapseTransform.d.ts +19 -0
- package/dist/types/Analysis/Passes/Transform/WaveCollapseTransform.d.ts +20 -0
- package/dist/types/Analysis/Passes/Utility/CharacterCountAnalyzer.d.ts +19 -0
- package/dist/types/Analysis/Pipeline/Pipeline.d.ts +69 -0
- package/dist/types/Analysis/Pipeline/PipelineBuilder.d.ts +31 -0
- package/dist/types/Analysis/Pipeline/PipelineStage.d.ts +17 -0
- package/dist/types/Analysis/Utils/color-utils.d.ts +60 -0
- package/dist/types/Analysis/__tests__/Integration.test.d.ts +8 -0
- package/dist/types/Analysis/__tests__/PaletteRemap.test.d.ts +14 -0
- package/dist/types/Analysis/__tests__/Pipeline.test.d.ts +10 -0
- package/dist/types/Analysis/__tests__/RoundTrip.test.d.ts +30 -0
- package/dist/types/Analysis/__tests__/SymbolAnalyzer.test.d.ts +11 -0
- package/dist/types/Analysis/index.d.ts +40 -0
- package/dist/types/BBCode/BBCodeDocumentModel.d.ts +92 -0
- package/dist/types/BBCode/BBCodeToGreenNode.d.ts +111 -0
- package/dist/types/BBCode/Parser.d.ts +78 -0
- package/dist/types/BBCode/index.d.ts +4 -0
- package/dist/types/Collab/positions.d.ts +55 -0
- package/dist/types/Commands/Command.d.ts +37 -0
- package/dist/types/Commands/CommandRegistry.d.ts +48 -0
- package/dist/types/Commands/DeleteNode.d.ts +7 -0
- package/dist/types/Commands/InsertText.d.ts +8 -0
- package/dist/types/Commands/SplitMerge.d.ts +9 -0
- package/dist/types/Commands/WrapInTag.d.ts +8 -0
- package/dist/types/Commands/index.d.ts +6 -0
- package/dist/types/Diff/TreeDiffer.d.ts +52 -0
- package/dist/types/Diff/__tests__/TreeDiffer.test.d.ts +1 -0
- package/dist/types/Diff/index.d.ts +2 -0
- package/dist/types/Edits/EditPlan.d.ts +144 -0
- package/dist/types/Edits/Optimizer.d.ts +78 -0
- package/dist/types/Edits/Rules/Rule.d.ts +91 -0
- package/dist/types/Edits/Rules/dropEmptyTags.d.ts +40 -0
- package/dist/types/Edits/Rules/dropRedundantNesting.d.ts +59 -0
- package/dist/types/Edits/Rules/index.d.ts +9 -0
- package/dist/types/Edits/Rules/mergeAdjacent.d.ts +93 -0
- package/dist/types/Edits/Rules/reorderWrappers.d.ts +42 -0
- package/dist/types/Edits/Rules/shortenHex.d.ts +30 -0
- package/dist/types/Edits/Rules/tagValue.d.ts +56 -0
- package/dist/types/Edits/Rules/unwrapInvisibleColor.d.ts +47 -0
- package/dist/types/Edits/applyEdits.d.ts +36 -0
- package/dist/types/Edits/index.d.ts +6 -0
- package/dist/types/Events/EventBus.d.ts +103 -0
- package/dist/types/Events/index.d.ts +2 -0
- package/dist/types/Formatter/Formatter.d.ts +32 -0
- package/dist/types/Formatter/index.d.ts +2 -0
- package/dist/types/HTML/HTMLDocumentModel.d.ts +13 -0
- package/dist/types/HTML/HTMLToGreenNode.d.ts +4 -0
- package/dist/types/Incremental/BracketIndex.d.ts +101 -0
- package/dist/types/Incremental/ChangeTracker.d.ts +69 -0
- package/dist/types/Incremental/IncrementalParser.d.ts +246 -0
- package/dist/types/Incremental/__tests__/BracketIndex.test.d.ts +1 -0
- package/dist/types/Incremental/__tests__/IncrementalParser.test.d.ts +1 -0
- package/dist/types/Incremental/index.d.ts +4 -0
- package/dist/types/Lexer/BBCodeLexer.d.ts +74 -0
- package/dist/types/Lexer/Lexer.d.ts +54 -0
- package/dist/types/Lexer/index.d.ts +4 -0
- package/dist/types/Linter/Linter.d.ts +58 -0
- package/dist/types/Linter/index.d.ts +2 -0
- package/dist/types/Markdown/MarkdownAST.d.ts +122 -0
- package/dist/types/Markdown/MarkdownDocumentModel.d.ts +15 -0
- package/dist/types/Markdown/MarkdownLexer.d.ts +155 -0
- package/dist/types/Markdown/MarkdownParser.d.ts +42 -0
- package/dist/types/Markdown/MarkdownToGreenNode.d.ts +6 -0
- package/dist/types/MilHibri/MilHibriDocumentModel.d.ts +21 -0
- package/dist/types/Model/DocumentModel.d.ts +318 -0
- package/dist/types/Model/NodeFactory.d.ts +29 -0
- package/dist/types/Model/TagRegistry.d.ts +165 -0
- package/dist/types/Model/index.d.ts +5 -0
- package/dist/types/Plugins/PluginAPI.d.ts +44 -0
- package/dist/types/Plugins/PluginRegistry.d.ts +81 -0
- package/dist/types/Plugins/index.d.ts +3 -0
- package/dist/types/Queries/QueryEngine.d.ts +32 -0
- package/dist/types/Queries/index.d.ts +1 -0
- package/dist/types/Reconciler/SurgicalReconciler.d.ts +23 -0
- package/dist/types/RenderPipeline/RenderPipeline.d.ts +46 -0
- package/dist/types/RenderPipeline/RenderTree.d.ts +55 -0
- package/dist/types/RenderPipeline/index.d.ts +4 -0
- package/dist/types/Repair/NestingRepair.d.ts +48 -0
- package/dist/types/Repair/index.d.ts +1 -0
- package/dist/types/Semantic/SemanticAnalyzer.d.ts +321 -0
- package/dist/types/Semantic/index.d.ts +2 -0
- package/dist/types/Symbols/SymbolTable.d.ts +47 -0
- package/dist/types/Symbols/index.d.ts +1 -0
- package/dist/types/Syntax/GreenNode.d.ts +154 -0
- package/dist/types/Syntax/GreenNodePool.d.ts +134 -0
- package/dist/types/Syntax/NodeMatcher.d.ts +75 -0
- package/dist/types/Syntax/RedNode.d.ts +280 -0
- package/dist/types/Syntax/RedNodeStore.d.ts +116 -0
- package/dist/types/Syntax/TreeBuilder.d.ts +54 -0
- package/dist/types/Syntax/__tests__/GreenNode.test.d.ts +1 -0
- package/dist/types/Syntax/__tests__/RedNode.test.d.ts +1 -0
- package/dist/types/Syntax/__tests__/RedNodeStore.test.d.ts +1 -0
- package/dist/types/Syntax/greenEdit.d.ts +64 -0
- package/dist/types/Syntax/hash.d.ts +17 -0
- package/dist/types/Syntax/index.d.ts +8 -0
- package/dist/types/Syntax/nodeAttr.d.ts +63 -0
- package/dist/types/Syntax/partition.d.ts +67 -0
- package/dist/types/Syntax/preserveNodeIds.d.ts +47 -0
- package/dist/types/Tokens/TokenResolver.d.ts +25 -0
- package/dist/types/Tokens/index.d.ts +1 -0
- package/dist/types/Transactions/Transaction.d.ts +36 -0
- package/dist/types/Transactions/UndoManager.d.ts +63 -0
- package/dist/types/Transactions/index.d.ts +3 -0
- package/dist/types/Transformers/ASTOptimizer.d.ts +34 -0
- package/dist/types/Transformers/GradientTransformer.d.ts +18 -0
- package/dist/types/Transformers/GrowTransformer.d.ts +16 -0
- package/dist/types/Transformers/RainbowTransformer.d.ts +17 -0
- package/dist/types/Transformers/SineWaveTransformer.d.ts +17 -0
- package/dist/types/Transformers/Transformer.d.ts +17 -0
- package/dist/types/Types/core.d.ts +104 -0
- package/dist/types/Types/diagnostics.d.ts +85 -0
- package/dist/types/Types/index.d.ts +11 -0
- package/dist/types/Types/operations.d.ts +109 -0
- package/dist/types/Types/queries.d.ts +67 -0
- package/dist/types/Types/symbols.d.ts +42 -0
- package/dist/types/Types/tokens.d.ts +75 -0
- package/dist/types/Utils/BBCodeGenerator.d.ts +21 -0
- package/dist/types/Utils/ColorMath.d.ts +92 -0
- package/dist/types/Utils/EffectMath.d.ts +643 -0
- package/dist/types/Utils/TagVocabulary.d.ts +104 -0
- package/dist/types/Utils/TagVocabulary.test.d.ts +1 -0
- package/dist/types/Utils/color.d.ts +29 -0
- package/dist/types/Utils/dom-to-svg.d.ts +28 -0
- package/dist/types/Utils/dom-to-svg.test.d.ts +1 -0
- package/dist/types/Utils/treeTransformers.d.ts +160 -0
- package/dist/types/Visitors/BBBlocksExporter.d.ts +27 -0
- package/dist/types/Visitors/BBCodeExporter.d.ts +90 -0
- package/dist/types/Visitors/BlockPatcher.d.ts +91 -0
- package/dist/types/Visitors/DOMMorpher.d.ts +13 -0
- package/dist/types/Visitors/HTMLRenderer.d.ts +380 -0
- package/dist/types/Visitors/JSONExporter.d.ts +42 -0
- package/dist/types/Visitors/MarkdownExporter.d.ts +28 -0
- package/dist/types/Visitors/SVGRenderer.d.ts +16 -0
- package/dist/types/Visitors/TiptapExporter.d.ts +40 -0
- package/dist/types/Visitors/Visitor.d.ts +35 -0
- package/dist/types/Visitors/index.d.ts +9 -0
- package/dist/types/Visuals/BoxDrawer.d.ts +49 -0
- package/dist/types/Visuals/index.d.ts +38 -0
- package/dist/types/index.d.ts +154 -0
- package/package.json +14 -10
- package/src/Analysis/Passes/Analysis/ColorUsageAnalyzer.ts +103 -0
- package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +294 -20
- package/src/Analysis/Passes/Analysis/SymbolAnalyzer.ts +257 -0
- package/src/Analysis/Passes/Decision/PaletteRemapDecision.ts +425 -0
- package/src/Analysis/Pipeline/Pipeline.ts +15 -2
- package/src/Analysis/index.ts +8 -2
- package/src/BBCode/BBCodeDocumentModel.ts +3 -0
- package/src/BBCode/BBCodeToGreenNode.ts +34 -11
- package/src/BBCode/Parser.ts +38 -1
- package/src/Edits/EditPlan.ts +290 -0
- package/src/Edits/Optimizer.ts +185 -0
- package/src/Edits/Rules/Rule.ts +145 -0
- package/src/Edits/Rules/dropEmptyTags.ts +110 -0
- package/src/Edits/Rules/dropRedundantNesting.ts +120 -0
- package/src/Edits/Rules/index.ts +28 -0
- package/src/Edits/Rules/mergeAdjacent.ts +224 -0
- package/src/Edits/Rules/reorderWrappers.ts +154 -0
- package/src/Edits/Rules/shortenHex.ts +79 -0
- package/src/Edits/Rules/tagValue.ts +105 -0
- package/src/Edits/Rules/unwrapInvisibleColor.ts +93 -0
- package/src/Edits/applyEdits.ts +54 -0
- package/src/Edits/index.ts +22 -0
- package/src/Events/EventBus.ts +18 -0
- package/src/HTML/HTMLToGreenNode.ts +185 -37
- package/src/Incremental/BracketIndex.ts +249 -0
- package/src/Incremental/IncrementalParser.ts +464 -41
- package/src/Lexer/BBCodeLexer.ts +25 -3
- package/src/Lexer/index.ts +1 -1
- package/src/Markdown/MarkdownAST.ts +65 -2
- package/src/Markdown/MarkdownLexer.ts +69 -14
- package/src/Markdown/MarkdownParser.ts +749 -62
- package/src/Markdown/MarkdownToGreenNode.ts +148 -27
- package/src/MilHibri/MilHibriDocumentModel.ts +56 -0
- package/src/Model/DocumentModel.ts +138 -5
- package/src/Model/TagRegistry.ts +132 -134
- package/src/Reconciler/SurgicalReconciler.ts +460 -0
- package/src/Repair/NestingRepair.ts +116 -0
- package/src/Repair/index.ts +1 -0
- package/src/Semantic/SemanticAnalyzer.ts +1802 -65
- package/src/Syntax/GreenNodePool.ts +12 -12
- package/src/Syntax/RedNode.ts +17 -3
- package/src/Syntax/RedNodeStore.ts +9 -1
- package/src/Syntax/nodeAttr.ts +172 -0
- package/src/Tokens/TokenResolver.ts +55 -0
- package/src/Tokens/index.ts +1 -0
- package/src/Types/core.ts +15 -0
- package/src/Utils/ColorMath.ts +276 -55
- package/src/Utils/EffectMath.ts +2462 -0
- package/src/Utils/TagVocabulary.ts +141 -0
- package/src/Utils/dom-to-svg.ts +28 -12
- package/src/Visitors/BBBlocksExporter.ts +15 -1
- package/src/Visitors/BBCodeExporter.ts +319 -32
- package/src/Visitors/BlockPatcher.ts +87 -11
- package/src/Visitors/DOMMorpher.ts +15 -5
- package/src/Visitors/HTMLRenderer.ts +452 -142
- package/src/Visitors/MarkdownExporter.ts +98 -41
- package/src/Visitors/TiptapExporter.ts +1 -0
- package/src/Visuals/index.ts +7 -4
- package/src/Visuals/lyne.css +191 -21
- package/src/Visuals/osu.css +36 -5
- package/src/index.ts +172 -5
- package/dist/index.d.mts +0 -4609
- package/dist/index.d.ts +0 -4609
- package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +0 -135
- package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +0 -84
- package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +0 -99
- package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +0 -119
- package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +0 -179
- package/src/Analysis/__tests__/Integration.test.ts +0 -162
- package/src/Analysis/__tests__/Pipeline.test.ts +0 -133
- package/src/Diff/__tests__/TreeDiffer.test.ts +0 -65
- package/src/Incremental/__tests__/IncrementalParser.test.ts +0 -164
- package/src/Syntax/__tests__/GreenNode.test.ts +0 -33
- package/src/Syntax/__tests__/RedNode.test.ts +0 -81
- package/src/Syntax/__tests__/RedNodeStore.test.ts +0 -104
- package/src/Tests/ASTOptimizerIdempotence.test.ts +0 -77
- package/src/Tests/BlockPatcher.test.ts +0 -437
- package/src/Tests/BlockPatcherWindowed.test.ts +0 -364
- package/src/Tests/BoxDrawer.test.ts +0 -217
- package/src/Tests/BoxRichTitle.test.ts +0 -105
- package/src/Tests/Chars500kBenchmark.test.ts +0 -151
- package/src/Tests/Chars500kEdits.test.ts +0 -321
- package/src/Tests/CollabPositions.test.ts +0 -146
- package/src/Tests/CompilerPathProfiling.test.ts +0 -186
- package/src/Tests/DOMMorpher.test.ts +0 -142
- package/src/Tests/DomPatchPerf.test.ts +0 -60
- package/src/Tests/EffectSegments.snapshot.json +0 -616
- package/src/Tests/EffectSegments.test.ts +0 -68
- package/src/Tests/FindNodeAtOffset.test.ts +0 -65
- package/src/Tests/Fuzzer.test.ts +0 -166
- package/src/Tests/GreenNodePool.test.ts +0 -153
- package/src/Tests/Lexer.test.ts +0 -238
- package/src/Tests/LyneMode.test.ts +0 -291
- package/src/Tests/ModelCoherence.test.ts +0 -180
- package/src/Tests/Partition.test.ts +0 -238
- package/src/Tests/PluginTags.test.ts +0 -150
- package/src/Tests/ProblematicSection.test.ts +0 -46
- package/src/Tests/ProblematicSectionHTML.test.ts +0 -58
- package/src/Tests/RedReuse.test.ts +0 -134
- package/src/Tests/ReproDelete20k.test.ts +0 -62
- package/src/Tests/SemanticValidators.test.ts +0 -136
- package/src/Tests/StableNodeIds.test.ts +0 -210
- package/src/Tests/StudioColorBloat.test.ts +0 -25
- package/src/Tests/StudioDebugText.test.ts +0 -27
- package/src/Tests/StudioTrailingChar.test.ts +0 -25
- package/src/Tests/StudioValidText.test.ts +0 -25
- package/src/Tests/UrlImgBug.test.ts +0 -23
- package/src/Tests/VisualBuilderFidelity.test.ts +0 -105
- package/src/Tests/referenceDocument.ts +0 -119
- package/src/Utils/dom-to-svg.test.ts +0 -86
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The partition invariant (roadmap point 14).
|
|
3
|
-
*
|
|
4
|
-
* The tree must account for every character of the source exactly once. This
|
|
5
|
-
* was false in three independent ways before the width model, and each one is
|
|
6
|
-
* pinned here by the minimal case that exposed it — because the failure mode
|
|
7
|
-
* is silent: nothing crashes, offsets just quietly stop meaning anything, and
|
|
8
|
-
* everything built on them (`findNodeAtOffset`, the Monaco↔AST mapping,
|
|
9
|
-
* diagnostics, incremental reparse) inherits the ambiguity.
|
|
10
|
-
*
|
|
11
|
-
* Since point 5, green nodes carry widths and no positions, so most of the
|
|
12
|
-
* invariant holds by construction. What these tests check is therefore split
|
|
13
|
-
* in two: the WIDTHS on the green tree, and the POSITIONS the red tree derives
|
|
14
|
-
* from them — which is where a mistake would now actually show up.
|
|
15
|
-
*/
|
|
16
|
-
import { describe, it, expect } from 'vitest'
|
|
17
|
-
import { REFERENCE_DOCUMENT, REFERENCE_DOCUMENT_WITH_GRADIENT } from './referenceDocument'
|
|
18
|
-
import { parseBBCode } from '../BBCode/Parser'
|
|
19
|
-
import { greenToRedNode } from '../BBCode/BBCodeToGreenNode'
|
|
20
|
-
import { checkPartition } from '../Syntax/partition'
|
|
21
|
-
import { GreenNode } from '../Syntax/GreenNode'
|
|
22
|
-
import type { RedNode } from '../Syntax/RedNode'
|
|
23
|
-
|
|
24
|
-
const violations = (source: string, strictMode = false) =>
|
|
25
|
-
checkPartition(parseBBCode(source, { strictMode }), source.length, {
|
|
26
|
-
limit: 50,
|
|
27
|
-
checkLeafWidths: true,
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
const expectPartitions = (source: string, strictMode = false) => {
|
|
31
|
-
const vs = violations(source, strictMode)
|
|
32
|
-
expect(
|
|
33
|
-
vs.map(v => `${v.kind} at ${v.path}: ${v.detail}`),
|
|
34
|
-
`source: ${JSON.stringify(source)}`,
|
|
35
|
-
).toEqual([])
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Parse and build the red tree, where absolute positions live. */
|
|
39
|
-
const redOf = (source: string): RedNode => greenToRedNode(parseBBCode(source))
|
|
40
|
-
|
|
41
|
-
/** Every character offset must resolve to exactly one owning node. */
|
|
42
|
-
function ownersPerOffset(root: RedNode, length: number): number[] {
|
|
43
|
-
const owners = new Array<number>(length).fill(0)
|
|
44
|
-
const stack: RedNode[] = [root]
|
|
45
|
-
while (stack.length > 0) {
|
|
46
|
-
const node = stack.pop()!
|
|
47
|
-
// A node OWNS only what its children do not: its delimiters.
|
|
48
|
-
if (node.children.length === 0) {
|
|
49
|
-
for (let i = node.range.start; i < node.range.end; i++) owners[i]++
|
|
50
|
-
} else {
|
|
51
|
-
const first = node.children[0]
|
|
52
|
-
const last = node.children[node.children.length - 1]
|
|
53
|
-
for (let i = node.range.start; i < first.range.start; i++) owners[i]++
|
|
54
|
-
for (let i = last.range.end; i < node.range.end; i++) owners[i]++
|
|
55
|
-
for (const c of node.children) stack.push(c)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return owners
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
describe('partition invariant', () => {
|
|
62
|
-
describe('the three original defects', () => {
|
|
63
|
-
it('delimiters are accounted for by leading/trailing width', () => {
|
|
64
|
-
const bold = redOf('[b]hola[/b]').children[0].children[0]
|
|
65
|
-
|
|
66
|
-
expect(bold.range).toEqual({ start: 0, end: 11 })
|
|
67
|
-
expect(bold.green.leadingWidth).toBe(3) // '[b]'
|
|
68
|
-
expect(bold.green.trailingWidth).toBe(4) // '[/b]'
|
|
69
|
-
expect(bold.children[0].range).toEqual({ start: 3, end: 7 })
|
|
70
|
-
expectPartitions('[b]hola[/b]')
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('siblings never share a range — the spacing/empty_line case', () => {
|
|
74
|
-
// This exact input produced `spacing [4..6]` AND `empty_line [4..6]`.
|
|
75
|
-
const kinds: { kind: string; start: number; end: number }[] = []
|
|
76
|
-
const collect = (n: RedNode): void => {
|
|
77
|
-
if (n.kind === 'spacing' || n.kind === 'empty_line') {
|
|
78
|
-
kinds.push({ kind: n.kind, start: n.range.start, end: n.range.end })
|
|
79
|
-
}
|
|
80
|
-
for (const c of n.children) collect(c)
|
|
81
|
-
}
|
|
82
|
-
collect(redOf('hola\n\nmundo'))
|
|
83
|
-
|
|
84
|
-
expect(kinds).toEqual([
|
|
85
|
-
{ kind: 'spacing', start: 4, end: 5 },
|
|
86
|
-
{ kind: 'empty_line', start: 5, end: 6 },
|
|
87
|
-
])
|
|
88
|
-
expectPartitions('hola\n\nmundo')
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('an orphaned closing tag survives as text instead of vanishing', () => {
|
|
92
|
-
const source = 'hola [/b] mundo'
|
|
93
|
-
const texts: string[] = []
|
|
94
|
-
parseBBCode(source).walk(n => { if (n.kind === 'text') texts.push(n.text) })
|
|
95
|
-
|
|
96
|
-
expect(texts.join('')).toBe(source)
|
|
97
|
-
expectPartitions(source)
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('an auto-closed inner tag does not swallow the closing delimiter', () => {
|
|
102
|
-
// `[b][i]x[/b]` used to give italic and bold BOTH ending at 11.
|
|
103
|
-
const bold = redOf('[b][i]x[/b]').children[0].children[0]
|
|
104
|
-
const italic = bold.children[0]
|
|
105
|
-
|
|
106
|
-
expect(bold.range).toEqual({ start: 0, end: 11 })
|
|
107
|
-
expect(italic.range).toEqual({ start: 3, end: 7 })
|
|
108
|
-
expect(italic.green.trailingWidth).toBe(0)
|
|
109
|
-
expect(bold.green.trailingWidth).toBe(4)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('the root always spans the whole source', () => {
|
|
113
|
-
for (const source of ['', 'hola', '[/b]', '[/b][/i]', '[b]', 'a\n']) {
|
|
114
|
-
const tree = parseBBCode(source)
|
|
115
|
-
expect([source, tree.width]).toEqual([source, source.length])
|
|
116
|
-
expect([source, redOf(source).range]).toEqual([
|
|
117
|
-
source,
|
|
118
|
-
{ start: 0, end: source.length },
|
|
119
|
-
])
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
describe('holds on real and hostile input', () => {
|
|
124
|
-
// Was two untracked `.milia` files read from disk; see `referenceDocument.ts`.
|
|
125
|
-
const fixtures = {
|
|
126
|
-
'reference document': REFERENCE_DOCUMENT,
|
|
127
|
-
'reference document with gradient': REFERENCE_DOCUMENT_WITH_GRADIENT,
|
|
128
|
-
}
|
|
129
|
-
for (const [name, source] of Object.entries(fixtures)) {
|
|
130
|
-
it(`fixture ${name}`, () => {
|
|
131
|
-
expectPartitions(source)
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const cases = [
|
|
136
|
-
'', 'hola', 'hola\n\nmundo', '\n\n\n\n', 'a\r\n\r\nb',
|
|
137
|
-
'[b]hola[/b]', '[b][i]x[/b]', '[/b]', '[b]sin cerrar', '[b][/b]',
|
|
138
|
-
'[code]raw [b]x[/b][/code]', '[code]sin cerrar', '[c]x[/c]',
|
|
139
|
-
'[list][*]a[*]b[/list]', '[list][*]a', '[*]suelto',
|
|
140
|
-
'[Gateron]', '[90 misses]', '[[[[', ']]]]', '[',
|
|
141
|
-
'[box=[b]t[/b]]c[/box]', '[b]hola[/b]texto[i]y[/i]',
|
|
142
|
-
'[url=https://osu.ppy.sh]x[/url][/url]', '[quote][/b][/quote]',
|
|
143
|
-
'🎵[b]á€ñ[/b]🎵',
|
|
144
|
-
]
|
|
145
|
-
for (const source of cases) {
|
|
146
|
-
it(`case ${JSON.stringify(source)}`, () => {
|
|
147
|
-
expectPartitions(source)
|
|
148
|
-
expectPartitions(source, true) // strict mode too
|
|
149
|
-
})
|
|
150
|
-
}
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
it('every offset has exactly one owner', () => {
|
|
154
|
-
const source = '[centre][b]hola[/b]\n\n[/i]mundo[list][*]a[/list][b]abierto'
|
|
155
|
-
const owners = ownersPerOffset(redOf(source), source.length)
|
|
156
|
-
|
|
157
|
-
const unowned = owners.flatMap((c, i) => (c === 0 ? [i] : []))
|
|
158
|
-
const shared = owners.flatMap((c, i) => (c > 1 ? [i] : []))
|
|
159
|
-
expect({ unowned, shared }).toEqual({ unowned: [], shared: [] })
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('red offsets agree with the source text they claim', () => {
|
|
163
|
-
// The check that only exists once positions are DERIVED: every leaf must
|
|
164
|
-
// still be able to find itself in the source at the offset it reports.
|
|
165
|
-
const source = REFERENCE_DOCUMENT_WITH_GRADIENT
|
|
166
|
-
const mismatches: string[] = []
|
|
167
|
-
const visit = (n: RedNode): void => {
|
|
168
|
-
if (n.kind === 'text' && mismatches.length < 5) {
|
|
169
|
-
const slice = source.slice(n.range.start, n.range.end)
|
|
170
|
-
if (slice !== n.text) {
|
|
171
|
-
mismatches.push(`[${n.range.start}..${n.range.end}] ${JSON.stringify(slice)} != ${JSON.stringify(n.text)}`)
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
for (const c of n.children) visit(c)
|
|
175
|
-
}
|
|
176
|
-
visit(greenToRedNode(parseBBCode(source)))
|
|
177
|
-
|
|
178
|
-
expect(mismatches).toEqual([])
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
describe('seeded fuzz', () => {
|
|
182
|
-
// mulberry32 — same generator the other fuzz suites use, so a failure is
|
|
183
|
-
// reproducible from the seed alone.
|
|
184
|
-
function mulberry32(seed: number): () => number {
|
|
185
|
-
let a = seed >>> 0
|
|
186
|
-
return () => {
|
|
187
|
-
a = (a + 0x6d2b79f5) >>> 0
|
|
188
|
-
let t = a
|
|
189
|
-
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
190
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
191
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const TAGS = ['b', 'i', 'u', 'color=red', 'size=50', 'url=https://osu.ppy.sh',
|
|
196
|
-
'notice', 'centre', 'box=t', 'spoiler', 'list', 'code', 'quote']
|
|
197
|
-
const WORDS = ['osu!', 'peppy', 'combo', 'hola', 'mundo', '[', ']', '[/', 'á€ñ', '🎵']
|
|
198
|
-
const SPACES = [' ', ' ', '\n', '\n\n', '\n\n\n', '\r\n', '']
|
|
199
|
-
|
|
200
|
-
function genDoc(rnd: () => number, depth = 0): string {
|
|
201
|
-
const pick = <T>(xs: T[]): T => xs[Math.floor(rnd() * xs.length)]
|
|
202
|
-
let out = ''
|
|
203
|
-
const n = 1 + Math.floor(rnd() * 6)
|
|
204
|
-
for (let i = 0; i < n; i++) {
|
|
205
|
-
const c = Math.floor(rnd() * 12)
|
|
206
|
-
if (c < 3) out += pick(WORDS) + pick(SPACES)
|
|
207
|
-
else if (c < 6 && depth < 4) {
|
|
208
|
-
const t = pick(TAGS)
|
|
209
|
-
out += `[${t}]${genDoc(rnd, depth + 1)}[/${t.split('=')[0]}]`
|
|
210
|
-
} else if (c < 7) out += `[/${pick(TAGS).split('=')[0]}]` // orphan close
|
|
211
|
-
else if (c < 8 && depth < 4) out += `[${pick(TAGS)}]${genDoc(rnd, depth + 1)}` // unclosed
|
|
212
|
-
else if (c < 9) out += `[${pick(['Gateron', '90 misses', '???'])}]` // unknown tag
|
|
213
|
-
else if (c < 10) out += pick(['[*]', '[*] item', '[[[[', ']]]]'])
|
|
214
|
-
else out += pick(SPACES) + pick(WORDS)
|
|
215
|
-
}
|
|
216
|
-
return out
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
for (const seed of [1, 7, 42, 1337, 99999, 0x5eed1a57]) {
|
|
220
|
-
it(`seed ${seed}: 1000 documents partition their source`, () => {
|
|
221
|
-
const rnd = mulberry32(seed)
|
|
222
|
-
for (let i = 0; i < 1000; i++) {
|
|
223
|
-
const source = genDoc(rnd)
|
|
224
|
-
const vs = checkPartition(parseBBCode(source), source.length, {
|
|
225
|
-
limit: 3,
|
|
226
|
-
checkLeafWidths: true,
|
|
227
|
-
})
|
|
228
|
-
if (vs.length > 0) {
|
|
229
|
-
throw new Error(
|
|
230
|
-
`seed ${seed} doc #${i}\n src: ${JSON.stringify(source)}\n` +
|
|
231
|
-
vs.map(v => ` ${v.kind} at ${v.path}: ${v.detail}`).join('\n'),
|
|
232
|
-
)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
})
|
|
236
|
-
}
|
|
237
|
-
})
|
|
238
|
-
})
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { BBCodeDocumentModel } from '../BBCode/BBCodeDocumentModel'
|
|
3
|
-
import { PluginAPI } from '../Plugins/PluginAPI'
|
|
4
|
-
import { HTMLRenderer } from '../Visitors/HTMLRenderer'
|
|
5
|
-
import { BBCodeExporter } from '../Visitors/BBCodeExporter'
|
|
6
|
-
import type { RedNode } from '../Syntax/RedNode'
|
|
7
|
-
import type { NodeKind } from '../Types/core'
|
|
8
|
-
import type { TagDefinition } from '../Model/TagRegistry'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* A plugin tag must live the FULL life of a tag: lex → parse → tree → render
|
|
12
|
-
* → export → re-parse. Before `ParseOptions.extraTags`, a registered tag
|
|
13
|
-
* could render and export but never parse — the parser turned it into
|
|
14
|
-
* literal text, so the rest of the pipeline had nothing to work with. The
|
|
15
|
-
* platform's plugin promise starts at the parser or it doesn't start.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
function findKind(root: RedNode, kind: string): RedNode | null {
|
|
19
|
-
let found: RedNode | null = null
|
|
20
|
-
root.walk(n => {
|
|
21
|
-
if (!found && n.kind === kind) found = n
|
|
22
|
-
})
|
|
23
|
-
return found
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const blurTag: TagDefinition = {
|
|
27
|
-
name: 'blur',
|
|
28
|
-
kind: 'blur' as NodeKind,
|
|
29
|
-
label: 'Blur',
|
|
30
|
-
category: 'text',
|
|
31
|
-
isInline: true,
|
|
32
|
-
isSelfClosing: false,
|
|
33
|
-
canHaveChildren: true,
|
|
34
|
-
toRenderNode: (ctx) => ({
|
|
35
|
-
kind: 'blur',
|
|
36
|
-
text: '',
|
|
37
|
-
children: [{ kind: 'text', text: `⟦blur⟧${ctx.visitChildren(ctx.node)}⟦/blur⟧`, children: [], props: {} }],
|
|
38
|
-
props: {},
|
|
39
|
-
}),
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const wiggleTag: TagDefinition = {
|
|
43
|
-
name: 'wiggle',
|
|
44
|
-
kind: 'wiggle' as NodeKind,
|
|
45
|
-
label: 'Wiggle',
|
|
46
|
-
isInline: true,
|
|
47
|
-
isSelfClosing: false,
|
|
48
|
-
canHaveChildren: true,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe('plugin tags — full pipeline', () => {
|
|
52
|
-
it('parses a registered tag as a real container, with attrs and nesting', () => {
|
|
53
|
-
const model = new BBCodeDocumentModel({ source: '' })
|
|
54
|
-
model.tagRegistry.register(blurTag)
|
|
55
|
-
model.tagRegistry.register(wiggleTag)
|
|
56
|
-
|
|
57
|
-
model.rebuild('hola [blur]mundo [b]fuerte[/b][/blur] y [wiggle=3]algo[/wiggle]')
|
|
58
|
-
|
|
59
|
-
const blur = findKind(model.redRoot!, 'blur')!
|
|
60
|
-
expect(blur).toBeTruthy()
|
|
61
|
-
expect(findKind(blur, 'bold')).toBeTruthy() // nesting inside the plugin tag
|
|
62
|
-
const wiggle = findKind(model.redRoot!, 'wiggle')!
|
|
63
|
-
expect(wiggle.text).toBe('=3') // attrs preserved like any builtin
|
|
64
|
-
|
|
65
|
-
// The tree still partitions the source: the plugin tag owns its delimiters.
|
|
66
|
-
expect(blur.range.end - blur.range.start).toBe('[blur]mundo [b]fuerte[/b][/blur]'.length)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('unregistered tags still fall to literal text — prose stays visible', () => {
|
|
70
|
-
const model = new BBCodeDocumentModel({ source: 'combo de [90 misses] con [Gateron Yellow]' })
|
|
71
|
-
expect(findKind(model.redRoot!, 'custom')).toBeNull()
|
|
72
|
-
const html = new HTMLRenderer().render(model.redRoot!)
|
|
73
|
-
expect(html).toContain('[90 misses]')
|
|
74
|
-
expect(html).toContain('[Gateron Yellow]')
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('renders through the registry handler, and safely without it', () => {
|
|
78
|
-
const model = new BBCodeDocumentModel({ source: '' })
|
|
79
|
-
model.tagRegistry.register(blurTag)
|
|
80
|
-
model.rebuild('[blur]contenido[/blur]')
|
|
81
|
-
|
|
82
|
-
const withRegistry = new HTMLRenderer({ registry: model.tagRegistry }).render(model.redRoot!)
|
|
83
|
-
expect(withRegistry).toContain('⟦blur⟧')
|
|
84
|
-
expect(withRegistry).toContain('contenido')
|
|
85
|
-
|
|
86
|
-
// Without the registry the wrapper is unknown, but content must survive.
|
|
87
|
-
const bare = new HTMLRenderer().render(model.redRoot!)
|
|
88
|
-
expect(bare).toContain('contenido')
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('exports under its registered name and round-trips', () => {
|
|
92
|
-
const model = new BBCodeDocumentModel({ source: '' })
|
|
93
|
-
model.tagRegistry.register(blurTag)
|
|
94
|
-
model.tagRegistry.register(wiggleTag)
|
|
95
|
-
const source = 'hola [blur]mundo[/blur] y [wiggle=3]algo[/wiggle]'
|
|
96
|
-
model.rebuild(source)
|
|
97
|
-
|
|
98
|
-
const exporter = new BBCodeExporter(model.tagRegistry)
|
|
99
|
-
const exported = exporter.export(model.redRoot!)
|
|
100
|
-
expect(exported).toBe(source)
|
|
101
|
-
|
|
102
|
-
// Fixpoint: parsing the export reproduces the same export.
|
|
103
|
-
model.rebuild(exported)
|
|
104
|
-
expect(exporter.export(model.redRoot!)).toBe(exported)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('works through PluginAPI, and unregistering returns the tag to prose', () => {
|
|
108
|
-
const model = new BBCodeDocumentModel({ source: '' })
|
|
109
|
-
const api = new PluginAPI(model)
|
|
110
|
-
api.registerPlugin(
|
|
111
|
-
{ name: 'blur-plugin', version: '1.0.0' },
|
|
112
|
-
{ tags: [blurTag] },
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
model.rebuild('[blur]x[/blur]')
|
|
116
|
-
expect(findKind(model.redRoot!, 'blur')).toBeTruthy()
|
|
117
|
-
|
|
118
|
-
api.unregisterPlugin('blur-plugin')
|
|
119
|
-
model.rebuild('[blur]x[/blur]')
|
|
120
|
-
expect(findKind(model.redRoot!, 'blur')).toBeNull()
|
|
121
|
-
// Back to what unknown tags always were: visible text.
|
|
122
|
-
expect(new HTMLRenderer().render(model.redRoot!)).toContain('[blur]')
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('incremental editing with plugin tags matches a full rebuild', () => {
|
|
126
|
-
let src = ''
|
|
127
|
-
for (let i = 0; i < 60; i++) src += `parrafo ${i} con [blur]nucleo ${i}[/blur] normal\n\n`
|
|
128
|
-
|
|
129
|
-
const reuse = new BBCodeDocumentModel({ source: '' })
|
|
130
|
-
const control = new BBCodeDocumentModel({ source: '', incremental: false })
|
|
131
|
-
reuse.tagRegistry.register(blurTag)
|
|
132
|
-
control.tagRegistry.register(blurTag)
|
|
133
|
-
reuse.rebuild(src)
|
|
134
|
-
control.rebuild(src)
|
|
135
|
-
|
|
136
|
-
const renderer = new HTMLRenderer()
|
|
137
|
-
for (let edit = 0; edit < 30; edit++) {
|
|
138
|
-
const at = (edit * 137) % (reuse.source.length - 10)
|
|
139
|
-
const next = reuse.source.slice(0, at) + 'x' + reuse.source.slice(at)
|
|
140
|
-
reuse.applyTextUpdate(next)
|
|
141
|
-
control.applyTextUpdate(next)
|
|
142
|
-
|
|
143
|
-
// Sin los ids: son identidades por modelo, y aquí hay dos modelos
|
|
144
|
-
// independientes. Lo que debe coincidir es la estructura.
|
|
145
|
-
const sinIds = (html: string) => html.replace(/ data-node-id="[^"]*"/g, '')
|
|
146
|
-
expect(sinIds(renderer.render(reuse.redRoot!))).toBe(sinIds(renderer.render(control.redRoot!)))
|
|
147
|
-
expect(reuse.redRoot!.green).toBe(reuse.greenRoot)
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
})
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { TagRegistry } from '../Model/TagRegistry'
|
|
3
|
-
import { BBCodeExporter } from '../Visitors/BBCodeExporter'
|
|
4
|
-
import { processStudioAST } from '@miliastry/quasar-studio'
|
|
5
|
-
import { REFERENCE_DOCUMENT, REFERENCE_GRADIENT_LAYER } from './referenceDocument'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Round-tripping a document through the Studio must not invent or drop layout.
|
|
9
|
-
*
|
|
10
|
-
* The name of this suite always promised that, but the body only rendered the
|
|
11
|
-
* document and `console.log`ged the result — `expect` was imported and never
|
|
12
|
-
* called, so it could not fail. It also read an untracked `.milia` fixture,
|
|
13
|
-
* which is what finally broke it. Both are fixed here: the invariant its name
|
|
14
|
-
* describes is now actually asserted, against `referenceDocument.ts`.
|
|
15
|
-
*
|
|
16
|
-
* Braille blanks (`⠀`, U+2800) are load-bearing in these posts — authors use
|
|
17
|
-
* them as padding because osu! collapses ordinary whitespace. Gaining or losing
|
|
18
|
-
* one silently reflows somebody's profile.
|
|
19
|
-
*/
|
|
20
|
-
describe('Problematic Section Export', () => {
|
|
21
|
-
const exportWithLayers = (source: string, layers: unknown[]) => {
|
|
22
|
-
const registry = new TagRegistry()
|
|
23
|
-
const { redRoot } = processStudioAST(source, layers as never, {})
|
|
24
|
-
return new BBCodeExporter(registry).export(redRoot)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const brailleCount = (s: string) => (s.match(/⠀/g) || []).length
|
|
28
|
-
|
|
29
|
-
it('preserves every braille blank through a plain round trip', () => {
|
|
30
|
-
const exported = exportWithLayers(REFERENCE_DOCUMENT, [])
|
|
31
|
-
expect(brailleCount(exported)).toBe(brailleCount(REFERENCE_DOCUMENT))
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('preserves every braille blank through a gradient layer', () => {
|
|
35
|
-
// A gradient splits text into one node per character; the padding must come
|
|
36
|
-
// out the far side with the same count it went in with.
|
|
37
|
-
const exported = exportWithLayers(REFERENCE_DOCUMENT, [REFERENCE_GRADIENT_LAYER])
|
|
38
|
-
expect(brailleCount(exported)).toBe(brailleCount(REFERENCE_DOCUMENT))
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('does not add a trailing blank line', () => {
|
|
42
|
-
const exported = exportWithLayers(REFERENCE_DOCUMENT, [])
|
|
43
|
-
const trailing = (s: string) => s.length - s.replace(/\n+$/, '').length
|
|
44
|
-
expect(trailing(exported)).toBe(trailing(REFERENCE_DOCUMENT))
|
|
45
|
-
})
|
|
46
|
-
})
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { TagRegistry } from '../Model/TagRegistry'
|
|
3
|
-
import { HTMLRenderer } from '../Visitors/HTMLRenderer'
|
|
4
|
-
import { processStudioAST } from '@miliastry/quasar-studio'
|
|
5
|
-
import { REFERENCE_DOCUMENT, REFERENCE_GRADIENT_LAYER } from './referenceDocument'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* A substantial document rendered with and without a Studio gradient layer.
|
|
9
|
-
*
|
|
10
|
-
* This used to assert nothing at all: it rendered both variants and wrote
|
|
11
|
-
* them to `orig.html` and `grad.html` in the repo root — tracked files, so
|
|
12
|
-
* every test run dirtied the working tree, and no failure was possible. What
|
|
13
|
-
* it should have been checking is below.
|
|
14
|
-
*/
|
|
15
|
-
describe('Problematic Section HTML Output', () => {
|
|
16
|
-
// Was an untracked `.milia` read from disk; see `referenceDocument.ts`.
|
|
17
|
-
const text = REFERENCE_DOCUMENT
|
|
18
|
-
const layers = [REFERENCE_GRADIENT_LAYER]
|
|
19
|
-
|
|
20
|
-
const render = (studioLayers: unknown[]) => {
|
|
21
|
-
const registry = new TagRegistry()
|
|
22
|
-
const renderer = new HTMLRenderer({ registry })
|
|
23
|
-
const { redRoot } = processStudioAST(text, studioLayers as never, {})
|
|
24
|
-
return renderer.render(redRoot)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
it('renders the document without a gradient layer', () => {
|
|
28
|
-
const html = render([])
|
|
29
|
-
|
|
30
|
-
expect(html.length).toBeGreaterThan(1000)
|
|
31
|
-
// Structure of the source survives to the output.
|
|
32
|
-
expect(html).toContain('data-node-id')
|
|
33
|
-
// No per-character colouring without a gradient layer.
|
|
34
|
-
expect(html).not.toMatch(/color:\s*#e8b04b/i)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('a gradient layer colours the text, and only the text', () => {
|
|
38
|
-
const plain = render([])
|
|
39
|
-
const gradient = render(layers)
|
|
40
|
-
|
|
41
|
-
expect(gradient).not.toBe(plain)
|
|
42
|
-
// The layer's own stops reach the output as inline colours.
|
|
43
|
-
expect(gradient).toMatch(/color:\s*#e8b04b/i)
|
|
44
|
-
// Colorear no puede cambiar el TEXTO, que es la invariante de verdad
|
|
45
|
-
// (contar `data-node-id` no servía: el degradado parte el texto en un
|
|
46
|
-
// nodo por carácter, así que el número cambia legítimamente).
|
|
47
|
-
const texto = (s: string) => s.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim()
|
|
48
|
-
expect(texto(gradient)).toBe(texto(plain))
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('rendering is deterministic', () => {
|
|
52
|
-
// Node ids come from a monotonic per-process counter, so two parses of
|
|
53
|
-
// the same text legitimately number their nodes differently. Identity is
|
|
54
|
-
// not content: strip it and the output must be identical.
|
|
55
|
-
const withoutIds = (html: string) => html.replace(/ data-node-id="[^"]*"/g, '')
|
|
56
|
-
expect(withoutIds(render(layers))).toBe(withoutIds(render(layers)))
|
|
57
|
-
})
|
|
58
|
-
})
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { BBCodeDocumentModel } from '../BBCode/BBCodeDocumentModel'
|
|
3
|
-
import { HTMLRenderer } from '../Visitors/HTMLRenderer'
|
|
4
|
-
import type { RedNode } from '../Syntax/RedNode'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Red-subtree reuse: on the incremental path, subtrees whose green is shared
|
|
8
|
-
* by reference must be ADOPTED (same RedNode objects) instead of rebuilt —
|
|
9
|
-
* building red was the largest phase of a keystroke.
|
|
10
|
-
*
|
|
11
|
-
* The property demanded is the engine's usual one: the resulting tree must be
|
|
12
|
-
* IDENTICAL — kind, text, range, structure, metadata — to what a full rebuild
|
|
13
|
-
* of the same final text produces. Ids are exempt (reuse is what makes them
|
|
14
|
-
* stable; a fresh rebuild has no history), and object identity is the point.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
function bigDoc(): string {
|
|
18
|
-
// Comfortably above MIN_SOURCE_LENGTH (2500) so the incremental path engages.
|
|
19
|
-
let src = ''
|
|
20
|
-
for (let i = 0; i < 40; i++) {
|
|
21
|
-
src += `parrafo numero ${i} con [b]negrita[/b] y [color=#ff66ab]color[/color]\n\n`
|
|
22
|
-
}
|
|
23
|
-
src += '[notice]interior del notice con [i]cursiva[/i] y texto largo para editar[/notice]\n\n'
|
|
24
|
-
for (let i = 0; i < 20; i++) {
|
|
25
|
-
src += `cola numero ${i} despues del notice\n\n`
|
|
26
|
-
}
|
|
27
|
-
return src
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Walk both trees asserting deep equality of everything but ids. */
|
|
31
|
-
function expectSameTree(a: RedNode, b: RedNode, path = 'root'): void {
|
|
32
|
-
expect(a.kind, `${path}: kind`).toBe(b.kind)
|
|
33
|
-
expect(a.text, `${path}: text`).toBe(b.text)
|
|
34
|
-
expect(a.range.start, `${path}: range.start`).toBe(b.range.start)
|
|
35
|
-
expect(a.range.end, `${path}: range.end`).toBe(b.range.end)
|
|
36
|
-
expect(JSON.stringify(a.metadata), `${path}: metadata`).toBe(JSON.stringify(b.metadata))
|
|
37
|
-
expect(a.children.length, `${path}: childCount`).toBe(b.children.length)
|
|
38
|
-
for (let i = 0; i < a.children.length; i++) {
|
|
39
|
-
expectSameTree(a.children[i], b.children[i], `${path}.${a.children[i].kind}[${i}]`)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function mulberry32(seed: number): () => number {
|
|
44
|
-
let a = seed >>> 0
|
|
45
|
-
return () => {
|
|
46
|
-
a |= 0; a = (a + 0x6D2B79F5) | 0
|
|
47
|
-
let t = Math.imul(a ^ (a >>> 15), 1 | a)
|
|
48
|
-
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
49
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
describe('red-subtree reuse', () => {
|
|
54
|
-
it('adopts untouched subtrees as the same objects, with correct shifted ranges', () => {
|
|
55
|
-
const model = new BBCodeDocumentModel({ source: bigDoc() })
|
|
56
|
-
const root = model.redRoot!
|
|
57
|
-
const noticeIdx = root.children.findIndex(c => c.kind === 'notice')
|
|
58
|
-
expect(noticeIdx).toBeGreaterThan(0)
|
|
59
|
-
|
|
60
|
-
const beforeBlock = root.children[noticeIdx - 2] // a paragraph before the notice
|
|
61
|
-
const afterBlock = root.children[root.children.length - 2]
|
|
62
|
-
const afterStart = afterBlock.range.start
|
|
63
|
-
|
|
64
|
-
// Type one char inside the notice.
|
|
65
|
-
const src = model.source
|
|
66
|
-
const at = src.indexOf('texto largo') + 5
|
|
67
|
-
model.applyTextUpdate(src.slice(0, at) + 'X' + src.slice(at))
|
|
68
|
-
|
|
69
|
-
const newRoot = model.redRoot!
|
|
70
|
-
expect(newRoot).not.toBe(root)
|
|
71
|
-
|
|
72
|
-
// Prefix subtree: same object, same position.
|
|
73
|
-
expect(newRoot.children[noticeIdx - 2]).toBe(beforeBlock)
|
|
74
|
-
// Suffix subtree: same object, range shifted by the inserted char.
|
|
75
|
-
const afterNow = newRoot.children[newRoot.children.length - 2]
|
|
76
|
-
expect(afterNow).toBe(afterBlock)
|
|
77
|
-
expect(afterNow.range.start).toBe(afterStart + 1)
|
|
78
|
-
// Adopted nodes are correctly parented into the new tree.
|
|
79
|
-
expect(afterNow.parent).toBe(newRoot)
|
|
80
|
-
expect(afterNow.index).toBe(newRoot.children.length - 2)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('reuseRed: false keeps the old behavior (no shared objects)', () => {
|
|
84
|
-
const model = new BBCodeDocumentModel({ source: bigDoc(), reuseRed: false })
|
|
85
|
-
const root = model.redRoot!
|
|
86
|
-
const firstBlock = root.children[0]
|
|
87
|
-
|
|
88
|
-
const src = model.source
|
|
89
|
-
const at = src.indexOf('texto largo') + 5
|
|
90
|
-
model.applyTextUpdate(src.slice(0, at) + 'X' + src.slice(at))
|
|
91
|
-
|
|
92
|
-
expect(model.redRoot!.children[0]).not.toBe(firstBlock)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('differential: reuse tree is identical to a full rebuild, over seeded edits', () => {
|
|
96
|
-
const renderer = new HTMLRenderer()
|
|
97
|
-
const rand = mulberry32(777)
|
|
98
|
-
const insertables = ['a', 'X', ' ', '\n', '[', ']', '[b]', '[/b]', '[i]', '[*]', '[notice]', '[/notice]']
|
|
99
|
-
|
|
100
|
-
const reuse = new BBCodeDocumentModel({ source: bigDoc() })
|
|
101
|
-
const control = new BBCodeDocumentModel({ source: bigDoc(), incremental: false })
|
|
102
|
-
|
|
103
|
-
for (let edit = 0; edit < 120; edit++) {
|
|
104
|
-
const src = reuse.source
|
|
105
|
-
const pos = Math.floor(rand() * (src.length + 1))
|
|
106
|
-
const next = rand() < 0.7
|
|
107
|
-
? src.slice(0, pos) + insertables[Math.floor(rand() * insertables.length)] + src.slice(pos)
|
|
108
|
-
: src.slice(0, pos) + src.slice(Math.min(pos + 1 + Math.floor(rand() * 3), src.length))
|
|
109
|
-
|
|
110
|
-
reuse.applyTextUpdate(next)
|
|
111
|
-
control.applyTextUpdate(next)
|
|
112
|
-
|
|
113
|
-
expectSameTree(reuse.redRoot!, control.redRoot!)
|
|
114
|
-
expect(renderer.render(reuse.redRoot!).replace(/ data-node-id="[^"]*"/g, ''))
|
|
115
|
-
.toBe(renderer.render(control.redRoot!).replace(/ data-node-id="[^"]*"/g, ''))
|
|
116
|
-
expect(reuse.redRoot!.green).toBe(reuse.greenRoot)
|
|
117
|
-
}
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('ids stay unique after adoption plus fresh nodes', () => {
|
|
121
|
-
const model = new BBCodeDocumentModel({ source: bigDoc() })
|
|
122
|
-
const rand = mulberry32(4242)
|
|
123
|
-
|
|
124
|
-
for (let edit = 0; edit < 40; edit++) {
|
|
125
|
-
const src = model.source
|
|
126
|
-
const pos = Math.floor(rand() * (src.length + 1))
|
|
127
|
-
model.applyTextUpdate(src.slice(0, pos) + 'y' + src.slice(pos))
|
|
128
|
-
|
|
129
|
-
const ids: string[] = []
|
|
130
|
-
model.redRoot!.walk(n => { ids.push(String(n.id)) })
|
|
131
|
-
expect(new Set(ids).size).toBe(ids.length)
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
})
|