Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export HTML utilities #193

Merged
merged 1 commit into from
Sep 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
});
});
});