diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ffbaed..ff7e1d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 10d3328..25c9412 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,19 @@ import { MarkdownHelp } from 'markdownz' Guide to Markdown} /> ``` +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 diff --git a/src/components/markdown.jsx b/src/components/markdown.jsx index eb4b1ba..7343313 100644 --- a/src/components/markdown.jsx +++ b/src/components/markdown.jsx @@ -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'; @@ -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() { @@ -108,7 +47,6 @@ Markdown.defaultProps = { tag: 'div', content: '', inline: false, - transform: replaceSymbols, project: null, baseURI: null, className: '', diff --git a/src/index.js b/src/index.js index 2482c35..62db60d 100644 --- a/src/index.js +++ b/src/index.js @@ -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'; diff --git a/src/lib/utils.js b/src/lib/utils.js new file mode 100644 index 0000000..7551503 --- /dev/null +++ b/src/lib/utils.js @@ -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; + } +} diff --git a/test/markdown-test.jsx b/test/markdown-test.jsx index 93b893d..48c1f03 100644 --- a/test/markdown-test.jsx +++ b/test/markdown-test.jsx @@ -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; @@ -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('

test header

\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('

A link

\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('

Test text

\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('

Test

\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('

Test

\n'); - }); - }); - describe('#render', () => { let editor; let md; @@ -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(); }); diff --git a/test/utils-test.jsx b/test/utils-test.jsx new file mode 100644 index 0000000..a87f50c --- /dev/null +++ b/test/utils-test.jsx @@ -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('

test header

\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('

A link

\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('

Test text

\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('

Test

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

Test

\n'); + }); + }); +});