Skip to content

Commit d38d4ff

Browse files
committed
updateCodeFiles.tsを追加
`api/code/:projectname/:pagetitle/:filename`で取得できるコードを直接書き換えることが出来る
1 parent cb9d58e commit d38d4ff

File tree

1 file changed

+265
-0
lines changed

1 file changed

+265
-0
lines changed
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import type { Line } from "../../deps/scrapbox-rest.ts";
2+
import {
3+
Change,
4+
DeleteCommit,
5+
InsertCommit,
6+
Socket,
7+
socketIO,
8+
UpdateCommit,
9+
wrap,
10+
} from "../../deps/socket.ts";
11+
import { HeadData, pull } from "./pull.ts";
12+
import { getCodeBlocks, TinyCodeBlock } from "./getCodeBlocks.ts";
13+
import { diffToChanges } from "./diffToChanges.ts";
14+
import { createNewLineId, getProjectId, getUserId } from "./id.ts";
15+
import { pushCommit } from "./_fetch.ts";
16+
17+
/** コードブロックの上書きに使う情報のinterface */
18+
export interface CodeFile {
19+
/** ファイル名 */
20+
filename: string;
21+
22+
/** コードブロックの中身(文字列のみ) */
23+
content: string | string[];
24+
25+
/** コードブロック内の強調表示に使う言語名(省略時はfilenameに含まれる拡張子を使用する) */
26+
lang?: string;
27+
}
28+
29+
/** updateCodeFile()に使われているオプション */
30+
export interface UpdateCodeFileOptions {
31+
/**
32+
* 指定したファイルが存在しなかった時、新しいコードブロックをページのどの位置に配置するか
33+
*
34+
* - `"notInsert"`(既定):存在しなかった場合は何もしない
35+
* - `"top"`:ページ上部(タイトル行の真下)
36+
* - `"bottom"`:ページ下部
37+
*/
38+
insertPositionIfNotExist?: "top" | "bottom" | "notInsert";
39+
40+
/** WebSocketの通信に使うsocket */
41+
socket?: Socket;
42+
43+
/** trueでデバッグ出力ON */
44+
debug?: boolean;
45+
}
46+
47+
/** REST API経由で取得できるようなコードファイルの中身をまるごと書き換える
48+
*
49+
* ファイルが存在していなかった場合、既定では何も書き換えない \
50+
*
51+
* 対象と同じ名前のコードブロックが同じページの複数の行にまたがっていた場合も全て書き換える \
52+
* その際、書き換え後のコードをそれぞれのコードブロックへ分散させるが、それっぽく分けるだけで見た目などは保証しないので注意
53+
*
54+
* @param codeFile 書き換え後のコードファイルの中身
55+
* @param project 書き換えたいページのプロジェクト名(Project urlの設定で使われている方)
56+
* @param title 書き換えたいページのタイトル
57+
* @param options [UpdateCodeFileOptions] その他の設定
58+
*/
59+
export const updateCodeFile = async (
60+
codeFile: CodeFile,
61+
project: string,
62+
title: string,
63+
options?: UpdateCodeFileOptions,
64+
): Promise<void> => {
65+
/** optionsの既定値はこの中に入れる */
66+
const defaultOptions: Required<UpdateCodeFileOptions> = {
67+
insertPositionIfNotExist: "notInsert",
68+
socket: options?.socket ?? await socketIO(),
69+
debug: false,
70+
};
71+
const opt = options ? { ...defaultOptions, ...options } : defaultOptions;
72+
const newCode = Array.isArray(codeFile.content)
73+
? codeFile.content
74+
: codeFile.content.split("\n");
75+
const head = await pull(project, title);
76+
const lines: Line[] = head.lines;
77+
const codeBlocks = await getCodeBlocks({
78+
lines: lines,
79+
}, {
80+
filename: codeFile.filename,
81+
});
82+
const codeBodies = flatCodeBodies(codeBlocks);
83+
84+
if (codeBlocks.length <= 0) {
85+
// 更新対象のコードブロックが存在していなかった場合は、新しいコードブロックを作成して終了する
86+
if (opt.insertPositionIfNotExist === "notInsert") return;
87+
const insertLineId =
88+
opt.insertPositionIfNotExist === "top" && lines.length >= 1
89+
? lines[1].id
90+
: "_end";
91+
const commits = await makeCommitsNewCodeBlock(
92+
codeFile,
93+
insertLineId,
94+
);
95+
if (codeBodies.length <= 0) {
96+
await applyCommit(commits, head, project, opt.socket);
97+
}
98+
return;
99+
} else if (codeBodies.length <= 0) {
100+
// codeBodiesが無かった場合はdiffToChangesが例外を引き起こすので、その対策
101+
const insertLineId = codeBlocks[0].nextLine
102+
? codeBlocks[0].nextLine.id
103+
: "_end";
104+
const commits = await makeCommitsNewCodeBlock(
105+
codeFile,
106+
insertLineId,
107+
);
108+
if (codeBodies.length <= 0) {
109+
await applyCommit(commits.splice(1), head, project, opt.socket);
110+
}
111+
return;
112+
}
113+
114+
const changes = [...diffToChanges(
115+
codeBodies,
116+
newCode,
117+
{ userId: await getUserId() },
118+
)];
119+
120+
// insert行のIDと各行のインデントを修正する
121+
const commits = fixCommits(changes, codeBlocks);
122+
123+
if (opt.debug) {
124+
console.log("vvv original code Blocks vvv");
125+
console.log(codeBlocks);
126+
console.log("vvv original code lines vvv");
127+
console.log(codeBodies);
128+
console.log("vvv new codes vvv");
129+
console.log(newCode);
130+
console.log("vvv commits vvv");
131+
console.log(commits);
132+
}
133+
134+
// 差分を送信
135+
await applyCommit(commits, head, project, opt.socket);
136+
137+
if (!options?.socket) opt.socket.disconnect();
138+
};
139+
140+
/** TinyCodeBlocksの配列からコード本文をフラットな配列に格納して返す \
141+
* その際、コードブロックの左側に存在していたインデントは削除する
142+
*/
143+
function flatCodeBodies(codeBlocks: readonly TinyCodeBlock[]): Line[] {
144+
return codeBlocks.map((block) => {
145+
const title = block.titleLine.text;
146+
const indent = title.length - title.trimStart().length + 1;
147+
return block.bodyLines.map((body) => {
148+
return { ...body, text: body.text.slice(indent) };
149+
});
150+
}).flat();
151+
}
152+
153+
/** コミットを送信する一連の処理 */
154+
async function applyCommit(
155+
commits: Change[],
156+
head: HeadData,
157+
projectName: string,
158+
socket: Socket,
159+
): ReturnType<typeof pushCommit> {
160+
const [projectId, userId] = await Promise.all([
161+
getProjectId(projectName),
162+
getUserId(),
163+
]);
164+
165+
// 3回retryする
166+
for (let i = 0; i < 3; i++) {
167+
try {
168+
// 差分を送信
169+
const { request } = wrap(socket);
170+
const res = await pushCommit(request, commits, {
171+
parentId: head.commitId,
172+
projectId: projectId,
173+
pageId: head.pageId,
174+
userId: userId,
175+
});
176+
return res;
177+
} catch (_e: unknown) {
178+
console.log(
179+
"Faild to push a commit.",
180+
);
181+
if (i === 2) break;
182+
}
183+
}
184+
throw Error("Faild to retry pushing.");
185+
}
186+
187+
/** 新規コードブロックのコミットを作成する */
188+
async function makeCommitsNewCodeBlock(
189+
code: CodeFile,
190+
insertLineId: string,
191+
): Promise<InsertCommit[]> {
192+
const userId = await getUserId();
193+
const codeName = code.filename + (code.lang ? `(${code.lang})` : "");
194+
const codeBody = Array.isArray(code.content)
195+
? code.content
196+
: code.content.split("\n");
197+
const commits: InsertCommit[] = [{
198+
_insert: insertLineId,
199+
lines: {
200+
id: createNewLineId(userId),
201+
text: `code:${codeName}`,
202+
},
203+
}];
204+
for (const bodyLine of codeBody) {
205+
commits.push({
206+
_insert: insertLineId,
207+
lines: {
208+
id: createNewLineId(userId),
209+
text: " " + bodyLine,
210+
},
211+
});
212+
}
213+
return commits;
214+
}
215+
216+
/** insert行のIDと各行のインデントを修正する */
217+
function fixCommits(
218+
commits: (InsertCommit | UpdateCommit | DeleteCommit)[],
219+
codeBlocks: TinyCodeBlock[],
220+
) {
221+
const idReplacePatterns: {
222+
from: string;
223+
to: string;
224+
// indent: number;
225+
}[] = (() => {
226+
const patterns = [];
227+
for (let i = 0; i < codeBlocks.length; i++) {
228+
// コード本体の先頭ID -> 1つ前のコードブロックの真下の行のID
229+
const currentCode = codeBlocks[i];
230+
const nextCode = codeBlocks[i + 1];
231+
if (!currentCode.nextLine) continue;
232+
patterns.push({
233+
from: nextCode?.bodyLines[0].id ?? "_end",
234+
to: currentCode.nextLine.id,
235+
});
236+
}
237+
return patterns;
238+
})();
239+
for (const commit of commits) {
240+
if ("_delete" in commit) continue;
241+
else if ("_insert" in commit) {
242+
// ID修正
243+
for (const pattern of idReplacePatterns) {
244+
if (commit._insert !== pattern.from) continue;
245+
commit._insert = pattern.to;
246+
break;
247+
}
248+
}
249+
// インデント挿入
250+
const belongBlock = codeBlocks.find((block) => {
251+
const targetId = "_update" in commit ? commit._update : commit._insert;
252+
if (block.bodyLines.some((e) => e.id === targetId)) return true;
253+
if ("_update" in commit) return false;
254+
if (targetId === block.nextLine?.id) return true;
255+
return false;
256+
});
257+
console.log("vvv belong vvv");
258+
console.log(belongBlock);
259+
if (belongBlock === undefined) continue;
260+
const titleText = belongBlock.titleLine.text;
261+
const indent = titleText.length - titleText.trimStart().length + 1;
262+
commit.lines.text = " ".repeat(indent) + commit.lines.text;
263+
}
264+
return commits;
265+
}

0 commit comments

Comments
 (0)