| // 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', |
| 'application/x-erb': 'erb', |
| '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-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', |
| }; |
| var ASYNC_DELAY = 10; |
| |
| 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-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, |
| }; |
| |
| var CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</; |
| var CPP_WCHAR_PATTERN = /L\'.\'/g; |
| var JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g; |
| var GO_BACKSLASH_LITERAL = '\'\\\\\''; |
| var GLOBAL_LT_PATTERN = /</g; |
| |
| 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, |
| _hljs: Object, |
| }, |
| |
| 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.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') { |
| 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: function(state) { |
| var baseLine; |
| var revisionLine; |
| |
| 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) { |
| 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: function(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"."'); |
| } |
| |
| 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.indexOf(GO_BACKSLASH_LITERAL) !== -1) { |
| return line.replace(GO_BACKSLASH_LITERAL, '"\\\\"'); |
| } |
| |
| return line; |
| }, |
| |
| /** |
| * 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); |
| }); |
| }, |
| |
| _loadHLJS: function() { |
| return this.$.libLoader.get().then(function(hljs) { |
| this._hljs = hljs; |
| }.bind(this)); |
| }, |
| }); |
| })(); |