Skip to content

PT-4730: Limit how much text can be copied from a chapter - #2868

Merged
irahopkinson merged 3 commits into
mainfrom
pt-4730-resource-copy-limit
Oct 19, 2026
Merged

irahopkinson merged 3 commits into
mainfrom
pt-4730-resource-copy-limit

Conversation

@irahopkinson

@irahopkinson irahopkinson commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Branch: pt-4730-resource-copy-limit

Base: origin/main

Date: 2026-09-28, updated 2026-09-30 after review

Review model: Claude Opus 5.5

Files changed: 54 (9 generated: the lib/platform-bible-react and lib/platform-bible-utils dist/ files)

Overview

PT-4730 protects the rights of text owners by limiting how much text can be copied at once from protected resource texts, as P9 does. This PR is the second of a set of three:

  1. scripture-editors#23 adds the editor mechanism, EditorOptions.copyLimit.
  2. This PR asks the project for each chapter's limit and passes it to the copy paths in the Scripture editor extension.
  3. The third PR, outside this repo, decides which texts are limited and by how much.

Here the limit is only enabled, never applied: the public ResourceCopyLimit always answers "no limit".

  • The data type: a new project interface, platformScripture.CopyLimit, with a read-only BookCopyLimits data type. Its selector names a book. It returns the per-chapter limits in UTF-16 code units, indexed like the chapter-text endpoints, by the project's own chapter numbers.

  • The C# side: ParatextProjectDataProvider serves it straight from ResourceCopyLimit. Every data update the provider sends also refreshes BookCopyLimits, through a new overridable DataProvider.ExpandDataUpdateScope hook (ADR adr-data-update-scope-hook).

  • The extension side: useChapterCopyLimit resolves a chapter's limit, and it is wired into the places text can be copied from:

    • the main Scripture editor, the Resource Viewer and the model text panel;
    • the Text Collection's grid cells, whose existing right-click Copy now applies the limit and copies only the verse text, without the resource name label;
    • the footnotes pane;
    • the footnote editor's Copy button.

    Copying is blocked (limit 0) while a chapter or its limit is loading, or when the limit can't be read. While the cursor is in a chapter's introduction (chapter 0, shown as the start of chapter 1), chapter 1's limit applies.

  • Other changes: the Select All suppression is added to the keyboard-shortcuts catalog, and the decision to keep the rule downstream is recorded in the ADR adr-copy-limit-rule-lives-downstream.

Merge order: scripture-editors#23 is merged and platform-yalc has moved, so main already has the new editor and its unicode-segmenter lockfile entry. The branch is rebased onto main, and the lockfile commit dropped out as empty.

Changes after review

The changes are in their own commit, on top of the feature commit:

  • Indexing: the versification mapper is removed. Limits are indexed like the chapter-text endpoints, by the project's own chapter numbers, so the limit and the text always refer to the same chapter. The selector's versification is documented as ignored, and the hook subscribes once per book.
  • Chapter 0: while the cursor is in a chapter's introduction, chapter 1's limit applies, since the introduction is shown as the start of chapter 1.
  • The seam for the downstream implementation:
    • ResourceCopyLimit's remarks state what an implementation must do: cover every chapter, and return an array it won't change later.
    • A per-provider test seam pins what GetBookCopyLimits passes and returns.
    • A reflection test pins the frozen signature.
  • The update-scope hook: typed as List<string>, with the base class decoding "*", blank and empty scopes first. It moved out of the Scripture-methods region and is recorded as a pattern in adr-data-update-scope-hook.
  • The footnotes pane: a copy that fits is left to the browser, so it keeps its formatting. A null clipboardData stays blocked rather than throwing. This is covered by unit tests only: when tested on 30 Sep, before PT-4189: Edit footnotes in the footnotes pane in Standard view #2805 reworked the pane, it dropped a selection as soon as it was made, on main too, so nothing could be copied from it. That was a separate, pre-existing bug.
  • The provider type: ICopyLimitProjectDataProvider declares its members, with TSDoc.
  • Tests and cleanup:
    • A test confirms that a copy spanning two grid cells is shortened by the editor's page guard.
    • A shared test util resets the lookup cache.
    • The zoom contract test pins ref={contentRef}.
    • The grid cell uses the corrected loading flag.
    • The segmentation contract is stated on truncateToCopyLimit.
    • The selection round-trip in the grid cell and the footnote editor's clipboard write are wrapped in try/catch.
    • The unit ("UTF-16 code units") is used everywhere.
  • Not changed: the lookup-failure recovery the review traced, where a pane could stay at 0 until it remounts. None of those cases has been observed, so they'll be fixed when one is.

