@descope-ui/descope-enriched-text 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
+
5
+ ## 0.0.1 (2025-07-03)
6
+
7
+ ### Dependency Updates
8
+
9
+ * `e2e-utils` updated to version `0.0.1`
10
+ * `@descope-ui/common` updated to version `0.0.16`
11
+ * `@descope-ui/theme-globals` updated to version `0.0.17`
12
+ * `@descope-ui/descope-text` updated to version `0.0.17`
13
+ * `@descope-ui/descope-link` updated to version `0.0.1`
14
+ # Changelog
@@ -0,0 +1,65 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { getStoryUrl, loopConfig } from 'e2e-utils';
3
+
4
+ const componentAttributes = {
5
+ mode: ['primary', 'secondary', 'success', 'error'],
6
+ variant: ['h1', 'h2', 'h3', 'subtitle1', 'subtitle2', 'body1', 'body2'],
7
+ 'text-align': ['left', 'center', 'right'],
8
+ 'hide-when-empty': ['true', 'false'],
9
+ 'full-width': ['true', 'false'],
10
+ direction: ['ltr'],
11
+ readonly: ['true', 'false'],
12
+ textSample: [
13
+ 'headings',
14
+ 'horizontal-rules',
15
+ 'emphasis',
16
+ 'blockquotes',
17
+ 'lists',
18
+ 'code',
19
+ 'tables',
20
+ 'links',
21
+ 'images',
22
+ 'mixedContent',
23
+ 'escaping',
24
+ 'escapedMarkdown',
25
+ ],
26
+ };
27
+
28
+ const storyName = 'descope-enriched-text';
29
+ const componentName = 'descope-enriched-text';
30
+
31
+ test.describe('theme', () => {
32
+ test('simple text', async ({ page }) => {
33
+ await page.goto(getStoryUrl(storyName, { text: 'Simple Text Test' }), {
34
+ waitUntil: 'networkidle',
35
+ });
36
+ const component = page.locator(componentName);
37
+ expect(await component.screenshot()).toMatchSnapshot();
38
+ });
39
+
40
+ loopConfig(componentAttributes, (attr, value) => {
41
+ test(`${attr}: ${value}`, async ({ page }) => {
42
+ await page.goto(getStoryUrl(storyName, { textSample: 'mix', [attr]: value }), {
43
+ waitUntil: 'networkidle',
44
+ });
45
+ const component = page.locator(componentName);
46
+ expect(await component.screenshot()).toMatchSnapshot();
47
+ });
48
+ });
49
+
50
+ test('direction: rtl', async ({ page }) => {
51
+ await page.goto(getStoryUrl(storyName, { direction: 'rtl', text: '-Hello World' }), {
52
+ waitUntil: 'networkidle',
53
+ });
54
+ const component = page.locator(componentName);
55
+ expect(await component.screenshot()).toMatchSnapshot();
56
+ });
57
+ });
58
+
59
+ test.describe('logic', () => {
60
+ test('support title tooltip', async ({ page }) => {
61
+ await page.goto(getStoryUrl(storyName, { textSample: 'links' }), { waitUntil: 'networkidle' });
62
+ const component = page.locator('a').last();
63
+ await expect(component).toHaveAttribute('title', 'title text!');
64
+ });
65
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@descope-ui/descope-enriched-text",
3
+ "version": "0.0.1",
4
+ "exports": {
5
+ ".": {
6
+ "import": "./src/component/index.js"
7
+ },
8
+ "./theme": {
9
+ "import": "./src/theme.js"
10
+ },
11
+ "./class": {
12
+ "import": "./src/component/EnrichedTextClass.js"
13
+ }
14
+ },
15
+ "devDependencies": {
16
+ "@playwright/test": "1.38.1",
17
+ "e2e-utils": "0.0.1"
18
+ },
19
+ "dependencies": {
20
+ "markdown-it": "14.1.0",
21
+ "@descope-ui/common": "0.0.16",
22
+ "@descope-ui/theme-globals": "0.0.17",
23
+ "@descope-ui/descope-text": "0.0.17",
24
+ "@descope-ui/descope-link": "0.0.1"
25
+ },
26
+ "publishConfig": {
27
+ "link-workspace-packages": false
28
+ },
29
+ "scripts": {
30
+ "test": "echo 'No tests defined' && exit 0",
31
+ "test:e2e": "echo 'No e2e tests defined' && exit 0"
32
+ }
33
+ }
package/project.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@descope-ui/descope-enriched-text",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "packages/web-components/components/descope-enriched-text/src",
5
+ "projectType": "library",
6
+ "targets": {
7
+ "version": {
8
+ "executor": "@jscutlery/semver:version",
9
+ "options": {
10
+ "trackDeps": true,
11
+ "push": false,
12
+ "preset": "conventional"
13
+ }
14
+ }
15
+ },
16
+ "tags": []
17
+ }
@@ -0,0 +1,208 @@
1
+ /* eslint-disable no-param-reassign */
2
+
3
+ import MarkdownIt from 'markdown-it';
4
+ import { createStyleMixin, draggableMixin, componentNameValidationMixin } from '@descope-ui/common/components-mixins';
5
+ import { compose } from '@descope-ui/common/utils';
6
+ import { disableRules } from './consts';
7
+ import { createBaseClass } from '@descope-ui/common/base-classes';
8
+ import { decodeHTML } from './helpers';
9
+ import { getComponentName, injectStyle, observeChildren } from '@descope-ui/common/components-helpers';
10
+
11
+ export const componentName = getComponentName('enriched-text');
12
+
13
+ class EnrichedText extends createBaseClass({ componentName, baseSelector: ':host > div' }) {
14
+ #origLinkRenderer;
15
+
16
+ #origEmRenderer;
17
+
18
+ constructor() {
19
+ super();
20
+
21
+ this.attachShadow({ mode: 'open' }).innerHTML = `
22
+ <div class="content"></div>
23
+ `;
24
+
25
+ injectStyle(
26
+ `
27
+ :host {
28
+ line-height: 1em;
29
+ word-break: break-word;
30
+ }
31
+ :host > slot {
32
+ width: 100%;
33
+ display: inline-block;
34
+ }
35
+ *, *:last-child {
36
+ margin: 0;
37
+ }
38
+ h1,
39
+ h2,
40
+ h3,
41
+ h4,
42
+ h5,
43
+ h6,
44
+ p {
45
+ margin-bottom: 1em;
46
+ }
47
+ a {
48
+ cursor: pointer;
49
+ }
50
+ blockquote {
51
+ padding: 0 2em;
52
+ }
53
+ u {
54
+ text-decoration: underline
55
+ }
56
+ s {
57
+ color: currentColor;
58
+ }
59
+ `,
60
+ this
61
+ );
62
+
63
+ this.#initProcessor();
64
+
65
+ observeChildren(this, this.#parseChildren.bind(this));
66
+ }
67
+
68
+ static get observedAttributes() {
69
+ return ['readonly', 'link-target-blank'];
70
+ }
71
+
72
+ attributeChangedCallback(attrName, oldValue, newValue) {
73
+ super.attributeChangedCallback?.(attrName, oldValue, newValue);
74
+
75
+ if (newValue !== oldValue) {
76
+ if (attrName === 'readonly') {
77
+ this.onReadOnlyChange(newValue === 'true');
78
+ }
79
+
80
+ if (attrName === 'link-target-blank') {
81
+ this.#initProcessor();
82
+ }
83
+ }
84
+ }
85
+
86
+ // We're overriding the rule for em with single underscore to perform as underline. (_underline_)
87
+ customUnderlineRenderer() {
88
+ this.processor.renderer.rules.em_open = (tokens, idx, options, env, self) => {
89
+ if (tokens[idx].markup === '_') tokens[idx].tag = 'u';
90
+ return this.#origEmRenderer(tokens, idx, options, env, self);
91
+ };
92
+ this.processor.renderer.rules.em_close = (tokens, idx, options, env, self) => {
93
+ if (tokens[idx].markup === '_') tokens[idx].tag = 'u';
94
+ return this.#origEmRenderer(tokens, idx, options, env, self);
95
+ };
96
+ }
97
+
98
+ #customizeLinkRenderer() {
99
+ if (this.linkTargetBlank) {
100
+ this.processor.renderer.rules.link_open = (tokens, idx, options, env, self) => {
101
+ // Add a new `target` attribute, or replace the value of the existing one.
102
+ tokens[idx].attrSet('target', '_blank');
103
+ // Pass the token to the default renderer.
104
+ return this.#origLinkRenderer(tokens, idx, options, env, self);
105
+ };
106
+ } else {
107
+ this.processor.renderer.rules.link_open = this.#origLinkRenderer;
108
+ }
109
+ }
110
+
111
+ #disableCustomRules() {
112
+ if (!this.processor) {
113
+ return;
114
+ }
115
+ this.processor.disable(disableRules);
116
+ }
117
+
118
+ #updateProcessorRules() {
119
+ this.#disableCustomRules();
120
+ }
121
+
122
+ #storeOrigRenderers() {
123
+ const defaultLinkRenderer = (tokens, idx, options, _, self) =>
124
+ self.renderToken(tokens, idx, options);
125
+ this.#origLinkRenderer = this.processor.renderer.rules.link_open || defaultLinkRenderer;
126
+
127
+ const defaultStrongRenderer = (tokens, idx, options, _, self) =>
128
+ self.renderToken(tokens, idx, options);
129
+ this.#origEmRenderer = this.processor.renderer.rules.em_open || defaultStrongRenderer;
130
+ }
131
+
132
+ #initProcessor() {
133
+ this.processor = new MarkdownIt('commonmark', { html: true });
134
+ this.#storeOrigRenderers();
135
+ this.#updateProcessorRules();
136
+ this.#customizeLinkRenderer();
137
+ this.customUnderlineRenderer();
138
+ }
139
+
140
+ get linkTargetBlank() {
141
+ return this.getAttribute('link-target-blank') === 'true';
142
+ }
143
+
144
+ get contentNode() {
145
+ return this.shadowRoot.querySelector('.content');
146
+ }
147
+
148
+ #parseChildren() {
149
+ if (!this.processor) {
150
+ return;
151
+ }
152
+
153
+ let html = decodeHTML(this.innerHTML);
154
+
155
+ if (!html?.trim() && this.isConnected) {
156
+ this.setAttribute('empty', 'true');
157
+ } else {
158
+ this.removeAttribute('empty');
159
+ }
160
+
161
+ try {
162
+ const tokens = this.processor.parse(html, { references: undefined });
163
+ html = this.processor.renderer.render(tokens, { html: true, breaks: true });
164
+ } catch (e) {
165
+ // eslint-disable-next-line no-console
166
+ console.warn('Not parsing invalid markdown token');
167
+ }
168
+
169
+ this.contentNode.innerHTML = html;
170
+ }
171
+
172
+ onReadOnlyChange(isReadOnly) {
173
+ if (isReadOnly) {
174
+ this.contentNode.setAttribute('inert', isReadOnly);
175
+ } else {
176
+ this.contentNode.removeAttribute('inert');
177
+ }
178
+ }
179
+ }
180
+
181
+ export const EnrichedTextClass = compose(
182
+ createStyleMixin({
183
+ mappings: {
184
+ hostWidth: { selector: () => ':host', property: 'width' },
185
+ hostDisplay: { selector: () => ':host', property: 'display', fallback: 'inline-block' },
186
+ hostDirection: { selector: () => ':host', property: 'direction' },
187
+ fontSize: {},
188
+ fontFamily: {},
189
+ fontWeight: {},
190
+ fontWeightBold: [
191
+ { selector: () => ':host strong', property: 'font-weight' },
192
+ { selector: () => ':host b', property: 'font-weight' },
193
+ ],
194
+ textColor: { property: 'color' },
195
+ textLineHeight: { property: 'line-height' },
196
+ textAlign: {},
197
+ linkColor: { selector: 'a', property: 'color' },
198
+ linkTextDecoration: { selector: 'a', property: 'text-decoration' },
199
+ linkHoverTextDecoration: { selector: 'a:hover', property: 'text-decoration' },
200
+ minHeight: {},
201
+ minWidth: {},
202
+ },
203
+ }),
204
+ createStyleMixin({ componentNameOverride: getComponentName('link') }),
205
+ createStyleMixin({ componentNameOverride: getComponentName('text') }),
206
+ draggableMixin,
207
+ componentNameValidationMixin
208
+ )(EnrichedText);
@@ -0,0 +1,14 @@
1
+ export const disableRules = [
2
+ 'blockquote',
3
+ 'list',
4
+ 'image',
5
+ 'table',
6
+ 'code',
7
+ 'hr',
8
+ 'backticks',
9
+ 'fence',
10
+ 'reference',
11
+ 'heading',
12
+ 'lheading',
13
+ 'html_block',
14
+ ];
@@ -0,0 +1,5 @@
1
+ export const decodeHTML = (html) => {
2
+ const textArea = document.createElement('textarea');
3
+ textArea.innerHTML = html;
4
+ return textArea.value;
5
+ };
@@ -0,0 +1,7 @@
1
+ import '@descope-ui/descope-link';
2
+
3
+ import { componentName, EnrichedTextClass } from './EnrichedTextClass';
4
+
5
+ customElements.define(componentName, EnrichedTextClass);
6
+
7
+ export { EnrichedTextClass, componentName };
package/src/theme.js ADDED
@@ -0,0 +1,40 @@
1
+ import globals from '@descope-ui/theme-globals';
2
+ import { getThemeRefs, useVar } from '@descope-ui/common/theme-helpers';
3
+ import { EnrichedTextClass } from './component/EnrichedTextClass';
4
+ import { vars as textCompVars } from '@descope-ui/descope-text/theme';
5
+ import { vars as linkCompVars } from '@descope-ui/descope-link/theme';
6
+
7
+ const globalRefs = getThemeRefs(globals);
8
+ const vars = EnrichedTextClass.cssVarList;
9
+
10
+ const enrichedText = {
11
+ [vars.hostDirection]: globalRefs.direction,
12
+ [vars.hostWidth]: useVar(textCompVars.hostWidth),
13
+
14
+ [vars.textLineHeight]: useVar(textCompVars.textLineHeight),
15
+ [vars.textColor]: useVar(textCompVars.textColor),
16
+ [vars.textAlign]: useVar(textCompVars.textAlign),
17
+
18
+ [vars.fontSize]: useVar(textCompVars.fontSize),
19
+ [vars.fontWeight]: useVar(textCompVars.fontWeight),
20
+ [vars.fontFamily]: useVar(textCompVars.fontFamily),
21
+
22
+ [vars.linkColor]: useVar(linkCompVars.textColor),
23
+ [vars.linkTextDecoration]: 'none',
24
+ [vars.linkHoverTextDecoration]: 'underline',
25
+
26
+ [vars.fontWeightBold]: '900',
27
+ [vars.minWidth]: '0.25em',
28
+ [vars.minHeight]: '1.35em',
29
+
30
+ [vars.hostDisplay]: 'inline-block',
31
+
32
+ _empty: {
33
+ _hideWhenEmpty: {
34
+ [vars.hostDisplay]: 'none',
35
+ },
36
+ },
37
+ };
38
+
39
+ export default enrichedText;
40
+ export { vars };
@@ -0,0 +1,245 @@
1
+ import { componentName } from '../src/component';
2
+ import {
3
+ textContentControl,
4
+ textAlignControl,
5
+ modeControl,
6
+ fullWidthControl,
7
+ directionControl,
8
+ typographyVariantControl,
9
+ readOnlyControl,
10
+ } from '@descope-ui/common/sb-controls';
11
+
12
+ const textSamples = {
13
+ mix: `
14
+ ## H2 Heading
15
+
16
+ regular text
17
+ regular text with \\*\\*native\\*\\* asterisks
18
+
19
+ **This is bold text**
20
+
21
+ *This is italic text*
22
+
23
+ _This is underline text_
24
+
25
+
26
+ + list item 1
27
+ + list item 2
28
+
29
+ [link text](https://descope.com)
30
+ `,
31
+ markdownParsingIssues: `**}**abc\n**{**abc to be cleared with HTML: <b>}}</b>abc`,
32
+ escapedMarkdown: `***\\*text\\**** **\\*text\\*** _\\_text\\__ __\\_\\_text\\_\\___ <u><s><b>\\~\\~text\\~\\~</b></s></u>`,
33
+ escaping:
34
+ 'Multiple Slashes: ///\nSingle backslash: \\ \\ \\ \n Multiple backslashes: \\\\\\\\\\\\\\\\ \n\\*single asterisk\\*\n\\*\\*double asterisk\\*\\*\n\\*\\*\\*triple asterisk\\*\\*\\*\n\\_underscore\\_\n\\_\\_double underscore\\_\\_\n\\_underline (single underscore)\\_\n\\~\\~strikethrough\\~\\~\n\\[link\\](url)\nLine-break: \\n',
35
+ headings: `
36
+ # H1 Heading
37
+ # H1 **Heading with bold**
38
+ ## H2 Heading
39
+ ## H2 **Heading with bold**
40
+ ### H3 Heading
41
+ ### H3 **Heading with bold**
42
+ #### H4 Heading
43
+ #### H4 **Heading with bold**
44
+ ##### H5 Heading
45
+ ###### H6 Heading
46
+
47
+ regular text
48
+
49
+ # sep
50
+
51
+ **regular text**
52
+ `,
53
+ 'horizontal-rules': `
54
+ ## Horizontal Rules
55
+
56
+ ___
57
+
58
+ ---
59
+
60
+ ***
61
+ `,
62
+ emphasis: `
63
+ ## Emphasis
64
+
65
+ <b>This is bold text</b>
66
+
67
+ __This is bold text__
68
+
69
+ *This is italic text*
70
+
71
+ <i>This is italic text</i>
72
+
73
+ _This is underline text_
74
+
75
+ <u>This is underline text</u>
76
+
77
+ <u><s><b><i>[This is a text with multiple formats](http://example)</i></b></s></u>
78
+
79
+
80
+ \~\~\*This is single slash escaping\*\~\~
81
+ \\~\\~\\*This is double slash escaping\\*\\~\\~
82
+
83
+ <s>Strikethrough</s>
84
+ `,
85
+ blockquotes: `
86
+ ## Blockquotes
87
+
88
+
89
+ > Blockquotes can also be nested...
90
+ >> ...by using additional greater-than signs right next to each other...
91
+ > > > ...or with spaces between arrows.
92
+ `,
93
+ lists: `
94
+ ## Lists
95
+
96
+ Unordered
97
+
98
+ + Create a list by starting a line with "+", "-", or "*"
99
+ + Sub-lists are made by indenting 2 spaces:
100
+ - Marker character change forces new list start:
101
+ * Ac tristique libero volutpat at
102
+ + Facilisis in pretium nisl aliquet
103
+ - Nulla volutpat aliquam velit
104
+ + Very easy!
105
+
106
+ Ordered
107
+
108
+ 1. Lorem ipsum dolor sit amet
109
+ 2. Consectetur adipiscing elit
110
+ 3. Integer molestie lorem at massa
111
+
112
+ 1. You can use sequential numbers...
113
+ 1. ...or keep all the numbers as 1.
114
+
115
+ Start numbering with offset:
116
+
117
+ 57. foo
118
+ 1. bar
119
+ `,
120
+ code: `
121
+ ## Code
122
+
123
+ Inline \`code\`
124
+
125
+ Indented code
126
+
127
+ // Some comments
128
+ line 1 of code
129
+ line 2 of code
130
+ line 3 of code
131
+
132
+
133
+ Block code "fences"
134
+
135
+ \`\`\`
136
+ Sample text here...
137
+ \`\`\`
138
+ `,
139
+ tables: `
140
+ ## Tables
141
+
142
+ | Option | Description |
143
+ | ------ | ----------- |
144
+ | data | path to data files to supply the data that will be passed into templates. |
145
+ | engine | engine to be used for processing templates. Handlebars is the default. |
146
+ | ext | extension to be used for dest files. |
147
+
148
+ Right aligned columns
149
+
150
+ | Option | Description |
151
+ | ------:| -----------:|
152
+ | data | path to data files to supply the data that will be passed into templates. |
153
+ | engine | engine to be used for processing templates. Handlebars is the default. |
154
+ | ext | extension to be used for dest files. |
155
+ `,
156
+ links: `
157
+ ## Links
158
+
159
+ regular text with [link text](https://descope.com)
160
+
161
+ regular text with [link with title](https://descope.com "title text!")
162
+ `,
163
+ images: `
164
+ ## Images
165
+
166
+ ![Minion](https://octodex.github.com/images/minion.png)
167
+ `,
168
+ mixedContent: `
169
+ ## heading <s>[with strikedthrough link](#)</s> and more content with asterisk* and a {{dynamic.value}}
170
+
171
+ #### Another paragraph, with --- three dashes and the following hashes ## should not render as heading
172
+ `,
173
+ };
174
+
175
+ const Template = ({
176
+ mode,
177
+ variant,
178
+ 'text-align': textAlign,
179
+ 'full-width': fullWidth,
180
+ 'link-target-blank': linkTargetBlank,
181
+ text,
182
+ readonly,
183
+ direction,
184
+ textSample,
185
+ 'hide-when-empty': hideWhenEmpty,
186
+ }) =>
187
+ `
188
+ <descope-enriched-text
189
+ mode="${mode || ''}"
190
+ variant="${variant || ''}"
191
+ full-width="${fullWidth || false}"
192
+ text-align="${textAlign}"
193
+ st-host-direction="${direction ?? ''}"
194
+ readonly="${readonly || false}"
195
+ link-target-blank="${linkTargetBlank || false}"
196
+ hide-when-empty="${hideWhenEmpty || false}"
197
+ >${textSample === 'none' ? text : textSamples[textSample]}</descope-enriched-text>
198
+ `;
199
+
200
+ export default {
201
+ component: componentName,
202
+ title: 'descope-enriched-text',
203
+ argTypes: {
204
+ ...textContentControl,
205
+ textSample: {
206
+ name: 'Content Samples',
207
+ options: [
208
+ 'none',
209
+ 'mix',
210
+ 'escaping',
211
+ 'markdownParsingIssues',
212
+ 'escapedMarkdown',
213
+ 'headings',
214
+ 'horizontal-rules',
215
+ 'emphasis',
216
+ 'blockquotes',
217
+ 'lists',
218
+ 'code',
219
+ 'tables',
220
+ 'links',
221
+ 'images',
222
+ 'mixedContent',
223
+ ],
224
+ control: { type: 'select' },
225
+ },
226
+ ...modeControl,
227
+ ...typographyVariantControl,
228
+ ...textAlignControl,
229
+ ...fullWidthControl,
230
+ ...directionControl,
231
+ ...readOnlyControl,
232
+ },
233
+ };
234
+
235
+ export const Default = Template.bind({});
236
+
237
+ Default.args = {
238
+ text: 'Lorem Ipsum [link](https://descope.com)',
239
+ mode: 'primary',
240
+ readonly: false,
241
+ 'full-width': true, // full-width is true by default for test purposes, which expect this as default and test the FALSE override only
242
+ 'link-target-blank': true,
243
+ textSample: 'none',
244
+ 'hide-when-empty': false,
245
+ };