recon-js

Record Notation (RECON) JavaScript Implementation

Usage no npm install needed!

<script type="module">
  import reconJs from 'https://cdn.skypack.dev/recon-js';
</script>

README

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.

recon.header(value, tag)

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.

recon.headers(value, tag)

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');
// {scheme: "http", authority: {host: "example.com"}}

recon.Uri.parse('http://example.com/');
// {scheme: "http", authority: {host: "example.com"}, path: ["/"]}

recon.Uri.parse('http://example.com/foo/bar');
// {scheme: "http", authority: {host: "example.com"}, path: ["/", "foo", "/", "bar"]}

recon.Uri.parse('http://example.com?search');
// {scheme: "http", authority: {host: "example.com"}, query: "search"}

recon.Uri.parse('http://example.com?key=value');
// {scheme: "http", authority: {host: "example.com"}, query: {key: "value"}}

recon.Uri.parse('http://example.com?key=value&other');
// {scheme: "http", authority: {host: "example.com"}, query: [{key: "value"}, "other"]}

recon.Uri.parse('http://example.com#anchor');
// {scheme: "http", authority: {host: "example.com"}, fragment: "anchor"}

recon.Uri.parse('http://user@example.com');
// {scheme: "http", authority: {host: "example.com", userInfo: "user"}}

recon.Uri.parse('http://user:pass@example.com');
// {scheme: "http", authority: {host: "example.com", username: "user", password: "pass"}}

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]*