TraceMaker
C++ API of the engine
Loading...
Searching...
No Matches
sexpr.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-3.0-or-later
2#pragma once
3// Lossless s-expression documents for KiCad files (design doc 08 §3).
4//
5// The parser keeps the original text and records each node's byte span, so writing an unmodified
6// document reproduces the input byte for byte. Edits are recorded as span replacements and insertions
7// and applied on write; untouched text is never re-formatted.
8#include <charconv>
9#include <cstdint>
10#include <optional>
11#include <span>
12#include <stdexcept>
13#include <string>
14#include <string_view>
15#include <vector>
16
17namespace tmk::sexpr {
18
19using NodeId = std::uint32_t;
20inline constexpr NodeId kNoNode = 0xFFFFFFFFu;
21
22enum class Kind : std::uint8_t { List, Symbol, String };
23
24struct Node {
26 std::uint32_t begin; // byte offset of '(' / first char of the atom / opening quote
27 std::uint32_t end; // one past ')' / last char / closing quote
28 std::uint32_t first_child; // index into Document::kids_ (lists only)
29 std::uint32_t child_count;
31};
32
33class ParseError : public std::runtime_error {
34 public:
35 ParseError(const std::string& msg, std::size_t offset, int line)
36 : std::runtime_error(msg + " at line " + std::to_string(line)), offset_(offset), line_(line) {}
37 std::size_t offset() const { return offset_; }
38 int line() const { return line_; }
39
40 private:
41 std::size_t offset_;
42 int line_;
43};
44
45class Document {
46 public:
47 // Parses `text`. Throws ParseError on malformed input. The document owns a copy of the text.
48 static Document parse(std::string text);
49 static Document load(const std::string& path);
50
51 NodeId root() const { return root_; }
52 const Node& node(NodeId id) const { return nodes_[id]; }
53 std::size_t node_count() const { return nodes_.size(); }
54 const std::string& text() const { return text_; }
55
56 // ---- navigation ----
57 std::span<const NodeId> children(NodeId list) const {
58 const Node& n = nodes_[list];
59 return {kids_.data() + n.first_child, n.child_count};
60 }
61 NodeId child(NodeId list, std::size_t i) const {
62 const Node& n = nodes_[list];
63 return i < n.child_count ? kids_[n.first_child + i] : kNoNode;
64 }
65 // First token of a list, e.g. "segment" for (segment ...). Empty for atoms or empty lists.
66 std::string_view head(NodeId list) const;
67 bool is_list(NodeId id) const { return nodes_[id].kind == Kind::List; }
68 // First child list whose head is `name`, or kNoNode.
69 NodeId find(NodeId list, std::string_view name) const;
70 // All child lists whose head is `name`.
71 std::vector<NodeId> find_all(NodeId list, std::string_view name) const;
72
73 // ---- atoms ----
74 // Raw source text of a node (for strings, including quotes).
75 std::string_view raw(NodeId id) const {
76 const Node& n = nodes_[id];
77 return std::string_view(text_).substr(n.begin, n.end - n.begin);
78 }
79 // Atom value: symbols as-is, strings unescaped.
80 std::string str(NodeId atom) const;
81 // Value of the i-th child atom of `list` (i counts the head as 0).
82 std::string str_at(NodeId list, std::size_t i) const;
83 std::optional<double> number_at(NodeId list, std::size_t i) const;
84 // Parses a decimal millimetre value to integer nanometres exactly (no binary floating point).
85 std::optional<std::int64_t> nm_at(NodeId list, std::size_t i) const;
86 // 1-based line number of a byte offset (for error messages).
87 int line_of(std::size_t offset) const;
88
89 // ---- edits (applied by write()) ----
90 // Replaces the node's text.
91 void replace(NodeId id, std::string new_text);
92 // Removes the node together with its leading whitespace on the same line and the line break, when the
93 // node occupies whole lines; otherwise removes the node and one preceding space.
94 void remove(NodeId id);
95 // Inserts `text` as new children at the end of `list`, each on its own line, indented one level deeper
96 // than the list's own line. `text` must be one or more complete s-expressions without leading indent;
97 // embedded newlines are re-indented.
98 void append_child(NodeId list, std::string_view text);
99 bool modified() const { return !edits_.empty(); }
100 // Text after applying all edits.
101 std::string write() const;
102 void save(const std::string& path) const;
103
104 // The indentation (tabs/spaces) at the start of the line containing `offset`.
105 std::string_view indent_of(std::size_t offset) const;
106
107 private:
108 struct Edit {
109 std::uint32_t begin, end; // replaced span [begin, end); begin == end for pure insertions
110 std::string text;
111 std::uint64_t seq; // insertion order, for stable ordering of insertions at the same offset
112 };
113
114 std::string text_;
115 std::vector<Node> nodes_;
116 std::vector<NodeId> kids_;
117 NodeId root_ = kNoNode;
118 std::vector<Edit> edits_;
119 mutable std::vector<std::uint32_t> line_starts_;
120};
121
122// Escapes a string for KiCad s-expressions and adds quotes.
123std::string quote(std::string_view s);
124
125// Formats nanometres as a KiCad millimetre number: shortest exact decimal, no trailing zeros ("0.25", "-12", "1.000001").
126std::string format_mm(std::int64_t nm);
127
128// Parses a decimal millimetre string to nanometres exactly; rounds half away from zero beyond 6 decimals.
129std::optional<std::int64_t> parse_mm(std::string_view s);
130
131} // namespace tmk::sexpr
NodeId root() const
Definition sexpr.hpp:51
std::string str_at(NodeId list, std::size_t i) const
Value of the i-th child atom of list (i counts the head as 0).
std::span< const NodeId > children(NodeId list) const
-— navigation -—
Definition sexpr.hpp:57
std::string write() const
Text after applying all edits.
static Document parse(std::string text)
Parses text. Throws ParseError on malformed input. The document owns a copy of the text.
void replace(NodeId id, std::string new_text)
-— edits (applied by write()) -— Replaces the node's text.
std::string_view indent_of(std::size_t offset) const
The indentation (tabs/spaces) at the start of the line containing offset.
void save(const std::string &path) const
std::string str(NodeId atom) const
Atom value: symbols as-is, strings unescaped.
NodeId find(NodeId list, std::string_view name) const
First child list whose head is name, or kNoNode.
const Node & node(NodeId id) const
Definition sexpr.hpp:52
NodeId child(NodeId list, std::size_t i) const
Definition sexpr.hpp:61
std::string_view raw(NodeId id) const
-— atoms -— Raw source text of a node (for strings, including quotes).
Definition sexpr.hpp:75
std::optional< double > number_at(NodeId list, std::size_t i) const
std::optional< std::int64_t > nm_at(NodeId list, std::size_t i) const
Parses a decimal millimetre value to integer nanometres exactly (no binary floating point).
const std::string & text() const
Definition sexpr.hpp:54
void append_child(NodeId list, std::string_view text)
Inserts text as new children at the end of list, each on its own line, indented one level deeper than...
std::vector< NodeId > find_all(NodeId list, std::string_view name) const
All child lists whose head is name.
std::size_t node_count() const
Definition sexpr.hpp:53
bool is_list(NodeId id) const
Definition sexpr.hpp:67
static Document load(const std::string &path)
int line_of(std::size_t offset) const
1-based line number of a byte offset (for error messages).
std::string_view head(NodeId list) const
First token of a list, e.g. "segment" for (segment ...). Empty for atoms or empty lists.
void remove(NodeId id)
Removes the node together with its leading whitespace on the same line and the line break,...
bool modified() const
Definition sexpr.hpp:99
ParseError(const std::string &msg, std::size_t offset, int line)
Definition sexpr.hpp:35
int line() const
Definition sexpr.hpp:38
std::size_t offset() const
Definition sexpr.hpp:37
std::string format_mm(std::int64_t nm)
Formats nanometres as a KiCad millimetre number: shortest exact decimal, no trailing zeros ("0....
std::uint32_t NodeId
Definition sexpr.hpp:19
std::string quote(std::string_view s)
Escapes a string for KiCad s-expressions and adds quotes.
constexpr NodeId kNoNode
Definition sexpr.hpp:20
std::optional< std::int64_t > parse_mm(std::string_view s)
Parses a decimal millimetre string to nanometres exactly; rounds half away from zero beyond 6 decimal...
std::uint32_t child_count
Definition sexpr.hpp:29
std::uint32_t end
one past ')' / last char / closing quote
Definition sexpr.hpp:27
std::uint32_t first_child
index into Document::kids_ (lists only)
Definition sexpr.hpp:28
NodeId parent
Definition sexpr.hpp:30
std::uint32_t begin
byte offset of '(' / first char of the atom / opening quote
Definition sexpr.hpp:26