@oh-my-pi/pi-utils 16.3.6 → 16.3.7
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 +6 -0
- package/dist/types/env.d.ts +1 -1
- package/dist/types/fetch-retry.d.ts +2 -0
- package/dist/types/index.d.ts +34 -34
- package/dist/types/json-parse.d.ts +26 -0
- package/dist/types/mermaid-ascii.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +2 -2
- package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +3 -3
- package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +2 -2
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +3 -3
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/index.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/parser.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +1 -1
- package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +1 -1
- package/package.json +2 -2
- package/src/fetch-retry.ts +18 -0
- package/src/json-parse.ts +209 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.3.7] - 2026-07-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `classifyJsonPrefix`, a strict RFC 8259 streaming-buffer classifier (`"complete" | "prefix" | "invalid"`). Providers use it to disambiguate identifierless streaming tool-call deltas: a `{`-prefixed chunk only advances to a sibling call when the current argument buffer cannot absorb it.
|
|
10
|
+
|
|
5
11
|
## [16.3.1] - 2026-07-02
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/dist/types/env.d.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* by the OpenAI Codex and Google Gemini retry helpers.
|
|
4
4
|
*
|
|
5
5
|
* Header sources (checked in order):
|
|
6
|
+
* - `retry-after-ms` (milliseconds)
|
|
6
7
|
* - `Retry-After` (numeric seconds, or HTTP date)
|
|
8
|
+
* - `x-ratelimit-reset-ms` (delta ms, or Unix epoch ms/s for large values)
|
|
7
9
|
* - `x-ratelimit-reset` (Unix epoch seconds)
|
|
8
10
|
* - `x-ratelimit-reset-after` (seconds)
|
|
9
11
|
*
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
export { once, untilAborted } from "./abortable";
|
|
2
|
-
export * from "./async";
|
|
3
|
-
export * from "./binary";
|
|
4
|
-
export * from "./color";
|
|
5
|
-
export * from "./dirs";
|
|
6
|
-
export * from "./env";
|
|
7
|
-
export * from "./fetch-retry";
|
|
8
|
-
export * from "./format";
|
|
9
|
-
export * from "./frontmatter";
|
|
10
|
-
export * from "./fs-error";
|
|
11
|
-
export * from "./glob";
|
|
12
|
-
export * from "./json";
|
|
13
|
-
export * from "./json-parse";
|
|
14
|
-
export * as logger from "./logger";
|
|
15
|
-
export * from "./loop-phase";
|
|
16
|
-
export * from "./mermaid-ascii";
|
|
17
|
-
export * from "./mime";
|
|
18
|
-
export * from "./path";
|
|
19
|
-
export * from "./path-tree";
|
|
20
|
-
export * from "./peek-file";
|
|
21
|
-
export * as postmortem from "./postmortem";
|
|
22
|
-
export * as procmgr from "./procmgr";
|
|
23
|
-
export * as prompt from "./prompt";
|
|
24
|
-
export * as ptree from "./ptree";
|
|
25
|
-
export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
|
|
26
|
-
export * from "./runtime-install";
|
|
27
|
-
export * from "./sanitize-text";
|
|
28
|
-
export * from "./snowflake";
|
|
29
|
-
export * from "./stream";
|
|
30
|
-
export * from "./tab-spacing";
|
|
31
|
-
export * from "./temp";
|
|
32
|
-
export * from "./tls-fetch";
|
|
33
|
-
export * from "./type-guards";
|
|
34
|
-
export * from "./which";
|
|
1
|
+
export { once, untilAborted } from "./abortable.js";
|
|
2
|
+
export * from "./async.js";
|
|
3
|
+
export * from "./binary.js";
|
|
4
|
+
export * from "./color.js";
|
|
5
|
+
export * from "./dirs.js";
|
|
6
|
+
export * from "./env.js";
|
|
7
|
+
export * from "./fetch-retry.js";
|
|
8
|
+
export * from "./format.js";
|
|
9
|
+
export * from "./frontmatter.js";
|
|
10
|
+
export * from "./fs-error.js";
|
|
11
|
+
export * from "./glob.js";
|
|
12
|
+
export * from "./json.js";
|
|
13
|
+
export * from "./json-parse.js";
|
|
14
|
+
export * as logger from "./logger.js";
|
|
15
|
+
export * from "./loop-phase.js";
|
|
16
|
+
export * from "./mermaid-ascii.js";
|
|
17
|
+
export * from "./mime.js";
|
|
18
|
+
export * from "./path.js";
|
|
19
|
+
export * from "./path-tree.js";
|
|
20
|
+
export * from "./peek-file.js";
|
|
21
|
+
export * as postmortem from "./postmortem.js";
|
|
22
|
+
export * as procmgr from "./procmgr.js";
|
|
23
|
+
export * as prompt from "./prompt.js";
|
|
24
|
+
export * as ptree from "./ptree.js";
|
|
25
|
+
export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree.js";
|
|
26
|
+
export * from "./runtime-install.js";
|
|
27
|
+
export * from "./sanitize-text.js";
|
|
28
|
+
export * from "./snowflake.js";
|
|
29
|
+
export * from "./stream.js";
|
|
30
|
+
export * from "./tab-spacing.js";
|
|
31
|
+
export * from "./temp.js";
|
|
32
|
+
export * from "./tls-fetch.js";
|
|
33
|
+
export * from "./type-guards.js";
|
|
34
|
+
export * from "./which.js";
|
|
35
35
|
export declare function structuredCloneJSON<T>(value: T): T;
|
|
@@ -46,3 +46,29 @@ export declare function parseStreamingJsonThrottled<T = Record<string, unknown>>
|
|
|
46
46
|
value: T;
|
|
47
47
|
parsedLen: number;
|
|
48
48
|
} | null;
|
|
49
|
+
/**
|
|
50
|
+
* Classification of a streaming buffer against strict JSON (RFC 8259):
|
|
51
|
+
* - `"complete"`: exactly one whole JSON value (plus surrounding whitespace).
|
|
52
|
+
* - `"prefix"`: a proper prefix of some valid JSON value — more bytes can
|
|
53
|
+
* still complete it.
|
|
54
|
+
* - `"invalid"`: no suffix can ever make it valid strict JSON (e.g. a raw
|
|
55
|
+
* control character inside a string, or a second top-level value).
|
|
56
|
+
*/
|
|
57
|
+
export type JsonPrefixState = "complete" | "prefix" | "invalid";
|
|
58
|
+
/**
|
|
59
|
+
* Classify `text` as a strict-JSON value, prefix, or dead end.
|
|
60
|
+
*
|
|
61
|
+
* Providers use this to disambiguate identifierless streaming tool-call
|
|
62
|
+
* deltas: a chunk starting with `{` is a *new* sibling call only if the
|
|
63
|
+
* current call's argument buffer cannot absorb it — the buffer is already a
|
|
64
|
+
* complete value, already unsalvageable (lossy hosts abandon buffers
|
|
65
|
+
* mid-string, leaving raw control characters strict JSON forbids), or the
|
|
66
|
+
* concatenation would break it. Unlike {@link parseStreamingJson} this is
|
|
67
|
+
* deliberately strict: forgiving repair would mask exactly the corruption
|
|
68
|
+
* signals the caller needs.
|
|
69
|
+
*
|
|
70
|
+
* A top-level number at end-of-input classifies as `"complete"` even though
|
|
71
|
+
* more digits could extend it; tool-argument buffers are always objects, so
|
|
72
|
+
* the ambiguity is immaterial here.
|
|
73
|
+
*/
|
|
74
|
+
export declare function classifyJsonPrefix(text: string): JsonPrefixState;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AsciiRenderOptions } from "./vendor/mermaid-ascii";
|
|
1
|
+
import { type AsciiRenderOptions } from "./vendor/mermaid-ascii/index.js";
|
|
2
2
|
export type { AsciiRenderOptions as MermaidAsciiRenderOptions };
|
|
3
3
|
export declare function renderMermaidAscii(source: string, options?: AsciiRenderOptions): string;
|
|
4
4
|
export declare function renderMermaidAsciiSafe(source: string, options?: AsciiRenderOptions): string | null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CharRole, AsciiTheme, ColorMode } from './types';
|
|
1
|
+
import type { CharRole, AsciiTheme, ColorMode } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Default ASCII theme derived from the SVG renderer's color palette.
|
|
4
4
|
* Uses the same mixing ratios to maintain visual consistency.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Canvas, DrawingCoord, RoleCanvas, CharRole, AsciiTheme, ColorMode } from './types';
|
|
1
|
+
import type { Canvas, DrawingCoord, RoleCanvas, CharRole, AsciiTheme, ColorMode } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Create a blank canvas filled with spaces.
|
|
4
4
|
* Dimensions are inclusive: mkCanvas(3, 2) creates a 4x3 grid (indices 0..3, 0..2).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { MermaidGraph } from '../types';
|
|
2
|
-
import type { AsciiGraph, AsciiConfig } from './types';
|
|
1
|
+
import type { MermaidGraph } from '../types.js';
|
|
2
|
+
import type { AsciiGraph, AsciiConfig } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Convert a parsed MermaidGraph into an AsciiGraph ready for grid layout.
|
|
5
5
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Canvas, DrawingCoord, AsciiGraph, AsciiNode, AsciiEdge, AsciiSubgraph, AsciiEdgeStyle } from './types';
|
|
1
|
+
import type { Canvas, DrawingCoord, AsciiGraph, AsciiNode, AsciiEdge, AsciiSubgraph, AsciiEdgeStyle } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Draw a node using its shape type.
|
|
4
4
|
* Returns a standalone canvas containing the rendered shape.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Direction, AsciiEdge, AsciiGraph } from './types';
|
|
1
|
+
import type { Direction, AsciiEdge, AsciiGraph } from './types.js';
|
|
2
2
|
export declare function getOpposite(d: Direction): Direction;
|
|
3
3
|
/** Compare directions by value (not reference). */
|
|
4
4
|
export declare function dirEquals(a: Direction, b: Direction): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GridCoord, DrawingCoord, Direction, AsciiGraph, AsciiNode, AsciiSubgraph } from './types';
|
|
1
|
+
import type { GridCoord, DrawingCoord, Direction, AsciiGraph, AsciiNode, AsciiSubgraph } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Convert a grid coordinate to a drawing (character) coordinate.
|
|
4
4
|
* Sums column widths up to the target column, and row heights up to the target row,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { detectColorMode, DEFAULT_ASCII_THEME } from './ansi';
|
|
2
|
-
import type { AsciiTheme, ColorMode } from './types';
|
|
3
|
-
import type { Direction } from '../types';
|
|
1
|
+
import { detectColorMode, DEFAULT_ASCII_THEME } from './ansi.js';
|
|
2
|
+
import type { AsciiTheme, ColorMode } from './types.js';
|
|
3
|
+
import type { Direction } from '../types.js';
|
|
4
4
|
export type { AsciiTheme, ColorMode };
|
|
5
5
|
export { DEFAULT_ASCII_THEME, detectColorMode };
|
|
6
6
|
export interface AsciiRenderOptions {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GridCoord, AsciiNode } from './types';
|
|
1
|
+
import type { GridCoord, AsciiNode } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Manhattan distance with a +1 penalty when both dx and dy are non-zero.
|
|
4
4
|
* This encourages the pathfinder to prefer straight lines and minimize corners.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { AsciiNodeShape, Canvas, DrawingCoord, Direction } from '../types';
|
|
2
|
-
import type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions, ShapeRegistry } from './types';
|
|
1
|
+
import type { AsciiNodeShape, Canvas, DrawingCoord, Direction } from '../types.js';
|
|
2
|
+
import type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions, ShapeRegistry } from './types.js';
|
|
3
3
|
export type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions, ShapeRegistry };
|
|
4
4
|
/**
|
|
5
5
|
* Global shape registry — maps shape types to their renderers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Canvas, DrawingCoord, Direction } from '../types';
|
|
2
|
-
import type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions } from './types';
|
|
3
|
-
import { type CornerChars } from './corners';
|
|
1
|
+
import type { Canvas, DrawingCoord, Direction } from '../types.js';
|
|
2
|
+
import type { ShapeRenderer, ShapeDimensions, ShapeRenderOptions } from './types.js';
|
|
3
|
+
import { type CornerChars } from './corners.js';
|
|
4
4
|
/**
|
|
5
5
|
* Calculate standard box dimensions for any rectangular shape.
|
|
6
6
|
* Used by rectangle, circle, diamond, hexagon, etc.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { AsciiConfig, AsciiTheme, ColorMode } from './types';
|
|
1
|
+
import type { AsciiConfig, AsciiTheme, ColorMode } from './types.js';
|
|
2
2
|
export declare function renderXYChartAscii(text: string, config: AsciiConfig, colorMode: ColorMode, theme: AsciiTheme): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './ascii/index';
|
|
1
|
+
export * from './ascii/index.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "16.3.
|
|
4
|
+
"version": "16.3.7",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "16.3.
|
|
34
|
+
"@oh-my-pi/pi-natives": "16.3.7",
|
|
35
35
|
"handlebars": "^4.7.9",
|
|
36
36
|
"winston": "^3.19.0",
|
|
37
37
|
"winston-daily-rotate-file": "^5.0.0"
|
package/src/fetch-retry.ts
CHANGED
|
@@ -16,7 +16,9 @@ const TRY_AGAIN_PATTERN = /try again in\s+~?\s*([0-9.]+)\s*(ms|sec|s|minutes?|mi
|
|
|
16
16
|
* by the OpenAI Codex and Google Gemini retry helpers.
|
|
17
17
|
*
|
|
18
18
|
* Header sources (checked in order):
|
|
19
|
+
* - `retry-after-ms` (milliseconds)
|
|
19
20
|
* - `Retry-After` (numeric seconds, or HTTP date)
|
|
21
|
+
* - `x-ratelimit-reset-ms` (delta ms, or Unix epoch ms/s for large values)
|
|
20
22
|
* - `x-ratelimit-reset` (Unix epoch seconds)
|
|
21
23
|
* - `x-ratelimit-reset-after` (seconds)
|
|
22
24
|
*
|
|
@@ -31,6 +33,11 @@ const TRY_AGAIN_PATTERN = /try again in\s+~?\s*([0-9.]+)\s*(ms|sec|s|minutes?|mi
|
|
|
31
33
|
export function extractRetryHint(source: Response | Headers | null | undefined, body?: string): number | undefined {
|
|
32
34
|
const headers = source instanceof Headers ? source : (source?.headers ?? undefined);
|
|
33
35
|
if (headers) {
|
|
36
|
+
const retryAfterMs = headers.get("retry-after-ms");
|
|
37
|
+
if (retryAfterMs) {
|
|
38
|
+
const ms = Number(retryAfterMs);
|
|
39
|
+
if (Number.isFinite(ms) && ms >= 0) return ms;
|
|
40
|
+
}
|
|
34
41
|
const retryAfter = headers.get("retry-after");
|
|
35
42
|
if (retryAfter) {
|
|
36
43
|
const seconds = Number(retryAfter);
|
|
@@ -38,6 +45,17 @@ export function extractRetryHint(source: Response | Headers | null | undefined,
|
|
|
38
45
|
const parsedDate = Date.parse(retryAfter);
|
|
39
46
|
if (!Number.isNaN(parsedDate)) return Math.max(0, parsedDate - Date.now());
|
|
40
47
|
}
|
|
48
|
+
const rateLimitResetMs = headers.get("x-ratelimit-reset-ms");
|
|
49
|
+
if (rateLimitResetMs) {
|
|
50
|
+
const value = Number(rateLimitResetMs);
|
|
51
|
+
if (Number.isFinite(value) && value > 0) {
|
|
52
|
+
// > 1e12 → epoch ms; > 1e9 → epoch s; otherwise a delta in ms.
|
|
53
|
+
const targetMs = value > 1e12 ? value : value > 1e9 ? value * 1000 : undefined;
|
|
54
|
+
if (targetMs === undefined) return value;
|
|
55
|
+
const delta = targetMs - Date.now();
|
|
56
|
+
if (delta > 0) return delta;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
41
59
|
const rateLimitReset = headers.get("x-ratelimit-reset");
|
|
42
60
|
if (rateLimitReset) {
|
|
43
61
|
const resetSeconds = Number.parseInt(rateLimitReset, 10);
|
package/src/json-parse.ts
CHANGED
|
@@ -610,3 +610,212 @@ export function parseStreamingJsonThrottled<T = Record<string, unknown>>(
|
|
|
610
610
|
if (len === 0 || (lastParsedLen > 0 && len - lastParsedLen < minGrowthBytes)) return null;
|
|
611
611
|
return { value: parseStreamingJson<T>(partialJson), parsedLen: len };
|
|
612
612
|
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Classification of a streaming buffer against strict JSON (RFC 8259):
|
|
616
|
+
* - `"complete"`: exactly one whole JSON value (plus surrounding whitespace).
|
|
617
|
+
* - `"prefix"`: a proper prefix of some valid JSON value — more bytes can
|
|
618
|
+
* still complete it.
|
|
619
|
+
* - `"invalid"`: no suffix can ever make it valid strict JSON (e.g. a raw
|
|
620
|
+
* control character inside a string, or a second top-level value).
|
|
621
|
+
*/
|
|
622
|
+
export type JsonPrefixState = "complete" | "prefix" | "invalid";
|
|
623
|
+
|
|
624
|
+
/** What the strict-prefix scanner expects at the current position. */
|
|
625
|
+
const enum JsonExpect {
|
|
626
|
+
Value,
|
|
627
|
+
ObjKeyOrEnd,
|
|
628
|
+
ObjKey,
|
|
629
|
+
ObjColon,
|
|
630
|
+
ObjCommaOrEnd,
|
|
631
|
+
ArrValueOrEnd,
|
|
632
|
+
ArrCommaOrEnd,
|
|
633
|
+
End,
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Classify `text` as a strict-JSON value, prefix, or dead end.
|
|
638
|
+
*
|
|
639
|
+
* Providers use this to disambiguate identifierless streaming tool-call
|
|
640
|
+
* deltas: a chunk starting with `{` is a *new* sibling call only if the
|
|
641
|
+
* current call's argument buffer cannot absorb it — the buffer is already a
|
|
642
|
+
* complete value, already unsalvageable (lossy hosts abandon buffers
|
|
643
|
+
* mid-string, leaving raw control characters strict JSON forbids), or the
|
|
644
|
+
* concatenation would break it. Unlike {@link parseStreamingJson} this is
|
|
645
|
+
* deliberately strict: forgiving repair would mask exactly the corruption
|
|
646
|
+
* signals the caller needs.
|
|
647
|
+
*
|
|
648
|
+
* A top-level number at end-of-input classifies as `"complete"` even though
|
|
649
|
+
* more digits could extend it; tool-argument buffers are always objects, so
|
|
650
|
+
* the ambiguity is immaterial here.
|
|
651
|
+
*/
|
|
652
|
+
export function classifyJsonPrefix(text: string): JsonPrefixState {
|
|
653
|
+
const n = text.length;
|
|
654
|
+
let i = 0;
|
|
655
|
+
// Container stack: true = object, false = array.
|
|
656
|
+
const stack: boolean[] = [];
|
|
657
|
+
let expect = JsonExpect.Value;
|
|
658
|
+
|
|
659
|
+
/** Consume a string starting at the opening quote. 1 = ok, 0 = prefix, -1 = invalid. */
|
|
660
|
+
const scanString = (): 1 | 0 | -1 => {
|
|
661
|
+
i++; // opening quote
|
|
662
|
+
while (i < n) {
|
|
663
|
+
const c = text.charCodeAt(i);
|
|
664
|
+
if (c === QUOTE) {
|
|
665
|
+
i++;
|
|
666
|
+
return 1;
|
|
667
|
+
}
|
|
668
|
+
if (c === BACKSLASH) {
|
|
669
|
+
i++;
|
|
670
|
+
if (i >= n) return 0;
|
|
671
|
+
const e = text.charCodeAt(i);
|
|
672
|
+
if (e >= 128 || !VALID_ESCAPE_CHAR[e]) return -1;
|
|
673
|
+
i++;
|
|
674
|
+
if (e === U) {
|
|
675
|
+
for (let k = 0; k < 4; k++, i++) {
|
|
676
|
+
if (i >= n) return 0;
|
|
677
|
+
if (!isHexDigit(text.charCodeAt(i))) return -1;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (c < 0x20) return -1; // raw control char: strict JSON forbids it
|
|
683
|
+
i++;
|
|
684
|
+
}
|
|
685
|
+
return 0;
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
/** Consume a number starting at `-` or a digit. 1 = token done, 0 = prefix, -1 = invalid. */
|
|
689
|
+
const scanNumber = (): 1 | 0 | -1 => {
|
|
690
|
+
if (text.charCodeAt(i) === 0x2d) i++; // -
|
|
691
|
+
if (i >= n) return 0;
|
|
692
|
+
let c = text.charCodeAt(i);
|
|
693
|
+
if (c === 0x30) {
|
|
694
|
+
i++; // 0: no further integer digits allowed
|
|
695
|
+
} else if (c >= 0x31 && c <= 0x39) {
|
|
696
|
+
while (i < n && text.charCodeAt(i) >= 0x30 && text.charCodeAt(i) <= 0x39) i++;
|
|
697
|
+
} else {
|
|
698
|
+
return -1;
|
|
699
|
+
}
|
|
700
|
+
if (i < n && text.charCodeAt(i) === 0x2e) {
|
|
701
|
+
i++; // .
|
|
702
|
+
if (i >= n) return 0;
|
|
703
|
+
if (text.charCodeAt(i) < 0x30 || text.charCodeAt(i) > 0x39) return -1;
|
|
704
|
+
while (i < n && text.charCodeAt(i) >= 0x30 && text.charCodeAt(i) <= 0x39) i++;
|
|
705
|
+
}
|
|
706
|
+
c = i < n ? text.charCodeAt(i) : 0;
|
|
707
|
+
if (c === 0x65 || c === 0x45) {
|
|
708
|
+
i++; // e | E
|
|
709
|
+
if (i < n && (text.charCodeAt(i) === 0x2b || text.charCodeAt(i) === 0x2d)) i++;
|
|
710
|
+
if (i >= n) return 0;
|
|
711
|
+
if (text.charCodeAt(i) < 0x30 || text.charCodeAt(i) > 0x39) return -1;
|
|
712
|
+
while (i < n && text.charCodeAt(i) >= 0x30 && text.charCodeAt(i) <= 0x39) i++;
|
|
713
|
+
}
|
|
714
|
+
return 1;
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
/** Consume `true`/`false`/`null`. 1 = done, 0 = prefix, -1 = invalid. */
|
|
718
|
+
const scanKeyword = (): 1 | 0 | -1 => {
|
|
719
|
+
for (const word of ["true", "false", "null"] as const) {
|
|
720
|
+
if (word.charCodeAt(0) !== text.charCodeAt(i)) continue;
|
|
721
|
+
const available = Math.min(word.length, n - i);
|
|
722
|
+
if (!word.startsWith(text.slice(i, i + available))) return -1;
|
|
723
|
+
i += available;
|
|
724
|
+
return available === word.length ? 1 : 0;
|
|
725
|
+
}
|
|
726
|
+
return -1;
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
/** A value just finished; the next expectation follows from the stack. */
|
|
730
|
+
const valueDone = (): JsonExpect =>
|
|
731
|
+
stack.length === 0
|
|
732
|
+
? JsonExpect.End
|
|
733
|
+
: stack[stack.length - 1]
|
|
734
|
+
? JsonExpect.ObjCommaOrEnd
|
|
735
|
+
: JsonExpect.ArrCommaOrEnd;
|
|
736
|
+
|
|
737
|
+
while (i < n) {
|
|
738
|
+
const c = text.charCodeAt(i);
|
|
739
|
+
if (isWhitespace(c)) {
|
|
740
|
+
i++;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
switch (expect) {
|
|
744
|
+
case JsonExpect.Value:
|
|
745
|
+
case JsonExpect.ArrValueOrEnd: {
|
|
746
|
+
if (c === 0x5d && expect === JsonExpect.ArrValueOrEnd) {
|
|
747
|
+
stack.pop();
|
|
748
|
+
i++;
|
|
749
|
+
expect = valueDone();
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
if (c === 0x7b) {
|
|
753
|
+
stack.push(true);
|
|
754
|
+
i++;
|
|
755
|
+
expect = JsonExpect.ObjKeyOrEnd;
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
if (c === 0x5b) {
|
|
759
|
+
stack.push(false);
|
|
760
|
+
i++;
|
|
761
|
+
expect = JsonExpect.ArrValueOrEnd;
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
let r: 1 | 0 | -1;
|
|
765
|
+
if (c === QUOTE) r = scanString();
|
|
766
|
+
else if (c === 0x2d || (c >= 0x30 && c <= 0x39)) r = scanNumber();
|
|
767
|
+
else if (c === 0x74 || c === 0x66 || c === 0x6e) r = scanKeyword();
|
|
768
|
+
else return "invalid";
|
|
769
|
+
if (r === -1) return "invalid";
|
|
770
|
+
if (r === 0) return "prefix";
|
|
771
|
+
expect = valueDone();
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
case JsonExpect.ObjKeyOrEnd:
|
|
775
|
+
case JsonExpect.ObjKey: {
|
|
776
|
+
if (c === 0x7d && expect === JsonExpect.ObjKeyOrEnd) {
|
|
777
|
+
stack.pop();
|
|
778
|
+
i++;
|
|
779
|
+
expect = valueDone();
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
if (c !== QUOTE) return "invalid";
|
|
783
|
+
const r = scanString();
|
|
784
|
+
if (r === -1) return "invalid";
|
|
785
|
+
if (r === 0) return "prefix";
|
|
786
|
+
expect = JsonExpect.ObjColon;
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
case JsonExpect.ObjColon:
|
|
790
|
+
if (c !== 0x3a) return "invalid";
|
|
791
|
+
i++;
|
|
792
|
+
expect = JsonExpect.Value;
|
|
793
|
+
break;
|
|
794
|
+
case JsonExpect.ObjCommaOrEnd:
|
|
795
|
+
if (c === 0x7d) {
|
|
796
|
+
stack.pop();
|
|
797
|
+
i++;
|
|
798
|
+
expect = valueDone();
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
if (c !== 0x2c) return "invalid";
|
|
802
|
+
i++;
|
|
803
|
+
expect = JsonExpect.ObjKey;
|
|
804
|
+
break;
|
|
805
|
+
case JsonExpect.ArrCommaOrEnd:
|
|
806
|
+
if (c === 0x5d) {
|
|
807
|
+
stack.pop();
|
|
808
|
+
i++;
|
|
809
|
+
expect = valueDone();
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
if (c !== 0x2c) return "invalid";
|
|
813
|
+
i++;
|
|
814
|
+
expect = JsonExpect.Value;
|
|
815
|
+
break;
|
|
816
|
+
case JsonExpect.End:
|
|
817
|
+
return "invalid"; // trailing non-whitespace after a complete value
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return expect === JsonExpect.End ? "complete" : "prefix";
|
|
821
|
+
}
|