prism-match-braces.js 4.4 KB

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