Skip to content

Commit 365faad

Browse files
New Match Braces plugin (#1944)
This adds a new plugin to highlight matching braces in highlighted code.
1 parent 56a8711 commit 365faad

File tree

6 files changed

+294
-1
lines changed

6 files changed

+294
-1
lines changed

components.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components.json

+4
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,10 @@
11301130
"require": "toolbar",
11311131
"noCSS": true
11321132
},
1133+
"match-braces": {
1134+
"title": "Match braces",
1135+
"owner": "RunDevelopment"
1136+
},
11331137
"diff-highlight": {
11341138
"title": "Diff Highlight",
11351139
"owner": "RunDevelopment",

plugins/match-braces/index.html

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
5+
<meta charset="utf-8" />
6+
<link rel="icon" href="favicon.png" />
7+
<title>Match braces ▲ Prism plugins</title>
8+
<base href="../.." />
9+
<link rel="stylesheet" href="style.css" />
10+
<link rel="stylesheet" href="themes/prism.css" data-noprefix />
11+
<link rel="stylesheet" href="plugins/match-braces/prism-match-braces.css" data-noprefix />
12+
<script src="scripts/prefixfree.min.js"></script>
13+
14+
<script>var _gaq = [['_setAccount', 'UA-33746269-1'], ['_trackPageview']];</script>
15+
<script src="https://www.google-analytics.com/ga.js" async></script>
16+
</head>
17+
<body class="language-none">
18+
19+
<header>
20+
<div class="intro" data-src="templates/header-plugins.html" data-type="text/html"></div>
21+
22+
<h2>Match braces</h2>
23+
<p>Highlights matching braces.</p>
24+
</header>
25+
26+
<section class="language-markup">
27+
<h1>How to use</h1>
28+
29+
<p>To enable this plugin add the <code>match-braces</code> class to a code block:</p>
30+
31+
<pre><code>&lt;pre>&lt;code class="language-xxxx match-braces">...&lt;/pre>&lt;/code></code></pre>
32+
33+
<p>Just like <code>language-xxxx</code>, the <code>match-braces</code> class is inherited, so you can add the class to the <code>&lt;body></code> to enable the plugin for the whole page.</p>
34+
35+
<p>The plugin will highlight brace pairs when the cursor hovers over one of the braces. The highlighting effect will disappear as soon as the cursor leaves the brace pair.<br>
36+
The hover effect can be disabled by adding the <code>no-brace-hover</code> to the code block. This class can also be inherited.</p>
37+
38+
<p>You can also click on a brace to select the brace pair. To deselect the pair, click anywhere within the code block or select another pair.<br>
39+
The selection effect can be disabled by adding the <code>no-brace-select</code> to the code block. This class can also be
40+
inherited.</p>
41+
42+
<h2>Rainbow braces &#x1F308;</h2>
43+
44+
<p>To enable rainbow braces, simply add the <code>rainbow-braces</code> class to a code block. This class can also get inherited.</p>
45+
</section>
46+
47+
<section class="match-braces">
48+
<h1>Examples</h1>
49+
50+
<h2>JavaScript</h2>
51+
<pre data-src="plugins/match-braces/prism-match-braces.js"></pre>
52+
53+
<h2>Lisp</h2>
54+
<pre class="language-lisp"><code>(defun factorial (n)
55+
(if (= n 0) 1
56+
(* n (factorial (- n 1)))))</code></pre>
57+
58+
<h2>Lisp with rainbow braces &#x1F308; but without hover</h2>
59+
<pre class="language-lisp rainbow-braces no-brace-hover"><code>(defun factorial (n)
60+
(if (= n 0) 1
61+
(* n (factorial (- n 1)))))</code></pre>
62+
</section>
63+
64+
<footer data-src="templates/footer.html" data-type="text/html"></footer>
65+
66+
<script src="prism.js"></script>
67+
<script src="plugins/match-braces/prism-match-braces.js"></script>
68+
<script src="plugins/autoloader/prism-autoloader.js" data-autoloader-path="components/"></script>
69+
<script src="scripts/utopia.js"></script>
70+
<script src="components.js"></script>
71+
<script src="scripts/code.js"></script>
72+
73+
74+
</body>
75+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.token.punctuation.brace-hover,
2+
.token.punctuation.brace-selected {
3+
outline: solid 1px;
4+
}
5+
6+
.rainbow-braces .token.punctuation.brace-level-1,
7+
.rainbow-braces .token.punctuation.brace-level-5,
8+
.rainbow-braces .token.punctuation.brace-level-9 {
9+
color: #E50;
10+
opacity: 1;
11+
}
12+
.rainbow-braces .token.punctuation.brace-level-2,
13+
.rainbow-braces .token.punctuation.brace-level-6,
14+
.rainbow-braces .token.punctuation.brace-level-10 {
15+
color: #0B3;
16+
opacity: 1;
17+
}
18+
.rainbow-braces .token.punctuation.brace-level-3,
19+
.rainbow-braces .token.punctuation.brace-level-7,
20+
.rainbow-braces .token.punctuation.brace-level-11 {
21+
color: #26F;
22+
opacity: 1;
23+
}
24+
.rainbow-braces .token.punctuation.brace-level-4,
25+
.rainbow-braces .token.punctuation.brace-level-8,
26+
.rainbow-braces .token.punctuation.brace-level-12 {
27+
color: #E0E;
28+
opacity: 1;
29+
}
+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
(function () {
2+
3+
if (typeof self === 'undefined' || !self.Prism || !self.document) {
4+
return;
5+
}
6+
7+
var MATCH_ALL_CLASS = /(?:^|\s)match-braces(?:\s|$)/;
8+
9+
var BRACE_HOVER_CLASS = /(?:^|\s)brace-hover(?:\s|$)/;
10+
var BRACE_SELECTED_CLASS = /(?:^|\s)brace-selected(?:\s|$)/;
11+
12+
var NO_BRACE_HOVER_CLASS = /(?:^|\s)no-brace-hover(?:\s|$)/;
13+
var NO_BRACE_SELECT_CLASS = /(?:^|\s)no-brace-select(?:\s|$)/;
14+
15+
var PARTNER = {
16+
'(': ')',
17+
'[': ']',
18+
'{': '}',
19+
};
20+
21+
var NAMES = {
22+
'(': 'brace-round',
23+
'[': 'brace-square',
24+
'{': 'brace-curly',
25+
};
26+
27+
var LEVEL_WARP = 12;
28+
29+
var pairIdCounter = 0;
30+
31+
var BRACE_ID_PATTERN = /^(pair-\d+-)(open|close)$/;
32+
33+
/**
34+
* Returns the brace partner given one brace of a brace pair.
35+
*
36+
* @param {HTMLElement} brace
37+
* @returns {HTMLElement}
38+
*/
39+
function getPartnerBrace(brace) {
40+
var match = BRACE_ID_PATTERN.exec(brace.id);
41+
return document.querySelector('#' + match[1] + (match[2] == 'open' ? 'close' : 'open'));
42+
}
43+
44+
/**
45+
* @this {HTMLElement}
46+
*/
47+
function hoverBrace() {
48+
for (var parent = this.parentElement; parent; parent = parent.parentElement) {
49+
if (NO_BRACE_HOVER_CLASS.test(parent.className)) {
50+
return;
51+
}
52+
}
53+
54+
[this, getPartnerBrace(this)].forEach(function (ele) {
55+
ele.className = (ele.className.replace(BRACE_HOVER_CLASS, ' ') + ' brace-hover').replace(/\s+/g, ' ');
56+
});
57+
}
58+
/**
59+
* @this {HTMLElement}
60+
*/
61+
function leaveBrace() {
62+
[this, getPartnerBrace(this)].forEach(function (ele) {
63+
ele.className = ele.className.replace(BRACE_HOVER_CLASS, ' ');
64+
});
65+
}
66+
/**
67+
* @this {HTMLElement}
68+
*/
69+
function clickBrace() {
70+
for (var parent = this.parentElement; parent; parent = parent.parentElement) {
71+
if (NO_BRACE_SELECT_CLASS.test(parent.className)) {
72+
return;
73+
}
74+
}
75+
76+
[this, getPartnerBrace(this)].forEach(function (ele) {
77+
ele.className = (ele.className.replace(BRACE_SELECTED_CLASS, ' ') + ' brace-selected').replace(/\s+/g, ' ');
78+
});
79+
}
80+
81+
Prism.hooks.add('complete', function (env) {
82+
83+
/** @type {HTMLElement} */
84+
var code = env.element;
85+
var pre = code.parentElement;
86+
87+
if (!pre || pre.tagName != 'PRE') {
88+
return;
89+
}
90+
91+
// find the braces to match
92+
/** @type {string[]} */
93+
var toMatch = [];
94+
for (var ele = code; ele; ele = ele.parentElement) {
95+
if (MATCH_ALL_CLASS.test(ele.className)) {
96+
toMatch.push('(', '[', '{');
97+
break;
98+
}
99+
}
100+
101+
if (toMatch.length == 0) {
102+
// nothing to match
103+
return;
104+
}
105+
106+
if (!pre.__listenerAdded) {
107+
// code blocks might be highlighted more than once
108+
pre.addEventListener('mousedown', function removeBraceSelected() {
109+
// the code element might have been replaced
110+
var code = pre.querySelector('code');
111+
Array.prototype.slice.call(code.querySelectorAll('.brace-selected')).forEach(function (element) {
112+
element.className = element.className.replace(BRACE_SELECTED_CLASS, ' ');
113+
});
114+
});
115+
Object.defineProperty(pre, '__listenerAdded', { value: true });
116+
}
117+
118+
/** @type {HTMLSpanElement[]} */
119+
var punctuation = Array.prototype.slice.call(code.querySelectorAll('span.token.punctuation'));
120+
121+
/** @type {{ index: number, open: boolean, element: HTMLElement }[]} */
122+
var allBraces = [];
123+
124+
toMatch.forEach(function (open) {
125+
var close = PARTNER[open];
126+
var name = NAMES[open];
127+
128+
/** @type {[number, number][]} */
129+
var pairs = [];
130+
/** @type {number[]} */
131+
var openStack = [];
132+
133+
for (var i = 0; i < punctuation.length; i++) {
134+
var element = punctuation[i];
135+
if (element.childElementCount == 0) {
136+
var text = element.textContent;
137+
if (text === open) {
138+
allBraces.push({ index: i, open: true, element: element });
139+
element.className += ' ' + name;
140+
element.className += ' brace-open';
141+
openStack.push(i);
142+
} else if (text === close) {
143+
allBraces.push({ index: i, open: false, element: element });
144+
element.className += ' ' + name;
145+
element.className += ' brace-close';
146+
if (openStack.length) {
147+
pairs.push([i, openStack.pop()]);
148+
}
149+
}
150+
}
151+
}
152+
153+
pairs.forEach(function (pair) {
154+
var pairId = 'pair-' + (pairIdCounter++) + '-';
155+
156+
var openEle = punctuation[pair[0]];
157+
var closeEle = punctuation[pair[1]];
158+
159+
openEle.id = pairId + 'open';
160+
closeEle.id = pairId + 'close';
161+
162+
[openEle, closeEle].forEach(function (ele) {
163+
ele.addEventListener('mouseenter', hoverBrace);
164+
ele.addEventListener('mouseleave', leaveBrace);
165+
ele.addEventListener('click', clickBrace);
166+
});
167+
});
168+
});
169+
170+
var level = 0;
171+
allBraces.sort(function (a, b) { return a.index - b.index; });
172+
allBraces.forEach(function (brace) {
173+
if (brace.open) {
174+
brace.element.className += ' brace-level-' + (level % LEVEL_WARP + 1);
175+
level++;
176+
} else {
177+
level = Math.max(0, level - 1);
178+
brace.element.className += ' brace-level-' + (level % LEVEL_WARP + 1);
179+
}
180+
});
181+
182+
});
183+
184+
}());

plugins/match-braces/prism-match-braces.min.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)