mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-22 22:07:48 +00:00
318 lines
No EOL
7.8 KiB
TypeScript
318 lines
No EOL
7.8 KiB
TypeScript
/*
|
|
* Keyman is copyright (C) SIL International. MIT License.
|
|
*
|
|
* Low-friction tool to extract javadoc-style comments from Keyman API headers.
|
|
* Could be replaced in future with doxygen/moxygen type of toolchain, but
|
|
* for now this covers our needs with minimal fuss.
|
|
*
|
|
* Notes:
|
|
* - `@name` command is required for each documented item
|
|
* - `@param` and `@returns` are the only other supported commands
|
|
* - this is fairly naive so check outputs to make sure they are
|
|
* giving what you expect!
|
|
*/
|
|
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
|
|
/** Stop processing header file after encountering this marker */
|
|
const EOF_MARKER = '/* $EOF */';
|
|
|
|
/** For every symbol found, keep a reference for hyperlinks */
|
|
interface ApiEndpoint {
|
|
filename: string;
|
|
type: string;
|
|
endpoint: string;
|
|
};
|
|
|
|
/** Filename and content for output files */
|
|
interface DocFile {
|
|
filename: string;
|
|
lines: string[];
|
|
};
|
|
|
|
type ProcessorMode = keyof typeof processor;
|
|
|
|
// global variables
|
|
|
|
let mode: ProcessorMode;
|
|
let currentFilename: string | undefined;
|
|
let lines: string[], result: string[], apiEndpoints: ApiEndpoint[], files: DocFile[];
|
|
let sourceFilename: string, sourceLine: number;
|
|
|
|
const processor = {
|
|
preamble: (line: string) => {
|
|
if(line.trim() == '---') {
|
|
mode = 'filename';
|
|
}
|
|
},
|
|
|
|
filename: (line: string) => {
|
|
if(!line.trim().startsWith('filename:')) {
|
|
throw new Error(`Expecting line to start with "filename:" but instead got "${line}"`);
|
|
}
|
|
|
|
cacheResult();
|
|
|
|
currentFilename = line.split(':', 2)[1].trim();
|
|
mode = 'title';
|
|
result = [
|
|
'---' // first line of markdown title section
|
|
];
|
|
},
|
|
|
|
title: (line: string) => {
|
|
result.push(line);
|
|
if(line.trim() == '---') {
|
|
mode = 'content';
|
|
result.push(`<!-- generated by core/tools/api-header-extractor from core/include/keyman/*.h on ${new Date().toISOString().substring(0,10)} -->`);
|
|
}
|
|
},
|
|
|
|
content: (line: string) => {
|
|
if(line.trim() == '---') {
|
|
mode = 'filename';
|
|
return;
|
|
}
|
|
|
|
if(line.trim() == EOF_MARKER) {
|
|
mode = 'eof';
|
|
return;
|
|
}
|
|
|
|
if(line.trim() == '/**') {
|
|
// This is start of a new function definition
|
|
mode = 'functionComment';
|
|
return;
|
|
}
|
|
|
|
if(line.trim().startsWith('//')) {
|
|
// ignore inline comments
|
|
return;
|
|
}
|
|
|
|
if(line.trim().includes('/*')) {
|
|
// ignore start of comment that is not a doc comment -- note that this may
|
|
// leave comment body in the documentation, so currently assuming that we
|
|
// don't use this pattern
|
|
warn(`Unexpected multi-line comment token`, line);
|
|
return;
|
|
}
|
|
|
|
if(line.trim() == '*/') {
|
|
// end of a multi-line comment, we don't emit
|
|
return;
|
|
}
|
|
|
|
result.push(line);
|
|
},
|
|
|
|
functionComment: (line: string) => {
|
|
line = line.trim();
|
|
if(isCommentFinisher(line)) return;
|
|
const { command, param } = getCommentCommand(line);
|
|
if(!command) {
|
|
result.push(param);
|
|
return;
|
|
}
|
|
|
|
if(command == 'name') {
|
|
const m = param.match(/^([^ ]+)\s*(.*)$/);
|
|
if(!m) {
|
|
console.log('Warning: misformatted @name line');
|
|
return;
|
|
}
|
|
const name=m[1], title=`${m[1]} ${m[2]}`.trim();
|
|
result.push('');
|
|
result.push(`# ${title} {#${name}}`);
|
|
apiEndpoints.push({filename: currentFilename!, endpoint: name, type: m[2].trim()});
|
|
} else if(command == 'param') {
|
|
result.push('');
|
|
result.push(`## Parameters`);
|
|
mode = 'functionCommentParam';
|
|
processor.functionCommentParam('* @param '+param);
|
|
} else if(command == 'returns') {
|
|
result.push('');
|
|
result.push(`## Returns`);
|
|
mode = 'functionCommentReturns';
|
|
processor.functionCommentReturns('* '+param);
|
|
}
|
|
},
|
|
|
|
functionCommentParam: (line: string) => {
|
|
line = line.trim();
|
|
if(isCommentFinisher(line)) return;
|
|
const { command, param } = getCommentCommand(line);
|
|
if(command != 'param' && command != null) {
|
|
mode = 'functionComment';
|
|
return processor.functionComment(line);
|
|
}
|
|
if(command == 'param') {
|
|
// expect @param <name> [desc]
|
|
const m = param.match(/^([^ ]+)\s*(.*)$/);
|
|
if(!m) {
|
|
warn(`Warning: misformatted @param line`, line);
|
|
return;
|
|
}
|
|
const name=m[1], desc=m[2];
|
|
result.push('');
|
|
result.push(`### ${name}`);
|
|
result.push(desc);
|
|
} else {
|
|
// continuation of `param`
|
|
result.push(param);
|
|
}
|
|
},
|
|
|
|
functionCommentReturns: (line: string) => {
|
|
line = line.trim();
|
|
if(isCommentFinisher(line)) return;
|
|
const { command, param } = getCommentCommand(line);
|
|
if(command != null) {
|
|
mode = 'functionComment';
|
|
return processor.functionComment(line);
|
|
}
|
|
// continuation of `returns`
|
|
result.push(param);
|
|
},
|
|
|
|
declaration: (line: string) => {
|
|
if(line.trim().startsWith('//')) {
|
|
// Ignore internal comments
|
|
return;
|
|
}
|
|
|
|
if(line.trim() == '/**') {
|
|
// This is start of a new function definition
|
|
result.push('```');
|
|
mode = 'functionComment';
|
|
return;
|
|
}
|
|
|
|
if(line.trim() == EOF_MARKER) {
|
|
mode = 'eof';
|
|
cacheResult();
|
|
return;
|
|
}
|
|
|
|
if(line.trim() != '') {
|
|
result.push(line);
|
|
}
|
|
},
|
|
|
|
eof: (_line: string) => {
|
|
// ignore all content after EOF_MARKER
|
|
}
|
|
}
|
|
|
|
function getCommentCommand(line: string) {
|
|
const m = line.match(/^\s*\*\s+@(name|param|returns)(\s+.*)?$/);
|
|
if(!m) {
|
|
return { command: null, param: line.substring(2).trim() }; // strips initial '* '
|
|
}
|
|
return {
|
|
command: m[1],
|
|
param: (m[2] ?? '').trim()
|
|
}
|
|
}
|
|
|
|
function isCommentFinisher(line: string) {
|
|
if(line.endsWith('*/')) {
|
|
mode = 'declaration';
|
|
result.push('');
|
|
result.push('```c');
|
|
return true;
|
|
}
|
|
|
|
if(line == '---') {
|
|
// Starting a new file
|
|
mode = 'filename';
|
|
return true;
|
|
}
|
|
|
|
if(!line.startsWith('*')) {
|
|
warn(`Line had an unexpected start`, line);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Save the current output file, if not empty, as we are
|
|
* about to start a new file
|
|
*/
|
|
function cacheResult() {
|
|
if(currentFilename != undefined && result.length > 1) {
|
|
files.push({
|
|
filename: currentFilename,
|
|
lines: result
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write the current file out including appending the cross-references
|
|
* @param dstFolder target folder
|
|
* @param file documentation file to write out
|
|
* @param refs string of cross-references to add to bottom of file
|
|
*/
|
|
|
|
function writeFile(dstFolder: string, file: DocFile, refs: string): void {
|
|
fs.writeFileSync(path.join(dstFolder, file.filename), file.lines.join('\n') + '\n\n' + refs, 'utf-8');
|
|
}
|
|
|
|
function buildRefs(): string {
|
|
let result = [];
|
|
for(let ep of apiEndpoints) {
|
|
const filename = ep.filename.substring(0,ep.filename.length-3); // Strip .md, rough but ready
|
|
result.push(`[${ep.endpoint}]: ${filename}#${ep.endpoint} "${ep.endpoint} ${ep.type}"`);
|
|
}
|
|
return result.join('\n');
|
|
}
|
|
|
|
function warn(message: string, line: string) {
|
|
console.warn(`${sourceFilename}:${sourceLine} ${message}\n ${line}`);
|
|
}
|
|
|
|
function main() {
|
|
if(process.argv.length < 4) {
|
|
console.error('Invalid parameters');
|
|
console.log('Usage: node index.js <dstPath> <srcFile...>');
|
|
process.exit(1);
|
|
}
|
|
|
|
const dstPath = process.argv[2];
|
|
const src = process.argv.slice(3);
|
|
|
|
apiEndpoints = [];
|
|
files = [];
|
|
|
|
// Process each file
|
|
for(const s of src) {
|
|
result = [];
|
|
currentFilename = undefined;
|
|
mode = 'preamble';
|
|
|
|
lines = fs.readFileSync(s, 'utf-8').replace(/\r/g, '').split('\n');
|
|
|
|
sourceFilename = s;
|
|
sourceLine = 1;
|
|
for(const line of lines) {
|
|
processor[mode](line);
|
|
sourceLine++;
|
|
}
|
|
|
|
cacheResult();
|
|
}
|
|
// Build heading references
|
|
const refs = buildRefs();
|
|
|
|
// Write out
|
|
for(const file of files) {
|
|
writeFile(dstPath, file, refs);
|
|
}
|
|
}
|
|
|
|
main(); |