scrapbox-headless-script
/icons/hr.icon
名前
機能
joinPageRoom()
特定ページを操作する
戻り値の函数で操作する
insert(): 任意位置への文字列挿入
update(): 文字列の書き換え
delete(): 文字列の削除
listenPageUpdate(): ページの更新購読
patch(): ページ全体を書き換える
scrapbox.ioには差分だけ送信する
deletePage()
ページを削除する
listenStream()
指定したprojectのStreamを購読する
実装したいこと
cursor flag操作
ピンの付け外し
pin()
unpin()
タイトルの変更
update()でも実現出来るが、helper函数を用意したい
/icons/done.iconpatch(update)のupdateに同期函数・非同期函数のどちらでも指定できるようにする
判定方法
戻り値がPromiseかどうかをみればいいだろう
serverから送られてくるページの更新情報を反映する
これを使えばupdatedを再現できる
pinの付け外しも反映させる
エラー処理をちゃんとやる
エラーの型定義を作る
update()で複数行を指定できるようにする
以下のどちらに挙動を寄せるかで悩みそう
1行目のみ上書きし、残りの行はその下に挿入する
全て上書きする
指定行以降の行を、指定した行の数だけ書き換える
元の行数が指定した行数より少なかった場合は、その分だけ下に挿入する
そういえば、Change.lines.textにわざと改行を含んだ文字列を入れるとどうなるんだろう?
roomの切り替え機能を入れる?
JSDocでdocumentを作る
面倒だし、日本語で書いてしまおうか
refactoring
実装した方がいいけど難しそうなやつ
conflict解決
考えることがいろいろある
commitの衝突パターン
パターンごとに可能なmerge方法
merge出来なかった時の挙動
2022-01-04
16:42:58 release v0.2.0
2021-12-22
19:47:17 タイトルとサムネイルの更新などを実装している
あと実装すること
サムネイル画像の更新
code:ts
export interface ImageCommit {
image: string;
}
Gyazo URLの場合は、https://gyazo.com/.../rawに変換される
2021-12-29 型を追加した
リンク情報の更新
applyCommits()のテスト
2021-12-29
03:47:03 descriptionsの変更検知処理が二度手間になっているのを直したい
とくにpatch()の効率が悪い
2つの文字列から計算した差分情報から、元の2文字列を再び計算し直している
insert(), update(), delete()と同じdescriptions計算処理を用いているのが原因
実装
このlibraryは面倒な手続きを色々隠して、使いやすいinterfaceを提供するようにしている
本当に使いやすくなったかどうかはわからないがtakker.icon
少なくともprojectIdやcommitIdを操作しなくて済むようになってはいる
code: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&&iw===of;)++w,++f;return cs+n=p.length,p.push([{x:w,y:f},cs+(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)ds+n=l(s,ds-1+n+1,ds+1+n);for(let s=h+x;s>=h+1;--s)ds+n=l(s,ds-1+n+1,ds+1+n);dh+n=l(h,dh-1+n+1,dh+1+n)}while(dh+n!==o.length);let m=[],y=ch+n;for(;y!==-1;)m.push(py0),y=py1;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:og,type:r?"deleted":"added"},++g):f-u<g-s?(yield{value:is,type:r?"added":"deleted"},++s):(yield{value:is,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(to,ro);for(let o=r.length;o<t.length;o++)yield to}else{for(let o=0;o<t.length;o++)yield z(to,ro);for(let o=t.length;o<r.length;o++)yield ro}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 et}function*D(e,t,{userId:r}){let{buildSES:i}=J(e.map(({text:a})=>a),t),o=0,n=e0.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=eo?.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);oc.text=a.lines.text,oc.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){letr,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});(c0.text!==s0.text||!a)&&m.push({title:s0.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};