From 581008d06699ebd6c08fbaaae6f627032c6650c9 Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Fri, 20 Jul 2018 17:30:43 -0700 Subject: [PATCH 01/20] slugging is its own class, tests, added - before count per GH behavior --- src/gh-slugify.js | 26 ++++++++++++++++++++++++++ src/index.js | 16 ++++------------ test/index.test.js | 8 ++++---- test/slug.test.js | 41 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 src/gh-slugify.js create mode 100644 test/slug.test.js diff --git a/src/gh-slugify.js b/src/gh-slugify.js new file mode 100644 index 0000000..58e4b53 --- /dev/null +++ b/src/gh-slugify.js @@ -0,0 +1,26 @@ +import slugify from 'slugify'; + + +export default class GithubSlugify { + + constructor() { + this.slugs = {}; + } + + slug(text) { + const slug = slugify(text, { lower: true }); + let uniqueSlug = slug; + + this.slugs[slug] = this.slugs[slug] || 0; + if (this.slugs[slug]) { + uniqueSlug = `${slug}-${this.slugs[slug]}`; + } + this.slugs[slug] += 1; + return uniqueSlug; + } + + reset() { + this.slugs = {}; + } + +} diff --git a/src/index.js b/src/index.js index 62b9cba..4f986ee 100755 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import ReactMarkdown from 'react-markdown'; -import slugify from 'slugify'; +import GithubSlugify from './gh-slugify'; import URL from 'url-parse'; const isHash = /^#/; @@ -19,11 +19,11 @@ const isHash = /^#/; export default class ReactMarkdownGithub extends Component { constructor() { super(...arguments); - + this.slugify = new GithubSlugify(); this.transformLinkUri = this.transformLinkUri.bind(this); this.renderHeading = this.renderHeading.bind(this); this.transformImageUri = this.transformImageUri.bind(this); - this.slugs = {}; + this.state = {}; } @@ -145,15 +145,7 @@ export default class ReactMarkdownGithub extends Component { } }); - const slug = slugify(title, { lower: true }); - let uniqueSlug = slug; - - this.slugs[slug] = this.slugs[slug] || 0; - if (this.slugs[slug]) { - uniqueSlug = `${slug}${this.slugs[slug]}`; - } - - this.slugs[slug] += 1; + const uniqueSlug = this.slugify.slug(title); // eslint-disable-next-line react/no-children-prop return React.createElement(`h${props.level}`, { diff --git a/test/index.test.js b/test/index.test.js index 9024c7f..bfb709a 100755 --- a/test/index.test.js +++ b/test/index.test.js @@ -146,10 +146,10 @@ Repeat Header`; assume(tree.find('#header').find('a').prop('href')).is.equal('#header'); // check duplicates - assume(tree.find('#header1')).to.have.length(1); - assume(tree.find('#header1').find('a').prop('href')).is.equal('#header1'); - assume(tree.find('#header2')).to.have.length(1); - assume(tree.find('#header2').find('a').prop('href')).is.equal('#header2'); + assume(tree.find('#header-1')).to.have.length(1); + assume(tree.find('#header-1').find('a').prop('href')).is.equal('#header-1'); + assume(tree.find('#header-2')).to.have.length(1); + assume(tree.find('#header-2').find('a').prop('href')).is.equal('#header-2'); assume(tree.find('#super-long-header')).to.have.length(1); assume(tree.find('#super-long-header').find('a').prop('href')).is.equal('#super-long-header'); diff --git a/test/slug.test.js b/test/slug.test.js new file mode 100644 index 0000000..7a1e8c1 --- /dev/null +++ b/test/slug.test.js @@ -0,0 +1,41 @@ +import assume from 'assume'; +import GithubSlugify from '../src/gh-slugify'; + + +describe('GithubSlugify', function () { + + it('Can create a GithubSlugify', () => { + const slug = new GithubSlugify(); + assume(slug).is.an('object'); + assume(slug.slug('this is neat')).equals('this-is-neat'); + }); + + + it('increments duplicates', () => { + const slug = new GithubSlugify(); + assume(slug.slug('this is neat')).equals('this-is-neat'); + assume(slug.slug('something else')).equals('something-else'); + assume(slug.slug('this is neat')).equals('this-is-neat-1'); + assume(slug.slug('this is neat')).equals('this-is-neat-2'); + }); + + it('non-text-chars', () => { + const slug = new GithubSlugify(); + assume(slug.slug(' a `code block` in the header')).equals('a-code-block-in-the-header'); + assume(slug.slug(' a question mark?')).equals('a- question-mark'); + }); + + + it('can reset unique counts', () => { + const slug = new GithubSlugify(); + assume(slug.slug('this is neat')).equals('this-is-neat'); + assume(slug.slug('something else')).equals('something-else'); + assume(slug.slug('this is neat')).equals('this-is-neat-1'); + assume(slug.slug('this is neat')).equals('this-is-neat-2'); + assume(slug.slug('something else')).equals('something-else-1'); + slug.reset(); + assume(slug.slug('this is neat')).equals('this-is-neat'); + assume(slug.slug('something else')).equals('something-else'); + }); + +}); From 51681f00c286d40f2941af7f728d5d9ea23fa34c Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Fri, 20 Jul 2018 17:59:55 -0700 Subject: [PATCH 02/20] markdown header reference file --- test/test.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 test/test.md diff --git a/test/test.md b/test/test.md new file mode 100644 index 0000000..ce2e8c0 --- /dev/null +++ b/test/test.md @@ -0,0 +1,22 @@ +# test +test + + +# test +second test + +# test? + +# a `code block` in the header + + +# something & something else + +# 2 starts with number + + +# unicode ♥ is ☢ + +# copy © + +# greek ∆ does something \ No newline at end of file From 7a7deaafbe441517e99ff7c6ded1be418f0d73bc Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Fri, 20 Jul 2018 18:00:21 -0700 Subject: [PATCH 03/20] adding a number of failing tests --- test/slug.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/slug.test.js b/test/slug.test.js index 7a1e8c1..339ee53 100644 --- a/test/slug.test.js +++ b/test/slug.test.js @@ -22,7 +22,11 @@ describe('GithubSlugify', function () { it('non-text-chars', () => { const slug = new GithubSlugify(); assume(slug.slug(' a `code block` in the header')).equals('a-code-block-in-the-header'); - assume(slug.slug(' a question mark?')).equals('a- question-mark'); + assume(slug.slug(' a question mark?')).equals('a-question-mark'); + assume(slug.slug('something & something else')).equals("something--something-else"); + assume(slug.slug('unicode ♥ is ☢')).equals("unicode--is-"); + assume(slug.slug('greek ∆ delta')).equals("greek--does-something"); + assume(slug.slug('copy © stuff ')).equals("copy-"); }); From ad3aeb600474c638d2cf280cffdc6d11bf45084d Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Fri, 20 Jul 2018 18:11:45 -0700 Subject: [PATCH 04/20] fixed text string --- test/slug.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/slug.test.js b/test/slug.test.js index 339ee53..cda8b59 100644 --- a/test/slug.test.js +++ b/test/slug.test.js @@ -25,7 +25,7 @@ describe('GithubSlugify', function () { assume(slug.slug(' a question mark?')).equals('a-question-mark'); assume(slug.slug('something & something else')).equals("something--something-else"); assume(slug.slug('unicode ♥ is ☢')).equals("unicode--is-"); - assume(slug.slug('greek ∆ delta')).equals("greek--does-something"); + assume(slug.slug('greek ∆ does something')).equals("greek--does-something"); assume(slug.slug('copy © stuff ')).equals("copy-"); }); From 3efecb910e0289f8eaa2886ef24cd3430c7ab8f8 Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Fri, 20 Jul 2018 18:13:20 -0700 Subject: [PATCH 05/20] update test text --- test/slug.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/slug.test.js b/test/slug.test.js index cda8b59..477cb27 100644 --- a/test/slug.test.js +++ b/test/slug.test.js @@ -26,7 +26,7 @@ describe('GithubSlugify', function () { assume(slug.slug('something & something else')).equals("something--something-else"); assume(slug.slug('unicode ♥ is ☢')).equals("unicode--is-"); assume(slug.slug('greek ∆ does something')).equals("greek--does-something"); - assume(slug.slug('copy © stuff ')).equals("copy-"); + assume(slug.slug('copy ©')).equals("copy-"); }); From 89a8004daf2b7eba1144435bfdfda372fe0d9de4 Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Mon, 23 Jul 2018 11:17:08 -0700 Subject: [PATCH 06/20] more tests --- test/test.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/test.md b/test/test.md index ce2e8c0..e649b36 100644 --- a/test/test.md +++ b/test/test.md @@ -19,4 +19,22 @@ second test # copy © -# greek ∆ does something \ No newline at end of file +# greek ∆ does something + +# Other punctuation, such as d.o.t.s, commas + +# Emdash –– and dash -- + +# Ampersand & + +# Backslashes// or slashes\\ + +# Complex code blocks like `/foo/bar/:bazz?buzz=foo` + +# Bold formatting **like this** + +# Italic formatting _like this_ + +# All !@# the $%^ colors &*( of ){} the |~ punctuation < "' rainbow += + +# Seriously all of them ... Alt + [q-|] œ∑´®†¥¨ˆøπ“‘« \ No newline at end of file From 110b8e509e0ca2c3af2f8ed624d85edfefc8055f Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Mon, 23 Jul 2018 17:34:50 -0700 Subject: [PATCH 07/20] replaced slugify with regex similar to GH code. --- src/gh-slugify.js | 19 +++++++++++++++++-- test/index.test.js | 8 ++++---- test/slug.test.js | 17 +++++++++++++++-- test/test.md | 2 ++ 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/gh-slugify.js b/src/gh-slugify.js index 58e4b53..af60861 100644 --- a/src/gh-slugify.js +++ b/src/gh-slugify.js @@ -1,14 +1,29 @@ -import slugify from 'slugify'; +/** + * This code is attempting to copy the Ruby pipeline filter GH uses to add unique ids to headings: + * https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb + * + */ +const replace = /[^a-zA-Z\- ]/g; +const whitespace = /\s/g; + export default class GithubSlugify { constructor() { this.slugs = {}; + this.replacementChar = '-'; + } + + replace (string) { + string = string.toLowerCase(); + return string.trim() + .replace(replace, '') + .replace(whitespace, this.replacementChar) } slug(text) { - const slug = slugify(text, { lower: true }); + const slug = this.replace(text); let uniqueSlug = slug; this.slugs[slug] = this.slugs[slug] || 0; diff --git a/test/index.test.js b/test/index.test.js index bfb709a..23cb6b6 100755 --- a/test/index.test.js +++ b/test/index.test.js @@ -160,10 +160,10 @@ Repeat Header`; const input = '### `codething` in the header `moreCode` txt'; renderFullDom({ source: input }); - - assume(tree.find('#codething-in-the-header-morecode-txt')).to.have.length(1); - assume(tree.find('#codething-in-the-header-morecode-txt') - .find('a').prop('href')).is.equal('#codething-in-the-header-morecode-txt'); + console.log(tree.debug()); + assume(tree.find('#codething--in-the-header-morecode--txt')).to.have.length(1); + assume(tree.find('#codething--in-the-header-morecode--txt') + .find('a').prop('href')).is.equal('#codething--in-the-header-morecode--txt'); }); }); diff --git a/test/slug.test.js b/test/slug.test.js index 477cb27..f0ad633 100644 --- a/test/slug.test.js +++ b/test/slug.test.js @@ -22,11 +22,24 @@ describe('GithubSlugify', function () { it('non-text-chars', () => { const slug = new GithubSlugify(); assume(slug.slug(' a `code block` in the header')).equals('a-code-block-in-the-header'); + assume(slug.slug(' `codething` in the header `moreCode` txt')).equals('codething-in-the-header-morecode-txt'); + + assume(slug.slug(' a question mark?')).equals('a-question-mark'); assume(slug.slug('something & something else')).equals("something--something-else"); - assume(slug.slug('unicode ♥ is ☢')).equals("unicode--is-"); assume(slug.slug('greek ∆ does something')).equals("greek--does-something"); - assume(slug.slug('copy ©')).equals("copy-"); + assume(slug.slug('copy ©')).equals("copy-"); + assume(slug.slug('Other punctuation, such as d.o.t.s, commas')).equals("other-punctuation-such-as-dots-commas"); + assume(slug.slug('Emdash –– and dash --')).equals("emdash--and-dash---"); + assume(slug.slug('Ampersand &')).equals("ampersand-"); + assume(slug.slug('Backslashes// or slashes\\')).equals("backslashes-or-slashes"); + assume(slug.slug('Complex code blocks like `/foo/bar/:bazz?buzz=foo`')).equals("complex-code-blocks-like-foobarbazzbuzzfoo"); + assume(slug.slug(' Bold formatting **like this**')).equals("bold-formatting-like-this"); + assume(slug.slug('Italic formatting _like this_')).equals("italic-formatting-like-this"); + assume(slug.slug(' All !@# the $%^ colors &*( of ){} the |~ punctuation < "\' rainbow += ')).equals("all--the--colors--of--the--punctuation---rainbow-"); + // assume(slug.slug(' Seriously all of them ... Alt + [q-|] œ∑´®†¥¨ˆøπ“‘«')).equals("seriously-all-of-them--alt--q--œˆøπ"); + // assume(slug.slug('unicode ♥ is ☢')).equals("unicode--is-"); + }); diff --git a/test/test.md b/test/test.md index e649b36..1862795 100644 --- a/test/test.md +++ b/test/test.md @@ -9,6 +9,8 @@ second test # a `code block` in the header +# `codething` in the header `moreCode` txt + # something & something else From f2662695477487ffe8adbbf44e7d1febafb3550f Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Mon, 23 Jul 2018 17:46:08 -0700 Subject: [PATCH 08/20] linter fixes --- src/gh-slugify.js | 6 +-- test/index.test.js | 2 +- test/slug.test.js | 101 +++++++++++++++++++++++---------------------- 3 files changed, 55 insertions(+), 54 deletions(-) diff --git a/src/gh-slugify.js b/src/gh-slugify.js index af60861..ca9e246 100644 --- a/src/gh-slugify.js +++ b/src/gh-slugify.js @@ -1,7 +1,7 @@ /** * This code is attempting to copy the Ruby pipeline filter GH uses to add unique ids to headings: * https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb - * + * */ @@ -15,11 +15,11 @@ export default class GithubSlugify { this.replacementChar = '-'; } - replace (string) { + replace(string) { string = string.toLowerCase(); return string.trim() .replace(replace, '') - .replace(whitespace, this.replacementChar) + .replace(whitespace, this.replacementChar); } slug(text) { diff --git a/test/index.test.js b/test/index.test.js index 23cb6b6..f008c78 100755 --- a/test/index.test.js +++ b/test/index.test.js @@ -160,7 +160,7 @@ Repeat Header`; const input = '### `codething` in the header `moreCode` txt'; renderFullDom({ source: input }); - console.log(tree.debug()); + assume(tree.find('#codething--in-the-header-morecode--txt')).to.have.length(1); assume(tree.find('#codething--in-the-header-morecode--txt') .find('a').prop('href')).is.equal('#codething--in-the-header-morecode--txt'); diff --git a/test/slug.test.js b/test/slug.test.js index f0ad633..887a644 100644 --- a/test/slug.test.js +++ b/test/slug.test.js @@ -4,55 +4,56 @@ import GithubSlugify from '../src/gh-slugify'; describe('GithubSlugify', function () { - it('Can create a GithubSlugify', () => { - const slug = new GithubSlugify(); - assume(slug).is.an('object'); - assume(slug.slug('this is neat')).equals('this-is-neat'); - }); - - - it('increments duplicates', () => { - const slug = new GithubSlugify(); - assume(slug.slug('this is neat')).equals('this-is-neat'); - assume(slug.slug('something else')).equals('something-else'); - assume(slug.slug('this is neat')).equals('this-is-neat-1'); - assume(slug.slug('this is neat')).equals('this-is-neat-2'); - }); - - it('non-text-chars', () => { - const slug = new GithubSlugify(); - assume(slug.slug(' a `code block` in the header')).equals('a-code-block-in-the-header'); - assume(slug.slug(' `codething` in the header `moreCode` txt')).equals('codething-in-the-header-morecode-txt'); - - - assume(slug.slug(' a question mark?')).equals('a-question-mark'); - assume(slug.slug('something & something else')).equals("something--something-else"); - assume(slug.slug('greek ∆ does something')).equals("greek--does-something"); - assume(slug.slug('copy ©')).equals("copy-"); - assume(slug.slug('Other punctuation, such as d.o.t.s, commas')).equals("other-punctuation-such-as-dots-commas"); - assume(slug.slug('Emdash –– and dash --')).equals("emdash--and-dash---"); - assume(slug.slug('Ampersand &')).equals("ampersand-"); - assume(slug.slug('Backslashes// or slashes\\')).equals("backslashes-or-slashes"); - assume(slug.slug('Complex code blocks like `/foo/bar/:bazz?buzz=foo`')).equals("complex-code-blocks-like-foobarbazzbuzzfoo"); - assume(slug.slug(' Bold formatting **like this**')).equals("bold-formatting-like-this"); - assume(slug.slug('Italic formatting _like this_')).equals("italic-formatting-like-this"); - assume(slug.slug(' All !@# the $%^ colors &*( of ){} the |~ punctuation < "\' rainbow += ')).equals("all--the--colors--of--the--punctuation---rainbow-"); - // assume(slug.slug(' Seriously all of them ... Alt + [q-|] œ∑´®†¥¨ˆøπ“‘«')).equals("seriously-all-of-them--alt--q--œˆøπ"); - // assume(slug.slug('unicode ♥ is ☢')).equals("unicode--is-"); - - }); - - - it('can reset unique counts', () => { - const slug = new GithubSlugify(); - assume(slug.slug('this is neat')).equals('this-is-neat'); - assume(slug.slug('something else')).equals('something-else'); - assume(slug.slug('this is neat')).equals('this-is-neat-1'); - assume(slug.slug('this is neat')).equals('this-is-neat-2'); - assume(slug.slug('something else')).equals('something-else-1'); - slug.reset(); - assume(slug.slug('this is neat')).equals('this-is-neat'); - assume(slug.slug('something else')).equals('something-else'); - }); + it('Can create a GithubSlugify', () => { + const slug = new GithubSlugify(); + assume(slug).is.an('object'); + assume(slug.slug('this is neat')).equals('this-is-neat'); + }); + + + it('increments duplicates', () => { + const slug = new GithubSlugify(); + assume(slug.slug('this is neat')).equals('this-is-neat'); + assume(slug.slug('something else')).equals('something-else'); + assume(slug.slug('this is neat')).equals('this-is-neat-1'); + assume(slug.slug('this is neat')).equals('this-is-neat-2'); + }); + + it('non-text-chars', () => { + const slug = new GithubSlugify(); + assume(slug.slug(' a `code block` in the header')).equals('a-code-block-in-the-header'); + assume(slug.slug(' `codething` in the header `moreCode` txt')).equals('codething-in-the-header-morecode-txt'); + + + assume(slug.slug(' a question mark?')).equals('a-question-mark'); + assume(slug.slug('something & something else')).equals('something--something-else'); + assume(slug.slug('greek ∆ does something')).equals('greek--does-something'); + assume(slug.slug('copy ©')).equals('copy-'); + assume(slug.slug('Other punctuation, such as d.o.t.s, commas')).equals('other-punctuation-such-as-dots-commas'); + assume(slug.slug('Emdash –– and dash --')).equals('emdash--and-dash---'); + assume(slug.slug('Ampersand &')).equals('ampersand-'); + assume(slug.slug('Backslashes// or slashes\\')).equals('backslashes-or-slashes'); + assume(slug.slug('Complex code blocks like `/foo/bar/:bazz?buzz=foo`')).equals('complex-code-blocks-like-foobarbazzbuzzfoo'); + assume(slug.slug(' Bold formatting **like this**')).equals('bold-formatting-like-this'); + assume(slug.slug('Italic formatting _like this_')).equals('italic-formatting-like-this'); + assume(slug.slug(' All !@# the $%^ colors &*( of ){} the |~ punctuation < "\' rainbow += ')) + .equals('all--the--colors--of--the--punctuation---rainbow-'); + // assume(slug.slug(' Seriously all of them ... Alt + [q-|] œ∑´®†¥¨ˆøπ“‘«')).equals('seriously-all-of-them--alt--q--œˆøπ'); + // assume(slug.slug('unicode ♥ is ☢')).equals('unicode--is-'); + + }); + + + it('can reset unique counts', () => { + const slug = new GithubSlugify(); + assume(slug.slug('this is neat')).equals('this-is-neat'); + assume(slug.slug('something else')).equals('something-else'); + assume(slug.slug('this is neat')).equals('this-is-neat-1'); + assume(slug.slug('this is neat')).equals('this-is-neat-2'); + assume(slug.slug('something else')).equals('something-else-1'); + slug.reset(); + assume(slug.slug('this is neat')).equals('this-is-neat'); + assume(slug.slug('something else')).equals('something-else'); + }); }); From 4bf022c109399f2e5c1520235dd254dd9fb5be04 Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Mon, 23 Jul 2018 23:40:55 -0700 Subject: [PATCH 09/20] docs and cleanup --- src/gh-slugify.js | 47 +++++++++++++++++++++++++++++++++++++++++------ test/slug.test.js | 4 +--- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/gh-slugify.js b/src/gh-slugify.js index ca9e246..d8c49d0 100644 --- a/src/gh-slugify.js +++ b/src/gh-slugify.js @@ -1,13 +1,32 @@ /** - * This code is attempting to copy the Ruby pipeline filter GH uses to add unique ids to headings: + * This RegEx is attempting to copy the Ruby pipeline filter GH uses to add unique ids to headings: * https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb - * + * The replace isn't perfect as it doesn't correctly handle unicode characters. + * This is an area for improvement via future contribution. */ - -const replace = /[^a-zA-Z\- ]/g; +const replace = /[^a-zA-Z0-9\- ]/g; const whitespace = /\s/g; +/** + * A utility class that is used to create + * a normalized ID from a string of text: + * + * `This is my headline` becomes `this-is-my-headline` + * + * This is a statefull object such that duplicate + * occurances of the same normalized string will have + * sequence number apended to them. + * + * Passing `This is my headline` a second time becomes `this-is-my-headline-1` + * + * The normalization process is meant to mimic the headline + * linking behavior GitHub provides when it renders markdown + * to html. + * + * @class GithubSlugify + * @api public + */ export default class GithubSlugify { constructor() { @@ -15,13 +34,24 @@ export default class GithubSlugify { this.replacementChar = '-'; } + /** + * Convert the passed text into GH Slug. + * @api private + * @param {String} string - the txt to be converted to a slug. + * @returns {String} the text converted to a slug. + */ replace(string) { - string = string.toLowerCase(); - return string.trim() + return string.toLowerCase().trim() .replace(replace, '') .replace(whitespace, this.replacementChar); } + /** + * Generates a GH style slug from the passed text. + * @api public + * @param {String} text - the txt to be converted to a slug. + * @returns {String} the text converted to a slug. + */ slug(text) { const slug = this.replace(text); let uniqueSlug = slug; @@ -34,6 +64,11 @@ export default class GithubSlugify { return uniqueSlug; } + /** + * Resets the state of this object including + * the tracking of duplicate slugs. + * @api public + */ reset() { this.slugs = {}; } diff --git a/test/slug.test.js b/test/slug.test.js index 887a644..3a62f6b 100644 --- a/test/slug.test.js +++ b/test/slug.test.js @@ -23,8 +23,7 @@ describe('GithubSlugify', function () { const slug = new GithubSlugify(); assume(slug.slug(' a `code block` in the header')).equals('a-code-block-in-the-header'); assume(slug.slug(' `codething` in the header `moreCode` txt')).equals('codething-in-the-header-morecode-txt'); - - + assume(slug.slug(' 1) numbers in the 345 header ')).equals('1-numbers-in-the-345-header'); assume(slug.slug(' a question mark?')).equals('a-question-mark'); assume(slug.slug('something & something else')).equals('something--something-else'); assume(slug.slug('greek ∆ does something')).equals('greek--does-something'); @@ -40,7 +39,6 @@ describe('GithubSlugify', function () { .equals('all--the--colors--of--the--punctuation---rainbow-'); // assume(slug.slug(' Seriously all of them ... Alt + [q-|] œ∑´®†¥¨ˆøπ“‘«')).equals('seriously-all-of-them--alt--q--œˆøπ'); // assume(slug.slug('unicode ♥ is ☢')).equals('unicode--is-'); - }); From ab2187a33e21e28d1bdc06468eeef16203ee4303 Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Tue, 24 Jul 2018 12:28:09 -0700 Subject: [PATCH 10/20] change regex to match GH, added tests for _ and removed invalid tests --- src/gh-slugify.js | 2 +- test/slug.test.js | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/gh-slugify.js b/src/gh-slugify.js index d8c49d0..e8fee1e 100644 --- a/src/gh-slugify.js +++ b/src/gh-slugify.js @@ -5,7 +5,7 @@ * This is an area for improvement via future contribution. */ -const replace = /[^a-zA-Z0-9\- ]/g; +const replace = /[^\w\- ]/g; const whitespace = /\s/g; /** diff --git a/test/slug.test.js b/test/slug.test.js index 3a62f6b..2a90667 100644 --- a/test/slug.test.js +++ b/test/slug.test.js @@ -19,6 +19,11 @@ describe('GithubSlugify', function () { assume(slug.slug('this is neat')).equals('this-is-neat-2'); }); + it('trim white space', () => { + const slug = new GithubSlugify(); + assume(slug.slug(' lots of extra space ')).equals('lots-of-extra-space'); + }); + it('non-text-chars', () => { const slug = new GithubSlugify(); assume(slug.slug(' a `code block` in the header')).equals('a-code-block-in-the-header'); @@ -29,14 +34,17 @@ describe('GithubSlugify', function () { assume(slug.slug('greek ∆ does something')).equals('greek--does-something'); assume(slug.slug('copy ©')).equals('copy-'); assume(slug.slug('Other punctuation, such as d.o.t.s, commas')).equals('other-punctuation-such-as-dots-commas'); + assume(slug.slug(' random _ in my header ')).equals('random-_-in-my-header'); assume(slug.slug('Emdash –– and dash --')).equals('emdash--and-dash---'); assume(slug.slug('Ampersand &')).equals('ampersand-'); assume(slug.slug('Backslashes// or slashes\\')).equals('backslashes-or-slashes'); assume(slug.slug('Complex code blocks like `/foo/bar/:bazz?buzz=foo`')).equals('complex-code-blocks-like-foobarbazzbuzzfoo'); - assume(slug.slug(' Bold formatting **like this**')).equals('bold-formatting-like-this'); - assume(slug.slug('Italic formatting _like this_')).equals('italic-formatting-like-this'); + // In the order of operations Markdown formatting will be striped from the headline before generating the slug. + // assume(slug.slug(' Bold formatting **like this**')).equals('bold-formatting-like-this'); + // assume(slug.slug('Italic formatting _like this_')).equals('italic-formatting-like-this'); assume(slug.slug(' All !@# the $%^ colors &*( of ){} the |~ punctuation < "\' rainbow += ')) .equals('all--the--colors--of--the--punctuation---rainbow-'); + //We need full unicode support for these tests to match GH behavior. // assume(slug.slug(' Seriously all of them ... Alt + [q-|] œ∑´®†¥¨ˆøπ“‘«')).equals('seriously-all-of-them--alt--q--œˆøπ'); // assume(slug.slug('unicode ♥ is ☢')).equals('unicode--is-'); }); From 883fa496691e0e7bb776933ad034c22df9a2caea Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Tue, 24 Jul 2018 12:37:21 -0700 Subject: [PATCH 11/20] added changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2253960..3ed9419 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # `react-markdown-github` +[#4] improving headline ID/anchor rendering to be more GH compliant + ### 2.0.0 - [#2] **BREAKING** Handle hash URLs, provide both `{ filename, filepath }`. From c2c4f6c27c14aaa2483129d9ab107e2a81d47006 Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Tue, 24 Jul 2018 12:38:21 -0700 Subject: [PATCH 12/20] linter --- src/gh-slugify.js | 20 ++++++++++---------- test/slug.test.js | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/gh-slugify.js b/src/gh-slugify.js index e8fee1e..2496e55 100644 --- a/src/gh-slugify.js +++ b/src/gh-slugify.js @@ -1,29 +1,29 @@ /** * This RegEx is attempting to copy the Ruby pipeline filter GH uses to add unique ids to headings: * https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb - * The replace isn't perfect as it doesn't correctly handle unicode characters. - * This is an area for improvement via future contribution. + * The replace isn't perfect as it doesn't correctly handle unicode characters. + * This is an area for improvement via future contribution. */ const replace = /[^\w\- ]/g; const whitespace = /\s/g; /** - * A utility class that is used to create + * A utility class that is used to create * a normalized ID from a string of text: - * + * * `This is my headline` becomes `this-is-my-headline` - * + * * This is a statefull object such that duplicate - * occurances of the same normalized string will have + * occurances of the same normalized string will have * sequence number apended to them. - * + * * Passing `This is my headline` a second time becomes `this-is-my-headline-1` - * + * * The normalization process is meant to mimic the headline - * linking behavior GitHub provides when it renders markdown + * linking behavior GitHub provides when it renders markdown * to html. - * + * * @class GithubSlugify * @api public */ diff --git a/test/slug.test.js b/test/slug.test.js index 2a90667..f5f00b8 100644 --- a/test/slug.test.js +++ b/test/slug.test.js @@ -39,12 +39,12 @@ describe('GithubSlugify', function () { assume(slug.slug('Ampersand &')).equals('ampersand-'); assume(slug.slug('Backslashes// or slashes\\')).equals('backslashes-or-slashes'); assume(slug.slug('Complex code blocks like `/foo/bar/:bazz?buzz=foo`')).equals('complex-code-blocks-like-foobarbazzbuzzfoo'); - // In the order of operations Markdown formatting will be striped from the headline before generating the slug. - // assume(slug.slug(' Bold formatting **like this**')).equals('bold-formatting-like-this'); - // assume(slug.slug('Italic formatting _like this_')).equals('italic-formatting-like-this'); + // In the order of operations Markdown formatting will be striped from the headline before generating the slug. + // assume(slug.slug(' Bold formatting **like this**')).equals('bold-formatting-like-this'); + // assume(slug.slug('Italic formatting _like this_')).equals('italic-formatting-like-this'); assume(slug.slug(' All !@# the $%^ colors &*( of ){} the |~ punctuation < "\' rainbow += ')) .equals('all--the--colors--of--the--punctuation---rainbow-'); - //We need full unicode support for these tests to match GH behavior. + // We need full unicode support for these tests to match GH behavior. // assume(slug.slug(' Seriously all of them ... Alt + [q-|] œ∑´®†¥¨ˆøπ“‘«')).equals('seriously-all-of-them--alt--q--œˆøπ'); // assume(slug.slug('unicode ♥ is ☢')).equals('unicode--is-'); }); From 3ef07ceda82e1a8d340f2a82ce37f209930d88f3 Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Tue, 24 Jul 2018 12:40:35 -0700 Subject: [PATCH 13/20] fixed typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed9419..60d115f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # `react-markdown-github` -[#4] improving headline ID/anchor rendering to be more GH compliant +- [#4] improving headline ID/anchor rendering to be more GH compliant ### 2.0.0 From 871310d79159a3d53b5a86a6174d9528d67c1b76 Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Tue, 24 Jul 2018 12:45:38 -0700 Subject: [PATCH 14/20] remove slugify --- package-lock.json | 5 ----- package.json | 1 - 2 files changed, 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 828ea8b..c4c5192 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7215,11 +7215,6 @@ "is-fullwidth-code-point": "2.0.0" } }, - "slugify": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.0.tgz", - "integrity": "sha512-vvz+9ANt7CtdTHwJpfrsHOnGkgxky+CUPnvtzDZBZYFo/H/CdZkd5lJL7z7RqtH/x9QW/ItYYfHlcGf38CBK1w==" - }, "sortobject": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/sortobject/-/sortobject-1.1.1.tgz", diff --git a/package.json b/package.json index 9e29f7f..8117b67 100755 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ }, "dependencies": { "react-markdown": "^3.3.0", - "slugify": "^1.2.9", "url-parse": "^1.4.0" } } From c976d266af6a8169d54e2d9d43aeafc7687a49ee Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Tue, 24 Jul 2018 16:25:53 -0700 Subject: [PATCH 15/20] exporting GithubSlugify --- index.js | 7 +++++++ package.json | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 index.js diff --git a/index.js b/index.js new file mode 100644 index 0000000..f9359e2 --- /dev/null +++ b/index.js @@ -0,0 +1,7 @@ +import ReactMarkdownGithub from 'src/index.js'; +import GithubSlugify from 'src/gh-slugify.js'; + +export { + ReactMarkdownGithub, + GithubSlugify +}; diff --git a/package.json b/package.json index 8117b67..f8501ba 100755 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "react-markdown-github", "description": "React component that renders Markdown similarly to Github's formatting", "version": "2.0.0", - "main": "lib/index.js", - "browser": "lib/index.js", - "module": "src/index.js", + "main": "./index.js", + "browser": "./index.js", + "module": "./index.js", "babel": { "plugins": [ "transform-object-rest-spread" @@ -16,7 +16,7 @@ }, "scripts": { "lint": "eslint-godaddy-react src/*.js test/*.js", - "prepublishOnly": "mkdir -p lib && babel -o lib/index.js src/index.js", + "prepublishOnly": "mkdir -p lib && babel -d ./lib index.js src/*.js", "pretest": "npm run lint", "test": "nyc --reporter=text --reporter=json-summary npm run test:mocha", "test:mocha": "mocha --require test/setup ./test/*.test.js" From 65d3dc6d8abcd4b791d40ba6a3990ac00e97c4d5 Mon Sep 17 00:00:00 2001 From: Charlie Robbins Date: Thu, 26 Jul 2018 12:10:39 -0700 Subject: [PATCH 16/20] Move to canonical src/ style structure (#6) * [refactor dist] Move `src/index.js` to `src/component.js` * [refactor dist] Move index.js to src/index.js * [fix dist] Update main, browser, & module. --- index.js | 7 - package.json | 8 +- src/component.js | 197 +++++++++++++++++++++ src/index.js | 200 +--------------------- test/{index.test.js => component.test.js} | 2 +- 5 files changed, 207 insertions(+), 207 deletions(-) delete mode 100644 index.js create mode 100755 src/component.js mode change 100755 => 100644 src/index.js rename test/{index.test.js => component.test.js} (99%) diff --git a/index.js b/index.js deleted file mode 100644 index f9359e2..0000000 --- a/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import ReactMarkdownGithub from 'src/index.js'; -import GithubSlugify from 'src/gh-slugify.js'; - -export { - ReactMarkdownGithub, - GithubSlugify -}; diff --git a/package.json b/package.json index f8501ba..eb1ceb9 100755 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "react-markdown-github", "description": "React component that renders Markdown similarly to Github's formatting", "version": "2.0.0", - "main": "./index.js", - "browser": "./index.js", - "module": "./index.js", + "main": "./lib/index.js", + "browser": "./lib/index.js", + "module": "./src/index.js", "babel": { "plugins": [ "transform-object-rest-spread" @@ -16,7 +16,7 @@ }, "scripts": { "lint": "eslint-godaddy-react src/*.js test/*.js", - "prepublishOnly": "mkdir -p lib && babel -d ./lib index.js src/*.js", + "prepublishOnly": "mkdir -p lib && babel -d ./lib src/*.js", "pretest": "npm run lint", "test": "nyc --reporter=text --reporter=json-summary npm run test:mocha", "test:mocha": "mocha --require test/setup ./test/*.test.js" diff --git a/src/component.js b/src/component.js new file mode 100755 index 0000000..ab5223d --- /dev/null +++ b/src/component.js @@ -0,0 +1,197 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ReactMarkdown from 'react-markdown'; +import GithubSlugify from './gh-slugify'; +import URL from 'url-parse'; + +const isHash = /^#/; + +/** + * A react component that wraps [react-markdown](react-markdown) that: + * - links all headers with an anchor link. + * - resolves all relative links to absolute Github URLs based on the sourceUri of the document. + * e.g. /foo/bar.md becomes https://github.mycorp.com/org/component/blob/master/foo/bar.md + * - allows the parent component to override the resolved url + * + * @class ReactMarkdownGithub + * @api public + */ +export default class ReactMarkdownGithub extends Component { + constructor() { + super(...arguments); + this.slugify = new GithubSlugify(); + this.transformLinkUri = this.transformLinkUri.bind(this); + this.renderHeading = this.renderHeading.bind(this); + this.transformImageUri = this.transformImageUri.bind(this); + + this.state = {}; + } + + /** + * Parses url into usable github components. + * @param {String} uri - a valid Github url. + * @returns {Object} { github, org, repo, filename, filepath } + * @api private + */ + static normalizeGithubUrl(uri) { + const { origin, pathname } = new URL(uri); + const parts = pathname.split('/'); + const [, org, repo] = parts; + const filepath = `/${parts.slice(5).join('/')}`; + const filename = parts[parts.length - 1]; + + return { + github: `${origin}/`, + filepath, + filename, + org, + repo + }; + } + + /** + * React lifecyle method to ensure that the github url prop is parsed each time + * it is updated. + * @param {Object} nextProps - new component props + * @param {Object} prevState - prior component state + * @returns {Object} returns new state or null if not modified. + * @api private + */ + static getDerivedStateFromProps({ sourceUri }, prevState) { + if (sourceUri !== prevState.sourceUri) { + return { + sourceUri: sourceUri, + ...ReactMarkdownGithub.normalizeGithubUrl(sourceUri) + }; + } + return null; + } + + /** + * Converts the passed url until an absolute url. If the passed URL is absolute + * it will be returned unmodified. If the URL is realitive then it will be + * merged with the current `sourceUri` property. + * + * @param {String} uri - absolute or realitive URL. + * @returns {url} - will return a absolute URL. + * @api private + */ + normalizeLinkUri(uri) { + // Do not attempt to parse "pure" hashes since they + // are not fully qualified URLs by definition. This will + // not work for querystring plus hash, but Github does not + // support querystring so this is by design. + if (isHash.test(uri)) { + return uri; + } + + const withinFile = new RegExp(`.?/?${this.state.filename}#(.*)$`, 'i'); + const parsed = new URL(uri, this.props.sourceUri); + const isWithinFile = withinFile.test(uri); + + return isWithinFile + ? parsed.hash + : parsed.href; + } + + /** + * The callback handler from `ReactMarkdown` . + * + * @param {String} uri - Markdown link URL. + * @param {Object} children - Child Elements of the link. + * @param {String} title - link title. + * @returns {url} - will return a absolute URL. + * @api private + */ + transformLinkUri(uri, children, title) { + const { transformLinkUri } = this.props; + const normalized = this.normalizeLinkUri(uri); + const opts = { ...this.state, uri: normalized, children, title }; + return transformLinkUri && transformLinkUri(opts) || normalized; + } + + /** + * The callback handler from `ReactMarkdown` . + * + * @param {String} uri - Markdown image URL. + * @returns {url} - will return a absolute URL. + * @api private + */ + transformImageUri(uri) { + const { transformImageUri } = this.props; + const opts = { ...this.state, uri }; + return transformImageUri && transformImageUri(opts) || uri; + } + + /** + * The callback handler from `ReactMarkdown` . Generates an `A` anchor link + * around the Header text + * + * @param {Object} props - properties passed from `ReactMarkdown` + * @param {Int} props.level - The level of the header to render. used for + * generating + * @param {Array} props.children - Array of strings from the heading + * @returns {Component} - A react component for the linked header. + * @api private + */ + renderHeading(props) { + let title = ''; + + props.children.forEach((child) => { + if (child.props && child.props.children) { + title += child.props.children + ' '; + } else { + title += child; + } + }); + + const uniqueSlug = this.slugify.slug(title); + + // eslint-disable-next-line react/no-children-prop + return React.createElement(`h${props.level}`, { + id: uniqueSlug, + className: 'headline-primary', + children: + { props.children } + + }); + } + + /** + * @returns {ReactMarkdown} react tree + * @api private + */ + render() { + const renderers = { + heading: this.renderHeading, + ...this.props.renderers + }; + + return ( + + ); + } +} + +ReactMarkdownGithub.propTypes = { + /** {source} The Markdown content to be rendered by `ReactMarkdown` */ + source: PropTypes.string, + /** {sourceUri} The absolute url to the Github source document. All + * relative urls will be assumed to be realitve to this file: + * e.g. https://github.mycorp.com/org/component/blob/master/README.md' + */ + sourceUri: PropTypes.string, + /** {transformLinkUri} The callback function executed for each found URL */ + transformLinkUri: PropTypes.func, + /** {transformImageUri} The callback function executed for each found image */ + transformImageUri: PropTypes.func, + /** {renderers} the collection of resolvers to pass to `ReactMarkdown` */ + renderers: PropTypes.object, + /** {className} the css class to to pass to `ReactMarkdown` */ + className: PropTypes.string +}; diff --git a/src/index.js b/src/index.js old mode 100755 new mode 100644 index ab5223d..3fad208 --- a/src/index.js +++ b/src/index.js @@ -1,197 +1,7 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import ReactMarkdown from 'react-markdown'; -import GithubSlugify from './gh-slugify'; -import URL from 'url-parse'; +import ReactMarkdownGithub from 'src/component.js'; +import GithubSlugify from 'src/gh-slugify.js'; -const isHash = /^#/; - -/** - * A react component that wraps [react-markdown](react-markdown) that: - * - links all headers with an anchor link. - * - resolves all relative links to absolute Github URLs based on the sourceUri of the document. - * e.g. /foo/bar.md becomes https://github.mycorp.com/org/component/blob/master/foo/bar.md - * - allows the parent component to override the resolved url - * - * @class ReactMarkdownGithub - * @api public - */ -export default class ReactMarkdownGithub extends Component { - constructor() { - super(...arguments); - this.slugify = new GithubSlugify(); - this.transformLinkUri = this.transformLinkUri.bind(this); - this.renderHeading = this.renderHeading.bind(this); - this.transformImageUri = this.transformImageUri.bind(this); - - this.state = {}; - } - - /** - * Parses url into usable github components. - * @param {String} uri - a valid Github url. - * @returns {Object} { github, org, repo, filename, filepath } - * @api private - */ - static normalizeGithubUrl(uri) { - const { origin, pathname } = new URL(uri); - const parts = pathname.split('/'); - const [, org, repo] = parts; - const filepath = `/${parts.slice(5).join('/')}`; - const filename = parts[parts.length - 1]; - - return { - github: `${origin}/`, - filepath, - filename, - org, - repo - }; - } - - /** - * React lifecyle method to ensure that the github url prop is parsed each time - * it is updated. - * @param {Object} nextProps - new component props - * @param {Object} prevState - prior component state - * @returns {Object} returns new state or null if not modified. - * @api private - */ - static getDerivedStateFromProps({ sourceUri }, prevState) { - if (sourceUri !== prevState.sourceUri) { - return { - sourceUri: sourceUri, - ...ReactMarkdownGithub.normalizeGithubUrl(sourceUri) - }; - } - return null; - } - - /** - * Converts the passed url until an absolute url. If the passed URL is absolute - * it will be returned unmodified. If the URL is realitive then it will be - * merged with the current `sourceUri` property. - * - * @param {String} uri - absolute or realitive URL. - * @returns {url} - will return a absolute URL. - * @api private - */ - normalizeLinkUri(uri) { - // Do not attempt to parse "pure" hashes since they - // are not fully qualified URLs by definition. This will - // not work for querystring plus hash, but Github does not - // support querystring so this is by design. - if (isHash.test(uri)) { - return uri; - } - - const withinFile = new RegExp(`.?/?${this.state.filename}#(.*)$`, 'i'); - const parsed = new URL(uri, this.props.sourceUri); - const isWithinFile = withinFile.test(uri); - - return isWithinFile - ? parsed.hash - : parsed.href; - } - - /** - * The callback handler from `ReactMarkdown` . - * - * @param {String} uri - Markdown link URL. - * @param {Object} children - Child Elements of the link. - * @param {String} title - link title. - * @returns {url} - will return a absolute URL. - * @api private - */ - transformLinkUri(uri, children, title) { - const { transformLinkUri } = this.props; - const normalized = this.normalizeLinkUri(uri); - const opts = { ...this.state, uri: normalized, children, title }; - return transformLinkUri && transformLinkUri(opts) || normalized; - } - - /** - * The callback handler from `ReactMarkdown` . - * - * @param {String} uri - Markdown image URL. - * @returns {url} - will return a absolute URL. - * @api private - */ - transformImageUri(uri) { - const { transformImageUri } = this.props; - const opts = { ...this.state, uri }; - return transformImageUri && transformImageUri(opts) || uri; - } - - /** - * The callback handler from `ReactMarkdown` . Generates an `A` anchor link - * around the Header text - * - * @param {Object} props - properties passed from `ReactMarkdown` - * @param {Int} props.level - The level of the header to render. used for - * generating - * @param {Array} props.children - Array of strings from the heading - * @returns {Component} - A react component for the linked header. - * @api private - */ - renderHeading(props) { - let title = ''; - - props.children.forEach((child) => { - if (child.props && child.props.children) { - title += child.props.children + ' '; - } else { - title += child; - } - }); - - const uniqueSlug = this.slugify.slug(title); - - // eslint-disable-next-line react/no-children-prop - return React.createElement(`h${props.level}`, { - id: uniqueSlug, - className: 'headline-primary', - children: - { props.children } - - }); - } - - /** - * @returns {ReactMarkdown} react tree - * @api private - */ - render() { - const renderers = { - heading: this.renderHeading, - ...this.props.renderers - }; - - return ( - - ); - } -} - -ReactMarkdownGithub.propTypes = { - /** {source} The Markdown content to be rendered by `ReactMarkdown` */ - source: PropTypes.string, - /** {sourceUri} The absolute url to the Github source document. All - * relative urls will be assumed to be realitve to this file: - * e.g. https://github.mycorp.com/org/component/blob/master/README.md' - */ - sourceUri: PropTypes.string, - /** {transformLinkUri} The callback function executed for each found URL */ - transformLinkUri: PropTypes.func, - /** {transformImageUri} The callback function executed for each found image */ - transformImageUri: PropTypes.func, - /** {renderers} the collection of resolvers to pass to `ReactMarkdown` */ - renderers: PropTypes.object, - /** {className} the css class to to pass to `ReactMarkdown` */ - className: PropTypes.string +export { + ReactMarkdownGithub, + GithubSlugify }; diff --git a/test/index.test.js b/test/component.test.js similarity index 99% rename from test/index.test.js rename to test/component.test.js index f09b3ef..cd33c02 100755 --- a/test/index.test.js +++ b/test/component.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import ReactMarkdownGithub from '../src'; +import ReactMarkdownGithub from '../src/component'; import { mount } from 'enzyme'; import assume from 'assume'; import assumeEnzyme from 'assume-enzyme'; From e88b9a90d38fa1de0bfce6f38afdec607dccf642 Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Thu, 26 Jul 2018 15:12:24 -0700 Subject: [PATCH 17/20] fixed header spacing with non-text rendering. Now matches GH output. --- src/component.js | 2 +- test/component.test.js | 31 ++++++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/component.js b/src/component.js index ab5223d..e44156e 100755 --- a/src/component.js +++ b/src/component.js @@ -139,7 +139,7 @@ export default class ReactMarkdownGithub extends Component { props.children.forEach((child) => { if (child.props && child.props.children) { - title += child.props.children + ' '; + title += child.props.children; } else { title += child; } diff --git a/test/component.test.js b/test/component.test.js index cd33c02..25a7f16 100755 --- a/test/component.test.js +++ b/test/component.test.js @@ -156,14 +156,35 @@ Repeat Header`; assume(tree.find('#blort')).to.have.length(0); }); - it('A header with non text elements', () => { - const input = '### `codething` in the header `moreCode` txt'; + it('A header with code elements', () => { + const input = '### a `codething` in the header `moreCode` txt'; renderFullDom({ source: input }); - assume(tree.find('#codething--in-the-header-morecode--txt')).to.have.length(1); - assume(tree.find('#codething--in-the-header-morecode--txt') - .find('a').prop('href')).is.equal('#codething--in-the-header-morecode--txt'); + assume(tree.find('#a-codething-in-the-header-morecode-txt')).to.have.length(1); + assume(tree.find('#a-codething-in-the-header-morecode-txt') + .find('a').prop('href')).is.equal('#a-codething-in-the-header-morecode-txt'); + }); + + it('A header with bold element', () => { + const input = '### bold in the **bold** header '; + + renderFullDom({ source: input }); + + assume(tree.find('#bold-in-the-bold-header')).to.have.length(1); + assume(tree.find('#bold-in-the-bold-header') + .find('a').prop('href')).is.equal('#bold-in-the-bold-header'); + }); + + + it('A header with Italic element', () => { + const input = '### Italics in the header _Italics_'; + + renderFullDom({ source: input }); + + assume(tree.find('#italics-in-the-header-italics')).to.have.length(1); + assume(tree.find('#italics-in-the-header-italics') + .find('a').prop('href')).is.equal('#italics-in-the-header-italics'); }); }); From f17fb313bbb87a080590bb0efa49477abeaca15f Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Thu, 26 Jul 2018 15:49:41 -0700 Subject: [PATCH 18/20] tests now use index. fixed import path. --- src/index.js | 4 ++-- test/component.test.js | 2 +- test/slug.test.js | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index 3fad208..6aa0a44 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ -import ReactMarkdownGithub from 'src/component.js'; -import GithubSlugify from 'src/gh-slugify.js'; +import ReactMarkdownGithub from './component.js'; +import GithubSlugify from './gh-slugify.js'; export { ReactMarkdownGithub, diff --git a/test/component.test.js b/test/component.test.js index 25a7f16..5c5151c 100755 --- a/test/component.test.js +++ b/test/component.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import ReactMarkdownGithub from '../src/component'; +import { ReactMarkdownGithub } from '../src/'; import { mount } from 'enzyme'; import assume from 'assume'; import assumeEnzyme from 'assume-enzyme'; diff --git a/test/slug.test.js b/test/slug.test.js index f5f00b8..8fa3997 100644 --- a/test/slug.test.js +++ b/test/slug.test.js @@ -1,6 +1,5 @@ import assume from 'assume'; -import GithubSlugify from '../src/gh-slugify'; - +import { GithubSlugify } from '../src/'; describe('GithubSlugify', function () { From a4554c5219d0c3b706bcdf7861b942fbb95c4c51 Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Thu, 26 Jul 2018 15:54:59 -0700 Subject: [PATCH 19/20] path nits --- test/component.test.js | 2 +- test/slug.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/component.test.js b/test/component.test.js index 5c5151c..0af779f 100755 --- a/test/component.test.js +++ b/test/component.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { ReactMarkdownGithub } from '../src/'; +import { ReactMarkdownGithub } from '../src'; import { mount } from 'enzyme'; import assume from 'assume'; import assumeEnzyme from 'assume-enzyme'; diff --git a/test/slug.test.js b/test/slug.test.js index 8fa3997..4a731d3 100644 --- a/test/slug.test.js +++ b/test/slug.test.js @@ -1,5 +1,5 @@ import assume from 'assume'; -import { GithubSlugify } from '../src/'; +import { GithubSlugify } from '../src'; describe('GithubSlugify', function () { From 87ceb23861f068299274c93abc6860370a914ef0 Mon Sep 17 00:00:00 2001 From: Jason Cline Date: Fri, 27 Jul 2018 13:47:48 -0700 Subject: [PATCH 20/20] bump to 3.0 --- CHANGELOG.md | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ca97b..2749112 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # `react-markdown-github` +### 3.0.0 - [#4] improving headline ID/anchor rendering to be more GH compliant - [#5] **BREAKING** Change `resolver` prop to `transformLinkUri` to be consistent with `react-markdown` diff --git a/package.json b/package.json index eb1ceb9..7c973bd 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-markdown-github", "description": "React component that renders Markdown similarly to Github's formatting", - "version": "2.0.0", + "version": "3.0.0", "main": "./lib/index.js", "browser": "./lib/index.js", "module": "./src/index.js",