Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ flow-typed
test-results
/libdefs/*.js
pnpm-lock.yaml
**/.next/**
4 changes: 4 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {fixupPluginRules} from '@eslint/compat';
import js from '@eslint/js';
import lexicalInternalPlugin from '@lexical/eslint-plugin-internal';
import prettierConfig from 'eslint-config-prettier';
import compat from 'eslint-plugin-compat';
import _headerPlugin from 'eslint-plugin-header';
import importXPlugin from 'eslint-plugin-import-x';
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
Expand Down Expand Up @@ -447,4 +448,7 @@ export default [

// Prettier must be last to override formatting rules
prettierConfig,

// Compatibility with browserslist
compat.configs['flat/recommended'],
];
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
"cross-env": "^10.1.0",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-compat": "^7.0.2",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import-x": "^4.16.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
Expand Down Expand Up @@ -196,6 +197,12 @@
"semver": "^7.7.4",
"yjs": "^13.6.30"
},
"browserslist": [
"Chrome >= 86",
"Firefox >= 115",
"Safari >= 15",
"Edge >= 86"
],
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
Expand Down
1 change: 1 addition & 0 deletions packages/lexical-markdown/flow/LexicalMarkdown.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type ElementTransformer = {
isImport: boolean,
) => boolean | void,
type: 'element',
triggerOnEnter?: boolean,
};

export type MultilineElementTransformer = {
Expand Down
28 changes: 21 additions & 7 deletions packages/lexical-markdown/src/MarkdownShortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function runElementTransformers(
anchorNode: TextNode,
anchorOffset: number,
elementTransformers: ReadonlyArray<ElementTransformer>,
triggerOnEnter?: boolean,
): boolean {
const grandParentNode = parentNode.getParent();

Expand All @@ -62,18 +63,21 @@ function runElementTransformers(
// TODO:
// Can have a quick check if caret is close enough to the beginning of the string (e.g. offset less than 10-20)
// since otherwise it won't be a markdown shortcut, but tables are exception
if (textContent[anchorOffset - 1] !== ' ') {
return false;
if (!triggerOnEnter) {
if (textContent[anchorOffset - 1] !== ' ') {
return false;
}
}

for (const {regExp, replace} of elementTransformers) {
const match = textContent.match(regExp);

if (
match &&
match[0].length ===
(match[0].endsWith(' ') ? anchorOffset : anchorOffset - 1)
) {
const expectedMatchLength =
triggerOnEnter || (match && match[0].endsWith(' '))
? anchorOffset
: anchorOffset - 1;

if (match && match[0].length === expectedMatchLength) {
const nextSiblings = anchorNode.getNextSiblings();
const [leadingNode, remainderNode] = anchorNode.splitText(anchorOffset);
const siblings = remainderNode
Expand Down Expand Up @@ -437,6 +441,9 @@ export function registerMarkdownShortcuts(
transformers: Array<Transformer> = TRANSFORMERS,
): () => void {
const byType = transformersByType(transformers);
const elementTransformersForEnter = byType.element.filter(
t => t.triggerOnEnter,
);
const textFormatTransformersByTrigger = indexBy(
byType.textFormat,
({tag}) => tag[tag.length - 1],
Expand Down Expand Up @@ -647,6 +654,13 @@ export function registerMarkdownShortcuts(
anchorOffset,
byType.multilineElement,
true,
) ||
runElementTransformers(
parentNode,
anchorNode,
anchorOffset,
elementTransformersForEnter,
true,
)
) {
if (event !== null) {
Expand Down
15 changes: 13 additions & 2 deletions packages/lexical-markdown/src/MarkdownTransformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ export type ElementTransformer = {
isImport: boolean,
) => boolean | void;
type: 'element';
/**
* When set, `registerMarkdownShortcuts` may run this transformer from `KEY_ENTER_COMMAND`
* at end-of-line without requiring a trailing space (the update listener still uses the
* space after the markdown token). Omit or false to disable Enter-triggered shortcuts.
*/
triggerOnEnter?: boolean;
};

export type MultilineElementTransformer = {
Expand Down Expand Up @@ -483,6 +489,7 @@ export const HEADING: ElementTransformer = {
const tag = ('h' + match[1].length) as HeadingTagType;
return $createHeadingNode(tag);
}),
triggerOnEnter: true,
type: 'element',
};

Expand Down Expand Up @@ -521,12 +528,12 @@ export const QUOTE: ElementTransformer = {
node.select(0, 0);
}
},
triggerOnEnter: true,
type: 'element',
};

