@lexical/markdown 0.44.1-nightly.20260519.0 → 0.45.1-dev.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.
Files changed (29) hide show
  1. package/{LexicalMarkdown.dev.js → dist/LexicalMarkdown.dev.js} +1 -1
  2. package/{LexicalMarkdown.dev.mjs → dist/LexicalMarkdown.dev.mjs} +1 -1
  3. package/dist/LexicalMarkdown.prod.js +9 -0
  4. package/dist/LexicalMarkdown.prod.mjs +9 -0
  5. package/package.json +37 -22
  6. package/src/MarkdownExport.ts +627 -0
  7. package/src/MarkdownImport.ts +363 -0
  8. package/src/MarkdownShortcuts.ts +677 -0
  9. package/src/MarkdownTransformers.ts +962 -0
  10. package/src/importTextFormatTransformer.ts +389 -0
  11. package/src/importTextMatchTransformer.ts +110 -0
  12. package/src/importTextTransformers.ts +141 -0
  13. package/src/index.ts +138 -0
  14. package/src/utils.ts +472 -0
  15. package/LexicalMarkdown.prod.js +0 -9
  16. package/LexicalMarkdown.prod.mjs +0 -9
  17. /package/{LexicalMarkdown.js → dist/LexicalMarkdown.js} +0 -0
  18. /package/{LexicalMarkdown.js.flow → dist/LexicalMarkdown.js.flow} +0 -0
  19. /package/{LexicalMarkdown.mjs → dist/LexicalMarkdown.mjs} +0 -0
  20. /package/{LexicalMarkdown.node.mjs → dist/LexicalMarkdown.node.mjs} +0 -0
  21. /package/{MarkdownExport.d.ts → dist/MarkdownExport.d.ts} +0 -0
  22. /package/{MarkdownImport.d.ts → dist/MarkdownImport.d.ts} +0 -0
  23. /package/{MarkdownShortcuts.d.ts → dist/MarkdownShortcuts.d.ts} +0 -0
  24. /package/{MarkdownTransformers.d.ts → dist/MarkdownTransformers.d.ts} +0 -0
  25. /package/{importTextFormatTransformer.d.ts → dist/importTextFormatTransformer.d.ts} +0 -0
  26. /package/{importTextMatchTransformer.d.ts → dist/importTextMatchTransformer.d.ts} +0 -0
  27. /package/{importTextTransformers.d.ts → dist/importTextTransformers.d.ts} +0 -0
  28. /package/{index.d.ts → dist/index.d.ts} +0 -0
  29. /package/{utils.d.ts → dist/utils.d.ts} +0 -0