Changes after the rebase

main moved a long way while this waited for platform-yalc, so the branch is rebased onto it. #2805 restructured the footnotes pane, so the pane's copy handling was ported into its new FootnotesPane component, not merged textually. One new commit fits the copy limit to what main changed:

  • The grid cell view: main added a contentRef prop. The view's own copy ref is now copyAreaRef, and the header layout's content area takes both.
  • The footnotes pane's row editor (new in PT-4189: Edit footnotes in the footnotes pane in Standard view #2805) gets the copy-limited editor options, as the footnote popover's editor does.
  • The footnotes pane leaves alone a copy that a row editor has already handled with its own copy guard, instead of rewriting it as plain text. A test pins this, and it fails without the change.

API Changes

  • extensions/src/platform-scripture/src/types/platform-scripture.d.ts (platform-scripture module): added CopyLimitProjectInterfaceDataTypes, with one data type, BookCopyLimits: DataProviderDataType<SerializedVerseRef, (number | undefined)[] | undefined, never>. Its TSDoc states the unit, UTF-16 code units.
  • platform-scripture module: added ICopyLimitProjectDataProvider, declaring getBookCopyLimits, setBookCopyLimits (always throws) and subscribeBookCopyLimits, with TSDoc. The selector's versification, chapter and verse are ignored.
  • papi-shared-types augmentation: added 'platformScripture.CopyLimit': ICopyLimitProjectDataProvider to ProjectDataProviderInterfaces.
  • lib/platform-bible-utils: added the export truncateToCopyLimit(text: string, copyLimit: number | undefined): string. It returns text unchanged when copyLimit is undefined.
  • lib/platform-bible-react: no signature or export changes. FootnoteEditor's Copy button now shortens its text to editorOptions.copyLimit when one is set.
  • C# backend:
    • platformScripture.CopyLimit is listed in projectInterfaces for every Paratext project, published and unpublished. It registers getBookCopyLimits, and a setBookCopyLimits that always throws.
    • Every data-update event from ParatextProjectDataProvider also names BookCopyLimits.
    • NetworkObjects/DataProvider gains protected virtual List<string> ExpandDataUpdateScope(List<string> dataTypes). The base class decodes the scope first: "*" never reaches the hook, and nothing is sent for a blank data type or an empty list.
  • lib/papi-dts/papi.d.ts: unchanged.

Findings

Critical — Must address before merge

None.

