@adecore/agent-contracts 0.38.0 → 0.40.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/dist/chat.js +3 -3
- package/dist/text.js +2 -1
- package/package.json +4 -4
- package/src/agent.ts +1 -0
- package/src/chat.ts +15 -4
- package/src/envelope.ts +1 -0
- package/src/model.ts +4 -0
- package/src/text.ts +10 -1
- package/src/usage.ts +3 -0
- package/src/visual.ts +60 -0
package/dist/chat.js
CHANGED
|
@@ -107,9 +107,9 @@ export const ChatUiAnswerSchema = ChatUiChoiceOriginSchema.extend({
|
|
|
107
107
|
queued: z.boolean(),
|
|
108
108
|
turnId: z.string().min(1)
|
|
109
109
|
});
|
|
110
|
-
// A message typed while a turn was running; the daemon sends it when that turn settles.
|
|
111
110
|
/* The reply a quoted passage was taken from, so a client can lead the reader back to it. */
|
|
112
111
|
export const ChatQuoteSourceSchema = z.object({ itemId: z.string().min(1).max(256) });
|
|
112
|
+
// A message typed while a turn was running; the daemon sends it when that turn settles.
|
|
113
113
|
export const ChatQueuedMessageSchema = z.object({
|
|
114
114
|
id: z.string().min(1),
|
|
115
115
|
// Older saved queues have no reserved turn yet; draining them still mints one.
|
|
@@ -430,13 +430,13 @@ export const ChatSubagentUsageSchema = z.object({
|
|
|
430
430
|
toolUses: z.number().int().nonnegative(),
|
|
431
431
|
durationMs: z.number().int().nonnegative()
|
|
432
432
|
});
|
|
433
|
+
// The `origin` of a subagent row a host opened with a task of its own rather than the CLI with its tool; a legacy spelling that stored threads carry.
|
|
434
|
+
export const HOST_TASK_ORIGIN = 'ruimte';
|
|
433
435
|
/*
|
|
434
436
|
* One agent the agent delegated to, foreground or background. Its own work stays in the thread as
|
|
435
437
|
* ordinary items that carry `parentToolUseId`, so streaming keeps working; the timeline gathers
|
|
436
438
|
* them under this row. `result` is the report it ended with, as markdown.
|
|
437
439
|
*/
|
|
438
|
-
// The `origin` of a subagent row a host opened with a task of its own rather than the CLI with its tool; a legacy spelling that stored threads carry.
|
|
439
|
-
export const HOST_TASK_ORIGIN = 'ruimte';
|
|
440
440
|
export const ChatSubagentItemSchema = z.object({
|
|
441
441
|
...base,
|
|
442
442
|
kind: z.literal('subagent'),
|
package/dist/text.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
let graphemes;
|
|
2
2
|
/*
|
|
3
3
|
* The longest start of `text` that fits in `max` UTF-16 units, the measure a schema's `max` counts in,
|
|
4
4
|
* cut only between characters as a person sees them. A cut between the halves of a surrogate pair
|
|
@@ -8,6 +8,7 @@ export function clipText(text, max) {
|
|
|
8
8
|
if (text.length <= max) {
|
|
9
9
|
return text;
|
|
10
10
|
}
|
|
11
|
+
graphemes ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
11
12
|
let end = 0;
|
|
12
13
|
for (const { segment } of graphemes.segment(text)) {
|
|
13
14
|
if (end + segment.length > max) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agent-contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Schemas and port contracts for agent chats, provider accounts, models and usage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -86,11 +86,11 @@
|
|
|
86
86
|
"test": "bun --conditions=source test"
|
|
87
87
|
},
|
|
88
88
|
"dependencies": {
|
|
89
|
-
"@adecore/intelligent-ui": "0.
|
|
90
|
-
"zod": "^4.6.
|
|
89
|
+
"@adecore/intelligent-ui": "0.40.0",
|
|
90
|
+
"zod": "^4.6.7"
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
|
93
|
-
"@types/bun": "^1.4.
|
|
93
|
+
"@types/bun": "^1.4.4",
|
|
94
94
|
"typescript": "^7.0.2"
|
|
95
95
|
},
|
|
96
96
|
"license": "FSL-1.1-MIT",
|
package/src/agent.ts
CHANGED
package/src/chat.ts
CHANGED
|
@@ -60,18 +60,23 @@ const IMAGE_MIME_BY_EXTENSION = new Map([
|
|
|
60
60
|
['webp', 'image/webp'],
|
|
61
61
|
['gif', 'image/gif']
|
|
62
62
|
]);
|
|
63
|
+
|
|
63
64
|
const IMAGE_MIME_TYPES = new Set(IMAGE_MIME_BY_EXTENSION.values());
|
|
64
65
|
|
|
65
66
|
// A generic MIME type from Finder may still name a picture; a declared PDF must stay a PDF.
|
|
66
67
|
export function attachmentImageMime({ name, mime }: { name: string; mime: string }): string | null {
|
|
67
68
|
const type = mime.split(';')[0]!.trim().toLowerCase();
|
|
69
|
+
|
|
68
70
|
if (IMAGE_MIME_TYPES.has(type)) {
|
|
69
71
|
return type;
|
|
70
72
|
}
|
|
73
|
+
|
|
71
74
|
if (type !== '' && type !== 'application/octet-stream' && type !== 'binary/octet-stream') {
|
|
72
75
|
return null;
|
|
73
76
|
}
|
|
77
|
+
|
|
74
78
|
const dot = name.lastIndexOf('.');
|
|
79
|
+
|
|
75
80
|
return dot < 0 ? null : (IMAGE_MIME_BY_EXTENSION.get(name.slice(dot + 1).toLowerCase()) ?? null);
|
|
76
81
|
}
|
|
77
82
|
|
|
@@ -129,11 +134,11 @@ export const ChatUiAnswerSchema = ChatUiChoiceOriginSchema.extend({
|
|
|
129
134
|
});
|
|
130
135
|
export type ChatUiAnswer = z.infer<typeof ChatUiAnswerSchema>;
|
|
131
136
|
|
|
132
|
-
// A message typed while a turn was running; the daemon sends it when that turn settles.
|
|
133
137
|
/* The reply a quoted passage was taken from, so a client can lead the reader back to it. */
|
|
134
138
|
export const ChatQuoteSourceSchema = z.object({ itemId: z.string().min(1).max(256) });
|
|
135
139
|
export type ChatQuoteSource = z.infer<typeof ChatQuoteSourceSchema>;
|
|
136
140
|
|
|
141
|
+
// A message typed while a turn was running; the daemon sends it when that turn settles.
|
|
137
142
|
export const ChatQueuedMessageSchema = z.object({
|
|
138
143
|
id: z.string().min(1),
|
|
139
144
|
// Older saved queues have no reserved turn yet; draining them still mints one.
|
|
@@ -518,14 +523,14 @@ export const ChatSubagentUsageSchema = z.object({
|
|
|
518
523
|
});
|
|
519
524
|
export type ChatSubagentUsage = z.infer<typeof ChatSubagentUsageSchema>;
|
|
520
525
|
|
|
526
|
+
// The `origin` of a subagent row a host opened with a task of its own rather than the CLI with its tool; a legacy spelling that stored threads carry.
|
|
527
|
+
export const HOST_TASK_ORIGIN = 'ruimte';
|
|
528
|
+
|
|
521
529
|
/*
|
|
522
530
|
* One agent the agent delegated to, foreground or background. Its own work stays in the thread as
|
|
523
531
|
* ordinary items that carry `parentToolUseId`, so streaming keeps working; the timeline gathers
|
|
524
532
|
* them under this row. `result` is the report it ended with, as markdown.
|
|
525
533
|
*/
|
|
526
|
-
// The `origin` of a subagent row a host opened with a task of its own rather than the CLI with its tool; a legacy spelling that stored threads carry.
|
|
527
|
-
export const HOST_TASK_ORIGIN = 'ruimte';
|
|
528
|
-
|
|
529
534
|
export const ChatSubagentItemSchema = z.object({
|
|
530
535
|
...base,
|
|
531
536
|
kind: z.literal('subagent'),
|
|
@@ -733,6 +738,7 @@ export function activeSubagents(items: readonly ChatItem[]): ChatSubagentItem[]
|
|
|
733
738
|
const lastMessageAt = lastMessage < 0 ? null : items[lastMessage]!.createdAt;
|
|
734
739
|
const rows = items.flatMap((item, index) => (item.kind === 'subagent' ? [{ item, index }] : []));
|
|
735
740
|
const turns = new Set(rows.flatMap(({ item }) => (item.status === 'running' && item.turnId !== null ? [item.turnId] : [])));
|
|
741
|
+
|
|
736
742
|
return rows
|
|
737
743
|
.filter(
|
|
738
744
|
({ item, index }) =>
|
|
@@ -824,14 +830,17 @@ export const ChatAttachPayloadSchema = ChatTargetPayloadSchema.extend({
|
|
|
824
830
|
// The last seq this client saw; honored when the daemon still holds everything after it.
|
|
825
831
|
since: z.number().int().nonnegative().optional()
|
|
826
832
|
});
|
|
833
|
+
|
|
827
834
|
export const ChatHistoryPayloadSchema = ChatTargetPayloadSchema.extend({
|
|
828
835
|
cursor: z.string().min(1).max(128),
|
|
829
836
|
limit: z.number().int().min(1).max(100).optional()
|
|
830
837
|
});
|
|
838
|
+
|
|
831
839
|
export const ChatHistoryPageSchema = z.object({
|
|
832
840
|
start: z.number().int().nonnegative(),
|
|
833
841
|
cursor: z.string().nullable()
|
|
834
842
|
});
|
|
843
|
+
|
|
835
844
|
export const ChatHistoryResultSchema = z.object({ items: z.array(ChatItemSchema), history: ChatHistoryPageSchema });
|
|
836
845
|
export type ChatHistoryResult = z.infer<typeof ChatHistoryResultSchema>;
|
|
837
846
|
|
|
@@ -1159,6 +1168,8 @@ export function generatedImageAttachment(item: ChatItem): ChatAttachment | null
|
|
|
1159
1168
|
if (item.kind !== 'tool' || item.name !== 'ImageGeneration') {
|
|
1160
1169
|
return null;
|
|
1161
1170
|
}
|
|
1171
|
+
|
|
1162
1172
|
const parsed = GeneratedImageInputSchema.safeParse(item.input);
|
|
1173
|
+
|
|
1163
1174
|
return parsed.success ? (parsed.data.attachment ?? null) : null;
|
|
1164
1175
|
}
|
package/src/envelope.ts
CHANGED
package/src/model.ts
CHANGED
|
@@ -155,6 +155,7 @@ export const ProviderInstallSchema = z.object({
|
|
|
155
155
|
signIn: z.array(z.unknown()).transform((ways) =>
|
|
156
156
|
ways.flatMap((way) => {
|
|
157
157
|
const parsed = ProviderSignInSchema.safeParse(way);
|
|
158
|
+
|
|
158
159
|
return parsed.success ? [parsed.data] : [];
|
|
159
160
|
})
|
|
160
161
|
)
|
|
@@ -204,7 +205,9 @@ export function pickerGroupsOf<T extends { kind: AgentKind; name: string }>(prov
|
|
|
204
205
|
nameCollator ??= new Intl.Collator(undefined, { sensitivity: 'base' });
|
|
205
206
|
const collator = nameCollator;
|
|
206
207
|
const sorted = [...providers].sort((a, b) => collator.compare(a.name, b.name));
|
|
208
|
+
|
|
207
209
|
const inGroup = (group: ProviderPickerGroup): T[] => sorted.filter((provider) => providerGroupOf(provider.kind) === group);
|
|
210
|
+
|
|
208
211
|
return { frontier: inGroup('frontier'), others: inGroup('others'), api: inGroup('api') };
|
|
209
212
|
}
|
|
210
213
|
|
|
@@ -220,6 +223,7 @@ export type ProviderListResult = z.infer<typeof ProviderListResultSchema>;
|
|
|
220
223
|
*/
|
|
221
224
|
export function resumeCommandFor(template: string, agentSessionId: string, flags: string[] = []): string {
|
|
222
225
|
const id = `'${agentSessionId.replaceAll("'", `'\\''`)}'`;
|
|
226
|
+
|
|
223
227
|
return template
|
|
224
228
|
.split(' ')
|
|
225
229
|
.flatMap((word) => (word === '{flags}' ? flags : [word.replace('{id}', id)]))
|
package/src/text.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
let graphemes: Intl.Segmenter | undefined;
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
* The longest start of `text` that fits in `max` UTF-16 units, the measure a schema's `max` counts in,
|
|
@@ -9,13 +9,18 @@ export function clipText(text: string, max: number): string {
|
|
|
9
9
|
if (text.length <= max) {
|
|
10
10
|
return text;
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
graphemes ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
12
14
|
let end = 0;
|
|
15
|
+
|
|
13
16
|
for (const { segment } of graphemes.segment(text)) {
|
|
14
17
|
if (end + segment.length > max) {
|
|
15
18
|
break;
|
|
16
19
|
}
|
|
20
|
+
|
|
17
21
|
end += segment.length;
|
|
18
22
|
}
|
|
23
|
+
|
|
19
24
|
return text.slice(0, end);
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -26,12 +31,16 @@ export const TOOL_OUTPUT_TAIL = 64 * 1024;
|
|
|
26
31
|
export function appendToolOutput(output: string | null, text: string): string {
|
|
27
32
|
const whole = (output ?? '') + text;
|
|
28
33
|
let start = whole.length - TOOL_OUTPUT_TAIL;
|
|
34
|
+
|
|
29
35
|
if (start <= 0) {
|
|
30
36
|
return whole;
|
|
31
37
|
}
|
|
38
|
+
|
|
32
39
|
const unit = whole.charCodeAt(start);
|
|
40
|
+
|
|
33
41
|
if (unit >= 0xdc00 && unit <= 0xdfff) {
|
|
34
42
|
start += 1;
|
|
35
43
|
}
|
|
44
|
+
|
|
36
45
|
return whole.slice(start);
|
|
37
46
|
}
|
package/src/usage.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { ProviderAccountIdSchema } from './provider-accounts.ts';
|
|
|
4
4
|
/* The providers whose usage the daemon reads: the CLIs' own records, and the log a host keeps of what it calls itself. A subset of `AgentKindSchema`: it grows with the readers. */
|
|
5
5
|
export const UsageProviderSchema = z.enum(['claude', 'codex', 'nanogpt', 'grok', 'copilot', 'anthropic-claude-api']);
|
|
6
6
|
export type UsageProvider = z.infer<typeof UsageProviderSchema>;
|
|
7
|
+
|
|
7
8
|
export const USAGE_PROVIDERS = UsageProviderSchema.options;
|
|
8
9
|
|
|
9
10
|
export const UsageResolutionSchema = z.enum(['hour', 'day']);
|
|
@@ -194,8 +195,10 @@ export function evenPace(window: UsageWindow, now: number): EvenPace | null {
|
|
|
194
195
|
if (window.resetsAt === null || window.durationMs === null || window.durationMs <= 0) {
|
|
195
196
|
return null;
|
|
196
197
|
}
|
|
198
|
+
|
|
197
199
|
const percent = Math.round(Math.min(1, Math.max(0, 1 - (window.resetsAt - now) / window.durationMs)) * 100);
|
|
198
200
|
const used = Math.round(window.used * 100);
|
|
201
|
+
|
|
199
202
|
return { percent, standing: used > percent ? 'ahead' : used < percent ? 'behind' : 'even' };
|
|
200
203
|
}
|
|
201
204
|
|
package/src/visual.ts
CHANGED
|
@@ -63,17 +63,21 @@ function clampHeight(height: number): number {
|
|
|
63
63
|
export function visualFrameHeight(visual: Pick<ChatVisual, 'maxHeight' | 'heights'>, width: number): number | undefined {
|
|
64
64
|
let below: VisualHeight | undefined;
|
|
65
65
|
let above: VisualHeight | undefined;
|
|
66
|
+
|
|
66
67
|
for (const measured of visual.heights ?? []) {
|
|
67
68
|
if (measured[0] <= width && (below === undefined || measured[0] > below[0])) {
|
|
68
69
|
below = measured;
|
|
69
70
|
}
|
|
71
|
+
|
|
70
72
|
if (measured[0] >= width && (above === undefined || measured[0] < above[0])) {
|
|
71
73
|
above = measured;
|
|
72
74
|
}
|
|
73
75
|
}
|
|
76
|
+
|
|
74
77
|
if (below === undefined && above === undefined) {
|
|
75
78
|
return undefined;
|
|
76
79
|
}
|
|
80
|
+
|
|
77
81
|
return clampHeight(Math.min(Math.max(below?.[1] ?? 0, above?.[1] ?? 0), visual.maxHeight));
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -174,8 +178,10 @@ function webUrl(value: unknown): string | undefined {
|
|
|
174
178
|
if (typeof value !== 'string') {
|
|
175
179
|
return undefined;
|
|
176
180
|
}
|
|
181
|
+
|
|
177
182
|
try {
|
|
178
183
|
const url = new (globalThis as unknown as { URL: UrlParser }).URL(value);
|
|
184
|
+
|
|
179
185
|
return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : undefined;
|
|
180
186
|
} catch {
|
|
181
187
|
return undefined;
|
|
@@ -190,7 +196,9 @@ export function parseVisualMessage(data: unknown): VisualMessage | undefined {
|
|
|
190
196
|
if (!isRecord(data) || data.jsonrpc !== '2.0' || typeof data.method !== 'string') {
|
|
191
197
|
return undefined;
|
|
192
198
|
}
|
|
199
|
+
|
|
193
200
|
const params = isRecord(data.params) ? data.params : {};
|
|
201
|
+
|
|
194
202
|
switch (data.method) {
|
|
195
203
|
case VISUAL_BRIDGE_METHODS.sandboxProxyReady:
|
|
196
204
|
return { method: data.method };
|
|
@@ -200,15 +208,19 @@ export function parseVisualMessage(data: unknown): VisualMessage | undefined {
|
|
|
200
208
|
if (params.theme !== 'light' && params.theme !== 'dark') {
|
|
201
209
|
return undefined;
|
|
202
210
|
}
|
|
211
|
+
|
|
203
212
|
const styles = isRecord(params.styles) ? params.styles : {};
|
|
213
|
+
|
|
204
214
|
return { method: data.method, theme: { appearance: params.theme, variables: cleanVisualVariables(styles.variables) } };
|
|
205
215
|
}
|
|
206
216
|
case VISUAL_BRIDGE_METHODS.sizeChanged: {
|
|
207
217
|
const height = sizeOf(params.height);
|
|
208
218
|
const width = sizeOf(params.width);
|
|
219
|
+
|
|
209
220
|
if (height === undefined) {
|
|
210
221
|
return undefined;
|
|
211
222
|
}
|
|
223
|
+
|
|
212
224
|
return width === undefined ? { method: data.method, height } : { method: data.method, width, height };
|
|
213
225
|
}
|
|
214
226
|
case VISUAL_BRIDGE_METHODS.viewportReady:
|
|
@@ -217,26 +229,33 @@ export function parseVisualMessage(data: unknown): VisualMessage | undefined {
|
|
|
217
229
|
if (data.params === null) {
|
|
218
230
|
return { method: data.method, viewport: null };
|
|
219
231
|
}
|
|
232
|
+
|
|
220
233
|
const top = sizeOf(params.top);
|
|
221
234
|
const height = sizeOf(params.height);
|
|
235
|
+
|
|
222
236
|
return top !== undefined && height !== undefined && height > 0 ? { method: data.method, viewport: { top, height } } : undefined;
|
|
223
237
|
}
|
|
224
238
|
case VISUAL_BRIDGE_METHODS.scrollRequest: {
|
|
225
239
|
if (typeof params.by === 'number' && Number.isFinite(params.by)) {
|
|
226
240
|
return { method: data.method, request: { by: Math.max(-100_000, Math.min(params.by, 100_000)) } };
|
|
227
241
|
}
|
|
242
|
+
|
|
228
243
|
const top = sizeOf(params.to);
|
|
244
|
+
|
|
229
245
|
if (top !== undefined) {
|
|
230
246
|
return { method: data.method, request: { to: top } };
|
|
231
247
|
}
|
|
248
|
+
|
|
232
249
|
return params.edge === 'start' || params.edge === 'end' ? { method: data.method, request: { edge: params.edge } } : undefined;
|
|
233
250
|
}
|
|
234
251
|
case VISUAL_BRIDGE_METHODS.openLink: {
|
|
235
252
|
const url = webUrl(params.url);
|
|
236
253
|
const id = data.id;
|
|
254
|
+
|
|
237
255
|
if (url === undefined || !(typeof id === 'string' || (typeof id === 'number' && Number.isFinite(id)))) {
|
|
238
256
|
return undefined;
|
|
239
257
|
}
|
|
258
|
+
|
|
240
259
|
return { method: data.method, id, url };
|
|
241
260
|
}
|
|
242
261
|
default:
|
|
@@ -377,14 +396,17 @@ const MAX_VALUE_LENGTH = 512;
|
|
|
377
396
|
/* The variables of a theme a page may be given, from anything: the names it may hold and the values that cannot break out of their rule. */
|
|
378
397
|
export function cleanVisualVariables(input: unknown): Record<string, string> {
|
|
379
398
|
const clean: Record<string, string> = {};
|
|
399
|
+
|
|
380
400
|
if (!isRecord(input)) {
|
|
381
401
|
return clean;
|
|
382
402
|
}
|
|
403
|
+
|
|
383
404
|
for (const [name, value] of Object.entries(input)) {
|
|
384
405
|
if (VARIABLE_NAME.test(name) && typeof value === 'string' && value.length <= MAX_VALUE_LENGTH && !UNSAFE_VALUE.test(value) && value.trim() !== '') {
|
|
385
406
|
clean[name] = value.trim();
|
|
386
407
|
}
|
|
387
408
|
}
|
|
409
|
+
|
|
388
410
|
return clean;
|
|
389
411
|
}
|
|
390
412
|
|
|
@@ -421,6 +443,7 @@ const BASE_CSS = [
|
|
|
421
443
|
|
|
422
444
|
function rootRule(theme: VisualTheme): string {
|
|
423
445
|
const declarations = Object.entries(theme.variables).map(([name, value]) => `${name}:${value};`);
|
|
446
|
+
|
|
424
447
|
return `:root{color-scheme:${theme.appearance};\n${declarations.join('\n')}\n}`;
|
|
425
448
|
}
|
|
426
449
|
|
|
@@ -445,6 +468,7 @@ function bootstrapScript(): string {
|
|
|
445
468
|
sizeChanged: VISUAL_BRIDGE_METHODS.sizeChanged,
|
|
446
469
|
openLink: VISUAL_BRIDGE_METHODS.openLink
|
|
447
470
|
};
|
|
471
|
+
|
|
448
472
|
return `(function () {
|
|
449
473
|
var script = document.currentScript;
|
|
450
474
|
var style = script && script.previousElementSibling;
|
|
@@ -540,6 +564,7 @@ if (framed && typeof ResizeObserver === 'function') {
|
|
|
540
564
|
|
|
541
565
|
function bootstrapMarkup(viewport: boolean): string {
|
|
542
566
|
const theme = `${rootRule(VISUAL_DARK_THEME)}\n@media (prefers-color-scheme: light){\n${rootRule(VISUAL_LIGHT_THEME)}\n}\n${BASE_CSS}`;
|
|
567
|
+
|
|
543
568
|
return [
|
|
544
569
|
// Always first: the stored file is UTF-8 whatever the page declares, and the first charset wins.
|
|
545
570
|
'<meta charset="utf-8">',
|
|
@@ -564,41 +589,56 @@ function commentEnd(html: string, from: number): number {
|
|
|
564
589
|
if (html.startsWith('>', from)) {
|
|
565
590
|
return from + 1;
|
|
566
591
|
}
|
|
592
|
+
|
|
567
593
|
if (html.startsWith('->', from)) {
|
|
568
594
|
return from + 2;
|
|
569
595
|
}
|
|
596
|
+
|
|
570
597
|
const ends = [html.indexOf('-->', from), html.indexOf('--!>', from)].filter((index) => index >= 0);
|
|
598
|
+
|
|
571
599
|
if (ends.length === 0) {
|
|
572
600
|
return html.length;
|
|
573
601
|
}
|
|
602
|
+
|
|
574
603
|
const end = Math.min(...ends);
|
|
604
|
+
|
|
575
605
|
return end + (html.startsWith('-->', end) ? 3 : 4);
|
|
576
606
|
}
|
|
577
607
|
|
|
578
608
|
// Past the `>` of a tag, skipping a quoted attribute value, which may hold one.
|
|
579
609
|
function tagEnd(html: string, from: number): number {
|
|
580
610
|
let at = from;
|
|
611
|
+
|
|
581
612
|
while (at < html.length) {
|
|
582
613
|
const char = html[at];
|
|
614
|
+
|
|
583
615
|
if (char === '>') {
|
|
584
616
|
return at + 1;
|
|
585
617
|
}
|
|
618
|
+
|
|
586
619
|
at++;
|
|
620
|
+
|
|
587
621
|
if (char !== '=') {
|
|
588
622
|
continue;
|
|
589
623
|
}
|
|
624
|
+
|
|
590
625
|
while (at < html.length && /\s/.test(html[at]!)) {
|
|
591
626
|
at++;
|
|
592
627
|
}
|
|
628
|
+
|
|
593
629
|
const quote = html[at];
|
|
630
|
+
|
|
594
631
|
if (quote === '"' || quote === "'") {
|
|
595
632
|
const close = html.indexOf(quote, at + 1);
|
|
633
|
+
|
|
596
634
|
if (close < 0) {
|
|
597
635
|
return html.length;
|
|
598
636
|
}
|
|
637
|
+
|
|
599
638
|
at = close + 1;
|
|
600
639
|
}
|
|
601
640
|
}
|
|
641
|
+
|
|
602
642
|
return html.length;
|
|
603
643
|
}
|
|
604
644
|
|
|
@@ -606,9 +646,11 @@ function rawTextEnd(html: string, name: string, from: number): number {
|
|
|
606
646
|
if (name === 'plaintext') {
|
|
607
647
|
return html.length;
|
|
608
648
|
}
|
|
649
|
+
|
|
609
650
|
const close = new RegExp(`</${name}[\\s/>]`, 'gi');
|
|
610
651
|
close.lastIndex = from;
|
|
611
652
|
const found = close.exec(html);
|
|
653
|
+
|
|
612
654
|
return found === null ? html.length : tagEnd(html, found.index + 2);
|
|
613
655
|
}
|
|
614
656
|
|
|
@@ -618,15 +660,20 @@ function rawTextEnd(html: string, name: string, from: number): number {
|
|
|
618
660
|
*/
|
|
619
661
|
function* tokens(html: string): Generator<Token> {
|
|
620
662
|
let at = 0;
|
|
663
|
+
|
|
621
664
|
while (at < html.length) {
|
|
622
665
|
const open = html.indexOf('<', at);
|
|
666
|
+
|
|
623
667
|
if (open < 0) {
|
|
624
668
|
yield { kind: 'text', name: '', start: at, end: html.length };
|
|
669
|
+
|
|
625
670
|
return;
|
|
626
671
|
}
|
|
672
|
+
|
|
627
673
|
const next = html[open + 1] ?? '';
|
|
628
674
|
let token: Token | null = null;
|
|
629
675
|
let resume = 0;
|
|
676
|
+
|
|
630
677
|
if (html.startsWith('<!--', open)) {
|
|
631
678
|
token = { kind: 'comment', name: '', start: open, end: commentEnd(html, open + 4) };
|
|
632
679
|
} else if (next === '!' || next === '?') {
|
|
@@ -639,21 +686,26 @@ function* tokens(html: string): Generator<Token> {
|
|
|
639
686
|
} else if (/[a-z]/i.test(next)) {
|
|
640
687
|
const name = /^<([a-z][^\s/>]*)/i.exec(html.slice(open, open + 64))![1]!.toLowerCase();
|
|
641
688
|
token = { kind: 'start', name, start: open, end: tagEnd(html, open + 1 + name.length) };
|
|
689
|
+
|
|
642
690
|
if (RAW_TEXT.has(name)) {
|
|
643
691
|
resume = rawTextEnd(html, name, token.end);
|
|
644
692
|
}
|
|
645
693
|
}
|
|
694
|
+
|
|
646
695
|
if (token === null) {
|
|
647
696
|
// A `<` that opens nothing is text, which goes on to the next `<`.
|
|
648
697
|
const following = html.indexOf('<', open + 1);
|
|
649
698
|
const end = following < 0 ? html.length : following;
|
|
650
699
|
yield { kind: 'text', name: '', start: at, end };
|
|
651
700
|
at = end;
|
|
701
|
+
|
|
652
702
|
continue;
|
|
653
703
|
}
|
|
704
|
+
|
|
654
705
|
if (open > at) {
|
|
655
706
|
yield { kind: 'text', name: '', start: at, end: open };
|
|
656
707
|
}
|
|
708
|
+
|
|
657
709
|
yield token;
|
|
658
710
|
at = Math.max(token.end, resume);
|
|
659
711
|
}
|
|
@@ -672,20 +724,25 @@ function bootstrapPlace(html: string): { at: number; head: boolean } {
|
|
|
672
724
|
if (token.kind === 'comment' || token.kind === 'doctype' || (token.kind === 'text' && BLANK.test(html.slice(token.start, token.end)))) {
|
|
673
725
|
continue;
|
|
674
726
|
}
|
|
727
|
+
|
|
675
728
|
if (token.kind === 'start' && token.name === 'html') {
|
|
676
729
|
continue;
|
|
677
730
|
}
|
|
731
|
+
|
|
678
732
|
if (token.kind === 'start' && token.name === 'head') {
|
|
679
733
|
return { at: token.end, head: true };
|
|
680
734
|
}
|
|
735
|
+
|
|
681
736
|
return { at: token.start, head: false };
|
|
682
737
|
}
|
|
738
|
+
|
|
683
739
|
return { at: html.length, head: false };
|
|
684
740
|
}
|
|
685
741
|
|
|
686
742
|
// Whether the page sets its own viewport, outside a template.
|
|
687
743
|
function hasViewport(html: string): boolean {
|
|
688
744
|
let templates = 0;
|
|
745
|
+
|
|
689
746
|
for (const token of tokens(html)) {
|
|
690
747
|
if (token.name === 'template') {
|
|
691
748
|
templates = Math.max(0, templates + (token.kind === 'start' ? 1 : token.kind === 'end' ? -1 : 0));
|
|
@@ -695,6 +752,7 @@ function hasViewport(html: string): boolean {
|
|
|
695
752
|
}
|
|
696
753
|
}
|
|
697
754
|
}
|
|
755
|
+
|
|
698
756
|
return false;
|
|
699
757
|
}
|
|
700
758
|
|
|
@@ -706,11 +764,13 @@ function hasViewport(html: string): boolean {
|
|
|
706
764
|
export function injectVisualBootstrap(html: string): string {
|
|
707
765
|
const place = bootstrapPlace(html);
|
|
708
766
|
const bootstrap = bootstrapMarkup(!hasViewport(html));
|
|
767
|
+
|
|
709
768
|
return html.slice(0, place.at) + (place.head ? bootstrap : `<head>${bootstrap}</head>`) + html.slice(place.at);
|
|
710
769
|
}
|
|
711
770
|
|
|
712
771
|
function hostPageScript(): string {
|
|
713
772
|
const methods = { ready: VISUAL_BRIDGE_METHODS.sandboxProxyReady, resource: VISUAL_BRIDGE_METHODS.sandboxResourceReady };
|
|
773
|
+
|
|
714
774
|
return `(function () {
|
|
715
775
|
if (window.parent === window) { return; }
|
|
716
776
|
var methods = ${inlineJson(methods)};
|