├── .github └── workflows │ └── dependabot-auto-merge.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── package.json ├── playground ├── app.tsx ├── example.md ├── global.d.ts ├── index.html └── styled.ts ├── src ├── List.ts ├── TokenCollector.ts ├── id.ts ├── index.ts ├── types.ts └── utils.ts ├── tsconfig.build.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto merge 2 | 3 | on: 4 | check_suite: 5 | types: 6 | - completed 7 | pull_request: 8 | types: 9 | - labeled 10 | - unlabeled 11 | - synchronize 12 | - opened 13 | - edited 14 | - ready_for_review 15 | - reopened 16 | - unlocked 17 | 18 | jobs: 19 | auto-merge: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: auto-merge 23 | uses: ridedott/dependabot-auto-merge-action@master 24 | with: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .idea 4 | .cache 5 | *.tgz 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | /src 4 | /demo 5 | /playground 6 | .prettierrc 7 | tsconfig.json 8 | tsconfig.build.json 9 | tslint.json 10 | yarn.lock 11 | *.tgz 12 | .cache 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": true, 4 | "trailingComma": "all", 5 | "singleQuote": true, 6 | "arrowParens": "always", 7 | "jsxBracketSameLine": false, 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 LeetCode Open Source 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdown-it-grouped-code-fence 2 | 3 | _Grouped code fence_ is almost the same as [code fence](https://spec.commonmark.org/0.28/#code-fence). The only difference is that you can use a syntax, __`keyword-title` within a pair of brackets__ , in the [info string](https://spec.commonmark.org/0.28/#info-string) to combine multiple code fence into a single group. In a Markdown renderer that does not support this syntax, will ignore the syntax and render it as a normal code fence. 4 | 5 | 6 | ## Syntax 7 | ~~~ 8 | ```language [keyword-title] 9 | ``` 10 | ~~~ 11 | 12 | #### `keyword` 13 | Optional, Used to distinguish between different groups. default will consider as a anonymous group. 14 | 15 | #### `title` 16 | Optional, Used to customize the title of each code fence. default will using the language name. 17 | 18 | 19 | ## Examples 20 | Go to [Playground](https://leetcode-opensource.github.io/markdown-it-grouped-code-fence/) to see the output. 21 | 22 | ### Use keywords to distinguish between different groups 23 | ~~~ 24 | ```ruby [printA] 25 | puts 'A' 26 | ``` 27 | 28 | ```python [printA-python3] 29 | print('A') 30 | ``` 31 | 32 | ```javascript [printB] 33 | console.log('B') 34 | ``` 35 | ~~~ 36 | 37 | ##### output: 38 | ```ruby [printA] 39 | puts 'A' 40 | ``` 41 | 42 | ```python [printA-python3] 43 | print('A') 44 | ``` 45 | 46 | ```javascript [printB] 47 | console.log('B') 48 | ``` 49 | 50 | 51 | ### Anonymous group 52 | ~~~ 53 | ```ruby [] 54 | put 'Hello world!' 55 | ``` 56 | 57 | ```python [-python3] 58 | print('Hello world!') 59 | ``` 60 | 61 | ```javascript [] 62 | console.log('Hello world!') 63 | ``` 64 | ~~~ 65 | 66 | ##### output: 67 | ```ruby [] 68 | put 'Hello world!' 69 | ``` 70 | 71 | ```python [-python3] 72 | print('Hello world!') 73 | ``` 74 | 75 | ```javascript [] 76 | console.log('Hello world!') 77 | ``` 78 | 79 | 80 | ## Installation 81 | 82 | Using [yarn](https://yarnpkg.com/): 83 | ```bash 84 | yarn add markdown-it-grouped-code-fence 85 | ``` 86 | 87 | Or via [npm](https://docs.npmjs.com): 88 | ```bash 89 | npm install markdown-it-grouped-code-fence 90 | ``` 91 | 92 | Then, to enable the feature: 93 | 94 | ```javascript 95 | import MarkdownIt from 'markdown-it'; 96 | import { groupedCodeFencePlugin } from 'markdown-it-grouped-code-fence'; 97 | 98 | const md = new MarkdownIt(); 99 | 100 | md.use( 101 | groupedCodeFencePlugin({ 102 | className: { 103 | container: 'class name here', 104 | navigationBar: 'class name here', 105 | fenceRadio: 'class name here', 106 | labelRadio: 'class name here', 107 | }, 108 | }), 109 | ); 110 | ``` 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-it-grouped-code-fence", 3 | "version": "1.2.0", 4 | "description": "markdown-it plugin for grouping the code fence", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "dev": "parcel ./playground/index.html --no-cache --out-dir ./playground/dist", 9 | "build:playground": "rm -rf ./playground/dist/* && parcel build ./playground/index.html --out-dir ./playground/dist --public-url ./", 10 | "build": "rm -rf ./dist && tsc -p ./tsconfig.build.json", 11 | "lint": "tslint -p tsconfig.json --fix", 12 | "prettier": "prettier --write '{playground,src}/**/*'", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/LeetCode-OpenSource/markdown-it-grouped-code-fence.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/LeetCode-OpenSource/markdown-it-grouped-code-fence/issues" 21 | }, 22 | "homepage": "https://github.com/LeetCode-OpenSource/markdown-it-grouped-code-fence#readme", 23 | "keywords": [ 24 | "markdown-it", 25 | "markdown-it-plugin" 26 | ], 27 | "author": "LeetCode front-end team", 28 | "license": "MIT", 29 | "husky": { 30 | "hooks": { 31 | "pre-commit": "lint-staged" 32 | } 33 | }, 34 | "lint-staged": { 35 | "*.{ts,tsx}": [ 36 | "prettier --write", 37 | "tslint -p tsconfig.json --fix", 38 | "git add" 39 | ], 40 | "*.{html,less}": [ 41 | "prettier --write", 42 | "git add" 43 | ] 44 | }, 45 | "assetTypesToStringify": [ 46 | "md" 47 | ], 48 | "devDependencies": { 49 | "@emotion/core": "^10.0.5", 50 | "@emotion/styled": "^10.0.5", 51 | "@types/codemirror": "^0.0.108", 52 | "@types/markdown-it": "^12.0.0", 53 | "@types/react": "^17.0.0", 54 | "@types/react-dom": "^17.0.0", 55 | "codemirror": "^5.42.0", 56 | "emotion": "^11.0.0", 57 | "husky": "^4.0.0", 58 | "less": "^4.0.0", 59 | "lint-staged": "^10.0.1", 60 | "markdown-it": "^12.0.0", 61 | "normalize.css": "^8.0.1", 62 | "parcel": "^1.10.3", 63 | "parcel-bundler": "^1.10.3", 64 | "parcel-plugin-stringify-anything": "^1.2.0", 65 | "prettier": "2.2.1", 66 | "react": "^16.6.3", 67 | "react-codemirror2": "^7.0.0", 68 | "react-dom": "^17.0.0", 69 | "react-simple-resizer": "^2.1.0", 70 | "tslib": "^2.0.0", 71 | "tslint": "^6.1.2", 72 | "tslint-eslint-rules": "^5.3.1", 73 | "tslint-react": "^5.0.0", 74 | "tslint-sonarts": "^1.7.0", 75 | "typescript": "^4.0.3" 76 | }, 77 | "peerDependencies": { 78 | "markdown-it": "^12.0.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /playground/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Container } from 'react-simple-resizer'; 4 | import { UnControlled as CodeMirror } from 'react-codemirror2'; 5 | import MD from 'markdown-it'; 6 | import 'codemirror/mode/markdown/markdown.js'; 7 | 8 | import { groupedCodeFencePlugin } from '../src'; 9 | import { Editor, Section, Bar, MarkDown, GroupClassName } from './styled'; 10 | import README from '../README.md'; 11 | 12 | const md = new MD(); 13 | 14 | md.use( 15 | groupedCodeFencePlugin({ 16 | className: GroupClassName, 17 | }), 18 | ); 19 | 20 | interface State { 21 | value: string; 22 | } 23 | 24 | class Edit extends React.PureComponent<{}, State> { 25 | state: State = { 26 | value: README, 27 | }; 28 | 29 | render() { 30 | return ( 31 | 32 |
33 | 34 | 48 | 49 |
50 | 51 |
52 | 55 |
56 |
57 | ); 58 | } 59 | 60 | private onChange = (_: any, __: any, value: string) => { 61 | this.setState({ value }); 62 | }; 63 | } 64 | 65 | ReactDOM.render(, document.getElementById('app')); 66 | -------------------------------------------------------------------------------- /playground/example.md: -------------------------------------------------------------------------------- 1 | # Title 2 | 3 | ```javascript [twoSum] 4 | /** 5 | * @param {number[]} nums 6 | * @param {number} target 7 | * @return {number[]} 8 | */ 9 | var twoSum = function(nums, target) { 10 | return 0; 11 | }; 12 | ``` 13 | 14 | ```python [twoSum] 15 | class Solution(object): 16 | def twoSum(self, nums, target): 17 | """ 18 | :type nums: List[int] 19 | :type target: int 20 | :rtype: List[int] 21 | """ 22 | ``` 23 | 24 | ```python [twoSum] 25 | class Solution: 26 | def twoSum(self, nums, target): 27 | """ 28 | :type nums: List[int] 29 | :type target: int 30 | :rtype: List[int] 31 | """ 32 | ``` 33 | 34 | ```go [] 35 | func twoSum(nums []int, target int) []int { 36 | 37 | } 38 | ``` 39 | 40 | ```java [] 41 | class Solution { 42 | public int[] twoSum(int[] nums, int target) { 43 | 44 | } 45 | } 46 | ``` 47 | 48 | some text... 49 | 50 | ```ruby [] 51 | # @param {Integer[]} nums 52 | # @param {Integer} target 53 | # @return {Integer[]} 54 | def two_sum(nums, target) 55 | 56 | end 57 | ``` 58 | -------------------------------------------------------------------------------- /playground/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.md' { 2 | const markdown: string; 3 | export default markdown; 4 | } 5 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | Playground 10 | 14 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /playground/styled.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | import styled from '@emotion/styled'; 3 | import { 4 | Bar as ResizerBar, 5 | Section as ResizerSection, 6 | } from 'react-simple-resizer'; 7 | 8 | export const Editor = styled.div({ 9 | fontSize: '16px', 10 | 11 | '.CodeMirror': { 12 | height: '100%', 13 | }, 14 | }); 15 | 16 | export const Bar = styled(ResizerBar)({ 17 | zIndex: 1, 18 | }); 19 | 20 | export const Section = styled(ResizerSection)({ 21 | zIndex: 0, 22 | height: '100%', 23 | overflow: 'auto', 24 | }); 25 | 26 | export const MarkDown = styled.div({ 27 | padding: '20px', 28 | }); 29 | 30 | export const GroupClassName = { 31 | container: css({ 32 | margin: '20px', 33 | border: '1px solid #EEEEEE', 34 | boxShadow: '0px 1px 5px 0px rgba(0,0,0,0.04)', 35 | borderRadius: '3px', 36 | }), 37 | 38 | navigationBar: css({ 39 | padding: 0, 40 | margin: 0, 41 | userSelect: 'none', 42 | borderBottom: '1px solid #EEEEEE', 43 | whiteSpace: 'nowrap', 44 | width: '100%', 45 | overflow: 'auto', 46 | 47 | li: { 48 | display: 'inline', 49 | lineHeight: '32px', 50 | padding: '0 10px', 51 | position: 'relative', 52 | 53 | '~ li::before': { 54 | content: '""', 55 | height: '12px', 56 | width: '1px', 57 | background: '#EEEEEE', 58 | position: 'absolute', 59 | left: '0px', 60 | top: '50%', 61 | transform: 'translate(-50%, -50%)', 62 | }, 63 | }, 64 | }), 65 | 66 | fenceRadio: css({ 67 | display: 'none', 68 | 69 | '&:not(:checked) + pre': { 70 | display: 'none', 71 | }, 72 | }), 73 | 74 | labelRadio: css({ 75 | display: 'none', 76 | 77 | ':not(:checked) + label': { 78 | opacity: 0.5, 79 | }, 80 | }), 81 | }; 82 | -------------------------------------------------------------------------------- /src/List.ts: -------------------------------------------------------------------------------- 1 | import Token = require('markdown-it/lib/token'); 2 | 3 | import { Config, Nesting } from './types'; 4 | import { makeLabelTokens, makeToken, tokenMaker } from './utils'; 5 | 6 | export class List { 7 | private readonly listChildLevel = this.level + 2; 8 | 9 | private readonly listLevel = this.level + 1; 10 | 11 | private readonly openToken = makeToken({ 12 | type: 'bullet_list_open', 13 | tag: 'ul', 14 | nesting: Nesting.open, 15 | level: this.level, 16 | attrs: [['class', this.className.navigationBar]], 17 | }); 18 | 19 | private readonly closeToken = makeToken({ 20 | type: 'bullet_list_close', 21 | tag: 'ul', 22 | nesting: Nesting.close, 23 | level: this.level, 24 | }); 25 | 26 | private readonly makeListToken = tokenMaker({ 27 | type: 'list_item', 28 | nesting: Nesting.selfClose, 29 | tag: 'li', 30 | level: this.listLevel, 31 | }); 32 | 33 | private readonly listTokens: Token[] = []; 34 | 35 | get isEmptyList() { 36 | return this.count === 0; 37 | } 38 | 39 | get count() { 40 | return this.listTokens.length; 41 | } 42 | 43 | constructor( 44 | private readonly className: Config['className'], 45 | private readonly level: number, 46 | ) {} 47 | 48 | add({ 49 | inputID, 50 | inputName, 51 | title, 52 | }: { 53 | title: string; 54 | inputID: string; 55 | inputName: string; 56 | }) { 57 | this.listTokens.push( 58 | this.makeListToken({ nesting: Nesting.open }), 59 | ...makeLabelTokens({ 60 | inputID, 61 | inputName, 62 | labelText: title, 63 | level: this.listChildLevel, 64 | radioClassName: this.className.labelRadio, 65 | isCheckedByDefault: this.isEmptyList, 66 | }), 67 | this.makeListToken({ nesting: Nesting.close }), 68 | ); 69 | } 70 | 71 | get tokens() { 72 | return [this.openToken, ...this.listTokens, this.closeToken]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/TokenCollector.ts: -------------------------------------------------------------------------------- 1 | import Token = require('markdown-it/lib/token'); 2 | 3 | import { List } from './List'; 4 | import { 5 | getInputID, 6 | getInputName, 7 | makeNestedToken, 8 | makeRadioToken, 9 | tokenMaker, 10 | } from './utils'; 11 | import { getAndIncreaseID } from './id'; 12 | import { Config, Nesting, TOKEN_TYPE } from './types'; 13 | 14 | const makeOpenToken = tokenMaker({ 15 | type: TOKEN_TYPE, 16 | tag: 'div', 17 | nesting: Nesting.open, 18 | }); 19 | 20 | const makeCloseToken = tokenMaker({ 21 | type: TOKEN_TYPE, 22 | tag: 'div', 23 | nesting: Nesting.close, 24 | }); 25 | 26 | export class TokenCollector { 27 | private tokens: Token[] = []; 28 | 29 | private currentGroupID: number = -1; 30 | 31 | private currentGroupIndex: number = -1; 32 | 33 | private list: List | null = null; 34 | 35 | private get isGroupClosed() { 36 | const isGroupClosed = this.currentGroupIndex === -1 || this.list === null; 37 | 38 | if ( 39 | isGroupClosed && 40 | (this.currentGroupIndex !== -1 || this.list !== null) 41 | ) { 42 | throw new Error( 43 | 'if Group is closed, currentGroupIndex must be `-1` and list must be `null`.', 44 | ); 45 | } 46 | 47 | return isGroupClosed; 48 | } 49 | 50 | constructor(private readonly config: Config) {} 51 | 52 | addToken(token: Token) { 53 | this.tokens.push(token); 54 | } 55 | 56 | addTokenIntoCurrentGroup( 57 | token: Token, 58 | title: string, 59 | closeGroupAfterAddingToken: boolean, 60 | ) { 61 | if (this.isGroupClosed) { 62 | throw new Error('Current is no Group exist.'); 63 | } 64 | 65 | const inputID = getInputID(this.currentGroupID, this.list!.count); 66 | const inputName = getInputName(this.currentGroupID); 67 | 68 | const fenceRadioToken = makeRadioToken({ 69 | level: token.level + 1, 70 | attrs: { 71 | id: getInputID(this.currentGroupID, this.list!.count), 72 | name: getInputName(this.currentGroupID), 73 | checked: this.list!.isEmptyList, 74 | className: this.config.className.fenceRadio, 75 | }, 76 | }); 77 | 78 | const fenceToken = makeNestedToken({ token, nestLevel: 1 }); 79 | 80 | this.list!.add({ title, inputID, inputName }); 81 | this.tokens.push(fenceRadioToken, fenceToken); 82 | 83 | if (closeGroupAfterAddingToken) { 84 | this.closeCurrentGroup(token.level); 85 | } 86 | } 87 | 88 | startNewGroup(level: number, closePreviousBeforeStartANewOne: boolean) { 89 | if (closePreviousBeforeStartANewOne) { 90 | this.closeCurrentGroup(level); 91 | } 92 | 93 | if (!this.isGroupClosed) { 94 | throw new Error( 95 | 'Start a new Group before close the previous one is invalid', 96 | ); 97 | } 98 | 99 | this.currentGroupID = getAndIncreaseID(); 100 | this.currentGroupIndex = this.tokens.length; 101 | 102 | this.list = new List(this.config.className, level + 1); 103 | this.tokens.push( 104 | makeOpenToken({ 105 | level, 106 | attrs: [['class', this.config.className.container]], 107 | }), 108 | ); 109 | } 110 | 111 | closeCurrentGroup(level: number) { 112 | if (this.isGroupClosed) { 113 | throw new Error('Closing a non-existing Group is invalid.'); 114 | } 115 | 116 | this.tokens.splice(this.currentGroupIndex + 1, 0, ...this.list!.tokens); 117 | 118 | this.list = null; 119 | this.currentGroupIndex = -1; 120 | this.tokens.push(makeCloseToken({ level })); 121 | } 122 | 123 | getTokens(): Token[] { 124 | if (this.isGroupClosed) { 125 | return this.tokens.slice(0); 126 | } else { 127 | throw new Error( 128 | 'You can not get the tokens, because current Group is not closed.', 129 | ); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/id.ts: -------------------------------------------------------------------------------- 1 | let id: number = 0; 2 | 3 | // reset id for server-side-render use; 4 | export function resetID() { 5 | id = 0; 6 | } 7 | 8 | export function getAndIncreaseID() { 9 | id += 1; 10 | return id; 11 | } 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import MarkdownIt = require('markdown-it'); 2 | import StateCore = require('markdown-it/lib/rules_core/state_core'); 3 | 4 | import { RULE_NAME, Config } from './types'; 5 | import { TokenCollector } from './TokenCollector'; 6 | import { filterTokenInfo } from './utils'; 7 | 8 | function groupedCodeFence(config: Config, state: StateCore) { 9 | const tokenCollector = new TokenCollector(config); 10 | const maxIndex = state.tokens.length - 1; 11 | let prevGroupScope: string | null = null; 12 | 13 | state.tokens.forEach((token, index) => { 14 | const isEnd = index === maxIndex; 15 | const { scope: currentGroupScope, title } = filterTokenInfo(token.info); 16 | 17 | if (prevGroupScope === currentGroupScope) { 18 | const isInCurrentGroup = currentGroupScope !== null; 19 | 20 | if (isInCurrentGroup) { 21 | tokenCollector.addTokenIntoCurrentGroup(token, title, isEnd); 22 | } else { 23 | tokenCollector.addToken(token); 24 | } 25 | } else { 26 | // below condition means that prevGroupScope not equal to null. so previous token must be a Group 27 | const currentTokenIsNotGroup = currentGroupScope === null; 28 | 29 | if (currentTokenIsNotGroup) { 30 | tokenCollector.closeCurrentGroup(token.level); 31 | tokenCollector.addToken(token); 32 | } else { 33 | const prevGroupNeedToBeClosed = prevGroupScope !== null; 34 | tokenCollector.startNewGroup(token.level, prevGroupNeedToBeClosed); 35 | tokenCollector.addTokenIntoCurrentGroup(token, title, isEnd); 36 | } 37 | } 38 | 39 | prevGroupScope = currentGroupScope; 40 | }); 41 | 42 | state.tokens = tokenCollector.getTokens(); 43 | } 44 | 45 | export function groupedCodeFencePlugin(config: Config) { 46 | return (md: MarkdownIt) => 47 | md.core.ruler.push(RULE_NAME, groupedCodeFence.bind(null, config)); 48 | } 49 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import Token = require('markdown-it/lib/token'); 2 | 3 | export const RULE_NAME = 'GROUPED_CODE_FENCE'; 4 | export const TOKEN_TYPE = `${RULE_NAME}_TYPE`; 5 | 6 | export enum Nesting { 7 | open = 1, 8 | close = -1, 9 | selfClose = 0, 10 | } 11 | 12 | export type Attrs = [string, string][]; // [ [ name, value ] ] 13 | 14 | export type TokenObject = { 15 | [K in keyof Token]?: Token[K] extends Function ? never : Token[K] 16 | } & { 17 | type: string; 18 | tag: string; 19 | nesting: Nesting; 20 | attrs?: Attrs; 21 | }; 22 | 23 | export interface TokenInfo { 24 | scope: string | null; 25 | title: string; 26 | } 27 | 28 | export interface Config { 29 | className: { 30 | container: string; 31 | navigationBar: string; 32 | labelRadio: string; 33 | fenceRadio: string; 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import Token = require('markdown-it/lib/token'); 2 | 3 | import { Nesting, TokenObject, TokenInfo, Attrs } from './types'; 4 | 5 | const GROUP_REGEX = / \[([^\[\]]*)]/; // group can not be language, so there have a space before `group` 6 | const LANGUAGE_REGEX = /^[^ ]+/; 7 | 8 | function filterGroupResult( 9 | info: string, 10 | ): { scope: string | null; title: string | null } { 11 | const regexResult = GROUP_REGEX.exec(info); 12 | 13 | if (regexResult) { 14 | const [scope, title] = (regexResult[1] || '').split('-'); 15 | return { scope, title }; 16 | } else { 17 | return { 18 | scope: null, 19 | title: null, 20 | }; 21 | } 22 | } 23 | 24 | export const filterTokenInfo = (info: string): TokenInfo => { 25 | const languageResult = LANGUAGE_REGEX.exec(info); 26 | const language = (languageResult && languageResult[0]) || ''; 27 | 28 | const { scope, title } = filterGroupResult(info); 29 | 30 | return { scope, title: title || language }; 31 | }; 32 | 33 | export function makeToken({ type, tag, nesting, ...restValue }: TokenObject) { 34 | return Object.assign(new Token(type, tag, nesting), restValue); 35 | } 36 | 37 | export function makeNestedToken({ 38 | token, 39 | nestLevel, 40 | }: { 41 | token: Token; 42 | nestLevel: number; 43 | }) { 44 | return Object.assign(Object.create(Token.prototype), token, { 45 | level: token.level + nestLevel, 46 | }); 47 | } 48 | 49 | export function tokenMaker(defaultTokenValue: TokenObject) { 50 | return (tokenValue: Partial) => 51 | makeToken({ ...defaultTokenValue, ...tokenValue }); 52 | } 53 | 54 | export function getInputID(groupID: number, listCount: number) { 55 | return `group-${groupID}-${listCount}`; 56 | } 57 | 58 | export function getInputName(groupID: number) { 59 | return `group-${groupID}`; 60 | } 61 | 62 | export function makeRadioToken({ 63 | level, 64 | attrs: { id, name, className, checked }, 65 | }: { 66 | level: number; 67 | attrs: { id: string; name: string; className: string; checked?: boolean }; 68 | }) { 69 | const attrs: Attrs = [ 70 | ['type', 'radio'], 71 | ['style', 'display: none;'], 72 | ['class', className], 73 | ['id', id], 74 | ['name', name], 75 | ]; 76 | 77 | if (checked) { 78 | attrs.push(['checked', '']); 79 | } 80 | 81 | return makeToken({ 82 | level, 83 | attrs, 84 | type: 'radio_input', 85 | tag: 'input', 86 | nesting: Nesting.selfClose, 87 | }); 88 | } 89 | 90 | export function makeLabelTokens({ 91 | inputID, 92 | inputName, 93 | labelText, 94 | level, 95 | radioClassName, 96 | isCheckedByDefault, 97 | }: { 98 | inputID: string; 99 | inputName: string; 100 | labelText: string; 101 | level: number; 102 | radioClassName: string; 103 | isCheckedByDefault?: boolean; 104 | }): Token[] { 105 | return [ 106 | makeRadioToken({ 107 | level, 108 | attrs: { 109 | id: `label-${inputID}`, 110 | name: `label-${inputName}`, 111 | className: radioClassName, 112 | checked: isCheckedByDefault, 113 | }, 114 | }), 115 | makeToken({ 116 | type: 'label_item_open', 117 | tag: 'label', 118 | nesting: Nesting.open, 119 | level, 120 | attrs: [ 121 | ['for', inputID], 122 | ['onclick', 'this.previousElementSibling.click()'], 123 | ], 124 | }), 125 | makeToken({ 126 | type: 'text', 127 | tag: '', 128 | nesting: Nesting.selfClose, 129 | content: labelText, 130 | level: level + 1, 131 | }), 132 | makeToken({ 133 | type: 'label_item_close', 134 | tag: 'label', 135 | level, 136 | nesting: Nesting.close, 137 | }), 138 | ]; 139 | } 140 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist/", 4 | "declaration": true, 5 | "removeComments": true, 6 | "preserveConstEnums": true, 7 | "allowSyntheticDefaultImports": true, 8 | "experimentalDecorators": true, 9 | "noUnusedParameters": true, 10 | "noUnusedLocals": true, 11 | "noImplicitAny": true, 12 | "strict": true, 13 | "noImplicitReturns": true, 14 | "moduleResolution": "node", 15 | "lib": ["dom", "es2015"], 16 | "jsx": "react", 17 | "target": "es5" 18 | }, 19 | "include": ["src", "playground"] 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-react", 4 | "tslint-eslint-rules", 5 | "tslint-sonarts" 6 | ], 7 | "defaultSeverity": "error", 8 | "rules": { 9 | "array-bracket-spacing": [true, "never", { 10 | "arraysInArrays": false, 11 | "singleValue": false, 12 | "objectsInArrays": false 13 | }], 14 | "jsx-boolean-value": false, 15 | "block-spacing": [true, "always"], 16 | "import-spacing": true, 17 | "no-boolean-literal-compare": true, 18 | "no-console": [true, "log", "time", "trace"], 19 | "no-duplicate-variable": true, 20 | "no-multi-spaces": true, 21 | "no-return-await": true, 22 | "no-string-literal": true, 23 | "no-string-throw": true, 24 | "no-trailing-whitespace": true, 25 | "no-unnecessary-initializer": true, 26 | "no-var-keyword": true, 27 | "object-curly-spacing": [true, "always"], 28 | "one-variable-per-declaration": [true, "ignore-for-loop"], 29 | "prefer-const": true, 30 | "quotemark": [true, "single", "jsx-double"], 31 | "ter-arrow-spacing": [true, { "before": true, "after": true }], 32 | "triple-equals": [true, "allow-null-check", "allow-undefined-check"], 33 | "whitespace": [ 34 | true, 35 | "check-branch", 36 | "check-decl", 37 | "check-operator", 38 | "check-module", 39 | "check-separator", 40 | "check-rest-spread", 41 | "check-type", 42 | "check-typecast", 43 | "check-type-operator", 44 | "check-preblock" 45 | ], 46 | "interface-over-type-literal": true, 47 | "no-consecutive-blank-lines": true, 48 | "space-before-function-paren": [true, { 49 | "anonymous": "never", 50 | "named": "never", 51 | "asyncArrow": "always" 52 | }], 53 | "space-within-parens": [true, 0], 54 | "jsx-curly-spacing": [true, "never"], 55 | "jsx-no-multiline-js": false, 56 | "jsx-equals-spacing": false, 57 | "jsx-no-bind": true, 58 | "jsx-key": true, 59 | "jsx-no-lambda": true, 60 | "jsx-no-string-ref": true, 61 | "jsx-wrap-multiline": true, 62 | "jsx-self-close": true, 63 | "cognitive-complexity": false, 64 | "no-duplicate-string": false, 65 | "no-big-function": false, 66 | "no-small-switch": false, 67 | "max-union-size": [true, 20], 68 | "parameters-max-number": false 69 | } 70 | } 71 | --------------------------------------------------------------------------------