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

Issue:
support delimiters with length above 1
Setup:
```js
md.use(attrs, {
  leftDelimiter: '{{{',
  rightDelimiter: '}}}'
})
```
Markdown:
```md
this is the markdown I'm trying to parse {{{.replace-me}}}
```

Current output:
```html
<p>this is the markdown I'm trying to parse {{{.replace-me}}}</p>
```

Expected output:
```html
<p class="replace-me">this is the markdown I'm trying to parse</p>
```

Pull request description:
support delimiters with length > 1
Fixes #65 and #66.

Patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index e039a34..50f7654 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -2,7 +2,7 @@
   "version": "0.2.0",
   "configurations": [
     {
-      "name": "Launch",
+      "name": "Debug",
       "type": "node",
       "request": "launch",
       "program": "${workspaceRoot}/debug.js",
@@ -20,27 +20,6 @@
       "console": "internalConsole",
       "sourceMaps": false,
       "outFiles": []
-    },
-    {
-      "name": "Attach",
-      "type": "node",
-      "request": "attach",
-      "port": 5858,
-      "address": "localhost",
-      "restart": false,
-      "sourceMaps": false,
-      "outFiles": [],
-      "localRoot": "${workspaceRoot}",
-      "remoteRoot": null
-    },
-    {
-      "name": "Attach to Process",
-      "type": "node",
-      "request": "attach",
-      "processId": "${command.PickProcess}",
-      "port": 5858,
-      "sourceMaps": false,
-      "outFiles": []
     }
   ]
 }
diff --git a/debug.js b/debug.js
index 53d3604..654ad6d 100644
--- a/debug.js
+++ b/debug.js
@@ -2,9 +2,12 @@
 const md = require('markdown-it')();
 const markdownItAttrs = require('./');
 
-md.use(markdownItAttrs).use(require('../markdown-it-implicit-figures'));
+md.use(markdownItAttrs, {
+  leftDelimiter: '{{',
+  rightDelimiter: '}}'
+}).use(require('../markdown-it-implicit-figures'));
 
-let src = '> quote\n{.c}';
+let src = 'asdf *asd*{{.c}} khg';
 
 let res = md.render(src);
 
diff --git a/index.js b/index.js
index 6157f92..2a4e2e6 100644
--- a/index.js
+++ b/index.js
@@ -12,7 +12,7 @@ module.exports = function attributes(md, options) {
     options = defaultOptions;
   }
 
-  const patterns = patternsConfig(options); 
+  const patterns = patternsConfig(options);
 
   function curlyAttrs(state) {
     let tokens = state.tokens;
diff --git a/patterns.js b/patterns.js
index dc1346c..bb147db 100644
--- a/patterns.js
+++ b/patterns.js
@@ -6,327 +6,332 @@
 
 const utils = require('./utils.js');
 
-module.exports = options => ([
-  {
-    /**
-     * ```python {.cls}
-     * for i in range(10):
-     *     print(i)
-     * ```
-     */
-    name: 'fenced code blocks',
-    tests: [
-      {
-        shift: 0,
-        block: true,
-        info: utils.hasDelimiters('end', options)
-      }
-    ],
-    transform: (tokens, i) => {
-      let token = tokens[i];
-      let start = token.info.lastIndexOf(options.leftDelimiter);
-      let attrs = utils.getAttrs(token.info, start, options);
-      utils.addAttrs(attrs, token);
-      token.info = utils.removeDelimiter(token.info, options);
-    }
-  }, {
-    /**
-     * 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: (str) => str === 'image' || str === 'code_inline'
-          }, {
-            shift: 0,
-            type: 'text',
-            content: utils.hasDelimiters('start', options)
-          }
-        ]
+module.exports = options => {
+  const __hr = new RegExp('^ {0,3}[-*_]{3,} ?'
+                          + utils.escapeRegExp(options.leftDelimiter)
+                          + '[^' + utils.escapeRegExp(options.rightDelimiter) + ']');
+
+  return ([
+    {
+      /**
+       * ```python {.cls}
+       * for i in range(10):
+       *     print(i)
+       * ```
+       */
+      name: 'fenced code blocks',
+      tests: [
+        {
+          shift: 0,
+          block: true,
+          info: utils.hasDelimiters('end', options)
+        }
+      ],
+      transform: (tokens, i) => {
+        let token = tokens[i];
+        let start = token.info.lastIndexOf(options.leftDelimiter);
+        let attrs = utils.getAttrs(token.info, start, options);
+        utils.addAttrs(attrs, token);
+        token.info = utils.removeDelimiter(token.info, options);
       }
-    ],
-    transform: (tokens, i, j) => {
-      let token = tokens[i].children[j];
-      let endChar = token.content.indexOf(options.rightDelimiter);
-      let attrToken = tokens[i].children[j - 1];
-      let attrs = utils.getAttrs(token.content, 0, 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);
+    }, {
+      /**
+       * 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: (str) => str === 'image' || str === 'code_inline'
+            }, {
+              shift: 0,
+              type: 'text',
+              content: utils.hasDelimiters('start', options)
+            }
+          ]
+        }
+      ],
+      transform: (tokens, i, j) => {
+        let token = tokens[i].children[j];
+        let endChar = token.content.indexOf(options.rightDelimiter);
+        let attrToken = tokens[i].children[j - 1];
+        let attrs = utils.getAttrs(token.content, 0, options);
+        utils.addAttrs(attrs, attrToken);
+        if (token.content.length === (endChar + options.rightDelimiter.length)) {
+          tokens[i].children.splice(j, 1);
+        } else {
+          token.content = token.content.slice(endChar + options.rightDelimiter.length);
+        }
       }
-    }
-  }, {
-    /**
-     * | 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.hasDelimiters('only', 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.hasDelimiters('only', options)
+        }
+      ],
+      transform: (tokens, i) => {
+        let token = tokens[i + 2];
+        let tableOpen = utils.getMatchingOpeningToken(tokens, i);
+        let attrs = utils.getAttrs(token.content, 0, options);
+        // add attributes
+        utils.addAttrs(attrs, tableOpen);
+        // remove <p>{.c}</p>
+        tokens.splice(i + 1, 3);
       }
-    ],
-    transform: (tokens, i) => {
-      let token = tokens[i + 2];
-      let tableOpen = utils.getMatchingOpeningToken(tokens, i);
-      let attrs = utils.getAttrs(token.content, 0, options);
-      // 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}
-          }, {
-            shift: 0,
-            type: 'text',
-            content: utils.hasDelimiters('start', options)
-          }
-        ]
+    }, {
+      /**
+       * *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.hasDelimiters('start', options)
+            }
+          ]
+        }
+      ],
+      transform: (tokens, i, j) => {
+        let token = tokens[i].children[j];
+        let content = token.content;
+        let attrs = utils.getAttrs(content, 0, options);
+        let openingToken = utils.getMatchingOpeningToken(tokens[i].children, j - 1);
+        utils.addAttrs(attrs, openingToken);
+        token.content = content.slice(content.indexOf(options.rightDelimiter) + options.rightDelimiter.length);
       }
-    ],
-    transform: (tokens, i, j) => {
-      let token = tokens[i].children[j];
-      let content = token.content;
-      let attrs = utils.getAttrs(content, 0, options);
-      let openingToken = utils.getMatchingOpeningToken(tokens[i].children, j - 1);
-      utils.addAttrs(attrs, openingToken);
-      token.content = content.slice(content.indexOf(options.rightDelimiter) + 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.hasDelimiters('only', options)
-          }
-        ]
+    }, {
+      /**
+       * - 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.hasDelimiters('only', options)
+            }
+          ]
+        }
+      ],
+      transform: (tokens, i, j) => {
+        let token = tokens[i].children[j];
+        let content = token.content;
+        let attrs = utils.getAttrs(content, 0, options);
+        let 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);
       }
-    ],
-    transform: (tokens, i, j) => {
-      let token = tokens[i].children[j];
-      let content = token.content;
-      let attrs = utils.getAttrs(content, 0, options);
-      let 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: (str) =>
-          str === 'bullet_list_close' ||
-          str === 'ordered_list_close'
-      }, {
-        shift: 1,
-        type: 'paragraph_open'
-      }, {
-        shift: 2,
-        type: 'inline',
-        content: utils.hasDelimiters('only', options),
-        children: (arr) => arr.length === 1
-      }, {
-        shift: 3,
-        type: 'paragraph_close'
+    }, {
+      /**
+       * - 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: (str) =>
+            str === 'bullet_list_close' ||
+            str === 'ordered_list_close'
+        }, {
+          shift: 1,
+          type: 'paragraph_open'
+        }, {
+          shift: 2,
+          type: 'inline',
+          content: utils.hasDelimiters('only', options),
+          children: (arr) => arr.length === 1
+        }, {
+          shift: 3,
+          type: 'paragraph_close'
+        }
+      ],
+      transform: (tokens, i) => {
+        let token = tokens[i + 2];
+        let content = token.content;
+        let attrs = utils.getAttrs(content, 0, options);
+        let openingToken = utils.getMatchingOpeningToken(tokens, i);
+        utils.addAttrs(attrs, openingToken);
+        tokens.splice(i + 1, 3);
       }
-    ],
-    transform: (tokens, i) => {
-      let token = tokens[i + 2];
-      let content = token.content;
-      let attrs = utils.getAttrs(content, 0, options);
-      let 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'
-      }, {
-        shift: 0,
-        type: 'inline',
-        children: [
-          {
-            position: -1,
-            content: utils.hasDelimiters('end', options)
-          }
-        ]
+    }, {
+      /**
+       * - end of {.list-item}
+       */
+      name: 'list item end',
+      tests: [
+        {
+          shift: -2,
+          type: 'list_item_open'
+        }, {
+          shift: 0,
+          type: 'inline',
+          children: [
+            {
+              position: -1,
+              content: utils.hasDelimiters('end', options)
+            }
+          ]
+        }
+      ],
+      transform: (tokens, i, j) => {
+        let token = tokens[i].children[j];
+        let content = token.content;
+        let attrs = utils.getAttrs(content, content.lastIndexOf(options.leftDelimiter), options);
+        utils.addAttrs(attrs, tokens[i - 2]);
+        let trimmed = content.slice(0, content.lastIndexOf(options.leftDelimiter));
+        token.content = last(trimmed) !== ' ' ?
+          trimmed : trimmed.slice(0, -1);
       }
-    ],
-    transform: (tokens, i, j) => {
-      let token = tokens[i].children[j];
-      let content = token.content;
-      let attrs = utils.getAttrs(content, content.lastIndexOf(options.leftDelimiter), options);
-      utils.addAttrs(attrs, tokens[i - 2]);
-      let trimmed = content.slice(0, content.lastIndexOf(options.leftDelimiter));
-      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'
-          }, {
-            position: -1,
-            type: 'text',
-            content: utils.hasDelimiters('only', options)
-          }
-        ]
+    }, {
+      /**
+       * 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.hasDelimiters('only', options)
+            }
+          ]
+        }
+      ],
+      transform: (tokens, i, j) => {
+        let token = tokens[i].children[j];
+        let attrs = utils.getAttrs(token.content, 0, options);
+        // find last closing tag
+        let ii = i + 1;
+        while (tokens[ii + 1] && tokens[ii + 1].nesting === -1) { ii++; }
+        let openingToken = utils.getMatchingOpeningToken(tokens, ii);
+        utils.addAttrs(attrs, openingToken);
+        tokens[i].children = tokens[i].children.slice(0, -2);
       }
-    ],
-    transform: (tokens, i, j) => {
-      let token = tokens[i].children[j];
-      let attrs = utils.getAttrs(token.content, 0, options);
-      // find last closing tag
-      let ii = i + 1;
-      while (tokens[ii + 1] && tokens[ii + 1].nesting === -1) { ii++; }
-      let openingToken = utils.getMatchingOpeningToken(tokens, ii);
-      utils.addAttrs(attrs, openingToken);
-      tokens[i].children = tokens[i].children.slice(0, -2);
-    }
-  }, {
-    /**
-     * horizontal rule --- {#id}
-     */
-    name: 'horizontal rule',
-    tests: [
-      {
-        shift: 0,
-        type: 'paragraph_open'
-      },
-      {
-        shift: 1,
-        type: 'inline',
-        children: (arr) => arr.length === 1,
-        content: (str) => str.match(/^ {0,3}[-*_]{3,} {0,1}\{[^{]/) !== null
-      },
-      {
-        shift: 2,
-        type: 'paragraph_close'
+    }, {
+      /**
+       * horizontal rule --- {#id}
+       */
+      name: 'horizontal rule',
+      tests: [
+        {
+          shift: 0,
+          type: 'paragraph_open'
+        },
+        {
+          shift: 1,
+          type: 'inline',
+          children: (arr) => arr.length === 1,
+          content: (str) => str.match(__hr) !== null,
+        },
+        {
+          shift: 2,
+          type: 'paragraph_close'
+        }
+      ],
+      transform: (tokens, i) => {
+        let token = tokens[i];
+        token.type = 'hr';
+        token.tag = 'hr';
+        token.nesting = 0;
+        let content = tokens[i + 1].content;
+        let start = content.lastIndexOf(options.leftDelimiter);
+        token.attrs = utils.getAttrs(content, start, options);
+        token.markup = content;
+        tokens.splice(i + 1, 2);
       }
-    ],
-    transform: (tokens, i) => {
-      let token = tokens[i];
-      token.type = 'hr';
-      token.tag = 'hr';
-      token.nesting = 0;
-      let content = tokens[i + 1].content;
-      let start = content.lastIndexOf('{');
-      token.attrs = utils.getAttrs(content, start, options);
-      token.markup = content;
-      tokens.splice(i + 1, 2);
-    }
-  }, {
-    /**
-     * end of {.block}
-     */
-    name: 'end of block',
-    tests: [
-      {
-        shift: 0,
-        type: 'inline',
-        children: [
-          {
-            position: -1,
-            content: utils.hasDelimiters('end', options),
-            type: (t) => t !== 'code_inline'
-          }
-        ]
+    }, {
+      /**
+       * end of {.block}
+       */
+      name: 'end of block',
+      tests: [
+        {
+          shift: 0,
+          type: 'inline',
+          children: [
+            {
+              position: -1,
+              content: utils.hasDelimiters('end', options),
+              type: (t) => t !== 'code_inline'
+            }
+          ]
+        }
+      ],
+      transform: (tokens, i, j) => {
+        let token = tokens[i].children[j];
+        let content = token.content;
+        let attrs = utils.getAttrs(content, content.lastIndexOf(options.leftDelimiter), 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(options.leftDelimiter));
+        token.content = last(trimmed) !== ' ' ?
+          trimmed : trimmed.slice(0, -1);
       }
-    ],
-    transform: (tokens, i, j) => {
-      let token = tokens[i].children[j];
-      let content = token.content;
-      let attrs = utils.getAttrs(content, content.lastIndexOf(options.leftDelimiter), 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(options.leftDelimiter));
-      token.content = last(trimmed) !== ' ' ?
-        trimmed : trimmed.slice(0, -1);
     }
-  }
-]);
+  ]);
+};
 
 // get last element of array or string
 function last(arr) {
   return arr.slice(-1)[0];
-}
-
+}
\ No newline at end of file
diff --git a/utils.js b/utils.js
index 770b11d..f0ba535 100644
--- a/utils.js
+++ b/utils.js
@@ -12,7 +12,6 @@ exports.getAttrs = function (str, start, options) {
   const keySeparator = '=';
   const classChar = '.';
   const idChar = '#';
-  const endChar = options.rightDelimiter;
 
   const attrs = [];
   let key = '';
@@ -21,14 +20,14 @@ exports.getAttrs = function (str, start, options) {
   let valueInsideQuotes = false;
 
   // read inside {}
-  // start + 1 to avoid beginning {
+  // start + left delimiter length to avoid beginning {
   // breaks when } is found or end of string
-  for (let i = start + 1; i < str.length; i++) {
-    let char_ = str.charAt(i);
-    if (char_ === endChar) {
+  for (let i = start + options.leftDelimiter.length; i < str.length; i++) {
+    if (str.slice(i, i + options.rightDelimiter.length) === options.rightDelimiter) {
       if (key !== '') { attrs.push([key, value]); }
       break;
     }
+    let char_ = str.charAt(i);
 
     // switch to reading value if equal sign
     if (char_ === keySeparator) {
@@ -136,47 +135,51 @@ exports.hasDelimiters = function (where, options) {
    */
   return function (str) {
     // we need minimum three chars, for example {b}
-    let minCurlyLength = 3;
+    let minCurlyLength = options.leftDelimiter.length + 1 + options.rightDelimiter.length;
     if (!str || typeof str !== 'string' || str.length < minCurlyLength) {
       return false;
     }
 
     function validCurlyLength (curly) {
-      let isClass = curly.charAt(1) === '.';
-      let isId = curly.charAt(1) === '#';
+      let isClass = curly.charAt(options.leftDelimiter.length) === '.';
+      let isId = curly.charAt(options.leftDelimiter.length) === '#';
       return (isClass || isId)
         ? curly.length >= (minCurlyLength + 1)
         : curly.length >= minCurlyLength;
     }
 
-    let start, end;
+    let start, end, slice, nextChar;
+    let rightDelimiterMinimumShift = minCurlyLength - options.rightDelimiter.length;
     switch (where) {
     case 'start':
       // first char should be {, } found in char 2 or more
-      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(options.leftDelimiter, 1);
-      end = start === -1 ? -1 : str.indexOf(options.rightDelimiter, start + minCurlyLength - 1);
+      slice = str.slice(0, options.leftDelimiter.length);
+      start = slice === options.leftDelimiter ? 0 : -1;
+      end = start === -1 ? -1 : str.indexOf(options.rightDelimiter, rightDelimiterMinimumShift);
+      // check if next character is not one of the delimiters
+      nextChar = str.charAt(end + options.rightDelimiter.length);
+      if (nextChar && options.rightDelimiter.indexOf(nextChar) !== -1) {
+        end = -1;
+      }
       break;
 
     case 'end':
       // last char should be }
-      end = str.charAt(str.length - 1) === options.rightDelimiter ? str.length - 1 : -1;
-      start = end === -1 ? -1 : str.lastIndexOf(options.leftDelimiter);
+      start = str.lastIndexOf(options.leftDelimiter);
+      end = start === -1 ? -1 : str.indexOf(options.rightDelimiter, start + rightDelimiterMinimumShift);
+      end = end === str.length - options.rightDelimiter.length ? end : -1;
       break;
 
     case 'only':
       // '{.a}'
-      start = str.charAt(0) === options.leftDelimiter ? 0 : -1;
-      end = str.charAt(str.length - 1) === options.rightDelimiter ? str.length - 1 : -1;
+      slice = str.slice(0, options.leftDelimiter.length);
+      start = slice === options.leftDelimiter ? 0 : -1;
+      slice = str.slice(str.length - options.rightDelimiter.length);
+      end = slice === options.rightDelimiter ? str.length - options.rightDelimiter.length : -1;
       break;
     }
 
-    return start !== -1 && end !== -1 && validCurlyLength(str.substring(start, end + 1));
+    return start !== -1 && end !== -1 && validCurlyLength(str.substring(start, end + options.rightDelimiter.length));
   };
 };
 
@@ -205,6 +208,7 @@ exports.removeDelimiter = function (str, options) {
 function escapeRegExp (s) {
   return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
 }
+exports.escapeRegExp = escapeRegExp;
 
 /**
  * find corresponding opening block


Test patch:
diff --git a/test.js b/test.js
index 5ca0c36..e881964 100644
--- a/test.js
+++ b/test.js
@@ -6,319 +6,323 @@ const implicitFigures = require('markdown-it-implicit-figures');
 const attrs = require('./');
 const utils = require('./utils.js');
 
-const options = {
+
+describeTestsWithOptions({
   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, 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, {
-      leftDelimiter: '[',
-      rightDelimiter: ']'
+}, '');
+
+describeTestsWithOptions({
+  leftDelimiter: '[',
+  rightDelimiter: ']'
+}, ' with [ ] delimiters');
+
+describeTestsWithOptions({
+  leftDelimiter: '[[',
+  rightDelimiter: ']]'
+}, ' with [[ ]] delimiters');
+
+function describeTestsWithOptions(options, postText) {
+  describe('markdown-it-attrs.utils' + postText, () => {
+    it(replaceDelimiters('should parse {.class ..css-module #id key=val}', options), () => {
+      let src = '{.red ..mod #head key=val}';
+      let expected = [['class', 'red'], ['css-module', 'mod'], ['id', 'head'], ['key', 'val']];
+      let res = utils.getAttrs(replaceDelimiters(src, options), 0, options);
+      assert.deepEqual(res, expected);
     });
-    assert.deepEqual(res, expected);
   });
-});
 
-describe('markdown-it-attrs', () => {
-  let md, src, expected;
-  beforeEach(() => {
-    md = Md().use(attrs);
-  });
+  describe('markdown-it-attrs' + postText, () => {
+    let md, src, expected;
+    beforeEach(() => {
+      md = Md().use(attrs, options);
+    });
 
-  it('should add attributes when {} in end of last inline', () => {
-    src = 'some text {with=attrs}';
-    expected = '<p with="attrs">some text</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should add attributes when {} in end of last inline', options), () => {
+      src = 'some text {with=attrs}';
+      expected = '<p with="attrs">some text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should add attributes when {} in last line', () => {
-    src = 'some text\n{with=attrs}';
-    expected = '<p with="attrs">some text</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should not add attributes when it has too many delimiters {{}}', options), () => {
+      src = 'some text {{with=attrs}}';
+      expected = '<p>some text {{with=attrs}}</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), replaceDelimiters(expected, options));
+    });
 
-  it('should add classes with {.class} dot notation', () => {
-    src = 'some text {.green}';
-    expected = '<p class="green">some text</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should add attributes when {} in last line', options), () => {
+      src = 'some text\n{with=attrs}';
+      expected = '<p with="attrs">some text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should add css-modules with {..css-module} double dot notation', () => {
-    src = 'some text {..green}';
-    expected = '<p css-module="green">some text</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should add classes with {.class} dot notation', options), () => {
+      src = 'some text {.green}';
+      expected = '<p class="green">some text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should add identifiers with {#id} hashtag notation', () => {
-    src = 'some text {#section2}';
-    expected = '<p id="section2">some text</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should add css-modules with {..css-module} double dot notation', options), () => {
+      src = 'some text {..green}';
+      expected = '<p css-module="green">some text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should support classes, css-modules, identifiers and attributes in same {}', () => {
-    src = 'some text {attr=lorem .class ..css-module #id}';
-    expected = '<p attr="lorem" class="class" css-module="css-module" id="id">some text</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should add identifiers with {#id} hashtag notation', options), () => {
+      src = 'some text {#section2}';
+      expected = '<p id="section2">some text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should support attributes inside " {attr="lorem ipsum"}', () => {
-    src = 'some text {attr="lorem ipsum"}';
-    expected = '<p attr="lorem ipsum">some text</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should support classes, css-modules, identifiers and attributes in same {}', options), () => {
+      src = 'some text {attr=lorem .class ..css-module #id}';
+      expected = '<p attr="lorem" class="class" css-module="css-module" id="id">some text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should add classes in same class attribute {.c1 .c2} -> class="c1 c2"', () => {
-    src = 'some text {.c1 .c2}';
-    expected = '<p class="c1 c2">some text</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should support attributes inside " {attr="lorem ipsum"}', options), () => {
+      src = 'some text {attr="lorem ipsum"}';
+      expected = '<p attr="lorem ipsum">some text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should add css-modules in same css-modules attribute {..c1 ..c2} -> css-module="c1 c2"', () => {
-    src = 'some text {..c1 ..c2}';
-    expected = '<p css-module="c1 c2">some text</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should add classes in same class attribute {.c1 .c2} -> class="c1 c2"', options), () => {
+      src = 'some text {.c1 .c2}';
+      expected = '<p class="c1 c2">some text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should add nested css-modules {..c1.c2} -> css-module="c1.c2"', () => {
-    src = 'some text {..c1.c2}';
-    expected = '<p css-module="c1.c2">some text</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should add css-modules in same css-modules attribute {..c1 ..c2} -> css-module="c1 c2"', options), () => {
+      src = 'some text {..c1 ..c2}';
+      expected = '<p css-module="c1 c2">some text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should support empty inline tokens', () => {
-    src = ' 1 | 2 \n --|-- \n a | ';
-    md.render(src);  // should not crash / throw error
-  });
+    it(replaceDelimiters('should add nested css-modules {..c1.c2} -> css-module="c1.c2"', options), () => {
+      src = 'some text {..c1.c2}';
+      expected = '<p css-module="c1.c2">some text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should add classes to inline elements', () => {
-    src = 'paragraph **bold**{.red} asdf';
-    expected = '<p>paragraph <strong class="red">bold</strong> asdf</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should support empty inline tokens', options), () => {
+      src = ' 1 | 2 \n --|-- \n a | ';
+      md.render(replaceDelimiters(src, options));  // should not crash / throw error
+    });
 
-  it('should only remove last {}', () => {
-    src = '{{.red}';
-    expected = '<p class="red">{</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should add classes to inline elements', options), () => {
+      src = 'paragraph **bold**{.red} asdf';
+      expected = '<p>paragraph <strong class="red">bold</strong> asdf</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should add classes for list items', () => {
-    src = '- item 1{.red}\n- item 2';
-    expected = '<ul>\n';
-    expected += '<li class="red">item 1</li>\n';
-    expected += '<li>item 2</li>\n';
-    expected += '</ul>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should not add classes to inline elements with too many {{}}', options), () => {
+      src = 'paragraph **bold**{{.red}} asdf';
+      expected = '<p>paragraph <strong>bold</strong>{{.red}} asdf</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), replaceDelimiters(expected, options));
+    });
 
-  it('should add classes in nested lists', () => {
-    src =  '- item 1{.a}\n';
-    src += '  - nested item {.b}\n';
-    src += '  {.c}\n';
-    src += '    1. nested nested item {.d}\n';
-    src += '    {.e}\n';
-    // Adding class to top ul not supported
-    //    src += '{.f}';
-    //    expected = '<ul class="f">\n';
-    expected = '<ul>\n';
-    expected += '<li class="a">item 1\n';
-    expected += '<ul class="c">\n';
-    expected += '<li class="b">nested item\n';
-    expected += '<ol class="e">\n';
-    expected += '<li class="d">nested nested item</li>\n';
-    expected += '</ol>\n';
-    expected += '</li>\n';
-    expected += '</ul>\n';
-    expected += '</li>\n';
-    expected += '</ul>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should only remove last {}', options), () => {
+      src = '{{.red}';
+      expected = replaceDelimiters('<p class="red">{</p>\n', options);
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should work with nested inline elements', () => {
-    src = '- **bold *italics*{.blue}**{.green}';
-    expected = '<ul>\n';
-    expected += '<li><strong class="green">bold <em class="blue">italics</em></strong></li>\n';
-    expected += '</ul>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should add classes for list items', options), () => {
+      src = '- item 1{.red}\n- item 2';
+      expected = '<ul>\n';
+      expected += '<li class="red">item 1</li>\n';
+      expected += '<li>item 2</li>\n';
+      expected += '</ul>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should add class to inline code block', () => {
-    src = 'bla `click()`{.c}';
-    expected = '<p>bla <code class="c">click()</code></p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should add classes in nested lists', options), () => {
+      src =  '- item 1{.a}\n';
+      src += '  - nested item {.b}\n';
+      src += '  {.c}\n';
+      src += '    1. nested nested item {.d}\n';
+      src += '    {.e}\n';
+      // Adding class to top ul not supported
+      //    src += '{.f}';
+      //    expected = '<ul class="f">\n';
+      expected = '<ul>\n';
+      expected += '<li class="a">item 1\n';
+      expected += '<ul class="c">\n';
+      expected += '<li class="b">nested item\n';
+      expected += '<ol class="e">\n';
+      expected += '<li class="d">nested nested item</li>\n';
+      expected += '</ol>\n';
+      expected += '</li>\n';
+      expected += '</ul>\n';
+      expected += '</li>\n';
+      expected += '</ul>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should not trim unrelated white space', () => {
-    src = '- **bold** text {.red}';
-    expected = '<ul>\n';
-    expected += '<li class="red"><strong>bold</strong> text</li>\n';
-    expected += '</ul>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should work with nested inline elements', options), () => {
+      src = '- **bold *italics*{.blue}**{.green}';
+      expected = '<ul>\n';
+      expected += '<li><strong class="green">bold <em class="blue">italics</em></strong></li>\n';
+      expected += '</ul>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should not create empty attributes', () => {
-    src = 'text { .red }';
-    expected = '<p class="red">text</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should add class to inline code block', options), () => {
+      src = 'bla `click()`{.c}';
+      expected = '<p>bla <code class="c">click()</code></p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should add attributes to ul when below last bullet point', () => {
-    src = '- item1\n- item2\n{.red}';
-    expected = '<ul class="red">\n<li>item1</li>\n<li>item2</li>\n</ul>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should not trim unrelated white space', options), () => {
+      src = '- **bold** text {.red}';
+      expected = '<ul>\n';
+      expected += '<li class="red"><strong>bold</strong> text</li>\n';
+      expected += '</ul>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should add classes for both last list item and ul', () => {
-    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);
-  });
+    it(replaceDelimiters('should not create empty attributes', options), () => {
+      src = 'text { .red }';
+      expected = '<p class="red">text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should work with ordered lists', () => {
-    src = '1. item\n{.blue}';
-    expected = '<ol class="blue">\n';
-    expected += '<li>item</li>\n';
-    expected += '</ol>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should add attributes to ul when below last bullet point', options), () => {
+      src = '- item1\n- item2\n{.red}';
+      expected = '<ul class="red">\n<li>item1</li>\n<li>item2</li>\n</ul>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should work with typography enabled', () => {
-    src = 'text {key="val with spaces"}';
-    expected = '<p key="val with spaces">text</p>\n';
-    let res = md.set({ typographer: true }).render(src);
-    assert.equal(res, expected);
-  });
+    it(replaceDelimiters('should add classes for both last list item and ul', options), () => {
+      src = '- item{.red}\n{.blue}';
+      expected = '<ul class="blue">\n';
+      expected += '<li class="red">item</li>\n';
+      expected += '</ul>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should support code blocks', () => {
-    src = '```{.c a=1 #ii}\nfor i in range(10):\n```';
-    expected = '<pre><code class="c" a="1" id="ii">for i in range(10):\n</code></pre>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should work with ordered lists', options), () => {
+      src = '1. item\n{.blue}';
+      expected = '<ol class="blue">\n';
+      expected += '<li>item</li>\n';
+      expected += '</ol>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should support code blocks with language defined', () => {
-    src = '```python {.c a=1 #ii}\nfor i in range(10):\n```';
-    expected = '<pre><code class="c language-python" a="1" id="ii">for i in range(10):\n</code></pre>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should work with typography enabled', options), () => {
+      src = 'text {key="val with spaces"}';
+      expected = '<p key="val with spaces">text</p>\n';
+      let res = md.set({ typographer: true }).render(replaceDelimiters(src, options));
+      assert.equal(res, expected);
+    });
 
-  it('should support blockquotes', () => {
-    src = '> quote\n{.c}';
-    expected = '<blockquote class="c">\n<p>quote</p>\n</blockquote>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should support code blocks', options), () => {
+      src = '```{.c a=1 #ii}\nfor i in range(10):\n```';
+      expected = '<pre><code class="c" a="1" id="ii">for i in range(10):\n</code></pre>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should support tables', () => {
-    src = '| h1 | h2 |\n';
-    src += '| -- | -- |\n';
-    src += '| c1 | c1 |\n';
-    src += '{.c}';
-    expected = '<table class="c">\n';
-    expected += '<thead>\n';
-    expected += '<tr>\n';
-    expected += '<th>h1</th>\n';
-    expected += '<th>h2</th>\n';
-    expected += '</tr>\n';
-    expected += '</thead>\n';
-    expected += '<tbody>\n';
-    expected += '<tr>\n';
-    expected += '<td>c1</td>\n';
-    expected += '<td>c1</td>\n';
-    expected += '</tr>\n';
-    expected += '</tbody>\n';
-    expected += '</table>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should support code blocks with language defined', options), () => {
+      src = '```python {.c a=1 #ii}\nfor i in range(10):\n```';
+      expected = '<pre><code class="c language-python" a="1" id="ii">for i in range(10):\n</code></pre>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should support nested lists', () => {
-    src =  '- item\n';
-    src += '  - nested\n';
-    src += '  {.red}\n';
-    src += '\n';
-    src += '{.blue}\n';
-    expected = '<ul class="blue">\n';
-    expected += '<li>item\n';
-    expected += '<ul class="red">\n';
-    expected += '<li>nested</li>\n';
-    expected += '</ul>\n';
-    expected += '</li>\n';
-    expected += '</ul>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should support blockquotes', options), () => {
+      src = '> quote\n{.c}';
+      expected = '<blockquote class="c">\n<p>quote</p>\n</blockquote>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should support images', () => {
-    src =  '![alt](img.png){.a}';
-    expected = '<p><img src="img.png" alt="alt" class="a"></p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should support tables', options), () => {
+      src = '| h1 | h2 |\n';
+      src += '| -- | -- |\n';
+      src += '| c1 | c1 |\n';
+      src += '{.c}';
+      expected = '<table class="c">\n';
+      expected += '<thead>\n';
+      expected += '<tr>\n';
+      expected += '<th>h1</th>\n';
+      expected += '<th>h2</th>\n';
+      expected += '</tr>\n';
+      expected += '</thead>\n';
+      expected += '<tbody>\n';
+      expected += '<tr>\n';
+      expected += '<td>c1</td>\n';
+      expected += '<td>c1</td>\n';
+      expected += '</tr>\n';
+      expected += '</tbody>\n';
+      expected += '</table>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should work with plugin implicit-figures', () => {
-    md = md.use(implicitFigures);
-    src =  '![alt](img.png){.a}';
-    expected = '<figure><img src="img.png" alt="alt" class="a"></figure>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should support nested lists', options), () => {
+      src =  '- item\n';
+      src += '  - nested\n';
+      src += '  {.red}\n';
+      src += '\n';
+      src += '{.blue}\n';
+      expected = '<ul class="blue">\n';
+      expected += '<li>item\n';
+      expected += '<ul class="red">\n';
+      expected += '<li>nested</li>\n';
+      expected += '</ul>\n';
+      expected += '</li>\n';
+      expected += '</ul>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should not apply inside `code{.red}`', () => {
-    src =  'paragraph `code{.red}`';
-    expected = '<p>paragraph <code>code{.red}</code></p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should support images', options), () => {
+      src =  '![alt](img.png){.a}';
+      expected = '<p><img src="img.png" alt="alt" class="a"></p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should work with multiple inline code blocks in same paragraph', () => {
-    src = 'bla `click()`{.c} blah `release()`{.cpp}';
-    expected = '<p>bla <code class="c">click()</code> blah <code class="cpp">release()</code></p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should work with plugin implicit-figures', options), () => {
+      md = md.use(implicitFigures);
+      src =  '![alt](img.png){.a}';
+      expected = '<figure><img src="img.png" alt="alt" class="a"></figure>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should support {} curlies with length == 3', () => {
-    src = 'text {1}';
-    expected = '<p 1="">text</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should not apply inside `code{.red}`', options), () => {
+      src =  'paragraph `code{.red}`';
+      expected = '<p>paragraph <code>code{.red}</code></p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), replaceDelimiters(expected, options));
+    });
 
-  it('should do nothing with empty classname {.}', () => {
-    src = 'text {.}';
-    expected = '<p>text {.}</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should work with multiple inline code blocks in same paragraph', options), () => {
+      src = 'bla `click()`{.c} blah `release()`{.cpp}';
+      expected = '<p>bla <code class="c">click()</code> blah <code class="cpp">release()</code></p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should do nothing with empty id {#}', () => {
-    src = 'text {#}';
-    expected = '<p>text {#}</p>\n';
-    assert.equal(md.render(src), expected);
-  });
+    it(replaceDelimiters('should support {} curlies with length == 3', options), () => {
+      src = 'text {1}';
+      expected = '<p 1="">text</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
 
-  it('should support configuring delimeter', () => {
-    md = Md().use(attrs, {
-      leftDelimiter: '[',
-      rightDelimiter: ']'
+    it(replaceDelimiters('should do nothing with empty classname {.}', options), () => {
+      src = 'text {.}';
+      expected = '<p>text {.}</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), replaceDelimiters(expected, options));
     });
 
-    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);
-  });
+    it(replaceDelimiters('should do nothing with empty id {#}', options), () => {
+      src = 'text {#}';
+      expected = '<p>text {#}</p>\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), replaceDelimiters(expected, options));
+    });
 
-  it('should support horizontal rules ---{#id}', () => {
-    src = '---{#id}';
-    expected = '<hr id="id">\n';
-    assert.equal(md.render(src), expected);
+    it(replaceDelimiters('should support horizontal rules ---{#id}', options), () => {
+      src = '---{#id}';
+      expected = '<hr id="id">\n';
+      assert.equal(md.render(replaceDelimiters(src, options)), expected);
+    });
   });
+}
 
-});
-
+function replaceDelimiters(text, options) {
+  return text.replace(/{/g, options.leftDelimiter).replace(/}/g, options.rightDelimiter);
+}
\ No newline at end of file