@aquera/nile-elements 1.9.7 → 1.9.9

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 (244) hide show
  1. package/README.md +6 -0
  2. package/dist/{index-249de90a.esm.js → index-289706c7.esm.js} +1 -1
  3. package/dist/{index-df196058.cjs.js → index-d7e6f1c6.cjs.js} +2 -2
  4. package/dist/index-d7e6f1c6.cjs.js.map +1 -0
  5. package/dist/index.cjs.js +1 -1
  6. package/dist/index.esm.js +1 -1
  7. package/dist/index.js +1587 -1276
  8. package/dist/katex-44476ab4.esm.js +1 -0
  9. package/dist/katex-f4186f87.cjs.js +2 -0
  10. package/dist/katex-f4186f87.cjs.js.map +1 -0
  11. package/dist/nile-color-picker/index.cjs.js +1 -1
  12. package/dist/nile-color-picker/index.esm.js +1 -1
  13. package/dist/nile-color-picker/nile-color-picker.cjs.js +1 -1
  14. package/dist/nile-color-picker/nile-color-picker.cjs.js.map +1 -1
  15. package/dist/nile-color-picker/nile-color-picker.esm.js +1 -1
  16. package/dist/nile-select/virtual-scroll-helper.cjs.js +1 -1
  17. package/dist/nile-select/virtual-scroll-helper.cjs.js.map +1 -1
  18. package/dist/nile-select/virtual-scroll-helper.esm.js +52 -44
  19. package/dist/nile-virtual-select/renderer.cjs.js +1 -1
  20. package/dist/nile-virtual-select/renderer.cjs.js.map +1 -1
  21. package/dist/nile-virtual-select/renderer.esm.js +23 -22
  22. package/dist/nile-wysiwyg-editor/engine/commands.cjs.js +1 -1
  23. package/dist/nile-wysiwyg-editor/engine/commands.cjs.js.map +1 -1
  24. package/dist/nile-wysiwyg-editor/engine/commands.esm.js +1 -1
  25. package/dist/nile-wysiwyg-editor/engine/create-state.cjs.js +1 -1
  26. package/dist/nile-wysiwyg-editor/engine/create-state.cjs.js.map +1 -1
  27. package/dist/nile-wysiwyg-editor/engine/create-state.esm.js +1 -1
  28. package/dist/nile-wysiwyg-editor/engine/index.cjs.js +1 -1
  29. package/dist/nile-wysiwyg-editor/engine/index.esm.js +1 -1
  30. package/dist/nile-wysiwyg-editor/engine/inputrules.cjs.js +1 -1
  31. package/dist/nile-wysiwyg-editor/engine/keymap.cjs.js +1 -1
  32. package/dist/nile-wysiwyg-editor/engine/keymap.esm.js +1 -1
  33. package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.cjs.js +1 -1
  34. package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.cjs.js.map +1 -1
  35. package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.esm.js +1 -1
  36. package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.cjs.js +1 -1
  37. package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.cjs.js.map +1 -1
  38. package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.esm.js +1 -1
  39. package/dist/nile-wysiwyg-editor/engine/plugins/embeds.cjs.js +1 -1
  40. package/dist/nile-wysiwyg-editor/engine/plugins/embeds.cjs.js.map +1 -1
  41. package/dist/nile-wysiwyg-editor/engine/plugins/embeds.esm.js +1 -1
  42. package/dist/nile-wysiwyg-editor/engine/plugins/math-view.cjs.js +1 -1
  43. package/dist/nile-wysiwyg-editor/engine/plugins/math-view.cjs.js.map +1 -1
  44. package/dist/nile-wysiwyg-editor/engine/plugins/math-view.esm.js +1 -1
  45. package/dist/nile-wysiwyg-editor/engine/plugins/style-block-view.cjs.js +2 -0
  46. package/dist/nile-wysiwyg-editor/engine/plugins/style-block-view.cjs.js.map +1 -0
  47. package/dist/nile-wysiwyg-editor/engine/plugins/style-block-view.esm.js +1 -0
  48. package/dist/nile-wysiwyg-editor/engine/plugins/tables.cjs.js +1 -1
  49. package/dist/nile-wysiwyg-editor/engine/plugins/tables.cjs.js.map +1 -1
  50. package/dist/nile-wysiwyg-editor/engine/plugins/tables.esm.js +1 -1
  51. package/dist/nile-wysiwyg-editor/engine/sanitize.cjs.js +1 -1
  52. package/dist/nile-wysiwyg-editor/engine/sanitize.cjs.js.map +1 -1
  53. package/dist/nile-wysiwyg-editor/engine/sanitize.esm.js +1 -1
  54. package/dist/nile-wysiwyg-editor/engine/schema.cjs.js +1 -1
  55. package/dist/nile-wysiwyg-editor/engine/schema.cjs.js.map +1 -1
  56. package/dist/nile-wysiwyg-editor/engine/schema.esm.js +1 -1
  57. package/dist/nile-wysiwyg-editor/engine/selection-state.cjs.js +1 -1
  58. package/dist/nile-wysiwyg-editor/engine/selection-state.cjs.js.map +1 -1
  59. package/dist/nile-wysiwyg-editor/engine/selection-state.esm.js +1 -1
  60. package/dist/nile-wysiwyg-editor/index.cjs.js +1 -1
  61. package/dist/nile-wysiwyg-editor/index.esm.js +1 -1
  62. package/dist/nile-wysiwyg-editor/math/katex-renderer.cjs.js +2 -0
  63. package/dist/nile-wysiwyg-editor/math/katex-renderer.cjs.js.map +1 -0
  64. package/dist/nile-wysiwyg-editor/math/katex-renderer.esm.js +1 -0
  65. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.cjs.js +1 -1
  66. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.cjs.js.map +1 -1
  67. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.cjs.js +1 -1
  68. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.cjs.js.map +1 -1
  69. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.esm.js +22 -3
  70. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.esm.js +7 -6
  71. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.cjs.js +1 -1
  72. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.esm.js +1 -1
  73. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.cjs.js +2 -0
  74. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.cjs.js.map +1 -0
  75. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.esm.js +1 -0
  76. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.cjs.js +1 -1
  77. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.cjs.js.map +1 -1
  78. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.cjs.js +1 -1
  79. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.cjs.js.map +1 -1
  80. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.esm.js +33 -54
  81. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.esm.js +83 -95
  82. package/dist/nile-wysiwyg-editor/types.cjs.js +1 -1
  83. package/dist/nile-wysiwyg-editor/types.cjs.js.map +1 -1
  84. package/dist/nile-wysiwyg-editor/types.esm.js +1 -1
  85. package/dist/nile-wysiwyg-editor/ui/bubble-item.cjs.js +2 -0
  86. package/dist/nile-wysiwyg-editor/ui/bubble-item.cjs.js.map +1 -0
  87. package/dist/nile-wysiwyg-editor/ui/bubble-item.esm.js +1 -0
  88. package/dist/nile-wysiwyg-editor/ui/bubble-menu.cjs.js +1 -1
  89. package/dist/nile-wysiwyg-editor/ui/bubble-menu.cjs.js.map +1 -1
  90. package/dist/nile-wysiwyg-editor/ui/bubble-menu.esm.js +23 -15
  91. package/dist/nile-wysiwyg-editor/ui/code-block-menu.cjs.js +1 -1
  92. package/dist/nile-wysiwyg-editor/ui/code-block-menu.cjs.js.map +1 -1
  93. package/dist/nile-wysiwyg-editor/ui/code-block-menu.esm.js +3 -3
  94. package/dist/nile-wysiwyg-editor/ui/embed-panel.cjs.js +1 -1
  95. package/dist/nile-wysiwyg-editor/ui/embed-panel.cjs.js.map +1 -1
  96. package/dist/nile-wysiwyg-editor/ui/embed-panel.esm.js +2 -2
  97. package/dist/nile-wysiwyg-editor/ui/math-panel.cjs.js +1 -1
  98. package/dist/nile-wysiwyg-editor/ui/math-panel.cjs.js.map +1 -1
  99. package/dist/nile-wysiwyg-editor/ui/math-panel.esm.js +0 -3
  100. package/dist/nile-wysiwyg-editor/ui/preview.cjs.js +2 -0
  101. package/dist/nile-wysiwyg-editor/ui/preview.cjs.js.map +1 -0
  102. package/dist/nile-wysiwyg-editor/ui/preview.esm.js +8 -0
  103. package/dist/nile-wysiwyg-editor/ui/source-view.cjs.js +1 -1
  104. package/dist/nile-wysiwyg-editor/ui/source-view.cjs.js.map +1 -1
  105. package/dist/nile-wysiwyg-editor/ui/source-view.esm.js +225 -24
  106. package/dist/nile-wysiwyg-editor/ui/suggest-list.cjs.js +1 -1
  107. package/dist/nile-wysiwyg-editor/ui/suggest-list.cjs.js.map +1 -1
  108. package/dist/nile-wysiwyg-editor/ui/suggest-list.esm.js +9 -9
  109. package/dist/nile-wysiwyg-editor/ui/table-grid-picker.cjs.js +1 -1
  110. package/dist/nile-wysiwyg-editor/ui/table-grid-picker.cjs.js.map +1 -1
  111. package/dist/nile-wysiwyg-editor/ui/table-grid-picker.esm.js +115 -13
  112. package/dist/nile-wysiwyg-editor/ui/table-menu.cjs.js +1 -1
  113. package/dist/nile-wysiwyg-editor/ui/table-menu.cjs.js.map +1 -1
  114. package/dist/nile-wysiwyg-editor/ui/table-menu.esm.js +13 -12
  115. package/dist/src/index.d.ts +5 -0
  116. package/dist/src/index.js +3 -0
  117. package/dist/src/index.js.map +1 -1
  118. package/dist/src/nile-color-picker/nile-color-picker.d.ts +1 -0
  119. package/dist/src/nile-color-picker/nile-color-picker.js +1 -0
  120. package/dist/src/nile-color-picker/nile-color-picker.js.map +1 -1
  121. package/dist/src/nile-select/virtual-scroll-helper.js +9 -1
  122. package/dist/src/nile-select/virtual-scroll-helper.js.map +1 -1
  123. package/dist/src/nile-virtual-select/renderer.js +1 -0
  124. package/dist/src/nile-virtual-select/renderer.js.map +1 -1
  125. package/dist/src/nile-wysiwyg-editor/engine/index.d.ts +1 -0
  126. package/dist/src/nile-wysiwyg-editor/engine/index.js +1 -0
  127. package/dist/src/nile-wysiwyg-editor/engine/index.js.map +1 -1
  128. package/dist/src/nile-wysiwyg-editor/engine/plugins/embed-view.js +3 -0
  129. package/dist/src/nile-wysiwyg-editor/engine/plugins/embed-view.js.map +1 -1
  130. package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.d.ts +19 -4
  131. package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.js +23 -18
  132. package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.js.map +1 -1
  133. package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.d.ts +8 -3
  134. package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.js +41 -4
  135. package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.js.map +1 -1
  136. package/dist/src/nile-wysiwyg-editor/engine/plugins/style-block-view.d.ts +17 -0
  137. package/dist/src/nile-wysiwyg-editor/engine/plugins/style-block-view.js +174 -0
  138. package/dist/src/nile-wysiwyg-editor/engine/plugins/style-block-view.js.map +1 -0
  139. package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.js +33 -2
  140. package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.js.map +1 -1
  141. package/dist/src/nile-wysiwyg-editor/engine/pm.test.js +49 -8
  142. package/dist/src/nile-wysiwyg-editor/engine/pm.test.js.map +1 -1
  143. package/dist/src/nile-wysiwyg-editor/engine/sanitize.d.ts +12 -0
  144. package/dist/src/nile-wysiwyg-editor/engine/sanitize.js +76 -8
  145. package/dist/src/nile-wysiwyg-editor/engine/sanitize.js.map +1 -1
  146. package/dist/src/nile-wysiwyg-editor/engine/schema.d.ts +2 -0
  147. package/dist/src/nile-wysiwyg-editor/engine/schema.js +154 -2
  148. package/dist/src/nile-wysiwyg-editor/engine/schema.js.map +1 -1
  149. package/dist/src/nile-wysiwyg-editor/index.d.ts +5 -0
  150. package/dist/src/nile-wysiwyg-editor/index.js +3 -0
  151. package/dist/src/nile-wysiwyg-editor/index.js.map +1 -1
  152. package/dist/src/nile-wysiwyg-editor/math/katex-renderer.d.ts +38 -0
  153. package/dist/src/nile-wysiwyg-editor/math/katex-renderer.js +40 -0
  154. package/dist/src/nile-wysiwyg-editor/math/katex-renderer.js.map +1 -0
  155. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.js +22 -3
  156. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.js.map +1 -1
  157. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.d.ts +36 -0
  158. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.js +125 -8
  159. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.js.map +1 -1
  160. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.js +5 -2
  161. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.js.map +1 -1
  162. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.d.ts +2 -0
  163. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.js +1 -0
  164. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.js.map +1 -1
  165. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.d.ts +55 -0
  166. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.js +79 -0
  167. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.js.map +1 -0
  168. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.js +33 -54
  169. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.js.map +1 -1
  170. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.d.ts +50 -2
  171. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.js +158 -102
  172. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.js.map +1 -1
  173. package/dist/src/nile-wysiwyg-editor/types.d.ts +4 -2
  174. package/dist/src/nile-wysiwyg-editor/types.js +2 -2
  175. package/dist/src/nile-wysiwyg-editor/types.js.map +1 -1
  176. package/dist/src/nile-wysiwyg-editor/ui/bubble-item.d.ts +48 -0
  177. package/dist/src/nile-wysiwyg-editor/ui/bubble-item.js +69 -0
  178. package/dist/src/nile-wysiwyg-editor/ui/bubble-item.js.map +1 -0
  179. package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.d.ts +17 -2
  180. package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.js +128 -35
  181. package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.js.map +1 -1
  182. package/dist/src/nile-wysiwyg-editor/ui/embed-panel.d.ts +2 -1
  183. package/dist/src/nile-wysiwyg-editor/ui/embed-panel.js +4 -3
  184. package/dist/src/nile-wysiwyg-editor/ui/embed-panel.js.map +1 -1
  185. package/dist/src/nile-wysiwyg-editor/ui/math-panel.js +0 -3
  186. package/dist/src/nile-wysiwyg-editor/ui/math-panel.js.map +1 -1
  187. package/dist/src/nile-wysiwyg-editor/ui/preview.d.ts +20 -0
  188. package/dist/src/nile-wysiwyg-editor/ui/preview.js +44 -0
  189. package/dist/src/nile-wysiwyg-editor/ui/preview.js.map +1 -0
  190. package/dist/src/nile-wysiwyg-editor/ui/source-view.d.ts +35 -3
  191. package/dist/src/nile-wysiwyg-editor/ui/source-view.js +337 -24
  192. package/dist/src/nile-wysiwyg-editor/ui/source-view.js.map +1 -1
  193. package/dist/src/nile-wysiwyg-editor/ui/suggest-list.d.ts +1 -0
  194. package/dist/src/nile-wysiwyg-editor/ui/suggest-list.js +9 -0
  195. package/dist/src/nile-wysiwyg-editor/ui/suggest-list.js.map +1 -1
  196. package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.d.ts +13 -2
  197. package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.js +166 -10
  198. package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.js.map +1 -1
  199. package/dist/src/nile-wysiwyg-editor/ui/table-menu.d.ts +1 -0
  200. package/dist/src/nile-wysiwyg-editor/ui/table-menu.js +10 -8
  201. package/dist/src/nile-wysiwyg-editor/ui/table-menu.js.map +1 -1
  202. package/dist/src/version.js +1 -1
  203. package/dist/src/version.js.map +1 -1
  204. package/dist/tsconfig.tsbuildinfo +1 -1
  205. package/dist/xml-25c4108a.esm.js +1 -0
  206. package/dist/xml-90fd974e.cjs.js +2 -0
  207. package/dist/xml-90fd974e.cjs.js.map +1 -0
  208. package/package.json +2 -1
  209. package/rollup.config.js +1 -0
  210. package/src/index.ts +8 -0
  211. package/src/nile-color-picker/nile-color-picker.ts +1 -0
  212. package/src/nile-select/virtual-scroll-helper.ts +9 -1
  213. package/src/nile-virtual-select/renderer.ts +1 -1
  214. package/src/nile-wysiwyg-editor/engine/index.ts +1 -0
  215. package/src/nile-wysiwyg-editor/engine/plugins/embed-view.ts +3 -0
  216. package/src/nile-wysiwyg-editor/engine/plugins/embeds.ts +26 -20
  217. package/src/nile-wysiwyg-editor/engine/plugins/math-view.ts +45 -4
  218. package/src/nile-wysiwyg-editor/engine/plugins/style-block-view.ts +178 -0
  219. package/src/nile-wysiwyg-editor/engine/plugins/tables.ts +34 -2
  220. package/src/nile-wysiwyg-editor/engine/pm.test.ts +58 -8
  221. package/src/nile-wysiwyg-editor/engine/sanitize.ts +71 -8
  222. package/src/nile-wysiwyg-editor/engine/schema.ts +158 -7
  223. package/src/nile-wysiwyg-editor/index.ts +5 -0
  224. package/src/nile-wysiwyg-editor/math/katex-renderer.ts +59 -0
  225. package/src/nile-wysiwyg-editor/math/katex.d.ts +17 -0
  226. package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.ts +22 -3
  227. package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.ts +5 -2
  228. package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.ts +142 -7
  229. package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.ts +2 -0
  230. package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.ts +68 -0
  231. package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.ts +33 -54
  232. package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.ts +202 -108
  233. package/src/nile-wysiwyg-editor/types.ts +6 -4
  234. package/src/nile-wysiwyg-editor/ui/bubble-item.ts +60 -0
  235. package/src/nile-wysiwyg-editor/ui/bubble-menu.ts +147 -40
  236. package/src/nile-wysiwyg-editor/ui/embed-panel.ts +4 -3
  237. package/src/nile-wysiwyg-editor/ui/math-panel.ts +0 -3
  238. package/src/nile-wysiwyg-editor/ui/preview.ts +42 -0
  239. package/src/nile-wysiwyg-editor/ui/source-view.ts +339 -31
  240. package/src/nile-wysiwyg-editor/ui/suggest-list.ts +10 -0
  241. package/src/nile-wysiwyg-editor/ui/table-grid-picker.ts +160 -10
  242. package/src/nile-wysiwyg-editor/ui/table-menu.ts +10 -8
  243. package/vscode-html-custom-data.json +75 -6
  244. package/dist/index-df196058.cjs.js.map +0 -1
