diff --git a/src/core/database.js b/src/core/database.js index df20012..82edb53 100644 --- a/src/core/database.js +++ b/src/core/database.js @@ -3,20 +3,19 @@ const { getContent } = require('./filesystem') async function generateDatabase (manifest) { const db = [] - const _recursive = async ({ items, ...item }, bc = []) => { + const _recursive = async ({ items, ...item }) => { if (item.input) { db.push({ url: item.url, - breadcrumb: bc, title: item.title, + breadcrumb: item.breadcrumb, content: await getContent(item.input), }) } if (items) { - const breadcrumb = bc.concat(item.title) await Promise.all( - items.map(i => _recursive(i, breadcrumb)) + items.map(i => _recursive(i)) ) } } diff --git a/src/core/hydrate.js b/src/core/hydrate.js index 37d1bb2..9e70f67 100644 --- a/src/core/hydrate.js +++ b/src/core/hydrate.js @@ -140,6 +140,22 @@ async function hydrateTree (tree, config, onRegenerate) { throw new Error(`Duplicated URL was found: ${duplicated.join('\n\t- ')}`) } + // continue the breadcrumb from parent + if (config.breadcrumbs && metaData.breadcrumbs !== false) { + const breadcrumbs = [] + const breadcrumbsParent = itemParent.breadcrumbs || [] + + breadcrumbsParent + .concat({ title: hydratedItem.title, url: hydratedItem.url }) + // only add unique urls to the breadcrumb + .forEach(crumb => + breadcrumbs.findIndex(i => i.url === crumb.url) === -1 && + breadcrumbs.push(crumb) + ) + + hydratedItem.breadcrumbs = breadcrumbs + } + // pull in source items if one exists if (metaData.source) { const source = await walkSource(config.temp, hoistedItem.path, metaData) diff --git a/src/core/output.js b/src/core/output.js index f25152d..bd11ddd 100644 --- a/src/core/output.js +++ b/src/core/output.js @@ -27,6 +27,7 @@ module.exports = async (entrypoints, props) => { await fs.outputFile(outputHtml, template) await fs.outputJson(outputJson, { + title: item.title, content: item.content, }) } diff --git a/src/utils/config.js b/src/utils/config.js index 1514574..d9e0dad 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -28,6 +28,7 @@ const DEFAULT_CONFIG = { languages: ['bash', 'json'], header_links: [], theme: 'default', + breadcrumbs: true, prefix_titles: false, table_of_contents: { page: true, diff --git a/tests/manifest.test.js b/tests/manifest.test.js index 2ec2068..fca32ce 100644 --- a/tests/manifest.test.js +++ b/tests/manifest.test.js @@ -19,6 +19,9 @@ describe('integration: manifest', () => { expect(res.toc.folder[0].title).to.equal('The Foo') expect(res.toc.folder[0].description).to.equal('This is a test') expect(res.toc.folder[0].url).to.equal('/foo/') + expect(res.breadcrumbs).to.have.length(1) + expect(res.breadcrumbs[0].title).to.equal('Mock') + expect(res.breadcrumbs[0].url).to.equal('/') expect(res.items).to.have.length(7) expect(res.items[0].path).to.equal('foo') expect(res.items[0].title).to.equal('The Foo') @@ -26,6 +29,11 @@ describe('integration: manifest', () => { expect(res.items[0].url).to.equal('/foo/') expect(res.items[0].input).to.equal(syspath.resolve(__dirname, 'mock/foo/index.md')) expect(res.items[0].outputDir).to.equal('.gitdocs_build/foo/') + expect(res.items[0].breadcrumbs).to.have.length(2) + expect(res.items[0].breadcrumbs[0].title).to.equal('Mock') + expect(res.items[0].breadcrumbs[0].url).to.equal('/') + expect(res.items[0].breadcrumbs[1].title).to.equal('The Foo') + expect(res.items[0].breadcrumbs[1].url).to.equal('/foo/') expect(res.items[0].items).to.have.length(3) expect(res.items[1].title).to.equal('Garply') expect(res.items[1].items[1].draft).to.be.true() @@ -38,11 +46,17 @@ describe('integration: manifest', () => { expect(res.items[4].url).to.equal('/gitdocs/') expect(res.items[4].input).to.match(/\/@repos\/gitdocs/) expect(res.items[4].outputDir).to.equal('.gitdocs_build/gitdocs/') + expect(res.items[4].breadcrumbs).to.have.length(2) + expect(res.items[4].breadcrumbs[0].title).to.equal('Mock') + expect(res.items[4].breadcrumbs[0].url).to.equal('/') + expect(res.items[4].breadcrumbs[1].title).to.equal('GitDocs') + expect(res.items[4].breadcrumbs[1].url).to.equal('/gitdocs/') expect(res.items[4].items).to.have.length(1) expect(res.items[4].items[0].path).to.equal('externals.md') expect(res.items[4].items[0].title).to.equal('Externals') expect(res.items[4].items[0].url).to.equal('/gitdocs/externals/') expect(res.items[5].component).to.equal('Divider') expect(res.items[6].title).to.equal('The Quux') + expect(res.items[6].items[1].breadcrumbs).to.be.undefined() }) }) diff --git a/tests/mock/qux/corge.md b/tests/mock/qux/corge.md index 75309bf..5f4f05b 100644 --- a/tests/mock/qux/corge.md +++ b/tests/mock/qux/corge.md @@ -1 +1,4 @@ +--- +breadcrumbs: false +--- # The Corge diff --git a/themes/default/breadcrumbs/index.js b/themes/default/breadcrumbs/index.js new file mode 100644 index 0000000..a51bc03 --- /dev/null +++ b/themes/default/breadcrumbs/index.js @@ -0,0 +1,36 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { + Wrapper, + CrumbWrapper, + Crumb, + Seperator, +} from './styles' + +const Breadcrumbs = (props) => { + // don't show breadcrumbs if there is only one item + if (props.items.length < 2) { + return
+ } + + return ( + + {props.items.map((item, i) => item.url && ( + + {i > 0 && } + {item.title} + + ))} + + ) +} + +Breadcrumbs.propTypes = { + items: PropTypes.array, +} + +Breadcrumbs.defaultProps = { + items: [], +} + +export default Breadcrumbs diff --git a/themes/default/breadcrumbs/styles.js b/themes/default/breadcrumbs/styles.js new file mode 100644 index 0000000..c27eea2 --- /dev/null +++ b/themes/default/breadcrumbs/styles.js @@ -0,0 +1,31 @@ +import styled from 'react-emotion' +import { Link } from 'react-router-dom' +import { ChevronRight } from 'react-feather' + +export const Wrapper = styled('nav')` + margin-bottom: 20px; +` + +export const CrumbWrapper = styled('div')` + display: inline-block; +` + +export const Crumb = styled(Link)` + color: #848B8E; + font-weight: 600; + font-size: 1rem; + text-decoration: none; + opacity: .5; + transition: opacity .1s; + &:hover { + opacity: 1; + } +` + +export const Seperator = styled(ChevronRight)` + display: inline-block; + opacity: .2; + padding: 0 5px; + position: relative; + top: 2px; +` diff --git a/themes/default/page/index.js b/themes/default/page/index.js index e4984c7..a24b074 100644 --- a/themes/default/page/index.js +++ b/themes/default/page/index.js @@ -2,6 +2,7 @@ import React, { Component } from 'react' import Helmet from 'react-helmet' import axios from 'axios' import Markdown from '../markdown' +import Breadcrumbs from '../breadcrumbs' import Loading from '../loading' import TocPage from '../toc/page' import TocFolder from '../toc/folder' @@ -90,6 +91,9 @@ export default class Page extends Component { {route.title} + {route.breadcrumbs && + } + {loading ? : ( diff --git a/themes/default/search/index.js b/themes/default/search/index.js index b8e24ac..4c7f198 100644 --- a/themes/default/search/index.js +++ b/themes/default/search/index.js @@ -151,25 +151,21 @@ class Search extends Component { } renderBreadCrumb (result) { - return result - .breadcrumb + return result.breadcrumb .slice(1, result.breadcrumb.length) - .concat(result.title) - .map((b, i) => ( - - { - i !== 0 && - - } - {b} + .map(({ title }, i) => ( + + {i !== 0 && } + + {title} )) } @@ -181,7 +177,7 @@ class Search extends Component { // Map over search results and create links const items = results.map((r, i) => i === selectedIndex ? this.activeItem = ref : null} onClick={this.clearSearch}