@@ -3,7 +3,6 @@ import pick from 'lodash/pick'
|
||||
|
||||
import type { BreadcrumbT } from 'components/page-header/Breadcrumbs'
|
||||
import type { FeatureFlags } from 'components/hooks/useFeatureFlags'
|
||||
import { ExcludesNull } from 'components/lib/ExcludesNull'
|
||||
|
||||
export type ProductT = {
|
||||
external: boolean
|
||||
@@ -27,14 +26,9 @@ type VersionItem = {
|
||||
}
|
||||
|
||||
export type ProductTreeNode = {
|
||||
page: {
|
||||
hidden?: boolean
|
||||
documentType: 'article' | 'mapTopic'
|
||||
title: string
|
||||
shortTitle: string
|
||||
}
|
||||
renderedShortTitle?: string
|
||||
renderedFullTitle: string
|
||||
documentType: 'article' | 'mapTopic'
|
||||
title: string
|
||||
shortTitle: string
|
||||
href: string
|
||||
childPages: Array<ProductTreeNode>
|
||||
}
|
||||
@@ -178,9 +172,10 @@ export const getMainContext = async (req: any, res: any): Promise<MainContextT>
|
||||
enterpriseServerVersions: req.context.enterpriseServerVersions,
|
||||
allVersions: req.context.allVersions,
|
||||
currentVersion: req.context.currentVersion,
|
||||
currentProductTree: req.context.currentProductTree
|
||||
? getCurrentProductTree(req.context.currentProductTree)
|
||||
: null,
|
||||
// This is a slimmed down version of `req.context.currentProductTree`
|
||||
// that only has the minimal titles stuff needed for sidebars and
|
||||
// any page that is hidden is omitted.
|
||||
currentProductTree: req.context.currentProductTreeTitlesExcludeHidden || null,
|
||||
featureFlags: {},
|
||||
searchVersions: req.context.searchVersions,
|
||||
nonEnterpriseDefaultVersion: req.context.nonEnterpriseDefaultVersion,
|
||||
@@ -189,26 +184,6 @@ export const getMainContext = async (req: any, res: any): Promise<MainContextT>
|
||||
}
|
||||
}
|
||||
|
||||
// only pull things we need from the product tree, and make sure there are default values instead of `undefined`
|
||||
const getCurrentProductTree = (input: any): ProductTreeNode | null => {
|
||||
if (input.page.hidden) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
href: input.href,
|
||||
renderedShortTitle: input.renderedShortTitle || '',
|
||||
renderedFullTitle: input.renderedFullTitle || '',
|
||||
page: {
|
||||
hidden: input.page.hidden || false,
|
||||
documentType: input.page.documentType,
|
||||
title: input.page.title,
|
||||
shortTitle: input.page.shortTitle || '',
|
||||
},
|
||||
childPages: (input.childPages || []).map(getCurrentProductTree).filter(ExcludesNull),
|
||||
}
|
||||
}
|
||||
|
||||
export const MainContext = createContext<MainContextT | null>(null)
|
||||
|
||||
export const useMainContext = (): MainContextT => {
|
||||
|
||||
@@ -110,7 +110,7 @@ export const getProductLandingContextFromRequest = async (
|
||||
hasGuidesPage,
|
||||
product: {
|
||||
href: productTree.href,
|
||||
title: productTree.renderedShortTitle || productTree.renderedFullTitle,
|
||||
title: productTree.page.shortTitle || productTree.page.title,
|
||||
},
|
||||
whatsNewChangelog: req.context.whatsNewChangelog || [],
|
||||
changelogUrl: req.context.changelogUrl || [],
|
||||
|
||||
@@ -19,7 +19,7 @@ export const ProductArticlesList = () => {
|
||||
return (
|
||||
<div className="d-flex gutter flex-wrap" data-testid="product-articles-list">
|
||||
{currentProductTree.childPages.map((treeNode, i) => {
|
||||
if (treeNode.page.documentType === 'article') {
|
||||
if (treeNode.documentType === 'article') {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const ProductTreeNodeList = ({ treeNode }: { treeNode: ProductTreeNode }) => {
|
||||
<div className="col-12 col-lg-4 mb-6 height-full">
|
||||
<h3 className="mb-3 f4">
|
||||
<Link className="color-unset text-underline" href={treeNode.href}>
|
||||
{treeNode.renderedFullTitle}
|
||||
{treeNode.title}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
@@ -58,8 +58,8 @@ const ProductTreeNodeList = ({ treeNode }: { treeNode: ProductTreeNode }) => {
|
||||
}}
|
||||
>
|
||||
<Link className="d-block width-full" href={childNode.href}>
|
||||
{childNode.renderedFullTitle}
|
||||
{childNode.page.documentType === 'mapTopic' ? (
|
||||
{childNode.title}
|
||||
{childNode.documentType === 'mapTopic' ? (
|
||||
<small className="color-fg-muted d-inline-block">
|
||||
• {childNode.childPages.length} articles
|
||||
</small>
|
||||
|
||||
@@ -29,7 +29,7 @@ export const ProductCollapsibleSection = (props: SectionProps) => {
|
||||
|
||||
// The lowest level page link displayed in the tree
|
||||
const renderTerminalPageLink = (page: ProductTreeNode) => {
|
||||
const title = page.renderedShortTitle || page.renderedFullTitle
|
||||
const title = page.shortTitle || page.title
|
||||
|
||||
const isCurrent = routePath === page.href
|
||||
return (
|
||||
@@ -78,10 +78,10 @@ export const ProductCollapsibleSection = (props: SectionProps) => {
|
||||
{
|
||||
<>
|
||||
{/* <!-- some pages have nested child pages (formerly known as a mapTopic) --> */}
|
||||
{page.childPages[0]?.page.documentType === 'mapTopic' ? (
|
||||
{page.childPages[0]?.documentType === 'mapTopic' ? (
|
||||
<ul className="list-style-none position-relative">
|
||||
{page.childPages.map((childPage, i) => {
|
||||
const childTitle = childPage.renderedShortTitle || childPage.renderedFullTitle
|
||||
const childTitle = childPage.shortTitle || childPage.title
|
||||
|
||||
const isActive = routePath.includes(childPage.href)
|
||||
const isCurrent = routePath === childPage.href
|
||||
@@ -108,7 +108,7 @@ export const ProductCollapsibleSection = (props: SectionProps) => {
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
) : page.childPages[0]?.page.documentType === 'article' ? (
|
||||
) : page.childPages[0]?.documentType === 'article' ? (
|
||||
<div data-testid="sidebar-article-group" className="pb-0">
|
||||
<ActionList variant="full" className="my-2">
|
||||
{page.childPages.map(renderTerminalPageLink)}
|
||||
|
||||
@@ -181,7 +181,7 @@ export const RestCollapsibleSection = (props: SectionProps) => {
|
||||
</div>
|
||||
) : (
|
||||
page.childPages.map((childPage, i) => {
|
||||
const childTitle = childPage.renderedShortTitle || childPage.renderedFullTitle
|
||||
const childTitle = childPage.shortTitle || childPage.title
|
||||
const isActive = routePath.includes(childPage.href)
|
||||
const isCurrent = routePath === childPage.href
|
||||
|
||||
|
||||
@@ -36,16 +36,16 @@ export const SidebarProduct = () => {
|
||||
routePath.includes(href)
|
||||
)
|
||||
|
||||
const productTitle = currentProductTree.renderedShortTitle || currentProductTree.renderedFullTitle
|
||||
const productTitle = currentProductTree.shortTitle || currentProductTree.title
|
||||
|
||||
const productSection = () => (
|
||||
<li className="my-3" data-testid="product-sidebar-items">
|
||||
<ul className="list-style-none">
|
||||
{currentProductTree &&
|
||||
currentProductTree.childPages.map((childPage, i) => {
|
||||
const isStandaloneCategory = childPage.page.documentType === 'article'
|
||||
const isStandaloneCategory = childPage.documentType === 'article'
|
||||
|
||||
const childTitle = childPage.renderedShortTitle || childPage.renderedFullTitle
|
||||
const childTitle = childPage.shortTitle || childPage.title
|
||||
const isActive =
|
||||
routePath.includes(childPage.href + '/') || routePath === childPage.href
|
||||
const defaultOpen = hasExactCategory ? isActive : false
|
||||
@@ -96,8 +96,8 @@ export const SidebarProduct = () => {
|
||||
<li className="my-3">
|
||||
<ul className="list-style-none">
|
||||
{conceptualPages.map((childPage, i) => {
|
||||
const isStandaloneCategory = childPage.page.documentType === 'article'
|
||||
const childTitle = childPage.renderedShortTitle || childPage.renderedFullTitle
|
||||
const isStandaloneCategory = childPage.documentType === 'article'
|
||||
const childTitle = childPage.shortTitle || childPage.title
|
||||
const isActive =
|
||||
routePath.includes(childPage.href + '/') || routePath === childPage.href
|
||||
const defaultOpen = hasExactCategory ? isActive : false
|
||||
@@ -145,9 +145,8 @@ export const SidebarProduct = () => {
|
||||
<li className="my-3">
|
||||
<ul className="list-style-none">
|
||||
{restPages.map((childPage, i) => {
|
||||
const isStandaloneCategory = childPage.page.documentType === 'article'
|
||||
|
||||
const childTitle = childPage.renderedShortTitle || childPage.renderedFullTitle
|
||||
const isStandaloneCategory = childPage.documentType === 'article'
|
||||
const childTitle = childPage.shortTitle || childPage.title
|
||||
const isActive =
|
||||
routePath.includes(childPage.href + '/') || routePath === childPage.href
|
||||
const defaultOpen = hasExactCategory ? isActive : false
|
||||
@@ -178,19 +177,15 @@ export const SidebarProduct = () => {
|
||||
<ul data-testid="sidebar" className={styles.container}>
|
||||
<AllProductsLink />
|
||||
|
||||
{!currentProductTree.page.hidden && (
|
||||
<>
|
||||
<li data-testid="sidebar-product" title={productTitle} className="my-2">
|
||||
<Link
|
||||
href={currentProductTree.href}
|
||||
className="pl-4 pr-5 pb-1 f4 color-fg-default no-underline"
|
||||
>
|
||||
{productTitle}
|
||||
</Link>
|
||||
</li>
|
||||
{currentProduct && currentProduct.id === 'rest' ? restSection() : productSection()}
|
||||
</>
|
||||
)}
|
||||
<li data-testid="sidebar-product" title={productTitle} className="my-2">
|
||||
<Link
|
||||
href={currentProductTree.href}
|
||||
className="pl-4 pr-5 pb-1 f4 color-fg-default no-underline"
|
||||
>
|
||||
{productTitle}
|
||||
</Link>
|
||||
</li>
|
||||
{currentProduct && currentProduct.id === 'rest' ? restSection() : productSection()}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,16 +3,12 @@ import path from 'path'
|
||||
import languages from './languages.js'
|
||||
import { allVersions } from './all-versions.js'
|
||||
import createTree, { getBasePath } from './create-tree.js'
|
||||
import renderContent from './render-content/index.js'
|
||||
import loadSiteData from './site-data.js'
|
||||
import nonEnterpriseDefaultVersion from './non-enterprise-default-version.js'
|
||||
import Page from './page.js'
|
||||
import shortVersionsMiddleware from '../middleware/contextualizers/short-versions.js'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const versions = Object.keys(allVersions)
|
||||
const enterpriseServerVersions = versions.filter((v) => v.startsWith('enterprise-server@'))
|
||||
const renderOpts = { textOnly: true, encodeEntities: true }
|
||||
|
||||
// These are the exceptions to the rule.
|
||||
// If a URI starts with one of these prefixes, it basically means we don't
|
||||
@@ -103,31 +99,6 @@ export async function versionPages(obj, version, langCode, site) {
|
||||
(pl.pageVersion === 'homepage' && version === nonEnterpriseDefaultVersion)
|
||||
).href
|
||||
|
||||
const req = {}
|
||||
|
||||
req.context = {
|
||||
allVersions,
|
||||
enterpriseServerVersions,
|
||||
currentLanguage: langCode,
|
||||
currentVersion: version,
|
||||
site: site[langCode].site,
|
||||
}
|
||||
|
||||
// The Liquid parseAndRender method is MUCH faster than renderContent or renderProp.
|
||||
// This only works for titles and short titles, which have no other Markdown that needs
|
||||
// to be converted to HTML, so we can get away with _only_ parsing Liquid.
|
||||
await shortVersionsMiddleware(req, null, () => {})
|
||||
|
||||
obj.renderedFullTitle = obj.page.title.includes('{')
|
||||
? await renderContent.liquid.parseAndRender(obj.page.title, req.context, renderOpts)
|
||||
: obj.page.title
|
||||
|
||||
if (obj.page.shortTitle) {
|
||||
obj.renderedShortTitle = obj.page.shortTitle.includes('{')
|
||||
? await renderContent.liquid.parseAndRender(obj.page.shortTitle, req.context, renderOpts)
|
||||
: obj.page.shortTitle
|
||||
}
|
||||
|
||||
if (!obj.childPages) return obj
|
||||
const versionedChildPages = await Promise.all(
|
||||
obj.childPages
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import liquid from '../../lib/render-content/liquid.js'
|
||||
|
||||
export default async function breadcrumbs(req, res, next) {
|
||||
export default function breadcrumbs(req, res, next) {
|
||||
if (!req.context.page) return next()
|
||||
const isEarlyAccess = req.context.page.relativePath.startsWith('early-access')
|
||||
if (req.context.page.hidden && !isEarlyAccess) return next()
|
||||
@@ -12,76 +10,81 @@ export default async function breadcrumbs(req, res, next) {
|
||||
return next()
|
||||
}
|
||||
|
||||
req.context.breadcrumbs = await getBreadcrumbs(req, isEarlyAccess)
|
||||
req.context.breadcrumbs = getBreadcrumbs(req, isEarlyAccess)
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
const earlyAccessExceptions = ['insights', 'enterprise-importer']
|
||||
|
||||
async function getBreadcrumbs(req, isEarlyAccess = false) {
|
||||
const crumbs = []
|
||||
const { currentPath, currentVersion } = req.context
|
||||
const split = currentPath.split('/')
|
||||
let cutoff = 2
|
||||
function getBreadcrumbs(req, isEarlyAccess) {
|
||||
let cutoff = 0
|
||||
// When in Early access docs consider the "root" be much higher.
|
||||
// E.g. /en/early-access/github/migrating/understanding/about
|
||||
// we only want it start at /migrating/understanding/about
|
||||
// Essentially, we're skipping "/early-access" and its first
|
||||
// top-level like "/github"
|
||||
if (isEarlyAccess) {
|
||||
// When in Early access docs consider the "root" be much higher.
|
||||
// E.g. /en/early-access/github/migrating/understanding/about
|
||||
// we only want it start at /migrating/understanding/about
|
||||
// Essentially, we're skipping "/early-access" and its first
|
||||
// top-level like "/github"
|
||||
cutoff++
|
||||
|
||||
const split = req.context.currentPath.split('/')
|
||||
// There are a few exceptions to this rule for the
|
||||
// /{version}/early-access/<product-name>/... URLs because they're a
|
||||
// bit different.
|
||||
// If there are more known exceptions, add them to the array above.
|
||||
if (!earlyAccessExceptions.some((product) => split.includes(product))) {
|
||||
cutoff++
|
||||
}
|
||||
|
||||
// If the URL is early access AND has a version in it, go even further
|
||||
// E.g. /en/enterprise-server@3.3/early-access/admin/hosting/mysql
|
||||
// should start at /hosting/mysql.
|
||||
if (currentVersion !== 'free-pro-team@latest') {
|
||||
cutoff++
|
||||
}
|
||||
}
|
||||
while (split.length > cutoff && split[split.length - 1] !== currentVersion) {
|
||||
const href = split.join('/')
|
||||
const page = req.context.pages[href]
|
||||
if (page) {
|
||||
crumbs.push({
|
||||
href,
|
||||
title: await getShortTitle(page, req.context),
|
||||
})
|
||||
if (earlyAccessExceptions.some((product) => split.includes(product))) {
|
||||
cutoff = 1
|
||||
} else {
|
||||
console.warn(`No page found with for '${href}'`)
|
||||
cutoff = 2
|
||||
}
|
||||
split.pop()
|
||||
}
|
||||
crumbs.reverse()
|
||||
|
||||
const breadcrumbs = traverseTreeTitles(
|
||||
req.context.currentPath,
|
||||
req.context.currentProductTreeTitles
|
||||
)
|
||||
;[...Array(cutoff)].forEach(() => breadcrumbs.shift())
|
||||
|
||||
return breadcrumbs
|
||||
}
|
||||
|
||||
// Return an array as if you'd traverse down a tree. Imagine a tree like
|
||||
//
|
||||
// (root /)
|
||||
// / \
|
||||
// (/foo) (/bar)
|
||||
// / \
|
||||
// (/foo/bar) (/foo/buzz)
|
||||
//
|
||||
// If the "currentPath" is `/foo/buzz` what you want to return is:
|
||||
//
|
||||
// [
|
||||
// {href: /, title: TITLE},
|
||||
// {href: /foo, title: TITLE}
|
||||
// {href: /foo/buzz, title: TITLE}
|
||||
// ]
|
||||
//
|
||||
function traverseTreeTitles(currentPath, tree) {
|
||||
const { href, title, shortTitle } = tree
|
||||
const crumbs = [
|
||||
{
|
||||
href,
|
||||
title: shortTitle || title,
|
||||
},
|
||||
]
|
||||
const currentPathSplit = Array.isArray(currentPath) ? currentPath : currentPath.split('/')
|
||||
for (const child of tree.childPages) {
|
||||
if (isParentOrEqualArray(child.href.split('/'), currentPathSplit)) {
|
||||
crumbs.push(...traverseTreeTitles(currentPathSplit, child))
|
||||
// Only ever going down 1 of the children
|
||||
break
|
||||
}
|
||||
}
|
||||
return crumbs
|
||||
}
|
||||
|
||||
async function getShortTitle(page, context) {
|
||||
// Note! Don't use `page.title` or `page.shortTitle` because if they get
|
||||
// set during rendering, they become the HTML entities encoded string.
|
||||
// E.g. "Delete & restore a package"
|
||||
|
||||
if (page.rawShortTitle) {
|
||||
if (page.rawShortTitle.includes('{')) {
|
||||
// Can't easily cache this because the `page` is reused for multiple
|
||||
// permalinks. We could do what the `Page.render()` method does which
|
||||
// specifically caches based on the `context.currentPath` but at
|
||||
// this point it's probably not worth it.
|
||||
return await liquid.parseAndRender(page.rawShortTitle, context)
|
||||
}
|
||||
return page.rawShortTitle
|
||||
}
|
||||
if (page.rawTitle.includes('{')) {
|
||||
return await liquid.parseAndRender(page.rawTitle, context)
|
||||
}
|
||||
return page.rawTitle
|
||||
// Return true if an array is part of another array or equal.
|
||||
// Like `/foo/bar` is part of `/foo/bar/buzz`.
|
||||
// But also include `/foo/bar/buzz`.
|
||||
// Don't include `/foo/ba` if the final path is `/foo/baring`.
|
||||
function isParentOrEqualArray(base, final) {
|
||||
return base.every((part, i) => part === final[i])
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import path from 'path'
|
||||
import liquid from '../../lib/render-content/liquid.js'
|
||||
import findPageInSiteTree from '../../lib/find-page-in-site-tree.js'
|
||||
import removeFPTFromPath from '../../lib/remove-fpt-from-path.js'
|
||||
|
||||
// This module adds currentProductTree to the context object for use in layouts.
|
||||
export default function currentProductTree(req, res, next) {
|
||||
export default async function currentProductTree(req, res, next) {
|
||||
if (!req.context.page) return next()
|
||||
if (req.context.page.documentType === 'homepage') return next()
|
||||
|
||||
@@ -20,13 +21,68 @@ export default function currentProductTree(req, res, next) {
|
||||
req.context.currentProduct
|
||||
)
|
||||
)
|
||||
const currentProductTree = findPageInSiteTree(
|
||||
req.context.currentProductTree = findPageInSiteTree(
|
||||
currentRootTree,
|
||||
req.context.currentEnglishTree,
|
||||
currentProductPath
|
||||
)
|
||||
|
||||
req.context.currentProductTree = currentProductTree
|
||||
// First make a slim tree of just the 'href', 'title', 'shortTitle'
|
||||
// 'documentType' and 'childPages' (which is recursive).
|
||||
// This gets used for map topic and category pages.
|
||||
req.context.currentProductTreeTitles = await getCurrentProductTreeTitles(
|
||||
req.context.currentProductTree,
|
||||
req.context
|
||||
)
|
||||
// Now make an even slimmer version that excludes all hidden pages.
|
||||
// This is i used for sidebars.
|
||||
req.context.currentProductTreeTitlesExcludeHidden = excludeHidden(
|
||||
req.context.currentProductTreeTitles
|
||||
)
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
// Return a nested object that contains the bits and pieces we need
|
||||
// for the tree which is used for sidebars and listing
|
||||
async function getCurrentProductTreeTitles(input, context) {
|
||||
const { page } = input
|
||||
const childPages = await Promise.all(
|
||||
(input.childPages || []).map((child) => getCurrentProductTreeTitles(child, context))
|
||||
)
|
||||
|
||||
const renderedFullTitle = page.rawTitle.includes('{')
|
||||
? await liquid.parseAndRender(page.rawTitle, context)
|
||||
: page.rawTitle
|
||||
let renderedShortTitle = ''
|
||||
if (page.rawShortTitle) {
|
||||
renderedShortTitle = page.rawShortTitle.includes('{')
|
||||
? await liquid.parseAndRender(page.rawShortTitle, context)
|
||||
: page.rawShortTitle
|
||||
}
|
||||
|
||||
const node = {
|
||||
href: input.href,
|
||||
title: renderedFullTitle,
|
||||
shortTitle:
|
||||
renderedShortTitle && (renderedShortTitle || '') !== renderedFullTitle
|
||||
? renderedShortTitle
|
||||
: '',
|
||||
documentType: page.documentType,
|
||||
childPages: childPages.filter(Boolean),
|
||||
}
|
||||
if (page.hidden) node.hidden = true
|
||||
return node
|
||||
}
|
||||
|
||||
function excludeHidden(tree) {
|
||||
if (tree.hidden) return null
|
||||
const newTree = {
|
||||
href: tree.href,
|
||||
title: tree.title,
|
||||
shortTitle: tree.shortTitle,
|
||||
documentType: tree.documentType,
|
||||
childPages: tree.childPages.map(excludeHidden).filter(Boolean),
|
||||
}
|
||||
return newTree
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import findPageInSiteTree from '../../lib/find-page-in-site-tree.js'
|
||||
import { liquid } from '../../lib/render-content/index.js'
|
||||
|
||||
// This module adds either flatTocItems or nestedTocItems to the context object for
|
||||
// product, categorie, and map topic TOCs that don't have other layouts specified.
|
||||
@@ -47,7 +48,7 @@ export default async function genericToc(req, res, next) {
|
||||
const isEarlyAccess = req.context.currentPath.includes('/early-access/')
|
||||
const isArticlesCategory = req.context.currentPath.endsWith('/articles')
|
||||
|
||||
req.context.showHiddenTocItems =
|
||||
const includeHidden =
|
||||
earlyAccessToc || (isCategoryOrMapTopic && isEarlyAccess && !isArticlesCategory)
|
||||
|
||||
// Conditionally run getTocItems() recursively.
|
||||
@@ -59,49 +60,67 @@ export default async function genericToc(req, res, next) {
|
||||
if (currentTocType === 'flat' && !isOneOffProductToc) {
|
||||
isRecursive = false
|
||||
renderIntros = true
|
||||
req.context.genericTocFlat = await getTocItems(
|
||||
treePage.childPages,
|
||||
req.context,
|
||||
isRecursive,
|
||||
renderIntros
|
||||
)
|
||||
req.context.genericTocFlat = []
|
||||
req.context.genericTocFlat = await getTocItems(treePage, req.context, {
|
||||
recurse: isRecursive,
|
||||
renderIntros,
|
||||
includeHidden,
|
||||
})
|
||||
}
|
||||
|
||||
// Get an array of child map topics and their child articles and add it to the context object.
|
||||
if (currentTocType === 'nested' || isOneOffProductToc) {
|
||||
isRecursive = !isOneOffProductToc
|
||||
renderIntros = false
|
||||
req.context.genericTocNested = await getTocItems(
|
||||
treePage.childPages,
|
||||
req.context,
|
||||
isRecursive,
|
||||
renderIntros
|
||||
)
|
||||
req.context.genericTocNested = await getTocItems(treePage, req.context, {
|
||||
recurse: isRecursive,
|
||||
renderIntros,
|
||||
includeHidden,
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
async function getTocItems(pagesArray, context, isRecursive, renderIntros) {
|
||||
return (
|
||||
await Promise.all(
|
||||
pagesArray.map(async (child) => {
|
||||
// only include a hidden page if showHiddenTocItems is true
|
||||
if (child.page.hidden && !context.showHiddenTocItems) return
|
||||
// Return a nested object that contains the bits and pieces we need
|
||||
// for the tree which is used for sidebars and listing
|
||||
async function getTocItems(node, context, opts) {
|
||||
// Cleaner than trying to be too terse inside the `.filter()` inline callback.
|
||||
function filterHidden(child) {
|
||||
return opts.includeHidden || !child.page.hidden
|
||||
}
|
||||
|
||||
return {
|
||||
title: child.renderedFullTitle,
|
||||
fullPath: child.href,
|
||||
// renderProp is the most expensive part of this function.
|
||||
intro: renderIntros
|
||||
? await child.page.renderProp('intro', context, { unwrap: true })
|
||||
: null,
|
||||
childTocItems:
|
||||
isRecursive && child.childPages
|
||||
? await getTocItems(child.childPages, context, isRecursive, renderIntros)
|
||||
: null,
|
||||
return await Promise.all(
|
||||
node.childPages.filter(filterHidden).map(async (child) => {
|
||||
const { page } = child
|
||||
const title = page.rawTitle.includes('{')
|
||||
? await liquid.parseAndRender(page.rawTitle, context)
|
||||
: page.rawTitle
|
||||
let intro = null
|
||||
if (opts.renderIntros) {
|
||||
intro = ''
|
||||
if (page.rawIntro) {
|
||||
intro = page.rawIntro.includes('{')
|
||||
? await liquid.parseAndRender(page.rawIntro, context)
|
||||
: page.rawIntro
|
||||
}
|
||||
})
|
||||
)
|
||||
).filter(Boolean)
|
||||
}
|
||||
|
||||
let childTocItems = null
|
||||
if (opts.recurse) {
|
||||
childTocItems = []
|
||||
if (child.childPages) {
|
||||
childTocItems.push(...(await getTocItems(child, context, opts)))
|
||||
}
|
||||
}
|
||||
|
||||
const fullPath = child.href
|
||||
return {
|
||||
title,
|
||||
fullPath,
|
||||
intro,
|
||||
childTocItems,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -263,9 +263,9 @@ export default function (app) {
|
||||
app.use(instrument(webhooks, './contextualizers/webhooks'))
|
||||
app.use(asyncMiddleware(instrument(whatsNewChangelog, './contextualizers/whats-new-changelog')))
|
||||
app.use(instrument(layout, './contextualizers/layout'))
|
||||
app.use(instrument(currentProductTree, './contextualizers/current-product-tree'))
|
||||
app.use(asyncMiddleware(instrument(currentProductTree, './contextualizers/current-product-tree')))
|
||||
app.use(asyncMiddleware(instrument(genericToc, './contextualizers/generic-toc')))
|
||||
app.use(asyncMiddleware(instrument(breadcrumbs, './contextualizers/breadcrumbs')))
|
||||
app.use(instrument(breadcrumbs, './contextualizers/breadcrumbs'))
|
||||
app.use(instrument(features, './contextualizers/features'))
|
||||
app.use(asyncMiddleware(instrument(productExamples, './contextualizers/product-examples')))
|
||||
app.use(asyncMiddleware(instrument(productGroups, './contextualizers/product-groups')))
|
||||
|
||||
@@ -41,9 +41,6 @@ describe('siteTree', () => {
|
||||
expect(pageWithDynamicTitle.page.title).toEqual(
|
||||
'Installing {% data variables.product.prodname_enterprise %}'
|
||||
)
|
||||
|
||||
// Confirm a new property contains the rendered title
|
||||
expect(pageWithDynamicTitle.renderedFullTitle).toEqual('Installing GitHub Enterprise')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -173,4 +173,16 @@ describe('sidebar', () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
test("test a page where there's known sidebar short titles that use Liquid and ampersands", async () => {
|
||||
const url =
|
||||
'/en/issues/organizing-your-work-with-project-boards/tracking-work-with-project-boards'
|
||||
const $ = await getDOM(url)
|
||||
const linkTexts = []
|
||||
$('[data-testid=sidebar] a').each((i, element) => {
|
||||
linkTexts.push($(element).text())
|
||||
})
|
||||
// This makes sure that none of the texts in there has their final HTML
|
||||
// to be HTML entity encoded.
|
||||
expect(linkTexts.filter((text) => text.includes('&')).length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user