build-defs.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. // Builds Tern definitions for the three.js API by scanning the JSDoc comments
  2. // in a three.js build file (e.g. build/three.core.js).
  3. //
  4. // The build output is machine-formatted (one declaration per line, tab
  5. // indentation, every documented symbol preceded by a `/** ... */` block), so a
  6. // lightweight line scanner is enough — no full JS parser is required.
  7. //
  8. // Usage: const defs = buildDefs( sourceText );
  9. //
  10. // `defs` is a Tern definitions object of the shape consumed by the editor's
  11. // TernServer: { "!name": "threejs", "THREE": { Box3: { prototype: { ... } } } }
  12. // --- JSDoc type -> Tern type ------------------------------------------------
  13. function mapType( raw, classes ) {
  14. if ( ! raw ) return null;
  15. let type = raw.trim();
  16. // strip a single leading/trailing brace pair if present
  17. type = type.replace( /^\{/, '' ).replace( /\}$/, '' ).trim();
  18. // unions: Tern has no union type, take the first member
  19. if ( type.includes( '|' ) ) type = type.split( '|' )[ 0 ].trim();
  20. // nullable / non-nullable / rest markers
  21. type = type.replace( /^[?!]/, '' ).replace( /^\.\.\./, '' ).trim();
  22. // Array forms: Array<X>, Array.<X>, X[]
  23. let m = type.match( /^Array\.?<(.+)>$/ );
  24. if ( m ) return '[' + ( mapType( m[ 1 ], classes ) || '?' ) + ']';
  25. m = type.match( /^(.+)\[\]$/ );
  26. if ( m ) return '[' + ( mapType( m[ 1 ], classes ) || '?' ) + ']';
  27. switch ( type ) {
  28. case 'number': case 'string': case 'boolean': return type;
  29. case 'function': case 'Function': return 'fn()';
  30. case '*': case 'any': case 'Object': case 'object':
  31. case 'undefined': case 'null': case 'void': return '?';
  32. }
  33. if ( classes.has( type ) ) return '+THREE.' + type;
  34. return '?'; // unknown (TypedArray, external types, generics, …)
  35. }
  36. // --- JSDoc block parser -----------------------------------------------------
  37. function parseDoc( lines ) {
  38. const params = [];
  39. let returns = null, atType = null, readonly = false;
  40. const desc = [];
  41. for ( let raw of lines ) {
  42. const line = raw.replace( /^\s*\*?\s?/, '' ); // strip ` * `
  43. const pm = line.match( /^@param\s+(\{[^}]*\})?\s*\[?([\w.]+)/ );
  44. if ( pm ) { params.push( { type: pm[ 1 ] || null, name: pm[ 2 ].split( '.' )[ 0 ] } ); continue; }
  45. const rm = line.match( /^@returns?\s+(\{[^}]*\})/ );
  46. if ( rm ) { returns = rm[ 1 ]; continue; }
  47. const tm = line.match( /^@type\s+(\{[^}]*\})/ );
  48. if ( tm ) { atType = tm[ 1 ]; continue; }
  49. if ( /^@readonly/.test( line ) ) { readonly = true; continue; }
  50. if ( /^@/.test( line ) ) continue; // other tags ignored
  51. if ( line.trim() ) desc.push( line.trim() );
  52. }
  53. // de-duplicate rest params that share a base name
  54. const seen = new Set(), uniqueParams = [];
  55. for ( const p of params ) if ( ! seen.has( p.name ) ) { seen.add( p.name ); uniqueParams.push( p ); }
  56. return { params: uniqueParams, returns, atType, readonly, doc: desc.join( ' ' ) };
  57. }
  58. function fnType( doc, classes ) {
  59. const args = doc.params.map( p => p.name + ': ' + ( mapType( p.type, classes ) || '?' ) ).join( ', ' );
  60. let t = 'fn(' + args + ')';
  61. const ret = mapType( doc.returns, classes );
  62. if ( ret ) t += ' -> ' + ret;
  63. return t;
  64. }
  65. function entry( type, doc ) {
  66. const e = { '!type': type };
  67. if ( doc.doc ) e[ '!doc' ] = doc.doc;
  68. return e;
  69. }
  70. // --- main scan --------------------------------------------------------------
  71. export default function buildDefs( source ) {
  72. const lines = source.split( '\n' );
  73. // pass 1: collect class names (so types can resolve to +THREE.X)
  74. const classes = new Set();
  75. for ( const line of lines ) {
  76. const m = line.match( /^class\s+(\w+)/ );
  77. if ( m ) classes.add( m[ 1 ] );
  78. }
  79. const THREE = {};
  80. let cur = null; // current class def object
  81. let curName = null; // current class name
  82. let pending = null; // most recent parsed JSDoc block
  83. for ( let i = 0; i < lines.length; i ++ ) {
  84. const line = lines[ i ];
  85. // JSDoc block: collect then resolve against the next code line below
  86. const t = line.trim();
  87. if ( t.startsWith( '/**' ) ) {
  88. const block = [];
  89. if ( ! t.endsWith( '*/' ) ) {
  90. i ++;
  91. for ( ; i < lines.length; i ++ ) {
  92. if ( lines[ i ].trim().endsWith( '*/' ) ) break;
  93. block.push( lines[ i ] );
  94. }
  95. }
  96. pending = parseDoc( block );
  97. continue;
  98. }
  99. // class declaration
  100. const cm = line.match( /^class\s+(\w+)(?:\s+extends\s+(\w+))?/ );
  101. if ( cm ) {
  102. curName = cm[ 1 ];
  103. cur = THREE[ curName ] || ( THREE[ curName ] = {} );
  104. // Declaring the class with a function `!type` makes Tern treat it as a
  105. // constructor, so `new THREE.X()` resolves to X.prototype. The signature
  106. // is refined once the constructor's JSDoc is read.
  107. cur[ '!type' ] = 'fn()';
  108. cur.prototype = cur.prototype || {};
  109. if ( cm[ 2 ] && classes.has( cm[ 2 ] ) ) cur.prototype[ '!proto' ] = 'THREE.' + cm[ 2 ] + '.prototype';
  110. if ( pending && pending.doc ) cur[ '!doc' ] = pending.doc;
  111. pending = null;
  112. continue;
  113. }
  114. // end of a top-level class
  115. if ( line === '}' ) { cur = null; curName = null; pending = null; continue; }
  116. if ( ! cur || ! pending ) { if ( t === '' ) continue; pending = null; continue; }
  117. // inside a class, the line right after a JSDoc block:
  118. // instance field: \t\tthis.name =
  119. let m = line.match( /^\t\tthis\.(\w+)\s*=/ );
  120. if ( m ) {
  121. const ty = mapType( pending.atType, classes ) || '?';
  122. cur.prototype[ m[ 1 ] ] = entry( ty, pending );
  123. pending = null;
  124. continue;
  125. }
  126. // static method: \tstatic name(
  127. m = line.match( /^\tstatic\s+(\w+)\s*\(/ );
  128. if ( m ) {
  129. cur[ m[ 1 ] ] = entry( fnType( pending, classes ), pending );
  130. pending = null;
  131. continue;
  132. }
  133. // accessor: \tget name() / \tset name(
  134. m = line.match( /^\t(?:get|set)\s+(\w+)\s*\(/ );
  135. if ( m ) {
  136. const ty = mapType( pending.atType || pending.returns, classes ) || '?';
  137. if ( ! cur.prototype[ m[ 1 ] ] ) cur.prototype[ m[ 1 ] ] = entry( ty, pending );
  138. pending = null;
  139. continue;
  140. }
  141. // constructor: \tconstructor( -> refine the class signature
  142. if ( /^\tconstructor\s*\(/.test( line ) ) {
  143. cur[ '!type' ] = 'fn(' + pending.params.map( p => p.name + ': ' + ( mapType( p.type, classes ) || '?' ) ).join( ', ' ) + ')';
  144. pending = null;
  145. continue;
  146. }
  147. // instance method: \t[async ]name(
  148. m = line.match( /^\t(?:async\s+)?(\w+)\s*\(/ );
  149. if ( m ) {
  150. cur.prototype[ m[ 1 ] ] = entry( fnType( pending, classes ), pending );
  151. pending = null;
  152. continue;
  153. }
  154. pending = null;
  155. }
  156. return { '!name': 'threejs', THREE };
  157. }
粤ICP备19079148号