Image Alt to Title

Hover tooltip of image displaying alt attribute, original title, some accessibility-related properties, and URL info.

当前为 2022-09-05 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Image Alt to Title
// @namespace   myfonj
// @include     *
// @grant       none
// @version     1.6.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://greasyfork.org/en/scripts/418348/versions/new
 * 
 * § 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
if (document.querySelector('body>img[alt="' + document.location.href + '"]:only-child')) {
  // @ts-ignore (GreaseMonkey script is in fact function body)
  return
}

const originalTitles = new WeakMap();
let lastSetTitle = '';
const docEl = document.documentElement;
const listenerConf = { capture: true, passive: true };

docEl.addEventListener('mouseenter', altToTitle, listenerConf);
docEl.addEventListener('mouseleave', restoreTitle, listenerConf);

function altToTitle (event) {
  const tgt = event.target
  if (tgt.tagName && tgt.tagName == 'IMG') {
    if(originalTitles.has(tgt) || (tgt.title && tgt.title === lastSetTitle)) {
      // few times I got situations when mouseout was not triggered
      // presumably because someting covered the image
      // or whole context were temporarily replaced or covered
      // or perhaps it was reconstructed from dirty snapshot
      // so this should prevent exoponentially growing title
      return
    }
    originalTitles.set(tgt, tgt.getAttribute('title'));
    altPic(tgt);
  }
}

function restoreTitle (event) {
  const tgt = event.target;
  if (originalTitles.has(tgt)) {
    let ot = originalTitles.get(tgt);
    if(ot === null) {
      tgt.removeAttribute('title');
    } else {
      tgt.title = ot;
    }
    originalTitles.delete(tgt);
  }
}

/**
 * @param {HTMLImageElement} img
 */
function altPic (img) {
  try {
    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: ' + 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): ${altText}`);
        } else {
          info.push(`Alt: ${altText}`);
        }
    }

    if (title && alt != title) {
      info.push(separator);
      info.push('Title: ' + title);
    }

    const descby = img.getAttribute('aria-describedby');
    if (descby) {
      info.push(separator);
      info.push('Described by `' + descby + '`: ' + (document.getElementById(descby) || { textContent: '(element not found)' }).textContent);
    }

    // depreated, 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: ' + longdesc);
    }

    const arialabel = img.getAttribute('aria-label');
    if (arialabel) {
      info.push(separator);
      info.push('Label (ARIA): ' + 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: ' + histeve);
    }

    var fig = getClosestEl(img, 'FIGURE');
    if( fig ){
      let capt = fig.querySelector('figcaption');
      if( capt) {
        info.push(separator);
        info.push('Figcaption: ' + 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.search) {
          info.push('Params: ' + srcURI.search);
        }
        info.push('File: ' + srcURI.pathname.match(slugRx));
        let path = srcURI.pathname.replace(slugRx, '');
        if(path && path != '/') {
          info.push('Path: ' + srcURI.pathname.replace(slugRx, ''));
        }
        if (document.location.hostname != srcURI.hostname || window != window.top) {
          info.push('Host: ' + srcURI.hostname);
        }
        break;
      }
      case 'data:': {
        let durichunks = srcURI.href.split(',');
        info.push(durichunks[0] + ', + ' + durichunks[1].length + ' b.');
        break;
      }
      default:
        info.push('Src: ' + 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: ' + CSSsizes);
    img.title = info.join('\n');
    lastSetTitle = img.title;
  } catch (e) {
    // console.error('altPic ERROR', e, img);
  }
}

/**
 * @param {HTMLElement} el
 */
function getClosestTitle (el) {
  do {
    if (el.title) {
      return el.title;
    }
  } while (el = el.parentElement);
  return ''
}

/**
 * @param {HTMLElement} el
 */
function getClosestEl (el, tagName) {
  do {
    if (el.tagName == tagName) {
      return el;
    }
  } while (el = el.parentElement);
  return false
}


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 + ']'
}