@agent-native/core-corpus 0.216.0 → 0.218.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/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +61 -1
- package/corpus/templates/analytics/AGENTS.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +3 -0
- package/corpus/templates/analytics/actions/get-session-replay-summary.ts +9 -65
- package/corpus/templates/analytics/actions/list-event-catalog.ts +1 -1
- package/corpus/templates/analytics/actions/list-session-outputs.ts +40 -0
- package/corpus/templates/analytics/actions/list-session-recordings.ts +17 -4
- package/corpus/templates/analytics/actions/view-screen.ts +29 -3
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
- package/corpus/templates/analytics/app/i18n/en-US.ts +27 -0
- package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
- package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +67 -0
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
- package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
- package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
- package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
- package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
- package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
- package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
- package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
- package/corpus/templates/analytics/server/plugins/db.ts +46 -0
- package/corpus/templates/analytics/shared/output-use.ts +205 -0
- package/corpus/templates/analytics/shared/session-events.ts +6 -0
- package/corpus/templates/assets/CHANGELOG.md +6 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
- package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
- package/corpus/templates/brain/app/routes/sources.tsx +1 -1
- package/corpus/templates/brain/vitest.config.ts +7 -0
- package/corpus/templates/calendar/CHANGELOG.md +12 -0
- package/corpus/templates/calendar/actions/create-event.ts +2 -4
- package/corpus/templates/calendar/actions/update-event.ts +35 -6
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
- package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
- package/corpus/templates/clips/CHANGELOG.md +7 -20
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
- package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
- package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
- package/corpus/templates/content/CHANGELOG.md +10 -0
- package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
- package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
- package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
- package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
- package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
- package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
- package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
- package/corpus/templates/content/actions/_database-utils.ts +93 -7
- package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
- package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
- package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
- package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
- package/corpus/templates/content/actions/delete-document.ts +41 -3
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
- package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
- package/corpus/templates/content/actions/get-document.ts +2 -0
- package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
- package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
- package/corpus/templates/content/actions/update-document.ts +139 -20
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +119 -30
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +56 -13
- package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
- package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
- package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
- package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
- package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
- package/corpus/templates/content/app/global.css +31 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
- package/corpus/templates/content/app/i18n-data.ts +1 -0
- package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
- package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
- package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
- package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
- package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
- package/corpus/templates/content/parity/matrix.md +42 -42
- package/corpus/templates/content/parity/matrix.ts +3 -1
- package/corpus/templates/content/server/db/schema.ts +20 -0
- package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
- package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
- package/corpus/templates/content/server/live-copy/binding.ts +31 -0
- package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
- package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
- package/corpus/templates/content/server/live-copy/holds.ts +59 -0
- package/corpus/templates/content/server/live-copy/persist.ts +656 -0
- package/corpus/templates/content/server/live-copy/switch.ts +136 -0
- package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
- package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
- package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
- package/corpus/templates/content/server/live-copy/write.ts +85 -0
- package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
- package/corpus/templates/content/server/plugins/collab.ts +3 -0
- package/corpus/templates/content/server/plugins/db.ts +28 -0
- package/corpus/templates/content/shared/api.ts +30 -1
- package/corpus/templates/content/shared/database-item-layout.ts +54 -22
- package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
- package/corpus/templates/content/shared/og-description.ts +258 -36
- package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
- package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
- package/corpus/templates/content/shared/suggestion-text.ts +20 -5
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
- package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
- package/corpus/templates/design/CHANGELOG.md +5 -0
- package/corpus/templates/design/actions/get-getting-started.ts +53 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
- package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
- package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
- package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
- package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
- package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
- package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
- package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
- package/corpus/templates/design/app/global.css +6 -0
- package/corpus/templates/design/app/i18n/en-US.ts +17 -0
- package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
- package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
- package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
- package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
- package/corpus/templates/design/app/pages/Index.tsx +28 -67
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
- package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
- package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
- package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
- package/corpus/templates/design/server/lib/getting-started.ts +267 -0
- package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
- package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
- package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
- package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
- package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
- package/corpus/templates/design/shared/design-template-presets.ts +8 -0
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
- package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/plan/CHANGELOG.md +6 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
- package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +7 -0
- package/corpus/templates/slides/actions/delete-deck.ts +7 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
- package/corpus/templates/slides/app/pages/Index.tsx +7 -2
- package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
- package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
- package/package.json +1 -1
- package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
- package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +0 -83
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ContentDatabaseItemLayout,
|
|
3
3
|
ContentDatabaseItemLeadingKind,
|
|
4
|
+
ContentDatabaseItemLine2Source,
|
|
4
5
|
ContentDatabaseItemTitleWeight,
|
|
5
6
|
} from "./api";
|
|
6
7
|
import {
|
|
@@ -25,6 +26,11 @@ export const CONTENT_DATABASE_ITEM_TITLE_WEIGHTS = [
|
|
|
25
26
|
|
|
26
27
|
export const CONTENT_DATABASE_ITEM_LINE2_LINES = [1, 2, 3] as const;
|
|
27
28
|
|
|
29
|
+
export const CONTENT_DATABASE_ITEM_LINE2_SOURCES = [
|
|
30
|
+
"property",
|
|
31
|
+
"page",
|
|
32
|
+
] as const satisfies readonly ContentDatabaseItemLine2Source[];
|
|
33
|
+
|
|
28
34
|
/** Most properties line 3 or the trailing slot may show. */
|
|
29
35
|
export const MAX_ITEM_LAYOUT_META_PROPERTIES = 6;
|
|
30
36
|
|
|
@@ -92,7 +98,7 @@ export function itemLayoutLeadingCandidates<
|
|
|
92
98
|
);
|
|
93
99
|
}
|
|
94
100
|
|
|
95
|
-
/** Text-like properties line 2 can show.
|
|
101
|
+
/** Text-like properties line 2 can show. Page text is its own choice. */
|
|
96
102
|
export function itemLayoutLine2Candidates<
|
|
97
103
|
P extends ContentDatabaseItemLayoutPropertyLike,
|
|
98
104
|
>(properties: readonly P[]): P[] {
|
|
@@ -135,6 +141,14 @@ function propertyIdList(value: unknown) {
|
|
|
135
141
|
return ids;
|
|
136
142
|
}
|
|
137
143
|
|
|
144
|
+
/** Line 2 with a new property ID, unless it shows page text. */
|
|
145
|
+
function withLine2PropertyId(
|
|
146
|
+
line2: ContentDatabaseItemLayout["line2"],
|
|
147
|
+
propertyId: string | null,
|
|
148
|
+
): ContentDatabaseItemLayout["line2"] {
|
|
149
|
+
return line2.source === "page" ? line2 : { propertyId, lines: line2.lines };
|
|
150
|
+
}
|
|
151
|
+
|
|
138
152
|
function record(value: unknown): Record<string, unknown> {
|
|
139
153
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
140
154
|
? (value as Record<string, unknown>)
|
|
@@ -174,7 +188,10 @@ export function normalizeContentDatabaseItemLayout(
|
|
|
174
188
|
)
|
|
175
189
|
? (raw.titleWeight as ContentDatabaseItemTitleWeight)
|
|
176
190
|
: "medium",
|
|
177
|
-
line2:
|
|
191
|
+
line2:
|
|
192
|
+
line2.source === "page"
|
|
193
|
+
? { source: "page", propertyId: null, lines }
|
|
194
|
+
: { propertyId: propertyIdOrNull(line2.propertyId), lines },
|
|
178
195
|
line3: propertyIdList(raw.line3),
|
|
179
196
|
trailing: propertyIdList(raw.trailing),
|
|
180
197
|
};
|
|
@@ -214,7 +231,8 @@ export function withSavedContentDatabaseItemLayout<
|
|
|
214
231
|
export interface ResolvedContentDatabaseItemLayout<P> {
|
|
215
232
|
leading: { kind: ContentDatabaseItemLeadingKind; property: P | null };
|
|
216
233
|
titleWeight: ContentDatabaseItemTitleWeight;
|
|
217
|
-
|
|
234
|
+
/** `pageText` rows show the first lines of their page instead of a property. */
|
|
235
|
+
line2: { property: P | null; pageText: boolean; lines: 1 | 2 | 3 };
|
|
218
236
|
line3: P[];
|
|
219
237
|
trailing: P[];
|
|
220
238
|
}
|
|
@@ -248,9 +266,11 @@ export function resolveContentDatabaseItemLayout<
|
|
|
248
266
|
: (leadingCandidates[0] ?? null)
|
|
249
267
|
: null;
|
|
250
268
|
const line2Candidates = new Set(itemLayoutLine2Candidates(properties));
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
269
|
+
const pageText = layout.line2.source === "page";
|
|
270
|
+
const line2Property =
|
|
271
|
+
!pageText && layout.line2.propertyId
|
|
272
|
+
? (byId.get(layout.line2.propertyId) ?? null)
|
|
273
|
+
: null;
|
|
254
274
|
const meta = (ids: readonly string[]) =>
|
|
255
275
|
ids.flatMap((id) => {
|
|
256
276
|
const property = byId.get(id);
|
|
@@ -272,6 +292,7 @@ export function resolveContentDatabaseItemLayout<
|
|
|
272
292
|
line2Property && line2Candidates.has(line2Property)
|
|
273
293
|
? line2Property
|
|
274
294
|
: null,
|
|
295
|
+
pageText,
|
|
275
296
|
lines: layout.line2.lines,
|
|
276
297
|
},
|
|
277
298
|
line3: meta(layout.line3),
|
|
@@ -353,10 +374,10 @@ export function pruneContentDatabaseItemLayout(
|
|
|
353
374
|
propertyId: resolved.leading.property ? layout.leading.propertyId : null,
|
|
354
375
|
},
|
|
355
376
|
titleWeight: layout.titleWeight,
|
|
356
|
-
line2:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
377
|
+
line2: withLine2PropertyId(
|
|
378
|
+
layout.line2,
|
|
379
|
+
resolved.line2.property?.definition.id ?? null,
|
|
380
|
+
),
|
|
360
381
|
line3: ids(resolved.line3),
|
|
361
382
|
trailing: ids(resolved.trailing),
|
|
362
383
|
};
|
|
@@ -379,10 +400,7 @@ export function dropDeletedContentDatabaseItemLayoutReferences(
|
|
|
379
400
|
? { kind: "page_icon", propertyId: null }
|
|
380
401
|
: layout.leading,
|
|
381
402
|
titleWeight: layout.titleWeight,
|
|
382
|
-
line2:
|
|
383
|
-
propertyId: kept(layout.line2.propertyId),
|
|
384
|
-
lines: layout.line2.lines,
|
|
385
|
-
},
|
|
403
|
+
line2: withLine2PropertyId(layout.line2, kept(layout.line2.propertyId)),
|
|
386
404
|
line3: layout.line3.filter((id) => exists.has(id)),
|
|
387
405
|
trailing: layout.trailing.filter((id) => exists.has(id)),
|
|
388
406
|
};
|
|
@@ -444,13 +462,12 @@ export function savableContentDatabaseItemLayout(
|
|
|
444
462
|
{
|
|
445
463
|
leading: savedLeading ? layout.leading : showable.leading,
|
|
446
464
|
titleWeight: layout.titleWeight,
|
|
447
|
-
line2:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
},
|
|
465
|
+
line2: withLine2PropertyId(
|
|
466
|
+
layout.line2,
|
|
467
|
+
saved?.line2.propertyId === layout.line2.propertyId
|
|
468
|
+
? layout.line2.propertyId
|
|
469
|
+
: showable.line2.propertyId,
|
|
470
|
+
),
|
|
454
471
|
line3: meta("line3"),
|
|
455
472
|
trailing: meta("trailing"),
|
|
456
473
|
},
|
|
@@ -462,7 +479,7 @@ export function savableContentDatabaseItemLayout(
|
|
|
462
479
|
* The layout someone starts editing from on a view with no saved layout. It
|
|
463
480
|
* matches the default rows: page icon, title, and the first four properties.
|
|
464
481
|
* Line 2 is the only slot that shows a Blocks field, so one of those goes
|
|
465
|
-
* there.
|
|
482
|
+
* there.
|
|
466
483
|
*/
|
|
467
484
|
export function startingContentDatabaseItemLayout(
|
|
468
485
|
visibleProperties: readonly ContentDatabaseItemLayoutPropertyLike[],
|
|
@@ -482,6 +499,21 @@ export function startingContentDatabaseItemLayout(
|
|
|
482
499
|
};
|
|
483
500
|
}
|
|
484
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Whether any List view shows page text in line 2, so list reads carry each
|
|
504
|
+
* row's excerpt. Other collections never read page bodies for their rows.
|
|
505
|
+
*/
|
|
506
|
+
export function contentDatabaseViewsShowPageText(
|
|
507
|
+
views: readonly {
|
|
508
|
+
type: string;
|
|
509
|
+
itemLayout?: ContentDatabaseItemLayout | null;
|
|
510
|
+
}[],
|
|
511
|
+
): boolean {
|
|
512
|
+
return views.some(
|
|
513
|
+
(view) => view.type === "list" && view.itemLayout?.line2.source === "page",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
485
517
|
export interface ContentDatabaseItemLayoutPreset {
|
|
486
518
|
id: ContentDatabaseItemLayoutPresetId;
|
|
487
519
|
/** Null for Default, which keeps the view's default rows. */
|
|
@@ -518,6 +518,18 @@ function mergePlainParagraph(
|
|
|
518
518
|
};
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
+
/** The blocks `after` changed from `before`; none when the structure changed. */
|
|
522
|
+
export function changedBlocksBetween(before: string, after: string): number[] {
|
|
523
|
+
if (before === after) return [];
|
|
524
|
+
const base = nfmToDoc(before).content;
|
|
525
|
+
const candidate = nfmToDoc(after).content;
|
|
526
|
+
return base.length === candidate.length
|
|
527
|
+
? base.flatMap((block, index) =>
|
|
528
|
+
stableBlock(block) !== stableBlock(candidate[index]) ? [index] : [],
|
|
529
|
+
)
|
|
530
|
+
: [];
|
|
531
|
+
}
|
|
532
|
+
|
|
521
533
|
export function mergeDocumentBodyIntents(args: {
|
|
522
534
|
authoredBaseContent: string;
|
|
523
535
|
authoredCandidateContent: string;
|
|
@@ -530,18 +542,13 @@ export function mergeDocumentBodyIntents(args: {
|
|
|
530
542
|
args.currentRevision === args.incoming.authoredBaseRevision &&
|
|
531
543
|
args.currentContent === args.authoredBaseContent
|
|
532
544
|
) {
|
|
533
|
-
const base = nfmToDoc(args.authoredBaseContent).content;
|
|
534
|
-
const candidate = nfmToDoc(args.authoredCandidateContent).content;
|
|
535
|
-
const changedBlockIndexes =
|
|
536
|
-
base.length === candidate.length
|
|
537
|
-
? base.flatMap((block, index) =>
|
|
538
|
-
stableBlock(block) !== stableBlock(candidate[index]) ? [index] : [],
|
|
539
|
-
)
|
|
540
|
-
: [];
|
|
541
545
|
return {
|
|
542
546
|
status: "resolved",
|
|
543
547
|
content: args.authoredCandidateContent,
|
|
544
|
-
changedBlockIndexes
|
|
548
|
+
changedBlockIndexes: changedBlocksBetween(
|
|
549
|
+
args.authoredBaseContent,
|
|
550
|
+
args.authoredCandidateContent,
|
|
551
|
+
),
|
|
545
552
|
displaced: false,
|
|
546
553
|
};
|
|
547
554
|
}
|
|
@@ -7,7 +7,12 @@ function stripFrontmatter(markdown: string): string {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
function stripFencedCode(markdown: string): string {
|
|
10
|
-
|
|
10
|
+
// A closing fence is at least as long as the opening one and has nothing
|
|
11
|
+
// after it, so a line like ```js inside the block doesn't close it.
|
|
12
|
+
return markdown.replace(
|
|
13
|
+
/(?:^|\n) {0,3}((`|~)\2{2,})[\s\S]*?(?:\n {0,3}\1\2*[ \t]*(?=\n|$)|$)/g,
|
|
14
|
+
"\n",
|
|
15
|
+
);
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
function decodeCommonEntities(text: string): string {
|
|
@@ -22,9 +27,10 @@ function decodeCommonEntities(text: string): string {
|
|
|
22
27
|
|
|
23
28
|
function normalizeForComparison(text: string): string {
|
|
24
29
|
return stripMarkdownForPreview(text)
|
|
30
|
+
.normalize("NFC")
|
|
25
31
|
.toLowerCase()
|
|
26
32
|
.replace(/&/g, "and")
|
|
27
|
-
.replace(/[
|
|
33
|
+
.replace(/[^\p{L}\p{M}\p{N}]+/gu, " ")
|
|
28
34
|
.trim();
|
|
29
35
|
}
|
|
30
36
|
|
|
@@ -39,11 +45,19 @@ function isHeadingBlock(block: string): boolean {
|
|
|
39
45
|
if (/^#{1,6}\s+\S/.test(trimmed)) return true;
|
|
40
46
|
if (/^<h[1-6]\b/i.test(trimmed)) return true;
|
|
41
47
|
|
|
48
|
+
// Lines of paragraph text underlined with `=` or `-`. Under a list, quote
|
|
49
|
+
// or table, a `---` is a divider instead.
|
|
42
50
|
const lines = trimmed.split("\n");
|
|
43
51
|
return (
|
|
44
|
-
lines.length
|
|
45
|
-
lines[
|
|
46
|
-
|
|
52
|
+
lines.length >= 2 &&
|
|
53
|
+
/^\s*(=+|-+)\s*$/.test(lines[lines.length - 1]) &&
|
|
54
|
+
lines
|
|
55
|
+
.slice(0, -1)
|
|
56
|
+
.every(
|
|
57
|
+
(line) =>
|
|
58
|
+
line.trim().length > 0 &&
|
|
59
|
+
!/^\s{0,3}(?:(?:[-+*]|\d+[.)])(?:\s|$)|>|\|)/.test(line),
|
|
60
|
+
)
|
|
47
61
|
);
|
|
48
62
|
}
|
|
49
63
|
|
|
@@ -52,41 +66,51 @@ function isDividerBlock(block: string): boolean {
|
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
export function stripMarkdownForPreview(markdown: string): string {
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
return (
|
|
70
|
+
decodeCommonEntities(markdown)
|
|
71
|
+
.replace(/<[^>\n]+\/>/g, " ")
|
|
72
|
+
.replace(/<\/?[^>\n]+>/g, " ")
|
|
73
|
+
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
|
|
74
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
75
|
+
.replace(/\[([^\]]+)\]\[[^\]]*\]/g, "$1")
|
|
76
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
77
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
78
|
+
.replace(/\s+#+\s*$/gm, "")
|
|
79
|
+
.replace(/^\s{0,3}>\s?/gm, "")
|
|
80
|
+
.replace(/^\s*[-+*]\s+\[[ xX]\]\s+/gm, "")
|
|
81
|
+
.replace(/^\s*[-+*]\s+/gm, "")
|
|
82
|
+
.replace(/^\s*\d+[.)]\s+/gm, "")
|
|
83
|
+
.replace(/^\s*\|?(?:\s*:?-{3,}:?\s*\|)+\s*$/gm, " ")
|
|
84
|
+
// Heading underlines and dividers that share a block with text.
|
|
85
|
+
.replace(/^[ \t]{0,3}(?:=+|-+)[ \t]*$/gm, " ")
|
|
86
|
+
.replace(/\|/g, " ")
|
|
87
|
+
.replace(/\*\*([^*\n]+)\*\*/g, "$1")
|
|
88
|
+
.replace(/__([^_\n]+)__/g, "$1")
|
|
89
|
+
.replace(/\*([^*\n]+)\*/g, "$1")
|
|
90
|
+
.replace(/_([^_\n]+)_/g, "$1")
|
|
91
|
+
.replace(/~~([^~\n]+)~~/g, "$1")
|
|
92
|
+
.replace(/\\([\\`*_[\]{}()#+\-.!|>])/g, "$1")
|
|
93
|
+
.replace(/\s+/g, " ")
|
|
94
|
+
.trim()
|
|
95
|
+
);
|
|
78
96
|
}
|
|
79
97
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
/** Ends text with "...", shortening it at a word so the whole fits. */
|
|
99
|
+
function withEllipsis(text: string, maxLength: number): string {
|
|
83
100
|
const hardLimit = Math.max(20, maxLength - 3);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
wordBoundary
|
|
101
|
+
let kept = text;
|
|
102
|
+
if (kept.length > hardLimit) {
|
|
103
|
+
const prefix = kept.slice(0, hardLimit);
|
|
104
|
+
const wordBoundary = prefix.lastIndexOf(" ");
|
|
105
|
+
kept =
|
|
106
|
+
wordBoundary > hardLimit * 0.6 ? prefix.slice(0, wordBoundary) : prefix;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return `${kept.replace(/[.,;:!?\s-]+$/g, "")}...`;
|
|
110
|
+
}
|
|
88
111
|
|
|
89
|
-
|
|
112
|
+
function truncateDescription(text: string, maxLength: number): string {
|
|
113
|
+
return text.length <= maxLength ? text : withEllipsis(text, maxLength);
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
export function buildPublicDocumentDescription({
|
|
@@ -126,3 +150,201 @@ export function buildPublicDocumentDescription({
|
|
|
126
150
|
? truncateDescription(fallback, maxLength)
|
|
127
151
|
: DEFAULT_DESCRIPTION;
|
|
128
152
|
}
|
|
153
|
+
|
|
154
|
+
/** A line frontmatter holds: a key, list item, indented value, comment or blank. */
|
|
155
|
+
/** A frontmatter key and its colon. A quoted key can hold colons. */
|
|
156
|
+
const FRONTMATTER_KEY_SOURCE = String.raw`(?:"[^"\n]*"|'[^'\n]*'|[^\s#:-][^:\n]*):(?=\s|$)`;
|
|
157
|
+
const FRONTMATTER_KEY = new RegExp(`^${FRONTMATTER_KEY_SOURCE}`);
|
|
158
|
+
const FRONTMATTER_LINE = new RegExp(
|
|
159
|
+
String.raw`^(?:[ \t]*(?:#.*)?|[ \t]+\S.*|-(?:[ \t].*)?|${FRONTMATTER_KEY_SOURCE}.*)$`,
|
|
160
|
+
);
|
|
161
|
+
const FRONTMATTER_START = new RegExp(
|
|
162
|
+
String.raw`^---[ \t]*\n(?:#|${FRONTMATTER_KEY_SOURCE})`,
|
|
163
|
+
);
|
|
164
|
+
// A key cut before its colon: one word, or a quoted name.
|
|
165
|
+
const FRONTMATTER_KEY_START = /^(?:"[^"\n]*|'[^'\n]*|[^\s#:-][^\s:]*)$/;
|
|
166
|
+
// A key written the way frontmatter keys are: one word that doesn't start
|
|
167
|
+
// with a capital, or a quoted name. A label like `Note:` starts prose.
|
|
168
|
+
const CONVENTIONAL_FRONTMATTER_KEY =
|
|
169
|
+
/^(?:"[^"\n]*"|'[^'\n]*'|[^\s\p{Lu}#:"'-][^\s:]*):(?=\s|$)/u;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The markdown after its frontmatter, or null when a prefix ends inside it.
|
|
173
|
+
* Frontmatter opens with `---` and a key or comment, and holds only YAML lines,
|
|
174
|
+
* at least one a key, up to a closing `---`. A divider at the top of a page is
|
|
175
|
+
* followed by a blank line or by text that isn't YAML.
|
|
176
|
+
*/
|
|
177
|
+
function stripExcerptFrontmatter(
|
|
178
|
+
markdown: string,
|
|
179
|
+
truncated: boolean,
|
|
180
|
+
): string | null {
|
|
181
|
+
if (!FRONTMATTER_START.test(markdown)) return markdown;
|
|
182
|
+
const lines = markdown.split("\n");
|
|
183
|
+
let hasKey = false;
|
|
184
|
+
for (let index = 1; index < lines.length; index++) {
|
|
185
|
+
const line = lines[index];
|
|
186
|
+
if (/^---[ \t]*$/.test(line)) {
|
|
187
|
+
return hasKey ? lines.slice(index + 1).join("\n") : markdown;
|
|
188
|
+
}
|
|
189
|
+
if (FRONTMATTER_KEY.test(line)) hasKey = true;
|
|
190
|
+
// A prefix's last line may be cut short. After a whole line, it counts if
|
|
191
|
+
// it could still begin a YAML line. Cut as the first line, it counts only
|
|
192
|
+
// with a conventional key, so a long `Note: …` paragraph after a top
|
|
193
|
+
// divider stays page text while a long `description:` stays hidden.
|
|
194
|
+
if (truncated && index === lines.length - 1) {
|
|
195
|
+
const couldBeYaml =
|
|
196
|
+
index > 1
|
|
197
|
+
? FRONTMATTER_LINE.test(line) || FRONTMATTER_KEY_START.test(line)
|
|
198
|
+
: CONVENTIONAL_FRONTMATTER_KEY.test(line);
|
|
199
|
+
if (!couldBeYaml) return markdown;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
if (!FRONTMATTER_LINE.test(line)) return markdown;
|
|
203
|
+
}
|
|
204
|
+
// A prefix that has shown only YAML lines may end inside the frontmatter,
|
|
205
|
+
// even before its first key.
|
|
206
|
+
return truncated ? null : markdown;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Drops opening markers with no partner after them, like the `**` of a bold
|
|
211
|
+
* run a cut split. Closed spans keep their text, markers and all.
|
|
212
|
+
*/
|
|
213
|
+
function dropUnclosedMarkers(markdown: string): string {
|
|
214
|
+
return markdown.replace(
|
|
215
|
+
/(^|\s)(\*{1,3}|_{1,3}|~~|`+)(?=[\p{L}\p{M}\p{N}])/gu,
|
|
216
|
+
(match, space: string, marker: string, offset: number) =>
|
|
217
|
+
markdown.slice(offset + match.length).includes(marker) ? match : space,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const DEFAULT_EXCERPT_LENGTH = 300;
|
|
222
|
+
/** Scripts written without spaces between words. */
|
|
223
|
+
const SPACELESS_SCRIPT =
|
|
224
|
+
/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}]/u;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Plain text from the start of a page body, for a list row. Images, code,
|
|
228
|
+
* dividers and a first paragraph that repeats the title drop out, and
|
|
229
|
+
* headings only count when the page has no other text. `truncated` means the
|
|
230
|
+
* markdown is only the body's first part, so it may end partway through;
|
|
231
|
+
* `nextCharacter`, when known, tells whether its last word goes on.
|
|
232
|
+
*/
|
|
233
|
+
export function buildDocumentExcerpt({
|
|
234
|
+
content,
|
|
235
|
+
title,
|
|
236
|
+
truncated = false,
|
|
237
|
+
nextCharacter,
|
|
238
|
+
maxLength = DEFAULT_EXCERPT_LENGTH,
|
|
239
|
+
}: {
|
|
240
|
+
content?: string | null;
|
|
241
|
+
title?: string | null;
|
|
242
|
+
truncated?: boolean;
|
|
243
|
+
nextCharacter?: string;
|
|
244
|
+
maxLength?: number;
|
|
245
|
+
}): string {
|
|
246
|
+
const body = stripExcerptFrontmatter(
|
|
247
|
+
content?.replace(/\r\n?/g, "\n") ?? "",
|
|
248
|
+
truncated,
|
|
249
|
+
);
|
|
250
|
+
// A prefix that ends inside the frontmatter has no body text yet.
|
|
251
|
+
if (body === null) return "";
|
|
252
|
+
let markdown = body;
|
|
253
|
+
if (truncated) {
|
|
254
|
+
// Drop an image or tag the cut leaves open. A link it leaves open keeps
|
|
255
|
+
// its text, and when the cut falls in its address, that text is whole.
|
|
256
|
+
let linkTextEnded = false;
|
|
257
|
+
markdown = markdown
|
|
258
|
+
.replace(/!\[[^\]\n]*(?:\]\([^)\n]*)?$/, "")
|
|
259
|
+
.replace(/<[A-Za-z/!][^>\n]*$/, "")
|
|
260
|
+
.replace(
|
|
261
|
+
/\[([^\]\n]*)(\]\([^)\n]*)?$/,
|
|
262
|
+
(_match, text: string, address?: string) => {
|
|
263
|
+
linkTextEnded = address !== undefined;
|
|
264
|
+
return text;
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
// Drop a word the cut splits, with any markup or punctuation in front of
|
|
268
|
+
// it. A cut right after punctuation or closing markup ends a whole word,
|
|
269
|
+
// an apostrophe after the cut means the word goes on, and text in a
|
|
270
|
+
// script written without spaces has no word to finish.
|
|
271
|
+
const lastWord = /\s(\S*[\p{L}\p{M}\p{N}'’])$/u.exec(markdown)?.[1];
|
|
272
|
+
const wordGoesOn =
|
|
273
|
+
nextCharacter === undefined || /[\p{L}\p{M}\p{N}'’]/u.test(nextCharacter);
|
|
274
|
+
if (
|
|
275
|
+
lastWord &&
|
|
276
|
+
wordGoesOn &&
|
|
277
|
+
!linkTextEnded &&
|
|
278
|
+
!SPACELESS_SCRIPT.test(lastWord)
|
|
279
|
+
) {
|
|
280
|
+
markdown = markdown.slice(0, -lastWord.length);
|
|
281
|
+
}
|
|
282
|
+
// Then markup or an opening bracket or quote left with nothing after it.
|
|
283
|
+
markdown = markdown.replace(/(^|\s)[*_~`(["'“‘]+$/u, "$1");
|
|
284
|
+
}
|
|
285
|
+
const blocks = stripFencedCode(markdown)
|
|
286
|
+
// Headings and dividers stand alone even without blank lines around them.
|
|
287
|
+
// A line of `=` or `-` right under a line of text underlines it as a
|
|
288
|
+
// heading, so it stays with that line.
|
|
289
|
+
.replace(/^ {0,3}#{1,6}[ \t].*$/gm, "\n$&\n")
|
|
290
|
+
.replace(/(?<=\S[^\n]*\n) {0,3}(?:=+|-+)[ \t]*$/gm, "$&\n")
|
|
291
|
+
.replace(/^ {0,3}(?:(?:\*[ \t]*){3,}|(?:_[ \t]*){3,})$/gm, "\n")
|
|
292
|
+
.replace(/(^|\n\n)[ \t]{0,3}(?:[-*_][ \t]*){3,}(?=\n|$)/g, "$1\n")
|
|
293
|
+
.split(/\n{2,}/);
|
|
294
|
+
const paragraphs: string[] = [];
|
|
295
|
+
const headings: string[] = [];
|
|
296
|
+
let length = 0;
|
|
297
|
+
for (const [index, block] of blocks.entries()) {
|
|
298
|
+
if (length > maxLength) break;
|
|
299
|
+
if (!block.trim() || isDividerBlock(block)) continue;
|
|
300
|
+
// The cut can only leave markup open in the last block.
|
|
301
|
+
const source =
|
|
302
|
+
truncated && index === blocks.length - 1
|
|
303
|
+
? dropUnclosedMarkers(block)
|
|
304
|
+
: block;
|
|
305
|
+
const text = stripMarkdownForPreview(
|
|
306
|
+
source.replace(/!\[[^\]]*\]\([^)]*\)/g, " "),
|
|
307
|
+
);
|
|
308
|
+
if (!text) continue;
|
|
309
|
+
if (isHeadingBlock(block)) {
|
|
310
|
+
headings.push(text);
|
|
311
|
+
} else if (paragraphs.length > 0 || !isTitleRepeat(text, title ?? "")) {
|
|
312
|
+
paragraphs.push(text);
|
|
313
|
+
length += text.length + 1;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const text = (paragraphs.length > 0 ? paragraphs : headings).join(" ");
|
|
317
|
+
if (text.length > maxLength) return truncateDescription(text, maxLength);
|
|
318
|
+
return truncated && text ? withEllipsis(text, maxLength) : text;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* How much of a page body a list row's excerpt reads: enough markdown for
|
|
323
|
+
* three lines of text once links and formatting are stripped.
|
|
324
|
+
*/
|
|
325
|
+
export const EXCERPT_SOURCE_CHARS = 1000;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* A list row's excerpt, from only the first `EXCERPT_SOURCE_CHARS` characters
|
|
329
|
+
* of the page body. A whole body and a list read's prefix, which takes one
|
|
330
|
+
* character more to tell a longer body from one that fits, give the same text.
|
|
331
|
+
*/
|
|
332
|
+
export function buildListRowExcerpt({
|
|
333
|
+
content,
|
|
334
|
+
title,
|
|
335
|
+
}: {
|
|
336
|
+
content?: string | null;
|
|
337
|
+
title?: string | null;
|
|
338
|
+
}): string {
|
|
339
|
+
// Count characters the way the database does, not UTF-16 units. No
|
|
340
|
+
// character takes more than two units.
|
|
341
|
+
const characters = Array.from(
|
|
342
|
+
(content ?? "").slice(0, 2 * (EXCERPT_SOURCE_CHARS + 1)),
|
|
343
|
+
);
|
|
344
|
+
return buildDocumentExcerpt({
|
|
345
|
+
content: characters.slice(0, EXCERPT_SOURCE_CHARS).join(""),
|
|
346
|
+
title,
|
|
347
|
+
truncated: characters.length > EXCERPT_SOURCE_CHARS,
|
|
348
|
+
nextCharacter: characters[EXCERPT_SOURCE_CHARS],
|
|
349
|
+
});
|
|
350
|
+
}
|
|
@@ -483,6 +483,9 @@ function diffTokens(text: string): string[] {
|
|
|
483
483
|
// is no longer than either, as diff-match-patch's semantic cleanup does.
|
|
484
484
|
// Equalities between replacements stay, so separate word swaps remain
|
|
485
485
|
// separately reviewable, and a line never folds, so blocks stay separate.
|
|
486
|
+
// The exception is a line the merged run begins or ends with: that is one
|
|
487
|
+
// insertion (text plus a new paragraph) Myers aligned across the existing
|
|
488
|
+
// separator, and it would otherwise split whenever another hunk exists.
|
|
486
489
|
type ChangeRun = {
|
|
487
490
|
deleted: string;
|
|
488
491
|
inserted: string;
|
|
@@ -518,12 +521,17 @@ function foldShortEqualities(parts: DiffPart[]): DiffPart[] {
|
|
|
518
521
|
const right = segments[index + 1];
|
|
519
522
|
if (typeof equality !== "string" || typeof left !== "object") continue;
|
|
520
523
|
if (typeof right !== "object") continue;
|
|
524
|
+
const insertions = left.ownDeleted === 0 && right.ownDeleted === 0;
|
|
521
525
|
const oneSided =
|
|
522
|
-
(left.
|
|
523
|
-
|
|
526
|
+
insertions || (left.ownInserted === 0 && right.ownInserted === 0);
|
|
527
|
+
const merged = insertions
|
|
528
|
+
? left.inserted + equality + right.inserted
|
|
529
|
+
: left.deleted + equality + right.deleted;
|
|
524
530
|
if (
|
|
525
531
|
!oneSided ||
|
|
526
|
-
equality.includes("\n")
|
|
532
|
+
(equality.includes("\n") &&
|
|
533
|
+
!merged.startsWith(equality) &&
|
|
534
|
+
!merged.endsWith(equality)) ||
|
|
527
535
|
equality.length > Math.max(left.ownDeleted, left.ownInserted) ||
|
|
528
536
|
equality.length > Math.max(right.ownDeleted, right.ownInserted)
|
|
529
537
|
)
|
|
@@ -814,16 +822,24 @@ function wholeWordReplacements(
|
|
|
814
822
|
const removed = operation.before.changedText;
|
|
815
823
|
const inserted = operation.after.changedText;
|
|
816
824
|
const lexicalChange = /[\p{L}\p{M}\p{N}]/u.test(removed + inserted);
|
|
825
|
+
// An edge that is a line break, as when a whole paragraph is deleted,
|
|
826
|
+
// sits beside the neighboring block's word without splitting it.
|
|
827
|
+
const lineEdge = (edge: (text: string) => boolean) =>
|
|
828
|
+
edge(removed) && (!inserted || edge(inserted));
|
|
829
|
+
const widensBefore =
|
|
830
|
+
isWordBefore(before, from) && !lineEdge((text) => text.startsWith("\n"));
|
|
831
|
+
const widensAfter =
|
|
832
|
+
isWordAt(before, to) && !lineEdge((text) => text.endsWith("\n"));
|
|
817
833
|
let group = { from, to, members: [operation] };
|
|
818
834
|
if (
|
|
819
835
|
lexicalChange &&
|
|
820
836
|
!/<br\/?\s*>/u.test(removed + inserted) &&
|
|
821
837
|
(removed || !/\s/u.test(inserted)) &&
|
|
822
|
-
(
|
|
838
|
+
(widensBefore || widensAfter)
|
|
823
839
|
) {
|
|
824
|
-
while (isWordBefore(before, group.from))
|
|
840
|
+
while (widensBefore && isWordBefore(before, group.from))
|
|
825
841
|
group.from -= characterBefore(before, group.from).length;
|
|
826
|
-
while (isWordAt(before, group.to))
|
|
842
|
+
while (widensAfter && isWordAt(before, group.to))
|
|
827
843
|
group.to += characterAt(before, group.to).length;
|
|
828
844
|
}
|
|
829
845
|
// Two edits inside one word are one review decision.
|
|
@@ -1022,6 +1038,7 @@ export function markdownSuggestionOperationsForReplacements(input: {
|
|
|
1022
1038
|
const [{ from, to }] = replacements;
|
|
1023
1039
|
if (
|
|
1024
1040
|
operations.length > 1 &&
|
|
1041
|
+
!before.slice(from, to).includes("\n") &&
|
|
1025
1042
|
operations.every(
|
|
1026
1043
|
(operation) =>
|
|
1027
1044
|
operation.anchor.from >= from && operation.anchor.to <= to,
|