2020-08-17 11:50:22 -04:00
/ * *
* Copyright ( c ) Facebook , Inc . and its affiliates .
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree .
* /
import path from 'path' ;
import fs from 'fs-extra' ;
2021-10-21 06:27:57 -04:00
import chalk from 'chalk' ;
import { keyBy } from 'lodash' ;
2020-08-17 11:50:22 -04:00
import {
aliasedSitePath ,
getEditUrl ,
2020-11-26 06:16:46 -05:00
getFolderContainingFile ,
2020-11-30 10:42:58 -05:00
normalizeUrl ,
parseMarkdownString ,
2021-01-29 09:35:25 -05:00
posixPath ,
2021-07-15 07:21:41 -04:00
Globby ,
2021-08-19 04:31:15 -04:00
normalizeFrontMatterTags ,
2020-08-17 11:50:22 -04:00
} from '@docusaurus/utils' ;
2021-11-17 19:25:07 -05:00
import type { LoadContext } from '@docusaurus/types' ;
2020-08-17 11:50:22 -04:00
import { getFileLastUpdate } from './lastUpdate' ;
import {
2020-11-30 10:42:58 -05:00
DocFile ,
2020-08-17 11:50:22 -04:00
DocMetadataBase ,
2021-10-21 06:27:57 -04:00
DocMetadata ,
DocNavLink ,
2020-08-17 11:50:22 -04:00
LastUpdateData ,
MetadataOptions ,
PluginOptions ,
2020-11-30 10:42:58 -05:00
VersionMetadata ,
2021-10-21 06:27:57 -04:00
LoadedVersion ,
2020-08-17 11:50:22 -04:00
} from './types' ;
import getSlug from './slug' ;
import { CURRENT_VERSION_NAME } from './constants' ;
2020-11-26 06:16:46 -05:00
import { getDocsDirPaths } from './versions' ;
2021-04-21 06:06:06 -04:00
import { stripPathNumberPrefixes } from './numberPrefix' ;
import { validateDocFrontMatter } from './docFrontMatter' ;
2021-10-21 06:27:57 -04:00
import type { Sidebars } from './sidebars/types' ;
import { createSidebarsUtils } from './sidebars/utils' ;
2020-08-17 11:50:22 -04:00
type LastUpdateOptions = Pick <
PluginOptions ,
'showLastUpdateAuthor' | 'showLastUpdateTime'
> ;
async function readLastUpdateData (
filePath : string ,
options : LastUpdateOptions ,
) : Promise < LastUpdateData > {
const { showLastUpdateAuthor , showLastUpdateTime } = options ;
if ( showLastUpdateAuthor || showLastUpdateTime ) {
// Use fake data in dev for faster development.
const fileLastUpdateData =
process . env . NODE_ENV === 'production'
? await getFileLastUpdate ( filePath )
: {
author : 'Author' ,
timestamp : 1539502055 ,
} ;
if ( fileLastUpdateData ) {
const { author , timestamp } = fileLastUpdateData ;
return {
lastUpdatedAt : showLastUpdateTime ? timestamp : undefined ,
lastUpdatedBy : showLastUpdateAuthor ? author : undefined ,
} ;
}
}
return { } ;
}
export async function readDocFile (
2020-11-26 06:16:46 -05:00
versionMetadata : Pick <
VersionMetadata ,
2021-03-12 09:11:08 -05:00
'contentPath' | 'contentPathLocalized'
2020-11-26 06:16:46 -05:00
> ,
2020-08-17 11:50:22 -04:00
source : string ,
options : LastUpdateOptions ,
) : Promise < DocFile > {
2021-03-12 09:11:08 -05:00
const contentPath = await getFolderContainingFile (
2020-11-26 06:16:46 -05:00
getDocsDirPaths ( versionMetadata ) ,
source ,
) ;
2021-03-12 09:11:08 -05:00
const filePath = path . join ( contentPath , source ) ;
2020-11-26 06:16:46 -05:00
2020-08-17 11:50:22 -04:00
const [ content , lastUpdate ] = await Promise . all ( [
fs . readFile ( filePath , 'utf-8' ) ,
readLastUpdateData ( filePath , options ) ,
] ) ;
2021-03-12 09:11:08 -05:00
return { source , content , lastUpdate , contentPath , filePath } ;
2020-08-17 11:50:22 -04:00
}
export async function readVersionDocs (
versionMetadata : VersionMetadata ,
options : Pick <
PluginOptions ,
2021-07-15 07:21:41 -04:00
'include' | 'exclude' | 'showLastUpdateAuthor' | 'showLastUpdateTime'
2020-08-17 11:50:22 -04:00
> ,
) : Promise < DocFile [ ] > {
2021-07-15 07:21:41 -04:00
const sources = await Globby ( options . include , {
2021-03-12 09:11:08 -05:00
cwd : versionMetadata.contentPath ,
2021-07-15 07:21:41 -04:00
ignore : options.exclude ,
2020-08-17 11:50:22 -04:00
} ) ;
return Promise . all (
2020-11-26 06:16:46 -05:00
sources . map ( ( source ) = > readDocFile ( versionMetadata , source , options ) ) ,
2020-08-17 11:50:22 -04:00
) ;
}
2021-06-16 14:16:55 -04:00
function doProcessDocMetadata ( {
2020-08-17 11:50:22 -04:00
docFile ,
versionMetadata ,
context ,
options ,
} : {
docFile : DocFile ;
versionMetadata : VersionMetadata ;
context : LoadContext ;
options : MetadataOptions ;
} ) : DocMetadataBase {
2021-03-12 09:11:08 -05:00
const { source , content , lastUpdate , contentPath , filePath } = docFile ;
2020-12-28 04:25:47 -05:00
const { homePageId } = options ;
2021-03-05 09:30:09 -05:00
const { siteDir , i18n } = context ;
2020-08-17 11:50:22 -04:00
2021-04-21 06:06:06 -04:00
const {
frontMatter : unsafeFrontMatter ,
contentTitle ,
excerpt ,
2021-04-27 09:44:46 -04:00
} = parseMarkdownString ( content ) ;
2021-04-21 06:06:06 -04:00
const frontMatter = validateDocFrontMatter ( unsafeFrontMatter ) ;
2021-04-09 11:09:33 -04:00
2021-03-17 12:28:42 -04:00
const {
custom_edit_url : customEditURL ,
2021-04-15 10:20:11 -04:00
// Strip number prefixes by default (01-MyFolder/01-MyDoc.md => MyFolder/MyDoc) by default,
2021-08-11 10:07:17 -04:00
// but allow to disable this behavior with frontmatter
parse_number_prefixes : parseNumberPrefixes = true ,
2021-03-17 12:28:42 -04:00
} = frontMatter ;
2020-08-17 11:50:22 -04:00
2021-04-15 10:20:11 -04:00
// ex: api/plugins/myDoc -> myDoc
// ex: myDoc -> myDoc
const sourceFileNameWithoutExtension = path . basename (
source ,
path . extname ( source ) ,
) ;
// ex: api/plugins/myDoc -> api/plugins
// ex: myDoc -> .
const sourceDirName = path . dirname ( source ) ;
2021-08-11 10:07:17 -04:00
const { filename : unprefixedFileName , numberPrefix } = parseNumberPrefixes
2021-04-21 06:06:06 -04:00
? options . numberPrefixParser ( sourceFileNameWithoutExtension )
2021-04-15 10:20:11 -04:00
: { filename : sourceFileNameWithoutExtension , numberPrefix : undefined } ;
const baseID : string = frontMatter . id ? ? unprefixedFileName ;
2020-08-17 11:50:22 -04:00
if ( baseID . includes ( '/' ) ) {
2021-06-16 05:37:28 -04:00
throw new Error ( ` Document id " ${ baseID } " cannot include slash. ` ) ;
2020-08-17 11:50:22 -04:00
}
2021-04-15 10:20:11 -04:00
// For autogenerated sidebars, sidebar position can come from filename number prefix or frontmatter
const sidebarPosition : number | undefined =
frontMatter . sidebar_position ? ? numberPrefix ;
2020-08-17 11:50:22 -04:00
// TODO legacy retrocompatibility
// The same doc in 2 distinct version could keep the same id,
// we just need to namespace the data by version
2021-04-15 10:20:11 -04:00
const versionIdPrefix =
2020-08-17 11:50:22 -04:00
versionMetadata . versionName === CURRENT_VERSION_NAME
2021-04-15 10:20:11 -04:00
? undefined
: ` version- ${ versionMetadata . versionName } ` ;
2020-08-17 11:50:22 -04:00
// TODO legacy retrocompatibility
2021-04-15 10:20:11 -04:00
// I think it's bad to affect the frontmatter id with the dirname?
function computeDirNameIdPrefix() {
if ( sourceDirName === '.' ) {
return undefined ;
}
// Eventually remove the number prefixes from intermediate directories
2021-08-11 10:07:17 -04:00
return parseNumberPrefixes
2021-04-21 06:06:06 -04:00
? stripPathNumberPrefixes ( sourceDirName , options . numberPrefixParser )
2021-04-15 10:20:11 -04:00
: sourceDirName ;
}
2020-08-17 11:50:22 -04:00
2021-04-15 10:20:11 -04:00
const unversionedId = [ computeDirNameIdPrefix ( ) , baseID ]
. filter ( Boolean )
. join ( '/' ) ;
2020-08-17 11:50:22 -04:00
2021-04-15 10:20:11 -04:00
// TODO is versioning the id very useful in practice?
// legacy versioned id, requires a breaking change to modify this
const id = [ versionIdPrefix , unversionedId ] . filter ( Boolean ) . join ( '/' ) ;
2020-08-17 11:50:22 -04:00
// TODO remove soon, deprecated homePageId
const isDocsHomePage = unversionedId === ( homePageId ? ? '_index' ) ;
if ( frontMatter . slug && isDocsHomePage ) {
throw new Error (
2020-09-11 14:33:08 -04:00
` The docs homepage (homePageId= ${ homePageId } ) is not allowed to have a frontmatter slug= ${ frontMatter . slug } => you have to choose either homePageId or slug, not both ` ,
2020-08-17 11:50:22 -04:00
) ;
}
const docSlug = isDocsHomePage
? '/'
: getSlug ( {
baseID ,
2021-04-15 10:20:11 -04:00
dirName : sourceDirName ,
2020-08-17 11:50:22 -04:00
frontmatterSlug : frontMatter.slug ,
2021-08-11 10:07:17 -04:00
stripDirNumberPrefixes : parseNumberPrefixes ,
2021-04-21 06:06:06 -04:00
numberPrefixParser : options.numberPrefixParser ,
2020-08-17 11:50:22 -04:00
} ) ;
2021-06-03 11:45:19 -04:00
// Note: the title is used by default for page title, sidebar label, pagination buttons...
// frontMatter.title should be used in priority over contentTitle (because it can contain markdown/JSX syntax)
const title : string = frontMatter . title ? ? contentTitle ? ? baseID ;
2020-08-17 11:50:22 -04:00
2021-04-09 11:09:33 -04:00
const description : string = frontMatter . description ? ? excerpt ? ? '' ;
2020-08-17 11:50:22 -04:00
const permalink = normalizeUrl ( [ versionMetadata . versionPath , docSlug ] ) ;
2021-02-17 05:48:33 -05:00
function getDocEditUrl() {
2021-03-12 09:11:08 -05:00
const relativeFilePath = path . relative ( contentPath , filePath ) ;
2021-02-17 05:48:33 -05:00
if ( typeof options . editUrl === 'function' ) {
return options . editUrl ( {
version : versionMetadata.versionName ,
versionDocsDirPath : posixPath (
2021-03-12 09:11:08 -05:00
path . relative ( siteDir , versionMetadata . contentPath ) ,
2021-02-17 05:48:33 -05:00
) ,
docPath : posixPath ( relativeFilePath ) ,
permalink ,
locale : context.i18n.currentLocale ,
} ) ;
} else if ( typeof options . editUrl === 'string' ) {
2021-03-12 09:11:08 -05:00
const isLocalized = contentPath === versionMetadata . contentPathLocalized ;
2021-02-17 05:48:33 -05:00
const baseVersionEditUrl =
isLocalized && options . editLocalizedFiles
? versionMetadata . versionEditUrlLocalized
: versionMetadata . versionEditUrl ;
return getEditUrl ( relativeFilePath , baseVersionEditUrl ) ;
} else {
return undefined ;
}
}
2020-08-17 11:50:22 -04:00
// Assign all of object properties during instantiation (if possible) for
// NodeJS optimization.
// Adding properties to object after instantiation will cause hidden
// class transitions.
2020-11-30 10:42:58 -05:00
return {
2020-08-17 11:50:22 -04:00
unversionedId ,
id ,
isDocsHomePage ,
2021-06-03 11:45:19 -04:00
title ,
2020-08-17 11:50:22 -04:00
description ,
source : aliasedSitePath ( filePath , siteDir ) ,
2021-04-15 10:20:11 -04:00
sourceDirName ,
2020-08-17 11:50:22 -04:00
slug : docSlug ,
permalink ,
2021-03-17 12:28:42 -04:00
editUrl : customEditURL !== undefined ? customEditURL : getDocEditUrl ( ) ,
2021-08-19 04:31:15 -04:00
tags : normalizeFrontMatterTags ( versionMetadata . tagsPath , frontMatter . tags ) ,
2020-08-17 11:50:22 -04:00
version : versionMetadata.versionName ,
lastUpdatedBy : lastUpdate.lastUpdatedBy ,
lastUpdatedAt : lastUpdate.lastUpdatedAt ,
2021-03-05 09:30:09 -05:00
formattedLastUpdatedAt : lastUpdate.lastUpdatedAt
2021-03-15 13:02:53 -04:00
? new Intl . DateTimeFormat ( i18n . currentLocale ) . format (
2021-03-05 09:30:09 -05:00
lastUpdate . lastUpdatedAt * 1000 ,
)
: undefined ,
2021-04-15 10:20:11 -04:00
sidebarPosition ,
2021-03-26 14:54:29 -04:00
frontMatter ,
2020-08-17 11:50:22 -04:00
} ;
}
2021-06-16 14:16:55 -04:00
export function processDocMetadata ( args : {
docFile : DocFile ;
versionMetadata : VersionMetadata ;
context : LoadContext ;
options : MetadataOptions ;
} ) : DocMetadataBase {
try {
return doProcessDocMetadata ( args ) ;
} catch ( e ) {
console . error (
chalk . red (
2021-11-09 13:46:10 -05:00
` Can't process doc metadata for doc at path " ${ args . docFile . filePath } " in version " ${ args . versionMetadata . versionName } " ` ,
2021-06-16 14:16:55 -04:00
) ,
) ;
throw e ;
}
}
2021-10-21 06:27:57 -04:00
export function handleNavigation (
docsBase : DocMetadataBase [ ] ,
sidebars : Sidebars ,
sidebarFilePath : string ,
) : Pick < LoadedVersion , ' mainDocId ' | ' docs ' > {
const docsBaseById = keyBy ( docsBase , ( doc ) = > doc . id ) ;
const { checkSidebarsDocIds , getDocNavigation , getFirstDocIdOfFirstSidebar } =
createSidebarsUtils ( sidebars ) ;
const validDocIds = Object . keys ( docsBaseById ) ;
checkSidebarsDocIds ( validDocIds , sidebarFilePath ) ;
// Add sidebar/next/previous to the docs
function addNavData ( doc : DocMetadataBase ) : DocMetadata {
const { sidebarName , previousId , nextId } = getDocNavigation ( doc . id ) ;
const toDocNavLink = (
docId : string | null | undefined ,
type : 'prev' | 'next' ,
) : DocNavLink | undefined = > {
if ( ! docId ) {
return undefined ;
}
if ( ! docsBaseById [ docId ] ) {
// This could only happen if user provided the ID through front matter
throw new Error (
` Error when loading ${ doc . id } in ${ doc . sourceDirName } : the pagination_ ${ type } front matter points to a non-existent ID ${ docId } . ` ,
) ;
}
const {
title ,
permalink ,
frontMatter : {
pagination_label : paginationLabel ,
sidebar_label : sidebarLabel ,
} ,
} = docsBaseById [ docId ] ;
return { title : paginationLabel ? ? sidebarLabel ? ? title , permalink } ;
} ;
const {
frontMatter : {
pagination_next : paginationNext = nextId ,
pagination_prev : paginationPrev = previousId ,
} ,
} = doc ;
const previous = toDocNavLink ( paginationPrev , 'prev' ) ;
const next = toDocNavLink ( paginationNext , 'next' ) ;
return { . . . doc , sidebar : sidebarName , previous , next } ;
}
const docs = docsBase . map ( addNavData ) ;
// sort to ensure consistent output for tests
docs . sort ( ( a , b ) = > a . id . localeCompare ( b . id ) ) ;
/ * *
* The "main doc" is the "version entry point"
* We browse this doc by clicking on a version :
* - the "home" doc ( at '/docs/' )
* - the first doc of the first sidebar
* - a random doc ( if no docs are in any sidebar . . . edge case )
* /
function getMainDoc ( ) : DocMetadata {
const versionHomeDoc = docs . find ( ( doc ) = > doc . slug === '/' ) ;
const firstDocIdOfFirstSidebar = getFirstDocIdOfFirstSidebar ( ) ;
if ( versionHomeDoc ) {
return versionHomeDoc ;
} else if ( firstDocIdOfFirstSidebar ) {
return docs . find ( ( doc ) = > doc . id === firstDocIdOfFirstSidebar ) ! ;
} else {
return docs [ 0 ] ;
}
}
return { mainDocId : getMainDoc ( ) . unversionedId , docs } ;
}