sots-engine/src/mars/parse/blocks.h

91 lines
3.6 KiB
C++

// mars::parse -- reader for the engine's brace-block key/value data format.
//
// This one grammar carries almost every catalog the game ships: *.weapon,
// *.shipsection, *.tech, *.combat, *.def, *.script and the block-form *.txt
// files (scenarios, tutorial, credits, system names, sky defs, ...).
//
// body := (block | pair | item)*
// block := NAME '{' body '}'
// pair := NAME value
// item := QUOTED | NAME (a NAME directly before '}' or EOF)
// value := QUOTED | BAREWORD
// comment := '//' ... end of line
//
// The tokenizer is whitespace-driven, not line-based: a block may open on the
// same line as a preceding pair, a name may sit on the same line as its brace,
// and a quoted string may span lines. There is no escape syntax -- backslashes
// and '//' inside quotes are literal. Bytes pass through untouched (cp1252).
//
// Leniency (the shipped data needs both; the engine accepts them):
// * end of file closes every open block -> Diagnostic in Document::warnings
// * a stray '}' at top level is ignored -> Diagnostic in Document::warnings
// With Options::strict both become errors. Three things are always errors:
// an unterminated quoted string, a '{' with no name before it, and nothing else.
#pragma once
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include "result.h"
namespace mars::parse {
struct Node;
// One thing inside a block, in file order.
struct Entry {
enum class Kind { Pair, Item, Block };
Kind kind = Kind::Pair;
std::string key; // Pair/Block: the name as written (original case). Item: empty.
std::string value; // Pair: the value text; Item: the item text. Block: empty.
bool quoted = false; // value/item was written inside double quotes
std::unique_ptr<Node> block; // Block only
int line = 0; // 1-based line of the key (or of the item)
Entry();
Entry(Entry&&) noexcept;
Entry& operator=(Entry&&) noexcept;
~Entry();
Entry(const Entry&) = delete;
Entry& operator=(const Entry&) = delete;
bool is_pair() const { return kind == Kind::Pair; }
bool is_item() const { return kind == Kind::Item; }
bool is_block() const { return kind == Kind::Block; }
};
// A named block (or the document root, whose name is empty).
struct Node {
std::string name;
std::vector<Entry> entries;
int line = 0; // line of the opening brace (0 for the root)
// Lookups are case-insensitive on the key and see pairs *and* blocks
// (the data uses `option X` and `option { ... }` under one key).
const Entry* first(std::string_view key) const;
std::vector<const Entry*> all(std::string_view key) const;
// Convenience filters. `first_value` returns the first *pair* under the key.
const std::string* first_value(std::string_view key) const;
std::vector<std::string_view> values(std::string_view key) const;
const Node* first_block(std::string_view key) const;
std::vector<const Node*> blocks(std::string_view key) const;
std::vector<std::string_view> items() const; // bare quoted / trailing barewords
bool has(std::string_view key) const { return first(key) != nullptr; }
};
struct Document {
Node root; // the file's top level is itself a body
std::vector<Diagnostic> warnings; // lenient recoveries taken
};
struct Options {
bool strict = false; // reject files the engine would repair
};
Result<Document> parse_blocks(std::string_view text, Options options = {});
} // namespace mars::parse