blob: 478bcc88b9beec24b9012f22087d0e49d6169e4c [file] [log] [blame]
// 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/dart': 'dart',
'application/json': 'json',
'application/typescript': 'typescript',
'text/css': 'css',
'text/html': 'html',
'text/javascript': 'js',
'text/x-c': 'cpp',
'text/x-c++src': 'cpp',
'text/x-clojure': 'clojure',
'text/x-common-lisp': 'lisp',
'text/x-csharp': 'csharp',
'text/x-csrc': 'cpp',
'text/x-d': 'd',
'text/x-go': 'go',
'text/x-haskell': 'haskell',
'text/x-java': 'java',
'text/x-lua': 'lua',
'text/x-markdown': 'markdown',
'text/x-objectivec': 'objectivec',
'text/x-ocaml': 'ocaml',
'text/x-perl': 'perl',
'text/x-protobuf': 'protobuf',
'text/x-python': 'python',
'text/x-ruby': 'ruby',
'text/x-rustsrc': 'rust',
'text/x-scala': 'scala',
'text/x-sh': 'bash',
'text/x-sql': 'sql',
'text/x-swift': 'swift',
'text/x-yaml': 'yaml',
};
var ASYNC_DELAY = 10;
var HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
var CLASS_WHITELIST = {
'gr-diff gr-syntax gr-syntax-literal': true,
'gr-diff gr-syntax gr-syntax-keyword': true,
'gr-diff gr-syntax gr-syntax-selector-tag': true,
'gr-diff gr-syntax gr-syntax-built_in': true,
'gr-diff gr-syntax gr-syntax-type': true,
'gr-diff gr-syntax gr-syntax-selector-pseudo': true,
'gr-diff gr-syntax gr-syntax-template-variable': true,
'gr-diff gr-syntax gr-syntax-number': true,
'gr-diff gr-syntax gr-syntax-regexp': true,
'gr-diff gr-syntax gr-syntax-variable': true,
'gr-diff gr-syntax gr-syntax-selector-attr': true,
'gr-diff gr-syntax gr-syntax-template-tag': true,
'gr-diff gr-syntax gr-syntax-string': true,
'gr-diff gr-syntax gr-syntax-selector-id': true,
'gr-diff gr-syntax gr-syntax-title': true,
'gr-diff gr-syntax gr-syntax-params': true,
'gr-diff gr-syntax gr-syntax-comment': true,
'gr-diff gr-syntax gr-syntax-meta': true,
'gr-diff gr-syntax gr-syntax-meta-keyword': true,
'gr-diff gr-syntax gr-syntax-tag': true,
'gr-diff gr-syntax gr-syntax-name': true,
'gr-diff gr-syntax gr-syntax-attr': true,
'gr-diff gr-syntax gr-syntax-attribute': true,
'gr-diff gr-syntax gr-syntax-emphasis': true,
'gr-diff gr-syntax gr-syntax-strong': true,
'gr-diff gr-syntax gr-syntax-link': true,
'gr-diff gr-syntax gr-syntax-selector-class': true,
};
Polymer({
is: 'gr-syntax-layer',
properties: {
diff: {
type: Object,
observer: '_diffChanged',
},
enabled: {
type: Boolean,
value: true,
},
_baseRanges: {
type: Array,
value: function() { return []; },
},
_revisionRanges: {
type: Array,
value: function() { return []; },
},
_baseLanguage: String,
_revisionLanguage: String,
_listeners: {
type: Array,
value: function() { return []; },
},
_processHandle: Number,
},
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) {
if (!this.enabled) { return; }
// 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() {
// Discard existing ranges.
this._baseRanges = [];
this._revisionRanges = [];
if (!this.enabled || !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];
}
if (!this._baseLanguage && !this._revisionLanguage) {
return Promise.resolve();
}
var state = {
sectionIndex: 0,
lineIndex: 0,
baseContext: undefined,
revisionContext: undefined,
lineNums: {left: 1, right: 1},
lastNotify: {left: 1, right: 1},
};
return this._loadHLJS().then(function() {
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));
}.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' &&
CLASS_WHITELIST.hasOwnProperty(node.className)) {
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 hljs = this._getHighlightLib();
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);
});
},
_getHighlightLib: function() {
return window.hljs;
},
_isHighlightLibLoaded: function() {
return !!this._getHighlightLib();
},
_configureHighlightLib: function() {
this._getHighlightLib().configure(
{classPrefix: 'gr-diff gr-syntax gr-syntax-'});
},
_getLibRoot: function() {
if (this._cachedLibRoot) { return this._cachedLibRoot; }
return this._cachedLibRoot = document.head
.querySelector('link[rel=import][href$="gr-app.html"]')
.href
.match(/(.+\/)elements\/gr-app\.html/)[1];
},
_cachedLibRoot: null,
/**
* Load and configure the HighlightJS library. If the library is already
* loaded, then do nothing and resolve.
* @return {Promise}
*/
_loadHLJS: function() {
if (this._isHighlightLibLoaded()) { return Promise.resolve(); }
return new Promise(function(resolve) {
var script = document.createElement('script');
script.src = this._getLibRoot() + HLJS_PATH;
script.onload = function() {
this._configureHighlightLib();
resolve();
}.bind(this);
Polymer.dom(this.root).appendChild(script);
}.bind(this));
}
});
})();