関連ページリストを計算する
仕様
どうするか
現在のページにあるリンクの情報を保持
それぞれのリンクに対して関連ページを紐付けておく
ページが編集される度に、追加されたリンクがないか確認
追加されていたら、そのリンクをリストにいれる
削除されたリンクについては何もしない
データ構造
javascriptconst pageCache = [
{
project: "",
title: "",
parsedHtml: "", // なくても可
cardHtml: "",
},
];
pageCache[].cardHtml
は /api/pages/$project
を叩いて事前に全部作っておく
/api/pages/$project/$page/text
を叩く
test.jsclass 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;
}
}
javascriptconst relatedLinkCache = [
{
project: "",
title: "",
link1hop: "", // /project/titleの形式
links2hop: ["",], // /project/titleの形式
}
];
指定したページ内のリンクの2 hop links を取得するのに使う
project
, title
, link1hop
を指定して、 links2hop
を取得する
/${project}/${title}
中にあるリンクとそのリンクの2 hop linkを入れておく
relatedLinkCache[].links2hop
は重複しないように計算する
scrapboxの挙動に合わせる
カードが多い場合は、各projectごとに100以下に納める
並び順
特に考えない
もしくは、 linked
で並び替える
links1hop
は、 project
が違っても同じリンクだとみなして一緒にしてしまうか?
いや、data構造は別々でいい
あとで区別して使うかも知れない
dataを取り出すときに、 /^\/.+\/{link}\/$/
で取り出して合体できる
データはmain threadで持つ
UI threadとは、表示するのに必要なHTMLテキストだけやり取りする
やはりUI threadにもたせたほうがいい
LinkCache
はUI threadで使用しないのでWebWorkerに持たせておく
2 hop linkを計算するためのdata
javascriptconst LinkCache = [
{
project: "",
title: "",
links: ["",],// /project/titleの形式
},
];
/api/pages/$project/search/titles
で取得する
現在ページからDOM操作で取得するしかないか
links
には /project/title
が参照しているリンクを入れる
relatedLinkCache[].links2hop
を検索するときに使う
検索しやすいように、被リンクのリストをもたせたdataに変換する
変換後のdata
javascriptconst backLinksData = [
{
title: "",// /project/titleの形式
backLinks: ["",],// /project/titleの形式
},
];
↓使わない
APIを叩いて、リンクを探す
予めリンクをもっておく
javascriptconst externalProjectLinks = [
{
project: "",
title: "",
linked: ["",],
},
];
/api/pages/$project/search/titles
で取得する
linkedには被リンクのみ入れておく
linkCache[].links2hop
を検索するときに使う
被リンクではなく順リンクを保持したほうがいい?
↑使わない
link networkの更新
現在ページに紐付いているlinkについては、
関連ページリストの更新に応じて再計算する
どうやって既存のcacheを更新する?
一からリンクを計算し直すのか?
LinkCache
の対象の要素のみ更新する
そのあと、 BackLinks
を通じて relatedLinkCache
を再計算する
更新は div.related-page-list.clearfix ul.grid
をMutationObserverで監視する
#editor .lines
を監視しよう
それ以外のlinkは更新しない
頻繁にAPIを叩くのは負荷が高い
↓更新していない
test.jsimport {createPageCard} from '/api/code/takker/scrapboxのページカードを作成するscript/script.js';
let test_list =[];
const project_list = ['takker','takker-memex','takker-private'];
makePageCache(project_list).then(pageCache => {
test_list = pageCache;
console.log(test_list);
});
function makeRelatedLinkCache(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).backLinks
})));
}
async function makePageCache(projects) {
const promises = projects.map(project =>loadAllPages(project));
return await Promise.all(promises).then(pageCaches => pageCaches.flat());
}
async function 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 pageCache = [];
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 =>
pageCache.push(new PageCache({
project: project,
title: page.title,
descriptions: page.descriptions,
imageUrl: page.image})));
});
await Promise.all(promises);
console.log(`Loaded ${pageCache.length} page data from /${project}.`);
return pageCache;
}
test.jsasync function loadLinkCache(projects) {
const promises = projects.map(async project => {
let followingId = null;
const linkCache =[];
//console.log(`Start loading links from ${project}...`);
do {
//console.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 => `/${project}/link`)})));
} while(followingId);
return linkCache;
});
return await Promise.all(promises).then(LinkCaches => LinkCaches.flat());
}
// linkCacheから、[{title:"",backLinks: [...]},...]を作る
// link名はprojectで区別しない
function tobackLinksData(linkCache) {
// 被リンクを持つページを抽出して先にlistを作っておく
const result = [...new Set(linkCache.flatMap(page => page.links))]
.map(link => new Object({title: link, backLinks: []}));
linkCache.forEach(page => result.filter(value => page.links.includes(value.title))
.forEach(value => value.backLinks.push(`/${page.project}/${page.title}`)));
return result;
}