mq-lang 0.5.21

Core language implementation for mq query language
Documentation

# Based on YAML 1.2 specification

def is_digit(char):
  test(char, "^[0-9]+$")
end

def _parse_yaml_boolean(str):
  if (str == "true" || str == "True" || str == "TRUE" || str == "yes" || str == "Yes" || str == "YES"):
    true
  elif (str == "false" || str == "False" || str == "FALSE" || str == "no" || str == "No" || str == "NO"):
    false
  else:
    None
end

def _escape_string(str):
  replace(str, "\\", "\\\\") | replace("\"", "\\\"") | replace("\n", "\\n") | replace("\t", "\\t")
end

def _parse_yaml_value(str):
  let trimmed = trim(str)
  | if (is_empty(trimmed)):
      None
    # string
    elif (starts_with(trimmed, "\"") || starts_with(trimmed, "'")):
      if (starts_with(trimmed, "\"") && ends_with(trimmed, "\"")):
        _escape_string(trimmed[1:-1])
      elif (starts_with(trimmed, "'") && ends_with(trimmed, "'")):
        replace(trimmed[1:len(trimmed) - 1], "''", "'")
      else:
        trimmed
    # boolean
    elif (_parse_yaml_boolean(trimmed) != None):
      _parse_yaml_boolean(trimmed)
    # number
    elif (is_digit(trimmed) || first(trimmed) == "-" || first(trimmed) == "+"):
      if (contains(trimmed, ".")): to_number(trimmed) else: to_number(trimmed)
    # null
    elif (trimmed == "null" || trimmed == "Null" || trimmed == "NULL" || trimmed == "~" || trimmed == ""):
      None
    else:
      trimmed
end

def _parse_yaml_array_line(line):
  let trimed_line = trim(line)
  | if (starts_with(trimed_line, "- ")):
      _parse_yaml_value(trimed_line[2:len(trim(line))])
    else:
      None
end

def _parse_yaml_key_value(line):
  let colon_pos = index(line, ":")
  | let key = trim(line[0:colon_pos])
  | let value_part = if (colon_pos + 1 < len(line)): line[colon_pos + 1:len(line)] else: ""
  | let value = _parse_yaml_value(value_part)
  | let result = [key, value]
  | if (colon_pos != -1):
      result
    else:
      None
end

def _get_indent_level(line):
  var i = 0
  | let count = while (i < len(line) && line[i] == " "):
      i += 1
    end
  | i / 2 | floor()
end

def _is_yaml_array_item(line):
  starts_with(trim(line), "- ")
end

def _is_yaml_key_value(line):
  contains(line, ":") && !starts_with(trim(line), "- ")
end

