diff --git a/lib/create-tree.js b/lib/create-tree.js new file mode 100644 index 000000000000..eb2053c2803a --- /dev/null +++ b/lib/create-tree.js @@ -0,0 +1,60 @@ +const fs = require('fs') +const path = require('path') +const Page = require('./page') +const { sortBy } = require('lodash') + +let basePath + +module.exports = async function createTree (originalPath, langObj) { + // Do not reset this value on recursive runs + if (!basePath) basePath = originalPath + + // On recursive runs, this is processing page.children items in `/` format. + // If the path exists as is, assume this is a directory with a child index.md. + // Otherwise, assume it's a child .md file and add `.md` to the path. + let filepath + try { + await fs.promises.access(originalPath) + filepath = `${originalPath}/index.md` + } catch { + filepath = `${originalPath}.md` + } + + const relativePath = filepath.replace(`${basePath}/`, '') + const localizedBasePath = path.join(__dirname, '..', langObj.dir, 'content') + + // Initialize the Page! This is where the magic happens (sorry). + const page = await Page.init({ + basePath: localizedBasePath, + relativePath, + languageCode: langObj.code + }) + + if (!page) { + // Do not throw an error if early access is not available + if (relativePath.startsWith('early-access')) return + + throw Error(`Cannot initialize page for ${filepath}`) + } + + // Create the root tree object on the first run, and create children recursively + const item = { + relativePath, + title: page.shortTitle || page.title, + // parentPath: parentPath || null, + page + } + + // Process frontmatter children recursively + if (item.page.children) { + item.childPages = sortBy( + (await Promise.all(item.page.children + .map(async (child) => await createTree(path.join(originalPath, child), langObj)))) + .filter(Boolean), + // Sort by the ordered array of children in the frontmatter + item.page.children + ) + } + + return item +} diff --git a/lib/frontmatter.js b/lib/frontmatter.js index c875db25152b..918fa26d7400 100644 --- a/lib/frontmatter.js +++ b/lib/frontmatter.js @@ -36,6 +36,9 @@ const schema = { minimum: 2, maximum: 4 }, + children: { + type: 'array' + }, mapTopic: { type: 'boolean' }, diff --git a/lib/get-document-type.js b/lib/get-document-type.js new file mode 100644 index 000000000000..8c3fab956ed1 --- /dev/null +++ b/lib/get-document-type.js @@ -0,0 +1,17 @@ +module.exports = function getDocumentType (relativePath) { + if (!relativePath.endsWith('index.md')) { + return 'article' + } + + // Derive the document type from the path segment length + switch (relativePath.split('/').length) { + case 1: + return 'homepage' + case 2: + return 'product' + case 3: + return 'category' + case 4: + return 'mapTopic' + } +} diff --git a/lib/page.js b/lib/page.js index f9e2a43a17e3..7c7edd0af460 100644 --- a/lib/page.js +++ b/lib/page.js @@ -18,6 +18,7 @@ const statsd = require('./statsd') const readFileContents = require('./read-file-contents') const getLinkData = require('./get-link-data') const union = require('lodash/union') +// const getDocumentType = require('./get-document-type') class Page { static async init (opts) { @@ -69,8 +70,14 @@ class Page { this.rawLearningTracks = this.learningTracks this.rawIncludeGuides = this.includeGuides + // Is this a Product, Category, Topic, or Article? + // this.documentType = getDocumentType(this.relativePath) + + // Get array of versions that the page is available in for fast lookup + this.applicableVersions = getApplicableVersions(this.versions, this.fullPath) + // a page should only be available in versions that its parent product is available in - const versionsParentProductIsNotAvailableIn = getApplicableVersions(this.versions, this.fullPath) + const versionsParentProductIsNotAvailableIn = this.applicableVersions // only the homepage will not have this.parentProduct .filter(availableVersion => this.parentProduct && !this.parentProduct.versions.includes(availableVersion)) diff --git a/lib/pages.js b/lib/pages.js index c037c2213cbb..c4dd05bf5a24 100644 --- a/lib/pages.js +++ b/lib/pages.js @@ -1,7 +1,82 @@ const path = require('path') +const languages = require('./languages') +const versions = Object.keys(require('./all-versions')) +const createTree = require('./create-tree') +const nonEnterpriseDefaultVersion = require('./non-enterprise-default-version') +const englishPath = path.join(__dirname, '..', 'content') const walk = require('walk-sync').entries const Page = require('./page') -const languages = require('./languages') + +// This function creates a nested object that can be accessed like this: +// siteTree[languageCode][version].childPages[].childPages[] (etc...) +async function loadTree () { + // We only need to initialize pages once per language (since pages don't change per version), so we do that + // first since it's the most expensive work. This gets us a nested object with pages attached that we can use + // as the basis for the siteTree after we do some versioning. + const rawTree = {} + await Promise.all(Object.values(languages) + .map(async (langObj) => { + rawTree[langObj.code] = await createTree(englishPath, langObj) + })) + + // Now that we have the paged tree, we can walk it for every version and do two operations: + // 1. Add a versioned href to every node (we can get this easily from the permalinks array). + // 2. Recurisvely drop any child pages that are not available in the current version. + // Note that order of languages and versions doesn't matter, but order of child page arrays DOES matter (for nav). + const siteTree = {} + await Promise.all(Object.keys(languages).map(async (langCode) => { + const treePerVersion = {} + + await Promise.all(versions.map(async (version) => { + // Yes, we are mutating the rawTree object here. + versionPages(rawTree[langCode]) + + // This step can't be asynchronous because the order of child pages matters. + function versionPages (item) { + // Add a versioned href as a convenience for use in layouts. + item.href = item.page.permalinks + .find(pl => pl.pageVersion === version || (pl.pageVersion === 'homepage' && version === nonEnterpriseDefaultVersion)) + + if (!item.childPages) return item + + // Drop child pages that do not apply to the current version + item.childPages = item.childPages.filter(childPage => childPage.page.applicableVersions.includes(version)) + item.childPages.forEach(childPage => versionPages(childPage)) + } + + treePerVersion[version] = rawTree[langCode] + })) + + siteTree[langCode] = treePerVersion + })) + + console.log(siteTree.en[nonEnterpriseDefaultVersion]) + + return siteTree +} + +async function loadPageListFromTree (tree) { + const siteTree = tree || await loadTree() + + // Traverse the tree of pages and create a simple array of pages. + // (We don't care about the language code or version here, so we can start two levels in.) + const collection = Object.values(siteTree).map(v => Object.values(v)).flat() + const result = [] + collectPages(collection, result) + + function collectPages (arr, result) { + arr.forEach(item => { + if (item.page) { + result.push(item.page) + } + if (item.childPages) { + collectPages(item.childPages, result) + } + }) + } + + return result +} async function loadPageList () { // load english pages @@ -66,6 +141,8 @@ async function loadPageMap (pageList) { } module.exports = { + loadTree, + // loadPages: loadPageListFromTree, loadPages: loadPageList, loadPageMap }