Skip to content

Commit

Permalink
Individual exports for markdown transformers, allow passing within si…
Browse files Browse the repository at this point in the history
…ngle array, typedefs (facebook#2045)
  • Loading branch information
fantactuka authored May 3, 2022
1 parent 0c93cfc commit e27b569
Show file tree
Hide file tree
Showing 18 changed files with 377 additions and 279 deletions.
12 changes: 5 additions & 7 deletions packages/lexical-markdown/LexicalMarkdown.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@
*
*/

import type {DecoratorNode, LexicalEditor} from 'lexical';
import type {LexicalEditor} from 'lexical';
import type {Transformer} from '../src';

export function registerMarkdownShortcuts<T>(
export function registerMarkdownShortcuts(
editor: LexicalEditor,
createHorizontalRuleNode: () => DecoratorNode<T>,
transformers: Array<Transformer>,
): () => void;
export function $convertFromMarkdownString(
markdownString: string,
editor: LexicalEditor,
): void;
export function $convertFromMarkdownString(markdown: string): void;
export function $convertToMarkdownString(): string;
94 changes: 88 additions & 6 deletions packages/lexical-markdown/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,93 @@
# `@lexical/markdown`

This package contains markdown helpers and functionality for Lexical.
This package contains markdown helpers for Lexical: import, export and shortcuts.

The package focuses on markdown conversion.
## Import and export
```js
import {
$convertFromMarkdownString,
$convertToMarkdownString,
TRANSFORMERS,
} from '@lexical/markdown';

The package has 3 main functions:
editor.update(() => {
const markdown = $convertToMarkdownString(TRANSFORMERS);
...
});

1. It imports a string and converts into Lexical and then converts markup within the imported nodes. See convertFromPlainTextUtils.js
2. It exports Lexical to a plain text with markup. See convertToPlainTextUtils.js
3. It autoformats newly typed text by converting the markdown + some trigger to the appropriate stylized text. See autoFormatUtils.js
editor.update(() => {
$convertFromMarkdownString(markdown, TRANSFORMERS);
});
```

It can also be used for initializing editor's state from markdown string. Here's an example with react `<RichTextPlugin>`
```jsx
<LexicalComposer>
<RichTextPlugin initialEditorState={() => {
$convertFromMarkdownString(markdown, TRANSFORMERS);
}} />
</LexicalComposer>
```

## Shortcuts
Can use `<LexicalMarkdownShortcutPlugin>` if using React
```jsx
import { TRANSFORMERS } from '@lexical/markdown';
import LexicalMarkdownShortcutPlugin from '@lexical/react/LexicalMarkdownShortcutPlugin';

<LexicalComposer>
<LexicalMarkdownShortcutPlugin transformers={TRANSFORMERS} />
</LexicalComposer>
```

Or `registerMarkdownShortcuts` to register it manually:
```js
import {
registerMarkdownShortcuts,
TRANSFORMERS,
} from '@lexical/markdown';

const editor = createEditor(...);
registerMarkdownShortcuts(editor, TRANSFORMERS);
```

## Transformers
Markdown functionality relies on transformers configuration. It's an array of objects that define how certain text or nodes
are processed during import, export or while typing. `@lexical/markdown` package provides set of built-in transformers:
```js
// Element transformers
UNORDERED_LIST
CODE
HEADING
ORDERED_LIST
QUOTE

// Text format transformers
BOLD_ITALIC_STAR
BOLD_ITALIC_UNDERSCORE
BOLD_STAR
BOLD_UNDERSCORE
INLINE_CODE
ITALIC_STAR
ITALIC_UNDERSCORE
STRIKETHROUGH

// Text match transformers
LINK
```

And bundles of commonly used transformers:
- `TRANSFORMERS` - all built-in transformers
- `ELEMENT_TRANSFORMERS` - all built-in element transformers
- `TEXT_FORMAT_TRANSFORMERS` - all built-in text format trasnformers
- `TEXT_MATCH_TRANSFORMERS` - all built-in text match trasnformers

Transformers are explicitly passed to markdown API allowing application-specific subset of markdown or custom transformers.

There're three types of transformers:

- **Element transformer** handles top level elements (lists, headings, quotes, tables or code blocks)
- **Text format transformer** applies text range formats defined in `TextFormatType` (bold, italic, underline, strikethrough, code, subscript and superscript)
- **Text match transformer** relies on matching leaf text node content

See `MarkdownTransformers.js` for transformer implementation examples
50 changes: 44 additions & 6 deletions packages/lexical-markdown/flow/LexicalMarkdown.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,52 @@
* @flow strict
*/

import type {DecoratorNode, LexicalEditor} from 'lexical';
import type {LexicalEditor} from 'lexical';
import type {
Transformer,
ElementTransformer,
TextFormatTransformer,
TextMatchTransformer,
} from '../src';

declare export function registerMarkdownShortcuts<T>(
// TODO:
// transformers should be required argument, breaking change
declare export function registerMarkdownShortcuts(
editor: LexicalEditor,
createHorizontalRuleNode: () => DecoratorNode<T>,
transformers?: Array<Transformer>,
): () => void;

// TODO:
// transformers should be required argument, breaking change
declare export function $convertFromMarkdownString(
markdownString: string,
editor: LexicalEditor,
markdown: string,
transformers?: Array<Transformer>,
): void;
declare export function $convertToMarkdownString(): string;

// TODO:
// transformers should be required argument, breaking change
declare export function $convertToMarkdownString(
transformers?: Array<Transformer>,
): string;

declare export var BOLD_ITALIC_STAR: TextFormatTransformer;
declare export var BOLD_ITALIC_UNDERSCORE: TextFormatTransformer;
declare export var BOLD_STAR: TextFormatTransformer;
declare export var BOLD_UNDERSCORE: TextFormatTransformer;
declare export var INLINE_CODE: TextFormatTransformer;
declare export var ITALIC_STAR: TextFormatTransformer;
declare export var ITALIC_UNDERSCORE: TextFormatTransformer;
declare export var STRIKETHROUGH: TextFormatTransformer;

declare export var UNORDERED_LIST: ElementTransformer;
declare export var CODE: ElementTransformer;
declare export var HEADING: ElementTransformer;
declare export var ORDERED_LIST: ElementTransformer;
declare export var QUOTE: ElementTransformer;

declare export var LINK: TextMatchTransformer;

declare export var TRANSFORMERS: Array<Transformer>;
declare export var ELEMENT_TRANSFORMERS: Array<ElementTransformer>;
declare export var TEXT_FORMAT_TRANSFORMERS: Array<TextFormatTransformer>;
declare export var TEXT_MATCH_TRANSFORMERS: Array<TextFormatTransformer>;
4 changes: 2 additions & 2 deletions packages/lexical-markdown/src/convertToMarkdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export function $convertToMarkdownString(): string {
}

function exportTopLevelElementOrDecorator(node: LexicalNode): string | null {
const blockTransformers = getAllMarkdownCriteriaForParagraphs();
for (const transformer of blockTransformers) {
const elementTransformers = getAllMarkdownCriteriaForParagraphs();
for (const transformer of elementTransformers) {
if (transformer.export != null) {
const result = transformer.export(node, (_node) => exportChildren(_node));
if (result != null) {
Expand Down
140 changes: 92 additions & 48 deletions packages/lexical-markdown/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,62 +7,106 @@
* @flow strict
*/

import type {AutoFormatTriggerState} from './utils';
import type {DecoratorNode, LexicalEditor} from 'lexical';
import type {
ElementTransformer,
TextFormatTransformer,
TextMatchTransformer,
Transformer,
} from './v2/MarkdownTransformers';

import {createMarkdownExport} from './v2/MarkdownExport';
import {createMarkdownImport} from './v2/MarkdownImport';
import {registerMarkdownShortcuts} from './v2/MarkdownShortcuts';
import {
findScanningContext,
getTriggerState,
updateAutoFormatting,
} from './autoFormatUtils';
import {
convertMarkdownForElementNodes,
convertStringToLexical,
} from './convertFromPlainTextUtils.js';
import * as v2 from './v2';
BOLD_ITALIC_STAR,
BOLD_ITALIC_UNDERSCORE,
BOLD_STAR,
BOLD_UNDERSCORE,
CODE,
HEADING,
INLINE_CODE,
ITALIC_STAR,
ITALIC_UNDERSCORE,
LINK,
ORDERED_LIST,
QUOTE,
STRIKETHROUGH,
UNORDERED_LIST,
} from './v2/MarkdownTransformers';

export function registerMarkdownShortcuts<T>(
editor: LexicalEditor,
createHorizontalRuleNode: () => DecoratorNode<T>,
): () => void {
// The priorTriggerState is compared against the currentTriggerState to determine
// if the user has performed some typing event that warrants an auto format.
// For example, typing "#" and then " ", shoud trigger an format.
// However, given "#A B", where the user delets "A" should not.
const ELEMENT_TRANSFORMERS: Array<ElementTransformer> = [
HEADING,
QUOTE,
CODE,
UNORDERED_LIST,
ORDERED_LIST,
];

let priorTriggerState: null | AutoFormatTriggerState = null;
return editor.registerUpdateListener(({tags}) => {
// Examine historic so that we are not running autoformatting within markdown.
if (tags.has('historic') === false) {
const currentTriggerState = getTriggerState(editor.getEditorState());
const scanningContext =
currentTriggerState == null
? null
: findScanningContext(editor, currentTriggerState, priorTriggerState);
if (scanningContext != null) {
updateAutoFormatting(editor, scanningContext, createHorizontalRuleNode);
}
priorTriggerState = currentTriggerState;
} else {
priorTriggerState = null;
}
});
}
// Order of text format transformers matters:
//
// - code should go first as it prevents any transformations inside
// - then longer tags match (e.g. ** or __ should go before * or _)
const TEXT_FORMAT_TRANSFORMERS: Array<TextFormatTransformer> = [
INLINE_CODE,
BOLD_ITALIC_STAR,
BOLD_ITALIC_UNDERSCORE,
BOLD_STAR,
BOLD_UNDERSCORE,
ITALIC_STAR,
ITALIC_UNDERSCORE,
STRIKETHROUGH,
];

export function $convertFromMarkdownString<T>(
markdownString: string,
editor: LexicalEditor,
createHorizontalRuleNode: null | (() => DecoratorNode<T>),
const TEXT_MATCH_TRANSFORMERS: Array<TextMatchTransformer> = [LINK];

const TRANSFORMERS: Array<Transformer> = [
...ELEMENT_TRANSFORMERS,
...TEXT_FORMAT_TRANSFORMERS,
...TEXT_MATCH_TRANSFORMERS,
];

function $convertFromMarkdownString(
markdown: string,
transformers?: Array<Transformer> = TRANSFORMERS,
): void {
if (convertStringToLexical(markdownString, editor) != null) {
convertMarkdownForElementNodes(editor, createHorizontalRuleNode);
}
const importMarkdown = createMarkdownImport(transformers);
return importMarkdown(markdown);
}

function $convertToMarkdownString(
transformers?: Array<Transformer> = TRANSFORMERS,
): string {
const exportMarkdown = createMarkdownExport(transformers);
return exportMarkdown();
}

export {$convertToMarkdownString} from './convertToMarkdown';
export {v2};
export type {
BlockTransformer,
ElementTransformer,
TextFormatTransformer,
TextMatchTransformer,
} from './v2/MarkdownTransformers';
Transformer,
};

export {
$convertFromMarkdownString,
$convertToMarkdownString,
BOLD_ITALIC_STAR,
BOLD_ITALIC_UNDERSCORE,
BOLD_STAR,
BOLD_UNDERSCORE,
CODE,
ELEMENT_TRANSFORMERS,
HEADING,
INLINE_CODE,
ITALIC_STAR,
ITALIC_UNDERSCORE,
LINK,
ORDERED_LIST,
QUOTE,
registerMarkdownShortcuts,
STRIKETHROUGH,
TEXT_FORMAT_TRANSFORMERS,
TEXT_MATCH_TRANSFORMERS,
TRANSFORMERS,
UNORDERED_LIST,
};
Loading

0 comments on commit e27b569

Please sign in to comment.