export const CODE: MultilineElementTransformer = {
dependencies: [CodeNode],

export: (node: LexicalNode) => {
if (!$isCodeNode(node)) {
return null;
Expand Down Expand Up @@ -616,11 +623,11 @@ export const CODE: MultilineElementTransformer = {
CODE.replace(rootNode, null, startMatch, null, linesInBetween, true);
return [true, lines.length - 1];
},

regExpEnd: {
optional: true,
regExp: CODE_END_REGEX,
},

regExpStart: CODE_START_REGEX,

replace: (
Expand Down Expand Up @@ -680,6 +687,7 @@ export const CODE: MultilineElementTransformer = {
})(rootNode, children, startMatch, isImport);
}
},

type: 'multiline-element',
};

Expand All @@ -692,6 +700,7 @@ export const UNORDERED_LIST: ElementTransformer = {
},
regExp: UNORDERED_LIST_REGEX,
replace: listReplace('bullet'),
triggerOnEnter: true,
type: 'element',
};

Expand All @@ -704,6 +713,7 @@ export const CHECK_LIST: ElementTransformer = {
},
regExp: CHECK_LIST_REGEX,
replace: listReplace('check'),
triggerOnEnter: true,
type: 'element',
};

Expand All @@ -716,6 +726,7 @@ export const ORDERED_LIST: ElementTransformer = {
},
regExp: ORDERED_LIST_REGEX,
replace: listReplace('number'),
triggerOnEnter: true,
type: 'element',
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,102 @@ describe('Markdown', () => {
'<p><span style="white-space: pre-wrap;">```</span></p>',
);
});

it('should transform element markdown on Enter when trailing space was not required (custom element transformer)', () => {
const ELEMENT_TRIGGERED_FENCE: ElementTransformer = {
dependencies: [CodeNode],
export: () => null,
regExp: /^`{3,}(\w+)?$/,
replace: (parentNode, children, match, isImport) => {
const node = $createCodeNode(match[1]);
node.append(...children);
parentNode.replace(node);
if (!isImport) {
node.select(0, 0);
}
},
triggerOnEnter: true,
type: 'element',
};

const editor = createHeadlessEditor({
nodes: [
HeadingNode,
ListNode,
ListItemNode,
QuoteNode,
CodeNode,
LinkNode,
],
});

registerMarkdownShortcuts(editor, [ELEMENT_TRIGGERED_FENCE]);

editor.update(
() => {
const root = $getRoot();
const paragraph = $createParagraphNode();
root.append(paragraph);
paragraph.selectEnd();
const selection = $getSelection();
if ($isRangeSelection(selection)) {
selection.insertText('```');
}
},
{discrete: true},
);

editor.update(
() => {
editor.dispatchCommand(KEY_ENTER_COMMAND, null);
},
{discrete: true},
);

expect(editor.read(() => $generateHtmlFromNodes(editor))).toBe(
'<pre spellcheck="false"></pre>',
);
});

it('should transform heading on Enter when a line was inserted at once (no trailing space listener trigger)', () => {
const editor = createHeadlessEditor({
nodes: [
HeadingNode,
ListNode,
ListItemNode,
QuoteNode,
CodeNode,
LinkNode,
],
});

registerMarkdownShortcuts(editor, [HEADING]);

editor.update(
() => {
const root = $getRoot();
const paragraph = $createParagraphNode();
root.append(paragraph);
paragraph.selectEnd();
const selection = $getSelection();
if ($isRangeSelection(selection)) {
selection.insertText('## ');
}
},
{discrete: true},
);

editor.update(
() => {
editor.dispatchCommand(KEY_ENTER_COMMAND, null);
},
{discrete: true},
);

expect(editor.read(() => $generateHtmlFromNodes(editor))).toBe(
'<h2><br></h2>',
);
});
});

describe('composition-end trigger characters (#7026)', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {AutoLinkExtension, createLinkMatcherWithRegExp} from '@lexical/link';
import {configExtension} from 'lexical';

const URL_REGEX =
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)(?<![-.+():%])/;
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&//=]*[a-zA-Z0-9@_~#?&//=])?/;

const EMAIL_REGEX =
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ function useQuery(): (searchText: string) => SearchPromise {
}

function formatSuggestionText(suggestion: string): string {
// eslint-disable-next-line compat/compat
const userAgentData = window.navigator.userAgentData;
const isMobile =
userAgentData !== undefined
Expand Down
Loading