Important — Should address before merge

  • Nothing enforced the copy-limit refresh on data updates. Every update had to go through a private wrapper, while the base SendDataUpdateEvent stayed callable. The wrapper's comment was also wrong about the "*" update. (fixed during review:
    • DataProvider has a new protected virtual ExpandDataUpdateScope, called on every update's scope, and ParatextProjectDataProvider overrides it once to add BookCopyLimits. "*", an empty list and a blank string pass through unchanged;
    • the wrapper is gone, and the ~25 call sites match main again;
    • a new test pins that the full-project update is still sent as "*", and disabling the override fails the three refresh tests.)
  • The limit's unit wasn't stated. The docs said "characters", but everything that enforces the limit counts UTF-16 code units, and the private part-3 provider implements this contract. (fixed during review: the BookCopyLimits TSDoc and the C# ResourceCopyLimit and GetBookCopyLimits summaries now say UTF-16 code units.)
  • Copying is blocked in the user's own editable project while its limit loads, and when a lookup fails. (Intentional, per the spec: "Unknown limit: blocks copying for every text while loading or on failure." The author: "Intentional for now to give the most protection where it's needed for resources. We can look again if it proves to be an issue.")

Author response: the author chose the base-class hook over a source-scan test or an analyzer, because it makes skipping the refresh impossible and shrinks the diff. They asked that reviewers look at the base-class change (see Suggested Review Focus).

Minor — Consider

  • The same copyLimit === undefined ? text : truncateToCopyLimit(...) ternary appeared three times. (fixed during review: truncateToCopyLimit accepts number | undefined, with a new test. The three call sites drop the ternary, and both libraries' dist/ are rebuilt.)

  • About 20 lines of chapter and copy-limit wiring were repeated in the resource text panel and the grid cell. (fixed during review: a new copy-limit/use-chapter-usj-with-copy-limit.hook.ts, with tests, is used by both. It passes the same provider to ChapterUSJ and useIsProjectDataLoading, and its tests fail when it doesn't.)

  • options carried a partly applied copyLimit in platform-scripture-editor.web-view.tsx, and a TSDoc block there wasn't attached to anything. (fixed during review:

    • copyLimit is now a local from useChapterCopyLimit, and editorOptionsWithCopyLimit is { ...options, copyLimit };
    • copy-limit-wiring.contract.test.ts pins the new form;
    • the comment is fixed.)
  • getModelResourceProjectId lived in a component file that the web view imported from. (fixed during review: moved unchanged into resource-reference.utils.ts.)

  • CopyLimitVersificationMapper rebuilds its versification map on every request when either versification is customized. (Superseded: the mapper was removed after review. See "Changes after review". At the time: left as it is. Neither cheap fix is safe:

    • caching the map could go stale, because customized versifications are changed in place (ManageBooksService) and have no change token;
    • looking up every book's limits first would move the cost into the private build.

    Measured during manual testing (see "Other notes for reviewers"): about 5–22 ms per request, and only when a view sends a different versification than the text's own, which none of the current views does.)

  • getResourceChapter in model-text-panel.web-view.tsx now fetches the USJ and the text direction in parallel. (Kept. See "Other notes for reviewers".)

  • No story showed the disabled Copy item. (fixed during review: a new CopyLimitedMenuOpen story renders a cell with copyLimit={0}, selects a verse, opens the right-click menu and checks that Copy is disabled.)

Author response: the author asked for fixes to five of the seven, and for other reasonable deduplication. The subagent's decisions on the other candidates:

  • the model-text panel's plumbing works differently and shares no code with the new hook;
  • the repeated test fixtures are about 8 lines each, keyed differently;
  • the three copy handlers already share truncateToCopyLimit.

Template Propagation

Shared Regions Modified

None. Seven changed files contain the words "shared with", but only in ordinary comments, not #region shared with markers.

Extension Config Changes

None.

Other notes for reviewers

  • Blocking in the user's own project. Copy and Ctrl+A/⌘A are blocked in every text while its chapter or copy limit is loading, and when the limit can't be read. That includes the user's own translation in the main editor, not only resources. This is deliberate, to give the most protection to resources, and can be revisited if it proves to be a problem.
  • Silence. A blocked copy or Select All gives no feedback. That's a deliberate exception to "Never fail silently", for P9 parity, and is also recorded in scripture-editors#23.
  • Unrelated but harmless: getResourceChapter in the model-text web view now fetches the chapter's USJ and text direction in parallel (Promise.all), which shortens the time copying is blocked while a chapter loads.
  • Low findings left open from the local review rounds:
    • Multi-cell right-click Copy. Selecting across several Text Collection cells and choosing a cell's right-click Copy copies only that cell's part, even with no limit, so Ctrl+C and right-click Copy can put different text on the clipboard.
    • A stale read in the shared data hook. Every useProjectData consumer still reads the previous selector's or provider's data as settled for the render that changes it. That's pre-existing in create-use-data-hook.util.ts. The copy-limit paths work around it with useIsProjectDataLoading.

Positive Observations

  • The public repo holds only the mechanism. ResourceCopyLimit.cs is a single placeholder that always answers "no limit". Its remarks look forward: they warn not to change the signature, because the downstream patch depends on it. The BookCopyLimits TSDoc describes only what a caller can observe.
  • The new data type follows the existing read-only conventions:
    • the setter type is never, and SetBookCopyLimits throws NotSupportedException;
    • the constants follow the ProjectInterfaces / ProjectDataType layout;
    • a null C# array entry arrives as undefined in TypeScript, matching the declared type.
  • Test coverage is thorough at every layer:
    • truncateToCopyLimit, including Khmer conjuncts under a segmenter that splits them;
    • resolveCopyLimit, useChapterCopyLimit (with its lookup cache and failure retry), useIsProjectDataLoading and the new shared hook;
    • the model-text and resource-text web views, and the grid cells, including selections spanning several cells;
    • the footnotes-pane and footnote-editor Copy paths;
    • on the C# side, the placeholder and its frozen signature, the read-only setter, update events, the update-scope hook and the interface list.
  • The wiring contract test catches what jsdom can't. copy-limit-wiring.contract.test.ts checks the JSX props of a web view that can't be mounted in jsdom, using the TypeScript AST.
  • The copy-limit props are required rather than optional, so every caller has to state a limit.
  • The metadata lookup in useChapterCopyLimit follows adr-provider-lookup-is-a-fan-out: the promise is cached per project, and a failure is retried after 30 seconds while the view stays open.
  • The limit is applied the same way on every copy path: the editor, the footnotes pane, the footnote-editor Copy button and the grid cell menu. The grid menu applies it both when it opens and when Copy is chosen.
  • Conventions are followed:
    • the keyboard-shortcuts catalog is updated in the same change, with command left unset;
    • there are no new user-visible strings, and the grid's Copy item reuses COPY_KEY;
    • the ADR sits in its byte-order slug position;
    • comments look forward.

Interview Notes

  • Purpose: replicate P9's copy limit on protected resource texts in P10. This PR is the second of three. It enables the behaviour in core without applying any limit.
  • Design, in the author's words: "I followed the code in P9 … so we are doing the same in core. The changes here are just about enabling the behavior but don't actually do any limiting. The limiting code and which resources are not limited will be in a 3rd PR in the private P10 repo."
  • The update-event invariant: the author weighed three options after asking what patterns the repo already has: a source-scan test like SendReceiveWriteLockCoverageTests, a Roslyn analyzer, and an overridable base-class hook like DataProvider's existing protected virtual methods. They chose the hook, and asked that reviewers look at the base-class change.
  • Blocking while loading: intentional for now, to give the most protection where it's needed, for resources. To revisit if it proves to be a problem.
  • The versification mapper: removed after review, because the chapter-text endpoints fetch by the project's own chapter numbers.
  • The duplication: the author asked for the shared hook "and any other reasonable calls for the code to be DRYer".
  • No unresolved items, and no areas the author deferred.

In-Review Quality Check

These checks ran after all the in-review changes, judged by exit code. No fixes were needed.

  • Types: npm run typecheck exits 0 (all four sub-checks).

  • Lint: npm run lint exits 0: no errors, and one warning in a file this change doesn't touch.

  • Formatting: prettier left every changed file unchanged.

  • C#: dotnet test c-sharp-tests/ passes 1,788 tests, with 7 skipped and none failing. dotnet csharpier --check . passes.

  • TypeScript tests: npm test exited 1 on this machine, but not because of the code. Two things on the machine caused it:

    • A reserved port. Windows reserves ports 63310–63409 here, so vitest's browser-test server couldn't start for platform-bible-react.
    • A stale cache. The Storybook cache still held an older build of platform-bible-utils. The committed dist/ is current.

    In that run, every other workspace passed. That includes core (6,280 tests), platform-bible-utils (678), platform-scripture (679) and platform-scripture-editor (3,366).

    platform-bible-react's tests were run separately:

    • its unit and script tests: 1,351 passed;
    • its Storybook tests, through a temporary config that moves the browser port: 609 passed, once the cache was rebuilt.
  • The new story: CopyLimitedMenuOpen is an extension story, and no automated suite runs those. The author checked it by hand in Storybook: selecting text and right-clicking opens the menu with Copy disabled.

  • papi.d.ts: unchanged.

Suggested Review Focus

  • The new DataProvider.ExpandDataUpdateScope hook. It changes the shared base class of every C# data provider (Time, Inventory, DBL, Internet settings, CheckRunner and the project providers). The base class now decodes "*", blank and empty scopes before the hook, and only ParatextProjectDataProvider overrides it. Check that the other providers are unaffected.
  • Blocking in the user's own translation while a chapter or its limit loads, or when a lookup fails. Is that an acceptable trade-off for protecting editable resources?
  • Indexing: limits are indexed by the project's own chapter numbers, like the chapter-text endpoints, and chapter 0 uses chapter 1's limit.
  • useChapterCopyLimit and useIsProjectDataLoading: when the limit is 0, the lookup cache and retry, and the same-provider rule in the new shared hook.
  • The contract for part 3: is the BookCopyLimits TSDoc, and the C# ResourceCopyLimit summary, enough for a downstream implementation?

🤖 Generated with Claude Code


This change is Reviewable

@katherinejensen00 katherinejensen00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full review sent directly to avoid sharing potentially sensitive info.

@katherinejensen00 reviewed 54 files and all commit messages, and made 2 comments.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on irahopkinson).


extensions/src/platform-scripture-editor/src/copy-limit/resolve-copy-limit.util.ts line 18 at r1 (raw file):

}): number | undefined {
  if (isLoading || isPlatformError(value)) return 0;
  return value?.[chapterNum] ?? undefined;

2. Chapter 0 — book front matter — is copyable without limit

Severity: Medium (fail-open)
Attach to: extensions/src/platform-scripture-editor/src/copy-limit/resolve-copy-limit.util.ts:18 — return value?.[chapterNum] ?? undefined;

Index 0 is documented unused on both sides, so value[0] is always a hole, and a hole resolves to undefined — no limit — in a design where every other unknown resolves to 0.

chapterNum: 0 is reachable, and your own code says so. platform-scripture-editor.utils.ts:1558-1571: "It is reachable rather than theoretical: calculateTopMatch('GEN 0') yields chapterNum: 0 and handleTopMatchSelect passes it through with ?? 1, which a 0 survives." The same file notes chapter 0 addresses \id, \h, \toc, \mt, \ip — "real content the reader can see", and substantial in study Bibles and commentaries. There is a front-matter branch at platform-scripture-editor.web-view.tsx:3737.

The C# side already guards this: LimitAt is limits != null && chapter >= 1 && chapter < limits.Length. The TS side dropped the same bound. Same hole for any chapterNum > GetLastChapter(book).

Note that resolve-copy-limit.util.test.ts:20 currently pins the permissive behaviour (chapterNum: 9 → toBeUndefined()), so the test will need to change with the fix.

@irahopkinson
irahopkinson force-pushed the pt-4730-resource-copy-limit branch from 0866858 to 23ddc3e Compare September 30, 2026 01:53

@irahopkinson irahopkinson left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Katherine, and thanks for sending the details privately. The PR is rebased onto main. There's now a lockfile commit (the editor's new unicode-segmenter dependency) and a commit addressing your review. Using your numbering:

  • 1: the versification mapper is gone. Limits are indexed like the chapter-text endpoints, by the project's own chapter numbers, so the limit and the text always match. 14 and 21 go with it.
  • 2: chapter 0 now uses chapter 1's limit. See the thread on it.
  • 6 and 20: ResourceCopyLimit's remarks state the length and don't-change-it-later obligations. There's a per-provider test seam, and a reflection test that pins the frozen signature.
  • 10: ICopyLimitProjectDataProvider declares its members, with TSDoc.
  • 11: ExpandDataUpdateScope now takes and returns List<string>. The base class decodes "*", blank and empty scopes before the hook. It's out of the Scripture region, and it's recorded as a pattern in adr-data-update-scope-hook.
  • 16: a footnotes-pane copy that fits is now left to the browser. Note that you can't copy from the footnotes pane in the app at the moment, though, because the selection is dropped as soon as it's made. That's the same on main, so it's a separate, pre-existing bug. The change is covered by unit tests.
  • 4: confirmed covered. A new test shows a copy spanning two cells is shortened by the editor's page guard.
  • 9, 17, 18 and 23: done: the segmentation contract, a shared reset of the lookup cache, the zoom test pinning contentRef, and the grid cell's corrected loading flag.
  • 8 and 15: the description wording is fixed.
  • 3, 5, 12 and 13: these are traced in code but not observed, so I've left them for now. We'll fix any lookup-failed case we find, which also takes 0 back to meaning only "loading" or "a real limit" (your point in 19). I've skipped 19's new message, because the Product Owner chose silence.
  • 7: the editor blocks a cut at 0, so nothing is removed.
  • 22: left as it is, at about 1 ms per subscriber per write.
  • 24: the unit wording, the try/catch guards, the catalog locations and the test comment are done. The other nits are left.

The CI will fail until platform-yalc moves. I'll do that once this is approved.

@irahopkinson+AI made 2 comments.
Reviewable status: 18 of 61 files reviewed, 1 unresolved discussion (waiting on katherinejensen00).


extensions/src/platform-scripture-editor/src/copy-limit/resolve-copy-limit.util.ts line 18 at r1 (raw file):

Previously, katherinejensen00 wrote…

2. Chapter 0 — book front matter — is copyable without limit

Severity: Medium (fail-open)
Attach to: extensions/src/platform-scripture-editor/src/copy-limit/resolve-copy-limit.util.ts:18 — return value?.[chapterNum] ?? undefined;

Index 0 is documented unused on both sides, so value[0] is always a hole, and a hole resolves to undefined — no limit — in a design where every other unknown resolves to 0.

chapterNum: 0 is reachable, and your own code says so. platform-scripture-editor.utils.ts:1558-1571: "It is reachable rather than theoretical: calculateTopMatch('GEN 0') yields chapterNum: 0 and handleTopMatchSelect passes it through with ?? 1, which a 0 survives." The same file notes chapter 0 addresses \id, \h, \toc, \mt, \ip — "real content the reader can see", and substantial in study Bibles and commentaries. There is a front-matter branch at platform-scripture-editor.web-view.tsx:3737.

