@dxos/react-ui-editor 0.3.10-main.cbe7692 → 0.3.10-main.dcfebef

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.
Files changed (56) hide show
  1. package/dist/lib/browser/index.mjs +123 -689
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +1 -19
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +1 -2
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -1
  9. package/dist/types/src/components/TextEditor/extensions/basic.d.ts +1 -2
  10. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -1
  11. package/dist/types/src/components/TextEditor/extensions/demo.d.ts +1 -2
  12. package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -1
  13. package/dist/types/src/components/TextEditor/extensions/index.d.ts +0 -4
  14. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  15. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -1
  16. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -1
  17. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +1 -2
  18. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  19. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +2 -6
  20. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
  21. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +1 -2
  22. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -1
  23. package/dist/types/src/components/TextEditor/themes/default.d.ts +0 -5
  24. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
  25. package/dist/types/src/{hooks/yjs → components/TextEditor}/yjs.stories.d.ts +1 -1
  26. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -0
  27. package/dist/types/src/styles/markdown.d.ts +2 -3
  28. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  29. package/package.json +20 -20
  30. package/src/components/TextEditor/MarkdownEditor.stories.tsx +25 -98
  31. package/src/components/TextEditor/TextEditor.tsx +7 -16
  32. package/src/components/TextEditor/extensions/autocomplete.ts +20 -5
  33. package/src/components/TextEditor/extensions/basic.ts +1 -2
  34. package/src/components/TextEditor/extensions/demo.ts +1 -4
  35. package/src/components/TextEditor/extensions/index.ts +0 -4
  36. package/src/components/TextEditor/extensions/link.ts +23 -15
  37. package/src/components/TextEditor/extensions/listener.ts +3 -3
  38. package/src/components/TextEditor/extensions/markdown/bundle.ts +5 -10
  39. package/src/components/TextEditor/extensions/markdown/highlight.ts +104 -118
  40. package/src/components/TextEditor/extensions/tasklist.ts +20 -34
  41. package/src/components/TextEditor/themes/default.ts +2 -54
  42. package/src/{hooks/yjs → components/TextEditor}/yjs.stories.tsx +1 -1
  43. package/src/styles/markdown.ts +8 -14
  44. package/dist/types/src/components/TextEditor/extensions/blast.d.ts +0 -25
  45. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +0 -1
  46. package/dist/types/src/components/TextEditor/extensions/image.d.ts +0 -4
  47. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +0 -1
  48. package/dist/types/src/components/TextEditor/extensions/mention.d.ts +0 -6
  49. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +0 -1
  50. package/dist/types/src/components/TextEditor/extensions/table.d.ts +0 -8
  51. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +0 -1
  52. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +0 -1
  53. package/src/components/TextEditor/extensions/blast.ts +0 -375
  54. package/src/components/TextEditor/extensions/image.ts +0 -68
  55. package/src/components/TextEditor/extensions/mention.ts +0 -31
  56. package/src/components/TextEditor/extensions/table.ts +0 -123
