@jini-ai/vibecoding 0.6.10 → 0.6.12
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 +43 -0
- package/README.md +51 -7
- package/dist/core/history.d.ts +2 -0
- package/dist/core/history.js +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/project.d.ts +96 -0
- package/dist/core/project.js +10 -0
- package/dist/html/edit.d.ts +116 -0
- package/dist/html/edit.js +103 -0
- package/dist/html/index.d.ts +2 -0
- package/dist/html/index.js +1 -0
- package/dist/html/node/index.d.ts +2 -1
- package/dist/html/node/index.js +1 -1
- package/dist/html/node/parse5-region-parser.d.ts +13 -1
- package/dist/html/node/parse5-region-parser.js +164 -76
- package/dist/html/regions.d.ts +0 -53
- package/dist/html/regions.js +55 -1
- package/dist/node/index.d.ts +6 -0
- package/dist/node/index.js +4 -0
- package/dist/node/paths.d.ts +50 -0
- package/dist/node/paths.js +91 -0
- package/dist/node/project.d.ts +19 -0
- package/dist/node/project.js +367 -0
- package/dist/node/store.d.ts +102 -0
- package/dist/node/store.js +478 -0
- package/dist/react/session.d.ts +2 -107
- package/dist/react/session.js +2 -154
- package/dist/react/tools.d.ts +2 -90
- package/dist/react/tools.js +2 -245
- package/dist/session.d.ts +116 -0
- package/dist/session.js +179 -0
- package/dist/tools.d.ts +91 -0
- package/dist/tools.js +246 -0
- package/package.json +17 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
- @jini-ai/platform@0.6.12
|
|
9
|
+
- @jini-ai/core@0.6.12
|
|
10
|
+
|
|
11
|
+
## 0.6.11
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- `./node` file-tree projects with revision undo and a React-free `./session`; `./html` gains a non-throwing region scan and pure region edits; async atomic writes in platform.
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
- @jini-ai/core@0.6.11
|
|
20
|
+
- @jini-ai/platform@0.6.11
|
|
21
|
+
|
|
22
|
+
## Unreleased
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Add `./node` real file-tree projects with content-addressed revisions, pending-journal recovery,
|
|
27
|
+
external-drift capture, optional whole-candidate validation and exact pointer-part undo/redo.
|
|
28
|
+
- Add React-free `./session` with injectable project history/mutation; preserve React re-exports.
|
|
29
|
+
- Expose both retained history stacks for object/manifest garbage collection.
|
|
30
|
+
- Export non-throwing `scanHtmlRegions` from `./html/node`, with source-ordered inner/outer offsets,
|
|
31
|
+
tags, per-region problems, document diagnostics and top-level tagging metadata.
|
|
32
|
+
- Export pure lookup, inner replacement, whole-element move, ambiguity comparison, handle listing
|
|
33
|
+
and heading-based tagging suggestions from `./html`; slugification is supplied by the host.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Delegate the strict parse5 parser to the scan while preserving target validation. Ignore handle
|
|
38
|
+
examples in comments and literal-text bodies (including `noscript`), and collapse reconstructed
|
|
39
|
+
formatting clones to one unsafe address. The scan path keeps safe regions usable alongside
|
|
40
|
+
malformed markup.
|
|
41
|
+
- HTML5 recovery differs from Tovu's former lexical scanner: non-void self-closing syntax remains
|
|
42
|
+
open, an explicit later close permits editing, unclosed top-level elements use recovered advice
|
|
43
|
+
boundaries, malformed tokens follow parse5 recovery, and unquoted/upper-case handle attributes
|
|
44
|
+
are recognized.
|
|
45
|
+
|
|
3
46
|
## 0.6.10
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -33,10 +33,12 @@ Neither reimplements the loop.
|
|
|
33
33
|
| `./core` | universal | the loop and the `EditTarget` port — no React, no Node, no DOM |
|
|
34
34
|
| `./html` | universal | the tagged-region, single-HTML-document `EditTarget` (parser injected) |
|
|
35
35
|
| `./html/node` | node | a parse5-backed `HtmlRegionParser` for `./html`, for a Node host |
|
|
36
|
-
| `./
|
|
36
|
+
| `./session` | universal | Sessions and registered/direct tools; no React import |
|
|
37
|
+
| `./node` | node | Real local file trees, whole-turn validation, capture, journal recovery and exact history |
|
|
38
|
+
| `./react` | browser | Session/tool re-exports, hooks and file-viewer/renderer components |
|
|
37
39
|
|
|
38
|
-
`./node`
|
|
39
|
-
|
|
40
|
+
`./node` supplies a file-tree project behind the same `EditTarget` port. The host selects the
|
|
41
|
+
real working directory and a private state directory outside the app root. React is an **optional** peer of
|
|
40
42
|
`./react`: keeping the framework-free half importable without a UI dependency is the point of the
|
|
41
43
|
split, not a stylistic preference, and importing `.`/`./core`/`./html` never pulls React in.
|
|
42
44
|
|
|
@@ -108,7 +110,7 @@ React components keep their props and framework callbacks keep the framework's s
|
|
|
108
110
|
| Handle check / parser | `isValidRegionHandle({ handle })`, `createParse5RegionParser()` |
|
|
109
111
|
| Parser port | `parser.findRegions({ html })`, `parser.checkWellFormed?.({ html })` |
|
|
110
112
|
| Document store port | `store.read()`, `store.write({ html })` |
|
|
111
|
-
| Session | `createVibecodingSession({ target }, { historyOptions })` |
|
|
113
|
+
| Session | `createVibecodingSession({ target }, { historyOptions?, history?, applyTurn? })` |
|
|
112
114
|
| Session methods | `session.subscribe({ listener })`, `session.readPart({ id })`, `session.applyEdits({ edits }, { label })`, `session.restoreSnapshot({ snapshot })` |
|
|
113
115
|
| React hook | `useVibecodingSession({ session })`; its `readPart` and `applyEdits` actions use the session method shapes |
|
|
114
116
|
| Registered tools | `createVibecodingToolRegistrations({ session }, { policy })` |
|
|
@@ -117,9 +119,9 @@ React components keep their props and framework callbacks keep the framework's s
|
|
|
117
119
|
`createParse5RegionParser()` always provides `checkWellFormed({ html })`; optional chaining
|
|
118
120
|
is only needed when working with the generic `HtmlRegionParser` port.
|
|
119
121
|
|
|
120
|
-
`VibecodingSessionArgs` holds the required target; `VibecodingSessionOptions`
|
|
121
|
-
|
|
122
|
-
session port. All are exported from `./react`. Tool input JSON remains unchanged: `edits` and
|
|
122
|
+
`VibecodingSessionArgs` holds the required target; `VibecodingSessionOptions` holds optional
|
|
123
|
+
`historyOptions`, injected `history` and whole-turn `applyTurn`. `UseVibecodingSessionArgs` and `VibecodingToolArgs` describe the required
|
|
124
|
+
session port. All are exported from `./session` and re-exported from `./react`. Tool input JSON remains unchanged: `edits` and
|
|
123
125
|
`label` still belong to the same tool input object.
|
|
124
126
|
|
|
125
127
|
```ts
|
|
@@ -152,3 +154,45 @@ belong to the host. Integration verification is deferred by owner directive; see
|
|
|
152
154
|
## Design decisions
|
|
153
155
|
|
|
154
156
|
- [Ambiguous region detection refuses rather than dropping a handle](docs/decisions/DR-001-conservative-region-detection.md).
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## Local app projects
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { openFileTreeProject } from '@jini-ai/vibecoding/node';
|
|
163
|
+
import { createVibecodingSession } from '@jini-ai/vibecoding/session';
|
|
164
|
+
|
|
165
|
+
const project = await openFileTreeProject({
|
|
166
|
+
root: appDirectory,
|
|
167
|
+
stateDir: privateHostStateDirectory, // outside appDirectory: dev servers must never serve history
|
|
168
|
+
policy: { allows: ({ relativePath }) => allowedAppPath(relativePath) },
|
|
169
|
+
// Optional validator.validate({candidate, signal}) checks the entire candidate once.
|
|
170
|
+
});
|
|
171
|
+
const session = createVibecodingSession({ target: project.target }, {
|
|
172
|
+
history: project.history,
|
|
173
|
+
applyTurn: project.applyEdits,
|
|
174
|
+
});
|
|
175
|
+
await project.applyTurn({
|
|
176
|
+
expectedRevision: await project.capture(),
|
|
177
|
+
changes: [{ op: 'create', relativePath: 'src/App.tsx', content: appSource }],
|
|
178
|
+
});
|
|
179
|
+
await session.undo(); // removes the created file and its empty managed folders
|
|
180
|
+
await project.close();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
PartIds are canonical relative paths. Explicit creates need no reservation; replacements/deletes
|
|
184
|
+
must name existing parts. The root remains the real working tree watched by Vite and other local
|
|
185
|
+
dev servers; dependencies, secrets and generated output are excluded. `capture()` records npm,
|
|
186
|
+
editor or user drift before a turn/undo so it is not silently overwritten. Revisions deduplicate
|
|
187
|
+
unchanged bytes in objects plus manifests. A pending journal rolls interrupted materialization
|
|
188
|
+
forward on reopen. History is session-local; another writer's generation clears it and returns `CONFLICT`.
|
|
189
|
+
|
|
190
|
+
Defaults: 2000 files, 2 MiB/file, 64 MiB/tree, depth 32, 100 changes/turn, 30-second validation,
|
|
191
|
+
50 history entries, and 200-character relative paths. A host allowlist always intersects mandatory
|
|
192
|
+
exclusions. Candidate-wide case/NFC checks permit case-only rename turns; nonportable names and
|
|
193
|
+
physical escapes refuse. Snapshots may expire once no longer retained by current/undo/redo.
|
|
194
|
+
|
|
195
|
+
Read [Node projects](docs/spec/api.spec.md#node-projects), the
|
|
196
|
+
[error table](docs/spec/errors.spec.md#node-project-errors), and
|
|
197
|
+
[DR-002](docs/decisions/DR-002-file-tree-revisions.md) for recovery and authority limits. No process,
|
|
198
|
+
package-install, compiler, preview or deployment implementation is included in this step.
|
package/dist/core/history.d.ts
CHANGED
|
@@ -127,6 +127,8 @@ export interface EditHistory {
|
|
|
127
127
|
}): Promise<HistoryEntry | null>;
|
|
128
128
|
/** Undoable entries, oldest first. For rendering a history UI; do not mutate. */
|
|
129
129
|
entries(): readonly HistoryEntry[];
|
|
130
|
+
/** Entries in both stacks, for pinning revisions during garbage collection. */
|
|
131
|
+
retainedEntries(): readonly HistoryEntry[];
|
|
130
132
|
/** Discard both stacks. */
|
|
131
133
|
clear(): void;
|
|
132
134
|
}
|
package/dist/core/history.js
CHANGED
package/dist/core/index.d.ts
CHANGED
|
@@ -16,4 +16,5 @@ export type { ProposedEdit } from "./apply.js";
|
|
|
16
16
|
export { applyEdit, applyEdits, correctionsFor } from "./apply.js";
|
|
17
17
|
export type { EditHistory, EditHistoryOptions, HistoryEntry, PartChange } from "./history.js";
|
|
18
18
|
export { createEditHistory } from "./history.js";
|
|
19
|
+
export * from './project.js';
|
|
19
20
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/core/index.js
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { EditHistory } from './history.js';
|
|
2
|
+
import type { EditTarget } from './target.js';
|
|
3
|
+
import type { ApplyOutcome } from './types.js';
|
|
4
|
+
import type { ProposedEdit } from './apply.js';
|
|
5
|
+
/** Creates name a relative path; existing files use the same canonical path as PartId. */
|
|
6
|
+
export type FileChange = {
|
|
7
|
+
readonly op: 'create';
|
|
8
|
+
readonly relativePath: string;
|
|
9
|
+
readonly content: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly op: 'replace';
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly content: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly op: 'delete';
|
|
16
|
+
readonly id: string;
|
|
17
|
+
};
|
|
18
|
+
export interface RevisionToken {
|
|
19
|
+
readonly revisionId: string;
|
|
20
|
+
readonly generation: number;
|
|
21
|
+
}
|
|
22
|
+
export interface ProjectDiagnostic {
|
|
23
|
+
readonly code: string;
|
|
24
|
+
readonly message: string;
|
|
25
|
+
readonly relativePath?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Byte reads return copies, so a validator cannot mutate the eventual commit. */
|
|
28
|
+
export interface ProjectCandidate {
|
|
29
|
+
readonly paths: readonly string[];
|
|
30
|
+
readBytes(input: {
|
|
31
|
+
readonly relativePath: string;
|
|
32
|
+
}, options?: Record<string, never>): Promise<Uint8Array>;
|
|
33
|
+
}
|
|
34
|
+
export interface ProjectValidator {
|
|
35
|
+
validate(input: {
|
|
36
|
+
readonly candidate: ProjectCandidate;
|
|
37
|
+
readonly signal: AbortSignal;
|
|
38
|
+
}, options?: Record<string, never>): Promise<{
|
|
39
|
+
readonly ok: boolean;
|
|
40
|
+
readonly diagnostics?: readonly ProjectDiagnostic[];
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
export type ProjectErrorCode = 'CONFLICT' | 'INVALID_PATH' | 'INVALID_CHANGE' | 'LIMIT' | 'CANCELLED' | 'VALIDATION' | 'STORAGE' | 'CORRUPT' | 'CLOSED' | 'SNAPSHOT_EXPIRED';
|
|
44
|
+
/** Stable project-boundary errors; native causes remain available to the host. */
|
|
45
|
+
export declare class ProjectError extends Error {
|
|
46
|
+
constructor(input: {
|
|
47
|
+
code: ProjectErrorCode;
|
|
48
|
+
message: string;
|
|
49
|
+
cause?: unknown;
|
|
50
|
+
});
|
|
51
|
+
readonly code: ProjectErrorCode;
|
|
52
|
+
}
|
|
53
|
+
export interface ProjectLimits {
|
|
54
|
+
readonly maxFiles: number;
|
|
55
|
+
readonly maxFileBytes: number;
|
|
56
|
+
readonly maxTreeBytes: number;
|
|
57
|
+
readonly maxDepth: number;
|
|
58
|
+
readonly maxChanges: number;
|
|
59
|
+
readonly validationTimeoutMs: number;
|
|
60
|
+
}
|
|
61
|
+
export type TurnResult = {
|
|
62
|
+
readonly status: 'committed' | 'unchanged' | 'rejected';
|
|
63
|
+
readonly revision: RevisionToken;
|
|
64
|
+
readonly diagnostics: readonly ProjectDiagnostic[];
|
|
65
|
+
};
|
|
66
|
+
export interface ProjectSnapshot {
|
|
67
|
+
readonly revisionId: string;
|
|
68
|
+
}
|
|
69
|
+
export interface FileTreeProject {
|
|
70
|
+
readonly target: EditTarget;
|
|
71
|
+
readonly history: EditHistory;
|
|
72
|
+
getRevision(): Promise<RevisionToken>;
|
|
73
|
+
/** Captures external edits as an ordinary undoable revision, without rewriting the tree. */
|
|
74
|
+
capture(): Promise<RevisionToken>;
|
|
75
|
+
applyTurn(input: {
|
|
76
|
+
readonly expectedRevision: RevisionToken;
|
|
77
|
+
readonly changes: readonly FileChange[];
|
|
78
|
+
}, options?: {
|
|
79
|
+
readonly label?: string;
|
|
80
|
+
readonly signal?: AbortSignal;
|
|
81
|
+
}): Promise<TurnResult>;
|
|
82
|
+
/** Session adapter: replacements share the same whole-candidate validation and history. */
|
|
83
|
+
applyEdits(input: {
|
|
84
|
+
readonly edits: readonly ProposedEdit[];
|
|
85
|
+
}, options?: {
|
|
86
|
+
readonly label?: string;
|
|
87
|
+
}): Promise<readonly ApplyOutcome[]>;
|
|
88
|
+
snapshot(): Promise<ProjectSnapshot>;
|
|
89
|
+
restore(input: {
|
|
90
|
+
readonly snapshot: ProjectSnapshot;
|
|
91
|
+
}, options?: {
|
|
92
|
+
readonly label?: string;
|
|
93
|
+
}): Promise<RevisionToken>;
|
|
94
|
+
close(): Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=project.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Stable project-boundary errors; native causes remain available to the host. */
|
|
2
|
+
export class ProjectError extends Error {
|
|
3
|
+
constructor(input) {
|
|
4
|
+
super(input.message, { cause: input.cause });
|
|
5
|
+
this.name = 'ProjectError';
|
|
6
|
+
this.code = input.code;
|
|
7
|
+
}
|
|
8
|
+
code;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=project.js.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure region edits over a caller-owned scan. Offsets always index the original string (UTF-16
|
|
3
|
+
* code units); scan each new string once and keep storage, version checks and policy in the host.
|
|
4
|
+
* Unlike the EditTarget allowlist, these helpers permit adding/removing handles. The ambiguity
|
|
5
|
+
* comparison only identifies handles whose duplicate count increases.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const found = locateRegion({ scan, handle: "hero" });
|
|
9
|
+
* if ("region" in found) replaceRegionInner({ html, region: found.region, fragment: "<h1>Hi</h1>" });
|
|
10
|
+
*/
|
|
11
|
+
import type { ParsedRegion } from "./regions.js";
|
|
12
|
+
/** A discovered handle, including unsafe regions so reads never silently lose an address. */
|
|
13
|
+
export interface ScannedRegion extends ParsedRegion {
|
|
14
|
+
readonly tag: string;
|
|
15
|
+
/** Outer offsets; -1 means parse5 could not locate the boundary. Never splice a problem region. */
|
|
16
|
+
readonly start: number;
|
|
17
|
+
readonly end: number;
|
|
18
|
+
/** Why this region cannot safely be replaced or moved. */
|
|
19
|
+
readonly problem?: string;
|
|
20
|
+
}
|
|
21
|
+
/** A direct fragment child, used for host tagging advice; implicit closes are allowed here. */
|
|
22
|
+
export interface HtmlTopLevelElement {
|
|
23
|
+
readonly tag: string;
|
|
24
|
+
readonly handle?: string;
|
|
25
|
+
readonly start: number;
|
|
26
|
+
readonly end: number;
|
|
27
|
+
readonly innerStart: number;
|
|
28
|
+
readonly innerEnd: number;
|
|
29
|
+
}
|
|
30
|
+
/** Reusable analysis of exactly one source string. Parse errors are diagnostics, not read failures. */
|
|
31
|
+
export interface HtmlRegionScan {
|
|
32
|
+
readonly regions: readonly ScannedRegion[];
|
|
33
|
+
readonly topLevel: readonly HtmlTopLevelElement[];
|
|
34
|
+
/** A raw-source handle could not be accounted for in the recovered tree. */
|
|
35
|
+
readonly documentProblem?: string;
|
|
36
|
+
readonly parseErrorReasons?: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
/** Why lookup could not identify a single safe span. Legacy handle values are deliberately retained. */
|
|
39
|
+
export type RegionLookupProblem = {
|
|
40
|
+
readonly kind: "absent";
|
|
41
|
+
readonly available: readonly string[];
|
|
42
|
+
} | {
|
|
43
|
+
readonly kind: "ambiguous";
|
|
44
|
+
readonly count: number;
|
|
45
|
+
} | {
|
|
46
|
+
readonly kind: "not-replaceable";
|
|
47
|
+
readonly tag: string;
|
|
48
|
+
};
|
|
49
|
+
export type RegionLookup = {
|
|
50
|
+
readonly region: ScannedRegion;
|
|
51
|
+
} | {
|
|
52
|
+
readonly problem: RegionLookupProblem;
|
|
53
|
+
};
|
|
54
|
+
/** Resolve one address, refusing duplicates and unsafe boundaries without throwing on reads. */
|
|
55
|
+
export declare function locateRegion({ scan, handle }: {
|
|
56
|
+
readonly scan: HtmlRegionScan;
|
|
57
|
+
readonly handle: string;
|
|
58
|
+
}, _options?: Record<string, never>): RegionLookup;
|
|
59
|
+
/**
|
|
60
|
+
* Replace INNER content only: the addressed element, its handle and every surrounding byte survive.
|
|
61
|
+
* The region must be safely located in this exact html string; this helper performs no policy check.
|
|
62
|
+
*/
|
|
63
|
+
export declare function replaceRegionInner({ html, region, fragment }: {
|
|
64
|
+
readonly html: string;
|
|
65
|
+
readonly region: ParsedRegion;
|
|
66
|
+
readonly fragment: string;
|
|
67
|
+
}, _options?: Record<string, never>): string;
|
|
68
|
+
export type RegionMoveProblem = "self" | "into-itself";
|
|
69
|
+
/**
|
|
70
|
+
* Move a WHOLE safely located element, carrying trailing whitespace (or leading whitespace at EOF).
|
|
71
|
+
* Refuse self/descendant destinations. Both regions must have been resolved from this exact source.
|
|
72
|
+
* Everything else retains its original bytes and order; an adjacent move may return identical HTML.
|
|
73
|
+
*/
|
|
74
|
+
export declare function moveRegion({ html, moving, target, placement }: {
|
|
75
|
+
readonly html: string;
|
|
76
|
+
readonly moving: ScannedRegion;
|
|
77
|
+
readonly target: ScannedRegion;
|
|
78
|
+
readonly placement: "before" | "after";
|
|
79
|
+
}, _options?: Record<string, never>): {
|
|
80
|
+
readonly html: string;
|
|
81
|
+
} | {
|
|
82
|
+
readonly problem: RegionMoveProblem;
|
|
83
|
+
};
|
|
84
|
+
/** Host-owned slug policy keeps suggestions compatible without adding a slugifier dependency. */
|
|
85
|
+
export interface RegionHandleOptions {
|
|
86
|
+
readonly slugify: (text: string) => string;
|
|
87
|
+
}
|
|
88
|
+
/** A top-level element needing a handle, with a suggestion only when its own heading provides one. */
|
|
89
|
+
export interface UntaggedSection {
|
|
90
|
+
readonly tag: string;
|
|
91
|
+
readonly suggestedHandle: string | undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Derive a stable handle from the first heading, stripping inline tags and truncating to 48 chars.
|
|
95
|
+
* Never suggest positional handles: moving sections must not change their addresses. Collisions,
|
|
96
|
+
* absent headings and empty slugs get no suggestion; the host chooses its own slugification policy.
|
|
97
|
+
*/
|
|
98
|
+
export declare function suggestRegionHandle({ inner, taken }: {
|
|
99
|
+
readonly inner: string;
|
|
100
|
+
readonly taken: ReadonlySet<string>;
|
|
101
|
+
}, { slugify }: RegionHandleOptions): string | undefined;
|
|
102
|
+
/** Find untagged top-level content; reserve each suggestion so a batch never creates duplicates. */
|
|
103
|
+
export declare function untaggedTopLevelSections({ html, scan }: {
|
|
104
|
+
readonly html: string;
|
|
105
|
+
readonly scan: HtmlRegionScan;
|
|
106
|
+
}, options: RegionHandleOptions): readonly UntaggedSection[];
|
|
107
|
+
/** Report newly worsened ambiguity, once per handle in after's source order; existing duplicates stay editable. */
|
|
108
|
+
export declare function handlesMadeAmbiguous({ before, after }: {
|
|
109
|
+
readonly before: HtmlRegionScan;
|
|
110
|
+
readonly after: HtmlRegionScan;
|
|
111
|
+
}, _options?: Record<string, never>): string[];
|
|
112
|
+
/** All handles in source order, preserving duplicates, empty values and legacy grammar. */
|
|
113
|
+
export declare function regionHandlesIn({ scan }: {
|
|
114
|
+
readonly scan: HtmlRegionScan;
|
|
115
|
+
}, _options?: Record<string, never>): string[];
|
|
116
|
+
//# sourceMappingURL=edit.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Resolve one address, refusing duplicates and unsafe boundaries without throwing on reads. */
|
|
2
|
+
export function locateRegion({ scan, handle }, _options = {}) {
|
|
3
|
+
const matches = scan.regions.filter((region) => region.handle === handle);
|
|
4
|
+
if (matches.length === 0)
|
|
5
|
+
return { problem: { kind: "absent", available: regionHandlesIn({ scan }) } };
|
|
6
|
+
if (matches.length > 1)
|
|
7
|
+
return { problem: { kind: "ambiguous", count: matches.length } };
|
|
8
|
+
const region = matches[0]; // Exactly one match on this branch.
|
|
9
|
+
if (region.problem !== undefined)
|
|
10
|
+
return { problem: { kind: "not-replaceable", tag: region.tag } };
|
|
11
|
+
return { region };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Replace INNER content only: the addressed element, its handle and every surrounding byte survive.
|
|
15
|
+
* The region must be safely located in this exact html string; this helper performs no policy check.
|
|
16
|
+
*/
|
|
17
|
+
export function replaceRegionInner({ html, region, fragment }, _options = {}) {
|
|
18
|
+
return html.slice(0, region.innerStart) + fragment + html.slice(region.innerEnd);
|
|
19
|
+
}
|
|
20
|
+
/** The end of the whitespace run adjoining an element, in either direction. */
|
|
21
|
+
function whitespaceRun(html, from, direction) {
|
|
22
|
+
let cursor = from;
|
|
23
|
+
while (/\s/.test(html[direction === 1 ? cursor : cursor - 1] ?? ""))
|
|
24
|
+
cursor += direction;
|
|
25
|
+
return cursor;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Move a WHOLE safely located element, carrying trailing whitespace (or leading whitespace at EOF).
|
|
29
|
+
* Refuse self/descendant destinations. Both regions must have been resolved from this exact source.
|
|
30
|
+
* Everything else retains its original bytes and order; an adjacent move may return identical HTML.
|
|
31
|
+
*/
|
|
32
|
+
export function moveRegion({ html, moving, target, placement }, _options = {}) {
|
|
33
|
+
if (moving.start === target.start)
|
|
34
|
+
return { problem: "self" };
|
|
35
|
+
if (target.start > moving.start && target.end <= moving.end)
|
|
36
|
+
return { problem: "into-itself" };
|
|
37
|
+
let cutStart = moving.start;
|
|
38
|
+
const cutEnd = whitespaceRun(html, moving.end, 1);
|
|
39
|
+
if (cutEnd === moving.end)
|
|
40
|
+
cutStart = whitespaceRun(html, moving.start, -1);
|
|
41
|
+
const element = html.slice(moving.start, moving.end);
|
|
42
|
+
const gap = cutEnd > moving.end ? html.slice(moving.end, cutEnd) : html.slice(cutStart, moving.start);
|
|
43
|
+
const without = html.slice(0, cutStart) + html.slice(cutEnd);
|
|
44
|
+
const anchor = placement === "before" ? target.start : target.end;
|
|
45
|
+
const at = anchor >= cutEnd ? anchor - (cutEnd - cutStart) : anchor;
|
|
46
|
+
const piece = placement === "before" ? element + gap : gap + element;
|
|
47
|
+
return { html: without.slice(0, at) + piece + without.slice(at) };
|
|
48
|
+
}
|
|
49
|
+
const HEADING_PATTERN = /<h[1-6]\b[^>]*>([\s\S]*?)<\/h[1-6]>/i;
|
|
50
|
+
/**
|
|
51
|
+
* Derive a stable handle from the first heading, stripping inline tags and truncating to 48 chars.
|
|
52
|
+
* Never suggest positional handles: moving sections must not change their addresses. Collisions,
|
|
53
|
+
* absent headings and empty slugs get no suggestion; the host chooses its own slugification policy.
|
|
54
|
+
*/
|
|
55
|
+
export function suggestRegionHandle({ inner, taken }, { slugify }) {
|
|
56
|
+
const heading = HEADING_PATTERN.exec(inner);
|
|
57
|
+
if (!heading)
|
|
58
|
+
return undefined;
|
|
59
|
+
const slug = slugify(heading[1].replace(/<[^>]*>/g, " ")).slice(0, 48).replace(/-+$/g, "");
|
|
60
|
+
if (slug === "" || taken.has(slug))
|
|
61
|
+
return undefined;
|
|
62
|
+
return slug;
|
|
63
|
+
}
|
|
64
|
+
// Stylesheets and metadata need no content address. Requiring nested elements to carry handles
|
|
65
|
+
// would impose a stricter contract than "every top-level section" and make ordinary pages unusable.
|
|
66
|
+
const UNTAGGABLE_TOP_LEVEL_ELEMENTS = new Set(["style", "script", "template", "link", "meta", "base", "title"]);
|
|
67
|
+
/** Find untagged top-level content; reserve each suggestion so a batch never creates duplicates. */
|
|
68
|
+
export function untaggedTopLevelSections({ html, scan }, options) {
|
|
69
|
+
const taken = new Set(regionHandlesIn({ scan }));
|
|
70
|
+
const offenders = [];
|
|
71
|
+
for (const element of scan.topLevel) {
|
|
72
|
+
if (element.handle !== undefined || UNTAGGABLE_TOP_LEVEL_ELEMENTS.has(element.tag))
|
|
73
|
+
continue;
|
|
74
|
+
const inner = html.slice(element.innerStart, element.innerEnd);
|
|
75
|
+
const suggestedHandle = suggestRegionHandle({ inner, taken }, options);
|
|
76
|
+
if (suggestedHandle !== undefined)
|
|
77
|
+
taken.add(suggestedHandle);
|
|
78
|
+
offenders.push({ tag: element.tag, suggestedHandle });
|
|
79
|
+
}
|
|
80
|
+
return offenders;
|
|
81
|
+
}
|
|
82
|
+
/** Count every discovered handle, including duplicates and unsafe regions. */
|
|
83
|
+
function countHandles(scan) {
|
|
84
|
+
const counts = new Map();
|
|
85
|
+
for (const { handle } of scan.regions)
|
|
86
|
+
counts.set(handle, (counts.get(handle) ?? 0) + 1);
|
|
87
|
+
return counts;
|
|
88
|
+
}
|
|
89
|
+
/** Report newly worsened ambiguity, once per handle in after's source order; existing duplicates stay editable. */
|
|
90
|
+
export function handlesMadeAmbiguous({ before, after }, _options = {}) {
|
|
91
|
+
const was = countHandles(before);
|
|
92
|
+
const offenders = [];
|
|
93
|
+
for (const [handle, count] of countHandles(after)) {
|
|
94
|
+
if (count > 1 && count > (was.get(handle) ?? 0))
|
|
95
|
+
offenders.push(handle);
|
|
96
|
+
}
|
|
97
|
+
return offenders;
|
|
98
|
+
}
|
|
99
|
+
/** All handles in source order, preserving duplicates, empty values and legacy grammar. */
|
|
100
|
+
export function regionHandlesIn({ scan }, _options = {}) {
|
|
101
|
+
return scan.regions.map((region) => region.handle);
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=edit.js.map
|
package/dist/html/index.d.ts
CHANGED
|
@@ -9,4 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export type { HtmlDocumentStore, HtmlRegionParser, HtmlRegionTargetDeps, HtmlRegionTargetOptions, ParsedRegion, } from "./regions.js";
|
|
11
11
|
export { AGENT_ELEMENT_ATTRIBUTE, createHtmlRegionTarget, isValidRegionHandle, } from "./regions.js";
|
|
12
|
+
export type { HtmlRegionScan, HtmlTopLevelElement, RegionHandleOptions, RegionLookup, RegionLookupProblem, RegionMoveProblem, ScannedRegion, UntaggedSection, } from "./edit.js";
|
|
13
|
+
export { handlesMadeAmbiguous, locateRegion, moveRegion, regionHandlesIn, replaceRegionInner, suggestRegionHandle, untaggedTopLevelSections, } from "./edit.js";
|
|
12
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/html/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { AGENT_ELEMENT_ATTRIBUTE, createHtmlRegionTarget, isValidRegionHandle, } from "./regions.js";
|
|
2
|
+
export { handlesMadeAmbiguous, locateRegion, moveRegion, regionHandlesIn, replaceRegionInner, suggestRegionHandle, untaggedTopLevelSections, } from "./edit.js";
|
|
2
3
|
//# sourceMappingURL=index.js.map
|
|
@@ -6,5 +6,6 @@
|
|
|
6
6
|
* `./parse5-region-parser.ts` for the implementation and its findings.
|
|
7
7
|
* See docs/decisions/DR-001-conservative-region-detection.md.
|
|
8
8
|
*/
|
|
9
|
-
export { createParse5RegionParser, isValidRegionHandle } from "./parse5-region-parser.js";
|
|
9
|
+
export { createParse5RegionParser, isValidRegionHandle, scanHtmlRegions } from "./parse5-region-parser.js";
|
|
10
|
+
export type { HtmlRegionScan, HtmlTopLevelElement, ScannedRegion } from "../edit.js";
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/html/node/index.js
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
* `./parse5-region-parser.ts` for the implementation and its findings.
|
|
7
7
|
* See docs/decisions/DR-001-conservative-region-detection.md.
|
|
8
8
|
*/
|
|
9
|
-
export { createParse5RegionParser, isValidRegionHandle } from "./parse5-region-parser.js";
|
|
9
|
+
export { createParse5RegionParser, isValidRegionHandle, scanHtmlRegions } from "./parse5-region-parser.js";
|
|
10
10
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,11 +1,23 @@
|
|
|
1
|
+
import type { HtmlRegionScan } from "../edit.js";
|
|
1
2
|
import { isValidRegionHandle, type HtmlRegionParser } from "../regions.js";
|
|
3
|
+
/**
|
|
4
|
+
* Scan once without throwing for malformed markup: unsafe regions carry their own problem, while
|
|
5
|
+
* unaccounted-for source handles produce documentProblem. Every offset indexes the untouched input.
|
|
6
|
+
* Comments/raw-text bodies are blanked by parse5's own locations before the raw-mention check.
|
|
7
|
+
* @example const scan = scanHtmlRegions({ html: '<section data-agent-element="hero">Hi</section>' });
|
|
8
|
+
* @complexity One parse and node walk, plus source-order sorting: O(n + r log r) for n source code
|
|
9
|
+
* units and r discovered elements. Masked ranges are disjoint, so blanking is linear in source size.
|
|
10
|
+
*/
|
|
11
|
+
export declare function scanHtmlRegions({ html: html_ }: {
|
|
12
|
+
readonly html: string;
|
|
13
|
+
}, _options?: Record<string, never>): HtmlRegionScan;
|
|
2
14
|
/**
|
|
3
15
|
* Build the parse5-backed `HtmlRegionParser`. Stateless — a single instance is safe to share as a
|
|
4
16
|
* module-level singleton across every call, per `./regions.js`'s injected-port contract.
|
|
5
17
|
*
|
|
6
18
|
* @returns a parser ready to hand to `createHtmlRegionTarget`, with `checkWellFormed` always
|
|
7
19
|
* present even though the generic parser port allows implementations to omit it.
|
|
8
|
-
* @complexity 1 (delegates
|
|
20
|
+
* @complexity 1 (delegates to `scanHtmlRegions`)
|
|
9
21
|
*/
|
|
10
22
|
export declare function createParse5RegionParser(): HtmlRegionParser & Required<Pick<HtmlRegionParser, "checkWellFormed">>;
|
|
11
23
|
export { isValidRegionHandle };
|