@textbus/collaborate 2.0.0-beta.2 → 2.0.0-beta.22

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textbus/collaborate",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.0-beta.22",
4
4
  "description": "Textbus is a rich text editor and framework that is highly customizable and extensible to achieve rich wysiwyg effects.",
5
5
  "main": "./bundles/public-api.js",
6
6
  "module": "./bundles/public-api.js",
@@ -25,10 +25,10 @@
25
25
  "typescript editor"
26
26
  ],
27
27
  "dependencies": {
28
- "@tanbo/di": "^1.1.0",
29
- "@tanbo/stream": "^1.0.0",
30
- "@textbus/browser": "^2.0.0-beta.2",
31
- "@textbus/core": "^2.0.0-beta.2",
28
+ "@tanbo/di": "^1.1.1",
29
+ "@tanbo/stream": "^1.0.2",
30
+ "@textbus/browser": "^2.0.0-beta.22",
31
+ "@textbus/core": "^2.0.0-beta.22",
32
32
  "reflect-metadata": "^0.1.13",
33
33
  "y-protocols": "^1.0.5",
34
34
  "yjs": "^13.5.27"
@@ -44,5 +44,5 @@
44
44
  "bugs": {
45
45
  "url": "https://github.com/textbus/textbus.git/issues"
46
46
  },
47
- "gitHead": "b00dc88bfab655f64f203ca07581b923ce44bbb7"
47
+ "gitHead": "2ee52d09e59875e4e8fccd769ef395d4fe373b09"
48
48
  }
@@ -1,12 +1,11 @@
1
- import { Inject, Injectable } from '@tanbo/di'
1
+ import { Inject, Injectable, Optional } from '@tanbo/di'
2
2
  import {
3
3
  createElement,
4
- EDITABLE_DOCUMENT,
5
4
  EDITOR_CONTAINER,
6
5
  getLayoutRectByRange,
7
6
  SelectionBridge
8
7
  } from '@textbus/browser'
9
- import { Selection, SelectionPaths } from '@textbus/core'
8
+ import { Selection, SelectionPaths, Range as TBRange } from '@textbus/core'
10
9
  import { Subject } from '@tanbo/stream'
11
10
 
