From f226db9cc48a7334378d69f7aaa75e6c4b774d1a Mon Sep 17 00:00:00 2001 From: Jason Maurer Date: Fri, 1 Jun 2018 15:40:16 -0400 Subject: [PATCH] add folder level toc & update toc config --- src/core/filesystem.js | 6 --- src/core/hydrate.js | 41 ++++++++++++++++ src/core/output.js | 3 +- src/core/socket.js | 3 +- src/utils/config.js | 5 +- tests/manifest.test.js | 12 +++++ tests/mock/foo/index.md | 1 + tests/mock/readme.md | 1 + themes/default/page/index.js | 90 +++++++++++------------------------ themes/default/page/styles.js | 45 ------------------ themes/default/toc/folder.js | 29 +++++++++++ themes/default/toc/page.js | 40 ++++++++++++++++ themes/default/toc/styles.js | 85 +++++++++++++++++++++++++++++++++ 13 files changed, 242 insertions(+), 119 deletions(-) create mode 100644 themes/default/toc/folder.js create mode 100644 themes/default/toc/page.js create mode 100644 themes/default/toc/styles.js diff --git a/src/core/filesystem.js b/src/core/filesystem.js index a974e20..1dcde4e 100644 --- a/src/core/filesystem.js +++ b/src/core/filesystem.js @@ -1,7 +1,6 @@ const fs = require('fs-extra') const syspath = require('path') const { ncp } = require('ncp') -const toc = require('markdown-toc') const { parseFrontmatter } = require('../utils/frontmatter') const INDEX_FILES = ['index', 'readme'] @@ -156,10 +155,6 @@ async function getContent (path) { return content } -function getTableOfContents (content) { - return toc(content).json -} - /** * because of https://github.com/zeit/pkg/issues/420 */ @@ -177,7 +172,6 @@ module.exports = { checkForConflicts, dirTree, getContent, - getTableOfContents, copyDir, } diff --git a/src/core/hydrate.js b/src/core/hydrate.js index 921bc2f..3d01068 100644 --- a/src/core/hydrate.js +++ b/src/core/hydrate.js @@ -1,8 +1,10 @@ const syspath = require('path') // const chokidar = require('chokidar') +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') @@ -48,6 +50,34 @@ function normalizeItems (data) { } } +async function tableOfContents ({ toc, input, items }) { + // only add items that have a file associated with it + if (input) { + if (toc.page) { + const content = await getContent(input) + toc.page = markdownToc(content).json + } + + 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, onRegenerate) { const urls = {} const sitemap = new Sitemap() @@ -80,6 +110,8 @@ async function hydrateTree (tree, config, onRegenerate) { const hydratedItem = { path: path_relative, draft: metaData.draft || false, + description: metaData.description || '', + toc: Object.assign({}, config.table_of_contents, metaData.table_of_contents), title: metaData.title || (itemParent.path !== undefined // convert the file path into the title ? ourpath.titlify(hoistedItem.path) @@ -157,6 +189,14 @@ async function hydrateTree (tree, config, onRegenerate) { ...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(hydratedItem) + return hydratedItem } @@ -195,6 +235,7 @@ async function hydrateTree (tree, config, onRegenerate) { module.exports = { getMetaData, normalizeItems, + tableOfContents, hydrateTree, // hydrateContent, } diff --git a/src/core/output.js b/src/core/output.js index a2e33f5..f25152d 100644 --- a/src/core/output.js +++ b/src/core/output.js @@ -3,7 +3,7 @@ const syspath = require('path') const { warn } = require('../utils/emit') const { generateDatabase } = require('./database') const { templateForProduction } = require('./template') -const { getContent, getTableOfContents } = require('./filesystem') +const { getContent } = require('./filesystem') module.exports = async (entrypoints, props) => { const outputDB = syspath.join(props.config.output, 'db.json') @@ -28,7 +28,6 @@ module.exports = async (entrypoints, props) => { await fs.outputFile(outputHtml, template) await fs.outputJson(outputJson, { content: item.content, - toc: getTableOfContents(item.content), }) } diff --git a/src/core/socket.js b/src/core/socket.js index 1260481..e8e2ada 100644 --- a/src/core/socket.js +++ b/src/core/socket.js @@ -1,6 +1,6 @@ const fs = require('fs') const WebSocket = require('ws') -const { getContent, getTableOfContents } = require('./filesystem') +const { getContent } = require('./filesystem') module.exports = (server) => { const socket = new WebSocket.Server({ @@ -15,7 +15,6 @@ module.exports = (server) => { const content = await getContent(file) client.send(JSON.stringify({ content, - toc: getTableOfContents(content), })) } diff --git a/src/utils/config.js b/src/utils/config.js index 9516f7f..a7517f6 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -29,7 +29,10 @@ const DEFAULT_CONFIG = { header_links: [], theme: 'default', prefix_titles: true, - table_of_contents: true, + table_of_contents: { + page: true, + folder: true, + }, syntax: { theme: 'atom-one-light', renderer: 'hljs', diff --git a/tests/manifest.test.js b/tests/manifest.test.js index 1b11fcf..fa3e2ec 100644 --- a/tests/manifest.test.js +++ b/tests/manifest.test.js @@ -14,13 +14,23 @@ describe('integration: manifest', () => { expect(res.url).to.equal('/') expect(res.input).to.equal(syspath.resolve(__dirname, 'mock/readme.md')) expect(res.outputDir).to.equal('.gitdocs_build/') + expect(res.toc.page).to.have.length(1) + expect(res.toc.folder).to.have.length(5) + 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.items).to.have.length(7) expect(res.items[0].path).to.equal('foo') expect(res.items[0].draft).to.be.false() expect(res.items[0].title).to.equal('The Foo') + expect(res.items[0].description).to.equal('This is a test') 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].items).to.have.length(3) + expect(res.items[1].title).to.equal('Garply') + expect(res.items[2].title).to.equal('XYZZY') + expect(res.items[3].title).to.equal('Thud') expect(res.items[4].path).to.equal('external.md') expect(res.items[4].title).to.equal('GitDocs') expect(res.items[4].url).to.equal('/gitdocs/') @@ -30,5 +40,7 @@ describe('integration: manifest', () => { 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') }) }) diff --git a/tests/mock/foo/index.md b/tests/mock/foo/index.md index 2b2202e..cb65632 100644 --- a/tests/mock/foo/index.md +++ b/tests/mock/foo/index.md @@ -1,5 +1,6 @@ --- title: The Foo +description: This is a test items_append: - component: Divider - path: bar.md diff --git a/tests/mock/readme.md b/tests/mock/readme.md index 994163d..bb210b5 100644 --- a/tests/mock/readme.md +++ b/tests/mock/readme.md @@ -5,6 +5,7 @@ items: - xyzzy - thud.md - external.md + - component: Divider - path: qux title: The Quux items_prepend: diff --git a/themes/default/page/index.js b/themes/default/page/index.js index 5447767..97ea3f3 100644 --- a/themes/default/page/index.js +++ b/themes/default/page/index.js @@ -3,35 +3,10 @@ import Helmet from 'react-helmet' import axios from 'axios' import Markdown from '../markdown' import Loading from '../loading' +import TocPage from '../toc/page' +import TocFolder from '../toc/folder' import { ConfigContext } from '../context' -import { Wrapper, ContentWrapper, TOC } from './styles' - -const TableOfContents = ({ toc, sticky }) => { - // Don't show this if there aren't enough headers - if (!toc) return null - if (toc.length < 2) return null - - // Create TOC hierarchy and link to headers - const items = toc.map(t => ( -
  • - - {t.content} - -
  • - )) - - return ( - - - - ) -} +import { Wrapper, ContentWrapper } from './styles' const Content = ({ content, config, route }) => { const defaultContent = '##### _You don\'t have any content here yet!_' @@ -60,7 +35,6 @@ export default class Page extends Component { this.state = { loading: !props.route.content, content: props.route.content, - toc: props.route.toc, } } @@ -74,22 +48,15 @@ export default class Page extends Component { }) this._socket.addEventListener('message', evt => { - const { content, toc } = JSON.parse(evt.data) - this.setState({ - content, - toc, - loading: false, - }) + const { content } = JSON.parse(evt.data) + this.setState({ content, loading: false }) }) } else if (!this.state.content) { try { - const { data } = await axios.get('index.json') - - this.setState({ - content: data.content, - toc: data.toc, - loading: false, - }) + const { + data: { content }, + } = await axios.get('index.json') + this.setState({ content, loading: false }) } catch (err) { console.error(`Could not get page content: ${err}`) } @@ -111,7 +78,6 @@ export default class Page extends Component { const { loading, content, - toc, } = this.state return ( @@ -121,26 +87,24 @@ export default class Page extends Component { {route.title} - { - loading && - - } - { - !loading && - - } - { - !loading && - config.table_of_contents && - - } + + {loading + ? + : ( +
    + + + {route.toc.page && + } + + {route.toc.folder && + } +
    + )} } diff --git a/themes/default/page/styles.js b/themes/default/page/styles.js index 15c9d1d..dabe675 100644 --- a/themes/default/page/styles.js +++ b/themes/default/page/styles.js @@ -20,51 +20,6 @@ export const Wrapper = styled('div')` } ` -export const TOC = styled('nav')` - margin-left: 2rem; - width: 150px; - max-width: 150px; - min-width: 150px; - flex-grow: 0; - - @media (min-width: 1200px) { - ul { - position: ${props => props.sticky ? 'fixed' : 'initial'}; - top: 30px; - } - } - - ul { - list-style: none; - border-left: 1px solid #E6E9EB; - padding-left: 2rem; - } - - h5 { - color: #848B8E; - margin: 0; - } - - li { - font-size: 13px; - line-height: 30px; - display: block; - } - - a { - text-decoration: none; - color: #626469; - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - &:hover { - color: #5742C7; - } - } -` - export const ContentWrapper = styled('div')` padding: 0 50px; diff --git a/themes/default/toc/folder.js b/themes/default/toc/folder.js new file mode 100644 index 0000000..d68af91 --- /dev/null +++ b/themes/default/toc/folder.js @@ -0,0 +1,29 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Wrapper, FolderItem } from './styles' + +const Toc = (props) => { + return ( + + {props.items.map(item => ( + + {item.title} + {item.description} + + ))} + + ) +} + +Toc.defaultProps = { + items: [], +} + +Toc.propTypes = { + items: PropTypes.array, +} + +export default Toc diff --git a/themes/default/toc/page.js b/themes/default/toc/page.js new file mode 100644 index 0000000..7aec231 --- /dev/null +++ b/themes/default/toc/page.js @@ -0,0 +1,40 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { PageItem } from './styles' + +const Toc = (props) => { + // Don't show this if there aren't enough headers + if (!props.items) return null + if (props.items.length < 2) return null + + // Create TOC hierarchy and link to headers + const items = props.items.map(t => ( +
  • + + {t.content} + +
  • + )) + + return ( + +
      +
      Contents
      + {items} +
    +
    + ) +} + +Toc.defaultProps = { + items: [], +} + +Toc.propTypes = { + items: PropTypes.array, +} + +export default Toc diff --git a/themes/default/toc/styles.js b/themes/default/toc/styles.js new file mode 100644 index 0000000..6e1457f --- /dev/null +++ b/themes/default/toc/styles.js @@ -0,0 +1,85 @@ +import styled from 'react-emotion' +import { Link } from 'react-router-dom' + +export const Wrapper = styled('nav')` + border-top: 1px solid #E6E9EB; + margin-top: 60px; + padding: 60px; +` + +export const PageItem = styled('nav')` + margin-left: 2rem; + width: 150px; + max-width: 150px; + min-width: 150px; + flex-grow: 0; + + @media (min-width: 1200px) { + ul { + position: ${props => props.sticky ? 'fixed' : 'initial'}; + top: 30px; + right: 30px; + } + } + + ul { + list-style: none; + border-left: 1px solid #E6E9EB; + padding-left: 2rem; + } + + h5 { + color: #848B8E; + margin: 0; + } + + li { + font-size: 13px; + line-height: 30px; + display: block; + } + + a { + text-decoration: none; + color: #626469; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:hover { + color: #5742C7; + } + } +` + +export const FolderItem = styled(Link)` + display: inline-block; + width: 230px; + margin-right: 20px; + margin-bottom: 50px; + vertical-align: top; + text-decoration: none; + color: #4c555a; + position: relative; + font-size: .9rem; + b { + display: block; + font-weight: 600; + font-size: 1rem; + color: #0d2b3e; + } + &:hover { + opacity: .5; + } + &:before { + content: ""; + width: 50px; + height: 3px; + background: #6457DF; + position: absolute; + top: -10px; + left: 0; + opacity: .3; + } +`