restore2OriginalTitleOnFANZA (experimental)

FANZA独自の作品タイトルへの伏せ字を元に戻す

/* jshint esversion: 6 */
// ==UserScript==
// @name         restore2OriginalTitleOnFANZA (experimental)
// @namespace    https://greasyfork.org/ja/users/289387-unagionunagi
// @version      0.1.6
// @description  FANZA独自の作品タイトルへの伏せ字を元に戻す
// @author       unagiOnUnagi
// @match        *://*.dmm.co.jp/*/detail/*
// @match        *://*.dmm.co.jp/*/list/*
// @match        *://*.dmm.co.jp/*/search/*
// @grant        GM_setValue
// @grant        GM_getValue
// @license      GPL-2.0-or-later
// ==/UserScript==

function resolvePropVal(isChecked) {
    return isChecked ? ['inline', 'none'] : ['none', 'inline'];
}

function addStyleSheet() {
    let styleEl = document.createElement('style');
    styleEl.id = 'restoreoriginaltitle-css';
    document.head.appendChild(styleEl);

    let styleSheet = styleEl.sheet;

    let [restored, masked] = resolvePropVal(GM_getValue('checked', true));
    styleSheet.insertRule(`.restored-title {display: ${restored};}`, 0);
    styleSheet.insertRule(`.masked-title {display: ${masked};}`, 1);
}

function toggleTitle(ev) {
    let rules = document.getElementById('restoreoriginaltitle-css').sheet.rules;
    let isChecked = ev.target.checked;
    for (let [i, value] of resolvePropVal(isChecked).entries()) {
        rules[i].style.display = value;
    }
    GM_setValue('checked', isChecked);
}

function addCheckbox(parent, isListPage=false, nor=null) {

    let style = (isListPage)
    ? 'margin-left: 10px; font-size: 10px;'
    : 'margin-left: 10px; font-size: 10px; font-weight: normal; float: right;';

    let label = (nor == null)
    ? '伏せ字を復元'
    : ` 伏せ字を復元(${nor})`;

    let restoreSpan = document.createElement('span');
    restoreSpan.title = 'FANZA独自の作品タイトルへの伏せ字の復元を試みます';
    restoreSpan.style.cssText = style;
    parent.appendChild(restoreSpan);

    let restoreCb = document.createElement('input');
    restoreCb.type = 'checkbox';
    restoreCb.id = 'restoreoriginaltitle-cb';
    restoreCb.name = 'restoreoriginaltitle-cb';
    restoreCb.style.cssText = 'vertical-align: middle; transform: scale(0.8);';
    restoreSpan.appendChild(restoreCb);

    let restoreLabel = document.createElement('label');
    restoreLabel.htmlFor = 'restoreoriginaltitle-cb';
    restoreLabel.textContent = label;
    restoreSpan.appendChild(restoreLabel);

    restoreCb.checked = GM_getValue('checked', true);
    restoreCb.addEventListener('change', toggleTitle);
}

function _restore(title) {
    const _MASKED_WORDS = [
        ['犯●れ', '犯され'],
        ['●す', '犯す'],
        [/犯●([\s\d])/g, '犯す$1'],
        [/(?<!を)強●(?:(?=[BVしさ犯罪調魔事被白総映。!・\s\d])|$)/ig, '強姦'],
        [/強●(?=[AFMS\u30a0-\u30ff\u3040-\u309f\u3005-\u3006\u30e0-\u9fcf])/ig, '強制'],
        ['レ●プ', 'レイプ'],
        ['陵●', '陵辱'],
        ['凌●', '凌辱'],
        ['夜●い', '夜這い'],
        ['●っ払', '酔っ払'],
        ['●っぱら', '酔っぱら'],
        ['痴●', '痴漢'],
        ['輪●', '輪姦'],
        ['催●', '催眠'],
        ['泥●', '泥酔'],
        ['奴●', '奴隷'],
        ['シ●タ', 'ショタ'],
        [/(?<![子様])●校/g, '高校'],
        ['無●正', '無修正'],
        ['昏●', '昏睡'],
        ['折●', '折檻'],
        [/(?<=[薬酒]を)●ませ/g, '飲ませ'],
        ['◆', '♥'],
        // ['(ハート)', '♥'],
    ];

    for (let [masked, repl] of _MASKED_WORDS) {
        title = title.replaceAll(masked, repl);
    }

    return title;
}

function restore2OriginalTitle(titleElem) {
    for (let c of titleElem.childNodes) {
        if (c.nodeName == '#text') {
            let textc = c.nodeValue;
            if (!textc.trim()) continue;

            let masked = textc;
            let restored = _restore(masked);

            if (masked == restored) return false;
            console.log(`restore title:\n${masked.trim()} =>\n${restored.trim()}`);

            titleElem.removeChild(c);

            let restoreSpan = document.createElement('span');
            restoreSpan.classList.add('restored-title');
            restoreSpan.style.cssText = 'color: unset; font-size: unset; font-weight: unset;';
            restoreSpan.textContent = restored;
            titleElem.appendChild(restoreSpan);

            let maskedSpan = document.createElement('span');
            maskedSpan.classList.add('masked-title');
            maskedSpan.style.cssText = 'color: unset; font-size: unset; font-weight: unset;';
            maskedSpan.textContent = masked;
            titleElem.appendChild(maskedSpan);
            return true;
        }
    }
}

(function() {

    if (window.frameElement) return;

    addStyleSheet();

    let checkbox, parent;
    // 各商品ページの商品タイトル
    let titleElem = document.querySelector('#title,h1.item');
    if (titleElem) {
        parent = titleElem.parentNode;
    } else if ((titleElem = document.querySelector('.productTitle__txt'))) {
        parent = document.querySelector('.productInfo');
    }
    if (titleElem && restore2OriginalTitle(titleElem)) {
        addCheckbox(parent);
        return;
    }

    // 商品一覧
    let lineup = document.querySelectorAll('#list a .txt,table[summary="商品一覧"] .ttl a,' +
                                           '.m-productList .tileListTtl__txt a,'+
                                           '.l-areaListMain a .m-boxListBookProductTmb__ttl,' +
                                           '.l-areaListMain .m-boxListBookProductBlock__main__info__ttl a,' +
                                           '.list-favorite .ttl-data a,' +
                                           '.bookmark-list .item-name a');
    if (!lineup.length) return;

    let numOfRepd = 0;
    for (let elem of lineup) {
        if (restore2OriginalTitle(elem)) numOfRepd++;
    }

    let control = document.querySelector('.list-boxcaptside div.list-unit,.d-boxcaptside div.d-unit');
    if (control) {
        addCheckbox(control.parentNode, true, numOfRepd);
    } else if ((control = document.querySelector('#mu,.l-areaMainColumn,#l-contents,#main-bmk,#l_page_bookmark .basket-sortBox'))) {
        addCheckbox(control, true, numOfRepd);
    } else {
        console.log('Breadcrumb list not found');
    }

})();