From aafe9945081c96c42fb82772498875402914a0a7 Mon Sep 17 00:00:00 2001 From: Scott Date: Thu, 25 Apr 2024 08:58:48 +0100 Subject: [PATCH 1/2] feat(json.sk): add additional skulpt package to enable use of json package --- public/shims/json.sk/LICENSE | 22 + public/shims/json.sk/README.md | 72 ++ public/shims/json.sk/__init__.js | 98 +++ public/shims/json.sk/bower.json | 27 + public/shims/json.sk/stringify.js | 642 ++++++++++++++++++ .../Runners/PythonRunner/PythonRunner.jsx | 6 + 6 files changed, 867 insertions(+) create mode 100644 public/shims/json.sk/LICENSE create mode 100644 public/shims/json.sk/README.md create mode 100644 public/shims/json.sk/__init__.js create mode 100644 public/shims/json.sk/bower.json create mode 100644 public/shims/json.sk/stringify.js diff --git a/public/shims/json.sk/LICENSE b/public/shims/json.sk/LICENSE new file mode 100644 index 000000000..f5cf953f1 --- /dev/null +++ b/public/shims/json.sk/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Trinket + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/public/shims/json.sk/README.md b/public/shims/json.sk/README.md new file mode 100644 index 000000000..186688d8c --- /dev/null +++ b/public/shims/json.sk/README.md @@ -0,0 +1,72 @@ +json.sk +======= + +JSON module for Skulpt + +## Current Support + +This module is an attempt to reproduce some of the functionality provided by the Python JSON module for use in [Skulpt](http://www.skulpt.org/). + +### dump vs dumps + +So far, only dumps and its loads counterpart are supported. dump and load deal with streams whereas dumps and loads operate on strings. + + +### dumps keyword arguments + +While support of all arguments is far from complete this implementation passes many of the tests found in the Python distribution. Supported keyword arguments are: + +* indent +* ensure_ascii +* separators +* sort_keys + +### loads + +No arguments for loads are currently supported. This implementation relies on the browser implementation of JSON.parse and Skulpt's ffi.remapToPy. + +### Python docs + +Complete spec of the Python 2.x JSON implementation can be found at https://docs.python.org/2/library/json.html. + +## TODO + +While there is much to do to fully support the Python implementation, transformation for basic objects works quite well. + +### dumps + +Remaining keyword arguments to support include: + +* shipkeys +* check_circular +* allow_nan +* cls +* encoding +* default + +## Getting Started + +Create a basic html page: + +```html + +``` + +Add the json.sk specific Skulpt configuration options +```js +// tell Skulpt where to find json.sk and its dependencies +Sk.externalLibraries = { + json : { + path : '/path/to/json.sk/__init__.js', + dependencies : [ + '/path/to/json.sk/stringify.js' + ] + } +}; +``` + +Point your browser to your html page and have fun! + +## Dependencies + +Currently relies on a [browserify](https://github.com/substack/node-browserify)'d version of [json-stable-stringify](https://github.com/substack/json-stable-stringify). diff --git a/public/shims/json.sk/__init__.js b/public/shims/json.sk/__init__.js new file mode 100644 index 000000000..a459243ac --- /dev/null +++ b/public/shims/json.sk/__init__.js @@ -0,0 +1,98 @@ +var $builtinmodule = function(name) { + "use strict"; + var mod = {}; + + // skipkeys=False, + // ensure_ascii=True, + // check_circular=True, + // allow_nan=True, + // cls=None, + // indent=None, + // separators=None, + // encoding="utf-8", + // default=None, + // sort_keys=False, + // **kw + + var dumps_f = function(kwa) { + Sk.builtin.pyCheckArgs("dumps", arguments, 1, Infinity, true, false); + + var args = Array.prototype.slice.call(arguments, 1), + kwargs = new Sk.builtins.dict(kwa), + sort_keys = false, + stringify_opts, default_, jsobj, str; + + // default stringify options + stringify_opts = { + ascii : true, + separators : { + item_separator : ', ', + key_separator : ': ' + } + }; + + kwargs = Sk.ffi.remapToJs(kwargs); + jsobj = Sk.ffi.remapToJs(args[0]); + + // TODO: likely need to go through character by character to enable this + if (typeof(kwargs.ensure_ascii) === "boolean" && kwargs.ensure_ascii === false) { + stringify_opts.ascii = false; + } + + // TODO: javascript sort isn't entirely compatible with python's + if (typeof(kwargs.sort_keys) === "boolean" && kwargs.sort_keys) { + sort_keys = true; + } + + if (!sort_keys) { + // don't do any sorting unless sort_keys is true + // if sort_keys use stringify's default sort, which is alphabetical + stringify_opts.cmp = function(a, b) { + return 0; + }; + } + + // item_separator, key_separator) tuple. The default is (', ', ': '). + if (typeof(kwargs.separators) === "object" && kwargs.separators.length == 2) { + stringify_opts.separators.item_separator = kwargs.separators[0]; + stringify_opts.separators.key_separator = kwargs.separators[1]; + } + + // TODO: if indent is 0 it should add newlines + if (kwargs.indent) { + stringify_opts.space = kwargs.indent; + } + + // Sk.ffi.remapToJs doesn't map functions + if (kwargs.default) { + } + + // may need to create a clone of this to have more control/options + str = stringify(jsobj, stringify_opts); + + return new Sk.builtin.str(str); + }; + + dumps_f.co_kwargs = true; + mod.dumps = new Sk.builtin.func(dumps_f); + + // encoding[, cls[, object_hook[, parse_float[, parse_int[, parse_constant[, object_pairs_hook[, **kw]]]]]]] + var loads_f = function(kwa) { + Sk.builtin.pyCheckArgs("loads", arguments, 1, Infinity, true, false); + + var args = Array.prototype.slice.call(arguments, 1), + kwargs = new Sk.builtins.dict(kwa), + str, obj; + + kwargs = Sk.ffi.remapToJs(kwargs); + str = args[0].v; + obj = JSON.parse(str); + + return Sk.ffi.remapToPy(obj); + }; + + loads_f.co_kwargs = true; + mod.loads = new Sk.builtin.func(loads_f); + + return mod; +}; diff --git a/public/shims/json.sk/bower.json b/public/shims/json.sk/bower.json new file mode 100644 index 000000000..4003e53b8 --- /dev/null +++ b/public/shims/json.sk/bower.json @@ -0,0 +1,27 @@ +{ + "name": "json.sk", + "version": "0.0.0", + "homepage": "https://github.com/trinketapp/json.sk", + "authors": [ + "Brian Marks " + ], + "description": "JSON module for Skulpt.", + "main": "__init__.js", + "dependencies": { + "skulpt": "master" + }, + "keywords": [ + "skulpt", + "python", + "json", + "javascript" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/public/shims/json.sk/stringify.js b/public/shims/json.sk/stringify.js new file mode 100644 index 000000000..6eb643e61 --- /dev/null +++ b/public/shims/json.sk/stringify.js @@ -0,0 +1,642 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 127) { + hexadecimal = charCode.toString(16).toUpperCase(); + escaped = '\\u' + ('0000' + hexadecimal).slice(-4); + + result += escaped; + } + else { + result += character; + } + } + return result; +} + +},{"jsonify":3}],3:[function(require,module,exports){ +exports.parse = require('./lib/parse'); +exports.stringify = require('./lib/stringify'); + +},{"./lib/parse":4,"./lib/stringify":5}],4:[function(require,module,exports){ +var at, // The index of the current character + ch, // The current character + escapee = { + '"': '"', + '\\': '\\', + '/': '/', + b: '\b', + f: '\f', + n: '\n', + r: '\r', + t: '\t' + }, + text, + + error = function (m) { + // Call error when something is wrong. + throw { + name: 'SyntaxError', + message: m, + at: at, + text: text + }; + }, + + next = function (c) { + // If a c parameter is provided, verify that it matches the current character. + if (c && c !== ch) { + error("Expected '" + c + "' instead of '" + ch + "'"); + } + + // Get the next character. When there are no more characters, + // return the empty string. + + ch = text.charAt(at); + at += 1; + return ch; + }, + + number = function () { + // Parse a number value. + var number, + string = ''; + + if (ch === '-') { + string = '-'; + next('-'); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + if (ch === '.') { + string += '.'; + while (next() && ch >= '0' && ch <= '9') { + string += ch; + } + } + if (ch === 'e' || ch === 'E') { + string += ch; + next(); + if (ch === '-' || ch === '+') { + string += ch; + next(); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + } + number = +string; + if (!isFinite(number)) { + error("Bad number"); + } else { + return number; + } + }, + + string = function () { + // Parse a string value. + var hex, + i, + string = '', + uffff; + + // When parsing for string values, we must look for " and \ characters. + if (ch === '"') { + while (next()) { + if (ch === '"') { + next(); + return string; + } else if (ch === '\\') { + next(); + if (ch === 'u') { + uffff = 0; + for (i = 0; i < 4; i += 1) { + hex = parseInt(next(), 16); + if (!isFinite(hex)) { + break; + } + uffff = uffff * 16 + hex; + } + string += String.fromCharCode(uffff); + } else if (typeof escapee[ch] === 'string') { + string += escapee[ch]; + } else { + break; + } + } else { + string += ch; + } + } + } + error("Bad string"); + }, + + white = function () { + +// Skip whitespace. + + while (ch && ch <= ' ') { + next(); + } + }, + + word = function () { + +// true, false, or null. + + switch (ch) { + case 't': + next('t'); + next('r'); + next('u'); + next('e'); + return true; + case 'f': + next('f'); + next('a'); + next('l'); + next('s'); + next('e'); + return false; + case 'n': + next('n'); + next('u'); + next('l'); + next('l'); + return null; + } + error("Unexpected '" + ch + "'"); + }, + + value, // Place holder for the value function. + + array = function () { + +// Parse an array value. + + var array = []; + + if (ch === '[') { + next('['); + white(); + if (ch === ']') { + next(']'); + return array; // empty array + } + while (ch) { + array.push(value()); + white(); + if (ch === ']') { + next(']'); + return array; + } + next(','); + white(); + } + } + error("Bad array"); + }, + + object = function () { + +// Parse an object value. + + var key, + object = {}; + + if (ch === '{') { + next('{'); + white(); + if (ch === '}') { + next('}'); + return object; // empty object + } + while (ch) { + key = string(); + white(); + next(':'); + if (Object.hasOwnProperty.call(object, key)) { + error('Duplicate key "' + key + '"'); + } + object[key] = value(); + white(); + if (ch === '}') { + next('}'); + return object; + } + next(','); + white(); + } + } + error("Bad object"); + }; + +value = function () { + +// Parse a JSON value. It could be an object, an array, a string, a number, +// or a word. + + white(); + switch (ch) { + case '{': + return object(); + case '[': + return array(); + case '"': + return string(); + case '-': + return number(); + default: + return ch >= '0' && ch <= '9' ? number() : word(); + } +}; + +// Return the json_parse function. It will have access to all of the above +// functions and variables. + +module.exports = function (source, reviver) { + var result; + + text = source; + at = 0; + ch = ' '; + result = value(); + white(); + if (ch) { + error("Syntax error"); + } + + // If there is a reviver function, we recursively walk the new structure, + // passing each name/value pair to the reviver function for possible + // transformation, starting with a temporary root object that holds the result + // in an empty key. If there is not a reviver function, we simply return the + // result. + + return typeof reviver === 'function' ? (function walk(holder, key) { + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + }({'': result}, '')) : result; +}; + +},{}],5:[function(require,module,exports){ +var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + +function quote(string) { + // If the string contains no control characters, no quote characters, and no + // backslash characters, then we can safely slap some quotes around it. + // Otherwise we must also replace the offending characters with safe escape + // sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; +} + +function str(key, holder) { + // Produce a string from holder[key]. + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + + // If the value has a toJSON method, call it to obtain a replacement value. + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + + // If we were called with a replacer function, then call the replacer to + // obtain a replacement value. + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + + // What happens next depends on the value's type. + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + // JSON numbers must be finite. Encode non-finite numbers as null. + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + // If the value is a boolean or null, convert it to a string. Note: + // typeof null does not produce 'null'. The case is included here in + // the remote chance that this gets fixed someday. + return String(value); + + case 'object': + if (!value) return 'null'; + gap += indent; + partial = []; + + // Array.isArray + if (Object.prototype.toString.apply(value) === '[object Array]') { + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + + // Join all of the elements together, separated with commas, and + // wrap them in brackets. + v = partial.length === 0 ? '[]' : gap ? + '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + + // If the replacer is an array, use it to select the members to be + // stringified. + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + k = rep[i]; + if (typeof k === 'string') { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + else { + // Otherwise, iterate through all of the keys in the object. + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + + // Join all of the member texts together, separated with commas, + // and wrap them in braces. + + v = partial.length === 0 ? '{}' : gap ? + '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : + '{' + partial.join(',') + '}'; + gap = mind; + return v; + } +} + +module.exports = function (value, replacer, space) { + var i; + gap = ''; + indent = ''; + + // If the space parameter is a number, make an indent string containing that + // many spaces. + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + } + // If the space parameter is a string, it will be used as the indent string. + else if (typeof space === 'string') { + indent = space; + } + + // If there is a replacer, it must be a function or an array. + // Otherwise, throw an error. + rep = replacer; + if (replacer && typeof replacer !== 'function' + && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + + // Make a fake root object containing our value under the key of ''. + // Return the result of stringifying the value. + return str('', {'': value}); +}; + +},{}],6:[function(require,module,exports){ +// shim for using process in browser + +var process = module.exports = {}; + +process.nextTick = (function () { + var canSetImmediate = typeof window !== 'undefined' + && window.setImmediate; + var canPost = typeof window !== 'undefined' + && window.postMessage && window.addEventListener + ; + + if (canSetImmediate) { + return function (f) { return window.setImmediate(f) }; + } + + if (canPost) { + var queue = []; + window.addEventListener('message', function (ev) { + var source = ev.source; + if ((source === window || source === null) && ev.data === 'process-tick') { + ev.stopPropagation(); + if (queue.length > 0) { + var fn = queue.shift(); + fn(); + } + } + }, true); + + return function nextTick(fn) { + queue.push(fn); + window.postMessage('process-tick', '*'); + }; + } + + return function nextTick(fn) { + setTimeout(fn, 0); + }; +})(); + +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +} + +// TODO(shtylman) +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; + +},{}]},{},[1]); diff --git a/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx b/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx index f271560e7..10062dd5c 100644 --- a/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx @@ -50,6 +50,12 @@ const externalLibraries = { "./sense_hat.py": { path: `${process.env.ASSETS_URL}/shims/sense_hat/sense_hat_blob.py`, }, + "./json/__init__.js": { + path: `${process.env.ASSETS_URL}/shims/json.sk/__init__.js`, + dependencies : [ + `${process.env.ASSETS_URL}/shims/json.sk/stringify.js` + ] + }, }; const PythonRunner = () => { From 443f776ab2d854d341f8c3b884e5a7634b8ce830 Mon Sep 17 00:00:00 2001 From: Scott Date: Thu, 25 Apr 2024 09:21:33 +0100 Subject: [PATCH 2/2] fix(CI-issues): add to changelog and fix python runner linting issues --- CHANGELOG.md | 1 + src/components/Editor/Runners/PythonRunner/PythonRunner.jsx | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca005b59..5512ddb63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added +- Support for json package in skulpt python runner (#989) - Support to enable embedding iframes in HTML projects from in-house domains (#985) - Unit tests for `pyodide` runner (#976) diff --git a/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx b/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx index 10062dd5c..f375b28c2 100644 --- a/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx @@ -52,9 +52,7 @@ const externalLibraries = { }, "./json/__init__.js": { path: `${process.env.ASSETS_URL}/shims/json.sk/__init__.js`, - dependencies : [ - `${process.env.ASSETS_URL}/shims/json.sk/stringify.js` - ] + dependencies: [`${process.env.ASSETS_URL}/shims/json.sk/stringify.js`], }, };