diff --git a/packages/website/package.json b/packages/website/package.json index f83efc0a9f..e8404c7d79 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -126,6 +126,7 @@ "cache-loader": "^4.1.0", "compare-versions": "^3.5.1", "css-loader": "0.23.x", + "extend": "^3.0.2", "glob": "^7.1.4", "json-stringify-pretty-compact": "^2.0.0", "less-loader": "^4.1.0", @@ -149,6 +150,7 @@ "unist-util-find-after": "^2.0.4", "unist-util-modify-children": "^1.1.4", "unist-util-select": "^2.0.2", + "unist-util-visit": "^2.0.0", "unist-util-visit-parents": "^3.0.0", "webpack": "^4.39.2", "webpack-cli": "3.3.7", diff --git a/packages/website/ts/components/docs/mdx/headings.tsx b/packages/website/ts/components/docs/mdx/headings.tsx index 429e374a2f..7f5370a088 100644 --- a/packages/website/ts/components/docs/mdx/headings.tsx +++ b/packages/website/ts/components/docs/mdx/headings.tsx @@ -1,7 +1,35 @@ import styled from 'styled-components'; import { Heading } from 'ts/components/text'; -const H1 = styled(Heading).attrs({ +const MDXHeading = styled(Heading)` + position: relative; + + &:hover { + .heading-link-icon { + opacity: 1; + } + } + + .heading-link-icon { + display: inline-block; + width: 16px; + height: 16px; + + position: absolute; + transform: translateY(-50%); + top: 50%; + left: -26px; + padding-right: 26px; + + opacity: 0; + transition: opacity 200ms ease-in-out; + + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3M8 12h8'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + } +`; + +const H1 = styled(MDXHeading).attrs({ size: 34, asElement: 'h1', marginBottom: '1rem', @@ -12,32 +40,32 @@ const H1 = styled(Heading).attrs({ } `; -const H2 = styled(Heading).attrs({ +const H2 = styled(MDXHeading).attrs({ size: 'default', asElement: 'h2', marginBottom: '1rem', })``; -const H3 = styled(Heading).attrs({ +const H3 = styled(MDXHeading).attrs({ size: 'small', asElement: 'h3', fontWeight: '300', marginBottom: '1rem', })``; -const H4 = styled(Heading).attrs({ +const H4 = styled(MDXHeading).attrs({ asElement: 'h4', fontWeight: '300', marginBottom: '1rem', })``; -const H5 = styled(Heading).attrs({ +const H5 = styled(MDXHeading).attrs({ asElement: 'h5', fontWeight: '300', marginBottom: '1rem', })``; -const H6 = styled(Heading).attrs({ +const H6 = styled(MDXHeading).attrs({ asElement: 'h6', fontWeight: '300', marginBottom: '1rem', diff --git a/packages/website/webpack.config.js b/packages/website/webpack.config.js index a8fcb9f548..536638e016 100644 --- a/packages/website/webpack.config.js +++ b/packages/website/webpack.config.js @@ -5,6 +5,7 @@ const TerserPlugin = require('terser-webpack-plugin'); const RollbarSourceMapPlugin = require('rollbar-sourcemap-webpack-plugin'); const childProcess = require('child_process'); const remarkSlug = require('remark-slug'); +const remarkAutolinkHeadings = require('./webpack/remark_autolink_headings'); const remarkSectionizeHeadings = require('./webpack/remark_sectionize_headings'); const mdxTableOfContents = require('./webpack/mdx_table_of_contents'); @@ -65,7 +66,7 @@ const config = { { loader: '@mdx-js/loader', options: { - remarkPlugins: [remarkSlug, remarkSectionizeHeadings], + remarkPlugins: [remarkSlug, remarkAutolinkHeadings, remarkSectionizeHeadings], compilers: [mdxTableOfContents], }, }, diff --git a/packages/website/webpack/mdx_table_of_contents.js b/packages/website/webpack/mdx_table_of_contents.js index 8dfa845902..aa5faa3bd7 100644 --- a/packages/website/webpack/mdx_table_of_contents.js +++ b/packages/website/webpack/mdx_table_of_contents.js @@ -61,11 +61,14 @@ function isSlugifiedSection(node) { } function toFragment(nodes) { - if (nodes.length === 1 && nodes[0].type === 'text') { - return JSON.stringify(nodes[0].value); - } else { - return '' + nodes.map(toJSX).join('') + ''; + // Because of autolinking headings earlier (at remark stage), the headings (nodes) + // contain an anchor tag next to the title, we only want to render the text. + // Unless there is no text, then we render whatever nodes we get in a Fragment + const textNode = nodes.find(node => node.type === 'text'); + if (textNode) { + return JSON.stringify(textNode.value); } + return '' + nodes.map(toJSX).join('') + ''; } function tableOfContentsListSerializer(nodes, indent = 0) { diff --git a/packages/website/webpack/remark_autolink_headings.js b/packages/website/webpack/remark_autolink_headings.js new file mode 100644 index 0000000000..d9000f33e1 --- /dev/null +++ b/packages/website/webpack/remark_autolink_headings.js @@ -0,0 +1,45 @@ +const visit = require('unist-util-visit'); +const extend = require('extend'); + +const content = { + type: 'element', + tagName: 'i', + properties: { className: ['heading-link-icon'] }, +}; + +const linkProperties = { ariaHidden: 'true' }; + +const hChildren = Array.isArray(content) ? content : [content]; + +module.exports = plugin; + +function plugin() { + return transform; +} + +function transform(tree) { + visit(tree, 'heading', visitor); +} + +function visitor(node) { + const { data } = node; + const id = data && data.hProperties && data.hProperties.id; + const url = '#' + id; + + if (id) { + inject(node, url); + } +} + +function inject(node, url) { + node.children.unshift({ + type: 'link', + url, + title: null, + children: [], + data: { + hProperties: extend(true, {}, linkProperties), + hChildren: extend(true, [], hChildren), + }, + }); +} diff --git a/yarn.lock b/yarn.lock index 7bdb5d6d8f..afa58a90d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8903,9 +8903,10 @@ extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" -extend@~3.0.2: +extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== external-editor@^2.0.4: version "2.2.0" @@ -20723,7 +20724,7 @@ unist-util-visit-parents@^3.0.0: "@types/unist" "^2.0.3" unist-util-is "^4.0.0" -unist-util-visit@2.0.0: +unist-util-visit@2.0.0, unist-util-visit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.0.tgz#1fdae5ea88251651bfe49b7e84390d664fc227c5" integrity sha512-kiTpWKsF54u/78L/UU/i7lxrnqGiEWBgqCpaIZBYP0gwUC+Akq0Ajm4U8JiNIoQNfAioBdsyarnOcTEAb9mLeQ==