Task 425: .MSC File Format

Task 425: .MSC File Format

1. List of all the properties of this file format intrinsic to its file system

The .MSC file format is a text-based format used by Mscgen for describing Message Sequence Charts. It is not binary and has no magic number or header intrinsic to a file system (e.g., no specific byte signature, as it is plain ASCII/UTF-8 text). The format is defined by its syntax, which includes global options, entity declarations with attributes, arc declarations with types and attributes, and comments. All properties are keyword-based and case-insensitive for keywords, but case-sensitive for values like labels. The intrinsic properties are the syntactic elements that define the structure and content, as follows:

Global Options (key-value pairs at the start of the msc { block, comma-separated):

  • hscale: Horizontal scale factor (numeric or "auto").
  • width: Absolute image width in pixels (numeric or "auto").
  • arcgradient: Pixel slope for arcs (numeric).
  • wordwraparcs: Boolean to enable word-wrapping for arc labels (true/false, on/off, 1/0).

Entity Attributes (in square brackets after entity names, comma-separated):

  • label: Text label for the entity (string, supports \n for multi-line).
  • linecolour or linecolor: Color for the entity's lifeline.
  • textcolour or textcolor: Color for the entity's label text.
  • textbgcolour or textbgcolor: Background color for the entity's label.
  • arclinecolour or arclinecolor: Default line color for arcs from this entity.
  • arctextcolour or arctextcolor: Default text color for arcs from this entity.
  • arctextbgcolour or arctextbgcolor: Default text background color for arcs from this entity.

Arc Types (symbols used between entities to define interactions):

  • -> or <-: Standard message (solid line arrow).
  • => or <=: Method call.
  • >> or <<: Method return.
  • =>> or <<=: Callback.
  • :> or <:: Emphasized message (double line).
  • -x or x-: Lost message.
  • ->* or *<-: Broadcast to all entities.
  • ...: Discontinuity (dashed horizontal line).
  • ---: Divider (horizontal line for comments or sections).
  • |||: Vertical spacer.
  • box: Rectangular box (for states or conditions).
  • rbox: Rounded rectangular box.
  • abox: Angular (diamond) box.
  • note: Note box.

Arc Attributes (in square brackets after arc declarations, comma-separated):

  • label: Text displayed on the arc (string, supports \n for multi-line).
  • URL: Hyperlink for the label (string, e.g., URL or \ref for references).
  • ID: Superscript identifier for the arc (string).
  • IDURL: Hyperlink for the ID (string).
  • arcskip: Vertical row offset at destination (numeric).
  • linecolour or linecolor: Arc line color.
  • textcolour or textcolor: Arc label text color.
  • textbgcolour or textbgcolor: Arc label background color (fills boxes for box types).

Color Values (used in color attributes, RGB #hex or predefined names):

  • Predefined: white, silver, gray, black, maroon, red, orange, yellow, olive, green, lime, aqua, teal, blue, navy, indigo, purple, violet, fuchsia.
  • Custom: #RRGGBB (e.g., #ff0000 for red).

Other Structural Properties:

  • msc { ... }: Required enclosing block for the entire description.
  • Comments: #, //, or /* ... */.
  • Entity declarations: Comma-separated list before arcs (e.g., a,b,c;).
  • Whitespace and newlines: Ignored by parser.

The format is human-readable text, with no file system-specific metadata beyond standard text file properties (e.g., encoding typically UTF-8, line endings CR/LF or LF).

3. Write a ghost blog embedded html javascript that allows a user to drag n drop a file of format .MSC and it will dump to screen all these properties

MSC File Parser

Drag and Drop .MSC File

Drop .MSC file here

4. Write a python class that can open any file of format .MSC and decode read and write and print to console all the properties from the above list

import re
import json

class MSCParser:
    def __init__(self):
        self.properties = {
            'globalOptions': {},
            'entities': [],
            'arcs': [],
            'colors': set(),
            'comments': []
        }

    def read(self, filepath):
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()
        self._parse(content)

    def _parse(self, content):
        # Global options
        options_match = re.search(r'msc\s*{\s*([^}]*);', content, re.IGNORECASE)
        if options_match:
            options = options_match.group(1).split(',')
            for opt in options:
                key_value = opt.split('=')
                if len(key_value) == 2:
                    key = key_value[0].strip().replace('"', '').replace("'", '')
                    value = key_value[1].strip().replace('"', '').replace("'", '')
                    self.properties['globalOptions'][key] = value

        # Entities
        entity_matches = re.findall(r'([a-z0-9_"]+[^;]*);', content, re.IGNORECASE)
        for ent in entity_matches:
            entity_name = ent.split('[')[0].strip()
            attr_match = re.search(r'\[(.*)\]', ent)
            attrs = self._parse_attributes(attr_match.group(1)) if attr_match else {}
            self.properties['entities'].append({'name': entity_name, 'attributes': attrs})
            self._extract_colors(attrs)

        # Arcs
        arc_lines = [line.strip() for line in content.split(';') if re.search(r'[-:=x>|<.*]', line)]
        for arc in arc_lines:
            parts = re.split(r'([-><:=x|.-]+)', arc)
            if len(parts) >= 3:
                from_ent = parts[0].strip()
                arc_type = parts[1].strip()
                to_ent = parts[2].split('[')[0].strip()
                attr_match = re.search(r'\[(.*)\]', arc)
                attrs = self._parse_attributes(attr_match.group(1)) if attr_match else {}
                self.properties['arcs'].append({'from': from_ent, 'type': arc_type, 'to': to_ent, 'attributes': attrs})
                self._extract_colors(attrs)

        # Comments
        for line in content.split('\n'):
            stripped = line.strip()
            if stripped.startswith('#') or stripped.startswith('//'):
                self.properties['comments'].append(stripped)

        self.properties['colors'] = list(self.properties['colors'])

    def _parse_attributes(self, attr_str):
        attrs = {}
        attr_parts = attr_str.split(',')
        for attr in attr_parts:
            key_value = attr.split('=')
            if len(key_value) == 2:
                key = key_value[0].strip().replace('"', '').replace("'", '')
                value = key_value[1].strip().replace('"', '').replace("'", '')
                attrs[key] = value
        return attrs

    def _extract_colors(self, attrs):
        color_keys = ['linecolour', 'linecolor', 'textcolour', 'textcolor', 'textbgcolour', 'textbgcolor', 'arclinecolour', 'arclinecolor', 'arctextcolour', 'arctextcolor', 'arctextbgcolour', 'arctextbgcolor']
        for key, value in attrs.items():
            if key in color_keys:
                self.properties['colors'].add(value)

    def print_properties(self):
        print(json.dumps(self.properties, indent=4))

    def write(self, filepath):
        with open(filepath, 'w', encoding='utf-8') as f:
            # Reconstruct basic structure (simplified, not full round-trip)
            f.write('msc {\n')
            # Global options
            opts = ', '.join([f'{k}="{v}"' for k, v in self.properties['globalOptions'].items()])
            if opts:
                f.write(f'  {opts};\n')
            # Entities
            ents = ', '.join([f'{e["name"]} [{", ".join([f'{k}="{v}"' for k, v in e["attributes"].items()])}]' if e["attributes"] else e["name"] for e in self.properties['entities']])
            f.write(f'  {ents};\n')
            # Arcs
            for a in self.properties['arcs']:
                attrs = f' [{", ".join([f'{k}="{v}"' for k, v in a["attributes"].items()])}]' if a["attributes"] else ''
                f.write(f'  {a["from"]} {a["type"]} {a["to"]}{attrs};\n')
            # Comments
            for c in self.properties['comments']:
                f.write(f'{c}\n')
            f.write('}\n')

# Example usage
# parser = MSCParser()
# parser.read('example.msc')
# parser.print_properties()
# parser.write('output.msc')

5. Write a java class that can open any file of format .MSC and decode read and write and print to console all the properties from the above list

import java.io.*;
import java.util.*;
import java.util.regex.*;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class MSCParser {
    private Map<String, Object> properties = new HashMap<>();

    public MSCParser() {
        properties.put("globalOptions", new HashMap<String, String>());
        properties.put("entities", new ArrayList<Map<String, Object>>());
        properties.put("arcs", new ArrayList<Map<String, Object>>());
        properties.put("colors", new HashSet<String>());
        properties.put("comments", new ArrayList<String>());
    }

    public void read(String filepath) throws IOException {
        StringBuilder content = new StringBuilder();
        try (BufferedReader br = new BufferedReader(new FileReader(filepath))) {
            String line;
            while (line = br.readLine()) != null) {
                content.append(line).append("\n");
            }
        }
        parse(content.toString());
    }

    private void parse(String content) {
        // Global options
        Pattern optionsPattern = Pattern.compile("msc\\s*\\{\\s*([^}]*)\\;", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
        Matcher optionsMatcher = optionsPattern.matcher(content);
        if (optionsMatcher.find()) {
            String[] options = optionsMatcher.group(1).split(",");
            Map<String, String> globalOptions = (Map<String, String>) properties.get("globalOptions");
            for (String opt : options) {
                String[] kv = opt.split("=");
                if (kv.length == 2) {
                    String key = kv[0].trim().replaceAll("[\"']", "");
                    String value = kv[1].trim().replaceAll("[\"']", "");
                    globalOptions.put(key, value);
                }
            }
        }

        // Entities
        Pattern entityPattern = Pattern.compile("([a-z0-9_\"]+[^;]*);", Pattern.CASE_INSENSITIVE);
        Matcher entityMatcher = entityPattern.matcher(content);
        List<Map<String, Object>> entities = (List<Map<String, Object>>) properties.get("entities");
        while (entityMatcher.find()) {
            String ent = entityMatcher.group(1);
            String name = ent.split("\\[")[0].trim();
            Map<String, String> attrs = new HashMap<>();
            Pattern attrPattern = Pattern.compile("\\[(.*)\\]");
            Matcher attrMatcher = attrPattern.matcher(ent);
            if (attrMatcher.find()) {
                attrs = parseAttributes(attrMatcher.group(1));
            }
            Map<String, Object> entityMap = new HashMap<>();
            entityMap.put("name", name);
            entityMap.put("attributes", attrs);
            entities.add(entityMap);
            extractColors(attrs);
        }

        // Arcs
        String[] lines = content.split("\n");
        List<Map<String, Object>> arcs = (List<Map<String, Object>>) properties.get("arcs");
        for (String line : lines) {
            line = line.trim();
            if (line.matches(".*[-:=x>|<.*].*")) {
                Pattern arcPattern = Pattern.compile("(.*?)\\s*([-><:=x|.-]+)\\s*(.*)");
                Matcher arcMatcher = arcPattern.matcher(line);
                if (arcMatcher.find()) {
                    String from = arcMatcher.group(1).trim();
                    String type = arcMatcher.group(2).trim();
                    String toAndAttrs = arcMatcher.group(3).trim();
                    String to = toAndAttrs.split("\\[")[0].trim();
                    Map<String, String> attrs = new HashMap<>();
                    Pattern attrPattern = Pattern.compile("\\[(.*)\\]");
                    Matcher attrMatcher = attrPattern.matcher(toAndAttrs);
                    if (attrMatcher.find()) {
                        attrs = parseAttributes(attrMatcher.group(1));
                    }
                    Map<String, Object> arcMap = new HashMap<>();
                    arcMap.put("from", from);
                    arcMap.put("type", type);
                    arcMap.put("to", to);
                    arcMap.put("attributes", attrs);
                    arcs.add(arcMap);
                    extractColors(attrs);
                }
            }
        }

        // Comments
        List<String> comments = (List<String>) properties.get("comments");
        for (String line : lines) {
            String stripped = line.trim();
            if (stripped.startsWith("#") || stripped.startsWith("//")) {
                comments.add(stripped);
            }
        }

        ((Set<String>) properties.get("colors")).toArray(); // Convert to list if needed
    }

    private Map<String, String> parseAttributes(String attrStr) {
        Map<String, String> attrs = new HashMap<>();
        String[] attrParts = attrStr.split(",");
        for (String attr : attrParts) {
            String[] kv = attr.split("=");
            if (kv.length == 2) {
                String key = kv[0].trim().replaceAll("[\"']", "");
                String value = kv[1].trim().replaceAll("[\"']", "");
                attrs.put(key, value);
            }
        }
        return attrs;
    }

    private void extractColors(Map<String, String> attrs) {
        Set<String> colors = (Set<String>) properties.get("colors");
        String[] colorKeys = {"linecolour", "linecolor", "textcolour", "textcolor", "textbgcolour", "textbgcolor", "arclinecolour", "arclinecolor", "arctextcolour", "arctextcolor", "arctextbgcolour", "arctextbgcolor"};
        for (String key : colorKeys) {
            if (attrs.containsKey(key)) {
                colors.add(attrs.get(key));
            }
        }
    }

    public void printProperties() {
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        System.out.println(gson.toJson(properties));
    }

    public void write(String filepath) throws IOException {
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(filepath))) {
            bw.write("msc {\n");
            Map<String, String> globalOptions = (Map<String, String>) properties.get("globalOptions");
            String opts = String.join(", ", globalOptions.entrySet().stream().map(e -> e.getKey() + "=\"" + e.getValue() + "\"").toArray(String[]::new));
            if (!opts.isEmpty()) {
                bw.write("  " + opts + ";\n");
            }
            List<Map<String, Object>> entities = (List<Map<String, Object>>) properties.get("entities");
            String ents = String.join(", ", entities.stream().map(e -> {
                Map<String, String> attrs = (Map<String, String>) e.get("attributes");
                String attrStr = attrs.isEmpty() ? "" : " [" + String.join(", ", attrs.entrySet().stream().map(a -> a.getKey() + "=\"" + a.getValue() + "\"").toArray(String[]::new)) + "]";
                return (String) e.get("name") + attrStr;
            }).toArray(String[]::new));
            bw.write("  " + ents + ";\n");
            List<Map<String, Object>> arcs = (List<Map<String, Object>>) properties.get("arcs");
            for (Map<String, Object> a : arcs) {
                Map<String, String> attrs = (Map<String, String>) a.get("attributes");
                String attrStr = attrs.isEmpty() ? "" : " [" + String.join(", ", attrs.entrySet().stream().map(at -> at.getKey() + "=\"" + at.getValue() + "\"").toArray(String[]::new)) + "]";
                bw.write("  " + a.get("from") + " " + a.get("type") + " " + a.get("to") + attrStr + ";\n");
            }
            List<String> comments = (List<String>) properties.get("comments");
            for (String c : comments) {
                bw.write(c + "\n");
            }
            bw.write("}\n");
        }
    }

    // Example usage
    // public static void main(String[] args) throws IOException {
    //     MSCParser parser = new MSCParser();
    //     parser.read("example.msc");
    //     parser.printProperties();
    //     parser.write("output.msc");
    // }
}

