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 = 'diffy')
|
||||||
define_license(name = 'fetch')
|
define_license(name = 'fetch')
|
||||||
define_license(name = 'h2')
|
define_license(name = 'h2')
|
||||||
|
define_license(name = 'highlightjs')
|
||||||
define_license(name = 'jgit')
|
define_license(name = 'jgit')
|
||||||
define_license(name = 'jsch')
|
define_license(name = 'jsch')
|
||||||
define_license(name = 'MPL1.1')
|
define_license(name = 'MPL1.1')
|
||||||
|
|
|
@ -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.
|
|
@ -0,0 +1,5 @@
|
||||||
|
export_file(
|
||||||
|
name = 'highlightjs',
|
||||||
|
src = 'highlight.min.js',
|
||||||
|
visibility = ['PUBLIC'],
|
||||||
|
)
|
|
@ -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
|
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',
|
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',
|
name = 'polygerrit_components',
|
||||||
deps = [
|
deps = [
|
||||||
'//lib/js:fetch',
|
'//lib/js:fetch',
|
||||||
|
'//lib/js:highlightjs',
|
||||||
'//lib/js:iron-autogrow-textarea',
|
'//lib/js:iron-autogrow-textarea',
|
||||||
'//lib/js:iron-dropdown',
|
'//lib/js:iron-dropdown',
|
||||||
'//lib/js:iron-input',
|
'//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="../../../bower_components/polymer/polymer.html">
|
||||||
<link rel="import" href="../gr-diff-processor/gr-diff-processor.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-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">
|
<dom-module id="gr-diff-builder">
|
||||||
<template>
|
<template>
|
||||||
|
@ -25,6 +26,9 @@ limitations under the License.
|
||||||
<gr-ranged-comment-layer
|
<gr-ranged-comment-layer
|
||||||
id="rangeLayer"
|
id="rangeLayer"
|
||||||
comments="[[comments]]"></gr-ranged-comment-layer>
|
comments="[[comments]]"></gr-ranged-comment-layer>
|
||||||
|
<gr-syntax-layer
|
||||||
|
id="syntaxLayer"
|
||||||
|
diff="[[diff]]"></gr-syntax-layer>
|
||||||
<gr-diff-processor
|
<gr-diff-processor
|
||||||
id="processor"
|
id="processor"
|
||||||
groups="{{_groups}}"></gr-diff-processor>
|
groups="{{_groups}}"></gr-diff-processor>
|
||||||
|
@ -77,6 +81,7 @@ limitations under the License.
|
||||||
attached: function() {
|
attached: function() {
|
||||||
// Setup annotation layers.
|
// Setup annotation layers.
|
||||||
this._layers = [
|
this._layers = [
|
||||||
|
this.$.syntaxLayer,
|
||||||
this._createIntralineLayer(),
|
this._createIntralineLayer(),
|
||||||
this.$.rangeLayer,
|
this.$.rangeLayer,
|
||||||
];
|
];
|
||||||
|
@ -85,6 +90,7 @@ limitations under the License.
|
||||||
render: function(comments, prefs) {
|
render: function(comments, prefs) {
|
||||||
// Stop the processor (if it's running).
|
// Stop the processor (if it's running).
|
||||||
this.$.processor.cancel();
|
this.$.processor.cancel();
|
||||||
|
this.$.syntaxLayer.cancel();
|
||||||
|
|
||||||
this._builder = this._getDiffBuilder(this.diff, comments, prefs);
|
this._builder = this._getDiffBuilder(this.diff, comments, prefs);
|
||||||
|
|
||||||
|
@ -98,6 +104,7 @@ limitations under the License.
|
||||||
if (this.isImageDiff) {
|
if (this.isImageDiff) {
|
||||||
this._builder.renderDiffImages();
|
this._builder.renderDiffImages();
|
||||||
}
|
}
|
||||||
|
this.$.syntaxLayer.process();
|
||||||
console.timeEnd('diff render');
|
console.timeEnd('diff render');
|
||||||
this.fire('render');
|
this.fire('render');
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
@ -280,7 +287,7 @@ limitations under the License.
|
||||||
// differences to highlight and apply them to the element as
|
// differences to highlight and apply them to the element as
|
||||||
// annotations.
|
// annotations.
|
||||||
annotate: function(el, line, GrAnnotation) {
|
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) {
|
line.highlights.forEach(function(highlight) {
|
||||||
// The start and end indices could be the same if a highlight is
|
// 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
|
// 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,
|
GrDiffBuilder.prototype.findLinesByRange = function(start, end, opt_side,
|
||||||
out_lines, out_elements) {
|
out_lines, out_elements) {
|
||||||
var groups = this.getGroupsByLineRange(start, end, opt_side);
|
var groups = this.getGroupsByLineRange(start, end, opt_side);
|
||||||
|
|
||||||
groups.forEach(function(group) {
|
groups.forEach(function(group) {
|
||||||
var content = null;
|
var content = null;
|
||||||
group.lines.forEach(function(line) {
|
group.lines.forEach(function(line) {
|
||||||
|
|
|
@ -176,7 +176,7 @@ limitations under the License.
|
||||||
test('comment-mouse-over from ranged comment causes set', function() {
|
test('comment-mouse-over from ranged comment causes set', function() {
|
||||||
sandbox.stub(element, 'set');
|
sandbox.stub(element, 'set');
|
||||||
sandbox.stub(element, '_indexOfComment').returns(0);
|
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);
|
assert.isTrue(element.set.called);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -101,14 +101,14 @@ limitations under the License.
|
||||||
max-width: var(--content-width, 80ch);
|
max-width: var(--content-width, 80ch);
|
||||||
min-width: var(--content-width, 80ch);
|
min-width: var(--content-width, 80ch);
|
||||||
}
|
}
|
||||||
.content.add hl,
|
.content.add .intraline,
|
||||||
.content.add.darkHighlight {
|
.content.add.darkHighlight {
|
||||||
background-color: var(--dark-add-highlight-color);
|
background-color: var(--dark-add-highlight-color);
|
||||||
}
|
}
|
||||||
.content.add.lightHighlight {
|
.content.add.lightHighlight {
|
||||||
background-color: var(--light-add-highlight-color);
|
background-color: var(--light-add-highlight-color);
|
||||||
}
|
}
|
||||||
.content.remove hl,
|
.content.remove .intraline,
|
||||||
.content.remove.darkHighlight {
|
.content.remove.darkHighlight {
|
||||||
background-color: var(--dark-remove-highlight-color);
|
background-color: var(--dark-remove-highlight-color);
|
||||||
}
|
}
|
||||||
|
@ -140,6 +140,82 @@ limitations under the License.
|
||||||
content: '\00BB';
|
content: '\00BB';
|
||||||
position: absolute;
|
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>
|
</style>
|
||||||
<div class$="[[_computeContainerClass(_loggedIn, viewMode)]]"
|
<div class$="[[_computeContainerClass(_loggedIn, viewMode)]]"
|
||||||
on-tap="_handleTap">
|
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-patch-range-select/gr-patch-range-select_test.html',
|
||||||
'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_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-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-account-info/gr-account-info_test.html',
|
||||||
'settings/gr-email-editor/gr-email-editor_test.html',
|
'settings/gr-email-editor/gr-email-editor_test.html',
|
||||||
'settings/gr-group-list/gr-group-list_test.html',
|
'settings/gr-group-list/gr-group-list_test.html',
|
||||||
|
|
Loading…
Reference in New Issue