@echovisionlab/geul-common 0.2.2 → 0.2.4
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 +1 -1
- package/src/collaboration/block-room-codec/inline-content-model.ts +308 -0
- package/src/collaboration/block-room-codec/inline-content-mutations.ts +13 -273
- package/src/collaboration/block-room-codec/inline-content-projection.ts +12 -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/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.2.
|
|
5
|
+
"version": "0.2.4",
|
|
6
6
|
"private": false,
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
@@ -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,74 +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 minimalDiff(
|
|
417
|
-
before: string,
|
|
418
|
-
after: string,
|
|
419
|
-
): {
|
|
420
|
-
from: number;
|
|
421
|
-
oldTo: number;
|
|
422
|
-
newTo: number;
|
|
423
|
-
} {
|
|
424
|
-
let from = 0;
|
|
425
|
-
while (
|
|
426
|
-
from < before.length &&
|
|
427
|
-
from < after.length &&
|
|
428
|
-
before[from] === after[from]
|
|
429
|
-
) {
|
|
430
|
-
from += 1;
|
|
431
|
-
}
|
|
432
|
-
let oldTo = before.length;
|
|
433
|
-
let newTo = after.length;
|
|
434
|
-
while (
|
|
435
|
-
oldTo > from &&
|
|
436
|
-
newTo > from &&
|
|
437
|
-
before[oldTo - 1] === after[newTo - 1]
|
|
438
|
-
) {
|
|
439
|
-
oldTo -= 1;
|
|
440
|
-
newTo -= 1;
|
|
441
|
-
}
|
|
442
|
-
return { from, oldTo, newTo };
|
|
443
|
-
}
|
|
444
|
-
|
|
445
183
|
function segmentText(segment: RawTextSegment): string {
|
|
446
184
|
return segment.text?.toString() ?? "";
|
|
447
185
|
}
|
|
@@ -621,7 +359,9 @@ function reconcileRawGroup(
|
|
|
621
359
|
return;
|
|
622
360
|
}
|
|
623
361
|
if (raw.kind === "link" && desired.kind === "link") {
|
|
624
|
-
raw.link.
|
|
362
|
+
if (raw.link.get("href") !== desired.href) {
|
|
363
|
+
raw.link.set("href", desired.href);
|
|
364
|
+
}
|
|
625
365
|
const content = raw.content ?? new Y.Array<unknown>();
|
|
626
366
|
if (!raw.content && desired.runs.some((run) => run.text.length > 0)) {
|
|
627
367
|
raw.link.set("content", content);
|
|
@@ -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 };
|
|
@@ -167,9 +160,13 @@ function projectedTextField(
|
|
|
167
160
|
return fail(`${reason}:embed`);
|
|
168
161
|
}
|
|
169
162
|
const item: ProjectedTextRun = { text: operation.insert };
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
if (operation.attributes !== undefined || styleValue !== undefined) {
|
|
164
|
+
const style = jsonStyle(
|
|
165
|
+
mergedTextStyle(legacyStyle, operation.attributes),
|
|
166
|
+
);
|
|
167
|
+
if (style) {
|
|
168
|
+
item.styles = style;
|
|
169
|
+
}
|
|
173
170
|
}
|
|
174
171
|
appendProjectedText(result, item);
|
|
175
172
|
}
|
|
@@ -347,13 +344,12 @@ export function fromYRichTextBlockPayload(
|
|
|
347
344
|
"inline_content:payload_json",
|
|
348
345
|
) as JsonValue;
|
|
349
346
|
}
|
|
350
|
-
const payload = Object.fromEntries(value.entries());
|
|
351
347
|
const projected = decodeOtherFields(
|
|
352
348
|
value,
|
|
353
349
|
"content",
|
|
354
350
|
"inline_content:payload_json",
|
|
355
351
|
);
|
|
356
|
-
const rawContent =
|
|
352
|
+
const rawContent = value.get("content");
|
|
357
353
|
if (rawContent === undefined) {
|
|
358
354
|
return projected as JsonValue;
|
|
359
355
|
}
|
|
@@ -366,10 +362,6 @@ export function fromYRichTextBlockPayload(
|
|
|
366
362
|
return projected as JsonValue;
|
|
367
363
|
}
|
|
368
364
|
|
|
369
|
-
export function richTextStyleKeys(): readonly RichTextStyleKey[] {
|
|
370
|
-
return RICH_TEXT_STYLE_KEYS;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
365
|
export function projectedStyledTextRuns(
|
|
374
366
|
value: unknown,
|
|
375
367
|
reason = "inline_content:styled_text",
|