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 // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`with custom heading levels 1`] = ` exports[`getTOC with custom heading levels 1`] = `
Array [ Array [
Object { Object {
"children": Array [ "children": Array [
@ -105,7 +105,7 @@ Array [
] ]
`; `;
exports[`with defaults 1`] = ` exports[`getTOC with defaults 1`] = `
Array [ Array [
Object { Object {
"children": Array [ "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

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

View file

@ -9,7 +9,7 @@ const Remarkable = require('remarkable');
const mdToc = require('markdown-toc'); const mdToc = require('markdown-toc');
const toSlug = require('./toSlug'); 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 * 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 * 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 headingLevels = [].concat(headingTags).map(tagToLevel);
const subHeadingLevels = subHeadingTags const subHeadingLevels = subHeadingTags
? [].concat(subHeadingTags).map(tagToLevel) ? [].concat(subHeadingTags).map(tagToLevel)
: []; : [];
const allowedHeadingLevels = headingLevels.concat(subHeadingLevels); const allowedHeadingLevels = headingLevels.concat(subHeadingLevels);
const md = new Remarkable(); const md = new Remarkable();
const headings = mdToc(content).json; const headings = mdToc(content).json;
const toc = []; const toc = [];
const context = {}; const context = {};
let current; 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 // we need always generate slugs to ensure, that we will have consistent
// slug indexes for headings with the same names // slug indexes for headings with the same names
const hashLink = toSlug(heading.content, context); const hashLink = toSlug(heading.content, context);
if (!allowedHeadingLevels.includes(heading.lvl)) { if (!allowedHeadingLevels.includes(heading.lvl)) {
return; return;
} }
const rawContent = mdToc.titleize(heading.content); const rawContent = mdToc.titleize(heading.content);
const entry = { const entry = {
hashLink, hashLink,
@ -48,7 +45,6 @@ module.exports = (content, headingTags = 'h2', subHeadingTags = 'h3') => {
content: md.renderInline(rawContent), content: md.renderInline(rawContent),
children: [], children: [],
}; };
if (headingLevels.includes(heading.lvl)) { if (headingLevels.includes(heading.lvl)) {
toc.push(entry); toc.push(entry);
current = entry; current = entry;
@ -56,6 +52,25 @@ module.exports = (content, headingTags = 'h2', subHeadingTags = 'h3') => {
current.children.push(entry); current.children.push(entry);
} }
}); });
return toc; 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$/, '') ? path.replace(/\/index.html$/, '')
: removeExtension(path); : removeExtension(path);
} }
return path; return path;
} }

View file

@ -5,19 +5,11 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
const { const routing = require('../routing');
blogRouting,
docsRouting,
dotRouting,
feedRouting,
noExtRouting,
pageRouting,
sitemapRouting,
} = require('../routing');
describe('Blog routing', () => { describe('Blog routing', () => {
const blogRegex = blogRouting('/'); const blogRegex = routing.blog('/');
const blogRegex2 = blogRouting('/react/'); const blogRegex2 = routing.blog('/react/');
test('valid blog', () => { test('valid blog', () => {
expect('/blog/test.html').toMatch(blogRegex); expect('/blog/test.html').toMatch(blogRegex);
@ -43,8 +35,8 @@ describe('Blog routing', () => {
}); });
describe('Docs routing', () => { describe('Docs routing', () => {
const docsRegex = docsRouting('/'); const docsRegex = routing.docs('/');
const docsRegex2 = docsRouting('/reason/'); const docsRegex2 = routing.docs('/reason/');
test('valid docs', () => { test('valid docs', () => {
expect('/docs/en/test.html').toMatch(docsRegex); expect('/docs/en/test.html').toMatch(docsRegex);
@ -70,7 +62,7 @@ describe('Docs routing', () => {
}); });
describe('Dot routing', () => { describe('Dot routing', () => {
const dotRegex = dotRouting(); const dotRegex = routing.dotfiles();
test('valid url with dot after last slash', () => { test('valid url with dot after last slash', () => {
expect('/docs/en/test.23').toMatch(dotRegex); expect('/docs/en/test.23').toMatch(dotRegex);
@ -96,8 +88,8 @@ describe('Dot routing', () => {
}); });
describe('Feed routing', () => { describe('Feed routing', () => {
const feedRegex = feedRouting('/'); const feedRegex = routing.feed('/');
const feedRegex2 = feedRouting('/reason/'); const feedRegex2 = routing.feed('/reason/');
test('valid feed url', () => { test('valid feed url', () => {
expect('/blog/atom.xml').toMatch(feedRegex); expect('/blog/atom.xml').toMatch(feedRegex);
@ -126,7 +118,7 @@ describe('Feed routing', () => {
}); });
describe('Extension-less url routing', () => { describe('Extension-less url routing', () => {
const noExtRegex = noExtRouting(); const noExtRegex = routing.noExtension();
test('valid no extension url', () => { test('valid no extension url', () => {
expect('/test').toMatch(noExtRegex); expect('/test').toMatch(noExtRegex);
@ -146,8 +138,8 @@ describe('Extension-less url routing', () => {
}); });
describe('Page routing', () => { describe('Page routing', () => {
const pageRegex = pageRouting('/'); const pageRegex = routing.page('/');
const pageRegex2 = pageRouting('/reason/'); const pageRegex2 = routing.page('/reason/');
test('valid page url', () => { test('valid page url', () => {
expect('/index.html').toMatch(pageRegex); expect('/index.html').toMatch(pageRegex);
@ -173,8 +165,8 @@ describe('Page routing', () => {
}); });
describe('Sitemap routing', () => { describe('Sitemap routing', () => {
const sitemapRegex = sitemapRouting('/'); const sitemapRegex = routing.sitemap('/');
const sitemapRegex2 = sitemapRouting('/reason/'); const sitemapRegex2 = routing.sitemap('/reason/');
test('valid sitemap url', () => { test('valid sitemap url', () => {
expect('/sitemap.xml').toMatch(sitemapRegex); expect('/sitemap.xml').toMatch(sitemapRegex);

View file

@ -8,6 +8,24 @@ const path = require('path');
const fs = require('fs'); const fs = require('fs');
const utils = require('../utils'); const utils = require('../utils');
jest.mock('../env', () => ({
translation: {
enabled: true,
enabledLanguages: () => [
{
enabled: true,
name: 'English',
tag: 'en',
},
{
enabled: true,
name: '日本語',
tag: 'ja',
},
],
},
}));
describe('server utils', () => { describe('server utils', () => {
test('minify css', () => { test('minify css', () => {
const testCss = fs.readFileSync( const testCss = fs.readFileSync(
@ -21,4 +39,44 @@ describe('server utils', () => {
utils.minifyCss(testCss).then(css => expect(css).toMatchSnapshot()); utils.minifyCss(testCss).then(css => expect(css).toMatchSnapshot());
utils.minifyCss(notCss).catch(e => expect(e).toMatchSnapshot()); utils.minifyCss(notCss).catch(e => expect(e).toMatchSnapshot());
}); });
test('getLanguage', () => {
const testDocEnglish = path.join('translated_docs', 'en', 'test.md');
const testDocJapanese = path.join('translated_docs', 'ja', 'test.md');
const testDocJapaneseInSubfolder = path.join(
'translated_docs',
'ja',
'en',
'test.md'
);
const testDocInSubfolder = path.join('docs', 'ro', 'test.md');
const testDocNoLanguage = path.join('docs', 'test.md');
expect(utils.getLanguage(testDocEnglish, 'translated_docs')).toBe('en');
expect(utils.getLanguage(testDocJapanese, 'translated_docs')).toBe('ja');
expect(
utils.getLanguage(testDocJapaneseInSubfolder, 'translated_docs')
).toBe('ja');
expect(utils.getLanguage(testDocInSubfolder, 'docs')).toBeNull();
expect(utils.getLanguage(testDocNoLanguage, 'docs')).toBeNull();
});
test('getSubdir', () => {
const docA = path.join('docs', 'endiliey', 'a.md');
const docB = path.join('docs', 'nus', 'hackers', 'b.md');
const docC = path.join('docs', 'c.md');
const docD = path.join('website', 'translated_docs', 'wow', 'd.md');
const docE = path.join('website', 'translated_docs', 'lol', 'lah', 'e.md');
const docsDir = path.join('docs');
const translatedDir = path.join('website', 'translated_docs');
expect(utils.getSubDir(docA, docsDir)).toEqual('endiliey');
expect(utils.getSubDir(docA, translatedDir)).toBeNull();
expect(utils.getSubDir(docB, docsDir)).toEqual('nus/hackers');
expect(utils.getSubDir(docB, translatedDir)).toBeNull();
expect(utils.getSubDir(docC, docsDir)).toBeNull();
expect(utils.getSubDir(docC, translatedDir)).toBeNull();
expect(utils.getSubDir(docD, docsDir)).toBeNull();
expect(utils.getSubDir(docD, translatedDir)).toEqual('wow');
expect(utils.getSubDir(docE, docsDir)).toBeNull();
expect(utils.getSubDir(docE, translatedDir)).toEqual('lol/lah');
});
}); });

View file

@ -14,9 +14,9 @@ async function execute() {
const fs = require('fs-extra'); const fs = require('fs-extra');
const readMetadata = require('./readMetadata.js'); const readMetadata = require('./readMetadata.js');
const path = require('path'); const path = require('path');
const getTOC = require('../core/getTOC'); const {insertTOC} = require('../core/toc');
const utils = require('../core/utils.js'); const {getPath} = require('../core/utils.js');
const serverUtils = require('./utils'); const {minifyCss, isSeparateCss} = require('./utils');
const React = require('react'); const React = require('react');
const mkdirp = require('mkdirp'); const mkdirp = require('mkdirp');
const glob = require('glob'); const glob = require('glob');
@ -53,36 +53,6 @@ async function execute() {
} }
} }
const TABLE_OF_CONTENTS_TOKEN = '<AUTOGENERATED_TABLE_OF_CONTENTS>';
// takes the content of a doc article and returns the content with a table of
// contents inserted
const insertTableOfContents = 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);
};
// returns true if a file should be excluded from concatentation to
// default Docusaurus styles
function isSeparateCss(file) {
if (!siteConfig.separateCss) {
return false;
}
for (let i = 0; i < siteConfig.separateCss.length; i++) {
if (file.includes(siteConfig.separateCss[i])) {
return true;
}
}
return false;
}
console.log('generate.js triggered...'); console.log('generate.js triggered...');
// array of tags of enabled languages // array of tags of enabled languages
@ -103,23 +73,7 @@ async function execute() {
const buildDir = join(CWD, 'build', siteConfig.projectName); const buildDir = join(CWD, 'build', siteConfig.projectName);
// mdToHtml is a map from a markdown file name to its html link, used to const mdToHtml = metadataUtils.mdToHtml(Metadata, siteConfig.baseUrl);
// change relative markdown links that work on GitHub into actual site links
const mdToHtml = {};
Object.keys(Metadata).forEach(id => {
const metadata = Metadata[id];
if (metadata.language !== 'en' || metadata.original_id) {
return;
}
let htmlLink =
siteConfig.baseUrl + metadata.permalink.replace('/next/', '/');
if (htmlLink.includes('/docs/en/')) {
htmlLink = htmlLink.replace('/docs/en/', '/docs/en/VERSION/');
} else {
htmlLink = htmlLink.replace('/docs/', '/docs/VERSION/');
}
mdToHtml[metadata.source] = htmlLink;
});
const DocsLayout = require('../core/DocsLayout.js'); const DocsLayout = require('../core/DocsLayout.js');
const Redirect = require('../core/Redirect.js'); const Redirect = require('../core/Redirect.js');
@ -154,16 +108,14 @@ async function execute() {
const language = metadata.language; const language = metadata.language;
// generate table of contents if appropriate // generate table of contents if appropriate
if (rawContent && rawContent.indexOf(TABLE_OF_CONTENTS_TOKEN) !== -1) { rawContent = insertTOC(rawContent);
rawContent = insertTableOfContents(rawContent);
}
const defaultVersion = env.versioning.defaultVersion; const defaultVersion = env.versioning.defaultVersion;
// replace any links to markdown files to their website html links // replace any links to markdown files to their website html links
Object.keys(mdToHtml).forEach(key => { Object.keys(mdToHtml).forEach(key => {
let link = mdToHtml[key]; let link = mdToHtml[key];
link = utils.getPath(link, siteConfig.cleanUrl); link = getPath(link, siteConfig.cleanUrl);
link = link.replace('/en/', `/${language}/`); link = link.replace('/en/', `/${language}/`);
link = link.replace( link = link.replace(
'/VERSION/', '/VERSION/',
@ -171,14 +123,9 @@ async function execute() {
? `/${metadata.version}/` ? `/${metadata.version}/`
: '/' : '/'
); );
// replace relative links without "./" // replace relative links with & without "./"
rawContent = rawContent.replace( rawContent = rawContent.replace(
new RegExp(`\\]\\(${key}`, 'g'), new RegExp(`\\]\\((${key}|\\./${key})`, 'g'),
`](${link}`
);
// replace relative links with "./"
rawContent = rawContent.replace(
new RegExp(`\\]\\(\\./${key}`, 'g'),
`](${link}` `](${link}`
); );
}); });
@ -204,10 +151,7 @@ async function execute() {
env.translation.enabled && env.translation.enabled &&
metadata.permalink.indexOf('docs/en') !== -1 metadata.permalink.indexOf('docs/en') !== -1
) { ) {
const redirectlink = utils.getPath( const redirectlink = getPath(metadata.permalink, siteConfig.cleanUrl);
metadata.permalink,
siteConfig.cleanUrl
);
const redirectComp = ( const redirectComp = (
<Redirect <Redirect
metadata={metadata} metadata={metadata}
@ -391,7 +335,10 @@ async function execute() {
// Remember the nuance of glob: https://www.npmjs.com/package/glob#windows // Remember the nuance of glob: https://www.npmjs.com/package/glob#windows
const normalizedFile = path.normalize(file); const normalizedFile = path.normalize(file);
// parse css files to replace colors and fonts according to siteConfig // parse css files to replace colors and fonts according to siteConfig
if (normalizedFile.match(/\.css$/) && !isSeparateCss(normalizedFile)) { if (
normalizedFile.match(/\.css$/) &&
!isSeparateCss(normalizedFile, siteConfig.separateCss)
) {
const mainCss = join(buildDir, 'css', 'main.css'); const mainCss = join(buildDir, 'css', 'main.css');
let cssContent = fs.readFileSync(normalizedFile, 'utf8'); let cssContent = fs.readFileSync(normalizedFile, 'utf8');
cssContent = `${fs.readFileSync(mainCss, 'utf8')}\n${cssContent}`; cssContent = `${fs.readFileSync(mainCss, 'utf8')}\n${cssContent}`;
@ -445,7 +392,7 @@ async function execute() {
// Use cssnano to minify the final combined CSS. // Use cssnano to minify the final combined CSS.
const mainCss = join(buildDir, 'css', 'main.css'); const mainCss = join(buildDir, 'css', 'main.css');
const cssContent = fs.readFileSync(mainCss, 'utf8'); const cssContent = fs.readFileSync(mainCss, 'utf8');
const css = await serverUtils.minifyCss(cssContent); const css = await minifyCss(cssContent);
fs.writeFileSync(mainCss, css); fs.writeFileSync(mainCss, css);
// compile/copy pages from user // compile/copy pages from user

View file

@ -63,6 +63,27 @@ function extractMetadata(content) {
return {metadata, rawContent: both.content}; return {metadata, rawContent: both.content};
} }
// mdToHtml is a map from a markdown file name to its html link, used to
// change relative markdown links that work on GitHub into actual site links
function mdToHtml(Metadata, baseUrl) {
const result = {};
Object.keys(Metadata).forEach(id => {
const metadata = Metadata[id];
if (metadata.language !== 'en' || metadata.original_id) {
return;
}
let htmlLink = baseUrl + metadata.permalink.replace('/next/', '/');
if (htmlLink.includes('/docs/en/')) {
htmlLink = htmlLink.replace('/docs/en/', '/docs/en/VERSION/');
} else {
htmlLink = htmlLink.replace('/docs/', '/docs/VERSION/');
}
result[metadata.source] = htmlLink;
});
return result;
}
module.exports = { module.exports = {
extractMetadata, extractMetadata,
mdToHtml,
}; };

View file

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

View file

@ -18,17 +18,9 @@ function execute(port, options) {
const request = require('request'); const request = require('request');
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const getTOC = require('../core/getTOC'); const {insertTOC} = require('../core/toc');
const utils = require('../core/utils'); const {getPath} = require('../core/utils');
const { const {isSeparateCss} = require('./utils');
blogRouting,
docsRouting,
dotRouting,
feedRouting,
pageRouting,
noExtRouting,
sitemapRouting,
} = require('../core/routing');
const mkdirp = require('mkdirp'); const mkdirp = require('mkdirp');
const glob = require('glob'); const glob = require('glob');
const chalk = require('chalk'); const chalk = require('chalk');
@ -41,6 +33,7 @@ function execute(port, options) {
const feed = require('./feed'); const feed = require('./feed');
const sitemap = require('./sitemap'); const sitemap = require('./sitemap');
const routing = require('./routing');
const CWD = process.cwd(); const CWD = process.cwd();
@ -101,32 +94,6 @@ function execute(port, options) {
} }
} }
const TABLE_OF_CONTENTS_TOKEN = '<AUTOGENERATED_TABLE_OF_CONTENTS>';
const insertTableOfContents = 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);
};
function isSeparateCss(file) {
if (!siteConfig.separateCss) {
return false;
}
for (let i = 0; i < siteConfig.separateCss.length; i++) {
if (file.includes(siteConfig.separateCss[i])) {
return true;
}
}
return false;
}
function requestFile(url, res, notFoundCallback) { function requestFile(url, res, notFoundCallback) {
request.get(url, (error, response, body) => { request.get(url, (error, response, body) => {
if (!error) { if (!error) {
@ -183,7 +150,7 @@ function execute(port, options) {
const app = express(); const app = express();
// handle all requests for document pages // handle all requests for document pages
app.get(docsRouting(siteConfig.baseUrl), (req, res, next) => { app.get(routing.docs(siteConfig.baseUrl), (req, res, next) => {
const url = req.path.toString().replace(siteConfig.baseUrl, ''); const url = req.path.toString().replace(siteConfig.baseUrl, '');
// links is a map from a permalink to an id for each document // links is a map from a permalink to an id for each document
@ -193,23 +160,7 @@ function execute(port, options) {
links[metadata.permalink] = id; links[metadata.permalink] = id;
}); });
// mdToHtml is a map from a markdown file name to its html link, used to const mdToHtml = metadataUtils.mdToHtml(Metadata, siteConfig.baseUrl);
// change relative markdown links that work on GitHub into actual site links
const mdToHtml = {};
Object.keys(Metadata).forEach(id => {
const metadata = Metadata[id];
if (metadata.language !== 'en' || metadata.original_id) {
return;
}
let htmlLink =
siteConfig.baseUrl + metadata.permalink.replace('/next/', '/');
if (htmlLink.includes('/docs/en/')) {
htmlLink = htmlLink.replace('/docs/en/', '/docs/en/VERSION/');
} else {
htmlLink = htmlLink.replace('/docs/', '/docs/VERSION/');
}
mdToHtml[metadata.source] = htmlLink;
});
const metadata = Metadata[links[url]]; const metadata = Metadata[links[url]];
if (!metadata) { if (!metadata) {
@ -242,16 +193,14 @@ function execute(port, options) {
).rawContent; ).rawContent;
// generate table of contents if appropriate // generate table of contents if appropriate
if (rawContent && rawContent.indexOf(TABLE_OF_CONTENTS_TOKEN) !== -1) { rawContent = insertTOC(rawContent);
rawContent = insertTableOfContents(rawContent);
}
const defaultVersion = env.versioning.defaultVersion; const defaultVersion = env.versioning.defaultVersion;
// replace any links to markdown files to their website html links // replace any links to markdown files to their website html links
Object.keys(mdToHtml).forEach(key => { Object.keys(mdToHtml).forEach(key => {
let link = mdToHtml[key]; let link = mdToHtml[key];
link = utils.getPath(link, siteConfig.cleanUrl); link = getPath(link, siteConfig.cleanUrl);
link = link.replace('/en/', `/${language}/`); link = link.replace('/en/', `/${language}/`);
link = link.replace( link = link.replace(
'/VERSION/', '/VERSION/',
@ -259,14 +208,9 @@ function execute(port, options) {
? `/${metadata.version}/` ? `/${metadata.version}/`
: '/' : '/'
); );
// replace relative links without "./" // replace relative links with & without "./"
rawContent = rawContent.replace( rawContent = rawContent.replace(
new RegExp(`\\]\\(${key}`, 'g'), new RegExp(`\\]\\((${key}|\\./${key})`, 'g'),
`](${link}`
);
// replace relative links with "./"
rawContent = rawContent.replace(
new RegExp(`\\]\\(\\./${key}`, 'g'),
`](${link}` `](${link}`
); );
}); });
@ -305,7 +249,7 @@ function execute(port, options) {
res.send(renderToStaticMarkupWithDoctype(docComp)); res.send(renderToStaticMarkupWithDoctype(docComp));
}); });
app.get(sitemapRouting(siteConfig.baseUrl), (req, res) => { app.get(routing.sitemap(siteConfig.baseUrl), (req, res) => {
sitemap((err, xml) => { sitemap((err, xml) => {
if (err) { if (err) {
res.status(500).send('Sitemap error'); res.status(500).send('Sitemap error');
@ -316,7 +260,7 @@ function execute(port, options) {
}); });
}); });
app.get(feedRouting(siteConfig.baseUrl), (req, res, next) => { app.get(routing.feed(siteConfig.baseUrl), (req, res, next) => {
res.set('Content-Type', 'application/rss+xml'); res.set('Content-Type', 'application/rss+xml');
const file = req.path const file = req.path
.toString() .toString()
@ -331,7 +275,7 @@ function execute(port, options) {
}); });
// Handle all requests for blog pages and posts. // Handle all requests for blog pages and posts.
app.get(blogRouting(siteConfig.baseUrl), (req, res, next) => { app.get(routing.blog(siteConfig.baseUrl), (req, res, next) => {
// Regenerate the blog metadata in case it has changed. Consider improving // Regenerate the blog metadata in case it has changed. Consider improving
// this to regenerate on file save rather than on page request. // this to regenerate on file save rather than on page request.
reloadMetadataBlog(); reloadMetadataBlog();
@ -421,7 +365,7 @@ function execute(port, options) {
}); });
// handle all other main pages // handle all other main pages
app.get(pageRouting(siteConfig.baseUrl), (req, res, next) => { app.get(routing.page(siteConfig.baseUrl), (req, res, next) => {
// Look for user-provided HTML file first. // Look for user-provided HTML file first.
let htmlFile = req.path.toString().replace(siteConfig.baseUrl, ''); let htmlFile = req.path.toString().replace(siteConfig.baseUrl, '');
htmlFile = join(CWD, 'pages', htmlFile); htmlFile = join(CWD, 'pages', htmlFile);
@ -541,7 +485,7 @@ function execute(port, options) {
const files = glob.sync(join(CWD, 'static', '**', '*.css')); const files = glob.sync(join(CWD, 'static', '**', '*.css'));
files.forEach(file => { files.forEach(file => {
if (isSeparateCss(file)) { if (isSeparateCss(file, siteConfig.separateCss)) {
return; return;
} }
cssContent = `${cssContent}\n${fs.readFileSync(file, { cssContent = `${cssContent}\n${fs.readFileSync(file, {
@ -596,7 +540,7 @@ function execute(port, options) {
// "redirect" requests to pages ending with "/" or no extension so that, // "redirect" requests to pages ending with "/" or no extension so that,
// for example, request to "blog" returns "blog/index.html" or "blog.html" // for example, request to "blog" returns "blog/index.html" or "blog.html"
app.get(noExtRouting(), (req, res, next) => { app.get(routing.noExtension(), (req, res, next) => {
const slash = req.path.toString().endsWith('/') ? '' : '/'; const slash = req.path.toString().endsWith('/') ? '' : '/';
const requestUrl = `http://localhost:${port}${req.path}`; const requestUrl = `http://localhost:${port}${req.path}`;
requestFile(`${requestUrl + slash}index.html`, res, () => { requestFile(`${requestUrl + slash}index.html`, res, () => {
@ -612,7 +556,7 @@ function execute(port, options) {
// handle special cleanUrl case like '/blog/1.2.3' & '/blog.robots.hai' // handle special cleanUrl case like '/blog/1.2.3' & '/blog.robots.hai'
// where we should try to serve 'blog/1.2.3.html' & '/blog.robots.hai.html' // where we should try to serve 'blog/1.2.3.html' & '/blog.robots.hai.html'
app.get(dotRouting(), (req, res, next) => { app.get(routing.dotfiles(), (req, res, next) => {
if (!siteConfig.cleanUrl) { if (!siteConfig.cleanUrl) {
next(); next();
return; return;

View file

@ -9,23 +9,14 @@ const cssnano = require('cssnano');
const path = require('path'); const path = require('path');
const escapeStringRegexp = require('escape-string-regexp'); const escapeStringRegexp = require('escape-string-regexp');
// Return the subdirectory path from a reference directory
// Example:
// (file: 'docs/projectA/test.md', refDir: 'docs')
// returns 'projectA'
function getSubDir(file, refDir) { function getSubDir(file, refDir) {
let subDir = path.dirname(path.relative(refDir, file)); const subDir = path.dirname(path.relative(refDir, file)).replace('\\', '/');
subDir = subDir.replace('\\', '/'); return subDir !== '.' && !subDir.includes('..') ? subDir : null;
return subDir !== '.' ? subDir : null;
} }
// Get the corresponding enabled language locale of a file.
// Example:
// (file: '/website/translated_docs/ko/projectA/test.md', refDir: 'website/translated_docs')
// returns 'ko'
function getLanguage(file, refDir) { function getLanguage(file, refDir) {
const regexSubFolder = new RegExp( const regexSubFolder = new RegExp(
`/${escapeStringRegexp(path.basename(refDir))}/(.*)/.*/` `${escapeStringRegexp(path.basename(refDir))}/(.*?)/.*`
); );
const match = regexSubFolder.exec(file); const match = regexSubFolder.exec(file);
@ -42,6 +33,18 @@ function getLanguage(file, refDir) {
return null; return null;
} }
function isSeparateCss(file, separateDirs) {
if (!separateDirs) {
return false;
}
for (let i = 0; i < separateDirs.length; i++) {
if (file.includes(separateDirs[i])) {
return true;
}
}
return false;
}
function minifyCss(cssContent) { function minifyCss(cssContent) {
return cssnano return cssnano
.process(cssContent, { .process(cssContent, {
@ -54,5 +57,6 @@ function minifyCss(cssContent) {
module.exports = { module.exports = {
getSubDir, getSubDir,
getLanguage, getLanguage,
isSeparateCss,
minifyCss, minifyCss,
}; };