// SafeC Standard Library — XML serialization implementation (see xml.h)
#pragma once
#include <std/serial/xml.h>
#include <std/serial/value.h>
#include <std/collections/string.h>
#include <std/fmt.h>
#include <std/convert.h>
namespace std {
// Batches runs of bytes needing no escaping into one push_n_() call rather
// than one push_char() per byte — most string content has no special
// chars, so this is the hot path.
static void xml_flush_run_(&String out, const char* s, unsigned long run_start, unsigned long i) {
if (i > run_start) {
unsafe { out.push_n_(s + run_start, i - run_start); }
}
}
static void xml_append_escaped_(&String out, const char* s) {
unsigned long i = 0UL;
unsigned long run_start = 0UL;
while (1) {
char c;
unsafe { c = s[i]; }
if (c == (char)0) { break; }
if (c == '&' || c == '<' || c == '>' || c == '"' || c == '\'') {
xml_flush_run_(out, s, run_start, i);
if (c == '&') { unsafe { out.push("&"); } }
else if (c == '<') { unsafe { out.push("<"); } }
else if (c == '>') { unsafe { out.push(">"); } }
else if (c == '"') { unsafe { out.push("""); } }
else { unsafe { out.push("'"); } }
i = i + 1UL;
run_start = i;
} else {
i = i + 1UL;
}
}
xml_flush_run_(out, s, run_start, i);
}
// Shared by json.sc's json_append_float_ in spirit, duplicated rather than
// shared across modules since the two are otherwise independent leaf
// files (see the file-level comments in each) — trims push_float's fixed
// trailing zeros.
static void xml_append_float_(&String out, double v) {
unsafe {
unsigned long before = out.length();
out.push_float(v, 6);
unsigned long end = out.length();
while (end > before) {
int c = out.char_at(end - 1UL);
if (c != (int)'0') { break; }
end = end - 1UL;
}
if (end > before) {
int c = out.char_at(end - 1UL);
if (c == (int)'.') { end = end - 1UL; }
}
out.truncate(end);
}
}
void xml_write(const &Value v, const &char tag, &String out) {
int kind;
unsafe { kind = v.kind; }
if (kind == VAL_NULL) {
unsafe { out.push_char('<'); out.push(tag); out.push("/>"); }
return;
}
unsafe { out.push_char('<'); out.push(tag); out.push_char('>'); }
if (kind == VAL_BOOL) {
int b;
unsafe { b = v.bool_val; }
unsafe { out.push(b != 0 ? "true" : "false"); }
} else if (kind == VAL_INT) {
long long n;
unsafe { n = v.int_val; }
unsafe { out.push_int(n); }
} else if (kind == VAL_FLOAT) {
double f;
unsafe { f = v.float_val; }
xml_append_float_(out, f);
} else if (kind == VAL_STRING) {
const char* s;
unsafe {
if (v.str_val != (char*)0) { s = (const char*)v.str_val; }
else { s = (const char*)""; }
}
xml_append_escaped_(out, s);
} else if (kind == VAL_ARRAY) {
unsigned long n;
unsafe { n = v.arr_val.length(); }
unsigned long i = 0UL;
while (i < n) {
&Value elem;
unsafe { elem = v.arr_val.at(i); }
xml_write(elem, "item", out);
i = i + 1UL;
}
} else if (kind == VAL_OBJECT) {
unsigned long n;
unsafe { n = v.obj_val.length(); }
unsigned long i = 0UL;
while (i < n) {
struct ObjectEntry* e;
unsafe { e = v.obj_val.at(i); }
const char* key;
unsafe { key = (const char*)e->key; }
&Value val;
unsafe { val = e->val; }
unsafe { xml_write(val, key, out); }
i = i + 1UL;
}
}
unsafe { out.push("</"); out.push(tag); out.push_char('>'); }
}
inline struct String value_to_xml(const &Value v, const &char root_tag) {
struct String out = String::new();
xml_write(v, root_tag, &out);
return out;
}
// ── Parser ────────────────────────────────────────────────────────────────
// Recursive descent over this module's own output shape (see xml_write
// above) — not a general XML parser (no attributes, no CDATA, no entity
// refs beyond the 5 xml_append_escaped_ emits, no processing instructions).
// Two mapping ambiguities are inherent to the tag-per-value grammar itself
// (not parser bugs) and are resolved the same way on every parse:
// - <t></t> (no children, no text) could be an empty string or an empty
// object; this parser always resolves it to VAL_STRING "".
// - scalar text content that happens to look like a number/bool ("42",
// "true") is read back as VAL_INT/VAL_BOOL rather than VAL_STRING, same
// as it would if it had actually been serialized from one. JSON has no
// such ambiguity (its syntax marks type explicitly) — prefer it when
// lossless round-tripping through arbitrary string content matters.
struct XmlCursor_ {
const char* text;
unsigned long pos;
int ok;
};
static char xml_peek_(struct XmlCursor_* c) {
unsafe { return c->text[c->pos]; }
}
static int xml_eat_(struct XmlCursor_* c, char expected) {
if (xml_peek_(c) != expected) { unsafe { c->ok = 0; } return 0; }
unsafe { c->pos = c->pos + 1UL; }
return 1;
}
// Matches a literal keyword starting at the cursor, consuming it on success
// and leaving the cursor untouched on failure.
static int xml_eat_literal_(struct XmlCursor_* c, const char* word) {
unsigned long start;
unsafe { start = c->pos; }
unsigned long i = 0UL;
while (1) {
char want;
unsafe { want = word[i]; }
if (want == (char)0) { break; }
if (xml_peek_(c) != want) { unsafe { c->pos = start; } return 0; }
unsafe { c->pos = c->pos + 1UL; }
i = i + 1UL;
}
return 1;
}
// True if the cursor sits at '</' — the closing tag of the element whose
// content is currently being parsed (i.e. no more children / no text).
static int xml_at_close_(struct XmlCursor_* c) {
if (xml_peek_(c) != '<') { return 0; }
unsigned long p;
char n;
unsafe { p = c->pos; n = c->text[p + 1UL]; }
return n == '/';
}
static struct String xml_read_name_(struct XmlCursor_* c) {
unsigned long start;
unsafe { start = c->pos; }
while (1) {
char ch = xml_peek_(c);
if (ch == (char)0 || ch == '>' || ch == '/' || ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') { break; }
unsafe { c->pos = c->pos + 1UL; }
}
unsigned long end;
unsafe { end = c->pos; }
struct String out = String::new();
if (end > start) { unsafe { out.push_n_(c->text + start, end - start); } }
return out;
}
// Reads the tag name of the element starting at the cursor without moving
// it — used by the children loop to decide array-vs-object (and to get an
// object key) before recursing into xml_parse_element_ for the value.
static struct String xml_peek_tag_name_(struct XmlCursor_* c) {
unsigned long saved;
unsafe { saved = c->pos; }
unsafe { c->pos = c->pos + 1UL; } // skip '<'
struct String name = xml_read_name_(c);
unsafe { c->pos = saved; }
return name;
}
// Reads raw text up to (not including) the next '<' or end of input,
// decoding the 5 entities xml_append_escaped_ emits.
static struct String xml_decode_text_(struct XmlCursor_* c) {
struct String out = String::new();
unsigned long run_start;
unsafe { run_start = c->pos; }
while (1) {
char ch = xml_peek_(c);
if (ch == (char)0 || ch == '<') { break; }
if (ch == '&') {
unsigned long cur;
unsafe { cur = c->pos; }
if (cur > run_start) { unsafe { out.push_n_(c->text + run_start, cur - run_start); } }
unsafe { c->pos = c->pos + 1UL; }
if (xml_eat_literal_(c, "amp;")) { out.push_char('&'); }
else if (xml_eat_literal_(c, "lt;")) { out.push_char('<'); }
else if (xml_eat_literal_(c, "gt;")) { out.push_char('>'); }
else if (xml_eat_literal_(c, "quot;")) { out.push_char('"'); }
else if (xml_eat_literal_(c, "apos;")) { out.push_char('\''); }
else { unsafe { c->ok = 0; } break; }
unsafe { run_start = c->pos; }
} else {
unsafe { c->pos = c->pos + 1UL; }
}
}
unsigned long cur2;
unsafe { cur2 = c->pos; }
if (cur2 > run_start) { unsafe { out.push_n_(c->text + run_start, cur2 - run_start); } }
return out;
}
// Classifies decoded scalar text the same way json_parse_number_ would —
// "true"/"false" -> bool, an all-digit (optional leading '-') span -> int,
// a digit span containing '.'/'e'/'E' -> float, everything else -> string.
static struct Value xml_infer_scalar_(struct String* text) {
if (text->eq_cstr("true")) { return Value::from_bool(1); }
if (text->eq_cstr("false")) { return Value::from_bool(0); }
unsigned long len = text->length();
if (len > 0UL) {
int allInt = 1;
int allFloatChars = 1;
int hasDigit = 0;
int hasDotOrExp = 0;
unsigned long i = 0UL;
while (i < len) {
int ch = text->char_at(i);
int isDigit = (ch >= (int)'0' && ch <= (int)'9');
int isSign = (ch == (int)'-' || ch == (int)'+');
int isDotExp = (ch == (int)'.' || ch == (int)'e' || ch == (int)'E');
if (isDigit) { hasDigit = 1; }
if (isDotExp) { hasDotOrExp = 1; }
if (!(isDigit || (isSign && i == 0UL))) { allInt = 0; }
if (!(isDigit || isSign || isDotExp)) { allFloatChars = 0; }
i = i + 1UL;
}
if (allInt && hasDigit) {
int ok = 0;
long long n = text->parse_int(&ok);
if (ok) { return Value::from_int(n); }
}
if (allFloatChars && hasDigit && hasDotOrExp) {
int ok = 0;
double f = text->parse_float(&ok);
if (ok) { return Value::from_float(f); }
}
}
return Value::from_str(text->as_ptr());
}
static struct Value xml_parse_element_(struct XmlCursor_* c) {
xml_eat_(c, '<');
struct String tag = xml_read_name_(c);
while (1) {
char ch = xml_peek_(c);
if (ch == (char)0) { unsafe { c->ok = 0; } break; }
if (ch == '>' || ch == '/') { break; }
unsafe { c->pos = c->pos + 1UL; }
}
int selfClose = 0;
if (xml_peek_(c) == '/') { selfClose = 1; unsafe { c->pos = c->pos + 1UL; } }
xml_eat_(c, '>');
struct Value result;
int okNow;
unsafe { okNow = c->ok; }
if (selfClose || !okNow) {
result = Value::null();
} else if (xml_at_close_(c)) {
// No children, no text: could be an empty string or empty object —
// see the file-level ambiguity note above.
result = Value::from_str("");
} else if (xml_peek_(c) == '<') {
struct Value arr = Value::array();
struct Value obj = Value::object();
int isArray = 1;
int first = 1;
while (xml_at_close_(c) == 0) {
int stillOk;
unsafe { stillOk = c->ok; }
if (!stillOk) { break; }
if (xml_peek_(c) != '<') { unsafe { c->ok = 0; } break; }
struct String childTag = xml_peek_tag_name_(c);
struct Value childVal = xml_parse_element_(c);
int childIsItem = childTag.eq_cstr("item");
if (first) { isArray = childIsItem; first = 0; }
if (isArray) {
arr.array_push(childVal);
} else {
unsafe { obj.object_set(childTag.as_ptr(), childVal); }
}
childTag.free();
}
if (isArray) { obj.free(); result = arr; }
else { arr.free(); result = obj; }
} else {
struct String text = xml_decode_text_(c);
result = xml_infer_scalar_(&text);
text.free();
}
// Self-closing elements ('<t/>') have no separate '</t>' to consume.
if (!selfClose) {
xml_eat_(c, '<');
xml_eat_(c, '/');
struct String closeName = xml_read_name_(c);
if (closeName.eq(&tag) == 0) { unsafe { c->ok = 0; } }
closeName.free();
while (1) {
char cc = xml_peek_(c);
if (cc == '>' || cc == (char)0) { break; }
unsafe { c->pos = c->pos + 1UL; }
}
xml_eat_(c, '>');
}
tag.free();
return result;
}
struct Value xml_parse(const &char text, ?&int ok) {
struct XmlCursor_ c;
c.text = text;
c.pos = 0UL;
c.ok = 1;
struct Value v;
if (xml_peek_(&c) != '<') {
c.ok = 0;
v = Value::null();
} else {
v = xml_parse_element_(&c);
}
if (ok != (int*)0) { unsafe { *ok = c.ok; } }
if (c.ok == 0) {
v.free();
return Value::null();
}
return v;
}
struct Xml Xml::new(const &char root_tag) {
struct Xml x;
x.root = root_tag;
return x;
}
void Xml::write(const &Value v, &String out) const { xml_write(v, self.root, out); }
struct Value Xml::parse(const &char text, ?&int ok) const { return xml_parse(text, ok); }
} // namespace std