Skip to content

Commit

Permalink
feat(gatsby-transformer-remark): add options for tableOfContents (#9814)
Browse files Browse the repository at this point in the history
  • Loading branch information
byzanth authored and pieh committed Feb 1, 2019
1 parent 74fac69 commit 8290dfe
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 16 deletions.
40 changes: 38 additions & 2 deletions packages/gatsby-transformer-remark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,28 @@ Using the following GraphQL query you'll be able to get the table of contents
edges {
node {
html
tableOfContents(pathToSlugField: "frontmatter.path")
tableOfContents
}
}
}
}
```

### Configuring the tableOfContents

By default the tableOfContents is using the field `slug` to generate URLs. You can however provide another field using the pathToSlugField parameter. **Note** that providing a non existing field will cause the result to be null. To alter the default values for tableOfContents generation, include values for `heading` (string) and/or `maxDepth` (number 1 to 6) in graphQL query. If a value for `heading` is given, the first heading that matches will be ommitted and the toc is generated from the next heading of the same depth onwards. Value for `maxDepth` sets the maximum depth of the toc (i.e. if a maxDepth of 3 is set, only h1 to h3 headings will appear in the toc).

```graphql
{
allMarkdownRemark {
edges {
node {
html
tableOfContents(
pathToSlugField: "frontmatter.path"
heading: "only show toc from this heading onwards"
maxDepth: 2
)
frontmatter {
# Assumes you're using path in your frontmatter.
path
Expand All @@ -104,7 +125,22 @@ Using the following GraphQL query you'll be able to get the table of contents
}
```

By default the tableOfContents is using the field `slug` to generate URLs. You can however provide another field using the pathToSlugField parameter. **Note** that providing a non existing field will cause the result to be null.
To pass default options to the plugin generating the tableOfContents, configure it in gatsby-config.js as shown below. The options shown below are the defaults used by the plugin.

```javascript
// In your gatsby-config.js
plugins: [
{
resolve: `gatsby-transformer-remark`,
options: {
tableOfContents: {
heading: null,
maxDepth: 6,
},
},
},
]
```

### Excerpts

Expand Down
83 changes: 83 additions & 0 deletions packages/gatsby-transformer-remark/src/__tests__/extend-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,89 @@ final text
expect(node).toMatchSnapshot()
}
)

bootstrapTest(
`table of contents is generated with correct depth (graphql option)`,
`---
title: "my little pony"
date: "2017-09-18T23:19:51.246Z"
---
# first title
some text
## second title
some other text`,
`tableOfContents(pathToSlugField: "frontmatter.title", maxDepth: 1)
frontmatter {
title
}`,
node => {
expect(node.tableOfContents).toBe(`<ul>
<li><a href="/my%20little%20pony/#first-title">first title</a></li>
</ul>`)
}
)

bootstrapTest(
`table of contents is generated with correct depth (plugin option)`,
`---
title: "my little pony"
date: "2017-09-18T23:19:51.246Z"
---
# first title
some text
## second title
some other text`,
`tableOfContents(pathToSlugField: "frontmatter.title")
frontmatter {
title
}`,
node => {
expect(node.tableOfContents).toBe(`<ul>
<li><a href="/my%20little%20pony/#first-title">first title</a></li>
</ul>`)
},
{
pluginOptions: {
tableOfContents: {
maxDepth: 1,
},
},
}
)

bootstrapTest(
`table of contents is generated from given heading onwards`,
`---
title: "my little pony"
date: "2017-09-18T23:19:51.246Z"
---
# first title
some text
## second title
some other text
# third title
final text`,
`tableOfContents(pathToSlugField: "frontmatter.title", heading: "first title")
frontmatter {
title
}`,
node => {
expect(node.tableOfContents).toBe(`<ul>
<li><a href="/my%20little%20pony/#third-title">third title</a></li>
</ul>`)
}
)
})

describe(`Links are correctly prefixed`, () => {
Expand Down
51 changes: 37 additions & 14 deletions packages/gatsby-transformer-remark/src/extend-node-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@ const headingsCacheKey = node =>
`transformer-remark-markdown-headings-${
node.internal.contentDigest
}-${pluginsCacheStr}-${pathPrefixCacheStr}`
const tableOfContentsCacheKey = node =>
const tableOfContentsCacheKey = (node, appliedTocOptions) =>
`transformer-remark-markdown-toc-${
node.internal.contentDigest
}-${pluginsCacheStr}-${pathPrefixCacheStr}`
}-${pluginsCacheStr}-${JSON.stringify(
appliedTocOptions
)}-${pathPrefixCacheStr}`

// ensure only one `/` in new url
const withPathPrefix = (url, pathPrefix) =>
Expand Down Expand Up @@ -98,16 +100,21 @@ module.exports = (
return new Promise((resolve, reject) => {
// Setup Remark.
const {
blocks,
commonmark = true,
footnotes = true,
pedantic = true,
gfm = true,
blocks,
pedantic = true,
tableOfContents = {
heading: null,
maxDepth: 6,
},
} = pluginOptions
const tocOptions = tableOfContents
const remarkOptions = {
gfm,
commonmark,
footnotes,
gfm,
pedantic,
}
if (_.isArray(blocks)) {
Expand Down Expand Up @@ -271,27 +278,37 @@ module.exports = (
}
}

async function getTableOfContents(markdownNode, pathToSlugField) {
const cachedToc = await cache.get(tableOfContentsCacheKey(markdownNode))
async function getTableOfContents(markdownNode, gqlTocOptions) {
// fetch defaults
let appliedTocOptions = { ...tocOptions, ...gqlTocOptions }
// get cached toc
const cachedToc = await cache.get(
tableOfContentsCacheKey(markdownNode, appliedTocOptions)
)
if (cachedToc) {
return cachedToc
} else {
const ast = await getAST(markdownNode)
const tocAst = mdastToToc(ast)
const tocAst = mdastToToc(ast, appliedTocOptions)

let toc
if (tocAst.map) {
const addSlugToUrl = function(node) {
if (node.url) {
if (_.get(markdownNode, pathToSlugField) === undefined) {
if (
_.get(markdownNode, appliedTocOptions.pathToSlugField) ===
undefined
) {
console.warn(
`Skipping TableOfContents. Field '${pathToSlugField}' missing from markdown node`
`Skipping TableOfContents. Field '${
appliedTocOptions.pathToSlugField
}' missing from markdown node`
)
return null
}
node.url = [
pathPrefix,
_.get(markdownNode, pathToSlugField),
_.get(markdownNode, appliedTocOptions.pathToSlugField),
node.url,
]
.join(`/`)
Expand All @@ -309,7 +326,7 @@ module.exports = (
} else {
toc = ``
}
cache.set(tableOfContentsCacheKey(markdownNode), toc)
cache.set(tableOfContentsCacheKey(markdownNode, appliedTocOptions), toc)
return toc
}
}
Expand Down Expand Up @@ -528,9 +545,15 @@ module.exports = (
type: GraphQLString,
defaultValue: `fields.slug`,
},
maxDepth: {
type: GraphQLInt,
},
heading: {
type: GraphQLString,
},
},
resolve(markdownNode, { pathToSlugField }) {
return getTableOfContents(markdownNode, pathToSlugField)
resolve(markdownNode, args) {
return getTableOfContents(markdownNode, args)
},
},
// TODO add support for non-latin languages https://github.com/wooorm/remark/issues/251#issuecomment-296731071
Expand Down

0 comments on commit 8290dfe

Please sign in to comment.