├── .gitignore ├── tests ├── simple.md ├── simple.html ├── nested.md ├── nested.html ├── README.md └── index.js ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | tmp 4 | *.log* 5 | -------------------------------------------------------------------------------- /tests/simple.md: -------------------------------------------------------------------------------- 1 | [text in the span]{#id .class .other-class key=val another=example} 2 | -------------------------------------------------------------------------------- /tests/simple.html: -------------------------------------------------------------------------------- 1 |
text in the span
2 | -------------------------------------------------------------------------------- /tests/nested.md: -------------------------------------------------------------------------------- 1 | # example [span text in the header]{#h1 .class .other-class key=val another=example} 2 | 3 | some text [some other text]{.class .other-class key=val another=example} 4 | 5 | more text [some other text]{.class .other-class key=val another=example} with text at the end 6 | -------------------------------------------------------------------------------- /tests/nested.html: -------------------------------------------------------------------------------- 1 |some text some other text
3 |more text some other text with text at the end
4 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | Tests are run using [tape](https://npmjs.com/tape). 4 | 5 | Linting is performed using [standard](https://npmjs.com/standard) 6 | 7 | ## Running tests 8 | 9 | ```sh 10 | git clone {this repo} 11 | cd {this repo} 12 | npm install 13 | npm test 14 | ``` 15 | 16 | `npm test` runs both the linter and the tests. 17 | 18 | ### Just run the linter 19 | 20 | ```sh 21 | npm run lint 22 | ``` 23 | 24 | ### Only run the tests 25 | 26 | ```sh 27 | npm run test:no-lint 28 | ``` 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remark-bracketed-spans", 3 | "version": "3.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "dependencies": { 10 | "unist-util-remove": "^0.2.1", 11 | "unist-util-visit": "^1.1.1" 12 | }, 13 | "devDependencies": { 14 | "rehype-remark": "github:sethvincent/rehype-remark", 15 | "remark": "^7.0.0", 16 | "remark-html": "^6.0.0", 17 | "remark-stringify": "^3.0.0", 18 | "rehype": "^4.0.0", 19 | "standard": "^9.0.0", 20 | "tape": "^4.6.3" 21 | }, 22 | "scripts": { 23 | "test": "standard && node tests/index.js" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/sethvincent/remark-bracketed-spans.git" 28 | }, 29 | "keywords": [ 30 | "remark", 31 | "pandoc", 32 | "bracketed", 33 | "spans" 34 | ], 35 | "author": "sethvincent", 36 | "license": "ISC", 37 | "bugs": { 38 | "url": "https://github.com/sethvincent/remark-bracketed-spans/issues" 39 | }, 40 | "homepage": "https://github.com/sethvincent/remark-bracketed-spans#readme" 41 | } 42 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | 4 | var test = require('tape') 5 | 6 | var remark = require('remark') 7 | var toHTML = require('remark-html') 8 | var toMarkdown = require('remark-stringify') 9 | var rehype = require('rehype') 10 | var rehype2remark = require('rehype-remark') 11 | 12 | var bracketedSpans = require('../index') 13 | 14 | test('bracketed span', function (t) { 15 | var md = fs.readFileSync(path.join(__dirname, 'simple.md'), 'utf8') 16 | var html = remark().use(bracketedSpans).use(toHTML).processSync(md).toString() 17 | var expectedHTML = fs.readFileSync(path.join(__dirname, 'simple.html'), 'utf8') 18 | var outputMD = rehype().use(bracketedSpans.html2md).use(rehype2remark).use(toMarkdown).use(bracketedSpans.mdVisitors).processSync(html).toString() 19 | t.equal(html, expectedHTML) 20 | t.equal(outputMD, md) 21 | t.end() 22 | }) 23 | 24 | test('nested bracketed span', function (t) { 25 | var md = fs.readFileSync(path.join(__dirname, 'nested.md'), 'utf8') 26 | var html = remark().use(bracketedSpans).use(toHTML).processSync(md).toString() 27 | var expected = fs.readFileSync(path.join(__dirname, 'nested.html'), 'utf8') 28 | var outputMD = rehype().use(bracketedSpans.html2md).use(rehype2remark).use(toMarkdown).use(bracketedSpans.mdVisitors).processSync(html).toString() 29 | t.equal(html, expected) 30 | t.equal(outputMD, md) 31 | t.end() 32 | }) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # remark-bracketed-spans 2 | 3 | Add an id, classes, and data attributes to `` tags in markdown. 4 | 5 | [![npm][npm-image]][npm-url] 6 | [![travis][travis-image]][travis-url] 7 | [![standard][standard-image]][standard-url] 8 | [![conduct][conduct]][conduct-url] 9 | 10 | [npm-image]: https://img.shields.io/npm/v/remark-bracketed-spans.svg?style=flat-square 11 | [npm-url]: https://www.npmjs.com/package/remark-bracketed-spans 12 | [travis-image]: https://img.shields.io/travis/sethvincent/remark-bracketed-spans.svg?style=flat-square 13 | [travis-url]: https://travis-ci.org/sethvincent/remark-bracketed-spans 14 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 15 | [standard-url]: http://npm.im/standard 16 | [conduct]: https://img.shields.io/badge/code%20of%20conduct-contributor%20covenant-green.svg?style=flat-square 17 | [conduct-url]: CONDUCT.md 18 | 19 | ## About 20 | 21 | A [remark](http://npmjs.com/remark) plugin for adding attributes to span tags in markdown that works even when the span is nested inside other markdown elements. 22 | 23 | Usage looks like this: 24 | 25 | ```md 26 | [text in the span]{.class .other-class key=val another=example} 27 | ``` 28 | 29 | And results in HTML like this: 30 | 31 | ```html 32 |text in the span
33 | ``` 34 | 35 | ## Install 36 | 37 | ```sh 38 | npm install --save remark-bracketed-spans 39 | ``` 40 | 41 | ## Usage 42 | 43 | This module is a [remark](http://npmjs.com/remark) plugin, and can be used like this: 44 | 45 | ```js 46 | var remark = require('remark') 47 | var toHTML = require('remark-html') 48 | var bracketedSpans = require('remark-bracketed-spans') 49 | 50 | var md = '[text in the span]{.class .other-class key=val another=example}' 51 | 52 | var html = remark().use(bracketedSpans).use(toHTML).processSync(md).toString() 53 | 54 | console.log(html) 55 | ``` 56 | 57 | ## License 58 | 59 | [ISC](LICENSE.md) 60 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var visit = require('unist-util-visit') 2 | var remove = require('unist-util-remove') 3 | 4 | function md2html () { 5 | return function transformer (tree) { 6 | visit(tree, function visitor (node, i, parent) { 7 | if (!node.children) return 8 | var data = parseMarkdown(node, i, parent, tree) 9 | 10 | if (data) { 11 | parent.children[i] = { 12 | type: 'html', 13 | value: data.html 14 | } 15 | 16 | if (data.trailingText) { 17 | parent.children[i + 1] = { 18 | type: 'text', 19 | value: data.trailingText 20 | } 21 | } else { 22 | remove(parent, parent.children[i + 1]) 23 | } 24 | } 25 | }) 26 | } 27 | } 28 | 29 | /* 30 | * if a bracket span statement is found: returns an object 31 | * with id, classList, attr, and children properties 32 | * else returns false 33 | */ 34 | function parseMarkdown (node, i, parent, tree) { 35 | if (!node.children) return false 36 | 37 | if (node.type && 38 | node.type === 'linkReference' && 39 | node.children && 40 | parent.children[i + 1].value && 41 | parent.children[i + 1].value.match(/\{(.+)\}/).length 42 | ) { 43 | var text = node.children && node.children[0] && node.children[0].value 44 | var value = parent.children[i + 1].value 45 | var data = { 46 | id: null, 47 | classList: [], 48 | attr: {} 49 | } 50 | 51 | var idMatch = value.match(/#\w+/) 52 | var classMatch = value.match(/\.\w+(-)?\w+/g) 53 | var attrMatch = value.match(/(?:\w*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^}\s]+))/g) 54 | 55 | if (idMatch) { 56 | data.id = idMatch[0].replace('#', '') 57 | } 58 | 59 | if (classMatch) { 60 | data.classList = classMatch.map(function (cls) { 61 | return cls.replace('.', '') 62 | }) 63 | } 64 | 65 | if (attrMatch) { 66 | attrMatch.map(function (item) { 67 | var split = item.split('=') 68 | var key = split[0].trim().replace('{', '') 69 | var val = split[1].trim().replace(/"/g, '') 70 | data.attr[key] = val 71 | }) 72 | } 73 | 74 | data.text = text 75 | data.trailingText = value.split('}')[1] 76 | data.html = createSpan(data) 77 | return data 78 | } else { 79 | return false 80 | } 81 | } 82 | 83 | function createSpan (data) { 84 | var classes = data.classList ? data.classList.join(' ') : '' 85 | var text = data.text 86 | var id = data.id 87 | 88 | var attr = Object.keys(data.attr).map(function (key) { 89 | return `data-${key}="${data.attr[key]}"` 90 | }).join(' ') 91 | 92 | return `${text}` 93 | } 94 | 95 | function html2md () { 96 | return function transformer (tree) { 97 | visit(tree, visitor) 98 | } 99 | 100 | function visitor (node, index, parent) { 101 | if ( 102 | node.tagName && 103 | node.tagName === 'span' && 104 | node.properties && 105 | ( 106 | node.properties.className || 107 | node.properties.id || 108 | hasDataAttr(node.properties) 109 | ) 110 | ) { 111 | var props = node.properties 112 | var text = '[' + node.children[0].value + ']{' 113 | var attrs = [] 114 | if (props.id) { 115 | attrs.push('#' + props.id) 116 | delete props.id 117 | } 118 | 119 | if (props.className) { 120 | attrs.push(props.className.map(function (name) { 121 | return '.' + name 122 | }).join(' ')) 123 | delete props.className 124 | } 125 | 126 | var dataKeys = Object.keys(props) 127 | 128 | dataKeys.forEach(function (key) { 129 | var attrkey = key.replace('data', '').toLowerCase() 130 | attrs.push(attrkey + '=' + props[key]) 131 | }) 132 | 133 | text += attrs.join(' ') + '}' 134 | 135 | parent.children[index] = { 136 | type: 'text', 137 | value: text.trim() 138 | } 139 | } 140 | } 141 | } 142 | 143 | function hasDataAttr (props) { 144 | if (!props) return false 145 | var keys = Object.keys(props) 146 | if (!keys.length) return false 147 | var i = 0 148 | for (i; i < keys.length; i++) { 149 | var key = keys[i] 150 | if (key.indexOf('data') === 0) { 151 | return true 152 | } 153 | } 154 | return false 155 | } 156 | 157 | /* clean up md output */ 158 | function mdVisitors () { 159 | var processor = this 160 | var Compiler = processor.Compiler 161 | var visitors = Compiler.prototype.visitors 162 | var text = visitors.text 163 | 164 | /* Add a visitor for `heading`s. */ 165 | visitors.text = function (node, parent) { 166 | var textMatch = node.value.match(/\[(.*?)\]/) 167 | var bracketMatch = node.value.match(/\{(.+)\}/) 168 | if ( 169 | textMatch && 170 | bracketMatch 171 | ) { 172 | node.value = node.value.replace(/\//g, '') 173 | return node.value.replace(/\/+?(?=\[)/g, '') 174 | } else { 175 | return text.apply(this, arguments) 176 | } 177 | } 178 | } 179 | 180 | module.exports = md2html 181 | md2html.html2md = html2md 182 | md2html.mdVisitors = mdVisitors 183 | md2html.parseMarkdown = parseMarkdown 184 | --------------------------------------------------------------------------------