Browse Source

Merge branch 'line_numbers'

pull/75/head
adam j hartz 1 month ago
parent
commit
6316a5fbf1
Signed by: hz <hartz@mit.edu> GPG Key ID: 5FDD2840E179AD62
7 changed files with 396 additions and 17 deletions
  1. +5
    -0
      CHANGELOG.md
  2. +32
    -0
      LICENSE.bundled_software
  3. +304
    -0
      catsoop/__STATIC__/scripts/highlightjs-line-numbers.js
  4. +12
    -2
      catsoop/__STATIC__/templates/main.template
  5. +15
    -4
      catsoop/__STATIC__/themes/base.css
  6. +5
    -0
      catsoop/__UTIL__/jslicense.html/content.xml
  7. +23
    -11
      catsoop/language.py

+ 5
- 0
CHANGELOG.md View File

@@ -7,6 +7,9 @@ _Work toward next release. Currently under development._
* Added ability to conditionally show or hide HTML elements via the
`cs-show-if` and `cs-hide-if` attributes.

* Added support for showing line numbers next to code snippets by adding
`-lines` to the end of the language specified for a code block.

**CHANGED:**

* Auto-generated `csq_name` fields increment for every question, even those
@@ -17,6 +20,8 @@ _Work toward next release. Currently under development._