6. Write a javascript class that can open any file of format .MSC and decode read and write and print to console all the properties from the above list

class MSCParser {
  constructor() {
    this.properties = {
      globalOptions: {},
      entities: [],
      arcs: [],
      colors: new Set(),
      comments: []
    };
  }

  async read(filepath) {
    // For node.js, use fs
    const fs = require('fs');
    const content = fs.readFileSync(filepath, 'utf8');
    this._parse(content);
  }

  _parse(content) {
    // Global options
    const optionsMatch = content.match(/msc\s*{\s*([^}]*);/i);
    if (optionsMatch) {
      const options = optionsMatch[1].split(',');
      options.forEach(opt => {
        const [key, value] = opt.split('=').map(s => s.trim().replace(/["']/g, ''));
        if (key) this.properties.globalOptions[key] = value;
      });
    }

    // Entities
    const entityMatch = content.match(/([a-z0-9_"]+[^;]*);/gi);
    if (entityMatch) {
      entityMatch.forEach(ent => {
        const entity = ent.replace(';', '').trim();
        const attrMatch = entity.match(/\[(.*)\]/);
        const attrs = attrMatch ? this._parseAttributes(attrMatch[1]) : {};
        this.properties.entities.push({ name: entity.split('[')[0].trim(), attributes: attrs });
        this._extractColors(attrs);
      });
    }

    // Arcs
    const arcLines = content.split(';').filter(line => line.trim().match(/[-:=x>|<.*]/));
    arcLines.forEach(arc => {
      const parts = arc.split(/([-><:=x|.-]+)/).map(s => s.trim());
      const type = parts[1] || '';
      const attrMatch = arc.match(/\[(.*)\]/);
      const attrs = attrMatch ? this._parseAttributes(attrMatch[1]) : {};
      this.properties.arcs.push({ from: parts[0], type, to: parts[2], attributes: attrs });
      this._extractColors(attrs);
    });

    // Comments
    content.split('\n').forEach(line => {
      const stripped = line.trim();
      if (stripped.startsWith('#') || stripped.startsWith('//')) {
        this.properties.comments.push(stripped);
      }
    });

    this.properties.colors = Array.from(this.properties.colors);
  }

  _parseAttributes(str) {
    const attrs = {};
    str.split(',').forEach(attr => {
      const [key, value] = attr.split('=').map(s => s.trim().replace(/["']/g, ''));
      if (key) attrs[key] = value;
    });
    return attrs;
  }

  _extractColors(attrs) {
    Object.entries(attrs).forEach(([key, value]) => {
      if (key.includes('colour') || key.includes('color')) {
        this.properties.colors.add(value);
      }
    });
  }

  printProperties() {
    console.log(JSON.stringify(this.properties, null, 2));
  }

  write(filepath) {
    const fs = require('fs');
    let output = 'msc {\n';
    // Global options
    const opts = Object.entries(this.properties.globalOptions).map(([k, v]) => `${k}="${v}"`).join(', ');
    if (opts) output += `  ${opts};\n`;
    // Entities
    const ents = this.properties.entities.map(e => {
      const attrs = Object.entries(e.attributes).map(([k, v]) => `${k}="${v}"`).join(', ');
      return attrs ? `${e.name} [${attrs}]` : e.name;
    }).join(', ');
    output += `  ${ents};\n`;
    // Arcs
    this.properties.arcs.forEach(a => {
      const attrs = Object.entries(a.attributes).map(([k, v]) => `${k}="${v}"`).join(', ');
      const attrStr = attrs ? ` [${attrs}]` : '';
      output += `  ${a.from} ${a.type} ${a.to}${attrStr};\n`;
    });
    // Comments
    this.properties.comments.forEach(c => {
      output += `${c}\n`;
    });
    output += '}\n';
    fs.writeFileSync(filepath, output, 'utf8');
  }
}

// Example usage (in Node.js)
// const parser = new MSCParser();
// await parser.read('example.msc');
// parser.printProperties();
// parser.write('output.msc');

7. Write a c class that can open any file of format .MSC and decode read and write and print to console all the properties from the above list

Note: This is implemented in C++ for class support, as "c class" likely implies C++.

#include <iostream>
#include <fstream>
#include <sstream>
#include <regex>
#include <map>
#include <vector>
#include <set>
#include <nlohmann/json.hpp> // Assume json library for printing, or replace with custom

using json = nlohmann::json;

class MSCParser {
private:
    std::map<std::string, std::string> globalOptions;
    std::vector<std::map<std::string, std::string>> entities; // Simplified, name in map as "name"
    std::vector<std::map<std::string, std::string>> arcs; // from, type, to as strings in map
    std::set<std::string> colors;
    std::vector<std::string> comments;

public:
    void read(const std::string& filepath) {
        std::ifstream file(filepath);
        std::stringstream content;
        content << file.rdbuf();
        parse(content.str());
    }

    void parse(const std::string& content) {
        // Global options
        std::regex optionsRegex("msc\\s*\\{\\s*([^}]*)\\;", std::regex::icase);
        std::smatch optionsMatch;
        if (std::regex_search(content, optionsMatch, optionsRegex)) {
            std::string optionsStr = optionsMatch[1].str();
            std::regex optRegex("([^,]+)");
            std::sregex_iterator optIter(optionsStr.begin(), optionsStr.end(), optRegex);
            for (; optIter != std::sregex_iterator(); ++optIter) {
                std::string opt = optIter->str();
                size_t eqPos = opt.find('=');
                if (eqPos != std::string::npos) {
                    std::string key = opt.substr(0, eqPos);
                    std::string value = opt.substr(eqPos + 1);
                    key.erase(remove(key.begin(), key.end(), '"'), key.end());
                    key.erase(remove(key.begin(), key.end(), '\''), key.end());
                    value.erase(remove(value.begin(), value.end(), '"'), value.end());
                    value.erase(remove(value.begin(), value.end(), '\''), value.end());
                    key.erase(0, key.find_first_not_of(" \t"));
                    key.erase(key.find_last_not_of(" \t") + 1);
                    value.erase(0, value.find_first_not_of(" \t"));
                    value.erase(value.find_last_not_of(" \t") + 1);
                    globalOptions[key] = value;
                }
            }
        }

        // Entities
        std::regex entityRegex("([a-z0-9_\"]+[^;]*);", std::regex::icase);
        std::sregex_iterator entityIter(content.begin(), content.end(), entityRegex);
        for (; entityIter != std::sregex_iterator(); ++entityIter) {
            std::string ent = entityIter->str(1);
            std::map<std::string, std::string> entityMap;
            size_t bracketPos = ent.find('[');
            std::string name = ent.substr(0, bracketPos);
            name.erase(0, name.find_first_not_of(" \t"));
            name.erase(name.find_last_not_of(" \t") + 1);
            entityMap["name"] = name;
            if (bracketPos != std::string::npos) {
                std::string attrStr = ent.substr(bracketPos + 1, ent.find(']') - bracketPos - 1);
                entityMap.update(parseAttributes(attrStr));
                extractColors(entityMap);
            }
            entities.push_back(entityMap);
        }

        // Arcs
        std::stringstream ss(content);
        std::string line;
        while (std::getline(ss, line, ';')) {
            line.erase(0, line.find_first_not_of(" \t\n\r\f\v"));
            line.erase(line.find_last_not_of(" \t\n\r\f\v") + 1);
            if (std::regex_match(line, std::regex(".*[-:=x>|<.*].*"))) {
                std::regex arcRegex("(.*?)\\s*([-><:=x|.-]+)\\s*(.*)");
                std::smatch arcMatch;
                if (std::regex_match(line, arcMatch, arcRegex)) {
                    std::map<std::string, std::string> arcMap;
                    arcMap["from"] = arcMatch[1].str();
                    arcMap["type"] = arcMatch[2].str();
                    std::string toAndAttrs = arcMatch[3].str();
                    size_t bracketPos = toAndAttrs.find('[');
                    std::string to = toAndAttrs.substr(0, bracketPos);
                    to.erase(0, to.find_first_not_of(" \t"));
                    to.erase(to.find_last_not_of(" \t") + 1);
                    arcMap["to"] = to;
                    if (bracketPos != std::string::npos) {
                        std::string attrStr = toAndAttrs.substr(bracketPos + 1, toAndAttrs.find(']') - bracketPos - 1);
                        auto attrs = parseAttributes(attrStr);
                        for (auto& pair : attrs) {
                            arcMap[pair.first] = pair.second;
                        }
                        extractColors(attrs);
                    }
                    arcs.push_back(arcMap);
                }
            }
        }

        // Comments
        std::stringstream ss2(content);
        while (std::getline(ss2, line)) {
            line.erase(0, line.find_first_not_of(" \t"));
            line.erase(line.find_last_not_of(" \t") + 1);
            if (line.rfind('#', 0) == 0 || line.rfind("//", 0) == 0) {
                comments.push_back(line);
            }
        }
    }

    std::map<std::string, std::string> parseAttributes(const std::string& attrStr) {
        std::map<std::string, std::string> attrs;
        std::regex attrRegex("([^,]+)");
        std::sregex_iterator attrIter(attrStr.begin(), attrStr.end(), attrRegex);
        for (; attrIter != std::sregex_iterator(); ++attrIter) {
            std::string attr = attrIter->str();
            size_t eqPos = attr.find('=');
            if (eqPos != std::string::npos) {
                std::string key = attr.substr(0, eqPos);
                std::string value = attr.substr(eqPos + 1);
                key.erase(remove(key.begin(), key.end(), '"'), key.end());
                key.erase(remove(key.begin(), key.end(), '\''), key.end());
                value.erase(remove(value.begin(), value.end(), '"'), value.end());
                value.erase(remove(value.begin(), value.end(), '\''), value.end());
                key.erase(0, key.find_first_not_of(" \t"));
                key.erase(key.find_last_not_of(" \t") + 1);
                value.erase(0, value.find_first_not_of(" \t"));
                value.erase(value.find_last_not_of(" \t") + 1);
                attrs[key] = value;
            }
        }
        return attrs;
    }

    void extractColors(const std::map<std::string, std::string>& attrs) {
        std::vector<std::string> colorKeys = {"linecolour", "linecolor", "textcolour", "textcolor", "textbgcolour", "textbgcolor", "arclinecolour", "arclinecolor", "arctextcolour", "arctextcolor", "arctextbgcolour", "arctextbgcolor"};
        for (const auto& key : colorKeys) {
            auto it = attrs.find(key);
            if (it != attrs.end()) {
                colors.insert(it->second);
            }
        }
    }

    void printProperties() {
        json j;
        j["globalOptions"] = globalOptions;
        j["entities"] = entities;
        j["arcs"] = arcs;
        std::vector<std::string> colorVec(colors.begin(), colors.end());
        j["colors"] = colorVec;
        j["comments"] = comments;
        std::cout << j.dump(4) << std::endl;
    }

    void write(const std::string& filepath) {
        std::ofstream file(filepath);
        file << "msc {" << std::endl;
        std::string opts;
        for (const auto& pair : globalOptions) {
            if (!opts.empty()) opts += ", ";
            opts += pair.first + "=\"" + pair.second + "\"";
        }
        if (!opts.empty()) file << "  " << opts << ";" << std::endl;
        std::string ents;
        for (const auto& e : entities) {
            if (!ents.empty()) ents += ", ";
            ents += e.at("name");
            std::string attrStr;
            for (const auto& pair : e) {
                if (pair.first != "name") {
                    if (!attrStr.empty()) attrStr += ", ";
                    attrStr += pair.first + "=\"" + pair.second + "\"";
                }
            }
            if (!attrStr.empty()) ents += " [" + attrStr + "]";
        }
        file << "  " << ents << ";" << std::endl;
        for (const auto& a : arcs) {
            std::string from = a.at("from");
            std::string type = a.at("type");
            std::string to = a.at("to");
            std::string attrStr;
            for (const auto& pair : a) {
                if (pair.first != "from" && pair.first != "type" && pair.first != "to") {
                    if (!attrStr.empty()) attrStr += ", ";
                    attrStr += pair.first + "=\"" + pair.second + "\"";
                }
            }
            file << "  " << from << " " << type << " " << to;
            if (!attrStr.empty()) file << " [" << attrStr << "]";
            file << ";" << std::endl;
        }
        for (const auto& c : comments) {
            file << c << std::endl;
        }
        file << "}" << std::endl;
    }
};

// Example usage
// int main() {
//     MSCParser parser;
//     parser.read("example.msc");
//     parser.printProperties();
//     parser.write("output.msc");
//     return 0;
// }