12
11
  export interface RemoteSelection {
@@ -15,21 +14,28 @@ export interface RemoteSelection {
15
14
  paths: SelectionPaths
16
15
  }
17
16
 
18
- export interface SelectionRect {
19
- color: string
20
- username: string
17
+ export interface Rect {
21
18
  x: number
22
19
  y: number
23
20
  width: number
24
21
  height: number
25
22
  }
26
23
 
24
+ export interface SelectionRect extends Rect {
25
+ color: string
26
+ username: string
27
+ }
28
+
27
29
  export interface RemoteSelectionCursor {
28
30
  cursor: HTMLElement
29
31
  anchor: HTMLElement
30
32
  userTip: HTMLElement
31
33
  }
32
34
 
35
+ export abstract class CollaborateCursorAwarenessDelegate {
36
+ abstract getRects(range: TBRange, nativeRange: Range): false | Rect[]
37
+ }
38
+
33
39
  @Injectable()
34
40
  export class CollaborateCursor {
35
41
  private canvas = createElement('canvas', {
@@ -60,7 +66,7 @@ export class CollaborateCursor {
60
66
  private onRectsChange = new Subject<SelectionRect[]>()
61
67
 
62
68
  constructor(@Inject(EDITOR_CONTAINER) private container: HTMLElement,
63
- @Inject(EDITABLE_DOCUMENT) private document: Document,
69
+ @Optional() private awarenessDelegate: CollaborateCursorAwarenessDelegate,
64
70
  private nativeSelection: SelectionBridge,
65
71
  private selection: Selection) {
66
72
  container.prepend(this.canvas, this.tooltips)
@@ -85,55 +91,73 @@ export class CollaborateCursor {
85
91
 
86
92
 
87
93
  paths.filter(i => {
88
- return i.paths.start.length && i.paths.end.length
94
+ return i.paths.anchor.length && i.paths.focus.length
89
95
  }).forEach(item => {
90
- const startOffset = item.paths.start.pop()!
91
- const startSlot = this.selection.findSlotByPaths(item.paths.start)
92
- const endOffset = item.paths.end.pop()!
93
- const endSlot = this.selection.findSlotByPaths(item.paths.end)
94
-
95
- if (startSlot && endSlot) {
96
- const position = this.nativeSelection.getPositionByRange({
97
- startOffset,
98
- endOffset,
99
- startSlot,
100
- endSlot
96
+ const anchorOffset = item.paths.anchor.pop()!
97
+ const anchorSlot = this.selection.findSlotByPaths(item.paths.anchor)
98
+ const focusOffset = item.paths.focus.pop()!
99
+ const focusSlot = this.selection.findSlotByPaths(item.paths.focus)
100
+ if (!anchorSlot || !focusSlot) {
101
+ return
102
+ }
103
+
104
+ const {focus, anchor} = this.nativeSelection.getPositionByRange({
105
+ focusOffset,
106
+ anchorOffset,
107
+ focusSlot,
108
+ anchorSlot
109
+ })
110
+ if (!focus || !anchor) {
111
+ return
112
+ }
113
+ const nativeRange = document.createRange()
114
+ nativeRange.setStart(anchor.node, anchor.offset)
115
+ nativeRange.setEnd(focus.node, focus.offset)
116
+ if ((anchor.node !== focus.node || anchor.offset !== focus.offset) && nativeRange.collapsed) {
117
+ nativeRange.setStart(focus.node, focus.offset)
118
+ nativeRange.setEnd(anchor.node, anchor.offset)
119
+ }
120
+
121
+ let rects: Rect[] | DOMRectList | false = false
122
+ if (this.awarenessDelegate) {
123
+ rects = this.awarenessDelegate.getRects({
124
+ focusOffset,
125
+ anchorOffset,
126
+ focusSlot,
127
+ anchorSlot
128
+ }, nativeRange)
129
+ }
130
+ if (!rects) {
131
+ rects = nativeRange.getClientRects()
132
+ }
133
+ const selectionRects: SelectionRect[] = []
134
+ for (let i = rects.length - 1; i >= 0; i--) {
135
+ const rect = rects[i]
136
+ selectionRects.push({
137
+ color: item.color,
138
+ username: item.username,
139
+ x: rect.x - containerRect.x,
140
+ y: rect.y - containerRect.y,
141
+ width: rect.width,
142
+ height: rect.height,
101
143
  })
102
- if (position.start && position.end) {
103
- const nativeRange = this.document.createRange()
104
- nativeRange.setStart(position.start.node, position.start.offset)
105
- nativeRange.setEnd(position.end.node, position.end.offset)
106
-
107
- const rects = nativeRange.getClientRects()
108
- const selectionRects: SelectionRect[] = []
109
- for (let i = rects.length - 1; i >= 0; i--) {
110
- const rect = rects[i]
111
- selectionRects.push({
112
- color: item.color,
113
- username: item.username,
114
- x: rect.x - containerRect.x,
115
- y: rect.y - containerRect.y,
116
- width: rect.width,
117
- height: rect.height,
118
- })
119
- }
120
- this.onRectsChange.next(selectionRects)
121
-
122
- const cursorRange = nativeRange.cloneRange()
123
- cursorRange.collapse(!item.paths.focusEnd)
124
-
125
- const cursorRect = getLayoutRectByRange(cursorRange)
126
-
127
- users.push({
128
- username: item.username,
129
- color: item.color,
130
- x: cursorRect.x - containerRect.x,
131
- y: cursorRect.y - containerRect.y,
132
- width: 2,
133
- height: cursorRect.height
134
- })
135
- }
136
144
  }
145
+ this.onRectsChange.next(selectionRects)
146
+
147
+ const cursorRange = nativeRange.cloneRange()
148
+ cursorRange.setStart(focus.node, focus.offset)
149
+ cursorRange.collapse(true)
150
+
151
+ const cursorRect = getLayoutRectByRange(cursorRange)
152
+
153
+ users.push({
154
+ username: item.username,
155
+ color: item.color,
156
+ x: cursorRect.x - containerRect.x,
157
+ y: cursorRect.y - containerRect.y,
158
+ width: 2,
159
+ height: cursorRect.height
160
+ })
137
161
  })
138
162
  this.drawUserCursor(users)
139
163
  }
@@ -1,24 +1,24 @@
1
1
  import { Injectable } from '@tanbo/di'
2
- import { merge, microTask, Observable, Subject, Subscription } from '@tanbo/stream'
2
+ import { delay, Observable, Subject, Subscription } from '@tanbo/stream'
3
3
  import {
4
- RootComponentRef,
5
- Starter,
6
- Translator,
4
+ ComponentInstance,
5
+ ContentType,
6
+ Formats,
7
+ History,
8
+ makeError,
7
9
  Registry,
10
+ RootComponentRef,
11
+ Scheduler,
8
12
  Selection,
9
13
  SelectionPaths,
10
- History, Renderer, Slot, ComponentInstance, makeError, Formats
14
+ Slot,
15
+ Starter,
16
+ Translator
11
17
  } from '@textbus/core'
12
- import {
13
- Doc as YDoc,
14
- Map as YMap,
15
- Text as YText,
16
- Array as YArray,
17
- UndoManager,
18
- Transaction
19
- } from 'yjs'
18
+ import { Array as YArray, Doc as YDoc, Map as YMap, Text as YText, Transaction, UndoManager } from 'yjs'
20
19
 
21
- import { CollaborateCursor, RemoteSelection } from './collab/_api'
20
+ import { CollaborateCursor, RemoteSelection } from './collaborate-cursor'
21
+ import { createUnknownComponent } from './unknown.component'
22
22
 
23
23
  const collaborateErrorFn = makeError('Collaborate')
24
24
 
@@ -60,12 +60,12 @@ export class Collaborate implements History {
60
60
 
61
61
  constructor(private rootComponentRef: RootComponentRef,
62
62
  private collaborateCursor: CollaborateCursor,
63
+ private scheduler: Scheduler,
63
64
  private translator: Translator,
64
- private renderer: Renderer,
65
65
  private registry: Registry,
66
66
  private selection: Selection,
67
67
  private starter: Starter) {
68
- this.onSelectionChange = this.selectionChangeEvent.asObservable()
68
+ this.onSelectionChange = this.selectionChangeEvent.asObservable().pipe(delay())
69
69
  this.onBack = this.backEvent.asObservable()
70
70
  this.onForward = this.forwardEvent.asObservable()
71
71
  this.onChange = this.changeEvent.asObservable()
@@ -74,14 +74,12 @@ export class Collaborate implements History {
74
74
 
75
75
  setup() {
76
76
  this.subscriptions.push(
77
- this.starter.onReady.subscribe(() => {
78
- this.listen2()
79
- }),
80
77
  this.selection.onChange.subscribe(() => {
81
78
  const paths = this.selection.getPaths()
82
79
  this.selectionChangeEvent.next(paths)
83
80
  })
84
81
  )
82
+ this.syncRootComponent()
85
83
  }
86
84
 
87
85
  updateRemoteSelection(paths: RemoteSelection[]) {
@@ -94,13 +92,17 @@ export class Collaborate implements History {
94
92
 
95
93
  back() {
96
94
  if (this.canBack) {
95
+ this.scheduler.stopBroadcastChanges = true
97
96
  this.manager.undo()
97
+ this.scheduler.stopBroadcastChanges = false
98
98
  }
99
99
  }
100
100
 
101
101
  forward() {
102
102
  if (this.canForward) {
103
+ this.scheduler.stopBroadcastChanges = true
103
104
  this.manager.redo()
105
+ this.scheduler.stopBroadcastChanges = false
104
106
  }
105
107
  }
106
108
 
@@ -108,7 +110,7 @@ export class Collaborate implements History {
108
110
  this.subscriptions.forEach(i => i.unsubscribe())
109
111
  }
110
112
 
111
- private listen2() {
113
+ private syncRootComponent() {
112
114
  const root = this.yDoc.getText('content')
113
115
  const rootComponent = this.rootComponentRef.component!
114
116
  this.manager = new UndoManager(root, {
@@ -117,20 +119,13 @@ export class Collaborate implements History {
117
119
  this.syncContent(root, rootComponent.slots.get(0)!)
118
120
 
119
121
  this.subscriptions.push(
120
- merge(
121
- rootComponent.changeMarker.onForceChange,
122
- rootComponent.changeMarker.onChange
123
- ).pipe(
124
- microTask()
125
- ).subscribe(() => {
122
+ this.scheduler.onDocChange.subscribe(() => {
126
123
  this.yDoc.transact(() => {
127
124
  this.updateRemoteActions.forEach(fn => {
128
125
  fn()
129
126
  })
130
127
  this.updateRemoteActions = []
131
128
  }, this.yDoc)
132
- this.renderer.render()
133
- this.selection.restore()
134
129
  })
135
130
  )
136
131
  }
@@ -157,17 +152,18 @@ export class Collaborate implements History {
157
152
  slot.insert(action.insert, makeFormats(this.registry, action.attributes))
158
153
  } else {
159
154
  const sharedComponent = action.insert as YMap<any>
160
- const component = this.createComponentBySharedComponent(sharedComponent)
155
+ const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
156
+ const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
161
157
  this.syncSlots(sharedComponent.get('slots'), component)
162
158
  this.syncComponent(sharedComponent, component)
163
159
  slot.insert(component)
164
160
  }
165
161
  if (this.selection.isSelected) {
166
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
167
- this.selection.setStart(slot, this.selection.startOffset! + length)
162
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! > index) {
163
+ this.selection.setAnchor(slot, this.selection.anchorOffset! + length)
168
164
  }
169
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
170
- this.selection.setEnd(slot, this.selection.endOffset! + length)
165
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! > index) {
166
+ this.selection.setFocus(slot, this.selection.focusOffset! + length)
171
167
  }
172
168
  }
173
169
  } else if (action.delete) {
@@ -175,11 +171,11 @@ export class Collaborate implements History {
175
171
  slot.retain(slot.index)
176
172
  slot.delete(action.delete)
177
173
  if (this.selection.isSelected) {
178
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
179
- this.selection.setStart(slot, this.selection.startOffset! - action.delete)
174
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! >= index) {
175
+ this.selection.setAnchor(slot, this.selection.startOffset! - action.delete)
180
176
  }
181
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
182
- this.selection.setEnd(slot, this.selection.endOffset! - action.delete)
177
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! >= index) {
178
+ this.selection.setFocus(slot, this.selection.focusOffset! - action.delete)
183
179
  }
184
180
  }
185
181
  } else if (action.attributes) {
@@ -219,16 +215,14 @@ export class Collaborate implements History {
219
215
  const isEmpty = delta.length === 1 && delta[0].insert === Slot.emptyPlaceholder
220
216
  if (typeof action.content === 'string') {
221
217
  length = action.content.length
222
- content.insert(offset, action.content)
218
+ content.insert(offset, action.content, action.formats || {})
223
219
  } else {
224
220
  length = 1
225
221
  const component = slot.getContentAtIndex(offset) as ComponentInstance
226
222
  const sharedComponent = this.createSharedComponentByComponent(component)
227
223
  content.insertEmbed(offset, sharedComponent)
228
224
  }
229
- if (action.formats) {
230
- content.format(offset, length, action.formats)
231
- }
225
+
232
226
  if (isEmpty && offset === 0) {
233
227
  content.delete(content.length - 1, 1)
234
228
  }
@@ -243,6 +237,12 @@ export class Collaborate implements History {
243
237
  }
244
238
  })
245
239
  })
240
+
241
+ sub.add(slot.onChildComponentRemove.subscribe(components => {
242
+ components.forEach(c => {
243
+ this.cleanSubscriptionsByComponent(c)
244
+ })
245
+ }))
246
246
  this.contentSyncCaches.set(slot, () => {
247
247
  content.unobserve(syncRemote)
248
248
  sub.unsubscribe()
@@ -281,18 +281,21 @@ export class Collaborate implements History {
281
281
  const slots = component.slots
282
282
  const syncRemote = (ev, tr) => {
283
283
  this.runRemoteUpdate(tr, () => {
284
+ let index = 0
284
285
  ev.delta.forEach(action => {
285
286
  if (Reflect.has(action, 'retain')) {
286
- slots.retain(action.retain!)
287
+ index += action.retain
288
+ slots.retain(index)
287
289
  } else if (action.insert) {
288
290
  (action.insert as Array<YMap<any>>).forEach(item => {
289
291
  const slot = this.createSlotBySharedSlot(item)
290
292
  slots.insert(slot)
291
293
  this.syncContent(item.get('content'), slot)
292
294
  this.syncSlot(item, slot)
295
+ index++
293
296
  })
294
297
  } else if (action.delete) {
295
- slots.retain(slots.index)
298
+ slots.retain(index)
296
299
  slots.delete(action.delete)
297
300
  }
298
301
  })
@@ -313,15 +316,18 @@ export class Collaborate implements History {
313
316
  remoteSlots.insert(index, [sharedSlot])
314
317
  index++
315
318
  } else if (action.type === 'delete') {
316
- slots.slice(index, index + action.count).forEach(slot => {
317
- this.cleanSubscriptionsBySlot(slot)
318
- })
319
319
  remoteSlots.delete(index, action.count)
320
320
  }
321
321
  })
322
322
  })
323
323
  })
324
324
 
325
+ sub.add(slots.onChildSlotRemove.subscribe(slots => {
326
+ slots.forEach(slot => {
327
+ this.cleanSubscriptionsBySlot(slot)
328
+ })
329
+ }))
330
+
325
331
  this.slotsSyncCaches.set(component, () => {
326
332
  remoteSlots.unobserve(syncRemote)
327
333
  sub.unsubscribe()
@@ -366,7 +372,7 @@ export class Collaborate implements History {
366
372
  return
367
373
  }
368
374
  this.updateFromRemote = true
369
- fn()
375
+ this.scheduler.remoteUpdateTransact(fn)
370
376
  this.updateFromRemote = false
371
377
  }
372
378
 
@@ -414,7 +420,7 @@ export class Collaborate implements History {
414
420
  return sharedSlot
415
421
  }
416
422
 
417
- private createComponentBySharedComponent(yMap: YMap<any>): ComponentInstance {
423
+ private createComponentBySharedComponent(yMap: YMap<any>, canInsertInlineComponent: boolean): ComponentInstance {
418
424
  const sharedSlots = yMap.get('slots') as YArray<YMap<any>>
419
425
  const slots: Slot[] = []
420
426
  sharedSlots.forEach(sharedSlot => {
@@ -428,13 +434,17 @@ export class Collaborate implements History {
428
434
  })
429
435
  if (instance) {
430
436
  instance.slots.toArray().forEach((slot, index) => {
431
- const sharedSlot = sharedSlots.get(index)
437
+ let sharedSlot = sharedSlots.get(index)
438
+ if (!sharedSlot) {
439
+ sharedSlot = this.createSharedSlotBySlot(slot)
440
+ sharedSlots.push([sharedSlot])
441
+ }
432
442
  this.syncSlot(sharedSlot, slot)
433
443
  this.syncContent(sharedSlot.get('content'), slot)
434
444
  })
435
445
  return instance
436
446
  }
437
- throw collaborateErrorFn(`cannot find component factory \`${name}\`.`)
447
+ return createUnknownComponent(name, canInsertInlineComponent).createInstance(this.starter)
438
448
  }
439
449
 
440
450
  private createSlotBySharedSlot(sharedSlot: YMap<any>): Slot {
@@ -454,7 +464,8 @@ export class Collaborate implements History {
454
464
  slot.insert(action.insert, makeFormats(this.registry, action.attributes))
455
465
  } else {
456
466
  const sharedComponent = action.insert as YMap<any>
457
- const component = this.createComponentBySharedComponent(sharedComponent)
467
+ const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
468
+ const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
458
469
  slot.insert(component)
459
470
  this.syncSlots(sharedComponent.get('slots'), component)
460
471
  this.syncComponent(sharedComponent, component)
@@ -494,7 +505,7 @@ export class Collaborate implements History {
494
505
  function makeFormats(registry: Registry, attrs?: any) {
495
506
  const formats: Formats = []
496
507
  if (attrs) {
497
- Object.keys(attrs).map(key => {
508
+ Object.keys(attrs).forEach(key => {
498
509
  const formatter = registry.getFormatter(key)
499
510
  if (formatter) {
500
511
  formats.push([formatter, attrs[key]])
@@ -0,0 +1,35 @@
1
+ import { Plugin, Renderer, Scheduler } from '@textbus/core'
2
+ import { Injector } from '@tanbo/di'
3
+ import { Caret, CaretPosition } from '@textbus/browser'
4
+ import { Subscription } from '@tanbo/stream'
5
+
6
+ export class FixedCaretPlugin implements Plugin {
7
+ private subscriptions = new Subscription()
8
+
9
+ constructor(public scrollContainer: HTMLElement) {
10
+ }
11
+
12
+ setup(injector: Injector) {
13
+ const scheduler = injector.get(Scheduler)
14
+ const caret = injector.get(Caret)
15
+ const renderer = injector.get(Renderer)
16
+
17
+ let isChanged = false
18
+ let caretPosition: CaretPosition | null = null
19
+ renderer.onViewChecked.subscribe(() => {
20
+ isChanged = true
21
+ })
22
+ this.subscriptions.add(caret.onPositionChange.subscribe(position => {
23
+ if (isChanged && caretPosition && position && !scheduler.hasLocalUpdate) {
24
+ const offset = position.top - caretPosition.top
25
+ this.scrollContainer.scrollTop += offset
26
+ isChanged = false
27
+ }
28
+ caretPosition = position
29
+ }))
30
+ }
31
+
32
+ onDestroy() {
33
+ this.subscriptions.unsubscribe()
34
+ }
35
+ }
package/src/public-api.ts CHANGED
@@ -1,2 +1,19 @@
1
- export * from './collab/_api'
1
+ import { History, Module } from '@textbus/core'
2
+
3
+ import { Collaborate } from './collaborate'
4
+ import { CollaborateCursor } from './collaborate-cursor'
5
+
2
6
  export * from './collaborate'
7
+ export * from './collaborate-cursor'
8
+ export * from './fixed-caret.plugin'
9
+
10
+ export const collaborateModule: Module = {
11
+ providers: [
12
+ Collaborate,
13
+ CollaborateCursor,
14
+ {
15
+ provide: History,
16
+ useClass: Collaborate
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,22 @@
1
+ import { ContentType, defineComponent, VElement } from '@textbus/core'
2
+
3
+ export function createUnknownComponent(factoryName: string, canInsertInlineComponent: boolean) {
4
+ const unknownComponent = defineComponent({
5
+ type: canInsertInlineComponent ? ContentType.InlineComponent : ContentType.BlockComponent,
6
+ name: 'UnknownComponent',
7
+ setup() {
8
+ console.error(`cannot find component factory \`${factoryName}\`.`)
9
+ return {
10
+ render() {
11
+ return VElement.createElement('textbus-unknown-component', {
12
+ style: {
13
+ display: canInsertInlineComponent ? 'inline' : 'block',
14
+ color: '#f00'
15
+ }
16
+ }, unknownComponent.name)
17
+ }
18
+ }
19
+ }
20
+ })
21
+ return unknownComponent
22
+ }
@@ -1 +0,0 @@
1
- export * from './collaborate-cursor';
@@ -1,2 +0,0 @@
1
- export * from './collaborate-cursor';
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiX2FwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9jb2xsYWIvX2FwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHNCQUFzQixDQUFBIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0ICogZnJvbSAnLi9jb2xsYWJvcmF0ZS1jdXJzb3InXG4iXX0=