@k2b/cloud 0.44.0 → 0.46.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/package.json +9 -2
- package/src/_internal/app-queries.ts +73 -0
- package/src/_internal/capabilities.ts +137 -9
- package/src/_internal/define-app.ts +2 -0
- package/src/_internal/registry-validation.ts +17 -4
- package/src/_internal/registry.ts +32 -4
- package/src/_internal/resource-principals.ts +36 -0
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +11 -2
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/ai/capabilities.ts +4 -2
- package/src/ai/chat/map-messages.ts +270 -0
- package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
- package/src/ai/chat/map-notebook.tsx +301 -0
- package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
- package/src/ai/chat/map-widget.css +107 -0
- package/src/ai/chat/map-widget.tsx +1279 -0
- package/src/ai/chat/message-utils.ts +1 -0
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +2 -1
- package/src/ai/chat/turn-view.tsx +17 -1
- package/src/ai/default-tools.ts +3 -0
- package/src/ai/dictation-runtime.ts +6 -2
- package/src/ai/executor.ts +22 -4
- package/src/ai/index.ts +1 -0
- package/src/ai/map-contracts.ts +209 -0
- package/src/ai/map-execution.ts +14 -0
- package/src/ai/map-notebook-save.ts +276 -0
- package/src/ai/map-notebook.ts +32 -0
- package/src/ai/map-opening-hours.ts +40 -0
- package/src/ai/map-routes.ts +84 -0
- package/src/ai/map-service.ts +288 -0
- package/src/ai/map-store.ts +137 -0
- package/src/ai/map-tool.ts +73 -0
- package/src/ai/migrate.ts +3 -0
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +56 -46
- package/src/ai/projects.ts +100 -79
- package/src/ai/protocol.ts +3 -1
- package/src/ai/recent-resources.ts +3 -3
- package/src/ai/routes.ts +32 -30
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- package/src/ai/store.ts +19 -6
- package/src/ai/tools.ts +14 -0
- package/src/ai/types.ts +3 -0
- package/src/api/accounts-entities.ts +6 -3
- package/src/api/admin-ai-skills.ts +4 -1
- package/src/api/auth.ts +9 -2
- package/src/api/capabilities.ts +90 -12
- package/src/api/mcp.ts +4 -2
- package/src/browser/notifications.ts +25 -8
- package/src/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -0
- package/src/capabilities/discussion-providers.ts +99 -0
- package/src/capabilities/executions.ts +2 -2
- package/src/capabilities/migrate.ts +10 -1
- package/src/capabilities/resource-readers.ts +58 -0
- package/src/capabilities/server-invoke.ts +23 -13
- package/src/capabilities/server.ts +17 -6
- package/src/capabilities/testing.ts +13 -0
- package/src/contracts/capabilities.ts +137 -4
- package/src/contracts/discussions.ts +91 -0
- package/src/contracts/geo-document.ts +60 -0
- package/src/contracts/geo-snapshot.ts +86 -0
- package/src/contracts/geo.ts +14 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/resource-types.ts +21 -0
- package/src/contracts/shared.ts +2 -1
- package/src/geo/AddressAutocomplete.tsx +213 -0
- package/src/geo/messages.ts +29 -0
- package/src/geo/navigation.ts +25 -0
- package/src/geo/ui.ts +2 -0
- package/src/server/api/respond.ts +10 -2
- package/src/server/index.ts +1 -0
- package/src/server/services/access.ts +23 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +56 -17
- package/src/services/accounts/entities.ts +36 -16
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +26 -17
- package/src/services/accounts/identities.ts +8 -1
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +141 -27
- package/src/services/accounts/machine-group-management.ts +94 -0
- package/src/services/accounts/users.ts +38 -8
- package/src/services/geo/cache.ts +2 -1
- package/src/services/geo/geoapify.ts +3 -0
- package/src/services/identity/invocation-token.ts +110 -0
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +1 -0
- package/src/services/pdf/geo-snapshot.ts +107 -0
- package/src/services/pdf/index.ts +2 -0
- package/src/services/session/index.ts +12 -3
- package/src/shared/email-html.ts +1 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +428 -77
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AdminArea.tsx +2 -2
- package/src/ssr/AdminLayout.tsx +1 -1
- package/src/ssr/AdminSidebar.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/CloudShell.island.tsx +19 -3
- package/src/ssr/Layout.tsx +1 -1
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-navigation.ts +1 -11
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/platform-messages.ts +2 -0
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/shell-data.tsx +1 -1
- package/src/styles/global.css +2 -0
- package/src/workflows/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +2 -1
|
@@ -25,8 +25,15 @@ import { escapeHtml } from "./shared";
|
|
|
25
25
|
// Create a configured marked instance
|
|
26
26
|
type MarkdownProfile = "content" | "help";
|
|
27
27
|
type LinkStyle = "widget" | "plain";
|
|
28
|
+
type MarkdownOutput = "cloud" | "email";
|
|
28
29
|
|
|
29
|
-
const createMarked = (
|
|
30
|
+
const createMarked = (
|
|
31
|
+
profile: MarkdownProfile,
|
|
32
|
+
links: LinkStyle,
|
|
33
|
+
infoBlocks: MarkedExtension,
|
|
34
|
+
locale?: string,
|
|
35
|
+
output: MarkdownOutput = "cloud",
|
|
36
|
+
) => {
|
|
30
37
|
const marked = new Marked();
|
|
31
38
|
|
|
32
39
|
marked.use({
|
|
@@ -38,12 +45,12 @@ const createMarked = (profile: MarkdownProfile, links: LinkStyle, infoBlocks: Ma
|
|
|
38
45
|
// Note: katexExtension must come before codeExtension to handle ```math blocks
|
|
39
46
|
marked.use(infoBlocks);
|
|
40
47
|
marked.use(taskListExtension());
|
|
41
|
-
marked.use(tablesExtension());
|
|
48
|
+
marked.use(tablesExtension(output === "email"));
|
|
42
49
|
// Plain output keeps marked's own link renderer and renders code blocks as a bare `pre`.
|
|
43
50
|
// Cloud links are calm reference pills and quiet web links from `@k2b/ui`.
|
|
44
51
|
if (links === "widget") marked.use(linksExtension({ internalTarget: profile === "help" ? "_self" : "_blank", locale }));
|
|
45
52
|
marked.use(imagesExtension());
|
|
46
|
-
marked.use(katexExtension());
|
|
53
|
+
marked.use(katexExtension(output === "email"));
|
|
47
54
|
marked.use(links === "widget" ? codeExtension() : plainCodeExtension());
|
|
48
55
|
// Inline-style decorators come last so they run after structural tokenizers.
|
|
49
56
|
marked.use(markExtension());
|
|
@@ -57,12 +64,12 @@ const createMarked = (profile: MarkdownProfile, links: LinkStyle, infoBlocks: Ma
|
|
|
57
64
|
// headers choose the locale, and `markdownInfoBlocks()` returns one extension
|
|
58
65
|
// for every locale that shares a catalog, so the cache stays bounded.
|
|
59
66
|
const instances = new Map<MarkedExtension, Map<string, Marked>>();
|
|
60
|
-
const markedFor = (profile: MarkdownProfile, links: LinkStyle = "widget", locale?: string): Marked => {
|
|
67
|
+
const markedFor = (profile: MarkdownProfile, links: LinkStyle = "widget", locale?: string, output: MarkdownOutput = "cloud"): Marked => {
|
|
61
68
|
const infoBlocks = markdownInfoBlocks({ locale });
|
|
62
69
|
const byStyle = instances.get(infoBlocks) ?? new Map<string, Marked>();
|
|
63
70
|
instances.set(infoBlocks, byStyle);
|
|
64
|
-
const key = `${profile}:${links}`;
|
|
65
|
-
const instance = byStyle.get(key) ?? createMarked(profile, links, infoBlocks, locale);
|
|
71
|
+
const key = `${profile}:${links}:${output}`;
|
|
72
|
+
const instance = byStyle.get(key) ?? createMarked(profile, links, infoBlocks, locale, output);
|
|
66
73
|
byStyle.set(key, instance);
|
|
67
74
|
return instance;
|
|
68
75
|
};
|
|
@@ -70,18 +77,21 @@ const markedFor = (profile: MarkdownProfile, links: LinkStyle = "widget", locale
|
|
|
70
77
|
const marked = markedFor("content");
|
|
71
78
|
|
|
72
79
|
export type MarkdownRenderOptions = {
|
|
80
|
+
/** `"email"` keeps strikethrough and structural table alignment, and renders table formulas and math as literal source. Defaults to Cloud's styled output with formula evaluation. */
|
|
81
|
+
output?: MarkdownOutput;
|
|
73
82
|
/** `"plain"` renders each link as an ordinary anchor around its text, for HTML read outside Cloud such as email. It also renders a code block, fenced or indented and including a Mermaid fence, as its source in a `pre` without the language label. The default renders references to Cloud content as calm pills and web links as quiet text links. */
|
|
74
83
|
links?: LinkStyle;
|
|
75
84
|
/** Request locale for the screen-reader names of untitled info blocks. Defaults to English on the server and the page language in the browser. */
|
|
76
85
|
locale?: string;
|
|
77
86
|
};
|
|
78
87
|
|
|
79
|
-
const sanitizeRenderedHtml = (html: string, profile: MarkdownProfile = "content"): string => {
|
|
88
|
+
const sanitizeRenderedHtml = (html: string, profile: MarkdownProfile = "content", output: MarkdownOutput = "cloud"): string => {
|
|
80
89
|
const callouts = markdownCalloutSanitizeOptions();
|
|
81
90
|
return sanitizeHtml(html, {
|
|
82
91
|
...callouts,
|
|
83
92
|
allowedTags: [
|
|
84
93
|
...sanitizeHtml.defaults.allowedTags,
|
|
94
|
+
...(output === "email" ? ["del"] : []),
|
|
85
95
|
"annotation",
|
|
86
96
|
"aside",
|
|
87
97
|
"br",
|
|
@@ -137,6 +147,7 @@ const sanitizeRenderedHtml = (html: string, profile: MarkdownProfile = "content"
|
|
|
137
147
|
input: ["checked", "class", "disabled", "type"],
|
|
138
148
|
math: ["xmlns"],
|
|
139
149
|
pre: ["class"],
|
|
150
|
+
...(output === "email" ? { th: ["align"], td: ["align"] } : {}),
|
|
140
151
|
...callouts.allowedAttributes,
|
|
141
152
|
span: ["aria-hidden", "class", "style", "title"],
|
|
142
153
|
},
|
|
@@ -192,10 +203,10 @@ const sanitizeRenderedHtml = (html: string, profile: MarkdownProfile = "content"
|
|
|
192
203
|
export function renderMarkdown(content: string, options: MarkdownRenderOptions = {}): string {
|
|
193
204
|
if (!content || typeof content !== "string") return "";
|
|
194
205
|
|
|
195
|
-
const html = markedFor("content", options.links, options.locale).parse(content);
|
|
206
|
+
const html = markedFor("content", options.links, options.locale, options.output).parse(content);
|
|
196
207
|
if (typeof html !== "string") return "";
|
|
197
208
|
|
|
198
|
-
return sanitizeRenderedHtml(html);
|
|
209
|
+
return sanitizeRenderedHtml(html, "content", options.output);
|
|
199
210
|
}
|
|
200
211
|
|
|
201
212
|
/**
|
|
@@ -204,10 +215,10 @@ export function renderMarkdown(content: string, options: MarkdownRenderOptions =
|
|
|
204
215
|
export function renderMarkdownSync(content: string, options: MarkdownRenderOptions = {}): string {
|
|
205
216
|
if (!content || typeof content !== "string") return "";
|
|
206
217
|
|
|
207
|
-
const html = markedFor("content", options.links, options.locale).parse(content);
|
|
218
|
+
const html = markedFor("content", options.links, options.locale, options.output).parse(content);
|
|
208
219
|
if (typeof html !== "string") return "";
|
|
209
220
|
|
|
210
|
-
return sanitizeRenderedHtml(html);
|
|
221
|
+
return sanitizeRenderedHtml(html, "content", options.output);
|
|
211
222
|
}
|
|
212
223
|
|
|
213
224
|
/**
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
|
|
3
|
+
/** Capture an existing character, never a boundary that could drift to a neighbour. */
|
|
4
|
+
export const captureTextAnchor = (text: Y.Text, index: number): Uint8Array => {
|
|
5
|
+
if (!text.doc || !Number.isSafeInteger(index) || index < 0 || index >= text.length)
|
|
6
|
+
throw new RangeError("A text anchor must identify an existing character in an attached Y.Text");
|
|
7
|
+
return Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text, index, 0));
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve only the original live character. Ordinary relative positions also
|
|
12
|
+
* resolve beside deleted characters and follow undo replacements; neither is
|
|
13
|
+
* a safe target for a delayed structural action.
|
|
14
|
+
*/
|
|
15
|
+
export const resolveTextAnchor = (text: Y.Text, bytes: Uint8Array): number | null => {
|
|
16
|
+
if (!text.doc) return null;
|
|
17
|
+
try {
|
|
18
|
+
const anchor = Y.decodeRelativePosition(bytes);
|
|
19
|
+
if (!anchor.item || anchor.assoc !== 0) return null;
|
|
20
|
+
const position = Y.createAbsolutePositionFromRelativePosition(anchor, text.doc, false);
|
|
21
|
+
if (!position || position.type !== text || position.index < 0 || position.index >= text.length) return null;
|
|
22
|
+
// Item encodings omit the root type name. Compare two decoded positions,
|
|
23
|
+
// rather than a decoded anchor with a fresh position containing that name.
|
|
24
|
+
const live = Y.decodeRelativePosition(captureTextAnchor(text, position.index));
|
|
25
|
+
return Y.compareRelativePositions(anchor, live) ? position.index : null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as decoding from "lib0/decoding";
|
|
2
|
+
import * as encoding from "lib0/encoding";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
const unsigned = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
6
|
+
const positionId = z.strictObject({ client: unsigned, clock: unsigned });
|
|
7
|
+
const relativePosition = z
|
|
8
|
+
.strictObject({
|
|
9
|
+
type: positionId.nullable().optional(),
|
|
10
|
+
tname: z.string().nullable().optional(),
|
|
11
|
+
item: positionId.nullable().optional(),
|
|
12
|
+
assoc: z.number().int().min(Number.MIN_SAFE_INTEGER).max(Number.MAX_SAFE_INTEGER).optional(),
|
|
13
|
+
})
|
|
14
|
+
.refine((position) => position.type != null || position.tname != null);
|
|
15
|
+
const stateSchema = z
|
|
16
|
+
.strictObject({
|
|
17
|
+
// Identity is supplied by the authenticated transport, never copied from here.
|
|
18
|
+
user: z.unknown().optional(),
|
|
19
|
+
cursor: z.strictObject({ anchor: relativePosition, head: relativePosition }).nullable().optional(),
|
|
20
|
+
})
|
|
21
|
+
.nullable();
|
|
22
|
+
|
|
23
|
+
export class InvalidSharedTextAwarenessError extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super("Invalid shared text awareness update");
|
|
26
|
+
this.name = "InvalidSharedTextAwarenessError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const encode = (clientId: number, clock: number, state: unknown): Uint8Array => {
|
|
31
|
+
const encoder = encoding.createEncoder();
|
|
32
|
+
encoding.writeVarUint(encoder, 1);
|
|
33
|
+
encoding.writeVarUint(encoder, clientId);
|
|
34
|
+
encoding.writeVarUint(encoder, clock);
|
|
35
|
+
encoding.writeVarString(encoder, JSON.stringify(state));
|
|
36
|
+
return encoding.toUint8Array(encoder);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** One authorized connection, with an ID allocated uniquely by its transport. */
|
|
40
|
+
export const createTrustedTextAwareness = (config: {
|
|
41
|
+
clientId: number;
|
|
42
|
+
textName: string;
|
|
43
|
+
user: { userId: string; name: string; color: string };
|
|
44
|
+
}) => {
|
|
45
|
+
if (!Number.isSafeInteger(config.clientId) || config.clientId < 2 ** 32) throw new RangeError("Invalid server awareness ID");
|
|
46
|
+
const user = { userId: config.user.userId, name: config.user.name, color: config.user.color };
|
|
47
|
+
let localClientId: number | undefined;
|
|
48
|
+
let clock = 0;
|
|
49
|
+
let active = false;
|
|
50
|
+
const next = (state: unknown): Uint8Array => {
|
|
51
|
+
if (clock === Number.MAX_SAFE_INTEGER) throw new RangeError("Awareness clock exhausted");
|
|
52
|
+
return encode(config.clientId, ++clock, state);
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
/** Validate one local state and replace its envelope and identity. Throws on invalid input. */
|
|
56
|
+
publish(bytes: Uint8Array): Uint8Array {
|
|
57
|
+
try {
|
|
58
|
+
const decoder = decoding.createDecoder(bytes);
|
|
59
|
+
if (decoding.readVarUint(decoder) !== 1) throw new InvalidSharedTextAwarenessError();
|
|
60
|
+
const clientId = decoding.readVarUint(decoder);
|
|
61
|
+
const clientClock = decoding.readVarUint(decoder);
|
|
62
|
+
if (!Number.isSafeInteger(clientId) || !Number.isSafeInteger(clientClock)) throw new InvalidSharedTextAwarenessError();
|
|
63
|
+
const state = stateSchema.parse(JSON.parse(decoding.readVarString(decoder)));
|
|
64
|
+
if (decoder.pos !== bytes.length || (localClientId !== undefined && localClientId !== clientId))
|
|
65
|
+
throw new InvalidSharedTextAwarenessError();
|
|
66
|
+
if (
|
|
67
|
+
state?.cursor &&
|
|
68
|
+
[state.cursor.anchor, state.cursor.head].some((position) => position.tname !== config.textName || position.type != null)
|
|
69
|
+
)
|
|
70
|
+
throw new InvalidSharedTextAwarenessError();
|
|
71
|
+
const result = next(state === null ? null : { user, writerId: clientId, cursor: state.cursor ?? null });
|
|
72
|
+
localClientId = clientId;
|
|
73
|
+
active = state !== null;
|
|
74
|
+
return result;
|
|
75
|
+
} catch {
|
|
76
|
+
throw new InvalidSharedTextAwarenessError();
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
/** Publish on leave/close, including when the browser cannot send its own null state. */
|
|
80
|
+
remove(): Uint8Array | null {
|
|
81
|
+
if (!active) return null;
|
|
82
|
+
const result = next(null);
|
|
83
|
+
active = false;
|
|
84
|
+
return result;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SharedTextChange } from "./types";
|
|
2
|
+
|
|
3
|
+
export const validateChanges = (text: string, changes: readonly SharedTextChange[], maxLength: number): SharedTextChange[] => {
|
|
4
|
+
const sorted = [...changes].sort((a, b) => a.from - b.from || a.to - b.to);
|
|
5
|
+
let length = text.length;
|
|
6
|
+
const splitsCharacter = (at: number) =>
|
|
7
|
+
at > 0 && at < text.length && /[\uD800-\uDBFF]/.test(text[at - 1]!) && /[\uDC00-\uDFFF]/.test(text[at]!);
|
|
8
|
+
for (let index = 0; index < sorted.length; index++) {
|
|
9
|
+
const change = sorted[index]!;
|
|
10
|
+
const previous = sorted[index - 1];
|
|
11
|
+
if (
|
|
12
|
+
!Number.isSafeInteger(change.from) ||
|
|
13
|
+
!Number.isSafeInteger(change.to) ||
|
|
14
|
+
change.from < 0 ||
|
|
15
|
+
change.to < change.from ||
|
|
16
|
+
change.to > text.length ||
|
|
17
|
+
(change.recreate === true && change.to === change.from) ||
|
|
18
|
+
(previous && (change.from < previous.to || change.from === previous.from))
|
|
19
|
+
)
|
|
20
|
+
throw new RangeError("Shared text changes must have distinct, non-overlapping original-source ranges");
|
|
21
|
+
if (splitsCharacter(change.from) || splitsCharacter(change.to) || !change.insert.isWellFormed())
|
|
22
|
+
throw new RangeError("Shared text changes must preserve Unicode characters");
|
|
23
|
+
length += change.insert.length - (change.to - change.from);
|
|
24
|
+
}
|
|
25
|
+
if (length > maxLength) throw new RangeError("Shared text changes exceed maxLength");
|
|
26
|
+
return sorted.filter((change) => change.recreate === true || text.slice(change.from, change.to) !== change.insert);
|
|
27
|
+
};
|