YN-SearchOrigin

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

  1. // ==UserScript==
  2. // @name YN-SearchOrigin
  3. // @name:ja Yahoo!ニュースの元記事を探す
  4. // @namespace https://furyutei.work
  5. // @license MIT
  6. // @version 0.1.16
  7. // @description Find the original article of the article of Yahoo News Japan.
  8. // @description:ja Yahoo!ニュースの記事の、元となった記事探しを助けます
  9. // @author furyu
  10. // @match https://news.yahoo.co.jp/*
  11. // @match https://www.google.com/search*
  12. // @grant none
  13. // @compatible chrome
  14. // @compatible firefox
  15. // @supportURL https://github.com/furyutei/YN-SearchOrigin/issues
  16. // @contributionURL https://memo.furyutei.work/about#send_donation
  17. // ==/UserScript==
  18.  
  19. ( async () => {
  20. 'use strict';
  21.  
  22. const
  23. SCRIPT_NAME = 'YN-SearchOrigin',
  24. DEBUG = false,
  25. IMAGE_ALT_TO_HOSTNAME_MAP = Object.assign( Object.create( null ), {
  26. 'THE PAGE' : null,
  27. '47NEWS' : 'www.47news.jp',
  28. 'テレビ朝日系(ANN)' : 'news.tv-asahi.co.jp',
  29. 'Impress Watch' : 'watch.impress.co.jp',
  30. } ),
  31. HOSTNAME_TO_VALID_HOSTNAME_MAP = Object.assign( Object.create( null ), {
  32. 'news.yahoo.co.jp' : null,
  33. 'www.watch.impress.co.jp' : 'watch.impress.co.jp',
  34. 'japanese.yonhapnews.co.kr' : 'jp.yna.co.kr',
  35. } ),
  36. CONTROL_CONTAINER_CLASS = SCRIPT_NAME + '-control-container',
  37. SEARCH_BUTTON_CLASS = SCRIPT_NAME + '-search-button',
  38. MODE_SELECTOR_CLASS = SCRIPT_NAME + '-mode-selector',
  39. SEARCHING_CLASS = SCRIPT_NAME + '-searching',
  40. CSS_STYLE_CLASS = SCRIPT_NAME + '-css-rule',
  41. SEARCH_BUTTON_TEXT = '元記事検索',
  42. MODE_SELECTOR_AUTO_TEXT = '自動',
  43. PAGE_TRANSITION_DELAY = 800, // TODO: Chromeで、ページ遷移までの時間が短すぎると(?) history に記録されない場合がある模様→止むをえず、遅延させている
  44. self = undefined,
  45. format_date = ( date, format, is_utc ) => {
  46. if ( ! format ) {
  47. format = 'YYYY-MM-DD hh:mm:ss.SSS';
  48. }
  49. let msec = ( '00' + ( ( is_utc ) ? date.getUTCMilliseconds() : date.getMilliseconds() ) ).slice( -3 ),
  50. msec_index = 0;
  51. if ( is_utc ) {
  52. format = format
  53. .replace( /YYYY/g, date.getUTCFullYear() )
  54. .replace( /MM/g, ( '0' + ( 1 + date.getUTCMonth() ) ).slice( -2 ) )
  55. .replace( /DD/g, ( '0' + date.getUTCDate() ).slice( -2 ) )
  56. .replace( /hh/g, ( '0' + date.getUTCHours() ).slice( -2 ) )
  57. .replace( /mm/g, ( '0' + date.getUTCMinutes() ).slice( -2 ) )
  58. .replace( /ss/g, ( '0' + date.getUTCSeconds() ).slice( -2 ) )
  59. .replace( /S/g, ( all ) => {
  60. return msec.charAt( msec_index ++ );
  61. } );
  62. }
  63. else {
  64. format = format
  65. .replace( /YYYY/g, date.getFullYear() )
  66. .replace( /MM/g, ( '0' + ( 1 + date.getMonth() ) ).slice( -2 ) )
  67. .replace( /DD/g, ( '0' + date.getDate() ).slice( -2 ) )
  68. .replace( /hh/g, ( '0' + date.getHours() ).slice( -2 ) )
  69. .replace( /mm/g, ( '0' + date.getMinutes() ).slice( -2 ) )
  70. .replace( /ss/g, ( '0' + date.getSeconds() ).slice( -2 ) )
  71. .replace( /S/g, ( all ) => {
  72. return msec.charAt( msec_index ++ );
  73. } );
  74. }
  75. return format;
  76. },
  77. get_gmt_datetime = ( time, is_msec ) => {
  78. let date = new Date( ( is_msec ) ? time : 1000 * time );
  79. return format_date( date, 'YYYY-MM-DD_hh:mm:ss_GMT', true );
  80. },
  81. get_log_timestamp = () => format_date( new Date() ),
  82. log_debug = ( ... args ) => {
  83. if ( ! DEBUG ) {
  84. return;
  85. }
  86. console.debug( '%c[' + SCRIPT_NAME + '] ' + get_log_timestamp(), 'color: gray;', ... args );
  87. },
  88. log = ( ... args ) => {
  89. console.log( '%c[' + SCRIPT_NAME + '] ' + get_log_timestamp(), 'color: teal;', ... args );
  90. },
  91. log_info = ( ... args ) => {
  92. console.info( '%c[' + SCRIPT_NAME + '] ' + get_log_timestamp(), 'color: darkslateblue;', ... args );
  93. },
  94. log_error = ( ... args ) => {
  95. console.error( '%c[' + SCRIPT_NAME + '] ' + get_log_timestamp(), 'color: purple;', ... args );
  96. },
  97. current_url_object = new URL( location.href ),
  98. WindowNameStorage = class {
  99. constructor( target_window, storage_name ) {
  100. const
  101. self = this;
  102. self.init( target_window, storage_name );
  103. }
  104. init( target_window, storage_name ) {
  105. const
  106. self = this;
  107. Object.assign( self, {
  108. target_window,
  109. storage_name,
  110. } );
  111. return self;
  112. }
  113. get value() {
  114. const
  115. self = this,
  116. target_window = self.target_window,
  117. storage_name = self.storage_name;
  118. if ( ( ! target_window ) || ( ! storage_name ) ) {
  119. return {};
  120. }
  121. try {
  122. return JSON.parse( target_window.name )[ storage_name ] || {};
  123. }
  124. catch ( error ) {
  125. return {};
  126. }
  127. }
  128. set value( spec_value ) {
  129. this.target_window.name = this.get_name( spec_value );
  130. }
  131. get_name( spec_value ) {
  132. const
  133. self = this,
  134. target_window = self.target_window,
  135. storage_name = self.storage_name;
  136. let original_name_params = {};
  137. if ( target_window ) {
  138. try {
  139. original_name_params = JSON.parse( target_window.name );
  140. if ( ! ( original_name_params instanceof Object ) ) {
  141. original_name_params = {};
  142. }
  143. }
  144. catch ( error ) {
  145. original_name_params = {};
  146. }
  147. }
  148. try {
  149. if ( storage_name ) {
  150. if ( spec_value === undefined ) {
  151. delete original_name_params[ storage_name ];
  152. }
  153. else {
  154. original_name_params[ storage_name ] = spec_value;
  155. }
  156. }
  157. return JSON.stringify( original_name_params );
  158. }
  159. catch ( error ) {
  160. return '';
  161. }
  162. }
  163. },
  164. WindowControl = class {
  165. constructor( url = null, options = {} ) {
  166. const
  167. self = this;
  168. self.initial_url = url;
  169. self.child_window_counter = 0;
  170. self.existing_window = null;
  171. if ( ! url ) {
  172. return;
  173. }
  174. self.open( url, options );
  175. }
  176. open( url, options ) {
  177. const
  178. self = this;
  179. if ( ! options ) {
  180. options = {};
  181. }
  182. let child_window = options.existing_window || self.existing_window;
  183. if ( ! options.child_call_parameters ) {
  184. options.child_call_parameters = {};
  185. }
  186. try {
  187. Object.assign( options.child_call_parameters, {
  188. script_name : SCRIPT_NAME,
  189. child_window_id : '' + ( new Date().getTime() ) + '-' + ( ++ self.child_window_counter ),
  190. transition_complete : false,
  191. } );
  192. }
  193. catch ( error ) {
  194. log_error( error );
  195. }
  196. if ( child_window ) {
  197. new WindowNameStorage( child_window, SCRIPT_NAME ).value = options.child_call_parameters;
  198. if ( child_window.location.href != url ) {
  199. setTimeout( () => {
  200. child_window.location.href = url;
  201. }, PAGE_TRANSITION_DELAY );
  202. }
  203. }
  204. else {
  205. child_window = window.open( url, new WindowNameStorage( null, SCRIPT_NAME ).get_name( options.child_call_parameters ) );
  206. //new WindowNameStorage( child_window, SCRIPT_NAME ).value = options.child_call_parameters;
  207. }
  208. self.existing_window = child_window;
  209. return self;
  210. }
  211. close() {
  212. const
  213. self = this;
  214. if ( ! self.existing_window ) {
  215. return self;
  216. }
  217. try {
  218. self.existing_window.close();
  219. }
  220. catch ( error ) {
  221. }
  222. self.existing_window = null;
  223. return self;
  224. }
  225. },
  226. ModeControl = class {
  227. constructor() {
  228. this.storage_mode_info_name = SCRIPT_NAME + '-mode_info';
  229. this.load_mode_info();
  230. }
  231. load_mode_info() {
  232. try {
  233. this.mode_info = JSON.parse( localStorage.getItem( this.storage_mode_info_name ) );
  234. }
  235. catch ( error ) {
  236. }
  237. if ( ! this.mode_info ) {
  238. this.mode_info = {};
  239. }
  240. if ( Object.keys( this.mode_info ).length <= 0 ) {
  241. this.mode_info = {
  242. is_automode : false,
  243. };
  244. }
  245. }
  246. save_mode_info() {
  247. localStorage.setItem( this.storage_mode_info_name, JSON.stringify( this.mode_info ) );
  248. }
  249. get is_automode() {
  250. return this.mode_info.is_automode;
  251. }
  252. set is_automode( specified_mode ) {
  253. this.mode_info.is_automode = !! specified_mode;
  254. this.save_mode_info();
  255. }
  256. create_control_element() {
  257. const
  258. self = this,
  259. control_element = document.createElement( 'label' ),
  260. automode_checkbox = document.createElement( 'input' );
  261. control_element.className = MODE_SELECTOR_CLASS;
  262. control_element.textContent = MODE_SELECTOR_AUTO_TEXT;
  263. automode_checkbox.type = 'checkbox';
  264. automode_checkbox.checked = self.is_automode;
  265. automode_checkbox.addEventListener( 'change', ( event ) => {
  266. event.stopPropagation();
  267. event.preventDefault();
  268. self.is_automode = automode_checkbox.checked;
  269. } );
  270. control_element.firstChild.before( automode_checkbox );
  271. return control_element;
  272. }
  273. },
  274. searching_icon_control = new class {
  275. constructor() {
  276. const
  277. self = this;
  278. self.searching_container = null;
  279. }
  280. create() {
  281. const
  282. self = this;
  283. if ( self.searching_container ) {
  284. return self;
  285. }
  286. const
  287. 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" />',
  288. searchin_icon = document.createElement( 'div' ),
  289. searching_container = self.searching_container = document.createElement( 'div' );
  290. searchin_icon.className = 'icon';
  291. searchin_icon.insertAdjacentHTML( 'beforeend', searching_icon_svg );
  292. searching_container.className = SEARCHING_CLASS;
  293. searching_container.appendChild( searchin_icon );
  294. document.documentElement.appendChild( searching_container );
  295. return self;
  296. }
  297. hide() {
  298. const
  299. self = this;
  300. if ( ! self.searching_container ) {
  301. return self;
  302. }
  303. self.searching_container.classList.add( 'hidden' );
  304. return self;
  305. }
  306. show() {
  307. const
  308. self = this;
  309. if ( ! self.searching_container ) {
  310. return self;
  311. }
  312. self.searching_container.classList.remove( 'hidden' );
  313. return self;
  314. }
  315. },
  316. get_search_hostname = ( site_link ) => {
  317. let image_alt = ( site_link.querySelector( 'img[alt]' ) || {} ).alt,
  318. hostname = ( image_alt in IMAGE_ALT_TO_HOSTNAME_MAP ) ? IMAGE_ALT_TO_HOSTNAME_MAP[ image_alt ] : new URL( site_link.href ).hostname;
  319. if ( hostname ) {
  320. hostname = hostname.replace( /^www\./, '' );
  321. }
  322. if ( hostname && ( hostname in HOSTNAME_TO_VALID_HOSTNAME_MAP ) ) {
  323. hostname = HOSTNAME_TO_VALID_HOSTNAME_MAP[ hostname ];
  324. }
  325. return hostname;
  326. },
  327. get_search_info = () => {
  328. const
  329. site_link = document.querySelector( 'main[id="contents"] div[id="contentsWrap"] > article > header > div > div:last-child > a' );
  330. if ( ! site_link ) {
  331. return null;
  332. }
  333. const
  334. hostname = get_search_hostname( site_link );
  335. if ( ! hostname ) {
  336. return null;
  337. }
  338. const
  339. 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();
  340. return {
  341. site_link,
  342. hostname,
  343. keyword,
  344. search_url : 'https://www.google.com/search?ie=UTF-8&q=' + encodeURIComponent( 'site:' + hostname + ' ' + keyword ),
  345. };
  346. },
  347. create_control_container = ( parameters ) => {
  348. if ( ! parameters ) {
  349. parameters = {};
  350. }
  351. let container = document.createElement( 'div' );
  352. container.className = CONTROL_CONTAINER_CLASS;
  353. return container;
  354. },
  355. create_button = ( parameters ) => {
  356. if ( ! parameters ) {
  357. parameters = {};
  358. }
  359. let button = document.createElement( 'a' );
  360. button.className = SEARCH_BUTTON_CLASS;
  361. button.textContent = SEARCH_BUTTON_TEXT;
  362. button.href = parameters.url ? parameters.url : '#';
  363. if ( parameters.onclick ) {
  364. button.addEventListener( 'click', parameters.onclick );
  365. }
  366. return button;
  367. },
  368. child_called_parameters = new WindowNameStorage( window, SCRIPT_NAME ).value,
  369. is_child_page = child_called_parameters.script_name,
  370. is_auto_transition_page = ! child_called_parameters.transition_complete,
  371. check_pickup_page = () => {
  372. log_debug( 'check_pickup_page()' );
  373. if ( document.querySelector( '.' + SEARCH_BUTTON_CLASS ) ) {
  374. return true;
  375. }
  376. const
  377. readmore_link = document.querySelector( '[data-ylk^="rsec:tpc_main;slk:headline;pos:"]' );
  378. if ( ! readmore_link ) {
  379. return false;
  380. }
  381. let
  382. container = create_control_container(),
  383. mode_control = new ModeControl(),
  384. button = create_button( {
  385. url : readmore_link.href,
  386. onclick : ( event ) => {
  387. event.stopPropagation();
  388. event.preventDefault();
  389. new WindowControl( readmore_link.href );
  390. },
  391. } );
  392. container.appendChild( button );
  393. button.after( mode_control.create_control_element() );
  394. readmore_link.after( container );
  395. if ( is_auto_transition_page && mode_control.is_automode ) {
  396. new WindowControl( readmore_link.href, {
  397. existing_window : window,
  398. } );
  399. }
  400. return true;
  401. },
  402. check_article_page = () => {
  403. log_debug( 'check_article_page()' );
  404. if ( document.querySelector( '.' + SEARCH_BUTTON_CLASS ) ) {
  405. return true;
  406. }
  407. const
  408. search_info = get_search_info();
  409. log_debug( 'search_info', search_info );
  410. if ( ! search_info ) {
  411. return false;
  412. }
  413. let
  414. container = create_control_container(),
  415. mode_control = new ModeControl(),
  416. button = create_button( {
  417. url : search_info.search_url,
  418. onclick : ( event ) => {
  419. event.stopPropagation();
  420. event.preventDefault();
  421. new WindowControl( search_info.search_url, {
  422. child_call_parameters : {
  423. hostname : search_info.hostname,
  424. keyword : search_info.keyword,
  425. },
  426. } );
  427. },
  428. } );
  429. container.appendChild( button );
  430. button.after( mode_control.create_control_element() );
  431. search_info.site_link.after( container );
  432. if ( is_auto_transition_page && mode_control.is_automode ) {
  433. new WindowControl( search_info.search_url, {
  434. existing_window : window,
  435. child_call_parameters : {
  436. hostname : search_info.hostname,
  437. keyword : search_info.keyword,
  438. },
  439. } );
  440. }
  441. return true;
  442. },
  443. check_child_article_page = () => {
  444. log_debug( 'check_child_article_page()' );
  445. const
  446. search_info = get_search_info();
  447. log_debug( 'search_info', search_info );
  448. if ( ! search_info ) {
  449. return false;
  450. }
  451. setTimeout( () => {
  452. location.href = search_info.search_url;
  453. }, PAGE_TRANSITION_DELAY );
  454. return false;
  455. },
  456. check_search_page = () => {
  457. log_debug( 'check_search_page()' );
  458. const
  459. query = current_url_object.searchParams.get( 'q' ) || '',
  460. hostname = ( query.match( /(?:^|\s)site:([^\s]+)/ ) || [] )[ 1 ];
  461. if ( ! hostname ) {
  462. return true;
  463. }
  464. const
  465. site_link = [ ... document.querySelectorAll( '#rso .g > .rc > div > a, #rso .g a[ping]:not(.fl)' ) ].filter( link => {
  466. let url_object = new URL( link.href, location.href );
  467. if ( url_object.hostname.slice( - hostname.length ) == hostname ) {
  468. return true;
  469. }
  470. let url = ( [ ... url_object.searchParams ].filter( param => param[ 0 ] == 'url' )[ 0 ] || [] )[ 1 ];
  471. if ( url && ( new URL( url ).hosname.slice( - hostname.length ) == hostname ) ) {
  472. return true;
  473. }
  474. return false;
  475. } )[ 0 ];
  476. let name_storage = new WindowNameStorage( window, SCRIPT_NAME );
  477. name_storage.value = Object.assign( name_storage.value, {
  478. transition_complete : true,
  479. } );
  480. if ( ! site_link ) {
  481. current_url_object.searchParams.set( 'q', query.replace( /(^|\s)site:[^\s]+/, '$1-site:news.yahoo.co.jp' ) );
  482. setTimeout( () => {
  483. location.href = current_url_object.href;
  484. }, PAGE_TRANSITION_DELAY );
  485. return false;
  486. }
  487. setTimeout( () => {
  488. location.href = site_link.href;
  489. }, PAGE_TRANSITION_DELAY );
  490. return false;
  491. },
  492. check_page = ( () => {
  493. if ( /^\/pickup\//.test( current_url_object.pathname ) ) {
  494. return check_pickup_page;
  495. }
  496. if ( /^\/articles\//.test( current_url_object.pathname ) ) {
  497. if ( is_child_page && is_auto_transition_page ) {
  498. searching_icon_control.create().show();
  499. return check_child_article_page;
  500. }
  501. else {
  502. return check_article_page;
  503. }
  504. }
  505. if ( /(^www\.)?google\.com/.test( current_url_object.hostname ) && ( current_url_object.pathname == '/search' ) ) {
  506. if ( ! is_child_page ) {
  507. return null;
  508. }
  509. if ( ! is_auto_transition_page ) {
  510. return null;
  511. }
  512. searching_icon_control.create().show();
  513. return check_search_page;
  514. }
  515. return null;
  516. } )();
  517.  
  518. if ( ! check_page ) {
  519. return;
  520. }
  521.  
  522. const
  523. insert_css_rule = () => {
  524. const
  525. css_rule_text = `
  526. .${CONTROL_CONTAINER_CLASS} {
  527. background: lightblue !important;
  528. text-align: center;
  529. }
  530. .${SEARCH_BUTTON_CLASS} {
  531. display: inline-block !important;
  532. margin: auto 8px !important;
  533. text-align: center !important;
  534. font-weight: bolder !important;
  535. color: navy !important;
  536. background: lightblue !important;
  537. }
  538. .${SEARCH_BUTTON_CLASS}:hover {
  539. text-decoration: underline !important;
  540. }
  541. .${MODE_SELECTOR_CLASS} {
  542. display: inline-block !important;
  543. cursor: pointer;
  544. font-size: 12px;
  545. font-weight: bolder;
  546. }
  547. .${MODE_SELECTOR_CLASS} > input {
  548. margin-right: 6px;
  549. }
  550. .${SEARCHING_CLASS} {
  551. position: fixed;
  552. top: 0px;
  553. left: 0px;
  554. z-index: 10000;
  555. width: 100%;
  556. height: 100%;
  557. background: black;
  558. opacity: 0.5;
  559. }
  560. .${SEARCHING_CLASS} .icon {
  561. position: absolute;
  562. top: 0px;
  563. right: 0px;
  564. bottom: 0px;
  565. left: 0px;
  566. margin: auto;
  567. width: 100px;
  568. height: 100px;
  569. color: #f3a847;
  570. }
  571. .${SEARCHING_CLASS} .icon svg {
  572. animation: searching 1.5s linear infinite;
  573. }
  574. @keyframes searching {
  575. 0% {transform: rotate(0deg);}
  576. 100% {transform: rotate(360deg);}
  577. }
  578. .${SEARCHING_CLASS}.hidden {
  579. display: none;
  580. }
  581. `;
  582. let css_style = document.querySelector( '.' + CSS_STYLE_CLASS );
  583. if ( css_style ) css_style.remove();
  584. css_style = document.createElement( 'style' );
  585. css_style.classList.add( CSS_STYLE_CLASS );
  586. css_style.textContent = css_rule_text;
  587. document.querySelector( 'head' ).appendChild( css_style );
  588. },
  589. observer = new MutationObserver( ( records ) => {
  590. let stop_request = false;
  591. try {
  592. stop_observe();
  593. stop_request = check_page();
  594. }
  595. finally {
  596. if ( stop_request ) {
  597. searching_icon_control.hide();
  598. }
  599. else {
  600. start_observe();
  601. }
  602. }
  603. } ),
  604. start_observe = () => observer.observe( document.body, { childList : true, subtree : true } ),
  605. stop_observe = () => observer.disconnect();
  606.  
  607. document.body.addEventListener( 'click', ( event ) => {
  608. new WindowNameStorage( window, SCRIPT_NAME ).value = undefined;
  609. }, true );
  610.  
  611. insert_css_rule();
  612. start_observe();
  613. check_page();
  614.  
  615. } )();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址