Web Inspector

Allows you to inspect web pages

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

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

QingJ © 2025

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