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' ;
2020-08-28 12:37:49 -04:00
import {
PluginOptions ,
2021-06-24 12:04:16 -04:00
VersionBanner ,
2020-08-28 12:37:49 -04:00
VersionMetadata ,
VersionOptions ,
VersionsOptions ,
} from './types' ;
2020-08-17 11:50:22 -04:00
import {
VERSIONS_JSON_FILE ,
VERSIONED_DOCS_DIR ,
VERSIONED_SIDEBARS_DIR ,
CURRENT_VERSION_NAME ,
} from './constants' ;
import { DEFAULT_PLUGIN_ID } from '@docusaurus/core/lib/constants' ;
import { LoadContext } from '@docusaurus/types' ;
2020-12-28 04:25:47 -05:00
import { getPluginI18nPath , normalizeUrl , posixPath } from '@docusaurus/utils' ;
2020-08-28 12:37:49 -04:00
import { difference } from 'lodash' ;
2021-06-02 12:21:45 -04:00
import { resolveSidebarPathOption } from './sidebars' ;
2020-08-17 11:50:22 -04:00
// retro-compatibility: no prefix for the default plugin id
function addPluginIdPrefix ( fileOrDir : string , pluginId : string ) : string {
if ( pluginId === DEFAULT_PLUGIN_ID ) {
return fileOrDir ;
} else {
return ` ${ pluginId } _ ${ fileOrDir } ` ;
}
}
export function getVersionedDocsDirPath (
siteDir : string ,
pluginId : string ,
) : string {
return path . join ( siteDir , addPluginIdPrefix ( VERSIONED_DOCS_DIR , pluginId ) ) ;
}
export function getVersionedSidebarsDirPath (
siteDir : string ,
pluginId : string ,
) : string {
return path . join (
siteDir ,
addPluginIdPrefix ( VERSIONED_SIDEBARS_DIR , pluginId ) ,
) ;
}
export function getVersionsFilePath ( siteDir : string , pluginId : string ) : string {
return path . join ( siteDir , addPluginIdPrefix ( VERSIONS_JSON_FILE , pluginId ) ) ;
}
function ensureValidVersionString ( version : unknown ) : asserts version is string {
if ( typeof version !== 'string' ) {
throw new Error (
2021-06-16 05:37:28 -04:00
` Versions should be strings. Found type " ${ typeof version } " for version " ${ version } ". ` ,
2020-08-17 11:50:22 -04:00
) ;
}
// Should we forbid versions with special chars like / ?
if ( version . trim ( ) . length === 0 ) {
2021-06-16 05:37:28 -04:00
throw new Error ( ` Invalid version " ${ version } ". ` ) ;
2020-08-17 11:50:22 -04:00
}
}
function ensureValidVersionArray (
versionArray : unknown ,
) : asserts versionArray is string [ ] {
if ( ! ( versionArray instanceof Array ) ) {
throw new Error (
2021-06-16 05:37:28 -04:00
` The versions file should contain an array of versions! Found content: ${ JSON . stringify (
2020-08-17 11:50:22 -04:00
versionArray ,
) } ` ,
) ;
}
versionArray . forEach ( ensureValidVersionString ) ;
}
// TODO not easy to make async due to many deps
function readVersionsFile ( siteDir : string , pluginId : string ) : string [ ] | null {
const versionsFilePath = getVersionsFilePath ( siteDir , pluginId ) ;
if ( fs . existsSync ( versionsFilePath ) ) {
const content = JSON . parse ( fs . readFileSync ( versionsFilePath , 'utf8' ) ) ;
ensureValidVersionArray ( content ) ;
return content ;
} else {
return null ;
}
}
// TODO not easy to make async due to many deps
function readVersionNames (
siteDir : string ,
options : Pick <
PluginOptions ,
'id' | 'disableVersioning' | 'includeCurrentVersion'
> ,
) : string [ ] {
const versionFileContent = readVersionsFile ( siteDir , options . id ) ;
if ( ! versionFileContent && options . disableVersioning ) {
throw new Error (
2021-06-16 05:37:28 -04:00
` Docs: using "disableVersioning= ${ options . disableVersioning } " option on a non-versioned site does not make sense. ` ,
2020-08-17 11:50:22 -04:00
) ;
}
const versions = options . disableVersioning ? [ ] : versionFileContent ? ? [ ] ;
// We add the current version at the beginning, unless
// - user don't want to
// - it's been explicitly added to versions.json
if (
options . includeCurrentVersion &&
! versions . includes ( CURRENT_VERSION_NAME )
) {
versions . unshift ( CURRENT_VERSION_NAME ) ;
}
if ( versions . length === 0 ) {
throw new Error (
2021-06-16 05:37:28 -04:00
` It is not possible to use docs without any version. Please check the configuration of these options: "includeCurrentVersion= ${ options . includeCurrentVersion } ", "disableVersioning= ${ options . disableVersioning } ". ` ,
2020-08-17 11:50:22 -04:00
) ;
}
return versions ;
}
2020-12-28 04:25:47 -05:00
function getDocsDirPathLocalized ( {
siteDir ,
locale ,
pluginId ,
versionName ,
} : {
siteDir : string ;
locale : string ;
pluginId : string ;
versionName : string ;
} ) {
return getPluginI18nPath ( {
siteDir ,
locale ,
pluginName : 'docusaurus-plugin-content-docs' ,
pluginId ,
subPaths : [
versionName === CURRENT_VERSION_NAME
? CURRENT_VERSION_NAME
: ` version- ${ versionName } ` ,
] ,
} ) ;
}
2020-08-17 11:50:22 -04:00
function getVersionMetadataPaths ( {
versionName ,
context ,
options ,
} : {
versionName : string ;
2020-11-26 06:16:46 -05:00
context : Pick < LoadContext , ' siteDir ' | ' i18n ' > ;
2020-08-17 11:50:22 -04:00
options : Pick < PluginOptions , ' id ' | ' path ' | ' sidebarPath ' > ;
2020-11-26 06:16:46 -05:00
} ) : Pick <
VersionMetadata ,
2021-03-12 09:11:08 -05:00
'contentPath' | 'contentPathLocalized' | 'sidebarFilePath'
2020-11-26 06:16:46 -05:00
> {
2020-08-17 11:50:22 -04:00
const isCurrentVersion = versionName === CURRENT_VERSION_NAME ;
2021-03-12 09:11:08 -05:00
const contentPath = isCurrentVersion
2020-08-17 11:50:22 -04:00
? path . resolve ( context . siteDir , options . path )
: path . join (
getVersionedDocsDirPath ( context . siteDir , options . id ) ,
` version- ${ versionName } ` ,
) ;
2021-03-12 09:11:08 -05:00
const contentPathLocalized = getDocsDirPathLocalized ( {
2020-11-26 06:16:46 -05:00
siteDir : context.siteDir ,
locale : context.i18n.currentLocale ,
pluginId : options.id ,
2020-12-28 04:25:47 -05:00
versionName ,
2020-11-26 06:16:46 -05:00
} ) ;
2021-05-18 12:27:46 -04:00
function getSidebarFilePath() {
if ( isCurrentVersion ) {
2021-06-02 12:21:45 -04:00
return resolveSidebarPathOption ( context . siteDir , options . sidebarPath ) ;
2021-05-18 12:27:46 -04:00
} else {
return path . join (
2020-08-17 11:50:22 -04:00
getVersionedSidebarsDirPath ( context . siteDir , options . id ) ,
` version- ${ versionName } -sidebars.json ` ,
) ;
2021-05-18 12:27:46 -04:00
}
}
2020-08-17 11:50:22 -04:00
2021-05-18 12:27:46 -04:00
return {
contentPath ,
contentPathLocalized ,
sidebarFilePath : getSidebarFilePath ( ) ,
} ;
2020-08-17 11:50:22 -04:00
}
2020-12-28 04:25:47 -05:00
function getVersionEditUrls ( {
2021-03-12 09:11:08 -05:00
contentPath ,
contentPathLocalized ,
2020-12-28 04:25:47 -05:00
context : { siteDir , i18n } ,
options : { id , path : currentVersionPath , editUrl , editCurrentVersion } ,
} : {
2021-03-12 09:11:08 -05:00
contentPath : string ;
contentPathLocalized : string ;
2020-12-28 04:25:47 -05:00
context : Pick < LoadContext , ' siteDir ' | ' i18n ' > ;
options : Pick <
PluginOptions ,
'id' | 'path' | 'editUrl' | 'editCurrentVersion'
> ;
} ) : { versionEditUrl : string ; versionEditUrlLocalized : string } | undefined {
if ( ! editUrl ) {
return undefined ;
}
2021-01-29 09:35:25 -05:00
// if the user is using the functional form of editUrl,
// he has total freedom and we can't compute a "version edit url"
if ( typeof editUrl === 'function' ) {
return undefined ;
}
2021-03-12 09:11:08 -05:00
const editDirPath = editCurrentVersion ? currentVersionPath : contentPath ;
2020-12-28 04:25:47 -05:00
const editDirPathLocalized = editCurrentVersion
? getDocsDirPathLocalized ( {
siteDir ,
locale : i18n.currentLocale ,
versionName : CURRENT_VERSION_NAME ,
pluginId : id ,
} )
2021-03-12 09:11:08 -05:00
: contentPathLocalized ;
2020-12-28 04:25:47 -05:00
const versionPathSegment = posixPath (
path . relative ( siteDir , path . resolve ( siteDir , editDirPath ) ) ,
) ;
const versionPathSegmentLocalized = posixPath (
path . relative ( siteDir , path . resolve ( siteDir , editDirPathLocalized ) ) ,
) ;
const versionEditUrl = normalizeUrl ( [ editUrl , versionPathSegment ] ) ;
const versionEditUrlLocalized = normalizeUrl ( [
editUrl ,
versionPathSegmentLocalized ,
] ) ;
return {
versionEditUrl ,
versionEditUrlLocalized ,
} ;
}
2021-06-24 12:04:16 -04:00
function getDefaultVersionBanner ( {
versionName ,
versionNames ,
lastVersionName ,
} : {
versionName : string ;
versionNames : string [ ] ;
lastVersionName : string ;
2021-09-01 08:34:26 -04:00
} ) : VersionBanner | null {
2021-06-24 12:04:16 -04:00
// Current version: good, no banner
if ( versionName === lastVersionName ) {
2021-09-01 08:34:26 -04:00
return null ;
2021-06-24 12:04:16 -04:00
}
// Upcoming versions: unreleased banner
else if (
versionNames . indexOf ( versionName ) < versionNames . indexOf ( lastVersionName )
) {
return 'unreleased' ;
}
// Older versions: display unmaintained banner
else {
return 'unmaintained' ;
}
}
function getVersionBanner ( {
versionName ,
versionNames ,
lastVersionName ,
options ,
} : {
versionName : string ;
versionNames : string [ ] ;
lastVersionName : string ;
options : Pick < PluginOptions , ' versions ' > ;
2021-09-01 08:34:26 -04:00
} ) : VersionBanner | null {
const versionBannerOption = options . versions [ versionName ] ? . banner ;
if ( versionBannerOption ) {
return versionBannerOption === 'none' ? null : versionBannerOption ;
}
return getDefaultVersionBanner ( {
versionName ,
versionNames ,
lastVersionName ,
} ) ;
2021-06-24 12:04:16 -04:00
}
2021-08-31 09:40:37 -04:00
function getVersionBadge ( {
versionName ,
versionNames ,
options ,
} : {
versionName : string ;
versionNames : string [ ] ;
options : Pick < PluginOptions , ' versions ' > ;
} ) : boolean {
const versionBadgeOption = options . versions [ versionName ] ? . badge ;
// If site is not versioned or only one version is included
// we don't show the version badge by default
// See https://github.com/facebook/docusaurus/issues/3362
const versionBadgeDefault = versionNames . length !== 1 ;
return versionBadgeOption ? ? versionBadgeDefault ;
}
function getVersionClassName ( {
versionName ,
options ,
} : {
versionName : string ;
options : Pick < PluginOptions , ' versions ' > ;
} ) : string {
const versionClassNameOption = options . versions [ versionName ] ? . className ;
const versionClassNameDefault = ` docs-version- ${ versionName } ` ;
return versionClassNameOption ? ? versionClassNameDefault ;
}
2020-08-17 11:50:22 -04:00
function createVersionMetadata ( {
versionName ,
2021-06-24 12:04:16 -04:00
versionNames ,
lastVersionName ,
2020-08-17 11:50:22 -04:00
context ,
options ,
} : {
versionName : string ;
2021-06-24 12:04:16 -04:00
versionNames : string [ ] ;
lastVersionName : string ;
2020-11-26 06:16:46 -05:00
context : Pick < LoadContext , ' siteDir ' | ' baseUrl ' | ' i18n ' > ;
2020-08-28 12:37:49 -04:00
options : Pick <
PluginOptions ,
2020-12-28 04:25:47 -05:00
| 'id'
| 'path'
| 'sidebarPath'
| 'routeBasePath'
| 'versions'
| 'editUrl'
| 'editCurrentVersion'
2020-08-28 12:37:49 -04:00
> ;
2020-08-17 11:50:22 -04:00
} ) : VersionMetadata {
2020-11-26 06:16:46 -05:00
const {
sidebarFilePath ,
2021-03-12 09:11:08 -05:00
contentPath ,
contentPathLocalized ,
2020-11-26 06:16:46 -05:00
} = getVersionMetadataPaths ( {
2020-08-17 11:50:22 -04:00
versionName ,
context ,
options ,
} ) ;
2021-06-24 12:04:16 -04:00
const isLast = versionName === lastVersionName ;
2020-08-28 12:37:49 -04:00
// retro-compatible values
const defaultVersionLabel =
2020-08-17 11:50:22 -04:00
versionName === CURRENT_VERSION_NAME ? 'Next' : versionName ;
2021-06-24 12:12:48 -04:00
function getDefaultVersionPathPart() {
if ( isLast ) {
return '' ;
}
return versionName === CURRENT_VERSION_NAME ? 'next' : versionName ;
}
const defaultVersionPathPart = getDefaultVersionPathPart ( ) ;
2020-08-17 11:50:22 -04:00
2020-08-28 12:37:49 -04:00
const versionOptions : VersionOptions = options . versions [ versionName ] ? ? { } ;
const versionLabel = versionOptions . label ? ? defaultVersionLabel ;
const versionPathPart = versionOptions . path ? ? defaultVersionPathPart ;
2020-08-17 11:50:22 -04:00
const versionPath = normalizeUrl ( [
context . baseUrl ,
options . routeBasePath ,
versionPathPart ,
] ) ;
2020-12-28 04:25:47 -05:00
const versionEditUrls = getVersionEditUrls ( {
2021-03-12 09:11:08 -05:00
contentPath ,
contentPathLocalized ,
2020-12-28 04:25:47 -05:00
context ,
options ,
} ) ;
2020-08-17 11:50:22 -04:00
// Because /docs/:route` should always be after `/docs/versionName/:route`.
const routePriority = versionPathPart === '' ? - 1 : undefined ;
2021-08-19 04:31:15 -04:00
// the path that will be used to refer the docs tags
// example below will be using /docs/tags
const tagsPath = normalizeUrl ( [ versionPath , 'tags' ] ) ;
2020-08-17 11:50:22 -04:00
return {
versionName ,
versionLabel ,
versionPath ,
2021-08-19 04:31:15 -04:00
tagsPath ,
2020-12-28 04:25:47 -05:00
versionEditUrl : versionEditUrls?.versionEditUrl ,
versionEditUrlLocalized : versionEditUrls?.versionEditUrlLocalized ,
2021-06-24 12:04:16 -04:00
versionBanner : getVersionBanner ( {
versionName ,
versionNames ,
lastVersionName ,
options ,
} ) ,
2021-08-31 09:40:37 -04:00
versionBadge : getVersionBadge ( { versionName , versionNames , options } ) ,
versionClassName : getVersionClassName ( { versionName , options } ) ,
2020-08-17 11:50:22 -04:00
isLast ,
routePriority ,
sidebarFilePath ,
2021-03-12 09:11:08 -05:00
contentPath ,
contentPathLocalized ,
2020-08-17 11:50:22 -04:00
} ;
}
function checkVersionMetadataPaths ( {
2020-12-28 13:50:12 -05:00
versionMetadata ,
context ,
} : {
versionMetadata : VersionMetadata ;
context : Pick < LoadContext , ' siteDir ' > ;
} ) {
2021-03-12 09:11:08 -05:00
const { versionName , contentPath , sidebarFilePath } = versionMetadata ;
2020-12-28 13:50:12 -05:00
const { siteDir } = context ;
2021-05-18 12:27:46 -04:00
const isCurrentVersion = versionName === CURRENT_VERSION_NAME ;
2020-12-28 13:50:12 -05:00
2021-03-12 09:11:08 -05:00
if ( ! fs . existsSync ( contentPath ) ) {
2020-08-17 11:50:22 -04:00
throw new Error (
2021-06-16 05:37:28 -04:00
` The docs folder does not exist for version " ${ versionName } ". A docs folder is expected to be found at ${ path . relative (
2020-12-28 13:50:12 -05:00
siteDir ,
2021-03-12 09:11:08 -05:00
contentPath ,
2021-06-16 05:37:28 -04:00
) } . ` ,
2020-08-17 11:50:22 -04:00
) ;
}
2020-09-01 10:31:33 -04:00
2021-05-18 12:27:46 -04:00
// If the current version defines a path to a sidebar file that does not exist, we throw!
// Note: for versioned sidebars, the file may not exist (as we prefer to not create it rather than to create an empty file)
2020-09-01 10:31:33 -04:00
// See https://github.com/facebook/docusaurus/issues/3366
2021-05-18 12:27:46 -04:00
// See https://github.com/facebook/docusaurus/pull/4775
if (
isCurrentVersion &&
typeof sidebarFilePath === 'string' &&
! fs . existsSync ( sidebarFilePath )
) {
2021-06-16 05:37:28 -04:00
throw new Error ( ` The path to the sidebar file does not exist at " ${ path . relative (
2021-05-18 12:27:46 -04:00
siteDir ,
sidebarFilePath ,
2021-06-16 05:37:28 -04:00
) } " .
Please set the docs "sidebarPath" field in your config file to :
2021-05-18 12:27:46 -04:00
- a sidebars path that exists
- false : to disable the sidebar
- undefined : for Docusaurus generates it automatically ` );
2020-08-17 11:50:22 -04:00
}
}
// TODO for retrocompatibility with existing behavior
// We should make this configurable
// "last version" is not a very good concept nor api surface
2020-08-28 12:37:49 -04:00
function getDefaultLastVersionName ( versionNames : string [ ] ) {
2020-08-17 11:50:22 -04:00
if ( versionNames . length === 1 ) {
return versionNames [ 0 ] ;
} else {
return versionNames . filter (
( versionName ) = > versionName !== CURRENT_VERSION_NAME ,
) [ 0 ] ;
}
}
2020-08-28 12:37:49 -04:00
function checkVersionsOptions (
availableVersionNames : string [ ] ,
options : VersionsOptions ,
) {
const availableVersionNamesMsg = ` Available version names are: ${ availableVersionNames . join (
', ' ,
) } ` ;
if (
options . lastVersion &&
! availableVersionNames . includes ( options . lastVersion )
) {
throw new Error (
` Docs option lastVersion= ${ options . lastVersion } is invalid. ${ availableVersionNamesMsg } ` ,
) ;
}
2020-08-31 11:08:24 -04:00
const unknownVersionConfigNames = difference (
2020-08-28 12:37:49 -04:00
Object . keys ( options . versions ) ,
availableVersionNames ,
) ;
2020-08-31 11:08:24 -04:00
if ( unknownVersionConfigNames . length > 0 ) {
2020-08-28 12:37:49 -04:00
throw new Error (
2021-06-16 05:37:28 -04:00
` Invalid docs option "versions": unknown versions ( ${ unknownVersionConfigNames . join (
2020-08-28 12:37:49 -04:00
',' ,
2021-06-16 05:37:28 -04:00
) } ) found . $ { availableVersionNamesMsg } ` ,
2020-08-28 12:37:49 -04:00
) ;
}
2020-08-31 11:08:24 -04:00
if ( options . onlyIncludeVersions ) {
if ( options . onlyIncludeVersions . length === 0 ) {
throw new Error (
2021-06-16 05:37:28 -04:00
` Invalid docs option "onlyIncludeVersions": an empty array is not allowed, at least one version is needed. ` ,
2020-08-31 11:08:24 -04:00
) ;
}
const unknownOnlyIncludeVersionNames = difference (
options . onlyIncludeVersions ,
availableVersionNames ,
) ;
if ( unknownOnlyIncludeVersionNames . length > 0 ) {
throw new Error (
2021-06-16 05:37:28 -04:00
` Invalid docs option "onlyIncludeVersions": unknown versions ( ${ unknownOnlyIncludeVersionNames . join (
2020-08-31 11:08:24 -04:00
',' ,
2021-06-16 05:37:28 -04:00
) } ) found . $ { availableVersionNamesMsg } ` ,
2020-08-31 11:08:24 -04:00
) ;
}
if (
options . lastVersion &&
! options . onlyIncludeVersions . includes ( options . lastVersion )
) {
throw new Error (
2021-06-16 05:37:28 -04:00
` Invalid docs option "lastVersion": if you use both the "onlyIncludeVersions" and "lastVersion" options, then "lastVersion" must be present in the provided "onlyIncludeVersions" array. ` ,
2020-08-31 11:08:24 -04:00
) ;
}
}
}
// Filter versions according to provided options
// Note: we preserve the order in which versions are provided
// the order of the onlyIncludeVersions array does not matter
function filterVersions (
versionNamesUnfiltered : string [ ] ,
options : Pick < PluginOptions , ' onlyIncludeVersions ' > ,
) {
if ( options . onlyIncludeVersions ) {
return versionNamesUnfiltered . filter ( ( name ) = >
2021-03-17 12:28:42 -04:00
( options . onlyIncludeVersions || [ ] ) . includes ( name ) ,
2020-08-31 11:08:24 -04:00
) ;
} else {
return versionNamesUnfiltered ;
}
2020-08-28 12:37:49 -04:00
}
2021-04-15 10:20:11 -04:00
// TODO make this async (requires plugin init to be async)
2020-08-17 11:50:22 -04:00
export function readVersionsMetadata ( {
context ,
options ,
} : {
2020-11-26 06:16:46 -05:00
context : Pick < LoadContext , ' siteDir ' | ' baseUrl ' | ' i18n ' > ;
2020-08-17 11:50:22 -04:00
options : Pick <
PluginOptions ,
| 'id'
| 'path'
| 'sidebarPath'
| 'routeBasePath'
| 'includeCurrentVersion'
| 'disableVersioning'
2020-08-28 12:37:49 -04:00
| 'lastVersion'
| 'versions'
2020-08-31 11:08:24 -04:00
| 'onlyIncludeVersions'
2020-12-28 04:25:47 -05:00
| 'editUrl'
| 'editCurrentVersion'
2020-08-17 11:50:22 -04:00
> ;
} ) : VersionMetadata [ ] {
2020-08-31 11:08:24 -04:00
const versionNamesUnfiltered = readVersionNames ( context . siteDir , options ) ;
checkVersionsOptions ( versionNamesUnfiltered , options ) ;
2020-08-28 12:37:49 -04:00
2020-08-31 11:08:24 -04:00
const versionNames = filterVersions ( versionNamesUnfiltered , options ) ;
2020-08-28 12:37:49 -04:00
const lastVersionName =
options . lastVersion ? ? getDefaultLastVersionName ( versionNames ) ;
2020-08-17 11:50:22 -04:00
const versionsMetadata = versionNames . map ( ( versionName ) = >
createVersionMetadata ( {
versionName ,
2021-06-24 12:04:16 -04:00
versionNames ,
lastVersionName ,
2020-08-17 11:50:22 -04:00
context ,
options ,
} ) ,
) ;
2020-12-28 13:50:12 -05:00
versionsMetadata . forEach ( ( versionMetadata ) = >
checkVersionMetadataPaths ( { versionMetadata , context } ) ,
) ;
2020-08-17 11:50:22 -04:00
return versionsMetadata ;
}
2020-11-26 06:16:46 -05:00
// order matter!
// Read in priority the localized path, then the unlocalized one
// We want the localized doc to "override" the unlocalized one
export function getDocsDirPaths (
versionMetadata : Pick <
VersionMetadata ,
2021-03-12 09:11:08 -05:00
'contentPath' | 'contentPathLocalized'
2020-11-26 06:16:46 -05:00
> ,
) : [ string , string ] {
2021-03-12 09:11:08 -05:00
return [ versionMetadata . contentPathLocalized , versionMetadata . contentPath ] ;
2020-11-26 06:16:46 -05:00
}