Skip to content

Commit

Permalink
Export HTML utilities (#193)
Browse files Browse the repository at this point in the history
Export the markdown-to-HTML utilities separately from the React components, so that they can be used by consumers that need to convert Zooniverse-flavoured markdown to HTML strings.
  • Loading branch information
eatyourgreens authored Sep 17, 2023
1 parent a2da0a2 commit 657581a
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 116 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## [Unreleased](https://github.com/zooniverse/markdownz/tree/master) (2023-09-17)
Export the HTML utilities separately from the React components.
```js
import { utils } from 'markdownz';

const content = `
# A test document
This is a test [with a link](https://www.zooniverse.org).
`
const html = utils.getHTML({ content });
```

## [v8.0.7](https://github.com/zooniverse/markdownz/tree/v8.0.7) (2023-08-29)

* Bump @babel/cli from 7.22.5 to 7.22.6 by @dependabot in https://github.com/zooniverse/markdownz/pull/169
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ import { MarkdownHelp } from 'markdownz'
<MarkdownHelp talk={true} title={<h1>Guide to Markdown</h1>} />
```

Utilities:

```js
import { utils } from 'markdownz';

const content = `
# A test document
This is a test [with a link](https://www.zooniverse.org).
`
const html = utils.getHTML({ content });
```

## Supported Properties

### Viewer
Expand Down
80 changes: 9 additions & 71 deletions src/components/markdown.jsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,8 @@
import React from 'react';
import MarkdownIt from 'markdown-it';
import MarkdownItContainer from 'markdown-it-container';
import markdownEmoji from 'markdown-it-emoji';
import markdownSub from 'markdown-it-sub';
import markdownSup from 'markdown-it-sup';
import markdownFootnote from 'markdown-it-footnote';
import markdownImsize from 'markdown-it-imsize';
import markdownVideo from 'markdown-it-video';
import markdownTableOfContents from 'markdown-it-table-of-contents';
import markdownAnchor from 'markdown-it-anchor';
import twemoji from '@twemoji/api';
import html5Embed from 'markdown-it-html5-embed';
import { PureComponent } from 'react';

import * as utils from '../lib/utils';

import replaceSymbols from '../lib/default-transformer';
import relNofollow from '../lib/links-rel-nofollow';
import markdownNewTab from '../lib/links-in-new-tabs';

function markdownIt() {
return new MarkdownIt({ linkify: true, breaks: true })
.use(markdownEmoji)
.use(markdownSub)
.use(markdownSup)
.use(markdownFootnote)
.use(markdownImsize)
.use(markdownNewTab)
.use(markdownVideo)
.use(markdownAnchor)
.use(markdownTableOfContents)
.use(MarkdownItContainer, 'partners')
.use(MarkdownItContainer, 'attribution')
.use(html5Embed, {});
}

export default class Markdown extends React.Component {

markdownify(input) {
Markdown.counter += 1;
const id = this.props.idPrefix || (Date.now().toString(16) + Markdown.counter);
const env = { docId: id };
if (this.props && this.props.inline) {
return this.renderer().renderInline(input, env);
}

return this.renderer().render(input, env);
}

renderer() {
if (this.props && this.props.relNofollow) {
return markdownIt().use(relNofollow);
}
return markdownIt();
}
export default class Markdown extends PureComponent {

captureFootnoteLinks() {
const backrefs = '.footnote-ref > a, .footnote-backref';
Expand All @@ -70,24 +21,12 @@ export default class Markdown extends React.Component {
}

getHtml() {
const content = this.props.children || this.props.content;

try {
const { project, baseURI } = this.props;

if (typeof this.props.transform === 'function') {
const transformed = this.props.transform(content, { project, baseURI });
return this.emojify(this.markdownify(transformed));
}

return this.emojify(this.markdownify(content));
} catch (e) {
return content;
}
}

emojify(input) {
return twemoji.parse(input);
const { children, ...props } = this.props;
const content = children || this.props.content;
return utils.getHtml({
...props,
content
});
}

render() {
Expand All @@ -108,7 +47,6 @@ Markdown.defaultProps = {
tag: 'div',
content: '',
inline: false,
transform: replaceSymbols,
project: null,
baseURI: null,
className: '',
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as Markdown } from './components/markdown';
export { default as MarkdownEditor } from './components/markdown-editor';
export { default as MarkdownHelp } from './components/markdown-help';
export * as utils from './lib/utils';
83 changes: 83 additions & 0 deletions src/lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import MarkdownIt from 'markdown-it';
import MarkdownItContainer from 'markdown-it-container';
import markdownEmoji from 'markdown-it-emoji';
import markdownSub from 'markdown-it-sub';
import markdownSup from 'markdown-it-sup';
import markdownFootnote from 'markdown-it-footnote';
import markdownImsize from 'markdown-it-imsize';
import markdownVideo from 'markdown-it-video';
import markdownTableOfContents from 'markdown-it-table-of-contents';
import markdownAnchor from 'markdown-it-anchor';
import html5Embed from 'markdown-it-html5-embed';
import twemoji from '@twemoji/api';

import markdownNewTab from '../lib/links-in-new-tabs';
import relNofollow from '../lib/links-rel-nofollow';
import replaceSymbols from '../lib/default-transformer';

let counter = 0;

export function emojify(input) {
return twemoji.parse(input);
}

export function markdownz() {
return new MarkdownIt({ linkify: true, breaks: true })
.use(markdownEmoji)
.use(markdownSub)
.use(markdownSup)
.use(markdownFootnote)
.use(markdownImsize)
.use(markdownNewTab)
.use(markdownVideo)
.use(markdownAnchor)
.use(markdownTableOfContents)
.use(MarkdownItContainer, 'partners')
.use(MarkdownItContainer, 'attribution')
.use(html5Embed, {});
}

const baseRenderer = markdownz();
const noFollowRenderer = markdownz().use(relNofollow);

export function renderer({ relNoFollow = false }) {
return relNoFollow ? noFollowRenderer : baseRenderer;
}

export function markdownify({
idPrefix,
inline = false,
input,
relNoFollow = false
}) {
counter += 1;
const id = idPrefix || (Date.now().toString(16) + counter);
const env = { docId: id };
if (inline) {
return renderer({ relNoFollow }).renderInline(input, env);
}

return renderer({ relNoFollow }).render(input, env);
}

export function getHtml({
baseURI,
content,
idPrefix,
inline = false,
project,
relNoFollow = false,
transform = replaceSymbols
}) {
let input = content;
try {
if (typeof transform === 'function') {
input = transform(content, { project, baseURI });
}

const html = markdownify({ idPrefix, inline, input, relNoFollow });
return emojify(html);
} catch (e) {
return content;
}
}
47 changes: 2 additions & 45 deletions test/markdown-test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import TestUtils from 'react-dom/test-utils';
import { Markdown } from '../src/index';
import * as utils from '../src/lib/utils';

describe('Markdown', () => {
let markdown;
Expand All @@ -20,55 +21,11 @@ describe('Markdown', () => {
baseURI: null,
project: null,
relNofollow: false,
transform: Markdown.defaultProps.transform,
className: '',
idPrefix: null
});
});

describe('#markdownify', () => {
it('renders markdown', () => {
const md = markdown.markdownify('# test header');
expect(md).to.equal('<h1 id="test-header" tabindex="-1">test header</h1>\n');
});

it('opens links in a new tab when prefixed by +tab+', () => {
const md = markdown.markdownify('[A link](+tab+http://www.google.com)');
expect(md).to.equal('<p><a href="http://www.google.com" target="_blank" rel="noopener nofollow noreferrer">A link</a></p>\n');
});
});

describe('#getHtml', () => {
const errorTransform = () => {
throw new Error('fail');
};

it('returns the formatted html', () => {
let md = TestUtils.renderIntoDocument(React.createElement(Markdown, { className: 'MyComponent' }, 'Test text'));

let html = md.getHtml();
expect(html).to.equal('<p>Test text</p>\n');
});

it('renders bare child content on error', () => {
const md = TestUtils.renderIntoDocument(React.createElement(Markdown, { className: 'MyComponent', transform: errorTransform }, 'Test text'));
const html = md.getHtml();
expect(html).to.equal('Test text');
});
});

describe('#renderer', () => {
it('uses relNofollow when passed as a prop', () => {
const md = TestUtils.renderIntoDocument(React.createElement(Markdown, { className: 'MyComponent', relNofollow: true }, '[Test](link)'));
expect(md.getHtml()).to.equal('<p><a href="link" rel="nofollow noreferrer">Test</a></p>\n');
});

it('doesn\'t use relNofollow when not passed as a prop', () => {
const md = TestUtils.renderIntoDocument(React.createElement(Markdown, { className: 'MyComponent', relNofollow: false }, '[Test](link)'));
expect(md.getHtml()).to.equal('<p><a href="link">Test</a></p>\n');
});
});

describe('#render', () => {
let editor;
let md;
Expand All @@ -89,7 +46,7 @@ describe('Markdown', () => {
});

it('calls getHtml in render', () => {
const getHtmlSpy = spy.on(md, 'getHtml');
const getHtmlSpy = spy.on(utils, 'getHtml');
md.render();
expect(getHtmlSpy).to.have.been.called();
});
Expand Down
40 changes: 40 additions & 0 deletions test/utils-test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import TestUtils from 'react-dom/test-utils';
import * as utils from '../src/lib/utils';

describe('Utilities', () => {
describe('#markdownify', () => {
it('renders markdown', () => {
const md = utils.markdownify({ input: '# test header' });
expect(md).to.equal('<h1 id="test-header" tabindex="-1">test header</h1>\n');
});

it('opens links in a new tab when prefixed by +tab+', () => {
const md = utils.markdownify({ input: '[A link](+tab+http://www.google.com)' });
expect(md).to.equal('<p><a href="http://www.google.com" target="_blank" rel="noopener nofollow noreferrer">A link</a></p>\n');
});
});

describe('#getHtml', () => {
const errorTransform = () => {
throw new Error('fail');
};

it('returns the formatted html', () => {
let html = utils.getHtml({ content: 'Test text' });
expect(html).to.equal('<p>Test text</p>\n');
});

it('renders bare child content on error', () => {
const html = utils.getHtml({ content: 'Test text', transform: errorTransform });
expect(html).to.equal('Test text');
});

it('uses relNofollow when passed as a prop', () => {
expect(utils.getHtml({ content: '[Test](link)', relNoFollow: true })).to.equal('<p><a href="link" rel="nofollow noreferrer">Test</a></p>\n');
});

it('doesn\'t use relNofollow when not passed as a prop', () => {
expect(utils.getHtml({ content: '[Test](link)' })).to.equal('<p><a href="link">Test</a></p>\n');
});
});
});

0 comments on commit 657581a

Please sign in to comment.