diff --git a/change/@ni-nimble-components-99393253-07d1-4ec4-902b-41680bddd36e.json b/change/@ni-nimble-components-99393253-07d1-4ec4-902b-41680bddd36e.json new file mode 100644 index 0000000000..2ba5e906eb --- /dev/null +++ b/change/@ni-nimble-components-99393253-07d1-4ec4-902b-41680bddd36e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Implementation of markdown parser for converting markdown input to rich text content in the viewer", + "packageName": "@ni/nimble-components", + "email": "123377523+vivinkrishna-ni@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/package-lock.json b/package-lock.json index bd9258d79f..02b5de0dd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5913,6 +5913,60 @@ "node": ">=12" } }, + "node_modules/@rollup/plugin-inject": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.3.tgz", + "integrity": "sha512-411QlbL+z2yXpRWFXSmw/teQRMkXcAAC8aYTemc15gwJRpvEVDQwoe+N/HTFD8RFG8+88Bme9DK2V9CVm7hJdA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "estree-walker": "^2.0.2", + "magic-string": "^0.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-inject/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.0.0.tgz", + "integrity": "sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.1.0.tgz", @@ -9707,12 +9761,26 @@ "dev": true, "peer": true }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" + }, "node_modules/@types/lodash": { "version": "4.14.195", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", "dev": true }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, "node_modules/@types/mdast": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.12.tgz", @@ -9722,6 +9790,11 @@ "@types/unist": "^2" } }, + "node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" + }, "node_modules/@types/mdx": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.5.tgz", @@ -22200,8 +22273,7 @@ "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", - "dev": true + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "node_modules/media-typer": { "version": "0.3.0", @@ -24116,15 +24188,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/ora/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -24186,15 +24249,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/ora/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/ora/node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -24208,29 +24262,10 @@ "node": ">=8" } }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==" }, "node_modules/os-tmpdir": { "version": "1.0.2", @@ -26364,6 +26399,62 @@ "react-is": "^16.13.1" } }, + "node_modules/prosemirror-markdown": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.11.0.tgz", + "integrity": "sha512-yP9mZqPRstjZhhf3yykCQNE3AijxARrHe4e7esV9A+gp4cnGOH4QvrKYPpXLHspNWyvJJ+0URH+iIvV5qP1I2Q==", + "dependencies": { + "markdown-it": "^13.0.1", + "prosemirror-model": "^1.0.0" + } + }, + "node_modules/prosemirror-markdown/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/prosemirror-markdown/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/prosemirror-markdown/node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/prosemirror-markdown/node_modules/markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/prosemirror-model": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.2.tgz", + "integrity": "sha512-RXl0Waiss4YtJAUY3NzKH0xkJmsZupCIccqcIFoLTIKFlKNbIvFDRl27/kQy1FP8iUAxrjRRfIVvOebnnXJgqQ==", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -27894,6 +27985,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-polyfill-node": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-polyfill-node/-/rollup-plugin-polyfill-node-0.12.0.tgz", + "integrity": "sha512-PWEVfDxLEKt8JX1nZ0NkUAgXpkZMTb85rO/Ru9AQ69wYW8VUCfDgP4CGRXXWYni5wDF0vIeR1UoF3Jmw/Lt3Ug==", + "dev": true, + "dependencies": { + "@rollup/plugin-inject": "^5.0.1" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0 || ^3.0.0" + } + }, "node_modules/rollup-plugin-sourcemaps": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz", @@ -30798,8 +30901,7 @@ "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, "node_modules/uglify-js": { "version": "3.17.4", @@ -32737,11 +32839,14 @@ "@types/d3-scale": "^4.0.2", "@types/d3-selection": "^3.0.0", "@types/d3-zoom": "^3.0.0", + "@types/markdown-it": "^12.2.3", "d3-array": "^3.2.2", "d3-random": "^3.0.1", "d3-scale": "^4.0.2", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0", + "prosemirror-markdown": "^1.11.0", + "prosemirror-model": "^1.19.2", "tslib": "^2.2.0" }, "devDependencies": { @@ -32750,6 +32855,7 @@ "@ni/eslint-config-javascript": "^4.2.0", "@ni/eslint-config-typescript": "^4.2.0", "@rollup/plugin-commonjs": "^24.0.1", + "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-replace": "^5.0.1", "@rollup/plugin-terser": "^0.4.0", @@ -32793,6 +32899,7 @@ "puppeteer": "^10.1.0", "remark-gfm": "^3.0.1", "rollup": "^3.10.1", + "rollup-plugin-polyfill-node": "^0.12.0", "rollup-plugin-sourcemaps": "^0.6.3", "storybook": "^7.0.4", "ts-loader": "^9.2.5", diff --git a/packages/nimble-components/package.json b/packages/nimble-components/package.json index 9d4062dfe8..24d98b4a4a 100644 --- a/packages/nimble-components/package.json +++ b/packages/nimble-components/package.json @@ -69,11 +69,14 @@ "@types/d3-scale": "^4.0.2", "@types/d3-selection": "^3.0.0", "@types/d3-zoom": "^3.0.0", + "@types/markdown-it": "^12.2.3", "d3-array": "^3.2.2", "d3-random": "^3.0.1", "d3-scale": "^4.0.2", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0", + "prosemirror-markdown": "^1.11.0", + "prosemirror-model": "^1.19.2", "tslib": "^2.2.0" }, "devDependencies": { @@ -82,6 +85,7 @@ "@ni/eslint-config-javascript": "^4.2.0", "@ni/eslint-config-typescript": "^4.2.0", "@rollup/plugin-commonjs": "^24.0.1", + "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-replace": "^5.0.1", "@rollup/plugin-terser": "^0.4.0", @@ -125,6 +129,7 @@ "puppeteer": "^10.1.0", "remark-gfm": "^3.0.1", "rollup": "^3.10.1", + "rollup-plugin-polyfill-node": "^0.12.0", "rollup-plugin-sourcemaps": "^0.6.3", "storybook": "^7.0.4", "ts-loader": "^9.2.5", diff --git a/packages/nimble-components/rollup.config.js b/packages/nimble-components/rollup.config.js index f57f21f1f4..5064ac22b2 100644 --- a/packages/nimble-components/rollup.config.js +++ b/packages/nimble-components/rollup.config.js @@ -3,6 +3,8 @@ import resolve from '@rollup/plugin-node-resolve'; import sourcemaps from 'rollup-plugin-sourcemaps'; import terser from '@rollup/plugin-terser'; import replace from '@rollup/plugin-replace'; +import json from '@rollup/plugin-json'; +import nodePolyfills from 'rollup-plugin-polyfill-node'; const umdDevelopmentPlugin = () => replace({ 'process.env.NODE_ENV': JSON.stringify('development') @@ -21,7 +23,14 @@ export default [ format: 'iife', sourcemap: true }, - plugins: [umdDevelopmentPlugin(), sourcemaps(), resolve(), commonJS()] + plugins: [ + umdDevelopmentPlugin(), + nodePolyfills(), + sourcemaps(), + resolve(), + commonJS(), + json() + ] }, { input: 'dist/esm/all-components.js', @@ -37,6 +46,13 @@ export default [ }) ] }, - plugins: [umdProductionPlugin(), sourcemaps(), resolve(), commonJS()] + plugins: [ + umdProductionPlugin(), + nodePolyfills(), + sourcemaps(), + resolve(), + commonJS(), + json() + ] } ]; diff --git a/packages/nimble-components/src/rich-text-editor/specs/README.md b/packages/nimble-components/src/rich-text-editor/specs/README.md index 0255c99218..43d92ec6a4 100644 --- a/packages/nimble-components/src/rich-text-editor/specs/README.md +++ b/packages/nimble-components/src/rich-text-editor/specs/README.md @@ -15,7 +15,7 @@ including Comments and other instances that necessitate rich text capabilities. [Nimble issue #1288](https://github.com/ni/nimble/issues/1288) -[Comments UI mockup](https://www.figma.com/proto/Q5SU1OwrnD08keon3zObRX/SystemLink?type=design&node-id=6280-94118&scaling=min-zoom&page-id=2428%3A32954&starting-point-node-id=6280%3A94118&show-proto-sidebar=1) +Comments UI mockups - [Desktop view](https://www.figma.com/file/Q5SU1OwrnD08keon3zObRX/SystemLink?type=design&node-id=6280-94045&mode=design&t=aC5VQw42BYcOesm2-0) and [Mobile view](https://www.figma.com/file/Q5SU1OwrnD08keon3zObRX/SystemLink?type=design&node-id=7258-115224&mode=design&t=aC5VQw42BYcOesm2-0) [Comments Feature](https://dev.azure.com/ni/DevCentral/_backlogs/backlog/ASW%20SystemLink%20Platform/Initiatives/?workitem=2205215) @@ -62,7 +62,7 @@ The `nimble-rich-text-viewer` provides support for converting the input markdown - Due to immediate requirements for comments feature from a business customer, any additional enhancements or requirements apart from whatever is mentioned in this spec are deferred to future scope. - Currently, we will begin by referring to the existing - [Interaction design workflow](https://www.figma.com/proto/Q5SU1OwrnD08keon3zObRX/SystemLink?type=design&node-id=6280-94118&scaling=min-zoom&page-id=2428%3A32954&starting-point-node-id=6280%3A94118&show-proto-sidebar=1) + [Interaction design workflow](https://www.figma.com/file/Q5SU1OwrnD08keon3zObRX/SystemLink?type=design&node-id=6280-94045&mode=design&t=aC5VQw42BYcOesm2-0) of the comments feature. Once the visual design for these components is complete, we will then be implementing those specific changes within the defined scope of development. However, we will still make use of existing nimble components such as `nimble-toggle-button` and `nimble-text-area` to maintain a consistent design for the initial release. @@ -104,9 +104,9 @@ _Props/Attrs_ - `markdown` - is an accessor used to get and set the markdown value. - `getter` - this will serialize the content by extracting the Node from the editor and convert it into a markdown string using - [prosemirror-markdown serializer](https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/to_markdown.ts#L30). + [prosemirror-markdown serializer](https://github.com/ProseMirror/prosemirror-markdown/blob/9049cd1ec20540d70352f8a3e8736fb0d1f9ce1b/src/to_markdown.ts#L30). - `setter` - this will parse the markdown string into a Node and load it back into the editor using - [prosemirror-markdown parser](https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.ts#L199). + [prosemirror-markdown parser](https://github.com/ProseMirror/prosemirror-markdown/blob/9049cd1ec20540d70352f8a3e8736fb0d1f9ce1b/src/from_markdown.ts#L199). - `isEmpty` - 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 that can be used to bind it in the Angular application. @@ -205,12 +205,9 @@ tasks to convert the markdown string to corresponding HTML nodes for each text f _Props/Attrs_ -- `markdown` - is an accessor used to get and set the markdown value. - - `getter` - this will merely return the markdown string that is set to the component. - - `setter` - this will parse the markdown string into a Node using - [prosemirror-markdown parser](https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.ts#L199) and convert to an HTML to - render it in the component section. -- `fitToContent` - is a boolean attribute allows the text area to expand vertically to fit the content. +- `markdown` - for retrieving and modifying the markdown value. If the client modifies the markdown value, it will be parsed into a Node using the + [prosemirror-markdown parser](https://github.com/ProseMirror/prosemirror-markdown/blob/9049cd1ec20540d70352f8a3e8736fb0d1f9ce1b/src/from_markdown.ts#L199). + The parsed node will then be rendered in the viewer component as rich text. _Methods_ @@ -222,8 +219,9 @@ _Events_ _CSS Classes and CSS Custom Properties that affect the component_ -- The sizing behavior of the component will remain same as the editor component. The height of the component will grow to fit the content based - on the `fitToContent` attribute. +- The sizing behavior of the component will remain same as the editor component. The height of the component will grow to fit the content if + there is no height restrictions from the consumer. If there is any height set by the consumer, the vertical scrollbar will be + enabled when there is overflow of content in the component. - The width of the component will be determined by the client. Reducing the width will cause the content to reflow, resulting in an increased height of the component or will enable the vertical scrollbar. diff --git a/packages/nimble-components/src/rich-text-viewer/index.ts b/packages/nimble-components/src/rich-text-viewer/index.ts index e6fdab370e..132daa978d 100644 --- a/packages/nimble-components/src/rich-text-viewer/index.ts +++ b/packages/nimble-components/src/rich-text-viewer/index.ts @@ -1,4 +1,11 @@ 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'; @@ -11,7 +18,95 @@ declare global { /** * A nimble styled rich text viewer */ -export class RichTextViewer extends FoundationElement {} +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: MarkdownParser; + private readonly domSerializer: DOMSerializer; + + public constructor() { + super(); + this.domSerializer = DOMSerializer.fromSchema(schema); + this.markdownParser = this.initializeMarkdownParser(); + } + + /** + * @internal + */ + public override connectedCallback(): void { + super.connectedCallback(); + this.updateView(); + } + + /** + * @internal + */ + public markdownChanged(): void { + if (this.$fastController.isConnected) { + this.updateView(); + } + } + + 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); + this.viewer.replaceChildren(serializedContent); + } else { + this.viewer.innerHTML = ''; + } + } +} const nimbleRichTextViewer = RichTextViewer.compose({ baseName: 'rich-text-viewer', diff --git a/packages/nimble-components/src/rich-text-viewer/styles.ts b/packages/nimble-components/src/rich-text-viewer/styles.ts index a47a813379..a77cdee753 100644 --- a/packages/nimble-components/src/rich-text-viewer/styles.ts +++ b/packages/nimble-components/src/rich-text-viewer/styles.ts @@ -1,12 +1,58 @@ import { css } from '@microsoft/fast-element'; import { display } from '@microsoft/fast-foundation'; -import { bodyFont, bodyFontColor } from '../theme-provider/design-tokens'; +import { + bodyFont, + bodyFontColor, + linkActiveFontColor, + linkFontColor +} from '../theme-provider/design-tokens'; export const styles = css` ${display('flex')} :host { font: ${bodyFont}; + outline: none; color: ${bodyFontColor}; + inline-size: auto; + overflow: auto; + block-size: 100%; + min-block-size: 36px; + } + + .viewer { + font: inherit; + outline: none; + box-sizing: border-box; + position: relative; + color: inherit; + } + + .viewer > :first-child { + margin-block-start: 0; + } + + .viewer > :last-child { + margin-block-end: 0; + } + + li > p { + margin-block: 0; + } + + ${ + /* In Firefox, if the paragraph within the list is empty, the ordered lists overlap. Therefore, hiding the paragraph element allows for the proper rendering of empty lists. */ '' + } + li > p:empty { + display: none; + } + + a { + word-break: break-all; + color: ${linkFontColor}; + } + + a:active { + color: ${linkActiveFontColor}; } `; diff --git a/packages/nimble-components/src/rich-text-viewer/template.ts b/packages/nimble-components/src/rich-text-viewer/template.ts index 38b554bf57..49d02acbc6 100644 --- a/packages/nimble-components/src/rich-text-viewer/template.ts +++ b/packages/nimble-components/src/rich-text-viewer/template.ts @@ -1,6 +1,6 @@ -import { html } from '@microsoft/fast-element'; +import { html, ref } from '@microsoft/fast-element'; import type { RichTextViewer } from '.'; export const template = html` - +
`; 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 new file mode 100644 index 0000000000..4495234666 --- /dev/null +++ b/packages/nimble-components/src/rich-text-viewer/testing/rich-text-viewer.pageobject.ts @@ -0,0 +1,59 @@ +import type { RichTextViewer } from '..'; + +/** + * Page object for the `nimble-rich-text-viewer` component. + */ +export class RichTextViewerPageObject { + public constructor( + private readonly richTextViewerElement: RichTextViewer + ) {} + + public getRenderedMarkdownLastChildContents(): string { + return this.getLastChildMarkdownRenderedElement()?.textContent || ''; + } + + public getRenderedMarkdownLastChildAttribute(attribute: string): string { + return ( + this.getLastChildMarkdownRenderedElement()?.getAttribute( + attribute + ) || '' + ); + } + + /** + * Retrieves tag names for the rendered markdown content in document order + * @returns An array of tag names in document order + */ + public getRenderedMarkdownTagNames(): string[] { + return Array.from( + this.getMarkdownRenderedElement()!.querySelectorAll('*') + ).map(el => el.tagName); + } + + /** + * Retrieves text contents for the rendered markdown content in document order + * @returns An array of text contents of last elements in a tree + */ + public getRenderedMarkdownLeafContents(): string[] { + return Array.from( + this.getMarkdownRenderedElement()!.querySelectorAll('*') + ) + .filter((el, _) => { + return el.children.length === 0; + }) + .map(el => el.textContent || ''); + } + + private getMarkdownRenderedElement(): Element | null | undefined { + return this.richTextViewerElement.shadowRoot?.querySelector('.viewer'); + } + + private getLastChildMarkdownRenderedElement(): Element | null | undefined { + let lastElement = this.getMarkdownRenderedElement()?.lastElementChild; + + while (lastElement?.lastElementChild) { + lastElement = lastElement?.lastElementChild; + } + return lastElement; + } +} 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 index be6ec4ee44..d0fc167fa5 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 @@ -10,6 +10,12 @@ import { } 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 { + cssPropertyFromTokenName, + tokenNames +} from '../../theme-provider/design-token-names'; const metadata: Meta = { title: 'Tests/Rich Text Viewer', @@ -22,13 +28,105 @@ export default metadata; // prettier-ignore const component = (): ViewTemplate => html` - <${richTextViewerTag}> + <${richTextViewerTag} :markdown="${_ => richTextMarkdownString}"> +`; + +const viewerSizingTestCase = ( + [widthLabel, widthStyle]: [string, string], + [heightLabel, heightStyle]: [string, string] +): ViewTemplate => html` +

${widthLabel}; ${heightLabel}

+
+ <${richTextViewerTag} + style="${widthStyle}; ${heightStyle}; outline: 1px dashed red;" + :markdown="${_ => richTextMarkdownString}" + > + +
+`; + +const viewerDifferentContentTestCase = ( + [label, markdownContent]: [string, string], + [heightLabel, parentHeightStyle]: [string, string] +): ViewTemplate => html` +

${label}; ${heightLabel}

+
+ <${richTextViewerTag} + :markdown="${_ => markdownContent}" + > + +
+`; + +const componentFitToContent = ([widthLabel, widthStyle]: [ + string, + string +]): ViewTemplate => html` +

${widthLabel}

+ <${richTextViewerTag} style="${widthStyle}; outline: 1px dotted black" + :markdown="${_ => `${loremIpsum}\n\n **${loremIpsum}**\n\n ${loremIpsum}`}"> + `; export const richTextViewerThemeMatrix: StoryFn = createMatrixThemeStory( createMatrix(component) ); +export const richTextViewerSizing: StoryFn = createStory(html` + ${createMatrix(viewerSizingTestCase, [ + [ + ['No width', ''], + ['Width 300px', 'width: 300px'], + ['Width 100%', 'width: 100%'] + ], + [ + ['No height', ''], + ['Height 75px', 'height: 75px'], + ['Height 100%', 'height: 100%'] + ] + ])} +`); + +export const differentContentsInMobileWidth: StoryFn = createStory(html` + ${createMatrix(viewerDifferentContentTestCase, [ + [ + ['No content', ''], + ['Plain text', loremIpsum], + [ + 'Multiple sup points', + '1. Sub point 1\n 1. Sub point 2\n 1. Sub point 3\n 1. Sub point 4\n 1. Sub point 5\n 1. Sub point 6\n 1. Sub point 7\n' + ], + [ + 'Long link', + '' + ], + [ + 'Long word', + 'ThisIsALongWordWithoutSpaceToTestLongWordInSmallWidth' + ] + ], + [ + ['No height', ''], + ['Height 100px', 'height: 100px'] + ] + ])} +`); + +export const fitToContentTest: StoryFn = createStory(html` + ${createMatrix(componentFitToContent, [ + [ + ['no width', ''], + ['width 300px', 'width: 300px'] + ] + ])} +`); + export const hiddenRichTextViewer: StoryFn = createStory( hiddenWrapper(html`<${richTextViewerTag} hidden>`) ); 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 c99d8bd0c6..40853806d1 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,6 +1,27 @@ +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> { + return fixture( + html`` + ); +} describe('RichTextViewer', () => { + let element: RichTextViewer; + let connect: () => Promise; + let disconnect: () => Promise; + let pageObject: RichTextViewerPageObject; + + beforeEach(async () => { + ({ element, connect, disconnect } = await setup()); + pageObject = new RichTextViewerPageObject(element); + }); + it('can construct an element instance', () => { expect( document.createElement('nimble-rich-text-viewer') @@ -10,4 +31,548 @@ describe('RichTextViewer', () => { it('should export its tag', () => { expect(richTextViewerTag).toBe('nimble-rich-text-viewer'); }); + + it('set the markdown attribute and ensure the markdown property is not modified', async () => { + await connect(); + + element.setAttribute('markdown', '**markdown string**'); + + expect(element.markdown).toBe(''); + + await disconnect(); + }); + + it('set the markdown property and ensure there is no markdown attribute', async () => { + await connect(); + + element.markdown = '**markdown string**'; + + expect(element.hasAttribute('markdown')).toBeFalse(); + + await disconnect(); + }); + + it('set the markdown property and ensure that getting the markdown property returns the same value', async () => { + await connect(); + + element.markdown = '**markdown string**'; + + expect(element.markdown).toBe('**markdown string**'); + + await disconnect(); + }); + + it('set a empty string should clear a value in the viewer', async () => { + await connect(); + + element.markdown = 'markdown string'; + expect(pageObject.getRenderedMarkdownTagNames()).toEqual(['P']); + expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( + 'markdown string' + ); + + element.markdown = ''; + expect(pageObject.getRenderedMarkdownTagNames()).toEqual([]); + expect(pageObject.getRenderedMarkdownLastChildContents()).toBe(''); + + element.markdown = 'new markdown string'; + expect(pageObject.getRenderedMarkdownTagNames()).toEqual(['P']); + expect(pageObject.getRenderedMarkdownLastChildContents()).toBe( + 'new markdown string' + ); + + 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('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('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[] = []; + 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(); + } + ); + } + }); }); 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 index 6ad586d3ab..cf1e2f14ac 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 @@ -5,9 +5,11 @@ import { incubatingWarning } from '../../utilities/tests/storybook'; import { richTextViewerTag } from '..'; +import { richTextMarkdownString } from '../../utilities/tests/rich-text-markdown-string'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface RichTextViewerArgs {} +interface RichTextViewerArgs { + markdown: string; +} const richTextViewerDescription = 'The rich text viewer component allows users to view text formatted with various styling options including bold, italics, numbered lists, and bulleted lists. The rich text to render is provided as a markdown string.\n\n See the [rich text editor](?path=/docs/incubating-rich-text-editor--docs) component to enable users to modify the markdown contents.'; @@ -27,8 +29,20 @@ const metadata: Meta = { componentName: 'rich text viewer', statusLink: 'https://github.com/ni/nimble/issues/1288' })} - <${richTextViewerTag}> - `) + <${richTextViewerTag} + :markdown="${x => x.markdown}" + > + + `), + argTypes: { + markdown: { + description: + 'Input markdown string for the supported text formatting options in a [CommonMark](https://commonmark.org/) flavor.' + } + }, + args: { + markdown: richTextMarkdownString + } }; export default metadata; diff --git a/packages/nimble-components/src/utilities/tests/rich-text-markdown-string.ts b/packages/nimble-components/src/utilities/tests/rich-text-markdown-string.ts new file mode 100644 index 0000000000..7a70a69fcd --- /dev/null +++ b/packages/nimble-components/src/utilities/tests/rich-text-markdown-string.ts @@ -0,0 +1 @@ +export const richTextMarkdownString = 'Supported rich text formatting options:\n1. **Bold**\n2. *Italics*\n3. Numbered lists\n 1. Option 1\n 2. Option 2\n4. Bulleted lists\n * Option 1\n * Option 2\n5. Direct link: ';