Skip to content

Commit 41ebf6f

Browse files
authored
feat(markdownToHTML): adds full markdown support (#2715)
* feat(markdownToHTML): add full markdown syntax support
1 parent 02d72ef commit 41ebf6f

File tree

15 files changed

+813
-271
lines changed

15 files changed

+813
-271
lines changed
152 KB
Binary file not shown.

.yarn/offline-mirror/marked-1.1.0.tgz

56 KB
Binary file not shown.

packages/react/src/__tests__/__snapshots__/storyshots.test.js.snap

+662-174
Large diffs are not rendered by default.

packages/react/src/components/LocaleModal/LocaleModalCountries.js

+7
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,14 @@ const LocaleModalCountries = ({
166166
* @type {{regionList: Array, availabilityText: string, unavailabilityText: string, placeHolderText: string, labelText: string}}
167167
*/
168168
LocaleModalCountries.propTypes = {
169+
/**
170+
* Array of regions, countries, and languages.
171+
*/
169172
regionList: PropTypes.array,
173+
174+
/**
175+
* Func to clear search input.
176+
*/
170177
setClearResults: PropTypes.func,
171178
};
172179

packages/react/src/patterns/blocks/CalloutWithMedia/__stories__/CalloutWithMedia.stories.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export default {
6161
),
6262
copy: text(
6363
'copy',
64-
'Lorem ipsum *dolor* sit amet, consectetur adipiscing elit. Aenean et ultricies est.\n Mauris iaculis eget dolor nec hendrerit. Phasellus at elit sollicitudin, sodales\n nulla quis, *consequat* libero. Here are\n some common categories:\n\n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean et ultricies est. Mauris iaculis eget dolor nec hendrerit. Phasellus at elit sollicitudin, sodales nulla quis, consequat libero.\n\n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean et ultricies est. Mauris iaculis eget dolor nec hendrerit. Phasellus at elit sollicitudin, sodales nulla quis, consequat libero.',
64+
'Lorem ipsum *dolor* sit amet, consectetur adipiscing elit. Aenean et ultricies est.\n Mauris iaculis eget dolor nec hendrerit. Phasellus at elit sollicitudin, sodales\n nulla quis, *consequat* libero. Here are\n some common categories:',
6565
groupId
6666
),
6767
};

packages/react/src/patterns/blocks/ContentBlockSegmented/__stories__/ContentBlockSegmented.stories.js

+4-8
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,7 @@ Default.story = {
122122
const defaultItems = [
123123
{
124124
heading: 'Lorem ipsum dolor sit amet.',
125-
copy: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.
126-
127-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.`,
125+
copy: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.`,
128126
cta: {
129127
type: 'local',
130128
copy: 'Lorem Ipsum dolor sit',
@@ -134,9 +132,7 @@ Default.story = {
134132
{
135133
heading: 'Lorem ipsum dolor sit amet.',
136134
image,
137-
copy: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.
138-
139-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.`,
135+
copy: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.`,
140136
cta: {
141137
type: 'local',
142138
copy: 'Lorem Ipsum dolor sit',
@@ -189,14 +185,14 @@ WithAsideElements.story = {
189185
heading: 'Lorem ipsum dolor sit amet.',
190186
copy: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.
191187
192-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.`,
188+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.`,
193189
},
194190
{
195191
heading: 'Lorem ipsum dolor sit amet.',
196192
image,
197193
copy: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.
198194
199-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.`,
195+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.`,
200196
},
201197
];
202198

packages/react/src/patterns/blocks/ContentBlockSimple/__stories__/ContentBlockSimple.stories.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,14 @@ const copy = `Lorem ipsum *dolor* sit amet, consectetur adipiscing elit. Aenean
5959
nulla quis, *consequat* libero. Here are
6060
some common categories:
6161
62-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean et ultricies est. Mauris iaculis eget dolor nec hendrerit. Phasellus at elit sollicitudin, sodales nulla quis, consequat libero.
62+
Lorem ipsum dolor sit amet, [consectetur adipiscing](https://www.ibm.com) elit. Aenean et ultricies est. Mauris iaculis eget dolor nec hendrerit. Phasellus at elit sollicitudin, sodales nulla quis, consequat libero.
6363
6464
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean et ultricies est. Mauris iaculis eget dolor nec hendrerit. Phasellus at elit sollicitudin, sodales nulla quis, consequat libero.
65+
66+
- [list item](https://www.ibm.com)
67+
1. list item 1a
68+
1. list item 2
69+
- list item 2a
6570
`;
6671

6772
/**

packages/react/src/patterns/blocks/ContentGroupSimple/__stories__/data/ContentGroupSimple.knobs.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@
1111
* type {{}}
1212
*/
1313

14+
const copyWithList = `Lorem ipsum *dolor* sit amet, consectetur adipiscing elit. Aenean et ultricies est.
15+
Mauris iaculis eget dolor nec hendrerit. Phasellus at elit sollicitudin, sodales
16+
nulla quis, *consequat* libero. Here are
17+
some common categories:
18+
19+
Lorem ipsum dolor sit amet, [consectetur adipiscing](https://www.ibm.com) elit. Aenean et ultricies est. Mauris iaculis eget dolor nec hendrerit. Phasellus at elit sollicitudin, sodales nulla quis, consequat __libero__.
20+
21+
- [list item link](https://www.ibm.com)
22+
1. list item 1a
23+
2. list item 2a
24+
- list item 2a.a
25+
1. list item 2
26+
- list item 2a
27+
`;
28+
1429
const ContentGroupSimpleKnobs = {
1530
heading: 'Lorem ipsum dolor sit amet',
1631
copy:
@@ -52,7 +67,11 @@ const ContentGroupSimpleKnobs = {
5267
{
5368
heading: 'Lorem ipsum dolor sit amet.',
5469
copy:
55-
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.',
70+
'Lorem ipsum dolor sit amet, *consectetur* adipiscing elit. Vivamus sed interdum tortor. Sed id pellentesque diam. In ut quam id mauris finibus efficitur quis ut arcu. Praesent purus turpis, venenatis eget odio et, tincidunt bibendum sem. Curabitur pretium elit non blandit lobortis. Donec quis pretium odio, in dignissim sapien.',
71+
},
72+
{
73+
heading: 'Lorem ipsum dolor sit amet.',
74+
copy: copyWithList,
5675
},
5776
{
5877
heading: 'Lorem ipsum dolor sit amet.',

packages/react/src/patterns/sections/CTASection/__stories__/CTASection.stories.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@ const types = ['local', 'external', 'default'];
1515
const contentItemsProps = [
1616
{
1717
heading: 'Get connected',
18-
copy: `
19-
IBM DevOps partners have a wide range of expertise.
20-
Find one to build the right solution for you.
21-
`,
18+
copy:
19+
'IBM DevOps partners have a wide range of expertise. Find one to build the right solution for you.',
2220
cta: {
2321
copy: 'Find a partner',
2422
type: types[0],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Copyright IBM Corp. 2016, 2020
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
@import '../../globals/imports';
9+
@import '../../temp-carbon-expressive/temp-carbon-expressive';
10+
@import 'carbon-components/scss/components/list/list';
11+
12+
@mixin list {
13+
.#{$prefix}--list--ordered,
14+
.#{$prefix}--list--unordered {
15+
@include carbon--type-style('body-long-02');
16+
}
17+
18+
.#{$prefix}--list--ordered {
19+
margin-left: 1rem;
20+
21+
&:not(.#{$prefix}--list--nested) {
22+
> .#{$prefix}--list__item {
23+
&::before {
24+
left: -1rem;
25+
}
26+
}
27+
}
28+
}
29+
30+
.#{$prefix}--list--unordered {
31+
margin-left: 1rem;
32+
33+
.#{$prefix}--list__item {
34+
&::before {
35+
left: -1rem;
36+
}
37+
}
38+
}
39+
40+
.#{$prefix}--list__item {
41+
margin-top: 0.25rem;
42+
}
43+
}
44+
45+
@include exports('list') {
46+
@include list;
47+
}

packages/styles/scss/internal/content-item/_content-item.scss

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
@import '../../globals/imports';
99
@import '../../globals/utils/content-width';
10+
@import '../../components/list/list';
1011

1112
@mixin content-item {
1213
.#{$prefix}--content-item--inverse {

packages/utilities/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,13 @@
6161
"cli-table": "^0.3.0",
6262
"core-js": "^3.1.3",
6363
"cross-env": "^5.2.0",
64+
"dompurify": "2.0.11",
6465
"fetch-mock": "^7.3.6",
6566
"gzip-size": "^5.0.0",
6667
"jest": "24.0.0",
6768
"jest-circus": "24.0.0",
6869
"jsdoc": "^3.6.2",
70+
"marked": "^1.1.0",
6971
"object-assign": "^4.1.1",
7072
"promise": "^8.0.1",
7173
"rimraf": "^2.6.3",

packages/utilities/src/utilities/markdownToHtml/__tests__/markdownToHtml.test.js

+24-26
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,46 @@
66
*/
77

88
import { markdownToHtml } from '../';
9-
import { settings } from 'carbon-components';
9+
import settings from 'carbon-components/es/globals/js/settings';
1010

1111
const { prefix } = settings;
1212

1313
describe('Markdown converter utility', () => {
1414
const str =
1515
'This <p>is</p> <input value="something" /> _italic_ and **bold**.';
16-
17-
const strNewLine =
18-
'This paragraph is created when two new lines are detected.';
16+
const link = '[This](https://www.ibm.com) is an anchor link.';
17+
const ol = `
18+
1. list item 1
19+
2. list item 2
20+
3. list item 3
21+
`;
22+
const ul = `
23+
- list item 1
24+
- list item 2
25+
- list item 3
26+
`;
1927

2028
it('returns the converted string with italic and bold', () => {
2129
const output = markdownToHtml(str);
22-
const expected = `<p>This is <em class="${prefix}--type-light">italic</em> and <strong class="${prefix}--type-semibold">bold</strong>.</p>`;
23-
expect(output).toBe(expected);
24-
});
25-
26-
it('returns the converted string with italic', () => {
27-
const output = markdownToHtml(str, { bold: false });
28-
const expected = `<p>This is <em class="${prefix}--type-light">italic</em> and **bold**.</p>`;
29-
expect(output).toBe(expected);
30+
const expected = `<p>This is <em>italic</em> and <strong>bold</strong>.</p>`;
31+
expect(output.trim()).toBe(expected);
3032
});
3133

32-
it('returns the converted string with bold', () => {
33-
const output = markdownToHtml(str, { italic: false });
34-
const expected = `<p>This is _italic_ and <strong class="${prefix}--type-semibold">bold</strong>.</p>`;
35-
expect(output).toBe(expected);
34+
it('returns the converted string with link', () => {
35+
const output = markdownToHtml(link);
36+
const expected = `<p><a href="https://www.ibm.com" class="${prefix}--link">This</a> is an anchor link.</p>`;
37+
expect(output.trim()).toBe(expected);
3638
});
3739

38-
it('returns the converted string in paragraphs', () => {
39-
const output = markdownToHtml(strNewLine, { bold: false, italic: false });
40-
const expected = `<p>This paragraph is created when two new lines are detected.</p>`;
40+
it('returns the converted string with an ordered list', () => {
41+
const output = markdownToHtml(ol);
42+
const expected = `<ol class="${prefix}--list--ordered"><li class="${prefix}--list__item">list item 1</li><li class="${prefix}--list__item">list item 2</li><li class="${prefix}--list__item">list item 3</li></ol>`;
4143
expect(output).toBe(expected);
4244
});
4345

44-
it('returns the converted string without carbon classes and allowing html', () => {
45-
const output = markdownToHtml(str, {
46-
allowHtml: true,
47-
useCarbonClasses: false,
48-
});
49-
const expected =
50-
'<p>This <p>is</p> <input value="something" /> <em>italic</em> and <strong>bold</strong>.</p>';
46+
it('returns the converted string with an unordered list', () => {
47+
const output = markdownToHtml(ul);
48+
const expected = `<ul class="${prefix}--list--unordered"><li class="${prefix}--list__item">list item 1</li><li class="${prefix}--list__item">list item 2</li><li class="${prefix}--list__item">list item 3</li></ul>`;
5149
expect(output).toBe(expected);
5250
});
5351
});

packages/utilities/src/utilities/markdownToHtml/markdownToHtml.js

+27-56
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
import DOMPurify from 'dompurify';
9+
import marked from 'marked';
810
import settings from 'carbon-components/es/globals/js/settings';
911
const { prefix } = settings;
1012

1113
const _htmlTagRegex = /<.*?>/g;
1214
const _cleanStringRegex = /\n|\s{2,}|&([a-zA-Z]+);/g;
13-
const _italicRegex = /[_*](.*?)[_*]/g;
14-
const _boldRegex = /[_*]{2}(.*?)[_*]{2}/g;
15-
const _paraRegex = /\n\n/g;
1615

1716
/**
1817
* Removes any html tags from a string and keeps inner text if any
@@ -34,75 +33,47 @@ const _removeHtmlTags = str => str.replace(_htmlTagRegex, '');
3433
const _cleanString = str => str.replace(_cleanStringRegex, ' ');
3534

3635
/**
37-
* Converts some markdown syntaxes into html
38-
* It's not a full markdown-to-html converter
39-
* It currently supports three syntaxes: <strong>Bold</strong>, <em>Italic</em>, and <p>Paragraph</p>
40-
*
41-
* Bold: Double asterisk (**) or double underscore (__)
42-
* Bold examples: **Lorem ipsum** __dolor__
43-
* Italic: Single asterisk (*) or single underscore (_)
44-
* Italic examples: _Lorem ipsum_ *dolor*
45-
* Paragraph: Double new line per paragraph
46-
* Paragraph examples: This is paragraph one.\n\nThis is paragraph two.
36+
* Converts markdown syntaxes into html
4737
*
4838
* @param {string} str String to convert to html
4939
* @param {object} [options={}] Object with options for the conversion
50-
* @param {boolean} [options.italic=true] Defines if should convert italic
51-
* @param {boolean} [options.bold=true] Defines if should convert bold
52-
* @param {boolean} [options.useCarbonClasses=true] Defines if should use carbon typography classes
5340
* @param {boolean} [options.allowHtml=false] Defines if should allow or remove html tags
54-
* @param {boolean} [options.cleanString=false] Defines if string should be cleaned of multiple spaces, html entities, or single new lines
55-
* @param {boolean} [options.createParagraphs=true] Defines if paragraphs should be rendered wrapped in <p> tags
5641
* @returns {string} String converted to html
5742
* @example
5843
* import { markdownToHtml } from '@carbon/ibmdotcom-utilities';
5944
*
6045
* markdownToHtml('Lorem *ipsum* dolor __sit__.')
6146
* // 'Lorem <em class="bx--type-light">ipsum</em> dolor <strong class="bx--type-semibold">sit</strong>.'
6247
*/
63-
function markdownToHtml(
64-
str,
65-
{
66-
italic = true,
67-
bold = true,
68-
useCarbonClasses = true,
69-
allowHtml = false,
70-
cleanString = false,
71-
createParagraphs = true,
72-
} = {}
73-
) {
74-
let paraList = '';
48+
function markdownToHtml(str, { allowHtml = false } = {}) {
7549
let converted = allowHtml ? str : _removeHtmlTags(str);
76-
converted = cleanString ? _cleanString(converted) : converted;
77-
const paras = converted.split(_paraRegex);
7850

79-
paras.map(para => {
80-
if (italic) {
81-
para = _cleanString(para).replace(_italicRegex, (match, p1) => {
82-
if (!p1.length) {
83-
return match;
84-
}
85-
return useCarbonClasses
86-
? `<em class="${prefix}--type-light">${p1}</em>`
87-
: `<em>${p1}</em>`;
88-
});
89-
}
51+
/**
52+
* Custom rendering options to add Carbon styles
53+
*
54+
*/
55+
const renderer = {
56+
link(href, title, text) {
57+
const linkTitle = title ? `title="${title}"` : null;
58+
return `<a class="${prefix}--link" href="${href}" ${linkTitle}>${text}</a>`;
59+
},
60+
list(body, ordered) {
61+
const listType = ordered ? 'ol' : 'ul';
62+
const listClass = ordered
63+
? `${prefix}--list--ordered`
64+
: `${prefix}--list--unordered`;
9065

91-
if (bold) {
92-
para = _cleanString(para).replace(_boldRegex, (match, p1) => {
93-
if (!p1.length) {
94-
return match;
95-
}
96-
return useCarbonClasses
97-
? `<strong class="${prefix}--type-semibold">${p1}</strong>`
98-
: `<strong>${p1}</strong>`;
99-
});
100-
}
66+
return `<${listType} class="${listClass}">${body}</${listType}>`;
67+
},
68+
listitem(text) {
69+
return `<li class="${prefix}--list__item">${text}</li>`;
70+
},
71+
};
10172

102-
paraList += createParagraphs ? `<p>${para}</p>` : para;
103-
});
73+
marked.use({ renderer });
74+
const convertedMarkdown = DOMPurify.sanitize(marked(converted));
10475

105-
return paraList;
76+
return _cleanString(convertedMarkdown);
10677
}
10778

10879
export default markdownToHtml;

0 commit comments

Comments
 (0)