11#include <initializer_list>
48 "empty",
"tone.info",
"tone.ok",
"tone.warn",
"tone.err",
"nav.back",
49 "nav.forward",
"nav.expand",
"nav.collapse",
"close",
"date.pick"};
54[[nodiscard]]
inline bool isIconName(std::string_view name) {
55 if (name.empty() || name.size() > 64 || name.front() <
'a' || name.front() >
'z') {
58 return std::ranges::all_of(name, [](
char const chr) {
59 return (chr >=
'a' && chr <=
'z') || (chr >=
'0' && chr <=
'9') || chr ==
'.' || chr ==
'-';
66[[nodiscard]]
inline std::string_view
iconGlyph(std::string_view name) {
67 static constexpr std::array<std::pair<std::string_view, std::string_view>, 10> kGlyphs = {{
73 {
"nav.collapse",
"▾"},
79 for (
auto const& [icon, glyph] : kGlyphs) {
99using TokenSet = std::map<std::string, Token, std::less<>>;
110 std::uint8_t
a = 255;
118 [[nodiscard]] std::string
hex()
const {
119 static constexpr std::string_view kDigits =
"0123456789abcdef";
120 std::string out =
"#";
121 auto const put = [&out](std::uint8_t
const channel) {
122 out += kDigits.at(
static_cast<std::size_t
>(channel >> 4U));
123 out += kDigits.at(
static_cast<std::size_t
>(channel & 0xFU));
140 std::map<std::string, std::string, std::less<>>
variants;
142 std::map<std::string, std::string, std::less<>>
densities;
144 std::map<std::string, std::string, std::less<>>
surfaces;
146 std::map<std::string, std::string, std::less<>>
icons;
155 if (
auto const*
const value = std::get_if<double>(&json.data)) {
158 if (
auto const*
const value = std::get_if<std::int64_t>(&json.data)) {
159 return static_cast<double>(*value);
161 if (
auto const*
const value = std::get_if<std::uint64_t>(&json.data)) {
162 return static_cast<double>(*value);
171[[nodiscard]]
inline Json const*
memberOf(Json::object_t
const&
object, std::string_view name) {
172 auto const*
const found =
object.find(name);
173 return found ==
object.end() ? nullptr : &found->second;
183 auto const*
const object = std::get_if<Json::object_t>(&json.data);
184 if (
object ==
nullptr) {
189 auto const length = value !=
nullptr ?
numberOf(*value) : std::nullopt;
190 auto const*
const unitText = unit !=
nullptr ? std::get_if<std::string>(&unit->data) :
nullptr;
191 if (!length || unitText ==
nullptr) {
194 if (*unitText ==
"px") {
197 if (*unitText ==
"rem") {
206[[nodiscard]]
inline std::optional<Rgba>
hexColor(std::string_view
text) {
207 if (!
text.starts_with(
'#')) {
210 text.remove_prefix(1);
211 auto const digit = [](
char const symbol) ->
int {
212 if (symbol >=
'0' && symbol <=
'9') {
215 if (symbol >=
'a' && symbol <=
'f') {
216 return symbol -
'a' + 10;
218 if (symbol >=
'A' && symbol <=
'F') {
219 return symbol -
'A' + 10;
223 if (!std::ranges::all_of(
text, [&digit](
char const symbol) {
return digit(symbol) >= 0; })) {
226 auto const pair = [&digit,
text](std::size_t
const offset) {
227 return static_cast<std::uint8_t
>((digit(
text.at(offset)) * 16) + digit(
text.at(offset + 1)));
229 if (
text.size() == 3) {
230 auto const single = [&digit,
text](std::size_t
const offset) {
231 return static_cast<std::uint8_t
>(digit(
text.at(offset)) * 17);
233 return Rgba{.r = single(0), .g = single(1), .b = single(2), .a = 255};
235 if (
text.size() == 6 ||
text.size() == 8) {
236 return Rgba{.r = pair(0), .g = pair(2), .b = pair(4), .a =
text.size() == 8 ? pair(6) : std::uint8_t{255}};
244[[nodiscard]]
inline std::optional<std::uint8_t>
channelOf(
double const value) {
245 if (value < 0.0 || value > 1.0) {
248 return static_cast<std::uint8_t
>(std::lround(value * 255.0));
254[[nodiscard]]
inline std::optional<std::string_view>
aliasOf(std::string_view
text) {
255 if (
text.size() < 3 || !
text.starts_with(
'{') || !
text.ends_with(
'}')) {
258 return text.substr(1,
text.size() - 2);
266 if (
auto const*
const text = std::get_if<std::string>(&json.data)) {
267 return aliasOf(*text).has_value();
269 if (
auto const*
const array = std::get_if<Json::array_t>(&json.data)) {
270 for (
Json const& item : *array) {
276 if (
auto const*
const object = std::get_if<Json::object_t>(&json.data)) {
277 for (
auto const& [name, member] : *
object) {
278 static_cast<void>(name);
289 "color",
"dimension",
"number",
"fontFamily",
"fontWeight",
"duration",
"cubicBezier",
290 "shadow",
"typography",
"border",
"strokeStyle",
"gradient",
"transition"};
294 "thin",
"hairline",
"extra-light",
"ultra-light",
"light",
"normal",
295 "regular",
"book",
"medium",
"semi-bold",
"demi-bold",
"bold",
296 "extra-bold",
"ultra-bold",
"black",
"heavy",
"extra-black",
"ultra-black"};
302[[nodiscard]]
inline std::optional<std::string> checkTokenValue(std::string_view type,
Json const& value);
310[[nodiscard]]
inline std::optional<Rgba>
colorOf(
Json const& json) {
311 if (
auto const*
const text = std::get_if<std::string>(&json.data)) {
314 auto const*
const object = std::get_if<Json::object_t>(&json.data);
315 if (
object ==
nullptr) {
318 std::optional<double> alpha = 1.0;
327 auto const*
const text = std::get_if<std::string>(&hex->data);
336 auto const*
const spaceName = space !=
nullptr ? std::get_if<std::string>(&space->data) :
nullptr;
337 auto const*
const list = components !=
nullptr ? std::get_if<Json::array_t>(&components->data) :
nullptr;
338 if (spaceName ==
nullptr || *spaceName !=
"srgb" || list ==
nullptr || list->size() != 3) {
341 std::array<std::uint8_t, 3> channels{};
342 for (std::size_t index = 0; index < 3; ++index) {
348 channels.at(index) = *channel;
350 return Rgba{.r = channels.at(0), .g = channels.at(1), .b = channels.at(2), .a = *opacity};
359[[nodiscard]]
inline std::optional<std::string>
checkMeasure(std::string_view type,
Json const& value) {
360 bool const dimension = type ==
"dimension";
361 auto const*
const object = std::get_if<Json::object_t>(&value.data);
362 Json const*
const number =
object !=
nullptr ?
memberOf(*
object,
"value") :
nullptr;
363 Json const*
const unit =
object !=
nullptr ?
memberOf(*
object,
"unit") :
nullptr;
364 auto const*
const unitText = unit !=
nullptr ? std::get_if<std::string>(&unit->data) :
nullptr;
365 std::array<std::string_view, 2>
const units =
366 dimension ? std::array<std::string_view, 2>{
"px",
"rem"} : std::array<std::string_view, 2>{
"ms",
"s"};
367 bool const known = unitText !=
nullptr && std::ranges::find(units, *unitText) != units.end();
368 if (number !=
nullptr &&
numberOf(*number) && known) {
371 return dimension ? std::string{
"a dimension is an object of a number value and the unit px or rem"}
372 : std::string{
"a duration is an object of a number value and the unit ms or s"};
379 if (std::holds_alternative<std::string>(value.data)) {
382 auto const*
const names = std::get_if<Json::array_t>(&value.data);
383 bool const allNames = names !=
nullptr && !names->empty() && std::ranges::all_of(*names, [](
Json const& name) {
384 return std::holds_alternative<std::string>(name.data);
386 return allNames ? std::nullopt : std::optional<std::string>{
"a font family is a name or a list of names"};
393 if (
auto const number =
numberOf(value)) {
394 return *number >= 1.0 && *number <= 1000.0 ? std::nullopt
395 : std::optional<std::string>{
"a weight is 1 to 1000"};
397 auto const*
const name = std::get_if<std::string>(&value.data);
400 : std::optional<std::string>{
"not a font weight"};
408 auto const one = [](
Json const& layer) {
409 auto const*
const object = std::get_if<Json::object_t>(&layer.data);
410 if (
object ==
nullptr) {
414 if (color ==
nullptr || !
colorOf(*color)) {
417 return std::ranges::all_of(std::array<std::string_view, 4>{
"offsetX",
"offsetY",
"blur",
"spread"},
418 [object](std::string_view name) {
420 return length !=
nullptr && !
checkMeasure(
"dimension", *length);
423 if (
auto const*
const layers = std::get_if<Json::array_t>(&value.data)) {
424 if (!layers->empty() && std::ranges::all_of(*layers, one)) {
427 }
else if (one(value)) {
430 return std::string{
"a shadow is an object of a colour and the dimensions offsetX, offsetY, blur and spread"};
434 if (type ==
"shadow") {
437 if (type ==
"color") {
438 return colorOf(value) ? std::nullopt : std::optional<std::string>{
"not a colour in sRGB or hex"};
440 if (type ==
"dimension" || type ==
"duration") {
443 if (type ==
"number") {
444 return numberOf(value) ? std::nullopt : std::optional<std::string>{
"not a number"};
446 if (type ==
"fontFamily") {
449 if (type ==
"fontWeight") {
458[[nodiscard]]
inline std::string
joined(std::initializer_list<std::string_view> parts) {
460 for (std::string_view
const part : parts) {
472[[nodiscard]]
inline std::expected<std::string, std::string>
declaredType(Json::object_t
const&
object,
473 std::string_view where,
474 std::string
const& inherited) {
476 if (declared ==
nullptr) {
479 auto const*
const text = std::get_if<std::string>(&declared->data);
480 if (
text ==
nullptr) {
481 return std::unexpected(
joined({
"`", where,
"$type` is a string"}));
493[[nodiscard]]
inline std::optional<std::string>
readToken(std::string
const& path, Json::object_t
const&
object,
494 Json const& value, std::string
const& type,
TokenSet& out) {
495 auto tokenType =
declaredType(
object, path +
".", type);
497 return std::move(tokenType.error());
500 if (tokenType->empty()) {
501 if (alias && std::holds_alternative<std::string>(value.data)) {
502 out.insert_or_assign(path,
Token{.type = {}, .value = value});
505 return joined({
"the token `", path,
"` has no `$type`"});
512 return joined({
"the token `", path,
"`: ", *reason});
515 out.insert_or_assign(path,
Token{.type = std::move(*tokenType), .value = value});
523[[nodiscard]]
inline std::optional<std::string>
checkMemberName(std::string
const& prefix, std::string
const& name) {
524 if (name.starts_with(
'!')) {
525 return joined({
"the must-understand key `", prefix, name,
"` is not understood"});
527 if (name.find_first_of(
".{}") != std::string::npos) {
528 return joined({
"the token or group name `", prefix, name,
"` has `.`, `{` or `}`"});
541[[nodiscard]]
inline std::optional<std::string>
readGroup(Json::object_t
const& group, std::string
const& prefix,
542 std::string
const& inherited,
TokenSet& out) {
544 auto const type =
declaredType(group, prefix, inherited);
548 if (
memberOf(group,
"$extends") !=
nullptr) {
549 return joined({
"`", prefix,
"$extends`: group inheritance is not supported"});
551 for (
auto const& [name, member] : group) {
552 if (name.starts_with(
'$')) {
558 auto const*
const object = std::get_if<Json::object_t>(&member.data);
559 if (
object ==
nullptr) {
560 return joined({
"`", prefix, name,
"` is a token or a group, an object"});
562 std::string
const path = prefix + name;
564 auto reason = value ==
nullptr ?
readGroup(*
object, path +
".", *type, out)
565 :
readToken(path, *
object, *value, *type, out);
570 return "a token file holds more than " + std::to_string(
kMaxThemeTokens) +
" tokens";
585 [[nodiscard]] std::expected<TokenSet, std::string>
all() {
586 for (
auto const& [path, token] : *_merged) {
587 static_cast<void>(token);
588 if (
auto reason = resolve(path)) {
589 return std::unexpected(std::move(*reason));
592 return std::move(_done);
598 [[nodiscard]] std::optional<std::string> resolve(std::string
const& path) {
599 if (_done.contains(path)) {
602 if (std::ranges::find(_chain, path) != _chain.end()) {
604 for (std::string
const& step : _chain) {
605 cycle += step +
" -> ";
607 return "an alias cycle: " + cycle + path;
609 if (_chain.size() >= kMaxAliasChain) {
610 return "an alias chain longer than " + std::to_string(kMaxAliasChain) +
" at `" + path +
"`";
612 auto const found = _merged->find(path);
613 if (found == _merged->end()) {
615 return "`" + _chain.back() +
"` refers to `" + path +
"`, which no token file of the variant has";
617 _chain.push_back(path);
618 Token token = found->second;
619 if (
auto const*
const text = std::get_if<std::string>(&token.value.data)) {
620 if (
auto const target =
aliasOf(*text)) {
621 std::string
const targetPath{*target};
622 if (
auto reason = resolve(targetPath)) {
625 Token
const&
resolved = _done.at(targetPath);
626 if (!token.type.empty() && token.type !=
resolved.type) {
627 return "the " + token.type +
" token `" + path +
"` aliases the " +
resolved.type +
" token `" +
632 }
else if (
auto reason = replaceAliases(token.value)) {
636 return "the token `" + path +
"`: " + *reason;
639 _done.insert_or_assign(path, std::move(token));
645 [[nodiscard]] std::optional<std::string> replaceAliases(Json& value) {
646 if (
auto const*
const text = std::get_if<std::string>(&value.data)) {
647 if (
auto const target =
aliasOf(*text)) {
648 std::string
const targetPath{*target};
649 if (
auto reason = resolve(targetPath)) {
652 value = _done.at(targetPath).value;
656 if (
auto*
const array = std::get_if<Json::array_t>(&value.data)) {
657 for (Json& item : *array) {
658 if (
auto reason = replaceAliases(item)) {
662 }
else if (
auto*
const object = std::get_if<Json::object_t>(&value.data)) {
663 for (
auto& [name, member] : *
object) {
664 static_cast<void>(name);
665 if (
auto reason = replaceAliases(member)) {
675 std::vector<std::string> _chain;
683[[nodiscard]]
inline std::optional<std::string>
readThemeMap(Json::object_t
const& index, std::string_view key,
684 std::map<std::string, std::string, std::less<>>& out) {
686 if (member ==
nullptr) {
689 auto const*
const object = std::get_if<Json::object_t>(&member->data);
690 if (
object ==
nullptr) {
691 return "`" + std::string{key} +
"` is an object of name to token file";
693 for (
auto const& [name, file] : *
object) {
694 auto const*
const fileId = std::get_if<std::string>(&file.data);
695 if (fileId ==
nullptr || fileId->empty()) {
696 return "`" + std::string{key} +
"." + name +
"` names a token file";
698 out.insert_or_assign(name, *fileId);
708[[nodiscard]]
inline std::optional<std::string>
readIcons(Json::object_t
const& index,
709 std::map<std::string, std::string, std::less<>>& out) {
711 if (icons ==
nullptr) {
714 auto const*
const set = std::get_if<Json::object_t>(&icons->data);
715 if (set ==
nullptr) {
716 return std::string{
"`icons` is an object of icon name to icon file"};
719 return "an icon set holds at most " + std::to_string(
kMaxThemeIcons) +
" names";
721 for (
auto const& [name, file] : *set) {
723 return "`" + name +
"` is not an icon name: icon names match [a-z][a-z0-9.-]{0,63}";
725 auto const*
const fileId = std::get_if<std::string>(&file.data);
726 if (fileId ==
nullptr || fileId->empty()) {
727 return "`icons." + name +
"` names an icon file";
729 out.insert_or_assign(name, *fileId);
744 auto const*
const object = std::get_if<Json::object_t>(&json->data);
746 return vocab !=
nullptr && std::holds_alternative<std::string>(vocab->data);
758[[nodiscard]]
inline std::expected<TokenSet, std::string>
parseTokens(std::string_view
text) {
761 return std::unexpected(json.error());
763 auto const*
const object = std::get_if<Json::object_t>(&json->data);
764 if (
object ==
nullptr) {
765 return std::unexpected(std::string{
"a token file is an object of groups and tokens"});
769 return std::unexpected(std::move(*reason));
780[[nodiscard]]
inline std::expected<TokenSet, std::string>
resolveTokens(std::vector<TokenSet const*>
const& layers) {
782 for (
TokenSet const*
const layer : layers) {
783 for (
auto const& [path, token] : *layer) {
784 merged.insert_or_assign(path, token);
802 return std::unexpected(json.error());
804 auto const*
const object = std::get_if<Json::object_t>(&json->data);
805 if (
object ==
nullptr) {
806 return std::unexpected(std::string{
"a theme index is an object"});
809 return std::unexpected(std::move(*reason));
812 auto const*
const vocabText = vocab !=
nullptr ? std::get_if<std::string>(&vocab->data) :
nullptr;
814 return std::unexpected(std::string{
"a theme index's vocab is ui-theme/1"});
817 for (
auto const& [key, member] : *
object) {
818 if (key ==
"vocab" || key ==
"density" || key ==
"surfaces" || key ==
"icons") {
821 auto const*
const fileId = std::get_if<std::string>(&member.data);
822 if (fileId ==
nullptr || fileId->empty()) {
823 return std::unexpected(
"the variant `" + key +
"` names a token file");
826 index.
base = *fileId;
828 index.
variants.insert_or_assign(key, *fileId);
831 if (index.
base.empty()) {
832 return std::unexpected(std::string{
"a theme index names its `base` token file"});
835 return std::unexpected(std::move(*reason));
838 return std::unexpected(std::move(*reason));
841 return std::unexpected(std::move(*reason));
845 return std::unexpected(
"a theme index lists more than " + std::to_string(
kMaxThemeVariants) +
846 " variants, densities or surfaces");
865 std::string_view variant,
866 std::string_view density,
867 std::string_view surface) {
869 std::vector<TokenSet const*> layers;
870 auto const layer = [&](std::string
const& fileId) -> std::optional<std::string> {
871 TokenSet const*
const tokens = files(fileId);
872 if (tokens ==
nullptr) {
873 return "the token file `" + fileId +
"` is not in the bundle";
875 layers.push_back(tokens);
878 auto const named = [&](std::map<std::string, std::string, std::less<>>
const& from, std::string_view name,
879 std::string_view what) -> std::optional<std::string> {
883 auto const found = from.find(name);
884 if (found == from.end()) {
885 return "the theme has no " + std::string{what} +
" `" + std::string{name} +
"`";
887 return layer(found->second);
889 std::optional<std::string> reason = layer(index.
base);
891 reason = named(index.
variants, variant,
"variant");
894 reason = named(index.
densities, density,
"density");
897 reason = named(index.
surfaces, surface,
"surface");
900 return std::unexpected(std::move(*reason));
911 auto const namesOf = [](std::map<std::string, std::string, std::less<>>
const& from) {
912 std::vector<std::string> names{std::string{}};
913 for (
auto const& [name, fileId] : from) {
914 static_cast<void>(fileId);
915 names.push_back(name);
920 auto const label = [](std::string
const& variant, std::string
const& density, std::string
const& surface) {
921 std::string out =
"base";
922 if (!variant.empty()) {
923 out +=
" + " + variant;
925 if (!density.empty()) {
926 out +=
" + density " + density;
928 if (!surface.empty()) {
929 out +=
" + surface " + surface;
933 std::vector<std::string>
const surfaces = namesOf(index.
surfaces);
934 for (std::string
const& variant : namesOf(index.
variants)) {
935 for (std::string
const& density : namesOf(index.
densities)) {
936 for (std::string
const& surface : surfaces) {
937 auto const resolved =
resolveTheme(index, files, variant, density, surface);
939 return label(variant, density, surface) +
": " + resolved.error();
1028 {.path =
"elevation.card",
1032 .shadow = {.color =
"#00000014", .offsetX = 0.0, .offsetY = 2.0, .blur = 10.0, .spread = 0.0}},
1054 std::map<std::string, Rgba, std::less<>>
colors;
1056 std::map<std::string, double, std::less<>>
numbers;
1060 std::map<std::string, ThemeShadow, std::less<>>
shadows;
1062 std::map<std::string, std::vector<std::string>, std::less<>>
families;
1070[[nodiscard]]
inline std::optional<ThemeShadow>
shadowOf(
Json const& json) {
1071 Json const* layer = &json;
1072 if (
auto const*
const layers = std::get_if<Json::array_t>(&json.data)) {
1073 if (layers->empty()) {
1074 return std::nullopt;
1076 layer = &layers->front();
1078 auto const*
const object = std::get_if<Json::object_t>(&layer->data);
1079 if (
object ==
nullptr) {
1080 return std::nullopt;
1083 auto const rgba = color !=
nullptr ?
colorOf(*color) : std::nullopt;
1084 auto const length = [object](std::string_view name) -> std::optional<double> {
1086 return member !=
nullptr ?
pixelsOf(*member) : std::nullopt;
1088 auto const x = length(
"offsetX");
1089 auto const y = length(
"offsetY");
1090 auto const blur = length(
"blur");
1091 auto const spread = length(
"spread");
1092 if (!rgba || !x || !y || !blur || !spread) {
1093 return std::nullopt;
1095 return ThemeShadow{.color = *rgba, .offsetX = *x, .offsetY = *y, .blur = *blur, .spread = *spread};
1102 std::vector<std::string> names;
1103 if (
auto const*
const name = std::get_if<std::string>(&json.data)) {
1104 names.push_back(*name);
1105 }
else if (
auto const*
const list = std::get_if<Json::array_t>(&json.data)) {
1106 for (
Json const& entry : *list) {
1107 if (
auto const*
const listed = std::get_if<std::string>(&entry.data)) {
1108 names.push_back(*
listed);
1124 auto const found = resolved.find(path);
1125 return found == resolved.end() ? nullptr : &found->second;
1133 if (found !=
nullptr && found->
type ==
"fontFamily") {
1136 std::vector<std::string> names;
1137 if (!spec.
family.empty()) {
1138 names.emplace_back(spec.
family);
1153 for (ThemeTokenSpec
const& spec : kThemeTokens) {
1154 Token
const*
const token = detail::tokenAt(resolved, spec.path);
1155 if (spec.kind == ThemeTokenKind::Color) {
1156 std::optional<Rgba> color;
1157 if (token !=
nullptr && token->type ==
"color") {
1158 color = colorOf(token->value);
1160 values.colors.insert_or_assign(std::string{spec.path},
1163 std::optional<ThemeShadow> shadow;
1164 if (token !=
nullptr && token->type ==
"shadow") {
1167 values.shadows.insert_or_assign(
1168 std::string{spec.path},
1170 .offsetX = spec.shadow.offsetX,
1171 .offsetY = spec.shadow.offsetY,
1172 .blur = spec.shadow.blur,
1173 .spread = spec.shadow.spread}));
1175 values.families.insert_or_assign(std::string{spec.path},
detail::familyValue(spec, token));
1177 std::optional<double> pixels;
1178 if (token !=
nullptr && token->type ==
"dimension") {
1181 values.dimensions.insert_or_assign(std::string{spec.path}, pixels.value_or(spec.number));
1183 std::optional<double> number;
1184 if (token !=
nullptr && token->type ==
"number") {
1187 values.numbers.insert_or_assign(std::string{spec.path}, number.value_or(spec.number));
What the UI service's client and server share (spec 5 §12): content hashes, the bundle's manifest,...
std::optional< std::string > mustUnderstand(Json const &json, std::string_view where)
The must-understand key (!name) of one object, if it has one.
Handle< Member > at(Member Class::*member) const
A field of the value, by member pointer: a renamed field is a compile error.
Resolves tokens' aliases, depth first, each token once.
std::expected< TokenSet, std::string > all()
Resolves every token.
AliasResolver(TokenSet const &merged)
A resolver over merged.
bool listed(std::span< std::string_view const > names, std::string_view name)
Whether a list of names holds one.
Resolved const & resolved()
The process registries, matched and validated on the first call.
std::shared_ptr< ModelChannel > channelOf(ModelAlias &alias, ScreenCore &core)
The channel an alias is bound to for its current token, binding it when the token changed.
std::optional< std::pair< math::Rational, std::uint32_t > > numberOf(Value const &value)
The places a number is shown with: an int has none, a decimal or quantity its declared places.
A colour, 8 bits per channel.
std::uint8_t a
Alpha; 255 is opaque.
bool operator==(Rgba const &) const =default
Field-wise equality.
std::string hex() const
The colour as #rrggbb, or #rrggbbaa when it is not opaque.
The index of a theme: which token files make up each variant.
std::string base
The file every variant layers over.
std::map< std::string, std::string, std::less<> > densities
Densities, such as comfortable and compact, each one file.
std::map< std::string, std::string, std::less<> > icons
The icon set: icon name to the bundle's icon file that draws it; empty for none.
std::map< std::string, std::string, std::less<> > surfaces
Named surfaces, such as sidebar, each one file that overrides the variant for a subtree.
std::map< std::string, std::string, std::less<> > variants
Named variants, such as light and dark, each one file.
A shadow token's default, in the form a constexpr table can hold.
std::string_view color
The colour, #rrggbbaa.
double offsetX
The horizontal offset, in px.
double spread
The spread, in px.
double blur
The blur radius, in px.
double offsetY
The vertical offset, in px.
A shadow of the set, resolved.
double blur
The blur radius, in px.
double offsetY
The vertical offset, in px.
double offsetX
The horizontal offset, in px.
double spread
The spread, in px.
One token of the closed ui-theme/1 set: its path, kind and the MorphUi default.
double number
A number's default, or a dimension's in px.
ThemeTokenKind kind
How it is read.
ThemeShadowSpec shadow
A shadow's default.
std::string_view path
The dotted path.
std::string_view family
A font family's default; empty for none, which draws in the platform font.
std::string_view color
A colour's default, #rrggbb; empty for a number.
The ui-theme/1 set's values for one resolved theme.
std::map< std::string, std::vector< std::string >, std::less<> > families
Every font family list of the set, by path, in order of preference; empty for none.
std::map< std::string, ThemeShadow, std::less<> > shadows
Every shadow of the set, by path.
std::map< std::string, double, std::less<> > dimensions
Every dimension of the set, by path, in px.
std::map< std::string, double, std::less<> > numbers
Every number of the set, by path.
std::map< std::string, Rgba, std::less<> > colors
Every colour of the set, by path.
One token: its DTCG type and its value, which may hold aliases until the set is resolved.
std::string type
The $type, own or inherited; empty for an alias whose type is its target's.
std::optional< std::string_view > aliasOf(std::string_view text)
Whether a string is an alias, {a.b}.
std::map< std::string, Token, std::less<> > TokenSet
Tokens by their dotted path, such as color.accent.
std::expected< std::string, std::string > declaredType(Json::object_t const &object, std::string_view where, std::string const &inherited)
The $type an object declares, or the one it inherits.
std::optional< std::string > readIcons(Json::object_t const &index, std::map< std::string, std::string, std::less<> > &out)
Reads a theme index's icons: an object of icon name (isIconName) to icon file id, at most kMaxThemeIc...
std::optional< std::string > checkTheme(ThemeIndex const &index, TokenFileLookup const &files)
Checks that every combination of a theme resolves: each variant (or none), density (or none) and surf...
bool isIconName(std::string_view name)
Whether name is an icon name: [a-z][a-z0-9.-]{0,63}.
constexpr std::array< std::string_view, 18 > kFontWeights
The named font weights of DTCG.
constexpr std::array< std::string_view, 13 > kKnownTokenTypes
The types the client checks; a token of another $type the client still knows is accepted unchecked.
bool isThemeIndex(std::string_view text)
Whether a theme file is a theme index rather than a token file: its top level has a string vocab.
Json const * memberOf(Json::object_t const &object, std::string_view name)
A member of a JSON object.
std::string_view iconGlyph(std::string_view name)
The glyph a text renderer draws for a framework icon name.
std::expected< TokenSet, std::string > resolveTokens(std::vector< TokenSet const * > const &layers)
Layers token files, later ones winning per path, and resolves every alias.
bool holdsAlias(Json const &json)
Whether a value holds an alias anywhere.
ThemeValues themeValues(TokenSet const &resolved={})
Fills the closed ui-theme/1 set from a resolved theme.
std::vector< std::string > familyValue(ThemeTokenSpec const &spec, Token const *found)
A font.family token's value: the theme's names when it has the token as a fontFamily,...
std::optional< ThemeShadow > shadowOf(Json const &json)
A checked shadow value as a ThemeShadow; of a list of layers, the first.
std::expected< TokenSet, std::string > parseTokens(std::string_view text)
Parses a DTCG 2025.10 token file.
std::string joined(std::initializer_list< std::string_view > parts)
Joins text, for a refusal built inside a loop.
std::vector< std::string > familiesOf(Json const &json)
A checked fontFamily value as its names, in order: one name, or a list of them.
constexpr std::size_t kMaxAliasChain
The longest chain of aliases a token may resolve through.
std::optional< std::string > readGroup(Json::object_t const &group, std::string const &prefix, std::string const &inherited, TokenSet &out)
Reads the tokens of one group into out.
std::optional< Rgba > hexColor(std::string_view text)
Reads #rgb, #rrggbb or #rrggbbaa.
std::optional< std::string > checkMemberName(std::string const &prefix, std::string const &name)
Refuses a member name a token file may not have.
std::optional< std::string > checkMeasure(std::string_view type, Json const &value)
Checks a dimension or a duration: an object of a number value and a known unit.
std::optional< std::string > readToken(std::string const &path, Json::object_t const &object, Json const &value, std::string const &type, TokenSet &out)
Reads one token into out.
constexpr std::size_t kMaxThemeIcons
The most names a theme index's icon set may hold.
std::function< TokenSet const *(std::string_view fileId)> TokenFileLookup
Finds a token file of the bundle by id.
std::optional< Rgba > colorOf(Json const &json)
Reads a colour value: #rgb, #rrggbb, #rrggbbaa, or the DTCG 2025.10 object, by its hex or by its srgb...
std::optional< double > pixelsOf(Json const &json)
A DTCG dimension value in px.
std::optional< std::string > checkFontFamily(Json const &value)
Checks a fontFamily: a name, or a non-empty list of names.
constexpr double kPixelsPerRem
How many px a rem is: the default root font size, which the renderers do not change.
constexpr std::array< std::string_view, 11 > kFrameworkIcons
The icon names the built-in components draw: an empty state without an icon of its own,...
constexpr std::array< ThemeTokenSpec, 40 > kThemeTokens
The closed ui-theme/1 token set, with the MorphUi defaults: the colour roles, the opacity and the spa...
std::optional< std::string > readThemeMap(Json::object_t const &index, std::string_view key, std::map< std::string, std::string, std::less<> > &out)
Reads a map of name to file id from a theme index.
std::expected< ThemeIndex, std::string > parseThemeIndex(std::string_view text)
Parses a theme index: {"vocab": "ui-theme/1", "base": id, "dark": id, …, "density": {....
constexpr std::size_t kMaxThemeTokens
The most tokens one token file may hold.
constexpr std::size_t kMaxThemeVariants
The most variants, densities or surfaces a theme index may list, each.
ThemeTokenKind
What a ui-theme/1 token is read as.
@ Shadow
A shadow token: a colour and four px lengths; of a list of layers, the first.
@ Dimension
A dimension token, read in px (a rem is 16 px).
@ FontFamily
A fontFamily token: a family name, or a list of them in order of preference.
std::optional< std::string > checkShadow(Json const &value)
Checks a shadow: an object of a color and the dimensions offsetX, offsetY, blur and spread,...
Token const * tokenAt(TokenSet const &resolved, std::string_view path)
A resolved theme's token at a path.
std::optional< std::string > checkFontWeight(Json const &value)
Checks a fontWeight: 1 to 1000, or a DTCG weight name.
std::expected< TokenSet, std::string > resolveTheme(ThemeIndex const &index, TokenFileLookup const &files, std::string_view variant, std::string_view density, std::string_view surface)
Resolves one combination of a theme: base, then the variant, the density and the surface,...
std::optional< std::string > checkTokenValue(std::string_view type, Json const &value)
Whether a resolved value has its type's shape.
constexpr std::string_view kThemeVocabulary
The vocabulary of a theme index.
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
std::expected< Json, std::string > parseJson(std::string_view text)
Parses JSON text into the document layer's JSON tree.
glz::generic_u64 Json
The JSON tree the document layer reads and writes: glaze's generic value in its integer-preserving mo...
Node text(Text spec)
Static or bound text.