Repository navigation
[lexical-y][lexical-website] Documentation Update: Scope D99, decide D95, record #9589 and the #9725 text entity limit #9727
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9fcda31
cd6fd8d
8bbab36
c3ee11c
d059e5f
c3ce36c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| /** | ||
| * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| * | ||
| */ | ||
|
|
||
| // @vitest-environment node | ||
|
|
||
| import { | ||
| $createHashtagNode, | ||
| HashtagExtension, | ||
| HashtagNode, | ||
| } from '@lexical/hashtag'; | ||
| import {registerLexicalTextEntity} from '@lexical/text'; | ||
| import { | ||
| $create, | ||
| $createParagraphNode, | ||
| $createTextNode, | ||
| $getRoot, | ||
| $getState, | ||
| $setState, | ||
| createEditor, | ||
| createState, | ||
| TextNode, | ||
| } from 'lexical'; | ||
| import {expect, test} from 'vitest'; | ||
|
|
||
| class Letter extends TextNode { | ||
| $config() { | ||
| return this.config('letter', {extends: TextNode}); | ||
| } | ||
| isTextEntity(): true { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| const note = createState('note', { | ||
| parse: value => (typeof value === 'string' ? value : ''), | ||
| }); | ||
|
|
||
| // #9725 (known limit, see transforms.md): typing into a run of repeated | ||
| // characters, read as text inserted at the end of the run, drops the NodeState | ||
| // of the last existing character when a matcher that looks back into the run | ||
| // makes it an entity. `test.fails` until the edit's reading is fixed. | ||
|
|
||
| // Matches an "A" only when "$AA" comes right before it. | ||
| // eslint-disable-next-line compat/compat -- a Node test | ||
| const LOOKBEHIND = /^x$|^A$|(?<=^#)x|(?<=\$AA)A/; | ||
| const getMatch = (text: string) => { | ||
| const m = LOOKBEHIND.exec(text); | ||
| return m ? {end: m.index + m[0].length, start: m.index} : null; | ||
| }; | ||
|
|
||
| // Types an "A" into "$AA" right after "$" and returns the characters with | ||
| // their NodeState. | ||
| const typeIntoRun = () => { | ||
| const editor = createEditor({ | ||
| nodes: [HashtagNode, Letter], | ||
| onError: error => { | ||
| throw error; | ||
| }, | ||
| }); | ||
| registerLexicalTextEntity( | ||
| editor, | ||
| HashtagExtension.config!.getHashtagMatch, | ||
| HashtagNode, | ||
| node => $createHashtagNode(node.getTextContent()), | ||
| ); | ||
| registerLexicalTextEntity(editor, getMatch, Letter, node => | ||
| $create(Letter).setTextContent(node.getTextContent()), | ||
| ); | ||
| editor.update( | ||
| () => { | ||
| $getRoot().append( | ||
| $createParagraphNode().append( | ||
| $createTextNode('y').setFormat('bold'), | ||
| $createTextNode('a'), | ||
| $createHashtagNode('#x'), | ||
| $setState($createTextNode('$AA'), note, 'c1'), | ||
| ), | ||
| ); | ||
| }, | ||
| {discrete: true}, | ||
| ); | ||
| editor.update( | ||
| () => { | ||
| $getRoot() | ||
| .getAllTextNodes() | ||
| .find(node => node.getTextContent() === '$AA')! | ||
| .select(1, 1) | ||
| .insertText('A'); | ||
| }, | ||
| {discrete: true}, | ||
| ); | ||
| const nodes = editor.read(() => | ||
| $getRoot() | ||
| .getAllTextNodes() | ||
| .map(node => ({ | ||
| note: $getState(node, note), | ||
| text: node.getTextContent(), | ||
| type: node.getType(), | ||
| })), | ||
| ); | ||
| const chars = nodes.flatMap(node => | ||
| node.text.split('').map(text => ({note: node.note, text})), | ||
| ); | ||
| return chars; | ||
| }; | ||
|
|
||
| test('typing into a run of repeated characters keeps the text', () => { | ||
| // the typed A is at index 5 | ||
| expect( | ||
| typeIntoRun() | ||
| .map(c => c.text) | ||
| .join(''), | ||
| ).toBe('ya#x$AAA'); | ||
| }); | ||
|
|
||
| test.fails( | ||
| 'typing into a run of repeated characters keeps NodeState on an existing character', | ||
| () => { | ||
| // the A that was last before typing is at index 7 | ||
| expect(typeIntoRun()[7]).toEqual({note: 'c1', text: 'A'}); | ||
| }, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -193,6 +193,12 @@ registerLexicalTextEntity<T extends TextNode>( | |
| ): Array<() => void>; | ||
| ``` | ||
|
|
||
| Known limit: a matcher that looks back into a run of repeated characters | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| (such as `/(?<=\$AA)A/`, an `A` right after `$AA`) can drop the NodeState of | ||
| one existing character when typing extends the run, because the edit is read | ||
| as text inserted at the end of the run | ||
| ([#9725](https://github.com/meta/lexical/issues/9725)). | ||
|
Comment on lines
+196
to
+200
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional: |
||
|
|
||
| ## Examples | ||
|
|
||
| 1. [Emojis](https://github.com/meta/lexical/blob/main/packages/lexical-playground/src/plugins/EmojisExtension/index.ts) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: this single-client case reproduces deterministically with one client (bold
abcd, type plainZaftera, boldaZ, type plainYbetweencandd, undo three times:dloses its bold, gc on and off), and no test pins it. Atest.failsbesideFormatRuleTrade.test.tswould guard the behavior documented here.