From 42928b304ad9b9362501f6bc923f21b4e1e81ddc Mon Sep 17 00:00:00 2001 From: aagash-ni <123377167+aagash-ni@users.noreply.github.com> Date: Tue, 22 Aug 2023 17:56:05 +0530 Subject: [PATCH] Rich Text Editor | Add setMarkdown() and getMarkdown() methods to set and get content from Tip Tap editor (#1424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## 🤨 Rationale This PR contains logic for parsing the assigned markdown content, setting it to the editor and also serializing the editor's data to markdown content. AzDo Feature: https://dev.azure.com/ni/DevCentral/_backlogs/backlog/ASW%20SystemLink%20LIMS/Features/?workitem=2350963 Issue: https://github.com/ni/nimble/issues/1288 ## 👩‍💻 Implementation * Exposed getMarkdown() and setmarkdown() methods to facilitate the exchange of data with the editor. * Used MarkdownParser from [prosemirror-markdown](https://github.com/ProseMirror/prosemirror-markdown/tree/master) package for parsing markdown strings, DOM serializer from [prosemirror-model ](https://github.com/ProseMirror/prosemirror-model) package to serialize the content as DOM structure, XML Serializer to serialize it to HTML string and sets it to the editor. * To serialize the tip-tap [document ](https://prosemirror.net/docs/ref/#model.Document_Structure)in getMarkdown() used MarkdownSerializer from [prosemirror-markdown](https://github.com/ProseMirror/prosemirror-markdown/tree/master) package to serialize the node based on schema. * Enabled `emphasis` and `list` rules in MarkdownParser, this allows users to set bold, italics, numbered, and bulleted lists in a CommonMark flavor to the editor. All other basic markdown formats are disabled. * Added only respective nodes and marks for bold, italics, numbered and bulleted lists in `MarkdownSerializer` ## 🧪 Testing * Added unit tests and visual tests for the component. * Manually tested and verified the functionality of the supported features. ## ✅ Checklist - [x] I have updated the project documentation to reflect my changes or determined no changes are needed. --------- Signed-off-by: Aagash Raaj Co-authored-by: SOLITONTECH\vivin.krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Co-authored-by: SOLITONTECH\aagash.maridas Co-authored-by: Jesse Attas --- ...-9d517129-f95f-4278-97e4-b2987f9507f4.json | 7 + .../src/rich-text-editor/index.ts | 135 +++- .../src/rich-text-editor/styles.ts | 4 + .../src/rich-text-editor/template.ts | 6 +- .../testing/rich-text-editor.pageobject.ts | 43 +- .../tests/rich-text-editor-matrix.stories.ts | 51 ++ .../tests/rich-text-editor.spec.ts | 724 +++++++++++++++++- .../tests/rich-text-editor.stories.ts | 76 +- 8 files changed, 1013 insertions(+), 33 deletions(-) create mode 100644 change/@ni-nimble-components-9d517129-f95f-4278-97e4-b2987f9507f4.json diff --git a/change/@ni-nimble-components-9d517129-f95f-4278-97e4-b2987f9507f4.json b/change/@ni-nimble-components-9d517129-f95f-4278-97e4-b2987f9507f4.json new file mode 100644 index 0000000000..660c1db7a0 --- /dev/null +++ b/change/@ni-nimble-components-9d517129-f95f-4278-97e4-b2987f9507f4.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Add setMarkdown and getMarkdown methods in rich-text-editor", + "packageName": "@ni/nimble-components", + "email": "jattasNI@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/nimble-components/src/rich-text-editor/index.ts b/packages/nimble-components/src/rich-text-editor/index.ts index 3c6687ae68..b88609bf97 100644 --- a/packages/nimble-components/src/rich-text-editor/index.ts +++ b/packages/nimble-components/src/rich-text-editor/index.ts @@ -2,6 +2,15 @@ import { observable } from '@microsoft/fast-element'; import { DesignSystem, FoundationElement } from '@microsoft/fast-foundation'; import { keyEnter, keySpace } from '@microsoft/fast-web-utilities'; import { Editor } from '@tiptap/core'; +import { + schema, + defaultMarkdownParser, + MarkdownParser, + MarkdownSerializer, + defaultMarkdownSerializer, + MarkdownSerializerState +} from 'prosemirror-markdown'; +import { DOMSerializer, Node } from 'prosemirror-model'; import Bold from '@tiptap/extension-bold'; import BulletList from '@tiptap/extension-bullet-list'; import Document from '@tiptap/extension-document'; @@ -52,16 +61,29 @@ export class RichTextEditor extends FoundationElement { /** * @internal */ - public editor!: HTMLDivElement; + public editorContainer!: HTMLDivElement; private tiptapEditor!: Editor; + private editor!: HTMLDivElement; + + private readonly markdownParser = this.initializeMarkdownParser(); + private readonly markdownSerializer = this.initializeMarkdownSerializer(); + private readonly domSerializer = DOMSerializer.fromSchema(schema); + private readonly xmlSerializer = new XMLSerializer(); + + public constructor() { + super(); + this.initializeEditor(); + } /** * @internal */ public override connectedCallback(): void { super.connectedCallback(); - this.initializeEditor(); + if (!this.editor.isConnected) { + this.editorContainer.append(this.editor); + } this.bindEditorTransactionEvent(); } @@ -153,6 +175,26 @@ export class RichTextEditor extends FoundationElement { return true; } + /** + * This function load tip tap editor with provided markdown content by parsing into html + * @public + */ + public setMarkdown(markdown: string): void { + const html = this.getHtmlContent(markdown); + this.tiptapEditor.commands.setContent(html); + } + + /** + * This function returns markdown string by serializing tiptap editor document using prosemirror MarkdownSerializer + * @public + */ + public getMarkdown(): string { + const markdownContent = this.markdownSerializer.serialize( + this.tiptapEditor.state.doc + ); + return markdownContent; + } + /** * @internal */ @@ -163,7 +205,96 @@ export class RichTextEditor extends FoundationElement { return false; } + /** + * This function takes the Fragment from parseMarkdownToDOM function and return the serialized string using XMLSerializer + */ + private getHtmlContent(markdown: string): string { + const documentFragment = this.parseMarkdownToDOM(markdown); + return this.xmlSerializer.serializeToString(documentFragment); + } + + private initializeMarkdownParser(): MarkdownParser { + /** + * It configures the tokenizer of the default Markdown parser with the 'zero' preset. + * The 'zero' preset is a configuration with no rules enabled by default to selectively enable specific rules. + * https://github.com/markdown-it/markdown-it/blob/2b6cac25823af011ff3bc7628bc9b06e483c5a08/lib/presets/zero.js#L1 + * + */ + const zeroTokenizerConfiguration = defaultMarkdownParser.tokenizer.configure('zero'); + + // The detailed information of the supported rules were provided in the below CommonMark spec document. + // https://spec.commonmark.org/0.30/ + const supportedTokenizerRules = zeroTokenizerConfiguration.enable([ + 'emphasis', + 'list' + ]); + + return new MarkdownParser( + schema, + supportedTokenizerRules, + defaultMarkdownParser.tokens + ); + } + + private initializeMarkdownSerializer(): MarkdownSerializer { + /** + * orderedList Node is getting 'order' attribute which it is not present in the + * tip-tap orderedList Node and having start instead of order, Changed it to start (nodes.attrs.start) + * Assigned updated node in place of orderedList node from defaultMarkdownSerializer + * https://github.com/ProseMirror/prosemirror-markdown/blob/b7c1fd2fb74c7564bfe5428c7c8141ded7ebdd9f/src/to_markdown.ts#L94C2-L101C7 + */ + const orderedListNode = function orderedList( + state: MarkdownSerializerState, + node: Node + ): void { + const start = (node.attrs.start as number) || 1; + const maxW = String(start + node.childCount - 1).length; + const space = state.repeat(' ', maxW + 2); + state.renderList(node, space, i => { + const nStr = String(start + i); + return `${state.repeat(' ', maxW - nStr.length) + nStr}. `; + }); + }; + + /** + * Internally Tiptap editor creates it own schema ( Nodes AND Marks ) based on the extensions ( Here Starter Kit is used for Bold, italic, orderedList and + * bulletList extensions) and defaultMarkdownSerializer uses schema from prosemirror-markdown to serialize the markdown. + * So, there is variations in the nodes and marks name (Eg. 'ordered_list' in prosemirror-markdown schema whereas 'orderedList' in tip tap editor schema), + * To fix up this reassigned the respective nodes and marks with tip-tap editor schema. + */ + const nodes = { + bulletList: defaultMarkdownSerializer.nodes.bullet_list!, + listItem: defaultMarkdownSerializer.nodes.list_item!, + orderedList: orderedListNode, + doc: defaultMarkdownSerializer.nodes.doc!, + paragraph: defaultMarkdownSerializer.nodes.paragraph!, + text: defaultMarkdownSerializer.nodes.text! + }; + const marks = { + italic: defaultMarkdownSerializer.marks.em!, + bold: defaultMarkdownSerializer.marks.strong! + }; + return new MarkdownSerializer(nodes, marks); + } + + private parseMarkdownToDOM(value: string): HTMLElement | DocumentFragment { + const parsedMarkdownContent = this.markdownParser.parse(value); + if (parsedMarkdownContent === null) { + return document.createDocumentFragment(); + } + + return this.domSerializer.serializeFragment( + parsedMarkdownContent.content + ); + } + private initializeEditor(): void { + // Create div from the constructor because the TipTap editor requires its host element before the template is instantiated. + this.editor = document.createElement('div'); + this.editor.className = 'editor'; + this.editor.setAttribute('aria-multiline', 'true'); + this.editor.setAttribute('role', 'textbox'); + /** * For more information on the extensions for the supported formatting options, refer to the links below. * Tiptap marks: https://tiptap.dev/api/marks diff --git a/packages/nimble-components/src/rich-text-editor/styles.ts b/packages/nimble-components/src/rich-text-editor/styles.ts index 2a14c19c84..e32148e60e 100644 --- a/packages/nimble-components/src/rich-text-editor/styles.ts +++ b/packages/nimble-components/src/rich-text-editor/styles.ts @@ -73,6 +73,10 @@ export const styles = css` overflow: auto; } + .editor-container { + display: contents; + } + .ProseMirror { ${ /** diff --git a/packages/nimble-components/src/rich-text-editor/template.ts b/packages/nimble-components/src/rich-text-editor/template.ts index 4cf48755a7..e8db67808a 100644 --- a/packages/nimble-components/src/rich-text-editor/template.ts +++ b/packages/nimble-components/src/rich-text-editor/template.ts @@ -11,11 +11,7 @@ import { iconNumberListTag } from '../icons/number-list'; export const template = html`