docusaurus/v1/lib/server/readMetadata.js
Dom Corvasce 61078e38a9 feat: Allow modifying docs url prefix (#914)
* Allow other routes than /docs in the URL

siteConfig.js has a new mandatory field named *docsRoute* which default
value is 'docs' and that can be customized by the user.

This change will allow users who uses the library to host guides and
tutorials to customize their websites by assign 'docsRoute' values
like 'tutorials' or 'guides'.

Fixes #879

* Make "docsRoute" field optional

* Isolate docsRoute login in getDocsRoute function

* Rename docsRoute to docsUrl

* Run prettier

* Remove old folders

* fix: Restore docusaurus reference link

* fix: Add `docsUrl` param fallback. Refactor multiple function calls

* Fix linting errors

* Update description for docsUrl field

* Reduce redundant calls to getDocsUrl

* Replace a missed use case for `docsUrl` instead of the function call

* Move `getDocsUrl` out from `server/routing.js` to `server/utils.js`

**Why?**
Because `routing.js` is exporting all router RegEx's, and the
`getDocsUrl` suffices more as a util

* WiP: Align leading slashes and fix routing around `docsUrl`

Checklist:
- [x] Added `removeDuplicateLeadingSlashes` util to make sure there is only
one leading slash
- [-] Fix edge cases for routing:
  - [x] `docsUrl: ''`
  - [ ] `docsUrl: '/'`
  - [ ] make it work with languages
  - [ ] make it work with versioning

* Make leading slashes canonical cross routing and generated links

This ensures correct routing for customized `baseUrl` and `docsUrl`.

- Changed all routing functions to take `siteConfig` instead of
`siteConfig.baseUrl`
- Updated tests accordingly

* Alternative fallback for `docsUrl`

* rework/ fix implementation

* cleanup

* refactor and add docs for config props

* fix typo

* fix broken url
2018-11-28 15:34:16 +08:00

405 lines
11 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 metadataUtils = require('./metadataUtils');
const env = require('./env.js');
const blog = require('./blog.js');
const loadConfig = require('./config');
const siteConfig = loadConfig(`${CWD}/siteConfig.js`);
const versionFallback = require('./versionFallback.js');
const utils = require('./utils.js');
const docsPart = `${siteConfig.docsUrl ? `${siteConfig.docsUrl}/` : ''}`;
const SupportedHeaderFields = new Set([
'id',
'title',
'author',
'authorURL',
'authorFBID',
'sidebar_label',
'original_id',
'hide_title',
'layout',
'custom_edit_url',
]);
let allSidebars;
if (fs.existsSync(`${CWD}/sidebars.json`)) {
allSidebars = require(`${CWD}/sidebars.json`);
} else {
allSidebars = {};
}
// 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(sidebars = {}) {
Object.assign(sidebars, versionFallback.sidebarData());
const items = {};
Object.keys(sidebars).forEach(sidebar => {
const categories = sidebars[sidebar];
const sidebarItems = [];
Object.keys(categories).forEach(category => {
const categoryItems = categories[category];
categoryItems.forEach(categoryItem => {
if (typeof categoryItem === 'object') {
switch (categoryItem.type) {
case 'subcategory':
categoryItem.ids.forEach(subcategoryItem => {
sidebarItems.push({
id: subcategoryItem,
category,
subcategory: categoryItem.label,
order: sidebarItems.length + 1,
});
});
return;
default:
return;
}
}
// Is a regular id value.
sidebarItems.push({
id: categoryItem,
category,
subcategory: null,
order: sidebarItems.length + 1,
});
});
});
for (let i = 0; i < sidebarItems.length; i++) {
const item = sidebarItems[i];
let previous = null;
let next = null;
if (i > 0) {
previous = sidebarItems[i - 1].id;
}
if (i < sidebarItems.length - 1) {
next = sidebarItems[i + 1].id;
}
items[item.id] = {
previous,
next,
sidebar,
category: item.category,
subcategory: item.subcategory,
order: item.order,
};
}
});
return items;
}
// process the metadata for a document found in either 'docs' or 'translated_docs'
function processMetadata(file, refDir) {
const result = metadataUtils.extractMetadata(fs.readFileSync(file, 'utf8'));
const language = utils.getLanguage(file, refDir) || 'en';
const metadata = {};
Object.keys(result.metadata).forEach(fieldName => {
if (SupportedHeaderFields.has(fieldName)) {
metadata[fieldName] = result.metadata[fieldName];
} else {
console.warn(`Header field "${fieldName}" in ${file} is not supported.`);
}
});
const rawContent = result.rawContent;
if (!metadata.id) {
metadata.id = path.basename(file, path.extname(file));
}
if (metadata.id.includes('/')) {
throw new Error('Document id cannot include "/".');
}
// If a file is located in a subdirectory, prepend the subdir to it's ID
// Example:
// (file: 'docusaurus/docs/projectA/test.md', ID 'test', refDir: 'docusaurus/docs')
// returns 'projectA/test'
const subDir = utils.getSubDir(file, refDir);
if (subDir) {
metadata.id = `${subDir}/${metadata.id}`;
}
// Example: `docs/projectA/test.md` source is `projectA/test.md`
metadata.source = subDir
? `${subDir}/${path.basename(file)}`
: path.basename(file);
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 = `${docsPart}${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 items = readSidebar(allSidebars);
const id = metadata.localized_id;
const item = items[id];
if (item) {
metadata.sidebar = item.sidebar;
metadata.category = item.category;
metadata.subcategory = item.subcategory;
metadata.order = item.order;
if (item.next) {
metadata.next_id = item.next;
metadata.next =
(env.translation.enabled ? `${language}-` : '') + item.next;
}
if (item.previous) {
metadata.previous_id = item.previous;
metadata.previous =
(env.translation.enabled ? `${language}-` : '') + item.previous;
}
}
return {metadata, rawContent};
}
// process metadata for all docs and save into core/metadata.js
function generateMetadataDocs() {
let order;
try {
order = readSidebar(allSidebars);
} 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
const docsDir = path.join(CWD, '../', getDocsPath());
let files = glob.sync(`${docsDir}/**`);
files.forEach(file => {
const extension = path.extname(file);
if (extension === '.md' || extension === '.markdown') {
const res = processMetadata(file, docsDir);
if (!res) {
return;
}
const 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 => currentLanguage !== 'en')
.forEach(currentLanguage => {
const baseMetadata = Object.assign({}, metadata);
baseMetadata.id = baseMetadata.id
.toString()
.replace(/^en-/, `${currentLanguage}-`);
if (baseMetadata.permalink) {
baseMetadata.permalink = baseMetadata.permalink
.toString()
.replace(
new RegExp(`^${docsPart}en/`),
`${docsPart}${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 translatedDir = path.join(CWD, 'translated_docs');
files = glob.sync(`${CWD}/translated_docs/**`);
files.forEach(file => {
if (!utils.getLanguage(file, translatedDir)) {
return;
}
const extension = path.extname(file);
if (extension === '.md' || extension === '.markdown') {
const res = processMetadata(file, translatedDir);
if (!res) {
return;
}
const 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;
metadata.subcategory = order[id].subcategory;
metadata.order = order[id].order;
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(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(
path.join(__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 = [];
const files = glob.sync(`${CWD}/blog/**/*.*`);
files
.sort()
.reverse()
.forEach(file => {
const extension = path.extname(file);
if (extension !== '.md' && extension !== '.markdown') {
return;
}
const metadata = blog.getMetadata(file);
// Extract, YYYY, MM, DD from the file name
const 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, 10) - parseInt(a.seconds, 10),
);
fs.writeFileSync(
path.join(__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,
processMetadata,
generateMetadataDocs,
generateMetadataBlog,
};