1 | 'use strict';
|
2 |
|
3 | const parseAttr = require('md-attr-parser');
|
4 | const htmlElemAttr = require('html-element-attributes');
|
5 |
|
6 | const supportedElements = ['link', 'atxHeading', 'strong', 'emphasis', 'deletion', 'code', 'setextHeading', 'fencedCode'];
|
7 | const blockElements = ['atxHeading', 'setextHeading'];
|
8 | const particularElements = ['fencedCode'];
|
9 |
|
10 | const particularTokenize = {};
|
11 |
|
12 | const DOMEventHandler = require('./dom-event-handler.js');
|
13 |
|
14 |
|
15 | const convTypeTag = {
|
16 | image: 'img',
|
17 | link: 'a',
|
18 | heading: 'h1',
|
19 | strong: 'strong',
|
20 | emphasis: 'em',
|
21 | delete: 's',
|
22 | inlineCode: 'code',
|
23 | code: 'code',
|
24 | '*': '*',
|
25 | };
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | function tokenizeGenerator(prefix, oldParser, config) {
|
47 | function token(eat, value, silent) {
|
48 |
|
49 | const self = this;
|
50 | let eaten = oldParser.call(self, eat, value, silent);
|
51 |
|
52 | let index = 0;
|
53 | let parsedAttr;
|
54 | const {length} = value;
|
55 |
|
56 | if (!eaten || !eaten.position) {
|
57 | return undefined;
|
58 | }
|
59 |
|
60 | const type = convTypeTag[eaten.type];
|
61 |
|
62 | index = eaten.position.end.offset - eaten.position.start.offset;
|
63 |
|
64 |
|
65 | if (index + prefix.length < length && value.charAt(index + prefix.length) === '{') {
|
66 |
|
67 | parsedAttr = parseAttr(value, index + prefix.length, config.mdAttrConfig);
|
68 | }
|
69 |
|
70 |
|
71 | if (parsedAttr) {
|
72 | if (config.scope && config.scope !== 'none') {
|
73 | const filtredProp = filterAttributes(parsedAttr.prop, config, type);
|
74 | if (filtredProp !== {}) {
|
75 | if (eaten.data) {
|
76 | eaten.data.hProperties = filtredProp;
|
77 | } else {
|
78 | eaten.data = {hProperties: filtredProp};
|
79 | }
|
80 | }
|
81 | }
|
82 | eaten = eat(prefix + parsedAttr.eaten)(eaten);
|
83 | }
|
84 |
|
85 | return eaten;
|
86 | }
|
87 |
|
88 | return token;
|
89 | }
|
90 |
|
91 |
|
92 | function filterAttributes(prop, config, type) {
|
93 | const {scope} = config;
|
94 | const {extend} = config;
|
95 | const {allowDangerousDOMEventHandlers} = config;
|
96 | const specific = htmlElemAttr;
|
97 |
|
98 | const extendTag = (extend => {
|
99 | const t = {};
|
100 | Object.getOwnPropertyNames(extend).forEach(p => {
|
101 | t[convTypeTag[p]] = extend[p];
|
102 | });
|
103 | return t;
|
104 | })(extend);
|
105 |
|
106 |
|
107 | Object.getOwnPropertyNames(prop).forEach(p => {
|
108 | if (p !== 'key' && p !== 'class' && p !== 'id') {
|
109 | prop[p] = prop[p] || '';
|
110 | }
|
111 | });
|
112 |
|
113 | const isDangerous = p => DOMEventHandler.indexOf(p) >= 0;
|
114 | const isSpecific = p => type in specific && specific[type].indexOf(p) >= 0;
|
115 | const isGlobal = p => htmlElemAttr['*'].indexOf(p) >= 0 || p.match(/^aria-[a-z]{3,24}$/);
|
116 |
|
117 | let inScope = _ => false;
|
118 |
|
119 |
|
120 | const orFunc = (fun, fun2) => x => fun(x) || fun2(x);
|
121 |
|
122 |
|
123 | switch (scope) {
|
124 | case 'none':
|
125 | break;
|
126 | case 'permissive':
|
127 | case 'every':
|
128 | if (allowDangerousDOMEventHandlers) {
|
129 | inScope = _ => true;
|
130 | } else {
|
131 | inScope = x => !isDangerous(x);
|
132 | }
|
133 | break;
|
134 | case 'extended':
|
135 | default:
|
136 | inScope = p => extendTag && type in extendTag && extendTag[type].indexOf(p) >= 0;
|
137 | inScope = orFunc(inScope, p => '*' in extendTag && extendTag['*'].indexOf(p) >= 0);
|
138 |
|
139 | case 'specific':
|
140 | inScope = orFunc(inScope, isSpecific);
|
141 |
|
142 | case 'global':
|
143 | inScope = orFunc(inScope, isGlobal);
|
144 | if (allowDangerousDOMEventHandlers) {
|
145 | inScope = orFunc(inScope, isDangerous);
|
146 | }
|
147 | }
|
148 |
|
149 |
|
150 | Object.getOwnPropertyNames(prop).forEach(p => {
|
151 | if (!inScope(p)) {
|
152 | delete prop[p];
|
153 | }
|
154 | });
|
155 |
|
156 | return prop;
|
157 | }
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 | function tokenizeFencedCode(oldParser, config) {
|
166 | const prefix = '\n';
|
167 | function token(eat, value, silent) {
|
168 |
|
169 | const self = this;
|
170 | let eaten = oldParser.call(self, eat, value, silent);
|
171 |
|
172 | let parsedAttr;
|
173 | const parsedByCustomAttr = false;
|
174 |
|
175 | if (!eaten || !eaten.position) {
|
176 | return undefined;
|
177 | }
|
178 |
|
179 | const type = convTypeTag[eaten.type];
|
180 |
|
181 |
|
182 |
|
183 |
|
184 | if (eaten.lang) {
|
185 |
|
186 | if (eaten.meta) {
|
187 | parsedAttr = parseAttr(eaten.meta);
|
188 | } else {
|
189 |
|
190 |
|
191 |
|
192 | parsedAttr = parseAttr(value, value.indexOf(' '));
|
193 | }
|
194 | }
|
195 |
|
196 |
|
197 | if (parsedAttr) {
|
198 | if (config.scope && config.scope !== 'none') {
|
199 | const filtredProp = filterAttributes(parsedAttr.prop, config, type);
|
200 |
|
201 | if (filtredProp !== {}) {
|
202 | if (eaten.data) {
|
203 | eaten.data.hProperties = {...eaten.data.hProperties, ...filtredProp};
|
204 | } else {
|
205 | eaten.data = {hProperties: filtredProp};
|
206 | }
|
207 | }
|
208 | }
|
209 | if (parsedByCustomAttr) {
|
210 | eaten = eat(prefix + parsedAttr.eaten)(eaten);
|
211 | }
|
212 | }
|
213 |
|
214 | return eaten;
|
215 | }
|
216 |
|
217 |
|
218 |
|
219 | return token;
|
220 | }
|
221 |
|
222 | particularTokenize.fencedCode = tokenizeFencedCode;
|
223 |
|
224 | remarkAttr.SUPPORTED_ELEMENTS = supportedElements;
|
225 |
|
226 | module.exports = remarkAttr;
|
227 |
|
228 |
|
229 | function remarkAttr(userConfig) {
|
230 | const parser = this.Parser;
|
231 |
|
232 | const defaultConfig = {
|
233 | allowDangerousDOMEventHandlers: false,
|
234 | elements: supportedElements,
|
235 | extend: {},
|
236 | scope: 'extended',
|
237 | mdAttrConfig: undefined,
|
238 | };
|
239 | const config = {...defaultConfig, ...userConfig};
|
240 |
|
241 | if (!isRemarkParser(parser)) {
|
242 | throw new Error('Missing parser to attach `remark-attr` [link] (to)');
|
243 | }
|
244 |
|
245 | const tokenizers = parser.prototype.inlineTokenizers;
|
246 | const tokenizersBlock = parser.prototype.blockTokenizers;
|
247 |
|
248 |
|
249 | config.elements.forEach(elem => {
|
250 | if (supportedElements.indexOf(elem) >= 0) {
|
251 | if (blockElements.indexOf(elem) >= 0) {
|
252 | const oldElem = tokenizersBlock[elem];
|
253 | tokenizersBlock[elem] = tokenizeGenerator('\n', oldElem, config);
|
254 | } else if (particularElements.indexOf(elem) >= 0) {
|
255 | const oldElem = tokenizersBlock[elem];
|
256 | tokenizersBlock[elem] = particularTokenize[elem](oldElem, config);
|
257 | } else {
|
258 | const oldElem = tokenizers[elem];
|
259 | const elemTokenize = tokenizeGenerator('', oldElem, config);
|
260 | elemTokenize.locator = tokenizers[elem].locator;
|
261 | tokenizers[elem] = elemTokenize;
|
262 | }
|
263 | }
|
264 | });
|
265 | }
|
266 |
|
267 | function isRemarkParser(parser) {
|
268 | return Boolean(
|
269 | parser &&
|
270 | parser.prototype &&
|
271 | parser.prototype.inlineTokenizers &&
|
272 | parser.prototype.inlineTokenizers.link &&
|
273 | parser.prototype.inlineTokenizers.link.locator
|
274 | );
|
275 | }
|
276 |
|