@echovisionlab/geul-common 0.6.1 → 0.7.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 +56 -3
- package/src/collaboration/block-room-codec/inline-content-mutations.ts +57 -4
- package/src/collaboration/block-room-codec/inline-content-projection.ts +44 -3
- package/src/collaboration/block-room-codec/internal.ts +10 -0
- package/src/collaboration/block-room-codec/replay-inline-merge.ts +111 -2
- package/src/site-variables.ts +26 -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.7.0",
|
|
6
6
|
"private": false,
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
@@ -59,13 +59,14 @@
|
|
|
59
59
|
"./media/block-schemas": "./src/media/block-schemas.ts",
|
|
60
60
|
"./media/hydration": "./src/media/hydration.ts",
|
|
61
61
|
"./post": "./src/post/index.ts",
|
|
62
|
+
"./site-variables": "./src/site-variables.ts",
|
|
62
63
|
"./test/random-id": "./src/test/random-id.ts",
|
|
63
64
|
"./translation/release": "./src/translation/release.ts",
|
|
64
65
|
"./types": "./src/types.ts"
|
|
65
66
|
},
|
|
66
67
|
"dependencies": {
|
|
67
68
|
"@bufbuild/protobuf": "^2.16.0",
|
|
68
|
-
"@echovisionlab/geul-proto": "^0.
|
|
69
|
+
"@echovisionlab/geul-proto": "^0.7.0",
|
|
69
70
|
"diff": "9.0.0",
|
|
70
71
|
"zod": "^4.6.5"
|
|
71
72
|
},
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { JsonValue } from "@bufbuild/protobuf";
|
|
2
|
+
import {
|
|
3
|
+
isSiteVariableKey,
|
|
4
|
+
type SiteVariableKey,
|
|
5
|
+
} from "@echovisionlab/geul-proto/content/block_catalog.ts";
|
|
2
6
|
import { fail } from "./internal.ts";
|
|
3
7
|
|
|
4
8
|
export const RICH_TEXT_STYLE_KEYS = [
|
|
@@ -34,6 +38,13 @@ interface DesiredMathGroup {
|
|
|
34
38
|
readonly source: string;
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
interface DesiredSiteVariableGroup {
|
|
42
|
+
readonly kind: "siteVariable";
|
|
43
|
+
readonly key: SiteVariableKey;
|
|
44
|
+
readonly linked: boolean;
|
|
45
|
+
readonly styles: Style;
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
interface DesiredHardBreakGroup {
|
|
38
49
|
readonly kind: "hardBreak";
|
|
39
50
|
}
|
|
@@ -42,6 +53,7 @@ export type DesiredGroup =
|
|
|
42
53
|
| DesiredTextGroup
|
|
43
54
|
| DesiredLinkGroup
|
|
44
55
|
| DesiredMathGroup
|
|
56
|
+
| DesiredSiteVariableGroup
|
|
45
57
|
| DesiredHardBreakGroup;
|
|
46
58
|
|
|
47
59
|
function object(value: unknown, reason: string): Record<string, unknown> {
|
|
@@ -119,15 +131,47 @@ export function styledInput(value: unknown, reason: string): StyledRun {
|
|
|
119
131
|
};
|
|
120
132
|
}
|
|
121
133
|
|
|
134
|
+
export function siteVariableInput(
|
|
135
|
+
value: unknown,
|
|
136
|
+
reason: string,
|
|
137
|
+
): DesiredSiteVariableGroup {
|
|
138
|
+
const variable = object(value, reason);
|
|
139
|
+
if (typeof variable.key !== "string" || !isSiteVariableKey(variable.key)) {
|
|
140
|
+
return fail(`${reason}:key`);
|
|
141
|
+
}
|
|
142
|
+
if (variable.linked !== undefined && typeof variable.linked !== "boolean") {
|
|
143
|
+
return fail(`${reason}:linked`);
|
|
144
|
+
}
|
|
145
|
+
if (
|
|
146
|
+
variable.linked &&
|
|
147
|
+
(variable.key === "site.title" || variable.key === "site.companyName")
|
|
148
|
+
) {
|
|
149
|
+
return fail(`${reason}:linked_key`);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
kind: "siteVariable",
|
|
153
|
+
key: variable.key,
|
|
154
|
+
linked: variable.linked === true,
|
|
155
|
+
styles: checkedStyles(variable.styles, `${reason}:styles`),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
122
159
|
function inputItem(value: JsonValue, reason: string): DesiredGroup {
|
|
123
160
|
const item = object(value, reason);
|
|
124
|
-
const cases = [
|
|
125
|
-
|
|
126
|
-
|
|
161
|
+
const cases = [
|
|
162
|
+
"text",
|
|
163
|
+
"link",
|
|
164
|
+
"mathInline",
|
|
165
|
+
"hardBreak",
|
|
166
|
+
"siteVariable",
|
|
167
|
+
].filter((key) => item[key] !== undefined);
|
|
127
168
|
if (cases.length !== 1) {
|
|
128
169
|
return fail(`${reason}:oneof`);
|
|
129
170
|
}
|
|
130
171
|
const selected = cases[0]!;
|
|
172
|
+
if (selected === "siteVariable") {
|
|
173
|
+
return siteVariableInput(item.siteVariable, `${reason}:siteVariable`);
|
|
174
|
+
}
|
|
131
175
|
if (selected === "text") {
|
|
132
176
|
const run = styledInput(item.text, `${reason}:text`);
|
|
133
177
|
return { kind: "text", runs: run.text ? [run] : [] };
|
|
@@ -207,6 +251,15 @@ export function normalizeInlineContent(values: readonly JsonValue[]): string {
|
|
|
207
251
|
},
|
|
208
252
|
};
|
|
209
253
|
}
|
|
254
|
+
if (group.kind === "siteVariable") {
|
|
255
|
+
return {
|
|
256
|
+
siteVariable: {
|
|
257
|
+
key: group.key,
|
|
258
|
+
linked: group.linked,
|
|
259
|
+
styles: normalizeStyles(group.styles),
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
210
263
|
if (group.kind === "mathInline") {
|
|
211
264
|
return { mathInline: { source: group.source } };
|
|
212
265
|
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
projectedStyledTextRuns,
|
|
17
17
|
} from "./inline-content-projection.ts";
|
|
18
18
|
import {
|
|
19
|
+
RICH_TEXT_STYLE_KEYS,
|
|
19
20
|
changedStyleAttributes,
|
|
20
21
|
desiredGroups,
|
|
21
22
|
minimalDiff,
|
|
@@ -70,13 +71,25 @@ interface RawMathGroup {
|
|
|
70
71
|
readonly text: Y.Text;
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
interface RawSiteVariableGroup {
|
|
75
|
+
readonly kind: "siteVariable";
|
|
76
|
+
readonly rawStart: number;
|
|
77
|
+
readonly rawEnd: number;
|
|
78
|
+
readonly variable: Y.Map<unknown>;
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
interface RawHardBreakGroup {
|
|
74
82
|
readonly kind: "hardBreak";
|
|
75
83
|
readonly rawStart: number;
|
|
76
84
|
readonly rawEnd: number;
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
type RawGroup =
|
|
87
|
+
type RawGroup =
|
|
88
|
+
| RawTextGroup
|
|
89
|
+
| RawLinkGroup
|
|
90
|
+
| RawMathGroup
|
|
91
|
+
| RawHardBreakGroup
|
|
92
|
+
| RawSiteVariableGroup;
|
|
80
93
|
|
|
81
94
|
function yMap(value: unknown, reason: string): Y.Map<unknown> {
|
|
82
95
|
if (!(value instanceof Y.Map)) {
|
|
@@ -110,9 +123,21 @@ function rawInlineItem(
|
|
|
110
123
|
// reconcileArray first runs fromYInlineContent, which validates this map and
|
|
111
124
|
// its oneof before the raw handles are resolved.
|
|
112
125
|
const item = array.get(index) as Y.Map<unknown>;
|
|
113
|
-
const selected = [
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
const selected = [
|
|
127
|
+
"text",
|
|
128
|
+
"link",
|
|
129
|
+
"mathInline",
|
|
130
|
+
"hardBreak",
|
|
131
|
+
"siteVariable",
|
|
132
|
+
].find((key) => item.has(key))!;
|
|
133
|
+
if (selected === "siteVariable") {
|
|
134
|
+
return {
|
|
135
|
+
kind: "siteVariable",
|
|
136
|
+
rawStart: index,
|
|
137
|
+
rawEnd: index + 1,
|
|
138
|
+
variable: yMap(item.get("siteVariable"), `${reason}:siteVariable`),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
116
141
|
if (selected === "text") {
|
|
117
142
|
const segment = rawStyledText(item.get("text"), index, `${reason}:text`);
|
|
118
143
|
return {
|
|
@@ -376,6 +401,25 @@ function reconcileRawGroup(
|
|
|
376
401
|
);
|
|
377
402
|
return;
|
|
378
403
|
}
|
|
404
|
+
if (raw.kind === "siteVariable" && desired.kind === "siteVariable") {
|
|
405
|
+
if (raw.variable.get("key") !== desired.key)
|
|
406
|
+
raw.variable.set("key", desired.key);
|
|
407
|
+
if ((raw.variable.get("linked") === true) !== desired.linked)
|
|
408
|
+
raw.variable.set("linked", desired.linked);
|
|
409
|
+
let styles = raw.variable.get("styles") as Y.Map<unknown> | undefined;
|
|
410
|
+
if (!styles && Object.keys(desired.styles).length > 0) {
|
|
411
|
+
styles = new Y.Map<unknown>();
|
|
412
|
+
raw.variable.set("styles", styles);
|
|
413
|
+
}
|
|
414
|
+
if (styles)
|
|
415
|
+
for (const key of RICH_TEXT_STYLE_KEYS) {
|
|
416
|
+
const value = desired.styles[key];
|
|
417
|
+
if (value === undefined || value === false || value === "")
|
|
418
|
+
styles.delete(key);
|
|
419
|
+
else if (styles.get(key) !== value) styles.set(key, value);
|
|
420
|
+
}
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
379
423
|
if (raw.kind === "mathInline" && desired.kind === "mathInline") {
|
|
380
424
|
const diff = minimalDiff(raw.text.toString(), desired.source);
|
|
381
425
|
if (diff.oldTo > diff.from) {
|
|
@@ -402,6 +446,15 @@ function createRawValue(group: DesiredGroup): JsonValue {
|
|
|
402
446
|
},
|
|
403
447
|
};
|
|
404
448
|
}
|
|
449
|
+
if (group.kind === "siteVariable") {
|
|
450
|
+
return {
|
|
451
|
+
siteVariable: {
|
|
452
|
+
key: group.key,
|
|
453
|
+
linked: group.linked,
|
|
454
|
+
styles: group.styles,
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
405
458
|
if (group.kind === "mathInline") {
|
|
406
459
|
return { mathInline: { source: group.source } };
|
|
407
460
|
}
|
|
@@ -8,12 +8,14 @@ import * as Y from "yjs";
|
|
|
8
8
|
import {
|
|
9
9
|
fail,
|
|
10
10
|
fromYValue,
|
|
11
|
+
yMap,
|
|
11
12
|
type BlockRoomNodeFamily,
|
|
12
13
|
type JsonObject,
|
|
13
14
|
} from "./internal.ts";
|
|
14
15
|
|
|
15
16
|
import {
|
|
16
17
|
RICH_TEXT_STYLE_KEYS,
|
|
18
|
+
siteVariableInput,
|
|
17
19
|
type RichTextStyleKey,
|
|
18
20
|
} from "./inline-content-model.ts";
|
|
19
21
|
type RichTextStyleValue = boolean | string;
|
|
@@ -205,13 +207,52 @@ function projectedInlineItem(value: unknown, reason: string): JsonValue[] {
|
|
|
205
207
|
if (!(value instanceof Y.Map)) {
|
|
206
208
|
return fail(reason);
|
|
207
209
|
}
|
|
208
|
-
const cases = [
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
const cases = [
|
|
211
|
+
"text",
|
|
212
|
+
"link",
|
|
213
|
+
"mathInline",
|
|
214
|
+
"hardBreak",
|
|
215
|
+
"siteVariable",
|
|
216
|
+
].filter((key) => value.has(key));
|
|
211
217
|
if (cases.length !== 1) {
|
|
212
218
|
return fail(`${reason}:oneof`);
|
|
213
219
|
}
|
|
214
220
|
const selected = cases[0]!;
|
|
221
|
+
if (selected === "siteVariable") {
|
|
222
|
+
const raw = yMap(value.get("siteVariable"), `${reason}:siteVariable`);
|
|
223
|
+
const rawStyles = raw.get("styles");
|
|
224
|
+
const key = raw.get("key");
|
|
225
|
+
const linked = raw.get("linked");
|
|
226
|
+
const variable = siteVariableInput(
|
|
227
|
+
{
|
|
228
|
+
key,
|
|
229
|
+
// Independently valid key/link writes can merge into a forbidden pair.
|
|
230
|
+
// Normalize only that pair here; authored inputs stay strictly checked.
|
|
231
|
+
linked:
|
|
232
|
+
linked === true &&
|
|
233
|
+
(key === "site.title" || key === "site.companyName")
|
|
234
|
+
? false
|
|
235
|
+
: linked,
|
|
236
|
+
styles:
|
|
237
|
+
rawStyles === undefined
|
|
238
|
+
? undefined
|
|
239
|
+
: Object.fromEntries(
|
|
240
|
+
yMap(rawStyles, `${reason}:siteVariable:styles`).entries(),
|
|
241
|
+
),
|
|
242
|
+
},
|
|
243
|
+
`${reason}:siteVariable`,
|
|
244
|
+
);
|
|
245
|
+
const styles = jsonStyle(variable.styles);
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
siteVariable: {
|
|
249
|
+
key: variable.key,
|
|
250
|
+
...(variable.linked ? { linked: true } : {}),
|
|
251
|
+
...(styles ? { styles } : {}),
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
}
|
|
215
256
|
if (selected === "text") {
|
|
216
257
|
return projectedStyledText(value.get("text"), `${reason}:text`).map(
|
|
217
258
|
(styledText) => ({ text: styledText }),
|
|
@@ -170,6 +170,16 @@ export function toYValue(
|
|
|
170
170
|
}
|
|
171
171
|
if (value && typeof value === "object") {
|
|
172
172
|
const map = new Y.Map<unknown>();
|
|
173
|
+
if (
|
|
174
|
+
/^content(?:\.rows\[\d+\]\.cells\[\d+\]\.content)?\[\d+\]\.siteVariable$/.test(
|
|
175
|
+
path,
|
|
176
|
+
) &&
|
|
177
|
+
value.styles === undefined
|
|
178
|
+
) {
|
|
179
|
+
// Establish one shared map before replicas fork, even for unstyled atoms.
|
|
180
|
+
// Creating it during each client's first style edit loses one whole map.
|
|
181
|
+
map.set("styles", new Y.Map<unknown>());
|
|
182
|
+
}
|
|
173
183
|
for (const [key, child] of Object.entries(value).sort(([left], [right]) =>
|
|
174
184
|
left.localeCompare(right),
|
|
175
185
|
)) {
|
|
@@ -5,6 +5,7 @@ import { desiredGroups, type Style } from "./inline-content-model.ts";
|
|
|
5
5
|
type Attributes = Record<string, string | boolean>;
|
|
6
6
|
type Token =
|
|
7
7
|
| { kind: "text"; value: string; attributes: Attributes }
|
|
8
|
+
| { kind: "siteVariable"; attributes: Attributes }
|
|
8
9
|
| { kind: "mathInline"; attributes: Attributes }
|
|
9
10
|
| { kind: "hardBreak"; attributes: Attributes }
|
|
10
11
|
| { kind: "emptyLink"; attributes: Attributes }
|
|
@@ -117,11 +118,96 @@ function withAttributes(token: Token, attributes: Attributes): Token {
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
function insertionIdentity(token: Token): string {
|
|
121
|
+
if (token.kind === "siteVariable")
|
|
122
|
+
return `siteVariable:${token.attributes.key}`;
|
|
120
123
|
if (token.kind === "mathInline") return `math:${token.attributes.source}`;
|
|
121
124
|
if (token.kind === "emptyLink") return `emptyLink:${token.attributes.href}`;
|
|
122
125
|
return tokenIdentity(token);
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
/** Align surviving keys with text anchors; equal occurrence counts retain edits. */
|
|
129
|
+
function siteVariableAnchors(
|
|
130
|
+
before: readonly Token[],
|
|
131
|
+
after: readonly Token[],
|
|
132
|
+
): Map<number, Token> {
|
|
133
|
+
const base = before.filter((token) => token.kind === "siteVariable");
|
|
134
|
+
const variables = after.filter((token) => token.kind === "siteVariable");
|
|
135
|
+
if (base.length === variables.length)
|
|
136
|
+
return new Map(variables.map((token, index) => [index, token]));
|
|
137
|
+
// Filtering first discards the text separating equal keys and can identify
|
|
138
|
+
// a deleted occurrence as the survivor. Diff the complete inline sequence.
|
|
139
|
+
const retained = project(before, after, insertionIdentity).retained;
|
|
140
|
+
const anchors = new Map<number, Token>();
|
|
141
|
+
let occurrence = 0;
|
|
142
|
+
before.forEach((token, position) => {
|
|
143
|
+
if (token.kind !== "siteVariable") return;
|
|
144
|
+
const survivor = retained.get(position);
|
|
145
|
+
if (survivor) anchors.set(occurrence, survivor);
|
|
146
|
+
occurrence++;
|
|
147
|
+
});
|
|
148
|
+
return anchors;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function occurrenceIdentity(
|
|
152
|
+
before: readonly Token[],
|
|
153
|
+
local: readonly Token[],
|
|
154
|
+
peer: readonly Token[],
|
|
155
|
+
): (token: Token) => string {
|
|
156
|
+
const variables = (tokens: readonly Token[]) =>
|
|
157
|
+
tokens.filter((token) => token.kind === "siteVariable");
|
|
158
|
+
const base = variables(before);
|
|
159
|
+
const localVariables = variables(local);
|
|
160
|
+
const peerVariables = variables(peer);
|
|
161
|
+
const localAnchors = siteVariableAnchors(before, local);
|
|
162
|
+
const peerAnchors = siteVariableAnchors(before, peer);
|
|
163
|
+
const identities = new Map<Token, string>();
|
|
164
|
+
base.forEach((token, index) =>
|
|
165
|
+
identities.set(token, `siteVariable:${index}`),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const align = (
|
|
169
|
+
tokens: readonly Token[],
|
|
170
|
+
anchors: Map<number, Token>,
|
|
171
|
+
otherAnchors: Map<number, Token>,
|
|
172
|
+
) => {
|
|
173
|
+
const tokenPositions = new Map(
|
|
174
|
+
tokens.map((token, index) => [token, index]),
|
|
175
|
+
);
|
|
176
|
+
let baseStart = 0;
|
|
177
|
+
let start = 0;
|
|
178
|
+
for (const [position, token] of [
|
|
179
|
+
...anchors,
|
|
180
|
+
[base.length, undefined] as const,
|
|
181
|
+
]) {
|
|
182
|
+
const end =
|
|
183
|
+
token === undefined ? tokens.length : tokenPositions.get(token)!;
|
|
184
|
+
// If this deletion was already replayed, the other side's retained
|
|
185
|
+
// occurrences also anchor peer keys that have since changed.
|
|
186
|
+
const candidates = Array.from(
|
|
187
|
+
{ length: position - baseStart },
|
|
188
|
+
(_, index) => baseStart + index,
|
|
189
|
+
);
|
|
190
|
+
const hinted = candidates.filter((index) => otherAnchors.has(index));
|
|
191
|
+
const positions = hinted.length === end - start ? hinted : candidates;
|
|
192
|
+
for (let index = start; index < end; index++) {
|
|
193
|
+
const baseIndex = positions[index - start];
|
|
194
|
+
if (baseIndex !== undefined)
|
|
195
|
+
identities.set(tokens[index]!, `siteVariable:${baseIndex}`);
|
|
196
|
+
}
|
|
197
|
+
if (token !== undefined)
|
|
198
|
+
identities.set(token, `siteVariable:${position}`);
|
|
199
|
+
baseStart = position + 1;
|
|
200
|
+
start = end + 1;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
align(localVariables, localAnchors, peerAnchors);
|
|
204
|
+
align(peerVariables, peerAnchors, localAnchors);
|
|
205
|
+
return (token) =>
|
|
206
|
+
token.kind === "siteVariable"
|
|
207
|
+
? (identities.get(token) ?? insertionIdentity(token))
|
|
208
|
+
: tokenIdentity(token);
|
|
209
|
+
}
|
|
210
|
+
|
|
125
211
|
/** Preserve each newly inserted adjacent link as a semantic group. */
|
|
126
212
|
function linkedInsertionGroups(tokens: readonly Token[]): Token[][] | null {
|
|
127
213
|
if (!tokens.some((token) => token.kind === "linkBoundary")) return null;
|
|
@@ -211,8 +297,9 @@ function mergeTokens(
|
|
|
211
297
|
local: readonly Token[],
|
|
212
298
|
peer: readonly Token[],
|
|
213
299
|
): Token[] {
|
|
214
|
-
const
|
|
215
|
-
const
|
|
300
|
+
const identity = occurrenceIdentity(before, local, peer);
|
|
301
|
+
const localProjection = project(before, local, identity);
|
|
302
|
+
const peerProjection = project(before, peer, identity);
|
|
216
303
|
const result: Token[] = [];
|
|
217
304
|
for (let position = 0; position <= before.length; position++) {
|
|
218
305
|
for (const token of mergeInsertions(
|
|
@@ -266,6 +353,15 @@ function inlineTokens(content: readonly JsonValue[]): Token[] {
|
|
|
266
353
|
for (const value of run.text)
|
|
267
354
|
tokens.push({ kind: "text", value, attributes });
|
|
268
355
|
}
|
|
356
|
+
} else if (group.kind === "siteVariable") {
|
|
357
|
+
tokens.push({
|
|
358
|
+
kind: "siteVariable",
|
|
359
|
+
attributes: {
|
|
360
|
+
...normalizedStyles(group.styles),
|
|
361
|
+
key: group.key,
|
|
362
|
+
linked: group.linked,
|
|
363
|
+
},
|
|
364
|
+
});
|
|
269
365
|
} else if (group.kind === "mathInline") {
|
|
270
366
|
tokens.push({ kind: "mathInline", attributes: { source: group.source } });
|
|
271
367
|
} else {
|
|
@@ -283,6 +379,19 @@ function inlineContent(tokens: readonly Token[]): JsonValue[] {
|
|
|
283
379
|
if (token.kind !== "text") {
|
|
284
380
|
runs = undefined;
|
|
285
381
|
if (token.kind === "linkBoundary") continue;
|
|
382
|
+
if (token.kind === "siteVariable") {
|
|
383
|
+
const { key, linked, ...styles } = token.attributes;
|
|
384
|
+
result.push({
|
|
385
|
+
siteVariable: {
|
|
386
|
+
key,
|
|
387
|
+
...(linked && key !== "site.title" && key !== "site.companyName"
|
|
388
|
+
? { linked: true }
|
|
389
|
+
: {}),
|
|
390
|
+
...(Object.keys(styles).length ? { styles } : {}),
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
286
395
|
result.push(
|
|
287
396
|
token.kind === "emptyLink"
|
|
288
397
|
? { link: { href: token.attributes.href as string, content: [] } }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SiteVariableKey } from "@echovisionlab/geul-proto/content/block_catalog.ts";
|
|
2
|
+
|
|
3
|
+
export interface SiteVariableContext {
|
|
4
|
+
readonly siteTitle: string;
|
|
5
|
+
readonly siteUrl: string;
|
|
6
|
+
readonly companyName: string;
|
|
7
|
+
readonly legalEmail: string;
|
|
8
|
+
readonly supportEmail: string;
|
|
9
|
+
readonly privacyEmail: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const contextFields = {
|
|
13
|
+
"site.title": "siteTitle",
|
|
14
|
+
"site.url": "siteUrl",
|
|
15
|
+
"site.companyName": "companyName",
|
|
16
|
+
"contact.legalEmail": "legalEmail",
|
|
17
|
+
"contact.supportEmail": "supportEmail",
|
|
18
|
+
"contact.privacyEmail": "privacyEmail",
|
|
19
|
+
} as const satisfies Record<SiteVariableKey, keyof SiteVariableContext>;
|
|
20
|
+
|
|
21
|
+
export function resolveSiteVariable(
|
|
22
|
+
key: SiteVariableKey,
|
|
23
|
+
context: SiteVariableContext,
|
|
24
|
+
): string {
|
|
25
|
+
return context[contextFields[key]];
|
|
26
|
+
}
|