├── 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 `![image](${node.src})`; 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 `${node.path}.icon`; 118 | } else if (node.pathType === "root") { 119 | return `${node.path}.icon`; 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 | --------------------------------------------------------------------------------