UNPKG

akasharender

Version:

Rendering support for generating static HTML websites or EPUB eBooks

1,338 lines 276 kB
/** * * Copyright 2014-2025 David Herron * * This file is part of AkashaCMS (http://akashacms.com/). * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _BaseFileCache_instances, _BaseFileCache_config, _BaseFileCache_name, _BaseFileCache_dirs, _BaseFileCache_is_ready, _BaseFileCache_cache_content, _BaseFileCache_map_renderpath, _BaseFileCache_dao, _BaseFileCache_watcher, _BaseFileCache_queue, _BaseFileCache_fExistsInDir; import { DirsWatcher } from '@akashacms/stacked-dirs'; import path from 'node:path'; import util from 'node:util'; import FS from 'fs'; import EventEmitter from 'events'; import micromatch from 'micromatch'; import { field, id, index, table, schema, BaseDAO } from 'sqlite3orm'; import { sqdb } from '../sqdb.js'; import fastq from 'fastq'; ///////////// Assets table let Asset = class Asset { }; __decorate([ id({ name: 'vpath', dbtype: 'TEXT' }), index('asset_vpath'), __metadata("design:type", String) ], Asset.prototype, "vpath", void 0); __decorate([ field({ name: 'mime', dbtype: 'TEXT' }), __metadata("design:type", String) ], Asset.prototype, "mime", void 0); __decorate([ field({ name: 'mounted', dbtype: 'TEXT' }), index('asset_mounted'), __metadata("design:type", String) ], Asset.prototype, "mounted", void 0); __decorate([ field({ name: 'mountPoint', dbtype: 'TEXT' }), index('asset_mountPoint'), __metadata("design:type", String) ], Asset.prototype, "mountPoint", void 0); __decorate([ field({ name: 'pathInMounted', dbtype: 'TEXT' }), index('asset_pathInMounted'), __metadata("design:type", String) ], Asset.prototype, "pathInMounted", void 0); __decorate([ field({ name: 'fspath', dbtype: 'TEXT' }), index('asset_fspath'), __metadata("design:type", String) ], Asset.prototype, "fspath", void 0); __decorate([ field({ name: 'renderPath', dbtype: 'TEXT' }), index('asset_renderPath'), __metadata("design:type", String) ], Asset.prototype, "renderPath", void 0); __decorate([ field({ name: 'mtimeMs', dbtype: "TEXT DEFAULT(datetime('now') || 'Z')" }), __metadata("design:type", String) ], Asset.prototype, "mtimeMs", void 0); __decorate([ field({ name: 'info', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Asset.prototype, "info", void 0); Asset = __decorate([ table({ name: 'ASSETS', withoutRowId: true, }) ], Asset); export { Asset }; await schema().createTable(sqdb, 'ASSETS'); export const assetsDAO = new BaseDAO(Asset, sqdb); await assetsDAO.createIndex('asset_vpath'); await assetsDAO.createIndex('asset_mounted'); await assetsDAO.createIndex('asset_mountPoint'); await assetsDAO.createIndex('asset_pathInMounted'); await assetsDAO.createIndex('asset_fspath'); await assetsDAO.createIndex('asset_renderPath'); //////////// Partials Table let Partial = class Partial { }; __decorate([ id({ name: 'vpath', dbtype: 'TEXT' }), index('partial_vpath'), __metadata("design:type", String) ], Partial.prototype, "vpath", void 0); __decorate([ field({ name: 'mime', dbtype: 'TEXT' }), __metadata("design:type", String) ], Partial.prototype, "mime", void 0); __decorate([ field({ name: 'mounted', dbtype: 'TEXT' }), index('partial_mounted'), __metadata("design:type", String) ], Partial.prototype, "mounted", void 0); __decorate([ field({ name: 'mountPoint', dbtype: 'TEXT' }), index('partial_mountPoint'), __metadata("design:type", String) ], Partial.prototype, "mountPoint", void 0); __decorate([ field({ name: 'pathInMounted', dbtype: 'TEXT' }), index('partial_pathInMounted'), __metadata("design:type", String) ], Partial.prototype, "pathInMounted", void 0); __decorate([ field({ name: 'fspath', dbtype: 'TEXT' }), index('partial_fspath'), __metadata("design:type", String) ], Partial.prototype, "fspath", void 0); __decorate([ field({ name: 'renderPath', dbtype: 'TEXT' }), index('partial_renderPath'), __metadata("design:type", String) ], Partial.prototype, "renderPath", void 0); __decorate([ field({ name: 'mtimeMs', dbtype: "TEXT DEFAULT(datetime('now') || 'Z')" }), __metadata("design:type", String) ], Partial.prototype, "mtimeMs", void 0); __decorate([ field({ name: 'docMetadata', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Partial.prototype, "docMetadata", void 0); __decorate([ field({ name: 'docContent', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Partial.prototype, "docContent", void 0); __decorate([ field({ name: 'docBody', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Partial.prototype, "docBody", void 0); __decorate([ field({ name: 'metadata', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Partial.prototype, "metadata", void 0); __decorate([ field({ name: 'info', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Partial.prototype, "info", void 0); Partial = __decorate([ table({ name: 'PARTIALS', withoutRowId: true, }) ], Partial); export { Partial }; await schema().createTable(sqdb, 'PARTIALS'); export const partialsDAO = new BaseDAO(Partial, sqdb); await partialsDAO.createIndex('partial_vpath'); await partialsDAO.createIndex('partial_mounted'); await partialsDAO.createIndex('partial_mountPoint'); await partialsDAO.createIndex('partial_pathInMounted'); await partialsDAO.createIndex('partial_fspath'); await partialsDAO.createIndex('partial_renderPath'); ///////////////// Layouts Table let Layout = class Layout { }; __decorate([ id({ name: 'vpath', dbtype: 'TEXT' }), index('layout_vpath'), __metadata("design:type", String) ], Layout.prototype, "vpath", void 0); __decorate([ field({ name: 'mime', dbtype: 'TEXT' }), __metadata("design:type", String) ], Layout.prototype, "mime", void 0); __decorate([ field({ name: 'mounted', dbtype: 'TEXT' }), index('layout_mounted'), __metadata("design:type", String) ], Layout.prototype, "mounted", void 0); __decorate([ field({ name: 'mountPoint', dbtype: 'TEXT' }), index('layout_mountPoint'), __metadata("design:type", String) ], Layout.prototype, "mountPoint", void 0); __decorate([ field({ name: 'pathInMounted', dbtype: 'TEXT' }), index('layout_pathInMounted'), __metadata("design:type", String) ], Layout.prototype, "pathInMounted", void 0); __decorate([ field({ name: 'fspath', dbtype: 'TEXT' }), index('layout_fspath'), __metadata("design:type", String) ], Layout.prototype, "fspath", void 0); __decorate([ field({ name: 'renderPath', dbtype: 'TEXT' }), index('layout_renderPath'), __metadata("design:type", String) ], Layout.prototype, "renderPath", void 0); __decorate([ field({ name: 'mtimeMs', dbtype: "TEXT DEFAULT(datetime('now') || 'Z')" }), __metadata("design:type", String) ], Layout.prototype, "mtimeMs", void 0); __decorate([ field({ name: 'docMetadata', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Layout.prototype, "docMetadata", void 0); __decorate([ field({ name: 'docContent', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Layout.prototype, "docContent", void 0); __decorate([ field({ name: 'docBody', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Layout.prototype, "docBody", void 0); __decorate([ field({ name: 'metadata', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Layout.prototype, "metadata", void 0); __decorate([ field({ name: 'info', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Layout.prototype, "info", void 0); Layout = __decorate([ table({ name: 'LAYOUTS', withoutRowId: true, }) ], Layout); export { Layout }; await schema().createTable(sqdb, 'LAYOUTS'); export const layoutsDAO = new BaseDAO(Layout, sqdb); await layoutsDAO.createIndex('layout_vpath'); await layoutsDAO.createIndex('layout_mounted'); await layoutsDAO.createIndex('layout_mountPoint'); await layoutsDAO.createIndex('layout_pathInMounted'); await layoutsDAO.createIndex('layout_fspath'); await layoutsDAO.createIndex('layout_renderPath'); /////////////// Documents Table let Document = class Document { }; __decorate([ id({ name: 'vpath', dbtype: 'TEXT' }), index('docs_vpath'), __metadata("design:type", String) ], Document.prototype, "vpath", void 0); __decorate([ field({ name: 'mime', dbtype: 'TEXT' }), __metadata("design:type", String) ], Document.prototype, "mime", void 0); __decorate([ field({ name: 'mounted', dbtype: 'TEXT' }), index('docs_mounted'), __metadata("design:type", String) ], Document.prototype, "mounted", void 0); __decorate([ field({ name: 'mountPoint', dbtype: 'TEXT' }), index('docs_mountPoint'), __metadata("design:type", String) ], Document.prototype, "mountPoint", void 0); __decorate([ field({ name: 'pathInMounted', dbtype: 'TEXT' }), index('docs_pathInMounted'), __metadata("design:type", String) ], Document.prototype, "pathInMounted", void 0); __decorate([ field({ name: 'fspath', dbtype: 'TEXT' }), index('docs_fspath'), __metadata("design:type", String) ], Document.prototype, "fspath", void 0); __decorate([ field({ name: 'renderPath', dbtype: 'TEXT' }), index('docs_renderPath'), __metadata("design:type", String) ], Document.prototype, "renderPath", void 0); __decorate([ field({ name: 'rendersToHTML', dbtype: 'INTEGER' }), index('docs_rendersToHTML'), __metadata("design:type", Boolean) ], Document.prototype, "rendersToHTML", void 0); __decorate([ field({ name: 'dirname', dbtype: 'TEXT' }), index('docs_dirname'), __metadata("design:type", String) ], Document.prototype, "dirname", void 0); __decorate([ field({ name: 'parentDir', dbtype: 'TEXT' }), index('docs_parentDir'), __metadata("design:type", String) ], Document.prototype, "parentDir", void 0); __decorate([ field({ name: 'mtimeMs', dbtype: "TEXT DEFAULT(datetime('now') || 'Z')" }), __metadata("design:type", String) ], Document.prototype, "mtimeMs", void 0); __decorate([ field({ name: 'docMetadata', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Document.prototype, "docMetadata", void 0); __decorate([ field({ name: 'docContent', dbtype: 'TEXT', isJson: false }), __metadata("design:type", String) ], Document.prototype, "docContent", void 0); __decorate([ field({ name: 'docBody', dbtype: 'TEXT', isJson: false }), __metadata("design:type", String) ], Document.prototype, "docBody", void 0); __decorate([ field({ name: 'metadata', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Document.prototype, "metadata", void 0); __decorate([ field({ name: 'tags', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Document.prototype, "tags", void 0); __decorate([ field({ name: 'layout', dbtype: 'TEXT', isJson: false }), index('docs_layout'), __metadata("design:type", String) ], Document.prototype, "layout", void 0); __decorate([ field({ name: 'blogtag', dbtype: 'TEXT', isJson: false }), index('docs_blogtag'), __metadata("design:type", String) ], Document.prototype, "blogtag", void 0); __decorate([ field({ name: 'info', dbtype: 'TEXT', isJson: true }), __metadata("design:type", Object) ], Document.prototype, "info", void 0); Document = __decorate([ table({ name: 'DOCUMENTS', withoutRowId: true, }) ], Document); export { Document }; await schema().createTable(sqdb, 'DOCUMENTS'); export const documentsDAO = new BaseDAO(Document, sqdb); await documentsDAO.createIndex('docs_vpath'); await documentsDAO.createIndex('docs_mounted'); await documentsDAO.createIndex('docs_mountPoint'); await documentsDAO.createIndex('docs_pathInMounted'); await documentsDAO.createIndex('docs_fspath'); await documentsDAO.createIndex('docs_renderPath'); await documentsDAO.createIndex('docs_rendersToHTML'); await documentsDAO.createIndex('docs_dirname'); await documentsDAO.createIndex('docs_parentDir'); await documentsDAO.createIndex('docs_blogtag'); let TagGlue = class TagGlue { }; __decorate([ field({ name: 'docvpath', dbtype: 'TEXT' }) // @fk('tag_docvpath', 'DOCUMENTS', 'vpath') , index('tagglue_vpath'), __metadata("design:type", String) ], TagGlue.prototype, "docvpath", void 0); __decorate([ field({ name: 'tagName', dbtype: 'TEXT' }) // @fk('tag_slug', 'TAGS', 'slug') , index('tagglue_name'), __metadata("design:type", String) ], TagGlue.prototype, "tagName", void 0); TagGlue = __decorate([ table({ name: 'TAGGLUE' }) ], TagGlue); await schema().createTable(sqdb, 'TAGGLUE'); export const tagGlueDAO = new BaseDAO(TagGlue, sqdb); await tagGlueDAO.createIndex('tagglue_vpath'); await tagGlueDAO.createIndex('tagglue_name'); // @table({ name: 'TAGS' }) // class Tag { // @field({ // name: 'tagname', // dbtype: 'TEXT' // }) // tagname: string; // @id({ // name: 'slug', dbtype: 'TEXT' // }) // @index('tag_slug') // slug: string; // @field({ // name: 'description', dbtype: 'TEXT' // }) // description?: string; // } // await schema().createTable(sqdb, 'TAGS'); // const tagsDAO = new BaseDAO<Tag>(Tag, sqdb); // Convert AkashaCMS mount points into the mountpoint // used by DirsWatcher const remapdirs = (dirz) => { return dirz.map(dir => { // console.log('document dir ', dir); if (typeof dir === 'string') { return { mounted: dir, mountPoint: '/', baseMetadata: {} }; } else { if (!dir.dest) { throw new Error(`remapdirs invalid mount specification ${util.inspect(dir)}`); } return { mounted: dir.src, mountPoint: dir.dest, baseMetadata: dir.baseMetadata, ignore: dir.ignore }; } }); }; export class BaseFileCache extends EventEmitter { /** * @param config AkashaRender Configuration object * @param dirs array of directories and mount points to watch * @param name string giving the name for this watcher name * @param dao The SQLITE3ORM DAO instance to use */ constructor(config, name, dirs, dao // BaseDAO<T> ) { super(); _BaseFileCache_instances.add(this); _BaseFileCache_config.set(this, void 0); _BaseFileCache_name.set(this, void 0); _BaseFileCache_dirs.set(this, void 0); _BaseFileCache_is_ready.set(this, false); _BaseFileCache_cache_content.set(this, void 0); _BaseFileCache_map_renderpath.set(this, void 0); _BaseFileCache_dao.set(this, void 0); // BaseDAO<T>; // SKIP: getDynamicView _BaseFileCache_watcher.set(this, void 0); _BaseFileCache_queue.set(this, void 0); // console.log(`BaseFileCache ${name} constructor dirs=${util.inspect(dirs)}`); __classPrivateFieldSet(this, _BaseFileCache_config, config, "f"); __classPrivateFieldSet(this, _BaseFileCache_name, name, "f"); __classPrivateFieldSet(this, _BaseFileCache_dirs, dirs, "f"); __classPrivateFieldSet(this, _BaseFileCache_is_ready, false, "f"); __classPrivateFieldSet(this, _BaseFileCache_cache_content, false, "f"); __classPrivateFieldSet(this, _BaseFileCache_map_renderpath, false, "f"); __classPrivateFieldSet(this, _BaseFileCache_dao, dao, "f"); } get config() { return __classPrivateFieldGet(this, _BaseFileCache_config, "f"); } get name() { return __classPrivateFieldGet(this, _BaseFileCache_name, "f"); } get dirs() { return __classPrivateFieldGet(this, _BaseFileCache_dirs, "f"); } set cacheContent(doit) { __classPrivateFieldSet(this, _BaseFileCache_cache_content, doit, "f"); } get gacheContent() { return __classPrivateFieldGet(this, _BaseFileCache_cache_content, "f"); } set mapRenderPath(doit) { __classPrivateFieldSet(this, _BaseFileCache_map_renderpath, doit, "f"); } get mapRenderPath() { return __classPrivateFieldGet(this, _BaseFileCache_map_renderpath, "f"); } get dao() { return __classPrivateFieldGet(this, _BaseFileCache_dao, "f"); } async close() { if (__classPrivateFieldGet(this, _BaseFileCache_queue, "f")) { __classPrivateFieldGet(this, _BaseFileCache_queue, "f").killAndDrain(); __classPrivateFieldSet(this, _BaseFileCache_queue, undefined, "f"); } if (__classPrivateFieldGet(this, _BaseFileCache_watcher, "f")) { // console.log(`CLOSING ${this.name}`); await __classPrivateFieldGet(this, _BaseFileCache_watcher, "f").close(); __classPrivateFieldSet(this, _BaseFileCache_watcher, undefined, "f"); } this.removeAllListeners('changed'); this.removeAllListeners('added'); this.removeAllListeners('unlinked'); this.removeAllListeners('ready'); await sqdb.close(); } /** * Set up receiving events from DirsWatcher, and dispatching to * the handler methods. */ async setup() { const fcache = this; if (__classPrivateFieldGet(this, _BaseFileCache_watcher, "f")) { await __classPrivateFieldGet(this, _BaseFileCache_watcher, "f").close(); } __classPrivateFieldSet(this, _BaseFileCache_queue, fastq.promise(async function (event) { if (event.code === 'changed') { try { // console.log(`change ${event.name} ${event.info.vpath}`); await fcache.handleChanged(event.name, event.info); fcache.emit('change', event.name, event.info); } catch (e) { fcache.emit('error', { code: event.code, name: event.name, vpath: event.info.vpath, error: e }); } } else if (event.code === 'added') { try { // console.log(`add ${event.name} ${event.info.vpath}`); await fcache.handleAdded(event.name, event.info); fcache.emit('add', event.name, event.info); } catch (e) { fcache.emit('error', { code: event.code, name: event.name, vpath: event.info.vpath, error: e }); } } else if (event.code === 'unlinked') { try { // console.log(`unlink ${event.name} ${event.info.vpath}`, event.info); await fcache.handleUnlinked(event.name, event.info); fcache.emit('unlink', event.name, event.info); } catch (e) { fcache.emit('error', { code: event.code, name: event.name, vpath: event.info.vpath, error: e }); } /* } else if (event.code === 'error') { await fcache.handleError(event.name) */ } else if (event.code === 'ready') { await fcache.handleReady(event.name); fcache.emit('ready', event.name); } }, 10), "f"); __classPrivateFieldSet(this, _BaseFileCache_watcher, new DirsWatcher(this.name), "f"); __classPrivateFieldGet(this, _BaseFileCache_watcher, "f").on('change', async (name, info) => { // console.log(`${name} changed ${info.mountPoint} ${info.vpath}`); try { if (!this.ignoreFile(info)) { // console.log(`PUSH ${name} changed ${info.mountPoint} ${info.vpath}`); __classPrivateFieldGet(this, _BaseFileCache_queue, "f").push({ code: 'changed', name, info }); } else { console.log(`Ignored 'change' for ${info.vpath}`); } } catch (err) { console.error(`FAIL change ${info.vpath} because ${err.stack}`); } }) .on('add', async (name, info) => { try { // console.log(`${name} add ${info.mountPoint} ${info.vpath}`); if (!this.ignoreFile(info)) { // console.log(`PUSH ${name} add ${info.mountPoint} ${info.vpath}`); __classPrivateFieldGet(this, _BaseFileCache_queue, "f").push({ code: 'added', name, info }); } else { console.log(`Ignored 'add' for ${info.vpath}`); } } catch (err) { console.error(`FAIL add ${info.vpath} because ${err.stack}`); } }) .on('unlink', async (name, info) => { // console.log(`unlink ${name} ${info.vpath}`); try { if (!this.ignoreFile(info)) { __classPrivateFieldGet(this, _BaseFileCache_queue, "f").push({ code: 'unlinked', name, info }); } else { console.log(`Ignored 'unlink' for ${info.vpath}`); } } catch (err) { console.error(`FAIL unlink ${info.vpath} because ${err.stack}`); } }) .on('ready', async (name) => { // console.log(`${name} ready`); __classPrivateFieldGet(this, _BaseFileCache_queue, "f").push({ code: 'ready', name }); }); const mapped = remapdirs(this.dirs); // console.log(`setup ${this.#name} watch ${util.inspect(this.#dirs)} ==> ${util.inspect(mapped)}`); await __classPrivateFieldGet(this, _BaseFileCache_watcher, "f").watch(mapped); // console.log(`DAO ${this.dao.table.name} ${util.inspect(this.dao.table.fields)}`); } gatherInfoData(info) { // Placeholder which some subclasses // are expected to override info.renderPath = info.vpath; } async handleChanged(name, info) { // console.log(`PROCESS ${name} handleChanged`, info.vpath); if (this.ignoreFile(info)) { // console.log(`OOOOOOOOGA!!! Received a file that should be ingored `, info); return; } if (name !== this.name) { throw new Error(`handleChanged event for wrong name; got ${name}, expected ${this.name}`); } // console.log(`handleChanged ${info.vpath} ${info.metadata && info.metadata.publicationDate ? info.metadata.publicationDate : '???'}`); this.gatherInfoData(info); info.stack = undefined; const result = await this.dao.selectAll({ vpath: { eq: info.vpath }, mounted: { eq: info.mounted } }); if (!Array.isArray(result) || result.length <= 0) { // It wasn't found in the database. Hence // we should add it. return this.handleAdded(name, info); } info.stack = undefined; await this.updateDocInDB(info); await this.config.hookFileChanged(name, info); } async updateDocInDB(info) { await __classPrivateFieldGet(this, _BaseFileCache_dao, "f").update({ vpath: info.vpath, mime: info.mime, mounted: info.mounted, mountPoint: info.mountPoint, pathInMounted: info.pathInMounted, mtimeMs: new Date(info.statsMtime).toISOString(), fspath: path.join(info.mounted, info.pathInMounted), renderPath: info.renderPath, rendersToHTML: info.rendersToHTML, dirname: path.dirname(info.renderPath), docMetadata: info.docMetadata, // docContent: info.docContent, // docBody: info.docBody, metadata: info.metadata, info, }); } /** * We receive this: * * { * fspath: fspath, * vpath: vpath, * mime: mime.getType(fspath), * mounted: dir.mounted, * mountPoint: dir.mountPoint, * pathInMounted: computed relative path * stack: [ array of these instances ] * } * * Need to add: * renderPath * And for HTML render files, add the baseMetadata and docMetadata * * Should remove the stack, since it's likely not useful to us. */ async handleAdded(name, info) { // console.log(`PROCESS ${name} handleAdded`, info.vpath); if (this.ignoreFile(info)) { // console.log(`OOOOOOOOGA!!! Received a file that should be ingored `, info); return; } if (name !== this.name) { throw new Error(`handleAdded event for wrong name; got ${name}, expected ${this.name}`); } this.gatherInfoData(info); info.stack = undefined; await this.insertDocToDB(info); await this.config.hookFileAdded(name, info); } async insertDocToDB(info) { await __classPrivateFieldGet(this, _BaseFileCache_dao, "f").insert({ vpath: info.vpath, mime: info.mime, mounted: info.mounted, mountPoint: info.mountPoint, pathInMounted: info.pathInMounted, mtimeMs: new Date(info.statsMtime).toISOString(), fspath: path.join(info.mounted, info.pathInMounted), renderPath: info.renderPath, rendersToHTML: info.rendersToHTML, dirname: path.dirname(info.renderPath), docMetadata: info.docMetadata, // docContent: info.docContent, // docBody: info.docBody, metadata: info.metadata, info, }); } async handleUnlinked(name, info) { // console.log(`PROCESS ${name} handleUnlinked`, info.vpath); if (name !== this.name) { throw new Error(`handleUnlinked event for wrong name; got ${name}, expected ${this.name}`); } await this.config.hookFileUnlinked(name, info); await __classPrivateFieldGet(this, _BaseFileCache_dao, "f").deleteAll({ vpath: { eq: info.vpath }, mounted: { eq: info.mounted } }); } async handleReady(name) { // console.log(`PROCESS ${name} handleReady`); if (name !== this.name) { throw new Error(`handleReady event for wrong name; got ${name}, expected ${this.name}`); } __classPrivateFieldSet(this, _BaseFileCache_is_ready, true, "f"); this.emit('ready', name); } /** * Find the directory mount corresponding to the file. * * @param {*} info * @returns */ fileDirMount(info) { const mapped = remapdirs(this.dirs); for (const dir of mapped) { // console.log(`dirMount for ${info.vpath} -- ${util.inspect(info)} === ${util.inspect(dir)}`); if (info.mountPoint === dir.mountPoint) { return dir; } } return undefined; } /** * Should this file be ignored, based on the `ignore` field * in the matching `dir` mount entry. * * @param {*} info * @returns */ ignoreFile(info) { // console.log(`ignoreFile ${info.vpath}`); const dirMount = this.fileDirMount(info); // console.log(`ignoreFile ${info.vpath} dirMount ${util.inspect(dirMount)}`); let ignore = false; if (dirMount) { let ignores; if (typeof dirMount.ignore === 'string') { ignores = [dirMount.ignore]; } else if (Array.isArray(dirMount.ignore)) { ignores = dirMount.ignore; } else { ignores = []; } for (const i of ignores) { if (micromatch.isMatch(info.vpath, i)) ignore = true; // console.log(`dirMount.ignore ${fspath} ${i} => ${ignore}`); } // if (ignore) console.log(`MUST ignore File ${info.vpath}`); // console.log(`ignoreFile for ${info.vpath} ==> ${ignore}`); return ignore; } else { // no mount? that means something strange console.error(`No dirMount found for ${info.vpath} / ${info.dirMountedOn}`); return true; } } /** * Allow a caller to wait until the <em>ready</em> event has * been sent from the DirsWatcher instance. This event means the * initial indexing has happened. */ async isReady() { // If there's no directories, there won't be any files // to load, and no need to wait while (__classPrivateFieldGet(this, _BaseFileCache_dirs, "f").length > 0 && !__classPrivateFieldGet(this, _BaseFileCache_is_ready, "f")) { // This does a 100ms pause // That lets us check is_ready every 100ms // at very little cost // console.log(`!isReady ${this.name} ${this[_symb_dirs].length} ${this[_symb_is_ready]}`); await new Promise((resolve, reject) => { setTimeout(() => { resolve(undefined); }, 100); }); } return true; } async paths(rootPath) { const fcache = this; let rootP = rootPath?.startsWith('/') ? rootPath?.substring(1) : rootPath; // This is copied from the older version // (LokiJS version) of this function. It // seems meant to eliminate duplicates. const vpathsSeen = new Set(); const selector = { order: { mtimeMs: true } }; if (typeof rootP === 'string' && rootP.length >= 1) { selector.renderPath = { isLike: `${rootP}%` // sql: ` renderPath regexp '^${rootP}' ` }; } // console.log(`paths ${util.inspect(selector)}`); const result = await this.dao.selectAll(selector); const result2 = result.filter(item => { // console.log(`paths ?ignore? ${item.vpath}`); if (fcache.ignoreFile(item)) { return false; } if (vpathsSeen.has(item.vpath)) { return false; } else { vpathsSeen.add(item.vpath); return true; } }); // const result3 = result2.sort((a, b) => { // // We need these to be one of the concrete // // types so that the mtimeMs field is // // recognized by TypeScript. The Asset // // class is a good substitute for the base // // class of cached files. // const aa = <Asset>a; // const bb = <Asset>b; // if (aa.mtimeMs < bb.mtimeMs) return 1; // if (aa.mtimeMs === bb.mtimeMs) return 0; // if (aa.mtimeMs > bb.mtimeMs) return -1; // }); // This stage converts the items // received by this function into // what is required from // the paths method. // const result4 // = new Array<PathsReturnType>(); // for (const item of result3) { // result4.push(<PathsReturnType>{ // vpath: item.vpath, // mime: item.mime, // mounted: item.mounted, // mountPoint: item.mountPoint, // pathInMounted: item.pathInMounted, // mtimeMs: item.mtimeMs, // info: item.info, // fspath: path.join(item.mounted, item.pathInMounted), // renderPath: item.vpath // }); // } // console.log(result2/*.map(item => { // return { // vpath: item.vpath, // mtimeMs: item.mtimeMs // }; // }) */); return result2; } /** * Find the file within the cache. * * @param _fpath The vpath or renderPath to look for * @returns boolean true if found, false otherwise */ async find(_fpath) { if (typeof _fpath !== 'string') { throw new Error(`find parameter not string ${typeof _fpath}`); } const fpath = _fpath.startsWith('/') ? _fpath.substring(1) : _fpath; const fcache = this; const result1 = await this.dao.selectAll({ or: [ { vpath: { eq: fpath } }, { renderPath: { eq: fpath } } ] }); // console.log(`find ${_fpath} ${fpath} ==> result1 ${util.inspect(result1)} `); const result2 = result1.filter(item => { return !(fcache.ignoreFile(item)); }); // console.log(`find ${_fpath} ${fpath} ==> result2 ${util.inspect(result2)} `); let ret; if (Array.isArray(result2) && result2.length > 0) { ret = result2[0]; } else if (Array.isArray(result2) && result2.length <= 0) { ret = undefined; } else { ret = result2; } return ret; } /** * Fulfills the "find" operation not by * looking in the database, but by scanning * the filesystem using synchronous calls. * * @param _fpath * @returns */ findSync(_fpath) { if (typeof _fpath !== 'string') { throw new Error(`find parameter not string ${typeof _fpath}`); } const fpath = _fpath.startsWith('/') ? _fpath.substring(1) : _fpath; const fcache = this; const mapped = remapdirs(this.dirs); // console.log(`findSync looking for ${fpath} in ${util.inspect(mapped)}`); for (const dir of mapped) { if (!(dir?.mountPoint)) { console.warn(`findSync bad dirs in ${util.inspect(this.dirs)}`); } const found = __classPrivateFieldGet(this, _BaseFileCache_instances, "m", _BaseFileCache_fExistsInDir).call(this, fpath, dir); if (found) { // console.log(`findSync ${fpath} found`, found); return found; } } return undefined; } async findAll() { const fcache = this; const result1 = await this.dao.selectAll({}); const result2 = result1.filter(item => { // console.log(`findAll ?ignore? ${item.vpath}`); return !(fcache.ignoreFile(item)); }); return result2; } } _BaseFileCache_config = new WeakMap(), _BaseFileCache_name = new WeakMap(), _BaseFileCache_dirs = new WeakMap(), _BaseFileCache_is_ready = new WeakMap(), _BaseFileCache_cache_content = new WeakMap(), _BaseFileCache_map_renderpath = new WeakMap(), _BaseFileCache_dao = new WeakMap(), _BaseFileCache_watcher = new WeakMap(), _BaseFileCache_queue = new WeakMap(), _BaseFileCache_instances = new WeakSet(), _BaseFileCache_fExistsInDir = function _BaseFileCache_fExistsInDir(fpath, dir) { // console.log(`#fExistsInDir ${fpath} ${util.inspect(dir)}`); if (dir.mountPoint === '/') { const fspath = path.join(dir.mounted, fpath); let fsexists = FS.existsSync(fspath); if (fsexists) { let stats = FS.statSync(fspath); return { vpath: fpath, renderPath: fpath, fspath: fspath, mime: undefined, mounted: dir.mounted, mountPoint: dir.mountPoint, pathInMounted: fpath, statsMtime: stats.mtimeMs }; } else { return undefined; } } let mp = dir.mountPoint.startsWith('/') ? dir.mountPoint.substring(1) : dir.mountPoint; mp = mp.endsWith('/') ? mp : (mp + '/'); if (fpath.startsWith(mp)) { let pathInMounted = fpath.replace(dir.mountPoint, ''); let fspath = path.join(dir.mounted, pathInMounted); // console.log(`Checking exist for ${dir.mountPoint} ${dir.mounted} ${pathInMounted} ${fspath}`); let fsexists = FS.existsSync(fspath); if (fsexists) { let stats = FS.statSync(fspath); return { vpath: fpath, renderPath: fpath, fspath: fspath, mime: undefined, mounted: dir.mounted, mountPoint: dir.mountPoint, pathInMounted: pathInMounted, statsMtime: stats.mtimeMs }; } } return undefined; }; export class TemplatesFileCache extends BaseFileCache { constructor(config, name, dirs, dao) { super(config, name, dirs, dao); } /** * Gather the additional data suitable * for Partial and Layout templates. The * full data set required for Documents is * not suitable for the templates. * * @param info */ gatherInfoData(info) { info.renderPath = info.vpath; info.dirname = path.dirname(info.vpath); if (info.dirname === '.') info.dirname = '/'; let renderer = this.config.findRendererPath(info.vpath); info.renderer = renderer; if (renderer) { if (renderer.parseMetadata) { // Using <any> here covers over // that parseMetadata requires // a RenderingContext which // in turn requires a // metadata object. const rc = renderer.parseMetadata({ fspath: info.fspath, content: FS.readFileSync(info.fspath, 'utf-8') }); // docMetadata is the unmodified metadata/frontmatter // in the document info.docMetadata = rc.metadata; // docContent is the unparsed original content // including any frontmatter info.docContent = rc.content; // docBody is the parsed body -- e.g. following the frontmatter info.docBody = rc.body; // This is the computed metadata that includes data from // several sources info.metadata = {}; if (!info.docMetadata) info.docMetadata = {}; for (let yprop in info.baseMetadata) { // console.log(`initMetadata ${basedir} ${fpath} baseMetadata ${baseMetadata[yprop]}`); info.metadata[yprop] = info.baseMetadata[yprop]; } } } // console.log(`TemplatesFileCache after gatherInfoData `, info); } async updateDocInDB(info) { await this.dao.update({ vpath: info.vpath, mime: info.mime, mounted: info.mounted, mountPoint: info.mountPoint, pathInMounted: info.pathInMounted, mtimeMs: new Date(info.statsMtime).toISOString(), fspath: path.join(info.mounted, info.pathInMounted), renderPath: info.renderPath, rendersToHTML: info.rendersToHTML, dirname: path.dirname(info.renderPath), docMetadata: info.docMetadata, docContent: info.docContent, docBody: info.docBody, metadata: info.metadata, info, }); } async insertDocToDB(info) { await this.dao.insert({ vpath: info.vpath, mime: info.mime, mounted: info.mounted, mountPoint: info.mountPoint, pathInMounted: info.pathInMounted, mtimeMs: new Date(info.statsMtime).toISOString(), fspath: path.join(info.mounted, info.pathInMounted), renderPath: info.renderPath, rendersToHTML: info.rendersToHTML, dirname: path.dirname(info.renderPath), docMetadata: info.docMetadata, docContent: info.docContent, docBody: info.docBody, metadata: info.metadata, info, }); } } export class DocumentsFileCache extends BaseFileCache { constructor(config, name, dirs) { super(config, name, dirs, documentsDAO); } gatherInfoData(info) { info.renderPath = info.vpath; info.dirname = path.dirname(info.vpath); if (info.dirname === '.') info.dirname = '/'; info.parentDir = path.dirname(info.dirname); // find the mounted directory, // get the baseMetadata for (let dir of remapdirs(this.dirs)) { if (dir.mounted === info.mounted) { if (dir.baseMetadata) { info.baseMetadata = dir.baseMetadata; } break; } } // set publicationDate somehow let renderer = this.config.findRendererPath(info.vpath); info.renderer = renderer; if (renderer) { info.renderPath = renderer.filePath(info.vpath); // This was in the LokiJS code, but // was not in use. // info.rendername = path.basename( // info.renderPath // ); info.rendersToHTML = micromatch.isMatch(info.renderPath, '**/*.html') ? true : false; if (renderer.parseMetadata) { // Using <any> here covers over // that parseMetadata requires // a RenderingContext which // in turn requires a // metadata object. const rc = renderer.parseMetadata({ fspath: info.fspath, content: FS.readFileSync(info.fspath, 'utf-8') }); // docMetadata is the unmodified metadata/frontmatter // in the document info.docMetadata = rc.metadata; // docContent is the unparsed original content // including any frontmatter info.docContent = rc.content; // docBody is the parsed body -- e.g. following the frontmatter info.docBody = rc.body; // This is the computed metadata that includes data from // several sources info.metadata = {}; if (!info.docMetadata) info.docMetadata = {}; // The rest of this is adapted from the old function // HTMLRenderer.newInitMetadata // For starters the metadata is collected from several sources. // 1) the metadata specified in the directory mount where // this document was found // 2) metadata in the project configuration // 3) the metadata in the document, as captured in docMetadata for (let yprop in info.baseMetadata) { // console.log(`initMetadata ${basedir} ${fpath} baseMetadata ${baseMetadata[yprop]}`); info.metadata[yprop] = info.baseMetadata[yprop]; } for (let yprop in this.config.metadata) { info.metadata[yprop] = this.config.metadata[yprop]; } let fmmcount = 0; for (let yprop in info.docMetadata) { info.metadata[yprop] = info.docMetadata[yprop]; fmmcount++; } // The rendered version of the content lands here info.metadata.content = ""; // The document object has been useful for // communicating the file path and other data. info.metadata.document = {}; info.metadata.document.basedir = info.mountPoint; info.metadata.document.relpath = info.pathInMounted; info.metadata.document.relrender = renderer.filePath(info.pathInMounted); info.metadata.document.path = info.vpath; info.metadata.document.renderTo = info.renderPath; // Ensure the <em>tags</em> field is an array if (!(info.metadata.tags)) { info.metadata.tags = []; } else if (typeof (info.metadata.tags) === 'string') { let taglist = []; const re = /\s*,\s*/; info.metadata.tags.split(re).forEach(tag => { taglist.push(tag.trim()); }); info.metadata.tags = taglist; } else if (!Array.isArray(info.metadata.tags)) { throw new Error(`FORMAT ERROR - ${info.vpath} has badly formatted tags `, info.metadata.tags); } info.docMetadata.tags = info.metadata.tags; // The root URL for the project info.metadata.root_url = this.config.root_url; // Compute the URL this document will render to if (this.config.root_url) { let uRootUrl = new URL(this.config.root_url, 'http://example.com'); uRootUrl.pathname = path.normalize(path.join(uRootUrl.pathname, info.metadata.document.renderTo)); info.metadata.rendered_url = uRootUrl.toString(); } else { info.metadata.rendered_url = info.metadata.document.renderTo; } // info.metadata.rendered_date = info.stats.mtime; const parsePublDate = (date) => { const parsed = Date.parse(date); if (!isNaN(parsed)) { info.metadata.publicationDate = new Date(parsed); info.publicationDate = info.metadata.publicationDate; info.publicationTime = info.publicationDate.getTime(); } }; if (info.docMetadata && typeof info.docMetadata.publDate