@charcuterie/ui 3.12.0 → 3.14.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 (53) hide show
  1. package/dist/Avatar/Avatar.d.ts +163 -0
  2. package/dist/Avatar/Avatar.d.ts.map +1 -0
  3. package/dist/Avatar/Avatar.js +139 -0
  4. package/dist/Avatar/Avatar.js.map +1 -0
  5. package/dist/Board/BoardLaneList.d.ts +29 -0
  6. package/dist/Board/BoardLaneList.d.ts.map +1 -1
  7. package/dist/Board/BoardLaneList.js +2 -1
  8. package/dist/Board/BoardLaneList.js.map +1 -1
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/markdownEditorCodeMirror/MarkdownView.d.ts +140 -0
  14. package/dist/markdownEditorCodeMirror/MarkdownView.d.ts.map +1 -0
  15. package/dist/markdownEditorCodeMirror/MarkdownView.js +268 -0
  16. package/dist/markdownEditorCodeMirror/MarkdownView.js.map +1 -0
  17. package/dist/markdownEditorCodeMirror/index.d.ts +14 -3
  18. package/dist/markdownEditorCodeMirror/index.d.ts.map +1 -1
  19. package/dist/markdownEditorCodeMirror/index.js +12 -3
  20. package/dist/markdownEditorCodeMirror/index.js.map +1 -1
  21. package/dist/markdownEditorCodeMirror/livePreview.d.ts +42 -2
  22. package/dist/markdownEditorCodeMirror/livePreview.d.ts.map +1 -1
  23. package/dist/markdownEditorCodeMirror/livePreview.js +243 -26
  24. package/dist/markdownEditorCodeMirror/livePreview.js.map +1 -1
  25. package/dist/markdownEditorCodeMirror/livePreviewRanges.d.ts.map +1 -1
  26. package/dist/markdownEditorCodeMirror/livePreviewRanges.js +49 -7
  27. package/dist/markdownEditorCodeMirror/livePreviewRanges.js.map +1 -1
  28. package/dist/markdownEditorCodeMirror/safeUrls.d.ts +46 -0
  29. package/dist/markdownEditorCodeMirror/safeUrls.d.ts.map +1 -0
  30. package/dist/markdownEditorCodeMirror/safeUrls.js +123 -0
  31. package/dist/markdownEditorCodeMirror/safeUrls.js.map +1 -0
  32. package/package.json +1 -1
  33. package/src/Avatar/Avatar.mdx +92 -0
  34. package/src/Avatar/Avatar.stories.tsx +254 -0
  35. package/src/Avatar/Avatar.test.tsx +143 -0
  36. package/src/Avatar/Avatar.tsx +270 -0
  37. package/src/Board/Board.mdx +26 -0
  38. package/src/Board/Board.stories.tsx +47 -21
  39. package/src/Board/Board.test.tsx +62 -0
  40. package/src/Board/BoardLaneList.tsx +52 -1
  41. package/src/MarkdownEditor/MarkdownEditor.mdx +5 -0
  42. package/src/index.ts +5 -0
  43. package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.mdx +11 -0
  44. package/src/markdownEditorCodeMirror/MarkdownView.mdx +157 -0
  45. package/src/markdownEditorCodeMirror/MarkdownView.stories.tsx +314 -0
  46. package/src/markdownEditorCodeMirror/MarkdownView.test.tsx +288 -0
  47. package/src/markdownEditorCodeMirror/MarkdownView.tsx +373 -0
  48. package/src/markdownEditorCodeMirror/index.ts +17 -2
  49. package/src/markdownEditorCodeMirror/livePreview.ts +329 -33
  50. package/src/markdownEditorCodeMirror/livePreviewRanges.ts +64 -7
  51. package/src/markdownEditorCodeMirror/safeUrls.test.ts +0 -0
  52. package/src/markdownEditorCodeMirror/safeUrls.ts +142 -0
  53. package/src/sourceRules.test.ts +9 -1
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The scheme guard, for markdown this surface did not write.
3
+ *
4
+ * `MarkdownView` renders files fetched from a git host, so its
5
+ * document is hostile input by default. Most of what that usually
6
+ * means is already answered by construction here: there is no
7
+ * `innerHTML` anywhere in this subpath, every rendered node is
8
+ * `createElement` plus `textContent`, and CodeMirror's state holds
9
+ * the source as *text* — so a `<script>` tag in a fetched README is
10
+ * literal characters on screen and never a script.
11
+ *
12
+ * One thing that construction did not cover, and it was live in the
13
+ * editor as well: **a URL is taken from the document and used as a
14
+ * URL.** `[click me](javascript:…)` conceals its markup, paints as a
15
+ * link, and `window.open` then runs the script in this origin. The
16
+ * image path is the same shape one step down — `![](…)` writes an
17
+ * `src` verbatim.
18
+ *
19
+ * So every URL crosses this module before it reaches an `href`, an
20
+ * `src` or `window.open`, in **both** components. It is a defect in
21
+ * a shape the two share, which makes it a defect in the layer they
22
+ * share.
23
+ *
24
+ * ### An allowlist, and nothing clever
25
+ *
26
+ * Denying `javascript:` is the version that has a bug. Browsers
27
+ * throw tabs and newlines away before they read a scheme, so
28
+ * `JaVaScript:`, a tab in the middle of the word, and a leading
29
+ * newline are one URL to a navigation and three different strings to
30
+ * a blocklist. This normalises the way a browser does, asks what
31
+ * scheme is left, and accepts only names it recognises. A URL with
32
+ * **no** scheme is relative and is always fine.
33
+ */
34
+
35
+ /**
36
+ * A scheme, per RFC 3986: a letter, then letters, digits, `+`, `-`
37
+ * or `.`, then the colon. Anchored, and the character class has no
38
+ * `/` in it — so `notes/2026:rack.md` is read as the relative path
39
+ * it is rather than as a `notes/2026` scheme.
40
+ */
41
+ const SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):/
42
+
43
+ /**
44
+ * `[text](<url with spaces>)` is CommonMark's escape hatch for a URL
45
+ * that contains a space, and the parser hands the angle brackets
46
+ * through as part of the `URL` node. They are not part of the URL.
47
+ */
48
+ const toBareUrl = (url: string) => url.replace(/^<|>$/g, "")
49
+
50
+ /**
51
+ * What a browser discards before it looks at a URL: the C0 control
52
+ * range and the space. Stripping all of it is deliberately *more*
53
+ * aggressive than the URL standard, which removes only tab and the
54
+ * two newline characters — erring this way can only turn a URL that
55
+ * would have been treated as relative into one that is rejected,
56
+ * which is the harmless direction.
57
+ */
58
+ const toNormalizedUrl = (url: string) =>
59
+ toBareUrl(url)
60
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: matching them is the entire point
61
+ .replace(/[\u0000-\u0020]/g, "")
62
+ .toLowerCase()
63
+
64
+ const toSchemeName = (url: string) =>
65
+ SCHEME_PATTERN.exec(toNormalizedUrl(url))?.[1]
66
+
67
+ /**
68
+ * The schemes a link may carry.
69
+ *
70
+ * `mailto:` and `tel:` are here because a README that prints a
71
+ * contact address is ordinary and neither one can execute. `file:`
72
+ * is not, because it reads the *reader's* disk.
73
+ */
74
+ const LINK_SCHEMES = new Set([
75
+ "http",
76
+ "https",
77
+ "mailto",
78
+ "tel",
79
+ ])
80
+
81
+ /** The schemes an image's `src` may carry, `data:` aside. */
82
+ const IMAGE_SCHEMES = new Set(["http", "https"])
83
+
84
+ /**
85
+ * `data:`, allowed for an image type only — which is not the hole it
86
+ * looks like.
87
+ *
88
+ * A `data:image/svg+xml` document loaded through `<img>` is a
89
+ * replaced element: no script execution, no network, no reach into
90
+ * the embedding page. That is why it is the one inline format worth
91
+ * having, and why this repo's own `Avatar` story uses it for a
92
+ * portrait that needs no fixture file. `data:text/html` is a
93
+ * different thing entirely and does not match this.
94
+ */
95
+ const DATA_IMAGE_PATTERN = /^data:image\/[a-z0-9.+-]+[,;]/
96
+
97
+ /**
98
+ * The link's URL, or `undefined` when it must not become one.
99
+ *
100
+ * A rejected URL is not rewritten and not blanked — the text stays
101
+ * exactly where the author put it and simply stops being a link.
102
+ * That is the honest failure: the reader still sees every character
103
+ * of the source, and can still read the URL itself in the
104
+ * raw-markdown view, but nothing on the page will navigate to it.
105
+ */
106
+ export const toSafeLinkUrl = (
107
+ url: string,
108
+ ): string | undefined => {
109
+ const scheme = toSchemeName(url)
110
+
111
+ if (scheme === undefined) {
112
+ // No scheme at all: a relative path, a `#fragment`, a `?query`,
113
+ // or a protocol-relative `//host/path`. None of them can name an
114
+ // executable scheme, so all of them are fine.
115
+ return toBareUrl(url)
116
+ }
117
+
118
+ return LINK_SCHEMES.has(scheme)
119
+ ? toBareUrl(url)
120
+ : undefined
121
+ }
122
+
123
+ /** The image's `src`, or `undefined` when it must not have one. */
124
+ export const toSafeImageUrl = (
125
+ url: string,
126
+ ): string | undefined => {
127
+ const scheme = toSchemeName(url)
128
+
129
+ if (scheme === undefined) {
130
+ return toBareUrl(url)
131
+ }
132
+
133
+ if (scheme === "data") {
134
+ return DATA_IMAGE_PATTERN.test(toNormalizedUrl(url))
135
+ ? toBareUrl(url)
136
+ : undefined
137
+ }
138
+
139
+ return IMAGE_SCHEMES.has(scheme)
140
+ ? toBareUrl(url)
141
+ : undefined
142
+ }
@@ -465,7 +465,15 @@ test("the barrel is the only place components are re-exported", async () => {
465
465
  // count by the `<Name>/<Name>.tsx` rule, and it IS exported from
466
466
  // the barrel as a function, because a host with its own clipboard
467
467
  // bridge needs the fallback chain without the button.
468
- expect(componentNames.length).toBe(54)
468
+ //
469
+ // `Avatar` — a person as a coloured chip, so a board stops
470
+ // printing the same name on every card — is +1 -> 55. It has no
471
+ // members: the initials derivation is a private function in
472
+ // `Avatar.tsx`, and the colour it falls back to is
473
+ // `getCategoricalIndex`, which already lives in
474
+ // `@charcuterie/tokens` because a Node service and a Satori render
475
+ // both need to answer the colour question with no React in scope.
476
+ expect(componentNames.length).toBe(55)
469
477
 
470
478
  for (const name of componentNames) {
471
479
  expect(barrel).toContain(`export { ${name} }`)