Skip to content

Instantly share code, notes, and snippets.

@hikari-no-yume
Last active August 29, 2015 14:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save hikari-no-yume/085463d0bfd0d81463e7 to your computer and use it in GitHub Desktop.
Save hikari-no-yume/085463d0bfd0d81463e7 to your computer and use it in GitHub Desktop.
#define ggon_decode
// real/string ggon_decode(string text)
// Decodes a GGON (Gang Garrison Object Notation) text
// Returns either a string or a ds_map handle
var text;
text = argument0;
var tokens;
tokens = ggon_tokenise(text);
var result;
result = ggon_parse_tokens(tokens);
ds_queue_destroy(tokens);
return result;
#define ggon_encode
// string ggon_encode(real/string value)
// Encodes a ds_map or string to a GGON (Gang Garrison Object Notation) text
// Encoding is recursive, real values in the maptreated as ds_map handles
// Returns the encoded text
var value;
value = argument0;
var out;
// string value
if (is_string(value))
{
// Check if alphanumeric
var alphanumeric, i, char;
// We don't set alphanumeric to true ahead of time because of empty strings
alphanumeric = false;
for (i = 1; i <= string_length(value); i += 1)
{
char = string_char_at(value, i);
if (('a' <= char and char <= 'z') or ('A' <= char and char <= 'Z') or ('0' <= char and char <= '9') or char == '_' or char == '.' or char == '+' or char == '-')
{
alphanumeric = true;
}
else
{
alphanumeric = false;
break;
}
}
// As no quoting is necessary, just output verbatim
if (alphanumeric)
{
out = value;
return out;
}
out = "'";
for (i = 1; i <= string_length(value); i += 1)
{
char = string_char_at(value, i);
// ' and \ are escaped as \' and ''
if (char == "'" or char == "\")
out += '\' + char;
// newlines, carriage returns, tabs and null bytes are escaped specially
else if (char == chr(10))
out += '\n';
else if (char == chr(13))
out += '\r';
else if (char == chr(9))
out += '\t';
else if (char == chr(0))
out += '\0';
// Otherwise we can just output verbatim
else
out += char;
}
out += "'";
return out;
// not string, therefore real, therefore ds_map representing map value
} else {
out = "{";
var key, first;
first = true;
for (key = ds_map_find_first(value); is_string(key); key = ds_map_find_next(value, key))
{
if (!first)
out += ',';
else
first = false;
out += ggon_encode(key) + ':' + ggon_encode(ds_map_find_value(value, key));
}
out += "}";
return out;
}
#define ggon_tokenise
// real ggon_tokenise(string text)
// Tokenises a GGON (Gang Garrison Object Notation) text
// Returns a ds_queue of tokens
// For each token, list has type ("punctuation" or "string") and value
// Thus you must iterate over the list two elements at a time
var text;
text = argument0;
var tokens;
tokens = ds_queue_create();
while (string_length(text) > 0)
{
var char;
char = string_char_at(text, 1);
// basic punctuation: '{', '}', ':' and ','
if (char == '{' or char == '}' or char == ':' or char == ',')
{
ds_queue_enqueue(tokens, 'punctuation');
ds_queue_enqueue(tokens, char);
text = string_copy(text, 2, string_length(text) - 1);
continue;
}
// skip whitespace (space, tab, new line or carriage return)
if (char == ' ' or char == chr(9) or char == chr(10) or char == chr(13))
{
text = string_copy(text, 2, string_length(text) - 1);
continue;
}
// "identifiers" (bare word strings, really) of format [a-zA-Z0-9_]+
if (('a' <= char and char <= 'z') or ('A' <= char and char <= 'Z') or ('0' <= char and char <= '9') or char == '_' or char == '.' or char == '+' or char == '-')
{
var identifier;
identifier = '';
while (('a' <= char and char <= 'z') or ('A' <= char and char <= 'Z') or ('0' <= char and char <= '9') or char == '_' or char == '.' or char == '+' or char == '-')
{
if (string_length(text) == 0)
show_error('Error when tokenising GGON: unexpected end of text while parsing string', true);
identifier += char;
text = string_copy(text, 2, string_length(text) - 1);
char = string_char_at(text, 1);
}
ds_queue_enqueue(tokens, 'string');
ds_queue_enqueue(tokens, identifier);
continue;
}
// string
if (char == "'")
{
var str;
str = '';
text = string_copy(text, 2, string_length(text) - 1);
char = string_char_at(text, 1);
while (char != "'")
{
if (string_length(text) == 0)
show_error('Error when tokenising GGON: unexpected end of text while parsing string', true);
// escaping
if (char == '\')
{
text = string_copy(text, 2, string_length(text) - 1);
char = string_char_at(text, 1);
if (char == "'" or char == '\')
str += char;
// new line escape
else if (char == 'n')
str += chr(10);
// carriage return escape
else if (char == 'r')
str += chr(13);
// tab escape
else if (char == 't')
str += chr(9);
// null byte escape
else if (char == '0')
str += chr(0);
else
show_error('Error when tokenising GGON: unknown escape sequence "\' + char + '"', true);
}
else
{
str += char;
}
text = string_copy(text, 2, string_length(text) - 1);
char = string_char_at(text, 1);
}
if (char != "'")
show_error('Error when tokenising GGON: unexpected character "' + char + '" while parsing string, expected "' + "'" + '"', true);
text = string_copy(text, 2, string_length(text) - 1);
char = string_char_at(text, 1);
ds_queue_enqueue(tokens, 'string');
ds_queue_enqueue(tokens, str);
continue;
}
show_error('Error when tokenising GGON: unexpected character "' + char + '"', true);
}
return tokens;
#define ggon_parse_tokens
// real/string ggon_decode(real tokens)
// Decodes a tokenised GGON (Gang Garrison Object Notation) text ds_queue
// Returns either a string or a ds_map handle
var tokens;
tokens = argument0;
var tokenType, tokenValue;
while (!ds_queue_empty(tokens))
{
tokenType = ds_queue_dequeue(tokens);
tokenValue = ds_queue_dequeue(tokens);
if (tokenType == 'string')
return tokenValue;
if (tokenType == 'punctuation')
{
// GGON has only two primitives - it could only be string or opening {
if (tokenValue != '{')
show_error('Error when parsing GGON: unexpected token "' + tokenValue + '"', true);
var map;
map = ds_map_create();
tokenType = ds_queue_head(tokens);
if (tokenType == 'punctuation')
{
tokenType = ds_queue_dequeue(tokens);
tokenValue = ds_queue_dequeue(tokens);
// { can only be followed by } or a key
if (tokenValue != '}')
show_error('Error when parsing GGON: unexpected token "' + tokenValue + '" after opening {', true);
// It's {} so we can just return our empty map
return map;
}
else if (tokenType == 'string')
{
// Parse each key of our map
while (!ds_queue_empty(tokens))
{
tokenType = ds_queue_dequeue(tokens);
tokenValue = ds_queue_dequeue(tokens);
var key;
key = tokenValue;
tokenType = ds_queue_dequeue(tokens);
tokenValue = ds_queue_dequeue(tokens);
// Following token must be a : as we have a key
if (tokenType != 'punctuation')
show_error('Error when parsing GGON: unexpected ' + tokenType + ' after key', true);
if (tokenValue != ':')
show_error('Error when parsing GGON: unexpected token "' + tokenValue + '" after key', true);
// Now we recurse to parse our value!
var value;
value = ggon_parse_tokens(tokens);
ds_map_add(map, key, value);
tokenType = ds_queue_dequeue(tokens);
tokenValue = ds_queue_dequeue(tokens);
// After key, colon and value, next token must be , or }
if (tokenType != 'punctuation')
show_error('Error when parsing GGON: unexpected ' + tokenType + ' after value', true);
if (tokenValue == ',')
continue;
else if (tokenValue == '}')
return map;
else
show_error('Error when parsing GGON: unexpected token "' + tokenValue + '" after value', true);
}
}
else
show_error('Error when parsing GGON: unknown token type "' + tokenType + '"', true);
}
show_error('Error when parsing GGON: unknown token type "' + tokenType + '"', true);
}
#define ggon_list_to_map
// real ggon_list_to_map(real list)
// Takes a ds_list
// Returns a ds_map which has a "length" key and a string key for each index
// This is a function to make dealing with GGON easier, as GGON lacks lists
var list;
list = argument0;
var map;
map = ds_map_create();
ds_map_add(map, "length", string(ds_list_size(list)));
var i;
for (i = 0; i < ds_list_size(list); i += 1)
{
ds_map_add(map, string(i), ds_list_find_value(list, i));
}
return map;
#define ggon_map_to_list
// real ggon_map_to_list(real map)
// Takes a ds_map which has a "length" key and string indexes up to length
// Returns a ds_list which has the values of those indexes in order
// This is a function to make dealing with GGON easier, as GGON lacks lists
// This is designed to work with the map ggon_list_to_map would produce
var map;
map = argument0;
var list, length;
list = ds_list_create();
length = real(ds_map_find_value(map, 'length'));
var i;
for (i = 0; i < length; i += 1)
ds_list_add(list, ds_map_find_value(map, string(i)));
return list;
#define ggon_destroy_map
// void ggon_destroy_map(real map)
// Destroys a ds_map recursively (real values assumed to be ds_maps)
var map;
map = argument0;
var key, value;
for (key = ds_map_find_first(map); is_string(key); key = ds_map_find_next(map, key))
{
value = ds_map_find_value(map, key);
if (is_real(value))
ggon_destroy_map(value);
}
ds_map_destroy(map);

GGON - Gang Garrison Object Notation

Notation

GGON has two primitives:

  • Strings - These convert to and from GML strings. There are two styles:

    • Unquoted:

        foo
      

      Strings don't need quotes if they fit the format [a-zA-Z0-9\.\-\+]+

      This means all of the following are valid unquoted strings:

      • 12.5
      • true
      • under_scores
      • camelCase
      • things-with-dashes
      • -12.2e75
      • .2
      • object.style
      • domain.com
    • Quoted:

        'foo bar'
      

      These allow arbitrary characters and support six escape codes:

      • \\ - Backslash
      • \' - Single quote
      • \n - Newline
      • \r - Carriage Return
      • \t - Tab
      • \0 - Null byte

      Only single quotes are allowed because double quotes are dumb.

  • Maps. These look much like JSON Objects and convert to and from GML ds_maps. They map string keys to string or map values:

      {
          someKey: someValue,
          'some key': someValue,
          nestedMap: {}
      }
    

GGON ignores whitespace, so you can format things how you like.

If you're wondering what encoding scheme GGON uses, its core syntax is ASCII. You can put whatever you want in your strings. UTF-8 would obviously be compatible, but Gang Garrison 2 doesn't really understand that.

Usage

From GML, it is easy to work with. To make a map, just make a ds_map then use ggon_encode:

var map, ggon;

map = ds_map_create();
ds_map_add(map, 'someKey', 'someValue');
ds_map_add(map, 'some key', 'someValue');

ggon = ggon_encode(map);
ds_map_destroy(map);

ggon_encode will assume reals are handles to a ds_map for creating a nested map. This means if you want to store a real, you need to convert it to a string. For example:

var map, nestedMap, ggon;

nestedMap = ds_map_create();
ds_map_add(map, 'foo', 'bar');
ds_map_add(map, 'foobar', 'qux');

map = ds_map_create();
ds_map_add(map, 'nest', nestedMap);
ds_map_add(map, 'x', string(x));
ds_map_add(map, 'y', string(y));

ggon = ggon_encode(map);
ds_map_destroy(map);
ds_map_destroy(nestedMap);

Decoding works similarly - use ggon_decode:

var ggon, map, foo, nestedMap, foobar;

ggon = '{foo:bar,qux:{foobar:boo}}';
map = ggon_decode(ggon);

foo = ds_map_find_value(map, 'foo');
nestedMap = ds_map_find_value(map, 'qux');
foobar = ds_map_find_value(nestedMap, 'foobar');

Because ds_maps produced by ggon_decode contain only string keys, they can be easily looped over:

var key;
for (key = ds_map_find_first(map); is_string(key); key = ds_map_find_next(map, key))
{
    show_message(key + ': ' + ds_map_find_value(map, key));
}

Sadly, because GML only has two types and hence you cannot distinguish between a ds_map and ds_list handle, GGON does not have a list syntax or directly support them. For this reason, utility functions are provided to convert lists to maps and vice-versa. Convering a list to a map uses ggon_list_to_map:

var list, map, ggon;
list = ds_list_create();
ds_list_add(list, "example");
ds_list_add(list, ggon_decode('{some:map}'));

map = ggon_list_to_map(list);
ggon = ggon_encode(map);
show_message(ggon); // output: {0:example,1:{some:map},length:2}

ds_map_destroy(map);
ds_map_destroy(ds_list_find_value(list, 1));
ds_list_destroy(list);

Converting back to a map uses ggon_map_to_list:

var ggon, map, list;
ggon = '{0:example,1:{some:map},length:2}';
map = ggon_decode(ggon);

list = ggon_map_to_list(map);
ds_map_destroy(map);
show_message(ds_list_find_value(list, 0)); // output: example

ds_map_destroy(ds_list_find_value(list, 1));
ds_list_destroy(list);

Because destroying maps with nested maps can be a hastle, the convenience function ggon_destroy_map is provided:

var map;
map = ggon_decode('{this:{map:{has:{a:{lot:{of:{nesting:wow}}}}}}}');

// Do stuff with map here

ggon_destroy_map(map); // This destroys the map and every nested map to an infinite depth
Copy link

ghost commented Aug 14, 2014

"Only single quotes are allowed because double quotes are dumb."
I'm sure double quotes think you are dumb too.

@hikari-no-yume
Copy link
Author

@MrRatermat Heh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment