UNPKG

20.1 kBJavaScriptView Raw
1(function() {
2
3 if (
4 typeof self !== 'undefined' && !self.Prism ||
5 !self.document || !Function.prototype.bind
6 ) {
7 return;
8 }
9
10 var previewers = {
11 // gradient must be defined before color and angle
12 'gradient': {
13 create: (function () {
14
15 // Stores already processed gradients so that we don't
16 // make the conversion every time the previewer is shown
17 var cache = {};
18
19 /**
20 * Returns a W3C-valid linear gradient
21 * @param {string} prefix Vendor prefix if any ("-moz-", "-webkit-", etc.)
22 * @param {string} func Gradient function name ("linear-gradient")
23 * @param {string[]} values Array of the gradient function parameters (["0deg", "red 0%", "blue 100%"])
24 */
25 var convertToW3CLinearGradient = function(prefix, func, values) {
26 // Default value for angle
27 var angle = '180deg';
28
29 if (/^(?:-?\d*\.?\d+(?:deg|rad)|to\b|top|right|bottom|left)/.test(values[0])) {
30 angle = values.shift();
31 if (angle.indexOf('to ') < 0) {
32 // Angle uses old keywords
33 // W3C syntax uses "to" + opposite keywords
34 if (angle.indexOf('top') >= 0) {
35 if (angle.indexOf('left') >= 0) {
36 angle = 'to bottom right';
37 } else if (angle.indexOf('right') >= 0) {
38 angle = 'to bottom left';
39 } else {
40 angle = 'to bottom';
41 }
42 } else if (angle.indexOf('bottom') >= 0) {
43 if (angle.indexOf('left') >= 0) {
44 angle = 'to top right';
45 } else if (angle.indexOf('right') >= 0) {
46 angle = 'to top left';
47 } else {
48 angle = 'to top';
49 }
50 } else if (angle.indexOf('left') >= 0) {
51 angle = 'to right';
52 } else if (angle.indexOf('right') >= 0) {
53 angle = 'to left';
54 } else if (prefix) {
55 // Angle is shifted by 90deg in prefixed gradients
56 if (angle.indexOf('deg') >= 0) {
57 angle = (90 - parseFloat(angle)) + 'deg';
58 } else if (angle.indexOf('rad') >= 0) {
59 angle = (Math.PI / 2 - parseFloat(angle)) + 'rad';
60 }
61 }
62 }
63 }
64
65 return func + '(' + angle + ',' + values.join(',') + ')';
66 };
67
68 /**
69 * Returns a W3C-valid radial gradient
70 * @param {string} prefix Vendor prefix if any ("-moz-", "-webkit-", etc.)
71 * @param {string} func Gradient function name ("linear-gradient")
72 * @param {string[]} values Array of the gradient function parameters (["0deg", "red 0%", "blue 100%"])
73 */
74 var convertToW3CRadialGradient = function(prefix, func, values) {
75 if (values[0].indexOf('at') < 0) {
76 // Looks like old syntax
77
78 // Default values
79 var position = 'center';
80 var shape = 'ellipse';
81 var size = 'farthest-corner';
82
83 if (/\bcenter|top|right|bottom|left\b|^\d+/.test(values[0])) {
84 // Found a position
85 // Remove angle value, if any
86 position = values.shift().replace(/\s*-?\d+(?:rad|deg)\s*/, '');
87 }
88 if (/\bcircle|ellipse|closest|farthest|contain|cover\b/.test(values[0])) {
89 // Found a shape and/or size
90 var shapeSizeParts = values.shift().split(/\s+/);
91 if (shapeSizeParts[0] && (shapeSizeParts[0] === 'circle' || shapeSizeParts[0] === 'ellipse')) {
92 shape = shapeSizeParts.shift();
93 }
94 if (shapeSizeParts[0]) {
95 size = shapeSizeParts.shift();
96 }
97
98 // Old keywords are converted to their synonyms
99 if (size === 'cover') {
100 size = 'farthest-corner';
101 } else if (size === 'contain') {
102 size = 'clothest-side';
103 }
104 }
105
106 return func + '(' + shape + ' ' + size + ' at ' + position + ',' + values.join(',') + ')';
107 }
108 return func + '(' + values.join(',') + ')';
109 };
110
111 /**
112 * Converts a gradient to a W3C-valid one
113 * Does not support old webkit syntax (-webkit-gradient(linear...) and -webkit-gradient(radial...))
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\((?:(?:rgb|hsl)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, 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+(?: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+/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(' + location.href + '#prism-previewer-easing-marker)" marker-end="url(' + location.href + '#prism-previewer-easing-marker)" />' +
357 '<line x1="100" y1="0" x2="40" y2="30" marker-start="url(' + location.href + '#prism-previewer-easing-marker)" marker-end="url(' + location.href + '#prism-previewer-easing-marker)" />' +
358 '</svg>';
359 });
360 },
361 tokens: {
362 'easing': {
363 pattern: /\bcubic-bezier\((?:-?\d*\.?\d+,\s*){3}-?\d*\.?\d+\)\B|\b(?:linear|ease(?:-in)?(?:-out)?)(?=\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+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 * @param {HTMLElement} element
468 * @returns {{top: number, right: number, bottom: number, left: number, width: number, height: number}}
469 */
470 var getOffset = function (element) {
471 var elementBounds = element.getBoundingClientRect();
472 var left = elementBounds.left;
473 var top = elementBounds.top;
474 var documentBounds = document.documentElement.getBoundingClientRect();
475 left -= documentBounds.left;
476 top -= documentBounds.top;
477
478 return {
479 top: top,
480 right: innerWidth - left - elementBounds.width,
481 bottom: innerHeight - top - elementBounds.height,
482 left: left,
483 width: elementBounds.width,
484 height: elementBounds.height
485 };
486 };
487
488 var tokenRegexp = /(?:^|\s)token(?=$|\s)/;
489 var activeRegexp = /(?:^|\s)active(?=$|\s)/g;
490 var flippedRegexp = /(?:^|\s)flipped(?=$|\s)/g;
491
492 /**
493 * Previewer constructor
494 * @param {string} type Unique previewer type
495 * @param {function} updater Function that will be called on mouseover.
496 * @param {string[]|string=} supportedLanguages Aliases of the languages this previewer must be enabled for. Defaults to "*", all languages.
497 * @param {function=} initializer Function that will be called on initialization.
498 * @constructor
499 */
500 var Previewer = function (type, updater, supportedLanguages, initializer) {
501 this._elt = null;
502 this._type = type;
503 this._clsRegexp = RegExp('(?:^|\\s)' + type + '(?=$|\\s)');
504 this._token = null;
505 this.updater = updater;
506 this._mouseout = this.mouseout.bind(this);
507 this.initializer = initializer;
508
509 var self = this;
510
511 if (!supportedLanguages) {
512 supportedLanguages = ['*'];
513 }
514 if (!Array.isArray(supportedLanguages)) {
515 supportedLanguages = [supportedLanguages];
516 }
517 supportedLanguages.forEach(function (lang) {
518 if (typeof lang !== 'string') {
519 lang = lang.lang;
520 }
521 if (!Previewer.byLanguages[lang]) {
522 Previewer.byLanguages[lang] = [];
523 }
524 if (Previewer.byLanguages[lang].indexOf(self) < 0) {
525 Previewer.byLanguages[lang].push(self);
526 }
527 });
528 Previewer.byType[type] = this;
529 };
530
531 /**
532 * Creates the HTML element for the previewer.
533 */
534 Previewer.prototype.init = function () {
535 if (this._elt) {
536 return;
537 }
538 this._elt = document.createElement('div');
539 this._elt.className = 'prism-previewer prism-previewer-' + this._type;
540 document.body.appendChild(this._elt);
541 if(this.initializer) {
542 this.initializer();
543 }
544 };
545
546 Previewer.prototype.isDisabled = function (token) {
547 do {
548 if (token.hasAttribute && token.hasAttribute('data-previewers')) {
549 var previewers = token.getAttribute('data-previewers');
550 return (previewers || '').split(/\s+/).indexOf(this._type) === -1;
551 }
552 } while(token = token.parentNode);
553 return false;
554 };
555
556 /**
557 * Checks the class name of each hovered element
558 * @param token
559 */
560 Previewer.prototype.check = function (token) {
561 if (tokenRegexp.test(token.className) && this.isDisabled(token)) {
562 return;
563 }
564 do {
565 if (tokenRegexp.test(token.className) && this._clsRegexp.test(token.className)) {
566 break;
567 }
568 } while(token = token.parentNode);
569
570 if (token && token !== this._token) {
571 this._token = token;
572 this.show();
573 }
574 };
575
576 /**
577 * Called on mouseout
578 */
579 Previewer.prototype.mouseout = function() {
580 this._token.removeEventListener('mouseout', this._mouseout, false);
581 this._token = null;
582 this.hide();
583 };
584
585 /**
586 * Shows the previewer positioned properly for the current token.
587 */
588 Previewer.prototype.show = function () {
589 if (!this._elt) {
590 this.init();
591 }
592 if (!this._token) {
593 return;
594 }
595
596 if (this.updater.call(this._elt, this._token.textContent)) {
597 this._token.addEventListener('mouseout', this._mouseout, false);
598
599 var offset = getOffset(this._token);
600 this._elt.className += ' active';
601
602 if (offset.top - this._elt.offsetHeight > 0) {
603 this._elt.className = this._elt.className.replace(flippedRegexp, '');
604 this._elt.style.top = offset.top + 'px';
605 this._elt.style.bottom = '';
606 } else {
607 this._elt.className += ' flipped';
608 this._elt.style.bottom = offset.bottom + 'px';
609 this._elt.style.top = '';
610 }
611
612 this._elt.style.left = offset.left + Math.min(200, offset.width / 2) + 'px';
613 } else {
614 this.hide();
615 }
616 };
617
618 /**
619 * Hides the previewer.
620 */
621 Previewer.prototype.hide = function () {
622 this._elt.className = this._elt.className.replace(activeRegexp, '');
623 };
624
625 /**
626 * Map of all registered previewers by language
627 * @type {{}}
628 */
629 Previewer.byLanguages = {};
630
631 /**
632 * Map of all registered previewers by type
633 * @type {{}}
634 */
635 Previewer.byType = {};
636
637 /**
638 * Initializes the mouseover event on the code block.
639 * @param {HTMLElement} elt The code block (env.element)
640 * @param {string} lang The language (env.language)
641 */
642 Previewer.initEvents = function (elt, lang) {
643 var previewers = [];
644 if (Previewer.byLanguages[lang]) {
645 previewers = previewers.concat(Previewer.byLanguages[lang]);
646 }
647 if (Previewer.byLanguages['*']) {
648 previewers = previewers.concat(Previewer.byLanguages['*']);
649 }
650 elt.addEventListener('mouseover', function (e) {
651 var target = e.target;
652 previewers.forEach(function (previewer) {
653 previewer.check(target);
654 });
655 }, false);
656 };
657 Prism.plugins.Previewer = Previewer;
658
659 Prism.hooks.add('before-highlight', function (env) {
660 for (var previewer in previewers) {
661 var languages = previewers[previewer].languages;
662 if (env.language && languages[env.language] && !languages[env.language].initialized) {
663 var lang = languages[env.language];
664 if (!Array.isArray(lang)) {
665 lang = [lang];
666 }
667 lang.forEach(function (lang) {
668 var before, inside, root, skip;
669 if (lang === true) {
670 before = 'important';
671 inside = env.language;
672 lang = env.language;
673 } else {
674 before = lang.before || 'important';
675 inside = lang.inside || lang.lang;
676 root = lang.root || Prism.languages;
677 skip = lang.skip;
678 lang = env.language;
679 }
680
681 if (!skip && Prism.languages[lang]) {
682 Prism.languages.insertBefore(inside, before, previewers[previewer].tokens, root);
683 env.grammar = Prism.languages[lang];
684
685 languages[env.language] = {initialized: true};
686 }
687 });
688 }
689 }
690 });
691
692 // Initialize the previewers only when needed
693 Prism.hooks.add('after-highlight', function (env) {
694 if(Previewer.byLanguages['*'] || Previewer.byLanguages[env.language]) {
695 Previewer.initEvents(env.element, env.language);
696 }
697 });
698
699 for (var previewer in previewers) {
700 previewers[previewer].create();
701 }
702
703}());