godot/platform/web/js/jsdoc2rst/publish.js

/* eslint-disable strict */

'use strict';

const fs = require('fs');

class JSDoclet {
	constructor(doc) {
		this.doc = doc;
		this.description = doc['description'] || '';
		this.name = doc['name'] || 'unknown';
		this.longname = doc['longname'] || '';
		this.types = [];
		if (doc['type'] && doc['type']['names']) {
			this.types = doc['type']['names'].slice();
		}
		this.type = this.types.length > 0 ? this.types.join('\\|') : '*';
		this.variable = doc['variable'] || false;
		this.kind = doc['kind'] || '';
		this.memberof = doc['memberof'] || null;
		this.scope = doc['scope'] || '';
		this.members = [];
		this.optional = doc['optional'] || false;
		this.defaultvalue = doc['defaultvalue'];
		this.summary = doc['summary'] || null;
		this.classdesc = doc['classdesc'] || null;

		// Parameters (functions)
		this.params = [];
		this.returns = doc['returns'] ? doc['returns'][0]['type']['names'][0] : 'void';
		this.returns_desc = doc['returns'] ? doc['returns'][0]['description'] : null;

		this.params = (doc['params'] || []).slice().map((p) => new JSDoclet(p));

		// Custom tags
		this.tags = doc['tags'] || [];
		this.header = this.tags.filter((t) => t['title'] === 'header').map((t) => t['text']).pop() || null;
	}

	add_member(obj) {
		this.members.push(obj);
	}

	is_static() {
		return this.scope === 'static';
	}

	is_instance() {
		return this.scope === 'instance';
	}

	is_object() {
		return this.kind === 'Object' || (this.kind === 'typedef' && this.type === 'Object');
	}

	is_class() {
		return this.kind === 'class';
	}

	is_function() {
		return this.kind === 'function' || (this.kind === 'typedef' && this.type === 'function');
	}

	is_module() {
		return this.kind === 'module';
	}
}

function format_table(f, data, depth = 0) {
	if (!data.length) {
		return;
	}

	const column_sizes = new Array(data[0].length).fill(0);

	data.forEach((row) => {
		row.forEach((e, idx) => {
			column_sizes[idx] = Math.max(e.length, column_sizes[idx]);
		});
	});

	const indent = ' '.repeat(depth);
	let sep = indent;
	column_sizes.forEach((size) => {
		sep += '+';
		sep += '-'.repeat(size + 2);
	});
	sep += '+\n';
	f.write(sep);

	data.forEach((row) => {
		let row_text = `${indent}|`;
		row.forEach((entry, idx) => {
			row_text += ` ${entry.padEnd(column_sizes[idx])} |`;
		});
		row_text += '\n';
		f.write(row_text);
		f.write(sep);
	});

	f.write('\n');
}

function make_header(header, sep) {
	return `${header}\n${sep.repeat(header.length)}\n\n`;
}

function indent_multiline(text, depth) {
	const indent = ' '.repeat(depth);
	return text.split('\n').map((l) => (l === '' ? l : indent + l)).join('\n');
}

function make_rst_signature(obj, types = false, style = false) {
	let out = '';
	const fmt = style ? '*' : '';
	obj.params.forEach((arg, idx) => {
		if (idx > 0) {
			if (arg.optional) {
				out += ` ${fmt}[`;
			}
			out += ', ';
		} else {
			out += ' ';
			if (arg.optional) {
				out += `${fmt}[ `;
			}
		}
		if (types) {
			out += `${arg.type} `;
		}
		const variable = arg.variable ? '...' : '';
		const defval = arg.defaultvalue !== undefined ? `=${arg.defaultvalue}` : '';
		out += `${variable}${arg.name}${defval}`;
		if (arg.optional) {
			out += ` ]${fmt}`;
		}
	});
	out += ' ';
	return out;
}

function make_rst_param(f, obj, depth = 0) {
	const indent = ' '.repeat(depth * 3);
	f.write(indent);
	f.write(`:param ${obj.type} ${obj.name}:\n`);
	f.write(indent_multiline(obj.description, (depth + 1) * 3));
	f.write('\n\n');
}

function make_rst_attribute(f, obj, depth = 0, brief = false) {
	const indent = ' '.repeat(depth * 3);
	f.write(indent);
	f.write(`.. js:attribute:: ${obj.name}\n\n`);

	if (brief) {
		if (obj.summary) {
			f.write(indent_multiline(obj.summary, (depth + 1) * 3));
		}
		f.write('\n\n');
		return;
	}

	f.write(indent_multiline(obj.description, (depth + 1) * 3));
	f.write('\n\n');

	f.write(indent);
	f.write(`   :type: ${obj.type}\n\n`);

	if (obj.defaultvalue !== undefined) {
		let defval = obj.defaultvalue;
		if (defval === '') {
			defval = '""';
		}
		f.write(indent);
		f.write(`   :value: \`\`${defval}\`\`\n\n`);
	}
}

