openbird 1.0.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.
Potentially problematic release.
This version of openbird might be problematic. Click here for more details.
- package/.env.example +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- package/.github/ISSUE_TEMPLATE/custom.md +10 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/dependabot.yml +13 -0
- package/.github/workflows/publish.yml +23 -0
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/bin/openbird.js +26 -0
- package/docs/plans/2026-02-12-openbird-v1-design.md +281 -0
- package/examples/basic-usage.mjs +117 -0
- package/package.json +38 -0
- package/src/core/api.js +1182 -0
- package/src/core/auth.js +39 -0
- package/src/core/builders/header.js +560 -0
- package/src/core/builders/params.js +96 -0
- package/src/core/builders/proto.js +4432 -0
- package/src/core/builders/richtext.js +592 -0
- package/src/core/generated/proto.js +19041 -0
- package/src/core/generated/proto_pb.js +16469 -0
- package/src/core/proto/proto.proto +949 -0
- package/src/core/proto/proto_pb.d.ts +4383 -0
- package/src/core/proto/proto_pb.js +785 -0
- package/src/core/utils/cookie.js +58 -0
- package/src/core/utils/encryption.js +216 -0
- package/src/core/utils/time.js +79 -0
- package/src/core/utils/varint.js +31 -0
- package/src/core/websocket.js +311 -0
- package/src/index.js +68 -0
- package/src/logger.js +28 -0
- package/src/mcp/server.js +234 -0
- package/src/webhook/dispatcher.js +42 -0
- package/src/webhook/normalizer.js +86 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RichText Builder for Feishu messages
|
|
3
|
+
* Converts Markdown-like syntax to Feishu RichText protobuf structure
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { create, toBinary } from '@bufbuild/protobuf';
|
|
7
|
+
import { TextPropertySchema } from '../proto/proto_pb.js';
|
|
8
|
+
import { pushVarintLength } from '../utils/varint.js';
|
|
9
|
+
|
|
10
|
+
// RichText Element Tags
|
|
11
|
+
const Tags = {
|
|
12
|
+
TEXT: 1,
|
|
13
|
+
P: 3,
|
|
14
|
+
A: 6,
|
|
15
|
+
DOCS: 22,
|
|
16
|
+
UL: 26,
|
|
17
|
+
OL: 27,
|
|
18
|
+
LI: 28,
|
|
19
|
+
QUOTE: 29,
|
|
20
|
+
CODE_BLOCK: 31,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Style keys for text formatting
|
|
24
|
+
const Styles = {
|
|
25
|
+
ITALIC: { fontStyle: 'italic' },
|
|
26
|
+
BOLD: { fontWeight: 'bold' },
|
|
27
|
+
STRIKETHROUGH: { '-lark-textDecoration': 'lineThrough' },
|
|
28
|
+
UNDERLINE: { '-lark-textDecoration': 'underline' },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Simple ID generator
|
|
32
|
+
let idCounter = 0;
|
|
33
|
+
function nextId() {
|
|
34
|
+
return String(++idCounter);
|
|
35
|
+
}
|
|
36
|
+
function resetIdCounter() {
|
|
37
|
+
idCounter = 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Manually build RichTextElement bytes
|
|
42
|
+
* Format: tag(varint) + property(bytes, can be empty 1a00) + childIds(repeated string)
|
|
43
|
+
*/
|
|
44
|
+
function buildElementBytes(tag, property = null, childIds = []) {
|
|
45
|
+
const parts = [];
|
|
46
|
+
|
|
47
|
+
// Field 1: tag (varint)
|
|
48
|
+
parts.push(0x08); // field 1, wire type 0
|
|
49
|
+
parts.push(tag);
|
|
50
|
+
|
|
51
|
+
// Field 3: property (bytes) - MUST include even if empty
|
|
52
|
+
parts.push(0x1a); // field 3, wire type 2
|
|
53
|
+
if (property && property.length > 0) {
|
|
54
|
+
pushVarintLength(parts, property.length);
|
|
55
|
+
parts.push(...property);
|
|
56
|
+
} else {
|
|
57
|
+
parts.push(0x00); // empty bytes
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Field 4: childIds (repeated string)
|
|
61
|
+
for (const childId of childIds) {
|
|
62
|
+
parts.push(0x22); // field 4, wire type 2
|
|
63
|
+
const idBytes = new TextEncoder().encode(childId);
|
|
64
|
+
pushVarintLength(parts, idBytes.length);
|
|
65
|
+
parts.push(...idBytes);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return new Uint8Array(parts);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build dictionary entry bytes
|
|
73
|
+
*/
|
|
74
|
+
function buildDictEntry(key, elementBytes) {
|
|
75
|
+
const keyBytes = new TextEncoder().encode(key);
|
|
76
|
+
const entryParts = [];
|
|
77
|
+
|
|
78
|
+
// Field 1: key (string)
|
|
79
|
+
entryParts.push(0x0a);
|
|
80
|
+
pushVarintLength(entryParts, keyBytes.length);
|
|
81
|
+
entryParts.push(...keyBytes);
|
|
82
|
+
|
|
83
|
+
// Field 2: value (element bytes)
|
|
84
|
+
entryParts.push(0x12);
|
|
85
|
+
pushVarintLength(entryParts, elementBytes.length);
|
|
86
|
+
entryParts.push(...elementBytes);
|
|
87
|
+
|
|
88
|
+
return new Uint8Array(entryParts);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build complete dictionary bytes from entries
|
|
93
|
+
*/
|
|
94
|
+
function buildDictionaryBytes(entries) {
|
|
95
|
+
const parts = [];
|
|
96
|
+
for (const [key, elementBytes] of Object.entries(entries)) {
|
|
97
|
+
const entryBytes = buildDictEntry(key, elementBytes);
|
|
98
|
+
// Map entry wrapper
|
|
99
|
+
parts.push(0x0a); // field 1 (map), wire type 2
|
|
100
|
+
pushVarintLength(parts, entryBytes.length);
|
|
101
|
+
parts.push(...entryBytes);
|
|
102
|
+
}
|
|
103
|
+
return new Uint8Array(parts);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build TEXT element with property
|
|
108
|
+
*/
|
|
109
|
+
function buildTextElement(text, styles = {}) {
|
|
110
|
+
const textProperty = create(TextPropertySchema, { content: text });
|
|
111
|
+
const propertyBytes = toBinary(TextPropertySchema, textProperty);
|
|
112
|
+
|
|
113
|
+
// If has styles, need to add style field
|
|
114
|
+
if (Object.keys(styles).length > 0) {
|
|
115
|
+
return buildElementBytesWithStyle(Tags.TEXT, propertyBytes, [], styles);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return buildElementBytes(Tags.TEXT, propertyBytes, []);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build element with style
|
|
123
|
+
*/
|
|
124
|
+
function buildElementBytesWithStyle(tag, property, childIds, styles) {
|
|
125
|
+
const parts = [];
|
|
126
|
+
|
|
127
|
+
// Field 1: tag
|
|
128
|
+
parts.push(0x08);
|
|
129
|
+
parts.push(tag);
|
|
130
|
+
|
|
131
|
+
// Field 2: style (map<string, string>)
|
|
132
|
+
for (const [key, value] of Object.entries(styles)) {
|
|
133
|
+
const keyBytes = new TextEncoder().encode(key);
|
|
134
|
+
const valueBytes = new TextEncoder().encode(value);
|
|
135
|
+
|
|
136
|
+
// Style entry
|
|
137
|
+
const styleEntry = [];
|
|
138
|
+
styleEntry.push(0x0a); // key field
|
|
139
|
+
pushVarintLength(styleEntry, keyBytes.length);
|
|
140
|
+
styleEntry.push(...keyBytes);
|
|
141
|
+
styleEntry.push(0x12); // value field
|
|
142
|
+
pushVarintLength(styleEntry, valueBytes.length);
|
|
143
|
+
styleEntry.push(...valueBytes);
|
|
144
|
+
|
|
145
|
+
parts.push(0x12); // field 2, wire type 2
|
|
146
|
+
pushVarintLength(parts, styleEntry.length);
|
|
147
|
+
parts.push(...styleEntry);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Field 3: property
|
|
151
|
+
parts.push(0x1a);
|
|
152
|
+
if (property && property.length > 0) {
|
|
153
|
+
pushVarintLength(parts, property.length);
|
|
154
|
+
parts.push(...property);
|
|
155
|
+
} else {
|
|
156
|
+
parts.push(0x00);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Field 4: childIds
|
|
160
|
+
for (const childId of childIds) {
|
|
161
|
+
parts.push(0x22);
|
|
162
|
+
const idBytes = new TextEncoder().encode(childId);
|
|
163
|
+
pushVarintLength(parts, idBytes.length);
|
|
164
|
+
parts.push(...idBytes);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return new Uint8Array(parts);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Encode link property
|
|
172
|
+
*/
|
|
173
|
+
function encodeLinkProperty(url, text) {
|
|
174
|
+
const urlBytes = new TextEncoder().encode(url);
|
|
175
|
+
const textBytes = new TextEncoder().encode(text);
|
|
176
|
+
const buffer = [];
|
|
177
|
+
buffer.push(0x0a);
|
|
178
|
+
pushVarintLength(buffer, urlBytes.length);
|
|
179
|
+
buffer.push(...urlBytes);
|
|
180
|
+
buffer.push(0x12);
|
|
181
|
+
pushVarintLength(buffer, textBytes.length);
|
|
182
|
+
buffer.push(...textBytes);
|
|
183
|
+
buffer.push(0x48, 0x01); // field 9 = 1
|
|
184
|
+
buffer.push(0x50, 0x03); // field 10 = 3
|
|
185
|
+
return new Uint8Array(buffer);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Encode UL property
|
|
190
|
+
*/
|
|
191
|
+
function encodeULProperty() {
|
|
192
|
+
return new Uint8Array([0x08, 0x00]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Encode OL property with start index
|
|
197
|
+
* @param {number} startIndex - The starting number for the ordered list (default: 1)
|
|
198
|
+
*/
|
|
199
|
+
function encodeOLProperty(startIndex = 1) {
|
|
200
|
+
// Ensure startIndex is at least 1
|
|
201
|
+
const index = Math.max(1, startIndex);
|
|
202
|
+
|
|
203
|
+
// For numbers <= 127, single byte encoding works
|
|
204
|
+
if (index <= 127) {
|
|
205
|
+
return new Uint8Array([0x08, 0x00, 0x10, index]);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// For larger numbers, use varint encoding for field 2
|
|
209
|
+
const bytes = [0x08, 0x00, 0x10];
|
|
210
|
+
let value = index;
|
|
211
|
+
while (value > 127) {
|
|
212
|
+
bytes.push((value & 0x7f) | 0x80);
|
|
213
|
+
value >>= 7;
|
|
214
|
+
}
|
|
215
|
+
bytes.push(value);
|
|
216
|
+
return new Uint8Array(bytes);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Unwrap inline Markdown style markers when they wrap the whole text
|
|
221
|
+
* Supports nested wrappers like **__text__**
|
|
222
|
+
*/
|
|
223
|
+
function unwrapInlineStyles(text) {
|
|
224
|
+
let content = text;
|
|
225
|
+
let style = {};
|
|
226
|
+
let changed = true;
|
|
227
|
+
|
|
228
|
+
while (changed && content) {
|
|
229
|
+
changed = false;
|
|
230
|
+
let match;
|
|
231
|
+
|
|
232
|
+
match = content.match(/^\*\*(.+)\*\*$/);
|
|
233
|
+
if (match) {
|
|
234
|
+
style = { ...style, ...Styles.BOLD };
|
|
235
|
+
content = match[1];
|
|
236
|
+
changed = true;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
match = content.match(/^__(.+)__$/);
|
|
241
|
+
if (match) {
|
|
242
|
+
style = { ...style, ...Styles.UNDERLINE };
|
|
243
|
+
content = match[1];
|
|
244
|
+
changed = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
match = content.match(/^~~(.+)~~$/);
|
|
249
|
+
if (match) {
|
|
250
|
+
style = { ...style, ...Styles.STRIKETHROUGH };
|
|
251
|
+
content = match[1];
|
|
252
|
+
changed = true;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
match = content.match(/^\*(.+)\*$/) || content.match(/^_(.+)_$/);
|
|
257
|
+
if (match) {
|
|
258
|
+
style = { ...style, ...Styles.ITALIC };
|
|
259
|
+
content = match[1];
|
|
260
|
+
changed = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { content, style };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Parse inline formatting
|
|
269
|
+
*/
|
|
270
|
+
function parseInlineFormatting(text) {
|
|
271
|
+
const elements = [];
|
|
272
|
+
const elementBytes = {};
|
|
273
|
+
let innerText = '';
|
|
274
|
+
|
|
275
|
+
const combinedRegex = /(\*\*\[([^\]]+)\]\(([^)]+)\)\*\*)|(\[\*\*([^\]]+)\*\*\]\(([^)]+)\))|(\*\*(.+?)\*\*)|(~~(.+?)~~)|(__(.+?)__)|(\*(.+?)\*)|(_([^_]+)_)|(\[([^\]]+)\]\(([^)]+)\))/g;
|
|
276
|
+
|
|
277
|
+
let match;
|
|
278
|
+
const matches = [];
|
|
279
|
+
|
|
280
|
+
while ((match = combinedRegex.exec(text)) !== null) {
|
|
281
|
+
let type = 'unknown';
|
|
282
|
+
let content = '';
|
|
283
|
+
let url = null;
|
|
284
|
+
let style = {};
|
|
285
|
+
|
|
286
|
+
if (match[1]) {
|
|
287
|
+
type = 'link';
|
|
288
|
+
content = match[2];
|
|
289
|
+
url = match[3];
|
|
290
|
+
style = { ...Styles.BOLD };
|
|
291
|
+
} else if (match[4]) {
|
|
292
|
+
type = 'link';
|
|
293
|
+
content = match[5];
|
|
294
|
+
url = match[6];
|
|
295
|
+
style = { ...Styles.BOLD };
|
|
296
|
+
} else if (match[7]) {
|
|
297
|
+
type = 'bold';
|
|
298
|
+
content = match[8];
|
|
299
|
+
} else if (match[9]) {
|
|
300
|
+
type = 'strikethrough';
|
|
301
|
+
content = match[10];
|
|
302
|
+
} else if (match[11]) {
|
|
303
|
+
type = 'underline';
|
|
304
|
+
content = match[12];
|
|
305
|
+
} else if (match[13]) {
|
|
306
|
+
type = 'italic';
|
|
307
|
+
content = match[14];
|
|
308
|
+
} else if (match[15]) {
|
|
309
|
+
type = 'italic';
|
|
310
|
+
content = match[16];
|
|
311
|
+
} else if (match[17]) {
|
|
312
|
+
type = 'link';
|
|
313
|
+
content = match[18];
|
|
314
|
+
url = match[19];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
matches.push({
|
|
318
|
+
index: match.index,
|
|
319
|
+
length: match[0].length,
|
|
320
|
+
type,
|
|
321
|
+
content,
|
|
322
|
+
url,
|
|
323
|
+
style,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
matches.sort((a, b) => a.index - b.index);
|
|
328
|
+
|
|
329
|
+
let currentIndex = 0;
|
|
330
|
+
|
|
331
|
+
for (const m of matches) {
|
|
332
|
+
if (m.index > currentIndex) {
|
|
333
|
+
const plainText = text.substring(currentIndex, m.index);
|
|
334
|
+
if (plainText) {
|
|
335
|
+
const id = nextId();
|
|
336
|
+
elements.push(id);
|
|
337
|
+
elementBytes[id] = buildTextElement(plainText);
|
|
338
|
+
innerText += plainText;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const id = nextId();
|
|
343
|
+
elements.push(id);
|
|
344
|
+
|
|
345
|
+
if (m.type === 'link') {
|
|
346
|
+
const normalized = unwrapInlineStyles(m.content);
|
|
347
|
+
const linkContent = normalized.content;
|
|
348
|
+
const linkStyle = { ...m.style, ...normalized.style };
|
|
349
|
+
const linkProp = encodeLinkProperty(m.url, linkContent);
|
|
350
|
+
|
|
351
|
+
if (Object.keys(linkStyle).length > 0) {
|
|
352
|
+
elementBytes[id] = buildElementBytesWithStyle(Tags.A, linkProp, [], linkStyle);
|
|
353
|
+
} else {
|
|
354
|
+
elementBytes[id] = buildElementBytes(Tags.A, linkProp, []);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
innerText += linkContent;
|
|
358
|
+
} else {
|
|
359
|
+
let style = {};
|
|
360
|
+
switch (m.type) {
|
|
361
|
+
case 'bold': style = Styles.BOLD; break;
|
|
362
|
+
case 'italic': style = Styles.ITALIC; break;
|
|
363
|
+
case 'strikethrough': style = Styles.STRIKETHROUGH; break;
|
|
364
|
+
case 'underline': style = Styles.UNDERLINE; break;
|
|
365
|
+
}
|
|
366
|
+
elementBytes[id] = buildTextElement(m.content, style);
|
|
367
|
+
innerText += m.content;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
currentIndex = m.index + m.length;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (currentIndex < text.length) {
|
|
374
|
+
const plainText = text.substring(currentIndex);
|
|
375
|
+
if (plainText) {
|
|
376
|
+
const id = nextId();
|
|
377
|
+
elements.push(id);
|
|
378
|
+
elementBytes[id] = buildTextElement(plainText);
|
|
379
|
+
innerText += plainText;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (elements.length === 0) {
|
|
384
|
+
const id = nextId();
|
|
385
|
+
elements.push(id);
|
|
386
|
+
elementBytes[id] = buildTextElement(text);
|
|
387
|
+
innerText = text;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return { elementIds: elements, elementBytes, innerText };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Parse a single line and return its block structure
|
|
395
|
+
* IDs are assigned depth-first (outside-in per block)
|
|
396
|
+
* @param {string} line - Single line of text
|
|
397
|
+
* @returns {Object} { blockChildId, elementBytes, innerText }
|
|
398
|
+
*/
|
|
399
|
+
function parseSingleLine(line) {
|
|
400
|
+
const elementBytes = {};
|
|
401
|
+
let innerText = '';
|
|
402
|
+
|
|
403
|
+
// Check for quote
|
|
404
|
+
if (line.startsWith('> ')) {
|
|
405
|
+
const content = line.substring(2);
|
|
406
|
+
|
|
407
|
+
// Parse inline formatting within quote content
|
|
408
|
+
const inline = parseInlineFormatting(content);
|
|
409
|
+
Object.assign(elementBytes, inline.elementBytes);
|
|
410
|
+
innerText = inline.innerText;
|
|
411
|
+
|
|
412
|
+
// Structure: QUOTE → P → [inline elements]
|
|
413
|
+
const quoteId = nextId();
|
|
414
|
+
const pId = nextId();
|
|
415
|
+
|
|
416
|
+
elementBytes[pId] = buildElementBytes(Tags.P, null, inline.elementIds);
|
|
417
|
+
elementBytes[quoteId] = buildElementBytes(Tags.QUOTE, null, [pId]);
|
|
418
|
+
|
|
419
|
+
return { blockChildId: quoteId, elementBytes, innerText };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Check for unordered list
|
|
423
|
+
if (/^[-*] /.test(line)) {
|
|
424
|
+
const content = line.substring(2);
|
|
425
|
+
|
|
426
|
+
// Parse inline formatting within list item
|
|
427
|
+
const inline = parseInlineFormatting(content);
|
|
428
|
+
Object.assign(elementBytes, inline.elementBytes);
|
|
429
|
+
innerText = inline.innerText;
|
|
430
|
+
|
|
431
|
+
// Structure: UL → LI → [inline elements]
|
|
432
|
+
const ulId = nextId();
|
|
433
|
+
const liId = nextId();
|
|
434
|
+
|
|
435
|
+
elementBytes[liId] = buildElementBytes(Tags.LI, null, inline.elementIds);
|
|
436
|
+
elementBytes[ulId] = buildElementBytes(Tags.UL, encodeULProperty(), [liId]);
|
|
437
|
+
|
|
438
|
+
return { blockChildId: ulId, elementBytes, innerText };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Check for ordered list
|
|
442
|
+
const olMatch = line.match(/^(\d+)\. /);
|
|
443
|
+
if (olMatch) {
|
|
444
|
+
const startIndex = parseInt(olMatch[1], 10);
|
|
445
|
+
const content = line.replace(/^\d+\. /, '');
|
|
446
|
+
|
|
447
|
+
// Parse inline formatting within list item
|
|
448
|
+
const inline = parseInlineFormatting(content);
|
|
449
|
+
Object.assign(elementBytes, inline.elementBytes);
|
|
450
|
+
innerText = inline.innerText;
|
|
451
|
+
|
|
452
|
+
// Structure: OL → LI → [inline elements]
|
|
453
|
+
const olId = nextId();
|
|
454
|
+
const liId = nextId();
|
|
455
|
+
|
|
456
|
+
elementBytes[liId] = buildElementBytes(Tags.LI, null, inline.elementIds);
|
|
457
|
+
elementBytes[olId] = buildElementBytes(Tags.OL, encodeOLProperty(startIndex), [liId]);
|
|
458
|
+
|
|
459
|
+
return { blockChildId: olId, elementBytes, innerText };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Plain text line - may contain inline formatting (bold, italic, etc.)
|
|
463
|
+
// Parse inline formatting and wrap in P element
|
|
464
|
+
const inline = parseInlineFormatting(line);
|
|
465
|
+
Object.assign(elementBytes, inline.elementBytes);
|
|
466
|
+
innerText = inline.innerText;
|
|
467
|
+
|
|
468
|
+
// Wrap inline elements in a P element
|
|
469
|
+
const pId = nextId();
|
|
470
|
+
elementBytes[pId] = buildElementBytes(Tags.P, null, inline.elementIds);
|
|
471
|
+
|
|
472
|
+
return { blockChildId: pId, elementBytes, innerText };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Build RichText structure from Markdown text
|
|
477
|
+
* Supports multi-line mixed format (plain text + quotes + lists)
|
|
478
|
+
*/
|
|
479
|
+
export function buildRichText(markdown) {
|
|
480
|
+
resetIdCounter();
|
|
481
|
+
const elementBytes = {};
|
|
482
|
+
let innerText = '';
|
|
483
|
+
|
|
484
|
+
// Check for code block (special case - takes entire content)
|
|
485
|
+
const codeBlockMatch = markdown.match(/^```(\w*)\n?([\s\S]*?)```$/);
|
|
486
|
+
if (codeBlockMatch) {
|
|
487
|
+
const language = codeBlockMatch[1] || 'java';
|
|
488
|
+
const code = codeBlockMatch[2].trim();
|
|
489
|
+
innerText = `[代码块]`;
|
|
490
|
+
|
|
491
|
+
// Build from outside to inside, IDs from 1 to N
|
|
492
|
+
// Structure: DOCS(1) → P(2) → CODE_BLOCK(3)
|
|
493
|
+
const docsId = nextId(); // "1"
|
|
494
|
+
const pId = nextId(); // "2"
|
|
495
|
+
const codeId = nextId(); // "3"
|
|
496
|
+
|
|
497
|
+
// CODE_BLOCK with language property
|
|
498
|
+
const langBytes = new TextEncoder().encode(language);
|
|
499
|
+
const codeBlockProp = new Uint8Array([
|
|
500
|
+
0x08, 0x1f, // field 1 = 31
|
|
501
|
+
0x12, langBytes.length + 2, 0x12, langBytes.length, ...langBytes
|
|
502
|
+
]);
|
|
503
|
+
elementBytes[codeId] = buildElementBytes(Tags.CODE_BLOCK, codeBlockProp, []);
|
|
504
|
+
|
|
505
|
+
// P → CODE_BLOCK
|
|
506
|
+
elementBytes[pId] = buildElementBytes(Tags.P, null, [codeId]);
|
|
507
|
+
|
|
508
|
+
// DOCS → P
|
|
509
|
+
elementBytes[docsId] = buildElementBytes(Tags.DOCS, null, [pId]);
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
elementIds: [docsId],
|
|
513
|
+
innerText,
|
|
514
|
+
dictionaryBytes: buildDictionaryBytes(elementBytes),
|
|
515
|
+
isBlockLevel: true,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Split into lines and filter empty lines
|
|
520
|
+
const lines = markdown.split('\n').filter(line => line.trim());
|
|
521
|
+
|
|
522
|
+
// Check if this is multi-line or has block-level elements
|
|
523
|
+
const hasMultipleLines = lines.length > 1;
|
|
524
|
+
const hasBlockElement = lines.some(line =>
|
|
525
|
+
line.startsWith('> ') ||
|
|
526
|
+
/^[-*] /.test(line) ||
|
|
527
|
+
/^\d+\. /.test(line)
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
// If single line without block elements, use simple inline formatting
|
|
531
|
+
if (!hasMultipleLines && !hasBlockElement) {
|
|
532
|
+
const inline = parseInlineFormatting(markdown);
|
|
533
|
+
Object.assign(elementBytes, inline.elementBytes);
|
|
534
|
+
innerText = inline.innerText;
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
elementIds: inline.elementIds,
|
|
538
|
+
innerText,
|
|
539
|
+
dictionaryBytes: buildDictionaryBytes(elementBytes),
|
|
540
|
+
isBlockLevel: false,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Multi-line or has block elements: build DOCS structure
|
|
545
|
+
// IDs assigned depth-first: DOCS first, then each block fully (outside-in)
|
|
546
|
+
const docsId = nextId(); // "1"
|
|
547
|
+
const blockChildIds = [];
|
|
548
|
+
const innerTexts = [];
|
|
549
|
+
|
|
550
|
+
for (const line of lines) {
|
|
551
|
+
const parsed = parseSingleLine(line);
|
|
552
|
+
blockChildIds.push(parsed.blockChildId);
|
|
553
|
+
Object.assign(elementBytes, parsed.elementBytes);
|
|
554
|
+
innerTexts.push(parsed.innerText);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Build DOCS with all block children
|
|
558
|
+
elementBytes[docsId] = buildElementBytes(Tags.DOCS, null, blockChildIds);
|
|
559
|
+
innerText = innerTexts.join('');
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
elementIds: [docsId],
|
|
563
|
+
innerText,
|
|
564
|
+
dictionaryBytes: buildDictionaryBytes(elementBytes),
|
|
565
|
+
isBlockLevel: true,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Check if text contains any Markdown formatting
|
|
571
|
+
*/
|
|
572
|
+
export function hasMarkdownFormatting(text) {
|
|
573
|
+
// Multi-line text is always treated as rich text
|
|
574
|
+
if (text.includes('\n')) {
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
const patterns = [
|
|
578
|
+
/\*\*.+?\*\*/,
|
|
579
|
+
/\*.+?\*/,
|
|
580
|
+
/_.+?_/,
|
|
581
|
+
/~~.+?~~/,
|
|
582
|
+
/__.+?__/,
|
|
583
|
+
/\[.+?\]\(.+?\)/,
|
|
584
|
+
/^> /m,
|
|
585
|
+
/^[-*] /m,
|
|
586
|
+
/^\d+\. /m,
|
|
587
|
+
/^```/m,
|
|
588
|
+
];
|
|
589
|
+
return patterns.some(p => p.test(text));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export { Tags, Styles };
|