The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

$File::Sticker::Scribe::YamlPrefix::VERSION = '4.0101';
=head1 NAME
File::Sticker::Scribe::YamlPrefix - write and standardize meta-data from YAML file
=head1 VERSION
version 4.0101
=head1 SYNOPSIS
use File::Sticker::Scribe::YamlPrefix;
my $obj = File::Sticker::Scribe::YamlPrefix->new(%args);
my %meta = $obj->read_meta($filename);
$obj->write_meta(%args);
=head1 DESCRIPTION
This will read and write meta-data from plain text files where the first part
of the file contains YAML data, set up as if it is a YAML stream. That is, the
file starts with '---' on one line, then there is YAML data, then there is
another '---' line, and all content after that is ignored. Then it will
standardize it to a common nomenclature, such as "tags" for things called tags,
or Keywords or Subject etc.
This format can be useful as a way of storing meta-data in documents or in
wiki pages.
=cut
# FOR DEBUGGING
=head1 DEBUGGING
=head2 whoami
Used for debugging info
=cut
sub whoami { ( caller(1) )[3] }
=head1 METHODS
=head2 priority
The priority of this scribe. Scribes with higher priority get tried first.
=cut
sub priority {
my $class = shift;
return 1;
} # priority
=head2 allowed_file
If this scribe can be used for the given file, then this returns true.
File must be plain text and NOT end with '.yml'
If the file does not exist, it cannot be written to.
If it does exist, the YAML-prefix area must exist also.
=cut
sub allowed_file {
my $self = shift;
my $file = shift;
say STDERR whoami(), " file=$file" if $self->{verbose} > 2;
my $ft = $self->{file_magic}->info_from_filename($file);
# This needs to be a plain text file
# We don't want to include .yml files because they are dealt with separately
if (-r $file
and $ft->{mime_type} =~ m{^text/plain}
and $file !~ /\.yml$/)
{
return 1;
}
return 0;
} # allowed_file
=head2 allowed_fields
If this scribe can be used for the known and wanted fields, then this returns true.
For YAML, this always returns true.
if ($scribe->allowed_fields())
{
....
}
=cut
sub allowed_fields {
my $self = shift;
return 1;
} # allowed_fields
=head2 known_fields
Returns the fields which this scribe knows about.
This scribe has no limitations.
my $known_fields = $scribe->known_fields();
=cut
sub known_fields {
my $self = shift;
if ($self->{wanted_fields})
{
return $self->{wanted_fields};
}
return {};
} # known_fields
=head2 read_meta
Read the meta-data from the given file.
my $meta = $obj->read_meta($filename);
=cut
sub read_meta {
my $self = shift;
my $filename = shift;
say STDERR whoami(), " filename=$filename" if $self->{verbose} > 2;
my ($yaml_str,$more) = $self->_yaml_and_more($filename);
my %meta = ();
my $info;
eval {$info = Load($yaml_str);};
if ($@)
{
warn __PACKAGE__, " Load of data failed: $@";
say "======\n$yaml_str\n=====" if $self->{verbose} > 1;
return \%meta;
}
if (!$info)
{
warn __PACKAGE__, " no legal YAML";
return \%meta;
}
# now standardize the meta-data
foreach my $key (sort keys %{$info})
{
my $val = $info->{$key};
if ($val)
{
if ($key eq 'tags')
{
$meta{tags} = $val;
# If there are no commas, change spaces to commas. This is
# because if we are using commas to separate, we allow
# multi-word tags with spaces in them, so we don't want to turn
# those spaces into commas!
if ($meta{tags} !~ /,/)
{
$meta{tags} =~ s/ /,/g; # spaces to commas
}
}
elsif ($key eq 'dublincore.source')
{
$meta{'url'} = $val;
}
elsif ($key eq 'dublincore.title')
{
$meta{'title'} = $val;
}
elsif ($key eq 'dublincore.creator')
{
$meta{'creator'} = $val;
}
elsif ($key eq 'dublincore.description')
{
$meta{'description'} = $val;
}
elsif ($key eq 'private')
{
# deal with this after tags
}
else
{
$meta{$key} = $val;
}
}
}
if ($info->{private})
{
$meta{tags} .= ",private";
}
# Check for wiki-specific meta-data in the "more" part
if ($more =~ m/\[\[\!meta title="([^"]+)"\]\]/)
{
$meta{title} = $1 if !$meta{title};
$more =~ s/\[\[\!meta title="([^"]+)"\]\]//;
}
if ($more =~ m/\[\[\!meta description="([^"]+)"\]\]/)
{
$meta{description} = $1 if !$meta{description};
$more =~ s/\[\[\!meta description="([^"]+)"\]\]//;
}
return \%meta;
} # read_meta
=head2 delete_field_from_file
Completely remove the given field.
This does no checking for multi-valued fields, it just deletes the whole thing.
$scribe->delete_field_from_file(filename=>$filename,field=>$field);
=cut
sub delete_field_from_file {
my $self = shift;
my %args = @_;
say STDERR whoami(), " filename=$args{filename}" if $self->{verbose} > 2;
my $filename = $args{filename};
my $field = $args{field};
my $info = $self->_load_meta($filename);
delete $info->{$field};
$self->_write_meta(filename=>$filename,meta=>$info);
} # delete_field_from_file
=head2 replace_all_meta
Overwrite the existing meta-data with that given.
(This supercedes the parent method because we can do it more efficiently this way)
$scribe->replace_all_meta(filename=>$filename,meta=>\%meta);
=cut
sub replace_all_meta {
my $self = shift;
my %args = @_;
say STDERR whoami(), " filename=$args{filename}" if $self->{verbose} > 2;
my $filename = $args{filename};
my $meta = $args{meta};
$self->_write_meta(filename=>$filename,meta=>$meta);
} # replace_all_meta
=head2 replace_one_field
Overwrite the given field. This does no checking.
$scribe->replace_one_field(filename=>$filename,field=>$field,value=>$value);
=cut
sub replace_one_field {
my $self = shift;
my %args = @_;
say STDERR whoami(), " filename=$args{filename}" if $self->{verbose} > 2;
my $filename = $args{filename};
my $field = $args{field};
my $value = $args{value};
my $info = $self->_load_meta($filename);
$info->{$field} = $value;
$self->_write_meta(filename=>$filename,meta=>$info);
} # replace_one_field
=head1 Helper Functions
Private interface.
=head2 _has_yaml
The file has YAML if the FIRST line is '---'
=cut
sub _has_yaml {
my $self = shift;
my $filename = shift;
my $fh;
if (!open($fh, '<', $filename))
{
die __PACKAGE__, " Unable to open file '" . $filename ."': $!\n";
}
my $first_line = <$fh>;
close($fh);
return 0 if !$first_line;
chomp $first_line;
return ($first_line eq '---');
} # _has_yaml
=head2 _yaml_and_more
Get the YAML part of the file (if any)
by reading the stuff between the first set of --- lines
and also the rest of the file as a separate part.
=cut
sub _yaml_and_more {
my $self = shift;
my $filename = shift;
say STDERR whoami(), " filename=$filename" if $self->{verbose} > 2;
my $fh;
if (!open($fh, '<', $filename))
{
die __PACKAGE__, " Unable to open file '" . $filename ."': $!\n";
}
my $yaml_str = '';
my $more_str = '';
my $yaml_started = 0;
my $yaml_finished = 0;
while (<$fh>) {
if (/^---$/) {
# There could be "---" lines after the YAML is finished!
if (!$yaml_started and !$yaml_finished)
{
$yaml_started = 1;
next;
}
elsif (!$yaml_finished) # end of the YAML part
{
$yaml_started = 0;
$yaml_finished = 1;
next;
}
}
if ($yaml_started)
{
$yaml_str .= $_;
}
elsif ($yaml_finished)
{
$more_str .= $_;
}
}
close($fh);
return ($yaml_str,$more_str);
} # _yaml_and_more
=head2 _load_meta
Quick non-checking loading of the meta-data. Does not standardize any fields.
=cut
sub _load_meta {
my $self = shift;
my $filename = shift;
say STDERR whoami(), " filename=$filename" if $self->{verbose} > 2;
my ($yaml_str,$more) = $self->_yaml_and_more($filename);
my $meta;
eval {$meta = Load($yaml_str);};
if ($@)
{
warn __PACKAGE__, " Load of data failed: $@";
return {};
}
if (!$meta)
{
warn __PACKAGE__, " no legal YAML";
return {};
}
return $meta;
} # _load_meta
=head2 _write_meta
Overwrites the file completely with the given metadata
plus the rest of its contents
This saves multi-value comma-separated fields as arrays.
=cut
sub _write_meta {
my $self = shift;
my %args = @_;
my $filename = $args{filename};
my $meta = $args{meta};
# restore multi-value comma-separated fields to arrays
foreach my $fn (keys %{$self->{wanted_fields}})
{
if ($self->{wanted_fields}->{$fn} eq 'MULTI'
and exists $meta->{$fn}
and defined $meta->{$fn}
and $meta->{$fn} =~ /,/)
{
my @vals = split(/,/, $meta->{$fn});
$meta->{$fn} = \@vals;
}
}
my ($yaml_str,$file_rest) = $self->_yaml_and_more($filename);
my $fh;
if (!open($fh, '>', $filename))
{
die __PACKAGE__, " Unable to open file '" . $filename ."': $!\n";
}
print $fh Dump($meta);
print $fh "---\n";
print $fh $file_rest;
close $fh;
} # _write_meta
=head1 BUGS
Please report any bugs or feature requests to the author.
=cut
1; # End of File::Sticker::Scribe
__END__