function make_rst_function(f, obj, depth = 0) {
	let prefix = '';
	if (obj.is_instance()) {
		prefix = 'prototype.';
	}

	const indent = ' '.repeat(depth * 3);
	const sig = make_rst_signature(obj);
	f.write(indent);
	f.write(`.. js:function:: ${prefix}${obj.name}(${sig})\n`);
	f.write('\n');

	f.write(indent_multiline(obj.description, (depth + 1) * 3));
	f.write('\n\n');

	obj.params.forEach((param) => {
		make_rst_param(f, param, depth + 1);
	});

	if (obj.returns !== 'void') {
		f.write(indent);
		f.write('   :return:\n');
		f.write(indent_multiline(obj.returns_desc, (depth + 2) * 3));
		f.write('\n\n');
		f.write(indent);
		f.write(`   :rtype: ${obj.returns}\n\n`);
	}
}

function make_rst_object(f, obj) {
	let brief = false;
	// Our custom header flag.
	if (obj.header !== null) {
		f.write(make_header(obj.header, '-'));
		f.write(`${obj.description}\n\n`);
		brief = true;
	}

	// Format members table and descriptions
	const data = [['type', 'name']].concat(obj.members.map((m) => [m.type, `:js:attr:\`${m.name}\``]));

	f.write(make_header('Properties', '^'));
	format_table(f, data, 0);

	make_rst_attribute(f, obj, 0, brief);

	if (!obj.members.length) {
		return;
	}

	f.write('   **Property Descriptions**\n\n');

	// Properties first
	obj.members.filter((m) => !m.is_function()).forEach((m) => {
		make_rst_attribute(f, m, 1);
	});

	// Callbacks last
	obj.members.filter((m) => m.is_function()).forEach((m) => {
		make_rst_function(f, m, 1);
	});
}

function make_rst_class(f, obj) {
	const header = obj.header ? obj.header : obj.name;
	f.write(make_header(header, '-'));

	if (obj.classdesc) {
		f.write(`${obj.classdesc}\n\n`);
	}

	const funcs = obj.members.filter((m) => m.is_function());
	function make_data(m) {
		const base = m.is_static() ? obj.name : `${obj.name}.prototype`;
		const params = make_rst_signature(m, true, true);
		const sig = `:js:attr:\`${m.name} <${base}.${m.name}>\` **(**${params}**)**`;
		return [m.returns, sig];
	}
	const sfuncs = funcs.filter((m) => m.is_static());
	const ifuncs = funcs.filter((m) => !m.is_static());

	f.write(make_header('Static Methods', '^'));
	format_table(f, sfuncs.map((m) => make_data(m)));

	f.write(make_header('Instance Methods', '^'));
	format_table(f, ifuncs.map((m) => make_data(m)));

	const sig = make_rst_signature(obj);
	f.write(`.. js:class:: ${obj.name}(${sig})\n\n`);
	f.write(indent_multiline(obj.description, 3));
	f.write('\n\n');

	obj.params.forEach((p) => {
		make_rst_param(f, p, 1);
	});

	f.write('   **Static Methods**\n\n');
	sfuncs.forEach((m) => {
		make_rst_function(f, m, 1);
	});

	f.write('   **Instance Methods**\n\n');
	ifuncs.forEach((m) => {
		make_rst_function(f, m, 1);
	});
}

function make_rst_module(f, obj) {
	const header = obj.header !== null ? obj.header : obj.name;
	f.write(make_header(header, '='));
	f.write(obj.description);
	f.write('\n\n');
}

function write_base_object(f, obj) {
	if (obj.is_object()) {
		make_rst_object(f, obj);
	} else if (obj.is_function()) {
		make_rst_function(f, obj);
	} else if (obj.is_class()) {
		make_rst_class(f, obj);
	} else if (obj.is_module()) {
		make_rst_module(f, obj);
	}
}

function generate(f, docs) {
	const globs = [];
	const SYMBOLS = {};
	docs.filter((d) => !d.ignore && d.kind !== 'package').forEach((d) => {
		SYMBOLS[d.name] = d;
		if (d.memberof) {
			const up = SYMBOLS[d.memberof];
			if (up === undefined) {
				console.log(d); // eslint-disable-line no-console
				console.log(`Undefined symbol! ${d.memberof}`); // eslint-disable-line no-console
				throw new Error('Undefined symbol!');
			}
			SYMBOLS[d.memberof].add_member(d);
		} else {
			globs.push(d);
		}
	});

	f.write('.. _doc_html5_shell_classref:\n\n');
	globs.forEach((obj) => write_base_object(f, obj));
}

/**
 * Generate documentation output.
 *
 * @param {TAFFY} data - A TaffyDB collection representing
 *                       all the symbols documented in your code.
 * @param {object} opts - An object with options information.
 */
exports.publish = function (data, opts) {
	const docs = data().get().filter((doc) => !doc.undocumented && !doc.ignore).map((doc) => new JSDoclet(doc));
	const dest = opts.destination;
	if (dest === 'dry-run') {
		process.stdout.write('Dry run... ');
		generate({
			write: function () { /* noop */ },
		}, docs);
		process.stdout.write('Okay!\n');
		return;
	}
	if (dest !== '' && !dest.endsWith('.rst')) {
		throw new Error('Destination file must be either a ".rst" file, or an empty string (for printing to stdout)');
	}
	if (dest !== '') {
		const f = fs.createWriteStream(dest);
		generate(f, docs);
	} else {
		generate(process.stdout, docs);
	}
};