/** * Created by Capricorncd. * https://github.com/capricorncd * Date: 2022/06/11 13:13:33 (GMT+0900) */ /* eslint-disable @typescript-eslint/no-var-requires */ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { isObject } from '@zx/utils' import { BLANK_LINE, DOC_TYPES } from './const' import { getCommentsData } from './docs-input' import { mkdirSync, isFileLike, isValidArray, formatAsArray, createPropsTable, mergeIntoArray, } from './helpers' import { log } from './log' import type { OutputFileOptions, CommentInfoItem, OutputFileInput, CommentInfoItemProp, } from './types.d' /** * create method docs * @param item `CommentInfoItem` * @param lines `string[]` */ function createMethodsDoc( item: CommentInfoItem, lines: string[], options: OutputFileOptions = {} ) { if (!item.returns.length) { item.returns.push({ raw: '`void`', types: ['void'], desc: [], }) } lines.push( `### ${item.fullName}`, BLANK_LINE, ...item.desc, BLANK_LINE, // '*' will be replaced by 'npx pretty-quick --staged' with '-' // item.params.map((param) => `* @param ${param}`), ...(options.methodWithRaw ? item.params.map((param) => `- @param ${param.raw}`) : createPropsTable(item.params, DOC_TYPES.method, 'Param', options)), BLANK_LINE ) if (item.generics.length) { lines.push('- @generic `' + item.generics.join(', ') + '`', BLANK_LINE) } lines.push(...item.returns.map((ret) => `- @returns ${ret.raw}`), BLANK_LINE) pushCodesIntoLines(item.codes, lines) } /** * 将注释中的代码,添加到desc行的后面 * 目前document/property/method有效 * `isExtractCodeFromComments`为`true`时,`codes`里才有数据 * @param codes 注释中的代码数组 * @param lines 已处理的文档行数组 * @param options */ function pushCodesIntoLines(codes: string[], lines: string[]) { if (isValidArray(codes)) { lines.push(...codes, BLANK_LINE) } } /** * create types docs * @param item `CommentInfoItem` * @param lines `string[]` * @param options `{typeWithTable: false, typeWithSourceCode: false}` */ function createTypesDoc( item: CommentInfoItem, lines: string[], options: OutputFileOptions = {} ) { lines.push(`### ${item.fullName}`, BLANK_LINE, ...item.desc, BLANK_LINE) // table const typeTable = createPropsTable( item.props as CommentInfoItemProp[], DOC_TYPES.type, 'Prop', options ) // codes const codes = ['```ts', ...item.codes, '```', BLANK_LINE] // source code alias const sourceCodeSummary = options.alias?.sourceCodeSummary const details = [ '
', `${sourceCodeSummary || 'Source Code'}`, BLANK_LINE, ...codes, BLANK_LINE, '
', BLANK_LINE, ] const { typeWithSourceCode, typeWithTable, typeWithAuto } = options if (typeWithSourceCode && typeWithTable) { lines.push(...typeTable, ...codes) } else if (typeWithSourceCode) { // only source code lines.push(...codes) } else if (typeWithTable) { // only table lines.push(...typeTable) } else { // table and source code if (typeTable.length) { lines.push(...typeTable, ...details) } else { if (typeWithAuto) { // When typeWithAuto is true and typeTable is not, display only code. lines.push(...codes) } else { // default `
...
` lines.push(...details) } } } } /** * remove consecutive blank lines * @param lines `string[]` * @returns `string[]` */ function removeConsecutiveBlankLine(lines: string[]) { let blankLineCount = 0 const outputLines: string[] = [] lines.forEach((line) => { if (line === BLANK_LINE) { blankLineCount++ } else { blankLineCount = 0 } if (blankLineCount > 1) return outputLines.push(line) }) return outputLines } // # Documents function handleDocumentLines( arr: CommentInfoItem[], options: OutputFileOptions, lines: string[] ) { if (!isValidArray(arr)) return // types alias const typesAlias = options.alias?.types || {} const linesAfterTitles = formatAsArray( options.lines?.afterTitle?.[DOC_TYPES.document] ) let outputFileName = null arr.forEach((item, i) => { if (i === 0) { outputFileName = item.name + '.md' lines.push( `# ${typesAlias[DOC_TYPES.document] || item.fullName}`, BLANK_LINE ) // insert lines after method title if (isValidArray(linesAfterTitles)) { lines.push(...linesAfterTitles, BLANK_LINE) } } else { lines.push(`### ${item.fullName}`, BLANK_LINE) } lines.push(...item.desc, BLANK_LINE) pushCodesIntoLines(item.codes, lines) }) return outputFileName } // ## Methods export function handleMethodLines( arr: CommentInfoItem[], options: OutputFileOptions, lines: string[] ) { if (!isValidArray(arr)) return handleMarkdownTitle(DOC_TYPES.method, options, lines) arr.forEach((item) => { createMethodsDoc(item, lines, options) }) } // ## Types function handleTypesLines( arr: CommentInfoItem[], options: OutputFileOptions, lines: string[] ) { if (!isValidArray(arr)) return handleMarkdownTitle(DOC_TYPES.type, options, lines) arr.forEach((item) => { createTypesDoc(item, lines, options) }) } function handleMarkdownTitle( type: string, options: OutputFileOptions, lines: string[] ) { const typesAlias = options.alias?.types || {} const mdTitles = { document: 'Document', method: 'Methods', type: 'Types', constant: 'Constants', property: 'Property', } lines.push( `## ${typesAlias[type] || mdTitles[type as keyof typeof mdTitles] || type}`, BLANK_LINE ) const linesAfterTitles = formatAsArray(options.lines?.afterTitle?.[type]) // insert lines after type title if (isValidArray(linesAfterTitles)) { lines.push(...linesAfterTitles, BLANK_LINE) } } function handleConstLines( arr: CommentInfoItem[], options: OutputFileOptions, lines: string[] ) { if (!isValidArray(arr)) return handleMarkdownTitle(DOC_TYPES.constant, options, lines) arr.forEach((item) => { lines.push(`### ${item.fullName}`, BLANK_LINE, ...item.desc, BLANK_LINE) if (isValidArray(item.codes)) { lines.push('```ts', ...item.codes, '```', BLANK_LINE) } }) } /** * handle property * @param arr * @param options * @param lines */ export function handlePropertyLines( arr: CommentInfoItem[], options: OutputFileOptions, lines: string[] ) { if (!isValidArray(arr)) return handleMarkdownTitle(DOC_TYPES.property, options, lines) arr.forEach((item) => { lines.push(`### ${item.fullName}`, BLANK_LINE, ...item.desc, BLANK_LINE) if (item.returns.length) { lines.push( ...item.returns.map((ret) => `- @returns ${ret.raw}`), BLANK_LINE ) } pushCodesIntoLines(item.codes, lines) }) } /** * handle output * @param arr `CommentInfoItem[]` * @param outputDir `string` optional parameter. * @param options `OutputFileOptions` * @returns `{ outputFileName: string | null, lines: string[], data: CommentInfoItem[] }` */ function handleOutput( arr: CommentInfoItem[], outputDir?: string, options: OutputFileOptions = {} ) { console.log('Output file is start ...') // method|type|constant|document|component|... const originalData: Record = {} let outputFileName = null arr.forEach((item) => { if (!originalData[item.type]) { originalData[item.type] = [] } originalData[item.type].push(item) }) const lines: string[] = [] // start lines const startLines = formatAsArray(options.lines?.start) if (isValidArray(startLines)) { lines.push(...startLines, BLANK_LINE) } // linesAfterType const linesAfterType = options.lines?.afterType || {} // Output types and their order const outputDocTypesAndOrder = isValidArray(options.outputDocTypesAndOrder) ? options.outputDocTypesAndOrder : [ DOC_TYPES.document, DOC_TYPES.property, DOC_TYPES.method, DOC_TYPES.type, DOC_TYPES.constant, ] outputDocTypesAndOrder.forEach((type) => { const handler = options.handlers?.[type] if (typeof handler === 'function') { handler(originalData[type], options, lines) } else { // # document if (type === DOC_TYPES.document) { outputFileName = handleDocumentLines(originalData[type], options, lines) } else if (type === DOC_TYPES.property) { handlePropertyLines(originalData[type], options, lines) } else if (type === DOC_TYPES.method) { handleMethodLines(originalData[type], options, lines) } else if (type === DOC_TYPES.type) { handleTypesLines(originalData[type], options, lines) } else if (type === DOC_TYPES.constant) { handleConstLines(originalData[type], options, lines) } } // lines after docTypes if (linesAfterType[type]) { lines.push(...formatAsArray(linesAfterType[type]), BLANK_LINE) } }) // end lines const endLines = formatAsArray(options.lines?.end) if (isValidArray(endLines)) { lines.push(...endLines, BLANK_LINE) } const outputLines = removeConsecutiveBlankLine(lines) if (outputDir) { // file check if (isFileLike(outputDir)) { outputFileName = outputDir } else if (outputFileName) { outputFileName = path.join(outputDir, outputFileName) } // output file if (outputFileName) writeFileSync(outputFileName, outputLines) } log(outputFileName || 'no files were output!') console.log('Output file is ended.') return { outputFileName, lines: outputLines, data: arr, } } /** * @method writeFileSync(outputFileName, outputLines) * Synchronized file write function. * @param outputFileName `string` Output filename, absolute path. * @param outputLines `string[] | NodeJS.ArrayBufferView | string` The output file content, an array of strings. */ export function writeFileSync( outputFileName: string, outputLines: string[] | NodeJS.ArrayBufferView | string ) { if (Array.isArray(outputLines)) { outputLines = outputLines.join(os.EOL) } fs.writeFileSync(outputFileName, outputLines, 'utf8') } /** * @method outputFile(input, outputDirOrFile, options) * Output the obtained annotation content as a document. * @param input `{[filePath]: {[key]: CommentInfoItem}} | CommentInfoItem[] | string` Comment obtained from the source. When `string` it's a file path, and the [getCommentsData](#getcommentsdatainput-needarray-options) will be called. What's [CommentInfoItem](#commentinfoitem). * @param outputDirOrFile? `string` Optional parameter. The file or directory where the output will be written. When `outputDirOrFile` is `undefined`, no file will be output. * @param options? `OutputFileOptions` [OutputFileOptions](#OutputFileOptions) * @returns `OutputFileReturns | OutputFileReturns[]` What's [OutputFileReturns](#outputfilereturns) */ export function outputFile( input: OutputFileInput, outputDirOrFile?: string | OutputFileOptions, options?: OutputFileOptions ) { // check other parameters if (isObject(outputDirOrFile)) { options = outputDirOrFile outputDirOrFile = undefined } options = options || {} // file or directory's path, or an array of paths if ( // file or directory's path typeof input === 'string' || // or an array of paths (isValidArray(input) && (input as string[]).every((str) => typeof str === 'string')) ) { input = getCommentsData(input as string | string[], true, options) } const optionsLines = options.lines || {} const optionsAlias = options.alias || {} options = { ...options, lines: optionsLines, alias: optionsAlias, } if (outputDirOrFile && !fs.existsSync(outputDirOrFile)) { if (isFileLike(outputDirOrFile)) { const outputDir = outputDirOrFile.split('/') outputDir.pop() mkdirSync(outputDir.join('/')) } else { mkdirSync(outputDirOrFile) } } if (Array.isArray(input)) { return handleOutput(input as CommentInfoItem[], outputDirOrFile, options) } else { // Combine output into one file return handleOutput( mergeIntoArray(input, options), outputDirOrFile, options ) // // Separate output of different files, provided that outputDirOrFile is a file directory, and each file contains `@document`. // return Object.keys(input).map((key) => { // return handleOutput( // toArray(input[key], options), // outputDirOrFile, // options // ) // }) } }