@@ -5,15 +5,34 @@ import { NodeView } from 'prosemirror-view';
5
5
  export type MathRenderer = (latex: string, display: 'inline' | 'block') => string;
6
6
 
7
7
  /**
8
- * Inline atom rendering a math formula. When the host supplies a renderer the
9
- * formula is drawn with it; otherwise the raw LaTeX is shown in a math chip so
10
- * the content is still visible and editable without a math engine.
8
+ * Lazy-loaded KaTeX module (loaded once, only when the first formula renders,
9
+ * so editors without math pay nothing). Resolves to the katex default export.
10
+ */
11
+ let katexPromise: Promise<{ renderToString(tex: string, opts?: Record<string, unknown>): string }> | null =
12
+ null;
13
+ function loadKatex() {
14
+ if (!katexPromise) {
15
+ katexPromise = import('katex').then(m => m.default);
16
+ }
17
+ return katexPromise;
18
+ }
19
+
20
+ /**
21
+ * Inline atom rendering a math formula. Precedence:
22
+ * 1. A host-supplied `mathRenderer` (full control — e.g. KaTeX HTML, MathJax).
23
+ * 2. The built-in default: KaTeX compiled to **MathML**, which browsers render
24
+ * natively with no stylesheet or web fonts (works cleanly inside the
25
+ * editor's shadow root).
26
+ * 3. If KaTeX can't load / parse, the raw LaTeX stays visible.
11
27
  */
