Skip to content

Commit 45d200f

Browse files
authored
Merge pull request #108 from MijinkoSD/set-codeblock
コードブロックの取得、および上書きを行う関数の追加
2 parents 1d64735 + ca42333 commit 45d200f

File tree

12 files changed

+1407
-0
lines changed

12 files changed

+1407
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
export const snapshot = {};
2+
3+
snapshot[`extractFromCodeTitle() > accurate titles > "code:foo.extA(extB)" 1`] = `
4+
{
5+
filename: "foo.extA",
6+
indent: 0,
7+
lang: "extB",
8+
}
9+
`;
10+
11+
snapshot[`extractFromCodeTitle() > accurate titles > " code:foo.extA(extB)" 1`] = `
12+
{
13+
filename: "foo.extA",
14+
indent: 1,
15+
lang: "extB",
16+
}
17+
`;
18+
19+
snapshot[`extractFromCodeTitle() > accurate titles > " code: foo.extA (extB)" 1`] = `
20+
{
21+
filename: "foo.extA",
22+
indent: 2,
23+
lang: "extB",
24+
}
25+
`;
26+
27+
snapshot[`extractFromCodeTitle() > accurate titles > " code: foo (extB) " 1`] = `
28+
{
29+
filename: "foo",
30+
indent: 2,
31+
lang: "extB",
32+
}
33+
`;
34+
35+
snapshot[`extractFromCodeTitle() > accurate titles > " code: foo.extA " 1`] = `
36+
{
37+
filename: "foo.extA",
38+
indent: 2,
39+
lang: "extA",
40+
}
41+
`;
42+
43+
snapshot[`extractFromCodeTitle() > accurate titles > " code: foo " 1`] = `
44+
{
45+
filename: "foo",
46+
indent: 2,
47+
lang: "foo",
48+
}
49+
`;
50+
51+
snapshot[`extractFromCodeTitle() > accurate titles > " code: .foo " 1`] = `
52+
{
53+
filename: ".foo",
54+
indent: 2,
55+
lang: ".foo",
56+
}
57+
`;

browser/websocket/_codeBlock.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/// <reference lib="deno.ns" />
2+
3+
import { assertEquals, assertSnapshot } from "../../deps/testing.ts";
4+
import { extractFromCodeTitle } from "./_codeBlock.ts";
5+
6+
Deno.test("extractFromCodeTitle()", async (t) => {
7+
await t.step("accurate titles", async (st) => {
8+
const titles = [
9+
"code:foo.extA(extB)",
10+
" code:foo.extA(extB)",
11+
" code: foo.extA (extB)",
12+
" code: foo (extB) ",
13+
" code: foo.extA ",
14+
" code: foo ",
15+
" code: .foo ",
16+
];
17+
for (const title of titles) {
18+
await st.step(`"${title}"`, async (sst) => {
19+
await assertSnapshot(sst, extractFromCodeTitle(title));
20+
});
21+
}
22+
});
23+
24+
await t.step("inaccurate titles", async (st) => {
25+
const nonTitles = [
26+
" code: foo. ", // コードブロックにはならないので`null`が正常
27+
"any:code: foo ",
28+
" I'm not code block ",
29+
];
30+
for (const title of nonTitles) {
31+
await st.step(`"${title}"`, async () => {
32+
await assertEquals(null, extractFromCodeTitle(title));
33+
});
34+
}
35+
});
36+
});

