book/BookDirectoryStep.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import { HtmlRR0SsgContext } from "../RR0SsgContext"
import { DirectoryStep, HtmlLinks, HtmlMeta, OutputFunc, SsgConfig, SsgFile } from "ssg-api"
import { RR0FileUtil } from "../util/file/RR0FileUtil"
import { Book } from "./Book"
import { StringUtil } from "../util/string/StringUtil"
import { HtmlTag } from "../util/HtmlTag"
import fs from "fs"
import path from "path"
import { Chapter } from "./Chapters"
import { PeopleService } from "../people/PeopleService"

/**
 * Scan directories for book information, then populates a template with collected data.
 */
export class BookDirectoryStep extends DirectoryStep {

  constructor(dirs: string[], template: string, protected outputFunc: OutputFunc,
              config: SsgConfig, protected outDir: string, name: string,
              protected bookMeta: Map<string, HtmlMeta>, protected bookLinks: Map<string, HtmlLinks>,
              protected peopleService: PeopleService) {
    super(dirs, [], template, config, name)
  }

  static async create(outputFunc: OutputFunc, config: SsgConfig, bookMeta: Map<string, HtmlMeta>,
                      bookLinks: Map<string, HtmlLinks>, peopleService: PeopleService): Promise<BookDirectoryStep> {
    const dirs = RR0FileUtil.findDirectoriesContaining("book*.json")
    return new BookDirectoryStep(dirs, "book/index.html", outputFunc, config, config.outDir, "all books", bookMeta,
      bookLinks, peopleService)
  }

  protected async processDirs(context: HtmlRR0SsgContext, dirNames: string[]): Promise<void> {
    const books = this.scan(context, dirNames)
    await this.tocAll(context, books)
    const directoriesHtml = this.toList(books)
    context.outputFile.contents = context.outputFile.contents.replace(`<!--#echo var="directories" -->`,
      directoriesHtml)
    await this.outputFunc(context, context.outputFile)
  }

  protected scan(context: HtmlRR0SsgContext, dirNames: string[]): Book[] {
    const books: Book[] = []
    for (const dirName of dirNames) {
      const dirBook: Book = {
        dirName,
        authors: [],
        publication: {publisher: "", time: undefined},
        summary: "",
        title: "",
        variants: []
      }
      books.push(dirBook)
      try {
        const jsonFileInfo = SsgFile.read(context, `${dirName}/book.json`)
        Object.assign(dirBook, JSON.parse(jsonFileInfo.contents))
      } catch (e) {
        context.warn(`${dirName} has no book*.json description`)
      }
    }
    return books
  }

  /**
   * Convert an array of Case[] to an <ul> HTML unordered list.
   *
   * @param books
   */
  protected toList(books: Book[]) {
    const listItems = books.map(dirBook => {
      if (!dirBook.title) {
        const lastSlash = dirBook.dirName.lastIndexOf("/")
        const lastDir = dirBook.dirName.substring(lastSlash + 1)
        dirBook.title = StringUtil.camelToText(lastDir)
      }
      return this.toListItem(dirBook)
    })
    return HtmlTag.toString("ul", listItems.join("\n"), {class: "links"})
  }

  /**
   * Convert a Case object to an HTML list item.
   *
   * @param dirBook
   */
  protected toListItem(dirBook: Book) {
    const attrs: { [name: string]: string } = {}
    const titles = []
    const details: string[] = []
    const authors = dirBook.authors
    const authorStr = authors ? authors.join(" & ") + ": " : ""
    const time = dirBook.publication.time
    if (time) {
      const timeDetail = time.getYear()
      details.push(HtmlTag.toString("time", timeDetail.toString()))
    }
    const text: (string | string[])[] = [authorStr, dirBook.title]
    if (details.length > 0) {
      text.push(`(${details.join(", ")})`)
    }
    const innerHTML = text.join(" ").trim()
    const a = fs.existsSync(path.join(dirBook.dirName, "index.html")) ? HtmlTag.toString("a", innerHTML,
      {href: "/" + dirBook.dirName + "/"}) : innerHTML
    if (titles.length) {
      attrs.title = titles.join(", ")
    }
    return HtmlTag.toString("li", a, attrs)
  }

  protected async tocAll(context: HtmlRR0SsgContext, books: Book[]) {
    for (const book of books) {
      await this.toc(context, book)
    }
  }

  protected async toc(context: HtmlRR0SsgContext, book: Book) {
    const startFileName = path.join(book.dirName, "index.html")
    try {
      context.read(startFileName)
      const startFileNames = [context.inputFile.name]
      const variants = context.inputFile.lang.variants
      for (const variant of variants) {
        const parsed = path.parse(startFileName)
        const variantFileName = path.join(parsed.dir, `${parsed.name}_${variant}${parsed.ext}`)
        startFileNames.push(variantFileName)
      }
      for (const startFileName of startFileNames) {
        const chapter = new Chapter(context, startFileName)
        await chapter.scan()
        const chapterBefore = chapter.toString()
        context.logger.debug("toc before:", chapterBefore)
        await chapter.update()
        const chapterAfter = chapter.toString()
        context.logger.debug("toc after:", chapterAfter)
        context.logger.log("Updated toc for", chapter.context.inputFile.name)
        book.variants.push(chapter)
        this.bookMeta.set(startFileName, chapter.context.outputFile.meta)
        this.bookLinks.set(startFileName, chapter.context.outputFile.links)
      }
    } catch (e) {
      context.logger.error("Could not check TOC of " + startFileName, e.message)
    }
  }
}