Web Inspector

Allows you to inspect web pages

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

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

QingJ © 2025

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