YN-SearchOrigin

Find the original article of the article of Yahoo News Japan.

// ==UserScript==
// @name            YN-SearchOrigin
// @name:ja         Yahoo!ニュースの元記事を探す
// @namespace       https://furyutei.work
// @license         MIT
// @version         0.1.16
// @description     Find the original article of the article of Yahoo News Japan.
// @description:ja  Yahoo!ニュースの記事の、元となった記事探しを助けます
// @author          furyu
// @match           https://news.yahoo.co.jp/*
// @match           https://www.google.com/search*
// @grant           none
// @compatible      chrome
// @compatible      firefox
// @supportURL      https://github.com/furyutei/YN-SearchOrigin/issues
// @contributionURL https://memo.furyutei.work/about#send_donation
// ==/UserScript==

( async () => {
'use strict';

const
    SCRIPT_NAME = 'YN-SearchOrigin',
    DEBUG = false,
    
    IMAGE_ALT_TO_HOSTNAME_MAP = Object.assign( Object.create( null ), {
        'THE PAGE' : null,
        '47NEWS' : 'www.47news.jp',
        'テレビ朝日系(ANN)' : 'news.tv-asahi.co.jp',
        'Impress Watch' : 'watch.impress.co.jp',
    } ),
    
    HOSTNAME_TO_VALID_HOSTNAME_MAP = Object.assign( Object.create( null ), {
        'news.yahoo.co.jp' : null,
        'www.watch.impress.co.jp' : 'watch.impress.co.jp',
        'japanese.yonhapnews.co.kr' : 'jp.yna.co.kr',
    } ),
    
    CONTROL_CONTAINER_CLASS = SCRIPT_NAME + '-control-container',
    SEARCH_BUTTON_CLASS = SCRIPT_NAME + '-search-button',
    MODE_SELECTOR_CLASS = SCRIPT_NAME + '-mode-selector',
    SEARCHING_CLASS = SCRIPT_NAME + '-searching',
    CSS_STYLE_CLASS = SCRIPT_NAME + '-css-rule',
    
    SEARCH_BUTTON_TEXT = '元記事検索',
    MODE_SELECTOR_AUTO_TEXT = '自動',
    
    PAGE_TRANSITION_DELAY = 800, // TODO: Chromeで、ページ遷移までの時間が短すぎると(?) history に記録されない場合がある模様→止むをえず、遅延させている
    
    self = undefined,
    
    format_date = ( date, format, is_utc ) => {
        if ( ! format ) {
            format = 'YYYY-MM-DD hh:mm:ss.SSS';
        }
        
        let msec = ( '00' + ( ( is_utc ) ? date.getUTCMilliseconds() : date.getMilliseconds() ) ).slice( -3 ),
            msec_index = 0;
        
        if ( is_utc ) {
            format = format
                .replace( /YYYY/g, date.getUTCFullYear() )
                .replace( /MM/g, ( '0' + ( 1 + date.getUTCMonth() ) ).slice( -2 ) )
                .replace( /DD/g, ( '0' + date.getUTCDate() ).slice( -2 ) )
                .replace( /hh/g, ( '0' + date.getUTCHours() ).slice( -2 ) )
                .replace( /mm/g, ( '0' + date.getUTCMinutes() ).slice( -2 ) )
                .replace( /ss/g, ( '0' + date.getUTCSeconds() ).slice( -2 ) )
                .replace( /S/g, ( all ) => {
                    return msec.charAt( msec_index ++ );
                } );
        }
        else {
            format = format
                .replace( /YYYY/g, date.getFullYear() )
                .replace( /MM/g, ( '0' + ( 1 + date.getMonth() ) ).slice( -2 ) )
                .replace( /DD/g, ( '0' + date.getDate() ).slice( -2 ) )
                .replace( /hh/g, ( '0' + date.getHours() ).slice( -2 ) )
                .replace( /mm/g, ( '0' + date.getMinutes() ).slice( -2 ) )
                .replace( /ss/g, ( '0' + date.getSeconds() ).slice( -2 ) )
                .replace( /S/g, ( all ) => {
                    return msec.charAt( msec_index ++ );
                } );
        }
        
        return format;
    },
    
    get_gmt_datetime = ( time, is_msec ) => {
        let date = new Date( ( is_msec ) ? time : 1000 * time );
        
        return format_date( date, 'YYYY-MM-DD_hh:mm:ss_GMT', true );
    },
    
    get_log_timestamp = () => format_date( new Date() ),
    
    log_debug = ( ... args ) => {
        if ( ! DEBUG ) {
            return;
        }
        console.debug( '%c[' + SCRIPT_NAME + '] ' + get_log_timestamp(), 'color: gray;', ... args );
    },
    
    log = ( ... args ) => {
        console.log( '%c[' + SCRIPT_NAME + '] ' + get_log_timestamp(), 'color: teal;', ... args );
    },
    
    log_info = ( ... args ) => {
        console.info( '%c[' + SCRIPT_NAME + '] ' + get_log_timestamp(), 'color: darkslateblue;', ... args );
    },
    
    log_error = ( ... args ) => {
        console.error( '%c[' + SCRIPT_NAME + '] ' + get_log_timestamp(), 'color: purple;', ... args );
    },
    
    current_url_object = new URL( location.href ),
    
    WindowNameStorage = class {
        constructor( target_window, storage_name ) {
            const
                self = this;
            
            self.init( target_window, storage_name );
        }
        
        init( target_window, storage_name ) {
            const
                self = this;
            
            Object.assign( self, {
                target_window,
                storage_name,
            } );
            
            return self;
        }
        
        get value() {
            const
                self = this,
                target_window = self.target_window,
                storage_name = self.storage_name;
            
            if ( ( ! target_window ) || ( ! storage_name ) ) {
                return {};
            }
            
            try {
                return JSON.parse( target_window.name )[ storage_name ] || {};
            }
            catch ( error ) {
                return {};
            }
        }
        
        set value( spec_value ) {
            this.target_window.name = this.get_name( spec_value );
        }
        
        get_name( spec_value ) {
            const
                self = this,
                target_window = self.target_window,
                storage_name = self.storage_name;
            
            let original_name_params = {};
            
            if ( target_window ) {
                try {
                    original_name_params = JSON.parse( target_window.name );
                    if ( ! ( original_name_params instanceof Object ) ) {
                        original_name_params = {};
                    }
                }
                catch ( error ) {
                    original_name_params = {};
                }
            }
            
            try {
                if ( storage_name ) {
                    if ( spec_value === undefined ) {
                        delete original_name_params[ storage_name ];
                    }
                    else {
                        original_name_params[ storage_name ] = spec_value;
                    }
                }
                return JSON.stringify( original_name_params );
            }
            catch ( error ) {
                return '';
            }
        }
    },
    
    WindowControl = class {
        constructor( url = null, options = {} ) {
            const
                self = this;
            
            self.initial_url = url;
            self.child_window_counter = 0;
            self.existing_window = null;
            
            if ( ! url ) {
                return;
            }
            
            self.open( url, options );
        }
        
        open( url, options ) {
            const
                self = this;
            
            if ( ! options ) {
                options = {};
            }
            
            let child_window = options.existing_window || self.existing_window;
            
            if ( ! options.child_call_parameters ) {
                options.child_call_parameters = {};
            }
            
            try {
                Object.assign( options.child_call_parameters, {
                    script_name : SCRIPT_NAME,
                    child_window_id : '' + ( new Date().getTime() ) + '-' + ( ++ self.child_window_counter ),
                    transition_complete : false,
                } );
            }
            catch ( error ) {
                log_error( error );
            }
            
            if ( child_window ) {
                new WindowNameStorage( child_window, SCRIPT_NAME ).value = options.child_call_parameters;
                
                if ( child_window.location.href != url ) {
                    setTimeout( () => {
                        child_window.location.href = url;
                    }, PAGE_TRANSITION_DELAY );
                }
            }
            else {
                child_window = window.open( url, new WindowNameStorage( null, SCRIPT_NAME ).get_name( options.child_call_parameters ) );
                //new WindowNameStorage( child_window, SCRIPT_NAME ).value = options.child_call_parameters;
            }
            
            self.existing_window = child_window;
            
            return self;
        }
        
        close() {
            const
                self = this;
            
            if ( ! self.existing_window ) {
                return self;
            }
            
            try {
                self.existing_window.close();
            }
            catch ( error ) {
            }
            self.existing_window = null;
            
            return self;
        }
    },
    
    ModeControl = class {
        constructor() {
            this.storage_mode_info_name = SCRIPT_NAME + '-mode_info';
            this.load_mode_info();
        }
        
        load_mode_info() {
            try {
                this.mode_info = JSON.parse( localStorage.getItem( this.storage_mode_info_name ) );
            }
            catch ( error ) {
            }
            
            if ( ! this.mode_info ) {
                this.mode_info = {};
            }
            
            if ( Object.keys( this.mode_info ).length <= 0 ) {
                this.mode_info = {
                    is_automode : false,
                };
            }
        }
        
        save_mode_info() {
            localStorage.setItem( this.storage_mode_info_name, JSON.stringify( this.mode_info ) );
        }
        
        get is_automode() {
            return this.mode_info.is_automode;
        }
        
        set is_automode( specified_mode ) {
            this.mode_info.is_automode = !! specified_mode;
            this.save_mode_info();
        }
        
        create_control_element() {
            const
                self = this,
                control_element = document.createElement( 'label' ),
                automode_checkbox = document.createElement( 'input' );
            
            
            control_element.className = MODE_SELECTOR_CLASS;
            control_element.textContent = MODE_SELECTOR_AUTO_TEXT;
            
            automode_checkbox.type = 'checkbox';
            automode_checkbox.checked = self.is_automode;
            
            automode_checkbox.addEventListener( 'change', ( event ) => {
                event.stopPropagation();
                event.preventDefault();
                self.is_automode = automode_checkbox.checked;
            } );
            
            control_element.firstChild.before( automode_checkbox );
            
            return control_element;
        }
    },
    
    searching_icon_control = new class {
        constructor() {
            const
                self = this;
            
            self.searching_container = null;
        }
        
        create() {
            const
                self = this;
            
            if ( self.searching_container ) {
                return self;
            }
            
            const
                searching_icon_svg = '<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" fill="none" r="10" stroke-width="4" style="stroke: currentColor; opacity: 0.4;"></circle><path d="M 12,2 a 10 10 -90 0 1 9,5.6" fill="none" stroke="currentColor" stroke-width="4" />',
                searchin_icon = document.createElement( 'div' ),
                searching_container = self.searching_container = document.createElement( 'div' );
            
            searchin_icon.className = 'icon';
            searchin_icon.insertAdjacentHTML( 'beforeend', searching_icon_svg );
            
            searching_container.className = SEARCHING_CLASS;
            searching_container.appendChild( searchin_icon );
            
            document.documentElement.appendChild( searching_container );
            return self;
        }
        
        hide() {
            const
                self = this;
            
            if ( ! self.searching_container ) {
                return self;
            }
            
            self.searching_container.classList.add( 'hidden' );
            return self;
        }
        
        show() {
            const
                self = this;
            
            if ( ! self.searching_container ) {
                return self;
            }
            
            self.searching_container.classList.remove( 'hidden' );
            return self;
        }
    },
    
    get_search_hostname = ( site_link ) => {
        let image_alt = ( site_link.querySelector( 'img[alt]' ) || {} ).alt,
            hostname = ( image_alt in IMAGE_ALT_TO_HOSTNAME_MAP ) ? IMAGE_ALT_TO_HOSTNAME_MAP[ image_alt ] : new URL( site_link.href ).hostname;
        
        if ( hostname ) {
            hostname = hostname.replace( /^www\./, '' );
        }
        
        if ( hostname && ( hostname in HOSTNAME_TO_VALID_HOSTNAME_MAP ) ) {
            hostname = HOSTNAME_TO_VALID_HOSTNAME_MAP[ hostname ];
        }
        
        return hostname;
    },
    
    get_search_info = () => {
        const
            site_link = document.querySelector( 'main[id="contents"] div[id="contentsWrap"] > article > header > div > div:last-child > a' );
        
        if ( ! site_link ) {
            return null;
        }
        
        const
            hostname = get_search_hostname( site_link );
        
        if ( ! hostname ) {
            return null;
        }
        
        const
            keyword = ( ( document.querySelector( 'main[id="contents"] div[id="contentsWrap"] > article > header > h1' ) || {} ).textContent || ( ( document.querySelector( 'meta[property="og:title"]' ) || {} ).content || document.title ).replace( /([((].*?[))])?\s*-[^\-]*$/, '' ) || '' ).trim();
        
        return {
            site_link,
            hostname,
            keyword,
            search_url : 'https://www.google.com/search?ie=UTF-8&q=' + encodeURIComponent( 'site:' + hostname + ' ' + keyword ),
        };
    },
    
    create_control_container = ( parameters ) => {
        if ( ! parameters ) {
            parameters = {};
        }
        
        let container = document.createElement( 'div' );
        
        container.className = CONTROL_CONTAINER_CLASS;
        
        return container;
    },
    
    create_button = ( parameters ) => {
        if ( ! parameters ) {
            parameters = {};
        }
        let button = document.createElement( 'a' );
        
        button.className = SEARCH_BUTTON_CLASS;
        button.textContent = SEARCH_BUTTON_TEXT;
        button.href = parameters.url ? parameters.url : '#';
        
        if ( parameters.onclick ) {
            button.addEventListener( 'click', parameters.onclick );
        }
        
        return button;
    },
    
    child_called_parameters = new WindowNameStorage( window, SCRIPT_NAME ).value,
    is_child_page = child_called_parameters.script_name,
    is_auto_transition_page = ! child_called_parameters.transition_complete,
    
    check_pickup_page = () => {
        log_debug( 'check_pickup_page()' );
        
        if ( document.querySelector( '.' + SEARCH_BUTTON_CLASS ) ) {
            return true;
        }
        
        const
            readmore_link = document.querySelector( '[data-ylk^="rsec:tpc_main;slk:headline;pos:"]' );
        
        if ( ! readmore_link ) {
            return false;
        }
        
        let
            container = create_control_container(),
            mode_control = new ModeControl(),
            button = create_button( {
                url : readmore_link.href,
                onclick : ( event ) => {
                    event.stopPropagation();
                    event.preventDefault();
                    
                    new WindowControl( readmore_link.href );
                },
            } );
        
        container.appendChild( button );
        button.after( mode_control.create_control_element() );
        readmore_link.after( container );
        
        if ( is_auto_transition_page && mode_control.is_automode ) {
            new WindowControl( readmore_link.href, {
                existing_window : window,
            } );
        }
        
        return true;
    },
    
    check_article_page = () => {
        log_debug( 'check_article_page()' );
        
        if ( document.querySelector( '.' + SEARCH_BUTTON_CLASS ) ) {
            return true;
        }
        
        const
            search_info = get_search_info();
        
        log_debug( 'search_info', search_info );
        
        if ( ! search_info ) {
            return false;
        }
        
        let
            container = create_control_container(),
            mode_control = new ModeControl(),
            button = create_button( {
                url : search_info.search_url,
                onclick : ( event ) => {
                    event.stopPropagation();
                    event.preventDefault();
                    
                    new WindowControl( search_info.search_url, {
                        child_call_parameters : {
                            hostname : search_info.hostname,
                            keyword : search_info.keyword,
                        },
                    } );
                },
            } );
        
        container.appendChild( button );
        button.after( mode_control.create_control_element() );
        search_info.site_link.after( container );
        
        if ( is_auto_transition_page && mode_control.is_automode ) {
            new WindowControl( search_info.search_url, {
                existing_window : window,
                child_call_parameters : {
                    hostname : search_info.hostname,
                    keyword : search_info.keyword,
                },
            } );
        }
        
        return true;
    },
    
    check_child_article_page = () => {
        log_debug( 'check_child_article_page()' );
        
        const
            search_info = get_search_info();
        
        log_debug( 'search_info', search_info );
        
        if ( ! search_info ) {
            return false;
        }
        
        setTimeout( () => {
            location.href = search_info.search_url;
        }, PAGE_TRANSITION_DELAY );
        
        return false;
    },
    
    check_search_page = () => {
        log_debug( 'check_search_page()' );
        
        const
            query = current_url_object.searchParams.get( 'q' ) || '',
            hostname = ( query.match( /(?:^|\s)site:([^\s]+)/ ) || [] )[ 1 ];
        
        if ( ! hostname ) {
            return true;
        }
        
        const
            site_link = [ ... document.querySelectorAll( '#rso .g > .rc > div > a, #rso .g a[ping]:not(.fl)' ) ].filter( link => {
                let url_object = new URL( link.href, location.href );
                
                if ( url_object.hostname.slice( - hostname.length ) == hostname ) {
                    return true;
                }
                
                let url = ( [ ... url_object.searchParams ].filter( param => param[ 0 ] == 'url' )[ 0 ] || [] )[ 1 ];
                
                if ( url && ( new URL( url ).hosname.slice( - hostname.length ) == hostname ) ) {
                    return true;
                }
                
                return false;
            } )[ 0 ];
        
        let name_storage = new WindowNameStorage( window, SCRIPT_NAME );
        
        name_storage.value = Object.assign( name_storage.value, {
            transition_complete : true,
        } );
        
        if ( ! site_link ) {
            current_url_object.searchParams.set( 'q', query.replace( /(^|\s)site:[^\s]+/, '$1-site:news.yahoo.co.jp' ) );
            setTimeout( () => {
                location.href = current_url_object.href;
            }, PAGE_TRANSITION_DELAY );
            return false;
        }
        
        setTimeout( () => {
            location.href = site_link.href;
        }, PAGE_TRANSITION_DELAY );
        
        return false;
    },
    
    check_page = ( () => {
        if ( /^\/pickup\//.test( current_url_object.pathname ) ) {
            return check_pickup_page;
        }
        
        if ( /^\/articles\//.test( current_url_object.pathname ) ) {
            if ( is_child_page && is_auto_transition_page ) {
                searching_icon_control.create().show();
                return check_child_article_page;
            }
            else {
                return check_article_page;
            }
        }
        
        if ( /(^www\.)?google\.com/.test( current_url_object.hostname ) && ( current_url_object.pathname == '/search' ) ) {
            if ( ! is_child_page ) {
                return null;
            }
            
            if ( ! is_auto_transition_page ) {
                return null;
            }
            
            searching_icon_control.create().show();
            
            return check_search_page;
        }
        return null;
    } )();

if ( ! check_page ) {
    return;
}

const
    insert_css_rule = () => {
        const
            css_rule_text = `
                .${CONTROL_CONTAINER_CLASS} {
                    background: lightblue !important;
                    text-align: center;
                }
                
                .${SEARCH_BUTTON_CLASS} {
                    display: inline-block !important;
                    margin: auto 8px !important;
                    text-align: center !important;
                    font-weight: bolder !important;
                    color: navy !important;
                    background: lightblue !important;
                }
                
                .${SEARCH_BUTTON_CLASS}:hover {
                    text-decoration: underline !important;
                }
                
                .${MODE_SELECTOR_CLASS} {
                    display: inline-block !important;
                    cursor: pointer;
                    font-size: 12px;
                    font-weight: bolder;
                }
                
                .${MODE_SELECTOR_CLASS} > input {
                    margin-right: 6px;
                }
                
                .${SEARCHING_CLASS} {
                    position: fixed;
                    top: 0px;
                    left: 0px;
                    z-index: 10000;
                    width: 100%;
                    height: 100%;
                    background: black;
                    opacity: 0.5;
                }
                
                .${SEARCHING_CLASS} .icon {
                    position: absolute;
                    top: 0px;
                    right: 0px;
                    bottom: 0px;
                    left: 0px;
                    margin: auto;
                    width: 100px;
                    height: 100px;
                    color: #f3a847;
                }
                
                .${SEARCHING_CLASS} .icon svg {
                    animation: searching 1.5s linear infinite;
                }
                
                @keyframes searching {
                    0% {transform: rotate(0deg);}
                    100% {transform: rotate(360deg);}
                }
                
                .${SEARCHING_CLASS}.hidden {
                    display: none;
                }
        `;
        
        let css_style = document.querySelector( '.' + CSS_STYLE_CLASS );
        
        if ( css_style ) css_style.remove();
        
        css_style = document.createElement( 'style' );
        css_style.classList.add( CSS_STYLE_CLASS );
        css_style.textContent = css_rule_text;
        
        document.querySelector( 'head' ).appendChild( css_style );
    },
    
    observer = new MutationObserver( ( records ) => {
        let stop_request = false;
        
        try {
            stop_observe();
            stop_request = check_page();
        }
        finally {
            if ( stop_request ) {
                searching_icon_control.hide();
            }
            else {
                start_observe();
            }
        }
    } ),
    start_observe = () => observer.observe( document.body, { childList : true, subtree : true } ),
    stop_observe = () => observer.disconnect();

document.body.addEventListener( 'click', ( event ) => {
    new WindowNameStorage( window, SCRIPT_NAME ).value = undefined;
}, true );

insert_css_rule();
start_observe();
check_page();

} )();