@echovisionlab/geul-common 0.2.3 → 0.3.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 +3 -2
- package/src/collaboration/block-room-codec/inline-content-model.ts +308 -0
- package/src/collaboration/block-room-codec/inline-content-mutations.ts +10 -296
- package/src/collaboration/block-room-codec/inline-content-projection.ts +4 -15
- package/src/collaboration/block-room-codec/internal.ts +39 -20
- package/src/collaboration/block-room-codec/locale-presence-storage.ts +170 -0
- package/src/collaboration/block-room-codec/locale-presence.ts +25 -839
- package/src/collaboration/block-room-codec/locale-targets.ts +513 -0
- package/src/collaboration/block-room-codec/locale-value-projection.ts +183 -0
- package/src/collaboration/block-room-codec/replay-base.ts +195 -0
- package/src/collaboration/block-room-codec/replay-locale.ts +182 -0
- package/src/collaboration/block-room-codec/replay-payload.ts +317 -0
- package/src/collaboration/block-room-codec/replay-structure.ts +504 -0
- package/src/collaboration/block-room-codec/replay.ts +75 -0
- package/src/collaboration/block-room-codec.ts +1 -0
- package/src/collaboration/form-schema-delta.ts +440 -0
- package/src/collaboration/menu.ts +109 -19
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@echovisionlab/geul-common",
|
|
3
3
|
"description": "Shared TypeScript contracts and helpers for Geul editors, collaboration, localized content, media blocks, pages, posts, and runtime events.",
|
|
4
4
|
"author": "state303 <state303@dsub.io>",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.3.0",
|
|
6
6
|
"private": false,
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"./collaboration/document": "./src/collaboration/document.ts",
|
|
41
41
|
"./collaboration/email-layout": "./src/collaboration/email-layout.ts",
|
|
42
42
|
"./collaboration/form": "./src/collaboration/form.ts",
|
|
43
|
+
"./collaboration/form-schema-delta": "./src/collaboration/form-schema-delta.ts",
|
|
43
44
|
"./collaboration/label": "./src/collaboration/label.ts",
|
|
44
45
|
"./collaboration/map-theme": "./src/collaboration/map-theme.ts",
|
|
45
46
|
"./collaboration/menu": "./src/collaboration/menu.ts",
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
},
|
|
63
64
|
"dependencies": {
|
|
64
65
|
"@bufbuild/protobuf": "^2.14.0",
|
|
65
|
-
"@echovisionlab/geul-proto": "^0.
|
|
66
|
+
"@echovisionlab/geul-proto": "^0.3.0",
|
|
66
67
|
"zod": "^4.4.3"
|
|
67
68
|
},
|
|
68
69
|
"peerDependencies": {
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import type { JsonValue } from "@bufbuild/protobuf";
|
|
2
|
+
import { fail } from "./internal.ts";
|
|
3
|
+
|
|
4
|
+
export const RICH_TEXT_STYLE_KEYS = [
|
|
5
|
+
"bold",
|
|
6
|
+
"italic",
|
|
7
|
+
"underline",
|
|
8
|
+
"strike",
|
|
9
|
+
"code",
|
|
10
|
+
"textColor",
|
|
11
|
+
"backgroundColor",
|
|
12
|
+
] as const;
|
|
13
|
+
const styleKeys = RICH_TEXT_STYLE_KEYS;
|
|
14
|
+
|
|
15
|
+
export type RichTextStyleKey = (typeof styleKeys)[number];
|
|
16
|
+
|
|
17
|
+
export type StyleValue = boolean | string;
|
|
18
|
+
export type Style = Partial<Record<(typeof styleKeys)[number], StyleValue>>;
|
|
19
|
+
export type StyledRun = { text: string; styles: Style };
|
|
20
|
+
|
|
21
|
+
interface DesiredTextGroup {
|
|
22
|
+
readonly kind: "text";
|
|
23
|
+
readonly runs: StyledRun[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DesiredLinkGroup {
|
|
27
|
+
readonly kind: "link";
|
|
28
|
+
readonly href: string;
|
|
29
|
+
readonly runs: StyledRun[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface DesiredMathGroup {
|
|
33
|
+
readonly kind: "mathInline";
|
|
34
|
+
readonly source: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface DesiredHardBreakGroup {
|
|
38
|
+
readonly kind: "hardBreak";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type DesiredGroup =
|
|
42
|
+
| DesiredTextGroup
|
|
43
|
+
| DesiredLinkGroup
|
|
44
|
+
| DesiredMathGroup
|
|
45
|
+
| DesiredHardBreakGroup;
|
|
46
|
+
|
|
47
|
+
function object(value: unknown, reason: string): Record<string, unknown> {
|
|
48
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
49
|
+
return fail(reason);
|
|
50
|
+
}
|
|
51
|
+
return value as Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function checkedStyles(value: unknown, reason: string): Style {
|
|
55
|
+
if (value === undefined || value === null) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
const raw = object(value, reason);
|
|
59
|
+
const result: Style = {};
|
|
60
|
+
for (const key of styleKeys) {
|
|
61
|
+
const style = raw[key];
|
|
62
|
+
if (style === undefined || style === null) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (key === "textColor" || key === "backgroundColor") {
|
|
66
|
+
if (typeof style !== "string") {
|
|
67
|
+
return fail(`${reason}:${key}`);
|
|
68
|
+
}
|
|
69
|
+
} else if (typeof style !== "boolean") {
|
|
70
|
+
return fail(`${reason}:${key}`);
|
|
71
|
+
}
|
|
72
|
+
result[key] = style;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeStyles(value: Style): Style {
|
|
78
|
+
const result: Style = {};
|
|
79
|
+
for (const key of styleKeys) {
|
|
80
|
+
const style = value[key];
|
|
81
|
+
if (style === undefined || style === false || style === "") {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
result[key] = style;
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function styleKey(value: Style): string {
|
|
90
|
+
return JSON.stringify(normalizeStyles(value));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function appendRun(target: StyledRun[], next: StyledRun): void {
|
|
94
|
+
const previous = target[target.length - 1];
|
|
95
|
+
if (previous && styleKey(previous.styles) === styleKey(next.styles)) {
|
|
96
|
+
previous.text += next.text;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
target.push({ text: next.text, styles: { ...next.styles } });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function appendSourceRun(target: StyledRun[], next: StyledRun): void {
|
|
103
|
+
if (next.text.length > 0) {
|
|
104
|
+
target.push({ text: next.text, styles: { ...next.styles } });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function styledInput(value: unknown, reason: string): StyledRun {
|
|
109
|
+
if (typeof value === "string") {
|
|
110
|
+
return { text: value, styles: {} };
|
|
111
|
+
}
|
|
112
|
+
const styled = object(value, reason);
|
|
113
|
+
if (typeof styled.text !== "string") {
|
|
114
|
+
return fail(`${reason}:text`);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
text: styled.text,
|
|
118
|
+
styles: checkedStyles(styled.styles, `${reason}:styles`),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function inputItem(value: JsonValue, reason: string): DesiredGroup {
|
|
123
|
+
const item = object(value, reason);
|
|
124
|
+
const cases = ["text", "link", "mathInline", "hardBreak"].filter(
|
|
125
|
+
(key) => item[key] !== undefined,
|
|
126
|
+
);
|
|
127
|
+
if (cases.length !== 1) {
|
|
128
|
+
return fail(`${reason}:oneof`);
|
|
129
|
+
}
|
|
130
|
+
const selected = cases[0]!;
|
|
131
|
+
if (selected === "text") {
|
|
132
|
+
const run = styledInput(item.text, `${reason}:text`);
|
|
133
|
+
return { kind: "text", runs: run.text ? [run] : [] };
|
|
134
|
+
}
|
|
135
|
+
if (selected === "link") {
|
|
136
|
+
const link = object(item.link, `${reason}:link`);
|
|
137
|
+
if (typeof link.href !== "string" || !Array.isArray(link.content)) {
|
|
138
|
+
return fail(`${reason}:link_shape`);
|
|
139
|
+
}
|
|
140
|
+
const runs: StyledRun[] = [];
|
|
141
|
+
link.content.forEach((child, index) => {
|
|
142
|
+
appendSourceRun(
|
|
143
|
+
runs,
|
|
144
|
+
styledInput(child, `${reason}:link_content:${index}`),
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
return { kind: "link", href: link.href, runs };
|
|
148
|
+
}
|
|
149
|
+
if (selected === "mathInline") {
|
|
150
|
+
const math = object(item.mathInline, `${reason}:math`);
|
|
151
|
+
if (math.source !== undefined && typeof math.source !== "string") {
|
|
152
|
+
return fail(`${reason}:math_source`);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
kind: "mathInline",
|
|
156
|
+
source: (math.source as string | undefined) ?? "",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return { kind: "hardBreak" };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function desiredGroups(values: readonly JsonValue[]): DesiredGroup[] {
|
|
163
|
+
const result: DesiredGroup[] = [];
|
|
164
|
+
values.forEach((value, index) => {
|
|
165
|
+
const group = inputItem(value, `inline_content:input:${index}`);
|
|
166
|
+
const previous = result[result.length - 1];
|
|
167
|
+
if (group.kind === "text" && previous?.kind === "text") {
|
|
168
|
+
for (const run of group.runs) {
|
|
169
|
+
appendSourceRun(previous.runs, run);
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (group.kind === "text" && group.runs.length === 0) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
result.push(group);
|
|
177
|
+
});
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function normalizeInlineContent(values: readonly JsonValue[]): string {
|
|
182
|
+
const normalized = desiredGroups(values).map((group) => {
|
|
183
|
+
if (group.kind === "text") {
|
|
184
|
+
const canonicalRuns: StyledRun[] = [];
|
|
185
|
+
for (const run of group.runs) {
|
|
186
|
+
appendRun(canonicalRuns, run);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
text: canonicalRuns.map((run) => ({
|
|
190
|
+
text: run.text,
|
|
191
|
+
styles: normalizeStyles(run.styles),
|
|
192
|
+
})),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (group.kind === "link") {
|
|
196
|
+
const canonicalRuns: StyledRun[] = [];
|
|
197
|
+
for (const run of group.runs) {
|
|
198
|
+
appendRun(canonicalRuns, run);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
link: {
|
|
202
|
+
href: group.href,
|
|
203
|
+
content: canonicalRuns.map((run) => ({
|
|
204
|
+
text: run.text,
|
|
205
|
+
styles: normalizeStyles(run.styles),
|
|
206
|
+
})),
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (group.kind === "mathInline") {
|
|
211
|
+
return { mathInline: { source: group.source } };
|
|
212
|
+
}
|
|
213
|
+
return { hardBreak: {} };
|
|
214
|
+
});
|
|
215
|
+
return JSON.stringify(normalized);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface StyleSpan {
|
|
219
|
+
readonly from: number;
|
|
220
|
+
readonly to: number;
|
|
221
|
+
readonly styles: Style;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function styleSpans(runs: readonly StyledRun[], start = 0): StyleSpan[] {
|
|
225
|
+
const result: StyleSpan[] = [];
|
|
226
|
+
let from = start;
|
|
227
|
+
for (const run of runs) {
|
|
228
|
+
const to = from + run.text.length;
|
|
229
|
+
result.push({ from, to, styles: normalizeStyles(run.styles) });
|
|
230
|
+
from = to;
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function changedStyleAttributes(
|
|
236
|
+
before: Style,
|
|
237
|
+
after: Style,
|
|
238
|
+
): Record<string, StyleValue> {
|
|
239
|
+
const current = normalizeStyles(before);
|
|
240
|
+
const desired = normalizeStyles(after);
|
|
241
|
+
const result: Record<string, StyleValue> = {};
|
|
242
|
+
for (const key of styleKeys) {
|
|
243
|
+
if (current[key] === desired[key]) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const value = desired[key];
|
|
247
|
+
result[key] =
|
|
248
|
+
value !== undefined
|
|
249
|
+
? value
|
|
250
|
+
: key === "textColor" || key === "backgroundColor"
|
|
251
|
+
? ""
|
|
252
|
+
: false;
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function isSurrogatePairBoundary(value: string, boundary: number): boolean {
|
|
258
|
+
if (boundary <= 0 || boundary >= value.length) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
const before = value.charCodeAt(boundary - 1);
|
|
262
|
+
const after = value.charCodeAt(boundary);
|
|
263
|
+
return (
|
|
264
|
+
before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function minimalDiff(
|
|
269
|
+
before: string,
|
|
270
|
+
after: string,
|
|
271
|
+
): {
|
|
272
|
+
from: number;
|
|
273
|
+
oldTo: number;
|
|
274
|
+
newTo: number;
|
|
275
|
+
} {
|
|
276
|
+
let from = 0;
|
|
277
|
+
while (
|
|
278
|
+
from < before.length &&
|
|
279
|
+
from < after.length &&
|
|
280
|
+
before[from] === after[from]
|
|
281
|
+
) {
|
|
282
|
+
from += 1;
|
|
283
|
+
}
|
|
284
|
+
if (
|
|
285
|
+
isSurrogatePairBoundary(before, from) ||
|
|
286
|
+
isSurrogatePairBoundary(after, from)
|
|
287
|
+
) {
|
|
288
|
+
from -= 1;
|
|
289
|
+
}
|
|
290
|
+
let oldTo = before.length;
|
|
291
|
+
let newTo = after.length;
|
|
292
|
+
while (
|
|
293
|
+
oldTo > from &&
|
|
294
|
+
newTo > from &&
|
|
295
|
+
before[oldTo - 1] === after[newTo - 1]
|
|
296
|
+
) {
|
|
297
|
+
oldTo -= 1;
|
|
298
|
+
newTo -= 1;
|
|
299
|
+
}
|
|
300
|
+
if (
|
|
301
|
+
isSurrogatePairBoundary(before, oldTo) ||
|
|
302
|
+
isSurrogatePairBoundary(after, newTo)
|
|
303
|
+
) {
|
|
304
|
+
oldTo += 1;
|
|
305
|
+
newTo += 1;
|
|
306
|
+
}
|
|
307
|
+
return { from, oldTo, newTo };
|
|
308
|
+
}
|
|
@@ -14,8 +14,17 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
fromYInlineContent,
|
|
16
16
|
projectedStyledTextRuns,
|
|
17
|
-
richTextStyleKeys,
|
|
18
17
|
} from "./inline-content-projection.ts";
|
|
18
|
+
import {
|
|
19
|
+
changedStyleAttributes,
|
|
20
|
+
desiredGroups,
|
|
21
|
+
minimalDiff,
|
|
22
|
+
normalizeInlineContent,
|
|
23
|
+
styledInput,
|
|
24
|
+
styleSpans,
|
|
25
|
+
type DesiredGroup,
|
|
26
|
+
type StyledRun,
|
|
27
|
+
} from "./inline-content-model.ts";
|
|
19
28
|
import {
|
|
20
29
|
nodeKind,
|
|
21
30
|
payloadArray,
|
|
@@ -32,12 +41,6 @@ type RichTextCatalogEntry = {
|
|
|
32
41
|
const richTextCatalog = richTextBlockCatalog as Readonly<
|
|
33
42
|
Record<string, RichTextCatalogEntry>
|
|
34
43
|
>;
|
|
35
|
-
const styleKeys = richTextStyleKeys();
|
|
36
|
-
|
|
37
|
-
type StyleValue = boolean | string;
|
|
38
|
-
type Style = Partial<Record<(typeof styleKeys)[number], StyleValue>>;
|
|
39
|
-
type StyledRun = { text: string; styles: Style };
|
|
40
|
-
|
|
41
44
|
interface RawTextSegment {
|
|
42
45
|
text?: Y.Text;
|
|
43
46
|
readonly styledText: Y.Map<unknown>;
|
|
@@ -75,39 +78,6 @@ interface RawHardBreakGroup {
|
|
|
75
78
|
|
|
76
79
|
type RawGroup = RawTextGroup | RawLinkGroup | RawMathGroup | RawHardBreakGroup;
|
|
77
80
|
|
|
78
|
-
interface DesiredTextGroup {
|
|
79
|
-
readonly kind: "text";
|
|
80
|
-
readonly runs: StyledRun[];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
interface DesiredLinkGroup {
|
|
84
|
-
readonly kind: "link";
|
|
85
|
-
readonly href: string;
|
|
86
|
-
readonly runs: StyledRun[];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
interface DesiredMathGroup {
|
|
90
|
-
readonly kind: "mathInline";
|
|
91
|
-
readonly source: string;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface DesiredHardBreakGroup {
|
|
95
|
-
readonly kind: "hardBreak";
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
type DesiredGroup =
|
|
99
|
-
| DesiredTextGroup
|
|
100
|
-
| DesiredLinkGroup
|
|
101
|
-
| DesiredMathGroup
|
|
102
|
-
| DesiredHardBreakGroup;
|
|
103
|
-
|
|
104
|
-
function object(value: unknown, reason: string): Record<string, unknown> {
|
|
105
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
106
|
-
return fail(reason);
|
|
107
|
-
}
|
|
108
|
-
return value as Record<string, unknown>;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
81
|
function yMap(value: unknown, reason: string): Y.Map<unknown> {
|
|
112
82
|
if (!(value instanceof Y.Map)) {
|
|
113
83
|
return fail(reason);
|
|
@@ -115,170 +85,6 @@ function yMap(value: unknown, reason: string): Y.Map<unknown> {
|
|
|
115
85
|
return value;
|
|
116
86
|
}
|
|
117
87
|
|
|
118
|
-
function checkedStyles(value: unknown, reason: string): Style {
|
|
119
|
-
if (value === undefined || value === null) {
|
|
120
|
-
return {};
|
|
121
|
-
}
|
|
122
|
-
const raw = object(value, reason);
|
|
123
|
-
const result: Style = {};
|
|
124
|
-
for (const key of styleKeys) {
|
|
125
|
-
const style = raw[key];
|
|
126
|
-
if (style === undefined || style === null) {
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (key === "textColor" || key === "backgroundColor") {
|
|
130
|
-
if (typeof style !== "string") {
|
|
131
|
-
return fail(`${reason}:${key}`);
|
|
132
|
-
}
|
|
133
|
-
} else if (typeof style !== "boolean") {
|
|
134
|
-
return fail(`${reason}:${key}`);
|
|
135
|
-
}
|
|
136
|
-
result[key] = style;
|
|
137
|
-
}
|
|
138
|
-
return result;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function normalizeStyles(value: Style): Style {
|
|
142
|
-
const result: Style = {};
|
|
143
|
-
for (const key of styleKeys) {
|
|
144
|
-
const style = value[key];
|
|
145
|
-
if (style === undefined || style === false || style === "") {
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
result[key] = style;
|
|
149
|
-
}
|
|
150
|
-
return result;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function styleKey(value: Style): string {
|
|
154
|
-
return JSON.stringify(normalizeStyles(value));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function appendRun(target: StyledRun[], next: StyledRun): void {
|
|
158
|
-
const previous = target[target.length - 1];
|
|
159
|
-
if (previous && styleKey(previous.styles) === styleKey(next.styles)) {
|
|
160
|
-
previous.text += next.text;
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
target.push({ text: next.text, styles: { ...next.styles } });
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function appendSourceRun(target: StyledRun[], next: StyledRun): void {
|
|
167
|
-
if (next.text.length > 0) {
|
|
168
|
-
target.push({ text: next.text, styles: { ...next.styles } });
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function styledInput(value: unknown, reason: string): StyledRun {
|
|
173
|
-
if (typeof value === "string") {
|
|
174
|
-
return { text: value, styles: {} };
|
|
175
|
-
}
|
|
176
|
-
const styled = object(value, reason);
|
|
177
|
-
if (typeof styled.text !== "string") {
|
|
178
|
-
return fail(`${reason}:text`);
|
|
179
|
-
}
|
|
180
|
-
return {
|
|
181
|
-
text: styled.text,
|
|
182
|
-
styles: checkedStyles(styled.styles, `${reason}:styles`),
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function inputItem(value: JsonValue, reason: string): DesiredGroup {
|
|
187
|
-
const item = object(value, reason);
|
|
188
|
-
const cases = ["text", "link", "mathInline", "hardBreak"].filter(
|
|
189
|
-
(key) => item[key] !== undefined,
|
|
190
|
-
);
|
|
191
|
-
if (cases.length !== 1) {
|
|
192
|
-
return fail(`${reason}:oneof`);
|
|
193
|
-
}
|
|
194
|
-
const selected = cases[0]!;
|
|
195
|
-
if (selected === "text") {
|
|
196
|
-
const run = styledInput(item.text, `${reason}:text`);
|
|
197
|
-
return { kind: "text", runs: run.text ? [run] : [] };
|
|
198
|
-
}
|
|
199
|
-
if (selected === "link") {
|
|
200
|
-
const link = object(item.link, `${reason}:link`);
|
|
201
|
-
if (typeof link.href !== "string" || !Array.isArray(link.content)) {
|
|
202
|
-
return fail(`${reason}:link_shape`);
|
|
203
|
-
}
|
|
204
|
-
const runs: StyledRun[] = [];
|
|
205
|
-
link.content.forEach((child, index) => {
|
|
206
|
-
appendSourceRun(
|
|
207
|
-
runs,
|
|
208
|
-
styledInput(child, `${reason}:link_content:${index}`),
|
|
209
|
-
);
|
|
210
|
-
});
|
|
211
|
-
return { kind: "link", href: link.href, runs };
|
|
212
|
-
}
|
|
213
|
-
if (selected === "mathInline") {
|
|
214
|
-
const math = object(item.mathInline, `${reason}:math`);
|
|
215
|
-
if (math.source !== undefined && typeof math.source !== "string") {
|
|
216
|
-
return fail(`${reason}:math_source`);
|
|
217
|
-
}
|
|
218
|
-
return {
|
|
219
|
-
kind: "mathInline",
|
|
220
|
-
source: (math.source as string | undefined) ?? "",
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
return { kind: "hardBreak" };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function desiredGroups(values: readonly JsonValue[]): DesiredGroup[] {
|
|
227
|
-
const result: DesiredGroup[] = [];
|
|
228
|
-
values.forEach((value, index) => {
|
|
229
|
-
const group = inputItem(value, `inline_content:input:${index}`);
|
|
230
|
-
const previous = result[result.length - 1];
|
|
231
|
-
if (group.kind === "text" && previous?.kind === "text") {
|
|
232
|
-
for (const run of group.runs) {
|
|
233
|
-
appendSourceRun(previous.runs, run);
|
|
234
|
-
}
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
if (group.kind === "text" && group.runs.length === 0) {
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
result.push(group);
|
|
241
|
-
});
|
|
242
|
-
return result;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function normalizeInlineContent(values: readonly JsonValue[]): string {
|
|
246
|
-
const normalized = desiredGroups(values).map((group) => {
|
|
247
|
-
if (group.kind === "text") {
|
|
248
|
-
const canonicalRuns: StyledRun[] = [];
|
|
249
|
-
for (const run of group.runs) {
|
|
250
|
-
appendRun(canonicalRuns, run);
|
|
251
|
-
}
|
|
252
|
-
return {
|
|
253
|
-
text: canonicalRuns.map((run) => ({
|
|
254
|
-
text: run.text,
|
|
255
|
-
styles: normalizeStyles(run.styles),
|
|
256
|
-
})),
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
if (group.kind === "link") {
|
|
260
|
-
const canonicalRuns: StyledRun[] = [];
|
|
261
|
-
for (const run of group.runs) {
|
|
262
|
-
appendRun(canonicalRuns, run);
|
|
263
|
-
}
|
|
264
|
-
return {
|
|
265
|
-
link: {
|
|
266
|
-
href: group.href,
|
|
267
|
-
content: canonicalRuns.map((run) => ({
|
|
268
|
-
text: run.text,
|
|
269
|
-
styles: normalizeStyles(run.styles),
|
|
270
|
-
})),
|
|
271
|
-
},
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
if (group.kind === "mathInline") {
|
|
275
|
-
return { mathInline: { source: group.source } };
|
|
276
|
-
}
|
|
277
|
-
return { hardBreak: {} };
|
|
278
|
-
});
|
|
279
|
-
return JSON.stringify(normalized);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
88
|
function rawStyledText(
|
|
283
89
|
value: unknown,
|
|
284
90
|
rawIndex: number,
|
|
@@ -374,98 +180,6 @@ function rawGroupKind(group: RawGroup): DesiredGroup["kind"] {
|
|
|
374
180
|
return group.kind;
|
|
375
181
|
}
|
|
376
182
|
|
|
377
|
-
interface StyleSpan {
|
|
378
|
-
readonly from: number;
|
|
379
|
-
readonly to: number;
|
|
380
|
-
readonly styles: Style;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function styleSpans(runs: readonly StyledRun[], start = 0): StyleSpan[] {
|
|
384
|
-
const result: StyleSpan[] = [];
|
|
385
|
-
let from = start;
|
|
386
|
-
for (const run of runs) {
|
|
387
|
-
const to = from + run.text.length;
|
|
388
|
-
result.push({ from, to, styles: normalizeStyles(run.styles) });
|
|
389
|
-
from = to;
|
|
390
|
-
}
|
|
391
|
-
return result;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function changedStyleAttributes(
|
|
395
|
-
before: Style,
|
|
396
|
-
after: Style,
|
|
397
|
-
): Record<string, StyleValue> {
|
|
398
|
-
const current = normalizeStyles(before);
|
|
399
|
-
const desired = normalizeStyles(after);
|
|
400
|
-
const result: Record<string, StyleValue> = {};
|
|
401
|
-
for (const key of styleKeys) {
|
|
402
|
-
if (current[key] === desired[key]) {
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
const value = desired[key];
|
|
406
|
-
result[key] =
|
|
407
|
-
value !== undefined
|
|
408
|
-
? value
|
|
409
|
-
: key === "textColor" || key === "backgroundColor"
|
|
410
|
-
? ""
|
|
411
|
-
: false;
|
|
412
|
-
}
|
|
413
|
-
return result;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function isSurrogatePairBoundary(value: string, boundary: number): boolean {
|
|
417
|
-
if (boundary <= 0 || boundary >= value.length) {
|
|
418
|
-
return false;
|
|
419
|
-
}
|
|
420
|
-
const before = value.charCodeAt(boundary - 1);
|
|
421
|
-
const after = value.charCodeAt(boundary);
|
|
422
|
-
return (
|
|
423
|
-
before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function minimalDiff(
|
|
428
|
-
before: string,
|
|
429
|
-
after: string,
|
|
430
|
-
): {
|
|
431
|
-
from: number;
|
|
432
|
-
oldTo: number;
|
|
433
|
-
newTo: number;
|
|
434
|
-
} {
|
|
435
|
-
let from = 0;
|
|
436
|
-
while (
|
|
437
|
-
from < before.length &&
|
|
438
|
-
from < after.length &&
|
|
439
|
-
before[from] === after[from]
|
|
440
|
-
) {
|
|
441
|
-
from += 1;
|
|
442
|
-
}
|
|
443
|
-
if (
|
|
444
|
-
isSurrogatePairBoundary(before, from) ||
|
|
445
|
-
isSurrogatePairBoundary(after, from)
|
|
446
|
-
) {
|
|
447
|
-
from -= 1;
|
|
448
|
-
}
|
|
449
|
-
let oldTo = before.length;
|
|
450
|
-
let newTo = after.length;
|
|
451
|
-
while (
|
|
452
|
-
oldTo > from &&
|
|
453
|
-
newTo > from &&
|
|
454
|
-
before[oldTo - 1] === after[newTo - 1]
|
|
455
|
-
) {
|
|
456
|
-
oldTo -= 1;
|
|
457
|
-
newTo -= 1;
|
|
458
|
-
}
|
|
459
|
-
if (
|
|
460
|
-
isSurrogatePairBoundary(before, oldTo) ||
|
|
461
|
-
isSurrogatePairBoundary(after, newTo)
|
|
462
|
-
) {
|
|
463
|
-
oldTo += 1;
|
|
464
|
-
newTo += 1;
|
|
465
|
-
}
|
|
466
|
-
return { from, oldTo, newTo };
|
|
467
|
-
}
|
|
468
|
-
|
|
469
183
|
function segmentText(segment: RawTextSegment): string {
|
|
470
184
|
return segment.text?.toString() ?? "";
|
|
471
185
|
}
|
|
@@ -12,17 +12,10 @@ import {
|
|
|
12
12
|
type JsonObject,
|
|
13
13
|
} from "./internal.ts";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"strike",
|
|
20
|
-
"code",
|
|
21
|
-
"textColor",
|
|
22
|
-
"backgroundColor",
|
|
23
|
-
] as const;
|
|
24
|
-
|
|
25
|
-
type RichTextStyleKey = (typeof RICH_TEXT_STYLE_KEYS)[number];
|
|
15
|
+
import {
|
|
16
|
+
RICH_TEXT_STYLE_KEYS,
|
|
17
|
+
type RichTextStyleKey,
|
|
18
|
+
} from "./inline-content-model.ts";
|
|
26
19
|
type RichTextStyleValue = boolean | string;
|
|
27
20
|
type RichTextStyle = Partial<Record<RichTextStyleKey, RichTextStyleValue>>;
|
|
28
21
|
type ProjectedTextRun = JsonObject & { text: string };
|
|
@@ -369,10 +362,6 @@ export function fromYRichTextBlockPayload(
|
|
|
369
362
|
return projected as JsonValue;
|
|
370
363
|
}
|
|
371
364
|
|
|
372
|
-
export function richTextStyleKeys(): readonly RichTextStyleKey[] {
|
|
373
|
-
return RICH_TEXT_STYLE_KEYS;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
365
|
export function projectedStyledTextRuns(
|
|
377
366
|
value: unknown,
|
|
378
367
|
reason = "inline_content:styled_text",
|