@rr0/cms
Version:
RR0 Content Management System (CMS)
274 lines (273 loc) • 16.3 kB
JavaScript
import path from "path";
import fs from "fs";
import { CaseSummaryRenderer, ChronologyReplacerFactory, CsvMapper, EventReplacer, EventReplacerFactory, HttpSource, SsiTitleReplaceCommand, TimeElementFactory, TimeLinkDefaultHandler, TimeRenderer, TimeReplacer, TimeReplacerFactory, TimeService, TimeTextBuilder, TimeUrlBuilder } from "./time/index.js";
import { CaseDirectoryStep, CaseFactory, CaseService } from "./science/crypto/ufo/enquete/dossier/index.js";
import { cities, CityService, CmsOrganizationFactory, countries, departments, DepartmentService, OrganizationService, regions, RegionService } from "./org/index.js";
import { RR0ContextImpl } from "./RR0Context.js";
import { HtmlTable } from "./util/index.js";
import { ClassDomReplaceCommand, CopyStep, DomReplaceCommand, HtAccessToNetlifyConfigReplaceCommand, HtmlFileContents, Ssg, SsiIncludeReplaceCommand } from "ssg-api";
import { AuthorReplaceCommand, PeopleDirectoryStepFactory, PeopleReplacerFactory } from "./people/index.js";
import { PersistentSourceRegistry, SourceFileCounter, SourceIndexStep, SourceRenderer, SourceReplacer, SourceReplacerFactory } from "./source/index.js";
import { NoteFileCounter, NoteRenderer, NoteReplacer, NoteReplacerFactory } from "./note/index.js";
import { AnchorReplaceCommand, CaseAnchorHandler, DataAnchorHandler } from "./anchor/index.js";
import { MetaLinkReplaceCommand } from "./MetaLinkReplaceCommand.js";
import { OutlineReplaceCommand } from "./outline/index.js";
import { ImageCommand } from "./ImageCommand.js";
import { SearchIndexStep, SearchVisitor } from "./search/index.js";
import { OpenGraphCommand } from "./OpenGraphCommand.js";
import { BookContentVisitor, BookDirectoryStep } from "./book/index.js";
import { APIFactory } from "./tech/index.js";
import { RR0ContentStep } from "./RR0ContentStep.js";
import { DataContentVisitor } from "./DataContentVisitor.js";
import { TimeContext } from "@rr0/time";
import { writeFile } from "@javarome/fileutil";
import { AllDataService, EventDataFactory, PeopleFactory, PeopleService, RR0EventFactory, TypedDataFactory } from "@rr0/data";
import { GooglePlaceService } from "@rr0/place";
import { PeopleHtmlRenderer } from "./people/PeopleHtmlRenderer.js";
import { CountryService } from "./org/country/CountryService.js";
const outputFunc = async (context, outFile) => {
try {
if (context.file instanceof HtmlFileContents) {
context.file.contents = context.file.serialize();
}
context.log("Writing", outFile.name);
await outFile.write();
context.file.contents = outFile.contents;
}
catch (e) {
context.error(outFile.name, e);
}
};
export class CMSGenerator {
constructor(options) {
this.options = options;
this.http = new HttpSource();
this.config = {
getOutputPath(context) {
return path.join(options.outDir, context.file.name);
}
};
const eventFactory = new RR0EventFactory();
const orgFactory = new CmsOrganizationFactory(eventFactory);
const orgConfig = options.dataOptions.org;
const sightingFactory = new EventDataFactory(eventFactory, ["sighting"], ["index"]);
const caseFactory = this.caseFactory = new CaseFactory(eventFactory);
const peopleFactory = this.peopleFactory = new PeopleFactory(eventFactory);
const apiFactory = new APIFactory(eventFactory);
const bookFactory = new TypedDataFactory(eventFactory, "book");
const articleFactory = new TypedDataFactory(eventFactory, "article");
const factories = [orgFactory, caseFactory, peopleFactory, bookFactory, articleFactory, sightingFactory, apiFactory];
const dataService = this.dataService = new AllDataService(factories);
dataService.getFromDir("", ["people", "case"]).then(data => {
console.debug(data);
});
this.orgService = new OrganizationService(dataService, orgFactory, orgConfig, undefined, []);
const countryService = this.countryService = new CountryService(dataService, orgFactory, orgConfig, undefined, countries);
const regionService = new RegionService(dataService, orgFactory, orgConfig, countryService, regions);
const departmentService = this.departmentService = new DepartmentService(dataService, orgFactory, orgConfig, regionService, departments);
const cityService = new CityService(dataService, orgFactory, orgConfig, departmentService, cities);
this.placeService = new GooglePlaceService("place", options.googleMapsApiKey);
this.cityService = cityService;
const timeTextBuilder = this.timeTextBuilder = new TimeTextBuilder(options.timeFormat);
const timeOptions = options.dataOptions.time;
const timeUrlBuilder = this.timeUrlBuilder = new TimeUrlBuilder(timeOptions);
this.timeRenderer = new TimeRenderer(timeUrlBuilder, timeTextBuilder);
this.timeService = new TimeService(dataService, timeOptions);
}
async generate(args) {
const timeContext = new TimeContext();
const context = new RR0ContextImpl(this.options.locale, timeContext, this.config, undefined, undefined, undefined, this);
context.setVar("mapsApiKey", this.options.googleMapsApiKey);
context.setVar("mail", this.options.mail);
const config = this.config;
const copies = this.options.copies;
const outDir = this.options.outDir;
const force = args.force === "true";
const ssg = new Ssg(config);
const dataService = this.dataService;
const timeService = this.timeService;
const timeRenderer = this.timeRenderer;
const timeFormat = this.options.timeFormat;
const { timeFiles, timeElementFactory, timeReplacer } = this.setupTime(context);
const orgFactory = dataService.factories.find(f => f.type === "org");
if (orgFactory) {
const orgFiles = this.options.dataOptions.org.files;
context.setVar("orgFilesCount", orgFiles.length);
}
const placeFactory = dataService.factories.find(f => f.type === "place");
if (placeFactory) {
const placeFiles = await placeFactory.getFiles();
context.setVar("placeFilesCount", placeFiles.length);
}
const { caseService, ufoCasesStep } = await this.setupCases(timeElementFactory);
const peopleRenderer = new PeopleHtmlRenderer();
const { peopleService, peopleSteps } = await this.setupPeople(context, peopleRenderer, this.options.copies);
const timeTextBuilder = this.timeTextBuilder;
const searchVisitor = new SearchVisitor({ notIndexedUrls: ["404.html", "Referencement.html"], indexWords: false }, timeTextBuilder);
const { sourceRenderer, sourceFactory, sourceReplacerFactory } = this.setupSources(timeTextBuilder, timeFormat);
const { noteRenderer, noteReplacerFactory } = this.setupNotes();
const caseRenderer = new CaseSummaryRenderer(noteRenderer, sourceFactory, sourceRenderer, timeElementFactory);
const mappings = this.options.mappings || [];
mappings.forEach(mapping => mapping.init(this));
const timeUrlBuilder = this.timeUrlBuilder;
const databaseAggregationCommand = new DomReplaceCommand(".contents ul", new ChronologyReplacerFactory(timeUrlBuilder, mappings, caseRenderer));
const eventReplacer = new EventReplacer(caseRenderer, dataService);
const getOutputPath = (context) => path.join(outDir, context.file.name);
const toProcess = new Set(this.options.directoryPages);
const csvTransformer = new class {
transform(context, file) {
const fileName = file.name;
if (!fileName.endsWith(".csv")) {
return undefined;
}
const csv = fs.readFileSync(fileName, { encoding: "utf-8" });
const headers = [];
const obj = new CsvMapper().parse(csv, headers);
return HtmlTable.create(obj, headers);
}
}();
const htAccessToNetlifyConfig = {
replacements: [new HtAccessToNetlifyConfigReplaceCommand(this.options.siteBaseUrl)],
roots: [".htaccess"],
getOutputPath: (_context) => "netlify.toml"
};
const contentRoots = this.options.contentRoots;
const contentStepOptions = {
contentConfigs: [htAccessToNetlifyConfig, {
roots: contentRoots,
replacements: [new class extends SsiIncludeReplaceCommand {
filePath(context, fileNameArg) {
const dirName = path.dirname(context.file.name);
return fileNameArg.startsWith("/") ? path.join(process.cwd(), fileNameArg) : path.join(dirName, fileNameArg);
}
}([csvTransformer])],
getOutputPath
}],
outputFunc, fileVisitors: [], contentVisitors: [], force, name: "content includes", toProcess
};
const includeStep = new RR0ContentStep(contentStepOptions, timeService);
ssg.add(includeStep);
ssg.add(ufoCasesStep);
ssg.add(...peopleSteps);
if (contentRoots) {
const dataContentVisitor = new DataContentVisitor(dataService, caseRenderer, timeElementFactory);
const contentVisitors = [dataContentVisitor, searchVisitor];
const timeDefaultHandler = (context) => this.timeService.titleFromFile(context, context.file.name, this.timeTextBuilder);
const pageReplaceCommands = [
new SsiTitleReplaceCommand([timeDefaultHandler]),
new AuthorReplaceCommand(timeRenderer),
...this.options.contentReplacers
];
const contentsReplaceCommand = [
new ClassDomReplaceCommand(new EventReplacerFactory(eventReplacer), "event"),
new ClassDomReplaceCommand(sourceReplacerFactory, "source"),
new DomReplaceCommand("time", new TimeReplacerFactory(timeReplacer, timeUrlBuilder)),
new ClassDomReplaceCommand(new PeopleReplacerFactory(peopleService, peopleRenderer), "people"),
new ClassDomReplaceCommand(noteReplacerFactory, "note"),
new MetaLinkReplaceCommand(new TimeLinkDefaultHandler(timeService, timeUrlBuilder, timeTextBuilder)),
databaseAggregationCommand
];
const contentReplacements = [
...pageReplaceCommands,
...contentsReplaceCommand,
new OutlineReplaceCommand(),
new AnchorReplaceCommand(this.options.siteBaseUrl, [new CaseAnchorHandler(caseService, timeTextBuilder), new DataAnchorHandler(dataService)]),
new ImageCommand(outDir, 275, 500),
new OpenGraphCommand(outDir, timeFiles, this.options.siteBaseUrl, timeService, timeTextBuilder)
];
ssg.add(new RR0ContentStep({
contentConfigs: [{ roots: contentRoots, replacements: contentReplacements, getOutputPath }],
outputFunc, fileVisitors: [], contentVisitors, force, name: "contents replacements", toProcess
}, timeService));
if (args.books) {
const bookMeta = new Map();
const bookLinks = new Map();
contentVisitors.push(new BookContentVisitor(bookMeta, bookLinks));
ssg.add(await BookDirectoryStep.create(outputFunc, config, bookMeta, bookLinks));
}
}
const reindex = args.reindex;
if (reindex === null || reindex === void 0 ? void 0 : reindex.includes("search")) {
ssg.add(new SearchIndexStep("search/index.json", searchVisitor));
}
if (reindex === null || reindex === void 0 ? void 0 : reindex.includes("sources")) {
ssg.add(new SourceIndexStep(this.options.sourceRegistryFileName, sourceFactory));
}
if (copies) {
const copyConfig = {
getOutputPath,
sourcePatterns: Array.from(new Set(copies)),
options: { ignore: ["node_modules/**", "out/**"] }
};
ssg.add(new CopyStep(copyConfig));
}
try {
const result = await ssg.start(context);
context.log("Completed", result);
}
catch (err) {
try {
context.error(err, context.file.name);
}
catch (e) {
context.error(err);
}
}
finally {
console.timeEnd("ssg");
}
}
setupSources(timeTextBuilder, timeFormat) {
const sourceRenderer = new SourceRenderer(timeTextBuilder);
const sourceFactory = new PersistentSourceRegistry(this.dataService, this.http, this.options.siteBaseUrl, this.options.sourceRegistryFileName, timeFormat, this.timeService);
const sourceCounter = new SourceFileCounter();
const sourceReplacer = new SourceReplacer(sourceRenderer, sourceFactory, sourceCounter);
const sourceReplacerFactory = new SourceReplacerFactory(sourceReplacer);
return { sourceRenderer, sourceFactory, sourceReplacerFactory };
}
setupTime(context) {
const timeFiles = this.options.dataOptions.time.files;
context.setVar("timeFilesCount", timeFiles.length);
const timeElementFactory = new TimeElementFactory(this.timeRenderer);
const timeReplacer = new TimeReplacer(timeElementFactory);
return { timeFiles, timeElementFactory, timeReplacer };
}
async setupPeople(context, peopleRenderer, copies) {
const peopleFiles = await this.peopleFactory.getFiles();
const peopleService = new PeopleService(this.dataService, this.peopleFactory, { files: peopleFiles, rootDir: "people" });
const peopleList = await peopleService.getAll();
context.setVar("peopleFilesCount", peopleFiles.length);
const peopleDirectoryFactory = new PeopleDirectoryStepFactory(outputFunc, this.config, peopleService, peopleRenderer, this.options.directoryExcluded);
const directoryOptions = this.options.directoryOptions;
for (const directoryOption in directoryOptions) {
directoryOptions[directoryOption] = directoryOptions[directoryOption];
}
const peopleSteps = await peopleDirectoryFactory.create(directoryOptions);
const dirsContainingPeopleJson = peopleSteps.reduce((rootDirs, peopleStep) => {
rootDirs.push(...peopleStep.config.rootDirs);
return rootDirs;
}, []);
copies.push(...dirsContainingPeopleJson.map(dir => path.join(dir, "people.json")));
await writeFile(path.join(this.options.outDir, "peopleDirs.json"), JSON.stringify(peopleList.map(people => people.dirName)), "utf-8");
return { peopleService, peopleSteps, copies };
}
setupNotes() {
const noteCounter = new NoteFileCounter();
const noteRenderer = new NoteRenderer(noteCounter);
const noteReplacer = new NoteReplacer(noteRenderer);
const noteReplacerFactory = new NoteReplacerFactory(noteReplacer);
return { noteRenderer, noteReplacerFactory };
}
async setupCases(timeElementFactory) {
const caseFiles = await this.caseFactory.getFiles();
const caseService = new CaseService(this.dataService, this.caseFactory, timeElementFactory, caseFiles);
const ufoCaseDirectoryFile = this.options.ufoCaseDirectoryFile;
const ufoCasesExclusions = this.options.ufoCasesExclusions;
const ufoCasesStep = new CaseDirectoryStep(caseService, caseService.files, ufoCasesExclusions, ufoCaseDirectoryFile, outputFunc, this.config);
// Publish case.json files so that vraiufo.com will find them
const ufoCasesRootDirs = ufoCasesStep.config.rootDirs;
this.options.copies.push(...ufoCasesRootDirs.map(dir => path.join(dir, "case.json")));
await writeFile(path.join(this.options.outDir, "casesDirs.json"), JSON.stringify(ufoCasesRootDirs), "utf-8");
return { caseService, ufoCasesStep };
}
}