Skip to content

コードブロックの取得、および上書きを行う関数の追加 #108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Mar 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
cdba538
BaseLine配列からコードブロックを抽出する函数を追加
MijinkoSD Feb 6, 2023
b4bf4ff
途中計算の記述ミスを修正
MijinkoSD Feb 7, 2023
62a1753
TinyCodeBlockにnextLineプロパティを追加
MijinkoSD Feb 7, 2023
61ee5c6
拡張子の解析がうまくいっていなかった
MijinkoSD Feb 7, 2023
3f9cdde
わかりにくいJSDocを修正
MijinkoSD Feb 8, 2023
6fea813
getCodeBlocksをfilterに対応させた
MijinkoSD Feb 8, 2023
c530c2c
設定ミスを修正
MijinkoSD Feb 8, 2023
b04d815
リテラル型の不適切な使い方がされていた
MijinkoSD Feb 8, 2023
cb9d58e
filterの適用が不完全だった
MijinkoSD Feb 9, 2023
d38d4ff
`updateCodeFiles.ts`を追加
MijinkoSD Feb 9, 2023
e42ba95
コメントの削除し忘れ
MijinkoSD Feb 9, 2023
dc3c398
Merge branch 'main' into set-codeblock
MijinkoSD Feb 9, 2023
626fb4a
若干refactoringした
MijinkoSD Feb 10, 2023
c563d15
`updateCodeFile.ts`の一部函数を`_codeBlock.ts`へ分割
MijinkoSD Feb 10, 2023
e188921
refactoringを試みた
MijinkoSD Feb 13, 2023
59b88ff
既存のコードブロックが無かった際に使用するコードを最小限にした
MijinkoSD Feb 13, 2023
821844c
JSDocの一貫性のなさを修正
MijinkoSD Feb 13, 2023
42cea00
コードブロック本体のインデント計算処理を分けた
MijinkoSD Feb 14, 2023
488d288
TinyCodeBlockの拡張と、それに伴うupdateCodeFileの破壊的変更
MijinkoSD Feb 14, 2023
03240ad
指定したコードブロックを書き換えられるようにした
MijinkoSD Feb 14, 2023
a9471a6
JSDocを追記
MijinkoSD Feb 14, 2023
4d727ec
`updateCodeBlock()`のJSDocの書き忘れを追記
MijinkoSD Feb 14, 2023
2594880
外部へexportする
MijinkoSD Feb 15, 2023
721ffc1
`getCodeBlocks.ts`を`rest/`以下へ移動
MijinkoSD Feb 16, 2023
c31e115
`_codeBlocks.ts`のimport元の修正し忘れ
MijinkoSD Feb 16, 2023
663fd1b
`getCodeBlocks()`のテストコードを追加
MijinkoSD Feb 16, 2023
3f82ce8
isCodeFile()のテストコードを追加
MijinkoSD Feb 16, 2023
b440bd0
extractFromCodeTitle()のテストコードを追加
MijinkoSD Feb 16, 2023
5baa0a9
`CodeFile`を`SimpleCodeFile`へ変更
MijinkoSD Feb 17, 2023
7116444
deno fmtのやり忘れを修正
MijinkoSD Feb 17, 2023
a5f735a
`isSimpleCodeFile()`を別ファイルへ分離
MijinkoSD Feb 18, 2023
8e45e09
deno fmtのやり忘れ
MijinkoSD Feb 18, 2023
ca6e48c
debug文に色を付けた
MijinkoSD Feb 24, 2023
5b53e79
`getCodeBlocks()`のfilter処理のrefactoring
MijinkoSD Feb 28, 2023
37cbabf
`assertSnapshot()`に頼り切りにならない実装にした
MijinkoSD Feb 28, 2023
ee8566b
`isMatchFilter()`の実装を少し変更
MijinkoSD Feb 28, 2023
b871342
`getCodeBlocks()`がタイトル行の行IDのfilterに対応
MijinkoSD Feb 28, 2023
24006a9
行の末尾に`\r`が含まれていた場合`bodyLines`の取得に失敗していた
MijinkoSD Mar 1, 2023
ec79ffd
Fix: functionをarrow functionにする
MijinkoSD Mar 24, 2023
ca42333
Fix: functionをarrow functionにする(完結編)
MijinkoSD Mar 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions browser/websocket/__snapshots__/_codeBlock.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export const snapshot = {};

snapshot[`extractFromCodeTitle() > accurate titles > "code:foo.extA(extB)" 1`] = `
{
filename: "foo.extA",
indent: 0,
lang: "extB",
}
`;

snapshot[`extractFromCodeTitle() > accurate titles > " code:foo.extA(extB)" 1`] = `
{
filename: "foo.extA",
indent: 1,
lang: "extB",
}
`;

snapshot[`extractFromCodeTitle() > accurate titles > " code: foo.extA (extB)" 1`] = `
{
filename: "foo.extA",
indent: 2,
lang: "extB",
}
`;

snapshot[`extractFromCodeTitle() > accurate titles > " code: foo (extB) " 1`] = `
{
filename: "foo",
indent: 2,
lang: "extB",
}
`;

snapshot[`extractFromCodeTitle() > accurate titles > " code: foo.extA " 1`] = `
{
filename: "foo.extA",
indent: 2,
lang: "extA",
}
`;

snapshot[`extractFromCodeTitle() > accurate titles > " code: foo " 1`] = `
{
filename: "foo",
indent: 2,
lang: "foo",
}
`;

snapshot[`extractFromCodeTitle() > accurate titles > " code: .foo " 1`] = `
{
filename: ".foo",
indent: 2,
lang: ".foo",
}
`;
36 changes: 36 additions & 0 deletions browser/websocket/_codeBlock.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/// <reference lib="deno.ns" />

import { assertEquals, assertSnapshot } from "../../deps/testing.ts";
import { extractFromCodeTitle } from "./_codeBlock.ts";

Deno.test("extractFromCodeTitle()", async (t) => {
await t.step("accurate titles", async (st) => {
const titles = [
"code:foo.extA(extB)",
" code:foo.extA(extB)",
" code: foo.extA (extB)",
" code: foo (extB) ",
" code: foo.extA ",
" code: foo ",
" code: .foo ",
];
for (const title of titles) {
await st.step(`"${title}"`, async (sst) => {
await assertSnapshot(sst, extractFromCodeTitle(title));
});
}
});

await t.step("inaccurate titles", async (st) => {
const nonTitles = [
" code: foo. ", // コードブロックにはならないので`null`が正常
"any:code: foo ",
" I'm not code block ",
];
for (const title of nonTitles) {
await st.step(`"${title}"`, async () => {
await assertEquals(null, extractFromCodeTitle(title));
});
}
});
});
77 changes: 77 additions & 0 deletions browser/websocket/_codeBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Change, Socket, wrap } from "../../deps/socket.ts";
import { TinyCodeBlock } from "../../rest/getCodeBlocks.ts";
import { HeadData } from "./pull.ts";
import { getProjectId, getUserId } from "./id.ts";
import { pushWithRetry } from "./_fetch.ts";

/** コードブロックのタイトル行の情報を保持しておくためのinterface */
export interface CodeTitle {
filename: string;
lang: string;
indent: number;
}

/** コミットを送信する一連の処理 */
export const applyCommit = async (
commits: Change[],
head: HeadData,
projectName: string,
pageTitle: string,
socket: Socket,
userId?: string,
): ReturnType<typeof pushWithRetry> => {
const [projectId, userId_] = await Promise.all([
getProjectId(projectName),
userId ?? getUserId(),
]);
const { request } = wrap(socket);
return await pushWithRetry(request, commits, {
parentId: head.commitId,
projectId: projectId,
pageId: head.pageId,
userId: userId_,
project: projectName,
title: pageTitle,
retry: 3,
});
};

