Skip to content

Commit 03240ad

Browse files
committed
指定したコードブロックを書き換えられるようにした
1 parent 488d288 commit 03240ad

File tree

4 files changed

+217
-45
lines changed

4 files changed

+217
-45
lines changed

browser/websocket/_codeBlock.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,70 @@ import { getProjectId, getUserId } from "./id.ts";
44
import { pushWithRetry } from "./_fetch.ts";
55
import { TinyCodeBlock } from "./getCodeBlocks.ts";
66

7+
/** コードブロックのタイトル行の情報を保持しておくためのinterface */
8+
export interface CodeTitle {
9+
filename: string;
10+
lang: string;
11+
indent: number;
12+
}
13+
714
/** コミットを送信する一連の処理 */
815
export async function applyCommit(
916
commits: Change[],
1017
head: HeadData,
1118
projectName: string,
1219
pageTitle: string,
1320
socket: Socket,
21+
userId?: string,
1422
): ReturnType<typeof pushWithRetry> {
15-
const [projectId, userId] = await Promise.all([
23+
const [projectId, userId_] = await Promise.all([
1624
getProjectId(projectName),
17-
getUserId(),
25+
userId ?? getUserId(),
1826
]);
1927
const { request } = wrap(socket);
2028
return await pushWithRetry(request, commits, {
2129
parentId: head.commitId,
2230
projectId: projectId,
2331
pageId: head.pageId,
24-
userId: userId,
32+
userId: userId_,
2533
project: projectName,
2634
title: pageTitle,
2735
retry: 3,
2836
});
2937
}
3038

39+
/** コードブロックのタイトル行から各種プロパティを抽出する
40+
*
41+
* @param lineText {string} 行テキスト
42+
* @return `lineText`がコードタイトル行であれば`CodeTitle`を、そうでなければ`null`を返す
43+
*/
44+
export function extractFromCodeTitle(lineText: string): CodeTitle | null {
45+
const matched = lineText.match(/^(\s*)code:(.+?)(\(.+\)){0,1}\s*$/);
46+
if (matched === null) return null;
47+
const filename = matched[2].trim();
48+
let lang = "";
49+
if (matched[3] === undefined) {
50+
const ext = filename.match(/.+\.(.*)$/);
51+
if (ext === null) {
52+
// `code:ext`
53+
lang = filename;
54+
} else if (ext[1] === "") {
55+
// `code:foo.`の形式はコードブロックとして成り立たないので排除する
56+
return null;
57+
} else {
58+
// `code:foo.ext`
59+
lang = ext[1].trim();
60+
}
61+
} else {
62+
lang = matched[3].slice(1, -1);
63+
}
64+
return {
65+
filename: filename,
66+
lang: lang,
67+
indent: matched[1].length,
68+
};
69+
}
70+
3171
/** コードブロック本文のインデント数を計算する */
3272
export function countBodyIndent(
3373
codeBlock: Pick<TinyCodeBlock, "titleLine">,

browser/websocket/getCodeBlocks.ts

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Line } from "../../deps/scrapbox-rest.ts";
22
import { pull } from "./pull.ts";
3+
import { CodeTitle, extractFromCodeTitle } from "./_codeBlock.ts";
34

45
/** pull()から取れる情報で構成したコードブロックの最低限の情報 */
56
export interface TinyCodeBlock {
@@ -30,13 +31,6 @@ export interface GetCodeBlocksFilter {
3031
lang?: string;
3132
}
3233

33-
/** コードブロックのタイトル行の情報を保持しておくためのinterface */
34-
interface CodeTitle {
35-
fileName: string;
36-
lang: string;
37-
indent: number;
38-
}
39-
4034
/** 他のページ(または取得済みの行データ)のコードブロックを全て取得する
4135
*
4236
* ファイル単位ではなく、コードブロック単位で返り値を生成する \
@@ -61,7 +55,7 @@ export const getCodeBlocks = async (
6155
} = {
6256
isCodeBlock: false,
6357
isCollect: false,
64-
fileName: "",
58+
filename: "",
6559
lang: "",
6660
indent: 0,
6761
};
@@ -87,7 +81,7 @@ export const getCodeBlocks = async (
8781
currentCode = { isCodeBlock: true, isCollect: isCollect, ...matched };
8882
if (!currentCode.isCollect) continue;
8983
codeBlocks.push({
90-
filename: currentCode.fileName,
84+
filename: currentCode.filename,
9185
lang: currentCode.lang,
9286
titleLine: line,
9387
bodyLines: [],
@@ -114,44 +108,12 @@ async function getLines(
114108
}
115109
}
116110

117-
/** コードブロックのタイトル行から各種プロパティを抽出する
118-
*
119-
* @param lineText {string} 行テキスト
120-
* @return `lineText`がコードタイトル行であれば`CodeTitle`を、そうでなければ`null`を返す
121-
*/
122-
function extractFromCodeTitle(lineText: string): CodeTitle | null {
123-
const matched = lineText.match(/^(\s*)code:(.+?)(\(.+\)){0,1}\s*$/);
124-
if (matched === null) return null;
125-
const fileName = matched[2].trim();
126-
let lang = "";
127-
if (matched[3] === undefined) {
128-
const ext = fileName.match(/.+\.(.*)$/);
129-
if (ext === null) {
130-
// `code:ext`
131-
lang = fileName;
132-
} else if (ext[1] === "") {
133-
// `code:foo.`の形式はコードブロックとして成り立たないので排除する
134-
return null;
135-
} else {
136-
// `code:foo.ext`
137-
lang = ext[1].slice(1, -1).trim();
138-
}
139-
} else {
140-
lang = matched[3];
141-
}
142-
return {
143-
fileName: fileName,
144-
lang: lang,
145-
indent: matched[1].length,
146-
};
147-
}
148-
149111
/** コードタイトルのフィルターを検証する */
150112
function isMatchFilter(
151113
codeTitle: CodeTitle,
152114
filter?: GetCodeBlocksFilter,
153115
): boolean {
154-
if (filter?.filename && filter.filename !== codeTitle.fileName) return false;
116+
if (filter?.filename && filter.filename !== codeTitle.filename) return false;
155117
if (filter?.lang && filter.lang !== codeTitle.lang) return false;
156118
return true;
157119
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { Line } from "../../deps/scrapbox-rest.ts";
2+
import {
3+
DeleteCommit,
4+
InsertCommit,
5+
Socket,
6+
socketIO,
7+
UpdateCommit,
8+
} from "../../deps/socket.ts";
9+
import { diffToChanges } from "./diffToChanges.ts";
10+
import { TinyCodeBlock } from "./getCodeBlocks.ts";
11+
import { getUserId } from "./id.ts";
12+
import { pull } from "./pull.ts";
13+
import { CodeFile, isCodeFile } from "./updateCodeFile.ts";
14+
import {
15+
applyCommit,
16+
countBodyIndent,
17+
extractFromCodeTitle,
18+
} from "./_codeBlock.ts";
19+
20+
export interface UpdateCodeBlockOptions {
21+
/** WebSocketの通信に使うsocket */
22+
socket?: Socket;
23+
24+
/** `true`でデバッグ出力ON */
25+
debug?: boolean;
26+
}
27+
28+
/** コードブロックの中身を更新する
29+
*
30+
* @param newCode 更新後のコードブロック
31+
* @param target 更新対象のコードブロック
32+
* @param project 更新対象のコードブロックが存在するプロジェクト名
33+
*/
34+
export const updateCodeBlock = async (
35+
newCode: string | string[] | CodeFile,
36+
target: TinyCodeBlock,
37+
options?: UpdateCodeBlockOptions,
38+
) => {
39+
/** optionsの既定値はこの中に入れる */
40+
const defaultOptions: Required<UpdateCodeBlockOptions> = {
41+
socket: options?.socket ?? await socketIO(),
42+
debug: false,
43+
};
44+
const opt = options ? { ...defaultOptions, ...options } : defaultOptions;
45+
const { projectName, pageTitle } = target.pageInfo;
46+
const [
47+
head,
48+
userId,
49+
] = await Promise.all([
50+
pull(projectName, pageTitle),
51+
getUserId(),
52+
]);
53+
const newCodeBody = getCodeBody(newCode);
54+
const bodyIndent = countBodyIndent(target);
55+
const oldCodeWithoutIndent: Line[] = target.bodyLines.map((e) => {
56+
return { ...e, text: e.text.slice(bodyIndent) };
57+
});
58+
59+
const diffGenerator = diffToChanges(oldCodeWithoutIndent, newCodeBody, {
60+
userId,
61+
});
62+
const commits = [...fixCommits([...diffGenerator], target)];
63+
if (isCodeFile(newCode)) {
64+
const titleCommit = makeTitleChangeCommit(newCode, target);
65+
if (titleCommit) commits.push(titleCommit);
66+
}
67+
68+
if (opt.debug) {
69+
console.log("vvv original code block vvv");
70+
console.log(target);
71+
console.log("vvv new codes vvv");
72+
console.log(newCode);
73+
console.log("vvv commits vvv");
74+
console.log(commits);
75+
}
76+
77+
await applyCommit(commits, head, projectName, pageTitle, opt.socket, userId);
78+
if (!options?.socket) opt.socket.disconnect();
79+
};
80+
81+
function getCodeBody(code: string | string[] | CodeFile): string[] {
82+
const content = isCodeFile(code) ? code.content : code;
83+
if (Array.isArray(content)) return content;
84+
return content.split("\n");
85+
}
86+
87+
/** insertコミットの行IDとtextのインデントを修正する */
88+
function* fixCommits(
89+
commits: readonly (DeleteCommit | InsertCommit | UpdateCommit)[],
90+
target: TinyCodeBlock,
91+
): Generator<DeleteCommit | InsertCommit | UpdateCommit, void, unknown> {
92+
const { nextLine } = target;
93+
const indent = " ".repeat(countBodyIndent(target));
94+
for (const commit of commits) {
95+
if ("_delete" in commit) {
96+
yield commit;
97+
} else if (
98+
"_update" in commit
99+
) {
100+
yield {
101+
...commit,
102+
lines: {
103+
...commit.lines,
104+
text: indent + commit.lines.text,
105+
},
106+
};
107+
} else if (
108+
commit._insert != "_end" ||
109+
nextLine === null
110+
) {
111+
yield {
112+
...commit,
113+
lines: {
114+
...commit.lines,
115+
text: indent + commit.lines.text,
116+
},
117+
};
118+
} else {
119+
yield {
120+
_insert: nextLine.id,
121+
lines: {
122+
...commit.lines,
123+
text: indent + commit.lines.text,
124+
},
125+
};
126+
}
127+
}
128+
}
129+
130+
/** コードタイトルが違う場合は書き換える */
131+
function makeTitleChangeCommit(
132+
code: CodeFile,
133+
target: Pick<TinyCodeBlock, "titleLine">,
134+
): UpdateCommit | null {
135+
const lineId = target.titleLine.id;
136+
const targetTitle = extractFromCodeTitle(target.titleLine.text);
137+
if (
138+
targetTitle &&
139+
code.filename.trim() == targetTitle.filename &&
140+
code.lang?.trim() == targetTitle.lang
141+
) return null;
142+
const ext = (() => {
143+
const matched = code.filename.match(/.+\.(.*)$/);
144+
if (matched === null) return code.filename;
145+
else if (matched[1] === "") return "";
146+
else return matched[1].trim();
147+
})();
148+
const title = code.filename +
149+
(code.lang && code.lang != ext ? `(${code.lang})` : "");
150+
return {
151+
_update: lineId,
152+
lines: {
153+
text: " ".repeat(countBodyIndent(target) - 1) + "code:" + title,
154+
},
155+
};
156+
}

browser/websocket/updateCodeFile.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ export interface UpdateCodeFileOptions {
4545
debug?: boolean;
4646
}
4747

48+
/** objectがCodeFile型かどうかを判別する */
49+
export function isCodeFile(obj: unknown): obj is CodeFile {
50+
if (Array.isArray(obj) || !(obj instanceof Object)) return false;
51+
const code = obj as CodeFile;
52+
const { filename, content, lang } = code;
53+
return (
54+
typeof filename == "string" &&
55+
(typeof content == "string" ||
56+
(Array.isArray(content) &&
57+
(content.length == 0 || typeof content[0] == "string"))) &&
58+
(typeof lang == "string" || lang === undefined)
59+
);
60+
}
61+
4862
/** REST API経由で取得できるようなコードファイルの中身をまるごと書き換える
4963
*
5064
* ファイルが存在していなかった場合、既定では何も書き換えない \

0 commit comments

Comments
 (0)