163 const std::string shown(text);
164 if (text.size() != 7 && text.size() != 9) {
165 return absl::InvalidArgumentError(
166 absl::StrFormat(
"color \"%s\" must be #RRGGBB or #RRGGBBAA", shown));
168 if (text[0] !=
'#') {
169 return absl::InvalidArgumentError(
170 absl::StrFormat(
"color \"%s\" must start with #", shown));
173 for (
size_t i = 1; i < text.size(); ++i) {
174 const char c = text[i];
176 if (c >=
'0' && c <=
'9')
178 if (c >=
'a' && c <=
'f')
179 digit = c -
'a' + 10;
180 if (c >=
'A' && c <=
'F')
181 digit = c -
'A' + 10;
183 return absl::InvalidArgumentError(
184 absl::StrFormat(
"color \"%s\" has a non-hex digit", shown));
186 value = (value << 4) | static_cast<uint32_t>(digit);
188 if (text.size() == 7) {
189 value = (value << 8) | 0xFF;
195 std::string_view json_text,
int max_room_id) {
196 json root = json::parse(json_text.begin(), json_text.end(),
nullptr,
198 if (root.is_discarded()) {
199 return absl::InvalidArgumentError(
"proposal overlay is not valid JSON");
201 if (!root.is_object()) {
202 return Invalid(
"root",
"must be an object");
205 return Invalid(
"root",
206 absl::StrFormat(
"\"format\" must be \"%s\"",
209 auto version = root.find(
"version");
210 if (version == root.end() || !version->is_number_integer() ||
212 return Invalid(
"root",
213 absl::StrFormat(
"unsupported \"version\" (expected %d)",
218 overlay.
title = OptionalString(root,
"title");
219 overlay.
status = OptionalString(root,
"status");
220 if (
auto notes = root.find(
"notes");
221 notes != root.end() && notes->is_array()) {
222 for (
const auto& note : *notes) {
223 if (note.is_string())
224 overlay.
notes.push_back(note.get<std::string>());
228 auto rooms = root.find(
"rooms");
229 if (rooms == root.end() || !rooms->is_array() || rooms->empty()) {
230 return Invalid(
"root",
"\"rooms\" must be a non-empty list");
232 std::set<int> room_ids;
233 for (
size_t i = 0; i < rooms->size(); ++i) {
234 const std::string where = absl::StrFormat(
"rooms[%d]", i);
235 const json& entry = (*rooms)[i];
237 entry.is_object() && entry.contains(
"room") ? entry[
"room"] : entry;
238 auto room = ParseRoomId(
id, where, max_room_id);
240 return room.status();
241 if (!room_ids.insert(*room).second) {
242 return Invalid(where,
243 absl::StrFormat(
"room 0x%X is listed twice", *room));
245 overlay.
rooms.push_back(
246 {*room, entry.is_object() ? OptionalString(entry,
"label") :
""});
249 auto layers = root.find(
"layers");
250 if (layers == root.end() || !layers->is_array()) {
251 return Invalid(
"root",
"\"layers\" must be a list");
253 std::set<std::string> layer_ids;
254 for (
size_t li = 0; li < layers->size(); ++li) {
255 const json& entry = (*layers)[li];
256 std::string where = absl::StrFormat(
"layers[%d]", li);
257 if (!entry.is_object()) {
258 return Invalid(where,
"must be an object");
261 layer.
id = OptionalString(entry,
"id");
262 if (layer.
id.empty()) {
263 return Invalid(where,
"missing \"id\"");
265 if (!layer_ids.insert(layer.
id).second) {
266 return Invalid(where,
267 absl::StrFormat(
"duplicate layer id \"%s\"", layer.
id));
269 where = absl::StrFormat(
"layer \"%s\"", layer.
id);
270 layer.
label = OptionalString(entry,
"label");
271 if (layer.
label.empty())
273 if (
auto color = entry.find(
"color"); color != entry.end()) {
274 if (!color->is_string()) {
275 return Invalid(where,
"\"color\" must be a string");
279 return Invalid(where, std::string(rgba.status().message()));
283 if (
auto visible = entry.find(
"visible");
284 visible != entry.end() && visible->is_boolean()) {
285 layer.
visible = visible->get<
bool>();
287 auto shapes = entry.find(
"shapes");
288 if (shapes == entry.end() || !shapes->is_array()) {
289 return Invalid(where,
"\"shapes\" must be a list");
291 for (
size_t si = 0; si < shapes->size(); ++si) {
293 ParseShape((*shapes)[si], absl::StrFormat(
"%s shapes[%d]", where, si),
294 room_ids, max_room_id);
296 return shape.status();
297 layer.
shapes.push_back(std::move(*shape));
299 overlay.
layers.push_back(std::move(layer));