@agent-native/core 0.176.1 → 0.177.0-nightly-20260828155142
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/corpus/README.md +1 -1
- package/corpus/templates/analytics/actions/set-workspace-feature-flag.ts +2 -2
- package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +16 -1
- package/corpus/templates/analytics/changelog/2026-08-27-show-analytics-s-own-registered-flags-in-fleet-feature-flag-.md +6 -0
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +204 -32
- package/corpus/templates/assets/changelog/2026-08-26-templates-across-brand-kits.md +1 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +61 -68
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +9 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +3 -3
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +8 -3
- package/corpus/templates/calendar/app/lib/calendar-timezone.ts +12 -3
- package/corpus/templates/calendar/changelog/2026-08-28-all-day-events-stay-in-the-compact-top-bar-in-day-and-week-v.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-28-event-creators-can-choose-whether-a-meeting-shows-as-free-or.md +6 -0
- package/corpus/templates/clips/app/global.css +0 -27
- package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
- package/corpus/templates/clips/app/routes/download.tsx +131 -16
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +35 -20
- package/corpus/templates/clips/server/routes/api/clips-latest.json.get.ts +77 -22
- package/corpus/templates/forms/actions/response-insights.ts +4 -4
- package/corpus/templates/forms/actions/retry-response-deliveries.ts +26 -0
- package/corpus/templates/forms/server/db/schema.ts +64 -17
- package/corpus/templates/forms/server/handlers/submissions.ts +1022 -94
- package/corpus/templates/forms/server/lib/integrations.ts +100 -33
- package/corpus/templates/forms/server/middleware/00-public-cors.ts +1 -1
- package/corpus/templates/forms/server/plugins/db.ts +40 -0
- package/corpus/templates/plan/agent-native.json +1 -4
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +6 -1
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +2 -2
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -6
- package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +78 -12
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +4 -4
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +111 -27
- package/corpus/templates/slides/app/components/editor/SlidesLayersPanel.tsx +20 -34
- package/corpus/templates/slides/app/components/editor/slide-style.ts +2 -0
- package/corpus/templates/slides/app/global.css +35 -0
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +133 -5
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +675 -0
- package/corpus/templates/slides/app/lib/validate-native-slide-html.ts +1 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -44
- package/corpus/templates/slides/app/root.tsx +1 -22
- package/corpus/templates/slides/changelog/2026-08-27-image-fit-controls-apply-cover-crops-and-position.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-28-shared-slides-with-a-style-block-render-again.md +6 -0
- package/dist/action.d.ts +3 -1
- package/dist/client/FeedbackButton.d.ts +12 -0
- package/dist/client/FeedbackButton.js +62 -28
- package/dist/client/app-providers.js +13 -1
- package/dist/client/feedback-context.d.ts +1 -0
- package/dist/client/feedback-context.js +3 -2
- package/dist/client/host-bridge.d.ts +1 -0
- package/dist/client/observability/ThumbsFeedback.js +73 -10
- package/dist/client/webmcp.d.ts +4 -0
- package/dist/client/webmcp.js +51 -4
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/observability/routes.js +8 -2
- package/dist/observability/store.d.ts +1 -1
- package/dist/observability/store.js +18 -3
- package/dist/observability/types.d.ts +1 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-routes.d.ts +1 -6
- package/dist/server/action-routes.js +58 -17
- package/dist/server/agent-chat-plugin.js +8 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/webmcp.mdx +31 -12
- package/package.json +1 -1
|
@@ -9,6 +9,20 @@ export interface SlideImageDropPosition {
|
|
|
9
9
|
y: number;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export const IMAGE_OBJECT_POSITION_VALUES = [
|
|
13
|
+
"left top",
|
|
14
|
+
"center top",
|
|
15
|
+
"right top",
|
|
16
|
+
"left center",
|
|
17
|
+
"center center",
|
|
18
|
+
"right center",
|
|
19
|
+
"left bottom",
|
|
20
|
+
"center bottom",
|
|
21
|
+
"right bottom",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export type ImageObjectPosition = (typeof IMAGE_OBJECT_POSITION_VALUES)[number];
|
|
25
|
+
|
|
12
26
|
export interface OptimisticImagePreview {
|
|
13
27
|
previewSrc: string;
|
|
14
28
|
replaceSrc: string | null;
|
|
@@ -65,6 +79,43 @@ function serializeFragment(doc: Document): string {
|
|
|
65
79
|
return doc.body.innerHTML;
|
|
66
80
|
}
|
|
67
81
|
|
|
82
|
+
export function normalizeImageObjectPosition(
|
|
83
|
+
value: string | null | undefined,
|
|
84
|
+
): ImageObjectPosition {
|
|
85
|
+
const normalized = value?.trim().toLowerCase().replace(/\s+/g, " ");
|
|
86
|
+
const aliases: Record<string, ImageObjectPosition> = {
|
|
87
|
+
left: "left center",
|
|
88
|
+
right: "right center",
|
|
89
|
+
top: "center top",
|
|
90
|
+
bottom: "center bottom",
|
|
91
|
+
"top left": "left top",
|
|
92
|
+
"top center": "center top",
|
|
93
|
+
"top right": "right top",
|
|
94
|
+
"center left": "left center",
|
|
95
|
+
"center right": "right center",
|
|
96
|
+
"bottom left": "left bottom",
|
|
97
|
+
"bottom center": "center bottom",
|
|
98
|
+
"bottom right": "right bottom",
|
|
99
|
+
"0% 0%": "left top",
|
|
100
|
+
"50% 0%": "center top",
|
|
101
|
+
"100% 0%": "right top",
|
|
102
|
+
"0% 50%": "left center",
|
|
103
|
+
"50% 50%": "center center",
|
|
104
|
+
"100% 50%": "right center",
|
|
105
|
+
"0% 100%": "left bottom",
|
|
106
|
+
"50% 100%": "center bottom",
|
|
107
|
+
"100% 100%": "right bottom",
|
|
108
|
+
};
|
|
109
|
+
if (normalized && normalized in aliases) return aliases[normalized];
|
|
110
|
+
if (
|
|
111
|
+
normalized &&
|
|
112
|
+
IMAGE_OBJECT_POSITION_VALUES.includes(normalized as ImageObjectPosition)
|
|
113
|
+
) {
|
|
114
|
+
return normalized as ImageObjectPosition;
|
|
115
|
+
}
|
|
116
|
+
return "center center";
|
|
117
|
+
}
|
|
118
|
+
|
|
68
119
|
function hasImageSource(content: string, src: string): boolean {
|
|
69
120
|
const doc = parseFragment(content);
|
|
70
121
|
return Array.from(doc.body.querySelectorAll<HTMLImageElement>("img")).some(
|
|
@@ -166,6 +217,630 @@ function replaceImageSrc(
|
|
|
166
217
|
return serializeFragment(doc);
|
|
167
218
|
}
|
|
168
219
|
|
|
220
|
+
type ImageStyleUpdates = {
|
|
221
|
+
objectFit?: "cover" | "contain";
|
|
222
|
+
objectPosition?: ImageObjectPosition;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
function applyImageStyle(
|
|
226
|
+
image: HTMLImageElement,
|
|
227
|
+
updates: ImageStyleUpdates,
|
|
228
|
+
): void {
|
|
229
|
+
if (updates.objectFit) {
|
|
230
|
+
image.style.setProperty("object-fit", updates.objectFit);
|
|
231
|
+
}
|
|
232
|
+
if (updates.objectPosition) {
|
|
233
|
+
if (!updates.objectFit) image.style.setProperty("object-fit", "cover");
|
|
234
|
+
image.style.setProperty("object-position", updates.objectPosition);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function imageStyleDeclarations(updates: ImageStyleUpdates): string {
|
|
239
|
+
return [
|
|
240
|
+
updates.objectFit || (updates.objectPosition ? "cover" : undefined),
|
|
241
|
+
updates.objectPosition,
|
|
242
|
+
]
|
|
243
|
+
.map((value, index) =>
|
|
244
|
+
value
|
|
245
|
+
? `${index === 0 ? "object-fit" : "object-position"}: ${value}`
|
|
246
|
+
: null,
|
|
247
|
+
)
|
|
248
|
+
.filter((declaration): declaration is string => declaration !== null)
|
|
249
|
+
.join("; ");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function escapeHtmlAttribute(value: string): string {
|
|
253
|
+
return value
|
|
254
|
+
.replace(/&/g, "&")
|
|
255
|
+
.replace(/"/g, """)
|
|
256
|
+
.replace(/</g, "<")
|
|
257
|
+
.replace(/>/g, ">");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
interface ImageSourceToken {
|
|
261
|
+
alt?: string;
|
|
262
|
+
end: number;
|
|
263
|
+
kind: "html" | "markdown";
|
|
264
|
+
source: string;
|
|
265
|
+
start: number;
|
|
266
|
+
title?: string;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function decodeMarkdownImageDestination(rawSource: string): string {
|
|
270
|
+
const destination = rawSource.startsWith("<")
|
|
271
|
+
? rawSource.slice(1, -1)
|
|
272
|
+
: rawSource;
|
|
273
|
+
const decoded = parseFragment(`<span>${destination}</span>`).body
|
|
274
|
+
.firstElementChild?.textContent;
|
|
275
|
+
return (decoded ?? destination).replace(/\\([\\()])/g, "$1");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isMarkdownCharacterEscaped(content: string, index: number): boolean {
|
|
279
|
+
let backslashes = 0;
|
|
280
|
+
for (
|
|
281
|
+
let cursor = index - 1;
|
|
282
|
+
cursor >= 0 && content[cursor] === "\\";
|
|
283
|
+
cursor -= 1
|
|
284
|
+
) {
|
|
285
|
+
backslashes += 1;
|
|
286
|
+
}
|
|
287
|
+
return backslashes % 2 === 1;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function findClosingMarkdownBracket(
|
|
291
|
+
content: string,
|
|
292
|
+
openIndex: number,
|
|
293
|
+
): number {
|
|
294
|
+
let depth = 0;
|
|
295
|
+
for (let index = openIndex; index < content.length; index += 1) {
|
|
296
|
+
if (content[index] === "\\") {
|
|
297
|
+
index += 1;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (content[index] === "[") {
|
|
301
|
+
depth += 1;
|
|
302
|
+
} else if (content[index] === "]") {
|
|
303
|
+
depth -= 1;
|
|
304
|
+
if (depth === 0) return index;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return -1;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function skipMarkdownWhitespace(content: string, start: number): number {
|
|
311
|
+
let index = start;
|
|
312
|
+
while (index < content.length && /\s/.test(content[index] ?? "")) {
|
|
313
|
+
index += 1;
|
|
314
|
+
}
|
|
315
|
+
return index;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
interface MarkdownDestination {
|
|
319
|
+
end: number;
|
|
320
|
+
rawSource: string;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function parseMarkdownDestination(
|
|
324
|
+
content: string,
|
|
325
|
+
start: number,
|
|
326
|
+
endsAtClosingParenthesis: boolean,
|
|
327
|
+
): MarkdownDestination | null {
|
|
328
|
+
if (content[start] === "<") {
|
|
329
|
+
for (let index = start + 1; index < content.length; index += 1) {
|
|
330
|
+
if (content[index] === "\\") {
|
|
331
|
+
index += 1;
|
|
332
|
+
} else if (content[index] === ">") {
|
|
333
|
+
return {
|
|
334
|
+
end: index + 1,
|
|
335
|
+
rawSource: content.slice(start, index + 1),
|
|
336
|
+
};
|
|
337
|
+
} else if (/\s/.test(content[index] ?? "")) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let depth = 0;
|
|
345
|
+
let end = start;
|
|
346
|
+
for (; end < content.length; end += 1) {
|
|
347
|
+
const character = content[end];
|
|
348
|
+
if (character === "\\") {
|
|
349
|
+
end += 1;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (/\s/.test(character ?? "")) break;
|
|
353
|
+
if (character === "(") {
|
|
354
|
+
depth += 1;
|
|
355
|
+
} else if (character === ")") {
|
|
356
|
+
if (depth === 0 && endsAtClosingParenthesis) break;
|
|
357
|
+
if (depth > 0) depth -= 1;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return end > start ? { end, rawSource: content.slice(start, end) } : null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function parseMarkdownTitle(
|
|
365
|
+
content: string,
|
|
366
|
+
start: number,
|
|
367
|
+
): { end: number; value: string } | null {
|
|
368
|
+
const opener = content[start];
|
|
369
|
+
if (opener === '"' || opener === "'") {
|
|
370
|
+
for (let index = start + 1; index < content.length; index += 1) {
|
|
371
|
+
if (content[index] === "\\") {
|
|
372
|
+
index += 1;
|
|
373
|
+
} else if (content[index] === opener) {
|
|
374
|
+
return {
|
|
375
|
+
end: index + 1,
|
|
376
|
+
value: content.slice(start + 1, index),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (opener !== "(") return null;
|
|
384
|
+
let depth = 0;
|
|
385
|
+
for (let index = start; index < content.length; index += 1) {
|
|
386
|
+
if (content[index] === "\\") {
|
|
387
|
+
index += 1;
|
|
388
|
+
} else if (content[index] === "(") {
|
|
389
|
+
depth += 1;
|
|
390
|
+
} else if (content[index] === ")") {
|
|
391
|
+
depth -= 1;
|
|
392
|
+
if (depth === 0) {
|
|
393
|
+
return {
|
|
394
|
+
end: index + 1,
|
|
395
|
+
value: content.slice(start + 1, index),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function normalizeMarkdownReferenceLabel(value: string): string {
|
|
404
|
+
return value
|
|
405
|
+
.replace(/\\([\\[\]])/g, "$1")
|
|
406
|
+
.replace(/\s+/g, " ")
|
|
407
|
+
.trim()
|
|
408
|
+
.toLowerCase();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
interface MarkdownReferenceDefinition {
|
|
412
|
+
source: string;
|
|
413
|
+
title?: string;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function markdownReferenceDefinitions(
|
|
417
|
+
content: string,
|
|
418
|
+
): Map<string, MarkdownReferenceDefinition> {
|
|
419
|
+
const definitions = new Map<string, MarkdownReferenceDefinition>();
|
|
420
|
+
const nonRenderedRanges = nonRenderedSourceRanges(content, true);
|
|
421
|
+
let rangeIndex = 0;
|
|
422
|
+
let lineStart = 0;
|
|
423
|
+
|
|
424
|
+
while (lineStart < content.length) {
|
|
425
|
+
while (
|
|
426
|
+
rangeIndex < nonRenderedRanges.length &&
|
|
427
|
+
lineStart >= nonRenderedRanges[rangeIndex].end
|
|
428
|
+
) {
|
|
429
|
+
rangeIndex += 1;
|
|
430
|
+
}
|
|
431
|
+
const newline = content.indexOf("\n", lineStart);
|
|
432
|
+
const lineEnd = newline === -1 ? content.length : newline;
|
|
433
|
+
const line = content.slice(lineStart, lineEnd).replace(/\r$/, "");
|
|
434
|
+
const range = nonRenderedRanges[rangeIndex];
|
|
435
|
+
if (range && lineStart >= range.start && lineStart < range.end) {
|
|
436
|
+
if (newline === -1) break;
|
|
437
|
+
lineStart = newline + 1;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
let cursor = 0;
|
|
441
|
+
while (cursor < 3 && line[cursor] === " ") cursor += 1;
|
|
442
|
+
|
|
443
|
+
const labelStart = cursor;
|
|
444
|
+
if (line[cursor] === "[") {
|
|
445
|
+
const labelEnd = findClosingMarkdownBracket(line, cursor);
|
|
446
|
+
if (labelEnd !== -1 && line[labelEnd + 1] === ":") {
|
|
447
|
+
cursor = skipMarkdownWhitespace(line, labelEnd + 2);
|
|
448
|
+
const destination = parseMarkdownDestination(line, cursor, false);
|
|
449
|
+
if (destination) {
|
|
450
|
+
cursor = skipMarkdownWhitespace(line, destination.end);
|
|
451
|
+
const parsedTitle = parseMarkdownTitle(line, cursor);
|
|
452
|
+
if (parsedTitle) {
|
|
453
|
+
cursor = skipMarkdownWhitespace(line, parsedTitle.end);
|
|
454
|
+
}
|
|
455
|
+
if (cursor === line.length) {
|
|
456
|
+
const label = normalizeMarkdownReferenceLabel(
|
|
457
|
+
line.slice(labelStart + 1, labelEnd),
|
|
458
|
+
);
|
|
459
|
+
if (label && !definitions.has(label)) {
|
|
460
|
+
definitions.set(label, {
|
|
461
|
+
source: decodeMarkdownImageDestination(destination.rawSource),
|
|
462
|
+
...(parsedTitle ? { title: parsedTitle.value } : {}),
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (newline === -1) break;
|
|
471
|
+
lineStart = newline + 1;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return definitions;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function markdownReferenceImageToken(
|
|
478
|
+
content: string,
|
|
479
|
+
start: number,
|
|
480
|
+
alt: string,
|
|
481
|
+
afterAlt: number,
|
|
482
|
+
definitions: Map<string, MarkdownReferenceDefinition>,
|
|
483
|
+
): ImageSourceToken | null {
|
|
484
|
+
let label = alt;
|
|
485
|
+
let end = afterAlt;
|
|
486
|
+
if (content[afterAlt] === "[") {
|
|
487
|
+
const labelEnd = findClosingMarkdownBracket(content, afterAlt);
|
|
488
|
+
if (labelEnd === -1) return null;
|
|
489
|
+
label = content.slice(afterAlt + 1, labelEnd) || alt;
|
|
490
|
+
end = labelEnd + 1;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const definition = definitions.get(normalizeMarkdownReferenceLabel(label));
|
|
494
|
+
if (!definition) return null;
|
|
495
|
+
return {
|
|
496
|
+
alt,
|
|
497
|
+
end,
|
|
498
|
+
kind: "markdown",
|
|
499
|
+
source: definition.source,
|
|
500
|
+
start,
|
|
501
|
+
...(definition.title ? { title: definition.title } : {}),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function markdownImageTokenAt(
|
|
506
|
+
content: string,
|
|
507
|
+
start: number,
|
|
508
|
+
definitions: Map<string, MarkdownReferenceDefinition>,
|
|
509
|
+
): ImageSourceToken | null {
|
|
510
|
+
const altEnd = findClosingMarkdownBracket(content, start + 1);
|
|
511
|
+
if (altEnd === -1) return null;
|
|
512
|
+
const alt = content.slice(start + 2, altEnd);
|
|
513
|
+
const afterAlt = altEnd + 1;
|
|
514
|
+
|
|
515
|
+
if (content[afterAlt] === "(") {
|
|
516
|
+
const destination = parseMarkdownDestination(content, afterAlt + 1, true);
|
|
517
|
+
if (!destination) return null;
|
|
518
|
+
let cursor = skipMarkdownWhitespace(content, destination.end);
|
|
519
|
+
const parsedTitle = parseMarkdownTitle(content, cursor);
|
|
520
|
+
if (parsedTitle) {
|
|
521
|
+
cursor = skipMarkdownWhitespace(content, parsedTitle.end);
|
|
522
|
+
}
|
|
523
|
+
if (content[cursor] !== ")") return null;
|
|
524
|
+
return {
|
|
525
|
+
alt,
|
|
526
|
+
end: cursor + 1,
|
|
527
|
+
kind: "markdown",
|
|
528
|
+
source: decodeMarkdownImageDestination(destination.rawSource),
|
|
529
|
+
start,
|
|
530
|
+
...(parsedTitle ? { title: parsedTitle.value } : {}),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return markdownReferenceImageToken(
|
|
535
|
+
content,
|
|
536
|
+
start,
|
|
537
|
+
alt,
|
|
538
|
+
afterAlt,
|
|
539
|
+
definitions,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
interface SourceRange {
|
|
544
|
+
end: number;
|
|
545
|
+
start: number;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function markdownFenceEndAt(content: string, start: number): number | null {
|
|
549
|
+
if (start > 0 && content[start - 1] !== "\n") return null;
|
|
550
|
+
|
|
551
|
+
const openingLineEnd = content.indexOf("\n", start);
|
|
552
|
+
const openingLine = content
|
|
553
|
+
.slice(start, openingLineEnd === -1 ? content.length : openingLineEnd)
|
|
554
|
+
.replace(/\r$/, "");
|
|
555
|
+
const opener = openingLine.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
556
|
+
if (!opener) return null;
|
|
557
|
+
|
|
558
|
+
const marker = opener[1][0];
|
|
559
|
+
const markerLength = opener[1].length;
|
|
560
|
+
const closingFence = new RegExp(`^ {0,3}${marker}{${markerLength},}[ \\t]*$`);
|
|
561
|
+
let lineStart = openingLineEnd === -1 ? content.length : openingLineEnd + 1;
|
|
562
|
+
|
|
563
|
+
while (lineStart < content.length) {
|
|
564
|
+
const lineEnd = content.indexOf("\n", lineStart);
|
|
565
|
+
const line = content
|
|
566
|
+
.slice(lineStart, lineEnd === -1 ? content.length : lineEnd)
|
|
567
|
+
.replace(/\r$/, "");
|
|
568
|
+
if (closingFence.test(line)) {
|
|
569
|
+
return lineEnd === -1 ? content.length : lineEnd + 1;
|
|
570
|
+
}
|
|
571
|
+
if (lineEnd === -1) break;
|
|
572
|
+
lineStart = lineEnd + 1;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return content.length;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function markdownInlineCodeEndAt(
|
|
579
|
+
content: string,
|
|
580
|
+
start: number,
|
|
581
|
+
): number | null {
|
|
582
|
+
if (content[start] !== "`" || isMarkdownCharacterEscaped(content, start)) {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let delimiterLength = 1;
|
|
587
|
+
while (content[start + delimiterLength] === "`") delimiterLength += 1;
|
|
588
|
+
const delimiter = "`".repeat(delimiterLength);
|
|
589
|
+
for (let end = start + delimiterLength; end < content.length; end += 1) {
|
|
590
|
+
if (!content.startsWith(delimiter, end)) continue;
|
|
591
|
+
if (content[end - 1] === "`" || content[end + delimiterLength] === "`") {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
return end + delimiterLength;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function findHtmlTagEnd(content: string, start: number): number | null {
|
|
601
|
+
let quote: '"' | "'" | null = null;
|
|
602
|
+
for (let end = start + 1; end < content.length; end += 1) {
|
|
603
|
+
const character = content[end];
|
|
604
|
+
if (quote) {
|
|
605
|
+
if (character === quote) quote = null;
|
|
606
|
+
} else if (character === '"' || character === "'") {
|
|
607
|
+
quote = character;
|
|
608
|
+
} else if (character === ">") {
|
|
609
|
+
return end + 1;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const NON_RENDERED_HTML_ELEMENTS = new Set([
|
|
616
|
+
"code",
|
|
617
|
+
"pre",
|
|
618
|
+
"script",
|
|
619
|
+
"style",
|
|
620
|
+
"textarea",
|
|
621
|
+
]);
|
|
622
|
+
|
|
623
|
+
function htmlTagEndAt(content: string, start: number): number | null {
|
|
624
|
+
if (content[start] !== "<" || isMarkdownCharacterEscaped(content, start)) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const next = content[start + 1];
|
|
629
|
+
if (!next || (!/[A-Za-z]/.test(next) && !["/", "!", "?"].includes(next))) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (content.startsWith("<!--", start)) {
|
|
634
|
+
const commentEnd = content.indexOf("-->", start + 4);
|
|
635
|
+
return commentEnd === -1 ? content.length : commentEnd + 3;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return findHtmlTagEnd(content, start);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function htmlNonRenderedElementRangeEndAt(
|
|
642
|
+
content: string,
|
|
643
|
+
start: number,
|
|
644
|
+
): number | null {
|
|
645
|
+
const tagEnd = htmlTagEndAt(content, start);
|
|
646
|
+
if (tagEnd === null) return null;
|
|
647
|
+
|
|
648
|
+
const rawTag = content.slice(start, tagEnd);
|
|
649
|
+
const element = rawTag.match(/^<([A-Za-z][\w:-]*)(?:\s|\/?>)/);
|
|
650
|
+
if (
|
|
651
|
+
!element ||
|
|
652
|
+
!NON_RENDERED_HTML_ELEMENTS.has(element[1].toLowerCase()) ||
|
|
653
|
+
/\/\s*>$/.test(rawTag)
|
|
654
|
+
) {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const closingTag = new RegExp(`</${element[1]}\\s*>`, "i").exec(
|
|
659
|
+
content.slice(tagEnd),
|
|
660
|
+
);
|
|
661
|
+
return closingTag
|
|
662
|
+
? tagEnd + closingTag.index + closingTag[0].length
|
|
663
|
+
: content.length;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function nonRenderedSourceRanges(
|
|
667
|
+
content: string,
|
|
668
|
+
includeHtmlTags: boolean,
|
|
669
|
+
): SourceRange[] {
|
|
670
|
+
const ranges: SourceRange[] = [];
|
|
671
|
+
let start = 0;
|
|
672
|
+
while (start < content.length) {
|
|
673
|
+
const htmlTagEnd = htmlTagEndAt(content, start);
|
|
674
|
+
const end =
|
|
675
|
+
markdownFenceEndAt(content, start) ??
|
|
676
|
+
markdownInlineCodeEndAt(content, start) ??
|
|
677
|
+
htmlNonRenderedElementRangeEndAt(content, start) ??
|
|
678
|
+
(includeHtmlTags ? htmlTagEnd : null);
|
|
679
|
+
if (end === null || end <= start) {
|
|
680
|
+
if (!includeHtmlTags && htmlTagEnd !== null) {
|
|
681
|
+
start = htmlTagEnd;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
start += 1;
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
ranges.push({ end, start });
|
|
688
|
+
start = end;
|
|
689
|
+
}
|
|
690
|
+
return ranges;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function markdownImageSourceTokens(content: string): ImageSourceToken[] {
|
|
694
|
+
const definitions = markdownReferenceDefinitions(content);
|
|
695
|
+
const nonRenderedRanges = nonRenderedSourceRanges(content, true);
|
|
696
|
+
const tokens: ImageSourceToken[] = [];
|
|
697
|
+
let rangeIndex = 0;
|
|
698
|
+
for (let start = 0; start < content.length - 1; start += 1) {
|
|
699
|
+
while (
|
|
700
|
+
rangeIndex < nonRenderedRanges.length &&
|
|
701
|
+
start >= nonRenderedRanges[rangeIndex].end
|
|
702
|
+
) {
|
|
703
|
+
rangeIndex += 1;
|
|
704
|
+
}
|
|
705
|
+
const range = nonRenderedRanges[rangeIndex];
|
|
706
|
+
if (range && start >= range.start && start < range.end) {
|
|
707
|
+
start = range.end - 1;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (
|
|
711
|
+
content[start] !== "!" ||
|
|
712
|
+
content[start + 1] !== "[" ||
|
|
713
|
+
isMarkdownCharacterEscaped(content, start)
|
|
714
|
+
) {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const token = markdownImageTokenAt(content, start, definitions);
|
|
718
|
+
if (token) {
|
|
719
|
+
tokens.push(token);
|
|
720
|
+
start = token.end - 1;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return tokens;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function htmlImageSourceTokens(content: string): ImageSourceToken[] {
|
|
727
|
+
const tokens: ImageSourceToken[] = [];
|
|
728
|
+
const nonRenderedRanges = nonRenderedSourceRanges(content, false);
|
|
729
|
+
let rangeIndex = 0;
|
|
730
|
+
let cursor = 0;
|
|
731
|
+
while (cursor < content.length) {
|
|
732
|
+
const start = content.indexOf("<", cursor);
|
|
733
|
+
if (start === -1) break;
|
|
734
|
+
while (
|
|
735
|
+
rangeIndex < nonRenderedRanges.length &&
|
|
736
|
+
start >= nonRenderedRanges[rangeIndex].end
|
|
737
|
+
) {
|
|
738
|
+
rangeIndex += 1;
|
|
739
|
+
}
|
|
740
|
+
const range = nonRenderedRanges[rangeIndex];
|
|
741
|
+
if (range && start >= range.start && start < range.end) {
|
|
742
|
+
cursor = range.end;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const end = htmlTagEndAt(content, start);
|
|
746
|
+
if (end === null) {
|
|
747
|
+
cursor = start + 1;
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const rawTag = content.slice(start, end);
|
|
752
|
+
if (/^<img\b/i.test(rawTag)) {
|
|
753
|
+
const image = parseFragment(rawTag).body.querySelector("img");
|
|
754
|
+
const source = image?.getAttribute("src");
|
|
755
|
+
if (source) {
|
|
756
|
+
tokens.push({
|
|
757
|
+
end,
|
|
758
|
+
kind: "html",
|
|
759
|
+
source,
|
|
760
|
+
start,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
cursor = end;
|
|
765
|
+
}
|
|
766
|
+
return tokens;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function imageSourceTokens(content: string): ImageSourceToken[] {
|
|
770
|
+
const tokens = [
|
|
771
|
+
...htmlImageSourceTokens(content),
|
|
772
|
+
...markdownImageSourceTokens(content),
|
|
773
|
+
];
|
|
774
|
+
return tokens.sort((a, b) => a.start - b.start);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export function imageOccurrenceInRenderedSlide(
|
|
778
|
+
root: ParentNode | null,
|
|
779
|
+
image: HTMLImageElement,
|
|
780
|
+
): number {
|
|
781
|
+
const src = image.getAttribute("src");
|
|
782
|
+
if (!root || !src) return 0;
|
|
783
|
+
|
|
784
|
+
return Math.max(
|
|
785
|
+
0,
|
|
786
|
+
Array.from(root.querySelectorAll<HTMLImageElement>(".slide-content img"))
|
|
787
|
+
.filter((candidate) => candidate.getAttribute("src") === src)
|
|
788
|
+
.indexOf(image),
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function markdownImageStyleDeclarations(updates: ImageStyleUpdates): string {
|
|
793
|
+
const imageStyle = imageStyleDeclarations(updates);
|
|
794
|
+
return ["display: block", "width: 100%", "aspect-ratio: 16 / 9", imageStyle]
|
|
795
|
+
.filter(Boolean)
|
|
796
|
+
.join("; ");
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function updateMarkdownImageAt(
|
|
800
|
+
content: string,
|
|
801
|
+
token: ImageSourceToken,
|
|
802
|
+
updates: ImageStyleUpdates,
|
|
803
|
+
): string {
|
|
804
|
+
if (token.alt === undefined) return content;
|
|
805
|
+
const style = markdownImageStyleDeclarations(updates);
|
|
806
|
+
const title = token.title
|
|
807
|
+
? ` title="${escapeHtmlAttribute(token.title)}"`
|
|
808
|
+
: "";
|
|
809
|
+
const replacement = `<img data-markdown-image="true" src="${escapeHtmlAttribute(token.source)}" alt="${escapeHtmlAttribute(token.alt)}"${title} style="${style};">`;
|
|
810
|
+
return content.slice(0, token.start) + replacement + content.slice(token.end);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function updateHtmlImageAt(
|
|
814
|
+
content: string,
|
|
815
|
+
token: ImageSourceToken,
|
|
816
|
+
updates: ImageStyleUpdates,
|
|
817
|
+
): string {
|
|
818
|
+
const image = parseFragment(
|
|
819
|
+
content.slice(token.start, token.end),
|
|
820
|
+
).body.querySelector<HTMLImageElement>("img");
|
|
821
|
+
if (!image || image.getAttribute("src") !== token.source) return content;
|
|
822
|
+
applyImageStyle(image, updates);
|
|
823
|
+
const replacement = image.outerHTML;
|
|
824
|
+
return content.slice(0, token.start) + replacement + content.slice(token.end);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export function updateImageFitInSlideHtml(
|
|
828
|
+
content: string,
|
|
829
|
+
src: string,
|
|
830
|
+
updates: ImageStyleUpdates,
|
|
831
|
+
imageOccurrence = 0,
|
|
832
|
+
): string {
|
|
833
|
+
let matchingImage = 0;
|
|
834
|
+
for (const token of imageSourceTokens(content)) {
|
|
835
|
+
if (token.source !== src) continue;
|
|
836
|
+
if (matchingImage++ !== Math.max(0, imageOccurrence)) continue;
|
|
837
|
+
return token.kind === "html"
|
|
838
|
+
? updateHtmlImageAt(content, token, updates)
|
|
839
|
+
: updateMarkdownImageAt(content, token, updates);
|
|
840
|
+
}
|
|
841
|
+
return content;
|
|
842
|
+
}
|
|
843
|
+
|
|
169
844
|
/** Replace one optimistic preview, or remove it when its upload failed. */
|
|
170
845
|
export function replaceOptimisticImagePreview(
|
|
171
846
|
content: string,
|