prism-match-braces.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. (function () {
  2. if (typeof Prism === 'undefined' || typeof document === 'undefined') {
  3. return;
  4. }
  5. function mapClassName(name) {
  6. var customClass = Prism.plugins.customClass;
  7. if (customClass) {
  8. return customClass.apply(name, 'none');
  9. } else {
  10. return name;
  11. }
  12. }
  13. var PARTNER = {
  14. '(': ')',
  15. '[': ']',
  16. '{': '}',
  17. };
  18. // The names for brace types.
  19. // These names have two purposes: 1) they can be used for styling and 2) they are used to pair braces. Only braces
  20. // of the same type are paired.
  21. var NAMES = {
  22. '(': 'brace-round',
  23. '[': 'brace-square',
  24. '{': 'brace-curly',
  25. };
  26. // A map for brace aliases.
  27. // This is useful for when some braces have a prefix/suffix as part of the punctuation token.
  28. var BRACE_ALIAS_MAP = {
  29. '${': '{', // JS template punctuation (e.g. `foo ${bar + 1}`)
  30. };
  31. var LEVEL_WARP = 12;
  32. var pairIdCounter = 0;
  33. var BRACE_ID_PATTERN = /^(pair-\d+-)(close|open)$/;
  34. /**
  35. * Returns the brace partner given one brace of a brace pair.
  36. *
  37. * @param {HTMLElement} brace
  38. * @returns {HTMLElement}
  39. */
  40. function getPartnerBrace(brace) {
  41. var match = BRACE_ID_PATTERN.exec(brace.id);
  42. return document.querySelector('#' + match[1] + (match[2] == 'open' ? 'close' : 'open'));
  43. }
  44. /**
  45. * @this {HTMLElement}
  46. */
  47. function hoverBrace() {
  48. if (!Prism.util.isActive(this, 'brace-hover', true)) {
  49. return;
  50. }
  51. [this, getPartnerBrace(this)].forEach(function (e) {
  52. e.classList.add(mapClassName('brace-hover'));
  53. });
  54. }
  55. /**
  56. * @this {HTMLElement}
  57. */
  58. function leaveBrace() {
  59. [this, getPartnerBrace(this)].forEach(function (e) {
  60. e.classList.remove(mapClassName('brace-hover'));
  61. });
  62. }
  63. /**
  64. * @this {HTMLElement}
  65. */
  66. function clickBrace() {
  67. if (!Prism.util.isActive(this, 'brace-select', true)) {
  68. return;
  69. }
  70. [this, getPartnerBrace(this)].forEach(function (e) {
  71. e.classList.add(mapClassName('brace-selected'));
  72. });
  73. }
  74. Prism.hooks.add('complete', function (env) {
  75. /** @type {HTMLElement} */
  76. var code = env.element;
  77. var pre = code.parentElement;
  78. if (!pre || pre.tagName != 'PRE') {
  79. return;
  80. }
  81. // find the braces to match
  82. /** @type {string[]} */
  83. var toMatch = [];
  84. if (Prism.util.isActive(code, 'match-braces')) {
  85. toMatch.push('(', '[', '{');
  86. }
  87. if (toMatch.length == 0) {
  88. // nothing to match
  89. return;
  90. }
  91. if (!pre.__listenerAdded) {
  92. // code blocks might be highlighted more than once
  93. pre.addEventListener('mousedown', function removeBraceSelected() {
  94. // the code element might have been replaced
  95. var code = pre.querySelector('code');
  96. var className = mapClassName('brace-selected');
  97. Array.prototype.slice.call(code.querySelectorAll('.' + className)).forEach(function (e) {
  98. e.classList.remove(className);
  99. });
  100. });
  101. Object.defineProperty(pre, '__listenerAdded', { value: true });
  102. }
  103. /** @type {HTMLSpanElement[]} */
  104. var punctuation = Array.prototype.slice.call(
  105. code.querySelectorAll('span.' + mapClassName('token') + '.' + mapClassName('punctuation'))
  106. );
  107. /** @type {{ index: number, open: boolean, element: HTMLElement }[]} */
  108. var allBraces = [];
  109. toMatch.forEach(function (open) {
  110. var close = PARTNER[open];
  111. var name = mapClassName(NAMES[open]);
  112. /** @type {[number, number][]} */
  113. var pairs = [];
  114. /** @type {number[]} */
  115. var openStack = [];
  116. for (var i = 0; i < punctuation.length; i++) {
  117. var element = punctuation[i];
  118. if (element.childElementCount == 0) {
  119. var text = element.textContent;
  120. text = BRACE_ALIAS_MAP[text] || text;
  121. if (text === open) {
  122. allBraces.push({ index: i, open: true, element: element });
  123. element.classList.add(name);
  124. element.classList.add(mapClassName('brace-open'));
  125. openStack.push(i);
  126. } else if (text === close) {
  127. allBraces.push({ index: i, open: false, element: element });
  128. element.classList.add(name);
  129. element.classList.add(mapClassName('brace-close'));
  130. if (openStack.length) {
  131. pairs.push([i, openStack.pop()]);
  132. }
  133. }
  134. }
  135. }
  136. pairs.forEach(function (pair) {
  137. var pairId = 'pair-' + (pairIdCounter++) + '-';
  138. var opening = punctuation[pair[0]];
  139. var closing = punctuation[pair[1]];
  140. opening.id = pairId + 'open';
  141. closing.id = pairId + 'close';
  142. [opening, closing].forEach(function (e) {
  143. e.addEventListener('mouseenter', hoverBrace);
  144. e.addEventListener('mouseleave', leaveBrace);
  145. e.addEventListener('click', clickBrace);
  146. });
  147. });
  148. });
  149. var level = 0;
  150. allBraces.sort(function (a, b) { return a.index - b.index; });
  151. allBraces.forEach(function (brace) {
  152. if (brace.open) {
  153. brace.element.classList.add(mapClassName('brace-level-' + (level % LEVEL_WARP + 1)));
  154. level++;
  155. } else {
  156. level = Math.max(0, level - 1);
  157. brace.element.classList.add(mapClassName('brace-level-' + (level % LEVEL_WARP + 1)));
  158. }
  159. });
  160. });
  161. }());