The C# side already guards this: LimitAt is limits != null && chapter >= 1 && chapter < limits.Length. The TS side dropped the same bound. Same hole for any chapterNum > GetLastChapter(book).

Note that resolve-copy-limit.util.test.ts:20 currently pins the permissive behaviour (chapterNum: 9 → toBeUndefined()), so the test will need to change with the fix.

Thanks, fixed. Chapter 0 (the introduction) is shown as the start of chapter 1, so while the cursor is in it the views now use chapter 1's limit. That's in resolveCopyLimit, with a test for chapter 0 that failed before the fix. A chapter past the end of the book has no text, so a missing entry there still means no limit, and the permissive test is split to say so.

@katherinejensen00 katherinejensen00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making those changes, Ira!
:lgtm:

@katherinejensen00 reviewed 43 files and all commit messages, made 1 comment, and resolved 1 discussion.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved.

@irahopkinson
irahopkinson marked this pull request as ready for review October 19, 2026 18:10
irahopkinson and others added 2 commits October 20, 2026 09:48
Adds the public mechanism for a per-chapter copy limit. The public build
answers "no limit" everywhere; a downstream implementation supplies the
answer.

- C#: `ResourceCopyLimit.GetBookCopyLimits` placeholder (the seam a
  downstream patch replaces) exposed as a new read-only project interface,
  `platformScripture.CopyLimit` / `BookCopyLimits`, counted in UTF-16 code
  units. The selector names the book and the caller's versification;
  `CopyLimitVersificationMapper` re-indexes the per-project-chapter answer
  into that versification, mapping in both directions and across books,
  and taking the smallest limit when a requested chapter spans several
  project chapters. Subscribers are refreshed with every update the
  provider sends, through a new overridable
  `DataProvider.ExpandDataUpdateScope` hook.
