UNPKG

6.66 kBJavaScriptView Raw
1/**
2 * Helpers
3 */
4const escapeTest = /[&<>"']/;
5const escapeReplace = /[&<>"']/g;
6const escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/;
7const escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g;
8const escapeReplacements = {
9 '&': '&amp;',
10 '<': '&lt;',
11 '>': '&gt;',
12 '"': '&quot;',
13 "'": '&#39;'
14};
15const getEscapeReplacement = (ch) => escapeReplacements[ch];
16export function escape(html, encode) {
17 if (encode) {
18 if (escapeTest.test(html)) {
19 return html.replace(escapeReplace, getEscapeReplacement);
20 }
21 } else {
22 if (escapeTestNoEncode.test(html)) {
23 return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
24 }
25 }
26
27 return html;
28}
29
30const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig;
31
32/**
33 * @param {string} html
34 */
35export function unescape(html) {
36 // explicitly match decimal, hex, and named HTML entities
37 return html.replace(unescapeTest, (_, n) => {
38 n = n.toLowerCase();
39 if (n === 'colon') return ':';
40 if (n.charAt(0) === '#') {
41 return n.charAt(1) === 'x'
42 ? String.fromCharCode(parseInt(n.substring(2), 16))
43 : String.fromCharCode(+n.substring(1));
44 }
45 return '';
46 });
47}
48
49const caret = /(^|[^\[])\^/g;
50
51/**
52 * @param {string | RegExp} regex
53 * @param {string} opt
54 */
55export function edit(regex, opt) {
56 regex = typeof regex === 'string' ? regex : regex.source;
57 opt = opt || '';
58 const obj = {
59 replace: (name, val) => {
60 val = val.source || val;
61 val = val.replace(caret, '$1');
62 regex = regex.replace(name, val);
63 return obj;
64 },
65 getRegex: () => {
66 return new RegExp(regex, opt);
67 }
68 };
69 return obj;
70}
71
72const nonWordAndColonTest = /[^\w:]/g;
73const originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;
74
75/**
76 * @param {boolean} sanitize
77 * @param {string} base
78 * @param {string} href
79 */
80export function cleanUrl(sanitize, base, href) {
81 if (sanitize) {
82 let prot;
83 try {
84 prot = decodeURIComponent(unescape(href))
85 .replace(nonWordAndColonTest, '')
86 .toLowerCase();
87 } catch (e) {
88 return null;
89 }
90 if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
91 return null;
92 }
93 }
94 if (base && !originIndependentUrl.test(href)) {
95 href = resolveUrl(base, href);
96 }
97 try {
98 href = encodeURI(href).replace(/%25/g, '%');
99 } catch (e) {
100 return null;
101 }
102 return href;
103}
104
105const baseUrls = {};
106const justDomain = /^[^:]+:\/*[^/]*$/;
107const protocol = /^([^:]+:)[\s\S]*$/;
108const domain = /^([^:]+:\/*[^/]*)[\s\S]*$/;
109
110/**
111 * @param {string} base
112 * @param {string} href
113 */
114export function resolveUrl(base, href) {
115 if (!baseUrls[' ' + base]) {
116 // we can ignore everything in base after the last slash of its path component,
117 // but we might need to add _that_
118 // https://tools.ietf.org/html/rfc3986#section-3
119 if (justDomain.test(base)) {
120 baseUrls[' ' + base] = base + '/';
121 } else {
122 baseUrls[' ' + base] = rtrim(base, '/', true);
123 }
124 }
125 base = baseUrls[' ' + base];
126 const relativeBase = base.indexOf(':') === -1;
127
128 if (href.substring(0, 2) === '//') {
129 if (relativeBase) {
130 return href;
131 }
132 return base.replace(protocol, '$1') + href;
133 } else if (href.charAt(0) === '/') {
134 if (relativeBase) {
135 return href;
136 }
137 return base.replace(domain, '$1') + href;
138 } else {
139 return base + href;
140 }
141}
142
143export const noopTest = { exec: function noopTest() {} };
144
145export function merge(obj) {
146 let i = 1,
147 target,
148 key;
149
150 for (; i < arguments.length; i++) {
151 target = arguments[i];
152 for (key in target) {
153 if (Object.prototype.hasOwnProperty.call(target, key)) {
154 obj[key] = target[key];
155 }
156 }
157 }
158
159 return obj;
160}
161
162export function splitCells(tableRow, count) {
163 // ensure that every cell-delimiting pipe has a space
164 // before it to distinguish it from an escaped pipe
165 const row = tableRow.replace(/\|/g, (match, offset, str) => {
166 let escaped = false,
167 curr = offset;
168 while (--curr >= 0 && str[curr] === '\\') escaped = !escaped;
169 if (escaped) {
170 // odd number of slashes means | is escaped
171 // so we leave it alone
172 return '|';
173 } else {
174 // add space before unescaped |
175 return ' |';
176 }
177 }),
178 cells = row.split(/ \|/);
179 let i = 0;
180
181 // First/last cell in a row cannot be empty if it has no leading/trailing pipe
182 if (!cells[0].trim()) { cells.shift(); }
183 if (cells.length > 0 && !cells[cells.length - 1].trim()) { cells.pop(); }
184
185 if (cells.length > count) {
186 cells.splice(count);
187 } else {
188 while (cells.length < count) cells.push('');
189 }
190
191 for (; i < cells.length; i++) {
192 // leading or trailing whitespace is ignored per the gfm spec
193 cells[i] = cells[i].trim().replace(/\\\|/g, '|');
194 }
195 return cells;
196}
197
198/**
199 * Remove trailing 'c's. Equivalent to str.replace(/c*$/, '').
200 * /c*$/ is vulnerable to REDOS.
201 *
202 * @param {string} str
203 * @param {string} c
204 * @param {boolean} invert Remove suffix of non-c chars instead. Default falsey.
205 */
206export function rtrim(str, c, invert) {
207 const l = str.length;
208 if (l === 0) {
209 return '';
210 }
211
212 // Length of suffix matching the invert condition.
213 let suffLen = 0;
214
215 // Step left until we fail to match the invert condition.
216 while (suffLen < l) {
217 const currChar = str.charAt(l - suffLen - 1);
218 if (currChar === c && !invert) {
219 suffLen++;
220 } else if (currChar !== c && invert) {
221 suffLen++;
222 } else {
223 break;
224 }
225 }
226
227 return str.slice(0, l - suffLen);
228}
229
230export function findClosingBracket(str, b) {
231 if (str.indexOf(b[1]) === -1) {
232 return -1;
233 }
234 const l = str.length;
235 let level = 0,
236 i = 0;
237 for (; i < l; i++) {
238 if (str[i] === '\\') {
239 i++;
240 } else if (str[i] === b[0]) {
241 level++;
242 } else if (str[i] === b[1]) {
243 level--;
244 if (level < 0) {
245 return i;
246 }
247 }
248 }
249 return -1;
250}
251
252export function checkSanitizeDeprecation(opt) {
253 if (opt && opt.sanitize && !opt.silent) {
254 console.warn('marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options');
255 }
256}
257
258// copied from https://stackoverflow.com/a/5450113/806777
259/**
260 * @param {string} pattern
261 * @param {number} count
262 */
263export function repeatString(pattern, count) {
264 if (count < 1) {
265 return '';
266 }
267 let result = '';
268 while (count > 1) {
269 if (count & 1) {
270 result += pattern;
271 }
272 count >>= 1;
273 pattern += pattern;
274 }
275 return result + pattern;
276}