UNPKG

20.2 kBJavaScriptView Raw
1(function () {
2
3 if (typeof Prism === 'undefined' || typeof document === 'undefined' || !Function.prototype.bind) {
4 return;
5 }
6
7 var previewers = {
8 // gradient must be defined before color and angle
9 'gradient': {
10 create: (function () {
11
12 // Stores already processed gradients so that we don't
13 // make the conversion every time the previewer is shown
14 var cache = {};
15
16 /**
17 * Returns a W3C-valid linear gradient
18 *
19 * @param {string} prefix Vendor prefix if any ("-moz-", "-webkit-", etc.)
20 * @param {string} func Gradient function name ("linear-gradient")
21 * @param {string[]} values Array of the gradient function parameters (["0deg", "red 0%", "blue 100%"])
22 */
23 var convertToW3CLinearGradient = function (prefix, func, values) {
24 // Default value for angle
25 var angle = '180deg';
26
27 if (/^(?:-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad)|to\b|top|right|bottom|left)/.test(values[0])) {
28 angle = values.shift();
29 if (angle.indexOf('to ') < 0) {
30 // Angle uses old keywords
31 // W3C syntax uses "to" + opposite keywords
32 if (angle.indexOf('top') >= 0) {
33 if (angle.indexOf('left') >= 0) {
34 angle = 'to bottom right';
35 } else if (angle.indexOf('right') >= 0) {
36 angle = 'to bottom left';
37 } else {
38 angle = 'to bottom';
39 }
40 } else if (angle.indexOf('bottom') >= 0) {
41 if (angle.indexOf('left') >= 0) {
42 angle = 'to top right';
43 } else if (angle.indexOf('right') >= 0) {
44 angle = 'to top left';
45 } else {
46 angle = 'to top';
47 }
48 } else if (angle.indexOf('left') >= 0) {
49 angle = 'to right';
50 } else if (angle.indexOf('right') >= 0) {
51 angle = 'to left';
52 } else if (prefix) {
53 // Angle is shifted by 90deg in prefixed gradients
54 if (angle.indexOf('deg') >= 0) {
55 angle = (90 - parseFloat(angle)) + 'deg';
56 } else if (angle.indexOf('rad') >= 0) {
57 angle = (Math.PI / 2 - parseFloat(angle)) + 'rad';
58 }
59 }
60 }
61 }
62
63 return func + '(' + angle + ',' + values.join(',') + ')';
64 };
65
66 /**
67 * Returns a W3C-valid radial gradient
68 *
69 * @param {string} prefix Vendor prefix if any ("-moz-", "-webkit-", etc.)
70 * @param {string} func Gradient function name ("linear-gradient")
71 * @param {string[]} values Array of the gradient function parameters (["0deg", "red 0%", "blue 100%"])
72 */
73 var convertToW3CRadialGradient = function (prefix, func, values) {
74 if (values[0].indexOf('at') < 0) {
75 // Looks like old syntax
76
77 // Default values
78 var position = 'center';
79 var shape = 'ellipse';
80 var size = 'farthest-corner';
81
82 if (/\b(?:bottom|center|left|right|top)\b|^\d+/.test(values[0])) {
83 // Found a position
84 // Remove angle value, if any
85 position = values.shift().replace(/\s*-?\d+(?:deg|rad)\s*/, '');
86 }
87 if (/\b(?:circle|closest|contain|cover|ellipse|farthest)\b/.test(values[0])) {
88 // Found a shape and/or size
89 var shapeSizeParts = values.shift().split(/\s+/);
90 if (shapeSizeParts[0] && (shapeSizeParts[0] === 'circle' || shapeSizeParts[0] === 'ellipse')) {
91 shape = shapeSizeParts.shift();
92 }
93 if (shapeSizeParts[0]) {
94 size = shapeSizeParts.shift();
95 }
96
97 // Old keywords are converted to their synonyms
98 if (size === 'cover') {
99 size = 'farthest-corner';
100 } else if (size === 'contain') {
101 size = 'clothest-side';
102 }
103 }
104
105 return func + '(' + shape + ' ' + size + ' at ' + position + ',' + values.join(',') + ')';
106 }
107 return func + '(' + values.join(',') + ')';
108 };
109
110 /**
111 * Converts a gradient to a W3C-valid one
112 * Does not support old webkit syntax (-webkit-gradient(linear...) and -webkit-gradient(radial...))
113 *
114 * @param {string} gradient The CSS gradient
115 */
116 var convertToW3CGradient = function (gradient) {
117 if (cache[gradient]) {
118 return cache[gradient];
119 }
120 var parts = gradient.match(/^(\b|\B-[a-z]{1,10}-)((?:repeating-)?(?:linear|radial)-gradient)/);
121 // "", "-moz-", etc.
122 var prefix = parts && parts[1];
123 // "linear-gradient", "radial-gradient", etc.
124 var func = parts && parts[2];
125
126 var values = gradient.replace(/^(?:\b|\B-[a-z]{1,10}-)(?:repeating-)?(?:linear|radial)-gradient\(|\)$/g, '').split(/\s*,\s*/);
127
128 if (func.indexOf('linear') >= 0) {
129 return cache[gradient] = convertToW3CLinearGradient(prefix, func, values);
130 } else if (func.indexOf('radial') >= 0) {
131 return cache[gradient] = convertToW3CRadialGradient(prefix, func, values);
132 }
133 return cache[gradient] = func + '(' + values.join(',') + ')';
134 };
135
136 return function () {
137 new Prism.plugins.Previewer('gradient', function (value) {
138 this.firstChild.style.backgroundImage = '';
139 this.firstChild.style.backgroundImage = convertToW3CGradient(value);
140 return !!this.firstChild.style.backgroundImage;
141 }, '*', function () {
142 this._elt.innerHTML = '<div></div>';
143 });
144 };
145 }()),
146 tokens: {
147 'gradient': {
148 pattern: /(?:\b|\B-[a-z]{1,10}-)(?:repeating-)?(?:linear|radial)-gradient\((?:(?:hsl|rgb)a?\(.+?\)|[^\)])+\)/gi,
149 inside: {
150 'function': /[\w-]+(?=\()/,
151 'punctuation': /[(),]/
152 }
153 }
154 },
155 languages: {
156 'css': true,
157 'less': true,
158 'sass': [
159 {
160 lang: 'sass',
161 before: 'punctuation',
162 inside: 'inside',
163 root: Prism.languages.sass && Prism.languages.sass['variable-line']
164 },
165 {
166 lang: 'sass',
167 before: 'punctuation',
168 inside: 'inside',
169 root: Prism.languages.sass && Prism.languages.sass['property-line']
170 }
171 ],
172 'scss': true,
173 'stylus': [
174 {
175 lang: 'stylus',
176 before: 'func',
177 inside: 'rest',
178 root: Prism.languages.stylus && Prism.languages.stylus['property-declaration'].inside
179 },
180 {
181 lang: 'stylus',
182 before: 'func',
183 inside: 'rest',
184 root: Prism.languages.stylus && Prism.languages.stylus['variable-declaration'].inside
185 }
186 ]
187 }
188 },
189 'angle': {
190 create: function () {
191 new Prism.plugins.Previewer('angle', function (value) {
192 var num = parseFloat(value);
193 var unit = value.match(/[a-z]+$/i);
194 var max; var percentage;
195 if (!num || !unit) {
196 return false;
197 }
198 unit = unit[0];
199
200 switch (unit) {
201 case 'deg':
202 max = 360;
203 break;
204 case 'grad':
205 max = 400;
206 break;
207 case 'rad':
208 max = 2 * Math.PI;
209 break;
210 case 'turn':
211 max = 1;
212 }
213
214 percentage = 100 * num / max;
215 percentage %= 100;
216
217 this[(num < 0 ? 'set' : 'remove') + 'Attribute']('data-negative', '');
218 this.querySelector('circle').style.strokeDasharray = Math.abs(percentage) + ',500';
219 return true;
220 }, '*', function () {
221 this._elt.innerHTML = '<svg viewBox="0 0 64 64">' +
222 '<circle r="16" cy="32" cx="32"></circle>' +
223 '</svg>';
224 });
225 },
226 tokens: {
227 'angle': /(?:\b|\B-|(?=\B\.))(?:\d+(?:\.\d+)?|\.\d+)(?:deg|g?rad|turn)\b/i
228 },
229 languages: {
230 'css': true,
231 'less': true,
232 'markup': {
233 lang: 'markup',
234 before: 'punctuation',
235 inside: 'inside',
236 root: Prism.languages.markup && Prism.languages.markup['tag'].inside['attr-value']
237 },
238 'sass': [
239 {
240 lang: 'sass',
241 inside: 'inside',
242 root: Prism.languages.sass && Prism.languages.sass['property-line']
243 },
244 {
245 lang: 'sass',
246 before: 'operator',
247 inside: 'inside',
248 root: Prism.languages.sass && Prism.languages.sass['variable-line']
249 }
250 ],
251 'scss': true,
252 'stylus': [
253 {
254 lang: 'stylus',
255 before: 'func',
256 inside: 'rest',
257 root: Prism.languages.stylus && Prism.languages.stylus['property-declaration'].inside
258 },
259 {
260 lang: 'stylus',
261 before: 'func',
262 inside: 'rest',
263 root: Prism.languages.stylus && Prism.languages.stylus['variable-declaration'].inside
264 }
265 ]
266 }
267 },
268 'color': {
269 create: function () {
270 new Prism.plugins.Previewer('color', function (value) {
271 this.style.backgroundColor = '';
272 this.style.backgroundColor = value;
273 return !!this.style.backgroundColor;
274 });
275 },
276 tokens: {
277 'color': [Prism.languages.css['hexcode']].concat(Prism.languages.css['color'])
278 },
279 languages: {
280 // CSS extras is required, so css and scss are not necessary
281 'css': false,
282 'less': true,
283 'markup': {
284 lang: 'markup',
285 before: 'punctuation',
286 inside: 'inside',
287 root: Prism.languages.markup && Prism.languages.markup['tag'].inside['attr-value']
288 },
289 'sass': [
290 {
291 lang: 'sass',
292 before: 'punctuation',
293 inside: 'inside',
294 root: Prism.languages.sass && Prism.languages.sass['variable-line']
295 },
296 {
297 lang: 'sass',
298 inside: 'inside',
299 root: Prism.languages.sass && Prism.languages.sass['property-line']
300 }
301 ],
302 'scss': false,
303 'stylus': [
304 {
305 lang: 'stylus',
306 before: 'hexcode',
307 inside: 'rest',
308 root: Prism.languages.stylus && Prism.languages.stylus['property-declaration'].inside
309 },
310 {
311 lang: 'stylus',
312 before: 'hexcode',
313 inside: 'rest',
314 root: Prism.languages.stylus && Prism.languages.stylus['variable-declaration'].inside
315 }
316 ]
317 }
318 },
319 'easing': {
320 create: function () {
321 new Prism.plugins.Previewer('easing', function (value) {
322
323 value = {
324 'linear': '0,0,1,1',
325 'ease': '.25,.1,.25,1',
326 'ease-in': '.42,0,1,1',
327 'ease-out': '0,0,.58,1',
328 'ease-in-out': '.42,0,.58,1'
329 }[value] || value;
330
331 var p = value.match(/-?(?:\d+(?:\.\d+)?|\.\d+)/g);
332
333 if (p.length === 4) {
334 p = p.map(function (p, i) { return (i % 2 ? 1 - p : p) * 100; });
335
336 this.querySelector('path').setAttribute('d', 'M0,100 C' + p[0] + ',' + p[1] + ', ' + p[2] + ',' + p[3] + ', 100,0');
337
338 var lines = this.querySelectorAll('line');
339 lines[0].setAttribute('x2', p[0]);
340 lines[0].setAttribute('y2', p[1]);
341 lines[1].setAttribute('x2', p[2]);
342 lines[1].setAttribute('y2', p[3]);
343
344 return true;
345 }
346
347 return false;
348 }, '*', function () {
349 this._elt.innerHTML = '<svg viewBox="-20 -20 140 140" width="100" height="100">' +
350 '<defs>' +
351 '<marker id="prism-previewer-easing-marker" viewBox="0 0 4 4" refX="2" refY="2" markerUnits="strokeWidth">' +
352 '<circle cx="2" cy="2" r="1.5" />' +
353 '</marker>' +
354 '</defs>' +
355 '<path d="M0,100 C20,50, 40,30, 100,0" />' +
356 '<line x1="0" y1="100" x2="20" y2="50" marker-start="url(#prism-previewer-easing-marker)" marker-end="url(#prism-previewer-easing-marker)" />' +
357 '<line x1="100" y1="0" x2="40" y2="30" marker-start="url(#prism-previewer-easing-marker)" marker-end="url(#prism-previewer-easing-marker)" />' +
358 '</svg>';
359 });
360 },
361 tokens: {
362 'easing': {
363 pattern: /\bcubic-bezier\((?:-?(?:\d+(?:\.\d+)?|\.\d+),\s*){3}-?(?:\d+(?:\.\d+)?|\.\d+)\)\B|\b(?:ease(?:-in)?(?:-out)?|linear)(?=\s|[;}]|$)/i,
364 inside: {
365 'function': /[\w-]+(?=\()/,
366 'punctuation': /[(),]/
367 }
368 }
369 },
370 languages: {
371 'css': true,
372 'less': true,
373 'sass': [
374 {
375 lang: 'sass',
376 inside: 'inside',
377 before: 'punctuation',
378 root: Prism.languages.sass && Prism.languages.sass['variable-line']
379 },
380 {
381 lang: 'sass',
382 inside: 'inside',
383 root: Prism.languages.sass && Prism.languages.sass['property-line']
384 }
385 ],
386 'scss': true,
387 'stylus': [
388 {
389 lang: 'stylus',
390 before: 'hexcode',
391 inside: 'rest',
392 root: Prism.languages.stylus && Prism.languages.stylus['property-declaration'].inside
393 },
394 {
395 lang: 'stylus',
396 before: 'hexcode',
397 inside: 'rest',
398 root: Prism.languages.stylus && Prism.languages.stylus['variable-declaration'].inside
399 }
400 ]
401 }
402 },
403
404 'time': {
405 create: function () {
406 new Prism.plugins.Previewer('time', function (value) {
407 var num = parseFloat(value);
408 var unit = value.match(/[a-z]+$/i);
409 if (!num || !unit) {
410 return false;
411 }
412 unit = unit[0];
413 this.querySelector('circle').style.animationDuration = 2 * num + unit;
414 return true;
415 }, '*', function () {
416 this._elt.innerHTML = '<svg viewBox="0 0 64 64">' +
417 '<circle r="16" cy="32" cx="32"></circle>' +
418 '</svg>';
419 });
420 },
421 tokens: {
422 'time': /(?:\b|\B-|(?=\B\.))(?:\d+(?:\.\d+)?|\.\d+)m?s\b/i
423 },
424 languages: {
425 'css': true,
426 'less': true,
427 'markup': {
428 lang: 'markup',
429 before: 'punctuation',
430 inside: 'inside',
431 root: Prism.languages.markup && Prism.languages.markup['tag'].inside['attr-value']
432 },
433 'sass': [
434 {
435 lang: 'sass',
436 inside: 'inside',
437 root: Prism.languages.sass && Prism.languages.sass['property-line']
438 },
439 {
440 lang: 'sass',
441 before: 'operator',
442 inside: 'inside',
443 root: Prism.languages.sass && Prism.languages.sass['variable-line']
444 }
445 ],
446 'scss': true,
447 'stylus': [
448 {
449 lang: 'stylus',
450 before: 'hexcode',
451 inside: 'rest',
452 root: Prism.languages.stylus && Prism.languages.stylus['property-declaration'].inside
453 },
454 {
455 lang: 'stylus',
456 before: 'hexcode',
457 inside: 'rest',
458 root: Prism.languages.stylus && Prism.languages.stylus['variable-declaration'].inside
459 }
460 ]
461 }
462 }
463 };
464
465 /**
466 * Returns the absolute X, Y offsets for an element
467 *
468 * @param {HTMLElement} element
469 * @returns {{top: number, right: number, bottom: number, left: number, width: number, height: number}}
470 */
471 var getOffset = function (element) {
472 var elementBounds = element.getBoundingClientRect();
473 var left = elementBounds.left;
474 var top = elementBounds.top;
475 var documentBounds = document.documentElement.getBoundingClientRect();
476 left -= documentBounds.left;
477 top -= documentBounds.top;
478
479 return {
480 top: top,
481 right: innerWidth - left - elementBounds.width,
482 bottom: innerHeight - top - elementBounds.height,
483 left: left,
484 width: elementBounds.width,
485 height: elementBounds.height
486 };
487 };
488
489 var TOKEN_CLASS = 'token';
490 var ACTIVE_CLASS = 'active';
491 var FLIPPED_CLASS = 'flipped';
492
493 /**
494 * Previewer constructor
495 *
496 * @param {string} type Unique previewer type
497 * @param {Function} updater Function that will be called on mouseover.
498 * @param {string[]|string} [supportedLanguages] Aliases of the languages this previewer must be enabled for. Defaults to "*", all languages.
499 * @param {Function} [initializer] Function that will be called on initialization.
500 * @class
501 */
502 var Previewer = function (type, updater, supportedLanguages, initializer) {
503 this._elt = null;
504 this._type = type;
505 this._token = null;
506 this.updater = updater;
507 this._mouseout = this.mouseout.bind(this);
508 this.initializer = initializer;
509
510 var self = this;
511
512 if (!supportedLanguages) {
513 supportedLanguages = ['*'];
514 }
515 if (!Array.isArray(supportedLanguages)) {
516 supportedLanguages = [supportedLanguages];
517 }
518 supportedLanguages.forEach(function (lang) {
519 if (typeof lang !== 'string') {
520 lang = lang.lang;
521 }
522 if (!Previewer.byLanguages[lang]) {
523 Previewer.byLanguages[lang] = [];
524 }
525 if (Previewer.byLanguages[lang].indexOf(self) < 0) {
526 Previewer.byLanguages[lang].push(self);
527 }
528 });
529 Previewer.byType[type] = this;
530 };
531
532 /**
533 * Creates the HTML element for the previewer.
534 */
535 Previewer.prototype.init = function () {
536 if (this._elt) {
537 return;
538 }
539 this._elt = document.createElement('div');
540 this._elt.className = 'prism-previewer prism-previewer-' + this._type;
541 document.body.appendChild(this._elt);
542 if (this.initializer) {
543 this.initializer();
544 }
545 };
546
547 /**
548 * @param {Element} token
549 * @returns {boolean}
550 */
551 Previewer.prototype.isDisabled = function (token) {
552 do {
553 if (token.hasAttribute && token.hasAttribute('data-previewers')) {
554 var previewers = token.getAttribute('data-previewers');
555 return (previewers || '').split(/\s+/).indexOf(this._type) === -1;
556 }
557 } while ((token = token.parentNode));
558 return false;
559 };
560
561 /**
562 * Checks the class name of each hovered element
563 *
564 * @param {Element} token
565 */
566 Previewer.prototype.check = function (token) {
567 if (token.classList.contains(TOKEN_CLASS) && this.isDisabled(token)) {
568 return;
569 }
570 do {
571 if (token.classList && token.classList.contains(TOKEN_CLASS) && token.classList.contains(this._type)) {
572 break;
573 }
574 } while ((token = token.parentNode));
575
576 if (token && token !== this._token) {
577 this._token = token;
578 this.show();
579 }
580 };
581
582 /**
583 * Called on mouseout
584 */
585 Previewer.prototype.mouseout = function () {
586 this._token.removeEventListener('mouseout', this._mouseout, false);
587 this._token = null;
588 this.hide();
589 };
590
591 /**
592 * Shows the previewer positioned properly for the current token.
593 */
594 Previewer.prototype.show = function () {
595 if (!this._elt) {
596 this.init();
597 }
598 if (!this._token) {
599 return;
600 }
601
602 if (this.updater.call(this._elt, this._token.textContent)) {
603 this._token.addEventListener('mouseout', this._mouseout, false);
604
605 var offset = getOffset(this._token);
606 this._elt.classList.add(ACTIVE_CLASS);
607
608 if (offset.top - this._elt.offsetHeight > 0) {
609 this._elt.classList.remove(FLIPPED_CLASS);
610 this._elt.style.top = offset.top + 'px';
611 this._elt.style.bottom = '';
612 } else {
613 this._elt.classList.add(FLIPPED_CLASS);
614 this._elt.style.bottom = offset.bottom + 'px';
615 this._elt.style.top = '';
616 }
617
618 this._elt.style.left = offset.left + Math.min(200, offset.width / 2) + 'px';
619 } else {
620 this.hide();
621 }
622 };
623
624 /**
625 * Hides the previewer.
626 */
627 Previewer.prototype.hide = function () {
628 this._elt.classList.remove(ACTIVE_CLASS);
629 };
630
631 /**
632 * Map of all registered previewers by language
633 *
634 * @type {{}}
635 */
636 Previewer.byLanguages = {};
637
638 /**
639 * Map of all registered previewers by type
640 *
641 * @type {{}}
642 */
643 Previewer.byType = {};
644
645 /**
646 * Initializes the mouseover event on the code block.
647 *
648 * @param {HTMLElement} elt The code block (env.element)
649 * @param {string} lang The language (env.language)
650 */
651 Previewer.initEvents = function (elt, lang) {
652 var previewers = [];
653 if (Previewer.byLanguages[lang]) {
654 previewers = previewers.concat(Previewer.byLanguages[lang]);
655 }
656 if (Previewer.byLanguages['*']) {
657 previewers = previewers.concat(Previewer.byLanguages['*']);
658 }
659 elt.addEventListener('mouseover', function (e) {
660 var target = e.target;
661 previewers.forEach(function (previewer) {
662 previewer.check(target);
663 });
664 }, false);
665 };
666 Prism.plugins.Previewer = Previewer;
667
668 Prism.hooks.add('before-highlight', function (env) {
669 for (var previewer in previewers) {
670 var languages = previewers[previewer].languages;
671 if (env.language && languages[env.language] && !languages[env.language].initialized) {
672 var lang = languages[env.language];
673 if (!Array.isArray(lang)) {
674 lang = [lang];
675 }
676 lang.forEach(function (lang) {
677 var before; var inside; var root; var skip;
678 if (lang === true) {
679 before = 'important';
680 inside = env.language;
681 lang = env.language;
682 } else {
683 before = lang.before || 'important';
684 inside = lang.inside || lang.lang;
685 root = lang.root || Prism.languages;
686 skip = lang.skip;
687 lang = env.language;
688 }
689
690 if (!skip && Prism.languages[lang]) {
691 Prism.languages.insertBefore(inside, before, previewers[previewer].tokens, root);
692 env.grammar = Prism.languages[lang];
693
694 languages[env.language] = { initialized: true };
695 }
696 });
697 }
698 }
699 });
700
701 // Initialize the previewers only when needed
702 Prism.hooks.add('after-highlight', function (env) {
703 if (Previewer.byLanguages['*'] || Previewer.byLanguages[env.language]) {
704 Previewer.initEvents(env.element, env.language);
705 }
706 });
707
708 for (var previewer in previewers) {
709 previewers[previewer].create();
710 }
711
712}());