diff --git a/gatsby-config.js b/gatsby-config.js
index 8dffd74fa..c6e12da59 100644
--- a/gatsby-config.js
+++ b/gatsby-config.js
@@ -1,4 +1,5 @@
const purgecssWhitelist = require('./purgecss-whitelist')
+const { remarkSyntaxDiagram } = require('./src/lib/remarkSyntaxDiagram')
module.exports = {
siteMetadata: {
@@ -52,6 +53,7 @@ module.exports = {
},
},
],
+ remarkPlugins: [() => remarkSyntaxDiagram],
},
},
{
diff --git a/package.json b/package.json
index ad1de2b8b..c4da9d9c0 100644
--- a/package.json
+++ b/package.json
@@ -46,16 +46,21 @@
"remark-html": "^11.0.1"
},
"devDependencies": {
+ "@prantlf/railroad-diagrams": "^1.0.1",
+ "anafanafo": "^1.0.0",
"bulma": "^0.8.2",
"husky": "^4.2.5",
+ "jest": "^26.2.2",
"lint-staged": "^10.2.6",
"node-sass": "^4.14.1",
+ "pegjs": "^0.10.0",
"prettier": "2.0.5",
"replacestream": "^4.0.3",
"resolve-url-loader": "^3.1.1",
"rimraf": "^3.0.2",
"signale": "^1.4.0",
"to-readable-stream": "^2.1.0",
+ "unist-util-flatmap": "^1.0.0",
"yargs": "^15.3.1"
},
"keywords": [
@@ -68,7 +73,7 @@
"start:0.0.0.0": "gatsby develop --host 0.0.0.0",
"serve": "gatsby serve",
"clean": "gatsby clean",
- "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1",
+ "test": "jest --coverage --roots src",
"clean:docs:docs-tidb:en": "rimraf ./markdown-pages/contents/en/docs-tidb/**/*.md",
"clean:docs:docs-tidb:zh": "rimraf ./markdown-pages/contents/zh/docs-tidb/**/*.md",
"clean:docs:docs-tidb-operator:en": "rimraf ./markdown-pages/contents/en/docs-tidb-operator/**/*.md",
diff --git a/src/components/shortcodes/index.js b/src/components/shortcodes/index.js
index e8c723cc3..73e9e9c0c 100644
--- a/src/components/shortcodes/index.js
+++ b/src/components/shortcodes/index.js
@@ -6,6 +6,7 @@ import NavColumn from './navColumn'
import ColumnTitle from './columnTitle'
import WithCopy from './withCopy'
import TabsPanel from './tabsPanel'
+import SyntaxDiagram from './syntaxDiagram'
import EmbedYouTube from './embedYouTube'
export {
@@ -17,5 +18,6 @@ export {
ColumnTitle,
WithCopy,
TabsPanel,
+ SyntaxDiagram,
EmbedYouTube,
}
diff --git a/src/components/shortcodes/syntaxDiagram.js b/src/components/shortcodes/syntaxDiagram.js
new file mode 100644
index 000000000..5e05e979a
--- /dev/null
+++ b/src/components/shortcodes/syntaxDiagram.js
@@ -0,0 +1,46 @@
+import PropTypes from 'prop-types'
+import { FormattedMessage } from 'react-intl'
+import React from 'react'
+import AccountTreeRoundedIcon from '@material-ui/icons/AccountTreeRounded'
+import CodeIcon from '@material-ui/icons/Code'
+import '../../styles/components/syntaxDiagram.scss'
+
+const SyntaxDiagram = ({ children }) => {
+ const [value, setValue] = React.useState(0)
+
+ return (
+
+
+
+
+
+
+ {children[0]}
+
+
{children[1]}
+
+ )
+}
+
+SyntaxDiagram.propTypes = {
+ children: PropTypes.node.isRequired,
+}
+
+export default SyntaxDiagram
diff --git a/src/intl/en.json b/src/intl/en.json
index 9a9ee7e55..af49cff0a 100644
--- a/src/intl/en.json
+++ b/src/intl/en.json
@@ -18,6 +18,10 @@
"tip": "Tip",
"error": "Error"
},
+ "syntaxDiagram": {
+ "syntaxDiagram": "Diagram",
+ "ebnf": "Source"
+ },
"search": {
"type": "Type:",
"version": "Version:",
diff --git a/src/intl/zh.json b/src/intl/zh.json
index 66aeef4cf..dbc120dba 100644
--- a/src/intl/zh.json
+++ b/src/intl/zh.json
@@ -18,6 +18,10 @@
"tip": "小贴士",
"error": "错误"
},
+ "syntaxDiagram": {
+ "syntaxDiagram": "语法图",
+ "ebnf": "代码"
+ },
"search": {
"type": "分类:",
"version": "版本:",
diff --git a/src/lib/__tests__/remarkSyntaxDiagram.js b/src/lib/__tests__/remarkSyntaxDiagram.js
new file mode 100644
index 000000000..17ae3052c
--- /dev/null
+++ b/src/lib/__tests__/remarkSyntaxDiagram.js
@@ -0,0 +1,486 @@
+const {
+ deepEq,
+ appendNodeToChoices,
+ appendNodeToSequence,
+ toRailroad,
+ remarkSyntaxDiagram,
+} = require('../remarkSyntaxDiagram')
+
+describe('deepEq', () => {
+ it('returns false when type differs', () => {
+ expect(
+ deepEq(
+ { type: 'Terminal', text: 'a' },
+ { type: 'NonTerminal', text: 'a' }
+ )
+ ).toBeFalsy()
+ })
+ it('can compare leaves', () => {
+ expect(
+ deepEq(
+ { type: 'NonTerminal', text: 'a' },
+ { type: 'NonTerminal', text: 'b' }
+ )
+ ).toBeFalsy()
+ expect(
+ deepEq(
+ { type: 'NonTerminal', text: 'c' },
+ { type: 'NonTerminal', text: 'c' }
+ )
+ ).toBeTruthy()
+ })
+ it('can compare Optional', () => {
+ expect(
+ deepEq(
+ { type: 'Optional', item: { type: 'Terminal', text: 'a' } },
+ { type: 'Optional', item: { type: 'Terminal', text: 'a' } }
+ )
+ ).toBeTruthy()
+ expect(
+ deepEq(
+ { type: 'Optional', item: { type: 'Terminal', text: 'a' } },
+ { type: 'Optional', item: { type: 'NonTerminal', text: 'a' } }
+ )
+ ).toBeFalsy()
+ })
+ it('can compare OneOrMore', () => {
+ expect(
+ deepEq(
+ {
+ type: 'OneOrMore',
+ item: { type: 'Skip' },
+ repeat: { type: 'Terminal', text: 'a' },
+ },
+ {
+ type: 'OneOrMore',
+ item: { type: 'Skip' },
+ repeat: { type: 'Terminal', text: 'a' },
+ }
+ )
+ ).toBeTruthy()
+ expect(
+ deepEq(
+ {
+ type: 'OneOrMore',
+ item: { type: 'Skip' },
+ repeat: { type: 'Terminal', text: 'a' },
+ },
+ {
+ type: 'OneOrMore',
+ item: { type: 'Terminal', text: 'a' },
+ repeat: { type: 'Skip' },
+ }
+ )
+ ).toBeFalsy()
+ })
+ it('can compare Choice', () => {
+ expect(
+ deepEq(
+ {
+ type: 'Choice',
+ normalIndex: 0,
+ options: [
+ { type: 'Terminal', text: 'a' },
+ { type: 'Terminal', text: 'b' },
+ ],
+ },
+ {
+ type: 'Choice',
+ normalIndex: 0,
+ options: [
+ { type: 'Terminal', text: 'a' },
+ { type: 'Terminal', text: 'b' },
+ ],
+ }
+ )
+ ).toBeTruthy()
+ expect(
+ deepEq(
+ {
+ type: 'Choice',
+ normalIndex: 0,
+ options: [
+ { type: 'Terminal', text: 'a' },
+ { type: 'Terminal', text: 'b' },
+ ],
+ },
+ {
+ type: 'Choice',
+ normalIndex: 0,
+ options: [
+ { type: 'Terminal', text: 'a' },
+ { type: 'Terminal', text: 'b' },
+ { type: 'Terminal', text: 'c' },
+ ],
+ }
+ )
+ ).toBeFalsy()
+ expect(
+ deepEq(
+ {
+ type: 'Choice',
+ normalIndex: 0,
+ options: [
+ { type: 'Terminal', text: 'a' },
+ { type: 'Terminal', text: 'b' },
+ ],
+ },
+ {
+ type: 'Choice',
+ normalIndex: 0,
+ options: [
+ { type: 'Terminal', text: 'a' },
+ { type: 'Terminal', text: 'c' },
+ ],
+ }
+ )
+ ).toBeFalsy()
+ })
+ it('can compare Sequence', () => {
+ expect(
+ deepEq(
+ {
+ type: 'Sequence',
+ items: [
+ { type: 'Terminal', text: 'a' },
+ { type: 'Terminal', text: 'b' },
+ ],
+ },
+ {
+ type: 'Sequence',
+ items: [
+ { type: 'Terminal', text: 'a' },
+ { type: 'Terminal', text: 'b' },
+ ],
+ }
+ )
+ ).toBeTruthy()
+ expect(
+ deepEq(
+ {
+ type: 'Sequence',
+ items: [
+ { type: 'Terminal', text: 'a' },
+ { type: 'Terminal', text: 'b' },
+ ],
+ },
+ {
+ type: 'Sequence',
+ items: [
+ { type: 'Terminal', text: 'a' },
+ { type: 'Terminal', text: 'b' },
+ { type: 'Terminal', text: 'c' },
+ ],
+ }
+ )
+ ).toBeFalsy()
+ expect(
+ deepEq(
+ {
+ type: 'Sequence',
+ items: [
+ { type: 'Terminal', text: 'a' },
+ { type: 'Terminal', text: 'b' },
+ ],
+ },
+ {
+ type: 'Sequence',
+ items: [
+ { type: 'Terminal', text: 'b' },
+ { type: 'Terminal', text: 'a' },
+ ],
+ }
+ )
+ ).toBeFalsy()
+ })
+ it('fails with unknown type', () => {
+ expect(() =>
+ deepEq(
+ { type: '!@#$%^&*', exoticProperty: '????' },
+ { type: '!@#$%^&*', exoticProperty: '????' }
+ )
+ ).toThrow()
+ })
+})
+
+describe('appendNodeToChoices', () => {
+ it('can recognize the `a | a? b` pattern', () => {
+ let opts = [
+ { type: 'Terminal', text: 'x' },
+ { type: 'Terminal', text: 'a' },
+ ]
+ appendNodeToChoices(opts, {
+ type: 'Sequence',
+ items: [
+ { type: 'Optional', item: { type: 'Terminal', text: 'a' } },
+ { type: 'Terminal', text: 'b' },
+ { type: 'Terminal', text: 'c' },
+ ],
+ })
+ expect(opts).toEqual([
+ { type: 'Terminal', text: 'x' },
+ {
+ type: 'OptionalSequence',
+ items: [
+ { type: 'Terminal', text: 'a' },
+ {
+ type: 'Sequence',
+ items: [
+ { type: 'Terminal', text: 'b' },
+ { type: 'Terminal', text: 'c' },
+ ],
+ },
+ ],
+ },
+ ])
+ })
+ it('can recognize the `b | a b?` pattern', () => {
+ let opts = [
+ { type: 'Terminal', text: 'x' },
+ { type: 'Terminal', text: 'c' },
+ ]
+ appendNodeToChoices(opts, {
+ type: 'Sequence',
+ items: [
+ { type: 'Optional', item: { type: 'Terminal', text: 'a' } },
+ { type: 'Optional', item: { type: 'Terminal', text: 'b' } },
+ { type: 'Optional', item: { type: 'Terminal', text: 'c' } },
+ ],
+ })
+ expect(opts).toEqual([
+ { type: 'Terminal', text: 'x' },
+ {
+ type: 'OptionalSequence',
+ items: [
+ {
+ type: 'Sequence',
+ items: [
+ { type: 'Optional', item: { type: 'Terminal', text: 'a' } },
+ { type: 'Optional', item: { type: 'Terminal', text: 'b' } },
+ ],
+ },
+ { type: 'Terminal', text: 'c' },
+ ],
+ },
+ ])
+ })
+ it('just appends without special patterns', () => {
+ let opts = [
+ { type: 'Terminal', text: 'x' },
+ { type: 'Terminal', text: 'b' },
+ ]
+ appendNodeToChoices(opts, {
+ type: 'Sequence',
+ items: [
+ { type: 'Terminal', text: '(' },
+ { type: 'Optional', item: { type: 'Terminal', text: 'b' } },
+ { type: 'Terminal', text: ')' },
+ ],
+ })
+ expect(opts).toEqual([
+ { type: 'Terminal', text: 'x' },
+ { type: 'Terminal', text: 'b' },
+ {
+ type: 'Sequence',
+ items: [
+ { type: 'Terminal', text: '(' },
+ { type: 'Optional', item: { type: 'Terminal', text: 'b' } },
+ { type: 'Terminal', text: ')' },
+ ],
+ },
+ ])
+ })
+})
+
+describe('appendNodeToSequence', () => {
+ it('can recognize the `a (b a)*` pattern', () => {
+ let items = [
+ { type: 'Terminal', text: 'x' },
+ { type: 'Terminal', text: 'a' },
+ ]
+ appendNodeToSequence(items, {
+ type: 'OneOrMore',
+ item: { type: 'Skip' },
+ repeat: {
+ type: 'Sequence',
+ items: [
+ { type: 'Terminal', text: ',' },
+ { type: 'Terminal', text: 'a' },
+ ],
+ },
+ })
+ expect(items).toEqual([
+ { type: 'Terminal', text: 'x' },
+ {
+ type: 'OneOrMore',
+ item: { type: 'Terminal', text: 'a' },
+ repeat: { type: 'Terminal', text: ',' },
+ },
+ ])
+ })
+ it('just appends without special patterns', () => {
+ let items = [
+ { type: 'Terminal', text: 'x' },
+ { type: 'Terminal', text: 'a' },
+ ]
+ appendNodeToSequence(items, {
+ type: 'OneOrMore',
+ item: { type: 'Skip' },
+ repeat: {
+ type: 'Sequence',
+ items: [
+ { type: 'Terminal', text: ',' },
+ { type: 'Terminal', text: 'b' },
+ ],
+ },
+ })
+ expect(items).toEqual([
+ { type: 'Terminal', text: 'x' },
+ { type: 'Terminal', text: 'a' },
+ {
+ type: 'OneOrMore',
+ item: { type: 'Skip' },
+ repeat: {
+ type: 'Sequence',
+ items: [
+ { type: 'Terminal', text: ',' },
+ { type: 'Terminal', text: 'b' },
+ ],
+ },
+ },
+ ])
+ })
+})
+
+describe('toRailroad', () => {
+ it('can handle Terminal width', () => {
+ const node = toRailroad({ type: 'Terminal', text: 'PARTITION' })
+ expect(node.width).toBe(101)
+ })
+ it('can handle NonTerminal width', () => {
+ const node = toRailroad({ type: 'NonTerminal', text: 'PARTITION' })
+ expect(node.width).toBe(87)
+ })
+ it('can handle width of composite object', () => {
+ const node = toRailroad({
+ type: 'Sequence',
+ items: [
+ { type: 'Terminal', text: 'PARTITION' },
+ { type: 'NonTerminal', text: 'PARTITION' },
+ ],
+ })
+ expect(node.width).toBe(208)
+ })
+})
+
+describe('remarkSyntaxDiagram', () => {
+ it('ignores ```ebnf blocks', () => {
+ const src = {
+ type: 'root',
+ children: [
+ { type: 'code', lang: 'ebnf', value: 'a ::= b' },
+ { type: 'code', lang: null, value: 'c ::= d' },
+ ],
+ }
+ expect(remarkSyntaxDiagram(src)).toEqual(src)
+ })
+ it('handles ```ebnf+diagram blocks', () => {
+ expect(
+ remarkSyntaxDiagram({
+ type: 'root',
+ children: [
+ { type: 'thematicBreak' },
+ { type: 'code', lang: 'ebnf+diagram', value: 'a ::= b' },
+ { type: 'paragraph', children: [{ type: 'text', value: '????' }] },
+ ],
+ })
+ ).toEqual({
+ type: 'root',
+ children: [
+ { type: 'thematicBreak' },
+ { type: 'jsx', value: '' },
+ {
+ type: 'html',
+ value: `- a
-
+
`,
+ },
+ { type: 'code', lang: 'ebnf', value: 'a ::= b' },
+ { type: 'jsx', value: '' },
+ { type: 'paragraph', children: [{ type: 'text', value: '????' }] },
+ ],
+ })
+ })
+ it('generates multiple