@oh-my-pi/hashline 17.0.2 → 17.0.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.0.4] - 2026-07-18
6
+
7
+ ### Fixed
8
+
9
+ - Rejected `DEL N:` headers with a trailing colon instead of silently tolerating the colon, so delete-with-body mistakes surface the corrective "has no colon" guidance.
10
+
5
11
  ## [17.0.0] - 2026-07-15
6
12
 
7
13
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/hashline",
4
- "version": "17.0.2",
4
+ "version": "17.0.4",
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/tokenizer.ts CHANGED
@@ -40,6 +40,7 @@ const CHAR_HASH = 35;
40
40
  const CHAR_TAB = 9;
41
41
  const CHAR_SPACE = 32;
42
42
  const CHAR_DOT = 46;
43
+ const CHAR_COMMA = 44;
43
44
  const CHAR_HYPHEN = 45;
44
45
  const CHAR_ELLIPSIS = 0x2026;
45
46
  const CHAR_EQUALS = 61;
@@ -165,7 +166,7 @@ function scanRangeSeparator(line: string, index: number, end: number): number |
165
166
  consumedSeparator = true;
166
167
  continue;
167
168
  }
168
- if (code === CHAR_HYPHEN || code === CHAR_ELLIPSIS) {
169
+ if (code === CHAR_COMMA || code === CHAR_HYPHEN || code === CHAR_ELLIPSIS) {
169
170
  cursor++;
170
171
  consumedSeparator = true;
171
172
  continue;
@@ -252,6 +253,17 @@ function consumeOptionalColon(line: string, index: number, end: number): number
252
253
  cursor = skipStrayDot(line, cursor, end);
253
254
  return cursor < end && line.charCodeAt(cursor) === CHAR_COLON ? skipWhitespace(line, cursor + 1, end) : cursor;
254
255
  }
256
+ /**
257
+ * Recover local-model replace trailers that permute `:` and `=` as `:=:` or
258
+ * `=:`. The range has already been parsed, so these suffixes are unambiguous.
259
+ */
260
+ function consumeReplaceColon(line: string, index: number, end: number): number {
261
+ const canonical = consumeOptionalColon(line, index, end);
262
+ if (canonical >= end || line.charCodeAt(canonical) !== CHAR_EQUALS) return canonical;
263
+ const afterEquals = skipWhitespace(line, canonical + 1, end);
264
+ if (afterEquals >= end || line.charCodeAt(afterEquals) !== CHAR_COLON) return canonical;
265
+ return skipWhitespace(line, afterEquals + 1, end);
266
+ }
255
267
 
256
268
  function scanInsertTarget(line: string, index: number, end: number): TargetScan | null {
257
269
  if (index >= end || line.charCodeAt(index) !== CHAR_DOT) return null;
@@ -341,7 +353,7 @@ function scanHunkAnchor(line: string, start: number, end: number): TargetScan |
341
353
  if (range === null) return null;
342
354
  return {
343
355
  target: { kind: "replace", range: range.range },
344
- nextIndex: consumeOptionalColon(line, range.nextIndex, end),
356
+ nextIndex: consumeReplaceColon(line, range.nextIndex, end),
345
357
  };
346
358
  }
347
359
  // `delete_block N` — resolve N to a tree-sitter block range at apply time
@@ -356,12 +368,13 @@ function scanHunkAnchor(line: string, start: number, end: number): TargetScan |
356
368
  if (next < end && line.charCodeAt(next) === CHAR_COLON) return null;
357
369
  return { target: { kind: "delete_block", anchor: { line: anchor.line } }, nextIndex: next };
358
370
  }
371
+ // `delete N.=M` — like `delete_block N`, takes no body and no trailing
372
+ // colon; a colon here falls through to contamination detection.
359
373
  const deleteEnd = scanKeyword(line, cursor, end, HL_DELETE_KEYWORD);
360
374
  if (deleteEnd !== null) {
361
375
  const range = scanHeaderRange(line, deleteEnd, end, true);
362
376
  if (range === null) return null;
363
- let next = skipWhitespace(line, range.nextIndex, end);
364
- next = skipStrayDot(line, next, end);
377
+ const next = skipStrayDot(line, range.nextIndex, end);
365
378
  if (next < end && line.charCodeAt(next) === CHAR_COLON) return null;
366
379
  return { target: { kind: "delete", range: range.range }, nextIndex: next };
367
380
  }