def parse_yaml_content(input):
  def _handle_object_empty_or_comment(result, current_index):
    [result, current_index + 1]
  end

  def _handle_object_indent_break(result, current_index):
    [result, current_index, true]
  end

  def _handle_nested_object_item(result, key, nested_result):
    [set(result, key, nested_result[0]), nested_result[1]]
  end

  def _process_key_value_pair(result, lines, current_index, target_indent, kv_pair):
    let key = kv_pair[0]
    | let value = kv_pair[1]
    | let next_index = current_index + 1
    | let next_line = lines[next_index]
    | if (next_index < len(lines) && _get_indent_level(lines[next_index]) > target_indent):
        if (trim(to_string(value)) == "|"):
          _handle_nested_object_item(result, key, parse_yaml_lines_with_options(lines, next_index, _get_indent_level(lines[next_index]), true))
        elif (!_is_yaml_array_item(to_string(next_line)) && !_is_yaml_key_value(to_string(next_line))):
          _handle_nested_object_item(result, key, parse_yaml_lines_with_options(lines, next_index, _get_indent_level(lines[next_index]), true))
        else:
          _handle_nested_object_item(result, key, parse_yaml_lines(lines, next_index, _get_indent_level(lines[next_index])))
      else:
        [set(result, key, value), next_index]
  end

  def _handle_array_empty_or_comment(result, current_index):
    [result, current_index + 1]
  end

  def _handle_array_indent_break(result, current_index):
    [result, current_index, true]
  end

  def _handle_nested_array_item(result, lines, next_index, nested_result):
    [result + [nested_result[0]], nested_result[1]]
  end

  def _parse_yaml_object(lines, start_index, target_indent):
    var obj = {}
    | var current_index = start_index
    | while (current_index < len(lines)):
        let line = lines[current_index]
        | let trimmed_line = trim(line)
        | let result = if (is_empty(trimmed_line) || starts_with(trimmed_line, "#")):
                  _handle_object_empty_or_comment(obj, current_index) + [false]
                elif (_get_indent_level(line) < target_indent):
                  _handle_object_indent_break(obj, current_index)
                elif (_get_indent_level(line) == target_indent && _is_yaml_key_value(line)):
                  do
                    let kv_pair = _parse_yaml_key_value(lines[current_index])
                    | if (kv_pair != None):
                        _process_key_value_pair(obj, lines, current_index, target_indent, kv_pair) + [false]
                      else:
                        [obj, current_index + 1, false]
                  end
                elif (line == "---"):
                  [obj, current_index + 1, false]
                else:
                  _handle_object_indent_break(obj, current_index)

        | obj = result[0]
        | current_index = result[1]
        | if (result[2]): break else: result
      end
  end

  def _parse_yaml_array(lines, start_index, target_indent):
    var arr = []
    | var current_index = start_index
    | while (current_index < len(lines)):
        let line = lines[current_index]
        | let trimmed_line = trim(line)
        | let result = if (is_empty(trimmed_line) || starts_with(trimmed_line, "#")):
                  _handle_array_empty_or_comment(arr, current_index) + [false]
                elif (_get_indent_level(line) < target_indent):
                  _handle_array_indent_break(arr, current_index)
                elif (_get_indent_level(line) == target_indent && _is_yaml_array_item(line)):
                  do
                    let item_value = _parse_yaml_array_line(lines[current_index])
                    | let next_index = current_index + 1
                    | if (next_index < len(lines) && _get_indent_level(lines[next_index]) > target_indent):
                        _handle_nested_array_item(arr, lines, next_index, parse_yaml_lines(lines, next_index, _get_indent_level(lines[next_index]))) + [false]
                      else:
                        [arr + [item_value], next_index, false]
                  end
                elif (line == "---"):
                  [arr + line, current_index + 1, false]
                else:
                  _handle_array_indent_break(arr, current_index)

        | arr = result[0]
        | current_index = result[1]
        | if (result[2]): break else: result
      end
  end

  def _parse_yaml_multi_lines(lines, start_index, target_indent):
    var multi_line = ""
    | var current_index = start_index
    | while (current_index < len(lines)):
        let line = lines[current_index]
        | let trimmed_line = trim(line)
        | let result = if (is_empty(trimmed_line) || starts_with(trimmed_line, "#")):
                  [multi_line, current_index + 1]
                elif (_get_indent_level(line) < target_indent):
                  break
                else:
                  [if (is_empty(multi_line)): trimmed_line else: s"${multi_line}
${trimmed_line}", current_index + 1]
        | multi_line = result[0]
        | current_index = result[1]
        | result
      end
  end

  def parse_yaml_lines_with_options(lines, current_index, current_indent, is_multi_line):
    def handle_empty_or_comment_line(lines, current_index, current_indent):
      parse_yaml_lines(lines, current_index + 1, current_indent)
    end

    def _handle_skip_line(lines, current_index, current_indent):
      parse_yaml_lines(lines, current_index + 1, current_indent)
    end

    | let line = lines[current_index]
    | let trimmed_line = trim(line)
    | if (current_index >= len(lines)):
        [{}, current_index]
      elif (is_empty(trimmed_line) || starts_with(trimmed_line, "#")):
        handle_empty_or_comment_line(lines, current_index, current_indent)
      elif (is_multi_line):
        _parse_yaml_multi_lines(lines, current_index, current_indent)
      elif (_get_indent_level(line) < current_indent):
        [{}, current_index]
      elif (_is_yaml_array_item(line)):
        _parse_yaml_array(lines, current_index, current_indent)
      elif (_is_yaml_key_value(line)):
        _parse_yaml_object(lines, current_index, current_indent)
      else:
        parse_yaml_lines(lines, current_index + 1, current_indent)
  end

  def parse_yaml_lines(lines, current_index, current_indent):
    parse_yaml_lines_with_options(lines, current_index, current_indent, false)
  end

  | let lines = split(input, "\n")
  | parse_yaml_lines(lines, 0, 0)
end

def _yaml_stringify_value(value, indent_level):
  let indent = join(map(range(0, indent_level * 2), fn(_): " ";), "")
  | if (is_string(value)):
      if (contains(value, "\n") || contains(value, "\"") || contains(value, "'")):
        "\"" + _escape_string(value) + "\""
      else:
        value
    elif (is_number(value)):
      to_string(value)
    elif (is_bool(value)):
      if (value): "true" else: "false"
    elif (is_none(value)):
      "null"
    elif (is_array(value)):
      if (is_empty(value)):
        "[]"
      else:
        join(map(value, fn(item): "\n" + indent + "- " + _yaml_stringify_value(item, indent_level + 1);), "")
    elif (is_dict(value)):
      if (is_empty(keys(value))):
        "{}"
      else:
        join(map(keys(value), fn(k): "\n" + indent + k + ": " + _yaml_stringify_value(value[k], indent_level + 1);), "")
    else:
      to_string(value)
end

# Parses a YAML string and returns the parsed data structure.
def yaml_parse(input):
  parse_yaml_content(to_string(input)) | first()
end

# Converts a data structure to a YAML string representation.
def yaml_stringify(data):
  if (is_array(data)):
    if (is_empty(data)):
      "[]"
    else:
      join(map(data, fn(item): "- " + _yaml_stringify_value(item, 1);), "\n")
  elif (is_dict(data)):
    if (is_empty(keys(data))):
      "{}"
    else:
      join(map(keys(data), fn(k): k + ": " + _yaml_stringify_value(data[k], 1);), "\n")
  else:
    _yaml_stringify_value(data, 0)
end

# Converts a YAML data structure to a Markdown table.
def yaml_to_markdown_table(data):
  if (is_array(data)):
    do
      let headers = keys(first(data))
      | let header_row = "| " + join(headers, " | ") + " |"
      | let separator_row = "| " + join(map(headers, fn(_): "---";), " | ") + " |"
      | let data_rows = map(data, fn(row):
              "| " + join(map(headers, fn(header): if (is_none(row[header])): "" else: replace(to_string(row[header]), "\n", "\\n");), " | ") + " |"
            end)
      | [header_row, separator_row] + data_rows
      | join("\n")
    end
  elif (is_dict(data)):
    do
      let headers = ["Key", "Value"]
      | let header_row = "| " + join(headers, " | ") + " |"
      | let separator_row = "| " + join(map(headers, fn(_): "---";), " | ") + " |"
      | let data_rows = map(keys(data), fn(key):
              "| " + key + " | " + (if (is_none(data[key])): "" else: replace(to_string(data[key]), "\n", "\\n")) + " |"
            end)
      | [header_row, separator_row] + data_rows
      | join("\n")
    end
  else:
    "| Value |\n| --- |\n| " + to_string(data) + " |"
end

# Converts a data structure to a JSON string representation.
def yaml_to_json(data):
  if (is_dict(data)):
    "{" + join(map(keys(data), fn(k): "\"" + k + "\": " + yaml_to_json(data[k]);), ", ") + "}"
  elif (is_array(data)):
    "[" + join(map(data, yaml_to_json), ", ") + "]"
  elif (is_string(data)):
    "\"" + _escape_string(data) + "\""
  elif (is_number(data)):
    to_string(data)
  elif (is_bool(data)):
    if (data): "true" else: "false"
  elif (is_none(data)):
    "null"
  else:
    "\"" + to_string(data) + "\""
end