@csntgao/uni-base 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/.prettierignore +4 -0
  2. package/.prettierrc.json +6 -0
  3. package/AGENTS.md +519 -0
  4. package/CLAUDE.md +512 -0
  5. package/README.md +734 -0
  6. package/docs//346/225/260/345/255/227/345/221/230/345/267/245/345/256/214/346/225/264/346/212/275/345/261/211/346/216/245/345/205/245/350/257/264/346/230/216.md +110 -0
  7. package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/215/225/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +79 -0
  8. package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/244/232/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +93 -0
  9. package/docs//350/264/246/346/210/267/344/270/216/347/231/273/345/275/225/347/273/204/344/273/266/345/212/250/346/200/201/346/216/245/345/205/245/350/257/264/346/230/216.md +201 -0
  10. package/eslint.config.js +30 -0
  11. package/examples/vanilla/index.html +33 -0
  12. package/examples/vanilla/main.js +247 -0
  13. package/examples/vanilla/package.json +15 -0
  14. package/examples/vanilla/public/runtime-config.js +10 -0
  15. package/examples/vanilla/style.css +99 -0
  16. package/package.json +31 -0
  17. package/packages/core/package.json +30 -0
  18. package/packages/core/src/client.ts +1132 -0
  19. package/packages/core/src/customer-support-client.ts +825 -0
  20. package/packages/core/src/customer-support-types.ts +123 -0
  21. package/packages/core/src/index.ts +79 -0
  22. package/packages/core/src/notification-center-client.ts +462 -0
  23. package/packages/core/src/notification-center-types.ts +154 -0
  24. package/packages/core/src/protocol.ts +237 -0
  25. package/packages/core/src/types.ts +170 -0
  26. package/packages/core/tests/client.test.ts +760 -0
  27. package/packages/core/tests/customer-support-client.test.ts +576 -0
  28. package/packages/core/tests/fake-websocket.ts +55 -0
  29. package/packages/core/tests/notification-center-client.test.ts +467 -0
  30. package/packages/core/tests/protocol.test.ts +124 -0
  31. package/packages/core/tsconfig.json +7 -0
  32. package/packages/core/tsup.config.ts +11 -0
  33. package/packages/web-components/package.json +35 -0
  34. package/packages/web-components/scripts/verify-runtime-build.mjs +111 -0
  35. package/packages/web-components/src/account-context.ts +1540 -0
  36. package/packages/web-components/src/application-switcher-transport.ts +243 -0
  37. package/packages/web-components/src/application-switcher.ts +1103 -0
  38. package/packages/web-components/src/chat.ts +1754 -0
  39. package/packages/web-components/src/customer-support-chat.ts +862 -0
  40. package/packages/web-components/src/customer-support-transport.ts +269 -0
  41. package/packages/web-components/src/index.ts +256 -0
  42. package/packages/web-components/src/notification-center.ts +1066 -0
  43. package/packages/web-components/src/notification-popover.ts +615 -0
  44. package/packages/web-components/src/organization-multi-employee-selector.ts +1564 -0
  45. package/packages/web-components/src/organization-onboarding-transport.ts +54 -0
  46. package/packages/web-components/src/organization-onboarding.ts +1216 -0
  47. package/packages/web-components/src/organization-selector.ts +534 -0
  48. package/packages/web-components/src/organization-single-employee-selector.ts +114 -0
  49. package/packages/web-components/src/prototype-icons.ts +242 -0
  50. package/packages/web-components/src/safe-image-url.ts +29 -0
  51. package/packages/web-components/src/safe-markdown.ts +192 -0
  52. package/packages/web-components/src/uniplat-base-account-context-transport.ts +625 -0
  53. package/packages/web-components/src/uniplat-base-application-switcher-transport.ts +418 -0
  54. package/packages/web-components/src/uniplat-base-application-ticket.ts +254 -0
  55. package/packages/web-components/src/uniplat-base-identity-ticket.ts +202 -0
  56. package/packages/web-components/src/uniplat-base-notification-center-transport.ts +378 -0
  57. package/packages/web-components/src/uniplat-base-organization-multi-employee-selector-transport.ts +394 -0
  58. package/packages/web-components/src/uniplat-base-organization-onboarding-transport.ts +183 -0
  59. package/packages/web-components/src/uniplat-base-organization-single-employee-selector-transport.ts +14 -0
  60. package/packages/web-components/src/user-login-transport.ts +563 -0
  61. package/packages/web-components/src/user-login.ts +2027 -0
  62. package/packages/web-components/src/user-menu.ts +880 -0
  63. package/packages/web-components/tests/account-context.test.ts +798 -0
  64. package/packages/web-components/tests/application-switcher.test.ts +562 -0
  65. package/packages/web-components/tests/chat.test.ts +1130 -0
  66. package/packages/web-components/tests/customer-support-chat.test.ts +428 -0
  67. package/packages/web-components/tests/customer-support-transport.test.ts +79 -0
  68. package/packages/web-components/tests/notification-center-transport.test.ts +271 -0
  69. package/packages/web-components/tests/notification-center.test.ts +429 -0
  70. package/packages/web-components/tests/notification-popover.test.ts +283 -0
  71. package/packages/web-components/tests/organization-multi-employee-selector-transport.test.ts +332 -0
  72. package/packages/web-components/tests/organization-multi-employee-selector.test.ts +225 -0
  73. package/packages/web-components/tests/organization-onboarding.test.ts +260 -0
  74. package/packages/web-components/tests/organization-selector.test.ts +127 -0
  75. package/packages/web-components/tests/organization-single-employee-selector.test.ts +145 -0
  76. package/packages/web-components/tests/uniplat-base-account-context-transport.test.ts +496 -0
  77. package/packages/web-components/tests/uniplat-base-application-switcher-transport.test.ts +598 -0
  78. package/packages/web-components/tests/uniplat-base-application-ticket.test.ts +233 -0
  79. package/packages/web-components/tests/uniplat-base-identity-ticket.test.ts +226 -0
  80. package/packages/web-components/tests/uniplat-base-organization-onboarding-transport.test.ts +227 -0
  81. package/packages/web-components/tests/user-login-transport.test.ts +443 -0
  82. package/packages/web-components/tests/user-login.test.ts +626 -0
  83. package/packages/web-components/tests/user-menu.test.ts +271 -0
  84. package/packages/web-components/tsconfig.json +8 -0
  85. package/packages/web-components/tsup.config.ts +12 -0
  86. package/packages/web-components/tsup.runtime.config.ts +37 -0
  87. package/pnpm-workspace.yaml +6 -0
  88. package/tsconfig.base.json +21 -0
  89. package/vitest.config.ts +20 -0
  90. package//344/272/244/346/216/245/346/226/207/346/241/243.md +254 -0
