Skip to content

Commit

Permalink
Interactive synopsis Fix #2024 (#3140)
Browse files Browse the repository at this point in the history
* added html to react

* utility html to react parser

* using code component and minor changes

* added args linker plugin to add ids to options

* removed hover css

* fixed selection logic

* removed html to react dependency

* added anchor icon in front of list items

* update to fix only use args from square brackets

* show anchor icon on list hover

* added outline

* linkified whole arguments with parameters

* allowed two level of square brackets nestings

* made link icon visible on focus

* updated command linker to add hash links for args

* fix codeclimate

* added id attribute to respective inline code block

* added id to list and paragraph instead

* updated code component to use all props

* added ids for argument to respective code block

* used lodash has property for key check

* replaced custom component with prism hook

* generalized the arg regex

* updated command linker

* reduced line for code climate fix

* used same link icon source for consistency

* added pathname params

* code fixes

* aligned the icons
  • Loading branch information
yathomasi authored Jan 11, 2022
1 parent f6b583c commit 15aac29
Show file tree
Hide file tree
Showing 10 changed files with 2,463 additions and 2,299 deletions.
10 changes: 10 additions & 0 deletions config/prismjs/dvc-hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/* eslint-env node */

const Prism = require('prismjs')
const argsRegex = new RegExp(/\-{1,2}[a-zA-Z-]*/, 'ig')

// Make sure the $ part of the command prompt in shell
// examples isn't copiable by making it an 'input' token.
Expand All @@ -17,3 +18,12 @@ Prism.hooks.add('after-tokenize', env => {
}
}
})

Prism.hooks.add('wrap', env => {
if (env.language === 'usage' && env.type === 'arg') {
const { content } = env
env.tag = 'a'
const href = content.match(argsRegex)[0]
env.attributes.href = `#${href}`
}
})
8 changes: 8 additions & 0 deletions config/prismjs/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,13 @@ Prism.languages.usage = {
},
usage: {
pattern: /(^|\n)\s*(usage|positional arguments|optional arguments)/
},
args: {
pattern: /(?<=\[)(?:[^\]\[]+|\[(?:[^\]\[]+|\[[^\]\[]*\])*\])*(?=\])/,
inside: {
arg: {
pattern: /\-{1,2}[^|]*/
}
}
}
}
12 changes: 11 additions & 1 deletion gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const apiMiddleware = require('./src/server/middleware/api')
const redirectsMiddleware = require('./src/server/middleware/redirects')
const makeFeedHtml = require('./plugins/utils/makeFeedHtml')
const { BLOG } = require('./src/consts')
const { linkIcon } = require('./static/icons')

