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

Commit

Permalink
Get markdown working
Browse files Browse the repository at this point in the history
Refine interface a little bit, remove `configure()` function.

Allow passing in `opts` as a second parameter. Possible options:

- `correctnewlines` - comes from `project.flags.correctnewlines`
- `stripHtml` - passed in in certain cases

Outstanding issues:
https://github.com/readmeio/api-explorer/issues/29
https://github.com/readmeio/api-explorer/issues/28
  • Loading branch information
Dom Harrington committed Sep 13, 2017
1 parent 9639c07 commit fab9103
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 235 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render markdown 1`] = `
"<p><img src=\\"http://example.com/image.png\\" alt=\\"Image\\"></p>
<ul>
<li >listitem1</li><li style=\\"list-style: none\\" class=\\"checklist\\"><input type=\\"checkbox\\" disabled> checklistitem1</li></ul>
<div class=\\"marked-table\\"><table>
<thead>
<tr>
<th>Tables</th>
<th style=\\"text-align:center\\">Are</th>
<th style=\\"text-align:right\\">Cool</th>
</tr>
</thead>
<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>
</table></div>
<h1 class=\\"header-scroll\\"><div class=\\"anchor waypoint\\" id=\\"section-h1\\"></div>h1<a class=\\"fa fa-anchor\\" href=\\"#section-h1\\"></a></h1>
<h2 class=\\"header-scroll\\"><div class=\\"anchor waypoint\\" id=\\"section-h2\\"></div>h2<a class=\\"fa fa-anchor\\" href=\\"#section-h2\\"></a></h2>
<h3 class=\\"header-scroll\\"><div class=\\"anchor waypoint\\" id=\\"section-h3\\"></div>h3<a class=\\"fa fa-anchor\\" href=\\"#section-h3\\"></a></h3>
<p><a href=\\"http://example.com\\" target=\\"_self\\">link</a></p>
<p><img src=\\"/img/emojis/joy.png\\" alt=\\":joy+:\\" title=\\":joy:\\" class=\\"emoji\\" align=\\"absmiddle\\" height=\\"20\\" width=\\"20\\"><br><i class=\\"fa fa-lock\\"></i><br>:unknown-emoji:</p>
<pre><code class=\\"lang-js\\"><span class=\\"cm-s-tomorrow-night\\">var a = 1;</span>
</code></pre>
"
`;
24 changes: 24 additions & 0 deletions packages/api-explorer-ui/__tests__/lib/markdown.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
![Image](http://example.com/image.png)

- listitem1
- [ ] checklistitem1

| Tables | Are | Cool |
| ------------- |:-------------:| -----:|
| col 3 is | right-aligned | $1600 |
| col 2 is | centered | $12 |
| zebra stripes | are neat | $1 |

# h1
## h2
### h3

[link](http://example.com)

:joy:
:fa-lock:
:unknown-emoji:

```js
var a = 1;
```
23 changes: 23 additions & 0 deletions packages/api-explorer-ui/__tests__/lib/marked.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const fs = require('fs');
const markdown = require('../../src/lib/markdown');

const fixture = fs.readFileSync(`${__dirname}/markdown.txt`, 'utf8');

test('should render markdown', () => {
expect(markdown(fixture)).toMatchSnapshot();
});

test('should render empty string if nothing passed in', () => {
expect(markdown('')).toBe('');
});

test('`correctnewlines` option', () => {
expect(markdown('test\ntest\ntest', { correctnewlines: true })).toBe('<p>test\ntest\ntest</p>\n');
expect(markdown('test\ntest\ntest', { correctnewlines: false })).toBe('<p>test<br>test<br>test</p>\n');
});

test('`stripHtml` option', () => {
expect(markdown('<p>Test</p>')).toBe('<p><p>Test</p></p>\n');
expect(markdown('<p>Test</p>', { stripHtml: false })).toBe('<p><p>Test</p></p>\n');
expect(markdown('<p>Test</p>', { stripHtml: true })).toBe('<p>&lt;p&gt;Test&lt;/p&gt;</p>\n');
});
34 changes: 34 additions & 0 deletions packages/api-explorer-ui/src/lib/markdown/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const marked = require('marked');
const Emoji = require('./emojis.js').emoji;
const syntaxHighlighter = require('../../../../readme-syntax-highlighter');
const sanitizer = require('./sanitizer');
const renderer = require('./renderer');

const emojis = new Emoji();

module.exports = function markdown(text, opts = {}) {
marked.setOptions({
sanitize: true,
preserveNumbering: true,
renderer,
emoji(emojiText) {
const emoji = emojiText.replace(/[^-_+a-zA-Z0-9]/g, '').toLowerCase();
if (emoji.substr(0, 3) === 'fa-') {
return `<i class="fa ${emoji}"></i>`;
}
if (emojis.is(emoji)) {
return `<img src="/img/emojis/${emoji}.png" alt=":${emoji}+:" title=":${emoji}:" class="emoji" align="absmiddle" height="20" width="20">`;
}
return `:${emoji}:`;
},
highlight(code, language) {
if (!language) return undefined;
return syntaxHighlighter(code, language);
},
gfm: true,
breaks: !opts.correctnewlines,
sanitizer: opts.stripHtml ? undefined : sanitizer,
});
return marked(text);
};

125 changes: 125 additions & 0 deletions packages/api-explorer-ui/src/lib/markdown/renderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const marked = require('marked');

const renderer = new marked.Renderer();

renderer.image = function image(href, title, text) {
let out = `<img src="${href}" alt="${text}"`;
if (title && title.substr(0, 6) === 'style|') {
out += ` style="${title.substr(6).replace(/"/g, "'")}"`;
} else if (title && title.substr(0, 5) === 'right') {
out += ' style="float: right; margin 0 0 15px 15px;"';
} else if (title && title.substr(0, 4) === 'left') {
out += ' style="float: left; margin 0 15px 15px 0;"';
} else if (title) {
out += ` title="${title}"`;
}