@@ -0,0 +1,242 @@
1
+ import { html, svg, type TemplateResult } from 'lit'
2
+
3
+ /**
4
+ * Exact SVG geometry from the Tabler Icons 2.47.0 assets used by the 40.1 prototype.
5
+ * Source: https://github.com/tabler/tabler-icons/tree/v2.47.0/icons
6
+ */
7
+ export type PrototypeIconName =
8
+ | 'alert-circle'
9
+ | 'alert-triangle'
10
+ | 'arrow-left'
11
+ | 'arrow-right'
12
+ | 'bell'
13
+ | 'building'
14
+ | 'chevron-down'
15
+ | 'chevron-right'
16
+ | 'circle-check-filled'
17
+ | 'clock-exclamation'
18
+ | 'clock-hour-4'
19
+ | 'clipboard-check'
20
+ | 'coin'
21
+ | 'discount-check'
22
+ | 'discount-check-filled'
23
+ | 'headset'
24
+ | 'id-badge-2'
25
+ | 'layout-grid'
26
+ | 'logout'
27
+ | 'messages'
28
+ | 'mail-check'
29
+ | 'message-2'
30
+ | 'plus'
31
+ | 'robot'
32
+ | 'selector'
33
+ | 'speakerphone'
34
+ | 'user-cog'
35
+ | 'users'
36
+ | 'x'
37
+
38
+ export function prototypeIcon(name: PrototypeIconName): TemplateResult {
39
+ const content = prototypeIconContent(name)
40
+ return html`<svg
41
+ viewBox="0 0 24 24"
42
+ stroke-width="2"
43
+ stroke="currentColor"
44
+ fill="none"
45
+ stroke-linecap="round"
46
+ stroke-linejoin="round"
47
+ aria-hidden="true"
48
+ data-prototype-icon=${name}
49
+ >
50
+ ${content}
51
+ </svg>`
52
+ }
53
+
54
+ function prototypeIconContent(name: PrototypeIconName): TemplateResult {
55
+ if (name === 'layout-grid') {
56
+ return svg`
57
+ <path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" />
58
+ <path d="M14 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" />
59
+ <path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" />
60
+ <path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" />
61
+ `
62
+ }
63
+ if (name === 'headset') {
64
+ return svg`
65
+ <path d="M4 14v-3a8 8 0 1 1 16 0v3" />
66
+ <path d="M18 19c0 1.657 -2.686 3 -6 3" />
67
+ <path d="M4 14a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-1a2 2 0 0 1 -2 -2v-3z" />
68
+ <path d="M15 14a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-1a2 2 0 0 1 -2 -2v-3z" />
69
+ `
70
+ }
71
+ if (name === 'robot') {
72
+ return svg`
73
+ <path d="M6 4m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z" />
74
+ <path d="M12 2v2" />
75
+ <path d="M9 12v9" />
76
+ <path d="M15 12v9" />
77
+ <path d="M5 16l4 -2" />
78
+ <path d="M15 14l4 2" />
79
+ <path d="M9 18h6" />
80
+ <path d="M10 8v.01" />
81
+ <path d="M14 8v.01" />
82
+ `
83
+ }
84
+ if (name === 'messages') {
85
+ return svg`
86
+ <path d="M21 14l-3 -3h-7a1 1 0 0 1 -1 -1v-6a1 1 0 0 1 1 -1h9a1 1 0 0 1 1 1v10" />
87
+ <path d="M14 15v2a1 1 0 0 1 -1 1h-7l-3 3v-10a1 1 0 0 1 1 -1h2" />
88
+ `
89
+ }
90
+ if (name === 'message-2') {
91
+ return svg`
92
+ <path d="M8 9h8" />
93
+ <path d="M8 13h6" />
94
+ <path d="M9 18h-3a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-3l-3 3l-3 -3z" />
95
+ `
96
+ }
97
+ if (name === 'arrow-left') {
98
+ return svg`<path d="M5 12l14 0" /><path d="M5 12l6 6" /><path d="M5 12l6 -6" />`
99
+ }
100
+ if (name === 'arrow-right') {
101
+ return svg`<path d="M5 12l14 0" /><path d="M13 18l6 -6" /><path d="M13 6l6 6" />`
102
+ }
103
+ if (name === 'bell') {
104
+ return svg`
105
+ <path d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6" />
106
+ <path d="M9 17v1a3 3 0 0 0 6 0v-1" />
107
+ `
108
+ }
109
+ if (name === 'chevron-down') return svg`<path d="M6 9l6 6l6 -6" />`
110
+ if (name === 'chevron-right') return svg`<path d="M9 6l6 6l-6 6" />`
111
+ if (name === 'x') return svg`<path d="M18 6l-12 12" /><path d="M6 6l12 12" />`
112
+ if (name === 'selector') {
113
+ return svg`<path d="M8 9l4 -4l4 4" /><path d="M16 15l-4 4l-4 -4" />`
114
+ }
115
+ if (name === 'plus') return svg`<path d="M12 5l0 14" /><path d="M5 12l14 0" />`
116
+ if (name === 'logout') {
117
+ return svg`
118
+ <path d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2" />
119
+ <path d="M9 12h12l-3 -3" />
120
+ <path d="M18 15l3 -3" />
121
+ `
122
+ }
123
+ if (name === 'users') {
124
+ return svg`
125
+ <path d="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" />
126
+ <path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
127
+ <path d="M16 3.13a4 4 0 0 1 0 7.75" />
128
+ <path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
129
+ `
130
+ }
131
+ if (name === 'building') {
132
+ return svg`
133
+ <path d="M3 21l18 0" />
134
+ <path d="M9 8l1 0" />
135
+ <path d="M9 12l1 0" />
136
+ <path d="M9 16l1 0" />
137
+ <path d="M14 8l1 0" />
138
+ <path d="M14 12l1 0" />
139
+ <path d="M14 16l1 0" />
140
+ <path d="M5 21v-16a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v16" />
141
+ `
142
+ }
143
+ if (name === 'clipboard-check') {
144
+ return svg`
145
+ <path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2" />
146
+ <path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z" />
147
+ <path d="M9 14l2 2l4 -4" />
148
+ `
149
+ }
150
+ if (name === 'coin') {
151
+ return svg`
152
+ <path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
153
+ <path d="M14.8 9a2 2 0 0 0 -1.8 -1h-2a2 2 0 1 0 0 4h2a2 2 0 1 1 0 4h-2a2 2 0 0 1 -1.8 -1" />
154
+ <path d="M12 7v10" />
155
+ `
156
+ }
157
+ if (name === 'discount-check') {
158
+ return svg`
159
+ <path d="M5 7.2a2.2 2.2 0 0 1 2.2 -2.2h1a2.2 2.2 0 0 0 1.55 -.64l.7 -.7a2.2 2.2 0 0 1 3.12 0l.7 .7c.412 .41 .97 .64 1.55 .64h1a2.2 2.2 0 0 1 2.2 2.2v1c0 .58 .23 1.138 .64 1.55l.7 .7a2.2 2.2 0 0 1 0 3.12l-.7 .7a2.2 2.2 0 0 0 -.64 1.55v1a2.2 2.2 0 0 1 -2.2 2.2h-1a2.2 2.2 0 0 0 -1.55 .64l-.7 .7a2.2 2.2 0 0 1 -3.12 0l-.7 -.7a2.2 2.2 0 0 0 -1.55 -.64h-1a2.2 2.2 0 0 1 -2.2 -2.2v-1a2.2 2.2 0 0 0 -.64 -1.55l-.7 -.7a2.2 2.2 0 0 1 0 -3.12l.7 -.7a2.2 2.2 0 0 0 .64 -1.55v-1" />
160
+ <path d="M9 12l2 2l4 -4" />
161
+ `
162
+ }
163
+ if (name === 'mail-check') {
164
+ return svg`
165
+ <path d="M11 19h-6a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v6" />
166
+ <path d="M3 7l9 6l9 -6" />
167
+ <path d="M15 19l2 2l4 -4" />
168
+ `
169
+ }
170
+ if (name === 'speakerphone') {
171
+ return svg`
172
+ <path d="M18 8a3 3 0 0 1 0 6" />
173
+ <path d="M10 8v11a1 1 0 0 1 -1 1h-1a1 1 0 0 1 -1 -1v-5" />
174
+ <path d="M12 8h0l4.524 -3.77a.9 .9 0 0 1 1.476 .692v12.156a.9 .9 0 0 1 -1.476 .692l-4.524 -3.77h-8a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1h8" />
175
+ `
176
+ }
177
+ if (name === 'user-cog') {
178
+ return svg`
179
+ <path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" />
180
+ <path d="M6 21v-2a4 4 0 0 1 4 -4h2.5" />
181
+ <path d="M19.001 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
182
+ <path d="M19.001 15.5v1.5" />
183
+ <path d="M19.001 21v1.5" />
184
+ <path d="M22.032 17.25l-1.299 .75" />
185
+ <path d="M17.27 20l-1.3 .75" />
186
+ <path d="M15.97 17.25l1.3 .75" />
187
+ <path d="M20.733 20l1.3 .75" />
188
+ `
189
+ }
190
+ if (name === 'id-badge-2') {
191
+ return svg`
192
+ <path d="M7 12h3v4h-3z" />
193
+ <path d="M10 6h-6a1 1 0 0 0 -1 1v12a1 1 0 0 0 1 1h16a1 1 0 0 0 1 -1v-12a1 1 0 0 0 -1 -1h-6" />
194
+ <path d="M10 3m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" />
195
+ <path d="M14 16h2" />
196
+ <path d="M14 12h4" />
197
+ `
198
+ }
199
+ if (name === 'alert-circle') {
200
+ return svg`
201
+ <path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
202
+ <path d="M12 8v4" />
203
+ <path d="M12 16h.01" />
204
+ `
205
+ }
206
+ if (name === 'clock-hour-4') {
207
+ return svg`
208
+ <path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
209
+ <path d="M12 12l3 2" />
210
+ <path d="M12 7v5" />
211
+ `
212
+ }
213
+ if (name === 'alert-triangle') {
214
+ return svg`
215
+ <path d="M12 9v4" />
216
+ <path
217
+ d="M10.363 3.591l-8.106 13.534a1.914 1.914 0 0 0 1.636 2.871h16.214a1.914 1.914 0 0 0 1.636 -2.87l-8.106 -13.536a1.914 1.914 0 0 0 -3.274 0z"
218
+ />
219
+ <path d="M12 16h.01" />
220
+ `
221
+ }
222
+ if (name === 'clock-exclamation') {
223
+ return svg`
224
+ <path d="M20.986 12.502a9 9 0 1 0 -5.973 7.98" />
225
+ <path d="M12 7v5l3 3" />
226
+ <path d="M19 16v3" />
227
+ <path d="M19 22v.01" />
228
+ `
229
+ }
230
+ if (name === 'circle-check-filled') {
231
+ return svg`<path
232
+ d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z"
233
+ stroke-width="0"
234
+ fill="currentColor"
235
+ />`
236
+ }
237
+ return svg`<path
238
+ d="M12.01 2.011a3.2 3.2 0 0 1 2.113 .797l.154 .145l.698 .698a1.2 1.2 0 0 0 .71 .341l.135 .008h1a3.2 3.2 0 0 1 3.195 3.018l.005 .182v1c0 .27 .092 .533 .258 .743l.09 .1l.697 .698a3.2 3.2 0 0 1 .147 4.382l-.145 .154l-.698 .698a1.2 1.2 0 0 0 -.341 .71l-.008 .135v1a3.2 3.2 0 0 1 -3.018 3.195l-.182 .005h-1a1.2 1.2 0 0 0 -.743 .258l-.1 .09l-.698 .697a3.2 3.2 0 0 1 -4.382 .147l-.154 -.145l-.698 -.698a1.2 1.2 0 0 0 -.71 -.341l-.135 -.008h-1a3.2 3.2 0 0 1 -3.195 -3.018l-.005 -.182v-1a1.2 1.2 0 0 0 -.258 -.743l-.09 -.1l-.697 -.698a3.2 3.2 0 0 1 -.147 -4.382l.145 -.154l.698 -.698a1.2 1.2 0 0 0 .341 -.71l.008 -.135v-1l.005 -.182a3.2 3.2 0 0 1 3.013 -3.013l.182 -.005h1a1.2 1.2 0 0 0 .743 -.258l.1 -.09l.698 -.697a3.2 3.2 0 0 1 2.269 -.944zm3.697 7.282a1 1 0 0 0 -1.414 0l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.32 1.497l2 2l.094 .083a1 1 0 0 0 1.32 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z"
239
+ stroke-width="0"
240
+ fill="currentColor"
241
+ />`
242
+ }
@@ -0,0 +1,29 @@
1
+ export function normalizeSafeImageUrl(value: unknown, label: string): string {
2
+ if (value === undefined || value === null) return ''
3
+ if (typeof value !== 'string') throw new TypeError(`${label} must be a string`)
4
+ const normalized = value.trim()
5
+ if (!normalized) return ''
6
+ if (normalized.length > 2_048) throw new TypeError(`${label} is too long`)
7
+
8
+ let url: URL
9
+ try {
10
+ const baseUrl = typeof document === 'undefined' ? 'https://localhost/' : document.baseURI
11
+ url = new URL(normalized, baseUrl)
12
+ } catch {
13
+ throw new TypeError(`${label} is invalid`)
14
+ }
15
+
16
+ if (!['http:', 'https:'].includes(url.protocol)) {
17
+ throw new TypeError(`${label} must use HTTP or HTTPS`)
18
+ }
19
+ if (url.username || url.password || url.hash) {
20
+ throw new TypeError(`${label} cannot contain credentials or a fragment`)
21
+ }
22
+ for (const key of url.searchParams.keys()) {
23
+ const normalizedKey = key.toLowerCase().replace(/[^a-z0-9]/g, '')
24
+ if (/(?:authorization|jwt|ticket|token)/.test(normalizedKey)) {
25
+ throw new TypeError(`${label} cannot contain authentication parameters`)
26
+ }
27
+ }
28
+ return url.href
29
+ }
@@ -0,0 +1,192 @@
1
+ import { html, type TemplateResult } from 'lit'
2
+
3
+ // Allow-listed Markdown renderer shared by server-authored component content.
4
+ // Source text is always passed through Lit interpolation; only the fixed tags
5
+ // below are produced. Raw HTML and unsafe link schemes therefore stay inert.
6
+
7
+ type InlineNode = TemplateResult | string
8
+
9
+ const INLINE_PATTERN =
10
+ /(`([^`]+)`)|(\*\*([^*]+)\*\*)|(__([^_]+)__)|(~~([^~]+)~~)|(\*([^*]+)\*)|(_([^_]+)_)|(\[([^\]]+)\]\(([^)\s]+)\))/
11
+
12
+ export function renderSafeMarkdown(source: string): TemplateResult[] {
13
+ const lines = String(source ?? '')
14
+ .replace(/\r\n?/g, '\n')
15
+ .split('\n')
16
+ const blocks: TemplateResult[] = []
17
+ let index = 0
18
+
19
+ while (index < lines.length) {
20
+ const line = lines[index] ?? ''
21
+ if (line.trim() === '') {
22
+ index += 1
23
+ continue
24
+ }
25
+
26
+ if (/^```/.test(line.trim())) {
27
+ const code: string[] = []
28
+ index += 1
29
+ while (index < lines.length && !/^```/.test((lines[index] ?? '').trim())) {
30
+ code.push(lines[index] ?? '')
31
+ index += 1
32
+ }
33
+ if (index < lines.length) index += 1
34
+ blocks.push(html`<pre class="md-pre"><code>${code.join('\n')}</code></pre>`)
35
+ continue
36
+ }
37
+
38
+ const heading = /^(#{1,6})\s+(.+)$/.exec(line.trim())
39
+ if (heading) {
40
+ const content = renderInline(heading[2] ?? '')
41
+ switch (heading[1]?.length) {
42
+ case 1:
43
+ blocks.push(html`<h1 class="md-heading md-h1">${content}</h1>`)
44
+ break
45
+ case 2:
46
+ blocks.push(html`<h2 class="md-heading md-h2">${content}</h2>`)
47
+ break
48
+ case 3:
49
+ blocks.push(html`<h3 class="md-heading md-h3">${content}</h3>`)
50
+ break
51
+ default:
52
+ blocks.push(html`<h4 class="md-heading md-h4">${content}</h4>`)
53
+ }
54
+ index += 1
55
+ continue
56
+ }
57
+
58
+ if (/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(line)) {
59
+ blocks.push(html`<hr class="md-hr" />`)
60
+ index += 1
61
+ continue
62
+ }
63
+
64
+ if (/^\s*>\s?/.test(line)) {
65
+ const quoted: string[] = []
66
+ while (index < lines.length && /^\s*>\s?/.test(lines[index] ?? '')) {
67
+ quoted.push((lines[index] ?? '').replace(/^\s*>\s?/, ''))
68
+ index += 1
69
+ }
70
+ blocks.push(html`<blockquote class="md-quote">${renderLines(quoted)}</blockquote>`)
71
+ continue
72
+ }
73
+
74
+ if (isUnorderedItem(line)) {
75
+ const items: string[] = []
76
+ while (index < lines.length && isUnorderedItem(lines[index] ?? '')) {
77
+ items.push((lines[index] ?? '').replace(/^\s*[-*+]\s+/, ''))
78
+ index += 1
79
+ }
80
+ blocks.push(
81
+ html`<ul class="md-list md-ul">
82
+ ${items.map((item) => html`<li>${renderInline(item)}</li>`)}
83
+ </ul>`,
84
+ )
85
+ continue
86
+ }
87
+
88
+ if (isOrderedItem(line)) {
89
+ const items: string[] = []
90
+ while (index < lines.length && isOrderedItem(lines[index] ?? '')) {
91
+ items.push((lines[index] ?? '').replace(/^\s*\d+[.)]\s+/, ''))
92
+ index += 1
93
+ }
94
+ blocks.push(
95
+ html`<ol class="md-list md-ol">
96
+ ${items.map((item) => html`<li>${renderInline(item)}</li>`)}
97
+ </ol>`,
98
+ )
99
+ continue
100
+ }
101
+
102
+ const paragraph: string[] = []
103
+ while (index < lines.length) {
104
+ const current = lines[index] ?? ''
105
+ if (current.trim() === '' || isBlockStart(current)) break
106
+ paragraph.push(current)
107
+ index += 1
108
+ }
109
+ blocks.push(html`<p class="md-p">${renderLines(paragraph)}</p>`)
110
+ }
111
+
112
+ return blocks
113
+ }
114
+
115
+ function isBlockStart(line: string): boolean {
116
+ const trimmed = line.trim()
117
+ return (
118
+ /^```/.test(trimmed) ||
119
+ /^#{1,6}\s+/.test(trimmed) ||
120
+ /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(line) ||
121
+ /^\s*>\s?/.test(line) ||
122
+ isUnorderedItem(line) ||
123
+ isOrderedItem(line)
124
+ )
125
+ }
126
+
127
+ function isUnorderedItem(line: string): boolean {
128
+ return /^\s*[-*+]\s+/.test(line)
129
+ }
130
+
131
+ function isOrderedItem(line: string): boolean {
132
+ return /^\s*\d+[.)]\s+/.test(line)
133
+ }
134
+
135
+ function renderLines(lines: readonly string[]): InlineNode[] {
136
+ const nodes: InlineNode[] = []
137
+ lines.forEach((line, index) => {
138
+ if (index > 0) nodes.push(html`<br />`)
139
+ nodes.push(...renderInline(line))
140
+ })
141
+ return nodes
142
+ }
143
+
144
+ function renderInline(text: string): InlineNode[] {
145
+ const nodes: InlineNode[] = []
146
+ let rest = text
147
+ while (rest) {
148
+ const match = INLINE_PATTERN.exec(rest)
149
+ if (!match || match.index === undefined) {
150
+ nodes.push(rest)
151
+ break
152
+ }
153
+ if (match.index > 0) nodes.push(rest.slice(0, match.index))
154
+ if (match[1]) {
155
+ nodes.push(html`<code class="md-code">${match[2] ?? ''}</code>`)
156
+ } else if (match[3]) {
157
+ nodes.push(html`<strong>${renderInline(match[4] ?? '')}</strong>`)
158
+ } else if (match[5]) {
159
+ nodes.push(html`<strong>${renderInline(match[6] ?? '')}</strong>`)
160
+ } else if (match[7]) {
161
+ nodes.push(html`<del>${renderInline(match[8] ?? '')}</del>`)
162
+ } else if (match[9]) {
163
+ nodes.push(html`<em>${renderInline(match[10] ?? '')}</em>`)
164
+ } else if (match[11]) {
165
+ nodes.push(html`<em>${renderInline(match[12] ?? '')}</em>`)
166
+ } else if (match[13]) {
167
+ const url = safeLinkUrl(match[15] ?? '')
168
+ const label = renderInline(match[14] ?? '')
169
+ nodes.push(
170
+ url
171
+ ? html`<a href=${url} target="_blank" rel="noopener noreferrer">${label}</a>`
172
+ : (match[13] ?? ''),
173
+ )
174
+ }
175
+ rest = rest.slice(match.index + match[0].length)
176
+ }
177
+ return nodes
178
+ }
179
+
180
+ function safeLinkUrl(raw: string): string {
181
+ const value = raw.trim()
182
+ if (!value) return ''
183
+ if (/^mailto:[^\s]+@[^\s]+$/i.test(value)) return value
184
+ try {
185
+ const base = typeof document === 'undefined' ? 'https://localhost/' : document.baseURI
186
+ const url = new URL(value, base)
187
+ if (['http:', 'https:'].includes(url.protocol)) return url.href
188
+ } catch {
189
+ // Unsafe or malformed links are rendered as their original Markdown text.
190
+ }
191
+ return ''
192
+ }