diff --git a/JavaScript/loonParser.js b/JavaScript/loonParser.js index f725fff..116b993 100644 --- a/JavaScript/loonParser.js +++ b/JavaScript/loonParser.js @@ -1,218 +1,201 @@ const fs = require("fs"); -const path = require("path"); -function inferValueType(value, labels) { - value = value.trim(); - if (value.startsWith("[") && value.endsWith("]") && labels) { - return resolveReference(value, labels); - } - - if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { - return value.slice(1, -1); - } else if (value.toLowerCase() === "true") { - return true; - } else if (value.toLowerCase() === "false") { - return false; - } else { - const num = Number(value); - return isNaN(num) ? value : num; - } +function inferValueType(value, labels = {}, hiddenLabels = {}) { + value = value.trim(); + if (value.startsWith("[") && value.endsWith("]")) { + return resolveReference(value, labels, hiddenLabels); + } + if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) { + return value.slice(1, -1); + } else if (value.toLowerCase() === "true") { + return true; + } else if (value.toLowerCase() === "false") { + return false; + } else { + const num = Number(value); + return isNaN(num) ? value : num; + } } -function resolveReference(value, labels) { - const ref = value.slice(1, -1).trim(); - - if (ref.includes(".")) { - const [scope, identity] = ref.split(".", 2); - const data = scope.includes(":") ? - labels[scope.split(":")[0]][scope.split(":")[1]] : - labels[scope]; - return data?.[identity]; - } else if (ref.includes(":")) { - const [lbl, sp] = ref.split(":", 2); - return labels[lbl][sp]; - } else { - return labels[ref]; - } +function resolveReference(value, labels, hiddenLabels) { + const ref = value.slice(1, -1).trim(); + const allLabels = { + ...labels, + ...hiddenLabels + }; + + if (ref.includes(".")) { + const [scope, identity] = ref.split(".", 2); + const data = scope.includes(":") ? allLabels[scope.split(":")[0]][scope.split(":")[1]] : allLabels[scope]; + return data[identity]; + } else if (ref.includes(":")) { + const [lbl, sp] = ref.split(":", 2); + return allLabels[lbl][sp]; + } else { + return allLabels[ref]; + } } -function parseLoonFile(filename, labels = {}, spaces = {}) { - if (!filename.endsWith(".loon")) { - console.error("ERROR: file must be a .loon file"); - process.exit(1); - } - - const lines = fs.readFileSync(filename, "utf-8") - .split("\n") - .map(line => line.trim()) - .filter(line => line && !line.startsWith("<")); - - let currentLabel = null; - let currentSpace = null; - const labelStack = {}; - const spaceStack = {}; - let insertInSpace = false; - - for (const line of lines) { - if (line.startsWith("(") && line.endsWith(")")) { - currentLabel = line.slice(1, -1); - labelStack[currentLabel] = []; - currentSpace = null; - insertInSpace = false; - - } else if (line.startsWith(":")) { - currentSpace = line.slice(1); - spaceStack[currentSpace] = null; - insertInSpace = true; - - } else if (line === "end:") { - const result = spaceStack[currentSpace]; - labelStack[currentLabel].push([currentSpace, result]); - spaces[currentSpace] = result; - currentSpace = null; - insertInSpace = false; - - } else if (line === "end") { - const result = {}; - for (const item of labelStack[currentLabel]) { - if (Array.isArray(item)) { - const [key, val] = item; - result[key] = val; - } else if (typeof item === "object") { - Object.assign(result, item); - } else if (typeof item === "string") { - result[item] = null; - } - } - labels[currentLabel] = result; - currentLabel = null; - - } else if (line.includes("=")) { - const [k, v] = line.split("=").map(s => s.trim()); - const val = inferValueType(v, labels); - if (insertInSpace) { - let blk = spaceStack[currentSpace]; - if (blk === null) { - blk = {}; - spaceStack[currentSpace] = blk; - } else if (Array.isArray(blk)) { - throw new Error(`Cannot mix key-value with list in space '${currentSpace}'`); - } - blk[k] = val; - } else { - labelStack[currentLabel].push({ - [k]: val - }); - } - - } else if (line.startsWith("@")) { - const fileName = line.slice(1); - if (!fileName.endsWith(".loon")) { - console.error("ERROR: file must be a .loon file"); - process.exit(1); - } - const tempLabels = {}; - const parsedImport = parseLoonFile(fileName, tempLabels, spaces); - if (currentLabel === null) { - Object.assign(labels, parsedImport); - } else if (insertInSpace) { - let blk = spaceStack[currentSpace]; - if (blk === null) { - blk = []; - spaceStack[currentSpace] = blk; - } else if (!Array.isArray(blk)) { - throw new Error(`Cannot mix structured injection with key-value in space '${currentSpace}'`); - } - blk.push({ - [currentLabel]: parsedImport - }); - } else { - labelStack[currentLabel].push(parsedImport); - } - - } else if (!line.startsWith("->")) { - const val = inferValueType(line, labels); - let blk = spaceStack[currentSpace]; - if (insertInSpace) { - if (blk === null) { - blk = []; - spaceStack[currentSpace] = blk; - } else if (!Array.isArray(blk)) { - blk = Object.entries(blk).map(([k, v]) => ({ - [k]: v - })); - spaceStack[currentSpace] = blk; - } - blk.push(val); - } else { - labelStack[currentLabel].push(val); - } - - } else if (line.startsWith("->")) { - let raw = line.slice(2).trim(); - let isValueOnly = false; - - if (raw.endsWith("&")) { - isValueOnly = true; - raw = raw.slice(0, -1).trim(); - } - - let injected; - if (raw.includes(".")) { - const [scope, identity] = raw.split(".", 2); - const data = scope.includes(":") ? - labels[scope.split(":")[0]][scope.split(":")[1]] : - labels[scope]; - const val = data?.[identity]; - injected = isValueOnly ? val : { - [identity]: val - }; - - } else if (raw.includes(":")) { - const [lbl, sp] = raw.split(":", 2); - const data = labels[lbl][sp]; - injected = isValueOnly ? data : { - [sp]: data - }; - - } else { - const data = labels[raw]; - injected = isValueOnly ? data : { - [raw]: data - }; - } - - if (insertInSpace) { - let blk = spaceStack[currentSpace]; - if (isValueOnly) { - if (blk === null) { - blk = []; - spaceStack[currentSpace] = blk; - } else if (!Array.isArray(blk)) { - blk = Object.entries(blk).map(([k, v]) => ({ - [k]: v - })); - spaceStack[currentSpace] = blk; - } - blk.push(injected); - } else { - if (blk === null) { - blk = {}; - spaceStack[currentSpace] = blk; - } else if (Array.isArray(blk)) { - throw new Error(`Cannot mix structured injection with list in space '${currentSpace}'`); - } - Object.assign(blk, injected); - } - } else { - labelStack[currentLabel].push(injected); - } - } - } - - return labels; +function parseLoonFile(filename, labels = {}, spaces = {}, hiddenLabels = {}, labelHiddenMap = {}) { + if (!filename.endsWith(".loon")) { + console.error("ERROR: file must be a .loon file"); + process.exit(); + } + + const raw = fs.readFileSync(filename, "utf-8"); + const code = raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("<")); + + let currentLabel = null; + let currentSpace = null; + let insertInSpace = false; + const labelStack = {}; + const spaceStack = {}; + + for (const line of code) { + if ((line.startsWith("%(") || line.startsWith("(")) && line.endsWith(")")) { + const isHidden = line.startsWith("%("); + const labelName = isHidden ? line.slice(2, -1) : line.slice(1, -1); + currentLabel = labelName; + labelStack[currentLabel] = []; + labelHiddenMap[currentLabel] = isHidden; + currentSpace = null; + insertInSpace = false; + + } else if (line.startsWith(":")) { + currentSpace = line.slice(1); + spaceStack[currentSpace] = null; + insertInSpace = true; + } else if (line === "end:") { + const result = spaceStack[currentSpace]; + labelStack[currentLabel].push([currentSpace, result]); + spaces[currentSpace] = result; + insertInSpace = false; + currentSpace = null; + } else if (line === "end") { + const result = {}; + for (const item of labelStack[currentLabel]) { + if (Array.isArray(item)) { + const [key, val] = item; + result[key] = val; + } else if (typeof item === "object") { + Object.assign(result, item); + } else if (typeof item === "string") { + result[item] = null; + } + } + if (labelHiddenMap[currentLabel]) { + hiddenLabels[currentLabel] = result; + } else { + labels[currentLabel] = result; + } + currentLabel = null; + } else if (line.includes("=")) { + const [k, v] = line.split("=", 2).map((s) => s.trim()); + const val = inferValueType(v, labels, hiddenLabels); + let blk = spaceStack[currentSpace]; + if (insertInSpace) { + if (blk === null) { + blk = {}; + spaceStack[currentSpace] = blk; + } else if (Array.isArray(blk)) { + throw new Error(`Cannot mix key-value with list in space '${currentSpace}'`); + } + blk[k] = val; + } else { + labelStack[currentLabel].push({ + [k]: val + }); + } + } else if (line.startsWith("@")) { + const fileName = line.slice(1); + if (!fileName.endsWith(".loon")) { + console.error("ERROR: file must be a .loon file"); + process.exit(); + } + const tempLabels = {}; + const tempHidden = {}; + const tempHiddenMap = {}; + parseLoonFile(fileName, tempLabels, spaces, tempHidden, tempHiddenMap); + Object.assign(labels, tempLabels); + Object.assign(hiddenLabels, tempHidden); + Object.assign(labelHiddenMap, tempHiddenMap); + } else if (!line.startsWith("->")) { + const val = inferValueType(line, labels, hiddenLabels); + let blk = spaceStack[currentSpace]; + if (insertInSpace) { + if (blk === null) { + blk = []; + spaceStack[currentSpace] = blk; + } else if (!Array.isArray(blk)) { + blk = Object.entries(blk).map(([k, v]) => ({ + [k]: v + })); + spaceStack[currentSpace] = blk; + } + blk.push(val); + } else { + labelStack[currentLabel].push(val); + } + } else if (line.startsWith("->")) { + let raw = line.slice(2).trim(); + const isValueOnly = raw.endsWith("&"); + if (isValueOnly) raw = raw.slice(0, -1).trim(); + const allLabels = { + ...labels, + ...hiddenLabels + }; + let injected; + if (raw.includes(".")) { + const [scope, identity] = raw.split(".", 2); + const data = scope.includes(":") ? allLabels[scope.split(":")[0]][scope.split(":")[1]] : allLabels[scope]; + const val = data[identity]; + injected = isValueOnly ? val : { + [identity]: val + }; + } else if (raw.includes(":")) { + const [lbl, sp] = raw.split(":", 2); + const data = allLabels[lbl][sp]; + injected = isValueOnly ? data : { + [sp]: data + }; + } else { + const data = allLabels[raw]; + injected = isValueOnly ? data : { + [raw]: data + }; + } + let blk = spaceStack[currentSpace]; + if (insertInSpace) { + if (isValueOnly) { + if (blk === null) { + blk = []; + spaceStack[currentSpace] = blk; + } else if (!Array.isArray(blk)) { + blk = Object.entries(blk).map(([k, v]) => ({ + [k]: v + })); + spaceStack[currentSpace] = blk; + } + blk.push(injected); + } else { + if (blk === null) { + blk = {}; + spaceStack[currentSpace] = blk; + } else if (Array.isArray(blk)) { + throw new Error(`Cannot mix structured injection with list in space '${currentSpace}'`); + } + Object.assign(blk, injected); + } + } else { + labelStack[currentLabel].push(injected); + } + } + + } + + return labels; } module.exports = { - parseLoonFile + parseLoonFile }; diff --git a/Python/loon/parser.py b/Python/loon/parser.py index 056ced2..93ae7d7 100644 --- a/Python/loon/parser.py +++ b/Python/loon/parser.py @@ -1,178 +1,188 @@ -import json -import re - -def infer_value_type(value: str, labels=None): - value = value.strip() - if value.startswith("[") and value.endswith("]") and labels: - return resolve_reference(value, labels) - - if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")): - return value[1:-1] - elif value.lower() == "true": - return True - elif value.lower() == "false": - return False - else: - try: - return float(value) if '.' in value else int(value) - except ValueError: - return value - -def resolve_reference(value, labels): - ref = value[1:-1].strip() - if "." in ref: - scope, identity = ref.split(".", 1) - if ":" in scope: - lbl, sp = scope.split(":", 1) - data = labels[lbl][sp] - else: - data = labels[scope] - return data.get(identity) - elif ":" in ref: - lbl, sp = ref.split(":", 1) - return labels[lbl][sp] - else: - return labels[ref] - -def parse_loon_file(filename, labels=None, spaces=None): - if labels is None: - labels = {} - if spaces is None: - spaces = {} - with open(filename, "r") as file: - if not filename.endswith(".loon"): - print("ERROR: file must be a .loon file") - exit() - code = [line.strip() for line in file if line.strip() and not line.strip().startswith("<")] - current_label = None - current_space = None - label_stack = {} - space_stack = {} - insert_in_space = False - - for line in code: - if line.startswith("(") and line.endswith(")"): - current_label = line[1:-1] - label_stack[current_label] = [] - current_space = None - insert_in_space = False - - elif line.startswith(":"): - current_space = line[1:] - space_stack[current_space] = None - insert_in_space = True - - elif line == "end:": - result = space_stack[current_space] - label_stack[current_label].append((current_space, result)) - spaces[current_space] = result - insert_in_space = False - current_space = None - - elif line == "end": - result = {} - for item in label_stack[current_label]: - if isinstance(item, tuple): - key, val = item - result[key] = val - elif isinstance(item, dict): - result.update(item) - elif isinstance(item, str): - result[item] = None - labels[current_label] = result - current_label = None - - elif "=" in line: - k, v = map(str.strip, line.split("=", 1)) - val = infer_value_type(v, labels) - if insert_in_space: - blk = space_stack[current_space] - if blk is None: - blk = {} - space_stack[current_space] = blk - elif isinstance(blk, list): - raise Exception(f"Cannot mix key-value with list in space '{current_space}'") - blk[k] = val - else: - label_stack[current_label].append({k: val}) - elif line.startswith("@"): - file_name = line[1:] - if not file_name.endswith(".loon"): - print("ERROR: file must be a .loon file") - exit() - temp_labels = {} - parsed_import_file = parse_loon_file(file_name, temp_labels, spaces) - if current_label is None: - labels.update(parsed_import_file) - else: - if insert_in_space: - blk = space_stack[current_space] - if blk is None: - blk = [] - space_stack[current_space] = blk - elif isinstance(blk, list): - raise Exception(f"Cannot mix key-value with list in space '{current_space}'") - blk.append({current_label: parsed_import_file}) - else: - label_stack[current_label].append(parsed_import_file) - continue - elif not line.startswith("->"): - val = infer_value_type(line, labels) - if insert_in_space: - blk = space_stack[current_space] - if blk is None: - blk = [] - space_stack[current_space] = blk - elif isinstance(blk, dict): - blk = [{k: v} for k, v in blk.items()] - space_stack[current_space] = blk - blk.append(val) - else: - label_stack[current_label].append(val) - - elif line.startswith("->"): - raw = line[2:].strip() - is_value_only = raw.endswith("&") - if is_value_only: - raw = raw[:-1].strip() - - if "." in raw: - scope, identity = raw.split(".", 1) - if ":" in scope: - lbl, sp = scope.split(":", 1) - data = labels[lbl][sp] - else: - data = labels[scope] - val = data.get(identity) - injected = val if is_value_only else {identity: val} - - elif ":" in raw: - lbl, sp = raw.split(":", 1) - data = labels[lbl][sp] - injected = data if is_value_only else {sp: data} - - else: - data = labels[raw] - injected = data if is_value_only else {raw: data} - - if insert_in_space: - blk = space_stack[current_space] - if is_value_only: - if blk is None: - blk = [] - space_stack[current_space] = blk - elif isinstance(blk, dict): - blk = [{k: v} for k, v in blk.items()] - space_stack[current_space] = blk - blk.append(injected) - else: - if blk is None: - blk = {} - space_stack[current_space] = blk - elif isinstance(blk, list): - raise Exception(f"Cannot mix structured injection with list in space '{current_space}'") - blk.update(injected) - else: - label_stack[current_label].append(injected) - +import json +import re + +def infer_value_type(value: str, labels=None, hidden_labels=None): + value = value.strip() + if value.startswith("[") and value.endswith("]") and labels: + return resolve_reference(value, labels, hidden_labels) + + if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")): + return value[1:-1] + elif value.lower() == "true": + return True + elif value.lower() == "false": + return False + else: + try: + return float(value) if '.' in value else int(value) + except ValueError: + return value + +def resolve_reference(value, labels, hidden_labels): + ref = value[1:-1].strip() + all_labels = {**labels, **hidden_labels} + + if "." in ref: + scope, identity = ref.split(".", 1) + if ":" in scope: + lbl, sp = scope.split(":", 1) + data = all_labels[lbl][sp] + else: + data = all_labels[scope] + return data.get(identity) + elif ":" in ref: + lbl, sp = ref.split(":", 1) + return all_labels[lbl][sp] + else: + return all_labels[ref] + +def parse_loon_file(filename, labels=None, spaces=None, hidden_labels=None, label_hidden_map=None): + if labels is None: + labels = {} + if hidden_labels is None: + hidden_labels = {} + if label_hidden_map is None: + label_hidden_map = {} + if spaces is None: + spaces = {} + + with open(filename, "r") as file: + if not filename.endswith(".loon"): + print("ERROR: file must be a .loon file") + exit() + code = [line.strip() for line in file if line.strip() and not line.strip().startswith("<")] + + current_label = None + current_space = None + label_stack = {} + space_stack = {} + insert_in_space = False + + for line in code: + if (line.startswith("%(") or line.startswith("(")) and line.endswith(")"): + is_hidden = line.startswith("%(") + label_name = line[2:-1] if is_hidden else line[1:-1] + current_label = label_name + label_stack[current_label] = [] + label_hidden_map[current_label] = is_hidden + current_space = None + insert_in_space = False + + elif line.startswith(":"): + current_space = line[1:] + space_stack[current_space] = None + insert_in_space = True + + elif line == "end:": + result = space_stack[current_space] + label_stack[current_label].append((current_space, result)) + spaces[current_space] = result + insert_in_space = False + current_space = None + + elif line == "end": + result = {} + for item in label_stack[current_label]: + if isinstance(item, tuple): + key, val = item + result[key] = val + elif isinstance(item, dict): + result.update(item) + elif isinstance(item, str): + result[item] = None + if label_hidden_map.get(current_label): + hidden_labels[current_label] = result + else: + labels[current_label] = result + current_label = None + + elif "=" in line: + k, v = map(str.strip, line.split("=", 1)) + val = infer_value_type(v, labels, hidden_labels) + if insert_in_space: + blk = space_stack[current_space] + if blk is None: + blk = {} + space_stack[current_space] = blk + elif isinstance(blk, list): + raise Exception(f"Cannot mix key-value with list in space '{current_space}'") + blk[k] = val + else: + label_stack[current_label].append({k: val}) + + elif line.startswith("@"): + file_name = line[1:] + if not file_name.endswith(".loon"): + print("ERROR: file must be a .loon file") + exit() + temp_labels = {} + temp_hidden = {} + temp_hidden_map = {} + parse_loon_file(file_name, temp_labels, spaces, temp_hidden, temp_hidden_map) + labels.update(temp_labels) + hidden_labels.update(temp_hidden) + label_hidden_map.update(temp_hidden_map) + continue + + elif not line.startswith("->"): + val = infer_value_type(line, labels, hidden_labels) + if insert_in_space: + blk = space_stack[current_space] + if blk is None: + blk = [] + space_stack[current_space] = blk + elif isinstance(blk, dict): + blk = [{k: v} for k, v in blk.items()] + space_stack[current_space] = blk + blk.append(val) + else: + label_stack[current_label].append(val) + + elif line.startswith("->"): + raw = line[2:].strip() + is_value_only = raw.endswith("&") + if is_value_only: + raw = raw[:-1].strip() + + all_labels = {**labels, **hidden_labels} + + if "." in raw: + scope, identity = raw.split(".", 1) + if ":" in scope: + lbl, sp = scope.split(":", 1) + data = all_labels[lbl][sp] + else: + data = all_labels[scope] + val = data.get(identity) + injected = val if is_value_only else {identity: val} + + elif ":" in raw: + lbl, sp = raw.split(":", 1) + data = all_labels[lbl][sp] + injected = data if is_value_only else {sp: data} + + else: + data = all_labels[raw] + injected = data if is_value_only else {raw: data} + + if insert_in_space: + blk = space_stack[current_space] + if is_value_only: + if blk is None: + blk = [] + space_stack[current_space] = blk + elif isinstance(blk, dict): + blk = [{k: v} for k, v in blk.items()] + space_stack[current_space] = blk + blk.append(injected) + else: + if blk is None: + blk = {} + space_stack[current_space] = blk + elif isinstance(blk, list): + raise Exception(f"Cannot mix structured injection with list in space '{current_space}'") + blk.update(injected) + else: + label_stack[current_label].append(injected) + return labels