@portabletext/plugin-sdk-value 8.0.5 → 8.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.
package/dist/index.js CHANGED
@@ -1,16 +1,17 @@
1
1
  import { c } from "react/compiler-runtime";
2
- import { editDocument, getDocumentState, subscribeDocumentEvents, useApplyDocumentActions, useEditDocument, usePresenceForDocument, useReportPresence, useSanityInstance } from "@sanity/sdk-react";
2
+ import { editDocument, getDocumentState, subscribeDocumentEvents, useApplyDocumentActions, useCommentActions, useComments, useEditDocument, usePresenceForDocument, useReportPresence, useSanityInstance } from "@sanity/sdk-react";
3
3
  import { PortableTextEditable, useEditor, useEditorSelector } from "@portabletext/editor";
4
- import { getSelection } from "@portabletext/editor/selectors";
4
+ import { getSelectedTextBlocks, getSelection } from "@portabletext/editor/selectors";
5
5
  import { isEqualSelections } from "@portabletext/editor/utils";
6
6
  import { useState } from "react";
7
7
  import { applyAll } from "@portabletext/patches";
8
8
  import { diffValue } from "@sanity/diff-patch";
9
- import { parsePath } from "@sanity/json-match";
9
+ import { parsePath, stringifyPath } from "@sanity/json-match";
10
10
  import { useActorRef } from "@xstate/react";
11
11
  import { fromCallback, setup } from "xstate";
12
12
  import rawDebug from "debug";
13
13
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
14
+ import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, applyPatches, cleanupEfficiency, makeDiff, makePatches } from "@sanity/diff-match-patch";
14
15
  /**
15
16
  * Reduces a selection to a caret at its focus point.
16
17
  *
@@ -125,7 +126,15 @@ const debug = {
125
126
  push: createDebugger("push"),
126
127
  remote: createDebugger("remote"),
127
128
  repair: createDebugger("repair")
128
- }, ARRAYIFY_ERROR_MESSAGE = "Unexpected path format from diffValue output. Please report this issue.";
129
+ };
130
+ function normalizeDocumentHandle(handle) {
131
+ let { source, ...normalizedHandle } = handle;
132
+ return normalizedHandle.resource !== void 0 || source === void 0 ? normalizedHandle : {
133
+ ...normalizedHandle,
134
+ resource: source
135
+ };
136
+ }
137
+ const ARRAYIFY_ERROR_MESSAGE = "Unexpected path format from diffValue output. Please report this issue.";
129
138
  function* getSegments(node) {
130
139
  node.base && (yield* getSegments(node.base)), node.segment.type !== "This" && (yield node.segment);
131
140
  }
@@ -227,11 +236,11 @@ function convertPatchesToSanity(patches, options) {
227
236
  }
228
237
  });
229
238
  }
230
- function segmentsEqual(a, b) {
239
+ function segmentsEqual$1(a, b) {
231
240
  return typeof a == "string" || typeof a == "number" ? a === b : typeof b == "string" || typeof b == "number" || Array.isArray(a) || Array.isArray(b) ? !1 : a._key === b._key;
232
241
  }
233
- function pathsEqual(a, b) {
234
- return a.length === b.length && a.every((segment, index) => segmentsEqual(segment, b[index]));
242
+ function pathsEqual$1(a, b) {
243
+ return a.length === b.length && a.every((segment, index) => segmentsEqual$1(segment, b[index]));
235
244
  }
236
245
  /**
237
246
  * The editor engine can only resolve keyed/indexed segments against the
@@ -256,7 +265,7 @@ function findSidecarArrayPath(path) {
256
265
  }
257
266
  return null;
258
267
  }
259
- function getValueAtPath(value, path) {
268
+ function getValueAtPath$1(value, path) {
260
269
  let current = value;
261
270
  for (let segment of path) {
262
271
  if (current == null) return;
@@ -290,10 +299,10 @@ function toEngineSafePatches(patches, targetValue) {
290
299
  safe.push(patch);
291
300
  continue;
292
301
  }
293
- sidecarPaths.some((existing) => pathsEqual(existing, sidecarPath)) || sidecarPaths.push(sidecarPath);
302
+ sidecarPaths.some((existing) => pathsEqual$1(existing, sidecarPath)) || sidecarPaths.push(sidecarPath);
294
303
  }
295
304
  for (let sidecarPath of sidecarPaths) {
296
- let value = getValueAtPath(targetValue, sidecarPath);
305
+ let value = getValueAtPath$1(targetValue, sidecarPath);
297
306
  safe.push(value === void 0 ? {
298
307
  type: "unset",
299
308
  path: sidecarPath,
@@ -318,7 +327,7 @@ function toEngineSafePatches(patches, targetValue) {
318
327
  function filterInsertsMissingFromRemoteValue(patches, remoteValue) {
319
328
  return patches.flatMap((patch) => {
320
329
  if (patch.type !== "insert") return [patch];
321
- let parentValue = getValueAtPath(remoteValue, patch.path.slice(0, -1));
330
+ let parentValue = getValueAtPath$1(remoteValue, patch.path.slice(0, -1));
322
331
  if (!Array.isArray(parentValue)) return [patch];
323
332
  let items = patch.items.filter((item) => {
324
333
  let itemKey = getKey(item);
@@ -355,7 +364,7 @@ function toMergeableMarkDefsPatches(patches, getCurrentValue) {
355
364
  if (patch.type !== "set" || patch.path.at(-1) !== "markDefs" || !Array.isArray(patch.value)) return [patch];
356
365
  let currentValue = getCurrentValue();
357
366
  if (!currentValue) return [patch];
358
- let root = currentValue, storeMarkDefs = getValueAtPath(root, patch.path), storeBlock = getValueAtPath(root, patch.path.slice(0, -1));
367
+ let root = currentValue, storeMarkDefs = getValueAtPath$1(root, patch.path), storeBlock = getValueAtPath$1(root, patch.path.slice(0, -1));
359
368
  if (!Array.isArray(storeMarkDefs) || typeof storeBlock != "object" || !storeBlock) return [patch];
360
369
  let local = patch.value, store = storeMarkDefs;
361
370
  if (local.some((item) => item._key === void 0)) return [patch];
@@ -407,8 +416,8 @@ function canApplyToValue(patch, value) {
407
416
  switch (patch.type) {
408
417
  case "unset":
409
418
  case "insert":
410
- case "diffMatchPatch": return getValueAtPath(root, patch.path) !== void 0;
411
- case "set": return patch.path.length === 0 || getValueAtPath(root, patch.path.slice(0, -1)) !== void 0;
419
+ case "diffMatchPatch": return getValueAtPath$1(root, patch.path) !== void 0;
420
+ case "set": return patch.path.length === 0 || getValueAtPath$1(root, patch.path.slice(0, -1)) !== void 0;
412
421
  default: return !0;
413
422
  }
414
423
  }
@@ -440,7 +449,7 @@ function scopeRemotePatches(patches, fieldPath) {
440
449
  let prefix = arrayifyPath(fieldPath), converted = convertPatches(patches), scoped = [];
441
450
  for (let patch of converted) {
442
451
  let overlap = Math.min(patch.path.length, prefix.length), touchesField = !0;
443
- for (let index = 0; index < overlap; index++) if (!segmentsEqual(patch.path[index], prefix[index])) {
452
+ for (let index = 0; index < overlap; index++) if (!segmentsEqual$1(patch.path[index], prefix[index])) {
444
453
  touchesField = !1;
445
454
  break;
446
455
  }
@@ -691,14 +700,12 @@ function getPublishedDocumentId(id) {
691
700
  * @public
692
701
  */
