@portabletext/plugin-list-index 1.0.34 → 2.0.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 +3 -5
- package/dist/index.js.map +1 -1
- package/package.json +10 -19
package/dist/index.js
CHANGED
|
@@ -26,11 +26,9 @@ function serializePath(path) {
|
|
|
26
26
|
* sequence. List items nested inside a container (e.g. a table cell) number
|
|
27
27
|
* within their own array, independently of siblings and the enclosing array.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* the document root from `context.value`. Keep the numbering semantics in sync
|
|
33
|
-
* with `packages/editor/src/internal-utils/build-index-maps.ts`.
|
|
29
|
+
* This is the sole implementation of the numbering semantics; the editor
|
|
30
|
+
* core does not compute list indices. Descends containers with the public
|
|
31
|
+
* `getContainerChildren`, seeding the document root from `context.value`.
|
|
34
32
|
*/
|
|
35
33
|
function buildListIndexMap(context) {
|
|
36
34
|
let listIndexMap = /* @__PURE__ */ new Map();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["EditorContext","Path","RegisteredContainer","getContainerChildren","isKeyedSegment","isTextBlock","serializePath","path","reduce","result","segment","index","_key","separator","ListIndexInput","Pick","Partial","TraversalContext","Block","NonNullable","ReturnType","buildListIndexMap","context","Map","listIndexMap","traversalContext","schema","containers","collectListIndexes","value","undefined","blocks","ReadonlyArray","basePath","parent","levelIndexMaps","previousListItem","listItem","level","block","blockPath","clear","childResult","children","container","field","name","listIndex","levelIndexMap","get","set","forEach","levelsToDelete","_","push","delete","levelCounter","useEditor","Editor","Path","createContext","useCallback","useContext","useEffect","useMemo","useSyncExternalStore","ReactNode","buildListIndexMap","serializePath","ListIndexStore","get","serializedPath","subscribeKey","callback","subscribe","createListIndexStore","editor","listIndexMap","getSnapshot","context","subscribers","Map","Set","rebuild","previousListIndexMap","callbacks","bucket","undefined","set","add","delete","size","subscription","on","events","some","event","operation","type","batch","unsubscribe","ListIndexContext","ListIndexProvider","props","$","_c","t0","store","t1","t2","t3","children","useListIndex","path","Error"],"sources":["../src/build-list-index-map.ts","../src/plugin.list-index.tsx"],"sourcesContent":["import type {\n EditorContext,\n Path,\n RegisteredContainer,\n} from '@portabletext/editor'\nimport {getContainerChildren} from '@portabletext/editor/traversal'\nimport {isKeyedSegment, isTextBlock} from '@portabletext/editor/utils'\n\n/**\n * Serialize a keyed path to a string using Sanity's bracket notation,\n * e.g. `[{_key: 'k0'}]` becomes `[_key==\"k0\"]`.\n *\n * Duplicated from the editor's internal `serializePath` rather than\n * imported: exporting it from core would add permanent public API for what\n * is an implementation detail of this plugin.\n */\nexport function serializePath(path: Path): string {\n return path.reduce<string>((result, segment, index) => {\n if (isKeyedSegment(segment)) {\n return `${result}[_key==\"${segment._key}\"]`\n }\n\n const separator = index === 0 ? '' : '.'\n return `${result}${separator}${segment}`\n }, '')\n}\n\n// `containers` is optional: the production caller passes the full snapshot\n// context, while the flat (top-level only) test scenarios omit it. Without\n// containers, container blocks resolve to no children and are not descended,\n// the pre-container behavior.\ntype ListIndexInput = Pick<EditorContext, 'schema' | 'value'> &\n Partial<Pick<EditorContext, 'containers'>>\n\ntype TraversalContext = Pick<EditorContext, 'schema' | 'containers'>\n\n// The block type `getContainerChildren` yields. `Node` is not public, so\n// derive it from the (public) traversal primitive rather than naming it.\ntype Block = NonNullable<\n ReturnType<typeof getContainerChildren>\n>['children'][number]\n\n/**\n * Compute the list index for every list item block in the value, at any\n * depth.\n *\n * Returns a fresh `Map` keyed by the serialized full block path with the\n * 1-based index of the block within its list, honoring list type and\n * indentation level: same-type items count up across consecutive blocks on\n * the same level, deeper levels restart at 1, and non-list blocks break the\n * sequence. List items nested inside a container (e.g. a table cell) number\n * within their own array, independently of siblings and the enclosing array.\n *\n * Duplicated from the editor's internal `buildIndexMaps` (the part that fills\n * `listIndexMap`) rather than imported, for the same reason as `serializePath`\n * above. Descends containers with the public `getContainerChildren`, seeding\n * the document root from `context.value`. Keep the numbering semantics in sync\n * with `packages/editor/src/internal-utils/build-index-maps.ts`.\n */\nexport function buildListIndexMap(\n context: ListIndexInput,\n): Map<string, number> {\n const listIndexMap = new Map<string, number>()\n const traversalContext: TraversalContext = {\n schema: context.schema,\n containers: context.containers ?? new Map<string, RegisteredContainer>(),\n }\n collectListIndexes(\n traversalContext,\n context.value,\n [],\n undefined,\n listIndexMap,\n )\n return listIndexMap\n}\n\n/**\n * Walk one block array, numbering its list items, then descend into any\n * container blocks. List state is scoped per array: a list inside a container\n * numbers independently, so each array starts fresh from 1.\n */\nfunction collectListIndexes(\n context: TraversalContext,\n blocks: ReadonlyArray<Block | undefined>,\n basePath: Path,\n parent: RegisteredContainer | undefined,\n listIndexMap: Map<string, number>,\n): void {\n const levelIndexMaps = new Map<string, Map<number, number>>()\n\n let previousListItem:\n | {\n listItem: string\n level: number\n }\n | undefined\n\n for (const block of blocks) {\n if (block === undefined || block._key === undefined) {\n continue\n }\n\n const blockPath: Path = [...basePath, {_key: block._key}]\n\n // A non-list block breaks the list run for this array. Containers are\n // descended into; each nested array numbers independently.\n //\n // Unlike the engine's internal `isTextBlockNode`, `isTextBlock` also\n // requires `children`, which holds for the post-apply snapshot values\n // this plugin reads.\n if (\n !isTextBlock(context, block) ||\n block.listItem === undefined ||\n block.level === undefined\n ) {\n levelIndexMaps.clear()\n previousListItem = undefined\n\n const childResult = getContainerChildren(\n context.containers,\n block,\n parent,\n )\n if (childResult) {\n collectListIndexes(\n context,\n childResult.children,\n [...blockPath, childResult.container.field.name],\n childResult.container,\n listIndexMap,\n )\n }\n\n continue\n }\n\n // If we encounter a new list item, we set the initial index to 1 for the\n // list type on that level.\n if (!previousListItem) {\n const listIndex = 1\n const levelIndexMap =\n levelIndexMaps.get(block.listItem) ?? new Map<number, number>()\n levelIndexMap.set(block.level, listIndex)\n levelIndexMaps.set(block.listItem, levelIndexMap)\n\n listIndexMap.set(serializePath(blockPath), listIndex)\n\n previousListItem = {\n listItem: block.listItem,\n level: block.level,\n }\n\n continue\n }\n\n // If the previous list item is of the same type but on a lower level, we\n // need to reset the level index map for that type.\n if (\n previousListItem.listItem === block.listItem &&\n previousListItem.level < block.level\n ) {\n const listIndex = 1\n const levelIndexMap =\n levelIndexMaps.get(block.listItem) ?? new Map<number, number>()\n levelIndexMap.set(block.level, listIndex)\n levelIndexMaps.set(block.listItem, levelIndexMap)\n\n listIndexMap.set(serializePath(blockPath), listIndex)\n\n previousListItem = {\n listItem: block.listItem,\n level: block.level,\n }\n\n continue\n }\n\n // Reset other list types at current level and deeper\n levelIndexMaps.forEach((levelIndexMap, listItem) => {\n if (listItem === block.listItem) {\n return\n }\n\n // Reset all levels that are >= current level\n const levelsToDelete: number[] = []\n\n levelIndexMap.forEach((_, level) => {\n if (block.level !== undefined && level >= block.level) {\n levelsToDelete.push(level)\n }\n })\n\n levelsToDelete.forEach((level) => {\n levelIndexMap.delete(level)\n })\n })\n\n const levelIndexMap =\n levelIndexMaps.get(block.listItem) ?? new Map<number, number>()\n const levelCounter = levelIndexMap.get(block.level) ?? 0\n levelIndexMap.set(block.level, levelCounter + 1)\n levelIndexMaps.set(block.listItem, levelIndexMap)\n\n listIndexMap.set(serializePath(blockPath), levelCounter + 1)\n\n previousListItem = {\n listItem: block.listItem,\n level: block.level,\n }\n }\n}\n","import {useEditor, type Editor, type Path} from '@portabletext/editor'\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from 'react'\nimport {buildListIndexMap, serializePath} from './build-list-index-map'\n\n/**\n * One store per editor: a single `operation` subscription maintains the\n * list index map, and per-path subscriber buckets make notification\n * O(changed paths) rather than O(subscribers).\n */\ntype ListIndexStore = {\n get: (serializedPath: string) => number | undefined\n subscribeKey: (serializedPath: string, callback: () => void) => () => void\n /**\n * Starts the `operation` subscription. Returns the unsubscribe.\n */\n subscribe: () => () => void\n}\n\nfunction createListIndexStore(editor: Editor): ListIndexStore {\n let listIndexMap = buildListIndexMap(editor.getSnapshot().context)\n const subscribers = new Map<string, Set<() => void>>()\n\n function rebuild() {\n const previousListIndexMap = listIndexMap\n\n // Swap before notifying: `useSyncExternalStore` re-reads the snapshot\n // synchronously on notification and skips the re-render when it reads\n // an unchanged (stale) value.\n listIndexMap = buildListIndexMap(editor.getSnapshot().context)\n\n for (const [serializedPath, callbacks] of subscribers) {\n if (\n previousListIndexMap.get(serializedPath) !==\n listIndexMap.get(serializedPath)\n ) {\n for (const callback of callbacks) {\n callback()\n }\n }\n }\n }\n\n return {\n get: (serializedPath) => listIndexMap.get(serializedPath),\n subscribeKey: (serializedPath, callback) => {\n let bucket = subscribers.get(serializedPath)\n\n if (bucket === undefined) {\n bucket = new Set()\n subscribers.set(serializedPath, bucket)\n }\n\n bucket.add(callback)\n\n return () => {\n bucket.delete(callback)\n\n if (bucket.size === 0) {\n subscribers.delete(serializedPath)\n }\n }\n },\n subscribe: () => {\n // Operations applied between store creation (render) and subscription\n // (effect) are not observed, so reconcile once up front.\n rebuild()\n\n // Batched delivery coalesces a burst (e.g. one operation per block\n // during a large delete, insert, or undo) into a single call carrying\n // the whole burst, so the map rebuilds at most once per burst instead\n // of once per operation. Text ops never change list structure; every\n // other document-changing op can (including deep edits inside a\n // container), so rebuild only when the burst contains one. Inspecting\n // the whole burst is why this needs `batch` rather than a last-event\n // delivery, which would drop a structural op whenever a text op\n // happened to be last in the burst.\n const subscription = editor.on(\n 'operation',\n (events) => {\n if (\n events.some(\n (event) =>\n event.operation.type !== 'insert.text' &&\n event.operation.type !== 'remove.text',\n )\n ) {\n rebuild()\n }\n },\n {batch: true},\n )\n\n return () => {\n subscription.unsubscribe()\n }\n },\n }\n}\n\nconst ListIndexContext = createContext<ListIndexStore | undefined>(undefined)\n\n/**\n * Maintains a list index map for the editor and serves it through context.\n * Mount inside `EditorProvider`, wrapping whatever reads the indices:\n *\n * ```tsx\n * <EditorProvider initialConfig={...}>\n * <ListIndexProvider>\n * <PortableTextEditable />\n * </ListIndexProvider>\n * </EditorProvider>\n * ```\n *\n * The map is rebuilt at most once per microtask burst of operations,\n * regardless of how many operations the burst contains or how many\n * components read it, and only when the burst contains an operation that can\n * affect list indices. Reads via {@link useListIndex} only re-render when the\n * index at their own path changes.\n *\n * @beta\n */\nexport function ListIndexProvider(props: {children?: ReactNode}) {\n const editor = useEditor()\n const store = useMemo(() => createListIndexStore(editor), [editor])\n\n useEffect(() => {\n return store.subscribe()\n }, [store])\n\n return (\n <ListIndexContext.Provider value={store}>\n {props.children}\n </ListIndexContext.Provider>\n )\n}\n\n/**\n * Read the 1-based list index of the block at `path`, or `undefined` when\n * the block is not a list item. Re-renders only when the index at this\n * path changes.\n *\n * `path` is the keyed block path render callbacks receive, e.g.\n * `[{_key: 'b0'}]`.\n *\n * @beta\n */\nexport function useListIndex(path: Path): number | undefined {\n const store = useContext(ListIndexContext)\n\n if (store === undefined) {\n throw new Error('useListIndex must be used below a <ListIndexProvider>')\n }\n\n // Callers typically pass a fresh `path` array every render, so the\n // serialized string, not the array, is what keeps `subscribe` stable\n // below. Memoizing on the array would never hit.\n const serializedPath = serializePath(path)\n\n const subscribe = useCallback(\n (callback: () => void) => store.subscribeKey(serializedPath, callback),\n [store, serializedPath],\n )\n\n const getSnapshot = () => store.get(serializedPath)\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n"],"mappings":";;;;;;;;;;;;;;AAgBA,SAAgBM,cAAcC,MAAoB;CAChD,OAAOA,KAAKC,QAAgBC,QAAQC,SAASC,UACvCP,eAAeM,OAAO,IACjB,GAAGD,OAAM,UAAWC,QAAQE,KAAI,MAIlC,GAAGH,SADQE,UAAU,IAAI,KAAK,MACND,WAC9B,EAAE;AACP;;;;;;;;;;;;;;;;;;AAkCA,SAAgBW,kBACdC,SACqB;CACrB,IAAME,+BAAe,IAAID,IAAoB;CAY7C,OAPAK,mBACEH;EAJAC,QAAQJ,QAAQI;EAChBC,YAAYL,QAAQK,8BAAc,IAAIJ,IAAiC;CAGvEE,GACAH,QAAQO,OACR,CAAA,GACAC,KAAAA,GACAN,YACF,GACOA;AACT;;;;;;AAOA,SAASI,mBACPN,SACAS,QACAE,UACAC,QACAV,cACM;CACN,IAAMW,iCAAiB,IAAIZ,IAAiC,GAExDa;CAOJ,KAAK,IAAMG,SAASR,QAAQ;EAC1B,IAAIQ,UAAUT,KAAAA,KAAaS,MAAM3B,SAASkB,KAAAA,GACxC;EAGF,IAAMU,YAAkB,CAAC,GAAGP,UAAU,EAACrB,MAAM2B,MAAM3B,KAAI,CAAC;EAQxD,IACE,CAACP,YAAYiB,SAASiB,KAAK,KAC3BA,MAAMF,aAAaP,KAAAA,KACnBS,MAAMD,UAAUR,KAAAA,GAChB;GAEAM,AADAD,eAAeM,MAAM,GACrBL,mBAAmBN,KAAAA;GAEnB,IAAMY,cAAcvC,qBAClBmB,QAAQK,YACRY,OACAL,MACF;GACA,AAAIQ,eACFd,mBACEN,SACAoB,YAAYC,UACZ,CAAC,GAAGH,WAAWE,YAAYE,UAAUC,MAAMC,IAAI,GAC/CJ,YAAYE,WACZpB,YACF;GAGF;EACF;EAIA,IAAI,CAACY,kBAAkB;GACrB,IACMY,gBACJb,eAAec,IAAIV,MAAMF,QAAQ,qBAAK,IAAId,IAAoB;GAMhEa,AALAY,cAAcE,IAAIX,MAAMD,OAAOS,CAAS,GACxCZ,eAAee,IAAIX,MAAMF,UAAUW,aAAa,GAEhDxB,aAAa0B,IAAI5C,cAAckC,SAAS,GAAGO,CAAS,GAEpDX,mBAAmB;IACjBC,UAAUE,MAAMF;IAChBC,OAAOC,MAAMD;GACf;GAEA;EACF;EAIA,IACEF,iBAAiBC,aAAaE,MAAMF,YACpCD,iBAAiBE,QAAQC,MAAMD,OAC/B;GACA,IACMU,gBACJb,eAAec,IAAIV,MAAMF,QAAQ,qBAAK,IAAId,IAAoB;GAMhEa,AALAY,cAAcE,IAAIX,MAAMD,OAAOS,CAAS,GACxCZ,eAAee,IAAIX,MAAMF,UAAUW,aAAa,GAEhDxB,aAAa0B,IAAI5C,cAAckC,SAAS,GAAGO,CAAS,GAEpDX,mBAAmB;IACjBC,UAAUE,MAAMF;IAChBC,OAAOC,MAAMD;GACf;GAEA;EACF;EAGAH,eAAegB,SAASH,eAAeX,aAAa;GAClD,IAAIA,aAAaE,MAAMF,UACrB;GAIF,IAAMe,iBAA2B,CAAA;GAQjCA,AANAJ,cAAcG,SAASE,GAAGf,UAAU;IAClC,AAAIC,MAAMD,UAAUR,KAAAA,KAAaQ,SAASC,MAAMD,SAC9Cc,eAAeE,KAAKhB,KAAK;GAE7B,CAAC,GAEDc,eAAeD,SAASb,UAAU;IAChCU,cAAcO,OAAOjB,KAAK;GAC5B,CAAC;EACH,CAAC;EAED,IAAMU,gBACJb,eAAec,IAAIV,MAAMF,QAAQ,qBAAK,IAAId,IAAoB,GAC1DiC,eAAeR,cAAcC,IAAIV,MAAMD,KAAK,KAAK;EAMvDF,AALAY,cAAcE,IAAIX,MAAMD,OAAOkB,eAAe,CAAC,GAC/CrB,eAAee,IAAIX,MAAMF,UAAUW,aAAa,GAEhDxB,aAAa0B,IAAI5C,cAAckC,SAAS,GAAGgB,eAAe,CAAC,GAE3DpB,mBAAmB;GACjBC,UAAUE,MAAMF;GAChBC,OAAOC,MAAMD;EACf;CACF;AACF;ACzLA,SAASqC,qBAAqBC,QAAgC;CAC5D,IAAIC,eAAeV,kBAAkBS,OAAOE,YAAY,CAAC,CAACC,OAAO,GAC3DC,8BAAc,IAAIC,IAA6B;CAErD,SAASE,UAAU;EACjB,IAAMC,uBAAuBP;EAK7BA,eAAeV,kBAAkBS,OAAOE,YAAY,CAAC,CAACC,OAAO;EAE7D,KAAK,IAAM,CAACR,gBAAgBc,cAAcL,aACxC,IACEI,qBAAqBd,IAAIC,cAAc,MACvCM,aAAaP,IAAIC,cAAc,GAE/B,KAAK,IAAME,YAAYY,WACrBZ,SAAS;CAIjB;CAEA,OAAO;EACLH,MAAMC,mBAAmBM,aAAaP,IAAIC,cAAc;EACxDC,eAAeD,gBAAgBE,aAAa;GAC1C,IAAIa,SAASN,YAAYV,IAAIC,cAAc;GAS3C,OAPIe,WAAWC,KAAAA,MACbD,yBAAS,IAAIJ,IAAI,GACjBF,YAAYQ,IAAIjB,gBAAgBe,MAAM,IAGxCA,OAAOG,IAAIhB,QAAQ,SAEN;IAGX,AAFAa,OAAOI,OAAOjB,QAAQ,GAElBa,OAAOK,SAAS,KAClBX,YAAYU,OAAOnB,cAAc;GAErC;EACF;EACAG,iBAAiB;GAGfS,QAAQ;GAWR,IAAMS,eAAehB,OAAOiB,GAC1B,cACCC,WAAW;IACV,AACEA,OAAOC,MACJC,UACCA,MAAMC,UAAUC,SAAS,iBACzBF,MAAMC,UAAUC,SAAS,aAC7B,KAEAf,QAAQ;GAEZ,GACA,EAACgB,OAAO,GAAI,CACd;GAEA,aAAa;IACXP,aAAaQ,YAAY;GAC3B;EACF;CACF;AACF;AAEA,MAAMC,mBAAmBzC,cAA0C2B,KAAAA,CAAS;;;;;;;;;;;;;;;;;;;;;AAsB5E,SAAOe,kBAAAC,OAAA;CAAA,IAAAC,IAAAC,EAAA,CAAA,GACL7B,SAAenB,UAAU,GAACiD;CAAA,AAAAF,EAAA,OAAA5B,SAC8B8B,KAAAF,EAAA,MAA5BE,KAAA/B,qBAAqBC,MAAM,GAAC4B,EAAA,KAAA5B,QAAA4B,EAAA,KAAAE;CAAxD,IAAAC,QAA4BD,IAAuCE,IAAAC;CAEnE9C,AAFmEyC,EAAA,OAAAG,SAIzDC,KAAAJ,EAAA,IAAAK,KAAAL,EAAA,OAFAI,WACDD,MAAKjC,UAAW,GACtBmC,KAAA,CAACF,KAAK,GAACH,EAAA,KAAAG,OAAAH,EAAA,KAAAI,IAAAJ,EAAA,KAAAK,KAFV9C,UAAU6C,IAEPC,EAAO;CAAC,IAAAC;CAKmB,OALnBN,EAAA,OAAAD,MAAAQ,YAAAP,EAAA,OAAAG,SAGTG,KAAA,oBAAA,iBAAA,UAAA;EAAkCH,OAAAA;EAC/BJ,UAAAA,MAAKQ;CACR,CAAA,GAA4BP,EAAA,KAAAD,MAAAQ,UAAAP,EAAA,KAAAG,OAAAH,EAAA,KAAAM,MAAAA,KAAAN,EAAA,IAF5BM;AAE4B;;;;;;;;;;;AAchC,SAAOE,aAAAC,MAAA;CAAA,IAAAT,IAAAC,EAAA,CAAA,GACLE,QAAc7C,WAAWuC,gBAAgB;CAEzC,IAAIM,UAAUpB,KAAAA,GACZ,MAAU2B,MAAM,uDAAuD;CACxE,IAAAR;CAAA,AAAAF,EAAA,OAAAS,OAKyCP,KAAAF,EAAA,MAAnBE,KAAAtC,cAAc6C,IAAI,GAACT,EAAA,KAAAS,MAAAT,EAAA,KAAAE;CAA1C,IAAAnC,iBAAuBmC,IAAmBE;CAAA,AAAAJ,EAAA,OAAAjC,kBAAAiC,EAAA,OAAAG,SAGxCC,MAAAnC,aAA0BkC,MAAKnC,aAAcD,gBAAgBE,QAAQ,GAAC+B,EAAA,KAAAjC,gBAAAiC,EAAA,KAAAG,OAAAH,EAAA,KAAAI,MAAAA,KAAAJ,EAAA;CADxE,IAAA9B,YAAkBkC,IAGjBC;CAAA,AAAAL,EAAA,OAAAjC,kBAAAiC,EAAA,OAAAG,SAEmBE,WAAMF,MAAKrC,IAAKC,cAAc,GAACiC,EAAA,KAAAjC,gBAAAiC,EAAA,KAAAG,OAAAH,EAAA,KAAAK,MAAAA,KAAAL,EAAA;CAAnD,IAAA1B,cAAoB+B;CAA+B,OAE5C5C,qBAAqBS,WAAWI,aAAaA,WAAW;AAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["createListIndexStore(editor)","useMemo(() => createListIndexStore(editor), [editor])","() => {\n return store.subscribe()\n }","[store]","<ListIndexContext.Provider value={store}>\n {props.children}\n </ListIndexContext.Provider>","children","ListIndexContext.Provider","serializePath(path)","(callback: () => void) => store.subscribeKey(serializedPath, callback)","useCallback(\n (callback: () => void) => store.subscribeKey(serializedPath, callback),\n [store, serializedPath],\n )","() => store.get(serializedPath)"],"sources":["../src/build-list-index-map.ts","../src/plugin.list-index.tsx"],"sourcesContent":["import type {\n EditorContext,\n Path,\n RegisteredContainer,\n} from '@portabletext/editor'\nimport {getContainerChildren} from '@portabletext/editor/traversal'\nimport {isKeyedSegment, isTextBlock} from '@portabletext/editor/utils'\n\n/**\n * Serialize a keyed path to a string using Sanity's bracket notation,\n * e.g. `[{_key: 'k0'}]` becomes `[_key==\"k0\"]`.\n *\n * Duplicated from the editor's internal `serializePath` rather than\n * imported: exporting it from core would add permanent public API for what\n * is an implementation detail of this plugin.\n */\nexport function serializePath(path: Path): string {\n return path.reduce<string>((result, segment, index) => {\n if (isKeyedSegment(segment)) {\n return `${result}[_key==\"${segment._key}\"]`\n }\n\n const separator = index === 0 ? '' : '.'\n return `${result}${separator}${segment}`\n }, '')\n}\n\n// `containers` is optional: the production caller passes the full snapshot\n// context, while the flat (top-level only) test scenarios omit it. Without\n// containers, container blocks resolve to no children and are not descended,\n// the pre-container behavior.\ntype ListIndexInput = Pick<EditorContext, 'schema' | 'value'> &\n Partial<Pick<EditorContext, 'containers'>>\n\ntype TraversalContext = Pick<EditorContext, 'schema' | 'containers'>\n\n// The block type `getContainerChildren` yields. `Node` is not public, so\n// derive it from the (public) traversal primitive rather than naming it.\ntype Block = NonNullable<\n ReturnType<typeof getContainerChildren>\n>['children'][number]\n\n/**\n * Compute the list index for every list item block in the value, at any\n * depth.\n *\n * Returns a fresh `Map` keyed by the serialized full block path with the\n * 1-based index of the block within its list, honoring list type and\n * indentation level: same-type items count up across consecutive blocks on\n * the same level, deeper levels restart at 1, and non-list blocks break the\n * sequence. List items nested inside a container (e.g. a table cell) number\n * within their own array, independently of siblings and the enclosing array.\n *\n * This is the sole implementation of the numbering semantics; the editor\n * core does not compute list indices. Descends containers with the public\n * `getContainerChildren`, seeding the document root from `context.value`.\n */\nexport function buildListIndexMap(\n context: ListIndexInput,\n): Map<string, number> {\n const listIndexMap = new Map<string, number>()\n const traversalContext: TraversalContext = {\n schema: context.schema,\n containers: context.containers ?? new Map<string, RegisteredContainer>(),\n }\n collectListIndexes(\n traversalContext,\n context.value,\n [],\n undefined,\n listIndexMap,\n )\n return listIndexMap\n}\n\n/**\n * Walk one block array, numbering its list items, then descend into any\n * container blocks. List state is scoped per array: a list inside a container\n * numbers independently, so each array starts fresh from 1.\n */\nfunction collectListIndexes(\n context: TraversalContext,\n blocks: ReadonlyArray<Block | undefined>,\n basePath: Path,\n parent: RegisteredContainer | undefined,\n listIndexMap: Map<string, number>,\n): void {\n const levelIndexMaps = new Map<string, Map<number, number>>()\n\n let previousListItem:\n | {\n listItem: string\n level: number\n }\n | undefined\n\n for (const block of blocks) {\n if (block === undefined || block._key === undefined) {\n continue\n }\n\n const blockPath: Path = [...basePath, {_key: block._key}]\n\n // A non-list block breaks the list run for this array. Containers are\n // descended into; each nested array numbers independently.\n //\n // Unlike the engine's internal `isTextBlockNode`, `isTextBlock` also\n // requires `children`, which holds for the post-apply snapshot values\n // this plugin reads.\n if (\n !isTextBlock(context, block) ||\n block.listItem === undefined ||\n block.level === undefined\n ) {\n levelIndexMaps.clear()\n previousListItem = undefined\n\n const childResult = getContainerChildren(\n context.containers,\n block,\n parent,\n )\n if (childResult) {\n collectListIndexes(\n context,\n childResult.children,\n [...blockPath, childResult.container.field.name],\n childResult.container,\n listIndexMap,\n )\n }\n\n continue\n }\n\n // If we encounter a new list item, we set the initial index to 1 for the\n // list type on that level.\n if (!previousListItem) {\n const listIndex = 1\n const levelIndexMap =\n levelIndexMaps.get(block.listItem) ?? new Map<number, number>()\n levelIndexMap.set(block.level, listIndex)\n levelIndexMaps.set(block.listItem, levelIndexMap)\n\n listIndexMap.set(serializePath(blockPath), listIndex)\n\n previousListItem = {\n listItem: block.listItem,\n level: block.level,\n }\n\n continue\n }\n\n // If the previous list item is of the same type but on a lower level, we\n // need to reset the level index map for that type.\n if (\n previousListItem.listItem === block.listItem &&\n previousListItem.level < block.level\n ) {\n const listIndex = 1\n const levelIndexMap =\n levelIndexMaps.get(block.listItem) ?? new Map<number, number>()\n levelIndexMap.set(block.level, listIndex)\n levelIndexMaps.set(block.listItem, levelIndexMap)\n\n listIndexMap.set(serializePath(blockPath), listIndex)\n\n previousListItem = {\n listItem: block.listItem,\n level: block.level,\n }\n\n continue\n }\n\n // Reset other list types at current level and deeper\n levelIndexMaps.forEach((levelIndexMap, listItem) => {\n if (listItem === block.listItem) {\n return\n }\n\n // Reset all levels that are >= current level\n const levelsToDelete: number[] = []\n\n levelIndexMap.forEach((_, level) => {\n if (block.level !== undefined && level >= block.level) {\n levelsToDelete.push(level)\n }\n })\n\n levelsToDelete.forEach((level) => {\n levelIndexMap.delete(level)\n })\n })\n\n const levelIndexMap =\n levelIndexMaps.get(block.listItem) ?? new Map<number, number>()\n const levelCounter = levelIndexMap.get(block.level) ?? 0\n levelIndexMap.set(block.level, levelCounter + 1)\n levelIndexMaps.set(block.listItem, levelIndexMap)\n\n listIndexMap.set(serializePath(blockPath), levelCounter + 1)\n\n previousListItem = {\n listItem: block.listItem,\n level: block.level,\n }\n }\n}\n","import {useEditor, type Editor, type Path} from '@portabletext/editor'\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from 'react'\nimport {buildListIndexMap, serializePath} from './build-list-index-map'\n\n/**\n * One store per editor: a single `operation` subscription maintains the\n * list index map, and per-path subscriber buckets make notification\n * O(changed paths) rather than O(subscribers).\n */\ntype ListIndexStore = {\n get: (serializedPath: string) => number | undefined\n subscribeKey: (serializedPath: string, callback: () => void) => () => void\n /**\n * Starts the `operation` subscription. Returns the unsubscribe.\n */\n subscribe: () => () => void\n}\n\nfunction createListIndexStore(editor: Editor): ListIndexStore {\n let listIndexMap = buildListIndexMap(editor.getSnapshot().context)\n const subscribers = new Map<string, Set<() => void>>()\n\n function rebuild() {\n const previousListIndexMap = listIndexMap\n\n // Swap before notifying: `useSyncExternalStore` re-reads the snapshot\n // synchronously on notification and skips the re-render when it reads\n // an unchanged (stale) value.\n listIndexMap = buildListIndexMap(editor.getSnapshot().context)\n\n for (const [serializedPath, callbacks] of subscribers) {\n if (\n previousListIndexMap.get(serializedPath) !==\n listIndexMap.get(serializedPath)\n ) {\n for (const callback of callbacks) {\n callback()\n }\n }\n }\n }\n\n return {\n get: (serializedPath) => listIndexMap.get(serializedPath),\n subscribeKey: (serializedPath, callback) => {\n let bucket = subscribers.get(serializedPath)\n\n if (bucket === undefined) {\n bucket = new Set()\n subscribers.set(serializedPath, bucket)\n }\n\n bucket.add(callback)\n\n return () => {\n bucket.delete(callback)\n\n if (bucket.size === 0) {\n subscribers.delete(serializedPath)\n }\n }\n },\n subscribe: () => {\n // Operations applied between store creation (render) and subscription\n // (effect) are not observed, so reconcile once up front.\n rebuild()\n\n // Batched delivery coalesces a burst (e.g. one operation per block\n // during a large delete, insert, or undo) into a single call carrying\n // the whole burst, so the map rebuilds at most once per burst instead\n // of once per operation. Text ops never change list structure; every\n // other document-changing op can (including deep edits inside a\n // container), so rebuild only when the burst contains one. Inspecting\n // the whole burst is why this needs `batch` rather than a last-event\n // delivery, which would drop a structural op whenever a text op\n // happened to be last in the burst.\n const subscription = editor.on(\n 'operation',\n (events) => {\n if (\n events.some(\n (event) =>\n event.operation.type !== 'insert.text' &&\n event.operation.type !== 'remove.text',\n )\n ) {\n rebuild()\n }\n },\n {batch: true},\n )\n\n return () => {\n subscription.unsubscribe()\n }\n },\n }\n}\n\nconst ListIndexContext = createContext<ListIndexStore | undefined>(undefined)\n\n/**\n * Maintains a list index map for the editor and serves it through context.\n * Mount inside `EditorProvider`, wrapping whatever reads the indices:\n *\n * ```tsx\n * <EditorProvider initialConfig={...}>\n * <ListIndexProvider>\n * <PortableTextEditable />\n * </ListIndexProvider>\n * </EditorProvider>\n * ```\n *\n * The map is rebuilt at most once per microtask burst of operations,\n * regardless of how many operations the burst contains or how many\n * components read it, and only when the burst contains an operation that can\n * affect list indices. Reads via {@link useListIndex} only re-render when the\n * index at their own path changes.\n *\n * @beta\n */\nexport function ListIndexProvider(props: {children?: ReactNode}) {\n const editor = useEditor()\n const store = useMemo(() => createListIndexStore(editor), [editor])\n\n useEffect(() => {\n return store.subscribe()\n }, [store])\n\n return (\n <ListIndexContext.Provider value={store}>\n {props.children}\n </ListIndexContext.Provider>\n )\n}\n\n/**\n * Read the 1-based list index of the block at `path`, or `undefined` when\n * the block is not a list item. Re-renders only when the index at this\n * path changes.\n *\n * `path` is the keyed block path render callbacks receive, e.g.\n * `[{_key: 'b0'}]`.\n *\n * @beta\n */\nexport function useListIndex(path: Path): number | undefined {\n const store = useContext(ListIndexContext)\n\n if (store === undefined) {\n throw new Error('useListIndex must be used below a <ListIndexProvider>')\n }\n\n // Callers typically pass a fresh `path` array every render, so the\n // serialized string, not the array, is what keeps `subscribe` stable\n // below. Memoizing on the array would never hit.\n const serializedPath = serializePath(path)\n\n const subscribe = useCallback(\n (callback: () => void) => store.subscribeKey(serializedPath, callback),\n [store, serializedPath],\n )\n\n const getSnapshot = () => store.get(serializedPath)\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/plugin-list-index",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Compute the list index of each list item for custom list rendering",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"list-index",
|
|
@@ -24,52 +24,43 @@
|
|
|
24
24
|
],
|
|
25
25
|
"type": "module",
|
|
26
26
|
"sideEffects": false,
|
|
27
|
-
"main": "./dist/index.js",
|
|
28
|
-
"module": "./dist/index.js",
|
|
29
27
|
"types": "./dist/index.d.ts",
|
|
30
28
|
"exports": {
|
|
31
29
|
".": "./dist/index.js",
|
|
32
30
|
"./package.json": "./package.json"
|
|
33
31
|
},
|
|
34
32
|
"devDependencies": {
|
|
35
|
-
"@
|
|
36
|
-
"@sanity/pkg-utils": "^12.1.0",
|
|
33
|
+
"@sanity/pkg-utils": "^12.3.0",
|
|
37
34
|
"@sanity/tsconfig": "^2.1.0",
|
|
38
35
|
"@types/react": "^19.2.17",
|
|
39
36
|
"@types/react-dom": "^19.2.3",
|
|
40
|
-
"@vitejs/plugin-react": "^6.0
|
|
37
|
+
"@vitejs/plugin-react": "^6.1.0",
|
|
41
38
|
"@vitest/browser": "^4.1.10",
|
|
42
39
|
"@vitest/browser-playwright": "^4.1.10",
|
|
43
|
-
"
|
|
44
|
-
"eslint": "^9.39.4",
|
|
45
|
-
"eslint-plugin-react-hooks": "^7.1.1",
|
|
40
|
+
"oxc-transform-react": "^0.145.0",
|
|
46
41
|
"react": "^19.2.8",
|
|
47
42
|
"react-dom": "^19.2.8",
|
|
48
|
-
"typescript": "
|
|
49
|
-
"typescript-eslint": "^8.62.0",
|
|
43
|
+
"typescript": "^7.0.2",
|
|
50
44
|
"vite": "^8.2.0",
|
|
51
45
|
"vitest": "^4.1.10",
|
|
52
46
|
"vitest-browser-react": "^2.2.0",
|
|
53
|
-
"@portabletext/editor": "^
|
|
54
|
-
"@portabletext/schema": "^
|
|
55
|
-
"@portabletext/test": "^
|
|
47
|
+
"@portabletext/editor": "^8.0.0",
|
|
48
|
+
"@portabletext/schema": "^3.0.0",
|
|
49
|
+
"@portabletext/test": "^2.0.0"
|
|
56
50
|
},
|
|
57
51
|
"peerDependencies": {
|
|
58
52
|
"react": "^19.2",
|
|
59
|
-
"@portabletext/editor": "^
|
|
53
|
+
"@portabletext/editor": "^8.0.0"
|
|
60
54
|
},
|
|
61
55
|
"engines": {
|
|
62
|
-
"node": ">=
|
|
56
|
+
"node": ">=22.12"
|
|
63
57
|
},
|
|
64
58
|
"scripts": {
|
|
65
59
|
"build": "pkg-utils build --strict --check",
|
|
66
|
-
"check:lint": "biome lint .",
|
|
67
|
-
"check:react-compiler": "eslint .",
|
|
68
60
|
"check:types": "tsc",
|
|
69
61
|
"check:types:watch": "tsc --watch",
|
|
70
62
|
"clean": "del .turbo && del dist && del node_modules",
|
|
71
63
|
"dev": "pkg-utils watch",
|
|
72
|
-
"lint:fix": "biome lint --write .",
|
|
73
64
|
"test:browser": "vitest --run --project browser",
|
|
74
65
|
"test:browser:chromium": "vitest --run --project \"browser (chromium)\"",
|
|
75
66
|
"test:unit": "vitest --run --project unit",
|