NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name TiddlyWiki5: Combine TW5 and search engine results // @description Combine TiddlyWiki and your preferred search engine to find your own answers more easily // @version 1.2.3 // @author bimlas + Lin Onetwo // @supportURL https://github.com/linonetwo/tiddlywiki-search-tw5-and-search-engine-at-once-user-script/issues // @downloadURL https://raw.github.com/linonetwo/tiddlywiki-search-tw5-and-search-engine-at-once-user-script/master/combine-tw5-and-search-engine-results.user.js // @updateURL https://raw.github.com/linonetwo/tiddlywiki-search-tw5-and-search-engine-at-once-user-script/master/combine-tw5-and-search-engine-results.user.js // @icon https://tiddlywiki.com/favicon.ico // @namespace https://github.com/linonetwo // // @license MIT // // @run-at document-end // @require https://openuserjs.org/src/libs/sizzle/GM_config.js // @match *://www.google.com/search* // @match *://cn.bing.com/* // @match *://www.bing.com/* // @match *://www.baidu.com/* // @match *://www.startpage.com/* // @match *://duckduckgo.com/* // @match *://www.ecosia.org/search* // @grant GM_xmlhttpRequest // @grant GM.getValue // @grant GM.setValue // ==/UserScript== // READ THE DOCUMENTATION BEFORE TRYING TO USE THE SCRIPT! // https://github.com/linonetwo/tiddlywiki-search-tw5-and-search-engine-at-once-user-script // This is a fork of https://github.com/bimlas/userscript-combine-tw5-and-search-engine-results let gmc = new GM_config({ 'id': 'wikiConfig', // The id used for this instance of GM_config 'fields': // Fields object { 'wikis': // This is the id of the field { 'label': 'Wiki List (Each in a new line)', // Appears next to field 'type': 'textarea', // Makes this setting a text field 'default': 'http://localhost:5212' // Default value if user doesn't change it }, 'searchFilter': { 'label': 'Search sub filter (You need to enable it, see https://tiddlywiki.com/#WebServer%20API%3A%20Get%20All%20Tiddlers ) (only search title if not enabled this way)', 'labelPos': 'above', 'type': 'textarea', // Makes this setting a text field 'default': '[!is[shadow]!is[system]!field:calendarEntry[yes]search[${query}]]' // Default value if user doesn't change it } } }); // Promise resolves when initialization completes let onInit = config => new Promise(resolve => { let isInit = () => setTimeout(() => config.isInit ? resolve() : isInit(), 0); isInit(); }); let init = onInit(gmc); init.then(() => { const wikis = gmc.get('wikis').split('\n').filter(Boolean); const searchFilter = gmc.get('searchFilter'); const buildWikiFilter = function(query) { const parts = searchFilter.split('${query}'); return `${parts[0]}${query}${parts[1]}`; } // if allow all filter is not enabled, use fallback filter // https://tiddlywiki.com/#WebServer%20API%3A%20Get%20All%20Tiddlers const buildWikiFilterFallback = function(query) { return `[all[tiddlers]!is[system]sort[title]]`; } // NOTE: If you want to show results in the sidebar, change this option to // 'sidebar', but remember that the sidebar is not always visible (for example, // if the window is too narrow). const placementOfResults = 'sidebar'; const searchEngineConfigs = { 'www.google.com': { searchInputSelector: 'textarea[name=q]', searchResultsSelector: { main: '#center_col', // better use id, so if it no exist, we can recreate one sidebar: '#rhs' } }, 'cn.bing.com': { searchInputSelector: 'input#sb_form_q', searchResultsSelector: { main: '#b_results', sidebar: '#b_context' } }, 'www.bing.com': { searchInputSelector: 'input#sb_form_q', searchResultsSelector: { main: '#b_results', sidebar: '#b_context' } }, 'www.baidu.com': { searchInputSelector: 'input#kw', searchResultsSelector: { main: '#content_left', sidebar: '#content_right' } }, // StartPage changes its URL and website structure, so the script does not work in all cases 'www.startpage.com': { searchInputSelector: '#q', searchResultsSelector: { main: 'div.mainline-results', sidebar: 'div.sidebar-results' } }, 'duckduckgo.com': { searchInputSelector: 'input[name=q]', searchResultsSelector: { main: '#links.results', sidebar: 'div.sidebar-modules' } }, 'www.ecosia.org': { searchInputSelector: 'input[name=q]', searchResultsSelector: { main: 'div.mainline', sidebar: 'div.sidebar' } }, } const searchEngine = searchEngineConfigs[document.domain]; function fetchJSON(origin, url) { return new Promise((resolve, reject) => { try { GM.xmlHttpRequest({ method: "GET", headers: { "Origin": origin, }, url: url, onload: function(response) { if (response.status !== 200) { return reject() } resolve(JSON.parse(response.responseText)); }, onerror: (error) => {console.error(error); reject(error)}, onabort: (error) => {console.error(error); reject(error)}, }); } catch (error) { console.error(error); reject(error) } }); } function getTiddlerLink(wiki, title) { const urlEncodedTitle = encodeURIComponent(title); const singleViewUrl = `${wiki}/${urlEncodedTitle}`; const normalViewUrl = `${wiki}/#${urlEncodedTitle}`; return `<a href="${singleViewUrl}">${title}</a> (<a href="${normalViewUrl}">#</a>)`; } function getWikiTitle(wiki) { return new Promise((resolve, reject) => { const urlEncodedQuery = encodeURIComponent('$:/SiteTitle'); const url = `${wiki}/recipes/default/tiddlers/${urlEncodedQuery}`; fetchJSON(wiki, url) .then(results => { resolve(results.text); }); }); } let searchEngineResults = document.querySelector(searchEngine.searchResultsSelector[placementOfResults]); // google remove the sidebar, we have to create one manually if (searchEngineResults === null && placementOfResults === 'sidebar') { const mainContentParent = document.querySelector('#center_col').parentElement; const sidebarElement = document.createElement('div'); sidebarElement.id = searchEngine.searchResultsSelector[placementOfResults].replace('#', ''); mainContentParent.appendChild(sidebarElement); searchEngineResults = sidebarElement; } let configButtonAdded = false; function makeConfigButton() { const button = document.createElement('button'); button.innerText = "⚙️TW"; button.style = "background-color: rgba(255, 255, 255, 0.05);border: none;cursor: pointer;"; button.onclick = () => gmc.open(); return button; } if (!configButtonAdded) { searchEngineResults.prepend(makeConfigButton()); configButtonAdded = true; } function addToPage(text) { const resultContainer = document.createElement('div'); resultContainer.style.display = 'inline-flex'; resultContainer.style.flexDirection = 'column'; resultContainer.style.margin = '1em'; resultContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.05)'; resultContainer.innerHTML = text; searchEngineResults.append(resultContainer); } function makeHtmlListFromTiddlers(wiki, listOfTiddlers) { const htmlList = listOfTiddlers.reduce((text, tiddler) => { return text + `<li>${getTiddlerLink(wiki, tiddler.title)}</li>`; }, ''); return `<ul>${htmlList}</ul>`; } const query = document.querySelector(searchEngine.searchInputSelector).value; const urlEncodedQuery = encodeURIComponent(buildWikiFilter(query)); let searchResults = ''; wikis.forEach(wiki => { const url = `${wiki}/recipes/default/tiddlers.json?filter=${urlEncodedQuery}`; Promise.all([ fetchJSON(wiki, url).catch(() => { const query = document.querySelector(searchEngine.searchInputSelector).value; const urlEncodedQuery = encodeURIComponent(buildWikiFilterFallback(query)); const url = `${wiki}/recipes/default/tiddlers.json?filter=${urlEncodedQuery}`; return fetchJSON(wiki, url).then(results => { // sort manually, because can't sort on server using filter, user not allow it using https://tiddlywiki.com/#WebServer%20API%3A%20Get%20All%20Tiddlers return results.filter(tiddler => tiddler.title.includes(query)); }); }), getWikiTitle(wiki) ]) .then(([results, wikiTitle]) => { if(!results.length) return; const wikiLink = `<small><a href="${wiki}">${wiki}</a></small>`; const header = `<h3>${wikiTitle}</h3>${wikiLink}<p>`; addToPage(`<div style="margin: 1em;">${header}<p>${makeHtmlListFromTiddlers(wiki, results)}</p><div>`); }); }); });