generated at
advanced-related-pages
Next version
hr
データ構造は変えてある

2020-11-14 13:15:00 scrapbox-text-bubble用のhtmlを生成する函数を実装した
2020-11-12 03:32:19 debug情報を表示する函数を作った
2020-11-10 17:06:00 現在いるページが吹き出し表示されないようにした
2020-11-10 16:45:24 重複除去処理を変えた
JSON.stringify でうまく行かなかったので、正規表現に直した
2020-11-10 14:06:38 classを使うようにした

実装したいこと
ページリストの更新
RelatedPageManagerに更新機能を組み込んでおく
ページリストを読み込み中であることを表示したい
こんな感じ
RelatedPageManagerに読み込み中かどうかを示すpropertyを生やす
scrapbox-card-bubble側でそのflagを読み込み、表示する
どう表示するかはscrapbox-card-bubbleに任せる
逆リンク計算を遅延させる
指定したページにあるリンクの逆リンクのみを計算する
計算はページを読み込んだときと本文のあるリンクをhoverしたときに実行するようにする
計算はbackgroundに任せる
計算中もカードを表示できるようにするため
ひらがなカタカナ、全角半角、大文字小文字の違いを無視する
大文字小文字無視は標準でつける
全角半角スペースとアンダーバーの違いも無視する
ひらがなカタカナ無視はどうしよう
入れなくてもいいかもしれない

