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