people/PeopleDirectoryStep.ts

Summary

Maintainability
D
2 days
Test Coverage
import { Occupation } from "./Occupation"
import { Time } from "../time/Time"
import { KnownPeople, People } from "./People"
import { promise as glob } from "glob-promise"
import { HtmlRR0SsgContext } from "../RR0SsgContext"
import { HtmlTag } from "../util/HtmlTag"
import { DirectoryStep, OutputFunc, SsgConfig, SsgStep } from "ssg-api"
import { StringUtil } from "../util/string/StringUtil"
import { RR0FileUtil } from "../util/file/RR0FileUtil"
import { PeopleService } from "./PeopleService"
import { CountryCode } from "../org/country/CountryCode"
import { Gender } from "@rr0/common"

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

  constructor(dirs: string[], excludedDirs: string[], template: string, protected outputFunc: OutputFunc,
              config: SsgConfig, protected filterOccupations: Occupation[], protected service: PeopleService,
              name = "people directory") {
    super(dirs, excludedDirs, template, config, name)
  }

  static getPeopleLink(context: HtmlRR0SsgContext,
                       people: KnownPeople, pseudoPeopleList: People[], allCountries: Set<CountryCode>,
                       occupations: Set<Occupation>, filterOccupations: Occupation[], content?: string): HTMLElement {
    const dirName = people.dirName
    const titles = []
    const classList = ["people-resolved"]
    if (pseudoPeopleList.indexOf(people) >= 0) {
      classList.push("pseudonym")
      titles.push("(pseudonyme)")
    }
    if (people.hoax) {
      classList.push("canular")
    }
    let birthTimeStr = people.birthTime as unknown as string
    if (birthTimeStr) {
      const birthTime = people.birthTime = Time.dateFromIso(birthTimeStr)
      birthTimeStr = birthTime.getFullYear().toString()
    }
    let deathTimeStr = people.deathTime as unknown as string
    if (deathTimeStr) {
      const deathTime = people.deathTime = Time.dateFromIso(deathTimeStr)
      deathTimeStr = deathTime.getFullYear().toString()
    }
    if (people.isDeceased()) {
      classList.push("deceased")
    }
    if (birthTimeStr || deathTimeStr) {
      const timeStr = birthTimeStr ? deathTimeStr ? birthTimeStr + "-" + deathTimeStr : birthTimeStr + "-" : "-" + deathTimeStr
      titles.push(timeStr)
    }
    const age = people.getAge()
    if (age) {
      titles.push(`${age} ans`)
    }
    const countries = people.countries
    if (countries) {
      for (const country of countries) {
        allCountries.add(country)
        const countryLabel = context.messages.country[country]?.title
        if (!countryLabel) {
          throw new Error(`No title for country "${country}"`)
        }
        titles.push(countryLabel)
        classList.push(`country-${country}`)
      }
    }
    const gender = people.gender || Gender.male
    for (const occupation of people.occupations) {
      if (filterOccupations.length > 1 || !filterOccupations.includes(occupation)) {
        occupations.add(occupation)
        const occupationMsg = context.messages.people.occupation[occupation]
        if (!occupationMsg) {
          throw Error(
            `No message to translate occupation "${occupation}" in ${context.locale}, as specified in ${people.dirName}/people*.json`)
        }
        classList.push(`occupation-${occupation}`)
        titles.push(occupationMsg(gender))
      }
    }
    const text = content || people.lastAndFirstName
    const document = context.outputFile.document
    const peopleLink = document.createElement("a")
    peopleLink.innerHTML = text
    peopleLink.href = `/${dirName}/`
    if (people.discredited) {
      peopleLink.append(" 🤥")
      titles.push("discrédité")
    }
    const elem = document.createElement("span")
    if (titles.length) {
      elem.title = titles.join(", ")
    }
    if (classList.length) {
      elem.classList.add(...classList)
    }
    const portraitUrl = people.portraitUrl
    if (portraitUrl) {
      const portraitElem = document.createElement("img")
      portraitElem.src = portraitUrl
      portraitElem.alt = people.lastAndFirstName
      portraitElem.className = "portrait"
      portraitElem.loading = "lazy"
      portraitElem.width = 75
      peopleLink.append(portraitElem)
    }
    elem.append(peopleLink)
    return elem
  }

  static async create(outputFunc: OutputFunc, config: SsgConfig, service: PeopleService): Promise<SsgStep[]> {
    const dirs = RR0FileUtil.findDirectoriesContaining("people*.json")
    const excludedDirs = ["people/Astronomers_fichiers", "people/witness", "people/author"]
    const scientistsDirectoryStep = this.createScientists(dirs, excludedDirs, outputFunc, config, service)
    const ufologistsDirectoryStep = this.createUfologists(dirs, excludedDirs, outputFunc, config, service)
    const ufoWitnessesDirectoryStep = this.createWitnesses(dirs, excludedDirs, outputFunc, config, service)
    const astronomersDirectoryStep = this.createAstronomers(dirs, excludedDirs, outputFunc, config, service)
    const contacteesDirectoryStep = this.createContactees(dirs, excludedDirs, outputFunc, config, service)
    const pilotsDirectoryStep = this.createPilots(dirs, excludedDirs, outputFunc, config, service)
    const militaryDirectoryStep = this.createMilitary(dirs, excludedDirs, outputFunc, config, service)
    const allPeopleDirectoryStep = this.createAll(dirs, excludedDirs, outputFunc, config, service)
    const letterDirectorySteps = await this.createLetters(outputFunc, config, service)
    return [
      scientistsDirectoryStep,
      ufologistsDirectoryStep,
      ufoWitnessesDirectoryStep,
      astronomersDirectoryStep,
      contacteesDirectoryStep,
      pilotsDirectoryStep,
      militaryDirectoryStep,
      allPeopleDirectoryStep,
      ...letterDirectorySteps
    ]
  }

  static async createLetters(outputFunc: OutputFunc, config: SsgConfig, service: PeopleService) {
    const letterDirs = await glob("people/*/")
    const peopleLetterFiles = letterDirs.filter(l => /(.*?)\/[a-z]\//.test(l))
    const letterDirectorySteps: PeopleDirectoryStep[] = []
    for (const peopleLetterFile of peopleLetterFiles) {
      const c = peopleLetterFile.charAt(peopleLetterFile.length - 2)
      letterDirectorySteps.push(new PeopleDirectoryStep(
        [`people/${c}/*/`],
        [],
        `people/${c}/index.html`,
        outputFunc, config, [], service))
    }
    return letterDirectorySteps
  }

  static createAll(dirs: string[], excludedDirs: string[],
                   outputFunc: OutputFunc, config: SsgConfig, service: PeopleService) {
    return new PeopleDirectoryStep(dirs, excludedDirs, "people/index.html", outputFunc, config, [], service,
      "all people directories")
  }

  static createMilitary(dirs: string[], excludedDirs: string[],
                        outputFunc: OutputFunc, config: SsgConfig, service: PeopleService) {
    return new PeopleDirectoryStep(dirs, excludedDirs, "people/militaires.html", outputFunc, config,
      [Occupation.military], service, "military people directories")
  }

  static createPilots(dirs: string[], excludedDirs: string[], outputFunc: OutputFunc, config: SsgConfig,
                      service: PeopleService) {
    return new PeopleDirectoryStep(dirs, excludedDirs, "people/pilotes.html", outputFunc, config,
      [Occupation.astronaut, Occupation.pilot], service, "pilots directories")
  }

  static createContactees(dirs: string[], excludedDirs: string[], outputFunc: OutputFunc, config: SsgConfig,
                          service: PeopleService) {
    return new PeopleDirectoryStep(dirs, excludedDirs, "people/contactes.html", outputFunc, config,
      [Occupation.contactee], service, "contactees directories")
  }

  static createAstronomers(dirs: string[], excludedDirs: string[], outputFunc: OutputFunc, config: SsgConfig,
                           service: PeopleService) {
    return new PeopleDirectoryStep(dirs, excludedDirs, "people/astronomes.html", outputFunc, config,
      [Occupation.astronomer], service, "astronomers directories")
  }

  static createWitnesses(dirs: string[], excludedDirs: string[], outputFunc: OutputFunc, config: SsgConfig,
                         service: PeopleService) {
    return new PeopleDirectoryStep(dirs, excludedDirs, "people/witness/index.html", outputFunc, config,
      [Occupation.ufoWitness, Occupation.ufoWitness2, Occupation.contactee], service, "UFO witnesses directories")
  }

  static createUfologists(dirs: string[], excludedDirs: string[], outputFunc: OutputFunc, config: SsgConfig,
                          service: PeopleService) {
    return new PeopleDirectoryStep(dirs, excludedDirs, "people/ufologues.html", outputFunc, config,
      [Occupation.ufologist], service, "ufologists directories")
  }

  static createScientists(dirs: string[], excludedDirs: string[], outputFunc: OutputFunc, config: SsgConfig,
                          service: PeopleService) {
    return new PeopleDirectoryStep(dirs, excludedDirs,
      "people/scientifiques.html",
      outputFunc, config,
      [
        Occupation.anthropologist, Occupation.astronomer, Occupation.astrophysicist, Occupation.archeologist,
        Occupation.biochemist, Occupation.biologist, Occupation.biophysicist, Occupation.botanist,
        Occupation.chemist,
        Occupation.engineer, Occupation.exobiologist, Occupation.ethnologist,
        Occupation.geophysicist, Occupation.geologist, Occupation.geographer,
        Occupation.historian,
        Occupation.mathematician, Occupation.meteorologist,
        Occupation.neuroscientist, Occupation.neurologist, Occupation.neuropsychiatrist,
        Occupation.oceanographer,
        Occupation.philosopher, Occupation.physician, Occupation.psychologist, Occupation.physicist, Occupation.psychiatrist,
        Occupation.radioastronomer,
        Occupation.sociologist, Occupation.softwareEngineer
      ],
      service,
      "scientists directories"
    )
  }

  protected async processDirs(context: HtmlRR0SsgContext, dirNames: string[]): Promise<void> {
    let peopleList = await this.service.getPeopleFromDirs(context, dirNames)
    if (this.filterOccupations.length > 0) {
      peopleList = peopleList.filter((p: People) => p.occupations.some(o => this.filterOccupations.includes(o)))
    }
    const pseudoPeopleList = peopleList.reduce((prev: KnownPeople[], p: KnownPeople) => {
      if (p.pseudonyms?.length > 0) {
        for (const pseudonym of p.pseudonyms) {
          const pseudo = new KnownPeople(p.firstNames, p.lastName, p.pseudonyms, p.occupations, p.countries,
            p.discredited, p.birthTime, p.deathTime, p.gender, p.dirName, p.portraitUrl)
          pseudo.lastAndFirstName = pseudonym
          prev.push(pseudo)
        }
      }
      return prev
    }, [])
    peopleList = peopleList.concat(pseudoPeopleList).sort(
      (p1, p2) => p1.lastAndFirstName.localeCompare(p2.lastAndFirstName))
    const allCountries = new Set<CountryCode>()
    const occupations = new Set<Occupation>()
    const outputFile = context.outputFile
    const listItems = peopleList.map(
      people => {
        const elem = PeopleDirectoryStep.getPeopleLink(context, people, pseudoPeopleList, allCountries, occupations,
          this.filterOccupations)
        const item = outputFile.document.createElement("li")
        item.appendChild(elem)
        return item
      })
    const ul = outputFile.document.createElement("ul")
    ul.append(...listItems)
    ul.className = "links"
    outputFile.contents = outputFile.contents.replace(`<!--#echo var="directories" -->`,
      ul.outerHTML)
    {
      let countriesHtml = ""
      for (const country of Array.from(allCountries).sort()) {
        const countryStr = context.messages.country[country].title
        countriesHtml += `<span class="option"><label><input type="checkbox" id="country-${country}" onchange="find(event)"> ${countryStr}</label></span>`
      }
      outputFile.contents = outputFile.contents.replace(`<!--#echo var="countries" -->`,
        HtmlTag.toString("div", countriesHtml, {id: "countries"}))
    }
    {
      let occupationsHtml = ""
      for (const occupation of Array.from(occupations).sort()) {
        const occupationStr = StringUtil.capitalizeFirstLetter(
          context.messages.people.occupation[occupation](Gender.male))
        occupationsHtml += `<span class="option"><label><input type="checkbox" id="occupation-${occupation}" onchange="find(event)"> ${occupationStr}</label></span>`
      }
      outputFile.contents = outputFile.contents.replace(`<!--#echo var="occupations" -->`,
        HtmlTag.toString("div", occupationsHtml, {id: "occupations"}))
    }
    await this.outputFunc(context, outputFile)
  }
}