solidity-docgen
Version:
Documentation generator for Solidity smart contracts.
117 lines (101 loc) • 4.12 kB
text/typescript
import path from 'path';
import fs, { promises as fsPromise } from 'fs';
import Handlebars, { RuntimeOptions } from 'handlebars';
import { findIn } from './utils/fs-exists';
import { mapValues } from './utils/map-values';
import { mapKeys } from './utils/map-keys';
import { DocItemContext } from './site';
import * as defaultProperties from './common/properties';
export type PropertyGetter = (ctx: DocItemContext, original?: unknown) => unknown;
export type Properties = Record<string, PropertyGetter>;
export interface Templates {
partials?: Record<string, () => string>;
helpers?: Record<string, (...args: unknown[]) => string>;
properties?: Record<string, PropertyGetter>;
}
/**
* Loads the templates that will be used for rendering a site based on a
* default theme and user templates.
*
* The result contains all partials, helpers, and property getters defined in
* the user templates and the default theme, where the user's take precedence
* if there is a clash. Additionally, all theme partials and helpers are
* included with the theme prefix, e.g. `markdown/contract` will be a partial.
*/
export async function loadTemplates(defaultTheme: string, root: string, userTemplatesPath?: string): Promise<Templates> {
const themes = await readThemes();
// Initialize templates with the default theme.
const templates: Required<Templates> = {
partials: { ...themes[defaultTheme]?.partials },
helpers: { ...themes[defaultTheme]?.helpers },
properties: { ...defaultProperties },
};
// Overwrite default theme with user templates.
if (userTemplatesPath) {
const userTemplates = await readTemplates(path.resolve(root, userTemplatesPath));
Object.assign(templates.partials, userTemplates.partials);
Object.assign(templates.helpers, userTemplates.helpers);
Object.assign(templates.properties, userTemplates.properties);
}
// Add partials and helpers from all themes, prefixed with the theme name.
for (const [themeName, theme] of Object.entries(themes)) {
const addPrefix = (k: string) => `${themeName}/${k}`;
Object.assign(templates.partials, mapKeys(theme.partials, addPrefix));
Object.assign(templates.helpers, mapKeys(theme.helpers, addPrefix));
}
return templates;
}
/**
* Read templates and helpers from a directory.
*/
export async function readTemplates(partialsDir: string, helpersDir = partialsDir): Promise<Required<Templates>> {
return {
partials: await readPartials(partialsDir),
helpers: await readHelpers(helpersDir, 'helpers'),
properties: await readHelpers(helpersDir, 'properties'),
};
}
async function readPartials(dir: string) {
const partials: NonNullable<Templates['partials']> = {};
for (const p of await fsPromise.readdir(dir)) {
const { name, ext } = path.parse(p);
if (ext === '.hbs') {
partials[name] = () => fs.readFileSync(path.join(dir, p), 'utf8');
}
}
return partials;
}
async function readHelpers(dir: string, name: string) {
let helpersPath;
try {
helpersPath = require.resolve(path.join(dir, name));
} catch {
return {};
}
const h = await import(helpersPath);
const helpers: Record<string, (...args: any[]) => any> = {};
for (const name in h) {
if (typeof h[name] === 'function') {
helpers[name] = h[name];
}
}
return helpers;
}
/**
* Reads all built-in themes into an object. Partials will always be found in
* src/themes, whereas helpers may instead be found in dist/themes if TypeScript
* can't be imported directly.
*/
async function readThemes(): Promise<Record<string, Required<Templates>>> {
const themes: Record<string, Required<Templates>> = {};
// Handlebars partials are located in src and not in dist
const srcThemes = path.resolve(__dirname, '../src/themes');
const distThemes = path.resolve(__dirname, 'themes');
for (const theme of await fsPromise.readdir(srcThemes, { withFileTypes: true })) {
if (theme.isDirectory()) {
const { name } = theme;
themes[name] = await readTemplates(path.join(srcThemes, name), path.join(distThemes, name));
}
}
return themes;
}