CX+AI

Documents: one door in, one door out

JSON, XML, CSV, HTML, email, images, bytes - one parseDoc, one exportDoc

Documents: one door in, one door out

A program that reads XML, JSON, CSV, a mailbox and a PNG usually carries five libraries with five ideas of what a "node" is. In CX+AI there is one door in, parseDoc, one door out, exportDoc, and everything that comes through either is the same json document you already know how to read.

One page. Everything here was run.


The two doors

json d = parseDoc(text);                 // read it, whatever it is
string s = exportDoc(d);                 // write it back, as itself

parseDoc takes the text and, optionally, the format and some options. exportDoc takes the document and, optionally, a format to write it AS.

json d = parseDoc(text, CX_CSV);         // read it as this
string x = exportDoc(d, CX_XML);         // write it as that

That second line is the whole point of one door: a document read through one lens can be written through another, because there is only ever one kind of document in between.


Ask the document about itself

A document's own facts live under the reserved word doc:

json d = parseDoc("{\"a\":1,\"b\":[2,3]}");
printf("valid=%d count=%d\n", d->doc->valid, d->doc->count);
valid=1 count=2

d->doc->valid is whether it parsed, ->doc->count how many members the root has, ->doc->type the root's kind, and ->doc->format which lens it came from. Everything else — d["a"], d[0], d->len — is the json you already write.


Leave the format off and it works it out

json j = parseDoc("{\"a\":1,\"b\":[2,3]}");
json x = parseDoc("<r id=\"7\"><k>v</k></r>");
printf("J=%d X=%s\n", j["a"], x["k"]);
J=1 X=v

With no format argument the door sniffs: JSON, XML (and the HTML and SVG it recognises inside XML), or plain text. Every other lens is asked for by name — a CSV file and a hex dump are both "some text" to a sniffer, and guessing between them is how a tool corrupts a file while reporting success.


The lenses

ConstantReadsComes out as
CX_JSONJSON textthe document itself
CX_XMLXML@attr for attributes, #text for text
CX_SVGSVGthe XML lens in ordered mode
CX_HTMLa web pagethe XML lens, tolerant
CX_CSVCSVan array of objects keyed by the header row
CX_TEXTanythingone string
CX_EMAILan RFC-822 messageheaders, body, parts, attachments
CX_BINARYraw bytes{"#bytes", "size", "mime"}
CX_HEXa hex dump or hex stringthe same document, written as text
CX_IMAGEpng / jpg / bmp / gifthe header decoded, the file kept whole
CX_FORMa=1&b=x+y{"a": "1", "b": "x y"}
CX_CXCX sourcethe token rows the record writes

XML: four conventions, and they are the whole of it

json d = parseDoc("<order id=\"7\"><item>rope</item><item>salt</item></order>", CX_XML);
printf("id=%s first=%s n=%d\n", d["@id"], d["item"][0], d["item"]->doc->count);
printf("root=%s\n", jsonRootName(d));
id=7 first=rope n=2
root=order
  1. An attribute is the @-prefixed member.
  2. An element's own text is #text — or the member itself, when it has neither attributes nor children.
  3. A repeated element name is an array. item above is two elements, so it is one array under one key.
  4. The root element's NAME lives on the document (jsonRootName), because a json document has nowhere else to put it.

There is no XML document type. An XML document is a json document, and d->doc->format is what decides which text comes back out.


SVG: a picture is a document, and the order is the picture

A drawing is markup, so CX+AI writes one the same way it writes any other document — build it, say what it is, ask for the text.

_json pic {
  {
    "@xmlns": "http://www.w3.org/2000/svg",
    "@viewBox": "0 0 60 20",
    "rect": { "@x": "0", "@y": "0", "@width": "60", "@height": "20", "@fill": "#eee" },
    "text": { "@x": "6", "@y": "14", "#text": "CX+AI" }
  }
}
pic->doc->format = CX_XML;
jsonSetRootName(pic, "svg");
println(exportDoc(pic));
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 20"><rect x="0" y="0" width="60" height="20" fill="#eee"></rect><text x="6" y="14">CX+AI</text></svg>

That is the four XML conventions doing all the work and nothing new being learned: attributes are @ members, the element's own text is #text, and the root element's name lives on the document. Hand that string to a browser and it draws. cx_http serves it as image/svg+xml from the .svg extension, so a CX program can generate a diagram and serve it without either half touching a file.

Reading one back: order is the content

Every other document in this chapter is a bag of named things, and the order siblings arrived in does not matter. A picture is not. SVG is painter's order — later shapes draw on top of earlier ones — so a reader that groups rect, circle, rect into "two rects and a circle" has changed the picture, not just the tree.

So the SVG lens keeps the order, and it does not have to be asked:

