@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.
- package/dist/Avatar/Avatar.d.ts +163 -0
- package/dist/Avatar/Avatar.d.ts.map +1 -0
- package/dist/Avatar/Avatar.js +139 -0
- package/dist/Avatar/Avatar.js.map +1 -0
- package/dist/Board/BoardLaneList.d.ts +29 -0
- package/dist/Board/BoardLaneList.d.ts.map +1 -1
- package/dist/Board/BoardLaneList.js +2 -1
- package/dist/Board/BoardLaneList.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/markdownEditorCodeMirror/MarkdownView.d.ts +140 -0
- package/dist/markdownEditorCodeMirror/MarkdownView.d.ts.map +1 -0
- package/dist/markdownEditorCodeMirror/MarkdownView.js +268 -0
- package/dist/markdownEditorCodeMirror/MarkdownView.js.map +1 -0
- package/dist/markdownEditorCodeMirror/index.d.ts +14 -3
- package/dist/markdownEditorCodeMirror/index.d.ts.map +1 -1
- package/dist/markdownEditorCodeMirror/index.js +12 -3
- package/dist/markdownEditorCodeMirror/index.js.map +1 -1
- package/dist/markdownEditorCodeMirror/livePreview.d.ts +42 -2
- package/dist/markdownEditorCodeMirror/livePreview.d.ts.map +1 -1
- package/dist/markdownEditorCodeMirror/livePreview.js +243 -26
- package/dist/markdownEditorCodeMirror/livePreview.js.map +1 -1
- package/dist/markdownEditorCodeMirror/livePreviewRanges.d.ts.map +1 -1
- package/dist/markdownEditorCodeMirror/livePreviewRanges.js +49 -7
- package/dist/markdownEditorCodeMirror/livePreviewRanges.js.map +1 -1
- package/dist/markdownEditorCodeMirror/safeUrls.d.ts +46 -0
- package/dist/markdownEditorCodeMirror/safeUrls.d.ts.map +1 -0
- package/dist/markdownEditorCodeMirror/safeUrls.js +123 -0
- package/dist/markdownEditorCodeMirror/safeUrls.js.map +1 -0
- package/package.json +1 -1
- package/src/Avatar/Avatar.mdx +92 -0
- package/src/Avatar/Avatar.stories.tsx +254 -0
- package/src/Avatar/Avatar.test.tsx +143 -0
- package/src/Avatar/Avatar.tsx +270 -0
- package/src/Board/Board.mdx +26 -0
- package/src/Board/Board.stories.tsx +47 -21
- package/src/Board/Board.test.tsx +62 -0
- package/src/Board/BoardLaneList.tsx +52 -1
- package/src/MarkdownEditor/MarkdownEditor.mdx +5 -0
- package/src/index.ts +5 -0
- package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.mdx +11 -0
- package/src/markdownEditorCodeMirror/MarkdownView.mdx +157 -0
- package/src/markdownEditorCodeMirror/MarkdownView.stories.tsx +314 -0
- package/src/markdownEditorCodeMirror/MarkdownView.test.tsx +288 -0
- package/src/markdownEditorCodeMirror/MarkdownView.tsx +373 -0
- package/src/markdownEditorCodeMirror/index.ts +17 -2
- package/src/markdownEditorCodeMirror/livePreview.ts +329 -33
- package/src/markdownEditorCodeMirror/livePreviewRanges.ts +64 -7
- package/src/markdownEditorCodeMirror/safeUrls.test.ts +0 -0
- package/src/markdownEditorCodeMirror/safeUrls.ts +142 -0
- 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
|
+
}
|
package/src/sourceRules.test.ts
CHANGED
|
@@ -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
|
-
|
|
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} }`)
|