nexus-shared 1.1.20 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +903 -0
  3. package/dist/Client.Index.d.ts +44 -0
  4. package/dist/Client.Index.js +49 -0
  5. package/dist/Components/Documents/Button.d.ts +29 -0
  6. package/dist/Components/Documents/Button.js +11 -0
  7. package/dist/Components/Documents/Menu.d.ts +102 -0
  8. package/dist/Components/Documents/Menu.js +238 -0
  9. package/dist/Components/Documents/SplitButton.d.ts +64 -0
  10. package/dist/Components/Documents/SplitButton.js +127 -0
  11. package/dist/Components/Documents/TabButtons.d.ts +62 -0
  12. package/dist/Components/Documents/TabButtons.js +19 -0
  13. package/dist/Components/Forms/ApiForm.d.ts +21 -0
  14. package/dist/Components/Forms/ApiForm.js +99 -0
  15. package/dist/Components/Forms/Crud.d.ts +17 -0
  16. package/dist/Components/Forms/Crud.js +556 -0
  17. package/dist/Components/Forms/CrudParts.d.ts +15 -0
  18. package/dist/Components/Forms/CrudParts.js +36 -0
  19. package/dist/Components/Forms/Form.d.ts +52 -0
  20. package/dist/Components/Forms/Form.js +319 -0
  21. package/dist/Components/Forms/SubmitForm.d.ts +65 -0
  22. package/dist/Components/Forms/SubmitForm.js +198 -0
  23. package/dist/Components/Inputs/Calendar.d.ts +69 -0
  24. package/dist/Components/Inputs/Calendar.js +258 -0
  25. package/dist/Components/Inputs/CheckBox.d.ts +53 -0
  26. package/dist/Components/Inputs/CheckBox.js +87 -0
  27. package/dist/Components/Inputs/CheckBoxGroup.d.ts +20 -0
  28. package/dist/Components/Inputs/CheckBoxGroup.js +80 -0
  29. package/dist/Components/Inputs/ChoiceField.d.ts +122 -0
  30. package/dist/Components/Inputs/ChoiceField.js +169 -0
  31. package/dist/Components/Inputs/DatePicker.d.ts +54 -0
  32. package/dist/Components/Inputs/DatePicker.js +246 -0
  33. package/dist/Components/Inputs/DateRangePicker.d.ts +62 -0
  34. package/dist/Components/Inputs/DateRangePicker.js +283 -0
  35. package/dist/Components/Inputs/DateTimePicker.d.ts +75 -0
  36. package/dist/Components/Inputs/DateTimePicker.js +399 -0
  37. package/dist/Components/Inputs/Dropdown.d.ts +117 -0
  38. package/dist/Components/Inputs/Dropdown.js +839 -0
  39. package/dist/Components/Inputs/DropdownParts.d.ts +84 -0
  40. package/dist/Components/Inputs/DropdownParts.js +102 -0
  41. package/dist/Components/Inputs/EditorShell.d.ts +45 -0
  42. package/dist/Components/Inputs/EditorShell.js +214 -0
  43. package/dist/Components/Inputs/Field.d.ts +53 -0
  44. package/dist/Components/Inputs/Field.js +16 -0
  45. package/dist/Components/Inputs/GroupForm.d.ts +20 -0
  46. package/dist/Components/Inputs/GroupForm.js +170 -0
  47. package/dist/Components/Inputs/InputField.d.ts +59 -0
  48. package/dist/Components/Inputs/InputField.js +169 -0
  49. package/dist/Components/Inputs/InputRenderer.d.ts +35 -0
  50. package/dist/Components/Inputs/InputRenderer.js +99 -0
  51. package/dist/Components/Inputs/MarkdownEditor.d.ts +67 -0
  52. package/dist/Components/Inputs/MarkdownEditor.js +289 -0
  53. package/dist/Components/Inputs/NumberBox.d.ts +53 -0
  54. package/dist/Components/Inputs/NumberBox.js +228 -0
  55. package/dist/Components/Inputs/RadioGroup.d.ts +20 -0
  56. package/dist/Components/Inputs/RadioGroup.js +43 -0
  57. package/dist/Components/Inputs/ReadOnlyNotice.d.ts +36 -0
  58. package/dist/Components/Inputs/ReadOnlyNotice.js +116 -0
  59. package/dist/Components/Inputs/RichTextEditor.d.ts +103 -0
  60. package/dist/Components/Inputs/RichTextEditor.js +1170 -0
  61. package/dist/Components/Inputs/RichTextParts.d.ts +63 -0
  62. package/dist/Components/Inputs/RichTextParts.js +157 -0
  63. package/dist/Components/Inputs/RowsInput.d.ts +104 -0
  64. package/dist/Components/Inputs/RowsInput.js +259 -0
  65. package/dist/Components/Inputs/Slider.d.ts +87 -0
  66. package/dist/Components/Inputs/Slider.js +216 -0
  67. package/dist/Components/Inputs/TabularForm.d.ts +23 -0
  68. package/dist/Components/Inputs/TabularForm.js +283 -0
  69. package/dist/Components/Inputs/TextArea.d.ts +43 -0
  70. package/dist/Components/Inputs/TextArea.js +31 -0
  71. package/dist/Components/Inputs/TextBox.d.ts +52 -0
  72. package/dist/Components/Inputs/TextBox.js +58 -0
  73. package/dist/Components/Inputs/TextField.d.ts +31 -0
  74. package/dist/Components/Inputs/TextField.js +35 -0
  75. package/dist/Components/Inputs/TimePanel.d.ts +34 -0
  76. package/dist/Components/Inputs/TimePanel.js +288 -0
  77. package/dist/Components/Inputs/TimePicker.d.ts +54 -0
  78. package/dist/Components/Inputs/TimePicker.js +239 -0
  79. package/dist/Components/Inputs/TreeView.d.ts +160 -0
  80. package/dist/Components/Inputs/TreeView.js +947 -0
  81. package/dist/Components/Layouts/MessageBox.d.ts +71 -0
  82. package/dist/Components/Layouts/MessageBox.js +104 -0
  83. package/dist/Components/Layouts/Popup.d.ts +92 -0
  84. package/dist/Components/Layouts/Popup.js +376 -0
  85. package/dist/Components/Layouts/PopupContext.d.ts +2 -0
  86. package/dist/Components/Layouts/PopupContext.js +4 -0
  87. package/dist/Components/Layouts/PopupDock.d.ts +22 -0
  88. package/dist/Components/Layouts/PopupDock.js +121 -0
  89. package/dist/Components/Layouts/PopupHost.d.ts +56 -0
  90. package/dist/Components/Layouts/PopupHost.js +163 -0
  91. package/dist/Components/Layouts/PopupRules.d.ts +30 -0
  92. package/dist/Components/Layouts/PopupRules.js +63 -0
  93. package/dist/Components/Layouts/ThemePicker.d.ts +13 -0
  94. package/dist/Components/Layouts/ThemePicker.js +22 -0
  95. package/dist/Components/Layouts/ThemeScript.d.ts +14 -0
  96. package/dist/Components/Layouts/ThemeScript.js +13 -0
  97. package/dist/Components/Layouts/ThemeSwitcher.d.ts +13 -0
  98. package/dist/Components/Layouts/ThemeSwitcher.js +29 -0
  99. package/dist/Components/Layouts/Toaster.d.ts +76 -0
  100. package/dist/Components/Layouts/Toaster.js +172 -0
  101. package/dist/Components/Viewers/DataTable.d.ts +25 -0
  102. package/dist/Components/Viewers/DataTable.js +542 -0
  103. package/dist/Components/Viewers/DataTableParts.d.ts +134 -0
  104. package/dist/Components/Viewers/DataTableParts.js +145 -0
  105. package/dist/Components/Viewers/MarkdownView.d.ts +26 -0
  106. package/dist/Components/Viewers/MarkdownView.js +79 -0
  107. package/dist/Components/Viewers/RichTextView.d.ts +18 -0
  108. package/dist/Components/Viewers/RichTextView.js +17 -0
  109. package/dist/Helpers/AnimationHelpers.d.ts +6 -0
  110. package/dist/Helpers/AnimationHelpers.js +35 -0
  111. package/dist/Helpers/ApiClient.d.ts +74 -0
  112. package/dist/Helpers/ApiClient.js +197 -0
  113. package/dist/Helpers/ApiFormHelpers.d.ts +26 -0
  114. package/dist/Helpers/ApiFormHelpers.js +56 -0
  115. package/dist/Helpers/ApiModules.d.ts +135 -0
  116. package/dist/Helpers/ApiModules.js +194 -0
  117. package/dist/Helpers/ApiResponses.d.ts +41 -0
  118. package/dist/Helpers/ApiResponses.js +159 -0
  119. package/dist/Helpers/ApiRoutes.d.ts +150 -0
  120. package/dist/Helpers/ApiRoutes.js +191 -0
  121. package/dist/Helpers/ApiToasts.d.ts +21 -0
  122. package/dist/Helpers/ApiToasts.js +22 -0
  123. package/dist/Helpers/ClassHelpers.d.ts +3 -0
  124. package/dist/Helpers/ClassHelpers.js +4 -0
  125. package/dist/Helpers/CrudBackend.d.ts +56 -0
  126. package/dist/Helpers/CrudBackend.js +119 -0
  127. package/dist/Helpers/CrudHelpers.d.ts +76 -0
  128. package/dist/Helpers/CrudHelpers.js +202 -0
  129. package/dist/Helpers/DateHelpers.d.ts +88 -0
  130. package/dist/Helpers/DateHelpers.js +363 -0
  131. package/dist/Helpers/DatePreferences.d.ts +14 -0
  132. package/dist/Helpers/DatePreferences.js +94 -0
  133. package/dist/Helpers/DateTimeHelpers.d.ts +87 -0
  134. package/dist/Helpers/DateTimeHelpers.js +286 -0
  135. package/dist/Helpers/DragHelpers.d.ts +129 -0
  136. package/dist/Helpers/DragHelpers.js +281 -0
  137. package/dist/Helpers/DropdownHelpers.d.ts +56 -0
  138. package/dist/Helpers/DropdownHelpers.js +180 -0
  139. package/dist/Helpers/FormDrafts.d.ts +18 -0
  140. package/dist/Helpers/FormDrafts.js +76 -0
  141. package/dist/Helpers/FormStore.d.ts +231 -0
  142. package/dist/Helpers/FormStore.js +734 -0
  143. package/dist/Helpers/InputParamsHelpers.d.ts +79 -0
  144. package/dist/Helpers/InputParamsHelpers.js +388 -0
  145. package/dist/Helpers/MarkdownEditing.d.ts +27 -0
  146. package/dist/Helpers/MarkdownEditing.js +250 -0
  147. package/dist/Helpers/MarkdownHelpers.d.ts +82 -0
  148. package/dist/Helpers/MarkdownHelpers.js +775 -0
  149. package/dist/Helpers/MessageBuilder.d.ts +74 -0
  150. package/dist/Helpers/MessageBuilder.js +254 -0
  151. package/dist/Helpers/NepaliCalendar.d.ts +25 -0
  152. package/dist/Helpers/NepaliCalendar.js +107 -0
  153. package/dist/Helpers/NumberHelpers.d.ts +71 -0
  154. package/dist/Helpers/NumberHelpers.js +202 -0
  155. package/dist/Helpers/Permissions.d.ts +62 -0
  156. package/dist/Helpers/Permissions.js +133 -0
  157. package/dist/Helpers/PopoverHelpers.d.ts +43 -0
  158. package/dist/Helpers/PopoverHelpers.js +91 -0
  159. package/dist/Helpers/RichTextHelpers.d.ts +77 -0
  160. package/dist/Helpers/RichTextHelpers.js +1072 -0
  161. package/dist/Helpers/RowsStore.d.ts +160 -0
  162. package/dist/Helpers/RowsStore.js +584 -0
  163. package/dist/Helpers/TableColumns.d.ts +40 -0
  164. package/dist/Helpers/TableColumns.js +254 -0
  165. package/dist/Helpers/TableExport.d.ts +30 -0
  166. package/dist/Helpers/TableExport.js +97 -0
  167. package/dist/Helpers/TableHelpers.d.ts +102 -0
  168. package/dist/Helpers/TableHelpers.js +281 -0
  169. package/dist/Helpers/TableStore.d.ts +142 -0
  170. package/dist/Helpers/TableStore.js +530 -0
  171. package/dist/Helpers/ThemeHelpers.d.ts +15 -0
  172. package/dist/Helpers/ThemeHelpers.js +16 -0
  173. package/dist/Helpers/TimeHelpers.d.ts +73 -0
  174. package/dist/Helpers/TimeHelpers.js +253 -0
  175. package/dist/Helpers/TreeStore.d.ts +188 -0
  176. package/dist/Helpers/TreeStore.js +577 -0
  177. package/dist/Helpers/ValidationHelpers.d.ts +124 -0
  178. package/dist/Helpers/ValidationHelpers.js +301 -0
  179. package/dist/Interfaces/ApiInterfaces.d.ts +149 -0
  180. package/dist/Interfaces/ApiInterfaces.js +24 -0
  181. package/dist/Interfaces/CrudInterfaces.d.ts +171 -0
  182. package/{src/sso-client.tsx → dist/Interfaces/CrudInterfaces.js} +1 -1
  183. package/dist/Interfaces/DateInterfaces.d.ts +154 -0
  184. package/dist/Interfaces/DateInterfaces.js +29 -0
  185. package/dist/Interfaces/FormInterfaces.d.ts +352 -0
  186. package/dist/Interfaces/FormInterfaces.js +1 -0
  187. package/dist/Interfaces/InputInterfaces.d.ts +141 -0
  188. package/dist/Interfaces/InputInterfaces.js +1 -0
  189. package/dist/Interfaces/MessageInterfaces.d.ts +111 -0
  190. package/dist/Interfaces/MessageInterfaces.js +5 -0
  191. package/dist/Interfaces/TableInterfaces.d.ts +291 -0
  192. package/dist/Interfaces/TableInterfaces.js +1 -0
  193. package/dist/Interfaces/ThemeInterfaces.d.ts +17 -0
  194. package/dist/Interfaces/ThemeInterfaces.js +17 -0
  195. package/dist/Interfaces/TypeInterfaces.d.ts +4 -0
  196. package/dist/Interfaces/TypeInterfaces.js +1 -0
  197. package/dist/Server.Index.d.ts +2 -0
  198. package/dist/Server.Index.js +5 -0
  199. package/dist/Services/ApiProxy.d.ts +32 -0
  200. package/dist/Services/ApiProxy.js +111 -0
  201. package/dist/Services/BrowserApi.d.ts +78 -0
  202. package/dist/Services/BrowserApi.js +111 -0
  203. package/dist/Services/ServerApi.d.ts +49 -0
  204. package/dist/Services/ServerApi.js +75 -0
  205. package/dist/Services/ThemeService.d.ts +17 -0
  206. package/dist/Services/ThemeService.js +83 -0
  207. package/dist/Shared.Index.d.ts +43 -0
  208. package/dist/Shared.Index.js +47 -0
  209. package/package.json +82 -33
  210. package/src/Styles/Nexus.Base.css +70 -0
  211. package/src/Styles/Nexus.Button.css +151 -0
  212. package/src/Styles/Nexus.Calendar.css +275 -0
  213. package/src/Styles/Nexus.Choice.css +345 -0
  214. package/src/Styles/Nexus.Crud.css +84 -0
  215. package/src/Styles/Nexus.DateTime.css +81 -0
  216. package/src/Styles/Nexus.Dropdown.css +774 -0
  217. package/src/Styles/Nexus.Editor.css +297 -0
  218. package/src/Styles/Nexus.Form.css +298 -0
  219. package/src/Styles/Nexus.Index.css +34 -0
  220. package/src/Styles/Nexus.Input.css +637 -0
  221. package/src/Styles/Nexus.Markdown.css +265 -0
  222. package/src/Styles/Nexus.Menu.css +249 -0
  223. package/src/Styles/Nexus.Popup.css +731 -0
  224. package/src/Styles/Nexus.RichText.css +463 -0
  225. package/src/Styles/Nexus.Rows.css +566 -0
  226. package/src/Styles/Nexus.Slider.css +278 -0
  227. package/src/Styles/Nexus.Tab.Buttons.css +364 -0
  228. package/src/Styles/Nexus.Table.css +1014 -0
  229. package/src/Styles/Nexus.Theme.Picker.css +159 -0
  230. package/src/Styles/Nexus.Themes.css +194 -0
  231. package/src/Styles/Nexus.Time.css +147 -0
  232. package/src/Styles/Nexus.Toast.css +265 -0
  233. package/src/Styles/Nexus.Tokens.css +72 -0
  234. package/src/Styles/Nexus.Tree.css +435 -0
  235. package/src/api-services/authentication-service.tsx +0 -23
  236. package/src/api-services/preference-service.tsx +0 -5
  237. package/src/api-services/system-service.tsx +0 -34
  238. package/src/client.ts +0 -31
  239. package/src/components/documents/button.tsx +0 -142
  240. package/src/components/documents/icon-box.tsx +0 -93
  241. package/src/components/documents/page-title.tsx +0 -7
  242. package/src/components/documents/tab-button.tsx +0 -172
  243. package/src/components/documents/tag.tsx +0 -30
  244. package/src/components/index.js +0 -0
  245. package/src/components/inputs/checkbox-input.tsx +0 -66
  246. package/src/components/inputs/input-box.tsx +0 -48
  247. package/src/components/inputs/input-element.tsx +0 -65
  248. package/src/components/inputs/input-form.tsx +0 -164
  249. package/src/components/inputs/input.tsx +0 -181
  250. package/src/components/inputs/number-input.tsx +0 -108
  251. package/src/components/inputs/radiobox-input.tsx +0 -53
  252. package/src/components/inputs/textarea-input.tsx +0 -47
  253. package/src/components/inputs/textbox-input.tsx +0 -45
  254. package/src/components/layouts/global-dialogbox.tsx +0 -431
  255. package/src/components/layouts/global-layout.tsx +0 -59
  256. package/src/components/layouts/layout-helpers.tsx +0 -20
  257. package/src/components/layouts/panels/user-panel.tsx +0 -112
  258. package/src/components/layouts/utility-menu.tsx +0 -49
  259. package/src/components/panels/theme-panel.tsx +0 -46
  260. package/src/helpers/bitwise-helpers.tsx +0 -11
  261. package/src/helpers/browser-helpers.tsx +0 -156
  262. package/src/helpers/datasource-helpers.tsx +0 -99
  263. package/src/helpers/element-helpers.tsx +0 -57
  264. package/src/helpers/input-helpers.tsx +0 -74
  265. package/src/helpers/string-helpers.tsx +0 -28
  266. package/src/helpers/utility-helpers.tsx +0 -45
  267. package/src/helpers/validation-helpers.tsx +0 -302
  268. package/src/index.ts +0 -26
  269. package/src/interface.ts +0 -19
  270. package/src/interfaces/auth-token-interfaces.tsx +0 -6
  271. package/src/interfaces/browser-interfaces.tsx +0 -36
  272. package/src/interfaces/button-interfaces.tsx +0 -63
  273. package/src/interfaces/datasource-interfaces.tsx +0 -22
  274. package/src/interfaces/datatable-interfaces.tsx +0 -25
  275. package/src/interfaces/dialogbox-interfaces.tsx +0 -5
  276. package/src/interfaces/exception-interfaces.tsx +0 -99
  277. package/src/interfaces/http-interfaces.tsx +0 -129
  278. package/src/interfaces/icon-interfaces.tsx +0 -129
  279. package/src/interfaces/input-interfaces.tsx +0 -410
  280. package/src/interfaces/layout-interfaces.tsx +0 -191
  281. package/src/interfaces/menu-interfaces.tsx +0 -49
  282. package/src/interfaces/message-interfaces.tsx +0 -32
  283. package/src/interfaces/permission-interfaces.tsx +0 -9
  284. package/src/interfaces/storage-interfaces.tsx +0 -5
  285. package/src/interfaces/system-interfaces.tsx +0 -22
  286. package/src/interfaces/theme-interfaces.tsx +0 -123
  287. package/src/interfaces/type-interfaces.tsx +0 -28
  288. package/src/interfaces/user-interfaces.tsx +0 -47
  289. package/src/nexus-client.tsx +0 -21
  290. package/src/nexus.environments.tsx +0 -66
  291. package/src/proxy-api/proxy-backend.tsx +0 -60
  292. package/src/proxy-api/proxy-constants.tsx +0 -28
  293. package/src/proxy-api/proxy-helpers.tsx +0 -91
  294. package/src/proxy-api-client.tsx +0 -1
  295. package/src/proxy-api-server.tsx +0 -2
  296. package/src/services/http-client-services.tsx +0 -75
  297. package/src/services/http-services.tsx +0 -158
  298. package/src/services/loader-service.tsx +0 -185
  299. package/src/services/localstorage-service.tsx +0 -119
  300. package/src/services/message-services.tsx +0 -383
  301. package/src/services/theme-service.tsx +0 -161
  302. package/src/services/user-services.tsx +0 -10
  303. package/src/sso-config/auth-token-validation.ts +0 -16
  304. package/src/sso-config/callback-route.tsx +0 -35
  305. package/src/sso-config/cookie-encryption.ts +0 -51
  306. package/src/sso-config/cookie-helpers.tsx +0 -102
  307. package/src/sso-config/forgot-password-route.tsx +0 -36
  308. package/src/sso-config/oauth-callback-state.ts +0 -61
  309. package/src/sso-config/pkce-helpers.ts +0 -9
  310. package/src/sso-config/provider-complete-route.tsx +0 -18
  311. package/src/sso-config/redirect-context-actions.tsx +0 -45
  312. package/src/sso-config/redirect-context-helpers.tsx +0 -159
  313. package/src/sso-config/redirect-context-persist.tsx +0 -18
  314. package/src/sso-config/redirect-context-route.tsx +0 -45
  315. package/src/sso-config/refresh-route.tsx +0 -23
  316. package/src/sso-config/sign-in-route.tsx +0 -74
  317. package/src/sso-config/sign-out-route.tsx +0 -57
  318. package/src/sso-config/sign-up-route.tsx +0 -57
  319. package/src/sso-config/sso-backend-flow.ts +0 -38
  320. package/src/sso-config/sso-complete-route.tsx +0 -50
  321. package/src/sso-config/sso-config.ts +0 -59
  322. package/src/sso-config/sso-interfaces.tsx +0 -99
  323. package/src/sso-config/sso-response-helpers.tsx +0 -20
  324. package/src/sso-server.tsx +0 -21
  325. package/src/styles/nexus.animation.css +0 -269
  326. package/src/styles/nexus.core.css +0 -119
  327. package/src/styles/nexus.dialog.css +0 -144
  328. package/src/styles/nexus.icon.css +0 -51
  329. package/src/styles/nexus.input.css +0 -207
  330. package/src/styles/nexus.loader.css +0 -11
  331. package/src/styles/nexus.logic.css +0 -47
  332. package/src/styles/nexus.utility.css +0 -347
