USDAParser.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899
  1. // Pre-compiled regex patterns for performance
  2. const DEF_MATCH_REGEX = /^def\s+(?:(\w+)\s+)?"?([^"]+)"?$/;
  3. const VARIANT_STRING_REGEX = /^string\s+(\w+)$/;
  4. const ATTR_MATCH_REGEX = /^(?:uniform\s+)?(\w+(?:\[\])?)\s+(.+)$/;
  5. // Spec types (must match USDCParser/USDComposer)
  6. const SpecType = {
  7. Attribute: 1,
  8. Prim: 6,
  9. Relationship: 8
  10. };
  11. class USDAParser {
  12. parseText( text ) {
  13. // Preprocess: strip comments and normalize multiline values
  14. text = this._preprocess( text );
  15. const root = {};
  16. const lines = text.split( '\n' );
  17. let string = null;
  18. let target = root;
  19. const stack = [ root ];
  20. for ( const line of lines ) {
  21. if ( line.includes( '=' ) ) {
  22. // Find the first '=' that's not inside quotes
  23. const eqIdx = this._findAssignmentOperator( line );
  24. if ( eqIdx === - 1 ) {
  25. string = line.trim();
  26. continue;
  27. }
  28. const lhs = line.slice( 0, eqIdx ).trim();
  29. const rhs = line.slice( eqIdx + 1 ).trim();
  30. if ( rhs.endsWith( '{' ) ) {
  31. const group = {};
  32. stack.push( group );
  33. target[ lhs ] = group;
  34. target = group;
  35. } else if ( rhs.endsWith( '(' ) ) {
  36. // see #28631
  37. const values = rhs.slice( 0, - 1 );
  38. target[ lhs ] = values;
  39. const meta = {};
  40. stack.push( meta );
  41. target = meta;
  42. } else {
  43. target[ lhs ] = rhs;
  44. }
  45. } else if ( line.includes( ':' ) && ! line.includes( '=' ) ) {
  46. // Handle dictionary entries like "0: [(...)...]" for timeSamples
  47. const colonIdx = line.indexOf( ':' );
  48. const key = line.slice( 0, colonIdx ).trim();
  49. const value = line.slice( colonIdx + 1 ).trim();
  50. // Only process if key looks like a number (timeSamples frame)
  51. if ( /^[\d.]+$/.test( key ) ) {
  52. target[ key ] = value;
  53. }
  54. } else if ( line.endsWith( '{' ) ) {
  55. const group = target[ string ] || {};
  56. stack.push( group );
  57. target[ string ] = group;
  58. target = group;
  59. } else if ( line.endsWith( '}' ) ) {
  60. stack.pop();
  61. if ( stack.length === 0 ) continue;
  62. target = stack[ stack.length - 1 ];
  63. } else if ( line.endsWith( '(' ) ) {
  64. const meta = {};
  65. stack.push( meta );
  66. string = line.split( '(' )[ 0 ].trim() || string;
  67. target[ string ] = meta;
  68. target = meta;
  69. } else if ( line.endsWith( ')' ) ) {
  70. stack.pop();
  71. target = stack[ stack.length - 1 ];
  72. } else if ( line.trim() ) {
  73. string = line.trim();
  74. }
  75. }
  76. return root;
  77. }
  78. _preprocess( text ) {
  79. // Remove block comments /* ... */
  80. text = this._stripBlockComments( text );
  81. // Collapse triple-quoted strings into single lines
  82. text = this._collapseTripleQuotedStrings( text );
  83. // Remove line comments # ... (but preserve #usda header)
  84. // Only remove # comments that aren't at the start of a line or after whitespace
  85. const lines = text.split( '\n' );
  86. const processed = [];
  87. let inMultilineValue = false;
  88. let bracketDepth = 0;
  89. let parenDepth = 0;
  90. let accumulated = '';
  91. for ( let i = 0; i < lines.length; i ++ ) {
  92. let line = lines[ i ];
  93. // Strip inline comments (but not inside strings)
  94. line = this._stripInlineComment( line );
  95. // Track bracket/paren depth for multiline values
  96. const trimmed = line.trim();
  97. if ( inMultilineValue ) {
  98. // Continue accumulating multiline value
  99. accumulated += ' ' + trimmed;
  100. // Update depths
  101. for ( const ch of trimmed ) {
  102. if ( ch === '[' ) bracketDepth ++;
  103. else if ( ch === ']' ) bracketDepth --;
  104. else if ( ch === '(' && bracketDepth > 0 ) parenDepth ++;
  105. else if ( ch === ')' && bracketDepth > 0 ) parenDepth --;
  106. }
  107. // Check if multiline value is complete
  108. if ( bracketDepth === 0 && parenDepth === 0 ) {
  109. processed.push( accumulated );
  110. accumulated = '';
  111. inMultilineValue = false;
  112. }
  113. } else {
  114. // Check if this line starts a multiline array value
  115. // Look for patterns like "attr = [" or "attr = @path@[" without closing ]
  116. if ( trimmed.includes( '=' ) ) {
  117. const eqIdx = this._findAssignmentOperator( trimmed );
  118. if ( eqIdx !== - 1 ) {
  119. const rhs = trimmed.slice( eqIdx + 1 ).trim();
  120. // Count brackets in the value part
  121. let openBrackets = 0;
  122. let closeBrackets = 0;
  123. for ( const ch of rhs ) {
  124. if ( ch === '[' ) openBrackets ++;
  125. else if ( ch === ']' ) closeBrackets ++;
  126. }
  127. if ( openBrackets > closeBrackets ) {
  128. // Multiline array detected
  129. inMultilineValue = true;
  130. bracketDepth = openBrackets - closeBrackets;
  131. parenDepth = 0;
  132. accumulated = trimmed;
  133. continue;
  134. }
  135. }
  136. }
  137. processed.push( trimmed );
  138. }
  139. }
  140. return processed.join( '\n' );
  141. }
  142. _stripBlockComments( text ) {
  143. // Iteratively remove /* ... */ comments without regex backtracking
  144. let result = '';
  145. let i = 0;
  146. while ( i < text.length ) {
  147. // Check for block comment start
  148. if ( text[ i ] === '/' && i + 1 < text.length && text[ i + 1 ] === '*' ) {
  149. // Find the closing */
  150. let j = i + 2;
  151. while ( j < text.length ) {
  152. if ( text[ j ] === '*' && j + 1 < text.length && text[ j + 1 ] === '/' ) {
  153. // Found closing, skip past it
  154. j += 2;
  155. break;
  156. }
  157. j ++;
  158. }
  159. // Move past the comment (or to end if unclosed)
  160. i = j;
  161. } else {
  162. result += text[ i ];
  163. i ++;
  164. }
  165. }
  166. return result;
  167. }
  168. _collapseTripleQuotedStrings( text ) {
  169. let result = '';
  170. let i = 0;
  171. while ( i < text.length ) {
  172. if ( i + 2 < text.length ) {
  173. const triple = text.slice( i, i + 3 );
  174. if ( triple === '\'\'\'' || triple === '"""' ) {
  175. const quoteChar = triple;
  176. result += quoteChar;
  177. i += 3;
  178. while ( i < text.length ) {
  179. if ( i + 2 < text.length && text.slice( i, i + 3 ) === quoteChar ) {
  180. result += quoteChar;
  181. i += 3;
  182. break;
  183. } else {
  184. if ( text[ i ] === '\n' ) {
  185. result += '\\n';
  186. } else if ( text[ i ] !== '\r' ) {
  187. result += text[ i ];
  188. }
  189. i ++;
  190. }
  191. }
  192. continue;
  193. }
  194. }
  195. result += text[ i ];
  196. i ++;
  197. }
  198. return result;
  199. }
  200. _stripInlineComment( line ) {
  201. // Don't strip if line starts with #usda
  202. if ( line.trim().startsWith( '#usda' ) ) return line;
  203. // Find # that's not inside a string
  204. let inString = false;
  205. let stringChar = null;
  206. let escaped = false;
  207. for ( let i = 0; i < line.length; i ++ ) {
  208. const ch = line[ i ];
  209. if ( escaped ) {
  210. escaped = false;
  211. continue;
  212. }
  213. if ( ch === '\\' ) {
  214. escaped = true;
  215. continue;
  216. }
  217. if ( ! inString && ( ch === '"' || ch === '\'' ) ) {
  218. inString = true;
  219. stringChar = ch;
  220. } else if ( inString && ch === stringChar ) {
  221. inString = false;
  222. stringChar = null;
  223. } else if ( ! inString && ch === '#' ) {
  224. // Found comment start outside of string
  225. return line.slice( 0, i ).trimEnd();
  226. }
  227. }
  228. return line;
  229. }
  230. _findAssignmentOperator( line ) {
  231. // Find the first '=' that's not inside quotes
  232. let inString = false;
  233. let stringChar = null;
  234. let escaped = false;
  235. for ( let i = 0; i < line.length; i ++ ) {
  236. const ch = line[ i ];
  237. if ( escaped ) {
  238. escaped = false;
  239. continue;
  240. }
  241. if ( ch === '\\' ) {
  242. escaped = true;
  243. continue;
  244. }
  245. if ( ! inString && ( ch === '"' || ch === '\'' ) ) {
  246. inString = true;
  247. stringChar = ch;
  248. } else if ( inString && ch === stringChar ) {
  249. inString = false;
  250. stringChar = null;
  251. } else if ( ! inString && ch === '=' ) {
  252. return i;
  253. }
  254. }
  255. return - 1;
  256. }
  257. /**
  258. * Parse USDA text and return raw spec data in specsByPath format.
  259. * Used by USDComposer for unified scene composition.
  260. */
  261. parseData( text ) {
  262. const root = this.parseText( text );
  263. const specsByPath = {};
  264. // Parse root metadata
  265. const rootFields = {};
  266. if ( '#usda 1.0' in root ) {
  267. const header = root[ '#usda 1.0' ];
  268. if ( header.upAxis ) {
  269. rootFields.upAxis = header.upAxis.replace( /"/g, '' );
  270. }
  271. if ( header.defaultPrim ) {
  272. rootFields.defaultPrim = header.defaultPrim.replace( /"/g, '' );
  273. }
  274. if ( header.metersPerUnit !== undefined ) {
  275. rootFields.metersPerUnit = parseFloat( header.metersPerUnit );
  276. }
  277. if ( header.framesPerSecond !== undefined ) {
  278. rootFields.framesPerSecond = parseFloat( header.framesPerSecond );
  279. }
  280. if ( header.timeCodesPerSecond !== undefined ) {
  281. rootFields.timeCodesPerSecond = parseFloat( header.timeCodesPerSecond );
  282. }
  283. }
  284. specsByPath[ '/' ] = { specType: SpecType.Prim, fields: rootFields };
  285. // Walk the tree and build specsByPath
  286. const walkTree = ( data, parentPath ) => {
  287. const primChildren = [];
  288. for ( const key in data ) {
  289. // Skip metadata
  290. if ( key === '#usda 1.0' ) continue;
  291. if ( key === 'variants' ) continue;
  292. // Check for primitive definitions
  293. // Matches both 'def TypeName "name"' and 'def "name"' (no type)
  294. const defMatch = key.match( DEF_MATCH_REGEX );
  295. if ( defMatch ) {
  296. const typeName = defMatch[ 1 ] || '';
  297. const name = defMatch[ 2 ];
  298. const path = parentPath === '/' ? '/' + name : parentPath + '/' + name;
  299. primChildren.push( name );
  300. const primFields = { typeName };
  301. const primData = data[ key ];
  302. // Extract attributes and relationships from this prim
  303. this._extractPrimData( primData, path, primFields, specsByPath, SpecType );
  304. specsByPath[ path ] = { specType: SpecType.Prim, fields: primFields };
  305. // Recurse into children
  306. walkTree( primData, path );
  307. }
  308. }
  309. // Add primChildren to parent spec
  310. if ( primChildren.length > 0 && specsByPath[ parentPath ] ) {
  311. specsByPath[ parentPath ].fields.primChildren = primChildren;
  312. }
  313. };
  314. walkTree( root, '/' );
  315. // Fallback: infer elementSize for primvars:skel:jointIndices/jointWeights
  316. // when not explicitly declared in the USDA text
  317. this._inferSkelElementSize( specsByPath );
  318. return { specsByPath };
  319. }
  320. _inferSkelElementSize( specsByPath ) {
  321. // For each mesh prim with primvars:skel:jointIndices/jointWeights but no
  322. // elementSize, infer it from the data: elementSize = array.length / numVertices.
  323. for ( const path in specsByPath ) {
  324. const spec = specsByPath[ path ];
  325. if ( spec.specType !== SpecType.Prim || spec.fields.typeName !== 'Mesh' ) continue;
  326. const pointsSpec = specsByPath[ path + '.points' ];
  327. if ( ! pointsSpec || ! pointsSpec.fields.default ) continue;
  328. const numVertices = pointsSpec.fields.default.length / 3;
  329. if ( numVertices === 0 ) continue;
  330. this._inferElementSize( specsByPath[ path + '.primvars:skel:jointIndices' ], numVertices );
  331. this._inferElementSize( specsByPath[ path + '.primvars:skel:jointWeights' ], numVertices );
  332. }
  333. }
  334. _inferElementSize( attrSpec, numVertices ) {
  335. if ( ! attrSpec || attrSpec.fields.elementSize !== undefined || ! attrSpec.fields.default ) return;
  336. const len = attrSpec.fields.default.length;
  337. if ( len > 0 && len % numVertices === 0 ) attrSpec.fields.elementSize = len / numVertices;
  338. }
  339. _extractPrimData( data, path, primFields, specsByPath, SpecType ) {
  340. if ( ! data || typeof data !== 'object' ) return;
  341. for ( const key in data ) {
  342. // Skip nested defs (handled by walkTree)
  343. if ( key.startsWith( 'def ' ) ) continue;
  344. if ( key === 'prepend references' ) {
  345. primFields.references = [ data[ key ] ];
  346. continue;
  347. }
  348. if ( key === 'payload' ) {
  349. primFields.payload = data[ key ];
  350. continue;
  351. }
  352. if ( key === 'variants' ) {
  353. const variantSelection = {};
  354. const variants = data[ key ];
  355. for ( const vKey in variants ) {
  356. const match = vKey.match( VARIANT_STRING_REGEX );
  357. if ( match ) {
  358. const variantSetName = match[ 1 ];
  359. const variantValue = variants[ vKey ].replace( /"/g, '' );
  360. variantSelection[ variantSetName ] = variantValue;
  361. }
  362. }
  363. if ( Object.keys( variantSelection ).length > 0 ) {
  364. primFields.variantSelection = variantSelection;
  365. }
  366. continue;
  367. }
  368. if ( key.startsWith( 'rel ' ) ) {
  369. const relName = key.slice( 4 );
  370. const relPath = path + '.' + relName;
  371. const target = data[ key ].replace( /[<>]/g, '' );
  372. specsByPath[ relPath ] = {
  373. specType: SpecType.Relationship,
  374. fields: { targetPaths: [ target ] }
  375. };
  376. continue;
  377. }
  378. // Handle xformOpOrder
  379. if ( key.includes( 'xformOpOrder' ) ) {
  380. const ops = data[ key ]
  381. .replace( /[\[\]]/g, '' )
  382. .split( ',' )
  383. .map( s => s.trim().replace( /"/g, '' ) );
  384. primFields.xformOpOrder = ops;
  385. continue;
  386. }
  387. // Handle typed attributes
  388. // Format: [qualifier] type attrName (e.g., "uniform token[] joints", "float3 position")
  389. const attrMatch = key.match( ATTR_MATCH_REGEX );
  390. if ( attrMatch ) {
  391. const valueType = attrMatch[ 1 ];
  392. const attrName = attrMatch[ 2 ];
  393. const rawValue = data[ key ];
  394. // Handle connection attributes (e.g., "inputs:normal.connect = </path>")
  395. if ( attrName.endsWith( '.connect' ) ) {
  396. const baseAttrName = attrName.slice( 0, - 8 ); // Remove '.connect'
  397. const attrPath = path + '.' + baseAttrName;
  398. // Parse connection path - extract from <path> format
  399. let connPath = String( rawValue ).trim();
  400. if ( connPath.startsWith( '<' ) ) connPath = connPath.slice( 1 );
  401. if ( connPath.endsWith( '>' ) ) connPath = connPath.slice( 0, - 1 );
  402. // Get or create the attribute spec
  403. if ( ! specsByPath[ attrPath ] ) {
  404. specsByPath[ attrPath ] = {
  405. specType: SpecType.Attribute,
  406. fields: { typeName: valueType }
  407. };
  408. }
  409. specsByPath[ attrPath ].fields.connectionPaths = [ connPath ];
  410. continue;
  411. }
  412. // Handle timeSamples attributes specially
  413. if ( attrName.endsWith( '.timeSamples' ) && typeof rawValue === 'object' ) {
  414. const baseAttrName = attrName.slice( 0, - 12 ); // Remove '.timeSamples'
  415. const attrPath = path + '.' + baseAttrName;
  416. // Parse timeSamples dictionary into times and values arrays
  417. const times = [];
  418. const values = [];
  419. for ( const frameKey in rawValue ) {
  420. const frame = parseFloat( frameKey );
  421. if ( isNaN( frame ) ) continue;
  422. times.push( frame );
  423. values.push( this._parseAttributeValue( valueType, rawValue[ frameKey ] ) );
  424. }
  425. // Sort by time
  426. const sorted = times.map( ( t, i ) => ( { t, v: values[ i ] } ) ).sort( ( a, b ) => a.t - b.t );
  427. specsByPath[ attrPath ] = {
  428. specType: SpecType.Attribute,
  429. fields: {
  430. timeSamples: { times: sorted.map( s => s.t ), values: sorted.map( s => s.v ) },
  431. typeName: valueType
  432. }
  433. };
  434. } else {
  435. // Parse value based on type
  436. const parsedValue = this._parseAttributeValue( valueType, rawValue );
  437. // Store as attribute spec, preserving any existing fields
  438. // (e.g. connectionPaths set by an earlier `.connect` form)
  439. const attrPath = path + '.' + attrName;
  440. if ( specsByPath[ attrPath ] ) {
  441. specsByPath[ attrPath ].fields.default = parsedValue;
  442. specsByPath[ attrPath ].fields.typeName = valueType;
  443. } else {
  444. specsByPath[ attrPath ] = {
  445. specType: SpecType.Attribute,
  446. fields: { default: parsedValue, typeName: valueType }
  447. };
  448. }
  449. }
  450. }
  451. }
  452. }
  453. _parseAttributeValue( valueType, rawValue ) {
  454. if ( rawValue === undefined || rawValue === null ) return undefined;
  455. const str = String( rawValue ).trim();
  456. // Array types
  457. if ( valueType.endsWith( '[]' ) ) {
  458. let result;
  459. // Parse JSON-like arrays
  460. try {
  461. // Handle arrays with parentheses like [(1,2,3), (4,5,6)]
  462. // Remove trailing comma (valid in USDA but not JSON)
  463. let cleaned = str.replace( /\(/g, '[' ).replace( /\)/g, ']' );
  464. if ( cleaned.endsWith( ',' ) ) cleaned = cleaned.slice( 0, - 1 );
  465. const parsed = JSON.parse( cleaned );
  466. // Flatten nested arrays for types like point3f[]
  467. result = Array.isArray( parsed ) && Array.isArray( parsed[ 0 ] ) ? parsed.flat() : parsed;
  468. } catch ( e ) {
  469. // Try simple array parsing
  470. const cleaned = str.replace( /[\[\]]/g, '' );
  471. result = cleaned.split( ',' ).map( s => {
  472. const trimmed = s.trim();
  473. const num = parseFloat( trimmed );
  474. return isNaN( num ) ? trimmed.replace( /"/g, '' ) : num;
  475. } );
  476. }
  477. //reorder (w, x, y, z) to (x, y, z, w)
  478. if ( valueType.startsWith( 'quat' ) ) {
  479. for ( let i = 0; i < result.length; i += 4 ) {
  480. const w = result[ i ];
  481. result[ i ] = result[ i + 1 ];
  482. result[ i + 1 ] = result[ i + 2 ];
  483. result[ i + 2 ] = result[ i + 3 ];
  484. result[ i + 3 ] = w;
  485. }
  486. }
  487. return result;
  488. }
  489. // Vector types (double3, float3, point3f, etc.)
  490. if ( valueType.includes( '3' ) || valueType.includes( '2' ) || valueType.includes( '4' ) ) {
  491. // Parse (x, y, z) format
  492. const cleaned = str.replace( /[()]/g, '' );
  493. const values = cleaned.split( ',' ).map( s => parseFloat( s.trim() ) );
  494. return values;
  495. }
  496. // Quaternion types (quatf, quatd, quath)
  497. // Text format is (w, x, y, z), convert to (x, y, z, w)
  498. if ( valueType.startsWith( 'quat' ) ) {
  499. const cleaned = str.replace( /[()]/g, '' );
  500. const values = cleaned.split( ',' ).map( s => parseFloat( s.trim() ) );
  501. return [ values[ 1 ], values[ 2 ], values[ 3 ], values[ 0 ] ];
  502. }
  503. // Matrix types
  504. if ( valueType.includes( 'matrix' ) ) {
  505. const cleaned = str.replace( /[()]/g, '' );
  506. const values = cleaned.split( ',' ).map( s => parseFloat( s.trim() ) );
  507. return values;
  508. }
  509. // Scalar numeric types
  510. if ( valueType === 'float' || valueType === 'double' || valueType === 'int' ) {
  511. return parseFloat( str );
  512. }
  513. // String/token types
  514. if ( valueType === 'string' || valueType === 'token' ) {
  515. return this._parseString( str );
  516. }
  517. // Asset path
  518. if ( valueType === 'asset' ) {
  519. return str.replace( /@/g, '' ).replace( /"/g, '' );
  520. }
  521. // Default: return as string with quotes removed
  522. return this._parseString( str );
  523. }
  524. _parseString( str ) {
  525. // Remove surrounding quotes
  526. if ( ( str.startsWith( '"' ) && str.endsWith( '"' ) ) ||
  527. ( str.startsWith( '\'' ) && str.endsWith( '\'' ) ) ) {
  528. str = str.slice( 1, - 1 );
  529. }
  530. // Handle escape sequences
  531. let result = '';
  532. let i = 0;
  533. while ( i < str.length ) {
  534. if ( str[ i ] === '\\' && i + 1 < str.length ) {
  535. const next = str[ i + 1 ];
  536. switch ( next ) {
  537. case 'n': result += '\n'; break;
  538. case 't': result += '\t'; break;
  539. case 'r': result += '\r'; break;
  540. case '\\': result += '\\'; break;
  541. case '"': result += '"'; break;
  542. case '\'': result += '\''; break;
  543. default: result += next; break;
  544. }
  545. i += 2;
  546. } else {
  547. result += str[ i ];
  548. i ++;
  549. }
  550. }
  551. return result;
  552. }
  553. }
  554. export { USDAParser };
粤ICP备19079148号