12
28
  export class MathView implements NodeView {
13
29
  dom: HTMLSpanElement;
14
30
 
15
31
  private renderer?: MathRenderer;
16
32
 
33
+ /** Bumps on every render so a slow async KaTeX result for a stale node is dropped. */
34
+ private token = 0;
35
+
17
36
  constructor(node: PMNode, renderer?: MathRenderer) {
18
37
  this.renderer = renderer;
19
38
  this.dom = document.createElement('span');
@@ -31,16 +50,38 @@ export class MathView implements NodeView {
31
50
  this.dom.setAttribute('data-math', latex);
32
51
  this.dom.setAttribute('data-math-display', display);
33
52
  this.dom.title = latex;
53
+
54
+ const token = ++this.token;
55
+
56
+ // 1. Host renderer wins.
34
57
  if (this.renderer) {
35
58
  try {
36
59
  // Host renderer output is trusted (same trust level as uploadHandler).
37
60
  this.dom.innerHTML = this.renderer(latex, display);
38
61
  return;
39
62
  } catch {
40
- // Fall back to raw LaTeX if the renderer throws.
63
+ // Fall through to the built-in renderer.
41
64
  }
42
65
  }
66
+
67
+ // 2. Built-in KaTeX → MathML. Show raw LaTeX until the (lazy) engine loads.
43
68
  this.dom.textContent = latex;
69
+ loadKatex()
70
+ .then(katex => {
71
+ if (token !== this.token) return; // node changed while loading
72
+ try {
73
+ this.dom.innerHTML = katex.renderToString(latex, {
74
+ output: 'mathml',
75
+ displayMode: display === 'block',
76
+ throwOnError: false,
77
+ });
78
+ } catch {
79
+ this.dom.textContent = latex;
80
+ }
81
+ })
82
+ .catch(() => {
83
+ /* KaTeX unavailable — keep the raw LaTeX already shown. */
84
+ });
44
85
  }
45
86
 
46
87
  update(node: PMNode): boolean {
@@ -0,0 +1,178 @@
1
+ import { Node as PMNode } from 'prosemirror-model';
2
+ import { NodeView } from 'prosemirror-view';
3
+
4
+ /** Content root the editor mounts the document under. Author CSS is scoped to
5
+ * it so a rule such as `div { background: green }` styles the content only and
6
+ * can never paint the editor chrome (toolbar, panels, surface) that shares the
7
+ * same shadow root. */
8
+ const CONTENT_SCOPE = '.ProseMirror';
9
+
10
+ /** Advances past a quoted string starting at `i` (the opening quote), honoring
11
+ * backslash escapes; returns the index just after the closing quote. */
12
+ function skipString(css: string, i: number): number {
13
+ const quote = css[i];
14
+ i += 1;
15
+ while (i < css.length) {
16
+ if (css[i] === '\\') {
17
+ i += 2;
18
+ continue;
19
+ }
20
+ if (css[i] === quote) return i + 1;
21
+ i += 1;
22
+ }
23
+ return i;
24
+ }
25
+
26
+ /** Splits on a top-level separator, ignoring ones inside (), [] or strings. */
27
+ function splitTopLevel(str: string, sep: string): string[] {
28
+ const parts: string[] = [];
29
+ let depth = 0;
30
+ let buf = '';
31
+ for (let i = 0; i < str.length; i += 1) {
32
+ const c = str[i];
33
+ if (c === '"' || c === "'") {
34
+ const end = skipString(str, i);
35
+ buf += str.slice(i, end);
36
+ i = end - 1;
37
+ continue;
38
+ }
39
+ if (c === '(' || c === '[') depth += 1;
40
+ else if (c === ')' || c === ']') depth -= 1;
41
+ if (c === sep && depth === 0) {
42
+ parts.push(buf);
43
+ buf = '';
44
+ continue;
45
+ }
46
+ buf += c;
47
+ }
48
+ parts.push(buf);
49
+ return parts;
50
+ }
51
+
52
+ /** Confines one complex selector to the content root. `html`/`body`/`:root`
53
+ * map onto the root itself. */
54
+ function scopeSelector(selector: string, scope: string): string {
55
+ const sel = selector.trim();
56
+ if (!sel) return sel;
57
+ if (/^(?:html|body|:root)$/i.test(sel)) return scope;
58
+ const root = sel.match(/^(?:html|body|:root)\b\s*/i);
59
+ return root ? `${scope} ${sel.slice(root[0].length)}`.trim() : `${scope} ${sel}`;
60
+ }
61
+
62
+ /** Confines a comma-separated selector list to the content root. */
63
+ function scopeSelectorList(list: string, scope: string): string {
64
+ return splitTopLevel(list, ',')
65
+ .map(part => scopeSelector(part, scope))
66
+ .filter(Boolean)
67
+ .join(', ');
68
+ }
69
+
70
+ const NESTED_AT_RULES = new Set(['media', 'supports', 'container', 'layer', 'scope']);
71
+
72
+ /**
73
+ * Scopes every rule in a CSS block to the content root by prefixing selectors,
74
+ * keeping each declaration block **verbatim**. Declarations are never re-parsed
75
+ * (a CSSOM round-trip drops `var()` inside shorthands, e.g. gradient text), and
76
+ * strings/nested braces are tracked so `content: "}"` can't derail it. Nested
77
+ * conditionals (`@media`, `@supports`, …) recurse; other at-rules
78
+ * (`@keyframes`, `@font-face`, …) pass through untouched.
79
+ */
80
+ function scopeBlock(css: string, scope: string): string {
81
+ let out = '';
82
+ let i = 0;
83
+ const n = css.length;
84
+ while (i < n) {
85
+ let j = i;
86
+ while (j < n) {
87
+ const c = css[j];
88
+ if (c === '"' || c === "'") {
89
+ j = skipString(css, j);
90
+ continue;
91
+ }
92
+ if (c === '{' || c === ';') break;
93
+ j += 1;
94
+ }
95
+ if (j >= n) {
96
+ out += css.slice(i);
97
+ break;
98
+ }
99
+ if (css[j] === ';') {
100
+ // A statement at-rule (e.g. a stray @charset) — keep verbatim.
101
+ out += css.slice(i, j + 1);
102
+ i = j + 1;
103
+ continue;
104
+ }
105
+ const prelude = css.slice(i, j).trim();
106
+ let depth = 1;
107
+ let k = j + 1;
108
+ while (k < n && depth > 0) {
109
+ const c = css[k];
110
+ if (c === '"' || c === "'") {
111
+ k = skipString(css, k);
112
+ continue;
113
+ }
114
+ if (c === '{') depth += 1;
115
+ else if (c === '}') depth -= 1;
116
+ k += 1;
117
+ }
118
+ const body = css.slice(j + 1, k - 1);
119
+ if (prelude.startsWith('@')) {
120
+ const name = (prelude.match(/^@([a-z-]+)/i)?.[1] ?? '').toLowerCase();
121
+ out += NESTED_AT_RULES.has(name)
122
+ ? `${prelude} {${scopeBlock(body, scope)}}`
123
+ : `${prelude} {${body}}`;
124
+ } else if (prelude) {
125
+ out += `${scopeSelectorList(prelude, scope)} {${body}}`;
126
+ }
127
+ i = k;
128
+ }
129
+ return out;
130
+ }
131
+
132
+ /** Returns author CSS confined to the content root. */
133
+ function scopeCss(css: string, scope: string): string {
134
+ if (!css.trim()) return '';
135
+ try {
136
+ return scopeBlock(css, scope);
137
+ } catch {
138
+ return css;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Node view for an author `<style>` block. It injects the CSS live — scoped to
144
+ * the content root — so the styles apply to the document as authored, with no
145
+ * visible chip. The block itself renders nothing (a hidden `<style>` host); it
146
+ * round-trips to `<style>` in the output via the schema's toDOM, and the same
147
+ * raw CSS is what the HTML-source preview renders.
148
+ */
149
+ export class StyleBlockView implements NodeView {
150
+ dom: HTMLElement;
151
+
152
+ private styleEl: HTMLStyleElement;
153
+
154
+ constructor(private node: PMNode) {
155
+ this.dom = document.createElement('div');
156
+ this.dom.className = 'nile-wysiwyg__style-block';
157
+ this.dom.contentEditable = 'false';
158
+
159
+ // A <style> applies regardless of its (display:none) host, so the block is
160
+ // invisible yet its rules take effect on the content.
161
+ this.styleEl = document.createElement('style');
162
+ this.styleEl.textContent = scopeCss(node.attrs.css, CONTENT_SCOPE);
163
+ this.dom.append(this.styleEl);
164
+ }
165
+
166
+ update(node: PMNode): boolean {
167
+ if (node.type !== this.node.type) return false;
168
+ if (node.attrs.css !== this.node.attrs.css) {
169
+ this.styleEl.textContent = scopeCss(node.attrs.css, CONTENT_SCOPE);
170
+ }
171
+ this.node = node;
172
+ return true;
173
+ }
174
+
175
+ ignoreMutation(): boolean {
176
+ return true;
177
+ }
178
+ }
@@ -1,5 +1,34 @@
1
1
  import { Plugin } from 'prosemirror-state';
2
- import { tableEditing, columnResizing } from 'prosemirror-tables';
2
+ import { Node as PMNode } from 'prosemirror-model';
3
+ import { tableEditing, columnResizing, TableView } from 'prosemirror-tables';
4
+
5
+ /**
6
+ * TableView that also reflects the author's `class`/`id` onto the inner
7
+ * `<table>` element (the base TableView leaves it bare and puts its own class on
8
+ * the wrapper div). The attributes round-trip via the schema's toDOM on export;
9
+ * this makes `table.grid …` style rules match in the live editor too. Passed to
10
+ * `columnResizing` as its `View`, so column resizing keeps working.
11
+ */
12
+ class HtmlAttrTableView extends TableView {
13
+ constructor(node: PMNode, defaultCellMinWidth: number) {
14
+ super(node, defaultCellMinWidth);
15
+ this.applyHtmlAttrs(node);
16
+ }
17
+
18
+ private applyHtmlAttrs(node: PMNode): void {
19
+ const htmlClass = node.attrs.htmlClass as string | null;
20
+ const htmlId = node.attrs.htmlId as string | null;
21
+ this.table.className = htmlClass ?? '';
22
+ if (htmlId) this.table.id = htmlId;
23
+ else this.table.removeAttribute('id');
24
+ }
25
+
26
+ update(node: PMNode): boolean {
27
+ const updated = super.update(node);
28
+ if (updated) this.applyHtmlAttrs(node);
29
+ return updated;
30
+ }
31
+ }
3
32
 
4
33
  /**
5
34
  * prosemirror-tables wiring: cell selections, fix-tables normalization, and
@@ -8,5 +37,8 @@ import { tableEditing, columnResizing } from 'prosemirror-tables';
8
37
  * is inlined in nile-wysiwyg-editor.css.ts.
9
38
  */
10
39
  export function tablePlugins(): Plugin[] {
11
- return [columnResizing(), tableEditing({ allowTableNodeSelection: true })];
40
+ return [
41
+ columnResizing({ View: HtmlAttrTableView }),
42
+ tableEditing({ allowTableNodeSelection: true }),
43
+ ];
12
44
  }
@@ -68,12 +68,34 @@ describe('sanitizeHtml', () => {
68
68
  expect(sanitizeHtml('<article><p>kept</p></article>')).to.equal('<p>kept</p>');
69
69
  });
70
70
 
71
- it('drops iframes, styles and forms with content', () => {
71
+ it('drops non-https iframes and forms with content', () => {
72
72
  const out = sanitizeHtml(
73
- '<iframe src="x"></iframe><style>p{}</style><form><input></form><p>ok</p>'
73
+ '<iframe src="x"></iframe><form><input></form><p>ok</p>'
74
74
  );
75
75
  expect(out).to.equal('<p>ok</p>');
76
76
  });
77
+
78
+ it('keeps <style> blocks and sanitizes their CSS', () => {
79
+ const out = sanitizeHtml(
80
+ '<style>@import url("http://evil/x.css"); p{color:red} a{behavior:url(x.htc)} ' +
81
+ 'b{width:expression(alert(1))} c{background:url(http://evil/x.png)}</style><p>ok</p>'
82
+ );
83
+ expect(out).to.include('<style>');
84
+ expect(out).to.include('color:red');
85
+ expect(out).to.include('<p>ok</p>');
86
+ expect(out).to.not.match(/@import/i);
87
+ expect(out).to.not.match(/expression\s*\(/i);
88
+ expect(out).to.not.match(/behavior\s*:/i);
89
+ expect(out).to.not.match(/url\(\s*['"]?http/i);
90
+ });
91
+
92
+ it('keeps safe data-image and #fragment url() in <style>', () => {
93
+ const out = sanitizeHtml(
94
+ '<style>a{background:url(data:image/png;base64,AAAA)} b{clip-path:url(#f)}</style>'
95
+ );
96
+ expect(out).to.include('url(data:image/png');
97
+ expect(out).to.include('url(#f)');
98
+ });
77
99
  });
78
100
 
79
101
  describe('serializer round-trip', () => {
@@ -105,6 +127,21 @@ describe('serializer round-trip', () => {
105
127
  expect(docToHtml(doc)).to.include('<strong>');
106
128
  });
107
129
 
130
+ it('round-trips an author <style> block through the schema', () => {
131
+ const doc = htmlToDoc(defaultSchema, '<style>p{color:red}</style><p>hi</p>');
132
+ expect(doc.firstChild!.type.name).to.equal('styleBlock');
133
+ const out = docToHtml(doc);
134
+ expect(out).to.include('<style>');
135
+ expect(out).to.match(/color:\s*red/i);
136
+ expect(out).to.include('<p>hi</p>');
137
+ // Malicious CSS is neutralized on the round-trip, not just at the surface.
138
+ const evil = docToHtml(
139
+ htmlToDoc(defaultSchema, '<style>@import url(http://evil);x{color:blue}</style><p>ok</p>')
140
+ );
141
+ expect(evil).to.include('<style>');
142
+ expect(evil).to.not.match(/@import/i);
143
+ });
144
+
108
145
  it('detects empty documents', () => {
109
146
  expect(isDocEmpty(htmlToDoc(defaultSchema, ''))).to.equal(true);
110
147
  expect(isDocEmpty(htmlToDoc(defaultSchema, '<p></p>'))).to.equal(true);
@@ -482,20 +519,33 @@ describe('embeds', () => {
482
519
  ).to.equal('loom');
483
520
  });
484
521
 
485
- it('rejects non-provider and non-https URLs', () => {
486
- expect(toEmbedUrl('https://evil.example.com/embed/x')).to.equal(null);
522
+ it('accepts any https URL as a generic embed, rejects non-https', () => {
523
+ expect(toEmbedUrl('https://example.com/player/x')).to.deep.equal({
524
+ src: 'https://example.com/player/x',
525
+ provider: 'generic',
526
+ });
527
+ // Lookalike hosts are no longer canonicalized as a provider, just generic.
528
+ expect(toEmbedUrl('https://youtube.com.evil.com/watch?v=x')!.provider).to.equal('generic');
529
+ // Non-https is still refused.
487
530
  expect(toEmbedUrl('http://www.youtube.com/watch?v=dQw4w9WgXcQ')).to.equal(null);
488
531
  expect(toEmbedUrl('javascript:alert(1)')).to.equal(null);
489
- expect(toEmbedUrl('https://youtube.com.evil.com/watch?v=x')).to.equal(null);
490
- expect(isAllowedEmbedSrc('https://evil.com/embed/x')).to.equal(false);
532
+ expect(isAllowedEmbedSrc('https://example.com/player/x')).to.equal(true);
533
+ expect(isAllowedEmbedSrc('http://example.com/x')).to.equal(false);
491
534
  });
492
535
 
493
- it('keeps allowlisted iframes through sanitization, drops the rest', () => {
536
+ it('keeps any https iframe through sanitization but force-sandboxes it', () => {
494
537
  const good = sanitizeHtml(
495
538
  '<div data-embed-provider="youtube"><iframe src="https://www.youtube-nocookie.com/embed/abc123xyz"></iframe></div>'
496
539
  );
497
540
  expect(good).to.include('youtube-nocookie.com/embed');
498
- const evil = sanitizeHtml('<iframe src="https://evil.example.com/steal"></iframe><p>ok</p>');
541
+ expect(good).to.include('sandbox=');
542
+ // A non-provider https iframe is kept, but sandboxed and cannot navigate the top.
543
+ const generic = sanitizeHtml('<iframe src="https://example.com/player"></iframe><p>ok</p>');
544
+ expect(generic).to.include('example.com/player');
545
+ expect(generic).to.include('sandbox=');
546
+ expect(generic).to.not.include('allow-top-navigation');
547
+ // Non-https iframes are still dropped entirely.
548
+ const evil = sanitizeHtml('<iframe src="http://evil.example.com/steal"></iframe><p>ok</p>');
499
549
  expect(evil).to.equal('<p>ok</p>');
500
550
  });
501
551
 
@@ -6,11 +6,11 @@
6
6
  * set the schema understands.
7
7
  */
8
8
 
9
- import { isAllowedEmbedSrc } from './plugins/embeds';
9
+ import { isAllowedEmbedSrc, EMBED_SANDBOX, EMBED_REFERRER_POLICY } from './plugins/embeds';
10
10
 
11
11
  /** Elements removed together with their content. */
12
12
  const DROP_WITH_CONTENT = new Set([
13
- 'script', 'style', 'frame', 'frameset', 'object', 'embed',
13
+ 'script', 'frame', 'frameset', 'object', 'embed',
14
14
  'applet', 'form', 'input', 'textarea', 'select', 'button', 'meta', 'link',
15
15
  'base', 'title', 'noscript', 'template', 'svg', 'math', 'canvas', 'audio',
16
16
  'video', 'dialog', 'slot',
@@ -43,10 +43,24 @@ const TAG_ATTRS: Record<string, Set<string>> = {
43
43
  th: new Set(['colspan', 'rowspan', 'data-colwidth']),
44
44
  col: new Set(['span', 'width']),
45
45
  div: new Set(['data-embed-provider', 'data-width', 'data-align']),
46
- iframe: new Set(['src', 'title', 'frameborder', 'allowfullscreen', 'allow']),
46
+ iframe: new Set([
47
+ 'src', 'title', 'frameborder', 'allowfullscreen', 'allow', 'sandbox', 'referrerpolicy',
48
+ ]),
47
49
  };
48
50
 
49
- const GLOBAL_ATTRS = new Set(['style', 'align']);
51
+ const GLOBAL_ATTRS = new Set(['style', 'align', 'class', 'id']);
52
+
53
+ /** Author class/id names starting with these are the editor's own; drop them so
54
+ * content can't impersonate or restyle the chrome. */
55
+ const RESERVED_NAME = /^(?:nile-wysiwyg|ProseMirror)/;
56
+
57
+ /** Filtered `class`: reserved-prefixed tokens removed. Empty → drop attribute. */
58
+ function cleanClassAttr(value: string): string {
59
+ return value
60
+ .split(/\s+/)
61
+ .filter(token => token && !RESERVED_NAME.test(token))
62
+ .join(' ');
63
+ }
50
64
 
51
65
  /** Style declarations the schema can represent. Everything else is noise. */
52
66
  const ALLOWED_STYLES = new Set([
@@ -95,6 +109,33 @@ function filterStyle(value: string, tag: string): string {
95
109
  .join('; ');
96
110
  }
97
111
 
112
+ /**
113
+ * Sanitizes the CSS inside a <style> block. Neutralizes the vectors that make
114
+ * a raw stylesheet dangerous while leaving ordinary rules intact:
115
+ * - `@import` (external fetch / data exfiltration) — removed,
116
+ * - `expression()`, `behavior:`, `-moz-binding:` (script execution) — broken,
117
+ * - `url(...)` pointing anywhere other than a safe data: image or a local
118
+ * `#fragment` — replaced with `url(#)`,
119
+ * - any `</style` sequence — stripped so the text can't break out of the
120
+ * element when the document is re-serialized.
121
+ * CSS comments are dropped first so none of the above can hide inside one.
122
+ */
123
+ export function sanitizeCss(css: string): string {
124
+ return css
125
+ .replace(/<\/style/gi, '')
126
+ .replace(/\/\*[\s\S]*?\*\//g, '')
127
+ .replace(/@import[^;]*;?/gi, '')
128
+ .replace(/expression\s*\(/gi, 'blocked(')
129
+ .replace(/(behavior|-moz-binding)(\s*):/gi, 'blocked-prop$2:')
130
+ .replace(/url\(\s*(['"]?)([^)]*?)\1\s*\)/gi, (full, _quote, raw) => {
131
+ const url = String(raw).trim();
132
+ if (/^#/.test(url)) return full;
133
+ if (/^data:image\/(png|gif|jpe?g|webp|avif|bmp);/i.test(url)) return full;
134
+ return 'url(#)';
135
+ })
136
+ .trim();
137
+ }
138
+
98
139
  function sanitizeElement(el: Element): void {
99
140
  const tag = el.tagName.toLowerCase();
100
141
  const allowed = TAG_ATTRS[tag];
@@ -111,6 +152,12 @@ function sanitizeElement(el: Element): void {
111
152
  const filtered = filterStyle(attr.value, tag);
112
153
  if (filtered) el.setAttribute('style', filtered);
113
154
  else el.removeAttribute('style');
155
+ } else if (name === 'class') {
156
+ const cleaned = cleanClassAttr(attr.value);
157
+ if (cleaned) el.setAttribute('class', cleaned);
158
+ else el.removeAttribute('class');
159
+ } else if (name === 'id') {
160
+ if (RESERVED_NAME.test(attr.value)) el.removeAttribute('id');
114
161
  } else if (name === 'href' || name === 'src') {
115
162
  // Raster data: URLs are allowed on images only.
116
163
  if (!isSafeUrl(attr.value, tag === 'img')) el.removeAttribute(attr.name);
@@ -128,9 +175,22 @@ function walk(node: Element): void {
128
175
  child.remove();
129
176
  continue;
130
177
  }
131
- // iframes are allowed ONLY for allowlisted embed providers.
132
- if (tag === 'iframe' && !isAllowedEmbedSrc(child.getAttribute('src') ?? '')) {
133
- child.remove();
178
+ // iframes are allowed for any https src, but always force-sandboxed so an
179
+ // untrusted embed can never navigate the top window or run unrestricted.
180
+ if (tag === 'iframe') {
181
+ if (!isAllowedEmbedSrc(child.getAttribute('src') ?? '')) {
182
+ child.remove();
183
+ continue;
184
+ }
185
+ child.setAttribute('sandbox', EMBED_SANDBOX);
186
+ child.setAttribute('referrerpolicy', EMBED_REFERRER_POLICY);
187
+ }
188
+ // <style> blocks are kept, but their CSS is sanitized and any attributes
189
+ // (media, nonce, …) dropped. Handled before the unwrap branch below so the
190
+ // raw CSS is never spilled into the document as visible text.
191
+ if (tag === 'style') {
192
+ child.textContent = sanitizeCss(child.textContent ?? '');
193
+ for (const attr of Array.from(child.attributes)) child.removeAttribute(attr.name);
134
194
  continue;
135
195
  }
136
196
  if (!ALLOWED_TAGS.has(tag)) {
@@ -150,7 +210,10 @@ function walk(node: Element): void {
150
210
  * DOMParser, so embedded scripts never execute and images never load.
151
211
  */
152
212
  export function sanitizeHtml(html: string): string {
153
- const doc = new DOMParser().parseFromString(html, 'text/html');
213
+ // Wrap in an explicit <body> so metadata-like tags (notably <style>) stay in
214
+ // the body instead of being hoisted into <head> by the HTML parser — which
215
+ // would drop them before walk() ever sees them. Mirrors htmlToDoc's parse.
216
+ const doc = new DOMParser().parseFromString(`<body>${html}</body>`, 'text/html');
154
217
  walk(doc.body);
155
218
  return doc.body.innerHTML;
156
219
  }