Syntax highlighting
Introduces the gr-syntax-layer element. This element works as an annotation layer that is configured with the diff and asynchronously computes/applies syntax for the diff. Introduces a custom build of Highlight.js which gr-syntax-layer makes use of. Building the script is documented in scripts/vendor/highlight/building.md. The layer is connected to the annotation pipeline in gr-diff-builder as the lowest layer and syntax processing is triggered only after a diff has been completely rendered. A number of styles are added to the gr-diff element for syntax markers. Tests added for gr-syntax-layer. Bug: Issue 3916 Change-Id: Ic33e40f4fe39dfce1a62de133cfaf32be5e3f25a
This commit is contained in:
parent
18af6cfdc4
commit
650c529276
1
lib/BUCK
1
lib/BUCK
@ -13,6 +13,7 @@ define_license(name = 'codemirror-original')
|
||||
define_license(name = 'diffy')
|
||||
define_license(name = 'fetch')
|
||||
define_license(name = 'h2')
|
||||
define_license(name = 'highlightjs')
|
||||
define_license(name = 'jgit')
|
||||
define_license(name = 'jsch')
|
||||
define_license(name = 'MPL1.1')
|
||||
|
24
lib/LICENSE-highlightjs
Normal file
24
lib/LICENSE-highlightjs
Normal file
@ -0,0 +1,24 @@
|
||||
Copyright (c) 2006, Ivan Sagalaev
|
||||
All rights reserved.
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of highlight.js nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
5
lib/highlightjs/BUCK
Normal file
5
lib/highlightjs/BUCK
Normal file
@ -0,0 +1,5 @@
|
||||
export_file(
|
||||
name = 'highlightjs',
|
||||
src = 'highlight.min.js',
|
||||
visibility = ['PUBLIC'],
|
||||
)
|
45
lib/highlightjs/building.md
Normal file
45
lib/highlightjs/building.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Building Highlight.js for Gerrit
|
||||
|
||||
Highlight JS needs to be built with specific language support. Here are the
|
||||
steps to build the minified file that appears here.
|
||||
|
||||
NOTE: If you are adding support for a language to Highlight.js make sure to add
|
||||
it to the list of languages in the build command below.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You will need:
|
||||
|
||||
* nodejs
|
||||
* closure-compiler
|
||||
* git
|
||||
|
||||
## Steps to Create the Pack File
|
||||
|
||||
The packed version of Highlight.js is an un-minified JS file with all of the
|
||||
languages included. Build it with the following:
|
||||
|
||||
$> # start in some temp directory
|
||||
$> git clone https://github.com/isagalaev/highlight.js.git
|
||||
$> cd highlight.js
|
||||
$> node tools/build.js -n json css xml html javascript cpp go haskell \
|
||||
markdown perl python bash sql scala prolog java objectivec
|
||||
|
||||
The resulting JS file will appear in the "build" directory of the Highlight.js
|
||||
repo under the name "highlight.pack.js".
|
||||
|
||||
## Minification
|
||||
|
||||
Minify the file using closure-compiler using the command below. (Modify
|
||||
`/path/to` with the path to your compiler jar.)
|
||||
|
||||
$> java -jar /path/to/closure-compiler.jar \
|
||||
--js build/highlight.pack.js \
|
||||
--js_output_file build/highlight.min.js
|
||||
|
||||
Copy the header comment that appears on the first line of
|
||||
build/highlight.pack.js and add it to the start of build/highlight.min.js.
|
||||
|
||||
## Finish
|
||||
|
||||
Copy the resulting build/highlight.min.js file to lib/highlightjs
|
64
lib/highlightjs/highlight.min.js
vendored
Normal file
64
lib/highlightjs/highlight.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
15
lib/js/BUCK
15
lib/js/BUCK
@ -402,3 +402,18 @@ bower_component(
|
||||
sha1 = '8ba97a4a279ec6973a19b171c462a7b5cf454fb9',
|
||||
)
|
||||
|
||||
# Zip highlightjs so that it can be imported as though it were a
|
||||
# bower_component and also attach the library license to the Buck dependency
|
||||
# graph.
|
||||
HLJS_DIR = 'bower_components/highlightjs'
|
||||
genrule(
|
||||
name = 'highlightjs',
|
||||
cmd = ' && '.join([
|
||||
'mkdir -p %s' % HLJS_DIR,
|
||||
'cp $(location //lib/highlightjs:highlightjs) %s/highlight.min.js' % HLJS_DIR,
|
||||
'zip -r $OUT bower_components',
|
||||
]),
|
||||
out = 'highlightjs.zip',
|
||||
license = 'highlightjs',
|
||||
visibility = ['PUBLIC'],
|
||||
)
|
||||
|
@ -4,6 +4,7 @@ bower_components(
|
||||
name = 'polygerrit_components',
|
||||
deps = [
|
||||
'//lib/js:fetch',
|
||||
'//lib/js:highlightjs',
|
||||
'//lib/js:iron-autogrow-textarea',
|
||||
'//lib/js:iron-dropdown',
|
||||
'//lib/js:iron-input',
|
||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
|
||||
<link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html">
|
||||
<link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html">
|
||||
|
||||
<dom-module id="gr-diff-builder">
|
||||
<template>
|
||||
@ -25,6 +26,9 @@ limitations under the License.
|
||||
<gr-ranged-comment-layer
|
||||
id="rangeLayer"
|
||||
comments="[[comments]]"></gr-ranged-comment-layer>
|
||||
<gr-syntax-layer
|
||||
id="syntaxLayer"
|
||||
diff="[[diff]]"></gr-syntax-layer>
|
||||
<gr-diff-processor
|
||||
id="processor"
|
||||
groups="{{_groups}}"></gr-diff-processor>
|
||||
@ -77,6 +81,7 @@ limitations under the License.
|
||||
attached: function() {
|
||||
// Setup annotation layers.
|
||||
this._layers = [
|
||||
this.$.syntaxLayer,
|
||||
this._createIntralineLayer(),
|
||||
this.$.rangeLayer,
|
||||
];
|
||||
@ -85,6 +90,7 @@ limitations under the License.
|
||||
render: function(comments, prefs) {
|
||||
// Stop the processor (if it's running).
|
||||
this.$.processor.cancel();
|
||||
this.$.syntaxLayer.cancel();
|
||||
|
||||
this._builder = this._getDiffBuilder(this.diff, comments, prefs);
|
||||
|
||||
@ -98,6 +104,7 @@ limitations under the License.
|
||||
if (this.isImageDiff) {
|
||||
this._builder.renderDiffImages();
|
||||
}
|
||||
this.$.syntaxLayer.process();
|
||||
console.timeEnd('diff render');
|
||||
this.fire('render');
|
||||
}.bind(this));
|
||||
@ -280,7 +287,7 @@ limitations under the License.
|
||||
// differences to highlight and apply them to the element as
|
||||
// annotations.
|
||||
annotate: function(el, line, GrAnnotation) {
|
||||
var HL_CLASS = 'style-scope gr-diff';
|
||||
var HL_CLASS = 'style-scope gr-diff intraline';
|
||||
line.highlights.forEach(function(highlight) {
|
||||
// The start and end indices could be the same if a highlight is
|
||||
// meant to start at the end of a line and continue onto the
|
||||
|
@ -137,7 +137,6 @@
|
||||
GrDiffBuilder.prototype.findLinesByRange = function(start, end, opt_side,
|
||||
out_lines, out_elements) {
|
||||
var groups = this.getGroupsByLineRange(start, end, opt_side);
|
||||
|
||||
groups.forEach(function(group) {
|
||||
var content = null;
|
||||
group.lines.forEach(function(line) {
|
||||
|
@ -176,7 +176,7 @@ limitations under the License.
|
||||
test('comment-mouse-over from ranged comment causes set', function() {
|
||||
sandbox.stub(element, 'set');
|
||||
sandbox.stub(element, '_indexOfComment').returns(0);
|
||||
element.fire('comment-mouse-over', {comment: {range:{}}});
|
||||
element.fire('comment-mouse-over', {comment: {range: {}}});
|
||||
assert.isTrue(element.set.called);
|
||||
});
|
||||
|
||||
|
@ -101,14 +101,14 @@ limitations under the License.
|
||||
max-width: var(--content-width, 80ch);
|
||||
min-width: var(--content-width, 80ch);
|
||||
}
|
||||
.content.add hl,
|
||||
.content.add .intraline,
|
||||
.content.add.darkHighlight {
|
||||
background-color: var(--dark-add-highlight-color);
|
||||
}
|
||||
.content.add.lightHighlight {
|
||||
background-color: var(--light-add-highlight-color);
|
||||
}
|
||||
.content.remove hl,
|
||||
.content.remove .intraline,
|
||||
.content.remove.darkHighlight {
|
||||
background-color: var(--dark-remove-highlight-color);
|
||||
}
|
||||
@ -140,6 +140,82 @@ limitations under the License.
|
||||
content: '\00BB';
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Syntax highlights */
|
||||
/* Highlight.js emits the following classes that do not have styles here:
|
||||
subst, symbol, class, function, doctag, meta-string, section,
|
||||
builtin-name, bulletm, code, formula, quote, addition, deletion
|
||||
See:
|
||||
http://highlightjs.readthedocs.io/en/latest/css-classes-reference.html
|
||||
*/
|
||||
.gr-syntax-literal,
|
||||
.gr-syntax-keyword,
|
||||
.gr-syntax-selector-tag {
|
||||
font-weight: bold;
|
||||
color: #00f;
|
||||
}
|
||||
.gr-syntax-built_in {
|
||||
color: #555;
|
||||
}
|
||||
.gr-syntax-type,
|
||||
.gr-syntax-selector-pseudo,
|
||||
.gr-syntax-template-variable {
|
||||
color: #ff00e7;
|
||||
}
|
||||
.gr-syntax-number {
|
||||
color: violet;
|
||||
}
|
||||
.gr-syntax-regexp,
|
||||
.gr-syntax-variable,
|
||||
.gr-syntax-selector-attr,
|
||||
.gr-syntax-template-tag {
|
||||
color: #FA8602;
|
||||
}
|
||||
.gr-syntax-string,
|
||||
.gr-syntax-selector-id {
|
||||
color: #018846;
|
||||
}
|
||||
.gr-syntax-title {
|
||||
color: teal;
|
||||
}
|
||||
.gr-syntax-params {
|
||||
color: red;
|
||||
}
|
||||
.gr-syntax-comment {
|
||||
color: #af72a9;
|
||||
font-style: italic;
|
||||
}
|
||||
.gr-syntax-meta {
|
||||
color: #0091AD;
|
||||
}
|
||||
.gr-syntax-meta-keyword {
|
||||
color: #00426b;
|
||||
font-weight: bold;
|
||||
}
|
||||
.gr-syntax-tag {
|
||||
color: #DB7C00;
|
||||
}
|
||||
.gr-syntax-name { /* XML/HTML Tag Name */
|
||||
color: brown;
|
||||
}
|
||||
.gr-syntax-attr { /* XML/HTML Attribute */
|
||||
color: #8C7250;
|
||||
}
|
||||
.gr-syntax-attribute { /* CSS Property */
|
||||
color: #299596;
|
||||
}
|
||||
.gr-syntax-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
.gr-syntax-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
.gr-syntax-link {
|
||||
color: blue;
|
||||
}
|
||||
.gr-syntax-selector-class {
|
||||
color: #1F71FF;
|
||||
}
|
||||
</style>
|
||||
<div class$="[[_computeContainerClass(_loggedIn, viewMode)]]"
|
||||
on-tap="_handleTap">
|
||||
|
@ -0,0 +1,22 @@
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||
<script src="../../../bower_components/highlightjs/highlight.min.js"></script>
|
||||
<dom-module id="gr-syntax-layer">
|
||||
<script src="../gr-diff/gr-diff-line.js"></script>
|
||||
<script src="../gr-diff-highlight/gr-annotation.js"></script>
|
||||
<script src="gr-syntax-layer.js"></script>
|
||||
</dom-module>
|
@ -0,0 +1,303 @@
|
||||
// Copyright (C) 2016 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var LANGUAGE_MAP = {
|
||||
'application/json': 'json',
|
||||
'text/css': 'css',
|
||||
'text/html': 'html',
|
||||
'text/javascript': 'js',
|
||||
'text/x-c++src': 'cpp',
|
||||
'text/x-go': 'go',
|
||||
'text/x-haskell': 'haskell',
|
||||
'text/x-java': 'java',
|
||||
'text/x-markdown': 'markdown',
|
||||
'text/x-objectivec': 'objectivec',
|
||||
'text/x-perl': 'perl',
|
||||
'text/x-python': 'python',
|
||||
'text/x-sh': 'bash',
|
||||
'text/x-sql': 'sql',
|
||||
'text/x-scala': 'scala',
|
||||
};
|
||||
|
||||
var ASYNC_DELAY = 10;
|
||||
|
||||
Polymer({
|
||||
is: 'gr-syntax-layer',
|
||||
|
||||
properties: {
|
||||
diff: {
|
||||
type: Object,
|
||||
observer: '_diffChanged',
|
||||
},
|
||||
_baseRanges: {
|
||||
type: Array,
|
||||
value: function() { return []; },
|
||||
},
|
||||
_revisionRanges: {
|
||||
type: Array,
|
||||
value: function() { return []; },
|
||||
},
|
||||
_baseLanguage: String,
|
||||
_revisionLanguage: String,
|
||||
_listeners: {
|
||||
type: Array,
|
||||
value: function() { return []; },
|
||||
},
|
||||
_processHandle: Number,
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
hljs.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'});
|
||||
},
|
||||
|
||||
addListener: function(fn) {
|
||||
this.push('_listeners', fn);
|
||||
},
|
||||
|
||||
/**
|
||||
* Annotation layer method to add syntax annotations to the given element
|
||||
* for the given line.
|
||||
* @param {!HTMLElement} el
|
||||
* @param {!GrDiffLine} line
|
||||
*/
|
||||
annotate: function(el, line, GrAnnotation) {
|
||||
// Determine the side.
|
||||
var side;
|
||||
if (line.type === GrDiffLine.Type.REMOVE || (
|
||||
line.type === GrDiffLine.Type.BOTH &&
|
||||
el.getAttribute('data-side') !== 'right')) {
|
||||
side = 'left';
|
||||
} else if (line.type === GrDiffLine.Type.ADD || (
|
||||
el.getAttribute('data-side') !== 'left')) {
|
||||
side = 'right';
|
||||
}
|
||||
|
||||
// Find the relevant syntax ranges, if any.
|
||||
var ranges = [];
|
||||
if (side === 'left' && this._baseRanges.length >= line.beforeNumber) {
|
||||
ranges = this._baseRanges[line.beforeNumber - 1] || [];
|
||||
} else if (side === 'right' &&
|
||||
this._revisionRanges.length >= line.afterNumber) {
|
||||
ranges = this._revisionRanges[line.afterNumber - 1] || [];
|
||||
}
|
||||
|
||||
// Apply the ranges to the element.
|
||||
ranges.forEach(function(range) {
|
||||
GrAnnotation.annotateElement(
|
||||
el, range.start, range.length, range.className);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Start processing symtax for the loaded diff and notify layer listeners
|
||||
* as syntax info comes online.
|
||||
* @return {Promise}
|
||||
*/
|
||||
process: function() {
|
||||
if (!this.diff.content.length) { return Promise.resolve(); }
|
||||
|
||||
this.cancel();
|
||||
|
||||
if (this.diff.meta_a) {
|
||||
this._baseLanguage = LANGUAGE_MAP[this.diff.meta_a.content_type];
|
||||
}
|
||||
if (this.diff.meta_b) {
|
||||
this._revisionLanguage = LANGUAGE_MAP[this.diff.meta_b.content_type];
|
||||
}
|
||||
|
||||
var state = {
|
||||
sectionIndex: 0,
|
||||
lineIndex: 0,
|
||||
baseContext: undefined,
|
||||
revisionContext: undefined,
|
||||
lineNums: {left: 1, right: 1},
|
||||
lastNotify: {left: 1, right: 1},
|
||||
};
|
||||
this._baseRanges = [];
|
||||
this._revisionRanges = [];
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
var nextStep = function() {
|
||||
this._processHandle = null;
|
||||
this._processNextLine(state);
|
||||
|
||||
// Move to the next line in the section.
|
||||
state.lineIndex++;
|
||||
|
||||
// If the section has been exhausted, move to the next one.
|
||||
if (this._isSectionDone(state)) {
|
||||
state.lineIndex = 0;
|
||||
state.sectionIndex++;
|
||||
}
|
||||
|
||||
// If all sections have been exhausted, finish.
|
||||
if (state.sectionIndex >= this.diff.content.length) {
|
||||
resolve();
|
||||
this._notify(state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.sectionIndex !== 0 && state.lineIndex % 100 === 0) {
|
||||
this._notify(state);
|
||||
this._processHandle = this.async(nextStep, ASYNC_DELAY);
|
||||
} else {
|
||||
nextStep.call(this);
|
||||
}
|
||||
};
|
||||
|
||||
this._processHandle = this.async(nextStep, 1);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel any asynchronous syntax processing jobs.
|
||||
*/
|
||||
cancel: function() {
|
||||
if (this._processHandle) {
|
||||
this.cancelAsync(this._processHandle);
|
||||
this._processHandle = null;
|
||||
}
|
||||
},
|
||||
|
||||
_diffChanged: function() {
|
||||
this.cancel();
|
||||
this._baseRanges = [];
|
||||
this._revisionRanges = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Take a string of HTML with the (potentially nested) syntax markers
|
||||
* Highlight.js emits and emit a list of text ranges and classes for the
|
||||
* markers.
|
||||
* @param {string} str The string of HTML.
|
||||
* @return {!Array<!Object>} The list of ranges.
|
||||
*/
|
||||
_rangesFromString: function(str) {
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = str;
|
||||
return this._rangesFromElement(div, 0);
|
||||
},
|
||||
|
||||
_rangesFromElement: function(elem, offset) {
|
||||
var result = [];
|
||||
for (var i = 0; i < elem.childNodes.length; i++) {
|
||||
var node = elem.childNodes[i];
|
||||
var nodeLength = GrAnnotation.getLength(node);
|
||||
// Note: HLJS may emit a span with class undefined when it thinks there
|
||||
// may be a syntax error.
|
||||
if (node.tagName === 'SPAN' && node.className !== 'undefined') {
|
||||
result.push({
|
||||
start: offset,
|
||||
length: nodeLength,
|
||||
className: node.className,
|
||||
});
|
||||
if (node.children.length) {
|
||||
result = result.concat(this._rangesFromElement(node, offset));
|
||||
}
|
||||
}
|
||||
offset += nodeLength;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* For a given state, process the syntax for the next line (or pair of
|
||||
* lines).
|
||||
* @param {!Object} state The processing state for the layer.
|
||||
*/
|
||||
_processNextLine: function(state) {
|
||||
var baseLine = undefined;
|
||||
var revisionLine = undefined;
|
||||
|
||||
var section = this.diff.content[state.sectionIndex];
|
||||
if (section.ab) {
|
||||
baseLine = section.ab[state.lineIndex];
|
||||
revisionLine = section.ab[state.lineIndex];
|
||||
state.lineNums.left++;
|
||||
state.lineNums.right++;
|
||||
} else {
|
||||
if (section.a && section.a.length > state.lineIndex) {
|
||||
baseLine = section.a[state.lineIndex];
|
||||
state.lineNums.left++;
|
||||
}
|
||||
if (section.b && section.b.length > state.lineIndex) {
|
||||
revisionLine = section.b[state.lineIndex];
|
||||
state.lineNums.right++;
|
||||
}
|
||||
}
|
||||
|
||||
// To store the result of the syntax highlighter.
|
||||
var result;
|
||||
|
||||
if (this._baseLanguage && baseLine !== undefined) {
|
||||
result = hljs.highlight(this._baseLanguage, baseLine, true,
|
||||
state.baseContext);
|
||||
this.push('_baseRanges', this._rangesFromString(result.value));
|
||||
state.baseContext = result.top;
|
||||
}
|
||||
|
||||
if (this._revisionLanguage && revisionLine !== undefined) {
|
||||
result = hljs.highlight(this._revisionLanguage, revisionLine, true,
|
||||
state.revisionContext);
|
||||
this.push('_revisionRanges', this._rangesFromString(result.value));
|
||||
state.revisionContext = result.top;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Tells whether the state has exhausted its current section.
|
||||
* @param {!Object} state
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isSectionDone: function(state) {
|
||||
var section = this.diff.content[state.sectionIndex];
|
||||
if (section.ab) {
|
||||
return state.lineIndex >= section.ab.length;
|
||||
} else {
|
||||
return (!section.a || state.lineIndex >= section.a.length) &&
|
||||
(!section.b || state.lineIndex >= section.b.length);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* For a given state, notify layer listeners of any processed line ranges
|
||||
* that have not yet been notified.
|
||||
* @param {!Object} state
|
||||
*/
|
||||
_notify: function(state) {
|
||||
if (state.lineNums.left - state.lastNotify.left) {
|
||||
this._notifyRange(
|
||||
state.lastNotify.left,
|
||||
state.lineNums.left,
|
||||
'left');
|
||||
state.lastNotify.left = state.lineNums.left;
|
||||
}
|
||||
if (state.lineNums.right - state.lastNotify.right) {
|
||||
this._notifyRange(
|
||||
state.lastNotify.right,
|
||||
state.lineNums.right,
|
||||
'right');
|
||||
state.lastNotify.right = state.lineNums.right;
|
||||
}
|
||||
},
|
||||
|
||||
_notifyRange: function(start, end, side) {
|
||||
this._listeners.forEach(function(fn) {
|
||||
fn(start, end, side);
|
||||
});
|
||||
},
|
||||
});
|
||||
})();
|
@ -0,0 +1,309 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
Copyright (C) 2016 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
|
||||
<title>gr-syntax-layer</title>
|
||||
|
||||
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
|
||||
<script src="../../../bower_components/web-component-tester/browser.js"></script>
|
||||
|
||||
<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
|
||||
<link rel="import" href="gr-syntax-layer.html">
|
||||
|
||||
<test-fixture id="basic">
|
||||
<template>
|
||||
<gr-syntax-layer></gr-syntax-layer>
|
||||
</template>
|
||||
</test-fixture>
|
||||
|
||||
<script>
|
||||
suite('gr-syntax-layer tests', function() {
|
||||
var sandbox;
|
||||
var diff;
|
||||
var element;
|
||||
|
||||
setup(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
element = fixture('basic');
|
||||
var mock = document.createElement('mock-diff-response');
|
||||
diff = mock.diffResponse;
|
||||
element.diff = diff;
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
test('annotate without range does nothing', function() {
|
||||
var annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
|
||||
var el = document.createElement('div');
|
||||
el.textContent = 'Etiam dui, blandit wisi.';
|
||||
var line = new GrDiffLine(GrDiffLine.Type.REMOVE);
|
||||
line.beforeNumber = 12;
|
||||
|
||||
element.annotate(el, line, GrAnnotation);
|
||||
|
||||
assert.isFalse(annotationSpy.called);
|
||||
});
|
||||
|
||||
test('annotate with range applies it', function() {
|
||||
var str = 'Etiam dui, blandit wisi.';
|
||||
var start = 6;
|
||||
var length = 3;
|
||||
var className = 'foobar';
|
||||
|
||||
var annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
|
||||
var el = document.createElement('div');
|
||||
el.textContent = str;
|
||||
var line = new GrDiffLine(GrDiffLine.Type.REMOVE);
|
||||
line.beforeNumber = 12;
|
||||
element._baseRanges[11] = [{
|
||||
start: start,
|
||||
length: length,
|
||||
className: className,
|
||||
}];
|
||||
|
||||
element.annotate(el, line, GrAnnotation);
|
||||
|
||||
assert.isTrue(annotationSpy.called);
|
||||
assert.equal(annotationSpy.lastCall.args[0], el);
|
||||
assert.equal(annotationSpy.lastCall.args[1], start);
|
||||
assert.equal(annotationSpy.lastCall.args[2], length);
|
||||
assert.equal(annotationSpy.lastCall.args[3], className);
|
||||
assert.isOk(el.querySelector('hl.' + className));
|
||||
});
|
||||
|
||||
test('process on empty diff does nothing', function(done) {
|
||||
element.diff = {
|
||||
meta_a: {content_type: 'application/json'},
|
||||
meta_b: {content_type: 'application/json'},
|
||||
content: [],
|
||||
};
|
||||
var processNextSpy = sandbox.spy(element, '_processNextLine');
|
||||
|
||||
var processPromise = element.process();
|
||||
|
||||
processPromise.then(function() {
|
||||
assert.isFalse(processNextSpy.called);
|
||||
assert.equal(element._baseRanges.length, 0);
|
||||
assert.equal(element._revisionRanges.length, 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('process highlight ipsum', function(done) {
|
||||
element.diff.meta_a.content_type = 'application/json';
|
||||
element.diff.meta_b.content_type = 'application/json';
|
||||
var processNextSpy = sandbox.spy(element, '_processNextLine');
|
||||
var highlightStub = sandbox.stub(hljs, 'highlight', function(
|
||||
lang, line, ignore, state) {
|
||||
return {
|
||||
value: line.replace(/ipsum/, '<span class="foobar">ipsum</span>'),
|
||||
top: state === undefined ? 1 : state + 1,
|
||||
};
|
||||
});
|
||||
|
||||
var processPromise = element.process();
|
||||
|
||||
processPromise.then(function() {
|
||||
var linesA = diff.meta_a.lines;
|
||||
var linesB = diff.meta_b.lines;
|
||||
|
||||
assert.isTrue(processNextSpy.called);
|
||||
assert.equal(element._baseRanges.length, linesA);
|
||||
assert.equal(element._revisionRanges.length, linesB);
|
||||
|
||||
assert.equal(highlightStub.callCount, linesA + linesB);
|
||||
|
||||
// The first line of both sides have a range.
|
||||
[element._baseRanges[0], element._revisionRanges[0]]
|
||||
.forEach(function(range) {
|
||||
assert.equal(range.length, 1);
|
||||
assert.equal(range[0].className, 'foobar');
|
||||
assert.equal(range[0].start, 'lorem '.length);
|
||||
assert.equal(range[0].length, 'ipsum'.length);
|
||||
});
|
||||
|
||||
// There are no ranges from ll.1-12 on the left and ll.1-11 on the
|
||||
// right.
|
||||
element._baseRanges.slice(1, 12)
|
||||
.concat(element._revisionRanges.slice(1, 11))
|
||||
.forEach(function(range) {
|
||||
assert.equal(range.length, 0);
|
||||
});
|
||||
|
||||
// There should be another pair of ranges on l.13 for the left and l.12
|
||||
// for the right.
|
||||
[element._baseRanges[13], element._revisionRanges[12]]
|
||||
.forEach(function(range) {
|
||||
assert.equal(range.length, 1);
|
||||
assert.equal(range[0].className, 'foobar');
|
||||
assert.equal(range[0].start, 32);
|
||||
assert.equal(range[0].length, 'ipsum'.length);
|
||||
});
|
||||
|
||||
// The next group should have a similar instance on either side.
|
||||
|
||||
var range = element._baseRanges[15];
|
||||
assert.equal(range.length, 1);
|
||||
assert.equal(range[0].className, 'foobar');
|
||||
assert.equal(range[0].start, 34);
|
||||
assert.equal(range[0].length, 'ipsum'.length);
|
||||
|
||||
range = element._revisionRanges[14];
|
||||
assert.equal(range.length, 1);
|
||||
assert.equal(range[0].className, 'foobar');
|
||||
assert.equal(range[0].start, 35);
|
||||
assert.equal(range[0].length, 'ipsum'.length);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('_diffChanged calls cancel', function() {
|
||||
var cancelSpy = sandbox.spy(element, '_diffChanged');
|
||||
element.diff = {content: []};
|
||||
assert.isTrue(cancelSpy.called);
|
||||
});
|
||||
|
||||
test('_rangesFromElement no ranges', function() {
|
||||
var elem = document.createElement('span');
|
||||
elem.textContent = 'Etiam dui, blandit wisi.';
|
||||
var offset = 100;
|
||||
|
||||
var result = element._rangesFromElement(elem, offset);
|
||||
|
||||
assert.equal(result.length, 0);
|
||||
});
|
||||
|
||||
test('_rangesFromElement single range', function() {
|
||||
var str0 = 'Etiam ';
|
||||
var str1 = 'dui, blandit';
|
||||
var str2 = ' wisi.';
|
||||
var className = 'theclass';
|
||||
var offset = 100;
|
||||
|
||||
var elem = document.createElement('span');
|
||||
elem.appendChild(document.createTextNode(str0));
|
||||
var span = document.createElement('span');
|
||||
span.textContent = str1;
|
||||
span.className = className;
|
||||
elem.appendChild(span);
|
||||
elem.appendChild(document.createTextNode(str2));
|
||||
|
||||
var result = element._rangesFromElement(elem, offset);
|
||||
|
||||
assert.equal(result.length, 1);
|
||||
assert.equal(result[0].start, str0.length + offset);
|
||||
assert.equal(result[0].length, str1.length);
|
||||
assert.equal(result[0].className, className);
|
||||
});
|
||||
|
||||
test('_rangesFromElement milti range', function() {
|
||||
var str0 = 'Etiam ';
|
||||
var str1 = 'dui,';
|
||||
var str2 = ' blandit';
|
||||
var str3 = ' wisi.';
|
||||
var className = 'theclass';
|
||||
var offset = 100;
|
||||
|
||||
var elem = document.createElement('span');
|
||||
elem.appendChild(document.createTextNode(str0));
|
||||
var span = document.createElement('span');
|
||||
span.textContent = str1;
|
||||
span.className = className;
|
||||
elem.appendChild(span);
|
||||
elem.appendChild(document.createTextNode(str2));
|
||||
span = document.createElement('span');
|
||||
span.textContent = str3;
|
||||
span.className = className;
|
||||
elem.appendChild(span);
|
||||
|
||||
var result = element._rangesFromElement(elem, offset);
|
||||
|
||||
assert.equal(result.length, 2);
|
||||
|
||||
assert.equal(result[0].start, str0.length + offset);
|
||||
assert.equal(result[0].length, str1.length);
|
||||
assert.equal(result[0].className, className);
|
||||
|
||||
assert.equal(result[1].start,
|
||||
str0.length + str1.length + str2.length + offset);
|
||||
assert.equal(result[1].length, str3.length);
|
||||
assert.equal(result[1].className, className);
|
||||
});
|
||||
|
||||
test('_rangesFromElement nested range', function() {
|
||||
var str0 = 'Etiam ';
|
||||
var str1 = 'dui,';
|
||||
var str2 = ' blandit';
|
||||
var str3 = ' wisi.';
|
||||
var className = 'theclass';
|
||||
var offset = 100;
|
||||
|
||||
var elem = document.createElement('span');
|
||||
elem.appendChild(document.createTextNode(str0));
|
||||
var span1 = document.createElement('span');
|
||||
span1.textContent = str1;
|
||||
span1.className = className;
|
||||
elem.appendChild(span1);
|
||||
var span2 = document.createElement('span');
|
||||
span2.textContent = str2;
|
||||
span2.className = className;
|
||||
span1.appendChild(span2);
|
||||
elem.appendChild(document.createTextNode(str3));
|
||||
|
||||
var result = element._rangesFromElement(elem, offset);
|
||||
|
||||
assert.equal(result.length, 2);
|
||||
|
||||
assert.equal(result[0].start, str0.length + offset);
|
||||
assert.equal(result[0].length, str1.length + str2.length);
|
||||
assert.equal(result[0].className, className);
|
||||
|
||||
assert.equal(result[1].start, str0.length + str1.length + offset);
|
||||
assert.equal(result[1].length, str2.length);
|
||||
assert.equal(result[1].className, className);
|
||||
});
|
||||
|
||||
test('_isSectionDone', function() {
|
||||
var state = {sectionIndex: 0, lineIndex: 0};
|
||||
assert.isFalse(element._isSectionDone(state));
|
||||
|
||||
state = {sectionIndex: 0, lineIndex: 2};
|
||||
assert.isFalse(element._isSectionDone(state));
|
||||
|
||||
state = {sectionIndex: 0, lineIndex: 4};
|
||||
assert.isTrue(element._isSectionDone(state));
|
||||
|
||||
state = {sectionIndex: 1, lineIndex: 2};
|
||||
assert.isFalse(element._isSectionDone(state));
|
||||
|
||||
state = {sectionIndex: 1, lineIndex: 3};
|
||||
assert.isTrue(element._isSectionDone(state));
|
||||
|
||||
state = {sectionIndex: 3, lineIndex: 0};
|
||||
assert.isFalse(element._isSectionDone(state));
|
||||
|
||||
state = {sectionIndex: 3, lineIndex: 3};
|
||||
assert.isFalse(element._isSectionDone(state));
|
||||
|
||||
state = {sectionIndex: 3, lineIndex: 4};
|
||||
assert.isTrue(element._isSectionDone(state));
|
||||
});
|
||||
});
|
||||
</script>
|
@ -60,6 +60,7 @@ limitations under the License.
|
||||
'diff/gr-patch-range-select/gr-patch-range-select_test.html',
|
||||
'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html',
|
||||
'diff/gr-selection-action-box/gr-selection-action-box_test.html',
|
||||
'diff/gr-syntax-layer/gr-syntax-layer_test.html',
|
||||
'settings/gr-account-info/gr-account-info_test.html',
|
||||
'settings/gr-email-editor/gr-email-editor_test.html',
|
||||
'settings/gr-group-list/gr-group-list_test.html',
|
||||
|
Loading…
x
Reference in New Issue
Block a user