takker99/ScrapBubble
ScrapBubbleの実装
takker99/ScrapBubble
使い方
注意
公式でも指摘されていますが、UserScriptは上級者向け機能です。
以下、JavaScriptを知らない方でも導入できると思われる程度まで解説はしてありますが、ScrapboxのUserScriptの原理や、JavaScriptの文法は知っているものとして、説明は一切しておりません。
そのつもりで読んでいただければと思います
0. /villagepump/2022/01/17#61e56d5a1280f000000754bcにあるCSSを事前にUserCSSに入れて下さい
2022-05-27 13:18:21 これまだいるのかな?
1. このリンクを踏んでコードを生成する
2. 生成したコードを適当なページのコードブロック記法に貼り付ける
どのprojectのページに貼り付けても動作しますが、UserScriptの改竄を防ぐため、自分のみが編集できるprojectのページに貼り付けることを強く勧めます
以下、[/my-project/ScrapBubble]のmod.jsに貼り付けたとします
3. 以下のコードを自分のページのscript.jsに貼り付ける
code:js
import { mount } from "../../my-project/ScrapBubble/mod.js";
mount();
project名(my-project)、ページタイトル(ScrapBubble)、コードブロック名(mod.js)は各自の設定で置き換えてください
補足(わかっている人むけ)
コード生成の設定は、URL parameterをいじると変えられます
例
sourcemap付きで生成
scrapbox json dataで生成
code:template
ScrapBubble-min
/takker/takker99/ScrapBubbleをこのリンク @URL@でbuildしたもの
code:mod.js
@CODE@
コード生成でやっていることは、transpileとbundle、minifyだけです
jsr: specifierとnpm: specifierに対応しているbundlerなら同じことをできます
mount()にoptionを渡して挙動を変更できます
optionの一覧および解説はdocsを参照
code:script.js
import { mount } from "../../my-project/ScrapBubble/mod.js";
mount({
whiteList: "takker-memex", "takker-private", "takker-PS",
});
TamperMonkeyで動かしたいときはTamperMonkeyでScrapBubbleを使うを参照ください
変更点
開発方法を完全に変えた
git管理
TSXで書く
テスト書く
実装
web componentsは使わない
代わりにElement.shadowRootにReact appをmountする
ScrapBubbleのcache戦略
/icons/done.iconページタイトルの変更に対応するhttps://github.com/takker99/ScrapBubble/commit/e67b7c1c0a6e4dd7909c7674648b5202efb0485e
?followRename=trueを使う
/icons/done.iconCardをCardBubbleに内蔵する
TextBubbleの実装と統一させる
useBubbles()の設計メモ
実装したいこと
リンク同一判定をカスタムできるようにする
例:/villagepump/yyyy/MM/ddをyyyy-MM-ddにマッチさせる
実質表記ゆれ吸収機能になるな
scrapboxの数式記法をコピー可能にする
document.getSelection()をいじる?
そんなことできるのか?
KaTeXのDOMとtex commandとの1対1対応を作らなければならない
コピーボタンをつける
任意の$ xに$ y^2を代入して……のような文章があったら、大量のコピーボタンが現れることになる
うざったい
Context Menuにコピーメニューを入れる
mathjaxと同じ戦略
複雑にしすぎてない?
書き込めるようにする?
編集モードと閲覧モードを用意するUIなら簡単に実装できるはず
ページ全体を<textarea>に切り替える
編集結果はpatch()で送ればいい
空リンクではないが、重複除去処理した結果なにも表示するものがないリンクを特別なスタイルにする?
CSS classは付与しといてもよさそう
links2hopsを使えば、より読み込み速度を上げられそう
先回りしてカードを出せる
空リンクかどうかも、ページ内のすべてのリンクに対して一度に判定できる
これでも空リンクをすぐ判定できないケース
全てのprojectで空リンクなリンク
そのリンクのページデータを全てのprojectから取得しないと判定できない
基本空リンク(赤リンク)にし、繋がりを検知し次第青リンクに切り替える
空リンクを判定できないケースが少ないから、これで問題ない
2023-01-21 08:02:13 すでに実装済み
どこで実装したかは忘れた
✅変換処理を切り出す (takker99/ScrapBubble)で安定化させた
card bubblesの並び替え
現状:更新日時の古い順
順番を何も考えずにrenderingしたらこうなった
流石に使いにくい
変更後:Most related+project順序の合せ技
同一projectはMost relatedで決める
algorithmの構築に時間がかかりそう。ひとまずは更新日時順にしておく
project間は、1. whiteList順 2. lastAccessedの早い順に並べる
2022-10-29 19:44:58 なぜかwhitelistの逆順に表示されてしまう…
useBubble()の責任領域を変える
こいつがやることは
各階層でhoverされているリンクの情報の提供
前階層までにhoverしたリンクのリストの提供
重複を取り除くのに必要な情報
bubbleのhover・click event handlersの提供
これが難しい。bubble()内で新しく子要素用のbubble()を作成する必要が生じ、循環参照に陥る
これどういう現象だ?なんでこんなことしなきゃいけなくなったのか忘れてしまったtakker.icon
なんとか実装した
表示・非表示情報
これも更に難しい
bubbleしても、表示するかどうかは決められない
データがあるかどうか、bubble対象のprojectか確認してからbubble()を実行すれば解決する?
2022-10-09 06:05:50 新しい方法を思いついた
attachHover()
リンクホバーから実際にbubbleするかどうか判定するまでの処理を抽象化したもの
onStartでリンクホバー時の操作(prefetchなど)、intervalでホバー判定する時間の長さ、onEndでホバー成功時の操作(bubble実行など)を設定する
イベント処理をカプセル化しているので、端末に応じてホバー操作を代替操作に置き換えたりもできる
code:attachHover.ts
export const attachHover = <E extends Element = HTMLElement, T = void>(
element: E,
{ onStart, onEnd, onCancel, interval }: AttachHoverInit,
): void => {
let canceled = false;
const handleEnter = async (e) => {
const promise = onStart?.(e);
const handleCancel = (e) => {
canceled = true;
promise?.then?.((p) => onCancel?.(e, p)) ?? onCancel?.(e);
};
element.addEventListener("pointerleave", handleCancel);
await sleep(interval);
if (!canceled) onEnd(e, await promise);
element.removeEventListener("pointerleave", handleCancel);
canceled = false;
};
element.addEventListener("pointerenter", handleEnter);
return () => {
element.removeEventListener("pointerenter", handleEnter);
canceled = true;
};
};
export interface AttachHoverInit<E extends Element, T> {
onStart?: () => Promise<T>;
onEnd: (e: PointerEvent<E>, startResult: T) => void;
onCancel?: (e: PointerEvent<E>, startResult: T) => void;
interval: number;
}
ちなみに、bubbleのデータ作成には二つのアプローチがある
page dataも含めて、useBubbles()でひとつの配列として生成する
初期に使っていた方法
すべてのbubbleデータの読み込みをやらなければならない
配列で一気に取得する必要がある
空リンク判定処理も密結合してしまう
表示するページタイトルだけをbubbleデータの配列に含める
cacheを使い始めた頃から使っている方法
データ読み込み処理を分離できる
各bubbleのcomponentで読み込めるようになる
配列で一度に取得する必要がなくなる。
各componentで好きなタイミングで読み込めばいい
ページの存在判定を分離しなければならない
同じ函数で処理したいのに、分散してしまう
データに齟齬が生じる
のみ。ページデータの読み込みや加工には関与しない
位置情報は、ピクセルではなく比率で保持したほうがいいかも
画面回転などで位置が崩れにくい
code:ts
export interface Bubble {
project: string;
title: string;
/** Bubbleの表示位置計算で使う座標
*
* 左から順に、top left bottom right
*/
position: number, number, number, number;
/** 親階層のbubblesのページタイトル */
parentTitles: string[];
/** 現在の階層の下に新しいbubbleを出す
*
* すでにbubbleされていた場合、それ以降のbubbleを含めて消してから新しいのを出す
* @param project bubbleするページのメインプロジェクト
* @param title bubbleするページのタイトル
* @param position bubbleするカードの表示位置
*/
bubble: (project: string, title: string, position: Position) => void;
/** 現在の階層より下のbubblesをすべて消す */
hide: () => void;
}
export const useBubble: () => Bubble[];
タイトル変更を考慮してScrapboxのページデータを取得する
mobileではリンクの側にbubbleボタンを置く
mobileではhoverをうまく扱えなくて不便
hover判定させるには長押しする必要がある
しかも、長押しすると右クリックメニュー相当のダイアログが出てきてしまう
いちいち消すのがうっとおしい
代わりにボタンを置く
delayを短くするだけでも実現できそう
/villagepump/2022/01/17#61e56d5a1280f000000754bcを入れなくてもbubblesが削れないようにする
bodyにmountすればいい?
18:00:04 これ今もそうなのだろうか?
CSSを外して確認したい
networkから取得したデータの状態がエラーの場合は、cacheから返すようにする
fetch.tsにfallbackToCacheオプションを生やす
2023-04-29 11:57:11 cache firstで返すので不要
page titleにproject nameを含める
背景
project Aのリンクからproject Bのページをbubbleしたとき、違うprojectであることがわからなくなった
✅️同名タイトルのページをタブで切り替えられるようにする (takker99/ScrapBubble)で、複数ページあるときにしかproject nameを表示しなくなったため
解決策
page titleにproject nameを含める
card bubbleのheaderの部分にもproject nameを入れようかなtakker.icon
現状はScrapboxのThemeの色の違いで見分けている
同じthemeのprojectのcardsが並ぶと、違うprojectであることがわかりにくい
/motoso/ScrapBubbleを試す#61e3dee1774b170000b80b42
bubbleするまでに必要な時間を長くするのもありかも
現状は650ミリ秒にしている
ScrapScriptsと同じ
これって調節できなかったっけ?
propertyを生やした気がしたのだが
生やしていなかったら生やそう
あ、やっぱり生えてた
mount({delay: 1000})で1秒にできる
project badgeをposition: fixedにする
読み込み状況を表示する
回線の遅い環境で使っていると、scriptがバグっているのか単に読み込みが遅いのかの違いがわからない
useStatusBar()を使えば見やすく表示できる
長いタイトルを刻むページ機能
黒糖をhoverしたら、黒糖入りホットミルクのページがページリストに加わる
黒糖入りホットミルクが空ページなら、黒糖入りホットミルクの逆リンクが表示される
この要領で類似したタイトルのページを関連ページとして表示するも取り込めそう
ページの中身を各componentではなく中央に全て管理させる
ページ内容とbubbleの表示順序、重複除去処理が本質的に関わり合っていて、下手に分離させるとむしろ不都合が生じる
初期の設計と同じく、useBubbleですべてのデータを配信したほうがよさそうだ
2023-01-20 06:14:31 ✅変換処理を切り出す (takker99/ScrapBubble)した感触だと、うまく分離できそうな気もする
⬜️有効/無効切り替え機能を入れる (takker99/ScrapBubble)
⬜️contextの関係しないPage.tsxのComponentを別ファイルに切り出す?
⬜任意の場所からbubble出来るようにする (takker99/ScrapBubble)
prefetchをpropsで渡す必要があるか?
いまはこうなっている
code:ts
/** ページデータを先読みする
*
* white listにない外部プロジェクトリンクは、そのページだけを読み込む
*/
const prefetch = useCallback((project: string, title: string) => {
const projects = new Set(scrapbox.Project.name, ...whiteList);
prefetch_(
title,
projects.has(project) ? projects : new Set(project),
watchList,
);
}, whiteList, watchList);
わざわざこの程度のwrapperを作って渡す必要がないような……
いや、これのおかげで下部componentが直接watchListに依存しないようになっているのか
もしこのwrapperを定義しないと、prefetchでしか使わないwatchListをpropsバケツリレーすることになる
/yosider-scripts/takker99/ScrapBubble#666ec37be5172d000033084a
これは無理
fetchのエラーはcatchしたとしてもコンソールに流れてしまう
cacheしているが、一定時間経過したら再びfetchするようにしている
ページが新規作成されているかもしれないから
これは改善予定
ScrapBubbleのcache戦略
バグ
/villagepump/2023/07/28をbubbleすると壊れる
[/ 斜体]でパースエラーしている
scrapbox-parserに読ませて結果を確認しよう
✅️スクロールロックを復活させる
子要素があるかどうかは、データの読み込み状況から判断する必要があり、難しい
useBubbles()だけでは決めきれない
もしかしたらいらないかも?
おそらく✅リンク先にスクロールする機能が壊れているのを直す (takker99/ScrapBubble)で修正した
⬜bubblesが画面外にはみ出さないようにする
⬜コードブロック中のTrojan sourceを可視化する
何らかの条件がそろうとscrapboxをいっさいクリックできなくなる
再読込するしかなくなる
event handlingで何か失敗している?
状態をどこかに表示して、条件を調べよう
.status-barの色が見にくいかも
[/A]でbubbleした[/B/page1]中にある[/B/page2]をbubbleして[/A/page2]が表示されたとき、[/A/pages]にproject badgeが表示されない
大本のprojectと一致するかどうかでproject badgeの表示非表示を切り替えているので、挙動は正しい
しかしこの場合はproject badgeを表示してほしい
bubble元のURLと違うから
あー、大本のprojectではなく、bubble元のURLと一致するかどうかで表示を切り替えたほうがいいか。
すべてページにタイトルを表示すればいいのでは
どのページだけ表示するかという判断をしなくて済む
何らかの条件下でbubbleが表示されなくなる
Pull 14のcommitでも発生した
smartphoneでよく発生するようだ
リンクを長押ししても何も表示されない
以前のbubblesが消されないまま残ってしまうこともある
どこをクリックしても消えない
⬜bubblesの読み込みが異様に遅い
/motoso/ScrapBubbleを試す#61ebcc14774b1700003decf6
これは認識していなかった
なんだろう?一瞬表示判定されてしまうとか?
bubblingのevent handlingを変えたい
現状
ふつうのリンクのpointerenterは大本でcapture phaseを用いて捕捉している
card bubblesだけ個別にevent listenerを渡している
どうしたいか
全てのcomponentのeventを大本の<App />で捕捉する
現状だと、card bubblesの個数分event listenerを作る必要がある
これ単にlambda函数をその場で渡しているから、常にre-renderが発生してしまうんだよな
そもそもcard bubblesだけ特別扱いなのがおかしい
そんなことする理由がない
/motoso/ScrapBubbleを試す#61eec037774b170000161867
えっまじ!?takker.icon
おかしい。ちゃんと無視しているはずなのだが……
明日これ聞いておくか
mobile版scrapboxの/takkerで起動しなかった
bookmarkletからなら動いた
/takker-memexでも動いている
なんだろう?takker.icon
たまたまかな?
katexエラー
\tag{}で発生している
version upで直るかな?
scrapbox ver. assets-20220818-060939 で使われているKaTeXのversionが0.12.0
ScrapBubbleおよびscrapbox-katex-previewerで使っているのが0.13.3……あ、あれ?ScrapBubbleのversionのほうが大きいのか?
てことは設定ミスだろうか?
2023-01-21 08:04:08 今も発生しているかは未確認
一度表示されたCardsが消える
/takker-R000000025-I00463/ビットパラレル手法によるアライメントアルゴリズム#61f9ee251280f00000f40c03で発生した
Chrome for Android
間を置かずにbubbleし直しても同じ現象が発生したので、cacheの再読込は原因ではなさそう
もしくは、なぜか何度も再読込が発生してる?
メモリに最新の状態が反映されていない可能性もある
逆リンク計算アルゴリズムを切り出して、単体テストしたほうがよさそう
→✅変換処理を切り出す (takker99/ScrapBubble)
2023-01-18 19:51:58 まだ発生している……
テスト書かないとだめだな
import.meta.url周りの話
UserCSSの設定でよろしくないことが起きている
/villagepump/@yuyasurarin#63723b9d1280f0000080c666
Cannot read properties of undefined (reading 'replaceAll')
bubble.tsで発生
何らかの条件に一致したbubbleが表示された時、preactのかなり深いところでnullエラーを踏む
source mapを使って追跡しないと原因がわからなそう
20:08:43 500 Internal Server errorなどの失敗したresponseをcacheから読み出す時、問答無用でthrowしてしまっているのが原因
HTTPエラーは想定内のエラーだから、panicさせずにResult<T, E>で処理すべき
V1 Road map for scrapbox-userscript-stdを終わらせないと直せないな
takker99/ScrapBubbleの開発ログ兼ね作業ログ
code:mod.tsx
export * from "https://raw.githubusercontent.com/takker99/ScrapBubble/0.9.11/mod.tsx";
#2024-08-12 19:15:30 /villagepump/✅️同名タイトルのページをタブで切り替えられるようにする (takker99/ScrapBubble)#66b9d4d971b3c20000055cb6を受け、リンクを修正
#2024-07-31 15:31:28
#2024-07-09 20:24:51
#2024-06-17 12:48:11
#2024-06-03 10:40:44
#2024-01-29 19:26:55
#2023-10-26 08:12:27
#2023-06-28 16:17:10
#2023-06-02 07:09:54
#2023-04-29 11:58:43
#2023-01-23 17:14:27
#2023-01-21 08:04:46
#2023-01-20 05:15:06
#2023-01-19 19:47:05
#2023-01-18 19:52:57
#2023-01-17 12:21:56
#2023-01-11
#2022-12-25 06:24:46
#2022-12-24 17:56:04
#2022-12-15 19:30:32
#2022-11-14 22:04:08
#2022-11-02 10:05:09
#2022-10-31 23:09:12
#2022-10-28 05:10:08
#2022-10-09 06:37:16
#2022-09-25 15:17:04
#2022-08-18 10:59:39
#2022-08-13 05:27:20
#2022-08-06 08:02:17
#2022-08-05 13:00:34
#2022-08-01 10:57:55
#2022-07-28 10:48:19
#2022-07-27 04:33:06
#2022-07-05 05:46:26
#2022-06-09 05:45:47
#2022-06-06 07:10:29
#2022-05-27 05:49:12
#2022-05-10 18:37:04
#2022-02-20 17:03:47
#2022-02-07 06:17:13
#2022-02-03 09:07:38
#2022-01-25 08:00:46
#2022-01-23 07:17:32
#2022-01-18 00:01:38
#2022-01-16 11:53:27
#2022-01-15 11:28:40
#2022-01-12 22:28:19
#2022-01-09 23:31:38
#2022-01-07 20:44:03
#2022-01-06 18:57:53
#2022-01-05 19:22:41
#2021-12-29 04:24:33
#2021-12-21 09:15:14
#2021-12-16 10:46:22
#2021-12-15 08:47:31
#2021-12-14 06:13:08
#2021-12-12 08:19:31
#2021-12-11 06:43:02
#2021-12-07 12:28:25
#2021-11-15 08:30:08
#2021-11-13 20:36:11