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