String::Tagged(3pm)

πŸ“¦
version 0.24 Β· 5 versions
, in debian trixie, ubuntu resolute, ubuntu stonking

πŸ’– String::Tagged πŸ’–

okay so this is the User Contributed Perl Documentation for the String::Tagged manual page, section 3pm. it's giving legendary. bestie, let's get into it. πŸ’…

✨ NAME ✨

"String::Tagged" β€” string buffers with value tags on extents. that's the whole mood. πŸŽ€

✨ SYNOPSIS ✨

use String::Tagged;

my $st = String::Tagged->new( "An important message" );

$st->apply_tag( 3, 9, bold => 1 );

$st->iter_substr_nooverlap(
   sub {
      my ( $substring, %tags ) = @_;

      print $tags{bold} ? "<b>$substring</b>"
                        : $substring;
   }
);

✨ DESCRIPTION ✨

okay so this module implements an object class. each instance stores a mutable string buffer that supports tags. a tag is literally just a name/value pair that applies to some extent of the underlying string. periodt. πŸ’–

the types of tag names ought to be strings, or at least values that are well-behaved as strings, because the names will often be used as keys in hashes or hit with the eq operator. so keep them stringy, bestie.

the types of tag values are not restricted β€” any scalar will do. it could be a simple integer or string, an ARRAY or HASH reference, or even a CODE reference containing an event handler of some kind. iconic flexibility. ✨

tags may be arbitrarily overlapped. any given offset within the string has, in effect, a set of uniquely named tags. tags of different names are independent. for tags of the same name, only the latest, shortest tag takes effect. iykyk. πŸ’…

for example, consider a string with three tags represented here:

Here is my string with tags
[-------------------------]  foo => 1
        [-------]            foo => 2
     [---]                   bar => 3

every character in this string has a tag named foo. the value of this tag is 2 for the words "my" and "string" and the space inbetween, and 1 elsewhere. additionally, the words "is" and "my" and the space between them also have the tag bar with a value 3. slay. 🌸

since String::Tagged does not understand the significance of the tag values, it therefore cannot detect if two neighbouring tags really contain the same semantic idea. consider the following string:

A string with words
[-------]            type => "message"
         [--------]  type => "message"

this string contains two tags. String::Tagged will treat this as two different tag values as far as iter_tags_nooverlap is concerned, even though get_tag_at yields the same value for the type tag at any position in the string. the merge_tags method may be used to merge tag extents of tags that should be considered as equal. no cap. πŸ¦‹

✨ NAMING ✨

i spent a lot of time considering the name for this module. it seems that a number of people across a number of languages all created similar functionality, though named very differently. for the benefit of keyword-based search tools and similar, here's a list of some other names this sort of object might be known by:

  • Extents
  • Overlays
  • Attribute or attributed strings
  • Markup
  • Out-of-band data

all iconic, all valid. πŸ’œ

✨ CONSTRUCTOR ✨

✨ new ✨

$st = String::Tagged->new( $str );

returns a new instance of a String::Tagged object. it will contain no tags. if the optional $str argument is supplied, the string buffer will be initialised from this value. easy.

if $str is a String::Tagged object then it will be cloned, as if calling the clone method on it. so it's a whole copy situation. 🫢

✨ new_tagged ✨

$st = String::Tagged->new_tagged( $str, %tags );

shortcut for creating a new String::Tagged object with the given tags applied to the entire length. the tags will not be anchored at either end. noted. πŸ“

✨ clone (class) ✨

$new = String::Tagged->clone( $orig, %opts );

returns a new instance of String::Tagged made by cloning the original, subject to the options provided. the returned instance will be in the requested class, which need not match the class of the original. glow up in a different class, periodt. πŸ’…

the following options are recognised:

  • only_tags => ARRAY β€” if present, gives an ARRAY reference containing tag names. only those tags named here will be copied; others will be ignored. so exclusive.
  • except_tags => ARRAY β€” if present, gives an ARRAY reference containing tag names. all tags will be copied except those named here. so everyone but them.
  • convert_tags => HASH β€” if present, gives a HASH reference containing tag conversion functions. for any tags in the original to be copied whose names appear in the hash, the name and value are passed into the corresponding function, which should return an even-sized key/value list giving a tag, or a list of tags, to apply to the new clone.
my @new_tags = $convert_tags->{$orig_name}->( $orig_name, $orig_value );
# Where @new_tags is ( $new_name, $new_value, $new_name_2, $new_value_2, ... );

