generated at
選択範囲をMarkdown記法に変換してclip boardにcopyするPopupMenu
選択範囲markdown記法に変換してclipboardにcopyするPopupMenuを作ってみた
2020-10-16 08:57:37 できた
アイコン記法は無視している

動機
Scrapboxに書いた文章をレポートに書き写すことがよくある
それを簡単にできるようにしたい
あとグループでの共同編集をやりやすくする
Wordの共同編集は重いので、3 ~ 4人で共同編集しようとすると固まったりエラーが出たり場合によってはデータが飛ぶことがある
代わりにreal time編集に優れたScrapboxで文章を書き、それをwordなりmarkdownなりに変換&提出すればレポート作成がとても楽になる

実装
UserScriptで使えるscrapbox-parserを使って構文解析する
userは事前に自分のページで↑を読み込んでもらう
見出し
トップレベルのみ
見出し以外は無視
アイコン記法は全て無視する
doingコードブロックの言語名
言語名と拡張子との対応リストを別に作っておいたほうがいいな
switch 文中に書いたりすると、読みにくくなる
こんな感じ
data.json
[ { "extensions":["javascript","js"], "filetype": "javascript" }, ]
2020-11-05 10:00:11 少しだけ実装した

やりたい
doing相対的にindentを変える
例えば選択範囲の開始がインデントを1つ含んでいた場合は、それ以降1 indentをtop indentとして扱う
より少ないインデントの行を選択範囲中に含んでいた場合は、そちらを基準にする
それをこっちにscriptに移植すればいいだろう。
2020-11-12 01:45:28 codeだけ書いた
テストはしていない
refactoring
TypeScriptにする
arrow functionにする
テストを書く

hr
似たようなUserScriptは既にある
line.section.startを見出し変換につかっている
よさそうtakker
bookmarkletにしてもいいかも?
takkerは選択範囲に適用できるようにしたい

2023-02-06
21:47:07 iconをfont awesomeにした
2022-09-25
>linkの時になぜかhttps://scrapbox.io が入ってしまう
直した
2022-05-20
知らんかった
試した
外部リンク記法は問題なく変換できる
https://xxx.com を変換すると [](https://xxx.com) にある
あーそういうことかtakker
https://xxx.com のときは生のURLを直接貼ることにしよう
修正done
2022-05-15
17:50:02 変換部分だけJSDocを付けた
その他、deno fmtで整形した
コードが雑な点には手を入れていない
そのうち直したいっちゃ直したい

$ curl https://scrapbox.io/api/code/takker/選択範囲をMarkdown記法に変換してclip_boardにcopyするPopupMenu/deno.json > deno.json
$ deno check --remote -r=https://scrapbox.io https://scrapbox.io/api/code/takker/選択範囲をMarkdown記法に変換してclip_boardにcopyするPopupMenu/script.js
script.js
import { convertSb2Md } from "./convert.js"; import { ScrapboxParser } from "../scrapbox-parser.min.js/parser.js"; // markdown変換 scrapbox.PopupMenu.addButton({ title: "\uf60f", onClick: (text) => { (async () => { try { const blocks = ScrapboxParser.parse(text, { hasTitle: false }); //console.log('Parserd text:'); //console.log(blocks); // このindent levelを基準にする const topIndentLevel = Math.min(...blocks.map((block) => block.indent)); await navigator.clipboard.writeText( blocks.map((block) => convertSb2Md(block, topIndentLevel)).join("\n"), ); //console.log('Copied.'); } catch (e) { alert(`Failed to copy:\n${JSON.stringify(e)}`); } })(); }, });

