@erdemtuna/doc-review 0.7.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/LICENSE +21 -0
- package/README.md +117 -0
- package/package.json +57 -0
- package/src/SKILL.md +143 -0
- package/src/anchor-text.js +115 -0
- package/src/chrome-client.js +986 -0
- package/src/chrome-session.js +15 -0
- package/src/chrome.css +458 -0
- package/src/chrome.html +83 -0
- package/src/cli.js +341 -0
- package/src/click-target.js +26 -0
- package/src/editing.js +99 -0
- package/src/frame-channel.js +44 -0
- package/src/frame-policy.js +16 -0
- package/src/html-transform.js +62 -0
- package/src/markdown.js +87 -0
- package/src/paths.js +87 -0
- package/src/sdk.js +1520 -0
- package/src/serialize.js +46 -0
- package/src/server-entry.js +26 -0
- package/src/server-lock.js +101 -0
- package/src/server.js +1268 -0
- package/src/setup.js +117 -0
- package/src/state.js +491 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Peter Yang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Doc Review
|
|
2
|
+
|
|
3
|
+
Edit HTML and Markdown files directly, leave comments like a Google Doc, and send all your feedback to your AI agent at once.
|
|
4
|
+
|
|
5
|
+
[Read the original Human Review launch post](https://creatoreconomy.so/p/use-my-human-review-skill-to-edit-html-markdown-visually)
|
|
6
|
+
|
|
7
|
+
https://github.com/user-attachments/assets/7cab09c9-eaa0-4e8b-984d-2925e810b5c2
|
|
8
|
+
|
|
9
|
+
## Problem
|
|
10
|
+
|
|
11
|
+
Giving AI feedback on files in chat is painful.
|
|
12
|
+
|
|
13
|
+
Sometimes you want to change one sentence yourself. Instead, you end up typing:
|
|
14
|
+
|
|
15
|
+
> In the third paragraph, change X to Y. Cut the third card because it repeats the first one. Also rewrite the CTA.
|
|
16
|
+
|
|
17
|
+
Then the agent changes the file and you have to check whether it understood every instruction. This gets even harder when you’re reviewing a long plan, Markdown document, landing page, or multi-page website.
|
|
18
|
+
|
|
19
|
+
## How to install /doc-review
|
|
20
|
+
|
|
21
|
+
The easiest way to install the skill is to paste this into ChatGPT, Claude Code, Codex, or your favorite coding agent:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
Install the /doc-review skill globally from https://github.com/erdemtuna/doc-review
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
You can also install it with `npx`:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx -y @erdemtuna/doc-review setup --global
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This fork uses the `doc-review` command, `/doc-review` skill, and
|
|
34
|
+
`~/.doc-review` state directory. It does not install a `human-review` alias or
|
|
35
|
+
migrate upstream state and skill files; remove those old files manually if you
|
|
36
|
+
no longer need them.
|
|
37
|
+
|
|
38
|
+
## How to use /doc-review
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
Open an HTML or Markdown file:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
/doc-review (your file)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Review a page running on localhost:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
/doc-review (localhost URL)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Doc Review opens the file in your browser. Make direct edits, leave comments, and click Send. Your agent receives all your feedback in one batch, updates the source, and refreshes the page for another review.
|
|
55
|
+
|
|
56
|
+
Note: For HTML files, direct edits and resizes save automatically. For Markdown and localhost pages, click Send so your agent can apply them to the source.
|
|
57
|
+
|
|
58
|
+
File and rendered Markdown reviews run with authored scripts and inline handlers
|
|
59
|
+
blocked, an opaque iframe origin, and no popup or download permission. Their
|
|
60
|
+
per-render message capability is rotated for every load and navigation. It is
|
|
61
|
+
kept out of document URLs, HTML attributes, authored DOM, and global JavaScript
|
|
62
|
+
state; the single-use artifact URL loads a same-origin bootstrap module under a
|
|
63
|
+
nonce-based CSP. Relative assets, including a same-artifact `<base>`, resolve
|
|
64
|
+
beside the reviewed file, while review navigation always stays relative to the
|
|
65
|
+
source file. External bases are ignored.
|
|
66
|
+
|
|
67
|
+
Localhost reviews keep `allow-same-origin`, popup, and download compatibility so
|
|
68
|
+
application behavior still works. Because localhost application scripts are
|
|
69
|
+
trusted in that mode, the render capability provides correlation and stale
|
|
70
|
+
message rejection rather than an authorization boundary. The authenticated
|
|
71
|
+
parent still validates links and never exposes the raw-file save route to a
|
|
72
|
+
localhost review.
|
|
73
|
+
|
|
74
|
+
Agent polling returns an immutable `batch_id`. After applying the batch, the
|
|
75
|
+
agent acknowledges that exact receipt and keeps waiting:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
npx -y @erdemtuna/doc-review poll path/to/file.html --ack b_0123456789abcdef --timeout 600
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
A stale or repeated batch ID is harmless: it never clears newer feedback. The
|
|
82
|
+
complete acknowledgement command is included in each response's `next_step`.
|
|
83
|
+
|
|
84
|
+
## What this skill lets you do
|
|
85
|
+
|
|
86
|
+
- **Edit text directly and tweak basic formatting** (e.g., bold, italic).
|
|
87
|
+
- **Make bulleted and numbered lists** — type `- ` or `1. ` at the start of a line, or press ⌘⇧8 / ⌘⇧7. Tab and Shift+Tab indent and outdent.
|
|
88
|
+
- **Add links** — select text and press ⌘K. ⌘K inside an existing link edits or removes it.
|
|
89
|
+
- **Resize images** by dragging their corner, and **move images** by dragging them to a new spot.
|
|
90
|
+
- **Rearrange the page** — hover any block and drag the handle on its left edge to move the whole block somewhere else.
|
|
91
|
+
- **Paste images** from your clipboard — file reviews save them beside the document; localhost reviews stage them for the agent to place in the app source.
|
|
92
|
+
- **Select a phrase and leave a comment** anchored to the exact text.
|
|
93
|
+
- **Comment on an image, chart, or section** by clicking the element.
|
|
94
|
+
- **Remove elements** without explaining the deletion in chat.
|
|
95
|
+
- **Command-click links** to review multiple pages without losing your feedback.
|
|
96
|
+
- **Send every edit and comment at once** instead of writing a long chat message.
|
|
97
|
+
|
|
98
|
+
Doc Review works well for editing AI-generated plans, updating landing pages, reviewing localhost apps, and removing extra copy from a UX.
|
|
99
|
+
|
|
100
|
+
## What’s inside
|
|
101
|
+
|
|
102
|
+
- [`cli.js`](src/cli.js) contains the `doc-review`, `poll`, `status`, and `setup` commands.
|
|
103
|
+
- [`server.js`](src/server.js) runs the local review session.
|
|
104
|
+
- [`sdk.js`](src/sdk.js) handles editing, comments, highlights, and feedback.
|
|
105
|
+
- [`chrome-client.js`](src/chrome-client.js) contains the visual review interface.
|
|
106
|
+
- [`markdown.js`](src/markdown.js) renders Markdown files for review.
|
|
107
|
+
- [`SKILL.md`](src/SKILL.md) teaches Claude Code, Codex, and other agents how to use Doc Review.
|
|
108
|
+
|
|
109
|
+
Everything runs on your computer. Doc Review doesn’t require an account, cloud service, database, or API key.
|
|
110
|
+
|
|
111
|
+
## Upstream project
|
|
112
|
+
|
|
113
|
+
Doc Review is an independent fork of [Human Review](https://github.com/petergyang/human-review), originally created by Peter Yang. The fork preserves the upstream MIT license and continues from the upstream `v0.6.1` release.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@erdemtuna/doc-review",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Review and edit agent-generated files and localhost pages in the browser, then send the whole batch back to your agent.",
|
|
5
|
+
"author": "Peter Yang",
|
|
6
|
+
"homepage": "https://github.com/erdemtuna/doc-review#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/erdemtuna/doc-review.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/erdemtuna/doc-review/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"doc-review": "src/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "npm run test:unit",
|
|
28
|
+
"test:unit": "node --test",
|
|
29
|
+
"test:browser": "playwright test",
|
|
30
|
+
"test:all": "npm run test:unit && npm run test:browser",
|
|
31
|
+
"start": "node src/cli.js"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"html",
|
|
35
|
+
"markdown",
|
|
36
|
+
"localhost",
|
|
37
|
+
"nextjs",
|
|
38
|
+
"review",
|
|
39
|
+
"comments",
|
|
40
|
+
"agent",
|
|
41
|
+
"claude-code",
|
|
42
|
+
"codex",
|
|
43
|
+
"cli"
|
|
44
|
+
],
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public",
|
|
48
|
+
"registry": "https://registry.npmjs.org"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@playwright/test": "^1.62.1",
|
|
52
|
+
"jsdom": "^29.1.1"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"marked": "^18.0.7"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/SKILL.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doc-review
|
|
3
|
+
description: Open an HTML file, Markdown file, or localhost page in the browser so the user can edit text directly and leave comments on specific parts, then send all edits and comments back to you. Use after writing or updating something the user will read — specs, plans, reports, newsletter drafts, landing pages, slide decks, and locally running web pages.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# doc-review
|
|
7
|
+
|
|
8
|
+
The user reviews your HTML, Markdown, or localhost page in a real browser: they fix small things
|
|
9
|
+
by typing, select anything to comment on it, and send you the whole batch at once.
|
|
10
|
+
|
|
11
|
+
Markdown files open rendered. Their quotes and edits reference the rendered text,
|
|
12
|
+
and the file itself is never touched — apply every change to the Markdown source,
|
|
13
|
+
keeping its formatting syntax.
|
|
14
|
+
|
|
15
|
+
## The loop
|
|
16
|
+
|
|
17
|
+
1. Write or update the HTML or Markdown file, or start the local page being reviewed.
|
|
18
|
+
2. Open it for the user:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx -y @erdemtuna/doc-review path/to/file.html
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For a page served by a local development server, open the real route instead
|
|
25
|
+
of recreating it as a separate HTML file:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npx -y @erdemtuna/doc-review http://localhost:3000/wiki
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
3. Wait for feedback. This blocks until they hit Send, or the timeout passes:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npx -y @erdemtuna/doc-review poll path/to/file.html --timeout 600
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Keep this command in the foreground. Do not end your turn while it is waiting.
|
|
38
|
+
If your shell returns a process or session handle, keep waiting on that handle
|
|
39
|
+
until the command exits. If it prints `{"status":"timeout"}`, no feedback has
|
|
40
|
+
arrived yet — run the same poll command again to keep waiting. Feedback is
|
|
41
|
+
saved even if a poll dies, so nothing is ever lost.
|
|
42
|
+
|
|
43
|
+
If it prints `{"status":"closed"}`, the user ended the review from the
|
|
44
|
+
browser — stop polling and do not run the poll command again. Unsent
|
|
45
|
+
feedback is kept and ships the next time this target is reviewed.
|
|
46
|
+
|
|
47
|
+
4. Apply what comes back, then wait again. Copy `batch_id` from the response;
|
|
48
|
+
only that exact receipt can clear the batch you handled:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npx -y @erdemtuna/doc-review poll path/to/file.html --ack b_0123456789abcdef --timeout 600
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The response's `next_step` contains the complete acknowledgement command.
|
|
55
|
+
Never acknowledge a different or guessed ID.
|
|
56
|
+
|
|
57
|
+
Repeat 3–4 until the user says they are done.
|
|
58
|
+
|
|
59
|
+
Not sure whether feedback is already waiting — say, at the start of a new turn?
|
|
60
|
+
This answers instantly without blocking:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
npx -y @erdemtuna/doc-review status path/to/file.html
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It prints `{"status": "feedback-waiting"}` when a batch is ready for a poll,
|
|
67
|
+
plus counts of unsent comments and edits still in the browser.
|
|
68
|
+
|
|
69
|
+
## What you get
|
|
70
|
+
|
|
71
|
+
One batch covers every page the user visited, grouped by file or localhost URL.
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"batch_id": "b_0123456789abcdef",
|
|
76
|
+
"status": "feedback",
|
|
77
|
+
"pages": [
|
|
78
|
+
{
|
|
79
|
+
"file": "/abs/path/to/page.html",
|
|
80
|
+
"comments": [
|
|
81
|
+
{ "id": "c_1", "kind": "selection", "quote": "the exact text they selected",
|
|
82
|
+
"anchor": { "prefix": "...", "quote": "...", "suffix": "..." },
|
|
83
|
+
"feedback": "what they want changed" }
|
|
84
|
+
],
|
|
85
|
+
"edits": [
|
|
86
|
+
{ "label": "Problem body", "kind": "edited",
|
|
87
|
+
"before": "the original wording",
|
|
88
|
+
"after": "their exact new wording",
|
|
89
|
+
"after_html": "their exact new wording with <strong>formatting</strong>" }
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"overall_note": "feedback not tied to any one page",
|
|
94
|
+
"next_step": "Apply this feedback, then run: npx -y @erdemtuna/doc-review poll ... --ack b_0123456789abcdef --timeout 600"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Rules
|
|
99
|
+
|
|
100
|
+
- **`edits` are changes the user already made.** `after` is their exact wording —
|
|
101
|
+
carry it across verbatim and never revert it. If the HTML was generated from
|
|
102
|
+
something else (MDX, Markdown, a template), apply `after` to the **source** too,
|
|
103
|
+
or their fix disappears on the next build.
|
|
104
|
+
- When `before_html`/`after_html` are present, the user changed formatting, not
|
|
105
|
+
just words — bold, italic, underline, links. Use the HTML version to carry the
|
|
106
|
+
formatting into the source, translated to its syntax (e.g. `<strong>` → `**`
|
|
107
|
+
in Markdown/MDX).
|
|
108
|
+
- A page with `kind: "url"` was edited directly in the review UI. Its `file`
|
|
109
|
+
and `url` fields name the localhost route, not a writable file. Find the
|
|
110
|
+
matching project source (such as MDX, TSX, or a template), apply every edit
|
|
111
|
+
and deletion there, then acknowledge so the route reloads. Never write the
|
|
112
|
+
rendered HTTP response back into the app.
|
|
113
|
+
- When an edit's `after_html` contains `<img src="assets/...">`, the user pasted
|
|
114
|
+
an image: the file already exists in an `assets/` folder next to the reviewed
|
|
115
|
+
file. Keep that relative path — in Markdown, reference it as
|
|
116
|
+
``. Never regenerate or inline the image.
|
|
117
|
+
- On a localhost page, a pasted image arrives under `staged_assets`. Copy its
|
|
118
|
+
local `path` into the app's appropriate asset folder, replace the temporary
|
|
119
|
+
preview URL in `after_html`, and preserve the image at the user's insertion
|
|
120
|
+
point. Never leave the temporary preview URL in source.
|
|
121
|
+
- An edit with `kind: "moved"` means the user relocated that whole block.
|
|
122
|
+
Reposition it in the source without rewriting its content: it now sits right
|
|
123
|
+
after the block whose text starts with `moved_after`, and right before the
|
|
124
|
+
block whose text starts with `moved_before`. An empty `moved_after` means it
|
|
125
|
+
is now the first block in its container.
|
|
126
|
+
- Find each comment by its `quote`; that exact string is in the file.
|
|
127
|
+
- `kind: "element"` points at a whole block, so `quote` is its label, not body text.
|
|
128
|
+
- Fix every page in `pages`, not just the first.
|
|
129
|
+
- **Do not write a reply.** There is no chat. The user sees your work when the page
|
|
130
|
+
reloads, which happens on its own the moment you save the file.
|
|
131
|
+
|
|
132
|
+
## Better edit labels (optional)
|
|
133
|
+
|
|
134
|
+
Name the sections you author and the user's edit list uses your names instead of
|
|
135
|
+
guessing from the DOM:
|
|
136
|
+
|
|
137
|
+
```html
|
|
138
|
+
<p data-block="Problem body">…</p>
|
|
139
|
+
<div data-container="Metrics callout">…</div>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`data-block` names a region for the edit list. `data-container` also makes the block
|
|
143
|
+
clickable as a comment target.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quote-with-context anchoring, in the spirit of the W3C TextQuoteSelector.
|
|
3
|
+
*
|
|
4
|
+
* These functions are pure string maths so they can be unit-tested in Node and
|
|
5
|
+
* shipped verbatim to the browser, where the DOM half of anchoring lives.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const CONTEXT_PAD = 32;
|
|
9
|
+
|
|
10
|
+
/** Capture `quote` plus surrounding context so it can be re-found after edits. */
|
|
11
|
+
export function buildContext(text, start, end, pad = CONTEXT_PAD) {
|
|
12
|
+
return {
|
|
13
|
+
prefix: text.slice(Math.max(0, start - pad), start),
|
|
14
|
+
quote: text.slice(start, end),
|
|
15
|
+
suffix: text.slice(end, Math.min(text.length, end + pad)),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function commonSuffixLength(a, b) {
|
|
20
|
+
let n = 0;
|
|
21
|
+
while (n < a.length && n < b.length && a[a.length - 1 - n] === b[b.length - 1 - n]) n += 1;
|
|
22
|
+
return n;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function commonPrefixLength(a, b) {
|
|
26
|
+
let n = 0;
|
|
27
|
+
while (n < a.length && n < b.length && a[n] === b[n]) n += 1;
|
|
28
|
+
return n;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function occurrences(text, quote) {
|
|
32
|
+
const found = [];
|
|
33
|
+
let at = text.indexOf(quote);
|
|
34
|
+
while (at !== -1) {
|
|
35
|
+
found.push(at);
|
|
36
|
+
at = text.indexOf(quote, at + 1);
|
|
37
|
+
}
|
|
38
|
+
return found;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bestHit(text, hits, quote, prefix, suffix) {
|
|
42
|
+
let best = hits[0];
|
|
43
|
+
let bestScore = -1;
|
|
44
|
+
for (const at of hits) {
|
|
45
|
+
const before = text.slice(Math.max(0, at - prefix.length), at);
|
|
46
|
+
const after = text.slice(at + quote.length, at + quote.length + suffix.length);
|
|
47
|
+
const score = commonSuffixLength(prefix, before) + commonPrefixLength(suffix, after);
|
|
48
|
+
if (score > bestScore) {
|
|
49
|
+
bestScore = score;
|
|
50
|
+
best = at;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { at: best, score: bestScore };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const collapse = (s) => String(s || "").replace(/\s+/g, " ");
|
|
57
|
+
|
|
58
|
+
/** Collapse whitespace runs, keeping a map from each kept char to its original index. */
|
|
59
|
+
function collapseWithMap(text) {
|
|
60
|
+
let flat = "";
|
|
61
|
+
const map = [];
|
|
62
|
+
let pendingWs = -1;
|
|
63
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
64
|
+
if (/\s/.test(text[i])) {
|
|
65
|
+
if (pendingWs === -1) pendingWs = i;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (pendingWs !== -1 && flat) {
|
|
69
|
+
flat += " ";
|
|
70
|
+
map.push(pendingWs);
|
|
71
|
+
}
|
|
72
|
+
pendingWs = -1;
|
|
73
|
+
flat += text[i];
|
|
74
|
+
map.push(i);
|
|
75
|
+
}
|
|
76
|
+
return { flat, map };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Locate `ctx.quote` in `text`, using prefix/suffix to disambiguate repeats.
|
|
81
|
+
* Returns `{ start, end, exact }` or null when the quote is gone entirely.
|
|
82
|
+
*/
|
|
83
|
+
export function findQuote(text, ctx) {
|
|
84
|
+
const quote = ctx && ctx.quote;
|
|
85
|
+
if (!quote) return null;
|
|
86
|
+
|
|
87
|
+
const hits = occurrences(text, quote);
|
|
88
|
+
if (hits.length === 1) {
|
|
89
|
+
return { start: hits[0], end: hits[0] + quote.length, exact: true };
|
|
90
|
+
}
|
|
91
|
+
if (hits.length > 1) {
|
|
92
|
+
const { at, score } = bestHit(text, hits, quote, ctx.prefix || "", ctx.suffix || "");
|
|
93
|
+
return { start: at, end: at + quote.length, exact: score > 0 };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Reformatting (a prettier run, an agent rewrite) reflows whitespace without
|
|
97
|
+
// changing any words. Match again on whitespace-collapsed text and map the
|
|
98
|
+
// hit back to real offsets, so those comments survive instead of orphaning.
|
|
99
|
+
const { flat, map } = collapseWithMap(text);
|
|
100
|
+
const flatQuote = collapse(quote).trim();
|
|
101
|
+
if (!flatQuote || !map.length) return null;
|
|
102
|
+
const flatHits = occurrences(flat, flatQuote);
|
|
103
|
+
if (!flatHits.length) return null;
|
|
104
|
+
const { at } = bestHit(flat, flatHits, flatQuote, collapse(ctx.prefix || ""), collapse(ctx.suffix || ""));
|
|
105
|
+
return { start: map[at], end: map[at + flatQuote.length - 1] + 1, exact: false };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Collapse runs of whitespace for display in a comment card. */
|
|
109
|
+
export function tidy(text, limit = 0) {
|
|
110
|
+
const flat = String(text == null ? "" : text)
|
|
111
|
+
.replace(/\s+/g, " ")
|
|
112
|
+
.trim();
|
|
113
|
+
if (!limit || flat.length <= limit) return flat;
|
|
114
|
+
return `${flat.slice(0, limit - 1).trimEnd()}…`;
|
|
115
|
+
}
|