From 1c0e5777a40988819ee30b14bb34a828692ee602 Mon Sep 17 00:00:00 2001 From: "SOLITONTECH\\vivin.krishna" <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:07:54 +0530 Subject: [PATCH 01/56] restructure folder changes --- package-lock.json | 94 ++-- packages/nimble-components/package.json | 21 +- .../nimble-components/src/all-components.ts | 4 +- .../src/rich-text-editor/index.ts | 361 ------------- .../src/rich-text/editor/index.ts | 481 ++++++++++++++++++ .../editor}/specs/README.md | 12 +- .../specs/spec-images/button-state.png | Bin .../specs/spec-images/editor-sample.png | Bin .../specs/spec-images/viewer-sample.png | Bin .../editor}/styles.ts | 99 +++- .../editor}/template.ts | 26 +- .../testing/rich-text-editor.pageobject.ts | 71 ++- .../editor}/testing/types.ts | 0 .../tests/rich-text-editor-matrix.stories.ts | 98 +++- .../editor}/tests/rich-text-editor.spec.ts | 233 ++++++++- .../editor}/tests/rich-text-editor.stories.ts | 53 +- .../editor}/tests/types.spec.ts | 0 .../models/markdown-parser.ts} | 89 +--- .../rich-text/models/markdown-serializer.ts | 47 ++ .../src/rich-text/viewer/index.ts | 75 +++ .../viewer}/specs/README.md | 0 .../viewer}/styles.ts | 2 +- .../viewer}/template.ts | 0 .../testing/rich-text-viewer.pageobject.ts | 0 .../tests/rich-text-viewer-matrix.stories.ts | 12 +- .../viewer}/tests/rich-text-viewer.spec.ts | 8 +- .../viewer}/tests/rich-text-viewer.stories.ts | 4 +- 27 files changed, 1201 insertions(+), 589 deletions(-) delete mode 100644 packages/nimble-components/src/rich-text-editor/index.ts create mode 100644 packages/nimble-components/src/rich-text/editor/index.ts rename packages/nimble-components/src/{rich-text-editor => rich-text/editor}/specs/README.md (96%) rename packages/nimble-components/src/{rich-text-editor => rich-text/editor}/specs/spec-images/button-state.png (100%) rename packages/nimble-components/src/{rich-text-editor => rich-text/editor}/specs/spec-images/editor-sample.png (100%) rename packages/nimble-components/src/{rich-text-editor => rich-text/editor}/specs/spec-images/viewer-sample.png (100%) rename packages/nimble-components/src/{rich-text-editor => rich-text/editor}/styles.ts (64%) rename packages/nimble-components/src/{rich-text-editor => rich-text/editor}/template.ts (75%) rename packages/nimble-components/src/{rich-text-editor => rich-text/editor}/testing/rich-text-editor.pageobject.ts (75%) rename packages/nimble-components/src/{rich-text-editor => rich-text/editor}/testing/types.ts (100%) rename packages/nimble-components/src/{rich-text-editor => rich-text/editor}/tests/rich-text-editor-matrix.stories.ts (54%) rename packages/nimble-components/src/{rich-text-editor => rich-text/editor}/tests/rich-text-editor.spec.ts (86%) rename packages/nimble-components/src/{rich-text-editor => rich-text/editor}/tests/rich-text-editor.stories.ts (71%) rename packages/nimble-components/src/{rich-text-editor => rich-text/editor}/tests/types.spec.ts (100%) rename packages/nimble-components/src/{rich-text-viewer/index.ts => rich-text/models/markdown-parser.ts} (55%) create mode 100644 packages/nimble-components/src/rich-text/models/markdown-serializer.ts create mode 100644 packages/nimble-components/src/rich-text/viewer/index.ts rename packages/nimble-components/src/{rich-text-viewer => rich-text/viewer}/specs/README.md (100%) rename packages/nimble-components/src/{rich-text-viewer => rich-text/viewer}/styles.ts (96%) rename packages/nimble-components/src/{rich-text-viewer => rich-text/viewer}/template.ts (100%) rename packages/nimble-components/src/{rich-text-viewer => rich-text/viewer}/testing/rich-text-viewer.pageobject.ts (100%) rename packages/nimble-components/src/{rich-text-viewer => rich-text/viewer}/tests/rich-text-viewer-matrix.stories.ts (91%) rename packages/nimble-components/src/{rich-text-viewer => rich-text/viewer}/tests/rich-text-viewer.spec.ts (99%) rename packages/nimble-components/src/{rich-text-viewer => rich-text/viewer}/tests/rich-text-viewer.stories.ts (92%) diff --git a/package-lock.json b/package-lock.json index 17c6824c54..a7c8871191 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9477,9 +9477,9 @@ } }, "node_modules/@tiptap/core": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.0.4.tgz", - "integrity": "sha512-2YOMjRqoBGEP4YGgYpuPuBBJHMeqKOhLnS0WVwjVP84zOmMgZ7A8M6ILC9Xr7Q/qHZCvyBGWOSsI7+3HsEzzYQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.1.7.tgz", + "integrity": "sha512-1pqTwlTnwTKQSNQmmTWhs2lwdvd+hFFNFZnrRAfvZhQZA6qPmPmKMNTcYmK38Tn4axKth6mhBamzTJgMZFI7ng==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -9489,9 +9489,9 @@ } }, "node_modules/@tiptap/extension-bold": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.0.4.tgz", - "integrity": "sha512-CWSQy1uWkVsen8HUsqhm+oEIxJrCiCENABUbhaVcJL/MqhnP4Trrh1B6O00Yfoc0XToPRRibDaHMFs4A3MSO0g==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.1.7.tgz", + "integrity": "sha512-GZV2D91WENkWd1W29vM4kyGWObcxOKQrY8MuCvTdxni1kobEc/LPZzQ1XiQmiNTvXTMcBz5ckLpezdjASV1dNg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -9501,9 +9501,9 @@ } }, "node_modules/@tiptap/extension-bullet-list": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.4.tgz", - "integrity": "sha512-JSZKBVTaKSuLl5fR4EKE4dOINOrgeRHYA25Vj6cWjgdvpTw5ef7vcUdn9yP4JwTmLRI+VnnMlYL3rqigU3iZNg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.1.7.tgz", + "integrity": "sha512-BReix1wkGNH12DSWGnWPKNu4do92Avh98aLkRS1o1V1Y49/+YGMYtfBXB9obq40o0WqKvk4MoM+rhKbfEc44Gg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -9513,9 +9513,9 @@ } }, "node_modules/@tiptap/extension-document": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.0.4.tgz", - "integrity": "sha512-mCj2fAhnNhIHttPSqfTPSSTGwClGaPYvhT56Ij/Pi4iCrWjPXzC4XnIkIHSS34qS2tJN4XJzr/z7lm3NeLkF1w==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.1.7.tgz", + "integrity": "sha512-tZyoPPmvzti7PEnyulXomEtINd/Oi2S84uOt6gw7DTCnDq5bF5sn1IfN8Icqp9t4jDwyLXy2TL0Zg/sR0a2Ibg==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -9525,9 +9525,9 @@ } }, "node_modules/@tiptap/extension-history": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.0.4.tgz", - "integrity": "sha512-3GAUszn1xZx3vniHMiX9BSKmfvb5QOb0oSLXInN+hx80CgJDIHqIFuhx2dyV9I/HWpa0cTxaLWj64kfDzb1JVg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.1.7.tgz", + "integrity": "sha512-8SIEKSImrIkqJThym1bPD13sC4/76UrG+piQ30xKQU4B7zUFCbutvrwYuQHSRvaEt8BPdTv2LWIK+wBkIgbWVA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -9538,9 +9538,9 @@ } }, "node_modules/@tiptap/extension-italic": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.0.4.tgz", - "integrity": "sha512-C/6+qs4Jh8xERRP0wcOopA1+emK8MOkBE4RQx5NbPnT2iCpERP0GlmHBFQIjaYPctZgKFHxsCfRnneS5Xe76+A==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.1.7.tgz", + "integrity": "sha512-7e37f+OFqisdY19nWIthbSNHMJy4+4dec06rUICPrkiuFaADj5HjUQr0dyWpL/LkZh92Wf/rWgp4V/lEwon3jA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -9550,9 +9550,9 @@ } }, "node_modules/@tiptap/extension-list-item": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.0.4.tgz", - "integrity": "sha512-tSkbLgRo1QMNDJttWs9FeRywkuy5T2HdLKKfUcUNzT3s0q5AqIJl7VyimsBL4A6MUfN1qQMZCMHB4pM9Mkluww==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.1.7.tgz", + "integrity": "sha512-hd/E4qQopBXWa6kdFY19qFVgqj4fzdPgAnzdXJ2XW7bC6O2CusmHphRRZ5FBsuspYTN/6/fv0i0jK9rSGlsEyA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -9562,9 +9562,9 @@ } }, "node_modules/@tiptap/extension-ordered-list": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.4.tgz", - "integrity": "sha512-Kfg+8k9p4iJCUKP/yIa18LfUpl9trURSMP/HX3/yQTz9Ul1vDrjxeFjSE5uWNvupcXRAM24js+aYrCmV7zpU+Q==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.1.7.tgz", + "integrity": "sha512-3XIXqbZmYkNzF+8PQ2jcCOCj0lpC3y9HGM/+joPIunhiUiktrIgpbUDv2E1Gq5lJHYqthIeujniI2dB85tkwJQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -9574,9 +9574,9 @@ } }, "node_modules/@tiptap/extension-paragraph": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.0.4.tgz", - "integrity": "sha512-nDxpopi9WigVqpfi8nU3B0fWYB14EMvKIkutNZo8wJvKGTZufNI8hw66wupIx/jZH1gFxEa5dHerw6aSYuWjgQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.1.7.tgz", + "integrity": "sha512-cLqX27hNrXrwZCKrIW8OC3rW2+MT8hhS37+cdqOxZo5hUqQ9EF/puwS0w8uUZ7B3awX9Jm1QZDMjjERLkcmobw==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -9585,10 +9585,23 @@ "@tiptap/core": "^2.0.0" } }, + "node_modules/@tiptap/extension-placeholder": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.1.7.tgz", + "integrity": "sha512-IiBoItYYNS7hb/zmPitw3w6Cylmp9qX+zW+QKe3lDkCNPeKxyQr86AnVLcQYOuXg62cLV9dp+4azZzHoz9SOcg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, "node_modules/@tiptap/extension-text": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.0.4.tgz", - "integrity": "sha512-i8/VFlVZh7TkAI49KKX5JmC0tM8RGwyg5zUpozxYbLdCOv07AkJt+E1fLJty9mqH4Y5HJMNnyNxsuZ9Ol/ySRA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.1.7.tgz", + "integrity": "sha512-3xaMMMNydLgoS+o+yOvaZF04ui9spJwJZl8VyYgcJKVGGLGRlWHrireXN5/OqXG2jLb/jWqXVx5idppQjX+PMA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -33325,16 +33338,17 @@ "@ni/nimble-tokens": "^6.3.0", "@tanstack/table-core": "^8.9.3", "@tanstack/virtual-core": "^3.0.0-beta.44", - "@tiptap/core": "^2.0.4", - "@tiptap/extension-bold": "^2.0.4", - "@tiptap/extension-bullet-list": "^2.0.4", - "@tiptap/extension-document": "^2.0.4", - "@tiptap/extension-history": "^2.0.4", - "@tiptap/extension-italic": "^2.0.4", - "@tiptap/extension-list-item": "^2.0.4", - "@tiptap/extension-ordered-list": "^2.0.4", - "@tiptap/extension-paragraph": "^2.0.4", - "@tiptap/extension-text": "^2.0.4", + "@tiptap/core": "^2.1.6", + "@tiptap/extension-bold": "^2.1.6", + "@tiptap/extension-bullet-list": "^2.1.6", + "@tiptap/extension-document": "^2.1.6", + "@tiptap/extension-history": "^2.1.6", + "@tiptap/extension-italic": "^2.1.6", + "@tiptap/extension-list-item": "^2.1.6", + "@tiptap/extension-ordered-list": "^2.1.6", + "@tiptap/extension-paragraph": "^2.1.6", + "@tiptap/extension-placeholder": "^2.1.6", + "@tiptap/extension-text": "^2.1.6", "@types/d3-array": "^3.0.4", "@types/d3-random": "^3.0.1", "@types/d3-scale": "^4.0.2", diff --git a/packages/nimble-components/package.json b/packages/nimble-components/package.json index f7edd3b9bc..07c757fa6c 100644 --- a/packages/nimble-components/package.json +++ b/packages/nimble-components/package.json @@ -64,16 +64,17 @@ "@ni/nimble-tokens": "^6.3.0", "@tanstack/table-core": "^8.9.3", "@tanstack/virtual-core": "^3.0.0-beta.44", - "@tiptap/core": "^2.0.4", - "@tiptap/extension-bold": "^2.0.4", - "@tiptap/extension-bullet-list": "^2.0.4", - "@tiptap/extension-document": "^2.0.4", - "@tiptap/extension-history": "^2.0.4", - "@tiptap/extension-italic": "^2.0.4", - "@tiptap/extension-list-item": "^2.0.4", - "@tiptap/extension-ordered-list": "^2.0.4", - "@tiptap/extension-paragraph": "^2.0.4", - "@tiptap/extension-text": "^2.0.4", + "@tiptap/core": "^2.1.6", + "@tiptap/extension-bold": "^2.1.6", + "@tiptap/extension-bullet-list": "^2.1.6", + "@tiptap/extension-document": "^2.1.6", + "@tiptap/extension-history": "^2.1.6", + "@tiptap/extension-italic": "^2.1.6", + "@tiptap/extension-list-item": "^2.1.6", + "@tiptap/extension-ordered-list": "^2.1.6", + "@tiptap/extension-paragraph": "^2.1.6", + "@tiptap/extension-placeholder": "^2.1.6", + "@tiptap/extension-text": "^2.1.6", "@types/d3-array": "^3.0.4", "@types/d3-random": "^3.0.1", "@types/d3-scale": "^4.0.2", diff --git a/packages/nimble-components/src/all-components.ts b/packages/nimble-components/src/all-components.ts index 4ed75dd127..db13e259ad 100644 --- a/packages/nimble-components/src/all-components.ts +++ b/packages/nimble-components/src/all-components.ts @@ -33,8 +33,8 @@ import './menu-item'; import './number-field'; import './radio'; import './radio-group'; -import './rich-text-editor'; -import './rich-text-viewer'; +import './rich-text/editor'; +import './rich-text/viewer'; import './select'; import './spinner'; import './switch'; diff --git a/packages/nimble-components/src/rich-text-editor/index.ts b/packages/nimble-components/src/rich-text-editor/index.ts deleted file mode 100644 index b88609bf97..0000000000 --- a/packages/nimble-components/src/rich-text-editor/index.ts +++ /dev/null @@ -1,361 +0,0 @@ -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'; -import History from '@tiptap/extension-history'; -import Italic from '@tiptap/extension-italic'; -import ListItem from '@tiptap/extension-list-item'; -import OrderedList from '@tiptap/extension-ordered-list'; -import Paragraph from '@tiptap/extension-paragraph'; -import Text from '@tiptap/extension-text'; -import { template } from './template'; -import { styles } from './styles'; -import type { ToggleButton } from '../toggle-button'; - -declare global { - interface HTMLElementTagNameMap { - 'nimble-rich-text-editor': RichTextEditor; - } -} - -/** - * A nimble styled rich text editor - */ -export class RichTextEditor extends FoundationElement { - /** - * @internal - */ - @observable - public boldButton!: ToggleButton; - - /** - * @internal - */ - @observable - public italicsButton!: ToggleButton; - - /** - * @internal - */ - @observable - public bulletListButton!: ToggleButton; - - /** - * @internal - */ - @observable - public numberedListButton!: ToggleButton; - - /** - * @internal - */ - 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(); - if (!this.editor.isConnected) { - this.editorContainer.append(this.editor); - } - this.bindEditorTransactionEvent(); - } - - /** - * @internal - */ - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this.unbindEditorTransactionEvent(); - } - - /** - * Toggle the bold mark and focus back to the editor - * @internal - */ - public boldButtonClick(): void { - this.tiptapEditor.chain().focus().toggleBold().run(); - } - - /** - * Toggle the bold mark and focus back to the editor - * @internal - */ - public boldButtonKeyDown(event: KeyboardEvent): boolean { - if (this.keyActivatesButton(event)) { - this.tiptapEditor.chain().focus().toggleBold().run(); - return false; - } - return true; - } - - /** - * Toggle the italics mark and focus back to the editor - * @internal - */ - public italicsButtonClick(): void { - this.tiptapEditor.chain().focus().toggleItalic().run(); - } - - /** - * Toggle the italics mark and focus back to the editor - * @internal - */ - public italicsButtonKeyDown(event: KeyboardEvent): boolean { - if (this.keyActivatesButton(event)) { - this.tiptapEditor.chain().focus().toggleItalic().run(); - return false; - } - return true; - } - - /** - * Toggle the unordered list node and focus back to the editor - * @internal - */ - public bulletListButtonClick(): void { - this.tiptapEditor.chain().focus().toggleBulletList().run(); - } - - /** - * Toggle the unordered list node and focus back to the editor - * @internal - */ - public bulletListButtonKeyDown(event: KeyboardEvent): boolean { - if (this.keyActivatesButton(event)) { - this.tiptapEditor.chain().focus().toggleBulletList().run(); - return false; - } - return true; - } - - /** - * Toggle the ordered list node and focus back to the editor - * @internal - */ - public numberedListButtonClick(): void { - this.tiptapEditor.chain().focus().toggleOrderedList().run(); - } - - /** - * Toggle the ordered list node and focus back to the editor - * @internal - */ - public numberedListButtonKeyDown(event: KeyboardEvent): boolean { - if (this.keyActivatesButton(event)) { - this.tiptapEditor.chain().focus().toggleOrderedList().run(); - return false; - } - 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 - */ - public stopEventPropagation(event: Event): boolean { - // Don't bubble the 'change' event from the toggle button because - // all the formatting button has its own 'toggle' event through 'click' and 'keydown'. - event.stopPropagation(); - 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 - * Tiptap nodes: https://tiptap.dev/api/nodes - */ - this.tiptapEditor = new Editor({ - element: this.editor, - extensions: [ - Document, - Paragraph, - Text, - BulletList, - OrderedList, - ListItem, - Bold, - Italic, - History - ] - }); - } - - /** - * Binding the "transaction" event to the editor allows continuous monitoring the events and updating the button state in response to - * various actions such as mouse events, keyboard events, changes in the editor content etc,. - * https://tiptap.dev/api/events#transaction - */ - private bindEditorTransactionEvent(): void { - this.tiptapEditor.on('transaction', () => { - this.updateEditorButtonsState(); - }); - } - - private unbindEditorTransactionEvent(): void { - this.tiptapEditor.off('transaction'); - } - - private updateEditorButtonsState(): void { - this.boldButton.checked = this.tiptapEditor.isActive('bold'); - this.italicsButton.checked = this.tiptapEditor.isActive('italic'); - this.bulletListButton.checked = this.tiptapEditor.isActive('bulletList'); - this.numberedListButton.checked = this.tiptapEditor.isActive('orderedList'); - } - - private keyActivatesButton(event: KeyboardEvent): boolean { - switch (event.key) { - case keySpace: - case keyEnter: - return true; - default: - return false; - } - } -} - -const nimbleRichTextEditor = RichTextEditor.compose({ - baseName: 'rich-text-editor', - template, - styles -}); - -DesignSystem.getOrCreate() - .withPrefix('nimble') - .register(nimbleRichTextEditor()); -export const richTextEditorTag = DesignSystem.tagFor(RichTextEditor); diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts new file mode 100644 index 0000000000..173e7a9cf7 --- /dev/null +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -0,0 +1,481 @@ +import { observable, attr, DOM } from '@microsoft/fast-element'; +import { + applyMixins, + ARIAGlobalStatesAndProperties, + DesignSystem, + FoundationElement +} from '@microsoft/fast-foundation'; +import { keyEnter, keySpace } from '@microsoft/fast-web-utilities'; +import { Editor, AnyExtension, Extension } from '@tiptap/core'; +import Bold from '@tiptap/extension-bold'; +import BulletList from '@tiptap/extension-bullet-list'; +import Document from '@tiptap/extension-document'; +import History from '@tiptap/extension-history'; +import Italic from '@tiptap/extension-italic'; +import ListItem from '@tiptap/extension-list-item'; +import OrderedList from '@tiptap/extension-ordered-list'; +import Paragraph from '@tiptap/extension-paragraph'; +import Placeholder from '@tiptap/extension-placeholder'; +import type { PlaceholderOptions } from '@tiptap/extension-placeholder'; +import Text from '@tiptap/extension-text'; +import { template } from './template'; +import { styles } from './styles'; +import type { ToggleButton } from '../../toggle-button'; +import type { ErrorPattern } from '../../patterns/error/types'; +import { RichTextMarkdownParser } from '../models/markdown-parser'; +import { richTextMarkdownSerializer } from '../models/markdown-serializer'; + +declare global { + interface HTMLElementTagNameMap { + 'nimble-rich-text-editor': RichTextEditor; + } +} + +/** + * A nimble styled rich text editor + */ +export class RichTextEditor extends FoundationElement implements ErrorPattern { + /** + * @internal + */ + public editor = this.createEditor(); + + /** + * @internal + */ + public tiptapEditor = this.createTiptapEditor(); + + /** + * Whether to disable user from editing and interacting with toolbar buttons + * + * @public + * HTML Attribute: disabled + */ + @attr({ mode: 'boolean' }) + public disabled = false; + + /** + * Whether to hide the footer of the rich text editor + * + * @public + * HTML Attribute: footer-hidden + */ + @attr({ attribute: 'footer-hidden', mode: 'boolean' }) + public footerHidden = false; + + /** + * Whether to display the error state. + * + * @public + * HTML Attribute: error-visible + */ + @attr({ attribute: 'error-visible', mode: 'boolean' }) + public errorVisible = false; + + /** + * A message explaining why the value is invalid. + * + * @public + * HTML Attribute: error-text + */ + @attr({ attribute: 'error-text' }) + public errorText?: string; + + /** + * @public + * HTML Attribute: placeholder + */ + @attr + public placeholder?: string; + + /** + * True if the editor is empty or contains only whitespace, false otherwise. + * + * @public + */ + public get empty(): boolean { + // Tiptap [isEmpty](https://tiptap.dev/api/editor#is-empty) returns false even if the editor has only whitespace. + // However, the expectation is to return true if the editor is empty or contains only whitespace. + // Hence, by retrieving the current text content using Tiptap state docs and then trimming the string to determine whether it is empty or not. + return this.tiptapEditor.state.doc.textContent.trim().length === 0; + } + + /** + * @internal + */ + @observable + public boldButton!: ToggleButton; + + /** + * @internal + */ + @observable + public italicsButton!: ToggleButton; + + /** + * @internal + */ + @observable + public bulletListButton!: ToggleButton; + + /** + * @internal + */ + @observable + public numberedListButton!: ToggleButton; + + /** + * The width of the vertical scrollbar, if displayed. + * @internal + */ + @observable + public scrollbarWidth = -1; + + /** + * @internal + */ + public editorContainer!: HTMLDivElement; + + private resizeObserver?: ResizeObserver; + private updateScrollbarWidthQueued = false; + + private readonly markdownParser = new RichTextMarkdownParser(); + private readonly markdownSerializer = richTextMarkdownSerializer(); + private readonly xmlSerializer = new XMLSerializer(); + + /** + * @internal + */ + public override connectedCallback(): void { + super.connectedCallback(); + if (!this.editor.isConnected) { + this.editorContainer.append(this.editor); + } + this.bindEditorTransactionEvent(); + this.bindEditorUpdateEvent(); + this.stopNativeInputEventPropagation(); + this.resizeObserver = new ResizeObserver(() => this.onResize()); + this.resizeObserver.observe(this); + } + + /** + * @internal + */ + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this.unbindEditorTransactionEvent(); + this.unbindEditorUpdateEvent(); + this.unbindNativeInputEvent(); + this.resizeObserver?.disconnect(); + } + + /** + * @internal + */ + public disabledChanged(): void { + this.tiptapEditor.setEditable(!this.disabled); + this.setEditorTabIndex(); + this.editor.setAttribute( + 'aria-disabled', + this.disabled ? 'true' : 'false' + ); + } + + /** + * Update the placeholder text and view of the editor. + * @internal + */ + public placeholderChanged(): void { + const placeholderExtension = this.getTipTapExtension( + 'placeholder' + ) as Extension; + placeholderExtension.options.placeholder = this.placeholder ?? ''; + this.tiptapEditor.view.dispatch(this.tiptapEditor.state.tr); + + this.queueUpdateScrollbarWidth(); + } + + /** + * @internal + */ + public ariaLabelChanged(): void { + if (this.ariaLabel !== null && this.ariaLabel !== undefined) { + this.editor.setAttribute('aria-label', this.ariaLabel); + } else { + this.editor.removeAttribute('aria-label'); + } + } + + /** + * Toggle the bold mark and focus back to the editor + * @internal + */ + public boldButtonClick(): void { + this.tiptapEditor.chain().focus().toggleBold().run(); + } + + /** + * Toggle the bold mark and focus back to the editor + * @internal + */ + public boldButtonKeyDown(event: KeyboardEvent): boolean { + if (this.keyActivatesButton(event)) { + this.tiptapEditor.chain().focus().toggleBold().run(); + return false; + } + return true; + } + + /** + * Toggle the italics mark and focus back to the editor + * @internal + */ + public italicsButtonClick(): void { + this.tiptapEditor.chain().focus().toggleItalic().run(); + } + + /** + * Toggle the italics mark and focus back to the editor + * @internal + */ + public italicsButtonKeyDown(event: KeyboardEvent): boolean { + if (this.keyActivatesButton(event)) { + this.tiptapEditor.chain().focus().toggleItalic().run(); + return false; + } + return true; + } + + /** + * Toggle the unordered list node and focus back to the editor + * @internal + */ + public bulletListButtonClick(): void { + this.tiptapEditor.chain().focus().toggleBulletList().run(); + } + + /** + * Toggle the unordered list node and focus back to the editor + * @internal + */ + public bulletListButtonKeyDown(event: KeyboardEvent): boolean { + if (this.keyActivatesButton(event)) { + this.tiptapEditor.chain().focus().toggleBulletList().run(); + return false; + } + return true; + } + + /** + * Toggle the ordered list node and focus back to the editor + * @internal + */ + public numberedListButtonClick(): void { + this.tiptapEditor.chain().focus().toggleOrderedList().run(); + } + + /** + * Toggle the ordered list node and focus back to the editor + * @internal + */ + public numberedListButtonKeyDown(event: KeyboardEvent): boolean { + if (this.keyActivatesButton(event)) { + this.tiptapEditor.chain().focus().toggleOrderedList().run(); + return false; + } + 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 + */ + public stopEventPropagation(event: Event): boolean { + // Don't bubble the 'change' event from the toggle button because + // all the formatting button has its own 'toggle' event through 'click' and 'keydown'. + event.stopPropagation(); + return false; + } + + private createEditor(): HTMLDivElement { + const editor = document.createElement('div'); + editor.className = 'editor'; + editor.setAttribute('aria-multiline', 'true'); + editor.setAttribute('role', 'textbox'); + editor.setAttribute('aria-disabled', 'false'); + return editor; + } + + private createTiptapEditor(): Editor { + /** + * For more information on the extensions for the supported formatting options, refer to the links below. + * Tiptap marks: https://tiptap.dev/api/marks + * Tiptap nodes: https://tiptap.dev/api/nodes + */ + return new Editor({ + element: this.editor, + extensions: [ + Document, + Paragraph, + Text, + BulletList, + OrderedList, + ListItem, + Bold, + Italic, + History, + Placeholder.configure({ + placeholder: '', + showOnlyWhenEditable: false + }) + ] + }); + } + + /** + * This function takes the Fragment from parseMarkdownToDOM function and return the serialized string using XMLSerializer + */ + private getHtmlContent(markdown: string): string { + const documentFragment = this.markdownParser.parseMarkdownToDOM(markdown); + return this.xmlSerializer.serializeToString(documentFragment); + } + + /** + * Binding the "transaction" event to the editor allows continuous monitoring the events and updating the button state in response to + * various actions such as mouse events, keyboard events, changes in the editor content etc,. + * https://tiptap.dev/api/events#transaction + */ + private bindEditorTransactionEvent(): void { + this.tiptapEditor.on('transaction', () => { + this.updateEditorButtonsState(); + }); + } + + private unbindEditorTransactionEvent(): void { + this.tiptapEditor.off('transaction'); + } + + private updateEditorButtonsState(): void { + this.boldButton.checked = this.tiptapEditor.isActive('bold'); + this.italicsButton.checked = this.tiptapEditor.isActive('italic'); + this.bulletListButton.checked = this.tiptapEditor.isActive('bulletList'); + this.numberedListButton.checked = this.tiptapEditor.isActive('orderedList'); + } + + private keyActivatesButton(event: KeyboardEvent): boolean { + switch (event.key) { + case keySpace: + case keyEnter: + return true; + default: + return false; + } + } + + private unbindEditorUpdateEvent(): void { + this.tiptapEditor.off('update'); + } + + /** + * input event is fired when there is a change in the content of the editor. + * + * https://tiptap.dev/api/events#update + */ + private bindEditorUpdateEvent(): void { + this.tiptapEditor.on('update', () => { + this.$emit('input'); + this.queueUpdateScrollbarWidth(); + }); + } + + /** + * Stopping the native input event propagation emitted by the contenteditable element in the Tiptap + * since there is an issue (linked below) in ProseMirror where selecting the text and removing it + * does not trigger the native HTMLElement input event. So using the "update" event emitted by the + * Tiptap to capture it as an "input" customEvent in the rich text editor. + * + * Prose Mirror issue: https://discuss.prosemirror.net/t/how-to-handle-select-backspace-delete-cut-type-kind-of-events-handletextinput-or-handledomevents-input-doesnt-help/4844 + */ + private stopNativeInputEventPropagation(): void { + this.tiptapEditor.view.dom.addEventListener('input', event => { + event.stopPropagation(); + }); + } + + private unbindNativeInputEvent(): void { + this.tiptapEditor.view.dom.removeEventListener('input', () => {}); + } + + private queueUpdateScrollbarWidth(): void { + if (!this.$fastController.isConnected) { + return; + } + if (!this.updateScrollbarWidthQueued) { + this.updateScrollbarWidthQueued = true; + DOM.queueUpdate(() => this.updateScrollbarWidth()); + } + } + + private updateScrollbarWidth(): void { + this.updateScrollbarWidthQueued = false; + this.scrollbarWidth = this.tiptapEditor.view.dom.offsetWidth + - this.tiptapEditor.view.dom.clientWidth; + } + + private onResize(): void { + this.scrollbarWidth = this.tiptapEditor.view.dom.offsetWidth + - this.tiptapEditor.view.dom.clientWidth; + } + + private getTipTapExtension( + extensionName: string + ): AnyExtension | undefined { + return this.tiptapEditor.extensionManager.extensions.find( + extension => extension.name === extensionName + ); + } + + private setEditorTabIndex(): void { + this.tiptapEditor.setOptions({ + editorProps: { + attributes: { + tabindex: this.disabled ? '-1' : '0' + } + } + }); + } +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface RichTextEditor extends ARIAGlobalStatesAndProperties {} +applyMixins(RichTextEditor, ARIAGlobalStatesAndProperties); + +const nimbleRichTextEditor = RichTextEditor.compose({ + baseName: 'rich-text-editor', + template, + styles +}); + +DesignSystem.getOrCreate() + .withPrefix('nimble') + .register(nimbleRichTextEditor()); +export const richTextEditorTag = DesignSystem.tagFor(RichTextEditor); diff --git a/packages/nimble-components/src/rich-text-editor/specs/README.md b/packages/nimble-components/src/rich-text/editor/specs/README.md similarity index 96% rename from packages/nimble-components/src/rich-text-editor/specs/README.md rename to packages/nimble-components/src/rich-text/editor/specs/README.md index d5928ee243..1157db3c4b 100644 --- a/packages/nimble-components/src/rich-text-editor/specs/README.md +++ b/packages/nimble-components/src/rich-text/editor/specs/README.md @@ -102,8 +102,8 @@ Example usage of the `nimble-rich-text-editor` in the application layer is as fo _Props/Attrs_ -- `empty` - is a read-only property that indicates whether the editor is empty or not. This will be achieved through Tiptap's - [isEmpty](https://tiptap.dev/api/editor#is-empty) API. The component and the Angular directive will have a getter method +- `empty` - is a read-only property that indicates whether the editor is empty or not. This will be achieved by retrieving the current text + content from the editor and calculating its length. The component and the Angular directive will have a getter method that can be used to bind it in the Angular application. - `fit-to-content` - is a boolean attribute allows the text area to expand vertically to fit the content. - `placeholder` - is a string attribute to include a placeholder text for the editor when it is empty. This text is passed as plain text (not markdown) @@ -150,6 +150,14 @@ problematic when attempting to clear the editor's content by setting the markdow empty and hasn't undergone processing. To overcome this issue, utilizing `methods` could offer a potential solution, allowing the content to be set regardless of whether it has changed from its previous value. +_empty_ + +We considered utilizing Tiptap's [isEmpty](https://tiptap.dev/api/editor#is-empty) API to determine whether the editor is empty. However, this API +does not return true if the editor only consists of whitespace. In the context of the comments feature, this property is exposed to find out the +editor's empty state, even when it contains only whitespace. This is necessary because the Backend service for comments does not permit the +creation of comments comprised of just whitespace. Consequently, by using this property, we should disable the `OK` button when the editor is +empty. To achieve this, we retrieve the current text content value, trim the string, and return true if its length is zero. + _Events_ - `input` - event emitted when there is a change in the editor. This can be achieved through Tiptap's [update event](https://tiptap.dev/api/events#update). diff --git a/packages/nimble-components/src/rich-text-editor/specs/spec-images/button-state.png b/packages/nimble-components/src/rich-text/editor/specs/spec-images/button-state.png similarity index 100% rename from packages/nimble-components/src/rich-text-editor/specs/spec-images/button-state.png rename to packages/nimble-components/src/rich-text/editor/specs/spec-images/button-state.png diff --git a/packages/nimble-components/src/rich-text-editor/specs/spec-images/editor-sample.png b/packages/nimble-components/src/rich-text/editor/specs/spec-images/editor-sample.png similarity index 100% rename from packages/nimble-components/src/rich-text-editor/specs/spec-images/editor-sample.png rename to packages/nimble-components/src/rich-text/editor/specs/spec-images/editor-sample.png diff --git a/packages/nimble-components/src/rich-text-editor/specs/spec-images/viewer-sample.png b/packages/nimble-components/src/rich-text/editor/specs/spec-images/viewer-sample.png similarity index 100% rename from packages/nimble-components/src/rich-text-editor/specs/spec-images/viewer-sample.png rename to packages/nimble-components/src/rich-text/editor/specs/spec-images/viewer-sample.png diff --git a/packages/nimble-components/src/rich-text-editor/styles.ts b/packages/nimble-components/src/rich-text/editor/styles.ts similarity index 64% rename from packages/nimble-components/src/rich-text-editor/styles.ts rename to packages/nimble-components/src/rich-text/editor/styles.ts index e32148e60e..2af9ce8127 100644 --- a/packages/nimble-components/src/rich-text-editor/styles.ts +++ b/packages/nimble-components/src/rich-text/editor/styles.ts @@ -1,17 +1,24 @@ import { css } from '@microsoft/fast-element'; import { display } from '@microsoft/fast-foundation'; import { + bodyDisabledFontColor, bodyFont, bodyFontColor, borderHoverColor, borderRgbPartialColor, borderWidth, + controlLabelFontColor, + controlLabelDisabledFontColor, + failColor, + iconSize, smallDelay, standardPadding -} from '../theme-provider/design-tokens'; +} from '../../theme-provider/design-tokens'; +import { styles as errorStyles } from '../../patterns/error/styles'; export const styles = css` ${display('inline-flex')} + ${errorStyles} :host { font: ${bodyFont}; @@ -21,6 +28,10 @@ export const styles = css` --ni-private-rich-text-editor-hover-indicator-width: calc( ${borderWidth} + 1px ); + ${ + /** Initial height of rich text editor with one line space when the footer is visible. */ '' + } + height: 82px; --ni-private-rich-text-editor-footer-section-height: 40px; ${ /** Minimum width is added to accommodate all the possible buttons in the toolbar and to support the mobile width. */ '' @@ -29,6 +40,7 @@ export const styles = css` } .container { + box-sizing: border-box; display: flex; flex-direction: column; position: relative; @@ -60,38 +72,56 @@ export const styles = css` } } + :host([disabled]) .container { + color: ${bodyDisabledFontColor}; + border: ${borderWidth} solid rgba(${borderRgbPartialColor}, 0.1); + } + + :host([error-visible]) .container { + border-bottom-color: ${failColor}; + } + :host(:hover) .container::after { - width: 100%; + width: calc(100% + 2 * ${borderWidth}); + } + + :host([disabled]:hover) .container::after { + width: 0px; + } + + :host([error-visible]) .container::after { + border-bottom-color: ${failColor}; + } + + .editor-container { + display: contents; } .editor { + display: flex; + flex-direction: column; border: ${borderWidth} solid transparent; border-radius: 0px; - height: calc( - 100% - var(--ni-private-rich-text-editor-footer-section-height) - ); - overflow: auto; + flex: 1; + overflow: hidden; } - .editor-container { - display: contents; + :host([footer-hidden]) .editor { + height: 100%; } .ProseMirror { - ${ - /** - * Min height represents the one line space for the initial view and max height is referred from the visual design. - * However, max height will be `fit-content` when the `fit-to-content` attribute for the editor component is implemented. - */ '' - } - min-height: 32px; - max-height: 132px; + overflow: auto; height: 100%; - border: ${borderWidth} solid transparent; + border: 0px; border-radius: 0px; background-color: transparent; font: inherit; padding: 8px; + ${ + /* This padding ensures that showing/hiding the error icon doesn't affect text layout */ '' + } + padding-right: calc(${iconSize}); box-sizing: border-box; position: relative; color: inherit; @@ -139,15 +169,39 @@ export const styles = css` margin-block: 0; } + ${ + /** + * Styles provided by Tiptap are necessary to display the placeholder value when the editor is empty. + * Tiptap doc reference: https://tiptap.dev/api/extensions/placeholder#additional-setup + */ '' + } + .ProseMirror p.is-editor-empty:first-child::before { + color: ${controlLabelFontColor}; + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; + word-break: break-word; + } + + :host([disabled]) .ProseMirror p.is-editor-empty:first-child::before { + color: ${controlLabelDisabledFontColor}; + } + .footer-section { display: flex; justify-content: space-between; + flex-shrink: 0; border: ${borderWidth} solid transparent; border-top-color: rgba(${borderRgbPartialColor}, 0.1); height: var(--ni-private-rich-text-editor-footer-section-height); overflow: hidden; } + :host([footer-hidden]) .footer-section { + display: none; + } + nimble-toolbar::part(positioning-region) { background: transparent; padding-right: 8px; @@ -164,4 +218,15 @@ export const styles = css` gap: ${standardPadding}; place-items: center; } + + :host([error-visible]) .error-icon { + display: none; + } + + :host([error-visible]) .error-icon.scrollbar-width-calculated { + display: inline-flex; + position: absolute; + top: calc(${standardPadding} / 2); + right: var(--ni-private-rich-text-editor-scrollbar-width); + } `; diff --git a/packages/nimble-components/src/rich-text-editor/template.ts b/packages/nimble-components/src/rich-text/editor/template.ts similarity index 75% rename from packages/nimble-components/src/rich-text-editor/template.ts rename to packages/nimble-components/src/rich-text/editor/template.ts index e8db67808a..f171ade200 100644 --- a/packages/nimble-components/src/rich-text-editor/template.ts +++ b/packages/nimble-components/src/rich-text/editor/template.ts @@ -1,11 +1,13 @@ import { html, ref } from '@microsoft/fast-element'; import type { RichTextEditor } from '.'; -import { toolbarTag } from '../toolbar'; -import { toggleButtonTag } from '../toggle-button'; -import { iconBoldBTag } from '../icons/bold-b'; -import { iconItalicITag } from '../icons/italic-i'; -import { iconListTag } from '../icons/list'; -import { iconNumberListTag } from '../icons/number-list'; +import { toolbarTag } from '../../toolbar'; +import { toggleButtonTag } from '../../toggle-button'; +import { iconBoldBTag } from '../../icons/bold-b'; +import { iconItalicITag } from '../../icons/italic-i'; +import { iconListTag } from '../../icons/list'; +import { iconNumberListTag } from '../../icons/number-list'; +import { errorTextTemplate } from '../../patterns/error/template'; +import { iconExclamationMarkTag } from '../../icons/exclamation-mark'; // prettier-ignore export const template = html` @@ -13,12 +15,18 @@ export const template = html`
-
`; diff --git a/packages/nimble-components/src/rich-text-editor/testing/rich-text-editor.pageobject.ts b/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts similarity index 75% rename from packages/nimble-components/src/rich-text-editor/testing/rich-text-editor.pageobject.ts rename to packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts index 2ad02b8ee3..e81c375ab6 100644 --- a/packages/nimble-components/src/rich-text-editor/testing/rich-text-editor.pageobject.ts +++ b/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts @@ -1,7 +1,7 @@ import { keySpace, keyEnter, keyTab } from '@microsoft/fast-web-utilities'; import type { RichTextEditor } from '..'; -import { waitForUpdatesAsync } from '../../testing/async-helpers'; -import type { ToggleButton } from '../../toggle-button'; +import { waitForUpdatesAsync } from '../../../testing/async-helpers'; +import type { ToggleButton } from '../../../toggle-button'; import type { ToolbarButton } from './types'; /** @@ -72,38 +72,22 @@ export class RichTextEditorPageObject { await waitForUpdatesAsync(); } - /** - * To click a formatting button in the footer section, pass its position value as an index (starting from '0') - * @param button can be imported from an enum for each button using the `ButtonIndex`. - */ public async clickFooterButton(button: ToolbarButton): Promise { const toggleButton = this.getFormattingButton(button); toggleButton!.click(); await waitForUpdatesAsync(); } - /** - * To retrieve the checked state of the button, provide its position value as an index (starting from '0') - * @param button can be imported from an enum for each button using the `ButtonIndex`. - */ public getButtonCheckedState(button: ToolbarButton): boolean { const toggleButton = this.getFormattingButton(button); return toggleButton!.checked; } - /** - * To retrieve the tab index of the button, provide its position value as an index (starting from '0') - * @param button can be imported from an enum for each button using the `ButtonIndex`. - */ public getButtonTabIndex(button: ToolbarButton): number { const toggleButton = this.getFormattingButton(button); return toggleButton!.tabIndex; } - /** - * To trigger a space key press for the button, provide its position value as an index (starting from '0') - * @param button can be imported from an enum for each button using the `ButtonIndex`. - */ public spaceKeyActivatesButton(button: ToolbarButton): void { const toggleButton = this.getFormattingButton(button)!; const event = new KeyboardEvent('keypress', { @@ -112,10 +96,6 @@ export class RichTextEditorPageObject { toggleButton.control.dispatchEvent(event); } - /** - * To trigger a enter key press for the button, provide its position value as an index (starting from '0') - * @param button can be imported from an enum for each button using the `ButtonIndex`. - */ public enterKeyActivatesButton(button: ToolbarButton): void { const toggleButton = this.getFormattingButton(button)!; const event = new KeyboardEvent('keypress', { @@ -156,10 +136,53 @@ export class RichTextEditorPageObject { .map(el => el.textContent || ''); } + public getEditorTabIndex(): string { + return this.getTiptapEditor()?.getAttribute('tabindex') ?? ''; + } + + public async setFooterHidden(footerHidden: boolean): Promise { + if (footerHidden) { + this.richTextEditorElement.setAttribute('footer-hidden', ''); + } else { + this.richTextEditorElement.removeAttribute('footer-hidden'); + } + await waitForUpdatesAsync(); + } + + public isFooterHidden(): boolean { + const footerSection = this.getFooter()!; + return window.getComputedStyle(footerSection).display === 'none'; + } + + public async setDisabled(disabled: boolean): Promise { + if (disabled) { + this.richTextEditorElement.setAttribute('disabled', ''); + } else { + this.richTextEditorElement.removeAttribute('disabled'); + } + await waitForUpdatesAsync(); + } + + public isButtonDisabled(button: ToolbarButton): boolean { + const toggleButton = this.getFormattingButton(button)!; + return toggleButton.hasAttribute('disabled'); + } + + public getPlaceholderValue(): string { + const editor = this.getTiptapEditor()!; + return editor.firstElementChild?.getAttribute('data-placeholder') ?? ''; + } + private getEditorSection(): Element | null | undefined { return this.richTextEditorElement.shadowRoot?.querySelector('.editor'); } + private getFooter(): Element | null | undefined { + return this.richTextEditorElement.shadowRoot!.querySelector( + '.footer-section' + ); + } + private getTiptapEditor(): Element | null | undefined { return this.richTextEditorElement.shadowRoot?.querySelector( '.ProseMirror' @@ -167,11 +190,11 @@ export class RichTextEditorPageObject { } private getFormattingButton( - index: ToolbarButton + button: ToolbarButton ): ToggleButton | null | undefined { const buttons: NodeListOf = this.richTextEditorElement.shadowRoot!.querySelectorAll( 'nimble-toggle-button' ); - return buttons[index]; + return buttons[button]; } } diff --git a/packages/nimble-components/src/rich-text-editor/testing/types.ts b/packages/nimble-components/src/rich-text/editor/testing/types.ts similarity index 100% rename from packages/nimble-components/src/rich-text-editor/testing/types.ts rename to packages/nimble-components/src/rich-text/editor/testing/types.ts diff --git a/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor-matrix.stories.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts similarity index 54% rename from packages/nimble-components/src/rich-text-editor/tests/rich-text-editor-matrix.stories.ts rename to packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts index a88a07e49a..42223413be 100644 --- a/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor-matrix.stories.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts @@ -3,19 +3,25 @@ import { html, ViewTemplate } from '@microsoft/fast-element'; import { createMatrixThemeStory, createStory -} from '../../utilities/tests/storybook'; +} from '../../../utilities/tests/storybook'; import { createMatrix, sharedMatrixParameters -} from '../../utilities/tests/matrix'; -import { hiddenWrapper } from '../../utilities/tests/hidden'; +} from '../../../utilities/tests/matrix'; +import { hiddenWrapper } from '../../../utilities/tests/hidden'; import { richTextEditorTag } from '..'; import { cssPropertyFromTokenName, tokenNames -} from '../../theme-provider/design-token-names'; -import { buttonTag } from '../../button'; -import { loremIpsum } from '../../utilities/tests/lorem-ipsum'; +} from '../../../theme-provider/design-token-names'; +import { buttonTag } from '../../../button'; +import { loremIpsum } from '../../../utilities/tests/lorem-ipsum'; +import { + DisabledState, + ErrorState, + disabledStates, + errorStates +} from '../../../utilities/tests/states'; const metadata: Meta = { title: 'Tests/Rich Text Editor', @@ -28,9 +34,43 @@ const richTextMarkdownString = '1. **Bold*Italics***'; export default metadata; +const footerHiddenStates = [ + ['Footer Visible', false], + ['Footer Hidden', true] +] as const; +type FooterHiddenState = (typeof footerHiddenStates)[number]; + +const placeholderValueStates = [ + ['', null], + ['Placeholder', 'Placeholder text'] +] as const; +type PlaceholderValueStates = (typeof placeholderValueStates)[number]; + // prettier-ignore -const component = (): ViewTemplate => html` - <${richTextEditorTag}> +const component = ( + [disabledName, disabled]: DisabledState, + [footerHiddenName, footerHidden]: FooterHiddenState, + [errorStateName, isError, errorText]: ErrorState, + [placeholderName, placeholderText]: PlaceholderValueStates +): ViewTemplate => html` +

+ ${() => footerHiddenName} ${() => errorStateName} ${() => placeholderName} ${() => disabledName} +

+ <${richTextEditorTag} + style="margin: 5px 0px; width: 500px;" + ?disabled="${() => disabled}" + ?footer-hidden="${() => footerHidden}" + ?error-visible="${() => isError}" + error-text="${() => errorText}" + placeholder="${() => placeholderText}" + > + `; const playFunction = (): void => { @@ -38,15 +78,22 @@ const playFunction = (): void => { editorNodeList.forEach(element => element.setMarkdown(richTextMarkdownString)); }; +const longTextPlayFunction = (): void => { + const editorNodeList = document.querySelectorAll('nimble-rich-text-editor'); + editorNodeList.forEach(element => element.setMarkdown( + `${loremIpsum}\n\n **${loremIpsum}**\n\n ${loremIpsum}` + )); +}; + const editorSizingTestCase = ( [widthLabel, widthStyle]: [string, string], [heightLabel, heightStyle]: [string, string] ): ViewTemplate => html`

${widthLabel}; ${heightLabel}

+ )}); margin-bottom: 0px;">${() => widthLabel}; ${() => heightLabel}

- <${richTextEditorTag} style="${widthStyle}; ${heightStyle};"> + <${richTextEditorTag} style="${() => widthStyle}; ${() => heightStyle};"> <${buttonTag} slot="footer-actions" appearance="ghost">Cancel <${buttonTag} slot="footer-actions" appearance="outline">Ok @@ -54,11 +101,34 @@ const editorSizingTestCase = ( `; export const richTextEditorThemeMatrix: StoryFn = createMatrixThemeStory( - createMatrix(component) + createMatrix(component, [ + disabledStates, + footerHiddenStates, + errorStates, + [placeholderValueStates[0]] + ]) ); - richTextEditorThemeMatrix.play = playFunction; +export const errorStateThemeMatrixWithLengthyContent: StoryFn = createMatrixThemeStory( + createMatrix(component, [ + [disabledStates[0]], + [footerHiddenStates[0]], + errorStates, + [placeholderValueStates[0]] + ]) +); +errorStateThemeMatrixWithLengthyContent.play = longTextPlayFunction; + +export const placeholderStateThemeMatrix: StoryFn = createMatrixThemeStory( + createMatrix(component, [ + disabledStates, + [footerHiddenStates[0]], + [errorStates[0]], + placeholderValueStates + ]) +); + export const richTextEditorSizing: StoryFn = createStory(html` ${createMatrix(editorSizingTestCase, [ [ @@ -82,7 +152,6 @@ const mobileWidthComponent = html` `; export const plainTextContentInMobileWidth: StoryFn = createStory(mobileWidthComponent); - plainTextContentInMobileWidth.play = (): void => { document.querySelector('nimble-rich-text-editor')!.setMarkdown(loremIpsum); }; @@ -99,7 +168,6 @@ const multipleSubPointsContent = ` 1. Sub point 9`; export const multipleSubPointsContentInMobileWidth: StoryFn = createStory(mobileWidthComponent); - multipleSubPointsContentInMobileWidth.play = (): void => { document .querySelector('nimble-rich-text-editor')! @@ -107,7 +175,6 @@ multipleSubPointsContentInMobileWidth.play = (): void => { }; export const longWordContentInMobileWidth: StoryFn = createStory(mobileWidthComponent); - longWordContentInMobileWidth.play = (): void => { document .querySelector('nimble-rich-text-editor')! @@ -115,6 +182,7 @@ longWordContentInMobileWidth.play = (): void => { 'ThisIsALongWordWithoutSpaceToTestLongWordInSmallWidthThisIsALongWordWithoutSpaceToTestLongWordInSmallWidth' ); }; + export const hiddenRichTextEditor: StoryFn = createStory( hiddenWrapper(html`<${richTextEditorTag} hidden>`) ); diff --git a/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts similarity index 86% rename from packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.spec.ts rename to packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index 761db3fb45..7769ef0065 100644 --- a/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -1,12 +1,13 @@ import { html } from '@microsoft/fast-element'; import { richTextEditorTag, RichTextEditor } from '..'; -import { type Fixture, fixture } from '../../utilities/tests/fixture'; -import { getSpecTypeByNamedList } from '../../utilities/tests/parameterized'; +import { type Fixture, fixture } from '../../../utilities/tests/fixture'; +import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; import { RichTextEditorPageObject } from '../testing/rich-text-editor.pageobject'; -import { wackyStrings } from '../../utilities/tests/wacky-strings'; -import type { Button } from '../../button'; -import type { ToggleButton } from '../../toggle-button'; +import { wackyStrings } from '../../../utilities/tests/wacky-strings'; +import type { Button } from '../../../button'; +import type { ToggleButton } from '../../../toggle-button'; import { ToolbarButton } from '../testing/types'; +import { createEventListener } from '../../../utilities/tests/component'; async function setup(): Promise> { return fixture( @@ -47,7 +48,7 @@ describe('RichTextEditor', () => { it('should initialize Tiptap editor', () => { expect(pageObject.editorSectionHasChildNodes()).toBeTrue(); expect(pageObject.getEditorSectionFirstElementChildClassName()).toBe( - 'ProseMirror' + 'tiptap ProseMirror' ); }); @@ -63,6 +64,34 @@ describe('RichTextEditor', () => { expect(editor!.getAttribute('aria-multiline')).toBe('true'); }); + it('should initialize "aria-label" with undefined when there is no "aria-label" set in the element', () => { + const editor = element.shadowRoot?.querySelector('.editor'); + + expect(editor!.hasAttribute('aria-label')).toBeFalse(); + }); + + it('should forwards value of aria-label to internal control', () => { + const editor = element.shadowRoot?.querySelector('.editor'); + element.ariaLabel = 'Rich Text Editor'; + + expect(editor!.getAttribute('aria-label')).toBe('Rich Text Editor'); + }); + + it('should support setting blank "aria-label" value when setting empty string', () => { + const editor = element.shadowRoot?.querySelector('.editor'); + element.ariaLabel = ''; + + expect(editor!.getAttribute('aria-label')).toBe(''); + }); + + it('should remove value of aria-label from internal control when cleared from host', () => { + const editor = element.shadowRoot?.querySelector('.editor'); + element.ariaLabel = 'not empty'; + element.ariaLabel = null; + + expect(editor!.getAttribute('aria-label')).toBeNull(); + }); + it('should have either one of the list buttons checked at the same time on click', async () => { expect( pageObject.getButtonCheckedState(ToolbarButton.bulletList) @@ -146,7 +175,6 @@ describe('RichTextEditor', () => { for (const value of formattingButtons) { const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `"${value.name}" button click check`, // eslint-disable-next-line @typescript-eslint/no-loop-func @@ -180,7 +208,6 @@ describe('RichTextEditor', () => { for (const value of formattingButtons) { const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `"${value.name}" button key press check`, // eslint-disable-next-line @typescript-eslint/no-loop-func @@ -211,7 +238,6 @@ describe('RichTextEditor', () => { for (const value of formattingButtons) { const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `"${value.name}" button key press check`, // eslint-disable-next-line @typescript-eslint/no-loop-func @@ -242,7 +268,6 @@ describe('RichTextEditor', () => { for (const value of formattingButtons) { const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `"${value.name}" button keyboard shortcut check`, // eslint-disable-next-line @typescript-eslint/no-loop-func @@ -274,7 +299,6 @@ describe('RichTextEditor', () => { for (const value of formattingButtons) { const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `"${value.name}" button not propagate change event to parent element`, // eslint-disable-next-line @typescript-eslint/no-loop-func @@ -603,7 +627,6 @@ describe('RichTextEditor', () => { wackyStrings.forEach(value => { const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `wacky string "${value.name}" that are unmodified when rendered the same value within paragraph tag`, // eslint-disable-next-line @typescript-eslint/no-loop-func @@ -914,7 +937,6 @@ describe('RichTextEditor', () => { const disabled: string[] = []; for (const value of notSupportedMarkdownStrings) { const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `string "${value.name}" renders as plain text "${value.name}" within paragraph tag`, // eslint-disable-next-line @typescript-eslint/no-loop-func @@ -946,7 +968,6 @@ describe('RichTextEditor', () => { focused, disabled ); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `wacky string "${value.name}" that are unmodified when set the same "${value.name}" within paragraph tag`, // eslint-disable-next-line @typescript-eslint/no-loop-func @@ -982,7 +1003,6 @@ describe('RichTextEditor', () => { for (const value of modifiedWackyStrings) { const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `wacky string "${value.name}" modified when rendered`, // eslint-disable-next-line @typescript-eslint/no-loop-func @@ -1146,7 +1166,6 @@ describe('RichTextEditor', () => { const disabled: string[] = []; for (const value of notSupportedMarkdownStrings) { const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `markdown string "${value.name}" returns as plain text "${value.name}" without any change`, // eslint-disable-next-line @typescript-eslint/no-loop-func @@ -1190,7 +1209,6 @@ describe('RichTextEditor', () => { const disabled: string[] = []; for (const value of specialMarkdownStrings) { const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `special markdown string "${value.name}" returns as plain text "${value.value}" with added esacpe character`, // eslint-disable-next-line @typescript-eslint/no-loop-func @@ -1223,7 +1241,6 @@ describe('RichTextEditor', () => { focused, disabled ); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `wacky string "${value.name}" returns unmodified when set the same markdown string"${value.name}"`, // eslint-disable-next-line @typescript-eslint/no-loop-func @@ -1254,9 +1271,8 @@ describe('RichTextEditor', () => { const disabled: string[] = []; for (const value of wackyStringWithSpecialMarkdownCharacter) { const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( - ` wacky string contains special markdown syntax "${value.name}" returns as plain text "${value.value}" with added esacpe character`, + ` wacky string contains special markdown syntax "${value.name}" returns as plain text "${value.value}" with added escape character`, // eslint-disable-next-line @typescript-eslint/no-loop-func async () => { element.setMarkdown(value.name); @@ -1286,7 +1302,6 @@ describe('RichTextEditor', () => { for (const value of modifiedWackyStrings) { const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `wacky string "${value.name}" returns modified when assigned`, // eslint-disable-next-line @typescript-eslint/no-loop-func @@ -1302,6 +1317,182 @@ describe('RichTextEditor', () => { ); } }); + + describe('disabled state', () => { + it('should reflect disabled value to the aria-disabled of editor-section', async () => { + const editor = element.shadowRoot?.querySelector('.editor'); + expect(editor!.getAttribute('aria-disabled')).toBe('false'); + + await pageObject.setDisabled(true); + + expect(editor!.getAttribute('aria-disabled')).toBe('true'); + }); + + it('should reflect disabled value to the "contenteditable" attribute of tiptap editor', async () => { + const editor = element.shadowRoot?.querySelector('.ProseMirror'); + expect(editor!.getAttribute('contenteditable')).toBe('true'); + + await pageObject.setDisabled(true); + + expect(editor!.getAttribute('contenteditable')).toBe('false'); + }); + + it('should enable the editor when "disabled" attribute is set and removed', async () => { + const editor = element.shadowRoot?.querySelector('.ProseMirror'); + expect(pageObject.getEditorTabIndex()).toBe('0'); + + await pageObject.setDisabled(true); + await pageObject.setDisabled(false); + + expect(editor!.getAttribute('contenteditable')).toBe('true'); + }); + + it('should change the tabindex value of the editor when disabled value changes', async () => { + expect(pageObject.getEditorTabIndex()).toBe('0'); + + await pageObject.setDisabled(true); + + expect(pageObject.getEditorTabIndex()).toBe('-1'); + }); + + describe('should reflect disabled value to the disabled and aria-disabled state of toggle buttons', () => { + const focused: string[] = []; + const disabled: string[] = []; + for (const value of formattingButtons) { + const specType = getSpecTypeByNamedList( + value, + focused, + disabled + ); + specType( + `for "${value.name}" button`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + async () => { + expect( + pageObject.isButtonDisabled( + value.toolbarButtonIndex + ) + ).toBeFalse(); + + await pageObject.setDisabled(true); + + expect( + pageObject.isButtonDisabled( + value.toolbarButtonIndex + ) + ).toBeTrue(); + } + ); + } + }); + }); + + it('should hide the footer when "footer-hidden" attribute is enabled', async () => { + expect(pageObject.isFooterHidden()).toBeFalse(); + + await pageObject.setFooterHidden(true); + + expect(pageObject.isFooterHidden()).toBeTrue(); + }); + + it('should show the footer when "footer-hidden" attribute is disabled', async () => { + expect(pageObject.isFooterHidden()).toBeFalse(); + + await pageObject.setFooterHidden(true); + await pageObject.setFooterHidden(false); + + expect(pageObject.isFooterHidden()).toBeFalse(); + }); + + it('should fire "input" event when there is an input to the editor', async () => { + const inputEventListener = createEventListener(element, 'input'); + + await pageObject.setEditorTextContent('input'); + await inputEventListener.promise; + + expect(inputEventListener.spy).toHaveBeenCalledTimes(1); + }); + + it('should not fire "input" event when setting the content through "setMarkdown"', () => { + const inputEventListener = createEventListener(element, 'input'); + + element.setMarkdown('input'); + + expect(inputEventListener.spy).not.toHaveBeenCalled(); + }); + + it('should fire "input" event when the text is updated/removed from the editor', async () => { + const inputEventListener = createEventListener(element, 'input'); + + await pageObject.setEditorTextContent('update'); + await inputEventListener.promise; + + expect(inputEventListener.spy).toHaveBeenCalledTimes(1); + + await pageObject.setEditorTextContent(''); + await inputEventListener.promise; + + expect(inputEventListener.spy).toHaveBeenCalledTimes(1); + }); + + it('should initialize "empty" to true and set false when there is content', async () => { + expect(element.empty).toBeTrue(); + + await pageObject.setEditorTextContent('not empty'); + expect(element.empty).toBeFalse(); + + await pageObject.setEditorTextContent(''); + expect(element.empty).toBeTrue(); + }); + + it('should update "empty" when the content is loaded with "setMarkdown"', () => { + expect(element.empty).toBeTrue(); + + element.setMarkdown('not empty'); + expect(element.empty).toBeFalse(); + + element.setMarkdown(''); + expect(element.empty).toBeTrue(); + }); + + it('should return true for "empty" if there is only whitespace', async () => { + expect(element.empty).toBeTrue(); + + await pageObject.setEditorTextContent(' '); + expect(element.empty).toBeTrue(); + + element.setMarkdown(' '); + expect(element.empty).toBeTrue(); + }); + + it('should return true for "empty" even if the placeholder content is set', () => { + expect(element.empty).toBeTrue(); + + element.placeholder = 'Placeholder text'; + expect(element.empty).toBeTrue(); + }); + + it('should initialize the "placeholder" attribute with undefined', () => { + expect(element.placeholder).toBeUndefined(); + }); + + it('should reflect the "placeholder" value to its internal attribute', () => { + expect(pageObject.getPlaceholderValue()).toBe(''); + + element.placeholder = 'Placeholder text'; + + expect(pageObject.getPlaceholderValue()).toBe('Placeholder text'); + }); + + it('should set "placeholder" value to empty when attribute is cleared with an empty string', () => { + element.placeholder = 'Placeholder text'; + + expect(pageObject.getPlaceholderValue()).toBe('Placeholder text'); + + element.placeholder = ''; + + expect(pageObject.getPlaceholderValue()).toBe(''); + }); }); describe('RichTextEditor Before DOM Connection', () => { diff --git a/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.stories.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts similarity index 71% rename from packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.stories.ts rename to packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts index ff2adc95cf..b068e3a376 100644 --- a/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.stories.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts @@ -1,11 +1,12 @@ import { html, ref, when } from '@microsoft/fast-element'; import type { Meta, StoryObj } from '@storybook/html'; +import { withActions } from '@storybook/addon-actions/decorator'; import { createUserSelectedThemeStory, incubatingWarning -} from '../../utilities/tests/storybook'; +} from '../../../utilities/tests/storybook'; import { RichTextEditor, richTextEditorTag } from '..'; -import { buttonTag } from '../../button'; +import { buttonTag } from '../../../button'; // eslint-disable-next-line @typescript-eslint/no-empty-interface interface RichTextEditorArgs { @@ -14,6 +15,13 @@ interface RichTextEditorArgs { getMarkdown: undefined; editorRef: RichTextEditor; setMarkdownData: (args: RichTextEditorArgs) => void; + disabled: boolean; + footerHidden: boolean; + errorVisible: boolean; + errorText: string; + input: unknown; + empty: unknown; + placeholder: string; } type ExampleDataType = (typeof exampleDataType)[keyof typeof exampleDataType]; @@ -54,11 +62,15 @@ client application must implement that functionality. const metadata: Meta = { title: 'Incubating/Rich Text Editor', tags: ['autodocs'], + decorators: [withActions], parameters: { docs: { description: { component: richTextEditorDescription } + }, + actions: { + handles: ['input'] } }, // prettier-ignore @@ -70,6 +82,11 @@ const metadata: Meta = { <${richTextEditorTag} ${ref('editorRef')} data-unused="${x => x.setMarkdownData(x)}" + ?disabled="${x => x.disabled}" + ?footer-hidden="${x => x.footerHidden}" + ?error-visible="${x => x.errorVisible}" + error-text="${x => x.errorText}" + placeholder="${x => x.placeholder}" > ${when(x => x.footerActionButtons, html` <${buttonTag} appearance="ghost" slot="footer-actions">Cancel @@ -103,11 +120,43 @@ const metadata: Meta = { }, setMarkdownData: { table: { disable: true } + }, + errorVisible: { + description: + 'Whether the editor should be styled to indicate that it is in an invalid state.' + }, + errorText: { + description: + 'A message to be displayed when the editor is in the invalid state explaining why the value is invalid.' + }, + placeholder: { + description: 'Placeholder text to show when editor is empty.' + }, + footerHidden: { + description: + 'Setting `footer-hidden` hides the footer section which consists of all formatting option buttons and the `footer-actions` slot content.' + }, + empty: { + name: 'empty', + description: + 'Read-only boolean value. Returns true if editor is either empty or contains only whitespace.', + control: false + }, + input: { + name: 'input', + description: + 'This event is fired when there is a change in the content of the editor.', + control: false } }, args: { data: exampleDataType.plainString, footerActionButtons: false, + disabled: false, + footerHidden: false, + errorVisible: false, + errorText: 'Value is invalid', + placeholder: 'Placeholder', editorRef: undefined, setMarkdownData: x => { void (async () => { diff --git a/packages/nimble-components/src/rich-text-editor/tests/types.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/types.spec.ts similarity index 100% rename from packages/nimble-components/src/rich-text-editor/tests/types.spec.ts rename to packages/nimble-components/src/rich-text/editor/tests/types.spec.ts diff --git a/packages/nimble-components/src/rich-text-viewer/index.ts b/packages/nimble-components/src/rich-text/models/markdown-parser.ts similarity index 55% rename from packages/nimble-components/src/rich-text-viewer/index.ts rename to packages/nimble-components/src/rich-text/models/markdown-parser.ts index 132daa978d..ab06283a96 100644 --- a/packages/nimble-components/src/rich-text-viewer/index.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-parser.ts @@ -1,60 +1,36 @@ -import { DesignSystem, FoundationElement } from '@microsoft/fast-foundation'; import { schema, defaultMarkdownParser, MarkdownParser } from 'prosemirror-markdown'; import { DOMSerializer } from 'prosemirror-model'; -import { observable } from '@microsoft/fast-element'; -import { template } from './template'; -import { styles } from './styles'; - -declare global { - interface HTMLElementTagNameMap { - 'nimble-rich-text-viewer': RichTextViewer; - } -} /** - * A nimble styled rich text viewer + * Provides markdown parser for rich text components */ -export class RichTextViewer extends FoundationElement { - /** - * - * @public - * Markdown string to render its corresponding rich text content in the component. - */ - @observable - public markdown = ''; - - /** - * @internal - */ - public viewer!: HTMLDivElement; +export class RichTextMarkdownParser { private readonly markdownParser: MarkdownParser; private readonly domSerializer: DOMSerializer; public constructor() { - super(); - this.domSerializer = DOMSerializer.fromSchema(schema); this.markdownParser = this.initializeMarkdownParser(); + this.domSerializer = DOMSerializer.fromSchema(schema); } /** - * @internal - */ - public override connectedCallback(): void { - super.connectedCallback(); - this.updateView(); - } - - /** - * @internal + * + * This function takes a markdown string, parses it using the ProseMirror MarkdownParser, serializes the parsed content into a + * DOM structure using a DOMSerializer, and returns the serialized result. + * If the markdown parser returns null, it will clear the viewer component by creating an empty document fragment. */ - public markdownChanged(): void { - if (this.$fastController.isConnected) { - this.updateView(); + public parseMarkdownToDOM(value: string): HTMLElement | DocumentFragment { + const parsedMarkdownContent = this.markdownParser.parse(value); + if (parsedMarkdownContent === null) { + return document.createDocumentFragment(); } + return this.domSerializer.serializeFragment( + parsedMarkdownContent.content + ); } private initializeMarkdownParser(): MarkdownParser { @@ -80,41 +56,4 @@ export class RichTextViewer extends FoundationElement { defaultMarkdownParser.tokens ); } - - /** - * - * This function takes a markdown string, parses it using the ProseMirror MarkdownParser, serializes the parsed content into a - * DOM structure using a DOMSerializer, and returns the serialized result. - * If the markdown parser returns null, it will clear the viewer component by creating an empty document fragment. - */ - 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 updateView(): void { - if (this.markdown) { - const serializedContent = this.parseMarkdownToDOM(this.markdown); - this.viewer.replaceChildren(serializedContent); - } else { - this.viewer.innerHTML = ''; - } - } } - -const nimbleRichTextViewer = RichTextViewer.compose({ - baseName: 'rich-text-viewer', - template, - styles -}); - -DesignSystem.getOrCreate() - .withPrefix('nimble') - .register(nimbleRichTextViewer()); -export const richTextViewerTag = DesignSystem.tagFor(RichTextViewer); diff --git a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts new file mode 100644 index 0000000000..231bd0dad4 --- /dev/null +++ b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts @@ -0,0 +1,47 @@ +import { + MarkdownSerializer, + defaultMarkdownSerializer, + MarkdownSerializerState +} from 'prosemirror-markdown'; +import type { Node } from 'prosemirror-model'; + +export function richTextMarkdownSerializer(): 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); +} diff --git a/packages/nimble-components/src/rich-text/viewer/index.ts b/packages/nimble-components/src/rich-text/viewer/index.ts new file mode 100644 index 0000000000..dcc5146380 --- /dev/null +++ b/packages/nimble-components/src/rich-text/viewer/index.ts @@ -0,0 +1,75 @@ +import { DesignSystem, FoundationElement } from '@microsoft/fast-foundation'; +import { observable } from '@microsoft/fast-element'; +import { template } from './template'; +import { styles } from './styles'; +import { RichTextMarkdownParser } from '../models/markdown-parser'; + +declare global { + interface HTMLElementTagNameMap { + 'nimble-rich-text-viewer': RichTextViewer; + } +} + +/** + * A nimble styled rich text viewer + */ +export class RichTextViewer extends FoundationElement { + /** + * + * @public + * Markdown string to render its corresponding rich text content in the component. + */ + @observable + public markdown = ''; + + /** + * @internal + */ + public viewer!: HTMLDivElement; + + private readonly markdownParser: RichTextMarkdownParser; + + public constructor() { + super(); + this.markdownParser = new RichTextMarkdownParser(); + } + + /** + * @internal + */ + public override connectedCallback(): void { + super.connectedCallback(); + this.updateView(); + } + + /** + * @internal + */ + public markdownChanged(): void { + if (this.$fastController.isConnected) { + this.updateView(); + } + } + + private updateView(): void { + if (this.markdown) { + const serializedContent = this.markdownParser.parseMarkdownToDOM( + this.markdown + ); + this.viewer.replaceChildren(serializedContent); + } else { + this.viewer.innerHTML = ''; + } + } +} + +const nimbleRichTextViewer = RichTextViewer.compose({ + baseName: 'rich-text-viewer', + template, + styles +}); + +DesignSystem.getOrCreate() + .withPrefix('nimble') + .register(nimbleRichTextViewer()); +export const richTextViewerTag = DesignSystem.tagFor(RichTextViewer); diff --git a/packages/nimble-components/src/rich-text-viewer/specs/README.md b/packages/nimble-components/src/rich-text/viewer/specs/README.md similarity index 100% rename from packages/nimble-components/src/rich-text-viewer/specs/README.md rename to packages/nimble-components/src/rich-text/viewer/specs/README.md diff --git a/packages/nimble-components/src/rich-text-viewer/styles.ts b/packages/nimble-components/src/rich-text/viewer/styles.ts similarity index 96% rename from packages/nimble-components/src/rich-text-viewer/styles.ts rename to packages/nimble-components/src/rich-text/viewer/styles.ts index f40f0522c6..0d9d8fcbfa 100644 --- a/packages/nimble-components/src/rich-text-viewer/styles.ts +++ b/packages/nimble-components/src/rich-text/viewer/styles.ts @@ -5,7 +5,7 @@ import { bodyFontColor, linkActiveFontColor, linkFontColor -} from '../theme-provider/design-tokens'; +} from '../../theme-provider/design-tokens'; export const styles = css` ${display('flex')} diff --git a/packages/nimble-components/src/rich-text-viewer/template.ts b/packages/nimble-components/src/rich-text/viewer/template.ts similarity index 100% rename from packages/nimble-components/src/rich-text-viewer/template.ts rename to packages/nimble-components/src/rich-text/viewer/template.ts diff --git a/packages/nimble-components/src/rich-text-viewer/testing/rich-text-viewer.pageobject.ts b/packages/nimble-components/src/rich-text/viewer/testing/rich-text-viewer.pageobject.ts similarity index 100% rename from packages/nimble-components/src/rich-text-viewer/testing/rich-text-viewer.pageobject.ts rename to packages/nimble-components/src/rich-text/viewer/testing/rich-text-viewer.pageobject.ts diff --git a/packages/nimble-components/src/rich-text-viewer/tests/rich-text-viewer-matrix.stories.ts b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer-matrix.stories.ts similarity index 91% rename from packages/nimble-components/src/rich-text-viewer/tests/rich-text-viewer-matrix.stories.ts rename to packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer-matrix.stories.ts index 7c91e47565..8e4f36ff3d 100644 --- a/packages/nimble-components/src/rich-text-viewer/tests/rich-text-viewer-matrix.stories.ts +++ b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer-matrix.stories.ts @@ -3,19 +3,19 @@ import { html, ViewTemplate } from '@microsoft/fast-element'; import { createMatrixThemeStory, createStory -} from '../../utilities/tests/storybook'; +} from '../../../utilities/tests/storybook'; import { createMatrix, sharedMatrixParameters -} from '../../utilities/tests/matrix'; -import { hiddenWrapper } from '../../utilities/tests/hidden'; +} from '../../../utilities/tests/matrix'; +import { hiddenWrapper } from '../../../utilities/tests/hidden'; import { richTextViewerTag } from '..'; -import { richTextMarkdownString } from '../../utilities/tests/rich-text-markdown-string'; -import { loremIpsum } from '../../utilities/tests/lorem-ipsum'; +import { richTextMarkdownString } from '../../../utilities/tests/rich-text-markdown-string'; +import { loremIpsum } from '../../../utilities/tests/lorem-ipsum'; import { cssPropertyFromTokenName, tokenNames -} from '../../theme-provider/design-token-names'; +} from '../../../theme-provider/design-token-names'; const metadata: Meta = { title: 'Tests/Rich Text Viewer', diff --git a/packages/nimble-components/src/rich-text-viewer/tests/rich-text-viewer.spec.ts b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts similarity index 99% rename from packages/nimble-components/src/rich-text-viewer/tests/rich-text-viewer.spec.ts rename to packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts index 1235e90ba1..726feb4a93 100644 --- a/packages/nimble-components/src/rich-text-viewer/tests/rich-text-viewer.spec.ts +++ b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts @@ -1,9 +1,9 @@ import { html } from '@microsoft/fast-element'; -import { RichTextViewer, richTextViewerTag } from '..'; -import { fixture, type Fixture } from '../../utilities/tests/fixture'; +import { fixture, type Fixture } from '../../../utilities/tests/fixture'; import { RichTextViewerPageObject } from '../testing/rich-text-viewer.pageobject'; -import { wackyStrings } from '../../utilities/tests/wacky-strings'; -import { getSpecTypeByNamedList } from '../../utilities/tests/parameterized'; +import { wackyStrings } from '../../../utilities/tests/wacky-strings'; +import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; +import { RichTextViewer, richTextViewerTag } from '..'; async function setup(): Promise> { return fixture( diff --git a/packages/nimble-components/src/rich-text-viewer/tests/rich-text-viewer.stories.ts b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.stories.ts similarity index 92% rename from packages/nimble-components/src/rich-text-viewer/tests/rich-text-viewer.stories.ts rename to packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.stories.ts index cf1e2f14ac..25dfe832e5 100644 --- a/packages/nimble-components/src/rich-text-viewer/tests/rich-text-viewer.stories.ts +++ b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.stories.ts @@ -3,9 +3,9 @@ import type { Meta, StoryObj } from '@storybook/html'; import { createUserSelectedThemeStory, incubatingWarning -} from '../../utilities/tests/storybook'; +} from '../../../utilities/tests/storybook'; import { richTextViewerTag } from '..'; -import { richTextMarkdownString } from '../../utilities/tests/rich-text-markdown-string'; +import { richTextMarkdownString } from '../../../utilities/tests/rich-text-markdown-string'; interface RichTextViewerArgs { markdown: string; From fa86b5a78f927b771efa5c3bd357f04b713edf77 Mon Sep 17 00:00:00 2001 From: "SOLITONTECH\\vivin.krishna" <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:17:31 +0530 Subject: [PATCH 02/56] Fix component import in angular --- .../rich-text-viewer/nimble-rich-text-viewer.directive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/nimble-rich-text-viewer.directive.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/nimble-rich-text-viewer.directive.ts index 1440b0d0c0..7609f35d8d 100644 --- a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/nimble-rich-text-viewer.directive.ts +++ b/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/nimble-rich-text-viewer.directive.ts @@ -1,5 +1,5 @@ import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; -import type { RichTextViewer } from '@ni/nimble-components/dist/esm/rich-text-viewer'; +import type { RichTextViewer } from '@ni/nimble-components/dist/esm/rich-text/viewer'; export type { RichTextViewer }; From c7f895dd2790e5dfdb5b7eb06969cb648901acba Mon Sep 17 00:00:00 2001 From: "SOLITONTECH\\vivin.krishna" <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:33:44 +0530 Subject: [PATCH 03/56] Fixing build issue in angular viewer module --- .../rich-text-viewer/nimble-rich-text-viewer.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/nimble-rich-text-viewer.module.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/nimble-rich-text-viewer.module.ts index c94455a3a8..d3bc69cf06 100644 --- a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/nimble-rich-text-viewer.module.ts +++ b/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/nimble-rich-text-viewer.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { NimbleRichTextViewerDirective } from './nimble-rich-text-viewer.directive'; -import '@ni/nimble-components/dist/esm/rich-text-viewer'; +import '@ni/nimble-components/dist/esm/rich-text/viewer'; @NgModule({ declarations: [NimbleRichTextViewerDirective], From 01afe477046952ea45b7e97225051687184c014c Mon Sep 17 00:00:00 2001 From: "SOLITONTECH\\vivin.krishna" <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 31 Aug 2023 15:08:48 +0530 Subject: [PATCH 04/56] Updated the markdown serializer class --- .../src/rich-text/editor/index.ts | 9 +- .../src/rich-text/models/markdown-parser.ts | 1 - .../rich-text/models/markdown-serializer.ts | 91 +++++++++++-------- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index 173e7a9cf7..68b79e63cf 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -23,7 +23,7 @@ import { styles } from './styles'; import type { ToggleButton } from '../../toggle-button'; import type { ErrorPattern } from '../../patterns/error/types'; import { RichTextMarkdownParser } from '../models/markdown-parser'; -import { richTextMarkdownSerializer } from '../models/markdown-serializer'; +import { RichTextMarkdownSerializer } from '../models/markdown-serializer'; declare global { interface HTMLElementTagNameMap { @@ -140,7 +140,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { private updateScrollbarWidthQueued = false; private readonly markdownParser = new RichTextMarkdownParser(); - private readonly markdownSerializer = richTextMarkdownSerializer(); + private readonly markdownSerializer = new RichTextMarkdownSerializer(); private readonly xmlSerializer = new XMLSerializer(); /** @@ -300,10 +300,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { * @public */ public getMarkdown(): string { - const markdownContent = this.markdownSerializer.serialize( - this.tiptapEditor.state.doc - ); - return markdownContent; + return this.markdownSerializer.serializeToMarkdown(this.tiptapEditor.state.doc); } /** diff --git a/packages/nimble-components/src/rich-text/models/markdown-parser.ts b/packages/nimble-components/src/rich-text/models/markdown-parser.ts index ab06283a96..9a1ae9a2c4 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-parser.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-parser.ts @@ -18,7 +18,6 @@ export class RichTextMarkdownParser { } /** - * * This function takes a markdown string, parses it using the ProseMirror MarkdownParser, serializes the parsed content into a * DOM structure using a DOMSerializer, and returns the serialized result. * If the markdown parser returns null, it will clear the viewer component by creating an empty document fragment. diff --git a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts index 231bd0dad4..1f859ac17a 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts @@ -5,43 +5,58 @@ import { } from 'prosemirror-markdown'; import type { Node } from 'prosemirror-model'; -export function richTextMarkdownSerializer(): 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}. `; - }); - }; +/** + * Provides markdown serializer for rich text components + */ +export class RichTextMarkdownSerializer { + private readonly markdownSerializer: MarkdownSerializer; - /** - * 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); + public constructor() { + this.markdownSerializer = this.initializeMarkdownSerializer(); + } + + public serializeToMarkdown(doc: Node): string { + return this.markdownSerializer.serialize(doc); + } + + 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); + } } From bafbe48ffe3fd126a5ed7143b9b8cfe92fde3d7f Mon Sep 17 00:00:00 2001 From: "SOLITONTECH\\vivin.krishna" <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 31 Aug 2023 15:56:01 +0530 Subject: [PATCH 05/56] Renaming the initialization markdown serializer method --- .../src/rich-text/models/markdown-serializer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts index 1f859ac17a..0b2436a47b 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts @@ -12,14 +12,14 @@ export class RichTextMarkdownSerializer { private readonly markdownSerializer: MarkdownSerializer; public constructor() { - this.markdownSerializer = this.initializeMarkdownSerializer(); + this.markdownSerializer = this.initializeMarkdownSerializerForTipTap(); } public serializeToMarkdown(doc: Node): string { return this.markdownSerializer.serialize(doc); } - private initializeMarkdownSerializer(): MarkdownSerializer { + private initializeMarkdownSerializerForTipTap(): 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) From 91bb520c481337193b592e77b493952f6318cb41 Mon Sep 17 00:00:00 2001 From: "SOLITONTECH\\vivin.krishna" <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 31 Aug 2023 16:29:50 +0530 Subject: [PATCH 06/56] Fix lint errors --- packages/nimble-components/src/rich-text/editor/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index 68b79e63cf..59815ce72c 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -300,7 +300,9 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { * @public */ public getMarkdown(): string { - return this.markdownSerializer.serializeToMarkdown(this.tiptapEditor.state.doc); + return this.markdownSerializer.serializeToMarkdown( + this.tiptapEditor.state.doc + ); } /** From 3a701565de86f2e4b339939b41b39714f43c51ee Mon Sep 17 00:00:00 2001 From: "SOLITONTECH\\vivin.krishna" <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 31 Aug 2023 16:32:16 +0530 Subject: [PATCH 07/56] Change files --- ...imble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json | 7 +++++++ ...le-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json create mode 100644 change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json diff --git a/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json b/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json new file mode 100644 index 0000000000..4cc1292b1d --- /dev/null +++ b/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Revamp rich text components folder structure", + "packageName": "@ni/nimble-angular", + "email": "123377523+vivinkrishna-ni@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json b/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json new file mode 100644 index 0000000000..e64898b55e --- /dev/null +++ b/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Update folder paths of rich text components in the nimble-angular", + "packageName": "@ni/nimble-components", + "email": "123377523+vivinkrishna-ni@users.noreply.github.com", + "dependentChangeType": "patch" +} From d37e001ebcbd0ca77487df463a0453ea99d0aa88 Mon Sep 17 00:00:00 2001 From: "SOLITONTECH\\vivin.krishna" <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 31 Aug 2023 16:33:45 +0530 Subject: [PATCH 08/56] Update change file description --- ...@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json | 2 +- ...-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json b/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json index 4cc1292b1d..8edd5163fd 100644 --- a/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json +++ b/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json @@ -1,6 +1,6 @@ { "type": "patch", - "comment": "Revamp rich text components folder structure", + "comment": "Update folder paths for importing rich text components", "packageName": "@ni/nimble-angular", "email": "123377523+vivinkrishna-ni@users.noreply.github.com", "dependentChangeType": "patch" diff --git a/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json b/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json index e64898b55e..9c790f46ee 100644 --- a/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json +++ b/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json @@ -1,6 +1,6 @@ { "type": "patch", - "comment": "Update folder paths of rich text components in the nimble-angular", + "comment": "Revamp rich text components folder structure", "packageName": "@ni/nimble-components", "email": "123377523+vivinkrishna-ni@users.noreply.github.com", "dependentChangeType": "patch" From 2c2d5ac7edaddc535a68e6a9775d748de60eb347 Mon Sep 17 00:00:00 2001 From: "SOLITONTECH\\vivin.krishna" <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Fri, 1 Sep 2023 10:13:10 +0530 Subject: [PATCH 09/56] Resolve merge conflicts --- .../src/rich-text/editor/index.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index 59815ce72c..41b750cdba 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -212,6 +212,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { */ public boldButtonClick(): void { this.tiptapEditor.chain().focus().toggleBold().run(); + this.forceFocusEditor(); } /** @@ -221,6 +222,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { public boldButtonKeyDown(event: KeyboardEvent): boolean { if (this.keyActivatesButton(event)) { this.tiptapEditor.chain().focus().toggleBold().run(); + this.forceFocusEditor(); return false; } return true; @@ -232,6 +234,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { */ public italicsButtonClick(): void { this.tiptapEditor.chain().focus().toggleItalic().run(); + this.forceFocusEditor(); } /** @@ -241,6 +244,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { public italicsButtonKeyDown(event: KeyboardEvent): boolean { if (this.keyActivatesButton(event)) { this.tiptapEditor.chain().focus().toggleItalic().run(); + this.forceFocusEditor(); return false; } return true; @@ -252,6 +256,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { */ public bulletListButtonClick(): void { this.tiptapEditor.chain().focus().toggleBulletList().run(); + this.forceFocusEditor(); } /** @@ -261,6 +266,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { public bulletListButtonKeyDown(event: KeyboardEvent): boolean { if (this.keyActivatesButton(event)) { this.tiptapEditor.chain().focus().toggleBulletList().run(); + this.forceFocusEditor(); return false; } return true; @@ -272,6 +278,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { */ public numberedListButtonClick(): void { this.tiptapEditor.chain().focus().toggleOrderedList().run(); + this.forceFocusEditor(); } /** @@ -281,6 +288,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { public numberedListButtonKeyDown(event: KeyboardEvent): boolean { if (this.keyActivatesButton(event)) { this.tiptapEditor.chain().focus().toggleOrderedList().run(); + this.forceFocusEditor(); return false; } return true; @@ -462,6 +470,15 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { } }); } + + // In Firefox browser, once the editor gets focused, the blinking caret will be visible until we click format buttons (Bold, Italic ...) in the Firefox browser (changing focus). + // But once any of the toolbar button is clicked, editor internally has its focus but the blinking caret disappears. + // As a workaround, manually triggering blur and setting focus on editor makes the blinking caret to re-appear. + // Mozilla issue https://bugzilla.mozilla.org/show_bug.cgi?id=1496769 tracks removal of this workaround. + private forceFocusEditor(): void { + this.tiptapEditor.commands.blur(); + this.tiptapEditor.commands.focus(); + } } // eslint-disable-next-line @typescript-eslint/no-empty-interface From b7fe9d8dc2a90053fb0fd4b283c5174d20f53dfb Mon Sep 17 00:00:00 2001 From: "SOLITONTECH\\vivin.krishna" <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Fri, 1 Sep 2023 10:18:03 +0530 Subject: [PATCH 10/56] Update editor component paths for rich text editor --- .../rich-text-editor/nimble-rich-text-editor.directive.ts | 2 +- .../rich-text-editor/nimble-rich-text-editor.module.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.directive.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.directive.ts index abd5a2a0a8..865b5f0439 100644 --- a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.directive.ts +++ b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.directive.ts @@ -1,5 +1,5 @@ import { Directive, ElementRef, EventEmitter, HostListener, Input, Output, Renderer2 } from '@angular/core'; -import type { RichTextEditor } from '@ni/nimble-components/dist/esm/rich-text-editor'; +import type { RichTextEditor } from '@ni/nimble-components/dist/esm/rich-text/editor'; import { BooleanValueOrAttribute, toBooleanProperty } from '@ni/nimble-angular/internal-utilities'; export type { RichTextEditor }; diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.module.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.module.ts index 0c6c57ce98..4164c9f09b 100644 --- a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.module.ts +++ b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { NimbleRichTextEditorDirective } from './nimble-rich-text-editor.directive'; -import '@ni/nimble-components/dist/esm/rich-text-editor'; +import '@ni/nimble-components/dist/esm/rich-text/editor'; @NgModule({ declarations: [NimbleRichTextEditorDirective], From b819d0e73c2af9b023fe0dcde90563b6a3c97cc4 Mon Sep 17 00:00:00 2001 From: "SOLITONTECH\\vivin.krishna" <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Fri, 1 Sep 2023 10:36:14 +0530 Subject: [PATCH 11/56] Updating pageobject paths for angular files --- .../rich-text-editor/testing/rich-text-editor.pageobject.ts | 4 ++-- .../rich-text-viewer/testing/rich-text-viewer.pageobject.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/rich-text-editor.pageobject.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/rich-text-editor.pageobject.ts index 075882fc1b..3047e7e51a 100644 --- a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/rich-text-editor.pageobject.ts +++ b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/rich-text-editor.pageobject.ts @@ -1,5 +1,5 @@ -import { RichTextEditorPageObject } from '@ni/nimble-components/dist/esm/rich-text-editor/testing/rich-text-editor.pageobject'; -import type { ToolbarButton } from '@ni/nimble-components/dist/esm/rich-text-editor/testing/types'; +import { RichTextEditorPageObject } from '@ni/nimble-components/dist/esm/rich-text/editor/testing/rich-text-editor.pageobject'; +import type { ToolbarButton } from '@ni/nimble-components/dist/esm/rich-text/editor/testing/types'; export { RichTextEditorPageObject }; export type { ToolbarButton }; \ No newline at end of file diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/rich-text-viewer.pageobject.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/rich-text-viewer.pageobject.ts index 0deb789be0..84b9fedbbb 100644 --- a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/rich-text-viewer.pageobject.ts +++ b/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/rich-text-viewer.pageobject.ts @@ -1,3 +1,3 @@ -import { RichTextViewerPageObject } from '@ni/nimble-components/dist/esm/rich-text-viewer/testing/rich-text-viewer.pageobject'; +import { RichTextViewerPageObject } from '@ni/nimble-components/dist/esm/rich-text/viewer/testing/rich-text-viewer.pageobject'; export { RichTextViewerPageObject }; \ No newline at end of file From 4acdff35737cea1a3f919e0dff0f98673ced1c43 Mon Sep 17 00:00:00 2001 From: "SOLITONTECH\\vivin.krishna" <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Fri, 1 Sep 2023 11:34:03 +0530 Subject: [PATCH 12/56] Resolve merge conflicts --- .../src/rich-text/editor/index.ts | 20 +++++++++++++++---- .../editor/tests/rich-text-editor.spec.ts | 4 ++-- .../src/rich-text/editor/tests/types.spec.ts | 9 +++++++++ .../src/rich-text/editor/types.ts | 11 ++++++++++ 4 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 packages/nimble-components/src/rich-text/editor/types.ts diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index 41b750cdba..56a3d9b349 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -6,7 +6,13 @@ import { FoundationElement } from '@microsoft/fast-foundation'; import { keyEnter, keySpace } from '@microsoft/fast-web-utilities'; -import { Editor, AnyExtension, Extension } from '@tiptap/core'; +import { + Editor, + findParentNode, + isList, + AnyExtension, + Extension +} from '@tiptap/core'; import Bold from '@tiptap/extension-bold'; import BulletList from '@tiptap/extension-bullet-list'; import Document from '@tiptap/extension-document'; @@ -22,6 +28,7 @@ import { template } from './template'; import { styles } from './styles'; import type { ToggleButton } from '../../toggle-button'; import type { ErrorPattern } from '../../patterns/error/types'; +import { TipTapNodeName } from './types'; import { RichTextMarkdownParser } from '../models/markdown-parser'; import { RichTextMarkdownSerializer } from '../models/markdown-serializer'; @@ -382,10 +389,15 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { } private updateEditorButtonsState(): void { + const { extensionManager, state } = this.tiptapEditor; + const { extensions } = extensionManager; + const { selection } = state; + const parentList = findParentNode((node: { type: { name: string } }) => isList(node.type.name, extensions))(selection); + this.boldButton.checked = this.tiptapEditor.isActive('bold'); this.italicsButton.checked = this.tiptapEditor.isActive('italic'); - this.bulletListButton.checked = this.tiptapEditor.isActive('bulletList'); - this.numberedListButton.checked = this.tiptapEditor.isActive('orderedList'); + this.bulletListButton.checked = parentList?.node.type.name === TipTapNodeName.bulletList; + this.numberedListButton.checked = parentList?.node.type.name === TipTapNodeName.numberedList; } private keyActivatesButton(event: KeyboardEvent): boolean { @@ -457,7 +469,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { extensionName: string ): AnyExtension | undefined { return this.tiptapEditor.extensionManager.extensions.find( - extension => extension.name === extensionName + (extension: { name: string }) => extension.name === extensionName ); } diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index 5196f43885..4c486996a0 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -452,7 +452,7 @@ describe('RichTextEditor', () => { ]); expect( pageObject.getButtonCheckedState(ToolbarButton.numberedList) - ).toBeTrue(); + ).toBeFalse(); expect( pageObject.getButtonCheckedState(ToolbarButton.bulletList) ).toBeTrue(); @@ -534,7 +534,7 @@ describe('RichTextEditor', () => { ).toBeTrue(); expect( pageObject.getButtonCheckedState(ToolbarButton.bulletList) - ).toBeTrue(); + ).toBeFalse(); }); it('should have "ul" tag names for bullet lists when clicking "tab" to make it nested and "shift+Tab" to make it usual list', async () => { diff --git a/packages/nimble-components/src/rich-text/editor/tests/types.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/types.spec.ts index 4f52aecc3b..5f142d7813 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/types.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/types.spec.ts @@ -1,4 +1,5 @@ import type { ToolbarButton } from '../testing/types'; +import type { TipTapNodeName } from '../types'; describe('Editor Toolbar button page object types', () => { it('ToolbarButton fails compile if assigning arbitrary string values', () => { @@ -8,3 +9,11 @@ describe('Editor Toolbar button page object types', () => { expect(value).toEqual('hello'); }); }); + +describe('Tiptap node types', () => { + it('TipTapNodeName fails compile if assigning arbitrary string values', () => { + // @ts-expect-error This expect will fail if the enum-like type is missing "as const" + const value: TipTapNodeName = 'hello'; + expect(value).toEqual('hello'); + }); +}); diff --git a/packages/nimble-components/src/rich-text/editor/types.ts b/packages/nimble-components/src/rich-text/editor/types.ts new file mode 100644 index 0000000000..e402f2de45 --- /dev/null +++ b/packages/nimble-components/src/rich-text/editor/types.ts @@ -0,0 +1,11 @@ +/** + * TipTap node types. + * @public + */ +export const TipTapNodeName = { + bulletList: 'bulletList', + numberedList: 'orderedList' +} as const; + +export type TipTapNodeName = + (typeof TipTapNodeName)[keyof typeof TipTapNodeName]; From fdc3cf7edc6f7aec1b4397f497efe9a7353f815f Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Fri, 1 Sep 2023 18:09:20 +0530 Subject: [PATCH 13/56] Updated serialize method name --- packages/nimble-components/src/rich-text/editor/index.ts | 2 +- .../src/rich-text/models/markdown-serializer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index b096b8e7d4..8b8006e957 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -315,7 +315,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { * @public */ public getMarkdown(): string { - return this.markdownSerializer.serializeToMarkdown( + return this.markdownSerializer.serializeDOMToMarkdown( this.tiptapEditor.state.doc ); } diff --git a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts index 0b2436a47b..e6fbae74a8 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts @@ -15,7 +15,7 @@ export class RichTextMarkdownSerializer { this.markdownSerializer = this.initializeMarkdownSerializerForTipTap(); } - public serializeToMarkdown(doc: Node): string { + public serializeDOMToMarkdown(doc: Node): string { return this.markdownSerializer.serialize(doc); } From 65acbd701836b28aa090d493bc35d4c4f040e334 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Fri, 1 Sep 2023 19:28:16 +0530 Subject: [PATCH 14/56] Moved the parser initialization in viewer component just like the editor and removed constructor --- packages/nimble-components/src/rich-text/viewer/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/nimble-components/src/rich-text/viewer/index.ts b/packages/nimble-components/src/rich-text/viewer/index.ts index dcc5146380..6d0b86dc2d 100644 --- a/packages/nimble-components/src/rich-text/viewer/index.ts +++ b/packages/nimble-components/src/rich-text/viewer/index.ts @@ -27,12 +27,7 @@ export class RichTextViewer extends FoundationElement { */ public viewer!: HTMLDivElement; - private readonly markdownParser: RichTextMarkdownParser; - - public constructor() { - super(); - this.markdownParser = new RichTextMarkdownParser(); - } + private readonly markdownParser = new RichTextMarkdownParser(); /** * @internal From b38df31c8b9aba847b5e2aadfc9af80a40b43081 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Mon, 4 Sep 2023 09:36:10 +0530 Subject: [PATCH 15/56] Resolve PR comments --- .../projects/example-client-app/src/app/app.module.ts | 4 ++-- .../src/app/customapp/customapp.component.ts | 2 +- .../{rich-text-editor => rich-text/editor}/ng-package.json | 0 .../editor}/nimble-rich-text-editor.directive.ts | 0 .../editor}/nimble-rich-text-editor.module.ts | 0 .../{rich-text-editor => rich-text/editor}/public-api.ts | 0 .../editor}/testing/ng-package.json | 0 .../editor}/testing/public-api.ts | 0 .../editor}/testing/rich-text-editor.pageobject.ts | 0 .../editor}/tests/nimble-rich-text-editor.directive.spec.ts | 0 .../{rich-text-viewer => rich-text/viewer}/ng-package.json | 0 .../viewer}/nimble-rich-text-viewer.directive.ts | 0 .../viewer}/nimble-rich-text-viewer.module.ts | 0 .../{rich-text-viewer => rich-text/viewer}/public-api.ts | 0 .../viewer}/testing/ng-package.json | 0 .../viewer}/testing/public-api.ts | 0 .../viewer}/testing/rich-text-viewer.pageobject.ts | 0 .../viewer}/tests/nimble-rich-text-viewer.directive.spec.ts | 0 ...nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json | 2 +- ...ble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json | 2 +- .../src/rich-text/models/markdown-serializer.ts | 6 +----- .../nimble-components/src/rich-text/viewer/specs/README.md | 2 +- 22 files changed, 7 insertions(+), 11 deletions(-) rename angular-workspace/projects/ni/nimble-angular/{rich-text-editor => rich-text/editor}/ng-package.json (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-editor => rich-text/editor}/nimble-rich-text-editor.directive.ts (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-editor => rich-text/editor}/nimble-rich-text-editor.module.ts (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-editor => rich-text/editor}/public-api.ts (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-editor => rich-text/editor}/testing/ng-package.json (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-editor => rich-text/editor}/testing/public-api.ts (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-editor => rich-text/editor}/testing/rich-text-editor.pageobject.ts (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-editor => rich-text/editor}/tests/nimble-rich-text-editor.directive.spec.ts (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-viewer => rich-text/viewer}/ng-package.json (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-viewer => rich-text/viewer}/nimble-rich-text-viewer.directive.ts (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-viewer => rich-text/viewer}/nimble-rich-text-viewer.module.ts (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-viewer => rich-text/viewer}/public-api.ts (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-viewer => rich-text/viewer}/testing/ng-package.json (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-viewer => rich-text/viewer}/testing/public-api.ts (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-viewer => rich-text/viewer}/testing/rich-text-viewer.pageobject.ts (100%) rename angular-workspace/projects/ni/nimble-angular/{rich-text-viewer => rich-text/viewer}/tests/nimble-rich-text-viewer.directive.spec.ts (100%) diff --git a/angular-workspace/projects/example-client-app/src/app/app.module.ts b/angular-workspace/projects/example-client-app/src/app/app.module.ts index 3b92310d7e..6f59580b53 100644 --- a/angular-workspace/projects/example-client-app/src/app/app.module.ts +++ b/angular-workspace/projects/example-client-app/src/app/app.module.ts @@ -22,8 +22,8 @@ import { NimbleTableColumnDateTextModule } from '@ni/nimble-angular/table-column import { NimbleTableColumnEnumTextModule } from '@ni/nimble-angular/table-column/enum-text'; import { NimbleTableColumnIconModule } from '@ni/nimble-angular/table-column/icon'; import { NimbleTableColumnNumberTextModule } from '@ni/nimble-angular/table-column/number-text'; -import { NimbleRichTextViewerModule } from '@ni/nimble-angular/rich-text-viewer'; -import { NimbleRichTextEditorModule } from '@ni/nimble-angular/rich-text-editor'; +import { NimbleRichTextViewerModule } from '@ni/nimble-angular/rich-text/viewer'; +import { NimbleRichTextEditorModule } from '@ni/nimble-angular/rich-text/editor'; import { AppComponent } from './app.component'; import { CustomAppComponent } from './customapp/customapp.component'; import { HeaderComponent } from './header/header.component'; diff --git a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts index 21c3a5c6a0..62455a4309 100644 --- a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts +++ b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts @@ -3,7 +3,7 @@ import { Component, Inject, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { DrawerLocation, MenuItem, NimbleDialogDirective, NimbleDrawerDirective, OptionNotFound, OPTION_NOT_FOUND, UserDismissed } from '@ni/nimble-angular'; import type { TableRecord } from '@ni/nimble-angular/table'; -import { NimbleRichTextEditorDirective } from '@ni/nimble-angular/rich-text-editor'; +import { NimbleRichTextEditorDirective } from '@ni/nimble-angular/rich-text/editor'; import { BehaviorSubject, Observable } from 'rxjs'; interface ComboboxItem { diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/ng-package.json b/angular-workspace/projects/ni/nimble-angular/rich-text/editor/ng-package.json similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-editor/ng-package.json rename to angular-workspace/projects/ni/nimble-angular/rich-text/editor/ng-package.json diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.directive.ts b/angular-workspace/projects/ni/nimble-angular/rich-text/editor/nimble-rich-text-editor.directive.ts similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.directive.ts rename to angular-workspace/projects/ni/nimble-angular/rich-text/editor/nimble-rich-text-editor.directive.ts diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.module.ts b/angular-workspace/projects/ni/nimble-angular/rich-text/editor/nimble-rich-text-editor.module.ts similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.module.ts rename to angular-workspace/projects/ni/nimble-angular/rich-text/editor/nimble-rich-text-editor.module.ts diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/public-api.ts b/angular-workspace/projects/ni/nimble-angular/rich-text/editor/public-api.ts similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-editor/public-api.ts rename to angular-workspace/projects/ni/nimble-angular/rich-text/editor/public-api.ts diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/ng-package.json b/angular-workspace/projects/ni/nimble-angular/rich-text/editor/testing/ng-package.json similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/ng-package.json rename to angular-workspace/projects/ni/nimble-angular/rich-text/editor/testing/ng-package.json diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/public-api.ts b/angular-workspace/projects/ni/nimble-angular/rich-text/editor/testing/public-api.ts similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/public-api.ts rename to angular-workspace/projects/ni/nimble-angular/rich-text/editor/testing/public-api.ts diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/rich-text-editor.pageobject.ts b/angular-workspace/projects/ni/nimble-angular/rich-text/editor/testing/rich-text-editor.pageobject.ts similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/rich-text-editor.pageobject.ts rename to angular-workspace/projects/ni/nimble-angular/rich-text/editor/testing/rich-text-editor.pageobject.ts diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/tests/nimble-rich-text-editor.directive.spec.ts b/angular-workspace/projects/ni/nimble-angular/rich-text/editor/tests/nimble-rich-text-editor.directive.spec.ts similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-editor/tests/nimble-rich-text-editor.directive.spec.ts rename to angular-workspace/projects/ni/nimble-angular/rich-text/editor/tests/nimble-rich-text-editor.directive.spec.ts diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/ng-package.json b/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/ng-package.json similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-viewer/ng-package.json rename to angular-workspace/projects/ni/nimble-angular/rich-text/viewer/ng-package.json diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/nimble-rich-text-viewer.directive.ts b/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/nimble-rich-text-viewer.directive.ts similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-viewer/nimble-rich-text-viewer.directive.ts rename to angular-workspace/projects/ni/nimble-angular/rich-text/viewer/nimble-rich-text-viewer.directive.ts diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/nimble-rich-text-viewer.module.ts b/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/nimble-rich-text-viewer.module.ts similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-viewer/nimble-rich-text-viewer.module.ts rename to angular-workspace/projects/ni/nimble-angular/rich-text/viewer/nimble-rich-text-viewer.module.ts diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/public-api.ts b/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/public-api.ts similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-viewer/public-api.ts rename to angular-workspace/projects/ni/nimble-angular/rich-text/viewer/public-api.ts diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/ng-package.json b/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/testing/ng-package.json similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/ng-package.json rename to angular-workspace/projects/ni/nimble-angular/rich-text/viewer/testing/ng-package.json diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/public-api.ts b/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/testing/public-api.ts similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/public-api.ts rename to angular-workspace/projects/ni/nimble-angular/rich-text/viewer/testing/public-api.ts diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/rich-text-viewer.pageobject.ts b/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/testing/rich-text-viewer.pageobject.ts similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/rich-text-viewer.pageobject.ts rename to angular-workspace/projects/ni/nimble-angular/rich-text/viewer/testing/rich-text-viewer.pageobject.ts diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/tests/nimble-rich-text-viewer.directive.spec.ts b/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/tests/nimble-rich-text-viewer.directive.spec.ts similarity index 100% rename from angular-workspace/projects/ni/nimble-angular/rich-text-viewer/tests/nimble-rich-text-viewer.directive.spec.ts rename to angular-workspace/projects/ni/nimble-angular/rich-text/viewer/tests/nimble-rich-text-viewer.directive.spec.ts diff --git a/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json b/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json index 8edd5163fd..5681985229 100644 --- a/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json +++ b/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json @@ -1,6 +1,6 @@ { "type": "patch", - "comment": "Update folder paths for importing rich text components", + "comment": "Revamp folder structure for rich text components", "packageName": "@ni/nimble-angular", "email": "123377523+vivinkrishna-ni@users.noreply.github.com", "dependentChangeType": "patch" diff --git a/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json b/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json index 9c790f46ee..a6bf4a46e8 100644 --- a/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json +++ b/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json @@ -1,6 +1,6 @@ { "type": "patch", - "comment": "Revamp rich text components folder structure", + "comment": "Revamp folder structure for rich text components", "packageName": "@ni/nimble-components", "email": "123377523+vivinkrishna-ni@users.noreply.github.com", "dependentChangeType": "patch" diff --git a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts index e6fbae74a8..48b5f65557 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts @@ -9,11 +9,7 @@ import type { Node } from 'prosemirror-model'; * Provides markdown serializer for rich text components */ export class RichTextMarkdownSerializer { - private readonly markdownSerializer: MarkdownSerializer; - - public constructor() { - this.markdownSerializer = this.initializeMarkdownSerializerForTipTap(); - } + private readonly markdownSerializer = this.initializeMarkdownSerializerForTipTap(); public serializeDOMToMarkdown(doc: Node): string { return this.markdownSerializer.serialize(doc); diff --git a/packages/nimble-components/src/rich-text/viewer/specs/README.md b/packages/nimble-components/src/rich-text/viewer/specs/README.md index 2af2ffa797..f14ecb9f52 100644 --- a/packages/nimble-components/src/rich-text/viewer/specs/README.md +++ b/packages/nimble-components/src/rich-text/viewer/specs/README.md @@ -1,3 +1,3 @@ # Nimble Rich Text Viewer -The spec of this component is added as part of the [`/rich-text-editor/specs/README.md`](../../rich-text-editor/specs/README.md) +The spec of this component is added as part of the [`/rich-text/editor/specs/README.md`](../../editor/specs/README.md) From 163de0ec83c4e9ac5850a463ba70cabd8f3deca7 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Mon, 4 Sep 2023 13:28:59 +0530 Subject: [PATCH 16/56] Moved the specs folder to common rich-text folder --- .../src/rich-text/{editor => }/specs/README.md | 0 .../{editor => }/specs/spec-images/button-state.png | Bin .../specs/spec-images/editor-sample.png | Bin .../specs/spec-images/viewer-sample.png | Bin .../src/rich-text/viewer/specs/README.md | 3 --- 5 files changed, 3 deletions(-) rename packages/nimble-components/src/rich-text/{editor => }/specs/README.md (100%) rename packages/nimble-components/src/rich-text/{editor => }/specs/spec-images/button-state.png (100%) rename packages/nimble-components/src/rich-text/{editor => }/specs/spec-images/editor-sample.png (100%) rename packages/nimble-components/src/rich-text/{editor => }/specs/spec-images/viewer-sample.png (100%) delete mode 100644 packages/nimble-components/src/rich-text/viewer/specs/README.md diff --git a/packages/nimble-components/src/rich-text/editor/specs/README.md b/packages/nimble-components/src/rich-text/specs/README.md similarity index 100% rename from packages/nimble-components/src/rich-text/editor/specs/README.md rename to packages/nimble-components/src/rich-text/specs/README.md diff --git a/packages/nimble-components/src/rich-text/editor/specs/spec-images/button-state.png b/packages/nimble-components/src/rich-text/specs/spec-images/button-state.png similarity index 100% rename from packages/nimble-components/src/rich-text/editor/specs/spec-images/button-state.png rename to packages/nimble-components/src/rich-text/specs/spec-images/button-state.png diff --git a/packages/nimble-components/src/rich-text/editor/specs/spec-images/editor-sample.png b/packages/nimble-components/src/rich-text/specs/spec-images/editor-sample.png similarity index 100% rename from packages/nimble-components/src/rich-text/editor/specs/spec-images/editor-sample.png rename to packages/nimble-components/src/rich-text/specs/spec-images/editor-sample.png diff --git a/packages/nimble-components/src/rich-text/editor/specs/spec-images/viewer-sample.png b/packages/nimble-components/src/rich-text/specs/spec-images/viewer-sample.png similarity index 100% rename from packages/nimble-components/src/rich-text/editor/specs/spec-images/viewer-sample.png rename to packages/nimble-components/src/rich-text/specs/spec-images/viewer-sample.png diff --git a/packages/nimble-components/src/rich-text/viewer/specs/README.md b/packages/nimble-components/src/rich-text/viewer/specs/README.md deleted file mode 100644 index f14ecb9f52..0000000000 --- a/packages/nimble-components/src/rich-text/viewer/specs/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Nimble Rich Text Viewer - -The spec of this component is added as part of the [`/rich-text/editor/specs/README.md`](../../editor/specs/README.md) From c2c6367b6e675b6ddd28d2e76e20ed1d16c64315 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Wed, 6 Sep 2023 09:42:10 +0530 Subject: [PATCH 17/56] Resolving PR comments --- .../rich-text/editor/ng-package.json | 2 +- .../rich-text/editor/testing/ng-package.json | 2 +- .../rich-text/viewer/ng-package.json | 2 +- .../rich-text/viewer/testing/ng-package.json | 2 +- .../src/rich-text/editor/index.ts | 96 +++++++++++++++++-- .../src/rich-text/models/markdown-parser.ts | 58 ----------- .../rich-text/models/markdown-serializer.ts | 58 ----------- .../src/rich-text/viewer/index.ts | 60 +++++++++++- 8 files changed, 149 insertions(+), 131 deletions(-) delete mode 100644 packages/nimble-components/src/rich-text/models/markdown-parser.ts delete mode 100644 packages/nimble-components/src/rich-text/models/markdown-serializer.ts diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text/editor/ng-package.json b/angular-workspace/projects/ni/nimble-angular/rich-text/editor/ng-package.json index 7945e60e70..e5440110fb 100644 --- a/angular-workspace/projects/ni/nimble-angular/rich-text/editor/ng-package.json +++ b/angular-workspace/projects/ni/nimble-angular/rich-text/editor/ng-package.json @@ -1,5 +1,5 @@ { - "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "$schema": "../../../../../../node_modules/ng-packagr/ng-package.schema.json", "lib": { "entryFile": "public-api.ts" } diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text/editor/testing/ng-package.json b/angular-workspace/projects/ni/nimble-angular/rich-text/editor/testing/ng-package.json index e5440110fb..55f020bdfb 100644 --- a/angular-workspace/projects/ni/nimble-angular/rich-text/editor/testing/ng-package.json +++ b/angular-workspace/projects/ni/nimble-angular/rich-text/editor/testing/ng-package.json @@ -1,5 +1,5 @@ { - "$schema": "../../../../../../node_modules/ng-packagr/ng-package.schema.json", + "$schema": "../../../../../../../node_modules/ng-packagr/ng-package.schema.json", "lib": { "entryFile": "public-api.ts" } diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/ng-package.json b/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/ng-package.json index 7945e60e70..e5440110fb 100644 --- a/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/ng-package.json +++ b/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/ng-package.json @@ -1,5 +1,5 @@ { - "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "$schema": "../../../../../../node_modules/ng-packagr/ng-package.schema.json", "lib": { "entryFile": "public-api.ts" } diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/testing/ng-package.json b/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/testing/ng-package.json index e5440110fb..55f020bdfb 100644 --- a/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/testing/ng-package.json +++ b/angular-workspace/projects/ni/nimble-angular/rich-text/viewer/testing/ng-package.json @@ -1,5 +1,5 @@ { - "$schema": "../../../../../../node_modules/ng-packagr/ng-package.schema.json", + "$schema": "../../../../../../../node_modules/ng-packagr/ng-package.schema.json", "lib": { "entryFile": "public-api.ts" } diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index 8b8006e957..7744f61000 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -13,6 +13,15 @@ import { AnyExtension, Extension } 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'; @@ -29,8 +38,6 @@ import { styles } from './styles'; import type { ToggleButton } from '../../toggle-button'; import { TipTapNodeName } from './types'; import type { ErrorPattern } from '../../patterns/error/types'; -import { RichTextMarkdownParser } from '../models/markdown-parser'; -import { RichTextMarkdownSerializer } from '../models/markdown-serializer'; declare global { interface HTMLElementTagNameMap { @@ -146,8 +153,9 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { private resizeObserver?: ResizeObserver; private updateScrollbarWidthQueued = false; - private readonly markdownParser = new RichTextMarkdownParser(); - private readonly markdownSerializer = new RichTextMarkdownSerializer(); + private readonly markdownParser = this.initializeMarkdownParser(); + private readonly markdownSerializer = this.initializeMarkdownSerializer(); + private readonly domSerializer = DOMSerializer.fromSchema(schema); private readonly xmlSerializer = new XMLSerializer(); /** @@ -315,9 +323,10 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { * @public */ public getMarkdown(): string { - return this.markdownSerializer.serializeDOMToMarkdown( + const markdownContent = this.markdownSerializer.serialize( this.tiptapEditor.state.doc ); + return markdownContent; } /** @@ -369,10 +378,85 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { * This function takes the Fragment from parseMarkdownToDOM function and return the serialized string using XMLSerializer */ private getHtmlContent(markdown: string): string { - const documentFragment = this.markdownParser.parseMarkdownToDOM(markdown); + 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 + ); + } + /** * Binding the "transaction" event to the editor allows continuous monitoring the events and updating the button state in response to * various actions such as mouse events, keyboard events, changes in the editor content etc,. diff --git a/packages/nimble-components/src/rich-text/models/markdown-parser.ts b/packages/nimble-components/src/rich-text/models/markdown-parser.ts deleted file mode 100644 index 9a1ae9a2c4..0000000000 --- a/packages/nimble-components/src/rich-text/models/markdown-parser.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - schema, - defaultMarkdownParser, - MarkdownParser -} from 'prosemirror-markdown'; -import { DOMSerializer } from 'prosemirror-model'; - -/** - * Provides markdown parser for rich text components - */ -export class RichTextMarkdownParser { - private readonly markdownParser: MarkdownParser; - private readonly domSerializer: DOMSerializer; - - public constructor() { - this.markdownParser = this.initializeMarkdownParser(); - this.domSerializer = DOMSerializer.fromSchema(schema); - } - - /** - * This function takes a markdown string, parses it using the ProseMirror MarkdownParser, serializes the parsed content into a - * DOM structure using a DOMSerializer, and returns the serialized result. - * If the markdown parser returns null, it will clear the viewer component by creating an empty document fragment. - */ - public parseMarkdownToDOM(value: string): HTMLElement | DocumentFragment { - const parsedMarkdownContent = this.markdownParser.parse(value); - if (parsedMarkdownContent === null) { - return document.createDocumentFragment(); - } - return this.domSerializer.serializeFragment( - parsedMarkdownContent.content - ); - } - - 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', - 'autolink' - ]); - - return new MarkdownParser( - schema, - supportedTokenizerRules, - defaultMarkdownParser.tokens - ); - } -} diff --git a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts deleted file mode 100644 index 48b5f65557..0000000000 --- a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - MarkdownSerializer, - defaultMarkdownSerializer, - MarkdownSerializerState -} from 'prosemirror-markdown'; -import type { Node } from 'prosemirror-model'; - -/** - * Provides markdown serializer for rich text components - */ -export class RichTextMarkdownSerializer { - private readonly markdownSerializer = this.initializeMarkdownSerializerForTipTap(); - - public serializeDOMToMarkdown(doc: Node): string { - return this.markdownSerializer.serialize(doc); - } - - private initializeMarkdownSerializerForTipTap(): 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); - } -} diff --git a/packages/nimble-components/src/rich-text/viewer/index.ts b/packages/nimble-components/src/rich-text/viewer/index.ts index 6d0b86dc2d..132daa978d 100644 --- a/packages/nimble-components/src/rich-text/viewer/index.ts +++ b/packages/nimble-components/src/rich-text/viewer/index.ts @@ -1,8 +1,13 @@ import { DesignSystem, FoundationElement } from '@microsoft/fast-foundation'; +import { + schema, + defaultMarkdownParser, + MarkdownParser +} from 'prosemirror-markdown'; +import { DOMSerializer } from 'prosemirror-model'; import { observable } from '@microsoft/fast-element'; import { template } from './template'; import { styles } from './styles'; -import { RichTextMarkdownParser } from '../models/markdown-parser'; declare global { interface HTMLElementTagNameMap { @@ -26,8 +31,14 @@ export class RichTextViewer extends FoundationElement { * @internal */ public viewer!: HTMLDivElement; + private readonly markdownParser: MarkdownParser; + private readonly domSerializer: DOMSerializer; - private readonly markdownParser = new RichTextMarkdownParser(); + public constructor() { + super(); + this.domSerializer = DOMSerializer.fromSchema(schema); + this.markdownParser = this.initializeMarkdownParser(); + } /** * @internal @@ -46,11 +57,50 @@ export class RichTextViewer extends FoundationElement { } } + 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', + 'autolink' + ]); + + return new MarkdownParser( + schema, + supportedTokenizerRules, + defaultMarkdownParser.tokens + ); + } + + /** + * + * This function takes a markdown string, parses it using the ProseMirror MarkdownParser, serializes the parsed content into a + * DOM structure using a DOMSerializer, and returns the serialized result. + * If the markdown parser returns null, it will clear the viewer component by creating an empty document fragment. + */ + 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 updateView(): void { if (this.markdown) { - const serializedContent = this.markdownParser.parseMarkdownToDOM( - this.markdown - ); + const serializedContent = this.parseMarkdownToDOM(this.markdown); this.viewer.replaceChildren(serializedContent); } else { this.viewer.innerHTML = ''; From 6c9de89db60d70268c5ae806ade06502715615ad Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Wed, 6 Sep 2023 09:57:29 +0530 Subject: [PATCH 18/56] Minor import order in viewer spec --- .../src/rich-text/viewer/tests/rich-text-viewer.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts index 726feb4a93..7246dcecc4 100644 --- a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts +++ b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts @@ -1,9 +1,9 @@ import { html } from '@microsoft/fast-element'; +import { RichTextViewer, richTextViewerTag } from '..'; import { fixture, type Fixture } from '../../../utilities/tests/fixture'; import { RichTextViewerPageObject } from '../testing/rich-text-viewer.pageobject'; import { wackyStrings } from '../../../utilities/tests/wacky-strings'; import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; -import { RichTextViewer, richTextViewerTag } from '..'; async function setup(): Promise> { return fixture( From 065743482ee34a9ede4ced6de1770fb43d31f93e Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Wed, 6 Sep 2023 12:08:05 +0530 Subject: [PATCH 19/56] Update paths for label spec file --- .../editor/tests/rich-text-editor-labels.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-labels.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-labels.spec.ts index 8bcd734c6b..78178af003 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-labels.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-labels.spec.ts @@ -1,15 +1,15 @@ import { html } from '@microsoft/fast-element'; import { richTextEditorTag, type RichTextEditor } from '..'; -import { type Fixture, fixture } from '../../utilities/tests/fixture'; -import { themeProviderTag, type ThemeProvider } from '../../theme-provider'; +import { type Fixture, fixture } from '../../../utilities/tests/fixture'; +import { themeProviderTag, type ThemeProvider } from '../../../theme-provider'; import { LabelProviderRichText, labelProviderRichTextTag -} from '../../label-provider/rich-text'; +} from '../../../label-provider/rich-text'; import { RichTextEditorPageObject } from '../testing/rich-text-editor.pageobject'; import { LabelProvider, ToolbarButton } from '../testing/types'; -import { getSpecTypeByNamedList } from '../../utilities/tests/parameterized'; -import { waitForUpdatesAsync } from '../../testing/async-helpers'; +import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; +import { waitForUpdatesAsync } from '../../../testing/async-helpers'; async function setup(): Promise> { return fixture( From 92df4e9a12f72e410c78e1c7a8c0a9596301767e Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Wed, 6 Sep 2023 12:32:03 +0530 Subject: [PATCH 20/56] Added markdown model files --- .../src/rich-text/editor/index.ts | 96 ++----------------- .../src/rich-text/models/markdown-parser.ts | 58 +++++++++++ .../rich-text/models/markdown-serializer.ts | 58 +++++++++++ .../src/rich-text/viewer/index.ts | 60 +----------- 4 files changed, 127 insertions(+), 145 deletions(-) create mode 100644 packages/nimble-components/src/rich-text/models/markdown-parser.ts create mode 100644 packages/nimble-components/src/rich-text/models/markdown-serializer.ts diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index 7744f61000..8b8006e957 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -13,15 +13,6 @@ import { AnyExtension, Extension } 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'; @@ -38,6 +29,8 @@ import { styles } from './styles'; import type { ToggleButton } from '../../toggle-button'; import { TipTapNodeName } from './types'; import type { ErrorPattern } from '../../patterns/error/types'; +import { RichTextMarkdownParser } from '../models/markdown-parser'; +import { RichTextMarkdownSerializer } from '../models/markdown-serializer'; declare global { interface HTMLElementTagNameMap { @@ -153,9 +146,8 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { private resizeObserver?: ResizeObserver; private updateScrollbarWidthQueued = false; - private readonly markdownParser = this.initializeMarkdownParser(); - private readonly markdownSerializer = this.initializeMarkdownSerializer(); - private readonly domSerializer = DOMSerializer.fromSchema(schema); + private readonly markdownParser = new RichTextMarkdownParser(); + private readonly markdownSerializer = new RichTextMarkdownSerializer(); private readonly xmlSerializer = new XMLSerializer(); /** @@ -323,10 +315,9 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { * @public */ public getMarkdown(): string { - const markdownContent = this.markdownSerializer.serialize( + return this.markdownSerializer.serializeDOMToMarkdown( this.tiptapEditor.state.doc ); - return markdownContent; } /** @@ -378,85 +369,10 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { * 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); + const documentFragment = this.markdownParser.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 - ); - } - /** * Binding the "transaction" event to the editor allows continuous monitoring the events and updating the button state in response to * various actions such as mouse events, keyboard events, changes in the editor content etc,. diff --git a/packages/nimble-components/src/rich-text/models/markdown-parser.ts b/packages/nimble-components/src/rich-text/models/markdown-parser.ts new file mode 100644 index 0000000000..9a1ae9a2c4 --- /dev/null +++ b/packages/nimble-components/src/rich-text/models/markdown-parser.ts @@ -0,0 +1,58 @@ +import { + schema, + defaultMarkdownParser, + MarkdownParser +} from 'prosemirror-markdown'; +import { DOMSerializer } from 'prosemirror-model'; + +/** + * Provides markdown parser for rich text components + */ +export class RichTextMarkdownParser { + private readonly markdownParser: MarkdownParser; + private readonly domSerializer: DOMSerializer; + + public constructor() { + this.markdownParser = this.initializeMarkdownParser(); + this.domSerializer = DOMSerializer.fromSchema(schema); + } + + /** + * This function takes a markdown string, parses it using the ProseMirror MarkdownParser, serializes the parsed content into a + * DOM structure using a DOMSerializer, and returns the serialized result. + * If the markdown parser returns null, it will clear the viewer component by creating an empty document fragment. + */ + public parseMarkdownToDOM(value: string): HTMLElement | DocumentFragment { + const parsedMarkdownContent = this.markdownParser.parse(value); + if (parsedMarkdownContent === null) { + return document.createDocumentFragment(); + } + return this.domSerializer.serializeFragment( + parsedMarkdownContent.content + ); + } + + 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', + 'autolink' + ]); + + return new MarkdownParser( + schema, + supportedTokenizerRules, + defaultMarkdownParser.tokens + ); + } +} diff --git a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts new file mode 100644 index 0000000000..48b5f65557 --- /dev/null +++ b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts @@ -0,0 +1,58 @@ +import { + MarkdownSerializer, + defaultMarkdownSerializer, + MarkdownSerializerState +} from 'prosemirror-markdown'; +import type { Node } from 'prosemirror-model'; + +/** + * Provides markdown serializer for rich text components + */ +export class RichTextMarkdownSerializer { + private readonly markdownSerializer = this.initializeMarkdownSerializerForTipTap(); + + public serializeDOMToMarkdown(doc: Node): string { + return this.markdownSerializer.serialize(doc); + } + + private initializeMarkdownSerializerForTipTap(): 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); + } +} diff --git a/packages/nimble-components/src/rich-text/viewer/index.ts b/packages/nimble-components/src/rich-text/viewer/index.ts index 132daa978d..6d0b86dc2d 100644 --- a/packages/nimble-components/src/rich-text/viewer/index.ts +++ b/packages/nimble-components/src/rich-text/viewer/index.ts @@ -1,13 +1,8 @@ import { DesignSystem, FoundationElement } from '@microsoft/fast-foundation'; -import { - schema, - defaultMarkdownParser, - MarkdownParser -} from 'prosemirror-markdown'; -import { DOMSerializer } from 'prosemirror-model'; import { observable } from '@microsoft/fast-element'; import { template } from './template'; import { styles } from './styles'; +import { RichTextMarkdownParser } from '../models/markdown-parser'; declare global { interface HTMLElementTagNameMap { @@ -31,14 +26,8 @@ export class RichTextViewer extends FoundationElement { * @internal */ public viewer!: HTMLDivElement; - private readonly markdownParser: MarkdownParser; - private readonly domSerializer: DOMSerializer; - public constructor() { - super(); - this.domSerializer = DOMSerializer.fromSchema(schema); - this.markdownParser = this.initializeMarkdownParser(); - } + private readonly markdownParser = new RichTextMarkdownParser(); /** * @internal @@ -57,50 +46,11 @@ export class RichTextViewer extends FoundationElement { } } - 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', - 'autolink' - ]); - - return new MarkdownParser( - schema, - supportedTokenizerRules, - defaultMarkdownParser.tokens - ); - } - - /** - * - * This function takes a markdown string, parses it using the ProseMirror MarkdownParser, serializes the parsed content into a - * DOM structure using a DOMSerializer, and returns the serialized result. - * If the markdown parser returns null, it will clear the viewer component by creating an empty document fragment. - */ - 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 updateView(): void { if (this.markdown) { - const serializedContent = this.parseMarkdownToDOM(this.markdown); + const serializedContent = this.markdownParser.parseMarkdownToDOM( + this.markdown + ); this.viewer.replaceChildren(serializedContent); } else { this.viewer.innerHTML = ''; From 46126aa8084389a4b634bbdf74848b09a87b1fd0 Mon Sep 17 00:00:00 2001 From: Sai krishnan Perumal Date: Wed, 6 Sep 2023 15:11:03 +0530 Subject: [PATCH 21/56] refactor: migrate markdown parser tests from text editor and viewer component to model scripts Signed-off-by: Sai krishnan Perumal --- .../editor/tests/rich-text-editor.spec.ts | 323 --------- .../models/tests/markdown-parser.spec.ts | 332 ++++++++++ .../viewer/tests/rich-text-viewer.spec.ts | 616 ------------------ 3 files changed, 332 insertions(+), 939 deletions(-) create mode 100644 packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index 4c486996a0..9e663d3ff2 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -670,329 +670,6 @@ describe('RichTextEditor', () => { ]); }); - describe('supported rich text formatting options from markdown string to its respective HTML elements', () => { - beforeEach(async () => { - await connect(); - }); - - afterEach(async () => { - await disconnect(); - }); - - it('bold markdown string("**") to "strong" HTML tag', () => { - element.setMarkdown('**Bold**'); - expect(pageObject.getEditorTagNames()).toEqual(['P', 'STRONG']); - expect(pageObject.getEditorLeafContents()).toEqual(['Bold']); - }); - - it('bold markdown string("__") to "strong" HTML tag', () => { - element.setMarkdown('__Bold__'); - - expect(pageObject.getEditorTagNames()).toEqual(['P', 'STRONG']); - expect(pageObject.getEditorLeafContents()).toEqual(['Bold']); - }); - - it('italics markdown string("*") to "em" HTML tag', () => { - element.setMarkdown('*Italics*'); - - expect(pageObject.getEditorTagNames()).toEqual(['P', 'EM']); - expect(pageObject.getEditorLeafContents()).toEqual(['Italics']); - }); - - it('italics markdown string("_") to "em" HTML tag', () => { - element.setMarkdown('_Italics_'); - - expect(pageObject.getEditorTagNames()).toEqual(['P', 'EM']); - expect(pageObject.getEditorLeafContents()).toEqual(['Italics']); - }); - - it('numbered list markdown string("1.") to "ol" and "li" HTML tags', () => { - element.setMarkdown('1. Numbered list'); - - expect(pageObject.getEditorTagNames()).toEqual(['OL', 'LI', 'P']); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'Numbered list' - ]); - }); - - it('numbered list markdown string("1)") to "ol" and "li" HTML tags', () => { - element.setMarkdown('1) Numbered list'); - - expect(pageObject.getEditorTagNames()).toEqual(['OL', 'LI', 'P']); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'Numbered list' - ]); - }); - - it('multiple numbered lists markdown string("1.\n2.") to "ol" and "li" HTML tags', () => { - element.setMarkdown('1. Option 1\n 2. Option 2'); - - expect(pageObject.getEditorTagNames()).toEqual([ - 'OL', - 'LI', - 'P', - 'LI', - 'P' - ]); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'Option 1', - 'Option 2' - ]); - }); - - it('multiple empty numbered lists markdown string("1.\n2.") to "ol" and "li" HTML tags', () => { - element.setMarkdown('1. \n 2. '); - - expect(pageObject.getEditorTagNames()).toEqual([ - 'OL', - 'LI', - 'P', - 'BR', - 'LI', - 'P', - 'BR' - ]); - expect(pageObject.getEditorLeafContents()).toEqual(['', '']); - }); - - it('numbered lists that start with numbers and are not sequential to "ol" and "li" HTML tags', () => { - element.setMarkdown('1. Option 1\n 1. Option 2'); - - expect(pageObject.getEditorTagNames()).toEqual([ - 'OL', - 'LI', - 'P', - 'LI', - 'P' - ]); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'Option 1', - 'Option 2' - ]); - }); - - it('numbered lists if there is some content between lists', () => { - element.setMarkdown( - '1. Option 1\n\nSome content in between lists\n\n 2. Option 2' - ); - - expect(pageObject.getEditorTagNames()).toEqual([ - 'OL', - 'LI', - 'P', - 'P', - 'OL', - 'LI', - 'P' - ]); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'Option 1', - 'Some content in between lists', - 'Option 2' - ]); - }); - - it('bulleted list markdown string("*") to "ul" and "li" HTML tags', () => { - element.setMarkdown('* Bulleted list'); - - expect(pageObject.getEditorTagNames()).toEqual(['UL', 'LI', 'P']); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'Bulleted list' - ]); - }); - - it('bulleted list markdown string("-") to "ul" and "li" HTML tags', () => { - element.setMarkdown('- Bulleted list'); - - expect(pageObject.getEditorTagNames()).toEqual(['UL', 'LI', 'P']); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'Bulleted list' - ]); - }); - - it('bulleted list markdown string("+") to "ul" and "li" HTML tags', () => { - element.setMarkdown('+ Bulleted list'); - - expect(pageObject.getEditorTagNames()).toEqual(['UL', 'LI', 'P']); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'Bulleted list' - ]); - }); - - it('multiple bulleted lists markdown string("* \n* \n*") to "ul" and "li" HTML tags', () => { - element.setMarkdown('* Option 1\n * Option 2\n * Option 3'); - - expect(pageObject.getEditorTagNames()).toEqual([ - 'UL', - 'LI', - 'P', - 'LI', - 'P', - 'LI', - 'P' - ]); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'Option 1', - 'Option 2', - 'Option 3' - ]); - }); - - it('bulleted lists if there is some content between lists', () => { - element.setMarkdown( - '* Option 1\n\nSome content in between lists\n\n * Option 2' - ); - - expect(pageObject.getEditorTagNames()).toEqual([ - 'UL', - 'LI', - 'P', - 'P', - 'UL', - 'LI', - 'P' - ]); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'Option 1', - 'Some content in between lists', - 'Option 2' - ]); - }); - - it('numbered list with bold markdown string to "ol", "li" and "strong" HTML tags', () => { - element.setMarkdown('1. **Numbered list in bold**'); - - expect(pageObject.getEditorTagNames()).toEqual([ - 'OL', - 'LI', - 'P', - 'STRONG' - ]); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'Numbered list in bold' - ]); - }); - - it('bulleted list with italics markdown string to "ul", "li" and "em" HTML tags', () => { - element.setMarkdown('* *Bulleted list in italics*'); - - expect(pageObject.getEditorTagNames()).toEqual([ - 'UL', - 'LI', - 'P', - 'EM' - ]); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'Bulleted list in italics' - ]); - }); - - it('combination of all supported markdown string', () => { - element.setMarkdown( - '1. ***Numbered list with bold and italics***\n* ___Bulleted list with bold and italics___' - ); - - expect(pageObject.getEditorTagNames()).toEqual([ - 'OL', - 'LI', - 'P', - 'STRONG', - 'EM', - 'UL', - 'LI', - 'P', - 'STRONG', - 'EM' - ]); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'Numbered list with bold and italics', - 'Bulleted list with bold and italics' - ]); - }); - }); - - describe('various not supported markdown string values render as unchanged strings', () => { - const notSupportedMarkdownStrings: { name: string }[] = [ - { name: '> blockquote' }, - { name: '`code`' }, - { name: '```fence```' }, - { name: '~~Strikethrough~~' }, - { name: '# Heading 1' }, - { name: '## Heading 2' }, - { name: '### Heading 3' }, - { name: '[link](url)' }, - { name: '[ref][link] [link]:url' }, - { name: '![Text](Image)' }, - { name: ' ' }, - { name: '---' }, - { name: '***' }, - { name: '___' }, - { name: '(c) (C) (r) (R) (tm) (TM) (p) (P) +-' }, - { name: '

text

' }, - { name: 'not bold' }, - { name: 'not italic' }, - { name: '
  1. not list
  2. not list
' }, - { name: '
  • not list
  • not list
' }, - { - name: 'https://nimble.ni.dev/' - }, - { name: '' } - ]; - - const focused: string[] = []; - const disabled: string[] = []; - for (const value of notSupportedMarkdownStrings) { - const specType = getSpecTypeByNamedList(value, focused, disabled); - specType( - `string "${value.name}" renders as plain text "${value.name}" within paragraph tag`, - // eslint-disable-next-line @typescript-eslint/no-loop-func - async () => { - element.setMarkdown(value.name); - - await connect(); - - expect(pageObject.getEditorTagNames()).toEqual(['P']); - expect(pageObject.getEditorLeafContents()).toEqual([ - value.name - ]); - - await disconnect(); - } - ); - } - }); - - describe('various wacky string values render as unchanged strings', () => { - const focused: string[] = []; - const disabled: string[] = []; - - wackyStrings - .filter(value => value.name !== '\x00') - .forEach(value => { - const specType = getSpecTypeByNamedList( - value, - focused, - disabled - ); - specType( - `wacky string "${value.name}" that are unmodified when set the same "${value.name}" within paragraph tag`, - // eslint-disable-next-line @typescript-eslint/no-loop-func - async () => { - element.setMarkdown(value.name); - - await connect(); - - expect(pageObject.getEditorTagNames()).toEqual(['P']); - expect(pageObject.getEditorLeafContents()).toEqual([ - value.name - ]); - - await disconnect(); - } - ); - }); - }); - describe('various wacky string values modified when rendered', () => { const focused: string[] = []; const disabled: string[] = []; diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts new file mode 100644 index 0000000000..73ac008cbf --- /dev/null +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -0,0 +1,332 @@ +import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; +import { wackyStrings } from '../../../utilities/tests/wacky-strings'; +import { RichTextMarkdownParser } from '../markdown-parser'; + +describe('Markdown parser', () => { + let markDownParser: RichTextMarkdownParser = new RichTextMarkdownParser(); + + beforeEach(() => { + markDownParser = new RichTextMarkdownParser(); + }); + + function getTagsFromDocumentFragment(doc: DocumentFragment): string[] { + const nodes = Array.from(doc.querySelectorAll('*')).map( + el => el.tagName + ); + return nodes; + } + + function getLeafContentsFromDocumentFragment(doc: DocumentFragment): string[] { + const nodes = Array.from(doc.querySelectorAll('*')) + .filter((el, _) => { + return el.children.length === 0; + }) + .map(el => el.textContent || ''); + return nodes; + } + + describe('supported rich text formatting options from markdown string to its respective HTML elements', () => { + it('bold markdown string("**") to "strong" HTML tag', () => { + const doc = markDownParser.parseMarkdownToDOM('**Bold**'); + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['P', 'STRONG']); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual(['Bold']); + }); + + it('bold markdown string("__") to "strong" HTML tag', () => { + const doc = markDownParser.parseMarkdownToDOM('__Bold__'); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['P', 'STRONG']); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual(['Bold']); + }); + + it('italics markdown string("*") to "em" HTML tag', () => { + const doc = markDownParser.parseMarkdownToDOM('*Italics*'); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['P', 'EM']); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual(['Italics']); + }); + + it('italics markdown string("_") to "em" HTML tag', () => { + const doc = markDownParser.parseMarkdownToDOM('_Italics_'); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['P', 'EM']); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual(['Italics']); + }); + + it('numbered list markdown string("1.") to "ol" and "li" HTML tags', () => { + const doc = markDownParser.parseMarkdownToDOM('1. Numbered list'); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['OL', 'LI', 'P']); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'Numbered list' + ]); + }); + + it('numbered list markdown string("1)") to "ol" and "li" HTML tags', () => { + const doc = markDownParser.parseMarkdownToDOM('1) Numbered list'); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['OL', 'LI', 'P']); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'Numbered list' + ]); + }); + + it('multiple numbered lists markdown string("1.\n2.") to "ol" and "li" HTML tags', () => { + const doc = markDownParser.parseMarkdownToDOM('1. Option 1\n 2. Option 2'); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'OL', + 'LI', + 'P', + 'LI', + 'P' + ]); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'Option 1', + 'Option 2' + ]); + }); + + it('multiple empty numbered lists markdown string("1.\n2.") to "ol" and "li" HTML tags', () => { + const doc = markDownParser.parseMarkdownToDOM('1. \n 2. '); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'OL', + 'LI', + 'P', + 'LI', + 'P' + ]); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual(['', '']); + }); + + it('numbered lists that start with numbers and are not sequential to "ol" and "li" HTML tags', () => { + const doc = markDownParser.parseMarkdownToDOM('1. Option 1\n 1. Option 2'); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'OL', + 'LI', + 'P', + 'LI', + 'P' + ]); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'Option 1', + 'Option 2' + ]); + }); + + it('numbered lists if there is some content between lists', () => { + const doc = markDownParser.parseMarkdownToDOM( + '1. Option 1\n\nSome content in between lists\n\n 2. Option 2' + ); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'OL', + 'LI', + 'P', + 'P', + 'OL', + 'LI', + 'P' + ]); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'Option 1', + 'Some content in between lists', + 'Option 2' + ]); + }); + + it('bulleted list markdown string("*") to "ul" and "li" HTML tags', () => { + const doc = markDownParser.parseMarkdownToDOM('* Bulleted list'); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['UL', 'LI', 'P']); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'Bulleted list' + ]); + }); + + it('bulleted list markdown string("-") to "ul" and "li" HTML tags', () => { + const doc = markDownParser.parseMarkdownToDOM('- Bulleted list'); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['UL', 'LI', 'P']); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'Bulleted list' + ]); + }); + + it('bulleted list markdown string("+") to "ul" and "li" HTML tags', () => { + const doc = markDownParser.parseMarkdownToDOM('+ Bulleted list'); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['UL', 'LI', 'P']); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'Bulleted list' + ]); + }); + + it('multiple bulleted lists markdown string("* \n* \n*") to "ul" and "li" HTML tags', () => { + const doc = markDownParser.parseMarkdownToDOM('* Option 1\n * Option 2\n * Option 3'); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'UL', + 'LI', + 'P', + 'LI', + 'P', + 'LI', + 'P' + ]); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'Option 1', + 'Option 2', + 'Option 3' + ]); + }); + + it('bulleted lists if there is some content between lists', () => { + const doc = markDownParser.parseMarkdownToDOM( + '* Option 1\n\nSome content in between lists\n\n * Option 2' + ); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'UL', + 'LI', + 'P', + 'P', + 'UL', + 'LI', + 'P' + ]); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'Option 1', + 'Some content in between lists', + 'Option 2' + ]); + }); + + it('numbered list with bold markdown string to "ol", "li" and "strong" HTML tags', () => { + const doc = markDownParser.parseMarkdownToDOM('1. **Numbered list in bold**'); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'OL', + 'LI', + 'P', + 'STRONG' + ]); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'Numbered list in bold' + ]); + }); + + it('bulleted list with italics markdown string to "ul", "li" and "em" HTML tags', () => { + const doc = markDownParser.parseMarkdownToDOM('* *Bulleted list in italics*'); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'UL', + 'LI', + 'P', + 'EM' + ]); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'Bulleted list in italics' + ]); + }); + + it('combination of all supported markdown string', () => { + const doc = markDownParser.parseMarkdownToDOM( + '1. ***Numbered list with bold and italics***\n* ___Bulleted list with bold and italics___' + ); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'OL', + 'LI', + 'P', + 'EM', + 'STRONG', + 'UL', + 'LI', + 'P', + 'EM', + 'STRONG' + ]); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + 'Numbered list with bold and italics', + 'Bulleted list with bold and italics' + ]); + }); + }); + + describe('various not supported markdown string values render as unchanged strings', () => { + const notSupportedMarkdownStrings: { name: string }[] = [ + { name: '> blockquote' }, + { name: '`code`' }, + { name: '```fence```' }, + { name: '~~Strikethrough~~' }, + { name: '# Heading 1' }, + { name: '## Heading 2' }, + { name: '### Heading 3' }, + { name: '[link](url)' }, + { name: '[ref][link] [link]:url' }, + { name: '![Text](Image)' }, + { name: ' ' }, + { name: '---' }, + { name: '***' }, + { name: '___' }, + { name: '(c) (C) (r) (R) (tm) (TM) (p) (P) +-' }, + { name: '

text

' }, + { name: 'not bold' }, + { name: 'not italic' }, + { name: '
  1. not list
  2. not list
' }, + { name: '
  • not list
  • not list
' }, + { + name: 'https://nimble.ni.dev/' + }, + { name: '' } + ]; + + const focused: string[] = []; + const disabled: string[] = []; + for (const value of notSupportedMarkdownStrings) { + const specType = getSpecTypeByNamedList(value, focused, disabled); + specType( + `string "${value.name}" renders as plain text "${value.name}" within paragraph tag`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + const doc = markDownParser.parseMarkdownToDOM(value.name); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['P']); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + value.name + ]); + } + ); + } + }); + + describe('various wacky string values render as unchanged strings', () => { + const focused: string[] = []; + const disabled: string[] = []; + + wackyStrings + .filter(value => value.name !== '\x00') + .forEach(value => { + const specType = getSpecTypeByNamedList( + value, + focused, + disabled + ); + specType( + `wacky string "${value.name}" that are unmodified when set the same "${value.name}" within paragraph tag`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + const doc = markDownParser.parseMarkdownToDOM(value.name); + + expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['P']); + expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + value.name + ]); + } + ); + }); + }); +}); \ No newline at end of file diff --git a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts index 7246dcecc4..845407faf5 100644 --- a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts +++ b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts @@ -84,622 +84,6 @@ describe('RichTextViewer', () => { await disconnect(); }); - describe('supported rich text formatting options from markdown string to its respective HTML elements', () => { - afterEach(async () => { - await disconnect(); - }); - - it('bold markdown string("**") to "strong" HTML tag', async () => { - element.markdown = '**Bold**'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'P', - 'STRONG' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'Bold' - ); - }); - - it('bold markdown string("__") to "strong" HTML tag', async () => { - element.markdown = '__Bold__'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'P', - 'STRONG' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'Bold' - ); - }); - - it('italics markdown string("*") to "em" HTML tag', async () => { - element.markdown = '*Italics*'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'P', - 'EM' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'Italics' - ); - }); - - it('italics markdown string("_") to "em" HTML tag', async () => { - element.markdown = '_Italics_'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'P', - 'EM' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'Italics' - ); - }); - - it('numbered list markdown string("1.") to "ol" and "li" HTML tags', async () => { - element.markdown = '1. Numbered list'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'OL', - 'LI', - 'P' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'Numbered list' - ); - }); - - it('numbered list markdown string("1)") to "ol" and "li" HTML tags', async () => { - element.markdown = '1) Numbered list'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'OL', - 'LI', - 'P' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'Numbered list' - ); - }); - - it('multiple numbered lists markdown string("1.\n2.") to "ol" and "li" HTML tags', async () => { - element.markdown = '1. Option 1\n 2. Option 2'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'OL', - 'LI', - 'P', - 'LI', - 'P' - ]); - expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ - 'Option 1', - 'Option 2' - ]); - }); - - it('nested numbered lists markdown string to "ol" and "li" HTML tags', async () => { - element.markdown = '1. Option 1\n\n 1. Option 2'; - - await connect(); - - expect( - pageObject.getRenderedMarkdownTagNamesWithClosingTags() - ).toEqual([ - 'OL', - 'LI', - 'P', - '/P', - 'OL', - 'LI', - 'P', - '/P', - '/LI', - '/OL', - '/LI', - '/OL' - ]); - expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ - 'Option 1', - 'Option 2' - ]); - }); - - it('numbered lists markdown string to "ol" tag and nested bullet list markdown string to "ul" tag', async () => { - element.markdown = '1. Option 1\n\n * Option 2'; - - await connect(); - - expect( - pageObject.getRenderedMarkdownTagNamesWithClosingTags() - ).toEqual([ - 'OL', - 'LI', - 'P', - '/P', - 'UL', - 'LI', - 'P', - '/P', - '/LI', - '/UL', - '/LI', - '/OL' - ]); - expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ - 'Option 1', - 'Option 2' - ]); - }); - - it('sequential numbered and bulleted lists should to "ol" and once "ol" tags are closed, should have "ul" tags', async () => { - element.markdown = '1. Option 1\n\n* Option 2'; - - await connect(); - - expect( - pageObject.getRenderedMarkdownTagNamesWithClosingTags() - ).toEqual([ - 'OL', - 'LI', - 'P', - '/P', - '/LI', - '/OL', - 'UL', - 'LI', - 'P', - '/P', - '/LI', - '/UL' - ]); - expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ - 'Option 1', - 'Option 2' - ]); - }); - - it('multiple empty numbered lists markdown string("1.\n2.") to "ol" and "li" HTML tags', async () => { - element.markdown = '1. \n 2. '; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'OL', - 'LI', - 'P', - 'LI', - 'P' - ]); - expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ - '', - '' - ]); - }); - - it('numbered lists that start with numbers and are not sequential to "ol" and "li" HTML tags', async () => { - element.markdown = '1. Option 1\n 1. Option 2'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'OL', - 'LI', - 'P', - 'LI', - 'P' - ]); - expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ - 'Option 1', - 'Option 2' - ]); - }); - - it('numbered lists if there is some content between lists', async () => { - element.markdown = '1. Option 1\n\nSome content in between lists\n\n 2. Option 2'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'OL', - 'LI', - 'P', - 'P', - 'OL', - 'LI', - 'P' - ]); - expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ - 'Option 1', - 'Some content in between lists', - 'Option 2' - ]); - }); - - it('bulleted list markdown string("*") to "ul" and "li" HTML tags', async () => { - element.markdown = '* Bulleted list'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'UL', - 'LI', - 'P' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'Bulleted list' - ); - }); - - it('bulleted list markdown string("-") to "ul" and "li" HTML tags', async () => { - element.markdown = '- Bulleted list'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'UL', - 'LI', - 'P' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'Bulleted list' - ); - }); - - it('bulleted list markdown string("+") to "ul" and "li" HTML tags', async () => { - element.markdown = '+ Bulleted list'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'UL', - 'LI', - 'P' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'Bulleted list' - ); - }); - - it('nested bullet lists markdown string to "ul" and "li" HTML tags', async () => { - element.markdown = '* Option 1\n\n * Option 2'; - - await connect(); - - expect( - pageObject.getRenderedMarkdownTagNamesWithClosingTags() - ).toEqual([ - 'UL', - 'LI', - 'P', - '/P', - 'UL', - 'LI', - 'P', - '/P', - '/LI', - '/UL', - '/LI', - '/UL' - ]); - expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ - 'Option 1', - 'Option 2' - ]); - }); - - it('bullet lists markdown string to "ul" tag and nested numbered list markdown string to "ol" tag', async () => { - element.markdown = '* Option 1\n\n 1. Option 2'; - - await connect(); - - expect( - pageObject.getRenderedMarkdownTagNamesWithClosingTags() - ).toEqual([ - 'UL', - 'LI', - 'P', - '/P', - 'OL', - 'LI', - 'P', - '/P', - '/LI', - '/OL', - '/LI', - '/UL' - ]); - expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ - 'Option 1', - 'Option 2' - ]); - }); - - it('sequential bullet and numbered lists should to "ul" and once "ul" tags are closed, should have "ol" tags', async () => { - element.markdown = '* Option 1\n\n1. Option 2'; - - await connect(); - - expect( - pageObject.getRenderedMarkdownTagNamesWithClosingTags() - ).toEqual([ - 'UL', - 'LI', - 'P', - '/P', - '/LI', - '/UL', - 'OL', - 'LI', - 'P', - '/P', - '/LI', - '/OL' - ]); - expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ - 'Option 1', - 'Option 2' - ]); - }); - - it('multiple bulleted lists markdown string("* \n* \n*") to "ul" and "li" HTML tags', async () => { - element.markdown = '* Option 1\n * Option 2\n * Option 3'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'UL', - 'LI', - 'P', - 'LI', - 'P', - 'LI', - 'P' - ]); - expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ - 'Option 1', - 'Option 2', - 'Option 3' - ]); - }); - - it('bulleted lists if there is some content between lists', async () => { - element.markdown = '* Option 1\n\nSome content in between lists\n\n * Option 2'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'UL', - 'LI', - 'P', - 'P', - 'UL', - 'LI', - 'P' - ]); - expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ - 'Option 1', - 'Some content in between lists', - 'Option 2' - ]); - }); - - it('direct link markdown string to "a" tags with the link as the text content', async () => { - element.markdown = ''; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'P', - 'A' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'https://nimble.ni.dev/' - ); - expect( - pageObject.getRenderedMarkdownLastChildAttribute('href') - ).toBe('https://nimble.ni.dev/'); - }); - - it('numbered list with bold markdown string to "ol", "li" and "strong" HTML tags', async () => { - element.markdown = '1. **Numbered list in bold**'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'OL', - 'LI', - 'P', - 'STRONG' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'Numbered list in bold' - ); - }); - - it('bulleted list with italics markdown string to "ul", "li" and "em" HTML tags', async () => { - element.markdown = '* *Bulleted list in italics*'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'UL', - 'LI', - 'P', - 'EM' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'Bulleted list in italics' - ); - }); - - it('bulleted list with direct links markdown string to "ul", "li" and "a" HTML tags', async () => { - element.markdown = '* '; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'UL', - 'LI', - 'P', - 'A' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'https://nimble.ni.dev/' - ); - expect( - pageObject.getRenderedMarkdownLastChildAttribute('href') - ).toBe('https://nimble.ni.dev/'); - }); - - it('direct links in bold markdown string to "strong" and "a" HTML tags', async () => { - element.markdown = '****'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'P', - 'STRONG', - 'A' - ]); - expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( - 'https://nimble.ni.dev/' - ); - expect( - pageObject.getRenderedMarkdownLastChildAttribute('href') - ).toBe('https://nimble.ni.dev/'); - }); - - it('combination of all supported markdown string', async () => { - element.markdown = '1. ***Numbered list with bold and italics***\n* ___Bulleted list with bold and italics___\n\n'; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'OL', - 'LI', - 'P', - 'EM', - 'STRONG', - 'UL', - 'LI', - 'P', - 'EM', - 'STRONG', - 'P', - 'A' - ]); - expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ - 'Numbered list with bold and italics', - 'Bulleted list with bold and italics', - 'https://nimble.ni.dev/' - ]); - }); - }); - - describe('various not supported markdown string values render as unchanged strings', () => { - const notSupportedMarkdownStrings: { name: string }[] = [ - { name: '> blockquote' }, - { name: '`code`' }, - { name: '```fence```' }, - { name: '~~Strikethrough~~' }, - { name: '# Heading 1' }, - { name: '## Heading 2' }, - { name: '### Heading 3' }, - { name: '[link](url)' }, - { name: '[ref][link] [link]:url' }, - { name: '![Text](Image)' }, - { name: ' ' }, - { name: '---' }, - { name: '***' }, - { name: '___' }, - { name: '(c) (C) (r) (R) (tm) (TM) (p) (P) +-' }, - { name: '

text

' }, - { name: 'not bold' }, - { name: 'not italic' }, - { name: '
  1. not list
  2. not list
' }, - { name: '
  • not list
  • not list
' }, - { - name: 'https://nimble.ni.dev/' - }, - { name: '' } - ]; - - const focused: string[] = []; - const disabled: string[] = []; - for (const value of notSupportedMarkdownStrings) { - const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func - specType( - `string "${value.name}" renders as plain text "${value.name}" within paragraph tag`, - // eslint-disable-next-line @typescript-eslint/no-loop-func - async () => { - element.markdown = value.name; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual([ - 'P' - ]); - expect( - pageObject.getRenderedMarkdownLastChildContents() - ).toBe(value.name); - - await disconnect(); - } - ); - } - }); - - describe('various wacky string values render as unchanged strings', () => { - const focused: string[] = []; - const disabled: string[] = []; - - wackyStrings - .filter(value => value.name !== '\x00') - .forEach(value => { - const specType = getSpecTypeByNamedList( - value, - focused, - disabled - ); - // eslint-disable-next-line @typescript-eslint/no-loop-func - specType( - `wacky string "${value.name}" that are unmodified when rendered the same "${value.name}" within paragraph tag`, - // eslint-disable-next-line @typescript-eslint/no-loop-func - async () => { - element.markdown = value.name; - - await connect(); - - expect( - pageObject.getRenderedMarkdownTagNames() - ).toEqual(['P']); - expect( - pageObject.getRenderedMarkdownLastChildContents() - ).toBe(value.name); - - await disconnect(); - } - ); - }); - }); - describe('various wacky string values modified when rendered', () => { const focused: string[] = []; const disabled: string[] = []; From 219941a00af07553019c42987f70ccb84d5586a0 Mon Sep 17 00:00:00 2001 From: Sai krishnan Perumal Date: Wed, 6 Sep 2023 15:28:27 +0530 Subject: [PATCH 22/56] chore: run format Signed-off-by: Sai krishnan Perumal --- .../models/tests/markdown-parser.spec.ts | 274 ++++++++++-------- .../viewer/tests/rich-text-viewer.spec.ts | 1 - 2 files changed, 146 insertions(+), 129 deletions(-) diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index 73ac008cbf..3b42e6ed79 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -16,7 +16,9 @@ describe('Markdown parser', () => { return nodes; } - function getLeafContentsFromDocumentFragment(doc: DocumentFragment): string[] { + function getLeafContentsFromDocumentFragment( + doc: DocumentFragment + ): string[] { const nodes = Array.from(doc.querySelectorAll('*')) .filter((el, _) => { return el.children.length === 0; @@ -28,92 +30,104 @@ describe('Markdown parser', () => { describe('supported rich text formatting options from markdown string to its respective HTML elements', () => { it('bold markdown string("**") to "strong" HTML tag', () => { const doc = markDownParser.parseMarkdownToDOM('**Bold**'); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['P', 'STRONG']); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual(['Bold']); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['P', 'STRONG']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Bold']); }); it('bold markdown string("__") to "strong" HTML tag', () => { const doc = markDownParser.parseMarkdownToDOM('__Bold__'); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['P', 'STRONG']); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual(['Bold']); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['P', 'STRONG']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Bold']); }); it('italics markdown string("*") to "em" HTML tag', () => { const doc = markDownParser.parseMarkdownToDOM('*Italics*'); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['P', 'EM']); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual(['Italics']); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['P', 'EM']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Italics']); }); it('italics markdown string("_") to "em" HTML tag', () => { const doc = markDownParser.parseMarkdownToDOM('_Italics_'); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['P', 'EM']); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual(['Italics']); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['P', 'EM']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Italics']); }); it('numbered list markdown string("1.") to "ol" and "li" HTML tags', () => { const doc = markDownParser.parseMarkdownToDOM('1. Numbered list'); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['OL', 'LI', 'P']); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'Numbered list' - ]); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['OL', 'LI', 'P']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Numbered list']); }); it('numbered list markdown string("1)") to "ol" and "li" HTML tags', () => { const doc = markDownParser.parseMarkdownToDOM('1) Numbered list'); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['OL', 'LI', 'P']); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'Numbered list' - ]); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['OL', 'LI', 'P']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Numbered list']); }); it('multiple numbered lists markdown string("1.\n2.") to "ol" and "li" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM('1. Option 1\n 2. Option 2'); + const doc = markDownParser.parseMarkdownToDOM( + '1. Option 1\n 2. Option 2' + ); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'OL', - 'LI', - 'P', - 'LI', - 'P' - ]); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'Option 1', - 'Option 2' - ]); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['OL', 'LI', 'P', 'LI', 'P']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Option 1', 'Option 2']); }); it('multiple empty numbered lists markdown string("1.\n2.") to "ol" and "li" HTML tags', () => { const doc = markDownParser.parseMarkdownToDOM('1. \n 2. '); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'OL', - 'LI', - 'P', - 'LI', - 'P' - ]); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual(['', '']); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['OL', 'LI', 'P', 'LI', 'P']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['', '']); }); it('numbered lists that start with numbers and are not sequential to "ol" and "li" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM('1. Option 1\n 1. Option 2'); + const doc = markDownParser.parseMarkdownToDOM( + '1. Option 1\n 1. Option 2' + ); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'OL', - 'LI', - 'P', - 'LI', - 'P' - ]); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'Option 1', - 'Option 2' - ]); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['OL', 'LI', 'P', 'LI', 'P']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Option 1', 'Option 2']); }); it('numbered lists if there is some content between lists', () => { @@ -121,16 +135,12 @@ describe('Markdown parser', () => { '1. Option 1\n\nSome content in between lists\n\n 2. Option 2' ); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'OL', - 'LI', - 'P', - 'P', - 'OL', - 'LI', - 'P' - ]); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['OL', 'LI', 'P', 'P', 'OL', 'LI', 'P']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual([ 'Option 1', 'Some content in between lists', 'Option 2' @@ -140,47 +150,47 @@ describe('Markdown parser', () => { it('bulleted list markdown string("*") to "ul" and "li" HTML tags', () => { const doc = markDownParser.parseMarkdownToDOM('* Bulleted list'); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['UL', 'LI', 'P']); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'Bulleted list' - ]); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['UL', 'LI', 'P']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Bulleted list']); }); it('bulleted list markdown string("-") to "ul" and "li" HTML tags', () => { const doc = markDownParser.parseMarkdownToDOM('- Bulleted list'); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['UL', 'LI', 'P']); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'Bulleted list' - ]); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['UL', 'LI', 'P']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Bulleted list']); }); it('bulleted list markdown string("+") to "ul" and "li" HTML tags', () => { const doc = markDownParser.parseMarkdownToDOM('+ Bulleted list'); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['UL', 'LI', 'P']); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'Bulleted list' - ]); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['UL', 'LI', 'P']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Bulleted list']); }); it('multiple bulleted lists markdown string("* \n* \n*") to "ul" and "li" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM('* Option 1\n * Option 2\n * Option 3'); + const doc = markDownParser.parseMarkdownToDOM( + '* Option 1\n * Option 2\n * Option 3' + ); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'UL', - 'LI', - 'P', - 'LI', - 'P', - 'LI', - 'P' - ]); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'Option 1', - 'Option 2', - 'Option 3' - ]); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['UL', 'LI', 'P', 'LI', 'P', 'LI', 'P']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Option 1', 'Option 2', 'Option 3']); }); it('bulleted lists if there is some content between lists', () => { @@ -188,16 +198,12 @@ describe('Markdown parser', () => { '* Option 1\n\nSome content in between lists\n\n * Option 2' ); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'UL', - 'LI', - 'P', - 'P', - 'UL', - 'LI', - 'P' - ]); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['UL', 'LI', 'P', 'P', 'UL', 'LI', 'P']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual([ 'Option 1', 'Some content in between lists', 'Option 2' @@ -205,31 +211,29 @@ describe('Markdown parser', () => { }); it('numbered list with bold markdown string to "ol", "li" and "strong" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM('1. **Numbered list in bold**'); + const doc = markDownParser.parseMarkdownToDOM( + '1. **Numbered list in bold**' + ); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'OL', - 'LI', - 'P', - 'STRONG' - ]); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'Numbered list in bold' - ]); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['OL', 'LI', 'P', 'STRONG']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Numbered list in bold']); }); it('bulleted list with italics markdown string to "ul", "li" and "em" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM('* *Bulleted list in italics*'); + const doc = markDownParser.parseMarkdownToDOM( + '* *Bulleted list in italics*' + ); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'UL', - 'LI', - 'P', - 'EM' - ]); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - 'Bulleted list in italics' - ]); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['UL', 'LI', 'P', 'EM']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['Bulleted list in italics']); }); it('combination of all supported markdown string', () => { @@ -237,7 +241,9 @@ describe('Markdown parser', () => { '1. ***Numbered list with bold and italics***\n* ___Bulleted list with bold and italics___' ); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual([ 'OL', 'LI', 'P', @@ -249,7 +255,9 @@ describe('Markdown parser', () => { 'EM', 'STRONG' ]); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual([ 'Numbered list with bold and italics', 'Bulleted list with bold and italics' ]); @@ -294,10 +302,14 @@ describe('Markdown parser', () => { () => { const doc = markDownParser.parseMarkdownToDOM(value.name); - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['P']); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ - value.name - ]); + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['P']); + expect( + getLeafContentsFromDocumentFragment( + doc as DocumentFragment + ) + ).toEqual([value.name]); } ); } @@ -319,14 +331,20 @@ describe('Markdown parser', () => { `wacky string "${value.name}" that are unmodified when set the same "${value.name}" within paragraph tag`, // eslint-disable-next-line @typescript-eslint/no-loop-func () => { - const doc = markDownParser.parseMarkdownToDOM(value.name); - - expect(getTagsFromDocumentFragment(doc as DocumentFragment)).toEqual(['P']); - expect(getLeafContentsFromDocumentFragment(doc as DocumentFragment)).toEqual([ + const doc = markDownParser.parseMarkdownToDOM( value.name - ]); + ); + + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['P']); + expect( + getLeafContentsFromDocumentFragment( + doc as DocumentFragment + ) + ).toEqual([value.name]); } ); }); }); -}); \ No newline at end of file +}); diff --git a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts index 845407faf5..e5f8cc788b 100644 --- a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts +++ b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts @@ -2,7 +2,6 @@ import { html } from '@microsoft/fast-element'; import { RichTextViewer, richTextViewerTag } from '..'; import { fixture, type Fixture } from '../../../utilities/tests/fixture'; import { RichTextViewerPageObject } from '../testing/rich-text-viewer.pageobject'; -import { wackyStrings } from '../../../utilities/tests/wacky-strings'; import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; async function setup(): Promise> { From 5d8087823ff53ab578330197d2b1a1e5f018a821 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 7 Sep 2023 00:46:10 +0530 Subject: [PATCH 23/56] Added spec file for serializer and updated static classes for model files --- .../src/rich-text/editor/index.ts | 6 +- .../editor/tests/rich-text-editor.spec.ts | 35 ------ .../src/rich-text/models/markdown-parser.ts | 13 +-- .../rich-text/models/markdown-serializer.ts | 6 +- .../models/tests/markdown-parser.spec.ts | 84 ++++++++++----- .../models/tests/markdown-serializer.spec.ts | 100 ++++++++++++++++++ .../src/rich-text/viewer/index.ts | 4 +- .../viewer/tests/rich-text-viewer.spec.ts | 39 ------- 8 files changed, 168 insertions(+), 119 deletions(-) create mode 100644 packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index 8b8006e957..cfec597676 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -146,8 +146,6 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { private resizeObserver?: ResizeObserver; private updateScrollbarWidthQueued = false; - private readonly markdownParser = new RichTextMarkdownParser(); - private readonly markdownSerializer = new RichTextMarkdownSerializer(); private readonly xmlSerializer = new XMLSerializer(); /** @@ -315,7 +313,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { * @public */ public getMarkdown(): string { - return this.markdownSerializer.serializeDOMToMarkdown( + return RichTextMarkdownSerializer.serializeDOMToMarkdown( this.tiptapEditor.state.doc ); } @@ -369,7 +367,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { * This function takes the Fragment from parseMarkdownToDOM function and return the serialized string using XMLSerializer */ private getHtmlContent(markdown: string): string { - const documentFragment = this.markdownParser.parseMarkdownToDOM(markdown); + const documentFragment = RichTextMarkdownParser.parseMarkdownToDOM(markdown); return this.xmlSerializer.serializeToString(documentFragment); } diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index 9e663d3ff2..0877f829d8 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -670,41 +670,6 @@ describe('RichTextEditor', () => { ]); }); - describe('various wacky string values modified when rendered', () => { - const focused: string[] = []; - const disabled: string[] = []; - const modifiedWackyStrings: { - name: string, - tags: string[], - textContent: string[] - }[] = [ - { name: '\0', tags: ['P'], textContent: ['�'] }, - { name: '\uFFFD', tags: ['P'], textContent: ['�'] }, - { name: '\x00', tags: ['P'], textContent: ['�'] }, - { name: '\r\r', tags: ['P', 'BR'], textContent: [''] } - ]; - - for (const value of modifiedWackyStrings) { - const specType = getSpecTypeByNamedList(value, focused, disabled); - specType( - `wacky string "${value.name}" modified when rendered`, - // eslint-disable-next-line @typescript-eslint/no-loop-func - async () => { - element.setMarkdown(value.name); - - await connect(); - - expect(pageObject.getEditorTagNames()).toEqual(value.tags); - expect(pageObject.getEditorLeafContents()).toEqual( - value.textContent - ); - - await disconnect(); - } - ); - } - }); - it('Should return a empty string when empty string is assigned', () => { element.setMarkdown('markdown string'); expect(element.getMarkdown()).toBe('markdown string'); diff --git a/packages/nimble-components/src/rich-text/models/markdown-parser.ts b/packages/nimble-components/src/rich-text/models/markdown-parser.ts index 9a1ae9a2c4..01f71708b5 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-parser.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-parser.ts @@ -9,20 +9,15 @@ import { DOMSerializer } from 'prosemirror-model'; * Provides markdown parser for rich text components */ export class RichTextMarkdownParser { - private readonly markdownParser: MarkdownParser; - private readonly domSerializer: DOMSerializer; - - public constructor() { - this.markdownParser = this.initializeMarkdownParser(); - this.domSerializer = DOMSerializer.fromSchema(schema); - } + private static readonly markdownParser = this.initializeMarkdownParser(); + private static readonly domSerializer = DOMSerializer.fromSchema(schema); /** * This function takes a markdown string, parses it using the ProseMirror MarkdownParser, serializes the parsed content into a * DOM structure using a DOMSerializer, and returns the serialized result. * If the markdown parser returns null, it will clear the viewer component by creating an empty document fragment. */ - public parseMarkdownToDOM(value: string): HTMLElement | DocumentFragment { + public static parseMarkdownToDOM(value: string): HTMLElement | DocumentFragment { const parsedMarkdownContent = this.markdownParser.parse(value); if (parsedMarkdownContent === null) { return document.createDocumentFragment(); @@ -32,7 +27,7 @@ export class RichTextMarkdownParser { ); } - private initializeMarkdownParser(): MarkdownParser { + private static 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. diff --git a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts index 48b5f65557..cad1e14484 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts @@ -9,13 +9,13 @@ import type { Node } from 'prosemirror-model'; * Provides markdown serializer for rich text components */ export class RichTextMarkdownSerializer { - private readonly markdownSerializer = this.initializeMarkdownSerializerForTipTap(); + private static readonly markdownSerializer = this.initializeMarkdownSerializerForTipTap(); - public serializeDOMToMarkdown(doc: Node): string { + public static serializeDOMToMarkdown(doc: Node): string { return this.markdownSerializer.serialize(doc); } - private initializeMarkdownSerializerForTipTap(): MarkdownSerializer { + private static initializeMarkdownSerializerForTipTap(): 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) diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index 3b42e6ed79..f8a3f95a55 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -3,12 +3,6 @@ import { wackyStrings } from '../../../utilities/tests/wacky-strings'; import { RichTextMarkdownParser } from '../markdown-parser'; describe('Markdown parser', () => { - let markDownParser: RichTextMarkdownParser = new RichTextMarkdownParser(); - - beforeEach(() => { - markDownParser = new RichTextMarkdownParser(); - }); - function getTagsFromDocumentFragment(doc: DocumentFragment): string[] { const nodes = Array.from(doc.querySelectorAll('*')).map( el => el.tagName @@ -29,7 +23,7 @@ describe('Markdown parser', () => { describe('supported rich text formatting options from markdown string to its respective HTML elements', () => { it('bold markdown string("**") to "strong" HTML tag', () => { - const doc = markDownParser.parseMarkdownToDOM('**Bold**'); + const doc = RichTextMarkdownParser.parseMarkdownToDOM('**Bold**'); expect( getTagsFromDocumentFragment(doc as DocumentFragment) ).toEqual(['P', 'STRONG']); @@ -39,7 +33,7 @@ describe('Markdown parser', () => { }); it('bold markdown string("__") to "strong" HTML tag', () => { - const doc = markDownParser.parseMarkdownToDOM('__Bold__'); + const doc = RichTextMarkdownParser.parseMarkdownToDOM('__Bold__'); expect( getTagsFromDocumentFragment(doc as DocumentFragment) @@ -50,7 +44,7 @@ describe('Markdown parser', () => { }); it('italics markdown string("*") to "em" HTML tag', () => { - const doc = markDownParser.parseMarkdownToDOM('*Italics*'); + const doc = RichTextMarkdownParser.parseMarkdownToDOM('*Italics*'); expect( getTagsFromDocumentFragment(doc as DocumentFragment) @@ -61,7 +55,7 @@ describe('Markdown parser', () => { }); it('italics markdown string("_") to "em" HTML tag', () => { - const doc = markDownParser.parseMarkdownToDOM('_Italics_'); + const doc = RichTextMarkdownParser.parseMarkdownToDOM('_Italics_'); expect( getTagsFromDocumentFragment(doc as DocumentFragment) @@ -72,7 +66,7 @@ describe('Markdown parser', () => { }); it('numbered list markdown string("1.") to "ol" and "li" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM('1. Numbered list'); + const doc = RichTextMarkdownParser.parseMarkdownToDOM('1. Numbered list'); expect( getTagsFromDocumentFragment(doc as DocumentFragment) @@ -83,7 +77,7 @@ describe('Markdown parser', () => { }); it('numbered list markdown string("1)") to "ol" and "li" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM('1) Numbered list'); + const doc = RichTextMarkdownParser.parseMarkdownToDOM('1) Numbered list'); expect( getTagsFromDocumentFragment(doc as DocumentFragment) @@ -94,7 +88,7 @@ describe('Markdown parser', () => { }); it('multiple numbered lists markdown string("1.\n2.") to "ol" and "li" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM( + const doc = RichTextMarkdownParser.parseMarkdownToDOM( '1. Option 1\n 2. Option 2' ); @@ -107,7 +101,7 @@ describe('Markdown parser', () => { }); it('multiple empty numbered lists markdown string("1.\n2.") to "ol" and "li" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM('1. \n 2. '); + const doc = RichTextMarkdownParser.parseMarkdownToDOM('1. \n 2. '); expect( getTagsFromDocumentFragment(doc as DocumentFragment) @@ -118,7 +112,7 @@ describe('Markdown parser', () => { }); it('numbered lists that start with numbers and are not sequential to "ol" and "li" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM( + const doc = RichTextMarkdownParser.parseMarkdownToDOM( '1. Option 1\n 1. Option 2' ); @@ -131,7 +125,7 @@ describe('Markdown parser', () => { }); it('numbered lists if there is some content between lists', () => { - const doc = markDownParser.parseMarkdownToDOM( + const doc = RichTextMarkdownParser.parseMarkdownToDOM( '1. Option 1\n\nSome content in between lists\n\n 2. Option 2' ); @@ -148,7 +142,7 @@ describe('Markdown parser', () => { }); it('bulleted list markdown string("*") to "ul" and "li" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM('* Bulleted list'); + const doc = RichTextMarkdownParser.parseMarkdownToDOM('* Bulleted list'); expect( getTagsFromDocumentFragment(doc as DocumentFragment) @@ -159,7 +153,7 @@ describe('Markdown parser', () => { }); it('bulleted list markdown string("-") to "ul" and "li" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM('- Bulleted list'); + const doc = RichTextMarkdownParser.parseMarkdownToDOM('- Bulleted list'); expect( getTagsFromDocumentFragment(doc as DocumentFragment) @@ -170,7 +164,7 @@ describe('Markdown parser', () => { }); it('bulleted list markdown string("+") to "ul" and "li" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM('+ Bulleted list'); + const doc = RichTextMarkdownParser.parseMarkdownToDOM('+ Bulleted list'); expect( getTagsFromDocumentFragment(doc as DocumentFragment) @@ -181,7 +175,7 @@ describe('Markdown parser', () => { }); it('multiple bulleted lists markdown string("* \n* \n*") to "ul" and "li" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM( + const doc = RichTextMarkdownParser.parseMarkdownToDOM( '* Option 1\n * Option 2\n * Option 3' ); @@ -194,7 +188,7 @@ describe('Markdown parser', () => { }); it('bulleted lists if there is some content between lists', () => { - const doc = markDownParser.parseMarkdownToDOM( + const doc = RichTextMarkdownParser.parseMarkdownToDOM( '* Option 1\n\nSome content in between lists\n\n * Option 2' ); @@ -211,7 +205,7 @@ describe('Markdown parser', () => { }); it('numbered list with bold markdown string to "ol", "li" and "strong" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM( + const doc = RichTextMarkdownParser.parseMarkdownToDOM( '1. **Numbered list in bold**' ); @@ -224,7 +218,7 @@ describe('Markdown parser', () => { }); it('bulleted list with italics markdown string to "ul", "li" and "em" HTML tags', () => { - const doc = markDownParser.parseMarkdownToDOM( + const doc = RichTextMarkdownParser.parseMarkdownToDOM( '* *Bulleted list in italics*' ); @@ -237,7 +231,7 @@ describe('Markdown parser', () => { }); it('combination of all supported markdown string', () => { - const doc = markDownParser.parseMarkdownToDOM( + const doc = RichTextMarkdownParser.parseMarkdownToDOM( '1. ***Numbered list with bold and italics***\n* ___Bulleted list with bold and italics___' ); @@ -300,7 +294,7 @@ describe('Markdown parser', () => { `string "${value.name}" renders as plain text "${value.name}" within paragraph tag`, // eslint-disable-next-line @typescript-eslint/no-loop-func () => { - const doc = markDownParser.parseMarkdownToDOM(value.name); + const doc = RichTextMarkdownParser.parseMarkdownToDOM(value.name); expect( getTagsFromDocumentFragment(doc as DocumentFragment) @@ -331,7 +325,7 @@ describe('Markdown parser', () => { `wacky string "${value.name}" that are unmodified when set the same "${value.name}" within paragraph tag`, // eslint-disable-next-line @typescript-eslint/no-loop-func () => { - const doc = markDownParser.parseMarkdownToDOM( + const doc = RichTextMarkdownParser.parseMarkdownToDOM( value.name ); @@ -347,4 +341,42 @@ describe('Markdown parser', () => { ); }); }); + + describe('various wacky string values modified when rendered', () => { + const focused: string[] = []; + const disabled: string[] = []; + const modifiedWackyStrings: { + name: string, + tags: string[], + textContent: string[] + }[] = [ + { name: '\0', tags: ['P'], textContent: ['�'] }, + { name: '\r\r', tags: ['P'], textContent: [''] }, + { name: '\uFFFD', tags: ['P'], textContent: ['�'] }, + { name: '\x00', tags: ['P'], textContent: ['�'] } + ]; + + for (const value of modifiedWackyStrings) { + const specType = getSpecTypeByNamedList(value, focused, disabled); + // eslint-disable-next-line @typescript-eslint/no-loop-func + specType( + `wacky string "${value.name}" modified when rendered`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + value.name + ); + + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(value.tags); + expect( + getLeafContentsFromDocumentFragment( + doc as DocumentFragment + ) + ).toEqual(value.textContent); + } + ); + } + }); }); diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts new file mode 100644 index 0000000000..c1514d6a8e --- /dev/null +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts @@ -0,0 +1,100 @@ +import { Editor } from '@tiptap/core'; +import Bold from '@tiptap/extension-bold'; +import BulletList from '@tiptap/extension-bullet-list'; +import Document from '@tiptap/extension-document'; +import Italic from '@tiptap/extension-italic'; +import ListItem from '@tiptap/extension-list-item'; +import OrderedList from '@tiptap/extension-ordered-list'; +import Paragraph from '@tiptap/extension-paragraph'; +import Text from '@tiptap/extension-text'; +import type { Node } from 'prosemirror-model'; +import { RichTextMarkdownSerializer } from '../markdown-serializer'; +import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; + +describe('Markdown serializer', () => { + const mockEditor = new Editor({ + element: document.createElement('div'), + extensions: [ + Document, + Paragraph, + Text, + BulletList, + OrderedList, + ListItem, + Bold, + Italic + ] + }); + + function getNode(htmlString: string): Node { + mockEditor.commands.setContent(htmlString); + return mockEditor.state.doc; + } + + afterAll(() => { + mockEditor.destroy(); + }); + + describe('various supported nodes should be serialized to a markdown output', () => { + const supportedNodesMarks: { name: string, html: string, markdown: string }[] = [ + { name: 'Bold', html: 'Bold', markdown: '**Bold**' }, + { name: 'Italics', html: 'Italics', markdown: '*Italics*' }, + { name: 'Bold and Italics', html: 'Bold and Italics', markdown: '***Bold and Italics***' }, + { name: 'Numbered list', html: '
  1. Numbered list

', markdown: '1. Numbered list' }, + { name: 'Multiple numbered list', html: '
  1. list 1

  2. list 2

', markdown: '1. list 1\n\n2. list 2' }, + { name: 'Numbered list with bold', html: '
  1. Numbered list with bold

', markdown: '1. **Numbered list with bold**' }, + { name: 'Numbered list with italics', html: '
  1. Numbered list with italics

', markdown: '1. *Numbered list with italics*' }, + { name: 'Bullet list', html: '
  • Bullet list

', markdown: '* Bullet list' }, + { name: 'Multiple Bullet list', html: '
  • list 1

  • list 2

', markdown: '* list 1\n\n* list 2' }, + { name: 'Bullet list with bold', html: '
  • Bullet list with bold

', markdown: '* **Bullet list with bold**' }, + { name: 'Bullet list with italics', html: '
  • Bullet list with italics

', markdown: '* *Bullet list with italics*' } + ]; + + const focused: string[] = []; + const disabled: string[] = []; + for (const value of supportedNodesMarks) { + const specType = getSpecTypeByNamedList(value, focused, disabled); + specType( + `Should return ${value.name} markdown (${value.markdown}) when its respective node is passed`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + const node = getNode(value.html); + + expect(RichTextMarkdownSerializer.serializeDOMToMarkdown(node)).toBe(value.markdown); + } + ); + } + }); + + describe('various not supported nodes should be serialized to a plain text', () => { + const notSupportedNodesMarks: { name: string, html: string, plainText: string }[] = [ + { name: 'Blockquote', html: '

Blockquote

', plainText: 'Blockquote' }, + { name: 'Code', html: 'Code', plainText: 'Code' }, + { name: 'CodeBlock', html: '
CodeBlock
', plainText: 'CodeBlock' }, + { name: 'Heading', html: '

Heading

', plainText: 'Heading' }, + { name: 'HardBreak', html: '

Hard
Break

', plainText: 'Hard Break' }, + { name: 'HorizontalRule', html: '

Horizontal


Rule

', plainText: 'Horizontal\n\nRule' }, + { name: 'Highlight', html: 'Highlight', plainText: 'Highlight' }, + { name: 'Link', html: 'Link', plainText: 'Link' }, + { name: 'Strikethrough', html: 'Strikethrough', plainText: 'Strikethrough' }, + { name: 'Subscript', html: 'Subscript', plainText: 'Subscript' }, + { name: 'Span', html: 'Span', plainText: 'Span' }, + { name: 'Underline', html: 'Underline', plainText: 'Underline' } + ]; + + const focused: string[] = []; + const disabled: string[] = []; + for (const value of notSupportedNodesMarks) { + const specType = getSpecTypeByNamedList(value, focused, disabled); + specType( + `Should return exact node when not supported markdown (${value.name}) is passed`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + const node = getNode(value.html); + + expect(RichTextMarkdownSerializer.serializeDOMToMarkdown(node)).toBe(value.plainText); + } + ); + } + }); +}); diff --git a/packages/nimble-components/src/rich-text/viewer/index.ts b/packages/nimble-components/src/rich-text/viewer/index.ts index 6d0b86dc2d..9c809e13ae 100644 --- a/packages/nimble-components/src/rich-text/viewer/index.ts +++ b/packages/nimble-components/src/rich-text/viewer/index.ts @@ -27,8 +27,6 @@ export class RichTextViewer extends FoundationElement { */ public viewer!: HTMLDivElement; - private readonly markdownParser = new RichTextMarkdownParser(); - /** * @internal */ @@ -48,7 +46,7 @@ export class RichTextViewer extends FoundationElement { private updateView(): void { if (this.markdown) { - const serializedContent = this.markdownParser.parseMarkdownToDOM( + const serializedContent = RichTextMarkdownParser.parseMarkdownToDOM( this.markdown ); this.viewer.replaceChildren(serializedContent); diff --git a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts index e5f8cc788b..763a1265ba 100644 --- a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts +++ b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.spec.ts @@ -2,7 +2,6 @@ import { html } from '@microsoft/fast-element'; import { RichTextViewer, richTextViewerTag } from '..'; import { fixture, type Fixture } from '../../../utilities/tests/fixture'; import { RichTextViewerPageObject } from '../testing/rich-text-viewer.pageobject'; -import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; async function setup(): Promise> { return fixture( @@ -82,42 +81,4 @@ describe('RichTextViewer', () => { await disconnect(); }); - - describe('various wacky string values modified when rendered', () => { - const focused: string[] = []; - const disabled: string[] = []; - const modifiedWackyStrings: { - name: string, - tags: string[], - textContent: string[] - }[] = [ - { name: '\0', tags: ['P'], textContent: ['�'] }, - { name: '\r\r', tags: ['P'], textContent: [''] }, - { name: '\uFFFD', tags: ['P'], textContent: ['�'] }, - { name: '\x00', tags: ['P'], textContent: ['�'] } - ]; - - for (const value of modifiedWackyStrings) { - const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func - specType( - `wacky string "${value.name}" modified when rendered`, - // eslint-disable-next-line @typescript-eslint/no-loop-func - async () => { - element.markdown = value.name; - - await connect(); - - expect(pageObject.getRenderedMarkdownTagNames()).toEqual( - value.tags - ); - expect( - pageObject.getRenderedMarkdownLeafContents() - ).toEqual(value.textContent); - - await disconnect(); - } - ); - } - }); }); From 3a67d52ee3adc0c5fcbfe2fe54d85dcd31e8dade Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 7 Sep 2023 09:38:57 +0530 Subject: [PATCH 24/56] Fix lint --- .../src/rich-text/models/markdown-parser.ts | 4 +- .../models/tests/markdown-parser.spec.ts | 4 +- .../models/tests/markdown-serializer.spec.ts | 134 +++++++++++++++--- 3 files changed, 117 insertions(+), 25 deletions(-) diff --git a/packages/nimble-components/src/rich-text/models/markdown-parser.ts b/packages/nimble-components/src/rich-text/models/markdown-parser.ts index 01f71708b5..e37f4045e0 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-parser.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-parser.ts @@ -17,7 +17,9 @@ export class RichTextMarkdownParser { * DOM structure using a DOMSerializer, and returns the serialized result. * If the markdown parser returns null, it will clear the viewer component by creating an empty document fragment. */ - public static parseMarkdownToDOM(value: string): HTMLElement | DocumentFragment { + public static parseMarkdownToDOM( + value: string + ): HTMLElement | DocumentFragment { const parsedMarkdownContent = this.markdownParser.parse(value); if (parsedMarkdownContent === null) { return document.createDocumentFragment(); diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index f8a3f95a55..fa3f274150 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -294,7 +294,9 @@ describe('Markdown parser', () => { `string "${value.name}" renders as plain text "${value.name}" within paragraph tag`, // eslint-disable-next-line @typescript-eslint/no-loop-func () => { - const doc = RichTextMarkdownParser.parseMarkdownToDOM(value.name); + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + value.name + ); expect( getTagsFromDocumentFragment(doc as DocumentFragment) diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts index c1514d6a8e..161460836d 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts @@ -36,18 +36,66 @@ describe('Markdown serializer', () => { }); describe('various supported nodes should be serialized to a markdown output', () => { - const supportedNodesMarks: { name: string, html: string, markdown: string }[] = [ - { name: 'Bold', html: 'Bold', markdown: '**Bold**' }, - { name: 'Italics', html: 'Italics', markdown: '*Italics*' }, - { name: 'Bold and Italics', html: 'Bold and Italics', markdown: '***Bold and Italics***' }, - { name: 'Numbered list', html: '
  1. Numbered list

', markdown: '1. Numbered list' }, - { name: 'Multiple numbered list', html: '
  1. list 1

  2. list 2

', markdown: '1. list 1\n\n2. list 2' }, - { name: 'Numbered list with bold', html: '
  1. Numbered list with bold

', markdown: '1. **Numbered list with bold**' }, - { name: 'Numbered list with italics', html: '
  1. Numbered list with italics

', markdown: '1. *Numbered list with italics*' }, - { name: 'Bullet list', html: '
  • Bullet list

', markdown: '* Bullet list' }, - { name: 'Multiple Bullet list', html: '
  • list 1

  • list 2

', markdown: '* list 1\n\n* list 2' }, - { name: 'Bullet list with bold', html: '
  • Bullet list with bold

', markdown: '* **Bullet list with bold**' }, - { name: 'Bullet list with italics', html: '
  • Bullet list with italics

', markdown: '* *Bullet list with italics*' } + const supportedNodesMarks: { + name: string, + html: string, + markdown: string + }[] = [ + { + name: 'Bold', + html: 'Bold', + markdown: '**Bold**' + }, + { + name: 'Italics', + html: 'Italics', + markdown: '*Italics*' + }, + { + name: 'Bold and Italics', + html: 'Bold and Italics', + markdown: '***Bold and Italics***' + }, + { + name: 'Numbered list', + html: '
  1. Numbered list

', + markdown: '1. Numbered list' + }, + { + name: 'Multiple numbered list', + html: '
  1. list 1

  2. list 2

', + markdown: '1. list 1\n\n2. list 2' + }, + { + name: 'Numbered list with bold', + html: '
  1. Numbered list with bold

', + markdown: '1. **Numbered list with bold**' + }, + { + name: 'Numbered list with italics', + html: '
  1. Numbered list with italics

', + markdown: '1. *Numbered list with italics*' + }, + { + name: 'Bullet list', + html: '
  • Bullet list

', + markdown: '* Bullet list' + }, + { + name: 'Multiple Bullet list', + html: '
  • list 1

  • list 2

', + markdown: '* list 1\n\n* list 2' + }, + { + name: 'Bullet list with bold', + html: '
  • Bullet list with bold

', + markdown: '* **Bullet list with bold**' + }, + { + name: 'Bullet list with italics', + html: '
  • Bullet list with italics

', + markdown: '* *Bullet list with italics*' + } ]; const focused: string[] = []; @@ -60,26 +108,64 @@ describe('Markdown serializer', () => { () => { const node = getNode(value.html); - expect(RichTextMarkdownSerializer.serializeDOMToMarkdown(node)).toBe(value.markdown); + expect( + RichTextMarkdownSerializer.serializeDOMToMarkdown(node) + ).toBe(value.markdown); } ); } }); describe('various not supported nodes should be serialized to a plain text', () => { - const notSupportedNodesMarks: { name: string, html: string, plainText: string }[] = [ - { name: 'Blockquote', html: '

Blockquote

', plainText: 'Blockquote' }, + const notSupportedNodesMarks: { + name: string, + html: string, + plainText: string + }[] = [ + { + name: 'Blockquote', + html: '

Blockquote

', + plainText: 'Blockquote' + }, { name: 'Code', html: 'Code', plainText: 'Code' }, - { name: 'CodeBlock', html: '
CodeBlock
', plainText: 'CodeBlock' }, + { + name: 'CodeBlock', + html: '
CodeBlock
', + plainText: 'CodeBlock' + }, { name: 'Heading', html: '

Heading

', plainText: 'Heading' }, - { name: 'HardBreak', html: '

Hard
Break

', plainText: 'Hard Break' }, - { name: 'HorizontalRule', html: '

Horizontal


Rule

', plainText: 'Horizontal\n\nRule' }, - { name: 'Highlight', html: 'Highlight', plainText: 'Highlight' }, + { + name: 'HardBreak', + html: '

Hard
Break

', + plainText: 'Hard Break' + }, + { + name: 'HorizontalRule', + html: '

Horizontal


Rule

', + plainText: 'Horizontal\n\nRule' + }, + { + name: 'Highlight', + html: 'Highlight', + plainText: 'Highlight' + }, { name: 'Link', html: 'Link', plainText: 'Link' }, - { name: 'Strikethrough', html: 'Strikethrough', plainText: 'Strikethrough' }, - { name: 'Subscript', html: 'Subscript', plainText: 'Subscript' }, + { + name: 'Strikethrough', + html: 'Strikethrough', + plainText: 'Strikethrough' + }, + { + name: 'Subscript', + html: 'Subscript', + plainText: 'Subscript' + }, { name: 'Span', html: 'Span', plainText: 'Span' }, - { name: 'Underline', html: 'Underline', plainText: 'Underline' } + { + name: 'Underline', + html: 'Underline', + plainText: 'Underline' + } ]; const focused: string[] = []; @@ -92,7 +178,9 @@ describe('Markdown serializer', () => { () => { const node = getNode(value.html); - expect(RichTextMarkdownSerializer.serializeDOMToMarkdown(node)).toBe(value.plainText); + expect( + RichTextMarkdownSerializer.serializeDOMToMarkdown(node) + ).toBe(value.plainText); } ); } From 5beb94009c19ff4cf0787a6b24d3f9ecc89d5c9b Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 7 Sep 2023 09:40:48 +0530 Subject: [PATCH 25/56] Removed previous branch change files --- ...imble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json | 7 ------- ...le-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json | 7 ------- 2 files changed, 14 deletions(-) delete mode 100644 change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json delete mode 100644 change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json diff --git a/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json b/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json deleted file mode 100644 index 5681985229..0000000000 --- a/change/@ni-nimble-angular-3a7f08ef-dd7e-4bae-8b30-937d3830d563.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "patch", - "comment": "Revamp folder structure for rich text components", - "packageName": "@ni/nimble-angular", - "email": "123377523+vivinkrishna-ni@users.noreply.github.com", - "dependentChangeType": "patch" -} diff --git a/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json b/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json deleted file mode 100644 index a6bf4a46e8..0000000000 --- a/change/@ni-nimble-components-c9aef77f-1a41-4240-b114-67fc1d5c6c2f.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "patch", - "comment": "Revamp folder structure for rich text components", - "packageName": "@ni/nimble-components", - "email": "123377523+vivinkrishna-ni@users.noreply.github.com", - "dependentChangeType": "patch" -} From a6490b95b165de4dd8e5d3fa97f7af8f8899ae0b Mon Sep 17 00:00:00 2001 From: Sai krishnan Perumal Date: Thu, 7 Sep 2023 10:16:20 +0530 Subject: [PATCH 26/56] Change files --- ...le-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@ni-nimble-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json diff --git a/change/@ni-nimble-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json b/change/@ni-nimble-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json new file mode 100644 index 0000000000..0022132e06 --- /dev/null +++ b/change/@ni-nimble-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "Refactored rich text components to use common models", + "packageName": "@ni/nimble-components", + "email": "sai.krishnan.perumal@ni.com", + "dependentChangeType": "none" +} From 7664716b368b9717746965b8252b12e732a53788 Mon Sep 17 00:00:00 2001 From: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> Date: Thu, 7 Sep 2023 12:27:07 +0530 Subject: [PATCH 27/56] refactor: update email id in change file Signed-off-by: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> --- ...-nimble-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/change/@ni-nimble-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json b/change/@ni-nimble-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json index 0022132e06..7fbe260da7 100644 --- a/change/@ni-nimble-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json +++ b/change/@ni-nimble-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json @@ -2,6 +2,6 @@ "type": "none", "comment": "Refactored rich text components to use common models", "packageName": "@ni/nimble-components", - "email": "sai.krishnan.perumal@ni.com", + "email": "123591928+saikrishnan-ni@users.noreply.github.com", "dependentChangeType": "none" } From f674ddf0e43f443740b3af9143da19fdf530a125 Mon Sep 17 00:00:00 2001 From: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> Date: Thu, 7 Sep 2023 13:19:58 +0530 Subject: [PATCH 28/56] refactor: remove redundant eslint disable Signed-off-by: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> --- .../src/rich-text/models/tests/markdown-parser.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index fa3f274150..72613e6b45 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -360,7 +360,6 @@ describe('Markdown parser', () => { for (const value of modifiedWackyStrings) { const specType = getSpecTypeByNamedList(value, focused, disabled); - // eslint-disable-next-line @typescript-eslint/no-loop-func specType( `wacky string "${value.name}" modified when rendered`, // eslint-disable-next-line @typescript-eslint/no-loop-func From 6109d1f0534439067a7f1c043603e37375c3eb6f Mon Sep 17 00:00:00 2001 From: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> Date: Thu, 7 Sep 2023 16:29:26 +0530 Subject: [PATCH 29/56] refactor: add in review suggestion test cases Signed-off-by: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> --- .../models/tests/markdown-serializer.spec.ts | 92 ++++++++++++++++--- 1 file changed, 81 insertions(+), 11 deletions(-) diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts index 161460836d..3ef28c8a56 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts @@ -77,25 +77,85 @@ describe('Markdown serializer', () => { markdown: '1. *Numbered list with italics*' }, { - name: 'Bullet list', - html: '
  • Bullet list

', - markdown: '* Bullet list' + name: 'Bulleted list', + html: '
  • Bulleted list

', + markdown: '* Bulleted list' }, { - name: 'Multiple Bullet list', + name: 'Multiple Bulleted list', html: '
  • list 1

  • list 2

', markdown: '* list 1\n\n* list 2' }, { - name: 'Bullet list with bold', - html: '
  • Bullet list with bold

', - markdown: '* **Bullet list with bold**' + name: 'Bulleted list with bold', + html: '
  • Bulleted list with bold

', + markdown: '* **Bulleted list with bold**' }, { - name: 'Bullet list with italics', - html: '
  • Bullet list with italics

', - markdown: '* *Bullet list with italics*' - } + name: 'Bulleted list with italics', + html: '
  • Bulleted list with italics

', + markdown: '* *Bulleted list with italics*' + }, + { + name: 'Nested list with levels 1 - Bulleted list, 2 - Numbered list (Bold)', + html: '
  • Bulleted list

    1. Nested bold numbered list

', + markdown: '* Bulleted list\n\n 1. **Nested bold numbered list**' + }, + { + name: 'Nested list with levels 1 - Bulleted list, 2 - Numbered list (Italics)', + html: '
  • Bulleted list

    1. Nested bold numbered list

', + markdown: '* Bulleted list\n\n 1. *Nested bold numbered list*' + }, + { + name: 'Nested list with levels 1- Numbered list (Bold), 2-Bulleted list', + html: '
  1. Numbered list bold

    • Nested bulleted list

', + markdown: '1. **Numbered list bold**\n\n * Nested bulleted list' + }, + { + name: 'Nested list with levels 1- Numbered list (Italics), 2-Bulleted list', + html: '
  1. Numbered list italics

    • Nested bulleted list

', + markdown: '1. *Numbered list italics*\n\n * Nested bulleted list' + }, + { + name: 'Nested list with levels 1- Numbered list, level 2- Bulleted list with multiple items', + html: '
  1. Numbered list

    • list 1

    • list 2

', + markdown: '1. Numbered list\n\n * list 1\n\n * list 2' + }, + { + name: 'Nested list with levels 1- Bulleted list, level 2- Numbered list with multiple items', + html: '
  • Bulleted list

    1. list 1

    2. list 2

', + markdown: '* Bulleted list\n\n 1. list 1\n\n 2. list 2' + }, + { + name: 'HTML entities <&>', + html: '&', + markdown: '&' + }, + { + name: 'HTML entities <™>', + html: '™', + markdown: '™' + }, + { + name: 'HTML entities <&euro>', + html: '€', + markdown: '€' + }, + { + name: 'Markdown syntax strings <*>', + html: '*', + markdown: '\\*' + }, + { + name: 'Markdown syntax strings <**>', + html: '**', + markdown: '\\*\\*' + }, + { + name: 'Markdown syntax strings <_>', + html: '_', + markdown: '\\_' + }, ]; const focused: string[] = []; @@ -165,6 +225,16 @@ describe('Markdown serializer', () => { name: 'Underline', html: 'Underline', plainText: 'Underline' + }, + { + name: 'Script tag', + html: '', + plainText: '' + }, + { + name: 'iframe tag', + html: '', + plainText: '' } ]; From 81e348a975766cb432ef52fdae3b34a791e99131 Mon Sep 17 00:00:00 2001 From: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> Date: Thu, 7 Sep 2023 17:00:06 +0530 Subject: [PATCH 30/56] chore: run format Signed-off-by: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> --- .../models/tests/markdown-serializer.spec.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts index 3ef28c8a56..5f2c39529a 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts @@ -99,7 +99,8 @@ describe('Markdown serializer', () => { { name: 'Nested list with levels 1 - Bulleted list, 2 - Numbered list (Bold)', html: '
  • Bulleted list

    1. Nested bold numbered list

', - markdown: '* Bulleted list\n\n 1. **Nested bold numbered list**' + markdown: + '* Bulleted list\n\n 1. **Nested bold numbered list**' }, { name: 'Nested list with levels 1 - Bulleted list, 2 - Numbered list (Italics)', @@ -109,12 +110,14 @@ describe('Markdown serializer', () => { { name: 'Nested list with levels 1- Numbered list (Bold), 2-Bulleted list', html: '
  1. Numbered list bold

    • Nested bulleted list

', - markdown: '1. **Numbered list bold**\n\n * Nested bulleted list' + markdown: + '1. **Numbered list bold**\n\n * Nested bulleted list' }, { name: 'Nested list with levels 1- Numbered list (Italics), 2-Bulleted list', html: '
  1. Numbered list italics

    • Nested bulleted list

', - markdown: '1. *Numbered list italics*\n\n * Nested bulleted list' + markdown: + '1. *Numbered list italics*\n\n * Nested bulleted list' }, { name: 'Nested list with levels 1- Numbered list, level 2- Bulleted list with multiple items', @@ -155,7 +158,7 @@ describe('Markdown serializer', () => { name: 'Markdown syntax strings <_>', html: '_', markdown: '\\_' - }, + } ]; const focused: string[] = []; @@ -228,12 +231,12 @@ describe('Markdown serializer', () => { }, { name: 'Script tag', - html: '', + html: '', plainText: '' }, { name: 'iframe tag', - html: '', + html: '', plainText: '' } ]; From 1a6e97f9fe69283908eeb4264df530649b07ce21 Mon Sep 17 00:00:00 2001 From: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> Date: Thu, 7 Sep 2023 17:41:34 +0530 Subject: [PATCH 31/56] refactor: add in review changes Signed-off-by: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> --- .../rich-text/models/tests/markdown-serializer.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts index 5f2c39529a..1c60d696c0 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts @@ -131,7 +131,7 @@ describe('Markdown serializer', () => { }, { name: 'HTML entities <&>', - html: '&', + html: '&', markdown: '&' }, { @@ -147,17 +147,17 @@ describe('Markdown serializer', () => { { name: 'Markdown syntax strings <*>', html: '*', - markdown: '\\*' + markdown: String.raw`\*` }, { name: 'Markdown syntax strings <**>', html: '**', - markdown: '\\*\\*' + markdown: String.raw`\*\*` }, { name: 'Markdown syntax strings <_>', html: '_', - markdown: '\\_' + markdown: String.raw`\_` } ]; From 7bedc9c705a5945dce2ae7d879aed56babdb292d Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 7 Sep 2023 19:45:01 +0530 Subject: [PATCH 32/56] absolute link support in editor --- package-lock.json | 22 ++ packages/nimble-components/package.json | 1 + .../src/rich-text/editor/index.ts | 42 ++- .../src/rich-text/editor/styles.ts | 23 ++ .../testing/rich-text-editor.pageobject.ts | 41 ++- .../tests/rich-text-editor-matrix.stories.ts | 4 +- .../editor/tests/rich-text-editor.spec.ts | 341 ++++++++++++++++++ .../editor/tests/rich-text-editor.stories.ts | 15 +- .../src/rich-text/models/markdown-parser.ts | 39 +- .../rich-text/models/markdown-serializer.ts | 10 +- .../models/tests/markdown-parser.spec.ts | 204 ++++++++++- .../models/tests/markdown-serializer.spec.ts | 37 +- .../src/rich-text/viewer/styles.ts | 16 +- 13 files changed, 751 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e7034d708..4721a15fe3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10705,6 +10705,22 @@ "@tiptap/core": "^2.0.0" } }, + "node_modules/@tiptap/extension-link": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.1.7.tgz", + "integrity": "sha512-NDfoMCkThng1B530pMg5y69+eWoghZXK2uCntrJH7Rs8jNeGMyt9wGIOd7N8ZYz0oJ2ZYKzZjS0RANdBDS17DA==", + "dependencies": { + "linkifyjs": "^4.1.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, "node_modules/@tiptap/extension-list-item": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.1.6.tgz", @@ -22663,6 +22679,11 @@ "uc.micro": "^1.0.1" } }, + "node_modules/linkifyjs": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.1.tgz", + "integrity": "sha512-zFN/CTVmbcVef+WaDXT63dNzzkfRBKT1j464NJQkV7iSgJU0sLBus9W0HBwnXK13/hf168pbrx/V/bjEHOXNHA==" + }, "node_modules/liquidjs": { "version": "9.43.0", "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-9.43.0.tgz", @@ -34734,6 +34755,7 @@ "@tiptap/extension-document": "^2.1.6", "@tiptap/extension-history": "^2.1.6", "@tiptap/extension-italic": "^2.1.6", + "@tiptap/extension-link": "^2.1.6", "@tiptap/extension-list-item": "^2.1.6", "@tiptap/extension-ordered-list": "^2.1.6", "@tiptap/extension-paragraph": "^2.1.6", diff --git a/packages/nimble-components/package.json b/packages/nimble-components/package.json index 5c4d6b1d23..8a67925132 100644 --- a/packages/nimble-components/package.json +++ b/packages/nimble-components/package.json @@ -70,6 +70,7 @@ "@tiptap/extension-document": "^2.1.6", "@tiptap/extension-history": "^2.1.6", "@tiptap/extension-italic": "^2.1.6", + "@tiptap/extension-link": "^2.1.6", "@tiptap/extension-list-item": "^2.1.6", "@tiptap/extension-ordered-list": "^2.1.6", "@tiptap/extension-paragraph": "^2.1.6", diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index d32f23b716..dceadbd357 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -11,13 +11,15 @@ import { findParentNode, isList, AnyExtension, - Extension + Extension, + Mark } from '@tiptap/core'; import Bold from '@tiptap/extension-bold'; import BulletList from '@tiptap/extension-bullet-list'; import Document from '@tiptap/extension-document'; import History from '@tiptap/extension-history'; import Italic from '@tiptap/extension-italic'; +import Link, { LinkOptions } from '@tiptap/extension-link'; import ListItem from '@tiptap/extension-list-item'; import OrderedList from '@tiptap/extension-ordered-list'; import Paragraph from '@tiptap/extension-paragraph'; @@ -31,6 +33,7 @@ import { TipTapNodeName } from './types'; import type { ErrorPattern } from '../../patterns/error/types'; import { RichTextMarkdownParser } from '../models/markdown-parser'; import { RichTextMarkdownSerializer } from '../models/markdown-serializer'; +import { anchorTag } from '../../anchor'; declare global { interface HTMLElementTagNameMap { @@ -330,6 +333,8 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { } private createTiptapEditor(): Editor { + const customLink = this.getCustomLinkExtension(); + /** * For more information on the extensions for the supported formatting options, refer to the links below. * Tiptap marks: https://tiptap.dev/api/marks @@ -350,11 +355,46 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { Placeholder.configure({ placeholder: '', showOnlyWhenEditable: false + }), + customLink.configure({ + // HTMLAttribute cannot be in camelCase as we want to match it with the name in Tiptap + // eslint-disable-next-line @typescript-eslint/naming-convention + HTMLAttributes: { + rel: 'noopener noreferrer', + target: null + }, + openOnClick: false, + linkOnPaste: false, + validate: href => /^https?:\/\//.test(href) }) ] }); } + /** + * Extending the default link mark schema defined in the TipTap. + * + * "exclude": https://prosemirror.net/docs/ref/#model.MarkSpec.excludes + * "inclusive": https://prosemirror.net/docs/ref/#model.MarkSpec.inclusive + * "parseHTML": https://tiptap.dev/guide/custom-extensions#parse-html + * "renderHTML": https://tiptap.dev/guide/custom-extensions/#render-html + */ + private getCustomLinkExtension(): Mark { + return Link.extend({ + excludes: '_', + inclusive: false, + parseHTML() { + return [{ tag: anchorTag }]; + }, + // HTMLAttribute cannot be in camelCase as we want to match it with the name in Tiptap + // eslint-disable-next-line @typescript-eslint/naming-convention + renderHTML({ HTMLAttributes }) { + HTMLAttributes.tabindex = '-1'; + return [anchorTag, HTMLAttributes]; + } + }); + } + /** * This function takes the Fragment from parseMarkdownToDOM function and return the serialized string using XMLSerializer */ diff --git a/packages/nimble-components/src/rich-text/editor/styles.ts b/packages/nimble-components/src/rich-text/editor/styles.ts index 2af9ce8127..aac2bba140 100644 --- a/packages/nimble-components/src/rich-text/editor/styles.ts +++ b/packages/nimble-components/src/rich-text/editor/styles.ts @@ -77,6 +77,12 @@ export const styles = css` border: ${borderWidth} solid rgba(${borderRgbPartialColor}, 0.1); } + :host([disabled]) nimble-anchor::part(control) { + color: ${bodyDisabledFontColor}; + fill: currentcolor; + cursor: default; + } + :host([error-visible]) .container { border-bottom-color: ${failColor}; } @@ -188,6 +194,23 @@ export const styles = css` color: ${controlLabelDisabledFontColor}; } + nimble-anchor { + white-space: normal; + ${ + /** + * Restricting the pointer events for the following reasons: + * 1. nimble-anchor inside a "contenteditable" div is not working as native anchor HTML anchor tag. + * i.e. clicking on the link opens in the same tab whereas the default behavior of native HTML anchor + * tag is not clickable inside "contenteditable" div. + * 2. Restricting the user from opening a link using the right-click context menu: If the user manually edits + * the link, the 'href' attribute of the anchor tag will not be updated. If they attempt to open it using + * the right-click context menu with 'Open in new tab/window,' it will still navigate to the link specified + * in the 'href' attribute. + */ '' + } + pointer-events: none; + } + .footer-section { display: flex; justify-content: space-between; diff --git a/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts b/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts index aa639921b6..05b440680e 100644 --- a/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts +++ b/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts @@ -105,15 +105,15 @@ export class RichTextEditorPageObject { } public async setEditorTextContent(value: string): Promise { - let lastElement = this.getTiptapEditor()?.lastElementChild; - - while (lastElement?.lastElementChild) { - lastElement = lastElement?.lastElementChild; - } + const lastElement = this.getEditorLastChildElement(); lastElement!.parentElement!.textContent = value; await waitForUpdatesAsync(); } + public getEditorLastChildAttribute(attribute: string): string { + return this.getEditorLastChildElement()?.getAttribute(attribute) || ''; + } + public getEditorFirstChildTagName(): string { return this.getTiptapEditor()?.firstElementChild?.tagName ?? ''; } @@ -136,6 +136,28 @@ export class RichTextEditorPageObject { .map(el => el.textContent || ''); } + public getEditorTagNamesWithClosingTags(): string[] { + const tagNames: string[] = []; + const tiptapEditor = this.getTiptapEditor(); + + const processNode = (node: Node): void => { + if (node.nodeType === Node.ELEMENT_NODE) { + const el = node as Element; + tagNames.push(el.tagName); + + el.childNodes.forEach(processNode); + + tagNames.push(`/${el.tagName}`); + } + }; + + if (tiptapEditor) { + processNode(tiptapEditor); + } + + return tagNames.slice(1, -1); + } + public getFormattingButtonTextContent( toolbarButton: ToolbarButton ): string { @@ -217,4 +239,13 @@ export class RichTextEditorPageObject { ); return buttons[button]; } + + private getEditorLastChildElement(): Element | null | undefined { + let lastElement = this.getTiptapEditor()?.lastElementChild; + + while (lastElement?.lastElementChild) { + lastElement = lastElement?.lastElementChild; + } + return lastElement; + } } diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts index f942be18b2..76fd51bfec 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts @@ -30,7 +30,7 @@ const metadata: Meta = { } }; -const richTextMarkdownString = '1. **Bold*Italics***'; +const richTextMarkdownString = '1. \n2. **Bold*Italics***'; export default metadata; @@ -63,7 +63,7 @@ const component = ( ${() => footerHiddenName} ${() => errorStateName} ${() => placeholderName} ${() => disabledName}

<${richTextEditorTag} - style="margin: 5px 0px; width: 500px;" + style="margin: 5px 0px; width: 500px; height: 100px" ?disabled="${() => disabled}" ?footer-hidden="${() => footerHidden}" ?error-visible="${() => isError}" diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index 0877f829d8..0746e0f369 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -625,6 +625,160 @@ describe('RichTextEditor', () => { 'bold, italics and bullet list' ]); }); + + describe('Absolute link interactions in the editor', () => { + it('should change the text to "nimble-anchor" tag when it is a valid absolute link', async () => { + await pageObject.setEditorTextContent( + 'https://nimble.ni.dev/ ' + ); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'P', + 'NIMBLE-ANCHOR' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'https://nimble.ni.dev/' + ]); + }); + + it('should have the right attributes to the nimble-anchor', async () => { + await pageObject.setEditorTextContent( + 'https://nimble.ni.dev/ ' + ); + + expect(pageObject.getEditorLastChildAttribute('href')).toBe( + 'https://nimble.ni.dev/' + ); + expect(pageObject.getEditorLastChildAttribute('rel')).toBe( + 'noopener noreferrer' + ); + expect(pageObject.getEditorLastChildAttribute('tabindex')).toBe( + '-1' + ); + }); + + it('should not affect bold formatting on the link in editor', async () => { + await pageObject.clickFooterButton(ToolbarButton.bold); + await pageObject.setEditorTextContent( + 'https://nimble.ni.dev/ ' + ); + + expect(pageObject.getEditorTagNamesWithClosingTags()).toEqual([ + 'P', + 'NIMBLE-ANCHOR', + '/NIMBLE-ANCHOR', + 'STRONG', + '/STRONG', + '/P' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'https://nimble.ni.dev/', + ' ' + ]); + }); + + it('should not affect italics formatting on the link in editor', async () => { + await pageObject.clickFooterButton(ToolbarButton.italics); + await pageObject.setEditorTextContent( + 'https://nimble.ni.dev/ ' + ); + + expect(pageObject.getEditorTagNamesWithClosingTags()).toEqual([ + 'P', + 'NIMBLE-ANCHOR', + '/NIMBLE-ANCHOR', + 'EM', + '/EM', + '/P' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'https://nimble.ni.dev/', + ' ' + ]); + }); + + it('should able to add links to the bullet list', async () => { + await pageObject.setEditorTextContent( + 'https://nimble.ni.dev/ ' + ); + await pageObject.clickFooterButton(ToolbarButton.bulletList); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'UL', + 'LI', + 'P', + 'NIMBLE-ANCHOR' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'https://nimble.ni.dev/' + ]); + }); + + it('should able to add links to the numbered list', async () => { + await pageObject.setEditorTextContent( + 'https://nimble.ni.dev/ ' + ); + await pageObject.clickFooterButton(ToolbarButton.numberedList); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'OL', + 'LI', + 'P', + 'NIMBLE-ANCHOR' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'https://nimble.ni.dev/' + ]); + }); + + describe('various absolute links with different schemas other than https/http should be render as unchanged strings', () => { + const notSupportedAbsoluteLink: { name: string }[] = [ + { name: 'ftp://example.com/files/document.pdf ' }, + { name: 'mailto:info@example.com ' }, + { name: 'file:///path/to/local/file.txt ' }, + { name: 'tel:+1234567890 ' }, + // eslint-disable-next-line no-script-url + { name: 'javascript:void(0) ' }, + { name: 'data:image/png;base64,iVBORw0KG... ' }, + { name: 'ftps://example.com/files/document.pdf ' }, + { name: 'ssh://username@example.com ' }, + { name: 'urn:isbn:0451450523 ' }, + { + name: 'magnet:?xt=urn:btih:8c6dcd8d4f9151cb5cc01c68225b92db417c411f&dn=ExampleFile.iso ' + }, + { + name: 'bitcoin:1Hf1KqNPZzkFJ5Wv8VPop9uaF5RjKN3N9s?amount=0.001 ' + }, + // eslint-disable-next-line no-script-url + { name: 'javascript:vbscript:alert("not alert") ' }, + { name: 'test://test.com ' } + ]; + + const focused: string[] = []; + const disabled: string[] = []; + for (const value of notSupportedAbsoluteLink) { + const specType = getSpecTypeByNamedList( + value, + focused, + disabled + ); + specType( + `string "${value.name}" renders as plain text "${value.name}" within paragraph tag`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + async () => { + await pageObject.setEditorTextContent(value.name); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'P' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + value.name + ]); + } + ); + } + }); + }); }); describe('various wacky string values input into the editor', () => { @@ -670,6 +824,193 @@ describe('RichTextEditor', () => { ]); }); + describe('Absolute link markdown tests', () => { + describe('asserting rendered links in the editor', () => { + it('absolute link markdown string to "nimble-anchor" tags with the link as the text content', () => { + element.setMarkdown(''); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'P', + 'NIMBLE-ANCHOR' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'https://nimble.ni.dev/' + ]); + expect(pageObject.getEditorLastChildAttribute('href')).toBe( + 'https://nimble.ni.dev/' + ); + }); + + it('bulleted list with absolute links markdown string to "ul", "li" and "nimble-anchor" tags', () => { + element.setMarkdown('* '); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'UL', + 'LI', + 'P', + 'NIMBLE-ANCHOR' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'https://nimble.ni.dev/' + ]); + expect(pageObject.getEditorLastChildAttribute('href')).toBe( + 'https://nimble.ni.dev/' + ); + }); + + it('numbered list with absolute links markdown string to "ol", "li" and "nimble-anchor" tags', () => { + element.setMarkdown('1. '); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'OL', + 'LI', + 'P', + 'NIMBLE-ANCHOR' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'https://nimble.ni.dev/' + ]); + expect(pageObject.getEditorLastChildAttribute('href')).toBe( + 'https://nimble.ni.dev/' + ); + }); + + it('absolute links in bold markdown string should not be parsed to "strong" tag', () => { + element.setMarkdown('****'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'P', + 'NIMBLE-ANCHOR' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'https://nimble.ni.dev/' + ]); + expect(pageObject.getEditorLastChildAttribute('href')).toBe( + 'https://nimble.ni.dev/' + ); + }); + + it('absolute links in italics markdown string should not be parsed to "em" tag', () => { + element.setMarkdown('**'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'P', + 'NIMBLE-ANCHOR' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'https://nimble.ni.dev/' + ]); + expect(pageObject.getEditorLastChildAttribute('href')).toBe( + 'https://nimble.ni.dev/' + ); + }); + + it('absolute links in both bold and italics markdown string should not be parsed to "strong" and "em" tag', () => { + element.setMarkdown('______'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'P', + 'NIMBLE-ANCHOR' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'https://nimble.ni.dev/' + ]); + expect(pageObject.getEditorLastChildAttribute('href')).toBe( + 'https://nimble.ni.dev/' + ); + }); + + it('adding marks like bold inside absolute links should not be parsed to "strong" tag', () => { + element.setMarkdown(''); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'P', + 'NIMBLE-ANCHOR' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'https://**nimble**.ni.dev/' + ]); + expect(pageObject.getEditorLastChildAttribute('href')).toBe( + 'https://**nimble**.ni.dev/' + ); + }); + }); + + describe('asserting getMarkdown for rendered links', () => { + it('absolute link markdown string', () => { + element.setMarkdown(''); + + expect(element.getMarkdown()).toEqual( + '' + ); + }); + + it('bulleted list with absolute links markdown string', () => { + element.setMarkdown('* '); + + expect(element.getMarkdown()).toEqual( + '* ' + ); + }); + + it('numbered list with absolute links markdown string', () => { + element.setMarkdown('1. '); + + expect(element.getMarkdown()).toEqual( + '1. ' + ); + }); + + it('absolute links in bold markdown string should not be serialized to link in bold markdown', () => { + element.setMarkdown('****'); + + expect(element.getMarkdown()).toEqual( + '' + ); + }); + + it('absolute links in italics markdown string should not be serialized to link in italics markdown', () => { + element.setMarkdown('**'); + + expect(element.getMarkdown()).toEqual( + '' + ); + }); + + it('absolute links in both bold and italics markdown string should not be serialized to link in bold and italics markdown', () => { + element.setMarkdown('______'); + + expect(element.getMarkdown()).toEqual( + '' + ); + }); + + it('adding marks like bold inside absolute links should not be serialized to bold markdown', () => { + element.setMarkdown(''); + + expect(element.getMarkdown()).toEqual( + '' + ); + }); + + it('adding marks like italics inside absolute links should not be serialized to italics markdown', () => { + element.setMarkdown(''); + + expect(element.getMarkdown()).toEqual( + '' + ); + }); + + it('adding both the italics and bold inside absolute links should not be serialized to bold and italics markdown', () => { + element.setMarkdown(''); + + expect(element.getMarkdown()).toEqual( + '' + ); + }); + }); + }); + it('Should return a empty string when empty string is assigned', () => { element.setMarkdown('markdown string'); expect(element.getMarkdown()).toBe('markdown string'); diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts index a1a34763fa..9d9a42f3f7 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts @@ -12,6 +12,7 @@ import { type LabelUserArgs } from '../../../label-provider/base/tests/label-user-stories-utils'; import { labelProviderRichTextTag } from '../../../label-provider/rich-text'; +import { richTextMarkdownString } from '../../../utilities/tests/rich-text-markdown-string'; // eslint-disable-next-line @typescript-eslint/no-empty-interface interface RichTextEditorArgs extends LabelUserArgs { @@ -38,20 +39,9 @@ const exampleDataType = { const plainString = 'Plain text' as const; -const markdownString = ` -Supported rich text formatting options: -1. **Bold** -2. *Italics* -3. Numbered lists - 1. Option 1 - 2. Option 2 -4. Bulleted lists - * Option 1 - * Option 2` as const; - const dataSets = { [exampleDataType.plainString]: plainString, - [exampleDataType.markdownString]: markdownString + [exampleDataType.markdownString]: richTextMarkdownString } as const; const richTextEditorDescription = 'The rich text editor component allows users to add/edit text formatted with various styling options including bold, italics, numbered lists, and bulleted lists. The editor generates markdown output and takes markdown as input. The markdown flavor used is [CommonMark](https://spec.commonmark.org/0.30/).\n\n See the [rich text viewer](?path=/docs/incubating-rich-text-viewer--docs) component to render markdown without allowing editing.'; @@ -86,6 +76,7 @@ const metadata: Meta = { })} <${richTextEditorTag} ${ref('editorRef')} + style="height: 160px" data-unused="${x => x.setMarkdownData(x)}" ?disabled="${x => x.disabled}" ?footer-hidden="${x => x.footerHidden}" diff --git a/packages/nimble-components/src/rich-text/models/markdown-parser.ts b/packages/nimble-components/src/rich-text/models/markdown-parser.ts index e37f4045e0..bd36eeaa64 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-parser.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-parser.ts @@ -3,14 +3,18 @@ import { defaultMarkdownParser, MarkdownParser } from 'prosemirror-markdown'; -import { DOMSerializer } from 'prosemirror-model'; +import { DOMSerializer, Schema } from 'prosemirror-model'; +import { anchorTag } from '../../anchor'; /** * Provides markdown parser for rich text components */ export class RichTextMarkdownParser { + private static readonly updatedSchema = this.getUpdatedSchema(); private static readonly markdownParser = this.initializeMarkdownParser(); - private static readonly domSerializer = DOMSerializer.fromSchema(schema); + private static readonly domSerializer = DOMSerializer.fromSchema( + this.updatedSchema + ); /** * This function takes a markdown string, parses it using the ProseMirror MarkdownParser, serializes the parsed content into a @@ -46,10 +50,39 @@ export class RichTextMarkdownParser { 'autolink' ]); + supportedTokenizerRules.validateLink = href => /^https?:\/\//.test(href); + return new MarkdownParser( - schema, + this.updatedSchema, supportedTokenizerRules, defaultMarkdownParser.tokens ); } + + private static getUpdatedSchema(): Schema { + return new Schema({ + nodes: schema.spec.nodes, + marks: { + link: { + attrs: { + href: {}, + rel: { default: 'noopener noreferrer' } + }, + inclusive: false, + excludes: '_', + toDOM(node) { + return [ + anchorTag, + { + href: node.attrs.href as Attr, + rel: node.attrs.rel as Attr + } + ]; + } + }, + em: schema.spec.marks.get('em')!, + strong: schema.spec.marks.get('strong')! + } + }); + } } diff --git a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts index cad1e14484..61be5da5dd 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts @@ -51,7 +51,15 @@ export class RichTextMarkdownSerializer { }; const marks = { italic: defaultMarkdownSerializer.marks.em!, - bold: defaultMarkdownSerializer.marks.strong! + bold: defaultMarkdownSerializer.marks.strong!, + // Autolink markdown in CommonMark flavor: https://spec.commonmark.org/0.30/#autolinks + link: { + open: '<', + close: '>', + mixable: true, + escape: false, + expelEnclosingWhitespace: true + } }; return new MarkdownSerializer(nodes, marks); } diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index fa3f274150..bb25e3d366 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -21,6 +21,24 @@ describe('Markdown parser', () => { return nodes; } + function getEditorLastChildAttribute( + attribute: string, + doc: DocumentFragment + ): string { + return getEditorLastChildElement(doc)?.getAttribute(attribute) || ''; + } + + function getEditorLastChildElement( + doc: DocumentFragment + ): Element | null | undefined { + let lastElement = doc.lastElementChild; + + while (lastElement?.lastElementChild) { + lastElement = lastElement?.lastElementChild; + } + return lastElement; + } + describe('supported rich text formatting options from markdown string to its respective HTML elements', () => { it('bold markdown string("**") to "strong" HTML tag', () => { const doc = RichTextMarkdownParser.parseMarkdownToDOM('**Bold**'); @@ -230,9 +248,185 @@ describe('Markdown parser', () => { ).toEqual(['Bulleted list in italics']); }); + describe('Absolute link', () => { + it('absolute link markdown string to "a" tags with the link as the text content', () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + '' + ); + + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['P', 'NIMBLE-ANCHOR']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['https://nimble.ni.dev/']); + expect( + getEditorLastChildAttribute('href', doc as DocumentFragment) + ).toBe('https://nimble.ni.dev/'); + }); + + it('absolute link should add "rel" attribute', () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + '' + ); + + expect( + getEditorLastChildAttribute('rel', doc as DocumentFragment) + ).toBe('noopener noreferrer'); + }); + + it('bulleted list with absolute links markdown string to "ul", "li" and "a" HTML tags', () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + '* ' + ); + + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['UL', 'LI', 'P', 'NIMBLE-ANCHOR']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['https://nimble.ni.dev/']); + expect( + getEditorLastChildAttribute('href', doc as DocumentFragment) + ).toBe('https://nimble.ni.dev/'); + }); + + it('numbered list with absolute links markdown string to "ol", "li" and "a" HTML tags', () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + '1. ' + ); + + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['OL', 'LI', 'P', 'NIMBLE-ANCHOR']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['https://nimble.ni.dev/']); + expect( + getEditorLastChildAttribute('href', doc as DocumentFragment) + ).toBe('https://nimble.ni.dev/'); + }); + + it('absolute links in bold markdown string should not be parsed to "strong" HTML tag', () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + '****' + ); + + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['P', 'NIMBLE-ANCHOR']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['https://nimble.ni.dev/']); + expect( + getEditorLastChildAttribute('href', doc as DocumentFragment) + ).toBe('https://nimble.ni.dev/'); + }); + + it('absolute links in italics markdown string should not be parsed to "em" HTML tag', () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + '**' + ); + + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['P', 'NIMBLE-ANCHOR']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['https://nimble.ni.dev/']); + expect( + getEditorLastChildAttribute('href', doc as DocumentFragment) + ).toBe('https://nimble.ni.dev/'); + }); + + it('absolute links in both bold and italics markdown string should not be parsed to "strong" and "em" HTML tag', () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + '______' + ); + + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['P', 'NIMBLE-ANCHOR']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['https://nimble.ni.dev/']); + expect( + getEditorLastChildAttribute('href', doc as DocumentFragment) + ).toBe('https://nimble.ni.dev/'); + }); + + it('adding marks like bold inside absolute links should not be parsed to "strong" HTML tag', () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + '' + ); + + expect( + getTagsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['P', 'NIMBLE-ANCHOR']); + expect( + getLeafContentsFromDocumentFragment(doc as DocumentFragment) + ).toEqual(['https://**nimble**.ni.dev/']); + expect( + getEditorLastChildAttribute('href', doc as DocumentFragment) + ).toBe('https://**nimble**.ni.dev/'); + }); + + describe('various absolute links with different schemas other than https/http should be render as unchanged strings', () => { + const notSupportedAbsoluteLink: { name: string }[] = [ + { name: '' }, + { name: '' }, + { name: '' }, + { name: '' }, + { name: '' }, + { name: '' }, + { name: '' }, + { name: '' }, + { name: '' }, + { + name: '' + }, + { + name: '' + }, + { name: '' }, + { name: '' } + ]; + + const focused: string[] = []; + const disabled: string[] = []; + for (const value of notSupportedAbsoluteLink) { + const specType = getSpecTypeByNamedList( + value, + focused, + disabled + ); + specType( + `string "${value.name}" renders as plain text "${value.name}" within paragraph tag`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + value.name + ); + + expect( + getTagsFromDocumentFragment( + doc as DocumentFragment + ) + ).toEqual(['P']); + expect( + getLeafContentsFromDocumentFragment( + doc as DocumentFragment + ) + ).toEqual([value.name]); + } + ); + } + }); + }); + it('combination of all supported markdown string', () => { const doc = RichTextMarkdownParser.parseMarkdownToDOM( - '1. ***Numbered list with bold and italics***\n* ___Bulleted list with bold and italics___' + '1. ***Numbered list with bold and italics***\n* ___Bulleted list with bold and italics___\n* ' ); expect( @@ -247,13 +441,17 @@ describe('Markdown parser', () => { 'LI', 'P', 'EM', - 'STRONG' + 'STRONG', + 'LI', + 'P', + 'NIMBLE-ANCHOR' ]); expect( getLeafContentsFromDocumentFragment(doc as DocumentFragment) ).toEqual([ 'Numbered list with bold and italics', - 'Bulleted list with bold and italics' + 'Bulleted list with bold and italics', + 'https://nimble.ni.dev/' ]); }); }); diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts index 161460836d..05502d5f97 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts @@ -7,6 +7,7 @@ import ListItem from '@tiptap/extension-list-item'; import OrderedList from '@tiptap/extension-ordered-list'; import Paragraph from '@tiptap/extension-paragraph'; import Text from '@tiptap/extension-text'; +import Link from '@tiptap/extension-link'; import type { Node } from 'prosemirror-model'; import { RichTextMarkdownSerializer } from '../markdown-serializer'; import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; @@ -22,7 +23,10 @@ describe('Markdown serializer', () => { OrderedList, ListItem, Bold, - Italic + Italic, + Link.extend({ + excludes: '_' + }) ] }); @@ -51,11 +55,31 @@ describe('Markdown serializer', () => { html: 'Italics', markdown: '*Italics*' }, + { + name: 'Link', + html: '

Link

', + markdown: '' + }, { name: 'Bold and Italics', html: 'Bold and Italics', markdown: '***Bold and Italics***' }, + { + name: 'Link and Bold', + html: '

Link and Bold

', + markdown: '' + }, + { + name: 'Link and Italics', + html: '

Link and Italics

', + markdown: '' + }, + { + name: 'Link, Bold and Italics', + html: '

Link, Bold and Italics

', + markdown: '' + }, { name: 'Numbered list', html: '
  1. Numbered list

', @@ -76,6 +100,11 @@ describe('Markdown serializer', () => { html: '
  1. Numbered list with italics

', markdown: '1. *Numbered list with italics*' }, + { + name: 'Numbered list with link', + html: '
  1. Numbered list with link

', + markdown: '1. ' + }, { name: 'Bullet list', html: '
  • Bullet list

', @@ -95,6 +124,11 @@ describe('Markdown serializer', () => { name: 'Bullet list with italics', html: '
  • Bullet list with italics

', markdown: '* *Bullet list with italics*' + }, + { + name: 'Bullet list with link', + html: '', + markdown: '* ' } ]; @@ -149,7 +183,6 @@ describe('Markdown serializer', () => { html: 'Highlight', plainText: 'Highlight' }, - { name: 'Link', html: 'Link', plainText: 'Link' }, { name: 'Strikethrough', html: 'Strikethrough', diff --git a/packages/nimble-components/src/rich-text/viewer/styles.ts b/packages/nimble-components/src/rich-text/viewer/styles.ts index 0d9d8fcbfa..5d41ae0709 100644 --- a/packages/nimble-components/src/rich-text/viewer/styles.ts +++ b/packages/nimble-components/src/rich-text/viewer/styles.ts @@ -1,11 +1,6 @@ import { css } from '@microsoft/fast-element'; import { display } from '@microsoft/fast-foundation'; -import { - bodyFont, - bodyFontColor, - linkActiveFontColor, - linkFontColor -} from '../../theme-provider/design-tokens'; +import { bodyFont, bodyFontColor } from '../../theme-provider/design-tokens'; export const styles = css` ${display('flex')} @@ -45,13 +40,4 @@ export const styles = css` li > p:empty { display: none; } - - a { - word-break: break-all; - color: ${linkFontColor}; - } - - a:active { - color: ${linkActiveFontColor}; - } `; From fd65fdb7b81b6cf876f15662e0f10dc00ab6a396 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 7 Sep 2023 20:37:16 +0530 Subject: [PATCH 33/56] Minor comment description --- packages/nimble-components/src/rich-text/editor/styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nimble-components/src/rich-text/editor/styles.ts b/packages/nimble-components/src/rich-text/editor/styles.ts index aac2bba140..1166f9adea 100644 --- a/packages/nimble-components/src/rich-text/editor/styles.ts +++ b/packages/nimble-components/src/rich-text/editor/styles.ts @@ -199,7 +199,7 @@ export const styles = css` ${ /** * Restricting the pointer events for the following reasons: - * 1. nimble-anchor inside a "contenteditable" div is not working as native anchor HTML anchor tag. + * 1. nimble-anchor inside a "contenteditable" div is not working as native HTML anchor tag. * i.e. clicking on the link opens in the same tab whereas the default behavior of native HTML anchor * tag is not clickable inside "contenteditable" div. * 2. Restricting the user from opening a link using the right-click context menu: If the user manually edits From e1ed701572decf1688c1c3bb9161221a2e44837a Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:15:04 +0530 Subject: [PATCH 34/56] Updated the comment description about link behavior --- packages/nimble-components/src/rich-text/editor/styles.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/styles.ts b/packages/nimble-components/src/rich-text/editor/styles.ts index 1166f9adea..b12ca76b5e 100644 --- a/packages/nimble-components/src/rich-text/editor/styles.ts +++ b/packages/nimble-components/src/rich-text/editor/styles.ts @@ -202,10 +202,11 @@ export const styles = css` * 1. nimble-anchor inside a "contenteditable" div is not working as native HTML anchor tag. * i.e. clicking on the link opens in the same tab whereas the default behavior of native HTML anchor * tag is not clickable inside "contenteditable" div. + * Issue link: https://github.com/ni/nimble/issues/1502 * 2. Restricting the user from opening a link using the right-click context menu: If the user manually edits - * the link, the 'href' attribute of the anchor tag will not be updated. If they attempt to open it using + * the link's text content, the 'href' attribute of the anchor tag will not be updated. If they attempt to open it using * the right-click context menu with 'Open in new tab/window,' it will still navigate to the link specified - * in the 'href' attribute. + * in the 'href' attribute, which may create unnecessary confusion while trying to open the link */ '' } pointer-events: none; From b3ba37da4046cb99ddd9b81656765ee6e2af7078 Mon Sep 17 00:00:00 2001 From: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:43:54 +0530 Subject: [PATCH 35/56] refactor: add parser utils for reusable functions Signed-off-by: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> --- .../testing/rich-text-editor.pageobject.ts | 11 +++-------- .../models/testing/markdown-parser-utils.ts | 17 +++++++++++++++++ .../models/tests/markdown-parser.spec.ts | 19 +------------------ 3 files changed, 21 insertions(+), 26 deletions(-) create mode 100644 packages/nimble-components/src/rich-text/models/testing/markdown-parser-utils.ts diff --git a/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts b/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts index aa639921b6..d0e0fb42ce 100644 --- a/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts +++ b/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts @@ -3,6 +3,7 @@ import type { RichTextEditor } from '..'; import { waitForUpdatesAsync } from '../../../testing/async-helpers'; import type { ToggleButton } from '../../../toggle-button'; import type { ToolbarButton } from './types'; +import { getTagsFromDocumentFragment, getLeafContentsFromDocumentFragment } from '../../models/testing/markdown-parser-utils'; /** * Page object for the `nimble-rich-text-editor` component. @@ -123,17 +124,11 @@ export class RichTextEditorPageObject { } public getEditorTagNames(): string[] { - return Array.from(this.getTiptapEditor()!.querySelectorAll('*')).map( - el => el.tagName - ); + return getTagsFromDocumentFragment(this.getTiptapEditor() as HTMLElement); } public getEditorLeafContents(): string[] { - return Array.from(this.getTiptapEditor()!.querySelectorAll('*')) - .filter((el, _) => { - return el.children.length === 0; - }) - .map(el => el.textContent || ''); + return getLeafContentsFromDocumentFragment(this.getTiptapEditor() as HTMLElement); } public getFormattingButtonTextContent( diff --git a/packages/nimble-components/src/rich-text/models/testing/markdown-parser-utils.ts b/packages/nimble-components/src/rich-text/models/testing/markdown-parser-utils.ts new file mode 100644 index 0000000000..65de11d2f3 --- /dev/null +++ b/packages/nimble-components/src/rich-text/models/testing/markdown-parser-utils.ts @@ -0,0 +1,17 @@ +export const getTagsFromDocumentFragment = (doc: DocumentFragment | HTMLElement): string[] => { + const nodes = Array.from(doc.querySelectorAll('*')).map( + el => el.tagName + ); + return nodes; +}; + +export const getLeafContentsFromDocumentFragment = ( + doc: DocumentFragment | HTMLElement +): string[] => { + const nodes = Array.from(doc.querySelectorAll('*')) + .filter((el, _) => { + return el.children.length === 0; + }) + .map(el => el.textContent || ''); + return nodes; +}; \ No newline at end of file diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index 72613e6b45..436fef918c 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -1,26 +1,9 @@ import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; import { wackyStrings } from '../../../utilities/tests/wacky-strings'; import { RichTextMarkdownParser } from '../markdown-parser'; +import { getLeafContentsFromDocumentFragment, getTagsFromDocumentFragment } from '../testing/markdown-parser-utils'; describe('Markdown parser', () => { - function getTagsFromDocumentFragment(doc: DocumentFragment): string[] { - const nodes = Array.from(doc.querySelectorAll('*')).map( - el => el.tagName - ); - return nodes; - } - - function getLeafContentsFromDocumentFragment( - doc: DocumentFragment - ): string[] { - const nodes = Array.from(doc.querySelectorAll('*')) - .filter((el, _) => { - return el.children.length === 0; - }) - .map(el => el.textContent || ''); - return nodes; - } - describe('supported rich text formatting options from markdown string to its respective HTML elements', () => { it('bold markdown string("**") to "strong" HTML tag', () => { const doc = RichTextMarkdownParser.parseMarkdownToDOM('**Bold**'); From f6d27d8523d515a512eb1f05d62fb98d17bdda02 Mon Sep 17 00:00:00 2001 From: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:52:49 +0530 Subject: [PATCH 36/56] refactor:remove unwanted type conversion and update function naming Signed-off-by: Sai krishnan Perumal <123591928+saikrishnan-ni@users.noreply.github.com> --- .../testing/rich-text-editor.pageobject.ts | 11 +- .../models/testing/markdown-parser-utils.ts | 12 +- .../models/tests/markdown-parser.spec.ts | 247 ++++++++---------- 3 files changed, 128 insertions(+), 142 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts b/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts index d0e0fb42ce..fe979d864d 100644 --- a/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts +++ b/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts @@ -3,7 +3,10 @@ import type { RichTextEditor } from '..'; import { waitForUpdatesAsync } from '../../../testing/async-helpers'; import type { ToggleButton } from '../../../toggle-button'; import type { ToolbarButton } from './types'; -import { getTagsFromDocumentFragment, getLeafContentsFromDocumentFragment } from '../../models/testing/markdown-parser-utils'; +import { + getTagsFromElement, + getLeafContentsFromElement +} from '../../models/testing/markdown-parser-utils'; /** * Page object for the `nimble-rich-text-editor` component. @@ -124,11 +127,13 @@ export class RichTextEditorPageObject { } public getEditorTagNames(): string[] { - return getTagsFromDocumentFragment(this.getTiptapEditor() as HTMLElement); + return getTagsFromElement(this.getTiptapEditor() as HTMLElement); } public getEditorLeafContents(): string[] { - return getLeafContentsFromDocumentFragment(this.getTiptapEditor() as HTMLElement); + return getLeafContentsFromElement( + this.getTiptapEditor() as HTMLElement + ); } public getFormattingButtonTextContent( diff --git a/packages/nimble-components/src/rich-text/models/testing/markdown-parser-utils.ts b/packages/nimble-components/src/rich-text/models/testing/markdown-parser-utils.ts index 65de11d2f3..84a635973b 100644 --- a/packages/nimble-components/src/rich-text/models/testing/markdown-parser-utils.ts +++ b/packages/nimble-components/src/rich-text/models/testing/markdown-parser-utils.ts @@ -1,11 +1,11 @@ -export const getTagsFromDocumentFragment = (doc: DocumentFragment | HTMLElement): string[] => { - const nodes = Array.from(doc.querySelectorAll('*')).map( - el => el.tagName - ); +export const getTagsFromElement = ( + doc: DocumentFragment | HTMLElement +): string[] => { + const nodes = Array.from(doc.querySelectorAll('*')).map(el => el.tagName); return nodes; }; -export const getLeafContentsFromDocumentFragment = ( +export const getLeafContentsFromElement = ( doc: DocumentFragment | HTMLElement ): string[] => { const nodes = Array.from(doc.querySelectorAll('*')) @@ -14,4 +14,4 @@ export const getLeafContentsFromDocumentFragment = ( }) .map(el => el.textContent || ''); return nodes; -}; \ No newline at end of file +}; diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index 436fef918c..062920ed3f 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -1,73 +1,53 @@ import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; import { wackyStrings } from '../../../utilities/tests/wacky-strings'; import { RichTextMarkdownParser } from '../markdown-parser'; -import { getLeafContentsFromDocumentFragment, getTagsFromDocumentFragment } from '../testing/markdown-parser-utils'; +import { + getLeafContentsFromElement, + getTagsFromElement +} from '../testing/markdown-parser-utils'; describe('Markdown parser', () => { describe('supported rich text formatting options from markdown string to its respective HTML elements', () => { it('bold markdown string("**") to "strong" HTML tag', () => { const doc = RichTextMarkdownParser.parseMarkdownToDOM('**Bold**'); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['P', 'STRONG']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Bold']); + + expect(getTagsFromElement(doc)).toEqual(['P', 'STRONG']); + expect(getLeafContentsFromElement(doc)).toEqual(['Bold']); }); it('bold markdown string("__") to "strong" HTML tag', () => { const doc = RichTextMarkdownParser.parseMarkdownToDOM('__Bold__'); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['P', 'STRONG']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Bold']); + expect(getTagsFromElement(doc)).toEqual(['P', 'STRONG']); + expect(getLeafContentsFromElement(doc)).toEqual(['Bold']); }); it('italics markdown string("*") to "em" HTML tag', () => { const doc = RichTextMarkdownParser.parseMarkdownToDOM('*Italics*'); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['P', 'EM']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Italics']); + expect(getTagsFromElement(doc)).toEqual(['P', 'EM']); + expect(getLeafContentsFromElement(doc)).toEqual(['Italics']); }); it('italics markdown string("_") to "em" HTML tag', () => { const doc = RichTextMarkdownParser.parseMarkdownToDOM('_Italics_'); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['P', 'EM']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Italics']); + expect(getTagsFromElement(doc)).toEqual(['P', 'EM']); + expect(getLeafContentsFromElement(doc)).toEqual(['Italics']); }); it('numbered list markdown string("1.") to "ol" and "li" HTML tags', () => { const doc = RichTextMarkdownParser.parseMarkdownToDOM('1. Numbered list'); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['OL', 'LI', 'P']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Numbered list']); + expect(getTagsFromElement(doc)).toEqual(['OL', 'LI', 'P']); + expect(getLeafContentsFromElement(doc)).toEqual(['Numbered list']); }); it('numbered list markdown string("1)") to "ol" and "li" HTML tags', () => { const doc = RichTextMarkdownParser.parseMarkdownToDOM('1) Numbered list'); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['OL', 'LI', 'P']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Numbered list']); + expect(getTagsFromElement(doc)).toEqual(['OL', 'LI', 'P']); + expect(getLeafContentsFromElement(doc)).toEqual(['Numbered list']); }); it('multiple numbered lists markdown string("1.\n2.") to "ol" and "li" HTML tags', () => { @@ -75,23 +55,30 @@ describe('Markdown parser', () => { '1. Option 1\n 2. Option 2' ); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['OL', 'LI', 'P', 'LI', 'P']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Option 1', 'Option 2']); + expect(getTagsFromElement(doc)).toEqual([ + 'OL', + 'LI', + 'P', + 'LI', + 'P' + ]); + expect(getLeafContentsFromElement(doc)).toEqual([ + 'Option 1', + 'Option 2' + ]); }); it('multiple empty numbered lists markdown string("1.\n2.") to "ol" and "li" HTML tags', () => { const doc = RichTextMarkdownParser.parseMarkdownToDOM('1. \n 2. '); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['OL', 'LI', 'P', 'LI', 'P']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['', '']); + expect(getTagsFromElement(doc)).toEqual([ + 'OL', + 'LI', + 'P', + 'LI', + 'P' + ]); + expect(getLeafContentsFromElement(doc)).toEqual(['', '']); }); it('numbered lists that start with numbers and are not sequential to "ol" and "li" HTML tags', () => { @@ -99,12 +86,17 @@ describe('Markdown parser', () => { '1. Option 1\n 1. Option 2' ); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['OL', 'LI', 'P', 'LI', 'P']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Option 1', 'Option 2']); + expect(getTagsFromElement(doc)).toEqual([ + 'OL', + 'LI', + 'P', + 'LI', + 'P' + ]); + expect(getLeafContentsFromElement(doc)).toEqual([ + 'Option 1', + 'Option 2' + ]); }); it('numbered lists if there is some content between lists', () => { @@ -112,12 +104,16 @@ describe('Markdown parser', () => { '1. Option 1\n\nSome content in between lists\n\n 2. Option 2' ); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['OL', 'LI', 'P', 'P', 'OL', 'LI', 'P']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual([ + expect(getTagsFromElement(doc)).toEqual([ + 'OL', + 'LI', + 'P', + 'P', + 'OL', + 'LI', + 'P' + ]); + expect(getLeafContentsFromElement(doc)).toEqual([ 'Option 1', 'Some content in between lists', 'Option 2' @@ -127,34 +123,22 @@ describe('Markdown parser', () => { it('bulleted list markdown string("*") to "ul" and "li" HTML tags', () => { const doc = RichTextMarkdownParser.parseMarkdownToDOM('* Bulleted list'); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['UL', 'LI', 'P']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Bulleted list']); + expect(getTagsFromElement(doc)).toEqual(['UL', 'LI', 'P']); + expect(getLeafContentsFromElement(doc)).toEqual(['Bulleted list']); }); it('bulleted list markdown string("-") to "ul" and "li" HTML tags', () => { const doc = RichTextMarkdownParser.parseMarkdownToDOM('- Bulleted list'); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['UL', 'LI', 'P']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Bulleted list']); + expect(getTagsFromElement(doc)).toEqual(['UL', 'LI', 'P']); + expect(getLeafContentsFromElement(doc)).toEqual(['Bulleted list']); }); it('bulleted list markdown string("+") to "ul" and "li" HTML tags', () => { const doc = RichTextMarkdownParser.parseMarkdownToDOM('+ Bulleted list'); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['UL', 'LI', 'P']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Bulleted list']); + expect(getTagsFromElement(doc)).toEqual(['UL', 'LI', 'P']); + expect(getLeafContentsFromElement(doc)).toEqual(['Bulleted list']); }); it('multiple bulleted lists markdown string("* \n* \n*") to "ul" and "li" HTML tags', () => { @@ -162,12 +146,20 @@ describe('Markdown parser', () => { '* Option 1\n * Option 2\n * Option 3' ); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['UL', 'LI', 'P', 'LI', 'P', 'LI', 'P']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Option 1', 'Option 2', 'Option 3']); + expect(getTagsFromElement(doc)).toEqual([ + 'UL', + 'LI', + 'P', + 'LI', + 'P', + 'LI', + 'P' + ]); + expect(getLeafContentsFromElement(doc)).toEqual([ + 'Option 1', + 'Option 2', + 'Option 3' + ]); }); it('bulleted lists if there is some content between lists', () => { @@ -175,12 +167,16 @@ describe('Markdown parser', () => { '* Option 1\n\nSome content in between lists\n\n * Option 2' ); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['UL', 'LI', 'P', 'P', 'UL', 'LI', 'P']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual([ + expect(getTagsFromElement(doc)).toEqual([ + 'UL', + 'LI', + 'P', + 'P', + 'UL', + 'LI', + 'P' + ]); + expect(getLeafContentsFromElement(doc)).toEqual([ 'Option 1', 'Some content in between lists', 'Option 2' @@ -192,12 +188,15 @@ describe('Markdown parser', () => { '1. **Numbered list in bold**' ); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['OL', 'LI', 'P', 'STRONG']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Numbered list in bold']); + expect(getTagsFromElement(doc)).toEqual([ + 'OL', + 'LI', + 'P', + 'STRONG' + ]); + expect(getLeafContentsFromElement(doc)).toEqual([ + 'Numbered list in bold' + ]); }); it('bulleted list with italics markdown string to "ul", "li" and "em" HTML tags', () => { @@ -205,12 +204,10 @@ describe('Markdown parser', () => { '* *Bulleted list in italics*' ); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['UL', 'LI', 'P', 'EM']); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['Bulleted list in italics']); + expect(getTagsFromElement(doc)).toEqual(['UL', 'LI', 'P', 'EM']); + expect(getLeafContentsFromElement(doc)).toEqual([ + 'Bulleted list in italics' + ]); }); it('combination of all supported markdown string', () => { @@ -218,9 +215,7 @@ describe('Markdown parser', () => { '1. ***Numbered list with bold and italics***\n* ___Bulleted list with bold and italics___' ); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual([ + expect(getTagsFromElement(doc)).toEqual([ 'OL', 'LI', 'P', @@ -232,9 +227,7 @@ describe('Markdown parser', () => { 'EM', 'STRONG' ]); - expect( - getLeafContentsFromDocumentFragment(doc as DocumentFragment) - ).toEqual([ + expect(getLeafContentsFromElement(doc)).toEqual([ 'Numbered list with bold and italics', 'Bulleted list with bold and italics' ]); @@ -281,14 +274,10 @@ describe('Markdown parser', () => { value.name ); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['P']); - expect( - getLeafContentsFromDocumentFragment( - doc as DocumentFragment - ) - ).toEqual([value.name]); + expect(getTagsFromElement(doc)).toEqual(['P']); + expect(getLeafContentsFromElement(doc)).toEqual([ + value.name + ]); } ); } @@ -314,14 +303,10 @@ describe('Markdown parser', () => { value.name ); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(['P']); - expect( - getLeafContentsFromDocumentFragment( - doc as DocumentFragment - ) - ).toEqual([value.name]); + expect(getTagsFromElement(doc)).toEqual(['P']); + expect(getLeafContentsFromElement(doc)).toEqual([ + value.name + ]); } ); }); @@ -351,14 +336,10 @@ describe('Markdown parser', () => { value.name ); - expect( - getTagsFromDocumentFragment(doc as DocumentFragment) - ).toEqual(value.tags); - expect( - getLeafContentsFromDocumentFragment( - doc as DocumentFragment - ) - ).toEqual(value.textContent); + expect(getTagsFromElement(doc)).toEqual(value.tags); + expect(getLeafContentsFromElement(doc)).toEqual( + value.textContent + ); } ); } From 9969270d7abd877f53de4688b634f866b0d1d336 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Fri, 8 Sep 2023 14:14:52 +0530 Subject: [PATCH 37/56] Removed mixable configuration from the serializer for link mark --- .../src/rich-text/models/markdown-serializer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts index 61be5da5dd..53afa86875 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts @@ -56,7 +56,6 @@ export class RichTextMarkdownSerializer { link: { open: '<', close: '>', - mixable: true, escape: false, expelEnclosingWhitespace: true } From 78a9f4bb157c9b6d08a120aca7429cfaebdbc193 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Sat, 9 Sep 2023 08:04:46 +0530 Subject: [PATCH 38/56] Change files --- ...le-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json | 7 ------- ...le-components-9f73b6f1-7567-4817-9f21-3c1c80bdd5c3.json | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 change/@ni-nimble-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json create mode 100644 change/@ni-nimble-components-9f73b6f1-7567-4817-9f21-3c1c80bdd5c3.json diff --git a/change/@ni-nimble-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json b/change/@ni-nimble-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json deleted file mode 100644 index 7fbe260da7..0000000000 --- a/change/@ni-nimble-components-21de2fc8-dfc6-4249-9098-d7842ce0eff3.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "none", - "comment": "Refactored rich text components to use common models", - "packageName": "@ni/nimble-components", - "email": "123591928+saikrishnan-ni@users.noreply.github.com", - "dependentChangeType": "none" -} diff --git a/change/@ni-nimble-components-9f73b6f1-7567-4817-9f21-3c1c80bdd5c3.json b/change/@ni-nimble-components-9f73b6f1-7567-4817-9f21-3c1c80bdd5c3.json new file mode 100644 index 0000000000..3494da0727 --- /dev/null +++ b/change/@ni-nimble-components-9f73b6f1-7567-4817-9f21-3c1c80bdd5c3.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Support for absolute links in rich text components", + "packageName": "@ni/nimble-components", + "email": "123377523+vivinkrishna-ni@users.noreply.github.com", + "dependentChangeType": "patch" +} From 2ffd1fb5ab1becaac1889de9c823a36a527d4772 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Mon, 11 Sep 2023 09:58:29 +0530 Subject: [PATCH 39/56] Resolving comments partially --- .../src/rich-text/editor/index.ts | 4 +- .../src/rich-text/editor/styles.ts | 12 ++--- .../testing/rich-text-editor.pageobject.ts | 6 +-- .../editor/tests/rich-text-editor.spec.ts | 44 ++++++++++++++----- .../src/rich-text/models/markdown-parser.ts | 2 +- .../models/testing/markdown-parser-utils.ts | 4 +- 6 files changed, 47 insertions(+), 25 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index dceadbd357..407859d988 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -365,7 +365,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { }, openOnClick: false, linkOnPaste: false, - validate: href => /^https?:\/\//.test(href) + validate: href => /^https?:\/\//i.test(href) }) ] }); @@ -374,7 +374,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { /** * Extending the default link mark schema defined in the TipTap. * - * "exclude": https://prosemirror.net/docs/ref/#model.MarkSpec.excludes + * "excludes": https://prosemirror.net/docs/ref/#model.MarkSpec.excludes * "inclusive": https://prosemirror.net/docs/ref/#model.MarkSpec.inclusive * "parseHTML": https://tiptap.dev/guide/custom-extensions#parse-html * "renderHTML": https://tiptap.dev/guide/custom-extensions/#render-html diff --git a/packages/nimble-components/src/rich-text/editor/styles.ts b/packages/nimble-components/src/rich-text/editor/styles.ts index b12ca76b5e..df5c537073 100644 --- a/packages/nimble-components/src/rich-text/editor/styles.ts +++ b/packages/nimble-components/src/rich-text/editor/styles.ts @@ -77,12 +77,6 @@ export const styles = css` border: ${borderWidth} solid rgba(${borderRgbPartialColor}, 0.1); } - :host([disabled]) nimble-anchor::part(control) { - color: ${bodyDisabledFontColor}; - fill: currentcolor; - cursor: default; - } - :host([error-visible]) .container { border-bottom-color: ${failColor}; } @@ -212,6 +206,12 @@ export const styles = css` pointer-events: none; } + :host([disabled]) nimble-anchor::part(control) { + color: ${bodyDisabledFontColor}; + fill: currentcolor; + cursor: default; + } + .footer-section { display: flex; justify-content: space-between; diff --git a/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts b/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts index 25c3745d1f..9b18cb4e77 100644 --- a/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts +++ b/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts @@ -112,7 +112,7 @@ export class RichTextEditorPageObject { public async setEditorTextContent(value: string): Promise { const lastElement = this.getEditorLastChildElement(); - lastElement!.parentElement!.textContent = value; + lastElement.parentElement!.textContent = value; await waitForUpdatesAsync(); } @@ -245,7 +245,7 @@ export class RichTextEditorPageObject { return buttons[button]; } - private getEditorLastChildElement(): Element | null | undefined { - return getLastChildElement(this.getTiptapEditor() as HTMLElement); + private getEditorLastChildElement(): Element { + return getLastChildElement(this.getTiptapEditor() as HTMLElement)!; } } diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index 6289ab8e45..44e327bf1e 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -644,18 +644,40 @@ describe('RichTextEditor', () => { }); describe('Absolute link interactions in the editor', () => { - it('should change the text to "nimble-anchor" tag when it is a valid absolute link', async () => { - await pageObject.setEditorTextContent( - 'https://nimble.ni.dev/ ' - ); + describe('various absolute links without other nodes and marks', () => { + const supportedAbsoluteLink: { name: string }[] = [ + { name: 'https://nimble.ni.dev/ ' }, + { name: 'HTTPS://NIMBLE.NI.DEV ' }, + { name: 'HttPS://NIMBLE.ni.DEV ' }, + { name: 'http://nimble.ni.dev/ ' }, + { name: 'HTTP://NIMBLE.NI.DEV ' }, + { name: 'HttP://nimble.NI.dev ' }, + ]; - expect(pageObject.getEditorTagNames()).toEqual([ - 'P', - 'NIMBLE-ANCHOR' - ]); - expect(pageObject.getEditorLeafContents()).toEqual([ - 'https://nimble.ni.dev/' - ]); + const focused: string[] = []; + const disabled: string[] = []; + for (const value of supportedAbsoluteLink) { + const specType = getSpecTypeByNamedList( + value, + focused, + disabled + ); + specType( + `should change the ${value.name} to "nimble-anchor" tag when it is a valid absolute link`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + async () => { + await pageObject.setEditorTextContent(value.name); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'P', + 'NIMBLE-ANCHOR' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + value.name.slice(0, -1) + ]); + } + ); + } }); it('should have the right attributes to the nimble-anchor', async () => { diff --git a/packages/nimble-components/src/rich-text/models/markdown-parser.ts b/packages/nimble-components/src/rich-text/models/markdown-parser.ts index b137a1b6c6..32eb7d2d90 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-parser.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-parser.ts @@ -51,7 +51,7 @@ export class RichTextMarkdownParser { 'autolink' ]); - supportedTokenizerRules.validateLink = href => /^https?:\/\//.test(href); + supportedTokenizerRules.validateLink = href => /^https?:\/\//i.test(href); return new MarkdownParser( this.updatedSchema, diff --git a/packages/nimble-components/src/rich-text/models/testing/markdown-parser-utils.ts b/packages/nimble-components/src/rich-text/models/testing/markdown-parser-utils.ts index 34e098abdd..71437195fc 100644 --- a/packages/nimble-components/src/rich-text/models/testing/markdown-parser-utils.ts +++ b/packages/nimble-components/src/rich-text/models/testing/markdown-parser-utils.ts @@ -20,7 +20,7 @@ export const getLastChildElementAttribute = ( attribute: string, doc: DocumentFragment | HTMLElement ): string => { - return getLastChildElement(doc)?.getAttribute(attribute) || ''; + return getLastChildElement(doc)?.getAttribute(attribute) ?? ''; }; export function getLastChildElement( @@ -29,7 +29,7 @@ export function getLastChildElement( let lastElement = doc.lastElementChild; while (lastElement?.lastElementChild) { - lastElement = lastElement?.lastElementChild; + lastElement = lastElement.lastElementChild; } return lastElement; } From 946b41866aba7c5abe26ea40998d1d11c306ceca Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Mon, 11 Sep 2023 10:21:00 +0530 Subject: [PATCH 40/56] Fix lint --- .../src/rich-text/editor/tests/rich-text-editor.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index 44e327bf1e..a1e1917219 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -651,7 +651,7 @@ describe('RichTextEditor', () => { { name: 'HttPS://NIMBLE.ni.DEV ' }, { name: 'http://nimble.ni.dev/ ' }, { name: 'HTTP://NIMBLE.NI.DEV ' }, - { name: 'HttP://nimble.NI.dev ' }, + { name: 'HttP://nimble.NI.dev ' } ]; const focused: string[] = []; From 42c7b6816b60dfa8d173102f5a603e20bb36e491 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:54:41 +0530 Subject: [PATCH 41/56] Rendering "a" tag in place of "nimble-anchor" in editor. --- .../src/rich-text/editor/index.ts | 6 ++- .../src/rich-text/editor/styles.ts | 8 ++-- .../editor/tests/rich-text-editor.spec.ts | 42 +++++++++---------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index 407859d988..c8bae6b3ac 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -363,6 +363,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { rel: 'noopener noreferrer', target: null }, + autolink: true, openOnClick: false, linkOnPaste: false, validate: href => /^https?:\/\//i.test(href) @@ -389,8 +390,9 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { // HTMLAttribute cannot be in camelCase as we want to match it with the name in Tiptap // eslint-disable-next-line @typescript-eslint/naming-convention renderHTML({ HTMLAttributes }) { - HTMLAttributes.tabindex = '-1'; - return [anchorTag, HTMLAttributes]; + // The below 'a' tag should be replaced with 'nimble-anchor' once the below issue is fixed. + // https://github.com/ni/nimble/issues/1502 + return ['a', HTMLAttributes]; } }); } diff --git a/packages/nimble-components/src/rich-text/editor/styles.ts b/packages/nimble-components/src/rich-text/editor/styles.ts index df5c537073..e236bb34ec 100644 --- a/packages/nimble-components/src/rich-text/editor/styles.ts +++ b/packages/nimble-components/src/rich-text/editor/styles.ts @@ -12,7 +12,8 @@ import { failColor, iconSize, smallDelay, - standardPadding + standardPadding, + linkFontColor } from '../../theme-provider/design-tokens'; import { styles as errorStyles } from '../../patterns/error/styles'; @@ -188,12 +189,13 @@ export const styles = css` color: ${controlLabelDisabledFontColor}; } - nimble-anchor { + .ProseMirror a { + color: ${linkFontColor}; white-space: normal; ${ /** * Restricting the pointer events for the following reasons: - * 1. nimble-anchor inside a "contenteditable" div is not working as native HTML anchor tag. + * 1. If rendering 'nimble-anchor' inside a "contenteditable" div is not working as native HTML anchor tag. * i.e. clicking on the link opens in the same tab whereas the default behavior of native HTML anchor * tag is not clickable inside "contenteditable" div. * Issue link: https://github.com/ni/nimble/issues/1502 diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index a1e1917219..cda74e7818 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -663,14 +663,14 @@ describe('RichTextEditor', () => { disabled ); specType( - `should change the ${value.name} to "nimble-anchor" tag when it is a valid absolute link`, + `should change the ${value.name} to "a" tag when it is a valid absolute link`, // eslint-disable-next-line @typescript-eslint/no-loop-func async () => { await pageObject.setEditorTextContent(value.name); expect(pageObject.getEditorTagNames()).toEqual([ 'P', - 'NIMBLE-ANCHOR' + 'A' ]); expect(pageObject.getEditorLeafContents()).toEqual([ value.name.slice(0, -1) @@ -680,7 +680,7 @@ describe('RichTextEditor', () => { } }); - it('should have the right attributes to the nimble-anchor', async () => { + it('should have the right attributes to the "a" tag', async () => { await pageObject.setEditorTextContent( 'https://nimble.ni.dev/ ' ); @@ -691,9 +691,6 @@ describe('RichTextEditor', () => { expect(pageObject.getEditorLastChildAttribute('rel')).toBe( 'noopener noreferrer' ); - expect(pageObject.getEditorLastChildAttribute('tabindex')).toBe( - '-1' - ); }); it('should not affect bold formatting on the link in editor', async () => { @@ -704,8 +701,8 @@ describe('RichTextEditor', () => { expect(pageObject.getEditorTagNamesWithClosingTags()).toEqual([ 'P', - 'NIMBLE-ANCHOR', - '/NIMBLE-ANCHOR', + 'A', + '/A', 'STRONG', '/STRONG', '/P' @@ -724,8 +721,8 @@ describe('RichTextEditor', () => { expect(pageObject.getEditorTagNamesWithClosingTags()).toEqual([ 'P', - 'NIMBLE-ANCHOR', - '/NIMBLE-ANCHOR', + 'A', + '/A', 'EM', '/EM', '/P' @@ -746,7 +743,7 @@ describe('RichTextEditor', () => { 'UL', 'LI', 'P', - 'NIMBLE-ANCHOR' + 'A' ]); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://nimble.ni.dev/' @@ -763,7 +760,7 @@ describe('RichTextEditor', () => { 'OL', 'LI', 'P', - 'NIMBLE-ANCHOR' + 'A' ]); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://nimble.ni.dev/' @@ -774,6 +771,7 @@ describe('RichTextEditor', () => { const notSupportedAbsoluteLink: { name: string }[] = [ { name: 'ftp://example.com/files/document.pdf ' }, { name: 'mailto:info@example.com ' }, + { name: 'info@example.com ' }, { name: 'file:///path/to/local/file.txt ' }, { name: 'tel:+1234567890 ' }, // eslint-disable-next-line no-script-url @@ -865,12 +863,12 @@ describe('RichTextEditor', () => { describe('Absolute link markdown tests', () => { describe('asserting rendered links in the editor', () => { - it('absolute link markdown string to "nimble-anchor" tags with the link as the text content', () => { + it('absolute link markdown string to "a" tags with the link as the text content', () => { element.setMarkdown(''); expect(pageObject.getEditorTagNames()).toEqual([ 'P', - 'NIMBLE-ANCHOR' + 'A' ]); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://nimble.ni.dev/' @@ -880,14 +878,14 @@ describe('RichTextEditor', () => { ); }); - it('bulleted list with absolute links markdown string to "ul", "li" and "nimble-anchor" tags', () => { + it('bulleted list with absolute links markdown string to "ul", "li" and "a" tags', () => { element.setMarkdown('* '); expect(pageObject.getEditorTagNames()).toEqual([ 'UL', 'LI', 'P', - 'NIMBLE-ANCHOR' + 'A' ]); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://nimble.ni.dev/' @@ -897,14 +895,14 @@ describe('RichTextEditor', () => { ); }); - it('numbered list with absolute links markdown string to "ol", "li" and "nimble-anchor" tags', () => { + it('numbered list with absolute links markdown string to "ol", "li" and "a" tags', () => { element.setMarkdown('1. '); expect(pageObject.getEditorTagNames()).toEqual([ 'OL', 'LI', 'P', - 'NIMBLE-ANCHOR' + 'A' ]); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://nimble.ni.dev/' @@ -919,7 +917,7 @@ describe('RichTextEditor', () => { expect(pageObject.getEditorTagNames()).toEqual([ 'P', - 'NIMBLE-ANCHOR' + 'A' ]); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://nimble.ni.dev/' @@ -934,7 +932,7 @@ describe('RichTextEditor', () => { expect(pageObject.getEditorTagNames()).toEqual([ 'P', - 'NIMBLE-ANCHOR' + 'A' ]); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://nimble.ni.dev/' @@ -949,7 +947,7 @@ describe('RichTextEditor', () => { expect(pageObject.getEditorTagNames()).toEqual([ 'P', - 'NIMBLE-ANCHOR' + 'A' ]); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://nimble.ni.dev/' @@ -964,7 +962,7 @@ describe('RichTextEditor', () => { expect(pageObject.getEditorTagNames()).toEqual([ 'P', - 'NIMBLE-ANCHOR' + 'A' ]); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://**nimble**.ni.dev/' From 8d3d81e7e1cc635374a1ade5b09229fe9706ca5a Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Mon, 11 Sep 2023 15:15:22 +0530 Subject: [PATCH 42/56] Fix lint --- .../editor/tests/rich-text-editor.spec.ts | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index cda74e7818..46cc57130e 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -866,10 +866,7 @@ describe('RichTextEditor', () => { it('absolute link markdown string to "a" tags with the link as the text content', () => { element.setMarkdown(''); - expect(pageObject.getEditorTagNames()).toEqual([ - 'P', - 'A' - ]); + expect(pageObject.getEditorTagNames()).toEqual(['P', 'A']); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://nimble.ni.dev/' ]); @@ -915,10 +912,7 @@ describe('RichTextEditor', () => { it('absolute links in bold markdown string should not be parsed to "strong" tag', () => { element.setMarkdown('****'); - expect(pageObject.getEditorTagNames()).toEqual([ - 'P', - 'A' - ]); + expect(pageObject.getEditorTagNames()).toEqual(['P', 'A']); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://nimble.ni.dev/' ]); @@ -930,10 +924,7 @@ describe('RichTextEditor', () => { it('absolute links in italics markdown string should not be parsed to "em" tag', () => { element.setMarkdown('**'); - expect(pageObject.getEditorTagNames()).toEqual([ - 'P', - 'A' - ]); + expect(pageObject.getEditorTagNames()).toEqual(['P', 'A']); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://nimble.ni.dev/' ]); @@ -945,10 +936,7 @@ describe('RichTextEditor', () => { it('absolute links in both bold and italics markdown string should not be parsed to "strong" and "em" tag', () => { element.setMarkdown('______'); - expect(pageObject.getEditorTagNames()).toEqual([ - 'P', - 'A' - ]); + expect(pageObject.getEditorTagNames()).toEqual(['P', 'A']); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://nimble.ni.dev/' ]); @@ -960,10 +948,7 @@ describe('RichTextEditor', () => { it('adding marks like bold inside absolute links should not be parsed to "strong" tag', () => { element.setMarkdown(''); - expect(pageObject.getEditorTagNames()).toEqual([ - 'P', - 'A' - ]); + expect(pageObject.getEditorTagNames()).toEqual(['P', 'A']); expect(pageObject.getEditorLeafContents()).toEqual([ 'https://**nimble**.ni.dev/' ]); From b625927115776a48782ed46bdb7ffc7e7fe28441 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Tue, 12 Sep 2023 09:38:16 +0530 Subject: [PATCH 43/56] Updates to linking styles --- .../src/rich-text/editor/styles.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/styles.ts b/packages/nimble-components/src/rich-text/editor/styles.ts index e236bb34ec..647203eee2 100644 --- a/packages/nimble-components/src/rich-text/editor/styles.ts +++ b/packages/nimble-components/src/rich-text/editor/styles.ts @@ -194,21 +194,16 @@ export const styles = css` white-space: normal; ${ /** - * Restricting the pointer events for the following reasons: - * 1. If rendering 'nimble-anchor' inside a "contenteditable" div is not working as native HTML anchor tag. - * i.e. clicking on the link opens in the same tab whereas the default behavior of native HTML anchor - * tag is not clickable inside "contenteditable" div. - * Issue link: https://github.com/ni/nimble/issues/1502 - * 2. Restricting the user from opening a link using the right-click context menu: If the user manually edits - * the link's text content, the 'href' attribute of the anchor tag will not be updated. If they attempt to open it using - * the right-click context menu with 'Open in new tab/window,' it will still navigate to the link specified - * in the 'href' attribute, which may create unnecessary confusion while trying to open the link + * Restricting the user from opening a link using the right-click context menu: If the user manually edits + * the link's text content, the 'href' attribute of the anchor tag will not be updated. If they attempt to open it using + * the right-click context menu with 'Open in new tab/window,' it will still navigate to the link specified + * in the 'href' attribute, which may create unnecessary confusion while trying to open the link. */ '' } pointer-events: none; } - :host([disabled]) nimble-anchor::part(control) { + :host([disabled]) .ProseMirror a { color: ${bodyDisabledFontColor}; fill: currentcolor; cursor: default; From 1c3cbb487b89e34db7d87e908e2f12494ce18b58 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Tue, 12 Sep 2023 09:42:41 +0530 Subject: [PATCH 44/56] removing heights from the storybook docs --- .../rich-text/editor/tests/rich-text-editor-matrix.stories.ts | 2 +- .../src/rich-text/editor/tests/rich-text-editor.stories.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts index b3a08047bb..b847c05c73 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts @@ -63,7 +63,7 @@ const component = ( ${() => footerHiddenName} ${() => errorStateName} ${() => placeholderName} ${() => disabledName}

<${richTextEditorTag} - style="margin: 5px 0px; width: 500px; height: 100px" + style="margin: 5px 0px; width: 500px;" ?disabled="${() => disabled}" ?footer-hidden="${() => footerHidden}" ?error-visible="${() => isError}" diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts index 9d9a42f3f7..9418f4cb2c 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts @@ -76,7 +76,6 @@ const metadata: Meta = { })} <${richTextEditorTag} ${ref('editorRef')} - style="height: 160px" data-unused="${x => x.setMarkdownData(x)}" ?disabled="${x => x.disabled}" ?footer-hidden="${x => x.footerHidden}" From 676e4ac32e8e15d1e55fe08015b2de8a76e6507d Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:58:33 +0530 Subject: [PATCH 45/56] Normalization of link texts to render encoded and non-ascii character URLs --- .../src/rich-text/models/markdown-parser.ts | 5 + .../models/tests/markdown-parser.spec.ts | 188 ++++++++++++++++-- 2 files changed, 180 insertions(+), 13 deletions(-) diff --git a/packages/nimble-components/src/rich-text/models/markdown-parser.ts b/packages/nimble-components/src/rich-text/models/markdown-parser.ts index 32eb7d2d90..3767b3ced0 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-parser.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-parser.ts @@ -53,6 +53,11 @@ export class RichTextMarkdownParser { supportedTokenizerRules.validateLink = href => /^https?:\/\//i.test(href); + // To achieve rendering the encoded characters, non-ASCII characters, emojis, etc., as they are, + // bypassing the default normalization of link text in markdown-it because we only support "AutoLink" in markdown format. + // "normalizeLinkText" method reference in markdown-it: https://github.com/markdown-it/markdown-it/blob/2b6cac25823af011ff3bc7628bc9b06e483c5a08/lib/index.js#L67C1-L86C2 + supportedTokenizerRules.normalizeLinkText = url => url; + return new MarkdownParser( this.updatedSchema, supportedTokenizerRules, diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index 3dd147d093..aabae51244 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -212,18 +212,180 @@ describe('Markdown parser', () => { }); describe('Absolute link', () => { - it('absolute link markdown string to "nimble-anchor" tags with the link as the text content', () => { - const doc = RichTextMarkdownParser.parseMarkdownToDOM( - '' - ); + describe('various valid absolute links should render same as in the markdown', () => { + const supportedAbsoluteLink: { + name: string, + validLink: string + }[] = [ + { + name: 'Lowercase HTTPS URL', + validLink: '' + }, + { + name: 'Uppercase HTTPS URL', + validLink: '' + }, + { + name: 'Mixed case HTTPS URL', + validLink: '' + }, + { + name: 'Lowercase HTTP URL', + validLink: '' + }, + { + name: 'Uppercase HTTP URL', + validLink: '' + }, + { + name: 'Mixed case HTTP URL', + validLink: '' + }, + { + name: 'URL with query params & special characters', + validLink: + '' + }, + { + name: 'Whitespace encoded URL', + validLink: '' + }, + { + name: 'Question mark encoded URL', + validLink: + '' + }, + { + name: 'Emoji encoded URL', + validLink: + '' + }, + { + name: 'Ampersand encoded URL', + validLink: + '' + }, + { + name: 'Non-latin encoded URL', + validLink: + '' + }, + { + name: 'URL with Fragment Identifier', + validLink: '' + }, + { + name: 'URL with Port Number', + validLink: '' + } + ]; - expect(getTagsFromElement(doc)).toEqual(['P', 'NIMBLE-ANCHOR']); - expect(getLeafContentsFromElement(doc)).toEqual([ - 'https://nimble.ni.dev/' - ]); - expect(getLastChildElementAttribute('href', doc)).toBe( - 'https://nimble.ni.dev/' - ); + const focused: string[] = []; + const disabled: string[] = []; + for (const value of supportedAbsoluteLink) { + const specType = getSpecTypeByNamedList( + value, + focused, + disabled + ); + specType( + `${value.name} to "nimble-anchor" tags with the link as the text content`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + value.validLink + ); + const renderedLink = value.validLink.slice(1, -1); + + expect(getTagsFromElement(doc)).toEqual([ + 'P', + 'NIMBLE-ANCHOR' + ]); + expect(getLeafContentsFromElement(doc)).toEqual([ + renderedLink + ]); + expect( + getLastChildElementAttribute('href', doc) + ).toBe(renderedLink); + } + ); + } + }); + + describe('various absolute links with non-ASCII (IRI) characters within it', () => { + const supportedAbsoluteLink: { + name: string, + validLink: string, + encodeURL: string + }[] = [ + { + name: 'Emoji', + validLink: '', + encodeURL: 'https://example.com/smiley%F0%9F%98%80.html' + }, + { + name: 'Basic IRI characters', + validLink: '', + encodeURL: 'https://example.com/%C3%A9l%C3%A8ve.html' + }, + { + name: 'Non-Latin Scripts', + validLink: '', + encodeURL: + 'https://example.com/%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80.html' + }, + { + name: 'Math symbols', + validLink: '', + encodeURL: 'https://example.com/%E2%88%9A2.html' + }, + { + name: 'Special symbols', + validLink: '', + encodeURL: 'https://example.com/%E2%99%A5-music.html' + }, + { + name: 'Accented Characters', + validLink: '', + encodeURL: 'https://example.com/espa%C3%B1a.html' + }, + { + name: 'Japanese Characters', + validLink: '', + encodeURL: 'https://example.com/%E6%9D%B1%E4%BA%AC.html' + } + ]; + + const focused: string[] = []; + const disabled: string[] = []; + for (const value of supportedAbsoluteLink) { + const specType = getSpecTypeByNamedList( + value, + focused, + disabled + ); + specType( + `${value.name} to "nimble-anchor" tags with the non-ASCII characters as the text content and encoded as their href`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + value.validLink + ); + const renderedLink = value.validLink.slice(1, -1); + + expect(getTagsFromElement(doc)).toEqual([ + 'P', + 'NIMBLE-ANCHOR' + ]); + expect(getLeafContentsFromElement(doc)).toEqual([ + renderedLink + ]); + expect( + getLastChildElementAttribute('href', doc) + ).toBe(value.encodeURL); + } + ); + } }); it('absolute link should add "rel" attribute', () => { @@ -331,7 +493,7 @@ describe('Markdown parser', () => { }); describe('various absolute links with different schemas other than https/http should be render as unchanged strings', () => { - const notSupportedAbsoluteLink: { name: string }[] = [ + const differentProtocolLinks: { name: string }[] = [ { name: '' }, { name: '' }, { name: '' }, @@ -353,7 +515,7 @@ describe('Markdown parser', () => { const focused: string[] = []; const disabled: string[] = []; - for (const value of notSupportedAbsoluteLink) { + for (const value of differentProtocolLinks) { const specType = getSpecTypeByNamedList( value, focused, From 282f22b1edc8d0fb765c7d801681bd8606bdb0ad Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Tue, 12 Sep 2023 17:24:01 +0530 Subject: [PATCH 46/56] Added few more link tests --- .../models/tests/markdown-parser.spec.ts | 91 +++++++++++++++---- 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index aabae51244..438a8875c9 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -242,9 +242,9 @@ describe('Markdown parser', () => { validLink: '' }, { - name: 'URL with query params & special characters', + name: 'URL with reserved characters', validLink: - '' + '' }, { name: 'Whitespace encoded URL', @@ -274,6 +274,10 @@ describe('Markdown parser', () => { name: 'URL with Fragment Identifier', validLink: '' }, + { + name: 'URL with marks', + validLink: '' + }, { name: 'URL with Port Number', validLink: '' @@ -323,6 +327,36 @@ describe('Markdown parser', () => { validLink: '', encodeURL: 'https://example.com/smiley%F0%9F%98%80.html' }, + { + name: 'Square brackets', + validLink: '', + encodeURL: 'https://example.com/%5Bpage%5D/index.html' + }, + { + name: 'Backslashes', + validLink: '', + encodeURL: 'https://example.com%5Cpath%5Cto%5Cresource' + }, + { + name: 'Open and close braces', + validLink: '', + encodeURL: 'https://example.com/%7Bpage%7D/index.html' + }, + { + name: 'Pipe', + validLink: '', + encodeURL: 'https://example.com/page%7C/index.html' + }, + { + name: 'Caret', + validLink: '', + encodeURL: 'https://example.com/page%5E/index.html' + }, + { + name: 'Percent', + validLink: '', + encodeURL: 'https://example.com/page%25/index.html' + }, { name: 'Basic IRI characters', validLink: '', @@ -478,20 +512,6 @@ describe('Markdown parser', () => { ); }); - it('adding marks like bold inside absolute links should not be parsed to "strong" HTML tag', () => { - const doc = RichTextMarkdownParser.parseMarkdownToDOM( - '' - ); - - expect(getTagsFromElement(doc)).toEqual(['P', 'NIMBLE-ANCHOR']); - expect(getLeafContentsFromElement(doc)).toEqual([ - 'https://**nimble**.ni.dev/' - ]); - expect(getLastChildElementAttribute('href', doc)).toBe( - 'https://**nimble**.ni.dev/' - ); - }); - describe('various absolute links with different schemas other than https/http should be render as unchanged strings', () => { const differentProtocolLinks: { name: string }[] = [ { name: '' }, @@ -522,7 +542,44 @@ describe('Markdown parser', () => { disabled ); specType( - `string "${value.name}" renders as plain text "${value.name}" within paragraph tag`, + `string "${value.name}" renders as plain text within paragraph tag`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + value.name + ); + + expect(getTagsFromElement(doc)).toEqual(['P']); + expect(getLeafContentsFromElement(doc)).toEqual([ + value.name + ]); + } + ); + } + }); + + describe('various unsafe characters in an absolute link', () => { + const notSupportedAbsoluteLink: { + name: string + }[] = [ + { name: '>' }, + { name: '' }, + { name: 'http://www.example.com/' }, + { name: '' }, + { name: '' }, + { name: ' { const doc = RichTextMarkdownParser.parseMarkdownToDOM( From 2eba73d25e3534eb70254d77268c772aa7aed073 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Tue, 12 Sep 2023 18:26:36 +0530 Subject: [PATCH 47/56] Minor code changes --- .../nimble-components/src/rich-text/editor/index.ts | 2 +- .../nimble-components/src/rich-text/editor/styles.ts | 2 +- .../rich-text/editor/tests/rich-text-editor.spec.ts | 12 ++++++++---- .../src/rich-text/models/markdown-parser.ts | 4 ++-- .../rich-text/models/tests/markdown-parser.spec.ts | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index c8bae6b3ac..0d9d5e71ba 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -391,7 +391,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { // eslint-disable-next-line @typescript-eslint/naming-convention renderHTML({ HTMLAttributes }) { // The below 'a' tag should be replaced with 'nimble-anchor' once the below issue is fixed. - // https://github.com/ni/nimble/issues/1502 + // https://github.com/ni/nimble/issues/1516 return ['a', HTMLAttributes]; } }); diff --git a/packages/nimble-components/src/rich-text/editor/styles.ts b/packages/nimble-components/src/rich-text/editor/styles.ts index 647203eee2..306448b40e 100644 --- a/packages/nimble-components/src/rich-text/editor/styles.ts +++ b/packages/nimble-components/src/rich-text/editor/styles.ts @@ -194,7 +194,7 @@ export const styles = css` white-space: normal; ${ /** - * Restricting the user from opening a link using the right-click context menu: If the user manually edits + * Setting 'pointer-events: none;' to restrict the user from opening a link using the right-click context menu: If the user manually edits * the link's text content, the 'href' attribute of the anchor tag will not be updated. If they attempt to open it using * the right-click context menu with 'Open in new tab/window,' it will still navigate to the link specified * in the 'href' attribute, which may create unnecessary confusion while trying to open the link. diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index 46cc57130e..c5d91da4eb 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -651,7 +651,11 @@ describe('RichTextEditor', () => { { name: 'HttPS://NIMBLE.ni.DEV ' }, { name: 'http://nimble.ni.dev/ ' }, { name: 'HTTP://NIMBLE.NI.DEV ' }, - { name: 'HttP://nimble.NI.dev ' } + { name: 'HttP://nimble.NI.dev ' }, + { name: 'https://www.example.com/path/equals=ampersand&question?dollar$plus+comma,At@semicolon; ' }, + { name: 'https://example.com/my%20page.html ' }, + { name: 'https://example.com/smiley😀.html ' }, + { name: 'https://example.com/пример.html ' }, ]; const focused: string[] = []; @@ -767,8 +771,8 @@ describe('RichTextEditor', () => { ]); }); - describe('various absolute links with different schemas other than https/http should be render as unchanged strings', () => { - const notSupportedAbsoluteLink: { name: string }[] = [ + describe('various absolute links with different protocols other than https/http should be render as unchanged strings', () => { + const differentProtocolLinks: { name: string }[] = [ { name: 'ftp://example.com/files/document.pdf ' }, { name: 'mailto:info@example.com ' }, { name: 'info@example.com ' }, @@ -793,7 +797,7 @@ describe('RichTextEditor', () => { const focused: string[] = []; const disabled: string[] = []; - for (const value of notSupportedAbsoluteLink) { + for (const value of differentProtocolLinks) { const specType = getSpecTypeByNamedList( value, focused, diff --git a/packages/nimble-components/src/rich-text/models/markdown-parser.ts b/packages/nimble-components/src/rich-text/models/markdown-parser.ts index 3767b3ced0..34a95d0345 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-parser.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-parser.ts @@ -53,8 +53,8 @@ export class RichTextMarkdownParser { supportedTokenizerRules.validateLink = href => /^https?:\/\//i.test(href); - // To achieve rendering the encoded characters, non-ASCII characters, emojis, etc., as they are, - // bypassing the default normalization of link text in markdown-it because we only support "AutoLink" in markdown format. + // In order to display encoded characters, non-ASCII characters, emojis, and other special characters in their original form, + // we bypass the default normalization of link text in markdown-it. This is done because we support only "AutoLink" feature in CommonMark flavor. // "normalizeLinkText" method reference in markdown-it: https://github.com/markdown-it/markdown-it/blob/2b6cac25823af011ff3bc7628bc9b06e483c5a08/lib/index.js#L67C1-L86C2 supportedTokenizerRules.normalizeLinkText = url => url; diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index 438a8875c9..6c31bf7cf6 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -512,7 +512,7 @@ describe('Markdown parser', () => { ); }); - describe('various absolute links with different schemas other than https/http should be render as unchanged strings', () => { + describe('various absolute links with different protocols other than https/http should be render as unchanged strings', () => { const differentProtocolLinks: { name: string }[] = [ { name: '' }, { name: '' }, From 597de3cb771b22bafdf243ca7c3984d592529421 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Tue, 12 Sep 2023 18:31:14 +0530 Subject: [PATCH 48/56] Fix lint --- .../src/rich-text/editor/tests/rich-text-editor.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index c5d91da4eb..09d242e917 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -652,10 +652,12 @@ describe('RichTextEditor', () => { { name: 'http://nimble.ni.dev/ ' }, { name: 'HTTP://NIMBLE.NI.DEV ' }, { name: 'HttP://nimble.NI.dev ' }, - { name: 'https://www.example.com/path/equals=ampersand&question?dollar$plus+comma,At@semicolon; ' }, + { + name: 'https://www.example.com/path/equals=ampersand&question?dollar$plus+comma,At@semicolon; ' + }, { name: 'https://example.com/my%20page.html ' }, { name: 'https://example.com/smiley😀.html ' }, - { name: 'https://example.com/пример.html ' }, + { name: 'https://example.com/пример.html ' } ]; const focused: string[] = []; From 38739e398bff4784a808de601bc279d186c6420b Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Tue, 12 Sep 2023 19:03:37 +0530 Subject: [PATCH 49/56] Fix test case fail in editor --- .../src/rich-text/editor/tests/rich-text-editor.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index 09d242e917..b2e66d6cd2 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -653,7 +653,7 @@ describe('RichTextEditor', () => { { name: 'HTTP://NIMBLE.NI.DEV ' }, { name: 'HttP://nimble.NI.dev ' }, { - name: 'https://www.example.com/path/equals=ampersand&question?dollar$plus+comma,At@semicolon; ' + name: 'https://www.example.com/path/=equals&ersand?question$dollar+plus,comma;semicolon@At ' }, { name: 'https://example.com/my%20page.html ' }, { name: 'https://example.com/smiley😀.html ' }, From 7169a06142fcfe77ce51596b6c2818ccbe257f84 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Wed, 13 Sep 2023 10:27:09 +0530 Subject: [PATCH 50/56] Update the code comment to explain the rationale for the same. --- .../rich-text/editor/tests/rich-text-editor.spec.ts | 1 + .../src/rich-text/models/markdown-serializer.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index b2e66d6cd2..9babcc1010 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -679,6 +679,7 @@ describe('RichTextEditor', () => { 'A' ]); expect(pageObject.getEditorLeafContents()).toEqual([ + // Name without the trailing space used by the editor to trigger conversion to a link value.name.slice(0, -1) ]); } diff --git a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts index f48e4ebeb9..198c9cd6f8 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts @@ -52,8 +52,17 @@ export class RichTextMarkdownSerializer { const marks = { italic: defaultMarkdownSerializer.marks.em!, bold: defaultMarkdownSerializer.marks.strong!, - // Autolink markdown in CommonMark flavor: https://spec.commonmark.org/0.30/#autolinks - // ProseMirror model reference: https://github.com/ProseMirror/prosemirror-markdown/blob/c7210d0e55c82bfb0b2f7cba5dffe804575fafb3/src/to_markdown.ts#L3C1-L26C2 + /** + * When a user inserts an absolute link into the editor and then modifies it, the 'defaultMarkdownSerializer.marks.link' function + * will detect whether it should be serialized as an autolink () or a regular link ([text](url)) in Markdown format by + * comparing the link text with 'href'. Since our markdown-parser only supports the autolink format, we need to ensure that the + * serializer also only supports autolink. Unfortunately, prosemirror-markdown does not offer a built-in way to update the + * 'defaultMarkdownSerializer' for this purpose. Therefore, we had to create a modified implementation to enable support for + * only autolink in serialization. + + * Autolink markdown in CommonMark flavor: https://spec.commonmark.org/0.30/#autolinks + * ProseMirror model reference: https://github.com/ProseMirror/prosemirror-markdown/blob/c7210d0e55c82bfb0b2f7cba5dffe804575fafb3/src/to_markdown.ts#L3C1-L26C2 + */ link: { open: '<', close: '>', From 8de836102f52de75111ba149dd77611578324896 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:40:26 +0530 Subject: [PATCH 51/56] Resolve PR comments --- .../src/rich-text/editor/index.ts | 9 ++++++ .../src/rich-text/editor/styles.ts | 11 +++++++ .../editor/tests/rich-text-editor.spec.ts | 3 +- .../src/rich-text/models/markdown-parser.ts | 15 +++++++-- .../rich-text/models/markdown-serializer.ts | 24 ++++++++------ .../models/tests/markdown-parser.spec.ts | 5 +++ .../models/tests/markdown-serializer.spec.ts | 31 +++++++------------ 7 files changed, 65 insertions(+), 33 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index 0d9d5e71ba..88f394e722 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -365,6 +365,8 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { }, autolink: true, openOnClick: false, + // linkOnPaste can be enabled when hyperlink support added + // See: https://github.com/ni/nimble/issues/1527 linkOnPaste: false, validate: href => /^https?:\/\//i.test(href) }) @@ -382,9 +384,16 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { */ private getCustomLinkExtension(): Mark { return Link.extend({ + // Excludes can be removed enabled when hyperlink support added + // See: https://github.com/ni/nimble/issues/1527 excludes: '_', + // Inclusive can be updated when hyperlink support added + // See: https://github.com/ni/nimble/issues/1527 inclusive: false, parseHTML() { + // As the markdown parser parses the links in the markdown string to 'nimble-anchor' tags, + // the link extension should identify that all 'nimble-anchor' tags are links. Therefore, it should + // return the 'nimble-anchor' tag. return [{ tag: anchorTag }]; }, // HTMLAttribute cannot be in camelCase as we want to match it with the name in Tiptap diff --git a/packages/nimble-components/src/rich-text/editor/styles.ts b/packages/nimble-components/src/rich-text/editor/styles.ts index 902207e71e..4d8bc3789e 100644 --- a/packages/nimble-components/src/rich-text/editor/styles.ts +++ b/packages/nimble-components/src/rich-text/editor/styles.ts @@ -189,6 +189,12 @@ export const styles = css` color: ${controlLabelDisabledFontColor}; } + ${ + /** + * Custom anchor stylings can be removed once leveraging 'nimble-anchor' is supported. + * See: https://github.com/ni/nimble/issues/1516 + */ '' + } .ProseMirror a { color: ${linkFontColor}; white-space: normal; @@ -198,6 +204,9 @@ export const styles = css` * the link's text content, the 'href' attribute of the anchor tag will not be updated. If they attempt to open it using * the right-click context menu with 'Open in new tab/window,' it will still navigate to the link specified * in the 'href' attribute, which may create unnecessary confusion while trying to open the link. + * + * Using pointer-events: none to disable link interactions can be removed when hyperlink support is added. + * see: https://github.com/ni/nimble/issues/1527 */ '' } pointer-events: none; @@ -209,6 +218,8 @@ export const styles = css` cursor: default; } + ${/** End of anchor styles */ ''} + .footer-section { display: flex; justify-content: space-between; diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index f06162007b..9b55942546 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -679,6 +679,7 @@ describe('RichTextEditor', () => { }, { name: 'https://example.com/my%20page.html ' }, { name: 'https://example.com/smiley😀.html ' }, + { name: 'https://www.😀.com ' }, { name: 'https://example.com/пример.html ' } ]; @@ -709,7 +710,7 @@ describe('RichTextEditor', () => { } }); - it('should have the right attributes to the "a" tag', async () => { + it('the "a" tag should have href and rel attributes', async () => { await pageObject.setEditorTextContent( 'https://nimble.ni.dev/ ' ); diff --git a/packages/nimble-components/src/rich-text/models/markdown-parser.ts b/packages/nimble-components/src/rich-text/models/markdown-parser.ts index 446d03496a..392fc607e4 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-parser.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-parser.ts @@ -54,9 +54,14 @@ export class RichTextMarkdownParser { supportedTokenizerRules.validateLink = href => /^https?:\/\//i.test(href); - // In order to display encoded characters, non-ASCII characters, emojis, and other special characters in their original form, - // we bypass the default normalization of link text in markdown-it. This is done because we support only "AutoLink" feature in CommonMark flavor. - // "normalizeLinkText" method reference in markdown-it: https://github.com/markdown-it/markdown-it/blob/2b6cac25823af011ff3bc7628bc9b06e483c5a08/lib/index.js#L67C1-L86C2 + /** + * In order to display encoded characters, non-ASCII characters, emojis, and other special characters in their original form, + * we bypass the default normalization of link text in markdown-it. This is done because we support only "AutoLink" feature in CommonMark flavor. + * "normalizeLinkText" method reference in markdown-it: https://github.com/markdown-it/markdown-it/blob/2b6cac25823af011ff3bc7628bc9b06e483c5a08/lib/index.js#L67C1-L86C2 + * + * We can use the default normalization once hyperlink support is added. + * See: https://github.com/ni/nimble/issues/1527 + */ supportedTokenizerRules.normalizeLinkText = url => url; return new MarkdownParser( @@ -75,7 +80,11 @@ export class RichTextMarkdownParser { href: {}, rel: { default: 'noopener noreferrer' } }, + // Inclusive can be updated when hyperlink support added + // See: https://github.com/ni/nimble/issues/1527 inclusive: false, + // Excludes can be removed enabled when hyperlink support added + // See: https://github.com/ni/nimble/issues/1527 excludes: '_', toDOM(node) { return [ diff --git a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts index 198c9cd6f8..b6e8a145ef 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts @@ -53,16 +53,20 @@ export class RichTextMarkdownSerializer { italic: defaultMarkdownSerializer.marks.em!, bold: defaultMarkdownSerializer.marks.strong!, /** - * When a user inserts an absolute link into the editor and then modifies it, the 'defaultMarkdownSerializer.marks.link' function - * will detect whether it should be serialized as an autolink () or a regular link ([text](url)) in Markdown format by - * comparing the link text with 'href'. Since our markdown-parser only supports the autolink format, we need to ensure that the - * serializer also only supports autolink. Unfortunately, prosemirror-markdown does not offer a built-in way to update the - * 'defaultMarkdownSerializer' for this purpose. Therefore, we had to create a modified implementation to enable support for - * only autolink in serialization. - - * Autolink markdown in CommonMark flavor: https://spec.commonmark.org/0.30/#autolinks - * ProseMirror model reference: https://github.com/ProseMirror/prosemirror-markdown/blob/c7210d0e55c82bfb0b2f7cba5dffe804575fafb3/src/to_markdown.ts#L3C1-L26C2 - */ + * When a user inserts an absolute link into the editor and then modifies it, the 'defaultMarkdownSerializer.marks.link' function + * will detect whether it should be serialized as an autolink () or a hyperlink ([text](url)) in Markdown format by + * comparing the link text with 'href'. Since our markdown-parser only supports the autolink format, we need to ensure that the + * serializer also only supports autolink. Unfortunately, prosemirror-markdown does not offer a built-in way to update the + * 'defaultMarkdownSerializer' for this purpose. Therefore, we had to create a modified implementation to enable support for + * only autolink in serialization. This modified implementation will just load the link text content in between '<>' angular brackets + * and ignores the 'href' part. + * + * Autolink markdown in CommonMark flavor: https://spec.commonmark.org/0.30/#autolinks + * ProseMirror model reference: https://github.com/ProseMirror/prosemirror-markdown/blob/c7210d0e55c82bfb0b2f7cba5dffe804575fafb3/src/to_markdown.ts#L3C1-L26C2 + * + * The defaultMarkdownSerializer can be used once hyperlink support is added: + * See: https://github.com/ni/nimble/issues/1527 + */ link: { open: '<', close: '>', diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index a1ad019e66..890a891402 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -327,6 +327,11 @@ describe('Markdown parser', () => { validLink: '', encodeURL: 'https://example.com/smiley%F0%9F%98%80.html' }, + { + name: 'Emoji at the host (punycode encoded)', + validLink: '', + encodeURL: 'https://www.xn--h28h.com' + }, { name: 'Square brackets', validLink: '', diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts index 9dffc7a853..68c6e1c2c8 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts @@ -56,11 +56,9 @@ describe('Markdown serializer', () => { markdown: '*Italics*' }, { - // All links will have `` tag here as an input, since it is a mock editor. - // In the actual editor, links will render as `nimble-anchor` using `renderHTML`. name: 'Link', - html: '

Link

', - markdown: '' + html: '

https://nimble.ni.dev

', + markdown: '' }, { name: 'Bold and Italics', @@ -69,18 +67,18 @@ describe('Markdown serializer', () => { }, { name: 'Link and Bold', - html: '

Link and Bold

', - markdown: '' + html: '

https://nimble.ni.dev

', + markdown: '' }, { name: 'Link and Italics', - html: '

Link and Italics

', - markdown: '' + html: '

https://nimble.ni.dev

', + markdown: '' }, { name: 'Link, Bold and Italics', - html: '

Link, Bold and Italics

', - markdown: '' + html: '

https://nimble.ni.dev

', + markdown: '' }, { name: 'Numbered list', @@ -104,8 +102,8 @@ describe('Markdown serializer', () => { }, { name: 'Numbered list with link', - html: '
  1. Numbered list with link

', - markdown: '1. ' + html: '
  1. https://nimble.ni.dev

', + markdown: '1. ' }, { name: 'Bulleted list', @@ -127,15 +125,10 @@ describe('Markdown serializer', () => { html: '
  • Bulleted list with italics

', markdown: '* *Bulleted list with italics*' }, - { - name: 'Bullet list with italics', - html: '
  • Bullet list with italics

', - markdown: '* *Bullet list with italics*' - }, { name: 'Bullet list with link', - html: '', - markdown: '* ' + html: '', + markdown: '* ' }, { name: 'Nested list with levels 1 - Bulleted list, 2 - Numbered list (Bold)', From 3a35a26e6577dc3f57d6862e14a506991dc60329 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:55:53 +0530 Subject: [PATCH 52/56] Minor comment error change --- packages/nimble-components/src/rich-text/editor/index.ts | 2 +- .../nimble-components/src/rich-text/models/markdown-parser.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index 88f394e722..287e146ffa 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -384,7 +384,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { */ private getCustomLinkExtension(): Mark { return Link.extend({ - // Excludes can be removed enabled when hyperlink support added + // Excludes can be removed/enabled when hyperlink support added // See: https://github.com/ni/nimble/issues/1527 excludes: '_', // Inclusive can be updated when hyperlink support added diff --git a/packages/nimble-components/src/rich-text/models/markdown-parser.ts b/packages/nimble-components/src/rich-text/models/markdown-parser.ts index 392fc607e4..ce7b2aaea4 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-parser.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-parser.ts @@ -83,7 +83,7 @@ export class RichTextMarkdownParser { // Inclusive can be updated when hyperlink support added // See: https://github.com/ni/nimble/issues/1527 inclusive: false, - // Excludes can be removed enabled when hyperlink support added + // Excludes can be removed/enabled when hyperlink support added // See: https://github.com/ni/nimble/issues/1527 excludes: '_', toDOM(node) { From 9b0478772c9ea569e3fac083f5dc01791de71392 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:59:34 +0530 Subject: [PATCH 53/56] Minor test case update --- .../src/rich-text/models/tests/markdown-parser.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index 890a891402..dbef096d96 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -330,7 +330,7 @@ describe('Markdown parser', () => { { name: 'Emoji at the host (punycode encoded)', validLink: '', - encodeURL: 'https://www.xn--h28h.com' + encodeURL: 'https://www.xn--e28h.com/' }, { name: 'Square brackets', From f425b5c150bf2ba240c810754e7201822ca5b4c2 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:17:02 +0530 Subject: [PATCH 54/56] Removed forward slash in emoji test case --- .../src/rich-text/models/tests/markdown-parser.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index dbef096d96..c84da64367 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -330,7 +330,7 @@ describe('Markdown parser', () => { { name: 'Emoji at the host (punycode encoded)', validLink: '', - encodeURL: 'https://www.xn--e28h.com/' + encodeURL: 'https://www.xn--e28h.com' }, { name: 'Square brackets', From 533509119d93651ce559915f088f6583a6b29a65 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 14 Sep 2023 20:05:56 +0530 Subject: [PATCH 55/56] Added matrix test for long link in mobile width --- .../editor/tests/rich-text-editor-matrix.stories.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts index 5e95d60d5b..bfe669f555 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts @@ -197,6 +197,15 @@ longWordContentInMobileWidth.play = (): void => { ); }; +export const longLinkInMobileWidth: StoryFn = createStory(mobileWidthComponent); +longLinkInMobileWidth.play = (): void => { + document + .querySelector('nimble-rich-text-editor')! + .setMarkdown( + '' + ); +}; + export const hiddenRichTextEditor: StoryFn = createStory( hiddenWrapper(html`<${richTextEditorTag} hidden>`) ); From 23b8fddb6422ea0fdd33ad7c7a1a27f9ae2be794 Mon Sep 17 00:00:00 2001 From: Vivin Krishna <123377523+vivinkrishna-ni@users.noreply.github.com> Date: Thu, 14 Sep 2023 20:20:03 +0530 Subject: [PATCH 56/56] Updated the comment of parseHTML in editor --- packages/nimble-components/src/rich-text/editor/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index 287e146ffa..c9b5337666 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -391,9 +391,9 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { // See: https://github.com/ni/nimble/issues/1527 inclusive: false, parseHTML() { - // As the markdown parser parses the links in the markdown string to 'nimble-anchor' tags, - // the link extension should identify that all 'nimble-anchor' tags are links. Therefore, it should - // return the 'nimble-anchor' tag. + // To load the `nimble-anchor` from the HTML parsed content by markdown-parser as links in the + // Tiptap editor, the `parseHTML` of Link extension should return `anchorTag`. This is because the + // link mark schema in `markdown-parser.ts` file uses `` as anchor tag and not ``. return [{ tag: anchorTag }]; }, // HTMLAttribute cannot be in camelCase as we want to match it with the name in Tiptap