ClaFi configuration format v1.0

@What: Hierarchical, line-oriented text for configuration trees
@Part of: ClaFi - Clarity First the Framework
@Loaded and saved by: the Dom's FileFormat::ClaFi - see Dom
@Sibling: Fmt, the text language - see Writing Text

1. Design Manifesto = [

This specification describes the configuration file format used in ClaFi. It combines the line-oriented simplicity of flat key-value files with the recursive structure of a tree. It structures configuration trees without losing the clarity of a plain text file.

The format is designed to be simple and fast to parse:

Tooling Compatibility = [

The format uses comments starting with ;, value assignments with =, and square brackets [ for divisions. This layout mirrors legacy property files, allowing immediate syntax highlighting in VS Code when using .cfg or .ini extensions.

]
The Literal Advantage = [

The core philosophy is No Escaping. The backslash (\) is treated as a literal character, which provides several advantages:

  • Source-Code Friendly: You can paste C++, JavaScript, or Regex snippets directly into a value. *read how to paste multiline text in Continuations
  • Path Safe: File paths are stored exactly as they appear in the file system.
  • Predictable Round-Trips: Reading a file and writing it back results in the same data without substitutions.
]
]

2. Values & Arrays = [

Values are either quoted strings or unquoted literals. The format has no built-in type system; interpretation is left to the application.

2.1 Scalar Values

Content after the = is the value. If it starts and ends with ", the Bookend Rule applies. Otherwise, the value is taken literally after trimming outer whitespace.

; Quoted string
Name = "Nebula Framework"

; Unquoted literals
Version = 1.0

; Unquoted literal (application decides type)
Enabled = yes

; No escaping needed
Path = C:\Program Files\App
Implementation Freedom = [

Parsers return strings. Applications interpret types like integers, floats, or booleans. The format is type-agnostic.

]

2.2 Arrays

Arrays group multiple values under a key. Each item begins with = on its own line.

2.2.1 Scalar Array

Flags = [
    = "Optimize"
    = "Debug"
    = "Release"
]

2.2.2 Section Array (List of Sections)

Users = [
    =[
        Name = "Alice"
        Active = 1
    ]
    =[
        Name = "Bob"
        Active = 0
    ]
]
Parsing Arrays = [
  1. When Key = [ is encountered, begin collecting items.
  2. Each line starting with = begins a new item.
  3. If =[, parse as an anonymous section until the matching ].
  4. Otherwise, parse as a scalar value (applying Bookend Rule).
  5. Stop at ] on its own line.
]
]

3. Nested Sections = [

The format uses a tree structure where any attribute can branch into a deeper section. The format supports unlimited nesting depth.

Attributes assign nested sections using the [ token:

NestedSection = [
    SubKey = "Value"
]
]

4. Line-Orientation = [

The format is line-oriented. To keep parsers simple and git diffs clean, structural markers cannot be inlined.

MarkerRequirement
] (closing scope marker)Must be the first non-whitespace character on its own line.
= (array item marker)Must be the first non-whitespace character on its own line.
| or +Must be the first non-whitespace character on its own line.
]

5. Continuations = [

If a value spans multiple lines, the format provides two behaviors:

LongText = "First Line"
         | "Second Line"
         | "Third Line"
           ; Value: First Line\nSecond Line\nThird Line

LongPath = "C:\Program"
         + " Files"
         + "\Application"
           ; Value: C:\Program Files\Application

* Note on Multiline Snippets: To paste multiline content, prepend the newline continuation marker (|) at the start of each subsequent line.

Because leading whitespace on continuation lines is stripped up to the | marker, optional indentation allows you to align subsequent lines with the first one - even within deeply nested sections - greatly improving visual readability.

NestedKey = Line one content
          | Line two content
          | Line three content

Note: Double quotes are optional.

Addressing the "No Escaping" Critique: Some argue that requiring a prepended | on continuation lines acts as an escape sequence. In this format, a multiline string is modeled as an array of independent, single-line strings presented separately. The | is a structural syntax marker indicating how separate lines compile into the tree, not an escape character.

]

6. The Bookend Rule = [

To support quotes and protect whitespace boundaries without escaping, the format uses one rule for boundary detection:

Rule For Parsers = [

If a line starts and ends with a double-quote ("), the parser strips the first and last character.

]
Rule For Builders = [

To prevent data loss, a builder must wrap a value in double-quotes (") if the value:

  • Starts and ends with double-quotes (e.g. "Error").
  • Is exactly an opening square bracket ([) - prevents section hijacking.
  • Contains leading or trailing whitespace - prevents boundary loss during trimming.

The parser strips the outer quotes during load, restoring the exact value.

]

This preserves internal quotes and margins:

; 1. Quotes preserved without escaping
LogMessage = "System returned "Error 404" at runtime"
; Internal Value: System returned "Error 404" at runtime

; 2. Padding preserved (normally lost during trims)
PaddedText = "   Preserved Margins   "
; Internal Value:    Preserved Margins   

If the line does not have quotes at both ends, the parser treats it as an unquoted string and trims the outer whitespace.

]

7. Parser Guide = [

Implementing a parser is simple. Because the format is line-oriented, you do not need to scan back and forth within a line for transitions.

The Standard Algorithm = [
  1. Read: Take a line from the input.
  2. Initial Trim: Remove leading whitespace.
  3. Dispatch:
    • If @: Handle metadata.
    • If ;: Skip line.
    • If ]: Pop scope from the stack.
    • If | or +: Apply Bookend Rule and append to the last processed value.
    • If Key/Alpha: Split at =.
      • If Value is [: Push a new named section scope.
      • Else: Process value.
]
]

8. Marker Reference = [

SymbolLogic
@Optional Metadata Directive (e.g., encoding)
;Line Comment
Key = [Starts a Named Section or Array
=[Starts an Anonymous Section (Array Item)
]Ends current scope (Must be on new line)
=Value Assignment or Array Item Marker
|Newline continuation
+Concatenation continuation
"Bookend Quotes (Stripped if at both ends of a line)
Note = [

Values may contain any symbols, including reserved markers. Avoid using them in keys.

]
]

9. Full Showcase = [

@ClaFi 1.0
@encoding utf-8

; Example usage

ColorMode=Dark

MainForm=[
    Bounds=[
        Left=934
        Top=515
        Right=2886
        Bottom=1765
    ]
]

Tabs=[
    OpenTabs="Tab1,Tab2,Tab3"
    TabContent=[
        =[
            Id="Tab1"
            Title="Themes"
            Path="Home"
            Preview=1
            ThemeData=[
                BgSaturation=0
                AnchorHue=210
                Harmony="Monochromatic"
            ]
        ]
        =[
            Id="Tab2"
            Title="New Theme (2)"
            Preview=1
            ThemeData=[
                BgSaturation=0.07333333
                AnchorHue=71
            ]
        ]
    ]
    SelectedTab="Tab3"
]
]

10. C++ Examples = [

This chapter provides C++ examples for implementing key features. The snippets demonstrate the line-oriented design.

10.1 Implementing the Bookend Rule

// Outer whitespace removed, as from every key and value before it is read
std::wstring_view trimmed(std::wstring_view source)
{
    while (!source.empty() && std::iswspace(source.front()))
    {
        source.remove_prefix(1);
    }
    while (!source.empty() && std::iswspace(source.back()))
    {
        source.remove_suffix(1);
    }
    return source;
}

// Decoder following the Bookend Rule, over a trimmed value
std::wstring_view decodeValue(std::wstring_view source)
{
    if (source.size() > 1 && source.front() == L'"' && source.back() == L'"')
    {
        return source.substr(1, source.size() - 2);
    }
    return source;
}

// Builder side: protecting quotes, brackets, and boundary spaces
std::wstring encodeValue(std::wstring_view source)
{
    const bool isOpeningBracket = (source == L"[");
    const bool isAlreadyBookended = (source.size() >= 2 && source.front() == L'"' && source.back() == L'"');
    const bool hasBoundaryWhitespace = (!source.empty() && (std::iswspace(source.front()) || std::iswspace(source.back())));

    if (isOpeningBracket || isAlreadyBookended || hasBoundaryWhitespace)
    {
        return L"\"" + std::wstring(source) + L"\"";
    }
    return std::wstring(source);
}

10.2 Core Parser Loop Structure

The main dispatch logic showcasing the format's line-oriented simplicity:

void ConfigFile::load(std::wifstream& stream)
{
    std::wstring line;
    Node* currentScope = rootNode;

    while (getTrimLine(stream, line))
    {
        if (line.empty())
        {
            continue;
        }

        // Dispatch based on first character
        switch (line.front())
        {
            case L';':  // Comment
            {
                break;
            }

            case L']':   // End current scope
            {
                currentScope = currentScope->parent();
                break;
            }

            case L'|':  // Newline continuation
            case L'+':  // Concatenation continuation
            {
                if (lastValueNode)
                {
                    bool addNewline = (line.front() == L'|');
                    std::wstring_view chunk = trimmed(std::wstring_view{ line }.substr(1));
                    lastValueNode->appendValue(decodeValue(chunk), addNewline);
                }
                break;
            }

            default:  // Key=Value assignment or Array item
            {
                size_t eqPos = line.find(L'=');
                if (eqPos != std::wstring::npos)
                {
                    std::wstring_view whole = line;
                    std::wstring_view key = trimmed(whole.substr(0, eqPos));
                    std::wstring_view value = trimmed(whole.substr(eqPos + 1));

                    if (key.empty())
                    {
                        // Array item
                        if (value == L"[")
                        {
                            currentScope = &currentScope->addAnonymousSection();
                        }
                        else
                        {
                            currentScope->addArrayItem(decodeValue(value));
                        }
                    }
                    else
                    {
                        // Key=Value assignment - only a bare [ opens a section, a quoted one is a value
                        if (value == L"[")
                        {
                            currentScope = &currentScope->addNamedSection(key);
                        }
                        else
                        {
                            lastValueNode = &currentScope->assignValue(key, decodeValue(value));
                        }
                    }
                }
                else
                {
                    // Syntax Error: Every non-reserved line must contain an '='
                }
                break;
            }
        }
    }
}
]
All manuals Top