diff --git a/browser/dom/__snapshots__/extractCodeFiles.test.ts.snap b/browser/dom/__snapshots__/extractCodeFiles.test.ts.snap new file mode 100644 index 0000000..9bbfb04 --- /dev/null +++ b/browser/dom/__snapshots__/extractCodeFiles.test.ts.snap @@ -0,0 +1,95 @@ +export const snapshot = {}; + +snapshot[`extractCodeFiles 1`] = ` +Map { + "main.cpp" => { + blocks: [ + { + endId: "63b7b1261280f00000c9bc36", + indent: 2, + lines: [ + "#include ", + "", + ], + startId: "63b7b1261280f00000c9bc34", + updated: 1672982822, + }, + { + endId: "63b7b1261280f00000c9bc3c", + indent: 2, + lines: [ + "int main() {", + ' std::cout << "Hello, C++" << "from scrapbox.io" << std::endl;', + "}", + "", + ], + startId: "63b7b1261280f00000c9bc38", + updated: 1672982822, + }, + ], + filename: "main.cpp", + lang: "cpp", + }, + "py" => { + blocks: [ + { + endId: "63b7b1261280f00000c9bc2a", + indent: 0, + lines: [ + 'print("Hello World!")', + ], + startId: "63b7b1261280f00000c9bc29", + updated: 1672982822, + }, + ], + filename: "py", + lang: "py", + }, + "python" => { + blocks: [ + { + endId: "63b7b1261280f00000c9bc31", + indent: 1, + lines: [ + \`console.log("I'm JavaScript");\`, + ], + startId: "63b7b1261280f00000c9bc30", + updated: 1672982822, + }, + ], + filename: "python", + lang: "js", + }, + "インデント.md" => { + blocks: [ + { + endId: "63b7b1261280f00000c9bc2e", + indent: 1, + lines: [ + "- インデント", + " - インデント", + ], + startId: "63b7b1261280f00000c9bc2c", + updated: 1672982822, + }, + ], + filename: "インデント.md", + lang: "md", + }, + "コードブロック.py" => { + blocks: [ + { + endId: "63b7b1261280f00000c9bc27", + indent: 0, + lines: [ + 'print("Hello World!")', + ], + startId: "63b7b1261280f00000c9bc26", + updated: 1672982822, + }, + ], + filename: "コードブロック.py", + lang: "py", + }, +} +`; diff --git a/browser/dom/extractCodeFiles.test.ts b/browser/dom/extractCodeFiles.test.ts new file mode 100644 index 0000000..436a99b --- /dev/null +++ b/browser/dom/extractCodeFiles.test.ts @@ -0,0 +1,582 @@ +import { extractCodeFiles } from "./extractCodeFiles.ts"; +import { Line } from "../../deps/scrapbox.ts"; +import { assertSnapshot } from "../../deps/testing.ts"; + +const sample: Line[] = [ + { + "id": "63b7aeeb5defe7001ddae116", + "text": "コードブロック記法", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982645, + "updated": 1672982645, + "title": true, + "section": { + "number": 0, + "start": true, + "end": false, + }, + }, + { + "id": "63b7b0761280f00000c9bc21", + "text": "ここでは[コードブロック]を表現する[scrapbox記法]を示す", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982645, + "updated": 1672982671, + "section": { + "number": 0, + "start": false, + "end": false, + }, + "nodes": [ + "ここでは", + { + "type": "link", + "unit": { + "page": "コードブロック", + }, + "children": "コードブロック", + }, + "を表現する", + { + "type": "link", + "unit": { + "page": "scrapbox記法", + }, + "children": "scrapbox記法", + }, + "を示す", + ], + }, + { + "id": "63b7b0791280f00000c9bc22", + "text": "", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982648, + "updated": 1672982648, + "section": { + "number": 0, + "start": false, + "end": true, + }, + "nodes": "", + }, + { + "id": "63b7b12b1280f00000c9bc3d", + "text": "サンプル", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982826, + "updated": 1672982828, + "section": { + "number": 1, + "start": true, + "end": false, + }, + "nodes": "サンプル", + }, + { + "id": "63b7b12c1280f00000c9bc3e", + "text": " from [/villagepump/記法サンプル#61dd289e7838e30000dc9cb5]", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982828, + "updated": 1672982835, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "nodes": { + "type": "indent", + "unit": { + "whole": " from [/villagepump/記法サンプル#61dd289e7838e30000dc9cb5]", + "tag": " ", + "content": "from [/villagepump/記法サンプル#61dd289e7838e30000dc9cb5]", + }, + "children": [ + "from ", + { + "type": "link", + "unit": { + "project": "villagepump", + "page": "記法サンプル", + "line": "61dd289e7838e30000dc9cb5", + }, + "children": "/villagepump/記法サンプル#61dd289e7838e30000dc9cb5", + }, + ], + }, + }, + { + "id": "63b7b1261280f00000c9bc26", + "text": "code:コードブロック.py", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982821, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "py", + "filename": "コードブロック.py", + "indent": 1, + "start": true, + "end": false, + }, + }, + { + "id": "63b7b1261280f00000c9bc27", + "text": ' print("Hello World!")', + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "py", + "filename": "コードブロック.py", + "indent": 1, + "start": false, + "end": true, + }, + }, + { + "id": "63b7b1261280f00000c9bc28", + "text": "リンクなしコードブロック", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "nodes": "リンクなしコードブロック", + }, + { + "id": "63b7b1261280f00000c9bc29", + "text": "code:py", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "py", + "filename": "py", + "indent": 1, + "start": true, + "end": false, + }, + }, + { + "id": "63b7b1261280f00000c9bc2a", + "text": ' print("Hello World!")', + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "py", + "filename": "py", + "indent": 1, + "start": false, + "end": true, + }, + }, + { + "id": "63b7b1261280f00000c9bc2b", + "text": "インデントつき", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "nodes": "インデントつき", + }, + { + "id": "63b7b1261280f00000c9bc2c", + "text": " code:インデント.md", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "md", + "filename": "インデント.md", + "indent": 2, + "start": true, + "end": false, + }, + }, + { + "id": "63b7b1261280f00000c9bc2d", + "text": " - インデント", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "md", + "filename": "インデント.md", + "indent": 2, + "start": false, + "end": false, + }, + }, + { + "id": "63b7b1261280f00000c9bc2e", + "text": " - インデント", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "md", + "filename": "インデント.md", + "indent": 2, + "start": false, + "end": true, + }, + }, + { + "id": "63b7b1261280f00000c9bc2f", + "text": "言語を強制", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "nodes": "言語を強制", + }, + { + "id": "63b7b1261280f00000c9bc30", + "text": " code:python(js)", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "js", + "filename": "python", + "indent": 2, + "start": true, + "end": false, + }, + }, + { + "id": "63b7b1261280f00000c9bc31", + "text": ' console.log("I\'m JavaScript");', + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "js", + "filename": "python", + "indent": 2, + "start": false, + "end": true, + }, + }, + { + "id": "63b7b1261280f00000c9bc32", + "text": "文芸的プログラミング", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982825, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "nodes": "文芸的プログラミング", + }, + { + "id": "63b7b1261280f00000c9bc33", + "text": " 標準ヘッダファイルをインクルード", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "nodes": { + "type": "indent", + "unit": { + "whole": " 標準ヘッダファイルをインクルード", + "tag": " ", + "content": "標準ヘッダファイルをインクルード", + }, + "children": "標準ヘッダファイルをインクルード", + }, + }, + { + "id": "63b7b1261280f00000c9bc34", + "text": " code:main.cpp", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "cpp", + "filename": "main.cpp", + "indent": 3, + "start": true, + "end": false, + }, + }, + { + "id": "63b7b1261280f00000c9bc35", + "text": " #include ", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "cpp", + "filename": "main.cpp", + "indent": 3, + "start": false, + "end": false, + }, + }, + { + "id": "63b7b1261280f00000c9bc36", + "text": " ", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "cpp", + "filename": "main.cpp", + "indent": 3, + "start": false, + "end": true, + }, + }, + { + "id": "63b7b1261280f00000c9bc37", + "text": " main函数の定義", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "nodes": { + "type": "indent", + "unit": { + "whole": " main函数の定義", + "tag": " ", + "content": "main函数の定義", + }, + "children": "main函数の定義", + }, + }, + { + "id": "63b7b1261280f00000c9bc38", + "text": " code:main.cpp", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "cpp", + "filename": "main.cpp", + "indent": 3, + "start": true, + "end": false, + }, + }, + { + "id": "63b7b1261280f00000c9bc39", + "text": " int main() {", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "cpp", + "filename": "main.cpp", + "indent": 3, + "start": false, + "end": false, + }, + }, + { + "id": "63b7b1261280f00000c9bc3a", + "text": + ' std::cout << "Hello, C++" << "from scrapbox.io" << std::endl;', + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "cpp", + "filename": "main.cpp", + "indent": 3, + "start": false, + "end": false, + }, + }, + { + "id": "63b7b1261280f00000c9bc3b", + "text": " }", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "cpp", + "filename": "main.cpp", + "indent": 3, + "start": false, + "end": false, + }, + }, + { + "id": "63b7b1261280f00000c9bc3c", + "text": " ", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982822, + "updated": 1672982822, + "section": { + "number": 1, + "start": false, + "end": false, + }, + "codeBlock": { + "lang": "cpp", + "filename": "main.cpp", + "indent": 3, + "start": false, + "end": true, + }, + }, + { + "id": "63b7b0911280f00000c9bc23", + "text": "", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982673, + "updated": 1672982673, + "section": { + "number": 1, + "start": false, + "end": true, + }, + "nodes": "", + }, + { + "id": "63b7b0911280f00000c9bc24", + "text": "#2023-01-06 14:24:35 ", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982673, + "updated": 1672982674, + "section": { + "number": 2, + "start": true, + "end": false, + }, + "nodes": [ + { + "type": "hashTag", + "unit": { + "page": "2023-01-06", + "tag": "#", + }, + "children": "#2023-01-06", + }, + " 14:24:35 ", + ], + }, + { + "id": "63b7b0931280f00000c9bc25", + "text": "", + "userId": "5ef2bdebb60650001e1280f0", + "created": 1672982674, + "updated": 1672982674, + "section": { + "number": 2, + "start": false, + "end": true, + }, + "nodes": "", + }, +]; + +Deno.test("extractCodeFiles", async (t) => { + await assertSnapshot( + t, + extractCodeFiles(sample), + ); +}); diff --git a/browser/dom/extractCodeFiles.ts b/browser/dom/extractCodeFiles.ts new file mode 100644 index 0000000..adf2330 --- /dev/null +++ b/browser/dom/extractCodeFiles.ts @@ -0,0 +1,72 @@ +import { Line } from "../../deps/scrapbox.ts"; + +/** 一つのソースコードを表す */ +export interface CodeFile { + /** file name */ + filename: string; + + /** language */ + lang: string; + + /** splitted code */ + blocks: CodeBlock[]; +} + +/** 一つのコードブロックを表す */ +export interface CodeBlock { + /** 開始行のID */ + startId: string; + + /** 末尾の行のID */ + endId: string; + + /** コードブロックの最終更新日時 */ + updated: number; + + /** .code-titleのindent数 */ + indent: number; + + /** ブロック中のコード + * + * .code-titleは含まない + * + * 予めindentは削ってある + */ + lines: string[]; +} + +/** `scrapbox.Page.lines`からcode blocksを取り出す + * + * @param lines ページの行 + * @return filenameをkeyにしたソースコードのMap + */ +export const extractCodeFiles = ( + lines: Iterable, +): Map => { + const files = new Map(); + + for (const line of lines) { + if (!("codeBlock" in line)) continue; + const { filename, lang, ...rest } = line.codeBlock; + const file = files.get(filename) ?? { filename, lang, blocks: [] }; + if (rest.start || file.blocks.length === 0) { + file.blocks.push({ + startId: line.id, + endId: line.id, + updated: line.updated, + // 本文ではなく、.code-titleのインデント数を登録する + indent: rest.indent - 1, + lines: [], + }); + } else { + const block = file.blocks[file.blocks.length - 1]; + block.endId = line.id; + block.updated = Math.max(block.updated, line.updated); + block.lines.push([...line.text].slice(block.indent + 1).join("")); + } + + files.set(filename, file); + } + + return files; +}; diff --git a/browser/dom/mod.ts b/browser/dom/mod.ts index 9455b1a..5fda510 100644 --- a/browser/dom/mod.ts +++ b/browser/dom/mod.ts @@ -12,3 +12,4 @@ export * from "./cursor.ts"; export * from "./selection.ts"; export * from "./stores.ts"; export * from "./pushPageTransition.ts"; +export * from "./extractCodeFiles.ts"; diff --git a/deps/scrapbox-rest.ts b/deps/scrapbox-rest.ts index 5fde552..6e9ee49 100644 --- a/deps/scrapbox-rest.ts +++ b/deps/scrapbox-rest.ts @@ -25,4 +25,4 @@ export type { SessionError, Snapshot, TweetInfo, -} from "https://raw.githubusercontent.com/scrapbox-jp/types/0.3.6/rest.ts"; +} from "https://raw.githubusercontent.com/scrapbox-jp/types/0.3.8/rest.ts"; diff --git a/deps/scrapbox.ts b/deps/scrapbox.ts index fdb02a8..9185839 100644 --- a/deps/scrapbox.ts +++ b/deps/scrapbox.ts @@ -1,8 +1,8 @@ export type { Line, Scrapbox, -} from "https://raw.githubusercontent.com/scrapbox-jp/types/0.3.6/userscript.ts"; +} from "https://raw.githubusercontent.com/scrapbox-jp/types/0.3.8/userscript.ts"; export type { BaseStore, -} from "https://raw.githubusercontent.com/scrapbox-jp/types/0.3.6/baseStore.ts"; +} from "https://raw.githubusercontent.com/scrapbox-jp/types/0.3.8/baseStore.ts"; export * from "https://esm.sh/@progfay/scrapbox-parser@8.1.0";