├── Tests ├── assets │ ├── array.json │ ├── object.json │ ├── numbers.json │ ├── values.json │ └── strings.json └── TestJSONlib.sc ├── classes ├── extJSON.sc └── JSONlib.sc ├── JSONlib.quark ├── .gitignore ├── README.md ├── HelpSource └── Classes │ └── JSONlib.schelp └── LICENSE /Tests/assets/array.json: -------------------------------------------------------------------------------- 1 | [ 2 | 20, 3 | 30, 4 | 40 5 | ] 6 | -------------------------------------------------------------------------------- /Tests/assets/object.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": { 3 | "bar": "baz" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Tests/assets/numbers.json: -------------------------------------------------------------------------------- 1 | { 2 | "integer": 10, 3 | "negativeInteger": -10, 4 | "float": 10.0, 5 | "negativeFloat": -10.0, 6 | "bigE": 2E10, 7 | "smallE": 2e10, 8 | "negativeExponent": 2e-10 9 | } 10 | -------------------------------------------------------------------------------- /Tests/assets/values.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": "string", 3 | "number": 10, 4 | "object": { 5 | "foo": "bar" 6 | }, 7 | "array": [1, 2, 3], 8 | "true": true, 9 | "false": false, 10 | "null": null 11 | } 12 | -------------------------------------------------------------------------------- /classes/extJSON.sc: -------------------------------------------------------------------------------- 1 | + Collection { 2 | asJSON {|customEncoder=nil, postWarnings=true| 3 | ^JSONlib.convertToJSON(this, customEncoder, postWarnings); 4 | } 5 | } 6 | 7 | + Dictionary { 8 | asJSON {|customEncoder=nil, postWarnings=true| 9 | ^JSONlib.convertToJSON(this, customEncoder, postWarnings); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /JSONlib.quark: -------------------------------------------------------------------------------- 1 | ( 2 | \name: "JSONlib", 3 | \summary: "A JSON en- and decoder for SuperCollider", 4 | \isCompatible: { Main.versionAtMost(3, 10).not }, 5 | \author: "Julian Rohrhuber, Dennis Scheiba", 6 | \organisation: "IMM Duesseldorf", 7 | \helpdoc: "help", 8 | \since: "2023", 9 | \version: 0.1 10 | ) 11 | -------------------------------------------------------------------------------- /Tests/assets/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "text": "lorem ipsum", 3 | "quotationMark": "lorem\"ipsum", 4 | "reverseSolidus": "lorem\\ipsum", 5 | "solidus": "lorem\/ipsum", 6 | "backspace": "lorem\bipsum", 7 | "formfeed": "lorem\fipsum", 8 | "linefeed": "lorem\nipsum", 9 | "carriageReturn": "lorem\ripsum", 10 | "horizontalTab": "lorem\tipsum", 11 | "hexDigits": "lorem\u597Dipsum" 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Icon must end with two \r 7 | Icon 8 | 9 | # Thumbnails 10 | ._* 11 | 12 | # Files that might appear in the root of a volume 13 | .DocumentRevisions-V100 14 | .fseventsd 15 | .Spotlight-V100 16 | .TemporaryItems 17 | .Trashes 18 | .VolumeIcon.icns 19 | .com.apple.timemachine.donotpresent 20 | 21 | # Directories potentially created on remote AFP share 22 | .AppleDB 23 | .AppleDesktop 24 | Network Trash Folder 25 | Temporary Items 26 | .apdisk 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSONlib 2 | 3 | > A JSON de- and encoder for SuperCollider 4 | 5 | The JSON implementation of the SuperCollider standard library lacks certain features such as 6 | 7 | * [ ] a JSON encoder (the conversion of an dictionary to a JSON string) 8 | * [ ] parsing JSON values into their respective type (`.parseJSON` converts everything to a string) 9 | 10 | which this Quark implements, building on top of the existing implementation. 11 | 12 | There also exists a [json](https://github.com/supercollider-quarks/json) Quark which also adds a wrapper for [sccode.org](https://sccode.org) but lacks a recursive encoding of objects. 13 | The goal of `JSONlib` is to simply provide a full implementation of the JSON standard in sclang and nothing beyond it. 14 | 15 | ## Quickstart 16 | 17 | ### Installation 18 | 19 | ```supercollider 20 | // install the quark 21 | Quarks.install("https://github.com/musikinformatik/JSONlib.git"); 22 | 23 | // restart the interpreter so the new classes are available 24 | thisProcess.recompile; 25 | 26 | // open documention 27 | HelpBrowser.openHelpFor("Classes/JSONlib"); 28 | ``` 29 | 30 | ### Basic usage 31 | 32 | #### Parse a JSON 33 | 34 | Let's say we have a JSON with an integer as a value 35 | 36 | ```json 37 | { 38 | "hello": 42 39 | } 40 | ``` 41 | 42 | which we want to parse in sclang. 43 | 44 | ```supercollider 45 | // use Symbol instead of String to get rid of escaping quotation marks 46 | j = '{"hello": 42}'; 47 | 48 | // turn this into an Event 49 | d = JSONlib.convertToSC(j); 50 | // -> ( 'hello': 42 ) 51 | 52 | // an integer gets parsed as an integer 53 | d[\hello].class 54 | // -> Integer 55 | 56 | // compare to the built-in method of sclang 57 | // it uses a Dictionary instead of an Event 58 | d = j.parseJSON() 59 | // -> Dictionary[ (hello -> 42) ] 60 | 61 | // but 42 is a string here 62 | d["hello"].class 63 | // -> String 64 | ``` 65 | 66 | #### Encode an Event as JSON 67 | 68 | ```supercollider 69 | // create an event 70 | e = (\foo: "bar", \baz: (\nested: true)); 71 | 72 | e.asJSON 73 | // -> { "baz": { "nested": true }, "foo": "bar" } 74 | 75 | // or use the class JSONlib 76 | JSONlib.convertToJSON(e); 77 | // -> { "baz": { "nested": true }, "foo": "bar" } 78 | ``` 79 | 80 | Advanced usage is described in the SCdoc documentation. 81 | 82 | ## Development 83 | 84 | Make sure to run the tests via 85 | 86 | ```supercollider 87 | TestJSONlib.run; 88 | ``` 89 | 90 | ## License 91 | 92 | GPL-2.0 93 | -------------------------------------------------------------------------------- /classes/JSONlib.sc: -------------------------------------------------------------------------------- 1 | JSONlib { 2 | 3 | var <>postWarnings; 4 | var <>useEvent; 5 | var <>customEncoder; 6 | var <>customDecoder; 7 | 8 | *new { |postWarnings = true, useEvent=true, customEncoder=nil, customDecoder=nil| 9 | ^super.newCopyArgs(postWarnings, useEvent, customEncoder, customDecoder) 10 | } 11 | 12 | *convertToJSON {|object, customEncoder=nil, postWarnings=true| 13 | if((object.isKindOf(Dictionary) or: (object.isKindOf(SequenceableCollection))).not) { 14 | Error("Can only convert a Dictonary/Event/Array to JSON but received %".format(object.class)).throw 15 | }; 16 | ^this.new(postWarnings, customEncoder: customEncoder).prConvertToJson(object) 17 | } 18 | 19 | *convertToSC {|json, customDecoder=nil, useEvent=true, postWarnings=true| 20 | if(json.isKindOf(Symbol)) { 21 | json = json.asString; 22 | }; 23 | 24 | if(json.isKindOf(String).not) { 25 | Error("Can only parse a String to JSON but received %".format(json.class)).throw 26 | }; 27 | ^this.new( 28 | postWarnings, 29 | customDecoder: customDecoder, 30 | useEvent: useEvent 31 | ).prConvertToSC(json.parseJSON) 32 | } 33 | 34 | *parseFile {|filePath, customDecoder=nil, useEvent=true, postWarnings=true| 35 | ^this.new( 36 | postWarnings, 37 | customDecoder: customDecoder, 38 | useEvent: useEvent, 39 | ).prConvertToSC(filePath.parseJSONFile) 40 | } 41 | 42 | prConvertToJson {|v| 43 | var array, val; 44 | 45 | if(customEncoder.notNil) { 46 | val = customEncoder.value(v); 47 | if(val.notNil) { 48 | ^val 49 | } 50 | }; 51 | 52 | ^case 53 | { v.isKindOf(Symbol) } { this.prConvertToJson(v.asString) } 54 | // only check value if it is a ref 55 | { v.isNil or: { v.isKindOf(Ref) and: { v.value.isNil } } } { "null" } 56 | // sc closely implements the JSON string, see https://www.json.org/json-en.html 57 | // but the post window parses \n as linebreak etc. which makes copying of the JSON from 58 | // the post window error prone 59 | { v.isString } { 60 | v 61 | .replace("\\", "\\\\") // reverse solidus 62 | .replace("/", "\\/") // solidus 63 | .replace($", "\\\"") // quoatition mark 64 | .replace(0x08.asAscii, "\\b") // backspace 65 | .replace(0x0c.asAscii, "\\f") // formfeed 66 | .replace("\n", "\\n") // linefeed 67 | .replace("\r", "\\r") // carriage return 68 | .replace("\t", "\\t") // horizontal tab 69 | // @todo non ascii chars 70 | .quote 71 | } 72 | { v.isNumber } { 73 | case 74 | { v == inf } { "inf".quote } 75 | { v == inf.neg } { "-inf".quote } 76 | { v.asCompileString } 77 | } 78 | { v.isKindOf(Boolean) } { v.asBoolean } 79 | { v.isKindOf(SequenceableCollection) } { 80 | array = v.collect { |x| this.prConvertToJson(x) }; 81 | if(postWarnings and: { v.class !== Array }) { 82 | "JSON file format will not recover % class, but instead an Array".format(v.class.name).warn 83 | }; 84 | "[ % ]".format(array.join(", ")) 85 | } 86 | { v.isKindOf(Dictionary) } { 87 | array = v.asAssociations.sort.collect { |x| 88 | var key = x.key; 89 | if((key.isKindOf(String)).not) { 90 | if(key.isKindOf(Symbol).not) { 91 | "Key % of type % got transformed to a string".format(key, key.class).warn; 92 | }; 93 | key = key.asString; 94 | }; 95 | "%: %".format(key.quote, this.prConvertToJson(x.value)) 96 | }; 97 | "{ % }".format(array.join(", ")) 98 | } 99 | { 100 | if(postWarnings) { "JSON file format will not recover % class, but instead a compile string".format(v.class.name).warn }; 101 | this.prConvertToJson(v.asCompileString) 102 | } 103 | } 104 | 105 | prConvertToSC { |v| 106 | var res, val; 107 | if(customDecoder.notNil) { 108 | val = customDecoder.value(v); 109 | if(val.notNil) { 110 | ^val 111 | } 112 | }; 113 | 114 | ^case 115 | { v.isString and: { v.every { |x| x.isDecDigit } } } { v.asInteger } 116 | // see https://www.json.org/json-en.html Number section and 117 | // https://stackoverflow.com/a/6425559/3475778 118 | { v.isString and: { "^[-]?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$".matchRegexp(v.asString) } } { v.asFloat } 119 | { v == "true" } { true } 120 | { v == "false" } { false } 121 | // an event can not store nil as a value so wrap it in a Ref 122 | { v == nil } { Ref(nil) } 123 | { v.isArray } { v.collect { |x| this.prConvertToSC(x) } } 124 | { v.isKindOf(Dictionary) } { 125 | if(useEvent) { 126 | res = Event.new; 127 | v.pairsDo { |key, x| 128 | res.put(key.asSymbol, this.prConvertToSC(x)); 129 | }; 130 | res; 131 | } { 132 | v.collect { |x| this.prConvertToSC(x) } 133 | } 134 | } 135 | { v } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Tests/TestJSONlib.sc: -------------------------------------------------------------------------------- 1 | TestJSONlib : UnitTest { 2 | 3 | 4 | // encoding tests 5 | test_objectEncode { 6 | var o = (\foo: (\bar: "baz")); 7 | var j = JSONlib.convertToJSON(o); 8 | this.assertEquals(j, "{ \"foo\": { \"bar\": \"baz\" } }"); 9 | } 10 | 11 | test_objectStringKeysEncode { 12 | var o = ("foo": "bar"); 13 | var j = JSONlib.convertToJSON(o); 14 | this.assertEquals(j, "{ \"foo\": \"bar\" }", "use strings as keys"); 15 | } 16 | 17 | test_arrayEncode { 18 | var o = [20, 30, 40]; 19 | var j = JSONlib.convertToJSON(o); 20 | this.assertEquals(j, "[ 20, 30, 40 ]"); 21 | } 22 | 23 | test_valuesEncode { 24 | var o = ( 25 | \string: "string", 26 | \number: 10, 27 | \object: (\foo: "bar"), 28 | \array: [1,2,3], 29 | \true: true, 30 | \false: false, 31 | \null: `nil, 32 | ); 33 | var j = JSONlib.convertToJSON(o); 34 | this.assertEquals(j, "{ \"array\": [ 1, 2, 3 ], \"false\": false, \"null\": null, \"number\": 10, \"object\": { \"foo\": \"bar\" }, \"string\": \"string\", \"true\": true }"); 35 | } 36 | 37 | test_stringsEncode { 38 | var o = ( 39 | \text: "lorem ipsum", 40 | \quotationMark: "lorem\"ipsum", 41 | \reverseSolidus: "lorem\\ipsum", 42 | \solidus: "lorem/ipsum", 43 | \backspace: "lorem%ipsum".format(0x08.asAscii), 44 | \formfeed: "lorem%ipsum".format(0x0c.asAscii), 45 | \linefeed: "lorem\nipsum", 46 | \carriageReturn: "lorem\ripsum", 47 | \horizontalTab: "lorem\tipsum", 48 | // @todo non ascii chars 49 | ); 50 | 51 | this.assertEquals( 52 | JSONlib.convertToJSON((\text: "lorem ipsum")), 53 | "{ \"text\": \"lorem ipsum\" }", 54 | "normal text" 55 | ); 56 | 57 | this.assertEquals( 58 | JSONlib.convertToJSON((\text: "lorem\"ipsum")), 59 | "{ \"text\": \"lorem\\\"ipsum\" }", 60 | "quatition mark" 61 | ); 62 | 63 | this.assertEquals( 64 | JSONlib.convertToJSON((\text: "lorem\\ipsum")), 65 | "{ \"text\": \"lorem\\\\ipsum\" }", 66 | "reverse solidus" 67 | ); 68 | 69 | this.assertEquals( 70 | JSONlib.convertToJSON((\text: "lorem/ipsum")), 71 | "{ \"text\": \"lorem\\/ipsum\" }", 72 | "solidus" 73 | ); 74 | 75 | this.assertEquals( 76 | JSONlib.convertToJSON((\text: "lorem%ipsum".format(0x08.asAscii))), 77 | "{ \"text\": \"lorem\\bipsum\" }", 78 | "backspace" 79 | ); 80 | 81 | this.assertEquals( 82 | JSONlib.convertToJSON((\text: "lorem%ipsum".format(0x0c.asAscii))), 83 | "{ \"text\": \"lorem\\fipsum\" }", 84 | "formfeed" 85 | ); 86 | 87 | this.assertEquals( 88 | JSONlib.convertToJSON((\text: "lorem\nipsum")), 89 | "{ \"text\": \"lorem\\nipsum\" }", 90 | "linefeed" 91 | ); 92 | 93 | this.assertEquals( 94 | JSONlib.convertToJSON((\text: "lorem\ripsum")), 95 | "{ \"text\": \"lorem\\ripsum\" }", 96 | "carriage return" 97 | ); 98 | 99 | this.assertEquals( 100 | JSONlib.convertToJSON((\text: "lorem\tipsum")), 101 | "{ \"text\": \"lorem\\tipsum\" }", 102 | "horizontal tab" 103 | ); 104 | } 105 | 106 | test_numberEncode { 107 | [ 108 | // name, value, representation 109 | ["integer", 10, 10], 110 | ["negative integer", -10, -10], 111 | ["zero", 0, 0], 112 | ["negative zero", -0, 0], 113 | ["float", 3.14, 3.14], 114 | ["negative float", -3.14, -3.14], 115 | ["inf", inf, "\"inf\""], 116 | ["negative inf", -inf, "\"-inf\""], 117 | ["pi", pi, 3.1415926535898], 118 | ["exp writing", 1e5, 100000.0], 119 | ["neg-exp writing", 1e-5, 0.00001], 120 | ["hex", 0x10, 16], 121 | ].do({|v| 122 | var o = (v[0]: v[1]).postln; 123 | var j = JSONlib.convertToJSON(o); 124 | this.assertEquals( 125 | j, 126 | "{ \"%\": % }".format(v[0], v[2]), 127 | "number %".format(v[0]) 128 | ); 129 | }); 130 | } 131 | 132 | test_nullEncode { 133 | var o = ( 134 | null: `nil 135 | ); 136 | var j = JSONlib.convertToJSON(o); 137 | this.assertEquals( 138 | j, 139 | "{ \"null\": null }", 140 | "Use Ref(nil) to represent null in event"; 141 | ); 142 | 143 | // .parseJson allows us to store nil in a dict 144 | // which is not possible otherwise 145 | o = "{\"null\": null}".parseJSON; 146 | j = JSONlib.convertToJSON(o); 147 | this.assertEquals( 148 | j, 149 | "{ \"null\": null }", 150 | "nil should be represented as null" 151 | ); 152 | } 153 | 154 | // invalid encoding tests 155 | test_numberAsKey { 156 | var o = ( 157 | 1: 2 158 | ); 159 | // should print a warning (how to test this?) 160 | var j = JSONlib.convertToJSON(o); 161 | this.assertEquals( 162 | j, 163 | "{ \"1\": 2 }", 164 | "numbers as keys will be implicitly converted to strings" 165 | ); 166 | } 167 | 168 | // encoding of non-json values 169 | test_functionAsValue { 170 | var o = ( 171 | \func: {|x| "hello".postln} 172 | ); 173 | // should print a warning (how to test this?) 174 | var j = JSONlib.convertToJSON(o); 175 | this.assertEquals( 176 | j, 177 | // needs escaping within function 178 | "{ \"func\": \"{|x| \\\"hello\\\".postln}\" }", 179 | "Use compile string as fallback for non-JSON objects which needs escaping", 180 | ); 181 | } 182 | 183 | test_functionsAsValueRecursive { 184 | // same as above but now the json of a json as a value 185 | // primarly used to validate escaping of strings 186 | var o = ( 187 | \func: {"hello".postln}, 188 | ); 189 | var j = JSONlib.convertToJSON((\json: JSONlib.convertToJSON(o))); 190 | this.assertEquals( 191 | j, 192 | // sorry... 193 | "{ \"json\": \"{ \\\"func\\\": \\\"{\\\\\\\"hello\\\\\\\".postln}\\\" }\" }", 194 | "when used recursively we must escape all strings properly" 195 | ); 196 | } 197 | 198 | // additional ref/nil test 199 | test_anotherFunctionAsValue { 200 | // primary purpose to verify null check 201 | var o = ( 202 | \func: { |x| x !? (x+1) }, 203 | ); 204 | var j = JSONlib.convertToJSON(o); 205 | this.assertEquals( 206 | j, 207 | "{ \"func\": \"{ |x| x !? (x+1) }\" }", 208 | ".value should not get called on a non-ref", 209 | ); 210 | } 211 | 212 | test_ref { 213 | var o = ( 214 | \ref: `42, 215 | ); 216 | var j = JSONlib.convertToJSON(o); 217 | this.assertEquals( 218 | j, 219 | "{ \"ref\": \"`(42)\" }", 220 | "Non-nil refs should be encoded as compile strings", 221 | ); 222 | } 223 | 224 | test_stringNull { 225 | var o = ( 226 | \null: "null", 227 | ); 228 | var j = JSONlib.convertToJSON(o); 229 | this.assertEquals( 230 | j, 231 | "{ \"null\": \"null\" }", 232 | "A \"null\" string should be represented as a string", 233 | ) 234 | } 235 | 236 | // decoding tests - taken from json.org 237 | // we only test for valid json 238 | test_objectDecode { 239 | var j = this.prParseJsonFile("object.json"); 240 | this.assertEquals(j[\foo][\bar], "baz", "Parse nested objects"); 241 | } 242 | 243 | test_arrayDecode { 244 | var j = this.prParseJsonFile("array.json"); 245 | this.assertEquals(j, [20 , 30, 40], "JSON can contain also array as root"); 246 | } 247 | 248 | test_valuesDecode { 249 | var j = this.prParseJsonFile("values.json"); 250 | this.assertEquals(j[\string], "string", "Value can be strings"); 251 | this.assertEquals(j[\number], 10, "Value can be integer numbers"); 252 | this.assertEquals(j[\object][\foo], "bar", "Values can be objects"); 253 | this.assertEquals(j[\array], [1, 2, 3], "Value can be an array"); 254 | this.assertEquals(j[\true], true, "Value can be true"); 255 | this.assertEquals(j[\false], false, "Value can be false"); 256 | this.assertEquals(j[\null].class, Ref, "Null needs to be wrapped in a Ref"); 257 | this.assertEquals(j[\null].value, nil, "Unwrapping Ref(nil) should be nil"); 258 | } 259 | 260 | test_stringsDecode { 261 | var j = this.prParseJsonFile("strings.json"); 262 | this.assertEquals(j[\text], "lorem ipsum", "Standard text should be reproduced"); 263 | this.assertEquals(j[\quotationMark], "lorem\"ipsum", "Quotation needs to be escaped"); 264 | this.assertEquals(j[\reverseSolidus], "lorem\\ipsum", "Reverse Solidus needs to be escaped"); 265 | this.assertEquals(j[\solidus], "lorem/ipsum", "Solidus does not need a SC escape"); 266 | this.assertEquals(j[\backspace], "lorem%ipsum".format(0x08.asAscii), "Backspace needs escape"); 267 | this.assertEquals(j[\formfeed], "lorem%ipsum".format(0x0c.asAscii), "Formfeed needs escpae"); 268 | this.assertEquals(j[\linefeed], "lorem\nipsum", "Linebreak needs an escape"); 269 | this.assertEquals(j[\carriageReturn], "lorem\ripsum", "carriage return needs an escape"); 270 | this.assertEquals(j[\horizontalTab], "lorem\tipsum", "tab needs an escape"); 271 | 272 | // sc can not compare utf-8 chares so we make some extra steps 273 | this.assertEquals( 274 | j[\hexDigits].ascii[0..4].collect(_.asAscii).join, 275 | "lorem", 276 | "Hex encoding should not affect ascii chars lorem", 277 | ); 278 | 279 | this.assertEquals( 280 | j[\hexDigits].ascii[5..8].wrap(0, 255), 281 | // normaly hao is represented as e5a5bd in utf-8 282 | [0xe5, 0xa5, 0xbd, 0x69], 283 | "Hex encoding should be parsed for 好", 284 | ); 285 | 286 | this.assertEquals( 287 | j[\hexDigits].ascii[8..].collect(_.asAscii).join, 288 | "ipsum", 289 | "Hex encoding should not affect ascii chars ipsum", 290 | ); 291 | } 292 | 293 | test_numbersDecode { 294 | var j = this.prParseJsonFile("numbers.json"); 295 | this.assertEquals(j[\integer], 10, "Positive integer"); 296 | this.assertEquals(j[\negativeInteger], -1 * 10, "Negative integer"); 297 | this.assertEquals(j[\float], 10.0, "float"); 298 | this.assertEquals(j[\negativeFloat], -10.0, "negative float"); 299 | this.assertEquals(j[\bigE], 20000000000.0, "Big E writing"); 300 | this.assertEquals(j[\smallE], 20000000000.0, "small e writing"); 301 | this.assertEquals(j[\negativeExponent], 0.0000000002, "negative exponent"); 302 | } 303 | 304 | test_jsonNullDecode { 305 | var p = this.prGetPathFor("values.json"); 306 | var j = JSONlib.parseFile(p, useEvent: true); 307 | this.assert(j.keys.asArray.includes(\null), "Null ref value needs to have a key in event"); 308 | this.assertEquals(j[\null].value, nil, "As an Event can not store nil we wrap it in a Ref"); 309 | j = JSONlib.parseFile(p, useEvent: false); 310 | j.keys.asArray.postln; 311 | this.assert(j.keys.asArray.any({|x| x == "null"}), "Null ref value needs to have a string key in dictionary"); 312 | this.assertEquals(j["null"].value, nil, "As a Dictionary can not store nil we wrap it in a Ref"); 313 | } 314 | 315 | test_jsonEncodeDict { 316 | var t = "{ \"hello\": \"world\" }"; 317 | var j = JSONlib.convertToSC(t, useEvent: false); 318 | this.assertEquals(j.class, Dictionary, "Type should be Dictionary if not Event"); 319 | this.assertEquals(j["hello"], "world", "Dictonaries use Strings as keys"); 320 | this.assertEquals(j[\hello], nil, "Dictionaries use Strings as keys"); 321 | } 322 | 323 | test_jsonEncodeEvent { 324 | var t = "{ \"hello\": \"world\" }"; 325 | var j = JSONlib.convertToSC(t, useEvent: true); 326 | this.assertEquals(j.class, Event, "Type should be Event as default"); 327 | this.assertEquals(j["hello"], nil, "Events use symbols as keys"); 328 | this.assertEquals(j[\hello], "world", "Events use symbols as keys"); 329 | } 330 | 331 | // test custom decoder/encoders 332 | test_customEncoderFunc { 333 | var f = {|x| 334 | if(x.isFunction) { 335 | "--func%".format(x.cs).quote; 336 | } 337 | }; 338 | var o = ( 339 | \f: {|x| x}, 340 | ); 341 | var j = JSONlib.convertToJSON( 342 | object: o, 343 | customEncoder: f 344 | ); 345 | this.assertEquals( 346 | j, 347 | "{ \"f\": \"--func{|x| x}\" }", 348 | "Custom encoder values should be used for translating values to JSON", 349 | ); 350 | this.assertEquals( 351 | JSONlib.convertToJSON((\o: 42), f), 352 | "{ \"o\": 42 }", 353 | "Custom encoder should not affect other values", 354 | ); 355 | } 356 | 357 | test_customDecoderFunc { 358 | var f = {|x| 359 | if(x.isString) { 360 | if(x.beginsWith("--func")) { 361 | // somehow we need to run this twice 362 | x["--func".size()..].compile().(); 363 | }; 364 | }; 365 | }; 366 | var t = "{ \"f\": \"--func{|x| 42;}\" }"; 367 | var o = JSONlib.convertToSC( 368 | t, 369 | customDecoder: f 370 | ); 371 | o.postln; 372 | this.assertEquals( 373 | o[\f].(), 374 | 42, 375 | "Custom decoder should be used for translating JSON values to SC objects" 376 | ); 377 | this.assertEquals( 378 | JSONlib.convertToSC("{ \"f\": 20 }", f)[\f], 379 | 20, 380 | "Custom encoder should not affect other values", 381 | ); 382 | } 383 | 384 | // test external methods 385 | test_arrayMethod { 386 | var j = [0, nil, "20"].asJSON(); 387 | this.assertEquals( 388 | j, 389 | "[ 0, null, \"20\" ]", 390 | "Test array method" 391 | ) 392 | } 393 | 394 | test_dictMethod { 395 | var d = Dictionary().put("some", 20); 396 | var j = d.asJSON(); 397 | this.assertEquals( 398 | j, 399 | "{ \"some\": 20 }", 400 | "Test external method on a Dictionary" 401 | ); 402 | } 403 | 404 | test_eventMethod { 405 | var e = (\some: 30); 406 | var j = e.asJSON(); 407 | this.assertEquals( 408 | j, 409 | "{ \"some\": 30 }", 410 | "Test external method on an Event" 411 | ); 412 | } 413 | 414 | 415 | test_jsonSymbolAsString { 416 | var j = '{"hello": 42}'; 417 | var e = JSONlib.convertToSC(j); 418 | this.assertEquals( 419 | e[\hello], 420 | 42, 421 | "convertToSC needs to accept Symbols as well", 422 | ); 423 | } 424 | 425 | // private implementation 426 | // util 427 | 428 | prGetPathFor {|fileName| 429 | ^this.class.filenameSymbol.asString.dirname +/+ "assets" +/+ fileName 430 | } 431 | 432 | prParseJsonFile {|fileName| 433 | ^JSONlib.parseFile(this.prGetPathFor(fileName)) 434 | } 435 | 436 | } 437 | -------------------------------------------------------------------------------- /HelpSource/Classes/JSONlib.schelp: -------------------------------------------------------------------------------- 1 | TITLE:: JSONlib 2 | SUMMARY::A JSON en- and decoder 3 | CATEGORIES::Files,Utilities,External Control 4 | RELATED::Classes/String#YAML and JSON parsing,Classes/Event,Classes/Dictionary,Guides/OSC_communication 5 | 6 | DESCRIPTION:: 7 | 8 | Encode SuperCollider objects to the JSON format and decode them from the JSON format. 9 | 10 | SUBSECTION::Quickstart 11 | 12 | Encoding 13 | 14 | code:: 15 | e = (\foo: "mapping"); 16 | e.asJSON; 17 | // -> { "foo": "mapping" } 18 | 19 | // or use the lib directly 20 | JSONlib.convertToJSON(e); 21 | :: 22 | 23 | Decoding 24 | 25 | code:: 26 | // escaping of quotes is necessary 27 | j = "{\"hello\": 42}"; 28 | JSONlib.convertToSC(j); 29 | // -> ( 'hello': 42 ) 30 | 31 | // to avoid the need for escaping strings, you can also use a symbol. 32 | // (note that you still need to escape ' if they are in your json) 33 | j = '{"hello": 42}'; 34 | JSONlib.convertToSC(j); 35 | // -> ( 'hello': 42 ) 36 | :: 37 | 38 | Also see link::#*new:: and the link::#examples::. 39 | 40 | SUBSECTION::What is JSON 41 | 42 | EMPHASIS::JSON (JavaScript Object Notation) is a lightweight data-interchange format. It is easy for humans to read and write. It is easy for machines to parse and generate.:: footnote::https://www.json.org/:: 43 | 44 | JSON is a common exchange format in the internet as Browsers provide a JavaScript runtime and JSON is the native exchange format for JavaScript objects. 45 | This led to wide adaption as it is a fixed standard with a variety of use cases. 46 | The network communication abilties of SuperCollider are restricted to link::Guides/OSC_communication:: which can only transfer link::Classes/Array::s but no nested dictionaries. But by using a JSON string we can transfer nested key-value pairs (see link::#Transfer nested objects via OSC::). 47 | 48 | For more information consult https://www.json.org/ 49 | 50 | SUBSECTION::Yet another JSON parser? 51 | 52 | But isn't there alreday a built in method called link::Classes/String#-parseJSON::? 53 | Yes, but link::Classes/String#-parseJSON:: implementation 54 | 55 | LIST:: 56 | ## lacks an encoder, so the conversion of e.g. a link::Classes/Dictionary:: to a JSON string representation 57 | ## does not respect the types of a JSON but instead uses the type of String for every value 58 | :: 59 | 60 | An encoder emphasis::simply:: needs to translate the SuperCollider data types to the set of JSON data types. 61 | As there are far more SuperCollider data types than JSON data types we rely on some implicit and explicit conversions. 62 | Consult the source code on what kind of implicit conversions are made. 63 | These defaults can be overwritten and extended by providing a code::customEncoder:: or code::customDecoder:: (see link::Classes/JSONlib#*new::). 64 | 65 | The type conversion issue is a bit more difficult as it is not possible for us to recover the original types from the JSON explicitly. 66 | Was code::"42":: always a string or was it an link::Classes/Integer:: once that got converted by link::Classes/String#-parseJSON:: from an Integer to a link::Classes/String::? 67 | - so we decided to use implicit assignment of an appropriate class (also known as type casting in other programming languages) if the string matches a JSON representation other than String. 68 | The only exception here is code::null::, see below. 69 | 70 | If you do not want to make use of the implicit casting simply use the built in link::Classes/String#-parseJSON::. 71 | 72 | Consider the JSON code::{"number": 42}:: 73 | 74 | code:: 75 | j = "{\"number\": 42\}"; 76 | j.parseJSON["number"].class; 77 | // -> String 78 | JSONlib.convertToSC(j)[\number].class; 79 | // -> Integer 80 | :: 81 | 82 | 83 | SUBSECTION::Representing null in SuperCollider 84 | 85 | code::null:: is a special value in JSON which emphasis::represents the intentional absence of any object value.:: footnote::https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/null::. 86 | 87 | SuperColliders equivalent would be object code::nil::, check link::Classes/Nil:: for more information. 88 | Yet it is not as easy as that because we can not store code::nil:: as a value in a link::Classes/Dictionary:: as this is the sclang syntax to delete a key-value pair within a dictionary because nil represents the return value for an empty slot and code::put(key, nil):: is equivalent to removing the object at code::key::. 89 | 90 | code:: 91 | e = (\foo: nil, \bar: 42); 92 | // -> ( 'bar': 42 ) 93 | :: 94 | 95 | To overcome this constraint we wrap code::nil:: into a link::Classes/Ref:: (shortcut is using the backtick, so code::`nil::) wherever we encounter a code::null:: value in the JSON string. 96 | 97 | code:: 98 | e = (\foo: `nil, \bar: 42); 99 | // -> ( 'foo': `(nil), 'bar': 42 ) 100 | 101 | e[\foo]; 102 | // -> `(nil) 103 | 104 | // use .value to unpack the reference 105 | e[\foo].value 106 | // -> nil 107 | 108 | // you can also use .value on other data types 109 | e[\bar].value 110 | 111 | // generates a null value in our JSON 112 | e.asJSON 113 | // -> { "bar": 42, "foo": null } 114 | :: 115 | 116 | The same applies if we decode code::null:: from JSON string 117 | 118 | code:: 119 | j = "{\"foo\": null, \"bar\": 42}"; 120 | e = JSONlib.convertToSC(j); 121 | // -> ( 'bar': 42, 'foo': `(nil) ) 122 | e[\foo].value; 123 | // -> nil 124 | :: 125 | 126 | See https://github.com/musikinformatik/JSONlib/issues/7 and https://github.com/musikinformatik/JSONlib/issues/5 for discussion. 127 | 128 | CLASSMETHODS:: 129 | 130 | METHOD:: new 131 | 132 | argument:: postWarnings 133 | If code::true:: a warning will be posted if an implicit conversion to a link::Classes/Object#-asCompileString:: occurs as a fallback because JSON has only a limited set of data types. 134 | argument:: useEvent 135 | If code::true:: it will return a link::Classes/Event:: instead of a link::Classes/Dictionary:: which is what link::Classes/String#-parseJSON:: uses 136 | argument:: customEncoder 137 | Using a custom encoder allows to specify the encoding of an abitrary object into its JSON representation. 138 | To introduce a custom encoding (e.g. link::Classes/Point:: or link::Classes/Function::) simply provide a function which accepts the object as a first argument and return a non-code::nil:: value if it a custom representation should be used. 139 | Otherwise, it will be encoded as usual. 140 | 141 | STRONG::Example:: 142 | 143 | A custom encoder which converts a link::Classes/Point:: into an array for the JSON representation. 144 | 145 | code:: 146 | ( 147 | f = {|object| 148 | if(object.class == Point, { 149 | [object.x, object.y] 150 | }); 151 | }; 152 | ) 153 | e = (\myPoint: Point(x: 42, y: 9)); 154 | JSONlib.convertToJSON(e, customEncoder: f); 155 | // -> { "myPoint": [ 42, 9 ] } 156 | :: 157 | 158 | warning:: 159 | You need to take care of any escaping on link::Classes/String:: or any recursion due to using nested objects or arrays. 160 | :: 161 | 162 | argument:: customDecoder 163 | Allows to specify the decoding of a string by providing a function which takes as an argument every value within the return object of link::Classes/String#-parseJSON:: which includes link::Classes/String::, link::Classes/Array::, link::Classes/Dictionary:: and link::Classes/Nil::. 164 | If this function returns a non-code::nil:: value we will use this in the returned sclang object. 165 | 166 | STRONG::Example:: 167 | 168 | A (naive) custom decoder which converts the string code::"point#x#y":: to a link::Classes/Point::. 169 | Consider using regular expressions via e.g. link::Classes/String#-matchRegexp:: for more reliable implementation. 170 | 171 | code:: 172 | ( 173 | f = {|object| 174 | if(object.class == String) { 175 | if(object.beginsWith("point#")) { 176 | // 0 is "point" 177 | var x = object.split($#)[1]; 178 | var y = object.split($#)[2]; 179 | Point(x, y); 180 | }; 181 | }; 182 | }; 183 | ) 184 | j = "{\"myPoint\": \"point#42#9\"}"; 185 | e = JSONlib.convertToSC(j, customDecoder: f); 186 | // -> ( 'myPoint': Point( 42, 9 ) ) 187 | e[\myPoint].x 188 | // -> 42 189 | :: 190 | 191 | 192 | METHOD:: convertToSC 193 | Encodes a given JSON string to a link::Classes/Event::, link::Classes/Dictionary:: or link::Classes/Array::. 194 | 195 | argument:: json 196 | JSON link::Classes/String:: or link::Classes/Symbol:: one wants to parse. 197 | Keep in mind that when using a String any code::":: needs to be escaped with code::\":: (so at least every key needs this) and on a Symbol code::':: needs to be escaped with code::\'::. 198 | If you have copied a JSON from elsewhere either save it as a file and use link::#*parseFile:: or use the console of your browser by simply wrapping the JSON into backticks (e.g. code::`{"my": "json"}`::). 199 | The returned string will contain the all necessary string escapes for SuperCollider." 200 | argument:: customDecoder 201 | See link::#*new:: 202 | argument:: useEvent 203 | See link::#*new:: 204 | argument:: postWarnings 205 | See link::#*new:: 206 | 207 | METHOD:: parseFile 208 | Parses a code::.json:: file in its SuperCollider object representation via link::#*convertToSC:: 209 | argument:: filePath 210 | Path to the JSON file. 211 | argument:: customDecoder 212 | See link::#*new:: 213 | argument:: useEvent 214 | See link::#*new:: 215 | argument:: postWarnings 216 | See link::#*new:: 217 | 218 | 219 | METHOD:: convertToJSON 220 | Decodes a link::Classes/Event::, link::Classes/Dictionary:: or link::Classes/Array:: into its JSON string representation. 221 | argument:: object 222 | The object one wants a JSON representation of. 223 | argument:: customEncoder 224 | See link::#*new:: 225 | argument:: postWarnings 226 | See link::#*new:: 227 | 228 | 229 | INSTANCEMETHODS:: 230 | 231 | PRIVATE:: prConvertToSC 232 | 233 | METHOD:: postWarnings 234 | 235 | See link::#*new:: 236 | 237 | PRIVATE:: prConvertToJson 238 | 239 | METHOD:: customEncoder 240 | 241 | See link::#*new:: 242 | 243 | METHOD:: useEvent 244 | 245 | See link::#*new:: 246 | 247 | METHOD:: customDecoder 248 | 249 | See link::#*new:: 250 | 251 | 252 | EXAMPLES:: 253 | 254 | SUBSECTION::Convert an Event to a JSON string 255 | 256 | code:: 257 | // create a nested event 258 | ( 259 | e = ( 260 | \hello: 42, 261 | \world: ( 262 | \of: "nested", 263 | \objects: [ 264 | 1, 265 | true, 266 | false, 267 | ] 268 | ) 269 | ); 270 | ) 271 | 272 | // get JSON represenatiotn 273 | j = e.asJSON; 274 | // -> { "hello": 42, "world": { "objects": [ 1, true, false ], "of": "nested" } } 275 | 276 | // also possible to use JSONlib 277 | j = JSONlib.convertToJSON(e); 278 | :: 279 | 280 | SUBSECTION::Convert a Dictionary to a JSON string 281 | 282 | code:: 283 | d = Dictionary().put(\foo, 42); 284 | 285 | j = d.asJSON; 286 | // -> { "foo": 42 } 287 | 288 | // also possible to use JSONlib directly 289 | j = JSONlib.convertToJSON(d); 290 | :: 291 | 292 | SUBSECTION::Convert a JSON string to an Event 293 | 294 | See link::#*convertToSC:: on advice how to handle escaping of JSON strings. 295 | 296 | code:: 297 | j = "{\"foo\": 42}"; 298 | 299 | // as event 300 | e = JSONlib.convertToSC(j); 301 | // -> ( 'foo': 42 ) 302 | 303 | // access values via symbols 304 | e[\foo]; 305 | 306 | // as dictionary 307 | d = JSONlib.convertToSC(j, useEvent: false); 308 | // -> Dictionary[ (foo -> 42) ] 309 | 310 | // access values via strings 311 | d["foo"]; 312 | // -> 42 313 | :: 314 | 315 | SUBSECTION::Save to file 316 | 317 | code:: 318 | e = (\something: "new"); 319 | 320 | // convert to JSON string 321 | j = JSONlib.convertToJSON(e); 322 | 323 | // save JSON string on disk as .json file 324 | ( 325 | f = File("~/foo.json".standardizePath, "w"); 326 | f.write(j); 327 | f.close; 328 | ) 329 | 330 | // JSON string is now written to ~/foo.json 331 | :: 332 | 333 | See link::Classes/File:: for further details. 334 | 335 | SUBSECTION::Using null 336 | 337 | See also link::#Representing null in SuperCollider:: and link::Classes/Ref:: 338 | 339 | STRONG::Encoding:: 340 | 341 | code:: 342 | // wrap nil in a Ref (using `) to keep it as a value in an event 343 | e = (\nothing: `nil); 344 | // -> ( 'nothing': `(nil) ) 345 | 346 | JSONlib.convertToJSON(e); 347 | // -> { "nothing": null } 348 | 349 | // using nil directly does NOT work, see 350 | e = (\nothing: nil); 351 | // -> ( ) 352 | :: 353 | 354 | STRONG::Decoding:: 355 | 356 | Consider the JSON code::{"nothing": null}:: 357 | 358 | code:: 359 | j = "{\"nothing\": null}"; 360 | e = JSONlib.convertToSC(j); 361 | // -> ( 'nothing': `(nil) ) 362 | 363 | // accessing nothing returns only a ref 364 | e[\nothing]; 365 | // -> `(nil) 366 | 367 | // unwrap it via .value 368 | e[\nothing].value; 369 | // -> nil 370 | :: 371 | 372 | SUBSECTION::Use custom en/decoders to store functions 373 | 374 | WARNING::Executing arbitrary functions from JSON files can be a security risk. Trust your sources or verify the content before executing any functions.:: 375 | 376 | Useful if you want to exchange functions and data via network (see below) 377 | 378 | code:: 379 | // create event with function 380 | x = (\myFunc: {|x| x**2}, \data: 42); 381 | 382 | // encode it to JSON 383 | ( 384 | a = JSONlib.convertToJSON(x, customEncoder: {|x| 385 | if(x.class==Function, { 386 | "--func%".format(x.asCompileString).quote; 387 | }); 388 | }); 389 | ) 390 | // -> { "data": 42, "myFunc": --func{|x| x**2} } 391 | 392 | // decode JSON to SC with our custom decoder 393 | // which re-constructs the function 394 | ( 395 | b = JSONlib.convertToSC(a, customDecoder: {|x| 396 | if(x.class==String) { 397 | if(x.beginsWith("--func")) { 398 | x["--func".size()..].compile().(); 399 | } 400 | } 401 | }); 402 | ) 403 | // -> ( 'myFunc': a Function, 'data': 42 ) 404 | 405 | b[\myFunc].(4) 406 | // -> 16.0 407 | :: 408 | 409 | SUBSECTION::Transfer nested objects via OSC 410 | 411 | JSON allows to distribute and receive nested objects via OSC by distributing it as a link::Classes/String:: in an OSC message. 412 | 413 | code:: 414 | // create event 415 | e = (\hello: (\foo: 42)); 416 | 417 | // create local listener which converts a JSON string to an SC object 418 | ( 419 | OSCdef(\myJSONparser, func: {|m| 420 | // received strings are symbols, so we need to cast them to strings 421 | "Received %".format(JSONlib.convertToSC(m[1].asString)).postln; 422 | }, path: "/myAddress", recvPort: 57120); 423 | ) 424 | 425 | // send nested event as JSON 426 | ( 427 | NetAddr(hostname: "127.0.0.1", port: 57120).sendMsg( 428 | "/myAddress", 429 | JSONlib.convertToJSON(e), 430 | ); 431 | ) 432 | 433 | // Received ( 'hello': ( 'foo': 42 ) ) 434 | 435 | // clear OSC listener 436 | OSCdef.clear; 437 | :: 438 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 2023 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------