regexp-tester.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. /* global $create */// dom.js
  2. /* global URLS openURL tryRegExp */// toolbox.js
  3. /* global helpPopup */// util.js
  4. /* global t */// localization.js
  5. 'use strict';
  6. const regexpTester = (() => {
  7. const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
  8. const OWN_ICON = chrome.runtime.getManifest().icons['16'];
  9. const cachedRegexps = new Map();
  10. let currentRegexps = [];
  11. let isWatching = false;
  12. let isShown = false;
  13. return {
  14. toggle(state = !isShown) {
  15. if (state && !isShown) {
  16. if (!isWatching) {
  17. isWatching = true;
  18. chrome.tabs.onRemoved.addListener(onTabRemoved);
  19. chrome.tabs.onUpdated.addListener(onTabUpdated);
  20. }
  21. helpPopup.show('', $create('.regexp-report'));
  22. window.on('closeHelp', () => regexpTester.toggle(false), {once: true});
  23. isShown = true;
  24. } else if (!state && isShown) {
  25. unwatch();
  26. helpPopup.close();
  27. isShown = false;
  28. }
  29. },
  30. async update(newRegexps) {
  31. if (!isShown) {
  32. unwatch();
  33. return;
  34. }
  35. if (newRegexps) {
  36. currentRegexps = newRegexps;
  37. }
  38. const regexps = currentRegexps.map(text => {
  39. const rxData = Object.assign({text}, cachedRegexps.get(text));
  40. if (!rxData.urls) {
  41. cachedRegexps.set(text, Object.assign(rxData, {
  42. // imitate buggy Stylish-for-chrome
  43. rx: tryRegExp('^' + text + '$'),
  44. urls: new Map(),
  45. }));
  46. }
  47. return rxData;
  48. });
  49. const getMatchInfo = m => m && {text: m[0], pos: m.index};
  50. const tabs = await browser.tabs.query({});
  51. const supported = tabs.map(tab => tab.pendingUrl || tab.url).filter(URLS.supported);
  52. const unique = [...new Set(supported).values()];
  53. for (const rxData of regexps) {
  54. const {rx, urls} = rxData;
  55. if (rx) {
  56. const urlsNow = new Map();
  57. for (const url of unique) {
  58. const match = urls.get(url) || getMatchInfo(url.match(rx));
  59. if (match) {
  60. urlsNow.set(url, match);
  61. }
  62. }
  63. rxData.urls = urlsNow;
  64. }
  65. }
  66. const stats = {
  67. full: {data: [], label: t('styleRegexpTestFull')},
  68. partial: {
  69. data: [], label: [
  70. t('styleRegexpTestPartial'),
  71. t.template.regexpTestPartial.cloneNode(true),
  72. ],
  73. },
  74. none: {data: [], label: t('styleRegexpTestNone')},
  75. invalid: {data: [], label: t('styleRegexpTestInvalid')},
  76. };
  77. // collect stats
  78. for (const {text, rx, urls} of regexps) {
  79. if (!rx) {
  80. stats.invalid.data.push({text});
  81. continue;
  82. }
  83. if (!urls.size) {
  84. stats.none.data.push({text});
  85. continue;
  86. }
  87. const full = [];
  88. const partial = [];
  89. for (const [url, match] of urls.entries()) {
  90. const faviconUrl = url.startsWith(URLS.ownOrigin)
  91. ? OWN_ICON
  92. : GET_FAVICON_URL + new URL(url).hostname;
  93. const icon = $create('img', {src: faviconUrl});
  94. if (match.text.length === url.length) {
  95. full.push($create('a', {tabIndex: 0}, [
  96. icon,
  97. url,
  98. ]));
  99. } else {
  100. partial.push($create('a', {tabIndex: 0}, [
  101. icon,
  102. url.substr(0, match.pos),
  103. $create('mark', match.text),
  104. url.substr(match.pos + match.text.length),
  105. ]));
  106. }
  107. }
  108. if (full.length) {
  109. stats.full.data.push({text, urls: full});
  110. }
  111. if (partial.length) {
  112. stats.partial.data.push({text, urls: partial});
  113. }
  114. }
  115. // render stats
  116. const report = $create('.regexp-report');
  117. const br = $create('br');
  118. for (const type in stats) {
  119. // top level groups: full, partial, none, invalid
  120. const {label, data} = stats[type];
  121. if (!data.length) {
  122. continue;
  123. }
  124. const block = report.appendChild(
  125. $create('details', {open: true, dataset: {type}}, [
  126. $create('summary', label),
  127. ]));
  128. // 2nd level: regexp text
  129. for (const {text, urls} of data) {
  130. if (urls) {
  131. // type is partial or full
  132. block.appendChild(
  133. $create('details', {open: true}, [
  134. $create('summary', text),
  135. $create('div', urls),
  136. ]));
  137. } else {
  138. // type is none or invalid
  139. block.appendChild(document.createTextNode(text));
  140. block.appendChild(br.cloneNode());
  141. }
  142. }
  143. }
  144. helpPopup.show(t('styleRegexpTestTitle'), report);
  145. report.onclick = onClick;
  146. const note = $create('p.regexp-report-note',
  147. t('styleRegexpTestNote')
  148. .split(/(\\+)/)
  149. .map(s => (s.startsWith('\\') ? $create('code', s) : s)));
  150. report.appendChild(note);
  151. report.style.paddingBottom = note.offsetHeight + 'px';
  152. },
  153. };
  154. function onClick(event) {
  155. const a = event.target.closest('a, button');
  156. if (a) {
  157. event.preventDefault();
  158. openURL({
  159. url: a.href || a.textContent,
  160. currentWindow: null,
  161. });
  162. }
  163. }
  164. function onTabRemoved() {
  165. regexpTester.update();
  166. }
  167. function onTabUpdated(tabId, info) {
  168. if (info.url) {
  169. regexpTester.update();
  170. }
  171. }
  172. function unwatch() {
  173. if (isWatching) {
  174. chrome.tabs.onRemoved.removeListener(onTabRemoved);
  175. chrome.tabs.onUpdated.removeListener(onTabUpdated);
  176. isWatching = false;
  177. }
  178. }
  179. })();