out += this.options.xhtml ? '/>' : '>';
return out;
};

renderer.listitem = function listitem(text, val) {
const valAttr = val ? ` value="${val}"` : '';
if (/^\s*\[[x ]\]\s*/.test(text)) {
text = text
.replace(/^\s*\[ \]\s*/, '<input type="checkbox" disabled> ')
.replace(/^\s*\[x\]\s*/, '<input type="checkbox" checked disabled> ');
return `<li style="list-style: none" class="checklist">${text}</li>`;
}

return `<li ${valAttr}>${text}</li>`;
};

renderer.table = function table(header, body) {
return (
`${'<div class="marked-table"><table>\n<thead>\n'}${header}</thead>\n` +
`<tbody>\n${body}</tbody>\n</table></div>\n`
);
};

renderer.heading = function heading(text, level, raw) {
const id = this.options.headerPrefix + raw.toLowerCase().replace(/[^\w]+/g, '-');

return (
`<h${level} class="header-scroll">` +
`<div class="anchor waypoint" id="section-${id}"></div>${text}<a class="fa fa-anchor" href="#section-${id}"></a>` +
`</h${level}>\n`
);
};

renderer.link = function link(href, title, text) {
/* eslint no-param-reassign: 0 */
const doc = href.match(/^doc:([-_a-zA-Z0-9#]*)$/);
let isDoc = false;
let uiSref = false;

if (href.match(/^(data|javascript)[^a-zA-Z0-9/_-]/i)) {
// Avoid XSS
href = '';
}

if (doc) {
uiSref = `docs.show({'doc': '${doc[1]}'})`;
href = '';
isDoc = doc[1];
}

const ref = href.match(/^ref:([-_a-zA-Z0-9#]*)$/);
if (ref) {
const cat = '';
// TODO https://github.com/readmeio/api-explorer/issues/28
// if (req && req.project.appearance.categoriesAsDropdown) {
// cat = `/${req._referenceCategoryMap[ref[1]]}`;
// }
href = `/reference${cat}#${ref[1]}`;
}

const blog = href.match(/^blog:([-_a-zA-Z0-9#]*)$/);
if (blog) {
uiSref = `blog.show({'blog': '${blog[1]}'})`;
href = '';
}

const custompage = href.match(/^page:([-_a-zA-Z0-9#]*)$/);
if (custompage) {
uiSref = `custompages.show({'custompage': '${custompage[1]}'})`;
}

if (this.options.sanitize) {
let prot;
try {
prot = decodeURIComponent(unescape(href))
.replace(/[^\w:]/g, '')
.toLowerCase();
} catch (e) {
return '';
}
// eslint-disable-next-line no-script-url
if (prot.indexOf('javascript:') === 0) {
return '';
}
}

let out = '<a';

out += ` href="${href}"`;

if (uiSref) {
out += ` ui-sref="${uiSref}"`;
} else {
// This prevents full links from getting
// into a weird AJAX state
out += ' target="_self"';
}

if (title) {
out += ` title="${title}"`;
}
if (isDoc) {
out += ` class="doc-link" data-sidebar="${isDoc}"`;
}
out += `>${text}</a>`;
return out;
};

module.exports = renderer;
79 changes: 79 additions & 0 deletions packages/api-explorer-ui/src/lib/markdown/sanitizer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
function sanitizer(tag) {
// TODO: This is probably not secure enough; use html-sanatize when we move to the backend in hub2.
const tagName = tag.match(/<\/?([^>\s]+)/);

const allowedTags = [
'img',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'span',
'blockquote',
'p',
'a',
'ul',
'ol',
'nl',
'li',
'b',
'i',
'strong',
'em',
'strike',
'code',
'hr',
'br',
'div',
'table',
'thead',
'caption',
'tbody',
'tr',
'th',
'td',
'pre',
'dl',
'dd',
'dt',
'sub',
'sup',
'section',
];

const allowedAttrs = [
'class',
'id',
'style',
'cellpadding',
'cellspacing',
'width',
'align',
'height',
'colspan',
'href',
'name',
'target',
'src',
'title',
'alt',
];

if (allowedTags.indexOf(tagName[1]) <= -1) {
return tag.replace('<', '&lt;').replace('>', '&gt;');
}

let tagClean = tagName[0]; // add the tag here
tag.replace(/\s+([a-zA-Z0-9]+)=('.*?'|".*?")/g, (full, attr) => {
if (allowedAttrs.indexOf(attr) > -1) {
tagClean += full;
}
return '';
});

return `${tagClean}>`;
};

module.exports = sanitizer;
Loading

0 comments on commit fab9103

Please sign in to comment.