@miliastry/quasar 1.0.6 → 1.2.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 (278) hide show
  1. package/README.md +1 -1
  2. package/dist/Visuals/lyne.css +374 -9
  3. package/dist/Visuals/osu.css +393 -11
  4. package/dist/index.js +10470 -3336
  5. package/dist/index.mjs +10336 -3337
  6. package/dist/types/Analysis/Contracts/AnalysisReport.d.ts +22 -0
  7. package/dist/types/Analysis/Contracts/Contribution.d.ts +68 -0
  8. package/dist/types/Analysis/Contracts/Pass.d.ts +59 -0
  9. package/dist/types/Analysis/Contracts/PipelineContext.d.ts +34 -0
  10. package/dist/types/Analysis/Passes/Analysis/ColorUsageAnalyzer.d.ts +56 -0
  11. package/dist/types/Analysis/Passes/Analysis/GradientAnalyzer.d.ts +111 -0
  12. package/dist/types/Analysis/Passes/Analysis/MergeableColorAnalyzer.d.ts +26 -0
  13. package/dist/types/Analysis/Passes/Analysis/RainbowAnalyzer.d.ts +47 -0
  14. package/dist/types/Analysis/Passes/Analysis/SymbolAnalyzer.d.ts +89 -0
  15. package/dist/types/Analysis/Passes/Analysis/WaveAnalyzer.d.ts +44 -0
  16. package/dist/types/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.d.ts +4 -0
  17. package/dist/types/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.d.ts +4 -0
  18. package/dist/types/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.d.ts +4 -0
  19. package/dist/types/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.d.ts +4 -0
  20. package/dist/types/Analysis/Passes/Decision/DefaultDecision.d.ts +30 -0
  21. package/dist/types/Analysis/Passes/Decision/PaletteRemapDecision.d.ts +169 -0
  22. package/dist/types/Analysis/Passes/Decision/__tests__/DefaultDecision.test.d.ts +4 -0
  23. package/dist/types/Analysis/Passes/Transform/CollapseGradientTransform.d.ts +37 -0
  24. package/dist/types/Analysis/Passes/Transform/MergeColorsTransform.d.ts +28 -0
  25. package/dist/types/Analysis/Passes/Transform/RainbowCollapseTransform.d.ts +19 -0
  26. package/dist/types/Analysis/Passes/Transform/WaveCollapseTransform.d.ts +20 -0
  27. package/dist/types/Analysis/Passes/Utility/CharacterCountAnalyzer.d.ts +19 -0
  28. package/dist/types/Analysis/Pipeline/Pipeline.d.ts +69 -0
  29. package/dist/types/Analysis/Pipeline/PipelineBuilder.d.ts +31 -0
  30. package/dist/types/Analysis/Pipeline/PipelineStage.d.ts +17 -0
  31. package/dist/types/Analysis/Utils/color-utils.d.ts +60 -0
  32. package/dist/types/Analysis/__tests__/Integration.test.d.ts +8 -0
  33. package/dist/types/Analysis/__tests__/PaletteRemap.test.d.ts +14 -0
  34. package/dist/types/Analysis/__tests__/Pipeline.test.d.ts +10 -0
  35. package/dist/types/Analysis/__tests__/RoundTrip.test.d.ts +30 -0
  36. package/dist/types/Analysis/__tests__/SymbolAnalyzer.test.d.ts +11 -0
  37. package/dist/types/Analysis/index.d.ts +40 -0
  38. package/dist/types/BBCode/BBCodeDocumentModel.d.ts +102 -0
  39. package/dist/types/BBCode/BBCodeToGreenNode.d.ts +111 -0
  40. package/dist/types/BBCode/Parser.d.ts +78 -0
  41. package/dist/types/BBCode/index.d.ts +4 -0
  42. package/dist/types/Collab/positions.d.ts +55 -0
  43. package/dist/types/Commands/Command.d.ts +37 -0
  44. package/dist/types/Commands/CommandRegistry.d.ts +48 -0
  45. package/dist/types/Commands/DeleteNode.d.ts +7 -0
  46. package/dist/types/Commands/InsertText.d.ts +8 -0
  47. package/dist/types/Commands/SplitMerge.d.ts +9 -0
  48. package/dist/types/Commands/WrapInTag.d.ts +8 -0
  49. package/dist/types/Commands/index.d.ts +6 -0
  50. package/dist/types/Diff/TreeDiffer.d.ts +52 -0
  51. package/dist/types/Diff/__tests__/TreeDiffer.test.d.ts +1 -0
  52. package/dist/types/Diff/index.d.ts +2 -0
  53. package/dist/types/Edits/EditPlan.d.ts +144 -0
  54. package/dist/types/Edits/Optimizer.d.ts +78 -0
  55. package/dist/types/Edits/Rules/Rule.d.ts +91 -0
  56. package/dist/types/Edits/Rules/dropEmptyTags.d.ts +40 -0
  57. package/dist/types/Edits/Rules/dropRedundantNesting.d.ts +59 -0
  58. package/dist/types/Edits/Rules/index.d.ts +9 -0
  59. package/dist/types/Edits/Rules/mergeAdjacent.d.ts +93 -0
  60. package/dist/types/Edits/Rules/reorderWrappers.d.ts +42 -0
  61. package/dist/types/Edits/Rules/shortenHex.d.ts +30 -0
  62. package/dist/types/Edits/Rules/tagValue.d.ts +56 -0
  63. package/dist/types/Edits/Rules/unwrapInvisibleColor.d.ts +47 -0
  64. package/dist/types/Edits/applyEdits.d.ts +36 -0
  65. package/dist/types/Edits/index.d.ts +6 -0
  66. package/dist/types/Events/EventBus.d.ts +103 -0
  67. package/dist/types/Events/index.d.ts +2 -0
  68. package/dist/types/Formatter/Formatter.d.ts +32 -0
  69. package/dist/types/Formatter/index.d.ts +2 -0
  70. package/dist/types/HTML/HTMLDocumentModel.d.ts +13 -0
  71. package/dist/types/HTML/HTMLToGreenNode.d.ts +4 -0
  72. package/dist/types/Incremental/BracketIndex.d.ts +101 -0
  73. package/dist/types/Incremental/ChangeTracker.d.ts +69 -0
  74. package/dist/types/Incremental/IncrementalParser.d.ts +246 -0
  75. package/dist/types/Incremental/__tests__/BracketIndex.test.d.ts +1 -0
  76. package/dist/types/Incremental/__tests__/IncrementalParser.test.d.ts +1 -0
  77. package/dist/types/Incremental/index.d.ts +4 -0
  78. package/dist/types/Lexer/BBCodeLexer.d.ts +74 -0
  79. package/dist/types/Lexer/Lexer.d.ts +54 -0
  80. package/dist/types/Lexer/index.d.ts +4 -0
  81. package/dist/types/Linter/Linter.d.ts +58 -0
  82. package/dist/types/Linter/index.d.ts +2 -0
  83. package/dist/types/Markdown/MarkdownAST.d.ts +122 -0
  84. package/dist/types/Markdown/MarkdownDocumentModel.d.ts +15 -0
  85. package/dist/types/Markdown/MarkdownLexer.d.ts +155 -0
  86. package/dist/types/Markdown/MarkdownParser.d.ts +42 -0
  87. package/dist/types/Markdown/MarkdownToGreenNode.d.ts +6 -0
  88. package/dist/types/MilHibri/MilHibriDocumentModel.d.ts +21 -0
  89. package/dist/types/Model/DocumentModel.d.ts +318 -0
  90. package/dist/types/Model/NodeFactory.d.ts +29 -0
  91. package/dist/types/Model/TagRegistry.d.ts +165 -0
  92. package/dist/types/Model/index.d.ts +5 -0
  93. package/dist/types/Plugins/PluginAPI.d.ts +44 -0
  94. package/dist/types/Plugins/PluginRegistry.d.ts +81 -0
  95. package/dist/types/Plugins/index.d.ts +3 -0
  96. package/dist/types/Queries/QueryEngine.d.ts +32 -0
  97. package/dist/types/Queries/index.d.ts +1 -0
  98. package/dist/types/Reconciler/SurgicalReconciler.d.ts +23 -0
  99. package/dist/types/RenderPipeline/RenderPipeline.d.ts +46 -0
  100. package/dist/types/RenderPipeline/RenderTree.d.ts +55 -0
  101. package/dist/types/RenderPipeline/index.d.ts +4 -0
  102. package/dist/types/Repair/NestingRepair.d.ts +48 -0
  103. package/dist/types/Repair/index.d.ts +1 -0
  104. package/dist/types/Semantic/SemanticAnalyzer.d.ts +321 -0
  105. package/dist/types/Semantic/index.d.ts +2 -0
  106. package/dist/types/Symbols/SymbolTable.d.ts +47 -0
  107. package/dist/types/Symbols/index.d.ts +1 -0
  108. package/dist/types/Syntax/GreenNode.d.ts +154 -0
  109. package/dist/types/Syntax/GreenNodePool.d.ts +134 -0
  110. package/dist/types/Syntax/NodeMatcher.d.ts +75 -0
  111. package/dist/types/Syntax/RedNode.d.ts +280 -0
  112. package/dist/types/Syntax/RedNodeStore.d.ts +116 -0
  113. package/dist/types/Syntax/TreeBuilder.d.ts +54 -0
  114. package/dist/types/Syntax/__tests__/GreenNode.test.d.ts +1 -0
  115. package/dist/types/Syntax/__tests__/RedNode.test.d.ts +1 -0
  116. package/dist/types/Syntax/__tests__/RedNodeStore.test.d.ts +1 -0
  117. package/dist/types/Syntax/greenEdit.d.ts +64 -0
  118. package/dist/types/Syntax/hash.d.ts +17 -0
  119. package/dist/types/Syntax/index.d.ts +8 -0
  120. package/dist/types/Syntax/nodeAttr.d.ts +63 -0
  121. package/dist/types/Syntax/partition.d.ts +67 -0
  122. package/dist/types/Syntax/preserveNodeIds.d.ts +47 -0
  123. package/dist/types/Tokens/TokenResolver.d.ts +25 -0
  124. package/dist/types/Tokens/index.d.ts +1 -0
  125. package/dist/types/Transactions/Transaction.d.ts +36 -0
  126. package/dist/types/Transactions/UndoManager.d.ts +63 -0
  127. package/dist/types/Transactions/index.d.ts +3 -0
  128. package/dist/types/Transformers/ASTOptimizer.d.ts +34 -0
  129. package/dist/types/Transformers/GradientTransformer.d.ts +18 -0
  130. package/dist/types/Transformers/GrowTransformer.d.ts +16 -0
  131. package/dist/types/Transformers/RainbowTransformer.d.ts +17 -0
  132. package/dist/types/Transformers/SineWaveTransformer.d.ts +17 -0
  133. package/dist/types/Transformers/Transformer.d.ts +17 -0
  134. package/dist/types/Types/core.d.ts +104 -0
  135. package/dist/types/Types/diagnostics.d.ts +85 -0
  136. package/dist/types/Types/index.d.ts +11 -0
  137. package/dist/types/Types/operations.d.ts +109 -0
  138. package/dist/types/Types/queries.d.ts +67 -0
  139. package/dist/types/Types/symbols.d.ts +42 -0
  140. package/dist/types/Types/tokens.d.ts +75 -0
  141. package/dist/types/Utils/BBCodeGenerator.d.ts +21 -0
  142. package/dist/types/Utils/ColorMath.d.ts +92 -0
  143. package/dist/types/Utils/EffectMath.d.ts +643 -0
  144. package/dist/types/Utils/TagVocabulary.d.ts +104 -0
  145. package/dist/types/Utils/TagVocabulary.test.d.ts +1 -0
  146. package/dist/types/Utils/color.d.ts +29 -0
  147. package/dist/types/Utils/dom-to-svg.d.ts +28 -0
  148. package/dist/types/Utils/dom-to-svg.test.d.ts +1 -0
  149. package/dist/types/Utils/treeTransformers.d.ts +160 -0
  150. package/dist/types/Visitors/BBBlocksExporter.d.ts +27 -0
  151. package/dist/types/Visitors/BBCodeExporter.d.ts +90 -0
  152. package/dist/types/Visitors/BlockPatcher.d.ts +91 -0
  153. package/dist/types/Visitors/DOMMorpher.d.ts +19 -0
  154. package/dist/types/Visitors/HTMLRenderer.d.ts +492 -0
  155. package/dist/types/Visitors/JSONExporter.d.ts +42 -0
  156. package/dist/types/Visitors/MarkdownExporter.d.ts +28 -0
  157. package/dist/types/Visitors/SVGRenderer.d.ts +16 -0
  158. package/dist/types/Visitors/TiptapExporter.d.ts +40 -0
  159. package/dist/types/Visitors/Visitor.d.ts +35 -0
  160. package/dist/types/Visitors/index.d.ts +9 -0
  161. package/dist/types/Visuals/BoxDrawer.d.ts +54 -0
  162. package/dist/types/Visuals/LyneAudio.d.ts +8 -0
  163. package/dist/types/Visuals/index.d.ts +39 -0
  164. package/dist/types/index.d.ts +155 -0
  165. package/package.json +14 -10
  166. package/src/Analysis/Passes/Analysis/ColorUsageAnalyzer.ts +103 -0
  167. package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +294 -20
  168. package/src/Analysis/Passes/Analysis/SymbolAnalyzer.ts +257 -0
  169. package/src/Analysis/Passes/Decision/PaletteRemapDecision.ts +425 -0
  170. package/src/Analysis/Pipeline/Pipeline.ts +15 -2
  171. package/src/Analysis/index.ts +8 -2
  172. package/src/BBCode/BBCodeDocumentModel.ts +39 -1
  173. package/src/BBCode/BBCodeToGreenNode.ts +18 -12
  174. package/src/BBCode/Parser.ts +38 -1
  175. package/src/Edits/EditPlan.ts +290 -0
  176. package/src/Edits/Optimizer.ts +185 -0
  177. package/src/Edits/Rules/Rule.ts +145 -0
  178. package/src/Edits/Rules/dropEmptyTags.ts +110 -0
  179. package/src/Edits/Rules/dropRedundantNesting.ts +120 -0
  180. package/src/Edits/Rules/index.ts +28 -0
  181. package/src/Edits/Rules/mergeAdjacent.ts +224 -0
  182. package/src/Edits/Rules/reorderWrappers.ts +154 -0
  183. package/src/Edits/Rules/shortenHex.ts +79 -0
  184. package/src/Edits/Rules/tagValue.ts +105 -0
  185. package/src/Edits/Rules/unwrapInvisibleColor.ts +93 -0
  186. package/src/Edits/applyEdits.ts +54 -0
  187. package/src/Edits/index.ts +22 -0
  188. package/src/Events/EventBus.ts +18 -0
  189. package/src/HTML/HTMLToGreenNode.ts +193 -37
  190. package/src/Incremental/BracketIndex.ts +249 -0
  191. package/src/Incremental/IncrementalParser.ts +464 -41
  192. package/src/Lexer/BBCodeLexer.ts +25 -3
  193. package/src/Lexer/index.ts +1 -1
  194. package/src/Markdown/MarkdownAST.ts +65 -2
  195. package/src/Markdown/MarkdownLexer.ts +69 -14
  196. package/src/Markdown/MarkdownParser.ts +749 -62
  197. package/src/Markdown/MarkdownToGreenNode.ts +148 -27
  198. package/src/MilHibri/MilHibriDocumentModel.ts +56 -0
  199. package/src/Model/DocumentModel.ts +138 -5
  200. package/src/Model/TagRegistry.ts +129 -134
  201. package/src/Reconciler/SurgicalReconciler.ts +460 -0
  202. package/src/Repair/NestingRepair.ts +116 -0
  203. package/src/Repair/index.ts +1 -0
  204. package/src/Semantic/SemanticAnalyzer.ts +1845 -65
  205. package/src/Syntax/GreenNodePool.ts +12 -12
  206. package/src/Syntax/RedNode.ts +17 -3
  207. package/src/Syntax/RedNodeStore.ts +9 -1
  208. package/src/Syntax/nodeAttr.ts +172 -0
  209. package/src/Tokens/TokenResolver.ts +55 -0
  210. package/src/Tokens/index.ts +1 -0
  211. package/src/Types/core.ts +15 -0
  212. package/src/Utils/ColorMath.ts +276 -55
  213. package/src/Utils/EffectMath.ts +2462 -0
  214. package/src/Utils/TagVocabulary.ts +141 -0
  215. package/src/Utils/dom-to-svg.ts +28 -12
  216. package/src/Visitors/BBBlocksExporter.ts +15 -1
  217. package/src/Visitors/BBCodeExporter.ts +365 -32
  218. package/src/Visitors/BlockPatcher.ts +123 -22
  219. package/src/Visitors/DOMMorpher.ts +44 -28
  220. package/src/Visitors/HTMLRenderer.ts +771 -205
  221. package/src/Visitors/MarkdownExporter.ts +98 -41
  222. package/src/Visitors/TiptapExporter.ts +1 -0
  223. package/src/Visitors/index.ts +1 -1
  224. package/src/Visuals/BoxDrawer.ts +56 -10
  225. package/src/Visuals/LyneAudio.ts +346 -0
  226. package/src/Visuals/index.ts +8 -4
  227. package/src/Visuals/lyne.css +374 -9
  228. package/src/Visuals/osu.css +393 -11
  229. package/src/index.ts +178 -8
  230. package/dist/index.d.mts +0 -4612
  231. package/dist/index.d.ts +0 -4612
  232. package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +0 -135
  233. package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +0 -84
  234. package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +0 -99
  235. package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +0 -119
  236. package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +0 -179
  237. package/src/Analysis/__tests__/Integration.test.ts +0 -162
  238. package/src/Analysis/__tests__/Pipeline.test.ts +0 -133
  239. package/src/Diff/__tests__/TreeDiffer.test.ts +0 -65
  240. package/src/Incremental/__tests__/IncrementalParser.test.ts +0 -164
  241. package/src/Syntax/__tests__/GreenNode.test.ts +0 -33
  242. package/src/Syntax/__tests__/RedNode.test.ts +0 -81
  243. package/src/Syntax/__tests__/RedNodeStore.test.ts +0 -104
  244. package/src/Tests/ASTOptimizerIdempotence.test.ts +0 -77
  245. package/src/Tests/BlockPatcher.test.ts +0 -437
  246. package/src/Tests/BlockPatcherWindowed.test.ts +0 -364
  247. package/src/Tests/BoxDrawer.test.ts +0 -217
  248. package/src/Tests/BoxRichTitle.test.ts +0 -105
  249. package/src/Tests/Chars500kBenchmark.test.ts +0 -151
  250. package/src/Tests/Chars500kEdits.test.ts +0 -321
  251. package/src/Tests/CollabPositions.test.ts +0 -146
  252. package/src/Tests/CompilerPathProfiling.test.ts +0 -186
  253. package/src/Tests/DOMMorpher.test.ts +0 -142
  254. package/src/Tests/DomPatchPerf.test.ts +0 -60
  255. package/src/Tests/EffectSegments.snapshot.json +0 -616
  256. package/src/Tests/EffectSegments.test.ts +0 -68
  257. package/src/Tests/FindNodeAtOffset.test.ts +0 -65
  258. package/src/Tests/Fuzzer.test.ts +0 -166
  259. package/src/Tests/GreenNodePool.test.ts +0 -153
  260. package/src/Tests/Lexer.test.ts +0 -238
  261. package/src/Tests/LyneMode.test.ts +0 -291
  262. package/src/Tests/ModelCoherence.test.ts +0 -180
  263. package/src/Tests/Partition.test.ts +0 -238
  264. package/src/Tests/PluginTags.test.ts +0 -150
  265. package/src/Tests/ProblematicSection.test.ts +0 -46
  266. package/src/Tests/ProblematicSectionHTML.test.ts +0 -58
  267. package/src/Tests/RedReuse.test.ts +0 -134
  268. package/src/Tests/ReproDelete20k.test.ts +0 -62
  269. package/src/Tests/SemanticValidators.test.ts +0 -136
  270. package/src/Tests/StableNodeIds.test.ts +0 -210
  271. package/src/Tests/StudioColorBloat.test.ts +0 -25
  272. package/src/Tests/StudioDebugText.test.ts +0 -27
  273. package/src/Tests/StudioTrailingChar.test.ts +0 -25
  274. package/src/Tests/StudioValidText.test.ts +0 -25
  275. package/src/Tests/UrlImgBug.test.ts +0 -23
  276. package/src/Tests/VisualBuilderFidelity.test.ts +0 -105
  277. package/src/Tests/referenceDocument.ts +0 -119
  278. package/src/Utils/dom-to-svg.test.ts +0 -86
