@charcuterie/ui 3.16.0 → 3.18.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/Combobox/Combobox.d.ts.map +1 -1
- package/dist/Combobox/Combobox.js +92 -50
- package/dist/Combobox/Combobox.js.map +1 -1
- package/dist/MarkdownEditor/MarkdownEditor.d.ts +7 -1
- package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -1
- package/dist/MarkdownEditor/MarkdownEditor.js +1 -1
- package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -1
- package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.d.ts.map +1 -1
- package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.js +1 -1
- package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.js.map +1 -1
- package/dist/markdownEditorCodeMirror/MarkdownView.d.ts +24 -1
- package/dist/markdownEditorCodeMirror/MarkdownView.d.ts.map +1 -1
- package/dist/markdownEditorCodeMirror/MarkdownView.js +14 -2
- package/dist/markdownEditorCodeMirror/MarkdownView.js.map +1 -1
- package/dist/markdownEditorCodeMirror/livePreview.d.ts +7 -0
- package/dist/markdownEditorCodeMirror/livePreview.d.ts.map +1 -1
- package/dist/markdownEditorCodeMirror/livePreview.js +6 -2
- package/dist/markdownEditorCodeMirror/livePreview.js.map +1 -1
- package/dist/markdownEditorCodeMirror/livePreviewRanges.d.ts +22 -2
- package/dist/markdownEditorCodeMirror/livePreviewRanges.d.ts.map +1 -1
- package/dist/markdownEditorCodeMirror/livePreviewRanges.js +26 -7
- package/dist/markdownEditorCodeMirror/livePreviewRanges.js.map +1 -1
- package/package.json +2 -2
- package/src/Combobox/Combobox.test.tsx +89 -0
- package/src/Combobox/Combobox.tsx +100 -59
- package/src/MarkdownEditor/MarkdownEditor.mdx +12 -0
- package/src/MarkdownEditor/MarkdownEditor.stories.tsx +17 -0
- package/src/MarkdownEditor/MarkdownEditor.tsx +8 -0
- package/src/icons.storyHelpers.tsx +108 -0
- package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.mdx +16 -0
- package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.stories.tsx +55 -0
- package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.tsx +1 -0
- package/src/markdownEditorCodeMirror/MarkdownView.tsx +40 -1
- package/src/markdownEditorCodeMirror/livePreview.ts +18 -3
- package/src/markdownEditorCodeMirror/livePreviewRanges.test.ts +137 -0
- package/src/markdownEditorCodeMirror/livePreviewRanges.ts +50 -6
|
@@ -1003,3 +1003,140 @@ describe("toLivePreviewRanges", () => {
|
|
|
1003
1003
|
})
|
|
1004
1004
|
})
|
|
1005
1005
|
})
|
|
1006
|
+
|
|
1007
|
+
describe("resolveUrl", () => {
|
|
1008
|
+
const toResolved = (
|
|
1009
|
+
text: string,
|
|
1010
|
+
resolveUrl: (url: string) => string | undefined,
|
|
1011
|
+
) =>
|
|
1012
|
+
toLivePreviewRanges({
|
|
1013
|
+
isRawMode: false,
|
|
1014
|
+
resolveUrl,
|
|
1015
|
+
selections: [],
|
|
1016
|
+
text,
|
|
1017
|
+
tree: markdownLanguage.parser.parse(text),
|
|
1018
|
+
})
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* The case the option exists for: a relative path in a document
|
|
1022
|
+
* the app fetched from somewhere else. The browser resolves it
|
|
1023
|
+
* against the page the reader is ON, which is the app, so it
|
|
1024
|
+
* lands on nothing. Only the app knows what the path means.
|
|
1025
|
+
*/
|
|
1026
|
+
test("re-points a link the consumer claims", () => {
|
|
1027
|
+
const text = "see [the runbook](../docs/runbook.md)"
|
|
1028
|
+
|
|
1029
|
+
const linkText = toRanges(text).find(
|
|
1030
|
+
(range) =>
|
|
1031
|
+
range.type === "mark" &&
|
|
1032
|
+
range.markKind === "linkText",
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
expect(linkText).toMatchObject({
|
|
1036
|
+
url: "../docs/runbook.md",
|
|
1037
|
+
})
|
|
1038
|
+
|
|
1039
|
+
const resolved = toResolved(text, (url) =>
|
|
1040
|
+
url.startsWith("../")
|
|
1041
|
+
? `/files?path=${url.slice(3)}`
|
|
1042
|
+
: undefined,
|
|
1043
|
+
).find(
|
|
1044
|
+
(range) =>
|
|
1045
|
+
range.type === "mark" &&
|
|
1046
|
+
range.markKind === "linkText",
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
expect(resolved).toMatchObject({
|
|
1050
|
+
url: "/files?path=docs/runbook.md",
|
|
1051
|
+
})
|
|
1052
|
+
})
|
|
1053
|
+
|
|
1054
|
+
test("leaves a link alone when the consumer returns undefined", () => {
|
|
1055
|
+
const text = "see [the docs](https://example.invalid/a)"
|
|
1056
|
+
|
|
1057
|
+
expect(
|
|
1058
|
+
toResolved(text, () => undefined).find(
|
|
1059
|
+
(range) =>
|
|
1060
|
+
range.type === "mark" &&
|
|
1061
|
+
range.markKind === "linkText",
|
|
1062
|
+
),
|
|
1063
|
+
).toMatchObject({ url: "https://example.invalid/a" })
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
test("re-points a bare URL too", () => {
|
|
1067
|
+
const text = "see https://example.invalid/a for more"
|
|
1068
|
+
|
|
1069
|
+
expect(
|
|
1070
|
+
toResolved(text, () => "/elsewhere").find(
|
|
1071
|
+
(range) =>
|
|
1072
|
+
range.type === "mark" &&
|
|
1073
|
+
range.markKind === "autolink",
|
|
1074
|
+
),
|
|
1075
|
+
).toMatchObject({ url: "/elsewhere" })
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* The guard runs FIRST, so a refused URL never reaches the
|
|
1080
|
+
* consumer — it is not a link at all by then, and offering an app
|
|
1081
|
+
* the chance to resurrect one would undo `safeUrls.ts`.
|
|
1082
|
+
*/
|
|
1083
|
+
test("never offers a URL the scheme guard refused", () => {
|
|
1084
|
+
const seen: string[] = []
|
|
1085
|
+
|
|
1086
|
+
const text = "[click me](javascript:alert(1))"
|
|
1087
|
+
|
|
1088
|
+
toResolved(text, (url) => {
|
|
1089
|
+
seen.push(url)
|
|
1090
|
+
|
|
1091
|
+
return undefined
|
|
1092
|
+
})
|
|
1093
|
+
|
|
1094
|
+
expect(seen).toEqual([])
|
|
1095
|
+
})
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* An image is deliberately not offered. An app that maps a
|
|
1099
|
+
* document path to a PAGE url would turn a working image into a
|
|
1100
|
+
* broken one, and the image's `src` is the one URL here that has
|
|
1101
|
+
* to stay fetchable.
|
|
1102
|
+
*/
|
|
1103
|
+
test("does not offer an image's src", () => {
|
|
1104
|
+
const seen: string[] = []
|
|
1105
|
+
|
|
1106
|
+
const text = ""
|
|
1107
|
+
|
|
1108
|
+
const image = toResolved(text, (url) => {
|
|
1109
|
+
seen.push(url)
|
|
1110
|
+
|
|
1111
|
+
return "/elsewhere.png"
|
|
1112
|
+
}).find((range) => range.type === "image")
|
|
1113
|
+
|
|
1114
|
+
expect(seen).toEqual([])
|
|
1115
|
+
|
|
1116
|
+
expect(image).toMatchObject({
|
|
1117
|
+
url: "../docs/a.png",
|
|
1118
|
+
})
|
|
1119
|
+
})
|
|
1120
|
+
|
|
1121
|
+
test("re-points a link inside a table cell", () => {
|
|
1122
|
+
const text = [
|
|
1123
|
+
"| Port | Notes |",
|
|
1124
|
+
"| --- | --- |",
|
|
1125
|
+
"| 1 | see [the runbook](../docs/runbook.md) |",
|
|
1126
|
+
].join("\n")
|
|
1127
|
+
|
|
1128
|
+
const cell = toLivePreviewTableRanges({
|
|
1129
|
+
isRawMode: false,
|
|
1130
|
+
resolveUrl: () => "/files?path=docs/runbook.md",
|
|
1131
|
+
selections: [],
|
|
1132
|
+
text,
|
|
1133
|
+
tree: markdownLanguage.parser.parse(text),
|
|
1134
|
+
})[0]?.rows[1]?.cells[1]
|
|
1135
|
+
|
|
1136
|
+
expect(toFound(cell).segments).toContainEqual(
|
|
1137
|
+
expect.objectContaining({
|
|
1138
|
+
url: "/files?path=docs/runbook.md",
|
|
1139
|
+
}),
|
|
1140
|
+
)
|
|
1141
|
+
})
|
|
1142
|
+
})
|
|
@@ -225,6 +225,26 @@ export type ToLivePreviewRangesOptions = {
|
|
|
225
225
|
* the toggle is to see the markup, not to lose the highlighting.
|
|
226
226
|
*/
|
|
227
227
|
isRawMode?: boolean
|
|
228
|
+
/**
|
|
229
|
+
* Re-point a link before it becomes an `href`.
|
|
230
|
+
*
|
|
231
|
+
* The document's own URL goes through `toSafeLinkUrl` first, so
|
|
232
|
+
* what arrives here has already been refused if it named an
|
|
233
|
+
* executable scheme. What comes BACK is the consumer's own string
|
|
234
|
+
* and is used as given — an app that returns a route is trusted
|
|
235
|
+
* about its own routes.
|
|
236
|
+
*
|
|
237
|
+
* Return `undefined` to leave the link alone, which is also what
|
|
238
|
+
* omitting this does. **Links only**: an image's `src` is not
|
|
239
|
+
* offered, because a consumer that maps a document path to a page
|
|
240
|
+
* URL would turn a working image into a broken one.
|
|
241
|
+
*
|
|
242
|
+
* The case it exists for: a relative path in a fetched or imported
|
|
243
|
+
* document. `[the runbook](../docs/runbook.md)` resolves against
|
|
244
|
+
* the page the reader is on, which is the consuming app, and lands
|
|
245
|
+
* on nothing. Only the app knows what that path means.
|
|
246
|
+
*/
|
|
247
|
+
resolveUrl?: (url: string) => string | undefined
|
|
228
248
|
selections: readonly LivePreviewSelection[]
|
|
229
249
|
text: string
|
|
230
250
|
to?: number
|
|
@@ -912,13 +932,27 @@ const isTableRendered = ({
|
|
|
912
932
|
*/
|
|
913
933
|
const toWalker = ({
|
|
914
934
|
isRawMode,
|
|
935
|
+
resolveUrl,
|
|
915
936
|
selections,
|
|
916
937
|
text,
|
|
917
938
|
}: {
|
|
918
939
|
isRawMode: boolean
|
|
940
|
+
resolveUrl:
|
|
941
|
+
| ((url: string) => string | undefined)
|
|
942
|
+
| undefined
|
|
919
943
|
selections: readonly LivePreviewSelection[]
|
|
920
944
|
text: string
|
|
921
945
|
}) => {
|
|
946
|
+
/**
|
|
947
|
+
* The consumer's answer, or the document's own URL.
|
|
948
|
+
*
|
|
949
|
+
* Applied at the two places a LINK's URL is pushed, and nowhere
|
|
950
|
+
* else, so every downstream consumer — the anchor's `href`, the
|
|
951
|
+
* span the editor opens from, a link inside a table cell — gets
|
|
952
|
+
* the same answer without knowing this exists.
|
|
953
|
+
*/
|
|
954
|
+
const toHref = (url: string) => resolveUrl?.(url) ?? url
|
|
955
|
+
|
|
922
956
|
const toEnter = (ranges: LivePreviewRange[]) => {
|
|
923
957
|
const pushMarker = (
|
|
924
958
|
from: number,
|
|
@@ -1149,7 +1183,7 @@ const toWalker = ({
|
|
|
1149
1183
|
markKind: "linkText",
|
|
1150
1184
|
to: closeMark.from,
|
|
1151
1185
|
type: "mark",
|
|
1152
|
-
url: safeUrl,
|
|
1186
|
+
url: toHref(safeUrl),
|
|
1153
1187
|
})
|
|
1154
1188
|
}
|
|
1155
1189
|
|
|
@@ -1189,7 +1223,7 @@ const toWalker = ({
|
|
|
1189
1223
|
markKind: "autolink",
|
|
1190
1224
|
to: nodeRef.to,
|
|
1191
1225
|
type: "mark",
|
|
1192
|
-
url: safeUrl,
|
|
1226
|
+
url: toHref(safeUrl),
|
|
1193
1227
|
})
|
|
1194
1228
|
} else {
|
|
1195
1229
|
ranges.push({
|
|
@@ -1316,6 +1350,7 @@ const toWalker = ({
|
|
|
1316
1350
|
export const toLivePreviewRanges = ({
|
|
1317
1351
|
from,
|
|
1318
1352
|
isRawMode = false,
|
|
1353
|
+
resolveUrl,
|
|
1319
1354
|
selections,
|
|
1320
1355
|
text,
|
|
1321
1356
|
to,
|
|
@@ -1326,9 +1361,12 @@ export const toLivePreviewRanges = ({
|
|
|
1326
1361
|
tree.iterate({
|
|
1327
1362
|
from,
|
|
1328
1363
|
to,
|
|
1329
|
-
enter: toWalker({
|
|
1330
|
-
|
|
1331
|
-
|
|
1364
|
+
enter: toWalker({
|
|
1365
|
+
isRawMode,
|
|
1366
|
+
resolveUrl,
|
|
1367
|
+
selections,
|
|
1368
|
+
text,
|
|
1369
|
+
})(ranges),
|
|
1332
1370
|
})
|
|
1333
1371
|
|
|
1334
1372
|
return ranges
|
|
@@ -1361,6 +1399,7 @@ const TABLE_CONTAINER_NAMES = new Set([
|
|
|
1361
1399
|
*/
|
|
1362
1400
|
export const toLivePreviewTableRanges = ({
|
|
1363
1401
|
isRawMode = false,
|
|
1402
|
+
resolveUrl,
|
|
1364
1403
|
selections,
|
|
1365
1404
|
text,
|
|
1366
1405
|
tree,
|
|
@@ -1370,7 +1409,12 @@ export const toLivePreviewTableRanges = ({
|
|
|
1370
1409
|
>): LivePreviewTableRange[] => {
|
|
1371
1410
|
const ranges: LivePreviewTableRange[] = []
|
|
1372
1411
|
|
|
1373
|
-
const toEnter = toWalker({
|
|
1412
|
+
const toEnter = toWalker({
|
|
1413
|
+
isRawMode,
|
|
1414
|
+
resolveUrl,
|
|
1415
|
+
selections,
|
|
1416
|
+
text,
|
|
1417
|
+
})
|
|
1374
1418
|
|
|
1375
1419
|
tree.iterate({
|
|
1376
1420
|
enter: (nodeRef) => {
|