- Views: `useChapterCopyLimit` resolves the chapter's limit from the
  project's copy-limit provider (0 while unknown or failed), and
  `useIsProjectDataLoading` keeps it at 0 until the chapter text on screen
  matches it; `useChapterUsjWithCopyLimit` combines them with the chapter
  text for the resource text panel and grid cells. The Scripture editor
  (including its footnotes pane and the footnote editor's Copy button),
  resource text panel, Scripture Text Grid cells (including the grid's own
  right-click Copy) and model text panel pass it to the editor as
  `EditorOptions.copyLimit`.
- `truncateToCopyLimit` in platform-bible-utils cuts plain-text copies at
  character-cluster boundaries (unicode-segmenter).
- Keyboard shortcuts catalog entry for the blocked Select All shortcut.
- ADR `adr-copy-limit-rule-lives-downstream`.

Requires the `EditorOptions.copyLimit` release of the scripture-editors
platform editor.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
C# data provider
- BookCopyLimits is indexed like the chapter-text endpoints, by the
  project's own chapter numbers: GetBookCopyLimits passes only the book
  to ResourceCopyLimit.GetBookCopyLimits and returns its answer as it is.
  The selector's versification, chapter and verse are ignored.
  CopyLimitVersificationMapper and its tests are removed.
- GetBookCopyLimits calls the seam through an internal
  BookCopyLimitsSource property, so tests pin the book it asks for and
  that it returns the seam's answer unchanged; another test pins that
  the property defaults to ResourceCopyLimit.GetBookCopyLimits.
- ResourceCopyLimit's remarks state what an implementation owes: an
  array covering every chapter of the book, and one it will not change
  after returning it. A reflection test pins the class's name and
  namespace and the method's name, static-ness, parameter types and
  nullable return type.
- DataProvider.ExpandDataUpdateScope takes and returns List<string>.
  SendDataUpdateEventAsync decodes the scope first: "*" is sent without
  reaching the hook, nothing is sent for a blank data type or an empty
  list (before or after the hook), and the hook gets a copy of the
  caller's list. New tests cover the decoding. ParatextProjectDataProvider's
  override moves to the provider-wide region and chains to the base.

Views
- A reference in chapter 0 takes chapter 1's limit, since chapter 0 is
  shown as the start of chapter 1. A chapter past the end of the book
  has no entry, and so no limit, like any other missing entry.
- useChapterCopyLimit subscribes once per book, whatever the reference's
  versification.
- The footnotes pane lets a copy within the limit through natively, with
  all its clipboard formats; only a copy that needs shortening is
  written, as plain text, and one with no clipboard data stays blocked.
- useChapterUsjWithCopyLimit also returns isChapterTextLoading, and the
  Text Collection cell derives its state from it, so a cell never shows
  the previous chapter's or resource's text as ready.
- The grid cell's selection reader returns '' for a selection it cannot
  read, so the right-click menu still opens with Copy disabled.
- The footnote editor's Copy button awaits the clipboard write and logs
  a failure instead of leaving it unhandled.
- New test: a copy whose selection runs from one Text Collection cell
  into another, each with a real editor and a limit, is shortened to
  the smaller limit.

Types, docs and tests
- ICopyLimitProjectDataProvider declares getBookCopyLimits, the
  always-rejecting setBookCopyLimits and subscribeBookCopyLimits, with
  TSDoc, including what a limit of 0 means.
- truncateToCopyLimit's TSDoc states its segmentation contract.
- Limits are described in UTF-16 code units in the footnotes pane and
  grid cell prop docs and in the ADR.
- ADR: adr-copy-limit-rule-lives-downstream records the indexing and
  chapter 0; new entry adr-data-update-scope-hook.
- A shared copy-limit test util resets the lookup cache before each
  test in the grid cell and resource text panel suites.
  platform-scripture-editor's tsconfig excludes *.test-utils files, as
  platform-scripture's does, since they import vitest.
- The zoom contract test pins ref={contentRef} in both cell layouts.
- The keyboard shortcuts catalog entry lists
  use-chapter-usj-with-copy-limit.hook.ts.
- Rebuilt platform-bible-utils and platform-bible-react dist.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@irahopkinson
irahopkinson force-pushed the pt-4730-resource-copy-limit branch from 23ddc3e to 3bc290c Compare October 19, 2026 21:46
Main, since the branch was last rebased, added a `contentRef` prop to the
grid cell view and an editable row editor to the footnotes pane:

- The grid cell view's own copy ref is now `copyAreaRef`; the header
  layout's content area takes it and the caller's `contentRef` both. The
  first commit does not typecheck on its own without this (squash-merged).
- The footnotes pane's row editor gets the copy-limited editor options, as
  the footnote popover's editor does.
- The footnotes pane leaves alone a copy a row editor has already handled
  with its own copy guard, rather than rewriting it as plain text.
- The copy-limit wiring and Text Collection zoom-marker contract tests
  follow: both footnote editors take the copy-limited options, and the
  cell's scroll box takes `copyAreaRef`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@irahopkinson
irahopkinson force-pushed the pt-4730-resource-copy-limit branch from 3bc290c to 40ee822 Compare October 19, 2026 22:28
@irahopkinson
irahopkinson enabled auto-merge (squash) October 19, 2026 23:05
@irahopkinson
irahopkinson merged commit 378ebe8 into main Oct 19, 2026
6 of 7 checks passed
@irahopkinson
irahopkinson deleted the pt-4730-resource-copy-limit branch October 19, 2026 23:21

@irahopkinson irahopkinson left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@irahopkinson+AI made 1 comment.
Reviewable status: 27 of 67 files reviewed, 1 unresolved discussion (waiting on irahopkinson and katherinejensen00).


a discussion (no related file):
Thanks for the approval, Katherine. main had moved a long way while this waited on platform-yalc, so I've rebased it and force-pushed, which is why your files show as unreviewed again. Nothing in the first two commits changed beyond what the rebase needed. The one exception is the footnotes pane, which #2805 restructured, so our copy handling there was ported into its new FootnotesPane component rather than merged.

What's new is one commit on top, 40ee822631c, fitting the copy limit to what main changed:

  • Grid cell view: main added a contentRef prop. The view's own copy ref is now copyAreaRef, and the header layout's content area takes both.
  • Footnotes pane row editor (new in #2805): it gets the copy-limited editor options, as the popover's editor does.
  • Footnotes pane: it leaves alone a copy a row editor has already handled with its own copy guard, instead of rewriting it as plain text.

The lockfile commit dropped out, since main already has that entry. The PR description has a new "Changes after the rebase" section with the same detail. Could you take a look at that commit and the footnotes pane port?

@irahopkinson irahopkinson left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@irahopkinson+AI made 1 comment.
Reviewable status: 27 of 67 files reviewed, 1 unresolved discussion (waiting on irahopkinson and katherinejensen00).


a discussion (no related file):

Previously, irahopkinson+AI (Agent AI for Ira Hopkinson) wrote…

Thanks for the approval, Katherine. main had moved a long way while this waited on platform-yalc, so I've rebased it and force-pushed, which is why your files show as unreviewed again. Nothing in the first two commits changed beyond what the rebase needed. The one exception is the footnotes pane, which #2805 restructured, so our copy handling there was ported into its new FootnotesPane component rather than merged.

What's new is one commit on top, 40ee822631c, fitting the copy limit to what main changed:

  • Grid cell view: main added a contentRef prop. The view's own copy ref is now copyAreaRef, and the header layout's content area takes both.
  • Footnotes pane row editor (new in #2805): it gets the copy-limited editor options, as the popover's editor does.
  • Footnotes pane: it leaves alone a copy a row editor has already handled with its own copy guard, instead of rewriting it as plain text.

The lockfile commit dropped out, since main already has that entry. The PR description has a new "Changes after the rebase" section with the same detail. Could you take a look at that commit and the footnotes pane port?

This auto-merged on your earlier approval just before my comment went out, so the new commit hasn't had your eyes on it. A look is still very welcome. If you spot anything, I'll fix it in a follow-up PR.

timothy-mccormack added a commit that referenced this pull request Oct 20, 2026
Resolves resource-reference.utils.ts, where this branch's
isSameResource and PT-4730's getModelResourceProjectId (#2868) were
added at the same place: both kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jolierabideau added a commit that referenced this pull request Oct 20, 2026
…ollows-versification

Resolve the conflict with PT-4730's copy-limit tests (#2868) in
resource-text-panel.web-view.test.tsx. The panel now follows the scroll
group through useScrollGroupScrRef with the resource's project id, so the
copy-limit helper sets the chapter on that mock instead of through the
useWebViewScrollGroupScrRef prop, which the panel no longer reads.
Without that, "blocks copying on the render that changes chapter" never
changed chapter and failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@katherinejensen00

Copy link
Copy Markdown
Contributor

Thanks, @irahopkinson. I looked at 40ee822 and the footnotes pane port against the head I approved. The port is faithful, the defaultPrevented gate is sound (a selection reaching outside the row editor still falls to the pane), and the copyAreaRef/contentRef split matches where main put contentRef. Nothing needs a follow-up from me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants