aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.babelrc6
-rw-r--r--__tests__/index.js37
-rw-r--r--dist/index.js221
-rw-r--r--package.json27
-rw-r--r--src/dom-event-handler.js67
-rw-r--r--src/index.js (renamed from index.js)27
6 files changed, 329 insertions, 56 deletions
diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..1aa4b7b
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,6 @@
+{
+ "presets": ["env"],
+ "plugins": [
+ ["transform-object-rest-spread", { "useBuiltIns": true }]
+ ]
+}
diff --git a/__tests__/index.js b/__tests__/index.js
index c7f13a1..f96acd4 100644
--- a/__tests__/index.js
+++ b/__tests__/index.js
@@ -43,14 +43,7 @@ const generateExtendParser = extendsOptions => text => unified()
const parse = x => parse5.parse(x);
-/*
- * TODO :
- * - Invalid scope
- * - Invalid extended
- * - aria attributes
- */
-
-const mainTestString = `Inline *test*{style="em:4"} paragraph. Use **multiple**{ style="color:pink"} inline ~~block~~ tag. Line \`tagCode\`{ style="color:yellow"}.`;
+const mainTestString = 'Inline *test*{style="em:4"} paragraph. Use **multiple**{ style="color:pink"} inline ~~block~~ tag. Line `tagCode`{ style="color:yellow"}.';
test('basic-default', t => {
const {contents} = renderDefault(mainTestString);
@@ -71,7 +64,7 @@ test('basic-raw', t => {
test('em', t => {
const {contents} = render('textexamplenointerest **Important**{style=4em} still no interest');
- t.deepEqual(parse(contents), parse(`<p>textexamplenointerest <strong style="4em">Important</strong> still no interest</p>`));
+ t.deepEqual(parse(contents), parse('<p>textexamplenointerest <strong style="4em">Important</strong> still no interest</p>'));
});
test('readme-default', t => {
@@ -121,31 +114,37 @@ This is an awesome image : <img src="aws://image.jpg" alt="Awesome image" qualit
});
test('extended-global', t => {
- const renderExtended = generateExtendParser({extend: {'*': ['exAttr']}});
- const globalString = ` *Wait* ! You are **beautiful**{ exAttr="true" } !`;
+ const renderExtended = generateExtendParser({extend: {'*': ['ex-attr']}});
+ const globalString = ' *Wait* ! You are **beautiful**{ ex-attr="true" } !';
const {contents} = renderExtended(globalString);
- t.deepEqual(parse(contents), parse(`<p> <em>Wait</em> ! You are <strong ex-attr="true">beautiful</strong> !</p>`));
+ t.deepEqual(parse(contents), parse('<p> <em>Wait</em> ! You are <strong ex-attr="true">beautiful</strong> !</p>'));
});
test('extended-invalid-scope', t => {
- const renderExtended = generateExtendParser({scope: 'invalid', extend: {strong: ['exAttr']}});
- const invalidString = `*Wait* ! You are **beautiful**{ exAttr="true" onload="qdss" pss="NOK" } !`;
+ const renderExtended = generateExtendParser({scope: 'invalid', extend: {strong: ['ex-attr']}});
+ const invalidString = '*Wait* ! You are **beautiful**{ ex-attr="true" onload="qdss" pss="NOK" } !';
const {contents} = renderExtended(invalidString);
- t.deepEqual(parse(contents), parse(`<p><em>Wait</em> ! You are <strong ex-attr="true">beautiful</strong> !</p>`));
+ t.deepEqual(parse(contents), parse('<p><em>Wait</em> ! You are <strong ex-attr="true">beautiful</strong> !</p>'));
});
test('invalid-scope', t => {
const renderExtended = generateExtendParser({extend: 'exAttr'});
- const invalidString = ` *Wait* ! I **love**{ exAttr="true" onload="qdss" pss="NOK" } you !`;
+ const invalidString = ' *Wait* ! I **love**{ exAttr="true" onload="qdss" pss="NOK" } you !';
const {contents} = renderExtended(invalidString);
- t.deepEqual(parse(contents), parse(`<p> <em>Wait</em> ! I <strong>love</strong> you !</p>`));
+ t.deepEqual(parse(contents), parse('<p> <em>Wait</em> ! I <strong>love</strong> you !</p>'));
});
test('invalid-extend', t => {
const renderExtended = generateExtendParser({extend: 'exAttr'});
- const invalidString = ` *Wait* ! I **love**{ exAttr="true" onload="qdss" attr="NOK" style="color: red;"} you!`;
+ const invalidString = ' *Wait* ! I **love**{ exAttr="true" onload="qdss" attr="NOK" style="color: red;"} you!';
const {contents} = renderExtended(invalidString);
- t.deepEqual(parse(contents), parse(`<p> <em>Wait</em> ! I <strong style="color: red;">love</strong> you!</p>`));
+ t.deepEqual(parse(contents), parse('<p> <em>Wait</em> ! I <strong style="color: red;">love</strong> you!</p>'));
+});
+
+test('global-aria', t => {
+ const invalidString = ' *Wait* ! I **love**{ style="color: pink;" aria-love="true" } you!';
+ const {contents} = renderDefault(invalidString);
+ t.deepEqual(parse(contents), parse('<p> <em>Wait</em> ! I <strong style="color: pink;" aria-love="true">love</strong> you!</p>'));
});
test('fenced code', t => {
diff --git a/dist/index.js b/dist/index.js
new file mode 100644
index 0000000..1e83804
--- /dev/null
+++ b/dist/index.js
@@ -0,0 +1,221 @@
+'use strict';
+
+var parseAttr = require('md-attr-parser');
+var htmlElemAttr = require('html-element-attributes');
+
+var supportedElements = ['link', 'atxHeading', 'strong', 'emphasis', 'deletion', 'code', 'setextHeading'];
+var blockElements = ['atxHeading', 'setextHeading'];
+
+// The list of DOM Event handler
+var DOMEventHandler = ['onabort', 'onautocomplete', 'onautocompleteerror', 'onblur', 'oncancel', 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', 'onclose', 'oncontextmenu', 'oncuechange', 'ondblclick', 'ondrag', 'ondragend', 'ondragenter', 'ondragexit', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onfocus', 'oninput', 'oninvalid', 'onkeydown', 'onkeypress', 'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onpause', 'onplay', 'onplaying', 'onprogress', 'onratechange', 'onreset', 'onresize', 'onscroll', 'onseeked', 'onseeking', 'onselect', 'onshow', 'onsort', 'onstalled', 'onsubmit', 'onsuspend', 'ontimeupdate', 'ontoggle', 'onvolumechange', 'onwaiting'];
+
+/* Table convertion between type and HTML tagName */
+var convTypeTag = {
+ image: 'img',
+ link: 'a',
+ heading: 'h1',
+ strong: 'strong',
+ emphasis: 'em',
+ delete: 's',
+ inlineCode: 'code',
+ '*': '*'
+};
+
+/* This function is a generic function that transform
+ * the tokenize function a node type to a version that understand
+ * attributes.
+ *
+ * The tokenizer function of strong will tokenize **STRONG STRING**
+ * this function extand it to tokenize **STRONG STRING**{list=of attributes}
+ *
+ * - The prefix is '\n' for block node and '' for inline one
+ *
+ * The syntax is for atxHeading ::
+ * ## HEAD TITLE
+ * {attributes}
+ *
+ * Attributes are on the next line.
+ *
+ * - The old parser is the old function user to tokenize
+ * - The config is the configuration of this plugin
+ *
+ */
+function tokenizeGenerator(prefix, oldParser, config) {
+ function token(eat, value, silent) {
+ // This we call the old tokenize
+ var self = this;
+ var eaten = oldParser.call(self, eat, value, silent);
+
+ var index = 0;
+ var parsedAttr = void 0;
+ var length = value.length;
+
+
+ if (!eaten || !eaten.position) {
+ return undefined;
+ }
+
+ var type = convTypeTag[eaten.type];
+
+ index = eaten.position.end.offset - eaten.position.start.offset;
+
+ // Then we check for attributes
+ if (index + prefix.length < length && value.charAt(index + prefix.length) === '{') {
+ // If any, parse it
+ parsedAttr = parseAttr(value, index + prefix.length);
+ }
+
+ // If parsed configure the node
+ if (parsedAttr) {
+ if (config.scope && config.scope !== 'none') {
+ var filtredProp = filterAttributes(parsedAttr.prop, config, type);
+ if (filtredProp !== {}) {
+ if (eaten.data) {
+ eaten.data.hProperties = filtredProp;
+ } else {
+ eaten.data = { hProperties: filtredProp };
+ }
+ }
+ }
+ eaten = eat(prefix + parsedAttr.eaten)(eaten);
+ }
+
+ return eaten;
+ }
+ // Return the new tokenizer function
+ return token;
+}
+
+// A generic function to parse attributes
+function filterAttributes(prop, config, type) {
+ var scope = config.scope;
+ var extend = config.extend;
+ var allowDangerousDOMEventHandlers = config.allowDangerousDOMEventHandlers;
+
+ var specific = htmlElemAttr;
+
+ var extendTag = function (extend) {
+ var t = {};
+ Object.getOwnPropertyNames(extend).forEach(function (p) {
+ t[convTypeTag[p]] = extend[p];
+ });
+ return t;
+ }(extend);
+
+ // Delete empty key/class/id attributes
+ Object.getOwnPropertyNames(prop).forEach(function (p) {
+ if (p !== 'key' && p !== 'class' && p !== 'id') {
+ prop[p] = prop[p] || '';
+ }
+ });
+
+ var isDangerous = function isDangerous(p) {
+ return DOMEventHandler.indexOf(p) >= 0;
+ };
+ var isSpecific = function isSpecific(p) {
+ return type in specific && specific[type].indexOf(p) >= 0;
+ };
+ var isGlobal = function isGlobal(p) {
+ return htmlElemAttr['*'].indexOf(p) >= 0 || p.match(/^aria-[a-z]{3,24}$/);
+ };
+
+ var inScope = function inScope(_) {
+ return false;
+ };
+
+ // Function used to `or combine` two other function.
+ var orFunc = function orFunc(fun, fun2) {
+ return function (x) {
+ return fun(x) || fun2(x);
+ };
+ };
+
+ // Respect the scope configuration
+ switch (scope) {
+ case 'none':
+ // Plugin is disabled
+ break;
+ case 'permissive':
+ case 'every':
+ if (allowDangerousDOMEventHandlers) {
+ inScope = function inScope(_) {
+ return true;
+ };
+ } else {
+ inScope = function inScope(x) {
+ return !isDangerous(x);
+ };
+ }
+ break;
+ case 'extended':
+ default:
+ inScope = function inScope(p) {
+ return extendTag && type in extendTag && extendTag[type].indexOf(p) >= 0;
+ };
+ inScope = orFunc(inScope, function (p) {
+ return '*' in extendTag && extendTag['*'].indexOf(p) >= 0;
+ });
+ // Or if it in the specific scope, fallthrough
+ case 'specific':
+ inScope = orFunc(inScope, isSpecific);
+ // Or if it in the global scope fallthrough
+ case 'global':
+ inScope = orFunc(inScope, isGlobal);
+ if (allowDangerousDOMEventHandlers) {
+ // If allowed add dangerous attributes to global scope
+ inScope = orFunc(inScope, isDangerous);
+ }
+ }
+
+ // If an attributes isn't in the scope, delete it
+ Object.getOwnPropertyNames(prop).forEach(function (p) {
+ if (!inScope(p)) {
+ delete prop[p];
+ }
+ });
+
+ return prop;
+}
+
+remarkAttr.SUPPORTED_ELEMENTS = supportedElements;
+
+module.exports = remarkAttr;
+
+/* Function that is exported */
+function remarkAttr(userConfig) {
+ var parser = this.Parser;
+
+ var defaultConfig = {
+ allowDangerousDOMEventHandlers: false,
+ elements: supportedElements,
+ extend: {},
+ scope: 'extended'
+ };
+ var config = Object.assign({}, defaultConfig, userConfig);
+
+ if (!isRemarkParser(parser)) {
+ throw new Error('Missing parser to attach `remark-attr` [link] (to)');
+ }
+
+ var tokenizers = parser.prototype.inlineTokenizers;
+ var tokenizersBlock = parser.prototype.blockTokenizers;
+
+ // For each elements, replace the old tokenizer by the new one
+ config.elements.forEach(function (elem) {
+ if (supportedElements.indexOf(elem) >= 0) {
+ if (blockElements.indexOf(elem) >= 0) {
+ var oldElem = tokenizersBlock[elem];
+ tokenizersBlock[elem] = tokenizeGenerator('\n', oldElem, config);
+ } else {
+ var _oldElem = tokenizers[elem];
+ var elemTokenize = tokenizeGenerator('', _oldElem, config);
+ elemTokenize.locator = tokenizers[elem].locator;
+ tokenizers[elem] = elemTokenize;
+ }
+ }
+ });
+}
+
+function isRemarkParser(parser) {
+ return Boolean(parser && parser.prototype && parser.prototype.inlineTokenizers && parser.prototype.inlineTokenizers.link && parser.prototype.inlineTokenizers.link.locator);
+} \ No newline at end of file
diff --git a/package.json b/package.json
index 61de8f8..a845b45 100644
--- a/package.json
+++ b/package.json
@@ -1,16 +1,14 @@
{
"name": "remark-attr",
- "version": "0.6.3",
+ "version": "0.7.0",
"description": "Add support of custom attributes to Markdown syntax.",
- "main": "index.js",
+ "main": "dist/index.js",
"scripts": {
"pretest": "xo",
+ "prepare": "del-cli dist && cross-env BABEL_ENV=production babel src --out-dir dist",
"test": "ava"
},
- "repository": {
- "type": "git",
- "url": "git+https://github.com/arobase-che/remark-attr.git"
- },
+ "repository": "github:arobase-che/remark-attr",
"keywords": [
"markdown",
"attribute",
@@ -24,13 +22,18 @@
"homepage": "https://github.com/arobase-che/remark-attr#readme",
"devDependencies": {
"ava": "^0.25.0",
+ "babel-cli": "^6.26.0",
+ "babel-preset-env": "^1.6.1",
+ "babel-plugin-transform-object-rest-spread": "^6.26.0",
+ "cross-env": "^5.1.4",
+ "del-cli": "^1.1.0",
"parse5": "^5.0.0",
- "rehype-raw": "^2.0.0",
- "rehype-stringify": "^3.0.0",
+ "rehype-raw": "^3.0.0",
+ "rehype-stringify": "^4.0.0",
"remark-parse": "^5.0.0",
- "remark-rehype": "^3.0.0",
+ "remark-rehype": "^3.0.1",
"unified": "^7.0.0",
- "xo": "^0.21.0"
+ "xo": "^0.23.0"
},
"xo": {
"space": true,
@@ -42,7 +45,7 @@
}
},
"dependencies": {
- "html-element-attributes": "^1.3.1",
- "md-attr-parser": "^1.1.5"
+ "html-element-attributes": "^2.0.0",
+ "md-attr-parser": "^1.1.6"
}
}
diff --git a/src/dom-event-handler.js b/src/dom-event-handler.js
new file mode 100644
index 0000000..dc4f3bd
--- /dev/null
+++ b/src/dom-event-handler.js
@@ -0,0 +1,67 @@
+'use strict';
+
+module.export = [
+ 'onabort',
+ 'onautocomplete',
+ 'onautocompleteerror',
+ 'onblur',
+ 'oncancel',
+ 'oncanplay',
+ 'oncanplaythrough',
+ 'onchange',
+ 'onclick',
+ 'onclose',
+ 'oncontextmenu',
+ 'oncuechange',
+ 'ondblclick',
+ 'ondrag',
+ 'ondragend',
+ 'ondragenter',
+ 'ondragexit',
+ 'ondragleave',
+ 'ondragover',
+ 'ondragstart',
+ 'ondrop',
+ 'ondurationchange',
+ 'onemptied',
+ 'onended',
+ 'onerror',
+ 'onfocus',
+ 'oninput',
+ 'oninvalid',
+ 'onkeydown',
+ 'onkeypress',
+ 'onkeyup',
+ 'onload',
+ 'onloadeddata',
+ 'onloadedmetadata',
+ 'onloadstart',
+ 'onmousedown',
+ 'onmouseenter',
+ 'onmouseleave',
+ 'onmousemove',
+ 'onmouseout',
+ 'onmouseover',
+ 'onmouseup',
+ 'onmousewheel',
+ 'onpause',
+ 'onplay',
+ 'onplaying',
+ 'onprogress',
+ 'onratechange',
+ 'onreset',
+ 'onresize',
+ 'onscroll',
+ 'onseeked',
+ 'onseeking',
+ 'onselect',
+ 'onshow',
+ 'onsort',
+ 'onstalled',
+ 'onsubmit',
+ 'onsuspend',
+ 'ontimeupdate',
+ 'ontoggle',
+ 'onvolumechange',
+ 'onwaiting',
+];
diff --git a/index.js b/src/index.js
index ac2274e..49ee990 100644
--- a/index.js
+++ b/src/index.js
@@ -9,30 +9,7 @@ const particularElements = ['fencedCode'];
const particularTokenize = {};
-// The list of DOM Event handler
-const DOMEventHandler = [
- 'onabort', 'onautocomplete', 'onautocompleteerror',
- 'onblur', 'oncancel', 'oncanplay',
- 'oncanplaythrough', 'onchange', 'onclick',
- 'onclose', 'oncontextmenu', 'oncuechange',
- 'ondblclick', 'ondrag', 'ondragend',
- 'ondragenter', 'ondragexit', 'ondragleave',
- 'ondragover', 'ondragstart', 'ondrop',
- 'ondurationchange', 'onemptied', 'onended',
- 'onerror', 'onfocus', 'oninput',
- 'oninvalid', 'onkeydown', 'onkeypress',
- 'onkeyup', 'onload', 'onloadeddata',
- 'onloadedmetadata', 'onloadstart', 'onmousedown',
- 'onmouseenter', 'onmouseleave', 'onmousemove',
- 'onmouseout', 'onmouseover', 'onmouseup',
- 'onmousewheel', 'onpause', 'onplay',
- 'onplaying', 'onprogress', 'onratechange',
- 'onreset', 'onresize', 'onscroll',
- 'onseeked', 'onseeking', 'onselect',
- 'onshow', 'onsort', 'onstalled',
- 'onsubmit', 'onsuspend', 'ontimeupdate',
- 'ontoggle', 'onvolumechange', 'onwaiting',
-];
+const DOMEventHandler = require('./dom-event-handler.js');
/* Table convertion between type and HTML tagName */
const convTypeTag = {
@@ -135,7 +112,7 @@ function filterAttributes(prop, config, type) {
const isDangerous = p => DOMEventHandler.indexOf(p) >= 0;
const isSpecific = p => type in specific && specific[type].indexOf(p) >= 0;
- const isGlobal = p => htmlElemAttr['*'].indexOf(p) >= 0;
+ const isGlobal = p => htmlElemAttr['*'].indexOf(p) >= 0 || p.match(/^aria-[a-z]{3,24}$/);
let inScope = _ => false;