TestsTested | ✓ |
LangLanguage | SwiftSwift |
License | Apache 2 |
ReleasedLast Release | May 2016 |
SPMSupports SPM | ✓ |
Maintained by Chris Sachs, Ewan Mellor, Chris Sachs.
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.
To get started with the RECON Swift library, add the following to your CocoaPods profile:
use_frameworks!
pod 'Recon'
Next run pod install
to load the dependency. Then import the Recon
module in your code.
import Recon
RECON has three primitive datatypes: text, number, and data.
Text values take one of two forms: a quoted string, or an unquoted identifier.
"string"
identifier
Numbers serialize as decimal literals.
-1
3.14
6.02e23
Binary data serializes as a leading ’%’ symbol, followed by a base64 literal.
%AA==
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: {}
}
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!"
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}}
.
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}, "." }
Parse a RECON-encoded string by invoking the recon
function.
let event = recon("@event(onClick),@command")!
Serialize a RECON value using its recon
method.
event.recon // returns "{@event(onClick),@command}""
Use a value’s reconBlock
method to flatten its top-level record, if it has one.
event.reconBlock // returns "@event(onClick),@command""
Subscripts get a record’s children by index, or by key.
let msg = recon("{from: me, to: you}")!
msg[0] // returns Attr("from", "me")
msg["to"] // returns Item("you")
Subscripting a non-existent key returns Value.Absent
.
msg["cc"]
msg[2]
recon("2.0")!["number"]
Because Value.Absent
is a value, subscripting is a “closed” operation.
recon("{foo: {bar: {baz: win}}}")!["foo"]["bar"]["baz"] // returns Item("win")
Implicit conversion from Swift literals to RECON values makes record construction easy.
Value(Attr("img", [Slot("src", "...")]), Slot("width", 10), Slot("height", 10), [Attr("caption", [Slot("lang", "en")]), "English Caption"], [Attr("caption", [Slot("lang", "es")]), "Spanish Caption"])
// returns @img(src:"..."){width:10,height:10,@caption(lang:en)"English Caption",@caption(lang:es)"Spanish Caption"}
The Swift library represents RECON values using the following algebraic data type:
enum Item {
case Field(Recon.Field)
case Value(Recon.Value)
}
enum Field {
case Attr(String, Value)
case Slot(Value, Value)
}
enum Value {
case Record(Recon.Record)
case Text(String)
case Data(Recon.Data)
case Number(Double)
case Extant
case Absent
}
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 ('(' Block ')')?
BlockValue ::=
Attr SP* BlockValue? |
(Record | Markup | Ident | String | Number | Data) SP* (Attr SP* BlockValue?)?
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 '=') | ('=' '=')))?