Scrapbox書籍のformat@0.2.0
Scrapbox書籍のformatその2
特徴
1見出し1ページにまとめる
親見出しには子見出しへの階層構造型リンクを貼る
active読書のコツ
タイトルに見出し番号と見出しに含まれる最初のページ番号を入れる
一つのactive読書用projectで複数の書籍を読む際の重複防止用
目次には更に書名を入れておく
ページ番号はscrapboxのimport機能で重複が検出された時に手動でつければいいか。
使用するscrapbox書籍用目次のデータ構造
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
Scrapbox書籍のformat@0.2.0#63629ec91280f00000240b4cも参照
parserとstrinigfyの実装
$ deno check --remote -r=https://scrapbox.io https://scrapbox.io/api/code/takker/Scrapbox書籍のformat@0.2.0/mod.ts
$ deno test --doc -r=https://scrapbox.io https://scrapbox.io/api/code/takker/Scrapbox書籍のformat@0.2.0/mod.ts
code:mod.ts
import { toAsyncIterable } from "jsr:@core/iterutil@0.9/async/to-async-iterable";
export interface Page {
/** ページタイトル */
title: string;
/** このページに含める画像のインデックス */
indice: number[];
/** 前ページのタイトル
*
* なければ空文字
*/
prev: string;
/** 次ページのタイトル
*
* なければ空文字
*/
next: string;
/** 子ページのタイトル */
children: string[];
}
export interface PageSource {
url: string | URL;
text: string;
}
/** ページ情報と画像URLとテキストから、scrapboxのページを作る TransformStream */
export class PageStringifyStream extends TransformStream<Page, string[]> {
#stack: PageSource[] = [];
#iter: AsyncIterator<PageSource>;
#iterReady = Promise.resolve();
async #at(index: number) {
const item = this.#stack.at(index);
if (item) return item;
await this.#iterReady;
const ready = (async () => {
while (true) {
const { done, value } = await this.#iter.next();
if (done) return;
this.#stack.push(value);
if (index === this.#stack.length - 1) return value;
}
})();
this.#iterReady = ready as Promise<void>;
return ready;
}
/**
* @param source 書籍に使う画像のURLとテキストのセット
* @param offset=0 書籍formatで指定したページ番号が0以外で始まる場合、この数値を変えて調節する
*/
constructor(
source: Iterable<PageSource> | AsyncIterable<PageSource>,
offset=0,
) {
super({
transform: async (page, controller) => {
controller.enqueue([
page.title,
...(await Promise.all(page.indice.map(async (i) => {
const source = await this.#at(i+offset);
if (!source) {
console.warn(Could not find the image source for ${i}.);
return [];
}
return [
...source.text.split("\n"),
"",
[${source.url}],
"",
];
}))).flat(),
...page.children.map((title) => [${title}]),
"",
<= ${page.prev !== "" ? ${page.prev}` : ""} | ${
page.next !== "" ? [${page.next}] : ""
} =>`,
"",
]);
},
});
this.#iter = (Symbol.iterator in source ? toAsyncIterable(source) : source)
Symbol.asyncIterator();
}
}
/** 生成したページデータを並び替える
* @example
* `ts
* import { CsvParseStream } from "jsr:@std/csv@1/parse-stream";
* import { assertEquals } from "jsr:@std/assert@1/equals";
*
* 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 sorted = ReadableStream.from(csv)
* .pipeThrough(new CsvParseStream())
* .pipeThrough(new IndexParseStream())
* .pipeThrough(new PageSortStream());
*
* assertEquals<Page[]>(await Array.fromAsync(sorted), [
* {
* title: "1",
* next: "2",
* prev: "",
* indice: 0, 1, 2,
* children: "1.1", "1.2", "column1",
* },
* { title: "1.1", next: "1.2", prev: "1", indice: 3, children: [] },
* {
* title: "1.2",
* next: "column1",
* prev: "1.1",
* indice: [],
* children: "1.2.1", "1.2.2",
* },
* {
* 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: "column1",
* next: "2",
* prev: "1.2",
* indice: 13, 14,
* children: [],
* },
* {
* title: "2",
* next: "3",
* prev: "1",
* indice: [],
* children: "2.1", "2.2",
* },
* { title: "2.1", next: "2.2", prev: "2", indice: 15, 16, children: [] },
* {
* title: "2.2",
* next: "3",
* prev: "2.1",
* indice: 17, 18, 19, 20,
* children: "column2",
* },
* { title: "column2", next: "3", prev: "2.2", indice: 19, children: [] },
* {
* title: "3",
* next: "",
* prev: "2",
* indice: 20, 21, 22, 23, 24,
* children: [],
* },
* ]);
* `
*/
export class PageSortStream extends TransformStream<Page, Page> {
#stack: Page[] = [];
#find(title: string): Page | undefined {
const index = this.#stack.findIndex((page) => page.title === title);
if (index < 0) return;
const target = this.#stack.splice(index, 1);
return target;
}
constructor() {
let returnedFirst = false;
let next = "";
const enqueueAndSetNext = (
page: Page,
controller: TransformStreamDefaultController<Page>,
) => {
controller.enqueue(page);
next = page.children.at(0) ?? page.next;
};
super({
transform: (page, controller) => {
if (page.title === next || (!returnedFirst && page.prev === "")) {
if (!returnedFirst && page.prev === "") returnedFirst = true;
enqueueAndSetNext(page, controller);
return;
}
this.#stack.push(page);
},
flush: (controller) => {
if (!returnedFirst) {
const first = this.#stack.find((page) => page.prev === "");
if (!first) throw new Error("Could not find the first page.");
enqueueAndSetNext(first, controller);
}
while (next !== "") {
const page = this.#find(next);
if (!page) throw new Error(Could not find the next page "${next}".);
enqueueAndSetNext(page, controller);
}
},
});
}
}
/**
* @example
* `ts
* import { CsvParseStream } from "jsr:@std/csv@1/parse-stream";
* import { assertEquals } from "jsr:@std/assert@1/equals";
*
* 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 = ReadableStream.from(csv)
* .pipeThrough(new CsvParseStream())
* .pipeThrough(new IndexParseStream());
*
* assertEquals<Page[]>(await Array.fromAsync(data), [
* { 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.2",
* next: "3",
* prev: "2.1",
* indice: 17, 18, 19, 20,
* children: "column2",
* },
* {
* title: "2",
* next: "3",
* prev: "1",
* indice: [],
* children: "2.1", "2.2",
* },
* {
* title: "3",
* next: "",
* prev: "2",
* indice: 20, 21, 22, 23, 24,
* children: [],
* },
* ]);
* `
*/
export class IndexParseStream extends TransformStream<string[], Page> {
constructor() {
const parents: { page: Page; childNum: number }[] = [];
super({
transform(row, controller) {
// for loopの都合上、parentsには先頭から要素を追加する
// 階層の深い順にparentが並ぶ
const title, start, end, childNumText = row;
const s = /^\d+$/.test(start) ? Math.abs(parseInt(start)) : -1;
const e = /^\d+$/.test(end) ? Math.abs(parseInt(end)) : -1;
const indice = s !== -1 && e !== -1 ? ...range(s, e) : [];
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++) {
const parent = parentsi;
// 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 { page } of parents.splice(0, i)) {
controller.enqueue(page);
}
// 一旦parentsにためてから処理する
parents.unshift({ page, childNum });
},
flush(controller) {
for (const { page } of parents) {
controller.enqueue(page);
}
},
});
}
}
function* range(start: number, end: number) {
for (let i = start; i <= end; i++) {
yield i;
}
}
#2024-11-10 14:49:43 実装を破壊的に書き換えた
ReadableStreamベースにし、OCRの配列を全てfetchしなくてもstringifyできるようにするなどした
前の実装はScrapbox書籍のformat@0.2.0の実装~2023-05-07に隔離した
#2023-05-07 19:21:01
#2023-03-08 10:26:59
#2022-11-03 01:45:46
#2022-08-20 14:33:15
#2022-08-04 12:51:22
#2022-07-10 12:19:48