///| Expand only variables defined earlier, as pkg-config does.
///|
/// The map can include caller-provided builtins such as pcfiledir.
pub fn expand(
value : String,
variables : Map[String, String],
location~ : Location,
) -> Result[String, Diagnostic] {
let chars = value.to_array()
let out = StringBuilder()
let mut expanded_length = 0
let mut i = 0
while i < chars.length() {
if chars[i] == '$' && i + 1 < chars.length() && chars[i + 1] == '$' {
out.write_char('$')
expanded_length = expanded_length + 1
i = i + 2
} else if chars[i] == '$' && i + 1 < chars.length() && chars[i + 1] == '{' {
let start = i
i = i + 2
let name_start = i
while i < chars.length() && chars[i] != '}' {
i = i + 1
}
let at = { ..location, column: location.column + start, }
if i == chars.length() {
return Err(diag("PC003", "Unclosed variable reference", at))
}
let name = chars_text(chars, name_start, i)
match variables.get(name) {
Some(v) => {
expanded_length = expanded_length + v.length()
if expanded_length > 1048576 {
return Err(diag("PC009", "Expanded value exceeds 1 MiB", location))
}
out.write_string(v)
}
None => return Err(diag("PC004", "Undefined variable: " + name, at))
}
i = i + 1
} else {
out.write_char(chars[i])
expanded_length = expanded_length +
(if chars[i].to_int() > 65535 { 2 } else { 1 })
i = i + 1
}
if expanded_length > 1048576 {
return Err(diag("PC009", "Expanded value exceeds 1 MiB", location))
}
}
Ok(out.to_string())
}
///|
fn valid_key(name : String) -> Bool {
!name.is_empty() &&
name
.iter()
.all(c => {
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_' ||
c == '.' ||
c == '-'
})
}
///| Parse .pc syntax with ordered substitution, comments and continuations.
///|
/// Deliberately strict about duplicate definitions and undefined variables.
pub fn parse(
content : String,
source? : String = "input.pc",
pcfiledir? : String = ".",
overrides? : Map[String, String] = Map([]),
) -> Document {
let entries : Array[Entry] = []
let diagnostics : Array[Diagnostic] = []
let variables : Map[String, String] = { "pcfiledir": pcfiledir }
for name, value in overrides {
if name != "pcfiledir" {
variables[name] = value
}
}
let defined_variables : Map[String, Bool] = Map([])
let fields : Map[String, Bool] = Map([])
let chars = content.to_array()
let line = StringBuilder()
let mut physical_line = 1
let mut start_line = 1
let mut i = if chars.length() > 0 && chars[0] == '\u{FEFF}' { 1 } else { 0 }
while i < chars.length() {
let c = chars[i]
if c == '\\' && i + 1 < chars.length() {
let next = chars[i + 1]
if next == '\n' ||
(next == '\r' && i + 2 < chars.length() && chars[i + 2] == '\n') {
physical_line = physical_line + 1
i = i + (if next == '\r' { 3 } else { 2 })
continue
}
// Escaped hashes are literal, other escapes belong to the fragment parser.
if next == '#' {
line.write_char('#')
} else {
line.write_char(c)
line.write_char(next)
}
i = i + 2
continue
}
if c == '#' {
while i < chars.length() && chars[i] != '\n' {
i = i + 1
}
continue
}
if c == '\n' {
parse_line(
line.to_string(),
source,
start_line,
variables,
overrides,
defined_variables,
fields,
entries,
diagnostics,
)
line.reset()
physical_line = physical_line + 1
start_line = physical_line
} else {
line.write_char(c)
}
i = i + 1
}
parse_line(
line.to_string(),
source,
start_line,
variables,
overrides,
defined_variables,
fields,
entries,
diagnostics,
)
{ entries, diagnostics, }
}
///|
fn parse_line(
text : String,
source : String,
line_number : Int,
variables : Map[String, String],
overrides : Map[String, String],
defined_variables : Map[String, Bool],
fields : Map[String, Bool],
entries : Array[Entry],
diagnostics : Array[Diagnostic],
) -> Unit {
if text.is_blank() {
return
}
let chars = text.to_array()
let mut start = 0
while start < chars.length() && whitespace(chars[start]) {
start = start + 1
}
let location = { source, line: line_number, column: start + 1, }
let mut split = start
while split < chars.length() && chars[split] != '=' && chars[split] != ':' {
split = split + 1
}
if split == chars.length() {
diagnostics.push(
diag("PC001", "Expected variable=value or Field: value", location),
)
return
}
let name = chars_text(chars, start, split).trim().to_owned()
if !valid_key(name) {
diagnostics.push(diag("PC002", "Invalid key: " + name, location))
return
}
let kind = if chars[split] == '=' { Variable } else { Field }
if (
kind == Variable &&
(name == "pcfiledir" || defined_variables.contains(name))
) ||
(kind == Field && fields.contains(name)) {
diagnostics.push(
diag("PC005", "Duplicate or reserved definition: " + name, location),
)
return
}
let mut value_start = split + 1
while value_start < chars.length() && whitespace(chars[value_start]) {
value_start = value_start + 1
}
let raw = chars_text(chars, value_start, chars.length()).trim_end().to_owned()
let value_location = { ..location, column: value_start + 1, }
match expand(raw, variables, location=value_location) {
Err(d) => diagnostics.push(d)
Ok(expanded_value) => {
let value = match kind {
Variable => overrides.get(name).unwrap_or(expanded_value)
Field => expanded_value
}
entries.push({ kind, name, raw, value, location, value_location, })
match kind {
Variable => {
variables[name] = value
defined_variables[name] = true
}
Field => fields[name] = true
}
}
}
}