@oh-my-pi/hashline 15.10.1 → 15.10.2

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.
@@ -13,6 +13,14 @@
13
13
  * common case for echoed file content, and erroneously echoed prefixes will
14
14
  * otherwise turn every content line into a (malformed) op.
15
15
  */
16
+ /**
17
+ * Single-pass variant of {@link stripLeadingHashlinePrefixes} that strips at
18
+ * most one leading hashline prefix (`N:`, `>>>N:`, `+N:` etc.) and does NOT
19
+ * loop. Use this when the input carries at most one snapshot prefix (e.g. a
20
+ * bare body row paste from `read` output) — recursive stripping would corrupt
21
+ * content whose own text starts with `digits:`.
22
+ */
23
+ export declare function stripOneLeadingHashlinePrefix(line: string): string;
16
24
  /**
17
25
  * Strip whichever prefix scheme the lines appear to be carrying:
18
26
  * - hashline line-number prefixes (`123:`) when every content line has one
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/hashline",
4
- "version": "15.10.1",
4
+ "version": "15.10.2",
5
5
  "description": "Hashline: a compact, line-anchored patch language and applier. Pluggable FS/IO so it works over disk, in-memory, or any custom backend.",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
package/src/parser.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  EMPTY_INSERT,
13
13
  MINUS_ROW_REJECTED,
14
14
  } from "./messages";
15
+ import { stripOneLeadingHashlinePrefix } from "./prefixes";
15
16
  import { type BlockTarget, cloneCursor, type ParsedRange, type Token, Tokenizer } from "./tokenizer";
16
17
  import type { Anchor, Cursor, Edit } from "./types";
17
18
 
@@ -81,7 +82,7 @@ interface PendingComment {
81
82
  text: string;
82
83
  }
83
84
 
84
- type PayloadRow = { kind: "literal"; text: string; lineNum: number };
85
+ type PayloadRow = { kind: "literal"; text: string; lineNum: number; bare?: boolean };
85
86
 
86
87
  interface Pending {
87
88
  target: BlockTarget;
@@ -220,7 +221,14 @@ export class Executor {
220
221
  throw new Error(`line ${lineNum}: ${DELETE_BLOCK_TAKES_NO_BODY}`);
221
222
  if (text.trimStart().charCodeAt(0) === 45 /* - */) throw new Error(`line ${lineNum}: ${MINUS_ROW_REJECTED}`);
222
223
  if (!this.#warnings.includes(BARE_BODY_AUTO_PIPED_WARNING)) this.#warnings.push(BARE_BODY_AUTO_PIPED_WARNING);
223
- this.#pending.payloads.push({ kind: "literal", text, lineNum });
224
+ // Defer read-output line-number stripping to #flushPending: a bare
225
+ // "N:text" row is only a copy-paste artifact from snapshot output
226
+ // when *every* bare row in the hunk carries that prefix. Stripping a
227
+ // row in isolation would corrupt a genuine body that merely starts
228
+ // with "digits:" (YAML ports "42:hello", timestamps "12:30") when it
229
+ // sits next to an unprefixed sibling. Rows with an explicit "+" go
230
+ // through #handleLiteralPayload and are never bare, never stripped.
231
+ this.#pending.payloads.push({ kind: "literal", text, lineNum, bare: true });
224
232
  return;
225
233
  }
226
234
  if (text.trim().length === 0) return;
@@ -230,6 +238,26 @@ export class Executor {
230
238
  );
231
239
  }
232
240
 
241
+ /**
242
+ * Strip a single read-output line-number prefix (`N:`) from every bare body
243
+ * row, but only when *all* bare rows carry one. A uniform set of prefixes is
244
+ * the signature of content pasted straight from `read`/`search` output; a
245
+ * mixed set means the `N:` is genuine payload content and must stay. Rows
246
+ * authored with an explicit `+` are not bare and are never touched.
247
+ */
248
+ #stripBarePrefixesIfUniform(payloads: PayloadRow[]): void {
249
+ let sawBare = false;
250
+ for (const row of payloads) {
251
+ if (!row.bare) continue;
252
+ sawBare = true;
253
+ if (stripOneLeadingHashlinePrefix(row.text) === row.text) return;
254
+ }
255
+ if (!sawBare) return;
256
+ for (const row of payloads) {
257
+ if (row.bare) row.text = stripOneLeadingHashlinePrefix(row.text);
258
+ }
259
+ }
260
+
233
261
  #pushInsert(cursor: Cursor, text: string, lineNum: number, mode?: "replacement"): void {
234
262
  this.#edits.push({
235
263
  kind: "insert",
@@ -263,6 +291,7 @@ export class Executor {
263
291
  const pending = this.#pending;
264
292
  if (!pending) return;
265
293
  const { target, lineNum, payloads } = pending;
294
+ this.#stripBarePrefixesIfUniform(payloads);
266
295
  this.#pending = undefined;
267
296
  if (target.kind === "delete") {
268
297
  for (const anchor of expandRange(target.range)) this.#pushDelete(anchor, lineNum);
package/src/prefixes.ts CHANGED
@@ -31,6 +31,16 @@ function stripLeadingHashlinePrefixes(line: string): string {
31
31
  } while (result !== previous);
32
32
  return result;
33
33
  }
34
+ /**
35
+ * Single-pass variant of {@link stripLeadingHashlinePrefixes} that strips at
36
+ * most one leading hashline prefix (`N:`, `>>>N:`, `+N:` etc.) and does NOT
37
+ * loop. Use this when the input carries at most one snapshot prefix (e.g. a
38
+ * bare body row paste from `read` output) — recursive stripping would corrupt
39
+ * content whose own text starts with `digits:`.
40
+ */
41
+ export function stripOneLeadingHashlinePrefix(line: string): string {
42
+ return line.replace(HL_PREFIX_RE, "");
43
+ }
34
44
 
35
45
  interface LinePrefixStats {
36
46
  nonEmpty: number;