
It is highlighted in with javascript.js in highlight.js so it dosen't have a seperate file for it's lang. Bug: Issue 8646 Change-Id: I5f561c342b96334eda82f8165b018209f10cab36
451 lines
14 KiB
JavaScript
451 lines
14 KiB
JavaScript
/**
|
|
* @license
|
|
* 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';
|
|
|
|
const LANGUAGE_MAP = {
|
|
'application/dart': 'dart',
|
|
'application/json': 'json',
|
|
'application/typescript': 'typescript',
|
|
'application/x-erb': 'erb',
|
|
'text/css': 'css',
|
|
'text/html': 'html',
|
|
'text/javascript': 'js',
|
|
'text/jsx': 'jsx',
|
|
'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-kotlin': 'kotlin',
|
|
'text/x-lua': 'lua',
|
|
'text/x-markdown': 'markdown',
|
|
'text/x-objectivec': 'objectivec',
|
|
'text/x-ocaml': 'ocaml',
|
|
'text/x-perl': 'perl',
|
|
'text/x-php': 'php',
|
|
'text/x-protobuf': 'protobuf',
|
|
'text/x-puppet': 'puppet',
|
|
'text/x-python': 'python',
|
|
'text/x-ruby': 'ruby',
|
|
'text/x-rustsrc': 'rust',
|
|
'text/x-scala': 'scala',
|
|
'text/x-shell': 'shell',
|
|
'text/x-sh': 'bash',
|
|
'text/x-sql': 'sql',
|
|
'text/x-swift': 'swift',
|
|
'text/x-yaml': 'yaml',
|
|
};
|
|
const ASYNC_DELAY = 10;
|
|
|
|
const CLASS_WHITELIST = {
|
|
'gr-diff gr-syntax gr-syntax-attr': true,
|
|
'gr-diff gr-syntax gr-syntax-attribute': true,
|
|
'gr-diff gr-syntax gr-syntax-built_in': true,
|
|
'gr-diff gr-syntax gr-syntax-comment': true,
|
|
'gr-diff gr-syntax gr-syntax-emphasis': true,
|
|
'gr-diff gr-syntax gr-syntax-keyword': true,
|
|
'gr-diff gr-syntax gr-syntax-link': true,
|
|
'gr-diff gr-syntax gr-syntax-literal': true,
|
|
'gr-diff gr-syntax gr-syntax-meta': true,
|
|
'gr-diff gr-syntax gr-syntax-meta-keyword': true,
|
|
'gr-diff gr-syntax gr-syntax-name': true,
|
|
'gr-diff gr-syntax gr-syntax-number': true,
|
|
'gr-diff gr-syntax gr-syntax-regexp': true,
|
|
'gr-diff gr-syntax gr-syntax-selector-attr': true,
|
|
'gr-diff gr-syntax gr-syntax-selector-class': true,
|
|
'gr-diff gr-syntax gr-syntax-selector-id': true,
|
|
'gr-diff gr-syntax gr-syntax-selector-pseudo': true,
|
|
'gr-diff gr-syntax gr-syntax-selector-tag': true,
|
|
'gr-diff gr-syntax gr-syntax-string': true,
|
|
'gr-diff gr-syntax gr-syntax-strong': true,
|
|
'gr-diff gr-syntax gr-syntax-tag': true,
|
|
'gr-diff gr-syntax gr-syntax-template-tag': true,
|
|
'gr-diff gr-syntax gr-syntax-template-variable': true,
|
|
'gr-diff gr-syntax gr-syntax-title': true,
|
|
'gr-diff gr-syntax gr-syntax-type': true,
|
|
'gr-diff gr-syntax gr-syntax-variable': true,
|
|
};
|
|
|
|
const CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</;
|
|
const CPP_WCHAR_PATTERN = /L\'(\\)?.\'/g;
|
|
const JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g;
|
|
const GO_BACKSLASH_LITERAL = '\'\\\\\'';
|
|
const GLOBAL_LT_PATTERN = /</g;
|
|
|
|
Polymer({
|
|
is: 'gr-syntax-layer',
|
|
|
|
properties: {
|
|
diff: {
|
|
type: Object,
|
|
observer: '_diffChanged',
|
|
},
|
|
enabled: {
|
|
type: Boolean,
|
|
value: true,
|
|
},
|
|
_baseRanges: {
|
|
type: Array,
|
|
value() { return []; },
|
|
},
|
|
_revisionRanges: {
|
|
type: Array,
|
|
value() { return []; },
|
|
},
|
|
_baseLanguage: String,
|
|
_revisionLanguage: String,
|
|
_listeners: {
|
|
type: Array,
|
|
value() { return []; },
|
|
},
|
|
/** @type {?number} */
|
|
_processHandle: Number,
|
|
_hljs: Object,
|
|
},
|
|
|
|
addListener(fn) {
|
|
this.push('_listeners', fn);
|
|
},
|
|
|
|
/**
|
|
* Annotation layer method to add syntax annotations to the given element
|
|
* for the given line.
|
|
* @param {!HTMLElement} el
|
|
* @param {!Object} line (GrDiffLine)
|
|
*/
|
|
annotate(el, line) {
|
|
if (!this.enabled) { return; }
|
|
|
|
// Determine the side.
|
|
let 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.
|
|
let 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.
|
|
for (const range of ranges) {
|
|
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() {
|
|
// 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();
|
|
}
|
|
|
|
const state = {
|
|
sectionIndex: 0,
|
|
lineIndex: 0,
|
|
baseContext: undefined,
|
|
revisionContext: undefined,
|
|
lineNums: {left: 1, right: 1},
|
|
lastNotify: {left: 1, right: 1},
|
|
};
|
|
|
|
return this._loadHLJS().then(() => {
|
|
return new Promise(resolve => {
|
|
const nextStep = () => {
|
|
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.lineIndex % 100 === 0) {
|
|
this._notify(state);
|
|
this._processHandle = this.async(nextStep, ASYNC_DELAY);
|
|
} else {
|
|
nextStep.call(this);
|
|
}
|
|
};
|
|
|
|
this._processHandle = this.async(nextStep, 1);
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Cancel any asynchronous syntax processing jobs.
|
|
*/
|
|
cancel() {
|
|
if (this._processHandle) {
|
|
this.cancelAsync(this._processHandle);
|
|
this._processHandle = null;
|
|
}
|
|
},
|
|
|
|
_diffChanged() {
|
|
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(str) {
|
|
const div = document.createElement('div');
|
|
div.innerHTML = str;
|
|
return this._rangesFromElement(div, 0);
|
|
},
|
|
|
|
_rangesFromElement(elem, offset) {
|
|
let result = [];
|
|
for (const node of elem.childNodes) {
|
|
const 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') {
|
|
if (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(state) {
|
|
let baseLine;
|
|
let revisionLine;
|
|
|
|
const 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.
|
|
let result;
|
|
|
|
if (this._baseLanguage && baseLine !== undefined) {
|
|
baseLine = this._workaround(this._baseLanguage, baseLine);
|
|
result = this._hljs.highlight(this._baseLanguage, baseLine, true,
|
|
state.baseContext);
|
|
this.push('_baseRanges', this._rangesFromString(result.value));
|
|
state.baseContext = result.top;
|
|
}
|
|
|
|
if (this._revisionLanguage && revisionLine !== undefined) {
|
|
revisionLine = this._workaround(this._revisionLanguage, revisionLine);
|
|
result = this._hljs.highlight(this._revisionLanguage, revisionLine,
|
|
true, state.revisionContext);
|
|
this.push('_revisionRanges', this._rangesFromString(result.value));
|
|
state.revisionContext = result.top;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Ad hoc fixes for HLJS parsing bugs. Rewrite lines of code in constrained
|
|
* cases before sending them into HLJS so that they parse correctly.
|
|
*
|
|
* Important notes:
|
|
* * These tests should be as constrained as possible to avoid interfering
|
|
* with code it shouldn't AND to avoid executing regexes as much as
|
|
* possible.
|
|
* * These tests should document the issue clearly enough that the test can
|
|
* be condidently removed when the issue is solved in HLJS.
|
|
* * These tests should rewrite the line of code to have the same number of
|
|
* characters. This method rewrites the string that gets parsed, but NOT
|
|
* the string that gets displayed and highlighted. Thus, the positions
|
|
* must be consistent.
|
|
*
|
|
* @param {!string} language The name of the HLJS language plugin in use.
|
|
* @param {!string} line The line of code to potentially rewrite.
|
|
* @return {string} A potentially-rewritten line of code.
|
|
*/
|
|
_workaround(language, line) {
|
|
if (language === 'cpp') {
|
|
/**
|
|
* Prevent confusing < and << operators for the start of a meta string
|
|
* by converting them to a different operator.
|
|
* {@see Issue 4864}
|
|
* {@see https://github.com/isagalaev/highlight.js/issues/1341}
|
|
*/
|
|
if (CPP_DIRECTIVE_WITH_LT_PATTERN.test(line)) {
|
|
line = line.replace(GLOBAL_LT_PATTERN, '|');
|
|
}
|
|
|
|
/**
|
|
* Rewrite CPP wchar_t characters literals to wchar_t string literals
|
|
* because HLJS only understands the string form.
|
|
* {@see Issue 5242}
|
|
* {#see https://github.com/isagalaev/highlight.js/issues/1412}
|
|
*/
|
|
if (CPP_WCHAR_PATTERN.test(line)) {
|
|
line = line.replace(CPP_WCHAR_PATTERN, 'L"$1."');
|
|
}
|
|
|
|
return line;
|
|
}
|
|
|
|
/**
|
|
* Prevent confusing the closing paren of a parameterized Java annotation
|
|
* being applied to a formal argument as the closing paren of the argument
|
|
* list. Rewrite the parens as spaces.
|
|
* {@see Issue 4776}
|
|
* {@see https://github.com/isagalaev/highlight.js/issues/1324}
|
|
*/
|
|
if (language === 'java' && JAVA_PARAM_ANNOT_PATTERN.test(line)) {
|
|
return line.replace(JAVA_PARAM_ANNOT_PATTERN, '$1 $2 ');
|
|
}
|
|
|
|
/**
|
|
* HLJS misunderstands backslash character literals in Go.
|
|
* {@see Issue 5007}
|
|
* {#see https://github.com/isagalaev/highlight.js/issues/1411}
|
|
*/
|
|
if (language === 'go' && line.includes(GO_BACKSLASH_LITERAL)) {
|
|
return line.replace(GO_BACKSLASH_LITERAL, '"\\\\"');
|
|
}
|
|
|
|
return line;
|
|
},
|
|
|
|
/**
|
|
* Tells whether the state has exhausted its current section.
|
|
* @param {!Object} state
|
|
* @return {boolean}
|
|
*/
|
|
_isSectionDone(state) {
|
|
const 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(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(start, end, side) {
|
|
for (const fn of this._listeners) {
|
|
fn(start, end, side);
|
|
}
|
|
},
|
|
|
|
_loadHLJS() {
|
|
return this.$.libLoader.get().then(hljs => {
|
|
this._hljs = hljs;
|
|
});
|
|
},
|
|
});
|
|
})();
|