feat: implement table of contents

This commit is contained in:
Jean-Philippe Sirois
2020-05-12 00:05:17 -04:00
parent 465d026392
commit b17527cd6c
12 changed files with 194 additions and 14 deletions
+6
View File
@@ -71,6 +71,12 @@ module.exports = {
},
},
`gatsby-transformer-sharp`,
{
resolve: "gatsby-plugin-anchor-links",
options: {
offset: -100,
},
},
`gatsby-plugin-react-svg`,
`gatsby-plugin-remove-trailing-slashes`,
`gatsby-plugin-sharp`,
+24
View File
@@ -9627,6 +9627,14 @@
"micromatch": "^3.1.10"
}
},
"gatsby-plugin-anchor-links": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/gatsby-plugin-anchor-links/-/gatsby-plugin-anchor-links-1.1.1.tgz",
"integrity": "sha512-mgSHUAEa7RRWD/lcmJVUWD9mIT14EIJvMPcxJ1W2Ev+rNuqBBpk1j4vDWy1uxas+qusi0vrtZ4HdU7mse12qDw==",
"requires": {
"scroll-to-element": "^2.0.3"
}
},
"gatsby-plugin-layout": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/gatsby-plugin-layout/-/gatsby-plugin-layout-1.3.1.tgz",
@@ -16734,6 +16742,14 @@
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz",
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA=="
},
"raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"requires": {
"performance-now": "^2.1.0"
}
},
"ramda": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz",
@@ -18233,6 +18249,14 @@
"invariant": "^2.2.4"
}
},
"scroll-to-element": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/scroll-to-element/-/scroll-to-element-2.0.3.tgz",
"integrity": "sha512-5herPcm9jMfQgRwu94lH5mei+2YhipR4RQ2nAvnBxJb2tG+P7O0ctOKAaAZBXbBejnn+MImh3wrAUA5EcLnjEQ==",
"requires": {
"raf": "^3.4.0"
}
},
"scuid": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz",
+1
View File
@@ -31,6 +31,7 @@
"@reach/router": "^1.3.3",
"classnames": "^2.2.6",
"gatsby": "^2.21.21",
"gatsby-plugin-anchor-links": "^1.1.1",
"gatsby-plugin-layout": "^1.3.1",
"gatsby-plugin-manifest": "^2.4.2",
"gatsby-plugin-prefetch-google-fonts": "^1.4.3",
+12 -1
View File
@@ -1,11 +1,14 @@
import React, { FC, Fragment } from "react"
import { ITocItem } from "../../types"
import { humanize } from "../../utils"
import * as SC from "./styles"
import { Container } from "../Container"
import { Toc } from "../Toc"
import * as SC from "./styles"
interface IPageContentProps {
content: JSX.Element
toc?: ITocItem[]
recommendedReading?: string[]
externalResources?: string[]
}
@@ -42,6 +45,7 @@ function ExtraLink({
export const PageContent: FC<IPageContentProps> = ({
content,
toc = [],
recommendedReading,
externalResources,
}) => {
@@ -52,6 +56,13 @@ export const PageContent: FC<IPageContentProps> = ({
</SC.Content>
<SC.Sidebar>
{toc.length > 0 && (
<Toc
header={<SC.SidebarHeader>Table of Contents</SC.SidebarHeader>}
items={toc}
/>
)}
{recommendedReading && (
<SC.RecommendedReading>
<SC.SidebarHeader>Recommended reading</SC.SidebarHeader>
+7 -1
View File
@@ -21,6 +21,8 @@ export const Sidebar = styled.div`
color: ${props => (props.theme.name === "dark" ? "#f9f9f9" : "#172129")};
margin: 64px 0;
padding: 0 32px;
position: sticky;
top: 32px;
&:empty {
display: none;
@@ -50,7 +52,11 @@ export const SidebarHeader = styled.div`
`
const ExtraLinks = styled.div`
margin-top: 16px;
margin-top: 32px;
&:first-child {
margin-top: 0;
}
`
export const RecommendedReading = styled(ExtraLinks)``
+42
View File
@@ -0,0 +1,42 @@
import React, { FC } from "react"
import { ITocItem } from "../../types"
import * as SC from "./styles"
interface ITocProps {
header: React.ReactNode
items: ITocItem[]
}
interface ITitle {
prefix?: string
title: string
}
function extractTitle(title: string): ITitle {
const maybePrefix = title.match(/^(\w+\.)/)
const rest = maybePrefix ? title.replace(maybePrefix[0], "") : title
return {
prefix: maybePrefix ? maybePrefix[0] : undefined,
title: rest,
}
}
export const Toc: FC<ITocProps> = ({ header, items }) => {
const { pathname } = window.location
return (
<SC.TocWrapper>
{header}
{items.map(item => {
const { prefix, title } = extractTitle(item.title)
return (
<SC.TocItem key={item.link} className={`depth-${item.depth}`}>
{prefix}
<SC.TocLink to={`${pathname}${item.link}`}>{title}</SC.TocLink>
</SC.TocItem>
)
})}
</SC.TocWrapper>
)
}
+30
View File
@@ -0,0 +1,30 @@
import { AnchorLink } from "gatsby-plugin-anchor-links"
import styled from "styled-components"
export const TocWrapper = styled.div`
display: flex;
flex-direction: column;
`
export const TocItem = styled.div`
display: inline-block;
&.depth-3 {
margin-left: 8px;
}
&.depth-2 + &.depth-2,
&.depth-3 + &.depth-2 {
margin-top: 8px;
}
`
export const TocLink = styled(AnchorLink)`
color: ${props => props.theme.main.foreground};
text-decoration: none;
&:hover,
&:focus {
text-decoration: underline;
}
`
+13 -2
View File
@@ -1,21 +1,28 @@
import { graphql } from "gatsby"
import React, { Fragment } from "react"
import cx from "classnames"
import { MarkdownRemark } from "../../generated/graphql"
import { ComponentQuery } from "../../typings"
import { Markdown } from "../components/Markdown"
import { SEO } from "../components/SEO"
import { PageContent } from "../components/PageContent"
import { HeaderBarebone } from "../components/HeaderBarebone"
import { buildToc } from "../utils"
function AboutPage({ data }: ComponentQuery<{ md: MarkdownRemark }>) {
const { md } = data
const toc = buildToc(md.headings!)
return (
<Fragment>
<SEO title="About" />
<HeaderBarebone title="About us" />
<HeaderBarebone
title="About us"
className={cx({ shifted: toc.length })}
/>
<PageContent content={<Markdown content={md.html!} />} />
<PageContent content={<Markdown content={md.html!} />} toc={toc} />
</Fragment>
)
}
@@ -24,6 +31,10 @@ export const query = graphql`
query AboutPage {
md: markdownRemark(frontmatter: { path: { eq: "/about" } }) {
html
headings {
depth
value
}
}
}
`
+10 -2
View File
@@ -1,21 +1,25 @@
import { graphql } from "gatsby"
import React, { Fragment } from "react"
import cx from "classnames"
import { MarkdownRemark } from "../../generated/graphql"
import { ComponentQuery } from "../../typings"
import { HeaderBarebone } from "../components/HeaderBarebone"
import { Markdown } from "../components/Markdown"
import { PageContent } from "../components/PageContent"
import { SEO } from "../components/SEO"
import { buildToc } from "../utils"
function RulesPage({ data }: ComponentQuery<{ md: MarkdownRemark }>) {
const { md } = data
const toc = buildToc(md.headings!)
return (
<Fragment>
<SEO title="Rules" />
<HeaderBarebone title="Rules" />
<HeaderBarebone title="Rules" className={cx({ shifted: toc.length })} />
<PageContent content={<Markdown content={md.html!} />} />
<PageContent content={<Markdown content={md.html!} />} toc={toc} />
</Fragment>
)
}
@@ -24,6 +28,10 @@ export const query = graphql`
query RulesPage {
md: markdownRemark(frontmatter: { path: { eq: "/rules" } }) {
html
headings {
depth
value
}
}
}
`
+23 -6
View File
@@ -1,20 +1,32 @@
import { graphql } from "gatsby"
import React, { FC, Fragment } from "react"
import "katex/dist/katex.min.css"
import { Footer } from "../components/Footer"
import { Header } from "../components/Header"
import { Markdown } from "../components/Markdown"
import { SEO } from "../components/SEO"
import "katex/dist/katex.min.css"
import { PageContent } from "../components/PageContent"
import { Footer } from "../components/Footer"
import { SEO } from "../components/SEO"
import { buildToc } from "../utils"
// @todo maybe find alternative type for data
const LanguagePost: FC<any> = ({ data }) => {
const { relativePath } = data.file
const { html, excerpt, fields, frontmatter, timeToRead } = data.file.post
const {
html,
headings,
excerpt,
fields,
frontmatter,
timeToRead,
} = data.file.post
const toc = buildToc(headings)
const shiftLayout = Boolean(
frontmatter.recommended_reading || frontmatter.external_resources
frontmatter.recommended_reading ||
frontmatter.external_resources ||
toc.length
)
return (
@@ -36,6 +48,7 @@ const LanguagePost: FC<any> = ({ data }) => {
<Footer />
</>
}
toc={toc}
recommendedReading={frontmatter.recommended_reading}
externalResources={frontmatter.external_resources}
/>
@@ -51,6 +64,10 @@ export const query = graphql`
relativePath
post: childMarkdownRemark {
html
headings {
depth
value
}
excerpt
fields {
authors {
+6
View File
@@ -55,3 +55,9 @@ export interface IAllArchivesQuery extends IAllFilesQuery {
edges: IFileArchiveQuery[]
}
}
export interface ITocItem {
depth: number
link: string
title: string
}
+20 -2
View File
@@ -1,9 +1,11 @@
import kebabCase from "lodash/kebabCase"
import chain from "ramda/es/chain"
import partition from "ramda/es/partition"
import pipe from "ramda/es/pipe"
import sort from "ramda/es/sort"
import { sort } from "ramda"
import { IFile, IFileOrFolder, IFolder } from "../types"
import { IFile, IFileOrFolder, IFolder, ITocItem } from "../types"
import { MarkdownRemark } from "../../generated/graphql"
function specificWordsToUpper(str: string): string {
const wordsToUpper = ["pdo", "c"]
@@ -139,3 +141,19 @@ export function join([head, ...tail]: IFileOrFolder[]): IFileOrFolder[] {
return [current, ...join(remaining)]
}
export function buildToc(headings: MarkdownRemark["headings"]): ITocItem[] {
if (!headings) {
return []
}
return headings
.filter(h => h?.depth === 2)
.map(h => {
return {
depth: h!.depth!,
link: `#${kebabCase(h!.value!)}`,
title: h!.value!,
}
})
}