YN-SearchOrigin

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();

} )();