advanced-related-pages
Next version
データ構造は変えてある
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を生やす
逆リンク計算を遅延させる
指定したページにあるリンクの逆リンクのみを計算する
計算はページを読み込んだときと本文のあるリンクをhoverしたときに実行するようにする
計算はbackgroundに任せる
計算中もカードを表示できるようにするため
ひらがなカタカナ、全角半角、大文字小文字の違いを無視する
大文字小文字無視は標準でつける
全角半角スペースとアンダーバーの違いも無視する
ひらがなカタカナ無視はどうしよう
入れなくてもいいかもしれない
script.jsimport {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構造
tsinterface LinkCache {
project: string,
title: string,
links: {
project: string,
title: string,
}[];
}
tsinterface 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_disabledexport 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構想
tsinterface relatedLinksCache {
project: string,
title: string,
link1hop: {
project: string,
title: string,
}
links2hop: {
project: string,
title: string,
}[];
}
これいらない
LinkCache
と BackLinksData
を合体しただけ。
無駄。ふつうに分離してあるやつを持っておけばいい
WebWorkerのコード
やること
fetch
関連ページ計算
link-calc-worker.jsself.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);
}