Scrapbox記法をTeX記法に変換するScript
を作る
参考にするもの
変換処理がどこにあるのか、いまいちわかりにくい
変換対応
コードブロック
テーブル
箇条書き
2023-06-02
17:27:07
インデントを閉じきれていなかった
ループを抜けた後に、閉じきれていないインデントを閉じる
code:mod.ts
import type { Block, CodeBlock, Table, Line, Node as NodeType } from "../scrapbox-parser/mod.ts";
/** Scrapbox記法をMarkdown記法に変える */
export const convert = (blocks: Block[]): string => {
/** 変換後の文字列 */
let latex = "";
/** 現在解析している行のインデントの深さ
*
* この方法だと番号つき箇条書きをを区別できないが、我慢する
*/
let level = 0;
for (const block of blocks) {
// タイトルは今のところ無視しておく
if (block.type === "title") continue;
// インデントを下げる
if (block.indent > level) {
for (let i = level; i < block.indent; i++) {
code:mod.ts
latex += indent(${ i !== level ? "\\item" : ""}\\begin{itemize}, i * 2) + "\n";
}
}
// インデントを上げる
if (block.indent < level) {
for (let i = level; i > block.indent; i--) {
latex += indent("\\end{itemize}", (i - 1) * 2) + "\n";
}
}
level = block.indent;
latex += indent(${level > 0 ? "\\item " : ""}${fromBlock(block)}, level * 2) + "\n";
}
// 全てのインデントを閉じる
for (let i = level; i > 0; i--) {
latex += indent("\\end{itemize}", (i - 1) * 2) + "\n";
}
return latex;
};
/** Blockの変換 */
const fromBlock = (block: CodeBlock | Table | Line): string => {
switch (block.type) {
case "codeBlock":
return fromCodeBlock(block);
case "table":
return fromTable(block);
case "line":
return fromLine(block);
}
}
/** CodeBlock記法の変換
*
* indentは無視する
*/
const fromCodeBlock = (block: CodeBlock): string =>
${block.content}
\\end{lstlisting}`;
/** Table記法の変換
*
* indentは無視する
*/
const fromTable = (table: Table): string => {
const caption = \\caption{${escape(table.fileName)}}\\label{table:${escape(table.fileName)}};
// columnsの最大長を計算する
const maxCol = Math.max(...table.cells.map((row) => row.length));
// 行を変換する
const rows = table.cells
.map(
(row) => ` ${row.map(
(column) => column.map((node) => fromNode(node)).join("")
).join(" & ")}\\\\`
);
return `\\begin{table}htbp ${caption}
\\centering
\\begin{tabular}{${"c".repeat(maxCol)}}
${rows.length === 0 ?
// 空の場合
"" :
rows.length === 1 ?
// 一行のみの場合は\midruleを入れない
` \\toprule
${rows.join("\n")}
\\bottomrule` :
` \\toprule
\\midrule
${rows.slice(1).join("\n")}
\\bottomrule`
}
\\end{tabular}
\\end{table}`;
};
/** 行の変換 */
const fromLine = (line: Line): string =>
line.nodes.map((node) => fromNode(node)).join("");
変換対応
引用
Helpfeel記法、コマンドライン記法、インラインコード記法
画像
URLだけ示しておく
アイコン
italicで表記しておく
リンク
外部リンクのみ表示
それ以外はbracketを外しておく
code:mod.ts
/** Nodeを変換する */
const fromNode = (node: NodeType): string => {
switch (node.type) {
case "quote":
return `\\begin{quote}
${indent(node.nodes.map((node) => fromNode(node)).join(""), 2)}
\\end{quote}`;
case "image":
case "strongImage":
return `\\begin{figure}hbtp iamge:\\url{${node.src}}
\\end{figure}`;
case "icon":
case "strongIcon":
return \\textit{${escape(node.path)}};
case "strong":
return \\textbf{${node.nodes.map((node) => fromNode(node)).join("")}};
case "formula":
return $${node.formula}$;
case "decoration": {
let result = node.nodes.map((node) => fromNode(node)).join("");
if (node.decos.includes("/")) result = \\textit{${result}};
if (node.decos.some((deco) => /\*-/.test(deco0))) { result = \\textbf{${result}};
}
if (node.decos.includes("_")) result = \\uline{${result}};
if (node.decos.includes("-")) result = \\sout{${result}};
return result;
}
case "numberList":
return ${node.number}. ${node.nodes.map((node) => fromNode(node)).join("")};
case "helpfeel":
return \\lstinline!? ${escape(node.text)}!;
case "code":
return \\lstinline!${escape(node.text)}!;
case "commandLine":
return \\lstinline!${escape(node.symbol)} ${escape(node.text)}!;
case "link":
return node.pathType === "absolute" ?
node.content === "" ?
\\url{${node.href}} :
\\href{${node.href}}{${escape(node.content)}} :
escape(node.href);
case "googleMap":
return \\href{${node.url}}{${escape(node.place)}};
case "hashTag":
return escape(#${node.href});
case "blank":
case "plain":
return escape(node.text);
}
};
listingsの言語名に変換……するコードにするつもりだったが、面倒なのでやってない takker.iconの需要が出たら真面目に実装し直す
code:mod.ts
const extensionData = [
{
fileType: "javascript",
},
{
fileType: "typescript",
},
{
fileType: "C++",
},
{
fileType: "C",
},
{
fileType: "cs",
},
{
fileType: "markdown",
},
{
fileType: "html",
},
{
fileType: "json",
},
{
fileType: "xml",
},
{
fileType: "yaml",
},
{
fileType: "toml",
},
{
fileType: "ini",
},
{
fileType: "tex",
},
{
fileType: "svg",
},
];
/** ファイル名の拡張子から言語を取得する */
const getFileType = (filename: string): string => {
const filenameExtention = filename.replace(/^.*\.(\w+)$/, "$1");
return extensionData
.find((data) => data.extensions.includes(filenameExtention))?.fileType ??
"";
};
code:mod.ts
const escape =(text: string): string => text
.replaceAll("\\", "\\textbackslash ")
.replaceAll("#", "\\#")
.replaceAll("%", "\\%")
.replaceAll("$", "\\$")
.replaceAll("&", "\\&")
.replaceAll("~", "\\textasciitilde ")
.replaceAll("^", "\\textasciicircum ")
.replaceAll("_", "\\_")
.replaceAll("{", "\\{")
.replaceAll("}", "\\}")
.replaceAll("|", "\\textbar ")
.replaceAll("<", "\\textless ")
.replaceAll(">", "\\textgreater ");
各行の先頭に空白をいれるやつ
code:mod.ts
const indent = (text: string, indentNum: number): string =>
text.split("\n").map((line) =>${" ".repeat(indentNum)}${line}).join("\n");
テスト
code:test1.ts
import { parse } from "../scrapbox-parser/mod.ts";
import { convert } from "./mod.ts";
const res = await fetch("/api/pages/daiiz/Scrapboxでレポートを書きたい/text");
const latex = convert(parse(await res.text()));
console.log(latex);
code:test2.ts
import { parse } from "../scrapbox-parser/mod.ts";
import { convert } from "./mod.ts";
const res = await fetch("/api/pages/villagepump/記法サンプル/text");
const latex = convert(parse(await res.text()));
console.log(latex);