s.s = "<svg><rect id=\"under\"></rect><circle id=\"dot\"></circle><rect id=\"over\"></rect></svg>";
json d = parseDoc(s, CX_SVG);
printf("kids=%d order=%s,%s,%s\n", d["#kids"]->doc->count,
       d["#kids"][0]["#name"], d["#kids"][1]["#name"], d["#kids"][2]["#name"]);
printf("same=%d\n", exportDoc(d) == s);
kids=3 order=rect,circle,rect
same=1

An ordered document holds its children as one array under #kids, in document order, each carrying its element name as #name. Text between elements is an entry of its own, in its place, with no #name at all — which is how a reader tells a run of text from a child. A leaf is unchanged: its text is still just its text.

A document whose root element is svg is read this way without being told, and so is one read as CX_SVG.

Every other document is untouched, and that is deliberate

Order costs a member on every element that has children, so nothing pays for it unless it asks. The same markup, read both ways:

s.s = "<doc><rect id=\"a\"></rect><circle id=\"b\"></circle><rect id=\"c\"></rect></doc>";
json plain = parseDoc(s, CX_XML);
json kept  = parseDoc(s, CX_XML, "{\"ordered\":true}");
printf("plain=%s\n", exportDoc(plain));
printf("kept =%s\n", exportDoc(kept));
plain=<doc><rect id="a"></rect><rect id="c"></rect><circle id="b"></circle></doc>
kept =<doc><rect id="a"></rect><circle id="b"></circle><rect id="c"></rect></doc>

The first is the array-by-name rule from the section above, working exactly as it always has — d["rect"] is still an array of two. The second asked, so it got its order back. Ask with {"ordered": true} on any XML document where the sequence is the meaning: a rule list, an ordered form, XHTML.


HTML is the XML lens, forgiving

json d = parseDoc("<p>one<p>two<br>", CX_HTML);
printf("valid=%d\n", d->doc->valid);
valid=1

Unclosed p, li and td are closed by the rule every browser shares, void elements close themselves, entities are decoded, and script and style bodies come through verbatim. Pass {"tolerant": false} to refuse a broken page instead of repairing it.


Bytes are a document too

json d = parseDoc("hello", CX_BINARY);
printf("size=%d mime=%s\n", d["size"], d["mime"]);
size=5 mime=application/octet-stream

Nothing is decoded. #bytes is a blob — the bytes themselves, in memory, not a base64 string. They are only ever base64 when you ask for the document as JSON text, because JSON has no other way to carry a byte.

CX_HEX is the same document wearing text: in, a hexdump -C dump or a bare hex string; out, the canonical dump.


An image keeps the file

json d = parseDoc(fread("logo.png"), CX_IMAGE);
printf("%s %dx%d, %d channels\n", d["format"], d["width"], d["height"], d["channels"]);
string same = exportDoc(d);

The header is decoded — width, height, channels, format, mime — and the file itself is kept whole as the blob. So writing it back is byte-identical, not because the encoder is good but because nothing was ever decoded.


A form post

json d = parseDoc("name=Ada+Lovelace&tag=x&tag=y", CX_FORM);
printf("name=%s tags=%d\n", d["name"], d["tag"]->doc->count);
name=Ada Lovelace tags=2

A space is +, not %20 — that is form encoding, not URI encoding, and a decoder that reads + as a plus sign turns every space in a message into a plus sign. A repeated key is an array, the same convention XML uses for a repeated element, and the writer puts it back as one pair per element.


Your own source, as rows

json d = parseDoc("int a = 1;", CX_CX);
printf("rows=%d\n", d->doc->count);
rows=3

Each row is a token — its kind, its text, its subtype — the same rows the project's own source record is built from. One reader, so the record and the lens can never disagree about what a token is.

There is no writer yet, and exportDoc says so rather than guessing: writing CX back means spelling every name by its kind, and that needs the compiler's own classification, not a token stream. A guessed render compiles and is wrong invisibly, which is the one outcome worse than refusing.


What your program pays for

The compiler links the lenses your program names. Write CX_IMAGE and you get the image lens; never mention it and it is not in your binary. Measured, one small program per lens:

lensadded
binary+512
image+2,560
hex+3,072
form+3,584
email+6,144
csv+15,360
cx+833,536

JSON, XML and text are always there, which is why the sniffer only ever reaches those three. The CX lens is large because it is the compiler's own reader — which is exactly why no program should carry it unasked.

If the format is decided at run time — read from a file, a header, argv — nothing in your source names it, so say so once:

#pragma lens image

A lens that was not linked refuses and tells you that line. It never returns an empty document and lets you find out later.


Counting

Everything counts from 0, strings included. substr(s, 0, 3) is the first three characters and substr(s, -3) the last three; strstr answers -1 when the needle is absent, so >= 0 is the test and if (strstr(...)) is the bug.