Web Inspector

Allows you to inspect web pages

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

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

QingJ © 2025

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