script.js
import {createPageCard} from '/api/code/takker/scrapboxのページカードを作成するscript/script.js'; import {convertSb2HTML} from '/api/code/takker/convert_scrapbox_to_html/script.js'; class PageCache { constructor({project, title, descriptions, imageUrl}){ this.project = project; this.title = title, this.cardHtml = createPageCard({ project: project, title: title, description: descriptions.join('\n'), imageUrl: imageUrl}); this.parsedHtml = undefined; } } export class RelatedPageManager { constructor({projects}) { this.pageCache = []; this.LinkCache = []; this.BackLinksData =[]; this.projects = projects; this._worker = new Worker('/api/code/takker/advanced-related-pages/link-calc-worker.js'); this._worker.addEventListener('message', message => { this.LinkCache = message.data.linkCache; this.BackLinksData = message.data.backLinksData; //console.log(this.LinkCache); //console.log(this.BackLinksData); }); this.current = {project: scrapbox.Project.name, title: undefined, links1hop:[]}; }
data構造
ts
interface LinkCache { project: string, title: string, links: { project: string, title: string, }[]; }
ts
interface BackLinksData { project: string, title: string, backLinks: { project: string, title: string, }[]; }

script.js
async start(projects) { this._loadLinkCache(); await Promise.all(this.projects.map(project => this._loadAllPages(project))); } async _loadAllPages(project) { // projectの全ページ数を取得する const pageNum = await fetch(`/api/pages/${project}/?limit=1`) .then(response => response.json()) .then(json => parseInt(json.count)); const maxIndex = Math.floor(pageNum / 1000) + 1; const promises = [...Array(maxIndex)].map(async (_, index) => { const json = await fetch(`/api/pages/${project}/?limit=1000&skip=${index*1000}`) .then(res => res.json()); const pages = json.pages; pages.forEach(page => this.pageCache.push(new PageCache({ project: project, title: page.title, descriptions: page.descriptions, imageUrl: page.image}))); }); await Promise.all(promises); this._log(`Loaded ${this.pageCache.length} page data from /${project}.`); } _loadLinkCache() { //全部workerに投げる this._worker.postMessage({job: 'initialize', projects: this.projects}); } // 指定したページ中のリンクの2 hop linksのページカードを取得する async getCardHtmls({project,title,link1hop}) { if (!this.projects.includes(link1hop.project)) { return await this._getExternalLinks1hop(link1hop); } let links2hop = this.BackLinksData .filter(data => data.title.replace(/\s/g,'_') === link1hop.titleLc) .flatMap(data => data.backLinks); // 重複を除去する links2hop = [...new Set(links2hop.map(value => JSON.stringify(value)))] .map(value => JSON.parse(value)) // 吹き出し元のページを除外する .filter(link => link.project !== project || link.title !== title); this._log(`Got ${links2hop.length} cards.`); return links2hop.map(link2hop => this.pageCache .find(page => page.project === link2hop.project && page.title === link2hop.title) .cardHtml); } // 外部projectの場合 async _getExternalLinks1hop({project, titleLc}) { // 探す const data = this.BackLinksData .find(data => data.project === project && data.title.replace(/\s/g,'_') === titleLc); // なければfetchして取得する if (!data) { this._log(`Not found. Fetching 1 hop link data...`); const response = await fetch(`/api/pages/${project}/${titleLc}`); if(!response.ok) return; const json = await response.json(); const pages = json.relatedPages.links1hop .filter(link1hop => !json.links.includes(link1hop.title)); const pageCache = pages.map(page => new PageCache({ project: project, title: page.title, descriptions: page.descriptions, imageUrl: page.image})); this.pageCache.push(...pageCache); this.BackLinksData.push({project: project, title: json.title, backLinks: pages.map(page => new Object({project: project, title: page.title}))}); return pageCache.map(data => data.cardHtml); } return data.backLinks.map(link1hop => this.pageCache .find(page => page.project === link1hop.project && page.title === link1hop.title) .cardHtml); } // 与えられたページについて、2 hop linkを計算する // WIP /*calculateLinks2hop({project, title}) { if (this.current.project === project && this.current.title === title) return; this.current.project = project; this.current.title = title; const links1hop = this.linkCache .find(page => page.project === project && page.title === title); let prevBackLinks = []; for (const link1hop of links1hop) { const links2hop = this.BackLinksData .filter(data => data.title === link1hop.title) .flatMap(data => data.backLinks); prevBackLinks.push(); } }*/ // 指定したページの本文のHTMLを取得する getTextBubble({project,title}) { this.pageCache.find(page => page.project === project && page.title === title).parsedHtml = convertSb2HTML({project:project,title:title,hasTitle:true}); } // debug用 _log(msg, ...objects) { console.log(`[RelatedPageManager] ${msg}`, objects); } }


script.js_disabled
export function makeRelatedLinksCache(linkCache, backLinksData) { // 重複を除く処理をやっていないので正確ではない return linkCache.flatMap(page =>page.links.map(link =>new Object({ project: page.project, title: page.title, link1hop: link, links2hop: backLinksData.find(data => data.title === link.title).backLinks }))); }
data構想
ts
interface relatedLinksCache { project: string, title: string, link1hop: { project: string, title: string, } links2hop: { project: string, title: string, }[]; }
これいらない
LinkCache BackLinksData を合体しただけ。
無駄。ふつうに分離してあるやつを持っておけばいい

WebWorkerのコード
やること
fetch
関連ページ計算

link-calc-worker.js
self.addEventListener('message', async message => { switch (message.data.job) { case 'initialize': const promises = message.data.projects.map(project => loadLinkCache(project)); const linkCache = await Promise.all(promises).then(cache => cache.flat()); const backLinksData = tobackLinksData(linkCache); self.postMessage({linkCache: linkCache,backLinksData: backLinksData}); break; default: break; } }); async function loadLinkCache(project){ let followingId = null; const linkCache = []; _log(`tart loading links from ${project}...`); do { _log(`Loading links from ${project}: followingId = ${followingId}`); const json = await (!followingId ? fetch(`/api/pages/${project}/search/titles`) : fetch(`/api/pages/${project}/search/titles?followingId=${followingId}`)) .then(res => { followingId = res.headers.get('X-Following-Id'); return res.json(); }); linkCache.push(...json.map(page => new Object({ project: project, title: page.title, links: page.links.map(link => new Object({project:project,title:link}))}))); } while(followingId); _log(`Loaded ${linkCache.length} page data from /${project}.`); return linkCache; } // linkCacheから、[{title:"",backLinks: [...]},...]を作る // link名はprojectで区別しない function tobackLinksData(linkCache) { // 被リンクを持つページを抽出して先にlistを作っておく _log('Search for existing pages...') const backLinksData = [...new Set(linkCache.flatMap(page => page.links.map(link => `${link.project}/${link.title}`)))] .map(link => new Object({project: link.replace(/^([^\/]+)\/.*/,'$1'),title: link.replace(/^[^\/]+\/(.*)/,'$1'),backLinks: []})); _log('Finish. Calculating back links...'); linkCache.forEach(page => backLinksData // pageがリンクしているexistPageを抽出 .filter(existPage => page.links.some(link => link.title === existPage.title)) .forEach(existPage => existPage.backLinks.push({project:page.project,title:page.title}))); _log('Finish calculating back links.'); return backLinksData; } // debug用 function _log(msg, ...objects) { console.log(`[link-calc-worker] ${msg}`, objects); }

#2021-02-12 00:23:32
#2020-11-12 03:33:08