🏠 Home 

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