Refactor + add more tests (Part 1) (#847)

* Refactor mdToHtml out

* Refactor routing + move it to server instead of core

* Refactor & Add more tests for server utils

* Refactor isSeparateCss function from server & generate

* Refactor insertTableOfContents from server & generate + add tests

* undo small nits
This commit is contained in:
Endilie Yacop Sucipto 2018-07-14 02:31:37 +08:00 committed by Yangshun Tay
parent a7a214fb3a
commit defcbcc8ee
14 changed files with 322 additions and 235 deletions

View file

@ -0,0 +1,28 @@
---
id: pokemon-commands
title: Pokemon Commands
---
## Commands
<AUTOGENERATED_TABLE_OF_CONTENTS>
---
## Reference
### `pokemon-run`
Alias: `run`.
### `pokemon-fight`
Alias: `fight`
### `pokemon-bag`
Alias: `bag`
### `pokemon-rename`
Alias: `rename`

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`with custom heading levels 1`] = `
exports[`getTOC with custom heading levels 1`] = `
Array [
Object {
"children": Array [
@ -105,7 +105,7 @@ Array [
]
`;
exports[`with defaults 1`] = `
exports[`getTOC with defaults 1`] = `
Array [
Object {
"children": Array [
@ -185,3 +185,57 @@ Array [
},
]
`;
exports[`insertTOC AUTOGENERATED_TABLE_OF_CONTENTS does not exist 1`] = `
"## foo
### foo
### foo 1
## foo 1
## foo 2
### foo
#### 4th level headings
All 4th level headings should not be shown by default
## bar
### bar
#### bar
4th level heading should be ignored by default, but is should be always taken
into account, when generating slugs
### \`bar\`
#### \`bar\`
## bar
### bar
#### bar
## bar
"
`;
exports[`insertTOC AUTOGENERATED_TABLE_OF_CONTENTS exists 1`] = `
"
## Commands
- [\`pokemon-run\`](#pokemon-run)
- [\`pokemon-fight\`](#pokemon-fight)
- [\`pokemon-bag\`](#pokemon-bag)
- [\`pokemon-rename\`](#pokemon-rename)
---
## Reference
### \`pokemon-run\`
Alias: \`run\`.
### \`pokemon-fight\`
Alias: \`fight\`
### \`pokemon-bag\`
Alias: \`bag\`
### \`pokemon-rename\`
Alias: \`rename\`"
`;

View file

@ -1,33 +0,0 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const path = require('path');
const readFileSync = require('fs').readFileSync;
const getTOC = require('../getTOC');
const mdContents = readFileSync(
path.join(__dirname, '__fixtures__', 'getTOC.md'),
'utf8'
);
test('with defaults', () => {
const headings = getTOC(mdContents);
const headingsJson = JSON.stringify(headings);
expect(headings).toMatchSnapshot();
expect(headingsJson).toContain('bar-8'); // maximum unique bar index is 8
expect(headingsJson).not.toContain('4th level headings');
});
test('with custom heading levels', () => {
const headings = getTOC(mdContents, 'h2', ['h3', 'h4']);
const headingsJson = JSON.stringify(headings);
expect(headings).toMatchSnapshot();
expect(headingsJson).toContain('bar-8'); // maximum unique bar index is 8
expect(headingsJson).toContain('4th level headings');
});

View file

@ -1,196 +0,0 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const {
blogRouting,
docsRouting,
dotRouting,
feedRouting,
noExtRouting,
pageRouting,
sitemapRouting,
} = require('../routing');
describe('Blog routing', () => {
const blogRegex = blogRouting('/');
const blogRegex2 = blogRouting('/react/');
test('valid blog', () => {
expect('/blog/test.html').toMatch(blogRegex);
expect('/react/blog/test.html').toMatch(blogRegex2);
});
test('invalid blog', () => {
expect('/react/blog/test.html').not.toMatch(blogRegex);
expect('/blog/test.html').not.toMatch(blogRegex2);
});
test('assets not classified as blog', () => {
expect('/blog/assets/any.png').not.toMatch(blogRegex);
expect('/react/blog/assets/any.png').not.toMatch(blogRegex2);
});
test('docs not classified as blog', () => {
expect('/docs/en/blog.html').not.toMatch(blogRegex);
expect('/docs/en/blog/blog.html').not.toMatch(blogRegex);
expect('/react/docs/en/blog.html').not.toMatch(blogRegex2);
expect('/react/docs/en/blog/blog.html').not.toMatch(blogRegex2);
});
});
describe('Docs routing', () => {
const docsRegex = docsRouting('/');
const docsRegex2 = docsRouting('/reason/');
test('valid docs', () => {
expect('/docs/en/test.html').toMatch(docsRegex);
expect('/reason/docs/en/test.html').toMatch(docsRegex2);
});
test('invalid docs', () => {
expect('/reason/docs/en/test.html').not.toMatch(docsRegex);
expect('/docs/en/test.html').not.toMatch(docsRegex2);
});
test('assets not classified as docs', () => {
expect('/docs/en/notvalid.png').not.toMatch(docsRegex);
expect('/reason/docs/en/notvalid.png').not.toMatch(docsRegex2);
});
test('blog not classified as docs', () => {
expect('/blog/docs.html').not.toMatch(docsRegex);
expect('/blog/docs/docs.html').not.toMatch(docsRegex);
expect('/reason/blog/docs.html').not.toMatch(docsRegex2);
expect('/reason/blog/docs/docs.html').not.toMatch(docsRegex2);
});
});
describe('Dot routing', () => {
const dotRegex = dotRouting();
test('valid url with dot after last slash', () => {
expect('/docs/en/test.23').toMatch(dotRegex);
expect('/robots.hai.2').toMatch(dotRegex);
expect('/blog/1.2.3').toMatch(dotRegex);
expect('/this.is.my').toMatch(dotRegex);
});
test('html file is invalid', () => {
expect('/docs/en.html').not.toMatch(dotRegex);
expect('/users.html').not.toMatch(dotRegex);
expect('/blog/asdf.html').not.toMatch(dotRegex);
expect('/end/1234/asdf.html').not.toMatch(dotRegex);
expect('/test/lol.huam.html').not.toMatch(dotRegex);
});
test('extension-less url is not valid', () => {
expect('/reason/test').not.toMatch(dotRegex);
expect('/asdff').not.toMatch(dotRegex);
expect('/blog/asdf.ghg/').not.toMatch(dotRegex);
expect('/end/1234.23.55/').not.toMatch(dotRegex);
});
});
describe('Feed routing', () => {
const feedRegex = feedRouting('/');
const feedRegex2 = feedRouting('/reason/');
test('valid feed url', () => {
expect('/blog/atom.xml').toMatch(feedRegex);
expect('/blog/feed.xml').toMatch(feedRegex);
expect('/reason/blog/atom.xml').toMatch(feedRegex2);
expect('/reason/blog/feed.xml').toMatch(feedRegex2);
});
test('invalid feed url', () => {
expect('/blog/blog/feed.xml').not.toMatch(feedRegex);
expect('/blog/test.xml').not.toMatch(feedRegex);
expect('/reason/blog/atom.xml').not.toMatch(feedRegex);
expect('/reason/blog/feed.xml').not.toMatch(feedRegex);
expect('/blog/feed.xml/test.html').not.toMatch(feedRegex);
expect('/blog/atom.xml').not.toMatch(feedRegex2);
expect('/blog/feed.xml').not.toMatch(feedRegex2);
expect('/reason/blog/test.xml').not.toMatch(feedRegex2);
expect('/reason/blog/blog/feed.xml').not.toMatch(feedRegex2);
expect('/reason/blog/blog/atom.xml').not.toMatch(feedRegex2);
});
test('not a feed', () => {
expect('/blog/atom').not.toMatch(feedRegex);
expect('/reason/blog/feed').not.toMatch(feedRegex2);
});
});
describe('Extension-less url routing', () => {
const noExtRegex = noExtRouting();
test('valid no extension url', () => {
expect('/test').toMatch(noExtRegex);
expect('/reason/test').toMatch(noExtRegex);
});
test('url with file extension', () => {
expect('/robots.txt').not.toMatch(noExtRegex);
expect('/reason/robots.txt').not.toMatch(noExtRegex);
expect('/docs/en/docu.html').not.toMatch(noExtRegex);
expect('/reason/robots.html').not.toMatch(noExtRegex);
expect('/blog/atom.xml').not.toMatch(noExtRegex);
expect('/reason/sitemap.xml').not.toMatch(noExtRegex);
expect('/main.css').not.toMatch(noExtRegex);
expect('/reason/custom.css').not.toMatch(noExtRegex);
});
});
describe('Page routing', () => {
const pageRegex = pageRouting('/');
const pageRegex2 = pageRouting('/reason/');
test('valid page url', () => {
expect('/index.html').toMatch(pageRegex);
expect('/en/help.html').toMatch(pageRegex);
expect('/reason/index.html').toMatch(pageRegex2);
expect('/reason/ro/users.html').toMatch(pageRegex2);
});
test('docs not considered as page', () => {
expect('/docs/en/test.html').not.toMatch(pageRegex);
expect('/reason/docs/en/test.html').not.toMatch(pageRegex2);
});
test('blog not considered as page', () => {
expect('/blog/index.html').not.toMatch(pageRegex);
expect('/reason/blog/index.html').not.toMatch(pageRegex2);
});
test('not a page', () => {
expect('/yangshun.jpg').not.toMatch(pageRegex);
expect('/reason/endilie.png').not.toMatch(pageRegex2);
});
});
describe('Sitemap routing', () => {
const sitemapRegex = sitemapRouting('/');
const sitemapRegex2 = sitemapRouting('/reason/');
test('valid sitemap url', () => {
expect('/sitemap.xml').toMatch(sitemapRegex);
expect('/reason/sitemap.xml').toMatch(sitemapRegex2);
});
test('invalid sitemap url', () => {
expect('/reason/sitemap.xml').not.toMatch(sitemapRegex);
expect('/reason/sitemap.xml.html').not.toMatch(sitemapRegex);
expect('/sitemap/sitemap.xml').not.toMatch(sitemapRegex);
expect('/reason/sitemap/sitemap.xml').not.toMatch(sitemapRegex);
expect('/sitemap.xml').not.toMatch(sitemapRegex2);
});
test('not a sitemap', () => {
expect('/sitemap').not.toMatch(sitemapRegex);
expect('/reason/sitemap').not.toMatch(sitemapRegex2);
});
});

View file

@ -0,0 +1,60 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const path = require('path');
const readFileSync = require('fs').readFileSync;
const {getTOC, insertTOC} = require('../toc');
const {extractMetadata} = require('../../server/metadataUtils');
const getTOCmd = readFileSync(
path.join(__dirname, '__fixtures__', 'getTOC.md'),
'utf8'
);
const insertTOCmd = readFileSync(
path.join(__dirname, '__fixtures__', 'insertTOC.md'),
'utf8'
);
describe('getTOC', () => {
test('with defaults', () => {
const headings = getTOC(getTOCmd);
const headingsJson = JSON.stringify(headings);
expect(headings).toMatchSnapshot();
expect(headingsJson).toContain('bar-8'); // maximum unique bar index is 8
expect(headingsJson).not.toContain('4th level headings');
});
test('with custom heading levels', () => {
const headings = getTOC(getTOCmd, 'h2', ['h3', 'h4']);
const headingsJson = JSON.stringify(headings);
expect(headings).toMatchSnapshot();
expect(headingsJson).toContain('bar-8'); // maximum unique bar index is 8
expect(headingsJson).toContain('4th level headings');
});
});
describe('insertTOC', () => {
test('null or undefined content', () => {
expect(insertTOC(null)).toBeNull();
expect(insertTOC(undefined)).toBeUndefined();
});
test('AUTOGENERATED_TABLE_OF_CONTENTS does not exist', () => {
const rawContent = extractMetadata(getTOCmd).rawContent;
expect(insertTOC(rawContent)).toMatchSnapshot();
expect(insertTOC(rawContent)).toEqual(rawContent);
});
test('AUTOGENERATED_TABLE_OF_CONTENTS exists', () => {
const rawContent = extractMetadata(insertTOCmd).rawContent;
expect(insertTOC(rawContent)).toMatchSnapshot();
expect(insertTOC(rawContent)).not.toEqual(rawContent);
});
});

View file

@ -8,7 +8,7 @@
const React = require('react');
const siteConfig = require(`${process.cwd()}/siteConfig.js`);
const getTOC = require('../getTOC');
const {getTOC} = require('../toc');
const Link = ({hashLink, content}) => (
<a

View file

@ -1,48 +0,0 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
function blogRouting(baseUrl) {
return new RegExp(`^${baseUrl}blog/.*html$`);
}
function docsRouting(baseUrl) {
return new RegExp(`^${baseUrl}docs/.*html$`);
}
function dotRouting() {
return /(?!.*html$)^\/.*\.[^\n/]+$/;
}
function feedRouting(baseUrl) {
return new RegExp(`^${baseUrl}blog/(feed.xml|atom.xml)$`);
}
function noExtRouting() {
return /\/[^.]*\/?$/;
}
function pageRouting(baseUrl) {
const gr = regex => regex.toString().replace(/(^\/|\/$)/gm, '');
return new RegExp(
`(?!${gr(docsRouting(baseUrl))}|${gr(
blogRouting(baseUrl)
)})^${baseUrl}.*.html$`
);
}
function sitemapRouting(baseUrl) {
return new RegExp(`^${baseUrl}sitemap.xml$`);
}
module.exports = {
blogRouting,
docsRouting,
dotRouting,
feedRouting,
pageRouting,
noExtRouting,
sitemapRouting,
};

View file

@ -9,7 +9,7 @@ const Remarkable = require('remarkable');
const mdToc = require('markdown-toc');
const toSlug = require('./toSlug');
const tagToLevel = tag => Number(tag.slice(1));
const TABLE_OF_CONTENTS_TOKEN = '<AUTOGENERATED_TABLE_OF_CONTENTS>';
/**
* Returns a table of content from the headings
@ -18,16 +18,15 @@ const tagToLevel = tag => Number(tag.slice(1));
* Array of heading objects with `hashLink`, `content` and `children` fields
*
*/
module.exports = (content, headingTags = 'h2', subHeadingTags = 'h3') => {
function getTOC(content, headingTags = 'h2', subHeadingTags = 'h3') {
const tagToLevel = tag => Number(tag.slice(1));
const headingLevels = [].concat(headingTags).map(tagToLevel);
const subHeadingLevels = subHeadingTags
? [].concat(subHeadingTags).map(tagToLevel)
: [];
const allowedHeadingLevels = headingLevels.concat(subHeadingLevels);
const md = new Remarkable();
const headings = mdToc(content).json;
const toc = [];
const context = {};
let current;
@ -36,11 +35,9 @@ module.exports = (content, headingTags = 'h2', subHeadingTags = 'h3') => {
// we need always generate slugs to ensure, that we will have consistent
// slug indexes for headings with the same names
const hashLink = toSlug(heading.content, context);
if (!allowedHeadingLevels.includes(heading.lvl)) {
return;
}
const rawContent = mdToc.titleize(heading.content);
const entry = {
hashLink,
@ -48,7 +45,6 @@ module.exports = (content, headingTags = 'h2', subHeadingTags = 'h3') => {
content: md.renderInline(rawContent),
children: [],
};
if (headingLevels.includes(heading.lvl)) {
toc.push(entry);
current = entry;
@ -56,6 +52,25 @@ module.exports = (content, headingTags = 'h2', subHeadingTags = 'h3') => {
current.children.push(entry);
}
});
return toc;
}
// takes the content of a doc article and returns the content with a table of
// contents inserted
function insertTOC(rawContent) {
if (!rawContent || rawContent.indexOf(TABLE_OF_CONTENTS_TOKEN) === -1) {
return rawContent;
}
const filterRe = /^`[^`]*`/;
const headers = getTOC(rawContent, 'h3', null);
const tableOfContents = headers
.filter(header => filterRe.test(header.rawContent))
.map(header => ` - [${header.rawContent}](#${header.hashLink})`)
.join('\n');
return rawContent.replace(TABLE_OF_CONTENTS_TOKEN, tableOfContents);
}
module.exports = {
getTOC,
insertTOC,
};

View file

@ -30,7 +30,6 @@ function getPath(path, cleanUrl = false) {
? path.replace(/\/index.html$/, '')
: removeExtension(path);
}
return path;
}