├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .huskyrc.js
├── .lintstagedrc.js
├── .npmignore
├── LICENCE
├── README.md
├── dist
├── themes
│ └── default.styl
├── vuepress-plugin-tabs.cjs.js
└── vuepress-plugin-tabs.esm.js
├── package.json
├── rollup.config.js
├── src
├── index.js
├── tab.js
├── tabs.js
└── util.js
├── test
└── util.test.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "env", {
5 | "modules": false
6 | }
7 | ]
8 | ],
9 | "env": {
10 | "test": {
11 | "plugins": ["transform-es2015-modules-commonjs"]
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 |
13 | [.babelrc]
14 | indent_style = space
15 | indent_size = 2
16 |
17 | [*.js]
18 | indent_style = space
19 | indent_size = 2
20 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | 'plugin:jest/recommended',
10 | ],
11 | "parserOptions": {
12 | "ecmaVersion": 2018,
13 | "sourceType": "module"
14 | },
15 | 'plugins': [ 'jest' ],
16 | "rules": {
17 | "indent": [
18 | "error",
19 | 2,
20 | {
21 | "SwitchCase": 1
22 | }
23 | ],
24 | "linebreak-style": [
25 | "error",
26 | "unix"
27 | ]
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # npm modules
2 | node_modules
3 |
4 | # filesystem
5 | .DS_Store
6 |
7 | # editors
8 | .vscode
9 | .idea
10 |
11 | # error logs
12 | yarn-error.log
13 |
--------------------------------------------------------------------------------
/.huskyrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "hooks": {
3 | "pre-commit": "yarn lint-staged"
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/.lintstagedrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "linters" : {
3 | "*.js": [
4 | "yarn eslint"
5 | ]
6 | },
7 | "ignore": [
8 | "**/dist/*.js"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pskordilakis/vuepress-plugin-tabs/f8651e301cb4b84f07869995526d8738796692a1/.npmignore
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Panagiotis Skordilakis
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 | # Vuepress Plugin Tabs
2 |
3 | Tabs Container for Vuepress
4 |
5 | Expose [vue-tabs-component](https://github.com/spatie/vue-tabs-component) as custom markdown container
6 |
7 | Used with version >= 1.x.x of Vuepress. For version 0.x use [vuepress-tabs](https://github.com/pskordilakis/vuepress-tabs)
8 |
9 | ## Installation
10 |
11 | ``` bash
12 | yarn add vuepress-plugin-tabs vue-tabs-component
13 | ```
14 |
15 | or
16 |
17 | ``` bash
18 | npm install vuepress-plugin-tabs vue-tabs-component
19 | ```
20 |
21 | Enable plugin in .vuepress/config.js
22 |
23 | ``` js
24 | module.exports = {
25 | plugins: [ 'tabs' ]
26 | }
27 | ```
28 |
29 | import theme in .vuepress/styles/index.styl
30 |
31 | ``` stylus
32 | @require '~vuepress-plugin-tabs/dist/themes/default.styl'
33 | ```
34 |
35 | ## Usage
36 |
37 | ~~~ md
38 | :::: tabs
39 |
40 | ::: tab title
41 | __markdown content__
42 | :::
43 |
44 |
45 | ::: tab javascript
46 | ``` javascript
47 | () => {
48 | console.log('Javascript code example')
49 | }
50 | ```
51 | :::
52 |
53 | ::::
54 |
55 | ~~~
56 |
57 | ### Tabs attributes
58 |
59 | Everything after tabs will be passed to tabs component as attributes.
60 |
61 | ~~~ md
62 | :::: tabs cache-lifetime="10" :options="{ useUrlFragment: false }"
63 |
64 | ::: tab "Tab Title" id="first-tab"
65 | __markdown content__
66 | :::
67 |
68 |
69 | ::: tab javascript id="second-tab"
70 | ``` javascript
71 | () => {
72 | console.log('JavaScript code example')
73 | }
74 | ```
75 | :::
76 |
77 | ::::
78 |
79 | ~~~
80 |
81 |
82 | ### Tab attributes
83 |
84 | Everything after tab will be passed to tab component as attributes.
85 | Any value that does not have a name will be passed as the name attribute. Multiword names must be enclosed in quotes.
86 | Only one such value is valid.
87 |
88 | ~~~ md
89 | :::: tabs
90 |
91 | ::: tab "Tab Title" id="first-tab"
92 | __markdown content__
93 | :::
94 |
95 |
96 | ::: tab javascript id="second-tab"
97 | ``` javascript
98 | () => {
99 | console.log('JavaScript code example')
100 | }
101 | ```
102 | :::
103 |
104 | ::::
105 |
106 | ~~~
107 |
--------------------------------------------------------------------------------
/dist/themes/default.styl:
--------------------------------------------------------------------------------
1 | .tabs-component
2 | margin 2em 0
3 |
4 | .tabs-component-tabs
5 | border solid 1px #ddd
6 | border-radius 6px
7 | margin-bottom 5px
8 | padding-left 0
9 |
10 | .tabs-component-tab
11 | color #999
12 | font-size 14px
13 | font-weight 600
14 | margin-right 0
15 | list-style none
16 | &:hover
17 | color: #666
18 | &.is-active
19 | color $accentColor
20 | &.is-disabled *
21 | color #cdcdcd
22 | cursor not-allowed !important
23 |
24 | .tabs-component-tab-a
25 | align-items center
26 | color inherit
27 | display flex
28 | padding .25em .5em
29 | text-decoration none
30 |
31 | .tabs-component-panels
32 | padding 1em 0
33 |
34 | @media (min-width: 700px)
35 | .tabs-component-tabs
36 | border 0
37 | align-items stretch
38 | display flex
39 | justify-content flex-start
40 | margin-bottom -1px
41 |
42 | .tabs-component-tab
43 | background-color #fff
44 | border solid 1px #ddd
45 | border-radius 3px 3px 0 0
46 | margin-right .25em
47 | transition transform .3s ease
48 | &.is-active
49 | border-bottom solid 1px #fff
50 | z-index 2
51 | transform translateY(0)
52 |
53 | .tabs-component-panels
54 | border-top-left-radius 0
55 | background-color #fff
56 | border solid 1px #ddd
57 | border-radius 0 6px 6px 6px
58 | box-shadow 0 0 10px rgba(0, 0, 0, .05)
59 | padding 1em 1em
60 |
--------------------------------------------------------------------------------
/dist/vuepress-plugin-tabs.cjs.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var slicedToArray = function () {
4 | function sliceIterator(arr, i) {
5 | var _arr = [];
6 | var _n = true;
7 | var _d = false;
8 | var _e = undefined;
9 |
10 | try {
11 | for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
12 | _arr.push(_s.value);
13 |
14 | if (i && _arr.length === i) break;
15 | }
16 | } catch (err) {
17 | _d = true;
18 | _e = err;
19 | } finally {
20 | try {
21 | if (!_n && _i["return"]) _i["return"]();
22 | } finally {
23 | if (_d) throw _e;
24 | }
25 | }
26 |
27 | return _arr;
28 | }
29 |
30 | return function (arr, i) {
31 | if (Array.isArray(arr)) {
32 | return arr;
33 | } else if (Symbol.iterator in Object(arr)) {
34 | return sliceIterator(arr, i);
35 | } else {
36 | throw new TypeError("Invalid attempt to destructure non-iterable instance");
37 | }
38 | };
39 | }();
40 |
41 | // Map to keep track of used ids
42 | var tabIds = new Map();
43 |
44 | function dedupeId(id) {
45 | var normalizedId = String(id).toLowerCase().replace(' ', '-');
46 | var currentValue = !tabIds.has(normalizedId) ? 1 : tabIds.get(normalizedId) + 1;
47 | tabIds.set(normalizedId, currentValue);
48 |
49 | return normalizedId + '-' + currentValue;
50 | }
51 |
52 | function tabAttributes(val) {
53 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
54 |
55 | var attributes = val
56 | // sanitize input
57 | .trim().slice("tab".length).trim()
58 | // parse into array
59 | .split(/ +(?=(?:(?:[^"]*"){2})*[^"]*$)/g)
60 | // normalize name attribute
61 | .map(function (attr) {
62 | if (!attr.includes("=")) {
63 | if (!attr.startsWith('"')) {
64 | attr = '"' + attr;
65 | }
66 |
67 | if (!attr.endsWith('"')) {
68 | attr = attr + '"';
69 | }
70 |
71 | return 'name=' + attr;
72 | }
73 |
74 | return attr;
75 | });
76 |
77 | if (options.dedupeIds) {
78 | var idIndex = attributes.findIndex(function (attr) {
79 | return attr.startsWith('id=');
80 | });
81 | var nameIndex = attributes.findIndex(function (attr) {
82 | return attr.startsWith('name=');
83 | });
84 |
85 | if (idIndex !== -1) {
86 | var id = attributes[idIndex];
87 |
88 | var _id$split = id.split('='),
89 | _id$split2 = slicedToArray(_id$split, 2),
90 | idValue = _id$split2[1];
91 |
92 | attributes[idIndex] = 'id="' + dedupeId(idValue.substring(1, idValue.length - 1)) + '"';
93 | } else {
94 | var name = attributes[nameIndex];
95 |
96 | var _name$split = name.split('='),
97 | _name$split2 = slicedToArray(_name$split, 2),
98 | nameValue = _name$split2[1];
99 |
100 | attributes.unshift('id="' + dedupeId(nameValue.substring(1, nameValue.length - 1)) + '"');
101 | }
102 | }
103 |
104 | return attributes.join(" ");
105 | }
106 |
107 | function tabsAttributes(val) {
108 | return val
109 | // sanitize input
110 | .trim().slice("tabs".length).trim();
111 | }
112 |
113 | function defaultTabsAttributes(attributes) {
114 | var attributesString = [];
115 | if (!attributes || Object.keys(attributes).length === 0) {
116 | return '';
117 | }
118 |
119 | for (var key in attributes) {
120 | var substring = ':' + key + '=\'' + JSON.stringify(attributes[key]) + '\'';
121 | attributesString.push(substring);
122 | }
123 |
124 | return attributesString.join(' ');
125 | }
126 |
127 | var container = require('markdown-it-container');
128 |
129 | var tabs = (function (md, options) {
130 | md.use(container, 'tabs', {
131 | render: function render(tokens, idx) {
132 | var token = tokens[idx];
133 | var defaultAttributes = defaultTabsAttributes(options.tabsAttributes);
134 | var attributes = tabsAttributes(token.info);
135 |
136 | if (token.nesting === 1) {
137 | return '\n';
138 | } else {
139 | return '\n';
140 | }
141 | }
142 | });
143 | });
144 |
145 | var container$1 = require('markdown-it-container');
146 |
147 | var tab = (function (md, options) {
148 | md.use(container$1, 'tab', {
149 | render: function render(tokens, idx) {
150 | var token = tokens[idx];
151 | var attributes = tabAttributes(token.info, options);
152 |
153 | if (token.nesting === 1) {
154 | return '\n';
155 | } else {
156 | return '\n';
157 | }
158 | }
159 | });
160 | });
161 |
162 | module.exports = function (opts) {
163 | var defaultOptions = {
164 | dedupeIds: false
165 | };
166 |
167 | var options = Object.assign({}, defaultOptions, opts);
168 |
169 | return {
170 | enhanceAppFiles: [{
171 | name: 'register-vue-tabs-component',
172 | content: 'import Tabs from \'vue-tabs-component\';export default ({ Vue }) => Vue.use(Tabs)'
173 | }],
174 | extendMarkdown: function extendMarkdown(md) {
175 | tabs(md, options);
176 | tab(md, options);
177 | }
178 | };
179 | };
180 |
--------------------------------------------------------------------------------
/dist/vuepress-plugin-tabs.esm.js:
--------------------------------------------------------------------------------
1 | var slicedToArray = function () {
2 | function sliceIterator(arr, i) {
3 | var _arr = [];
4 | var _n = true;
5 | var _d = false;
6 | var _e = undefined;
7 |
8 | try {
9 | for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
10 | _arr.push(_s.value);
11 |
12 | if (i && _arr.length === i) break;
13 | }
14 | } catch (err) {
15 | _d = true;
16 | _e = err;
17 | } finally {
18 | try {
19 | if (!_n && _i["return"]) _i["return"]();
20 | } finally {
21 | if (_d) throw _e;
22 | }
23 | }
24 |
25 | return _arr;
26 | }
27 |
28 | return function (arr, i) {
29 | if (Array.isArray(arr)) {
30 | return arr;
31 | } else if (Symbol.iterator in Object(arr)) {
32 | return sliceIterator(arr, i);
33 | } else {
34 | throw new TypeError("Invalid attempt to destructure non-iterable instance");
35 | }
36 | };
37 | }();
38 |
39 | // Map to keep track of used ids
40 | var tabIds = new Map();
41 |
42 | function dedupeId(id) {
43 | var normalizedId = String(id).toLowerCase().replace(' ', '-');
44 | var currentValue = !tabIds.has(normalizedId) ? 1 : tabIds.get(normalizedId) + 1;
45 | tabIds.set(normalizedId, currentValue);
46 |
47 | return normalizedId + '-' + currentValue;
48 | }
49 |
50 | function tabAttributes(val) {
51 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
52 |
53 | var attributes = val
54 | // sanitize input
55 | .trim().slice("tab".length).trim()
56 | // parse into array
57 | .split(/ +(?=(?:(?:[^"]*"){2})*[^"]*$)/g)
58 | // normalize name attribute
59 | .map(function (attr) {
60 | if (!attr.includes("=")) {
61 | if (!attr.startsWith('"')) {
62 | attr = '"' + attr;
63 | }
64 |
65 | if (!attr.endsWith('"')) {
66 | attr = attr + '"';
67 | }
68 |
69 | return 'name=' + attr;
70 | }
71 |
72 | return attr;
73 | });
74 |
75 | if (options.dedupeIds) {
76 | var idIndex = attributes.findIndex(function (attr) {
77 | return attr.startsWith('id=');
78 | });
79 | var nameIndex = attributes.findIndex(function (attr) {
80 | return attr.startsWith('name=');
81 | });
82 |
83 | if (idIndex !== -1) {
84 | var id = attributes[idIndex];
85 |
86 | var _id$split = id.split('='),
87 | _id$split2 = slicedToArray(_id$split, 2),
88 | idValue = _id$split2[1];
89 |
90 | attributes[idIndex] = 'id="' + dedupeId(idValue.substring(1, idValue.length - 1)) + '"';
91 | } else {
92 | var name = attributes[nameIndex];
93 |
94 | var _name$split = name.split('='),
95 | _name$split2 = slicedToArray(_name$split, 2),
96 | nameValue = _name$split2[1];
97 |
98 | attributes.unshift('id="' + dedupeId(nameValue.substring(1, nameValue.length - 1)) + '"');
99 | }
100 | }
101 |
102 | return attributes.join(" ");
103 | }
104 |
105 | function tabsAttributes(val) {
106 | return val
107 | // sanitize input
108 | .trim().slice("tabs".length).trim();
109 | }
110 |
111 | function defaultTabsAttributes(attributes) {
112 | var attributesString = [];
113 | if (!attributes || Object.keys(attributes).length === 0) {
114 | return '';
115 | }
116 |
117 | for (var key in attributes) {
118 | var substring = ':' + key + '=\'' + JSON.stringify(attributes[key]) + '\'';
119 | attributesString.push(substring);
120 | }
121 |
122 | return attributesString.join(' ');
123 | }
124 |
125 | var container = require('markdown-it-container');
126 |
127 | var tabs = (function (md, options) {
128 | md.use(container, 'tabs', {
129 | render: function render(tokens, idx) {
130 | var token = tokens[idx];
131 | var defaultAttributes = defaultTabsAttributes(options.tabsAttributes);
132 | var attributes = tabsAttributes(token.info);
133 |
134 | if (token.nesting === 1) {
135 | return '\n';
136 | } else {
137 | return '\n';
138 | }
139 | }
140 | });
141 | });
142 |
143 | var container$1 = require('markdown-it-container');
144 |
145 | var tab = (function (md, options) {
146 | md.use(container$1, 'tab', {
147 | render: function render(tokens, idx) {
148 | var token = tokens[idx];
149 | var attributes = tabAttributes(token.info, options);
150 |
151 | if (token.nesting === 1) {
152 | return '\n';
153 | } else {
154 | return '\n';
155 | }
156 | }
157 | });
158 | });
159 |
160 | module.exports = function (opts) {
161 | var defaultOptions = {
162 | dedupeIds: false
163 | };
164 |
165 | var options = Object.assign({}, defaultOptions, opts);
166 |
167 | return {
168 | enhanceAppFiles: [{
169 | name: 'register-vue-tabs-component',
170 | content: 'import Tabs from \'vue-tabs-component\';export default ({ Vue }) => Vue.use(Tabs)'
171 | }],
172 | extendMarkdown: function extendMarkdown(md) {
173 | tabs(md, options);
174 | tab(md, options);
175 | }
176 | };
177 | };
178 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vuepress-plugin-tabs",
3 | "author": "Panagiotis Skordilakis
",
4 | "version": "0.3.0",
5 | "description": "Vuepress plugin tabs - markdown custom container to display content in tabs",
6 | "keywords": [
7 | "vuepress",
8 | "tabs"
9 | ],
10 | "homepage": "https://github.com/pskordilakis/vuepress-plugin-tabs",
11 | "bugs": "https://github.com/pskordilakis/vuepress-plugin-tabs/issues",
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/pskordilakis/vuepress-plugin-tabs.git"
15 | },
16 | "license": "MIT",
17 | "main": "dist/vuepress-plugin-tabs.cjs.js",
18 | "module": "dist/vuepress-plugin-tabs.esm.js",
19 | "devDependencies": {
20 | "babel-core": "^6.26.3",
21 | "babel-plugin-external-helpers": "^6.22.0",
22 | "babel-preset-env": "^1.7.0",
23 | "eslint": "^5.2.0",
24 | "eslint-plugin-jest": "^21.22.0",
25 | "husky": "^1.0.0-rc.13",
26 | "jest": "^23.6.0",
27 | "lint-staged": "^7.2.0",
28 | "markdown-it-container": "^2.0.0",
29 | "rollup": "^0.64.1",
30 | "rollup-plugin-babel": "^3.0.7",
31 | "rollup-plugin-node-resolve": "^3.3.0"
32 | },
33 | "scripts": {
34 | "build": "rollup -c",
35 | "dev": "rollup -c -w",
36 | "test": "jest --notify",
37 | "test-watch": "jest --watch --notify",
38 | "lint": "eslint . --ext=js"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel'
2 | import resolve from 'rollup-plugin-node-resolve'
3 |
4 | export default [
5 | {
6 | input: 'src/index.js',
7 | output: [
8 | {
9 | file: 'dist/vuepress-plugin-tabs.cjs.js',
10 | format: 'cjs',
11 | name: 'vuepress-plugin-tabs',
12 | external: [ 'markdown-it-container' ]
13 | },
14 | {
15 | file: 'dist/vuepress-plugin-tabs.esm.js',
16 | format: 'esm',
17 | name: 'vuepress-plugin-tabs',
18 | external: [ 'markdown-it-container' ]
19 | },
20 | ],
21 | plugins: [
22 | resolve({
23 | main: true,
24 | }),
25 | babel({
26 | exclude: ['node_modules/**'],
27 | plugins: ['external-helpers'],
28 | }),
29 | ],
30 | },
31 | ]
32 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import tabs from './tabs'
2 | import tab from './tab'
3 |
4 | module.exports = (opts) => {
5 | const defaultOptions = {
6 | dedupeIds: false
7 | }
8 |
9 | const options = Object.assign({}, defaultOptions, opts)
10 |
11 | return {
12 | enhanceAppFiles: [
13 | {
14 | name: 'register-vue-tabs-component',
15 | content: `import Tabs from 'vue-tabs-component';export default ({ Vue }) => Vue.use(Tabs)`
16 | }
17 | ],
18 | extendMarkdown: md => {
19 | tabs(md, options)
20 | tab(md, options)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/tab.js:
--------------------------------------------------------------------------------
1 | import { tabAttributes } from './util'
2 | const container = require('markdown-it-container')
3 |
4 | export default (md, options) => {
5 | md.use(container, 'tab', {
6 | render: (tokens, idx) => {
7 | const token = tokens[idx]
8 | const attributes = tabAttributes(token.info, options)
9 |
10 | if (token.nesting === 1) {
11 | return `\n`
12 | } else {
13 | return `\n`
14 | }
15 | }
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/src/tabs.js:
--------------------------------------------------------------------------------
1 | import { tabsAttributes, defaultTabsAttributes } from './util'
2 | const container = require('markdown-it-container')
3 |
4 | export default (md, options) => {
5 | md.use(container, 'tabs', {
6 | render: (tokens, idx) => {
7 | const token = tokens[idx]
8 | const defaultAttributes = defaultTabsAttributes(options.tabsAttributes)
9 | const attributes = tabsAttributes (token.info)
10 |
11 | if (token.nesting === 1) {
12 | return `\n`
13 | } else {
14 | return `\n`
15 | }
16 | }
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | // Map to keep track of used ids
2 | const tabIds = new Map();
3 |
4 | export function dedupeId(id) {
5 | const normalizedId = String(id).toLowerCase().replace(' ', '-')
6 | let currentValue = !tabIds.has(normalizedId) ? 1 : (tabIds.get(normalizedId) + 1);
7 | tabIds.set(normalizedId, currentValue);
8 |
9 | return `${normalizedId}-${currentValue}`;
10 | }
11 |
12 | export function tabAttributes(val, options = {}) {
13 | let attributes = val
14 | // sanitize input
15 | .trim()
16 | .slice("tab".length)
17 | .trim()
18 | // parse into array
19 | .split(/ +(?=(?:(?:[^"]*"){2})*[^"]*$)/g)
20 | // normalize name attribute
21 | .map(attr => {
22 | if (!attr.includes("=")) {
23 | if (!attr.startsWith('"')) {
24 | attr = `"${attr}`;
25 | }
26 |
27 | if (!attr.endsWith('"')) {
28 | attr = `${attr}"`;
29 | }
30 |
31 | return `name=${attr}`;
32 | }
33 |
34 | return attr;
35 | });
36 |
37 | if (options.dedupeIds) {
38 | const idIndex = attributes.findIndex(attr => attr.startsWith('id='));
39 | const nameIndex = attributes.findIndex(attr => attr.startsWith('name='));
40 |
41 |
42 | if (idIndex !== -1) {
43 | const id = attributes[idIndex];
44 | const [ , idValue ] = id.split('=');
45 | attributes[idIndex] = `id="${dedupeId(idValue.substring(1, idValue.length - 1))}"`;
46 | } else {
47 | const name = attributes[nameIndex];
48 | const [ , nameValue ] = name.split('=');
49 | attributes.unshift(`id="${dedupeId(nameValue.substring(1, nameValue.length - 1))}"`);
50 | }
51 | }
52 |
53 | return attributes.join(" ");
54 | }
55 |
56 | export function tabsAttributes(val) {
57 | return (
58 | val
59 | // sanitize input
60 | .trim()
61 | .slice("tabs".length)
62 | .trim()
63 | );
64 | }
65 |
66 | export function defaultTabsAttributes(attributes) {
67 | let attributesString = []
68 | if (!attributes || Object.keys(attributes).length === 0) {
69 | return ''
70 | }
71 |
72 | for (const key in attributes) {
73 | const substring = `:${key}='${JSON.stringify(attributes[key])}'`
74 | attributesString.push(substring)
75 | }
76 |
77 | return attributesString.join(' ')
78 | }
79 |
--------------------------------------------------------------------------------
/test/util.test.js:
--------------------------------------------------------------------------------
1 | import { tabAttributes, tabsAttributes, dedupeId, defaultTabsAttributes } from '../src/util'
2 |
3 | describe('tabAttributes', () => {
4 | test('must handle sorthand name attributes', () => {
5 | expect(tabAttributes('tab title')).toBe('name="title"')
6 | expect(tabAttributes('tab "My Title"')).toBe('name="My Title"')
7 | })
8 |
9 | test('must handle html attributes', () => {
10 | expect(tabAttributes('tab id="tab-id"')).toBe('id="tab-id"')
11 | expect(tabAttributes('tab name="some-name"')).toBe('name="some-name"')
12 | })
13 |
14 | test('must handle vue binded attributes', () => {
15 | expect(tabAttributes('tab :id="tabId"')).toBe(':id="tabId"')
16 | })
17 |
18 | test('must handle mixed attributes', () => {
19 | expect(tabAttributes('tab :id="tabId" class="some-class"')).toBe(':id="tabId" class="some-class"')
20 | })
21 |
22 | test('must handle sorthand name and mixed attributes', () => {
23 | expect(tabAttributes('tab title :id="tabId" class="some-class"')).toBe('name="title" :id="tabId" class="some-class"')
24 | expect(tabAttributes('tab "My Title" :id="tabId" class="some-class"')).toBe('name="My Title" :id="tabId" class="some-class"')
25 | })
26 | })
27 |
28 | describe('tabsAttributes', () => {
29 | test('must handle html attributes', () => {
30 | expect(tabsAttributes('tabs cache-lifetime="10"')).toBe('cache-lifetime="10"')
31 | })
32 |
33 | test('must handle vue binded attributes', () => {
34 | expect(tabsAttributes('tabs :options="{ useUrlFragment: false }"')).toBe(':options="{ useUrlFragment: false }"')
35 | })
36 |
37 | test('must handle mixed attributes', () => {
38 | expect(tabsAttributes('tabs cache-lifetime="10" :options="{ useUrlFragment: false }"')).toBe('cache-lifetime="10" :options="{ useUrlFragment: false }"')
39 | })
40 | })
41 |
42 | describe('dedupeId', () => {
43 | test('must add a number suffix if called with same parameter', () => {
44 | [...Array(5).keys()].map(i => i + 1).forEach(i => {
45 | expect(dedupeId('id')).toBe(`id-${i}`)
46 | });
47 | })
48 | })
49 |
50 | describe('defaultTabsAttributes', () => {
51 | test('must transform object to vue binded attributes', () => {
52 | expect(
53 | defaultTabsAttributes({ options: { foo: 'bar', bar: 123 }, baz: 123 })
54 | ).toBe(':options=\'{"foo":"bar","bar":123}\' :baz=\'123\'')
55 | })
56 | test('must transform plain object to empty string', () => {
57 | expect(
58 | defaultTabsAttributes({})
59 | ).toBe('')
60 | })
61 | })
62 |
--------------------------------------------------------------------------------