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:
Wyatt Allen 2016-06-22 17:18:06 -07:00
parent 18af6cfdc4
commit 650c529276
15 changed files with 877 additions and 5 deletions

View File

@ -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
View 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
View File

@ -0,0 +1,5 @@
export_file(
name = 'highlightjs',
src = 'highlight.min.js',
visibility = ['PUBLIC'],
)

View 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

File diff suppressed because one or more lines are too long

View File

@ -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'],
)

View File

@ -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',

View File

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

View File

@ -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) {

View File

@ -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);
});

View File

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

View File

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

View File

@ -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);
});
},
});
})();

View File

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

View File

@ -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',