Skip to content

Commit

Permalink
Merge pull request #2082 from epam/improve-lists-for-md
Browse files Browse the repository at this point in the history
[SlateEditor] Improve lists for md
  • Loading branch information
AlekseyManetov authored Mar 21, 2024
2 parents 0ec7a8f + 39598b3 commit 8c43a03
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const plugins = [

export default function SlateEditorBasicExample() {
const [value, setValue] = useState<EditorValue>(
deserializeMd(demoData.slateMdSerializationInitialData),
() => deserializeMd(demoData.slateMdSerializationInitialData),
);

const [mdContent, setMdContent] = useState('');
Expand Down Expand Up @@ -86,7 +86,7 @@ export default function SlateEditorBasicExample() {
onValueChange={ (v) => {
setMdContent(v);
} }
rows={ 16 }
rows={ 22 }
placeholder="Please type markdown here"
/>
)}
Expand Down
7 changes: 6 additions & 1 deletion uui-docs/src/demoData/slateMdSerializationInitialData.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
export const slateMdSerializationInitialData = `### Basic layout
We support inline text styles: **bold**, _italic_, underlined, several UUI-friendly text colors: red, yellow, and green.
We support inline text styles such as **bold** and _italic_ . Additionally, we provide support for three levels of headers and hyperlinks.
Numbered lists:
1. In edit mode, we detect '1. ' and start list automatically
1. You can use 'tab' / 'shift/tab' to indent the list
Bullet lists:
- Type '- ' to start the list
- You can create multi-level lists with 'tab' / 'shift+tab'. Example:
- Level 1
- Level 2
- Another item on level 2
- Level 3
- Another item on level 3
`;
4 changes: 3 additions & 1 deletion uui-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
"slate": "0.94.1",
"slate-history": "0.93.0",
"slate-hyperscript": "0.81.3",
"slate-react": "0.99.0"
"slate-react": "0.99.0",
"remark-parse": "9.0.0",
"unified": "9.2.2"
},
"devDependencies": {
"@types/lodash.debounce": "4.0.7",
Expand Down
37 changes: 29 additions & 8 deletions uui-editor/src/md-serializer/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const isLeafNode = (node: BlockType | LeafType): node is LeafType => {

const VOID_ELEMENTS: Array<keyof NodeTypes> = ['thematic_break', 'image'];

const BREAK_TAG = '<br>';
const BREAK_TAG = '<br/>';

export function serialize(
chunk: BlockType | LeafType,
Expand Down Expand Up @@ -45,7 +45,7 @@ export function serialize(

if (!isLeafNode(chunk)) {
children = chunk.children
.map((c: BlockType | LeafType) => {
.map((c: BlockType | LeafType, index, all) => {
const isList = !isLeafNode(c)
? (LIST_TYPES as string[]).includes(c.type || '')
: false;
Expand All @@ -71,10 +71,18 @@ export function serialize(
);
}

const listProps = isList || selfIsList ? {
index,
length: all.length,
} : {};

return serialize(
{
...c,
parentType: type,
parent: {
type,
...listProps,
},
},
{
nodeTypes,
Expand Down Expand Up @@ -108,7 +116,7 @@ export function serialize(
if (
!ignoreParagraphNewline
&& (text === '' || text === '\n')
&& chunk.parentType === nodeTypes.paragraph
&& chunk.parent?.type === nodeTypes.paragraph
) {
type = nodeTypes.paragraph;
children = BREAK_TAG;
Expand Down Expand Up @@ -185,10 +193,11 @@ export function serialize(

case nodeTypes.ul_list:
case nodeTypes.ol_list:
return `\n${children}`;
const newLineAfter = listDepth === 0 ? '\n' : '';
return `${children}${newLineAfter}`;

case nodeTypes.listItem:
const isOL = chunk && chunk.parentType === nodeTypes.ol_list;
const isOL = chunk && chunk.parent?.type === nodeTypes.ol_list;

let spacer = '';
for (let k = 0; listDepth > k; k++) {
Expand All @@ -199,10 +208,22 @@ export function serialize(
spacer += ' ';
}
}
return `${spacer}${isOL ? '1.' : '-'} ${children}\n`;

const isNewLine = chunk && (
chunk.parent?.type === nodeTypes.ol_list
|| chunk.parent?.type === nodeTypes.ul_list
);
const emptyBefore = isNewLine ? '\n' : '';

const isLastItem = chunk.parent
&& (chunk.parent.length! - 1 === chunk.parent.index)
&& (chunk as BlockType).children.length === 1;
const emptyAfter = isLastItem && listDepth === 0 ? '\n' : '';

return `${emptyBefore}${spacer}${isOL ? '1.' : '-'} ${children}`;

case nodeTypes.paragraph:
return `${children}\n`;
return `\n${children}\n`;

case nodeTypes.thematic_break:
return '\n---\n';
Expand Down
12 changes: 10 additions & 2 deletions uui-editor/src/md-serializer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,23 @@ export type NodeTypes = {
export interface LeafType {
text: string;
strikeThrough?: boolean;
parentType?: string;
parent?: {
type: string,
index?: number,
length?: number
},
['uui-richTextEditor-bold']?: boolean;
['uui-richTextEditor-italic']?: boolean;
['uui-richTextEditor-code']?: boolean;
}

export interface BlockType {
type: string;
parentType?: string;
parent?: {
type: string,
index?: number,
length?: number
},
url?: string;
caption?: Array<BlockType | LeafType>;
language?: string;
Expand Down
1 change: 0 additions & 1 deletion uui-editor/src/plugins/colorPlugin/colorPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export const colorPlugin = () => createFontColorPlugin({
},
options: {
floatingBarButton: ColorButton,
name: 'color-button',
},
});

Expand Down
143 changes: 143 additions & 0 deletions uui-editor/src/plugins/deserializeMdPlugin/deserializeMdPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
MARK_ITALIC, MARK_BOLD, MARK_CODE,
} from '@udecode/plate-basic-marks';
import {
getPluginType, PlateEditor, getPluginOptions,
} from '@udecode/plate-core';
import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph';
import {
RemarkTextRules,
MdastNodeType,
MdastNode,
RemarkPluginOptions,
remarkTransformText,
DeserializeMdPlugin,
KEY_DESERIALIZE_MD,
RemarkElementRule,
createDeserializeMdPlugin as createDeserializeMdRootPlugin,
remarkDefaultElementRules,
} from '@udecode/plate-serializer-md';
import {
TDescendant, TElement, Value,
} from '@udecode/slate';
import unified from 'unified';
import markdown from 'remark-parse';
import { isUrl } from '@udecode/plate-common';

const remarkDefaultTextRules: RemarkTextRules<Value> = {
text: {},
emphasis: { mark: ({ editor }) => getPluginType(editor, MARK_ITALIC) },
strong: { mark: ({ editor }) => getPluginType(editor, MARK_BOLD) },
inlineCode: { mark: ({ editor }) => getPluginType(editor, MARK_CODE) },
};

export const remarkTextTypes: MdastNodeType[] = [
'emphasis',
'strong',
'delete',
'inlineCode',
// 'html',
'text',
];

const remarkTransformNode = <V extends Value>(
node: MdastNode,
options: RemarkPluginOptions<V>,
): TDescendant | TDescendant[] => {
const { type } = node;

if (remarkTextTypes.includes(type!)) {
return remarkTransformText(node, options);
}

return remarkTransformElement(node, options);
};

function remarkPlugin<V extends Value>(options: RemarkPluginOptions<V>) {
const compiler = (node: { children: Array<MdastNode> }) => {
return node.children.flatMap((child) =>
remarkTransformNode(child, options));
};

// @ts-ignore
this.Compiler = compiler;
}

const remarkTransformElement = <V extends Value>(
node: MdastNode,
options: RemarkPluginOptions<V>,
): TElement | TElement[] => {
const { elementRules } = options;

const { type } = node;
const elementRule = (elementRules as any)[type!];
if (!elementRule) return [];

return elementRule.transform(node, options);
};

/**
* Deserialize content from Markdown format to Slate format.
* `editor` needs
*/
export const deserializeMd = <V extends Value>(
editor: PlateEditor<V>,
data: string,
) => {
const { elementRules, textRules } = getPluginOptions<DeserializeMdPlugin, V>(
editor,
KEY_DESERIALIZE_MD,
);

const tree: any = unified()
.use(markdown as any)
.use(remarkPlugin, {
editor,
elementRules,
textRules,
} as unknown as RemarkPluginOptions<V>)
.processSync(data);

return tree.result;
};

// TODO: move to plate
const htmlRule: RemarkElementRule<Value> = {
transform: (node, options) => {
return {
type: getPluginType(options.editor, ELEMENT_PARAGRAPH),
children: [{ text: node.value?.replace(/(<br>)|(<br\/>)/g, '') || '' }],
};
},
};

export const createDeserializeMdPlugin = () => createDeserializeMdRootPlugin({
then: (editor) => ({
editor: {
insertData: {
format: 'text/plain',
query: ({ data, dataTransfer }) => {
const htmlData = dataTransfer.getData('text/html');
if (htmlData) {
return false;
}

// if content is simply a URL pass through to not break LinkPlugin
const { files } = dataTransfer;
if (!files?.length && isUrl(data)) {
return false;
}
return true;
},
getFragment: ({ data }) => deserializeMd<Value>(editor, data),
},
},
}),
options: {
elementRules: {
...remarkDefaultElementRules,
html: htmlRule,
} as any,
textRules: remarkDefaultTextRules,
},
});
3 changes: 1 addition & 2 deletions uui-editor/src/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ import {
italicPlugin,
PARAGRAPH_TYPE,
} from './plugins';
import { createDeserializeMdPlugin, deserializeMd } from '@udecode/plate-serializer-md';
import { remarkNodeTypesMap, serialize } from './md-serializer';
import { createTempEditor, isEditorValueEmpty } from './helpers';
import { BaseEditor, Editor } from 'slate';
import { createAutoformatPlugin } from './plugins/autoformatPlugin/autoformatPlugin';
import { createDeserializeMdPlugin, deserializeMd } from './plugins/deserializeMdPlugin/deserializeMdPlugin';

type SerializerType = 'html' | 'md';

Expand Down
38 changes: 19 additions & 19 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -15455,6 +15455,13 @@ [email protected]:
micromark-extension-gfm "^2.0.0"
unified "^10.0.0"

[email protected], remark-parse@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-9.0.0.tgz#4d20a299665880e4f4af5d90b7c7b8a935853640"
integrity sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==
dependencies:
mdast-util-from-markdown "^0.8.0"

remark-parse@^10.0.0:
version "10.0.2"
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.2.tgz#ca241fde8751c2158933f031a4e3efbaeb8bc262"
Expand All @@ -15464,13 +15471,6 @@ remark-parse@^10.0.0:
mdast-util-from-markdown "^1.0.0"
unified "^10.0.0"

remark-parse@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-9.0.0.tgz#4d20a299665880e4f4af5d90b7c7b8a935853640"
integrity sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==
dependencies:
mdast-util-from-markdown "^0.8.0"

remark-rehype@^10.0.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279"
Expand Down Expand Up @@ -17923,6 +17923,18 @@ unicode-property-aliases-ecmascript@^2.0.0:
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd"
integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==

[email protected], unified@^9.2.2:
version "9.2.2"
resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975"
integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==
dependencies:
bail "^1.0.0"
extend "^3.0.0"
is-buffer "^2.0.0"
is-plain-obj "^2.0.0"
trough "^1.0.0"
vfile "^4.0.0"

unified@^10.0.0:
version "10.1.2"
resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df"
Expand All @@ -17936,18 +17948,6 @@ unified@^10.0.0:
trough "^2.0.0"
vfile "^5.0.0"

unified@^9.2.2:
version "9.2.2"
resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975"
integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==
dependencies:
bail "^1.0.0"
extend "^3.0.0"
is-buffer "^2.0.0"
is-plain-obj "^2.0.0"
trough "^1.0.0"
vfile "^4.0.0"

union-value@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
Expand Down

0 comments on commit 8c43a03

Please sign in to comment.