generated at
scrapbox-headless-script
2022-02-10 19:03:27 ✅scrapbox-headless-scriptをscrapbox-userscript-stdに統合したのでこのrepoはarchiveしました
今後はscrapbox-userscript-stdを使ってください
hr
ScrapboxのWebSocketを用いて、DOM操作なしにページの編集を行うUserScript

名前
backgroundで処理するという点が、headless chromeみたいに感じたので、「headless」を入れてみた
機能
joinPageRoom()
特定ページを操作する
戻り値の函数で操作する
insert() : 任意位置への文字列挿入
update() : 文字列の書き換え
delete() : 文字列の削除
listenPageUpdate() : ページの更新購読
patch() : ページ全体を書き換える
scrapbox.ioには差分だけ送信する
deletePage()
ページを削除する
listenStream()
指定したprojectのStreamを購読する

実装したいこと
cursor flag操作
ピンの付け外し
pin()
unpin()
タイトルの変更
update() でも実現出来るが、helper函数を用意したい
done patch(update) update に同期函数・非同期函数のどちらでも指定できるようにする
判定方法
戻り値が Promise かどうかをみればいいだろう
serverから送られてくるページの更新情報を反映する
どうやら、packetのidからScrapboxの行IDの書式に沿って抽出した日時が、その変更が行われた日時と一致するらしい
これを使えば updated を再現できる
pinの付け外しも反映させる
エラー処理をちゃんとやる
エラーの型定義を作る
O(NP) algorithm部分を別のrepoに切り出す
SESの計算量を調べたい
update() で複数行を指定できるようにする
以下のどちらに挙動を寄せるかで悩みそう
1行目のみ上書きし、残りの行はその下に挿入する
全て上書きする
指定行以降の行を、指定した行の数だけ書き換える
元の行数が指定した行数より少なかった場合は、その分だけ下に挿入する
そういえば、 Change.lines.text にわざと改行を含んだ文字列を入れるとどうなるんだろう?
roomの切り替え機能を入れる?
JSDocでdocumentを作る
面倒だし、日本語で書いてしまおうか

refactoring
ついでにリンクを置換するPopup Menu周りの挙動がおかしい原因を突き止めたい

実装した方がいいけど難しそうなやつ
conflict解決
考えることがいろいろある
commitの衝突パターン
パターンごとに可能なmerge方法
merge出来なかった時の挙動

2022-01-04
16:42:58 release v0.2.0
2021-12-22
19:47:17 タイトルとサムネイルの更新などを実装している
branchはsync
あと実装すること
サムネイル画像の更新
scrapbox-userscript-websocketに型を追加する必要がある
ts
export interface ImageCommit { image: string; }
Gyazo URLの場合は、 https://gyazo.com/.../raw に変換される
2021-12-29 型を追加した
リンク情報の更新
applyCommits() のテスト
2021-12-29
07:05:43 タイトルのcommit生成のalgorithmを、scrapboxのページタイトルの決定方法に従わせるべき
03:47:03 descriptions の変更検知処理が二度手間になっているのを直したい
とくに patch() の効率が悪い
2つの文字列から計算した差分情報から、元の2文字列を再び計算し直している
insert() , update() , delete() と同じ descriptions 計算処理を用いているのが原因

実装
内部でscrapbox-userscript-websocketを使っている
このlibraryは面倒な手続きを色々隠して、使いやすいinterfaceを提供するようにしている
本当に使いやすくなったかどうかはわからないがtakker
少なくとも projectId commitId を操作しなくて済むようになってはいる

mod.js
var Y="4.2.0";async function k(){let t=(await Z())("https://scrapbox.io",{reconnectionDelay:5e3,transports:["websocket"]});return await new Promise((r,i)=>{let o=n=>i(n);t.once("connect",()=>{t.off("disconnect",o),r()}),t.once("disconnect",o)}),t}function Z(){let e=`https://cdnjs.cloudflare.com/ajax/libs/socket.io/${Y}/socket.io.min.js`;if(document.querySelector(`script[src="${e}"]`))return Promise.resolve(window.io);let t=document.createElement("script");return t.src=e,new Promise((r,i)=>{t.onload=()=>r(window.io),t.onerror=o=>i(o),document.head.append(t)})}function I(e,t=9e4){function r(o,n){let a;return new Promise((c,p)=>{let l=d=>{clearTimeout(a),p(new Error(d))};e.emit(o,n,d=>{clearTimeout(a),e.off("disconnect",l),d.error&&p(new Error(JSON.stringify(d.error))),"data"in d?c(d?.data):c(void 0)}),a=setTimeout(()=>{e.off("disconnect",l),p(new Error(`Timeout: exceeded ${t}ms`))},t),e.once("disconnect",l)})}async function*i(...o){let n,a=()=>new Promise(p=>n=p),c=p=>{n?.(p)};for(let p of o)e.on(p,c);try{for(;;)yield await a()}finally{for(let p of o)e.off(p,c)}}return{request:r,response:i}}var b=e=>`connect.sid=${e}`;function B(e){return e!=null}function H(e){return B(e)?(e.name===void 0||typeof e.name=="string")&&typeof e.message=="string":!1}function L(e){try{let t=typeof e=="string"?JSON.parse(e):e;return H(t)?t:!1}catch(t){if(t instanceof SyntaxError)return!1;throw t}}function j(e,t){let r=new Error;return r.name=e,r.message=t,r}var _=e=>e.replaceAll(" ","_").replace(/[/?#\{}^|<>]/g,t=>encodeURIComponent(t));async function F(e,t){let r=`https://scrapbox.io/api/projects/${e}`,i=await fetch(r,t?.sid?{headers:{Cookie:b(t.sid)}}:void 0);if(!i.ok){let n=L(await i.json());if(!n)throw j("UnexpectedError",`Unexpected error has occuerd when fetching "${r}"`);return{ok:!1,value:n}}let o=await i.json();return{ok:!0,value:o}}async function $(e){let t="https://scrapbox.io/api/users/me",r=await fetch(t,e?.sid?{headers:{Cookie:b(e.sid)}}:void 0);if(!r.ok)throw j("UnexpectedError",`Unexpected error has occuerd when fetching "${t}"`);return await r.json()}async function A(e,t,r){let i=`https://scrapbox.io/api/pages/${e}/${_(t)}?followRename=${r?.followRename??!0}`,o=await fetch(i,r?.sid?{headers:{Cookie:b(r.sid)}}:void 0);if(!o.ok){let a=L(await o.text());if(!a)throw j("UnexpectedError",`Unexpected error has occuerd when fetching "${i}"`);return{ok:!1,value:a}}let n=await o.json();return{ok:!0,value:n}}var R;async function E(){if(R!==void 0)return R;let e=await $();if(e.isGuest)throw new Error("this script can only be executed by Logged in users");return R=e.id,R}var q=new Map;async function P(e){let t=q.get(e);if(t!==void 0)return t;let r=await F(e);if(!r.ok){let{name:o,message:n}=r.value;throw new Error(`${o} ${n}`)}let{id:i}=r.value;return q.set(e,i),i}function G(e){return e.padStart(8,"0")}function S(e){let t=Math.floor(new Date().getTime()/1e3).toString(16),r=Math.floor(16777214*Math.random()).toString(16);return`${G(t).slice(-8)}${e.slice(-6)}0000${G(r)}`}function U(e){if(!K(e))throw SyntaxError(`"${e}" is an invalid id.`);return parseInt(`0x${e.slice(0,8)}`,16)}function K(e){return/^[a-f\d]{24,32}$/.test(e)}function J(e,t){let r=e.length>t.length,i=r?t:e,o=r?e:t,n=i.length+1,a=i.length+o.length+3,c=new Array(a);c.fill(-1);let p=[];function l(s,g,u){let f=Math.max(g,u),w=f-s;for(;w<i.length&&f<o.length&&i[w]===o[f];)++w,++f;return c[s+n]=p.length,p.push([{x:w,y:f},c[s+(g>u?-1:1)+n]]),f}let d=new Array(a);d.fill(-1);let x=-1,h=o.length-i.length;do{++x;for(let s=-x;s<=h-1;++s)d[s+n]=l(s,d[s-1+n]+1,d[s+1+n]);for(let s=h+x;s>=h+1;--s)d[s+n]=l(s,d[s-1+n]+1,d[s+1+n]);d[h+n]=l(h,d[h-1+n]+1,d[h+1+n])}while(d[h+n]!==o.length);let m=[],y=c[h+n];for(;y!==-1;)m.push(p[y][0]),y=p[y][1];return{from:e,to:t,editDistance:h+x*2,buildSES:function*(){let s=0,g=0;for(let{x:u,y:f}of Q(m))for(;s<u||g<f;)f-u>g-s?(yield{value:o[g],type:r?"deleted":"added"},++g):f-u<g-s?(yield{value:i[s],type:r?"added":"deleted"},++s):(yield{value:i[s],type:"common"},++s,++g)}}}function*W(e){let t=[],r=[];function*i(){if(t.length>r.length){for(let o=0;o<r.length;o++)yield z(t[o],r[o]);for(let o=r.length;o<t.length;o++)yield t[o]}else{for(let o=0;o<t.length;o++)yield z(t[o],r[o]);for(let o=t.length;o<r.length;o++)yield r[o]}t=[],r=[]}for(let o of e)switch(o.type){case"added":t.push(o);break;case"deleted":r.push(o);break;case"common":yield*i(),yield o;break}yield*i()}function z(e,t){return{value:e.value,oldValue:t.value,type:"replaced"}}function*Q(e){for(let t=e.length-1;t>=0;t--)yield e[t]}function*D(e,t,{userId:r}){let{buildSES:i}=J(e.map(({text:a})=>a),t),o=0,n=e[0].id;for(let a of W(i())){switch(a.type){case"added":yield{_insert:n,lines:{id:S(r),text:a.value}};continue;case"deleted":yield{_delete:n,lines:-1};break;case"replaced":yield{_update:n,lines:{text:a.value}};break}o++,n=e[o]?.id??"_end"}}function T(e,t,{updated:r,userId:i}){let o=[...e],n=a=>{let c=o.findIndex(({id:p})=>p===a);if(c<0)throw RangeError(`No line whose id is ${a} found.`);return c};for(let a of t)if("_insert"in a){let c=U(a.lines.id),p={text:a.lines.text,id:a.lines.id,userId:i,updated:c,created:c};a._insert==="_end"?o.push(p):o.splice(n(a._insert),0,p)}else if("_update"in a){let c=n(a._update);o[c].text=a.lines.text,o[c].updated=typeof r=="string"?U(r):r??Math.round(new Date().getTime()/1e3)}else"_delete"in a&&o.splice(n(a._delete),1);return o}async function N(e,t,r){return t.length===0?{commitId:r.parentId}:await e("socket.io-request",{method:"commit",data:{kind:"page",...r,changes:t,cursor:null,freeze:!0}})}async function C(e,t,{project:r,title:i,retry:o=3,parentId:n,...a}){try{n=(await N(e,t,{parentId:n,...a})).commitId}catch{console.log("Faild to push a commit. Retry after pulling new commits");for(let p=0;p<o;p++){let{commitId:l}=await v(r,i);n=l;try{n=(await N(e,t,{parentId:n,...a})).commitId,console.log("Success in retrying");break}catch{continue}}throw Error("Faild to retry pushing.")}return n}async function v(e,t){let r=await A(e,t);if(!r.ok)throw new Error(`You have no privilege of editing "/${e}/${t}".`);return r.value}async function Oe(e,t){let[r,i,o]=await Promise.all([v(e,t),P(e),E()]),n=r.commitId,a=r.persistent,c=r.lines,p=r.id,l=await k(),{request:d,response:x}=I(l);await d("socket.io-request",{method:"room:join",data:{projectId:i,pageId:p,projectUpdatesStream:!1}}),(async()=>{for await(let{id:m,changes:y}of x("commit"))n=m,c=T(c,y,{updated:m,userId:o})})();async function h(m,y=3){let s=T(c,m,{userId:o});(c[0].text!==s[0].text||!a)&&m.push({title:s[0].text});let g=c.slice(1,6).map(f=>f.text),u=s.slice(1,6).map(f=>f.text);g.join(` `)!==u.join(` `)&&m.push({descriptions:u}),n=await C(d,m,{parentId:n,projectId:i,pageId:p,userId:o,project:e,title:t,retry:y}),a=!0,c=s}return{insert:async(m,y="_end")=>{let s=m.split(/\n|\r\n/).map(g=>({_insert:y,lines:{text:g,id:S(o)}}));await h(s)},remove:m=>h([{_delete:m,lines:-1}]),update:(m,y)=>h([{_update:y,lines:{text:m}}]),patch:async m=>{let y=async()=>{let s=m(c),g=s instanceof Promise?await s:s,u=[...D(c,g,{userId:o})],f=T(c,u,{userId:o});(c[0].text!==f[0].text||!a)&&u.push({title:f[0].text});let w=c.slice(1,6).map(M=>M.text),O=f.slice(1,6).map(M=>M.text);w.join(` `)!==O.join(` `)&&u.push({descriptions:O});let{commitId:X}=await N(d,u,{parentId:n,projectId:i,pageId:p,userId:o});n=X,a=!0,c=f};for(let s=0;s<3;s++)try{await y();break}catch{if(s===2)throw Error("Faild to retry pushing.");console.log("Faild to push a commit. Retry after pulling new commits");try{let u=await v(e,t);n=u.commitId,a=u.persistent,c=u.lines}catch(u){throw u}}},listenPageUpdate:()=>x("commit"),cleanup:()=>{l.disconnect()}}}var V=()=>Number.MAX_SAFE_INTEGER-Math.floor(Date.now()/1e3);async function Ve(e,t){let[{id:r,commitId:i,persistent:o},n,a]=await Promise.all([v(e,t),P(e),E()]),c=i;if(!o)return;let p=await k(),{request:l}=I(p);try{c=await C(l,[{deleted:!0}],{projectId:n,pageId:r,parentId:c,userId:a,project:e,title:t})}finally{p.disconnect()}}async function Xe(e,t,r){let[i,o,n]=await Promise.all([v(e,t),P(e),E()]),a=i.persistent,c=i.lines,p=i.commitId,l=i.id,d=await k();try{let{request:x}=I(d),h=async()=>{let m=r(c),y=m instanceof Promise?await m:m,s=[...D(c,y,{userId:n})],g=T(c,s,{userId:n});(c[0].text!==g[0].text||!a)&&s.push({title:g[0].text});let u=c.slice(1,6).map(w=>w.text),f=g.slice(1,6).map(w=>w.text);u.join(` `)!==f.join(` `)&&s.push({descriptions:f}),await N(x,s,{parentId:p,projectId:o,pageId:l,userId:n})};for(let m=0;m<3;m++)try{await h();break}catch{if(m===2)throw Error("Faild to retry pushing.");console.log("Faild to push a commit. Retry after pulling new commits");try{let s=await v(e,t);p=s.commitId,a=s.persistent,c=s.lines}catch(s){throw s}}}finally{d.disconnect()}}async function Ye(e,t,r){let[{id:i,commitId:o,persistent:n,pin:a},c,p]=await Promise.all([v(e,t),P(e),E()]),l=o;if(a>0||!n&&!(r?.create??!1))return;let d={projectId:c,pageId:i,userId:p,project:e,title:t},x=await k(),{request:h}=I(x);n||(l=await C(h,[{title:t}],{parentId:l,...d}));try{l=await C(h,[{pin:V()}],{parentId:l,...d})}finally{x.disconnect()}}async function Ze(e,t){let[{id:r,commitId:i,persistent:o,pin:n},a,c]=await Promise.all([v(e,t),P(e),E()]),p=i;if(n==0||!o)return;let l={projectId:a,pageId:r,userId:c,project:e,title:t},d=await k(),{request:x}=I(d);try{p=await C(x,[{pin:0}],{parentId:p,...l})}finally{d.disconnect()}}async function*rt(e,...t){let r=await P(e),i=await k(),{request:o,response:n}=I(i);await o("socket.io-request",{method:"room:join",data:{projectId:r,pageId:null,projectUpdatesStream:!0}});try{yield*n(...t.length>0?t:["projectUpdatesStream:event","projectUpdatesStream:commit"])}finally{i.disconnect()}}export{Ve as deletePage,Oe as joinPageRoom,rt as listenStream,Xe as patch,Ye as pin,Ze as unpin};

#2022-02-10 14:02:37
#2022-01-26 15:23:47
#2022-01-17 04:29:56
#2022-01-07 21:35:37
#2022-01-04 16:40:35
#2021-12-29 03:50:57
#2021-12-23 10:36:21
#2021-12-22 18:32:59
#2021-12-14 06:00:14
#2021-12-07 21:11:29
#2021-11-30 13:46:15 /takker/scrapbox-bundlerでbundleするようにした
#2021-11-07 10:35:35 306dd32を反映
#2021-10-28 20:28:25 2963b53cを反映
#2021-10-27 18:17:25 タイトルのencodeを忘れてた
#2021-10-25 17:45:05