您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Hover tooltip of image displaying alt attribute, original title, some accessibility-related properties, and URL info.
// ==UserScript== // @name Image Alt to Title // @namespace myfonj // @include * // @grant none // @version 1.9.2 // @run-at document-start // @description Hover tooltip of image displaying alt attribute, original title, some accessibility-related properties, and URL info. // @license CC0 // ==/UserScript== /* * https://gf.qytechs.cn/en/scripts/418348/versions/new * * Changelog: * 1.9.2 (2024-11-04): Another fix for SVG titles. Titled SVG (non-root) elements still take precedence over ours "view source" amendments. * 1.9.1 (2024-11-04): Fix for SVG source overshadowing parent (possibly HTML) title. * 1.9.0 (2024-10-31): SVG source to its title. Crude, but how I needed this, goddamit! * 1.8.9 (2024-01-24): better optical formatting of location search (URLSearchParams) * 1.8.8 (2023-09-12): no "none" background, further tab stop adjustments * 1.8.7 (2023-09-11): unified tab stop across devices (hopefuly) * 1.8.6 (2023-09-04): values separated by tab stops from labels * 1.8.5 (2023-09-04): for multiline string, break them below label, so the first line aligns with rest * 1.8.4 (2022-11-04): trim long strings * 1.8.3 (2022-11-02): ~ minor, omit empty filename from info. * 1.8.2 (2022-10-23): ~ minor, bail out from image-only page also in Chrome / Edge. * 1.8.1 (2022-10-19): ~ minor text corrections. * 1.8.0 (2022-10-18): + 'generator-unable-to-provide-required-alt' https://html.spec.whatwg.org/multipage/images.html#guidance-for-markup-generators. * * § Trivia: * ¶ Hover tooltip displays content of nearest element's title attribute (@title). * ¶ Alt attribute (@alt) is possible only at IMG element. * ¶ IMG@alt is not displayed in tooltip. * ¶ IMG cannot have children. * ¶ @title is possible on any element, including IMG. * ¶ IMG@src is also valuable. * * Goal: * Display image alt attribute value in images hover tooltip, add valuable @SRC chunks. * * Details * Pull @alt from image and set it so it is readable as @title tooltip * so that produced title value will not obscure existing parent title * that would be displayed otherwise. Also include image filename from @src, * and additionally path or domain. * * Means * Upon "hover" set image's title attribute. Luckily tooltips delay catches augmented value. * * § Tastcases * * FROM: * <a> * <img> * </a> * TO: * <a> * <img title="Alt missing."> * </a> * * FROM: * <a> * <img alt=""> * </a> * TO: * <a> * <img alt="" title="Alt: ''"> * </a> * * FROM: * <a> * <img alt="░"> * </a> * TO: * <a> * <img alt="░" title="Alt: ░"> * </a> * * FROM: * <a> * <img alt="░" title="▒"> * </a> * TO: * <a> * <img title="Alt: ░, title: ▒"> * </a> * FROM: * <a title="▒"> * <img alt="░"> * </a> * TO: * <a> * <img title="Alt: ░, title: ▒"> * </a> * */ // do not run at image-only pages // Firefox is adding alt same as location if ( document.querySelector(`body > img[src="${document.location.href}"]:only-child`) ) { // @ts-ignore (GreaseMonkey script is in fact function body) return } const originalTitles = new WeakMap(); const amendedSVG = new WeakMap(); let lastSetTitle = ''; const docEl = document.documentElement; const listenerConf = { capture: true, passive: true }; docEl.addEventListener('mouseenter', altToTitle, listenerConf); docEl.addEventListener('mouseleave', restoreTitle, listenerConf); const hoverLoadHandlerConf = { passive: true, once: false, capture: true }; function hoverLoadHandler (event) { const tgt = event.target; // console.log('load', tgt) altPic(tgt, 'prepend'); } function altToTitle (event) { const tgt = event.target; const tag = tgt.tagName; if(!tag) { return } if(tgt.namespaceURI === 'http://www.w3.org/2000/svg'){ const origTitle = getClosestTitle(tgt); const s = tgt.closest('svg'); if(amendedSVG.has(s)) { return } let st = s.querySelector('& > title'); // FIXME: add handling for nested titled SVG elements // not clear how exactly: to always show the full source // wou would have to temp-remove title elements a hoist // their text to our root constructed. let origSource = s.outerHTML; if( st ) { amendedSVG.set(s,st.textContent); } else { amendedSVG.set(s,null); st = s.appendChild( document.createElementNS( 'http://www.w3.org/2000/svg', 'title' ) ); } if(origTitle){ origSource = origTitle + '\n\n---\n\n' + origSource } st.textContent = origSource; return } if (tag == 'IMG') { if (originalTitles.has(tgt) || (tgt.title && tgt.title === lastSetTitle)) { // few times I got situations when mouseout was not triggered // presumably because something covered the image // or whole context was temporarily replaced or covered // or perhaps it was reconstructed from dirty snapshot // so this should prevent exponentially growing title return } tgt.addEventListener('load', hoverLoadHandler, hoverLoadHandlerConf); originalTitles.set(tgt, tgt.getAttribute('title')); altPic(tgt); } } function restoreTitle (event) { const tgt = event.target; if(tgt.namespaceURI==='http://www.w3.org/2000/svg'){ const s = tgt.closest('svg'); if(amendedSVG.has(s)) { const ot = amendedSVG.get(s); const te = s.querySelector('& > title'); if(ot) { te.textContent = ot; } else { te.remove(); } amendedSVG.delete(s); } return } if (originalTitles.has(tgt)) { let ot = originalTitles.get(tgt); if (ot === null) { tgt.removeAttribute('title'); } else { tgt.title = ot; } originalTitles.delete(tgt); } tgt.removeEventListener('load', hoverLoadHandler, hoverLoadHandlerConf); } /** * @param {HTMLImageElement} img * @param {'prepend'} [mode] */ function altPic (img, mode) { // console.log('altPic', mode); try { let titleToAppend = ''; if (mode == 'prepend') { titleToAppend = img.title; if (titleToAppend == lastSetTitle) { img.removeAttribute('title'); } } const separator = '---'; const info = []; const alt = img.getAttribute('alt'); let altText = alt || ''; const title = getClosestTitle(img); const role = img.getAttribute('role'); const isPresentation = role === 'presentation'; if (role) { info.push('Role:\t' + role); } switch (alt) { case null: info.push(isPresentation ? `(Alt missing but not needed for this role.)` : `⚠ Alt missing`); break; case '': info.push(`Alt: ""`); break; default: if (alt != alt.trim()) { // "quote" characters are generally useful only to reveal leading/trailing whitespace altText = `»${alt}«`; } if (alt == title) { info.push(`Alt (=title):\t${altText}`); } else { // break first line below "Alt:" label when alt also contains breaks. if(altText.includes('\n')){ altText = '\n' + altText; } info.push(`Alt:\t${altText}`); } } // https://html.spec.whatwg.org/multipage/images.html#guidance-for-markup-generators const gutpra = img.getAttribute('generator-unable-to-provide-required-alt'); if (gutpra !== null) { info.push(separator); info.push('generator-unable-to-provide-required-alt'); } if (title && alt != title) { info.push(separator); info.push('Title:\t' + title); } const descby = img.getAttribute('aria-describedby'); if (descby) { info.push(separator); info.push('Described by (ARIA)`' + descby + '`:\t' + (document.getElementById(descby) || { textContent: '(element not found)' }).textContent); } // deprecated, but let's see // https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/longDesc // https://www.stylemanual.gov.au/format-writing-and-structure/content-formats/images/alt-text-captions-and-titles-images const longdesc = img.getAttribute('longdesc'); if (longdesc) { info.push(separator); info.push('Long Description (deprecated):\t' + longdesc); } const arialabel = img.getAttribute('aria-label'); if (arialabel) { info.push(separator); info.push('Label (ARIA):\t' + arialabel); } // https://html5accessibility.com/stuff/2021/02/09/aria-description-by-public-demand-and-to-thunderous-applause/ const histeve = img.getAttribute('aria-description'); if (histeve) { info.push(separator); info.push('Description (ARIA):\t' + histeve); } var fig = img.closest('FIGURE'); if (fig) { let capt = fig.querySelector('figcaption'); if (capt && capt.textContent) { info.push(separator); info.push('Caption:\t' + capt.textContent.trim()); } } info.push(separator); const srcURI = new URL(img.currentSrc || img.src, img.baseURI); const slugRx = /[^/]+$/; switch (srcURI.protocol) { case 'http:': case 'https:': { if (srcURI.hash) { info.push('Hash:\t' + trimString(decodeURIComponent(srcURI.hash))); } if (srcURI.search) { info.push('Search Params:\t' + formatParams(srcURI.search)); } let filename = srcURI.pathname.match(slugRx); if (filename) { info.push('File:\t' + trimString(decodeURIComponent(String(filename)))); } let path = srcURI.pathname.replace(slugRx, ''); if (path && path != '/') { info.push('Path:\t' + trimString(decodeURIComponent(srcURI.pathname.replace(slugRx, '')))); } if (document.location.hostname != srcURI.hostname || window != window.top) { info.push('Host:\t' + trimString(srcURI.hostname)); } break; } case 'data:': { info.push(trimString(srcURI.href)); break; } default: info.push('Src:\t' + trimString(srcURI.href)); } // ↔ ↕ var CSSsizes = `${img.width} × ${img.height} CSSpx${findRatio(img.width, img.height)}`; var _width_ratio, _height_ratio; if (img.naturalWidth && img.naturalHeight) { // SVG have zero naturals if (img.naturalWidth == img.width && img.naturalHeight == img.height) { CSSsizes += ` (Natural)`; } else { _width_ratio = '~' + (img.width / img.naturalWidth * 100).toFixed(0) + '% of '; _height_ratio = '~' + (img.height / img.naturalHeight * 100).toFixed(0) + '% of '; if (_height_ratio == _width_ratio) { _height_ratio = ''; } CSSsizes += ` (${_width_ratio}${img.naturalWidth} × ${_height_ratio}${img.naturalHeight} natural px${findRatio(img.naturalWidth, img.naturalHeight)})`; } } info.push('Size:\t' + CSSsizes); const cs = getComputedStyle(img); if (cs.backgroundImage && cs.backgroundImage != 'none') { info.push(separator); info.push('Background:\t' + cs.backgroundImage); } // unified tab stop across devices (hopefuly) // hotfix for label length and tab widths // add bunch of spaces to get uniform lengths // to tab aligns values in all browsers // (each value has the label at the begining, or not at all) const labelRgx = /^([A-Z].*?:)(\t)/; const longestLength = 3 + info.reduce((acc,msg)=>{ if(!msg.startsWith('Background:\t') && labelRgx.test(msg)) { const l = msg.match(labelRgx)[1].length; if( acc < l ) { acc = l; } }; return(acc); },0); const finalTitle = info.map(msg=>{ if(labelRgx.test(msg)) { return msg.replace(labelRgx,(m0, m1, m2)=>{ return m1.padEnd(longestLength, '\u2002') + m2 }); }; return msg; }).join('\n'); img.title = finalTitle; if (titleToAppend && (finalTitle != titleToAppend)) { img.title += '\n\n-- Previously --\n\n' + titleToAppend; } lastSetTitle = img.title; } catch (e) { // console.error('altPic ERROR', e, img); } } /** * @param {HTMLElement|SVGElement} el */ function getClosestTitle (el) { let _ = el; do { let isSVG = _.namespaceURI === 'http://www.w3.org/2000/svg'; if(isSVG){ let svgTitle = _.querySelector('& > title'); if(svgTitle) { return svgTitle.textContent; } } else { if (_.title) { return _.title; } } } while (_.parentElement && (_ = _.parentElement)); return '' } function findRatio (x, y) { var smallest = Math.min(x, y); var n = 0; var res = n; while (++n <= smallest) { if (x % n == 0 && y % n == 0) res = n; } if (res == 1) { return '' } return ' [' + x / res + ':' + y / res + ']' } function trimString (str) { const limit = 524; if(str.length < limit) { return str; } return str.slice(0, limit) + ' (…+ '+ (str.length - limit) + ' characters)'; } function formatParams(search) { let result = []; for ( const [k, v] of new URLSearchParams(search) ) { result.push(trimString(`${k}${v?`\t=\t${v}`:``}`)) } if( result.length === 1) { return result } else if (result.length > 1){ return '\n' + result.map(_=>`\t${_}`).join('\n') } return '' }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址