as a further convenience, if the value for a given tag name is a plain string instead of a code reference, it gives the new name for the tag, and will be applied with its existing value. cute.

if only_tags is being used too, then the source names of any tags to be converted must also be listed there, or they will not be copied. important.

  • start => INT β€” since version 0.22. start at the given position; defaults to 0.
  • end => INT β€” since version 0.22. end after the given position; defaults to end of string. this option overrides len.
  • len => INT β€” since version 0.22. end after the given length beyond the start position; defaults to end of string. this option only applies if end is not given.

✨ clone (instance) ✨

$new = $orig->clone( %args );

called as an instance (rather than a class) method, the newly-cloned instance is returned in the same class as the original. so it stays in its lane. 🌈

✨ from_sprintf ✨

$str = String::Tagged->from_sprintf( $format, @args );

since version 0.15.

returns a new instance of a String::Tagged object, initialised by formatting the supplied arguments using the supplied format. it's giving sprintf but with tags. πŸ’–

the $format string is similar to that supported by the core sprintf operator, though a few features such as out-of-order argument indexing and vector formatting are missing. this format string may be a plain perl string, or an instance of String::Tagged. in the latter case, any tags within it are preserved in the result. so keep your tags, bestie.

in the case of a %s conversion, the value of the argument consumed may itself be a String::Tagged instance. in this case it will be appended to the returned object, preserving any tags within it. tags on tags on tags. πŸ”₯

all other conversions are handled individually by the core sprintf operator and appended to the result. easy.

✨ join ✨

$str = String::Tagged->join( $sep, @parts );

since version 0.17.