* Upgraded [highlight.js](https://highlightjs.org/) to version 10.0.2.

* Some small changes to the way code is displayed, to improve readability.

**DEPRECATED:**

**REMOVED:**


+ 32
- 0
LICENSE.bundled_software View File

@@ -80,3 +80,35 @@ library for math rendering in the browser (https://www.mathjax.org/), in the
__STATIC__/scripts/mathjax directory. Mathjax is licensed under the Apache
License (version 2.0):
https://github.com/mathjax/MathJax/blob/master/LICENSE


########


The CAT-SOOP distribution contains a verbatim copy of
highlightjs-line-numbers.js, which adds support for adding line numbers to code
highlighted by hljs.

This software is licensed according to the following terms:

The MIT License (MIT)

Copyright (c) 2017 Yauheni Pakala

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 304
- 0
catsoop/__STATIC__/scripts/highlightjs-line-numbers.js View File

@@ -0,0 +1,304 @@
// jshint multistr:true

(function (w, d) {
'use strict';

var TABLE_NAME = 'hljs-ln',
LINE_NAME = 'hljs-ln-line',
CODE_BLOCK_NAME = 'hljs-ln-code',
NUMBERS_BLOCK_NAME = 'hljs-ln-numbers',
NUMBER_LINE_NAME = 'hljs-ln-n',
DATA_ATTR_NAME = 'data-line-number',
BREAK_LINE_REGEXP = /\r\n|\r|\n/g;

if (w.hljs) {
w.hljs.initLineNumbersOnLoad = initLineNumbersOnLoad;
w.hljs.lineNumbersBlock = lineNumbersBlock;
w.hljs.lineNumbersValue = lineNumbersValue;

addStyles();
} else {
w.console.error('highlight.js not detected!');
}

function isHljsLnCodeDescendant(domElt) {
var curElt = domElt;
while (curElt) {
if (curElt.className && curElt.className.indexOf('hljs-ln-code') !== -1) {
return true;
}
curElt = curElt.parentNode;
}
return false;
}

function getHljsLnTable(hljsLnDomElt) {
var curElt = hljsLnDomElt;
while (curElt.nodeName !== 'TABLE') {
curElt = curElt.parentNode;
}
return curElt;
}

// Function to workaround a copy issue with Microsoft Edge.
// Due to hljs-ln wrapping the lines of code inside a <table> element,
// itself wrapped inside a <pre> element, window.getSelection().toString()
// does not contain any line breaks. So we need to get them back using the
// rendered code in the DOM as reference.
function edgeGetSelectedCodeLines(selection) {
// current selected text without line breaks
var selectionText = selection.toString();

// get the <td> element wrapping the first line of selected code
var tdAnchor = selection.anchorNode;
while (tdAnchor.nodeName !== 'TD') {
tdAnchor = tdAnchor.parentNode;
}

// get the <td> element wrapping the last line of selected code
var tdFocus = selection.focusNode;
while (tdFocus.nodeName !== 'TD') {
tdFocus = tdFocus.parentNode;
}

// extract line numbers
var firstLineNumber = parseInt(tdAnchor.dataset.lineNumber);
var lastLineNumber = parseInt(tdFocus.dataset.lineNumber);

// multi-lines copied case
if (firstLineNumber != lastLineNumber) {

var firstLineText = tdAnchor.textContent;
var lastLineText = tdFocus.textContent;

// if the selection was made backward, swap values
if (firstLineNumber > lastLineNumber) {
var tmp = firstLineNumber;
firstLineNumber = lastLineNumber;
lastLineNumber = tmp;
tmp = firstLineText;
firstLineText = lastLineText;
lastLineText = tmp;
}

// discard not copied characters in first line
while (selectionText.indexOf(firstLineText) !== 0) {
firstLineText = firstLineText.slice(1);
}

// discard not copied characters in last line
while (selectionText.lastIndexOf(lastLineText) === -1) {
lastLineText = lastLineText.slice(0, -1);
}

// reconstruct and return the real copied text
var selectedText = firstLineText;
var hljsLnTable = getHljsLnTable(tdAnchor);
for (var i = firstLineNumber + 1 ; i < lastLineNumber ; ++i) {
var codeLineSel = format('.{0}[{1}="{2}"]', [CODE_BLOCK_NAME, DATA_ATTR_NAME, i]);
var codeLineElt = hljsLnTable.querySelector(codeLineSel);
selectedText += '\n' + codeLineElt.textContent;
}
selectedText += '\n' + lastLineText;
return selectedText;
// single copied line case
} else {
return selectionText;
}
}

// ensure consistent code copy/paste behavior across all browsers
// (see https://github.com/wcoder/highlightjs-line-numbers.js/issues/51)
document.addEventListener('copy', function(e) {
// get current selection
var selection = window.getSelection();
// override behavior when one wants to copy line of codes
if (isHljsLnCodeDescendant(selection.anchorNode)) {
var selectionText;
// workaround an issue with Microsoft Edge as copied line breaks
// are removed otherwise from the selection string
if (window.navigator.userAgent.indexOf("Edge") !== -1) {
selectionText = edgeGetSelectedCodeLines(selection);
} else {
// other browsers can directly use the selection string
selectionText = selection.toString();
}
e.clipboardData.setData('text/plain', selectionText);
e.preventDefault();
}
});

function addStyles () {
var css = d.createElement('style');
css.type = 'text/css';
css.innerHTML = format(
'.{0}{border-collapse:collapse}' +
'.{0} td{padding:0}' +
'.{1}:before{content:attr({2})}',
[
TABLE_NAME,
NUMBER_LINE_NAME,
DATA_ATTR_NAME
]);
d.getElementsByTagName('head')[0].appendChild(css);
}

function initLineNumbersOnLoad (options) {
if (d.readyState === 'interactive' || d.readyState === 'complete') {
documentReady(options);
} else {
w.addEventListener('DOMContentLoaded', function () {
documentReady(options);
});
}
}

function documentReady (options) {
try {
var blocks = d.querySelectorAll('code.hljs,code.nohighlight');

for (var i in blocks) {
if (blocks.hasOwnProperty(i)) {
lineNumbersBlock(blocks[i], options);
}
}
} catch (e) {
w.console.error('LineNumbers error: ', e);
}
}

function lineNumbersBlock (element, options) {
if (typeof element !== 'object') return;

async(function () {
element.innerHTML = lineNumbersInternal(element, options);
});
}

function lineNumbersValue (value, options) {
if (typeof value !== 'string') return;

var element = document.createElement('code')
element.innerHTML = value

return lineNumbersInternal(element, options);
}

function lineNumbersInternal (element, options) {
// define options or set default
options = options || {
singleLine: false
};

// convert options
var firstLineIndex = !!options.singleLine ? 0 : 1;

duplicateMultilineNodes(element);

return addLineNumbersBlockFor(element.innerHTML, firstLineIndex);
}

function addLineNumbersBlockFor (inputHtml, firstLineIndex) {

var lines = getLines(inputHtml);

// if last line contains only carriage return remove it
if (lines[lines.length-1].trim() === '') {
lines.pop();
}

if (lines.length > firstLineIndex) {
var html = '';

for (var i = 0, l = lines.length; i < l; i++) {
html += format(
'<tr>' +
'<td class="{0} {1}" {3}="{5}">' +
'<div class="{2}" {3}="{5}"></div>' +
'</td>' +
'<td class="{0} {4}" {3}="{5}">' +
'{6}' +
'</td>' +
'</tr>',
[
LINE_NAME,
NUMBERS_BLOCK_NAME,
NUMBER_LINE_NAME,
DATA_ATTR_NAME,
CODE_BLOCK_NAME,
i + 1,
lines[i].length > 0 ? lines[i] : ' '
]);
}

return format('<table class="{0}">{1}</table>', [ TABLE_NAME, html ]);
}

return inputHtml;
}

/**
* Recursive method for fix multi-line elements implementation in highlight.js
* Doing deep passage on child nodes.
* @param {HTMLElement} element
*/
function duplicateMultilineNodes (element) {
var nodes = element.childNodes;
for (var node in nodes) {
if (nodes.hasOwnProperty(node)) {
var child = nodes[node];
if (getLinesCount(child.textContent) > 0) {
if (child.childNodes.length > 0) {
duplicateMultilineNodes(child);
} else {
duplicateMultilineNode(child.parentNode);
}
}
}
}
}

/**
* Method for fix multi-line elements implementation in highlight.js
* @param {HTMLElement} element
*/
function duplicateMultilineNode (element) {
var className = element.className;

if ( ! /hljs-/.test(className)) return;

var lines = getLines(element.innerHTML);

for (var i = 0, result = ''; i < lines.length; i++) {
var lineText = lines[i].length > 0 ? lines[i] : ' ';
result += format('<span class="{0}">{1}</span>\n', [ className, lineText ]);
}

element.innerHTML = result.trim();
}

function getLines (text) {
if (text.length === 0) return [];
return text.split(BREAK_LINE_REGEXP);
}

function getLinesCount (text) {
return (text.trim().match(BREAK_LINE_REGEXP) || []).length;
}

function async (func) {
w.setTimeout(func, 0);
}

/**
* {@link https://wcoder.github.io/notes/string-format-for-string-formating-in-javascript}
* @param {string} format
* @param {array} args
*/
function format (format, args) {
return format.replace(/\{(\d+)\}/g, function(m, n){
return args[n] ? args[n] : m;
});
}

}(window, document));

+ 12
- 2
catsoop/__STATIC__/templates/main.template View File

@@ -19,7 +19,7 @@
document.getElementsByTagName('header')[0].classList.toggle('responsive');
}}