693
702
  function SDKValuePlugin(props) {
694
- let $ = c(20), { documentId, documentType, path } = props, setSdkValue = useEditDocument(props), instance = useSanityInstance(props), applyActions = useApplyDocumentActions(), t0;
695
- $[0] !== documentId || $[1] !== documentType || $[2] !== instance || $[3] !== path ? (t0 = getDocumentState(instance, {
703
+ let $ = c(15), normalizedProps = normalizeDocumentHandle(props), { documentId, documentType, path } = normalizedProps, setSdkValue = useEditDocument(normalizedProps), instance = useSanityInstance(), applyActions = useApplyDocumentActions(), { getCurrent, subscribe } = getDocumentState(instance, {
696
704
  documentId,
697
705
  documentType,
698
706
  path
699
- }), $[0] = documentId, $[1] = documentType, $[2] = instance, $[3] = path, $[4] = t0) : t0 = $[4];
700
- let { getCurrent, subscribe } = t0, t1;
701
- $[5] !== documentId || $[6] !== instance || $[7] !== path ? (t1 = (callback) => subscribeDocumentEvents(instance, { eventHandler: (event) => {
707
+ }), t0;
708
+ $[0] !== documentId || $[1] !== instance || $[2] !== path ? (t0 = (callback) => subscribeDocumentEvents(instance, { eventHandler: (event) => {
702
709
  let candidate = event;
703
710
  if (!isRemotePatchesEvent(candidate) || candidate.origin !== "remote" || getPublishedDocumentId(candidate.documentId) !== getPublishedDocumentId(documentId)) return;
704
711
  let patches;
@@ -708,9 +715,9 @@ function SDKValuePlugin(props) {
708
715
  return;
709
716
  }
710
717
  patches && patches.length > 0 && callback(patches);
711
- } }), $[5] = documentId, $[6] = instance, $[7] = path, $[8] = t1) : t1 = $[8];
712
- let onRemotePatches = t1, t2;
713
- $[9] !== applyActions || $[10] !== documentId || $[11] !== documentType || $[12] !== path ? (t2 = (patches_0) => {
718
+ } }), $[0] = documentId, $[1] = instance, $[2] = path, $[3] = t0) : t0 = $[3];
719
+ let onRemotePatches = t0, t1;
720
+ $[4] !== applyActions || $[5] !== documentId || $[6] !== documentType || $[7] !== path ? (t1 = (patches_0) => {
714
721
  let sanityPatches = convertPatchesToSanity(patches_0, { prefix: path }), action = {
715
722
  ...editDocument({
716
723
  documentId,
@@ -719,15 +726,15 @@ function SDKValuePlugin(props) {
719
726
  preserveOperations: !0
720
727
  };
721
728
  applyActions(action);
722
- }, $[9] = applyActions, $[10] = documentId, $[11] = documentType, $[12] = path, $[13] = t2) : t2 = $[13];
723
- let pushPatches = t2, t3;
724
- return $[14] !== getCurrent || $[15] !== onRemotePatches || $[16] !== pushPatches || $[17] !== setSdkValue || $[18] !== subscribe ? (t3 = /* @__PURE__ */ jsx(ValueSyncPlugin, {
729
+ }, $[4] = applyActions, $[5] = documentId, $[6] = documentType, $[7] = path, $[8] = t1) : t1 = $[8];
730
+ let pushPatches = t1, t2;
731
+ return $[9] !== getCurrent || $[10] !== onRemotePatches || $[11] !== pushPatches || $[12] !== setSdkValue || $[13] !== subscribe ? (t2 = /* @__PURE__ */ jsx(ValueSyncPlugin, {
725
732
  getRemoteValue: getCurrent,
726
733
  pushValue: setSdkValue,
727
734
  onRemoteValueChange: subscribe,
728
735
  onRemotePatches,
729
736
  pushPatches
730
- }), $[14] = getCurrent, $[15] = onRemotePatches, $[16] = pushPatches, $[17] = setSdkValue, $[18] = subscribe, $[19] = t3) : t3 = $[19], t3;
737
+ }), $[9] = getCurrent, $[10] = onRemotePatches, $[11] = pushPatches, $[12] = setSdkValue, $[13] = subscribe, $[14] = t2) : t2 = $[14], t2;
731
738
  }
732
739
  /**
733
740
  * NOTE: You are probably looking for SDKValuePlugin instead of this.
@@ -775,14 +782,16 @@ function ValueSyncPlugin(props) {
775
782
  * @public
776
783
  */
777
784
  function SDKPresencePlugin(props) {
778
- let $ = c(7), handle, path;
779
- $[0] === props ? (handle = $[1], path = $[2]) : ({path, ...handle} = props, $[0] = props, $[1] = handle, $[2] = path);
780
- let selection = useLocalSelection(), fieldPath = useFieldPath(path), t0;
781
- return $[3] !== fieldPath || $[4] !== handle || $[5] !== selection ? (t0 = {
785
+ let $ = c(9), t0;
786
+ $[0] === props ? t0 = $[1] : (t0 = normalizeDocumentHandle(props), $[0] = props, $[1] = t0);
787
+ let handle, path;
788
+ $[2] === t0 ? (handle = $[3], path = $[4]) : ({path, ...handle} = t0, $[2] = t0, $[3] = handle, $[4] = path);
789
+ let selection = useLocalSelection(), fieldPath = useFieldPath(path), t1;
790
+ return $[5] !== fieldPath || $[6] !== handle || $[7] !== selection ? (t1 = {
782
791
  ...handle,
783
792
  path: fieldPath,
784
793
  selection
785
- }, $[3] = fieldPath, $[4] = handle, $[5] = selection, $[6] = t0) : t0 = $[6], useReportPresence(t0), null;
794
+ }, $[5] = fieldPath, $[6] = handle, $[7] = selection, $[8] = t1) : t1 = $[8], useReportPresence(t1), null;
786
795
  }
787
796
  /**
788
797
  * Other people's carets in a Portable Text field, as range decorations.
@@ -799,23 +808,25 @@ function SDKPresencePlugin(props) {
799
808
  * @public
800
809
  */
801
810
  function useSDKPresenceCursors(options) {
802
- let $ = c(12), handle, path, renderCursor;
803
- $[0] === options ? (handle = $[1], path = $[2], renderCursor = $[3]) : ({path, renderCursor, ...handle} = options, $[0] = options, $[1] = handle, $[2] = path, $[3] = renderCursor);
804
- let fieldPath = useFieldPath(path), t0;
805
- $[4] !== fieldPath || $[5] !== handle ? (t0 = {
811
+ let $ = c(14), t0;
812
+ $[0] === options ? t0 = $[1] : (t0 = normalizeDocumentHandle(options), $[0] = options, $[1] = t0);
813
+ let handle, path, renderCursor;
814
+ $[2] === t0 ? (handle = $[3], path = $[4], renderCursor = $[5]) : ({path, renderCursor, ...handle} = t0, $[2] = t0, $[3] = handle, $[4] = path, $[5] = renderCursor);
815
+ let fieldPath = useFieldPath(path), t1;
816
+ $[6] !== fieldPath || $[7] !== handle ? (t1 = {
806
817
  ...handle,
807
818
  path: fieldPath,
808
819
  excludeVersions: !0
809
- }, $[4] = fieldPath, $[5] = handle, $[6] = t0) : t0 = $[6];
810
- let { presence } = usePresenceForDocument(t0), t1;
811
- $[7] === presence ? t1 = $[8] : (t1 = presence.flatMap(_temp), $[7] = presence, $[8] = t1);
812
- let cursors = t1, t2;
813
- return $[9] !== cursors || $[10] !== renderCursor ? (t2 = {
820
+ }, $[6] = fieldPath, $[7] = handle, $[8] = t1) : t1 = $[8];
821
+ let { presence } = usePresenceForDocument(t1), t2;
822
+ $[9] === presence ? t2 = $[10] : (t2 = presence.flatMap(_temp$1), $[9] = presence, $[10] = t2);
823
+ let cursors = t2, t3;
824
+ return $[11] !== cursors || $[12] !== renderCursor ? (t3 = {
814
825
  cursors,
815
826
  renderCursor
816
- }, $[9] = cursors, $[10] = renderCursor, $[11] = t2) : t2 = $[11], useRemoteCursors(t2);
827
+ }, $[11] = cursors, $[12] = renderCursor, $[13] = t3) : t3 = $[13], useRemoteCursors(t3);
817
828
  }
818
- function _temp(participant) {
829
+ function _temp$1(participant) {
819
830
  return participant.selection ? [{
820
831
  sessionId: participant.sessionId,
821
832
  selection: participant.selection,
@@ -832,6 +843,339 @@ function useFieldPath(path) {
832
843
  return $[0] === path ? t0 = $[1] : (t0 = arrayifyPath(path), $[0] = path, $[1] = t0), t0;
833
844
  }
834
845
  /**
846
+ * How the Studio stores an inline comment's anchor: per Portable Text block the
847
+ * selection touches, the block's entire plain text with these two private-use
848
+ * characters inserted where the selection starts and ends. Text rather than
849
+ * offsets, so the anchor can be re-found after the text around it changes.
850
+ */
851
+ const COMMENT_INDICATORS = ["", ""], COMMENT_INDICATORS_REGEX = RegExp(`[${COMMENT_INDICATORS.join("")}]`, "g");
852
+ function isTextBlock(node) {
853
+ return typeof node == "object" && !!node && Array.isArray(node.children) && typeof node._key == "string";
854
+ }
855
+ function isSpan$1(child) {
856
+ return child._type === "span" && typeof child.text == "string";
857
+ }
858
+ function getValueAtPath(value, path) {
859
+ let current = value;
860
+ for (let segment of path) {
861
+ if (typeof current != "object" || !current) return;
862
+ if (typeof segment == "string") current = current[segment];
863
+ else if (typeof segment == "number") current = Array.isArray(current) ? current[segment] : void 0;
864
+ else if (isKeyedSegment$1(segment)) current = Array.isArray(current) ? current.find((item) => typeof item == "object" && !!item && item._key === segment._key) : void 0;
865
+ else return;
866
+ }
867
+ return current;
868
+ }
869
+ function isKeyedSegment$1(segment) {
870
+ return typeof segment == "object" && !!segment && "_key" in segment;
871
+ }
872
+ function toPlainTextWithChildSeparators(block) {
873
+ return block.children.map((child) => isSpan$1(child) ? (child.text ?? "").replaceAll("", " ") : "").join("");
874
+ }
875
+ function diffText(current, next) {
876
+ let diff = makeDiff(current, next), diffs = cleanupEfficiency(diff);
877
+ return {
878
+ patches: makePatches(current, diffs, { margin: 15 }),
879
+ levenshtein: diffsLevenshtein(diffs)
880
+ };
881
+ }
882
+ function diffApply(current, patches) {
883
+ return applyPatches(patches, current, {
884
+ allowExceedingIndices: !0,
885
+ margin: 15
886
+ })[0];
887
+ }
888
+ /**
889
+ * Finds each stored anchor in the current editor value.
890
+ *
891
+ * A direct port of the Studio's `buildRangeDecorationSelectionsFromComments`,
892
+ * minus its Studio-only inputs. The stored text is diffed against the block's
893
+ * current text, so an anchor survives edits around it and inside it up to a
894
+ * similarity threshold. An anchor whose text is gone, or changed beyond
895
+ * recognition, is dropped rather than drawn somewhere wrong.
896
+ *
897
+ * Live tracking while the user types is not this function's job: the editor's
898
+ * `RangeDecoration.onMoved` does that. This runs when comments load or change.
899
+ */
900
+ function resolveCommentSelections(options) {
901
+ let { value, comments } = options, anchored = [];
902
+ for (let { commentId, relativePath, selection } of comments) for (let selectionMember of selection.value) {
903
+ let container = relativePath.length > 0 ? getValueAtPath(value, relativePath) : value, matchedBlock = Array.isArray(container) ? container.find((block) => isTextBlock(block) && block._key === selectionMember._key) : void 0;
904
+ if (!matchedBlock || !isTextBlock(matchedBlock)) continue;
905
+ let selectionText = selectionMember.text.replaceAll(COMMENT_INDICATORS_REGEX, ""), textWithChildSeparators = toPlainTextWithChildSeparators(matchedBlock), { patches } = diffText(selectionText, selectionMember.text), diffedText = diffApply(textWithChildSeparators, patches), startIndex = diffedText.indexOf(COMMENT_INDICATORS[0]), endIndex = diffedText.replaceAll(COMMENT_INDICATORS[0], "").indexOf(COMMENT_INDICATORS[1]), textWithoutCommentTags = diffedText.replaceAll(COMMENT_INDICATORS_REGEX, "");
906
+ if (startIndex === -1 || endIndex === -1) continue;
907
+ let oldCommentedText = selectionMember.text.slice(selectionMember.text.indexOf(COMMENT_INDICATORS[0]) + 1, selectionMember.text.indexOf(COMMENT_INDICATORS[1])), newCommentedText = textWithoutCommentTags.slice(startIndex, endIndex), { levenshtein } = diffText(newCommentedText, oldCommentedText), threshold = Math.round(newCommentedText.length + oldCommentedText.length / 2);
908
+ if (newCommentedText.length === 0 || levenshtein > threshold || startIndex + 1 === endIndex) continue;
909
+ let childIndexAnchor = 0, anchorOffset = 0, childIndexFocus = 0, focusOffset = 0;
910
+ for (let i = 0; i < textWithoutCommentTags.length && (textWithoutCommentTags[i] === "" && (i <= startIndex && (anchorOffset = -1, childIndexAnchor++), focusOffset = -1, childIndexFocus++), i < startIndex && anchorOffset++, i < startIndex + newCommentedText.length && focusOffset++, i !== startIndex + newCommentedText.length); i++);
911
+ anchored.push({
912
+ commentId,
913
+ selection: {
914
+ anchor: {
915
+ path: [
916
+ ...relativePath,
917
+ { _key: matchedBlock._key },
918
+ "children",
919
+ { _key: matchedBlock.children[childIndexAnchor]._key }
920
+ ],
921
+ offset: anchorOffset
922
+ },
923
+ focus: {
924
+ path: [
925
+ ...relativePath,
926
+ { _key: matchedBlock._key },
927
+ "children",
928
+ { _key: matchedBlock.children[childIndexFocus]._key }
929
+ ],
930
+ offset: focusOffset
931
+ }
932
+ }
933
+ });
934
+ }
935
+ return anchored;
936
+ }
937
+ function diffsLevenshtein(diffs) {
938
+ let levenshtein = 0, insertions = 0, deletions = 0;
939
+ for (let [op, data] of diffs) switch (op) {
940
+ case DIFF_INSERT:
941
+ insertions += data.length;
942
+ break;
943
+ case DIFF_DELETE:
944
+ deletions += data.length;
945
+ break;
946
+ case DIFF_EQUAL: levenshtein += Math.max(insertions, deletions), insertions = 0, deletions = 0;
947
+ }
948
+ return levenshtein += Math.max(insertions, deletions), levenshtein;
949
+ }
950
+ /**
951
+ * Reduces a comment's stored field path to a path inside this editor.
952
+ *
953
+ * `undefined` means the comment belongs to some other editor: a different
954
+ * field, a sibling container, or a stored path that does not parse. Skipping
955
+ * those is what lets several editors on one document each decorate only their
956
+ * own comments.
957
+ */
958
+ function relativeCommentPath(basePath, fieldPath) {
959
+ let parsed;
960
+ try {
961
+ parsed = arrayifyPath(fieldPath);
962
+ } catch {
963
+ return;
964
+ }
965
+ if (!(parsed.length < basePath.length)) {
966
+ for (let i = 0; i < basePath.length; i++) if (!segmentsEqual(basePath[i], parsed[i])) return;
967
+ return parsed.slice(basePath.length);
968
+ }
969
+ }
970
+ function segmentsEqual(a, b) {
971
+ return isKeyedSegment$1(a) && isKeyedSegment$1(b) ? a._key === b._key : a === b;
972
+ }
973
+ /**
974
+ * Turns the current editor selection into the stored comment anchor.
975
+ *
976
+ * Per selected block, the block's entire plain text with the selection
977
+ * boundaries marked by the two indicator characters. The write-time mirror of
978
+ * `resolveCommentSelections`, and deliberately built the way the Studio builds
979
+ * it so a comment written here re-anchors there and back.
980
+ *
981
+ * Returns `null` when there is nothing commentable: a collapsed selection,
982
+ * no selected text, or a selection spanning blocks from different containing
983
+ * arrays, which a single stored path cannot describe.
984
+ */
985
+ function buildStoredSelection(options) {
986
+ let { selection, selectedBlocks } = options;
987
+ if (!selection || selectedBlocks.length === 0) return null;
988
+ let [start, end] = selection.backward ? [selection.focus, selection.anchor] : [selection.anchor, selection.focus], containerPath = selectedBlocks[0].path.slice(0, -1);
989
+ if (!selectedBlocks.every((selected) => pathsEqual(selected.path.slice(0, -1), containerPath))) return null;
990
+ let selectedCharacters = 0, value = selectedBlocks.map((selected, index) => {
991
+ let isFirst = index === 0, isLast = index === selectedBlocks.length - 1, plain = plainText(selected.node), from = isFirst ? plainOffset(selected.node, start.path, start.offset) : 0, to = isLast ? plainOffset(selected.node, end.path, end.offset) : plain.length;
992
+ return selectedCharacters += Math.max(0, to - from), {
993
+ _key: selected.node._key,
994
+ text: `${plain.slice(0, from)}${COMMENT_INDICATORS[0]}${plain.slice(from, to)}${COMMENT_INDICATORS[1]}${plain.slice(to)}`
995
+ };
996
+ });
997
+ return selectedCharacters === 0 ? null : {
998
+ containerPath,
999
+ selection: {
1000
+ type: "text",
1001
+ value
1002
+ }
1003
+ };
1004
+ }
1005
+ function plainText(block) {
1006
+ return block.children.map((child) => isSpan(child) ? child.text ?? "" : "").join("");
1007
+ }
1008
+ /**
1009
+ * Converts a selection point into an offset within the block's plain text: the
1010
+ * text of every span before the point's child, plus the offset inside it. A
1011
+ * point whose child is not in this block clamps to the block edge, which is
1012
+ * where a cross-block selection boundary lands.
1013
+ */
1014
+ function plainOffset(block, pointPath, offset) {
1015
+ let childSegment = pointPath[pointPath.length - 1];
1016
+ if (!isKeyedSegment(childSegment)) return offset;
1017
+ let total = 0;
1018
+ for (let child of block.children) {
1019
+ if (child._key === childSegment._key) return total + (isSpan(child) ? offset : 0);
1020
+ isSpan(child) && (total += (child.text ?? "").length);
1021
+ }
1022
+ return total;
1023
+ }
1024
+ function isSpan(child) {
1025
+ return child._type === "span" && typeof child.text == "string";
1026
+ }
1027
+ function isKeyedSegment(segment) {
1028
+ return typeof segment == "object" && !!segment && "_key" in segment;
1029
+ }
1030
+ function pathsEqual(a, b) {
1031
+ return a.length === b.length && a.every((segment, index) => {
1032
+ let other = b[index];
1033
+ return isKeyedSegment(segment) && isKeyedSegment(other) ? segment._key === other._key : segment === other;
1034
+ });
1035
+ }
1036
+ const NO_MOVES = {};
1037
+ /**
1038
+ * Anchors count as unchanged when every comment still sits at the same spot,
1039
+ * so resolving on each editor emission only re-renders when a highlight
1040
+ * actually needs to draw somewhere else.
1041
+ */
1042
+ function sameAnchors(a, b) {
1043
+ return a.length === b.length && a.every((anchor, index) => {
1044
+ let other = b[index];
1045
+ return anchor.commentId === other.commentId && samePoint(anchor.selection.anchor, other.selection.anchor) && samePoint(anchor.selection.focus, other.selection.focus);
1046
+ });
1047
+ }
1048
+ function samePoint(a, b) {
1049
+ return a.offset !== b.offset || a.path.length !== b.path.length ? !1 : a.path.every((segment, index) => {
1050
+ let other = b.path[index];
1051
+ return typeof segment == "object" && segment && typeof other == "object" && other ? segment._key === other._key : segment === other;
1052
+ });
1053
+ }
1054
+ /**
1055
+ * Inline comment highlights for a Portable Text field, as range decorations.
1056
+ *
1057
+ * Pass the result to `<PortableTextEditable rangeDecorations={...} />`. Each
1058
+ * thread's first comment that carries a text anchor on this field gets one
1059
+ * decoration. Highlights stay put while the local user types, and a highlight
1060
+ * whose text has been deleted or rewritten beyond recognition is dropped
1061
+ * rather than drawn on the wrong words.
1062
+ *
1063
+ * Suspends while the document's comments load, like every SDK read hook.
1064
+ *
1065
+ * Resolved threads draw nothing: this mirrors the Studio, where resolving a
1066
+ * thread removes its highlight from the text.
1067
+ *
1068
+ * @public
1069
+ */
1070
+ function useSDKCommentDecorations(options) {
1071
+ let $ = c(19), handle, path, renderDecoration;
1072
+ $[0] === options ? (handle = $[1], path = $[2], renderDecoration = $[3]) : ({path, renderDecoration, ...handle} = options, $[0] = options, $[1] = handle, $[2] = path, $[3] = renderDecoration);
1073
+ let editor = useEditor(), t0;
1074
+ $[4] === handle ? t0 = $[5] : (t0 = { ...handle }, $[4] = handle, $[5] = t0);
1075
+ let { comments } = useComments(t0), t1;
1076
+ if ($[6] !== comments || $[7] !== path) {
1077
+ let basePath = arrayifyPath(path);
1078
+ t1 = comments.flatMap((comment) => {
1079
+ if (comment.parentCommentId || !comment.selection || comment.status !== "open") return [];
1080
+ let relativePath = relativeCommentPath(basePath, comment.fieldPath);
1081
+ return relativePath === void 0 ? [] : [{
1082
+ comment,
1083
+ relativePath,
1084
+ selection: comment.selection
1085
+ }];
1086
+ }), $[6] = comments, $[7] = path, $[8] = t1;
1087
+ } else t1 = $[8];
1088
+ let inline = t1, t2;
1089
+ $[9] === inline ? t2 = $[10] : (t2 = (snapshot) => resolveCommentSelections({
1090
+ value: snapshot.context.value,
1091
+ comments: inline.map(_temp)
1092
+ }), $[9] = inline, $[10] = t2);
1093
+ let anchored = useEditorSelector(editor, t2, sameAnchors), t3;
1094
+ $[11] === Symbol.for("react.memo_cache_sentinel") ? (t3 = {}, $[11] = t3) : t3 = $[11];
1095
+ let t4;
1096
+ $[12] === inline ? t4 = $[13] : (t4 = {
1097
+ forComments: inline,
1098
+ selections: t3
1099
+ }, $[12] = inline, $[13] = t4);
1100
+ let [moved, setMoved] = useState(t4), movedSelections = moved.forComments === inline ? moved.selections : NO_MOVES, t5;
1101
+ if ($[14] !== anchored || $[15] !== inline || $[16] !== movedSelections || $[17] !== renderDecoration) {
1102
+ let commentsById = new Map(inline.map(_temp2));
1103
+ t5 = anchored.flatMap((anchor) => {
1104
+ let comment_2 = commentsById.get(anchor.commentId);
1105
+ if (!comment_2) return [];
1106
+ let movedSelection = movedSelections[anchor.commentId], selection_0 = movedSelection === void 0 ? anchor.selection : movedSelection;
1107
+ return selection_0 === null ? [] : [{
1108
+ component: renderDecoration(comment_2),
1109
+ selection: selection_0,
1110
+ onMoved: (t6) => {
1111
+ let { newSelection } = t6;
1112
+ setMoved((previous) => ({
1113
+ forComments: inline,
1114
+ selections: {
1115
+ ...previous.forComments === inline ? previous.selections : {},
1116
+ [anchor.commentId]: newSelection
1117
+ }
1118
+ }));
1119
+ },
1120
+ payload: { commentId: anchor.commentId }
1121
+ }];
1122
+ }), $[14] = anchored, $[15] = inline, $[16] = movedSelections, $[17] = renderDecoration, $[18] = t5;
1123
+ } else t5 = $[18];
1124
+ return t5;
1125
+ }
1126
+ function _temp2(t0) {
1127
+ let { comment: comment_1 } = t0;
1128
+ return [comment_1.id, comment_1];
1129
+ }
1130
+ function _temp(t0) {
1131
+ let { comment: comment_0, relativePath: relativePath_0, selection } = t0;
1132
+ return {
1133
+ commentId: comment_0.id,
1134
+ relativePath: relativePath_0,
1135
+ selection
1136
+ };
1137
+ }
1138
+ /** Reports the selection when it can take a comment, `null` otherwise. */
1139
+ function getCommentableSelection(snapshot) {
1140
+ let selection = getSelection(snapshot);
1141
+ return buildStoredSelection({
1142
+ selection,
1143
+ selectedBlocks: getSelectedTextBlocks(snapshot)
1144
+ }) ? selection : null;
1145
+ }
1146
+ /**
1147
+ * Lets the app author inline comments on a Portable Text field.
1148
+ *
1149
+ * The plugin captures the selection and writes the comment; the composer UI is
1150
+ * the app's, the same split the Studio and Canvas use. Comments are written in
1151
+ * the shape the Studio stores, so a thread started here shows up there.
1152
+ *
1153
+ * @public
1154
+ */
1155
+ function useSDKCommentAuthoring(options) {
1156
+ let $ = c(11), handle, path;
1157
+ $[0] === options ? (handle = $[1], path = $[2]) : ({path, ...handle} = options, $[0] = options, $[1] = handle, $[2] = path);
1158
+ let editor = useEditor(), { createComment } = useCommentActions(), commentableSelection = useEditorSelector(editor, getCommentableSelection, isEqualSelections), t0;
1159
+ $[3] !== createComment || $[4] !== editor || $[5] !== handle || $[6] !== path ? (t0 = (t1) => {
1160
+ let { message, commentId } = t1, snapshot = editor.getSnapshot(), built = buildStoredSelection({
1161
+ selection: getSelection(snapshot),
1162
+ selectedBlocks: getSelectedTextBlocks(snapshot)
1163
+ });
1164
+ return built ? createComment({
1165
+ ...handle,
1166
+ fieldPath: stringifyPath([...arrayifyPath(path), ...built.containerPath]),
1167
+ selection: built.selection,
1168
+ message,
1169
+ ...commentId === void 0 ? {} : { commentId }
1170
+ }) : Promise.reject(/* @__PURE__ */ Error("Nothing commentable is selected, so there is nothing to anchor the comment to."));
1171
+ }, $[3] = createComment, $[4] = editor, $[5] = handle, $[6] = path, $[7] = t0) : t0 = $[7];
1172
+ let t1;
1173
+ return $[8] !== commentableSelection || $[9] !== t0 ? (t1 = {
1174
+ commentableSelection,
1175
+ createInlineComment: t0
1176
+ }, $[8] = commentableSelection, $[9] = t0, $[10] = t1) : t1 = $[10], t1;
1177
+ }
1178
+ /**
835
1179
  * Mid-tone hues, so a caret stays legible whether the app is light or dark. This
836
1180
  * package cannot read the app's theme, so it does not try.
837
1181
  */
@@ -977,16 +1321,15 @@ function SDKPortableTextEditable(props) {
977
1321
  * @internal
978
1322
  */
979
1323
  function splitEditableProps(props) {
980
- let { documentId, documentType, projectId, dataset, resource, resourceName, source, liveEdit, perspective, path, renderCursor, rangeDecorations, ...editableProps } = props;
1324
+ let normalizedProps = normalizeDocumentHandle(props), { documentId, documentType, projectId, dataset, resource, resourceName, liveEdit, perspective, path, renderCursor, rangeDecorations, ...editableProps } = normalizedProps;
981
1325
  return {
982
1326
  handle: {
983
1327
  documentId,
984
1328
  documentType,
985
1329
  ..."projectId" in props && { projectId },
986
1330
  ..."dataset" in props && { dataset },
987
- ..."resource" in props && { resource },
1331
+ ..."resource" in normalizedProps && { resource },
988
1332
  ..."resourceName" in props && { resourceName },
989
- ..."source" in props && { source },
990
1333
  ..."liveEdit" in props && { liveEdit },
991
1334
  ..."perspective" in props && { perspective }
992
1335
  },
@@ -1021,6 +1364,6 @@ function resolveCursorRenderer(renderCursor) {
1021
1364
  function mergePresenceDecorations(rangeDecorations, cursors, drawCursors) {
1022
1365
  return drawCursors ? [...rangeDecorations ?? [], ...cursors] : rangeDecorations;
1023
1366
  }
1024
- export { SDKPortableTextEditable, SDKPresencePlugin, SDKValuePlugin, ValueSyncPlugin, useSDKPresenceCursors };
1367
+ export { SDKPortableTextEditable, SDKPresencePlugin, SDKValuePlugin, ValueSyncPlugin, useSDKCommentAuthoring, useSDKCommentDecorations, useSDKPresenceCursors };
1025
1368
 
1026
1369
  //# sourceMappingURL=index.js.map