///|
/// Whether a name can be written as an identifier rather than escaped.
///
/// The reference's rule, which is NOT the notation's own: it admits Unicode
/// letters, `_` and digits, and nothing else -- so an emoji identifier, which
/// the notation accepts, is written as a `#{...}` escape. Following the
/// notation's rule here instead would produce output the reference does not,
/// which is the one thing this printer must not do.
fn is_plain_identifier(s : String) -> Bool {
if s.length() == 0 {
return false
}
let body = if s.has_prefix("#%") {
s.clamped_view(start=2).to_owned()
} else {
s
}
if body.length() == 0 {
return false
}
let mut first = true
for c in body {
let ok = if first {
@unicode.is_letter(c) || c == '_'
} else {
@unicode.is_letter(c) || @unicode.is_numeric(c) || c == '_'
}
if !ok {
return false
}
first = false
}
true
}
///|
/// An atom, written the way the reference writes it.
fn atom_text(shrub : @ast.Shrub) -> String {
match shrub {
Id(name) =>
if is_plain_identifier(name) {
name
} else {
escape_symbol(name)
}
Op(name) => name
Kw(name) =>
if is_plain_identifier(name) {
"~" + name
} else {
"~#{" + racket_symbol(name) + "}"
}
Lit(d) => datum_text(d)
Parsed(d) => datum_text(d)
_ => ""
}
}
///|
fn escape_symbol(name : String) -> String {
"#{" + racket_symbol(name) + "}"
}
///|
/// Racket's `write` for a symbol: bars when the bare spelling would read as
/// something else, a backslash for a lone `|`, and as-is otherwise.
fn racket_symbol(name : String) -> String {
let out = StringBuilder()
if symbol_needs_bars(name) {
out.write_char('|')
for c in name {
if c == '|' || c == '\\' {
out.write_char('\\')
}
out.write_char(c)
}
out.write_char('|')
return out.to_string()
}
for c in name {
if c == '|' {
out.write_char('\\')
}
out.write_char(c)
}
out.to_string()
}
///|
fn symbol_needs_bars(name : String) -> Bool {
if name.length() == 0 || name == "." {
return true
}
if looks_numeric(name) {
return true
}
for c in name {
match c {
' ' | '\t' | '\n' | '\r' | '(' | ')' | '[' | ']' | '{' | '}' =>
return true
'"' | '\'' | '`' | ',' | ';' | '\\' => return true
_ => ()
}
}
false
}
///|
/// Whether the bare spelling would be read back as a number rather than a name.
fn looks_numeric(name : String) -> Bool {
let mut i = 0
if name.get_char(0) is Some('+') || name.get_char(0) is Some('-') {
i = 1
}
if i >= name.length() {
return false
}
let mut any = false
while i < name.length() {
match name.get_char(i) {
Some(c) if c >= '0' && c <= '9' => any = true
Some('.') | Some('/') => ()
_ => return false
}
i = i + 1
}
any
}
///|
/// A literal, written the way the reference writes it.
fn datum_text(d : @sexp.Datum) -> String {
match d {
Str(s) => string_to_string(s)
Bs(b) => bytes_to_string(b)
Int_(n) => n.to_string()
Rat(n, den) => n.to_string() + "/" + den.to_string()
Flo(v) =>
if v != v {
"#nan"
} else if v == @double.infinity {
"#inf"
} else if v == @double.neg_infinity {
"#neginf"
} else {
double_to_string(v)
}
Bool_(v) => if v { "#true" } else { "#false" }
Void => "#void"
Sym(name) =>
if is_plain_identifier(name) {
name
} else {
escape_symbol(name)
}
Kw(name) =>
if is_plain_identifier(name) {
"~" + name
} else {
"~#{" + racket_symbol(name) + "}"
}
Ch(c) => "#{" + char_text(c) + "}"
Rx(px, pattern) =>
"#{" + (if px { "#px" } else { "#rx" }) + string_to_string(pattern) + "}"
Nil => "#{()}"
Pair(_) => "#{" + racket_list_text(d) + "}"
Vec(xs) => {
let out = StringBuilder()
out.write_string("#{#(")
for i in 0.. 0 {
out.write_char(' ')
}
out.write_string(datum_inner_text(xs[i]))
}
out.write_string(")}")
out.to_string()
}
Other(text) => "#{" + text + "}"
}
}
///|
/// A datum written INSIDE a `#{...}`, where Racket's own notation applies and
/// shrubbery's does not.
fn datum_inner_text(d : @sexp.Datum) -> String {
match d {
Sym(name) => racket_symbol(name)
Kw(name) => "#:" + name
Str(s) => string_to_string(s)
Bs(b) => bytes_to_string(b)
Int_(n) => n.to_string()
Rat(n, den) => n.to_string() + "/" + den.to_string()
Flo(v) => double_to_string(v)
Bool_(v) => if v { "#t" } else { "#f" }
Void => "#"
Ch(c) => char_text(c)
Rx(px, pattern) =>
(if px { "#px" } else { "#rx" }) + string_to_string(pattern)
Nil => "()"
Pair(_) => racket_list_text(d)
Vec(xs) => {
let out = StringBuilder()
out.write_string("#(")
for i in 0.. 0 {
out.write_char(' ')
}
out.write_string(datum_inner_text(xs[i]))
}
out.write_char(')')
out.to_string()
}
Other(text) => text
}
}
///|
fn racket_list_text(d : @sexp.Datum) -> String {
let out = StringBuilder()
out.write_char('(')
let mut cur = d
let mut first = true
while cur is Pair(head, tail) {
if !first {
out.write_char(' ')
}
first = false
out.write_string(datum_inner_text(head))
cur = tail
}
if !(cur is Nil) {
out.write_string(" . ")
out.write_string(datum_inner_text(cur))
}
out.write_char(')')
out.to_string()
}
///|
/// Racket's character syntax.
fn char_text(c : Char) -> String {
let u = c.to_int()
match c {
'\u{0}' => "#\\nul"
'\u{7}' => "#\\alarm"
'\u{8}' => "#\\backspace"
'\t' => "#\\tab"
'\n' => "#\\newline"
'\u{B}' => "#\\vtab"
'\u{C}' => "#\\page"
'\r' => "#\\return"
' ' => "#\\space"
'\u{7F}' => "#\\rubout"
_ =>
if u < 0x20 {
"#\\u" + pad_hex(u, 4)
} else if u > 0xFFFF {
"#\\U" + pad_hex(u, 8)
} else {
"#\\" + c.to_string()
}
}
}
///|
fn pad_hex(v : Int, width : Int) -> String {
let hex = v.to_string(radix=16).to_upper()
let out = StringBuilder()
for _ in 0..<(width - hex.length()) {
out.write_char('0')
}
out.write_string(hex)
out.to_string()
}