
Parser.pudu
Pudu156 lines5.9 KB
1/** @Log.Domain.Parser.Module — message template text into tokens */2module PuduLangLog.Domain.Parser34import Std.Char as Char5import Std.List as List6import Std.Option as Option7import Std.Text as Text8import PuduLangLog as Log91011const LARGEST: Int = 214748364712131415export fn parse(text: Str) -> Log.Template {16 let characters = text.chars()17 var tokens: Array[Log.Token] = []18 if characters.isEmpty() { tokens = [Log.Literal("")] }19 var next = 020 while next < characters.length() {21 let (literal, afterLiteral) = literalAt(&characters, next)22 if afterLiteral > next { tokens = tokens.push(Log.Literal(literal)) }23 next = afterLiteral24 if next < characters.length() {25 let (token, afterHole) = holeAt(text, &characters, next)26 tokens = tokens.push(token)27 next = afterHole28 }29 }30 var holes: Array[Log.Hole] = []31 for token in tokens {32 if let Log.Placeholder(hole) = token { holes = holes.push(hole) }33 }34 Log.Template{text: text, tokens: tokens, holes: holes, binding: bindingOf(&holes)}35}363738export fn isValidName(name: Str) -> Bool { !name.trim().isEmpty() }394041fn bindingOf(holes: &Array[Log.Hole]) -> Log.Binding {42 if holes.isEmpty() { return Log.Unbound }43 for hole in holes {44 if hole.position == None { return Log.Named }45 }46 Log.Positional47}48495051fn literalAt(characters: &Array[Char], start: Int) -> (Str, Int) {52 var pieces: Array[Str] = []53 var index = start54 while index < characters.length() {55 let character = characters[index]56 let doubled = index + 1 < characters.length() && characters[index + 1] == character57 if character == '{' {58 if !doubled { return (pieces.join(""), index) }59 pieces = pieces.push("\{")60 index = index + 261 } else if character == '}' {62 pieces = pieces.push("\}")63 index = if doubled { index + 2 } else { index + 1 }64 } else {65 pieces = pieces.push(character.toText())66 index = index + 167 }68 }69 (pieces.join(""), index)70}71727374fn holeAt(text: Str, characters: &Array[Char], start: Int) -> (Log.Token, Int) {75 var close = start + 176 while close < characters.length() && characters[close] != '}' { close = close + 1 }77 if close == characters.length() { return (Log.Literal(text.slice(start, close)), close) }78 let raw = text.slice(start, close + 1)79 let token = match holeOf(text.slice(start + 1, close), raw) {80 case Some(hole) => Log.Placeholder(hole)81 case None => Log.Literal(raw)82 }83 (token, close + 1)84}858687fn holeOf(content: Str, raw: Str) -> Option[Log.Hole] {88 let (head, format, alignment) = split(content)89 let hint = match List.first(&head.chars()) {90 case Some('@') => Log.Destructure91 case Some('$') => Log.Stringify92 case _ => Log.Default93 }94 let name = if hint == Log.Default { head } else { head.drop(1) }95 let characters = name.chars()96 let first = List.first(&characters) ?97 let numbered = Char.isDigit(first)98 if numbered && !Text.allChars(name, |character: Char| Char.isDigit(character)) { return None }99 if !numbered && !isIdentifier(&characters) { return None }100 let position = if numbered { numberOf(&characters) } else { None }101 let aligned = Option.andThen(alignment, |text: Str| alignmentOf(text))102 if aligned == None && alignment != None { return None }103 Some(Log.Hole{name: name, raw: raw, format: format, alignment: aligned, hint: hint, position: position})104}105106107108109fn split(content: Str) -> (Str, Option[Str], Option[Str]) {110 let (head, formatPart) = Text.breakOn(content, ":")111 let (name, alignmentPart) = Text.breakOn(head, ",")112 let alignment = if alignmentPart.isEmpty() { None } else { Some(alignmentPart.drop(1)) }113 (name, formatOf(formatPart.drop(1)), alignment)114}115116117fn formatOf(text: Str) -> Option[Str] { if text.isEmpty() { None } else { Some(text) } }118119120fn alignmentOf(text: Str) -> Option[Log.Alignment] {121 let left = text.startsWith("-")122 let digits = if left { text.drop(1) } else { text }123 let width = numberOf(&digits.chars()) ?124 Some(Log.Alignment{left: left, width: width})125}126127128fn numberOf(characters: &Array[Char]) -> Option[Int] {129 if characters.isEmpty() { return None }130 var value = 0131 for character in characters {132 value = value * 10 + Char.toDigit(character) ?133 if value > LARGEST { return None }134 }135 Some(value)136}137138139fn isIdentifier(characters: &Array[Char]) -> Bool {140 var starting = true141 for character in characters {142 if starting {143 if !isLetter(character) && character != '_' { return false }144 starting = false145 } else if character == '.' {146 starting = true147 } else if !isLetter(character) && !Char.isDigit(character) && character != '_' {148 return false149 }150 }151 !starting152}153154155fn isLetter(character: Char) -> Bool { Char.isLetter(character) || !Char.isAscii(character) }156