Web Inspector

Allows you to inspect web pages

当前为 2024-12-18 提交的版本,查看 最新版本

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

QingJ © 2025

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