@@ -0,0 +1,1072 @@
1
+ import { safeUrl } from "./MarkdownHelpers.js";
2
+ import { DEFAULT_FIELD_NAME, maxLengthMessage, minLengthMessage, requiredMessage } from "./ValidationHelpers.js";
3
+ // ---------------------------------------------------------------------------------------------------------------
4
+ // Public API
5
+ /**
6
+ * The HTML a rich text editor stores: only the tags, attributes, and styles it makes, rebuilt from scratch, so script,
7
+ * event handlers, `javascript:` links, and foreign styles cannot pass. Pastes from Word and Google Docs keep their
8
+ * bold, italic, underline, colors, lists, and tables. Empty text gives "".
9
+ *
10
+ * @example sanitizeRichText('<p onclick="x()">Hi <b>there</b><script>alert(1)</script></p>') // '<p>Hi <strong>there</strong></p>'
11
+ */
12
+ export function sanitizeRichText(html, options = {}) {
13
+ return html ? emit(cleanDocument(html, options), {}) : "";
14
+ }
15
+ /** The same HTML as `sanitizeRichText`, one block per line and indented, for a source view. */
16
+ export function formatRichTextHtml(html, options = {}) {
17
+ return html ? emitPretty(cleanDocument(html, options), 0) : "";
18
+ }
19
+ /**
20
+ * The text a reader sees: one line per paragraph, heading, or list item, line breaks kept, table cells separated by
21
+ * tabs, and spaces collapsed. Tokens stay as written.
22
+ */
23
+ export function richTextToPlainText(html) {
24
+ return html ? analyze(html).text : "";
25
+ }
26
+ /** Whether rich text has nothing a reader sees: no text and no image. A rule or an empty table counts as empty. */
27
+ export function isRichTextEmpty(html) {
28
+ return html ? analyze(html).empty : true;
29
+ }
30
+ /**
31
+ * HTML for display (`RichTextView`): sanitized, with tokens filled from `values` and, with `newTab`, links to other
32
+ * sites opening in a new tab.
33
+ */
34
+ export function renderRichTextHtml(html, options = {}) {
35
+ if (!html)
36
+ return "";
37
+ const { tokenFormat = "{{key}}", values, newTab } = options;
38
+ return emit(cleanDocument(html, options), {
39
+ text: values ? text => fillTokens(text, tokenFormat, values) : undefined,
40
+ attrs: newTab ? linkInNewTab : undefined,
41
+ });
42
+ }
43
+ /** HTML for the editor itself: sanitized, with each token shown as a chip that is edited as one piece. */
44
+ export function richTextToEditorHtml(html, options = {}) {
45
+ if (!html)
46
+ return "";
47
+ const { tokenFormat, tokens } = options;
48
+ if (!tokenFormat)
49
+ return emit(cleanDocument(html, options), { rename: highlightAsSpan });
50
+ const known = tokens ? new Map(tokens.map(token => [token.key, token])) : null;
51
+ return emit(cleanDocument(html, options), { text: text => tokenChips(text, tokenFormat, known), rename: highlightAsSpan });
52
+ }
53
+ /** The HTML of one token chip, as the editor inserts it. */
54
+ export function richTextTokenChip(key, format, token, known = true) {
55
+ const raw = formatRichTextToken(key, format);
56
+ const title = known ? (token?.description ? `${raw}: ${token.description}` : raw) : `Unknown field: ${raw}`;
57
+ return (`<span class="nx-rte__token" contenteditable="false" data-token="${escapeAttribute(raw)}"${known ? "" : ' data-unknown=""'}` +
58
+ ` title="${escapeAttribute(title)}">${escapeText(token?.label || key)}</span>`);
59
+ }
60
+ /** A token as the template engine reads it: `|Key|` or `{{Key}}`. */
61
+ export function formatRichTextToken(key, format) {
62
+ return format === "|key|" ? `|${key}|` : `{{${key}}}`;
63
+ }
64
+ /** The keys of the tokens in the text, each once, in the order they first appear. */
65
+ export function findRichTextTokens(html, format) {
66
+ const keys = new Set();
67
+ for (const match of richTextToPlainText(html).matchAll(tokenPattern(format)))
68
+ keys.add(match[1]);
69
+ return [...keys];
70
+ }
71
+ /** Fills the tokens in sanitized HTML with values, as text. Tokens without a value stay as they are. */
72
+ export function fillRichTextTokens(html, values, format = "{{key}}") {
73
+ return renderRichTextHtml(html, { tokenFormat: format, values, images: true });
74
+ }
75
+ /** Checks rich text against its rules and returns the first message, or `null`. Lengths count the plain text. */
76
+ export function validateRichText(html, rules, fieldName = DEFAULT_FIELD_NAME) {
77
+ const value = html ?? "";
78
+ const { text, empty } = value ? analyze(value) : { text: "", empty: true };
79
+ if (!empty) {
80
+ const { minLength, maxLength } = rules;
81
+ if (minLength && text.length < minLength)
82
+ return minLengthMessage(fieldName, minLength);
83
+ if (maxLength && text.length > maxLength)
84
+ return maxLengthMessage(fieldName, maxLength);
85
+ }
86
+ else if (rules.required) {
87
+ return requiredMessage(fieldName);
88
+ }
89
+ return rules.validate?.(value, text) || null;
90
+ }
91
+ // The last text read: an editor asks for the text, the count, and the rules of the same value right after each other.
92
+ let lastAnalysis = null;
93
+ function analyze(html) {
94
+ if (lastAnalysis?.html === html)
95
+ return lastAnalysis.result;
96
+ const nodes = cleanDocument(html, { images: true });
97
+ const writer = { text: "" };
98
+ writePlain(nodes, writer);
99
+ const text = writer.text
100
+ .split("\n")
101
+ .map(line => line.replace(/ {2,}/g, " ").trim())
102
+ .join("\n")
103
+ .trim();
104
+ const result = { text, empty: !text && !hasTag(nodes, "img") };
105
+ lastAnalysis = { html, result };
106
+ return result;
107
+ }
108
+ const VOID_TAGS = new Set(["area", "base", "basefont", "bgsound", "br", "col", "embed", "frame", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]);
109
+ /** Elements whose content is not HTML: skipped whole, with their content. */
110
+ const RAW_TEXT_TAGS = new Set(["script", "style", "textarea", "title", "xmp", "iframe", "noembed", "noframes", "noscript"]);
111
+ const HEADINGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
112
+ const CELLS = new Set(["td", "th"]);
113
+ const SECTIONS = new Set(["thead", "tbody", "tfoot"]);
114
+ const TABLE_PARTS = new Set(["table", "caption", "thead", "tbody", "tfoot", "tr", "td", "th"]);
115
+ /** Start tags that close an open paragraph. */
116
+ const CLOSES_P = new Set([
117
+ "address", "article", "aside", "blockquote", "center", "details", "dialog", "dir", "div", "dl", "dd", "dt", "fieldset",
118
+ "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", "main",
119
+ "menu", "nav", "ol", "p", "pre", "section", "summary", "table", "ul",
120
+ ]);
121
+ const BUTTON_SCOPE = new Set(["td", "th", "table", "caption", "button", "marquee", "object", "template"]);
122
+ const LIST_SCOPE = new Set([...BUTTON_SCOPE, "ul", "ol"]);
123
+ const ROW_SCOPE = new Set(["tr", "table", "template"]);
124
+ const TABLE_SCOPE = new Set(["table", "template"]);
125
+ const TAG_NAME = /[a-zA-Z][^\t\n\f\r />]*/y;
126
+ const END_TAG = /<\/([a-zA-Z][^\t\n\f\r />]*)[^>]*>?/y;
127
+ const ATTR_NAME = /[^\t\n\f\r />][^\t\n\f\r />=]*/y;
128
+ const UNQUOTED = /[^\t\n\f\r >]*/y;
129
+ const CLOSING_TAG = /<\/([a-zA-Z][a-zA-Z0-9]*)[\t\n\f\r />]/g;
130
+ function isSpaceCode(code) {
131
+ return code === 32 || code === 9 || code === 10 || code === 12 || code === 13;
132
+ }
133
+ function skipSpaces(html, index) {
134
+ while (index < html.length && isSpaceCode(html.charCodeAt(index)))
135
+ index++;
136
+ return index;
137
+ }
138
+ function parseHtml(html) {
139
+ const root = { tag: "#root", attrs: new Map(), children: [] };
140
+ const stack = [root];
141
+ const length = html.length;
142
+ const current = () => stack[stack.length - 1];
143
+ function addText(text) {
144
+ if (!text)
145
+ return;
146
+ const children = current().children;
147
+ const decoded = decodeEntities(text);
148
+ const last = children.length - 1;
149
+ if (typeof children[last] === "string")
150
+ children[last] = children[last] + decoded;
151
+ else
152
+ children.push(decoded);
153
+ }
154
+ /** Closes the nearest open element named in `tags`, unless a boundary comes first. */
155
+ function closeInScope(tags, boundaries) {
156
+ for (let k = stack.length - 1; k > 0; k--) {
157
+ const tag = stack[k].tag;
158
+ if (typeof tags === "string" ? tag === tags : tags.has(tag)) {
159
+ stack.length = k;
160
+ return;
161
+ }
162
+ if (boundaries.has(tag))
163
+ return;
164
+ }
165
+ }
166
+ function openTag(tag, attrs) {
167
+ if (tag === "html" || tag === "body") {
168
+ // The body's content follows, so a head left open ends here.
169
+ if (tag === "body")
170
+ closeInScope("head", TABLE_SCOPE);
171
+ return;
172
+ }
173
+ if (tag === "li")
174
+ closeInScope("li", LIST_SCOPE);
175
+ else if (tag === "dd" || tag === "dt")
176
+ closeInScope(new Set(["dd", "dt"]), LIST_SCOPE);
177
+ else if (CELLS.has(tag))
178
+ closeInScope(CELLS, ROW_SCOPE);
179
+ else if (tag === "tr")
180
+ closeInScope("tr", TABLE_SCOPE);
181
+ else if (SECTIONS.has(tag)) {
182
+ closeInScope("tr", TABLE_SCOPE);
183
+ closeInScope(SECTIONS, TABLE_SCOPE);
184
+ }
185
+ else if (tag === "a")
186
+ closeInScope("a", BUTTON_SCOPE);
187
+ if (CLOSES_P.has(tag))
188
+ closeInScope("p", BUTTON_SCOPE);
189
+ if (HEADINGS.has(tag) && HEADINGS.has(current().tag))
190
+ stack.pop();
191
+ const element = { tag, attrs, children: [] };
192
+ current().children.push(element);
193
+ if (!VOID_TAGS.has(tag))
194
+ stack.push(element);
195
+ }
196
+ function closeTag(tag) {
197
+ // "</br>" is read as "<br>", as browsers do.
198
+ if (tag === "br") {
199
+ current().children.push({ tag: "br", attrs: new Map(), children: [] });
200
+ return;
201
+ }
202
+ const tablePart = TABLE_PARTS.has(tag);
203
+ for (let k = stack.length - 1; k > 0; k--) {
204
+ const open = stack[k].tag;
205
+ if (open === tag) {
206
+ stack.length = k;
207
+ return;
208
+ }
209
+ // An end tag does not reach out of a table cell or a table, except the table's own parts.
210
+ if (!tablePart && (open === "td" || open === "th" || open === "caption" || open === "table"))
211
+ return;
212
+ if (tablePart && open === "table")
213
+ return;
214
+ }
215
+ }
216
+ let index = 0;
217
+ while (index < length) {
218
+ const open = html.indexOf("<", index);
219
+ if (open < 0) {
220
+ addText(html.slice(index));
221
+ break;
222
+ }
223
+ addText(html.slice(index, open));
224
+ index = open;
225
+ const next = html.charCodeAt(open + 1);
226
+ if (html.startsWith("<!--", open)) {
227
+ const end = html.indexOf("-->", open + 4);
228
+ index = end < 0 ? length : end + 3;
229
+ }
230
+ else if (next === 33 || next === 63) {
231
+ // "<!DOCTYPE …>", "<![CDATA[…]]>", "<?xml …?>": comments here.
232
+ const end = html.indexOf(">", open + 2);
233
+ index = end < 0 ? length : end + 1;
234
+ }
235
+ else if (next === 47) {
236
+ END_TAG.lastIndex = open;
237
+ const match = END_TAG.exec(html);
238
+ if (match) {
239
+ closeTag(match[1].toLowerCase());
240
+ index = END_TAG.lastIndex;
241
+ }
242
+ else {
243
+ const end = html.indexOf(">", open + 2);
244
+ index = end < 0 ? length : end + 1;
245
+ }
246
+ }
247
+ else if ((next >= 65 && next <= 90) || (next >= 97 && next <= 122)) {
248
+ const tag = readTag(html, open + 1);
249
+ // The input ends inside a tag: the tag is dropped.
250
+ if (!tag)
251
+ break;
252
+ index = tag.end;
253
+ if (tag.name === "plaintext")
254
+ break;
255
+ if (RAW_TEXT_TAGS.has(tag.name))
256
+ index = skipRawText(html, tag.name, index);
257
+ else
258
+ openTag(tag.name, tag.attrs);
259
+ }
260
+ else {
261
+ addText("<");
262
+ index = open + 1;
263
+ }
264
+ }
265
+ return root;
266
+ }
267
+ function readTag(html, from) {
268
+ TAG_NAME.lastIndex = from;
269
+ const name = TAG_NAME.exec(html)[0].toLowerCase();
270
+ const attrs = new Map();
271
+ let index = TAG_NAME.lastIndex;
272
+ while (index < html.length) {
273
+ const code = html.charCodeAt(index);
274
+ if (code === 62)
275
+ return { name, attrs, end: index + 1 };
276
+ if (code === 47 || isSpaceCode(code)) {
277
+ index++;
278
+ continue;
279
+ }
280
+ ATTR_NAME.lastIndex = index;
281
+ const attrName = ATTR_NAME.exec(html)[0].toLowerCase();
282
+ index = ATTR_NAME.lastIndex;
283
+ let look = skipSpaces(html, index);
284
+ let value = "";
285
+ if (html.charCodeAt(look) === 61) {
286
+ look = skipSpaces(html, look + 1);
287
+ const quote = html[look];
288
+ if (quote === '"' || quote === "'") {
289
+ const close = html.indexOf(quote, look + 1);
290
+ if (close < 0)
291
+ return null;
292
+ value = html.slice(look + 1, close);
293
+ index = close + 1;
294
+ }
295
+ else {
296
+ UNQUOTED.lastIndex = look;
297
+ value = UNQUOTED.exec(html)[0];
298
+ index = UNQUOTED.lastIndex;
299
+ }
300
+ }
301
+ if (!attrs.has(attrName))
302
+ attrs.set(attrName, decodeEntities(value));
303
+ }
304
+ return null;
305
+ }
306
+ /** Skips the content of a script, style, or similar element, up to and past its end tag. */
307
+ function skipRawText(html, tag, from) {
308
+ CLOSING_TAG.lastIndex = from;
309
+ for (let match = CLOSING_TAG.exec(html); match; match = CLOSING_TAG.exec(html)) {
310
+ if (match[1].toLowerCase() === tag) {
311
+ const end = html.indexOf(">", match.index);
312
+ return end < 0 ? html.length : end + 1;
313
+ }
314
+ }
315
+ return html.length;
316
+ }
317
+ // ---------------------------------------------------------------------------------------------------------------
318
+ // Entities
319
+ const NAMED_ENTITIES = new Map();
320
+ // Latin-1, U+00A0 to U+00FF, in order.
321
+ const LATIN1 = "nbsp iexcl cent pound curren yen brvbar sect uml copy ordf laquo not shy reg macr deg plusmn sup2 sup3 acute micro para " +
322
+ "middot cedil sup1 ordm raquo frac14 frac12 frac34 iquest Agrave Aacute Acirc Atilde Auml Aring AElig Ccedil Egrave Eacute " +
323
+ "Ecirc Euml Igrave Iacute Icirc Iuml ETH Ntilde Ograve Oacute Ocirc Otilde Ouml times Oslash Ugrave Uacute Ucirc Uuml Yacute " +
324
+ "THORN szlig agrave aacute acirc atilde auml aring aelig ccedil egrave eacute ecirc euml igrave iacute icirc iuml eth ntilde " +
325
+ "ograve oacute ocirc otilde ouml divide oslash ugrave uacute ucirc uuml yacute thorn yuml";
326
+ LATIN1.split(" ").forEach((name, k) => NAMED_ENTITIES.set(name, String.fromCharCode(0xa0 + k)));
327
+ const OTHER_ENTITIES = "amp:38 lt:60 gt:62 quot:34 apos:39 OElig:338 oelig:339 Scaron:352 scaron:353 Yuml:376 fnof:402 circ:710 tilde:732 " +
328
+ "ensp:8194 emsp:8195 thinsp:8201 zwnj:8204 zwj:8205 lrm:8206 rlm:8207 ndash:8211 mdash:8212 lsquo:8216 rsquo:8217 " +
329
+ "sbquo:8218 ldquo:8220 rdquo:8221 bdquo:8222 dagger:8224 Dagger:8225 bull:8226 hellip:8230 permil:8240 prime:8242 " +
330
+ "Prime:8243 lsaquo:8249 rsaquo:8250 euro:8364 trade:8482 larr:8592 uarr:8593 rarr:8594 darr:8595 harr:8596 minus:8722 " +
331
+ "infin:8734 ne:8800 le:8804 ge:8805";
332
+ for (const pair of OTHER_ENTITIES.split(" ")) {
333
+ const [name, code] = pair.split(":");
334
+ NAMED_ENTITIES.set(name, String.fromCharCode(Number(code)));
335
+ }
336
+ /** Names browsers also read without the closing semicolon. */
337
+ const LEGACY_ENTITIES = new Set(["amp", "lt", "gt", "quot", "nbsp", "copy", "reg"]);
338
+ /** Numeric references from 0x80 to 0x9F mean Windows-1252 characters, as in browsers. */
339
+ const WINDOWS_1252 = [
340
+ 0x20ac, 0x81, 0x201a, 0x192, 0x201e, 0x2026, 0x2020, 0x2021, 0x2c6, 0x2030, 0x160, 0x2039, 0x152, 0x8d, 0x17d, 0x8f,
341
+ 0x90, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022, 0x2013, 0x2014, 0x2dc, 0x2122, 0x161, 0x203a, 0x153, 0x9d, 0x17e, 0x178,
342
+ ];
343
+ const ENTITY = /&(?:#(\d{1,8})|#[xX]([0-9a-fA-F]{1,7})|([A-Za-z][A-Za-z0-9]{1,31}));?/g;
344
+ const REPLACEMENT = String.fromCharCode(0xfffd);
345
+ function decodeEntities(text) {
346
+ if (!text.includes("&"))
347
+ return text;
348
+ return text.replace(ENTITY, (whole, decimal, hex, name) => {
349
+ if (name !== undefined) {
350
+ const char = NAMED_ENTITIES.get(name);
351
+ return char !== undefined && (whole.endsWith(";") || LEGACY_ENTITIES.has(name)) ? char : whole;
352
+ }
353
+ const code = decimal !== undefined ? Number.parseInt(decimal, 10) : Number.parseInt(hex, 16);
354
+ if (code === 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff))
355
+ return REPLACEMENT;
356
+ if (code >= 0x80 && code <= 0x9f)
357
+ return String.fromCharCode(WINDOWS_1252[code - 0x80]);
358
+ return String.fromCodePoint(code);
359
+ });
360
+ }
361
+ const BLOCK_TAGS = new Set(["p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "pre", "ul", "ol", "li", "table", "caption", "thead", "tbody", "tfoot", "tr", "td", "th", "hr"]);
362
+ const VOID_OUTPUT = new Set(["br", "hr", "img"]);
363
+ /** Elements dropped with their content: not text a reader sees, or not safe to keep. */
364
+ const DROPPED_TAGS = new Set([
365
+ "head", "template", "svg", "math", "select", "option", "optgroup", "datalist", "object", "embed", "applet", "frameset",
366
+ "frame", "canvas", "audio", "video", "picture", "map", "area", "input", "meta", "link", "base", "col", "colgroup",
367
+ "source", "track", "param", "wbr", "rt", "rp", "keygen", "basefont", "bgsound", "noscript", "style", "script",
368
+ ]);
369
+ /** Containers that become a paragraph when they hold only text, and disappear around blocks otherwise. */
370
+ const DIV_LIKE = new Set([
371
+ "div", "center", "section", "article", "header", "footer", "main", "nav", "aside", "figure", "figcaption", "address",
372
+ "hgroup", "details", "summary", "dialog", "fieldset", "form", "legend", "dl",
373
+ ]);
374
+ const FORMAT_TAGS = new Set(["b", "strong", "i", "em", "cite", "dfn", "var", "u", "ins", "s", "strike", "del", "sub", "sup", "span", "font", "mark"]);
375
+ const CODE_TAGS = new Set(["code", "tt", "kbd", "samp"]);
376
+ /** Inline elements the output keeps. */
377
+ const INLINE_OUTPUT = new Set(["strong", "em", "u", "s", "sub", "sup", "code", "a", "span", "mark"]);
378
+ const ALIGNMENTS = new Set(["left", "center", "right", "justify"]);
379
+ const CONTROL_CHARACTERS = /[\x00-\x08\x0B\x0E-\x1F\x7F]/g;
380
+ /** Zero-width spaces and byte order marks: pastes bring them, and the editor puts them around token chips. */
381
+ const INVISIBLE_CHARACTERS = new RegExp(`[${String.fromCharCode(0x200b)}${String.fromCharCode(0xfeff)}]`, "g");
382
+ const NBSP = String.fromCharCode(0xa0);
383
+ const isBlock = (node) => typeof node !== "string" && BLOCK_TAGS.has(node.tag);
384
+ const isBreak = (node) => typeof node !== "string" && node?.tag === "br";
385
+ const element = (tag, attrs = [], children = []) => ({ tag, attrs, children });
386
+ function cleanDocument(html, options) {
387
+ const context = { images: Boolean(options.images), inline: false, pre: false, link: false };
388
+ const blocks = wrapInlineRuns(trimFlow(cleanNodes(parseHtml(html).children, context)));
389
+ // Blank lines at the start and the end of the text are not kept.
390
+ while (blocks.length && isBlankBlock(blocks[0]))
391
+ blocks.shift();
392
+ while (blocks.length && isBlankBlock(blocks[blocks.length - 1]))
393
+ blocks.pop();
394
+ return blocks;
395
+ }
396
+ function cleanNodes(nodes, context) {
397
+ const out = [];
398
+ for (const node of nodes) {
399
+ if (typeof node === "string")
400
+ pushText(out, cleanText(node, context.pre), context.pre);
401
+ else
402
+ for (const item of cleanElement(node, context))
403
+ pushNode(out, item, context.pre);
404
+ }
405
+ // A block inside text keeps its content, on lines of its own.
406
+ return context.inline && out.some(isBlock) ? flatten(out, context.pre) : out;
407
+ }
408
+ function cleanText(text, pre) {
409
+ const clean = text.replace(CONTROL_CHARACTERS, "").replace(INVISIBLE_CHARACTERS, "");
410
+ return pre ? clean.replace(/\r\n?/g, "\n") : clean.replace(/[\t\n\f\r ]+/g, " ");
411
+ }
412
+ function pushNode(out, node, pre) {
413
+ if (typeof node === "string")
414
+ pushText(out, node, pre);
415
+ else
416
+ out.push(node);
417
+ }
418
+ function pushText(out, text, pre) {
419
+ if (!text)
420
+ return;
421
+ const last = out.length - 1;
422
+ if (typeof out[last] === "string") {
423
+ const joined = out[last] + text;
424
+ out[last] = pre ? joined : joined.replace(/ {2,}/g, " ");
425
+ }
426
+ else
427
+ out.push(text);
428
+ }
429
+ function cleanElement(raw, context) {
430
+ const { tag } = raw;
431
+ // Namespaced tags from Word ("o:p", "w:sdt") hold text.
432
+ if (tag.includes(":"))
433
+ return cleanNodes(raw.children, context);
434
+ if (DROPPED_TAGS.has(tag))
435
+ return [];
436
+ const style = parseStyle(raw.attrs.get("style"));
437
+ if (tag === "br")
438
+ return [element("br")];
439
+ if (tag === "hr")
440
+ return [element("hr")];
441
+ if (tag === "img")
442
+ return cleanImage(raw, context);
443
+ if (tag === "p" || HEADINGS.has(tag) || tag === "dt" || tag === "dd" || tag === "legend") {
444
+ const children = blockColor(trimFlow(cleanNodes(raw.children, { ...context, inline: true })), style);
445
+ return children.length ? [element(HEADINGS.has(tag) ? tag : "p", alignAttrs(raw, style), children)] : [];
446
+ }
447
+ if (DIV_LIKE.has(tag)) {
448
+ const children = blockColor(cleanNodes(raw.children, context), style);
449
+ if (context.inline || children.some(isBlock))
450
+ return children;
451
+ const trimmed = trimFlow(children);
452
+ const align = tag === "center" ? [["style", "text-align: center"]] : alignAttrs(raw, style);
453
+ return trimmed.length ? [element("p", align, trimmed)] : [];
454
+ }
455
+ if (tag === "blockquote") {
456
+ const children = trimFlow(cleanNodes(raw.children, { ...context, inline: false }));
457
+ return children.length ? [element("blockquote", [], children)] : [];
458
+ }
459
+ if (tag === "pre" || tag === "listing") {
460
+ const children = cleanNodes(raw.children, { ...context, inline: true, pre: true });
461
+ // A line break at the end of the last line does not show.
462
+ if (isBreak(children[children.length - 1]) && children.length > 1 && !isBreak(children[children.length - 2]))
463
+ children.pop();
464
+ return children.length ? [element("pre", [], children)] : [];
465
+ }
466
+ if (tag === "ul" || tag === "ol" || tag === "dir" || tag === "menu")
467
+ return cleanList(raw, context);
468
+ if (tag === "table")
469
+ return cleanTable(raw, context);
470
+ // Parts found outside their table or list read as what they hold.
471
+ if (tag === "li" || CELLS.has(tag) || tag === "caption") {
472
+ const children = trimFlow(cleanNodes(raw.children, { ...context, inline: true }));
473
+ return children.length ? [element("p", alignAttrs(raw, style), children)] : [];
474
+ }
475
+ if (tag === "tr" || SECTIONS.has(tag))
476
+ return cleanNodes(raw.children, context);
477
+ if (tag === "a")
478
+ return cleanLink(raw, context);
479
+ // A token chip from the editor is its token again.
480
+ const token = raw.attrs.get("data-token")?.trim();
481
+ if (tag === "span" && token && TOKEN_TEXT.test(token))
482
+ return [token];
483
+ if (FORMAT_TAGS.has(tag) || CODE_TAGS.has(tag))
484
+ return cleanFormat(raw, style, context);
485
+ // Anything else (abbr, label, small, custom elements) keeps its content.
486
+ return cleanNodes(raw.children, context);
487
+ }
488
+ function cleanImage(raw, context) {
489
+ const src = context.images ? safeUrl(raw.attrs.get("src") ?? "", "image") : null;
490
+ if (!src)
491
+ return [];
492
+ const attrs = [
493
+ ["src", src],
494
+ ["alt", (raw.attrs.get("alt") ?? "").trim()],
495
+ ];
496
+ for (const name of ["width", "height"]) {
497
+ const value = raw.attrs.get(name)?.trim();
498
+ if (value && /^\d{1,4}$/.test(value))
499
+ attrs.push([name, value]);
500
+ }
501
+ return [element("img", attrs)];
502
+ }
503
+ function cleanLink(raw, context) {
504
+ const href = raw.attrs.get("href");
505
+ const url = href === undefined || context.link ? null : safeUrl(href);
506
+ const inner = { ...context, link: context.link || Boolean(url) };
507
+ if (!url)
508
+ return cleanNodes(raw.children, inner);
509
+ const attrs = [["href", url]];
510
+ const title = raw.attrs.get("title")?.trim();
511
+ if (title)
512
+ attrs.push(["title", title]);
513
+ // A link around blocks (a card on a web page) links the text of each block.
514
+ if (hasBlockContent(raw))
515
+ return wrapInlineContent(cleanNodes(raw.children, inner), run => [element("a", attrs, run)]);
516
+ const children = cleanNodes(raw.children, { ...inner, inline: true });
517
+ return children.length ? [element("a", attrs, children)] : [];
518
+ }
519
+ function cleanFormat(raw, style, context) {
520
+ const format = formatFromStyle(style);
521
+ const { tag } = raw;
522
+ if (tag === "b" || tag === "strong")
523
+ format.bold = !isNormal(style.get("font-weight"));
524
+ else if (tag === "i" || tag === "em" || tag === "cite" || tag === "dfn" || tag === "var")
525
+ format.italic = !isNormal(style.get("font-style"));
526
+ else if (tag === "u" || tag === "ins")
527
+ format.underline = true;
528
+ else if (tag === "s" || tag === "strike" || tag === "del")
529
+ format.strike = true;
530
+ else if (tag === "sub" || tag === "sup")
531
+ format.script = tag;
532
+ else if (tag === "mark")
533
+ format.mark = true;
534
+ else if (tag === "font")
535
+ format.color ??= textColor(raw.attrs.get("color"));
536
+ else if (CODE_TAGS.has(tag))
537
+ format.code = true;
538
+ // Around blocks (Google Docs wraps a whole paste in `<b style="font-weight:normal">`): the format goes inside each block.
539
+ if (hasBlockContent(raw))
540
+ return wrapInlineContent(cleanNodes(raw.children, context), run => applyFormat(run, format));
541
+ return applyFormat(cleanNodes(raw.children, { ...context, inline: true }), format);
542
+ }
543
+ function applyFormat(children, format) {
544
+ if (!children.length)
545
+ return [];
546
+ let nodes = children;
547
+ if (format.code)
548
+ nodes = [element("code", [], nodes)];
549
+ if (format.script)
550
+ nodes = [element(format.script, [], nodes)];
551
+ if (format.strike)
552
+ nodes = [element("s", [], nodes)];
553
+ if (format.underline)
554
+ nodes = [element("u", [], nodes)];
555
+ if (format.italic)
556
+ nodes = [element("em", [], nodes)];
557
+ if (format.bold)
558
+ nodes = [element("strong", [], nodes)];
559
+ if (format.background || format.mark) {
560
+ const styles = [format.background && `background-color: ${format.background}`, format.color && `color: ${format.color}`].filter(Boolean);
561
+ nodes = [element("mark", styles.length ? [["style", styles.join("; ")]] : [], nodes)];
562
+ }
563
+ else if (format.color) {
564
+ nodes = [element("span", [["style", `color: ${format.color}`]], nodes)];
565
+ }
566
+ return nodes;
567
+ }
568
+ function formatFromStyle(style) {
569
+ const weight = style.get("font-weight") ?? "";
570
+ const decoration = `${style.get("text-decoration") ?? ""} ${style.get("text-decoration-line") ?? ""}`;
571
+ const align = style.get("vertical-align");
572
+ const background = parseColor(style.get("background-color") ?? style.get("background") ?? "");
573
+ return {
574
+ bold: weight === "bold" || weight === "bolder" || Number(weight) >= 600,
575
+ italic: /^(italic|oblique)/.test(style.get("font-style") ?? ""),
576
+ underline: decoration.includes("underline"),
577
+ strike: decoration.includes("line-through"),
578
+ script: align === "super" ? "sup" : align === "sub" ? "sub" : null,
579
+ code: false,
580
+ mark: false,
581
+ color: textColor(style.get("color")),
582
+ // White "highlights" come with text copied from web pages; they are page backgrounds, not highlights.
583
+ background: background === "#ffffff" ? null : background,
584
+ };
585
+ }
586
+ /** A text color worth keeping: black is the default color of pasted text, and would vanish in a dark theme. */
587
+ function textColor(value) {
588
+ const color = value ? parseColor(value) : null;
589
+ return color === "#000000" ? null : color;
590
+ }
591
+ const isNormal = (value) => value === "normal" || value === "lighter" || (Number(value) > 0 && Number(value) < 600);
592
+ /** Whether an element holds a block, directly or through inline elements. */
593
+ function hasBlockContent(raw) {
594
+ return raw.children.some(child => typeof child !== "string" &&
595
+ !DROPPED_TAGS.has(child.tag) &&
596
+ (CLOSES_P.has(child.tag) || TABLE_PARTS.has(child.tag) || DIV_LIKE.has(child.tag) || hasBlockContent(child)));
597
+ }
598
+ /** Wraps each run of inline content, inside blocks too, e.g. to keep a format that was around several paragraphs. */
599
+ function wrapInlineContent(nodes, wrap) {
600
+ const out = [];
601
+ let run = [];
602
+ const flush = () => {
603
+ if (run.some(node => typeof node !== "string" || node.trim()))
604
+ out.push(...wrap(run));
605
+ run = [];
606
+ };
607
+ for (const node of nodes) {
608
+ if (isBlock(node)) {
609
+ flush();
610
+ out.push(node.tag === "hr" ? node : element(node.tag, node.attrs, wrapInlineContent(node.children, wrap)));
611
+ }
612
+ else
613
+ run.push(node);
614
+ }
615
+ flush();
616
+ return out;
617
+ }
618
+ /** A text color set on a block (Word writes `<p style="color: …">`) goes on the block's text. */
619
+ function blockColor(children, style) {
620
+ const color = textColor(style.get("color"));
621
+ return color ? wrapInlineContent(children, run => [element("span", [["style", `color: ${color}`]], run)]) : children;
622
+ }
623
+ function cleanList(raw, context) {
624
+ const inner = { ...context, inline: false };
625
+ const items = [];
626
+ let loose = [];
627
+ const flushLoose = () => {
628
+ const children = loose.length ? trimFlow(cleanNodes(loose, inner)) : [];
629
+ loose = [];
630
+ if (children.length)
631
+ items.push(element("li", [], children));
632
+ };
633
+ for (const child of raw.children) {
634
+ if (typeof child !== "string" && child.tag === "li") {
635
+ flushLoose();
636
+ items.push(element("li", alignAttrs(child, parseStyle(child.attrs.get("style"))), withoutPlaceholder(trimFlow(cleanNodes(child.children, inner)))));
637
+ }
638
+ else if (typeof child !== "string" && (child.tag === "ul" || child.tag === "ol")) {
639
+ // A list straight inside a list (as editors make it on Tab) belongs to the item before it.
640
+ const nested = cleanList(child, context);
641
+ const last = items[items.length - 1];
642
+ if (last && !loose.some(node => typeof node !== "string" || node.trim()))
643
+ last.children.push(...nested);
644
+ else {
645
+ flushLoose();
646
+ if (nested.length)
647
+ items.push(element("li", [], nested));
648
+ }
649
+ loose = [];
650
+ }
651
+ else
652
+ loose.push(child);
653
+ }
654
+ flushLoose();
655
+ if (!items.length)
656
+ return [];
657
+ const attrs = [];
658
+ if (raw.tag === "ol") {
659
+ const start = raw.attrs.get("start")?.trim();
660
+ if (start && /^\d{1,9}$/.test(start) && Number(start) !== 1)
661
+ attrs.push(["start", String(Number(start))]);
662
+ const type = raw.attrs.get("type")?.trim();
663
+ if (type && /^[aAiI]$/.test(type))
664
+ attrs.push(["type", type]);
665
+ }
666
+ return [element(raw.tag === "ol" ? "ol" : "ul", attrs, items)];
667
+ }
668
+ function cleanTable(raw, context) {
669
+ // Content that sits in a table outside its cells shows before the table, as browsers show it.
670
+ const before = [];
671
+ const parts = [];
672
+ let caption = null;
673
+ let loose = [];
674
+ const flushLoose = () => {
675
+ const rows = loose.length ? collectRows(loose, context, before) : [];
676
+ loose = [];
677
+ if (rows.length)
678
+ parts.push(element("tbody", [], rows));
679
+ };
680
+ for (const child of raw.children) {
681
+ if (typeof child !== "string" && SECTIONS.has(child.tag)) {
682
+ flushLoose();
683
+ const rows = collectRows(child.children, context, before);
684
+ if (rows.length)
685
+ parts.push(element(child.tag, [], rows));
686
+ }
687
+ else if (typeof child !== "string" && child.tag === "caption") {
688
+ const children = trimFlow(cleanNodes(child.children, { ...context, inline: true }));
689
+ if (!caption && children.length)
690
+ caption = element("caption", [], children);
691
+ }
692
+ else
693
+ loose.push(child);
694
+ }
695
+ flushLoose();
696
+ if (!parts.length)
697
+ return before;
698
+ return [...before, element("table", [], caption ? [caption, ...parts] : parts)];
699
+ }
700
+ function collectRows(nodes, context, before) {
701
+ const rows = [];
702
+ let cells = [];
703
+ const flushCells = () => {
704
+ if (cells.length)
705
+ rows.push(element("tr", [], cells.map(cell => cleanCell(cell, context))));
706
+ cells = [];
707
+ };
708
+ for (const node of nodes) {
709
+ if (typeof node === "string") {
710
+ if (node.trim())
711
+ before.push(...wrapInlineRuns([cleanText(node, false)]));
712
+ }
713
+ else if (node.tag === "tr") {
714
+ flushCells();
715
+ const rowCells = [];
716
+ for (const child of node.children) {
717
+ if (typeof child !== "string" && CELLS.has(child.tag))
718
+ rowCells.push(cleanCell(child, context));
719
+ else if (typeof child !== "string")
720
+ before.push(...wrapInlineRuns(cleanElement(child, { ...context, inline: false })));
721
+ else if (child.trim())
722
+ before.push(...wrapInlineRuns([cleanText(child, false)]));
723
+ }
724
+ if (rowCells.length)
725
+ rows.push(element("tr", [], rowCells));
726
+ }
727
+ else if (CELLS.has(node.tag))
728
+ cells.push(node);
729
+ else if (SECTIONS.has(node.tag)) {
730
+ flushCells();
731
+ rows.push(...collectRows(node.children, context, before));
732
+ }
733
+ else
734
+ before.push(...wrapInlineRuns(cleanElement(node, { ...context, inline: false })));
735
+ }
736
+ flushCells();
737
+ return rows;
738
+ }
739
+ function cleanCell(raw, context) {
740
+ const attrs = [];
741
+ for (const name of ["colspan", "rowspan"]) {
742
+ const value = raw.attrs.get(name)?.trim();
743
+ if (value && /^\d{1,4}$/.test(value) && Number(value) > 1)
744
+ attrs.push([name, String(Math.min(Number(value), 1000))]);
745
+ }
746
+ attrs.push(...alignAttrs(raw, parseStyle(raw.attrs.get("style"))));
747
+ return element(raw.tag, attrs, withoutPlaceholder(trimFlow(cleanNodes(raw.children, { ...context, inline: false }))));
748
+ }
749
+ /** An empty cell or list item holds a line break only so the browser can put the caret there; it shows nothing. */
750
+ function withoutPlaceholder(children) {
751
+ return children.length === 1 && isBreak(children[0]) ? [] : children;
752
+ }
753
+ function alignAttrs(raw, style) {
754
+ const value = style.get("text-align") ?? raw.attrs.get("align")?.trim().toLowerCase();
755
+ return value && ALIGNMENTS.has(value) ? [["style", `text-align: ${value}`]] : [];
756
+ }
757
+ /** Inline content left at the top level, or among blocks, goes into paragraphs. */
758
+ function wrapInlineRuns(nodes) {
759
+ const out = [];
760
+ let run = [];
761
+ const flush = () => {
762
+ const trimmed = trimRun(run);
763
+ if (trimmed.length)
764
+ out.push(element("p", [], trimmed));
765
+ run = [];
766
+ };
767
+ for (const node of nodes) {
768
+ if (isBlock(node)) {
769
+ flush();
770
+ out.push(node);
771
+ }
772
+ else
773
+ run.push(node);
774
+ }
775
+ flush();
776
+ return out;
777
+ }
778
+ /** Blocks inside inline content: their content stays, each block on lines of its own. */
779
+ function flatten(nodes, pre) {
780
+ const out = [];
781
+ for (const node of nodes) {
782
+ if (!isBlock(node)) {
783
+ pushNode(out, node, pre);
784
+ continue;
785
+ }
786
+ const inner = node.tag === "hr" ? [] : flatten(node.children, pre);
787
+ if (!inner.length)
788
+ continue;
789
+ if (out.length && !isBreak(out[out.length - 1]))
790
+ out.push(element("br"));
791
+ for (const item of inner)
792
+ pushNode(out, item, pre);
793
+ out.push(element("br"));
794
+ }
795
+ while (isBreak(out[out.length - 1]))
796
+ out.pop();
797
+ return out;
798
+ }
799
+ /**
800
+ * Removes what does not show in a block's content: spaces at the start and the end of each run of text (at the
801
+ * edges of the block, and next to blocks inside it), and a single line break at the end of a run.
802
+ */
803
+ function trimFlow(nodes) {
804
+ const out = [];
805
+ let run = [];
806
+ const flush = () => {
807
+ out.push(...trimRun(run));
808
+ run = [];
809
+ };
810
+ for (const node of nodes) {
811
+ if (isBlock(node)) {
812
+ flush();
813
+ out.push(node);
814
+ }
815
+ else
816
+ run.push(node);
817
+ }
818
+ flush();
819
+ return out;
820
+ }
821
+ function trimRun(run) {
822
+ const nodes = run.slice();
823
+ trimStart(nodes);
824
+ trimEnd(nodes);
825
+ // A line break at the end of a line of text does not show.
826
+ if (isBreak(nodes[nodes.length - 1]) && nodes.length > 1 && !isBreak(nodes[nodes.length - 2])) {
827
+ nodes.pop();
828
+ trimEnd(nodes);
829
+ }
830
+ return nodes;
831
+ }
832
+ function trimStart(nodes) {
833
+ while (nodes.length) {
834
+ const first = nodes[0];
835
+ if (typeof first === "string") {
836
+ const text = first.replace(/^ +/, "");
837
+ if (text) {
838
+ nodes[0] = text;
839
+ return;
840
+ }
841
+ }
842
+ else if (INLINE_OUTPUT.has(first.tag)) {
843
+ const children = first.children.slice();
844
+ trimStart(children);
845
+ if (children.length) {
846
+ nodes[0] = { ...first, children };
847
+ return;
848
+ }
849
+ }
850
+ else
851
+ return;
852
+ nodes.shift();
853
+ }
854
+ }
855
+ function trimEnd(nodes) {
856
+ while (nodes.length) {
857
+ const index = nodes.length - 1;
858
+ const last = nodes[index];
859
+ if (typeof last === "string") {
860
+ const text = last.replace(/ +$/, "");
861
+ if (text) {
862
+ nodes[index] = text;
863
+ return;
864
+ }
865
+ }
866
+ else if (INLINE_OUTPUT.has(last.tag)) {
867
+ const children = last.children.slice();
868
+ trimEnd(children);
869
+ if (children.length) {
870
+ nodes[index] = { ...last, children };
871
+ return;
872
+ }
873
+ }
874
+ else
875
+ return;
876
+ nodes.pop();
877
+ }
878
+ }
879
+ /** A paragraph or heading with nothing but line breaks: an empty line. */
880
+ function isBlankBlock(node) {
881
+ return (node.tag === "p" || HEADINGS.has(node.tag)) && node.children.every(isBreak);
882
+ }
883
+ function hasTag(nodes, tag) {
884
+ return nodes.some(node => typeof node !== "string" && (node.tag === tag || hasTag(node.children, tag)));
885
+ }
886
+ // ---------------------------------------------------------------------------------------------------------------
887
+ // Styles and colors
888
+ function parseStyle(style) {
889
+ const map = new Map();
890
+ if (!style)
891
+ return map;
892
+ for (const part of style.split(";")) {
893
+ const colon = part.indexOf(":");
894
+ if (colon < 0)
895
+ continue;
896
+ const name = part.slice(0, colon).trim().toLowerCase();
897
+ const value = part
898
+ .slice(colon + 1)
899
+ .replace(/!\s*important/i, "")
900
+ .trim()
901
+ .toLowerCase();
902
+ // Escapes and functions other than colors are never needed, so a value with them is ignored.
903
+ if (name && value && !/[\\<>]|url\(|expression|var\(/.test(value))
904
+ map.set(name, value);
905
+ }
906
+ return map;
907
+ }
908
+ const NAMED_COLORS = {
909
+ black: "#000000", white: "#ffffff", red: "#ff0000", green: "#008000", blue: "#0000ff", yellow: "#ffff00",
910
+ orange: "#ffa500", purple: "#800080", gray: "#808080", grey: "#808080", silver: "#c0c0c0", maroon: "#800000",
911
+ navy: "#000080", teal: "#008080", olive: "#808000", lime: "#00ff00", aqua: "#00ffff", cyan: "#00ffff",
912
+ fuchsia: "#ff00ff", magenta: "#ff00ff", pink: "#ffc0cb", brown: "#a52a2a",
913
+ };
914
+ /** A CSS color as `#rrggbb`: hex, `rgb()`, `rgba()`, or a common name. Transparent and unknown colors give `null`. */
915
+ export function parseColor(value) {
916
+ const text = value.trim().toLowerCase();
917
+ const named = NAMED_COLORS[text];
918
+ if (named)
919
+ return named;
920
+ const hex = /^#([0-9a-f]{3,8})$/.exec(text)?.[1];
921
+ if (hex) {
922
+ if (hex.length === 3 || hex.length === 4) {
923
+ if (hex[3] === "0")
924
+ return null;
925
+ return `#${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}`;
926
+ }
927
+ if (hex.length === 6)
928
+ return `#${hex}`;
929
+ if (hex.length === 8)
930
+ return hex.endsWith("00") ? null : `#${hex.slice(0, 6)}`;
931
+ return null;
932
+ }
933
+ const rgb = /^rgba?\(([^)]*)\)$/.exec(text)?.[1];
934
+ if (rgb) {
935
+ const parts = rgb.split(/[\s,/]+/).filter(Boolean);
936
+ if (parts.length !== 3 && parts.length !== 4)
937
+ return null;
938
+ const channels = parts.slice(0, 3).map(part => (part.endsWith("%") ? (Number.parseFloat(part) / 100) * 255 : Number(part)));
939
+ if (channels.some(channel => !Number.isFinite(channel)))
940
+ return null;
941
+ if (parts[3] !== undefined) {
942
+ const alpha = parts[3].endsWith("%") ? Number.parseFloat(parts[3]) / 100 : Number(parts[3]);
943
+ if (!Number.isFinite(alpha) || alpha <= 0)
944
+ return null;
945
+ }
946
+ return `#${channels.map(channel => Math.round(Math.min(255, Math.max(0, channel))).toString(16).padStart(2, "0")).join("")}`;
947
+ }
948
+ return null;
949
+ }
950
+ function emit(nodes, options) {
951
+ let html = "";
952
+ for (const node of nodes) {
953
+ if (typeof node === "string") {
954
+ html += options.text ? options.text(node) : escapeText(node);
955
+ continue;
956
+ }
957
+ const [tag, attrs] = options.rename ? options.rename(node.tag, node.attrs) : [node.tag, node.attrs];
958
+ html += `<${tag}${attrText(options.attrs ? options.attrs(tag, attrs) : attrs)}>`;
959
+ if (!VOID_OUTPUT.has(tag))
960
+ html += `${emit(node.children, options)}</${tag}>`;
961
+ }
962
+ return html;
963
+ }
964
+ /** Containers whose parts go on lines of their own in the source view. */
965
+ const PRETTY_CONTAINERS = new Set(["ul", "ol", "table", "thead", "tbody", "tfoot", "tr"]);
966
+ function emitPretty(nodes, depth) {
967
+ const indent = " ".repeat(depth);
968
+ const lines = [];
969
+ let run = [];
970
+ const flush = () => {
971
+ if (run.length)
972
+ lines.push(indent + emit(run, {}));
973
+ run = [];
974
+ };
975
+ for (const node of nodes) {
976
+ if (!isBlock(node)) {
977
+ run.push(node);
978
+ continue;
979
+ }
980
+ flush();
981
+ const open = `${indent}<${node.tag}${attrText(node.attrs)}>`;
982
+ if (node.tag === "hr")
983
+ lines.push(open);
984
+ else if (PRETTY_CONTAINERS.has(node.tag) || (node.tag !== "pre" && node.children.some(isBlock))) {
985
+ lines.push(open, emitPretty(node.children, depth + 1), `${indent}</${node.tag}>`);
986
+ }
987
+ else
988
+ lines.push(`${open}${emit(node.children, {})}</${node.tag}>`);
989
+ }
990
+ flush();
991
+ return lines.join("\n");
992
+ }
993
+ /** Plain text: a block starts a line, a line break adds one, and table cells are separated by tabs. */
994
+ function writePlain(nodes, writer) {
995
+ const newLine = () => {
996
+ if (writer.text && !writer.text.endsWith("\n"))
997
+ writer.text += "\n";
998
+ };
999
+ for (const node of nodes) {
1000
+ if (typeof node === "string") {
1001
+ writer.text += node.replaceAll(NBSP, " ");
1002
+ continue;
1003
+ }
1004
+ if (node.tag === "br")
1005
+ writer.text += "\n";
1006
+ else if (CELLS.has(node.tag)) {
1007
+ if (writer.text && !writer.text.endsWith("\n"))
1008
+ writer.text += "\t";
1009
+ writePlain(node.children, writer);
1010
+ }
1011
+ else if (BLOCK_TAGS.has(node.tag)) {
1012
+ newLine();
1013
+ writePlain(node.children, writer);
1014
+ newLine();
1015
+ }
1016
+ else
1017
+ writePlain(node.children, writer);
1018
+ }
1019
+ }
1020
+ function attrText(attrs) {
1021
+ return attrs.map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`).join("");
1022
+ }
1023
+ function escapeText(text) {
1024
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replaceAll(NBSP, "&nbsp;");
1025
+ }
1026
+ function escapeAttribute(text) {
1027
+ return text.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1028
+ }
1029
+ /**
1030
+ * The editor shows highlights as styled spans, as browsers make them, so the browser's own "clear formatting" and
1031
+ * highlight commands work on them. The stored HTML has `mark` again.
1032
+ */
1033
+ function highlightAsSpan(tag, attrs) {
1034
+ if (tag !== "mark")
1035
+ return [tag, attrs];
1036
+ return ["span", attrs.length ? attrs : [["style", "background-color: #fef08a"]]];
1037
+ }
1038
+ function linkInNewTab(tag, attrs) {
1039
+ if (tag !== "a" || !/^https?:\/\//i.test(attrs[0]?.[1] ?? ""))
1040
+ return attrs;
1041
+ return [...attrs, ["target", "_blank"], ["rel", "noopener noreferrer"]];
1042
+ }
1043
+ // ---------------------------------------------------------------------------------------------------------------
1044
+ // Tokens
1045
+ /** A whole token in either format, as a chip's `data-token` holds it. */
1046
+ const TOKEN_TEXT = /^(?:\|[A-Za-z_][A-Za-z0-9_.]*\||\{\{\s*[A-Za-z_][A-Za-z0-9_.]*\s*\}\})$/;
1047
+ function tokenPattern(format) {
1048
+ return format === "|key|" ? /\|([A-Za-z_][A-Za-z0-9_.]*)\|/g : /\{\{\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\}\}/g;
1049
+ }
1050
+ function tokenChips(text, format, known) {
1051
+ let html = "";
1052
+ let last = 0;
1053
+ for (const match of text.matchAll(tokenPattern(format))) {
1054
+ const key = match[1];
1055
+ const token = known?.get(key) ?? null;
1056
+ html += escapeText(text.slice(last, match.index)) + richTextTokenChip(key, format, token, !known || Boolean(token));
1057
+ last = match.index + match[0].length;
1058
+ }
1059
+ return html + escapeText(text.slice(last));
1060
+ }
1061
+ function fillTokens(text, format, values) {
1062
+ let html = "";
1063
+ let last = 0;
1064
+ for (const match of text.matchAll(tokenPattern(format))) {
1065
+ const key = match[1];
1066
+ if (!Object.hasOwn(values, key))
1067
+ continue;
1068
+ html += escapeText(text.slice(last, match.index)) + escapeText(String(values[key] ?? ""));
1069
+ last = match.index + match[0].length;
1070
+ }
1071
+ return html + escapeText(text.slice(last));
1072
+ }