Skip to content

Commit

Permalink
New Match Braces plugin (#1944)
Browse files Browse the repository at this point in the history
This adds a new plugin to highlight matching braces in highlighted code.
  • Loading branch information
RunDevelopment committed Sep 4, 2019
1 parent 56a8711 commit 365faad
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 1 deletion.
2 changes: 1 addition & 1 deletion components.js

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions components.json
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,10 @@
"require": "toolbar",
"noCSS": true
},
"match-braces": {
"title": "Match braces",
"owner": "RunDevelopment"
},
"diff-highlight": {
"title": "Diff Highlight",
"owner": "RunDevelopment",
Expand Down
75 changes: 75 additions & 0 deletions plugins/match-braces/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>

<meta charset="utf-8" />
<link rel="icon" href="favicon.png" />
<title>Match braces ▲ Prism plugins</title>
<base href="../.." />
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="themes/prism.css" data-noprefix />
<link rel="stylesheet" href="plugins/match-braces/prism-match-braces.css" data-noprefix />
<script src="scripts/prefixfree.min.js"></script>

<script>var _gaq = [['_setAccount', 'UA-33746269-1'], ['_trackPageview']];</script>
<script src="https://www.google-analytics.com/ga.js" async></script>
</head>
<body class="language-none">

<header>
<div class="intro" data-src="templates/header-plugins.html" data-type="text/html"></div>

<h2>Match braces</h2>
<p>Highlights matching braces.</p>
</header>

<section class="language-markup">
<h1>How to use</h1>

<p>To enable this plugin add the <code>match-braces</code> class to a code block:</p>

<pre><code>&lt;pre>&lt;code class="language-xxxx match-braces">...&lt;/pre>&lt;/code></code></pre>

<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>

<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>
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>

<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>
The selection effect can be disabled by adding the <code>no-brace-select</code> to the code block. This class can also be
inherited.</p>

<h2>Rainbow braces &#x1F308;</h2>

<p>To enable rainbow braces, simply add the <code>rainbow-braces</code> class to a code block. This class can also get inherited.</p>
</section>

<section class="match-braces">
<h1>Examples</h1>

<h2>JavaScript</h2>
<pre data-src="plugins/match-braces/prism-match-braces.js"></pre>

<h2>Lisp</h2>
<pre class="language-lisp"><code>(defun factorial (n)
(if (= n 0) 1
(* n (factorial (- n 1)))))</code></pre>

<h2>Lisp with rainbow braces &#x1F308; but without hover</h2>
<pre class="language-lisp rainbow-braces no-brace-hover"><code>(defun factorial (n)
(if (= n 0) 1
(* n (factorial (- n 1)))))</code></pre>
</section>

<footer data-src="templates/footer.html" data-type="text/html"></footer>

<script src="prism.js"></script>
<script src="plugins/match-braces/prism-match-braces.js"></script>
<script src="plugins/autoloader/prism-autoloader.js" data-autoloader-path="components/"></script>
<script src="scripts/utopia.js"></script>
<script src="components.js"></script>
<script src="scripts/code.js"></script>


</body>
</html>
29 changes: 29 additions & 0 deletions plugins/match-braces/prism-match-braces.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.token.punctuation.brace-hover,
.token.punctuation.brace-selected {
outline: solid 1px;
}

.rainbow-braces .token.punctuation.brace-level-1,
.rainbow-braces .token.punctuation.brace-level-5,
.rainbow-braces .token.punctuation.brace-level-9 {
color: #E50;
opacity: 1;
}
.rainbow-braces .token.punctuation.brace-level-2,
.rainbow-braces .token.punctuation.brace-level-6,
.rainbow-braces .token.punctuation.brace-level-10 {
color: #0B3;
opacity: 1;
}
.rainbow-braces .token.punctuation.brace-level-3,
.rainbow-braces .token.punctuation.brace-level-7,
.rainbow-braces .token.punctuation.brace-level-11 {
color: #26F;
opacity: 1;
}
.rainbow-braces .token.punctuation.brace-level-4,
.rainbow-braces .token.punctuation.brace-level-8,
.rainbow-braces .token.punctuation.brace-level-12 {
color: #E0E;
opacity: 1;
}
184 changes: 184 additions & 0 deletions plugins/match-braces/prism-match-braces.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
(function () {

if (typeof self === 'undefined' || !self.Prism || !self.document) {
return;
}

var MATCH_ALL_CLASS = /(?:^|\s)match-braces(?:\s|$)/;

var BRACE_HOVER_CLASS = /(?:^|\s)brace-hover(?:\s|$)/;
var BRACE_SELECTED_CLASS = /(?:^|\s)brace-selected(?:\s|$)/;

var NO_BRACE_HOVER_CLASS = /(?:^|\s)no-brace-hover(?:\s|$)/;
var NO_BRACE_SELECT_CLASS = /(?:^|\s)no-brace-select(?:\s|$)/;

var PARTNER = {
'(': ')',
'[': ']',
'{': '}',
};

var NAMES = {
'(': 'brace-round',
'[': 'brace-square',
'{': 'brace-curly',
};

var LEVEL_WARP = 12;

var pairIdCounter = 0;

var BRACE_ID_PATTERN = /^(pair-\d+-)(open|close)$/;

/**
* Returns the brace partner given one brace of a brace pair.
*
* @param {HTMLElement} brace
* @returns {HTMLElement}
*/
function getPartnerBrace(brace) {
var match = BRACE_ID_PATTERN.exec(brace.id);
return document.querySelector('#' + match[1] + (match[2] == 'open' ? 'close' : 'open'));
}

/**
* @this {HTMLElement}
*/
function hoverBrace() {
for (var parent = this.parentElement; parent; parent = parent.parentElement) {
if (NO_BRACE_HOVER_CLASS.test(parent.className)) {
return;
}
}

[this, getPartnerBrace(this)].forEach(function (ele) {
ele.className = (ele.className.replace(BRACE_HOVER_CLASS, ' ') + ' brace-hover').replace(/\s+/g, ' ');
});
}
/**
* @this {HTMLElement}
*/
function leaveBrace() {
[this, getPartnerBrace(this)].forEach(function (ele) {
ele.className = ele.className.replace(BRACE_HOVER_CLASS, ' ');
});
}
/**
* @this {HTMLElement}
*/
function clickBrace() {
for (var parent = this.parentElement; parent; parent = parent.parentElement) {
if (NO_BRACE_SELECT_CLASS.test(parent.className)) {
return;
}
}

[this, getPartnerBrace(this)].forEach(function (ele) {
ele.className = (ele.className.replace(BRACE_SELECTED_CLASS, ' ') + ' brace-selected').replace(/\s+/g, ' ');
});
}

Prism.hooks.add('complete', function (env) {

/** @type {HTMLElement} */
var code = env.element;
var pre = code.parentElement;

if (!pre || pre.tagName != 'PRE') {
return;
}

// find the braces to match
/** @type {string[]} */
var toMatch = [];
for (var ele = code; ele; ele = ele.parentElement) {
if (MATCH_ALL_CLASS.test(ele.className)) {
toMatch.push('(', '[', '{');
break;
}
}

if (toMatch.length == 0) {
// nothing to match
return;
}

if (!pre.__listenerAdded) {
// code blocks might be highlighted more than once
pre.addEventListener('mousedown', function removeBraceSelected() {
// the code element might have been replaced
var code = pre.querySelector('code');
Array.prototype.slice.call(code.querySelectorAll('.brace-selected')).forEach(function (element) {
element.className = element.className.replace(BRACE_SELECTED_CLASS, ' ');
});
});
Object.defineProperty(pre, '__listenerAdded', { value: true });
}

/** @type {HTMLSpanElement[]} */
var punctuation = Array.prototype.slice.call(code.querySelectorAll('span.token.punctuation'));

/** @type {{ index: number, open: boolean, element: HTMLElement }[]} */
var allBraces = [];

toMatch.forEach(function (open) {
var close = PARTNER[open];
var name = NAMES[open];

/** @type {[number, number][]} */
var pairs = [];
/** @type {number[]} */
var openStack = [];

for (var i = 0; i < punctuation.length; i++) {
var element = punctuation[i];
if (element.childElementCount == 0) {
var text = element.textContent;
if (text === open) {
allBraces.push({ index: i, open: true, element: element });
element.className += ' ' + name;
element.className += ' brace-open';
openStack.push(i);
} else if (text === close) {
allBraces.push({ index: i, open: false, element: element });
element.className += ' ' + name;
element.className += ' brace-close';
if (openStack.length) {
pairs.push([i, openStack.pop()]);
}
}
}
}

pairs.forEach(function (pair) {
var pairId = 'pair-' + (pairIdCounter++) + '-';

var openEle = punctuation[pair[0]];
var closeEle = punctuation[pair[1]];

openEle.id = pairId + 'open';
closeEle.id = pairId + 'close';

[openEle, closeEle].forEach(function (ele) {
ele.addEventListener('mouseenter', hoverBrace);
ele.addEventListener('mouseleave', leaveBrace);
ele.addEventListener('click', clickBrace);
});
});
});

var level = 0;
allBraces.sort(function (a, b) { return a.index - b.index; });
allBraces.forEach(function (brace) {
if (brace.open) {
brace.element.className += ' brace-level-' + (level % LEVEL_WARP + 1);
level++;
} else {
level = Math.max(0, level - 1);
brace.element.className += ' brace-level-' + (level % LEVEL_WARP + 1);
}
});

});

}());
1 change: 1 addition & 0 deletions plugins/match-braces/prism-match-braces.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 365faad

Please sign in to comment.