選択範囲をMarkdown記法に変換してclip boardにcopyするPopupMenu
2020-10-16 08:57:37 できた
アイコン記法は無視している
動機
に書いた文章をレポートに書き写すことがよくある
それを簡単にできるようにしたい
Wordの共同編集は重いので、3 ~ 4人で共同編集しようとすると固まったりエラーが出たり
場合によってはデータが飛ぶことがある
代わりに
real time編集に優れた
で文章を書き、それをwordなりmarkdownなりに変換&提出すればレポート作成がとても楽になる
実装
見出し
トップレベルのみ
見出し以外は無視
アイコン記法は全て無視する
コードブロックの言語名
言語名と拡張子との対応リストを別に作っておいたほうがいいな
switch
文中に書いたりすると、読みにくくなる
こんな感じ
data.json[
{
"extensions":["javascript","js"],
"filetype": "javascript"
},
]
2020-11-05 10:00:11 少しだけ実装した
やりたい
相対的にindentを変える
例えば選択範囲の開始がインデントを1つ含んでいた場合は、それ以降1 indentをtop indentとして扱う
より少ないインデントの行を選択範囲中に含んでいた場合は、そちらを基準にする
それをこっちにscriptに移植すればいいだろう。
2020-11-12 01:45:28 codeだけ書いた
テストはしていない
refactoring
TypeScriptにする
arrow functionにする
テストを書く
似たようなUserScriptは既にある
よさそう
は選択範囲に適用できるようにしたい
2023-02-06
21:47:07 iconをfont awesomeにした
2022-09-25
直した
2022-05-20
試した
外部リンク記法は問題なく変換できる
https://xxx.com
を変換すると [](https://xxx.com)
にある
あーそういうことか
https://xxx.com
のときは生のURLを直接貼ることにしよう
修正done
2022-05-15
その他、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.jsimport { 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に変換するコードを書く必要がある
斜体
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;
}
};
あと\TeX用のも作りたい
コードブロックの言語識別用data
convert.jsconst 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 ??
"";
};
前にも同じこと書いていたみたい
実装
1行ごとに変換
正規表現で構文解析する
indentの調節の仕方でoptionを作る
1. indentを維持
2. 深さに合わせてindentを相対的に減らす