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`,
|
||||
{
|
||||
resolve: "gatsby-plugin-anchor-links",
|
||||
options: {
|
||||
offset: -100,
|
||||
},
|
||||
},
|
||||
`gatsby-plugin-react-svg`,
|
||||
`gatsby-plugin-remove-trailing-slashes`,
|
||||
`gatsby-plugin-sharp`,
|
||||
|
||||
Generated
+24
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)``
|
||||
|
||||
@@ -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 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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Vendored
+6
@@ -55,3 +55,9 @@ export interface IAllArchivesQuery extends IAllFilesQuery {
|
||||
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 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!,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user