/** コードブロックのタイトル行から各種プロパティを抽出する
*
* @param lineText {string} 行テキスト
* @return `lineText`がコードタイトル行であれば`CodeTitle`を、そうでなければ`null`を返す
*/
export const extractFromCodeTitle = (lineText: string): CodeTitle | null => {
const matched = lineText.match(/^(\s*)code:(.+?)(\(.+\)){0,1}\s*$/);
if (matched === null) return null;
const filename = matched[2].trim();
let lang = "";
if (matched[3] === undefined) {
const ext = filename.match(/.+\.(.*)$/);
if (ext === null) {
// `code:ext`
lang = filename;
} else if (ext[1] === "") {
// `code:foo.`の形式はコードブロックとして成り立たないので排除する
return null;
} else {
// `code:foo.ext`
lang = ext[1].trim();
}
} else {
lang = matched[3].slice(1, -1);
}
return {
filename: filename,
lang: lang,
indent: matched[1].length,
};
};

/** コードブロック本文のインデント数を計算する */
export function countBodyIndent(
codeBlock: Pick<TinyCodeBlock, "titleLine">,
): number {
return codeBlock.titleLine.text.length -
codeBlock.titleLine.text.trimStart().length + 1;
}
28 changes: 28 additions & 0 deletions browser/websocket/isSimpleCodeFile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { assert, assertFalse } from "../../deps/testing.ts";
import { isSimpleCodeFile } from "./isSimpleCodeFile.ts";
import { SimpleCodeFile } from "./updateCodeFile.ts";

const codeFile: SimpleCodeFile = {
filename: "filename",
content: ["line 0", "line 1"],
lang: "language",
};

Deno.test("isSimpleCodeFile()", async (t) => {
await t.step("SimpleCodeFile object", () => {
assert(isSimpleCodeFile(codeFile));
assert(isSimpleCodeFile({ ...codeFile, content: "line 0" }));
assert(isSimpleCodeFile({ ...codeFile, lang: undefined }));
});
await t.step("similer objects", () => {
assertFalse(isSimpleCodeFile({ ...codeFile, filename: 10 }));
assertFalse(isSimpleCodeFile({ ...codeFile, content: 10 }));
assertFalse(isSimpleCodeFile({ ...codeFile, content: [0, 1] }));
assertFalse(isSimpleCodeFile({ ...codeFile, lang: 10 }));
});
await t.step("other type values", () => {
assertFalse(isSimpleCodeFile(10));
assertFalse(isSimpleCodeFile(undefined));
assertFalse(isSimpleCodeFile(["0", "1", "2"]));
});
});
15 changes: 15 additions & 0 deletions browser/websocket/isSimpleCodeFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SimpleCodeFile } from "./updateCodeFile.ts";

/** objectがSimpleCodeFile型かどうかを判別する */
export function isSimpleCodeFile(obj: unknown): obj is SimpleCodeFile {
if (Array.isArray(obj) || !(obj instanceof Object)) return false;
const code = obj as SimpleCodeFile;
const { filename, content, lang } = code;
return (
typeof filename == "string" &&
(typeof content == "string" ||
(Array.isArray(content) &&
(content.length == 0 || typeof content[0] == "string"))) &&
(typeof lang == "string" || lang === undefined)
);
}
2 changes: 2 additions & 0 deletions browser/websocket/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export * from "./patch.ts";
export * from "./deletePage.ts";
export * from "./pin.ts";
export * from "./listen.ts";
export * from "./updateCodeBlock.ts";
export * from "./updateCodeFile.ts";
161 changes: 161 additions & 0 deletions browser/websocket/updateCodeBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { Line } from "../../deps/scrapbox-rest.ts";
import {
DeleteCommit,
InsertCommit,
Socket,
socketIO,
UpdateCommit,
} from "../../deps/socket.ts";
import { TinyCodeBlock } from "../../rest/getCodeBlocks.ts";
import { diffToChanges } from "./diffToChanges.ts";
import { getUserId } from "./id.ts";
import { isSimpleCodeFile } from "./isSimpleCodeFile.ts";
import { pull } from "./pull.ts";
import { SimpleCodeFile } from "./updateCodeFile.ts";
import {
applyCommit,
countBodyIndent,
extractFromCodeTitle,
} from "./_codeBlock.ts";

export interface UpdateCodeBlockOptions {
/** WebSocketの通信に使うsocket */
socket?: Socket;

/** `true`でデバッグ出力ON */
debug?: boolean;
}

