diff --git a/.gitignore b/.gitignore index 42f1e447dc..6ddaf0e001 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ docs/reference/* examples/3d/ .idea dist/ +*d.ts p5.zip bower-repo/ p5-website/ diff --git a/package.json b/package.json index 978e7d8703..270c442a58 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "bench:report": "vitest bench --reporter=verbose", "test": "vitest", "lint": "eslint .", - "lint:fix": "eslint --fix ." + "lint:fix": "eslint --fix .", + "generate-types": "npm run docs && node utils/generate-types && node utils/patch" }, "lint-staged": { "Gruntfile.js": "eslint", diff --git a/src/color/creating_reading.js b/src/color/creating_reading.js index f848ba3a25..f838c4ceff 100644 --- a/src/color/creating_reading.js +++ b/src/color/creating_reading.js @@ -1540,8 +1540,8 @@ function creatingReading(p5, fn){ * colorMode(). * * @method paletteLerp - * @param {[p5.Color|String|Number|Number[], Number][]} colors_stops color stops to interpolate from - * @param {Number} amt number to use to interpolate relative to color stops + * @param {Array.>} colors_stops color stops to interpolate from + * @param {Number} amt number to use to interpolate relative to color stops * @return {p5.Color} interpolated color. * * @example diff --git a/utils/convert.js b/utils/convert.js index a92049298a..dcc478b2a9 100644 --- a/utils/convert.js +++ b/utils/convert.js @@ -602,3 +602,5 @@ fs.mkdirSync(path.join(__dirname, '../docs/reference'), { recursive: true }); fs.writeFileSync(path.join(__dirname, '../docs/reference/data.json'), JSON.stringify(converted, null, 2)); fs.writeFileSync(path.join(__dirname, '../docs/reference/data.min.json'), JSON.stringify(converted)); buildParamDocs(JSON.parse(JSON.stringify(converted))); + +module.exports= { getAllEntries }; diff --git a/utils/generate-types.js b/utils/generate-types.js new file mode 100644 index 0000000000..6992f6b41d --- /dev/null +++ b/utils/generate-types.js @@ -0,0 +1,631 @@ +const fs = require('fs'); +const path = require('path'); +const { getAllEntries } = require("./convert"); + +// Read docs.json +const data = JSON.parse(fs.readFileSync(path.join(__dirname, '../docs/data.json'))); + +const organized = { + modules: {}, + classes: {}, + classitems: [], + consts: {} + }; + +// Add this helper function at the top with other helpers +function normalizeClassName(className) { + if (!className || className === 'p5') return 'p5'; + return className.startsWith('p5.') ? className : `p5.${className}`; +} + +// Organize data into structured format +function organizeData(data) { + const allData = getAllEntries(data); + + + // Process modules first + allData.forEach(entry => { + if (entry.tags?.some(tag => tag.title === 'module')) { + const { module, submodule } = getModuleInfo(entry); + organized.modules[module] = organized.modules[module] || { + name: module, + submodules: {}, + classes: {} + }; + if (submodule) { + organized.modules[module].submodules[submodule] = true; + } + } + }); + + // Process classes + allData.forEach(entry => { + if (entry.kind === 'class') { + const { module, submodule } = getModuleInfo(entry); + const className = entry.name; + const extendsTag = entry.tags?.find(tag => tag.title === 'extends'); + + organized.classes[className] = { + name: className, + description: extractDescription(entry.description), + params: (entry.params || []).map(param => ({ + name: param.name, + type: generateTypeFromTag(param), + optional: param.type?.type === 'OptionalType' + })), + module, + submodule, + extends: extendsTag?.name || null + }; + } + }); + + // Process class methods and properties + allData.forEach(entry => { + if (entry.kind === 'function' || entry.kind === 'property') { + const { module, submodule, forEntry } = getModuleInfo(entry); + + // Normalize memberof and forEntry + let memberof = entry.memberof; + if (memberof && memberof !== 'p5' && !memberof.startsWith('p5.')) { + memberof = 'p5.' + memberof; + } + + let normalizedForEntry = forEntry; + if (forEntry && forEntry !== 'p5' && !forEntry.startsWith('p5.')) { + normalizedForEntry = 'p5.' + forEntry; + } + + // Use memberof if available, fallback to forEntry, then default to 'p5' + const className = normalizeClassName(memberof || normalizedForEntry || 'p5'); + + const isStatic = entry.path?.[0]?.scope === 'static'; + // Handle overloads + const overloads = entry.overloads?.map(overload => ({ + params: overload.params, + returns: overload.returns, + description: extractDescription(overload.description) + })); + + organized.classitems.push({ + name: entry.name, + kind: entry.kind, + description: extractDescription(entry.description), + params: (entry.params || []).map(param => ({ + name: param.name, + type: generateTypeFromTag(param), + optional: param.type?.type === 'OptionalType' + })), + returnType: entry.returns?.[0] ? generateTypeFromTag(entry.returns[0]) : 'void', + module, + submodule, + class: className, + isStatic, + overloads + }); + } + }); + + // Process constants and typedefs + allData.forEach(entry => { + if (entry.kind === 'constant' || entry.kind === 'typedef') { + const { module, submodule, forEntry } = getModuleInfo(entry); + organized.consts[entry.name] = { + name: entry.name, + kind: entry.kind, + description: extractDescription(entry.description), + type: entry.type ? generateTypeFromTag(entry) : 'any', + module, + submodule, + class: forEntry || 'p5' + }; + } + fs.writeFileSync("./consts.json", JSON.stringify(organized.consts, null, 2), 'utf8'); + }); + + return organized; +} + +// Helper function to get module info +function getModuleInfo(entry) { + const moduleTag = entry.tags?.find(tag => tag.title === 'module'); + const submoduleTag = entry.tags?.find(tag => tag.title === 'submodule'); + const forTag = entry.tags?.find(tag => tag.title === 'for') + + return { + module: moduleTag?.name || 'p5', + submodule: submoduleTag?.description, + forEntry: forTag?.description || entry.memberof + }; +} + +// Function to extract text from description object or string +function extractDescription(desc) { + if (!desc) return ''; + if (typeof desc === 'string') return desc; + if (desc.children) { + return desc.children + .map(child => { + if (child.type === 'text') return child.value; + if (child.type === 'paragraph') return extractDescription(child); + if (child.type === 'inlineCode') return `\`${child.value}\``; + if (child.type === 'code') return `\`${child.value}\``; + return ''; + }) + .join('') + .trim() + .replace(/\n{3,}/g, '\n\n'); + } + return ''; +} + +// Format comment text for JSDoc +function formatJSDocComment(text, indentLevel = 0) { + if (!text) return ''; + const indent = ' '.repeat(indentLevel); + + const lines = text + .split('\n') + .map(line => line.trim()) + .reduce((acc, line) => { + // If we're starting and line is empty, skip it + if (acc.length === 0 && line === '') return acc; + // If we have content and hit an empty line, keep one empty line + if (acc.length > 0 && line === '' && acc[acc.length - 1] === '') return acc; + acc.push(line); + return acc; + }, []) + .filter((line, i, arr) => i < arr.length - 1 || line !== ''); // Remove trailing empty line + + return lines + .map(line => `${indent} * ${line}`) + .join('\n'); +} + +// Normalize type names to ensure primitive types are lowercase and handle object types +function normalizeTypeName(type) { + if (!type) return 'any'; + + // Handle object type notation + if (type === '[object Object]') return 'any'; + + const primitiveTypes = { + 'String': 'string', + 'Number': 'number', + 'Integer': 'number', + 'Boolean': 'boolean', + 'Void': 'void', + 'Object': 'object', + 'Array': 'Array', + 'Function': 'Function' + }; + + return primitiveTypes[type] || type; +} + +// Generate type from tag +function generateTypeFromTag(param) { + if (!param || !param.type) return 'any'; + + switch (param.type.type) { + case 'NameExpression': + return normalizeTypeName(param.type.name); + case 'TypeApplication': + const baseType = normalizeTypeName(param.type.expression.name); + + // Handle array cases + if (baseType === 'Array') { + const innerType = param.type.applications[0]; + + // Handle nested array that represents a tuple + if (innerType.type === 'TypeApplication' && + innerType.expression.name === 'Array') { + // Get all tuple element types + const tupleTypes = innerType.applications + .map(app => generateTypeFromTag({ type: app })) + .join(', '); + return `Array<[${tupleTypes}]>`; + } + + // Handle array with union type + if (innerType.type === 'UnionType') { + const unionTypes = innerType.elements + .map(el => generateTypeFromTag({ type: el })) + .join(' | '); + // If this is part of a tuple structure (has sibling types), wrap in tuple + if (param.type.applications.length > 1) { + const remainingTypes = param.type.applications + .slice(1) + .map(app => generateTypeFromTag({ type: app })) + .join(', '); + return `Array<[${unionTypes}, ${remainingTypes}]>`; + } + return `Array<${unionTypes}>`; + } + + // Regular array + const typeParam = generateTypeFromTag({ type: innerType }); + return `Array<${typeParam}>`; + } + + // Regular type application + const typeParams = param.type.applications + .map(app => generateTypeFromTag({ type: app })) + .join(', '); + return `${baseType}<${typeParams}>`; + case 'UnionType': + const unionTypes = param.type.elements + .map(el => generateTypeFromTag({ type: el })) + .join(' | '); + return unionTypes; + case 'OptionalType': + return generateTypeFromTag({ type: param.type.expression }); + case 'AllLiteral': + return 'any'; + case 'RecordType': + return 'object'; + case 'StringLiteralType': + return `'${param.type.value}'`; + case 'UndefinedLiteralType': + return 'undefined'; + case 'ArrayType': + // Check if it's a tuple type (array with specific types for each position) + if (param.type.elements) { + const tupleTypes = param.type.elements + .map(el => generateTypeFromTag({ type: el })) + .join(', '); + return `[${tupleTypes}]`; + } + // Regular array type + return `${generateTypeFromTag({ type: param.type.elementType })}[]`; + default: + return 'any'; + } +} + +// Generate parameter declaration +function generateParamDeclaration(param) { + if (!param) return 'any'; + + let type = param.type; + const isOptional = param.type?.type === 'OptionalType'; + if (typeof type === 'string') { + type = normalizeTypeName(type); + } else if (param.type?.type) { + type = generateTypeFromTag(param); + } else { + type = 'any'; + } + + return `${param.name}${isOptional ? '?' : ''}: ${type}`; +} + +// Generate function declaration +function generateFunctionDeclaration(funcDoc) { + let output = ''; + + // Add Comments + if (funcDoc.description || funcDoc.tags?.length > 0) { + output += '/**\n'; + const description = extractDescription(funcDoc.description); + if (description) { + output += formatJSDocComment(description) + '\n'; + } + if (funcDoc.tags) { + if (description) { + output += ' *\n'; // Add separator between description and tags + } + funcDoc.tags.forEach(tag => { + if (tag.description) { + const tagDesc = extractDescription(tag.description); + output += formatJSDocComment(`@${tag.title} ${tagDesc}`, 0) + '\n'; + } + }); + } + output += ' */\n'; + } + + // Generate function signature + const params = (funcDoc.params || []) + .map(param => generateParamDeclaration(param)) + .join(', '); + + const returnType = funcDoc.returns?.[0]?.type + ? generateTypeFromTag(funcDoc.returns[0]) + : 'void'; + + output += `function ${funcDoc.name}(${params}): ${returnType};\n\n`; + return output; +} + +// Helper function to generate method declarations +function generateMethodDeclarations(item, isStatic = false) { + let output = ''; + + // Add JSDoc comment + if (item.description) { + output += ' /**\n'; + const itemDesc = extractDescription(item.description); + output += formatJSDocComment(itemDesc, 2) + '\n'; + if (item.params?.length > 0) { + output += ' *\n'; + item.params.forEach(param => { + const paramDesc = extractDescription(param.description); + output += formatJSDocComment(`@param ${paramDesc}`, 2) + '\n'; + }); + } + if (item.returns) { + output += ' *\n'; + const returnDesc = extractDescription(item.returns[0]?.description); + output += formatJSDocComment(`@return ${returnDesc}`, 2) + '\n'; + } + output += ' */\n'; + } + + if (item.kind === 'function') { + const staticPrefix = isStatic ? 'static ' : ''; + + // If there are overloads, generate all overload signatures first + if (item.overloads?.length > 0) { + item.overloads.forEach(overload => { + const params = (overload.params || []) + .map(param => generateParamDeclaration(param)) + .join(', '); + const returnType = overload.returns?.[0]?.type + ? generateTypeFromTag(overload.returns[0]) + : 'void'; + output += ` ${staticPrefix}${item.name}(${params}): ${returnType};\n`; + }); + } + + // Generate the implementation signature + const params = (item.params || []) + .map(param => generateParamDeclaration(param)) + .join(', '); + output += ` ${staticPrefix}${item.name}(${params}): ${item.returnType};\n\n`; + } else { + // Handle properties + const staticPrefix = isStatic ? 'static ' : ''; + output += ` ${staticPrefix}${item.name}: ${item.returnType};\n\n`; + } + + return output; +} + +// Generate class declaration +function generateClassDeclaration(classDoc, organizedData) { + let output = ''; + + // Add comments + if (classDoc.description || classDoc.tags?.length > 0) { + output += '/**\n'; + const description = extractDescription(classDoc.description); + if (description) { + output += formatJSDocComment(description) + '\n'; + } + if (classDoc.tags) { + if (description) { + output += ' *\n'; + } + classDoc.tags.forEach(tag => { + if (tag.description) { + const tagDesc = extractDescription(tag.description); + + output += formatJSDocComment(`@${tag.title} ${tagDesc}`, 0) + '\n'; + } + }); + } + output += ' */\n'; + } + + // Get the parent class if it exists + const parentClass = classDoc.extends; + const extendsClause = parentClass ? ` extends ${parentClass}` : ''; + + // Start class declaration with inheritance if applicable + const fullClassName = normalizeClassName(classDoc.name); + const classDocName = fullClassName.replace('p5.', ''); + output += `class ${classDocName}${extendsClause} {\n`; + + // Add constructor if there are parameters + if (classDoc.params?.length > 0) { + output += ' constructor('; + output += classDoc.params + .map(param => generateParamDeclaration(param)) + .join(', '); + output += ');\n\n'; + } + + // Get all class items for this class + const classItems = organizedData.classitems.filter(item => + item.class === fullClassName || + item.class === fullClassName.replace('p5.', '') + ); + + // Separate static and instance members + const staticItems = classItems.filter(item => item.isStatic); + const instanceItems = classItems.filter(item => !item.isStatic); + + // Generate static members + staticItems.forEach(item => { + output += generateMethodDeclarations(item, true); + }); + + // Generate instance members + instanceItems.forEach(item => { + output += generateMethodDeclarations(item, false); + }); + + output += '}\n\n'; + return output; +} + +// Generate declaration file for a group of items +function generateDeclarationFile(items, filePath, organizedData) { + let output = '// This file is auto-generated from JSDoc documentation\n\n'; + + // Add imports based on dependencies + const imports = new Set([`import p5 from 'p5';`]); + + // Check for dependencies + const hasColorDependency = items.some(item => { + const typeName = item.type?.name; + const desc = extractDescription(item.description); + return typeName === 'Color' || (typeof desc === 'string' && desc.includes('Color')); + }); + + const hasVectorDependency = items.some(item => { + const typeName = item.type?.name; + const desc = extractDescription(item.description); + return typeName === 'Vector' || (typeof desc === 'string' && desc.includes('Vector')); + }); + + const hasConstantsDependency = items.some(item => + item.tags?.some(tag => tag.title === 'requires' && tag.description === 'constants') + ); + + if (hasColorDependency) { + imports.add(`import { Color } from '../color/p5.Color';`); + } + if (hasVectorDependency) { + imports.add(`import { Vector } from '../math/p5.Vector';`); + } + if (hasConstantsDependency) { + imports.add(`import * as constants from '../core/constants';`); + } + + output += Array.from(imports).join('\n') + '\n\n'; + + // Get module name + const moduleName = getModuleInfo(items[0]).module; + + // Begin module declaration + output += `declare module '${moduleName}' {\n`; + + // Find the class documentation if it exists + const classDoc = items.find(item => item.kind === 'class'); + if (classDoc) { + const fullClassName = normalizeClassName(classDoc.name); + const classDocName = fullClassName.replace('p5.', ''); + let parentClass = classDoc.tags?.find(tag => tag.title === 'extends')?.name; + if (parentClass) { + parentClass = parentClass.replace('p5.', ''); + } + const extendsClause = parentClass ? ` extends ${parentClass}` : ''; + + // Start class declaration + output += ` class ${classDocName}${extendsClause} {\n`; + + // Add constructor if there are parameters + if (classDoc.params?.length > 0) { + output += ' constructor('; + output += classDoc.params + .map(param => generateParamDeclaration(param)) + .join(', '); + output += ');\n\n'; + } + + // Get all class items for this class + const classItems = organizedData.classitems.filter(item => + item.class === fullClassName || + item.class === fullClassName.replace('p5.', '') + ); + + // Separate static and instance members + const staticItems = classItems.filter(item => item.isStatic); + const instanceItems = classItems.filter(item => !item.isStatic); + // Generate static members + staticItems.forEach(item => { + output += generateMethodDeclarations(item, true); + }); + // Generate instance members + instanceItems.forEach(item => { + output += generateMethodDeclarations(item, false); + }); + output += ' }\n\n'; + } + + // Add remaining items that aren't part of the class + items.forEach(item => { + if (item.kind !== 'class' && (!item.memberof || item.memberof !== classDoc?.name)) { + switch (item.kind) { + case 'function': + output += generateFunctionDeclaration(item); + break; + case 'constant': + case 'typedef': + const constData = organizedData.consts[item.name]; + if (constData) { + if (constData.description) { + output += ` /**\n * ${constData.description}\n */\n`; + } + if (constData.kind === 'constant') { + output += ` const ${constData.name}: ${constData.type};\n\n`; + } else { + output += ` type ${constData.name} = ${constData.type};\n\n`; + } + } + break; + } + } + }); + + // Close module declaration + output += '}\n\n'; + + // Add default export + const exportName = path.basename(filePath, '.js').replace('.', '_'); + output += `export default function ${exportName}(p5: any, fn: any): void;\n`; + + return output; +} + +// Group items by file +function groupByFile(items) { + const fileGroups = new Map(); + + items.forEach(item => { + if (!item.context || !item.context.file) return; + + const filePath = item.context.file; + if (!fileGroups.has(filePath)) { + fileGroups.set(filePath, []); + } + fileGroups.get(filePath).push(item); + }); + + return fileGroups; +} + +// Main function to generate all declaration files +function generateAllDeclarationFiles() { + // Organize all data first + const organizedData = organizeData(data); + + // Group items by file + const fileGroups = groupByFile(getAllEntries(data)); + + fileGroups.forEach((items, filePath) => { + // Convert the file path to a .d.ts path + const parsedPath = path.parse(filePath); + const relativePath = path.relative(process.cwd(), filePath); + const dtsPath = path.join( + path.dirname(relativePath), + `${parsedPath.name}.d.ts` + ); + + // Generate the declaration file content + const declarationContent = generateDeclarationFile(items, filePath, organizedData); + + // Create directory if it doesn't exist + fs.mkdirSync(path.dirname(dtsPath), { recursive: true }); + + // Write the declaration file + fs.writeFileSync(dtsPath, declarationContent, 'utf8'); + + console.log(`Generated ${dtsPath}`); + }); +} + +// Run the generator +generateAllDeclarationFiles(); \ No newline at end of file diff --git a/utils/patch.js b/utils/patch.js new file mode 100644 index 0000000000..2231e86361 --- /dev/null +++ b/utils/patch.js @@ -0,0 +1,47 @@ +const fs = require('fs'); + +const replace = (path, src, dest) => { + try { + const data = fs + .readFileSync(path, { encoding: 'utf-8' }) + .replace(src, dest); + fs.writeFileSync(path, data); + } catch (err) { + console.error(err); + } +}; + +replace( + "./src/core/structure.d.ts", + "function p5(sketch: object, node: string | HTMLElement): void;", + "function p5: typeof p5" +); + +replace( + "./src/webgl/p5.Geometry.d.ts", + "constructor(detailX?: number, detailY?: number, callback?: function);", + `constructor( + detailX?: number, + detailY?: number, + callback?: (this: { + detailY: number, + detailX: number, + vertices: p5.Vector[], + uvs: number[] + }) => void);` +); + +// https://github.com/p5-types/p5.ts/issues/31 +replace( + "./src/math/random.d.ts", + "function random(choices: Array): any;", + "function random(choices: T[]): T;" +); + +replace( + "./src/utilities/array_functions.d.ts", + "function append(array: Array, value: Any): Array;", + "function append(array: T[], value: T): T[];" +); + +