UNPKG

gitdocs

Version:

Easy to use, SEO-friendly, beautiful documentation that lives in your git repo.

259 lines (218 loc) 7.42 kB
const syspath = require('path') const markdownToc = require('markdown-toc') const ourpath = require('../utils/path') const { getFrontmatterOnly } = require('../utils/frontmatter') const { mergeLeftByKey } = require('../utils/merge') const { getContent } = require('./filesystem') const { walkSource } = require('./source') const Sitemap = require('./sitemap') async function getMetaData (item, parentItems) { const data = item.type === 'file' ? await getFrontmatterOnly(item.path) : {} // @TODO warn about conflicting items in parent and self // inherit front matter from the parent sidebar config const dataFromParent = parentItems .find(i => i.path === item.path_relative) return { ...data, ...dataFromParent } } function normalizeItems (data) { const itemsMessy = data.items || data.items_prepend || data.items_append || [] const items = itemsMessy.map(item => { if (typeof item !== 'string' && typeof item !== 'object') { throw new TypeError(`Each item must be a string or object, got ${typeof item}`) } return typeof item === 'string' ? { path: item } : item }) const hasProp = Object.prototype.hasOwnProperty const strategy = hasProp.call(data, 'items') ? 'replace' : hasProp.call(data, 'items_prepend') ? 'prepend' : hasProp.call(data, 'items_append') ? 'append' : null return { items, strategy, } } async function tableOfContents (toc, { input, items }) { // only add items that have a file associated with it if (input) { if (toc.page) { toc.page = markdownToc(await getContent(input)) .json.filter(i => i.lvl <= 2) } if (toc.folder) { toc.folder = items // only want children items that have an input .filter(item => item.input) // reduced data, since we don't need everything .map(item => ({ title: item.title, description: item.description, url: item.url, })) } } // dont keep empty arrays if (!toc.page || !toc.page.length) delete toc.page if (!toc.folder || !toc.folder.length) delete toc.folder return toc } async function hydrateTree (tree, config, opts = {}) { const sitemap = new Sitemap() if (tree.childrenIndex === undefined) { throw new Error('No index file was found! Create a `readme.md` at the root of your project.') } const _recursive = async ( item, itemParent = {}, itemParentItems = [], ) => { const { path_relative, childrenIndex, children = [], } = item // hoist the index file and use it instead of the current item, // if there is an index file under it's children const hoistedItem = childrenIndex !== undefined ? item.children[childrenIndex] : item // extract front matter from file while inheriting data from parent const metaData = await getMetaData(hoistedItem, itemParentItems) // start hydrating the current item const hydratedItem = { path: path_relative, title: metaData.title || ( itemParent.path !== undefined // convert the file path into the title ? ourpath.titlify(hoistedItem.path) // use the project name as the title if we are at the root : config.name ), url: ourpath.routify( syspath.resolve( '/', // don't resolve from the cwd itemParent.url || itemParent.path || config.baseURL, metaData.url || path_relative, ) ), } if (metaData.draft && !opts.includeDrafts) { return } // add these items from metadata, but only if not undefined if (metaData.hidden) hydratedItem.hidden = true if (metaData.description) hydratedItem.description = metaData.description if (metaData.tags) hydratedItem.tags = metaData.tags.split(',').map(i => i.trim()) if (metaData.related) hydratedItem.related = metaData.related.map(url => ({ title: ourpath.titlify(url), url })) // continue the breadcrumb from parent if (config.breadcrumbs && metaData.breadcrumbs !== false) { const breadcrumb = { title: hydratedItem.title } const breadcrumbs = [] const breadcrumbsParent = itemParent.breadcrumbs || [] if (hoistedItem.type === 'file') { breadcrumb.url = hydratedItem.url } breadcrumbsParent .concat(breadcrumb) // only add unique urls to the breadcrumb .forEach(crumb => breadcrumbs.findIndex(i => i.url === crumb.url) === -1 && breadcrumbs.push(crumb) ) hydratedItem.breadcrumbs = breadcrumbs } // only files should have an input and output value if (hoistedItem.type === 'file') { hydratedItem.input = hoistedItem.path hydratedItem.outputDir = syspath.join(config.output, hydratedItem.url) // pull in source items if one exists if (metaData.source) { const source = await walkSource(config.temp, hoistedItem.path, metaData) const sourceHydrated = await _recursive(source, hydratedItem) // don't inherit these items from the source delete sourceHydrated.path delete sourceHydrated.title // replace current item data with the source data Object.assign(hydratedItem, sourceHydrated) // don't register the url when there is a source (since item gets replaced) } else { // add url to the sitemap sitemap.addUrl(`${config.domain}${hydratedItem.url}`, { ...metaData.sitemap, filename: hoistedItem.path, }) } } // get sub items from the front matter const { items: metaDataItems, strategy: mergeStrategy, } = normalizeItems(metaData) // recurse sub items from the dir tree const childrenItemsUnsorted = await Promise.all( children .filter(({ index }) => !index) .map(childItem => _recursive(childItem, hydratedItem, metaDataItems)) ) // sort alphabetically by default const childrenSorted = childrenItemsUnsorted .filter(Boolean) .sort((a, b) => a.title.localeCompare(b.title)) // @TODO: figure out how to remove items_pre/append from the result const mergedItems = mergeLeftByKey(metaDataItems, childrenSorted, { key: 'path', name: hoistedItem.path, strategy: mergeStrategy, }) hydratedItem.items = [ ...mergedItems || [], ...hydratedItem.items || [], ] // don't keep an empty items array if (!hydratedItem.items.length) { delete hydratedItem.items.length } // add table of contents, if applicable hydratedItem.toc = await tableOfContents( Object.assign({}, config.table_of_contents, metaData.table_of_contents), hydratedItem, ) return hydratedItem } return { manifest: await _recursive(tree), sitemap: sitemap.generate(), } } // async function hydrateContent (manifest) { // const _recursive = async (item) => { // if (item.input) { // item.content = await getContent(item.input) // } // // if (item.items) { // item.items = await Promise.all( // item.items.map(i => _recursive(i)) // ) // } // // return item // } // // return _recursive(manifest) // } module.exports = { getMetaData, normalizeItems, tableOfContents, hydrateTree, // hydrateContent, }