Website icon getter

Get the icon of current tab

/* eslint-disable no-multi-spaces */

// ==UserScript==
// @name               Website icon getter
// @name:zh-CN         网站图标获取
// @name:en            Website icon getter
// @namespace          Website-icon-getter
// @version            0.1.4
// @description        Get the icon of current tab
// @description:zh-CN  获取当前网站的图标
// @description:en     Get the icon of current tab
// @author             PY-DNG
// @license            GPL-3.0-or-later
// @match              http*://*/*
// @match              file:///*
// @require            https://update.greasyfork.org/scripts/456034/1303041/Basic%20Functions%20%28For%20userscripts%29.js
// @icon               none
// @grant              GM_registerMenuCommand
// @grant              GM_setClipboard
// @grant              GM_xmlhttpRequest
// @noframes
// ==/UserScript==

/* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager */

(function __MAIN__() {
    'use strict';

	const CONST = {
		Text_AllLang: {
			DEFAULT: 'en',
			'en': {
				CopyIconUrl: 'Copy icon url of current tab',
				OpenIconInNewTab: 'Open icon in new tab',
				CopyIconBase64: 'Copy icon url of current tab in Base64 format',
				DownloadIcon: 'Download icon of current tab',
				Download_FileName: 'Icon - {Host}.ico',
			},
			'zh': {
				CopyIconUrl: '复制当前标签页图标地址',
				OpenIconInNewTab: '在新标签页查看图标',
				CopyIconBase64: '复制Base64格式的当前标签页图标地址',
				DownloadIcon: '下载当前标签页图标',
				Download_FileName: 'Icon - {Host}.ico',
			}
		}
	};
	const i18n = navigator.language.split('-')[0] || CONST.Text_AllLang.DEFAULT;
	CONST.Text = CONST.Text_AllLang[i18n];

	GM_registerMenuCommand(CONST.Text.CopyIconUrl, copyIconUrl);
	GM_registerMenuCommand(CONST.Text.OpenIconInNewTab, openIcon);
	GM_registerMenuCommand(CONST.Text.CopyIconBase64, copyIconBase64);
	GM_registerMenuCommand(CONST.Text.DownloadIcon, downloadIcon);

	function downloadIcon() {
		dl_browser(getIconUrl(), replaceText(CONST.Text.Download_FileName, {'{Host}': getHost()}));
	}

	async function copyIconBase64() {
		const url = await getImageUrl(getIconUrl());
		GM_setClipboard(url, 'text');
	}

	function copyIconUrl() {
		GM_setClipboard(getIconUrl(), 'text');
	}

	function openIcon() {
		window.open(getIconUrl());
	}

	function getIconUrl() {
		const head = document.head;
		const link = $(head, 'link[rel~="icon"]');
		return link ? link.href : getHost() + 'favicon.ico';
	}

	// get host part from a url(includes '^https://', '/$')
	function getHost(url=location.href) {
		const match = location.href.match(/https?:\/\/[^\/]+\//);
		return match ? match[0] : match;
	}

	// Get a base64-formatted url of an image
	async function getImageUrl(src) {
		const blob = await get(src, 'blob');
		return toDataURL(blob);
	}

	async function toDataURL(blob) {
		return new Promise((resolve, reject) => {
			try {
				const reader = new FileReader();
				reader.onload = e => resolve(reader.result);
				reader.onerror = reject;
				reader.readAsDataURL(blob);
			} catch(err) {
				reject(err);
			}
		});
	}

	function get(url, responseType='text') {
		return new Promise((resolve, reject) => {
			try {
				GM_xmlhttpRequest({
					method: 'GET',
					url, responseType,
					onload: resp => resolve(resp.response),
					onerror: reject,
					onabort: reject,
					ontimeout: reject
				});
			} catch(err) {
				reject(err);
			}
		});
	}

	function dl_browser(url, name) {
		const a = $CrE('a');
		a.href = url;
		a.download = name;
		a.click();
	}

	// Replace model text with no mismatching of replacing replaced text
	// e.g. replaceText('aaaabbbbccccdddd', {'a': 'b', 'b': 'c', 'c': 'd', 'd': 'e'}) === 'bbbbccccddddeeee'
	//      replaceText('abcdAABBAA', {'BB': 'AA', 'AAAAAA': 'This is a trap!'}) === 'abcdAAAAAA'
	//      replaceText('abcd{AAAA}BB}', {'{AAAA}': '{BB', '{BBBB}': 'This is a trap!'}) === 'abcd{BBBB}'
	//      replaceText('abcd', {}) === 'abcd'
	/* Note:
	    replaceText will replace in sort of replacer's iterating sort
	    e.g. currently replaceText('abcdAABBAA', {'BBAA': 'TEXT', 'AABB': 'TEXT'}) === 'abcdAATEXT'
	    but remember: (As MDN Web Doc said,) Although the keys of an ordinary Object are ordered now, this was
	    not always the case, and the order is complex. As a result, it's best not to rely on property order.
	    So, don't expect replaceText will treat replacer key-values in any specific sort. Use replaceText to
	    replace irrelevance replacer keys only.
	*/
	function replaceText(text, replacer) {
		if (Object.entries(replacer).length === 0) {return text;}
		const [models, targets] = Object.entries(replacer);
		const len = models.length;
		let text_arr = [{text: text, replacable: true}];
		for (const [model, target] of Object.entries(replacer)) {
			text_arr = replace(text_arr, model, target);
		}
		return text_arr.map((text_obj) => (text_obj.text)).join('');

		function replace(text_arr, model, target) {
			const result_arr = [];
			for (const text_obj of text_arr) {
				if (text_obj.replacable) {
					const splited = text_obj.text.split(model);
					for (const part of splited) {
						result_arr.push({text: part, replacable: true});
						result_arr.push({text: target, replacable: false});
					}
					result_arr.pop();
				} else {
					result_arr.push(text_obj);
				}
			}
			return result_arr;
		}
	}
})();