@@ -0,0 +1,627 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import type {
10
+ BaseSelection,
11
+ ElementNode,
12
+ LexicalNode,
13
+ LineBreakNode,
14
+ TextFormatType,
15
+ TextNode,
16
+ } from 'lexical';
17
+
18
+ import {$sliceSelectedTextNodeContent} from '@lexical/selection';
19
+ import {
20
+ $getRoot,
21
+ $getState,
22
+ $isDecoratorNode,
23
+ $isElementNode,
24
+ $isLineBreakNode,
25
+ $isTextNode,
26
+ } from 'lexical';
27
+
28
+ import {
29
+ ElementTransformer,
30
+ hardLineBreakState,
31
+ MultilineElementTransformer,
32
+ TextFormatTransformer,
33
+ TextMatchTransformer,
34
+ Transformer,
35
+ } from './MarkdownTransformers';
36
+ import {isEmptyParagraph, transformersByType} from './utils';
37
+
38
+ /**
39
+ * Renders string from markdown. The selection is moved to the start after the operation.
40
+ */
41
+ export function createMarkdownExport(
42
+ transformers: Array<Transformer>,
43
+ shouldPreserveNewLines: boolean = false,
44
+ ): (node?: ElementNode) => string {
45
+ const byType = transformersByType(transformers);
46
+ const elementTransformers = [...byType.multilineElement, ...byType.element];
47
+ const isNewlineDelimited = !shouldPreserveNewLines;
48
+
49
+ // Export only uses text formats that are responsible for single format
50
+ // e.g. it will filter out *** (bold, italic) and instead use separate ** and *
51
+ const textFormatTransformers = byType.textFormat
52
+ .filter(transformer => transformer.format.length === 1)
53
+ // Make sure all text transformers that contain 'code' in their format are at the end of the array. Otherwise, formatted code like
54
+ // <strong><code>code</code></strong> will be exported as `**Bold Code**`, as the code format will be applied first, and the bold format
55
+ // will be applied second and thus skipped entirely, as the code format will prevent any further formatting.
56
+ .sort((a, b) => {
57
+ return (
58
+ Number(a.format.includes('code')) - Number(b.format.includes('code'))
59
+ );
60
+ });
61
+
62
+ return node => {
63
+ const output = [];
64
+ const children = (node || $getRoot()).getChildren();
65
+
66
+ for (let i = 0; i < children.length; i++) {
67
+ const child = children[i];
68
+ const result = $exportTopLevelElements(
69
+ child,
70
+ elementTransformers,
71
+ textFormatTransformers,
72
+ byType.textMatch,
73
+ shouldPreserveNewLines,
74
+ );
75
+
76
+ if (result != null) {
77
+ output.push(
78
+ // separate consecutive group of texts with a line break: eg. ["hello", "world"] -> ["hello", "/nworld"]
79
+ isNewlineDelimited &&
80
+ i > 0 &&
81
+ !isEmptyParagraph(child) &&
82
+ !isEmptyParagraph(children[i - 1])
83
+ ? '\n'.concat(result)
84
+ : result,
85
+ );
86
+ }
87
+ }
88
+ // Ensure consecutive groups of texts are at least \n\n apart while each empty paragraph render as a newline.
89
+ // Eg. ["hello", "", "", "hi", "\nworld"] -> "hello\n\n\nhi\n\nworld"
90
+ return output.join('\n');
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Creates a markdown export function that only exports selected content.
96
+ * Uses a recursive structure similar to $appendNodesToHTML to support
97
+ * extractWithChild for proper handling of partial selections within
98
+ * inline elements like links.
99
+ */
100
+ export function createSelectionMarkdownExport(
101
+ transformers: Transformer[],
102
+ shouldPreserveNewLines: boolean = false,
103
+ ): (selection: BaseSelection) => string {
104
+ const byType = transformersByType(transformers);
105
+ const elementTransformers = [...byType.multilineElement, ...byType.element];
106
+ const isNewlineDelimited = !shouldPreserveNewLines;
107
+
108
+ const textFormatTransformers = byType.textFormat
109
+ .filter(transformer => transformer.format.length === 1)
110
+ .sort((a, b) => {
111
+ return (
112
+ Number(a.format.includes('code')) - Number(b.format.includes('code'))
113
+ );
114
+ });
115
+
116
+ return selection => {
117
+ const output = [];
118
+ const children = $getRoot().getChildren();
119
+
120
+ for (let i = 0; i < children.length; i++) {
121
+ const child = children[i];
122
+ const {shouldInclude, markdown} = $processNodeForSelection(
123
+ child,
124
+ selection,
125
+ elementTransformers,
126
+ textFormatTransformers,
127
+ byType.textMatch,
128
+ shouldPreserveNewLines,
129
+ );
130
+
131
+ if (shouldInclude && markdown != null) {
132
+ output.push(
133
+ isNewlineDelimited &&
134
+ i > 0 &&
135
+ !isEmptyParagraph(child) &&
136
+ !isEmptyParagraph(children[i - 1])
137
+ ? '\n'.concat(markdown)
138
+ : markdown,
139
+ );
140
+ }
141
+ }
142
+ return output.join('\n');
143
+ };
144
+ }
145
+
146
+ function $processNodeForSelection(
147
+ node: LexicalNode,
148
+ selection: BaseSelection,
149
+ elementTransformers: (ElementTransformer | MultilineElementTransformer)[],
150
+ textFormatTransformers: TextFormatTransformer[],
151
+ textMatchTransformers: TextMatchTransformer[],
152
+ shouldPreserveNewLines: boolean,
153
+ ): {shouldInclude: boolean; markdown: string | null} {
154
+ let shouldInclude = node.isSelected(selection);
155
+
156
+ // For element transformers (heading, quote, list, code block, etc.)
157
+ for (const transformer of elementTransformers) {
158
+ if (!transformer.export) {
159
+ continue;
160
+ }
161
+ const result = transformer.export(
162
+ node,
163
+ node_ =>
164
+ $exportChildrenForSelection(
165
+ node_,
166
+ selection,
167
+ textFormatTransformers,
168
+ textMatchTransformers,
169
+ shouldPreserveNewLines,
170
+ ).markdown,
171
+ selection,
172
+ );
173
+
174
+ if (result != null) {
175
+ if (!shouldInclude) {
176
+ // Check if any descendant is selected
177
+ if ($isElementNode(node)) {
178
+ const childResult = $exportChildrenForSelection(
179
+ node,
180
+ selection,
181
+ textFormatTransformers,
182
+ textMatchTransformers,
183
+ shouldPreserveNewLines,
184
+ );
185
+ if (childResult.shouldInclude) {
186
+ shouldInclude = true;
187
+ }
188
+ }
189
+ }
190
+ return {markdown: result, shouldInclude};
191
+ }
192
+ }
193
+
194
+ if ($isElementNode(node)) {
195
+ const childResult = $exportChildrenForSelection(
196
+ node,
197
+ selection,
198
+ textFormatTransformers,
199
+ textMatchTransformers,
200
+ shouldPreserveNewLines,
201
+ );
202
+ return {
203
+ markdown: childResult.markdown,
204
+ shouldInclude: shouldInclude || childResult.shouldInclude,
205
+ };
206
+ } else if ($isDecoratorNode(node)) {
207
+ return {markdown: node.getTextContent(), shouldInclude};
208
+ } else {
209
+ return {markdown: null, shouldInclude};
210
+ }
211
+ }
212
+
213
+ function $exportChildrenForSelection(
214
+ node: ElementNode,
215
+ selection: BaseSelection,
216
+ textFormatTransformers: TextFormatTransformer[],
217
+ textMatchTransformers: TextMatchTransformer[],
218
+ shouldPreserveNewLines: boolean,
219
+ unclosedTags?: {format: TextFormatType; tag: string}[],
220
+ unclosableTags?: {format: TextFormatType; tag: string}[],
221
+ ): {shouldInclude: boolean; markdown: string} {
222
+ const output = [];
223
+ const children = node.getChildren();
224
+ let anyChildIncluded = false;
225
+
226
+ if (!unclosedTags) {
227
+ unclosedTags = [];
228
+ }
229
+ if (!unclosableTags) {
230
+ unclosableTags = [];
231
+ }
232
+
233
+ mainLoop: for (const child of children) {
234
+ let childIncluded = child.isSelected(selection);
235
+
236
+ // Try text match transformers (links, etc.)
237
+ for (const transformer of textMatchTransformers) {
238
+ if (!transformer.export) {
239
+ continue;
240
+ }
241
+
242
+ const result = transformer.export(
243
+ child,
244
+ parentNode =>
245
+ $exportChildrenForSelection(
246
+ parentNode,
247
+ selection,
248
+ textFormatTransformers,
249
+ textMatchTransformers,
250
+ shouldPreserveNewLines,
251
+ unclosedTags,
252
+ [...unclosableTags, ...unclosedTags],
253
+ ).markdown,
254
+ (textNode, textContent) => {
255
+ const slicedNode = $sliceSelectedTextNodeContent(
256
+ selection,
257
+ textNode,
258
+ 'clone',
259
+ );
260
+ return exportTextFormat(
261
+ textNode,
262
+ slicedNode.getTextContent(),
263
+ textFormatTransformers,
264
+ unclosedTags,
265
+ unclosableTags,
266
+ shouldPreserveNewLines,
267
+ );
268
+ },
269
+ );
270
+
271
+ if (result != null) {
272
+ // Check extractWithChild if this node wasn't directly selected
273
+ if (
274
+ !childIncluded &&
275
+ $isElementNode(child) &&
276
+ child.getChildren().some(c => c.isSelected(selection)) &&
277
+ child.extractWithChild(child, selection, 'html')
278
+ ) {
279
+ childIncluded = true;
280
+ }
281
+ if (childIncluded) {
282
+ output.push(result);
283
+ anyChildIncluded = true;
284
+ }
285
+ continue mainLoop;
286
+ }
287
+ }
288
+
289
+ if ($isLineBreakNode(child)) {
290
+ if (childIncluded) {
291
+ output.push($exportLineBreak(child));
292
+ anyChildIncluded = true;
293
+ }
294
+ } else if ($isTextNode(child)) {
295
+ if (childIncluded) {
296
+ const target = $sliceSelectedTextNodeContent(selection, child, 'clone');
297
+ output.push(
298
+ exportTextFormat(
299
+ child,
300
+ target.getTextContent(),
301
+ textFormatTransformers,
302
+ unclosedTags,
303
+ unclosableTags,
304
+ shouldPreserveNewLines,
305
+ ),
306
+ );
307
+ anyChildIncluded = true;
308
+ }
309
+ } else if ($isElementNode(child)) {
310
+ const childResult = $exportChildrenForSelection(
311
+ child,
312
+ selection,
313
+ textFormatTransformers,
314
+ textMatchTransformers,
315
+ shouldPreserveNewLines,
316
+ unclosedTags,
317
+ unclosableTags,
318
+ );
319
+
320
+ // extractWithChild: if child has selected descendants, ask parent if it should be included
321
+ if (
322
+ !childIncluded &&
323
+ childResult.shouldInclude &&
324
+ child.extractWithChild(child, selection, 'html')
325
+ ) {
326
+ childIncluded = true;
327
+ }
328
+
329
+ if (childIncluded || childResult.shouldInclude) {
330
+ output.push(childResult.markdown);
331
+ anyChildIncluded = true;
332
+ }
333
+ } else if ($isDecoratorNode(child)) {
334
+ if (childIncluded) {
335
+ output.push(child.getTextContent());
336
+ anyChildIncluded = true;
337
+ }
338
+ }
339
+ }
340
+
341
+ return {markdown: output.join(''), shouldInclude: anyChildIncluded};
342
+ }
343
+
344
+ function $exportTopLevelElements(
345
+ node: LexicalNode,
346
+ elementTransformers: Array<ElementTransformer | MultilineElementTransformer>,
347
+ textTransformersIndex: Array<TextFormatTransformer>,
348
+ textMatchTransformers: Array<TextMatchTransformer>,
349
+ shouldPreserveNewLines: boolean,
350
+ ): string | null {
351
+ for (const transformer of elementTransformers) {
352
+ if (!transformer.export) {
353
+ continue;
354
+ }
355
+ const result = transformer.export(node, _node =>
356
+ $exportChildren(
357
+ _node,
358
+ textTransformersIndex,
359
+ textMatchTransformers,
360
+ undefined,
361
+ undefined,
362
+ shouldPreserveNewLines,
363
+ ),
364
+ );
365
+
366
+ if (result != null) {
367
+ return result;
368
+ }
369
+ }
370
+
371
+ if ($isElementNode(node)) {
372
+ return $exportChildren(
373
+ node,
374
+ textTransformersIndex,
375
+ textMatchTransformers,
376
+ undefined,
377
+ undefined,
378
+ shouldPreserveNewLines,
379
+ );
380
+ } else if ($isDecoratorNode(node)) {
381
+ return node.getTextContent();
382
+ } else {
383
+ return null;
384
+ }
385
+ }
386
+
387
+ function $exportChildren(
388
+ node: ElementNode,
389
+ textTransformersIndex: Array<TextFormatTransformer>,
390
+ textMatchTransformers: Array<TextMatchTransformer>,
391
+ unclosedTags?: Array<{format: TextFormatType; tag: string}>,
392
+ unclosableTags?: Array<{format: TextFormatType; tag: string}>,
393
+ shouldPreserveNewLines: boolean = false,
394
+ ): string {
395
+ const output = [];
396
+ const children = node.getChildren();
397
+ // keep track of unclosed tags from the very beginning
398
+ if (!unclosedTags) {
399
+ unclosedTags = [];
400
+ }
401
+ if (!unclosableTags) {
402
+ unclosableTags = [];
403
+ }
404
+
405
+ mainLoop: for (const child of children) {
406
+ for (const transformer of textMatchTransformers) {
407
+ if (!transformer.export) {
408
+ continue;
409
+ }
410
+
411
+ const result = transformer.export(
412
+ child,
413
+ parentNode =>
414
+ $exportChildren(
415
+ parentNode,
416
+ textTransformersIndex,
417
+ textMatchTransformers,
418
+ unclosedTags,
419
+ // Add current unclosed tags to the list of unclosable tags - we don't want nested tags from
420
+ // textmatch transformers to close the outer ones, as that may result in invalid markdown.
421
+ // E.g. **text [text**](https://lexical.io)
422
+ // is invalid markdown, as the closing ** is inside the link.
423
+ //
424
+ [...unclosableTags, ...unclosedTags],
425
+ shouldPreserveNewLines,
426
+ ),
427
+ (textNode, textContent) =>
428
+ exportTextFormat(
429
+ textNode,
430
+ textContent,
431
+ textTransformersIndex,
432
+ unclosedTags,
433
+ unclosableTags,
434
+ shouldPreserveNewLines,
435
+ ),
436
+ );
437
+
438
+ if (result != null) {
439
+ output.push(result);
440
+ continue mainLoop;
441
+ }
442
+ }
443
+
444
+ if ($isLineBreakNode(child)) {
445
+ output.push($exportLineBreak(child));
446
+ } else if ($isTextNode(child)) {
447
+ output.push(
448
+ exportTextFormat(
449
+ child,
450
+ child.getTextContent(),
451
+ textTransformersIndex,
452
+ unclosedTags,
453
+ unclosableTags,
454
+ shouldPreserveNewLines,
455
+ ),
456
+ );
457
+ } else if ($isElementNode(child)) {
458
+ // empty paragraph returns ""
459
+ output.push(
460
+ $exportChildren(
461
+ child,
462
+ textTransformersIndex,
463
+ textMatchTransformers,
464
+ unclosedTags,
465
+ unclosableTags,
466
+ shouldPreserveNewLines,
467
+ ),
468
+ );
469
+ } else if ($isDecoratorNode(child)) {
470
+ output.push(child.getTextContent());
471
+ }
472
+ }
473
+
474
+ return output.join('');
475
+ }
476
+
477
+ function $exportLineBreak(node: LineBreakNode): string {
478
+ return $getState(node, hardLineBreakState) + '\n';
479
+ }
480
+
481
+ function exportTextFormat(
482
+ node: TextNode,
483
+ textContent: string,
484
+ textTransformers: Array<TextFormatTransformer>,
485
+ // unclosed tags include the markdown tags that haven't been closed yet, and their associated formats
486
+ unclosedTags: Array<{format: TextFormatType; tag: string}>,
487
+ unclosableTags?: Array<{format: TextFormatType; tag: string}>,
488
+ shouldPreserveNewLines: boolean = false,
489
+ ): string {
490
+ // This function handles the case of a string looking like this: " foo "
491
+ // Where it would be invalid markdown to generate: "** foo **"
492
+ // If the node has no format, we use the original text.
493
+ // Otherwise, we escape leading and trailing whitespaces to their corresponding code points,
494
+ // ensuring the returned string maintains its original formatting, e.g., "**&#32;&#32;&#32;foo&#32;&#32;&#32;**".
495
+
496
+ let output = textContent;
497
+ if (!node.hasFormat('code')) {
498
+ // Preserve literal backslashes when preserving source newlines.
499
+ output = shouldPreserveNewLines
500
+ ? output.replace(/([*_`~])/g, '\\$1')
501
+ : output.replace(/([*_`~\\])/g, '\\$1');
502
+ }
503
+
504
+ // Extract leading and trailing whitespaces.
505
+ // CommonMark flanking rules require formatting tags to be adjacent to non-whitespace characters.
506
+ const match = output.match(/^(\s*)(.*?)(\s*)$/s) || ['', '', output, ''];
507
+ const leadingSpace = match[1];
508
+ const trimmedOutput = match[2];
509
+ const trailingSpace = match[3];
510
+ const isWhitespaceOnly = trimmedOutput === '';
511
+
512
+ // the opening tags to be added to the result
513
+ let openingTags = '';
514
+ // the closing tags to be added to the result
515
+ let closingTagsBefore = '';
516
+ let closingTagsAfter = '';
517
+
518
+ const prevNode = getTextSibling(node, true);
519
+ const nextNode = getTextSibling(node, false);
520
+
521
+ const applied = new Set();
522
+
523
+ for (const transformer of textTransformers) {
524
+ const format = transformer.format[0];
525
+ const tag = transformer.tag;
526
+
527
+ // dedup applied formats
528
+ if (checkHasFormat(node, format) && !applied.has(format)) {
529
+ applied.add(format);
530
+
531
+ // append the tag to openingTags, if it's not applied to the previous nodes,
532
+ // or the nodes before that (which would result in an unclosed tag)
533
+ if (
534
+ !checkHasFormat(prevNode, format) ||
535
+ !unclosedTags.find(element => element.tag === tag)
536
+ ) {
537
+ unclosedTags.push({format, tag});
538
+ openingTags += tag;
539
+ }
540
+ }
541
+ }
542
+
543
+ // close any tags in the same order they were applied, if necessary
544
+ for (let i = 0; i < unclosedTags.length; i++) {
545
+ const nodeHasFormat = hasFormat(node, unclosedTags[i].format);
546
+ const nextNodeHasFormat = hasFormat(nextNode, unclosedTags[i].format);
547
+
548
+ // prevent adding closing tag if next sibling will do it
549
+ if (nodeHasFormat && nextNodeHasFormat) {
550
+ continue;
551
+ }
552
+
553
+ const unhandledUnclosedTags = [...unclosedTags]; // Shallow copy to avoid modifying the original array
554
+
555
+ while (unhandledUnclosedTags.length > i) {
556
+ const unclosedTag = unhandledUnclosedTags.pop();
557
+
558
+ // If tag is unclosable, don't close it and leave it in the original array,
559
+ // So that it can be closed when it's no longer unclosable
560
+ if (
561
+ unclosableTags &&
562
+ unclosedTag &&
563
+ unclosableTags.find(element => element.tag === unclosedTag.tag)
564
+ ) {
565
+ continue;
566
+ }
567
+
568
+ if (unclosedTag && typeof unclosedTag.tag === 'string') {
569
+ if (!nodeHasFormat) {
570
+ // Handles cases where the tag has not been closed before, e.g. if the previous node
571
+ // was a text match transformer that did not account for closing tags of the next node (e.g. a link)
572
+ closingTagsBefore += unclosedTag.tag;
573
+ } else if (!nextNodeHasFormat) {
574
+ closingTagsAfter += unclosedTag.tag;
575
+ }
576
+ }
577
+ // Mutate the original array to remove the closed tag
578
+ unclosedTags.pop();
579
+ }
580
+ break;
581
+ }
582
+ // If the node is entirely whitespace, we don't apply opening/closing tags around it.
583
+ // However, it must still output closing tags from previous nodes.
584
+ if (isWhitespaceOnly && !node.hasFormat('code')) {
585
+ return closingTagsBefore + output;
586
+ }
587
+
588
+ // Flanking Compliance: Notice how openingTags and closingTagsAfter are placed INSIDE the whitespace boundaries!
589
+ return (
590
+ closingTagsBefore +
591
+ leadingSpace +
592
+ openingTags +
593
+ trimmedOutput +
594
+ closingTagsAfter +
595
+ trailingSpace
596
+ );
597
+ }
598
+
599
+ function getTextSibling(node: TextNode, backward: boolean): TextNode | null {
600
+ const sibling = backward ? node.getPreviousSibling() : node.getNextSibling();
601
+
602
+ if ($isTextNode(sibling)) {
603
+ return sibling;
604
+ }
605
+
606
+ return null;
607
+ }
608
+
609
+ function hasFormat(
610
+ node: LexicalNode | null | undefined,
611
+ format: TextFormatType,
612
+ ): boolean {
613
+ return $isTextNode(node) && node.hasFormat(format);
614
+ }
615
+
616
+ function checkHasFormat(n: TextNode | null, f: TextFormatType): boolean {
617
+ if (!hasFormat(n, f)) {
618
+ return false;
619
+ }
620
+ if (f === 'code') {
621
+ return true;
622
+ }
623
+ if (n && /^\s*$/.test(n.getTextContent())) {
624
+ return false;
625
+ }
626
+ return true;
627
+ }