const title = 'Data Version Control · DVC'
const description =
Expand Down Expand Up @@ -66,6 +67,14 @@ const plugins = [
plugins: [
'gatsby-remark-embedder',
'gatsby-remark-dvc-linker',
{
resolve: 'gatsby-remark-args-linker',
options: {
icon: linkIcon,
// Pathname can also be array of paths. eg: ['docs/command-reference;', 'docs/api']
pathname: 'docs/command-reference'
}
},
{
resolve: 'gatsby-remark-prismjs',
options: {
Expand Down Expand Up @@ -97,7 +106,8 @@ const plugins = [
resolve: 'gatsby-remark-autolink-headers',
options: {
enableCustomId: true,
isIconAfterHeader: true
isIconAfterHeader: true,
icon: linkIcon
}
},
{
Expand Down
100 changes: 100 additions & 0 deletions plugins/gatsby-remark-args-linker/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const visit = require('unist-util-visit')
const _ = require('lodash')

const argsRegex = new RegExp(/\-{1,2}[a-zA-Z-]*/, 'ig')

function patch(context, key, value) {
if (!_.has(context, key)) {
context[key] = value
}

return context[key]
}

const addIdAttrToNode = (node, id) => {
const data = patch(node, `data`, {})

patch(data, `id`, id)
patch(data, `htmlAttributes`, {})
patch(data, `hProperties`, {})
patch(data.htmlAttributes, `id`, id)
patch(data.hProperties, `id`, id)
}

module.exports = (
{ markdownAST, getNode, markdownNode },
{ icon = '', className = 'anchor', isIconAfterHeader = false, pathname = '' }
) => {
if (!pathname) return markdownAST
const parentNode = getNode(markdownNode.parent)
let isPath =
typeof pathname === 'string'
? parentNode.relativeDirectory.startsWith(pathname)
: Array.isArray(pathname)
? pathname.some(p => parentNode.relativeDirectory.startsWith(p))
: false
if (!isPath) return markdownAST
visit(
markdownAST,
node =>
node.type === 'listItem' &&
node.children[0]?.type === 'paragraph' &&
node.children[0]?.children[0]?.type === 'inlineCode' &&
String(node.children[0].children[0]?.value).startsWith('-'),
listItemNode => {
const isParagraphNode = listItemNode.children?.[0].type === 'paragraph'
if (!isParagraphNode) return
const paragraphNode = listItemNode.children[0]
const isFirstArgNode =
paragraphNode.children[0]?.type === 'inlineCode' &&
String(paragraphNode.children[0]?.value).startsWith('-')
if (isFirstArgNode) {
const firstArgNode = paragraphNode.children[0]
const value = firstArgNode.value
const id = value.match(argsRegex)[0]
addIdAttrToNode(firstArgNode, id)

const data = patch(listItemNode, `data`, {})
patch(data, `htmlAttributes`, {})
patch(data, `hProperties`, {})
if (icon) {
patch(data.hProperties, `style`, `position:relative;`)
const label = id.split(`-`).join(` `)
const method = isIconAfterHeader ? `push` : `unshift`
listItemNode.children[method]({
type: `link`,
url: `#${id}`,
title: null,
children: [],
data: {
hProperties: {
'aria-label': `option ${label.trim()} permalink`,
class: `${className} ${isIconAfterHeader ? `after` : `before`}`
},
hChildren: [
{
type: `raw`,
// The Octicon link icon is the default. But users can set their own icon via the "icon" option.
value: icon
}
]
}
})
}

const isSecondArgNode =
String(paragraphNode.children[1]?.value).trim() === ',' &&
paragraphNode.children[2]?.type === 'inlineCode' &&
String(paragraphNode.children[2].value).startsWith('-')
if (isSecondArgNode) {
const secondArgNode = paragraphNode.children[2]
const value = secondArgNode.value
const id = value.match(argsRegex)[0]
addIdAttrToNode(secondArgNode, id)
}
}
}
)
// Manipulate AST
return markdownAST
}
12 changes: 12 additions & 0 deletions plugins/gatsby-remark-args-linker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "gatsby-remark-args-linker",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
35 changes: 17 additions & 18 deletions plugins/gatsby-remark-dvc-linker/commandLinker.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,32 @@ const { getItemByPath } = require('../../src/utils/shared/sidebar')

const DVC_REGEXP = /dvc\s+[a-z][a-z-.]*/
const COMMAND_REGEXP = /^[a-z][a-z-]*$/
const ARGS_REGEXP = new RegExp(/\-{1,2}[a-zA-Z-]*/, 'ig')
const COMMAND_ROOT = '/doc/command-reference/'

module.exports = astNode => {
const node = astNode[0]
const parent = astNode[2]

if (parent.type !== 'link' && DVC_REGEXP.test(node.value)) {
const parts = node.value.split(/\s+/)
const index = parts.findIndex(part => String(part).trim() === 'dvc')
const command = parts[index + 1]
const baseUrl = `${COMMAND_ROOT}${command}`
let url

const hasThirdSegment = parts[2] && COMMAND_REGEXP.test(parts[2])
const isCommandPageExists = getItemByPath(`${COMMAND_ROOT}${parts[1]}`)
const isSubcommandPageExists =
isCommandPageExists &&
hasThirdSegment &&
getItemByPath(`${COMMAND_ROOT}${parts[1]}/${parts[2]}`)

if (isSubcommandPageExists) {
url = `${COMMAND_ROOT}${parts[1]}/${parts[2]}`
} else if (isCommandPageExists && hasThirdSegment) {
url = `${COMMAND_ROOT}${parts[1]}#${parts[2]}`
} else if (isCommandPageExists) {
url = `${COMMAND_ROOT}${parts[1]}`
const isCommandPageExists = getItemByPath(baseUrl)
if (isCommandPageExists) {
url = baseUrl
for (const arg of parts.slice(index + 2)) {
if (arg && COMMAND_REGEXP.test(arg) && getItemByPath(`${url}/${arg}`)) {
url = `${url}/${arg}`
} else if (arg && ARGS_REGEXP.test(arg)) {
const id = arg.match(ARGS_REGEXP)[0]
url = `${url}#${id}`
break
}
}
createLinkNode(url, astNode)
}

createLinkNode(url, astNode)
}

return astNode
}
21 changes: 21 additions & 0 deletions src/components/Documentation/Markdown/Main/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,31 @@
}
}

a.token.arg {
color: var(--color-light-blue);
outline-color: var(--color-light-blue-hover);
}

:global(a.gatsby-resp-image-link)::after {
content: unset;
}

li .anchor svg {
visibility: hidden;
}

li:hover .anchor svg {
visibility: visible;
}

li .anchor:focus svg {
visibility: visible;
}

li .anchor {
line-height: unset;
}

.anchor {
margin-left: -24px;
}
Expand Down
4 changes: 4 additions & 0 deletions static/icons/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const svgIcon = `<svg aria-hidden="true" height="16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"/></svg>`
module.exports = {
linkIcon: svgIcon
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"target": "es5",
"module": "commonjs",
"resolveJsonModule": true,
"lib": ["dom", "es2015", "es2017"],
"lib": ["dom", "es2015", "es2017", "es2021"],
"jsx": "react",
"sourceMap": true,
"strict": true,
Expand Down
Loading

0 comments on commit 15aac29

Please sign in to comment.