generated at
類似したタイトルのページを関連ページとして表示する

現在見ているページとタイトルが似ているページのリストを関連ページに追加するUserScriptです
リンクが繋がっているページはちゃんと除外される
scrapbox.Project.pagesにはタイトルしかないので、タイトルだけの簡易的な表示です
リンクは繋がっていないが関係のあるページを発見できることがあってたのしい
表記ゆれで同じようなページが複数作られたりしてても把握できるかも

すごいtakker😸erniogipollenjp

今のバージョンだと、モバイル版ではクリックできるが、PCではクリックできない
ページカードが高速にappend・destroyされているからだと思う
右クリックで開くのはできる
Page Historyにある一番古いバージョンは普通にできる

使い方
以下を自分のページに書く
js
import('/api/code/scrapboxlab/類似したタイトルのページを関連ページとして表示する/script.js');

script.js
if (!document.getElementById('asearch-list-grid')) { $('.page-wrapper .related-page-list.clearfix').append( '<ul class="grid" id="asearch-list-grid">' + '<li class="splitter" style="height: 30px !important" />' + '<li class="relation-label"><a><span class="title">Similar Pages</span>' + '<span class="kamon kamon-search icon-lg"/></a><span class="arrow"/></li></ul>' ); const grid = $('#asearch-list-grid'); let worker = new Worker('/api/code/scrapboxlab/類似したタイトルのページを関連ページとして表示する/worker.js'); worker.onmessage = (e) => { resetList(); const fragment = $(document.createDocumentFragment()); for (let { exists, title } of e.data) { const item = $(`<li class="page-list-item grid-style-item${ exists ? '' : ' empty' }"> <a href="/${scrapbox.Project.name}/${encodeURIComponent(title.replace(' ', '_'))}" rel="route"> <div class="hover"></div> <div class="content"> <div class="header"><div class="title"></div></div> <div class="description"><div class="line-img"> <div></div><div></div><div></div><div></div><div></div> </div></div> </div> </a>`); $('.title', item).text(title); fragment.append(item); } grid.append(fragment); }; let prevId = null; let titleLcMap; const observer = new MutationObserver(() => updateIfPage()); observer.observe($('title')[0], { childList: true }); observer.observe($('div.page-wrapper > div.related-page-list.clearfix > ul.grid:nth(0)')[0], { childList: true }); updateIfPage(); update(location.pathname); function resetList() { grid.children().slice(2).remove(); } function regenTitleLcMap() { titleLcMap = {}; for (let p of scrapbox.Project.pages) { titleLcMap[p.titleLc] = p.title; } } function pathToTitle(path) { const a = path.replace(/#.*$/, '').split('/'); if (a.length === 3 && a[2].length > 0) { const title = decodeURIComponent(a[2]); // Project.pagesを元にタイトル取得 // (`_`が実際は空白なのか`_`なのか分からないため) const titleLc = title.toLowerCase(); return titleLcMap[titleLc]; } return null; } function update() { if (scrapbox.Page.title === 'new') { resetList(); return; } // 既に関連リンクに入っているページのタイトルを取得する let links = new Set(); regenTitleLcMap(); $('div.page-wrapper > div.related-page-list.clearfix > ul.grid:nth(0) > li a').each( (i, e) => { links.add(pathToTitle(e.pathname)); } ); worker.postMessage({ title: scrapbox.Page.title, pages: scrapbox.Project.pages, links, replace: prevId === scrapbox.Page.id, // ページタイトルを変更中の場合 prevId, }); } function updateIfPage() { if (scrapbox.Layout !== 'page') return; update(); prevId = scrapbox.Page.id; } }

/villagepump/pin-diaryを参考にページ遷移検知処理を簡略化しました 2020/11/9
あと、Scrapbox側の関連ページリストが更新されたらこのスクリプトが生成するリストも更新されるようになったはず(以前はページ遷移時のみでした)

worker.js
// Asearch (https://github.com/shokai/node-asearch) const Asearch=function(){var t,i,s;function p(i){var p,r,n,o,e,h,u;for(this.source=i,this.shiftpat=[],this.epsilon=0,this.acceptpat=0,e=t,p=r=0,h=s;0<=h?r<h:r>h;p=0<=h?++r:--r)this.shiftpat[p]=0;for(n=0,o=(u=this.unpack(this.source)).length;n<o;n++)32===(p=u[n])?this.epsilon|=e:(this.shiftpat[p]|=e,this.shiftpat[this.toupper(p)]|=e,this.shiftpat[this.tolower(p)]|=e,e>>>=1);return this.acceptpat=e,this}return s=256,i=[t=2147483648,0,0,0],p.prototype.isupper=function(t){return t>=65&&t<=90},p.prototype.islower=function(t){return t>=97&&t<=122},p.prototype.tolower=function(t){return this.isupper(t)?t+32:t},p.prototype.toupper=function(t){return this.islower(t)?t-32:t},p.prototype.state=function(t,s){var p,r,n,o,e,h,u,a,c;for(null==t&&(t=i),null==s&&(s=""),n=t[0],o=t[1],e=t[2],h=t[3],r=0,u=(c=this.unpack(s)).length;r<u;r++)p=c[r],a=this.shiftpat[p],h=h&this.epsilon|(h&a)>>>1|e>>>1|e,e=e&this.epsilon|(e&a)>>>1|o>>>1|o,o=o&this.epsilon|(o&a)>>>1|n>>>1|n,h|=(e|=(o|=(n=n&this.epsilon|(n&a)>>>1)>>>1)>>>1)>>>1;return[n,o,e,h]},p.prototype.match=function(t,s){var p;return null==s&&(s=0),p=this.state(i,t),s<i.length||(s=i.length-1),0!=(p[s]&this.acceptpat)},p.prototype.unpack=function(t){var i,s,p,r,n;for(i=[],p=0,r=(n=t.split("")).length;p<r;p++)(s=n[p].charCodeAt(0))>255&&i.push((65280&s)>>>8),i.push(255&s);return i},p}(); onmessage = (e) => { const { title, pages, links, replace, prevId } = e.data; const search = new Asearch(` ${title} `); const ambig = Math.min( title.split('').every((s) => s.charCodeAt(0) < 0xff) ? 1 : 2, title.length - 3 ); const titleLc = title.replace(/ /g, '_').toLowerCase(); postMessage([...new Set(pages.flatMap(p => { if (p.title === title) return []; if (replace && p.id === prevId) return []; if (!( p.titleLc.includes(titleLc) || titleLc.includes(p.titleLc) || (ambig >= 0 && search.match(p.title, ambig)) )) return []; return [{ exists: p.exists, title: p.title }]; }))]); };