mirror of
https://github.com/Stone-Red-Code/website.git
synced 2026-09-04 09:15:58 +02:00
feat: implement table of contents
This commit is contained in:
@@ -71,6 +71,12 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
`gatsby-transformer-sharp`,
|
`gatsby-transformer-sharp`,
|
||||||
|
{
|
||||||
|
resolve: "gatsby-plugin-anchor-links",
|
||||||
|
options: {
|
||||||
|
offset: -100,
|
||||||
|
},
|
||||||
|
},
|
||||||
`gatsby-plugin-react-svg`,
|
`gatsby-plugin-react-svg`,
|
||||||
`gatsby-plugin-remove-trailing-slashes`,
|
`gatsby-plugin-remove-trailing-slashes`,
|
||||||
`gatsby-plugin-sharp`,
|
`gatsby-plugin-sharp`,
|
||||||
|
|||||||
Generated
+24
@@ -9627,6 +9627,14 @@
|
|||||||
"micromatch": "^3.1.10"
|
"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": {
|
"gatsby-plugin-layout": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/gatsby-plugin-layout/-/gatsby-plugin-layout-1.3.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz",
|
||||||
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA=="
|
"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": {
|
"ramda": {
|
||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz",
|
||||||
@@ -18233,6 +18249,14 @@
|
|||||||
"invariant": "^2.2.4"
|
"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": {
|
"scuid": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"@reach/router": "^1.3.3",
|
"@reach/router": "^1.3.3",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"gatsby": "^2.21.21",
|
"gatsby": "^2.21.21",
|
||||||
|
"gatsby-plugin-anchor-links": "^1.1.1",
|
||||||
"gatsby-plugin-layout": "^1.3.1",
|
"gatsby-plugin-layout": "^1.3.1",
|
||||||
"gatsby-plugin-manifest": "^2.4.2",
|
"gatsby-plugin-manifest": "^2.4.2",
|
||||||
"gatsby-plugin-prefetch-google-fonts": "^1.4.3",
|
"gatsby-plugin-prefetch-google-fonts": "^1.4.3",
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import React, { FC, Fragment } from "react"
|
import React, { FC, Fragment } from "react"
|
||||||
|
|
||||||
|
import { ITocItem } from "../../types"
|
||||||
import { humanize } from "../../utils"
|
import { humanize } from "../../utils"
|
||||||
import * as SC from "./styles"
|
|
||||||
import { Container } from "../Container"
|
import { Container } from "../Container"
|
||||||
|
import { Toc } from "../Toc"
|
||||||
|
import * as SC from "./styles"
|
||||||
|
|
||||||
interface IPageContentProps {
|
interface IPageContentProps {
|
||||||
content: JSX.Element
|
content: JSX.Element
|
||||||
|
toc?: ITocItem[]
|
||||||
recommendedReading?: string[]
|
recommendedReading?: string[]
|
||||||
externalResources?: string[]
|
externalResources?: string[]
|
||||||
}
|
}
|
||||||
@@ -42,6 +45,7 @@ function ExtraLink({
|
|||||||
|
|
||||||
export const PageContent: FC<IPageContentProps> = ({
|
export const PageContent: FC<IPageContentProps> = ({
|
||||||
content,
|
content,
|
||||||
|
toc = [],
|
||||||
recommendedReading,
|
recommendedReading,
|
||||||
externalResources,
|
externalResources,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -52,6 +56,13 @@ export const PageContent: FC<IPageContentProps> = ({
|
|||||||
</SC.Content>
|
</SC.Content>
|
||||||
|
|
||||||
<SC.Sidebar>
|
<SC.Sidebar>
|
||||||
|
{toc.length > 0 && (
|
||||||
|
<Toc
|
||||||
|
header={<SC.SidebarHeader>Table of Contents</SC.SidebarHeader>}
|
||||||
|
items={toc}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{recommendedReading && (
|
{recommendedReading && (
|
||||||
<SC.RecommendedReading>
|
<SC.RecommendedReading>
|
||||||
<SC.SidebarHeader>Recommended reading</SC.SidebarHeader>
|
<SC.SidebarHeader>Recommended reading</SC.SidebarHeader>
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export const Sidebar = styled.div`
|
|||||||
color: ${props => (props.theme.name === "dark" ? "#f9f9f9" : "#172129")};
|
color: ${props => (props.theme.name === "dark" ? "#f9f9f9" : "#172129")};
|
||||||
margin: 64px 0;
|
margin: 64px 0;
|
||||||
padding: 0 32px;
|
padding: 0 32px;
|
||||||
|
position: sticky;
|
||||||
|
top: 32px;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -50,7 +52,11 @@ export const SidebarHeader = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const ExtraLinks = styled.div`
|
const ExtraLinks = styled.div`
|
||||||
margin-top: 16px;
|
margin-top: 32px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const RecommendedReading = styled(ExtraLinks)``
|
export const RecommendedReading = styled(ExtraLinks)``
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
@@ -1,21 +1,28 @@
|
|||||||
import { graphql } from "gatsby"
|
import { graphql } from "gatsby"
|
||||||
import React, { Fragment } from "react"
|
import React, { Fragment } from "react"
|
||||||
|
import cx from "classnames"
|
||||||
import { MarkdownRemark } from "../../generated/graphql"
|
import { MarkdownRemark } from "../../generated/graphql"
|
||||||
import { ComponentQuery } from "../../typings"
|
import { ComponentQuery } from "../../typings"
|
||||||
import { Markdown } from "../components/Markdown"
|
import { Markdown } from "../components/Markdown"
|
||||||
import { SEO } from "../components/SEO"
|
import { SEO } from "../components/SEO"
|
||||||
import { PageContent } from "../components/PageContent"
|
import { PageContent } from "../components/PageContent"
|
||||||
import { HeaderBarebone } from "../components/HeaderBarebone"
|
import { HeaderBarebone } from "../components/HeaderBarebone"
|
||||||
|
import { buildToc } from "../utils"
|
||||||
|
|
||||||
function AboutPage({ data }: ComponentQuery<{ md: MarkdownRemark }>) {
|
function AboutPage({ data }: ComponentQuery<{ md: MarkdownRemark }>) {
|
||||||
const { md } = data
|
const { md } = data
|
||||||
|
|
||||||
|
const toc = buildToc(md.headings!)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<SEO title="About" />
|
<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>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -24,6 +31,10 @@ export const query = graphql`
|
|||||||
query AboutPage {
|
query AboutPage {
|
||||||
md: markdownRemark(frontmatter: { path: { eq: "/about" } }) {
|
md: markdownRemark(frontmatter: { path: { eq: "/about" } }) {
|
||||||
html
|
html
|
||||||
|
headings {
|
||||||
|
depth
|
||||||
|
value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
+10
-2
@@ -1,21 +1,25 @@
|
|||||||
import { graphql } from "gatsby"
|
import { graphql } from "gatsby"
|
||||||
import React, { Fragment } from "react"
|
import React, { Fragment } from "react"
|
||||||
|
import cx from "classnames"
|
||||||
import { MarkdownRemark } from "../../generated/graphql"
|
import { MarkdownRemark } from "../../generated/graphql"
|
||||||
import { ComponentQuery } from "../../typings"
|
import { ComponentQuery } from "../../typings"
|
||||||
import { HeaderBarebone } from "../components/HeaderBarebone"
|
import { HeaderBarebone } from "../components/HeaderBarebone"
|
||||||
import { Markdown } from "../components/Markdown"
|
import { Markdown } from "../components/Markdown"
|
||||||
import { PageContent } from "../components/PageContent"
|
import { PageContent } from "../components/PageContent"
|
||||||
import { SEO } from "../components/SEO"
|
import { SEO } from "../components/SEO"
|
||||||
|
import { buildToc } from "../utils"
|
||||||
|
|
||||||
function RulesPage({ data }: ComponentQuery<{ md: MarkdownRemark }>) {
|
function RulesPage({ data }: ComponentQuery<{ md: MarkdownRemark }>) {
|
||||||
const { md } = data
|
const { md } = data
|
||||||
|
|
||||||
|
const toc = buildToc(md.headings!)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<SEO title="Rules" />
|
<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>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -24,6 +28,10 @@ export const query = graphql`
|
|||||||
query RulesPage {
|
query RulesPage {
|
||||||
md: markdownRemark(frontmatter: { path: { eq: "/rules" } }) {
|
md: markdownRemark(frontmatter: { path: { eq: "/rules" } }) {
|
||||||
html
|
html
|
||||||
|
headings {
|
||||||
|
depth
|
||||||
|
value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
import { graphql } from "gatsby"
|
import { graphql } from "gatsby"
|
||||||
import React, { FC, Fragment } from "react"
|
import React, { FC, Fragment } from "react"
|
||||||
|
import "katex/dist/katex.min.css"
|
||||||
|
|
||||||
|
import { Footer } from "../components/Footer"
|
||||||
import { Header } from "../components/Header"
|
import { Header } from "../components/Header"
|
||||||
import { Markdown } from "../components/Markdown"
|
import { Markdown } from "../components/Markdown"
|
||||||
import { SEO } from "../components/SEO"
|
|
||||||
|
|
||||||
import "katex/dist/katex.min.css"
|
|
||||||
import { PageContent } from "../components/PageContent"
|
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
|
// @todo maybe find alternative type for data
|
||||||
const LanguagePost: FC<any> = ({ data }) => {
|
const LanguagePost: FC<any> = ({ data }) => {
|
||||||
const { relativePath } = data.file
|
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(
|
const shiftLayout = Boolean(
|
||||||
frontmatter.recommended_reading || frontmatter.external_resources
|
frontmatter.recommended_reading ||
|
||||||
|
frontmatter.external_resources ||
|
||||||
|
toc.length
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -36,6 +48,7 @@ const LanguagePost: FC<any> = ({ data }) => {
|
|||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
toc={toc}
|
||||||
recommendedReading={frontmatter.recommended_reading}
|
recommendedReading={frontmatter.recommended_reading}
|
||||||
externalResources={frontmatter.external_resources}
|
externalResources={frontmatter.external_resources}
|
||||||
/>
|
/>
|
||||||
@@ -51,6 +64,10 @@ export const query = graphql`
|
|||||||
relativePath
|
relativePath
|
||||||
post: childMarkdownRemark {
|
post: childMarkdownRemark {
|
||||||
html
|
html
|
||||||
|
headings {
|
||||||
|
depth
|
||||||
|
value
|
||||||
|
}
|
||||||
excerpt
|
excerpt
|
||||||
fields {
|
fields {
|
||||||
authors {
|
authors {
|
||||||
|
|||||||
Vendored
+6
@@ -55,3 +55,9 @@ export interface IAllArchivesQuery extends IAllFilesQuery {
|
|||||||
edges: IFileArchiveQuery[]
|
edges: IFileArchiveQuery[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ITocItem {
|
||||||
|
depth: number
|
||||||
|
link: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|||||||
+20
-2
@@ -1,9 +1,11 @@
|
|||||||
|
import kebabCase from "lodash/kebabCase"
|
||||||
import chain from "ramda/es/chain"
|
import chain from "ramda/es/chain"
|
||||||
import partition from "ramda/es/partition"
|
import partition from "ramda/es/partition"
|
||||||
import pipe from "ramda/es/pipe"
|
import pipe from "ramda/es/pipe"
|
||||||
|
import sort from "ramda/es/sort"
|
||||||
|
|
||||||
import { sort } from "ramda"
|
import { IFile, IFileOrFolder, IFolder, ITocItem } from "../types"
|
||||||
import { IFile, IFileOrFolder, IFolder } from "../types"
|
import { MarkdownRemark } from "../../generated/graphql"
|
||||||
|
|
||||||
function specificWordsToUpper(str: string): string {
|
function specificWordsToUpper(str: string): string {
|
||||||
const wordsToUpper = ["pdo", "c"]
|
const wordsToUpper = ["pdo", "c"]
|
||||||
@@ -139,3 +141,19 @@ export function join([head, ...tail]: IFileOrFolder[]): IFileOrFolder[] {
|
|||||||
|
|
||||||
return [current, ...join(remaining)]
|
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!,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user