import * as Acorn from 'acorn' ; import AcornJSX from 'acorn-jsx' ; import { generate , GENERATOR } from 'astring' ; import LRUCache from 'lru-cache' ; import { traverse } from 'estraverse' ; import { createAbsolutePositionFromRelativePosition , createRelativePositionFromTypeIndex , getItem , Doc , Text , } from 'yjs' ; import { createAbsolutePositionFromStateVector , createRelativePositionFromStateVector , getClock , } from '../yjs/YjsHelpers' ; import { VersionVector } from '@casual-simulation/causal-trees' ; /**
 * The symbol that is used in script dependencies to represent any argument.
 */ export const anyArgument = Symbol ( 'anyArgument' ) ; declare module 'acorn' { /**
     * Extends the acorn parser interface.
     */ interface Parser { type : Acorn . TokenType ; start : number ; startLoc : number ; value : string ; pos : number ; next ( ) : void ; parseLiteral ( value ) : Acorn . Node ; parseIdent ( ) : Acorn . Node ; parseExprSubscripts ( ) : Acorn . Node ; parseSubscript ( base , startPos , startLoc ) : Acorn . Node ; unexpected ( ) : void ; startNodeAt ( start , startLoc ) : Acorn . Node ; readToken ( code ) : any ; finishToken ( token ) : any ; finishNode ( node , type ) : Acorn . Node ; parseExprAtom ( refShortHandDefaultPos ) : Acorn . Node ; parseParenAndDistinguishExpression ( canBeArrow ) : Acorn . Node ; } } export type ExJsNode = TokenValueNode | ObjectValueNode ; export interface TokenValueNode extends Acorn . Node { type : 'TokenValue' ; identifier : Acorn . Node ; } export interface ObjectValueNode extends Acorn . Node { type : 'ObjectValue' ; identifier : Acorn . Node ; } const exJsGenerator = Object . assign ( { } , GENERATOR , { ImportExpression : function ( node , state ) { state . write ( 'import(' ) ; this [ node . source . type ] ( node . source , state ) ; state . write ( ')' ) ; } , } ) ; export interface TranspilerMacro { test : RegExp ; replacement : ( val : string ) => string ; } /**
 * The list of macros that the sandbox uses on the input code before transpiling it.
 */ const MACROS = [ { test : /^(?:\🧬)/g , replacement : ( val ) => '' , } , { test : /(?:[“”])/g , replacement : ( val ) => '"' , } , { test : /(?:[‘’])/g , replacement : ( val ) => "'" , } , ] ; param text export function replaceMacros ( text ) { if ( ! text ) { return text ; } for ( let m of MACROS ) { text = text . replace ( m . test , m . replacement ) ; } return text ; } export interface TranspilerOptions { jsxFactory ? : string ; jsxFragment ? : string ; forceSync ? : boolean ; } /**
 * Defines a class that is able to compile code from AUX's custom JavaScript dialect
 * into pure ES6 JavaScript. Does not preserve spacing or comments.
 *
 * See https://docs.google.com/document/d/1WQXQPjdXxyx_lau15WPpwTTYvt66_wPCu3x-08rpLoY/edit?usp=sharing
 */ export class Transpiler { private _parser : typeof Acorn . Parser ; private _jsxFactory : string ; private _jsxFragment : string ; private _forceSync : boolean ; private _cache : LRUCache < string , TranspilerResult > ; get forceSync ( ) { return this . _forceSync ; } set forceSync ( value ) { this . _forceSync = value ; } constructor ( options ? ) { this . _cache = new LRUCache < string , TranspilerResult > ( { max : 1000 , } ) ; this . _parser = Acorn . Parser . extend ( AcornJSX ( ) ) ; this . _jsxFactory = options ?. jsxFactory ?? 'h' ; this . _jsxFragment = options ?. jsxFragment ?? 'Fragment' ; this . _forceSync = options ?. forceSync ?? false ; } parse ( code ) { const macroed = replaceMacros ( code ) ; const node = this . _parse ( macroed ) ; return node ; } /**
     * Transpiles the given code into ES6 JavaScript Code.
     */ transpile ( code ) { const result = this . _transpile ( code ) ; return result . code ; } param code transpileWithMetadata ( code ) { return this . _transpile ( code ) ; } /**
     * Transpiles the given code into ES6 JavaScript Code.
     */ private _transpile ( code ) { const cached = this . _cache . get ( code ) ; if ( cached ) { return cached ; } const macroed = replaceMacros ( code ) ; const node = this . _parse ( macroed ) ; const doc = new Doc ( ) ; doc . clientID = 0 ; const text = doc . getText ( ) ; text . insert ( 0 , code ) ; this . _replace ( node , doc , text ) ; const finalCode = text . toString ( ) ; const result = { code : finalCode , original : macroed , metadata : { doc , text , } , } ; this . _cache . set ( code , result ) ; return result ; } param code private _parse ( code ) { const node = this . _parser . parse ( code , { ecmaVersion : < any > 11 , locations : true , } ) ; return node ; } getTagNodeValues ( n ) { let currentNode = n . identifier ; let identifier ; let args = [ ] ; let nodes = [ ] ; while ( currentNode . type === 'MemberExpression' ) { currentNode = currentNode . object ; } if ( currentNode . type === 'CallExpression' ) { identifier = currentNode . callee ; args = currentNode . arguments ; currentNode = n . identifier ; while ( currentNode . type === 'MemberExpression' ) { nodes . unshift ( currentNode ) ; currentNode = currentNode . object ; } } else { identifier = n . identifier ; } let tag ; if ( identifier . type === 'MemberExpression' ) { tag = this . toJs ( identifier ) ; } else { tag = identifier . name || identifier . value ; } return { tag , args , nodes } ; } toJs ( node ) { return generate ( < any > node , { generator : exJsGenerator , } ) ; } private _replace ( node , doc , text ) { if ( ! node ) { return ; } traverse ( < any > node , { enter : < any > ( ( n , parent ) => { if ( n . type === 'WhileStatement' ) { this . _replaceWhileStatement ( n , doc , text ) ; } else if ( n . type === 'DoWhileStatement' ) { this . _replaceDoWhileStatement ( n , doc , text ) ; } else if ( n . type === 'ForStatement' ) { this . _replaceForStatement ( n , doc , text ) ; } else if ( n . type === 'ForInStatement' ) { this . _replaceForInStatement ( n , doc , text ) ; } else if ( n . type === 'ForOfStatement' ) { this . _replaceForOfStatement ( n , doc , text ) ; } else if ( n . type === 'JSXElement' ) { this . _replaceJSXElement ( n , doc , text ) ; } else if ( n . type === 'JSXText' ) { this . _replaceJSXText ( n , doc , text ) ; } else if ( n . type === 'JSXExpressionContainer' ) { this . _replaceJSXExpressionContainer ( n , doc , text ) ; } else if ( n . type === 'JSXFragment' ) { this . _replaceJSXFragment ( n , doc , text ) ; } else if ( this . _forceSync && n . type === 'FunctionDeclaration' && n . async ) { this . _replaceAsyncFunction ( n , doc , text ) ; } else if ( this . _forceSync && n . type === 'ArrowFunctionExpression' && n . async ) { this . _replaceAsyncFunction ( n , doc , text ) ; } else if ( this . _forceSync && n . type === 'FunctionExpression' && n . async ) { if ( parent . type === 'Property' && parent . method ) { this . _replaceAsyncFunction ( parent , doc , text ) ; } else { this . _replaceAsyncFunction ( n , doc , text ) ; } } else if ( this . _forceSync && n . type === 'AwaitExpression' ) { this . _replaceAwaitExpression ( n , doc , text ) ; } } ) , keys : { JSXElement : [ ] , JSXFragment : [ ] , JSXOpeningElement : [ ] , JSXClosingElement : [ ] , JSXText : [ ] , JSXExpressionContainer : [ 'expression' ] , } , } ) ; } private _replaceWhileStatement ( node , doc , text ) { this . _insertEnergyCheckIntoStatement ( doc , text , node . body ) ; } private _replaceDoWhileStatement ( node , doc , text ) { this . _insertEnergyCheckIntoStatement ( doc , text , node . body ) ; } private _replaceForStatement ( node , doc , text ) { this . _insertEnergyCheckIntoStatement ( doc , text , node . body ) ; } private _replaceForInStatement ( node , doc , text ) { this . _insertEnergyCheckIntoStatement ( doc , text , node . body ) ; } private _replaceForOfStatement ( node , doc , text ) { this . _insertEnergyCheckIntoStatement ( doc , text , node . body ) ; } private _replaceJSXElement ( node , doc , text ) { this . _insertJSXFactoryCall ( node , node . openingElement , node . closingElement , doc , text ) ; this . _removeTag ( node , node . openingElement , node . closingElement , doc , text ) ; } private _replaceJSXFragment ( node , doc , text ) { this . _insertJSXFactoryCall ( node , node . openingFragment , node . closingFragment , doc , text ) ; this . _removeTag ( node , node . openingFragment , node . closingFragment , doc , text ) ; } private _insertJSXFactoryCall ( node , openElement , closeElement , doc , text ) { doc . clientID += 1 ; const version = { '0' : getClock ( doc , 0 ) } ; const openingFunctionCall = `${ this . _jsxFactory }(` ; const absoluteStart = createAbsolutePositionFromStateVector ( doc , text , version , node . start , undefined , true ) ; const end = createRelativePositionFromStateVector ( text , version , ! ! closeElement ? closeElement . end : node . end , - 1 , true ) ; const nameStart = node . type === 'JSXElement' ? createRelativePositionFromStateVector ( text , version , openElement . name . start , undefined , true ) : null ; const nameEnd = node . type == 'JSXElement' ? createRelativePositionFromStateVector ( text , version , openElement . name . end , - 1 , true ) : null ; let currentIndex = absoluteStart . index ; text . insert ( currentIndex , openingFunctionCall ) ; currentIndex += openingFunctionCall . length ; if ( node . type === 'JSXElement' ) { const nameElement = openElement . name ; let addQuotes = false ; if ( nameElement . type === 'JSXIdentifier' ) { const name = openElement . name . name ; if ( /^[a-z]/ . test ( name ) ) { addQuotes = true ; } } if ( addQuotes ) { const startIndex = createAbsolutePositionFromRelativePosition ( nameStart , doc ) ; text . insert ( startIndex . index , '"' ) ; const endIndex = createAbsolutePositionFromRelativePosition ( nameEnd , doc ) ; text . insert ( endIndex . index , '",' ) ; currentIndex = endIndex . index + 2 ; } else { const endIndex = createAbsolutePositionFromRelativePosition ( nameEnd , doc ) ; text . insert ( endIndex . index , ',' ) ; currentIndex = endIndex . index + 1 ; } } else { const nodeName = this . _jsxFragment + ',' ; text . insert ( currentIndex , nodeName ) ; currentIndex += nodeName . length ; } if ( openElement . attributes . length > 0 ) { this . _replaceJSXElementAttributes ( node , openElement , openElement . attributes , doc , text ) ; } else { const props = `null,` ; text . insert ( currentIndex , props ) ; } this . _replaceJSXElementChildren ( node , node . children , doc , text ) ; const absoluteEnd = createAbsolutePositionFromRelativePosition ( end , doc ) ; doc . clientID += 1 ; text . insert ( absoluteEnd . index , ')' ) ; } private _replaceJSXElementAttributes ( node , openElement , attributes , doc , text ) { const version = { '0' : getClock ( doc , 0 ) } ; const start = createRelativePositionFromStateVector ( text , version , openElement . name ?. end ?? openElement . start + 1 , undefined , true ) ; const end = createRelativePositionFromStateVector ( text , version , openElement . end , - 1 , true ) ; let attrs = [ ] ; for ( let attr of attributes ) { const attrStart = createRelativePositionFromStateVector ( text , version , attr . start , undefined , true ) ; if ( attr . type === 'JSXSpreadAttribute' ) { attrs . push ( [ attr , attrStart ] ) ; } else { const attrName = attr . name . name ; attrs . push ( [ attr , attrStart , attrName ] ) ; } } let index = 0 ; for ( let [ attr , start , name ] of attrs ) { const pos = createAbsolutePositionFromRelativePosition ( start , doc ) ; let val = '' ; if ( index > 0 ) { val = ',' ; } if ( attr . type !== 'JSXSpreadAttribute' ) { val += `"${ name }":` ; if ( ! attr . value ) { val += 'true' ; } } text . insert ( pos . index , val ) ; index ++ ; if ( attr . type === 'JSXSpreadAttribute' ) { this . _replace ( attr . argument , doc , text ) ; } else { this . _replace ( attr . value , doc , text ) ; } } const startAbsolute = createAbsolutePositionFromRelativePosition ( start , doc ) ; text . insert ( startAbsolute . index , '{' ) ; const endAbsolute = createAbsolutePositionFromRelativePosition ( end , doc ) ; text . insert ( endAbsolute . index , '},' ) ; } private _replaceJSXElementChildren ( node , children , doc , text ) { const version = { '0' : getClock ( doc , 0 ) } ; for ( let child of children ) { const pos = createRelativePositionFromStateVector ( text , version , child . end , undefined , true ) ; this . _replace ( child , doc , text ) ; doc . clientID += 1 ; const absoluteEnd = createAbsolutePositionFromRelativePosition ( pos , doc ) ; text . insert ( absoluteEnd . index , ',' ) ; } } private _replaceJSXText ( node , doc , text ) { doc . clientID += 1 ; const version = { '0' : getClock ( doc , 0 ) } ; const startIndex = node . start ; const endIndex = node . end ; const absoluteStart = createAbsolutePositionFromStateVector ( doc , text , version , startIndex , undefined , true ) ; text . insert ( absoluteStart . index , '`' ) ; const absoluteEnd = createAbsolutePositionFromStateVector ( doc , text , version , endIndex , undefined , true ) ; text . insert ( absoluteEnd . index , '`' ) ; } private _replaceJSXExpressionContainer ( node , doc , text ) { const version = { '0' : getClock ( doc , 0 ) } ; const valueStartBegin = createRelativePositionFromStateVector ( text , version , node . start , undefined , true ) ; const valueStartEnd = createRelativePositionFromStateVector ( text , version , node . start + 1 , - 1 , true ) ; const valueEndBegin = createRelativePositionFromStateVector ( text , version , node . end - 1 , undefined , true ) ; const valueEndEnd = createRelativePositionFromStateVector ( text , version , node . end + 1 , - 1 , true ) ; const absoluteValueStartBegin = createAbsolutePositionFromRelativePosition ( valueStartBegin , doc ) ; const absoluteValueStartEnd = createAbsolutePositionFromRelativePosition ( valueStartEnd , doc ) ; text . delete ( absoluteValueStartBegin . index , 1 ) ; const absoluteValueEndBegin = createAbsolutePositionFromRelativePosition ( valueEndBegin , doc ) ; const absoluteValueEndEnd = createAbsolutePositionFromRelativePosition ( valueEndEnd , doc ) ; text . delete ( absoluteValueEndBegin . index , 1 ) ; } private _removeTag ( node , openElement , closeElement , doc , text ) { doc . clientID += 1 ; const version = { '0' : getClock ( doc , 0 ) } ; const openStart = createRelativePositionFromStateVector ( text , version , openElement . start , undefined , true ) ; const openNameEnd = createRelativePositionFromStateVector ( text , version , openElement . name ?. end ?? openElement . start + 1 , - 1 , true ) ; const openEnd = createRelativePositionFromStateVector ( text , version , openElement . end - ( closeElement ? 1 : 2 ) , undefined , true ) ; const closingStart = ! ! closeElement ? createRelativePositionFromStateVector ( text , version , closeElement . start , undefined , true ) : null ; const closingEnd = ! ! closeElement ? createRelativePositionFromStateVector ( text , version , closeElement . end , - 1 , true ) : null ; let attributePositions = [ ] ; for ( let attribute of openElement . attributes ) { if ( attribute . type !== 'JSXSpreadAttribute' ) { const nameStart = createRelativePositionFromStateVector ( text , version , attribute . name . start , undefined , true ) ; const nameEnd = createRelativePositionFromStateVector ( text , version , ! ! attribute . value ? attribute . name . end + 1 : attribute . name . end , - 1 , true ) ; attributePositions . push ( [ nameStart , nameEnd ] ) ; } else { const openBraceStart = createRelativePositionFromStateVector ( text , version , attribute . start , undefined , true ) ; const openBraceEnd = createRelativePositionFromStateVector ( text , version , attribute . start + 1 , undefined , true ) ; const closeBraceStart = createRelativePositionFromStateVector ( text , version , attribute . end - 1 , - 1 , true ) ; const closeBraceEnd = createRelativePositionFromStateVector ( text , version , attribute . end , - 1 , true ) ; attributePositions . push ( [ openBraceStart , openBraceEnd ] ) ; attributePositions . push ( [ closeBraceStart , closeBraceEnd ] ) ; } } for ( let [ start , end ] of attributePositions ) { const nameStart = createAbsolutePositionFromRelativePosition ( start , doc ) ; const nameEnd = createAbsolutePositionFromRelativePosition ( end , doc ) ; text . delete ( nameStart . index , nameEnd . index - nameStart . index ) ; } const openStartAbsolute = createAbsolutePositionFromRelativePosition ( openStart , doc ) ; text . delete ( openStartAbsolute . index , 1 ) ; const openEndAbsolute = createAbsolutePositionFromRelativePosition ( openEnd , doc ) ; text . delete ( openEndAbsolute . index , closeElement ? 1 : 2 ) ; if ( closeElement ) { const closingStartAbsolute = createAbsolutePositionFromRelativePosition ( closingStart , doc ) ; const closingEndAbsolute = createAbsolutePositionFromRelativePosition ( closingEnd , doc ) ; text . delete ( closingStartAbsolute . index , closingEndAbsolute . index - closingStartAbsolute . index ) ; } } private _insertEnergyCheckIntoStatement ( doc , text , statement ) { doc . clientID += 1 ; const version = { '0' : getClock ( doc , 0 ) } ; let startIndex ; let postfix = '' ; let wrapWithBraces = false ; if ( statement . type === 'BlockStatement' ) { startIndex = statement . start + 1 ; postfix = ';' ; } else if ( statement . type === 'ExpressionStatement' ) { startIndex = statement . start ; postfix = ';' ; wrapWithBraces = true ; } else if ( statement . type === 'EmptyStatement' ) { startIndex = statement . start ; } else { startIndex = statement . start ; postfix = ';' ; wrapWithBraces = true ; } this . _insertEnergyCheck ( doc , text , version , startIndex , statement . end , postfix , wrapWithBraces ) ; } private _insertEnergyCheck ( doc , text , version , startIndex , endIndex , postfix , wrapWithBraces ) { if ( wrapWithBraces ) { const absolute = createAbsolutePositionFromStateVector ( doc , text , version , startIndex , undefined , true ) ; text . insert ( absolute . index , '{' ) ; } const absolute = createAbsolutePositionFromStateVector ( doc , text , version , startIndex , undefined , true ) ; text . insert ( absolute . index , ENERGY_CHECK_CALL + postfix ) ; if ( wrapWithBraces ) { const absolute = createAbsolutePositionFromStateVector ( doc , text , version , endIndex , undefined , true ) ; text . insert ( absolute . index , '}' ) ; } } private _replaceAsyncFunction ( node , doc , text ) { doc . clientID += 1 ; const version = { '0' : getClock ( doc , 0 ) } ; const functionStart = createRelativePositionFromStateVector ( text , version , node . start , undefined , true ) ; const functionStartAbsolute = createAbsolutePositionFromRelativePosition ( functionStart , doc ) ; text . delete ( functionStartAbsolute . index , 'async ' . length ) ; } private _replaceAwaitExpression ( node , doc , text ) { doc . clientID += 1 ; const version = { '0' : getClock ( doc , 0 ) } ; const keywordStart = createRelativePositionFromStateVector ( text , version , node . start , undefined , true ) ; const keywordStartAbsolute = createAbsolutePositionFromRelativePosition ( keywordStart , doc ) ; text . delete ( keywordStartAbsolute . index , 'await ' . length ) ; } } const ENERGY_CHECK_CALL = '__energyCheck()' ; export interface TranspilerResult { /**
     * The code that resulted from the transpiler.
     */ code : string ; /**
     * The original code.
     */ original : string ; /**
     * The metadata that the transpiler produced for the code.
     */ metadata : { /**
         * The document that was used to edit the code.
         */ doc : Doc ; /**
         * The text structure that was used to edit the code.
         */ text : Text ; } ; } /**
 * Defines an interface that represents a location in some code by line number and column.
 */ export interface CodeLocation { /**
     * The zero based line number that the location represents.
     */ lineNumber : number ; /**
     * The zero based column number that the location represents.
     */ column : number ; } export function calculateOriginalLineLocation ( result , location ) { const index = calculateIndexFromLocation ( result . code , location ) ; const relative = createRelativePositionFromTypeIndex ( result . metadata . text , index ) ; if ( relative . item . client === 0 ) { return calculateLocationFromIndex ( result . original , relative . item . clock ) ; } let item = getItem ( result . metadata . doc . store , relative . item ) ; while ( item && item . id . client !== 0 ) { item = item . left ; } return calculateLocationFromIndex ( result . original , item . id . clock + item . length ) ; } export function calculateIndexFromLocation ( code , location ) { let line = location . lineNumber ; let column = location . column ; let index = 0 ; for ( ; index < code . length ; index ++ ) { const char = code [ index ] ; if ( line > 0 ) { if ( char === '\n' ) { line -= 1 ; } } else { index += column ; break ; } } return index ; } export function calculateLocationFromIndex ( code , index ) { let line = 0 ; let lastLineIndex = 0 ; for ( let counter = 0 ; counter < code . length && counter < index ; counter ++ ) { const char = code [ counter ] ; if ( char === '\n' ) { line += 1 ; lastLineIndex = counter + 1 ; } } let column = index - lastLineIndex ; return { lineNumber : line , column , } ; }