Web Inspector

Allows you to inspect web pages

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

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

QingJ © 2025

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