// ==UserScript==
// @name Web Inspector
// @namespace http://tampermonkey.net/
// @version 1.1.4
// @description Allows you to inspect web pages
// @author https://gf.qytechs.cn/en/users/85040-dan-wl-danwl
// @license MIT
// @match *://*/*
// @run-at document-start
// @grant none
// ==/UserScript==
// MIT License
// Copyright(c) 2024 DanWL
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// This userscript defines a function on the web page you visit
// For the function to do anything, use this bookmarklet:
// javascript:(function(){WEB_INSPECTOR();})();
// A bookmarklet is essentially a regular browser bookmark/favorite but with a JavaScript url
// jshint esnext: false, esversion: 8
(() => {
function debugAlert(str) {
const debug = false;
if (debug) {
alert(str);
}
}
function el(tagName, className) {
const ret = document.createElement(tagName);
if (className) {
ret.className = className;
}
return ret;
}
function spanNode(className, innerTextOrText) {
const span = el('span', className);
if (typeof innerTextOrText !== 'undefined') {
if (typeof innerTextOrText === 'string') {
innerTextOrText = textNode(innerTextOrText);
}
if (typeof innerTextOrText === 'object' && isFinite(innerTextOrText.nodeType) && innerTextOrText.nodeType === Node.TEXT_NODE) {
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
// using instanceof doesnt always work
append(span, innerTextOrText);
}
else {
throw new Error('spanNode(className, innerTextOrText) innerTextOrText is ' + innerTextOrText);
}
}
return span;
}
function brNode() {
return el('br');
}
function textNode(txt) {
return document.createTextNode(txt);
}
function append(parent, nodes) {
// enables much better minimising
if (!Array.isArray(nodes)) {
nodes = [nodes];
}
nodes.forEach((node) => {
parent.appendChild(node);
});
}
function htmlSymbol(symbol) {
return spanNode('html-symbol', symbol);
}
function createTagNameNode(tagName) {
return spanNode('tag-name', tagName);
}
function createTagAttributeValueNode(attribute) {
const isLink = ['href', 'src'].includes(attribute.name);
const isStyle = attribute.name === 'style';
const span = spanNode('tag-attribute-value');
if (isLink) {
append(span, [textNode('"'), createLink(attribute.value, attribute.value), textNode('"')]);
}
else if (isStyle) {
append(span, [textNode('"'), parseStyle(attribute.ownerElement.style), textNode('"')]);
}
else {
append(span, textNode(JSON.stringify(attribute.value)));
}
return span;
}
function createPlainTextNode(node) {
// TODO html entities highlighting
return spanNode('text', textNode(applyHTMLWhitespaceRules(node.textContent)));
}
function elementDoesNotNeedToBeClosed(tagName) {
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element
return ['base', 'link', 'meta', 'hr', 'br', 'wbr', 'area', 'img', 'track', 'embed', 'source', 'input'].includes(tagName);
}
function applyHTMLWhitespaceRules(text) {
// https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace
return text
.replace(/^[\t ]+/mg, '')
.replace(/[\t\r\n]/g, ' ')
.replace(/ {2,}/g, ' ');
}
function createSpacer(spacing) {
const spacer = el('pre', 'spacer');
spacer.innerHTML = spacing;
return spacer;
}
function createIndentSpacer(indentLevel) {
const space = '\t';
let spacing = '';
while (indentLevel > 0) {
spacing += space;
indentLevel--;
}
const spacer = createSpacer(spacing);
spacer.className += ' indentation';
return spacer;
}
function createLink(url, displayText) {
const link = el('a');
link.href = url;
link.target = '_blank';
append(link, textNode(displayText));
return link;
}
function createExpandCollapseBtn(element) {
// https://www.amp-what.com ▼
const btn = el('button', 'expand-collapse-button');
btn.innerHTML = '▼';
return btn;
}
function setupExpandCollapseBtns(output) {
// outerHTML doesnt pass event handlers, so add them all after finished generating the content
const btns = output.querySelectorAll('button.expand-collapse-button');
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = function(e) {
const btn = e.target;
let element;
if (btn.parentNode.className.match(/^html-line\b/)) {
element = btn.parentNode.querySelector('.tag-inner');
}
else if (btn.parentNode.className.match(/^[a-z\-]+-rule\b/)) {
element = btn.parentNode.querySelector('.css-brace-content');
}
else {
console.error('btn', btn);
throw new Error('setupExpandCollapseBtns(output) btns[i].onclick showing/collapsing button does not correctly control html or css');
}
if (element.className.match(/ collapsed /)) {
element.className = element.className.replace(/ collapsed /, '');
}
else {
element.className += ' collapsed ';
}
};
}
}
function getUrlOrigin(link) {
debugAlert('getUrlOrigin(link) link = ' + link);
if (typeof URL !== 'function') {
throw new Error('Your browser does not support the URL constructor');
}
let url;
try {
url = new URL(link);
}
catch(error) {
throw new Error('getUrlOrigin(link) link is not a valid url. link = ' + link);
}
finally {
if (url && !url.origin) {
throw new Error('getUrlOrigin(link) link when turned into a URL does not have an origin. link = ' + link);
}
if (url.origin === 'null') {
// some urls like about:blank have a 'null' origin
return null;
}
return url.origin;
}
}
async function getIframeContent(node, tagNode, indentLevel) {
const iframeSrcOrigin = getUrlOrigin(node.src);
const tagInnerNode = spanNode('tag-inner collapsed ');
let _resolve;
function appendIframeOuterHTML(iframeOuterHTML) {
try {
window.removeEventListener('message', receiveParseHTMLOutputMessage);
tagInnerNode.insertAdjacentHTML('beforeend', iframeOuterHTML);
append(tagNode, tagInnerNode);
_resolve();
}
catch(error) {
throw error;
}
}
function receiveParseHTMLOutputMessage(event) {
// security does not matter as much for receiving the messages
// at worst its the incorrect html
try {
if (event.origin !== iframeSrcOrigin) {
return;
}
if (!(event.data && event.data.WEB_INSPECTOR && event.data.WEB_INSPECTOR.parseHTMLOutput)) {
return;
}
appendIframeOuterHTML(event.data.WEB_INSPECTOR.parseHTMLOutput);
}
catch(error) {
throw error;
}
}
return await new Promise((resolve) => {
_resolve = resolve;
if (!iframeSrcOrigin) {
appendIframeOuterHTML(spanNode('userscript-error', 'Error: unable to get iframe content because iframe src has a "null" origin. postMessage does not allow "null" origins to be used to communicate between windows.').outerHTML);
return;
}
window.addEventListener('message', receiveParseHTMLOutputMessage);
node.contentWindow.postMessage({WEB_INSPECTOR: {parseHTML: indentLevel + 1}}, iframeSrcOrigin);
});
}
function parseHTML_addNewLineSpacing(parent, indentLevel) {
append(parent, brNode());
if (indentLevel > 0) {
append(parent, createIndentSpacer(indentLevel));
}
const spacing = createSpacer(' ');
append(parent, spacing);
return spacing;
}
function parseHTML_parseElementAttributes(tagNode, node) {
const nodeAttributes = node.attributes;
if (nodeAttributes.length) {
const tagAttributesNode = spanNode('tag-attributes');
for (const attribute of nodeAttributes) {
append(tagAttributesNode, [htmlSymbol(' '), spanNode('tag-attribute-name', attribute.name), htmlSymbol('='), createTagAttributeValueNode(attribute)]);
}
append(tagNode, tagAttributesNode);
}
}
function parseHTML_parseElementCloseTag(indentLevel, line, tagNode, tagName) {
if (tagNode.querySelectorAll('.tag, .css, .script').length > 0) {
append(tagNode, brNode());
if (indentLevel > 0) {
append(tagNode, createIndentSpacer(indentLevel));
}
const spacing = parseHTML_addNewLineSpacing(line, indentLevel);
const expandCollapseBtn = createExpandCollapseBtn(tagNode.querySelector('.tag-inner'));
spacing.insertAdjacentElement('afterend', expandCollapseBtn);
expandCollapseBtn.insertAdjacentHTML('afterend', '<pre class="spacer"> </pre>');
append(tagNode, spacing);
}
append(tagNode, [htmlSymbol('</'), createTagNameNode(tagName), htmlSymbol('>')]);
}
async function parseHTML(node, parent, indentLevel) {
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
// using instanceof doesnt always work
const isElement = node.nodeType === Node.ELEMENT_NODE;
const isText = node.nodeType === Node.TEXT_NODE;
const isComment = node.nodeType === Node.COMMENT_NODE;
const isDoctype = node.nodeType === Node.DOCUMENT_TYPE_NODE;
const line = spanNode('html-line');
if (isElement) {
const tagNode = spanNode('tag');
const tagName = node.tagName.toLowerCase();
const elementIsSelfClosing = elementDoesNotNeedToBeClosed(tagName);
const style = getComputedStyle(node);
// FIXME isHidden detection isn't fully correct https://developer.mozilla.org/en-US/docs/Web/CSS/visibility
const isHidden = style.display === 'none' || style.visibility !== 'visible';
if (isHidden) {
tagNode.className += ' hidden-tag';
}
append(tagNode, [htmlSymbol('<'), createTagNameNode(tagName)]);
parseHTML_parseElementAttributes(tagNode, node);
append(tagNode, htmlSymbol((elementIsSelfClosing ? ' /' : '') + '>'));
if (tagName === 'iframe') {
if (node.srcdoc && node.src === 'about:srcdoc') {
// TODO support doctype declaration
const tagInnerNode = spanNode('tag-inner collapsed ');
const tmp = el('html');
tmp.innerHTML = node.srcdoc;
for (const child of tmp.childNodes) {
await parseHTML(child, tagInnerNode, indentLevel + 1).then(() => {}, (error) => {
debugAlert('in parseHTML iframe.srcdoc .then error');
throw error;
});
}
append(tagNode, tagInnerNode);
}
else if (node.src) {
await getIframeContent(node, tagNode, indentLevel).then(() => {}, (error) => {
debugAlert('in parseHTML getIframeContent.then error');
throw error;
});
}
}
else if (node.childNodes.length > 0) {
const tagInnerNode = spanNode('tag-inner');
if (isHidden || node.childNodes.length > 1) {
// initialise to collapsed, dont make it collapse again unless done so by user
tagInnerNode.className += ' collapsed ';
}
switch(tagName) {
case 'style': {
append(tagInnerNode, parseStyle(node.sheet, 0));
break;
}
case 'script': {
append(tagInnerNode, parseScript(node));
break;
}
default: {
for (const child of node.childNodes) {
await parseHTML(child, tagInnerNode, indentLevel + 1).then(() => {}, (error) => {
debugAlert('in parseHTML parseHTML.then error');
throw error;
});
}
}
}
append(tagNode, tagInnerNode);
}
if (!elementIsSelfClosing) {
parseHTML_parseElementCloseTag(indentLevel, line, tagNode, tagName);
}
append(line, tagNode);
}
else if (isText) {
append(line, createPlainTextNode(node));
}
else if (isComment) {
parseHTML_addNewLineSpacing(line, indentLevel);
append(line, spanNode('comment', '<!-- ' + node.textContent + '-->'));
}
else if (isDoctype) {
parseHTML_addNewLineSpacing(line, indentLevel);
append(line, spanNode('document-type', '<!DOCTYPE ' + node.nodeName + '>'));
}
else {
debugAlert('unexpected node');
console.log('isElement', isElement);
window._node = node;
console.error(node);
throw new Error('parseHTML(node, parent, indentLevel) unexpected node');
}
append(parent, line);
}
function validateIndentLevel(indentLevel) {
if (indentLevel === undefined || isNaN(indentLevel)) {
// any of these + 1 gives NaN
return true;
}
if (typeof indentLevel === 'number' && isFinite(indentLevel) && indentLevel >= 0) {
return true;
}
throw new Error('validateIndentLevel(indentLevel) indentLevel must be a number >= 0, undefined or NaN');
}
function cssSymbol(symbol) {
return spanNode('css-symbol', symbol);
}
function atRuleNameNode(name) {
return spanNode('css-at-rule-name', name);
}
function cssSelectorText(selectorText) {
// parsing selector text is very complex
// so just leave it as it is for now
// https://www.npmjs.com/package/css-selector-parser
// https://github.com/mdevils/css-selector-parser/blob/master/src/parser.ts
return spanNode('css-full-selector', selectorText);
}
function previewCSSColorNode(property, value) {
if (!property.match(/(^|-)color$/)) {
// properties with a color as a value are either 'color' or end with '-color'
return;
}
if (property.match(/^-/)) {
// could be a css varable which might not be a color value
return;
}
if (value.match(/^(-|var\()/i)) {
// cant easily preview variable colors
return;
}
if (value.match(/^(currentcolor|inherit|initial|revert|revert-layer|unset)$/i)) {
// cant easily preview global colors
return;
}
// the outline adds contrast
// getComputedStyle(preview) gives empty string so use the very new css invert function
// https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/invert
const span = spanNode('css-color-preview-container');
const preview = spanNode('css-color-preview');
const previewInner = spanNode();
preview.style.outlineColor = value;
previewInner.style.backgroundColor = value;
append(preview, previewInner);
append(span, [createSpacer(' '), preview]);
return span;
}
function parseStyle(cssStyleDeclaration, indentLevel) {
validateIndentLevel(indentLevel);
// https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model
// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting#nested_declarations_rule
// https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
const style = spanNode('css');
function addNewLineSpacing(parent, indentLevel, spacer) {
if (!isFinite(indentLevel)) {
return;
}
append(parent, [brNode(), createIndentSpacer(indentLevel), spacer ? spacer : createSpacer(' ')]);
}
function parseDeclaration(property, value, indentLevel, isLastDeclaration) {
validateIndentLevel(indentLevel);
const decNode = spanNode('css-declaration');
const propNode = spanNode('css-declaration-property', property);
const valNode = spanNode('css-declaration-value', value);
const colorPreviewNode = previewCSSColorNode(property, value);
addNewLineSpacing(decNode, indentLevel);
append(decNode, [propNode, cssSymbol(': ')]);
if (colorPreviewNode) {
append(valNode, colorPreviewNode);
}
append(decNode, [valNode, cssSymbol(';')]);
if (!isFinite(indentLevel) && !isLastDeclaration) {
append(decNode, cssSymbol(' '));
}
return decNode;
}
function parseRuleCSSRules(rule, indentLevel) {
if (!rule.cssRules.length) {
return textNode('');
}
const ruleRulesNode = spanNode();
for (const ruleRule of rule.cssRules) {
parseRule(ruleRulesNode, ruleRule, indentLevel + 1);
}
return ruleRulesNode;
}
function parseRule(parentElement, rule, indentLevel) {
validateIndentLevel(indentLevel);
const ruleNode = spanNode();
const braceLeadingNode = spanNode('css-brace-leading');
const braceContentNode = spanNode('css-brace-content');
const spacer = createSpacer(' ');
function insertExpandCollapseBtn() {
spacer.insertAdjacentElement('beforebegin', createExpandCollapseBtn(braceContentNode));
spacer.innerHTML = ' ';
}
addNewLineSpacing(ruleNode, indentLevel, spacer);
switch (rule.constructor.name) {
case 'CSSStyleRule': {
insertExpandCollapseBtn();
ruleNode.className = 'style-rule';
append(braceLeadingNode, cssSelectorText(rule.selectorText));
append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
append(braceContentNode, [parseRuleCSSRules(rule, indentLevel), parseStyle(rule.style, indentLevel)]);
addNewLineSpacing(braceContentNode, indentLevel);
append(ruleNode, [braceContentNode, cssSymbol('}')]);
break;
}
case 'CSSImportRule': {
ruleNode.className = 'import-rule';
const url = spanNode();
const layer = spanNode(0, rule.layerName === null ? '' : (' ' + (rule.layerName ? `layer(${rule.layerName})` : rule.layerName)));
const supports = spanNode(0, rule.supportsText === null ? '' : ` supports(${rule.supportsText})`);
append(url, [textNode('url("'), createLink(rule.styleSheet.href, rule.href), textNode('")')]);
append(ruleNode, [atRuleNameNode('@import '), url, layer, supports, spanNode(0, rule.media.mediaText)]);
break;
}
case 'CSSMediaRule': {
insertExpandCollapseBtn();
ruleNode.className = 'media-rule';
append(braceLeadingNode, [atRuleNameNode('@media '), textNode(rule.conditionText)]);
append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
append(braceContentNode, parseRuleCSSRules(rule, indentLevel));
addNewLineSpacing(braceContentNode, indentLevel);
append(ruleNode, [braceContentNode, cssSymbol('}')]);
break;
}
case 'CSSFontFaceRule': {
insertExpandCollapseBtn();
ruleNode.className = 'font-face-rule';
append(braceLeadingNode, atRuleNameNode('@font-face'));
append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
append(braceContentNode, parseStyle(rule.style, indentLevel + 1));
addNewLineSpacing(braceContentNode, indentLevel);
append(ruleNode, [braceContentNode, cssSymbol('}')]);
break;
}
case 'CSSPageRule': {
insertExpandCollapseBtn();
ruleNode.className = 'page-rule';
append(braceLeadingNode, atRuleNameNode('@page'));
if (rule.selectorText) {
append(braceLeadingNode, [cssSymbol(' '), cssSelectorText(rule.selectorText)]);
}
append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
append(braceContentNode, [parseRuleCSSRules(rule, indentLevel), parseStyle(rule.style, indentLevel + 1)]);
addNewLineSpacing(braceContentNode, indentLevel);
append(ruleNode, [braceContentNode, cssSymbol('}')]);
break;
}
case 'CSSNamespaceRule': {
ruleNode.className = 'namespace-rule';
append(ruleNode, atRuleNameNode('@namespace '));
if (rule.prefix) {
append(ruleNode, rule.prefix + ' ');
}
append(rule, [textNode('url("'), createLink(rule.namespaceURI, rule.namespaceURI), textNode('")')]);
break;
}
case 'CSSKeyframesRule': {
insertExpandCollapseBtn();
ruleNode.className = 'keyframes-rule';
append(braceLeadingNode, [atRuleNameNode('@keyframes '), textNode(rule.name)]);
append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
append(braceContentNode, parseRuleCSSRules(rule, indentLevel + 1));
addNewLineSpacing(braceContentNode, indentLevel);
append(ruleNode, [braceContentNode, cssSymbol('}')]);
break;
}
case 'CSSKeyframeRule': {
insertExpandCollapseBtn();
ruleNode.className = 'keyframe-rule';
append(braceLeadingNode, textNode(rule.keyText));
append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
append(braceContentNode, parseStyle(rule.style, indentLevel + 1));
addNewLineSpacing(braceContentNode, indentLevel);
append(ruleNode, [braceContentNode, cssSymbol('}')]);
break;
}
case 'CSSCounterStyleRule': {
insertExpandCollapseBtn();
ruleNode.className = 'counter-style-rule';
append(braceLeadingNode, [atRuleNameNode('@counter-style '), textNode(rule.name)]);
append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
[
['system', rule.system],
['symbols', rule.symbols],
['additiveSymbols', rule.additiveSymbols],
['negative', rule.negative],
['prefix', rule.prefix],
['suffix', rule.suffix],
['range', rule.range],
['pad', rule.pad],
['speak-as', rule.speakAs],
['fallback', rule.fallback]
].forEach((declaration) => {
if (declaration[1]) {
append(braceContentNode, parseDeclaration(declaration[0], declaration[1], indentLevel + 1));
}
});
addNewLineSpacing(braceContentNode, indentLevel);
append(ruleNode, [braceContentNode, cssSymbol('}')]);
break;
}
case 'CSSSupportsRule': {
insertExpandCollapseBtn();
ruleNode.className = 'supports-rule';
append(braceLeadingNode, [atRuleNameNode('@supports '), textNode(rule.conditionText)]);
append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
append(braceContentNode, parseRuleCSSRules(rule, indentLevel + 1));
addNewLineSpacing(braceContentNode, indentLevel);
append(ruleNode, [braceContentNode, cssSymbol('}')]);
break;
}
case 'CSSFontFeatureValuesRule': {
ruleNode.className = 'font-feature-values-rule';
// TODO test this in a browser that supports CSSFontFeatureValuesRule
// not supported in librewolf 133.0-1 on linux
// https://developer.mozilla.org/en-US/docs/Web/API/CSSFontFeatureValuesRule
// https://developer.mozilla.org/en-US/docs/Web/CSS/@font-feature-values
// https://developer.mozilla.org/en-US/docs/Web/CSS/@font-feature-values/font-display
console.warn(rule);
console.warn('unclear how to parse CSSFontFeatureValuesRule, using unformatted');
append(ruleNode, textNode(rule.cssText));
// ruleNode.appendChild(textNode('@font-feature-values '));
// ruleNode.appendChild(rule.fontFamily);
// ruleNode.appendChild(cssSymbol(' {'));
// who knows
// ruleNode.appendChild(cssSymbol('}'));
break;
}
case 'CSSFontPaletteValuesRule': {
ruleNode.className = 'font-palette-values-rule';
// TODO test this in a browser that supports CSSFontPaletteValuesRule
// not supported in librewolf 133.0-1 on linux
console.warn(rule);
console.warn('unclear how to parse CSSFontFeatureValuesRule, using unformatted');
append(ruleNode, textNode(rule.cssText));
// ruleNode.appendChild(textNode('@font-palette-values '));
// ruleNode.appendChild(rule.name);
// ruleNode.appendChild(cssSymbol(' {'));
// ruleNode.appendChild(parseDeclaration('font-family', rule.fontFamily, indentLevel + 1));
// ruleNode.appendChild(parseDeclaration('base-palette', rule.basePalette, indentLevel + 1));
// no idea how this will behave
// https://developer.mozilla.org/en-US/docs/Web/API/CSSFontPaletteValuesRule
// https://developer.mozilla.org/en-US/docs/Web/API/CSSFontPaletteValuesRule/overrideColors
// may need special treatment for formatting
// ruleNode.appendChild(parseDeclaration('override-colors', rule.overrideColors, indentLevel + 1, true))
// ruleNode.appendChild(cssSymbol('}'));
break;
}
case 'CSSLayerBlockRule': {
insertExpandCollapseBtn();
ruleNode.className = 'layer-block-rule';
append(braceLeadingNode, [atRuleNameNode('@layer '), textNode(rule.name)]);
append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
append(braceContentNode, parseRuleCSSRules(rule, indentLevel + 1));
addNewLineSpacing(braceContentNode, indentLevel);
append(ruleNode, [braceContentNode, cssSymbol('}')]);
break;
}
case 'CSSLayerBlockRule': {
ruleNode.className = 'layer-block-rule';
append(ruleNode, atRuleNameNode('@layer '));
rule.nameList.forEach((name, i) => {
append(ruleNode, textNode(name));
if (i + 1 < this.length) {
append(ruleNode, cssSymbol(', '));
}
});
append(ruleNode, cssSymbol(';'));
break;
}
case 'CSSPropertyRule': {
insertExpandCollapseBtn();
ruleNode.className = 'property-rule';
append(braceLeadingNode, [atRuleNameNode('@property '), textNode(rule.name)]);
append(ruleNode, [braceLeadingNode, cssSymbol(' {')]);
append(braceContentNode, [parseDeclaration('syntax', rule.syntax, indentLevel + 1), parseDeclaration('inherits', '' + rule.inherits, indentLevel + 1), parseDeclaration('initial-value', rule.initialValue, indentLevel + 1, true)]);
addNewLineSpacing(braceContentNode, indentLevel);
append(ruleNode, [braceContentNode, cssSymbol('}')]);
break;
}
default: {
ruleNode.className = 'unexpected-rule';
// should not need to explicitly handle CSSGroupingRule because other rule types inherit from it
// should not need to explicitly handle CSSNestedDeclarations because other rules pass a cssStyleDeclaration
console.warn(rule);
console.warn('unexpected css rule type, using unformatted');
append(ruleNode, textNode(rule.cssText));
break;
}
}
parentElement.appendChild(ruleNode);
}
if (cssStyleDeclaration instanceof CSSStyleSheet) {
const ruleRulesNode = spanNode();
for (const rule of cssStyleDeclaration.cssRules) {
parseRule(ruleRulesNode, rule, indentLevel);
}
append(style, ruleRulesNode);
}
else {
// previously use a for of before to filter out all style declarations
// need to know if there is a next declaration for formatting purposes
// element.style has numbered indexes for styles actually declared on the element
for (let i = 0; ; ) {
const prop = cssStyleDeclaration[i];
if (!prop) {
break;
}
i++;
const hasNext = !!cssStyleDeclaration[i];
append(style, parseDeclaration(prop, cssStyleDeclaration.getPropertyValue(prop), indentLevel + 1, !hasNext));
}
}
return style;
}
function parseScript(node) {
// TODO formatting, highlighting
return spanNode('script', node.textContent.trim());
}
function hideCollapsed() {
let a = '.collapsed';
let b = a;
['br', '.spacer', '.spacer'].forEach((c) => {
b += ' + ' + c;
a += ', ' + b;
});
return a + ' {\n\tdisplay: none;\n}';
}
function color(selector, color) {
return `${selector} {\n\tcolor: ${color};\n}`;
}
function getStyle() {
return `body {
margin: 1em;
padding: 0;
display: block;
font-family: monospace;
font-size: 1em;
line-height: 1.2em;
tab-size: 2;
color: #cbcbc7;
background: #232327;
word-break: break-word;
}
.spacer {
margin: 0;
padding: 0;
border: 0;
outline: 0;
display: inline-block;
}
${hideCollapsed()}
.expand-collapse-button {
margin: 0;
padding: 0;
border: 0;
width: fit-content;
cursor: pointer;
background: inherit;
color: #88888a;
}
.expand-collapse-button:has(+ .spacer + .tag > .collapsed), .expand-collapse-button:has(+ .spacer + .css-brace-leading + .css-symbol + .css-brace-content.collapsed) {
rotate: 270deg;
}
${color('.userscript-error', '#ff0000')}
.document-type {
font-style: italic;
}
${color('.html-symbol', '#7c7c7e')}
${color('.document-type', '#72baf9')}
${color('.comment', '#90EE90')}
${color('.tag-name', '#72baf9')}
${color('.tag-attribute-name', '#fb7be5')}
${color('.tag-attribute-value', '#9b79d4')}
.tag-attribute-value a {
color: inherit;
text-decoration: underline;
}
${color('.tag.hidden-tag .html-symbol', '#6e6e6e')}
${color('.tag.hidden-tag .tag-name', '#929294')}
${color('.tag.hidden-tag .tag-attribute-name', '#676768')}
${color('.tag.hidden-tag .tag-attribute-value', '#939394')}
${color('.css-symbol', '#7c7c7e')}
${color('.css-at-rule-name', '#72baf9')}
${color('.css-declaration-property', '#80d36f')}
${color('.css-declaration-value', '#fb7be5')}
.css-color-preview {
display: inline-block;
width: 1em;
height: 1em;
outline-width: 2px;
outline-style: solid;
filter: invert(100%);
}`;
}
async function main(outputWindow) {
const meta1 = el('meta');
const meta2 = el('meta');
const title = el('title');
const style = el('style');
const output = el('span');
meta1.setAttribute('charset', 'utf-8');
meta2.setAttribute('name', 'viewport');
meta2.setAttribute('content', 'width=device-width, initial-scale=1, minimum-scale=1');
title.innerHTML = 'Web Inspector - ' + document.title;
style.innerHTML = getStyle();
for (const node of document.childNodes) {
await parseHTML(node, output, 0).then(() => {}, (error) => {
debugAlert('in main parseHTML.then error');
throw error;
});
}
if (output.firstElementChild.tagName === 'BR') {
// remove unnecessary spacing at top
output.firstElementChild.remove();
}
setupExpandCollapseBtns(output);
outputWindow.document.write('<!DOCTYPE html><html><head></head><body></body></html>');
append(outputWindow.document.head, meta1);
append(outputWindow.document.head, meta2);
append(outputWindow.document.head, title);
append(outputWindow.document.head, style);
append(outputWindow.document.body, output);
}
async function receiveParseHTMLMessage(event) {
try {
// unable to access iframe content, so wait for a message from the top window
// then pass the output element
if (event.source !== top) {
// this check should reduce security issues
return;
}
if (!(event.data && event.data.WEB_INSPECTOR && event.data.WEB_INSPECTOR.parseHTML)) {
// make sure the instruction exists
return;
}
const indentLevel = parseInt(event.data.WEB_INSPECTOR.parseHTML);
if (!(isFinite(indentLevel) && indentLevel > 0)) {
return;
}
window.removeEventListener('message', receiveParseHTMLMessage);
const output = spanNode();
for (const node of document.childNodes) {
await parseHTML(node, output, indentLevel).then(() => {}, (error) => {
debugAlert('in receiveParseHTMLMessage parseHTML error');
throw error;
});
}
event.source.postMessage({WEB_INSPECTOR: {parseHTMLOutput: output.outerHTML}}, event.origin);
}
catch(err) {
throw err;
}
}
function isDefined(variable) {
return typeof variable !== 'undefined';
}
function extractErrorInfo(error) {
let str = error.name + ': ' + error.message;
if (isDefined(error.fileName) && isDefined(error.lineNumber) && isDefined(error.columnNumber)) {
str += ' in ' + error.fileName + ' ' + error.lineNumber + ':' + error.columnNumber;
}
if (isDefined(error.stack)) {
str += '\n' + error.stack;
}
return str;
}
function errorDetected(error) {
return prompt('Error while using Web Inspector:', extractErrorInfo(error));
}
if (self !== top) {
window.addEventListener('message', receiveParseHTMLMessage);
return;
}
window.WEB_INSPECTOR = function() {
// try to open in a new window
// if popups are blocked, replace the current webpage with the web inspector
const outputWindow = open('about:blank') || window;
main(outputWindow).then(() => {}, (error) => {
debugAlert('in window.WEB_INSPECTOR main error');
errorDetected(error);
});
outputWindow.onload = function() {
main(outputWindow).then(() => {}, (error) => {
debugAlert('in window.WEB_INSPECTOR outputWindow main error');
errorDetected(error);
});
};
};
})();