PageRenderTime 43ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js

https://gitlab.com/chenfengxu/gerrit
JavaScript | 450 lines | 319 code | 35 blank | 96 comment | 46 complexity | 3d76e5683cf569f2eb16f20ffef2889a MD5 | raw file
  1. /**
  2. * @license
  3. * Copyright (C) 2016 The Android Open Source Project
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. (function() {
  18. 'use strict';
  19. const LANGUAGE_MAP = {
  20. 'application/dart': 'dart',
  21. 'application/json': 'json',
  22. 'application/typescript': 'typescript',
  23. 'application/x-erb': 'erb',
  24. 'text/css': 'css',
  25. 'text/html': 'html',
  26. 'text/javascript': 'js',
  27. 'text/jsx': 'jsx',
  28. 'text/x-c': 'cpp',
  29. 'text/x-c++src': 'cpp',
  30. 'text/x-clojure': 'clojure',
  31. 'text/x-common-lisp': 'lisp',
  32. 'text/x-csharp': 'csharp',
  33. 'text/x-csrc': 'cpp',
  34. 'text/x-d': 'd',
  35. 'text/x-go': 'go',
  36. 'text/x-haskell': 'haskell',
  37. 'text/x-java': 'java',
  38. 'text/x-kotlin': 'kotlin',
  39. 'text/x-lua': 'lua',
  40. 'text/x-markdown': 'markdown',
  41. 'text/x-objectivec': 'objectivec',
  42. 'text/x-ocaml': 'ocaml',
  43. 'text/x-perl': 'perl',
  44. 'text/x-php': 'php',
  45. 'text/x-protobuf': 'protobuf',
  46. 'text/x-puppet': 'puppet',
  47. 'text/x-python': 'python',
  48. 'text/x-ruby': 'ruby',
  49. 'text/x-rustsrc': 'rust',
  50. 'text/x-scala': 'scala',
  51. 'text/x-shell': 'shell',
  52. 'text/x-sh': 'bash',
  53. 'text/x-sql': 'sql',
  54. 'text/x-swift': 'swift',
  55. 'text/x-yaml': 'yaml',
  56. };
  57. const ASYNC_DELAY = 10;
  58. const CLASS_WHITELIST = {
  59. 'gr-diff gr-syntax gr-syntax-attr': true,
  60. 'gr-diff gr-syntax gr-syntax-attribute': true,
  61. 'gr-diff gr-syntax gr-syntax-built_in': true,
  62. 'gr-diff gr-syntax gr-syntax-comment': true,
  63. 'gr-diff gr-syntax gr-syntax-emphasis': true,
  64. 'gr-diff gr-syntax gr-syntax-keyword': true,
  65. 'gr-diff gr-syntax gr-syntax-link': true,
  66. 'gr-diff gr-syntax gr-syntax-literal': true,
  67. 'gr-diff gr-syntax gr-syntax-meta': true,
  68. 'gr-diff gr-syntax gr-syntax-meta-keyword': true,
  69. 'gr-diff gr-syntax gr-syntax-name': true,
  70. 'gr-diff gr-syntax gr-syntax-number': true,
  71. 'gr-diff gr-syntax gr-syntax-regexp': true,
  72. 'gr-diff gr-syntax gr-syntax-selector-attr': true,
  73. 'gr-diff gr-syntax gr-syntax-selector-class': true,
  74. 'gr-diff gr-syntax gr-syntax-selector-id': true,
  75. 'gr-diff gr-syntax gr-syntax-selector-pseudo': true,
  76. 'gr-diff gr-syntax gr-syntax-selector-tag': true,
  77. 'gr-diff gr-syntax gr-syntax-string': true,
  78. 'gr-diff gr-syntax gr-syntax-strong': true,
  79. 'gr-diff gr-syntax gr-syntax-tag': true,
  80. 'gr-diff gr-syntax gr-syntax-template-tag': true,
  81. 'gr-diff gr-syntax gr-syntax-template-variable': true,
  82. 'gr-diff gr-syntax gr-syntax-title': true,
  83. 'gr-diff gr-syntax gr-syntax-type': true,
  84. 'gr-diff gr-syntax gr-syntax-variable': true,
  85. };
  86. const CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</;
  87. const CPP_WCHAR_PATTERN = /L\'(\\)?.\'/g;
  88. const JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g;
  89. const GO_BACKSLASH_LITERAL = '\'\\\\\'';
  90. const GLOBAL_LT_PATTERN = /</g;
  91. Polymer({
  92. is: 'gr-syntax-layer',
  93. properties: {
  94. diff: {
  95. type: Object,
  96. observer: '_diffChanged',
  97. },
  98. enabled: {
  99. type: Boolean,
  100. value: true,
  101. },
  102. _baseRanges: {
  103. type: Array,
  104. value() { return []; },
  105. },
  106. _revisionRanges: {
  107. type: Array,
  108. value() { return []; },
  109. },
  110. _baseLanguage: String,
  111. _revisionLanguage: String,
  112. _listeners: {
  113. type: Array,
  114. value() { return []; },
  115. },
  116. /** @type {?number} */
  117. _processHandle: Number,
  118. _hljs: Object,
  119. },
  120. addListener(fn) {
  121. this.push('_listeners', fn);
  122. },
  123. /**
  124. * Annotation layer method to add syntax annotations to the given element
  125. * for the given line.
  126. * @param {!HTMLElement} el
  127. * @param {!Object} line (GrDiffLine)
  128. */
  129. annotate(el, line) {
  130. if (!this.enabled) { return; }
  131. // Determine the side.
  132. let side;
  133. if (line.type === GrDiffLine.Type.REMOVE || (
  134. line.type === GrDiffLine.Type.BOTH &&
  135. el.getAttribute('data-side') !== 'right')) {
  136. side = 'left';
  137. } else if (line.type === GrDiffLine.Type.ADD || (
  138. el.getAttribute('data-side') !== 'left')) {
  139. side = 'right';
  140. }
  141. // Find the relevant syntax ranges, if any.
  142. let ranges = [];
  143. if (side === 'left' && this._baseRanges.length >= line.beforeNumber) {
  144. ranges = this._baseRanges[line.beforeNumber - 1] || [];
  145. } else if (side === 'right' &&
  146. this._revisionRanges.length >= line.afterNumber) {
  147. ranges = this._revisionRanges[line.afterNumber - 1] || [];
  148. }
  149. // Apply the ranges to the element.
  150. for (const range of ranges) {
  151. GrAnnotation.annotateElement(
  152. el, range.start, range.length, range.className);
  153. }
  154. },
  155. /**
  156. * Start processing symtax for the loaded diff and notify layer listeners
  157. * as syntax info comes online.
  158. * @return {Promise}
  159. */
  160. process() {
  161. // Discard existing ranges.
  162. this._baseRanges = [];
  163. this._revisionRanges = [];
  164. if (!this.enabled || !this.diff.content.length) {
  165. return Promise.resolve();
  166. }
  167. this.cancel();
  168. if (this.diff.meta_a) {
  169. this._baseLanguage = LANGUAGE_MAP[this.diff.meta_a.content_type];
  170. }
  171. if (this.diff.meta_b) {
  172. this._revisionLanguage = LANGUAGE_MAP[this.diff.meta_b.content_type];
  173. }
  174. if (!this._baseLanguage && !this._revisionLanguage) {
  175. return Promise.resolve();
  176. }
  177. const state = {
  178. sectionIndex: 0,
  179. lineIndex: 0,
  180. baseContext: undefined,
  181. revisionContext: undefined,
  182. lineNums: {left: 1, right: 1},
  183. lastNotify: {left: 1, right: 1},
  184. };
  185. return this._loadHLJS().then(() => {
  186. return new Promise(resolve => {
  187. const nextStep = () => {
  188. this._processHandle = null;
  189. this._processNextLine(state);
  190. // Move to the next line in the section.
  191. state.lineIndex++;
  192. // If the section has been exhausted, move to the next one.
  193. if (this._isSectionDone(state)) {
  194. state.lineIndex = 0;
  195. state.sectionIndex++;
  196. }
  197. // If all sections have been exhausted, finish.
  198. if (state.sectionIndex >= this.diff.content.length) {
  199. resolve();
  200. this._notify(state);
  201. return;
  202. }
  203. if (state.lineIndex % 100 === 0) {
  204. this._notify(state);
  205. this._processHandle = this.async(nextStep, ASYNC_DELAY);
  206. } else {
  207. nextStep.call(this);
  208. }
  209. };
  210. this._processHandle = this.async(nextStep, 1);
  211. });
  212. });
  213. },
  214. /**
  215. * Cancel any asynchronous syntax processing jobs.
  216. */
  217. cancel() {
  218. if (this._processHandle) {
  219. this.cancelAsync(this._processHandle);
  220. this._processHandle = null;
  221. }
  222. },
  223. _diffChanged() {
  224. this.cancel();
  225. this._baseRanges = [];
  226. this._revisionRanges = [];
  227. },
  228. /**
  229. * Take a string of HTML with the (potentially nested) syntax markers
  230. * Highlight.js emits and emit a list of text ranges and classes for the
  231. * markers.
  232. * @param {string} str The string of HTML.
  233. * @return {!Array<!Object>} The list of ranges.
  234. */
  235. _rangesFromString(str) {
  236. const div = document.createElement('div');
  237. div.innerHTML = str;
  238. return this._rangesFromElement(div, 0);
  239. },
  240. _rangesFromElement(elem, offset) {
  241. let result = [];
  242. for (const node of elem.childNodes) {
  243. const nodeLength = GrAnnotation.getLength(node);
  244. // Note: HLJS may emit a span with class undefined when it thinks there
  245. // may be a syntax error.
  246. if (node.tagName === 'SPAN' && node.className !== 'undefined') {
  247. if (CLASS_WHITELIST.hasOwnProperty(node.className)) {
  248. result.push({
  249. start: offset,
  250. length: nodeLength,
  251. className: node.className,
  252. });
  253. }
  254. if (node.children.length) {
  255. result = result.concat(this._rangesFromElement(node, offset));
  256. }
  257. }
  258. offset += nodeLength;
  259. }
  260. return result;
  261. },
  262. /**
  263. * For a given state, process the syntax for the next line (or pair of
  264. * lines).
  265. * @param {!Object} state The processing state for the layer.
  266. */
  267. _processNextLine(state) {
  268. let baseLine;
  269. let revisionLine;
  270. const section = this.diff.content[state.sectionIndex];
  271. if (section.ab) {
  272. baseLine = section.ab[state.lineIndex];
  273. revisionLine = section.ab[state.lineIndex];
  274. state.lineNums.left++;
  275. state.lineNums.right++;
  276. } else {
  277. if (section.a && section.a.length > state.lineIndex) {
  278. baseLine = section.a[state.lineIndex];
  279. state.lineNums.left++;
  280. }
  281. if (section.b && section.b.length > state.lineIndex) {
  282. revisionLine = section.b[state.lineIndex];
  283. state.lineNums.right++;
  284. }
  285. }
  286. // To store the result of the syntax highlighter.
  287. let result;
  288. if (this._baseLanguage && baseLine !== undefined) {
  289. baseLine = this._workaround(this._baseLanguage, baseLine);
  290. result = this._hljs.highlight(this._baseLanguage, baseLine, true,
  291. state.baseContext);
  292. this.push('_baseRanges', this._rangesFromString(result.value));
  293. state.baseContext = result.top;
  294. }
  295. if (this._revisionLanguage && revisionLine !== undefined) {
  296. revisionLine = this._workaround(this._revisionLanguage, revisionLine);
  297. result = this._hljs.highlight(this._revisionLanguage, revisionLine,
  298. true, state.revisionContext);
  299. this.push('_revisionRanges', this._rangesFromString(result.value));
  300. state.revisionContext = result.top;
  301. }
  302. },
  303. /**
  304. * Ad hoc fixes for HLJS parsing bugs. Rewrite lines of code in constrained
  305. * cases before sending them into HLJS so that they parse correctly.
  306. *
  307. * Important notes:
  308. * * These tests should be as constrained as possible to avoid interfering
  309. * with code it shouldn't AND to avoid executing regexes as much as
  310. * possible.
  311. * * These tests should document the issue clearly enough that the test can
  312. * be condidently removed when the issue is solved in HLJS.
  313. * * These tests should rewrite the line of code to have the same number of
  314. * characters. This method rewrites the string that gets parsed, but NOT
  315. * the string that gets displayed and highlighted. Thus, the positions
  316. * must be consistent.
  317. *
  318. * @param {!string} language The name of the HLJS language plugin in use.
  319. * @param {!string} line The line of code to potentially rewrite.
  320. * @return {string} A potentially-rewritten line of code.
  321. */
  322. _workaround(language, line) {
  323. if (language === 'cpp') {
  324. /**
  325. * Prevent confusing < and << operators for the start of a meta string
  326. * by converting them to a different operator.
  327. * {@see Issue 4864}
  328. * {@see https://github.com/isagalaev/highlight.js/issues/1341}
  329. */
  330. if (CPP_DIRECTIVE_WITH_LT_PATTERN.test(line)) {
  331. line = line.replace(GLOBAL_LT_PATTERN, '|');
  332. }
  333. /**
  334. * Rewrite CPP wchar_t characters literals to wchar_t string literals
  335. * because HLJS only understands the string form.
  336. * {@see Issue 5242}
  337. * {#see https://github.com/isagalaev/highlight.js/issues/1412}
  338. */
  339. if (CPP_WCHAR_PATTERN.test(line)) {
  340. line = line.replace(CPP_WCHAR_PATTERN, 'L"$1."');
  341. }
  342. return line;
  343. }
  344. /**
  345. * Prevent confusing the closing paren of a parameterized Java annotation
  346. * being applied to a formal argument as the closing paren of the argument
  347. * list. Rewrite the parens as spaces.
  348. * {@see Issue 4776}
  349. * {@see https://github.com/isagalaev/highlight.js/issues/1324}
  350. */
  351. if (language === 'java' && JAVA_PARAM_ANNOT_PATTERN.test(line)) {
  352. return line.replace(JAVA_PARAM_ANNOT_PATTERN, '$1 $2 ');
  353. }
  354. /**
  355. * HLJS misunderstands backslash character literals in Go.
  356. * {@see Issue 5007}
  357. * {#see https://github.com/isagalaev/highlight.js/issues/1411}
  358. */
  359. if (language === 'go' && line.includes(GO_BACKSLASH_LITERAL)) {
  360. return line.replace(GO_BACKSLASH_LITERAL, '"\\\\"');
  361. }
  362. return line;
  363. },
  364. /**
  365. * Tells whether the state has exhausted its current section.
  366. * @param {!Object} state
  367. * @return {boolean}
  368. */
  369. _isSectionDone(state) {
  370. const section = this.diff.content[state.sectionIndex];
  371. if (section.ab) {
  372. return state.lineIndex >= section.ab.length;
  373. } else {
  374. return (!section.a || state.lineIndex >= section.a.length) &&
  375. (!section.b || state.lineIndex >= section.b.length);
  376. }
  377. },
  378. /**
  379. * For a given state, notify layer listeners of any processed line ranges
  380. * that have not yet been notified.
  381. * @param {!Object} state
  382. */
  383. _notify(state) {
  384. if (state.lineNums.left - state.lastNotify.left) {
  385. this._notifyRange(
  386. state.lastNotify.left,
  387. state.lineNums.left,
  388. 'left');
  389. state.lastNotify.left = state.lineNums.left;
  390. }
  391. if (state.lineNums.right - state.lastNotify.right) {
  392. this._notifyRange(
  393. state.lastNotify.right,
  394. state.lineNums.right,
  395. 'right');
  396. state.lastNotify.right = state.lineNums.right;
  397. }
  398. },
  399. _notifyRange(start, end, side) {
  400. for (const fn of this._listeners) {
  401. fn(start, end, side);
  402. }
  403. },
  404. _loadHLJS() {
  405. return this.$.libLoader.getHLJS().then(hljs => {
  406. this._hljs = hljs;
  407. });
  408. },
  409. });
  410. })();