├── LICENSE ├── Makefile ├── README.md ├── js2py.coffee ├── package.json ├── prelude.py └── tests ├── arrays.js ├── loops.js ├── scope.js ├── strings.js ├── switch.js └── typeof.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Jez Ng 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = $(wildcard tests/*.js) 2 | LIB_COFFEE = $(wildcard *.coffee) 3 | 4 | test: $(TESTS:.js=.result) js2py.js 5 | 6 | %.actual: %.py 7 | @python $? > $@ 8 | 9 | %.py: %.js js2py.js 10 | @node js2py $< > $@ 11 | 12 | %.expected: %.js 13 | @node $? > $@ 14 | 15 | %.result: %.actual %.expected 16 | @diff $? 17 | @echo "$@ passed" 18 | 19 | js2py.js: js2py.coffee 20 | coffee -c $< 21 | 22 | .SECONDARY: 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | js2py 2 | ===== 3 | 4 | A Javascript-to-Python translation assistant. It does a simple translation from 5 | Javascript to Python (as detailed below). You'll most likely need to make 6 | further changes by hand, but this automates part of the work. 7 | 8 | I thought it would be nice to be able to `import` some libraries directly into 9 | Python, rather than shelling out to NodeJS. This script is the result of trying 10 | to make that a reality. 11 | 12 | I've successfully used it to port [Esprima][1] to Python -- the result is 13 | [PyEsprima][2]. 14 | 15 | Setup & Usage 16 | ------------- 17 | 18 | npm install 19 | ./js2py.coffee file.js > out.py 20 | 21 | Transformations and Shims 22 | ------------------------- 23 | 24 | * Create `global` declarations where necessary 25 | * Transform simple prototype-based classes into corresponding Python classes 26 | (but no inheritance) 27 | * Remove assignment statements from conditional tests 28 | * Convert switch statements into if-else chains 29 | * Convert pre-/post-increments into assignment statements 30 | * `Array.prototype.slice` and `String.prototype.substr` are converted to 31 | Python's slice notation 32 | * Function expressions are hoisted out as fully declared functions since 33 | Python's lambdas are limited 34 | * Shims for RegExp and JS-style dictionaries 35 | * Some support for `typeof` 36 | 37 | Limitations 38 | ----------- 39 | 40 | * No support for modifying non-global nonlocals (but easy enough to add with 41 | the `nonlocal` keyword in Python 3) 42 | * `a[i]` in JS will return `undefined` if `i >= a.length`, but in Python it 43 | raises an `IndexError`. 44 | * No support for `call` / `apply`. 45 | * There many more; pull requests are welcome! 46 | 47 | [1]: https://github.com/ariya/esprima 48 | [2]: https://github.com/int3/pyesprima 49 | -------------------------------------------------------------------------------- /js2py.coffee: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env coffee 2 | 3 | esprima = require 'esprima' 4 | estraverse = require 'estraverse' 5 | match = require 'pattern-match' 6 | 7 | class LinePrinter 8 | constructor: -> 9 | @indent = 0 10 | @lines = [] 11 | 12 | addLine: (content) -> 13 | @lines.push [@indent, content] 14 | return 15 | 16 | print: -> 17 | for line in @lines 18 | [indent, content] = line 19 | for i in [0...indent] 20 | process.stdout.write ' ' 21 | process.stdout.write content + '\n' 22 | 23 | estraverse.Syntax.PySlice = 'PySlice' 24 | estraverse.Syntax.PyClass = 'PyClass' 25 | estraverse.VisitorKeys.PySlice = ['callee', 'arguments'] 26 | estraverse.VisitorKeys.PyClass = ['id', 'methods'] 27 | 28 | 29 | tempNameCount = 0 30 | getTempName = -> "__temp__#{tempNameCount++}" 31 | 32 | extractSideEffects = (c) -> 33 | statements = [] 34 | expr = estraverse.replace c, 35 | enter: (c, parent) -> 36 | switch c.type 37 | when 'AssignmentExpression' 38 | statements.push c 39 | return c.left 40 | when 'CallExpression' 41 | left = { type: 'Identifier', name: getTempName() } 42 | statements.push { 43 | type: 'AssignmentExpression', 44 | operator: '=', left: left, right: c 45 | } 46 | return left 47 | return { expr, statements } 48 | 49 | transform = (c) -> 50 | root = c 51 | RESERVED_IDENTS = [ 'len', 'print', 'list', 'assert', 'str' ] 52 | 53 | classes = {} 54 | 55 | # Pattern match for classes, and create PyClass nodes 56 | estraverse.replace c, 57 | enter: (c, parent) -> 58 | switch c.type 59 | when 'FunctionDeclaration' 60 | if c.id and /[A-Z]/.test c.id.name[0] 61 | cls = { 62 | type: 'PyClass' 63 | id: { type: 'Identifier', name: c.id.name } 64 | methods: [ c ] 65 | } 66 | classes[cls.id.name] = cls 67 | c.id.name = '__init__' 68 | c.params.unshift({ type: 'Identifier', name: 'self' }) 69 | return cls 70 | when 'AssignmentExpression' 71 | if c.left.type is 'MemberExpression' and c.left.property.name is 'prototype' 72 | clsName = c.left.object.name 73 | if clsName not of classes 74 | throw new Error "Could not find class #{clsName}" 75 | c.right.class = classes[clsName] 76 | return 77 | when 'Property' 78 | if parent.class and c.value.type is 'FunctionExpression' 79 | parent.class.methods.push c.value 80 | c.value.type = 'FunctionDeclaration' 81 | c.value.params.unshift { type: 'Identifier', name: 'self' } 82 | c.value.id = c.key 83 | return 84 | 85 | leave: (c, parent) -> 86 | switch c.type 87 | when 'AssignmentExpression' 88 | if c.right.class 89 | return null 90 | 91 | currentFunction = null 92 | visibleFunctions = null 93 | tryGetType = (c) -> 94 | if not c then return true 95 | if c.type is 'Literal' 96 | return typeof c.value 97 | else if c.type is 'ObjectExpression' 98 | return 'object' 99 | return true 100 | 101 | # Find functions that modify globals, because Python needs 'global varname'. 102 | # Also hoist out function expressions, because Python's lambdas do not support statements. 103 | estraverse.replace c, 104 | enter: (c, parent) -> 105 | switch c.type 106 | when 'Program' 107 | currentFunction = c 108 | currentFunction.vars = {} 109 | currentFunction.globalVars = {} 110 | visibleFunctions = [ currentFunction ] 111 | when 'FunctionDeclaration', 'FunctionExpression' 112 | currentFunction = c 113 | visibleFunctions.push currentFunction 114 | currentFunction.vars = {} 115 | for param in c.params 116 | currentFunction.vars[param.name] = true 117 | currentFunction.globalVars = {} 118 | when 'VariableDeclarator' 119 | currentFunction.vars[c.id.name] = tryGetType c.init 120 | return 121 | leave: (c, parent) -> 122 | switch c.type 123 | when 'Program', 'FunctionDeclaration', 'FunctionExpression' 124 | visibleFunctions.pop() 125 | currentFunction = visibleFunctions[visibleFunctions.length - 1] 126 | if c.type is 'FunctionExpression' 127 | c.type = 'FunctionDeclaration' 128 | c.id = { type: 'Identifier', name: getTempName() } 129 | leavelist = this.__leavelist 130 | if leavelist[leavelist.length - 2].node.type is 'ObjectExpression' and # TODO handle assignment 131 | leavelist[leavelist.length - 3].node.type is 'AssignmentExpression' 132 | c.hoistedName = leavelist[leavelist.length-3].node.left 133 | body = currentFunction.body 134 | if body.body? then body = body.body 135 | body.unshift c 136 | return c.id 137 | 138 | catchClauses = [] # TODO make this more precise 139 | 140 | # passes that need to know the current function go here 141 | estraverse.replace c, 142 | enter: (c, parent) -> 143 | switch c.type 144 | when 'Program', 'FunctionDeclaration' 145 | currentFunction = c 146 | visibleFunctions.push c 147 | return 148 | when 'ThisExpression' 149 | if currentFunction.hoistedName 150 | return currentFunction.hoistedName 151 | 152 | leave: (c, parent) -> 153 | switch c.type 154 | when 'Program' 155 | visibleFunctions.pop() 156 | currentFunction = visibleFunctions[visibleFunctions.length - 1] 157 | return 158 | when 'FunctionDeclaration' 159 | visibleFunctions.pop() 160 | currentFunction = visibleFunctions[visibleFunctions.length - 1] 161 | return 162 | when 'UpdateExpression' 163 | if c.argument.type is 'Identifier' and c.argument.name not of currentFunction.vars 164 | currentFunction.globalVars[c.argument.name] = 'number' 165 | return 166 | when 'AssignmentExpression' 167 | if c.left.type is 'Identifier' 168 | if c.left.name not of currentFunction.vars 169 | currentFunction.globalVars[c.left.name] = tryGetType c.right 170 | else 171 | currentFunction.vars[c.left.name] = tryGetType c.right 172 | return 173 | 174 | estraverse.replace c, 175 | enter: (c, parent) -> 176 | switch c.type 177 | when 'Identifier' 178 | if RESERVED_IDENTS.indexOf(c.name) >= 0 and not c.generated 179 | c.name += '__py__' 180 | else if c.name is 'Error' 181 | c.name = 'RuntimeError' 182 | else if c.name is 'String' 183 | c.name = 'unicode' # TODO make more precise 184 | c 185 | when 'CallExpression' 186 | c = match c, ((when_) -> 187 | when_({ 188 | callee: { 189 | property: { type: 'Identifier', name: 'slice' } 190 | } 191 | }, (-> { 192 | type: 'PySlice' 193 | callee: c.callee.object 194 | arguments: c.arguments 195 | }), @) 196 | when_({ 197 | callee: { 198 | property: { type: 'Identifier', name: 'substr' } 199 | } 200 | }, (-> { 201 | type: 'PySlice' 202 | callee: c.callee.object 203 | arguments: [c.arguments[0], { 204 | type: 'BinaryExpression' 205 | operator: '+' 206 | left: c.arguments[0] 207 | right: c.arguments[1] 208 | }] 209 | }), @) 210 | when_({ 211 | callee: { 212 | property: { type: 'Identifier', name: 'charCodeAt' } 213 | } 214 | }, (-> { 215 | type: 'ConditionalExpression' 216 | test: { 217 | type: 'BinaryExpression', operator: '<', 218 | left: c.arguments[0], right: { 219 | type: 'CallExpression', 220 | callee: { type: 'Identifier', name: 'len', generated: true } 221 | arguments: [c.callee.object] 222 | } 223 | } 224 | consequent: { 225 | type: 'CallExpression' 226 | callee: { type: 'Identifier', name: 'ord' } 227 | arguments: [{ 228 | type: 'MemberExpression' 229 | object: c.callee.object 230 | property: c.arguments[0] 231 | computed: true 232 | }] 233 | } 234 | alternate: { type: 'Identifier', name: 'None' } 235 | }), @) 236 | when_({ 237 | callee: { 238 | property: { type: 'Identifier', name: 'push' } 239 | } 240 | }, (-> 241 | c.callee.property.name = 'append' 242 | c 243 | ), @) 244 | when_({ 245 | callee: { 246 | object: { type: 'Identifier', name: 'push' } 247 | } 248 | }, (-> 249 | c.callee.property.name = 'append' 250 | c 251 | ), @) 252 | when_({ 253 | callee: { 254 | object: { type: 'ArrayExpression' } 255 | property: { type: 'Identifier', name: 'indexOf' } 256 | } 257 | }, (-> { 258 | type: 'CallExpression' 259 | callee: { type: 'Identifier', name: 'list_indexOf' } 260 | arguments: [c.callee.object, c.arguments[0]] 261 | }), @) 262 | when_({ 263 | callee: { 264 | property: { type: 'Identifier', name: 'toString' } 265 | } 266 | }, (-> { 267 | type: 'CallExpression' 268 | callee: { type: 'Identifier', name: 'unicode', generated: true } 269 | arguments: [c.callee.object] 270 | }), @) 271 | when_({ 272 | callee: { 273 | object: { 274 | object: { 275 | object: { type: 'Identifier', name: 'Object' } 276 | property: { type: 'Identifier', name: 'prototype' } 277 | } 278 | property: { type: 'Identifier', name: 'hasOwnProperty' } 279 | } 280 | property: { type: 'Identifier', name: 'call' } 281 | } 282 | }, (-> { 283 | type: 'BinaryExpression' 284 | operator: 'in' 285 | left: c.arguments[1] 286 | right: c.arguments[0] 287 | }), @) 288 | when_(match.any, (-> c), @)) 289 | return c 290 | when 'MemberExpression' 291 | match c, ((when_) -> 292 | when_({ 293 | property: { 294 | type: 'Identifier', name: 'length' 295 | } 296 | }, (-> { 297 | type: 'CallExpression' 298 | callee: { type: 'Identifier', name: 'len', generated: true } 299 | arguments: [c.object] 300 | generated: true 301 | }), @) 302 | when_({ 303 | object: { type: 'Identifier', name: 'String' } 304 | property: { type: 'Identifier', name: 'fromCharCode' } 305 | }, (-> { 306 | type: 'Identifier', name: 'unichr' 307 | }), @) 308 | when_({ 309 | object: { type: 'Literal', value: match.string } 310 | property: { type: 'Identifier', name: 'indexOf' } 311 | }, (-> { 312 | type: c.type 313 | object: c.object 314 | property: { type: 'Identifier', name: 'find' } 315 | }), @) 316 | when_({ 317 | property: { type: 'Identifier', name: 'toLowerCase' } 318 | }, (-> { 319 | type: c.type 320 | object: c.object 321 | property: { type: 'Identifier', name: 'lower' } 322 | }), @) 323 | when_(match.any, (->), @)) 324 | when 'CatchClause' 325 | catchClauses.push(c) 326 | return 327 | 328 | leave: (c, parent) -> 329 | switch c.type 330 | when 'ForStatement' 331 | body = ensure_block c.body 332 | body.body.unshift { 333 | type: 'IfStatement' 334 | test: { 335 | type: 'UnaryExpression', operator: '!', prefix: true, argument: c.test 336 | } 337 | consequent: { type: 'BreakStatement' } 338 | } 339 | body.body.push c.update 340 | { 341 | type: 'WhileStatement' 342 | test: { type: 'Literal', value: 1 } 343 | body: body 344 | prelude: [c.init] 345 | } 346 | when 'DoWhileStatement' 347 | body = ensure_block c.body 348 | body.body.push { 349 | type: 'IfStatement' 350 | test: { 351 | type: 'UnaryExpression', operator: '!', prefix: true, argument: c.test 352 | } 353 | consequent: { type: 'BreakStatement' } 354 | } 355 | { 356 | type: 'WhileStatement' 357 | test: { type: 'Literal', value: 1 } 358 | body: body 359 | } 360 | when 'WhileStatement' 361 | c.body = ensure_block c.body 362 | {expr, statements} = extractSideEffects c.test 363 | Array::push.apply c.body.body, statements 364 | c.test = expr 365 | c.prelude ?= [] 366 | Array::push.apply c.prelude, statements 367 | return 368 | when 'SwitchStatement' 369 | body = [] 370 | firstClause = currentClause = { 371 | alternate: null 372 | } 373 | caseGroup = [] 374 | for cas in c.cases when cas.test 375 | caseGroup.push cas.test 376 | if cas.consequent.length == 0 377 | continue 378 | currentClause.alternate = { 379 | type: 'IfStatement' 380 | test: caseGroup.map((test) -> { 381 | type: 'BinaryExpression' 382 | operator: '===' 383 | left: c.discriminant 384 | right: test 385 | }).reduce(((all_cases, cas) -> 386 | if all_cases is null 387 | return cas 388 | { 389 | type: 'BinaryExpression' 390 | operator: '||' 391 | left: cas 392 | right: all_cases 393 | } 394 | ), null) 395 | consequent: { 396 | type: 'BlockStatement' 397 | body: cas.consequent 398 | } 399 | alternate: null 400 | } 401 | currentClause = currentClause.alternate 402 | caseGroup = [] 403 | lastCase = c.cases[c.cases.length-1] 404 | if lastCase.test is null 405 | currentClause.alternate = { 406 | type: 'BlockStatement' 407 | body: lastCase.consequent 408 | } 409 | body = [ 410 | firstClause.alternate, 411 | { type: 'BreakStatement', label: null } 412 | ] 413 | { 414 | type: 'WhileStatement' 415 | test: { type: 'Literal', value: 1 } 416 | body: { type: 'BlockStatement', body: body } 417 | } 418 | when 'BinaryExpression' 419 | if c.operator is 'instanceof' 420 | if c.right.type is 'Identifier' and c.right.name is 'String' 421 | right = { type: 'Identifier', name: 'str' } 422 | else 423 | right = c.right 424 | { 425 | type: 'CallExpression' 426 | callee: { type: 'Identifier', name: 'isinstance' } 427 | arguments: [c.left, right] 428 | } 429 | when 'ThrowStatement' 430 | if (lastCatch = catchClauses[catchClauses.length - 1]) and 431 | c.argument.type is 'Identifier' and 432 | c.argument.name is lastCatch.param.name 433 | c.argument = null 434 | return 435 | when 'CatchClause' 436 | catchClauses.pop() 437 | return 438 | c 439 | 440 | ensure_block = (c) -> 441 | if c.type isnt 'BlockStatement' 442 | return { 443 | type: 'BlockStatement' 444 | body: [c] 445 | } 446 | return c 447 | 448 | generate = (c) -> 449 | p = new LinePrinter 450 | isElse = false 451 | 452 | SIDE_EFFECT_FREE = ['Literal', 'Identifier', 'AssignmentExpression', 'UpdateExpression'] 453 | 454 | maybeParens = (expr) -> 455 | s = walk expr 456 | if expr.type not in ['BinaryExpression', 'UnaryExpression', 'LogicalExpression'] 457 | s 458 | else 459 | "(#{s})" 460 | 461 | maybeTruthy = (expr) -> 462 | s = walk expr 463 | if expr.type in ['LogicalExpression', 'BinaryExpression', 'UnaryExpression', 'Literal'] 464 | s 465 | else 466 | "#{s} not in [None, False, '']" 467 | 468 | walk = (c) -> 469 | return if c is null 470 | switch c.type 471 | when 'Program' 472 | for s in c.body 473 | walk s 474 | when 'BlockStatement' 475 | p.indent++ 476 | for s in c.body 477 | walk s 478 | p.indent-- 479 | when 'PyClass' 480 | p.addLine "class #{c.id.name}(object):" 481 | p.indent++ 482 | for m in c.methods 483 | walk m 484 | p.indent-- 485 | when 'FunctionDeclaration' 486 | p.addLine "def #{c.id.name}(#{c.params.map((p) -> (walk p) + '=None').join ', '}):" 487 | globals = Object.keys c.globalVars 488 | if globals.length > 0 489 | p.indent++ 490 | p.addLine "global #{globals.join ", "}" 491 | p.indent-- 492 | walk c.body 493 | p.addLine '' 494 | when 'IfStatement' 495 | p.addLine "#{if c.isElse then 'el' else ''}if #{maybeTruthy c.test}:" 496 | walk ensure_block c.consequent 497 | if c.alternate 498 | if c.alternate.type is 'IfStatement' 499 | c.alternate.isElse = true 500 | alternate = c.alternate 501 | else 502 | p.addLine "else:" 503 | alternate = ensure_block c.alternate 504 | walk alternate 505 | when 'WhileStatement' 506 | if c.prelude 507 | c.prelude.map(walk) 508 | p.addLine "while #{maybeTruthy c.test}:" 509 | walk c.body 510 | when 'VariableDeclaration' 511 | for d in c.declarations 512 | walk d 513 | when 'VariableDeclarator' 514 | if c.init isnt null then p.addLine "#{walk c.id} = #{walk c.init}" 515 | else p.addLine "#{walk c.id} = None" 516 | when 'AssignmentExpression' 517 | left = walk c.left 518 | p.addLine "#{left} #{c.operator} #{walk c.right}" 519 | left 520 | when 'BreakStatement' 521 | p.addLine 'break' 522 | when 'ReturnStatement' 523 | p.addLine "return #{if c.argument isnt null then walk c.argument else ''}" 524 | when 'ExpressionStatement' 525 | ex = walk c.expression 526 | if c.expression and \ 527 | c.expression.type not in SIDE_EFFECT_FREE 528 | p.addLine ex 529 | when 'SequenceExpression' 530 | c.expressions.map((expr) -> 531 | value = walk(expr) 532 | if expr.type not in SIDE_EFFECT_FREE 533 | p.addLine value 534 | )[c.expressions.length-1] 535 | when 'CallExpression', 'NewExpression' 536 | "#{walk c.callee}(#{c.arguments.map(walk).join ', '})" 537 | when 'ThrowStatement' 538 | p.addLine "raise #{(walk c.argument) ? ''}" 539 | when 'TryStatement' 540 | p.addLine "try:" 541 | walk c.block 542 | walk c.handlers[0] 543 | if c.finalizer 544 | p.addLine "finally:" 545 | walk c.finalizer 546 | when 'CatchClause' 547 | p.addLine "except Exception as #{walk c.param}:" 548 | walk c.body 549 | when 'PySlice' 550 | "#{walk c.callee}[#{maybeParens c.arguments[0]}:#{maybeParens c.arguments[1]}]" 551 | when 'ConditionalExpression' 552 | "(#{walk c.consequent} if #{maybeTruthy c.test} else #{walk c.alternate})" 553 | when 'MemberExpression' 554 | if c.computed 555 | "#{walk c.object}[#{walk c.property}]" 556 | else 557 | "#{walk c.object}.#{walk c.property}" 558 | when 'UpdateExpression' 559 | # XXX dangerous 560 | v = walk c.argument 561 | p.addLine "#{v} #{c.operator[0]}= 1" 562 | if c.prefix 563 | v 564 | else 565 | "#{v} #{if c.operator[0] is '+' then '-' else '+'} 1" 566 | when 'BinaryExpression', 'LogicalExpression' 567 | op = switch c.operator 568 | when '||' then 'or' 569 | when '&&' then 'and' 570 | when '===' then '==' 571 | when '!==' then '!=' 572 | when '==', '!=' then throw new Error('Unsupported') 573 | else c.operator 574 | "#{maybeParens c.left} #{op} #{maybeParens c.right}" 575 | when 'UnaryExpression' 576 | if c.operator is '!' 577 | "not #{maybeParens c.argument}" 578 | else if c.operator is 'delete' 579 | "del #{maybeParens c.argument}" 580 | else if c.operator is '-' 581 | "-#{maybeParens c.argument}" 582 | else if c.operator is 'typeof' 583 | v = walk c.argument 584 | if c.argument.type is 'Identifier' 585 | precheck = "'#{v}' in locals()" 586 | else if c.argument.type is 'MemberExpression' and \ 587 | typeof c.argument.property.value != 'number' 588 | precheck = "('#{walk c.argument.property}' in #{walk c.argument.object})" 589 | if precheck 590 | "'undefined' if not #{precheck} else typeof(#{v})" 591 | else 592 | "typeof(#{v})" 593 | else 594 | throw 'NYI: ' + c.operator 595 | when 'ThisExpression' 596 | return 'self' 597 | when 'Identifier' 598 | return c.name 599 | when 'ArrayExpression' 600 | "[#{c.elements.map(walk).join ', '}]" 601 | when 'ObjectExpression' 602 | rv = 'jsdict({\n' 603 | for prop in c.properties 604 | rv += "\"#{prop.key.name ? prop.key.value}\": #{walk prop.value},\n" 605 | rv += '})' 606 | when 'Literal' 607 | if c.value is null 608 | 'None' 609 | else if typeof c.value == 'string' 610 | v = c.value 611 | out = '' 612 | hasUniEscape = false 613 | for i in [0...v.length] 614 | ch = v.charCodeAt i 615 | if ch > 256 616 | uni = ch.toString 16 617 | out += '\\u' + ('0' for i in [0...4 - uni.length]).join('') + uni 618 | hasUniEscape = true 619 | else if ch < 32 or ch > 126 620 | hex = ch.toString 16 621 | out += '\\x' + ('0' for i in [0...2 - hex.length]).join('') + hex 622 | hasUniEscape = true 623 | else if v[i] is '\\' or v[i] is '"' 624 | out += '\\' + v[i] 625 | else 626 | out += v[i] 627 | "#{if hasUniEscape then 'u' else ''}\"#{out}\"" 628 | else if typeof c.value is 'boolean' 629 | stringRep = c.value + '' 630 | stringRep.charAt(0).toUpperCase() + stringRep.slice(1) 631 | else if c.value instanceof RegExp 632 | "RegExp(r'#{c.value.source}')" 633 | else 634 | c.value 635 | else 636 | console.log JSON.stringify c 637 | throw "NYI: #{c.type} at #{c.loc.start.line}" 638 | 639 | walk transform c 640 | path = require 'path' 641 | console.log(fs.readFileSync(path.join(__dirname, 'prelude.py'), 'utf-8')) 642 | p.print() 643 | 644 | if require.main == module 645 | fs = require 'fs' 646 | generate(esprima.parse (fs.readFileSync process.argv[2]), loc:true) 647 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "py2js", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "esprima": "*", 6 | "estraverse": "*", 7 | "pattern-match": "*" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /prelude.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | from __future__ import print_function 3 | import re, json 4 | def typeof(t): 5 | if t is None: return 'undefined' 6 | elif isinstance(t, bool): return 'boolean' 7 | elif isinstance(t, str): return 'string' 8 | elif isinstance(t, int) or isinstance(t, float): return 'number' 9 | elif hasattr(t, '__call__'): return 'function' 10 | else: return 'object' 11 | 12 | def list_indexOf(l, v): 13 | try: 14 | return l.index(v) 15 | except: 16 | return -1 17 | 18 | parseFloat = float 19 | parseInt = int 20 | 21 | class jsdict(object): 22 | def __init__(self, d): 23 | self.__dict__.update(d) 24 | def __getitem__(self, name): 25 | if name in self.__dict__: 26 | return self.__dict__[name] 27 | else: 28 | return None 29 | def __setitem__(self, name, value): 30 | self.__dict__[name] = value 31 | return value 32 | def __getattr__(self, name): 33 | try: 34 | return getattr(self, name) 35 | except: 36 | return None 37 | def __setattr__(self, name, value): 38 | self[name] = value 39 | return value 40 | def __contains__(self, name): 41 | return name in self.__dict__ 42 | def __repr__(self): 43 | return str(self.__dict__) 44 | 45 | class RegExp(object): 46 | def __init__(self, pattern, flags=''): 47 | self.flags = flags 48 | pyflags = 0 | re.M if 'm' in flags else 0 | re.I if 'i' in flags else 0 49 | self.source = pattern 50 | self.pattern = re.compile(pattern, pyflags) 51 | def test(self, s): 52 | return self.pattern.search(s) is not None 53 | 54 | console = jsdict({"log":print}) 55 | JSON = jsdict({"stringify": lambda a,b=None,c=None:json.dumps(a, default=b, indent=c)}) 56 | -------------------------------------------------------------------------------- /tests/arrays.js: -------------------------------------------------------------------------------- 1 | console.log(['a','e','i','o','u'].indexOf('i')); 2 | console.log(['a','e','i','o','u'].indexOf('z')); 3 | -------------------------------------------------------------------------------- /tests/loops.js: -------------------------------------------------------------------------------- 1 | for (var i = 0; i < 10; i++) { 2 | console.log(i); 3 | } 4 | 5 | var arr = [ 1, 2, 3, 5, 7, 11 ]; 6 | for (var i = 0; i < arr.length; i++) { 7 | console.log(i, arr[i]); 8 | } 9 | 10 | i = 11; 11 | do { 12 | console.log(i); 13 | } while (i < 10) 14 | 15 | while (i < 10) { 16 | console.log(i); 17 | } 18 | 19 | i = 0; 20 | 21 | while (i < 10) { 22 | console.log(i); 23 | i++; 24 | } 25 | -------------------------------------------------------------------------------- /tests/scope.js: -------------------------------------------------------------------------------- 1 | var bar = 1; 2 | var bat = 1; 3 | 4 | function foo(bat) { 5 | var baz = 0; 6 | bar++; 7 | baz++; 8 | bat++; 9 | console.log(bar); 10 | console.log(baz); 11 | console.log(bat); 12 | } 13 | 14 | foo(0); 15 | console.log(bar); 16 | console.log(bat); 17 | -------------------------------------------------------------------------------- /tests/strings.js: -------------------------------------------------------------------------------- 1 | var s = 'hello world!'; 2 | for (var i = 0; i < s.length; i++) { 3 | console.log(s.charCodeAt(i)); 4 | } 5 | 6 | for (var i = 0; i < s.length; i++) { 7 | for (var j = 0; j < s.length; j++) { 8 | console.log(s.slice(i, j)); 9 | } 10 | } 11 | 12 | for (var i = 0; i < 10; i++) { 13 | console.log(String.fromCharCode(97 + i)); 14 | } 15 | 16 | for (var i = 0; i < 26; i++) { 17 | console.log('aeiou'.indexOf(String.fromCharCode(97 + i))); 18 | } 19 | -------------------------------------------------------------------------------- /tests/switch.js: -------------------------------------------------------------------------------- 1 | for (var a = 0; a < 6; a++) { 2 | switch (a) { 3 | case 1: 4 | case 2: 5 | console.log('a'); 6 | break; 7 | case 3: 8 | case 4: 9 | console.log('b'); 10 | break; 11 | default: 12 | console.log('c'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/typeof.js: -------------------------------------------------------------------------------- 1 | console.log(typeof foo); 2 | foo = 1; 3 | console.log(typeof foo); 4 | foo = [1]; 5 | console.log(typeof foo); 6 | foo = 'wat'; 7 | console.log(typeof foo); 8 | //foo = {}; 9 | //console.log(typeof foo.bar); 10 | --------------------------------------------------------------------------------