@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.
Files changed (36) hide show
  1. package/dist/Combobox/Combobox.d.ts.map +1 -1
  2. package/dist/Combobox/Combobox.js +92 -50
  3. package/dist/Combobox/Combobox.js.map +1 -1
  4. package/dist/MarkdownEditor/MarkdownEditor.d.ts +7 -1
  5. package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -1
  6. package/dist/MarkdownEditor/MarkdownEditor.js +1 -1
  7. package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -1
  8. package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.d.ts.map +1 -1
  9. package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.js +1 -1
  10. package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.js.map +1 -1
  11. package/dist/markdownEditorCodeMirror/MarkdownView.d.ts +24 -1
  12. package/dist/markdownEditorCodeMirror/MarkdownView.d.ts.map +1 -1
  13. package/dist/markdownEditorCodeMirror/MarkdownView.js +14 -2
  14. package/dist/markdownEditorCodeMirror/MarkdownView.js.map +1 -1
  15. package/dist/markdownEditorCodeMirror/livePreview.d.ts +7 -0
  16. package/dist/markdownEditorCodeMirror/livePreview.d.ts.map +1 -1
  17. package/dist/markdownEditorCodeMirror/livePreview.js +6 -2
  18. package/dist/markdownEditorCodeMirror/livePreview.js.map +1 -1
  19. package/dist/markdownEditorCodeMirror/livePreviewRanges.d.ts +22 -2
  20. package/dist/markdownEditorCodeMirror/livePreviewRanges.d.ts.map +1 -1
  21. package/dist/markdownEditorCodeMirror/livePreviewRanges.js +26 -7
  22. package/dist/markdownEditorCodeMirror/livePreviewRanges.js.map +1 -1
  23. package/package.json +2 -2
  24. package/src/Combobox/Combobox.test.tsx +89 -0
  25. package/src/Combobox/Combobox.tsx +100 -59
  26. package/src/MarkdownEditor/MarkdownEditor.mdx +12 -0
  27. package/src/MarkdownEditor/MarkdownEditor.stories.tsx +17 -0
  28. package/src/MarkdownEditor/MarkdownEditor.tsx +8 -0
  29. package/src/icons.storyHelpers.tsx +108 -0
  30. package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.mdx +16 -0
  31. package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.stories.tsx +55 -0
  32. package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.tsx +1 -0
  33. package/src/markdownEditorCodeMirror/MarkdownView.tsx +40 -1
  34. package/src/markdownEditorCodeMirror/livePreview.ts +18 -3
  35. package/src/markdownEditorCodeMirror/livePreviewRanges.test.ts +137 -0
  36. 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 = "![a diagram](../docs/a.png)"
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({ isRawMode, selections, text })(
1330
- ranges,
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({ isRawMode, selections, text })
1412
+ const toEnter = toWalker({
1413
+ isRawMode,
1414
+ resolveUrl,
1415
+ selections,
1416
+ text,
1417
+ })
1374
1418
 
1375
1419
  tree.iterate({
1376
1420
  enter: (nodeRef) => {