export interface Page { /** ページタイトル */ title: string; /** このページに含める画像のインデックス */ indice: number[]; /** 前ページのタイトル * * なければ空文字 */ prev: string; /** 次ページのタイトル * * なければ空文字 */ next: string; /** 子ページのタイトル */ children: string[]; } /** ページ情報と画像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) => [ ...text[i + offset].split("\n"), "", `[${imageURLs[i + offset]}]`, "", ]), ...page.children.map((title) => `[${title}]`), "", `<= ${page.prev !== "" ? `[${page.prev}]` : ""} | ${page.next !== "" ? `[${page.next}]` : ""} =>`, "", ].join("\n"); /** 生成したページデータを並び替える */ export function* sort(pages: IterableIterator): Generator { 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 { 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; } } /** iteratorを少しずつ消化する函数 */ function* read(iterator: IterableIterator): Generator { while (true) { const { done, value } = iterator.next(); if (done) return; yield value; } } export function* parse(rows: Iterable): Generator { // for loopの都合上、parentsには先頭から要素を追加する // 階層の深い順にparentが並ぶ const parents: { page: Page, childNum: number; }[] = []; for (const row of rows) { 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 = parents[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; } }