Web Inspector

Allows you to inspect web pages

  1. // ==UserScript==
  2. // @name Web Inspector
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1.4
  5. // @description Allows you to inspect web pages
  6. // @author https://gf.qytechs.cn/en/users/85040-dan-wl-danwl
  7. // @license MIT
  8. // @match *://*/*
  9. // @run-at document-start
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. // MIT License
  14.  
  15. // Copyright(c) 2024 DanWL
  16.  
  17. // Permission is hereby granted, free of charge, to any person obtaining a copy
  18. // of this software and associated documentation files(the "Software"), to deal
  19. // in the Software without restriction, including without limitation the rights
  20. // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
  21. // copies of the Software, and to permit persons to whom the Software is
  22. // furnished to do so, subject to the following conditions:
  23.  
  24. // The above copyright notice and this permission notice shall be included in all
  25. // copies or substantial portions of the Software.
  26.  
  27. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  28. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  29. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
  30. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  31. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  32. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  33. // SOFTWARE.
  34.  
  35.  
  36. // This userscript defines a function on the web page you visit
  37. // For the function to do anything, use this bookmarklet:
  38. // javascript:(function(){WEB_INSPECTOR();})();
  39. // A bookmarklet is essentially a regular browser bookmark/favorite but with a JavaScript url
  40.  
  41. // jshint esnext: false, esversion: 8
  42.  
  43. (() => {
  44. function debugAlert(str) {
  45. const debug = false;
  46.  
  47. if (debug) {
  48. alert(str);
  49. }
  50. }
  51.  
  52. function el(tagName, className) {
  53. const ret = document.createElement(tagName);
  54.  
  55. if (className) {
  56. ret.className = className;
  57. }
  58.  
  59. return ret;
  60. }
  61.  
  62. function spanNode(className, innerTextOrText) {
  63. const span = el('span', className);
  64.  
  65. if (typeof innerTextOrText !== 'undefined') {
  66. if (typeof innerTextOrText === 'string') {
  67. innerTextOrText = textNode(innerTextOrText);
  68. }
  69.  
  70. if (typeof innerTextOrText === 'object' && isFinite(innerTextOrText.nodeType) && innerTextOrText.nodeType === Node.TEXT_NODE) {
  71. // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
  72. // using instanceof doesnt always work
  73.  
  74. append(span, innerTextOrText);
  75. }
  76. else {
  77. throw new Error('spanNode(className, innerTextOrText) innerTextOrText is ' + innerTextOrText);
  78. }
  79. }
  80.  
  81. return span;
  82. }
  83.  
  84. function brNode() {
  85. return el('br');
  86. }
  87.  
  88. function textNode(txt) {
  89. return document.createTextNode(txt);
  90. }
  91.  
  92. function append(parent, nodes) {
  93. // enables much better minimising
  94.  
  95. if (!Array.isArray(nodes)) {
  96. nodes = [nodes];
  97. }
  98.  
  99. nodes.forEach((node) => {
  100. parent.appendChild(node);
  101. });
  102. }
  103.  
  104. function htmlSymbol(symbol) {
  105. return spanNode('html-symbol', symbol);
  106. }
  107.  
  108. function createTagNameNode(tagName) {
  109. return spanNode('tag-name', tagName);
  110. }
  111.  
  112. function createTagAttributeValueNode(attribute) {
  113. const isLink = ['href', 'src'].includes(attribute.name);
  114. const isStyle = attribute.name === 'style';
  115. const span = spanNode('tag-attribute-value');
  116.  
  117. if (isLink) {
  118. append(span, [textNode('"'), createLink(attribute.value, attribute.value), textNode('"')]);
  119. }
  120. else if (isStyle) {
  121. append(span, [textNode('"'), parseStyle(attribute.ownerElement.style), textNode('"')]);
  122. }
  123. else {
  124. append(span, textNode(JSON.stringify(attribute.value)));
  125. }
  126.  
  127. return span;
  128. }
  129.  
  130. function createPlainTextNode(node) {
  131. // TODO html entities highlighting
  132.  
  133. return spanNode('text', textNode(applyHTMLWhitespaceRules(node.textContent)));
  134. }
  135.  
  136. function elementDoesNotNeedToBeClosed(tagName) {
  137. // https://developer.mozilla.org/en-US/docs/Web/HTML/Element
  138.  
  139. return ['base', 'link', 'meta', 'hr', 'br', 'wbr', 'area', 'img', 'track', 'embed', 'source', 'input'].includes(tagName);
  140. }
  141.  
  142. function applyHTMLWhitespaceRules(text) {
  143. // https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace
  144.  
  145. return text
  146. .replace(/^[\t ]+/mg, '')
  147. .replace(/[\t\r\n]/g, ' ')
  148. .replace(/ {2,}/g, ' ');
  149. }
  150.  
  151. function createSpacer(spacing) {
  152. const spacer = el('pre', 'spacer');
  153.  
  154. spacer.innerHTML = spacing;
  155.  
  156. return spacer;
  157. }
  158.  
  159. function createIndentSpacer(indentLevel) {
  160. const space = '\t';
  161. let spacing = '';
  162.  
  163. while (indentLevel > 0) {
  164. spacing += space;
  165. indentLevel--;
  166. }
  167.  
  168. const spacer = createSpacer(spacing);
  169.  
  170. spacer.className += ' indentation';
  171.  
  172. return spacer;
  173. }
  174.  
  175. function createLink(url, displayText) {
  176. const link = el('a');
  177.  
  178. link.href = url;
  179. link.target = '_blank';
  180.  
  181. append(link, textNode(displayText));
  182.  
  183. return link;
  184. }
  185.  
  186. function createExpandCollapseBtn(element) {
  187. // https://www.amp-what.com &#9660
  188.  
  189. const btn = el('button', 'expand-collapse-button');
  190.  
  191. btn.innerHTML = '▼';
  192.  
  193. return btn;
  194. }
  195.  
  196. function setupExpandCollapseBtns(output) {
  197. // outerHTML doesnt pass event handlers, so add them all after finished generating the content
  198.  
  199. const btns = output.querySelectorAll('button.expand-collapse-button');
  200.  
  201. for (let i = 0; i < btns.length; i++) {
  202. btns[i].onclick = function(e) {
  203. const btn = e.target;
  204. let element;
  205.  
  206. if (btn.parentNode.className.match(/^html-line\b/)) {
  207. element = btn.parentNode.querySelector('.tag-inner');
  208. }
  209. else if (btn.parentNode.className.match(/^[a-z\-]+-rule\b/)) {
  210. element = btn.parentNode.querySelector('.css-brace-content');
  211. }
  212. else {
  213. console.error('btn', btn);
  214.  
  215. throw new Error('setupExpandCollapseBtns(output) btns[i].onclick showing/collapsing button does not correctly control html or css');
  216. }
  217.  
  218. if (element.className.match(/ collapsed /)) {
  219. element.className = element.className.replace(/ collapsed /, '');
  220. }
  221. else {
  222. element.className += ' collapsed ';
  223. }
  224. };
  225. }
  226. }
  227.  
  228. function getUrlOrigin(link) {
  229. debugAlert('getUrlOrigin(link) link = ' + link);
  230.  
  231. if (typeof URL !== 'function') {
  232. throw new Error('Your browser does not support the URL constructor');
  233. }
  234.  
  235. let url;
  236.  
  237. try {
  238. url = new URL(link);
  239. }
  240. catch(error) {
  241. throw new Error('getUrlOrigin(link) link is not a valid url. link = ' + link);
  242. }
  243. finally {
  244. if (url && !url.origin) {
  245. throw new Error('getUrlOrigin(link) link when turned into a URL does not have an origin. link = ' + link);
  246. }
  247.  
  248. if (url.origin === 'null') {
  249. // some urls like about:blank have a 'null' origin
  250. return null;
  251. }
  252.  
  253. return url.origin;
  254. }
  255. }
  256.  
  257. async function getIframeContent(node, tagNode, indentLevel) {
  258. const iframeSrcOrigin = getUrlOrigin(node.src);
  259. const tagInnerNode = spanNode('tag-inner collapsed ');
  260. let _resolve;
  261.  
  262. function appendIframeOuterHTML(iframeOuterHTML) {
  263. try {
  264. window.removeEventListener('message', receiveParseHTMLOutputMessage);
  265. tagInnerNode.insertAdjacentHTML('beforeend', iframeOuterHTML);
  266. append(tagNode, tagInnerNode);
  267. _resolve();
  268. }
  269. catch(error) {
  270. throw error;
  271. }
  272. }
  273.  
  274. function receiveParseHTMLOutputMessage(event) {
  275. // security does not matter as much for receiving the messages
  276. // at worst its the incorrect html
  277.  
  278. try {
  279. if (event.origin !== iframeSrcOrigin) {
  280. return;
  281. }
  282.  
  283. if (!(event.data && event.data.WEB_INSPECTOR && event.data.WEB_INSPECTOR.parseHTMLOutput)) {
  284. return;
  285. }
  286.  
  287. appendIframeOuterHTML(event.data.WEB_INSPECTOR.parseHTMLOutput);
  288. }
  289. catch(error) {
  290. throw error;
  291. }
  292. }
  293.  
  294. return await new Promise((resolve) => {
  295. _resolve = resolve;
  296.  
  297. if (!iframeSrcOrigin) {
  298. appendIframeOuterHTML(spanNode('userscript-error', 'Error: unable to get iframe content because iframe src has a "null" origin. postMessage does not allow "null" origins to be used to communicate between windows.').outerHTML);
  299. return;
  300. }
  301.  
  302. window.addEventListener('message', receiveParseHTMLOutputMessage);
  303. node.contentWindow.postMessage({WEB_INSPECTOR: {parseHTML: indentLevel + 1}}, iframeSrcOrigin);
  304. });
  305. }
  306.  
  307. function parseHTML_addNewLineSpacing(parent, indentLevel) {
  308. append(parent, brNode());
  309.  
  310. if (indentLevel > 0) {
  311. append(parent, createIndentSpacer(indentLevel));
  312. }
  313.  
  314. const spacing = createSpacer(' ');
  315.  
  316. append(parent, spacing);
  317.  
  318. return spacing;
  319. }
  320.  
  321. function parseHTML_parseElementAttributes(tagNode, node) {
  322. const nodeAttributes = node.attributes;
  323.  
  324. if (nodeAttributes.length) {
  325. const tagAttributesNode = spanNode('tag-attributes');
  326.  
  327. for (const attribute of nodeAttributes) {
  328. append(tagAttributesNode, [htmlSymbol(' '), spanNode('tag-attribute-name', attribute.name), htmlSymbol('='), createTagAttributeValueNode(attribute)]);
  329. }
  330.  
  331. append(tagNode, tagAttributesNode);
  332. }
  333. }
  334.  
  335. function parseHTML_parseElementCloseTag(indentLevel, line, tagNode, tagName) {
  336. if (tagNode.querySelectorAll('.tag, .css, .script').length > 0) {
  337. append(tagNode, brNode());
  338.  
  339. if (indentLevel > 0) {
  340. append(tagNode, createIndentSpacer(indentLevel));
  341. }
  342.  
  343. const spacing = parseHTML_addNewLineSpacing(line, indentLevel);
  344. const expandCollapseBtn = createExpandCollapseBtn(tagNode.querySelector('.tag-inner'));
  345.  
  346. spacing.insertAdjacentElement('afterend', expandCollapseBtn);
  347. expandCollapseBtn.insertAdjacentHTML('afterend', '<pre class="spacer"> </pre>');
  348. append(tagNode, spacing);
  349. }
  350.  
  351. append(tagNode, [htmlSymbol('</'), createTagNameNode(tagName), htmlSymbol('>')]);
  352. }
  353. async function parseHTML(node, parent, indentLevel) {
  354. // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
  355. // using instanceof doesnt always work
  356.  
  357. const isElement = node.nodeType === Node.ELEMENT_NODE;
  358. const isText = node.nodeType === Node.TEXT_NODE;
  359. const isComment = node.nodeType === Node.COMMENT_NODE;
  360. const isDoctype = node.nodeType === Node.DOCUMENT_TYPE_NODE;
  361.  
  362. const line = spanNode('html-line');
  363.  
  364. if (isElement) {
  365. const tagNode = spanNode('tag');
  366. const tagName = node.tagName.toLowerCase();
  367. const elementIsSelfClosing = elementDoesNotNeedToBeClosed(tagName);
  368. const style = getComputedStyle(node);
  369.  
  370. // FIXME isHidden detection isn't fully correct https://developer.mozilla.org/en-US/docs/Web/CSS/visibility
  371. const isHidden = style.display === 'none' || style.visibility !== 'visible';
  372.  
  373. if (isHidden) {
  374. tagNode.className += ' hidden-tag';
  375. }
  376.  
  377. append(tagNode, [htmlSymbol('<'), createTagNameNode(tagName)]);
  378. parseHTML_parseElementAttributes(tagNode, node);
  379. append(tagNode, htmlSymbol((elementIsSelfClosing ? ' /' : '') + '>'));
  380.  
  381. if (tagName === 'iframe') {
  382. if (node.srcdoc && node.src === 'about:srcdoc') {
  383. // TODO support doctype declaration
  384. const tagInnerNode = spanNode('tag-inner collapsed ');
  385. const tmp = el('html');
  386.  
  387. tmp.innerHTML = node.srcdoc;
  388.  
  389. for (const child of tmp.childNodes) {
  390. await parseHTML(child, tagInnerNode, indentLevel + 1).then(() => {}, (error) => {
  391. debugAlert('in parseHTML iframe.srcdoc .then error');
  392. throw error;
  393. });
  394. }
  395.  
  396. append(tagNode, tagInnerNode);
  397. }
  398. else if (node.src) {
  399. await getIframeContent(node, tagNode, indentLevel).then(() => {}, (error) => {
  400. debugAlert('in parseHTML getIframeContent.then error');
  401. throw error;
  402. });
  403. }
  404. }
  405. else if (node.childNodes.length > 0) {
  406. const tagInnerNode = spanNode('tag-inner');
  407.  
  408. if (isHidden || node.childNodes.length > 1) {
  409. // initialise to collapsed, dont make it collapse again unless done so by user
  410. tagInnerNode.className += ' collapsed ';
  411. }
  412.  
  413. switch(tagName) {
  414. case 'style': {
  415. append(tagInnerNode, parseStyle(node.sheet, 0));
  416. break;
  417. }
  418. case 'script': {
  419. append(tagInnerNode, parseScript(node));
  420. break;
  421. }
  422. default: {
  423. for (const child of node.childNodes) {
  424. await parseHTML(child, tagInnerNode, indentLevel + 1).then(() => {}, (error) => {
  425. debugAlert('in parseHTML parseHTML.then error');
  426. throw error;
  427. });
  428. }
  429. }
  430. }
  431.  
  432. append(tagNode, tagInnerNode);
  433. }
  434.  
  435. if (!elementIsSelfClosing) {
  436. parseHTML_parseElementCloseTag(indentLevel, line, tagNode, tagName);
  437. }
  438.  
  439. append(line, tagNode);
  440. }
  441. else if (isText) {
  442. append(line, createPlainTextNode(node));
  443. }
  444. else if (isComment) {
  445. parseHTML_addNewLineSpacing(line, indentLevel);
  446.  
  447. append(line, spanNode('comment', '<!-- ' + node.textContent + '-->'));
  448. }
  449. else if (isDoctype) {
  450. parseHTML_addNewLineSpacing(line, indentLevel);
  451.  
  452. append(line, spanNode('document-type', '<!DOCTYPE ' + node.nodeName + '>'));
  453. }
  454. else {
  455. debugAlert('unexpected node');
  456.  
  457. console.log('isElement', isElement);
  458. window._node = node;
  459. console.error(node);
  460. throw new Error('parseHTML(node, parent, indentLevel) unexpected node');
  461. }
  462.  
  463. append(parent, line);
  464. }
  465.  
  466. function validateIndentLevel(indentLevel) {
  467. if (indentLevel === undefined || isNaN(indentLevel)) {
  468. // any of these + 1 gives NaN
  469. return true;
  470. }
  471.  
  472. if (typeof indentLevel === 'number' && isFinite(indentLevel) && indentLevel >= 0) {
  473. return true;
  474. }
  475.  
  476. throw new Error('validateIndentLevel(indentLevel) indentLevel must be a number >= 0, undefined or NaN');
  477. }
  478.  
  479. function cssSymbol(symbol) {
  480. return spanNode('css-symbol', symbol);
  481. }
  482.  
  483. function atRuleNameNode(name) {
  484. return spanNode('css-at-rule-name', name);
  485. }
  486.  
  487. function cssSelectorText(selectorText) {
  488. // parsing selector text is very complex
  489. // so just leave it as it is for now
  490. // https://www.npmjs.com/package/css-selector-parser
  491. // https://github.com/mdevils/css-selector-parser/blob/master/src/parser.ts
  492.  
  493. return spanNode('css-full-selector', selectorText);
  494. }
  495.  
  496. function previewCSSColorNode(property, value) {
  497. if (!property.match(/(^|-)color$/)) {
  498. // properties with a color as a value are either 'color' or end with '-color'
  499. return;
  500. }
  501.  
  502. if (property.match(/^-/)) {
  503. // could be a css varable which might not be a color value
  504. return;
  505. }
  506.  
  507. if (value.match(/^(-|var\()/i)) {
  508. // cant easily preview variable colors
  509. return;
  510. }
  511.  
  512. if (value.match(/^(currentcolor|inherit|initial|revert|revert-layer|unset)$/i)) {
  513. // cant easily preview global colors
  514. return;
  515. }
  516.  
  517. // the outline adds contrast
  518. // getComputedStyle(preview) gives empty string so use the very new css invert function
  519. // https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/invert
  520.  
  521. const span = spanNode('css-color-preview-container');
  522. const preview = spanNode('css-color-preview');
  523. const previewInner = spanNode();
  524.  
  525. preview.style.outlineColor = value;
  526. previewInner.style.backgroundColor = value;
  527.  
  528. append(preview, previewInner);
  529. append(span, [createSpacer(' '), preview]);
  530.  
  531. return span;
  532. }
  533.  
  534. function parseStyle(cssStyleDeclaration, indentLevel) {
  535. validateIndentLevel(indentLevel);
  536.  
  537. // https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model
  538. // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting#nested_declarations_rule
  539. // https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
  540.  
  541. const style = spanNode('css');
  542.  
  543. function addNewLineSpacing(parent, indentLevel, spacer) {
  544. if (!isFinite(indentLevel)) {
  545. return;
  546. }
  547.  
  548. append(parent, [brNode(), createIndentSpacer(indentLevel), spacer ? spacer : createSpacer(' ')]);
  549. }
  550.  
  551. function parseDeclaration(property, value, indentLevel, isLastDeclaration) {
  552. validateIndentLevel(indentLevel);
  553.  
  554. const decNode = spanNode('css-declaration');
  555. const propNode = spanNode('css-declaration-property', property);
  556. const valNode = spanNode('css-declaration-value', value);
  557. const colorPreviewNode = previewCSSColorNode(property, value);
  558.  
  559. addNewLineSpacing(decNode, indentLevel);
  560.  
  561. append(decNode, [propNode, cssSymbol(': ')]);
  562.  
  563. if (colorPreviewNode) {
  564. append(valNode, colorPreviewNode);
  565. }
  566.  
  567. append(decNode, [valNode, cssSymbol(';')]);
  568.  
  569. if (!isFinite(indentLevel) && !isLastDeclaration) {
  570. append(decNode, cssSymbol(' '));
  571. }
  572.  
  573. return decNode;
  574. }
  575.  
  576. function parseRuleCSSRules(rule, indentLevel) {
  577. if (!rule.cssRules.length) {
  578. return textNode('');
  579. }
  580.  
  581. const ruleRulesNode = spanNode();
  582.  
  583. for (const ruleRule of rule.cssRules) {
  584. parseRule(ruleRulesNode, ruleRule, indentLevel + 1);
  585. }
  586.  
  587. return ruleRulesNode;
  588. }
  589.  
  590. function parseRule(parentElement, rule, indentLevel) {
  591. validateIndentLevel(indentLevel);
  592.  
  593. const ruleNode = spanNode();
  594. const braceLeadingNode = spanNode('css-brace-leading');
  595. const braceContentNode = spanNode('css-brace-content');
  596. const spacer = createSpacer(' ');
  597.  
  598. function insertExpandCollapseBtn() {
  599. spacer.insertAdjacentElement('beforebegin', createExpandCollapseBtn(braceContentNode));
  600. spacer.innerHTML = ' ';
  601. }
  602.  
  603. addNewLineSpacing(ruleNode, indentLevel, spacer);
  604.  
  605. switch (rule.constructor.name) {
  606. case 'CSSStyleRule': {
  607. insertExpandCollapseBtn();
  608.  
  609. ruleNode.className = 'style-rule';
  610.  
  611. append(braceLeadingNode, cssSelectorText(rule.selectorText));
  612. append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
  613. append(braceContentNode, [parseRuleCSSRules(rule, indentLevel), parseStyle(rule.style, indentLevel)]);
  614. addNewLineSpacing(braceContentNode, indentLevel);
  615. append(ruleNode, [braceContentNode, cssSymbol('}')]);
  616.  
  617. break;
  618. }
  619. case 'CSSImportRule': {
  620. ruleNode.className = 'import-rule';
  621.  
  622. const url = spanNode();
  623. const layer = spanNode(0, rule.layerName === null ? '' : (' ' + (rule.layerName ? `layer(${rule.layerName})` : rule.layerName)));
  624. const supports = spanNode(0, rule.supportsText === null ? '' : ` supports(${rule.supportsText})`);
  625.  
  626. append(url, [textNode('url("'), createLink(rule.styleSheet.href, rule.href), textNode('")')]);
  627. append(ruleNode, [atRuleNameNode('@import '), url, layer, supports, spanNode(0, rule.media.mediaText)]);
  628.  
  629. break;
  630. }
  631. case 'CSSMediaRule': {
  632. insertExpandCollapseBtn();
  633.  
  634. ruleNode.className = 'media-rule';
  635.  
  636. append(braceLeadingNode, [atRuleNameNode('@media '), textNode(rule.conditionText)]);
  637. append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
  638. append(braceContentNode, parseRuleCSSRules(rule, indentLevel));
  639. addNewLineSpacing(braceContentNode, indentLevel);
  640. append(ruleNode, [braceContentNode, cssSymbol('}')]);
  641.  
  642. break;
  643. }
  644. case 'CSSFontFaceRule': {
  645. insertExpandCollapseBtn();
  646.  
  647. ruleNode.className = 'font-face-rule';
  648.  
  649. append(braceLeadingNode, atRuleNameNode('@font-face'));
  650. append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
  651. append(braceContentNode, parseStyle(rule.style, indentLevel + 1));
  652. addNewLineSpacing(braceContentNode, indentLevel);
  653. append(ruleNode, [braceContentNode, cssSymbol('}')]);
  654.  
  655. break;
  656. }
  657. case 'CSSPageRule': {
  658. insertExpandCollapseBtn();
  659.  
  660. ruleNode.className = 'page-rule';
  661.  
  662. append(braceLeadingNode, atRuleNameNode('@page'));
  663.  
  664. if (rule.selectorText) {
  665. append(braceLeadingNode, [cssSymbol(' '), cssSelectorText(rule.selectorText)]);
  666. }
  667.  
  668. append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
  669. append(braceContentNode, [parseRuleCSSRules(rule, indentLevel), parseStyle(rule.style, indentLevel + 1)]);
  670. addNewLineSpacing(braceContentNode, indentLevel);
  671. append(ruleNode, [braceContentNode, cssSymbol('}')]);
  672.  
  673. break;
  674. }
  675. case 'CSSNamespaceRule': {
  676. ruleNode.className = 'namespace-rule';
  677.  
  678. append(ruleNode, atRuleNameNode('@namespace '));
  679.  
  680. if (rule.prefix) {
  681. append(ruleNode, rule.prefix + ' ');
  682. }
  683.  
  684. append(rule, [textNode('url("'), createLink(rule.namespaceURI, rule.namespaceURI), textNode('")')]);
  685.  
  686. break;
  687. }
  688. case 'CSSKeyframesRule': {
  689. insertExpandCollapseBtn();
  690.  
  691. ruleNode.className = 'keyframes-rule';
  692.  
  693. append(braceLeadingNode, [atRuleNameNode('@keyframes '), textNode(rule.name)]);
  694. append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
  695. append(braceContentNode, parseRuleCSSRules(rule, indentLevel + 1));
  696. addNewLineSpacing(braceContentNode, indentLevel);
  697. append(ruleNode, [braceContentNode, cssSymbol('}')]);
  698.  
  699. break;
  700. }
  701. case 'CSSKeyframeRule': {
  702. insertExpandCollapseBtn();
  703.  
  704. ruleNode.className = 'keyframe-rule';
  705.  
  706. append(braceLeadingNode, textNode(rule.keyText));
  707. append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
  708. append(braceContentNode, parseStyle(rule.style, indentLevel + 1));
  709. addNewLineSpacing(braceContentNode, indentLevel);
  710. append(ruleNode, [braceContentNode, cssSymbol('}')]);
  711.  
  712. break;
  713. }
  714. case 'CSSCounterStyleRule': {
  715. insertExpandCollapseBtn();
  716.  
  717. ruleNode.className = 'counter-style-rule';
  718.  
  719. append(braceLeadingNode, [atRuleNameNode('@counter-style '), textNode(rule.name)]);
  720. append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
  721. [
  722. ['system', rule.system],
  723. ['symbols', rule.symbols],
  724. ['additiveSymbols', rule.additiveSymbols],
  725. ['negative', rule.negative],
  726. ['prefix', rule.prefix],
  727. ['suffix', rule.suffix],
  728. ['range', rule.range],
  729. ['pad', rule.pad],
  730. ['speak-as', rule.speakAs],
  731. ['fallback', rule.fallback]
  732. ].forEach((declaration) => {
  733. if (declaration[1]) {
  734. append(braceContentNode, parseDeclaration(declaration[0], declaration[1], indentLevel + 1));
  735. }
  736. });
  737. addNewLineSpacing(braceContentNode, indentLevel);
  738. append(ruleNode, [braceContentNode, cssSymbol('}')]);
  739.  
  740. break;
  741. }
  742. case 'CSSSupportsRule': {
  743. insertExpandCollapseBtn();
  744.  
  745. ruleNode.className = 'supports-rule';
  746.  
  747. append(braceLeadingNode, [atRuleNameNode('@supports '), textNode(rule.conditionText)]);
  748. append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
  749. append(braceContentNode, parseRuleCSSRules(rule, indentLevel + 1));
  750. addNewLineSpacing(braceContentNode, indentLevel);
  751. append(ruleNode, [braceContentNode, cssSymbol('}')]);
  752.  
  753. break;
  754. }
  755. case 'CSSFontFeatureValuesRule': {
  756. ruleNode.className = 'font-feature-values-rule';
  757.  
  758. // TODO test this in a browser that supports CSSFontFeatureValuesRule
  759. // not supported in librewolf 133.0-1 on linux
  760.  
  761. // https://developer.mozilla.org/en-US/docs/Web/API/CSSFontFeatureValuesRule
  762. // https://developer.mozilla.org/en-US/docs/Web/CSS/@font-feature-values
  763. // https://developer.mozilla.org/en-US/docs/Web/CSS/@font-feature-values/font-display
  764.  
  765. console.warn(rule);
  766. console.warn('unclear how to parse CSSFontFeatureValuesRule, using unformatted');
  767.  
  768. append(ruleNode, textNode(rule.cssText));
  769.  
  770. // ruleNode.appendChild(textNode('@font-feature-values '));
  771. // ruleNode.appendChild(rule.fontFamily);
  772. // ruleNode.appendChild(cssSymbol(' {'));
  773.  
  774. // who knows
  775.  
  776. // ruleNode.appendChild(cssSymbol('}'));
  777.  
  778. break;
  779. }
  780. case 'CSSFontPaletteValuesRule': {
  781. ruleNode.className = 'font-palette-values-rule';
  782.  
  783. // TODO test this in a browser that supports CSSFontPaletteValuesRule
  784. // not supported in librewolf 133.0-1 on linux
  785.  
  786. console.warn(rule);
  787. console.warn('unclear how to parse CSSFontFeatureValuesRule, using unformatted');
  788.  
  789. append(ruleNode, textNode(rule.cssText));
  790.  
  791. // ruleNode.appendChild(textNode('@font-palette-values '));
  792. // ruleNode.appendChild(rule.name);
  793. // ruleNode.appendChild(cssSymbol(' {'));
  794.  
  795. // ruleNode.appendChild(parseDeclaration('font-family', rule.fontFamily, indentLevel + 1));
  796. // ruleNode.appendChild(parseDeclaration('base-palette', rule.basePalette, indentLevel + 1));
  797.  
  798. // no idea how this will behave
  799. // https://developer.mozilla.org/en-US/docs/Web/API/CSSFontPaletteValuesRule
  800. // https://developer.mozilla.org/en-US/docs/Web/API/CSSFontPaletteValuesRule/overrideColors
  801. // may need special treatment for formatting
  802. // ruleNode.appendChild(parseDeclaration('override-colors', rule.overrideColors, indentLevel + 1, true))
  803.  
  804. // ruleNode.appendChild(cssSymbol('}'));
  805.  
  806. break;
  807. }
  808. case 'CSSLayerBlockRule': {
  809. insertExpandCollapseBtn();
  810.  
  811. ruleNode.className = 'layer-block-rule';
  812.  
  813. append(braceLeadingNode, [atRuleNameNode('@layer '), textNode(rule.name)]);
  814. append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
  815. append(braceContentNode, parseRuleCSSRules(rule, indentLevel + 1));
  816. addNewLineSpacing(braceContentNode, indentLevel);
  817. append(ruleNode, [braceContentNode, cssSymbol('}')]);
  818.  
  819. break;
  820. }
  821. case 'CSSLayerBlockRule': {
  822. ruleNode.className = 'layer-block-rule';
  823.  
  824. append(ruleNode, atRuleNameNode('@layer '));
  825.  
  826. rule.nameList.forEach((name, i) => {
  827. append(ruleNode, textNode(name));
  828.  
  829. if (i + 1 < this.length) {
  830. append(ruleNode, cssSymbol(', '));
  831. }
  832. });
  833.  
  834. append(ruleNode, cssSymbol(';'));
  835.  
  836. break;
  837. }
  838. case 'CSSPropertyRule': {
  839. insertExpandCollapseBtn();
  840.  
  841. ruleNode.className = 'property-rule';
  842.  
  843. append(braceLeadingNode, [atRuleNameNode('@property '), textNode(rule.name)]);
  844. append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
  845. append(braceContentNode, [parseDeclaration('syntax', rule.syntax, indentLevel + 1), parseDeclaration('inherits', '' + rule.inherits, indentLevel + 1), parseDeclaration('initial-value', rule.initialValue, indentLevel + 1, true)]);
  846. addNewLineSpacing(braceContentNode, indentLevel);
  847. append(ruleNode, [braceContentNode, cssSymbol('}')]);
  848.  
  849. break;
  850. }
  851. default: {
  852. ruleNode.className = 'unexpected-rule';
  853.  
  854. // should not need to explicitly handle CSSGroupingRule because other rule types inherit from it
  855. // should not need to explicitly handle CSSNestedDeclarations because other rules pass a cssStyleDeclaration
  856.  
  857. console.warn(rule);
  858. console.warn('unexpected css rule type, using unformatted');
  859.  
  860. append(ruleNode, textNode(rule.cssText));
  861.  
  862. break;
  863. }
  864. }
  865.  
  866. parentElement.appendChild(ruleNode);
  867. }
  868.  
  869. if (cssStyleDeclaration instanceof CSSStyleSheet) {
  870. const ruleRulesNode = spanNode();
  871.  
  872. for (const rule of cssStyleDeclaration.cssRules) {
  873. parseRule(ruleRulesNode, rule, indentLevel);
  874. }
  875.  
  876. append(style, ruleRulesNode);
  877. }
  878. else {
  879. // previously use a for of before to filter out all style declarations
  880. // need to know if there is a next declaration for formatting purposes
  881. // element.style has numbered indexes for styles actually declared on the element
  882.  
  883. for (let i = 0; ; ) {
  884. const prop = cssStyleDeclaration[i];
  885.  
  886. if (!prop) {
  887. break;
  888. }
  889.  
  890. i++;
  891.  
  892. const hasNext = !!cssStyleDeclaration[i];
  893.  
  894. append(style, parseDeclaration(prop, cssStyleDeclaration.getPropertyValue(prop), indentLevel + 1, !hasNext));
  895. }
  896. }
  897.  
  898. return style;
  899. }
  900.  
  901. function parseScript(node) {
  902. // TODO formatting, highlighting
  903.  
  904. return spanNode('script', node.textContent.trim());
  905. }
  906.  
  907. function hideCollapsed() {
  908. let a = '.collapsed';
  909. let b = a;
  910.  
  911. ['br', '.spacer', '.spacer'].forEach((c) => {
  912. b += ' + ' + c;
  913. a += ', ' + b;
  914. });
  915.  
  916. return a + ' {\n\tdisplay: none;\n}';
  917. }
  918.  
  919. function color(selector, color) {
  920. return `${selector} {\n\tcolor: ${color};\n}`;
  921. }
  922.  
  923. function getStyle() {
  924. return `body {
  925. margin: 1em;
  926. padding: 0;
  927. display: block;
  928. font-family: monospace;
  929. font-size: 1em;
  930. line-height: 1.2em;
  931. tab-size: 2;
  932. color: #cbcbc7;
  933. background: #232327;
  934. word-break: break-word;
  935. }
  936.  
  937. .spacer {
  938. margin: 0;
  939. padding: 0;
  940. border: 0;
  941. outline: 0;
  942. display: inline-block;
  943. }
  944.  
  945. ${hideCollapsed()}
  946.  
  947. .expand-collapse-button {
  948. margin: 0;
  949. padding: 0;
  950. border: 0;
  951. width: fit-content;
  952. cursor: pointer;
  953. background: inherit;
  954. color: #88888a;
  955. }
  956.  
  957. .expand-collapse-button:has(+ .spacer + .tag > .collapsed), .expand-collapse-button:has(+ .spacer + .css-brace-leading + .css-symbol + .css-brace-content.collapsed) {
  958. rotate: 270deg;
  959. }
  960.  
  961. ${color('.userscript-error', '#ff0000')}
  962.  
  963. .document-type {
  964. font-style: italic;
  965. }
  966.  
  967. ${color('.html-symbol', '#7c7c7e')}
  968.  
  969. ${color('.document-type', '#72baf9')}
  970.  
  971. ${color('.comment', '#90EE90')}
  972.  
  973. ${color('.tag-name', '#72baf9')}
  974.  
  975. ${color('.tag-attribute-name', '#fb7be5')}
  976.  
  977. ${color('.tag-attribute-value', '#9b79d4')}
  978.  
  979. .tag-attribute-value a {
  980. color: inherit;
  981. text-decoration: underline;
  982. }
  983.  
  984. ${color('.tag.hidden-tag .html-symbol', '#6e6e6e')}
  985.  
  986. ${color('.tag.hidden-tag .tag-name', '#929294')}
  987.  
  988. ${color('.tag.hidden-tag .tag-attribute-name', '#676768')}
  989.  
  990. ${color('.tag.hidden-tag .tag-attribute-value', '#939394')}
  991.  
  992. ${color('.css-symbol', '#7c7c7e')}
  993.  
  994. ${color('.css-at-rule-name', '#72baf9')}
  995.  
  996. ${color('.css-declaration-property', '#80d36f')}
  997.  
  998. ${color('.css-declaration-value', '#fb7be5')}
  999.  
  1000. .css-color-preview {
  1001. display: inline-block;
  1002. width: 1em;
  1003. height: 1em;
  1004. outline-width: 2px;
  1005. outline-style: solid;
  1006. filter: invert(100%);
  1007. }`;
  1008. }
  1009.  
  1010. async function main(outputWindow) {
  1011. const meta1 = el('meta');
  1012. const meta2 = el('meta');
  1013. const title = el('title');
  1014. const style = el('style');
  1015. const output = el('span');
  1016.  
  1017. meta1.setAttribute('charset', 'utf-8');
  1018.  
  1019. meta2.setAttribute('name', 'viewport');
  1020. meta2.setAttribute('content', 'width=device-width, initial-scale=1, minimum-scale=1');
  1021.  
  1022. title.innerHTML = 'Web Inspector - ' + document.title;
  1023.  
  1024. style.innerHTML = getStyle();
  1025.  
  1026. for (const node of document.childNodes) {
  1027. await parseHTML(node, output, 0).then(() => {}, (error) => {
  1028. debugAlert('in main parseHTML.then error');
  1029. throw error;
  1030. });
  1031. }
  1032.  
  1033. if (output.firstElementChild.tagName === 'BR') {
  1034. // remove unnecessary spacing at top
  1035. output.firstElementChild.remove();
  1036. }
  1037.  
  1038. setupExpandCollapseBtns(output);
  1039.  
  1040. outputWindow.document.write('<!DOCTYPE html><html><head></head><body></body></html>');
  1041.  
  1042. append(outputWindow.document.head, meta1);
  1043. append(outputWindow.document.head, meta2);
  1044. append(outputWindow.document.head, title);
  1045. append(outputWindow.document.head, style);
  1046. append(outputWindow.document.body, output);
  1047. }
  1048.  
  1049. async function receiveParseHTMLMessage(event) {
  1050. try {
  1051. // unable to access iframe content, so wait for a message from the top window
  1052. // then pass the output element
  1053.  
  1054. if (event.source !== top) {
  1055. // this check should reduce security issues
  1056. return;
  1057. }
  1058.  
  1059. if (!(event.data && event.data.WEB_INSPECTOR && event.data.WEB_INSPECTOR.parseHTML)) {
  1060. // make sure the instruction exists
  1061. return;
  1062. }
  1063.  
  1064. const indentLevel = parseInt(event.data.WEB_INSPECTOR.parseHTML);
  1065.  
  1066. if (!(isFinite(indentLevel) && indentLevel > 0)) {
  1067. return;
  1068. }
  1069.  
  1070. window.removeEventListener('message', receiveParseHTMLMessage);
  1071.  
  1072. const output = spanNode();
  1073.  
  1074. for (const node of document.childNodes) {
  1075. await parseHTML(node, output, indentLevel).then(() => {}, (error) => {
  1076. debugAlert('in receiveParseHTMLMessage parseHTML error');
  1077. throw error;
  1078. });
  1079. }
  1080.  
  1081. event.source.postMessage({WEB_INSPECTOR: {parseHTMLOutput: output.outerHTML}}, event.origin);
  1082. }
  1083. catch(err) {
  1084. throw err;
  1085. }
  1086. }
  1087.  
  1088. function isDefined(variable) {
  1089. return typeof variable !== 'undefined';
  1090. }
  1091.  
  1092. function extractErrorInfo(error) {
  1093. let str = error.name + ': ' + error.message;
  1094.  
  1095. if (isDefined(error.fileName) && isDefined(error.lineNumber) && isDefined(error.columnNumber)) {
  1096. str += ' in ' + error.fileName + ' ' + error.lineNumber + ':' + error.columnNumber;
  1097. }
  1098.  
  1099. if (isDefined(error.stack)) {
  1100. str += '\n' + error.stack;
  1101. }
  1102.  
  1103. return str;
  1104. }
  1105.  
  1106. function errorDetected(error) {
  1107. return prompt('Error while using Web Inspector:', extractErrorInfo(error));
  1108. }
  1109.  
  1110. if (self !== top) {
  1111. window.addEventListener('message', receiveParseHTMLMessage);
  1112.  
  1113. return;
  1114. }
  1115.  
  1116. window.WEB_INSPECTOR = function() {
  1117. // try to open in a new window
  1118. // if popups are blocked, replace the current webpage with the web inspector
  1119.  
  1120. const outputWindow = open('about:blank') || window;
  1121.  
  1122. main(outputWindow).then(() => {}, (error) => {
  1123. debugAlert('in window.WEB_INSPECTOR main error');
  1124. errorDetected(error);
  1125. });
  1126.  
  1127. outputWindow.onload = function() {
  1128. main(outputWindow).then(() => {}, (error) => {
  1129. debugAlert('in window.WEB_INSPECTOR outputWindow main error');
  1130. errorDetected(error);
  1131. });
  1132. };
  1133. };
  1134. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址