document.addEventListener('DOMContentLoaded', function(){{
window.addEventListener('DOMContentLoaded', function(){{
var menu_boxes = document.getElementsByClassName('dropdown-checkbox');
for(var i=0; i<menu_boxes.length; i++){{
menu_boxes[i].checked = false;
@@ -46,9 +46,19 @@

<!-- Syntax Highlighting -->
<script type="text/javascript" src="BASE/scripts/highlight/highlight.min.js"></script>
<script type="text/javascript" src="BASE/scripts/highlightjs-line-numbers.js"></script>
<script type="text/javascript">
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3
hljs.initHighlightingOnLoad();
hljs.initHighlightingOnLoad();
window.addEventListener('DOMContentLoaded', function(){{
document.querySelectorAll('code.highlight-lines').forEach(function(b){{
hljs.lineNumbersBlock(b);
if ((b.innerHTML.trim().match(/\r\n|\r|\n/g) || []).length > 0){{
b.parentElement.style = "padding-left: 0px !important;"
b.classList.add('hljs');
}}
}});
}});
// @license-end
</script>
<style>


+ 15
- 4
catsoop/__STATIC__/themes/base.css View File

@@ -75,10 +75,9 @@
}
code, kbd, pre, samp, tt {
font-family: Menlo, Consolas, Courier, Courier New, monospace;
font-size: 0.85em;
font-size: 0.95em;
}
blockquote {
font: 14px/22px normal helvetica, sans-serif;
margin-top: 10px;
margin-bottom: 10px;
margin-left: 50px;
@@ -90,14 +89,14 @@
code {
padding: 2px;
border-radius: 5px;
background-color: #eee;
background-color: #f0f0f0;
color: inherit;
}
pre {
padding: 1em;
white-space: pre-wrap;
border-radius: 5px;
background-color: #eee;
background-color: #f0f0f0;
}
pre code {
padding: 0px;
@@ -769,3 +768,15 @@
.hljs-emphasis {
font-style: italic;
}

td.hljs-ln-numbers {
border-right: 2px solid #aaa;
text-align: right;
padding-left: 5px !important;
padding-right: 5px !important;
background-color: #e0e0e0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", Helvetica, sans-serif;
}
td.hljs-ln-code {
padding-left: 10px !important;
}

+ 5
- 0
catsoop/__UTIL__/jslicense.html/content.xml View File

@@ -54,6 +54,11 @@ that are loaded by CAT-SOOP, including licensing information.
<td><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a></td>
<td><a href="BASE/scripts/highlight/highlight.js">highlight.js</a></td>
</tr>
<tr>
<td><a href="BASE/scripts/highlightjs-line-numbers.js">highlightjs-line-numbers.js</a></td>
<td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
<td><a href="BASE/scripts/highlightjs-line-numbers.js">highlightjs-line-numbers.js</a></td>
</tr>
<python>
for i in ace_files:
print("""\


+ 23
- 11
catsoop/language.py View File

@@ -651,7 +651,7 @@ def _make_python_handler(context, fulltext):
# decide whether to show the code
if "show" in opts:
opts.remove("show")
code = '<pre><code class="lang-python">%s</code></pre>'
code = '<pre><code class="language-python">%s</code></pre>'
out += code % html_format(body)
# decide whether to run the code
if "norun" in opts:
@@ -969,16 +969,28 @@ def handle_custom_tags(context, text):

# code blocks: specific default behavior
default_code_class = context.get("cs_default_code_language", "nohighlight")
if default_code_class is not None:
for i in tree.find_all("code"):
if i.parent.name != "pre":
continue
if "class" in i.attrs and (
isinstance(i.attrs["class"], str) or len(i.attrs["class"]) > 0
):
# this already has a class; skip!
continue
i.attrs["class"] = [default_code_class]
all_lines = context.get("cs_code_line_numbers", False)
for i in tree.find_all("code"):
if i.parent.name != "pre":
continue

if isinstance(i.attrs.setdefault("class", []), str):
i.attrs["class"] = [i.attrs["class"]]

# set default language if no language is given
if default_code_class is not None:
if len(i.attrs["class"]) == 0:
i.attrs["class"] = [default_code_class]

# add line numbers if we need to:
classes = i.attrs["class"]
if all_lines:
classes.append("highlight-lines")
else:
for j in range(len(i.attrs["class"])):
if classes[j].endswith("-lines") and classes[j] != "highlight-lines":
classes[j] = classes[j][:-6]
classes.append("highlight-lines")

return str(tree)



Loading…
Cancel
Save