browser/websocket/_codeBlock.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Change, Socket, wrap } from "../../deps/socket.ts";
2+
import { TinyCodeBlock } from "../../rest/getCodeBlocks.ts";
3+
import { HeadData } from "./pull.ts";
4+
import { getProjectId, getUserId } from "./id.ts";
5+
import { pushWithRetry } from "./_fetch.ts";
6+
7+
/** コードブロックのタイトル行の情報を保持しておくためのinterface */
8+
export interface CodeTitle {
9+
filename: string;
10+
lang: string;
11+
indent: number;
12+
}
13+
14+
/** コミットを送信する一連の処理 */
15+
export const applyCommit = async (
16+
commits: Change[],
17+
head: HeadData,
18+
projectName: string,
19+
pageTitle: string,
20+
socket: Socket,
21+
userId?: string,
22+
): ReturnType<typeof pushWithRetry> => {
23+
const [projectId, userId_] = await Promise.all([
24+
getProjectId(projectName),
25+
userId ?? getUserId(),
26+
]);
27+
const { request } = wrap(socket);
28+
return await pushWithRetry(request, commits, {
29+
parentId: head.commitId,
30+
projectId: projectId,
31+
pageId: head.pageId,
32+
userId: userId_,
33+
project: projectName,
34+
title: pageTitle,
35+
retry: 3,
36+
});
37+
};
38+
39+
/** コードブロックのタイトル行から各種プロパティを抽出する
40+
*
41+
* @param lineText {string} 行テキスト
42+
* @return `lineText`がコードタイトル行であれば`CodeTitle`を、そうでなければ`null`を返す
43+
*/
44+
export const 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+
71+
/** コードブロック本文のインデント数を計算する */
72+
export function countBodyIndent(
73+
codeBlock: Pick<TinyCodeBlock, "titleLine">,
74+
): number {
75+
return codeBlock.titleLine.text.length -
76+
codeBlock.titleLine.text.trimStart().length + 1;
77+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { assert, assertFalse } from "../../deps/testing.ts";
2+
import { isSimpleCodeFile } from "./isSimpleCodeFile.ts";
3+
import { SimpleCodeFile } from "./updateCodeFile.ts";
4+
5+
const codeFile: SimpleCodeFile = {
6+
filename: "filename",
7+
content: ["line 0", "line 1"],
8+
lang: "language",
9+
};
10+
11+
Deno.test("isSimpleCodeFile()", async (t) => {
12+
await t.step("SimpleCodeFile object", () => {
13+
assert(isSimpleCodeFile(codeFile));
14+
assert(isSimpleCodeFile({ ...codeFile, content: "line 0" }));
15+
assert(isSimpleCodeFile({ ...codeFile, lang: undefined }));
16+
});
17+
await t.step("similer objects", () => {
18+
assertFalse(isSimpleCodeFile({ ...codeFile, filename: 10 }));
19+
assertFalse(isSimpleCodeFile({ ...codeFile, content: 10 }));
20+
assertFalse(isSimpleCodeFile({ ...codeFile, content: [0, 1] }));
21+
assertFalse(isSimpleCodeFile({ ...codeFile, lang: 10 }));
22+
});
23+
await t.step("other type values", () => {
24+
assertFalse(isSimpleCodeFile(10));
25+
assertFalse(isSimpleCodeFile(undefined));
26+
assertFalse(isSimpleCodeFile(["0", "1", "2"]));
27+
});
28+
});

browser/websocket/isSimpleCodeFile.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { SimpleCodeFile } from "./updateCodeFile.ts";
2+
3+
/** objectがSimpleCodeFile型かどうかを判別する */
4+
export function isSimpleCodeFile(obj: unknown): obj is SimpleCodeFile {
5+
if (Array.isArray(obj) || !(obj instanceof Object)) return false;
6+
const code = obj as SimpleCodeFile;
7+
const { filename, content, lang } = code;
8+
return (
9+
typeof filename == "string" &&
10+
(typeof content == "string" ||
11+
(Array.isArray(content) &&
12+
(content.length == 0 || typeof content[0] == "string"))) &&
13+
(typeof lang == "string" || lang === undefined)
14+
);
15+
}

browser/websocket/mod.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ export * from "./patch.ts";
33
export * from "./deletePage.ts";
44
export * from "./pin.ts";
55
export * from "./listen.ts";
6+
export * from "./updateCodeBlock.ts";
7+
export * from "./updateCodeFile.ts";

browser/websocket/updateCodeBlock.ts

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

0 commit comments

Comments
 (0)