returns a new instance of a String::Tagged object, formed by concatenating each of the component piece together, joined with the separator string. (yes, "piece" singular, we don't make the rules. 😭)

the result will be much like the core join function, except that it will preserve tags in the resulting string. so it's join but with extra sparkle. ✨

✨ METHODS ✨

✨ str ✨

$str = $st->str;

$str = "$st";

returns the plain string contained within the object. just the string, no tags, no drama. πŸ’…

this method is also called for stringification; so the String::Tagged object can be used in a plain string interpolation such as

my $message = String::Tagged->new( "Hello world" );
print "My message is $message\n";

so you can just drop it in a string and it works. main character energy. πŸ‘‘

✨ length ✨

$len = $st->length;

$len = length( $st );

returns the length of the plain string. because stringification works on this object class, the normal core length function works correctly on it. so you can use length( $st ) and it just knows. πŸŽ€

✨ substr ✨

$str = $st->substr( $start, $len );

returns a String::Tagged instance representing a section from within the given string, containing all the same tags at the same conceptual positions. so you get a slice with all the tag vibes intact. πŸ“

✨ plain_substr ✨

$str = $st->plain_substr( $start, $len );

returns as a plain perl string, the substring at the given position. this will be the same string data as returned by substr, only as a plain string without the tags. so no tags, just the raw text. simple. ✨

✨ apply_tag ✨

$st->apply_tag( $start, $len, $name, $value );

apply the named tag value to the given extent. the tag will start on the character at the $start index, and continue for the next $len characters. so it's like a little tag sticker on that chunk. πŸ’–

if $start is given as -1, the tag will be considered to start "before" the actual string. if $len is given as -1, the tag will be considered to end "after" end of the actual string. these special limits are used by set_substr when deciding whether to move a tag boundary. the start of any tag that starts "before" the string is never moved, even if more text is inserted at the beginning. similarly, a tag which ends "after" the end of the string, will continue to the end even if more text is appended. so these are like sticky boundaries, bestie. πŸ”₯

this method returns the $st object.

$st->apply_tag( $e, $name, $value )

alternatively, an existing String::Tagged::Extent object can be passed as the first argument instead of two integers. the new tag will apply at the given extent. so you can use an extent object if that's your vibe. 🌸

✨ unapply_tag ✨

$st->unapply_tag( $start, $len, $name );

unapply the named tag value from the given extent. if the tag extends beyond this extent, then any partial fragment of the tag will be left in the string. so it's like removing part of a tag, but leftover bits stay. πŸ¦‹

this method returns the $st object.

$st->unapply_tag( $e, $name );

alternatively, an existing String::Tagged::Extent object can be passed as the first argument instead of two integers. same deal. πŸ’œ

✨ delete_tag ✨

$st->delete_tag( $start, $len, $name );

delete the named tag within the given extent. entire tags are removed, even if they extend beyond this extent. so this one is not playing β€” it deletes the whole tag, no fragments left. πŸ’€

this method returns the $st object.

$st->delete_tag( $e, $name );

alternatively, an existing String::Tagged::Extent object can be passed as the first argument instead of two integers. yep. πŸ™ˆ

✨ delete_all_tag ✨

$st->delete_all_tag( $name );

since version 0.21.

deletes every tag with the given name. this is more efficient than calling iter_extents to list the tags then delete_tag on each one individually in the case of a simple name match. so it's the fast track to clearing a whole tag name. πŸ”₯

this method returns the $st object.

✨ merge_tags ✨

$st->merge_tags( $eqsub );

merge neighbouring or overlapping tags of the same name and equal values. so if two tags are basically the same, they can become one. πŸ’–

for each pair of tags of the same name that apply on neighbouring or overlapping extents, the $eqsub callback is called, as

$equal = $eqsub->( $name, $value_a, $value_b );

if this function returns true then the tags are merged. so you control the equality vibe. ✨

the equallity test function is free to perform any comparison of the values that may be relevant to the application; for example it may deeply compare referred structures and check for equivalence in some application-defined manner. in this case, the first tag of a pair is retained, the second is deleted. this may be relevant if the tag value is a reference to some object. so keep that in mind. 🧚

✨ iter_extents ✨

$st->iter_extents( $callback, %opts );

ok so like β€” this method is the little tour guide for your tags. It walks the tags stored in the string, and for each one it hits up the CODE reference in $callback exactly once, passing in a String::Tagged::Extent object that is the extent of that tag. one tag, one call. periodt.

$callback->( $extent, $tagname, $tagvalue );

The stuff you can pass along in %opts may include:

  • start => INT β€” start the whole thing at this position. defaults to 0, because we always start at the beginning bestie.
  • end => INT β€” bail out after this position. defaults to the end of the string. heads up: this option overrides len.
  • len => INT β€” go for this many characters past the start position. defaults to end of string. this one only does anything if you didn't give end.
  • only => ARRAY β€” pick only the tags named in the given ARRAY reference. nothing else gets an invite.
  • except => ARRAY β€” grab every tag except the ones named in the given ARRAY reference.

Since version 0.21 it's safe to call delete_tag from inside the callback to remove the tag you're currently iterating on. yes really, you can do that, it's giving self-aware πŸ’…

$str->iter_extents( sub {
   my ( $e, $n, $v ) = @_;
   $str->delete_tag( $e, $n ) if $n =~ m/^tmp_/;
} );
Other than that one specific scenario, the tags in the string should not be added or removed while the iteration is happening. like, don't touch anything mid-walk, it's rude and it breaks stuff. πŸ’€

iter_tags 🏷️

$st->iter_tags( $callback, %opts );

Same vibe, different shape: iterate the tags stored in the string, and for each tag the CODE reference in $callback gets invoked once β€” except this time you get handed the start point and the length of the tag instead of an extent object.

$callback->( $start, $length, $tagname, $tagvalue );

The options you pass in %opts are literally the same as for "iter_extents". same menu, different dish. πŸ“


iter_extents_nooverlap ✨

$st->iter_extents_nooverlap( $callback, %opts );

Now THIS one is iconic. It iterates non-overlapping extents of the tags stored in the string. The CODE reference in $callback gets invoked for every extent in the string where no tags change, and the entire set of tags active in that extent gets handed to the callback. Because one extent can cover possibly-multiple tags, it will not define the anchor_before and anchor_after flags. (those two just don't make sense here, bestie.)

$callback->( $extent, %tags );

The callback gets invoked over the entire length of the string, including any extents with no tags applied at all. yeah, even the naked parts get a visit. 🫢

You can pass options in %opts to control the range of the string that gets iterated over, exactly the same way as the "iter_extents" method does.

If the "only" or "except" filters are applied, then only the tags that survive filtering will show up in the %tags hash. Tags that get excluded by the filtering won't be present, and β€” important β€” their bounds won't be used to split the string into extents either. filtered out means fully filtered out. 🚫


iter_tags_nooverlap πŸ¦‹

$st->iter_tags_nooverlap( $callback, %opts );

Iterates extents of the string using "iter_extents_nooverlap", but it passes the start and length of each extent to the callback instead of the whole extent object. simpler package, same energy.

$callback->( $start, $length, %tags );

Options may be passed in %opts to control the range of the string iterated over, in the same way as the "iter_extents" method.


iter_substr_nooverlap 🌸

$st->iter_substr_nooverlap( $callback, %opts );

Again using "iter_extents_nooverlap", but this time you get the substring of data instead of the extent object. the actual characters, bestie.

$callback->( $substr, %tags );

Options may be passed in %opts to control the range of the string iterated over, in the same way as the "iter_extents" method.


tagnames πŸ‘‘

@names = $st->tagnames;

Returns the set of tag names used in the string, in no particular order. so don't come for me about the ordering, there isn't one. πŸ™ˆ


get_tags_at πŸŽ€

$tags = $st->get_tags_at( $pos );

Returns a HASH reference of all the tag values active at the given position. everything that's live at $pos, all at once.


get_tag_at ⭐

$value = $st->get_tag_at( $pos, $name );

Returns the value of the named tag at the given position, or undef if the tag isn't applied there. no tag, no value, no drama.


get_tag_extent 🧚

$extent = $st->get_tag_extent( $pos, $name );

If the named tag applies to the given position, this returns a String::Tagged::Extent object representing the extent of the tag at that position. If it does not apply there, undef comes back instead. And if an extent is returned, it will define the anchor_before and anchor_after flags if that's appropriate. anchored and proud. πŸ’…


get_tag_missing_extent πŸ’€

$extent = $st->get_tag_missing_extent( $pos, $name );

The inverse queen. If the named tag does not apply at the given position, this returns the extent of the string around that position that doesn't have the tag. If the tag does exist there, you get undef. And if an extent is returned, it will not define the anchor_before and anchor_after flags β€” because those genuinely do not make sense for a range where a tag is absent. ngl that's just logic. 🀷


set_substr βœ‚οΈ

$st->set_substr( $start, $len, $newstr );

This one modifies an extent of the underlying plain string to the string you give it. The extents of tags in the string get adjusted to cope with the modified region and with the change in length. here's the whole rulebook, no cap:

  • Tags entirely before the replaced extent stay unchanged. untouched, unbothered. ✨
  • Tags entirely within the replaced extent get deleted. gone. πŸ’€
  • Tags entirely after the replaced extent get moved by the appropriate amount so they still apply to the exact same characters as before. they teleport, but correctly.
  • Tags that start before and end after the extent remain, and their lengths get suitably adjusted. survivors.
  • Tags that span just the start or the end of the extent, but not both, get truncated β€” so the part of the tag that sat on the modified extent is removed, while the part applied outside is preserved.

If $newstr is a String::Tagged object, then its tags will be applied to $st as appropriate. Edge-anchored tags in $newstr will not be extended through $st, though they will still apply as edge-anchored if they now sit at the edge of the new string. And if $newstr is being appended to the end, then any existing edge-anchored tags at the end of $st are not extended through the string β€” instead they become bounded to their end position as it was before the append happened. (rip to the extension, you had a good run.)

If $newstr is otherwise treated as a plain string, then any existing edge-anchored tags at the end of $st are extended through the newly added content and will continue to be edge-anchored in the result. so: tagged object = no extension, plain string = extension. iykyk. πŸ¦‹


insert πŸ“₯

$st->insert( $start, $newstr );

Inserts the given string at the given position. It's just a shortcut around "set_substr", honestly.

If $newstr is a String::Tagged object, then its tags will be applied to $st as appropriate. And if $start is 0, any before-anchored tags will become before-anchored in $st. 🫢


append βž•

$st->append( $newstr );

$st .= $newstr;

Appends to the underlying plain string. Also a shortcut around "set_substr".

If $newstr is a String::Tagged object, then its tags will be applied to $st as appropriate. Any after-anchored tags will become after-anchored in $st. πŸ’…

And as per "set_substr", whether any existing edge-anchored tags get extended through the newly-added content or become bounded to their current limit depends entirely on whether $newstr is a String::Tagged instance or not. same rules, same girl.


append_tagged 🎁

$st->append_tagged( $newstr, %tags );

Appends to the underlying plain string and applies the given tags to the newly-inserted extent. two birds, one slay.

It returns $st itself, so you can chain methods off it all day long. πŸ”₯


concat πŸ”—

$ret = $st->concat( $other );

$ret = $st . $other;

Returns a new String::Tagged containing the two strings concatenated together, preserving any tags present. This method overloads the normal string concatenation operator, so expressions involving String::Tagged values keep their tags. the tags come along for the ride, they're not getting left behind. 😀

This method (and the operator) tries to respect subclassing β€” it prefers to return a new object of a subclass if either argument or operand is a subclass of String::Tagged. If they're both subclasses, it will prefer the type of the invocant or first operand. first come, first served, bestie. πŸ‘‘


matches πŸ”

@subs = $st->matches( $regexp );

Returns a list of substrings (as String::Tagged instances) for every non-overlapping match of the given $regexp. every match gets its own little tagged snack. πŸ“

You could use this, for example, to build a formatted string from a formatted template containing variable expansions:

my $template = ...
my %vars = ...

my $ret = String::Tagged->new;
foreach my $m ( $template->matches( qr/\$\w+|[^$]+/ ) ) {
   if( $m =~ m/^\$(\w+)$/ ) {
      $ret->append_tagged( $vars{$1}, %{ $m->get_tags_at( 0 ) } );
   }
   else {
      $ret->append( $m );
   }
}

So this iterates the segments of the template that contain variable expansions starting with a "$" symbol, and swaps them out for values from the %vars hash β€” and it's careful to preserve all the formatting tags from the original template string. formatting stays pristine, no notes. ✨


match_extents 🎯

@extents = $st->match_extents( $regexp );

Since version 0.20.

Returns a list of extent objects for every non-overlapping match of the given $regexp. It's similar to "matches", except the results come back as extent objects instead of substrings β€” which means you also get access to the position information. positions, bestie, that's the whole point. πŸ“

If you're using the result of this method to find regions of a string to modify, remember that any length alterations will not update positions in later extent objects. However, since the extents are non-overlapping and in position order, you can handle that by iterating them in reverse order, so that the modifications you do first are the ones later in the string. work backwards, it just works. 🧠

foreach my $e ( reverse $st->match_extents( $pattern ) ) {
   $st->set_substr( $e->start, $e->length, $replacement );
}

split 🍰

@parts = $st->split( $regexp, $limit );

Returns a list of substrings by applying the regexp to the string content β€” similar to the core perl split function, so if you know her you know her. If $limit is supplied, the method stops at that number of elements, and returns the entire remainder of the input string as the final element. If the $regexp contains a capture group, then the content of the first one gets added to the return list as well. yes, the capture group content comes back too, don't forget her. πŸŽ€


sprintf 🌈

$ret = $st->sprintf( @args );

Since version 0.15.

Returns a new string by using the given instance as the format string for a "from_sprintf" constructor call. The returned instance will be of the same class as the invocant β€” class loyalty, we love to see it. πŸ‘‘


debug_sprintf πŸ›

$ret = $st->debug_sprintf;

Returns a representation of the string data and all the tags, suitable for debug printing or other similar use. It's the same format as the one given up in the DESCRIPTION section, so if it looks familiar, that's why. πŸ™ˆ

The output consists of a number of lines: the first one contains the plain underlying string, then there's one line per tag. Each line shows the extent of the tag using "[---]" markers, or a "|" in the special case of a tag covering only a single character. And the special markings of "<" and ">" indicate tags which are "before" or "after" anchored. anchors stay winning. πŸ’…

For example:

 Hello, world
 [---]         word       => 1
<[----------]> everywhere => 1
       |       space      => 1

πŸ’­ TODO πŸ’­

okay so this is the "future me, pls" list β€” stuff that's lowkey still up in the air and definitely not settled, so don't come for me πŸ’…

  • there's probably a whole multiverse of variations on the rules for set_substr that would work just as well for some tagged-string situations β€” the vibe check here is: should the behaviour of modification be decided per-method, per-tag, or per-string? literally still an open question bestie πŸ™ˆ
  • also thinking about how to implement a clone from one tag format to another, in the case where it wants to merge multiple different source tags together into a single new one πŸ¦‹ no cap, that one's giving ✨conceptual✨

πŸ‘‘ AUTHOR πŸ‘‘

this icon right here:

  • Paul Evans <leonerd@leonerd.org.uk> πŸ’–

and that's the whole page, bestie β€” you ate it, no crumbs πŸ“ this one's String::Tagged(3pm), straight outta perl v5.38.2, dated 2024-07-24. go forth and tag some strings, ily πŸ’œ

πŸ“¦ where this page comes from