documentation-hipster
Version:
documentation.js Bootstrap Theme with TypeScript and React support
214 lines (187 loc) • 7.63 kB
JavaScript
const fs = require('fs');
const path = require('path');
const process = require('process');
const File = require('vinyl');
const vfs = require('vinyl-fs');
const concat = require('concat-stream');
const map = require('map-stream');
const slugger = new require('github-slugger')();
const Handlebars = require('handlebars');
const index = Handlebars.compile(fs.readFileSync(path.join(__dirname, 'hbs', 'index.handlebars'), 'utf8'), { preventIndent: true });
for (const templ of fs.readdirSync(path.join(__dirname, 'hbs')).filter(f => f.match(/\.handlebars/))) {
const partial = Handlebars.compile(fs.readFileSync(path.join(__dirname, 'hbs', templ), 'utf8'), { preventIndent: true });
Handlebars.registerPartial(templ.split('.')[0], partial);
}
Handlebars.registerHelper('switch', function (value, options) {
this.switch_value = value;
this.switch_break = false;
return options.fn(this);
});
Handlebars.registerHelper('case', function (value, options) {
if (value == this.switch_value) {
this.switch_break = true;
return options.fn(this);
}
});
Handlebars.registerHelper('default', function (options) {
if (this.switch_break == false) {
return options.fn(this);
}
});
Handlebars.registerHelper('hasChildren', function (value, options) {
const children = Object.keys(value).reduce((a, m) => a + value[m].length, 0);
if (children > 0) {
return options.fn(this);
} else {
return options.inverse(this);
}
});
const defaultClasses = {
container: 'container d-flex flex-row',
nav: 'position-sticky nav-section list px-2',
main: 'main-section ps-3',
title: 'mt-2 mb-3 me-2',
examples: 'ms-4',
mainItem: 'me-1',
navItem: 'd-flex flex-row align-items-center',
navCollapse: 'btn btn-collapse m-0 me-2 p-0 align-items-center',
navList: 'list-group',
navListItem: 'list-group-item border-0',
navLink: 'text-decoration-none',
navText: 'm-0',
paramsTable: 'table table-light',
paramsParameterHeader: 'col-2',
paramsTypeHeader: 'col-4',
paramsDescriptionHeader: 'col-6',
paramsParameterData: '',
paramsTypeData: '',
paramsDescriptionData: '',
returns: 'me-1',
source: 'ms-auto fs-6 fw-lighter'
};
let userClasses = {};
function getClass(value) {
if (userClasses[value] !== undefined)
return userClasses[value];
return defaultClasses[value];
}
Handlebars.registerHelper('className', getClass);
const crossLinks = {};
let externalCrossLinks = () => undefined;
let crossLinksDupeWarning = true;
Handlebars.registerHelper('crossLink', function (value) {
const ext = externalCrossLinks(value);
if (ext)
return `<a href="${ext}">${value}</a>`;
if (crossLinks[value])
return `<a href="#${crossLinks[value]}">${value}</a>`;
return value;
});
Handlebars.registerHelper('crossLinkUrl', function (value) {
const ext = externalCrossLinks(value);
if (ext)
return ext;
if (crossLinks[value])
return '#' + crossLinks[value];
return value;
});
let srcLinkBase;
Handlebars.registerHelper('srcLink', function (value) {
if (srcLinkBase && value) {
const rel = path.relative(process.cwd(), value.file);
const line = (value.loc && value.loc.start && value.loc.start.line !== undefined) ? '#L' + value.loc.start.line : '';
return `<a class="${getClass('source')}" href="${srcLinkBase}${rel}${line}">${rel}</a>`;
}
});
Handlebars.registerHelper('debug', function (value) {
return JSON.stringify(value);
});
function slugify(block) {
block.slug = slugger.slug(block.name);
Object.keys(block.members).forEach((m) => block.members[m].forEach(slugify));
}
// Finds interfaces with React properties and injects them into their components
function propsify(props, block) {
const idx = props.findIndex((p) => block.name === p.tags.find((t) => t.title === 'propsfor').description);
if (idx >= 0) {
const p = props[idx];
props.splice(idx, 1);
block.props = p;
}
Object.keys(block.members).forEach((m) => block.members[m].forEach(propsify.bind(null, props)));
}
function crossify(list, block) {
if (list[block.name] === undefined) {
list[block.name] = block.slug;
} else {
if (crossLinksDupeWarning)
console.warn('Warning, duplicate names, disabling cross-links: ', block.name);
list[block.name] = null;
}
Object.keys(block.members).forEach((m) => block.members[m].forEach(crossify.bind(null, list)));
}
module.exports = function documentationHipster(comments, config) {
const themeConfig = config['documentation-hipster'] = config['documentation-hipster'] || {};
if (themeConfig.externalCrossLinks)
externalCrossLinks = require(path.resolve(process.cwd(), themeConfig.externalCrossLinks));
if (themeConfig.dumpAST)
fs.writeFileSync(themeConfig.dumpAST, JSON.stringify(comments));
crossLinksDupeWarning = themeConfig.crossLinksDupeWarning;
srcLinkBase = themeConfig.srcLinkBase;
userClasses = themeConfig.classes || {};
comments.forEach(slugify);
// find static members whose unknown tags list contains propsfor
const props = comments.filter((m) => (m.tags || []).map((t) => t.title).includes('propsfor'));
// remove them from the normal flow
comments = comments.filter((m) => !(m.tags || []).map((t) => t.title).includes('propsfor'));
// and attach them in their respective components
comments.forEach(propsify.bind(null, props));
// create automatic cross-links
comments.forEach(crossify.bind(null, crossLinks));
themeConfig.css = themeConfig.css ? path.join(process.cwd(), themeConfig.css) : path.join(__dirname, 'hipster.css')
const assets = [
require.resolve('bootstrap/dist/js/bootstrap.bundle.min.js'),
require.resolve('bootstrap/dist/css/bootstrap.min.css'),
themeConfig.css
];
themeConfig.css = path.basename(themeConfig.css);
if (themeConfig.extraCss) {
assets.push(path.join(process.cwd(), themeConfig.extraCss));
themeConfig.extraCss = path.basename(themeConfig.extraCss);
}
const generated = index({ config, comments });
if (typeof process.mainModule === 'undefined') {
// documentation.js >= 14
if (!config.output) {
return Promise.resolve(generated);
}
return fs.promises.mkdir(config.output, { recursive: true })
.then(() => Promise.all(
assets.map((asset) =>
fs.promises.copyFile(asset, path.join(config.output, path.basename(asset))))))
.then(() => fs.promises.writeFile(path.join(config.output, 'index.html'), generated, 'utf8'));
} else {
// documentation.js <= 13.x
// push assets into the pipeline as well.
return new Promise((resolve) => {
vfs.src(assets, { base: __dirname })
.pipe(map((file, cb) => {
file.base = path.dirname(file.path);
cb(null, file);
}))
.pipe(
concat(function (files) {
resolve(
files.concat(
new File({
path: 'index.html',
contents: Buffer.from(generated,
'utf8')
})
)
);
})
);
});
}
}