NAME

Term::Fabulous::Theme::Document - A theme file you can read, check and change, keeping its comments and layout

SYNOPSIS

use Term::Fabulous::Theme::Document;

my $document = Term::Fabulous::Theme::Document->read_file('themes/ocean.kdl');

# Is it a theme Term::Fabulous can load?
foreach my $error ( $document->errors ) {
	printf "line %d: %s\n", $error->{line}, $error->{message};
}
my $theme = $document->theme;    # a Term::Fabulous::Theme, or undef

# What it sets, and what a key is worth when it sets nothing.
say $document->setting('palette.accent');                       # #5fd3c0
say $document->effective_setting('button.border.color.focused')->{spec};    # accent

# Changes return a new document; the old one stays as it was.
my $warmer = $document
	->with_setting( 'palette.accent', '#ff9e64' )
	->with_setting( 'button[primary].text', 'accent' )
	->without_setting('divider.line.style');
$warmer->write_file('themes/ocean-warm.kdl');

DESCRIPTION

A theme file (see "THEME FILES" in Term::Fabulous::Theme) is a KDL document. This class reads one as text, checks it the way Term::Fabulous::Theme does, and tells for every mistake the line it is on, which Term::Fabulous::Theme->from_file cannot. It also changes settings in the text: a change rewrites only the lines that hold the changed setting and adds new settings where they belong (a slot in its family's block, a state in its state block), so comments, blank lines, indentation, line ends and the order of everything else stay as they were. A setting removed with "without_setting" takes the comment right above it along, and a block that becomes empty goes too.

Term::Fabulous::ThemeEditor works on this class: its form changes the text with "with_setting", and its text mode shows the "errors" at their lines. Use it on its own to check theme files in a test or to change them from a script.

A document never changes. "with_setting", "without_setting", "with_settings" and "without_variant" return a new document with the new text; reading it again with "parse" gives the same document.

Setting keys

A setting is named by a key:

theme.name                          the name in the theme node
theme.extends                       the built-in theme it starts from
palette.TOKEN                       a palette token: palette.accent
FAMILY.SLOT                         a slot in the normal state: button.border.color
FAMILY.SLOT.STATE                   a slot in a state: button.border.color.focused
FAMILY[VARIANT].SLOT[.STATE]        a slot of a variant: button[primary].text

The families, slots, states and tokens are those of "VOCABULARY" in Term::Fabulous::Theme; an unknown name dies, naming the known ones. FAMILY.SLOT.normal is the same key as FAMILY.SLOT. A key names one setting, wherever it is in the file: border style=Round color="border" in the button block holds the keys button.border.style and button.border.color, and focused { text "accent" } in it holds button.text.focused.

Values

Values are strings as the file holds them: a token name (accent), a color in a format Term::Fabulous::Color reads (#5fd3c0), a border style name (Round), none (also for #null in the file), reverse, or true or false for border.enabled, which the file holds as #true or #false (true or false in a KDL 1 file). "with_setting" checks a value as the theme does before it writes it, so a document never gets a value its slot cannot take from this class.

CONSTRUCTORS

parse

my $document = Term::Fabulous::Theme::Document->parse($kdl);

A document of a character string. It never dies for what the text holds: mistakes become "errors". Dies only when the text is not a string. An empty string is an empty theme (one that changes nothing in the built-in dark).

read_file

my $document = Term::Fabulous::Theme::Document->read_file($path);

A document of a file, read as UTF-8; a byte order mark at its start is dropped. Dies when the file cannot be read or is not UTF-8 text.

METHODS

text

The text of the document, a character string.

errors

foreach my $error ( $document->errors ) {
	warn "theme.kdl line $error->{line}: $error->{message}\n";
}

Every mistake in the text, sorted by line, each a hash reference with line (counted from 1) and message. A text that is not valid KDL has a single error, at the line where the KDL parser stopped, with a message that starts with KDL syntax:. Otherwise every mistake of the theme is listed: an unknown node, token, family, slot or state, a value its slot cannot take, a setting given twice. Empty for a valid theme.

is_valid

1 when "errors" is empty, else 0.

has_syntax_error

1 when the text is not valid KDL, else 0. Such a document cannot be changed: the change methods die.

theme

my $theme = $document->theme;

The Term::Fabulous::Theme the text describes, built by "from_string" in Term::Fabulous::Theme when first asked for, or undef when the text has errors.

name

The theme's name (theme.name), or undef.

extends

The built-in theme it starts from: theme.extends, or dark when the text does not say.

kdl_version

2, or 1 for a text that only KDL 1 reads. Changes to a KDL 1 text are written in KDL 1.

setting

my $value = $document->setting('button.border.color.focused');

The value the text gives a key (see "Values"), or undef when it gives none. Dies for a key that is not one (see "Setting keys").

setting_keys

The keys the text sets, in the order they appear in it.

variants

foreach my $variant ( $document->variants ) {
	my ( $family, $name ) = @$variant;
}

The variants the text defines with at least one setting, as [ family, name ] pairs in the order they appear in it.

line_of

my $line = $document->line_of('palette.accent');
my $line = $document->line_of('button[primary]');

The line a key is set on, or the line of a variant's first block given as FAMILY[VARIANT]; undef when the text does not set it.

effective_setting

my $effective = $document->effective_setting('button.text.hovered');
# { spec => 'text', origin => 'normal' }

What a key is worth in the theme, as a hash reference with spec (the value, see "Values") and origin, which says where it comes from:

set

the text sets it;

dark or light

a palette token the text does not set: the base theme's color, as #rrggbb (or #rrggbbaa for a translucent one);

default

a slot the text does not set: the vocabulary's default ("slot_info" in Term::Fabulous::Theme), the same in both base themes;

normal

a state that looks like the slot's normal state, unless a theme sets it: spec is the normal state's value;

family

a slot of a variant that the variant does not set: the family's value.

theme.name without a name has the spec undef; theme.extends without one is dark (default).

with_setting

my $changed = $document->with_setting( 'button.border.color.focused', 'danger' );

A new document in which the key has the value. The value is checked as the theme checks it and dies with the reason when the slot cannot take it (Term::Fabulous::Theme::Document: tabs.line.style cannot be 'none'); undef dies, see "without_setting". A key the text sets gets the new value in its place; the line holding it is written anew, the rest of the text stays. A value for border.enabled may also be 1 or 0; the file gets #true or #false. A new key is added: to the family's first block (made at the end of the text when there is none), in its state block or variant block (made as needed), next to a node with the same head (border color="accent" gets style=Round added); a token to the palette block, the name and the base to the theme node. Dies when the text is not valid KDL ("has_syntax_error"); other errors do not stop changes.

without_setting

my $changed = $document->without_setting('palette.accent');

A new document without the key's setting, so the key falls back to what "effective_setting" then says. The node holding it goes (or the property, when the node holds others), with the comment right above it; a palette, family, state or variant block left without settings goes too. A key the text does not set returns the document itself.

with_settings

my $changed = $document->with_settings( [
	[ 'palette.accent',     '#ff9e64' ],
	[ 'button.text.hovered', undef ],      # removed
] );

Several changes in order, each as "with_setting", or as "without_setting" for undef.

without_variant

my $changed = $document->without_variant( 'button', 'primary' );

A new document without the variant's blocks and everything in them; the document itself when it has none. Dies for an unknown family.

write_file

$document->write_file('themes/ocean.kdl');

Writes the text to a file as UTF-8 and returns the document. The text goes to a temporary file in the same directory first, which is then renamed over the target, so the target is never half written; an existing target keeps its permissions. Dies when the file cannot be written.

FUNCTIONS

parse_key

my $parsed = Term::Fabulous::Theme::Document::parse_key('button[primary].text.focused');
# { section => 'slot', family => 'button', variant => 'primary', slot => 'text', state => 'focused' }

A key taken apart: section is theme (with field), palette (with token) or slot (with family, variant or undef, slot and state). Dies for a key that is not one.

key_for

my $key = Term::Fabulous::Theme::Document::key_for( family => 'button', slot => 'text', state => 'focused' );

The key of the parts "parse_key" returns; a normal state is left out.

SEE ALSO

Term::Fabulous::Theme, Term::Fabulous::ThemeEditor, "THEMES" in Term::Fabulous::Manual::Looks, Text::KDL::XS.