Record Notation (RECON)
RECON brings attributes into the era of object notation, and provides a simple
grammar and uniform tree model for attributed text markup. RECON aims to
combine the minimalism of JSON with the expressiveness of XML in a
human-friendly syntax.
Getting Started
The RECON JavaScript library has no dependencies, and can run in any standard
JavaScript environment. Use npm
to incorporate the RECON JavaScript library
into Node.js projects.
npm install --save recon-js
var recon = require('recon-js');
var record = recon.parse('[Welcome @a(href:"index.html")@em[home].]');
Language Primer
Primtives
RECON has three primitive datatypes: text, number, and data.
Text
Text values take one of two forms: a quoted string, or an unquoted
identifier.
"string"
identifier
Numbers
Numbers serialize as decimal literals.
-1
3.14
6.02e23
Data
Binary data serializes as a leading '%' symbol, followed by a base64 literal.
%AA==
Records
RECON's sole aggregate datatype, the record, plays the combined role of array
and associative array. Think of a record as a partially keyed list. The
example record below contains two ordered items, first a "subject" field with
value "Greetings", then the unkeyed string "Hello, Earthlings!".
{ subject: "Greetings", "Hello, Earthlings!" }
A single comma, a single semicolon, or one or more newlines separate items.
Newline separated records provide a clean syntax for pretty-printed documents.
{
subject: "Re: Greetings"
"Hi Martians!"
}
Records support arbitrary values as slot keys.
{
@planet Jupiter: {}
@god Jupiter: {}
}
Blocks
Top-level documents can omit the curly braces around their root record. We
call the content of a record, sans curly braces, a block. When a block
contains only a single item, the value of the block reduces to just the value
of the item it contains. The example block below is equivalent to the sample
record above.
subject: "Re: Greetings"
"Hi Martians!"
Attributes
The @ sign introduces an attribute. Attributes call out key fields of a
record. The previous markup example further reduces to the form below.
{
"Hello, "
{
"@em":
"world"
}
"!"
}
Note that the @em
field above has no explicit value. The RECON data model
refers to unspecified–but existent–values as extant. We say that the record
@em[world]
has an extant attribute named em
.
Of course, attributes can have associated values too. Place attribute
parameters in parentheses, following the attribute's name.
@answer(42)
@event("onClick")
The above attributes are structurally equivalent to:
{"@answer":42}
{"@event":"onClick"}
Attribute parentheses enclose a block, meaning attribute values construct an
implicit record when needed. An example, with its desugared equivalent, follows.
@img(src: "tesseract.png", width: 10, height: 10, depth: 10, time: -1)
{
"@img": {
src: "tesseract.png"
width: 10
height: 10
depth: 10
time: -1
}
}
Attributes modify adjacent values. Modified values interpolate into the
record formed by their adjacent attributes. Here are some examples of values
with prefix, postfix, and circumfix attributes:
@duration 30
30 @seconds
@duration 30 @seconds
@relative @duration 30 @seconds
The above attribute expressions desugar to the following records:
{ "@duration":, 30 }
{ 30, "@seconds": }
{ "@duration":, 30, "@seconds": }
{ "@relative":, "@duration":, 30, "@seconds": }
Modified records flatten into the record formed by their adjacent attributes.
So @point{x:0,y:0}
, reduces to {"@point":,x:0,y:0}
, not
{"@point":,{x:0,y:0}}
.
Markup
Square brackets denote markup. Markup offers an inverted syntax for records,
with values embedded in text, as opposed to text embedded in records.
[Hello, @em[world]!]
Markup is really just syntactic sugar for records. The above example expresses
the exact same structure as the one below.
{ "Hello, "; @em "world"; "!" }
Curly braces within markup lift the enclosed block into the markup's record.
The following records are equivalent.
[Answer: {42}.]
{ "Answer", 42, "." }
Square brackets lift nested markup into the enclosing record. Make sure to
backslash escape square brackets if you want to include them verbatim.
[Say [what]?]
{ "Say ", "what", "?"}
[Say \[what\]?]
{ "Say [what]?" }
Sequential attributes within markup don't chain; each markup-embedded
attribute inserts a nested record.
[http@colon@slash@slash]
{ "http", @colon, @slash, @slash }
Attributes in markup can prefix curly brace enclosed blocks, and nested markup.
[Goals: @select(max:2){fast,good,cheap}.]
{ "Goals: ", @select(max:2){fast,good,cheap}, "." }
Beware that whitespace inside markup is significant. Notice how the single
space added to the example below completely changes its meaning, when compared
to the previous example.
[Goals: @select(max:2) {fast,good,cheap}.]
{ "Goals: ", @select(max:2), " ", {fast,good,cheap}, "." }
JavaScript Transcoding
RECON types map to JavaScript types as follows:
RECON Type | JavaScript Type |
---|
Text | String |
Number | Number |
Data | Uint8Array |
Field | Object |
Record | Array |
Extant | null |
Absent | undefined |
true | true |
false | false |
Note that RECON treats true
and false
as ordinary text values. But for
compatibility with JSON, the RECON JavaScript library decodes true
and
false
as JavaScript boolean values.
JavaScript Records
Since the order of items in RECON records is significant, records transcode as
JavaScript arrays. Fields within a record transcode as a JavaScript object
with a single key-value pair. Attrinbutes preserve the @
prefix in their
names to distinguish them from other fields.
For convenience, the RECON decoder also defines a non-enumerable object member
for each record field decoded into a JavaScript object. This enables field
access with subscript notation, without causing duplicate values to appear
when invoking JSON.stringify()
on a decoded RECON object. Note that updating
a JavaScript object member with subscript notation will leave its corresponding
array element in an inconsistent state. This shouldn't be an issue in cases
where the application doesn't care about the order of RECON-decoded JavaScript
objects. The RECON JavaScript library takes care to use the mutated versions
of object members when encoding a JavaScript object as RECON. The library also
provides a recon.set(object, key, value)
function that will update both the
named object member and the object's ordered field member, if present.
JSON Examples
RECON: {1, 2, 3}
JSON: [1, 2, 3]
RECON: {subject: "Greetings", "Hello, Jovians!"}
JSON: [{"subject": "Greetings"}, "Hello, Jovians!"]
RECON: @event("onClick")
JSON: {"@event": "onClick"}
RECON: [Hello, @em[world]!]
JSON: ["Hello, ", [{"@em": null}, "world"], "!"]
JavaScript API
recon.parse(string)
Parses a string for a RECON value.
recon.stringify(value)
Serializes a JavaScript value as a RECON string.
recon.base64(string)
Base64-decodes a string
into a Uint8Array
.
recon.length(value)
Returns the number of items in value
, if value
is a record. Returns 0
if value
is not a record.
recon.tag(value)
Returns the key of the head field, if value
is a record and its head value
is a field. Otherwise returns undefined
.
recon.target(value)
Returns the first non-Field item in value
, if value
is a record. Returns
value
itself if value
is not a record, or if it has no non-Field items.
recon.flattened(value)
If value
is a record that contains a single value, returns that value.
Returns null
if value
is an empty record or not a record.
Returns the value of the tag
field of value
if value
is a record whose
head item is a Field with key tag
. Otherwise returns undefined
.
Returns the value of the tag
field of value
, coerced to a record, if
value
is a record whose head item is a Field with key tag
. Otherwise
returns undefined
.
recon.head(value)
Returns the first value or field value, if value
is a record. Returns
value
itself if value
is not a record.
recon.tail(value)
Returns a record containing all but the first item of value
, if value
is a
record. Returns an empty record if value
is not a record.
recon.body(value)
Returns the flattened tail of value
.
recon.has(record, key)
Returns true
if some record
has a value associated with a key
.
recon.get(record, key)
Returns the value associated with a key
in some record
.
recon.set(record, key, value)
Associates a value
with a key
in some record
, keeping the record's array
representation consistent with its object representation.
recon.remove(record, key)
Removes any field with the given key
from some record
.
recon.keys(value)
Returns an array containing the field keys of value
, if value
is a record.
Returns an empty array if value
is not a record.
recon.values(value)
Returns an array containing the field and item values of value
, if value
is a record. Returns an empty array if value
is not a record.
recon.forEach(record, callback[, thisArg])
Invokes callback
for every item in record
. If provided, thisArg
will
be passed to each invocation of callback
for use as its this
value.
callback
is invoked with three arguments:
- the item value, if the item is a field, otherwise the item itself
- the item key, if the item is a field, otherwise
undefined
- the record being traversed
recon.concat(x, y)
Concatenates two RECON valuesinto a single, flattened record.
recon.equal(x, y)
Compares two RECON items for equality.
recon.compare(x, y)
Orders two RECON items relative to each other. Returns -1
if x
comes
before y
, returns 1
if x
comes after y
, and returns 0
if x
and y
are equivalent.
RECON defines a total ordering over all items. Items of different types sort
in the following relative order: attributes, slots, records, data, text,
numbers, extant, then absent.
recon.Uri.parse(string)
Parses a URI string into a structured URI object. Parsed URIs have the
following structure:
{
scheme: <string>,
authority: {
(host | ipv4 | ipv6): <string>,
(userInfo | username + password): <string>,
},
path: [<string>],
query: <string> | [], // key
fragment: <string>
}
If a URI string has an undefined component, then the corresponding field of
the parsed URI object will also be undefined. Query arrays have a
corresponding field member set for each key-value parameter.
Examples
recon.Uri.parse('http://example.com');
recon.Uri.parse('http://example.com/');
recon.Uri.parse('http://example.com/foo/bar');
recon.Uri.parse('http://example.com?search');
recon.Uri.parse('http://example.com?key=value');
recon.Uri.parse('http://example.com?key=value&other');
recon.Uri.parse('http://example.com#anchor');
recon.Uri.parse('http://user@example.com');
recon.Uri.parse('http://user:pass@example.com');
recon.Uri.stringify(uri)
Serializes a parsed URI object as a URI string.
recon.Uri.resolve(base, relative)
Returns the parsed absolute URI obtained by resolving a relative URI against
some base URI;
recon.Uri.unresolve(base, absolute)
Returns the parsed relative URI obtained by unresolving an absolute URI against
some base URI.
Language Grammar
SP ::= #x20 | #x9
NL ::= #xA | #xD
WS ::= SP | NL
Char ::= [#x1-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
NameStartChar ::=
[A-Z] | "_" | [a-z] |
[#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] |
[#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] |
[#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] |
[#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
NameChar ::= NameStartChar | '-' | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
MarkupChar ::= Char - ('\\' | '@' | '{' | '}' | '[' | ']')
StringChar ::= Char - ('"' | '\\' | '@' | '{' | '}' | '[' | ']' | '\b' | '\f' | '\n' | '\r' | '\t')
CharEscape ::= '\\' ('"' | '\\' | '/' | '@' | '{' | '}' | '[' | ']' | 'b' | 'f' | 'n' | 'r' | 't')
Base64Char ::= [A-Za-z0-9+/]
Block ::= WS* Slots WS*
Slots ::= Slot SP* ((',' | ';' | NL) WS* Slots)?
Slot ::= BlockValue (SP* ':' SP* BlockValue?)?
Attr ::= '@' (Ident | String) ('(' Block ')')?
BlockAttr ::= (Attr | Comment) SP* BlockValue?
BlockValue ::= (BlockAttr | Record | Markup | Ident | String | Number | Data | Comment) SP* BlockAttr?
InlineValue ::= Attr (Record | Markup)? | Record | Markup
Record ::= '{' Block '}'
Markup ::= '[' (MarkupChar* | CharEscape | InlineValue)* ']'
Ident ::= NameStartChar NameChar*
String ::= '"' (StringChar* | CharEscape)* '"'
Number ::= '-'? (([1-9] [0-9]*) | [0-9]) ('.' [0-9]+)? (('E' | 'e') ('+' | '-')? [0-9]+)?
Data ::= '%' (Base64Char{4})* (Base64Char Base64Char ((Base64Char '=') | ('=' '=')))?
Comment ::= '#' [^\n]*