あとは blocks をmarkdownに変換するコードを書く必要がある
まず、markdown記法をまとめる必要があるなtakker
斜体
convert.js
// @ts-check /** * @typedef {import("../scrapbox-parser/mod.ts").Block} Block * @typedef {import("../scrapbox-parser/mod.ts").Table} Table * @typedef {import("../scrapbox-parser/mod.ts").Line} Line * @typedef {import("../scrapbox-parser/mod.ts").Node} NodeType * @typedef {import("../scrapbox-jp%2Ftypes/userscript.ts").Scrapbox} Scrapbox */ /** Scrapbox記法をMarkdown記法に変える * * @param {Block} block * @param {number} topIndentLevel * @return {string} */ export const convertSb2Md = (block, topIndentLevel) => { switch (block.type) { case "title": return ""; // タイトルは選択範囲に入らないので無視 case "codeBlock": return [ block.fileName, `\n\`\`\`${getFileType(block.fileName)}`, block.content, "\`\`\`\n", ].join("\n"); case "table": return convertTable(block); case "line": return convertLine(block, topIndentLevel); } }; /** Table記法の変換 * * @param {Table} table * @return {string} */ const convertTable = (table) => { const line = [table.fileName]; // columnsの最大長を計算する const maxCol = Math.max(...table.cells.map((row) => row.length)); table.cells.forEach((row, i) => { line.push( `| ${ row.map((column) => column.map((node) => convertNode(node)).join("")) .join(" | ") } |`, ); if (i === 0) line.push(`|${" -- |".repeat(maxCol)}`); }); return line.join("\n"); }; const INDENT = " "; // インデントに使う文字 /** 行の変換 * * @param {Line} line * @param {number} topIndentLevel * @return {string} */ const convertLine = (line, topIndentLevel) => { const content = line.nodes .map((node) => convertNode(node, { section: line.indent === topIndentLevel }) ).join("").trim(); if (content === "") return ""; // 空行はそのまま返す // リストを作る if (line.indent === topIndentLevel) return content; // トップレベルの行はインデントにしない let result = INDENT.repeat(line.indent - topIndentLevel - 1); if (!/^\d+\. /.test(content)) result += "- "; // 番号なしの行は`-`を入れる return result + content; }; /** Nodeを変換する * * @param {NodeType} node * @param {{section?:boolean}} [init] * @return {string} */ const convertNode = (node, init) => { const { section = false } = init ?? {}; switch (node.type) { case "quote": return `> ${node.nodes.map((node) => convertNode(node)).join("")}`; case "helpfeel": return `\`? ${node.text}\``; case "image": case "strongImage": return `![image](${node.src})`; case "icon": case "strongIcon": // 仕切り線だけ変換する return ["/icons/hr", "/scrapboxlab/hr"] .includes(node.path) ? "---" : ""; case "strong": return `**${node.nodes.map((node) => convertNode(node)).join("")}**`; case "formula": return `$${node.formula}$`; case "decoration": { let result = node.nodes.map((node) => convertNode(node)).join(""); if (node.decos.includes("/")) result = `*${result}*`; // 見出しの変換 // お好みで変えて下さい if (section) { if (node.decos.includes("*-3")) result = `# ${result}\n`; if (node.decos.includes("*-2")) result = `## ${result}\n`; if (node.decos.includes("*-1")) result = `### ${result}\n`; } else { if (node.decos.some((deco) => /\*-/.test(deco[0]))) { result = `**${result}**`; } } if (node.decos.includes("~")) result = `~~${result}~~`; return result; } case "code": return `\`${node.text}\``; case "commandLine": return `\`${node.symbol} ${node.text}\``; case "link": switch (node.pathType) { case "root": return `[${node.href}](https://scrapbox.io${node.href})`; case "relative": //@ts-ignore declare宣言が使えないため、`scrapbox`に型定義をつけられない return `[${node.href}](https://scrapbox.io/${scrapbox.Project.name}/${node.href})`; default: return node.content === "" ? ` ${node.href} ` : `[${node.content}](${node.href})`; } case "googleMap": return `[${node.place}](${node.url})`; case "hashTag": //@ts-ignore declare宣言が使えないため、`scrapbox`に型定義をつけられない return `[#${node.href}](https://scrapbox.io/${scrapbox.Project.name}/${node.href})`; case "numberList": return `${node.number}. ${node.nodes.map((node) => convertNode(node)).join("")}`; case "blank": case "plain": return node.text; } };

HTMLに変換してコピーするUserScriptを作ってもいいかもしれない
あと\TeX用のも作りたい

コードブロックの言語識別用data
convert.js
const extensionData = [ { extensions: ["javascript", "js"], fileType: "javascript", }, { extensions: ["typescript", "ts"], fileType: "typescript", }, { extensions: ["cpp", "hpp"], fileType: "cpp", }, { extensions: ["c", "cc", "h"], fileType: "c", }, { extensions: ["cs", "csharp"], fileType: "cs", }, { extensions: ["markdown", "md"], fileType: "markdown", }, { extensions: ["htm", "html"], fileType: "html", }, { extensions: ["json"], fileType: "json", }, { extensions: ["xml"], fileType: "xml", }, { extensions: ["yaml", "yml"], fileType: "yaml", }, { extensions: ["toml"], fileType: "toml", }, { extensions: ["ini"], fileType: "ini", }, { extensions: ["tex", "sty"], fileType: "tex", }, { extensions: ["svg"], fileType: "svg", }, ];

コードブロックのファイル名から、programming言語を識別して返す
convert.js
/** ファイル名の拡張子から言語を取得する * * @param {string} filename * @return {string} */ const getFileType = (filename) => { const filenameExtention = filename.replace(/^.*\.(\w+)$/, "$1"); return extensionData .find((data) => data.extensions.includes(filenameExtention))?.fileType ?? ""; };

hr
前にも同じこと書いていたみたい
選択範囲markdown記法に変換してclip boardにcopyするPopupMenuを作りたい

実装
1行ごとに変換
正規表現で構文解析する
markdown記法にないものは無視する
indentの調節の仕方でoptionを作る
1. indentを維持
2. 深さに合わせてindentを相対的に減らす

#2024-11-13 16:54:43 型定義を通す
#2024-05-27 15:41:03 numberList に対応
#2023-02-06 21:47:21
#2022-09-25 15:57:54
#2022-05-20 17:50:43
#2022-05-15 17:50:48
#2022-05-07 23:06:02
#2020-11-12 01:45:43
#2020-11-05 09:52:44
#2020-10-22 00:55:51
#2020-10-21 12:03:07
#2020-10-16 08:58:33
#2020-10-15 13:59:49
#2020-10-14 23:23:32
#2020-10-11 21:39:31
#2020-10-05 20:59:02