From 3bb52993ea2a9420baddf6371cdd6cfaf42b72ea Mon Sep 17 00:00:00 2001 From: yu Date: Fri, 13 Jun 2025 16:05:01 +0800 Subject: [PATCH 1/5] fix: comments --- .npmrc | 1 + .../src/LexicalAutoLinkPlugin.ts | 36 ++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/.npmrc b/.npmrc index b6c243d6f36..0055b21aa17 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,3 @@ legacy-peer-deps=true init-module=./scripts/npm/npm-init.js +registry=https://registry.npmjs.org/ diff --git a/packages/lexical-react/src/LexicalAutoLinkPlugin.ts b/packages/lexical-react/src/LexicalAutoLinkPlugin.ts index 01ffdbe0eaa..35641faf16e 100644 --- a/packages/lexical-react/src/LexicalAutoLinkPlugin.ts +++ b/packages/lexical-react/src/LexicalAutoLinkPlugin.ts @@ -77,7 +77,7 @@ function findFirstMatch( return null; } - +// TODO yu 这里仅包含英文的分隔字符,应该增加更多语言,可以优化为可配置项,以支持传入 const PUNCTUATION_OR_SPACE = /[.,;\s]/; function isSeparator(char: string): boolean { @@ -177,7 +177,7 @@ function extractMatchingNodes( const currentNodeLength = currentNodeText.length; const currentNodeStart = currentOffset; const currentNodeEnd = currentOffset + currentNodeLength; - + // 完全在匹配文本前后的文本节点放入 unmodified if (currentNodeEnd <= startIndex) { unmodifiedBeforeNodes.push(currentNode); matchingOffset += currentNodeLength; @@ -226,6 +226,7 @@ function $createAutoLinkNode_( const firstTextNode = nodes[0]; let offset = firstTextNode.getTextContent().length; let firstLinkTextNode; + // 取开头 if (startIndex === 0) { firstLinkTextNode = firstTextNode; } else { @@ -233,16 +234,20 @@ function $createAutoLinkNode_( } const linkNodes = []; let remainingTextNode; + for (let i = 1; i < nodes.length; i++) { const currentNode = nodes[i]; const currentNodeText = currentNode.getTextContent(); const currentNodeLength = currentNodeText.length; const currentNodeStart = offset; const currentNodeEnd = offset + currentNodeLength; + // ? 这里的 if 判断似乎无意义 if (currentNodeStart < endIndex) { if (currentNodeEnd <= endIndex) { + // 取中间部分 linkNodes.push(currentNode); } else { + // 取结尾部分 const [linkTextNode, endNode] = currentNode.splitText( endIndex - currentNodeStart, ); @@ -256,13 +261,16 @@ function $createAutoLinkNode_( const selectedTextNode = selection ? selection.getNodes().find($isTextNode) : undefined; + // 单独对 firstLinkTextNode 做处理 const textNode = $createTextNode(firstLinkTextNode.getTextContent()); textNode.setFormat(firstLinkTextNode.getFormat()); textNode.setDetail(firstLinkTextNode.getDetail()); textNode.setStyle(firstLinkTextNode.getStyle()); linkNode.append(textNode, ...linkNodes); + // ? 从注释看起来如果光标不在 firstLinkTextNode 内,会自动保持光标位置 // it does not preserve caret position if caret was at the first text node // so we need to restore caret position + // 恢复 firstLinkTextNode selection if (selectedTextNode && selectedTextNode === firstLinkTextNode) { if ($isRangeSelection(selection)) { textNode.select(selection.anchor.offset, selection.focus.offset); @@ -293,6 +301,7 @@ function $handleLinkCreation( const matchStart = match.index; const matchLength = match.length; const matchEnd = matchStart + matchLength; + // 确保匹配的区间是一整段完整的词 const isValid = isContentAroundIsValid( invalidMatchEnd + matchStart, invalidMatchEnd + matchEnd, @@ -301,27 +310,35 @@ function $handleLinkCreation( ); if (isValid) { + // matchingOffset 为开头至匹配到的文本节点之前的offset const [matchingOffset, , matchingNodes, unmodifiedAfterNodes] = extractMatchingNodes( currentNodes, invalidMatchEnd + matchStart, invalidMatchEnd + matchEnd, ); - + // actualMatchStart 为匹配到的文本节点中从开头到匹配到的文本开头的offset + // actualMatchStart 为匹配到的文本节点数组从开头到匹配到的文本结尾的offset const actualMatchStart = invalidMatchEnd + matchStart - matchingOffset; const actualMatchEnd = invalidMatchEnd + matchEnd - matchingOffset; + // 简单来说,是把 nodes 的 offset 换成 matchingNodes 的 offset const remainingTextNode = $createAutoLinkNode_( matchingNodes, actualMatchStart, actualMatchEnd, match, ); + // 更新剩余的节点和offset currentNodes = remainingTextNode ? [remainingTextNode, ...unmodifiedAfterNodes] : unmodifiedAfterNodes; onChange(match.url, null); invalidMatchEnd = 0; } else { + // 匹配的文本周围没有分隔开,不满足自动链接的条件 + // ? 没有很清楚的想明白为什么这里可以直接跳过整段匹配的字符串,似乎会产生遗漏情况 + // 若放在场景里考虑,像链接和邮箱这样一大段的内容一般不会有从中间往后的字符可以重新匹配上 + // 以及子片段可以匹配上的情况,符合实际需求 invalidMatchEnd += matchEnd; } @@ -339,6 +356,7 @@ function handleLinkEdit( const childrenLength = children.length; for (let i = 0; i < childrenLength; i++) { const child = children[i]; + // 如果有 child 不为简单 TextNode 说明已失效 if (!$isTextNode(child) || !child.isSimpleText()) { replaceWithChildren(linkNode); onChange(null, linkNode.getURL()); @@ -349,6 +367,7 @@ function handleLinkEdit( // Check text content fully matches const text = linkNode.getTextContent(); const match = findFirstMatch(text, matchers); + // 如果无 match 说明已失效 if (match === null || match.text !== text) { replaceWithChildren(linkNode); onChange(null, linkNode.getURL()); @@ -356,18 +375,21 @@ function handleLinkEdit( } // Check neighbors + // 相邻节点的变更也会使得节点 dirty,所以检查一下 if (!isPreviousNodeValid(linkNode) || !isNextNodeValid(linkNode)) { + // 相邻节点与当前节点间没有有效分隔符 说明已失效 replaceWithChildren(linkNode); onChange(null, linkNode.getURL()); return; } + // 规则仍有效但内容变更了 const url = linkNode.getURL(); if (url !== match.url) { linkNode.setURL(match.url); onChange(match.url, url); } - + // 检查 rel target 属性 if (match.attributes) { const rel = linkNode.getRel(); if (rel !== match.attributes.rel) { @@ -410,6 +432,7 @@ function handleBadNeighbors( !nextSibling.getIsUnlinked() && !endsWithSeparator(text) ) { + // ? 为什么调用 replaceWithChildren 后还要再调用 handleLinkEdit replaceWithChildren(nextSibling); handleLinkEdit(nextSibling, matchers, onChange); onChange(null, nextSibling.getURL()); @@ -472,12 +495,17 @@ function useAutoLink( if ($isAutoLinkNode(parent) && !parent.getIsUnlinked()) { handleLinkEdit(parent, matchers, onChangeWrapped); } else if (!$isLinkNode(parent)) { + // 1.为简单文本节点 + // 2.开头为分隔符或前一个节点非 AutoLinkNode + // 如果前一个节点为 AutoLinkNode ,并且开头不是分隔符,则不需要重复处理 if ( textNode.isSimpleText() && (startsWithSeparator(textNode.getTextContent()) || !$isAutoLinkNode(previous)) ) { + // 获得连续的、无空白字符的几个简单文本节点 const textNodesToMatch = getTextNodesToMatch(textNode); + // 尝试生成 AutoLinkNode $handleLinkCreation(textNodesToMatch, matchers, onChangeWrapped); } From 609d658b73060fbd9c5571f1da641e3c4e4f60b6 Mon Sep 17 00:00:00 2001 From: yu Date: Mon, 30 Jun 2025 22:53:10 +0800 Subject: [PATCH 2/5] fix: comments --- packages/lexical-react/src/shared/useDecorators.tsx | 12 ++++++++++++ packages/lexical/src/LexicalEditor.ts | 1 + packages/lexical/src/LexicalUpdates.ts | 4 ++++ 3 files changed, 17 insertions(+) diff --git a/packages/lexical-react/src/shared/useDecorators.tsx b/packages/lexical-react/src/shared/useDecorators.tsx index aefda99d520..c33fdc45e05 100644 --- a/packages/lexical-react/src/shared/useDecorators.tsx +++ b/packages/lexical-react/src/shared/useDecorators.tsx @@ -34,6 +34,8 @@ export function useDecorators( // Subscribe to changes useLayoutEffect(() => { return editor.registerDecoratorListener((nextDecorators) => { + // 确保状态同步更新 + // 确保在下次绘制前状态已更新 flushSync(() => { setDecorators(nextDecorators); }); @@ -54,14 +56,24 @@ export function useDecorators( for (let i = 0; i < decoratorKeys.length; i++) { const nodeKey = decoratorKeys[i]; + // 渲染元素,来自 DecoratorNode.decorate + // 支持异步组件加载、错误处理 const reactDecorator = ( editor._onError(e)}> {decorators[nodeKey]} ); + // 占位符元素,来自 DecoratorNode.createDOM const element = editor.getElementByKey(nodeKey); if (element !== null) { + // 将渲染元素渲染到占位符元素中,连接 React 组件系统与 Lexical DOM 系统 + // 使用 createPortal 的好处: + // 1. 架构分离:React 组件与 Lexical DOM 解耦 + // 2. 功能完整:保持 React 生态(Hooks、Context、错误边界等) + // 3. 性能优化:渲染隔离,避免不必要的更新 + // 4. 开发体验:使用熟悉的 React 模式,易于调试 + // 5. 扩展性:支持复杂 UI 组件,易于添加新装饰器 decoratedPortals.push(createPortal(reactDecorator, element, nodeKey)); } } diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index f0b3ef52ade..d2704bf2df8 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -1286,6 +1286,7 @@ export class LexicalEditor { * run synchronously. */ update(updateFn: () => void, options?: EditorUpdateOptions): void { + // 当 options.discrete 为 true 时,强制设置为同步执行,否则放入微任务队列异步执行 updateEditor(this, updateFn, options); } diff --git a/packages/lexical/src/LexicalUpdates.ts b/packages/lexical/src/LexicalUpdates.ts index 41b32e7229e..68d08ce1b58 100644 --- a/packages/lexical/src/LexicalUpdates.ts +++ b/packages/lexical/src/LexicalUpdates.ts @@ -859,6 +859,7 @@ function processNestedUpdates( if (options.skipTransforms) { skipTransforms = true; } + // 检测到 discrete 配置,强制设置为同步执行 if (options.discrete) { const pendingEditorState = editor._pendingEditorState; invariant( @@ -914,6 +915,7 @@ function $beginUpdate( ); editorStateWasCloned = true; } + // 关键:将 discrete 配置设置到 pendingEditorState._flushSync 中 pendingEditorState._flushSync = discrete; const previousActiveEditorState = activeEditorState; @@ -1023,9 +1025,11 @@ function $beginUpdate( if (shouldUpdate) { if (pendingEditorState._flushSync) { + // 同步执行:直接调用 $commitPendingUpdates pendingEditorState._flushSync = false; $commitPendingUpdates(editor); } else if (editorStateWasCloned) { + // 异步执行:使用 scheduleMicroTask 延迟执行 scheduleMicroTask(() => { $commitPendingUpdates(editor); }); From bbb9e050b62ad5ae3b035cc90b7ae927382879ff Mon Sep 17 00:00:00 2001 From: yu Date: Mon, 7 Jul 2025 20:49:59 +0800 Subject: [PATCH 3/5] fix: comments --- packages/lexical/src/nodes/LexicalTextNode.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts index 4dbc6781b20..1aec19d51fb 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -922,7 +922,7 @@ export class TextNode extends LexicalNode { * This method is meant to be overridden by TextNode subclasses to control the behavior of those nodes * when a user event would cause text to be inserted before them in the editor. If true, Lexical will attempt * to insert text into this node. If false, it will insert the text in a new sibling node. - * + * * 不将新文本插入到当前节点中,保持节点独立性 * @returns true if text can be inserted before the node, false otherwise. */ canInsertTextBefore(): boolean { @@ -1163,7 +1163,8 @@ export class TextNode extends LexicalNode { * This method is meant to be overridden by TextNode subclasses to control the behavior of those nodes * when used with the registerLexicalTextEntity function. If you're using registerLexicalTextEntity, the * node class that you create and replace matched text with should return true from this method. - * + * * 返回 true 时表示这个 node 是一个整体 + * TODO yu 搞清楚 isTextEntity & registerLexicalTextEntity 的关系 * @returns true if the node is to be treated as a "text entity", false otherwise. */ isTextEntity(): boolean { From 5e73ba96cdde389986afe5bad2ac9ede5d965fd2 Mon Sep 17 00:00:00 2001 From: yu Date: Mon, 14 Jul 2025 16:05:26 +0800 Subject: [PATCH 4/5] fix: add debug --- .eslintrc.js | 7 ++++--- packages/lexical-playground/package.json | 1 + packages/lexical-playground/src/utils/debug.ts | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 packages/lexical-playground/src/utils/debug.ts diff --git a/.eslintrc.js b/.eslintrc.js index 6ede0823d2d..d8b74d421d3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -92,7 +92,8 @@ module.exports = { ERROR, {args: 'none', argsIgnorePattern: '^_', varsIgnorePattern: '^_'}, ], - 'header/header': [2, 'scripts/www/headerTemplate.js'], + // 暂时关闭版权头声明 + 'header/header': [OFF, 'scripts/www/headerTemplate.js'], }, }, { @@ -187,8 +188,8 @@ module.exports = { 'ft-flow/object-type-delimiter': OFF, 'ft-flow/sort-keys': ERROR, - - 'header/header': [2, 'scripts/www/headerTemplate.js'], + // 暂时关闭版权头声明 + 'header/header': [OFF, 'scripts/www/headerTemplate.js'], // (This helps configure simple-import-sort) Make sure all imports are at the top of the file 'import/first': ERROR, diff --git a/packages/lexical-playground/package.json b/packages/lexical-playground/package.json index 509377cb119..b9b32fc5fdc 100644 --- a/packages/lexical-playground/package.json +++ b/packages/lexical-playground/package.json @@ -26,6 +26,7 @@ "@lexical/selection": "0.32.1", "@lexical/table": "0.32.1", "@lexical/utils": "0.32.1", + "debug": "^4.4.1", "katex": "^0.16.10", "lexical": "0.32.1", "lodash-es": "^4.17.21", diff --git a/packages/lexical-playground/src/utils/debug.ts b/packages/lexical-playground/src/utils/debug.ts new file mode 100644 index 00000000000..b9d7dfa77d9 --- /dev/null +++ b/packages/lexical-playground/src/utils/debug.ts @@ -0,0 +1,3 @@ +import debug from 'debug'; + +export const DebugLog = debug('Debug'); From fb4a4b0a68099681cbe13f68d3f574fc6726b7d8 Mon Sep 17 00:00:00 2001 From: yu Date: Tue, 30 Sep 2025 15:28:11 +0800 Subject: [PATCH 5/5] chore: comments --- babel.config.js | 1 + packages/lexical-plain-text/src/index.ts | 1 + packages/lexical-playground/src/appSettings.ts | 3 ++- packages/lexical/src/LexicalUtils.ts | 1 + scripts/error-codes/transform-error-messages.js | 11 +++++++++-- 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/babel.config.js b/babel.config.js index 7ef7f39d372..fb31b18df53 100644 --- a/babel.config.js +++ b/babel.config.js @@ -12,6 +12,7 @@ module.exports = { plugins: [ [ require('./scripts/error-codes/transform-error-messages'), + // 禁用错误信息的压缩/简化处理 {noMinify: true}, ], ], diff --git a/packages/lexical-plain-text/src/index.ts b/packages/lexical-plain-text/src/index.ts index a77a4bcd817..bdf1521e2c0 100644 --- a/packages/lexical-plain-text/src/index.ts +++ b/packages/lexical-plain-text/src/index.ts @@ -376,6 +376,7 @@ export function registerPlainText(editor: LexicalEditor): () => void { const selection = $getSelection(); if (!$isRangeSelection(selection)) { + // 使用浏览器默认粘贴行为 return false; } diff --git a/packages/lexical-playground/src/appSettings.ts b/packages/lexical-playground/src/appSettings.ts index f5352f1cf8f..7073254f8f4 100644 --- a/packages/lexical-playground/src/appSettings.ts +++ b/packages/lexical-playground/src/appSettings.ts @@ -13,7 +13,8 @@ export const isDevPlayground: boolean = export const DEFAULT_SETTINGS = { disableBeforeInput: false, - emptyEditor: isDevPlayground, + // emptyEditor: isDevPlayground, + emptyEditor: false, hasLinkAttributes: false, isAutocomplete: false, isCharLimit: false, diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts index f302c177662..1b5c4c6fcc4 100644 --- a/packages/lexical/src/LexicalUtils.ts +++ b/packages/lexical/src/LexicalUtils.ts @@ -248,6 +248,7 @@ export function toggleTextFormatType( ) { return format; } + // 位运算 异或 实现 toggle let newFormat = format ^ activeFormat; if (type === 'subscript') { newFormat &= ~TEXT_TYPE_TO_FORMAT.superscript; diff --git a/scripts/error-codes/transform-error-messages.js b/scripts/error-codes/transform-error-messages.js index e90396b294b..a260f78337d 100644 --- a/scripts/error-codes/transform-error-messages.js +++ b/scripts/error-codes/transform-error-messages.js @@ -6,12 +6,19 @@ * */ +/** + * Babel 插件:转换 invariant() 错误消息 + * 功能: + * 1. 开发环境 - 保留完整错误信息(包含上下文和参数) + * 2. 生产环境 - 替换为简化的错误代码(通过 codes.json 映射) + */ + 'use strict'; // @ts-check const fs = require('fs-extra'); -const ErrorMap = require('./ErrorMap'); -const evalToString = require('./evalToString'); +const ErrorMap = require('./ErrorMap'); // 错误代码映射表管理 +const evalToString = require('./evalToString'); // 计算错误消息字符串 const helperModuleImports = require('@babel/helper-module-imports'); const prettier = require('prettier');