Web Inspector

Allows you to inspect web pages

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

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

QingJ © 2025

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