diff --git a/index.js b/index.js index 3441ae9..3911622 100644 --- a/index.js +++ b/index.js @@ -1,145 +1,118 @@ -require("./static/styles/highlight-line.less"); -var $ = require("jquery"); +require("bit-docs-prettify"); -var getLines = function(lineString) { - var lineArray = lineString.split(','); - var result = {}; +require("prismjs/plugins/line-highlight/prism-line-highlight"); +require("prismjs/plugins/line-highlight/prism-line-highlight.css"); - for (var i = 0; i < lineArray.length; i++) { - var val = lineArray[i]; +require("./prism-collapse"); +require("./prism-collapse.less"); - // Matches any string with 1+ digits dash 1+ digits - // will ignore non matching strings - if (/^([\d]+-[\d]+)$/.test(val)) { - var values = val.split('-'), - start = (values[0] - 1), - finish = (values[1] - 1); +/** + * Get node for provided line number + * Copied from prism-line-numbers.js and modified to support nested spans + * Original version assumed all line number spans were inside .line-numbers-rows + * but now they may be may be nested inside collapsed sections + * + * @param {Element} element pre element + * @param {Number} number line number + * @return {Element|undefined} + */ +Prism.plugins.lineNumbers.getLine = function (element, number) { + if (element.tagName !== 'PRE' || !element.classList.contains('line-numbers')) { + return; + } - for (var j = start; finish >= j; j++) { - result[j] = true; - } - //matches one or more digits - } else if (/^[\d]+$/.test(val)) { - result[val - 1] = true; - } else { - result[val] = true; - } + var lineNumberRows = element.querySelector('.line-numbers-rows'); + var lineNumbers = lineNumberRows.querySelectorAll('span'); // added + var lineNumberStart = parseInt(element.getAttribute('data-start'), 10) || 1; + var lineNumberEnd = lineNumberStart + (lineNumbers.length - 1); + if (number < lineNumberStart) { + number = lineNumberStart; + } + if (number > lineNumberEnd) { + number = lineNumberEnd; } - return result; + + var lineIndex = number - lineNumberStart; + + return lineNumbers[lineIndex]; }; -/** - * @parent bit-docs-html-highlight-line/static - * @module {function} bit-docs-html-highlight-line/highlight-line.js - * - * Main front end JavaScript file for static portion of this plugin. - * - * @signature `addHighlights()` - * - * Goes through the lines in a `` block and highlights the specified - * ranges. - * - * Finds all `` elements and uses those as - * directives for what to highlight. - * - * If the `only` option was specified to the - * [bit-docs-html-highlight-line/tags/highlight] tag, then non-highlighted - * lines will be collapsed if they exist greater than three lines away from a - * highlighted line. - */ -function addHighlights() { - - $('span[line-highlight]').each(function(i, el) { - var $el = $(el); - var lines = getLines($el.attr('line-highlight')); - var codeBlock = $el.parent().prev('pre').children('code'); - codeBlock.addClass("line-highlight"); - - var lineMap = [[]]; - var k = 0; - codeBlock.children().each(function(i, el) { - var nodeText = $(el).text(); - if (/\n/.test(nodeText)) { - - var cNames = $(el).attr('class'); - var str = nodeText.split('\n'); - var l = str.length; - - for (var j = 0; j < l; j++) { - var text = j === (l - 1) ? str[j] : str[j] + '\n'; - var newNode = document.createElement('span'); - newNode.className = cNames; - $(newNode).text(text); - lineMap[k].push(newNode); - - if (j !== (l - 1)) { - k++; - lineMap[k] = []; - } - } - } else { - lineMap[k].push(el); +var padding = 3; +var getConfig = function(lineString, lineCount) { + var lines = lineString + .split(',') + .map(function(data) { + return data.trim(); + }) + .filter(function(data) { + return data; + }) + ; + + var collapse = []; + var index = lines.indexOf('only'); + if (index > -1) { + lines.splice(index, 1); + + var current = 1; + for (var i = 0; i < lines.length; i++) { + var range = lines[i] + .split('-') + .map(function(val) { + return parseInt(val); + }) + .filter(function(val) { + return typeof val === 'number' && !isNaN(val); + }) + ; + + if (range[0] > current + padding) { + collapse.push(current + '-' + (range[0] - 1 - padding)); } - }); - - codeBlock.empty(); - if(lines.only) { - var segments = []; - lineMap.forEach(function(lineNodes, lineNumber){ - var visible = lines[lineNumber]; - var lineNode = document.createElement('span'); - $(lineNode).append(lineNodes); - lineNode.className = lines[lineNumber] ? 'line highlight line-'+lineNumber: 'line line-'+lineNumber ; - - var lastSegment = segments[segments.length - 1]; - if(!lastSegment || lastSegment.visible !== visible) { - segments.push(lastSegment = {visible: visible, lines: []}); - } - lastSegment.lines.push(lineNode); - - - }); - segments.forEach(function(segment, index){ - var next = segments[index+1]; - - if(segment.visible) { - // take 3 lines from next if possible - if(next) { - var first = next.lines.splice(0,3); - segment.lines = segment.lines.concat(first); - } - codeBlock.append(segment.lines); - } else { - // move 3 lines to next if possible - if(next) { - var last = segment.lines.splice(segment.lines.length-3); - next.lines = last.concat(next.lines); - } - if(segment.lines.length > 2) { - var expander = document.createElement('div'); - expander.className = "expand"; - expander.addEventListener("click", function(){ - $(expander).replaceWith(segment.lines); - }); - codeBlock.append(expander); - } else { - codeBlock.append(segment.lines); - } - } - }); - - - } else { - lineMap.forEach(function(lineNodes, lineNumber){ - var newNode = document.createElement('span'); - newNode.className = lines[lineNumber] ? 'line highlight': 'line' ; - $(newNode).append(lineNodes); - codeBlock.append(newNode); - }); + + current = (range[1] || range[0]) + padding + 1; } - }); + if (current < lineCount) { + collapse.push(current + '-' + lineCount); + } + } + + return { + lines: lines.length ? lines.join(',') : false, + collapse: collapse.length ? collapse.join(',') : false, + }; +}; + +function findPreviousSibling(el, tag) { + tag = tag.toUpperCase(); + + while (el = el.previousSibling) { + if (el.tagName && el.tagName.toUpperCase() === tag) { + return el; + } + } } -module.exports = addHighlights; +module.exports = function() { + var highlights = document.querySelectorAll('span[line-highlight]') + + for (var i = 0; i < highlights.length; i++) { + var highlight = highlights[i]; + + var preBlock = findPreviousSibling(highlight.parentElement, 'pre'); + var codeBlock = preBlock.childNodes.item(0); + + var total = codeBlock.innerHTML.split('\n').length - 1; + var config = getConfig(highlight.getAttribute('line-highlight'), total); + + if (preBlock) { + preBlock.setAttribute('data-line', config.lines); + + if (config.collapse) { + preBlock.setAttribute('data-collapse', config.collapse); + } + } + }; +}; diff --git a/package.json b/package.json index d4679a4..7bc791e 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "preversion": "npm test", "test": "mocha test.js --reporter spec", + "release:pre": "npm version prerelease && npm publish --tag=pre", "release:patch": "npm version patch && npm publish", "release:minor": "npm version minor && npm publish", "release:major": "npm version major && npm publish" @@ -25,12 +26,13 @@ }, "homepage": "https://github.com/bit-docs/bit-docs-html-highlight-line#readme", "dependencies": { - "jquery": "^2.2.4" + "bit-docs-prettify": "^0.2.2-8", + "prismjs": "^1.11.0" }, "devDependencies": { "bit-docs-generate-html": "^0.1.0", "connect": "^2.14.4", "mocha": "^2.5.3", - "zombie": "^4.2.1" + "zombie": "^4.3.0" } } diff --git a/prism-collapse.js b/prism-collapse.js new file mode 100644 index 0000000..dd40841 --- /dev/null +++ b/prism-collapse.js @@ -0,0 +1,162 @@ +if (typeof self === 'undefined' || !self.Prism || !self.document || !document.querySelector) { + throw new Error('Prism must be loaded before prism-collapse'); +} + +function hasClass(element, className) { + className = " " + className + " "; + return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(className) > -1 +} + +function adjustHighlights(pre, collapseRange, visible) { + collapseRange = collapseRange.split('-').map(function(value) { + return parseInt(value); + }); + + var highlights = pre.querySelectorAll('.line-highlight'); + for (var i = 0; i < highlights.length; i++) { + var highlight = highlights[i]; + + var range = highlight.getAttribute('data-range').split('-').map(function(value) { + return parseInt(value); + }); + + if (range.length === 1) { + var line = range[0]; + if (line < collapseRange[0]) { + continue; + } + + if (line > collapseRange[1]) { + var lineNode = Prism.plugins.lineNumbers.getLine(pre, line); + if (lineNode) { + highlight.style.top = lineNode.offsetTop + 'px'; + } + + continue; + } + + highlight.style.display = visible ? 'block' : 'none'; + } + + if (range.length === 2) { + if (range[1] < collapseRange[0]) { + continue; + } + + if (range[0] > collapseRange[1]) { + var lineNode = Prism.plugins.lineNumbers.getLine(pre, range[0]); + if (lineNode) { + highlight.style.top = lineNode.offsetTop + 'px'; + } + + continue; + } + + highlight.style.display = visible ? 'block' : 'none'; + } + }; +} + +function collapseLines(pre, config) { + var inserts = []; + + var ranges = config.split(','); + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + var parts = range.split('-'); + + var wrapper = '' + ]); + } + + inserts.sort(function (a, b) { + return b[0] - a[0]; + }); + + + var codeContainer = pre.children[0]; + + var numbersContainer = codeContainer.lastChild; + var numbers = numbersContainer.innerHTML.split(''); + numbersContainer.remove(); + + var code = codeContainer.innerHTML.split('\n'); + code = code.map(function(line, index) { + if (index === code.length - 1) { + return line; + } + + return line + '\n'; + }); + + for (var i = 0; i < inserts.length; i++) { + var line = Math.min(code.length - 1, inserts[i][0] - 1); + + code.splice(line, 0, inserts[i][1]); + numbers[line] += inserts[i][2]; + } + + codeContainer.innerHTML = code.join(''); + numbersContainer.innerHTML = numbers.join(''); + codeContainer.appendChild(numbersContainer); + + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + adjustHighlights(pre, range, false); + } +} + +function findPreviousParent(el, tag) { + tag = tag.toUpperCase(); + + while (el = el.parentElement) { + if (el.tagName && el.tagName.toUpperCase() === tag) { + return el; + } + } +} + +Prism.hooks.add('complete', function completeHook(env) { + var pre = env.element.parentNode; + var config = pre && pre.getAttribute('data-collapse'); + + if (!pre || !config || !/pre/i.test(pre.nodeName)) { + return; + } + + var isLineNumbersLoaded = env.plugins && env.plugins.lineNumbers; + + if (hasClass(pre, 'line-numbers') && !isLineNumbersLoaded) { + Prism.hooks.add('line-numbers', completeHook); + } else { + collapseLines(pre, config); + } +}); + +document.body.addEventListener('click', function(event) { + var collapse = event.target; + if (!collapse.classList.contains('collapse') || !collapse.classList.contains('collapsed')) { + return; + } + + var index = collapse.getAttribute('data-index'); + var code = findPreviousParent(collapse, 'code'); + var pre = findPreviousParent(code, 'pre'); + + var lines = code.querySelectorAll('.collapse[data-index="' + index + '"]'); + for (var i = 0; i < lines.length; i++) { + lines[i].classList.remove('collapsed'); + } + + adjustHighlights(pre, collapse.getAttribute('data-range'), true); +}, false); diff --git a/prism-collapse.less b/prism-collapse.less new file mode 100644 index 0000000..9b24372 --- /dev/null +++ b/prism-collapse.less @@ -0,0 +1,79 @@ +pre code { + .collapse.collapsed { + color: black; + text-align: center; + background-color: #eee; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; + padding: calc(~"0.25em - 1px") 0; + margin: 0 -1em; + + > .collapse-code { + display: none; + } + + &:hover { + opacity: .6; + cursor: pointer; + } + + &:before { + display: block; + content: '⇅ EXPAND ⇅'; + margin: 0 auto; + + width: 1em; + height: 1em; + line-height: 1em; + } + } +} + +pre.line-numbers code { + > .line-numbers-rows > .collapse.collapsed { + border: none; + padding: 0; + margin: 0; + height: 1.5em; + + opacity: 0; + + > .collapse-lines { + position: absolute; + height: 0; + overflow: hidden; + } + + &:before { + display: none; + } + } +} + + +/* conditional positioning of colored bar */ + +pre[data-line] code .collapse.collapsed { + margin-left: -3.8em; +} + +pre.line-numbers code .collapse.collapsed { + margin-left: -4em; +} + + +/* line-numbers overwrites so that spans can be nested */ + +.line-numbers-rows span { + pointer-events: none; + display: block; + counter-increment: linenumber; +} + +.line-numbers-rows span:before { + content: counter(linenumber); + color: #999; + display: block; + padding-right: .8em; + text-align: right; +} diff --git a/static/styles/highlight-line.less b/static/styles/highlight-line.less deleted file mode 100644 index 2abc9aa..0000000 --- a/static/styles/highlight-line.less +++ /dev/null @@ -1,38 +0,0 @@ -@import "locate://bit-docs-site/styles/variables.less"; - -/** - * @parent bit-docs-html-highlight-line/static - * @module bit-docs-html-highlight-line/static/styles/highlight-line.less - * - * @description Base styles for highlight tag. - * - * @body - * - * Adds styling for the `.expand` element. - * - * The `.expand` element gets added onload when the `only` option is used with - * the highlight tag, and the code block has lines before/after the highlight. - */ -article { - code { - .expand { - color: black; - padding: 4px 0px; - text-align: center; - background-color: #eee; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; - } - .expand:before { - width: 15px; - height: 15px; - margin: 0 auto; - display: block; - content: '⇅ EXPAND ⇅'; - } - .expand:hover { - opacity: .6; - cursor: pointer; - } - } -} diff --git a/test.js b/test.js index 9d48c69..0a6ff2f 100644 --- a/test.js +++ b/test.js @@ -3,68 +3,38 @@ var generate = require("bit-docs-generate-html/generate"); var path = require("path"); var fs = require("fs"); -var Browser = require("zombie"), - connect = require("connect"); +var Browser = require("zombie"); +var connect = require("connect"); -var find = function(browser, property, callback, done){ - var start = new Date(); - var check = function(){ - if(browser.window && browser.window[property]) { - callback(browser.window[property]); - } else if(new Date() - start < 2000){ - setTimeout(check, 20); - } else { - done("failed to find "+property); - } - }; - check(); -}; - -var waitFor = function(browser, checker, callback, done){ - var start = new Date(); - var check = function(){ - if(checker(browser.window)) { - callback(browser.window); - } else if(new Date() - start < 2000){ - setTimeout(check, 20); - } else { - done(new Error("checker was never true")); - } - }; - check(); -}; - - -var open = function(url, callback, done){ - var server = connect().use(connect.static(path.join(__dirname))).listen(8081); +var open = function(url, callback, done) { + var server = connect().use(connect.static(path.join(__dirname, "temp"))).listen(8081); var browser = new Browser(); - browser.visit("http://localhost:8081/"+url) - .then(function(){ - callback(browser, function(){ + browser.visit("http://localhost:8081/" + url) + .then(function() { + callback(browser, function() { server.close(); }); - }).catch(function(e){ + }).catch(function(e) { server.close(); done(e); }); }; -describe("bit-docs-tag-demo", function(){ - it("basics works", function(done){ +describe("bit-docs-html-highlight-line", function() { + it("basics works", function(done) { this.timeout(60000); var docMap = Promise.resolve({ index: { name: "index", demo: "path/to/demo.html", - body: ""+fs.readFileSync(__dirname+"/test-demo.md") + body: fs.readFileSync(__dirname+"/test-demo.md", "utf8") } }); generate(docMap, { html: { dependencies: { - "bit-docs-prettify": "^0.1.0", "bit-docs-html-highlight-line": __dirname } }, @@ -73,23 +43,19 @@ describe("bit-docs-tag-demo", function(){ forceBuild: true, debug: true, minifyBuild: false - }).then(function(){ + }).then(function() { + open("index.html",function(browser, close) { + var doc = browser.window.document; + + var lineCodes = doc.querySelectorAll('pre[data-line] code'); + var collapseCodes = doc.querySelectorAll('pre[data-collapse] code'); + + assert.ok(lineCodes.length, "there are code blocks with data-line"); + assert.ok(collapseCodes.length, "there are code blocks with data-collapse"); - open("temp/index.html",function(browser, close){ - waitFor(browser, function(window){ - return window.document.getElementsByClassName("highlight").length; - }, function(){ - var doc = browser.window.document; - var highlights = doc.getElementsByClassName("highlight"); - // NOTE: there should be 2 lines. But it seems - // like prettify doesn't work in zombie right. - assert.ok(highlights.length, "there are 2 tabs"); - var codeBlocks = doc.getElementsByClassName("line-highlight"); - assert.ok(codeBlocks.length, "there are code blocks with highlight class"); - close(); - done(); - }, done); - },done); + close(); + done(); + }, done); }, done); }); });