Repository: arve0/markdown-it-attrs (js)

Issue:
markdown-it-highlight lines conflict
I am using this with markdown-it-highlight lines. I transform everything into jsx for my app. When writing:

````
```js{2}
```
````

this plugin adds `2=""` to the code element. Which is invalid html and causes errors. 

Is there any way to turn off this plugin for code blocks? Or an option to configure the `{}` characters? If I could change them to `[]` I think everything would work.

Pull request description:
Configure Delimiter 
closes #63 

Patch:
diff --git a/README.md b/README.md
index b2543c9..8ae13d3 100644
--- a/README.md
+++ b/README.md
@@ -152,6 +152,21 @@ If you need finer control, [decorate](https://github.com/rstacruz/markdown-it-de
 
 Remember to [render attributes](https://github.com/arve0/markdown-it-attrs/blob/a75102ad571110659ce9545d184aa5658d2b4a06/index.js#L100) if you use a custom renderer.
 
+## Custom Delimiter
+
+You can configure `markdown-it-attrs` to use a different delimeter to declare attributes.
+
+```javascript
+md().use(attrs, {
+  leftDelimiter: '[',
+  rightDelimiter: ']'
+});
+```
+
+```markdown
+# Title [.large]
+```
+
 ## License
 
 MIT © [Arve Seljebu](http://arve0.github.io/)
diff --git a/index.js b/index.js
index 48aec4f..6157f92 100644
--- a/index.js
+++ b/index.js
@@ -1,8 +1,18 @@
 'use strict';
 
-const patterns = require('./patterns.js');
+const patternsConfig = require('./patterns.js');
 
-module.exports = function attributes(md) {
+const defaultOptions = {
+  leftDelimiter: '{',
+  rightDelimiter: '}'
+};
+
+module.exports = function attributes(md, options) {
+  if (!options) {
+    options = defaultOptions;
+  }
+
+  const patterns = patternsConfig(options); 
 
   function curlyAttrs(state) {
     let tokens = state.tokens;
diff --git a/markdown-it-attrs.browser.js b/markdown-it-attrs.browser.js
index 8bb6ab2..8d351aa 100644
--- a/markdown-it-attrs.browser.js
+++ b/markdown-it-attrs.browser.js
@@ -3,9 +3,17 @@
 
 var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
 
-var patterns = require('./patterns.js');
+var patternsConfig = require('./patterns.js');
+
+var defaultOptions = {
+  leftDelimiter: '{',
+  rightDelimiter: '}'
+};
 
 module.exports = function attributes(md) {
+  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultOptions;
+
+  var patterns = patternsConfig(options);
 
   function curlyAttrs(state) {
     var tokens = state.tokens;
@@ -219,268 +227,270 @@ function last(arr) {
 
 var utils = require('./utils.js');
 
-module.exports = [{
-  /**
-   * ```python {.cls}
-   * for i in range(10):
-   *     print(i)
-   * ```
-   */
-  name: 'fenced code blocks',
-  tests: [{
-    shift: 0,
-    block: true,
-    info: utils.hasCurly('end')
-  }],
-  transform: function transform(tokens, i) {
-    var token = tokens[i];
-    var start = token.info.lastIndexOf('{');
-    var attrs = utils.getAttrs(token.info, start);
-    utils.addAttrs(attrs, token);
-    token.info = utils.removeCurly(token.info);
-  }
-}, {
-  /**
-   * bla `click()`{.c} ![](img.png){.d}
-   *
-   * differs from 'inline attributes' as it does
-   * not have a closing tag (nesting: -1)
-   */
-  name: 'inline nesting 0',
-  tests: [{
-    shift: 0,
-    type: 'inline',
-    children: [{
-      shift: -1,
-      type: function type(str) {
-        return str === 'image' || str === 'code_inline';
-      }
-    }, {
+module.exports = function (options) {
+  return [{
+    /**
+     * ```python {.cls}
+     * for i in range(10):
+     *     print(i)
+     * ```
+     */
+    name: 'fenced code blocks',
+    tests: [{
       shift: 0,
-      type: 'text',
-      content: utils.hasCurly('start')
-    }]
-  }],
-  transform: function transform(tokens, i, j) {
-    var token = tokens[i].children[j];
-    var endChar = token.content.indexOf('}');
-    var attrToken = tokens[i].children[j - 1];
-    var attrs = utils.getAttrs(token.content, 0);
-    utils.addAttrs(attrs, attrToken);
-    if (token.content.length === endChar + 1) {
-      tokens[i].children.splice(j, 1);
-    } else {
-      token.content = token.content.slice(endChar + 1);
+      block: true,
+      info: utils.hasDelimiter('end', options)
+    }],
+    transform: function transform(tokens, i) {
+      var token = tokens[i];
+      var start = token.info.lastIndexOf(options.leftDelimiter);
+      var attrs = utils.getAttrs(token.info, start, null, options);
+      utils.addAttrs(attrs, token);
+      token.info = utils.removeDelimiter(token.info, options);
     }
-  }
-}, {
-  /**
-   * | h1 |
-   * | -- |
-   * | c1 |
-   * {.c}
-   */
-  name: 'tables',
-  tests: [{
-    // let this token be i, such that for-loop continues at
-    // next token after tokens.splice
-    shift: 0,
-    type: 'table_close'
   }, {
-    shift: 1,
-    type: 'paragraph_open'
-  }, {
-    shift: 2,
-    type: 'inline',
-    content: utils.hasCurly('only')
-  }],
-  transform: function transform(tokens, i) {
-    var token = tokens[i + 2];
-    var tableOpen = utils.getMatchingOpeningToken(tokens, i);
-    var attrs = utils.getAttrs(token.content, 0);
-    // add attributes
-    utils.addAttrs(attrs, tableOpen);
-    // remove <p>{.c}</p>
-    tokens.splice(i + 1, 3);
-  }
-}, {
-  /**
-   * *emphasis*{.with attrs=1}
-   */
-  name: 'inline attributes',
-  tests: [{
-    shift: 0,
-    type: 'inline',
-    children: [{
-      shift: -1,
-      nesting: -1 // closing inline tag, </em>{.a}
-    }, {
+    /**
+     * bla `click()`{.c} ![](img.png){.d}
+     *
+     * differs from 'inline attributes' as it does
+     * not have a closing tag (nesting: -1)
+     */
+    name: 'inline nesting 0',
+    tests: [{
       shift: 0,
-      type: 'text',
-      content: utils.hasCurly('start')
-    }]
-  }],
-  transform: function transform(tokens, i, j) {
-    var token = tokens[i].children[j];
-    var content = token.content;
-    var attrs = utils.getAttrs(content, 0);
-    var openingToken = utils.getMatchingOpeningToken(tokens[i].children, j - 1);
-    utils.addAttrs(attrs, openingToken);
-    token.content = content.slice(content.indexOf('}') + 1);
-  }
-}, {
-  /**
-   * - item
-   * {.a}
-   */
-  name: 'list softbreak',
-  tests: [{
-    shift: -2,
-    type: 'list_item_open'
+      type: 'inline',
+      children: [{
+        shift: -1,
+        type: function type(str) {
+          return str === 'image' || str === 'code_inline';
+        }
+      }, {
+        shift: 0,
+        type: 'text',
+        content: utils.hasDelimiter('start', options)
+      }]
+    }],
+    transform: function transform(tokens, i, j) {
+      var token = tokens[i].children[j];
+      var endChar = token.content.indexOf(options.rightDelimiter);
+      var attrToken = tokens[i].children[j - 1];
+      var attrs = utils.getAttrs(token.content, 0, null, options);
+      utils.addAttrs(attrs, attrToken);
+      if (token.content.length === endChar + 1) {
+        tokens[i].children.splice(j, 1);
+      } else {
+        token.content = token.content.slice(endChar + 1);
+      }
+    }
   }, {
-    shift: 0,
-    type: 'inline',
-    children: [{
-      position: -2,
-      type: 'softbreak'
+    /**
+     * | h1 |
+     * | -- |
+     * | c1 |
+     * {.c}
+     */
+    name: 'tables',
+    tests: [{
+      // let this token be i, such that for-loop continues at
+      // next token after tokens.splice
+      shift: 0,
+      type: 'table_close'
     }, {
-      position: -1,
-      content: utils.hasCurly('only')
-    }]
-  }],
-  transform: function transform(tokens, i, j) {
-    var token = tokens[i].children[j];
-    var content = token.content;
-    var attrs = utils.getAttrs(content, 0);
-    var ii = i - 2;
-    while (tokens[ii - 1] && tokens[ii - 1].type !== 'ordered_list_open' && tokens[ii - 1].type !== 'bullet_list_open') {
-      ii--;
-    }
-    utils.addAttrs(attrs, tokens[ii - 1]);
-    tokens[i].children = tokens[i].children.slice(0, -2);
-  }
-}, {
-  /**
-   * - nested list
-   *   - with double \n
-   *   {.a} <-- apply to nested ul
-   *
-   * {.b} <-- apply to root <ul>
-   */
-  name: 'list double softbreak',
-  tests: [{
-    // let this token be i = 0 so that we can erase
-    // the <p>{.a}</p> tokens below
-    shift: 0,
-    type: function type(str) {
-      return str === 'bullet_list_close' || str === 'ordered_list_close';
+      shift: 1,
+      type: 'paragraph_open'
+    }, {
+      shift: 2,
+      type: 'inline',
+      content: utils.hasDelimiter('only', options)
+    }],
+    transform: function transform(tokens, i) {
+      var token = tokens[i + 2];
+      var tableOpen = utils.getMatchingOpeningToken(tokens, i);
+      var attrs = utils.getAttrs(token.content, 0, null, options);
+      // add attributes
+      utils.addAttrs(attrs, tableOpen);
+      // remove <p>{.c}</p>
+      tokens.splice(i + 1, 3);
     }
   }, {
-    shift: 1,
-    type: 'paragraph_open'
+    /**
+     * *emphasis*{.with attrs=1}
+     */
+    name: 'inline attributes',
+    tests: [{
+      shift: 0,
+      type: 'inline',
+      children: [{
+        shift: -1,
+        nesting: -1 // closing inline tag, </em>{.a}
+      }, {
+        shift: 0,
+        type: 'text',
+        content: utils.hasDelimiter('start', options)
+      }]
+    }],
+    transform: function transform(tokens, i, j) {
+      var token = tokens[i].children[j];
+      var content = token.content;
+      var attrs = utils.getAttrs(content, 0, null, options);
+      var openingToken = utils.getMatchingOpeningToken(tokens[i].children, j - 1);
+      utils.addAttrs(attrs, openingToken);
+      token.content = content.slice(content.indexOf(options.rightDelimiter) + 1);
+    }
   }, {
-    shift: 2,
-    type: 'inline',
-    content: utils.hasCurly('only'),
-    children: function children(arr) {
-      return arr.length === 1;
+    /**
+     * - item
+     * {.a}
+     */
+    name: 'list softbreak',
+    tests: [{
+      shift: -2,
+      type: 'list_item_open'
+    }, {
+      shift: 0,
+      type: 'inline',
+      children: [{
+        position: -2,
+        type: 'softbreak'
+      }, {
+        position: -1,
+        content: utils.hasDelimiter('only', options)
+      }]
+    }],
+    transform: function transform(tokens, i, j) {
+      var token = tokens[i].children[j];
+      var content = token.content;
+      var attrs = utils.getAttrs(content, 0, null, options);
+      var ii = i - 2;
+      while (tokens[ii - 1] && tokens[ii - 1].type !== 'ordered_list_open' && tokens[ii - 1].type !== 'bullet_list_open') {
+        ii--;
+      }
+      utils.addAttrs(attrs, tokens[ii - 1]);
+      tokens[i].children = tokens[i].children.slice(0, -2);
     }
   }, {
-    shift: 3,
-    type: 'paragraph_close'
-  }],
-  transform: function transform(tokens, i) {
-    var token = tokens[i + 2];
-    var content = token.content;
-    var attrs = utils.getAttrs(content, 0);
-    var openingToken = utils.getMatchingOpeningToken(tokens, i);
-    utils.addAttrs(attrs, openingToken);
-    tokens.splice(i + 1, 3);
-  }
-}, {
-  /**
-   * - end of {.list-item}
-   */
-  name: 'list item end',
-  tests: [{
-    shift: -2,
-    type: 'list_item_open'
+    /**
+     * - nested list
+     *   - with double \n
+     *   {.a} <-- apply to nested ul
+     *
+     * {.b} <-- apply to root <ul>
+     */
+    name: 'list double softbreak',
+    tests: [{
+      // let this token be i = 0 so that we can erase
+      // the <p>{.a}</p> tokens below
+      shift: 0,
+      type: function type(str) {
+        return str === 'bullet_list_close' || str === 'ordered_list_close';
+      }
+    }, {
+      shift: 1,
+      type: 'paragraph_open'
+    }, {
+      shift: 2,
+      type: 'inline',
+      content: utils.hasDelimiter('only', options),
+      children: function children(arr) {
+        return arr.length === 1;
+      }
+    }, {
+      shift: 3,
+      type: 'paragraph_close'
+    }],
+    transform: function transform(tokens, i) {
+      var token = tokens[i + 2];
+      var content = token.content;
+      var attrs = utils.getAttrs(content, 0, null, options);
+      var openingToken = utils.getMatchingOpeningToken(tokens, i);
+      utils.addAttrs(attrs, openingToken);
+      tokens.splice(i + 1, 3);
+    }
   }, {
-    shift: 0,
-    type: 'inline',
-    children: [{
-      position: -1,
-      content: utils.hasCurly('end')
-    }]
-  }],
-  transform: function transform(tokens, i, j) {
-    var token = tokens[i].children[j];
-    var content = token.content;
-    var attrs = utils.getAttrs(content, content.lastIndexOf('{'));
-    utils.addAttrs(attrs, tokens[i - 2]);
-    var trimmed = content.slice(0, content.lastIndexOf('{'));
-    token.content = last(trimmed) !== ' ' ? trimmed : trimmed.slice(0, -1);
-  }
-}, {
-  /**
-   * something with softbreak
-   * {.cls}
-   */
-  name: '\n{.a} softbreak then curly in start',
-  tests: [{
-    shift: 0,
-    type: 'inline',
-    children: [{
-      position: -2,
-      type: 'softbreak'
+    /**
+     * - end of {.list-item}
+     */
+    name: 'list item end',
+    tests: [{
+      shift: -2,
+      type: 'list_item_open'
     }, {
-      position: -1,
-      type: 'text',
-      content: utils.hasCurly('only')
-    }]
-  }],
-  transform: function transform(tokens, i, j) {
-    var token = tokens[i].children[j];
-    var attrs = utils.getAttrs(token.content, 0);
-    // find last closing tag
-    var ii = i + 1;
-    while (tokens[ii + 1] && tokens[ii + 1].nesting === -1) {
-      ii++;
+      shift: 0,
+      type: 'inline',
+      children: [{
+        position: -1,
+        content: utils.hasDelimiter('end', options)
+      }]
+    }],
+    transform: function transform(tokens, i, j) {
+      var token = tokens[i].children[j];
+      var content = token.content;
+      var attrs = utils.getAttrs(content, content.lastIndexOf(options.leftDelimiter), null, options);
+      utils.addAttrs(attrs, tokens[i - 2]);
+      var trimmed = content.slice(0, content.lastIndexOf(options.leftDelimiter));
+      token.content = last(trimmed) !== ' ' ? trimmed : trimmed.slice(0, -1);
     }
-    var openingToken = utils.getMatchingOpeningToken(tokens, ii);
-    utils.addAttrs(attrs, openingToken);
-    tokens[i].children = tokens[i].children.slice(0, -2);
-  }
-}, {
-  /**
-   * end of {.block}
-   */
-  name: 'end of block',
-  tests: [{
-    shift: 0,
-    type: 'inline',
-    children: [{
-      position: -1,
-      content: utils.hasCurly('end'),
-      type: function type(t) {
-        return t !== 'code_inline';
+  }, {
+    /**
+     * something with softbreak
+     * {.cls}
+     */
+    name: '\n{.a} softbreak then curly in start',
+    tests: [{
+      shift: 0,
+      type: 'inline',
+      children: [{
+        position: -2,
+        type: 'softbreak'
+      }, {
+        position: -1,
+        type: 'text',
+        content: utils.hasDelimiter('only', options)
+      }]
+    }],
+    transform: function transform(tokens, i, j) {
+      var token = tokens[i].children[j];
+      var attrs = utils.getAttrs(token.content, 0, null, options);
+      // find last closing tag
+      var ii = i + 1;
+      while (tokens[ii + 1] && tokens[ii + 1].nesting === -1) {
+        ii++;
       }
-    }]
-  }],
-  transform: function transform(tokens, i, j) {
-    var token = tokens[i].children[j];
-    var content = token.content;
-    var attrs = utils.getAttrs(content, content.lastIndexOf('{'));
-    var ii = i + 1;
-    while (tokens[ii + 1] && tokens[ii + 1].nesting === -1) {
-      ii++;
+      var openingToken = utils.getMatchingOpeningToken(tokens, ii);
+      utils.addAttrs(attrs, openingToken);
+      tokens[i].children = tokens[i].children.slice(0, -2);
     }
-    var openingToken = utils.getMatchingOpeningToken(tokens, ii);
-    utils.addAttrs(attrs, openingToken);
-    var trimmed = content.slice(0, content.lastIndexOf('{'));
-    token.content = last(trimmed) !== ' ' ? trimmed : trimmed.slice(0, -1);
-  }
-}];
+  }, {
+    /**
+     * end of {.block}
+     */
+    name: 'end of block',
+    tests: [{
+      shift: 0,
+      type: 'inline',
+      children: [{
+        position: -1,
+        content: utils.hasDelimiter('end', options),
+        type: function type(t) {
+          return t !== 'code_inline';
+        }
+      }]
+    }],
+    transform: function transform(tokens, i, j) {
+      var token = tokens[i].children[j];
+      var content = token.content;
+      var attrs = utils.getAttrs(content, content.lastIndexOf(options.leftDelimiter), null, options);
+      var ii = i + 1;
+      while (tokens[ii + 1] && tokens[ii + 1].nesting === -1) {
+        ii++;
+      }
+      var openingToken = utils.getMatchingOpeningToken(tokens, ii);
+      utils.addAttrs(attrs, openingToken);
+      var trimmed = content.slice(0, content.lastIndexOf(options.leftDelimiter));
+      token.content = last(trimmed) !== ' ' ? trimmed : trimmed.slice(0, -1);
+    }
+  }];
+};
 
 // get last element of array or string
 function last(arr) {
@@ -496,7 +506,7 @@ function last(arr) {
  * @returns {2d array}: [['key', 'val'], ['class', 'red']]
  */
 
-exports.getAttrs = function (str, start, end) {
+exports.getAttrs = function (str, start, end, options) {
   // TODO: do not require `end`, stop when } is found
   // not tab, line feed, form feed, space, solidus, greater than sign, quotation mark, apostrophe and equals sign
   var allowedKeyChars = /[^\t\n\f />"'=]/;
@@ -504,7 +514,7 @@ exports.getAttrs = function (str, start, end) {
   var keySeparator = '=';
   var classChar = '.';
   var idChar = '#';
-  var endChar = '}';
+  var endChar = options.rightDelimiter;
 
   var attrs = [];
   var key = '';
@@ -618,7 +628,7 @@ exports.addAttrs = function (attrs, token) {
  * @param {string} where to expect {} curly. start, middle, end or only.
  * @return {function(string)} Function which testes if string has curly.
  */
-exports.hasCurly = function (where) {
+exports.hasDelimiter = function (where, options) {
 
   if (!where) {
     throw new Error('Parameter `where` not passed. Should be "start", "middle", "end" or "only".');
@@ -646,26 +656,26 @@ exports.hasCurly = function (where) {
     switch (where) {
       case 'start':
         // first char should be {, } found in char 2 or more
-        start = str.charAt(0) === '{' ? 0 : -1;
-        end = start === -1 ? -1 : str.indexOf('}', start + minCurlyLength - 1);
+        start = str.charAt(0) === options.leftDelimiter ? 0 : -1;
+        end = start === -1 ? -1 : str.indexOf(options.rightDelimiter, start + minCurlyLength - 1);
         break;
 
       case 'middle':
         // 'a{.b}'
-        start = str.indexOf('{', 1);
-        end = start === -1 ? -1 : str.indexOf('}', start + minCurlyLength - 1);
+        start = str.indexOf(options.leftDelimiter, 1);
+        end = start === -1 ? -1 : str.indexOf(options.rightDelimiter, start + minCurlyLength - 1);
         break;
 
       case 'end':
         // last char should be }
-        end = str.charAt(str.length - 1) === '}' ? str.length - 1 : -1;
-        start = end === -1 ? -1 : str.lastIndexOf('{');
+        end = str.charAt(str.length - 1) === options.rightDelimiter ? str.length - 1 : -1;
+        start = end === -1 ? -1 : str.lastIndexOf(options.leftDelimiter);
         break;
 
       case 'only':
         // '{.a}'
-        start = str.charAt(0) === '{' ? 0 : -1;
-        end = str.charAt(str.length - 1) === '}' ? str.length - 1 : -1;
+        start = str.charAt(0) === options.leftDelimiter ? 0 : -1;
+        end = str.charAt(str.length - 1) === options.rightDelimiter ? str.length - 1 : -1;
         break;
     }
 
@@ -673,11 +683,18 @@ exports.hasCurly = function (where) {
   };
 };
 
+function escapeRegExp(s) {
+  return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
+}
+
 /**
  * Removes last curly from string.
  */
-exports.removeCurly = function (str) {
-  var curly = /[ \n]?{[^{}}]+}$/;
+exports.removeDelimiter = function (str, options) {
+  var start = escapeRegExp(options.leftDelimiter);
+  var end = escapeRegExp(options.rightDelimiter);
+
+  var curly = new RegExp('[ \\n]?' + start + '[^' + start + end + end + ']+' + end + '$');
   var pos = str.search(curly);
 
   return pos !== -1 ? str.slice(0, pos) : str;
diff --git a/patterns.js b/patterns.js
index f79beae..13a24f5 100644
--- a/patterns.js
+++ b/patterns.js
@@ -6,7 +6,7 @@
 
 const utils = require('./utils.js');
 
-module.exports = [
+module.exports = options => ([
   {
     /**
      * ```python {.cls}
@@ -19,15 +19,15 @@ module.exports = [
       {
         shift: 0,
         block: true,
-        info: utils.hasCurly('end')
+        info: utils.hasDelimiter('end', options)
       }
     ],
     transform: (tokens, i) => {
       let token = tokens[i];
-      let start = token.info.lastIndexOf('{');
-      let attrs = utils.getAttrs(token.info, start);
+      let start = token.info.lastIndexOf(options.leftDelimiter);
+      let attrs = utils.getAttrs(token.info, start, null, options);
       utils.addAttrs(attrs, token);
-      token.info = utils.removeCurly(token.info);
+      token.info = utils.removeDelimiter(token.info, options);
     }
   }, {
     /**
@@ -48,16 +48,16 @@ module.exports = [
           }, {
             shift: 0,
             type: 'text',
-            content: utils.hasCurly('start')
+            content: utils.hasDelimiter('start', options)
           }
         ]
       }
     ],
     transform: (tokens, i, j) => {
       let token = tokens[i].children[j];
-      let endChar = token.content.indexOf('}');
+      let endChar = token.content.indexOf(options.rightDelimiter);
       let attrToken = tokens[i].children[j - 1];
-      let attrs = utils.getAttrs(token.content, 0);
+      let attrs = utils.getAttrs(token.content, 0, null, options);
       utils.addAttrs(attrs, attrToken);
       if (token.content.length === (endChar + 1)) {
         tokens[i].children.splice(j, 1);
@@ -85,13 +85,13 @@ module.exports = [
       }, {
         shift: 2,
         type: 'inline',
-        content: utils.hasCurly('only')
+        content: utils.hasDelimiter('only', options)
       }
     ],
     transform: (tokens, i) => {
       let token = tokens[i + 2];
       let tableOpen = utils.getMatchingOpeningToken(tokens, i);
-      let attrs = utils.getAttrs(token.content, 0);
+      let attrs = utils.getAttrs(token.content, 0, null, options);
       // add attributes
       utils.addAttrs(attrs, tableOpen);
       // remove <p>{.c}</p>
@@ -113,7 +113,7 @@ module.exports = [
           }, {
             shift: 0,
             type: 'text',
-            content: utils.hasCurly('start')
+            content: utils.hasDelimiter('start', options)
           }
         ]
       }
@@ -121,10 +121,10 @@ module.exports = [
     transform: (tokens, i, j) => {
       let token = tokens[i].children[j];
       let content = token.content;
-      let attrs = utils.getAttrs(content, 0);
+      let attrs = utils.getAttrs(content, 0, null, options);
       let openingToken = utils.getMatchingOpeningToken(tokens[i].children, j - 1);
       utils.addAttrs(attrs, openingToken);
-      token.content = content.slice(content.indexOf('}') + 1);
+      token.content = content.slice(content.indexOf(options.rightDelimiter) + 1);
     }
   }, {
     /**
@@ -145,7 +145,7 @@ module.exports = [
             type: 'softbreak'
           }, {
             position: -1,
-            content: utils.hasCurly('only')
+            content: utils.hasDelimiter('only', options)
           }
         ]
       }
@@ -153,7 +153,7 @@ module.exports = [
     transform: (tokens, i, j) => {
       let token = tokens[i].children[j];
       let content = token.content;
-      let attrs = utils.getAttrs(content, 0);
+      let attrs = utils.getAttrs(content, 0, null, options);
       let ii = i - 2;
       while (tokens[ii - 1] &&
         tokens[ii - 1].type !== 'ordered_list_open' &&
@@ -184,7 +184,7 @@ module.exports = [
       }, {
         shift: 2,
         type: 'inline',
-        content: utils.hasCurly('only'),
+        content: utils.hasDelimiter('only', options),
         children: (arr) => arr.length === 1
       }, {
         shift: 3,
@@ -194,7 +194,7 @@ module.exports = [
     transform: (tokens, i) => {
       let token = tokens[i + 2];
       let content = token.content;
-      let attrs = utils.getAttrs(content, 0);
+      let attrs = utils.getAttrs(content, 0, null, options);
       let openingToken = utils.getMatchingOpeningToken(tokens, i);
       utils.addAttrs(attrs, openingToken);
       tokens.splice(i + 1, 3);
@@ -214,7 +214,7 @@ module.exports = [
         children: [
           {
             position: -1,
-            content: utils.hasCurly('end')
+            content: utils.hasDelimiter('end', options)
           }
         ]
       }
@@ -222,9 +222,9 @@ module.exports = [
     transform: (tokens, i, j) => {
       let token = tokens[i].children[j];
       let content = token.content;
-      let attrs = utils.getAttrs(content, content.lastIndexOf('{'));
+      let attrs = utils.getAttrs(content, content.lastIndexOf(options.leftDelimiter), null, options);
       utils.addAttrs(attrs, tokens[i - 2]);
-      let trimmed = content.slice(0, content.lastIndexOf('{'));
+      let trimmed = content.slice(0, content.lastIndexOf(options.leftDelimiter));
       token.content = last(trimmed) !== ' ' ?
         trimmed : trimmed.slice(0, -1);
     }
@@ -245,14 +245,14 @@ module.exports = [
           }, {
             position: -1,
             type: 'text',
-            content: utils.hasCurly('only')
+            content: utils.hasDelimiter('only', options)
           }
         ]
       }
     ],
     transform: (tokens, i, j) => {
       let token = tokens[i].children[j];
-      let attrs = utils.getAttrs(token.content, 0);
+      let attrs = utils.getAttrs(token.content, 0, null, options);
       // find last closing tag
       let ii = i + 1;
       while (tokens[ii + 1] && tokens[ii + 1].nesting === -1) { ii++; }
@@ -272,7 +272,7 @@ module.exports = [
         children: [
           {
             position: -1,
-            content: utils.hasCurly('end'),
+            content: utils.hasDelimiter('end', options),
             type: (t) => t !== 'code_inline'
           }
         ]
@@ -281,17 +281,17 @@ module.exports = [
     transform: (tokens, i, j) => {
       let token = tokens[i].children[j];
       let content = token.content;
-      let attrs = utils.getAttrs(content, content.lastIndexOf('{'));
+      let attrs = utils.getAttrs(content, content.lastIndexOf(options.leftDelimiter), null, options);
       let ii = i + 1;
       while (tokens[ii + 1] && tokens[ii + 1].nesting === -1) { ii++; }
       let openingToken = utils.getMatchingOpeningToken(tokens, ii);
       utils.addAttrs(attrs, openingToken);
-      let trimmed = content.slice(0, content.lastIndexOf('{'));
+      let trimmed = content.slice(0, content.lastIndexOf(options.leftDelimiter));
       token.content = last(trimmed) !== ' ' ?
         trimmed : trimmed.slice(0, -1);
     }
   }
-];
+]);
 
 // get last element of array or string
 function last(arr) {
diff --git a/utils.js b/utils.js
index 35e077b..75945a3 100644
--- a/utils.js
+++ b/utils.js
@@ -5,7 +5,7 @@
  * @param {int} start: where to start parsing (including {)
  * @returns {2d array}: [['key', 'val'], ['class', 'red']]
  */
-exports.getAttrs = function (str, start, end) {
+exports.getAttrs = function (str, start, end, options) {
   // TODO: do not require `end`, stop when } is found
   // not tab, line feed, form feed, space, solidus, greater than sign, quotation mark, apostrophe and equals sign
   const allowedKeyChars = /[^\t\n\f />"'=]/;
@@ -13,7 +13,7 @@ exports.getAttrs = function (str, start, end) {
   const keySeparator = '=';
   const classChar = '.';
   const idChar = '#';
-  const endChar = '}';
+  const endChar = options.rightDelimiter;
 
   const attrs = [];
   let key = '';
@@ -125,7 +125,7 @@ exports.addAttrs = function (attrs, token) {
  * @param {string} where to expect {} curly. start, middle, end or only.
  * @return {function(string)} Function which testes if string has curly.
  */
-exports.hasCurly = function (where) {
+exports.hasDelimiter = function (where, options) {
 
   if (!where) {
     throw new Error('Parameter `where` not passed. Should be "start", "middle", "end" or "only".');
@@ -154,26 +154,26 @@ exports.hasCurly = function (where) {
     switch (where) {
     case 'start':
       // first char should be {, } found in char 2 or more
-      start = str.charAt(0) === '{' ? 0 : -1;
-      end = start === -1 ? -1 : str.indexOf('}', start + minCurlyLength - 1);
+      start = str.charAt(0) === options.leftDelimiter ? 0 : -1;
+      end = start === -1 ? -1 : str.indexOf(options.rightDelimiter, start + minCurlyLength - 1);
       break;
 
     case 'middle':
       // 'a{.b}'
-      start = str.indexOf('{', 1);
-      end = start === -1 ? -1 : str.indexOf('}', start + minCurlyLength - 1);
+      start = str.indexOf(options.leftDelimiter, 1);
+      end = start === -1 ? -1 : str.indexOf(options.rightDelimiter, start + minCurlyLength - 1);
       break;
 
     case 'end':
       // last char should be }
-      end = str.charAt(str.length - 1) === '}' ? str.length - 1 : -1;
-      start = end === -1 ? -1 : str.lastIndexOf('{');
+      end = str.charAt(str.length - 1) === options.rightDelimiter ? str.length - 1 : -1;
+      start = end === -1 ? -1 : str.lastIndexOf(options.leftDelimiter);
       break;
 
     case 'only':
       // '{.a}'
-      start = str.charAt(0) === '{' ? 0 : -1;
-      end = str.charAt(str.length - 1) === '}' ? str.length - 1 : -1;
+      start = str.charAt(0) === options.leftDelimiter ? 0 : -1;
+      end = str.charAt(str.length - 1) === options.rightDelimiter ? str.length - 1 : -1;
       break;
     }
 
@@ -181,11 +181,20 @@ exports.hasCurly = function (where) {
   };
 };
 
+function escapeRegExp(s) {
+  return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
+}
+
 /**
  * Removes last curly from string.
  */
-exports.removeCurly = function (str) {
-  let curly = /[ \n]?{[^{}}]+}$/;
+exports.removeDelimiter = function (str, options) {
+  const start = escapeRegExp(options.leftDelimiter);
+  const end = escapeRegExp(options.rightDelimiter);
+
+  let curly = new RegExp(
+    '[ \\n]?' + start + '[^' + start + end + end + ']+' + end + '$'
+  );
   let pos = str.search(curly);
 
   return pos !== -1 ? str.slice(0, pos) : str;


Test patch:
diff --git a/test.js b/test.js
index 5de5da9..68595ca 100644
--- a/test.js
+++ b/test.js
@@ -6,11 +6,26 @@ const implicitFigures = require('markdown-it-implicit-figures');
 const attrs = require('./');
 const utils = require('./utils.js');
 
+const options = {
+  leftDelimiter: '{',
+  rightDelimiter: '}'
+};
+
 describe('markdown-it-attrs.utils', () => {
   it('should parse {.class ..css-module #id key=val}', () => {
     let src = '{.red ..mod #head key=val}';
     let expected = [['class', 'red'], ['css-module', 'mod'], ['id', 'head'], ['key', 'val']];
-    let res = utils.getAttrs(src, 0);
+    let res = utils.getAttrs(src, 0, null, options);
+    assert.deepEqual(res, expected);
+  });
+
+  it('should parse [.class ..css-module #id key=val]', () => {
+    let src = '[.red ..mod #head key=val]';
+    let expected = [['class', 'red'], ['css-module', 'mod'], ['id', 'head'], ['key', 'val']];
+    let res = utils.getAttrs(src, 0, null, {
+      leftDelimiter: '[',
+      rightDelimiter: ']'
+    });
     assert.deepEqual(res, expected);
   });
 });
@@ -285,5 +300,18 @@ describe('markdown-it-attrs', () => {
     expected = '<p>text {#}</p>\n';
     assert.equal(md.render(src), expected);
   });
+
+  it('should support configuring delimeter', () => {
+    md = Md().use(attrs, {
+      leftDelimiter: '[',
+      rightDelimiter: ']'
+    });
+
+    src = '- item[.red]\n[.blue]';
+    expected = '<ul class="blue">\n';
+    expected += '<li class="red">item</li>\n';
+    expected += '</ul>\n';
+    assert.equal(md.render(src), expected);
+  });
 });