Scrapbox書籍のformat@0.2.0
特徴
タイトルに見出し番号と見出しに含まれる最初のページ番号を入れる
目次には更に書名を入れておく
table:index
見出し start end 子の数
1 0 2 3
1.1 3 3
1.2 x x 2
1.2.1 4 6 0
1.2.2 6 12 0
column1 13 14 0
2 2
2.1 15 16
2.2 17 20 1
column2 19 19 0
3 20 24 0
xもしくは空欄はページなし
子の数で、含まれる節の数を示す
省略した場合は0とみなす
この例の場合、以下のような階層構造になる
1
1.1
1.2
1.2.1
1.2.2
column1
2
2.1
2.2
column2
3
以下、目次のパーサーparse()とページテキスト変換処理stringify()の実装
code:mod.ts
export interface Page {
/** ページタイトル */
title: string;
/** このページに含める画像のインデックス */
indice: number[];
/** 前ページのタイトル
*
* なければ空文字
*/
prev: string;
/** 次ページのタイトル
*
* なければ空文字
*/
next: string;
/** 子ページのタイトル */
children: string[];
}
Pageからページテキストに変換する
別のフォーマットを使いたい場合は、このstringify()を別函数に差し替えればいい
ページレイアウト
code:1.2 title
見出しの本文
複数ページを含む場合
code:txt
1ページ目の本文
1ページ目の画像
2ページ目の本文
2ページ目の画像
ナビゲーション部
最初の見出しの場合
<= | [2. next] =>
最後の見出しの場合
<= [12. prev] | =>
章の最初の見出しの場合
<= [1 parent] | [1.2 next] =>
章の最後の見出しの場合
<= [1.3 prev] | [2 next] =>
次の見出しがなかった場合
<= [1.3 prev] | =>
code:mod.ts
/** ページ情報と画像URLとテキストから、scrapboxのページを作る
*
* @param page ページ情報
* @param imageURLs 書籍に使うすべての画像のURLリスト
* @param text imageURLsに対応するテキストデータ
* @param offset=0 書籍formatで指定したページ番号が0以外で始まる場合、この数値を変えて調節する */
export const stringify = (page: Page, imageURLs: string[], text: string[], offset = 0): string => [
page.title,
...page.indice.flatMap((i) => [
"",
[${imageURLs[i + offset]}],
"",
]),
...page.children.map((title) => [${title}]),
"",
"",
].join("\n");
2022-08-04
17:29:13 階層が切り替わったときのprevの値がおかしかった
実装
返却時にページの順番がめちゃくちゃになるのが欠点だな
前後情報を埋め込んであるから、めちゃくちゃになっても問題はない
並び替え直してから返せばいい?
並び替える函数を別途用意しよう
code:mod.ts
/** 生成したページデータを並び替える */
export function* sort(pages: IterableIterator<Page>): Generator<Page, void, unknown> {
const stack: Page[] = [];
// iteratorを少しずつ消費する
for (const page of read(pages)) {
stack.push(page);
if (page.prev === "") break;
}
if (stack.length === 0) return;
let now = stack.pop()!;
if (now.prev !== "") throw Error(Could not find the first page.);
yield now;
const find = (title: string): Page | undefined => {
const index = stack.findIndex((page) => page.title === title);
if (index >= 0) {
const target = stack.splice(index, 1); return target;
}
for (const page of read(pages)) {
if (page.title === title) return page;
stack.push(page);
}
};
function* sortChildren(parent: Page): Generator<Page, void, unknown> {
yield parent;
for (const title of parent.children) {
const child = find(title);
if (!child) throw Error(Could not find "${title}".);
yield* sortChildren(child);
}
}
while (now.next !== "") {
const next = find(now.next);
if (!next) throw Error(Could not find "${now.next}".);
yield* sortChildren(next);
now = next;
}
// まずあり得ないが、返し残しがあれば返却する
for (const page of stack) {
yield page;
}
for (const page of pages) {
yield page;
}
}
なんのために実装したんだろこの函数……
そんなことある?
code:mod.ts
/** iteratorを少しずつ消化する函数 */
function* read<T>(iterator: IterableIterator<T>): Generator<T, void, unknown> {
while (true) {
const { done, value } = iterator.next();
if (done) return;
yield value;
}
}
目次データを順にPageに変換する
ページ順に並び替えられていないので注意
並び替えたいときは、間にsort()を挟んで並び替える
code:mod.ts
export function* parse(rows: Iterable<string[]>): Generator<Page, void, unknown> {
// for loopの都合上、parentsには先頭から要素を追加する
// 階層の深い順にparentが並ぶ
const parents: { page: Page, childNum: number; }[] = [];
for (const row of rows) {
const s = /^\d+$/.test(start) ? Math.abs(parseInt(start)) : -1;
const e = /^\d+$/.test(end) ? Math.abs(parseInt(end)) : -1;
const childNum = /^\d+$/.test(childNumText) ? Math.abs(parseInt(childNumText)) : 0;
const page: Page = { title, indice, prev: "", next: "", children: [] };
// 親見出しから処理する
let i = 0;
for (; i < parents.length; i++) {
// pageを親見出しの子とする場合は、追加処理を行ってループを抜ける
if (parent.childNum !== 0 && parent.childNum > parent.page.children.length) {
// 親見出しの子にする
parent.page.children.push(page.title);
// 最初の子の場合のみ、親見出しとつなげる
if (parent.page.children.length === 1) {
page.prev = parent.page.title;
}
break;
}
// 見出しが完成しているときは、pageを同階層の次の見出しとみなす
// 小見出しがない見出しもここで処理される
parent.page.next = page.title;
page.prev = parent.page.title;
}
// 完成済みの見出しをparentsから削って返す
// 添字がi未満のparentsが、完成した見出しとなる
for (const item of parents.splice(0, i)) {
yield item.page;
}
// 一旦parentsにためてから処理する
parents.unshift({ page, childNum });
}
for (const parent of parents) {
yield parent.page;
}
}
function* range(start: number, end: number) {
for (let i = start; i <= end; i++) {
yield i;
}
}
parse()のテスト
code:mod_test.ts
import { parse, sort, Page } from "./mod.ts";
import Parser from "../papaparse/mod.ts";
import { assertEquals } from "../deno_std%2Ftesting/asserts.ts";
Deno.test("parse()", async (t) => {
await t.step("一致を確かめる", () => {
const csv =
`1,0,2,3
1.1,3,3,
1.2,x,x,2
1.2.1,4,6,0
1.2.2,6,12,0
column1,13,14,0
2,,,2
2.1,15,16,
2.2,17,20,1
column2,19,19,0
3,20,24,0`;
const { data } = Parser.parse<string[]>(csv);
{ title: "1.1", next: "1.2", prev: "1", indice: 3, children: [], }, { title: "1.2.1", next: "1.2.2", prev: "1.2", indice: 4, 5, 6, children: [], }, { title: "1.2.2", next: "column1", prev: "1.2.1", indice: 6, 7, 8, 9, 10, 11, 12, children: [], }, { title: "1.2", next: "column1", prev: "1.1", indice: [], children: "1.2.1", "1.2.2", }, { title: "column1", next: "2", prev: "1.2", indice: 13, 14, children: [], }, { title: "1", next: "2", prev: "", indice: 0, 1, 2, children: "1.1", "1.2", "column1", }, { title: "2.1", next: "2.2", prev: "2", indice: 15, 16, children: [], }, { title: "column2", next: "3", prev: "2.2", indice: 19, children: [], }, { title: "2", next: "3", prev: "1", indice: [], children: "2.1", "2.2", }, { title: "3", next: "", prev: "2", indice: 20, 21, 22, 23, 24, children: [], }, ]);
});
});
code:test.ts
import { parse, sort } from "./mod.ts";
import Parser from "../papaparse/mod.ts";
const csv = await res.text();
const { data } = Parser.parse<string[]>(csv);
open(URL.createObjectURL(blob));