///|
pub fn parse_brand_document(text : String) -> BrandDocumentResult {
let trimmed = text.trim().to_owned()
if trimmed == "" {
return {
value: None,
diagnostics: DiagnosticBag::empty().push(
Diagnostic::error("$", "document is empty", None),
),
}
}
if trimmed[0] == '{' || trimmed[0] == '[' {
let parsed = parse_json(text)
match parsed.value {
Some(value) if !parsed.diagnostics.has_errors() => build_brand_book(value)
_ => { value: None, diagnostics: parsed.diagnostics }
}
} else {
let parsed = parse_yaml(text)
match parsed.value {
Some(value) if !parsed.diagnostics.has_errors() => build_brand_book(value)
_ => { value: None, diagnostics: parsed.diagnostics }
}
}
}
///|
pub fn parse_brand_document_as(
text : String,
format : DocumentFormat,
) -> BrandDocumentResult {
match format {
Auto => parse_brand_document(text)
Json => {
let parsed = parse_json(text)
match parsed.value {
Some(value) if !parsed.diagnostics.has_errors() =>
build_brand_book(value)
_ => { value: None, diagnostics: parsed.diagnostics }
}
}
Yaml => {
let parsed = parse_yaml(text)
match parsed.value {
Some(value) if !parsed.diagnostics.has_errors() =>
build_brand_book(value)
_ => { value: None, diagnostics: parsed.diagnostics }
}
}
}
}
///|
fn build_brand_book(root : JsonValue) -> BrandDocumentResult {
let diagnostics : Array[Diagnostic] = []
let object = match get_object(Some(root), "$", diagnostics) {
Some(value) => value
None => return { value: None, diagnostics: { items: diagnostics } }
}
let name = get_required_string(object, "name", "name", diagnostics)
let tagline = get_optional_string(object, "tagline", "")
let voice_object = match object.object_field("voice") {
None => None
Some(value) => get_object(Some(value), "voice", diagnostics)
}
let voice = match voice_object {
Some(value) =>
Voice::{
tone: get_optional_string(value, "tone", ""),
writing_principles: string_array(
value.object_field("writing_principles"),
"voice.writing_principles",
diagnostics,
),
}
None => Voice::{ tone: "", writing_principles: [] }
}
let palette_object = get_object(
object.object_field("palette"),
"palette",
diagnostics,
)
let palette = match palette_object {
Some(value) =>
Palette::{
colors: build_colors(value.object_field("colors"), diagnostics),
gradients: build_gradients(value.object_field("gradients"), diagnostics),
}
None => Palette::{ colors: [], gradients: [] }
}
let assets_object = get_object(
object.object_field("assets"),
"assets",
diagnostics,
)
let assets = match assets_object {
Some(value) =>
AssetSet::{
primary_logo: build_logo(
value.object_field("primary_logo"),
"assets.primary_logo",
diagnostics,
),
alternate_logos: build_logos(
value.object_field("alternate_logos"),
"assets.alternate_logos",
diagnostics,
),
imagery_style: get_optional_string(value, "imagery_style", ""),
}
None =>
AssetSet::{
primary_logo: default_logo(),
alternate_logos: [],
imagery_style: "",
}
}
let typography_object = get_object(
object.object_field("typography"),
"typography",
diagnostics,
)
let typography = match typography_object {
Some(value) =>
Typography::{
families: build_fonts(value.object_field("families"), diagnostics),
scale: build_scale(value.object_field("scale"), diagnostics),
}
None => Typography::{ families: [], scale: [] }
}
let metadata_object = object.object_field("metadata")
let metadata = match metadata_object {
Some(value) =>
match value {
JObject(_) =>
Meta::{
source: get_optional_string(value, "source", ""),
maintainer: get_optional_string(value, "maintainer", ""),
license: get_optional_string(value, "license", "Apache-2.0"),
}
_ => {
add_document_error(diagnostics, "metadata", "expected an object")
Meta::{ source: "", maintainer: "", license: "Apache-2.0" }
}
}
None => Meta::{ source: "", maintainer: "", license: "Apache-2.0" }
}
let components = build_components(
object.object_field("components"),
diagnostics,
)
let forbidden = build_misuse_rules(
object.object_field("forbidden"),
diagnostics,
)
let book = BrandBook::{
name,
tagline,
voice,
assets,
palette,
typography,
components,
forbidden,
metadata,
}
if diagnostics.any(fn(item) { item.severity == Error }) {
{ value: None, diagnostics: { items: diagnostics } }
} else {
{ value: Some(book), diagnostics: { items: diagnostics } }
}
}
///|
fn string_array(
value : JsonValue?,
path : String,
diagnostics : Array[Diagnostic],
) -> Array[String] {
let result : Array[String] = []
for item in get_array(value, path, diagnostics) {
match item {
JString(text) => result.push(text)
_ => add_document_error(diagnostics, path, "array items must be strings")
}
}
result
}
///|
fn build_colors(
value : JsonValue?,
diagnostics : Array[Diagnostic],
) -> Array[ColorToken] {
let result : Array[ColorToken] = []
for i, item in get_array(value, "palette.colors", diagnostics) {
match item {
JObject(_) => {
let name = get_required_string(
item,
"name",
"palette.colors[" + i.to_string() + "].name",
diagnostics,
)
let color = get_required_string(
item,
"value",
"palette.colors[" + i.to_string() + "].value",
diagnostics,
)
let role = get_optional_string(item, "role", "")
if !is_hex_color(color) {
add_document_error(
diagnostics,
"palette.colors[" + i.to_string() + "].value",
"expected six-digit hex color",
)
}
result.push(ColorToken::hex(name, color, role~))
}
_ =>
add_document_error(
diagnostics,
"palette.colors[" + i.to_string() + "].",
"expected an object",
)
}
}
result
}
///|
fn build_gradients(
value : JsonValue?,
diagnostics : Array[Diagnostic],
) -> Array[GradientToken] {
let result : Array[GradientToken] = []
for i, item in get_array(value, "palette.gradients", diagnostics) {
match item {
JObject(_) =>
result.push(GradientToken::{
name: get_required_string(
item,
"name",
"palette.gradients[" + i.to_string() + "].name",
diagnostics,
),
stops: string_array(
item.object_field("stops"),
"palette.gradients[" + i.to_string() + "].stops",
diagnostics,
),
angle: get_int(
item,
"angle",
"palette.gradients[" + i.to_string() + "].angle",
diagnostics,
true,
),
usage: get_optional_string(item, "usage", ""),
})
_ =>
add_document_error(
diagnostics,
"palette.gradients[" + i.to_string() + "]",
"expected an object",
)
}
}
result
}
///|
fn build_logo(
value : JsonValue?,
path : String,
diagnostics : Array[Diagnostic],
) -> Logo {
let object = get_object(value, path, diagnostics)
match object {
Some(item) =>
Logo::{
name: get_required_string(item, "name", path + ".name", diagnostics),
path: get_required_string(item, "path", path + ".path", diagnostics),
min_width_px: get_int(
item,
"min_width_px",
path + ".min_width_px",
diagnostics,
true,
),
clear_space: Pixels(
get_int(
item,
"clear_space_px",
path + ".clear_space_px",
diagnostics,
true,
),
),
background: parse_background(
get_optional_string(item, "background", "light_or_dark"),
),
}
None => default_logo()
}
}
///|
fn build_logos(
value : JsonValue?,
path : String,
diagnostics : Array[Diagnostic],
) -> Array[Logo] {
let result : Array[Logo] = []
for i, item in get_array(value, path, diagnostics) {
result.push(
build_logo(Some(item), path + "[" + i.to_string() + "]", diagnostics),
)
}
result
}
///|
fn build_fonts(
value : JsonValue?,
diagnostics : Array[Diagnostic],
) -> Array[FontToken] {
let result : Array[FontToken] = []
for i, item in get_array(value, "typography.families", diagnostics) {
match item {
JObject(_) =>
result.push(FontToken::{
name: get_required_string(
item,
"name",
"typography.families[" + i.to_string() + "].name",
diagnostics,
),
fallback: get_optional_string(
item, "fallback", "system-ui, sans-serif",
),
usage: get_optional_string(item, "usage", ""),
})
_ =>
add_document_error(
diagnostics,
"typography.families[" + i.to_string() + "]",
"expected an object",
)
}
}
result
}
///|
fn build_scale(
value : JsonValue?,
diagnostics : Array[Diagnostic],
) -> Array[TypeStep] {
let result : Array[TypeStep] = []
for i, item in get_array(value, "typography.scale", diagnostics) {
match item {
JObject(_) =>
result.push(TypeStep::{
name: get_required_string(
item,
"name",
"typography.scale[" + i.to_string() + "].name",
diagnostics,
),
size_px: get_int(
item,
"size_px",
"typography.scale[" + i.to_string() + "].size_px",
diagnostics,
true,
),
line_height_px: get_int(
item,
"line_height_px",
"typography.scale[" + i.to_string() + "].line_height_px",
diagnostics,
true,
),
})
_ =>
add_document_error(
diagnostics,
"typography.scale[" + i.to_string() + "]",
"expected an object",
)
}
}
result
}
///|
fn build_components(
value : JsonValue?,
diagnostics : Array[Diagnostic],
) -> Array[ComponentSpec] {
let result : Array[ComponentSpec] = []
for i, item in get_array(value, "components", diagnostics) {
match item {
JObject(_) =>
result.push(ComponentSpec::{
name: get_required_string(
item,
"name",
"components[" + i.to_string() + "].name",
diagnostics,
),
purpose: get_optional_string(item, "purpose", ""),
anatomy: string_array(
item.object_field("anatomy"),
"components[" + i.to_string() + "].anatomy",
diagnostics,
),
tokens: string_array(
item.object_field("tokens"),
"components[" + i.to_string() + "].tokens",
diagnostics,
),
states: string_array(
item.object_field("states"),
"components[" + i.to_string() + "].states",
diagnostics,
),
})
_ =>
add_document_error(
diagnostics,
"components[" + i.to_string() + "]",
"expected an object",
)
}
}
result
}
///|
fn build_misuse_rules(
value : JsonValue?,
diagnostics : Array[Diagnostic],
) -> Array[MisuseRule] {
let result : Array[MisuseRule] = []
for i, item in get_array(value, "forbidden", diagnostics) {
match item {
JObject(_) =>
result.push(MisuseRule::{
title: get_required_string(
item,
"title",
"forbidden[" + i.to_string() + "].title",
diagnostics,
),
reason: get_optional_string(item, "reason", ""),
replacement: get_optional_string(item, "replacement", ""),
})
_ =>
add_document_error(
diagnostics,
"forbidden[" + i.to_string() + "]",
"expected an object",
)
}
}
result
}
///|
fn parse_background(text : String) -> BackgroundRule {
match normalize_identifier(text) {
"light-only" => LightOnly
"dark-only" => DarkOnly
"monochrome" => Monochrome
_ => LightOrDark
}
}
///|
fn default_logo() -> Logo {
Logo::{
name: "invalid",
path: "",
min_width_px: 0,
clear_space: Pixels(0),
background: LightOrDark,
}
}