// wget http://projects.scratch.mit.edu/internalapi/project/21234602/get/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #include <errno.h> #ifndef JSMN_PARENT_LINKS #define JSMN_PARENT_LINKS #endif #define JSMN_STRICT #include "jsmn/jsmn.c" #define JSMN_CHILD 4 // I originally added JSMN_CHILD to the enum, but I reverted jsmn to its original code // and added that element here as a #define. Being careful that it is higher than // the elements in the enum in case the author adds any new elements of his own. // (255 is probably better than 4) char *typename[] = { "primitive", "object", "array", "string", "child" }; typedef struct blockfm { char *name; char *scratchblock; } blockfm; blockfm block[] = { {"append:toList:", "add %s to %m.list\n"}, {"backdrop1", "switch backdrop to %m.backdrop and wait\n"}, {"bounceOffEdge", "if on edge, bounce\n"}, {"broadcast:", "broadcast %m.broadcast\n"}, {"changeGraphicEffect:by:", "change %m.effect effect by %n\n"}, {"changePenHueBy:", "change pen color by %n\n"}, {"changePenShadeBy:", "change pen shade by %n\n"}, {"changePenSizeBy:", "change pen size by %n\n"}, {"changeSizeBy:", "change size by %n\n"}, {"changeTempoBy:", "change tempo by %n\n"}, {"changeVar:by:", "change %m.var by %n\n"}, {"changeVolumeBy:", "change volume by %n\n"}, {"changeXposBy:", "change x by %n\n"}, {"changeYposBy:", "change y by %n\n"}, {"clearPenTrails", "clear\n"}, {"comeToFront", "go to front\n"}, {"createCloneOf", "create clone of %m.spriteOnly\n"}, {"deleteLine:ofList:", "delete %d.listDeleteItem of %m.list\n"}, {"doAsk", "ask %s and wait\n"}, {"doBroadcastAndWait", "broadcast %m.broadcast and wait\n"}, {"doPlaySoundAndWait", "play sound %m.sound until done\n"}, {"doWaitUntil", "wait until %b\n"}, {"drum:duration:elapsed:from:", "play drum %n for %n beats\n"}, {"filterReset", "clear graphic effects\n"}, {"forward:", "move %n steps\n"}, {"glideSecs:toX:y:elapsed:from:", "glide %n secs to x:%n y:%n\n"}, {"goBackByLayers:", "go back %n layers\n"}, {"gotoSpriteOrMouse:", "go to %m.spriteOrMouse\n"}, {"gotoX:y:", "go to x:%n y:%n\n"}, {"heading:", "point in direction %d.direction\n"}, {"hide", "hide\n"}, {"hideAll", "hide all sprites\n"}, {"hideList:", "hide list %m.list\n"}, {"hideVariable:", "hide variable %m.var\n"}, {"insert:at:ofList:", "insert %s at %d.listItem of %m.list\n"}, {"instrument:", "set instrument to %d.instrument\n"}, {"lookLike:", "switch costume to %m.costume\n"}, {"midiInstrument:", "set instrument to %n\n"}, {"nextBackground", "next background\n"}, {"nextCostume", "next costume\n"}, {"nextScene", "next backdrop\n"}, {"noteOn:duration:elapsed:from:", "play note %d.note for %n beats\n"}, {"penColor:", "set pen color to %c\n"}, {"penSize:", "set pen size to %n\n"}, {"playDrum", "play drum %d.drum for %n beats\n"}, {"playSound:", "play sound %m.sound\n"}, {"pointTowards:", "point towards %m.spriteOrMouse\n"}, {"putPenDown", "pen down\n"}, {"putPenUp", "pen up\n"}, {"rest:elapsed:from:", "rest for %n beats\n"}, {"say:", "say %s\n"}, {"say:duration:elapsed:from:", "say %s for %n secs\n"}, {"scrollAlign", "align scene %m.scrollAlign\n"}, {"scrollRight", "scroll right %n\n"}, {"scrollUp", "scroll up %n\n"}, {"setGraphicEffect:to:", "set %m.effect effect to %n\n"}, {"setLine:ofList:to:", "replace item %d.listItem of %m.list with %s\n"}, {"setPenHueTo:", "set pen color to %n\n"}, {"setPenShadeTo:", "set pen shade to %n\n"}, {"setRotationStyle", "set rotation style %m.rotationStyle\n"}, {"setSizeTo:", "set size to %n\\%\n"}, {"setTempoTo:", "set tempo to %n bpm\n"}, {"setVar:to:", "set %m.var to %s\n"}, {"setVideoState", "turn video %m.videoState\n"}, {"setVideoTransparency", "set video transparency to %n\\%\n"}, {"setVolumeTo:", "set volume to %n\\%\n"}, {"show", "show\n"}, {"showBackground:", "switch to background %m.costume\n"}, {"showList:", "show list %m.list\n"}, {"showVariable:", "show variable %m.var\n"}, {"stampCostume", "stamp\n"}, {"startScene", "switch backdrop to %m.backdrop\n"}, {"stopAllSounds", "stop all sounds\n"}, {"think:", "think %s\n"}, {"think:duration:elapsed:from:", "think %s for %n secs\n"}, {"timerReset", "reset timer\n"}, {"turnLeft:", "turn Left %n degrees\n"}, {"turnRight:", "turn Right %n degrees\n"}, {"wait:elapsed:from:", "wait %n secs\n"}, {"xpos:", "set x to %n\n"}, {"ypos:", "set y to %n\n"}, {"doForever", "forever\n%xend\n"}, {"doForeverIf", "forever if %b\n%xend\n"}, {"&", "%b and %b"}, {"|", "%b or %b"}, {"<", "%s < %s"}, {"=", "%s = %s"}, {">", "%s > %s"}, {"not", "not %b"}, {"isLoud", "loud?"}, {"mousePressed", "mouse down?"}, {"color:sees:", "color %c is touching %c?"}, {"keyPressed:", "key %m.key pressed?"}, {"list:contains:", "%m.list contains %s"}, {"touching:", "touching %m.touching?"}, {"touchingColor:", "touching color %c?"}, {"doForLoop", "for each %m.varName in %s\n%xend\n"}, {"doIf", "if %b then\n%xend\n"}, {"doRepeat", "repeat %n\n%xend\n"}, {"doUntil", "repeat until %b\n%xend\n"}, {"doWhile", "while %b\n%xend\n"}, {"doIfElse", "if %b then\n%xelse\n%xend\n"}, {"deleteClone", "delete this clone\n"}, {"doReturn", "stop script\n"}, {"stopAll", "stop all\n"}, {"stopScripts", "stop %m.stop\n"}, {"whenClicked", "\nwhen this sprite clicked\n"}, {"whenCloned", "\nwhen I start as a clone\n"}, {"whenGreenFlag", "\nwhen gf clicked\n"}, {"whenIReceive", "\nwhen I receive %m.broadcast\n"}, {"whenKeyPressed", "\nwhen %m.key key pressed\n"}, {"whenSceneStarts", "\nwhen backdrop switches to %m.backdrop\n"}, {"whenSensorGreaterThan", "\nwhen %m.triggerSensor > %n\n"}, {"%", "%n mod %n"}, {"*", "%n * %n"}, {"+", "%n + %n"}, {"-", "%n - %n"}, {"\\/", "%n / %n"}, {"abs", "abs %n"}, {"answer", "answer"}, {"backgroundIndex", "backdrop #"}, {"computeFunction:of:", "%m.mathOp of %n"}, {"concatenate:with:", "join %s %s"}, {"costumeIndex", "costume #"}, {"distanceTo:", "distance to %m.spriteOrMouse"}, {"getAttribute:of:", "%m.attribute of %m.spriteOrStage"}, {"getLine:ofList:", "item %d.listItem of %m.list"}, {"getUserId", "user id"}, {"getUserName", "username"}, {"heading", "direction"}, {"letter:of:", "letter %n of %s"}, {"lineCountOfList:", "length of %m.list"}, {"mouseX", "mouse x"}, {"mouseY", "mouse y"}, {"randomFrom:to:", "pick random %n to %n"}, {"rounded", "round %n"}, {"scale", "size"}, {"sceneName", "backdrop name"}, {"senseVideoMotion", "video %m.videoMotionType on %m.stageOrThis"}, {"soundLevel", "loudness"}, {"sqrt", "sqrt %n"}, {"stringLength:", "length of %s"}, {"tempo", "tempo"}, {"timeAndDate", "current %m.timeAndDate"}, {"timer", "timer"}, {"timestamp", "days since 2000"}, {"volume", "volume"}, {"xScroll", "x scroll"}, {"xpos", "x position"}, {"yScroll", "y scroll"}, {"ypos", "y position"}, {NULL, NULL} }; #ifndef TRUE #define TRUE (0==0) #define FALSE (0!=0) #endif // When I first coded this, I passed all data around as parameters, but since // this file totally encapsulates the translator anyway, there's no real benefit // from keeping the code 'pure' - and it's a lot easier to read without a bunch // of extra parameters to every procedure. Undoubtedly faster too. static char *js; #define BIGPROG 1000000 static jsmntok_t token[BIGPROG]; static jsmntok_t tree[BIGPROG]; static int codeindent = 0, lastch = '\n'; #define ARG(n) token[root + 1 + (n)].size void indent(int n) {while (n-- > 0) fprintf(stdout, " ");} void codechar(int c) {if (lastch == '\n') indent(codeindent); putchar(lastch = c);} void codestr(char *s) {while (*s != '\0') codechar(*s++);} // if you declare token[] on the stack inside main, (as was originally done following the // example in the jsmn package) you'll get a run time error if BIGPROG is much over 400000 // - it's better to be static or allocated off the heap. int tokstrcmp(jsmntok_t t, char *s) {return ((strncmp(js + t.start, s, t.end - t.start) == 0) && (strlen(s) == (t.end - t.start)));} void showtok(int tok) { int i; if ((token[tok].type != JSMN_OBJECT) && (token[tok].type != JSMN_ARRAY)) { for (i = token[tok].start; i < token[tok].end; i++) codechar(js[i]); } } void HEX(int tok) { #define hexdigit(i) codechar("0123456789ABCDEF"[i&15]) char *s, dec[256]; // need range check int i, hexnum; s = dec; if ((token[tok].type != JSMN_OBJECT) && (token[tok].type != JSMN_ARRAY)) { for (i = token[tok].start; i < token[tok].end; i++) *s++ = js[i]; *s = '\0'; hexnum = atoi(dec); codechar('#'); hexdigit(hexnum >> 20); hexdigit(hexnum >> 16); hexdigit(hexnum >> 12); hexdigit(hexnum >> 8); hexdigit(hexnum >> 4); hexdigit(hexnum); } } void reorder(jsmntok_t * tree, int tree_max, int root, int depth, int display) { static int tokenptr = 0, treeptr = 0; int base, children, i; // This makes up for a deficit in the lightweight json parser. The jsmn // code as supplied requires you to walk *all* leaves of the tree in left // to right order whenever you access the json structure. This is not // useful when you want to rearrange the order of blocks etc // so this code takes the serialised tree and inserts explicit branch // nodes, so that you may follow any path down the tree using your // preferred traverse order and omitting any parts that you are not // interested in. Unfortunately I don't see how to add this to the // original code as it builds the tree, so I have retrofitted it by having // it copy from the original tree to a new copy. Somewhat wasteful of // space, but not really an issue for Scratch json. tree[treeptr].type = token[root].type; tree[treeptr].start = token[root].start; tree[treeptr].end = token[root].end; tree[treeptr].size = token[root].size; #ifdef JSMN_PARENT_LINKS tree[treeptr].parent = token[root].parent; #endif if (treeptr + 1 >= tree_max) { // we could actually calculate the output size exactly with a dummy // pass through the data, allocate enough space off the heap, and then // copy the result back on top of the original array, followed by // freeing the heap space. Checking of course that the original array // is large enough before mallocing the temp workspace... next version // perhaps... fprintf(stderr, "reorder: the output array is not large enough - %d limit reached\n", tree_max); exit(4); } tokenptr += 1; treeptr += 1; base = treeptr; if (token[root].type == JSMN_OBJECT || token[root].type == JSMN_ARRAY) { children = token[root].size; if (treeptr + 1 + children >= tree_max) { fprintf(stderr, "reorder: the output array is not large enough - %d limit exceeded\n", tree_max); exit(5); } treeptr += children; // reserve extra space for the children for (i = 0; i < children; i++) { tree[base + i].type = JSMN_CHILD; tree[base + i].size = treeptr; reorder(tree, tree_max, tokenptr, depth + 3, display); } } } void codegen(int root); void translate(int root); void statement(int root) { int children, i; children = token[root].size; switch ((int)token[root].type) { case JSMN_CHILD: statement(ARG(0)); return; case JSMN_PRIMITIVE: if (js[token[root].start] == 'f') { // false - used for empty boolean parameters } else if (js[token[root].start] == 't') { // true - not sure if it is used in code } else if (js[token[root].start] == 'n') { // null } else { showtok(root); // otherwise a number. (starts with 0..9) } return; case JSMN_STRING: if (tokstrcmp(token[root], "_mouse_")) { codestr("mouse-pointer"); // hack. Would use an array as for the block names, if there were more of these... } else { showtok(root); } break; case JSMN_ARRAY: if (token[ARG(0)].type == JSMN_ARRAY) { // need to check that this one is correct... for (i = 0; i < children; i++) { statement(ARG(i));// possibly more children??? } break; } // TO DO: not handling comments yet. if (tokstrcmp(token[ARG(0)], "readVariable")) { // Handle this with a block string? statement(ARG(1)); } else if (tokstrcmp(token[ARG(0)], "procDef")) { // TO DO. codestr("\ndefine "); showtok(ARG(1)); // followed by number, boolean or string arguments in subarray ARG(2), encoded as %n %b and %s in ARG(1) codestr("\n"); } else if (tokstrcmp(token[ARG(0)], "getParam")) { // TO DO. codestr("(param "); statement(ARG(1)); codestr(")"); } else if (tokstrcmp(token[ARG(0)], "call")) { // TO DO. //codestr("CALL "); statement(ARG(1)); // followed by number, boolean or string arguments in subarray ARG(2), encoded as %n %b and %s in ARG(1) //translate(ARG(2)); // tweak translate to take two parameters - format string, and start of argument list // // watch out for the '%x' code which will need fixing... codestr("\n"); } else { translate(root); } break; default: codestr("[statement: did not expect an unknown jsmn type "); printf("%s", typename[token[root].type]); codestr(" here.]"); fprintf(stderr, "Error.\n"); exit(__LINE__); } } void translate(int root) { int argtype; int each = 0, nextarg = 1, i, j; char c, *format; char menutype[256]; // to do - add range check int found = FALSE; int argstr = ARG(0); // to do - define args do { if (tokstrcmp(token[argstr], block[each].name)) { found = TRUE; format = block[each].scratchblock; i = j = 0; for (;;) { c = format[i]; if (c == '\\') { codechar(format[++i]); } else if (c == '%') { argtype = format[++i]; if (format[i + 1] == '.') { i += 1; while (isalpha(format[++i])) { menutype[j++] = format[i]; } i -= 1; menutype[j] = '\0'; (void)menutype; } switch (argtype) { case 'p': // invented type for procedure parameters nextarg = ARG(2); break; case 'x': // invented type for nested block of text codeindent += 1; statement(ARG(nextarg++)); codeindent -= 1; break; // d, m, & c have a drop-down selector case 'd': if (token[ARG(nextarg)].type == JSMN_PRIMITIVE) { // [n] or [...string...] or [true] etc codestr("("); statement(ARG(nextarg++)); codestr(" v)"); // was [] BEING TESTED } else if (token[ARG(nextarg)].type == JSMN_ARRAY) { // [n] or [...string...] or [true] etc codestr("("); statement(ARG(nextarg++)); codestr(")"); } else if (token[ARG(nextarg)].type == JSMN_STRING) { // [n] or [...string...] or [true] etc codestr("("); statement(ARG(nextarg++)); codestr(" v)"); } else { codestr("{n1:"); codestr(typename[token[ARG(nextarg)].type]); codestr(": "); statement(ARG(nextarg)); codestr("}"); statement(ARG(nextarg++)); } break; case 'm': if (token[ARG(nextarg)].type == JSMN_PRIMITIVE) { // [n] or [...string...] or [true] etc codestr("["); statement(ARG(nextarg++)); codestr(" v]"); } else if (token[ARG(nextarg)].type == JSMN_STRING) { // [n] or [...string...] or [true] etc codestr("["); statement(ARG(nextarg++)); codestr(" v]"); } else { //codestr("{s:"); codestr(typename[token[ARG(nextarg)].type]); codestr(": "); statement(ARG(nextarg)); codestr("}"); codestr("("); statement(ARG(nextarg++)); codestr(")"); //statement(ARG(nextarg++)); } break; case 'c': // colour picker or name??? // if the param is a variable, show it (thus) otherwise: if (token[ARG(nextarg)].type == JSMN_PRIMITIVE) { // [n] or [...string...] or [true] etc codestr("["); HEX(ARG(nextarg++)); codestr("]"); //} else if (token[ARG(nextarg)].type == JSMN_STRING) { // [n] or [...string...] or [true] etc //codestr("["); HEX(ARG(nextarg++)); codestr("]"); } else { codestr("("); statement(ARG(nextarg++)); codestr(")"); } break; // string, number, boolean case 's': // [string] var expr if (token[ARG(nextarg)].type == JSMN_PRIMITIVE) { // [n] or [...string...] or [true] etc codestr("["); statement(ARG(nextarg++)); codestr("]"); } else if (token[ARG(nextarg)].type == JSMN_STRING) { // [n] or [...string...] or [true] etc codestr("["); statement(ARG(nextarg++)); codestr("]"); } else { //codestr("{s:"); codestr(typename[token[ARG(nextarg)].type]); codestr(": "); statement(ARG(nextarg)); codestr("}"); codestr("("); statement(ARG(nextarg++)); codestr(")"); //statement(ARG(nextarg++)); } break; case 'n': // [number] var expr if (token[ARG(nextarg)].type == JSMN_PRIMITIVE) { // [n] or [...string...] or [true] etc codestr("("); statement(ARG(nextarg++)); codestr(")"); // was [] BEING TESTED } else if (token[ARG(nextarg)].type == JSMN_ARRAY) { // [n] or [...string...] or [true] etc codestr("("); statement(ARG(nextarg++)); codestr(")"); } else if (token[ARG(nextarg)].type == JSMN_STRING) { // [n] or [...string...] or [true] etc codestr("("); statement(ARG(nextarg++)); codestr(")"); } else { codestr("{n2:"); codestr(typename[token[ARG(nextarg)].type]); codestr(": "); statement(ARG(nextarg)); codestr("}"); statement(ARG(nextarg++)); } break; case 'b': // <boolean> codestr("<"); statement(ARG(nextarg++)); codestr(">"); break; default: fprintf(stderr, "warning: unknown format type %%%c\n", c); statement(ARG(nextarg++)); } } else if (c == '\0') { break; } else { codechar(c); } i += 1; } } each += 1; } while (block[each].name != NULL); if (!found) { codestr("[% unknown block '"); showtok(ARG(0)); codestr("' %]"); } } void codeblock(int root) {int i; for (i = 0; i < token[root].size; i++) statement(ARG(i));} void section(int root) {codeblock(ARG(2));} void codegen(int root) {int i; for (i = 0; i < token[root].size; i++) section(ARG(i));} void tree_walk(int root, int depth, int display) { int keyword; // token index int children, i; children = token[root].size; switch (token[root].type) { case JSMN_OBJECT: if (display) putchar('{'); for (i = 0; i < children; i++) { if ((i & 1) == 0) { if (display) {putchar('\n'); indent(depth + 1);} keyword = ARG(i); } else { if (display) printf(": "); } if (i && ((i & 1) == 1) && (tokstrcmp(token[keyword], "variables") || tokstrcmp(token[keyword], "contents") || tokstrcmp(token[keyword], "lists") || tokstrcmp(token[keyword], "children") || tokstrcmp(token[keyword], "sounds") || tokstrcmp(token[keyword], "scripts") || tokstrcmp(token[keyword], "costumes") || tokstrcmp(token[keyword], "scriptComments"))) { // now that we've added explicit child links using the // 'reorder' procedure, we can skip the subtree entirely, // without reqiring to traverse it all in order to step // over data and reach the following sibling... if (tokstrcmp(token[keyword], "scripts")) { fflush(stdout); codestr("\n[scratchblocks]\n"); codegen(ARG(i)); codestr("[/scratchblocks]\n"); fflush(stdout); } else if (tokstrcmp(token[keyword], "children")) { tree_walk(ARG(i), depth, display); // sprites that hang off STAGE } else { if (display) fprintf(stdout, " << skipped >> "); } } else { tree_walk(ARG(i), depth + 1, display); } if ((i & 1) && ((i + 1) != children)) { if (display) printf(","); } } if (display) {putchar('\n'); indent(depth); putchar('}');} break; case JSMN_ARRAY: if ((depth == 0) || (depth == 2)) { // list of sprite files for (i = 0; i < children; i++) tree_walk(ARG(i), depth + 1, display); } else if (depth == 1) { // list of blocks x y subblock for (i = 2; i < children; i++) tree_walk(ARG(i), depth + 1, display); } else if (depth >= 3) { // single statement fprintf(stderr, "unexpected ARRAY at top level\n"); exit(__LINE__); } break; default: if (display) showtok(root); } } // On windows, you can remove the memory mapping code and just read the // file int a malloc()ed array with stdio. #include <fcntl.h> // open #include <sys/mman.h> // mmap #include <unistd.h> // lseek int main(int argc, char **argv) { int i, r; jsmn_parser p; int json_fd; off_t flen; if (argc != 2) { fprintf(stderr, "syntax: %s file.json\n", argv[1]); exit(1); } json_fd = open(argv[1], O_RDONLY); if (json_fd == -1) { fprintf(stderr, "%s - %s\n", argv[0], strerror(errno)); exit(2); } flen = lseek(json_fd, (off_t) 0L, SEEK_END); js = mmap(NULL, flen, PROT_READ, MAP_SHARED, json_fd, (off_t) 0L); jsmn_init(&p); r = jsmn_parse(&p, js, strlen(js), token, BIGPROG); if (r <= 0) { fprintf(stderr, "syntax: %s - parse failed\n", argv[1]); exit(3); } reorder(tree, BIGPROG, 0, 0, FALSE); for (i = 0; i < BIGPROG; i++) token[i] = tree[i]; tree_walk(0, 0, FALSE); exit(0); return 0; }