Image Alt to Title

Hover tooltip of image displaying alt attribute, original title and URL info.

目前为 2021-03-28 提交的版本。查看 最新版本

// ==UserScript==
// @name        Image Alt to Title
// @namespace   myfonj
// @include     *
// @grant       none
// @version     1.3.0
// @run-at      document-start
// @description Hover tooltip of image displaying alt attribute, original title and URL info.
// ==/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
 * Load (and error?) event listener constructing and setting title.
 * 
 * Dangers
 * Artificially altered alt or title after image load event will not be taken into account.
 * Mitigate with mutationObserver?
 * 
 * Process
 * Draw the rest of the owl
 * 
 * 
 * § 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> 
 * 
 * */

/**
 * @type {WeakMap.<HTMLImageElement,{originalTitle?:string,augmentedTitle?:string}>}
 */
const processedImages = new WeakMap();

const dppx = window.devicePixelRatio;

// 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
}

document.documentElement.addEventListener('load', altPic, true);
// document.documentElement.addEventListener('error', altPic, true);

/**
 * @param {{target: EventTarget}} event
 */
function altPic (event) {
  const separator = '---';
  try {
    const img = /**@type {HTMLImageElement}*/(event.target);
    if (!img.tagName) return;
    if (img.tagName != 'IMG') return;
    const info = [];
    const alt = img.alt;

    var known = processedImages.get(img);
    if (known) {
      // existing img load new src
      if (img.title == known.augmentedTitle) {
        // reset to not recursively add our own title to new title
        // console.log('known, resetting to original', known.originalTitle);
        img.title = known.originalTitle;
      } else {
        // in this case "original" means "augmented by page author"
        // console.log('known, title does not match', img.title);
        known.originalTitle = img.title;
      }
    } else {
      processedImages.set(img, { originalTitle: img.title });
      known = processedImages.get(img);
      // console.log('unknown', known);
    }

    const title = getClosestTitle(img);

    switch (alt) {
      case null:
        info.push('Alt completely missing!');
        break;
      case '':
        info.push('Alt: \'\'');
        break;
      default:
        if (alt == title) {
          info.push('Alt (=title): ' + alt);
        } else {
          info.push('Alt: ' + alt);
        }
    }

    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);
    }

    // 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);
    }

    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));
        info.push('Path: ' + srcURI.pathname.replace(slugRx, ''));
        if (document.location.hostname != srcURI.hostname || window != window.top) {
          info.push('Host: ' + srcURI.hostname);
        }
        break;
      }
      case 'data:': {
        info.push(img.src.split(',')[0] + ', + ' + img.src.split(',')[1].length + ' b.');
        break;
      }
      default:
        info.push('Src: ' + img.src);
    }
    var CSSsizes = `${img.width}×${img.height}px`;
    // var physicalSizes = `${img.width*dppx}×${img.height*dppx}px`;
    // ↑ hm, this is not working. how to get dimensions in sampling point counts when dppx != 1 / zoomed page?
    if (img.naturalWidth && img.naturalHeight) {
      // SVG have zero naturals
      if (img.naturalWidth == img.width && img.naturalHeight == img.height) {
        CSSsizes += ` (Natural)`;
      } else {
        CSSsizes += ` (Natural: ${img.naturalWidth}×${img.naturalHeight}px)`;
        // physicalSizes += ` (Natural: ${img.naturalWidth*dppx}×${img.naturalHeight*dppx}px)`;
      }
    }
    info.push('CSS Size: ' + CSSsizes);
    // if (dppx != 1) {
    //  info.push('Physical: ' + physicalSizes);
    // }
    img.title = known.augmentedTitle = info.join('\n');
  } catch (e) {
    // console.error(e);
  }
}

/**
 * @param {HTMLElement} el
 */
function getClosestTitle (el) {
  var ret = [];
  do {
    if (el.title) {
      return el.title;
    }
  } while ((el = el.parentElement) && el !== document.documentElement);
  return ''
}