///|
/// The printer: a CSS syntax tree back to CSS text.
///
/// Three modes, one traversal. The alternative -- a Wadler-style layout
/// document, the way `shrubbery`'s own printer works -- would be over-built
/// here: CSS has no expression nesting to break intelligently across lines, and
/// its whole layout question is "one declaration per line, indented by depth".
/// A layout engine earns its keep when there are genuine choices to search
/// over; here there are none, and the direct traversal is both shorter and
/// exactly predictable.
///
/// This package depends on `ast` and `span` and nothing else -- not on the
/// parser, not on `error-report`. Building a tree in memory and printing it is
/// a complete use of this library on its own.
///|
/// How much whitespace to spend.
pub(all) enum Style {
/// One declaration per line, two-space indent. What a person reads.
Pretty
/// One rule per line, declarations inline. What a diff reads.
Compact
/// No whitespace that can be dropped, no trailing semicolons.
Minified
} derive(Eq)
///|
priv struct Printer {
buf : StringBuilder
style : Style
mut depth : Int
}
///|
/// Render a stylesheet.
pub fn stylesheet(sheet : @ast.Stylesheet, style? : Style = Pretty) -> String {
let p = { buf: StringBuilder(), style, depth: 0, }
p.write_stylesheet(sheet)
p.buf.to_string()
}
///|
/// Render one rule, for tests and for error messages that quote a rule.
pub fn rule(r : @ast.CssRule, style? : Style = Pretty) -> String {
let p = { buf: StringBuilder(), style, depth: 0, }
p.write_rule(r)
p.buf.to_string()
}
///|
/// Render one declaration.
pub fn declaration(d : @ast.Declaration, style? : Style = Pretty) -> String {
let p = { buf: StringBuilder(), style, depth: 0, }
p.write_decl(d)
p.buf.to_string()
}
///|
/// Render one selector.
pub fn selector(s : @ast.Selector, style? : Style = Pretty) -> String {
let p = { buf: StringBuilder(), style, depth: 0, }
p.write_selector(s)
p.buf.to_string()
}
///|
/// Render a value list, the contents of one declaration.
pub fn value(
vs : Array[@ast.ComponentValue],
style? : Style = Pretty,
) -> String {
let p = { buf: StringBuilder(), style, depth: 0, }
p.write_values(vs[:])
p.buf.to_string()
}
// ------------------------------------------------------------------ mechanics
///|
fn Printer::put(self : Printer, s : String) -> Unit {
self.buf.write_string(s)
}
///|
fn Printer::is_min(self : Printer) -> Bool {
self.style == Minified
}
///|
/// A space that only a readable mode spends.
fn Printer::sp(self : Printer) -> Unit {
if !self.is_min() {
self.put(" ")
}
}
///|
/// A line break, or nothing at all when the mode has no lines.
fn Printer::nl(self : Printer) -> Unit {
match self.style {
Pretty => {
self.put("\n")
for _ in 0.. self.put(" ")
Minified => ()
}
}
///|
/// The separator between two rules at the same level.
fn Printer::rule_sep(self : Printer) -> Unit {
match self.style {
Pretty => {
self.put("\n")
for _ in 0.. self.put("\n")
Minified => ()
}
}
///|
/// A rule that is a rule only in position: it closes itself with nothing.
fn is_bogus_rule(r : @ast.CssRule) -> Bool {
match r {
Bogus(_) => true
_ => false
}
}
// ------------------------------------------------------------------ stylesheet
///|
fn Printer::write_stylesheet(self : Printer, sheet : @ast.Stylesheet) -> Unit {
let mut first = true
// At the top of a file `rule_sep` is a newline, and in `Minified` it is
// nothing at all -- so an echoed unparsable region ran straight into whatever
// followed it. A stray `;` between statements is valid CSS and ignored, which
// makes it the right thing to bound one with.
let mut prev_needs_semi = false
for item in sheet.items {
if !first {
if prev_needs_semi {
self.put(";")
}
self.rule_sep()
}
first = false
prev_needs_semi = match item {
Rule(r) => {
self.write_rule(r)
is_bogus_rule(r)
}
Comment(c) => {
self.write_comment(c)
false
}
Bogus(b) => {
self.put(b.text)
true
}
}
}
if !self.is_min() && !first {
self.put("\n")
}
}
///|
fn Printer::write_comment(self : Printer, c : @ast.Comment) -> Unit {
self.put("/*")
self.put(c.text)
self.put("*/")
}
///|
/// A `{ ... }` body: the one place indentation is decided.
fn Printer::write_body(
self : Printer,
items : ArrayView[@ast.BlockItem],
) -> Unit {
self.sp()
self.put("{")
if items.length() == 0 {
self.put("}")
return
}
self.depth = self.depth + 1
let mut prev_needs_semi = false
let mut first = true
for item in items {
if !first && prev_needs_semi {
self.put(";")
}
first = false
self.nl()
prev_needs_semi = match item {
Decl(d) => {
self.write_decl(d)
true
}
Rule(r) => {
self.write_rule(r)
// A rule closes itself with `}` -- unless it is a `Bogus`, which is a
// rule only in position. It carries arbitrary text and closes with
// nothing, so it needs the same `;` a bare `Bogus` item does.
is_bogus_rule(r)
}
Comment(c) => {
self.write_comment(c)
false
}
Bogus(b) => {
self.put(b.text)
// `true`, unlike a rule or a comment: those close themselves with `}`
// and `*/`, and a `Bogus` carries arbitrary text that closes nothing.
// Without the `;` the next declaration runs straight into it in
// `Minified`, where the newline that used to separate them is not
// written -- and a `;` after unparseable text is what CSS's own error
// recovery looks for anyway, so it bounds the damage rather than
// spreading it.
true
}
}
}
// A trailing `;` after the last declaration is optional in CSS and costs a
// byte, so only the readable modes spend it -- and they spend it because
// adding a declaration after it should be a one-line diff, not two.
if prev_needs_semi && !self.is_min() {
self.put(";")
}
self.depth = self.depth - 1
self.nl()
self.put("}")
}
///|
fn Printer::write_decl_body(self : Printer, b : @ast.DeclBlock) -> Unit {
self.write_body(b.decls[:])
}
// ----------------------------------------------------------------- declaration
///|
fn Printer::write_decl(self : Printer, d : @ast.Declaration) -> Unit {
self.put(d.property.text())
self.put(":")
self.sp()
self.write_values(d.value[:])
if d.important {
self.sp()
self.put("!important")
}
}
///|
/// Values, with the spacing CSS actually requires.
///
/// The only subtlety is that a `,` binds to what precedes it and a `/` sits
/// between its operands, so neither can take the plain "one space between
/// items" rule. Everything else does.
fn Printer::write_values(
self : Printer,
vs : ArrayView[@ast.ComponentValue],
) -> Unit {
let mut i = 0
while i < vs.length() {
let v = vs[i]
if i > 0 {
match v {
Comma => ()
Slash => self.sp()
_ =>
match vs[i - 1] {
Comma => self.sp()
Slash => self.sp()
_ => self.put(" ")
}
}
}
self.write_value(v)
i = i + 1
}
}
///|
fn Printer::write_value(self : Printer, v : @ast.ComponentValue) -> Unit {
match v {
Ident(s) => self.put(s)
Str(s) => self.write_quoted(s)
Num(n) => self.put(n.repr)
Dimension(n, u) => {
self.put(n.repr)
self.put(u)
}
Percentage(n) => {
self.put(n.repr)
self.put("%")
}
Hex(d) => {
self.put("#")
self.put(d)
}
Url(u) => {
self.put("url(")
self.write_quoted(u)
self.put(")")
}
Function(name, args) => {
self.put(name)
self.put("(")
self.write_values(args[:])
self.put(")")
}
Paren(inner) => {
self.put("(")
self.write_values(inner[:])
self.put(")")
}
Bracket(inner) => {
self.put("[")
self.write_values(inner[:])
self.put("]")
}
Comma => self.put(",")
Slash => self.put("/")
Delim(s) => self.put(s)
Bogus(b) => self.put(b.text)
}
}
///|
/// A double-quoted CSS string.
///
/// Double quotes always, because picking whichever quote appears less often in
/// the body would make the output depend on the content in a way that is
/// invisible until a diff shows it.
fn Printer::write_quoted(self : Printer, s : String) -> Unit {
self.put("\"")
for c in s {
match c {
'"' => self.put("\\\"")
'\\' => self.put("\\\\")
'\n' => self.put("\\A ")
'\r' => self.put("\\D ")
_ => self.buf.write_char(c)
}
}
self.put("\"")
}
// -------------------------------------------------------------------- selectors
///|
fn Printer::write_selector_list(
self : Printer,
sels : ArrayView[@ast.Selector],
) -> Unit {
let mut first = true
for s in sels {
if !first {
self.put(",")
self.sp()
}
first = false
self.write_selector(s)
}
}
///|
fn Printer::write_selector(self : Printer, s : @ast.Selector) -> Unit {
match s {
Simple(c) => self.write_compound(c)
Complex(left, comb, right) => {
self.write_selector(left)
self.write_combinator(comb)
self.write_compound(right)
}
Relative(comb, inner) => {
// A leading combinator keeps its trailing space but never a leading one.
match comb {
Descendant => ()
_ => {
self.put(comb_text(comb))
self.sp()
}
}
self.write_selector(inner)
}
Bogus(b) => self.put(b.text)
}
}
///|
/// The descendant combinator IS a space, so it cannot be dropped in minified
/// output the way the others can.
fn Printer::write_combinator(self : Printer, c : @ast.Combinator) -> Unit {
match c {
Descendant => self.put(" ")
_ => {
self.sp()
self.put(comb_text(c))
self.sp()
}
}
}
///|
fn comb_text(c : @ast.Combinator) -> String {
match c {
Descendant => " "
Child => ">"
NextSibling => "+"
SubsequentSibling => "~"
Column => "||"
}
}
///|
fn Printer::write_compound(self : Printer, c : @ast.Compound) -> Unit {
match c.type_sel {
Some(t) => self.write_type_selector(t)
None =>
// A compound with no type and no qualifiers is the universal selector
// spelled implicitly; it has to print as something.
if c.quals.length() == 0 {
self.put("*")
}
}
for q in c.quals {
self.write_qualifier(q)
}
}
///|
fn Printer::write_type_selector(self : Printer, t : @ast.TypeSelector) -> Unit {
match t {
Universal(ns) => {
self.write_ns(ns)
self.put("*")
}
Named(ns, name) => {
self.write_ns(ns)
self.put(name)
}
}
}
///|
fn Printer::write_ns(self : Printer, ns : @ast.NsPrefix?) -> Unit {
match ns {
None => ()
Some(None_) => self.put("|")
Some(Any) => self.put("*|")
Some(Named(n)) => {
self.put(n)
self.put("|")
}
}
}
///|
fn Printer::write_qualifier(self : Printer, q : @ast.Qualifier) -> Unit {
match q {
Class(n) => {
self.put(".")
self.put(n)
}
Id(n) => {
self.put("#")
self.put(n)
}
Nesting => self.put("&")
Attr(a) => self.write_attr(a)
Pseudo(p) => self.write_pseudo_class(p)
Element(e) => self.write_pseudo_element(e)
Bogus(b) => self.put(b.text)
}
}
///|
fn Printer::write_attr(self : Printer, a : @ast.AttrSelector) -> Unit {
self.put("[")
self.write_ns(a.ns)
self.put(a.name)
match a.matcher {
None => ()
Some((op, v)) => {
self.put(op.text())
match v {
Str(s) => self.write_quoted(s)
Ident(s) => self.put(s)
}
match a.case_ {
Default => ()
Insensitive => self.put(" i")
Sensitive => self.put(" s")
}
}
}
self.put("]")
}
///|
fn Printer::write_pseudo_class(self : Printer, p : @ast.PseudoClass) -> Unit {
self.put(":")
match p {
Simple(n) => self.put(n)
Sub(n, sels) => {
self.put(n)
self.put("(")
self.write_selector_list(sels[:])
self.put(")")
}
Nth(n, anb, of_) => {
self.put(n)
self.put("(")
self.write_anb(anb)
match of_ {
None => ()
Some(sels) => {
self.put(" of ")
self.write_selector_list(sels[:])
}
}
self.put(")")
}
Lang(langs) => {
self.put("lang(")
let mut first = true
for l in langs {
if !first {
self.put(",")
self.sp()
}
first = false
self.put(l)
}
self.put(")")
}
Dir(d) => {
self.put("dir(")
self.put(d)
self.put(")")
}
Unknown(n, args) => {
self.put(n)
if args.length() > 0 {
self.put("(")
self.write_values(args[:])
self.put(")")
}
}
}
}
///|
/// `An+B`, in the shortest spelling that means the same thing.
fn Printer::write_anb(self : Printer, anb : @ast.AnB) -> Unit {
if anb.a == 0 {
self.put(anb.b.to_string())
return
}
if anb.a == 1 {
self.put("n")
} else if anb.a == -1 {
self.put("-n")
} else {
self.put(anb.a.to_string())
self.put("n")
}
if anb.b > 0 {
self.put("+")
self.put(anb.b.to_string())
} else if anb.b < 0 {
self.put("-")
self.put((-anb.b).to_string())
}
}
///|
fn Printer::write_pseudo_element(
self : Printer,
e : @ast.PseudoElement,
) -> Unit {
// Always `::`, even for the four legacy single-colon spellings. The tree
// records that this is an element, not which colon count the source used,
// and `::` is correct for all of them.
self.put("::")
match e {
Simple(n) => self.put(n)
Sub(n, sels) => {
self.put(n)
self.put("(")
self.write_selector_list(sels[:])
self.put(")")
}
Fn(n, args) => {
self.put(n)
self.put("(")
self.write_values(args[:])
self.put(")")
}
}
}