Skip to content
This repository was archived by the owner on Nov 28, 2022. It is now read-only.

Commit aee9d82

Browse files
committed
feat: implement markdown/html in excerpt(#161)
1 parent f87fb13 commit aee9d82

File tree

7 files changed

+1770
-1345
lines changed

7 files changed

+1770
-1345
lines changed

example/swagger-files/response-schemas.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"/anything/array-of-primitives": {
1414
"get": {
1515
"summary": "Array of primitives",
16+
"description": "sadf<h1>this is a paragraph</h1> <img src='x' onerror='alert('charlie')> Test \n > this is markdown",
1617
"responses": {
1718
"200": {
1819
"description": "OK",
@@ -33,6 +34,7 @@
3334
"/anything/object": {
3435
"get": {
3536
"summary": "Object",
37+
"description": "<h3>this is a paragraph</h3>",
3638
"responses": {
3739
"200": {
3840
"description": "OK",
@@ -50,6 +52,7 @@
5052
"/anything/array-of-objects": {
5153
"get": {
5254
"summary": "Array of objects",
55+
"description": "Test \n > this is markdown",
5356
"responses": {
5457
"200": {
5558
"description": "OK",

packages/api-explorer/src/Doc.jsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const CodeSample = require('./CodeSample');
1414
const Response = require('./Response');
1515
const ResponseSchema = require('./ResponseSchema');
1616
const EndpointErrorBoundary = require('./EndpointErrorBoundary');
17+
const markdown = require('../../markdown');
1718

1819
const Oas = require('./lib/Oas');
1920
// const showCode = require('./lib/show-code');
@@ -315,10 +316,7 @@ class Doc extends React.Component {
315316
<h2>{doc.title}</h2>
316317
{doc.excerpt && (
317318
<div className="excerpt">
318-
{
319-
// eslint-disable-next-line react/no-danger
320-
<p dangerouslySetInnerHTML={{ __html: doc.excerpt }} />
321-
}
319+
{markdown(doc.excerpt)}
322320
</div>
323321
)}
324322
</header>

packages/markdown/__tests__/__snapshots__/index.test.js.snap

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

33
exports[`anchors 1`] = `
4-
"<p><a href=\\"http://example.com\\" target=\\"_self\\">link</a><br/>
5-
<a href=\\"\\" target=\\"_self\\">xss</a><br/>
6-
<a href=\\"/docs/slug\\" target=\\"_self\\" class=\\"doc-link\\" data-sidebar=\\"slug\\">doc</a><br/>
7-
<a href=\\"/reference#slug\\" target=\\"_self\\">ref</a><br/>
8-
<a href=\\"/blog/slug\\" target=\\"_self\\">blog</a><br/>
9-
<a href=\\"/page/slug\\" target=\\"_self\\">page</a><br/>
4+
"<p><a href=\\"http://example.com\\" target=\\"_self\\">link</a>
5+
<a href=\\"javascript:alert\\" target=\\"_self\\">xss</a>
6+
<a href=\\"/docs/slug\\" target=\\"_self\\" class=\\"doc-link\\" data-sidebar=\\"slug\\">doc</a>
7+
<a href=\\"/reference#slug\\" target=\\"_self\\">ref</a>
8+
<a href=\\"/blog/slug\\" target=\\"_self\\">blog</a>
9+
<a href=\\"/page/slug\\" target=\\"_self\\">page</a>
1010
</p>"
1111
`;
1212
1313
exports[`check list items 1`] = `
1414
"<ul>
15-
<li><input type=\\"checkbox\\" disabled=\\"\\"/> checklistitem1</li>
16-
<li><input type=\\"checkbox\\" checked=\\"\\" disabled=\\"\\"/> checklistitem1</li>
15+
<li class=\\"task-list-item\\"><input type=\\"checkbox\\" disabled=\\"\\"/> checklistitem1</li>
16+
<li class=\\"task-list-item\\"><input type=\\"checkbox\\" checked=\\"\\" disabled=\\"\\"/> checklistitem1</li>
1717
</ul>"
1818
`;
1919
@@ -25,13 +25,13 @@ exports[`code samples 1`] = `
2525
`;
2626
2727
exports[`emojis 1`] = `
28-
"<p><img src=\\"/img/emojis/joy.png\\" alt=\\":joy:\\" title=\\":joy:\\" class=\\"emoji\\" align=\\"absmiddle\\" height=\\"20\\" width=\\"20\\"/><br/>
29-
<i class=\\"fa fa-lock\\"></i><br/>
30-
:unknown-emoji:<br/>
28+
"<p>:joy:
29+
:fa-lock:
30+
:unknown-emoji:
3131
</p>"
3232
`;
3333
34-
exports[`glossary 1`] = `"<p></p>"`;
34+
exports[`glossary 1`] = `"<p>&lt;&gt;</p>"`;
3535
3636
exports[`headings 1`] = `
3737
"<h1 class=\\"header-scroll\\"><div class=\\"anchor waypoint\\" id=\\"section-h1\\"></div>h1<a class=\\"fa fa-anchor\\" href=\\"#section-h1\\"></a></h1>
@@ -55,10 +55,32 @@ exports[`list items 1`] = `
5555
exports[`should strip out inputs 1`] = `""`;
5656
5757
exports[`tables 1`] = `
58-
"<div class=\\"marked-table\\"><table>
59-
<thead><tr><th>Tables</th><th style=\\"text-align:center\\">Are</th><th style=\\"text-align:right\\">Cool</th></tr></thead>
60-
<tbody><tr><td>col 3 is</td><td style=\\"text-align:center\\">right-aligned</td><td style=\\"text-align:right\\">$1600</td></tr><tr><td>col 2 is</td><td style=\\"text-align:center\\">centered</td><td style=\\"text-align:right\\">$12</td></tr><tr><td>zebra stripes</td><td style=\\"text-align:center\\">are neat</td><td style=\\"text-align:right\\">$1</td></tr></tbody>
61-
</table></div>"
58+
"
59+
60+
61+
62+
63+
64+
65+
66+
67+
68+
69+
70+
71+
72+
73+
74+
75+
76+
77+
78+
79+
80+
81+
82+
83+
<div class=\\"marked-table\\"><table><thead><tr><th>Tables</th><th align=\\"center\\">Are</th><th align=\\"right\\">Cool</th></tr></thead><tbody><tr><td>col 3 is</td><td align=\\"center\\">right-aligned</td><td align=\\"right\\">$1600</td></tr><tr><td>col 2 is</td><td align=\\"center\\">centered</td><td align=\\"right\\">$12</td></tr><tr><td>zebra stripes</td><td align=\\"center\\">are neat</td><td align=\\"right\\">$1</td></tr></tbody></table></div>"
6284
`;
6385
64-
exports[`variables 1`] = `"<p><span>APIKEY</span></p>"`;
86+
exports[`variables 1`] = `"<p>&lt;&gt;</p>"`;

packages/markdown/__tests__/index.test.js

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,12 @@ test('should render nothing if nothing passed in', () => {
9696
expect(markdown('')).toBe(null);
9797
});
9898

99-
test('`correctnewlines` option', () => {
100-
expect(shallow(markdown('test\ntest\ntest', { correctnewlines: true })).html()).toBe(
101-
'<p>test\ntest\ntest</p>',
102-
);
103-
expect(shallow(markdown('test\ntest\ntest', { correctnewlines: false })).html()).toBe(
104-
'<p>test<br/>\ntest<br/>\ntest</p>',
105-
);
99+
test('should parse newline character', () => {
100+
expect(
101+
shallow(markdown('test\ntest\ntest'))
102+
.html()
103+
.includes('\n'),
104+
).toBe(true);
106105
});
107106

108107
test('variables', () => {
@@ -113,29 +112,34 @@ test('glossary', () => {
113112
expect(shallow(markdown(`<<glossary:term>>`)).html()).toMatchSnapshot();
114113
});
115114

116-
// TODO not sure if this needs to work or not?
117-
// Isn't it a good thing to always strip HTML?
118-
describe.skip('`stripHtml` option', () => {
119-
test('should allow html by default', () => {
120-
expect(markdown('<p>Test</p>')).toBe('<p><p>Test</p></p>\n');
121-
expect(markdown('<p>Test</p>', { stripHtml: false })).toBe('<p><p>Test</p></p>\n');
122-
});
115+
test('should escape unknown tags', () => {
116+
expect(shallow(markdown('<unknown-tag>Test</unknown-tag>')).html()).toBe('<p>Test</p>');
117+
});
123118

124-
test('should escape unknown tags', () => {
125-
expect(markdown('<unknown-tag>Test</unknown-tag>')).toBe(
126-
'<p>&lt;unknown-tag&gt;Test&lt;/unknown-tag&gt;</p>\n',
127-
);
128-
});
119+
test('should allow certain attributes', () => {
120+
expect(shallow(markdown('<p id="test">Test</p>')).html()).toBe(
121+
'<p id="user-content-test">Test</p>',
122+
);
123+
});
129124

130-
test('should allow certain attributes', () => {
131-
expect(markdown('<p id="test">Test</p>')).toBe('<p><p id="test">Test</p></p>\n');
132-
});
125+
test('should strip unknown attributes', () => {
126+
expect(shallow(markdown('<p unknown="test">Test</p>')).html()).toBe('<p>Test</p>');
127+
});
133128

134-
test('should strip unknown attributes', () => {
135-
expect(markdown('<p unknown="test">Test</p>')).toBe('<p><p>Test</p></p>\n');
136-
});
129+
test('should strip dangerous href', () => {
130+
expect(shallow(markdown('<a href="jAva script:alert(\'bravo\')">delta</a>')).html()).toBe(
131+
'<p><a href="" target="_self">delta</a></p>',
132+
);
133+
});
134+
135+
test('should strip dangerous iframe tag', () => {
136+
expect(
137+
shallow(markdown('<p><iframe src="javascript:alert(\'delta\')"></iframe></p>')).html(),
138+
).toBe('<p></p>');
139+
});
137140

138-
test('should escape everything if `stripHtml=true`', () => {
139-
expect(markdown('<p>Test</p>', { stripHtml: true })).toBe('<p>&lt;p&gt;Test&lt;/p&gt;</p>\n');
140-
});
141+
test('should strip dangerous img attributes', () => {
142+
expect(shallow(markdown('<img src="x" onerror="alert(\'charlie\')">')).html()).toBe(
143+
'<img src="x"/>',
144+
);
141145
});

packages/markdown/index.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
const React = require('react');
22
const remark = require('remark');
3-
const reactRenderer = require('remark-react');
4-
const breaks = require('remark-breaks');
53
const sanitize = require('hast-util-sanitize/lib/github.json');
64
const Variable = require('@readme/variable');
5+
const remarkRehype = require('remark-rehype');
6+
const rehypeRaw = require('rehype-raw');
7+
const remarkParse = require('remark-parse');
8+
const rehypeSanitize = require('rehype-sanitize');
9+
const rehypeReact = require('rehype-react');
10+
const rehype = require('rehype');
711

812
const variableParser = require('./variable-parser');
913
const gemojiParser = require('./gemoji-parser');
@@ -19,16 +23,22 @@ sanitize.ancestors.input = ['li'];
1923

2024
const GlossaryItem = require('./GlossaryItem');
2125

22-
module.exports = function markdown(text, opts = {}) {
26+
module.exports = function markdown(text) {
2327
if (!text) return null;
28+
// strip out unsafe html
29+
const parsedText = rehype()
30+
.use(rehypeSanitize)
31+
.processSync(text).contents;
2432

2533
return remark()
2634
.use(variableParser.sanitize(sanitize))
27-
.use(!opts.correctnewlines ? breaks : () => {})
2835
.use(gemojiParser.sanitize(sanitize))
29-
.use(reactRenderer, {
30-
sanitize,
31-
remarkReactComponents: {
36+
.use(remarkParse)
37+
.use(remarkRehype, { allowDangerousHTML: true })
38+
.use(rehypeRaw)
39+
.use(rehypeReact, {
40+
createElement: React.createElement,
41+
components: {
3242
'readme-variable': Variable,
3343
'readme-glossary-item': GlossaryItem,
3444
table: table(sanitize),
@@ -45,5 +55,5 @@ module.exports = function markdown(text, opts = {}) {
4555
div: props => React.createElement(React.Fragment, props),
4656
},
4757
})
48-
.processSync(text).contents;
58+
.processSync(parsedText).contents;
4959
};

0 commit comments

Comments
 (0)