286 lines
8.2 KiB
JavaScript
286 lines
8.2 KiB
JavaScript
function docOf(obj, prop) {
|
||
// Grab the top-level doc block on the object.
|
||
var block = obj[prosperon.DOC];
|
||
if (!block) return '';
|
||
|
||
// 1) If `block` is directly a string, that’s the entire doc for the object.
|
||
// If a sub-property was requested, we have nowhere to look that up → return ''.
|
||
if (typeof block === 'string') {
|
||
return prop ? '' : block;
|
||
}
|
||
|
||
// 2) Otherwise, `block` is (hopefully) an object. We handle two scenarios:
|
||
// (a) No `prop` asked for → return block.doc or block[prosperon.DOC].
|
||
// (b) If a `prop` is asked for → check if block[prop] is a string or object with its own doc.
|
||
if (typeof block === 'object') {
|
||
// 2a) If no property was requested, return block.doc or block[prosperon.DOC], if either is a string.
|
||
if (!prop) {
|
||
if (typeof block.doc === 'string') {
|
||
return block.doc;
|
||
}
|
||
if (typeof block[prosperon.DOC] === 'string') {
|
||
return block[prosperon.DOC];
|
||
}
|
||
return '';
|
||
}
|
||
|
||
// 2b) If a property is requested, see if there's a doc for that property inside the block.
|
||
var subBlock = block[prop];
|
||
if (!subBlock) return '';
|
||
|
||
// If subBlock is a string → return it as the doc
|
||
if (typeof subBlock === 'string') {
|
||
return subBlock;
|
||
}
|
||
|
||
// If subBlock is an object, see if it has .doc or [prosperon.DOC]
|
||
if (typeof subBlock === 'object') {
|
||
if (typeof subBlock.doc === 'string') {
|
||
return subBlock.doc;
|
||
}
|
||
if (typeof subBlock[prosperon.DOC] === 'string') {
|
||
return subBlock[prosperon.DOC];
|
||
}
|
||
return '';
|
||
}
|
||
}
|
||
|
||
// If nothing matched, return empty.
|
||
return '';
|
||
}
|
||
|
||
|
||
function parseDocStr(docStr) {
|
||
if (!docStr) return [];
|
||
|
||
var lines = docStr.split('\n');
|
||
var paramRe = /^:param\s+([A-Za-z0-9_]+)\s*:\s*(.*)$/;
|
||
var returnRe = /^:return:\s*(.*)$/;
|
||
|
||
// We'll collect three categories of lines, then reassemble:
|
||
var descLines = []; // For plain description lines
|
||
var paramLines = []; // For :param: lines
|
||
var returnLines = []; // For :return: lines
|
||
|
||
for (var i = 0; i < lines.length; i++) {
|
||
var line = lines[i].trim();
|
||
if (!line) {
|
||
// Keep empty lines with the "desc" section to maintain spacing
|
||
descLines.push('');
|
||
continue;
|
||
}
|
||
|
||
var pm = paramRe.exec(line);
|
||
var rm = returnRe.exec(line);
|
||
|
||
if (pm) {
|
||
// param line → store in paramLines
|
||
paramLines.push('**' + pm[1] + '**: ' + pm[2]);
|
||
paramLines.push('');
|
||
} else if (rm) {
|
||
// return line → store in returnLines
|
||
returnLines.push('**Returns**: ' + rm[1]);
|
||
returnLines.push('');
|
||
} else {
|
||
// Otherwise, it's a general description line
|
||
descLines.push(line);
|
||
}
|
||
}
|
||
|
||
// Now reassemble in the fixed order:
|
||
// 1) description lines, 2) param lines, 3) returns lines
|
||
var final = [];
|
||
|
||
if (descLines.length) {
|
||
final.push.apply(final, descLines);
|
||
// Insert blank line if needed
|
||
if (paramLines.length || returnLines.length) {
|
||
final.push('');
|
||
}
|
||
}
|
||
|
||
if (paramLines.length) {
|
||
final.push.apply(final, paramLines);
|
||
}
|
||
|
||
if (returnLines.length) {
|
||
// If there were param lines, ensure blank line before the returns
|
||
if (paramLines.length && returnLines[0] !== '') {
|
||
final.push('');
|
||
}
|
||
final.push.apply(final, returnLines);
|
||
}
|
||
|
||
return final;
|
||
}
|
||
|
||
|
||
function writeDocFile(obj, title) {
|
||
var lines = [];
|
||
var docTitle = title || "Documentation";
|
||
|
||
lines.push('# ' + docTitle + '\n');
|
||
|
||
walkObject(obj, lines, 1, docTitle);
|
||
|
||
return lines.join('\n');
|
||
// If you had I/O, you'd write to file here
|
||
// io.slurpwrite(outputPath, finalStr)
|
||
}
|
||
|
||
function walkObject(obj, lines, level, name) {
|
||
// See what doc string (if any) this object has
|
||
var topDocStr = docOf(obj);
|
||
|
||
// If there's an actual docstring for the object, don't output another heading
|
||
if (topDocStr) {
|
||
// parseDocStr is your existing function that handles :param etc.
|
||
var docOut = parseDocStr(topDocStr);
|
||
if (docOut.length > 0) {
|
||
lines.push(docOut.join('\n') + '\n');
|
||
}
|
||
}
|
||
// Otherwise, if there's no docstring, insert an auto-generated heading
|
||
else {
|
||
var heading = '#'.repeat(level + 1) + ' ' + name;
|
||
lines.push(heading + '\n');
|
||
}
|
||
|
||
// Now enumerate this object's direct properties
|
||
var propNames = Object.keys(obj);
|
||
for (var i = 0; i < propNames.length; i++) {
|
||
var prop = propNames[i];
|
||
if (prop === 'constructor') continue;
|
||
|
||
var desc = Object.getOwnPropertyDescriptor(obj, prop);
|
||
if (!desc) continue;
|
||
|
||
// Check if this is an accessor property (getter/setter)
|
||
var hasGetter = typeof desc.get === 'function';
|
||
var hasSetter = typeof desc.set === 'function';
|
||
|
||
if (hasGetter || hasSetter) {
|
||
// We handle them as a single "property" entry,
|
||
// not separate get/set headings.
|
||
writeProperty(lines, obj, prop, desc, level);
|
||
continue;
|
||
}
|
||
|
||
// Otherwise, if there's a "value" (could be a function or an object)
|
||
if ('value' in desc) {
|
||
var val = desc.value;
|
||
if (typeof val === 'function') {
|
||
writeMethod(lines, obj, prop, val, level);
|
||
} else if (val && typeof val === 'object') {
|
||
// Possibly a nested object; check if it or its children have doc
|
||
if (hasAnyDoc(val)) {
|
||
walkObject(val, lines, level + 1, prop);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Writes out a single property entry for an accessor (getter/setter).
|
||
* - If only a getter, adds "(read only)" just below the property name.
|
||
* - If only a setter, adds "(set only)" just below the property name.
|
||
* - If both, no extra text is added (but we do a single heading).
|
||
*/
|
||
function writeProperty(lines, parentObj, prop, desc, level) {
|
||
var heading = '#'.repeat(level + 2) + ' ' + prop;
|
||
lines.push(heading + '\n');
|
||
|
||
var hasGetter = typeof desc.get === 'function';
|
||
var hasSetter = typeof desc.set === 'function';
|
||
|
||
if (hasGetter && !hasSetter) {
|
||
lines.push('(read only)\n');
|
||
} else if (!hasGetter && hasSetter) {
|
||
lines.push('(set only)\n');
|
||
}
|
||
// If both, we do nothing extra
|
||
|
||
// Collect doc from the property-level doc block
|
||
// If that's empty, see if either accessor function has doc
|
||
var docStr = docOf(parentObj, prop);
|
||
if (!docStr && hasGetter) {
|
||
docStr = docOf(desc.get) || docStr;
|
||
}
|
||
if (!docStr && hasSetter) {
|
||
docStr = docOf(desc.set) || docStr;
|
||
}
|
||
|
||
var docOut = parseDocStr(docStr);
|
||
if (docOut.length > 0) {
|
||
lines.push(docOut.join('\n') + '\n');
|
||
}
|
||
}
|
||
|
||
function hasAnyDoc(o) {
|
||
if (!o) return false;
|
||
// If the object itself has a docOf
|
||
if (docOf(o)) return true;
|
||
|
||
// Or if any property is a function that has doc
|
||
var names = Object.getOwnPropertyNames(o);
|
||
for (var i = 0; i < names.length; i++) {
|
||
var desc = Object.getOwnPropertyDescriptor(o, names[i]);
|
||
if (!desc) continue;
|
||
|
||
// check if accessor doc
|
||
if (typeof desc.get === 'function' && docOf(desc.get)) return true;
|
||
if (typeof desc.set === 'function' && docOf(desc.set)) return true;
|
||
|
||
// check function doc
|
||
if (desc.value && typeof desc.value === 'function') {
|
||
if (docOf(desc.value) || docOf(o, names[i])) return true;
|
||
}
|
||
// or check nested object
|
||
if (desc.value && typeof desc.value === 'object') {
|
||
if (hasAnyDoc(desc.value)) return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function writeMethod(lines, parentObj, prop, fn, level) {
|
||
var docStr = fn[prosperon.DOC] || docOf(parentObj, prop) || '';
|
||
// parse :param lines
|
||
var paramNames = [];
|
||
|
||
if (docStr) {
|
||
var docLines = docStr.split('\n');
|
||
var paramRe = /^:param\s+([A-Za-z0-9_]+)\s*:\s*(.*)$/;
|
||
for (var j = 0; j < docLines.length; j++) {
|
||
var pm = paramRe.exec(docLines[j]);
|
||
if (pm) paramNames.push(pm[1]);
|
||
}
|
||
}
|
||
|
||
var heading = '#'.repeat(level + 2) + ' ' + prop;
|
||
// if we found param names in the doc, show them in the heading
|
||
if (paramNames.length > 0) {
|
||
heading += '(' + paramNames.join(', ') + ')';
|
||
} else {
|
||
// fallback: parse the function signature
|
||
var m = fn.toString().match(/\(([^)]*)\)/);
|
||
if (m && m[1].trim()) {
|
||
heading += '(' + m[1].trim() + ')';
|
||
}
|
||
}
|
||
lines.push(heading + '\n');
|
||
|
||
// parse doc lines
|
||
var docOut = parseDocStr(docStr);
|
||
if (docOut.length > 0) {
|
||
lines.push(docOut.join('\n') + '\n');
|
||
}
|
||
}
|
||
|
||
writeDocFile[prosperon.DOC] = `Return a markdown string for a given obj, with optional title.`;
|
||
|
||
return {
|
||
writeDocFile: writeDocFile
|
||
};
|