├── .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 |
50 |
51 |
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 |
--------------------------------------------------------------------------------