From 09eed169d54aefdea8ca41f34860a815eefd8f03 Mon Sep 17 00:00:00 2001 From: Pierre Krafft Date: Sat, 21 Oct 2017 02:24:31 +0200 Subject: [PATCH 1/9] Generate TypeScript definitions Uses the code by @toolness from http://processing.toolness.org/general/2016/03/16/typescript-to-the-rescue.html --- Gruntfile.js | 7 +- package.json | 5 +- tasks/typescript/emit.js | 81 +++++ .../generate-typescript-annotations.js | 288 ++++++++++++++++++ tasks/typescript/task.js | 8 + 5 files changed, 387 insertions(+), 2 deletions(-) create mode 100644 tasks/typescript/emit.js create mode 100644 tasks/typescript/generate-typescript-annotations.js create mode 100644 tasks/typescript/task.js diff --git a/Gruntfile.js b/Gruntfile.js index a1f05df498..8fc57527e9 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -383,6 +383,11 @@ module.exports = function(grunt) { // Load release task grunt.loadTasks('tasks/release'); + // Load typescript task + grunt.registerTask('typescript', function () { + require('./tasks/typescript/task.js')(grunt); + }); + // Load the external libraries used. grunt.loadNpmTasks('grunt-contrib-compress'); grunt.loadNpmTasks('grunt-contrib-connect'); @@ -426,7 +431,7 @@ module.exports = function(grunt) { 'mochaTest' ]); grunt.registerTask('test:nobuild', ['eslint:test', 'connect', 'mocha']); - grunt.registerTask('yui', ['yuidoc:prod', 'minjson']); + grunt.registerTask('yui', ['yuidoc:prod', 'minjson', 'typescript']); grunt.registerTask('yui:test', ['yuidoc:prod', 'connect', 'mocha:yui']); grunt.registerTask('default', ['test']); grunt.registerTask('saucetest', ['connect', 'saucelabs-mocha']); diff --git a/package.json b/package.json index 0520828155..cb2e98dffb 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "whatwg-fetch": "^2.0.3" }, "main": "./lib/p5.js", + "types": "./lib/p5.d.ts", "files": [ "license.txt", "lib/p5.min.js", @@ -107,7 +108,9 @@ "lib/addons/p5.sound.js", "lib/addons/p5.sound.min.js", "lib/addons/p5.dom.js", - "lib/addons/p5.dom.min.js" + "lib/addons/p5.dom.min.js", + "lib/p5.d.ts", + "lib/p5.global-mode.d.ts" ], "description": "[![Build Status](https://travis-ci.org/processing/p5.js.svg?branch=master)](https://travis-ci.org/processing/p5.js) [![npm version](https://badge.fury.io/js/p5.svg)](https://www.npmjs.com/package/p5)", "bugs": { diff --git a/tasks/typescript/emit.js b/tasks/typescript/emit.js new file mode 100644 index 0000000000..9a7c16ff41 --- /dev/null +++ b/tasks/typescript/emit.js @@ -0,0 +1,81 @@ +var fs = require('fs'); + +function shortenDescription(desc) { + var match = desc.match(/^((.|\n)+?\.)\s/); + + if (match) { + return match[1]; + } + return desc; +} + +function createEmitter(filename) { + var indentLevel = 0; + var lastText = ''; + var currentSourceFile; + var fd = fs.openSync(filename, 'w'); + + var emit = function(text) { + var indentation = []; + var finalText; + + for (var i = 0; i < indentLevel; i++) { + indentation.push(' '); + } + + finalText = indentation.join('') + text + '\n'; + fs.writeSync(fd, finalText); + + lastText = text; + }; + + emit.description = function(desc) { + if (!desc) { + return; + } + + emit.sectionBreak(); + emit('/**'); + shortenDescription(desc).split('\n').forEach(function(line) { + emit(' * ' + line); + }); + emit(' */'); + }; + + emit.setCurrentSourceFile = function(file) { + if (file !== currentSourceFile) { + currentSourceFile = file; + emit.sectionBreak(); + emit('// ' + file); + emit.sectionBreak(); + } + }; + + emit.sectionBreak = function() { + if (lastText !== '' && !/\{$/.test(lastText)) { + emit(''); + } + }; + + emit.getIndentLevel = function() { + return indentLevel; + }; + + emit.indent = function() { + indentLevel++; + }; + + emit.dedent = function() { + indentLevel--; + }; + + emit.close = function() { + fs.closeSync(fd); + }; + + emit('// This file was auto-generated. Please do not edit it.\n'); + + return emit; +} + +module.exports = createEmitter; diff --git a/tasks/typescript/generate-typescript-annotations.js b/tasks/typescript/generate-typescript-annotations.js new file mode 100644 index 0000000000..e14cea0fea --- /dev/null +++ b/tasks/typescript/generate-typescript-annotations.js @@ -0,0 +1,288 @@ +var createEmitter = require('./emit'); + +// mod is used to make yuidocs "global". It actually just calls generate() +// This design was selected to avoid rewriting the whole file from +// https://github.com/toolness/friendly-error-fellowship/blob/2093aee2acc53f0885fcad252a170e17af19682a/experiments/typescript/generate-typescript-annotations.js +function mod(yuidocs, localFileame, globalFilename) { + + // http://stackoverflow.com/a/2008353/2422398 + var JS_SYMBOL_RE = /^[$A-Z_][0-9A-Z_$]*$/i; + + var P5_CLASS_RE = /^p5\.([^.]+)$/; + + var P5_ALIASES = [ + 'p5', + // These are supposedly "classes" in our docs, but they don't exist + // as objects, and their methods are all defined on p5. + 'p5.dom', + 'p5.sound' + ]; + + var YUIDOC_TO_TYPESCRIPT_PARAM_MAP = { + // TODO: Not sure if there's a better type for generic Objects... + 'Object': 'any', + 'Any': 'any', + 'Number': 'number', + 'Integer': 'number', + 'String': 'string', + 'Array': 'any[]', + 'Boolean': 'boolean', + 'P5': 'p5', + // TODO: Not sure if there's a better type for functions. TypeScript's + // spec seems to mention something called "wildcard function types" + // here: https://github.com/Microsoft/TypeScript/issues/3970 + 'Function': '() => any', + }; + + function getClassitems(className) { + return yuidocs.classitems.filter(function(classitem) { + // Note that we check for classitem.name because some methods + // don't appear to define them... Filed this as + // https://github.com/processing/p5.js/issues/1252. + return classitem.class === className && classitem.name; + }); + } + + function isValidP5ClassName(className) { + return P5_CLASS_RE.test(className) && className in yuidocs.classes; + } + + function validateType(type) { + var subtypes = type.split('|'); + var subtype; + + for (var i = 0; i < subtypes.length; i++) { + subtype = subtypes[i]; + if (subtype in YUIDOC_TO_TYPESCRIPT_PARAM_MAP || + isValidP5ClassName(subtype)) { + continue; + } + return false; + } + + return true; + } + + function validateMethod(classitem) { + var errors = []; + var paramNames = {}; + var optionalParamFound = false; + + if (!classitem.is_constructor && !JS_SYMBOL_RE.test(classitem.name)) { + errors.push('"' + classitem.name + '" is not a valid JS symbol name'); + } + + (classitem.params || []).forEach(function(param) { + if (param.optional) { + optionalParamFound = true; + } else if (optionalParamFound) { + errors.push('required param "' + param.name + '" follows an ' + + 'optional param'); + } + + if (param.name in paramNames) { + errors.push('param "' + param.name + '" is defined multiple times'); + } + paramNames[param.name] = true; + + if (param.name === 'class') { + errors.push('param "' + param.name + '" is a reserved word in JS'); + } + + if (!JS_SYMBOL_RE.test(param.name)) { + errors.push('param "' + param.name + + '" is not a valid JS symbol name'); + } + + if (!validateType(param.type)) { + errors.push('param "' + param.name + '" has invalid type: ' + + param.type); + } + }); + + if (classitem.return && !validateType(classitem.return.type)) { + errors.push('return has invalid type: ' + classitem.return.type); + } + + return errors; + } + + function translateType(type) { + return type.split('|').map(function(subtype) { + if (subtype in YUIDOC_TO_TYPESCRIPT_PARAM_MAP) { + return YUIDOC_TO_TYPESCRIPT_PARAM_MAP[subtype]; + } + return subtype; + }).join('|'); + } + + function translateParam(param) { + return param.name + (param.optional ? '?' : '') + ': ' + + translateType(param.type); + } + + function generateClassMethod(emit, className, classitem) { + var errors = validateMethod(classitem); + var params = (classitem.params || []).map(translateParam); + var returnType = classitem.return ? translateType(classitem.return.type) : 'void'; + var decl; + + if (classitem.is_constructor) { + decl = 'constructor(' + params.join(', ') + ')'; + } else { + decl = (classitem.static ? 'static ' : '') + classitem.name + '(' + + params.join(', ') + '): ' + returnType; + } + + if (emit.getIndentLevel() === 0) { + decl = 'declare function ' + decl + ';'; + } + + if (errors.length) { + emit.sectionBreak(); + emit('// TODO: Fix ' + classitem.name + '() errors in ' + + classitem.file + ':'); + emit('//'); + errors.forEach(function(error) { + emit('// ' + error); + }); + emit('//'); + emit('// ' + decl); + emit(''); + } else { + emit.description(classitem.description); + emit(decl); + } + } + + function generateClassConstructor(emit, className) { + var classitem = yuidocs.classes[className]; + + if (!classitem.is_constructor) { + throw new Error(className + ' is not a constructor'); + } + + generateClassMethod(emit, className, classitem); + } + + function generateClassProperty(emit, className, classitem) { + var decl; + + if (JS_SYMBOL_RE.test(classitem.name)) { + // TODO: It seems our properties don't carry any type information, + // which is unfortunate. YUIDocs supports the @type tag on properties, + // and even encourages using it, but we don't seem to use it. + decl = classitem.name + ': any'; + + emit.description(classitem.description); + + if (emit.getIndentLevel() === 0) { + emit('declare var ' + decl + ';'); + } else { + emit(decl); + } + } else { + emit.sectionBreak(); + emit('// TODO: Property "' + classitem.name + + '", defined in ' + classitem.file + + ', is not a valid JS symbol name'); + emit.sectionBreak(); + } + } + + function generateClassProperties(emit, className) { + getClassitems(className).forEach(function(classitem) { + emit.setCurrentSourceFile(classitem.file); + if (classitem.itemtype === 'method') { + generateClassMethod(emit, className, classitem); + } else if (classitem.itemtype === 'property') { + generateClassProperty(emit, className, classitem); + } else { + emit('// TODO: Annotate ' + classitem.itemtype + ' "' + + classitem.name + '"'); + } + }); + } + + function generateP5Properties(emit, className) { + emit.sectionBreak(); + emit('// Properties from ' + className); + emit.sectionBreak(); + + generateClassProperties(emit, className); + } + + function generateP5Subclass(emit, className) { + var info = yuidocs.classes[className]; + var nestedClassName = className.match(P5_CLASS_RE)[1]; + + emit.setCurrentSourceFile(info.file); + + emit('class ' + nestedClassName + + (info.extends ? ' extends ' + info.extends : '') + ' {'); + emit.indent(); + + generateClassConstructor(emit, className); + generateClassProperties(emit, className); + + emit.dedent(); + emit('}'); + } + + function emitLocal(p5Aliases, p5Subclasses) { + var emit = createEmitter(localFileame); + + emit('declare class p5 {'); + emit.indent(); + + p5Aliases.forEach(generateP5Properties.bind(undefined, emit)); + + emit.dedent(); + emit('}\n'); + + emit('declare namespace p5 {'); + emit.indent(); + + p5Subclasses.forEach(generateP5Subclass.bind(undefined, emit)); + + emit.dedent(); + emit('}\n'); + + emit.close(); + } + + function emitGlobal(p5Aliases) { + var emit = createEmitter(globalFilename); + + emit('///\n'); + + p5Aliases.forEach(generateP5Properties.bind(undefined, emit)); + + emit.close(); + } + + function generate() { + var p5Aliases = []; + var p5Subclasses = []; + + Object.keys(yuidocs.classes).forEach(function(className) { + if (P5_ALIASES.indexOf(className) !== -1) { + p5Aliases.push(className); + } else if (P5_CLASS_RE.test(className)) { + p5Subclasses.push(className); + } else { + throw new Error(className + ' is documented as a class but ' + + 'I\'m not sure how to generate a type definition ' + + 'for it. I expect ' + className + ' to look like with p5.NAME'); + } + }); + + emitLocal(p5Aliases, p5Subclasses); + emitGlobal(p5Aliases); + } + + generate(); +} + + +module.exports = mod; diff --git a/tasks/typescript/task.js b/tasks/typescript/task.js new file mode 100644 index 0000000000..baff3abd37 --- /dev/null +++ b/tasks/typescript/task.js @@ -0,0 +1,8 @@ +var generate = require('./generate-typescript-annotations'); +var path = require('path'); + +module.exports = function(grunt) { + var yuidocs = require('../../docs/reference/data.json'); + var base = path.join(__dirname, '../../lib'); + generate(yuidocs, path.join(base, 'p5.d.ts'), path.join(base, 'p5.global-mode.d.ts')); +}; From 3c1dfd0198eb45d3ee1cc332463476e9c4c9f425 Mon Sep 17 00:00:00 2001 From: Spongman Date: Tue, 24 Oct 2017 18:33:14 -0700 Subject: [PATCH 2/9] additional typescript generation --- package.json | 4 +- tasks/typescript/emit.js | 10 +- .../generate-typescript-annotations.js | 332 +++++++++++++----- 3 files changed, 253 insertions(+), 93 deletions(-) diff --git a/package.json b/package.json index cb2e98dffb..9c27e22355 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "grunt-release-it": "^1.0.1", "grunt-saucelabs": "8.6.1", "grunt-update-json": "^0.2.1", + "html2plaintext": "^1.1.1", "husky": "^0.14.3", "jscs-stylish": "^0.3.1", "karma": "^1.7.1", @@ -87,7 +88,8 @@ "mocha": "^3.2.0", "phantomjs": "^2.1.7", "prettier": "^1.7.4", - "request": "^2.81.0" + "request": "^2.81.0", + "word-wrap": "^1.2.3" }, "license": "LGPL-2.1", "dependencies": { diff --git a/tasks/typescript/emit.js b/tasks/typescript/emit.js index 9a7c16ff41..aa4f964182 100644 --- a/tasks/typescript/emit.js +++ b/tasks/typescript/emit.js @@ -1,12 +1,12 @@ var fs = require('fs'); +var h2p = require('html2plaintext'); +var wrap = require('word-wrap'); function shortenDescription(desc) { - var match = desc.match(/^((.|\n)+?\.)\s/); - if (match) { - return match[1]; - } - return desc; + return wrap(h2p(desc), { + width: 50, + }); } function createEmitter(filename) { diff --git a/tasks/typescript/generate-typescript-annotations.js b/tasks/typescript/generate-typescript-annotations.js index e14cea0fea..ef4ebf26b5 100644 --- a/tasks/typescript/generate-typescript-annotations.js +++ b/tasks/typescript/generate-typescript-annotations.js @@ -1,9 +1,14 @@ -var createEmitter = require('./emit'); +/// @ts-check +const createEmitter = require('./emit'); // mod is used to make yuidocs "global". It actually just calls generate() // This design was selected to avoid rewriting the whole file from // https://github.com/toolness/friendly-error-fellowship/blob/2093aee2acc53f0885fcad252a170e17af19682a/experiments/typescript/generate-typescript-annotations.js -function mod(yuidocs, localFileame, globalFilename) { +function mod(yuidocs, localFileame, globalFilename, sourcePath) { + + var emit; + var constants = {}; + var missingTypes = {}; // http://stackoverflow.com/a/2008353/2422398 var JS_SYMBOL_RE = /^[$A-Z_][0-9A-Z_$]*$/i; @@ -18,6 +23,17 @@ function mod(yuidocs, localFileame, globalFilename) { 'p5.sound' ]; + var EXTERNAL_TYPES = [ + 'HTMLCanvasElement', + 'Float32Array', + 'AudioParam', + 'AudioNode', + 'GainNode', + 'DelayNode', + 'ConvolverNode', + 'Event' + ]; + var YUIDOC_TO_TYPESCRIPT_PARAM_MAP = { // TODO: Not sure if there's a better type for generic Objects... 'Object': 'any', @@ -25,8 +41,14 @@ function mod(yuidocs, localFileame, globalFilename) { 'Number': 'number', 'Integer': 'number', 'String': 'string', + 'Constant': 'any', + //'Color': 'number', + 'undefined': 'undefined', + 'Null': 'null', 'Array': 'any[]', 'Boolean': 'boolean', + '*': 'any', + 'Void': 'void', 'P5': 'p5', // TODO: Not sure if there's a better type for functions. TypeScript's // spec seems to mention something called "wildcard function types" @@ -35,7 +57,7 @@ function mod(yuidocs, localFileame, globalFilename) { }; function getClassitems(className) { - return yuidocs.classitems.filter(function(classitem) { + return yuidocs.classitems.filter(function (classitem) { // Note that we check for classitem.name because some methods // don't appear to define them... Filed this as // https://github.com/processing/p5.js/issues/1252. @@ -44,26 +66,18 @@ function mod(yuidocs, localFileame, globalFilename) { } function isValidP5ClassName(className) { - return P5_CLASS_RE.test(className) && className in yuidocs.classes; + return P5_CLASS_RE.test(className) && className in yuidocs.classes || + P5_CLASS_RE.test('p5.' + className) && ('p5.' + className) in yuidocs.classes; } + /** + * @param {string} type + */ function validateType(type) { - var subtypes = type.split('|'); - var subtype; - - for (var i = 0; i < subtypes.length; i++) { - subtype = subtypes[i]; - if (subtype in YUIDOC_TO_TYPESCRIPT_PARAM_MAP || - isValidP5ClassName(subtype)) { - continue; - } - return false; - } - - return true; + return translateType(type); } - function validateMethod(classitem) { + function validateMethod(classitem, overload) { var errors = []; var paramNames = {}; var optionalParamFound = false; @@ -72,12 +86,12 @@ function mod(yuidocs, localFileame, globalFilename) { errors.push('"' + classitem.name + '" is not a valid JS symbol name'); } - (classitem.params || []).forEach(function(param) { + (overload.params || []).forEach(function (param) { if (param.optional) { optionalParamFound = true; } else if (optionalParamFound) { errors.push('required param "' + param.name + '" follows an ' + - 'optional param'); + 'optional param'); } if (param.name in paramNames) { @@ -85,53 +99,154 @@ function mod(yuidocs, localFileame, globalFilename) { } paramNames[param.name] = true; + /* if (param.name === 'class') { errors.push('param "' + param.name + '" is a reserved word in JS'); } + */ if (!JS_SYMBOL_RE.test(param.name)) { errors.push('param "' + param.name + - '" is not a valid JS symbol name'); + '" is not a valid JS symbol name'); } if (!validateType(param.type)) { errors.push('param "' + param.name + '" has invalid type: ' + - param.type); + param.type); + } + + if (param.type === 'Constant') { + + var constantRe = /either\s+(?:[A-Z0-9_]+\s*,?\s*(?:or)?\s*)+/g; + var execResult = constantRe.exec(param.description); + var match; + if (execResult) { + match = execResult[0]; + } + if (classitem.name === 'endShape' && param.name === 'mode') { + match = 'CLOSE'; + } + if (match) { + + var values = []; + + var reConst = /[A-Z0-9_]+/g; + var matchConst; + while ((matchConst = reConst.exec(match)) !== null) { + values.push(matchConst); + } + var paramWords = param.name.split('.').pop().replace(/([A-Z])/g, ' $1').trim().toLowerCase().split(' '); + var propWords = classitem.name.split('.').pop().replace(/([A-Z])/g, ' $1').trim().toLowerCase().split(' '); + + var constName; + if (paramWords.length > 1 || propWords[0] === 'create') { + constName = paramWords.join('_'); + } else if (propWords[propWords.length - 1] === paramWords[paramWords.length - 1]) { + constName = propWords.join('_'); + } else { + constName = (propWords[0] + '_' + paramWords[paramWords.length - 1]); + } + + constName = constName.toUpperCase(); + constants[constName] = values; + + param.type = constName; + } } }); - if (classitem.return && !validateType(classitem.return.type)) { - errors.push('return has invalid type: ' + classitem.return.type); + if (overload.return && !validateType(overload.return.type)) { + errors.push('return has invalid type: ' + overload.return.type); } return errors; } - function translateType(type) { - return type.split('|').map(function(subtype) { - if (subtype in YUIDOC_TO_TYPESCRIPT_PARAM_MAP) { - return YUIDOC_TO_TYPESCRIPT_PARAM_MAP[subtype]; - } - return subtype; - }).join('|'); + /** + * + * @param {string} type + * @param {string} [defaultType] + */ + function translateType(type, defaultType) { + if (type === void 0) { + return defaultType; + } + + if (type === '') { + return ''; + } + + if (type.length > 2 && type.substring(type.length - 2) === '[]') { + return translateType(type.substr(0, type.length - 2), defaultType) + '[]'; + } + + type = type.trim(); + + var matchFunction = type.match(/Function\(([^)]*)\)/i); + if (matchFunction) { + var paramTypes = matchFunction[1].split(','); + return '(' + paramTypes.map((t, i) => 'p' + (i + 1) + ':' + translateType(t, 'any')).join(',') + ') => any'; + } + + var parts = type.split('|'); + if (parts.length > 1) { + return parts.map(t => translateType(t, defaultType)).join('|'); + } + + if (type in YUIDOC_TO_TYPESCRIPT_PARAM_MAP) { + return YUIDOC_TO_TYPESCRIPT_PARAM_MAP[type]; + } + + if (EXTERNAL_TYPES.indexOf(type) >= 0) { + return type; + } + + if (isValidP5ClassName(type)) { + return type; + } + + if (constants[type]) { + return type; + } + + missingTypes[type] = true; + return defaultType; } function translateParam(param) { - return param.name + (param.optional ? '?' : '') + ': ' + - translateType(param.type); + var name = param.name; + if (name === 'class') { + name = 'theClass'; + } + + return name + (param.optional ? '?' : '') + ': ' + translateType(param.type, 'any'); } - function generateClassMethod(emit, className, classitem) { - var errors = validateMethod(classitem); - var params = (classitem.params || []).map(translateParam); - var returnType = classitem.return ? translateType(classitem.return.type) : 'void'; + function generateClassMethod(className, classitem) { + if (classitem.overloads) { + classitem.overloads.forEach(function (overload) { + generateClassMethodWithParams(className, classitem, overload); + }); + } + else { + generateClassMethodWithParams(className, classitem, classitem); + } + } + + + function generateClassMethodWithParams(className, classitem, overload) { + var errors = validateMethod(classitem, overload); + var params = (overload.params || []).map(translateParam); + var returnType = overload.chainable ? className + : overload.return ? translateType(overload.return.type, 'any') + : 'void'; var decl; if (classitem.is_constructor) { decl = 'constructor(' + params.join(', ') + ')'; } else { - decl = (classitem.static ? 'static ' : '') + classitem.name + '(' + - params.join(', ') + '): ' + returnType; + decl = (overload.static ? 'static ' : '') + classitem.name + '(' + + params.join(', ') + '): ' + returnType; } if (emit.getIndentLevel() === 0) { @@ -141,9 +256,10 @@ function mod(yuidocs, localFileame, globalFilename) { if (errors.length) { emit.sectionBreak(); emit('// TODO: Fix ' + classitem.name + '() errors in ' + - classitem.file + ':'); + classitem.file + ', line ' + overload.line + ':'); emit('//'); - errors.forEach(function(error) { + errors.forEach(function (error) { + console.log(classitem.file + ':' + overload.line + ', ' + error); emit('// ' + error); }); emit('//'); @@ -155,87 +271,123 @@ function mod(yuidocs, localFileame, globalFilename) { } } - function generateClassConstructor(emit, className) { + function generateClassConstructor(className) { var classitem = yuidocs.classes[className]; if (!classitem.is_constructor) { throw new Error(className + ' is not a constructor'); } - generateClassMethod(emit, className, classitem); + generateClassMethod(className, classitem); } - function generateClassProperty(emit, className, classitem) { - var decl; - + function generateClassProperty(className, classitem) { if (JS_SYMBOL_RE.test(classitem.name)) { // TODO: It seems our properties don't carry any type information, // which is unfortunate. YUIDocs supports the @type tag on properties, // and even encourages using it, but we don't seem to use it. - decl = classitem.name + ': any'; + var translatedType = translateType(classitem.type, 'any'); + var defaultValue = classitem.default; + if (classitem.final && translatedType === 'string' && !defaultValue) { + defaultValue = classitem.name.toLowerCase().replace(/_/g, '-'); + } + + var decl; + if (defaultValue) { + decl = classitem.name + ': '; + if (translatedType === 'string') { + decl += '\'' + defaultValue.replace(/'/g, '\\\'') + '\''; + } + else { + decl += defaultValue; + } + } else { + decl = classitem.name + ': ' + translatedType; + } + emit.description(classitem.description); if (emit.getIndentLevel() === 0) { - emit('declare var ' + decl + ';'); + emit('declare ' + (classitem.final ? 'const ' : 'var ') + decl + ';'); } else { + if (classitem.final) { + return; + } emit(decl); } + } else { emit.sectionBreak(); emit('// TODO: Property "' + classitem.name + - '", defined in ' + classitem.file + - ', is not a valid JS symbol name'); + '", defined in ' + classitem.file + + ', is not a valid JS symbol name'); emit.sectionBreak(); } } - function generateClassProperties(emit, className) { - getClassitems(className).forEach(function(classitem) { + function generateClassProperties(className) { + getClassitems(className).forEach(function (classitem) { + classitem.file = classitem.file.replace(/\\/g, '/'); emit.setCurrentSourceFile(classitem.file); if (classitem.itemtype === 'method') { - generateClassMethod(emit, className, classitem); + generateClassMethod(className, classitem); } else if (classitem.itemtype === 'property') { - generateClassProperty(emit, className, classitem); + generateClassProperty(className, classitem); } else { emit('// TODO: Annotate ' + classitem.itemtype + ' "' + - classitem.name + '"'); + classitem.name + '"'); } }); } - function generateP5Properties(emit, className) { + function generateP5Properties(className) { emit.sectionBreak(); emit('// Properties from ' + className); emit.sectionBreak(); - generateClassProperties(emit, className); + generateClassProperties(className); } - function generateP5Subclass(emit, className) { + function generateP5Subclass(className) { var info = yuidocs.classes[className]; var nestedClassName = className.match(P5_CLASS_RE)[1]; + info.file = info.file.replace(/\\/g, '/'); emit.setCurrentSourceFile(info.file); emit('class ' + nestedClassName + - (info.extends ? ' extends ' + info.extends : '') + ' {'); + (info.extends ? ' extends ' + info.extends : '') + ' {'); emit.indent(); - generateClassConstructor(emit, className); - generateClassProperties(emit, className); + generateClassConstructor(className); + generateClassProperties(className); emit.dedent(); emit('}'); } - function emitLocal(p5Aliases, p5Subclasses) { - var emit = createEmitter(localFileame); + function generate() { + var p5Aliases = []; + var p5Subclasses = []; + + Object.keys(yuidocs.classes).forEach(function (className) { + if (P5_ALIASES.indexOf(className) !== -1) { + p5Aliases.push(className); + } else if (P5_CLASS_RE.test(className)) { + p5Subclasses.push(className); + } else { + throw new Error(className + ' is documented as a class but ' + + 'I\'m not sure how to generate a type definition for it'); + } + }); + + emit = createEmitter(localFileame); emit('declare class p5 {'); emit.indent(); - p5Aliases.forEach(generateP5Properties.bind(undefined, emit)); + p5Aliases.forEach(generateP5Properties); emit.dedent(); emit('}\n'); @@ -243,46 +395,52 @@ function mod(yuidocs, localFileame, globalFilename) { emit('declare namespace p5 {'); emit.indent(); - p5Subclasses.forEach(generateP5Subclass.bind(undefined, emit)); + p5Subclasses.forEach(generateP5Subclass); emit.dedent(); emit('}\n'); emit.close(); - } - function emitGlobal(p5Aliases) { - var emit = createEmitter(globalFilename); + emit = createEmitter(globalFilename); emit('///\n'); - p5Aliases.forEach(generateP5Properties.bind(undefined, emit)); + p5Aliases.forEach(generateP5Properties); - emit.close(); - } + emit('// Constants '); + Object.keys(constants).forEach(function (key) { + var values = constants[key]; - function generate() { - var p5Aliases = []; - var p5Subclasses = []; + /* + emit('// ' + key); + values.forEach(function (v) { + emit('declare const ' + v + ': string;'); + }); + */ + + emit('type ' + key + ' ='); + values.forEach(function (v, i) { + var str = ' typeof ' + v; + str = (i ? '|' : ' ') + str; + if (i === values.length - 1) { + str += ';'; + } + emit(' ' + str); + }); + + emit(''); - Object.keys(yuidocs.classes).forEach(function(className) { - if (P5_ALIASES.indexOf(className) !== -1) { - p5Aliases.push(className); - } else if (P5_CLASS_RE.test(className)) { - p5Subclasses.push(className); - } else { - throw new Error(className + ' is documented as a class but ' + - 'I\'m not sure how to generate a type definition ' + - 'for it. I expect ' + className + ' to look like with p5.NAME'); - } }); - emitLocal(p5Aliases, p5Subclasses); - emitGlobal(p5Aliases); + emit.close(); + + for (var t in missingTypes) { + console.log('MISSING: ', t); + } } generate(); } - module.exports = mod; From b79a9cc5a12e71265451a07d39c07a784f12e4ce Mon Sep 17 00:00:00 2001 From: Spongman Date: Sun, 5 Nov 2017 00:10:47 -0700 Subject: [PATCH 3/9] add parameter/return decsriptions to doc comments --- tasks/typescript/emit.js | 45 ++++++++++++++++--- .../generate-typescript-annotations.js | 4 +- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/tasks/typescript/emit.js b/tasks/typescript/emit.js index aa4f964182..314104c25c 100644 --- a/tasks/typescript/emit.js +++ b/tasks/typescript/emit.js @@ -4,7 +4,7 @@ var wrap = require('word-wrap'); function shortenDescription(desc) { - return wrap(h2p(desc), { + return wrap(h2p(desc).replace(/[\r\n]+/, ''), { width: 50, }); } @@ -29,16 +29,51 @@ function createEmitter(filename) { lastText = text; }; - emit.description = function(desc) { + emit.description = function(classitem, overload) { + var desc = classitem.description; if (!desc) { return; } + function emitDescription(desc) { + shortenDescription(desc).split('\n').forEach(function(line) { + emit(' * ' + line); + }); + } + emit.sectionBreak(); emit('/**'); - shortenDescription(desc).split('\n').forEach(function(line) { - emit(' * ' + line); - }); + emitDescription(desc); + emit(' *'); + if (overload) { + var alloverloads = [classitem]; + if (classitem.overloads) { + alloverloads = alloverloads.concat(classitem.overloads); + } + if (overload.params) { + overload.params.forEach(function (p) { + var arg = p.name; + var p2; + for (var i = 0; !p2 && i < alloverloads.length; i ++) { + if (alloverloads[i].params) { + p2 = alloverloads[i].params.find(p3 => p3.description && p3.name === arg); + if (p2) { + if (p.optional) { + arg = '[' + arg + ']'; + } + emitDescription('@param ' + arg + ' ' + p2.description); + break; + } + } + } + }); + } + if (overload.chainable) { + emitDescription('@chainable'); + } else if (overload.return && overload.return.description) { + emitDescription('@return ' + overload.return.description); + } + } emit(' */'); }; diff --git a/tasks/typescript/generate-typescript-annotations.js b/tasks/typescript/generate-typescript-annotations.js index ef4ebf26b5..14e4f9e459 100644 --- a/tasks/typescript/generate-typescript-annotations.js +++ b/tasks/typescript/generate-typescript-annotations.js @@ -266,7 +266,7 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { emit('// ' + decl); emit(''); } else { - emit.description(classitem.description); + emit.description(classitem, overload); emit(decl); } } @@ -306,7 +306,7 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { } - emit.description(classitem.description); + emit.description(classitem); if (emit.getIndentLevel() === 0) { emit('declare ' + (classitem.final ? 'const ' : 'var ') + decl + ';'); From 256e09406b53378a2f4cccf68755f2e952df364a Mon Sep 17 00:00:00 2001 From: Pierre Krafft Date: Thu, 9 Nov 2017 22:09:13 +0100 Subject: [PATCH 4/9] Cleanup typescript generation Improves error reporting. Correctly emits readonly properties. --- .../generate-typescript-annotations.js | 127 ++++++++++-------- 1 file changed, 69 insertions(+), 58 deletions(-) diff --git a/tasks/typescript/generate-typescript-annotations.js b/tasks/typescript/generate-typescript-annotations.js index 14e4f9e459..58ddcf40e6 100644 --- a/tasks/typescript/generate-typescript-annotations.js +++ b/tasks/typescript/generate-typescript-annotations.js @@ -1,6 +1,18 @@ /// @ts-check const createEmitter = require('./emit'); +function position(file, line) { + return file + ', line ' + line; +} + +function classitemPosition(classitem) { + return position(classitem.file, classitem.line); +} + +function overloadPosition(classitem, overload) { + return position(classitem.file, overload.line); +} + // mod is used to make yuidocs "global". It actually just calls generate() // This design was selected to avoid rewriting the whole file from // https://github.com/toolness/friendly-error-fellowship/blob/2093aee2acc53f0885fcad252a170e17af19682a/experiments/typescript/generate-typescript-annotations.js @@ -23,7 +35,7 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { 'p5.sound' ]; - var EXTERNAL_TYPES = [ + var EXTERNAL_TYPES = new Set([ 'HTMLCanvasElement', 'Float32Array', 'AudioParam', @@ -32,17 +44,15 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { 'DelayNode', 'ConvolverNode', 'Event' - ]; + ]); var YUIDOC_TO_TYPESCRIPT_PARAM_MAP = { - // TODO: Not sure if there's a better type for generic Objects... - 'Object': 'any', + 'Object': 'object', 'Any': 'any', 'Number': 'number', 'Integer': 'number', 'String': 'string', 'Constant': 'any', - //'Color': 'number', 'undefined': 'undefined', 'Null': 'null', 'Array': 'any[]', @@ -50,17 +60,22 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { '*': 'any', 'Void': 'void', 'P5': 'p5', - // TODO: Not sure if there's a better type for functions. TypeScript's - // spec seems to mention something called "wildcard function types" - // here: https://github.com/Microsoft/TypeScript/issues/3970 - 'Function': '() => any', + // When the docs don't specify what kind of function we expect, + // then we need to use the global type `Function` + 'Function': 'Function', }; function getClassitems(className) { return yuidocs.classitems.filter(function (classitem) { - // Note that we check for classitem.name because some methods - // don't appear to define them... Filed this as - // https://github.com/processing/p5.js/issues/1252. + // Note that we first find items with the right class name, + // but we also check for classitem.name because + // YUIDoc includes classitems that we want to be undocumented + // just because we used block comments. + // We have other checks in place for finding missing method names + // on public methods so a missing classitem.name implies that + // the method is undocumented on purpose. + // See https://github.com/processing/p5.js/issues/1252 and + // https://github.com/processing/p5.js/pull/2301 return classitem.class === className && classitem.name; }); } @@ -82,7 +97,7 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { var paramNames = {}; var optionalParamFound = false; - if (!classitem.is_constructor && !JS_SYMBOL_RE.test(classitem.name)) { + if (!(JS_SYMBOL_RE.test(classitem.name) || classitem.is_constructor)) { errors.push('"' + classitem.name + '" is not a valid JS symbol name'); } @@ -99,12 +114,6 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { } paramNames[param.name] = true; - /* - if (param.name === 'class') { - errors.push('param "' + param.name + '" is a reserved word in JS'); - } - */ - if (!JS_SYMBOL_RE.test(param.name)) { errors.push('param "' + param.name + '" is not a valid JS symbol name'); @@ -172,6 +181,8 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { return defaultType; } + type = type.trim(); + if (type === '') { return ''; } @@ -180,12 +191,15 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { return translateType(type.substr(0, type.length - 2), defaultType) + '[]'; } - type = type.trim(); - var matchFunction = type.match(/Function\(([^)]*)\)/i); if (matchFunction) { var paramTypes = matchFunction[1].split(','); - return '(' + paramTypes.map((t, i) => 'p' + (i + 1) + ':' + translateType(t, 'any')).join(',') + ') => any'; + const mappedParamTypes = paramTypes.map((t, i) => { + const paramName = 'p' + (i + 1) + const paramType = translateType(t, 'any') + return paramName+ ': ' + paramType + }) + return '(' + mappedParamTypes.join(',') + ') => any'; } var parts = type.split('|'); @@ -193,11 +207,12 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { return parts.map(t => translateType(t, defaultType)).join('|'); } - if (type in YUIDOC_TO_TYPESCRIPT_PARAM_MAP) { - return YUIDOC_TO_TYPESCRIPT_PARAM_MAP[type]; + const staticallyMappedType = YUIDOC_TO_TYPESCRIPT_PARAM_MAP[type]; + if (staticallyMappedType != null) { + return staticallyMappedType; } - if (EXTERNAL_TYPES.indexOf(type) >= 0) { + if (EXTERNAL_TYPES.has(type)) { return type; } @@ -256,10 +271,10 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { if (errors.length) { emit.sectionBreak(); emit('// TODO: Fix ' + classitem.name + '() errors in ' + - classitem.file + ', line ' + overload.line + ':'); + overloadPosition(classitem, overload) + ':'); emit('//'); errors.forEach(function (error) { - console.log(classitem.file + ':' + overload.line + ', ' + error); + console.log( classitem.name + '() ' + overloadPosition(classitem, overload) + ', ' + error); emit('// ' + error); }); emit('//'); @@ -309,18 +324,17 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { emit.description(classitem); if (emit.getIndentLevel() === 0) { - emit('declare ' + (classitem.final ? 'const ' : 'var ') + decl + ';'); + const declarationType = (classitem.final ? 'const ' : 'var '); + emit('declare ' + declarationType + decl + ';'); } else { - if (classitem.final) { - return; - } - emit(decl); + const modifier = classitem.final ? 'readonly ' : ''; + emit(modifier + decl); } } else { emit.sectionBreak(); emit('// TODO: Property "' + classitem.name + - '", defined in ' + classitem.file + + '", defined in ' + classitemPosition(classitem) + ', is not a valid JS symbol name'); emit.sectionBreak(); } @@ -336,7 +350,7 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { generateClassProperty(className, classitem); } else { emit('// TODO: Annotate ' + classitem.itemtype + ' "' + - classitem.name + '"'); + classitem.name + '", defined in ' + classitemPosition(classitem)); } }); } @@ -367,6 +381,26 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { emit('}'); } + function emitConstants() { + emit('// Constants '); + Object.keys(constants).forEach(function (key) { + var values = constants[key]; + + emit('type ' + key + ' ='); + values.forEach(function (v, i) { + var str = ' typeof ' + v; + str = (i ? '|' : ' ') + str; + if (i === values.length - 1) { + str += ';'; + } + emit(' ' + str); + }); + + emit(''); + + }); + } + function generate() { var p5Aliases = []; var p5Subclasses = []; @@ -408,30 +442,7 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { p5Aliases.forEach(generateP5Properties); - emit('// Constants '); - Object.keys(constants).forEach(function (key) { - var values = constants[key]; - - /* - emit('// ' + key); - values.forEach(function (v) { - emit('declare const ' + v + ': string;'); - }); - */ - - emit('type ' + key + ' ='); - values.forEach(function (v, i) { - var str = ' typeof ' + v; - str = (i ? '|' : ' ') + str; - if (i === values.length - 1) { - str += ';'; - } - emit(' ' + str); - }); - - emit(''); - - }); + emitConstants(); emit.close(); From f1ffb7de681c2763cc04e67800dada2ddc460da1 Mon Sep 17 00:00:00 2001 From: Pierre Krafft Date: Wed, 29 Nov 2017 22:11:40 +0100 Subject: [PATCH 5/9] Ignore hard to fix YUIDoc types Tone.Signal is a type from another library so we can't "see" its type. It's only used as a return from a constructor, but constructors can't have return types in TS so we don't even emit a definition for it. SoundObject is either p5.Sound or an object that has .connect(unit) (e.g. Amplitude, AudioNode). YUIDoc doesn't seem to have free-standing interfaces so expressing "has .connect" seems hard. --- tasks/typescript/generate-typescript-annotations.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tasks/typescript/generate-typescript-annotations.js b/tasks/typescript/generate-typescript-annotations.js index 58ddcf40e6..945a5388ca 100644 --- a/tasks/typescript/generate-typescript-annotations.js +++ b/tasks/typescript/generate-typescript-annotations.js @@ -63,6 +63,9 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { // When the docs don't specify what kind of function we expect, // then we need to use the global type `Function` 'Function': 'Function', + // Special ignore for hard to fix YUIDoc from p5.sound + 'Tone.Signal': 'any', + 'SoundObject': 'any', }; function getClassitems(className) { From e8be5d03794d262d21eeafdc388e137676202a27 Mon Sep 17 00:00:00 2001 From: Pierre Krafft Date: Thu, 30 Nov 2017 23:17:28 +0100 Subject: [PATCH 6/9] prettify --- Gruntfile.js | 2 +- tasks/typescript/emit.js | 19 +- .../generate-typescript-annotations.js | 186 +++++++++++------- tasks/typescript/task.js | 6 +- 4 files changed, 134 insertions(+), 79 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 8fc57527e9..ffbc4bcca1 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -384,7 +384,7 @@ module.exports = function(grunt) { grunt.loadTasks('tasks/release'); // Load typescript task - grunt.registerTask('typescript', function () { + grunt.registerTask('typescript', function() { require('./tasks/typescript/task.js')(grunt); }); diff --git a/tasks/typescript/emit.js b/tasks/typescript/emit.js index 314104c25c..861b555a1e 100644 --- a/tasks/typescript/emit.js +++ b/tasks/typescript/emit.js @@ -3,9 +3,8 @@ var h2p = require('html2plaintext'); var wrap = require('word-wrap'); function shortenDescription(desc) { - return wrap(h2p(desc).replace(/[\r\n]+/, ''), { - width: 50, + width: 50 }); } @@ -36,9 +35,11 @@ function createEmitter(filename) { } function emitDescription(desc) { - shortenDescription(desc).split('\n').forEach(function(line) { - emit(' * ' + line); - }); + shortenDescription(desc) + .split('\n') + .forEach(function(line) { + emit(' * ' + line); + }); } emit.sectionBreak(); @@ -51,12 +52,14 @@ function createEmitter(filename) { alloverloads = alloverloads.concat(classitem.overloads); } if (overload.params) { - overload.params.forEach(function (p) { + overload.params.forEach(function(p) { var arg = p.name; var p2; - for (var i = 0; !p2 && i < alloverloads.length; i ++) { + for (var i = 0; !p2 && i < alloverloads.length; i++) { if (alloverloads[i].params) { - p2 = alloverloads[i].params.find(p3 => p3.description && p3.name === arg); + p2 = alloverloads[i].params.find( + p3 => p3.description && p3.name === arg + ); if (p2) { if (p.optional) { arg = '[' + arg + ']'; diff --git a/tasks/typescript/generate-typescript-annotations.js b/tasks/typescript/generate-typescript-annotations.js index 945a5388ca..0046ce613e 100644 --- a/tasks/typescript/generate-typescript-annotations.js +++ b/tasks/typescript/generate-typescript-annotations.js @@ -17,7 +17,6 @@ function overloadPosition(classitem, overload) { // This design was selected to avoid rewriting the whole file from // https://github.com/toolness/friendly-error-fellowship/blob/2093aee2acc53f0885fcad252a170e17af19682a/experiments/typescript/generate-typescript-annotations.js function mod(yuidocs, localFileame, globalFilename, sourcePath) { - var emit; var constants = {}; var missingTypes = {}; @@ -47,29 +46,29 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { ]); var YUIDOC_TO_TYPESCRIPT_PARAM_MAP = { - 'Object': 'object', - 'Any': 'any', - 'Number': 'number', - 'Integer': 'number', - 'String': 'string', - 'Constant': 'any', - 'undefined': 'undefined', - 'Null': 'null', - 'Array': 'any[]', - 'Boolean': 'boolean', + Object: 'object', + Any: 'any', + Number: 'number', + Integer: 'number', + String: 'string', + Constant: 'any', + undefined: 'undefined', + Null: 'null', + Array: 'any[]', + Boolean: 'boolean', '*': 'any', - 'Void': 'void', - 'P5': 'p5', + Void: 'void', + P5: 'p5', // When the docs don't specify what kind of function we expect, // then we need to use the global type `Function` - 'Function': 'Function', + Function: 'Function', // Special ignore for hard to fix YUIDoc from p5.sound 'Tone.Signal': 'any', - 'SoundObject': 'any', + SoundObject: 'any' }; function getClassitems(className) { - return yuidocs.classitems.filter(function (classitem) { + return yuidocs.classitems.filter(function(classitem) { // Note that we first find items with the right class name, // but we also check for classitem.name because // YUIDoc includes classitems that we want to be undocumented @@ -84,8 +83,11 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { } function isValidP5ClassName(className) { - return P5_CLASS_RE.test(className) && className in yuidocs.classes || - P5_CLASS_RE.test('p5.' + className) && ('p5.' + className) in yuidocs.classes; + return ( + (P5_CLASS_RE.test(className) && className in yuidocs.classes) || + (P5_CLASS_RE.test('p5.' + className) && + 'p5.' + className in yuidocs.classes) + ); } /** @@ -104,12 +106,13 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { errors.push('"' + classitem.name + '" is not a valid JS symbol name'); } - (overload.params || []).forEach(function (param) { + (overload.params || []).forEach(function(param) { if (param.optional) { optionalParamFound = true; } else if (optionalParamFound) { - errors.push('required param "' + param.name + '" follows an ' + - 'optional param'); + errors.push( + 'required param "' + param.name + '" follows an ' + 'optional param' + ); } if (param.name in paramNames) { @@ -118,17 +121,16 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { paramNames[param.name] = true; if (!JS_SYMBOL_RE.test(param.name)) { - errors.push('param "' + param.name + - '" is not a valid JS symbol name'); + errors.push('param "' + param.name + '" is not a valid JS symbol name'); } if (!validateType(param.type)) { - errors.push('param "' + param.name + '" has invalid type: ' + - param.type); + errors.push( + 'param "' + param.name + '" has invalid type: ' + param.type + ); } if (param.type === 'Constant') { - var constantRe = /either\s+(?:[A-Z0-9_]+\s*,?\s*(?:or)?\s*)+/g; var execResult = constantRe.exec(param.description); var match; @@ -139,7 +141,6 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { match = 'CLOSE'; } if (match) { - var values = []; var reConst = /[A-Z0-9_]+/g; @@ -147,16 +148,31 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { while ((matchConst = reConst.exec(match)) !== null) { values.push(matchConst); } - var paramWords = param.name.split('.').pop().replace(/([A-Z])/g, ' $1').trim().toLowerCase().split(' '); - var propWords = classitem.name.split('.').pop().replace(/([A-Z])/g, ' $1').trim().toLowerCase().split(' '); + var paramWords = param.name + .split('.') + .pop() + .replace(/([A-Z])/g, ' $1') + .trim() + .toLowerCase() + .split(' '); + var propWords = classitem.name + .split('.') + .pop() + .replace(/([A-Z])/g, ' $1') + .trim() + .toLowerCase() + .split(' '); var constName; if (paramWords.length > 1 || propWords[0] === 'create') { constName = paramWords.join('_'); - } else if (propWords[propWords.length - 1] === paramWords[paramWords.length - 1]) { + } else if ( + propWords[propWords.length - 1] === + paramWords[paramWords.length - 1] + ) { constName = propWords.join('_'); } else { - constName = (propWords[0] + '_' + paramWords[paramWords.length - 1]); + constName = propWords[0] + '_' + paramWords[paramWords.length - 1]; } constName = constName.toUpperCase(); @@ -198,10 +214,10 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { if (matchFunction) { var paramTypes = matchFunction[1].split(','); const mappedParamTypes = paramTypes.map((t, i) => { - const paramName = 'p' + (i + 1) - const paramType = translateType(t, 'any') - return paramName+ ': ' + paramType - }) + const paramName = 'p' + (i + 1); + const paramType = translateType(t, 'any'); + return paramName + ': ' + paramType; + }); return '(' + mappedParamTypes.join(',') + ') => any'; } @@ -237,34 +253,42 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { name = 'theClass'; } - return name + (param.optional ? '?' : '') + ': ' + translateType(param.type, 'any'); + return ( + name + + (param.optional ? '?' : '') + + ': ' + + translateType(param.type, 'any') + ); } function generateClassMethod(className, classitem) { if (classitem.overloads) { - classitem.overloads.forEach(function (overload) { + classitem.overloads.forEach(function(overload) { generateClassMethodWithParams(className, classitem, overload); }); - } - else { + } else { generateClassMethodWithParams(className, classitem, classitem); } } - function generateClassMethodWithParams(className, classitem, overload) { var errors = validateMethod(classitem, overload); var params = (overload.params || []).map(translateParam); - var returnType = overload.chainable ? className - : overload.return ? translateType(overload.return.type, 'any') - : 'void'; + var returnType = overload.chainable + ? className + : overload.return ? translateType(overload.return.type, 'any') : 'void'; var decl; if (classitem.is_constructor) { decl = 'constructor(' + params.join(', ') + ')'; } else { - decl = (overload.static ? 'static ' : '') + classitem.name + '(' + - params.join(', ') + '): ' + returnType; + decl = + (overload.static ? 'static ' : '') + + classitem.name + + '(' + + params.join(', ') + + '): ' + + returnType; } if (emit.getIndentLevel() === 0) { @@ -273,11 +297,22 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { if (errors.length) { emit.sectionBreak(); - emit('// TODO: Fix ' + classitem.name + '() errors in ' + - overloadPosition(classitem, overload) + ':'); + emit( + '// TODO: Fix ' + + classitem.name + + '() errors in ' + + overloadPosition(classitem, overload) + + ':' + ); emit('//'); - errors.forEach(function (error) { - console.log( classitem.name + '() ' + overloadPosition(classitem, overload) + ', ' + error); + errors.forEach(function(error) { + console.log( + classitem.name + + '() ' + + overloadPosition(classitem, overload) + + ', ' + + error + ); emit('// ' + error); }); emit('//'); @@ -314,37 +349,38 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { if (defaultValue) { decl = classitem.name + ': '; if (translatedType === 'string') { - decl += '\'' + defaultValue.replace(/'/g, '\\\'') + '\''; - } - else { + decl += "'" + defaultValue.replace(/'/g, "\\'") + "'"; + } else { decl += defaultValue; } } else { decl = classitem.name + ': ' + translatedType; } - emit.description(classitem); if (emit.getIndentLevel() === 0) { - const declarationType = (classitem.final ? 'const ' : 'var '); + const declarationType = classitem.final ? 'const ' : 'var '; emit('declare ' + declarationType + decl + ';'); } else { const modifier = classitem.final ? 'readonly ' : ''; emit(modifier + decl); } - } else { emit.sectionBreak(); - emit('// TODO: Property "' + classitem.name + - '", defined in ' + classitemPosition(classitem) + - ', is not a valid JS symbol name'); + emit( + '// TODO: Property "' + + classitem.name + + '", defined in ' + + classitemPosition(classitem) + + ', is not a valid JS symbol name' + ); emit.sectionBreak(); } } function generateClassProperties(className) { - getClassitems(className).forEach(function (classitem) { + getClassitems(className).forEach(function(classitem) { classitem.file = classitem.file.replace(/\\/g, '/'); emit.setCurrentSourceFile(classitem.file); if (classitem.itemtype === 'method') { @@ -352,8 +388,14 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { } else if (classitem.itemtype === 'property') { generateClassProperty(className, classitem); } else { - emit('// TODO: Annotate ' + classitem.itemtype + ' "' + - classitem.name + '", defined in ' + classitemPosition(classitem)); + emit( + '// TODO: Annotate ' + + classitem.itemtype + + ' "' + + classitem.name + + '", defined in ' + + classitemPosition(classitem) + ); } }); } @@ -373,8 +415,12 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { info.file = info.file.replace(/\\/g, '/'); emit.setCurrentSourceFile(info.file); - emit('class ' + nestedClassName + - (info.extends ? ' extends ' + info.extends : '') + ' {'); + emit( + 'class ' + + nestedClassName + + (info.extends ? ' extends ' + info.extends : '') + + ' {' + ); emit.indent(); generateClassConstructor(className); @@ -386,11 +432,11 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { function emitConstants() { emit('// Constants '); - Object.keys(constants).forEach(function (key) { + Object.keys(constants).forEach(function(key) { var values = constants[key]; emit('type ' + key + ' ='); - values.forEach(function (v, i) { + values.forEach(function(v, i) { var str = ' typeof ' + v; str = (i ? '|' : ' ') + str; if (i === values.length - 1) { @@ -400,7 +446,6 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { }); emit(''); - }); } @@ -408,14 +453,17 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { var p5Aliases = []; var p5Subclasses = []; - Object.keys(yuidocs.classes).forEach(function (className) { + Object.keys(yuidocs.classes).forEach(function(className) { if (P5_ALIASES.indexOf(className) !== -1) { p5Aliases.push(className); } else if (P5_CLASS_RE.test(className)) { p5Subclasses.push(className); } else { - throw new Error(className + ' is documented as a class but ' + - 'I\'m not sure how to generate a type definition for it'); + throw new Error( + className + + ' is documented as a class but ' + + "I'm not sure how to generate a type definition for it" + ); } }); diff --git a/tasks/typescript/task.js b/tasks/typescript/task.js index baff3abd37..f5ef564a71 100644 --- a/tasks/typescript/task.js +++ b/tasks/typescript/task.js @@ -4,5 +4,9 @@ var path = require('path'); module.exports = function(grunt) { var yuidocs = require('../../docs/reference/data.json'); var base = path.join(__dirname, '../../lib'); - generate(yuidocs, path.join(base, 'p5.d.ts'), path.join(base, 'p5.global-mode.d.ts')); + generate( + yuidocs, + path.join(base, 'p5.d.ts'), + path.join(base, 'p5.global-mode.d.ts') + ); }; From 469841023e474ba69a7df49f15c3c981ee60f353 Mon Sep 17 00:00:00 2001 From: Spongman Date: Wed, 29 Nov 2017 18:06:47 -0800 Subject: [PATCH 7/9] typescript: missing p5 constructor --- src/core/core.js | 7 ++++--- tasks/typescript/generate-typescript-annotations.js | 9 ++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/core.js b/src/core/core.js index 5c364b8c58..09d442553d 100644 --- a/src/core/core.js +++ b/src/core/core.js @@ -26,13 +26,14 @@ var constants = require('./constants'); * "global" - all properties and methods are attached to the window * "instance" - all properties and methods are bound to this p5 object * - * @private + * @class p5 + * @constructor * @param {function} sketch a closure that can set optional preload(), * setup(), and/or draw() properties on the * given p5 instance - * @param {HTMLElement|boolean} [node] element to attach canvas to, if a + * @param {HTMLElement|Boolean} [node] element to attach canvas to, if a * boolean is passed in use it as sync - * @param {boolean} [sync] start synchronously (optional) + * @param {Boolean} [sync] start synchronously (optional) * @return {p5} a p5 instance */ var p5 = function(sketch, node, sync) { diff --git a/tasks/typescript/generate-typescript-annotations.js b/tasks/typescript/generate-typescript-annotations.js index 0046ce613e..d62515141c 100644 --- a/tasks/typescript/generate-typescript-annotations.js +++ b/tasks/typescript/generate-typescript-annotations.js @@ -36,6 +36,7 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { var EXTERNAL_TYPES = new Set([ 'HTMLCanvasElement', + 'HTMLElement', 'Float32Array', 'AudioParam', 'AudioNode', @@ -326,12 +327,9 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { function generateClassConstructor(className) { var classitem = yuidocs.classes[className]; - - if (!classitem.is_constructor) { - throw new Error(className + ' is not a constructor'); + if (classitem.is_constructor) { + generateClassMethod(className, classitem); } - - generateClassMethod(className, classitem); } function generateClassProperty(className, classitem) { @@ -405,6 +403,7 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { emit('// Properties from ' + className); emit.sectionBreak(); + generateClassConstructor(className); generateClassProperties(className); } From c915d64291963f8a615c8bfc4562e85ea333ca22 Mon Sep 17 00:00:00 2001 From: Pierre Krafft Date: Thu, 7 Dec 2017 00:13:18 +0100 Subject: [PATCH 8/9] Add .d.ts files to github release assets --- tasks/release/release-github.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tasks/release/release-github.js b/tasks/release/release-github.js index b22efb50ec..031a070edc 100644 --- a/tasks/release/release-github.js +++ b/tasks/release/release-github.js @@ -63,6 +63,12 @@ module.exports = function(grunt) { './lib/addons/p5.sound.min.js', 'application/javascript' ], + p5ts: ['p5.d.ts', './lib/p5.d.ts', 'text/plain'], + p5globalts: [ + 'p5.global-mode.d.ts', + './lib/p5.global-mode.d.ts', + 'text/plain' + ], p5zip: ['p5.zip', './p5.zip', 'application/zip'] }; From 291612bd4c002394c2894b8fc307039d2f95d26b Mon Sep 17 00:00:00 2001 From: Pierre Krafft Date: Mon, 1 Jan 2018 22:46:46 +0100 Subject: [PATCH 9/9] Add external type 'Blob' --- tasks/typescript/generate-typescript-annotations.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tasks/typescript/generate-typescript-annotations.js b/tasks/typescript/generate-typescript-annotations.js index d62515141c..1a5e83a21a 100644 --- a/tasks/typescript/generate-typescript-annotations.js +++ b/tasks/typescript/generate-typescript-annotations.js @@ -43,7 +43,8 @@ function mod(yuidocs, localFileame, globalFilename, sourcePath) { 'GainNode', 'DelayNode', 'ConvolverNode', - 'Event' + 'Event', + 'Blob' ]); var YUIDOC_TO_TYPESCRIPT_PARAM_MAP = {