@@ -1,375 +0,0 @@
1
- //
2
- // Copyright 2023 DXOS.org
3
- // Based on a demo by Joel Besada and Kushagra Gour.
4
- // https://twitter.com/JoelBesada/status/670343885655293952
5
- // https://github.com/chinchang/code-blast-codemirror/blob/master/code-blast.js
6
- //
7
-
8
- import { type Extension, StateField } from '@codemirror/state';
9
- import { EditorView, keymap } from '@codemirror/view';
10
- import defaultsDeep from 'lodash.defaultsdeep';
11
-
12
- import { invariant } from '@dxos/invariant';
13
-
14
- export type BlastOptions = {
15
- effect?: number;
16
- maxParticles: number;
17
- particleGravity: number;
18
- particleAlphaFadeout: number;
19
- particleSize: { min: number; max: number };
20
- particleNumRange: { min: number; max: number };
21
- particleVelocityRange: { x: [number, number]; y: [number, number] };
22
- particleShrinkRate: number;
23
- shakeIntensity: number;
24
- color?: () => number[];
25
- };
26
-
27
- export const defaultOptions: BlastOptions = {
28
- maxParticles: 200,
29
- particleGravity: 0.08,
30
- particleAlphaFadeout: 0.996,
31
- particleSize: { min: 2, max: 8 },
32
- particleNumRange: { min: 5, max: 10 },
33
- particleVelocityRange: { x: [-1, 1], y: [-3.5, -1.5] },
34
- particleShrinkRate: 0.95,
35
- shakeIntensity: 5,
36
- };
37
-
38
- export const blast = (options: Partial<BlastOptions>): Extension => {
39
- let blaster: Blaster | undefined;
40
- let last = 0;
41
-
42
- const getPoint = (view: EditorView, pos: number) => {
43
- const { left: x = 0, top: y = 0 } = view.coordsForChar(pos) ?? {};
44
- const element = document.elementFromPoint(x, y);
45
-
46
- const { offsetLeft: left = 0, offsetTop: top = 0 } = view.scrollDOM.parentElement ?? {};
47
- const point = { x: x - left, y: y - top };
48
-
49
- return { element, point };
50
- };
51
-
52
- return [
53
- // Cursor moved.
54
- EditorView.updateListener.of((update) => {
55
- // NOTE: The MarkdownEditor may recreated the EditorView.
56
- if (blaster?.node !== update.view.scrollDOM) {
57
- if (blaster) {
58
- blaster.destroy();
59
- }
60
-
61
- blaster = new Blaster(update.view.scrollDOM, defaultsDeep({}, options, defaultOptions));
62
- blaster.initialize();
63
- blaster.start(); // TODO(burdon): Stop/clean-up.
64
- } else {
65
- blaster.resize();
66
- }
67
-
68
- const current = update.view.state.selection.main.head;
69
- if (current !== last) {
70
- last = current;
71
- // TODO(burdon): Null if end of line.
72
- const { element, point } = getPoint(update.view, current);
73
- if (element && point) {
74
- blaster.spawn({ element, point });
75
- }
76
- }
77
- }),
78
-
79
- // Document changed.
80
- StateField.define({
81
- create: () => null,
82
- update: (_value, transaction) => {
83
- if (blaster && transaction.docChanged) {
84
- // TODO(burdon): Only on delete.
85
- const view = EditorView.findFromDOM(blaster.node)!;
86
- const { element, point } = getPoint(view, transaction.selection!.main.head);
87
- if (element && point) {
88
- blaster.spawn({ element, point });
89
- }
90
- }
91
-
92
- return null;
93
- },
94
- }),
95
-
96
- keymap.of([
97
- {
98
- any: (view, event) => {
99
- if (blaster && event.key === 'Enter' && event.shiftKey) {
100
- blaster.shake({ time: 0.4 });
101
- }
102
-
103
- return false;
104
- },
105
- },
106
- ]),
107
- ];
108
- };
109
-
110
- //
111
- // Blaster (no CM deps).
112
- //
113
-
114
- class Blaster {
115
- _canvas: HTMLCanvasElement | undefined;
116
- _ctx: CanvasRenderingContext2D | undefined | null;
117
-
118
- private readonly _effect: Effect;
119
-
120
- _running = false;
121
- _lastTime: number | undefined;
122
- _shakeTime = 0;
123
- _shakeTimeMax = 0;
124
-
125
- _particles: Particle[] = [];
126
- _particlePointer = 0;
127
-
128
- _lastPoint = { x: 0, y: 0 };
129
-
130
- constructor(private readonly _node: HTMLElement, private readonly _options: BlastOptions) {
131
- this._effect = this._options.effect === 1 ? new Effect1(_options) : new Effect2(_options);
132
- }
133
-
134
- get node() {
135
- return this._node;
136
- }
137
-
138
- initialize() {
139
- // console.log('initialize');
140
- invariant(!this._canvas && !this._ctx);
141
-
142
- this._canvas = document.createElement('canvas');
143
- this._canvas.id = 'code-blast-canvas';
144
- this._canvas.style.position = 'absolute';
145
- this._canvas.style.zIndex = '0';
146
- this._canvas.style.pointerEvents = 'none';
147
- // this._canvas.style.border = '2px solid red';
148
-
149
- this._ctx = this._canvas.getContext('2d');
150
-
151
- const parent = this._node.parentElement?.parentElement;
152
- parent?.appendChild(this._canvas);
153
-
154
- this.resize();
155
- }
156
-
157
- destroy() {
158
- this.stop();
159
- // console.log('destroy');
160
- if (this._canvas) {
161
- this._canvas.remove();
162
- this._canvas = undefined;
163
- this._ctx = undefined;
164
- }
165
- }
166
-
167
- resize() {
168
- if (this._node.parentElement && this._canvas) {
169
- const { offsetLeft: x, offsetTop: y, offsetWidth: width, offsetHeight: height } = this._node.parentElement;
170
- this._canvas.style.top = `${y}px`;
171
- this._canvas.style.left = `${x}px`;
172
- this._canvas.width = width;
173
- this._canvas.height = height;
174
- }
175
- }
176
-
177
- start() {
178
- // console.log('start');
179
- invariant(this._canvas && this._ctx);
180
- this._running = true;
181
- this.loop();
182
- }
183
-
184
- stop() {
185
- // console.log('stop');
186
- this._running = false;
187
- this._node.style.transform = 'translate(0px, 0px)';
188
- }
189
-
190
- loop() {
191
- if (!this._running || !this._canvas || !this._ctx) {
192
- return;
193
- }
194
-
195
- this._ctx.clearRect(0, 0, this._canvas.width ?? 0, this._canvas.height ?? 0);
196
-
197
- // Calculate the delta from the previous frame.
198
- const now = new Date().getTime();
199
- this._lastTime ??= now;
200
- const dt = (now - this._lastTime) / 1000;
201
- this._lastTime = now;
202
-
203
- if (this._shakeTime > 0) {
204
- this._shakeTime -= dt;
205
- const magnitude = (this._shakeTime / this._shakeTimeMax) * this._options.shakeIntensity;
206
- const shakeX = random(-magnitude, magnitude);
207
- const shakeY = random(-magnitude, magnitude);
208
- this._node!.style.transform = `translate(${shakeX}px,${shakeY}px)`;
209
- }
210
-
211
- this.drawParticles();
212
-
213
- requestAnimationFrame(this.loop.bind(this));
214
- }
215
-
216
- shake = throttle<{ time: number }>(({ time }) => {
217
- this._shakeTime = this._shakeTimeMax;
218
- this._shakeTimeMax = time;
219
- }, 100);
220
-
221
- spawn = throttle<{ element: Element; point: { x: number; y: number } }>(({ element, point }) => {
222
- const color = getRGBComponents(element, this._options.color);
223
- const numParticles = random(this._options.particleNumRange.min, this._options.particleNumRange.max);
224
- const dir = this._lastPoint.x === point.x ? 0 : this._lastPoint.x < point.x ? 1 : -1;
225
- this._lastPoint = point;
226
- for (let i = numParticles; i--; i > 0) {
227
- this._particles[this._particlePointer] = this._effect.create(point.x - dir * 16, point.y, color);
228
- this._particlePointer = (this._particlePointer + 1) % this._options.maxParticles;
229
- }
230
- }, 100);
231
-
232
- drawParticles() {
233
- for (let i = this._particles.length; i--; i > 0) {
234
- const particle = this._particles[i];
235
- if (!particle) {
236
- continue;
237
- }
238
-
239
- if (particle.alpha < 0.01 || particle.size <= 0.5) {
240
- continue;
241
- }
242
-
243
- this._effect.update(this._ctx!, particle);
244
- }
245
- }
246
- }
247
-
248
- //
249
- // Effects
250
- //
251
-
252
- type Particle = {
253
- x: number;
254
- y: number;
255
- vx: number;
256
- vy: number;
257
- size: number;
258
- color: (string | number)[];
259
- alpha: number;
260
- theta?: number;
261
- drag?: number;
262
- wander?: number;
263
- };
264
-
265
- abstract class Effect {
266
- constructor(protected readonly _options: BlastOptions) {}
267
- abstract create(x: number, y: number, color: Particle['color']): Particle;
268
- abstract update(ctx: CanvasRenderingContext2D, particle: Particle): void;
269
- }
270
-
271
- class Effect1 extends Effect {
272
- create(x: number, y: number, color: Particle['color']) {
273
- return {
274
- x,
275
- y: y + 10,
276
- vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
277
- vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
278
- size: random(this._options.particleSize.min, this._options.particleSize.max),
279
- color,
280
- alpha: 1,
281
- };
282
- }
283
-
284
- update(ctx: CanvasRenderingContext2D, particle: Particle) {
285
- particle.vy += this._options.particleGravity;
286
- particle.x += particle.vx;
287
- particle.y += particle.vy;
288
- particle.alpha *= this._options.particleAlphaFadeout;
289
-
290
- ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
291
- ctx.fillRect(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, particle.size);
292
- }
293
- }
294
-
295
- /**
296
- * Based on Soulwire's demo.
297
- * http://codepen.io/soulwire/pen/foktm
298
- */
299
- class Effect2 extends Effect {
300
- create(x: number, y: number, color: Particle['color']) {
301
- return {
302
- x,
303
- y: y + 10,
304
- vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
305
- vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
306
- size: random(this._options.particleSize.min, this._options.particleSize.max),
307
- color,
308
- alpha: 1,
309
- theta: (random(0, 360) * Math.PI) / 180,
310
- drag: 0.92,
311
- wander: 0.15,
312
- };
313
- }
314
-
315
- update(ctx: CanvasRenderingContext2D, particle: Particle) {
316
- particle.vy += this._options.particleGravity;
317
- particle.x += particle.vx;
318
- particle.y += particle.vy;
319
- particle.vx *= particle.drag!;
320
- particle.vy *= particle.drag!;
321
- particle.theta! += random(-0.5, 0.5);
322
- particle.vx += Math.sin(particle.theta!) * 0.1;
323
- particle.vy += Math.cos(particle.theta!) * 0.1;
324
- particle.size *= this._options.particleShrinkRate;
325
- particle.alpha *= this._options.particleAlphaFadeout;
326
-
327
- ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
328
- ctx.beginPath();
329
- ctx.arc(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, 0, 2 * Math.PI);
330
- ctx.fill();
331
- }
332
- }
333
-
334
- //
335
- // Utils
336
- //
337
-
338
- function throttle<T>(callback: (arg: T) => void, limit: number): (arg: T) => void {
339
- let wait = false;
340
- return function (...args: any[]) {
341
- if (!wait) {
342
- // @ts-ignore
343
- callback.apply(this, args);
344
- wait = true;
345
- setTimeout(() => {
346
- wait = false;
347
- }, limit);
348
- }
349
- };
350
- }
351
-
352
- const getRGBComponents = (node: Element, color: BlastOptions['color']): Particle['color'] => {
353
- if (typeof color === 'function') {
354
- return color();
355
- }
356
-
357
- const bgColor = getComputedStyle(node).color;
358
- if (bgColor) {
359
- const x = bgColor.match(/(\d+), (\d+), (\d+)/)?.slice(1);
360
- if (x) {
361
- return x;
362
- }
363
- }
364
-
365
- return [50, 50, 50];
366
- };
367
-
368
- const random = (min: number, max: number) => {
369
- if (!max) {
370
- max = min;
371
- min = 0;
372
- }
373
-
374
- return min + ~~(Math.random() * (max - min + 1));
375
- };
@@ -1,68 +0,0 @@
1
- //
2
- // Copyright 2023 DXOS.org
3
- //
4
-
5
- import { syntaxTree } from '@codemirror/language';
6
- import {
7
- type EditorState,
8
- type Extension,
9
- type RangeSet,
10
- RangeSetBuilder,
11
- StateField,
12
- type Transaction,
13
- } from '@codemirror/state';
14
- import { Decoration, EditorView, WidgetType } from '@codemirror/view';
15
-
16
- export type ImageOptions = {};
17
-
18
- export const image = (options: ImageOptions = {}): Extension => {
19
- return StateField.define<RangeSet<any>>({
20
- create: (state) => update(state, options),
21
- update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
22
- provide: (field) => EditorView.decorations.from(field),
23
- });
24
- };
25
-
26
- const update = (state: EditorState, options: ImageOptions) => {
27
- const builder = new RangeSetBuilder();
28
-
29
- // TODO(burdon): If not-readonly then show text.
30
- // if (state.readOnly) {
31
- syntaxTree(state).iterate({
32
- enter: (node) => {
33
- if (node.name === 'Image') {
34
- const urlNode = node.node.getChild('URL');
35
- if (urlNode) {
36
- const url = state.sliceDoc(urlNode.from, urlNode.to);
37
- builder.add(
38
- state.readOnly ? node.from : node.to,
39
- node.to,
40
- Decoration.widget({
41
- widget: new ImageWidget(url),
42
- }),
43
- );
44
- }
45
- }
46
- },
47
- });
48
- // }
49
-
50
- return builder.finish();
51
- };
52
-
53
- class ImageWidget extends WidgetType {
54
- constructor(readonly _url: string) {
55
- super();
56
- }
57
-
58
- override eq(other: WidgetType) {
59
- return this._url === (other as any as ImageWidget)?._url;
60
- }
61
-
62
- toDOM(view: EditorView) {
63
- const img = document.createElement('img');
64
- img.setAttribute('src', this._url);
65
- img.setAttribute('class', 'cm-image');
66
- return img;
67
- }
68
- }
@@ -1,31 +0,0 @@
1
- //
2
- // Copyright 2023 DXOS.org
3
- //
4
-
5
- import { autocompletion, type CompletionContext, type CompletionResult } from '@codemirror/autocomplete';
6
- import type { Extension } from '@codemirror/state';
7
-
8
- export type MentionOptions = {
9
- onSearch: (text: string) => string[];
10
- };
11
-
12
- export const mention = ({ onSearch }: MentionOptions): Extension => {
13
- return autocompletion({
14
- closeOnBlur: false, // For debugging.
15
- override: [
16
- (context: CompletionContext): CompletionResult | null => {
17
- const match = context.matchBefore(/@(\w+)?/);
18
- if (!match || (match.from === match.to && !context.explicit)) {
19
- return null;
20
- }
21
-
22
- console.log(match);
23
-
24
- return {
25
- from: match.from,
26
- options: onSearch(match.text.slice(1).toLowerCase()).map((value) => ({ label: `@${value}` })),
27
- };
28
- },
29
- ],
30
- });
31
- };
@@ -1,123 +0,0 @@
1
- //
2
- // Copyright 2023 DXOS.org
3
- //
4
-
5
- import { syntaxTree } from '@codemirror/language';
6
- import { type EditorState, type RangeSet, RangeSetBuilder, StateField, type Transaction } from '@codemirror/state';
7
- import { Decoration, EditorView, WidgetType } from '@codemirror/view';
8
-
9
- // TODO(burdon): Snippet to create basic table.
10
- // https://codemirror.net/docs/ref/#autocomplete.snippet
11
-
12
- export type TableOptions = {};
13
-
14
- /**
15
- * GFM tables.
16
- * https://github.github.com/gfm/#tables-extension
17
- */
18
- export const table = (options: TableOptions = {}) => {
19
- return StateField.define<RangeSet<any>>({
20
- create: (state) => update(state, options),
21
- update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
22
- provide: (field) => EditorView.decorations.from(field),
23
- });
24
- };
25
-
26
- type Table = {
27
- from: number;
28
- to?: number;
29
- header?: string[];
30
- rows?: string[][];
31
- };
32
-
33
- const update = (state: EditorState, options: TableOptions) => {
34
- const builder = new RangeSetBuilder();
35
- if (state.readOnly) {
36
- const tables: Table[] = [];
37
- const getTable = () => tables[tables.length - 1];
38
- const getRow = () => {
39
- const table = getTable();
40
- return table.rows?.[table.rows.length - 1];
41
- };
42
-
43
- // Parse table.
44
- // TODO(burdon): Use remark?: https://www.npmjs.com/package/remark-gfm
45
- syntaxTree(state).iterate({
46
- enter: (node) => {
47
- // console.log(node.name);
48
- // Check if cursor is inside text.
49
- switch (node.name) {
50
- case 'Table': {
51
- tables.push({ from: node.from });
52
- break;
53
- }
54
- case 'TableHeader': {
55
- getTable().header = [];
56
- break;
57
- }
58
- case 'TableRow': {
59
- (getTable().rows ??= []).push([]);
60
- break;
61
- }
62
- case 'TableCell': {
63
- const row = getRow();
64
- if (row) {
65
- row.push(state.sliceDoc(node.from, node.to));
66
- } else {
67
- getTable().header?.push(state.sliceDoc(node.from, node.to));
68
- }
69
- break;
70
- }
71
- case 'TableDelimiter': {
72
- getTable().to = node.to;
73
- break;
74
- }
75
- }
76
- },
77
- });
78
-
79
- tables.forEach((table) =>
80
- builder.add(table.from, table.to!, Decoration.replace({ widget: new TableWidget(table) })),
81
- );
82
- }
83
-
84
- return builder.finish();
85
- };
86
-
87
- class TableWidget extends WidgetType {
88
- constructor(readonly _table: Table) {
89
- super();
90
- }
91
-
92
- override eq(other: WidgetType) {
93
- return this._table.from === (other as any as TableWidget)?._table?.from;
94
- }
95
-
96
- toDOM(view: EditorView) {
97
- const table = document.createElement('table');
98
-
99
- {
100
- const header = table.appendChild(document.createElement('thead'));
101
- const row = header.appendChild(document.createElement('tr'));
102
- this._table.header?.forEach((cell) => {
103
- const th = document.createElement('th');
104
- th.setAttribute('class', 'cm-table-head');
105
- row.appendChild(th).textContent = cell;
106
- });
107
- }
108
-
109
- {
110
- const body = table.appendChild(document.createElement('tbody'));
111
- this._table.rows?.forEach((row) => {
112
- const tr = body.appendChild(document.createElement('tr'));
113
- row.forEach((cell) => {
114
- const td = document.createElement('td');
115
- td.setAttribute('class', 'cm-table-cell');
116
- tr.appendChild(td).textContent = cell;
117
- });
118
- });
119
- }
120
-
121
- return table;
122
- }
123
- }