├── README.md
├── convert.js
└── mod.ts
/README.md:
--------------------------------------------------------------------------------
1 | # ScrapboxToObsidian
2 | Convert scrapbox json file to a folder of markdown files for Obsidian.
3 | ## Usage
4 | `deno run --allow-run --allow-read --allow-write mod.ts SCRAPBOX_EXPORTED_FILE.json PROJECT_NAME`
5 | - The project `PROJECT_NAME` will be used as the source of icons.
--------------------------------------------------------------------------------
/convert.js:
--------------------------------------------------------------------------------
1 | // Based on https://scrapbox.io/takker/選択範囲をMarkdown記法に変換してclip_boardにcopyするPopupMenu
2 | // Modified by @blu3mo to convert to Obsidian syntax
3 |
4 | // @ts-check
5 |
6 | /**
7 | * @typedef {import("https://esm.sh/@progfay/scrapbox-parser@8.1.0").Block} Block
8 | * @typedef {import("https://esm.sh/@progfay/scrapbox-parser@8.1.0").Table} Table
9 | * @typedef {import("https://esm.sh/@progfay/scrapbox-parser@8.1.0").Line} Line
10 | * @typedef {import("https://esm.sh/@progfay/scrapbox-parser@8.1.0").Node} NodeType
11 | * @typedef {import("https://raw.githubusercontent.com/scrapbox-jp/types/0.3.4/scrapbox.ts").Scrapbox} Scrapbox
12 | */
13 |
14 | /** Scrapbox記法をMarkdown記法に変える
15 | *
16 | * @param {Block} block
17 | * @param {number} topIndentLevel
18 | * @param {string} projectName
19 | * @return {string}
20 | */
21 | export const convertScrapboxToObsidian = (
22 | block,
23 | topIndentLevel,
24 | projectName,
25 | ) => {
26 | switch (block.type) {
27 | case "title":
28 | return ""; // タイトルは選択範囲に入らないので無視
29 | case "codeBlock":
30 | return [
31 | block.fileName,
32 | `\n\`\`\`${getFileType(block.fileName)}`,
33 | block.content,
34 | "\`\`\`\n",
35 | ].join("\n");
36 | case "table":
37 | return convertTable(block, projectName);
38 | case "line":
39 | return convertLine(block, topIndentLevel, projectName);
40 | }
41 | };
42 |
43 | /** Table記法の変換
44 | *
45 | * @param {Table} table
46 | * @param {string} projectName
47 | * @return {string}
48 | */
49 | const convertTable = (table, projectName) => {
50 | const line = [table.fileName];
51 | // columnsの最大長を計算する
52 | const maxCol = Math.max(...table.cells.map((row) => row.length));
53 | table.cells.forEach((row, i) => {
54 | line.push(
55 | `| ${
56 | row.map((column) =>
57 | column.map((node) => convertNode(node, projectName)).join("")
58 | )
59 | .join(" | ")
60 | } |`,
61 | );
62 | if (i === 0) line.push(`|${" -- |".repeat(maxCol)}`);
63 | });
64 | return line.join("\n");
65 | };
66 |
67 | const INDENT = " "; // インデントに使う文字
68 |
69 | /** 行の変換
70 | *
71 | * @param {Line} line
72 | * @param {number} topIndentLevel
73 | * @param {string} projectName
74 | * @return {string}
75 | */
76 | const convertLine = (line, topIndentLevel, projectName) => {
77 | const content = line.nodes
78 | .map((node) =>
79 | convertNode(node, projectName, {
80 | section: line.indent === topIndentLevel,
81 | })
82 | ).join("").trim();
83 | if (content === "") return ""; // 空行はそのまま返す
84 |
85 | // リストを作る
86 | if (line.indent === topIndentLevel) return content; // トップレベルの行はインデントにしない
87 | let result = INDENT.repeat(line.indent - topIndentLevel - 1);
88 | if (!/^\d+\. /.test(content)) result += "- "; // 番号なしの行は`-`を入れる
89 | return result + content;
90 | };
91 |
92 | /** Nodeを変換する
93 | *
94 | * @param {NodeType} node
95 | * @param {string} projectName
96 | * @param {{section?:boolean}} [init]
97 | * @return {string}
98 | */
99 | const convertNode = (node, projectName, init) => {
100 | const { section = false } = init ?? {};
101 | switch (node.type) {
102 | case "quote":
103 | return `> ${
104 | node.nodes.map((node) => convertNode(node, projectName)).join("")
105 | }`;
106 | case "helpfeel":
107 | return `\`? ${node.text}\``;
108 | case "image":
109 | case "strongImage":
110 | return ``;
111 | case "icon":
112 | case "strongIcon":
113 | // 仕切り線だけ変換する
114 | if (["/icons/hr", "/scrapboxlab/hr", "hr", "-"].includes(node.path)) {
115 | return "---";
116 | } else if (node.pathType === "relative") {
117 | return `
`;
118 | } else if (node.pathType === "root") {
119 | return `
`;
120 | } else {
121 | return "";
122 | }
123 | case "strong":
124 | return `**${
125 | node.nodes.map((node) => convertNode(node, projectName)).join("")
126 | }**`;
127 | case "formula":
128 | return `$${node.formula}$`;
129 | case "decoration": {
130 | let result = node.nodes.map((node) => convertNode(node, projectName))
131 | .join("");
132 | if (node.decos.includes("/")) result = `*${result}*`;
133 | if (node.decos.includes("~")) result = `~~${result}~~`;
134 | if (node.decos.includes("+")) result = `==${result}==`;
135 | // 見出しの変換
136 | // お好みで変えて下さい
137 | if (section) {
138 | if (node.decos.includes("*-3")) result = `# ${result}\n`;
139 | if (node.decos.includes("*-2")) result = `## ${result}\n`;
140 | if (node.decos.includes("*-1")) result = `### ${result}\n`;
141 | } else {
142 | if (node.decos.some((deco) => /\*-/.test(deco[0]))) {
143 | result = `**${result}**`;
144 | }
145 | }
146 | return result;
147 | }
148 | case "code":
149 | return `\`${node.text}\``;
150 | case "commandLine":
151 | return `\`${node.symbol} ${node.text}\``;
152 | case "link":
153 | switch (node.pathType) {
154 | case "root":
155 | return `[${node.href}](https://scrapbox.io${node.href})`;
156 | case "relative":
157 | return `[[${node.href}]]`;
158 | default:
159 | return node.content === ""
160 | ? `[${node.href}](${node.href})`
161 | : `[${node.content}](${node.href})`;
162 | }
163 | case "googleMap":
164 | return `[${node.place}](${node.url})`;
165 | case "hashTag":
166 | return `#${node.href}`;
167 | case "blank":
168 | case "plain":
169 | return node.text;
170 | case "numberList":
171 | return `${node.number}. ${
172 | node.nodes.map((node) => convertNode(node, projectName)).join("")
173 | }`;
174 | }
175 | };
176 |
177 | const extensionData = [
178 | {
179 | extensions: ["javascript", "js"],
180 | fileType: "javascript",
181 | },
182 | {
183 | extensions: ["typescript", "ts"],
184 | fileType: "typescript",
185 | },
186 | {
187 | extensions: ["cpp", "hpp"],
188 | fileType: "cpp",
189 | },
190 | {
191 | extensions: ["c", "cc", "h"],
192 | fileType: "c",
193 | },
194 | {
195 | extensions: ["cs", "csharp"],
196 | fileType: "cs",
197 | },
198 | {
199 | extensions: ["markdown", "md"],
200 | fileType: "markdown",
201 | },
202 | {
203 | extensions: ["htm", "html"],
204 | fileType: "html",
205 | },
206 | {
207 | extensions: ["json"],
208 | fileType: "json",
209 | },
210 | {
211 | extensions: ["xml"],
212 | fileType: "xml",
213 | },
214 | {
215 | extensions: ["yaml", "yml"],
216 | fileType: "yaml",
217 | },
218 | {
219 | extensions: ["toml"],
220 | fileType: "toml",
221 | },
222 | {
223 | extensions: ["ini"],
224 | fileType: "ini",
225 | },
226 | {
227 | extensions: ["tex", "sty"],
228 | fileType: "tex",
229 | },
230 | {
231 | extensions: ["svg"],
232 | fileType: "svg",
233 | },
234 | ];
235 |
236 | /** ファイル名の拡張子から言語を取得する
237 | *
238 | * @param {string} filename
239 | * @return {string}
240 | */
241 | const getFileType = (filename) => {
242 | const filenameExtention = filename.replace(/^.*\.(\w+)$/, "$1");
243 | return extensionData
244 | .find((data) => data.extensions.includes(filenameExtention))?.fileType ??
245 | "";
246 | };
247 |
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | import { convertScrapboxToObsidian } from "./convert.js";
2 | import { parse } from "https://esm.sh/@progfay/scrapbox-parser@8.1.0";
3 | import { ensureDir } from "https://deno.land/std@0.170.0/fs/mod.ts";
4 |
5 | await ensureDir("./obsidianPages");
6 |
7 | const filePath = Deno.args[0];
8 | const projectName = Deno.args[1] ?? "PROJECT_NAME";
9 | try {
10 | const projectFile = await Deno.readTextFile(`./${filePath}`);
11 | const projectJson = JSON.parse(projectFile);
12 | const pages = projectJson["pages"];
13 | for (const page of pages) {
14 | const blocks = parse(page["lines"].join("\n"));
15 | const obsidianPage = blocks.map((block) =>
16 | convertScrapboxToObsidian(block, 0, projectName)
17 | ).join("\n");
18 | const obsidianPagePath = `./obsidianPages/${
19 | page["title"].replace(/\//gi, "-")
20 | }.md`;
21 | await Deno.writeTextFile(obsidianPagePath, obsidianPage);
22 | await Deno.utime(obsidianPagePath, new Date(), page["updated"]);
23 | }
24 | } catch (error) {
25 | if (error instanceof Deno.errors.NotFound) {
26 | console.error("the file was not found");
27 | } else {
28 | throw error;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------