@@ -41,7 +41,7 @@
41
41
 
42
42
  import { RedNode } from '../Syntax/RedNode'
43
43
  import { HTMLRenderer } from './HTMLRenderer'
44
- import { morphHTML } from './DOMMorpher'
44
+ import { morphHTML, morphElement } from './DOMMorpher'
45
45
  import type { TextChangeRange } from '../Incremental/ChangeTracker'
46
46
 
47
47
  export interface PatchBlocksOptions {
@@ -67,6 +67,8 @@ export interface PatchBlocksOptions {
67
67
  * path on small documents.
68
68
  */
69
69
  minWindowedBlocks?: number
70
+ /** If true, forces a full rebuild of the container instead of incremental patching. */
71
+ forceRebuild?: boolean
70
72
  }
71
73
 
72
74
  export interface PatchBlocksStats {
@@ -143,6 +145,53 @@ interface PatchCache {
143
145
 
144
146
  const caches = new WeakMap<HTMLElement, PatchCache>()
145
147
 
148
+ /**
149
+ * ¿Está encendido el volcado verboso? (`globalThis.__BP_DEBUG__ = true`)
150
+ *
151
+ * Imprime por consola el detalle de la reconciliación por ventana. Es caro:
152
+ * sirve para leer un caso a mano, NO para medir con él encendido.
153
+ */
154
+ function BP_DEBUG(): boolean {
155
+ return (globalThis as { __BP_DEBUG__?: boolean }).__BP_DEBUG__ === true
156
+ }
157
+
158
+ /**
159
+ * Deja constancia de POR QUÉ `patchBlocksInto` tomó el camino que tomó, en
160
+ * `globalThis.__BP_LAST__` (`globalThis.__BP_TRACE__ = true` para encenderlo).
161
+ *
162
+ * Va aparte del volcado verboso a propósito: esta traza es UNA asignación por
163
+ * parcheo, así que se puede dejar encendida MIENTRAS SE MIDE sin falsear la
164
+ * medida, que es justo cuando hace falta.
165
+ *
166
+ * Existe porque "¿por qué esto no usó el camino incremental?" es cara de
167
+ * contestar desde fuera: el bundle está minificado, `patchBlocksInto` devuelve
168
+ * `mode` pero no el motivo, y quien lo llama (el preview) tira las
169
+ * estadísticas. Diagnosticar la caída a `fullRebuild` por tecla en el
170
+ * documento de 547 KB costó desminificar a mano una función dentro de una
171
+ * línea de 75.000 caracteres; con esto se lee `__BP_LAST__` y ya está.
172
+ *
173
+ * Coste apagada: una comparación de identidad por parcheo.
174
+ */
175
+ function bpTrace(reason: Record<string, unknown>): void {
176
+ if ((globalThis as { __BP_TRACE__?: boolean }).__BP_TRACE__ === true) {
177
+ ;(globalThis as { __BP_LAST__?: unknown }).__BP_LAST__ = reason
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Evict `container` from the patch cache so the next patch rebuilds afresh.
183
+ */
184
+ export function clearPatchCache(container: HTMLElement): void {
185
+ // Contador de invalidaciones bajo la misma traza barata: "cuántas veces se
186
+ // tira la caché por pulsación" es la pregunta que destapó el fullRebuild por
187
+ // tecla, y no se puede contestar desde fuera del módulo.
188
+ if ((globalThis as { __BP_TRACE__?: boolean }).__BP_TRACE__ === true) {
189
+ const g = globalThis as { __BP_CLEARS__?: number }
190
+ g.__BP_CLEARS__ = (g.__BP_CLEARS__ ?? 0) + 1
191
+ }
192
+ caches.delete(container)
193
+ }
194
+
146
195
  const defaultRenderer = new HTMLRenderer()
147
196
 
148
197
  function getCache(container: HTMLElement): PatchCache {
@@ -166,6 +215,21 @@ function blockKey(node: RedNode, index: number): string {
166
215
  return node.id ?? `__block_${index}`
167
216
  }
168
217
 
218
+ /**
219
+ * Blocks whose HTML is NOT a function of the node alone.
220
+ *
221
+ * A newline leaf renders `<br>` or nothing depending on which tags surround
222
+ * it — `[/box]\n` loses the newline, `text\n` keeps it (see
223
+ * `HTMLRenderer.isNewlineSwallowed`). The block cache is keyed by node
224
+ * identity, so reordering blocks without touching them keeps every id and
225
+ * every cached fragment while changing what the newlines between them mean.
226
+ * Re-rendering them unconditionally costs a couple of sibling hops and keeps
227
+ * the patched DOM equal to a full render, which is the whole contract.
228
+ */
229
+ function isContextSensitive(node: RedNode): boolean {
230
+ return node.kind === 'spacing' || node.kind === 'empty_line'
231
+ }
232
+
169
233
  /** Build a DOM node from an HTML fragment (first child), decoding entities. */
170
234
  function nodeFromHtml(html: string): Node {
171
235
  const t = document.createElement('template')
@@ -230,7 +294,6 @@ function shouldMorphInPlace(
230
294
  return (
231
295
  run.kind === 'element' &&
232
296
  !!run.node.id &&
233
- run.node.children.length > 0 &&
234
297
  canMorphInPlace(run.node) &&
235
298
  renderedTag(run.html) === element.tagName
236
299
  )
@@ -352,7 +415,7 @@ function reconcileKeyed(
352
415
  let patched = 0
353
416
  try {
354
417
  runs = buildRuns(blocks, keys, (node, key) => {
355
- if (cache.lastNode.get(key) === node) {
418
+ if (cache.lastNode.get(key) === node && !isContextSensitive(node)) {
356
419
  return { html: cache.lastHtml.get(key) ?? '', kind: cache.lastClass.get(key) ?? 'none' }
357
420
  }
358
421
  const html = renderer.render(node)
@@ -420,13 +483,19 @@ function reconcileKeyed(
420
483
  }
421
484
 
422
485
  if (element.nodeType === 1 && shouldMorphInPlace(element as Element, run)) {
423
- // Element with children and a matching tag: morph its inner content in
424
- // place, preserving the element (and runtime state like an open
425
- // `<details>`). Tag mismatch (kind change at a stable slot) or wrapper
426
- // kinds (code/svg/imagemap) go the replace path below.
427
- morphHTML(element as HTMLElement, renderer.renderChildren(run.node))
486
+ // Element with a matching tag: morph its attributes and children in
487
+ // place via morphElement, preserving the element identity and runtime state
488
+ // (such as an open <details>, its <summary> and title, or media playback).
489
+ // Tag mismatch (kind change at a stable slot) or wrapper kinds
490
+ // (code/svg/imagemap) go the replace path below.
491
+ const newEl = nodeFromHtml(run.html)
492
+ if (newEl && newEl.nodeType === 1 && (newEl as Element).tagName === (element as Element).tagName) {
493
+ morphElement(element as Element, newEl as Element)
494
+ } else {
495
+ container.replaceChild(newEl, element)
496
+ }
428
497
  } else {
429
- // Text run, an element leaf (img/video/audio/`<br>`), a kind whose tag
498
+ // Text run, an element leaf (img/video/audio/<br>), a kind whose tag
430
499
  // changed, or a wrapper renderer kind: the node's own structure must
431
500
  // change — replace outright.
432
501
  container.replaceChild(nodeFromHtml(run.html), element)
@@ -590,7 +659,7 @@ function reconcileWindowed(
590
659
  let runs: PatchRun[]
591
660
  try {
592
661
  runs = buildRuns(blocks, keys, (node, key) => {
593
- if (cache.lastNode.get(key) === node) {
662
+ if (cache.lastNode.get(key) === node && !isContextSensitive(node)) {
594
663
  return { html: cache.lastHtml.get(key) ?? '', kind: cache.lastClass.get(key) ?? 'none' }
595
664
  }
596
665
  const html = renderer.render(node)
@@ -607,10 +676,15 @@ function reconcileWindowed(
607
676
 
608
677
  const oldRuns = cache.lastRuns
609
678
 
610
- const DBG = (globalThis as { __BP_DEBUG__?: boolean }).__BP_DEBUG__ === true
679
+ const DBG = BP_DEBUG()
611
680
  const dbg = (...a: unknown[]): void => {
612
681
  if (DBG) console.log('[BP]', ...a)
613
682
  }
683
+ // OJO: `dbg(...)` evalúa sus argumentos ANTES de entrar, así que las
684
+ // llamadas que construyen algo —`.map()`, plantillas, `slice`— van dentro
685
+ // de un `if (DBG)`. Este es el camino de cada pulsación: los cuatro
686
+ // volcados de abajo recorrían la ventana entera y construían una cadena por
687
+ // ejecución, con la depuración apagada, para tirarlo todo.
614
688
 
615
689
 
616
690
  // ── Old window (OLD coordinates). ──────────────────────────────────────
@@ -749,14 +823,22 @@ function reconcileWindowed(
749
823
  const oldWinRunsFinal = oldRuns.slice(oldWinFrom2, oldWinTo)
750
824
  const newWinRunsFinal = newWinRuns
751
825
 
752
- dbg('change', { start: change.start, end: change.end, endOld: change.endOld })
753
- dbg('oldWin', oldWinFrom2, oldWinTo, 'keys', oldWinRunsFinal.map((r) => `${r.key}:${r.node.kind}`).slice(0, 40))
754
- dbg('newWin', fromNew, toNew, 'keys', newWinRunsFinal.map((r) => `${r.key}:${r.node.kind}`).slice(0, 40))
755
- dbg('newWinKeySet', [...newWinRunsFinal.map((r) => r.key).slice(0, 40)])
826
+ if (DBG) {
827
+ dbg('change', { start: change.start, end: change.end, endOld: change.endOld })
828
+ dbg('oldWin', oldWinFrom2, oldWinTo, 'keys', oldWinRunsFinal.map((r) => `${r.key}:${r.node.kind}`).slice(0, 40))
829
+ dbg('newWin', fromNew, toNew, 'keys', newWinRunsFinal.map((r) => `${r.key}:${r.node.kind}`).slice(0, 40))
830
+ dbg('newWinKeySet', [...newWinRunsFinal.map((r) => r.key).slice(0, 40)])
831
+ }
756
832
 
757
833
  // DOM anchor at the window start, and the window's old nodes.
758
- let anchorNode: Node | null = container.firstChild
759
- for (let k = 0; k < oldWinFrom2; k++) anchorNode = anchorNode?.nextSibling ?? null
834
+ // Indexed, not walked. `childNodes` is a live NodeList with indexed access,
835
+ // so asking for the window's first node is one lookup; stepping to it one
836
+ // `nextSibling` at a time was O(blocks before the edit) and, on a document
837
+ // of 30.000 top-level blocks, the single largest term left in this
838
+ // function's bookkeeping — 11 ms per keystroke, more than the reconcile it
839
+ // was preparing for. The two are exactly equivalent: the k-th child is the
840
+ // k-th child.
841
+ const anchorNode: Node | null = container.childNodes[oldWinFrom2] ?? null
760
842
  const oldWinNodes: Node[] = []
761
843
  let node: Node | null = anchorNode
762
844
  for (let i = oldWinFrom2; i < oldWinTo; i++) {
@@ -800,7 +882,7 @@ function reconcileWindowed(
800
882
  const k = oldWinRunsFinal[i].key
801
883
  if (newKeySet.has(k) || newByNode.has(oldWinRunsFinal[i].node)) continue
802
884
  const el = oldWinNodes[i]
803
- dbg('orphan?', k, oldWinRunsFinal[i].node.kind, 'el=', el ? (el.nodeType === 1 ? (el as Element).tagName : `text:'${String((el as Text).data ?? '').slice(0, 20)}'`) : 'null')
885
+ if (DBG) dbg('orphan?', k, oldWinRunsFinal[i].node.kind, 'el=', el ? (el.nodeType === 1 ? (el as Element).tagName : `text:'${String((el as Text).data ?? '').slice(0, 20)}'`) : 'null')
804
886
  if (el && !removed.has(el)) {
805
887
  removed.add(el)
806
888
  container.removeChild(el)
@@ -828,7 +910,7 @@ function reconcileWindowed(
828
910
  const prev = oldRunByKey.get(run.key) ?? oldRunByNode.get(run.node)
829
911
  const element = oldByKey.get(run.key) ?? oldByNode.get(run.node)
830
912
 
831
- dbg('WALK', run.key, run.node.kind, 'element=', element ? (element.nodeType === 1 ? (element as Element).tagName : 'text') : 'NEW', 'cursor=', cursor ? (cursor.nodeType === 1 ? (cursor as Element).tagName : 'text') : 'null')
913
+ if (DBG) dbg('WALK', run.key, run.node.kind, 'element=', element ? (element.nodeType === 1 ? (element as Element).tagName : 'text') : 'NEW', 'cursor=', cursor ? (cursor.nodeType === 1 ? (cursor as Element).tagName : 'text') : 'null')
832
914
 
833
915
  if (!element) {
834
916
  container.insertBefore(nodeFromHtml(run.html), cursor)
@@ -847,7 +929,12 @@ function reconcileWindowed(
847
929
  }
848
930
 
849
931
  if (element.nodeType === 1 && shouldMorphInPlace(element as Element, run)) {
850
- morphHTML(element as HTMLElement, renderer.renderChildren(run.node))
932
+ const newEl = nodeFromHtml(run.html)
933
+ if (newEl && newEl.nodeType === 1 && (newEl as Element).tagName === (element as Element).tagName) {
934
+ morphElement(element as Element, newEl as Element)
935
+ } else {
936
+ container.replaceChild(newEl, element)
937
+ }
851
938
  } else {
852
939
  container.replaceChild(nodeFromHtml(run.html), element)
853
940
  }
@@ -858,7 +945,7 @@ function reconcileWindowed(
858
945
  // the first suffix node. Leftovers of runs that rendered to a different
859
946
  // node count sit between the walk's end and the suffix — drop them.
860
947
  let tail = cursor
861
- dbg('tail-safety: cursor=', cursor ? (cursor.nodeType === 1 ? (cursor as Element).tagName : `text:'${String((cursor as Text).data ?? '').slice(0, 15)}'`) : 'null', 'afterWindow=', afterWindow ? (afterWindow.nodeType === 1 ? (afterWindow as Element).tagName : `text:'${String((afterWindow as Text).data ?? '').slice(0, 15)}'`) : 'null')
948
+ if (DBG) dbg('tail-safety: cursor=', cursor ? (cursor.nodeType === 1 ? (cursor as Element).tagName : `text:'${String((cursor as Text).data ?? '').slice(0, 15)}'`) : 'null', 'afterWindow=', afterWindow ? (afterWindow.nodeType === 1 ? (afterWindow as Element).tagName : `text:'${String((afterWindow as Text).data ?? '').slice(0, 15)}'`) : 'null')
862
949
  let tailCount = 0
863
950
  while (tail !== afterWindow) {
864
951
  if (!tail) break
@@ -923,9 +1010,15 @@ export function patchBlocksInto(
923
1010
  return { mode: 'full', total: 0, patched: 0 }
924
1011
  }
925
1012
 
1013
+ if (options.forceRebuild) {
1014
+ bpTrace({ why: 'forceRebuild' })
1015
+ return fullRebuild(container, rootNode, renderer, cache)
1016
+ }
1017
+
926
1018
  // DOM out of sync with the cache (a host replaced the innerHTML behind our
927
1019
  // back, or a run rendered to a different node count than last time).
928
1020
  if (container.childNodes.length !== cache.lastRuns.length) {
1021
+ bpTrace({ why: 'desync', kids: container.childNodes.length, runs: cache.lastRuns.length })
929
1022
  return fullRebuild(container, rootNode, renderer, cache)
930
1023
  }
931
1024
 
@@ -938,7 +1031,13 @@ export function patchBlocksInto(
938
1031
  undefined
939
1032
  if (change) {
940
1033
  const windowed = reconcileWindowed(container, rootNode, change, renderer, cache, options)
941
- if (windowed) return windowed
1034
+ if (windowed) {
1035
+ bpTrace({ why: 'windowed' })
1036
+ return windowed
1037
+ }
1038
+ bpTrace({ why: 'windowed-declinado' })
1039
+ } else {
1040
+ bpTrace({ why: 'sin-changeRange' })
942
1041
  }
943
1042
 
944
1043
  const blocks = rootNode.children
@@ -956,8 +1055,10 @@ export function patchBlocksInto(
956
1055
  oldSet.add(k)
957
1056
  }
958
1057
  if (added >= cache.lastKeys.length) {
1058
+ bpTrace({ why: 'added>=lastKeys', added, lastKeys: cache.lastKeys.length })
959
1059
  return fullRebuild(container, rootNode, renderer, cache)
960
1060
  }
961
1061
 
1062
+ bpTrace({ why: 'keyed' })
962
1063
  return reconcileKeyed(container, rootNode, keys, renderer, cache, options)
963
1064
  }
@@ -97,34 +97,7 @@ function morphNodes(parent: Node, newParent: Node): void {
97
97
  newNode.nodeType === 1 &&
98
98
  (oldNode as Element).tagName === (newNode as Element).tagName
99
99
  ) {
100
- const oldEl = oldNode as Element
101
- const newEl = newNode as Element
102
-
103
- // Preserve interactive runtime states (such as <details open> for spoilerboxes and boxes)
104
- if (oldEl.tagName === 'DETAILS' && oldEl.hasAttribute('open')) {
105
- newEl.setAttribute('open', '')
106
- }
107
- if (oldEl.classList.contains('open')) {
108
- newEl.classList.add('open')
109
- }
110
- if (oldEl.classList.contains('is-open')) {
111
- newEl.classList.add('is-open')
112
- }
113
-
114
- // Sync attributes in-place
115
- for (const attr of Array.from(newEl.attributes)) {
116
- if (oldEl.getAttribute(attr.name) !== attr.value) {
117
- oldEl.setAttribute(attr.name, attr.value)
118
- }
119
- }
120
- for (const attr of Array.from(oldEl.attributes)) {
121
- if (!newEl.hasAttribute(attr.name)) {
122
- oldEl.removeAttribute(attr.name)
123
- }
124
- }
125
-
126
- // Morph child nodes recursively
127
- morphNodes(oldEl, newEl)
100
+ morphElement(oldNode as Element, newNode as Element)
128
101
  } else {
129
102
  // Different tag or nodeType: replace node
130
103
  oldNode.parentNode!.replaceChild(newNode.cloneNode(true), oldNode)
@@ -132,3 +105,46 @@ function morphNodes(parent: Node, newParent: Node): void {
132
105
  }
133
106
  }
134
107
  }
108
+
109
+ /**
110
+ * Reconcile two HTML elements of the same tag in-place.
111
+ * Syncs attributes, preserves interactive states (<details open>, .open classes),
112
+ * and recursively morphs child nodes.
113
+ */
114
+ export function morphElement(oldEl: Element, newEl: Element): void {
115
+ // Preserve interactive runtime states (such as <details open> for spoilerboxes and boxes)
116
+ if (oldEl.tagName === 'DETAILS' && oldEl.hasAttribute('open')) {
117
+ newEl.setAttribute('open', '')
118
+ }
119
+ if (oldEl.classList.contains('open')) {
120
+ newEl.classList.add('open')
121
+ }
122
+ if (oldEl.classList.contains('is-open')) {
123
+ newEl.classList.add('is-open')
124
+ }
125
+
126
+ // Sync attributes in-place.
127
+ //
128
+ // `attributes` se recorre por índice: `Array.from` asignaba un array
129
+ // por elemento y por sentido, dos por pareja morfada, y esto corre
130
+ // sobre la ventana cambiada en cada pulsación.
131
+ const newAttrs = newEl.attributes
132
+ for (let i = 0; i < newAttrs.length; i++) {
133
+ const attr = newAttrs[i]
134
+ if (oldEl.getAttribute(attr.name) !== attr.value) {
135
+ oldEl.setAttribute(attr.name, attr.value)
136
+ }
137
+ }
138
+ // Hacia atrás: quitar un atributo compacta la colección viva, y
139
+ // recorrerla hacia delante mientras se borra se salta el siguiente.
140
+ const oldAttrs = oldEl.attributes
141
+ for (let i = oldAttrs.length - 1; i >= 0; i--) {
142
+ const name = oldAttrs[i].name
143
+ if (!newEl.hasAttribute(name)) {
144
+ oldEl.removeAttribute(name)
145
+ }
146
+ }
147
+
148
+ // Morph child nodes recursively
149
+ morphNodes(oldEl, newEl)
150
+ }