docusaurus/lib/server/readMetadata.js
2018-04-25 10:40:53 -07:00

419 lines
12 KiB
JavaScript

/**
* 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 CWD = process.cwd();
const path = require('path');
const fs = require('fs');
const glob = require('glob');
const chalk = require('chalk');
const env = require('./env.js');
const siteConfig = require(CWD + '/siteConfig.js');
const versionFallback = require('./versionFallback.js');
const escapeStringRegexp = require('escape-string-regexp');
const SupportedHeaderFields = new Set([
'id',
'title',
'author',
'authorURL',
'authorFBID',
'sidebar_label',
'original_id',
'hide_title',
'layout',
'custom_edit_url',
]);
// Can have a custom docs path. Top level folder still needs to be in directory
// at the same level as `website`, not inside `website`.
// e.g., docs/whereDocsReallyExist
// website-docs/
// All .md docs still (currently) must be in one flat directory hierarchy.
// e.g., docs/whereDocsReallyExist/*.md (all .md files in this dir)
function getDocsPath() {
return siteConfig.customDocsPath ? siteConfig.customDocsPath : 'docs';
}
// returns map from id to object containing sidebar ordering info
function readSidebar() {
let allSidebars;
if (fs.existsSync(CWD + '/sidebars.json')) {
allSidebars = require(CWD + '/sidebars.json');
} else {
allSidebars = {};
}
Object.assign(allSidebars, versionFallback.sidebarData());
const order = {};
Object.keys(allSidebars).forEach(sidebar => {
const categories = allSidebars[sidebar];
let ids = [];
let categoryOrder = [];
Object.keys(categories).forEach(category => {
ids = ids.concat(categories[category]);
for (let i = 0; i < categories[category].length; i++) {
categoryOrder.push(category);
}
});
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
let previous, next;
if (i > 0) previous = ids[i - 1];
if (i < ids.length - 1) next = ids[i + 1];
order[id] = {
previous: previous,
next: next,
sidebar: sidebar,
category: categoryOrder[i],
};
}
});
return order;
}
// split markdown header
function splitHeader(content) {
// New line characters need to handle all operating systems.
const lines = content.split(/\r?\n/);
if (lines[0] !== '---') {
return {};
}
let i = 1;
for (; i < lines.length - 1; ++i) {
if (lines[i] === '---') {
break;
}
}
return {
header: lines.slice(1, i + 1).join('\n'),
content: lines.slice(i + 1).join('\n'),
};
}
// Extract markdown metadata header
function extractMetadata(content) {
const metadata = {};
const both = splitHeader(content);
if (Object.keys(both).length === 0) {
return {metadata, rawContent: content};
}
const lines = both.header.split('\n');
for (let i = 0; i < lines.length - 1; ++i) {
const keyvalue = lines[i].split(':');
const key = keyvalue[0].trim();
let value = keyvalue
.slice(1)
.join(':')
.trim();
try {
value = JSON.parse(value);
} catch (e) {}
metadata[key] = value;
}
return {metadata, rawContent: both.content};
}
// process the metadata for a document found in the docs folder
function processMetadata(file) {
const result = extractMetadata(fs.readFileSync(file, 'utf8'));
let regexSubFolder = new RegExp(
'/' + escapeStringRegexp(getDocsPath()) + '/(.*)/.*/'
);
const match = regexSubFolder.exec(file);
let language = match ? match[1] : 'en';
const metadata = {};
for (const fieldName of Object.keys(result.metadata)) {
if (SupportedHeaderFields.has(fieldName)) {
metadata[fieldName] = result.metadata[fieldName];
} else {
console.warn(`Header field "${fieldName}" in ${file} is not supported.`);
}
}
const rawContent = result.rawContent;
metadata.source = path.basename(file);
if (!metadata.id) {
metadata.id = path.basename(file, path.extname(file));
}
if (metadata.id.includes('/')) {
throw new Error('Document id cannot include "/".');
}
if (!metadata.title) {
metadata.title = metadata.id;
}
const langPart =
env.translation.enabled || siteConfig.useEnglishUrl ? language + '/' : '';
let versionPart = '';
if (env.versioning.enabled) {
metadata.version = 'next';
versionPart = 'next/';
}
metadata.permalink = 'docs/' + langPart + versionPart + metadata.id + '.html';
// change ids previous, next
metadata.localized_id = metadata.id;
metadata.id = (env.translation.enabled ? language + '-' : '') + metadata.id;
metadata.language = env.translation.enabled ? language : 'en';
const order = readSidebar();
const id = metadata.localized_id;
if (order[id]) {
metadata.sidebar = order[id].sidebar;
metadata.category = order[id].category;
if (order[id].next) {
metadata.next_id = order[id].next;
metadata.next =
(env.translation.enabled ? language + '-' : '') + order[id].next;
}
if (order[id].previous) {
metadata.previous_id = order[id].previous;
metadata.previous =
(env.translation.enabled ? language + '-' : '') + order[id].previous;
}
}
return {metadata, rawContent: rawContent};
}
// process metadata for all docs and save into core/metadata.js
function generateMetadataDocs() {
let order;
try {
order = readSidebar();
} catch (e) {
console.error(e);
process.exit(1);
}
const enabledLanguages = env.translation
.enabledLanguages()
.map(language => language.tag);
const metadatas = {};
const defaultMetadatas = {};
// metadata for english files
let files = glob.sync(CWD + '/../' + getDocsPath() + '/**');
files.forEach(file => {
let language = 'en';
const extension = path.extname(file);
if (extension === '.md' || extension === '.markdown') {
const res = processMetadata(file);
if (!res) {
return;
}
let metadata = res.metadata;
metadatas[metadata.id] = metadata;
// create a default list of documents for each enabled language based on docs in English
// these will get replaced if/when the localized file is downloaded from crowdin
enabledLanguages
.filter(currentLanguage => {
return currentLanguage != 'en';
})
.map(currentLanguage => {
let baseMetadata = Object.assign({}, metadata);
baseMetadata['id'] = baseMetadata['id']
.toString()
.replace(/^en-/, currentLanguage + '-');
if (baseMetadata['permalink'])
baseMetadata['permalink'] = baseMetadata['permalink']
.toString()
.replace(/^docs\/en\//, 'docs/' + currentLanguage + '/');
if (baseMetadata['next'])
baseMetadata['next'] = baseMetadata['next']
.toString()
.replace(/^en-/, currentLanguage + '-');
if (baseMetadata['previous'])
baseMetadata['previous'] = baseMetadata['previous']
.toString()
.replace(/^en-/, currentLanguage + '-');
baseMetadata['language'] = currentLanguage;
defaultMetadatas[baseMetadata['id']] = baseMetadata;
});
Object.assign(metadatas, defaultMetadatas);
}
});
// metadata for non-english docs
const regexSubFolder = /translated_docs\/(.*?)\/.*/;
files = glob.sync(CWD + '/translated_docs/**');
files.forEach(file => {
let language = 'en';
const match = regexSubFolder.exec(file);
if (match) {
language = match[1];
}
if (enabledLanguages.indexOf(language) === -1) {
return;
}
const extension = path.extname(file);
if (extension === '.md' || extension === '.markdown') {
const res = processMetadata(file);
if (!res) {
return;
}
let metadata = res.metadata;
metadatas[metadata.id] = metadata;
}
});
// metadata for versioned docs
const versionData = versionFallback.docData();
versionData.forEach(metadata => {
const id = metadata.localized_id;
if (order[id]) {
metadata.sidebar = order[id].sidebar;
metadata.category = order[id].category;
if (order[id].next) {
metadata.next_id = order[id].next.replace(
'version-' + metadata.version + '-',
''
);
metadata.next =
(env.translation.enabled ? metadata.language + '-' : '') +
order[id].next;
}
if (order[id].previous) {
metadata.previous_id = order[id].previous.replace(
'version-' + metadata.version + '-',
''
);
metadata.previous =
(env.translation.enabled ? metadata.language + '-' : '') +
order[id].previous;
}
}
metadatas[metadata.id] = metadata;
});
// Get the titles of the previous and next ids so that we can use them in
// navigation buttons in DocsLayout.js
Object.keys(metadatas).forEach(function(metadata) {
if (metadatas[metadata].previous) {
if (metadatas[metadatas[metadata].previous]) {
metadatas[metadata].previous_title =
metadatas[metadatas[metadata].previous].title;
} else {
metadatas[metadata].previous_title = 'Previous';
}
}
if (metadatas[metadata].next) {
if (metadatas[metadatas[metadata].next]) {
metadatas[metadata].next_title =
metadatas[metadatas[metadata].next].title;
} else {
metadatas[metadata].next_title = 'Next';
}
}
});
fs.writeFileSync(
__dirname + '/../core/metadata.js',
'/**\n' +
' * @' +
'generated\n' + // separate this out for Nuclide treating @generated as readonly
' */\n' +
'module.exports = ' +
JSON.stringify(metadatas, null, 2) +
';\n'
);
}
// process metadata for blog posts and save into core/MetadataBlog.js
function generateMetadataBlog() {
const metadatas = [];
let files = glob.sync(CWD + '/blog/**/*.*');
files
.sort()
.reverse()
.forEach(file => {
const extension = path.extname(file);
if (extension !== '.md' && extension !== '.markdown') {
return;
}
// Transform
// 2015-08-13-blog-post-name-0.5.md
// into
// 2015/08/13/blog-post-name-0-5.html
const filePath = path
.basename(file)
.replace('-', '/')
.replace('-', '/')
.replace('-', '/')
.replace(/\.md$/, '.html');
const result = extractMetadata(fs.readFileSync(file, {encoding: 'utf8'}));
const rawContent = result.rawContent;
const metadata = Object.assign(
{path: filePath, content: rawContent},
result.metadata
);
metadata.id = metadata.title;
// Extract, YYYY, MM, DD from the file name
let filePathDateArr = path
.basename(file)
.toString()
.split('-');
metadata.date = new Date(
filePathDateArr[0] +
'-' +
filePathDateArr[1] +
'-' +
filePathDateArr[2] +
'T06:00:00.000Z'
);
// allow easier sorting of blog by providing seconds since epoch
metadata.seconds = Math.round(metadata.date.getTime() / 1000);
metadatas.push(metadata);
});
const sortedMetadatas = metadatas.sort(
(a, b) => parseInt(b.seconds) - parseInt(a.seconds)
);
fs.writeFileSync(
__dirname + '/../core/MetadataBlog.js',
'/**\n' +
' * @' +
'generated\n' + // separate this out for Nuclide treating @generated as readonly
' */\n' +
'module.exports = ' +
JSON.stringify(sortedMetadatas, null, 2) +
';\n'
);
}
module.exports = {
getDocsPath,
readSidebar,
extractMetadata,
processMetadata,
generateMetadataDocs,
generateMetadataBlog,
};