
Display.pudu
Pudu161 lines6.9 KB
1/** @Log.Domain.Display.Module — values and messages rendered as styled text */2module PuduLangLog.Domain.Display34import Std.Decimal as Decimal5import PuduLangLog.Domain.Dates as Dates6import PuduLangLog.Domain.Json as Json7import PuduLangLog.Domain.Numbers as Numbers8import PuduLangLog.Domain.Padding as Padding9import PuduLangLog.Domain.Properties as Properties10import PuduLangLog as Log1112/** @Log.Domain.Display.Style — the role a piece of text plays */13export type Style = Plain | Secondary | Tertiary | Invalid | NullStyle | NameStyle | StringStyle | NumberStyle | BooleanStyle | ScalarStyle | LevelStyle(Log.Level)1415/** @Log.Domain.Display.Piece — text with its style */16export type Piece = { style: Style, text: Str }171819export const TYPE_TAG: Str = "$type"202122export fn plain(pieces: &Array[Piece]) -> Str { pieces.map(|piece: Piece| piece.text).join("") }232425export fn scalar(atom: &Log.Scalar, format: &Option[Str]) -> Piece {26 match atom {27 case Log.Null => Piece{style: NullStyle, text: "null"}28 case Log.Boolean(flag) => Piece{style: BooleanStyle, text: if flag { "true" } else { "false" }}29 case Log.Integer(number) => Piece{style: NumberStyle, text: numeric(Numbers.formatInteger(number, formatText(format)), show(number), format)}30 case Log.Real(number) => Piece{style: NumberStyle, text: numeric(Numbers.formatReal(number, formatText(format)), Numbers.realText(number), format)}31 case Log.Exact(number) => Piece{style: NumberStyle, text: numeric(Numbers.formatExact(number, formatText(format)), Decimal.toText(number), format)}32 case Log.Text(text) => Piece{style: StringStyle, text: if *format == Some("l") { text } else { "\"" + text.replace("\"", "\\\"") + "\"" }}33 case Log.Moment(moment) => Piece{style: ScalarStyle, text: Dates.formatMoment(&moment, format)}34 case Log.Span(millis) => Piece{style: ScalarStyle, text: Dates.formatSpan(millis, format)}35 case Log.Binary(bytes) => Piece{style: ScalarStyle, text: Json.hexOf(&bytes)}36 }37}383940fn numeric(formatted: Option[Str], fallback: Str, format: &Option[Str]) -> Str {41 if *format == None { return fallback }42 match formatted {43 case Some(text) => text44 case None => fallback45 }46}474849fn formatText(format: &Option[Str]) -> Str {50 match format {51 case Some(text) => text52 case None => ""53 }54}55565758export fn value(held: &Log.Value, format: &Option[Str]) -> Array[Piece] {59 match held {60 case Log.Scalar(atom) => [scalar(&atom, format)]61 case Log.Sequence(items) => {62 var pieces = [punctuation("[")]63 var first = true64 for item in items {65 if !first { pieces = pieces.push(punctuation(", ")) }66 pieces = pieces.concat(value(&item, format))67 first = false68 }69 pieces.push(punctuation("]"))70 }71 case Log.Structure(shape) => structure(&shape)72 case Log.Dictionary(entries) => {73 var pieces = [punctuation("[")]74 var first = true75 for entry in entries {76 pieces = pieces.push(punctuation(if first { "(" } else { ", (" }))77 pieces = pieces.push(scalar(&entry.key, &None)).push(punctuation(": ")).concat(value(&entry.value, &None)).push(punctuation(")"))78 first = false79 }80 pieces.push(punctuation("]"))81 }82 }83}848586fn structure(shape: &Log.Structure) -> Array[Piece] {87 var pieces: Array[Piece] = []88 if let Some(tag) = shape.tag { pieces = pieces.push(Piece{style: NameStyle, text: tag + " "}) }89 pieces = pieces.push(punctuation("\{ "))90 var first = true91 for property in shape.properties {92 if !first { pieces = pieces.push(punctuation(", ")) }93 pieces = pieces.push(Piece{style: NameStyle, text: property.name}).push(punctuation(": ")).concat(value(&property.value, &None))94 first = false95 }96 pieces.push(punctuation(if shape.properties.isEmpty() { "\}" } else { " \}" }))97}9899100fn punctuation(text: Str) -> Piece { Piece{style: Tertiary, text: text} }101102103export fn json(held: &Log.Value) -> Piece {104 let style = match held {105 case Log.Scalar(atom) => scalar(&atom, &None).style106 case _ => Tertiary107 }108 Piece{style: style, text: Json.value(held, &Some(TYPE_TAG))}109}110111112113114export fn message(template: &Log.Template, properties: &Array[Log.Property], format: &Option[Str]) -> Array[Piece] {115 let options = formatText(format)116 let literal = options.contains("l")117 let asJson = options.contains("j")118 var pieces: Array[Piece] = []119 for token in template.tokens {120 match token {121 case Log.Literal(text) => { pieces = pieces.push(Piece{style: Plain, text: text}) }122 case Log.Placeholder(placeholder) => { pieces = pieces.concat(hole(&placeholder, properties, literal, asJson)) }123 }124 }125 pieces126}127128129export fn hole(placeholder: &Log.Hole, properties: &Array[Log.Property], literal: Bool, asJson: Bool) -> Array[Piece] {130 let found = match Properties.find(properties, placeholder.name) {131 case Some(held) => held132 case None => { return [Piece{style: Invalid, text: placeholder.raw}] }133 }134 let rendered = match found {135 case Log.Scalar(Log.Text(text)) => if literal { [Piece{style: StringStyle, text: text}] } else { value(&found, &placeholder.format) }136 case _ => if asJson && placeholder.format == None { [json(&found)] } else { value(&found, &placeholder.format) }137 }138 aligned(&rendered, &placeholder.alignment)139}140141142export fn aligned(pieces: &Array[Piece], alignment: &Option[Log.Alignment]) -> Array[Piece] {143 let text = plain(pieces)144 let padding = Padding.padded(text, alignment).length() - text.length()145 if padding <= 0 { return *pieces }146 let spaces = Piece{style: Plain, text: " ".repeat(padding)}147 match alignment {148 case Some(shape) => if shape.left { pieces.push(spaces) } else { [spaces].concat(*pieces) }149 case None => *pieces150 }151}152153154155export fn failure(problem: &Log.Failure) -> Str {156 var lines = [if problem.message.isEmpty() { problem.kind } else { problem.kind + ": " + problem.message }]157 for frame in problem.trace { lines = lines.push(" at " + frame) }158 for cause in problem.causes { lines = lines.push(" ---> " + failure(&cause)) }159 lines.join("\n")160}161