@satorijs/element 3.1.7 → 4.0.0-alpha.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/src/index.ts DELETED
@@ -1,551 +0,0 @@
1
- import { arrayBufferToBase64, Awaitable, camelize, defineProperty, Dict, hyphenate, is, isNullable, makeArray } from 'cosmokit'
2
-
3
- declare global {
4
- namespace JSX {
5
- interface IntrinsicElements {
6
- [key: string]: any
7
- message: {
8
- id?: string
9
- forward?: boolean
10
- children?: any[]
11
- }
12
- quote: {
13
- id?: string
14
- name?: string
15
- avatar?: string
16
- children?: any[]
17
- }
18
- at: {
19
- id?: string
20
- name?: string
21
- avatar?: string
22
- role?: string
23
- type?: string
24
- }
25
- sharp: {
26
- id?: string
27
- name?: string
28
- avatar?: string
29
- }
30
- img: ResourceElement
31
- audio: ResourceElement
32
- video: ResourceElement
33
- file: ResourceElement
34
- }
35
-
36
- interface ResourceElement {
37
- [key: string]: any
38
- src?: string
39
- title?: string
40
- width?: string | number
41
- height?: string | number
42
- duration?: string | number
43
- poster?: string
44
- }
45
- }
46
- }
47
-
48
- const kElement = Symbol.for('satori.element')
49
-
50
- interface Element {
51
- [kElement]: true
52
- type: string
53
- attrs: Dict
54
- /** @deprecated use `attrs` instead */
55
- data: Dict
56
- children: Element[]
57
- source?: string
58
- toString(strip?: boolean): string
59
- }
60
-
61
- interface ElementConstructor extends Element { }
62
-
63
- class ElementConstructor {
64
- get data() {
65
- return this.attrs
66
- }
67
-
68
- getTagName() {
69
- if (this.type === 'component') {
70
- return this.attrs.is?.name ?? 'component'
71
- } else {
72
- return this.type
73
- }
74
- }
75
-
76
- toAttrString() {
77
- return Object.entries(this.attrs).map(([key, value]) => {
78
- if (isNullable(value)) return ''
79
- key = hyphenate(key)
80
- if (value === true) return ` ${key}`
81
- if (value === false) return ` no-${key}`
82
- return ` ${key}="${Element.escape('' + value, true)}"`
83
- }).join('')
84
- }
85
-
86
- toString(strip = false) {
87
- if (this.type === 'text' && 'content' in this.attrs) {
88
- return strip ? this.attrs.content : Element.escape(this.attrs.content)
89
- }
90
- const inner = this.children.map(child => child.toString(strip)).join('')
91
- if (strip) return inner
92
- const attrs = this.toAttrString()
93
- const tag = this.getTagName()
94
- if (!this.children.length) return `<${tag}${attrs}/>`
95
- return `<${tag}${attrs}>${inner}</${tag}>`
96
- }
97
- }
98
-
99
- defineProperty(ElementConstructor, 'name', 'Element')
100
- defineProperty(ElementConstructor.prototype, kElement, true)
101
-
102
- type RenderFunction = Element.Render<Element.Fragment, any>
103
-
104
- function Element(type: string | RenderFunction, ...children: Element.Fragment[]): Element
105
- function Element(type: string | RenderFunction, attrs: Dict, ...children: Element.Fragment[]): Element
106
- function Element(type: string | RenderFunction, ...args: any[]) {
107
- const el = Object.create(ElementConstructor.prototype)
108
- const attrs: Dict = {}, children: Element[] = []
109
- if (args[0] && typeof args[0] === 'object' && !Element.isElement(args[0]) && !Array.isArray(args[0])) {
110
- const props = args.shift()
111
- for (const [key, value] of Object.entries(props)) {
112
- if (isNullable(value)) continue
113
- // https://github.com/reactjs/rfcs/pull/107
114
- if (key === 'children') {
115
- args.push(...makeArray(value))
116
- } else {
117
- attrs[camelize(key)] = value
118
- }
119
- }
120
- }
121
- for (const child of args) {
122
- children.push(...Element.toElementArray(child))
123
- }
124
- if (typeof type === 'function') {
125
- attrs.is = type
126
- type = 'component'
127
- }
128
- return Object.assign(el, { type, attrs, children })
129
- }
130
-
131
- // eslint-disable-next-line no-new-func
132
- const evaluate = new Function('expr', 'context', `
133
- try {
134
- with (context) {
135
- return eval(expr)
136
- }
137
- } catch {}
138
- `) as ((expr: string, context: object) => string)
139
-
140
- namespace Element {
141
- export const jsx = Element
142
- export const jsxs = Element
143
- export const jsxDEV = Element
144
- export const Fragment = 'template'
145
-
146
- export type Fragment = string | Element | (string | Element)[]
147
- export type Visit<T, S> = (element: Element, session: S) => T
148
- export type Render<T, S> = (attrs: Dict, children: Element[], session: S) => T
149
- export type SyncTransformer<S = never> = boolean | Fragment | Render<boolean | Fragment, S>
150
- export type Transformer<S = never> = boolean | Fragment | Render<Awaitable<boolean | Fragment>, S>
151
-
152
- type SyncVisitor<S> = Dict<SyncTransformer<S>> | Visit<boolean | Fragment, S>
153
- type Visitor<S> = Dict<Transformer<S>> | Visit<Awaitable<boolean | Fragment>, S>
154
-
155
- export function isElement(source: any): source is Element {
156
- return source && typeof source === 'object' && source[kElement]
157
- }
158
-
159
- export function toElement(content: string | Element) {
160
- if (typeof content === 'string' || typeof content === 'number' || typeof content === 'boolean') {
161
- content = '' + content
162
- if (content) return Element('text', { content })
163
- } else if (isElement(content)) {
164
- return content
165
- } else if (!isNullable(content)) {
166
- throw new TypeError(`Invalid content: ${content}`)
167
- }
168
- }
169
-
170
- export function toElementArray(content: Element.Fragment) {
171
- if (Array.isArray(content)) {
172
- return content.map(toElement).filter(x => x)
173
- } else {
174
- return [toElement(content)].filter(x => x)
175
- }
176
- }
177
-
178
- export function normalize(source: Fragment, context?: any) {
179
- return typeof source === 'string' ? parse(source, context) : toElementArray(source)
180
- }
181
-
182
- export function escape(source: string, inline = false) {
183
- const result
184
- = (source ?? '').replace(/&/g, '&amp;')
185
- .replace(/</g, '&lt;')
186
- .replace(/>/g, '&gt;')
187
- return inline
188
- ? result.replace(/"/g, '&quot;')
189
- : result
190
- }
191
-
192
- export function unescape(source: string) {
193
- return source
194
- .replace(/&lt;/g, '<')
195
- .replace(/&gt;/g, '>')
196
- .replace(/&quot;/g, '"')
197
- .replace(/&#(\d+);/g, (_, code) => code === '38' ? _ : String.fromCharCode(+code))
198
- .replace(/&#x([0-9a-f]+);/gi, (_, code) => code === '26' ? _ : String.fromCharCode(parseInt(code, 16)))
199
- .replace(/&(amp|#38|#x26);/g, '&')
200
- }
201
-
202
- export interface FindOptions {
203
- type?: string
204
- caret?: boolean
205
- }
206
-
207
- /** @deprecated use `Element.select()` instead */
208
- export function from(source: string, options: FindOptions = {}): Element {
209
- const elements = parse(source)
210
- if (options.caret) {
211
- if (options.type && elements[0]?.type !== options.type) return
212
- return elements[0]
213
- }
214
- return select(elements, options.type || '*')[0]
215
- }
216
-
217
- type Combinator = ' ' | '>' | '+' | '~'
218
-
219
- export interface Selector {
220
- type: string
221
- combinator: Combinator
222
- }
223
-
224
- const combRegExp = / *([ >+~]) */g
225
-
226
- export function parseSelector(input: string): Selector[][] {
227
- return input.split(',').map((query) => {
228
- const selectors: Selector[] = []
229
- query = query.trim()
230
- let combCap: RegExpExecArray, combinator: Combinator = ' '
231
- while ((combCap = combRegExp.exec(query))) {
232
- selectors.push({ type: query.slice(0, combCap.index), combinator })
233
- combinator = combCap[1] as Combinator
234
- query = query.slice(combCap.index + combCap[0].length)
235
- }
236
- selectors.push({ type: query, combinator })
237
- return selectors
238
- })
239
- }
240
-
241
- export function select(source: string | Element[], query: string | Selector[][]): Element[] {
242
- if (!source || !query) return []
243
- if (typeof source === 'string') source = parse(source)
244
- if (typeof query === 'string') query = parseSelector(query)
245
- if (!query.length) return []
246
- let adjacent: Selector[][] = []
247
- const results: Element[] = []
248
- for (const [index, element] of source.entries()) {
249
- const inner: Selector[][] = []
250
- const local = [...query, ...adjacent]
251
- adjacent = []
252
- let matched = false
253
- for (const group of local) {
254
- const { type, combinator } = group[0]
255
- if (type === element.type || type === '*') {
256
- if (group.length === 1) {
257
- matched = true
258
- } else if ([' ', '>'].includes(group[1].combinator)) {
259
- inner.push(group.slice(1))
260
- } else if (group[1].combinator === '+') {
261
- adjacent.push(group.slice(1))
262
- } else {
263
- query.push(group.slice(1))
264
- }
265
- }
266
- if (combinator === ' ') {
267
- inner.push(group)
268
- }
269
- }
270
- if (matched) results.push(source[index])
271
- results.push(...select(element.children, inner))
272
- }
273
- return results
274
- }
275
-
276
- export function interpolate(expr: string, context: any) {
277
- expr = expr.trim()
278
- if (!/^[\w.]+$/.test(expr)) {
279
- return evaluate(expr, context) ?? ''
280
- }
281
- let value = context
282
- for (const part of expr.split('.')) {
283
- value = value[part]
284
- if (isNullable(value)) return ''
285
- }
286
- return value ?? ''
287
- }
288
-
289
- const tagRegExp1 = /(?<comment><!--[\s\S]*?-->)|(?<tag><(\/?)([^!\s>/]*)([^>]*?)\s*(\/?)>)/
290
- const tagRegExp2 = /(?<comment><!--[\s\S]*?-->)|(?<tag><(\/?)([^!\s>/]*)([^>]*?)\s*(\/?)>)|(?<curly>\{(?<derivative>[@:/#][^\s}]*)?[\s\S]*?\})/
291
- const attrRegExp1 = /([^\s=]+)(?:="(?<value1>[^"]*)"|='(?<value2>[^']*)')?/g
292
- const attrRegExp2 = /([^\s=]+)(?:="(?<value1>[^"]*)"|='(?<value2>[^']*)'|=\{(?<curly>[^}]+)\})?/g
293
-
294
- const enum Position {
295
- OPEN,
296
- CLOSE,
297
- EMPTY,
298
- CONTINUE,
299
- }
300
-
301
- interface Token {
302
- type: 'angle' | 'curly'
303
- name: string
304
- position: Position
305
- source: string
306
- extra: string
307
- children?: Dict<(string | Token)[]>
308
- }
309
-
310
- export function parse(source: string, context?: any) {
311
- const tokens: (string | Token)[] = []
312
- function pushText(content: string) {
313
- if (content) tokens.push(content)
314
- }
315
-
316
- const tagRegExp = context ? tagRegExp2 : tagRegExp1
317
- let tagCap: RegExpExecArray
318
- let trimStart = true
319
- while ((tagCap = tagRegExp.exec(source))) {
320
- const trimEnd = !tagCap.groups.curly
321
- parseContent(source.slice(0, tagCap.index), trimStart, trimEnd)
322
- trimStart = trimEnd
323
- source = source.slice(tagCap.index + tagCap[0].length)
324
- const [_, , , close, type, extra, empty] = tagCap
325
- if (tagCap.groups.comment) continue
326
- if (tagCap.groups.curly) {
327
- let name = '', position = Position.EMPTY
328
- if (tagCap.groups.derivative) {
329
- name = tagCap.groups.derivative.slice(1)
330
- position = {
331
- '@': Position.EMPTY,
332
- '#': Position.OPEN,
333
- '/': Position.CLOSE,
334
- ':': Position.CONTINUE,
335
- }[tagCap.groups.derivative[0]]
336
- }
337
- tokens.push({
338
- type: 'curly',
339
- name,
340
- position,
341
- source: tagCap.groups.curly,
342
- extra: tagCap.groups.curly.slice(1 + (tagCap.groups.derivative ?? '').length, -1),
343
- })
344
- continue
345
- }
346
- tokens.push({
347
- type: 'angle',
348
- source: _,
349
- name: type || Fragment,
350
- position: close ? Position.CLOSE : empty ? Position.EMPTY : Position.OPEN,
351
- extra,
352
- })
353
- }
354
-
355
- parseContent(source, trimStart, true)
356
- function parseContent(source: string, trimStart: boolean, trimEnd: boolean) {
357
- source = unescape(source)
358
- if (trimStart) source = source.replace(/^\s*\n\s*/, '')
359
- if (trimEnd) source = source.replace(/\s*\n\s*$/, '')
360
- pushText(source)
361
- }
362
-
363
- return parseTokens(foldTokens(tokens), context)
364
- }
365
-
366
- function foldTokens(tokens: (string | Token)[]) {
367
- const stack: [Token, string][] = [[{
368
- type: 'angle',
369
- name: Fragment,
370
- position: Position.OPEN,
371
- source: '',
372
- extra: '',
373
- children: { default: [] },
374
- }, 'default']]
375
-
376
- function pushToken(...tokens: (string | Token)[]) {
377
- const [token, slot] = stack[0]
378
- token.children[slot].push(...tokens)
379
- }
380
-
381
- for (const token of tokens) {
382
- if (typeof token === 'string') {
383
- pushToken(token)
384
- continue
385
- }
386
- const { name, position } = token
387
- if (position === Position.CLOSE) {
388
- if (stack[0][0].name === name) {
389
- stack.shift()
390
- }
391
- } else if (position === Position.CONTINUE) {
392
- stack[0][0].children[name] = []
393
- stack[0][1] = name
394
- } else if (position === Position.OPEN) {
395
- pushToken(token)
396
- token.children = { default: [] }
397
- stack.unshift([token, 'default'])
398
- } else {
399
- pushToken(token)
400
- }
401
- }
402
-
403
- return stack[stack.length - 1][0].children.default
404
- }
405
-
406
- function parseTokens(tokens: (string | Token)[], context?: any) {
407
- const result: Element[] = []
408
- for (const token of tokens) {
409
- if (typeof token === 'string') {
410
- result.push(Element('text', { content: token }))
411
- } else if (token.type === 'angle') {
412
- const attrs = {}
413
- const attrRegExp = context ? attrRegExp2 : attrRegExp1
414
- let attrCap: RegExpExecArray
415
- while ((attrCap = attrRegExp.exec(token.extra))) {
416
- const [, key, v1, v2 = v1, v3] = attrCap
417
- if (v3) {
418
- attrs[key] = interpolate(v3, context)
419
- } else if (!isNullable(v2)) {
420
- attrs[key] = unescape(v2)
421
- } else if (key.startsWith('no-')) {
422
- attrs[key.slice(3)] = false
423
- } else {
424
- attrs[key] = true
425
- }
426
- }
427
- result.push(Element(token.name, attrs, token.children && parseTokens(token.children.default, context)))
428
- } else if (!token.name) {
429
- result.push(...toElementArray(interpolate(token.extra, context)))
430
- } else if (token.name === 'if') {
431
- if (evaluate(token.extra, context)) {
432
- result.push(...parseTokens(token.children.default, context))
433
- } else {
434
- result.push(...parseTokens(token.children.else || [], context))
435
- }
436
- } else if (token.name === 'each') {
437
- const [expr, ident] = token.extra.split(/\s+as\s+/)
438
- const items = interpolate(expr, context)
439
- if (!items || !items[Symbol.iterator]) continue
440
- for (const item of items) {
441
- result.push(...parseTokens(token.children.default, { ...context, [ident]: item }))
442
- }
443
- }
444
- }
445
- return result
446
- }
447
-
448
- function visit<S>(element: Element, rules: Visitor<S>, session: S) {
449
- const { type, attrs, children } = element
450
- if (typeof rules === 'function') {
451
- return rules(element, session)
452
- } else {
453
- let result: any = rules[typeof type === 'string' ? type : ''] ?? rules.default ?? true
454
- if (typeof result === 'function') {
455
- result = result(attrs, children, session)
456
- }
457
- return result
458
- }
459
- }
460
-
461
- export function transform<S = never>(source: string, rules: SyncVisitor<S>, session?: S): string
462
- export function transform<S = never>(source: Element[], rules: SyncVisitor<S>, session?: S): Element[]
463
- export function transform<S>(source: string | Element[], rules: SyncVisitor<S>, session?: S) {
464
- const elements = typeof source === 'string' ? parse(source) : source
465
- const output: Element[] = []
466
- elements.forEach((element) => {
467
- const { type, attrs, children } = element
468
- const result = visit(element, rules, session)
469
- if (result === true) {
470
- output.push(Element(type, attrs, transform(children, rules, session)))
471
- } else if (result !== false) {
472
- output.push(...toElementArray(result))
473
- }
474
- })
475
- return typeof source === 'string' ? output.join('') : output
476
- }
477
-
478
- export async function transformAsync<S = never>(source: string, rules: Visitor<S>, session?: S): Promise<string>
479
- export async function transformAsync<S = never>(source: Element[], rules: Visitor<S>, session?: S): Promise<Element[]>
480
- export async function transformAsync<S>(source: string | Element[], rules: Visitor<S>, session?: S) {
481
- const elements = typeof source === 'string' ? parse(source) : source
482
- const children = (await Promise.all(elements.map(async (element) => {
483
- const { type, attrs, children } = element
484
- const result = await visit(element, rules, session)
485
- if (result === true) {
486
- return [Element(type, attrs, await transformAsync(children, rules, session))]
487
- } else if (result !== false) {
488
- return toElementArray(result)
489
- } else {
490
- return []
491
- }
492
- }))).flat(1)
493
- return typeof source === 'string' ? children.join('') : children
494
- }
495
-
496
- export type Factory<R extends any[]> = (...args: [...rest: R, attrs?: Dict]) => Element
497
-
498
- function createFactory<R extends any[] = any[]>(type: string, ...keys: string[]): Factory<R> {
499
- return (...args: any[]) => {
500
- const element = Element(type)
501
- keys.forEach((key, index) => {
502
- if (!isNullable(args[index])) {
503
- element.attrs[key] = args[index]
504
- }
505
- })
506
- if (args[keys.length]) {
507
- Object.assign(element.attrs, args[keys.length])
508
- }
509
- return element
510
- }
511
- }
512
-
513
- // eslint-disable-next-line prefer-const
514
- export let warn: (message: string) => void = () => { }
515
-
516
- function createAssetFactory(type: string): Factory<[data: string] | [data: Buffer | ArrayBuffer | ArrayBufferView, type: string]> {
517
- return (src, ...args) => {
518
- let prefix = 'base64://'
519
- if (typeof args[0] === 'string') {
520
- prefix = `data:${args.shift()};base64,`
521
- }
522
- if (is('Buffer', src)) {
523
- src = prefix + src.toString('base64')
524
- } else if (is('ArrayBuffer', src)) {
525
- src = prefix + arrayBufferToBase64(src)
526
- } else if (ArrayBuffer.isView(src)) {
527
- src = prefix + arrayBufferToBase64(src.buffer)
528
- }
529
- if (src.startsWith('base64://')) {
530
- warn(`protocol "base64:" is deprecated and will be removed in the future, please use "data:" instead`)
531
- }
532
- return Element(type, { ...args[0] as {}, src })
533
- }
534
- }
535
-
536
- export const text = createFactory<[content: any]>('text', 'content')
537
- export const at = createFactory<[id: any]>('at', 'id')
538
- export const sharp = createFactory<[id: any]>('sharp', 'id')
539
- export const quote = createFactory<[id: any]>('quote', 'id')
540
- export const image = createAssetFactory('img')
541
- export const img = createAssetFactory('img')
542
- export const video = createAssetFactory('video')
543
- export const audio = createAssetFactory('audio')
544
- export const file = createAssetFactory('file')
545
-
546
- export function i18n(path: string | Dict, children?: any[]) {
547
- return Element('i18n', typeof path === 'string' ? { path } : path, children)
548
- }
549
- }
550
-
551
- export = Element