/** コードブロックの中身を更新する
*
* newCodeにSimpleCodeFileオブジェクトを渡すと、そのオブジェクトに添ってコードブロックのファイル名も書き換えます
* (文字列や文字列配列を渡した場合は書き換えません)。
*
* @param newCode 更新後のコードブロック
* @param target 更新対象のコードブロック
* @param project 更新対象のコードブロックが存在するプロジェクト名
*/
export const updateCodeBlock = async (
newCode: string | string[] | SimpleCodeFile,
target: TinyCodeBlock,
options?: UpdateCodeBlockOptions,
) => {
/** optionsの既定値はこの中に入れる */
const defaultOptions: Required<UpdateCodeBlockOptions> = {
socket: options?.socket ?? await socketIO(),
debug: false,
};
const opt = options ? { ...defaultOptions, ...options } : defaultOptions;
const { projectName, pageTitle } = target.pageInfo;
const [
head,
userId,
] = await Promise.all([
pull(projectName, pageTitle),
getUserId(),
]);
const newCodeBody = getCodeBody(newCode);
const bodyIndent = countBodyIndent(target);
const oldCodeWithoutIndent: Line[] = target.bodyLines.map((e) => {
return { ...e, text: e.text.slice(bodyIndent) };
});

const diffGenerator = diffToChanges(oldCodeWithoutIndent, newCodeBody, {
userId,
});
const commits = [...fixCommits([...diffGenerator], target)];
if (isSimpleCodeFile(newCode)) {
const titleCommit = makeTitleChangeCommit(newCode, target);
if (titleCommit) commits.push(titleCommit);
}

if (opt.debug) {
console.log("%cvvv original code block vvv", "color: limegreen;");
console.log(target);
console.log("%cvvv new codes vvv", "color: limegreen;");
console.log(newCode);
console.log("%cvvv commits vvv", "color: limegreen;");
console.log(commits);
}

await applyCommit(commits, head, projectName, pageTitle, opt.socket, userId);
if (!options?.socket) opt.socket.disconnect();
};

/** コード本文のテキストを取得する */
const getCodeBody = (code: string | string[] | SimpleCodeFile): string[] => {
const content = isSimpleCodeFile(code) ? code.content : code;
if (Array.isArray(content)) return content;
return content.split("\n");
};

/** insertコミットの行IDとtextのインデントを修正する */
function* fixCommits(
commits: readonly (DeleteCommit | InsertCommit | UpdateCommit)[],
target: TinyCodeBlock,
): Generator<DeleteCommit | InsertCommit | UpdateCommit, void, unknown> {
const { nextLine } = target;
const indent = " ".repeat(countBodyIndent(target));
for (const commit of commits) {
if ("_delete" in commit) {
yield commit;
} else if (
"_update" in commit
) {
yield {
...commit,
lines: {
...commit.lines,
text: indent + commit.lines.text,
},
};
} else if (
commit._insert != "_end" ||
nextLine === null
) {
yield {
...commit,
lines: {
...commit.lines,
text: indent + commit.lines.text,
},
};
} else {
yield {
_insert: nextLine.id,
lines: {
...commit.lines,
text: indent + commit.lines.text,
},
};
}
}
}

/** コードタイトルが違う場合は書き換える */
const makeTitleChangeCommit = (
code: SimpleCodeFile,
target: Pick<TinyCodeBlock, "titleLine">,
): UpdateCommit | null => {
const lineId = target.titleLine.id;
const targetTitle = extractFromCodeTitle(target.titleLine.text);
if (
targetTitle &&
code.filename.trim() == targetTitle.filename &&
code.lang?.trim() == targetTitle.lang
) return null;
const ext = (() => {
const matched = code.filename.match(/.+\.(.*)$/);
if (matched === null) return code.filename;
else if (matched[1] === "") return "";
else return matched[1].trim();
})();
const title = code.filename +
(code.lang && code.lang != ext ? `(${code.lang})` : "");
return {
_update: lineId,
lines: {
text: " ".repeat(countBodyIndent(target) - 1) + "code:" + title,
},
};
};
Loading