Scrapbox APIにリンクからアクセスするUserScript
https://gyazo.com/b2733408fca030dc3a7f4d2674d4ea8a
code:script.js
(() => {
function contentMerge(contents, {reverse: reverse = false}) {
console.clear();
let list = new LinkedList();
const cs = contents.map(content => {
if (reverse) content = content.reverse();
content.map((pageJson, i) => {
if (i === 0) {
// 1件目はベースとして追加し、何もしない
const title = pageJson.title;
pageJson.lines.map((l, i) => {
// iが0(タイトル), 1(絶対空欄)にしているから、その分を除外
if (l.text === '' || i === 0 || i === 1) return;
list.insertAt(
{ text: l.text, title: title, level: l.level, updated: l.updated },
i);
});
} else {
let currentHeadText = '';
let parentHeadText = '';
let parentText = '';
let parentIndex = -1;
const title = pageJson.title;
pageJson.lines.map((l, i) => {
// iが0(タイトル), 1(絶対空欄)にしているから、その分を除外
if (l.text === '' || i === 0 || i === 1) return;
// インデント0は見出しとして扱う
if (l.level === 0) {
if (list.search(l.text) === -1) {
console.info(ベースページにない見出しなので最後に追加:${l.text});
list.addToTail(
{ text: l.text, title: title, level: l.level, updated: l.updated });
}
currentHeadText = l.text;
parentText = l.text;
return;
}
if (list.search(l.text) === -1) {
// 存在しないとき
let index = 0;
// 見出しが一つ前の見出しと同じ場合
if (currentHeadText === parentHeadText) {
// 見出しが同じ場合、一つ前のテキストの下にinsert
console.info('一つ前のテキストが親見出しなのでその下にinsert');
index = parentIndex + 1;
} else {
// 見出しが変わった場合、親見出しの下にinsert
console.info(親見出しにぶら下げてinsert currentHeadText:${currentHeadText}, parentText:${parentText});
if (currentHeadText === parentText) {
// 見出しと一つ前のテキストが同じ場合、一つ前のテキストの下にinsert
index = list.search(currentHeadText) + 1;
} else {
// 違う場合、一つ前のテキストの下にinsert
index = list.search(parentText) + 1;
}
}
console.info('現在読み込んでいるページ----------------------------');
console.info(${title});
console.info('現在の行-----------------------------------------');
console.info(level:${l.level} ${l.text});
console.info('親見出し-----------------------------------------');
console.info(${currentHeadText});
console.info(このテキストにinsertAt(${index})---------------------------------);
console.info(list.joinTexts({debug: false, index: true}));
list.insertAt(
{ text: l.text, title: title, level: l.level, updated: l.updated },
index);
parentHeadText = currentHeadText;
parentIndex = index;
} else {
// 存在するとき
}
parentText = l.text;
});
}
});
});
console.info(list.joinTexts({debug: true, formatted: true}));
return list.joinTexts({});
}
// 🔱で除外するページ名
const EXCLUDE_PAGE_LIST = [
'週報',
];
// 除外ページか判断
function isExcludePage(title) {
let isExclude = false;
EXCLUDE_PAGE_LIST.map(m => {
if(title.match(m) !== null) isExclude = true;
});
if (isExclude) return true;
return false;
}
// pageのjsonから1ホップリンクページのリンクを取得
function getRelatedPagesLink(page) {
const json = JSON.parse(page);
if (isEmpty(json)) return;
return json.relatedPages.links1hop.filter(f => !isExcludePage(f.title))
.map(m => {
return /api/pages/${getProjectName()}/${encodeURIComponent(m.title)};
});
}
function getProjectName() {
return encodeURIComponent(scrapbox.Project.name);
}
function getTitle() {
return encodeURIComponent(scrapbox.Page.title);
}
function windowOpenWithContent(content) {
const win = window.open();
win.document.open();
win.document.write(<pre style="white-space: pre-wrap;"=>${content}</pre>);
win.document.close();
}
// タイトルから1行目まで捨てる
function truncateTitle(content) {
return content.split('\n').map((m, i) => {
if (i === 0 || i === 1) return null;
return m;
}).filter(f => f !== null).join('\n');
}
// 中身空のページはpersistent = false?
function isEmpty(json) {
if (json.persistent) return false;
return true;
}
function moveLink(m) {
if (m.title === '---') {
// 何もしない
return;
} else if (m.title.endsWith('🔱')) {
// 読みだしたいページリスト
// 読みだしたいページリストをPromise.allで待つ, ページリストの中のリンクをPromise.allでさらに待つ
Promise.all(pages.map(d => {
return fetch(d).then(r => r.text())
.then(r => {
return Promise.all(getRelatedPagesLink(r).map(m => {
return fetch(m).then(r => r.text())
.then(r => {
// ページごとのjson
const json = JSON.parse(r);
json.lines.map(m => {
// textのインデントの数だけlevelとして保持する
const i = m.text.match(/^ */)0.length; return m.level = i;
});
return json;
});
}))
.then(content => {
// Promiseに渡したjsonの配列のまま返す
return content;
})
.catch(err => {
console.error(err);
});
});
}))
.then(contents => {
// ここで配列(ページ)のマージ
if (m.title.endsWith('順🔱')) {
windowOpenWithContent(contentMerge(contents, {reverse: false}));
} else if (m.title.endsWith('逆🔱')) {
windowOpenWithContent(contentMerge(contents, {reverse: true}));
}
}).catch(err => {
console.error(err);
});
} else if (m.title.endsWith('⛓')) {
// ⛓のリンク時は1 hopリンクを全て結合して出力
// 読みだしたいページリスト
// 読みだしたいページリストをPromise.allで待つ, ページリストの中のリンクをPromise.allでさらに待つ
Promise.all(pages.map(d => { return fetch(d).then(r => r.text())
.then(r => { return Promise.all(getRelatedPagesLink(r).map(m => fetch(m).then(r => r.text())
.then(r => JSON.parse(r)) // ページごとのjson
))
.then(content => content) // Promiseに渡したjsonの配列のまま返す
.catch(err => console.error(err))
});
}))
.then(contents => {
let concatText = [];
contents.map(content => {
if (m.title.endsWith('逆⛓')) content = content.reverse();
content.map((pageJson, i) => {
pageJson.lines.map((l, i) => {
if (i === 0) {
// 0はタイトル
concatText.push(■ ${l.text});
return;
}
concatText.push( ${l.text});
});
});
});
windowOpenWithContent(concatText.join('\n')
.replace(/^ +/gm, (match) => match.replace(/ /g, ' '))
);
})
.catch(err => console.error(err));
} else if (m.title.endsWith('📊')) {
// 📊のリンク時はテロメアっぽいものを付加して出力
fetch(m.href).then(r => { return r.text(); })
.then(r => {
const json = JSON.parse(r);
const created = json.created;
const lastUpdated = json.updated;
const lines = json.lines;
const content = lines.map(m => {
const diff = lastUpdated - m.updated;
const diffLog = Math.round(Math.log(1 + diff));
const telomere = ('||||||||||' + ' '.repeat(diffLog)).slice(-10);
return ${telomere} ${m.text.replace(/^[ ]+/gm, (match) => match.replace(/ /g, ' '))};
}).join('\n');
windowOpenWithContent(content);
});
} else if (m.title.endsWith('👀')) {
// 👀のリンク時は文頭整形して出力
fetch(m.href).then(r => { return r.text(); })
.then(r => {
const content = truncateTitle(r);
windowOpenWithContent(content.replace(/^ +/gm, (match) => match.replace(/ /g, ' ')));
});
} else if (m.title.endsWith('🔢')) {
// 🔢のリンク時は行番号を付加して出力
fetch(m.href).then(r => { return r.text(); })
.then(r => {
const content = truncateTitle(r);
windowOpenWithContent(content.replace(/^ +/gm, (match) => match.replace(/ /g, ' '))
.split('\n').map((m, i) => {
return ${(' ' + (i + 1)).slice(-5)}| ${m};
}).join('\n')
);
});
} else {
// それ以外はそのまま出力
window.open(m.href);
}
}
function addLinks() {
scrapbox.PageMenu.addMenu({
title: 'API',
onClick: () => {
let project = getProjectName();
let title = getTitle();
const endpoint = [
//{ title: ${scrapbox.Page.title} Feed, href: /api/feed/${project} },
// テキストを見栄えのために整形する系
{ title: ${scrapbox.Page.title}_ページ整形👀, href: /api/pages/${project}/${title}/text },
{ title: ${scrapbox.Page.title}_1hop集約_順🔱, href: /api/pages/${project}/${title} },
{ title: ${scrapbox.Page.title}_1hop集約_逆🔱, href: /api/pages/${project}/${title} },
{ title: ${scrapbox.Page.title}_1hop結合_順⛓, href: /api/pages/${project}/${title} },
{ title: ${scrapbox.Page.title}_1hop結合_逆⛓, href: /api/pages/${project}/${title} },
{ title: ${scrapbox.Page.title}_テロメア付📊, href: /api/pages/${project}/${title} },
{ title: ${scrapbox.Page.title}_行番号付🔢, href: /api/pages/${project}/${title}/text },
//{ title: ${scrapbox.Page.title}, href: /api/pages/${project}/${title}/text },
{ title: '---' },
// APIの結果をそのまま
{ title: ${scrapbox.Page.title}.json, href: /api/pages/${project}/${title} },
{ title: ${scrapbox.Page.title}.txt, href: /api/pages/${project}/${title}/text },
{ title: ${scrapbox.Page.title}.icon, href: /api/pages/${project}/${title}/icon },
{ title: '---' },
{ title: 'Search', href: /api/pages/${project}/search/titles },
{ title: 'Pages', href: /api/pages/${project} },
{ title: 'BackupList', href: /api/project-backup/${project}/list },
{ title: 'Snapshot', href: /api/page-snapshots/${project}/${scrapbox.Page.id} },
{ title: 'Export.json', href: /api/page-data/export/${project}.json },
{ title: 'AllProjects', href: /api/projects },
{ title: 'Project', href: /api/projects/${project} },
{ title: 'Invite', href: /api/projects/${project}/invitations },
{ title: 'Notify', href: /api/projects/${project}/notifications },
{ title: 'Streams', href: /api/stream/${project} },
{ title: 'Me', href: /api/users/me }
]
scrapbox.PageMenu('API').removeAllItems();
endpoint.map(m => {
scrapbox.PageMenu('API').addItem({
title: ${m.title},
onClick: () => moveLink(m)
});
});
}
});
}
addLinks();
// LinkedList
function LinkedList() {
this.head = null;
this.tail = null;
}
function Node(value, next, prev) {
this.value = value;
this.next = next;
this.prev = prev;
}
LinkedList.prototype.addToHead = function(value) {
var newNode = new Node(value, this.head, null);
if (this.head) this.head.prev = newNode;
else this.tail = newNode;
this.head = newNode;
};
LinkedList.prototype.addToTail = function(value) {
var newNode = new Node(value, null, this.tail);
if (this.tail) this.tail.next = newNode;
else this.head = newNode;
this.tail = newNode;
};
LinkedList.prototype.insertAt = function(value, index) {
if (index === 0) {
this.addToHead(value);
} else {
let current = this.head;
let i = 0;
while (current !== null && i < index) {
current = current.next;
i++;
}
if (current) {
let newNode = new Node(value, current, current.prev);
current.prev.next = newNode;
current.prev = newNode;
} else {
this.addToTail(value);
}
}
};
LinkedList.prototype.search = function(searchValue) {
var currentNode = this.head;
var index = 0;
while (currentNode) {
if (currentNode.value.text === searchValue) {
return index;
}
currentNode = currentNode.next;
index++;
}
return -1;
};
LinkedList.prototype.joinTexts = function({debug: debug = false, index: index = false, join: join = '\n', formatted: formatted = false }) {
var currentNode = this.head;
var texts = [];
var formattedDate = '';
var i = 0;
var formattedIndex = '';
while (currentNode) {
if (debug) {
var date = new Date(currentNode.value.updated * 1000);
formattedDate =
'[' + date.getFullYear() + '/' +
('0' + (date.getMonth() + 1)).slice(-2) + '/' +
('0' + date.getDate()).slice(-2) + ' ' +
('0' + date.getHours()).slice(-2) + ':' +
('0' + date.getMinutes()).slice(-2) + ':' +
('0' + date.getSeconds()).slice(-2) + ' ' +
currentNode.value.title + ']';
}
if (index) {
formattedIndex = [${i}];
}
texts.push(${formattedDate}${formattedIndex}${currentNode.value.text});
currentNode = currentNode.next;
i++;
}
let regexp = '';
if (debug || index) {
regexp = /\] +/gm;
} else {
regexp = /^ +/gm;
}
if (formatted) {
return texts.map((m, i) =>{
return ${m.replace(regexp, (match) => match.replace(/ /g, ' '))};
}).join(join);
} else {
return texts.join(join);
}
};
})();
履歴
page:changedだとタイトルを変更したあとにリロードしないと実行できなかったのでlines:changedに変えてみた(2023/04/05)
page:changedに戻してscrapbox.PageMenu.addMenuをクリックしたときのページタイトルを取得するようにしてみた(2023/04/17)
PageMenuを押した時にその時のタイトルを取得するようにしたらUserScript Eventいらなかった
🔱ボタンで関連ページを丸ごと取得するようにしてみた(2024/02/20)